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

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

もずはっく日記

2018年2月16日

WindowsのIME API、TSFTS_E_NOLAYOUT問題とは 初回投稿日時: 2018年02月17日01時03分16秒
最終更新日時: 2018年02月17日01時32分18秒
カテゴリ: IME TSF Windows
固定リンク: id=2018021600
リンク元: 2件
SNS: (list) シェア

私がよく愚痴っているTSFのTS_E_NOLAYOUT問題について、少しまとめておこうかと思ったので、このエントリを書くことにしました。WindowsネイティブアプリのIMEとの連携部分の自前実装をするという、今どきはあまりやらない事に関する話なので、興味のない方には読む価値はまったく無いかと思います。

最初に各種用語とTSFでの候補ウインドウやサジェストウインドウといった、IMEの出すUIの位置がどのように決まるのかをざっくりと解説しておきます。このあたりが分かってるよ、という方は本題だけ読めば十分かと思います。

TSF周りのざっくりとした解説

まず、TSFとはなんぞやということですが、略さずに書くと、Text Services Framework。その名の通り、単なるIME APIの枠を超えて、アプリにあるテキストのコンテンツ(しかもリッチテキスト対応)に手書き入力や、音声入力、スクリーンリーダー等の外部から自由にアクセスできるようにしようというかなり野心的な代物です。ちなみに、IMEにあたる、テキストの入力アプリのことを、TIPと呼びます。

TSFはWindows XPから標準搭載され(正確にはMS Office XPをWindows 2000にインストールでもOK)、Windows Vistaで一通りの機能が落ち着いた新しいIME APIです。Windows XPの言語設定にあった、「詳細なテキストサービス」というのがTSFの有効・無効設定だったのですが、こう言われると、あれかってなる人も多いんじゃないかと思います。ちなみに、FirefoxもTSFサポートをデフォルト設定で有効にする際、Windows Vista以降に限定しなくてはリリースできないぐらいには不安定でした。

それ以前はアプリはIMMというAPIを使ってIMEと通信していました。しかし、TSFに対応するのは大変なので、2018年現在でも自前でIMEと通信しているデスクトップアプリの多くはエミュレートされたIMMを利用していると考えられます。TSF-awareなデスクトップアプリとして有名なのは、MS OfficeのWord、Windowsアクセサリのワードパッド、ブラウザではFirefoxのみです。一方、UWPアプリはそもそもTSFしかサポートしておらず、必ずTSF-awareアプリと言えます。古いIMEが「設定」や、Edgeで利用できないのは、これらがUWPアプリだからです。

TSFはCOMオブジェクト同士が通信しあう形になっていて、そのインターフェースの数はざっと、120強。はっきりいって、習得の難易度高めです。ただ、TIPからのテキスト入力を受け付けるためにアプリが実装する必要があるのはITextStoreACPITfContextOwnerCompositionSinkの二つのインターフェースと、マウスでの未確定文字列の操作等もサポートするならITfMouseTrackerACPの合計三つとなります。これらをひとつのクラスで実装すると良いだけなので、意外と実装した場合のコードは大きくなかったりします(IMMに比べるとやはり大きくなりますが)。

このうち、アプリの実装側の核となるのはITextStoreACPインターフェースで、これは、TIPやTSFがアプリのコンテンツにアクセスする時に利用する、アプリ側から公開しているAPIに相当します。実際にTIPはこのインターフェースを経由して、文字列の挿入、選択範囲(キャレット位置)の変更、任意の範囲のテキストの取得、任意の範囲のテキストの表示スタイルの取得、任意のテキストの矩形の取得、ドキュメント全体(エディタ全体)の矩形の取得、任意の座標での文字へのヒットテストといったことができます。

で、これらの全てを書いてると、こんな日記のエントリではどうしようもなくなるので、今回採り上げたい「候補ウインドウ等のTIPのウインドウの表示位置決定方法」についてのみ流れを紹介しておきます。

TS_E_NOLAYOUT問題とは

TIPが特定の文字の下に候補ウインドウかサジェストウインドウを表示したい場合、まず、文字の矩形をITfContextView::GetTextExt()を呼び出して取得します。そして矩形の情報が正常に得られたら、その矩形の上下左右のいずれか(スクリーンの端との距離や、横書き・縦書きで変化)に表示したいウインドウを移動させます。

ITfContextView::GetTextExt()が呼び出されると、TSFは、アプリ側のカウンターパートであるITextStoreACP::GetTextExt()を呼び出します。もちろん、単純なアプリであれば即座に文字の矩形を返すだけでTIPはユーザの期待通りに動きます。

しかし、ブラウザのような複雑なアプリになるとそうは簡単に行きません。TSFはFirefoxでは、メインプロセスのメインスレッドで動作します。それに対して、タブの中にあるWebページ内のエディタは別プロセスにありますし、メインプロセスのハングアップを防ぐ為に非同期通信しか出来ません。このため、入力された直後の文字の矩形をTIPもしくはTSFに要求されたとしても、別プロセス内でのレイアウト処理が未だに完了していないのが普通です。ですがTSFを設計された方は素晴らしく、なんと、TSFは非同期でのレイアウト処理を想定して設計されているのです(ちなみに、macOSのCocoaの設計もTSFと似たような設計のAPIなのですが、非同期レイアウト非対応という厳しすぎる現実があります)。

具体的な処理の流れを見てみましょう。アプリ側のITextStoreACP::GetTextExt()TS_E_NOLAYOUTエラーをまずは返し、レイアウト計算の完了を待ちます。次に、レイアウト情報更新されたら、ITextStoreACPSink::OnLayoutChange()ITfContextOwnerServices::OnLayoutChange()を呼び出します。すると、TIPはレイアウト情報を再度取得するチャンスが得られます(無論、この時点でまだ目的の文字のレイアウトが完了していなければ、再び待つ必要があります)。

しかし、ここの実装に重大なバグが存在しており、多くのTIPの現在の挙動から察するにWindows 10の現行バージョンであるFall Creators Updateでも未だにバグっているように思います。そのバグとは、アプリのITextStoreACP::GetTextExt()から返されたTS_E_NOLAYOUTをTSFがTIPに、ITfContextView::GetTextExt()の戻り値としてE_FAILとして返してしまうというものです。E_FAILは一般的なエラーを示すエラーコードであり、TIP側からすると、何らかのアクシデントがあったように見えてしまいます。

実際、ほとんど全てのTIPは、アプリのITextStoreACP::GetTextExt()からTS_E_NOLAYOUTを返すと、表示しようとしていたウインドウの表示を断念したり、スクリーンの左上の端や、フォーカスを持ったウインドウの端等に表示し、なんとかユーザのアクセシビリティを確保しようとします。

しかし、アプリ側から見ると、このTIP側の「努力」がまた、困った問題の原因になるのです。アプリ側からすると、TS_E_NOLAYOUTを返した後に、レイアウトが完了した時点でITextStoreACPSink::OnLayoutChange()ITfContextOwnerServices::OnLayoutChange()を呼び出わけです。するとITextStoreACP::GetTextExt()が再度呼び出され、今度はTIPが期待する矩形を返すことができます。その結果、TIPは一瞬だけ他の位置に表示していたウインドウを期待通りの場所に移動したり、前回のエラーで非表示になっていたウインドウを再表示したりします(希に、表示を一度断念すると、その後も表示しないTIPやケースもあります)。ユーザから見ると、前者は一瞬だけ変な位置にウインドウが表示され、後者はウインドウがちらついて見えます。これは、候補ウインドウ内の選択項目を変更していくだけ、もしくは未確定文字列を一文字ずつ入力していく課程で繰り返し発生するため、非常にできの悪いアプリに見えてしまいます。

TS_E_NOLAYOUTに対応するには

まず第一に、Microsoftさんには一日も早くITfContextView::GetTextExt()TS_E_NOLAYOUTを返すようにしていただきたいです。似たAPIである、ITfContextView::GetRangeFromPoint()はアプリがITextStoreACP::GetACPFromPoint()からTS_E_NOLAYOUTを返すと期待通りにTS_E_NOLAYOUTが返されます。この事実から想像するに、難しい修正とは思えないのです。

TIPは、たとえWindows側でこのバグが修正されていなくても(どのみち、修正されてもWin8.1等へのバックポートが期待できませんが)、対応が可能です。なぜなら、ITextStoreACP::GetACPFromPoint()TS_E_NOLAYOUTが返されるからです。実際にGoogle日本語入力は最後のWindows版のバージョンアップの際にこれに対する修正が入っており、私が知る限りは、唯一、この問題に完全対応したTIPとなっています。その修正方法とはITfContextView::GetTextExt()E_FAILを返してきた場合にITfContextView::GetRangeFromPoint()を呼び出し、これがTS_E_NOLAYOUTを返してきた場合、ITfContextView::GetTextExt()の本当の戻り値はTS_E_NOLAYOUTだったと確認することができます。

そして、TIPにはもう一点、やらなくてはいけないことがあります。それは、ITfContextView::GetTextExt()の本当の戻り値がTS_E_NOLAYOUTだった場合に、初回の表示であれば表示を保留し、既に表示済みであればそのままの位置に留めておくべきです。そうしなければ、レイアウト計算完了通知が来た際の再表示、もしくは表示位置修正で点滅したり、一瞬だけ変な位置に表示されたりというバグが発生してしまいます。

ちなみに、Microsoft社製のTIPは全体的にこの問題に対応できておらず、日本語版Microsoft IME、簡体中国語のMicrosoft Pinyin、Microsoft Wubi、繁体中国語のMicrosoft Changjie、Microsoft Quickの全てで、なんらかのバグを確認しています(韓国語版Microsoft IME、Microsoft Old Hangulと、繁体中国語のMicrosoft Bopomofoはウインドウの表示があるのか無いのかもよく分からないので未確認)。これらのバグは、Firefoxのabout:configから、intl.tsf.hack.ms_japanese_ime.do_not_return_no_layout_error_at_caretintl.tsf.hack.ms_japanese_ime.do_not_return_no_layout_error_at_first_charintl.tsf.hack.ms_simplified_chinese.do_not_return_no_layout_errorintl.tsf.hack.ms_simplified_chinese.query_insert_resultintl.tsf.hack.ms_traditional_chinese.do_not_return_no_layout_errorintl.tsf.hack.ms_traditional_chinese.query_insert_resultのそれぞれをfalseにすることで、Firefox側で対応しているハックを個別に無効化して確認することができます。

とにかく、Microsoftさんにはこれらの修正をどうにかやっていただきたいです。FirefoxではアクティブなTIPと取得される範囲と未確定文字列との位置関係からホワイトリスト形式で、TS_E_NOLAYOUTエラーを返さずに、それっぽい座標を返すという対応でなんとかこの問題に対処しているのですが、このハックのコードが膨れ上がり過ぎてて、正直なところ、そろそろ限界です。

2018年2月11日

WebStudioともずはっく日記のHTML5化とその他もろもろ 初回投稿日時: 2018年02月11日21時59分20秒
最終更新日時: 2018年02月12日00時59分32秒
カテゴリ: WebSiteManager WebStudio
固定リンク: id=2018021100
リンク元: 0件
SNS: (list) シェア

前々から気になっていたものの、時間が確保できずに保留になってたこのサイト全体の更新を雑にやりました。まだまだ古い日記のエントリ等も書き換えていかないといけないので、その辺にアクセスされる場合はもうしばらくお待ちください。以下、ざっくりとやったこと。

XHTML 1.0 StrictからWHATWGのLiving Standardに書き換え

もともと、<hn>要素と、それに続く弟要素たちをグループ化していたdiv.sectionと、<hn>要素に続く弟要素たちをグループ化していたdiv.contentで似たような構造化を行っていましたが、今のブラウザのCSSセレクタの充実ぶりを考えると、後者はもう必要無いのでこれを削除、div.sectionを適当な要素で置き換えて行くという作業が大半を占めました。

作業には私が大昔に自作したWebSiteManagerというHTMLエディタを使いましたが、XHTML 1.1の時代までしか開発してなかったものなので、HTMLの要素や属性、CSSのプロパティやその値がリストに入っていなくて効率悪く、非常にストレスのたまる作業になってしまいました。もう、コンパイル環境(Delphi)が無いし、かといって今更高い金出してDelphi買っても、たぶん素直にコンパイル通らなさそうというのでどうしたもんかと。

HTML5は策定時の色々中途半端な知識がGeckoの開発時に問題になっても困るので、逐一知識をあえていれないようにしていたので、今回、イチから勉強する形になりました。アウトライン周りはなかなかに人によって意見が分かれそうな雑な定義で実に悩まされましたね……ググっても、どれを参考にすれば良いのかと思えるぐらい、検索結果が玉石混交っぽくて、今でもまだ正解が分かってません。

W3CのHTML5.xではなく、WHATWGの方を採用している(つもり)なのは、ブラウザベンダが基本的にはW3CのHTML5.xの方は向いていないからですね。

application/xhtml+xmlでの送信をやめた

HTML5化にあたって、XHTMLをHTMLに書き直し、application/xhtml+xmlで配信するのを完全にやめました。当時から対応していないブラウザやWebサービスを使っているユーザから苦言をいただきましたが、私の方へ来た意見の中で、サービスのベンダに報告しといたよっていう内容がゼロ件だったので、まあ、イラッとしましたね。

技術的には結構興味深かい生きた実験場でした。SGMLなHTMLから、機械とより相性の良いXMLで再定義されたXHTMLをフルにXMLとしてブラウザに解釈させるにはこのMIMEタイプでの配信が必須だったわけですが、当然、XMLなのでパースできないとエラーになってしまうわけです。

これがWebコンテンツでどういう意味を持つかというと、ブラウザはできるだけ高速にレンダリングしようとするため、読み込み中でもwell-formedであると仮定して、HTMLのように逐次表示を行っていくわけです。しかし、ロード中に回線が不安定になると(東海道新幹線N700車内のWiFi接続でしょっちゅう再現)、突然、途中まで表示していた結果すら破棄して、パースエラーを出すわけです。

また、日記のエントリをプレピュー表示しようとして、その内容がXML(XHTML)的にInvalidだった場合、当然、プレビューがXMLのパースエラー画面になってしまいます。つまり、昔流行った、Web2.0なコンテンツとは死ぬほど相性が悪いわけです。

これらのことは、近日中に静的なページを更新してそちらに書いておきますが、長年こだわって付き合った結果、XMLはWebページには相性の良い技術ではない、というのが私の感想です。

WebSiteManagerやLibraryにあった古いドキュメント、古いW3C仕様書の指摘翻訳は削除

これら、更新していないものたちは、百害あって一利なしと判断して削除しました。ただ、機種依存文字に関するドキュメントのみ、内容が古いですが、未だにアクセスが多いので残しておくことにしました。

モバイル対応を改善

本格的にやるつもりもないのですが、一応、雑に行っていたモバイル対応をもう少し現状にあわせたものに修正しておきました。画面解像度のブレイクポイントってどこに設定するのが良いのかは本当に難しいですね。PCだけはその対応から外しておきたいともなんとなくは思うのですが、やり方を思いつかず。

HTTPS化とwwwサブドメインの段階的な削除

Let's Encryptの無料SSL証明書が使えるようになったとアナウンスをもらっていましたが、なかなか作業できずに、結局このタイミングでやることに。

ついでに、リダイレクトでwww.d-toybox.comから、d-toybox.comに変えるようにしました。そのうち、サーバのドメイン設定からもwwwサブドメイン(と言って良い?)を削除してしまいたいなと。