たまに18歳未満の人や心臓の弱い人にはお勧めできない情報が含まれることもあるかもしれない、甘くなくて酸っぱくてしょっぱいチラシの裏。RSSによる簡単な更新情報を利用したりすると、ハッピーになるかも知れませんしそうでないかも知れません。
の動向はもえじら組ブログで。
宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能!
1つ前のエントリに書いた、可変フレームなトゥイーン効果の実装の話。
今時は便利なJavaScriptのアニメーション用ライブラリが色々あるからわざわざ自分で書くような必要はないんだろうけど、自分はほんの一箇所だけのためにライブラリ全部突っ込むというのは気が引けるタイプなので、ピンポイントな実装とその理屈を(雑学として)書いておこう。
先に、タブのインデント幅変更の処理の完成したものを貼っておく。説明を簡単にするためにちょっと省略してる。
indentDuration : 200,
updateTabIndent : function(aTab, aProp, aIndent)
{
this.stopTabIndentAnimation(aTab);
var startIndent = this.getPropertyPixelValue(aTab, aProp);
var delta = aIndent - startIndent;
var duration = this.indentDuration;
var startTime = Date.now();
aTab.__treestyletab__updateTabIndentTimer = window.setInterval(function(aSelf) {
var progress = Math.min(1, (Date.now() - startTime) / duration);
var powerForStyle = Math.sin(90 * power * Math.PI / 180);
var indent = (progress == 1) ?
aIndent :
startIndent + (delta * powerForStyle);
aTab.setAttribute(
'style',
aTab.getAttribute('style')+';'+
aProp+':'+indent+'px !important;'
);
if (progress == 1) aSelf.stopTabIndentAnimation(aTab);
}, 10, this);
},
stopTabIndentAnimation : function(aTab)
{
if (!aTab.__treestyletab__updateTabIndentTimer) return;
window.clearInterval(aTab.__treestyletab__updateTabIndentTimer);
aTab.__treestyletab__updateTabIndentTimer = null;
},
getPropertyPixelValue : function(aElement, aProp)
{
var style = window.getComputedStyle(aElement, null);
return Number(style.getPropertyValue(aProp).replace(/px$/, ''));
},
なんでCSSOM使ってないの、って所にはツッコまないように。
アニメーションというと、つまりはちょっとずつ値を変えて再描画するということで、単純に考えたら多分こうなる。
function doAnimation(aElement, aStart, aEnd)
{
for (var i = aStart; i < aEnd; i++)
{
aElement.style.marginLeft = i+'px';
}
}
でもこれはダメ。CSSのプロパティを変更しても、その状態が描画されるより前に次の値がセットされてしまうので、間のアニメーションがアニメーションにならない。(CSSのプロパティが変更された瞬間に再描画する実装だったらこれでもいいかもだけど、少なくともGeckoではダメ。)
もうちょっと改良すると、こう。
function doAnimation(aElement, aStart, aEnd)
{
var margin = aStart;
var timer = window.setInterval(function() {
aElement.style.marginLeft = (margin++)+'px';
if (margin == aEnd) {
window.clearInterval(timer);
}
}, 10);
}
こういう風にタイマーを使ってやれば、きちんと各コマが描画されるようになる。
しかしこのやり方だと、最初の値から最後の値までの全コマが必ず描画される(=フレームレート固定)ので、貧弱な環境だとものすごい遅いアニメーションになってしまう。上の例だと、10ミリ秒ごとに1ピクセルずらして、というのを移動距離の分だけ繰り返すことになってしまう。
「高速な環境ではたくさん描画していいけど、低速な環境だと再描画を減らしてほしい。とにかく、1回のアニメーションは決まった時間の中できちんと終わらせたい。」これが可変フレームレートの基本的な考え方。
可変フレームレートにする場合、描画と描画の間にどれだけ時間がかかったか、というのが鍵になる。
JavaScriptのsetIntervalのタイマーは、(少なくともGecko/SpiderMonkeyでは、)仮に実行間隔を10ミリ秒にした場合、実際には「1回目の処理の時間」+「10ミリ秒のインターバル」+「2回目の処理の時間」+「10ミリ秒のインターバル」……という風な感じで時間が過ぎていく。アニメーションにかける全体の時間が分かっているなら、「n回目の描画開始時の時点で、アニメーションにかける時間全体の何%が過ぎたか」をまず計算して、移動量をそのパーセンテージから求めてやればいい。
function doAnimation(aElement, aStart, aEnd)
{
var delta = aEnd - aStart;
var duration = 200; // アニメーション効果全体を200ミリ秒で終わらせる
var startTime = Date.now(); // (new Date()).getTime() と同等
var timer = window.setInterval(function() {
var progress = Math.min(
1,
(Date.now() - startTime) / duration
);
var margin = (progress == 1) ?
aEnd :
aStart + (delta * progress) ;
aElement.style.marginLeft = margin+'px';
if (progress == 1) {
window.clearInterval(timer);
}
}, 10);
}
これで、可変フレームレートのアニメーションになった。progress
はアニメーションの進行状況を示していて、0(開始時点=0%)から1(終了時点=100%)の間の値を取る。開始時の値と終了時の値の差にこれをかけてやれば、「今の時点ではこれだけ移動してるはず(その位置に描画すればよい)」ということが分かるワケ。(ちなみにこの時点で、値が増加していくのか減少していくのかどっちなんだ、ということも気にせずに済むようになっている。)
この時点では、値の変化率は一定なので、理科の時間に習う「等速直線運動」ってやつになっている。実際のUIでこれをそのまま使うとちょっと味気ないので、もうちょっとかっこよくしてやりたくなるところだ。可変フレームレートならそれも簡単にできる。
よくあるのは、「最初はすごい速度で飛んできて、最後はフワッと着地する」みたいな効果だろう。これは三角関数のsin()やcos()を使えば簡単に計算できる。
円と複素数平面の勉強をしたことがあれば、0°から90°の間を1°ずつ動く間に、Xの値は「最初はゆっくりと、最後は急速に」Yの値は「最初は速く、最後はゆっくり」増加していくことが分かるだろう。sin(θ)やcos(θ)を使えば、この「XやYの値の変化率」を取り出して利用できる。
例えば「最初は速く、最後はゆっくり」のアニメーションなら、上の例の(delta * progress)
の所を(delta * Math.sin( (progress * 90) * Math.PI / 180 ))
にすればいい。これで、0°から90°までの間のYの値の変化率に応じた移動量になる。(Math.sin()
やMath.cos()
は角度をラジアンで指定しないといけないので、θ×π÷180で度数をラジアンに変換している。)
以上、中学や高校で勉強する数学って案外無駄にならないんだよ、というお話でした。
ちなみに、aStart + (delta * progress)
の所あたりを任意の式に置き換えれば、動き方を等速直線運動や等加速度運動やはね回る等の色んな形に変えることができる。これを、「<ruby><rb>最初の値</rb><rp>(</rp><rt>Start</rt><rp>)</rp></ruby>・<ruby><rb>値の最終的な変化量</rb><rp>(</rp><rt>total Change</rt><rp>)</rp></ruby>・<ruby><rb>アニメーションにかけたい時間</rb><rp>(</rp><rt>Duration</rt><rp>)</rp></ruby>・<ruby><rb>現在までに過ぎた時間</rb><rp>(</rp><rt>Time</rt><rp>)</rp></ruby>(最後の2つから現在の進度が分かる)という4つのパラメータを受け取り現在の値を返す関数」として定義した物をeasing関数と呼ぶそうで、世にある多くのアニメーションライブラリは、このeasing関数を入れ替えることで色んなエフェクトを得られるようにしたもの、と言うことができる。
……という風に読み解いてみると、普段使ってるライブラリが一体何をどのように処理しているのか分かっておもしろいんじゃないでしょうか。僕はライブラリ使ってませんが。
追記。Norahさんのこれって yield 使ったらダメなんだろうか?
というコメントを見た。べつに使っちゃダメってことはないし、むしろ自分も一瞬「あれ、これyieldで書けるんじゃね?」と思ったんだけど、どっちにしろタイマーでループ回すという事には変わりないし、そうなるとここで書いてるくらいの規模だとコードの量が無駄に増えるだけって気がしたので、そのままタイマーだけで書いた次第です。
追記。easing関数の説明を間違えてたので修正しました……
trunkでとうとうgetBoxObjectForがエラーを吐くようになってしまった - alice0775のファイル置き場 - Yahoo!ジオシティーズ
これを見て焦って今頃になってやっと調べた。
パッチによると、nsIDOMNSDocumentからgetBoxObjectFor()
が消えて、nsIXULDocument専用のメソッドになった。ということなので、HTMLDocumentでgetBoxObjectFor()
を使っているコードは全滅だ。何とかして代わりの方法を見つけないといけない。
document.getBoxObjectFor(element)
で取れるのはnsIBoxObject、element.getBoundingClientRect()
で取れるのはnsIDOMClientRectで、インターフェースが違う。
取りたい値 | nsIBoxObject | nsIDOMClientRect |
---|---|---|
ボックスの幅 | box.width | rect.right-rect.leftまたはrect.width |
ボックスの高さ | box.height | rect.bottom-rect.topまたはrect.height |
ボックスの左上の点のX座標(ドキュメントの原点基準) | box.x+左ボーダー幅 | rect.left+window.scrollX |
ボックスの左上の点のY座標(ドキュメントの原点基準) | box.y+上ボーダー幅 | rect.top+window.scrollY |
ボックスの右下の点のX座標(ドキュメントの原点基準) | box.x-左ボーダー幅+box.width | rect.right+window.scrollX |
ボックスの右下の点のY座標(ドキュメントの原点基準) | box.y-上ボーダー幅+box.height | rect.bottom+window.scrollY |
ボックスの左上の点のX座標(ビューポートの原点基準) | box.x+左ボーダー幅-window.scrollX | rect.left |
ボックスの左上の点のY座標(ビューポートの原点基準) | box.y+上ボーダー幅-window.scrollY | rect.top |
ボックスの右下の点のX座標(ビューポートの原点基準) | box.x-左ボーダー幅+box.width-window.scrollX | rect.top |
ボックスの右下の点のY座標(ビューポートの原点基準) | box.y-上ボーダー幅+box.height-window.scrollY | rect.bottom |
nsIDOMClientRectのwidth
とheight
はどうもGecko 1.9.1以降でしか使えないっぽい。
例えば今使ってる環境のdocument.getElementById('reload-button')
の場合はこんな感じ。
この時の値は以下の通り。
nsIBoxObject | nsIDOMClientRect |
---|---|
box.width==36 | rect.width==36 |
box.height==36 | rect.height==36 |
box.x==83 | rect.left==82 |
box.y==23 | rect.top==22 |
rect.right==118 | |
rect.bottom==58 |
x
とleft
、y
とtop
の間にずれがあるのは、nsIBoxObjectのx
とy
がいわゆるborder-box(border + padding + contentのボックス)ではなくpadding-box(padding + contentのボックス)基準であるからということらしい。nsIBoxObjectからborder辺の座標を取るには、getComputedStyle()
でborderの幅を取って計算してやらないといけない。nsIBoxObjectもnsIDOMClientRectも、これら以外のプロパティはborder-box基準のようだ。
で、上の表には書いてないけど、nsIBoxObjectにはscreenX
とscreenY
というプロパティがあって、こちらで取れる画面上の座標もborder-box基準。そして、nsIDOMClientRectにはこれらプロパティが無いので、画面上の座標を取ることができない。
大まかに言って、nsIBoxObjectのx
はnsIDOMClientRectのleft
、nsIBoxObjectのy
はnsIDOMClientRectのtop
に読み替えて差し支えない。となると、残る問題は、screenX
とscreenY
に相当する値をどう取るかだ。
で、試行錯誤の結果、以下のようなコードができあがった。
function getBoxObjectFor(aNode)
{
// getBoxObjectFor() がある時はそれを使う。
if ('getBoxObjectFor' in aNode.ownerDocument)
return aNode.ownerDocument.getBoxObjectFor(aNode);
var box = {
x : 0,
y : 0,
width : 0,
height : 0,
screenX : 0,
screenY : 0
};
try {
var rect = aNode.getBoundingClientRect();
var frame = aNode.ownerDocument.defaultView;
box.x = rect.left + frame.scrollX;
box.y = rect.top + frame.scrollY;
box.width = rect.right-rect.left;
box.height = rect.bottom-rect.top;
// 親フレームの要素を辿っていく。
box.screenX = rect.left;
box.screenY = rect.top;
var owner = aNode;
while (true)
{
frame = owner.ownerDocument.defaultView;
owner = getFrameOwnerFromFrame(frame);
if (!owner) {
// 最上位のフレームまで来てしまったら、仕方ないのでwindowのプロパティを使う。
// でもウィンドウの枠の外側の座標なので、激しくずれる。
box.screenX += frame.screenX;
box.screenY += frame.screenY;
break;
}
if (owner.ownerDocument instanceof Ci.nsIDOMXULDocument) {
// XULのドキュメント中の要素なら画面上の正確な位置を取れる。
let ownerBox = owner.ownerDocument.getBoxObjectFor(owner);
box.screenX += ownerBox.screenX;
box.screenY += ownerBox.screenY;
break;
}
let ownerRect = owner.getBoundingClientRect();
box.screenX += ownerRect.left;
box.screenY += ownerRect.top;
}
}
catch(e) {
}
return box;
}
function getFrameOwnerFromFrame(aFrame)
{
// window.parentでは、<browser type="content"/> の内容の
// フレームからは親を辿れない。
// nsIDocShellTreeItemを経由すれば可能。
var parentItem = aFrame
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell)
.QueryInterface(Ci.nsIDocShellTreeNode)
.QueryInterface(Ci.nsIDocShellTreeItem)
.parent;
var isChrome = parentItem.itemType == parentItem.typeChrome;
var parentDocument = parentItem
.QueryInterface(Ci.nsIWebNavigation)
.document;
// フレームに結びついてるiframe要素を直接取る方法が分からないので、
// 泥臭い方法を……
var nodes = parentDocument.evaluate(
'/descendant::*[contains(" frame FRAME iframe IFRAME browser tabbrowser ",'+
'concat(" ", local-name(), " "))]',
parentDocument,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null
);
for (let i = 0, maxi = nodes.snapshotLength; i < maxi; i++)
{
let owner = nodes.snapshotItem(i);
if (isChrome && owner.wrappedJSObject) owner = owner.wrappedJSObject;
if (owner.localName == 'tabbrowser') {
let tabs = owner.mTabContainer.childNodes;
for (let i = 0, maxi = tabs.length; i < maxi; i++)
{
let browser = tabs[i].linkedBrowser;
if (browser.contentWindow == aFrame)
return browser;
}
}
else if (owner.contentWindow == aFrame) {
return owner;
}
}
return null;
}
これにさらにborder幅によるズレとかposition:fixed;
の場合への対応とかも盛り込んだ物を、ライブラリにしてみた。見ての通りchrome特権をバリバリに使ってるので、このコードはアドオンの中でしか動かない。画面上の絶対位置が必要になる場面なんてのはアドオンの場合くらいだろうから、別に問題ないと思うけど。
screenX
とscreenY
に相当する値を取るためにけっこう面倒なことをしているので、オーバーヘッドがきっと半端ない。nsIDOMClientRectの持ってる情報だけで済む場合はそれだけ使った方がいいと思う。
ちなみに、安直な発想でXULDocument.getBoxObjectFor.call(HTMLDocument, HTMLElement)
というのも考えてみたけど、これは実際には使えない。残念。
popInが入っているとテキストリンクが動かない、ことの理由はどうも以下の2点によるみたい。
stopPropagation()
しているため、テキストリンクにイベントが渡されなくなっている。stopPropagation()
されない状態にしてテキストリンク側でイベントを拾って処理を行おうとしても、イベント発生時とテキストリンクが処理を行う時とでDOMツリーの構造が微妙に変わっていて、正常に動かない。何か有効な対策が無いか考えてる。
ところでpopInは内部的にjQueryを使ってるようなんだけど、その旨の表記を僕には見つけられなかった。jQueryはMITライセンスとGPLのデュアルライセンスで、MITを選択した場合でもThe above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
(著作権表示とMITライセンスの許諾表示をソフトウェアの全コピーかもしくは重要な箇所で示す必要がある)ということなので、下手したらMITライセンスの違反ということになるような気が…… (一応フィードバックフォームから送ってはみた)
中野さんが、JavaScriptにはsleep(一定時間待ってから次の処理に進むという命令文)が無いせいでテストを書くのに難儀したという話を書かれているけれども。まさにこれをどうにかしたくて、UxUは進化してきたようなものと言える。
知ってる人は知ってるだろうけど、Firefox/Thunderbirdアドオン向けの自動テスト実行ツール・UxUでは、テストを書く上でsleepに相当する機能を利用できる。これは、JavaScript 1.7でジェネレータ・イテレータ機能を実現するために追加されたyield式のトリッキーな使い方だ。
これを実現しているのは、lib/utils.jsのdoIteration()
と、test/test_case.jsのrun()
ということになる。ここから要点だけを取り出すと、以下のような事をしている。
まず、スクリプトの書き手は、「sleepを使いたい処理」を関数として定義する。この時、sleep
の代わりにyield
を使う。
var task = function() {
doSomething1();
yield 1000;
doSomething2();
yield 1000;
doSomething3();
}
次に、スクリプトの書き手は、この関数を後述するdoIteration()
に渡す。すると、doIteration()
がよしなに計らって、「doSomething1()
を実行した後、1000ミリ秒待って、doSomething2()
を実行し、また1000ミリ秒待って、doSomething3()
を実行する」という風な形で先程の関数の内容を実行する。
この時のdoIteration()
の内容は、以下のような感じ。
function doIteration(aTask) {
if (typeof aTask == 'function') {
// 渡されたのが関数だったら、まず、評価した返り値を得る。
aTask = aTask();
}
if (!aTask ||
!('next' in aObject) ||
!('send' in aObject) ||
!('throw' in aObject) ||
!('close' in aObject) ||
aObject != '[object Generator]') {
// 渡されたオブジェクトまたは関数の返り値が
// ジェネレータ・イテレータではない場合、何もしない。
return;
}
// ここからがミソ!
// 全部の処理が終わったかどうか、を示すオブジェクトを定義
var finishFlag = { value : false, error : null };
var last = 0; // スリープ開始時点の時刻を保持する変数
var sleep = 0; // スリープの長さ(秒数)を保持する変数
var timer = window.setInterval(function() {
if (
// スリープの長さがちゃんと指定されていて
sleep > 0 &&
// スリープ開始時点からの経過時間がスリープとして
// 指定された時間未満であれば
(Date.now() - last) < sleep
) {
// ここで処理を終えて、100ミリ秒後まで待つ。
return;
}
// スリープとして指定された時間が経過したので、処理を進める。
try {
// 次にyieldが登場するまでの間の処理を実行。
sleep = aTask.next();
// next()の返り値はyield式に渡された値。
last = Date.now(); // スリープ開始時刻を保持して
return; // 処理を一旦終えて100ミリ秒後を待つ。
}
catch(e if e instanceof StopIteration) {
// 最後のyield式の後の内容が実行されて、定義された関数の内容が
// 最後まですべて実行されると、StopIteration例外が発生する。
// よって、処理完了とみなす。
finishFlag.value = true;
}
catch(e) {
// それ以外の未知の例外が発生した時は、処理中断とする。
finishFlag.error = e;
}
// 100ミリ秒ごとの繰り返し処理を停止。
window.clearInterval(timer);
}, 100);
return finishFlag;
}
UxUの内部でやってる事は基本的にはこういう事。ただ、実際にはもうちょっと使い勝手をよくするために細かい処理が加わってる。
doIteration()
が中で何をやってるのかを知らなければ、パッと見は、sleepという命令文の名前がyieldに変わっただけのようにすら見えるんじゃないだろうか。そこが、このやり方の狙いだ。スクリプトの書き手はタイムアウトだのコールバックだのといった難しい事を何も考えなくても良くて、単に「sleep文に相当する機能が加わったJavaScript」として好きなように処理を書く事ができる。テストを書くための工数が大幅に削減される(かもしれない)ので、テストを書くのが苦にならず、ばりばりテストを書けるようになる(はず)。その結果、充実したテストのおかげでより安心して開発に専念できるようになる(はず)。という理屈です。
ちなみに、勘のいい人は気付くだろうけど、setInterval()
に渡している関数の冒頭に以下の内容を挿入すれば、doIteration()
をいくらでも入れ子にできるようになる。
if (
typeof sleep == 'object' &&
(!sleep.value || !sleep.error)
) {
return;
}
var task = function() {
doSomething1();
yield 1000;
yield doIteration(function() {
doSomething2();
yield 500;
doSomething3();
yield doIteration(function() {
doSomething4();
yield 100;
doSomething5();
});
});
doSomething6();
};
doIteration(task);
さらに、こんなこともできる。
var task = function() {
doSomething1();
yield doIteration(function() {
var flag = { value : false; }
frame.addEventListener('load', function() {
frame.removeEventListener('load', arguments.callee, false);
flag.value = true;
}, true);
frame.contentDocument
.defaultView
.location.href = 'http://www.example.com/';
yield flag;
// フレームの読み込みが終わったらここに進む
doSomething2();
});
doSomething3();
};
doIteration(task);
この辺をもっと簡単に書けるようにヘルパーメソッドを色々と整備したのがUxUのテスト実行環境、と思ってもらえれば大体それで正解です。
25日追記。他にも色々やり方があるようです。(どっちもMozilla限定だけど)
このへんで触れてた件に、Firefox 3.1で変化があるみたい。
Bug 446026 – restore utility of eval(s, o)
でも何が変わったのか(何をできて、何をできないのか)よく分かってない。
AIRMigemoというライブラリがあることをリファラで知った。AIRアプリにMigemo検索機能を組み込むのに使えるということだろうか。でもライセンスが分からない……
肝心の正規表現の生成処理は、かなり真面目にやってるような印象。XUL/Migemoの現在の実装は長い文字列の一括置換と分割とソートによる擬似的な物なので、「短い入力で長い単語にマッチさせる」という元のMigemoの特徴の一つを損なうことなく持っていると考えられる。
UxU(UnitTest.XUL)を利用したFirefoxアドオンのデバッグの例 - ククログ(2008-11-17)
XUL/Migemo 0.11.7での修正内容が典型的な「自動テストを使ったデバッグ」だったので、UxUのチュートリアルを兼ねて、会社のサイトの方に書いてみました。UxUの解説って言うよりは、テスト駆動開発自体の解説という気もしますが。
リンク先に解説してるのはpXMigemoFindのfindFirstVisibleNodeメソッドだけのデバッグ話ですが、実際にはこのメソッドはだいぶ根幹に関わる物で、このメソッドの挙動の変更によって他の機能に色々と影響が出る可能性がありました。が、他の挙動に関しては一通り自動テストを作成済みだったために、後退バグの発生で収拾不能な事態に陥るということを恐れずに安心して修正に取り組むことができた、というまさに自動テスト様々な事例だったということも忘れずに付け加えておきたい所です。
XUL/Migemoの動作で怪しい所を見つける→再現条件確定→その条件下でのテストを行うためのUxUのテストケースを作成→何かちゃんと動かない→UxUのバグ発見→抜本的修正開始→途中で疲れて寝る→抜本的修正続き→やっとチェックイン→XUL/Migemoのテストを書く気力がなくなってる→それでもめげずにテスト書き再開→UxUの別の問題発覚→心が折れかける(今ここ)
今まで全然知らなかったんだけど、vimperatorでXUL/MigemoのAPIを使ってタブの切り替えやヒントモードを強化するなんてことをやってる人がいたんだ。(←って、分かったような書き方をしてるけどvimperatorの事は全然分かってません……)
その関係でいくつかページを渡り歩いてたら、XUL/Migemoのバグって話題が出ていて、なぬ!と思ってさらに辿ってみた所、半角括弧がらみの問題のことらしい。あーこの辺ちゃんと見直さないままずっとここまで来てたんですよね……UxU用のテストも基礎部分の単体テストはさっぱり手つかずのままだったし(ぉぃ)。ということで本腰入れてテスト書いて、潜在してたバグを潰し始めました。でもまだまだ見落としがありそう。
ツリー型タブとクリック証券のFXツールバーが衝突しているという話を見かけたので調べてみたんだけど、だいぶお手上げです。
リバースエンジニアリング禁止とあったけどべつに僕はこれを利用するつもりも使用するつもりもないのであくまで自作アドオンとの競合の原因を調査するために難読化されたコードを頑張って解読してみたところ、FXツールバーは非同期処理のための便利メソッドを持ってるくせに何故か初期化の時は同期処理にしているという事が分かった。何故そんなところで手を抜くんだ……
サーバからの設定の読み込みを非同期で行って、設定の読み込み完了後に初期化処理の続きを行うようにすれば、この問題は解消されると思うんだけど。ツリー型タブ以外にも物によっては衝突する可能性があるし、向こうの方で対処してくんないかなあ。望み薄かなあ。一応問い合わせ先のアドレス宛にメールしてみたけど……
続報。返信があり、初期化処理を非同期で行うように改良された新バージョンがもうすぐ公開されるとのことです。反応はやっ!(僕がメールするより前から準備してたっぽい?)