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

    How to create a radial (pie) menu?

    Cinema 4D SDK
    windows s26 python
    3
    6
    1.7k
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • G
      Gaal Dornik
      last edited by Gaal Dornik

      Hello, i'd like to create a radial (pie) menu in Python similar to M_GLOBAL_POPUP (V key), but without submenus - just regular commands instead.

      Is that possible?

      In similar thread was a mention that this can be done using GeDialog and GeUserArea classes. Maybe somebody can show little example of it?

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

        Hi @Gaal-Dornik

        Yes, you can create just regular command with c4d.gui.ShowPopupDialog
        Just like this:

        import c4d
        
        def main() -> None:
            
            menu = c4d.BaseContainer()
            # Add some commands
            menu.InsData(5159, "CMD")
            menu.InsData(1007455, "CMD")
            menu.InsData(1018544, "CMD")
            
            # Show the popup menu
            c4d.gui.ShowPopupDialog(cd=None, bc=menu, x=500, y=500)
                
        
        if __name__ == '__main__':
            main()
        

        And this is the result:
        af30aa55-9230-4436-bec3-5bb851027a47-image.png

        For more information about c4d.gui.ShowPopupDialog, you can go to This page.

        And about your question:

        How to create a radial (pie) menu?

        I've done some research on this:

        • Just using c4d.gui.ShowPopupDialog to create the radial menu. But I don't like the style of that. I want to highly customizable of UI creation methed.
        • Use c4d.gui.GeUserArea. we can use c4d.gui.GeUserArea to draw anything we want. But unfortunately, we can't just keep the UI widgets and make the background Dialog disappear.
        • Use third-party UI Library like PySide(PyQt), we can create any style of UI interface, but the problem is that Cinema 4D doesn't support third-party libraries, so we can't normally use PySide in Cinema 4D Python, While there are people in this thread who can use it, I think PySide is too big for a gadget like Radial Menu (it's fully 300MB in size).

        That's it. If you have intrested or some ideas for that, please tell me.

        Cheers~
        Gheyret

        www.boghma.com

        1 Reply Last reply Reply Quote 1
        • G
          Gaal Dornik
          last edited by

          Thanks for your response.

          Currently i've done it using c4d.gui.ShowPopupDialog, it's not very fancy, but does the job.

          1. Do you know is there a way to show multiple ShowPopupDialog at once?
          2. Maybe i can modify the existing M_GLOBAL_POPUP in Python?
          ferdinandF 1 Reply Last reply Reply Quote 0
          • ferdinandF
            ferdinand @Gaal Dornik
            last edited by ferdinand

            Hello @Gaal-Dornik,

            Thank you for reaching out to us. And thank you @gheyret for your community answer.

            @gheyret has already lined out some of the problems that come with creating a pie menu in Cinema 4D.

            Do you know is there a way to show multiple ShowPopupDialog at once?

            No, that is not possible, because a popup menu is intrinsically modal in Cinema 4D, i.e., blocking. So when you have this code,

            a = c4d.gui.ShowPopupDialog(cd=None, bc=menuA, x=300, y=300)
            b = c4d.gui.ShowPopupDialog(cd=None, bc=menuB, x=400, y=300)
            

            the second line will only run once the user made a choice the first popup menu.

            Maybe i can modify the existing M_GLOBAL_POPUP in Python?

            You can do that with c4d.gui.SearchMenuResource, but I would strongly advise against doing that at runtime; at least when you do not specify what 'modify' means. You can sort of do it when C4DPL_BUILDMENU is emitted, but even there it is quite risky when you delete or move things.

            Just appending things to existing menus is always fine, you can have a look at this thread for details.

            How to Deal with This?

            This is not the first time the question has been asked, and while @gheyret is correct in his analysis, an external UI is not the only solution.

            1. Handle multiple dialogs in tandem. We before talked about this here. Below I provided a bit more detailed sketch for doing this. When you play around with this, you will see that my sketch is still quite error prone. You could tidy this up by writing more code, but this multiple dialogs in tandem thing is tricky to handle.
            2. The alternative is draw the menu yourself into a viewport. The menu is therefore also restricted to viewports. There were multiple plugins in the past which have done this as commercial plugins. Here one of them had been discussed. The menu itself must be then a plugin hook which provides access to view port and is also allowed to manipulate the scene. ToolData is one choice, in C++ we can also use a SceneHookdata plugin.

            Cheers,
            Ferdinand

            Result:

            Code:

            """Realizes a dialog handler which handles multiple dialogs, primarily with the feature that closing
            one dialog entails closing all of them.
            
            Must be run in the script manager.
            """
            
            import c4d
            import math
            
            __res__: c4d.plugins.GeResource = c4d.plugins.GeResource()
            __res__.InitAsGlobal()
            
            class RadialHandler:
                """Realizes the dialog handler object which controls multiple dialogs in a radial menu fashion.
                """
                def __init__(self) -> None:
                    """Inits the handler.
                    """
                    self._collection: dict[MenuContainer, list[bool]] = {}
            
                def Open(self, position: tuple[int], radius: int = 200):
                    """Open all dialogs handled by the handler.
                    """
                    t: float = (2 * math.pi) / len(self._collection)
                    v: c4d.Vector = c4d.Vector(0, radius, 0)
                    for dlg in self._collection:
                        v *= c4d.utils.MatrixRotZ(t)
                        dlg._position = c4d.Vector(int(position[0] + v.x), int(position[1] + v.y), 0)
                        dlg.Open(c4d.DLG_TYPE_ASYNC_POPUPEDIT, xpos=int(dlg._position.x), ypos=int(dlg._position.y))
                        self._collection[item][0] = True
                    
                    dlg.SetTimer(250) 
                
                def OnTimerTick(self, sender: "MenuContainer", msg: c4d.BaseContainer) -> None:
                    """Called by the last dialog in #_collection with a stride of 250ms.
            
                    We use this to jump over a "focus gap". When we have three dialogs with the focus state:
            
                        [t, f, f]
            
                    i.e., the first dialog is focused. When the user now switches the focus, because he/she
                    opens the menu of dialog 2, then the next state is not:
            
                        [f, t, f]
            
                    but 
                        [f, f, f]
                    
                    because we are here inside a message stream and for a brief moment no dialog is in focus,
                    the stream will be:
            
                        [t, f, f]
                        [f, f, f]
                        [f, t, f]
            
                    But we still have to catch the event when the user clicks somewhere outside of the menu and
                    with that de-focuses all elements. So we use this timer to check every 250ms if the state
                    is [f, f, f] and then close all dialogs.
            
                    Note that what I did here is quite error prone, as there is nothing which prevents this
                    from happening (this is exaggerated there is not for 100ms no dialog in focus when 
                    one switches focus, it more like 2-3 ms between such messages) :
            
                        0.000ms: Timer Tick
                        0.100ms: The user switches focus.
                        0.200ms: Dialog 1 is defocused, all dialogs are now out of focus.
                        0.250ms: Timer Tick, all dialogs are out of focus, the menu is closed.
                        0.300ms: Cinema 4D tries to set Dialog 2 into focus (and possibly crashes because the 
                                 dialog does not exist anymore).
            
                    To avoid this, we would have to set the tick frequency to something like 100ms and then use
                    three events.
            
                        1st tick: Store the current focus state
                        2nd tick: Establish that no elements are selected.
                        3rd tick: Establish that STILL all elements are selected and then close the menu.
            
                    This would ensure that always at least 100 ms have passed with no dialog in focus.
                    """
                    if not any([state[1] for state in self._collection.values()]):
                        list(self._collection.keys())[0].Close() # Due to OnClose, closing one will close all.
            
                def OnFocusChange(self, sender: "MenuContainer", state: bool) -> None:
                    """Called by #MenuContainer when it changes its focus state.
                    """
                    self._collection[sender][1] = state
            
                def OnClose(self, sender: "MenuContainer") -> None:
                    """Called by #MenuContainer when it its closing.
                    """
                    self._collection[sender][0] = False
                    for dlg, (isOpen, _) in self._collection.items():
                        if isOpen:
                            dlg.Close()
            
                def Add(self, item: "MenuContainer") -> None:
                    """Adds a #MenuContainer to the managed dialogs of this radial menu.
                    """
                    if not isinstance(item, MenuContainer):
                        raise TypeError(f"{item = }")
                    if item in self._collection.keys():
                        raise KeyError(f"{item = }")
                    
                    item._handler = self
                    self._collection[item] = [False, False]
            
            class MenuContainer(c4d.gui.GeDialog):
                """Realizes a dialog which renders a title onto its canvas and opens a popup menu when the 
                canvas is clicked.
                """
                def __init__(self, menu: dict) -> None:
                    """Inits the dialog instance with a #menu dict.
                    """
                    def BuildMenu(menu: dict, title: str = "") -> c4d.BaseContainer:
                        """Converts a dict menu representation into its BaseContainer form.
            
                        The scheme of these dicts is of course not set in stone and there could be many more
                        things we can do, this form assume that there is a CallCommand (and icon) for each item. 
                        """
                        bc: c4d.BaseContainer = c4d.BaseContainer()
                        if title != "":
                            bc.InsData(1, title)
                        for key, value in menu.items():
                            if isinstance(value, int):
                                bc.InsData(value, f"{key}&i{value}&")
                            elif isinstance(value, dict):
                                sub: c4d.BaseContainer = BuildMenu(value, key)
                                bc.InsData(c4d.MENURESOURCE_SUBMENU, sub)
                        return bc
                    
                    self._handler: RadialHandler = None
                    self._title: str = menu.get("title", "None")
                    self._items: c4d.BaseContainer = BuildMenu(menu.get("items", {}))
                    self._position: c4d.Vector = c4d.Vector(0, 0, 0)
            
                def Command(self, id: int, msg: c4d.BaseContainer) -> bool:
                    """Opens the menu when the dialog is clicked and closes all dialogs by closing this dialog
                    when the user clicked an item.
                    """
                    res: int = c4d.gui.ShowPopupDialog(self, self._items, int(self._position.x), int(self._position.y))
                    if res != 0:
                        self.Close()
                    return True
                
                def AskClose(self) -> bool:
                    """Propagates the closing event of this dialog to the handler.
                    """
                    self._handler.OnClose(self)
                    return False
                
                def CreateLayout(self) -> bool:
                    """Adds the menu title to the dialog.
                    """
                    # Prepare the bitmap button container, we will draw the button without any bitmap, we are
                    # only after having a button with a centred text over the whole horizontal width. We could
                    # also use a GeUserArea instead to be more custom.
                    bc: c4d.BaseContainer = c4d.BaseContainer()
                    bc.SetInt32(c4d.BITMAPBUTTON_BACKCOLOR, c4d.COLOR_BG)
                    bc.SetBool(c4d.BITMAPBUTTON_DISABLE_FADING, True)
                    bc.SetString(c4d.BITMAPBUTTON_STRING, self._title)
            
                    # Add the button.
                    if not self.AddCustomGui(1000, c4d.CUSTOMGUI_BITMAPBUTTON, "", c4d.BFH_SCALE, 0, 0, bc):
                        raise MemoryError("Could not allocate bitmap button.")
                    
                    return True
            
                def Message(self, msg: c4d.BaseContainer, result: c4d.BaseContainer) -> int:
                    """Propagates the focus events of this dialog to the handler.
                    """
                    mid: int = msg.GetId()
                    if mid == c4d.BFM_GOTFOCUS:
                        self._handler.OnFocusChange(self, True)
                    if mid == c4d.BFM_LOSTFOCUS:
                        self._handler.OnFocusChange(self, False)
                    return super().Message(msg, result)
                
                def Timer(self, msg: c4d.BaseContainer) -> None:
                    """Sends timer ticks to the handler when the timer has been enabled for this dialog.
                    """
                    self._handler.OnTimerTick(self, msg)
            
            
            if __name__ == "__main__":
                # Define five menus to be managed by the radial handler.
                collection: list[MenuContainer] = [
                    MenuContainer({
                        "title": "Objects",
                        "items": {
                            "Cube": c4d.Ocube,
                            "Cone": c4d.Ocone,
                            "Sphere": c4d.Osphere,
                        }
                    }),
                    MenuContainer({
                        "title": "Deformers",
                        "items": {
                            "Bend": c4d.Obend,
                            "Twist": c4d.Otwist,
                            "Shear": c4d.Oshear,
                        }
                    }),
                    MenuContainer({
                        "title": "Generators",
                        "items": {
                            "Extrude": c4d.Oextrude,
                            "Lathe": c4d.Olathe,
                            "Loft": c4d.Oloft,
                            "Special": {
                                "SDS": c4d.Osds,
                                "Bool": c4d.Oboole
                            }
                        }
                    }),
                    MenuContainer({
                        "title": "Splines",
                        "items": {
                            "Circle": c4d.Osplinecircle,
                            "Rectangle": c4d.Osplinerectangle,
                            "Flower": c4d.Osplineflower,
                        }
                    }),
                    MenuContainer({
                        "title": "Scene",
                        "items": {
                            "Light": c4d.Olight,
                            "Camera": c4d.Ocamera,
                            "Sky": c4d.Osky,
                        }
                    })
                ]
            
                # Add the menus to the handler and open it.
                handler: RadialHandler = RadialHandler()
                for item in collection:
                    handler.Add(item)
            
                handler.Open(position=(300, 300), radius=100)
            

            MAXON SDK Specialist
            developers.maxon.net

            gheyretG 1 Reply Last reply Reply Quote 3
            • gheyretG
              gheyret @ferdinand
              last edited by

              Wow ! @ferdinand this has opened up a whole new world of ideas for me. I've come up with some new approaches and I can't wait to give them a try.

              www.boghma.com

              1 Reply Last reply Reply Quote 0
              • G
                Gaal Dornik
                last edited by Gaal Dornik

                Thanks for your detailed answers, i hoped there is a simpler way to do it)...
                Pie menus is a very common thing nowadays, why maxon developers haven't implemented it already?
                In modo for example you can create pie menus, popups...etc without any coding.
                Not every user is ready/have the time/etc to learn python to build a simple popup, but almost every user needs to customize their working software.
                I really hope you'll implement these features soon.

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