Drag & Drop to Reorder in GUI



  • @m_magalhaes
    Thank you for the reply and confirmation that it's possible. I'm happy to do the work with a little help from the Maxon SDK specialists.

    In the example below, the drag behavior seems right, but the print to the console on line 67 doesn't show until I save the file on my machine or hit enter in the console (and sometimes not at all) for some reason. I want to make sure it's not a memory leak. Could you please let me know if this is a recommended way to set up a drag in a GeUserArea? (I got the basis from this post). If so, how would I print the input events to the console?

    import c4d
    from c4d import gui
    from c4d.gui import GeUserArea, GeDialog
    
    class Area(c4d.gui.GeUserArea):
        def __init__(self):
            super(Area, self).__init__()
            self.rectangle=[-1,-1,-1,-1]
    
        def DrawMsg(self, x1, y1, x2, y2, msg):
            self.OffScreenOn()
            self.DrawSetPen(c4d.Vector(.2))
            self.DrawRectangle(x1, y1, x2, y2)
            xdr,ydr,x2dr,y2dr = self.toolDragSortEx()
            self.DrawSetPen(c4d.Vector(1))
            self.DrawBorder(c4d.BORDER_ACTIVE_4, xdr,ydr,x2dr,y2dr)
    
        def toolDragSortEx(self):
            if self.rectangle[0]<self.rectangle[2]:
                x1,x2 = self.rectangle[0],self.rectangle[2]
            else:
                x1,x2 = self.rectangle[2],self.rectangle[0]
            if self.rectangle[1]<self.rectangle[3]:
                y1,y2 = self.rectangle[1],self.rectangle[3]
            else:
                y1,y2 = self.rectangle[3],self.rectangle[1]
            return x1,y1,x2,y2
    
        def InputEvent(self, msg):
            dev = msg.GetLong(c4d.BFM_INPUT_DEVICE)
            if dev == c4d.BFM_INPUT_MOUSE:
                mousex = msg.GetLong(c4d.BFM_INPUT_X)
                mousey = msg.GetLong(c4d.BFM_INPUT_Y)
                start_x = mx = mousex - self.Local2Global()['x']
                start_y = my = mousey - self.Local2Global()['y']
                channel = msg.GetLong(c4d.BFM_INPUT_CHANNEL)
    
                if channel == c4d.BFM_INPUT_MOUSELEFT:
                    print("mouse down")
    
                #drag interaction
                state = c4d.BaseContainer()
                self.MouseDragStart(c4d.KEY_MLEFT,start_x, start_y, c4d.MOUSEDRAGFLAGS_DONTHIDEMOUSE| c4d.MOUSEDRAGFLAGS_NOMOVE )
                while True:
                    result, dx, dy, channels = self.MouseDrag()
    
                    #end of Drag
                    if result == c4d.MOUSEDRAGRESULT_ESCAPE:
                        break
                    if not self.GetInputState(c4d.BFM_INPUT_MOUSE, c4d.BFM_INPUT_MOUSELEFT, state):
                        print "other clicks"
                        break
                    if state[c4d.BFM_INPUT_VALUE] == 0:
                        #mouse release
                        self.rectangle = [-1,-1,-1,-1]
                        self.Redraw()
                        break
    
                    #not moving, continue
                    if dx == 0 and dy == 0:
                        continue
    
                    #dragging
                    mx -= dx
                    my -= dy
    
                    print(mx,my)
                    self.rectangle = [start_x,start_y,mx,my]
                    self.Redraw()
    
            return True
    
        def Message(self, msg, result):
            return c4d.gui.GeUserArea.Message(self, msg, result)
    
    class MyDialog(c4d.gui.GeDialog):
        def __init__(self):
            pass
    
        def CreateLayout(self):
            self.SetTitle("Drag Test")
            self.area = Area()
            self.AddUserArea(1000, c4d.BFH_SCALEFIT|c4d.BFV_SCALEFIT)
            self.AttachUserArea(self.area, 1000)
            self.GroupEnd()
            return True
    
    def main():
        dialog = None
        dialog = MyDialog()
        dialog.Open(c4d.DLG_TYPE_MODAL_RESIZEABLE, xpos=-2, ypos=-2, defaultw=960, defaulth=540)
    
    if __name__=='__main__':
        main()
    


  • Sorry for the delay here the full example.

    """
    Copyright: MAXON Computer GmbH
    Author: Maxime Adam
    
    Description:
        - Creates and attaches a GeUserArea to a Dialog.
        - Creates a series of aligned squares, that can be dragged and swapped together.
    
    Note:
        - This example uses weakref, I encourage you to read https://pymotw.com/2/weakref/.
    
    Class/method highlighted:
        - c4d.gui.GeUserArea
        - GeUserArea.DrawMsg
        - GeUserArea.InputEvent
        - GeUserArea.MouseDragStart
        - GeUserArea.MouseDrag
        - GeUserArea.MouseDragEnd
        - c4d.gui.GeDialog
        - GeDialog.CreateLayout
        - GeDialog.AddUserArea
        - GeDialog.AttachUserArea
    
    
    Compatible:
        - Win / Mac
        - R13, R14, R15, R16, R17, R18, R19, R20
    """
    import c4d
    import weakref
    
    # Global variable to determine the Size of our Square.
    SIZE = 100
    
    
    class Square(object):
        """
        Abstract class to represent a Square in a GeUserArea.
        """
    
        def __init__(self, geUserArea, index):
            self.index = index  # The initial square index (only used to differentiate square between each others)
            self.w = SIZE       # The width of the square
            self.h = SIZE       # The height of the square
            self.col = c4d.Vector(0.5)  # The default color of the square
            self.parentGeUserArea = weakref.ref(geUserArea)  # A weak reference to the host GeUserArea
    
        def GetParentedIndex(self):
            """
            Returns the current index in the parent list.
    
            :return: The index or c4d.NOTOK if there is no parent.
            :rtype: int
            """
            parent = self.GetParent()
            if parent is None:
                return c4d.NOTOK
    
            return parent._squareList.index(self)
    
        def GetParent(self):
            """
            Retrieves the parent instance, stored in the weakreaf self.parentGeUserArea.
    
            :return: The parent instance of the Square.
            :rtype: c4d.gui.GeUserArea
            """
            if self.parentGeUserArea:
                geUserArea = self.parentGeUserArea()
                if geUserArea is None:
                    raise RuntimeError("GeUserArea parent is not valid.")
                return geUserArea
    
            return None
    
        def DrawNormal(self, x, y):
            """
            Called by the parent GeUserArea to draw the Square normally.
    
            :param x: X position to draw.
            :param y: Y position to draw.
            """
            geUserArea = self.GetParent()
            geUserArea.DrawSetPen(self.col)
            geUserArea.DrawRectangle(x, y,
                                     x + self.w, y + self.h)
    
            geUserArea.DrawText(str(self.index), x, y)
    
        def DrawDraggedInitial(self, x, y):
            """
            Called by the parent GeUserArea when the Square is dragged
            with the initial position (same coordinate than DrawNormal).
    
            :param x: X position to draw.
            :param y: Y position to draw.
            """
            geUserArea = self.GetParent()
            geUserArea.DrawBorder(c4d.BORDER_ACTIVE_1,
                                  x, y,
                                  x + self.w, y + self.h)
    
        def DrawDragged(self, x, y):
            """
            Called by the parent GeUserArea when the Square is dragged
            with the current mouse position.
    
            :param x: X position to draw.
            :param y: Y position to draw.
            """
            geUserArea = self.GetParent()
            geUserArea.DrawSetPen(c4d.Vector(1))
            geUserArea.DrawRectangle(int(x), int(y),
                                     int(x + self.w), int(y + self.h))
    
            geUserArea.DrawText(str(self.index), int(x + (SIZE / 2.0)), int(y + (SIZE / 2.0)))
    
    
    class DraggingArea(c4d.gui.GeUserArea):
        """
        Custom implementation of a GeUserArea that creates 4 squares and lets you drag them.
        """
    
        def __init__(self):
            self._squareList = []   # Stores a list of Square that will be draw in the GeUserArea.
    
            self.draggedObj = None  # None if not dragging, Square if dragged
            self.clickedPos = None  # None if not dragging, tuple(X, Y) if dragged
    
            # Creates 4 squares
            self.CreateSquare()
            self.CreateSquare()
            self.CreateSquare()
            self.CreateSquare()
    
        # ===============================
        #  Square management
        # ===============================
        def CreateSquare(self):
            """
            Creates a square that will be draw later.
    
            :return: The created square
            :rtype: Square
            """
            square = Square(self, len(self._squareList))
            self._squareList.append(square)
            return square
    
        def GetXYFromId(self, index):
            """
            Retrieves the X, Y op, left position according to an index in order.
            This produces an array of Square correctly aligned.
    
            :param index: The index to retrieve X, Y from.
            :type index: int
            :return: tuple(x left position, y top position).
            :return: tuple(int, int)
            """
            x = SIZE * index
            xPadding = 5 * index
            x += xPadding
            y = 5
    
            return x, y
    
        def GetIdFromXY(self, xIn, yIn):
            """
            Retrieves the square id stored in self._squareList according to its normal (not dragged) position.
    
            :param xIn: The position in x.
            :type xIn: int
            :param yIn: The position in y.
            :type yIn: int
            :return: The id or c4d.NOTOK (-1) if not found.
            :rtype: int
            """
    
            # We could optimize the method by reversing the algorithm  from GetXYFromID,
            # But for now we just iterate all squares and see which one is correct.
            for squareId, square in enumerate(self._squareList):
                x, y = self.GetXYFromId(squareId)
    
                if x < xIn < x + SIZE and y < yIn < y + SIZE:
                    return squareId
    
            return c4d.NOTOK
    
        # ===============================
        #  Drawing management
        # ===============================
    
        def DrawSquares(self):
            """
            Called in DrawMsg.
            Draws all squares contained in self._squareList
            """
            for squareId, square in enumerate(self._squareList):
                x, y = self.GetXYFromId(squareId)
                if square is not self.draggedObj:
                    square.DrawNormal(x, y)
    
                else:
                    square.DrawDraggedInitial(x, y)
    
        def DrawDraggedSquare(self):
            """
            Called in DrawMsg.
            Draws the dragged squares
            """
            if self.draggedObj is None or self.clickedPos is None:
                return
    
            x, y = self.clickedPos
    
            self.draggedObj.DrawDragged(x, y)
    
        def DrawMsg(self, x1, y1, x2, y2, msg):
            """
            This method is called automatically when Cinema 4D Draw the Gadget.
    
            :param x1: The upper left x coordinate.
            :type x1: int
            :param y1: The upper left y coordinate.
            :type y1: int
            :param x2: The lower right x coordinate.
            :type x2: int
            :param y2: The lower right y coordinate.
            :type y2: int
            :param msg_ref: The original mesage container.
            :type msg_ref: c4d.BaseContainer
            """
    
            # Initializes draw region
            self.OffScreenOn()
            self.SetClippingRegion(x1, y1, x2, y2)
    
            # Get default Background color
            defaultColorRgbDict = self.GetColorRGB(c4d.COLOR_BG)
            defaultColorRgb = c4d.Vector(defaultColorRgbDict["r"], defaultColorRgbDict["g"], defaultColorRgbDict["b"])
            defaultColor = defaultColorRgb / 255.0
    
            self.DrawSetPen(defaultColor)
            self.DrawRectangle(x1, y1, x2, y2)
    
            # First draw pass, we draw all not dragged object
            self.DrawSquares()
    
            # Last draw pass, we draw the dragged object, this way dragged square is drawn on top of everything.
            self.DrawDraggedSquare()
    
        # ===============================
        #  Dragging management
        # ===============================
        @property
        def isCurrentlyDragged(self):
            """
            Checks if a dragging operation currently occurs.
    
            :return: True if a dragging operation currently occurs otherwise False.
            :rtype: bool
            """
            return self.clickedPos is not None and self.draggedObj is not None
    
        def GetDraggedSquareWithPosition(self):
            """
            Retrieves the clicked square during a drag event from the click position.
    
            :return: The square or None if there is nothing dragged.
            :rtype: Union[Square, None]
            """
            if self.clickedPos is None:
                return None
    
            x, y = self.clickedPos
            squareId = self.GetIdFromXY(x, y)
            if squareId == c4d.NOTOK:
                return None
    
            return self._squareList[squareId]
    
        def InputEvent(self, msg):
            """
            Called by Cinema 4D, when there is a user interaction (click) on the GeUserArea.
            This is the place to catch and handle drag interaction.
    
            :param msg: The event container.
            :type msg: c4d.BaseContainer
            :return: True if the event was handled, otherwise False.
            :rtype: bool
            """
            # Do nothing if its not a left mouse click event
            if msg[c4d.BFM_INPUT_DEVICE] != c4d.BFM_INPUT_MOUSE and msg[c4d.BFM_INPUT_CHANNEL] != c4d.BFM_INPUT_MOUSELEFT:
                return True
            
            # Retrieves the initial position of the click
            mouseX = msg[c4d.BFM_INPUT_X]
            mouseY = msg[c4d.BFM_INPUT_Y]
    
            # Initializes the start of the dragging process (needs to be initialized with the original mouseX, mouseY).
            self.MouseDragStart(c4d.KEY_MLEFT, mouseX, mouseY, c4d.MOUSEDRAGFLAGS_DONTHIDEMOUSE | c4d.MOUSEDRAGFLAGS_NOMOVE)
            isFirstTick = True
    
            # MouseDrag needs to be called all time to update information about the current drag process.
            # This allow to catch when the mouse is released and leave the infinite loop.
            while True:
    
                # Updates the current mouse information
                result, deltaX, deltaY, channels = self.MouseDrag()
                if result != c4d.MOUSEDRAGRESULT_CONTINUE:
                    break
    
                # The first tick is ignored as deltaX/Y include the mouse clicking behavior with a deltaX/Y always equal to 4.0.
                # However it can be useful to do some initialization or even trigger single click event
                if isFirstTick:
                    isFirstTick = False
                    continue
    
                # If the mouse didn't move, don't need to do anything
                if deltaX == 0.0 and deltaY == 0.0:
                    continue
    
                # Updates mouse position with the updated delta
                mouseX -= deltaX
                mouseY -= deltaY
                self.clickedPos = mouseX, mouseY
    
                # Retrieves the clicked square
                square = self.GetDraggedSquareWithPosition()
    
                # Defines the draggedObj only if the user clicked on a square and is not yet already defined
                if square is not None and self.draggedObj is None:
                    self.draggedObj = square
    
                # Redraw the GeUserArea (it will call DrawMsg)
                self.Redraw()
    
            # Asks why we leave the while loop
            endState = self.MouseDragEnd()
    
            # If the drag process was ended because the user releases the mouse.
            # Note that while we are not anymore really in the Drag Pooling, from our implementation we consider we are still
            # and don't clear directly the data, so self.clickedPos and self.draggedObj still refer to the last tick of the
            # MouseDrag pool and we will clear it once we don't need anymore those data (after this if statement).
            if endState == c4d.MOUSEDRAGRESULT_FINISHED:
    
                # Checks a dragged object is set
                # in case of a simple click without mouse movement nothing has to be done.
                if self.isCurrentlyDragged:
                    # Retrieves the initial index of the dragged object.
                    currentIndex = self.draggedObj.GetParentedIndex()
    
                    # Retrieves the index where the drag operation ended. If we find an ID, swap both items.
                    releasedSquare = self.GetDraggedSquareWithPosition()
                    if releasedSquare is not None:
                        targetIndex = releasedSquare.GetParentedIndex()
    
                        # Swap items only if source index and target index are different
                        if targetIndex != currentIndex:
                            self._squareList[currentIndex], self._squareList[targetIndex] = self._squareList[targetIndex], \
                                                                                            self._squareList[currentIndex]
    
                    # In case the user release the mouse not on another square.
                    # Swaps the current square to either the first position or last position.
                    else:
                        # if current Index is already the first one, make no sense to inserts it
                        if currentIndex != 0:
                            # If the X position is before the X position of the first square
                            # Removes and inserts it back to the first position.
                            if self.clickedPos[0] < self.GetXYFromId(0)[0]:
                                self._squareList.remove(self.draggedObj)
                                self._squareList.insert(0, self.draggedObj)
    
                        # Retrieves the last index
                        lastIndex = len(self._squareList) - 1
                        # if current Index is already the last one, make no sense to insert it
                        if currentIndex != lastIndex:
                            if self.clickedPos[0] > self.GetXYFromId(lastIndex)[0] + SIZE:
                                # If the X position is after the X position of the last square (and its size)
                                # Removes and inserts it back to the last position.
                                self._squareList.remove(self.draggedObj)
                                self._squareList.insert(lastIndex, self.draggedObj)
    
            # Cleanup and refresh information if we dragged something
            if self.clickedPos is not None or self.draggedObj is not None:
                self.clickedPos = None
                self.draggedObj = None
                self.Redraw()
    
            return True
    
    
    class MyDialog(c4d.gui.GeDialog):
        """
        Creates a Dialog with only a GeUserArea within.
        """
    
        def __init__(self):
            # It's important to stores our Python implementation instance of the GeUserArea in class variable,
            # This way we are sure the GeUserArea instance live as long as the GeDialog.
            self.area = DraggingArea()
    
        def CreateLayout(self):
            """
            This method is called automatically when Cinema 4D Create the Layout (display) of the Dialog.
            """
            self.AddUserArea(1000, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT)
            self.AttachUserArea(self.area, 1000)
            return True
    
    
    def main():
        # Creates a new dialog
        dialog = MyDialog()
    
        # Opens it
        dialog.Open(dlgtype=c4d.DLG_TYPE_MODAL_RESIZEABLE, defaultw=500, defaulth=500)
    
    
    if __name__ == '__main__':
        main()
    

    It will be included soon on Github.
    Cheers,
    Maxime



  • Hello. Just checking in again: can anyone please help me with some advice on dragging in the GeUserArea? Have I set it up properly and why is there no realtime feedback in the While loop?

    Thank you!



  • Hi @blastframe,

    In general, it's not necessary to bump a topic however since the topic is already 5 days ago without an answer from us, in this case, no issue at all, and sorry for the time needed, understand that we also have other duties to do but don't worry you are not forgotten.

    We are working on a more general example that draws a bunch of cubes and let you drag them. It's under review process, once the review will be done it will be on GitHub (and we will add it to this thread), here a preview of it.
    dragExample.gif

    Anyway since it's already 5days longs, dragging is a bit particular and our documentation/example will be adapted since explanations provided for the moment are not very clear.

    So here are the usual steps to do drag operation in a GeUserArea (Very Similar to what you will in a ToolData::MouseInput)

        def InputEvent(self, msg):
            """
            Called by Cinema 4D, when there is an user interaction (click) on the GeUserArea.
            This is the place to catch and handle drag interaction.
    
            :param msg: The event container.
            :type msg: c4d.BaseContainer
            :return: True if the event was handled, otherwise False.
            :rtype: bool
            """
            # Retrieves the initial position of the click
            mouseX = msg[c4d.BFM_INPUT_X]
            mouseY = msg[c4d.BFM_INPUT_Y]
    
            # Initializes the start of the dragging process (needs to be initialized with the original mouseX, mouseY).
            self.MouseDragStart(c4d.KEY_MLEFT, mouseX, mouseY, c4d.MOUSEDRAGFLAGS_DONTHIDEMOUSE | c4d.MOUSEDRAGFLAGS_NOMOVE)
            isFirstTick = True
    
            # MouseDrag needs to be called all times to update information about the current drag process.
            # This allows catching when the mouse is released and leaves the infinite loop.
            while True:
    
                # Updates the current mouse information
                result, deltaX, deltaY, channels = self.MouseDrag()
                if result != c4d.MOUSEDRAGRESULT_CONTINUE:
                    break
    
                # The first tick is ignored as deltaX/YY includes the mouse clicking behavior with a deltaX/Y always equal to 4.0.
                # However it can be useful to do some initialization or even trigger single click event
                if isFirstTick:
                    isFirstTick = False
                    continue
    
                # If the mouse didn't move, don't need to do anything, since we passed c4d.MOUSEDRAGFLAGS_NOMOVE it's unlikely to happen
                if deltaX == 0.0 and deltaY == 0.0:
                    continue
    
                # Updates mouse position with the updated delta
                mouseX -= deltaX
                mouseY -= deltaY
                self.clickedPos = mouseX, mouseY
    
                # Do some operation
    
                # Redraw the GeUserArea during the drag process (it will call DrawMsg)
                self.Redraw()
    
            # Asks why we leave the while loop
            endState = self.MouseDragEnd()
    
            # If the drag process was ended because the user releases the mouse.
            if endState == c4d.MOUSEDRAGRESULT_FINISHED:
                print "Drag finished normally"
                # Probably change some internal data and then redraw
    
            # If the drag process was canceled by the user
            elif endState == c4d.MOUSEDRAGRESULT_ESCAPE:
                print "Drag was escaped, do Nothing, reset initial state"
                    # Probably change some internal data and then redraw to restore the initial content
    
            return True
    

    Cheers,
    Maxime.



  • @m_adam

    Hi Maxime,
    Thank you for the response and guidance on dragging in the C4D GeUserArea. I had certainly imagined that your team has many other duties (including the fantastic support you provide on this forum). It seemed from the previous response that it was the last word as I didn't have any indication your team was still working on the issue. I am very happy to see that you will be shining light on the GeUserArea dragging process! The example you shared looks very promising as well. I very much look forward to seeing the code: thank you! 😄



  • Interesting example, @m_adam!

    If I copy/paste the code into the script manager the gui is empty. Which is funny, because yesterday or so I tried it and it worked...

    Has the code been updated? Anyone else experiencing this..?
    (checked in multiple versions...)

    Cheers,
    Lasse



  • @lasselauch said in Drag & Drop to Reorder in GUI:

    Interesting example, @m_adam!

    If I copy/paste the code into the script manager the gui is empty. Which is funny, because yesterday or so I tried it and it worked...

    Has the code been updated? Anyone else experiencing this..?
    (checked in multiple versions...)

    Cheers,
    Lasse

    Maybe because the code I provided is only for Mouse Input, so nothing is drawn and its purpose its only to demonstrate how to handle drag in a GeUserArea. So if you simply copy/paste my script yes it will not work, at least we didn't change anything.

    Cheers,
    Maxime.



  • @m_adam Ah... I thought that was a working example as shown in your gif and I've already had it running at one point... Sorry! My bad.



  • Sorry for the delay here the full example.

    """
    Copyright: MAXON Computer GmbH
    Author: Maxime Adam
    
    Description:
        - Creates and attaches a GeUserArea to a Dialog.
        - Creates a series of aligned squares, that can be dragged and swapped together.
    
    Note:
        - This example uses weakref, I encourage you to read https://pymotw.com/2/weakref/.
    
    Class/method highlighted:
        - c4d.gui.GeUserArea
        - GeUserArea.DrawMsg
        - GeUserArea.InputEvent
        - GeUserArea.MouseDragStart
        - GeUserArea.MouseDrag
        - GeUserArea.MouseDragEnd
        - c4d.gui.GeDialog
        - GeDialog.CreateLayout
        - GeDialog.AddUserArea
        - GeDialog.AttachUserArea
    
    
    Compatible:
        - Win / Mac
        - R13, R14, R15, R16, R17, R18, R19, R20
    """
    import c4d
    import weakref
    
    # Global variable to determine the Size of our Square.
    SIZE = 100
    
    
    class Square(object):
        """
        Abstract class to represent a Square in a GeUserArea.
        """
    
        def __init__(self, geUserArea, index):
            self.index = index  # The initial square index (only used to differentiate square between each others)
            self.w = SIZE       # The width of the square
            self.h = SIZE       # The height of the square
            self.col = c4d.Vector(0.5)  # The default color of the square
            self.parentGeUserArea = weakref.ref(geUserArea)  # A weak reference to the host GeUserArea
    
        def GetParentedIndex(self):
            """
            Returns the current index in the parent list.
    
            :return: The index or c4d.NOTOK if there is no parent.
            :rtype: int
            """
            parent = self.GetParent()
            if parent is None:
                return c4d.NOTOK
    
            return parent._squareList.index(self)
    
        def GetParent(self):
            """
            Retrieves the parent instance, stored in the weakreaf self.parentGeUserArea.
    
            :return: The parent instance of the Square.
            :rtype: c4d.gui.GeUserArea
            """
            if self.parentGeUserArea:
                geUserArea = self.parentGeUserArea()
                if geUserArea is None:
                    raise RuntimeError("GeUserArea parent is not valid.")
                return geUserArea
    
            return None
    
        def DrawNormal(self, x, y):
            """
            Called by the parent GeUserArea to draw the Square normally.
    
            :param x: X position to draw.
            :param y: Y position to draw.
            """
            geUserArea = self.GetParent()
            geUserArea.DrawSetPen(self.col)
            geUserArea.DrawRectangle(x, y,
                                     x + self.w, y + self.h)
    
            geUserArea.DrawText(str(self.index), x, y)
    
        def DrawDraggedInitial(self, x, y):
            """
            Called by the parent GeUserArea when the Square is dragged
            with the initial position (same coordinate than DrawNormal).
    
            :param x: X position to draw.
            :param y: Y position to draw.
            """
            geUserArea = self.GetParent()
            geUserArea.DrawBorder(c4d.BORDER_ACTIVE_1,
                                  x, y,
                                  x + self.w, y + self.h)
    
        def DrawDragged(self, x, y):
            """
            Called by the parent GeUserArea when the Square is dragged
            with the current mouse position.
    
            :param x: X position to draw.
            :param y: Y position to draw.
            """
            geUserArea = self.GetParent()
            geUserArea.DrawSetPen(c4d.Vector(1))
            geUserArea.DrawRectangle(int(x), int(y),
                                     int(x + self.w), int(y + self.h))
    
            geUserArea.DrawText(str(self.index), int(x + (SIZE / 2.0)), int(y + (SIZE / 2.0)))
    
    
    class DraggingArea(c4d.gui.GeUserArea):
        """
        Custom implementation of a GeUserArea that creates 4 squares and lets you drag them.
        """
    
        def __init__(self):
            self._squareList = []   # Stores a list of Square that will be draw in the GeUserArea.
    
            self.draggedObj = None  # None if not dragging, Square if dragged
            self.clickedPos = None  # None if not dragging, tuple(X, Y) if dragged
    
            # Creates 4 squares
            self.CreateSquare()
            self.CreateSquare()
            self.CreateSquare()
            self.CreateSquare()
    
        # ===============================
        #  Square management
        # ===============================
        def CreateSquare(self):
            """
            Creates a square that will be draw later.
    
            :return: The created square
            :rtype: Square
            """
            square = Square(self, len(self._squareList))
            self._squareList.append(square)
            return square
    
        def GetXYFromId(self, index):
            """
            Retrieves the X, Y op, left position according to an index in order.
            This produces an array of Square correctly aligned.
    
            :param index: The index to retrieve X, Y from.
            :type index: int
            :return: tuple(x left position, y top position).
            :return: tuple(int, int)
            """
            x = SIZE * index
            xPadding = 5 * index
            x += xPadding
            y = 5
    
            return x, y
    
        def GetIdFromXY(self, xIn, yIn):
            """
            Retrieves the square id stored in self._squareList according to its normal (not dragged) position.
    
            :param xIn: The position in x.
            :type xIn: int
            :param yIn: The position in y.
            :type yIn: int
            :return: The id or c4d.NOTOK (-1) if not found.
            :rtype: int
            """
    
            # We could optimize the method by reversing the algorithm  from GetXYFromID,
            # But for now we just iterate all squares and see which one is correct.
            for squareId, square in enumerate(self._squareList):
                x, y = self.GetXYFromId(squareId)
    
                if x < xIn < x + SIZE and y < yIn < y + SIZE:
                    return squareId
    
            return c4d.NOTOK
    
        # ===============================
        #  Drawing management
        # ===============================
    
        def DrawSquares(self):
            """
            Called in DrawMsg.
            Draws all squares contained in self._squareList
            """
            for squareId, square in enumerate(self._squareList):
                x, y = self.GetXYFromId(squareId)
                if square is not self.draggedObj:
                    square.DrawNormal(x, y)
    
                else:
                    square.DrawDraggedInitial(x, y)
    
        def DrawDraggedSquare(self):
            """
            Called in DrawMsg.
            Draws the dragged squares
            """
            if self.draggedObj is None or self.clickedPos is None:
                return
    
            x, y = self.clickedPos
    
            self.draggedObj.DrawDragged(x, y)
    
        def DrawMsg(self, x1, y1, x2, y2, msg):
            """
            This method is called automatically when Cinema 4D Draw the Gadget.
    
            :param x1: The upper left x coordinate.
            :type x1: int
            :param y1: The upper left y coordinate.
            :type y1: int
            :param x2: The lower right x coordinate.
            :type x2: int
            :param y2: The lower right y coordinate.
            :type y2: int
            :param msg_ref: The original mesage container.
            :type msg_ref: c4d.BaseContainer
            """
    
            # Initializes draw region
            self.OffScreenOn()
            self.SetClippingRegion(x1, y1, x2, y2)
    
            # Get default Background color
            defaultColorRgbDict = self.GetColorRGB(c4d.COLOR_BG)
            defaultColorRgb = c4d.Vector(defaultColorRgbDict["r"], defaultColorRgbDict["g"], defaultColorRgbDict["b"])
            defaultColor = defaultColorRgb / 255.0
    
            self.DrawSetPen(defaultColor)
            self.DrawRectangle(x1, y1, x2, y2)
    
            # First draw pass, we draw all not dragged object
            self.DrawSquares()
    
            # Last draw pass, we draw the dragged object, this way dragged square is drawn on top of everything.
            self.DrawDraggedSquare()
    
        # ===============================
        #  Dragging management
        # ===============================
        @property
        def isCurrentlyDragged(self):
            """
            Checks if a dragging operation currently occurs.
    
            :return: True if a dragging operation currently occurs otherwise False.
            :rtype: bool
            """
            return self.clickedPos is not None and self.draggedObj is not None
    
        def GetDraggedSquareWithPosition(self):
            """
            Retrieves the clicked square during a drag event from the click position.
    
            :return: The square or None if there is nothing dragged.
            :rtype: Union[Square, None]
            """
            if self.clickedPos is None:
                return None
    
            x, y = self.clickedPos
            squareId = self.GetIdFromXY(x, y)
            if squareId == c4d.NOTOK:
                return None
    
            return self._squareList[squareId]
    
        def InputEvent(self, msg):
            """
            Called by Cinema 4D, when there is a user interaction (click) on the GeUserArea.
            This is the place to catch and handle drag interaction.
    
            :param msg: The event container.
            :type msg: c4d.BaseContainer
            :return: True if the event was handled, otherwise False.
            :rtype: bool
            """
            # Do nothing if its not a left mouse click event
            if msg[c4d.BFM_INPUT_DEVICE] != c4d.BFM_INPUT_MOUSE and msg[c4d.BFM_INPUT_CHANNEL] != c4d.BFM_INPUT_MOUSELEFT:
                return True
            
            # Retrieves the initial position of the click
            mouseX = msg[c4d.BFM_INPUT_X]
            mouseY = msg[c4d.BFM_INPUT_Y]
    
            # Initializes the start of the dragging process (needs to be initialized with the original mouseX, mouseY).
            self.MouseDragStart(c4d.KEY_MLEFT, mouseX, mouseY, c4d.MOUSEDRAGFLAGS_DONTHIDEMOUSE | c4d.MOUSEDRAGFLAGS_NOMOVE)
            isFirstTick = True
    
            # MouseDrag needs to be called all time to update information about the current drag process.
            # This allow to catch when the mouse is released and leave the infinite loop.
            while True:
    
                # Updates the current mouse information
                result, deltaX, deltaY, channels = self.MouseDrag()
                if result != c4d.MOUSEDRAGRESULT_CONTINUE:
                    break
    
                # The first tick is ignored as deltaX/Y include the mouse clicking behavior with a deltaX/Y always equal to 4.0.
                # However it can be useful to do some initialization or even trigger single click event
                if isFirstTick:
                    isFirstTick = False
                    continue
    
                # If the mouse didn't move, don't need to do anything
                if deltaX == 0.0 and deltaY == 0.0:
                    continue
    
                # Updates mouse position with the updated delta
                mouseX -= deltaX
                mouseY -= deltaY
                self.clickedPos = mouseX, mouseY
    
                # Retrieves the clicked square
                square = self.GetDraggedSquareWithPosition()
    
                # Defines the draggedObj only if the user clicked on a square and is not yet already defined
                if square is not None and self.draggedObj is None:
                    self.draggedObj = square
    
                # Redraw the GeUserArea (it will call DrawMsg)
                self.Redraw()
    
            # Asks why we leave the while loop
            endState = self.MouseDragEnd()
    
            # If the drag process was ended because the user releases the mouse.
            # Note that while we are not anymore really in the Drag Pooling, from our implementation we consider we are still
            # and don't clear directly the data, so self.clickedPos and self.draggedObj still refer to the last tick of the
            # MouseDrag pool and we will clear it once we don't need anymore those data (after this if statement).
            if endState == c4d.MOUSEDRAGRESULT_FINISHED:
    
                # Checks a dragged object is set
                # in case of a simple click without mouse movement nothing has to be done.
                if self.isCurrentlyDragged:
                    # Retrieves the initial index of the dragged object.
                    currentIndex = self.draggedObj.GetParentedIndex()
    
                    # Retrieves the index where the drag operation ended. If we find an ID, swap both items.
                    releasedSquare = self.GetDraggedSquareWithPosition()
                    if releasedSquare is not None:
                        targetIndex = releasedSquare.GetParentedIndex()
    
                        # Swap items only if source index and target index are different
                        if targetIndex != currentIndex:
                            self._squareList[currentIndex], self._squareList[targetIndex] = self._squareList[targetIndex], \
                                                                                            self._squareList[currentIndex]
    
                    # In case the user release the mouse not on another square.
                    # Swaps the current square to either the first position or last position.
                    else:
                        # if current Index is already the first one, make no sense to inserts it
                        if currentIndex != 0:
                            # If the X position is before the X position of the first square
                            # Removes and inserts it back to the first position.
                            if self.clickedPos[0] < self.GetXYFromId(0)[0]:
                                self._squareList.remove(self.draggedObj)
                                self._squareList.insert(0, self.draggedObj)
    
                        # Retrieves the last index
                        lastIndex = len(self._squareList) - 1
                        # if current Index is already the last one, make no sense to insert it
                        if currentIndex != lastIndex:
                            if self.clickedPos[0] > self.GetXYFromId(lastIndex)[0] + SIZE:
                                # If the X position is after the X position of the last square (and its size)
                                # Removes and inserts it back to the last position.
                                self._squareList.remove(self.draggedObj)
                                self._squareList.insert(lastIndex, self.draggedObj)
    
            # Cleanup and refresh information if we dragged something
            if self.clickedPos is not None or self.draggedObj is not None:
                self.clickedPos = None
                self.draggedObj = None
                self.Redraw()
    
            return True
    
    
    class MyDialog(c4d.gui.GeDialog):
        """
        Creates a Dialog with only a GeUserArea within.
        """
    
        def __init__(self):
            # It's important to stores our Python implementation instance of the GeUserArea in class variable,
            # This way we are sure the GeUserArea instance live as long as the GeDialog.
            self.area = DraggingArea()
    
        def CreateLayout(self):
            """
            This method is called automatically when Cinema 4D Create the Layout (display) of the Dialog.
            """
            self.AddUserArea(1000, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT)
            self.AttachUserArea(self.area, 1000)
            return True
    
    
    def main():
        # Creates a new dialog
        dialog = MyDialog()
    
        # Opens it
        dialog.Open(dlgtype=c4d.DLG_TYPE_MODAL_RESIZEABLE, defaultw=500, defaulth=500)
    
    
    if __name__ == '__main__':
        main()
    

    It will be included soon on Github.
    Cheers,
    Maxime



  • This post is deleted!


  • @m_adam This is nice work, Maxime, thank you! I have to check out weakref 🔍

    I'm getting a couple of errors when clicking in a part of the GeUserArea without a square or hitting a key on the keyboard. How can I handle these?

    No square:

    line 331, in InputEvent
            currentIndex = self.draggedObj.GetParentedIndex()   
        AttributeError: 'NoneType' object has no attribute 'GetParentedIndex'`
    

    Keyboard input:

    line 278, in InputEvent
        self.MouseDragStart(c4d.KEY_MLEFT, mouseX, mouseY, c4d.MOUSEDRAGFLAGS_DONTHIDEMOUSE | c4d.MOUSEDRAGFLAGS_NOMOVE)
    TypeError: a float is required
    


  • Thanks, I just updated the previous code with a fix to both issues.
    The first one was related because in isCurrentlyDragged I used or while it should be and.
    The second one is because InputEvent is called for any kind of event, and I didn't filter when I want to do the drag operation so I added the next code at the start of the Message method:

            # Only do something if its a left mouse click
            if msg[c4d.BFM_INPUT_DEVICE] != c4d.BFM_INPUT_MOUSE and msg[c4d.BFM_INPUT_CHANNEL] != c4d.BFM_INPUT_MOUSELEFT:
                return True
    

    Cheers,
    Maxime.



  • @m_adam Terrific work, Maxime! This is very helpful to me and I'm sure the many others who want to learn about dragging in Cinema 4D's UI. Excellent job! 🍾


Log in to reply