システム環境設定にあるキーボードショートカットを設定するJXA

2年ぐらい前にポストした「[システム環境設定にあるキーボードショートカットを設定するAppleScript](https://qiita.com/littlebusters/items/65374b03efd9399cb688)」のJXA版です。

OS標準のショートカット設定では、設定をインポート/エクスポートができないため、それを補完する感じでJSONファイルで設定を書いて無理やり取り込むスクリプトです。インポートができれば設定ファイルが残るため、エクスポートは「ええやろ」という考えなのでエクスポートのスクリプトはありません。

High Sierra（10.13.6）で動作確認しています。

## 使い方

1. 環境設定 → セキュリティとプライバシー → プライバシー の「アクセシビリティ」で、スクリプトエディタ.appにコンピュータの操作を許可する
1. JSONファイルをこしらえる（:point_right_tone2: [JSONの書き方](#jsonの書き方)）
1. :point_right_tone2: [下記のスクリプト](#スクリプト)を「スクリプトエディタ」にコピペする（言語はJavaScriptを選択してください）
1. スクリプトエディタのツールバーにある右向きの三角で実行（ショートカット：Cmd + R）すると、ダイアログが表示されるので、こしらえたJSONファイルを選択する
1. あとは無事終わるまで眺める（スクリプトでGUIを操作しているので、文字通り眺めてください）

「アプリケーション」フォルダ直下にないアプリをJSONの`appName`で指定している場合、4でダイアログが表示されるので、`appName`と同じアプリを選択してください。

- スクリプトエディタ.appは、「アプリケーション/ユーティリティ」の中にあります。
- 「アプリケーション」フォルダは、FinderでCmd + Shift + Aで表示されるフォルダです。

## JSONの書き方

次のように記述します。

```json
[
    {
        "appName": "Sketch.app",
        "shortcut": [
            {
                "menu": "Flatten Selection to Bitmap",
                "key": "e",
                "modifier": [ "command", "control", "shift" ]
            }
        ]
    }
]
```

<dl>
<dt>appName:</dt>
<dd>アプリケーション名です。アプリケーションフォルダ内の名前を正確に入力してください。</dd>
<dt>shortcut:</dt>
<dd>
<dl>
<dt>menu:</dt>
<dd>アプリのメニュー項目を正確に入力してください。同じ項目名が複数ある場合は、親のメニュー項目を「->」でつなぎます。<br>
例：<code>Use Default</code>という項目がかぶる場合、<code>Kern->Use Default</code>と<code>Ligature->Use Default</code>と入力する。</dd>
<dt>key:</dt>
<dd>キーボードで文字が入力できるキーを1文字を小文字で指定してください。例外的に矢印キーは「up」「right」「down」「left」と指定できます。Fキーは要望があれば対応します。</dd>
<dt>modifier:</dt>
<dd>Command・Control・Shift・Optionキーを指定します。CommandまたはControlのどちらかは必ず含めてください。</dd>
</dl>
</dl>

### サンプル

- :point_right_tone2: [Adobe XDプラグイン「Adjust Size by Shortcut」のSketchライクなショートカットのJSONファイル](https://qiita.com/littlebusters/private/51c684aa90d507964dd8)

## スクリプト

```javascript
"use strict";

var app = Application.currentApplication();
app.includeStandardAdditions = true;

// ローカライズ
var langList = app.doShellScript("defaults read NSGlobalDomain AppleLanguages | awk '{gsub(/[^a-zA-Z-]/,\"\");print}' | grep -v '^$'");
var langs = langList.split('\r');

var chooseJSONLabel = 'Please select a JSON file:';
var shortcutLabel = 'App Shortcuts';
var menuItemLabel = 'Menu Title:';
var addButtonLabel = 'Add';
switch (langs[0].substr(0, 2)) {
	case 'ja':
		chooseJSONLabel = 'JSONファイルを選択してください。';
		shortcutLabel = 'アプリケーション';
		menuItemLabel = 'メニュータイトル:';
		addButtonLabel = '追加';
		break;
}

var appList = app.doShellScript('ls /Applications').split('\r');
		
// 設定ファイルを読み込んでJSONへパース
var chooseFile = app.chooseFile({
		withPrompt: chooseJSONLabel
	})
var json = JSON.parse(readFile(chooseFile));

// 環境設定を起動してキーボードを選択し、ショートカットタブを表示する
var pref = Application('com.apple.systempreferences');
pref.panes['com.apple.preference.keyboard'].anchors['shortcutsTab'].reveal();
pref.activate();
delay(2);

// 左のリストから「アプリケーション」を選択
var sysEvent = Application('System Events');
var prefKeyboard = sysEvent.processes['System Preferences'].windows[0];
var rows = prefKeyboard.tabGroups[0].splitterGroups[0].scrollAreas[0].tables[0].rows();
var shortcutPosition = 0;
for (let i = 0; i < rows.length; i++) {
	console.log(rows[i].staticTexts[0].name());
	if (shortcutLabel === rows[i].staticTexts[0].name()) {
		shortcutPosition = i;
		break;
	}
}
rows[shortcutPosition].select();

// ショートカットを設定
for (let i = 0; i < json.length; i++) {
	let appName = json[i].appName;
	let appExist = false;
	appList.some(function (obj) {
		if (appName === obj) {
			appExist = true;
			return true;
		}
	});

	let appSelected = false;
	
	for (let j = 0; j < json[i].shortcut.length; j++) {
		// [+]ボタンをクリックしてダイアログシートを表示
		prefKeyboard.tabGroups[0].groups[0].buttons[0].click();
		let settingSheet = prefKeyboard.sheets[0];

		// アプリケーションを設定
		if (!appSelected) {
			if (appExist) {
				settingSheet.popUpButtons[0].click().menus[0].menuItems[appName].click();
			} else {
				// ポップアップの中にアプリがない場合
				let appListItems = settingSheet.popUpButtons[0].click().menus[0].menuItems.length;
				settingSheet.popUpButtons[0].menus[0].menuItems[appListItems - 1].click();
				
				// アプリが選択されるまで待つ
				while (!appExist) {
					if(appName == settingSheet.popUpButtons[0].value()) {
						appExist = true;
					} else {
						delay(2);
					}
				}
			}
			appSelected = true;
		}

		// メニュー項目を設定
		settingSheet.textFields[menuItemLabel].value = json[i].shortcut[j].menu;

		// タブキーでフォーカスを移動
		sysEvent.keyCode(48);

		// ショートカットキーを設定
		let pressKey;
		switch (json[i].shortcut[j].key) {
			case 'left':
				pressKey = 123;
				break;
			case 'right':
				pressKey = 124;
				break;
			case 'up':
				pressKey = 125;
				break;
			case 'down':
				pressKey = 125;
				break;
			default:
				pressKey = json[i].shortcut[j].key
				break;
		}
		setShortcut(pressKey, json[i].shortcut[j].modifier);

		// 追加ボタンをクリック
		settingSheet.buttons[addButtonLabel].click();
	}
	appSelected = false;
}


// Helpers
function readFile(file) {
	var fileString = file.toString();
	return app.read(Path(fileString));
}

function setShortcut(key, mod) {
	for (var i = 0; i < mod.length; i++) {
		mod[i] = mod[i] + ' down';
	}
	if (isNaN(key)) {
		sysEvent.keystroke(key, {using: mod});
	} else {
		sysEvent.keyCode(key, {using: mod});
	}
}
```


## トラブルシューティング

エラー処理はあまり考えてないので、そこは大目にみてください :bow_tone2:

### 1回実行した後にエラーが出て再実行できない

スクリプトエディタでハンマーのアイコンをクリックしてみてください。ショートカットはCmd + Kです。

### アプリを選択したが進まない

JSONの`appName`で指定した名前が間違っている可能性がありますので、確認してください。