Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Unread
    • Recent
    • Tags
    • Users
    • Login

    Handling Treeview File Drag and Drop Events

    Cinema 4D SDK
    python c++ 2023
    2
    3
    674
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • ferdinandF
      ferdinand
      last edited by ferdinand

      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.

      Cheers,
      Ferdinand

      Result:

      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)
      

      MAXON SDK Specialist
      developers.maxon.net

      DunhouD 1 Reply Last reply Reply Quote 1
      • DunhouD
        Dunhou @ferdinand
        last edited by

        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👍

        70d73b1b-7ec2-4268-a6bf-ab177c0fa783-image.png

        Cheers~
        DunHou

        https://boghma.com
        https://github.com/DunHouGo

        ferdinandF 1 Reply Last reply Reply Quote 0
        • ferdinandF
          ferdinand @Dunhou
          last edited by

          Hey @Dunhou,

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

          Cheers,
          Ferdinand

          MAXON SDK Specialist
          developers.maxon.net

          1 Reply Last reply Reply Quote 0
          • ferdinandF ferdinand referenced this topic on
          • First post
            Last post