TAWAMIラボ

2023/09/13 Illustrator Script

いまさらイラレスクリプトのパレットの話

パレットというのは、ExtendScriptのScriptUIで生成できる3種類のウィンドウ(dialog, palette, window)のうちのpaletteのことで、
・常にIllustratorのウィンドウより手前に表示される。
・Illustratorをモーダル(入力待ち)状態にはしない。
という特徴があります。
簡単に言えば、イラレで作業中、表示しっぱなしにできるウィンドウです。


Illustratorでパレットを使ったスクリプトを初めて作るとき、つまずきポイントが2つあります。まず1つ目は、

つまずきポイント① スクリプトの実行方法によっては、生成したパレットが一瞬で消えてしまう

この現象を理解するには、スクリプトの実行エンジンについて知る必要があります。
ここで言うエンジンとは、スクリプトを走らせるための環境みたいなものです。名前空間とかメモリ領域とかが含まれます。
「GoogleがJavaScriptエンジン『V8』を開発したよ」とか言うときのエンジンとはちょっとニュアンスが違います。
このエンジンについて理解していただくために作ったのが次の図。

エンジンには、イラレ起動時から終了まで存在するmain、スクリプト実行時に生成されて終了時に破棄されるtransient、実行時に生成されてイラレ終了まで残る「自分で名前をつけたエンジン」の3種類があり、プリプロセッサディレクティブの#targetengineで指定できるが、#targetengineを省略した場合は実行方法により様々である…というようなことを図示しています。

で、お察しの通り、つまずきポイント①の原因はtransientエンジンです。

dialogを使ったスクリプトでは、Windowオブジェクトをshow()した後、ダイアログが表示され、ダイアログを閉じるとスクリプトのshow()の続きの部分が実行される、という流れになります。
一方paletteを使ったスクリプトでは、Windowオブジェクトをshow()するとパレットが表示され、それはそれとしてshow()の続きの部分が実行され、スクリプトはいったん終了します。
transientエンジンは終了時に破棄されるため、エンジン上に作られたパレットもまた消えてしまうわけです。

解決方法はmainエンジンまたは「自分で名前をつけたエンジン」を使うこと。


2つ目はこれ

つまずきポイント② パレットにボタンをつけて何らかの処理をさせようとしたとき、activeDocumentやselectionを含むようなコードを書くと、ボタンが反応しなくなってしまう

パレットにボタンをつけてなんらかの機能を持たせるには、ButtonオブジェクトにonClickイベントハンドラ(関数)を追加します。単純にアラートを出すだけならこんな感じ。

#targetengine hoge var w1=new Window("palette"); var b1=w1.add("button",undefined,"ボタン1"); b1.onClick=function(){ alert("ボタン1が押されました"); } w1.show();

これはまともに動きます。
次はonClickの中でIllustratorのドキュメント上で選択されているアイテムの数を取得してみます。

#targetengine hoge var w1=new Window("palette"); var b1=w1.add("button",undefined,"ボタン1"); b1.onClick=function(){ alert(app.activeDocument.selection.length+"個のアイテムが選択されています"); } w1.show();

こうすると、ボタンを押しても何も反応しなくなってしまいます。

これは、
「onClick(を含むパレットのイベントハンドラ)の中でactiveDocumentにアクセスするとエラーが出る」というバグだか何だかわからない現象と、「イベントハンドラの中でエラーが出た場合、エラーメッセージも何も出ずにその関数が終了する」という仕様によるものです。(try-catchを使ってエラーを取得し表示させることはできます)

このバグだか何だかわからない現象については、本当になんでこうなってるのかさっぱりわからないわけですが、昔から知られていて対処方法も確立されています。それはBridgeTalkを使うこと。

BridgeTalkは、Adobeのアプリケーション間(たとえばIllustratorとPhotoshop)でスクリプトを送り合って連携するためのしくみなのですが、Illustratorから自分自身(Illustrator)に送ることもできます。
これを使ってさきほどのコードを修正すると、こう。

#targetengine hoge var w1=new Window("palette"); var b1=w1.add("button",undefined,"ボタン1"); b1.onClick=function(){ var bt=new BridgeTalk; bt.target = "illustrator"; bt.body = "alert(app.activeDocument.selection.length+'個のアイテムが選択されています');" bt.send(); } w1.show();


これで2つのつまずきポイントが解決できましたが、もう1点問題があって、上のスクリプトはIllustratorが複数バージョンインストールされている環境で動かない場合があります。原因はBridgeTalkのtargetを単に"illustrator"としているから。本来はここに「今現在起動してスクリプトを実行しているこのIllustrator」を指す文字列を入れなくてはいけません。それはBridgeTalkオブジェクトのappSpecifierプロパティで取得できます。

あともう1点、問題点というほどではないけど、「BridgeTalkのbodyに実行するコードを全部文字列で入れるのめんどくさいしメンテナンス性が悪いよね」問題があります。

これらを踏まえて、私のおすすめの書き方のコード例を最後に書いておきます。
要点は、
・ターゲットエンジンはmain(BridgeTalkが実行されるのと同じエンジンを使う)
・グローバル変数で1つだけ、ユニークな名前でオブジェクト作る
・そのオブジェクトの中に関数とかパラメータを埋め込む
・メインの処理は無名即時実行関数で包む
・BridgeTalkのbodyは"オブジェクト名.関数名();"だけ
・BridgeTalkのtargetは"illustrator"じゃなくBridgeTalk.appSpecifierを使う

#targetengine main var nankaKakkoiiNamae={}; (function (){ var w1=new Window("palette"); var b1=w1.add("button",undefined,"ボタン1"); b1.onClick=function(){ var bt=new BridgeTalk; bt.target = BridgeTalk.appSpecifier; bt.body = "nankaKakkoiiNamae.func1();", bt.send(); } var cb1=w1.add("checkbox",undefined, "チェック項目1"); cb1.value=nankaKakkoiiNamae.check1=true; cb1.onClick=function(){ nankaKakkoiiNamae.check1=cb1.value; } nankaKakkoiiNamae.func1=function(){ alert(app.activeDocument.selection.length+"個のアイテムが選択されています"); alert("チェック項目1は"+(nankaKakkoiiNamae.check1?"ON":"OFF")+"です"); } w1.show(); })();


こぼれ話(追記)

つまずきポイント①についての解説で、mainエンジンまたは「自分で名前をつけたエンジン」を使えばパレットが一瞬で消えることはなくなると書きました。
しかし、

#targetengine main (function (){ var w1=new Window("palette"); var b1=w1.add("button",undefined,"ボタン1"); w1.show(); })();

これだと一瞬で消えてしまいます。
これはJavaScriptのガベージコレクション(どこからも参照されなくなったメモリ領域を自動的に解放する仕組み)が働いているためと考えられます。
無名関数が終了した時点で、その中のローカル変数はどこからも参照されなくなるため、パレットもろとも消えてしまうわけです。

これを防ぐには、

#targetengine main var myPalette=(function (){ var w1=new Window("palette"); var b1=w1.add("button",undefined,"ボタン1"); w1.show(); return w1; })();

のようにして、作ったWindowオブジェクトへの参照が残るようにすればいいです。

あれ?でも…

#targetengine main (function (){ var w1=new Window("palette"); var b1=w1.add("button",undefined,"ボタン1"); b1.onClick=function(){ alert("ボタン1が押されました"); } w1.show(); })();

これだとWindowオブジェクトへの参照が残らないはずなのに、パレットが消えないですね。

この現象は「関数内関数を作ったとき、ガベージコレクションが正しく働かない(メモリリークが起こる)バグ」ではないかという説があります。
気にしてもしょうがないですけどね。


コメント(1)

  1. Name:Sergey Date:2023/11/01 23:15[返信]

    Hello. Informative article. Have you encountered the following problem with Palette for Mac OS users: when the script is launched by by drag-and-drop from the folder to the Adobe Illustrator window, the Palette appears in the background and does not stay in the foreground. If you insert alert() when running the script, then when you drag the Adobe script, Illustrator is focused and Palette does not go to the background.

コメントを残す

メールアドレスが公開されることはありません。