Handling Treeview File Drag and Drop Events
-
Dear Community,
this question reached us via mail, and I thought the answer might be interesting for everyone. The question was:
How can I implement a
TreeViewFunctions
with which a user can drag (texture) files into a tree view to add them to the items in the tree view?The general logic is to implement
TreeViewFunctions.AcceptDragObject
to sort out invalid drag data sources and thenTreeViewFunctions.InsertObject
to carry out the valid ones. There is also a bug in the drag handling of image file drag events in Cinema 4D 2023.2 which causes the Picture Viewer to open when it should not. But I thought the general task of also unpacking asset drag events might be interesting for others too.Cheers,
FerdinandResult:
Code:
"""Realizes a dialog with a tree view which accepts textures being dragged into it. Must be run as a Script Manager script. Run the script and start dragging image files or image assets onto the rows in the dialog. Doing so will update them with the URL of the file or asset. Note: There is a regression in 2023.2.2 which causes Cinema 4D to interfere with drag events which end in a tree view. For such events Cinema 4D will still open a dragged image file in the Picture Viewer although the event should be consumed by the tree view. In past versions this was not the case. I am not yet sure if we will consider this a bug or not but I have filed it for now as a regression in our bug tracker. """ import c4d import maxon import os import typing c4d.DRAGTYPE_ASSET: int = 200001016 # Expose the drag type ID for assets as a symbol. class ListItem: """Represents an item in a list managed by a #ListHandler. """ def __init__(self, name: str, value: float, path: str) -> None: """ """ self._name: str = str(name) self._value: float = float(value) self._path: str = str(path) self._isSelected: bool = False @property def Label(self) -> str: """Returns the name and path of the item. """ return f"{self._name}{f'({os.path.split(self._path)[1]})' if self._path else ''}" class ListHandler(c4d.gui.TreeViewFunctions): """Manages a list of #ListItem instances in a TreeView. """ def GetFirst(self, root: list[ListItem], userData: None) -> ListItem | None: """Gets the first item in #root. """ return root[0] if root else None def GetNext(self, root: list[ListItem], userData: None, item: ListItem) -> ListItem | None: """Gets the successor item for #item in #data. """ i: int = root.index(item) return root[i+1] if (i + 1) < len(root) else None def GetPred(self, root: list[ListItem], userData: None, item: ListItem) -> ListItem | None: """Gets the predecessor item for #item in #data. """ i: int = root.index(item) return root[i-1] if (i - 1) >= 0 else None def GetName(self, root: list[ListItem], userData: None, item: ListItem) -> str: """Gets the name of #item in #data. """ return item.Label def Select(self, root: list[ListItem], userData: None, item: ListItem, mode: int) -> None: """Selects #item in #data. """ if mode == c4d.SELECTION_NEW: for other in root: other._isSelected = True if item == other else False else: item._isSelected = True if mode == c4d.SELECTION_ADD else False def IsSelected(self, root: list[ListItem], userData: None, item: ListItem) -> bool: """Returns the selection state of #item in #data. """ return item._isSelected @staticmethod def GetUrlFromImageDragData(data: any, index: int = 0) -> str | None: """Extracts a dragged file or asset URL from image type drag events represented by #data. When the drag #data contains multiple elements, the URL of the element at #index is returned. """ url: str | None = None # This is multiple files being dragged represented by a list of str. if isinstance(data, list) and len(data): data = data[index if len(data) > index else 0] # This is the drag data for a singular file, its path. if isinstance(data, str): url = data # This is data for an asset drag event which is populated. if isinstance(data, maxon.DragAndDropDataAssetArray) and data.GetAssetDescriptions(): # Unpack the asset data for the draged asset at #index. items: tuple[tuple[maxon.AssetDescription, maxon.Url, maxon.String]] = data.GetAssetDescriptions() item: tuple = items[index if len(items) > index else 0] desc: maxon.AssetDescription = item[0] assetUrl: maxon.Url = item[1] # Check if the dragged asset is MediaImage asset, i.e., a texture. subtype: maxon.Id = desc.GetMetaData().Get(maxon.ASSETMETADATA.SubType) if (subtype != maxon.ASSETMETADATA.SubType_ENUM_MediaImage): return url = assetUrl.GetUrl() # Return None when url is the empty string or None, otherwise its value. return url or None def AcceptDragObject(self, root: list[ListItem], userData: None, item: ListItem, dragType: int, dragData: any) -> tuple[bool, int]: """Called by Cinema 4D to decide if #dragData is valid data to be dragged onto #item. Since all rows accept textures being dragged onto them, we can ignore #item here. """ # When this is a image file or asset drag event, try to extract the url for the first item # among the dragged files or assets. url: str | None = None if dragType in (c4d.DRAGTYPE_FILENAME_IMAGE, c4d.DRAGTYPE_ASSET): url = ListHandler.GetUrlFromImageDragData(dragData, 0) # When extracting the #url was successful, indicate that #dragType can replace #item and # that #dragData does not have to be copied before doing so. This is not quite correct, # since #url can modify #item but not replace it. But what we return here as the int # determines the cursor in drag operations and #INSERT_REPLACE will give us a nice little + # icon. return c4d.INSERT_REPLACE, False if url else 0 def InsertObject(self, root: list[ListItem], userData: None, item: ListItem, dragType: int, dragData: any, insertMode: int, doCopy: bool) -> None: """Called by Cinema 4D once a drag event has finished which before has been indicated as valid by #AcceptDragObject. """ # This is pretty straight forward, we just extract the drag data again and assign the URL # to #item. url: str | None = ListHandler.GetUrlFromImageDragData(dragData) if url is None: return item._path = url def GetFloatValue(self, root: list[ListItem], userData: None, item: ListItem, column: int, sliderInfo: dict) -> None: """Defines the slider data and sets the value of the slider at #column of #item in #data. Since this example has only one slider per row, I ignore #column here. """ sliderInfo["minValue"] = 0. sliderInfo["maxValue"] = 1. sliderInfo["value"] = item._value def SetFloatValue(self, root: list[ListItem], userData: None, item: ListItem, column: int, value: float, finalValue: bool) -> None: """Returns the value of the slider at #column of #item in #data. Since this example has only one slider per row, I ignore #column here. NOTE: Since you tie the execution of your logic to the last argument, you called it #mouse_stop, you of course only write the value when it is the final value in a drag session, what you perceived as "not smooth". SetFloatValue() -> set_slider_val() -> mouse_stop condition -> update data model GetFloatValue() -> get_slider_val() -> polls data model (which has not yet been updated) """ item._value = value class ListViewDialog(c4d.gui.GeDialog): """Implements a dialog which uses a #ListHandler to display a list of data. """ ID_LISTVIEW: int = 1000 ID_NAME: int = 2000 ID_VALUE: int = 2001 def __init__(self) -> None: """Initializes the dialog instance. """ self._listView: c4d.gui.TreeViewCustomGui | None = None # The tree view gui. self._listHandler: ListHandler = ListHandler() # The list view handler. self._listData: list[ListItem] = [ # And the list view data. ListItem("Item 0", .5, ""), ListItem("Item 1", 0., ""), ListItem("Item 2", 1., ""), ListItem("Item 3", .75, ""), ] def CreateLayout(self) -> bool: """Adds gadgets to the dialog. """ bc: c4d.BaseContainer = c4d.BaseContainer() bc.SetBool(c4d.TREEVIEW_BORDER, True) bc.SetBool(c4d.TREEVIEW_HAS_HEADER, True) bc.SetBool(c4d.TREEVIEW_HIDE_LINES, False) bc.SetBool(c4d.TREEVIEW_MOVE_COLUMN, True) bc.SetBool(c4d.TREEVIEW_RESIZE_HEADER, True) bc.SetBool(c4d.TREEVIEW_FIXED_LAYOUT, True) bc.SetBool(c4d.TREEVIEW_ALTERNATE_BG, True) self._listView = self.AddCustomGui(ListViewDialog.ID_LISTVIEW, c4d.CUSTOMGUI_TREEVIEW, "", c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, 300, 300, bc) return True if self._listView else False def InitValues(self) -> bool: """Initializes the dialog once the layout has been created. """ layout = c4d.BaseContainer() layout.SetLong(ListViewDialog.ID_NAME, c4d.LV_TREE) layout.SetLong(ListViewDialog.ID_VALUE, c4d.LV_SLIDER) self._listView.SetLayout(2, layout) self._listView.SetHeaderText(ListViewDialog.ID_NAME, "Name") self._listView.SetHeaderText(ListViewDialog.ID_VALUE, "Value") self._listView.SetRoot(self._listData, self._listHandler, None) self._listView.Refresh() return True if __name__ == "__main__": dlg: ListViewDialog = ListViewDialog() dlg.Open(c4d.DLG_TYPE_ASYNC, defaultw=600)
-
Hi @ferdinand ,
The
SetFloatValue()
function in python document missing a parameter finalValue, it will raise an error , people can easily mess it up.Hope the next update can fix it
Cheers~
DunHou -
-