夏ライオンにメッセージフィルタリングをつけるハック

@akrさんがつくっている、夏ライオンというTwitterクライアントに、表示されるメッセージを特定の条件でフィルタリングするハックをしてみました。

何ができるか

特定のユーザのメッセージだけを表示したり、

ある言葉が含まれるメッセージだけを表示したりできます。

キモ

  • どこから手をつけるか
  • NSPredicate をつかう

長くなるので、続きは以下で。

経緯

Twitterでこんな発言をしてから できるまで。4時間くらい。

準備

以下は、夏ライオン rev309 にてのお話。

夏ライオンのソースを、SVNからcheckoutします。

svn checkout http://www.physalis.net/repos/natsulion/trunk/

ぼくのところではそのままだとXcodeでビルドできませんでした。以下で、問題とその対処を記します。

  • ‘pbxcp: MainMenu.nib: No such file or directory’
    • 日本語ローカライズの nib がないぽいので、Xcodeプロジェクトから MainMenu.nib (Japanese) と Welcome.nib (Japanese) を消しました。
  • ‘error: ‘for’ loop initial declaration used outside C99 mode’
    • C99拡張をつかってる (for (int i=0; i<N; i++) みたいな) ので、Project Info → Build から、”GCC 4.0 – Language” → “C Language Dialect” の値をC99 にしました。
  • ‘error: PSMTabBarControl/PSMTabBarControl.h: No Such file or directory
    • PSMTabBarControl というフレームワークを昔つかってた名残りだそうです。現在はつかわれていないので、#import を消してOK。
  • リンカエラー (_BIO_trl とか)
    • SSLぽいと感じたので、Project Info → Build → Linking → Other Linker Flags に、 -lcrypto をつけてOK。

これで、ビルドがとおるようになりました。

改造する

さて、メッセージのフィルタリングをどうやって実現するのか。

まず、夏ライオンはCocoaのMVCパターンに沿ってつくられているので、メッセージのModel/Viewに対するフィルタリングはControllerでやってるんだろうなという予測をしました。 ぐぐってみると、NSPredicateというものをつかうことで、Model/Viewに対するフィルタリングができるぽい。@akrさんにも確認

じゃあ、あとはユーザにフィルタのクエリを書いてもらうダイアログを表示し、その入力からNSPredicateをつくってModel/Viewに適用し、Viewを更新すればOKじゃなかろうかと考えました。

やってみた

NTLNFilterControllerというクラスとそのインスタンス、FilterWindowというフィルタクエリ入力のダイアログウィンドウをInterface Builderで作成し、ちょこちょこっとコードを書いたらできあがり (90行程度)。Interface Builder のつなぐだけ開発はすばらしいですね。

今回は、フィルタを入力するトリガとしてメニューを使いました。ウィンドウにコントローラを配備してもよかったのですが、現在の夏ライオンの設計からちょっとそれてしまうので今回は見送りに。

パッチ

nibファイルのパッチをつくるのは容易ではないので、上の図を参考にしてつないでみてください。

NatsuLion message filtering patch

diff -x .svn -x framework -x build -N -r natsulion-orig/src/NTLNFilterController.h natsuLion/src/NTLNFilterController.h
0a1,23
> //
> //  NTLNFilterController.h
> //  NatsuLion
> 
> #import <Cocoa/Cocoa.h>
> 
> 
> @interface NTLNFilterController : NSObject {
> 	id mainWindow;
> 	id filterWindow;
> 	id filterText;
> 	id messageListViewsController;
> 	id messageTableViewController;
> }
> 
> - (void) filter:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo;
> - (IBAction) openDialog:(id)sender;
> - (IBAction)dialogOk:(id)sender;
> @end
diff -x .svn -x framework -x build -N -r natsulion-orig/src/NTLNFilterController.m natsuLion/src/NTLNFilterController.m
0a1,39
> //
> //  NTLNFilterController.m
> //  NatsuLion
> //
> 
> #import "NTLNFilterController.h"
> #import "AppKit/NSApplication.h"
> #import "NTLNMessageListViewsController.h"
> 
> @implementation NTLNFilterController
> 
> - (IBAction) openDialog:(id)sender {
> 	[[NSApplication sharedApplication]
> 		beginSheet:filterWindow
> 		modalForWindow:mainWindow
> 		modalDelegate:self
> 		didEndSelector:@selector(
> 			filter:returnCode:contextInfo:)
> 		contextInfo:nil];
> }
> 
> - (void) filter:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
> 	[filterWindow orderOut:self];
> 	NSString *src = [NSString stringWithFormat:@"message.%@", [filterText stringValue]];
> 	NSLog(src);
> 	[messageListViewsController applyFilter:src];
> 	[messageTableViewController reloadTableView];
> 	NSLog(@"applyFilter finished.");
> }
> 
> - (IBAction)dialogOk:(id)sender {
> 	[[NSApplication sharedApplication]
> 		endSheet:filterWindow returnCode:0];
> }
> 
> @end
diff -x .svn -x framework -x build -N -r natsulion-orig/src/NTLNMainWindowController.h natsuLion/src/NTLNMainWindowController.h
2d1
< #import <PSMTabBarControl/PSMTabBarControl.h>
10a10
> #import "NTLNFilterController.h"
38a39
> 	IBOutlet NTLNFilterController *filterController;
diff -x .svn -x framework -x build -N -r natsulion-orig/src/NTLNMessageListViewsController.h natsuLion/src/NTLNMessageListViewsController.h
16a17
> - (void) applyFilter:(id) sender;
diff -x .svn -x framework -x build -N -r natsulion-orig/src/NTLNMessageListViewsController.m natsuLion/src/NTLNMessageListViewsController.m
133a137,141
> - (void) applyFilter:(id) sender {
>     [messageViewControllerArrayController setFilterPredicate:[NSPredicate predicateWithFormat:sender]];
>     // [messageViewControllerArrayController setFilterPredicate:[NSPredicate predicateWithFormat:@"message.status == 0"]];
> }
>

使い方

Menu → Filter (Cmd-F) すると、フィルタクエリのダイアログが出るので、クエリを書きます。

例:

  • screenName == 'mootoh' # screenName が ‘mootoh’
  • text LIKE '*hack*' # メッセージ本文に’hack’が含まれる

といった具合に指定します。

課題

メインウィンドウにクエリ入力欄を設けて現在のViewに対するフィルタリングをする、とかクエリの書き方をもっと易しくする、などといったものがあります。これは、ここまで読んでくださった方への宿題w

まとめ

  • ソースが公開されてると、好きにカスタマイズできて素晴らしいですね! 夏ライオン++
  • 改造したいという欲求がコードリーディングの力になります
  • 手を入れる場所を探すには、野生のカンとgrep
  • 勉強になります。
    フィルタリング機能が欲しいと発言してから自身で実装してしまうまでの早さに驚きです。



    手を入れる場所を探すには、野生のカンとgrep




    野生のカンは精進するのみですよね。僕もhackしつづけます。

  • akr

    わー、丁寧にまとめていただいてありがとうございます! 詳しくみてみます。

blog comments powered by Disqus