Now browsing the SCRAPBLOG weblog archives.

xul:datepicker と xul:timepicker

日付や時刻を入力するための XUL ウィジェットが実装された(Trunk ビルド限定)。

xul:timepicker

datepicker の方は、 type=”popup” でポップアップ式のカレンダー、 type=”grid” とすることで埋め込み式のカレンダーから入力させることも可能。

xul:datepicker

詳しくは以下の Wiki のページかXBLのソースを参照
XUL:Specs:DateTimePickers – MozillaWiki
chrome://global/content/bindings/datetimepicker.xml

TOP

xul:scale

いつのまにか scale という新しい XUL のウィジェットが実装されていた(Trunk ビルド限定)。
max, min 属性で最小値と最大値、 increment 属性でつまみを動かしたときのステップ数を指定し、値のセット/ゲットは value プロパティ(または属性)を通じて行う。
また、 onchange イベントハンドラにより変化に応じた処理が可能である。

<label value="やる気:" />
<scale value="0"
       min="-100"
       max="+100"
       increment="10"
       onchange="this.nextSibling.value = this.value;" />
<textbox />

orient 属性を vertical とすることで、縦方向のスライダも可能。

xul:slider

詳しくは以下の Wiki のページかXBLのソースを参照
XUL:Slider Tag – MozillaWiki
chrome://global/content/bindings/scale.xml

TOP

[userChrome.js] 軽量マウスジェスチャ(モーダルダイアログに関する不具合修正)

これまでは、例えば「alert(“OK”);」のようなモーダルダイアログ(閉じないと親のウィンドウの操作ができないタイプのダイアログ)の表示をジェスチャに割り当てると、ダイアログを閉じたあとにコンテキストメニューが出てしまうバグがあった。やむを得ず、「setTimeout(function(){ alert(“OK”); }, 0);」のように時間差で処理を実行させるという策を講じていたが、さきほどコンテキストメニューが出てしまう原因を調べてみた。

すると、モーダルダイアログを表示することで一見親ウィンドウの処理が一時停止しているかのように見えるが、なぜかダイアログ上でマウスを動かすとmousemoveイベントが発生し、 mouseup→contextmenu というイベント発生の順序が崩れて mouseup→mousemove→contextmenu になり、結果的に不具合が生じることがわかった。

そこで、以下のように微修正。スクリプト全体はこちらにあります。

変更前:

...
// [3] ジェスチャ終了~アクション実行
if (this._state == 1) {
    this._state = this._directionChain ? 2 : 3;
    // ジェスチャが認識されれば、この時点でthis._stateは2になる。
    // _stopGestureメソッドが_performActionメソッドを呼び出し、
    // 下記_stopGestureから呼ばれる_performActionメソッドの中でモーダルダイアログを表示すると…
    this._stopGesture(event);
    // ダイアログ上をマウスが通過した際に余計なmousemoveイベントが発生し、
    // ここを通過するときにはthis._stateは0になってしまう。
}
...

変更後:

...
// [3] ジェスチャ終了~アクション実行
if (this._state == 1) {
    var state = this._directionChain ? 2 : 3;
    this._stopGesture(event);
    this._state = state;
}
...

この修正に伴い、

// プライバシー情報の消去
setTimeout(function(){ document.getElementById("Tools:Sanitize").doCommand(); }, 0);
// オプション
setTimeout(function(){ openPreferences(); }, 0);

はそれぞれ以下のようにしてもOKになります。

// プライバシー情報の消去
document.getElementById("Tools:Sanitize").doCommand();
// オプション
openPreferences();

TOP

[userChrome.js] 軽量マウスジェスチャで利用可能なスクリプト集

[userChrome.js] 軽量マウスジェスチャで利用可能なスクリプト

「○○をジェスチャにしたいんだけど、userChrome.jsには何て書けばいいの」といった話題はこちらでコメントお願いします。

ナビゲーション

// 戻る
document.getElementById("Browser:Back").doCommand();
// 進む
document.getElementById("Browser:Forward").doCommand();
// 更新
document.getElementById("Browser:Reload").doCommand();
// 更新(キャッシュを無視)
document.getElementById("Browser:ReloadSkipCache").doCommand();
// ホーム
document.getElementById("Browser:Home").doCommand();
// 中止
document.getElementById("Browser:Stop").doCommand();
// 巻き戻し(履歴の先頭へ戻る)
if (gBrowser.sessionHistory.index > 0)
    gBrowser.gotoIndex(0);
// 早送り(履歴の末尾へ進む)
var nav = gBrowser.webNavigation;
var hist = nav.sessionHistory;
nav.gotoIndex(hist.count - 1);

ウィンドウ

// 新しいウィンドウ
document.getElementById("cmd_newNavigator").doCommand();
// ウィンドウを閉じる
document.getElementById("cmd_closeWindow").doCommand();
// ウィンドウの最小化
window.minimize();
// ウィンドウの最大化/元のサイズに戻す
window.windowState == window.STATE_MAXIMIZED ? window.restore() : window.maximize();
// 全画面表示
document.getElementById("View:FullScreen").doCommand();

タブ

// 新しいタブ
document.getElementById("cmd_newNavigatorTab").doCommand();
// タブを閉じる
document.getElementById("cmd_close").doCommand();
// 閉じたタブを元に戻す
document.getElementById("History:UndoCloseTab").doCommand();
// [Tab Mix Plus] 最後に閉じたタブを復元(TMP独自のセッションマネージャを利用している場合)
gBrowser.undoRemoveTab();
// 前のタブへ
gBrowser.mTabContainer.advanceSelectedTab(-1, true);
// 次のタブへ
gBrowser.mTabContainer.advanceSelectedTab(+1, true);
// 文字を小さくする
document.getElementById("cmd_textZoomReduce").doCommand();
// 文字を大きくする
document.getElementById("cmd_textZoomEnlarge").doCommand();
// 標準の文字サイズ
document.getElementById("cmd_textZoomReset").doCommand();
// タブの複製
openNewTabWith(gBrowser.currentURI.spec, null, null, null, false);
// 他のタブをすべて閉じる
var browser = getBrowser(); browser.removeAllTabsBut(browser.mCurrentTab);
// すべてのタブを閉じる
var browser = getBrowser(); var ctab = browser.addTab("about:blank"); browser.removeAllTabsBut(ctab);
// 左のタブをすべて閉じる
var tabs = gBrowser.mTabContainer.childNodes;
for (var i = tabs.length - 1; tabs[i] != gBrowser.mCurrentTab; i--){}
for (i--; i >=0 ; i--){
    gBrowser.removeTab(tabs[i]);
}
// 右のタブをすべて閉じる
var tabs = gBrowser.mTabContainer.childNodes;
for (var i = tabs.length - 1; tabs[i] != gBrowser.selectedTab; i--)
{
    gBrowser.removeTab(tabs[i]);
}
// [Tab Mix Plus] タブを凍結
gBrowser.freezeTab(gBrowser.mCurrentTab);
// [Tab Mix Plus] タブを保護
gBrowser.protectTab(gBrowser.mCurrentTab);
// [Tab Mix Plus] タブをロック
gBrowser.lockTab(gBrowser.mCurrentTab);

ページ

// 先頭へスクロール
goDoCommand("cmd_scrollTop");
// 末尾へスクロール
goDoCommand("cmd_scrollBottom");
// ページアップ
goDoCommand("cmd_scrollPageUp");
// ページダウン
goDoCommand("cmd_scrollPageDown");
// このページをブックマーク
document.getElementById("Browser:AddBookmarkAs").doCommand();
// 名前を付けてページを保存
document.getElementById("Browser:SavePage").doCommand();
// ページのソースを表示
document.getElementById("View:PageSource").doCommand();
// ページの情報を表示
document.getElementById("View:PageInfo").doCommand();
// 印刷プレビュー
document.getElementById("cmd_printPreview").doCommand();
// 印刷
document.getElementById("cmd_print").doCommand();

ツール

// ダウンロード
document.getElementById("Tools:Downloads").doCommand();
// アドオン
document.getElementById("Tools:Addons").doCommand();
// プライバシー情報の消去
setTimeout(function(){ document.getElementById("Tools:Sanitize").doCommand(); }, 0);
// エラーコンソール
toJavaScriptConsole();
// オプション
setTimeout(function(){ openPreferences(); }, 0);
// DOM Inspector
inspectDOMDocument(content.document);
// 検索バーを開く/閉じる
if ("isFindBarVisible" in gFindBar)
    gFindBar.isFindBarVisible() ? gFindBar.closeFindBar() : gFindBar.onFindCmd();
else
    gFindBar.hidden ? gFindBar.onFindCommand() : gFindBar.close();
// Web 検索ボックスのクリア
document.getElementById("searchbar").value = "";

サイドバー

// ブックマークサイドバー
toggleSidebar("viewBookmarksSidebar");
// 履歴サイドバー
toggleSidebar("viewHistorySidebar");
// [Sage] サイドバー
toggleSidebar("viewSageSidebar");
// [ScrapBook] サイドバー
toggleSidebar("viewScrapBookSidebar");
// [Foxage2ch] サイドバー
toggleSidebar("viewFoxage2chSidebar");
// サイドバーを閉じる
var sidebarBox = document.getElementById("sidebar-box");
if (!sidebarBox.hidden)
    toggleSidebar(sidebarBox.getAttribute("sidebarcommand"));

特殊操作

// ひとつ上の階層へ移動
var uri = gBrowser.currentURI;
if (uri.path == "/")
    return;
var pathList = uri.path.split("/");
if (!pathList.pop())
    pathList.pop();
loadURI(uri.prePath + pathList.join("/") + "/");
// 再起動
Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup)
.quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit);
// [ScrapBook] ページの取り込み
sbBrowserOverlay.execCapture(0, null, false , "urn:scrapbook:root");
// [ScrapBook] ページの詳細な取り込み
sbBrowserOverlay.execCapture(0, null, true , "urn:scrapbook:root");

TOP

[userChrome.js] 軽量マウスジェスチャ(改良バージョン)

[userChrome.js] 軽量マウスジェスチャを少し改良しました。先日のバージョンでは、右クリックによる mousedown イベントが発生した後に続けて mousemove イベントが発生して初めてジェスチャが開始されたとみなすロジックであった。

  • mousedown→mousemove(ジェスチャ開始)→mousemove(ジェスチャ継続)…→mouseup(ジェスチャ終了)

しかし、このロジックですと mousedown から1回目の mousemove までにどれだけ大きくマウスポインタの位置を動かしたとしてもそれはジェスチャとして認識されないという問題があります。実際のところ mousedown から1回目の mousemove までの時間は一瞬ですので、その間に大きくマウスを動かすことはかなり難しいですが、読み込み中のタブがあって動作が鈍くなっているときに↓のジェスチャでタブを閉じようとすると、なぜか認識されずにコンテキストメニューが出てしまうといったケースが時々見られました。

今回のバージョンでは mousedown をした時点でジェスチャ開始とみなすというシンプルなロジックにしています。これによって上記の問題は解決され、今のところ快調に動いているように感じられます。

  • mousedown(ジェスチャ開始)→mousemove(ジェスチャ継続)…→mouseup(ジェスチャ終了)

TOP

[userChrome.js] 軽量マウスジェスチャ

マウスジェスチャ機能は欲しいけど、All-in-One Gestures や Optimoz Mouse Gestures は余計な機能が多すぎる。AiOGから不要な機能を取っ払ってスリム化させようと試みたが、ソースコードが複雑すぎてやる気が失せた。そこで自分で一から考えて作ってみた結果、200行にも満たない userChrome.js 用スクリプトとして実装することができた。

xuldev.org :: userChrome.js scripts » Mouse Gestures

特徴

  1. マウストレイル(軌跡の描画)は重くなる原因なのであきらめた。とはいえ AiOG から trails.js を借りてきて一工夫すれば実装可能です。
  2. ジェスチャによって実行される処理は、極力ブラウザ本体に実装されたコマンドを呼び出すようにしている。
  3. 設定用GUIは無いが、スクリプトを直接編集することで無駄なく柔軟にカスタマイズできる。詳しくは下記スクリプト内の _performAction 関数をご覧ください。

TOP

[userChrome.js] Mouse Gestures

I like mouse gestures, but All-in-One Gestures and Optimoz Mouse Gestures both have too much unwanted features for me. I tried to reduce unwanted parts from AiOG but I gave up since the original source codes was very complex.
And so, I made it by myself from scratch as a small user script for userChrome.js extension, which has only less than 200 lines.

xuldev.org :: userChrome.js scripts » Mouse Gestures

Major features

  1. No mouse trails. Because it would be one of a factor to be slow down the gesture. Nonetheless we can implement it by picking ‘trails.js’ from AiOG.
  2. As much as possible, it executes the ‘built-in’ commands of Firefox itself when we perform actions by gestures.
  3. No options dialog. But we can economically and flexibly customize by editing the script directly. For more details, please see ‘_performAction’ method in the script below.

TOP

拡張機能が無効あるいは削除される前に何らかの処理を実行したい

ある拡張機能が無効あるいは削除される前に何らかの処理を実行したい場合がある。

拡張機能のインストールやアンインストール、無効・有効の変更の実際の処理は、 Firefox を起動してからブラウザのウィンドウが開かれるまでの間に行われるが、どうやらこのタイミングにフックして、無効あるいは削除対象の拡張機能の中の処理を実行することはできないもよう。

そのかわり、「ツール」→「アドオン」から「無効」あるいは「削除」ボタンをクリックすることである拡張機能を削除しようとするタイミングであれば、その拡張機能の中の処理を実行することは可能だ。拡張機能のインストール・アンインストール、有効・無効などの操作をしようとしたとき、「em-action-requested」というトピック名のグローバルな通知が送られるので、これを nsIObserver インタフェースを実装したオブザーバによって監視することで実現される。

トピック名「em-action-requested」のグローバルな通知が送られる時、observe メソッドに渡される引数から、どの拡張機能に対してどういう処理を行うかを判別できる。「削除」ボタンをクリックして拡張機能を削除しようとしたのであれば、3番目の引数は「item-uninstalled」である。したがってこのタイミングで、不要な設定値をリセットするなどの何らかの処理を実行させることが可能。ただし、一度削除しようとした後でも「キャンセル」ボタンをクリックして削除をやめることができるため、削除しようとした時点で処理を実行してしまうのは時期尚早といえる。この時点ではフラグを上げ下げするだけにして、実際に処理を行うのは Firefox を終了させる直前(トピック名 “quit-application” の通知が送信されるタイミング)まで待つといった工夫が必要だ。

問題点としては、「ツール」→「アドオン」からではなく、プロファイルフォルダの extensions フォルダから直接拡張機能のフォルダを削除してアンインストールした場合には対応できない点が挙げられる。

以下は “em-action-requested” の通知を監視する nsIObserver インタフェースを実装した独自XPCOMコンポーネントのコードの一部である。なお、 nsIModule::registerSelf() が呼ばれたタイミングで nsICategoryManager::addCategoryEntry() によってコンポーネントを “app-startup” カテゴリに登録することで、 Firefox 起動時(ブラウザのウィンドウが開く前)にオブザーバの登録処理を実行させることが可能となる。

_toBeDisabled: false,
_toBeUninstalled: false,

/**
 * nsIObserver
 */
observe: function(aSubject, aTopic, aData)
{
    var os = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
    switch (aTopic)
    {
        // Firefox起動時
        case "app-startup":
            // オブザーバを登録
            os.addObserver(this, "em-action-requested", false);
            os.addObserver(this, "quit-application", true);
            break;
        case "em-action-requested":
            const extid = "myextension@mysite.com";    // 拡張機能のID
            if (aSubject instanceof Ci.nsIUpdateItem && aSubject.id == extid)
            {
                switch (aData)
                {
                    case "item-disabled":
                        // 拡張機能を無効にしようとした
                        this._toBeDisabled = true;
                        break;
                    case "item-uninstalled":
                        // 拡張機能を削除しようとした
                        this._toBeUninstalled = true;
                        break;
                    case "item-cancel-action":
                        // 無効あるいは削除する処理をキャンセルした
                        this._toBeDisabled = false;
                        this._toBeUninstalled = false;
                        break;
                }
            }
            break;
        // Firefox終了時
        case "quit-application":
            // オブザーバの登録を解除
            os.removeObserver(this, "em-action-requested");
            os.removeObserver(this, "quit-application");
            if (this._toBeDisabled)
            {
                // 拡張機能を無効にする前に何らかの処理を実行
            }
            if (this._toBeUninstalled)
            {
                // 拡張機能を削除する前に何らかの処理を実行
            }
            break;
    }
},

TOP

拡張機能が設定値「browser.sessionstore.restore_prompt_uri」を変更することの危険性

先日開催された Firefox Developers Conference 2006 の個別セッションのコーナーにて、設定値「browser.sessionstore.restore_prompt_uri」を変更することで、クラッシュからのセッション回復時に拡張機能が独自にこしらえたプロンプトを使用することが可能となる、という話をして、実際にカウントダウン式のセッション回復プロンプトを動かすデモを行った。
これに関してPiroさんから、「拡張機能を削除して変更した設定値がそのまま残ってしまった場合、何らかのフォールバックはあるのか?」という指摘を頂いた。

調べてみたところ、nsSessionStartup.js の250行目あたりからを見ると、セッション回復プロンプトを開く処理は nsIWindowWatcher::openWindow() メソッドによって実行されていることがわかる。このメソッドの引数として「browser.sessionstore.restore_prompt_uri」の値を渡してウィンドウを開くのであるが、その値によって以下のような挙動となる。

存在するchrome: URI ウィンドウが開き、XULなどがロードされる。例えば、拡張機能独自のプロンプトが開かれる。
存在しないchrome: URI ウィンドウは開かず、何も起こらない。処理は先に進まない。
空の文字列 標準のセッション回復プロンプトが開かれる。
http: URI ウィンドウが開いてWebページがロードされる。ウィンドウのxボタンをクリックして閉じれば、セッションが回復する。
明らかにURIではない文字列 プロンプトは開かれず、セッションが回復する。これは openWindow メソッドで発生した例外をキャッチするフォールバック処理があるため。

やはり問題となるのは、拡張機能を削除した場合に存在しないchrome: URIが設定値として残ってしまうケースだろう。この場合の危険性は以下のような話に例えることができる。

Firefox 初級ユーザであるA君は、「Countdown Recovery」という拡張機能をインストールした。
すると設定値「browser.sessionstore.restore_prompt_uri」が「chrome://countdownrecovery/content/prompt.xul」に変更され、クラッシュからのセッション回復時に当該URIのプロンプトが開かれるようになった。
しばらくしてA君はこの拡張機能が気に入らなくなったので、アンインストールした。このとき、変更された設定値がそのまま残ってしまったことに、A君は当然気付くはずもなかった。
後日A君の Firefox がクラッシュしたため再起動したところ、何度試しても Firefox のブラウザウィンドウは開かれなかった。不思議に思ったA君は Firefox を再インストールしたが、結果は同じ。
困ったA君は Firefox 中級ユーザであるB君に助けを求め、セーフモードで起動しろというアドバイスに従って試したが、やはり Firefox のウィンドウは出現しない。これにはB君もお手上げ状態で、最後の手段としてプロファイルを作り直すしかないと判断した。A君は新しいプロファイルを作ることでようやく Firefox を起動することに成功したが、泣く泣くカスタマイズを一からやり直すこととなった。

この例え話で起こった症状は、Firefox を終了させた状態で prefs.js に書き込まれている問題の設定値を削除するか、あるいは sessionstore.js および sessionstore.bak を削除した上でセッション回復させずに Firefox を再起動し、about:config から設定値をリセットすることで解決される。

一般に、拡張機能をアンインストールしても、prefs.js に書き込まれたその拡張機能に関係する設定値などはそのまま残ってしまう。また、拡張機能が独自に生成したファイルなどのデータも当然残ったままだ。しかし、Piroさんの話では NoScript という拡張機能ではアンインストール時に何らかの処理を行う仕掛けがあるそうなので、さっそく調べてみた。

TOP

menulist 要素内での menuitem-iconic クラス

xul:menuitem 要素へアイコンの画像を表示させたい場合、menuitem-iconic クラス を付与して src 属性に画像の URI を指定すれば良い。この方法は xul:menu 要素や xul:toolbarbutton 要素 (type=”menu”) 配下に xul:menuitem 要素を置く場合であれば、うまく動作する。

<menu label="MENU">
  <menupopup>
    <menuitem class="menuitem-iconic" label="MENUITEM" src="http://www.mozilla.com/favicon.ico" />
  </menupopup>
</menu>
<toolbarbutton type="menu" label="TOOLBARBUTTON" >
  <menupopup>
    <menuitem class="menuitem-iconic" label="MENUITEM" src="http://www.mozilla.com/favicon.ico" />
  </menupopup>
</toolbarbutton>

ところが xul:menulist 要素配下に xul:menuitem 要素を置く場合、 xul:menupopup 要素のドロップダウンリスト中ではアイコンの画像が表示されない。

<menulist>
  <menupopup>
    <menuitem class="menuitem-iconic" label="2ちゃんねる"   src="http://www.2ch.net/favicon.ico" />
    <menuitem class="menuitem-iconic" label="PINKちゃんねる" src="http://www.bbspink.com/favicon.ico" />
    <menuitem class="menuitem-iconic" label="まちBBS"        src="http://www.machibbs.com/favicon.ico" />
    <menuitem class="menuitem-iconic" label="したらば"       src="http://jbbs.livedoor.jp/favicon.ico" />
  </menupopup>
</menulist>

menuitem-iconic

ドロップダウンリスト中にも画像を表示させるには、 chrome://browser/skin/feeds/subscribe.css を参考にして以下のような CSS を適用してやる。下記の例ではついでにサイズの大きな画像を 16×16 px に縮小させている。

menulist menupopup > menuitem {
  -moz-padding-start: 23px;
}

menulist menupopup > menuitem.menuitem-iconic {
  -moz-padding-start: 2px;
}

menulist menupopup > .menuitem-iconic > .menu-iconic-left {
  display: -moz-box;
  min-width: 16px;
  -moz-padding-end: 2px;
}

menulist .menulist-icon,
menulist menupopup > .menuitem-iconic > .menu-iconic-left > .menu-iconic-icon {
  width: 16px;
  height: 16px;
}

menuitem-iconic

TOP