2010-12-09

アプリに Apple イベントの処理を作り込む - アプリを開くリンク #2

前回(→「アプリを開くリンク #1」)に引き続き、Launch Services が送ってくる Apple イベントを処理するための作り込みについて調べた。簡単なサンプルアプリを作って動作を確認した。

以下では、WebView でアプリの独自スキームを持った URL へのリンクをクリックしたときに、アプリに送られてくる get URL という Apple イベントを処理するための作り込みの手順を説明する。

今回の記事のネタは主に「Cocoa Scripting Guide: How Applications Handle Apple Events」から拾っている。

Cocoa アプリにおける Apple イベントの処理

Automator.app を使ってアプリを使った作業の自動化(→「AppleScript を使って URL を Safari のタブで開く」)をしようとしてみると、アプリの中には Apple Script でさまざまな制御が可能なものと、そうでないものがあることに気付く。Safari などは前者で、ウィンドウのサイズを変えたり、指定した URL を開かせたりといったことが Apple Script から制御できる。一方、Cocoa Emacs のように限られた制御(アプリの起動等)しかできないアプリもある。前者のように様々な制御を受けるアプリのことを「スクリプト制御可能(scriptable)」だと言う。

アプリがスクリプト制御可能なように作られているかどうかに関係なく、Cocoa は Mac OS X が GUI アプリに送る必須イベントに対するデフォルトの処理(ハンドラ)を提供している。一番、わかりやすい例はアプリを起動する open application イベントだろう。このデフォルトの実装があるおかげで「Apple Script やイベントのことなんて知らない」と言うアプリでも、Launch Services から起動することができる。また、デフォルトハンドラはデリゲートの仕組みを使っており、NSApplication のデリゲート(NSApplicationDelegate プロトコルを実装したクラス)でデリゲートメソッドを定義してやればハンドラの動作をカスタマイズすることができる(イベントとデリゲートメソッドの組み合わせについては「Cocoa Scripting Guide: How Applications Handle Apple Events」を参照のこと)。

ただ、Cocoa が提供するデフォルトのイベントハンドラは限られていて、具体的には「アプリの起動」「アプリの再起動(アクティベート)」「ドキュメントを開く」「印刷」「コンテンツを開く」、そして「アプリの終了」だ。残念ながら URL を開くイベント(get URL)に対するハンドラはデフォルトの実装には含まれていない。これについては、アプリが独自ハンドラとして追加しなければならない。

Apple イベントハンドラの追加

イベントハンドラの追加はそれほど難しいことではない。特定の形のメソッドを定義し、それをハンドラとしてイベントマネージャ(NSAppleEventManager)に登録するだけで良い。

ハンドラは以下の形にする。ただ、handleAppleEvent の部分はイベントに応じて名前を変えても良いようだ。たとえば、後述するサンプルアプリで定義したハンドラではここは handleGetURLEvent となっている(これは「Cocoa Scripting Guide: How Applications Handle Apple Events」の Installing a Get URL Handler で例として使われている名前でもある)。一方で、Mac OS X Reference Library のサンプルコードでは同じイベントのハンドラに対して handleOpenLocationAppleEvent という名前が付いている。引数の数と型が一致していれば良いのかも。

- (void)handleAppleEvent:(NSAppleEventDescriptor *)event
          withReplyEvent:(NSAppleEventDescriptor *)replyEvent;

Cocoa Scripting Guide: How Applications Handle Apple Events」には具体的なハンドラの定義は載っていなかったため、Mac OS X Reference Library を検索したところ、サンプルコードに get URL イベントのハンドラの例を見つけた(→ CoreRecipesApp/AppDelegate.m)。次に示すサンプルアプリのハンドラの定義は、ここから流用したものだ。

サンプルアプリ

独自スキームの定義

以下は前回の記事(→「アプリを開くリンク #1」)に載せたスクリーンショットだが、再掲しておく。このサンプルアプリが受け付ける独自 URI スキームに sample を設定している。

Info.plist に URL タイプを追加する様子
URI スキームとして sample という文字列を指定している。
Get URL ハンドラの定義

ハンドラの定義とその登録は、ドキュメントの推奨どおりにアプリケーションデリゲイトで行っている。以下はその部分の抜粋だ。applicationWillFinishLaunching: で、Get URL ハンドラ handleGetURLEvent:withReplyEvent を登録している。

- (void)applicationWillFinishLaunching:(NSNotification *)notification
{
    NSAppleEventManager *appleEventManager = 
    [NSAppleEventManager sharedAppleEventManager];
    [appleEventManager setEventHandler:self 
                           andSelector:@selector(handleGetURLEvent:withReplyEvent:)
                         forEventClass:kInternetEventClass
                            andEventID:kAEGetURL];
}

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
 // Insert code here to initialize your application 
}

// Apple event handler
- (void)handleGetURLEvent:(NSAppleEventDescriptor *)event
           withReplyEvent:(NSAppleEventDescriptor *)replyEvent
{
    NSAppleEventDescriptor *directObjectDescriptor =
    [event paramDescriptorForKeyword:keyDirectObject];

    if (directObjectDescriptor) {
        NSString *urlString = [directObjectDescriptor stringValue];
        if (urlString) {
            NSURL *objectURL = [[NSURL alloc] initWithString:urlString];
            if (objectURL) {
                NSString *host = [objectURL host];
                NSString *path = [objectURL path];
                NSString *description =
                [NSString stringWithFormat:@"%@ in (%@)", path, host];
                [appController updateContent:description];
            }
        }
    }
}

このハンドラは受け取ったイベントの内容から URL を取り出し、さらにそこからホスト名とパスを抽出して、WebView を抱える AppController に渡している。

AppController では、イベントハンドラから受け取った文字列を WebView に渡す HTML に埋め込み、再表示させている。以下にその実装部(AppController.m)を示す。

#import <WebKit/WebKit.h>

#import "LaunchAppAppDelegate.h"
#import "AppController.h"

NSString * const HTMLTemplate = 
@"<html>\n"
@"<body>\n"
@"<h1>%@</h1>\n"
@"<ul>\n"
@"<li>Requested: %@</li>\n"
@"</ul>\n"
@"<p><a href='sample://1234567890/0987654321'>Click me!</a>"
@"(sample://1234567890/0987654321)</p>\n"
@"</body>\n"
@"</html>\n";

@interface AppController (PrivateMethods)
- (void)renderHTML:(NSString *)html;
@end


@implementation AppController
- (void)awakeFromNib
{
    appDelegate.appController = self;
    [self renderHTML:[NSString
                      stringWithFormat:HTMLTemplate, @"Sample App", @""]];
}

- (void)updateContent:(NSString *)request
{
    [self renderHTML:[NSString
                      stringWithFormat:HTMLTemplate, @"Sample App", request]];
}

@end

@implementation AppController (PrivateMethods)
- (void)renderHTML:(NSString *)html
{
    [[display mainFrame] loadHTMLString:html
                                baseURL:[NSURL URLWithString:@"http://localhost"]];
}
@end

HTMLTemplate の定義では見易くするために文字列オブジェクト定数を並べて置いている。これらはコンパイル時に連結され 1 つの文字列オブジェクト定数となる。

動作確認

クリックする前と後のスクリーンショットを示す。これにより、アプリの抱える WebView からそのアプリの独自 URL へのリンクをクリックすることで、実行中のアプリ自身にイベントを飛ばせることが確認できた。MacBloggerGlass で内部リンクの置き換えを実現できる目処が立ったことになる。

WebView で独自 URL をクリックした際の動作確認
クリック前
「Requested」の横には何も表示されていない。Click me! をクリックすると……
クリック後
「Requested」の横に URL にふくまれていた情報が抜き出されてきた。

関連リンク

関連記事

0 件のコメント:

コメントを投稿