oneLiner最終ベータ版(多分)公開! ついでに開発裏話

11月ぐらいからこつこつ作っていたInDesign oneLinerですが、ようやく「思いつく限りのシチュエーションでトラブルが発生しない」状態まで来ました(既知の問題はあるけど。バグが見つからなければそのまま公開バージョンとなります。

このスクリプトはInDesignの使い方を変えるものだと思ってますので是非使ってみてください。

紹介・説明・ダウンロードページはこちらになります。忙しくて使えそうもないという人は、せめて冒頭のデモ動画(2分4秒)を見てください。何をするものなのか、おおよそのことがわかります。

もしバグを見つけたら是非連絡をお願いします。できる限り対応します。また感想もお待ちしています。


さて、この下はスクリプター向けに、何をしていて、どこにどんだけ苦労したかを吐き捨てるコーナーです。スクリプターでないとわからないと思いますので、単に使いたいだけの人は読まなくて構いません。


このスクリプトを作るに当たって最大の問題は「スクリプトではクリックしたスプレッド上の座標を取得することができない」ということです。誰もが一度はぶち当たる壁ですね。座標は取得できないんです。これはもう、どうしようもない。多分UXPになってもできない。じゃあ、どうするか。

ポイントは「クリックした座標を知らなくてもよい。クリックした位置にオブジェクトを作成できる方法は何か」を考えることです。答えは1つしかありませんでした。

それは「配置」です。これ以外にユーザーが指定した位置にオブジェクトを作成する方法がありません。この答えを見つけ出すのに何年かかったんだろう。これがこのスクリプトのすべてと言っていいぐらい。

じゃあ、そこにオブジェクトを作成する方法は? これも答えは1つ。スニペットです。そのため、どうしてもスニペットファイルが必要になります。幸いなことに、スニペットファイルの中身はXMLなので、いつでもスクリプトから作成できます。しかもスニペットの中身は直線1つなので極めて小さい。余計な情報を削除するとたったこれだけ!


次の問題はスクリプトの分断です。ユーザーに配置させるには、そこでスクリプトを中断させなければなりません。中断させるのは仕方ないとして、再開するにはどうしたらいいの? ってことです。

ここからイベントリスナとの格闘が始まります。まあ、最初に横(縦)組みパスツールのアイコンをクリックした時点でもイベントを取得しなければならないので、必然なのですが。

まず、ツールが変更されたときのイベントToolBox.AFTER_ATTRIBUTE_CHANGED(長いので、以下「ToolChgイベント」と言います)を捕まえます。この時に横組みパスツールもしくは縦組みパスツールの場合に、PlaceGunにスニペットをロードします。コードにすると

app.documents[0].placeGuns.loadPlaceGun(スニペットファイル);

こうですね。すると、またToolChgイベントが発生します。これはloadPlaceGunメソッドでカーソルが配置カーソルに変更になるんですね。このイベントはToolBox(ツールパネル)と名前がついていますが、実際はカーソル変更を捕らえるもののようです。

そういえばDOM上ではToolBoxオブジェクトに「TABLE_TOOL」というのがあって、これはツールパネルに存在せず、長い間不明だったのですが、表作成時のカーソルでした。このページは修正しておきます。

スニペットを配置すると、またToolChgイベントが発生します。これはカーソルが元のツール(この場合は横(縦)組みパスツール)に戻るために発生するものです。さらに悪いことに、ESCキーを押して配置をキャンセルした場合でも、ToolChgイベントが発生します。

この動作は厄介です。ToolChgイベントが発生して値が横(縦)組みパスツールであるケースは3つあります。

(1)ツールパネル上をクリックした場合 → スニペットを配置するスクリプト

(2)スニペットが配置された場合 → 配置した直線にパス上文字を作成するスクリプト

(3)スニペットの配置がキャンセルされた場合 → スクリプトは実行しない

どうにかしてこの3つを判断しないといけないわけです。というか、それ以前にこんな状況が待っているなんて誰が想像できただろうか。

結局フラグを立てたりして凌ぐんですが、正解は何なんだろうと未だに思います。

無事配置されたら、今度は配置した直線にパス上文字を作成するんですが、これも大変。配置すると、配置されたオブジェクトが選択状態になりますので、そのイベントを捕まえてスクリプトを動かします。このとき使うのがEVENT.AFTER_SELECTION_CHANGED(これも長いので、以下「SelectChgイベント」と言います)です。

実は配置されるとEVENT.AFTER_PLACEが発生するんですが、これが役に立たない。この時点で配置されたオブジェクトが選択された状態になっているんですが、それをselectionプロパティで捕まえることができないんです。

この直後にSelectChgイベントが発生して、そこでようやく選択されたオブジェクトを取得できるみたいです。そのため、SelectChgイベントが発生した時点で、選択されているオブジェクトが配置したものかどうかを判断するという面倒くさいことになってしまっています。配置イベントに比べて選択イベントの方が遥かに多いのに。


テキスト長に応じて直線の長さを変更するところは、SelectChgイベントと選択オブジェクトのプロパティ変更後のイベントEvent.AFTER_SELECTION_ATTRIBUTE_CHANGED(これも長いので、以下「SelectAttrChgイベント」と言います)の両方を使っています。

まずテキストが選択されると直線を少し伸ばします。そうしないと入力中に文字あふれが起きてしまいます。直線が選択されると、テキスト長に合うように直線を縮めます。これだけならSelectChgイベントだけでいいんですが、実はテキスト長が変更するケースは文字入力だけではありません。文字サイズが変更されることでもテキスト長が変更します。そのためSelectAttrChgイベントも捕まえる必要があります。

ただ、これがまた厄介なことに、1つのプロパティを変更しただけなのに複数回イベントが発生します。どうも「イベントの伝達」が関係しているものと考えています。ですから、文字長を記録しておいて、変更がなければスルーするようにしておかないと大変なことになります。


ということで、個々の直線に対して変更される前のテキスト長をどこかに記録しておかなければなりません。そういうときの常とう手段として、labelプロパティがあります。ここには好きな文字列を入れておけるので、うってつけです。ただし、直線のオブジェクトのlabelプロパティに入れると、スクリプトラベルパネルで書き換えられてしまう可能性があるので、ここはダメです。じゃあ、どこに記録しておくか。いい場所を見つけました。TextPathオブジェクトです。ここにもlabelプロパティがあるんですが、TextPathオブジェクトは選択することができません。つまりスクリプトラベルパネルに表示されることがないんですね。ここに組方向、テキスト長、行揃えの情報を入れています。

実は2月までのバージョンではStoryオブジェクトのlabelプロパティに入れていました。ところが、作成された直線を複製すると、Storyオブジェクトは新しく作成されるようで、せっかく記録したlabelプロパティが引き継がれないという問題が生じていることがわかりました。Storyオブジェクトは要注意です。連結したテキストフレームの一部を削除した際も参照が外れるという現象にも見舞われました。


さて、TextPathオブジェクトは選択することができないわけですが、それが逆に非常に困ったことになりました。

TextPathオブジェクトには、パス上文字オプションで設定されるプロパティや始点ブラケット・終点ブラケットの移動というプロパティの変更が発生します。実は、始点・終点ブラケットの手動で変更したりパスの反転を行ったりするとロジックが破綻するので、これを禁止したいんです。

InDesignにはMutationEvent.AFTER_ATTRIBUTE_CHANGEDというのがあって、選択しているかどうかに関わらずプロパティの変更を捕まえるイベントなんですが、これが全く反応しないんです。Tenさんの検証記事を後追いしただけの徒労に終わりました。

考えあぐねた結果IdleTaskを使うことにしました。これは、ユーザーが操作していない間、一定時間間隔で実行するタスクです。ここにTextPathオブジェクトに変更がないか確認するプロセスを入れました。ただ問題は監視する間隔です。あまり短いと重くなる気がするし、長いと操作後にエラーメッセージが出るまでタイムラグがあるし。

最初は200ミリ秒(0.2秒)にしていました。しかし、予想外の事態が起きました。ドキュメントを閉じたタイミングでチェックプロセスが実行されると「開かれているドキュメントウィンドウがありません」というエラーが出るのです。チェックする前にドキュメントウィンドウが存在しているのを確認しているんですがねえ。

ドキュメントの存在チェックからTextPathオブジェクトの変更チェックまで6行しかないんです(選択オブジェクトがあるか、それがスクリプトで作成されたものか確認している。そのわずかの間にドキュメントウィンドウが消えてしまっているんでしょうねえ。

仕方がないので監視する間隔を250ミリ秒(0.25秒)にしてみたら、少しは緩和された気がします。ここは最後の修正で、スクリプトで作成された直線が選択されている状態では100ミリ秒(0.1秒、そうではない状態では1000ミリ秒(1秒)に、監視する間隔を変更しています。おそらくこれでエラーが出ることはまずないだろうと踏んでいるんですが。

もし出会ったら逆に幸運かもしれない、ぐらいだといいなあ。


話は変わって、直線の長さの変更プロセスも非常に難題でした。最初はアンカーポイントを動かせばいいやと思っていたんです。垂直線・水平線ならそれで問題ありません。しかし、私はこの機能をオブジェクトが回転した状態でも実行できないと意味がないと考えていたので、回転した状態でアンカーポイントを移動すると、想定していなかった事態が。

図のとおり、直線を回転した状態でアンカーポイントを動かすと、シェイプという属性が失われてパスになってしまう、という問題が。これでは非常に操作しにくい。なかなか単純には行きませんなあ。ということで、アンカーポイントの移動は断念。何とかシェイプの形状を保持したまま長さを変更できないかなー。

余談ですがIllustratorとInDesignはシェイプとパスの見た目や挙動の違いをもっとユーザーに意識させるべきなんじゃないか、と思います。現状は、できるだけ違いを意識させない方向で作られていて、それは初心者にはよいかもしれないけれど、きちんと理解しようとすると、理論的に説明できなくなっていて、それが逆に壁になっているんじゃないかなあと思います。その点Affinity Suiteは最初から違いが明確になっているので理解しやすいです。Photoshopも違いが明確ですね。

結局、transformメソッドを使って拡大/縮小しています。ただ、そのままtransformを使うと直線だけでなく、中のテキストまで拡大/縮小されてしまうという問題が。

そのため、transformメソッドの前にテキストを避難させています。その方法はわかってしまうと簡単なんだけど、思いつくまで非常に長い道のりで企業秘密にしたいぐらいなんだな。でもまあ、ばらしてしまおう。

非表示のテキストフレームを作成して、それとテキストパスを連結してます。そうすると直線上にあったテキストがテキストフレームに移動するので、その状態で変形し、その後にテキストフレームを削除して、テキストをテキストパスに戻すというものです。このときにStoryオブジェクトの参照が外れてしまうというわけです。

ただ、このせいで、文字サイズを極端に大きくすると画面が移動したり、少し離れた位置にキャレットが見えたりするという副作用が。

もっといい方法を思いつかない限り、これは仕方がないですね。


このスクリプトは一応売るつもりで作ってますから、ソースコードは秘匿したい(でもこれだけ仕組みをばらしてしまったらもはや意味がない?

ということでバイナリ形式(jsxbin)で配布したかったわけです。そこで、出来上がったスクリプトを配布用にバイナリ書き出ししたら全く動きません。

どうもバイナリ書き出しに制限かバグがあるようで、addEventListenerがあるとjsxbinでは動かないようです。そこで、なんか方法はないかなということで検索するとAdobeコミュニティにありましたよ。こんなことができるんですね。

eval("@JSXBIN@ES@2.0@My..."); //改行をトル

ありがとう、Marc、そしてAdobeコミュニティ。これでaddEventListenerを除く部分は秘匿化できる……。


ということで、渾身のスクリプトのできあがりです。これで約600行。普段1~150行程度のスクリプトしか書かないので、自分にしてはかなりの大物です。でもこの内容をたった600行で書けるInDesignはやっぱりすばらしい。