Building menus with C4DPL_BUILDMENU in S26+

Dear Community,

We have received the following support request via e-mail, and I thought it might be interesting to share the answer here:

Analysis

  1. We listen for the C4D PluginMessage named c4d.C4DPL_BUILDMENU, and build our own menus, if it is found.
  2. In R25 c4d.C4DPL_BUILDMENU gets emitted once, and everything works as expected.
  3. In R26 it gets emitted four times – the first time at the same position in the startup process, and then three more times later towards the end. This results in 4 custom menu builds on our side, which consume a lot of time...
  4. In R26 we cannot simply make sure that we build the menus only once.
    Even though this menu build happens at the same time in the startup sequence as in R25, the menus do not appear in the UI this way.

Question for Support

  1. Why is c4d.C4DPL_BUILDMENU fired 4 times, instead of once?
  2. If we react to the first pluginMessage only (→ same time in startup sequence), why is R26 behaving differently?
  3. Is there something like executedDelayed(), so that we can execute Python code at the very end of the startup sequence, after the UI initialization has been finished?

Cheers,
Ferdinand

Dear Community,

this is our answer 🙂

  1. Why is c4d.C4DPL_BUILDMENU fired 4 times, instead of once?

In general, we do not make any promises regarding messages being emitted only once or things not being None \ nullptr. When there are any guarantees, the message or function description will explicitly say so. The recent change in behavior is caused by the menu being dynamically rebuilt by Cinema 4D.

  1. If we react to the first pluginMessage only (→ same time in startup sequence), why is R26 behaving differently?

I am not quite sure how you mean that, what do you mean by first? Are you using a module attribute like, for example, didBuiltMenu? Your code did not show this. You could use a module attribute, and this would mostly work, as *.pyp plugin modules are persistent, but reloading the Python plugins will throw a wrench into things. I personally would simply check for the existence of a menu item by title.

  1. Is there something like executedDelayed(), so that we can execute Python code at the very end of the startup sequence, after the UI initialization has been finished?

There are multiple phases in the lifecycle of a Cinema 4D instance, they are all documented under PluginMessage. When you want to update the menu outside of C4DPL_BUILDMENU, you must call c4d.gui.UpdateMenus() after the changes.

But this all seems a bit too complicated for the task IMHO. Simply search for the menu entry you want to add, e. g., My Menu, and stop operations when it already does exist. You could also make this more complicated and index based, so that you can distinguish two menus called My Menu. You could also flush and update existing menus, in the end, menus are just an instance of c4d.BaseContainer. You can more or less do what you want.

Find a simple example for your problem below.

Cheers,
Ferdinand

Result:
Screenshot 2022-11-23 at 14.55.19.png

The code (must be saved as a pyp file):

"""Demonstrates adding and searching menu entries.
"""

import c4d
import typing

# A type alias for a menu data type used by the example, it is just easier to define a menu as
# JSON/a dict, than having to write lengthy code.
MenuData: typing.Type = dict[str, typing.Union[int, 'MenuData']]

# Define the menu which should be inserted. The data does not carry a title for the root of the
# menu, it will be defined by the #UpdateMenu call. All dictionaries are expanded (recursively) 
# into sub-menus and all integer values will become commands. The keys for commands do not matter
# in the sense that Cinema 4D will determine the label of a menu entry, but they are of course 
# required for the dictionary. For older version of Cinema 4D, you will have to use #OrderedDict,
# as ordered data for #dict is a more recent feature of Python (3.7 if I remember correctly).
MENU_DATA: MenuData = {
    "Objects": {
        "0": c4d.Ocube,
        "1": c4d.Osphere,
        "Splines": {
            "0": c4d.Osplinecircle,
            "1": c4d.Osplinerectangle
        }
    },
    "0": 13957, # Clear console command
    # "1": 12345678 # Your plugin command
}

def UpdateMenu(root: c4d.BaseContainer, title: str, data: MenuData, forceUpdate: bool = False) -> bool:
    """Adds #data to #root under a menu entry called #title when there is not yet a menu entry 
    called #title.

    When #forceUpdate is true, the menu of Cinema 4D will be forced to update to the new state 
    outside of C4DPL_BUILDMENU.
    """
    def doesContain(root: c4d.BaseContainer, title: str) -> bool:
        """Tests for the existence of direct sub containers with #title in #root.

        One could also use #c4d.gui.SearchMenuResource, this serves more as a starting point if one
        wants to customize this (search for a specific entry among multiple with the same title,
        flushing an entry, etc.).
        """
        # BaseContainer can be iterated like dict.items(), i.e., it yields keys and values.
        for _, value in root:
            if not isinstance(value, c4d.BaseContainer):
                continue
            elif value.GetString(c4d.MENURESOURCE_SUBTITLE) == title:
                return True
        
        return False
    
    def insert(root: c4d.BaseContainer, title: str, data: MenuData) -> c4d.BaseContainer:
        """Inserts #data recursively under #root under the entry #title.
        """
        # Create a new container and set its title.
        subMenu: c4d.BaseContainer = c4d.BaseContainer()
        subMenu.InsData(c4d.MENURESOURCE_SUBTITLE, title)

        # Iterate over the values in data, insert commands, and recurse for dictionaries.
        for key, value in data.items():
            if isinstance(value, dict):
                subMenu = insert(subMenu, key, value)
            elif isinstance(value, int):
                subMenu.InsData(c4d.MENURESOURCE_COMMAND, f"PLUGIN_CMD_{value}")
        
        root.InsData(c4d.MENURESOURCE_SUBMENU, subMenu)
        return root

    # #title is already contained in root, we get out. You could also fashion the function so
    # that is clears out an existing entry instead of just reporting its existence, but I did not
    # do that here.
    if doesContain(root, title):
        return False

    # Update #root and force a menu update when so indicated by the user.
    insert(root, title, data)
    if forceUpdate and c4d.threading.GeIsMainThreadAndNoDrawThread():
        c4d.gui.UpdateMenus()

    return True


def PluginMessage(mid: int, data: typing.Any) -> bool:
    """Updates the menu with some menu data only once when C4DPL_BUILDMENU is emitted.
    """
    if mid == c4d.C4DPL_BUILDMENU:
        # Get the whole menu of Cinema 4D an insert #MENU_DATA under a new entry "MyMenu"
        menu: c4d.BaseContainer = c4d.gui.GetMenuResource("M_EDITOR")
        UpdateMenu(root=menu, title="MyMenu", data=MENU_DATA)

def SomeFunction():
    """Can be called at any point to update the menu, as long as the call comes from the main
    thread (and is not a drawing thread).
    """
    # Get the whole menu of Cinema 4D an insert #MENU_DATA under a new entry "MyMenu"
    menu: c4d.BaseContainer = c4d.gui.GetMenuResource("M_EDITOR")
    UpdateMenu(root=menu, title="MyMenu", data=MENU_DATA, forceUpdate=True)

if __name__ == "__main__":
    pass