Insert my own command in main render Tab
-
I found where to insert my own command (plugin),
but I get following message.Traceback (most recent call last): File "scriptmanager", line 22, in <module> File "scriptmanager", line 18, in main TypeError: argument 3 must be PyCapsule, not tuple
Also I am not sure I insert my plugin correctly.
I do not fully understand how to indicate my plugin; PLUGIN_CMD_5159, name or id?Here the code
from typing import Optional import c4d doc: c4d.documents.BaseDocument # The active document op: Optional[c4d.BaseObject] # The active object, None if unselected def main() -> None: MainMenu = c4d.gui.GetMenuResource("M_EDITOR") for bcMenuId, bcMenu in MainMenu: #print (bcMenu[c4d.MENURESOURCE_SUBTITLE]) if bcMenu[c4d.MENURESOURCE_SUBTITLE] == "IDS_EDITOR_RENDER": for sub in bcMenu: if (sub[1] == "IDM_RENDERAUSSCHNITT"): print ("*** Insert after this ***") # Add my plugin bcMenu.InsDataAfter(c4d.MENURESOURCE_COMMAND, "PLUGIN_CMD_5159", sub) c4d.gui.UpdateMenus() if __name__ == '__main__': main()
-
Hello @pim,
Thank you for reaching out to us.
BaseContainer.__iter__
yields atuple[int, any]
for the key-value pairs of the container. You callBaseContainer.__iter__
withfor sub in bcMenu
.sub
is therefore atuple[int, any]
. YourbcMenu.InsDataAfter
then complains that you pass this tuple.I would also recommend having a look at this thread.
Cheers,
Ferdinand -
@ferdinand
Ok, I can now insert my command in the render Tab, but it is positioned at the end.
This is, I guess, because I use InsData() instead of InsDataAfter()Refering to my first code, this was my thinking:
When using InsDataAfter(), looking at the documentation, I thought that last(any) indicates the position after where to insert the command. That is why I used sub.So, how to indicate where to insert the command?
BaseContainer.InsDataAfter(self, id, n, last)¶ Inserts an arbitrary data at the specified id after last. Warning This function does not check if the ID already exists in the container! Parameters id (int) – The ID to insert at. n (any) – The data to insert. last (any) – The data to insert after.
Working code that inserts the command at the end.
from typing import Optional import c4d pluginId = "1052844" # official plugin ID def main() -> None: MainMenu = c4d.gui.GetMenuResource("M_EDITOR") for bcMenuId, bcMenu in MainMenu: #print (bcMenu[c4d.MENURESOURCE_SUBTITLE]) if bcMenu[c4d.MENURESOURCE_SUBTITLE] == "IDS_EDITOR_RENDER": for sub in bcMenu: if (sub[1] == "IDM_RENDERAUSSCHNITT"): print ("*** Insert after this ***") # Add my plugin #bcMenu.InsDataAfter(c4d.MENURESOURCE_COMMAND, "PLUGIN_CMD_5159", sub) bcMenu.InsData(c4d.MENURESOURCE_COMMAND, "PLUGIN_CMD_" + pluginId) c4d.gui.UpdateMenus() if __name__ == '__main__': main()
-
Hey @pim,
Using
BaseContainer.InsDataAfter()
does not make too much sense in this case, as you cannot get hold of thePyCObject
that is wrapping the data entries you are after. The method has its place in other cases, but for menus, it is pretty much meaningless in Python. A visualization of a menu container looks like this:{ MENURESOURCE_SUBTITLE: "My Menu" MENURESOURCE_COMMAND: [ ID_COMMAND_1, ID_COMMAND_2, ID_COMMAND_3 ] MENURESOURCE_SUBMENU: [ { MENURESOURCE_SUBTITLE: "Weeh, I am a sub menu!" MENURESOURCE_COMMAND: [ ID_FOO, ID_BAR ] }, { MENURESOURCE_SUBTITLE: "Can you please be quiet up there?" MENURESOURCE_COMMAND: [ ID_BOB, ID_IS, ID_YOUR, ID_UNCLE ] } ] }
As you can see here and in your own code, all entries in a menu that are commands, are stored under the same ID, namely
MENURESOURCE_COMMAND
. So, you would have to get hold of for example the data pointer forID_COMMAND_1
to insert something after it. But you cannot in Python, you can only get thePyCObject
data pointers for whole entries withBaseContainer.GetDataPointer
. What you must do in your case, is rebuilding the whole submenu. Find a code example below.WE DO NOT CONDONE PLUGINS THAT REORDER CONTENT IN THE NATIVE MENUS OF CINEMA 4D. In general, plugins should either appear under the main menu entry "Extensions" or have their own main menu entry, e.g., "XParticles". They should not inject themselves into native main menu entries, e.g., "Render". When it is unavoidable to do this (which it never is), you should at least NOT REORDER things. This applies only for publicly shipped plugins/scripts. In a private or studio environment, you are free to remodel all menus to your liking. Cheers,
FerdinandResult:
Code:
"""Demonstrates how to insert an item into an existing menu. WE DO NOT CONDONE PLUGINS THAT REORDER CONTENT IN THE NATIVE MENUS OF CINEMA 4D. In general, plugins should either appear under the main menu entry "Extensions" or have their own main menu entry, e.g., "XParticles". They should not inject themselves into native main menu entries, e.g., "Render". When it is unavoidable to do this (which it never is), you should at least NOT REORDER things: NOT OK - "Render": [a, myNewStuff, ..., n] OK-ish - "Render": [a, ..., n, myNewStuff] This has less a technical and more a UX reason, as we want to avoid having plugins going around that break the UX habits of users, i.e., the muscle memory of clicking the item that is "that much" pixels below the root of "Render". This applies only for publicly shipped plugins/scripts. In a private or studio environment, you are free to remodel all menus to your liking. """ import c4d def main() -> None: """Runs the example. """ for _, data in c4d.gui.GetMenuResource("M_EDITOR"): if not (isinstance(data, c4d.BaseContainer) and data[c4d.MENURESOURCE_SUBTITLE] == "IDS_EDITOR_RENDER"): continue # To do what you want to do, you will have to flush the container. When you are not careful # and remove menu items which Cinema 4D expects to be present, or change menu structures # that Cinema 4D considers to be given, you can crash Cinema 4D. # Get all items in 'M_EDITOR' as a list of key-value pairs and bail when our insertion point # is not in there. "IDM_RENDERAUSSCHNITT" as an insertion point alone is ambiguous, as we # do not care about a MENURESOURCE_SUBTITLE with that value for example. insertionPoint: tuple[int, str] = (c4d.MENURESOURCE_COMMAND, "IDM_RENDERAUSSCHNITT") items: list[tuple[int, str | c4d.BaseContainer]] = [item for item in data] if insertionPoint not in items: continue # Insert a new command for the cube object after that value. menuEntry: tuple[int, str] = (c4d.MENURESOURCE_COMMAND, "PLUGIN_CMD_5159") items.insert(items.index(insertionPoint) + 1, menuEntry) # Flush the container and write the new data. data.FlushAll() for cid, value in items: data.InsData(cid, value) # Make sure to update menus sparingly, your code did this with every iteration of the loop. c4d.gui.UpdateMenus() if __name__ == '__main__': main()
-
@ferdinand
I run the script, but it hangs cinema 4d.
After some debugging, I could see that the insertion point was found and that it was inserted in items.
I am not sure what statement hangs cinema 4d.I am using R2023.2.1 on a Windows 10 Pro 22H2.
-
Hey @pim,
Well, as I stated in the code above, doing this can crash your Cinema 4D as what you want to do is not intended. For me this is not crashing, but I am not surprised that it does on another machine. You could try to be more careful by calling
data.RemoveData()
(instead ofdata.FlushAll()
) to only clear out the things that you must remove, i.e., stuff inMENURESOURCE_COMMAND
, but then you have also to fiddle the container indexing into order. You could also try to defer things to C4DPL_BUILDMENU, as less things rely there on a certain state of a menu. But be warned, I have also experienced crashes from there, when I was too drastic in my changes.I understand that people do not like this answer, but what you want to do is simply out of scope of support. I have already helped you here more than I probably should have.
Cheers,
Ferdinand -
I really do appreciate all your effort and I will take your warning seriously and create my own menu Tab.
Thanks again.Regards,
Pim -