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

    How to implement an image control that can receive and generate image data drag events

    Cinema 4D SDK
    2025 python
    4
    10
    1.3k
    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,

      We recently got a non-public support request about handling drag and drop events for a custom image control in a dialog. And while we have shown this, drag event handling, multiple times before, I thought it does not hurt to answer this publicly, as this very case - an image control - is quite common, and it does not hurt having a bit more verbose public example for it.

      edit: I have updated this topic with new code and a new video, to also handle outgoing asset drag events, and talk a bit about the pros and cons of different approaches, and what you could do when not implementing a code example as I did here, and therefore have a bit more leverage room when it comes to complexity.

      In general, doing all this is possible. It is just that not everything is a ready-made solution for you, where you just call a function. At some point you have to "swim" here yourself, as we cannot write your code for you, but I hope the example helps shedding some light on the probably uncessarily complicated drag handling in Cinema 4D.

      Cheers,
      Ferdinand

      Result

      Code

      """Provides an example for implementing a custom control that can generate and receive drag events.
      
      The example implements a dialog which holds multiple "cards" (BitmapCard) that display an image
      and can receive drag events. The user can drag images from the file system into these cards (file
      paths) or drag texture assets from the Asset Browser into these cards. The cards can also generate
      drag events themselves, e.g., when the user drags from a card, it will generate a drag event. 
      Showcased (selectable via the combo box in the menu of the dialog) are three drag types:
      
      1. File paths: Dragging a card will generate an image file path drag event, which for example could 
         be dragged into a file path field of a shader, onto an object in the viewport, or onto other 
         BitmapCard controls. The advantage of this approach is that we do not have to create
         materials or assets, but can just use the file path directly.
      
      2. Materials: Dragging a card will generate an atom drag event, here populated with a Redshift
         material that has the texture of the card as an input. This material can then be dragged onto
         anything that accepts materials, like objects in the viewport. The disadvantage of this
         approach is that we have to create a material, and that we have to exactly define how the 
         material is constructed.
      
      3. Assets: Dragging a card will generate an asset drag event, here populated with a texture asset
         that has the texture of the card as an input. This asset can then be dragged onto anything that
         texture accepts assets, such as an object in the viewport. The disadvantage of this
         approach is that we have to create an asset.
      
         - The example also showcases a variation oof this approach, where we exploit a bit how the
           texture asset drag handling works, to avoid having to create an asset.
      
      
      Overview:
      
      - BitmapCard: A user area control that can receive drag events and generate drag events itself. Here
                    you will find almost all the relevant code for drag and drop handling
      - BitmapStackDialog: A dialog that holds multiple BitmapCard controls and allows the user to select
                      the drag type via a combo box. This dialog is the main entry point of the example
                      but does not contain much relevant code itself.
      """
      __author__ = "Ferdinand Hoppe"
      __copyright__ = "Copyright 2025 Maxon Computer GmbH"
      
      import os
      import re
      
      import c4d
      import maxon
      import mxutils
      
      # The file types that are supported to be dragged into a BitmapCard control.
      DRAG_IN_FILE_TYPES: list[str] = [".png", ".jpg", ".jpeg", ".tif"]
      
      # A regex to check of a string (which is meant to be a path/url) already as a scheme (like file:///
      # or http:///). The protocol we are mostly checking for is "asset:///", but we keep this regex generic
      # to allow for other schemes as well.
      RE_URL_HAS_SCHEME = re.compile("^[a-zA-Z][a-zA-Z\d+\-.]*:\/\/\/")
      
      # A non-public core message which need when we want to properly generate asset drag events.
      COREMSG_SETCOMMANDEDITMODE: int = 300001000
      
      
      class BitmapCard(c4d.gui.GeUserArea):
          """Implements a control that can receive bitmap path drag events and generates drag events itself.
          """
      
          def __init__(self, host: "BitmapStackDialog") -> None:
              """Constructs a new BitmapCard object.
              """
              # The host dialog that holds this card. This is used to access the drag type selected in the
              # combo box of the dialog.
              self._host: BitmapStackDialog = host
      
              # The image file path and the bitmap that is displayed in this card.
              self._path: str | None = ""
              self._bitmap: c4d.bitmaps.BaseBitmap | None = None
      
          def GetMinSize(self) -> tuple[int, int]:
              """Called by Cinema 4d to evaluate the minimum size of the user area.
              """
              return 250, 100
      
          def DrawMsg(self, x1, y1, x2, y2, msg_ref):
              """Called by Cinema 4D to let the user area draw itself.
              """
              self.OffScreenOn()
              self.SetClippingRegion(x1, y1, x2, y2)
      
              # Draw the background and then the bitmap if it exists. In the real world, we would have to
              # either adapt #GetMinSize to the size of the bitmap, or draw the bitmap in a way that it
              # fits into the user area, e.g., by scaling it down. Here we just draw it at a fixed size.
              self.DrawSetPen(c4d.gui.GetGuiWorldColor(c4d.COLOR_BGGADGET))
              self.DrawRectangle(x1, y1, x2, y2)
              if self._bitmap:
                  w, h = self._bitmap.GetSize()
                  self.DrawBitmap(self._bitmap, 5, 5, 240, 90, 0,
                                  0, w, h, c4d.BMP_NORMALSCALED)
      
          def Message(self, msg: c4d.BaseContainer, result: c4d.BaseContainer) -> int:
              """Called by Cinema 4D to handle messages sent to the user area.
      
              Here we implement receiving drag events when the user drags something onto this user area.
              """
              # A drag event is coming in.
              if msg.GetId() == c4d.BFM_DRAGRECEIVE:
                  # Get out when the drag event has been discarded.
                  if msg.GetInt32(c4d.BFM_DRAG_LOST) or msg.GetInt32(c4d.BFM_DRAG_ESC):
                      return self.SetDragDestination(c4d.MOUSE_FORBIDDEN)
      
                  # Get out when this is not a drag type we support (we just support images). Note that we
                  # cannot make up drag types ourselves, so when we want to send/receive complex data, we
                  # must use the DRAGTYPE_ATOMARRAY type and pack our data into a BaseContainer attached
                  # to the nodes we drag/send.
                  data: dict = self.GetDragObject(msg)
                  dragType: int = data.get("type", 0)
                  if dragType not in [c4d.DRAGTYPE_FILENAME_IMAGE, maxon.DRAGTYPE_ASSET]:
                      return self.SetDragDestination(c4d.MOUSE_FORBIDDEN)
      
                  # Here we could optionally check the drag event hitting some target area.
                  # yPos: float = self.GetDragPosition(msg).get('y', 0.0)
                  # if not 0 < yPos < 50 or not self.CheckDropArea(msg, True, True):
                  #     return self.SetDragDestination(c4d.MOUSE_FORBIDDEN)
      
                  # After this point, we are dealing with a valid drag event.
      
                  # The drag is still going on, we just set the mouse cursor to indicate that we are
                  # ready to receive the drag event.
                  if msg.GetInt32(c4d.BFM_DRAG_FINISHED) == 0:
                      return self.SetDragDestination(c4d.MOUSE_MOVE)
                  # The drag event is finished, we can now process the data.
                  else:
                      # Unpack a file being dragged directly.
                      path: str | None = None
                      if dragType == c4d.DRAGTYPE_FILENAME_IMAGE:
                          path = data.get("object", None)
                      # Unpack an asset being dragged.
                      elif dragType == maxon.DRAGTYPE_ASSET:
                          array: maxon.DragAndDropDataAssetArray = data.get(
                              "object", None)
                          descriptions: tuple[tuple[maxon.AssetDescription, maxon.Url, maxon.String]] = (
                              array.GetAssetDescriptions())
                          if not descriptions:
                              # Invalid asset but we are not in an error state.
                              return True
      
                          # Check that we are dealing with an image asset.
                          asset: maxon.AssetDescription = descriptions[0][0]
                          metadata: maxon.BaseContainer = asset.GetMetaData()
                          subType: maxon.Id = metadata.Get(
                              maxon.ASSETMETADATA.SubType, maxon.Id())
                          if (subType != maxon.ASSETMETADATA.SubType_ENUM_MediaImage):
                              # Invalid asset but we are not in an error state.
                              return True
      
                          path = str(maxon.AssetInterface.GetAssetUrl(asset, True))
      
                      if not isinstance(path, str):
                          return False  # Critical failure.
      
                      if (dragType == c4d.DRAGTYPE_FILENAME_IMAGE and
                          (not os.path.exists(path) or
                           os.path.splitext(path)[1].lower() not in DRAG_IN_FILE_TYPES)):
                          # Invalid file type but we are not in an error state.
                          return True
      
                      self._path = path
                      self._bitmap = c4d.bitmaps.BaseBitmap()
                      if self._bitmap.InitWith(path)[0] != c4d.IMAGERESULT_OK:
                          return False  # Critical failure.
      
                      self.Redraw()  # Redraw the user area to show the new bitmap.
      
                      return True
      
              # Process other messages, doing this is very important, as we otherwise break the
              # message chain.
              return c4d.gui.GeUserArea.Message(self, msg, result)
      
          def InputEvent(self, msg: c4d.BaseContainer) -> bool:
              """Called by Cinema 4D when the user area receives input events.
      
              Here we implement creating drag events when the user drags from this user area. The type of
              drag event which is initiated is determined by the drag type selected in the combo box
              of the dialog.
              """
              # When this is not a left mouse button event on this user area, we just get out without
              # consuming the event (by returning False).
              if (msg.GetInt32(c4d.BFM_INPUT_DEVICE) != c4d.BFM_INPUT_MOUSE or
                      msg.GetInt32(c4d.BFM_INPUT_CHANNEL) != c4d.BFM_INPUT_MOUSELEFT):
                  return False
      
              # Get the type of drag event that should be generated, and handle it.
              dragType: int = self._host.GetInt32(self._host.ID_DRAG_TYPE)
              return self.HandleDragEvent(msg, dragType)
          
          # --- Custom drag handling methods -------------------------------------------------------------
      
          # I have split up thing into three methods for readability, this all could also be done directly
          # in the InputEvent method.
      
          def HandleDragEvent(self, event: c4d.BaseContainer, dragType: int) -> bool:
              """Handles starting a drag event by generating the drag data and sending it to the system.
      
              This is called when the user starts dragging from this user area.
              """
              # This requires us to modify the document, so we must be on the main thread (technically
              # not true when the type is DRAGTYPE_FILENAME_IMAGE, but that would be for you to optimize).
              if not c4d.threading.GeIsMainThread():
                  raise False
      
              # Generate our drag data, either a file path, a material, or an asset.
              doc: c4d.documents.BaseDocument = c4d.documents.GetActiveDocument()
              data: object = self.GenerateDragData(doc, dragType)
      
              # Now we set off the drag event. When we are dragging assets, we have to sandwich the event
              # in these core message calls, as otherwise the palette edit mode toggling will not work
              # correctly.
              if dragType == maxon.DRAGTYPE_ASSET:
                  bc: c4d.BaseContainer = c4d.BaseContainer(
                      COREMSG_SETCOMMANDEDITMODE)
                  bc.SetInt32(1, True)
                  c4d.SendCoreMessage(c4d.COREMSG_CINEMA, bc, 0)
      
              # When #HandleMouseDrag returns #False, this means that the user has cancelled the drag
              # event, one case could be that the user actually did not intend to drag, but just
              # clicked on the user area.
              #
              # In this case we remove the drag data we generated, as it is not needed anymore. Note that
              # this will NOT catch the case that the drag event is cancelled by the recipient, e.g., the
              # user drags a material onto something that does not accept materials.
              if not self.HandleMouseDrag(event, dragType, data, 0):
                  self.RemoveDragData(doc, data)
                  return True
      
              # Other half of the sandwich, we toggle the palette edit mode back to normal.
              if dragType == maxon.DRAGTYPE_ASSET:
                  bc: c4d.BaseContainer = c4d.BaseContainer(
                      COREMSG_SETCOMMANDEDITMODE)
                  bc.SetInt32(1, False)
                  c4d.SendCoreMessage(c4d.COREMSG_CINEMA, bc, 0)
      
              return True
      
          def GenerateDragData(self, doc: c4d.documents.BaseDocument, dragType
                               ) -> str | c4d.BaseMaterial | maxon.DragAndDropDataAssetArray:
              """Generates the drag data for the given drag type.
      
              Each tile just encapsulates a file path, but we realize dragging them as file paths,
              materials, or assets. So, when the type is material or asset, the drag data must be
              generated from the file path. Which is why we have this method here.
              """
              if not c4d.threading.GeIsMainThread():
                  raise RuntimeError(
                      "GenerateDragData must be called from the main thread.")
      
              # The user has selected "File" as the drag type, we just return the file path.
              if dragType == c4d.DRAGTYPE_FILENAME_IMAGE:
                  return self._path
              # The user has selected "Material" as the drag type, we create a Redshift material with the
              # texture as input and return that material.
              elif dragType == c4d.DRAGTYPE_ATOMARRAY:
                  material: c4d.BaseMaterial = mxutils.CheckType(
                      c4d.BaseMaterial(c4d.Mmaterial))
      
                  # Create a simple graph using that texture, and insert the material into the
                  # active document.
                  maxon.GraphDescription.ApplyDescription(
                      maxon.GraphDescription.GetGraph(
                          material, maxon.NodeSpaceIdentifiers.RedshiftMaterial),
                      {
                          "$type": "Output",
                          "Surface": {
                              "$type": "Standard Material",
                              "Base/Color": {
                                  "$type": "Texture",
                                  # Set the texture. When our source is a file we will just have a plain
                                  # path, e.g. "C:/path/to/file.png" and we have to prefix with a scheme
                                  # (file) to make it a valid URL. When we are dealing with an asset, the
                                  # texture path will already be a valid URL in the "asset:///" scheme.
                                  "Image/Filename/Path": (maxon.Url(f"{self._path}")
                                                          if RE_URL_HAS_SCHEME.match(self._path) else
                                                          maxon.Url(f"file:///{self._path}"))
                              }
                          }
                      })
                  doc.InsertMaterial(material)
                  return [material]
              # The user has selected "Asset" as the drag type, we create a texture asset and return that.
              elif dragType == maxon.DRAGTYPE_ASSET:
                  # So we have to cases here, either that we want truly want to generate an asset drag
                  # event or that we just want to piggy-back onto the texture asset drag and drop
                  # mechanism of Cinema 4D, which is what we do here.
      
                  # Code for truly generating an asset drag event, where we would store the asset in the
                  # scene repository.
                  generateAssets: bool = False
                  if generateAssets:
                      # Get the scene repository and create an asset storage structure for the asset.
                      repo: maxon.AssetRepository = doc.GetSceneRepository(True)
                      store: maxon.StoreAssetStruct = maxon.StoreAssetStruct(
                          maxon.Id(), repo, repo)
      
                      # Now save the texture asset to the repository.
                      url: maxon.Url = maxon.Url(self._path)
                      name: str = url.GetName()
                      asset, _ = maxon.AssetCreationInterface.SaveTextureAsset(
                          url, name, store, (), True)
      
                      # Create a drag array for that asset. It is important that we use the URL of the
                      # asset (maxon.AssetInterface.GetAssetUrl) and not #url or asset.GetUrl(), as both
                      # are physical file paths, and will then cause the drag and drop handling to use
                      # these physical files, including the popup asking for wether the file should be
                      # copied. We will exploit exactly this behavior in the second case.
                      dragArray: maxon.DragAndDropDataAssetArray = maxon.DragAndDropDataAssetArray()
                      dragArray.SetLookupRepository(repo)
                      dragArray.SetAssetDescriptions((
                          (asset, maxon.AssetInterface.GetAssetUrl(asset, True), maxon.String(name)),
                      ))
      
                      return dragArray
                  # With this case we can piggy-back onto the existing drag and drop texture asset
                  # handling of Cinema 4D, without actually having to create an asset. THIS IS A
                  # WORKAROUND, we could decide at any moment to change how the drag and drop
                  # handling of assets works, possibly rendering this approach obsolete.
                  else:
                      # The slight disadvantage of this approach (besides it being a hack and us possibly
                      # removing it at some point) is that we have to pay the download cost of
                      # #self._host._dummyAssetId once. I.e., the user has to download that texture once
                      # either by using it in the Asset Browser or by running this code. Once the asset has
                      # been cached, this will not happen again.
                      #
                      # But since we search below in a 'whoever comes first' manner, what is the first
                      # texture asset could change, and this could then be an asset which has not been
                      # downloaded yet. So, in a more robust world we would pick a fixed asset below. But
                      # this comes then with the burden of maintenance, as we could remove one of the
                      # builtin texture assets at any time. So, the super advanced solution would be to
                      # ship the plugin with its own asset database, and mount and use that. Here we could
                      # use a hard-coded asset ID, and would have to have never pay download costs, as
                      # the repository would be local.
      
                      # Find the asset by its Id, which we have stored in the dialog.
                      repo: maxon.AssetRepositoryInterface = maxon.AssetInterface.GetUserPrefsRepository()
                      dummy: maxon.AssetDescription = repo.FindLatestAsset(
                          maxon.AssetTypes.File().GetId(), self._host._dummyAssetId, maxon.Id(),
                          maxon.ASSET_FIND_MODE.LATEST)
      
                      # Setup the drag array with the asset. Note that #asset must be a valid texture
                      # asset, so just passing maxon.AssetDescription() will not work. But the backend
                      # for drag and drop handling will actually ignore the asset except for type checking
                      # it, and use the URL we provided instead.
                      dragArray: maxon.DragAndDropDataAssetArray = maxon.DragAndDropDataAssetArray()
                      dragArray.SetLookupRepository(repo)
                      dragArray.SetAssetDescriptions((
                          (dummy, maxon.Url(self._path), maxon.String(os.path.basename(self._path))),
                      ))
                  return dragArray
              else:
                  raise ValueError(f"Unsupported drag type: {dragType}")
      
          def RemoveDragData(self, doc: c4d.documents.BaseDocument,
                             data: str | list[c4d.BaseMaterial] | maxon.DragAndDropDataAssetArray) -> bool:
              """Removes generated content when the user cancels a drag event.
      
              Sometimes the user starts a drag event, but then cancels it, e.g., by pressing the
              escape key. In this case, we have to remove the generated content, e.g., the material or
              asset that we created for the drag event.
              """
              if not c4d.threading.GeIsMainThread():
                  return False
      
              if isinstance(data, list):
                  # Remove the dragged materials from the document. Since we are always generating a new
                  # material, we can just remove them. That is in general probably not the best design,
                  # and a real world application should avoid duplicating materials.
                  for item in data:
                      if isinstance(item, c4d.BaseList2D):
                          item.Remove()
              elif isinstance(data, str):
                  # Here we could technically remove a file.
                  pass
              elif isinstance(data, maxon.DragAndDropDataAssetArray):
                  # We could remove the asset, but other than for the material, there is no guarantee
                  # that we are the only user of it, as the Asset API has a duplicate preventing
                  # mechanism. So, the #SaveTextureAsset call above could have returned an already
                  # existing asset. Since we could operate with a document bound repository, we could
                  # search the whole document for asset references and only when we find none, remove
                  # the asset.
                  #
                  # We use here the little hack that we actually do not create an asset, to just to piggy-
                  # back onto the material drag and drop mechanism of assets, so we can just ignore
                  # this case.
                  pass
      
              return True
      
      
      class BitmapStackDialog(c4d.gui.GeDialog):
          """Implements a a simple dialog that stacks multiple BitmapCard controls.
          """
          ID_DRAG_TYPE: int = 1002
      
          def __init__(self):
              """Constructs a new ExampleDialog object.
              """
              # The user area controls that will generate and receive drag events.
              self._cards: list[BitmapCard] = [
                  BitmapCard(host=self) for _ in range(4)]
      
              # The id for the dummy asset that used to generate 'fake' texture asset drag events. We
              # search for it here once, so that we do not have to do it in the __init__ of each
              # BitmapCard, or even worse, in the HandleDragEvent of each BitmapCard.
      
              # We could technically also store here the AssetDescription instead of the Id, but that
              # reference can go stale, so we just store the Id and then grab with it the asset when we
              # need it. Which is much faster than searching asset broadly as we do here. In the end, we
              # could probably also do all this in the drag handling of the BitmapCard, but a little bit
              # of optimization does not hurt.
              self._dummyAssetId: maxon.Id | None = None
              if not maxon.AssetDataBasesInterface.WaitForDatabaseLoading():
                  raise RuntimeError("Could not initialize the asset databases.")
      
              def findTexture(asset: maxon.AssetDescription) -> bool:
                  """Finds the first texture asset in the user preferences repository.
                  """
                  # When this is not a texture asset, we just continue searching.
                  meta: maxon.AssetMetaData = asset.GetMetaData()
                  if (meta.Get(maxon.ASSETMETADATA.SubType, maxon.Id()) !=
                          maxon.ASSETMETADATA.SubType_ENUM_MediaImage):
                      return True
      
                  # When it is a texture asset, we store it as the dummy asset and stop searching.
                  self._dummyAssetId = asset.GetId()
                  return False
      
              # Search for our dummy asset in the user preferences repository.
              repo: maxon.AssetRepositoryInterface = maxon.AssetInterface.GetUserPrefsRepository()
              repo.FindAssets(maxon.AssetTypes.File().GetId(), maxon.Id(), maxon.Id(),
                              maxon.ASSET_FIND_MODE.LATEST, findTexture)
      
          def CreateLayout(self):
              """Called by Cinema 4D to populate the dialog with controls.
              """
              self.SetTitle("Drag and Drop Example")
              self.GroupSpace(5, 5)
              self.GroupBorderSpace(5, 5, 5, 5)
      
              # Build the combo box in the menu bar of the dialog.
              self.GroupBeginInMenuLine()
              self.GroupBegin(1000, c4d.BFH_LEFT | c4d.BFV_TOP, cols=2)
              self.GroupBorderSpace(5, 5, 5, 5)
              self.GroupSpace(5, 5)
              self.AddStaticText(1001, c4d.BFH_LEFT |
                                 c4d.BFV_CENTER, name="Drag Events as:")
              self.AddComboBox(1002, c4d.BFH_RIGHT | c4d.BFV_TOP)
              self.GroupEnd()
              self.GroupEnd()
      
              # Add the BitmapCard controls to the dialog.
              for i, card in enumerate(self._cards):
                  self.AddUserArea(2000 + i, c4d.BFH_LEFT | c4d.BFV_TOP)
                  self.AttachUserArea(card, 2000 + i)
      
              # Add items to the combo box to select the drag type.
              self.AddChild(self.ID_DRAG_TYPE, c4d.DRAGTYPE_FILENAME_IMAGE, "Files")
              self.AddChild(self.ID_DRAG_TYPE, c4d.DRAGTYPE_ATOMARRAY, "Materials")
              self.AddChild(self.ID_DRAG_TYPE, maxon.DRAGTYPE_ASSET, "Assets")
              self.SetInt32(self.ID_DRAG_TYPE, c4d.DRAGTYPE_FILENAME_IMAGE)
      
              return True
      
      
      # Define a global variable of the dialog to keep it alive in a Script Manager script. ASYNC dialogs
      # should not be opened in production code from a Script Manager script, as this results in a
      # dangling dialog. Implement a command plugin when you need async dialogs in production.
      dlg: BitmapStackDialog = BitmapStackDialog()
      if __name__ == "__main__":
          dlg.Open(dlgtype=c4d.DLG_TYPE_ASYNC, defaultw=150, defaulth=250)
      
      

      MAXON SDK Specialist
      developers.maxon.net

      DunhouD 1 Reply Last reply Reply Quote 5
      • ferdinandF
        ferdinand
        last edited by

        I cleaned up the thread, removed older code and videos, and updated the first posting, to keep this readable for future readers. The example now also discusses and realizes outgoing asset drag events.

        MAXON SDK Specialist
        developers.maxon.net

        1 Reply Last reply Reply Quote 0
        • DunhouD
          Dunhou
          last edited by

          What a detailed explanation! Love these technical videos!

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

          1 Reply Last reply Reply Quote 0
          • gheyretG
            gheyret
            last edited by

            So detailed example as always, this is really helpfull!

            www.boghma.com

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

              Hey @ferdinand ,

              An extension issue, which is actually the original intention of this issue, I followed your suggestion and implemented "user seamless interaction" using a timer, but there is still one issue that troubles me.

              How to exclude mouse clicks before dragging event.

              • The reason I need to do this is that before clicking, there may not be any data on the hard drive to generate the material. After dragging and dropping, I want to perform a data check, but I don't want to trigger a download task every time I click
              • I tried using MouseDragStart polling to determine the position of the mouse when it is released, in order to determine whether the action should be considered a click, but at this point, the print ('try to start drag event ') did trigger, but there was no HandleMouseDrag action, and there was no interaction when the mouse moved over the object.
              • I followed the suggestion here and tried using GetInputState to determine mouse release, but I couldn't get rid of this limitation

              Uncertain alternative methods require testing

              • Generate empty material, if it is a valid dragging, check data and replace materials after successful execution, which may be feasible but requires a lot of additional code.
              • Generate an empty material, but download the address in the texture path after success, and then refresh the material.

              These methods certainly cannot separate clicking and dragging intuitively. Is it possible to achieve this?

              get idea from https://developers.maxon.net/forum/topic/16267/marquee-selection-of-items-in-geuserarea

              Cheers~
              DunHou

                  def InputEvent(self, msg: c4d.BaseContainer) -> bool:
              
                      
                      mx = int(msg[c4d.BFM_INPUT_X])
                      my = int(msg[c4d.BFM_INPUT_Y])
                      gx,gy = mx,my
                      mx -= self.Local2Global()["x"]
                      my -= self.Local2Global()["y"]
              
                      channel = msg[c4d.BFM_INPUT_CHANNEL]
                      state = c4d.BaseContainer()
                      mousex = mx
                      mousey = my
              
                      if channel == c4d.BFM_INPUT_MOUSELEFT:
                          # res, dx, dy, channels = self.MouseDrag()
                          # print(res, dx, dy, channels)
                          self.MouseDragStart(
                              c4d.BFM_INPUT_MOUSELEFT,
                              mx,
                              my,
                              c4d.MOUSEDRAGFLAGS_DONTHIDEMOUSE,
                          )
                          while True:
                              res, dx, dy, channels = self.MouseDrag()
                              
                              if res == c4d.MOUSEDRAGRESULT_ESCAPE:
                                  self._drag_enabled = False
                                  break                
                              elif res == c4d.MOUSEDRAGRESULT_CONTINUE:
                                  mx -= dx
                                  my -= dy
                                  
                              if not self.GetInputState(c4d.BFM_INPUT_MOUSE, c4d.BFM_INPUT_MOUSELEFT, state):
                                  break
                              
                              if state[c4d.BFM_INPUT_VALUE] == 0:
                                  print ("Released Left Mouse")
                                  break
              
                              if dx == 0 and dy == 0:
                                  continue
                          
                              mousex += dx
                              mousey += dy
                              print("Mouse Dragging at position [%f,%f]" % (mousex, mousey))
                              
                              if res == c4d.MOUSEDRAGRESULT_FINISHED:
                                  print("dra finished")
                                  # print(mx,my)
                                  # print(f"inside: {self.is_inside(mx, my)}")
                                  if self.is_inside(mx, my):
                                      print("end inside")
                                      if min(abs(gx-mx), abs(gy-my)) < 20:
                                          print("move little, treat as click")
                                          self._drag_enabled = False
                                          return True
                                      break
                                  else:
                                      self._drag_enabled = True
                                      print("end outside, drag enabled")
                                      break
              
                          self.MouseDragEnd()
              
                      if self._drag_enabled:
                          print('try to start drag event')
                          dragType: int = self._host.GetInt32(self._host.ID_DRAG_TYPE)
                          return self.HandleDragEvent(msg, dragType)
                      return True
              

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

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

                Hey @Dunhou,

                I am not sure if I understand the question. For HandleMouseDrag, this applies:

                        # When #HandleMouseDrag returns #False, this means that the user has cancelled the drag
                        # event, one case could be that the user actually did not intend to drag, but just
                        # clicked on the user area.
                        #
                        # In this case we remove the drag data we generated, as it is not needed anymore. Note that
                        # this will NOT catch the case that the drag event is cancelled by the recipient, e.g., the
                        # user drags a material onto something that does not accept materials.
                        if not self.HandleMouseDrag(event, dragType, data, 0):
                            self.RemoveDragData(doc, data)
                            return True
                

                For normal input polling, i.e., an BFM_INPUT event there is BFM_ACTION_INDRAG (which is at least emitted for sliders) and BFM_DRAGSTART and BFM_DRAGEND. See : Gui Messages. But you are doing continuous drag polling via MouseDragStart anyway.

                So, when I boil things down here, a normal click BFM_INPUT event does not contain any drag related markers/data. Which is what you are asking? I.e., when you do input handling in your dialog, and want to sort out your own dragging events, you should look for one of these drag event markers (or their absence)

                This might help: getting-only-the-last-value-of-the-slider-slide. But as stated above, I think BFM_ACTION_INDRAG is only emitted for sliders.

                Cheers,
                Ferdinand

                MAXON SDK Specialist
                developers.maxon.net

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

                  Hey @ferdinand ,

                  Sorry for silence, quite busy last days, anyway, back to the question.

                  It should be noted that from the results, I have found an alternative way to implement it as mentioned above. I just want to review the diagram to understand the logic of the input event
                  I'm not questioning the rationality of this setting, I just want to know if there's a way to control it more finely

                  A more precise question may be control over clicking and dragging, e.g. :

                  • Assume Cinema consider a click down and release under 0.1sencond and mouse position didn't change more than 2 pixels as a BFM_INPUT_MOUSELEFT, and hold on more than 0.1s and mouse move as HandleMouseDrag.
                  • Under this premise, if the user accidentally touches and causes the mouse to quickly click and the cursor to move some distance, this behavior will be considered as dragging.
                  • I hope this behavior is seen as a click (I know this is a bit strange, I just want to try and see if it can be achieved)
                  • I tried to use c4d.BFM-INPUT_LUE to retrieve mouse release, but as far as I know, this needs to be executed in a while loop.
                  • When there is a loop in InputEvent, the conditions I set are judged correctly, but the drag time is masked, which means that the drag behavior cannot be triggered(e.g., no plus under the cursor, and no material created).

                  like our asset browser, click or drag between the green arrow will not trigger the download progress. only click download/double click/drag outside will download and assign the asset.

                  88e2f25a-efa8-40a8-8a48-8cef92778f32-image.png

                  Cheers~
                  DunHou

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

                  1 Reply Last reply Reply Quote 0
                  • M
                    m_adam
                    last edited by m_adam

                    Did you had a look at geuserarea_drag_r13.py? There is no else statement but that would be the perfect place to do something specific on single click. But there is no built-in solution you have to kind of track all by yourself as done line 294.

                    Cheers,
                    Maxime.

                    MAXON SDK Specialist

                    Development Blog, MAXON Registered Developer

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

                      Hey @m_adam ,

                      Yes , I had a look at this example, but I'm confusing to understand how it can apply to my case, sorry for my foolish brain.

                      If I keep InputEvent as @ferdinand did in the example, HandleMouseDrag will return True if I have a little move, I try to stop this,
                      so I want to execute HandleDragEvent only when mouse "obvious interaction outside the ua",to avoid the accompanying effect of dragging when lightly clicked (download) .

                          def InputEvent(self, msg: c4d.BaseContainer) -> bool:
                              """Called by Cinema 4D when the user area receives input events.
                      
                              Here we implement creating drag events when the user drags from this user area. The type of
                              drag event which is initiated is determined by the drag type selected in the combo box
                              of the dialog.
                              """
                              # When this is not a left mouse button event on this user area, we just get out without
                              # consuming the event (by returning False).
                              if (msg.GetInt32(c4d.BFM_INPUT_DEVICE) != c4d.BFM_INPUT_MOUSE or
                                      msg.GetInt32(c4d.BFM_INPUT_CHANNEL) != c4d.BFM_INPUT_MOUSELEFT):
                                  return False
                      
                              # Get the type of drag event that should be generated, and handle it.
                              dragType: int = self._host.GetInt32(self._host.ID_DRAG_TYPE)
                              return self.HandleDragEvent(msg, dragType)
                      

                      my code did stop the HandleDragEvent execute if I didn't want to.

                      but in this situation, even if I print and execute HandleDragEvent successfully, C4D does not accept drag events, which means the material cannot be assigned to the object. I don't know what is preventing this.

                          def InputEvent(self, msg: c4d.BaseContainer) -> bool:
                              """Called by Cinema 4D when the user area receives input events.
                      
                              Here we implement creating drag events when the user drags from this user area. The type of
                              drag event which is initiated is determined by the drag type selected in the combo box
                              of the dialog.
                              """
                              # When this is not a left mouse button event on this user area, we just get out without
                              # consuming the event (by returning False).
                              if msg[c4d.BFM_INPUT_DEVICE] != c4d.BFM_INPUT_MOUSE and msg[c4d.BFM_INPUT_CHANNEL] != c4d.BFM_INPUT_MOUSELEFT:
                                  return False
                              
                              dragType: int = self._host.GetInt32(self._host.ID_DRAG_TYPE)
                              
                              if msg.GetBool(c4d.BFM_INPUT_DOUBLECLICK):
                                  print("Double click detected, generating drag event")
                                  return True
                         
                              mx = int(msg[c4d.BFM_INPUT_X])
                              my = int(msg[c4d.BFM_INPUT_Y])
                              mx -= self.Local2Global()["x"]
                              my -= self.Local2Global()["y"]
                              # print(f"Start mouse: {mx}, {my}")
                      
                              state = c4d.BaseContainer()
                      
                              self.MouseDragStart(c4d.BFM_INPUT_MOUSELEFT,mx,my,c4d.MOUSEDRAGFLAGS_DONTHIDEMOUSE|c4d.MOUSEDRAGFLAGS_NOMOVE)
                              isFirstTick = True
                              s = 0
                              dua = 0
                              
                              while True:
                                  res, dx, dy, channels = self.MouseDrag()
                                  if res != c4d.MOUSEDRAGRESULT_CONTINUE:
                                      break  
                      
                                  mx -= dx
                                  my -= dy
                                  
                                  self.GetInputState(c4d.BFM_INPUT_MOUSE, c4d.BFM_INPUT_MOUSELEFT, state)
                                  
                                  # mouse released, this can triggered by a click or a drag end
                                  if state[c4d.BFM_INPUT_VALUE] == 0:
                                      dua = time.perf_counter() - s
                                      print (f"Released Mouse in {dua:.4f} seconds")
                                      
                                      # drag too short, not starting drag event
                                      if dua < 0.15:
                                          return False
                                      break                
                      
                                  if isFirstTick:
                                      isFirstTick = False
                                      s = time.perf_counter()
                                      print(f"\t-- first click : {mx}, {my}")
                                      continue
                      
                              endState = self.MouseDragEnd()
                              if endState == c4d.MOUSEDRAGRESULT_FINISHED:
                                  print(f"\t-- drag finished : {mx}, {my}")
                                  
                                  # drag end inside ua, not generating drag event
                                  if (0 <= mx <= 0 + self.width and 0 <= my <= 0 + self.height): 
                                      return False
                      
                              print('Now ,try to start drag event')        
                              return self.HandleDragEvent(msg, dragType)
                      

                      Cheers~
                      DunHou

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

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

                        Hey @Dunhou,

                        I am still not 100% clear about what you are trying to do. But I guess what you want to do is distinguish a single-drag -click, i.e., the user is dragging something, from a single click. The issue with that is that we are in your code inside a while loop which just polls the input state as fast as it can and not in message stream, where we only get events for state changes. So, this means unless there is Speedy Gonzales at the mouse, even the quickest of single clicks will produce more than one iteration in the loop.

                        What is still unclear to me why you are doing all this, as knowing that the mouse is outside of the UA does not mean that we know if the user dropped the payload on an object. But this is how I would solve distinguishing a 'light click' (a single click) from a drag event.

                        A cleaner solution might be to let the convenance function InputEvent be a convenance function and move to the source Message. There you should be issue start and stop events for drag operations. But since you want to start it yourself, we are sort of in a pickle. I would have to play around a bit with the code to see if there is a better way with Message,

                        Cheers,
                        Ferdinand

                        def InputEvent(self, msg: c4d.BaseContainer) -> bool:
                            """Called by Cinema 4D when the user area receives input events.
                        
                            Here we implement creating drag events when the user drags from this user area. The type of
                            drag event which is initiated is determined by the drag type selected in the combo box
                            of the dialog.
                            """
                            # When this is not a left mouse button event on this user area, we just get out without
                            # consuming the event (by returning False).
                            if msg[c4d.BFM_INPUT_DEVICE] != c4d.BFM_INPUT_MOUSE and msg[c4d.BFM_INPUT_CHANNEL] != c4d.BFM_INPUT_MOUSELEFT:
                                return False
                            
                            dragType: int = self._host.GetInt32(self._host.ID_DRAG_TYPE)
                            mx = int(msg[c4d.BFM_INPUT_X])
                            my = int(msg[c4d.BFM_INPUT_Y])
                            mx -= self.Local2Global()["x"]
                            my -= self.Local2Global()["y"]
                        
                            state = c4d.BaseContainer()
                            self.MouseDragStart(c4d.BFM_INPUT_MOUSELEFT,mx,my,c4d.MOUSEDRAGFLAGS_DONTHIDEMOUSE|c4d.MOUSEDRAGFLAGS_NOMOVE)
                            lastPos: tuple[float, float] | None = None
                        
                            while True:
                                res, dx, dy, channels = self.MouseDrag()
                                if res != c4d.MOUSEDRAGRESULT_CONTINUE:
                                    break  
                                
                                self.GetInputState(c4d.BFM_INPUT_MOUSE, c4d.BFM_INPUT_MOUSELEFT, state)
                                
                                # This is how I debugged this, GetContainerTreeString (in the beta it might be already
                                # contained) is a feature of a future version of the SDK.
                                # print(f"{mxutils.GetContainerTreeString(state, 'BFM_')}")
                        
                                # State: Root (None , id = -1):
                                # ├── BFM_INPUT_QUALIFIER (DTYPE_LONG): 0
                                # ├── BFM_INPUT_MODIFIERS (DTYPE_LONG): 0
                                # ├── BFM_INPUT_DEVICE (DTYPE_LONG): 1836021107
                                # ├── BFM_INPUT_CHANNEL (DTYPE_LONG): 1
                                # ├── BFM_INPUT_VALUE (DTYPE_LONG): 1
                                # ├── BFM_INPUT_VALUE_REAL (DTYPE_REAL): 0.0001
                                # ├── BFM_INPUT_X (DTYPE_REAL): 203.13671875
                                # ├── BFM_INPUT_Y (DTYPE_REAL): 88.0390625
                                # ├── BFM_INPUT_Z (DTYPE_REAL): 0.0
                                # ├── BFM_INPUT_ORIENTATION (DTYPE_REAL): 0.0
                                # ├── 1768977011 (DTYPE_REAL): 1.0
                                # ├── BFM_INPUT_TILT (DTYPE_REAL): 0.0
                                # ├── BFM_INPUT_FINGERWHEEL (DTYPE_REAL): 0.0
                                # ├── BFM_INPUT_P_ROTATION (DTYPE_REAL): 0.0
                                # └── BFM_INPUT_DOUBLECLICK (DTYPE_LONG): 0
                        
                                # I.e., we are unfortunately neither being issued a BFM_DRAGSTART nor an 
                                # c4d.BFM_INTERACTSTART, I assume both or only emitted in the direct Message() loop.
                        
                                # But we can write code like this.
                        
                                # if state[c4d.BFM_INPUT_DOUBLECLICK]:
                                #     print(f"Double click detected at {mx}, {my}")
                                #     break
                                # elif state[c4d.BFM_INPUT_VALUE] != 1:
                                #     print(f"Mouse button not pressed anymore at {mx}, {my}")
                                #     break
                                # else:
                                #     print(f"Non double click at {mx}, {my}")
                        
                                # The issue with this is that we are here just in a loop polling the current left button
                                # state, not inside a message function where we get a state stream. So, for a single
                                # click, we end up with somewhat like this, and here I made sure to click really fast
                        
                                # Non double click at 96.8515625, 58.37109375
                                # Non double click at 96.8515625, 58.37109375
                                # Non double click at 96.8515625, 58.37109375
                                # Non double click at 96.8515625, 58.37109375
                                # Mouse button not pressed anymore at 96.8515625, 58.37109375
                        
                                # And this is a short drag event.
                        
                                # Non double click at 84.875, 56.5859375
                                # Non double click at 84.875, 56.5859375
                                # Non double click at 84.875, 56.5859375
                                # Non double click at 84.875, 56.5859375
                                # Non double click at 84.59765625, 56.5859375
                                # Non double click at 83.49609375, 56.94921875
                                # Non double click at 83.49609375, 56.94921875
                                # Non double click at 82.39453125, 57.3125
                                # Non double click at 82.39453125, 57.3125
                                # Non double click at 80.74609375, 58.1328125
                                # Non double click at 80.74609375, 58.1328125
                                # Non double click at 77.7265625, 58.6328125
                                # ...
                                # Non double click at -8.35546875, 80.16796875
                                # Non double click at -8.35546875, 80.16796875
                                # Non double click at -8.35546875, 80.16796875
                                # Mouse button not pressed anymore at -8.35546875, 80.16796875
                        
                                # So they are very similar, and we cannot go by the pure logic "when the coordinates
                                # do not change, we are in a drag event" because this is not an event stream, i.e., we
                                # might poll the same input state multiple times, depending on how fast our #while loop
                                # runs.
                        
                                # But what we could do, is postpone all actions until we see a change. In extreme cases,
                                # where the user is swiping very fast with the mouse and then clicks on a tile, this might
                                # fail.
                                mx -= dx
                                my -= dy
                        
                                currentPos: tuple[float, float] = (mx, my)
                                if lastPos is None and currentPos != lastPos:
                                    lastPos = currentPos
                        
                                # The mouse is not being pressed anymore.
                                if not state[c4d.BFM_INPUT_VALUE]:
                                    if currentPos != lastPos:
                                        print("Drag event")
                                    else:
                                        print("Click event")
                                    break
                        
                            return True
                        
                        
                        Click event
                        Drag event
                        Click event
                        Click event
                        Click event
                        Drag event
                        

                        MAXON SDK Specialist
                        developers.maxon.net

                        1 Reply Last reply Reply Quote 0
                        • First post
                          Last post