MacBookのあらゆるウィンドウをキー操作で自在に操るために(AppleScript + Quicksilver)

MacOSXのバージョンも10.5.3になり着々と洗練されてきたが、ウィンドウ操作だけはどうしても不満が残る。この不満はOSXになって以来、自分の中でずっと続いている。

キー操作またはマウスクリック一発で、ウィンドウを画面いっぱいまで最大化する方法がないこと。

確か...OS9の頃は「オプションキー + ズームボタンクリック」で画面いっぱいまで最大化できた記憶がある。(かなりのアプリケーションが対応してくれていた気がする。そして、もう一度押すと直前のウィンドウサイズに戻る。この繰り返しが非常に使い勝手が良かった。)自分の経験では、ノートパソコンの狭い画面では、常にウィンドウを最大化したい欲求があり、ドラッグ&ドロップの操作の時だけ一時的にウィンドウを小さくするような使い方が多い。ExposeやSpacesが利用できる今の環境では、最大化しておきたい欲求はさらに高まる。

  • それなのにズームボタンを押しても、Safariのウィンドウは逆に小さくなるし、もう一度押しても元のサイズに戻りさえしない...。
  • Firefox2の方はまだマシだが、最大化した時は右側に128ドットの隙間を残す。おそらくこの仕様は、ダウンロードしたファイル等をマウスで操作しやすい親切設計なのだろうけど、Dockのスタック表示あり、ダウンロードフォルダあり、ExposeやSpacesありの今となっては、無用なおせっかい機能な気がする。(Firefox3では目一杯最大化するようになった。)
  • Finderのカラム表示では今のウィンドウの高さを維持したまま、横幅だけ画面いっぱいにしたいことが多々ある。それなのに多くの場合、この時だけは画面目一杯に最大化されることが多い。(自分の勝手で申し訳ないが、Finderではドラッグ&ドロップの操作が多いので、最大化して欲しくないのだ。)

プレビューバージョンを含めればOSXになってから6世代この状態なのだから、OS側のサポートにはもう期待しない。Rubiscoという素晴らしいソフトウェアもあるのだが、Cocoaアプリケーションのみ対応。よって、Finder、FirefoxiTunesなどは操作できないそうだ。残念...。
こうなったら、いつものごとく、困ったときの最後の手段、AppleScriptでゴニョゴニョ...試行錯誤してみた。そうそう、GUIスクリプティングがあるさ、AppleScript対応に関係なく、GUIをOSレベルで直接操作できるのだから、理論的にはすべてのウィンドウを操れることになってるのだから...。

ウィンドウを最大化する

まずはウィンドウを最大化するスクリプトを書いてみた。アプリケーションのAppleScript対応状況に関係なく操作できるようにしたいので、GUIスクリプティングを利用して操作することに。だから、システム環境設定のユニバーサルアクセスの「補助装置にアクセスできるようにする」をチェックありの状態にしないと、以下のスクリプトは機能しない。

 ---------- zoom_window_full.scpt ----------
 --最前面のアプリケーションを取得する
on frontmost_app()
    tell application "System Events"
        set pList to name of every process whose frontmost is true
        set appName to item 1 of pList --"Script Editor"
        --set appName to name of (path to frontmost application)--"Script Editor.app"
    end tell
    --return appName--最後に評価された値が戻り値になるので不要
end frontmost_app

on run 
    --デスクトッップのサイズを取得
    tell application "Finder"
        set menuHight to 22
        set displayBounds to bounds of window of desktop
        set displayPosition to {item 1 of displayBounds, (item 2 of displayBounds) + menuHight}
        set displaySize to {item 3 of displayBounds, (item 4 of displayBounds) - menuHight}
    end tell

    set appName to frontmost_app()
    tell application "System Events"
        tell process appName			
            set topWindow to window 1
            set position of topWindow to displayPosition
            set size of topWindow to displaySize
        end tell
    end tell
end run

元のサイズ・ポシションに戻す

最大化するようになったが、これで満足してはいけない。やはり、同じ操作(スクリプト)を繰り返し実行した時は、元のウィンドウサイズ・ポジションに戻したい。プロパティを利用して以下のようにしてみた。

 ---------- zoom_window_full.scpt ----------
property lastName : ""
property lastPosition : {}
property lastSize : {}

 --最前面のアプリケーションを取得する
on frontmost_app()
    tell application "System Events"
        set pList to name of every process whose frontmost is true
        set appName to item 1 of pList --"Script Editor"
        --set appName to name of (path to frontmost application)--"Script Editor.app"
    end tell
    --return appName--最後に評価された値が戻り値になるので不要
end frontmost_app

on run 
    --デスクトッップのサイズを取得
    tell application "Finder"
        set menuHight to 22
        set displayBounds to bounds of window of desktop
        set displayPosition to {item 1 of displayBounds, (item 2 of displayBounds) + menuHight}
        set displaySize to {item 3 of displayBounds, (item 4 of displayBounds) - menuHight}
    end tell

    set appName to frontmost_app()
    tell application "System Events"
        tell process appName			
            set topWindow to window 1
            set topName to name of topWindow		
            if topName is lastName then
                set lastName to ""
                set position of topWindow to lastPosition
                set size of topWindow to lastSize
            else
                set lastName to topName
                set lastPosition to position of topWindow
                set lastSize to size of topWindow
		
                set position of topWindow to displayPosition
                set size of topWindow to displaySize
            end if
        end tell
    end tell
end run

これでウィンドウが、最大化と直前の状態を繰り返すようになった!

キー操作でスクリプトを呼び出す

上記スクリプトは、フォーマット: スクリプトで「~/Library/Scripts/」に保存すれば、メニューバーのスクリプトメニュー*1から利用できる。

  • Dockに登録してもフォーマット: スクリプトなので、スクリプトエディタで編集画面が開いてしまう。
  • そうするとフォーマット: アプリケーションで保存したくなるが、最前面のアプリケーションに対してのウィンドウ操作になるので、自分自身が対象になってしまいエラーが発生する。
  • AppleScriptを利用する時、一般的にフォーマット: スクリプトの方が、アプリケーションとして実行するよりもキビキビ動く気がする。

そしてもう一つ、素晴らしい実行方法がある。QuicksilverのTriggersにキーボードショットカットとして登録してしまうのだ。自分の場合は「ctrl + option + z」を割り当てた。これでウィンドウを最大化したくなった時、キー操作一発で瞬時に最大化する!もう一度同じキー操作をすれば、元の位置、大きさに戻る。これでちょっと満足。

Quicksilverについて
  • Quicksilverについては、あまりにも有名で奥深いソフトウェアなので、google:Quicksilver 使い方で、かなりの数の素晴らしい解説ページがヒットする。
  • 中でもわかばマークのMacの備忘録さんの解説記事にはたいへんお世話になっています。感謝です!
  • ちなみに、自分がMacBookにインストールしているバージョンは3815となっている。(QS.3815.dmgをダウンロードしてインストールした。)

ウィンドウを微妙にズラすために

面白くなってウィンドウをばしばし最大化していると、ちょっとだけ困った現象に遭遇した。例えば、4つのウィンドウを最大化してExposeで一覧表示してみると...

このように横1列に表示されてしまう...。確かに一覧表示にはなっているけど、スペースの使い方が今イチな感じだ。本来は以下のように並んでくれると嬉しい。

Exposeがどのようなルールでウィンドウを並べているかは理解できなかったが、全く同じ位置と大きさのウィンドウを並べる時に横一列の状況になってしまうようだ。解決策は簡単で、一つのウィンドウだけ、1ドットでもいいから他のウィンドウと異なるように縦横方向にズラしてあげること。ウィンドウを最大化するスクリプトで自動化することも考えたが、複数のウィンドウを管理するのが面倒だ。よって、最大化のスクリプトと同じように一つのスクリプトとして独立させ、困った時だけキーボードショットカットでズラしの操作をすることにした。

 ---------- move_window_1px.scpt ----------
 --最前面のアプリケーションを取得する
on frontmost_app()
    tell application "System Events"
        set pList to name of every process whose frontmost is true
        item 1 of pList --"Script Editor"
    end tell
end frontmost_app

on run
    set appName to frontmost_app()
    tell application "System Events"
        tell process appName
            set topWindow to window 1
            set topPosition to position of topWindow
            --Spacesで横一列、または縦一列の配置防止のため1ドットのズレを作る
            set position of topWindow to {(item 1 of topPosition) + 1, (item 2 of topPosition) + 1}
        end tell
    end tell
end run

上記スクリプトスクリプトメニューに登録して、Quicksilverでキーボードショットカットは「ctrl + option + x」を割り当てた。これでスペースを無駄なく利用できるようになった!

ウィンドウを操作するスクリプトいろいろ

ウィンドウを操作するスクリプトの作り方は分かってきたので、あとは想像力と試行錯誤で、自分好みの操作を実現するスクリプトを作って登録するだけだ。その後、好みで追加したスクリプトは以下。

  • ウィンドウを上下左右に寄せる。
    • jump_window_down.scpt
    • jump_window_left.scpt
    • jump_window_right.scpt
    • jump_window_up.scpt
  • ウィンドウを少しずつ移動する。
    • move_window_1px.scpt(Exposeで最適な配置にするために利用)
    • move_window_down.scpt
    • move_window_left.scpt
    • move_window_right.scpt
    • move_window_up.scpt
  • ウィンドウのサイズを少しずつ変更する。
    • resize_window_high.scpt(高くする)
    • resize_window_low.scpt(低くする)
    • resize_window_narrow.scpt(狭める)
    • resize_window_wide.scpt(広げる)
  • ウィンドウの位置とサイズを一気に変更する。
    • zoom_window_default.scpt(Finderのデフォルトサイズ750×425にする)
    • zoom_window_full_hight.scpt(高さだけ画面いっぱいにする)
    • zoom_window_full_width.scpt(幅だけ画面いっぱいにする)
    • zoom_window_full.scpt(画面いっぱいにする)
    • zoom_window_half_hight.scpt(高さだけ画面の半分にする)
    • zoom_window_half_width.scpt(幅だけ画面の半分にする)


上記スクリプトを作っている過程で、今までの1話完結のスクリプト方式では重複箇所が非常に多くなることに気付く。Railsから学んだ徹底したDRYの原則に大きく反する。しかし、Rubyのようにブロックを引数にする仕組みは無さそうなので、仕方なく以下のように書いてみた。

---------- zoom_window_full.scpt ----------
property windowBase : load script file (((path to scripts folder) as text) & "window_operation:window_base.scpt")

on run
    operate_window("full_size") of windowBase
end run
---------- window_base.scpt ----------
property lastWindow : load script file (((path to scripts folder) as text) & "window_operation:window_pref.scpt")

--最前面のアプリケーションを取得する
on frontmost_app()
    tell application "System Events"
        set pList to name of every process whose frontmost is true
        item 1 of pList
    end tell
end frontmost_app

--ウィンドウを操作する
on operate_window(op)
    --デスクトッップのサイズを取得
    tell application "Finder"
        set menuHight to 22
        set displayBounds to bounds of window of desktop
        set displayPosition to {item 1 of displayBounds, (item 2 of displayBounds) + menuHight}
        set displaySize to {item 3 of displayBounds, (item 4 of displayBounds) - menuHight}
        set displayHalfSize to {(item 1 of displaySize) / 2, (item 2 of displaySize) / 2}
        set displayFinderSize to {750, 425}
        set px to 20
    end tell
	
    set appName to frontmost_app()
    tell application "System Events"
        tell process appName
            load_pref() of lastWindow --前回のウィンドウ状態を取得
			
            set topWindow to front window --window 1と同等
            set storeName to name of topWindow & op
            set storeposition to position of topWindow
            set storeSize to size of topWindow
			
            if storeName is _name() of lastWindow then --以前のwindow配置を設定
                set afterPosition to _position() of lastWindow
                set afterSize to _size() of lastWindow
                set_name("") of lastWindow
            else --スクリーンに合わせたwindow配置に設定
                if op is "full_size" then
                    set afterPosition to displayPosition
                    set afterSize to displaySize
                    set_name(storeName) of lastWindow
                else if op is "full_width" then
                    set afterPosition to {item 1 of displayPosition, item 2 of storeposition}
                    set afterSize to {item 1 of displaySize, item 2 of storeSize}
                    set_name(storeName) of lastWindow
                else if op is "full_height" then
                    set afterPosition to {item 1 of storeposition, item 2 of displayPosition}
                    set afterSize to {item 1 of storeSize, item 2 of displaySize}
                    set_name(storeName) of lastWindow
                else if op is "half_width" then
                --(中略、ウィンドウ操作の数だけifが続く)--
                end if
            end if
            set position of topWindow to afterPosition
            set size of topWindow to afterSize
			
            set_position(storeposition) of lastWindow
            set_size(storeSize) of lastWindow
            save_pref() of lastWindow --ウィンドウ状態を保存する
        end tell
    end tell
end operate_window

AppleScriptを別ファイルに保存して、それをスクリプトオブジェクトとして呼び出して実行した時に、プロパティが思うように更新されなかった。悩んでみたが対策分からず...。しょうがなく自分でファイル操作をして、環境設定ファイルに保存するようにしてみた。

---------- window_pref.scpt ----------
property lastName : ""
property lastPosition : {}
property lastSize : {}

on pref_path()
    ((path to preferences folder) as text) & "com.bebekoubou.window_operation_pref.csv"
end pref_path

on save_pref()
    set prefList to lastPosition & lastSize
    set prefText to lastName
    repeat with prefItem in prefList
        set prefText to prefText & "," & (prefItem as text)
    end repeat
	
    try
        set f to open for access file pref_path() with write permission
        set eof f to 0
        write prefText to f
    end try
    close access f
end save_pref

on load_pref()
    try
        set f to open for access file pref_path()
        set prefList to read f using delimiter {","} as text
    on error
        set prefList to {"", "", "", "", ""}
    end try
    close access f
    prefList
	
    set lastName to item 1 of prefList
    set lastPosition to {item 2 of prefList, item 3 of prefList}
    set lastSize to {item 4 of prefList, item 5 of prefList}
end load_pref

on _name()
    lastName
end _name

on set_name(aName)
    set lastName to aName
end set_name

on _position()
    lastPosition
end _position

on set_position(aPosition)
    set lastPosition to aPosition
end set_position

on _size()
    lastSize
end _size

on set_size(aSize)
    set lastSize to aSize
end set_size
  • 「~/Library/Scripts/window_operation*2」というフォルダを作って、すべてのスクリプトはそこに登録した。
  • QuicksilverのTriggers設定は、自分では以下のようにしてみた。


以上で作業完了。これで自分のウィンドウ操作にまつわる不満は、かなり解消した!Quicksilverによるキー操作でのAppleScript呼び出しは、予想以上に快適だ!

解決できない問題

  • フローティングウィンドウがあると、編集中のメインウィンドウよりも優先して操作対象になってしまい、肝心のメインウィンドウが操作できない。
  • その場合の対応策としては、今のところフローティングウィンドウを閉じるしかない。(それならマウスで操作した方が早いじゃないかという感じ)
  • Aptana StudioとNeo Officeで操作できないことが判明...。何故だろう?GUIスクリプティング利用しているのに...。どうもjavaベースのアプリケーションがダメなようですね。
      • 日記のタイトル変えないとダメかな...。「MacBooKのあらゆるウィンドウ(javaベースアプリケーションは除く)...」

ダウンロード

  • 上記スクリプトダウンロードできるようにしておきました。
  • 解凍するとwindow_operationフォルダが出現するので「~/Library/Scripts/」へ移動すればインストール完了です。

環境

  • MacBook OSX 10.5.3環境で作成。同じ環境なら問題なく利用できると思います。(ちなみにPowerBookG4 OSX 10.4.11環境では動作しませんでした。)

*1:「/Applications/AppleScript/AppleScript ユーティリティ.app」を起動して、「メニューバーにスクリプトメニューを表示」をチェックありに。この設定画面でGUIスクリプティングを有効にすることも可能。

*2:全く同じフォルダ名 window_operation にしておかないと、このスクリプトは動かないので注意。