WebKit+CocoaでGmail専用ブラウザを作りたいなあ・その1

今までは GmailFirefox の Prism とかいうやつで使っていたけど、ちょっと気にいらないところがあるので軽量な Gmail 専用アプリがほしかった。

Adobe AIR のやつとか色々試したのだけど、どれもイマイチ。

やっぱ自作しかないかなー、とずっと思っていた。

そこで見つけたこの記事!

Objective-C なんてサッパリわかんないけど、NeoCat さんの解説がすばらしすぎるので、なんだか出来るような気がしてきたので作りはじめた。

以下に箇条書きでこれまでやったことをメモっておく。

  • 上の記事からコピペで、なんとか雛形はできた。
  • 初期 URL を http://mail.google.com/mail/ にして、サインイン。
  • すると、Ajax 版ではなく HTML 版に飛ばされてしまう。
    • UserAgent が Safari のものと違うために起こるらしい。
  • UA を変える方法は、こうやるっぽい。
- (void) awakeFromNib
{
	/* ここらへんに最初にコピペしたコードがあるよ */	

	// set UserAgent to that of Safari
	[browser setCustomUserAgent:@"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-us) AppleWebKit/528.16 (KHTML, like Gecko) Version/4.0 Safari/528.16"];	
}
  • これでめでたく Ajax 版ページが表示された!
  • と思ったのもつかの間、ページを開いた瞬間に Gmail が落ちる。
  • Twitter で NeoCat さんに助けを求めたところ、プラグインを読み込んだら落ちるらしい。
  • Gmailプラグインは要らないでしょ、ということでオフに。
  • ついでにフォントが小さすぎたのも直す。


  • "Window" というやつの中に置いた色付きの部分 (WebView 部分) をクリックしといて、Inspector を表示。
  • Inspector の一番左側のタブを、このように変更。
  • NeoCat さんは Java のほうだけチェックをはずせばいいと言っていたけど、僕のほうでは Plugin のチェックをはずさないといけなかった。
    • GmailYouTube のプレビューが見れたりするようになるらしいのでもったいない気もするけど、今のところはこれでいく。
  • これでちゃんと Gmail が見られるようになった。
    • Firefox を置き換えたぜイェイ!
  • 題名が空のメールを送ろうとして、問題が。
    • 何も起こらない!
    • 普通なら、「タイトルが空だけどいいの?」と聞いてくるはず。
  • ちょっと調べたところ、ダイアログ類は自分で作らないといけないらしい。
  • というわけで confirm パネルを実装。
// Javascript confirm
- (BOOL)webView:(WebView *)sender runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WebFrame *)frame
{
	WebDataSource* origin = ([frame dataSource]) ?[frame dataSource] : [frame provisionalDataSource];
	NSString* host = ([[[origin response] URL] host]) ? [[[origin response] URL] host] : @"JavaScript";

	//NSAlert* alert = [NSAlert new];  // same as below??
	NSAlert *alert = [[NSAlert alloc] init];
	[alert addButtonWithTitle:@"OK"];
	[alert addButtonWithTitle:@"Cancel"];
	[alert setMessageText: host];
	[alert setInformativeText: message];
	[alert setAlertStyle:NSInformationalAlertStyle];
		
	if ([alert runModal] == NSAlertFirstButtonReturn) {
		return YES;
	}else{
		return NO;
	}
}
  • Objective-C では true-false じゃなくて YES-NO なんだね。
  • [クラス new] という書き方は [[クラス alloc] init] と同じ意味で、クラスのインスタンスを作る常套手段らしい。
  • JavaScript を実行しているウィンドウ (フレーム) のドメインを取得するのに、あんなまわりくどいことをする必要あるんだろうか?
  • 次に alert ダイアログも作る。
// Javascript alerts
- (void) webView: (WebView*)webView runJavaScriptAlertPanelWithMessage: (NSString*) message initiatedByFrame: (WebFrame*) frame
{
	WebDataSource* origin = ([frame dataSource]) ?[frame dataSource] : [frame provisionalDataSource];
 	NSString* host = ([[[origin response] URL] host]) ? [[[origin response] URL] host] : @"JavaScript";

	//NSAlert* alert = [NSAlert new];  // same as below??
	NSAlert *alert = [[NSAlert alloc] init];
	[alert setMessageText: host];
	[alert setInformativeText: message];
	[alert setAlertStyle:NSInformationalAlertStyle];
	[alert runModal];
}
  • confirm のやつとほぼ同じ。
  • Cocoa では ダイアログ類のことは Panel というらしい。(Panel クラスを継承したオブジェクトで作るらしい)
  • runModal の modal というのは、それが走っている間は親ウィンドウの実行を中断する、という意味らしい。
  • JavaScript によるダイアログは alert、confirm、prompt なので、次に prompt も作ろうと思ったら、これは自分で作らなくても普通にちゃんと出てきたよ!
    • WebKit 中途半端やなあ。
    • 自分で実装することもできるらしい。runJavaScriptTextInputPanelWithPrompt
  • 次に Gmail にとって重要なのが、ファイルをアップロードするダイアログ。
  • これは Cocoa に専用のクラス (NSOpenPanel) が用意されているので、それを使えばいいらしい。
// Open file dialog
- (void)webView:(WebView *)sender runOpenPanelForFileButtonWithResultListener:(id < WebOpenPanelResultListener >)resultListener
{
	// doing with panel
	NSOpenPanel *opanel = [NSOpenPanel openPanel];
	int opRet;
	opRet = [opanel runModalForDirectory:NSHomeDirectory() file:nil types:nil];

	if (opRet == NSOKButton)
		[resultListener chooseFilename:[opanel filename]];
}
  • 上の confirm ダイアログと違うところは、こっちの関数は値 (選択したファイル名) を返すのではなく、resultListener というオブジェクトを生成して、それのメソッドにファイル名を渡すらしい。
  • 何故か。以下は自分なりの解釈。(違ってるかも)
    • たぶん、filename は複数選択することもできたりするし、ファイルだったりディレクトリだったりすることがあるので、返り値で判定しにくい。
      • というか Objective-C は関数が返り値の型を最初に宣言するので、返り値が Array だったり String だったりすることは無理だから。
  • ここまででほぼ十分 Gmail の機能が使えるブラウザが出来上がった。
    • まだ大事な「ダウンロードダイアログ」と「ダウンローダー」の実装が残っている。
  • ちょっと調べたところ、panel は別ウィンドウで表示する (上の3つの例。runModal〜というメソッド) こともできるし、sheet といって親ウィンドウにくっついた形で表示させることもできるらしい。
    • というわけで、さっきのファイル選択ダイアログを sheet にしてみようかな。
      • と考えて行き詰まった。。
// Open file dialog
- (void)webView:(WebView *)sender runOpenPanelForFileButtonWithResultListener:(id < WebOpenPanelResultListener >)resultListener
{
	// open file selection panel as a sheet
	[[NSOpenPanel openPanel] 
		beginSheetForDirectory:NSHomeDirectory() file:nil modalForWindow:window modalDelegate:self didEndSelector:@selector(openPanelDidEnd:returnCode:contextInfo:) contextInfo:nil];
}

- (void)openPanelDidEnd:(NSOpenPanel *)panel returnCode:(int)returnCode contextInfo:(void)contextInfo
{
	if(returnCode==NSOKButton)
		/* ここで選択したファイルを反映させたい! */
		/* [resultListener chooseFilename:[panel filename]]; ← これだと resultListener が無いので無理 */
}
  • sheet にすると、ファイル選択ダイアログの結果をそのメソッドの返り値にするわけではなく、別のイベントリスナーみたいなやつ (didEndSelector なのでセレクターと呼ぶべき?) をセットするらしい。
  • このイベントリスナーみたいな関数は別のスコープなので、resultListener が宣言されていない。
    • ではどうやって結果を返せばいいんだろう??
    • contextInfo に resultListener を入れたりしてアレコレしてみたけど、どうもうまくいかなかった。

↑知ってる人は教えてください。

まあわからなかったらここはこのまま放置するけど。