たまに18歳未満の人や心臓の弱い人にはお勧めできない情報が含まれることもあるかもしれない、甘くなくて酸っぱくてしょっぱいチラシの裏。RSSによる簡単な更新情報を利用したりすると、ハッピーになるかも知れませんしそうでないかも知れません。
の動向はもえじら組ブログで。
宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能!
既にご存じの方もいらっしゃるかとは思いますが、オライリーから「Firefox Hacks Rebooted」という本が出ました。3年前に出た「Firefox 3 Hacks」の続き……でもないのですが、似たようなコンセプトで「今」の話をまとめた本です。
本1冊を通して1つのストーリーに基づいて何かを体系立てて解説する、という本ではなくて、各著者が自分の得意分野で好きなように書きたい事を書きまくった本、というのが実情を正しく言い表している気がします(元々、オライリーの「○○ Hacks」というタイトルは「なんでもあり」のブランドなんだそうで……その言葉に甘えてしまいました)。
以下、各章の対象読者層と思われる層、内容の簡単な紹介と、定量的に傾向を把握するための手がかりとしてページ数と全体に対する割合を表にしてみました。これを見ると一発で分かりますが、僕(4章と6章の一部を担当)の暴走が著しいですね。やばい。
章 | 主な対象読者層 | 内容 | ページ数 | 全体の中でのパーセンテージ |
---|---|---|---|---|
1章 | エンドユーザ | Firefoxの新機能紹介など基本的な事。Firefox 3.6とFirefox 4以降との間で何が変わったのかのまとめ。 | 54 | 11% |
2章 | 開発者寄りのユーザ | VimperatorとKeySnailの解説。と、Twitter用アドオンの解説。ブラウザとしてのFirefoxをガンガン使いこなしていく人向け。 | 42 | 9% |
3章 | アドオン開発者 | Add-on SDKを使ったアドオン開発のチュートリアル。アドオンの開発をこれから始めたいという人向け。 | 68 | 14% |
4章 | アドオン開発経験者 | Bootstrapped Extensions、ChromeWorker、e10sなど、Add-on SDKよりも下のレイヤの技術の解説。今既にアドオンを開発してるという人や、Add-on SDKではできない・標準ライブラリでカバーされてない範囲の事に手を出そうと思ってる人向け。 | 156 | 33% |
5章 | Webデベロッパー | Firefoxで利用できるHTML5関連技術やECMAScriptの紹介。Webデベロッパー(主にフロントエンド)向け。 | 98 | 21% |
6章 | アドオン開発経験者、Webデベロッパー | ハードウェア寄りの話と、その他のこぼれ話。アドオンやWebアプリの開発で、ネイティブアプリ並の事をやりたい人向けの話が多め。かも。 | 56 | 12% |
自分の担当箇所だけ細かく見てみると、ページ数と全体に対する割合はこんな感じです。
調子に乗って脳汁ドバドバ出して書きまくる→ページ数が増える→値段上がる→損益分岐点も上がる という危険なコンボが発動してしまったためかお値段高めとなっておりますが、自分の担当箇所も他の方の担当箇所もひっくるめて、腰を据えて読むに値する情報ばかりなんじゃないかなーと思っております。Webで難なく読める文章の長さと、(紙でも電子でも)書籍の形になってた方が読みやすい文章の長さって、やっぱり違うと思いますしね。
本書のサポートサイトも公開されており、正誤表や本文中の各コードリストのダウンロード、本文の一部を切り出したサンプルPDFの無償公開などがあります。正誤表等はこれから更新されていくと思いますので、本書をお読みになられる際はこちらのサイトも併せてご参照いただければ幸いです。
以下は、思い出話です。
本書の企画がスタートしたのは、Firefox 4の正式版が出る前の2010年5月頃だったと思います(今メールボックスの一番古いメールの日付を見て確認した)。当時はまだ内部バージョンがFirefox 3.7とか言われていた頃で、Aero Glassが標準で入るのか?とか、そんな事を言ってた時期でした(という事も今Wikipediaを見て確認した)。
それからしばらく、どんな内容にするのがいいか? どんな人に執筆を依頼しようか?(今著者の一覧に名前が載ってる方の中には、最初の時点で名前が挙がってた著者の側から「是非この人にも書いてもらいたい!」と引っ張り込んだ人もいるのです) という事を話し合っていて、執筆者が確定したのが8月。そこからぼちぼち書き始めて、なんやかやで1年経ってしまいました(普通はここまで難産にはならないみたいです)。その間にFirefoxのバージョン番号もどんどん上がっていって、もうすぐFirefox 8ですよ奥さん! 時間が過ぎるのは本当に早いものですね。
最初の頃は、自分は「Firefox 3 Hacksで書いた内容をFriefox 4時点での情報に基づいてリライトするか?」程度の事を考えていました。でも、それじゃあ書いてる自分がつまらないし、それこそすぐに陳腐化してしまいます。それでうんうん唸りながら調べていくうちに、Firefox 4以降の話で新しい基盤の技術になりそうなトピックがいくつかあるという事が分かってきたので、それじゃあってんで、そこをターゲットにして書いていく事にしました。実は、自分の担当箇所は半年くらい前にほぼ完成してたんですよね。そこから本書の発売までの間は、(他の事に時間を使ってたからというのもありますが、)細かい記述のアップデート程度しかやってません。高速リリースになってからガンガン方針が変わったり実装が変わったりしてて、本書に書いた「テクニック」の中にも、既に「もうこんな回りくどい事しなくてもいいんだけどなぁ」な感じになってしまった話題がいくつかあるにはあるのですが(校正の詰めの詰めに入った段階でまたどどっと変更が入ってきて、どーしても間に合わなかったんです……)、全体としてはちゃんと今でも、そしてこれからも通用する話になってると思います。あの時の自分の目の付け所は、そんなには間違ってなかった、はず。
紆余曲折があった末にようやく世に出る事ができた本書ですが、アドオン開発者の方の手助けとなり、また、アドオン開発に手を出してみようと考えている方の手引きとなれば、幸いです。
他の著者の方々による本書の紹介のエントリ:
あかつかだいすけさんのご紹介で、6月27日に慶応大学湘南藤沢キャンパスでインタラクションデザインについての講義を1時間ほどやらせていただきました。その際の発表資料はいつもの通り「高橋メソッド in XUL Returns」で作ったのですが、Remote XULがデフォルトで禁止されるようになったFirefox 4以降だとこれを見るためには色々と面倒が多いので、代わりに講義の内容を思い出しながら資料を引用しながらエントリにまとめてみたいと思います。
事前に伺っていたオーダーは、これまでのアドオン開発の中でUIという点についてどういうデザインポリシーを持って開発してきたのかを、また、コンセプトを実装する際の経験から得られた知見を語るというものでした。今までにもこの場やTwitter等で断片的に語ってきている事ではありますが、改めて整理するいい機会になったと思います。
まず簡単に自分の事をお話ししておきましょう。
僕は大阪出身の28歳で、Firefoxのアドオンとの関わりは「Firefox」というプロダクトが世に出るより前からなので、もうそろそろ10年になろうとしています。アドオンをやり始めた頃は大学1年とか2年とかの頃だったと思いますので、講義を聴いていただいた皆さんくらいの年の頃からということになります。卒業制作(僕のいた学科では卒論ではなく卒業制作、しかも選択制だったのです……)もMozillaの拡張機能で「2次元的にWeb閲覧の履歴を表現する履歴表示UI・兼・Webブラウズ用UI」という物だったので、余計に感慨深い物があります。
そういう僕ではありますが、今はクリアコードという受託開発の会社でプログラマとして勤務しており、最近はRubyでWebサービスを作るといった案件に関わっています。仕事の上でインタラクションデザインの事を考える事は実際の所ほとんどなくて、そういう事を実践する機会趣味のアドオン開発の場面くらいしか無いというのが実情です。(きちんとインタラクションデザインの事について学んでいる人に比べると、おそらく物凄く初歩的な事しか理解していない、本来であれば人前でインタラクションデザインについて一席打っていいような立場ではないとは思っています。)
自分はこの何年かで数だけはやたら多くのアドオンを開発してきましたが、インタラクションデザインという観点から語りやすそうな物として、以下の4つを特に取り上げて語ってみたいと思います。
ニュース記事を書く時の5W1Hという原則をご存じでしょうか。
これらをきちんと抑えた文章にする事で、一体何の記事なのか、どういう事が起こったのか、という事を読者に過不足無く伝えられるという原則が5W1Hです。
僕は、UIのデザインにおいてもこれらの5W1Hが重要だと思っています。
これらを満たす事のできるUIを考える、というのが出発点としては分かりやすいのではないか。闇雲に「何か新しいUIを」と考えるよりも、これらを意識した方がゴールを設定しやすいのではないか。という事です。
何事でも目的は大事です。特にソフトウェア開発では、無目的に「単に技術的に可能なので作ってみました」という風に生み出したプロダクトがあっても、自分で使わないし誰も使わないために、そのまま死んだプロジェクトになってしまう恐れがあります。
ただ、それとは逆の場合もあると僕は思っています。「単に技術的に可能なので作ってみました」という物を、試しに自分で使ってみたら案外使い勝手が良かった。常用するためにもっと改良したくなった。というケースが僕には実際にありました。ツリー型タブは、元を正せばそのようにして生まれたアドオンです。
かつて僕はタブブラウザ拡張(Tabbrowser Extensions、TBE)という、Mozilla SuiteやFirefoxの貧弱なタブブラウズ機能をなんとなくいい感じにパワーアップするというようなオールインワン型のアドオンを開発していました。
その頃既に僕は、10とか20とかの多数のタブを同時に開いてWebを閲覧するという使い方をしていました。そういう使い方だと、通常の横一列のタブバーだと、今見ているタブが画面外に吹っ飛んでしまったり、さっきまで見ていたタブがどこに行ってしまったのか分からなくなったり、という具合に色々困った事が起こります。それで僕は2つの機能をTBEに持たせていました。
「多段タブ」は、僕がWindows 95を使い始めた頃から時々見かけていたインターフェースでした。Windowsのコントロールパネルで多数の設定項目を持つ項目を開いた場合に、そのダイアログが多段タブになっている場合があったからです。タブが多ければ2段や3段に分けて表示すればいい、というのは日常的に使うWindows自体の一般的なUIを見ていれば自然と思い浮かぶ発想でしょう。
タブの色分け表示は、元々は何か他のブラウザがそういう機能を持っていたのを真似て実装したような記憶があります。親となるタブと子タブとを同じ色で表示しておけば、タブの親子関係が一目で分かるようになります。IE7でもリンクからタブを開くとそれと同じようにタブが色分け表示されるようになっていましたが、当時としては、これもまたそう珍しくない機能だったのでしょう。
そこにツリー表示機能を加えるきっかけになったのは、malaさんが自身のブログで紹介されていたiRiderというIEコンポーネントブラウザでした。
livedoor Readerの開発者として知られるmalaさんは昔からJavaScriptで変態的な、もとい、他の人が思い付いたり実践したりしていなかったような先進的な試みを色々とやっている方なのですが、そのmalaさんが2005年に2005年もパクられてこなかったもの私的まとめベスト3というエントリを書かれていた際に、iRiderというブラウザの名前を挙げておられました。今では配布元のドメインが失効していて百式のエントリにあるスクリーンショットくらいしか様子を窺える物が残っていないのですが、あのmalaさんが(「あの」って付けるくらいに、僕はmalaさんの着眼点はいつも鋭いと考えているのです)名前を挙げているくらいなのだからさぞかし凄い物に違いない!と思ったので、興味本位に加えて腕試しにという思いもあって早速TBEに「タブの親子関係を色分けだけでなくツリー表示というシルエットの変化で示す」という機能、あと、リンク先のエントリでも触れられている「撫でるインターフェース」も併せて実装しました(実はこれが現在のマルチプルタブハンドラの原型だったりもします)。
iRiderのツリー表示というのは、実を言えば、「タブのツリー表示」ではありませんでした。ウィンドウの左に表示されているのはあくまで2次元的に表現されたWebページの閲覧履歴であって、それらの履歴項目をドラッグしたりクリックしたりするとウィンドウの右側でページを見る事ができる、というのがiRiderの挙動です。ただ、当時既にTBEが持っていた「全てのリンクを新しいタブで開く機能」「リンクから開かれたタブを現在のタブの子タブとして扱う機能」にいくつかの機能を加えさえすれば、(メモリの消費量等の観点ではオーバーヘッドはあるものの)表面的にはiRiderと同じような挙動を示すようになるだろうという見通しは立っていました。
ちなみに、タブに相当する物をツリーで表示するブラウザというのは当時既にいくつか存在していたようですが、僕はそれらの事を意識していませんでした。というか、ツリー? 一体何がおいしいの? みたいな認識でした。そういうわけなので、この時はタブをツリー表示する拡張機能を作ったというよりも、iRiderの挙動をFirefoxで(無理矢理)再現するというのが、やった事の本質を正しく言い表しています。
そういう事でとりあえず技術的には可能だったから実装してみるだけしてみたのですが、タブを履歴の代わりに使うというのは当時の自分にはあまりに違和感が大きすぎましたし、iRiderの「履歴」と違ってMozillaのタブは開いていればその間ずっとメモリを消費する物であるという背景もあって、自分はこの時実装した機能のうち「タブをツリー表示する機能」だけを、せっかく実装したのだから……と、ずっと使い続けていました。
そしたら、これが案外便利だったんですね。
という事で自分自身がすっかりツリー機能に依存するようになってしまいました。その後メンテナンスしきれなくなったために僕はTBEの開発を中止しましたが、まずは「なでるインターフェース」の再現のために書いたコードを応用して、その機能だけを抽出して「タブを選択してから操作する」というコンセプトで再設計したマルチプルタブハンドラを公開し、さらに続いて、タブのツリー表示機能だけを抽出したアドオンをツリー型タブとして公開して、TBEの頃と同じような使い勝手を保ち続けて今に至っています。
タブのツリー表示のどの点が便利だと感じたのか、自分自身でもそれと意識する事はなかったのですが、ツリー型タブをMozilla主催のアドオンコンテストに出品するにあたってアピール文を書こうとして、改めて「自分がツリー表示されたタブを使いやすいと感じた理由」を言語化してみる事にしました。
その時自分で分析して思いついた「使ってみて分かったツリー表示のいい所」は、以下のようなものでした。
これらをもっと突き詰めて一言にまとめると、「ツリー表示であればうろ覚えでも使える」からこそ自分は利便性を感じたからなのではないか。それが自分の辿り着いた結論でした。
うろ覚えで使える、とはどういう事か。もう少し具体的に考えてみましょう。
1は、表示される情報が適切な量になっているという事だと言えると思います。2から5は、学習コストが低いという事です。どうやら僕は、これらの点を満たしている時に「良いUI」と感じているみたいです。
この点はSFCでの講義では話せていなかった点ですが、このまとめでは書いておきます。
情報が適切に整理されている、という事は非常に重要です。例えばある家具屋さんの折り込みチラシを見てみましょう。 この種のチラシは「新春初売りセール開催中ですよ!」という事を顧客に告知して集客する事を第1の目的に、そして売り込みたい商品にどういう物があるのかをなるべく多く知らせる事を第2の目的にしていると考えられます。情報を見やすく整理するという事はあまり重視されていないので、余白を設けるよりも、少しでも多く情報を詰め込む工夫が為されています。顧客はこのチラシの中から目を皿のようにして安い商品を探す事になります。セールのチラシの場合は顧客には「そこにどんな情報があるのかをじっくり読み取る努力をするモチベーション」がありますから、多少の苦難は乗り越えられます。
しかしブラウザのUIは、使い手の側にそれほど強いモチベーションはありません。「さっきまで見ていたタブ」を見つけるためにいちいちこのようなゴチャゴチャした絵面の中を一生懸命探すのは、ただのストレスにしかならないでしょう。常用するUIがこんな感じではとても使えません。
視覚的な情報の量を調整するテクニックはいくつかあります。DTPのセオリーやWeb制作のセオリーなどを読んでもらうといいと思いますが、今自分でもぱっと思いつく物としては以下の4項目があります。
ツリー型タブによる縦型タブバーの場合、2から4が特に顕著だと僕は考えています。
スクリーンショットを見ると分かりますが、縦にUI要素が揃う事で、左にアイコン(とインデント)、真ん中にタイトル、右に「タブを閉じる」ボタンが集まっています。 ここにサムネイルというUI要素が加わっても、縦に同じような位置に同じような物が並んでいるので、これはまとめて意識の外に置いておきやすいはずだ、と僕は思っています。実際、自分はこのようなタブバーの左の方を見ている時は右側を無視していますし、右側を見ている時は左側を無視している気がします。
逆に、横置きのタブバーでは、「アイコン、タイトル、クローズボックス、アイコン、タイトル、クローズボックス……」とそれぞれ異なるUI要素が一列に並んでいるので、「アイコンにだけ注目する」とか「クローズボックスにだけ注目する」という事がやりにくいと僕は思っています。
タブの親子関係は、タブの色分けではなく、インデント幅というシルエットの違いで表現されています。これなら、2段階以上のネストであっても状態を正しく把握できるでしょう。
視覚的な情報の整理の仕方はデザインの本を学ぶと色々と情報を得られると思います。自分は一時期Webデザイナーになりたいと考えていたので、DTP的な記事が多く載っていたWebデザイン雑誌も何度か目にしていたのですが、上記のような話はそういった所で仕入れた物のような気がします。
ヒューマン・インターフェースのデザインのセオリーという物がいくつかありますが、それらはソフトウェアにおけるインターフェースのデザインにも適用できます。
何のための物か、何を表している物なのか、説明がなくても一目で分かるようなデザイン。形がそれ自身の役割を表す。これはUIのデザインでよく言われる事です。
ソフトウェアの場合は、まずそのOSにおける一般的なデザインの流儀に則ってUI要素をデザインしておけば、少なくとも「ここはクリックできる場所なのだな」といった事を伝えられます。その上で適切なアイコン画像を加えましょう。基本はやはり重要です。
見えているUI要素はそのまま使えるようにする、という事も大事です。「見えているのに使えない」「見た目通りには動かない」というのは、ユーザを惑わせるばかりの害悪です。例えば以下のような物は論外でしょう。
基本的に、人は他の場面で憶えたやり方を別の場面でも使いたくなるものです。タブを掴んで移動できる、という体験をExcelでやった後であれば、Firefoxのタブも掴んで移動できると考えておかしくないでしょう。UIの基本的なルールを統一しておく事の意義はそこにあります。
ですので、奇をてらってワケの分からないUIにするよりも、まずは既存の類似のソフトウェアの挙動に合わせる事、ユーザがこれまでに学習してきた事に反しないようにする事。これをまずは意識しておくのが大事です。AndroidアプリのUIガイドラインなど、各OSで何らかのデザインのガイドが提供されているのであれば、目を通しておいて損はないです。
ツリー型タブも、タブをドラッグ&ドロップした時の挙動はAdobe Illustratorのレイヤーの操作をモデルに整備しました。また、マルチプルタブハンドラも「Ctrl-クリックでタブの選択状態をトグルする」「Shift-クリックで複数のタブを一気に選択する」という、Windowsやなんかで一般的な操作で使えるようにしてます。こういうのも、既存の物に合わせている例ですね。
見た通りに使えるかどうか、という観点で「こりゃあかんわ、好きになれんわ」と自分が最近感じたUIは、以下の2つのアドオンです。
これらはいずれもFox Splitterと同ジャンルのアドオンで、Firefoxのブラウズ領域を複数に分割してそれぞれに別々のページを表示できるようにする物です。
実際インストールして試してみると分かると思いますが、これらはどちらも、ウィンドウの上の方にあるタブバーやナビゲーションツールバーという操作UIが、タイル表示されたWebページの間で共有される設計になっています。下のペインで「戻る」操作をしたければ下のペインにフォーカスしてから上のツールバーの「戻る」ボタンをクリックする、という具合に、操作の対象と操作のために入力UIがバラバラに切り離されてしまっています。
また、タブと対応するWebページとが離れてしまっているのも問題です。どのタブをクリックしたらどのコンテンツ領域がフォーカスされるのか、どのコンテンツ領域をクリックしたらどのタブがフォーカスされるのか、今どのコンテンツ領域にフォーカスが当たっているのか(フォーカスされているコンテンツ領域の周りには枠が表示されますが、背景色によっては枠がとけ込んでしまう)、といった事を、このUIだとぱっと見て判断しにくいです。慣れれば使えるのかもしれませんが、これに慣れたくはないなあ、というのが僕の正直な感想でした。
入力UIが状態の表示のためのUIを兼ねる、というのもUIのデザインでよく言われるセオリーです。
ソフトウェアの場合もやはり同じ事が言えます。例えばクリックする度に機能の有効・無効を切り替えるトグル式のツールバーボタン(XULでは<toolbarbutton/>要素)であれば、これはチェックボックス型(<toolbarbutton type="checkbox"/>)としておきましょう。チェックボックス型のツールバーボタンは、1回クリックすると押し下げられたままの見た目で固定され、もう1度クリックすると出っ張った状態に戻ります。これは、入力装置であると同時に、見た目で今の状態を表すインジケータとなります。
ツリー型タブの場合も、タブという元々入力と状態の表示を兼ねたUIであった物を、そのまま少し形を変えて縦に並べています。
使用頻度の高いUIについては、常時見えている事も大事です。クリックしないと出てこない物は、そのワンステップが煩わしくてそのうち使われなくなってしまいます。Firefox 4から標準搭載されているPanoramaもこの問題を抱えていますし、Google Chrome版のTree Style Tab(※作者は僕ではないです)も、Chromeの拡張機能向けAPIの仕様上の制限でそのようになってしまっています。パネル型でもいいので、これを表示したままの状態に固定できるようにならないと、常用するUIとしてはちょっとキツいと自分は思います。僕が自分でツリー型タブをChrome向けに移植しないのは、この点をクリアする方法がまだないように見えるからという理由もあります。
カーソルを合わせたら自動で出てきて、カーソルを外したら自動で消える、という物は、場合によっては有用です。
「自動で隠す」系のUIは、WindowsのタスクバーやGNOMEのGNOMEパネルのような、画面全体の一番端に貼り付くUI以外ではあまり実用的ではないと僕には思えます。(余談ですが、UI要素は大きければ大きいほどそれを操作しやすく、画面の端であればどこでも反応するというボタンやUI要素は「無限の大きさを持っているに等しい」とよく言われます。Windowsでウィンドウを最大化した時に画面の右上にマウスのポインタを移動させてみれば、大雑把に指し示しただけで「閉じる」ボタンがハイライトされる事が分かると思います。)
これは1つ前の話とも少しかぶるのですが、「入力するモード」と「それ以外の操作をするモード」という風な排他的なモード切り替えは、極力避けるべきだというのが僕の考えです。設計を見直して、そもそもモードの切り替えが必要ないようにする、くらいに突き詰めていいと思います。
モーダルなUIは、ユーザの持つメンタルモデルの中にモードの違いがキッチリ組み込まれていればいいのですが、そうでない場合(ユーザのメンタルモデルの構築に失敗している場合)には混乱の元になります。いつもやっている操作を違うモードでやっても何も反応しない。でもユーザは、モードが違うせいでそうなっているという事に気付かない。「違うモードなのでその操作はできませんよ」と警告を出しても、根本的な解決にはなりません。
この観点で僕がどうしても好きになれないソフトウェアの代表例が、viとかvimとかVimperatorとかそのあたりという事になります。vi系のエディタはコマンドモードと編集モードを行き来しながら操作するという設計になっていて、この種のストレスを自分はしょっちゅう感じます。
これは「UI」というよりも、もっと前の段階のテーマ決めの話になるかも知れません。
「何でもできる」という謳い文句は、「何をすればいいか分からない」という問題と表裏一体です。何をするための物なのかとか、どう使うべき物なのかとか、それを使う事で自分の生活がどう変わるのかとか、そういった事が伝わってこないとユーザは戸惑ってしまいます。先程、視覚的なデザインによって目に飛び込んでくる情報を制限して情報量を抑えるという事を述べましたが、コンセプトの設定にも同じ事が言えます。
「これができる」「これをする物だ」という事がはっきり見えていればいるほど、メリットが見やすくなるのと同時に、どういうデメリットがあるのかも見やすくなります。基本的にアドオンのインストールには多かれ少なかれデメリットがつきまとっていますが、そのデメリットを乗り越えてでも欲しい機能があるのだという人と、そういうデメリットがあるのであれば絶対に使いたくないという人とをきちんとスクリーニングできれば、ユーザを無駄に不幸にさせずに済むというのが僕の考えです。
カスタマイズ性の高い設計にしていたとしても、というか、カスタマイズ性が高い場合にこそ、既定の設定は重要です。第一に想定しているユーザ、この人にまずは使ってもらおうと思っている相手が、違和感なく使えるような設定をデフォルトにしておきましょう。インストールした後必ず自分に合わせた設定の変更を行わなくてはならないのでは、使い始める時の心理的コストが大きくなってしまいますし、そもそも、使う前から「どういう設定が望ましいか」をユーザが判断するというのが土台無理な話です。
また、最近はいろんな機能を1つのUIに統合する事が流行っているような感じがあると思いますが、「何でもできるUI」として1つのUIに2つも3つも機能を詰め込んでしまうと、実際の利用時に却って不便になる事もあります。
Firefoxのロケーションバーと検索バーを統合して「普通の単語を入力したらGoogleで検索して、URLっぽかったらタブを開いて……」という風な「文脈に応じた自動判別」を行うようにしたとして、「google.comという単語を検索したい」という意図をそのUIは正しく受け取れるでしょうか? 文脈の機械的な自動判別には今のところ限界があるので、誤判定、いわゆる「誤爆」の発生はゼロにはできません。
じゃあ考えられる候補をすべて一気に列挙してしまおう、というのも乱暴すぎる話です。入力欄に何か語句が入力されたら、検索結果を3件、その下に過去の履歴で入力とマッチした物を3件表示する、とした場合、ユーザが最初から「過去に見ていたページをもう一度見たくて」その入力を行っていたのであれば、最初に出てくる検索結果3件はただのノイズになってしまいます。ユーザがその時したい事に対して適切な結果が帰ってこないのは、単純にストレスですよね。
ある機能の利用頻度が特に高いのであれば、その機能には専用のUIを割り当ててやった方がいいです。よく使われている物を無理矢理他の物と統合するというのは、信頼性の低い自動判別を無駄に1枚噛ませるだけになったり、メニューを選ぶための手間を無駄に1つ増やすだけになったりという事になると思うので、僕はなるべくそういう事は避けたいと考えています。
Appleは、コンセプトをはっきりさせるという事を非常に強く実践しています。例えばiPodは非常に割り切った設計で「音楽を聴く」事に特化していて、関係ない要素をガンガン省いていました。それによって、「これは音楽を聴く物なのだ」という事が嫌でもユーザに伝わります。Appleの場合はスティーブ・ジョブズが「これがいい」と言った物を会社を挙げて製品にしており、製品の第1顧客はいつもスティーブ・ジョブズだと言えます。また、キングジム社のポメラも、会議の場で商品の企画が出た時にはほとんどの人が反対したけれど、1人だけ「買いたい」と言った人がいたので商品化できてヒットに繋がったといいます。(余談ですが、リンク先の記事によるとその1人というのは慶応大学の印南教授だそうです。何とも奇遇ですね。)
あらゆる人の方向を向こうとして誰の方向も向けないよりも、誰の方向を向いているのかがはっきり分かるようにした方がいい。100人を満足させようとしてみんなに「ふーん、悪くないね。まあ機会があれば試してみるかもね。」って言われて実際にはそのままスルーされてしまうよりも、99人にそっぽを向かれてもたった1人のユーザに「これすごくいい!!」と飛びついてもらえる、その人を確実に満足させられる方がいい。というのが今の自分の考えです。これだけ物や情報が溢れている世の中では、そうやって製品の特色をはっきりさせないと、評価してもらう前の段階で埋没してしまうのではないでしょうか。
これらの点で僕が「駄目だなー」と思っているのが、かつてのTBEやTab Mix Plusといったオールインワン型の多機能アドオンです。メリットを一言で説明しにくいから紹介する側も「とりあえずこれ入れとけ」という風な言い方にならざるを得ず、紹介された側はどこが具体的にどう変わるのか・どのようなデメリットがあるのかをきちんと把握できず、それのせいでトラブルが起こったとしても「何をするためのアドオンを入れたからこうなった」という事が全く分からないから原因を切り分ける事もできず、という負の連鎖が起こってしまいます。
僕が持ってる携帯電話は、待ち受け状態で数字をぽちぽち入力すると、その数字を電話番号として電話をかける以外にも、電卓機能を起動したり、積算メモ(簡易家計簿)に記入したり、という風な事ができます。メニューから「電卓」を起動して数値を入力するのでも、積算メモ機能を起動してから金額を入力するのでもなく、数字をぽちぽち入力したら画面の中で「この数字を電卓に流し込みたい時はこっちのボタンを押す」「この数字を積算メモに記録したい時はこっちのボタンを押す」という誘導が行われるんですね。
僕はどうも、そういうUIが好きみたいです。セカンドサーチは、Web検索バーで文字を入力した時に「既定の検索エンジンで検索するの? Amazonで検索するの? それとも英和辞書?」という具合に利用できる検索エンジンをポップアップで列挙して、文字入力からそのままスムーズに検索エンジンの選択に移れるようにする……というアドオンですが、前述の携帯電話のUIと考え方は共通していると思います。(なお、僕がセカンドサーチを作ったのはこの携帯電話を使い始めるよりずっと前でした。)
思い返してみると、セカンドサーチを作った動機は、「検索エンジンを選んでから検索して、また元のGoogleに戻して、という操作が死ぬほどめんどくさい。やってらんない。」という不満からでした。
エンジンを切り替えるのが面倒だから、大抵既定の検索エンジンで済ませちゃう。たまに検索エンジンを切り替えて使うと、その後無意識のうちに既定のGoogleで検索したくなって、しかし検索エンジンを切り替えたという事実をすっかり忘れていて、うっかり別の検索エンジンで検索してしまう。そういうのが鬱陶しくて、僕は結局Googleしか使わなくなってしまいました。でも、せっかくOpenSearchとかで検索エンジンを追加登録できても、それらを使う機会が全く訪れないなんて、こんな勿体ない事があっていいはずがない。そう思って、「先に検索エンジンを選ぶのと、後で元に戻すのがめんどくさいんだ」と判断して、「後から検索エンジンを選べて、既定の検索エンジンに戻す必要がない」という操作ができるようにしてみたんですね。
こういう傍目から見てアホとしか思えない行動を取ってしまう人はけっこういるんです。Web検索バーに文字を入力する時には、入力しようと思っている語句に注意がいっているから、「どの検索エンジンが選択されてるか」という事に注意が向かないんですね。そういう風な思考ができあがっている時に、期待通りの結果が得られないと、「なんでやねん」とストレスを感じてしまうわけです。
そういうミスに対して「それはあんたの使い方が悪いんだ」と責める人もいるでしょうけど、道具は使う人のためにデザインされるべき物と考える限りにおいては、人がそういう行動を取ってしまうのなら、その行動に合わせて道具をデザインした方がいいと僕は思っています。
1つ前の話を別の観点からもう1回語ってみます。
金額の事だけで頭がいっぱいになっていて、とりあえず無意識に数字を入力し始めてしまう、という僕の行動をトリガーにして、僕の携帯電話はよさげな機能をサジェストしてくれるわけです。同じようにセカンドサーチも、頭の中が調べたい単語の事でいっぱいになっていてとりあえず無意識に単語だけ入力してしまうという僕の行動をトリガーにして、適切な検索エンジンを後から選べるようにサジェストしてくれます。このように、無意識の行動を起点にして各機能を呼び出せるようになっていると、「機能をどこから呼び出すのか」を改めて考えなくて済みます。
機能をどこから呼び出すのか、というのは結構重要な問題です。
メニューを何階層も辿った奥にある機能というのは使うのが億劫なので、だんだん使わなくなってしまいます。しかし、だからといって階層の浅い所にメニュー項目を増やしていったり、ツールバーのボタンをどんどん増やしていくと、機能が増えていった時に収拾が付かなくなってしまいます。
WindowsのコマンドプロンプトやLinuxやMac OS Xのターミナル(端末エミュレータ)のようにコマンドを入力させる形式なら表示領域を悔いませんが、今度は、ユーザが「どんなコマンドが使えるのか」という事を常に頭の中に入れておかないといけないようになってしまいます。コマンドだけじゃなく、今どこのディレクトリにいるのかとか、ログインしているユーザが誰なのかとか、いろんな「モード」を把握してないと使えません。僕はそんなの全部は覚えてられないので、CUIでコマンドライン入力というのは基本的に好きじゃないです。
表示領域も食わないし文字列のコマンド入力よりも直感的、という事でマウスジェスチャ(リンクをドラッグしたままマウスを上下左右に動かすと、その軌跡(ジェスチャ)がコマンドとして解釈される機能)はどうなんだ? っていう話になってきますけど、マウスジェスチャも「ジェスチャを覚えないといけない」という問題はやっぱりあります。上上下下左右……みたいなややこしいジェスチャを全部覚えてられるかというと、まあ無理な話です。実際、マウスジェスチャを使ってる人でも「憶えてるのは上下左右の各方向にマウスを動かす単純な動き程度でそれより複雑な物は全然使わない」と言ってました。
という話の中にも出てきていますが、ごく簡単なジェスチャだったらアリだと言えます。リンクを引っ掴んで適当にぽいっと投げたらそれをタブで開く、とか、その程度の大雑把さなら苦になりません。ただ、この場合は「ジェスチャを入力している」と言うよりも、「リンクというオブジェクトを掴んで何かしようとしている」という場面だと解釈した方が正しいでしょう。
僕が「ユーザの入力をトリガーにする」という言葉で表現したかったのはそういう事です。何かキー入力を始めたとか、何かオブジェクトがドラッグされたとか、そういう行為そのものをトリガーにするっていう事であって、「どういう文字が入力されたのか」とか「どういう風にマウスを動かしたのか」とかの細かい所を情報として使うという話ではないんです。もちろん、入力された内容にそぐわない機能を除外した上でサジェストする(例えば数字だけが入力されていたら英和辞書は候補に出さないとかの)工夫はあってもいいと思いますが。
「冷蔵庫に卵とトマトがある事を確認して、それからクックパッドで卵とトマトを使ったレシピを探そう」という風な理路整然とした行動計画を、現状から目指すゴールまでを最初から完全にきちんとイメージできる人ばかりではないんです。というか僕自身がそういう事ができない方なんですね。おなかすいてとりあえず冷蔵庫を開けてみたとか、そういう無駄な事をやってしまう方です。
そういう最初のアクションを無意識レベルで起こした結果、機能がサジェストされたり視覚的なフィードバックがあったりして、そこからさらに「そういえば自分はこういうことをしたいんだった」と次の行動が喚起される。(もっと言うと、その時の既定の挙動が一番利用頻度の高い物になっている。)という風な誘導がウザくないレベルで働くようなUIが、僕にとっての「良いUI」なんでしょうね。
ちょっと話はズレますが、ストリートファイターとかの対戦格闘ゲームを考えてみましょう。この種のゲームは、技を出すための複雑なコマンドを覚えたり、覚えたコマンドを適切なタイミングで正確に入力できるようになったり、という努力の末の「上達」が「勝利(報酬)」に結び付くというゲームのデザインになっています。勝てるようになれたら嬉しくて、負ければ悔しくて、なのでみんな必死になってコマンドを覚えるし、正確に入力できるようになろうとする、そういうモチベーションがある。
翻ってブラウザのUIという物を考えてみると、誰が好きこのんで複雑なコマンドを覚えたくなるの? ブラウザを上手く使えるようになる努力をして、上達して、それのどこが嬉しいの? って話なんですよ。普通のユーザにとって、ブラウザとかUIとかってそんな努力を費やす対象ではないです。勉強しないとまともに使えない斬新な文房具とか包丁とか、使いたくなりますか? なりませんよね。普通の人にとってのブラウザって、そういうレベルの物でしかない。
UIをデザインするんだ!って思って頑張る時に勘違いしてしまいやすいのはそこなんですよね。こんだけ頑張って斬新なUIを考えました、他に類を見ないUIになりました、ただし使うのにはそれなりに慣れが必要です、っていって作った物をユーザに見せても相手にされないですよ。そんなの作り手の自己満足でしかない。自分にとって物作りそれ自体が楽しくなってきた頃に犯しがちなミスです。
実用品のUIと、娯楽だったり趣味の物だったりのUIとは、別の物として考えて欲しいです。日常的な利用にゲーム性を加えてみましたとか、普段の利用がちょっと楽しくなりますとか、たまにありますけど、それで本来の用途に支障をきたしたりストレスが増したりするようになってしまっては本末転倒です。
自分が生活する部屋や、日常的に使うキッチンの事を考えてみて下さい。
ソフトウェアのUIも基本は同じだと思います。
よく使うものを手前に置くとかの話はここまでで述べた話に通じています。ここで新しく述べたいのは、「どこに何を置いたかだいたい把握してるはず」という部分です。
ロケーションバーと検索バーの統合という風に、最近は(主に表示領域を広く取るために)複数のUIを1つにまとめる風潮があるみたいなんですが、そうすると使う時に余計なステップが増えてしまいかねないというデメリットがあります。UIをどんどんまとめていくという事に固執しないで、時にはUIを分けるという判断も必要だと僕は思っています。「使い方を頑張って覚えなきゃいけない様なUIは駄目だ」という風な事を前述しましたが、並んでる物を使い分ける程度の事は大抵の人にはできるはずです。あんまりユーザをなめんなよ、ということですね。URLを入力したい時にはロケーションバーをクリックして、検索したい時は検索バーをクリックして、という事をユーザが無意識にできるのであれば、そこはUIの設計に組み込んで活かしてもいいはずです。
あと、キッチンに例えるついでに言うと、キッチンを清潔に保った方がいいのと同じでソースコードも清潔にした方がいいです。詳しくは実装の話で述べますが、生ゴミを放っておくと腐って虫が湧くように、汚いソースコードも放置すればバグの温床になりますので。
一般的に言って、フツーの人は「どうしても必要だ!」という強烈な動機がない限りは新しい物の使い方を積極的には覚えようとしませんし、いつも取っている行動のパターンも変えたがりません。というか、無意識のうちに同じ行動を取ってしまいます。そこから意識して外れようとすると、これはストレスになります。何度も述べていますが、「素晴らしいUIを作ったから、ユーザは使い方を覚えるために苦労してくれるはずだ」というのは作り手の傲慢なんですね。そんな物は、そのうち使われなくなってしまうのがオチです。
ただ、あんまり人間の順応性をナメすぎてもいけません。最初は違和感があった「いつもと違う行動パターン」でも、そのうち慣れちゃうというのは結構あります。そこを考慮から外してしまうと、常用するのがウザいUI(例えば、いつまで経っても毎回馬鹿丁寧に「どうしますか?」と聞いてくるような)になってしまいます。
これが大事なんだ、「どこまで丁寧にやってどこからはユーザの慣れに期待するか」のバランス感覚が必要だ、と僕は思っています。
この観点から、僕はKDEでのファイルのドラッグ&ドロップの操作は「馬鹿丁寧すぎ」の方に入るのかなーと思っています。Linuxのデスクトップ環境の1つであるKDEでは、ファイルを別の位置にマウスの左ボタンでドラッグ&ドロップすると、必ずメニューが出てきて「コピー」「移動」みたいなその時可能な操作を選べるようになっています。これ、最初はいいなって僕自身思ってたんですが、使ってるとだんだんうざく感じるようになってしまいました。「上の階層のフォルダにドロップしたんだから、コピーじゃなくて移動したいに決まっとるがな!」「新しくフォルダ作ってそこにドロップしたんだから、移動したいに決まっとるがな!」と、自分の場合は少なくとも、ファイルのドラッグ&ドロップは「移動」のためにやる場合が圧倒的に多かったんですね。それなのに毎回「コピーですか? 移動ですか?」って訊かれる。これがそんなにストレスになるとは、実際使い続けてみるまで僕には分かりませんでした。
なので、セカンドサーチは何も検索エンジンを選ばなければ既定の検索エンジンで検索しますし、マルチプルタブハンドラも、「グッ」と押し込む感じで敢えて長くタブの上でボタンを押し続けない限りはタブの選択操作が始まらないようになっています(このタブ選択操作の挙動はユーザからの提案で実装したものですが、採用して大正解だったと思ってます)。
「ユーザからの提案を取り入れて正解だった」と書いた直後に述べるのも何ですが、ユーザの提案を何でもかんでも呑んでしまうのは避けた方がいいと僕は思ってます。
ユーザは基本的に自分の不満を素直に言ってくるもので、その提案を取り込んだ結果他のユーザにどういう影響があるかとか、実装がどう変わるかとか、そんな事までは考えてないと思っていいと思います。そういう提案をどんどん取り込んでしまうと、最初に定めたはずの「これは何をするためのソフトウェアだ」というゴール設定がブレてしまう事にもなりかねません。なので、僕はユーザからの提案に対しては、最初に決めたコンセプトに沿っているか・既存の物を壊してしまわないかという事をなるべく考えて、折り合いが付かなければ「それは取り込めない」と回答するようにしています。
タブブラウズの機能を何でもかんでも拡張するTBEというアドオンを作っていた時は、僕はわりと何でも提案を取り込んでしまってましたが、その結果どうなったかというと、ソフトウェアが肥大化するわUIが複雑化するわコードがごちゃごちゃになるわで散々でした。これは、当時の僕が名誉欲だとか人に感謝されたい欲だとかに突き動かされていたからという理由だけでなく、ソフトウェアのコンセプトが曖昧だったので、何を入れて何を外せばいいのかという判断がそもそもできなかったせいでもあるんじゃないかと、今では思ってます。
コンセプトがはっきりしていると、寄せられた意見を取捨選択しやすいです。意見を退ける時も、「それはコンセプトに合わないから」とはっきり言える。好き嫌いだとか意地悪だとかで言ってるんじゃないという、ある意味では言い訳を自分に対してできるんですね。八方美人タイプだと人の頼みを断る事自体がストレスになってしまいますが、こうすれば罪悪感を感じなくて済みます。
ということでNOと言う事の大事さを散々説いたのですが、かといって頑なになりすぎるのも良くないです。ユーザ1人だけが言ってきているのならまだしも、同じ要望・不満が別々のユーザ10人から飛んでくるようだと、これはユーザの使い方ではなくてUIの方が(ひょっとしたらコンセプトも)悪いという可能性を疑った方がいいでしょう。「なんでみんな理解してくれないんだ、こんなに素晴らしい物なのに!!」という思い上がりに囚われてしまってはいけません。
あと、意見を取り入れる時も、取り入れ方は工夫する必要があると思います。例えば「ここにこういうボタンが欲しい」という要望が来た時に、本当にそのまま言われた通りにボタンを追加する前に、なんでそこにボタンが欲しくなったんだろうかという事を考えるという事です。そこで考えた結果、やっぱりボタンを追加した方がいいという結論に辿り着くかも知れませんが、ボタンを追加しなくても別の方法で解決できるという事が分かるかも知れませんし、コンセプトの見直しを図るきっかけになるかも知れません。
余談ですが、Microsoftは製品を開発する時に、ユーザの言葉を聞くんじゃなくて、ユーザがその製品をどう使おうとしたかという様子を観察して製品にフィードバックするという事をやっている、という話を以前にどこかで読みました。ユーザ自身が自分の要望や不満を100%正しく言語化できるわけではない、ユーザ自身が気付いていない問題や視点があるのかも知れない、恥ずかしくて嘘をついているかも知れない、だから行動を見る……という話だったと思います。
Microsoftと同じ事をやるにはお金も手間もかかるのでそのままマネするのは難しいですが、そういう考え方は持っておいていいと思います。
僕がここで述べた事は、多分UIのデザインのセオリーと言われるような事ばかりです。名前を挙げた自作のアドオンも、最初からそういう理屈を考えて作ったというよりも、試行錯誤してるうちに辿り着いた結果を後から分析してみたら案外セオリーに則っていた、という感じです。もっとちゃんと体系立ててUIのデザインの理論を勉強してから臨んでいたら、もっと少ない労力で答えに辿り着けたのかも知れないなあと思っています。
セオリーにはセオリーと呼ばれるだけの理由がちゃんとあると思いますので、我流で突っ走る前に一応は基本を抑えておくといいんじゃないでしょうか。
人にやさしい・使いやすいUIを実現するためには、プログラムとしては複雑な物になってしまいがちです。逆に、プログラムとして簡単な物にしようと思うと、エンドユーザにとっての使いやすさが犠牲になってしまいがちです。
「Now Loading…」系の物なんかは、その代表例ですよね。プログラムを作る側としては、必要なデータが全部揃うまで待ってから最後に一気に処理した方が、コードとしてはシンプルに書けます。しかし、これではエンドユーザは待たされている間何もできません。今では考えられない事でしょうが、昔のブラウザもまさにそういう設計でした。ページの内容を全部インターネット経由でダウンロードしてくるまで、ページの描画をしてくれなかったのです。
今のブラウザはそんなことは無いですよね。読み込みが少しずつ進行するに従って、ダイナミックに表示を更新して、現在までに読み込みが完了した部分から順次表示してくれます。多少作るのが面倒であっても、こういう風な設計を心がける事はとても大事だと思います。
ただ、そのようにユーザにとっての使いやすさを追求するために、プログラムとしてのコードの綺麗さを犠牲にしすぎるのも良くないです。コードの見通しが悪くなってしまうと、いざ挙動がおかしい所を見つけたとしても、どこをどのように直せば良いのか見当が付かなかったり、ある所を変更したせいで別の所に意図しない影響が発生してしまったりするため、それ以上コードに変更を加えられない(ちょっとでも変更したらぶっ壊れてしまう)、みたいな事になってしまいます。
ユーザにとっての使いやすさを大事にする事と、プログラムとしての見通しの良さを高く保ってメンテナンスしやすくしておく事は、頑張り次第で両立できます。そのためのノウハウを、ここで少しだけご紹介しようと思います。
Firefoxのアドオン開発では、XMLとJavaScriptの組み合わせで「要素の振る舞い」を定義しておくXBLという技術によって「独自のタグ」のようなものを定義できます。しかし、メンテナンス性を高く保つという観点からは、XBLの利用はあまりお勧めできません。使い所をわきまえればXBLはそれなりに便利な技術ではあるのですが、「Firefoxのアドオン」の開発においては、便利さよりもデメリットの方が大きいと僕は思っています。
実際、僕は上述したアドオン「Fox Splitter」の旧バージョンをXBLを多用して開発してきましたが、XBLのせいで全体として見通しが悪くなってしまっていたり、開発時の構成に強く依存したコードがJavaScriptとXBLに散らばっていたせいで調べなければいけないコードの範囲が無駄に広がってしまったりしていたせいで、ドツボにハマってもはや自力ではメンテナンスできなくなってしまい、1年半もの間完全に放置することになってしまいました。
前述した話ともかぶりますが、不必要な多機能化も避けた方がいいです。ある機能Aと別の機能Bとで共通の処理があるから、AとBと共通の処理を1つのアドオンにまとめておこう、という風な考えに至るのはごく自然な発想だと思いますが、しかしAとBがそれぞれ全然関係の無い機能なのであれば、共通の処理を2回書く事になったとしても、AとBは別々のアドオンとして分けて開発する事を僕は強くお勧めします。
今はAとBで共通の処理を使っているとしても、Aの機能とBの機能のそれぞれのコンセプトを突き詰めていくと、共通の処理にできる部分はほとんどなくなってしまうかもしれません。にもかかわらず、「AとBとそれらの共通部分」という構成のコードがあると、この構成を維持する事に発想が囚われてしまって、AもBもコンセプト的に中途半端なままになってしまうのではないでしょうか。別々のアドオンに分けて開発していれば、そういう風に今ある実装に引きずられないで、コンセプトを突き詰めた開発をしやすいと僕は思っています。
他に思いつく事としては、ありきたりですが、プログラミングにおいて一般的なアンチパターン(これはマネしちゃいけない、という反面教師的なパターン)を避ける事がやはり大事だと思います。
例えば、1つのクラスに2つも3つも関係のない機能をまとめてしまうと、これは先ほど述べた「アドオンの多機能化」と同じような問題に繋がりかねません。それを避けるには、個々のクラスの実装が受け持つ機能の範囲を定める形で、実装を適切な粒度に分けておく事が大事です。
また、3項演算子や、論理演算子の優先順位を逆手に取ったプログラミングなどのトリッキーな書き方も、あまり多用するべきではないです。トリッキーなコードは芸術的で見ていて美しいものですが、あまりに芸術的すぎるコードは、非常に限定された条件下でしか正しく動かず、手を少し加えただけで動かなくなってしまう事もあったりして、その後の機能拡張やメンテナンスに対して脆弱になってしまいます。芸術品を作るのではなくて、実用品を作ろうとしているのだ、という事を常に念頭に置いておく事を僕はお勧めしたいです。
1つのプログラムを1人で集中して開発している時は、プログラムの全体像が頭に入っていますから、多少読みにくいコードでも開発する事ができます。
しかし、誰かに手伝ってもらうとしたらどうでしょうか。あるいは、1年間そのプログラムの事を全く考えずにいて、1年後に急に開発を再開する事になったとしたら(1年後の自分なんて、はっきり言って他人も同然です)。読みにくいコードになっていると、「この関数は何のための物なんだろう……」とか「なんでこんな設計になっているんだろう……」といった感じで、全体像を把握できなくて何も手出しできなくなってしまいます。また、そういう状態で不用意に既存のコードを書き換えると、思わぬ副作用が発生して、今まで動いていたものが全く動かなくなってしまうかも知れません。これでは開発になりません。
前述した「1つのクラスに2つも3つも機能が入ってしまっているコード」や「あまりに芸術的すぎるコード」も、そういう問題を引き起こしがちです。そういった問題を避けるための、「1年後の自分が読んでも分かるようなコードにするテクニック」をいくつか紹介しましょう。
変数やメソッド、クラスの名前は、それが何を表しているのか・何をするための物なのかをきちんと表す物にしましょう。例えば以下のようなコードは最悪ですね。
var bool = true; // 変数名が「真偽値である事」だけしか表していない
function f() { ... } // 関数に適切な名前が付いていない
これは、以下のような書き方をした方がいいです。(これはあくまで例です。表す内容がこれとは違う場面であれば、その都度適切な名前を付けましょう。)
var enabled = true; // 変数名から「何かの機能が有効か無効かを示すものだ」という事が分かる
function openTabAndInitialize() { ... } // 関数名が処理の内容を表している
ただ、名前に動詞や形容詞、前置詞などが2つも3つも入ってくるようだと、いくら処理の内容をきちんと言い表していても駄目です。関数や変数に簡潔明瞭な名前を付けられないのは、そもそも設計が悪いからと考えられます。きちんと設計してあれば、関数や変数には大抵の場合、ちゃんと簡潔明瞭な名前を付けられるのです。ですから、この例のopenTabAndInitialize()
だと以下のように設計し直す事になるでしょう。
function openTab() { // 2つの小さな処理単位からなる、1つの大きな処理単位
var tab = openNewTab();
initializeTab(tab);
}
function openNewTab() { ... } // 新しいタブを開く、という1つの小さな処理単位
function initializeTab(aTab) { ... } // タブを初期化する、という1つの小さな処理単位
Rubyというプログラミング言語の開発者のMatz氏の言う「名前重要」というスローガンは、この事を指しています。変数や関数に名前を付ける時はちゃんと考えてから付けないと駄目で、名前付けに困る時は設計を見直す事も視野に入れるべき、という事ですね。
データを表す時に、配列の1個目の要素が名前で、2個目の要素がデータの数を表して、3個目の要素がアイコンのサイズを表して……という風な作りにしてしまう事があります。
var data = ["foo", 11, 32];
こういうデータの設計はやってはいけません。「何番目のデータがこういう意味である」という風な、データの順番などがそのまま意味を表すような設計だと、順番をいちいち細かく覚えてられませんから、うっかりミスで間違えてしまいやすいです。また、こういうデータの設計だと、データ構造も変えにくいです(最後にどんどん新しい情報を付け足すというような、やっつけ仕事の解決策しか取れない)。
こういう物は、連想配列とかハッシュとか言われる形式でデータを表現する方がよいです。
var session = {
title : "foo",
tabsCount : 11,
size : 32
};
連想配列やハッシュであれば、ハッシュのキーがその値の意味を表すメタ情報になります。
1つ前の話と同じ事なのですが、以下のように関数の引数が3つも4つもあるような関数は作るべきではないです。関数を呼び出す時に、何番目にどの情報を渡せば良いのかを間違えやすくなってしまいます。
function load(uri, title, size,
referrer, ...)
こういうものは、ハッシュを1つだけ引数に取り、そのハッシュの値が個々のパラメータを表すという設計にしておきましょう。
load({
title : "foo",
uri : "http://...",
size : 32,
referrer : null
})
このようになっていれば、引数の順番を意識しなくて済みます。
自動テストとは、ある関数や機能について「どういう呼び出し方をした時に、どういう結果が返ってくるか」という事を確認するためのプログラムです。具体的には、以下のような「テスト関数」が沢山連なったものが「自動テスト」というプログラムになります。
function test_getMessage() {
// 実装を実際に呼び出してみる
var result = getMessage();
// 返り値は期待値と一致しているか?
assert.equal('expected message', result);
}
ある機能Aがあって、そのAに依存するBという機能があって、Bに依存するCという機能がある時に、Cが正しく動かなかったら、さてどこに問題があると考えれば良いでしょうか。Aが壊れているのかも知れないし、Bが壊れているかも知れないし、Cが壊れているのかも知れない、このままだと何が原因なのかは分かりません。
しかし、機能AとBは自動テストでちゃんと期待通りの動作をする事を保証されていて、今まさにCを開発している場面なのだとしたら、これはCに問題がある可能性が高いと言えます。このように、自動テストがある事で複雑なプログラムであっても一歩一歩着実に開発を進められるのです。
また、自動テストは、その関数や機能が期待通りに動くかどうかを検証するものであると同時に、その機能はどのようにして使う物なのか、どういう結果が返ってくる物なのか、といった事の例にもなります。
プログラムは、上から順に1行ずつ読んで処理の流れを把握できるようになっている物ばかりではありません。ユーザの入力を待ってから次の処理を行うとか、読み込みが完了するまでの間別の処理を走らせておいて読み込みが完了したら画面を切り替えるとか、このような「人にやさしい挙動」を実現しようと思うと、上から順に1行ずつ読んだのでは処理の流れを追う事ができないような、分かりにくいコードになってしまう事を避けられません。
Firefoxのアドオン開発の場合は言語はJavaScriptを使う事になりますが、JavaScriptでは非同期処理や遅延処理が関係する場面だとコールバック関数(ある処理を呼び出す際に、その処理が終わった後で実行されるべき処理を関数として定義して渡すという物)を使わざるを得ません。そのような処理を2つも3つも連携させようとすると、コールバック関数を何十にも重ねる事になってしまいがちです。
例えば「新しいウィンドウを開いて、そのウィンドウの読み込みが完了したらコールバック関数を実行する」という例は以下のように書けます。
function openAndWait(aCallback) {
var win = window.open(...);
win.addEventListener('load', function() {
win.removeEventListener('load', arguments.callee, false);
aCallback(win);
}, false);
}
これを使って「ウィンドウを1つずつ500ミリ秒おきに順番に3つ開いていく」という処理を書こうとすると、以下のようにコールバック関数をどんどんネストしていかないといけません。
openAndWait(function(aWindow) {
aWindow.move(...);
window.setTimeout(function() {
openAndWait(function(aWindow) {
aWindow.move(...);
window.setTimeout(function() {
openAndWait(function(aWindow) {
aWindow.move(...);
...
});
}, 500);
});
}, 500);
});
2階層や3階層ならまだしも、これが10階層や20階層も重なると、インデント幅が増えすぎてワケが分からなくなってしまいます。また、開き括弧と閉じ括弧の間に沢山のコードが入るため、コード全体の見通しも悪くなります。
このような問題に対処するための機能が、jQueryにはdeferredという名前で搭載されていますし、同様の機能だけを提供するJSDeferredという軽量なライブラリもあります。ここではJSDeferredの使い方を少しだけ解説したいと思います。
先の openAndWait()
という関数は、ウィンドウの読み込みが終わった後に次の処理としてコールバック関数を実行する物でした。これをJSDeferredを使って書き換えてみた物が、以下の例です。
function openAndWait() {
// (1) Deferredクラスのインスタンス(Deferredオブジェクト)を作る。
var deferred = new Deferred();
var win = window.open(...);
win.addEventListener('load', function() {
win.removeEventListener('load', arguments.callee, false);
// (3) 読み込みが終わったら、返したDeferredオブジェクトの
// call()メソッドを呼ぶ。
deferred.call(win);
}, false);
// (2) Deferredオブジェクトを返り値として返す。
return deferred;
}
このように書き換えた関数は、以下のようにして「次の処理」を記述できるようになります。
openAndWait()
.next(function(aWindow) {
aWindow.moveTo(...);
});
それどころか、さらにその先にもまだまだ「次の処理」を続けて記述できます。
openAndWait()
.next(function(aWindow) {
// 読み込みが終わったら実行
aWindow.moveTo(...);
})
.wait(0.5) // 500ミリ秒待つ
.next(function() {
// 次のウィンドウを開く
return openAndWait();
})
.next(function(aWindow) {
// 読み込みが終わったら実行
aWindow.moveTo();
})
.wait(0.5) // 500ミリ秒待つ
..
JSDeferredを使うと、何個も処理を重ねる場合でもコード上のネストがあまり深くなりません。また、コールバック関数を重ねる場合と異なり、コードを読む時に上と下を行ったり来たりする必要もありません。コールバック関数を1個目の引数に受け取るのか2個目の引数に受け取るのか、といったインターフェースの違いも意識しなくて済みます(全ての非同期処理を統一的なインターフェースで利用できるようになる、とも言えます)。
詳しい事はJSDeferred開発者のcho45氏自身による解説を読んで下さい。JSDeferredを使いこなせるようになると、表現の幅がぐっと広がります。
先に名前を挙げたアドオンFox Splitterも、JSDeferredに強く依存しています。Fox Splitterは新しいウィンドウを開いて位置や大きさを制御するという事をやっていますが、ウィンドウが開かれるのを待って次の処理に進んだり、ウィンドウの位置が確定するのを待って他のウィンドウの位置を調整したり、といった感じで「前の処理の完了を待ってから次の処理を行う」場面が非常に多いです。僕はJSDeferredのおかげで、そういった処理の前後関係を意識しないで複数の機能と機能を連携できるようになったために、Fox Splitterを無事完成させることができました。
前述した「ユーザの入力を一旦受け取って、入力が完了した後に次に進む」というような事も、JSDeferredを使えば比較的簡単に処理を記述できるでしょう。人にやさしいUIを作る上で、JSDeferredのようなライブラリは非常に役立ちます。
UIのアイディアを思いついたら、あれこれ計画を練って検討するのもいいですが、さっさと実際にプロトタイプを作ってみることを僕はお勧めしたいです。
コンセプトを形にする事は重要です。形にして実際使ってみて初めて分かる事もあります。プランを練っている時には「素晴らしい」と思っていても、実際使ってみると「アレ?」って事はよくあります。手早くプロトタイプを形にして、コンセプトが正しかったかどうかを初期段階で確認して軌道修正をするのが、後で痛い目を見ないで済むようにするコツです。問題点の発覚が遅れれば遅れるほど、取り返しが付きにくいです。
ブラウザのアドオンは、そういう意味では格好の実験場だと僕は思っています。
これは、大学の卒業制作でMozillaの拡張機能として「こんなUIがあってもいいんじゃないか?」という物を作った時に考えた理屈なのですが、ブラウザのアドオンとしてプロトタイプを作る事にはいくつかのメリットがあります。
Firefoxのアドオンの場合、ソースコードがオープンソースなライセンスで公開されている場合が多いというのもいい所です。分からない所があれば、やりたい事に近い事をやっているソフトウェアのソースコードをそのまま参考資料として参照できますし、ライセンスが定める条件に従ってさえいれば、そのままソースコードを流用する事すらできます。
ちなみに、「オープンソースの物を流用したら、その成果もいきなり全世界に公表しないといけないんでしょ? それじゃ秘密裏に実験的な事ができないじゃないか」という風に思う人がいるかもしれませんが、それは誤解です。例えばGPLなどのライセンスでは、ソースコードを流用して作ったソフトウェアのユーザに対してソースコードを開示する義務はあっても、ソフトウェアを渡されてもいなければ存在も知らないような人相手にまでソースコードを開示する義務はありません。安心して下さい。
質疑応答で少し話しましたが、ソフトウェアのUIとハードウェアの進歩は切っても切れない関係にあると思います。
「複雑なマウスジェスチャは覚えられないから使い物にならない、シンプルな動き以外はまともに使えない」という風な事を前述しましたが、MacやiPhone、iPadで可能な2本指・3本指でのジェスチャ(マルチタッチ)は、その制限を乗り越える技術です。指2本を開くような動き(ピンチアウト)で画面の拡大、指2本を閉じるような動き(ピンチイン)で画面の縮小、という風に、簡単でありながら表現力豊かなジェスチャ操作が可能です。これはハードウェア側のサポート無しには成り立たないUIです。
Panoramaはモーダルなせいでいまいち使いづらいですが、これはPCの画面が狭いという現在のデバイスの制約による所が大きいと僕は思っています。もしPCの画面が物凄く広ければ、あるいは、画面の外に空中に映像を投影するような技術があれば、Panoramaはモーダルにしないで常時表示しておけるかもしれません。
今あるハードウェアの限界の中で使いやすいUIを考える事も大切ですが、できない事はやっぱりできないので、そこは潔く諦めてハードウェアの進歩を待つといった判断も必要では無いかと僕は思います。あるいは、ハードウェアの制限に合わせる形でもっと別のUIを考えてみるとか。いずれにしても、今あるハードウェアの上では使いにくいと言わざるを得ない物を「理論的には使いやすいはずなんだ」みたいな感じでごり押しするような事はしないで、柔軟に考えるようにして下さい。
僕の場合は、今は自分自身が第1のターゲットユーザだと考えています。自分自身が常用するものだからこそ、クオリティを維持して快適な状態を保ちたいと思える。というのが今の僕の考えです。
自分以外の人をターゲットユーザに想定するのもありだとは思いますが、僕の場合は既に20を超えるアドオンを開発していて、それぞれにバグ報告が来て修正のためにてんてこ舞いになっているというのが実情で、自分で使う物についてすら十分なメンテナンスをできていない状態にあります。自分自身を第1のターゲットユーザとして考えるというのは、コンセプトをブレさせないための方法論でもありますが、ユーザから寄せられる大量の要望から距離を置いて自分の身(精神)を守るための手段でもあるんですね。
誰をターゲットにする場合であっても、基本的な所はセオリー通りにやる事が大事なのだと僕は思っています。その上で、例えばお年寄りのための物にするなら文字を大きくするとか、使う言葉を平易にするとか、応用を組み合わせていく事になるのだ……と、僕は思っています。
カスタマイズ性の高いブラウザだからみんなカスタマイズしたくなるかというと、そうとも言えません。統計として、Firefoxのユーザの全員がアドオンを使っている訳ではなく、また、アドオンを使っている人でもインストールしているアドオンの数は片手で数えられる程度である場合がほとんどだというデータが、過去のMozillaの調査で出ていました。カスタマイズ性の高い設計になっている事と、実際にカスタマイズして使われる事とは、あくまで別の事なんですね。(この調査結果は、Firefoxの初期状態が普通の人にそれなりに使いやすい形になっている事の顕れと言えるとも思います。)
僕自身は、ペンタブレット(ペン型の入力装置を使って、ペンで字や絵を描くのと同じ感覚で入力を行えるデバイス)を使う時に、指に当たって痛いなーと思ったらペンの軸に何か巻いてみたりプロテクターのような物を作って付けてみたりと、DIYで「カスタマイズ」して使っています。漫画家の人の中には、ペンの軸が長すぎるからとノコギリでぶった切って使っている人もいるそうです。
でも、そういう風にしない人もいるんですよね。使いやすいと思えるありものの製品を別に探したり、使いにくいから使うのをやめてしまったり。そもそも、自分が使いやすいと思うようにカスタマイズするという発想が無いという人もいるのだと思います。
僕自身、あまり拘りを持っていない分野ではそういう行動を取っている気がします。僕は料理をする事が好きで好きでたまらないという人ではないので、調理器具は買ってきた物をそのまま使っていますし、少々使いにくいと思っても面倒だからそのまま放置しています。カスタマイズしてでも使い続けるかどうかは、「なんでもカスタマイズしたがる性格」の有無以外にも、使いにくさを乗り越えてでもそれを使い続けるに足る動機があるのかどうか、どれだけそれを切実に必要としているのか、という事も影響しているのかもしれません。
そう考えると、カスタマイズ性を高くしておきさえすれば万事オッケーとは言えないのだと思います。僕自身、車や自転車にそれほど強い関心があるわけではないので、「タイヤをこれに替えるともっと快適だよ」と言われても「ふーん、そうなんだ。まあ面倒だから僕はそのまま使い続けますよ。」って思ってしまいますし、そのまま使い続けるのが辛いくらいに合わなければ、カスタマイズを検討する前に他の車や自転車に乗り換えてしまうのではないかと思います。同じように、Webを見る事に対してそれほど切実な動機を持っていない人に「カスタマイズすればもっと使いやすくなるよ」と言っても、「面倒だし別にいいよ」って多分言われてしまうでしょうし、その人が「なんかこれ使いにくい」って言ってる時に「カスタマイズすれば解消できるよ」と言っても、「自分で手を入れなきゃ使いやすくならないの? そうなんだったらもう最初から使わないでいいや……もっと別の物を探すよ」と言われてしまうと思います。
ほとんどの「ユーザ」はそういう物だ、と僕は考えるようにしています。だからこそ、デフォルト設定を使いやすくした方がいいとか、初見で使えるようにした方がいいとか、そういう事を言ってるんですね。
今プログラマ的にアツい分野というとHTML5とかWebサービスとかクラウドとかっていう話になると思うんですが、僕がWeb開発じゃなくブラウザのアドオンの開発をやり続けてる理由は、インターフェースとして、より人に近い部分で動く物を作れるからなんだと思います。
ユーザの何気ない無意識での入力をトリガーにして適切な次の処理をサジェストするというのは、普通のWebページやWebアプリの枠を超えた部分の話です。サジェストされる選択肢の1つとしてWebサービスに誘導するのはもちろんアリだと思いますが、起点になるのは特定のWebサービスの画面ではなくて、ブラウザのUIだったりスマートフォンのホーム画面だったりという、もっとユーザに近い部分になるはずです。
そこの所への関心が強いから、僕はWebアプリよりもアドオンを作るのが好きなんでしょうね。
Fox Splitterのバージョン2.0を公開した。1つ前のバージョンが「0.6.2009110501」だったから、順当に行けば「1.0」とかにするのが筋だと思うんだけど、設計的に前バージョンと全く違う物になってしまったのと、気分転換を図りたかったのとで、バージョン2という事にした。
前のバージョンから1年半ぶりくらいのアップデートということになる。名前かぶり問題で名称を「分割ブラウザ(Split Browser)」から「Fox Splitter」に変える事になった時、「次のバージョンから名前変える」と言っておきながらその「次のバージョン」が出てなかったので、配布ページ上の名前と実際にインストールした物の名前も一致してない状態だった。酷い話だ(他人事のように)。
1年半物間全く動きがなかったのはなんでかというと、ぶっちゃけ、これ以上頑張る意味を見つけられなかったからだ。独自に作ったバインディングを駆使して頑張ってみたけど、ブラウズ領域の上で発生するイベントを拾う形式のマウスジェスチャ系のアドオンとの相性が致命的に悪いという構造上の欠陥を抱え込んでしまったし、Firefox本体の仕様変更に追従しきれそうになかったし、という感じで前のバージョンの設計にはもはや全く将来性がなかった。それに、Tile TabsとかTile Viewとかのもっとユーザにウケそうな物も出てきてたし。
それでも、どういうわけか英語で「Fox SplitterをFirefox 4に対応してくれ!」というメールがわりと何通も来ていて、それなりに使われているんだなあという事は認識していたので、いずれはなんとかしないとなとは思ってた。Tile TabsやTile Viewのアプローチにはいまいち納得できてない所もあったので、そこがFox Splitterのカラーという事になるのかなあとも思った。
Auto Arrange Window After Detach Tabの存在を知った時から、次にやるならこの方式でやってみようとずっと思ってた。
Firefoxのウィンドウの内容を分割して複数のペインを表示するという使い方には、以下のような問題がある。
Auto Arrange Window After Detach Tabの発想は、それとは違った。1つのウィンドウの中を無理矢理分割するんじゃなくて、タブをドラッグ&ドロップしてウィンドウを切り離すというFirefox本体に備わった新機能をそのまま使い、ただ「ドロップした方向にウィンドウを並べる」という風にしてた。これは目から鱗だった。無茶する必要は無いんだ、Firefox本体でそういう機能が既にあるんだからそれに沿った設計にすればいいんだ、という事に気づかされた。
そういうわけで僕はAlice0775氏に感謝しているし、よくここに気がついたと尊敬している。Alice氏に批判的なレスを付けたどっかの誰かが別の誰かに「Piro乙」とか言われてたりして、どーも僕がAlice氏を全面的に毛嫌いしてるように思われてるようなんだけど、勘違いもいいとこだ。(ただ、実際にFox Splitterを作りなおすにあたっては、Auto Arrange Window After Detach Tabのコードは引用していない。設計思想が違う物のコードに引きずられて全体の整合性を保てなくなると困ると思ったから、イメージした通りの設計で一からスクラッチした。)
設計を変えるにあたって、どうせやるならBootstrapped Extensionにしようと思った。
といったあたりがその理由だ。
その過程で以下のような物ができた。
Add-on SDKにも似たようなライブラリがあった気がするけど、手厚いサポートを行わないいわゆる「薄いフレームワーク」の方が(そのライブラリを使ったアドオンを)作ってて全体の動きを把握しやすいんじゃないだろうか、というか自分はそうじゃなきゃ嫌だ、そっから下のレイヤに掘り進まない前提だったらRailsやAdd-on SDKみたいな分厚いフレームワークがいいだろうけどフレームワークの下にもどんどん潜らなきゃいけないとなるとそういう分厚いフレームワークは邪魔になる、とかそんな思いがあったので、これらのライブラリの作りはわりとシンプルだと思う。普通にアドオンを作る時に同じ事をやる際の頻出のイディオムを抽出しただけだと言えるだろう。
Mozillaが用意したオモチャの上で遊んでるだけのくせに偉そうなこと言うなやボケ、みたいな事を言われた(そういう事を言っても変じゃない経歴を持ってて「もも」という名前、ということで、この人はMozillaのコミッターだった(だよね?)桃井氏である可能性もあるのかなーと僕は勝手に思ってます)にもかかわらず、またこういうことをやっている。という自分に呆れもするけど、今はただ、とりあえず形にできたということで一息つきたい気分だ。
Firefox上でいくつかのサブウィンドウを開いていて、それらのウィンドウすべてがワンセットで他のウィンドウより前に出てきていて欲しい場面、というのがある。
例えば、GIMPはツールボックス等が複数のウィンドウにばらけている。これがもし、画像を編集するウィンドウにフォーカスしたらツールボックスのウィンドウがその背後に隠れてしまうという風になっていると、まるで作業にならないだろう(古いバージョンのGIMPをWindowsで使った時にはそんな風になってて頭を抱えた記憶がある)。
また、そういうワンセットで表示されていて欲しいウィンドウ達が、同時に起動している他のアプリケーションのウィンドウの前と後ろにそれぞればらけてしまうというのも、使う時に地味にうざい。
Firefoxの上で、拡張機能が開くウィンドウでGIMPのウィンドウ群のような振る舞いをさせるにはどうすればよいのか。このエントリでは2つの方法を紹介する。
まず1つ目。nsIXULWindowインターフェースのzLevelプロパティを使うと、Firefoxのプロセスが開くウィンドウの重ね合わせの順序をある程度制御する事ができる。一番単純なやり方は、ウィンドウを一時的に最前面表示に切り替えて、その後すぐに元に戻す、という方法だろう。
var XULWindow = window .top .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebNavigation) .QueryInterface(Ci.nsIDocShellTreeItem) .treeOwner .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIXULWindow); var originalZIndex = XULWindow.zLevel; XULWindow.zLevel = Ci.nsIXULWindow.highestZ; window.setTimeout(function() { XULWindow.zLevel = originalZIndex; }, 0);
ただ、仕様上の制限のため、この方法はWindowsでしか使えない。少なくともUbuntu 10.04のGnomeでは機能しなかった。
代わりに考えられるもう1つのやり方が、フォーカスを使うやり方だ。単純に考えても、window.focus()
でウィンドウにフォーカスを与えると、強制的にそのウィンドウを最前面に持ってくる事ができる。
しかしこの方法にも問題がある。普通にwindow.focus()
すると、例えばそのウィンドウの中の特定のテキストボックスにフォーカスしていたとしても、そのフォーカスが失われてしまう事になる。
この問題を回避するには、Gecko 1.9.2/Firefox 3.6から導入されたフォーカスマネージャを使わないといけない。具体的には以下のようにする。
var FocusManager = Cc['@mozilla.org/focus-manager;1'] .getService(Ci.nsIFocusManager); // 現在フォーカスされている要素を取得する。 // 第1引数:検索する最上位のDOMWindow // 第2引数:再帰的な検索を行うかどうかのフラグ(trueを渡す) // 第3引数:その要素が含まれているフレームのDOMWindowを // 受け取るためのスロットとなるオブジェクト var focusedWindow = {}; var focusedElement = FocusManager.getFocusedElementForWindow(window, true, focusedWindow); // 現在のフォーカスが何によって与えられたかの情報を取得する。 var reason = FocusManager.getLastFocusMethod(focusedWindow.value); // フレームにフォーカスする。 focusedWindow.value.focus(); // フォーカスされていた要素がある時は、その要素にもフォーカスする。 if (focusedElement) { // フォーカスを与える時に、スクロール状態等に変更を加えないように指定する let flags = Ci.nsIFocusManager.FLAG_RAISE | Ci.nsIFocusManager.FLAG_NOSCROLL | Ci.nsIFocusManager.FLAG_NOSWITCHFRAME | reason; FocusManager.setFocus(focusedElement, flags); }
こうすれば、Linuxでも要素のフォーカス状態を失わせずにウィンドウを最前面に持って来れる。ウィンドウのフォーカスを動的に切り替えるため、その都度各ウィンドウが画面上でぺかぺか点滅してしまう(一瞬だけフォーカスされて、その直後にフォーカスが失われるため)、というデメリットはあるが。
先に結論だけ。
Firefoxの起動を遅くするアドオンランキングなんてものが公開されて、そこにツリー型タブも堂々ランクインしてて、「多機能オールインワン型アドオンを散々批判してるくせに単機能とか言ってるお前のアドオンの方がよっぽど重てーじゃねえかプゲラ」的なコメントも見受けられる昨今(僕はメンテナンス性とか共存のしやすさとかの観点から多機能型を否定しているつもりなので、ある単機能のアドオンが必ずある多機能のアドオンより軽いはずだとかそんな事は言えないと思っているのですが)、皆様いかがお過ごしでしょうか。
自分はわりと富豪的なプログラムを書く方なので、欲しい機能を実現させるためにいろんなイベントを監視したり、メンテナンスしやすくするためにファイルを細かく分けたり、とかそういう事をやりがちなのですけれども、そういうのって一般的にFirefoxの起動とか動作とかを遅くさせるからやめなさいよというメッセージを、Mozillaは最近アドオン開発者に向けてよく発しています。上記のランキングもその一環ですね(あんまり遅いとこうして晒し挙げるぜ、という)。
で、その晒し挙げランキングのページからパフォーマンス改善の色々なテクニックがリンクされていて、これ読んで出直して来いよ、出直してこないならペナルティだぜ、という話なんですが、そのページの末尾でTalosというパフォーマンス測定ツールを使ってアドオンがFirefoxの起動速度に与える影響を計測する手順も紹介されてました。上記の晒し上げランキングもこれで計測した結果に基づいてるみたいです。
晒し上げランキングの中でツリー型タブがWindows XPで99% Slow(Firefoxの起動にかかる時間が2倍になる)と書かれてて、いくら僕でもちょっと信じがたかったので、実際に自分でもTalosを使ってパフォーマンス計測を試みてみました。僕が試験用に用意したWindows XP環境では、ツリー型タブ0.11.2011050602とNightlyとの組み合わせで35%前後という数値が出ていますが、これはランキングに出ている数値と大きく隔たりがあります。どういうことなんでしょうね。
まあそれはどうでもいいんですよこの際。とにかくこれが契機となって、自分の環境でもアドオンがFirefoxの起動速度に与える影響を機械的に測定できるようになったので、今後のパフォーマンス改善にはこれを1つの指標として使っていこうと思ってるわけです。
さて、そういう背景があってツリー型タブのリポジトリ上の最新版では、主要なコードをJavaScriptコードモジュール化してみたり、関連ライブラリの読み込みをXPCOMUtils.defineLazyGetter()
でやるようにしてみたり(この機能はFirefox 3.6以降でしか使えないので、自動的にツリー型タブの次のリリースはFirefox 3.5対応を切り捨てる事になります)、読み込ませるスタイルシートの数を大幅に減らすようにしたり、ついでに属性セレクタの使用を減らしてみたり、という風な、アーキテクチャに変更が無い範囲で前述の「パフォーマンス改善のためのテクニック」を色々と取り込んでいるのですけれども、まだ実施していない項目の1つに「XPIパッケージの作り方を工夫する」という物がありました。
XPIパッケージというのは、アドオンを構成するファイル一式をZIP形式で圧縮した、配布用のパッケージです。Firefox 3.6までは、アドオンをインストールするとこのXPIパッケージが自動的に展開されて、ユーザのPCのストレージ上に個々のファイルが配置されるようになってました。ただ、ファイルの数が多いとFirefoxの起動に時間がかかるようになってしまうためとか諸々の理由から、XULファイルやCSSファイルやJavaScriptファイルなどはJARファイル(これも実態はZIP形式の書庫ファイルです)にまとめておいて、見かけ上のファイルアクセスを減らす事が推奨されていました。
JARファイルにせよXPIファイルにせよ、圧縮率を高くすればするほど中身を取り出すのには余計な時間がかかる事になります。なので、ユーザの環境にインストールされた時に1回だけ展開されるXPIファイルは圧縮率を最高にして保存して、インストールされた後も毎回中身を参照されるJARファイルは無圧縮で保存する、という事によって配布ファイルのサイズを小さくすると同時に実際の使用時のパフォーマンスも高く保つ……というのが従来のXPIファイルの作り方の定石でした。
ところがFirefox 4以降、パフォーマンス改善の一環として、Firefoxの構成ファイルは可能な限り1つのJARファイルにまとめられるようになりました。これは「omnijar」と呼ばれている新機能で、今まではバラバラのファイルとして配置されていたJavaScript製のXPCOMコンポーネントやJavaScriptコードモジュールまでも、他のXULファイルやCSSファイルのようにJARファイルの中に格納してしまって、更なるパフォーマンスの向上を実現しようというわけです。
Firefox 4本体がそうなったのに併せて、アドオンにもomnijarの仕組みが使われるようになりました。今までならXPIファイルをインストールしたらその中身を常に展開するようになっていたのが、Firefox 4でアドオンをインストールした場合は原則としてXPIファイルのままで保存されて、中身にはomnijarの仕組みでアクセスするようになったんですね。
ここで3つの問題が起こります。
1については仕様上の制限という事で、こういう場合だけはFirefox 3.6の時と同様の形でXPIを展開した状態にしてインストールさせる必要があります。install.rdfにem:unpack="true"
という指定を書き加えると、アドオンのインストール時にXPIファイルが展開されるようになります。
2と3はパフォーマンス低下の問題です。前述の「パフォーマンス改善のためのテクニック」では2と3を理由として、無圧縮JARに固めてから最高圧縮率のXPIを作るのではなく、中身をJARに固めないで全体を圧縮率の低いXPIに固める(アーカイブを入れ子にしない)、という事が推奨されています。
しかしこれは見ての通り、Firefox 3.6以前のバージョン向けの定石と全く相反しています。Firefox 4のためにFirefox 3.6でのパフォーマンスを犠牲にするか、Firefox 3.6のためにFirefox 4でのパフォーマンスを犠牲にするか、どちらかを選ばないといけないという事になります。
でも、ほんとにそうなの? Firefox本体くらいの規模ならいざ知らず、それより遙かに小規模なアドオンでXPIファイルの作り方がそんなに起動速度に大きな影響を与えるの?
という事が気になったので、ツリー型タブのリポジトリ上のHEADでパッケージングの仕方だけを変えてTalosでパフォーマンスを計測してみました。結果は以下の通りでした。
パッケージングの仕方 | 1回目の結果 | 2回目の結果 | 3回目の結果 | 平均 | 余計にかかった起動時間 | 起動時間の増加率 |
---|---|---|---|---|---|---|
(アドオン無し) | 2029.53msec | 2091.58msec | 2058.53msec | 2059.88msec | 0 | - |
無圧縮JAR+圧縮率最高XPI | 2354.05msec | 2341.63msec | 2279.21msec | 2324.96msec | 265.08msec | 12.87% |
無圧縮JAR+圧縮率最高XPI+em:unpack="true" |
2327.0msec | 2314.84msec | 2337.63msec | 2326.49msec | 266.61msec | 12.94% |
無圧縮XPI | 2287.58msec | 2380.79msec | 2331.26msec | 2333.21msec | 273.33msec | 13.27% |
数値が物凄く大きいのは、試験環境がシングルコアなCeleronの上で動いてるVirtualPCの上で動いてるWindows XP SP3だったからです。無圧縮XPIは上記の「Firefo 4向けの推奨されるやり方」ではないのですが、CPUが低速なら展開に時間がかからない方が有利なんじゃないかと思ってそうしてみました。
表を見ての通り、XPIの作り方で劇的に起動速度が変わるという事はありませんでした。少なくともツリー型タブ程度の規模では、XPIファイルの作り方の違いを変えてもFirefoxの起動速度は大して早くならない、ぶっちゃけ好きなようにやればいいという事が言えると思います。
ちなみに、繰り返しテストする前に試験実行した時にはそもそもFirefox自体の起動時間が600ミリ秒ほど短い1400ミリ秒ほどであるという結果が出ていました。その時の計測結果も以下に記しておきます。
パッケージングの仕方 | テスト実行時の結果 | 余計にかかった起動時間 | 起動時間の増加率 |
---|---|---|---|
(アドオン無し) | 1411.26msec | 0 | - |
無圧縮JAR+圧縮率最高XPI | 1730.58msec | 319.32msec | 22.63% |
無圧縮JAR+圧縮率最高XPI+em:unpack="true" |
1718.05msec | 306.79msec | 21.74% |
無圧縮XPI | 1779.53msec | 368.27msec | 26.10% |
これを見ると、ツリー型タブがある時に絶対的に何ミリ秒余計な時間がかかるのか?という点では、先の表と併せて見ても、だいたい常に300ミリ秒くらい起動に余計に時間がかかってるみたいだという事が分かります。Firefox自体の起動が300ミリ秒くらいで済む環境でテストすると、ツリー型タブのせいで起動時間が2倍かかるようになると考えられます。もしこれが事実なら、上記の晒し上げランキングの元データは結構高速なマシンで計測した物という事になるのかもしれません。
結論としては、XPIファイルの作り方を見直すなんてのは最後の最後でいいと思います。それよりもアーキテクチャの変更とかの方が多分ずっと高速化には効きます。実際、ツリー型タブのここまでの改善の中では、レイジーゲッターとかJSコードモジュール化とかよりも、読み込んだまま使ってなかった大量のスタイルシートを必要最小限だけ読み込むようにして@import
を減らしたりセレクタを単純化したりした時の方が、大きな数値上の差が出てたと思います。
あと、このエントリ内での計測結果がおかしいと思う人は自分でPythonとTalosとNightlyを用意してパフォーマンス計測をやってみるといいです。こういう物が既にある以上、今後は、数値による裏付けが無い「遅い」「重い」といった類の話は無視しちゃっていいと思いますよ(動作時の速度についての言及は除く。上記の文書で解説されてる手順で計測できるのはあくまで起動にかかる時間だけで、メニューを展開したりタブを切り替えたりといった操作のパフォーマンスまでは測定してないので)。
最初に要点だけまとめておくと、
という話です。
先に結論だけ書くと、
という話です。以下、実際にどういうケースでこれが役立つかの説明です。
js-ctypesはFirefox 3.6から利用できるMozillaの独自の機能で、平たく言うとC言語の実装の中で定義された関数をJavaScriptから呼べるようにするという物。Pythonにctypesという機能があって、それのJavaScript版がjs-ctypes。
Firefox 3.6(Gecko 1.9.2)ではできる事の制限が厳しかったので使えるケースがあんまり無かったようなんだけど、Firefox 4(Gecko 2.0)では構造体がサポートされたので一気に使える場面が増えた。らしい。
システムモニターをFirefox 4に対応させなきゃねと思ってたんだけど、Compartmentがどうとか色々変更があったのを全部調べてたら絶対自分の手に負えん!!と思ったので、いっそのことjs-ctypesで実装すりゃいいんじゃね? と思って、試行錯誤しながらやってみてる。試行錯誤の様子はリポジトリを見るとバレバレです。
js-ctypesのいい所:
困った所:
parseInt()
する、みたいな癖を付けとくとハマらなくていいと思う。JavaScriptでFirefoxをクラッシュさせたかったらjs-ctypesでメモリ破壊とかやると手っ取り早いですよ! と、数え切れないほどFirefoxをクラッシュさせて思いました。
14日くらいから、Rewind/Fastforward Buttonsを入れてるとFirefoxが頻繁にクラッシュするという現象が発生するようになっているようです。wedataのAutoPagerize用データベースに新しく追加された項目がトリガーになって、長すぎる正規表現か何かの制限に引っかかるようになってクラッシュしているものと考えられます。問題としては認識しているのですが、現在修正のための時間を取れない状態なので、アドオンを無効化するか、about:configで以下の設定を変更して回避して下さい。
Firefox Developers Conference 2010の時に再起動不要なアドオンの作り方を調べた時、調査の成果を元にJetpack SDKもといAdd-on SDKを使わずにFirefox 3.6とMinefieldの両方に対応したBootstrapped Extensionを作るためのテンプレを作った。それ以後、新しく作るアドオンでBootstrapped Extensionにできそうな物はなるべくそのように開発していこうと思ってて、Back to Owner Tabは実際そうして作ったアドオンの第1号ということになる。
SDKの使い方や仕組みを調べた時にもちょっと思ったんだけど、Back to Owner Tabを作って、「設定ダイアログを持てないのがこんなに辛いとは……」ということを改めて感じた。しかしBootstrapped Extensionで設定ダイアログを提供するのは簡単には実現できなさそうだったので、ずっと諦めてた。
今回、うまく解決できそうな方法をふと思いついて、実際試してみたら案外うまくいったので、テンプレの一部としてライブラリ化してみた。Back to Owner Tabの設定ダイアログもこれで提供してる。
Firefox 2以降ではXULにprefwindowというウィジェットが存在していて、これはFirefox自体の設定ダイアログにも使われている物で、非常に出来がよい。今ある拡張機能の多くも、これを使って設定ダイアログを提供している。
prefwindowができるまでは「設定値を保持して、変更を監視して、変更があったらUIに反映して、ダイアログがキャンセルされたら変更を破棄して……」てな事をいちいち考えてゼロから設定UIを設計しなきゃいけなかったから、非常に辛かった。そういう思いがあるので、Bootstrapped Extensionでも是非これを使いたかったんだけど、Bootstrapped Extensionの「Chrome URLを登録できない」という制限のせいで無理だった。
どうしてもやりたければ、XUL以外の方法で頑張って設定UIを作るか、XULで物凄く苦労して作るしか無いってことになって、それはどっちも嫌だった。せっかくprefwindowという素晴らしい物が目の前にあるのに、それを使わないでオレオレUIをゼロから作るなんて、馬鹿馬鹿しくてやってられない。
せめて最新のSDKだったら設定ダイアログを作る機能が入ってたりしないだろうか? あるんならそれを丸パクリできないかな? と思ってたんだけど、SDKに設定ダイアログのためのAPIが入るのはまだ先のことらしいと聞いて、それも諦めざるを得なかった。
それで仕方なく「about:configでカスタマイズしてね」ということにしてたんだけど、まあ普通に考えてこれはエンドユーザ向けとしては酷い話なわけですよ。自分で使う時も、マウス操作だけで設定を変えられないのは困る。
そんな風に悶々としていて、ふと、「そういえばuserChrome.jsとかではdata: URIでXULドキュメントを動的に作ってたよな」ということに思い至った。試しにdata: URIにprefwindowで作った設定ダイアログの内容を突っ込んでnsIWindowWatcherのopenWindow()
でウィンドウとして開いてみたら、少なくともMinefield 4.0b11preでは正常に動いてくれた。data: URIでドキュメントを読み込んだりウィンドウを開いたりする時はオープン元のスクリプトの権限が引き継がれるっぽいので、file: URLでXULを読み込ませた時のような問題も起こらないようだ。
また、E4Xを使えばJavaScriptの中に直接XMLを書けて、しかも属性値にJavaScriptの変数を埋め込める。これなら、propertiesファイルベースでのローカライズでもラベル等を埋め込むのが苦にならない。で、そうして作ったE4XのXMLオブジェクトをtoXMLString()
すれば、data: URIで読み込ませるダイアログのためのソースコードが得られる。
ということで、「XULのソースコードを文字列として受け取ってdata: URIにしてウィンドウを開く機能」と「アドオンマネージャでアドオンの設定を開くボタンが押下された時に前述の機能を呼び出す機能」を作れば、Bootstrapped Extensionでもprefwindowベースの設定ダイアログを提供できる事が分かった。
propertiesファイルの内容はローカライズ済みの文字列をXULのstringbundle要素を介して取得する方法が一般的だと思うけど、より低レベルな実装のnsIStringBundleServiceを直接呼べば、JavaScriptコードモジュールやBootstrapped Extensionの中からでもpropertiesファイルの中身を簡単に取り出せる。(stringbundle要素はXBLでそういう処理を行うようにしてあるに過ぎない。)
上記のテンプレには、nsIStringBundleServiceをラップしてstringbundle要素と同じインターフェースで使えるようにするライブラリを既に入れてある。
var bundle = require('path/to/lib/locale.js')
.get('file://.../messages.properties');
var title = bundle.getString('config.title');
という風にすると、ブラウザのUIの言語がjaでmessages.properties.jaというファイルがあればそれが使われて、存在しない場合はmessages.properties.en-US→messages.properties(サフィックス無し)とフォールバックしていく。
Back to Owner Tabに入ってるバージョンのライブラリには含まれてないけど、テンプレのHEADでは resolve('path/to/file')
とすると現在のファイルを起点にして相対パスを解決できるようにしてあので、こういう風にも書ける。
var bundle = require('path/to/lib/locale.js')
.get(resolve('locale/messages.properties'));
なお、Bootstrapped Extensionの場合は読み込んだpropertiesファイルの内容がキャッシュされたままになってしまうとまずいので、shutdown()
の時にはnsIStringBundleServiceのflushBundles()
というメソッドを呼んで、メモリ上のキャッシュを消すようにする必要がある。このライブラリでもそうしてて、自分でnsIStringBundleServiceを使う時はここに気をつけないといけない。
Bootstrapped ExtensionやJavaScriptコードモジュールの名前空間からで既存のDOMWindowを取れない状態でウィンドウを開きたい場合には、nsIWindowWatcherのopenWindow()
を使う必要がある。このメソッドに渡すURIとしてdata: URIを指定すれば、その内容のウィンドウがChrome権限で開かれる。
var uri = 'data:application/vnd.mozilla.xul+xml,'
+ source;
Cc['@mozilla.org/embedcomp/window-watcher;1']
.getService(Ci.nsIWindowWatcher)
.openWindow(null, uri, '_blank',
'chrome,titlebar,toolbar,centerscreen',
null);
ソース文字列の生成は、E4Xを使うと楽にできる。
var xml = <>
<prefwindow id="backtoowner-config"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
title={bundle.getString('title')}>
<prefpane id="prefpane-general"
label={bundle.getString('general')}/>
</prefwindow>
</>;
var source = '<?xml version="1.0"?>'
+'<?xml-stylesheet href="chrome://global/skin/"?>'
+xml.toXMLString();
source = encodeURIComponent(source);
ラベル文字列等に日本語が入る場合を考慮して、ソース文字列はdata: URIに繋げる前にエスケープしておこう(これをしないと文字化けする)。
基本的にはこれでいいんだけど、実際このまま使うと何となく気持ち悪い。というのも、prefwindowは画面上でのウィンドウの位置や大きさ、最後に選択されていたパネルの名前等をプロファイル内のlocalstore.rdfに自動的に保存するんだけど、この時保存されるデータはウィンドウのURIをキーとして紐付けられているから、data: URIのウィンドウだとlocalstore.rdfが肥大化してしまう。しかも、設定項目を追加したりしてウィンドウの中身が変わるとdata: URIも変わるから、前回情報を保存した時のエントリとは別のエントリが作られてしまって、古いエントリは自動的には消えないから、localstore.rdfが際限なく膨らんでいってしまう事になる。長く使うことを考えたら、これはよくない。
で、何かいい方法はないか考えてみた。
document.write()
でウィンドウの中身を動的に書き換える。nsIWindowWatcherのopenWindow()
は開かれたウィンドウのDOMWindowオブジェクトを返す。なので、こんな風にできないかと最初は考えた。
var window = ww.openWindow(...);
window.document
.open('application/vnd.mozilla.xul+xml');
window.document.write(source);
window.document.close();
が、駄目だった。この方法でドキュメントを書き出すと強制的にHTMLのパーサーが使われてしまうらしく、XULのソースを書き出しても全く動作しない状態になってしまった。
次に、DOM操作でやることを考えた。
XUL Documentの中でRangeを作ってcreateContextualFragment()
すると、XULのソース文字列からDOMDocumentFragmentを作ることができる。こうしてルート要素も含めたドキュメント全体を作り直してゴソッと入れ替えたらどうなるだろうか。
var window = ww.openWindow(...);
var range = window.document.createRange();
range.selectNode(window.document.documentElement);
var fragment = range.createContextualFragment(xml.toXMLString());
window.document.replaceChild(fragment, window.document.documentElement);
実際やってみたら、これは期待通りには動いてくれなかった。多分、開かれたウィンドウの内容がまだ完全には読み込まれ切っていない状態でDOM操作を外から無理矢理やろうとしたからなんだろうと思う。
開かれたウィンドウのDOMContentLoadedを待ってからDOMを操作すればprefwindowについてはちゃんと動くみたいなんだけど、dialogとかのもっと汎用的な物も受け付ける事や、ダイアログの中にDOMContentLoadedを待って処理を開始するようなコードを含めたい時に、それでは問題が起こる気がする。
そういうわけで今のバージョンでは、ウィンドウを開く時にnsIVariantの形でソース文字列を渡して、開かれた側のウィンドウの中に1つだけ置いたscript要素でarguments[0]
を受け取ってすぐにドキュメントを書き換えるようにしてみている。要点だけ抜き出すとこんな感じ。
var variant = Cc['@mozilla.org/variant;1'].createInstance(Ci.nsIWritableVariant);
variant.setFromVariant(source);
ww.openWindow(
null,
'data:application/vnd.mozilla.xul+xml,'
+encodeURIComponent(
'<?xml version="1.0"?>\n'
+'<!-- ' + aURI + ' -->\n'
+'<?xml-stylesheet href="chrome://global/skin/"?>\n'
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/javascript"><![CDATA[
var d = document;
var e = d.documentElement;
var r = d.createRange();
r.selectNode(e);
d.replaceChild(r.createContextualFragment(arguments[0]), e);
r.detach();
]]></script>
</window>.toXMLString()
),
'_blank',
'chrome,titlebar,toolbar,centerscreen',
variant
);
URIをコメントで埋め込んでるのは、複数のアドオンでこのライブラリを使ってる時に衝突しないようにしたかったから。
実はここに辿り着くまでに結構紆余曲折があった。最初はルート要素を差し替えられるという事に気づいていなかったので、「ルート要素の属性値の情報」と「子孫要素のソース」をそれぞれ取り出してやってみていた。E4Xは実はよく分かってないからちょっと苦労した。
var content = xml.*.toXMLString(); // 子孫要素だけ抽出
var container = xml.copy(); // ルート要素だけ取り出すためにまず一旦全体を複製
var attributes = container[0].attributes(); // ルート要素の属性のリストを取得
var attributesHash = {};
for each (var attribute in attributes) { // 属性名と値のペアのハッシュにする
let name = attribute.name();
attributesHash[name] = attribute.toString(); // 属性値はtoString()で取れる
delete container[0]['@'+name]; // removeAttribute()に相当する操作はこう書く
}
// 子孫要素を削除して、コンテンツ差し替え用のスクリプトに入れ換える
delete container.*;
container.script = <script type="application/javascript">{this._loader}</script>;
container = container.toXMLString();
最初はこれでいいかと思ってたんだけど、コンテンツ差し替え用のスクリプトが走ってる時点で既にprefwindowのXBLのコンストラクタが実行されてしまっていて、複数のprefpaneがあるときの切り替え機能が働いてなかった。
ルート要素をもう一度作りなおしてドキュメントに挿入すればXBLのコンストラクタが実行されるはず、ということでdocument.removeChild(document.documentElement);
の後でdocument.appendChild(newRoot);
みたいな風な事も試してみたけど、一瞬documentElement
が無くなってしまうのがマズいようで、他のどっかの処理がルート要素を参照するタイミングとぶつかってエラーになってしまった。ここはdocument.replaceChild(newRoot, document.documentElement);
で一気に差し替えてしまうのが正解だった。昔のGeckoはreplaceChild()
でクラッシュする事があったような気がするので無意識に使用をずっと避けてきてたんだけど、Firefox 3.6以降ともなるとさすがに問題なく動いた。
あと、生成した新しいルート要素を挿入しようとするとエラーになる場合もあって、その時は挿入しようとしているノードをdocument.importNode()
に1回通してやれば動いた。まあ最終的にはそれ無しで乗り切れたんだけど。
これも難儀した。
Firefoxのアドオンマネージャは、install.rdfのoptionsURL
に書かれてるURIをwindow.openDialog()
で(Mac OS X以外では)モーダルダイアログとして開くようになっている。しかし前述の通りXULの設定ダイアログを読み込ませたかったらChrome URLにしないといけないので、これはそのままは使えない事になる。
とはいえ、optionsURLにはChrome URLしか使えないという話ではない。
最初は、ここに直接javascript:ダイアログを開くためのコード
という感じのスクリプトを書く事を考えてみた。でもメタデータの置き場所であるinstall.rdfにそういう実装を含めるのはどうかと思うし、そもそもここにjavascript:スキームを使って書いたスクリプトは普通にコンテンツ領域の権限でしか実行されないようなので、nsIObserverServiceを使ってイベントを発行するとかそういう高度な事は全然できなかった(実際試して無理だった)。
次に、nsIWindowWatcherでの監視とかnsIObserverServiceを介して送られるchrome-document-global-createdとかcontent-document-global-createdとかの通知でもって「あらかじめ登録されていたURI」が読み込まれた事を検知して、その読み込み(ウィンドウのオープン処理)をキャンセルして別の設定ダイアログを開くという事を考えた。
これは結構イイ線まで行ったんだけど、「Firefoxのアドオンマネージャが開いたウィンドウ」が一瞬だけ見えてしまうという点をどうしても回避できなかった。openDialog()
の実装を辿ってnsGlobalWindow::OpenDialog() → nsGlobalWindow::OpenInternal() → nsWindowWatcher::OpenWindowJS() → nsWindowWatcher::OpenWindowJSInternal()と掘り返して、何か抜け道はないか探しまくったんだけど、内部で新しいDOMWindowが生成されてウィンドウが実際に画面に出現する事がほとんど確定した後で初めてnsIDocShellのloadURI()
にURIが渡されてる(この間URI文字列はC++の普通の変数で引き回されてるだけでJavaScriptの層からは触りようがない)みたいで、ウィンドウがユーザの目に触れる前にどうこうってのは原理的に無理っぽい。
いや、nsIContentPolicyインターフェースを備えたXPCOMコンポーネントを作ってカテゴリマネージャに登録すれば、もしかしたらそこに割り込む事はなんとかできるかもしれない。けど、設定ダイアログ1つ開くためだけにそこまでする元気はなかったし、求める機能に対してコードが膨大になりすぎるから普通にこれはないわーって感じだしで、そういう方向性は最初から切り捨ててる。
結局、すべてのドキュメントの読み込みをchrome-document-global-createdとcontent-document-global-createdの通知で捕捉して、アドオンマネージャのウィンドウが開かれた時にはボタンのクリック操作を監視するイベントリスナを登録し、「設定」ボタンをユーザがクリックした瞬間を捕捉して、登録済みのURIだったらイベントをキャンセルして別のダイアログを開く、という所に落ち着いた。これだとアドオンマネージャの実装(現在どのアドオンが選択されているのか、そのアドオンの設定ダイアログのURIは何なのか、を取得する方法)にべったりになってしまわざるを得ないんだけど、まあその辺のレイヤだったら変更があっても追随するのはそう大変じゃなかろうと思って、妥協する事にした。
冒頭に書いたけど、ここまでのまとめがBootstrapped Extensionsテンプレートにライブラリとして入ってる。他のファイルの内容に可能な限り依存しないように作ったつもりなので、改造再利用も不可能ではないと思う。実際にどう使うのかというのはBack to Owner Tabでの利用例が参考になるかもしれない(って単にE4XでダイアログをJavaScriptのコードの中に埋め込んでるだけだけど)。
ほんとはFirefox 4に向けて記事を書いていかなきゃいけないんだけど、こういう「あれも駄目でした、これも駄目でした、結局こういう所に落ち着かざるを得ませんでした」という紆余曲折は本に入れてもウケないんじゃないかなーって思ったので、ここに書く事にした。また次に同じような事をしようと思った時に同じ轍を踏まなくてもいいように。技術情報のドキュメントは、未来の自分のために残しておく物なのです。