Dec 24, 2009

画面の描画内容を一時的にロックしておいて、裏であれこれして最後にまとめて描画させる方法の再考

画面の描画を一時停止する方法を先日書いたけど、案の定というかやっぱりというか、重大な弊害があることが分かった。また、その弊害にぶち当たらない安全なやり方も見つけることができた。

安全に画面の描画を一時停止・再開する方法は、以下の通り。

var baseWindow = window.top
                   .QueryInterface(Ci.nsIInterfaceRequestor)
                   .getInterface(Ci.nsIWebNavigation)
                   .QueryInterface(Ci.nsIDocShell)
                   .QueryInterface(Ci.nsIBaseWindow);
baseWindow.setPosition(window.innerWidth, window.innerHeight); // これで画面の描画が止まる

gBrowser.addTab(); // これによって起こる変化は画面上に現れない
gBrowser.addTab(); // この変化も画面上に現れない
gBrowser.addTab(); // 同上

baseWindow.setPosition(0, 0); // ここでやっと描画が再開される

以下、前のエントリに書いたやり方にどういう弊害があるのか、および、このエントリで紹介するやり方の方がどのくらい安全なのかについて詳しく説明する。

nsIContentViewerのhide()/show()は危険

前のエントリに書いたのは、nsIContentViewerというインターフェースのhide()メソッドで描画を一時停止させて、show()メソッドで再開させるという方法だった。しかし、このやり方をツリー型タブ実際に使ってみたところ、Webページ上でのコンテキストメニューがおかしくなるという障害が発生してしまった。

原因は、画面の描画を再開させた後document.commandDispatcher.focusedWindowの指す先がgBrowser.contentWindowではなくwindow.topになっていたせいだった。

通常、document.commandDispatcher.focusedWindowには「最後にフォーカスしていたフレーム」が格納されている。しかしtabbrowser要素は、現在のタブを閉じるとフォーカスしていたフレームも一緒に破棄してしまう。そこで、tabbrowser要素ではupdateCurrentBrowser()というメソッドの中でthis.mCurrentBrowser.focus()を呼んでいる。これはbrowser要素のfocus()メソッドを呼んでいるということなんだけれども、実際にはその中で表示しているフレームの方にフォーカスが移る。どのフレームにフォーカスを移すべきかという情報は、Firefoxが内部的に保持しているわけだ。

ところが、nsIContentViewerのhide()/show()を呼ぶとこれがうまくいかなくなる。hide()メソッドの実装を見るとDestroy()とかなにやら物騒な名前のメソッドが色々呼ばれてるんだけど、どうやらこのどこかで前述の「どのフレームにフォーカスを移すべきかという情報」が破壊されてしまうようで、その後show()を呼んでからbrowser.focus()しても、XULのbrowser要素にフォーカスが移るだけで終わるようになってしまう。

で、さらに困ったことに、一度この状態になってしまうとウィンドウ内のどこをクリックしてもフレーム間でフォーカスが移らなくなってしまう。Webページの中で右クリックしてコンテキストメニューを開くと、普通だったら強制的にそのフレームにフォーカスが移されて、document.commandDispatcher.focusedWindow.getSelection()で選択範囲の有無が判別されてメニューの内容が更新される……んだけど、フォーカスの情報が破壊された状態だと、document.commandDispatcher.focusedWindowの指す先がFirefoxのChromeウィンドウのままになってしまうため、選択範囲のテキストがない→コンテキストメニューの内容も選択範囲がない時の状態になる という感じで、メニューからコピー&ペーストができなくなってしまう。Ctrl-Cとかのショートカットも効かないまんま。この状態は、一度他のウィンドウにフォーカスを移して再度フォーカスを戻すまで継続されてしまう。

……というのが、前に書いた方法の重大な弊害の詳細です。

安全な方法

このエントリの冒頭に書いたnsIBaseWindowインターフェースのsetPosition()を使うと、前述のようなフォーカス情報の破壊を伴わずに画面の描画を一時停止・再開させるように見せかけることができる

このメソッドは一体何かというと、そのウィンドウの中のUI要素の描画位置を指定の座標にずらすという物。引数はウィンドウの左上(ウィンドウ枠の内側)を原点としたX, Yの各座標のピクセル値で、例えばsetPosition(10, 20)と書くと、ウィンドウの枠から10ピクセル右・20ピクセル下にずれた位置からウィンドウの内容の描画が始まるようになる。

冒頭の例ではこれを使って、setPosition(window.innerWidth, window.innerHeight)画面の内容の描画位置を画面外に吹っ飛ばしている。再描画は依然として行われてるんだけど、描画されてる位置がウィンドウの外側なので、ユーザの目には全く見えない。で、一通り描画が終わった後でsetPosition(0, 0)で描画位置を元に戻してやる。すると、体感的には「一時的に再描画が止まって、処理が終わった後にまた動き出した」ように感じられるわけです。原理からも分かるとおり、このやり方なら前述のフォーカス情報が破壊されないので安心して使えます。

ということで、画面描画を一時停止/再開するためのライブラリも、こっちのやり方を使うように書き直しました。ライブラリの方では念のため、ウィンドウサイズの3倍の位置に描画内容をずらすようにしてます。

エントリを編集します。

wikieditish message: Ready to edit this entry.











拡張機能