load nib

なんで、いまさら Mac OS X 10.3.9 と Mac OS X 10.4 をいったりきたりしているか?

Xcode 2.1...というより、AppleScript Studio が問題なんですね。新しいことはいいことだ、とばかりにインストールしたのはいいものの使いにくくない(=バグが多くない)でしょうか?。

AppleScript Studio を使っていると、挙動がおかしいところがよくある。スクリプティング自体が悪いのか、Xcode の問題なのか、AppleScript Studio か...どれかよく分からない。で、10.3.9 上の Xcode 1.5 を使って確認のためにいったりきたり。

Xcode ではなく、AppleScript Studio 1.4 の問題な気もします。具体的には、次のようなコード。

property notifWindow : missing value

on clicked theObject
    if name of theObject is "debug" then
        if notifWindow is missing value then
            load nib "Notification"
            set notifWindow to window "notification"
            show notifWindow
        else
            show notifWindow
        end if
    end if
end clicked

ウィンドウにボタンがあってクリックすると nib ファイルをロードしてその nib のウィンドウを表示する...ロードしたウィンドウを閉じて、再度ボタンをクリックしてウィンドウを表示させる。こういったことをしたいわけです。上記を実行すると、ウィンドウを閉じて再度表示するときにエラーになります。

このコード自体は珍しいものでもなく Dev Tools のサンプルの中にもありますし、今まで正常に動いていました。上記のコードを以下のようにすると動きます。

property notifWindow : missing value

on clicked theObject
    if name of theObject is "debug" then
        if not (exists notifWindow) then
            load nib "Notification"
            set notifWindow to window "notification"
            log ("Nib loaded")
            show notifWindow
        else
            log ("Nib unloaded")
            show notifWindow
        end if
    end if
end clicked

if の条件文を変更しただけです。実行すると分かるのですが、毎回ロードしています。プロパティの値を調べても、最初に nib をロードしたときのウィンドウの参照がちゃんと入っている。にもかかわらず、二度目の呼び出しでウィンドウがないと判断されてしまいます。もちろん、ウィンドウを閉じたときに解放しているのではありません。

結論。悪いのは、AppleScript Studio 1.4。また、Xcode にも問題はいっぱいあります。AppleScript からの操作がおかしい...ということがよくあります。特に、Xcode 2.1。

Xcode 1.5 は、まだまだ手放せそうにないです。

ハンドラを引数で渡す?

Perl や JavaScript や C 言語や...その他いろいろなプログラム、スクリプト言語では関数ポインタを使って汎用的な関数を作ることがあります。AppleScript で同じようなことができないか...と常々考えていました。

AppleScript は、ハンドラを変数に入れることができるのでこれが使えるかと思いましたが、上手くいきません。しかし、スクリプトオブジェクトを使えば関数ポインタのようなことができます。

Script Editor で開く

script MaxObject
    on comp(x, y)
        return x is less than y
    end comp
end script

script MinObject
    on comp(x, y)
        return x is greater than y
    end comp
end script

on run
    minOrMax({19, 16, 13, 10, 15, 20, 8}, MinObject)
end run

on minOrMax(theList, theObject)
    set firstItem to item 1 of theList
    set restList to rest of theList
    repeat with i from 1 to count of restList
        if theObject's comp(firstItem, item i of restList) then set firstItem to item i of restList
    end repeat
    return firstItem
end minOrMax

minOrMax() ハンドラは、リストの中の最小値(もしくは、最大値)を返すハンドラです。このハンドラにスクリプトオブジェクトを渡し、スクリプトオブジェクトで定義しているハンドラを呼び出しています。関数ポインタという感じではないですが...他に適切な呼び方を知りません。こういう方法をなんというのでしょう?

肝は、スクリプトオブジェクト内で同じ名前の比較ハンドラ(comp() ハンドラ)を定義していることです。こうすることで同一名のハンドラを複数作成することができ、かつ、渡すスクリプトオブジェクトによりハンドラの挙動を変えることができます。これって、よく知られているスクリプトオブジェクトの使い方なんでしょうか?

スクリプトオブジェクトは、まだまだ便利に使えそうです。もっと、研究しなくては...。

スリープの解除を検出

AppleScript でスリープから復帰したことを調べられないかと思い、探してみる。

なんのことはない。system.log にスリープとスリープ解除は書き出されているのですね。で、次のようなスクリプト。

Script Editor で開く

property theRes : ""

on run
    set theRes to do shell script "grep -i 'system wake' /var/log/system.log"
end run

on idle
    set tmp to do shell script "grep -i 'system wake' /var/log/system.log"
    if tmp is not theRes then
        set theRes to tmp
        display dialog "Wake up!" buttons {"OK"} default button 1 giving up after 15 with icon 1
    end if
    return 1
end idle

これだけでスリープ解除を調べることができました。詳しく動作の検証を行っていないのでもしかしたら動かなくなったりするかもしれません。

ちなみに、

Script Editor で開く

do shell script "grep -i 'system sleep' /var/log/system.log"

とすることでスリープを検出することができますが、何か動作を行う前にスリープしてしまい、スリープが解除されたときにスクリプトが実行されるのでこの方法は使えません。

AppleScript Studio 1.4 の Data View

AppleScript Studio 1.4 になって Data View(Table View と Outline View)の機能が強化されました。AppleScript Studio の機能強化は、Data View がほとんどだ...という素朴な感想はおいておくとして。

従来は、table view に新しい項目を追加するときは、新しい data row を作成し、その data cell のそれぞれの内容を設定していきました。

tell table view 1 of  scroll view 1 of window 1 
    set theRow to make new data row at end of data rows of data source 1 
    set content of data cell "name" of theRow to "新しいデータ"
end tell

こんな感じですね。新しく作り、内容を設定...ということを行うためどうしても時間がかかっていました。

AppleScript Studio 1.3 で table view にデータを追加するのに append という命令が追加されました。この命令を使うと data source にデータを追加するのに AppleScript のレコードを利用できます。

set theRecord to {{name:"新しいデータ"}}
tell table view 1 of scroll view 1 of window 1
    append data source 1 to theRecord
end tell

データの追加が簡単になり、より早くなったわけです。ただ、このようにデータの追加は早くなったのですが、問題がないわけではありませんでした。このようにレコードでデータを追加したなら、レコードでデータを取得したい...そう思ってもできなかったのです。

そして、AppleScript Studio 1.4 では、ようやくこれができるようになりました。table view に追加した項目を content(contents)属性で一括して設定/取り出しができるようになったのです。取り出しの際に data source の属性 returns records の真偽値を変更することでリストで取り出したり、レコードで取り出したりができます。

1.3 までは、このデータの設定が append 命令でレコードを利用できても同じ形式(リスト、レコード)でのデータの取り出しができなかったため、データの保存や復元、検索が面倒だったのです。が、1.4 でようやく AppleScript になじんだ方法でデータを扱えるようになり、速度的にも申し分ありません。

-- テーブル内のデータを保持するリスト
-- たとえば、以下のような感じ。
-- {{"myName","myAddress","myAge"},{"myName","myAddress","myAge"},...}
-- このリスト内にデータが5000項目あったとして...
property : theList

tell table view 1 of scroll view 1 of window 1
    set update views of data source 1 to false
    set contents to theList
    set update views of data source 1 to true
end tell

データにもよるのでしょうが 1 〜 2 秒で追加完了します。

1.4 の Data View は、いちいち data row を作成して data cell の内容を設定して...ということをしないでもいいですし、アプリケーションの起動時に data source を作成するということをしなくてもいいようになっています。おこなうことは、Interface Builder で Table View(Outline View)を配置してそれぞれ列の identifier を設定するだけです。そして、Table View(Outline View)のイベントハンドラ awake from nib を利用して次のように初期化します。

on awake from nib theObject
    set content of theObject to {}
end awake from nib

これだけで data source が作られて初期化完了なのですから、なんとも便利になったものです。新規に data row を追加するときは、もちろん append 命令を利用します。

iTunes のイコライザ

JAM LOG さんのところで、「iTunesのイコライザのパーフェクト設定」なる記事を見て衝動的に作ってしまいました。

Script Editor で開く

tell application "iTunes"
    activate
    set newPreset to make new EQ preset
    set name of newPreset to "Perfect"
    set band 1 of newPreset to 3
    set band 2 of newPreset to 6
    set band 3 of newPreset to 9
    set band 4 of newPreset to 7
    set band 5 of newPreset to 6
    set band 6 of newPreset to 5
    set band 7 of newPreset to 7
    set band 8 of newPreset to 9
    set band 9 of newPreset to 11
    set band 10 of newPreset to 8
    --preamp is -6.0?
    set preamp of newPreset to -6
    set current EQ preset to newPreset
end tell

JAM LOG さんのところから MacBSの日常生活的日記さんをのぞいて、結果、プリアンプも設定しています。どちらがいいかは聞く人次第。

確かに音がくっきりとしますね。

print ハンドラ

どれだけの人が知っているのでしょうか?AppleScript には、run ハンドラや idle ハンドラ、reopen ハンドラ、quit ハンドラといった標準で実装しているいくつかのハンドラがあります。

これらのハンドラの中に print というハンドラがあります。print という名前ですので印刷を行うのかと思いますが、違います。print ハンドラはスクリプトアプリケーション同士で通信を行うハンドラです。たとえば、以下のようなスクリプトを「実行後、自動的に終了しない」にチェックを入れてアプリケーションとして保存します。

Script Editor で開く

property counter : 0

on idle
    set counter to counter + 1

    return 10
end idle

on print
    return counter as Unicode text
end print

このアプリケーションを「Counter」という名前で保存したとします。このアプリケーションを起動させておきます。そして、以下のようなスクリプトを実行します。

Script Editor で開く

tell application "Counter"
    print
end tell

Counter アプリケーションは、idle ハンドラで 10 秒ごとに counter の値を増やしていきます。ですので、10 秒ごとに増えた値が結果として返ってきます。また、print ハンドラには引数を渡すことができるので次のようなこともできます。先ほどの Counter アプリケーションを次のように修正します。

Script Editor で開く

property counter : 0
property myName : "Counter"

on idle
    set counter to counter + 1

    return 10
end idle

on print {nameOption, greetingOption}
    if nameOption and greetingOption then
        return "Hello, my name is " & myName & ". " & counter & " counted."
    else if nameOption and not greetingOption then
        return "My name is " & myName & ". " & counter & " counted."
    else if not nameOption and greetingOption then
        return "Hello, " & counter & " counted."
    else
        return counter as Unicode text
    end if
end print

次のように呼び出します。

Script Editor で開く

tell application "Counter"
    print {false, true}
end tell

だからどうだというサンプルなのですが。print ハンドラは、idle ハンドラのようにアプリケーションとして保存し、かつ、実行後終了しない常駐アプリケーション形式でないと使えないハンドラです。スクリプト間通信を行いたいときに手軽に使えるので使い道があると思うのですが、いまだかって print ハンドラが入ったスクリプトを見たことがなかったり...。