이 내용은 Controlling IBus with DBusSharp (by hindlet)를 참고했다. 훌륭한 글을 쓴 저자에게 감사드린다. (Thanks for great article !!!)

Java에서 한글입력기의 한/영 상태를 바꾸는 기능이 필요했는데, 이것 저것 알아보던 중 IBus 를 사용하면 D-Bus 를 이용하여 한/영 상태를 바꾸는게 가능하다는 것을 알아냈다.

먼저 IBus를 설치하여 적절하게 설정한다. 그 다음 D-Bus Java 바인딩을 받아서 설치한다. (이 과정은 알아서...)

이제 코딩을 시작해야 하는데 먼저 org.freedesktop.dbus.DBusConnection 객체를 얻어야 한다. 그러려면 IBus 데몬의 주소를 알야야 하는데 $HOME/.config/ibus/bus 디렉토리 아래에 파일이 하나 있을 것이다. 이 파일을 열어 IBUS_ADDRESS값을 읽으면 된다. 그냥 프로퍼티 파일이므로 java.util.Properties 로 읽으면 된다. 이렇게 읽은 주소를 이용하여 org.freedesktop.dbus.DBusConnection 객체를 얻는다.

DBusConnection conn = DBusConnection.getConnection("unix:abstract=/tmp/dbus-wSEDsZTDyV,guid=aaaaaaaaaaaaaaaaaaaaaaaaaaaa") ;

이 커넥션으로 이제 IBus 객체를 얻어올 수 있는데, IBus의 이름과 패스를 알아야 한다. 자세히는 모르겠지만 패스는 D-Bus시스템 상에서 IBus가 가지는 가상의 주소인 것 같다. IBus 의 이름은 org.freedesktop.IBus , 패스는 /org/freedesktop/IBus이다. 이것을 이용하여 IBus를 나타내는 자바 객체를 얻을 수 있다.

??? ibus = (???)conn.getRemoteObject("org.freedesktop.IBus", "/org/freedesktop/IBus") ;

타입명 부분에 ???를 써 놓았다. 저 자리에 무엇을 넣어야 하나? 정답은 해당 리모트 객체가 구현한 인터페이스를 직접 선언하여 사용해야 한다는 것이다. 그럼 그 인터페이스는 어떻게 알 수 있나? 이런 경우를 위해 D-Bus에는 인트로스펙션 API가 있는데 IBus역시 이 기능을 이용하여 어떤 인터페이스를 구현했는지 정보를 제공한다. 다음처럼 하면 XML 형태로 정보를 볼 수 있다.

Introspectable ibus_intro = (Introspectable)conn.getRemoteObject("org.freedesktop.IBus", "/org/freedesktop/IBus" , Introspectable.class) ;
System.out.println(ibus_intro.Introspect());

org.freedesktop.DBus.Introspectable을 이용하여 필요한 정보를 볼 수 있다. 위 코드를 실행하면 아래와 같은 결과가 나온다.

<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
  <interface name="org.freedesktop.DBus.Introspectable">
    <method name="Introspect">
      <arg name="data" direction="out" type="s"/>
    </method>
  </interface>
  <interface name="org.freedesktop.IBus">
    <method name="GetAddress">
      <arg name="address" direction="out" type="s"/>
    </method>
    <method name="CreateInputContext">
      <arg name="name" direction="in" type="s"/>
      <arg name="context" direction="out" type="o"/>
    </method>
    <method name="CurrentInputContext">
      <arg name="name" direction="out" type="s"/>
    </method>
    <method name="RegisterComponent">
      <arg name="components" direction="in" type="v"/>
    </method>
    <method name="ListEngines">
      <arg name="engines" direction="out" type="av"/>
    </method>
    <method name="ListActiveEngines">
      <arg name="engines" direction="out" type="av"/>
    </method>
    <method name="Exit">
      <arg name="restart" direction="in" type="b"/>
    </method>
    <method name="Ping">
      <arg name="data" direction="in" type="v"/>
      <arg name="data" direction="out" type="v"/>
    </method>
    <signal name="RegistryChanged">
    </signal>
  </interface>
</node>

이 정보를 토대로 정의해야하는 인터페이스는 org.freedesktop.IBus이며 GetAddress등 여러 메소드가 있다는 것을 알 수 있다. 인터페이스의 메소드를 모두 선언할 필요는 없고 필요한 것만 선언하면 된다. 여기서는 현재의 입력컨텍스트 정보를 얻는 CurrentInputContext 메소드만 있으면 되므로 인터페이스를 다음처럼 선언한다.

package org.freedesktop;
import org.freedesktop.dbus.DBusInterface;
// 모든 DBus 인터페이스는 DBusInterface를 상속받아야 한다.
public interface IBus extends DBusInterface {
    public String CurrentInputContext();
}

이 인터페이스를 이용하여 현재 입력컨텍스트 정보를 얻으면 되는데, CurrentInputContext 메소드를 호출하면 현재 입력컨텍스트의 패스가 나온다. 이 패스를 이용하여 입력컨텍스트 객체를 얻으면 된다.

IBus ibus = (IBus)conn.getRemoteObject("org.freedesktop.IBus", "/org/freedesktop/IBus") ;
String context_path = ibus.CurrentInputContext() ;
??? context = (???) conn.getRemoteObject("org.freedesktop.IBus", context_path) ;

또 다시 물음표가 등장했다. 저기에는 무슨 타입을 써야하나? 이번에도 입력컨텍스트 패스에 인트로스펙션 API를 적용해보면 알 수 있다.

String context_path = ibus.CurrentInputContext() ;
        
Introspectable ctx_intro = (Introspectable) conn.getRemoteObject("org.freedesktop.IBus", context_path , Introspectable.class) ;
System.out.println(ctx_intro.Introspect());
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
  <interface name="org.freedesktop.DBus.Introspectable">
    <method name="Introspect">
      <arg name="data" direction="out" type="s"/>
    </method>
  </interface>
  <interface name="org.freedesktop.IBus.InputContext">
    <method name="ProcessKeyEvent">
      <arg name="keyval" direction="in" type="u"/>
      <arg name="keycode" direction="in" type="u"/>
      <arg name="state" direction="in" type="u"/>
      <arg name="handled" direction="out" type="b"/>
    </method>
    <method name="SetCursorLocation">
      <arg name="x" direction="in" type="i"/>
      <arg name="y" direction="in" type="i"/>
      <arg name="w" direction="in" type="i"/>
      <arg name="h" direction="in" type="i"/>
    </method>
    <method name="FocusIn"/>
    <method name="FocusOut"/>
    <method name="Reset"/>
    <method name="Enable"/>
    <method name="Disable"/>
    <method name="IsEnabled">
      <arg name="enable" direction="out" type="b"/>
    </method>
    <method name="SetCapabilities">
      <arg name="caps" direction="in" type="u"/>
    </method>
    <method name="SetEngine">
      <arg name="name" direction="in" type="s"/>
    </method>
    <method name="GetEngine">
      <arg name="desc" direction="out" type="v"/>
    </method>
    <method name="Destroy"/>
    <signal name="CommitText">
      <arg name="text" type="v"/>
    </signal>
    <signal name="Enabled"/>
    <signal name="Disabled"/>
    <signal name="ForwardKeyEvent">
      <arg name="keyval" type="u"/>
      <arg name="keycode" type="u"/>
      <arg name="state" type="u"/>
    </signal>
    <signal name="UpdatePreeditText">
      <arg name="text" type="v"/>
      <arg name="cursor_pos" type="u"/>
      <arg name="visible" type="b"/>
    </signal>
    <signal name="ShowPreeditText"/>
    <signal name="HidePreeditText"/>
    <signal name="UpdateAuxiliaryText">
      <arg name="text" type="v"/>
      <arg name="visible" type="b"/>
    </signal>
    <signal name="ShowAuxiliaryText"/>
    <signal name="HideAuxiliaryText"/>
    <signal name="UpdateLookupTable">
      <arg name="table" type="v"/>
      <arg name="visible" type="b"/>
    </signal>
    <signal name="ShowLookupTable"/>
    <signal name="HideLookupTable"/>
    <signal name="PageUpLookupTable"/>
    <signal name="PageDownLookupTable"/>
    <signal name="CursorUpLookupTable"/>
    <signal name="CursorDownLookupTable"/>
    <signal name="RegisterProperties">
      <arg name="props" type="v"/>
    </signal>
    <signal name="UpdateProperty">
      <arg name="prop" type="v"/>
    </signal>
  </interface>
</node>

정말 많은 메소드가 있지만 필요한 것은 현재 한글입력 상태인지 확인하는 IsEnabled , 한글입력 상태로 바꾸는 Enable, 영어입력 상태로 바꾸는 Disable 뿐이다. 인터페이스 이름이 org.freedesktop.IBus.InputContext이므로 위에서 만든 IBus 인터페이스 안에 정의해야 한다.

package org.freedesktop;
import org.freedesktop.dbus.DBusInterface;
// 모든 DBus 인터페이스는 DBusInterface를 상속받아야 한다.
public interface IBus extends DBusInterface {
    public String CurrentInputContext();
    
    public static interface InputContext extends DBusInterface {
        public boolean IsEnabled() ;
        public void Enable() ;
        public void Disable() ;
    }
}

이제 복잡한 것은 다 끝났다. 필요한 메소드를 차례대로 호출하면 된다. 시험삼아 SWT 프로그램을 만들었다. 텍스트 입력창에서 쉬프트키를 누르면 한영상태가 바뀐다. (Swing은 왠지 IBus와 붙지않는 듯하여 테스트하지 못했다. 뭔가 방법이 있겠지만 귀찮아서...)

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.freedesktop.IBus;
import org.freedesktop.IBus.InputContext;
import org.freedesktop.dbus.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
public class IBusTester {
    private static final String ADDR =
        "unix:abstract=/tmp/dbus-wSEDsZTDyV,guid=aaaaaaaaaaaaaaaaaaaaaaaaaaaa" ;
    
    public static void toggleIBus() throws DBusException {
        DBusConnection conn = DBusConnection.getConnection(ADDR) ;
        IBus ibus = (IBus)conn.getRemoteObject("org.freedesktop.IBus", "/org/freedesktop/IBus") ;
        String context_path = ibus.CurrentInputContext() ;
        InputContext context = (InputContext) conn.getRemoteObject("org.freedesktop.IBus", context_path) ;
        
        if(context.IsEnabled()) {
            context.Disable() ;
        }
        else {
            context.Enable() ;
        }
        conn.disconnect() ;
    }
    
    public static void main (String [] args) {
        Display display = new Display ();
        Shell shell = new Shell (display);
        shell.setLayout( new RowLayout() ) ;
        Text text = new Text (shell, SWT.BORDER);
        text.setSize(200, 50) ;
        
        text.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                if (e.keyCode == SWT.SHIFT) {
                    try {
                        toggleIBus() ;
                    }
                    catch (Throwable e1) {
                        e1.printStackTrace();
                    }
                }
            }
        }) ;
        
        
        shell.pack ();
        shell.open ();
        while (!shell.isDisposed ()) {
            if (!display.readAndDispatch ()) display.sleep ();
        }
        display.dispose ();
    }
}

몇가지 주의사항을 언급하자면

  • IBUS_ADDRESS 값은 IBus데몬이 새로 시작할 때 마다 바뀐다. 실제 코드에는 저렇게 하드 코딩하지 말고 파일을 읽어 처리하도록 수정해야 한다.
  • 커넥션을 연 후에는 필요없으면 닫아야 한다.
  • 커넥션을 닫은 후에는 해당 커넥션에서 얻어놓은 객체는 정상동작하지 않는다.
  • 텍스트 입력영역에 포커스가 있는 상태가 아닌데 IBus.CurrentInputContext()를 호출하면 예외가 발생한다.
  • 일단 InputContext 객체를 얻은 후에는 커넥션을 닫지 않는한 텍스트 입력영역에 포커스가 있지 않아도 입력상태를 바꾸는게 가능하다. 하지만 시스템 트레이에 상태가 바로 보이지는 않으며 텍스트 입력영역에 다시 포커스를 맞추면 최종 상태가 표시된다.
Posted by lispholic
,
Clean Code박재호님 번역으로 곧 출간된다고 한다. 이미 책 스택(큐 아님)에 읽지 않은 책이 쌓여있고, 오늘도 몇 권 더 쌓았지만, 출간되면 바로 사서 읽어보게 될 것 같다.
Posted by lispholic
,

Effective Java 2nd Edition

Java 2009. 9. 20. 22:05
Effective Java 2nd Edition 의 번역판이다. 첫번째 판에 java5 이후에 대한 내용 (Generic , Enum , Annotation 등) 이 추가되었다.  첫 판에서 객체지향 Enum 패턴을 소개했었는데, 2판에서는 Enum을 써라 라고 내용이 바뀐 것을 보니 감회가 새롭다.

번역은 전반적으로 잘 된 편이다. 다만 마음에 안드는게 하나 있는데, Reflexive를 "재귀"라고 번역한 것이다. 재귀라는 말을 들으면 누구나 Recursive를 떠올리지 Reflexive를 생각하지는 않을 것이다. 이것 때문에 읽는데 약간 고생했다. 이렇게 번역한 것에 뭔가 심오한 뜻이 있는지도 모르겠다. (하지만 번역한 분에게 물어볼 방법이 없으니 확인 불가)

Java 프로그래머라면 꼭 읽어봐야 할 것이다.

책 정보 보러가기








Posted by lispholic
,

junit 4.4 부터 hamcrest를 이용하여 좀 더 읽기 좋은 assertion 코드를 쓸 수 있게 되었다.

    int a = 2 + 3 ;
    assertThat( a , is(5) ) ;

다만 에러메시지를 따로 지정할 때 쓰는 방식은 별로 마음에 들지 않는다.

    assertThat("error message", a , is(5) ) ;

에러 메시지를 첫번째 매개변수로 받게 되어있는데 이것 때문에 코드의 가독성이 떨어진다. 에러 메시지가 없으면 일반 영어 문장처럼 읽을 수 있는데 에러 메시지 때문에 그렇게 할 수 없다. 에러 메시지를 세번째 매개변수로 하면 훨씬 나아진다.

    assertThat(a , is(5) ,
               "error message") ;

좀더 욕심을 내자면 slf4j에서 하는 것처럼 에러 메시지 포매팅도 지원하면 좋을 것 같다. (slf4j에서 코드를 좀 복사해서 이런 걸 만들어 사용하는 중이다.)

    assertThat(a , is(5) ,
               "{} should be {}", "a" , 5) ;
Posted by lispholic
,

얼마전에 Eclipse RCP 프로그램에서 Console 뷰 사용하기 라는 글을 썼는데, 몇가지 내용을 추가한다.

출력별로 색상지정하기

stdout 과 stderr 의 색을 다르게 지정하려면 그냥 MessageConsole 에서 MessageConsoleStream 을 두 개 만들어 각각 색을 지정하면 된다.

두번째 이후에 콘솔이 동작하지 않는 문제 해결하기

org.eclipse.ui.application.WorkbenchAdvisor 를 상속받은 클래스의 initialize(IWorkbenchConfigurer configurer) 메소드를 오버라이드하여 프로그램 상태를 저장하게 만든 경우 프로그램을 종료했다가 다시 띄우면 출력이 Console 뷰에 나타나지 않는다.

IPerspectiveFactory.createInitialLayout(IPageLayout layout) 메소드는 퍼스펙티브가 맨처음 떴을 때 호출되는데 , 설정을 저장한 이후부터는 이 메소드를 호출하지 않고 저장한 설정을 읽어서 퍼스펙티브를 초기화하기 때문이다.

이 문제를 해결하려면 MessageConsole 을 초기화하는 부분을 다른 곳으로 옮기면 된다. 나는 org.eclipse.ui.application.WorkbenchWindowAdvisor 를 상속받은 클래스의 postWindowOpen() 메소드 안으로 옮겼다. 이 메소드가 호출된 시점에서는 Display가 생성되어있으므로 이를 이용해 Console 출력의 색을 지정할 수 있다.

마지막으로 postWindowClose()를 오버라이드하여 윈도우가 닫힐 때 표준출력과 표준에러를 원래대로 되돌려 놓는다.

    @Override
    public void postWindowOpen() {
        super.postWindowOpen();
        MessageConsole console = new MessageConsole("Console", null, true);
        sys_out = System.out ;
        sys_err = System.err ;
        
        MessageConsoleStream new_out = console.newMessageStream();
        // 표준 출력은 파란색으로
        new_out.setColor(Display.getCurrent().getSystemColor(SWT.COLOR_BLUE));
        System.setOut(new PrintStream(new_out));
        MessageConsoleStream new_err = console.newMessageStream();
        // 표준 에러는 붉은색으로
        new_err.setColor(Display.getCurrent().getSystemColor(SWT.COLOR_RED));
        System.setErr(new PrintStream(new_err));
        ConsolePlugin.getDefault().getConsoleManager()
                .addConsoles(new IConsole[] { console });
    }
    @Override
    public void postWindowClose() {
        System.setOut(sys_out) ;
        System.setErr(sys_err) ;
        super.postWindowClose();
    }
Posted by lispholic
,