この日記はMozillaのプロダクトへの貢献者としての私の成果を中心に、気になったバグやWeb界隈の話題について書いていますが、 断り書きがある場合を除き、いかなる団体のオフィシャルな見解ではありません。あくまでも個人的なものです。 Mozilla Foundation、Mozilla Corporation、及び関連企業の公式情報ではないことに注意してください。

現在、XHTML 1.0 (もどき)から、HTML5なコンテンツに修正中です。古い日記は修正が完了していませんので表示が崩れます。 順次、修正していく予定ですのでしばらくお待ちください。

もずはっく日記(2015年2月)

2015年2月27日

Bug-org 1119609 event.key from nsIDOMWindowUtils.sendKeyEvent() is 'undefined'
初回投稿日時: 2015年02月27日03時25分43秒
カテゴリ: Events Firefox OS Mozilla Core Mozilla38 バグ修正
SNS: (list)

nsIDOMWindowUtils.sendKeyEvent()を利用してKeyboardEventを生成しても、KeyboardEvent.key等、D3Eで追加された新機能の一部に対応していないというバグです。

そもそもの原因は、このメソッドが自動テスト用に設計されたため、最小のコストで実装するために、当時のKeyboardEventの生成に必要な情報を全て引数で個別に設定できるようにしていることでした。その後、フラグをオプションの引数として追加することで、KeyboardEvent.location等にはだましだまし対応していましたが、KeyboardEvent.keyや、KeyboardEvent.codeの対応には、どうしても引数の追加が必要になります。しかし、そうすると、アドオンとの互換性が失われるため、好ましくありません。そして、今後も仕様側で属性が追加される度に引数を増やしていくのも現実的とは言えません。

ではその解決策をどうすれば良いのかと考えてみると、D3Eのイベントコンストラクタのように、Javascriptから任意のオブジェクトを受け取って、そこに保存されてる任意の情報からイベントを初期化していく、ということが考えられます。しかし、この方法は、XPCOM的には少し面倒です。そこで、直接的に、KeyboardEventそのものを引数として受け取れば良いという結論に至りました。これなら、単純に、nsIDOMKeyEventとしてオブジェクトを受け取れるためです。

幸い、Bug-org 930893の修正により、KeyboardEventは、Javascriptから、簡単にインスタンスを生成することができます。ですので、この方法がAPIの提供側、ユーザ側双方にとってベストな方法と言えます。

ですが、ここで一度、互換性を捨てて新しいAPIを用意するのであれば、他の懸念事項も解決しておこうと考えました。

一つ目の懸念事項は、keypressイベントの扱いです。このイベントは、D3Eでもレガシーイベントのひとつと定義され、具体的な仕様を定めず、各ブラウザがこれまでのバージョンとの互換を維持続けられるように配慮されています。ですが、nsIDOMWindowUtils.sendKeyEvent()は、「DOMイベントを生成すること」が目的のメソッドです。つまり、APIのユーザは、どのようなあり得ない順序・タイミングでのDOMイベントの生成も可能です。しかし、これはもちろん、Webアプリ作者の方にとってはたまったものではありません。

そこで視点を変えてみました。Geckoがネイティブイベントをハンドリングしている時、何をハンドリングしているのか、それは、キーが押された、もしくは離された、この二点のタイミングを元に、適切にDOMイベントを生成しているのです。つまり、アドオンやFirefox OSからも、この二点だけを通知してもらい、Gecko側で必要なイベントを適切に生成すれば、Webアプリから見た場合に、差異は無くなります。これは、自然な自動テストを書く上でも大切なことと言えます。

このことから、keydown()keyup()の二つ目のメソッドが存在すれば、良いことが分かります。

そして二つ目の懸念事項は、KeyboardEventが、どのタイミングでも生成できてしまう、という点です。

具体的には、GeckoはIMEの未確定文字列が存在する際には、KeyboardEventを一切生成しませんが、nsIDOMWindowUtils.sendKeyEvent()では、生成可能です。

Geckoの現在の動作は、最新のD3Eでも好ましい動作とされています。しかし、他のブラウザは、keydownkeyupは未確定文字列が存在していても生成していて、この差によって、期待通りに機能しないサイトというのが実際に報告されています。ですので、Gecko側が、今後、仕様よりも他のブラウザの動作にあわせる可能性もゼロではないのです。少し話が逸れましたが、要するに、Gecko側の動作にあわせて、アドオンやFirefox OS側も修正が必要になるような設計というのは現実的ではありません。キーが押された、離されたという事実を全てAPIで通知してもらい、必要な時だけGeckoがDOMイベントを生成するという形の方がより良い設計であることは誰の目にも明らかでしょう。

これを解決するためには、IMEの状態を知る必要があります。であれば、nsITextInputProcessorに、keydown()keyup()を追加してしまうのが理にかなっています。

大枠のアイデアはこれで決まりました。しかし、実際に実装しようとすると、いくつかの問題が出てきました。

まず、現在のD3E仕様では、AltMetaControlShift以外のモディファイアキーの状態を、KeyboardEventを生成する際に指定できません。また、モディファイアキーの状態を直接指定できてるのは不自然です。例えば、連続する文字入力のキーが、別々のモディファイアキーの状態である可能性があります。これは、モディファイアキーのイベントを監視しているWebアプリがあると、そういったアプリ上では期待通りに処理されない可能性があります。

こういった問題を解決するために、nsITextInputProcessor.keydown()nsITextInputProcessor.keyup()は、引数のKeyboardEventのモディファイアキーの状態を無視するようにしました。その代わりに、モディファイアキーのイベントを通知される度に、インスタンス内でその状態を記録し、KeyboardEventの初期化時には、その保持している情報のみを利用するようにしました。これにより、実際のキーボードと同じ手順を踏まなくては、モディファイアキーの状態を反映したKeyboardEventが生成できないようになっています。

もうひとつ別の問題点があります。それは、nsITextInputProcessor.keydown()は、自動的にkeypressイベントを生成するということです。これは例えば、Aキーが、未確定文字列を生成する際に、

TIP.keydown(new KeyboardEvent({ key: "a", code: "KeyA", KeyboardEvent.DOM_VK_A }));
TIP.setPendingComposition("あ");
TIP.appendClauseToPendingComposition(1, TIP.ATTR_RAW_CLAUSE);
TIP.flushPendingComposition();

というコードを書くと、keydownkeypresscompositionstartという順でイベントが発生します。こうなると、<input>要素等は、"aあ"という入力結果になってしまいます。

また、未確定文字列が変更される度に、keydown()keyup()を呼び出すのも手間ですし、本来不要なvirtual callが多数発生するため、あまり好ましくありません。

そこで、既存のnsITextInputProcessor.startComposition()nsITextInputProcessor.flushPendingComposition()nsITextInputProcessor.commitComposition()nsITextInputProcessor.cancelComposition()それぞれに、オプションの引数として、KeyboardEventを渡せるようにしました。つまり、各、未確定文字列への変更の原因となったキーの情報を提供してもらえれれば、後はうまくやりますよ、ということです。

上記の例は、

TIP.setPendingComposition("あ");
TIP.appendClauseToPendingComposition(1, TIP.ATTR_RAW_CLAUSE);
TIP.flushPendingComposition(new KeyboardEvent({ key: "a", code: "KeyA", KeyboardEvent.DOM_VK_A }));

という形で書けます。この結果、keydowncompositionstartの順でイベントが発生し、keypressイベントは発生しませんので、<input>要素等の内容は期待通りに、"あ"のみとなります。

このように、キーに起きた「事実」を全て通知してもらいさえすれば、OSからネイティブキーのイベントを受け取ってDOMイベントを生成したときと全く同じ条件で、アドオンやFirefox OSのキーイベントも生成されるようになりました。残る問題は、既存のアドオンやFirefox OSのキーボードの対応の問題ですが、このあたりは私にはどうしようもありません。残念ながら、生成するアドオンやキーボードに依存して、D3Eに対応したイベントが生成されるか否かが決定するという形になります。正直なところ、これは好ましい話とは思っていません。しかし、このような、仮想も含めたデバイスにWebアプリが依存する状況がまたひとつ産まれたというのは、Webアプリがネイティブアプリにまた一歩近づいたということでもあり、本当は好ましいことなのかもしれません。

ちなみに、Firefox OSでは、もう一層、CompositionManagerというAPI層があり、ここの仕様を修正するまでは、各キーボードやIMEがD3Eに対応することはできません。ここは、台湾のチームにがんばってもらおうかと勝手に思ってます。

なお、このAPIのより詳しい情報は、既にMDNで公開しています

関連するかもしれないエントリ

bug-org 1119609を含むエントリ