Home > Latest topics

Latest topics 近況報告

たまに18歳未満の人や心臓の弱い人にはお勧めできない情報が含まれることもあるかもしれない、甘くなくて酸っぱくてしょっぱいチラシの裏。RSSによる簡単な更新情報を利用したりすると、ハッピーになるかも知れませんしそうでないかも知れません。

萌えるふぉくす子さんだば子本制作プロジェクトの動向はもえじら組ブログで。

宣伝1。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能! シス管系女子って何!? - 「シス管系女子」特設サイト

宣伝2。Firefox Hacks Rebooted発売中。本書の1/3を使って、再起動不要なアドオンの作り方のテクニックや非同期処理の効率のいい書き方などを解説しています。既刊のFirefox 3 Hacks拡張機能開発チュートリアルと併せてどうぞ。

Firefox Hacks Rebooted ―Mozillaテクノロジ徹底活用テクニック
浅井 智也 池田 譲治 小山田 昌史 五味渕 大賀 下田 洋志 寺田 真 松澤 太郎
オライリージャパン

Page 3/239: « 1 2 3 4 5 6 7 8 9 »

XUL/MigemoとFirefox 3.1のロケーションバーや検索ツールバー - Sep 02, 2008

一旦は実装完了したと思ったんだけど実はまるっきり仕様を読み違えてた事が判明してから直さなきゃと思ってるうちに夏コミがあったりFirefox 3 Hacksの校正があったりですっかり放置してた、Firefox 3.1からの新機能への対応をぼちぼちと。

ていうかこの絞り込み機能、めっさ使いにくい気が……せめて初期状態のキーワードはもうちょっとわかりやすい物にした方がいいと思うんだ。こんなそこいらの使われてない記号を適当に当てはめただけのものなんて、とても憶えられんよ。

しかも複数指定可能とかで頭がこんがらがってきたので、手戻りを防ぐために自動テスト書いたりもしてみた。こういう時ほんとにUxUがあって良かったと思う。

あとlevelさんのエントリの後半で触れられてるスマートキーワードとの連携についても、パッチを見ながら実装してみた。でもこれって実装を見る限りPOSTメソッドのスマートキーワードには非対応ですよねぇ……どうすんだ?

他に、すでに開かれてるタブにマッチした時はどうこうするという話も出てるみたいだけど、僕としては本家の実装が出てきてからその動作を真似る形を取らざるを得ないので、現状は様子見です。

ああ、あとすべて強調表示が選択範囲になったことへの対応もしなきゃなあ。見た目の問題だけじゃなく、今のXUL/Migemoは選択範囲の位置で現在の検索のフォーカスを判断してるから、何もかもが動かなくなる予感。

textboxのsearch型というのは何か影響するんだろうか? 影響有りならこれも対応しないと……

続報(上記の事柄の調査結果まとめ)

スマートロケーションバーでNOT検索できるようにしたXUL/Migemo 0.10.5をリリースしたよ - Jul 07, 2008

表題の通り、XUL/Migemo 0.10.5からロケーションバーでNOT検索を可能にしました。「mozilla -firefox」という風に入力すると、「Mozillaという単語は含むがFirefoxという単語は含まない」候補だけがヒットするようになります。当然Migemo検索との併用も可能。いらん候補が大量にヒットするのがウゼーと常々思っていたので、バグ修正のついでにサクッと実装してみました。

残念ながら、履歴とブックマークの管理、履歴サイドバー、ブックマークサイドバーでの検索については非対応です。これらの検索機能はPlacesのクエリ機能に依存していて、そのクエリ機能にNOT検索の機能がないためです。クエリにNOT検索の機能が付いたら対応できるかも。

スマートロケーションバーの検索に対応したXUL/Migemo 0.10.0をリリースしたよ - Jul 03, 2008

ということでXUL/Migemo 0.10.0やっとリリース。 (動作してる様子のスクリーンショット)

スクリーンショットを見ると分かるとおり、なにげにAND検索にも対応してます。仕掛けは単純といえば単純で、入力された文字列をスペースで区切ってそれぞれについて正規表現を生成した後に、それらの順列組み合わせを全展開した正規表現をさらに生成する(これを使ってマッチングするので、各単語の順番が入れ替わってもマッチする)という、ものすんごい力業。単語数が増えると組み合わせの数が爆発的に増えて正規表現がクソ長くなってマッチングがクソ重くなるので、実用的な速度が出るのはだいたい3語くらいまでが限界だと思います……

ちなみに順列組み合わせの展開には無駄にMozStorageを使ってます。JavaScriptで一体どーいうアルゴリズムでやりゃぁいいのかちっとも分からんかったのでググってみたら、順列組み合わせの総数を求める方法ばっかり引っかかる中で一つだけSQLの自己結合を使った解き方が見つかったので、それをそのまま使わしてもらいました。pIXMigemoTextUtilsのgetANDFindRegExpFromTermsというメソッドがそれなので、興味ある人は見てみてください。

しかしまあ、今回のこれはFirefox 3 HacksのためにPlacesのことを詳しく調べてたからやっと実現できたようなもんで、つまりFirefox 3 Hacksを読めばこんなことは楽勝でできるようになるかもねということで、皆さんゼヒ買って下さい、と宣伝しておきます。オライリーから8月発売予定です(再掲)。

5日追記。ページ内検索では考慮しなくてもよかった問題が表面化してフリーズする(全角スペース1文字だけにヒットした場合に無限ループに陥ってしまう)という現象が起こってしまっていて焦った。対策を入れて早速更新した。

ロケーションバーでXUL/Migemoを使う計画がわりと現実的になってきたっぽい - Jul 02, 2008

places.sqliteが数十MBを超えているのりさんやdrryさんの環境では分単位で固まってしまって使い物にならん、ということだったので、お二人に協力してもらって各段階での処理の所要時間を調べてみた。

そしたらどうも、Placesデータベースから文字列をぶっこ抜いて正規表現でマッチングしてるところがものすごく重いらしいということが判明した。ログによると500万文字ある文字列に対してマッチングをかけようとしてたんだから、そりゃあ固まるわ、と。

固まる問題だけでもとりあえず何とかしよう、ということで数千件単位で文字列を取り出し→マッチング→取り出し→マッチング……という風に分割処理するようにしてみたところ、とりあえず固まる問題は何とかなったんだけど、でも検索結果が出るまで延々待ち続けないといけないのは相変わらずで。

と、そこでやっと気づいたんだけど、べつに正規表現でのマッチングとポップアップの内容の更新とを完全に分けて実行する必要はないんですよね。ちょっとずつマッチングしてちょっとずつ検索結果を取得してちょっとずつ結果のリストを更新していけば、全体ではものすごく時間がかかるようであっても、とりあえず最初の方の結果だけは見えるからストレスにはならない。必要な数だけ結果を取り出せたらそこで処理を打ち切ってしまえばいいんだし。

というわけであちこち書き直してみた結果、最初の実装に比べるとびっくりするほど快適に動くようになった。スレッド使ってなくてもそんなに重くない。のりさんにも「これなら常用できそう」と言ってもらえたし。

最終的にやってることは同じなんだけど、やり方を変えるだけでこんなにも体感速度に違いが出るものなんだなあ、ということをこれ以上ないほど実感した日でした。

それはそうと、途中の段階で分割処理を行うようにしたときのログを見てて気がついたんだけど、SQLite(というか RDBMS)ってすごいね。テストしてもらったのりさんの環境の場合、スマートロケーションバーの検索対象になる物だけでも46000件近く、そうじゃない物も含めればきっともっと大量のレコードがあるのに、「並べ替え後の順番で任意の箇所を取り出す」のに1秒もかからないというのには驚いた。今までちゃんとしたデータベースを触ったことがなかったから、こんなの未知の世界だ。世界規模のデータベースと世界規模の処理環境に憧れてグーグルを目指す人の気持ちが、少しは分かったような気がする。

ロケーションバーでXUL/Migemo - Jul 01, 2008

こないだから少しずつ取り組んでる。

当初は皆目見当も付かなかったけど、Firefox 3 Hacksを書くために調べた知識が早速役に立って、Places APIを使ってる「履歴とブックマークの管理」と「ブックマーク」「履歴」の各サイドバーについてはワリとあっさり実装できた。

ロケーションバーについては残念ながら普通のAPIを使っておらず、オートコンプリートの実装の中でガチガチに書かれてて、まともにやっても手出しできない。OR検索さえできればMigemoが使えるのに。ということで、まず最初は、オートコンプリートのコントローラに複数の単語を順番に渡して結果のリストを生成させてそれを最後にまとめる、という事をやってみた。これは結構イイ線いったんじゃないかと思ったんだけど、履歴の件数が増えたりヒットした単語数が増えたりするとえらい事になってしまって、実用的な速度は出なかった。

そこで諦めて腹をくくって、無い知恵絞ってSQL文書いて、なるべく効率よく処理するようにしてみた。これで速度はだいぶ上がって、3文字以上入力した先あたりならかなりサクサク動くようにはなったんだけど、1文字2文字程度しか入力していないとやっぱりズシッと重い感じがある。

XPCOMのスレッドを作る機能を使ってみた所、Firefoxが落ちたし。

Firefox 3での変更点(重箱の隅を突く的な) - May 26, 2008

アナウンスに含まれてるかどうか知らないけどものっそ細かい所での変更点で僕が詰まった所。

  • Windowsにおいて、ドラッグ中でもタイマーが機能するようになった
  • nsIDocShellTreeItemのchildOffsetプロパティがなくなった

まず一つめ。Firefox 2まででは、何かオブジェクトをドラッグしてる間はsetTimeoutやsetIntervalで設定したタイマーが発火しないという問題があった(ドラッグ&ドロップ中のタイマーを擬似的に再現する)。これが、いつの時点からかFirefox 3ではWindowsでも普通にタイマーが動作するようになってた。セカンドサーチツリー型タブで「ドラッグ中に検索バー(タブ)の上でしばらく待つと〜」系の動作がうまく機能しなくなっていたのは、これが原因。Firefox 3以降ではWindows環境でも普通にタイマーを使うようにコードを書き換えたらうまく動くようになった。

二つめ。 個々のフレームに対応するnsIDocShellのオブジェクトを取得するなどで触れたnsIDocShellTreeItemインターフェースについて、Firefox 3ではchildOffsetプロパティがなくなってしまった。XUL/MigemoでFirefox 3においてフレームを跨いだ検索ができなくなっていたのはこれが原因(今フォーカスしてるフレームの次のフレーム、を取得するためにこのプロパティを使っていた)。かっこ悪いけど、親フレームの子フレームリストを取ってきてループ回してchildOffsetに相当する値を算出するようにしたらうまく動くようになった。

なんで今更になってこんな所(後者)をいじったんだか……

Safari風アニメーションの実現方法と、クリック時にスクリーンを表示しない設定について - May 06, 2008

検索がヒットした箇所をハイライト表示していて「次候補」「前候補」を辿る時、フォーカスした要素をアニメーションで強調表示させる機能について、今までは簡易的な実装としてspan要素をposition:relativeにしてtopプロパティをいじることで「ぴょこん」とジャンプするような効果を付けてたんだけど、0.8.4でパクリ元のSafariと同じようなアニメーション効果(フォーカスされた箇所が一瞬拡大される)にするようにした。まああくまで擬似的な物なんですが。

検索にヒットした箇所にアニメーション効果を表示するやり方としては、canvasを使う方法など色々考えられましたが、思いつく限り最も単純なやり方で、要素をコピーして絶対配置するという方法で実装しました。これはText Shadowで折り返されたテキストに影を付ける方法を考えた時に思いついた手法の応用で、こんな風にしてます。

前のテキスト
<span style="position: relative;">
  ハイライトされたテキスト
  <span style="position: absolute;
               top: -0.2em;
               bottom: -0.2em;
               left: -0.2em;
               right: -0.2em;
               font-size: 1.02em;">
    ハイライトされたテキスト<!-- 複製されたノード -->
  </span>
</span>
後のテキスト

position: relativeなインライン要素の中にposition: absoluteな要素を置くと絶対配置の基準がそのインライン要素になる、というCSSのポジショニングの特性を利用して、同じ位置に配置しています。また、top/bottom/left/rightの各プロパティにマイナスの値を設定することで、四辺が親のボックスより大きくなるズームっぽい効果が得られます。フォントサイズも一応いじってますが、あんまり分かりませんね。

手抜きなので、折り返された語句は正しく表示できません。あと、Safariみたいにアニメーションが終わった後もその箇所を特別に強調する、という効果は付けてません(そのうちやるつもり)。

もう一つ、0.8.5での改良点。0.8.2からSafari風強調表示を有効にした状態で半透明のスクリーンの下に隠れているリンクなどをクリックした時にクリックイベントを再送するようにしましたが、この半透明のスクリーンはクリックすると消えてしまうため、ミドルクリックなどで新しいタブでリンクを開いた時にも強調表示が解除されてしまうという欠点がありました。そこで、強調表示を解除しない例外的な操作の設定(正確には「この操作だった場合は一度消した強調表示を自動的に再表示する」という機能なんですが)をできるようにしてみました。

デフォルトでは、ミドルクリック、Ctrl-左クリック(リンクを新しいタブで開く)、Alt-クリック(リンク先を保存)、Shift-クリック(リンクを新しいウィンドウで開く)あたりの操作に対して、強調表示の状態を維持するように設定してあります。他のアドオンを使ってすべてのリンクを常に新しいタブで開いているようにしているから、そういうケースでも強調表示を解除しないようにしたい、という場合には設定をabout:configあたりで編集する必要があります。

この動作を決めている設定はxulmigemo.highlight.hideScreen.restoreButtonsという文字列型の設定です。値は「1,0+1,0+2,0+4,0+8,0+6,0+12」という風なカンマ区切りのリストになっていて、一つ一つが「この場合には強調表示を維持する」という場合の指定になっています。例えば「1」は「ミドルクリック」、「0+2」は「Ctrl-左クリック」を意味しています。プラス記号の左側はボタン番号(0=左クリック、1=ミドルクリック、2=右クリック)で、プラス記号およびその右側の数字はモディファイアキーの指定です(このパートは省略可能)。

モディファイアキーはnsIDOMNSEventの定数プロパティで定義されているフラグで指定します。Altキーは1、Ctrlキーは2、Shiftキーは4、Metaキー(MacのCommandキー)は8で、複数のキーを同時押しした場合を指定するにはそれぞれの数値を足した数を指定します。例えば「0+6」と書いた場合、プラス記号の右側の6は2と4の合計なので、「Ctrl-Shift-左クリック」の意味になります。このフラグ指定の意味がよく分からないという人はビット演算の話を見て下さい。

ハイライト表示のためにテキストフィールド内に挿入されたspan要素が邪魔になる件 - May 04, 2008

前のエントリの続き。

Safari風ハイライトに限らず元々、Firefoxの検索での「すべて強調表示」では、背景色と文字色を指定したspan要素を検索がヒットした箇所に動的に埋め込むという形で、ハイライト表示を実現している。これはinput要素やtextarea要素の場合でも全く同じ。実はFirefoxではテキスト入力欄もすべて、内部的には編集可能なHTMLとして実装されていて、それ故にspan要素の埋め込みも可能になっている。

ただ、この時span要素が埋め込まれる先のDOMツリーはchildNodesとかのプロパティでは辿れない場所にあって、アクセスするにはこんな風にする必要がある。

var editable = content.document.getElementsByTagName('textarea')[0];
var nodesInEditable = editable
     .QueryInterface(Components.interfaces.nsIDOMNSEditableElement)
     .editor
     .rootElement
     .childNodes;

textareaでこれをやってみると、改行が内部的にはbr要素で表現されているとかそういうのも見て取れる。nsIFindで検索する時はテキストフィールド内のこうした「隠しDOMツリー」も普通に検索対象になるようで、span要素を埋め込む時も特に変わったことはしなくていいようだ。

しかし、ここで一つ問題がある。こうしてテキストフィールド内に普通のspan要素を埋め込んでしまうと、その要素は、選択も内部の文字の編集もできない、ワープロでいえば埋め込まれた画像みたいな状態になってしまう。普段は特に意識せずに済むけど、常に強調表示を有効にするようにしていると当然テキストフィールド内でハイライト表示が行われることになる場合も多くなり、この問題が目につくようになってくる。というか僕自身がテスト用ドキュメントでテストしていて、いいかげんウザくなってきたのでなんとかしたかった。

理想的には、テキストフィールドにフォーカスされた時に自動的に強調表示を解除するという風な挙動にできるとよかったんだけど、試してみるとどうもなかなか大変そうだということが分かった。挿入されるspan要素にonclickなどの属性でイベントハンドラを設定してみたところ、getAttributeやdispatchEventなどのメソッド、あるいはparentNodeなどのプロパティを参照しようとするとパーミッションエラーが表示されてしまった。これではnode.parentNode.removeChild()という風なことができないし、独自イベントを発行してChrome領域のスクリプトに後の処理を任せるということもできない。

これについては幸いにも、createRangeなどの機能は使うことができるようだったので、deleteContentsやextractContentsを使って自分自身を削除させるようにはできた。強調表示された箇所を選択して「選択範囲のソース」を表示してみれば、こんな風になっていることが分かると思う。

<h1>B.B.S. <span class="sub"><span onmousedown="
  try {
    var xpathResult = this.ownerDocument.evaluate(
        'ancestor::*[contains(&quot; INPUT input TEXTAREA textarea &quot;, concat(&quot; &quot;, local-name(), &quot; &quot;))]',
        this,
        null,
        XPathResult.FIRST_ORDERED_NODE_TYPE,
        null
      );
    if (!xpathResult.singleNodeValue) return;
  }
  catch(e) {
    // permission denied, then this is in the input area!
  }
  var range = document.createRange();
  range.selectNodeContents(this);
  var contents = range.extractContents(true);
  range.selectNode(this);
  range.deleteContents();
  range.insertNode(contents);
  range.detach();
" id="__firefox-findbar-search-id" style="...">掲示板</span></span></h1>

無駄とは分かっていても一応、正攻法の判別処理も入れてある。テキストフィールド以外の部分に挿入されたspanにも全部このイベントハンドラが設定されてしまうのは、挿入先に応じて挿入する内容を変えるのがめんどかったから。まあとりあえず、これがあっても正常に動かなくなるわけではないし、別にいいかなと。

これだとキーボード操作に対して反応させることができない(そもそもカーソルをspanの中に移動できない)し、本当は特にクリック等の操作をしなくても、テキストフィールドにフォーカスが当たった時点で強調表示を解除するという風な挙動を実現したかったわけで、そこら辺まだまだ改善の余地はある。

XUL/MigemoでのSafari風ハイライト処理で、要素の下にあるリンクにクリックイベントを送る - May 04, 2008

一つ前のエントリの続き。

「すべて強調表示」の時に強調箇所以外を暗くするというSafari風ハイライト表示は、元はSafariHighlightを取り込ませてもらったものなんだけれども、基本的には、「画面の最全面に半透明の黒いボックスを表示して全体を覆う」「その上にz-indexを調整して強調箇所を浮かび上がらせる」という二つの操作が鍵になっている。で、このうち前者の方の操作のせいで、「暗くなった所にあるリンクをクリックしても、ハイライトが解除されるだけで、リンク先には飛べない」という問題が起こっていた。

まあ、問題というか実装上そうならざるを得ないという感じで、本家のSafariもこういう仕様だったと思うからまあいいじゃんと思わなくもないんだけど、要望はあるようなので対応しなきゃなーと思ってた。で、この度晴れて対応してみた。highlight.jsの最後の方に付け加えたresendClickEventメソッドがそれ。

resendClickEvent : function(aEvent) 
{
  var utils = aEvent.view
    .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
    .getInterface(Components.interfaces.nsIDOMWindowUtils);
  if ('sendMouseEvent' in utils) { // Firefox 3
    var flags = 0;
    const nsIDOMNSEvent = Components.interfaces.nsIDOMNSEvent;
    if (aEvent.altKey) flags |= nsIDOMNSEvent.ALT_MASK;
    if (aEvent.ctrlKey) flags |= nsIDOMNSEvent.CONTROL_MASK;
    if (aEvent.shiftKey) flags |= nsIDOMNSEvent.SHIFT_MASK;
    if (aEvent.metaKey) flags |= nsIDOMNSEvent.META_MASK;
    window.setTimeout(function(aX, aY, aButton) {
      if (ZoomManager.useFullZoom) { // Firefox 3のフルズームへの対応
        aX = aX * ZoomManager.zoom;
        aY = aY * ZoomManager.zoom;
      }
      utils.sendMouseEvent('mousedown', aX, aY, aButton, 1, flags);
      utils.sendMouseEvent('mouseup', aX, aY, aButton, 1, flags);
    }, 0, aEvent.clientX, aEvent.clientY, aEvent.button);
  }
  else { // Firefox 2, emulation
    var args = [
        'click',
        aEvent.bubbles,
        aEvent.cancelable,
        aEvent.view,
        1,
        aEvent.screenX,
        aEvent.screenY,
        aEvent.clientX,
        aEvent.clientY,
        aEvent.ctrlKey,
        aEvent.altKey,
        aEvent.shiftKey,
        aEvent.metaKey,
        aEvent.button
      ];
    window.setTimeout(function(aSelf, aFrame, aX, aY) {
      var node = aSelf.getClickableElementFromPoint(aFrame, aX, aY);
      if (!node) return;
      var event = aFrame.document.createEvent('MouseEvents');
      args.push(node);
      event.initMouseEvent.apply(event, args);
      node.dispatchEvent(event);
      if ('focus' in node) node.focus();
    }, 0, this, aEvent.view, aEvent.screenX, aEvent.screenY);
  }
},

getClickableElementFromPoint : function(aWindow, aScreenX, aScreenY) 
{
  var accNode;
  try {
    var accService = Components.classes['@mozilla.org/accessibilityService;1']
              .getService(Components.interfaces.nsIAccessibilityService);
    var acc = accService.getAccessibleFor(aWindow.document);
    var box = aWindow.document.getBoxObjectFor(aWindow.document.documentElement);
    accNode = acc.getChildAtPoint(aScreenX, aScreenY);
    accNode = accNode.QueryInterface(Components.interfaces.nsIAccessNode).DOMNode;
  }
  catch(e) {
  }

  var filter = function(aNode) {
    switch (aNode.localName.toUpperCase()) {
      case 'A':
        if (aNode.href)
          return NodeFilter.FILTER_ACCEPT;
        break;
      case 'INPUT':
      case 'TEXTAREA':
      case 'BUTTON':
      case 'SELECT':
        return NodeFilter.FILTER_ACCEPT;
        break;
    }
    return NodeFilter.FILTER_SKIP;
  };

  if (accNode &&
    accNode.nodeType == Node.ELEMENT_NODE &&
    filter(accNode) == NodeFilter.FILTER_ACCEPT)
    return accNode;

  var doc = aWindow.document;
  var startNode = accNode || doc;
  var walker = aWindow.document.createTreeWalker(startNode, NodeFilter.SHOW_ELEMENT, filter, false);
  for (var node = walker.firstChild(); node != null; node = walker.nextNode())
  {
    var box = doc.getBoxObjectFor(node);
    var l = box.screenX;
    var t = box.screenY;
    var r = l + box.width;
    var b = t + box.height;
    if (l <= aScreenX && aScreenX <= r && t <= aScreenY && aScreenY <= b)
      return node;
  }
  return null;
},

見ての通り、Firefox 3ではnsIDOMWindowUtilsの新機能を使ってイベントを発行するようにして根本的解決を図り、Firefox 2ではTabScope由来のコードにnsIAccessibleを組み合わせたものを使ってそれっぽいことをしている、という感じです。どっちもタイマーを使って処理を遅らせているのは、画面全体を覆っているスクリーンが消えてからイベントを発行しないと、もう一度スクリーンをクリックしたのと同じ事になってしまうから。

最初、nsIDOMWindowUtilsのsendMouseEventでclickイベントを発行しようとしてうまくいかなかったんだけど、どうやらこれはDOMのイベントをそのまま発行するわけではなく、本当の本当にユーザの操作をエミュレートするという物みたいだ。間を開けずmousedown→mouseupと実行するとクリックしたのと同じ事になる。

強調表示関係ではこのほかに、テキストフィールド内の強調を自動的に解除するようにするための改良もあります。これも長くなったから別エントリで。

XUL/Migemoのリファクタリングとか改善とか - May 04, 2008

pXMigemoFind(pIXMigemoFindの実装)、特にfindメソッドまわりを中心にだいぶ書き直した。といってもアルゴリズム的には変わってなくて、主にメンテナンス性を向上することを目的にした書き換えです。こういうのもリファクタリングと言っていいんでしょうか。

findInDocumentメソッドがかなり長くて中のループのネストも深くなってたので、これをだいぶ細かく分けた。

まず、フレームのツリーを辿る処理がループの最後の方にどかっとあったので、これを取り出してIteratorパターンのヘルパーオブジェクトとして実装することにした(最後の方にあるDocShellIteratorというやつ)。最初Iteratorパターンというのを知った時には「こんなの何の役に立つんだ?」とか「配列みたいに長さを取り出せた方が便利じゃね?」とか思ってたけど、こうして実際に書いてみると、「次に処理対象にする物を探す」部分だけに特化して作り込めるので便利だ、ということがよく分かった。

具体的には、今までは前方検索と後方検索それぞれの場合で「一つ前のフレーム」に処理を移すのか「一つ後のフレーム」に処理を移すのかをいちいち判別してたんだけど、その判別を行う部分まで含めてDocShellIteratorとして分離することで、findInDocumentの側では何も考えずにDocShellIteratorのiterateNextメソッドを呼べば適切な結果が帰ってきてウマーという風にできるようになった。

実際のコードで見ると、findInDocument側は

docShell = this.getDocShellForFrame(doc.defaultView)
  .QueryInterface(Components.interfaces.nsIDocShellTreeNode);

if (aFindFlag & this.FIND_BACK) { // back
  docShell = this.getPrevDocShell(docShell);
  if (!docShell) {
    if (!(aFindFlag & this.FIND_WRAP)) {
      docShell = this.getDocShellForFrame(doc.defaultView.top);
      docShell = this.getLastChildDocShell(docShell.QueryInterface(Components.interfaces.nsIDocShellTreeNode));
      doc = docShell
        .QueryInterface(Components.interfaces.nsIDocShell)
        .QueryInterface(Components.interfaces.nsIWebNavigation)
        .document;
      this.document.commandDispatcher.focusedWindow = docShell
        .QueryInterface(Components.interfaces.nsIDocShell)
        .QueryInterface(Components.interfaces.nsIWebNavigation)
        .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
        .getInterface(Components.interfaces.nsIDOMWindow);
      if (
        !editableInOut ||
        findRange.sRange.startContainer == aDocument.body ||
        findRange.sRange.startContainer == aDocument.documentElement
        )
        aFindFlag |= this.FIND_WRAP;
      continue;
    }
    this.dispatchProgressEvent(found, aFindFlag);
    break doFind;
  }
}
else { // forward
  docShell = this.getNextDocShell(docShell);
  if (!docShell) {
    if (!(aFindFlag & this.FIND_WRAP)) {
      doc = Components.lookupMethod(doc.defaultView.top, 'document').call(doc.defaultView.top);
      this.document.commandDispatcher.focusedWindow = doc.defaultView.top;
      if (
        !editableInOut ||
        findRange.sRange.endContainer == aDocument.body ||
        findRange.sRange.endContainer == aDocument.documentElement
        )
        aFindFlag |= this.FIND_WRAP;
      continue;
    }
    this.dispatchProgressEvent(found, aFindFlag);
    break doFind;
  }
}
doc = docShell
    .QueryInterface(Components.interfaces.nsIDocShell)
    .QueryInterface(Components.interfaces.nsIWebNavigation)
    .document;
if (doc == aDocument) {
  this.dispatchProgressEvent(found, aFindFlag);
  break doFind;
}

こーんなだったのが

aDocShellIterator.iterateNext();

if (aDocShellIterator.wrapped) {
  if (!(aFindFlag & this.FIND_WRAP)) {
    this.document.commandDispatcher.focusedWindow = aDocShellIterator.view;
    if (
      !editableInOut ||
      !rangeSet ||
      aDocShellIterator.isRangeTopLevel(rangeSet.range)
      )
      aFindFlag |= this.FIND_WRAP;
    continue;
  }
  this.dispatchProgressEvent(aFindFlag, resultFlag);
  break;
}

if (aDocShellIterator.isInitial) {
  this.dispatchProgressEvent(aFindFlag, resultFlag);
  break;
}

ここまでスッキリしました。まあその代わり、フレームのツリーを辿る処理(DocShellIteratorの方がちょっと長くなってしまったんだけど、それぞれロジック的には綺麗に分かれているから、今後手を入れる時は「ツリーを辿ること」と「検索すること」のどっちか一方のことだけに集中して作業できるわけで、これこそがイテレータパターンの最も大きな意義だったんですね。デザインパターン万歳。

あと、findInDocumentの中でループの中でループを回していた部分をfindInDocumentInternalメソッドとして分離した。これはロジックを分離する効果はなくて、単にネストを浅くしてコードを見やすくするためだけの変更。のつもりだったんだけど、これをうまくやるために、他の部分も含めてだいぶ手直ししないといけなかった。

何故かというと、単純に内側のループだけをfindInDocumentInternalとして分離したところで、元のfindInDocumentとfindInDocumentInternalとの間でお互いに引き渡さないといけない情報(引数として引き渡す情報と、返り値として戻す情報の両方)が多すぎて、そのまま二つに分けただけだと却って分かりにくくなってしまったから。普通にreturnするだけじゃ情報を渡しきれないから、オブジェクトを引数で渡して、そのオブジェクトのプロパティとして返り値を設定して、変更されたプロパティを呼び出し元の側でまた参照して……という感じになってしまって、せっかくメソッドを分けたのにそこの所でガッツリ繋がってしまってて、これじゃ分けた意味がないよと。

というわけで最低限必要な情報だけに絞り込んでやりとりするように設計を検討したのに加えて、インターフェースの定義も見直して、今まであんまり有効活用してなかったビット演算を多用するようにしたことで、findInDocumentInternalからの返り値は最終的にビット列一つだけにまでまとめることができた。ビット演算いいよビット演算。

ビット演算を有効に利用するためには、どの意味をどのビットに与えるかというのをよく考えておかないといけない。とかなんとか大仰な言い方をしてみたけど、要するに、適当に定数プロパティを名前順に並べて先頭から0, 1, 2, 4, 8……と値を割り振っていくなんていいかげんな事してちゃ駄目だよってことですね。0.8.1まででは


const unsigned short FOUND             = 0;
const unsigned short NOTFOUND          = 1;
const unsigned short WRAPPED           = 2;
const unsigned short NOTLINK           = 4;
const unsigned short FOUND_IN_EDITABLE = 8;

こんなだったけど、これじゃあ実質的にはただの定数値が並んでるだけで単純比較にしか使えない。FOUNDが0だったり「検索はヒットしたけどリンクじゃないからヒット無しとみなす」という意味のNOTLINKなんてのがあったり。一つの値に複数の意味が割り当てられてるんじゃ、重複しないビットを割り当てても意味がないわけです。


const unsigned short NOTFOUND          = 0;
const unsigned short FOUND             = 1;
const unsigned short WRAPPED           = 2;
const unsigned short FOUND_IN_LINK     = 4;
const unsigned short FOUND_IN_EDITABLE = 8;

こーいう風にしておけば、NOTLINKに相当する場合は「FOUND | WRAPED」、そうでない場合には「FOUND | FOUND_IN_LINK」とかそんな風に書けるわけで、これなら「普通にヒットした」「普通にヒットしたけど一度ページの末尾まで検索してもう一度頭からやり直した」「入力欄の中でヒットした」などなどいろんな場合もひっくるめて全部「flag & FOUND」というビット演算いっこで判定できる。

あとSafari風強調表示の改善もあるんだけど、長くなるから別のエントリに分けます。

Page 3/239: « 1 2 3 4 5 6 7 8 9 »

Powered by blosxom 2.0 + starter kit
Home

カテゴリ一覧

過去の記事

1999.2~2005.8

最近のつぶやき

オススメ

Mozilla Firefox ブラウザ無料ダウンロード