このサイトの記事更新は2019年11月に終了されました。過去記事アーカイブを公開しています。
投稿されたすべてのトピック
Jetpack0.7がリリースされました
はじめまして、こんにちは!con_mame(こんまめ)です。
先日は、Mozilla勉強会でJetpackについて話させて頂きました(時間超過すいません><) 使用したスライドは、http://d.hatena.ne.jp/con_mame/20091220#1261301099 にのっけています。
さて、先日Jetpack0.7がリリース(http://mozillalabs.com/jetpack/2009/12/23/announcing-jetpack-0-7/)されました。今回のリリースではAPIの追加と既存の不具合の修正が行われています。
今回のリリースでFirst run APIが追加されました。このAPIを使用することでFeatureのインストール後にFeature作者が指定したサイトやメッセージを表示することが可能になります。主な利用方法としては、Featureの使用方法を記載したページを表示させるなどがあげられます。記述方法も非常に簡単で、manifest内にfirstRunPageプロパティを記述するだけです。以下の様に記述する事で、Featureのインストールが完了した際に表示されるページの内容の一部が指定した内容に書き換えられます。アドレスだけを指定した場合はiframe内に指定したサイトが表示されます。
//書き方1(メッセージを表示/E4Xでも記述出来ます)
var manifest = {
firstRunPage: 'インストールありがとう! 使い方
'
};
//書き方2(サイトを挿入)
var manifest = {
firstRunPage: "http://hoge.com/howtouse.html"
};
簡単ですね。
この他には、MeというAPIも追加されています。こちらはCallback関数を指定することでインストール完了後に処理を実行することが出来ますが、個人的にはあまり利用方法が思いつきません。
jetpack.future.import("me");
jetpack.me.onFirstRun(function () {
jetpack.notifications.show("Oh boy, I'm installed!");
});
この様に記述出来ます。詳しくはhttps://developer.mozilla.org/en/Jetpack/Meta/Me を参照して下さい。
今回のリリースではSettings APIとStatusBarの挙動が修正されています。
Settings APIでは、about:jetpack→Installed Features内の、SettingsボタンがSettings APIを使用していないFeatureについてはクリックが出来ないようになりました。(Ver.0.6ではクリックすることが可能で、クリックするとエラーが表示されていました)
StatusBarの修正は、StatusBarにFeatureを追加するとStatusBarの高さがどんどん高くなっていき、Featureのアイコンや文字が階段状に配置されてしまうという問題が修正されています。
しかし、修正されたコードを見ると、StatusBarのHeightを16pxに固定するように記述されているので、StatusBarに表示する文字のサイズを大きくしたりすると、文字の下部が見切れてしまいますので注意が必要です。(標準の文字サイズであれば問題ありません)
今回のリリースでは以上の点が目立った追加・変更かなと思います。
機能を他のアドオンからキャンセルできるようにする
このエントリは、一つ前のカスタムイベントの通知方法の紹介を踏まえた内容になっています。先に前のエントリを読んでからご覧になる事をお勧めします。
さて、前のエントリでは closeTabs()
関数が呼ばれた時にそれを通知する方法を解説しました。しかし前のエントリに書いた方法だけだと、以下のような制限があります。
- イベントを通知する側から検知する側へ、一方通行でしか情報を送れない。
例えば、何か時間のかかる処理をするアドオンを開発していたとしましょう。
gBrowser.addEventListener(
'ClosingTabs',
function (aEvent) {
// お、複数のタブが閉じられようとしているぞ!
if (aEvent.tabs.some(function(aTab) {
return aTab.hasAttribute('wait-for-a-while');
})) {
// 処理中のタブがある!
// これは今閉じられると困るなあ。
// でも、「やめて!」と伝える方法がない><
}
},
false
);
このように、処理の中止を訴えたくてもイベントを発行した側である closeTabs()
に対して値を返したりメッセージを送ったりできないので、指をくわえて見ていることしかできません。
こういうニーズに備えて、事前にキャンセルができるようなタイミングでカスタムイベントを通知する時は、キャンセルできるイベントとして通知するようにするとよいでしょう。
// イベント通知側
function closeTabs(aTabs) {
var event = document.createEvent('Events');
// 好きなイベント名を付ける。
// この時、第3引数が true だと、キャンセルできるイベントになる。
event.initEvent('ClosingTabs', true, true);
event.tabs = aTabs;
gBrowser.dispatchEvent(event);
// イベントがキャンセルされていたら、処理を中断する。
if (event.getPreventDefault())
return;
...
}
// イベント検知側
gBrowser.addEventListener(
'ClosingTabs',
function (aEvent) {
// お、複数のタブが閉じられようとしているぞ!
if (aEvent.tabs.some(function(aTab) {
return aTab.hasAttribute('wait-for-a-while');
})) {
// 処理中のタブがあるからやめて!
aEvent.preventDefault();
}
},
false
);
イベントを検知する側でイベントオブジェクトの preventDefault()
メソッドを呼ぶと、getPreventDefault()
の戻り値が false から true に変わります。なので、これを見ればイベント通知側からも、イベントがキャンセルされたかどうかを判別できるというわけです。
ただし、Firefox 3.6 以前では UIEvents として生成・初期化したイベントでなければ getPreventDefault()
が使えないという問題があります。Firefox 3.5 以前でも正常に動作するようにするには、以下のようなハックを行う必要があります。
function closeTabs(aTabs) {
var event = document.createEvent('Events');
event.initEvent('ClosingTabs', true, true);
event.tabs = aTabs;
gBrowser.dispatchEvent(event);
ensureEventCancelable(event); // ←ここで問題を解消
if (event.getPreventDefault())
return;
...
}
function ensureEventCancelable(aEvent) {
// Gecko 1.9.2 ( Firefox 3.6 )以降なら何もしない
if (aEvent.getPreventDefault) return;
// 元の関数を保存
aEvent.__original__preventDefault = aEvent.preventDefault;
aEvent.__canceled = false;
aEvent.preventDefault = function() {
this.__original__preventDefault();
// キャンセルされたかどうかのフラグを独自に立てる
this.__canceled = true;
};
aEvent.getPreventDefault = function() {
return this.__canceled;
};
}
……というこのハックを見るとバレバレなのですが、実はイベントオブジェクトのプロパティを介しても情報のやりとりはできます。ただ、preventDefault()
はリンクのクリックによるページ遷移をキャンセルするといった場合でも使われる、W3C の DOM Level2 Events の仕様にも含まれる広く知られたメソッドなので、それと同じ方法でイベントをキャンセルできるようにしておいた方が、イベントを検知するアドオンを作る人にとっては分かりやすいでしょう。
nsIObserverService など他の方法を使うと、クロスウィンドウでイベントを通知するなどのより高度なこともできます。とりあえず、まずはウィンドウ内で完結するイベントの仕組みから始めてみて、限界を感じたら別の方法を検討してみてください。
機能が呼び出されたことを他のアドオンに通知する
あなたはアドオンを作る時、他のアドオンと連携しやすいようにするということを意識しているでしょうか?
例えば、複数のタブをまとめて一気に閉じる機能を設けるために、タブの配列を受け取ってそれらをすべて閉じる closeTabs()
という関数を定義するとしましょう。
function closeTabs(aTabs) {
gBrowser.dispatchEvent(event);
aTabs.forEach(function(aTab) {
gBrowser.removeTab(aTab);
});
}
この機能を提供するアドオンの作者であるあなたにとっては、これ以上特に何も気にする事はありませんよね。
では、他のアドオン作者の人が、あなたのアドオンと連係して動作するアドオンを開発する場面を考えてみましょう。この関数が呼ばれたという事を他のアドオンから検知するにはどうすればよいでしょうか?
Firefox のユーザの多くは、複数のアドオンを組み合わせて使っています。アドオンを公開していると、「あなたのこのアドオンはとても素晴らしい! ところで自分は○○というアドオンを使ってるんだけど、あなたのアドオンの機能からもこの○○の機能を利用できるようにならないだろうか?」という風な要望が寄せられることもあります。私はそういう要望を受け取って初めてそのアドオンの存在を知ることが多いのですが、それでは連携してみようと思ってそのアドオンのソースコードを覗いてみて、途方に暮れてしまうことが少なくありません。そういう場合で一番多いのが、「このアドオンのこの機能が呼び出される直前に、何らかの処理を割り込ませたい。しかし、その方法がない。」というケースです。
もちろん、方法が全くないわけではない場合がほとんどです。例えばたいていの場合、以下のようにすれば任意の処理を割り込ませることができます。
eval('window.closeTabs = '+
window.closeTabs.toSource().replace(
'{',
'{ AnotherAddon.onTabsClosed(); '
)
);
しかし、このように eval()
を使って関数を書き換えたり、あるいは関数全体を置き換えたりする方法は、同じ事をやろうとするアドオンが沢山存在すると破綻してしまいます。また、書き換え(置き換え)対象の関数の定義が変わった時にも、期待通りに動かなくなる可能性があります。
実際に Firefox 1.5 以前の古いバージョンの Firefox では、tabbrowser 要素の addTab()
や removeTab()
といったメソッドを置き換えないと、「新しいタブが開かれた」「タブが閉じられた」「タブが移動された」といった場面で任意の処理を行う事はできませんでした(※厳密に言うと、間接的なやり方で実現できない事もなかったのですが……)。そのため、タブブラウズ機能に関係するアドオンを複数インストールするとそれらが衝突してまともに動かないということもざらにありました。
他のアドオン作者をこういう風に困らせてしまわないように、他のアドオンと連携しやすいアドオンを作る方法はないのでしょうか? それがこのエントリのテーマです。
複数のアドオン同士を連携しやすくする方法はいくつかありますが、その 1 つとして、DOM Level2 Events の仕組みを使う方法があります。例えば次のようにすると、前出の closeTabs()
が呼ばれた時に「複数のタブを閉じようとしていますよ!」というイベントを通知することができます。
function closeTabs(aTabs) {
// 新しいイベントオブジェクトを作る
var event = document.createEvent('Events');
// 好きなイベント名を付ける。
// 後の引数はとりあえず true, false と書いておけばいい。
event.initEvent('ClosingTabs', true, false);
// イベントオブジェクトに任意のプロパティを持たせて
// いろんな情報を送れる。
event.tabs = aTabs; // 閉じられようとしているタブ
// 任意の DOM 要素ノードの dispatchEvent() メソッドで、
// イベントの通知を開始!
gBrowser.dispatchEvent(event);
...
}
このようにカスタムイベントを通知するようにしておけば、他のアドオンの作者は以下のようにして closeTabs()
が呼ばれたことを簡単に検知できます。
gBrowser.addEventListener(
'ClosingTabs',
function (aEvent) {
// お、複数のタブが閉じられようとしているぞ!
var tabs = aEvent.tabs;
var labels = tabs.map(function(aTab) { return aTab.label; });
alert(labels.join('\n')+
'\n-----'+
'\n以上の'+tabs.length+'個のタブがこれから閉じられます');
},
false
);
Firefox 2 以降でも、これと同じ方法で、タブが開かれたり閉じられたりといったイベントを検知できます。つまり、Firefox 本体と同様の処理をあなたのアドオンにも持たせるということですね。
「他のアドオンと連携する事なんて興味ないよ」と思うかもしれませんが、このテクニックは他のアドオンとの連携以外でも役に立ちます。
例えば closeTabs()
を使う機能にさらに色々便利な機能を増やしたくなった時、closeTabs()
の中にその都度コードを書き加えていっていると、そのうち関数がどんどん長くなってメンテナンスが大変になってきます。何百行、何千行という長い関数の中でうまく動かない部分がでてくると、原因箇所の特定も、バグの修正も困難です。
でも上記のようにカスタムイベントを定義してあれば、機能を増やしたくなった時は、そのイベントを監視するイベントリスナを新しく定義するだけで済みます。機能ごとに細かく関数を分ければメンテナンス性も高まります。
カスタムイベントは、他のアドオンとの連携にもあなた自身がアドオンをメンテナンスする手間の削減にも役立つ一石二鳥なテクニックです。ぜひ活用してみて下さい。
クリックされたタブやボタンを確実に取得する方法
タブの上での特殊な操作をトリガーとして発動する機能を作りたい時は、イベントが発生した要素を取得する方法にも気を配りましょう。
例えば「ダブルクリックされたタブを閉じる」機能を実現する場合、単純に考えるとこのようになるでしょう。
gBrowser.mTabContainer.addEventListener(
'dblclick',
function(aEvent) {
var tab = aEvent.originalTarget;
if (tab && tab.localName == "tab")
gBrowser.removeTab(tab);
},
false
);
しかし、これは実際には期待通りに動きません。例えばタブの中の label 要素や image 要素でイベントが発生した場合、aEvent.originalTarget
は tab 要素ではなくなってしまうので、この条件ではマッチしないことになります。なのでもう少し工夫が必要です。
gBrowser.mTabContainer.addEventListener(
'dblclick',
function(aEvent) {
var target = aEvent.originalTarget;
var parent = target.parentNode;
if (target && target.localName == "tab")
gBrowser.removeTab(target);
else if (parent && parent.localName == "tab")
gBrowser.removeTab(parent);
},
false
);
素の Firefox での利用のみを前提とするなら、これでも十分かもしれません。
しかし実際には、ユーザはそのアドオンを他のアドオンと組み合わせて使うかもしれません。タブの中の label 要素にさらに子要素を追加するアドオンや、tab 要素のバインディングを変更するアドオンと組み合わせた時、このコードでは期待通りに動かなくなります。また、-moz-border-image
がサポートされていなかった頃からあるテーマの中にも、バインディングを使ってタブの外観を変えている物はたくさんあります。イベントが発生したノードとその直上の親ノードだけを調べてタブかどうかを判別するというやり方は、この手の変化に対し非常に脆弱です。(直上の親に限らず、「N 階層上の祖先」や「N 番目の子ノード」という調べ方でハードコーディングしてしまうやり方自体がそもそも変化に弱いということです。)
このような場面では、DOM3 XPath や Selectors API のように抽象的な指定でノードを取得する仕組みを使うことをお勧めします。
例えば上記の例では、特定のノードを起点として祖先方向に検索を行える DOM3 XPath を使うと問題を解決できます。
gBrowser.mTabContainer.addEventListener(
'dblclick',
function(aEvent) {
var tab = document.evaluate(
'ancestor-or-self::*[local-name()="tab"][1]',
aEvent.originalTarget,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
if (tab) gBrowser.removeTab(tab);
},
false
);
DOM3 XPath では、XPath 式で検索対象のノードを指定します。ここでは ancestor-or-self
という指定で「起点ノードもしくはその祖先」の要素ノードを検索して、その中でローカル名が「tab」である1番目の要素(※ XPath 式では N 番目の項目を指定する時、添字は 0 ではなく 1 から始まります)を取得しています。
アドオンを開発する時、特にそれがタブやツールバーなど他のアドオンによって色々と変更される可能性が高い箇所に対して機能を追加するのであれば、Firefox の素の状態からある程度の変化が加わっている可能性を見越して、柔軟に対応できるようにしておくとよいでしょう。また、自分のアドオンが他のアドオンに対して悪影響を及ぼさないよう、加える変化はなるべく少なくする事も大切です。どちらにしても、Firefox のセールスポイントが豊富なアドオンである以上、アドオンの作者は、自分のアドオンが他のアドオンと組み合わせて使われることを前提にして、なるべく衝突の可能性が低くなるように、また、衝突しても容易に対応できるような設計にしておくように気を遣う必要があります(←自戒を込めて)。
シンプルなテストケースを作ろう
実際にコードを書けない人でも、開発に大きく貢献しようと思えば、テストケースを作り、bugzillaの該当のバグに提出する、という作業で貢献することができます。役に立つテストケースは実際に、パッチを作成するエンジニアにとって非常に大きな助けとなります(私も昔、パッチが書けなかった時はよくやっていました)。
実際にパッチを書いている立場から、どのようなテストケースが有用で、助かるのかと問われれば、シンプルで、かつ、分かりやすいという(時には相反する)二つの重要なポイントを守っておいてください、と答えます。
例えば、キー入力やマウスの操作である等、挙動に関するバグのテストケースである場合、余計なスタイルシート等は指定せず、必要最小限のもののみを指定するようにします。なぜなら、エンジニアは検証時に、様々なスタイルが適用されているテストケースを見ると、それら全てがバグに関係があるものなのかと考えてしまうかもしれませんし、また、少なくとも疑ってかかる必要が出てくるためです。さらに、本当に重要なポイントが分かりにくくなる、という点も見逃せません。
これらの事から、HTML自体の記述では、ブロックレベルの要素ならdiv
要素、インラインレベルの要素が必要な場合はspan
要素を使うのが好ましいことが理解してもらえると思います。他の要素はこれらの要素にスタイルを追加していたり、特殊な動作を追加したりしているため、それそのものが複雑だからです。
ですが、複数の要素を配置する必要があり、それらの違いが分かる必要がある場合、例えば、HTMLやCSS等のレイアウトやレンダリングに関わるバグのテストケースではボックスごとに異なる色のborder
かbackground-color
のどちらかのみを指定する必要があることがあります。一般的に、レイアウトに関するバグの場合にborder
の利用には注意が必要です。border
はその太さの分、レイアウトに影響を与えてしまうためです。ですが、background-color
を指定した場合はその背後にあるボックスが見えなくなるという、特性があるためz-index
のテスト等では使いにくい、または使えないことがありますので、ケース毎に判断していく必要があります。
また、エンジニアはテストで長時間そのテストケースを見ることが多々ありますので、あまり目に優しくない色の組み合わせや、ビビッドな色の利用には注意しましょう。
HTML自体を記述する場合にも、strictなHTMLにこだわる必要はないという点に気をつけてください。テストケースがアクセシブルである必要や、複数のブラウザで表示できる必要は全くありません。Gecko (or Firefox)のためのテストなので、これでテスト可能な最低限のシンプルなコードを書くのが最も良いのです。
例えば、Standardsモードでレンダリングさせたい場合、<!DOCTYPE html>
と、HTML5のDOCTYPE宣言を入れるだけでかまいません。長い、HTML4のDOCTYPEを挿入したりする必要はありませんし、Standardsモード、Quirksモード、どちらでも良い場合であればこの一行そのものが必要ありません。
同様に、html
要素、head
要素、meta
要素、body
要素、さらにはtitle
要素も必要ありません。文字コード宣言も必要ありません。UTF-8で作成し、bugzillaに添付すれば、bugzillaはデフォルトで、ヘッダにcharset=UTF-8
を付与して送信します。UTF-8以外のテストケースが必要な場合であっても、meta
要素で文字コード宣言を行わなくても、添付時にContent Typeの項目で、enter manuallyを選択し、text/html;charset=Shift_JIS
と指定するだけで済みます。
最後に、シンプルなテストケースを作ることに成功しても、シンプル過ぎるが故に他人には分かりにくすぎるテストケースになってしまっている可能性が高いことには注意してください。例えば、CSSのテストではテストケースがどのように表示されるべきなのか分かりにくいことがよくあります。このような場合は別のファイルに、よりシンプルなスタイル指定で、本来表示されるべきリファレンスを作ってしまうと良いでしょう。そうしておけば、開発者はそれらのファイルをそのままreftestに流用可能です。これは、開発者の負担を大きく下げてくれますし、長期間、バグが放置された後も、バグが再現しなくなっているのかどうかの検証が誰にでも簡単に確実に行えるようになります。
それ以外の場合には、テストケースを添付する際のコメントでしっかりと説明しておきましょう。
テストケースを書くというのは地味ですが難しい作業です。つまり、この作業をパッチを作成できる人がやるのはプロジェクト全体としては非常に効率が悪いと言えます。実際にパッチを作成することに比べれば明らかに(技術的にも時間的にも)敷居は低いのでより多くの方がこの分野で活躍されれば、プロジェクト全体の開発速度を上げることに貢献できます。我こそは、という方は是非参加してみてください。
Mozilla 勉強会での Jetpack 会議
あかつか です。こんにちは。
先日行われた Mozilla 勉強会 では 3分Jetpacking の発表の後に、Jetpack へのご意見や要望、問題点などについて議論の場を設けて頂きました。おかげさまで、とても濃い話しができたと思いますし、さまざまなご意見を聞くことができました。議論の内容はご意見や要望などなどのページにまとめておきました。どうもありがとうございました!!
あ、あと、いちおう、この間発表した 3分Jetpacking も公開しました。
ではでは。
戻りやすくする:jetback
長いドキュメントを読み終わったあと、マウスカーソルはどこにあるだろうか。スクロールバーを多用する人にとってはスクロールバーの近くに、マウスホイールでスクロールをする人にとってはドキュメントのどこかにマウスカーソルはありそうである。ところで、”戻る”ボタンはブラウザのどこにあるだろうか。およそブラウザの左上に頑なに鎮座していると思われる。遠い。ならば、もっとカーソルの近いところにこの機能を持ってきてはどうだろうか。
てっとりばやく、ドキュメントの一番下にこの機能を追加してみる。
実装の目次
- ドキュメント読み込み完了のイベントを取る。
- history があるか検査する。
- ボタンを追加する。
- 右側へ。
- 色気を出す。
ドキュメント読み込み完了のイベントを取る。
ここはいつもと同じです。(説明はこちらをご参照くださいませ)
jetpack.tabs.onReady(function(targetDocument) {
if (targetDocument.defaultView.frameElement) {
return;
}
});
history があるか検査する。
“戻る”ボタンですので、閲覧履歴がないと意味がありません。そこで閲覧履歴の有無を検査します。具体的には window.history を検査しますが、window と書くだけでは Jetpack の名前空間内の window になってしまいます。読み込んだドキュメントの window を見るためには引数 targetDocument の defaultView にアクセスする必要があります。
jetpack.tabs.onReady(function(targetDocument) {
var contentWindow = targetDocument.defaultView;
if (contentWindow.frameElement) {
return;
}
//履歴があるかどうか検査する
if (! contentWindow.history.previous) {
return;//無いようだ。
}
});
ボタンを追加する。
ボタンとなる div 要素をドキュメントの一番最後に追加します。また、クリックイベントを取得して、一つ前のページへ戻します。
jetpack.tabs.onReady(function(targetDocument) {
var contentWindow = targetDocument.defaultView;
if (contentWindow.frameElement) {
return;
}
//履歴があるかどうか検査する
if (! contentWindow.history.previous) {
return;//無いようだ。
}
//ボタン要素
var back = targetDocument.createElement("div");
back.textContent = "戻る";
//戻る機能を付ける。jQueryオブジェクトにしています
var jback = jQuery(back);
jback.click(function() {
contentWindow.history.back();
});
//ドキュメントに追加
targetDocument.body.appendChild(back);
});
右側へ。
左側にボタンがあると、スクロールバーから遠いので、右側にもってくる。
jetpack.tabs.onReady(function(targetDocument) {
var contentWindow = targetDocument.defaultView;
if (contentWindow.frameElement) {
return;
}
//履歴があるかどうか検査する
if (! contentWindow.history.previous) {
return;//無いようだ。
}
//ボタンの下のdivを用意して、右側に寄せる。
var backdiv = targetDocument.createElement("div");
//ボタン要素
var back = targetDocument.createElement("div");
back.textContent = "戻る";
//右側に配置。あとで微調整ができるように absolute にしてある
back.setAttribute("style", "position:absolute; right: 10px;");
//div にボタンを追加
backdiv.appendChild(back);
//戻る機能を付ける。jQueryオブジェクトにしています
var jback = jQuery(back);
jback.click(function() {
contentWindow.history.back();
});
//ドキュメントに追加
targetDocument.body.appendChild(backdiv);
});
色気を出す。
あまりにもさっぱりしているのでイメージを利用してちゃんとボタンっぽくしてみます。現状(ver 0.6)の Jetpack では外部ファイルを同一パッケージにすることができません。イメージも同様です。そこで、イメージを Base64 でエンコードして javascript ファイルに組み込みます。
利用イメージ:
jetpack.tabs.onReady(function(targetDocument) {
var contentWindow = targetDocument.defaultView;
if (contentWindow.frameElement) {
return;
}
//履歴があるかどうか検査する
if (! contentWindow.history.previous) {
return;//無いようだ。
}
//ボタンの下のdivを用意して、右側に寄せる。
var backdiv = targetDocument.createElement("div");
//ボタン要素
var back = targetDocument.createElement("div");
//イメージを付ける
//右側に配置。あとで微調整ができるように absolute にしてある
back.setAttribute("style", "background: url('data:image/png;base64,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'); width: 128px; height: 128px; position:absolute; right: 10px;"); //div にボタンを追加
backdiv.appendChild(back);
//戻る機能を付ける。jQueryオブジェクトにしています
var jback = jQuery(back);
jback.click(function() {
contentWindow.history.back();
});
//ボタンを押したときのイメージへ
jback.mousedown(function() {
jback.css({"background-position":"right"});
});
//もとのイメージへ
jback.mouseup(function() {
jback.css({"background-position":"left"});
});
//ドキュメントに追加
targetDocument.body.appendChild(backdiv);
});
これで完成です。ドキュメントを読み終えると戻るボタンが目に入るようになり、サイズもかなり大きくしてあるのでかなり戻りやすくなっていると思います。ただし、このボタンは常にこれを押さなければならないというものではなく、マウスの位置に応じ、ブラウザの戻るボタンを使うなど、選択しながら利用すればよいのかと思います。
最後に
このソースコードおよび install html を固めてこちらにおきます。このコードではさらに、履歴の一番始めまで戻る機能もあわせて実装してみました。
画面の描画内容を一時的にロックする方法
Firefox のウィンドウ内の描画を一旦停止して、処理を行った後で改めて再描画させる、という事をしたいと思った事はないでしょうか。Firefox 3(Gecko 1.9)以降であれば、これは以下のコードで実現できます。
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); // ここでやっと描画が再開される
具体的な利用例を1つ挙げてみます。
CSS のプロパティを動的に変更して UI 要素の表示を変える事はよくあると思いますが、その際、複数の UI 要素の表示を調整するために順番に処理を行っていると、最初の状態から最終的な状態に至るまでの間の中間状態が描画されてしまって、画面がチラつく事があります。
例えば、普段はタブバーを非表示にしておき、ポインタが近づいた時だけタブバーを表示する、という機能は以下のように実装できます。
var TabbarAutoHide = {
hide : function() {
gBrowser.mStrip.collapsed = true;
// show()でずらした表示位置を元に戻す。
let style = gBrowser.mPanelContainer.style;
style.setProperty('position', '', '');
style.setProperty('margin-bottom', '', '');
style.setProperty('height', '', '');
// 一旦画面外まで大きくずらしているのは、その範囲を強制的に再描画させるため。
let viewer = gBrowser.markupDocumentViewer;
viewer.move(window.outerWidth, window.outerHeight);
viewer.move(0, 0);
},
show : function() {
gBrowser.mStrip.collapsed = false;
let tabHeight = gBrowser.mStrip.boxObject.height
let panelHeight = gBrowser.mPanelContainer.boxObject.height + tabHeight;
// コンテンツ表示領域をずらす分だけ下に空白ができてしまうので、それをごまかす
let style = gBrowser.mPanelContainer.style;
style.setProperty('position', 'relative', 'important');
style.setProperty('margin-bottom', -tabHeight+'px', 'important');
style.setProperty('height', panelHeight+'px', 'important');
// コンテンツ表示領域の表示位置をタブバーの分だけ上にずらす
let viewer = gBrowser.markupDocumentViewer;
viewer.move(window.outerWidth, window.outerHeight);
viewer.move(0, -tabHeight);
}
};
しかしこのままだと、タブバーの表示・非表示が切り替わる瞬間にコンテンツ表示領域がガタガタと揺れるように見えてしまってとても不格好です。
こういう時に、冒頭で紹介したテクニックが役に立ちます。
var TabbarAutoHide = {
get baseWindow() {
return window.top
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell)
.QueryInterface(Ci.nsIBaseWindow);
},
hide : function() {
this.baseWindow.setPosition(window.innerWidth, window.innerHeight);
gBrowser.mStrip.collapsed = true;
...
viewer.move(0, 0);
this.baseWindow.setPosition(0, 0);
},
show : function() {
this.baseWindow.setPosition(window.innerWidth, window.innerHeight);
gBrowser.mStrip.collapsed = false;
...
viewer.move(0, -tabHeight);
this.baseWindow.setPosition(0, 0);
}
};
このように、複数回の再描画が行われそうな部分の前後で描画の一時停止と再開を行えば、ユーザにとっては煩わしいだけの画面上のチラつきを減らすことができます。
ただ、複数の関数(メソッド)でそれぞれ独自に描画の停止/再開を行っていると、停止→停止→再開→再開 のような順で処理が行われてしまうため、せっかく見えないようにしたはずの処理中の画面が見えてしまうことがあります。このようなトラブルを防ぐには、呼び出しの深さをカウントするなどの方法で、実際に描画を停止/再開させるケースを判別する必要があります。この点だけに絞って最小構成でライブラリ化した stopRendering.js という物を作りましたので、ぜひ使ってみて下さい。window['piro.sakura.ne.jp'].stopRendering.stop()
で描画が停止され、window['piro.sakura.ne.jp'].stopRendering.start()
で再開されます。
なお、この方法の発見に至った経緯も公開していますので、アドオン開発がどういう風に行われているのかの一端を覗いてみたい方は見てみるといいかもしれません。(これはかなり特殊なケースですが……)
※このエントリでは当初 nsIContentViewer の hide()
メソッドと show()
メソッドを使った方法を紹介していましたが、この方法には弊害があることが判明したため、nsIBaseWindow の setPosition()
メソッドを使った方法に訂正しました。訂正前のエントリをご覧になった方はご注意下さい。訂正前の方法の弊害は自サイトの新しいエントリに詳しく記載しています。
Mozilla勉強会(#modest)に参加しました
Mozilla 勉強会 « Mozilla Developer Street (modest)
- プレゼン資料 アプリケーションプラットフォームとしてのFirefox拡張
- HTMLでプレゼンを作れるS5を使っています。gitとの相性もいいし、ブラウザの表現力を活かせるので好きです。
Firefox3 Hacksにサインをいただきました
ノベルティをいただきました
- クリアーファイル! レア物とのこと。
- ノートパソコン用バッグ! Firefoxのアイコン付き! これはすごい
- ステッカー、ボールペン、携帯ストラップ、ネックストラップなどなど!
ありがとうありがとう!
JetpackやMozillaの現状など、とても勉強になりました。次回もぜひお願いします。
Mozilla勉強会で発表してきました
変態Vimperator使いのteramakoです。こんにちは
Mozilla 勉強会 « Mozilla Developer Street (modest)にて発表してきました。
内容としては
かなり異色の部類になるかと思われる開発者向けのfeatureの紹介となってます。
jetpack feature installerに関してはローカルのJetpack FeatureファイルをインストールするFeature – hogehogeで紹介していたこともあり、id:con_mameさんに先に紹介されたり、懇親会で「使っていますよ」と言ってくれる人がいたりと嬉しい誤算がありつつ、なかなか楽しい勉強会でした。
Jetpackに関しては皆いろいろ思うところがあるようで、それなりに議論できたのかなと思います。
私自身それなり思うところがあるわけですが、Jetpackは今後、GoogleChromeのChromeExtensionと比較され続けるかと思います。ChromeExtensinと比較して優位性を示せるかが課題ではないでしょうか。本心としてはTwitter / teramakoで書いた通りなのですが・・・。
開発者を募集しているようです
話はちょっとそれますが、JetpackのMLでWe’re looking for a Jetpack API Developer! – mozilla-labs-jetpack | Google グループなんてのが流れてます。ちょっと条件が厳しいですが・・・、自信のある方は応募してみると現状を打開できるかも!? です。
さらに余談
そうそう、懇親会で私がLightweight Language Television (LLTV)にて発表したものが話題(XULとcanvasを使って画面いっぱいにSLを走らせるというもの)にあがり、デモったのですが、その時の資料はSL command – LLTV にあります。資料はHTML+JavaScriptで出来ていますので、該当ソースを抜きとれば貴方のページでSLを走らせることができますよ デモで使ったコードのソースは/lang/javascript/vimperator-plugins/trunk/sl.js – CodeRepos::Share – Tracです(注:Vimperatorのプラグインとして動作するように作られているので通常では動きません)。