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
- We listen for the C4D PluginMessage named c4d.C4DPL_BUILDMENU, and build our own menus, if it is found.
- In R25 c4d.C4DPL_BUILDMENU gets emitted once, and everything works as expected.
- 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...
- 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
- Why is c4d.C4DPL_BUILDMENU fired 4 times, instead of once?
- If we react to the first pluginMessage only (→ same time in startup sequence), why is R26 behaving differently?
- 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
- 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.- 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.- 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 callc4d.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,
FerdinandResult:
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
-
Hello Ferdinant. I'm following up on this ticket that I opened last November.
First of all thank you for your help. I am running some tests to make sure that our custom menus are built only once when starting R26. If possible, I would like to still use the C4DPL_BUILDMENU event and simply check if my menus already exist. I know all menus in C4D have their own id, so this should be an easy check.
However, I encouter a first issue:
When firing c4d.gui.GetMenuResource("M_EDITOR") to Get the whole menu of Cinema 4D (as you mention in your code here) I get a different address every time:>>> c4d.gui.GetMenuResource("M_EDITOR") <c4d.BaseContainer object at 0x000001B406C86580> >>> c4d.gui.GetMenuResource("M_EDITOR") <c4d.BaseContainer object at 0x000001B406CA6A40> >>> c4d.gui.GetMenuResource("M_EDITOR") <c4d.BaseContainer object at 0x000001B406C88C80>
So when I use c4d.gui.SearchMenuResource(main_menu_ressource, my_custom_menu) to check if my custom menu is part of the main menu, this always return False, as the address of the main_menu object changes.
Could you please enlighten me on this?
-
Hello @alexandre-dj,
I know all menus in C4D have their own id, so this should be an easy check.
That is only true on a relatively abstract level. There are string symbols, e.g.,
IDS_EDITOR_PLUGINS
for menu resources, but there is no strict namespace management or anything else on might associate with statements such as 'all menus in C4D have their own id'. Also note that all menu entries in a menu container are stored under the integer IDsMENURESOURCE_COMMAND
andMENURESOURCE_SUBMENU
as shown in my example above. So, when you have a menu with ten commands and four sub-menus, all fourteen items are stored under these two IDs. This works becauseBaseContainer
is actually not a hash map as one might think, and can store more than one value under a key.Long story short - traversing and modifying menus is not ultra complicated but I would also not label it as 'easy' ; which is why I did provide the example in my previous posting.
When firing c4d.gui.GetMenuResource("M_EDITOR") to Get the whole menu of Cinema 4D (as you mention in your code here) I get a different address every time:
>>> c4d.gui.GetMenuResource("M_EDITOR") <c4d.BaseContainer object at 0x000001B406C86580> >>> c4d.gui.GetMenuResource("M_EDITOR") <c4d.BaseContainer object at 0x000001B406CA6A40> >>> c4d.gui.GetMenuResource("M_EDITOR") <c4d.BaseContainer object at 0x000001B406C88C80>
You are calling here the Python layer for returning a Python object representation of the C++ object that represents the menu. Cinema 4D is still a C++ application and the menu exists in your memory with the layout defined by the C++ type
BaseContainer
. That type has memory-wise nothing to do with its Python counter partc4d.BaseContainer
.So, the Python layer must create that Python data on the fly, as otherwise the memory-footprint of Cinema 4D would double (actually more, because Python data is not exactly sparse data) and synchronizing both data layers would bring Cinema 4D to a crawl. TLDR; you retrieve the same data expressed as a different memory object because 'that is how Python works'.
So when I use c4d.gui.SearchMenuResource(main_menu_ressource, my_custom_menu) to check if my custom menu is part of the main menu, this always return False, as the address of the main_menu object changes.
Why your call fails here, depends a bit on what you are passing. The first argument should be the string symbol inside a sub menu, and the second item is the sub menu. When you store a reference to a sub-menu from a previous call to
GetMenuResource
, this might not work whenSearchPluginMenuResource
searches by identity instead of equality, i.e., it expects that container to be at a certain memory location (have not tried though). There is alsoc4d.gui.SearchPluginMenuResource(identifier='IDS_EDITOR_PLUGINS')
, which should be sufficient to find anything that does not produce a name collision. I personally would just write my own stuff as demonstrated above, since I can then search however I want to.Cheers,
Ferdinand -
-
-
-
-
-