ページ

2010年1月8日金曜日

BlogAssistant(4) - プラグインのプロトタイプ作成

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

(前回)Cocoaの日々: BlogAssistant(3) - GUIプロトタイプ

前回まで作ったコードを Safari プラグインに組み込む。今回はコンテキストメニューにテスト用のメニューを追加し、それを選択するとウィンドウを表示するようにしてみた。

クラス構成

構成はこんな感じ。※先頭の [Models]などは分類名。

[Models] CoreDataの管理およびモデルクラスなど
  |
  |-- Resource          モデル
  |-- ModelController   CoreData管理

[Viewer] ビューアウィンドウのクラス群
  |
  |-- ViewerController  ビューコントローラ(NSWindowControllerのサブクラス)
  |-- Viewer.xib        Xib/Nibファイル
  |-- CustomCell        カスタムセルクラス
  :      :

[Plugin Classes] プラグインクラス群
  |
  |--SXSafariContextMenuSwizzler  Safariコンテキストメニュー置き換えクラス
  |--PluginController             プラグインコントローラ(メインの制御を担当)

PluginController がメインの制御を受け持ち、ここがプラグイン動作すべてのコントロールを行う。動作イメージはこんな感じ。

[Safari]
  ↓
PluginController ー→ ModelController ー→ [CoreData Stack] 
  ↓               ↑   |
ViewerController ー+    |
  |                    ↓
  |                 NSManagedObjectContext
  ↓                    ↑
Viewer.xib       ー+    |
CustomTableView  ー+→ Resource(モデル)
CustomCell*      ー+

PluginControllerの初期化コード。Safari が最初に +[initialize] を呼び出すところから始まる。

PluginController.m

static BOOL _initialized_flag = NO;
static PluginController* _shared_instance;
+ (void)initialize
{
if (!_initialized_flag) {
_initialized_flag = YES;

// (1) PluginController
// #???: The instance will live until Safari shut down
_shared_instance = [[PluginController alloc] init];

// (2) SXSafariContextMenuSwizzler
[SXSafariContextMenuSwizzler setup];
NSMenuItem* item;
item = [[[NSMenuItem alloc] initWithTitle:@"test"
action:@selector(test:)
keyEquivalent:@""] autorelease];

[item setTarget:_shared_instance];
[SXSafariContextMenuSwizzler addMenuItem:item];

NSLog(@"BLogAssistant was loaded");
}
}

- (id)init
{
self = [super init];
if (self) {
viewerController = [[ViewerController alloc] init];
}

return self;
}



初期化時に必要なインスタンスを作成する。なお ViewerController は NSWindowController のサブクラスで、インスタンス化時に Viewer.xib を読み込む(正確には -[window]メソッドを呼んだとき)。


Safari コンテキストメニューへの追加

Safari のコンテキストメニューへの追加は Method Swizzling という手法を使う。この方法については過去の記事で解説があるので必要な方は参照されたい(右の検索窓で "swizzling" で検索する)。

今回は過去作成した SXSafariContextMenuSwizzler というクラスを流用する。

(参考)
Cocoaの日々: Safari用独自プラグインを作る(10) - コンテキストメニューの調査
Cocoaの日々: Safari用独自プラグインを作る(11) - スクリーンショットを撮る

Swizzling の内容は以前の記事に譲るとして、今回変更を入れた部分はメニューアイテムを外部から登録できるようにしたところ。

SXSafariContextMenuSwizzler.m

static NSMutableArray* _menuItems;


- (NSArray *)_sx_webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element
defaultMenuItems:(NSArray *)defaultMenuItems {
  :

for (NSMenuItem* item in _menuItems) {
[item setRepresentedObject:sender];
[new_menu_items addObject:item];
}
return new_menu_items;
}





+ (void)addMenuItem:(NSMenuItem*)menuItem
{
[_menuItems addObject:menuItem];
}


メニュー配列(_menuItems)が Global変数(疑似クラス変数)になっているのは、実行時にはメソッド入れ替えのために self が SXSafariContextMenuSwizzler のインスタンスではなく、Safari のクラス BrowserWebView のインスタンスになるため。SXSafariContextMenuSwizzler でインスタンス変数を定義しても実行時には参照できない。ややこしいのだが Method Swizzling とはそういうもの(それを忘れていて実は最初インスタンス変数を使って少々ハマってしまった)。実際メソッド内で self を表示させると次のようになる。

Safari[2521] self: <BrowserWebView: 0x4c31c0>


メニューで test が選択されたらログへ -[representedObject] の戻りを表示し、HUDウィンドウを表示する。-[representedObject] からは BroserWebView のインスタンスが取得できる(これはメニューを作る時に仕込んでおく〜SXSafariContextMenuSwizzler.m を参照)。これを後で使って表示中の URL やタイトル、表示view などの情報を得る。

PluginController.m

-(void)test:(id)sender
{
NSMenuItem* item = (NSMenuItem*)sender;
NSLog(@"test: %@", [item representedObject]);
[viewerController window];
}


実行

ビルドしたプラグインを Safari へ組み込んで実行してみよう。Safariを立ち上げる。

右クリックでコンテキストメニューを出す。"test" が最後に現れる。

なおプラグインのクラスが読み込まれるまでにはタイムラグがあって、右クリック初回は出てこない。1回目の後、数秒経ってから右クリックするとようやく出てくる。

"test" を選択するとHUDタイプ(半透明黒)のウィンドウが表示される。




デバッグ

プラグインのデバッグには NSLog( ) を使う。NSLog( ) の出力は コンソール.app というアプリケーションで確認することができる。コンソール.app はアプリケーションフォルダのユーティリティ配下にある。

Safari以外のすべてのログがここに表示されるので、右上のフィルタで Safari 関係のログだけを表示するようにする。

出力はこんな感じ。







(参考)
Cocoaの日々: Plug-in開発で検証・テスト用環境を作る〜 Xcodeで複数のターゲットを設定する

- - - -
HUDウィンドウは、NSWindowController + Xib の組み合わせで作っておいたので、組み込みが数行で済んでしまい非常に楽。データソース(今回はCoreData)との接続も ModelControllerを用意しておけばそれを使ってコントローラ内部で自分で接続してくれる。ModelController はCoreData Stackを扱うシングルトン。ここへ問い合わせれば NSManagedObject が手に入るようにしてある。こんなクラスを用意しておくとNSManagedObjectContext への参照でインスタンス変数を持つべきか、とかで悩まなくて済む。

この辺りのコンポーネント化は慣れると楽で気分がいい。そういえば iPhoneプログラミング もそんな感じだっけ(あっちは UIViewController + Xib の組み合わせが基本)。


ソースコード

GitHub からどうぞ
xcatsan's BlogAssistant at 2009-01-08 - GitHub
(あ、年号古いままだ。。)