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 10/239: « 6 7 8 9 10 11 12 13 14 »

コアJetpackミーティング - Jan 26, 2010

23日にMozilla Japanで行われたコアJetpackミーティングに参加してきました。2月にGomitaさんとあかつかさんがMozilla Corporationまで行く時に持って行く意見・アイデア等をまとめるのが趣旨の会合でした。Mozilla信者な視点だけからでは意見が偏るんじゃないかと思ったので、Chrome拡張機能のえらい人のos0xさんにも来てもらいました。

議題は「Jetpackのスクリプトの開発環境について」「Jetpack Galleryについて」「Jetpackが提供するAPIについて」の3つでした。以下、思い出しながらグダグダ書いてみます。

開発環境について

既存のアドオンを冷遇していくとかの話はさておき、これまで普通のアドオンを作ってた人が、Jetpackで事足りるような機能についてはJetpackでやるようになる、という未来を実現するには何が必要か? という話をしました。

結論としては、Bespin要らないんじゃね、ていうか好きなエディタを使わせて下さい、的な所に意見が集中しました。Bespin自体技術デモの性格が強いような物で、IMEがまともに使えない等のどうしようもない問題を沢山抱えていて、はっきり言って「これで(vim|Emacs|秀丸)を捨てられる!」みたいな未来は当分、つうか絶対に来ないと思われるわけで。かといって、eclipseみたいなファットな開発環境をJetpackの中に突っ込むというのも、それはなんか違うんじゃないの? とも思うし。既にデバッグ用にはFirebugを使うのがお勧めみたいな事も言ってる事だし、適材適所でそれぞれ分担すりゃいいじゃないの。頑張ってここ(BespinとJetpackの連携)に色々と機能追加する事にリソースを作よりも別の所で頑張りませんか? というのが、参加者達の総意だった気がします。

「新規のアドオン開発者」として取り込みたいターゲットと思われるWeb制作の現場の人のためには何をすればいいか? という事を次に議論したんですが、参加者の中にWebデベロッパが一人もいないという状況だったので(ひでーな!)想像だけで議論した限りでは、とりあえず、「スクリプトの頭と尻にお約束の構文を書いたら後はwindow等の見慣れたキーワードに普通にアクセスできる」ような状態にしておいた方がいいんじゃないのか、という事を提案しました。Greasemonkeyが受け入れられたのって、Webページの中に埋め込まれるスクリプトと全く同じ感覚で書けたからだと思うんですよね。で、Webのスクリプトを書く時、名前空間の違いとかクロスウィンドウなスクリプトとかそういうのを意識する事ってまず無くて、1つのフレームの中で動くスクリプトを書く感覚が染み着いてるんじゃないのかと。その感覚でスクリプトを書けるかどうかってのが、Webデベロッパに対する第1印象を決定づけるんじゃないのだろうかと。ちなみに、Google Chromeではこの要求に対する回答として、「コンテントスクリプト」という種類の拡張機能であればWebページ内のスクリプトと同じ感覚で書けるという設計になってるようです。

一般ユーザ、つまり今までアドオンを作った事もなければスクリプトを書いた事もないという層の人に対してはどうか。

グラフィカルプログラミングがどうとかそういう話もチラッと出はしたんですが、そこに力を入れるのもやっぱりJetpackの仕事じゃないんじゃないの? と。そういうのは大学の研究室とかで頭のいい人達が散々やってるわけだし。

ポイントは、「簡単にプログラムをかけるかどうか」「知識が無くてもプログラムをかけるかどうか」ではなくて、そもそも「プログラムを書かなくてもカスタマイズできるかどうか」なんじゃないのか、という指摘が出てました。Ubiquityでは複数のコマンドをユーザが組み合わせて使う事を想定している(これはコマンドラインで複数のコマンドをパイプで繋げて使うような感覚)、というのを引き合いに出して、例えるなら「新しい文房具を簡単に作れるようにする」んじゃなくて「ハサミとホッチキスとセロテープ、という道具を手に入れたユーザがそれを好きなように組み合わせて使えるようにする」という風な。そこが大事なんじゃないのか、と。だから例えばAutoPagerizeのSITEINFO on wedataとか、アップローダに勝手改造版スクリプトをアップロードするみたいな、「スクラッチでスクリプトを書ける人以外でも気軽に改良できる、参加できる」ような仕組みがあるといいんじゃないか? という提案が出ました。そういうAPIがJetpackに標準で入ってて、Jetpack GalleryでユーザがSITEINFOをどんどん追加できるようになってて、APIを使っていればそれを簡単に取得できる、みたいな仕組みが用意されていれば、もっと「Webを便利にしてくれる人」の裾野が広がるのではないかなー。と、僕は思ってます。

Jetpack Galleryについて

Jetpack Galleryへの要望・提案としては、まず前述の話にも出た、JetpackのAPIとも連携したwedataのような仕組みがあるといいんじゃないかという話を出しました。

それと、ドキュメント類が不足してる問題への解として、Galleryに登録されてるスクリプト全部をオンラインで検索できるMXRみたいな機能があればいいんじゃないの?という話も出しました。実際、自分がアドオン作る時でも、使い方が分からないAPIはMDCのドキュメントを見るだけでなく、MXRでFirefoxのソースを検索して「ほうほうこうやって使うのか」と調べる場合が非常に多いです(MDCにドキュメントが無ければそれしかないし)。ドキュメントを書け書けと言っても人はめんどくさがって絶対にそんなもん用意せんのだから、じゃあ既にあるコードをスニペットとして全部活用できるようにすりゃあいいじゃないか、という意図での提案です。

現状のGalleryにはレビューの仕組みがないので危険だ、という話も出ました。これは次の話にも関係してるので、そっちで詳しく書く事にします。

APIについて

ここは議論が割れました。あかつかさんはrawのような仕組み、つまりAPIが用意されてない部分にアクセスしてその気になればなんでもできるような余地を残しておいた方がいいんじゃないのか、という立場で、僕とかは、そういう余地は一切残すべきでないという立場で話をしました。

確かに、そういう余地があったからこそ今のFirefoxのアドオンはここまで色々なものが出てきたわけで、そういう自由度をなくしてしまうのは残念だという感覚は、僕もよく分かります。ただ、だからってFrozenじゃない所に全部アクセスできるようにしてしまうというのも乱暴すぎて、それじゃあ結局今のアドオンのように、Firefoxのバージョンが上がったら使えなくなっちゃいました、アドオンのバージョンアップ待ちです、みたいな状況が絶対また出てくる。それって、Jetpackが謳う「安定したAPIでバージョンごとの非互換から解放されます」という話とまるっきり矛盾してしまうわけです。

それに、レビューの問題もある。今はJetpack GalleryにはAMOのような「レビューが通らないと一般公開されない」という風な仕組みはないけど、「最初は無難そうなスクリプトで登録して関係者を欺いて、後からプライバシーぶっこ抜きなコードを追加したものを自動アップデートで一斉配信して、悪意のある開発者がウマー」という事態を防ぐには(今のGalleryではこれが出来てしまう!)、レビューの仕組みは絶対必要になります。

その時に、rawで生のXPCOM等を直接触れてしまう仕組みがあると、レビュワーは結局全部のコードをしっかり精査しないといけないことになる。レビュワーにも知識と経験が求められる。それでは、今のAMOにありがちな「有能なレビュワーのリソースは限られてるので、レビュー完了まで何ヶ月も待たされてしまう。なので、何ヶ月も最新版が公開されないで放置されてしまう。」という風な状態が、Jetpack Galleryにも発生してしまうわけです。

Jetpackが提供するAPIしか使えないのなら、そういう状況に陥る事を防げる。例えば、「Jetpackが提供するAPIでなければファイルにアクセスできないと」いう仕様で、スクリプトの先頭に書く各種の宣言部分で「このパス以下のファイルしか読み込みません」という宣言を書かなければファイルアクセスは一切できない、ファイルアクセスをする時も宣言された範囲のファイルにしかアクセスできない、そういう設計になったとしましょう。すると、レビュワーは冒頭の宣言部分だけ読めば「こいつは危険だな」とか「こいつは安全だ」といった判断ができて、レビューが早く進むようになる。また、スクリプト作者が面倒くさがって「あらゆるファイルにアクセスする可能性があります」みたいな宣言を書くと、レビュワーから疑われていつまでも放置されるから、いつまで経っても公開されないことになる。なのでスクリプト作者は詳細な指定を宣言に嫌でも書かないといけない事になる。そういう風に、みんながみんな自分の利益を最大化する事が結果としてコミュニティ全体の利益に繋がるような仕組みを、今から作る事ができるわけです。

Jetpackという仕組みをゼロから作ろうとしてる今こそ、過去のアドオンの「Firefoxのバージョンに依存する」「レビューにめちゃめちゃ時間がかかって、いつまでも最新バージョンが公開されない」という問題と決別するチャンスなんですよ。なんでそのチャンスをみすみす見逃して、同じ問題をまたJetpackの世界に持ち込むんですか? と。

だいたい、Jetpackが本体に入った後も既存のアドオンの仕組みは依然として残り続けるわけで、偉い人はXULとXPCOMを全部なくすとか言ってるけど実際にコアのコードを書いてる人曰く「そんなの絶対無理(加藤さん・談)」なわけで、だったら「安定したAPIのJetpackで、まずは作ってみる。それでできない事が出てきたら、自由度の高い(でもAPIが不安定な)既存のアドオンの仕組みで作る方にステップアップする」というスキームでやっていけばいいじゃないですか。と。

他には、プラットフォーム(XULRunner、Geckoのレイヤ)でやるべき事とJetpackがやるべき事はちゃんと切り分けて欲しいという話も出ました。例えば音声取り込み用のAPIなんてのは、Jetpackにプラットフォーム別のバイナリをブチ込むんじゃなくて、Geckoで面倒見るべき話でしょう。そのせいでJetpackの構成ファイルが何MBにもなってダウンロードも起動も遅くなって、その一方で、ほんのちょっと起動を速くするためにFUELを廃止するって、何ですかそれは? 何か大事な事を履き違えてるんじゃあないですか? と。

とにかく、Jetpackというプロジェクトが何にフォーカスしているのか、プロジェクトのポリシーが僕には全然見えない。Jetpackをウォッチし続けてるcon_mameさんもteramakoさんも、Jetpackの方針が揺らいでるように見えるという風におっしゃってました。なので、Mozilla Corporationに行く時にはGomitaさんとあかつかさんにはぜひとも、Jetpackが最優先しようとしているのは何なのか? 他のものを犠牲にしてでも貫かないといけないと考えているポイントはどこなのか? というプロジェクトの指針を明らかにしてきて欲しい、という事をこのミーティングの参加者達の最大の関心事として託しておきました。

余談:イノベーションの話

ミーティングの最中にtwitterで言及されてMicrosoftのAzureの話を読みました。既存の開発者のために腐心しすぎるとイノベーションが起こらなくなってしまう、僕のようなロートルがいつまでもでかい顔して居座っていられるような状況にしてしまっては新しいプレーヤーが参入して来れない、というのは実に耳の痛い話だと思いました。

もちろん将来的に、いつかはJetpack自体も今のアドオンのように「もう時代遅れだ」と冷遇され切り捨てられる時代が来るのかもしれません。が、それはその時に考えればいい事なんじゃないかと僕は思います。

しかし自分が特に耳が痛いのは、既存プレーヤーがデカいツラしやがるという話です。自分の既得権益、自分の居場所を守るために必死に抵抗すればするほど、自分自身が今のまま何も変わらずに一線に立ち続けようとすればするほど、自分がすがりついている物の未来を奪う事になって、自分も他の人も不幸にする。「シーンの最前線から退く覚悟はあるか?」というエントリは、本文の内容は全然この話題と関係ないし趣旨も違うけど、でも僕は最近この事を考えがちなので、タイトル見ただけでこの事に結び付けて受け取ってドキッとしました。みんなのためを思うなら、老兵は黙って去って、不幸になるのは自分だけで終わらせないといけないんじゃないのか。

余談:Mozillaの生き残り戦略について

Microsoftは、Windows 95上でWindows 3.1用のアプリの動作を保証するために、Windowsの側で頑張って対策をしてまで互換性を維持したそうです。VistaのUltimateだったかWindows 7だったかではWindows XPとの互換性を維持するために仮想マシンまで用意しました。昔のAPIでも、「obsolete」という風にはMSDNに書いてあっても、動く事は動くという状態でずっと残ってるそうです。そういう「とにかく過去のソフトウェア資産を使い続けられるようにする」戦略をMicrosoftは取り続けて、製品はいまいち垢抜けない物ばかりになってしまいましたが、その結果としてシェアはものすごい事になりました。圧倒的多数の凡人がちょっとずつ落とすお金のおかげで、Microsoftは食えてます。

Appleは、ジョブズが思い描く「素晴らしいプロダクト」を目指すために、古いAPIを容赦なく切り捨ててがんがんアップデートしてます。Mac OS Xで「obsolete」となったAPIは、2つ後のバージョンくらいではもう綺麗さっぱり消えてしまうそうです。その結果、ついていけない脱落者が多くなるし、世界の過半数を占めるようなものすごいシェアは獲れてない。けれども強烈な信者ができて、彼らのおかげでAppleは食えてるわけです。

Googleは広告のシェアをガッツリ掴む事で、Operaはモバイル市場をガッツリ掴む事で、食っていってますよね。

で、Mozillaはどんな道を歩むつもりなんでしょうか。

今までの路線を見る限り、安定版リリースの寿命は短い(次のメジャーバージョンが出たら前のバージョンは半年で更新が打ち切られる)し、安定したAPIになるとか言ってたFUELもメジャーバージョン2つと保たずに黒歴史になってしまいそうだし、どう見てもゲイツよりはジョブズの生き方に見える。でも、ジョブズがジョブズの生き方を貫けてるのはあくまで、信者がお金を出して高い製品を買ってくれてるからですよね。でもこれって、言ってみればブランド物の生き残り戦略ですよね。Mozillaは物を売ってないから、その戦略じゃあ明日のおまんまが食えなくなるんじゃないの? 開発者を養っていけなくなるんじゃないの? そこが僕は心配でならないです。「いやオープンソースでバザールだから開発者はみんな善意で関わり続けてくれるんだよ」、という反論はすぐに思いつきますけど、今は世間はみんなWebkitに流れてますよね。オープンソースの開発者も、Geckoより将来有望な(ように見える)Webkitに流れていくんじゃないですか? その時、Geckoに関わり続けてくれる開発者はどれくらい残るんでしょうか?

上に挙げた各社のどれとも違う別の道を歩む、というのが多分、Mozillaにとっての模範解答なんでしょうけどね。その「別の道」ってのが一体何なのか、僕にはまだよく分からんです。

汎用的なアンドゥ・リドゥ機能を提供するoperationHistory.jsの、理解しておかないといけない特性 - Jan 18, 2010

前のエントリではoperationHistory.jsの基本的な使い方を説明しましたが、次は、これを使うにあたって理解しておかないといけないポイントを解説しようと思います。


UIに対して行うアンドゥ可能にしたい操作の中には、他のアンドゥ可能にしたい操作を内部で呼び出すものがあるでしょう。例えばブックマークフォルダの内容をまとめてタブで開く時などに使われるtabbrowserのloadTabs()メソッドは、新しい空のタブを開くaddTab()メソッドを内部で呼び出しています。「新しいタブを開く」操作と「ブックマークフォルダの内容をまとめてタブで開く」操作をアンドゥ可能にする際は、これらのメソッドに対してそれぞれアンドゥ・リドゥの処理を定義することになります。

このように「アンドゥ可能な処理」同士が入れ子になっている時、operationHistoryは、それらすべてのアンドゥ可能な処理について、実行が始まった順番通りに履歴に登録を行います。例えば「loadTabs()で3つのタブを開く」という場面では、以下のように処理が行われます。

  1. loadTabs()実行。アンドゥ可能な操作Aが始まる。
    1. 対応する履歴項目A'が登録される。
    2. 1つ目のタブのaddTab()実行。アンドゥ可能な操作Bが始まる。
      1. 対応する履歴項目B'が登録される。
      2. アンドゥ可能な操作Bが完了する。
    3. 2つ目のタブのaddTab()実行。アンドゥ可能な操作Cが始まる。
      1. 対応する履歴項目C'が登録される。
      2. アンドゥ可能な操作Cが完了する。
    4. 3つ目のタブのaddTab()実行。アンドゥ可能な操作Dが始まる。
      1. 対応する履歴項目D'が登録される。
      2. アンドゥ可能な操作Dが完了する。
  2. アンドゥ可能な操作Aが完了する。

この時の操作B~Dは、操作Aが完了する前に、操作Aの中から呼び出されています。そのため、これらに対応する履歴項目B'~D'は、完了していない操作Aに対応する履歴項目A'の子項目として登録されます。つまり、このような親子関係が形成されます。

  1. 履歴項目A'
    1. 履歴項目B'
    2. 履歴項目C'
    3. 履歴項目D'

他の履歴項目の子項目として登録された履歴項目は、親となる項目の一部として扱われます。子項目になった履歴項目は、アンドゥ可能な操作の履歴の一覧には登場せず、「最大100回までアンドゥ可能」といった場合、子項目の数はそのカウントに含まれないことになります。

この履歴項目A'に対してアンドゥを指示すると、operationHistoryは以下の順で処理を行います。

  1. 履歴項目D'のonUndo()
  2. 履歴項目C'のonUndo()
  3. 履歴項目B'のonUndo()
  4. 履歴項目A'のonUndo()

この時の実行順序は項目の登録時の逆順であることに注意して下さい。なお、後で解説しますが、遅延処理のための仕組みによってこの実行順序は保証されます。前の項目のonUndo()が終わる前に次の項目のonUndo()が始まるという事はありません。

逆に履歴項目A'に対してリドゥを指示すると、operationHistoryは以下の順で処理を行います。

  1. 履歴項目A'のonRedo()
  2. 履歴項目B'のonRedo()
  3. 履歴項目C'のonRedo()
  4. 履歴項目D'のonRedo()

今度は、履歴項目の登録順の通りであることに注意して下さい。こちらについても、実行順序はこの通りに保証されます。

操作をアンドゥできるようにする際は、操作の中から呼び出している別の操作がアンドゥ可能である場合、上記の実行順の事を念頭に置いてアンドゥ・リドゥ用の処理を記述する必要があります。例えば上記の例であれば、アンドゥの際は以下の順でアンドゥのための処理が進みます。

  1. 3つ目のタブを開く操作のアンドゥ(タブが閉じられる)
  2. 2つ目のタブを開く操作のアンドゥ(タブが閉じられる)
  3. 1つ目のタブを開く操作のアンドゥ(タブが閉じられる)
  4. 複数のタブを開く操作のアンドゥ

これを見ると、4番目の項目の時点ではもう何もする必要が無いということが分かります。もし4の時点で、3つのタブを開いたのでそのアンドゥ操作としてタブを3つ閉じようとしても、閉じる対象のタブが存在しないためエラーになってしまいます。他のアンドゥ可能な操作を内部で呼び出す操作については、アンドゥ・リドゥの内容が重複しないように注意してください。

汎用的なアンドゥ・リドゥ機能を提供するoperationHistory.jsの基本的な使い方 - Jan 11, 2010

Undo Tab Operationsの核であるoperationHistory.jsは、タブに限らずいろんな操作に対してアンドゥ・リドゥを実装しやすくするための汎用のライブラリとして設計しています(一応)。こいつの使い方を、自分の頭の中の整理も兼ねて少しずつ解説していこうと思います。


  • 最新版はCOZMIX NGにあります。
  • 名前は「UI Operations History Manager」としています。長いので、以下「operationHistory」とだけ呼んでおきます。
  • ライセンスはMITライセンスとします。
  • Firefox 2以降でないと動きません。

読み込み方

まず、読み込みの方法。以下のようにJavaScriptのファイルを読み込ませるだけでOKです。

<script src="lib/operationHistory.js"
        type="application/javascript"/>

複数のアドオンでこのライブラリを読み込んでいる場合、最もリビジョンの新しい物が使われます。今後はAPIは変えないor後方互換を維持していくつもりなので、使う方はあんまり気にしないで使えるはずです。

operationHistoryの各機能には window['piro.sakura.ne.jp'].operationHistory でアクセスできます。以下の説明ではサンプルコードを短くするために、 OH という変数でこれを参照しているものとします。

var OH = window['piro.sakura.ne.jp']
               .operationHistory;

処理をやり直し可能にする

operationHistoryを使って任意の処理をアンドゥ・リドゥ可能にしたい時は、その処理を以下のように実行するようにします。

OH.doOperation(function() {
  // 任意の処理
});

doOperation()の引数に関数を渡すと、それがその場で実行されます。実行時のthisOH自身を指していますが、普通に分かりにくいんで、クロージャを使うなり何なりして好きなように書くといいと思います。

var MyAddon = {
  myFeature : function() {
    var self = this;
    OH.doOperation(function() {
      self.myInternalMethod();
    });
  },
  myInternalMethod : function() {
    // 何かの処理
  }
};

で、これだけだとまだアンドゥ・リドゥはできません。doOperation()に対して以下の引数をさらに指定してやる必要があります。

  • 履歴の名前(文字列、省略可能)
    • "MyAddonOperations" とかそんな感じで好きに名前を付けて下さい。省略すると、履歴の対象のウィンドウが指定されている場合は "global" 、そうでなければ "window" になります。
    • Undo Tab Operationsでは "TabbarOperations" という名前を指定してます。
  • 履歴の対象になるウィンドウ(nsIDOMChromeWindow、省略可能)
    • 拡張機能の名前空間で普通に見える所のwindowです。省略すると、ウィンドウ単位の履歴ではなく、クロスウィンドウな単一の履歴となります。が、動作が怪しいので今の所はウィンドウ単位での使い方だけ推奨しておきます。
  • 履歴エントリ(オブジェクト、必須)
    • オブジェクトリテラル、カスタムクラスなど、何でもOKです。詳しくは後述。

やり直し可能にしたい処理自体と合わせると、doOperation()は最大で4つまでの引数を取るという事ですね。引数は全部型が違う(関数、文字列、DOMWindow、オブジェクト)ので、doOperation()はそれらを受け取った後に、どれがどれなのかを自動的に判別します。なので引数はどの順番で指定しても構いません。

実際のアンドゥ・リドゥ処理を書く(関数を使うやり方)

実際のアンドゥ・リドゥ処理は、履歴エントリになるオブジェクトのプロパティとして関数で定義します。

var entry = {
  // 内部名
  name   : "undotab-addTab",
  // メニュー等に表示する「やり直す処理の名前」
  label  : "タブを開く",
  // アンドゥ時に実行される内容
  onUndo : function(aParams) {
    gBrowser.removeTab(this.tab);
  },
  // リドゥ時に実行される内容
  onRedo : function(aParams) {
    this.tab = gBrowser.addTab();
  },
  // 以下、任意のプロパティを好きなようにどうぞ
  tab : null
};

OH.doOperation(
  function() { // やり直し可能にする処理
    entry.tab = gBrowser.addTab();
  },
  'MyAddonOperations', // 履歴名
  window,              // 処理対象ウィンドウ
  entry                // 履歴エントリ
);

onUndoという名前のプロパティで関数を定義しておくとアンドゥ時にそれが実行されます。onRedoはリドゥの時に実行されます。どちらもthisは履歴エントリのオブジェクト自身になりますので、まあ見た通りで分かりやすいんじゃないかと思います。クロージャ使って書いても全然構いません。

namelabelは、文字列で好きなように名前を付けて下さい。定義しなくても使えますが、デバッグの時にはあると便利ですし、DOMイベントを使った記法(後で解説します)の時には無いと困ります。

アンドゥする・リドゥする

上記のようにして履歴に登録した処理は、以下のようにしてアンドゥ・リドゥできます。

// アンドゥ
OH.undo('MyAddonOperation', window);
// リドゥ
OH.redo('MyAddonOperation', window);

undo()redo()の引数には、doOperation()に対して指定したものと同じ履歴名と処理対象のウィンドウを渡します(こちらも引数の指定順は任意です)。ここではまだ「タブを開く操作」しか書いていませんが、同じ履歴名で「タブを閉じる操作」「タブを移動する操作」などに対してそれぞれアンドゥ・リドゥの処理を書いてやれば、線形にそれらをアンドゥ・リドゥできるようになります。

また、「戻る」「進む」のドロップダウンメニューで項目を指定してそこまで一気に飛ぶのと同じように、goToIndex()で履歴項目のインデックスを指定してそこまで一気にアンドゥする・リドゥする事もできます。

// 現在の位置を得る
var history = OH.getHistory('MyAddonOperation', window);
var current = history.index;
OH.goToIndex(current-3, 'MyAddonOperation', window);

getHistory()は、登録済みの履歴項目の全エントリを格納したオブジェクトを取得するメソッドです。それで取得したオブジェクトのindexプロパティで現在のフォーカス位置を得られるので、上の例ではそこから3つ手前に飛ぶ事になります。

要素やウィンドウに固有のIDを使う

ここまでの説明で既に疑問に思った人もいると思いますが、例えばこんな場合。

function NewTab() {
  var entry = {
    name   : "undotab-addTab",
    label  : "タブを開く",
    onUndo : function(aParams) {
      gBrowser.removeTab(this.tab);
    },
    onRedo : function(aParams) {
      this.tab = gBrowser.addTab();
      gBrowser.selectedTab = this.tab;
    },
    tab : null
  };
  OH.doOperation(
    function() {
      entry.tab = gBrowser.addTab();
      gBrowser.selectedTab = entry.tab;
    },
    'MyAddonOperations',
    window,
    entry
  );
}

function MoveTab(aTab) {
  var entry = {
    name   : "undotab-moveTab",
    label  : "タブを移動する",
    onUndo : function(aParams) {
      gBrowser.moveTabTo(this.tab, this.oldPosition);
    },
    onRedo : function(aParams) {
      gBrowser.moveTabTo(this.tab, this.newPosition);
    },
    tab : null
  };
  OH.doOperation(
    function() {
      entry.tab = aTab;
      entry.oldPosition = aTab._tPos;
      gBrowser.moveTabTo(aTab, 3);
      entry.newPosition = aTab._tPos;
    },
    'MyAddonOperations',
    window,
    entry
  );
}

NewTab()を実行→それで開かれたタブに対してMoveTab()を実行→アンドゥ→アンドゥ→リドゥ→リドゥ という順に操作すると、2つ目の履歴項目のリドゥ時にタブが見つからないせいでエラーになってしまいます。こうならないように、処理対象の要素は固有のIDなどで識別してやらないといけません。

operationHistoryにはそのために、要素に対して一意なIDを自動的に付与してそれを元に要素を検索する仕組みがあります。先の例を安全に書くと、以下のようになります。

function NewTab() {
  var entry = {
    name   : "undotab-addTab",
    label  : "タブを開く",
    onUndo : function(aParams) {
      var tab = OH.getElementById(this.tab,
                   gBrowser.mTabContainer);
      gBrowser.removeTab(tab);
    },
    onRedo : function(aParams) {
      var tab = gBrowser.addTab();
      OH.setElementId(tab, this.tab)
      gBrowser.selectedTab = tab;
    },
    tab : null
  };
  OH.doOperation(
    function() {
      var tab = gBrowser.addTab();
      entry.tab = OH.getElementId(tab);
      gBrowser.selectedTab = tab;
    },
    'MyAddonOperations',
    window,
    entry
  );
}

function MoveTab(aTab) {
  var entry = {
    name   : "undotab-moveTab",
    label  : "タブを移動する",
    onUndo : function(aParams) {
      var tab = OH.getElementById(this.tab,
                   gBrowser.mTabContainer);
      gBrowser.moveTabTo(tab, this.oldPosition);
    },
    onRedo : function(aParams) {
      var tab = OH.getElementById(this.tab,
                   gBrowser.mTabContainer);
      gBrowser.moveTabTo(tab, this.newPosition);
    },
    tab : null
  };
  OH.doOperation(
    function() {
      entry.tab = OH.getElementId(aTab);
      entry.oldPosition = aTab._tPos;
      gBrowser.moveTabTo(aTab, 3);
      entry.newPosition = aTab._tPos;
    },
    'MyAddonOperations',
    window,
    entry
  );
}

getElementId()は、要素に一意なIDが付いていなければ新しいIDを生成て設定した上でそのIDを、既にIDが付いていればその値を、文字列として返します。IDは普通のid属性ではなく別の属性名で保存されるので、通常の動作を破壊することはありません。

getElementById()は、そのID文字列をキーとして要素を検索するメソッドです。tabbrowserの場合はタブなどの内部の要素は普通のdocument.getElementById()等では取得できないのですが、getElementById()はID名の文字列以外に要素ノードを渡すと、その要素の子孫だけを検索するようになります。

ここでは、タブを開く操作のリドゥにおいてsetElementId()も使用しています。これは、既に生成されたID文字列を新しく復元された要素に付与することで、その要素を元の要素の代わりとして参照できるようにするためです。


とりあえず、まずはこの辺だけ解説しておきます。

→続き

汎用アンドゥ・リドゥのための仕組みがだいたいできてきたと思う - Jan 11, 2010

Undo Tab Operationsの開発を通じて実装をこねくり回してた汎用のアンドゥ・リドゥ用のライブラリだけど、最低限必要そうな一通りの機能を実装し終えた……と思う。

最初はもっとコンパクトになるかなと思ってたんだけど、なんだかんだで膨らんで35KBちょいになった(2010年1月11日現在)。ライブラリの使い方は……Undo Tab Operationsのソース読んで実際の使われ方を見た方が話が早いかも。


以下、ライブラリの使い方の説明じゃなくてただの苦労話です。

当初は、単純に以下のようにしようと思ってた。

  • 1つの履歴項目に1つの操作が対応する。
  • アンドゥ用・リドゥ用の処理を関数オブジェクトとして指定する。

しかし実際やってみるまでもなく、これだと考慮しないといけないケースがあまりに多くなりすぎる。例えばタブを開く操作だけでも、「タブバー上のボタン」「ブックマーク」「リンク」等々色々ある。それら1つ1つに対してアンドゥ・リドゥの処理を定義していくのはさすがに無理がある。また、例えば「タブバーのボタンで新しいタブを開く処理に対応する関数」に対してアンドゥ・リドゥの処理を定義したとして、その関数が他の処理の中から呼ばれないという保証はどこにもないわけで、アンドゥ用の処理がかぶったら、タブが2つも3つも開き直されたり、その逆に2つも3つも閉じられたりしかねない。これは危険すぎる。

という事くらいはすぐに思いついたので、次にこんな風に考えた。

  • gBrowser.addTab()gBrowser.moveTabTo()などの基本的な関数それぞれに対してアンドゥ・リドゥの処理を定義する。
  • gBrowser.loadOneTab()(内部でaddTab()moveTabTo()を呼んでいる)のような関数の存在も考慮して、1つの「やり直し可能な処理の単位」の中で行われたやり直し可能な処理はすべて、トップレベルの履歴項目に子供としてぶら下げる。
    • アンドゥ・リドゥの際は、それらすべてを実行する。
  • ただし、いくつかの処理については例外的に、それより下位の項目の実行をキャンセルして自前で処理するようにする。
    • 例えばgBrowser.swapBrowsersAndCloseOther()は場合によっては元のウィンドウを閉じてしまうため、ウィンドウを開き直す→ロード完了を待ってからタブを開き直す、という風な事をしないといけない。ウィンドウが開かれるまでの間に下位の項目(タブを開く、タブを閉じる等)のアンドゥ処理が走るとおかしな事になる。なので、下位の項目のアンドゥ処理はキャンセルして、swapBrowsersAndCloseOther()のアンドゥ処理の中で全部完結させるようにする。

例外を設けたのは、「前の履歴項目のアンドゥ処理の完了を待ってから次の履歴項目のアンドゥ処理を始める」という風な、非同期処理を考慮した仕組みを当初備えていなかったせい。なんでその仕組みを先に作らなかったのかというと、作るのがめんどかったからの一言に尽きる。

で、このような仕様で開発を進めて、Undo Tab Operationsについてはとりあえず素のFirefox上でならまともに使えるようになってきたかなあと思ったので、マルチプルタブハンドラとの連携に着手し始めた。そしたら破綻した。

例えばマルチプルタブハンドラは、タブ1つだけのドラッグ&ドロップでの移動を検知して選択されたタブ全部をその近くに移動するようになってるけど、これをアンドゥ可能にしようと思うと、そのアンドゥの処理が始まる前にUndo Tab Operationsによって行われるmoveTabTo()のアンドゥでタブの並び順が変わってしまうので、最終的なタブの並び順がグチャグチャになってしまう。手っ取り早く解決しようと思うと、Undo Tab Operationsによって行われるタブの移動のアンドゥ処理は全部キャンセルして、マルチプルタブハンドラ側で面倒を見てやった方が、書くのは簡単なわけです。

しかし、そんなことをそれぞれのアドオンがやり始めたら、絶対にどっかで考慮漏れが起こるわけですよ。Undo Tab Operationsとマルチプルタブハンドラだけだったら問題が起こらなくても、そこにツリー型タブや他のアドオンが加わってくると、互いにアンドゥ処理の優先権の取り合いになってしまうのは目に見えてる。

そういう未来が予想できてしまったので、ついに観念して、非同期処理に真面目に対応することにした。で、結局以下のようになった。

  • addTab()removeTab()など、それぞれの基本的な関数に対してアンドゥ・リドゥの処理を提供する。
  • アンドゥ処理には処理待ち機能を提供し、個々の段階の処理が走る時には必ず、前の段階の処理が終わっている事を保証する。

あとついでに、関数オブジェクトをそのまま履歴項目に使うようにすると履歴項目が増える度にクロージャが増えていってメモリリークの温床になりそうだなあと思ったので、DOMのイベントを監視することでも同じ事ができるようにAPIを整備した。Undo Tab Operations 0.2.2010011001以降ではこっちの方法を使ってそれぞれのアンドゥ・リドゥ処理を実装してある。

処理待ちにはJSDeferredを使ってもよかったんだけど、他のライブラリには依存させたくなかったので、JavaScript 1.7以降のジェネレータ・イテレータと継続関数で実現してみた。イベントオブジェクトのaEvent.wait()で処理待ち状態になって、aEvent.continue()で処理の完了を通知するというスタイルにしてある。

クロージャを使っても使わなくても、処理対象のタブを特定するには一意なIDが無いとどうにもならないなあと思ったので、汎用の「要素ノードに自動生成でIDを付与する」とかの機能もライブラリに含めることにした。これを使うことで、閉じられたタブに対応する開き直されたタブを確実に取得できるようになってる。

実際使ってみるとなかなか妙な感じですね。タブをウィンドウ外にドロップしてタブを切り離し→Shift-Ctrl-Zで元に戻す→Shit-Ctrl-Yでまた切り離す なんてことができて、キモくて面白いです。

Firefox上のあらゆるタブ関連の操作を一次元で記録してアンドゥ・リドゥ可能にするアドオン(実験段階) - Jan 07, 2010

こないだから作ってる汎用のアンドゥ・リドゥ用ライブラリを使って、タブ関係の操作をなんでもアンドゥできるようにするアドオンを作ってみた。名前はそのままUndo Tab Operationsです。

ライブラリ自体が実験段階だし、そのライブラリの使い方も(自分で作っておきながら)まだよく把握しきれてないので、練習がてらという感じです。動作は全体的に非常に怪しいです。あと、これを作る上で「あーこういう機能が必要だよな」と躓く度にライブラリの方に手を入れているので、一体何が目的だったのかもうワケが分かりません。

そう。目的。そもそもは「ツリー型タブでウィンドウをまたいだタブの移動を取り消せるようにしたい」的な所が出発点だったはずなんですよね。なのにどうしてこんなに遠回りをしているのか。富豪的にも程がありますよね。

まあ、「リンクからタブを開く時に今のタブのすぐ右にタブを開きたい、ただし連続してリンクを開く時は開いた順に整列させたい」という目的を達成するためだけに、すべてのタブの親子関係を完全に保持する、なんていうめちゃめちゃ無駄なことに力を注ぐような僕のやる事ですからねぇ。

とりあえずこのアドオンで一通りの基本的なところをカバーして、次に親子関係が絡まない単純な応用という事でマルチプルタブハンドラを、最後にツリー型タブを対応させるという感じで作業を進めていこうかなー。と。思ってますけどどうなるかは分かりません。現段階でものすんごいバグバグなので、ひょっとしたら全部お蔵入りになるかもね。

Firefox上のあらゆる操作を一次元で記録してアンドゥ・リドゥ可能にするライブラリ(実験段階) - Jan 04, 2010

ツリー型タブのAPIでこんなのが欲しいというのがあったら言っておくれと書いたところ、Alice0775さんからタブバーのドラッグ操作とかツリーのドラッグ操作とかをアンドゥする機能が欲しい(大意)という要望をいただいた。

そういえばこの前のMozilla勉強会の後の懇親会で、新しいタブが開かれたり新しいウィンドウが開かれたりした時に「戻る」ボタンで元のタブや元のウィンドウに戻れないことについて、あらゆる操作がアンドゥ可能になってないといけないんじゃないの?とかそんな感じの話が出ていたと思う。なので実験的にそういう物を作り始めてみた自動テスト)。

var current = TreeStyleTabService.currentTabbarPosition;
window['piro.sakura.ne.jp'].operationHistory.doUndoableTask(
  // やり直し可能にしたい処理
  function() {
    TreeStyleTabService.currentTabbarPosition = newPosition;
  },

  // 履歴の名前(省略可)
  'TabbarDNDOperations',

  // ウィンドウごとの履歴の場合の対象ウィンドウ(省略可)
  window,

  // 履歴の項目
  {
    // 項目名
    label  : 'タブバーの位置変更',
    // アンドゥの時に実行する内容
    onUndo : function() {
      TreeStyleTabService.currentTabbarPosition = current;
    },
    // リドゥの時に実行する内容(省略可)
    // →省略時は上記の「やり直し可能にしたい処理」が自動的に
    //   onRedoとして登録される
    onRedo : function() {
      TreeStyleTabService.currentTabbarPosition = newPosition;
    }
  }
);

という感じでアンドゥ・リドゥ時の動作を登録して、window['piro.sakura.ne.jp'].operationHistory.undo('TabbarDNDOperations', window)とかwindow['piro.sakura.ne.jp'].operationHistory.redo('TabbarDNDOperations', window)とか書くとヨロシク処理してくれる……という風な感じ。「あらゆる操作を一次元で記録して」とタイトルに書いてるけど、自動的に記録するんじゃなくてアドオン作者が手作業で記録する前提で、「履歴の記録」「履歴の呼び出し」の所を管理する手間を軽減するだけのライブラリなんで、そこの所はお間違えなきよう。

関数をそのまま登録するというのが乱暴と言えば乱暴なんだけど、柔軟性を高くしようと思ったらこうするのが手っ取り早いかなーって思いまして。一応ウィンドウごとの履歴とグローバルな履歴の両方を持てるようにしてみてる。イメージ的には、Adobe製品のヒストリ機能のような物を目指してる。

で、枠組みは用意したんだけど、タブバーの位置の移動みたいな単純な機能はいいとして、ツリーの移動みたいなややこしい物をどうやってアンドゥ・リドゥさせるかで暗礁に乗り上げてる。

  • マルチプルタブハンドラとの連携も考慮する必要がある。
  • ツリーの移動だけじゃなく複製も考慮しないといけない。
    • 複製のアンドゥは複製されたタブのクローズでいいけど、リドゥは? もう一回複製させる?
  • 移動・複製元のタブの一部が閉じられていた場合を考え始めると破綻する。
    • ツリーの移動だけを記録すると、ツリーを移動した→移動先のツリーのタブを1つ閉じた という操作の後でアンドゥした時に、移動されたツリーを元に戻せない(元のウィンドウに戻すべきタブが見つからなくなる)。
    • となると、タブを閉じる・タブを開く・(ツリーに関係なく)タブを移動する、といったあらゆる動作を記録しないといけない。

なんとなく、ツリー型タブからは分離して「タブバー上のあらゆる操作をアンドゥ可能にするアドオン」を新しく作った方がいいような気がしてきた。で、ツリー型タブが入ってる時はそいつのアンドゥ履歴の中に「タブバーの位置変更」の項目が混ざってくる、みたいな連携の仕方。

DOM2 Eventsの機能でアドオン同士を連携させる方法 - Dec 27, 2009

ツリー型タブに他のアドオン向けのAPIを加えていきたいという話を書いた関係でコードをあちこち見直して書き直していて、カスタムイベントを通知→イベントを捕捉した側でキャンセル→イベントを通知した側でキャンセルを検知して処理を中断 という事をやりたくなって調べた結果をmodestにまとめてみた。

ここに書かずにmodestの方に書いたのは、話のレベル的に今更感があったのと、わりと基礎的な話だからこんな世界の果てのオメガギークの日々の下らない愚痴に混ぜて書くだけにしたら誰にも見てもらえないんじゃないか(それよりももっと多くの人に読んで貰って取り入れてもらって、アドオン同士の連携を取りやすい世の中になってくれると嬉しい)と思ったというのと、というあたりが理由です。

でもなんかほんと今更過ぎるというかものすげえ基本的なことを得意げに解説してしまった気がしていて、失敗だったかなあとちょっとブルーです。

ツリー型タブとTabberwockyを同時に入れられるようにしたよ - Dec 25, 2009

Tree Style Tab 0.8.2009122501Tabberwocky用のコードを入れました。とりあえず、選択範囲のリンクをタブで開く機能と、新しいタブを現在のタブのすぐ隣に開く機能については、ちゃんと動くことを確認してます。他にうまく動かない機能があったら言って下さい。Tab Mix Plusに比べたら全然コードの量も少ないんで、たぶん対応できると思う。

Tab Mix Plusと組み合わせた時の問題もどうにかしようと思ったんだけど、見てみたらTMPの中にTST用のコードが入ってたので、さっくり諦めた。お互いがお互いに手を出すともはや収拾が付かなくなるから。なのでちょうどいい機会だと思って、TMPのフォーラムに「いいかげんこの状況なんとかしようよ」的な提案を書き込んでみた。そのついでに、ソースコード中で「PUBLIC API」と書いておきながら説明を書き忘れてたAPIをドキュメントに追加した

他のアドオンと連携を取りやすくするためのAPIを加えるのはやぶさかじゃないので、要望があれば是非言ってください。最近の例では、TreeStyleTabService.currentTabbarPositionTreeStyleTabService.treeViewEnabledはメールで「こういう事をしたいんだけどどうすればいいのさ」と問い合わせを受けたので追加したAPIです。

画面の描画内容を一時的にロックしておいて、裏であれこれして最後にまとめて描画させる方法の再考 - Dec 24, 2009

画面の描画を一時停止する方法を先日書いたけど、案の定というかやっぱりというか、重大な弊害があることが分かった。また、その弊害にぶち当たらない安全なやり方も見つけることができた。

安全に画面の描画を一時停止・再開する方法は、以下の通り。

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); // ここでやっと描画が再開される

以下、前のエントリに書いたやり方にどういう弊害があるのか、および、このエントリで紹介するやり方の方がどのくらい安全なのかについて詳しく説明する。

続きを表示する ...

ツリー型タブ0.8.2009122103 - Dec 21, 2009

ツリー型タブはバグをつぶし始めたらきりがなくなってきたので、適当なところで打ち切ってリリースしました。バグ報告への返信で「I'll update as soon.」とか書いちゃったからというのもある。

画面の描画内容をロックするアレについては、結局ライブラリ化しましたwindow['piro.sakura.ne.jp'].stopRendering.stop()で描画停止、window['piro.sakura.ne.jp'].stopRendering.start()で再開します。複数の機能で停止/再開をネストしても大丈夫なように、呼び出し回数をカウントするようにしてあります。start()し忘れると変なことになるので注意して下さい。

他にもいくつかAPIを追加したので、自作アドオンと連携して動作させてみたい人は参考にして下さい。

Page 10/239: « 6 7 8 9 10 11 12 13 14 »

Powered by blosxom 2.0 + starter kit
Home

カテゴリ一覧

過去の記事

1999.2~2005.8

最近のつぶやき

オススメ

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