MacBookのメニューをショートカットで操作する試行錯誤

OSXでは、システム環境設定 >> キーボード >> キーボードショートカットで、よく使うメニューにショートカットを設定できる。一見、とても便利なんだけど、でも完璧じゃない...。

  • 重複するショートカットも設定できるが、いざ操作してみると機能しない。
  • 操作時の状況によって、その都度変化するメニューもあり、そのようなメニューに対してはショートカットを設定できない。設定できても、初回操作の時、反応しない。
  • メニュー操作を完結することが目的ではなく、メニューを展開した状態を表示しておきたい場合もある。
    • 例1:表示 >> テキストエンコーディング >> Unicode (UTF-8)
      • 最初にメニュー操作した時に生成される。初回は必ずマウス操作が必要。その後、メニューが変化しない限りショートカットは有効。
    • 例2:ファイル >> 最近使った項目を開く
      • 常に動的に生成される。項目も一定せず、特定のメニューテキストを指定する意味がない。
      • この操作で目指すのは、特定のファイルを選択して開くことではなく、最近使った項目を開くメニューが展開された状態にしておきたいのだ。
      • そうすれば、最近使ったファイルを↑↓キーで簡単に選択できる。

GUIスクリプティングによるメニュー操作の基本

例1:表示 >> テキストエンコーディング >> Unicode (UTF-8)


tell application "System Events"
tell process "Safari"
set frontmost to true --必ず、アクティブにしておく

tell menu bar 1
UI elements
end tell

end tell
end tell

  • 上記コードを実行すると、以下の結果が取得できるので、目指すメニュー操作のヒントが理解できた。
  • ちなみに、ここでは関係ないが、アップルメニューなら、menu bar item "Apple"と指定すれば良いことが分かる。


{menu bar item "Apple" of menu bar 1 of application process "Safari" of application "System Events", ¬
menu bar item "Safari" of menu bar 1 of application process "Safari" of application "System Events", ¬
menu bar item "ファイル" of menu bar 1 of application process "Safari" of application "System Events", ¬
menu bar item "編集" of menu bar 1 of application process "Safari" of application "System Events", ¬
menu bar item "表示" of menu bar 1 of application process "Safari" of application "System Events", ¬
menu bar item "履歴" of menu bar 1 of application process "Safari" of application "System Events", ¬
menu bar item "ブックマーク" of menu bar 1 of application process "Safari" of application "System Events", ¬
menu bar item "開発" of menu bar 1 of application process "Safari" of application "System Events", ¬
menu bar item "GreaseKit" of menu bar 1 of application process "Safari" of application "System Events", ¬
menu bar item "Stand" of menu bar 1 of application process "Safari" of application "System Events", ¬
menu bar item "ウインドウ" of menu bar 1 of application process "Safari" of application "System Events", ¬
menu bar item "ヘルプ" of menu bar 1 of application process "Safari" of application "System Events", ¬
menu bar item "Debug" of menu bar 1 of application process "Safari" of application "System Events"}

  • 目的のメニューは"表示"なので、今度は tell menu bar item "表示" を追加して実行してみる。


tell application "System Events"
tell process "Safari"
set frontmost to true --必ず、アクティブにしておく

tell menu bar 1
tell menu bar item "表示"
UI elements
end tell
end tell

end tell
end tell

  • 実行結果は以下。意外にも1項目しかない。しかも、"表示"が重複している感じ。


{menu "表示" of menu bar item "表示" of menu bar 1 of application process "Safari" of application "System Events"}

  • 腑に落ちない疑問はあるけど、今は機械的に作業を続ける。


tell application "System Events"
tell process "Safari"
set frontmost to true --必ず、アクティブにしておく

tell menu bar 1
tell menu bar item "表示"
tell menu "表示"
UI elements
end tell
end tell
end tell

end tell
end tell

  • 次の実行結果は以下。やっと、表示メニューのリストが出て来た!(... = application process "Safari" of application "System Events")


{menu item "ブックマークバーを隠す" of menu "表示" of menu bar item "表示" of menu bar 1 of ..., ¬
menu item "ステータスバーを隠す" of menu "表示" of menu bar item "表示" of menu bar 1 of ..., ¬
menu item "タブバーを隠す" of menu "表示" of menu bar item "表示" of menu bar 1 of ..., ¬
menu item "ツールバーを隠す" of menu "表示" of menu bar item "表示" of menu bar 1 of ..., ¬
menu item "ツールバーをカスタマイズ..." of menu "表示" of menu bar item "表示" of menu bar 1 of ..., ¬
menu item 6 of menu "表示" of menu bar item "表示" of menu bar 1 of ..., ¬
menu item "中止" of menu "表示" of menu bar item "表示" of menu bar 1 of ..., ¬
menu item "ページを再読み込み" of menu "表示" of menu bar item "表示" of menu bar 1 of ..., ¬
menu item 9 of menu "表示" of menu bar item "表示" of menu bar 1 of ..., ¬
menu item "実際のサイズ" of menu "表示" of menu bar item "表示" of menu bar 1 of ..., ¬
menu item "拡大" of menu "表示" of menu bar item "表示" of menu bar 1 of ..., ¬
menu item "縮小" of menu "表示" of menu bar item "表示" of menu bar 1 of ..., ¬
menu item "テキストのみ拡大/縮小" of menu "表示" of menu bar item "表示" of menu bar 1 of ..., ¬
menu item 14 of menu "表示" of menu bar item "表示" of menu bar 1 of ..., ¬
menu item "ソースを表示" of menu "表示" of menu bar item "表示" of menu bar 1 of ..., ¬
menu item 16 of menu "表示" of menu bar item "表示" of menu bar 1 of ..., ¬
menu item "テキストエンコーディング" of menu "表示" of menu bar item "表示" of menu bar 1 of ...}

  • このように作業を続けると、最終的には以下のコードが導き出される。


tell application "System Events"
tell process "Safari"
set frontmost to true --必ず、アクティブにしておく

tell menu bar 1
tell menu bar item "表示"
tell menu "表示"
tell menu item "テキストエンコーディング"
tell menu "テキストエンコーディング"
tell menu item "UnicodeUTF-8)"
click
UI elements
end tell
end tell
end tell
end tell
end tell
end tell

end tell
end tell

  • 例1の目的は達成されたが、それにしても分かり難いコードだ。
  • こんなコードを毎回書くことを思うと、やる気が失せる。
  • そもそも、なぜ"表示"、"テキストエンコーディング"が重複しているのか?
  • この疑問は、上記コードを以下のように変形すると、少し納得できた。


tell application "System Events"
tell process "Safari"
set frontmost to true --必ず、アクティブにしておく

tell menu bar 1's menu bar item "表示"
tell menu "表示"'s menu item "テキストエンコーディング"
tell menu "テキストエンコーディング"'s menu item "UnicodeUTF-8)"
click
UI elements
end tell
end tell
end tell

end tell
end tell

  • GUI(オブジェクト)と対応させて、やっと納得できた。なるほど!
  • メニューオブジェクトは、グループ名と内包するリストアイテムで構成されている。
  • 選択したリストアイテム名と、その結果展開されるメニューオブジェクトのグループ名が同じなのだ。
    • tell menu bar 1's menu bar item "表示"


click_menuの定義

  • 上記のように書いたとしても、相変わらず分かり難いコードであることに変わりない。やる気は失せる...。
  • 理想としては、シンプルに以下のように書いて済ませたいのだ。


click_menu("Safari", "表示/テキストエンコーディング/UnicodeUTF-8)")

  • それを実現したくて、以前の日記:GUIスクリプティングなAppleScript環境を快適にするで、click_menu()を定義した。
  • 関係するところだけ抜粋すると、以下のようなコード。(今思い返すと、この頃の自分はまだ完全に理解できていなかった。無駄が多い。)


on click_menu(app_name, menu_path)
if menu_path is "" then
error "menu_path が入力されていません。"
end if

if app_name is "" then
set app_name to frontmost_app()
end if

set mp to split(menu_path, "/")


tell application "System Events"
tell process app_name
set frontmost to true --必ず、アクティブにしておく

if mp's length = 1 then
menu bar 1's (menu bar item (my number_from(mp's item 1)))
else
menu bar 1's (menu bar item (my number_from(mp's item 1)))'s menu 1
repeat with i from 2 to mp's length
if i < mp's length then
result's (menu item (my number_from(mp's item i)))'s menu 1
else
result's (menu item (my number_from(mp's item i)))
end if
end repeat
end if

click result --click:クリックする/pick:選択する--ほぼ同等だが、アイコンメニューにはclickが必須
delay 0.1 --連続してメニューを操作する時、ひと呼吸必要
end tell
end tell
end click_menu

on frontmost_app()
tell application "Finder"
set app_name to name of (path to frontmost application)
end tell
split(app_name, ".")'s item 1 as text
end frontmost_app

on split(sourceText, separator)
if sourceText = "" then return {}
set oldDelimiters to AppleScript's text item delimiters
set AppleScript's text item delimiters to {separator}
set theList to text items of sourceText
set AppleScript's text item delimiters to oldDelimiters
return theList
end split

on is_number(num)
num's class is integer or num's class is real
end is_number

on number_from(str)
try
str as number
on error
str
end try
end number_from

  • 上記click_menuは、以下のような仕様となった。
    • click_menu(app_name, menu_path)
    • メニュー操作をシンプルに実行する
    • app_nameは、操作対象のアプリケーション名。""にすると、実行時にアクティブなアプリケーションに対する操作となる。
    • menu_pathは、テキストまたはリスト


click_menu("", "編集/検索/検索...") --OK
click_menu("Script Editor", {"編集", "検索", "検索..."}) --OK
click_menu("Script Editor", "編集", "検索", "検索...") --NG(リストでないので、編集をクリックする操作になってしまう)

    • …(全角1文字)と...(半角ピリオド3文字)の種類に注意


click_menu("Script Editor", "Apple/システム環境設定…") --全角1文字
click_menu("Script Editor", "スクリプトエディタ/環境設定...") --半角ピリオド3文字

    • 小さいカナ文字に注意(×ウィンドウ ○ウインドウ)
    • アップルメニューの場合はpath_menuに"Apple"を指定する


click_menu("", "Apple/この Mac について")

    • アイテム番号による指定も可能
    • アイテム番号では区切り線も1と数える
    • サービスメニューのグループ名も1と数える
    • アイテム番号とアイテム名称の混在も可能


click_menu("AppleScript Editor", "AppleScript エディタ/サービス/選択部分を含む新しいテキストエディットウインドウを開く")
click_menu("AppleScript Editor", "2/5/20") --アイテム番号で指定
click_menu("AppleScript Editor", "2/サービス/選択部分を含む新しいテキストエディットウインドウを開く") --混在も可能

    • アイコンメニュー(ステータスメニュー)の操作は、アイテム番号の選択とキー操作で行う
    • アイコンメニュー(ステータスメニュー)のアイテム番号は、control-F8でステータスメニューを選択して、左端から矢印キーで移動しながら数えると確認し易い
    • アイコンメニュー(ステータスメニュー)のapp_nameは、"SystemUIServer"
    • "SystemUIServer"以外のアイコンメニューを操作する方法は、現状分からない...
    • 例:ログインウィンドウを表示する(ゲストなし、ログインユーザーが1人だけの場合)


click_menu("SystemUIServer", "13")
shortcut("↓")
shortcut("↓")
shortcut("space")

メニュー操作の途中経過も再現する

  • メニューの操作がかなり楽になったが、まだ問題がある。
  • 以下のように完結する操作なら、検索ウィンドウが表示されるが...
click_menu("", "編集/検索/検索...")
  • メニューのディレクトリに該当するアイテムを指定しても、何も起こらない。
click_menu("", "編集/検索")
  • 期待する動作としては、マウスを編集 >> 検索とホバーした状態を表示して欲しいのだ。
  • それを実現するためには、現状では以下のように書かなくてはならない。
click_menu("", "編集")
click_menu("", "編集/検索")
  • 途中の操作も含めて、すべての段階でクリックするように指定すれば良いのだ。
  • しかし、現状のclick_menuでは2行になってしまう。ここはやはり1行で書けるようにしたい。
  • コードを見直しながら書き直してみると、何と!if文が消えて、こんなにスッキリ書き直せるのであった。


on click_menu(app_name, menu_path)
if menu_path is "" then
error "menu_path が入力されていません。"
end if

if app_name is "" then
set app_name to frontmost_app()
end if

set mp to split(menu_path, "/")

tell application "System Events"
tell process app_name
set frontmost to true --必ず、アクティブにしておく

menu bar 1's menu bar item (my number_from(mp's item 1))
click result
repeat with i from 2 to mp's length
result's menu 1's menu item (my number_from(mp's item i))
click result
end repeat

delay 0.1 --連続してメニューを操作する時、ひと呼吸必要
end tell
end tell
end click_menu

...(以下変更なし)...

利用例

  • 上記のclick_menuは、~/Library/Scripts/_GUI.scpt として、GUIスクリプティングのライブラリのつもりで保存してある。
  • 利用する時は、プロパティーあるいは変数に取り込んで、以下のようにしている。


property GUI : load script file ((path to scripts folder as text) & "_gui.scpt")

GUI's click_menu("Safari", "表示/テキストエンコーディング/UnicodeUTF-8)")

  • 例2:ファイル >> 最近使った項目を開く


set GUI to load script file ((path to scripts folder as text) & "_gui.scpt")

GUI's click_menu("", "ファイル/最近使った項目を開く")

  • ストアカウントでファストユーザースイッチ(ステータスメニューの操作)


property GUI : load script file ((path to scripts folder as text) & "_gui.scpt")

GUI's click_menu("SystemUIServer", "13/1")

  • アップルメニューの最近使った項目
    • プロパティにparentを指定することで、_gui.scptを継承したスクリプト環境になる。
    • よって、GUI's click_menu()でなく、シンプルにclick_menu()と書ける。
    • 但し、複数のライブラリを継承することはできないと思う。(複数のライブラリを利用する時は、プロパティーあるいは変数に取り込むしかない...。)


property parent : load script file ((path to scripts folder as text) & "_gui.scpt")

click_menu("", "Apple/最近使った項目")


メニュー操作が再現できたら、そのスクリプトQuicksilver(Bulter、Spark等でもOKかも)でショートカットを設定して、あとは便利に使うだけ。