#TouchDesigner でのドラッグ＆ドロップについて ver.2022

# TouchDesigner でのドラッグ＆ドロップについて

**公式Wiki**

https://docs.derivative.ca/Drag-and-Drop

TouchDesignerでUI構築、ツール制作を行う際に便利なドラッグ＆ドロップについて解説します

サンプルファイルとか

https://github.com/Joe0hara/TD_DragAndDrop


2019年のアドカレで同じ内容の記事を書いていたみたいですが、機能が更新されていたり使いやすくなっていたので改めてドラッグ＆ドロップについてまとめていきたいと思います

:::note
動作環境
[TouchDesigner](https://derivative.ca/download) 2022.31030
:::

**2019年の記事**

https://qiita.com/joe0hara/items/a1be5e9e06b8feac6cc1

ファイルやエディタ上でのD&Dについては上の記事に詳しく書いてあります（ここら辺の機能は変わってません）


---

### 以前のD&Dシステム(Legacy Drag/Drop System)との違い

- 以前はドラッグ・ドロップがされたタイミングで実行でしたが、現在のバージョンではそれ以外にも特定の実行タイミングを持つ
- Legacy Drag/Drop Systemは新しいバージョンでも使えるので以前の記事を参照してください



# ドラッグ&ドロップの実装

では、実際にD&Dの仕組みを見ていきましょう！

これまでの仕組みと同様すべてのPanel ComponentsでD&DのPython Callbackを使用することができます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175399/ade31456-af34-d5b1-31b4-9d01bbee49bb.png)

- `When Dragging This`
- `On Dropping into`

という二つのパラメータを`Use Drag/Drop Callbacks`に変更します
そして、`Drag/Drop Callbacks`の右側にある`Add`をクリックすることでデフォルトのD&D Callbackが作成されPanelCOMPにDockされます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175399/4533af6d-efea-6542-ce76-ab06e020ef1b.png)


## ドラッグ可能なパネルの設定

まずはドラッグ操作を拡張させるためのCallbackの設定をします
自動で作成されたCallbackの書かれたTextDATの中のドラッグに関わるコールバック関数を見ていきましょう

```python
def onDragStartGetItems(comp, info):
	"""
	Called when information about dragged items is required.

	Args:
		comp: the panel clicked on to start drag
		info: A dictionary containing all info about drag
			callbackPanel: the panel Component pointing to this callback DAT
	Returns:
		A list of dragItems: [object1, object2, ...]
	"""
	dragItems = [comp] # drag the comp itself
	#debug('\nonDragStartGetItems comp:', comp.path, '- info:\n', info)
	return dragItems

def onDragEnd(comp, info):
	"""
	Called when a drag action ends.

	Args:
		comp: the panel clicked on to start drag
		info: A dictionary containing all info about drag, including:
			accepted: True if the drag was accepted, False if not
			dropResults: a dict of drop results. This is the return value of 
				onDropGetResults
			dragItems: the original dragItems for the drag
			callbackPanel: the panel Component pointing to this callback DAT
	"""
	#debug('\nonDragEnd comp:', comp.path, '- info:\n', info)
	

```

### onDragStartGetItems
UIをクリックし、ドラッグしたときにドラッグされている情報を伝える関数
ドラッグされたアイテムの情報が必要なときに呼び出される

**引数**
`comp` - ドラッグを開始するためにクリックされたパネル
`info` - ドラッグに関するすべての情報を含む辞書

ドラッグ操作を開始するためには、このコールバック関数からアイテムのリストを返す必要があります
これらは通常オペレータになりますが、必要に応じて、パラメータやCHOPのチャンネル、Pythonオブジェクトをアイテムリストに入れることができます

**返り値**
`onDragStartGetItems`
| 記述例 |  |
|:-|:-|
|`return [comp]`|ドラッグしたオペレータ自身（デフォルト設定）|
|`return [comp.parent()]`|ドラッグしたパネルの親|
|`return [op('custom1'}, op('custom2')]`|2つのオペレータ|
|`return [comp.par.x]`|ドラッグしたオペレータのパラメータ|
|`return [op('myCHOP')['speed']]`|CHOPの特定のチャンネル|

この返り値をカスタマイズすることで、好きなものをドラッグさせることができます

オペレータをドラッグするというのは簡単に想像ができると思います
ただパラメータやチャンネルをドラッグするというと文面では`???`となるかもしれません
これはCHOPの値をパラメータで使ったり、複数のパラメータをリンクさせるために行っている普段からやっているシンプルな操作になります

こういうの
![Desktop 2022.12.19 - 23.16.09.02 - frame at 0m9s.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175399/892cae3f-2562-9bdf-9042-d727b1325b90.jpeg)


### onDragEnd
ドラッグ完了時に呼び出される関数

`comp` - ドラッグをされたパネル
`info` - ドラッグに関するすべての情報を含む辞書（以下の情報を含む）
|キー|内容|
|:-:|:-:|
|Accepted|ドラッグが受け入れられた場合は true|
|dropResults|ドロップ結果の辞書, onDropGetResults(後述) の返り値|
|dragItems|ドラッグされているオブジェクトのリスト|

このコールバックはドロップが発生した後に実行されるため、ドロップの失敗成功を監視して処理したり、自動で読み込みなどを行えます
ただドロップされる側にもコールバック関数を設定できるので紛らわしくならないような実装が大切です

## パネルをドロップ ターゲットとして設定

ドラッグと同様にいくつかのコールバック関数が設定されています

```python
# callbacks for when associated Panel is being dropped on

def onHoverStartGetAccept(comp, info):
	"""
	Called when comp needs to know if dragItems are acceptable as a drop.

	Args:
		comp: the panel component being hovered over
		info: A dictionary containing all info about hover, including:
			dragItems: a list of objects being dragged over comp
			callbackPanel: the panel Component pointing to this callback DAT
	Returns:
		True if comp can receive dragItems
	"""
	#debug('\nonHoverStartGetAccept comp:', comp.path, '- info:\n', info)
	return True # accept what is being dragged

def onHoverEnd(comp, info):
	"""
	Called when dragItems leave comp's hover area.

	Args:
		comp: the panel component being hovered over
		info: A dictionary containing all info about hover, including:
			dragItems: a list of objects being dragged over comp
			callbackPanel: the panel Component pointing to this callback DAT
	"""
	#debug('\nonHoverEnd comp:', comp.path, '- info:\n', info)

def onDropGetResults(comp, info):
	"""
	Called when comp receives a drop of dragItems. This will only be called if
	onHoverStartGetAccept has returned True for these dragItems.

	Args:
		comp: the panel component being dropped on
		info: A dictionary containing all info about drop, including:
			dragItems: a list of objects being dropped on comp
			callbackPanel: the panel Component pointing to this callback DAT

	Returns:
		A dictionary of results with descriptive keys. Some possibilities:
			'droppedOn': the object receiving the drop
			'createdOPs': list of created ops in order of drag items
			'dropChoice': drop menu choice selected
			'modified': object modified by drop
	"""
	debug('\nonDropGetResults comp:', comp.path, '- info:\n', info)
	return {'droppedOn': comp}

```

### onHoverStartGetAccept
dragItems がドロップとして受け入れられるかどうかを知る必要がある場合に使用します
`comp` - ドロップされたパネル
`info['dragItems']` - コンポーネント上にドラッグされているオブジェクトのリスト

**返り値**
`True/False`
`dragItems`がドロップできるならTrue、そうでなければFalse

#### `dragItems`の確認例

```python
# ドラッグアイテムの要素数の確認
if len(info['dragItems']) == 1:
    debug('要素が一つのもののみ受け付けます！')

# isinstance関数を用いてドラッグアイテムがオペレータかどうかを判断（特定のクラスを判別）
if isinstance(info['dragItems'][0], OP):
    debug('アイテムの一つ目はオペレータです！')

# 確認によく使うクラス:
#	OP: オペレータ
#	COMP, TOP, CHOP, SOP, MAT, DAT: オペレータのカテゴリ
#	panelexecDAT, containerCOMP etc.: 特定のオペレータタイプ
#	Channel: CHOPのチャンネル
#	Par: パラメータ
#	tdu.FileInfo: ファイル

# all関数、リストの内包表現、isinstance関数を用いて、ドラッグアイテムのすべてがDATであるかを判別
if all([isinstance(item, DAT) for item in info['dragItems']]):
    debug('ドラッグされたアイテムは全部DATだよ！')
```

上記のスクリプトなどを用いて、特定のもののみドラッグを受け付けることが可能です

### onHoverEnd
dragItems がコンプのHover領域を離れたときに呼び出されます
`comp` - ドロップされたパネル
`info['dragItems']` - コンポーネント上にドラッグされているオブジェクトのリスト

### onDropGetResults
`onHoverStartGetAccept` が`dragItems` に対して `True` を返した場合にのみ呼び出されます

`comp` - ドロップされたパネル
`info['dragItems']` - コンポーネント上にドラッグされているオブジェクトのリスト

**返り値**
この関数からアイテムのDictを返すことはオプションですが、ドラッグ ソースとドロップ ターゲット間の相互にやり取りができます
Dictには、説明的なキーを含む結果が含まれている必要があります
次のキーと値のペアは標準であり、可能な場合は以下の例に従いましょう
```python
'createdOPs':[<list of created ops in order of corresponding drag item>]
```


どの関数にもデフォルトで`Debug()`でその関数の情報などがプリントできるようにかかれているので、まず始めはその部分のコメントアウトを解除して確認してみましょう


# 実践

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175399/7c63f562-fd77-546b-b6e7-7c1c06ba5da8.png)

今回作成するものはUIからD&Dすることで自分でカスタマイズしたUIをネットワークエディタに複製するというものです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175399/ada1b8a8-cde8-3ac4-93ea-066c9ffcf6bd.png)

まずは適当に`PanelCOMP`でいい感じにUIのパーツとなるコンポーネントを組んでおいてください (Widgetでいいだろとか言わないで)

検索対象にするために作ったコンポーネントにタグをつけます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175399/aec9da5c-91c2-3b8a-776d-e841460dea21.png)
今回は`my-panel`というタグをつけました

次にそれらのタグのついたオペレータをOP FindDATを用いてすべて取得します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175399/589401e7-0a6f-7d68-af77-d7e651b87dfe.png)


`OPFind DAT`のパラメータの`Filter/Tags`に`my-panel`(設定したTag)を打ち込みます
ついでに並び順を楽に決められるようにオペレータの置いてある位置を取っておきましょう
`OPFind DAT`のコールバック関数に以下のプログラムを書きます
```python
def onInitGetColumnNames(dat):
	return ['nodeY']

def onFindOPGetValues(dat, curOp, row):
	return [curOp.nodeY]
```
コールバック関数によって`OPFind DAT`で見つけられた要素に対して追加の情報をTableに入れることが可能となります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175399/7de99694-feb2-9cca-d309-20e209e829c6.png)
取得したNodeYを基準にSortDATで並べ替えを行います

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175399/fb456f19-7c4b-af6f-134e-9662add6cc90.png)
この処理をすることによりネットワークエディタ上での並び順をそのままUIの並び順に使用できます


### 続いて本題のドラッグドロップの処理

`OPFind` を基に作成されたTableからUIを作成します
そこで作成されたCompをドラッグすることによってカスタマイズされたコンポーネントを好きなところに持っていく仕組みです

```python
def onDragStartGetItems(comp, info):
    # それぞれの要素に応じてカスタマイズされているCOMPをドラッグする
	dragItems = [op(comp.op('select_info')[1, 'path'].val)]

	return dragItems

def onDragEnd(comp, info):
	if info['accepted']:
		create_op = info['dropResults']['createdOPs'][0]
	
	# ドロップされ作成されたオペレータに対して処理を行う
	# ここではExpressionが書かれているところを参照切れなどを起こさないように定数に変換
		for o in create_op.ops('*'):
			for p in o.pars():
				if p.mode == ParMode.EXPRESSION:
					p.val = p.eval()
					p.expr = ''
					p.mode = ParMode.CONSTANT
	
```

表示用のUIにはコンポーネント名とプレビューを出しています

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175399/a9d3b984-b331-8f40-ad59-415a1d689352.png)
それぞれのCOMPではDrag/Dropの設定は行っておらず、親の設定を使用する設定を行っています
これにより、一か所でスクリプトを管理することができます
親の設定を使用する場合は一番近い親の設定が使用されます
また共通のスクリプトであったとしても、取れる引数はパネルに応じて来るためしっかりと区別することも可能です


# 終わりに

このような形でドラッグ&ドロップの仕組みをざっくり解説しました
用途に応じてかなり拡張性のある仕組みなのでVJシステムや様々な人の触る可能性のあるコンテンツを作っている方々はぜひ一度検証してみてください！


## Ref

公式Wiki

https://docs.derivative.ca/Drag-and-Drop

サンプルファイルとか

https://github.com/Joe0hara/TD_DragAndDrop

2019年の記事

https://qiita.com/joe0hara/items/a1be5e9e06b8feac6cc1


タグって地味だけど便利なんだよーって記事

https://qiita.com/joe0hara/items/0f81083b5f186a69f5ee