2015年4月17日
TSFには、ITextStoreACP::GetTextExt()
から、TS_E_NOLAYOUT
を返しても、TIPにはE_FAIL
が返されてしまうので、レイアウトの再計算の終了を待つことができないというバグがあります(このバグはWindows 10では修正されている模様)。
このバグに個別に対応するため、問題が判明しているTIPの名前ごとに、必要なハックを用いて、「それっぽい位置」を返して、問題が無いかのように見せていますが、Google日本語入力の存在で、Windowsの言語が変わると、TIPの名前も変わっている可能性があることが分かりました。そこで、これまでにハックを行ったTIPを全て、そのターゲットとしている言語バージョンのWindowsと、英語版のWindowsで名前を確認していきました。
幸い、Google日本語入力以外では、Windowsの言語に関係無く、同じ文字列が名前として利用されていましたので、このバグ修正では、それぞれを確認するためのコードをTSFStaticSink
のメソッドにしてしまうことで、定数まみれだったコードを綺麗にしています。
Googleスプレッドシートで日本語入力中に、一度、他のアプリをアクティブにして、再びFirefoxにフォーカスを戻すと、TSFと、IMMを問わず、しかし、症状が異なるバグり方で、正常に日本語入力ができなくなるというバグです。
調査してみると、すぐに、IMEの状態管理に問題があることが分かりました。最初にスプレッドシートを開くと、どのセルも編集モードになっていなくても、IMEをオンにできてしまいます。また、スプレッドシートを表示している状態で設定タブを開き、スプレッドシートのタブに戻ってくると、セルの編集を開始しても、IMEが無効になったままでした。
そこで、IMEStateManager
のログ等からGoogleスプレッドシートの挙動を調べて見ると、まず、ドキュメントがフォーカスを得た時に、contenteditable
な要素にフォーカスを移動させている事が分かりました。ただ、問題は、ドキュメントのfocus
イベントハンドラ内で、フォーカスを移動させていたことにあります。このため、ドキュメントがフォーカスを得たことで、IMEを無効化しようとしますが、それを行う前に、エディタがフォーカスを得ることで、IMEを先に有効化してしまいます。そしてその後で、ドキュメントのフォーカス処理に戻って、IMEの無効化が完成します。
まず、最初のパッチでは、ドキュメントのフォーカス処理の最中に、別の要素にフォーカスが移動していたら、IMEの状態管理は(既に行われているはずなので)行わないように修正しました。
しかし、これだけではうまく動作しませんでした。さらに調査してみると、Googleスプレッドシートは、フォーカスの移動をブラウザに任せておらず、ドキュメントがフォーカスを得るときに、自身でフォーカスを行ったん別の要素に設定し、その後、元のフォーカスを持っていた要素にフォーカスを設定するということを行っていました。
ドキュメントにひとつしか存在しないnsHTMLEditor
は、入力イベントが来る度に、自分自身が管理すべき要素がフォーカスを持っているのかどうかを検査します。しかし、Googleスプレッドシートの自前フォーカス管理によって、この検査で「フォーカスを持っていない」と判断し、一部のCompositionEvent
を捨て、フォーカス移動によって発生している強制確定のリクエストを無視してしまっていました。
以前より、CompositionEvent
は、TextComposition
クラスが、compositionstart
からcompositionend
まで、必ず同じ要素上で発火するようにしています。そこで、これを利用して、nsHTMLEditor
はCompositionEvent
のターゲットが自分自身が管理しなくてはいけない要素のうちの一つであれば、フォーカスを持っていなくても処理を行う様に修正しました。
D3Eでは、各種イベントを生成する時に、dictionary
を用いた、簡単な記述で互換性の問題も発生しにくいコンストラクタが提案されています。基本的にはこのdictionary
は生成後にそのイベントの属性名になるものをキーとして値を設定します。例えば、
var keyboardEvent = new KeyboardEvent("keydown",
{ key: "Tab", code: "Tab", keyCode: 9, shiftKey: true });
しかし、D3EではKeyboardEvent.getModifierState()
や、MouseEvent.getModifierState()
で、キーとして定義されている全てのモディファイアの状態や、Accel
といった仮想モディファイアも状態を検査できるようになりました。しかし、逆に言うと、これらが属性という形式をとっていないため、Javascriptがイベントを生成する時に初期化する方法がありませんでした。
そこで、EventModifierInit
が新たに提案され、今回はこれの実験実装を行いました。これにより、例えば、CapsLock
と、OS
をtrue
にしたい場合、以下のように記述できます。
var keyboardEvent = new KeyboardEvent("keydown",
{ key: "Tab", code: "Tab", keyCode: 9,
modifierCapsLock: true, modifierOS: true }
D3Eでは、Microsoft製のマルチメディアキーボードに付いている、FLockというキーを、一応定義しています。これは通常のOS上のアプリからは全く見えない特殊なキーです。
このキーを、key
値ではFnLock
、code
値ではFLock
と定義されていましたが、このミスマッチはおかしいので、W3Cにバグを投げていましたが、ようやくFnLock
で統一されたので、code
値にFnLock
を追加しました(FLock
はこのミスマッチに気付いていたので、もともとサポートしていませんでした)。
今のところ、ITextStoreACP::GetACPFromPoint()
を活用しているTIPを見つけていないような状態でしたので、今まで実装していませんでしたが、これから、TSFをFirefoxで標準機能とした場合には、新しいTIPがこの機能を利用した時に、Firefoxが実装していなければ利用を断念するかもしれません。そんな状況はよくないので、今回、ある程度実装を行いました。
GXFPF_ROUND_NEAREST
とGXFPF_NEAREST
が指定されていて、指定位置に文字が無い場合、その場所をクリックしたときにキャレットが移動する場所を返します。
GXFPF_NEAREST
のみが指定されていて、指定位置に文字が無い場合、簡単にその位置に近い文字を探すAPIが残念ながらGecko内には存在していないため、このケースの実装のみ、正確性を欠いたものになっています。このケースでは、基本的に、その位置をクリックした場合のキャレット位置を返します。ただし、最後の文字の右側に来る場合は、ひとつ引いた値になります(文字の最大値は、キャレット位置の最大値 - 1なため)。これは、大きく異なる座標を返すことはありませんが、その座標から近い文字の右半分にヒットしている場合、ひとつ右側の文字のインデックスを返すことになります。
widget
がフォーカスを持っているエディタの内容を調査する時に利用している内部イベントのWidgetQueryContentEvent
、その調査結果はmReply
に格納されるのですが、イベントの種類によって、どのメンバーが更新されているのかが変わります。
これまではパフォーマンス重視で、初期化を行っていませんでしたが、もし、更新されない値を誤って参照した場合に、危険なクラッシュ等を引き起こさないように、全てのメンバを安全な値で初期化しておくように修正しました。
ついに、TSFモードがリリース版でもデフォルトで有効になることになりました。
古くから、現在はMozilla Japanのエンジニアである加藤誠さんからTSFに対応すべきだというバグ報告がありました。これが2001年7月2日のことです。
その後、話が私のところに来ましたが、私は当時、寝耳に水の話でしたし、ドキュメントを読んでもまったく理解できなかったので、手つかずのまま放置されていました。
大きく状況が動いたのは2008年7月5日でした。現在、Firefox for AndoidのキーボードやIME周りを一手に担当している、Jim ChenがGoogle Summer of Codeで、TSFモードの実装を開始しました。
彼のおかげで、おおまかに動くところまでは行ったのですが、Windows XP日本語版に標準搭載されていたNatural Inputの挙動がトリッキーだったり、そもそも、Windows XPのTSF自体に問題が多く、実用化には至りませんでした。
その後、TSFを実装するのに足りない機能や、不便だった点をGecko側で修正しつつ、TSF対応のコードも私が引き継いで、徐々に改善を続けていました。
そしてついに、現在報告されている深刻なバグの修正が全て終了したので、今回、リリース版でもデフォルトで有効にすることにしました。既にAurora (Developer Edition)では39から標準で有効になっていますので、Auroraで2サイクル、Betaで1サイクル、Nightly以外のテスタからのフィードバックも期待しています。この間に特に深刻な問題が報告されなかった場合は、40からリリース版でも有効になることになります。