たまに18歳未満の人や心臓の弱い人にはお勧めできない情報が含まれることもあるかもしれない、甘くなくて酸っぱくてしょっぱいチラシの裏。RSSによる簡単な更新情報を利用したりすると、ハッピーになるかも知れませんしそうでないかも知れません。
の動向はもえじら組ブログで。
宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。
以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能!
一つ前のエントリの続き。
「すべて強調表示」の時に強調箇所以外を暗くするというSafari風ハイライト表示は、元はSafariHighlightを取り込ませてもらったものなんだけれども、基本的には、「画面の最全面に半透明の黒いボックスを表示して全体を覆う」「その上にz-indexを調整して強調箇所を浮かび上がらせる」という二つの操作が鍵になっている。で、このうち前者の方の操作のせいで、「暗くなった所にあるリンクをクリックしても、ハイライトが解除されるだけで、リンク先には飛べない」という問題が起こっていた。
まあ、問題というか実装上そうならざるを得ないという感じで、本家のSafariもこういう仕様だったと思うからまあいいじゃんと思わなくもないんだけど、要望はあるようなので対応しなきゃなーと思ってた。で、この度晴れて対応してみた。highlight.jsの最後の方に付け加えたresendClickEventメソッドがそれ。
resendClickEvent : function(aEvent)
{
var utils = aEvent.view
.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindowUtils);
if ('sendMouseEvent' in utils) { // Firefox 3
var flags = 0;
const nsIDOMNSEvent = Components.interfaces.nsIDOMNSEvent;
if (aEvent.altKey) flags |= nsIDOMNSEvent.ALT_MASK;
if (aEvent.ctrlKey) flags |= nsIDOMNSEvent.CONTROL_MASK;
if (aEvent.shiftKey) flags |= nsIDOMNSEvent.SHIFT_MASK;
if (aEvent.metaKey) flags |= nsIDOMNSEvent.META_MASK;
window.setTimeout(function(aX, aY, aButton) {
if (ZoomManager.useFullZoom) { // Firefox 3のフルズームへの対応
aX = aX * ZoomManager.zoom;
aY = aY * ZoomManager.zoom;
}
utils.sendMouseEvent('mousedown', aX, aY, aButton, 1, flags);
utils.sendMouseEvent('mouseup', aX, aY, aButton, 1, flags);
}, 0, aEvent.clientX, aEvent.clientY, aEvent.button);
}
else { // Firefox 2, emulation
var args = [
'click',
aEvent.bubbles,
aEvent.cancelable,
aEvent.view,
1,
aEvent.screenX,
aEvent.screenY,
aEvent.clientX,
aEvent.clientY,
aEvent.ctrlKey,
aEvent.altKey,
aEvent.shiftKey,
aEvent.metaKey,
aEvent.button
];
window.setTimeout(function(aSelf, aFrame, aX, aY) {
var node = aSelf.getClickableElementFromPoint(aFrame, aX, aY);
if (!node) return;
var event = aFrame.document.createEvent('MouseEvents');
args.push(node);
event.initMouseEvent.apply(event, args);
node.dispatchEvent(event);
if ('focus' in node) node.focus();
}, 0, this, aEvent.view, aEvent.screenX, aEvent.screenY);
}
},
getClickableElementFromPoint : function(aWindow, aScreenX, aScreenY)
{
var accNode;
try {
var accService = Components.classes['@mozilla.org/accessibilityService;1']
.getService(Components.interfaces.nsIAccessibilityService);
var acc = accService.getAccessibleFor(aWindow.document);
var box = aWindow.document.getBoxObjectFor(aWindow.document.documentElement);
accNode = acc.getChildAtPoint(aScreenX, aScreenY);
accNode = accNode.QueryInterface(Components.interfaces.nsIAccessNode).DOMNode;
}
catch(e) {
}
var filter = function(aNode) {
switch (aNode.localName.toUpperCase()) {
case 'A':
if (aNode.href)
return NodeFilter.FILTER_ACCEPT;
break;
case 'INPUT':
case 'TEXTAREA':
case 'BUTTON':
case 'SELECT':
return NodeFilter.FILTER_ACCEPT;
break;
}
return NodeFilter.FILTER_SKIP;
};
if (accNode &&
accNode.nodeType == Node.ELEMENT_NODE &&
filter(accNode) == NodeFilter.FILTER_ACCEPT)
return accNode;
var doc = aWindow.document;
var startNode = accNode || doc;
var walker = aWindow.document.createTreeWalker(startNode, NodeFilter.SHOW_ELEMENT, filter, false);
for (var node = walker.firstChild(); node != null; node = walker.nextNode())
{
var box = doc.getBoxObjectFor(node);
var l = box.screenX;
var t = box.screenY;
var r = l + box.width;
var b = t + box.height;
if (l <= aScreenX && aScreenX <= r && t <= aScreenY && aScreenY <= b)
return node;
}
return null;
},
見ての通り、Firefox 3ではnsIDOMWindowUtilsの新機能を使ってイベントを発行するようにして根本的解決を図り、Firefox 2ではTabScope由来のコードにnsIAccessibleを組み合わせたものを使ってそれっぽいことをしている、という感じです。どっちもタイマーを使って処理を遅らせているのは、画面全体を覆っているスクリーンが消えてからイベントを発行しないと、もう一度スクリーンをクリックしたのと同じ事になってしまうから。
最初、nsIDOMWindowUtilsのsendMouseEventでclickイベントを発行しようとしてうまくいかなかったんだけど、どうやらこれはDOMのイベントをそのまま発行するわけではなく、本当の本当にユーザの操作をエミュレートするという物みたいだ。間を開けずmousedown→mouseupと実行するとクリックしたのと同じ事になる。
強調表示関係ではこのほかに、テキストフィールド内の強調を自動的に解除するようにするための改良もあります。これも長くなったから別エントリで。
pXMigemoFind(pIXMigemoFindの実装)、特にfindメソッドまわりを中心にだいぶ書き直した。といってもアルゴリズム的には変わってなくて、主にメンテナンス性を向上することを目的にした書き換えです。こういうのもリファクタリングと言っていいんでしょうか。
findInDocumentメソッドがかなり長くて中のループのネストも深くなってたので、これをだいぶ細かく分けた。
まず、フレームのツリーを辿る処理がループの最後の方にどかっとあったので、これを取り出してIteratorパターンのヘルパーオブジェクトとして実装することにした(最後の方にあるDocShellIteratorというやつ)。最初Iteratorパターンというのを知った時には「こんなの何の役に立つんだ?」とか「配列みたいに長さを取り出せた方が便利じゃね?」とか思ってたけど、こうして実際に書いてみると、「次に処理対象にする物を探す」部分だけに特化して作り込めるので便利だ、ということがよく分かった。
具体的には、今までは前方検索と後方検索それぞれの場合で「一つ前のフレーム」に処理を移すのか「一つ後のフレーム」に処理を移すのかをいちいち判別してたんだけど、その判別を行う部分まで含めてDocShellIteratorとして分離することで、findInDocumentの側では何も考えずにDocShellIteratorのiterateNextメソッドを呼べば適切な結果が帰ってきてウマーという風にできるようになった。
実際のコードで見ると、findInDocument側は
docShell = this.getDocShellForFrame(doc.defaultView)
.QueryInterface(Components.interfaces.nsIDocShellTreeNode);
if (aFindFlag & this.FIND_BACK) { // back
docShell = this.getPrevDocShell(docShell);
if (!docShell) {
if (!(aFindFlag & this.FIND_WRAP)) {
docShell = this.getDocShellForFrame(doc.defaultView.top);
docShell = this.getLastChildDocShell(docShell.QueryInterface(Components.interfaces.nsIDocShellTreeNode));
doc = docShell
.QueryInterface(Components.interfaces.nsIDocShell)
.QueryInterface(Components.interfaces.nsIWebNavigation)
.document;
this.document.commandDispatcher.focusedWindow = docShell
.QueryInterface(Components.interfaces.nsIDocShell)
.QueryInterface(Components.interfaces.nsIWebNavigation)
.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindow);
if (
!editableInOut ||
findRange.sRange.startContainer == aDocument.body ||
findRange.sRange.startContainer == aDocument.documentElement
)
aFindFlag |= this.FIND_WRAP;
continue;
}
this.dispatchProgressEvent(found, aFindFlag);
break doFind;
}
}
else { // forward
docShell = this.getNextDocShell(docShell);
if (!docShell) {
if (!(aFindFlag & this.FIND_WRAP)) {
doc = Components.lookupMethod(doc.defaultView.top, 'document').call(doc.defaultView.top);
this.document.commandDispatcher.focusedWindow = doc.defaultView.top;
if (
!editableInOut ||
findRange.sRange.endContainer == aDocument.body ||
findRange.sRange.endContainer == aDocument.documentElement
)
aFindFlag |= this.FIND_WRAP;
continue;
}
this.dispatchProgressEvent(found, aFindFlag);
break doFind;
}
}
doc = docShell
.QueryInterface(Components.interfaces.nsIDocShell)
.QueryInterface(Components.interfaces.nsIWebNavigation)
.document;
if (doc == aDocument) {
this.dispatchProgressEvent(found, aFindFlag);
break doFind;
}
こーんなだったのが
aDocShellIterator.iterateNext();
if (aDocShellIterator.wrapped) {
if (!(aFindFlag & this.FIND_WRAP)) {
this.document.commandDispatcher.focusedWindow = aDocShellIterator.view;
if (
!editableInOut ||
!rangeSet ||
aDocShellIterator.isRangeTopLevel(rangeSet.range)
)
aFindFlag |= this.FIND_WRAP;
continue;
}
this.dispatchProgressEvent(aFindFlag, resultFlag);
break;
}
if (aDocShellIterator.isInitial) {
this.dispatchProgressEvent(aFindFlag, resultFlag);
break;
}
ここまでスッキリしました。まあその代わり、フレームのツリーを辿る処理(DocShellIteratorの方がちょっと長くなってしまったんだけど、それぞれロジック的には綺麗に分かれているから、今後手を入れる時は「ツリーを辿ること」と「検索すること」のどっちか一方のことだけに集中して作業できるわけで、これこそがイテレータパターンの最も大きな意義だったんですね。デザインパターン万歳。
あと、findInDocumentの中でループの中でループを回していた部分をfindInDocumentInternalメソッドとして分離した。これはロジックを分離する効果はなくて、単にネストを浅くしてコードを見やすくするためだけの変更。のつもりだったんだけど、これをうまくやるために、他の部分も含めてだいぶ手直ししないといけなかった。
何故かというと、単純に内側のループだけをfindInDocumentInternalとして分離したところで、元のfindInDocumentとfindInDocumentInternalとの間でお互いに引き渡さないといけない情報(引数として引き渡す情報と、返り値として戻す情報の両方)が多すぎて、そのまま二つに分けただけだと却って分かりにくくなってしまったから。普通にreturnするだけじゃ情報を渡しきれないから、オブジェクトを引数で渡して、そのオブジェクトのプロパティとして返り値を設定して、変更されたプロパティを呼び出し元の側でまた参照して……という感じになってしまって、せっかくメソッドを分けたのにそこの所でガッツリ繋がってしまってて、これじゃ分けた意味がないよと。
というわけで最低限必要な情報だけに絞り込んでやりとりするように設計を検討したのに加えて、インターフェースの定義も見直して、今まであんまり有効活用してなかったビット演算を多用するようにしたことで、findInDocumentInternalからの返り値は最終的にビット列一つだけにまでまとめることができた。ビット演算いいよビット演算。
ビット演算を有効に利用するためには、どの意味をどのビットに与えるかというのをよく考えておかないといけない。とかなんとか大仰な言い方をしてみたけど、要するに、適当に定数プロパティを名前順に並べて先頭から0, 1, 2, 4, 8……と値を割り振っていくなんていいかげんな事してちゃ駄目だよってことですね。0.8.1まででは
const unsigned short FOUND = 0;
const unsigned short NOTFOUND = 1;
const unsigned short WRAPPED = 2;
const unsigned short NOTLINK = 4;
const unsigned short FOUND_IN_EDITABLE = 8;
こんなだったけど、これじゃあ実質的にはただの定数値が並んでるだけで単純比較にしか使えない。FOUNDが0だったり「検索はヒットしたけどリンクじゃないからヒット無しとみなす」という意味のNOTLINKなんてのがあったり。一つの値に複数の意味が割り当てられてるんじゃ、重複しないビットを割り当てても意味がないわけです。
const unsigned short NOTFOUND = 0;
const unsigned short FOUND = 1;
const unsigned short WRAPPED = 2;
const unsigned short FOUND_IN_LINK = 4;
const unsigned short FOUND_IN_EDITABLE = 8;
こーいう風にしておけば、NOTLINKに相当する場合は「FOUND | WRAPED」、そうでない場合には「FOUND | FOUND_IN_LINK」とかそんな風に書けるわけで、これなら「普通にヒットした」「普通にヒットしたけど一度ページの末尾まで検索してもう一度頭からやり直した」「入力欄の中でヒットした」などなどいろんな場合もひっくるめて全部「flag & FOUND」というビット演算いっこで判定できる。
あとSafari風強調表示の改善もあるんだけど、長くなるから別のエントリに分けます。
好きなように正規表現で検索できるようになりました。XUL/Migemo 0.8.0からの新機能ということになります。というかAPIとしては持ってたのにUI上から利用する手段が今までなかった。
「普通の検索」と「正規表現」と「Migemo」の3モードが排他的な切り替えということになるので、UIをどうするか迷って、チェックボックスを増やしてみたりドロップダウンリストを置いたり色々試してみたけど、最終的にはラジオボタンを並べる形に落ち着いた。
使う側としてはややこしくなってしまったので、せめてちょっとでも使い勝手をよくしておきたいと思って、キーボード操作だけで検索モードを切り替えたり、入力内容が /ほげほげ/ みたいな正規表現リテラルっぽい形だったら自動的に正規表現検索に切り替えたり(判別にはNarcissus由来の正規表現を使わせてもらいました)、という風な工夫も入れてみたけど、実際の所どうだろう。
という外見でよく分かる変更の他に変わった所では、後方検索の仕組みを今までとガラッと変えてみた。
元々の作者のplus7さんはどうされていたかというと、正規表現のマッチングで後方検索ができないから、「正規表現の内容を前後逆にひっくり返して」「検索対象の範囲のテキストも全部反転して」その上でマッチングを行う、という方法を採っていて、僕が引き継いだ後もそこは変わりなかった。
var regexp = /(ほげ|hoge|foo|bar)/;
var text = 'ほげほげ、hogehoge。foo? bar。';
// ここで、「bar」「foo」「hoge」「hoge」
// 「ほげ」「ほげ」の順に検索したい
regexp = regexp.compile(
regexp.source
.split('')
.reverse().join('')
.replace(/\)/g, '[[')
.replace(/\(/g, ')')
.replace(/\[\[/g, '(')
);
text = text.split('').reverse().join('');
var found;
while(text.match(regexp))
{
found = RegExp.lastMatch.split('').reverse().join('');
alert(found);
text = RegExp.rightContext;
}
(なんでこんなややこしいことをするのかについてはplus7さんの技術解説を参照のこと)
しかし、この度正規表現検索機能を表に出すにあたって重大な問題が発覚した。元の「正規表現を反転する処理」は、反転しなきゃいけない正規表現がXUL/Migemo自身の生成したごく単純なパターンの物だけだから、かなり簡単な作りになっていた。でも、ユーザが手入力で凝った正規表現を入力すると、途端に処理がうまくいかなくなってしまう。
これは非常にまずい。ということでなんとかならないかと考えてみた。
まず、正規表現を反転する処理をもっと改善する事を考えてみた。でもこれは早々に諦めた。調べてみればみるほど、正規表現という物は複雑怪奇な書き方ができるようで、その全部の記法に対応するなんでのはとてもじゃないけど僕の頭じゃ無理。
となると残された道としては、正規表現も検索対象の範囲のテキストも反転せずにどうにかして後方からマッチングを行う方法を見つけるしかない。
んでJavaScriptのリファレンスを見ながらうんうん考えてたんだけど、ふと思いついた方法を試してみたらなんとバッチリうまくいった。そのやり方というのは、「g」フラグを使うというもの。
gフラグを付けてマッチングを行うと、「最後にヒットした箇所」はマッチングの結果の配列の最後の要素として取得できる、というのは当たり前。でもそれだけじゃなくて、実験して確認してみたら、この時のRegExp.leftContext
とRegExp.rightContext
は、gフラグ無しの時は最初にマッチした箇所から見た「それより前」と「それより後」だったのが、今度は、最後にマッチした箇所に対する物になっていた(よく考えてみれば当たり前のことだけど、今まで全然気付いてなかった)。
ということは、このleftContextを次の検索のマッチング対象にし、そこでもまたgフラグを使って、さらにその時のleftContextを次の検索のマッチング対象にして……という風に繰り返していけば、「正規表現で後ろから検索」するのと同じ結果が得られるわけだ。
var regexp = /(ほげ|hoge|foo|bar)/;
var text = 'ほげほげ、hogehoge。foo? bar。';
regexp = regexp.compile(regexp.source, 'g');
var found;
while(text.match(regexp))
{
found = RegExp.lastMatch;
alert(found);
text = RegExp.leftContext;
}
というわけで無事、どんな正規表現でも問題なく後方検索にかけることができるようになった次第です。しかも文字列の分割とか連結とか配列の反転とかいろんな処理がゴッソリ不要になったから、長いページでは後方検索がちょっと高速になったんじゃないかと思う。
やっとBeta5準拠のデザインで巻き戻し/早送りボタンのアイコンを作り直したぜKeyholeにもばっちりたいおうだぜ、と思った瞬間にデザイン変更が入って僕涙目。
各ボタンの位置が2ピクセルずつ変わってるとかの地味な変更が。これはイジメだろう……
Firefox 2まででは、position:fixedな要素はz-indexを適切に指定すれば内容領域の上にも普通に表示できた。なので、ウィンドウ全体を覆い尽くすというのも簡単だった。
でもFirefox 3では仕様が変わって、常にサブフレームの内容が上に表示されるようになったため、CSSのポジショニングだけでは任意の要素を任意の位置に表示することはできなくなってしまった。仮にposition:fixed; top:0; left:0なボックスを置いても、その上にブラウズ領域の中身が表示されてしまう。ウィンドウの内容の上に何かを重ねて表示したい時には汎用のポップアップであるpanel要素を使え、というのがFirefox 3流のやり方らしい。
しかし古い拡張機能をアップデートするにあたってそこらへんのつくりを全面的に書き換えるのは大変な労力を要するし、果てしないregressionの嵐が発生しかねない。できれば最小限の労力で、今までのやり方に一工夫加えるだけで正常に動作するようにしたい。
ということでウィンドウ全体を覆い隠してゴニョゴニョするアレの時に試行錯誤して、CSSのポジショニングをどーしても使いたければサブフレームを新たに生成するしかないっぽいという結論に辿り着いていただけれども、その方法をタブカタログで使おうとしたところDOMDocumentの違いとかで死にそうになったので、諦めて別の方法をいくつか探ってみた。
で、結論としては、visibility:hiddenになったサブフレームの上なら、普通にCSSのポジショニングで要素を重ねられるようです。display:noneとかvisibility:collapseとかをサブフレームに使うとDocument Shell(nsIDocShellのオブジェクト)がぶっ壊れてしまってまともに動かなくなるので、この辺のプロパティをいじるのはヤバイと思って今まで全く触ってなかったんだけど、少なくともFirefox 3ではFirefox 2でもFirefox 3でも、visibility:hidden/visibleの切り替えではDocument Shellは破壊されないようだということに、ああでもないこうでもないといじってるうちにたまたま気がついたのですよ。
でもこれだとタブカタログのように、一旦全面を覆い尽くしてその上に全然別のコンテンツを表示するといった機能は作れるけど、InterNoteのようにページ上に任意の要素を置くっていうことはできそうにない。そういう場合はページ内のDOMDocumentを編集するしかないようだ。
regression(後退バグ。修正のために加えた変更が原因で新たな問題が発生すること)のせいで体力・精神力を消耗する事が重なり、自動テストドリブンな開発の重要さを身に染みて感じた。プレゼンでも言ったけど、「やってりゃ良かった」と後悔してばかりだ。まさかこんなに手こずる羽目になるとは当初は思っていなかったから。
自動テストはregressionの発生を防ぐ(正確には、regressionを残したままでいることを防ぐ)素晴らしいメソッドだ。と今になって改めて思う。
でも、どんなコードでも機械的に自動テストにかけられるわけではない。機械的に自動テストを実施するには、自動テストを実行しやすい設計になっていなければならない。自動テストを実行しやすい設計とは、粒度が小さい=オブジェクトやらメソッドやらが可能な限り細かい単位で分割されている設計のこと。テスト対象となる部品自体が可能な限り自己完結していて、外部的な要素は必要に応じてすべてパラメータとして与えるようになっていること。横着して一つの関数内からグローバル変数やら何やらを参照しまくっていると、その関数のロジックそのものをテストすることができない。
そういうわけで、自動テストドリブンな開発には気を遣わなければならない。気を遣わないといけないから気力を消耗する。だから、せずに済むのなら、しないままでいたい。でも、そう考えているうちに「自動テストを作成する&テストを実施しやすい設計にするコスト」と「やっつけで作って、その都度メンテナンスするコスト」の関係が、「前者>後者」だったのがいつの間にか「前者<後者」に逆転してしまうようになっていて、メチャメチャ後悔することになる。
今まで自分のやってきたことは9割方、自動テスト無しでもどうにかやってこれていたし、そもそも、自動テストの重要性とそのための設計の指針が分かった今改めて見返してみても、自動テスト化することが困難な物が多かった、と思う。だから、自動テストを前提にしてコードを書くという習慣が身についていない。自動テストが最初から不可能な事が多かったから、自動テストのできない設計にするしかなかったから、自動テストのできない設計にすることが当たり前になってしまっている。
でもいい加減、その悪習を断ち切らなければならない。25歳ももうすぐ終わりの、四捨五入すれば三十路の、今更も今更で手遅れ感がとても強いけれども、やらなければいけない。今まで自分がやってきた方法は通用しないということを自覚しないといけない。できて当たり前の事が今まで全くできていなかったという事、本当はこの面子の中で飛び抜けて一番遅れているという事、それなのに対等であるように勘違いして思い上がっているという事、今まで他人事だと思ってた「ダメな人の典型」に自分がまったく当てはまっているという事、全部認めないといけない。
そういうわけでとりあえずUxUはマジオススメ。
おお、これは……戻る/進むサムネイルでそのうちやりたいと思ってた事そのまんまだ。自前サムネイルを持つのをやめて、これのサムネイルを参照するようにしようかなぁ……
なんでも評点:人は選択肢が多いほど疲れることが判明 ― だからネタ探しは重労働。能動的なネットが受動的なテレビに勝てないのもこのため?
実感としてなんとなくそうだよなとずっと思っていたので、それが実験によって示されたというのは、ほっとしたというか、合点がいったというか、言葉にするのが難しいけど、「選択できるのが良いことだ」とオウムのようにそればっかり繰り返すことの愚かさを科学的に指摘できる材料が一つできてちょっとグッと来る、そんな感想を持った。拡張機能を何千個と並べてそれだけで良しとしてちゃいけねえよと。
でもこれはFirefoxにとっては武器にもなり得るよね。多機能な製品ほど精神を疲弊させる、その点素のFirefoxは機能がそんなに多くないから楽に使えるよと。必要になったら人に勧められた拡張機能をその都度入れればいいのだから、と。
オープンソース法 第7章 MPLライセンス 1. Mozillaの物語によると、弁護士でもあるMitchell Baker(Mozilla Corporation会長/トカゲ世話役主任)がMPL(当時はNPL)を作ったんだと。知らんかった。まあ確かに古株で弁護士資格を持ってるってことはそういうこともあったと考えて当然だったんだけど。
そういえばちょっと用事があってInkscapeを久しぶりにインストールして使ってみたら、ぼかし効果がサポートされててびびった。しかもレイヤツールとかのそれっぽいツールボックスも増えてるし、だいぶ実用的になってるように見える。Inkscape始まったな(今更)。
で、同じくぼかし効果が要のtext-shadowやbox-shadowはFirefoxでいつになったら使えるようになるんでしょーか。Firefox 4まで待てってことか……