Jan 21, 2007

Split Browser開発のよもやま話(6):分割されたブラウザの作り込み

1日目でとりあえず必要最低限の機能は揃ったので、2日目は作り込みのフェーズに入った。

最初の方で作った分割後の領域用のsubbrowserウィジェットは、ブラウザとしてまともな機能が全然無い状態だった。「戻る」や「進む」などのボタンは一応あったけど履歴が何もないときでも常時押せる状態だったし、faviconもページのタイトルも何も表示されないような物。これではさすがに話にならない。

subbrowserの中に何を入れるかなんだけど、今後のことはともかくとして、最初に公開するバージョンでは、「戻る」「進む」「更新」「中止」の4つのボタンと「ロケーションバー」+「移動」ボタン、「Webサイトのアイコン(favicon)」、読み込み状況を示す「プログレスバー」、それから前のエントリでもチラッと出てきたけど「閉じる」ボタン。これだけ設けておくことにした。

ただ、これだけの要素をあの狭いスペースにそのまま置くとだいぶアレなことになるので、Safariを参考に、「更新」と「中止」のボタン、それから「プログレスバー」と「ロケーションバー」はそれぞれ一つにまとめたんだけど。前者はCSSでの指定、後者はXULのスタックによって実現できた。

「更新」と「中止」は、よくよく見るとお互いにどっちかが有効な時はもう一方が無効になっている。ということは、無効な方は隠しちゃってもいいだろう、と。CSS2の属性セレクタあたりを使えば、これは簡単に実現できる。


.subbrowser-toolbar-item.button.reload[disabled="true"],
.subbrowser-toolbar-item.button.stop[disabled="true"] {
   visibility: collapse;
}

たったこれだけ。同じテクニックがuserChrome.cssによるFirefoxのツールバーボタンのカスタマイズでも使える。

ロケーションバーの方は、Firefoxのロケーションバーの作りを参考にした。


<xul:hbox class="subbrowser-toolbar-item urlbar-box" flex="1">
  <xul:stack flex="1">
    <xul:progressmeter class="subbrowser-toolbar-item urlbar-progress" flex="1"/>
    <xul:textbox class="subbrowser-toolbar-item urlbar-textbox"
      flex="1" type="autocomplete">
      <xul:box class="subbrowser-toolbar-item urlbar-favicon-box">
        <xul:image class="subbrowser-toolbar-item urlbar-favicon"/>
      </xul:box>
    </xul:textbox>
   </xul:stack>
</xul:hbox>

基本的にはこんな感じ。スタックを使って、ロケーションバーのテキストボックスの下にプログレスメーターを重ねる。favicon表示用のimage要素はテキストボックスの子要素として置いておくと勝手に文字入力欄の左に挿入してくれるので、その仕組みをそのまんま使わせて貰った。

ただ、これだとテキストボックスの下に置かれたプログレスメーターが見えないので、そこをCSSで解決する。


/* テキストボックスを、透明で枠のないただのボックスにする */
.subbrowser-toolbar-item.urlbar-textbox {
  -moz-appearance: none;
  background: transparent;
  border: 0 none;
  margin: 0;
  padding: 0;
}

/* その上で、スタックの親要素をテキストボックス的に表示させる */
.subbrowser-toolbar-item.urlbar-box {
  background: -moz-field;
  color: -moz-fieldtext;
  border: 1px solid -moz-use-text-color;
}

/* 普通のプログレスメーターが文字に重なると
   テーマによっては文字が読めなくなるんで、
   スタイルを設定し直す */
.subbrowser-toolbar-item.urlbar-progress {
  -moz-appearance: none;
  border: none;
  outline: none;
  background: transparent;
  padding: 0;
}
.subbrowser-toolbar-item.urlbar-progress > .progress-bar {
  -moz-appearance: none;
  background: ThreeDShadow;
}

一部省略してるけど、要点はこういう事。テキストボックスやプログレスバーは、-moz-appearanceというプロパティによってプラットフォーム標準のGUI部品と同じような表示が指定されているだけなので、それをリセットしてやったり、あるいは今回は使ってないけど親要素に改めて-moz-apperance:textfieldとか指定してやるなんてことも可能なのです(これは検索ツールバーで使われてるテクニックですね)。

「戻る」「進む」のようなボタンの状態を必要に応じて変えるのには、browser要素のプロパティが使える。canGoBackcanGoForwardといったプロパティ(どちらも真偽値)を参照すれば、今は「戻る」機能自体が使えないから「戻る」は無効にしておこう、という風なことができる。

って、ちょっと待てよと。どうやってそのプロパティの値が変化したことを知ればいいんだよと。

いやまあ「戻る」「進む」あたりのボタンの状態だけ変化させるんだったらonlaodとかonunloadとかのイベントを拾うだけでいけるかもだけど、読み込みがちょっとずつ進行するのに合わせてプログレスバー(処理の進行状況を示す棒グラフみたいなアレ)の状態を変更することは、この方法だと不可能だ。

こういう時に使うのが、nsIWebProgressListenerという機能。これは、プログレスリスナとなるオブジェクトをbrowserに登録しておくことによって、読み込み状況や状態などが変化するタイミングでリスナにそれを通知して、情報を受け取ったリスナがGUIの状態を更新できる、というものだ。

プログレスリスナはnsIWebProgressListenerのインターフェースを備えたオブジェクトを自分で生成してやる事になる。大まかに書くとこんな感じになる。


var listener = {
  onProgressChange : function (aWebProgress, aRequest,
                               aCurSelfProgress, aMaxSelfProgress,
                               aCurTotalProgress, aMaxTotalProgress) {
    ...
  },
  onStateChange : function(aWebProgress, aRequest, aStateFlags, aStatus) {
    ...
  },
  onLocationChange : function(aWebProgress, aRequest, aLocation) {
    ...
  },
  onStatusChange : function(aWebProgress, aRequest, aStatus, aMessage) {
    ...
  },
  onSecurityChange : function(aWebProgress, aRequest, aState) {
    ...
  },
  QueryInterface : function(aIID) {
    if (aIID.equals(Components.interfaces.nsIWebProgressListener) ||
        aIID.equals(Components.interfaces.nsISupportsWeakReference) ||
        aIID.equals(Components.interfaces.nsISupports))
        return this;
    throw Components.results.NS_NOINTERFACE;
  }
};

こんな感じで、所定のメソッドを一通り揃えたオブジェクトを定義すればそれがリスナになる。既定の名前のメソッドがいくつかあって、それだけ色々なことができる(色んな変化に応じて処理を行える)し、他にも自由にメソッドやプロパティを持たせられる。今回はプログレスバーの状態を変えるためのonProgressChange()と、「戻る」「進む」などのボタンの状態を変えるためのonStateChange()がメインなので、他はあんまり使ってないけど。

このあたりのコードを書く上では、chrome://global/content/bindings/tabbrowser.xml(toolkit.jar内のcontent/global/bindings/tabbrowser.xml)が非常に参考になる。っていうかSplit Browserのコードの中にはこっからコピペした部分が結構ある。tabbrowser.xml自体が、tabboxその他のタブ関連ウィジェットとbrowserウィジェットの組み合わせによってタブブラウザを作るという、browserウィジェットの使い方のナイス見本なのです。いやーGPL/LGPL/MPLってイイですね。オープンソース最高。

以下、「戻る」「進む」とかのボタンの状態を交信するためのonStateChange()の書き方。


onStateChange : function(aWebProgress, aRequest, aStateFlags, aStatus) {
  const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener;
  if (aStateFlags & nsIWebProgressListener.STATE_START &&
      aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) {
      // 読み込み開始時の処理
  }
  else if (aStateFlags & nsIWebProgressListener.STATE_STOP &&
           aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) {
      // 読み込み完了時の処理
  }
},

このメソッドには3番目の引数として、現在の状態を示すフラグが渡される。Components.interfaces.nsIWebProgressListenerのプロパティとしていくつか定数が定義されてるので、それと照合することで今の状態が分かるわけだ。

あとonProgressChange()


onProgressChange : function (aWebProgress, aRequest,
                             aCurSelfProgress, aMaxSelfProgress,
                             aCurTotalProgress, aMaxTotalProgress) {
  // 読み込み済み状態の割合(0~1.0)
  var progress = aMaxTotalProgress ? aCurTotalProgress / aMaxTotalProgress : 0 ;
  var val      = parseInt(progress * 100);
  if (val && val < 100) {
    this.mBrowser.progress.removeAttribute('hidden');
    this.mBrowser.progress.setAttribute('mode', 'normal');
    this.mBrowser.progress.setAttribute('value', val);
  }
  else {
    this.mBrowser.progress.setAttribute('hidden', true);
  }
},

ここではmBrowserとかのプロパティが出てるけど、これはsubbrowser要素とかツールバーとかへの参照。プログレスバー(プログレスメーター)ウィジェットの使い方はXULチュートリアルあたりを参照のこと。

faviconの更新とかはもうほんとにtabbrowser.xmlからのコピペそのまんまなので割愛します。ホントにこのファイルにはbrowserを使うためのテクニックが詰まってるから、ブラウザ機能を持ったアプリケーションや拡張機能を作るのならいっぺん見とくといいよ。

次は分割の状態やtabbrowserの履歴とかの保存と復元の話に続く。

エントリを編集します。

wikieditish message: Ready to edit this entry.











拡張機能