ドラッグアンドドロップ
ここには、Table View へと Table View からのドラッグアンドドロップについてメモする。
準備
NSTableView でドラッグアンドドロップを可能にするには、まず、Table View の delegate (dataSource も?)をスクリプトを書いている
オブジェクトのサブクラスにする(AppController とか)。これは、普通にテーブルを使うなら設定してあるはず。
そして、次の2つのメソッドを awakeFromNib に追加して、受け入れるデータの種類を指定する。
registerForDraggedTypes(array) で array には、受け入れるデータの種類を配列にしていれる。array は Ruby の Array オブジェクトでいい。
ここでは、テキストとファイル名にしぼっていくので、それ以外は、Xcode のデベロッパドキュメントを参照してください。
@table.registerForDraggedTypes([NSFilenamesPboardType,NSStringPboardType])
もうひとつの設定は setDraggingSourceOperationMask_forLocal(type,true/false) で、Dragging Operation の設定をするんだけど、
NSDragOperationEvery にすると、すべてのタイプが受け入れられるので、そうする。forLocal は true だとドラッグアンドドロップの
受け入れ先が設定するテーブルのあるアプリケーションだけになり、false だとそれ以外にもドラッグアンドドロップできる。
テーブルからのドラッグアンドドロップをアプリケーション外(Finder など)にしたい場合は、false にする。
これもドラッグアンドドロップを行いたい Table View に対して行う。
@table.setDraggingSourceOperationMask_forLocal(NSDragOperationEvery,false)
まあ、どうせなら、両方、という事で、次のようになる。
def awakeFromNib
@table.registerForDraggedTypes([NSFilenamesPboardType,NSStringPboardType])
@table.setDraggingSourceOperationMask_forLocal(NSDragOperationEvery,false)
end
テーブルへのドラッグアンドドロップ
次に、tableView_validateDrop_proposedRow_proposedDropOperation(tableView,info,row,operation) というメソッドを追加する。
これで、ドロップできるか決めているようで、これがないと、もちろんドロップ動作が始まらない。
ここでは、setDraggingSourceOperationMask_forLocal で指定したものと同じ、すべてのタイプの動作を受け入れる NSDragOperationEvery を返す。
def tableView_validateDrop_proposedRow_proposedDropOperation(tableView,info,row,operation)
return NSDragOperationEvery
end
最近発見したんだけど、ここで NSDragOperationEvery ではなく、NSDragOperationCopy にすると、ファイルをドラッグしてきたときに
マウスカーソルのアイコンがプラスのアイコンがついたものになる。
NSDragOperationLink にすると、くるっと巻いたような矢印になる。
あと、このメソッドの operation は2種類あって、テーブル上の列の間(NSTableViewDropAbove [1])と列の上(NSTableViewDropOn [0])になる。
つまり、ドロップするときに、列と列の間に線が入るようになる場合は、NSTableViewDropAbove が返る。これの値が1。何かを追加したいときにこれにする。
列の上になる場合は、NSTableViewDropOn が返る。これの値が0。テーブル上の列をドロップするもので置き換えたい場合なんかはこれにする。
そこで、テーブルに追加するために、列の間にきたときだけ反応するようにするには、operation == 1 の時に NSDragOperationCopy などドラッグを受け入れるものを返し、そうでない場合は、NSDragOperationNone を返せばいい。こうすることで、列の上になったときに反転しないで無反応になる。
def tableView_validateDrop_proposedRow_proposedDropOperation(table,info,row,operation)
if operation == 1
return NSDragOperationCopy
else
return NSDragOperationNone
end
end
最後に、実際にドロップされたデータを扱うために tableView_acceptDrop_row_dropOperation(tableView,info,row,operation) というメソッドを追加する。
ここでドロップされた場合の処理を入れるんだけど、処理後に true を返すとドロップされたファイルがその場で消える。false の場合や、
true を入れないで処理したときに true が返らないと false とされて、ファイルが元の場所に戻るような動作をする。
さて、実際の処理なんだけど、ここで得られる info にデータが入ってるので、それをまず、draggingPasteboard で pasterBoard オブジェクトにする。
pasteBoard = info.draggingPasteboard
次に pasteBoard から、データを取り出す。ファイルをドロップしてファイル名(パス)を取り入れる場合は、
propertyListForType(type) を使う。これは、データを配列として取り出す。type には NSFilenamesPboardType を指定する。
これは、データをファイル名として扱う。
data = pasteBoard.propertyListForType(NSFilenamesPboardType)
テキスト(文字列)をドロップして、それを取り込むには、pasteBoard.stringForType(type) を使う。これは、データを
文字列として取り出す。type には NSStringPboardType はデータを文字列として扱う。
data = pasteBoard.stringForType(NSStringPboardType)
データの方が違う場合は、nil が返るので、次のようにして、この2つを data と data2 に入れて、nil じゃないデータを
配列に入れていく。ちなみに、配列の1つ目が0なのは、テーブルの一つ目の列がチェックボックスになっているサンプルを使ったから。
そうじゃない場合は、好みの値を入れてください。もしかしたら、もっとコンパクトにできるかもしれないけど、
理解し始めに書いたスクリプトなので、今のところはこのままで。いつか書き直すかも。
このスクリプトでは、最後に配列の重複を消して、並べ替えてから、テーブルを更新している。
def tableView_acceptDrop_row_dropOperation(tableView,info,row,operation)
pasteBoard = info.draggingPasteboard
data = pasteBoard.propertyListForType(NSFilenamesPboardType)
data2 = pasteBoard.stringForType(NSStringPboardType)
case tableView.to_s
when @table.to_s
if data != nil
data.to_a.each do | file |
@output << [0,file.to_s].flatten
end
else
@output << [0,data2.to_s]
end
@output.uniq!
@output.sort!
@table.reloadData
end
return true
end
この最後のスクリプトは、Table View のところで、Xcode と Interface Builder で2列のテーブルを設定したものを使って、
ファイルとテキストのドロップができるようにしたものです。ついでなので、1列目はチェックボックスが入ってます。
もともとスクリーンキャプチャだったものを書き起こしたのでタイポがあるかもしれません。
require 'osx/cocoa'
class AppController < OSX::NSObject
include OSX
ib_outlets :window, :table
def initialize
@output = []
end
def awakeFromNib
checkCell = NSButtonCell.new
checkCell.setButtonType(3)
checkCell.setTitle("")
@table.tableColumnWithIdentifier("col1").setDataCell(checkCell)
@table.registerForDraggedTypes([NSFilenamesPboardType,NSStringPboardType])
@table.setDraggingSourceOperationMask_forLocal(NSdragOperationEvery,true)
end
def numberOfRowsInTableView(tableview)
case tableView.to_s
when @table.to_s
@output ? @output.length : 0
end
end
def tableView_objectValueForTableColumn_row(tableView,col,row)
case col.to_s
when @table.tableColumnWithIdentifier('col1')
@output[row][0] ? @output[row][0] : 0
when
@output[row][1]
end
end
def tableView_setObjectValue_forTableColumn_row(tableView,object,col,row)
case col.to_s
when @table.tableColumnWithIdentifier('col1).to_s
@output[row][0] = object.to_i
end
end
def tableView_validateDrop_proposedRow_proposedDropOperation(tableView,info,row,operation)
return NSDragOperationEvery
end
def tableView_acceptDrop_row_dropOperation(tableView,info,row,operation)
pasteBoard = info.draggingPasteboard
data = pasteBoard.propertyListForType(NSFilenamesPboardType)
data2 = pasteBoard.stringForType(NSStringPboardType)
case tableView.to_s
when @table.to_s
if data != nil
data.to_a.each do |file|
@output << [0,file.to_s]
end
else
@output << [0,data2.to_s]
end
@output.uniq!
@output.sort!
@table.reloadData
end
return true
end
end
テーブルからのドラッグアンドドロップ
つぎに、テーブルからドラッグアンドドロップする方法をメモする。
使うメソッドは2つ。tableView_writeRowsWithIndexes_toPasteboard(tableView,index,pasteBoard) と
tableView_namesOfPromisedFilesDroppedAtDestination_forDraggedRowsWithIndexes(tableView,url,index)。
まず、tableView_writeRowsWithIndexes_toPasteboard で、pasterBoard でくる NSPasterBoard オブジェクトに対して設定を行う。
この設定で、何をドラッグアンドドロップするかを決める。ここでは、ファイルを Finder にドラッグアンドドロップしたいので、
declareTypes(type,owner) で type に NSFilesPromisePboardType を指定する。owner は self にしておく。
これともう一つ、setPropertyList_forType(plist,type) で type に NSFilesPromisePboardType 指定して、plist にファイルパスを
いれるみたいなんだけど、あまりよくわかっていない。ファイル名じゃなくても拡張子を入れておけばファイルと認識するみたいで、
何でもいいからとりあえず拡張子を入れておけば動作するようだ。ここでは、"txt" にしておく。
def tableView_writeRowsWithIndexes_toPasteboard(table,idx,pasteboard)
pasteboard.declareTypes_owner([NSFilesPromisePboardType],self)
pasteboard.setPropertyList_forType(["txt"],NSFilesPromisePboardType)
true
end
テーブルのデータをドロップする先が決まったら(マウスのボタンから指を離したら)
tableView_namesOfPromisedFilesDroppedAtDestination_forDraggedRowsWithIndexes(tableView,url,index) が呼ばれる。
ここで、tableView には Table View オブジェクトが、url にはドロップ先の URL が index にはドラッグしているテーブルの列のインデックスが入っている。
これらの情報を元に、Finder にドロップするときにコピーする、もしくは移動させる処理を書く。
次のようなスクリプトを書いてみた。
def tableView_namesOfPromisedFilesDroppedAtDestination_forDraggedRowsWithIndexes(table,url,index)
index.to_a.each do |idx|
savePath = url.path.stringByAppendingPathComponent(File.basename(@output[idx][1]))
while NSFileManager.defaultManager.fileExistsAtPath(savePath)
savePath.match(/^(.+?)(\.[^\.]+)$/)
savePath = "#{$1}_1#{$2}"
end
NSFileManager.defaultManager.copyPath_toPath_handler(@output[idx][1],savePath,nil)
end
end
何をしているかというと、index に NSIndexSet として テーブルの列のインデックスが入っている。これを to_a で Ruby の Array オブジェクトにして、
each でブロック渡して処理している。ドロップ先は url に NSURL オブジェクトとして入っているので path で NSString のファイルパスに
変換して、stringByAppendingPathComponent(string) で元のファイルのファイル名を足している。テーブルへのドラッグアンドドロップで
@output という Ruby の Array のそれぞれの要素の中の2番目の要素にファイル名が入っているので、@output[idx][1] で取り出している。
次の while ループは、NSFileManger の fileExistsAtPath(path) というメソッドでファイルがドロップ先に既にある場合は _1 を付けるようにしてる。
まあ、一応方法を考えたので、忘れないようにメモ。ちなみに、Ruby の File.exist?(filename) もしくは FileTest.exist?(filename) でもいい。
で、最後に copyPath_toPath_handler(origPath,destPath,handler) でファイルをコピーしてる。もちろん Ruby の FileUtils.copy_file(src,dest) でいい。
とりあえず、これで動いているけど、また新たに何かわかったら後で手を加える。
Outline View へのドロップ
テーブルと同じように Outline View へもドロップができる。まずは、上のテーブルへのドロップと同じように、
outlineView_validateDrop_proposedItem_proposedChildIndex(view,info,item,index) でドロップができるかどうかを決める。
テーブルと同じように Drag Operation を返すんだけど、全部入りの NSDragOperationEvery にしておけばいい。
と思ったんだけど、上にも書いたように、NSDragOperationCopy にすると、ドラッグするときにカーソルのアイコンが
プラスのついたものになる。
def outlineView_validateDrop_proposedItem_proposedChildIndex(view,info,item,index)
return NSDragOperationEvery
end
要素自体にドロップしようとすると、index が -1 になるので、index が -1 じゃない場合は NSDragOperationNone を返せば、
ドロップの場所を要素だけにできる。逆にすれば、要素の間にドロップするときだけに処理を行える。
def outlineView_validateDrop_proposedItem_proposedChildIndex(view,info,item,index)
if index == -1
return NSDragOperationCopy
else
return NSDragOperationNone
end
end
つぎに、実際処理をする outlineView_acceptDrop_item_childIndex(view,info,item,index) を追加する。
item は 親要素で index は子要素のインデックス。これらから、その要素にドロップされたかを得る。
def outlineView_acceptDrop_item_childIndex(view,info,item,index)
ここに処理を入れる
return true
end
テーブル間(Outline View も含む)のドラッグアンドドロップ
テーブル間でドラッグアンドドロップする場合は、NSPasteboard の標準データにない配列や、Managed Object なんかを
受け渡ししないといけない。じゃ、どうするか。作ってしまえばいい。たとえば TableContentType というのを作るとする。
そのためには、TableContentType という定数を作ってしまえばいい。ちなみに、定数はメソッドの中では定義できないので注意。
TableContentType = "TableContentType"
これで、他のと同様に登録したり宣言したりすればいい。
@outlineView.registerForDraggedTypes([NSFilenamesPboardType,TableContentType])
pboard.declareTypes_owner([NSFilesPromisePboardType,TableContentType],self)
ここで、2つのテーブル間でのドラッグアンドドロップというのをメモってみる。2つのテーブルは @tableA と @tableB とする。それぞれの中身の配列は @outputA と @outputB とする。
まずは、要素が一つしか選べないようになっている場合で、一つだけ移動する例。とりあえずは動いてるのをコピーしてちょっと変えてるだけなので動くはず。ただ、もっといい方法があるかもしれない。それぞれの細かいことは上のドラッグやドロップの説明のところを参照。
def tableView_writeRowsWithIndexes_toPasteboard(table,idx,pasteboard)
case table.to_s
when @tableA.to_s
item = @outputA[idx] # idx のところの配列の要素を取り出す
table = 0 # どちらのテーブルからドラッグが始まったかわかるようにする
when @tableB.to_s
item = @outputB[idx]
table = 1
end
pasteboard.declareTypes_owner([TableContentType],self)
pasteboard.setPropertyList_forType([item,idx,table],TableContentType)
# propertyList はドラッグ先のテーブルに受け渡すものを配列で入れる。
# ここでは、受け渡す配列の要素とインデックス、それと移動元のテーブル。
# オブジェクトは送れないので、数字に置き変えておく。
return true
end
def tableView_validateDrop_proposedRow_proposedDropOperation(table,info,row,operation)
case table.to_s
when @tableA.to_s,@tableB.to_s
if operation == 0
return NSDragOperationNone # テーブルの列と重なった場合は反応しない
else
return NSDragOperationMove
end
end
end
def tableView_acceptDrop_row_dropOperation(table,info,row,operation)
pasteBoard = info.draggingPasteboard
case table.to_s
when @tableA.to_s
item = pasteBoard.propertyListForType(TableContentType)
if item[2] == 0 # item[2] はテーブルの番号なので、ここでは、@tableA の時
@outputA.delete_at(item[1]) # まず、移動元の要素を消す。
if item[1].to_i < row # 消したものが、ドロップする列よりも前(上)の場合。item[1] は NSNumber になっているので to_i で変換。
@outputA.insert(item[0],row - 1) # 消した分上にずれるのでインデックスから1を引く。
else # その他の場合、そのインデックスにドロップ。
@outputA.insert(item[0],row)
end
else
@outputB.delete_at(item[1]) # 移動元が @tableB なので、そちらから要素を削除。
if row < @outputA.length # 移動元と移動先のテーブルが違うので、削除した影響は考えない。
@outputA.insert(item[0],row)
else
@outputA.push(item[0])
end
end
return true
when @tableB.to_s
item = pasteBoard.propertyListForType(TableContentType)
if item[2] == 0
@outputA.delete_at(item[1])
if row < @outputB.length
@outputB.insert(item[0],row)
else
@outputB.push(item[0])
end
else
@outputB.delete_at(item[1])
if item[1].to_i < row
@outputB.insert(item[0],row - 1)
else
@outputB.insert(item[0],row)
end
end
end
return true
end