Jan 20, 2007

Split Browser開発のよもやま話(5):分割されたブラウザの削除

いつまで続くんだこの話。やっと開発一日目最後のところだよ。コード書くのは手っ取り早いけど文章書くのってだりい。ドキュメント整備が遅れるのは世の常ですね。

ええと。とりあえず「分割」する、ブラウザをどんどん増やす方向については前回までのところでできたんで、次は追加したブラウザを削除する方向の処理の話です。

例として、「一回右に分割して、さらにその分割されたブラウザを下に分割する」という場合を想定してみよう。 (配布ページのスクリーンショットみたいな状態を想定)

これはDOM操作的には、hboxにsubbrowser-containerを入れて、さらにその中のvboxにsubbrowser-containerを入れる、という感じになる。タグで書いたらこんな感じだ。


<subbrowser-container>
  <vbox>
    <hbox>
      <box anonid="wrapper">
        <tabbrowser/> <!-- 左 -->
      </box>
      <splitter/>
      <subbrowser-container>
        <vbox>
          <hbox>
            <box anonid="wrapper">
              <subbrowser/> <!-- 右上 -->
            </box>
          </hbox>
          <splitter/>
          <subbrowser-container>
            <vbox>
              <hbox>
                <box anonid="wrapper">
                  <subbrowser/> <!-- 右下 -->
                </box>
              </hbox>
            </vbox>
          </subbrowser-container>
        </vbox>
      </subbrowser-container>
    </hbox>
  </vbox>
</subbrowser-container>

右下のブラウザを削除する場合はどうするか? これは分かりやすい。subbrowserの親の親の親の親にあたるsubbrowser-container要素を削除すればOKだ。また、左の部分、つまり最初からFirefoxにあるブラウザは削除できないので、ここも問題無いといえば問題無い。

難しいのは、右上の位置にあるブラウザ……何らかの子ブラウザを抱えている、中間の階層のブラウザを削除する場合だ。

  • 親の親の親の親にあたるsubbrowser-containerを削除すると、右下の位置のブラウザである子ブラウザまで消えてしまう。かといってsubbrowser要素だけ消してしまうと、空っぽのボックスが残ってしまう。(スクリーンショット)
  • subbrowserの直上にあるbox[anonid="wrapper"]ごと削除すればいいんだけど、これはsubbrowser-containerの中にある匿名内容なので、subbrowserのparentNodeプロパティではアクセスできない(subbrowser-conttainerにアクセスしてしまう)。
  • だいたい、ブラウザの削除処理でbox[anonid="wrapper"]だけ削除したところで、今度はsubbrowser-containerからhboxまでが丸々残ってしまうから、結局subbrowser要素だけ消した時と同じような空っぽのボックスが残ってしまう。
  • hboxとその隣のsplitterを削除したとしても、 subbrowser-container > vbox > subbrowser-container > vbox > hbox > box[anonid="wrapper"] > subbrowser(これが右下のブラウザ)という構造になって、subbrowserから見て一番近い祖先のsubbrowser-containerの上の階層に、無駄なsubbrowser-containerとvboxが残ってしまう。

……という所でハマりにハマってしまい、2~3回構造や処理手順を変えてああでもないこうでもないとテストと試行錯誤を繰り返していた。

試してはみたものの結局うまくいかなかったやり方の一つに、DOM2 Rangeを使うやり方があった。

  1. box[anonid="wrapper"]の中にあるブラウザを閉じようとした場合、その親のhboxの内容でbox[anonid="wrapper"]以外を選択してextractContents()で抜き出す。
  2. 抜き出した内容を、subbrowser-containerの親になっている要素の子として埋め込みなおす。
  3. 同様に、vboxの内容でhbox以外を選択してextractContents()で抜き出し、subbrowser-containerの親になっている要素の子として埋め込みなおす。
  4. vboxもhboxも空っぽになったsubbrowser-containerを、削除する。

右上のブラウザを削除する場合なら、


var range = document.createRange();

range.selectNodeContents(hContainer);
range.setEndBefore(wrapper);
var before = range.extractContents();

range.selectNodeContents(hContainer);
range.setStartAfter(wrapper);
var after = range.extractContents();

parentContainer.hContainer.insertBefore(before, parentContainer.wrapper);
parentContainer.hContainer.appendChild(after);

みたいな感じで。でもこれだと、前の前のエントリで書いたように、extractContents()で内容をDOMツリーから切り離した瞬間にすでにあったbrowser要素の内容がぶっ壊されてしまって、再び埋め込んだときに空のbrowserになってしまう。それに、そもそも、こうして上の階層に内容を移動するとして、どこに入れれば一番自然かは場合によって全然異なるだろう。

この段階になって、box[anonid="wrapper"]の中にXBLのchildren要素を置いて通常の内容をそこに入れることに何も全然全くメリットが無いということにやっと気が付いた(children要素を置かずに、匿名内容だけで完結したものにする、という発想がそれまで自分に無かった)ので、構造を若干変えることにした。もうchildren要素を使った「匿名内容じゃない内容」として任意の位置にボックスを置く事は諦めて、おとなしくsubbrowserも匿名内容にしてしまっていいじゃないか、と。そうすればparentNodeプロパティで普通にhboxにアクセスできるようになるし。

そんな風にあれこれ試しているうちに、ジョージ・ジョースター1世が心の中で僕に語りかけてきた。「なにジョジョ? 無駄なボックスが残ってしまって困る(さっき挙げた4つの問題の4番目)? 逆に考えるんだ、無駄なボックスを残してもいいさと考えるんだ」と。

おお、エウレーカ!! これぞまさしく逆転の発想だ。無駄なボックスが存在していても他の処理(更なるブラウザの追加や状態の保存、削除など)に影響が出なくて、外見上も特に変化が無い(subbrowser-containerもvboxもhboxも、特に何もスタイルを設定していないし)なら、そのボックスを無理して消す必要は無いじゃないか。無駄なボックスを含んだままずっと動かし続けておいて、後になってsubbrowser要素を削除してsubbrowser-containerの内容がいよいよ空になってから、そのボックスを削除すればいいじゃないか。多少メモリの浪費にはなるけど、まあそのくらい大目に見てくださいよと。

DOMの構造で例示すると、


<subbrowser-container>
  <vbox>
    <hbox>
      <tabbrowser/> <!-- 左 -->
      <splitter/>
      <subbrowser-container>
        <vbox>
          <hbox>
            <subbrowser/> <!-- 右上 -->
          </hbox>
          <splitter/>
          <subbrowser-container>
            <vbox>
              <hbox>
                <subbrowser/> <!-- 右下 -->
              </hbox>
            </vbox>
          </subbrowser-container>
        </vbox>
      </subbrowser-container>
    </hbox>
  </vbox>
</subbrowser-container>

この部分を削除した後に


<subbrowser-container>
  <vbox>
    <hbox>
      <tabbrowser/> <!-- 左 -->
      <splitter/>
      <subbrowser-container>
        <vbox>
          <subbrowser-container>
            <vbox>
              <hbox>
                <subbrowser/> <!-- 右 -->
              </hbox>
            </vbox>
          </subbrowser-container>
        </vbox>
      </subbrowser-container>
    </hbox>
  </vbox>
</subbrowser-container>

こんな風に無駄なボックスが残ってしまうけれども、それは敢えて放置する。その上で、その中のsubbrowserを削除した後になって


<subbrowser-container>
  <vbox>
    <hbox>
      <tabbrowser/>
      <splitter/>
      <subbrowser-container>
        <vbox>
        </vbox>
      </subbrowser-container>
    </hbox>
  </vbox>
</subbrowser-container>

こんな風に本当に空っぽのボックスだけが残った時点で、その空のボックスを削除すればいいわけ。

これを実現するために、subbrowserを削除する処理と、それによってできた空のボックスを削除する処理とを、別々のメソッドに分けてみた。

  1. subbrowserを削除するメソッドを実行した時に、空のボックスを削除するメソッドも一緒に実行する。
  2. subbrowserの削除後に残った空のボックスを、直上のsubbrowser-containerまで遡って探して削除する。
  3. もしsubbrowser-containerの中身が完全に空なら、そのsubbrowser-container(と、その前後にあるsplitter)を削除する。
  4. そのsubbrowser-containerの親にあたるボックスについて、さらに空のボックスを遡って探す。以下、トップレベルのsubbrowser-containerまで、階層を遡りながら繰り返し。

2~4の「空のボックスを削除する」処理だけを再帰的に繰り返すことで、空になったボックスを漏れなく削除することができる。

やったよママン!

ところで、subbrowserを削除する処理というのはどこのオブジェクトのメソッドにするのがいいんだろう。subbrowser自身にメソッドとして持たせる? それともsubbrowser-containerのメソッド? でもどうやってそのsubbrowserに対する削除要求が発生したことを検知すればいいんだ?

やり方は色々あると思うけれども、僕は今回は、DOM2 Eventとサービスオブジェクトを使ったやり方を取ることにした。

  • subbrowserを削除する処理も空のボックスを削除する処理も、どっちもSplit Browser用のサービスを提供する単一のオブジェクトにメソッドとして持たせる。
  • そのサービスオブジェクトを、Firefox起動時にルート要素にイベントリスナとして登録しておく。
  • subbrowser要素の「閉じる」ボタンをクリックしたときには、「このsubbrowserを削除したいという要求が来てるよ」ということを伝えるイベントだけを発生させ、後は何もしない。
  • サービスオブジェクトがイベントリスナとしてイベントを検知した瞬間に、サービスオブジェクトが、削除要求を出されたsubbrowser要素を削除する。その後、空になったボックスを削除する処理も行う。

コードで示すと、こんな感じ。まずはsubbrowserの中のクローズボックス。


<!-- subbrowser要素の匿名内容の定義 -->
<xul:toolbarbutton
    class="tabs-closebutton toolbarbutton-1 chromeclass-toolbar-additional"
    oncommand="
      var node = this;
      while (node.localName != 'subbrowser') node = node.parentNode;
      node.close();
    "/>
...
<!-- subbrowser要素のメソッドの定義 -->
<method name="close">
  <body><![CDATA[
    var event = document.createEvent('Events');
    event.initEvent('SubBrowserRemoveRequest', false, true);
    this.dispatchEvent(event);
  ]]></body>
</method>

次に、サービスオブジェクト。これは他にも色々機能持たせてるんだけど、とりあえず削除処理に関係してるとこだけ。


var SplitBrowser = {
      ...
      handleEvent : function(aEvent)
      {
        switch (aEvent.type)
        {
          case 'load':
            this.init();
            break;

          case 'SubBrowserRemoveRequest':
            this.removeSubBrowser(aEvent.originalTarget);
            break;
        }
      },

      init : function()
      {
        document.documentElement.addEventListener(
          'SubBrowserRemoveRequest', this, false);
        window.removeEventListener('load', this, false);
      }
    };

window.addEventListener('load', SplitBrowser, false);

というわけで、これでやっと必要最低限の機能が出揃って、「ブラウザを無限に分割できる」拡張機能としての体裁が整ってきました、というところで開発一日目はおしまい。まぁ実際には、試行錯誤の途中で寝ちゃって、目が覚めた朝に先の決定版の処理を思いついて実装したんだけど。

実用性を高めるための機能の作り込みの話に、まだまだ続くのです。

エントリを編集します。

wikieditish message: Ready to edit this entry.











拡張機能