ページ

2009年10月22日木曜日

Safari用独自プラグインを作る(8) - コンテキストメニューにメニュー追加

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

(前回)Cocoaの日々: Safari用独自プラグインを作る(7) - Evernote を class-dump する

これまでの調査を元に Safari のコンテキストメニューへオリジナルのメニューを追加してみよう。

Safari ではコンテキストメニューに表示する NSMenuItem の配列を BrowserWebView の次のメソッドで取得するようだ。

- (id)webView:(id)arg1 contextMenuItemsForElement:(id)arg2 defaultMenuItems:(id)arg3;

Evernote はこのメソッドの置き換えているようなので、今回は同じようにこのメソッドを置き換えてみる。


まずコンテキストメニューに関する処理を受け持つ専用のクラスを用意した。名前は Evernote のそれをまねした。

SXSafariContextMenuSwizzler.h
@interface SXSafariContextMenuSwizzler : NSObject {
}
+ (void)setup;
@end

SXSafariContextMenuSwizzler.m
static SXSafariContextMenuSwizzler* _shared_instance = nil;
+ (void)setup
{
 if (!_shared_instance) {
  _shared_instance = [[SXSafariContextMenuSwizzler alloc] init];

  [_shared_instance swizzleMethodForClass:objc_getClass("BrowserWebView")
   orginalSelector:@selector( 
    webView:contextMenuItemsForElement:defaultMenuItems:)
   alternativeSelector:@selector(
    _sx_webView:contextMenuItemsForElement:defaultMenuItems:)];
 }
}

+[setup] でインスタンスを作り、置換メソッド swizzleMethodForClass:orginalSelector:alternativeSelector を使いメソッドを置き換える。

その置換メソッドはこんな感じ。
-(void)swizzleMethodForClass:(Class)cls orginalSelector:(SEL)org_sel alternativeSelector:(SEL)alt_sel
{
    Method org_method = class_getInstanceMethod(cls, org_sel);
 class_addMethod(cls, alt_sel,
  class_getMethodImplementation([self class], alt_sel),
  method_getTypeEncoding(org_method));

    Method alt_method = class_getInstanceMethod(cls, alt_sel);
    method_exchangeImplementations(org_method, alt_method);

}
メソッドを追加した後、method_exchangeImplementations() でメソッドを置換している。class_addMethod( ) の引数は若干ややこしいかもしれない。


続いて置換した代替メソッド。
- (NSArray *)_sx_webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element
defaultMenuItems:(NSArray *)defaultMenuItems {
 
 NSArray* menu_items = [self _sx_webView:sender
     contextMenuItemsForElement:element
         defaultMenuItems:defaultMenuItems];
 
 NSMutableArray* new_menu_items = [NSMutableArray arrayWithArray:menu_items];

 [new_menu_items addObject:[NSMenuItem separatorItem]];

 NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:@"Say Hello"
     action:@selector(sayHello:)
  keyEquivalent:@""] autorelease];
 [item setTarget:_shared_instance];
 [new_menu_items addObject:item];
 return new_menu_items;
}

最初にオリジナルメソッドを呼び出して MenuItem の配列を取得し、そこへ独自の MenuItem を追加する。ターゲットには+[setup]でとっておいた _shared_instance を割り当てる。

続いてメニューが選択された時の処理。ここではログへ文字列を表示してみた。
- (void)sayHello:(id)sender
{
 NSLog(@"Hello !");
}

最後にコントローラ。プラグインが読み込まれ +[load] が呼び出されたら、メソッドの置換を行う。
static BOOL initialized_flag = NO;
+(void)load
{
 if (!initialized_flag) {
  initialized_flag = YES;
  [SXSafariContextMenuSwizzler setup];
  NSLog(@"+[PluginController load] was called");
 }
}

さて実行してみよう。右クリックでコンテキストメニューを表示させる。

出た。

"Say Hello" を選ぶ。



"Hello !" がログに出た。


これでユーザからの操作を受け取る入り口が一つ確保できた。


ソースコード:SafariPlugInStudy-1.zip


----------------
(参考情報)
下記で公開されている SIMBLプラグインのソースコードも参考になった。
[SIMBL]Safari のコンテキストメニューから項目を削除するプラグイン書いた - Yarukidenized:ヤルキデナイズド
それと GitHub が便利そうだ。ダウンロードせずにソースコードが読めるのがいい。このブログのソースコードもいつか移していきたい(その前に Git を習得しなければならないが)。