右クリックメニューを作る

コンテキストメニューぽいものを作る

凝ってくるとコンテキストメニュー、いわゆる「右クリックメニュー」を作りたくなることもある。それ専用のオブジェクトは用意されていないが、組み合わせることによって、それっぽいものは作れる。

スクリプト Swatch のページで実際に作ったサンプル画像があるので合わせて確認してみて見てほしい。

ブラウザで右クリックした時の例。こんな感じのが作りたい。

何はともあれ、ウィンドウ

ウィンドウは必要であろう。何をするにもまずウィンドウオブジェクトがないと話にならない。なので、new Window() で作成したい。実際書き始めると、いろいろ迷いどころがあるはず。

まずウィンドウのタイプ、dialogかpaletteかwindowか。windowはまぁないとして、dialogかpaletteか。paletteかなと思うのが普通だろうか。正解。ただ初期段階ではEscでウィンドウを閉じれると楽なので一旦dialogで進めて、後でpaletteに書き換える。

次に、普通に作るとタイトルバーが付く。これじゃ右クリックメニューぽくない。なので作成時に properties の borderless を false にしておく。閉じるボタンが消えてしまうので、やはり最初はpaletteで進めるのはおすすめしない(最終的にはpalette)。

var w = new Window('dialog{ properties : {borderless : true} }');

w.show();

図を見ればわかる。よさげ。ウィンドウの消え方は何かしら考えて実装しないといけない。そこの仕様は後で考えるとして先に進む。

タイトルバーが邪魔
borderless = trueの状態よさげ

メニュー項目はリストボックス

次に、メニュー項目を表示したい。いろいろ実装方法はあると思うが、今回はリストボックスにしようと思う。理由は「メニューリストの表示や追加・変更が楽そうだから」。思いつくなら他の方法でもいい。

では、リストボックスを足してみる。ボタンなど、他の要素は追加するつもりはないので、alignmentはfillで。

var w = new Window('dialog{ properties : {borderless : true},\

lb : ListBox{ alignment : ["fill","fill"], properties:{ items : ["menu1","menu2","menu3"]} }\

}');

w.show();

alignmentを ["fill","fill"] にして、ついでに適当なリストアイテムも追加しておいた。(図4)

リストボックスの周りの空白が余計。これはウィンドウのmarginsのせい。0にする。

あと、ウィンドウ幅が小さい気もする。メニュー項目名が短いから、自動レイアウトによってリストボックスもそのサイズに合わさって、さらにウィンドウもそのリストボックスに合わせたサイズになっている。「別にこれでいい!」という人はこれでいい。「ある程度大きさは確保したい!」ていう人はリストボックスに preferredSize を設定してみるとよい。ただし、高さはメニュー項目数に合わせて自動的に決まったほうが美しいので、-1としている。

var w = new Window('dialog{ margins : 0 , properties : {borderless : true},\

lb : ListBox{ alignment : ["fill","fill"] , preferredSize : [200,-1] , properties : { items : ["menu1","menu2","menu3"] } }\

}');

w.show();

実は、ウィンドウ自体はこれでほぼ完成。

この例は最初からメニュー名(menu1,menu2,...)が決め打ちになっているが、表示前にリストメニューのアイテムを追加するようにすれば、クリクした場所、物に合わせて動的にメニュー項目名を変えたりできる。とあるフォルダにあるAEPファイル名のリストにしたり。そういうのは自分でやってもらうとして。

次はコールバック関数で挙動を設定していく。

図4. ほしいのとちょっと違う。
図5. marginsがなくなっていい感じ。幅も確保。

メニューをクリックしたら任意のコマンドを実行できるようにする

リストボックスをクリックしたら、何かしらのコマンドが実行されるようにする。

リストアイテム1つ1つに設定は出来ない。なのでリストボックスのonChange関数を使う。で、その関数内で選択したアイテムによって振り分けをする。

var w = new Window('dialog{ margins : 0 , spacing : 2 , properties : {borderless : true},\

lb : ListBox{ alignment : ["fill","fill"] , preferredSize : [200,-1] , properties : { items : ["menu1","menu2","menu3"] } }\

}');


w.lb.onChange = function(){

if( !this.selection ) return; //選択がなかったら何もしない

w.close(); //実際のコマンド実行する前に閉じとく。

//好きな関数を実行

switch( this.selection.index ){

case 0: alert("I am menu1!!"); break;

case 1: alert("I am menu2!!"); break;

case 2: alert("I am menu3!!"); break;

default : alert("コマンドが定義されてません");

}

}


w.show();

これで、リストアイテムをクリックしたらアラートが出るようになった。ウィンドウを閉じるコマンドを書いておくのも忘れずに。

ウィンドウが消えるタイミング

先程、メニュー実行前にウィンドウを閉じるようにしたが、ウィンドウを閉じたいタイミングは他にもある。

普段、右クリックメニュー出した後、キャンセルしたい時どうしてるか思い出して。・・・思い出した?

殆どの人が「メニュー以外の場所をクリック」を想像したろう。次点で「Escキーを押す」が来るか。

「メニュー以外の場所をクリック」というのは、要は「ウィンドウのアクティブが外れたら消える」ということであろう。だったら、アクティブが外れた時に実行される onDeactivate が使えそう。

「Escキーを押す」はそのままの意味。これはウィンドウが dialog であれば最初から出来る状態なので dialog のほうがいい気もするが、そうすると逆に「メニュー以外の場所をクリック」つまり「ウィンドウのアクティブが外れたら消える」というのが実装しにくい。「アクティブが外れたら閉じたい」のに、dialog は「閉じるまではアクティブのまま」だから。なのでここでようやく dialog から palette に書き換える。

キーイベントは onClick などのようには用意されていない(多分)のでイベントリスナーを使う。

      • addEventListener( イベントの種類 , 実行する関数 )

で定義する。今回は「キーを押したら」なのでイベントの種類は "keydown" になる。

var w = new Window('palette{ margins : 0 , spacing : 2 , properties : {borderless : true},\

lb : ListBox{ alignment : ["fill","fill"] , preferredSize : [200,-1] , properties : { items : ["menu1","menu2","menu3"] } }\

}');


w.lb.onChange = function(){

if( !this.selection ) return; //選択がなかったら何もしない

w.close(); //実際のコマンド実行する前に閉じとく。

//好きな関数を実行

switch( this.selection.index ){

case 0: alert("I am menu1!!"); break;

case 1: alert("I am menu2!!"); break;

case 2: alert("I am menu3!!"); break;

default : alert("コマンドが定義されてません");

}

}


w.onDeactivate = function(){

//this.close(); //開発中には邪魔なので今はコメントアウトしておく

$.writeln("onDeactivate"); //代わりにコンソールへの表示をする

}


w.addEventListener ("keydown", function(e){

if( e.keyName == "Escape" ) this.close();

});


w.show();

onDeactivate は発動させるためにウィンドウのアクティブを外す必要がある。もし、クリックせずとも「マウスカーソルが外れたらその時点で消える」としたい場合はイベントリスナーに書き換えてやればいい。イベントの種類は "mouseout"。その場合は以下のようになる。

w.addEventListener ("mouseout", function(e){

this.close();

});

表示させるタイミング

これまでで、ウィンドウの見た目、実行時とキャンセル時の挙動が実装できた。あとはこのウィンドウをどのタイミングで表示させるのか。

右クリックの想定で進めてきたが、別に、ボタン左クリック時でもいいし、なんでもいい。作りたいように作れば。

ここでは当初の目的である「右クリック時に表示」のまま進める。

で、早速、右クリックの判定方法が問題になる。どうやって右クリックされたと知るのか。onClick は左クリックしか反応しない。

ここで出てくるのがまたしてもイベントリスナー。イベントリスナーで実行する関数にはマウスの状態が格納されている MouseEventObject が渡されるのでそれの中身を見れば、どのボタンがクリックされたのかがわかる。左、右、中ボタンが判定できる。

テスト用に以下のコードを実行してみる。

var main = new Window('dialog{ text : "右クリックメニューのテスト" , preferredSize : [400,400] , \

gr : Panel{ text : "この中を右クリック" , preferredSize : [200,200] , alignment : ["center","center"] }\

}');


main.addEventListener ('click' , function(e){

if( e.button== 0 ) alert("左クリック");

if( e.button== 1 ) alert("中クリック");

if( e.button== 2 ) alert("右クリック");

});


main.show();

ここでちょっと問題がある。よく上記のコードを見ればわかる。イベントリスナーを当てているのは、ウィンドウに対してである。にも関わらず、パネルの中しかクリックの反応がない。これ、バグと言っていいと思うが、バージョンごとに挙動が違う。CS6だと左クリックしか反応しないという絶望を味わう。実際CS6ではこれのせいで右クリックメニューは実装できない(多分。やり方わからん。)。詳しくは別ページにまとめる。

ともあれ、上記のアラートの部分を書き換えて、先程までで作ったウィンドウを表示すればいい。さっきまでの右クリックメニュー作成から表示までの一連を showContextMenu として関数化して次のようにした。

var main = new Window('palette{ text : "右クリックメニューのテスト" , preferredSize : [400,400] , \

gr : Panel{ text : "この中を右クリック" , preferredSize : [200,200] , alignment : ["center","center"] }\

}');


main.addEventListener ('click' , function(e){

if( e.button == 0 && parseFloat (app.version) == 11 ) showContextMenu (); //テスト用の措置としてCS6の時は左クリックに当てておく。

if( e.button == 2 ) showContextMenu ();

});


function showContextMenu(){

var w = new Window('palette{ text : "aaa" , margins : 0 , spacing : 2 , properties : {borderless : true},\

lb : ListBox{ alignment : ["fill","fill"] , preferredSize : [200,-1] , properties : { items : ["menu1","menu2","menu3"] } }\

}');


w.lb.onChange = function(){

if( !this.selection ) return; //選択がなかったら何もしない

//好きな関数を実行

switch( this.selection.index ){

case 0: alert("I am menu1!!"); break;

case 1: alert("I am menu2!!"); break;

case 2: alert("I am menu3!!"); break;

default : alert("コマンドが定義されてません");

}

this.window.close(); //実際のコマンド実行する前に閉じとく。

}


w.onDeactivate = function(){

this.close(); //コメントアウトを外して実装した

//$.writeln("onDeactivate"); //コメントアウト

}


w.addEventListener ("keydown", function(e){

if( e.keyName == "Escape" ) this.close();

});


w.show();

}


main.show();

これでパネルの中を右クリック(CS6ならパネルの外を左クリック)すればメニューが出るはず。

実行した人は気づくであろうが、実はまだ足りないものがある。

カーソルの位置に表示する

どこでクリックしてもウィンドウが画面の真ん中に表示されている。右クリックメニューはカーソルの近くに表示されるものであろう!最後にウィンドウの表示位置を調整して完成である。ガンバろっ!

先程、MouseEventObject を使用してクリックされたボタンの種類を判定した。MouseEventObject には他にも多くの情報が格納されている。(右図)

この中でカーソルの座標に関するものは次の4つ。

      • clientX:イベント発生時のカーソル座標X。クリックしたターゲットを基準にしたローカル座標。

      • clientY:イベント発生時のカーソル座標Y。クリックしたターゲットを基準にしたローカル座標。

      • screenX:イベント発生時のカーソル座標X。スクリーン上の絶対座標。

      • screenY:イベント発生時のカーソル座標Y。スクリーン上の絶対座標。

XYの組み合わせなので、実質2つ。相対座標か絶対座標。どちらを使うか。ここは絶対座標だろう。

screenX,Yを使って、右クリックメニューを表示する時に位置を調整しようと思う。表示する時に実行されるonShow 内で書けばいい。showContextMenu関数が位置情報を受け取れるように、引数posを設定し、onShow内で利用。イベントリスナーの中で、showContextMenu を呼び出す際にscreenX,Yの値を与えるのも忘れずに。今回は [e.screenX , e.screenY] として配列にして渡す。

var main = new Window('palette{ text : "右クリックメニューのテスト" , preferredSize : [400,400] , \

gr : Panel{ text : "この中を右クリック" , preferredSize : [200,200] , alignment : ["center","center"] }\

}');


main.addEventListener ('click' , function(e){

if( e.button == 0 && parseFloat (app.version) == 11 ) showContextMenu ([e.screenX,e.screenY]); //テスト用の措置としてCS6の時は左クリックに当てておく。

if( e.button == 2 ) showContextMenu ([e.screenX,e.screenY]);

});


function showContextMenu( pos ){

var w = new Window('palette{ text : "aaa" , margins : 0 , spacing : 2 , properties : {borderless : true},\

lb : ListBox{ alignment : ["fill","fill"] , preferredSize : [200,-1] , properties : { items : ["menu1","menu2","menu3"] } }\

}');


w.lb.onChange = function(){

if( !this.selection ) return; //選択がなかったら何もしない

//好きな関数を実行

switch( this.selection.index ){

case 0: alert("I am menu1!!"); break;

case 1: alert("I am menu2!!"); break;

case 2: alert("I am menu3!!"); break;

default : alert("コマンドが定義されてません");

}

this.window.close(); //実際のコマンド実行する前に閉じとく。

}


w.onDeactivate = function(){

this.close(); //コメントアウトを外して実装した

//$.writeln("onDeactivate"); //コメントアウト

}


w.addEventListener ("keydown", function(e){

if( e.keyName == "Escape" ) this.close();

});


w.onShow = function(){

this.location = pos;

}


w.show();

}


main.show();

これでカーソルの位置に右クリックメニューが表示されるようになった。やったぁ~~~1!!!

もし「ギリギリじゃなくてもう少しカーソルに少し被った状態で表示したい」というのであれば、与える座標値を微調整してやればいい。

ちなみにCS6だとscreenX,screenYがバグってて[0,0]になってしまうので、画面の左上に表示されてしまう。なのでCS6は実装不可(だと思う。他のやり方知らん)。

MouseEventObjectの中

アイコンも表示したい、サブメニューが欲しい、欲張りなあなたへ

もしメニュー名だけでなく、アイコンも表示したいというのであれば、右図のようにできる。

メニューのリストは ListBox なのでアイテムに画像を設定することが可能。リストアイテムの image プロパティに設定してやればいい。詳しくは、ListBox のページを参照されたし。

さらにサブメニューが欲しいなら、メニューをクリックした時に例では alert していた部分を「子メニュー用のウィンドウを表示」する関数に書き換えれば出来そう。今度は絶対座標ではなく相対座標、clientX,Y の値を利用したほうがよさそうね。そこまでやったことないけど。

アイコン付きメニューの例

完成

以上で右クリックメニュー(コンテキストメニュー)を作成することが出来る。おめでとう!