マルチプラットフォームでIME対応させたいメモ

プログラマになったからには、死ぬまで一度はOSやコンパイラやインタプリタや検索エンジンやブラウザを作りたいと夢見ると思います。中でも人気の夢がエディタです。エディタを作るからにはIMEをきちんと取り扱えないといけません。

・・・というのはどうでもよくて、OpenGLを使ったマルチプラットフォームのアプリケーションを作るフレームワークとしては、現在はglfwが事実上の標準になっています。どうも日本語変換には対応していないっぽいので、色々調べ始めました。IME沼です。

Windows
==========

我らが石本さんが色々なコードに対して日本語入力パッチを送られているのでそれを見ているのですが、imm32というAPI群があるんですね！

https://github.com/atsuoishimoto/console2-ime/commit/a208e7fc1c649f9e072bdc1aef3f6ffafffa6675

TFS
------

で、いろいろ調べていたのですが、音声入力などにも対応するためにできた、TFSというのが現在は標準っぽいです。

https://ja.wikipedia.org/wiki/Text_Services_Framework

* [歴史的経緯とか](http://nyaruru.hatenablog.com/entry/20070309/p1)
* [最小のサンプル(まだ読んでません)](http://dev.activebasic.com/egtra/2009/02/19/193/)
* [C++ - TSFを利用して候補文字一覧を取得する](http://www.digi-con.co.jp/tech/node/37)

変換中の文字列: Compositionと呼ぶ

変換前後の文字列も一緒にTSFに送る。これは再変換や読み上げのようなアクセシビリティから使われるっぽい。

英語の記事。

* http://www.nxtbook.com/nxtbooks/cmp/msdnmag0707/index.php?startid=91

どうも、TSF関連はVistaの時代にいっぱいサンプルが作られたっぽくて、ステップバイステップのサンプルのリンクとかも見つかるんだけど、今の時代はもう破棄されて見られないのが多いっぽい。TSF冬の時代。

やっぱりIMM32
---------------

どうもレッドモンドの人たちは直接このAPIを触ってほしくないように思えました。.netなら、ラップされてIMM32しか対応しないIMEにも対応したwindow.forms.input.InputMethodを使うようで。後は、一般的にコンポジションの文字列を非表示にする用途(ゲーム)用に、DirectXのGXUIというのも提供されています。コードは非公開。完璧な対応というと次のようになるけど、一朝一夕じゃ無理。

<blockquote class="twitter-tweet" lang="en"><p lang="ja" dir="ltr">使っているIMEがIMM32のみ対応の場合はIMM32を、Windows 8以降ならITextStoreACP2を、そうでなければITextStoreACPを使うようなTSFのサンプルコード(確実に動く)はありませんか？ないですか？そうですか・・・</p>&mdash; 渋川よしき (@shibu_jp) <a href="https://twitter.com/shibu_jp/status/675194431516069888">December 11, 2015</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

* [MSDNのIMM関数のリファレンス](https://msdn.microsoft.com/ja-jp/library/cc422019.aspx)
* ChromiumのIME処理コード: [ヘッダー](http://src.chromium.org/chrome/branches/official/build_149/src/chrome/browser/ime_input.h) [ソース](http://src.chromium.org/chrome/branches/official/build_149/src/chrome/browser/ime_input.cc)
* GTK+のIME処理コード: [ヘッダー](https://git.gnome.org/browse/gtk+/tree/modules/input/gtkimcontextime.h) [ソース](https://git.gnome.org/browse/gtk+/tree/modules/input/gtkimcontextime.c)
* QtのIME処理コード: [ヘッダー](https://github.com/qtproject/qtbase/blob/dev/src/plugins/platforms/windows/qwindowsinputcontext.h) [ソース](https://github.com/qtproject/qtbase/blob/dev/src/plugins/platforms/windows/qwindowsinputcontext.cpp)
* ついでに粂井先生の猫でも分かるプログラミング: [IME](http://www.kumei.ne.jp/c_lang/sdk3/sdk_278.htm) はい猫以下です。にゃー。

主要なライブラリ、みんなIMMでやんの。

ついでの実験結果で、WM_IME_COMPOSITIONで、lparamがGCS_COMPSTRのイベントで、return TRUE(上流に流さないで握りつぶす)をすると、コンポジションウインドウが非表示になるっぽい。やった！

Mac OS X
=============

IMKitというのがある。でもこれはIMEを作る人用のものなので、関係ない。

* Mac OS X: IMKit
    https://developer.apple.com/library/mac/documentation/Cocoa/Reference/InputMethodKitFrameworkRef/

アプリケーション側は以下のプロトコルを実装するっぽい。

* Mac OS X: NSTextInputClient
    https://developer.apple.com/library/mac/documentation/Cocoa/Reference/NSTextInputClient_Protocol/

なお、NSTextInputというのは古いインタフェースなので、その単語が出てくるページからは逃げること。

* 以下のPull Requestで対応する？後で試す→試して、変換ウインドウの表示位置を少し直したら？とコメントした
    https://github.com/glfw/glfw/pull/643
    簡易な実装。っぽい。インライン変換などの対応はない。まあ、もろもろやるにはAPIの追加は必要だしな。

* Mac OS XのCocoa版GTK+で日本語入力を行うためのgtkimmodule（GtkIMCocoa）の開発
    http://www.clear-code.com/blog/2013/3/14.html

変換中の文字列: Marked Textと呼ぶ。NSTextInputClientプロトコルのsetMarkedTextメソッド(これはMac OSが呼び出す。アプリ側は、このメソッドの中身を定義して、引数で渡ってきたデータを自前でローカル変数に入れたり、画面に表示したりする)で、変換中文字列は取得できる。基本はこのsetMarkedTextの様子だけを見ておけば良さそうな気がする。「あいうえお」と入れてスペースを押した後にカーソルで変換範囲を移動してみた様子。

```
2015-12-07 14:58:51.207 events[43639:507] setMarkedText: あいう{
    NSUnderline = 2;
    NSUnderlineColor = &quot;NSCalibratedWhiteColorSpace 0 1&quot;;
}エオ{
    NSUnderline = 1;
    NSUnderlineColor = &quot;NSCalibratedWhiteColorSpace 0.17 1&quot;;
} selectedRange={0, 3} replacementRange: {9223372036854775807, 0}
```

replacementRangeはよく分からん。再変換用？selectedRangeは選択中の範囲(始点と長さ)を持っている。最初の引数の文字列は属性付き文字列になっていて、NSUnderline=2が今カーソルがある位置の単語っぽい。これを分解してコールバックでglfwを使っているアプリケーションに渡して、適切にフォーマットして見せてあげれば良さそう。↑のパッチが当たれば、入力文字列が一文字ずつ分解されてコールバックで渡ってくることは確認済み。

変換前後の文字列も一緒にNSTextInputClientに送る。これは再変換や辞書、読み上げのようなアクセシビリティから使われるっぽい。Windowsと同じ。

* [英語IMEに切り替え](https://github.com/norio-nomura/SpringboardSearchInRomanOnly/blob/master/SpringboardSearchInRomanOnly/SpringboardSearchInRomanOnly.m)
* [IMEの編集中の文字列の削除はこれ？](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/NSTextInputContext_Class/#//apple_ref/occ/instm/NSTextInputContext/discardMarkedText)
* IMEの切り替えのコールバック:
    * NSTextInputContextKeyboardSelectionDidChangeNotificationをNSNotificationCenterで通知してもらう

Macはキーボードが2種ではなくてたくさんあるのと、ASCII入力可能・不可能というグループでキーボード情報が取得できるので、最終的にこのようなコードになった。OFFの時はASCII入力可能の先頭に設定し、ONの時は全グループの先頭から順番に探索し、asciiの先頭でないものを選んでいます。asciiの先頭以外にもないかどうかのチェックが必要かは様子見。

```objc
NSArray* asciiInputSources = CFBridgingRelease(TISCreateASCIICapableInputSourceList());
TISInputSourceRef asciiSource = (__bridge TISInputSourceRef)([asciiInputSources firstObject]);                          
if (active) {
    NSArray* allInputSources = CFBridgingRelease(TISCreateInputSourceList(NULL, false));
    NSString* asciiSourceID = (__bridge NSString *)(TISGetInputSourceProperty(asciiSource, kTISPropertyInputSourceID)); 
    int i;
    int count = [allInputSources count];                                                                                
    for (i = 0; i < count; i++) {
        TISInputSourceRef source = (__bridge TISInputSourceRef)([allInputSources objectAtIndex: i]);
        NSString* sourceID = (__bridge NSString *)(TISGetInputSourceProperty(source, kTISPropertyInputSourceID));       
        if ([asciiSourceID compare: sourceID] != NSOrderedSame) {                                                       
            TISSelectInputSource(source);                                                                               
            break;                                                                                                      
        }                                                                                                               
    }
} else {
    if (asciiSource) {
        TISSelectInputSource(asciiSource);                                                                              
    }                                                                                                                   
}                                                                                                                       
```

IMEの入力のリセットは以下の様なコードにした。QtやGTKや数多くのコードがこれとほぼおなじコードなんだけど、なぜか動かない。

```objc
NSTextInputContext *context = [NSTextInputContext currentInputContext];                                                 
[context discardMarkedText];
[window->ns.view unmarkText];   
```

これ、じっくり理解しておきたい
https://developer.apple.com/library/mac/documentation/TextFonts/Conceptual/CocoaTextArchitecture/TextEditing/TextEditing.html

X Window
=========

fcitxが事実上の標準っぽい。Ubuntu Linuxも最新の15.10から、fcitx-mozcが標準で使えるようになったようです。Arch LinuxやMint Linuxもよくfcitxの話が出てきます。ymotongpooからのタレコミによると、「Google IMEもibusもうあかんわってことになってfctixのサポートに切り替えました」とのこと。FedoraはiBusなんですかね？

fcitxの話をぐぐっても、OSの設定の話ばかり出てきてアプリケーション側でどう使うか、という話は殆ど出てこないのですが、下記の超わかりやすいコードを見るとUNIXドメインソケットでアクセスできるようです。

https://github.com/koron/imcsc-vim/blob/master/fcitx-python/plugin/fcitx-py.py

APIドキュメントはこちら。R5/R6とあるのがやっかい？

https://github.com/fcitx/fcitx/blob/master/doc/API.txt

編集中の文字列はPreeditと呼んでいる？

...と、fcitxについていいろいろ調べていたんですが、どうもLinux版のglfwはXIMでの日本語入力はすでにできる模様。

fcitxはアプリケーションとの接続のインタフェースはいくつか持っていて、GTK+とQtは専用のものを用意しています(リポジトリに入っている)。それ以外のX WindowのアプリとのインタフェースはXIM経由でつなぎます。

https://fcitx-im.org/wiki/XIM

つうか、mattnだった。

松明を持って未開の地を歩いているつもりでも、我々の先にはmattnの足跡がすでに残っている(golang的寓話)

https://github.com/glfw/glfw/pull/473

XIMに関してはここが詳しい

* [XIM による日本語入力](http://rio.la.coocan.jp/lab/xlib/012xim.htm)

なお、[アプリ側では何もしない](https://github.com/glfw/glfw/blob/master/src/x11_window.c#L484-L491)という初期化のされ方をしている。↑のページで説明されている、ルートウィンドウ型である。Preeditも、変換ウインドウも、XIM側で面倒を見る、という方式。on-the-spotでやるには、この起動オプションだけを指定してあげれば良さそう。編集を頑張る(glfwWindowHintで指定？)時に、XIMPreeditNotingの代わりにXIMPreeditCallbacksを設定して、コールバック関数(新規追加)を設定すればおｋ．あとはStatus領域というか変換ウインドウの場所の設定さえわかればなんとかなりそう。ただ、検索しても「候補ウインドウの場所指定分からねえよ」というページばかりで設定の仕方が良くわからない。

...と思ったけど、やっぱり、WaylandやMir用のコードでは対応してないし、まるっとfcitx直アクセスが良いのでは？というコメントをもらいました(from moriyoshi)。とはいえ、いきなりフル機能は大変なので、まずは簡単でも対応させる方向で。

今のコードはルートウィンドウ型なので、コールバックを登録すれば編集中の情報は取得できる。やっかいなのは文字コードがマルチバイト単位で来たり、バイト単位で来たりいろいろある点。後はフィードバック(見た目の属性 - MacのAttributedと同じ)の解釈。このあたりを読めば大丈夫そう。

* [GTKのximドライバのソース](https://stuff.mit.edu/afs/athena/astaff/source/src-9.3/third/gtk2/modules/input/gtkimcontextxim.c)
* [13 章: ロケールと国際化テキスト関数群](http://xjman.dsl.gr.jp/X11R6/X11/CH13.html)
* [XIMFeedbackの仕様解釈と実装](http://www.maroontress.com/Imserver/feedback-impl.html)

表示位置は、ここのコードを参考にすると変更できそう。

* [GTK+のXIM対応コード](https://git.gnome.org/browse/gtk+/tree/modules/input/gtkimcontextxim.c#n769)

ブラウザ
==========

沼です。

* Google Documentの変換の仕組み
    https://docs.google.com/document/d/1RRBJqK9TPGFA5pQPnafDTa0VWKq6Wh9DNiKjsLaZ63U/edit?pli=1
* オンラインエディタのIME対応などのメモ
    http://javascripter.hatenablog.com/entry/2013/10/15/152216
* ブラウザごとのイベントの違い(2011)
    http://hondou.homedns.org/pukiwiki/pukiwiki.php?Javascript%20IME%C6%FE%CE%CF%C3%E6%A4%CE%C8%BD%C4%EA
* 日本語入力(ime)確定時のjavascriptイベント(取得keycode)を整理(2013)
    http://d.hatena.ne.jp/end0tknr/20130808/1375953351
* Input Method Editor APIの対応状況
    http://caniuse.com/#feat=ime
* Input Method Editorの仕様
    http://www.w3.org/TR/ime-api/
* IE系でinputイベントが発火しないのをなんとかしてみる（ついでにIME入力中は発火しないイベントも追加） これはすごい情報＆コード
    http://qiita.com/hrdaya/items/29576a5e1a303c074249

ブラウザごとの違いは新しい情報が知りたい。IME APIがみんなで使えたら楽になりそう。

フレームワーク側の対応
======================

glfw側に最大公約数的なAPIを追加するにはどんなのがあればいいのか案

* IME変換中のコールバック
* 変換に使う周辺テキストをIMEに通知(TFSのコンテキストを読んだ変換対応)
* 再変換処理をIMEに依頼
* 変換候補ウインドウの表示位置の通知
* IME起動とか終了

* GTK+のAPIが参考になりそう。
    https://developer.gnome.org/gtk3/stable/GtkIMContext.html