Document-based アプリケーションを作る その2

Document-based アプリケーションを作るで、簡単なエディタを作ったけど、Cocoa バインディングを使った方法を少し理解したので、そのメモ。基本的なアプリケーションを作るところは、Document-based アプリケーションを作るを参照。Open Panel をいじる方法は、Document-based アプリケーションを作る その3にメモした。

さらに、ここで書いたバインディングを使った方法とは別のバインディングを使った方法も試してみた、というか、そっちの方が楽かも、と思うので、それは、Document-based アプリケーションを作る その4としてまとめる。

前回のは、ファイルの内容を読み込んで、それを Text View に表示させるのに、NSTextView のメソッドを使ってやったので、readRTFDFromFile でリッチテキストを setString でプレインテキストを表示した。でも、NSAttributedString にすると、MS Word の .doc ファイルも読み込める。ただ、それを Text View に表示するには、insertString を使うしか思いつかないんだけど、Continuous Spell Check が On になってるときに、ミススペルと取られるような記号やらコードやらがたくさんある長い文書を表示させようとすると、それらのスペルをいちいちチェックするので、表示するまでにすごく時間がかかる。そこで、Text View のバインディングを使い Attributed String をバインドすることにする。これで、Word 文書も扱えるようにする。ただ、リッチテキストでもそうなんだけど、テキスト以外は情報が保持されない。画像なんかは貼付けても消されるし、開くときにもとの文書に画像が埋め込んであっても読み込まない。まあ、テキストだけでも変換しないで処理できるだけましとする。

まず、Xcode でターゲットの情報を見るで、書類のタイプに MS ワード文書というのを追加する。とりあえず拡張子は doc のみ。

次に Interface Builder で Object Controller を MyDocument.nib に追加する。

追加したら、どうせなので名前をつけておく。

MyDocument.rb にこの Object Controller の outlet を作り、結びつける。ここでは textViewController にした。

次に、Inspector の Attributes で key を追加する。今回は、Text View に表示する Attributed String だけなので、ひとつ text というのを追加する。このスクリーンショットでは入っていないけど、Prepare Content にチェックを入れる。

最後に、Text View をこれにバインドする。Inspector の Bindings で Value の Attributed String を開き、TextView Controller にバインドして Model Key Path は上で作った text にする。

これで、Interface Builder 上の作業は終わり。次はスクリプトの方をいじる。

以前作ったアプリケーションでは、MyDocument.rb の windowControllerDidLoadNib(aController) というメソッドの中でファイルを読み込む処理をしているのでそこをいじる。まず、Rich Text は readRTFDFromFile(file) というメソッドを使って Text View に直接読み込んでいた。

@textView.readRTFDFromFile(self.fileName)

今回は、まず Attributed String をファイルの内容から作る。Rich Text と Word 文書は initWithURL_documentAttributes(url,attributes) で取り出せる。attributes は元のままのでいいので nil にする。前回はファイル名で処理したけど、URL にしろというようなことがドキュメントに書いてあるので、self.fileURL で URL を取り出して処理している。

displayText = NSAttributedString.alloc.initWithURL_documentAttributes(self.fileURL,nil)

プレインテキストは、文字コードの処理もあるので、前回と同様に NSString でまず読み込み、それを Attributed String に変換する。まあ、とりあえずは UTF-8 だけにしておく。

displayPlainText = NSString.stringWithContentsOfURL_encoding_error(self.fileURL,NSUTF8StringEncoding,error)

displayText = NSAttributedString.alloc.initWithString(displayPlainText)

これでできた Attributed String を Object Controller に setContent(object) で追加する(注:最近もうちょっと Object Controller の使い方がわかってきたので、新しいページを作ってみた)。object は Interface Builder にあるように NSDictionary なんだけど、RubyCocoa なので Ruby の Hash で処理する。ただ、これだと、上の Prepare Content にチェックが入っていなくてもエラーが出ないけど、この Object Controller でほかのオブジェクトも管理しようとした場合、全部が消されて、ここで設定したものだけになる。

@textViewController.setContent({"text" => displayText})

それを避けるためには、実は、NSArrayController.arrangedObjects が NSArray であるように、NSObjectController.content は NSDictionary であるので、次のように書くことで新たなオブジェクトとして設定するのではなく、既にあるオブジェクトの中身を書き換えることができる。この場合は、上で Prepare Content にチェックが入っていないとエラーが出る。

@textViewController.conetnt["text"] = displayText

次に、書き込みの方。これは、dataOfType_error(type,error) というメソッドのところに書いてあるので、そこをいじる。

Object Controller から値を取り出すには、content というメソッドを使う。帰ってくるのは NSDictionary オブジェクトなので、ここでは Hash として扱い次のようにする。

saveText = @textViewController.content["text"]

これで取り出したオブジェクトは NSAttributedString なので、これを保存するための NSData に変換する。リッチテキストは RTFFromRange_documentAttributes(range,attributes) ワード文書は docFormatFromRange_documentAttributes(range,attributes) を使って NSAttributedString から直接 NSData オブジェクトを作る。attributes は新たに追加しないので nil にする。プレインテキストは文字コードがあるので、Attributed String を string メソッドを使って NSString オブジェクトに変換してから、dataUsingEncoding(encoding) で NSData にする。

saveText.RTFFromRange_documentAttributes([0,saveText.length],nil)

saveText.docFormatFromRange_documentAttributes([0,saveText.length],nil)

saveText.string.dataUsingEncoding(NSUTF8StringEncoding)

これで保存の処理も終わり。ついでに、revertToContentsOfURL_ofType_error のところも、同じように変更したら保存して実行すると、3種類の文書形式が扱えるようになっているはず。

という訳で、ここまでのスクリプトをまとめてみた。

require 'osx/cocoa'

include OSX

class MyDocument < NSDocument

ib_outlet :textView, :textViewController

def windowNibName

return "MyDocument"

end

def windowControllerDidLoadNib(aController)

super_windowControllerDidLoadNib(aController)

if self.fileName

if self.fileType == "リッチテキスト"

@textView.setRulerVisible(true)

displayText = NSAttributedString.alloc.initWithURL_documentAttributes(self.fileURL,nil)

elsif self.fileType == "MS ワード文書"

@textView.setRulerVisible(true)

displayText = NSAttributedString.alloc.initWithURL_documentAttributes(self.fileURL,nil)

elsif self.fileType == "プレインテキスト"

@textView.setRichText(false)

displayPlainText = NSString.stringWithContentsOfURL_encoding_error(self.fileURL,NSUTF8StringEncoding,nil)

displayText = NSAttributedString.alloc.initWithString(displayPlainText)

end

@textViewController.content['text'] = displayText

else

@textView.setRulerVisible(true)

end

end

def dataOfType_error(type,error)

if type == "リッチテキスト"

saveText = @textViewController.content["text"]

saveText.RTFFromRange_documentAttributes([0,saveText.length],nil)

elsif type == "MS ワード文書"

saveText = @textViewController.content["text"]

saveText.docFormatFromRange_documentAttributes([0,saveText.length],nil)

elsif type == "プレインテキスト"

saveText = @textViewController.content["text"]

saveText.string.dataUsingEncoding(NSUTF8StringEncoding)

end

end

def loadDataRepresentation_ofType(data, aType)

return true

end

def revertToContentsOfURL_ofType_error(url,type,error)

if self.fileType == "リッチテキスト"

displayText = NSAttributedString.alloc.initWithURL_documentAttributes(self.fileURL,nil)

elsif self.fileType == "MS ワード文書"

displayText = NSAttributedString.alloc.initWithURL_documentAttributes(self.fileURL,nil)

elsif self.fileType == "プレインテキスト"

displayPlainText = NSString.stringWithContentsOfURL_encoding_error(self.fileURL,NSUTF8StringEncoding,nil)

displayText = NSAttributedString.alloc.initWithString(displayPlainText)

end

@textViewController.setContent({"text" => displayText})

end

end

最後にスクリプトの全景。