ドラッグアンドドロップ

ここには、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