Macのキーボード入力、マウスクリックをJavaScriptで (JXA)

JXAを知らない方はとりあえず以下の記事をどうぞ。

- [JavaScript for Automation Release Notes](https://developer.apple.com/library/mac/releasenotes/InterapplicationCommunication/RN-JavaScriptForAutomation/) - 公式のドキュメント
- [鳶嶋工房 / AppleScript / JavaScript for Automation (JXA)](http://tonbi.jp/AppleScript/JavaScript/)
- [dtinth/JXA-Cookbook](https://github.com/dtinth/JXA-Cookbook)
- [知らないうちにMacがシステム標準でJavaScriptで操作できるようになってた (JXA) - Qiita](http://qiita.com/zakuroishikuro/items/1b02378bf9e940602d87)

ここではキーボードやマウスの操作をエミュレートする方法を書きます。
各アプリに用意された機能はスクリプトエディタから見られる「用語説明」等に任せます。

ちょっとしたコードを試すには`osascript`コマンドのインタラクティブモードが便利です。

```
$ osascript -l JavaScript -i
```
で起動できます。

# System Events

`System Events`はスクリプト専用の、GUIを持たないアプリケーションです。

```js:SystemEventsを取得
var se = Application("System Events"); //参照を取得
```

これを使ってユーザの操作をエミュレートできます。
以下より、変数`se`には`Application("System Events")`を参照しているものとします。

# キーボードの操作をエミュレート

キーボードを操作するには`.keystroke()`と`.keyCode()`を使います。

## .keystroke()

渡された文字列のキーを打鍵します。

```js:「A」キーを押す
se.keystroke("a");
```

2つ目の引数で、同時押しする特殊キーを指定できます。
複数の場合は配列で渡します。

```js:特殊キーを組み合わせて押す
// command + h (ウィンドウを隠す)
se.keystroke("h", {using:"command down"});

// command + alt + h (他のウィンドウを隠す)
se.keystroke("h", {using:["command down", "option down"]});
```

同時押しに指定できる特殊キーは以下の4種類のみです。

||文字列|
---|---
|⌘|"command down"|
|⌃|"control down"|
|⇧|"shift down"|
|⌥|"option down"|

2文字以上なら順番に打鍵されます。

```js:複数のキーを順番に押す
se.keystroke("abc");
```

> abc

エスケープ文字も可能です。
`\n`でエンターキーが押されますし、`\b`でdeleteキーが押されます。
(キーボードの打鍵のエミュレートなので、日本語は無理です。)

(追記: と、思ったけど`\n`はエンターじゃないっぽいです。ここちょっと怪しいのであんまり信じないでください。)

```js:エスケープ文字を押す
se.keystroke("//one\n//twoooooo\b\b\b\b\b\n");
```

>//one
>//two

2文字以上の文字列を渡した状態で特殊キーを指定すると **すべての文字で** 同時押しされます。

```js:複数のキーを特殊キーと一緒に順番に押す
// 新しいウィンドウを3つ開く
se.keystroke("nnn", {using:"command down"});
```

大文字もそのまま入力できました。
大文字はshift + 小文字なので、"shift down"を書く必要はなくなります。
command + shift + nというショートカットキーを押したい場合、`.keystroke("N", {using:"command down"})`だけでいけます。

## .keyCode()

`.keystroke`で押せないキーの場合は`.keyCode`を使います。
`.keystroke`と同様に、特殊キーの同時押しも可能です。

(追記: MacbookのJISキーボードでしか試していませんが、キーボードの配列によっても異なるっぽいのでここに書いてある通りにはならないかもしれません。)

各キーを表す数値を入力して使います。
たとえば、「英数」キーは102、「かな」キーは104です。

```js:キーコードを指定して押す
// 「かな」キーを押す
se.keyCode(104);

// 「英数」キーを押す
se.keyCode(102);
```

`.keyCode`も複数の入力が可能です。
キーコードの配列を渡します。

```js:複数のキーコードを順番に押す
se.keyCode(104); //「かな」キーを押す

se.keyCode([40, 31, 45, 45, 45, 34, 8, 4, 34, 13, 0]);
```

「こんにちわ」と入力されましたか？

### キーコードの確認方法

だいたいこのページに書いてあります。

- [Hys.LOG : Mac のキーコード 2012 (Full Keyboard)](http://hyslog.com/blog/2012/06/25/569)

または以下のコマンドで、バーチャルキーボードのキーコードを全部見れます。たぶん全部。

```
grep 'kVK_.*=' /System/Library/Frameworks/Carbon.framework/Frameworks/HIToolbox.framework/Headers/Events.h
```

キーボードを押してキーコードを個別に確認したい場合、Karabiner (元KeyRemap4MacBook) のEvent Viewerが便利です。

- [Karabiner - OS X用のソフトウェア#キーリマップの結果を確認する方法](https://pqrs.org/osx/karabiner/document.html#eventviewer)

Event Viewerは、押したキーのキーコードを16進数で表示してくれます。
JavaScriptは普通に16進数の数値も扱えるので、もちろんそのまま入力できます。

```js:16進数
// true
102 == 0x66;

// 「英数」キーを押す
se.keyCode(0x66);
```

## 出来ないこと

- 押したままにすること (押すことしか無理)
- 特殊キー以外のキーを同時押しすること

(する方法があれば教えてください。)

あと、`.keystroke`ではタブ、スペースをそのまま入力できないようです。
(渡す文字列にスペースやタブが含まれていると、なぜか日本語入力がONになります)
(追記: タブは"\t"で入力できました。コメントありがとうございます！)

`.keycode`で代用する場合はスペースキーは`0x31`、タブキーは`0x30`です。

文字列をキーコードの配列にする関数を作ると面白いかもですね。

```js:文字列をキーコードの配列に(適当に)変換する関数
CODES = {"a":0,"b":11,"c":8,"d":2,"e":14,"f":3,"g":5,"h":4,"i":34,"j":38,"k":40,"l":37,"m":46,"n":45,"o":31,"p":35,"q":12,"r":15,"s":1,"t":17,"u":32,"v":9,"w":13,"x":7,"y":16,"z":6,"0":29,"1":18,"2":19,"3":20,"4":21,"5":23,"6":22,"7":26,"8":28,"9":25,"-":27,"+":24,"[":33,"]":30,"=":42,";":41,"‘":39,",":43,".":47,"/":44," ":49,"\t":48,"\n":52};

function codes(str){
  var codes = [];
  for (var i = 0, code, char; char = str[i++];) {
    code = CODES[char.toLowerCase()];
    if (code != undefined) codes.push(code);
  };
  return codes;
};
```

```js:文字列から変換したものを試しに使ってみる
Application("System Events").keyCode([34, 49, 17, 4, 31, 32, 5, 4, 17, 49, 13, 4, 0, 17, 49, 34, 2, 49, 2, 31, 49, 13, 0, 1, 43, 49, 34, 2, 49, 35, 15, 14, 17, 14, 45, 2, 49, 34, 49, 13, 0, 1, 49, 31, 45, 14, 49, 31, 3, 49, 17, 4, 31, 1, 14, 49, 2, 14, 0, 3, 27, 46, 32, 17, 14, 1, 47]);
```

# マウスクリックをエミュレート

マウスのクリックにも`System Events`を使います。

こちらはキーボードのエミュレートとは少し勝手が違います。
「あのウィンドウのあのボタンを押したい」
というように、Macの画面内の部品(以下GUI)を指定する必要があります。

この機能を使うためには、GUIにアクセスする許可を得なければいけません。
`環境設定 > セキュリティとプライバシー > プライバシー > アクセシビリティ`
を開き、スクリプトエディタかターミナル、使っているものに許可を与えてください。

![スクリーンショット 2015-02-10 17.09.16.png](https://qiita-image-store.s3.amazonaws.com/0/18902/e8e5e39d-d844-5b69-38aa-7ccc2c239e3f.png "スクリーンショット 2015-02-10 17.09.16.png")

できましたか？
`System Events`の`uiElementsEnabled()`で確認できます。

```js:GUIにアクセス可能か確認
ui = Application("System Events").uiElementsEnabled();
// Application("AppleScript Utility").guiScriptingEnabled(); も同じ。

console.log(ui ? "せっていできてるよ。" : "せっていできてないよ。");
```

ちなみにシステム環境設定もJXAで操作できるので、以下のスクリプトを実行すれば上の画像にある設定画面が開きます。
(指定した画面を表示できるだけ。スクリプトで設定変更はできない。)

```js
var pref = Application("System Preferences");
pref.panes.byId("com.apple.preference.security").anchors["Privacy_Accessibility"].reveal();
pref.activate();
```

## とりあえずクリックしてみる

Appleメニューの「このMacについて」を押してみます。

```js:「このMacについて」をクリック
var finder = Application("System Events").processes["Finder"];

var e = finder.menuBars[0].menuBarItems["Apple"].menus["Apple"].menuItems["この Mac について"];

e.click(); // 要素を選んで.click()を呼ぶだけ
```

ちゃんと「このMacについて」が表示されましたか？
あ、配列を返すメソッドは`[]`に文字列を渡すとnameを指定して要素を取得できます。
(`byname`が呼び出されます。)

(追記：このコードはメニューバーをクリックしています。Macのいつかのアップデートでメニューバーは隠すように設定できるようになりました。Macのメニューバーが隠れている場合には動きません。)

## UIElement

まずはSystem Eventsからアプリケーションのプロセスを取得します。
プロセスからメニューやウィンドウを取得し、ウィンドウからボタンを取得し、という風に子要素を取得していくことになります。

プロセスも含め、それらは **全て`UIElement`のサブクラス** です。
(System Events自体は違います。)

UIElementの子要素は`.uiElements`で取得できます。
または`.menuBars`や`.windows`というようなクラス名の複数形プロパティでも取得できます。

`System Events`から参照できる全てのUI Elementsを一覧するスクリプトを書いたので使ってみてください。
https://github.com/zakuroishikuro/JXA-scripts/blob/master/ui-finder.js

![スクリーンショット 2015-02-11 20.42.06.png](https://qiita-image-store.s3.amazonaws.com/0/18902/bcfc7c54-f130-66b4-4885-f8040c594647.png "スクリーンショット 2015-02-11 20.42.06.png")

(これ使ってだいたいどんな感じか分かったら、ここから下は読まなくていいかもしれません。長いし。校正したいけどめんどいので無理です。)

### ApplicationProcess

`System Events`からuiElementsを取得していきます。

```js:ApplicationProcessを取得
var se = Application("System Events");
var elements = se.uiElements();

console.log("参照可能なプロセス数: " + elements.length);

var e = elements[0]; //最初の一つを取り出す

console.log("最初のプロセス: ");
console.log("  class: " + e.class());
console.log("   name: " + e.name());
```

>参照可能なプロセス数: 49
>最初のプロセス: 
>  class: applicationProcess
>   name: loginwindow


配列の最初には、loginwindowという名前の`ApplicationProcess`が入っていました。

`ApplicationProcess`は`Process`のサブクラスで、`Process`は`UIElement`のサブクラスです。
`.uiElements`だけでなく、そのサブクラスを複数形にしたプロパティ名でも取得できます。
System Eventsの `.uiElements` `.processes` `.applicationProcesses`は全て同じです。好きな呼び方で構いません。

```js:サブクラスの複数系でも取得可能
var elements    = se.uiElements();
var processes     = se.processes();
var app_processes = se.applicationProcesses();

console.log(elements.length);
console.log(processes.length);
console.log(app_processes.length);
```
> 49
> 49
> 49

あと、紛らわしいですが`Application`クラスとは異なることに注意してください。
`System Events`自体は`UIElements`でなく`Application`です。UIElementのメソッドは呼べません。

すべてのプロセスの名前を表示してみましょう。
クラスは全て`ApplicationProcess`です。

```js:アクセス可能な全てのプロセスの名前を表示
processes = se.processes();

processes.forEach(function(p, i){ console.log(i + ": " + p.name()) });
```
> 0: loginwindow
> 1: SystemUIServer
> 2: Dock
> 3: Finder
> (中略)
> 33: com.apple.WebKit.WebContent
> 34: Kobito
> 35: JapaneseIM
> 36: System Events

## UI Elementsを辿ってみる

上のコードにある

```js:「このMacについて」のパス
finder.menuBars[0].menuBarItems["Apple"].menus["Apple"].menuItems["この Mac について"];
```
みたいなパスを調べるために、`.uiElements`を一つずつ辿ってみましょう。

いちいち表示の処理を書くのはめんどくさいので、子UI Elementの名前とクラスを全て表示する関数を用意しました。

```js:子UIの情報を表示する(class,name,descriptionの順)
function wrap(e){
  return e.class() + " - " + e.name() + " (" + e.description() + ")";
}

function print(target){
  var childs = target.uiElements();
  
  console.log("\n" + wrap(target) + " has " + childs.length + " UI Elements.");
  
  childs.forEach(function(child, i){
    console.log("-  " + i + ": " + wrap(child))
  });
  
  console.log(); //改行
};
```

さっそく表示していきます。

```js:Finderのプロセス直下のuiElements
print(finder);
```

>Finder (applicationProcess) has 2 UI Elements.
>-  0: menuBar - null (メニューバー)
>-  1: scrollArea - null (デスクトップ)

メニューバーとデスクトップが見えました。
メニューバーを辿ります。

```js:menuBars
print( finder.menuBars[0] );
```

>menuBar - null (メニューバー) has 8 UI Elements.
>-  0: menuBarItem - Apple (メニューバー項目)
>-  1: menuBarItem - Finder (メニューバー項目)
>-  2: menuBarItem - ファイル (メニューバー項目)
>-  3: menuBarItem - 編集 (メニューバー項目)
>-  4: menuBarItem - 表示 (メニューバー項目)
>-  5: menuBarItem - 移動 (メニューバー項目)
>-  6: menuBarItem - ウインドウ (メニューバー項目)
>-  7: menuBarItem - ヘルプ (メニューバー項目)

クラス名に合わせて`.menuBars`としていますが、`.uiElements`でも同じです
メニューから「Apple」を選びます。

```js:menuBarItems
print( finder.menuBars[0].menuBarItems["Apple"] );
```
>menuBarItem - Apple (メニューバー項目) has 1 UI Elements.
>-  0: menu - Apple (メニュー)

...メニューの中にメニューが複数あるかもしれませんしね。仕方ないね。

```js:menus
print( finder.menuBars[0].menuBarItems["Apple"].menus[0] );
```

>menu - Apple (メニュー) has 19 UI Elements.
>-  0: menuItem - この Mac について (メニュー項目)
>-  1: menuItem - システム情報... (メニュー項目)
>-  2: menuItem - null (メニュー項目)
>-  3: menuItem - システム環境設定... (メニュー項目)
>~~~~~~~~~~中略~~~~~~~~~~~
>-  14: menuItem - システム終了... (メニュー項目)
>-  15: menuItem - システム終了 (メニュー項目)
>-  16: menuItem - null (メニュー項目)
>-  17: menuItem - (USER) をログアウト... (メニュー項目)
>-  18: menuItem - (USER) をログアウト (メニュー項目)

...やっと見えましたね。お疲れさまでした。

普通はこんなめんどくさいことしません。
どうなっているのかを知っておけば便利というだけです。

メニューやウィンドウを持たないプロセスもあります。
たとえば、Dockのプロセスは自身のアイコンの配列しか持っていません。

## 子UIを全て取得 (.entireContents)

子UIを一括で取得する方法があります。
UIElementから`.entireContents`を呼ぶのです。

```js:UIElement#entireContents
all = finder.entireContents();

all.length;
```
>338

```最初の5つを取り出してみる
all.slice(0, 5);
```
>[Application("System Events").applicationProcesses.byName("Finder").menuBars.at(0), Application("System Events").applicationProcesses.byName("Finder").menuBars.at(0).menuBarItems.byName("Apple"), Application("System Events").applicationProcesses.byName("Finder").menuBars.at(0).menuBarItems.byName("Apple").menus.byName("Apple"), Application("System Events").applicationProcesses.byName("Finder").menuBars.at(0).menuBarItems.byName("Apple").menus.byName("Apple").menuItems.byName("この Mac について"), Application("System Events").applicationProcesses.byName("Finder").menuBars.at(0).menuBarItems.byName("Apple").menus.byName("Apple").menuItems.byName("システム情報...")]

...見辛い。

多すぎて手に余りますし、扱いも困難です。

**`.entireContents`からは`[]`で位置や名前で指定して取り出すことはできません。**
`.uiElements["なまえ"]`みたいな絞り込みができないのです。
(これに気づくまで相当苦労しました。ネットで情報を探しても全く見当たりませんし...)

`.entireContents()`を呼ぶとUIElementの配列が取得できるので、そこから位置で指定して使うしかありません。
目的の項目はforEachとかで地道に探すしかないです。

UIElementは`console.log()`しても`[object ObjectSpecifier]`としか表示されません。
標準出力にはこのパスっぽいものが表示されていますが、これをJXA内で文字列として得る方法は(たぶん)ないです。

----

(追記2015/2/17:ありました。普通にAppleのドキュメントに書いてありました。

```
Automation.getDisplayString(hoge)
```

----

JXAは最後に評価した式の結果を必ず出力するので、それをパイプでファイルに保存すれば一応は文字列で得られます。
あとはRubyかなんかで`, `で分割して絞り込むとか・・・。

```
$ osascript -l JavaScript -e 'Application("System Events").processes["Finder"].entireContents()' > contents.txt
```

## X, Y座標を指定してクリック

```js:座標を指定してクリック
finder_app = Application("Finder");
finder_app.activate();

delay(0.1); // activateになるまで少しかかるので、0.1秒待つ

finder = Application("System Events").processes[finder_app.name()];

x = 100;
y = 10;

finder.click( {at:[x, y]} );
```

`Process`クラスの`click()`には座標を渡せます。
このコードを実行した場合、Finderの「Finder」メニューがクリックされます。(されるはずです。)

`[x, y]`はグローバル座標です。
UIElement内の相対的な座標ではなく、画面内の絶対座標です。

CSSピクセルですから、Retinaディスプレイでもそうでなくても同じ位置を指します。
Retinaディスプレイの場合はピクセル数 / 2の数値を指定してください。
「2560 * 1600」ピクセルのRetinaディスプレイの場合、xは「0〜1279」で、yは「0〜799」です。

その座標の位置にそのプロセスが持つUIElementが存在すればクリックされます。
そして、そのUIElementの参照が戻り値として返ってきます。
UIElementが存在しなければ何も起きず、nullが返ってきます。

そのプロセスが持っていないUIElementはクリックできません。
Finderのプロセスにドックをクリックさせようとしてもデスクトップの参照が返ってくるだけです。

メニューをクリックしたいなら、最前面のアプリのプロセスを指定する必要があります。

いまいち用途が分かりません。
ちなみにUIElementは`.position()`を呼べば座標を取得できます。

## 出来ないこと

- 右クリック
- ホイールクリック
- ドラッグ
- ドラッグ&ドロップ

(する方法あれば教えてください。)

# UI Elementを探索するツール

そろそろ力尽きてきたのである場所だけ。

## Accessibility Inspector

XCodeの中にあります。
そのままJXAで使える情報が表示されるわけではありませんが、ヒントにはなると思います。

![スクリーンショット 2015-02-10 19.38.43.png](https://qiita-image-store.s3.amazonaws.com/0/18902/69ca3c77-904c-c4a5-5174-3d82fa11f32c.png "スクリーンショット 2015-02-10 19.38.43.png")

## UI Browser

けっこう人気らしい。
$55です。30日間無料。

[UI Browser](http://pfiddlesoft.com/uibrowser/)

# 蛇足

Apple Scriptは真面目に勉強する気がおきず、最近JXAを始めたにわかなので何か間違った記述をしていたら教えていただけるとありがたいです。

殴り書きの文章すいませんした。

# 参考リンク

- [Hys.LOG : Mac のキーコード 2012 (Full Keyboard)](http://hyslog.com/blog/2012/06/25/569)
- [GUIスクリプティングで使いたくなる道具 - ザリガニが見ていた...。](http://d.hatena.ne.jp/zariganitosh/20100118/1263805168)
- [AS Hole（AppleScriptの穴） By Piyomaru Software » UI Browser 2.5が登場 » Blog Archive](http://piyocast.com/as/archives/3171)
- [Apple Script：UIElementInspector が便利です - thyVikings : iMac, iPad, iMat by aged engine](http://thyvikings.cool.coocan.jp/wordpress/?p=16869)