이 내용은 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
객체를 얻은 후에는 커넥션을 닫지 않는한 텍스트 입력영역에 포커스가 있지 않아도 입력상태를 바꾸는게 가능하다. 하지만 시스템 트레이에 상태가 바로 보이지는 않으며 텍스트 입력영역에 다시 포커스를 맞추면 최종 상태가 표시된다.