SOLVED Drag & Drop to Reorder in GUI

Hello,
I'm seeking advice for a drag & drop reorder effect similar to the one in this UIKit.

  1. Is something similar possible with conventional GUI groups & buttons?
  2. If not, are there Python examples of a similar behavior with GeUserArea?
  3. The only article I could find on dragging in Cinema 4D Python plugins was this article, 'Start Drag from GeUserArea'. Can anyone please help me with a proper GeUserArea drag example?

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,

we don't have any helpful functions to do that kind of drag and drop and no example to reproduce that within a GeUserArea.
It's still possible but really a lot of work.

Cheers,
Manuel

@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()

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