Linux上のJavaアプリでいわゆる「IMEをON」にする方法

結論から書きますと、今回紹介する方法で強引にONにはできますが、Javaのプログラム上で一般的に行う方法は無いようです。

きっかけ

Javaで日本語入力を伴う業務アプリを作ると、かならずIMの制御が出てきます。Windowsだと以下のような方法で「たまたま」可能です。(Vista以降は不明)

なぜ「たまたま」なのかは以下の文章やドキュメントが詳しいです。

Java IMFでは「IMを On/Offする」という概念はありません。
(中略)
言語を日本語に切り替えれば日本語IMがOnになるし、中国語に切り替えれば中国語IMがOnになるという考え方です。
(中略)
On/Off の概念のないJava IMF の仕組みの下で、Windowsの元々持っているOn/Off の仕組みが動いてしまっていると言うことです。

一方のMacは微妙らしいです。

そして、肝心のLinux(少なくともIIIMのATOK)では、上の方法は期待した結果になりません。しかももっと悪いことに、入力フィールドを移るごとに強制的にIMがOFFになります。WindowsだとウインドウごとにIMの状態が維持されていたような気がします。

どんなときに困るかというと、入力フィールドがたくさん(何十個とかそれ以上)あるときに、それぞれでIMのONをする必要があり、それが死ぬるほどめんどくさいのです。具体的には FreeMind 上で項目を入力するごとにIMをONにしなければならず、この動きが毎回とてつもなく思考を妨げるため、わざわざVMWareWindows を立ち上げて FreeMind を使うほどでした。

調査と失敗

ということで、Windowsの実装がたまたまそうなっているのなら、Linuxの実装もそうできるかも知れないと思い、JTextFieldから掘って調べてみました。

ソースコードからネイティブ一歩手前のコードまで探して、以下のようなコードでprivate変数まで調べてみましたが、捜し物はみつかりませんでした。

private void reportIC() {
    System.out.println("IC:"+textfield.getInputContext());
    if (textfield.getInputContext() != null) {
        sun.awt.im.InputContext icc = (sun.awt.im.InputContext)textfield.getInputContext();
        try {
            System.out.println("-------");
            Method met = icc.getClass().getSuperclass().getDeclaredMethod("getInputMethod",null);
            met.setAccessible(true);
            sun.awt.X11.XInputMethod im = (sun.awt.X11.XInputMethod)met.invoke(icc,null);
            String[] fields = {"clientComponentWindow",
            "awtFocussedComponent",
            "lastXICFocussedComponent",
            "isLastXICActive",
            "isActive",
            "isActiveClient",
            "highlightStyles",
            "disposed",
            "needResetXIC",
            "needResetXICClient",
            "compositionEnableSupported",
            "savedCompositionState",
            "committedText",
            "composedText",
            "rawFeedbacks",
            "pData",};
            for(int i=0;i<fields.length;i++) {
            	try {
                	java.lang.reflect.Field fs = X11InputMethod.class.getDeclaredField(fields[i]);
                	fs.setAccessible(true);
                	System.out.println("IC:"+fs.getName()+" / "+fs.get(im));
            	} catch (Exception ee) {
            		System.out.println("IC:"+fields[i]+" / "+ee.getMessage());
            	}
            }
            String[] fields2 = {"xicFocus"};
            for(int i=0;i<fields2.length;i++) {
            	try {
                	java.lang.reflect.Field fs = XInputMethod.class.getDeclaredField(fields2[i]);
                	fs.setAccessible(true);
                	System.out.println("IC:"+fs.getName()+" / "+fs.get(im));
            	} catch (Exception ee) {
            		System.out.println("IC:"+fields[i]+" / "+ee.getMessage());
            	}
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

ちなみに今回のようなGUIのイベント系の調査の場合、 Eclipse のデバッガでは フォーカスが移るなどのイベントが大量に発生するのでクラスの状態を観察しづらいです。こういうときは各地にログを入れてタイミングや順番を観察したり、直接private変数にアクセスする方法が有効です。

イベントの伝搬を調査してみるも、IMのON/OFFはJavaには流れてきてないようです。dispatchEventの実装を見る限り、もっと低レベルで処理されているような感じです。そもそもそういうアーキテクチャなので無理もないのかもしれません。

Robotで実現

やけになってRobotを使ってIMをONにするキーをエミュレートすると、とりあえずONには出来ることが分かりました。

ただ、やっぱり現在のIMの状態が取れないので、前の入力フィールドのIMの状態を引き継ぐには何か工夫する必要があります。入力された文字列の最後の文字が非ASCIIだったらONという条件で試してみたら、大抵の場合においてそれらしく動くので、自分の場合はこれでいいことにしました。以下のようなコードです。

private static boolean kanjiFlag = false;

public static void kanjiStart() {
	System.out.println("START : -> "+kanjiFlag);
	if (kanjiFlag) {
		kanjiStartGen();
	}
}

public static void kanjiEnd(String s) {
	if (s != null && s.length()>0) {
		if (s.charAt(s.length()-1) > 0x127) {
			kanjiFlag = true;
		} else {
			kanjiFlag = false;
		}
	}
	System.out.println("END : "+s+"  -> "+kanjiFlag);
}

private static void kanjiStartGen() {
	Thread thread = new Thread(new Runnable() {
		@Override
		public void run() {
			try {
				Robot rb = new Robot();
				rb.setAutoDelay(50);
				rb.keyPress(KeyEvent.VK_CONTROL);
				rb.keyPress(KeyEvent.VK_SPACE);
				rb.keyRelease(KeyEvent.VK_SPACE);
				rb.keyRelease(KeyEvent.VK_CONTROL);
			}catch (AWTException e) {
				e.printStackTrace();
			}
		}
	});
	thread.start();
}

入力フィールドの focusGeined で上の kanjiStart() を呼び、focusLost もしくは内容確定時に kanjiEnd(textfield.getText()) を呼ぶ感じです。

完璧ではありませんが、無いより遙かにマシになりました。