単語リストプログラム その7

サンプルスクリプトでバンディングまでがうまく行っていることが確認できたら、次はスクリプトでファイルを選んで表示させるところまで行く。

今回は、スクリプトを中心に進める。Interface Builder も少しは出てくるけど、ほとんどはテキストになるので、ページは軽いはず。

まずは、AppController で addFiles というメソッドを定義して、これを ib_action にする。ボタンが機能しているかと、どちらのボタンが押されたのかを確認したいので、定義の中に "add files" を表示するのとボタンの Alt. Title を表示する。

def addFiles(sender)

p "add files"

p sender.alternateTitle

end

ib_action :addFiles

Alt. Title ではなくて、Tag を設定した場合は、tag でタグの番号を得る。

p sender.tag

Interface Builder で App Controller の Received Actions を2つの Add ボタンに結びつける。

これで Command + R で Xcode から実行すると、Add ボタンをクリックするたびにコンソールに "add files" と押されたボタンの Alt. Title(左のものは #<NSCFString "0"> 右のものは #<NSCFString "1"> と表示されるはず)が表示される。ちなみにコンソールは Xcode で Command + Shift + R で表示される(またはメニューから実行 -> コンソール)。これで、ボタンが機能していることが確認できる。

そうしたら、addFiles の中で Open パネルを表示させてファイルもしくはフォルダを選択させるようにする。ここでは、細かい解説は飛ばすので、詳しいことは Open パネル Save パネルのページを参照してください。

Open パネルでプレインテキストファイルのデフォルトの文字コードを選べるようにしたいので、Accessory View に文字コードを選ぶための Pop Up Button を表示させる。詳しいことは Accessory View を Interface Builder で作るに書いてあるので、ここでは簡単な説明だけを書いておく。

まず、Interface Builder で nib ファイルのウィンドウに Library から Custom View を追加する。それをダブルクリックして開き、そこに Library から Pop Up Button をドラッグアンドドロップして配置する。さらに Label もドラッグアンドドロップして、Title を Encoding にする。まあ、日本語で文字コードとしてもいいけど。見た目はこんな感じにする。

これで、Pop Up Menu の項目を作るんだけど、前々回に作った Table View の Pop Up Button Cell のものをコピーしてペーストすることでわざわざ一から入力しなくてもいい。それには、Pup Up Button をダブルクリックしてメニューを開き、コピーした項目をペーストする。ペーストしたらもとからある項目を消す。最終的にはこんな感じにする。あとは、メニューの項目のタイトルがちゃんと表示されるように Pop Up Button の幅を調節して Custom View のサイズも調節して保存する。このサイズは、実際にパネルに表示されたものを確認して決めるといい。

次に、outlet にこの Custom View と Pop Up Button を追加して結びつける。

ib_outlet :accEncView, :encPopupBtn

これで準備ができたので、Open パネルを表示させるスクリプトを書く。パネルがジーニーエフェクトみたいに出てくるようにしたいので、beginSheetForDirectory_file_types_modalForWindow_modalDelegate_didEndSelector_contextInfo を使う。それ以外の設定は、次のようにする。

    • フォルダを選べるようにする - setCanChooseDirectories(true)

    • 複数のアイテムを選べるようにする - setAllowsMultipleSelection(true)

    • メッセージを設定する - setMessage(message)

    • 上で作った Accessory View を追加する - setAccessoryView(@accEncView)

beginSheetForDirectory_file_types_modalForWindow_modalDelegate_didEndSelector_contextInfo の設定は次の通り。

    • デフォルトのディレクトリ(Directory)- 指定しない(nil)

    • デフォルトのファイル(file)- 指定しない(nil)

    • 読み込むファイルのタイプ(type)- txt, doc, docx, rtf, rtfd, odt, sxw, pdf

    • パネルが出てくるウィンドウ(modalForWindow)- メインウィンドウ(@window)

    • delegate - self

    • 呼び出すメソッド - addFilePanelDidEnd_returnCode_contextInfo

    • contextInfo - nil

あと、どちらのボタンがクリックされたかを受け渡すために @addBtn というインスタンス変数を Alt. Title の文字を数値に変換して定義する。beginSheetForDirectory_file_types_modalForWindow_modalDelegate_didEndSelector_contextInfo の contextInfo で渡すのが正しいんだろうけど、どうやるのかがわからないので、まあ、とりあえず動く方法ってことで。あと、これを使うと、didEndSelector で指定したメソッドが呼び出されるので、それも用意する。

def addFiles(sender)

@addBtn = sender.alternateTitle.to_i # tag を使う場合は、@addBtn = sender.tag.to_i (.to_i はなくてもいけると思うけど念のため)

panel = NSOpenPanel.openPanel

panel.setCanChooseDirectories(true)

panel.setAllowsMultipleSelection(true)

panel.setMessage("Select file(s) to add to the list.")

panel.beginSheetForDirectory_file_types_modalForWindow_modalDelegate_didEndSelector_contextInfo(nil,

nil,

[:txt,:doc,:docx,:rtf,:rtfd,:odt,:sxw,:pdf],

@window,

self,

"addFilePanelDidEnd_returnCode_contextInfo",

nil)

end

ib_action :addFiles

def addFilePanelDidEnd_returnCode_contextInfo(panel,returnCode,info)

if returnCode == 1

p panel.filenames.to_a

p @addBtn

p @encPopupBtn.indexOfSelectedItem

p "add files"

end

end

このスクリプトで実行すると、Add ボタンをクリックするとパネルが出てきて、Encoding を選んで Open をクリックすると returnCode が 1 になり、コンソールに選んだディレクトリのパスの配列、ボタンの番号(左なら 0 右なら 1)、選ばれた Encode のインデックス(UTF-8 が 0 で以下 1 2 3...)、"add files" と表示されるはず。Cancel をクリックすると returnCode が 0 になり、何も実行されないでパネルが閉じる。何も選ばずに Open をクリックすると、開いているフォルダのパスが一つだけ入った配列が返ってくる。

この例の複数行に分かれているところは、スクリプトでは1行でいいけど、このページでは収まりきらなかったから複数行に分けて書いてある。@encPopupBtn が Encode の Pop Up Button で、indexOfSelectedItem で選ばれた項目のインデックスが返ってくる。

ここまでうまく行ったら、次は addFilePanelDidEnd_returnCode_contextInfo でファイル名を読み込んでテーブルに表示させるまでの処理をする。まあ、Array Controller とテーブルがバインドされているので、Array Controller に addObject で要素を追加すれば自動的に表示される。

さて、押されたボタンによって、右側と左側のテーブルを区別してデータを表示させるために一工夫してみる。クリックされたボタンによって、左が 0 で右が 1 という値が得られるので、Array Controller を配列に入れておいて、0 なら File Array 1 が 1 なら File Array 2 が取り出せればいい。というわけで、awakeFromNib で次のインスタンス変数を定義する。

def awakeFromNib(sender)

@fileAryCtl = [@fileAryCtl1, @fileAryCtl2]

end

これで @fileAryCtl[0] なら @fileAryCtl1 が @fileAryCtl[1] なら @fileAryCtl2 になる。これを @addBtn と組み合わせて @fileAryCtl[@addBtn] でデータを追加したい Array Controller にアクセスできる。

次は、返ってきたディレクトリのパスを処理する方法。このサンプルアプリケーションでは、フォルダが選ばれた場合、その中に含まれるすべてのファイルを読み込むようにする(すべてのサブディレクトリを含む)。この方法は、ファイル・ディレクトリ単語頻度を数えるに書いてあるので、そちらを参照してください。

選ばれたファイル/フォルダは panel.filenames で配列として得られるので、これをブロックで処理する。それぞれの要素は、NSPathStore になっているので Ruby で処理するためには to_s で String オブジェクトに変換する。Find モジュールを使うので、require 'find' で読み込む。

require 'find'

あと、読み込むファイルタイプかどうか判断するのに正規表現を使うので、その正規表現を awakeFromNib で定義しておく。

def awakeFromNib

@fileAryCtl = [@fileAryCtl1, @fileAryCtl2]

@fileExt = Regexp.new("\\.(?:txt|doc|docx|rtf|rtfd|odt|sxw|pdf)",Regexp::IGNORECASE)

end

def addFilePanelDidEnd_returnCode_contextInfo(panel,returnCode,info)

if returnCode == 1

panel.filenames.each do |dir|

Find.find(dir.to_s) do |path|

if @fileExt =~ File.extname(path)

p path

end

end

end

end

end

ここまでのスクリプトで実行すると、選んだのがファイルなら読み込むタイプか判断して、フォルダならそのサブフォルダも含めて入っているすべてのファイルに対して判断して、読み込むタイプであればコンソールにパスが表示されるはず。

次は、この path に入っているパスからファイル名を抜き出して、それと選んだ文字コードを Array Controller に入れていく作業に移る。

path からファイル名を取り出すには Ruby の File.basename(path) を使う。これと文字コードのインデックスを Hash にして配列にいれていく。そのための配列をまず定義しておく。それと、選ばれた文字コードのインデックスも別のオブジェクトを作っておく。

fileAry = Array.new

encode = @encPopupBtn.indexOfSelectedItem

この配列に、Hash を入れていく。実際は NSDictionary オブジェクトの配列なんだけど RubyCocoa では Hash で代用できるので。ただ、ここでもう一工夫。プレインテキスト(.txt)以外には文字コードは意味がないので、表示させないようにしたい。そこで、File.extname(path) が .txt の場合は encode の値をそのまま使い、そうでない場合は -1 を入れて Pop Up Button を空白にして表示するようにする。ただ、それでもテーブル上で文字コードが変えられることには変わりはないけど。どの文字コードでも処理されないけど、表示されていない方がすっきりするからという理由だけで、次の処理を入れてみた。

currentEncode = File.extname(path).downcase == ".txt" ? encode : -1

fileAry << {"fileName" => File.basename(path),"encoding" => currentEncode,"fullPath" => path}

最後に、@fileAryCtl[@addBtn] に addObjects で fileAry を追加する。

@fileAryCtl[@addBtn].addObjects(fileAry)

このままだと、重複したファイルもそのままなので、重複したファイルを消すのとついでに、ファイル名を並べ替える操作を入れてみる。NSArrayController では、重複する要素を消す方法が見つからなかったので、NSCountedSet を使うことにする。NSCountedSet オブジェクトは initWithArray で配列から作れて、配列の重複する要素を除外する。それから allObjects を使うと、また配列が取り出せる。

@fileAryCtl[@addBtn].addObjects(fileAry)

uniqFiles = NSCountedSet.alloc.initWithArray(@fileAryCtl[@addBtn].arrangedObjects).allObjects

@fileAryCtl[@addBtn].removeObjectsAtArrangedObjectIndexes(NSIndexSet.indexSetWithIndexesInRange([0,@fileAryCtl[@addBtn].arrangedObjects.length]))

@fileAryCtl[@addBtn].addObjects(uniqFiles)

ここでしているのは、まず、Array Controller に fileAry を追加して、それから arrangedObjects で取り出した配列で NSCountedSet オブジェクトを作って重複している要素を削除して、それからまた allObjects で配列として取り出して、元の Array Controller を空にした後にその Array Controller に addObjects で配列を追加している。

次は並べ替え。ここでは、大文字小文字の区別なしに並べ替えたいん。そこで、initWithKey_ascending_selector で selector に "caseInsenstiveCompare:" を指定して NSSortDescriptor オブジェクトを作り、NSArrayController の setSortDescriptors で適用して、rearrangeObjects で並べ替えを反映させている。setSortDescriptors は NSSortDescriptor オブジェクトの配列をとるので、最初の行では descriptor という配列を作っている。

descriptor = [NSSortDescriptor.alloc.initWithKey_ascending_selector("fileName",true,"caseInsensitiveCompare:")]

@fileAryCtl[@addBtn].setSortDescriptors(descriptor)

@fileAryCtl[@addBtn].rearrangeObjects

まとめるとこんな感じ。

def addFilePanelDidEnd_returnCode_contextInfo(panel,returnCode,info)

if returnCode == 1

fileAry = Array.new

encode = @encPopupBtn.indexOfSelectedItem

panel.filenames.each do |dir|

Find.find(dir.to_s) do |path|

if @fileExt =~ File.extname(path)

currentEncode = File.extname(path).downcase == ".txt" ? encode : -1

fileAry << {"fileName" => File.basename(path),"encoding" => currentEncode,"fullPath" => path}

end

end

end

@fileAryCtl[@addBtn].addObjects(fileAry)

uniqFiles = NSCountedSet.alloc.initWithArray(@fileAryCtl[@addBtn].arrangedObjects).allObjects

@fileAryCtl[@addBtn].removeObjectsAtArrangedObjectIndexes(NSIndexSet.indexSetWithIndexesInRange([0,@fileAryCtl[@addBtn].arrangedObjects.length]))

@fileAryCtl[@addBtn].addObjects(uniqFiles)

descriptor = [NSSortDescriptor.alloc.initWithKey_ascending_selector("fileName",true,"caseInsensitiveCompare:")]

@fileAryCtl[@addBtn].setSortDescriptors(descriptor)

@fileAryCtl[@addBtn].rearrangeObjects

end

end

このままで問題ないんだけど、あとで、テーブルへファイルをドラッグアンドドロップできる機能を付けたいので、そのときにも同じ処理をすることになるため、共通できる部分を新たなメソッドとして定義することにする。addFilesToTable というメソッドを作ってみた。

def addFilePanelDidEnd_returnCode_contextInfo(panel,returnCode,info)

if returnCode == 1

addFilesToTable(@addBtn,@encPopupBtn.indexOfSelectedItem,panel.filenames)

end

end

def addFilesToTable(aryID,encode,files)

fileAry = Array.new

files.each do |dir|

Find.find(dir.to_s) do |path|

if @fileExt =~ File.extname(path)

currentEncode = File.extname(path).downcase == ".txt" ? encode : -1

fileAry << {"fileName" => File.basename(path),"encoding" => currentEncode,"fullPath" => path}

end

end

end

@fileAryCtl[aryID].addObjects(fileAry)

uniqFiles = NSCountedSet.alloc.initWithArray(@fileAryCtl[aryID].arrangedObjects).allObjects

@fileAryCtl[aryID].removeObjectsAtArrangedObjectIndexes(NSIndexSet.indexSetWithIndexesInRange([0,@fileAryCtl[aryID].arrangedObjects.length]))

@fileAryCtl[aryID].addObjects(uniqFiles)

descriptor = [NSSortDescriptor.alloc.initWithKey_ascending_selector("fileName",true,"caseInsensitiveCompare:")]

@fileAryCtl[aryID].setSortDescriptors(descriptor)

@fileAryCtl[aryID].rearrangeObjects

end

これで完成。ここまでのスクリプトをまとめてみた。これで、Add ボタンをクリックしてファイル/フォルダを追加するとテーブルに表示されるはず。重複の削除と並べ替えも入れてある。いらないなら消してください。

require 'osx/cocoa'

require 'find'

$KCODE = 'UTF-8'

class AppController < OSX::NSObject

include OSX

ib_outlet :window, :tabView, :table1, :table2, :previewCheck, :previewText

ib_outlet :fileAryCtl1, :fileAryCtl2

ib_outlet :accEncView, :encPopupBtn

def awakeFromNib

@fileAryCtl = [@fileAryCtl1, @fileAryCtl2]

@fileExt = Regexp.new("\\.(?:txt|doc|docx|rtf|rtfd|odt|sxw|pdf)",Regexp::IGNORECASE)

end


def addFiles(sender)

@addBtn = sender.alternateTitle.to_i # tag を使う場合は、@addBtn = sender.tag.to_i (.to_i はなくてもいけると思うけど念のため)

panel = NSOpenPanel.openPanel

panel.setCanChooseDirectories(true)

panel.setAllowsMultipleSelection(true)

panel.setMessage("Select file(s) to add to the list.")

panel.setAccessoryView(@accEncView)

panel.beginSheetForDirectory_file_types_modalForWindow_modalDelegate_didEndSelector_contextInfo(nil,

nil,

[:txt,:doc,:docx,:rtf,:rtfd,:odt,:sxw,:pdf],

@window,

self,

"addFilePanelDidEnd_returnCode_contextInfo",

nil)

end


ib_action :addFiles

def addFilePanelDidEnd_returnCode_contextInfo(panel,returnCode,info)

if returnCode == 1

addFilesToTable(@addBtn,@encPopupBtn.indexOfSelectedItem,panel.filenames)

end

end

def addFilesToTable(aryID,encode,files)

fileAry = Array.new

files.each do |dir|

Find.find(dir.to_s) do |path|

if @fileExt =~ File.extname(path)

currentEncode = File.extname(path).downcase == ".txt" ? encode : -1

fileAry << {"fileName" => File.basename(path),"encoding" => currentEncode,"fullPath" => path}

end

end

end

@fileAryCtl[aryID].addObjects(fileAry)

uniqFiles = NSCountedSet.alloc.initWithArray(@fileAryCtl[aryID].arrangedObjects).allObjects

@fileAryCtl[aryID].removeObjectsAtArrangedObjectIndexes(NSIndexSet.indexSetWithIndexesInRange([0,@fileAryCtl[aryID].arrangedObjects.length]))

@fileAryCtl[aryID].addObjects(uniqFiles)

descriptor = [NSSortDescriptor.alloc.initWithKey_ascending_selector("fileName",true,"caseInsensitiveCompare:")]

@fileAryCtl[aryID].setSortDescriptors(descriptor)

@fileAryCtl[aryID].rearrangeObjects

end

end

今回はここまで。次回は、テーブルのファイルの全消去と、プレビューの処理をまとめる。

単語リストプログラム その8 - ファイルのプレビューなど