ScriptUIPanelで起動する
ドッキングパネルにしたい!!
スクリプトでUIを作る際、通常のウィンドウ(dialog、palette、window)として表示するのが基本になる。ただし条件を満たしていれば、AfterEffectsの「プロジェクト」や「コンポジション」などといった「ドッキング可能なパネル」として起動することが出来る。これは標準のパネルと同様に好きな位置・サイズで配置できる。便利!
条件は2つある。
「ウィンドウ」メニューから起動する
ドッキングパネルで起動されることを想定してスクリプトが作成されている
1は難しくない。このページでは主に2に重点を置いて書く。
条件1:「ウィンドウ」メニューから起動する
AfterEffectsの「ウィンドウ」メニューからスクリプトを起動すれればパネルとして起動される。ただそれだけ。ただそれだけなのだが、「ウィンドウ」メニューにスクリプトを表示させるためにすべきことがある。それは、
「ScriptUI Panels」フォルダにスクリプトファイルを置く
ただそれだけ。特に難しくない。こちらのページの「ファイルの保存場所と起動方法」も参照していただきたい。
AfterEffects各バージョンの「Scripts」フォルダ内にある「ScriptUI Panels」直下に置けばよい。直下ということを忘れない。通常、スクリプトを「Scripts」フォルダに置く時は、適当に作ったフォルダの中のスクリプトもAfterEffectsの「ファイル/スクリプト」メニューに現れるが、「ScriptUI Panels」フォルダの中は直下しか見てくれない。
ちなみに置くのは本体(*.jsx、*.jsxbin)はもちろん、本体へのショートカットでも構わない。「スクリプトで使う画像ファイルと一緒に置いておきたい」など、フォルダ構造を崩したくないスクリプトの場合は本体はどこでも好きな場所に置いて、その本体ファイルへのショートカットを「ScriptUI Panels」に置けばいい。
条件2:ドッキングパネルで起動されることを想定してスクリプトが作成されている
これが問題。作り方を知らないと、ただ単に「ScriptUI Panels」フォルダにスクリプトファイルを置いたとしても右図のように変な表示になってしまう。
こんなのを求める人は世界中に一人もいない。ただのミス。
パネルで起動されたにも関わらず、スクリプト内部でウィンドウを作成・表示するコードを書いているのでこのようになる。
どうするか。
ウィンドウじゃなくてパネルにアクセスする
以下のスクリプトを実行してみる。
alert( this );
「this」は使うタイミングによって中身がいろいろ変わる。「javascript this」でググってみれば情報が沢山出てくる。
通常、スクリプト内で「this」の中にはグローバルオブジェクト(まぁなんか宇宙みたいな存在)が入っている。ESTKから実行したり、jsxファイルとして保存後、AfterEffectsの「ファイル/スクリプト」メニューから実行すると画像の様に[object global]が返ってくるはず。
上記スクリプトをjsxファイルで保存、「ScriptUI Panels」フォルダに入れて「ウィンドウ」メニューから起動してみる。すると[object Panel]と表示される。
このPanelとは、まさにドッキングパネルそのものを指す。なのでパネル起動の時は、ウィンドウではなくこのthisにパーツを追加していけばいい。
パネルにボタンを表示するサンプル
先程も書いたように、通常は例えば、
var w = new Window('dialog'); //①
w.add('button',[0,0,100,100],"ボタン"); //②
w.show(); //③
のようにして、ウィンドウを作成(①)し、そのウィンドウにパーツを追加(②)して、ウィンドウ表示(③)していた。ドッキングパネルの時はウィンドウではなくパネルに追加する。パネルはthisで取得できるので、
this.add('button',[0,0,100,100],"ボタン");
とすればいい。この1行だけ。これを保存して「ウィンドウ」メニューから起動すれば右図のようにパネルにボタンが付いて表示されるはず。
注意したいのは、ここでウィンドウ表示の時のように、
this.show();
みたいにして、show()を付けてはいけない。
show()をつけてはいけない理由
ウィンドウ表示の時、w.show()を実行しなければ、いくらウィンドウにボタンなどを付けてもウィンドウ自体が表示されない。なのでパネル起動の時もそのノリでやりたくなる。
this.show();
やりたくなるのはわかるが、それはエラーになる。理由は簡単。show()メソッドはウィンドウオブジェクトだけに用意されたメソッドであって、パネルはそんなものもっていないから。パネル起動の時は、thisが[object Panel]になっているのはさっき見た通り。thisつまりPanelにはshow()なんてメソッドは用意されていない。
「じゃあどうやって表示すんの?」思うよね。実験してみる。ソースコードに何も書かなくていい。適当に名前を付けて保存して「ウィンドウ」メニューから実行してみて。
した?
パネル出たよね。ソースコード空っぽなのに、起動しただけでパネルが出た。つまり、そもそもこちらから表示の命令を出す必要が無いってこと。
ウィンドウはごちゃごちゃパーツ付けたり設定が完了してから、show()で表示するのが普通。一方、パネルの場合は真っ先にパネルが表示される。表示された後にパーツをごちゃごちゃ付けていく。
自動レイアウトを使う時の罠
上記の「パネルにボタンを表示するサンプル」で示したサンプルは、ボタンの位置とサイズ([0,0,100,100]の部分)を与えていたが、自動レイアウトを使いたい時はそこは指定してはいけないのであった。(自動レイアウトのページ参照)
具体的な指定をしないようにundefinedを入れて実行してみる。
//ウィンドウ専用のコード
var w = new Window('dialog');
w.add('button',undefined,"ボタン");
w.show();
これを通常の「ファイル/スクリプト」メニューから実行すると、問題無く表示される。
一方、パネル用のコードを同じ様にしてみる。
//パネル専用のコード
this.add('button',undefined,"ボタン");
これをパネル起動してみる。パネルは出るが、ボタンが付いていないはず。何故か。
layout()メソッドを忘れない
自動レイアウトのページでも書いているが、layout.layout()がミソ。
自動レイアウトの時は、
w.layout.layout();
を実行することで、自動レイアウトが実行され、位置・サイズを指定しなかったパーツのそれが自動で計算される。自動レイアウトと言っても結局各パーツの位置・サイズが決まっていないと表示しようがないから。
「じゃあなんでウィンドウ起動の時はちゃんと表示できてるんや!どこにもw.layout.layout()使ってないやん!!」
これも自動レイアウトのページに書いてある。
"Windowをshow()した時にも自動で実行される。なのでWindowの時は実は書かなくても表示できちゃう。"
そう、show()を実行すると、自動的にw.layout.layout()が実行されているのである。
//ウィンドウ専用のコード
var w = new Window('dialog');
w.add('button',undefined,"ボタン");
w.show();
これ実は、
//ウィンドウ専用のコード
var w = new Window('dialog');
w.add('button',undefined,"ボタン");
w.layout.layout();
w.show();
を実行しているのと一緒。だから自動レイアウトが実行されている。
ただ、パネル起動の時はどうか。パネルは勝手に表示されるし...そもそもshow()も使えないし...。layout()が実行されていない。だったら自分で呼び出すしかなかろう。
//パネル専用のコード
this.add('button',undefined,"ボタン");
this.layout.layout();
こうすれば表示される。
パネル表示で自動レイアウトを使用する時は絶対にlayout.layout()を実行しなければならない。
文字列方式で一気に追加したい時
ドッキングパネルでももちろん文字列方式でパーツの追加が出来る。
例えば、以下のコードをウィンドウでなく、パネル用に書き換えたいとする。
//ウィンドウ専用のコード
var w = new Window('dialog{\
et : EditText{ characters : 20 }\
gr : Group{\
ok : Button{ text : "OK" }\
cancel : Button{ text : "cancel" }\
}\
}');
w.show();
ウィンドウの時はこんな感じで、ウィンドウ作成と同時に一気にパーツをぶち込むことで、一度もadd()を使用しないということも出来る。
しかし、ドッキングパネルの時はそれが出来ない。スクリプト実行と同時に先にパネルが出来てしまうから。ウィンドウで例えるなら、
var w = new Window('dialog');
これだけ先にやられちゃったという感じ。ウィンドウだけを先に作った状態。なのでこのwに対して1度はadd()しないといけないよね。
今回はウィンドウではなく、パネルであるthisに対してadd()すればいいのだが、ウィンドウの時みたいにまとめて一発でバチーンと済ませたい。
自分でやってみてほしい。
やった?
つまづいた?
グループを噛ませてadd()を一回にまとめる
まずはウィンドウ直下に追加してるEditText。今回はthisにedittextを追加するから...
this.add('edittext{ characters : 20 }')
と書いて、気づくだろう。「え、グループどうしたらいいん?」と。
さっきはウィンドウの直下にEditTextとGroupがあった。パネルの直下に追加しようと思ったらまたadd()しないとダメなん?
まぁそれも方法の1つ。その場合、次のようにする。
//パネル起動専用のコード
this.et = this.add('edittext{ characters : 20 }');
this.gr = this.add('group{\
ok : Button{ text : "OK" }\
cancel : Button{ text : "cancel" }\
}');
this.layout.layout();
add()すると大体の場合、何かしら変数で受けないといけない(this.et = ~とかthis.gr = ~とか)が個人的には受け付けない。めんどくさい。
そんな「add()は一回で十分です!!」というadd()嫌い派の方にはこちらをおすすめする。
//パネル起動専用のコード
this.gr = this.add('group{alignment : ["fill","fill"],\
et : EditText{ characters : 20 }\
gr : Group{\
ok : Button{ text : "OK" }\
cancel : Button{ text : "cancel" }\
}\
}')
this.layout.layout();
一度トップレベル(ウィンドウorパネルのこと)の直下にグループを作成する。その中に全てのパーツを入れていくことで、add()は一回で済む。注意したいのは、このグループオブジェクトにalignment : ["fill","fill"]を設定しておくこと。このグループはadd()回数を減らすためのダミーな存在。そいつが各パーツのレイアウト時に邪魔になってはいけない。(最上位のグループのサイズが足りてないから正しく配置が出来ない、とか)
alignment : ["fill","fill"]にしておくことで、トップレベルにピッタリくっつくのでそれより内側に配置するパーツの邪魔にならない。
ウィンドウ/パネルどっちにも対応したハイブリッドな書き方
パネル起動する前提でthisにボタンなど追加していったとしても、ユーザーがパネル起動してくれる保証がどこにある?夢見てんじゃないよ!
「みなさ~ん!パネル起動前提のスクリプトなので『ファイル』メニューからは実行しないでくださーい!!必ずウィンドウメニューから実行してくださ~い!!」て言う?誰も聞かんよ、そんなの。そもそもその違いもよくわかってないもの。「バグりました。起動できません。怖いからもう使いません」。ユーザーに期待して傷つくのはあなたよ!!
普通に実行されてしまった場合、thisにはパネルではなくグローバル変数が入っているので、
this.add('button');
なんて、add()なんか使っちゃったりしちゃってエラーになる。それはもうスクリプト作者であるあなたが予測して回避しておくべきこと。パネルで起動されたのか違うのかを判断して、ソースコードを書き分けるべき。
instanceofを使って、ウィンドウ/パネルどちらの起動かの判別
パネル起動されたかどうかというのはthisの中身で判断できる。
「this = パネルオブジェクト」⇒ドッキングパネルでの起動
「this = グローバルオブジェクト」⇒普通の起動
このどっちかであるので、「thisがPanelオブジェクト かどうか」を調べればいい。
それにはこうする。
this instanceof Panel
instanceofは雑に言うと、「instanceofの左側のオブジェクト(this)の種類は右側のやつ(Panel)ですか?」というのを調べることが出来る。true/falseで返ってくる。上の例だと、thisがPanelオブジェクトの時はtrue、それ以外のときはfalseが返ってくる。
余談だが、これを使ってコンポ(CompItem)、AVレイヤー(AVLayer)、シェイプレイヤー(ShapeLayer)、プロパティ(Property)、ファイル(File)、UIのボタン(Button)などオブジェクトの種類をいろいろ調べることが出来る。スクリプト作るなら絶対に使うから覚えておく。
三項演算子を使っての場合分け
あとは簡単、パネル起動の時はthisをウィンドウ代わりに使えばいいし、パネル起動じゃ無い時はnew Window()でウィンドウを作成すれば良い。つまり、
if(this instanceof Panel){
var w = this;
}
else{
var w = new Window('dialog');
}
こんな感じで書けば、変数wにはパネルかウィンドウ、適切なものが入る。その後、wにadd()で他のパーツを追加していけばいい。
ただこの書き方、内容の割に行数も取って、あんまり美しくない。
ここで「三項演算子」を使うとよい。三項演算子を使うと先程のコードは次のように書ける。
var w = (this instanceof Panel)? this : new Window('dialog');
分からなかったらググって自分で調べる。今回やってることは難しくなくて、
( A )? B : C;
これは「AがtrueだったらB、falseだったらC」という意味。わからなければ最初のif文でも構わない。
もし「パネル起動しか許さぬ!」という場合は、
if(!(this instanceof Panel)){ alert("パネルじゃないときの処理!!"); }
みたいな風にして判断すればいい。this instanceof Panel全体がtrue/falseになるので!をつけて反転させている。その際、this instanceof Panel全体をカッコでくくっているのも見落とさない。カッコを付けないと
!this instanceof Panel
となり、!がthisにしか適用されないからだ。
これが一番現実的な書き方
というわけで、これが一番正解に近い書き方かしら。ウィンドウかパネルか関係なく、噛ませのグループを入れちゃう。
//これが最も限りなく正解に近い?
var w = (this instanceof Panel)? this : new Window('dialog');
var res = 'group{alignment : ["fill","fill"],\
et : EditText{ characters : 20 }\
gr : Group{\
ok : Button{ text : "OK" }\
cancel : Button{ text : "cancel" }\
}\
}';
w.gr = w.add( res );
w.layout.layout();
if( w instanceof Window ) w.show();
今回はadd()の引数にする文字列を一度変数res(リソース=resourceの略)で作成している。add()内で直接書いてもいいが、見やすさの問題なのでご自由に。
最後の行でif( w instanceof Window ) w.show();としているのは、ウィンドウの時はshow()で表示しないといけないが、パネルの時はshow()してはいけないから。w instanceof Windowでwの中身がウィンドウかどうかを判断している。
条件2のまとめ
ドッキングパネルに対応させる時は、
thisがPanelオブジェクトであることを利用する
layout()メソッドを実行するのを忘れない!!
show()してはいけない!!
グループを1つ噛ませることで、add()が一回で済む!!
ハイブリッドに対応させる!!
thisの中身で場合分け!!
ウィンドウの時は逆にshow()が必要!!
といった感じですかね。
パネル起動時の注意事項
自動レイアウト時はリサイズ関数をちゃんと書かないとダメ
パネルが見えた瞬間、パッと出てきたように見えてるけど、既に内部では2回程リサイズが行われている。衝撃よね。正確なタイミングはわからないが、AEさんはパネルサイズを前回終了時のサイズに直して表示しないといけなかったりするんで。その影響もあり、パネル起動時は自動レイアウトを使っていてもUIが適切なサイズで表示されるとは限らない。正確な処理順は知らないが恐らく、layout()で各パーツの位置・サイズが決定された後に、パネルのリサイズが行われているのだろう。そうすると、UIはリサイズ前のパネルサイズに合わせてレイアウトされるわけで、その後パネルサイズだけが変わることで、表示が狂っているように見える。
厄介かと思いきや、解決法は超単純。
onResizeコールバックにlayout.resize()を使えばいい。resize()の使い方は自動レイアウトのページを見てほしい。
要するに、パネルのサイズが変わるとonResizeコールバックが呼ばれるわけなので、resize()を書いておけばその度に自動レイアウトが働いて、表示までに2回リサイズが行われてようが、最終的には正しい見た目になるってわけ。
ウィンドウの時は、リサイズ可能にしたいなら自分でresizeableのプロパティを設定しないといけないが、パネル起動時は常にリサイズ可能状態。止めることは出来ない。でもよくよく考えてみ。ドッキングパネルなんだから、そりゃそうでしょうよ。そうじゃなきゃ困るでしょうよ。
「パネルの時は指定しなくても最初からリサイズ可能状態。onResizeコールバックを必ず設定しておく」
これを忘れない。
おしまい
長くはなったが、理解すれば、なんてこと無い。
書いてる私自身こんなに長くなると思ってなかったくらいに、簡単なことなので。
ESTKで作成時にF5でデバッグすると通常のウィンドウ起動扱いになるので、パネルでの挙動を確認したければ、
保存(これ重要)
パネルが開いた状態なら閉じる
「ウィンドウ」メニューから起動
を繰り返さないといけない。パネルが出たままだと更新されない。ちゃんとパネルを閉じて、「ウィンドウ」メニューから再度実行する。AEを再起動する必要はない。