Unsolved 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 then TreeViewFunctions.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.




"""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.

    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

    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
            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

    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.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):

            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:

        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)

        return True

if __name__ == "__main__":
    dlg: ListViewDialog = ListViewDialog()
    dlg.Open(c4d.DLG_TYPE_ASYNC, defaultw=600)

MAXON SDK Specialist

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:+1:



Hey @Dunhou,

yeah, I already saw and fixed that in the course of answering this.


MAXON SDK Specialist