'iBus'에 해당되는 글 1건

  1. 2009.12.31 Java에서 IBus 한글입력 상태 바꾸기

이 내용은 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
,