Finder の alias list、使えるみたい

Mac OS X 10.4 の Finder(10.4 以前でも?)では、alias list が使えなかった。alias list は Finder で定義されているクラス。これを使うとファイルやフォルダの参照を Finder の参照ではなく、alias 参照のリストで返してくれる。

Script Editor で開く

tell application "Finder"
    set curSelection to selection as alias list
end tell

いちいち as alias と型変換をしなくていいので便利なのだけど、Mac OS X 10.4 では選択されている項目がひとつだけだとエラーになって動かなかった。また、複数を選択していても alias 参照に変換してくれなかった。だから、Finder の選択項目を処理する時はわざわざ次のように繰り返しで alias 参照に変換していた。

Script Editor で開く

tell application "Finder"
    set curSelection to selection

    if curSelection is {} then return

    set theseItems to {}
    repeat with thisItem in curSelection
        set end of theseItems to thisItem as alias
    end repeat

    theseItems
end tell

Mac OS X 10.5 でもこのバグの名残を見る事ができます。/Library/Scripts/Printing Scripts に入っている Convert to PDF.scpt などのスクリプトを開いて見ると、run ハンドラの最初の方で奇妙な事をしています。分かってて長いことほったらかしだったんですね(Convert to PDF.scpt の作成日が 2003 年なんですから)。

Mac OS X 10.5 の Finder では、alias list がようやく使えるようになりました。きちんと結果が返ってきます。でも、Mac OS X 10.4 が現役な事を考えると、上記のような配慮は必要なんでしょうね。まだまだ。

Safari と System Events と時々 Spaces

私はひねくれ者で、面白いと評判のいいものを旬の時期に手を出さない、流行ものは流行が終わってからこっそり手を出す。『東京タワー ~オカンとボクと、時々、オトン~』しかり。『博士の愛した数式』しかり。なかなか損な性格かもしれない。

いや、特に意味はないのだけど、表題を拝借したから...パクったのではありません。今、個人的に旬な話題なだけで。

閑話休題。Safari の話。バージョンが 3 になって、タブが操作できるようになりましたね。この変更に伴い、do javascript 命令はオプション in でタブを指定するように変更されています。以前まではどのドキュメントかを指定していました。以下のような感じです。

Script Editor で開く

tell application "Safari"
    if exists front document then return do JavaScript "getSelection()+''" in front document

    return ""
end tell

このスクリプトは、このままでも Safari 3 で動きます。そのうち動かなくなるのかな...と思わないでもないです。用語説明にあるように in オプションでタブを指定するなら以下のようになります。

Script Editor で開く

tell application "Safari"
    if exists front window then ¬
        return do JavaScript "getSelection()+''" in current tab of front window

    return ""
end tell

Safari の環境設定でタブを利用しない設定にしていても動きます。tab クラスは window クラスの要素なので、対象ウィンドウのどのタブかを指定する事になります。

さて。このスクリプト。実際には問題があり、必ずしも動くとは限らない。これは AppleScript 2.0 に限った話ではなく、Mac OS X になってからずっとなのですが、。front window や front document という参照が最前面のウィンドウやドキュメントを指さない事があるのです。そのため、最前面のウィンドウで選択文字列があるにも関わらず、結果が空の文字列、またはエラーになる事があります。この現象は、Cocoa アプリケーションにおいて顕著です。

かなり以前から分かっていた問題なのですが、ここで紹介しているスクリプトでは敢えてこの事を無視していました。が、Safari のバージョンアップで do javascript でウィンドウの指定が必要になるなら話は変わってきます。今まではたいていのアプリケーションで document を対象にしていたのでなんとかやりくりしていましたが...それもここまでのようです(実際のところ Xcode でもこの問題に悩まされ、爾来、避けて通るようになったのですが)。

AppleScript は一番前面にあるウィンドウが window 1(= front window)、その後ろに続く順に 2、3、4...と、順番のつけ方が決まっています。他にも every window や every file といった参照を行った時どのような順番でそれぞれの要素が返ってくるかという事も AppleScript では仕様の上で決まっています。基本的には上(手前)からと下(奥)へ。左から右への順番です。これらの順番の付け方は、リストで返ってくる結果に影響します。

front window(front document)と書いて一番後ろのウィンドウの参照が返ってくるのは気持ち悪いし、予期していない結果をもたらすので確実に最前面にあるウィンドウの参照を得たい。が、これは一筋縄ではいかない問題なのです。例えば、以下のようにして全てのウィンドウの参照を得たとします。

Script Editor で開く

tell application "Safari"
    every window
end tell

結果を見ると、表示されていないウィンドウ(環境設定やダウンロード)といったウィンドウの参照まで含まれます。また、Safari でダウンロードをしたときに開かれる「ダウンロード」というタイトルを持つウィンドウは、表示されていなくても front window といった参照で指定されてしまう事もあります。これが front document という参照なら、こういった関係のないウィンドウは除外される事になるのですが。

ちなみに window = document ではありません。document は window の要素で window に含まれるものです。window の中に document があるので every window は全て(表示されていないウィンドウも含む)のウィンドウの事で every document は document を持った全てのウィンドウという事になります。

つまり、document の指定は不要なウィンドウを対象に含まないのでそれだけ目的のウィンドウを指定するのが楽になるのです。全ての window の中から目的のウィンドウを指定するのより遥かに楽です。

少し、整理しましょう。やりたい事は、見えているウィンドウの重なり順で一番手前にあるドキュメントを持ったウィンドウを指定したい。問題は必ずしも一番手前にあるウィンドウが front window(or front document)の参照で取得できない。なぜ、ウィンドウの参照が欲しいのかというと、do javascript 命令でタブの指定を行う必要があるから。タブを指定するにはウィンドウの参照が必要で、かつ、そのウィンドウがドキュメントを持っていないといけない(一律に current tab of front window とすると「ダウンロード」などのウィンドウを指す可能性があり、結果、エラーにより終了する)。

さて、まずは document を持ったウィンドウのみを抽出してみましょう。window クラスは属性に document を持っています。これを利用し、フィルタ参照で抽出しましょう。

Script Editor で開く

tell application "Safari"
    windows whose document of it is not missing value
end tell

これでドキュメントを持ったウィンドウを取得できます(逆に document から window の参照を得る事はできません。これができるといいのですが)。取得できるのですが、どのウィンドウが最前面にあるのかは分かりません(分からないというより、結果が信頼できない)。また、このスクリプトが動くのは Mac OS X 10.5 Leopard の Safari 3 以降、AppleScript 2.0 以降だけかもしれませんし、どの Cocoa アプリケーションでも動く保証はありません。逆に言うと、これら以前の環境では document を持った window を調べる手段がないという事です。

なぜかというと、window クラスが持っている document 属性が何も返さないお飾りの属性になっている場合があるし、また、上記のようなフィルタ参照は動かなかったと思います(Mac OS X 10.4.11 で試してみました。やはり動きませんでした)。古い環境を考えなくていいなら上記の方法でいいのですが、ここでは Mac OS X 10.4 と Mac OS X 10.5 の互換性を考えて document の name 属性を利用します。

Script Editor で開く

tell application "Safari"
    set documentList to name of documents
end tell

これで、ドキュメントを持ったウィンドウの取得はできました。AppleScript を信頼するなら返ってきたリストの最初の要素が最前面のドキュメントになるのですが。そこで、このリストの中から最前面のウィンドウを調べるのですが...。

スクリプタブルな Cocoa アプリケーションにも関わらず、一番手前にあるウィンドウの参照を確実に返すアプリケーションがあります。また、このアプリケーションは windows と記述したときに表示されているウィンドウのみを対象にします。System Events です。

Script Editor で開く

tell application "System Events"
    tell process "Safari"
        name of windows
    end tell
end tell

このスクリプトを実行すると、表示されているウィンドウを重なり順のリストで返してくれます。リストの一番最初にある要素が表示されているウィンドウの中で最前面にあるウィンドウになります。先のドキュメントのリストと System Events で得られるウィンドウのリストを比較すれば、どれが一番最前面のウィンドウかが分かります。具体的には以下のような感じです。

Script Editor で開く

set frontWindow to getFrontWindow("Safari")
if frontWindow is missing value then return

name of frontWindow

on getFrontWindow(applicationID)
    tell application applicationID
        set documentList to name of documents
        if documentList is {} then return missing value
        set documentName to my windowFilter(name of it, documentList)
        if documentName is missing value then return missing value

        set frontWindow to windows whose name of it is documentName
        if frontWindow is {} then return missing value

        return first item of frontWindow
    end tell
end getFrontWindow

on windowFilter(thisProcess, compList)
    tell application "System Events"
        tell process thisProcess
            set windowList to name of windows
            repeat with thisWindow in windowList
                set thisWindow to contents of thisWindow
                if thisWindow is in compList then return thisWindow
            end repeat

            return missing value
        end tell
    end tell
end windowFilter

なんでこんな面倒な事せにゃならんねん、と思わないでもないですが。しかし、このスクリプトは動かない事もあります。Mac OS X 10.5 Leopard の目玉機能の一つ Spaces です。

Spaces で Script Editor と Safari を個別の画面に設定しているのですが、System Events で対象のアプリケーションプロセスを処理する時、そのプロセスが Spaces で別の画面にあると処理が行われないのです。

Script Editor で開く

tell application "System Events"
    tell process "Safari"
        windows
        --> {}
    end tell
end tell

このスクリプトを Script Editor で開き、Spaces で Safari を Script Editor と別の画面に割り当てると、Safari でウィンドウを開いているにも関わらず結果は必ず空になります。これだから System Events 嫌いなんだよ。UI Element を操作する時はプロセスを前面に持ってくる必要があるとか、制限が多過ぎて「確実」、「完璧」そして「美しく」、「華麗に」が好きな私に相容れないんだよ。

Script Editor で動作を確認しながら動かしている時はともかく、スクリプトメニューなどから実行すれば問題はないのですが。結果、Safari で最前面にあるウィンドウの現在のタブで選択している文字列を取得するスクリプトは以下のようになりました。

Script Editor で開く

on run
    try
        set frontWindow to getFrontWindow("Safari")
        if frontWindow is missing value then return

        set textSelection to getSelection(frontWindow)
    on error eMessage number eNumber
        tell application (path to frontmost application as text)
            activate
            display dialog (eNumber & " : " & eMessage) as text buttons {"OK"} default button 1 with icon 1
        end tell
    end try
end run

on getSelection(thisWindow)
    tell application "Safari"
        set selectedText to {}
        set end of selectedText to do JavaScript "getSelection();" in current tab of thisWindow

        set frameNum to do JavaScript "parent.frames.length;" in current tab of thisWindow
        set num to 0
        repeat while num < frameNum
            set theCommand to "parent.frames[" & num & "].getSelection();" as text
            set end of selectedText to do JavaScript theCommand in current tab of thisWindow
            set num to num + 1
        end repeat
        return selectedText as text
    end tell
end getSelection

on getFrontWindow(applicationID)
    tell application applicationID
        set documentList to name of documents
        if documentList is {} then return missing value
        set documentName to my windowFilter(name of it, documentList)
        if documentName is missing value then return missing value

        set frontWindow to windows whose name of it is documentName
        if frontWindow is {} then return missing value

        return first item of frontWindow
    end tell
end getFrontWindow

on windowFilter(thisProcess, compList)
    tell application "System Events"
        tell process thisProcess
            set windowList to name of windows
            repeat with thisWindow in windowList
                set thisWindow to contents of thisWindow
                if thisWindow is in compList then return thisWindow
            end repeat

            return missing value
        end tell
    end tell
end windowFilter

一応、フレームのあるサイトの選択文字列にも対応。以上、「Safari」と「System Events」と「Spaces」による三題噺でした。