Cocoa Browser を作る上で使用 (発見) したテクニック (?) を紹介します。 参考になれば幸いです。(間違ってるかもですが。。。)
目次
Cocoa Browser は通常のドキュメントペースのアプリケーションじゃないけど、 複数のウィンドウを開けるようにしたかったので、多分邪道なんだろうなぁと 思いつつ、Document-based Application にしてみました。
ひな型で提供される4つのメソッドはそのままにしてありますが、それで特に 問題はないみたいです。余分な Open... とか Save とかのメニュー項目は 削除すべきかなぁ、という気もしつつ、そのままほったらかしです。
NSTextView ですが、普通にウィンドウいっぱいに配置するとスクロールバーと サイズボックスの位置がずれちゃいますよね。これを直すには、左右と下に それぞれ 1 ピクセルだけ大きくしてやればいいみたいです。
ところが、Cocoa Browser では NSTextView は NSSplitView の管理下にあり、 サイズはいぢれないようです。仕方がないので、ダミーの NSView をかませて みました。
NSBrowser ですが、まず表示に関しては、delegate を設定して、
- (int) browser: (NSBrowser *) sender numberOfRowsInColumn: (int) column;
- (void) browser: (NSBrowser *) sender
willDisplayCell: (id) cell atRow: (int) row column: (int) column;
のふたつを実装すればいいみたい。ちょっとややこしいけど、まぁ何とかなるよね。
で、NSBrowser で何か選択した時に処理を行ないたければ、適当なアクション メソッドを定義して、target と action を設定する必要があります。(さらに、 ダブルクリックを処理したい場合には Interface Builder では指定できなくて、 - awakeFromNil で - setDoubleAction: を呼ぶ必要が、、、)
ここではまったのが、これらが呼ばれる順番。アクションメソッドで選択された やつを憶えておいて、それを使って - browser:numberOfRowsInColumn: などを 実装したらうま動かない。原因はアクションメソッドが表示の後で呼ばれるため。 仕方ないので選択されているものを調べるメソッドを別に用意しましたとさ。
NSTextView と NSAttributedString を組み合わせると、HTML ドキュメントの 表示もできます。
NSData *data = <HTML データ>;
NSURL *url = <ベース URL>;
NSAttributedString *attrString = [[NSAttributedString alloc]
initWithHTML: data baseURL: url documentAttributes: (NSDictionary **) NULL];
[[textView textStorage] setAttributedString: attrString];
[attrString release];
たったこれだけ。なんて簡単な! しかも、ちゃんと baseURL を指定すれば、 相対パスで指定したイメージも表示してくれちゃうし。
ちなみに、完全な HTML である必要もないので、Cocoa Browser では、元の HTML ドキュメントを細切れにして表示させています。もちろん、細切れにする 際に変なところで切らないようにしないといけませんが。
Mac OS X には CVS が標準で付属するし、Project Builder もちゃんと対応 しているので、使わない手はありませんよね。今では何冊も本が出ているので、 基本的な使い方は割愛しますが、いくつか tips を。
まず、環境変数 CVSROOT ですが、checkout などの時だけ cvs -d として 指定すれば、ふだんは必要ありません。というか、そんなもん指定しない方が 複数のリポジトリを切り替えて使えるので、かえって便利だったりします。
それから、cvswrappers も、~/.cvswrappers に置くと全部で有効になっちゃう ので、リポジトリの中の CVSROOT/cvswrappers に置いた方がリポジトリごとに 設定を変更できて便利です。Mac OS X での開発用の cvswrappers のひな型が /Developer/Tools/cvswrappers に用意されていて、これを使うと .nib などの ファイルもちゃんと管理できるようになります。diff は取れないけど。
これを書き換えて .pbproj も .nib と同様に tar で固めて管理するように していますが、せっかくテキストなのにバイナリになっちゃうのが不満。 Project Builder の CVS メニューからも操作できないし。もしかして、 なんか間違ってるのかなぁ?
訂正: .pbproj はディレクトリとして cvs add して、その中の project.pbxproj ってファイルを普通に cvs add するのが正解みたいです。 そうすればちゃんと Project Builder でステータスが表示されるし、 Terminal でなら cvs diff できるし、余計な設定情報も含まれません。 あと、CVS はスペースを含むファイル名は管理できないので、CVS 上では Cocoa-Browser.pbproj という名前だったりします。
NSTextView に HTML ドキュメントを表示させた場合、リンクもそれらしく表示 され、クリックするとデフォルトのブラウザでリンク先に飛んでくれたりします。
これは、リンクをクリックすると NSTextView の delegate に - textView:clickOnLink:atIndex: というメッセージが送られ、このメソッドが 存在しなかったり、NO を返した場合にはデフォルトの処理が行われるようです。
で、Cocoa Browser では Cocoa Reference 内部でのリンクは Cocoa Browser の中で処理して YES を返し、外部へのリンクは NO を返すようにしています。
(リンクの上にマウスカーソルがある場合にカーソルの形状を変える方法は不明。 あと、内部リンクと外部リンクでは色を変えた方がいいかなぁ?)
.nib ファイルがひとつしかない場合にはメニュー項目から直接コントローラに 接続すればいいのですが、.nib ファイルを複数にした場合は?
簡単です。First Responder に目的のメソッドを追加して、そこに接続する だけです。First Responder というのは具体的なオブジェクトというよりも、 responder chain の中の適切なオブジェクトを探してくれる、というもので、 アクティブな view やウィンドウが理解できないメソッドはドキュメントの コントローラに送られることになっています。ので、存在しないメソッドを 新たに定義してやればいいのです。
さらに、デフォルトではメニュー項目を自動的に有効/無効化してくれるので、 例えばウィンドウを全部閉じたらドキュメントのコントローラもひとつも存在 しなくなるので、メニュー項目は無効になります。また、ドキュメントの状態 に応じてメニュー項目の有効/無効を制御したければ、- validateMenuItem: メソッドで行ないます。
NSBrowser で、何も選択されていない状態にするにはどうすれば良いでしょう? - selectRow:inColumn: に -1 とかを渡しても無駄なようです。
仕方ないので、いったん NSBrowser を空っぽにして、元に戻す、という方法を 使っています。具体的には、通常状態かどうかのフラグを用意して、通常状態 でなければ - browser:numberOfRowsInColumn: では 0 を返すようにして、
<フラグ = 通常状態でない>;
[browser reloadColumn: 0];
<フラグ = 通常状態>;
[browser reloadColumn: 0];
としてみました。なんかいまいちな気もするけど、とりあえず目的は達しました。
訂正: そんな面倒なことしなくても、- loadColumnZero いっぱつでした。
Cocoa Browser ver 0.1 をリリース後、何人かに「字がちっちゃい」という 意見をもらいました。確かに、iBook (Dual USB) などではちっちゃくて 読みにくいですね。
でも、HTML を attributes string にする際にフォントを指定する方法は 見当たりません。無理やり <font size="+1"> タグを追加するのも、 NSFontManager に - modifyFont: で NSSizeUpFontAction を送りつけるのも、 いずれも面倒だし、結果もいまいちだし。
で、行き詰まっていたら、NSView にそれらしい記述を発見しました。 なんと、Cocoa では任意の view を拡大/縮小して表示できるらしいのです! 早速 - setBoundsSize: を利用してみたらばっちりでした。ただ、たまに NSRunStorage が例外を起こします。いろいろ試したところ、
[[textView textStorage] setAttributedString: attrString];
の前に
[textView setString: @""];
を入れたらなおりました。
(拡大/縮小するとスクロール位置がずれちゃいます。これをなおすには - characterIndexForPoint: と - scrollRangeToVisible: を使えば良さそう ですが、- characterIndexForPoint: に渡す画面座標ってのが分かりません。 あと、NSBrowser のフォントも拡大/縮小できた方がいいかなぁ?)
Cocoa Browser はソースも公開しています。src.tar.gz ってやつです。 適当な場所に展開して、Cocoa Browser.pbproj を開いてコンパイルすれば いい筈です。
Cocoa Browser ver 0.2 では、Java 版のリファレンスドキュメントも読める
ようにしてみました。ただし、別のアプリケーションになっちゃいますが。
で、この Java 版をコンパイルするには、ClassNode.m の先頭付近にある
#define LANGUAGE_OBJECTIVE_C ってのを削除してください。
開発中は Development ビルドで、リリースの時だけ Deployment ビルドに してるんですが、バイナリのサイズがなんだかとてつもなく大きくなります。 おかしいなぁと思っていたら、 Witness of Teachtext: Tips & Tricks に対処法が載ってました。
方法は、プロジェクトウィンドウの「ターゲット」タブを選択し、ビルド スタイルの Deployment を選ぶと、「ビルド設定」ってのが出てくるので、 そこに DEBUGGING_SYMBOLS = NO ってのを追加すればいいみたいです。 とってもちっちゃくなりました。
クラスやメソッドがいっぱいある時に、名前の一部を入力したら、そこに 飛んでくれたら便利ですよね? ってことで、NSBrowser の delegate に
- (BOOL) _browser: (NSBrowser *) sender
keyEvent: (NSEvent *) event inColumn: (int) column;
とかいうメソッドを追加すればいいと、Max Horn さんが教えてくれました。 ただし、アンドキュメンテッドな API なので、将来の Mac OS X では 動かなくなる可能性があります。
現在選択されている項目があるカラムに対して有効なので、いったん他の クラスやメソッドを選択する必要があるのがちょっといまいちですね。 あと、NSBrowser のフォーカスはなんか変だし。 (それを逆手に取って、Tab や Shift + Tab に機能を割り当てちゃったけど。)
NSSlider のドラッグ中に、その値をリアルタイムに NSTextField に表示 するには、NSSlider から NSTextField に Control + ドラッグして、 - takeIntValueFrom: というアクションを指定すればよくって、簡単簡単。
と思ったら、たまに NSTextField に何も表示されないことがあります。 仕方ないので、いったん MyDocument にアクションを送って、値が変更 された時だけ NSTextField を更新するようにしたら、少し改善されました。 けど、まだちょっと変。なんか失敗してるのかなぁ?
リンクのカーソル変更の方法については、 Mac OS X Dev-jp Mailing List で イシカワさん に教えていただきました。ありがとうございます。
それを応用して、外部リンクの色を変えたり、Magnify や履歴の移動の際に スクロール位置を保存するようにしました。
その際、NSTextView のサブクラスを作ったけど、Interface Builder では NSTextView のサブクラスは指定できません。(他のクラスはできるのに。) CustomView でサブクラスを指定して、属性はプログラムから設定するのは 嫌だったので、プログラムで NSTextView をサブクラスと入れ換えて、 属性もコピーするようにしました。
その方法については、Aaron Hillegass さんの本 Cocoa Programming for Mac OS X を参考にしました。英語だけど、良い本だと思います。
追加: 日本語訳「 Mac OS X Cocoa プログラミング」が出ました。ぜひ読みましょう。
ツールバーの作成は、 Cocoaはやっぱり! 出張版第 6 回の記事や、/Developer/Examples/AppKit/SimpleToolbar を参考にしました。 (いちばん苦労したのは、矢印アイコンを描くことだったりして。)
それにしても、Cocoa のツールバーの実装や API の設計って、どうにか ならないものでしょうかねぇ。。。
追加: Mac OS X 10.2 では小さなアイコンが使えるようになって、ちょっと嬉しい。
ver 0.1 のところにも書いたけど、やっぱ邪道だよなぁと思って、試しに 書類のタイプから '????' ってのを削除してみたら、New や Open... が グレーになり、アプリケーション起動時にもウィンドウが開かなくなって しまいました。
そのままでは困るので、NSApp の delegate と、NSDocumentController の サブクラスを作って、そこでいろいろと細工してみました。
ウィンドウの位置の保存方法として、- setFrameAutosaveName: ってのが もともと用意されてますが、これだとひとつだけしか保存できないので 役に立ちません。(検索ダイアログでは使ってるけど。)
また、テキストの表示倍率や表示内容など、各ウィンドウの状態も保存する 必要があります。
そこで、各ウィンドウの情報を辞書 (NSDictionary) に格納し、その配列 (NSArray) を user defaults に保存するようにしてみました。ちなみに、 ウィンドウ位置の取得と設定には、それぞれ - stringWithSavedFrame と - setFrameFromString: ってのを使ってみました。
MyDocument.nib に Magnify シートも一緒に入れていたんですが、.nib ファイルがごちゃごちゃするし、各ウィンドウごとに Magnify シートが 存在するため、メモリも無駄に使われていました。
そこで、.nib ファイルを分離して、コントローラクラスも独立させて みました。MyDocument クラスもちょっとだけすっきり。
でも、MyDocument クラスはまだまだ混雑してるので、なんとか根本的に すっきりさせたいような気もします。ウィンドウコントローラに仕事を 割り振って、File's Owner もそっちに変更したらいいのかなぁ?
Node および ClassNode クラスも場当たり的に作ったので、拡張性という 点ではいまいちで、このままでは Cocoa 以外のドキュメントに対応させる のはちょっと難しいような気がしていました。
そこで、ClassNode クラスを NodeWithFile と NodeParser の二つに分割 してみました。NodeWithFile クラスはファイルや parser の参照を持つ だけにして、実際のファイルの解釈は NodeParser とそのサブクラスが 行います。
待望の Jaguar こと Mac OS X 10.2 ですが、いくつか問題が発生しました。
まず、NSTextView で HTML を表示する際、baseURL を指定しても、相対 リンクへ飛んでくれなくなってしまいました。10.1 ではちゃんとブラウザで 開いてくれたのに。仕方がないので、自分で処理するようにしました。と 言っても、NSWorkspace にメッセージ投げるだけですが。
次に、ver 0.4 のリリース直後に AppKiDo の作者の Andy Lee さんに教えてもらった (彼自身も同じところで はまったらしい) のですが、Delegate Method を検出するのに使っていた、 Methods Implemented By the Delegate という文字列のうち、By が by に 変わってしまってました。やれやれ。