Drag & Drop to Reorder in GUI
-
Hello,
I'm seeking advice for a drag & drop reorder effect similar to the one in this UIKit.- Is something similar possible with conventional GUI groups & buttons?
- If not, are there Python examples of a similar behavior with GeUserArea?
- 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?
-
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.
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. -
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,
LasseMaybe 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 inisCurrentlyDragged
I usedor
while it should beand
.
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 theMessage
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!