How do I sort the plug-in menu?
-
I registered several plugins in a .pyp file, but when I opened Cinema4D and opened the plugins menu, I found that his sorting is very bad, I want to know how to sort according to my requirements, and I want to add delimiters between the plugins, but I don't know how to do it
1、This is an example of a plug-in menu separator:
,
2、This is an example of the plugin I registered:import c4d, random, os, webbrowser from c4d import documents, gui, BaseContainer, storage as s, gui as g, utils as u, bitmaps from typing import Optional from c4d.gui import GeDialog # START ----------------------------- Plugins_ID ------------------------------------------------------------------------------------------------------------------------ Rig_Start_Plugins_ID = 9863771 Animation_Camera_Plugins_ID = 9863772 Add_Switch_Plugins_ID = 9863773 About_tree_Plugins_ID = 9863787 # END ----------------------------- Plugins_ID ------------------------------------------------------------------------------------------------------------------------ # START ----------------------------- 绑定开始 ------------------------------------------------------------------------------------------------------------------------ class Rig_Start(c4d.plugins.CommandData): def Execute(self, doc): pass return True if __name__ == "__main__": scriptPath = os.path.split(__file__)[0] bmp_Rig_Start = bitmaps.BaseBitmap() iconPath_Rig_Start = os.path.join(scriptPath, "res", "Rig_Start.tif") bmp_Rig_Start.InitWith(iconPath_Rig_Start) c4d.plugins.RegisterCommandPlugin( id=Rig_Start_Plugins_ID, str="绑定开始", info=0, icon=bmp_Rig_Start, help="创建绑定初始层组", dat=Rig_Start(), ) # END ----------------------------- 绑定开始 ------------------------------------------------------------------------------------------------------------------------ # START ----------------------------- Animation_Camera ------------------------------------------------------------------------------------------------------------------------ class Animation_Camera(c4d.plugins.CommandData): def Execute(self, doc): pass return True if __name__ == "__main__": scriptPath = os.path.split(__file__)[0] bmp_Animation_Camera = bitmaps.BaseBitmap() iconPath_Animation_Camera = os.path.join(scriptPath, "res", "Animation_Camera.tif") bmp_Animation_Camera.InitWith(iconPath_Animation_Camera) c4d.plugins.RegisterCommandPlugin( id=Animation_Camera_Plugins_ID, str="CameraAnimation", info=0, icon=bmp_Animation_Camera, help="创建动画摄像机层组", dat=Animation_Camera(), ) # END ----------------------------- Animation_Camera ------------------------------------------------------------------------------------------------------------------------ # START ----------------------------- 添加开关 ------------------------------------------------------------------------------------------------------------------------ class Add_Switch(c4d.plugins.CommandData): def Execute(self, doc): pass return True if __name__ == "__main__": # 获取脚本所在目录 scriptPath = os.path.split(__file__)[0] bmp_Add_Switch = bitmaps.BaseBitmap() iconPath_Add_Switch = os.path.join(scriptPath, "res", "Add_Switch.tif") bmp_Add_Switch.InitWith(iconPath_Add_Switch) c4d.plugins.RegisterCommandPlugin( id=Add_Switch_Plugins_ID, str="添加开关", info=0, icon=bmp_Add_Switch, help="选中层,点击添加开关!", dat=Add_Switch(), ) # END ----------------------------- 添加开关 ------------------------------------------------------------------------------------------------------------------------ # START ----------------------------- 关于... ------------------------------------------------------------------------------------------------------------------------ class About_tree(c4d.plugins.CommandData): def Execute(self, doc): pass return True if __name__ == "__main__": # 获取脚本所在目录 scriptPath = os.path.split(__file__)[0] bmp_about_tree = bitmaps.BaseBitmap() iconPath_about_tree = os.path.join(scriptPath, "res", "about_tree.tif") bmp_about_tree.InitWith(iconPath_about_tree) c4d.plugins.RegisterCommandPlugin( id=About_tree_Plugins_ID, str="关于...", info=0, icon=bmp_about_tree, help="使用教程及后续更新...", dat=About_tree(), ) # END ----------------------------- 关于... ------------------------------------------------------------------------------------------------------------------------
-
Hey @treezw , first of all, nice icons, love it!
If you want to customize your plugin menu, you can check this Building menus with C4DPL_BUILDMENU in S26+.
Cheers~
DunHou -
Hello @treezw,
Thank you for reaching out to us and thank you for providing stripped down example code, we really appreciate that. What @Dunhou said is right, you can build entirely custom menus entries as shown in that thread he linked to.
But I think you want to operate within the "Extensions" menu, right? I would not say that we discourage adding new top level menu entries in general, but the default should be that plugins are only placed in the "Extensions" menu. I am not quite sure how your screen shots came to pass, as they do not line up with the demo code you gave us. But in general, all entries are alpha-numerically sorted in the Extensions and its sub-menus. So, I would not call this 'very bad' sorting, it is just how Cinema does things. When we have for example the
Listing A
shown below, Cinema 4D will not care that we register the commands in the order Foo, Baz, Bar, it will put them in alphanumerical order into the menu, i.e., do this:This assumes that there is a
{SOMEPATH}\plugins\MyPluginCollection\myPluginCollection.pyp
with the code shown inListing A
and that the user has linked{SOMEPATH}\plugins
as a plugin path in his or her plugin collection. The sub menu entryMyPluginCollection
then comes from the folder of the same name.But we can disable this automated sorting with the
#$xy{SOMELABEL}
syntax, wherexy
is the two digit index of where the plugin hook should be displayed in its extensions menu. This is being shown inListing B
and will then result in this, where we force a non-alphanumerical order:One can also stack multiple folders in a plugin package to build complex menus in the extensions menu. E.g., this, where
{SOMEPATH}\py
is a plugins directory linked in a Cinema 4D instance:Will result in this:
There is of course some limit to which it makes sense to organize plugins in this manner. But in general, you should avoid opening new top level menu entries unless your plugin offers a s substantial feature set. This is not a technical limitation, there is no technical harm in doing that, but a user experience restriction. We want Cinema 4D to have a consistent and clean look. When every plugin creates its top-level menu entry, the menu bar of a user can become quite confusing or even overflow (become wider than one screen width). If your plugin suite mandates its own menu entry is up to you to decide but we would encourage asking yourself if that is really beneficial for your user experience.
Cheers,
FerdinandCode
Listing A
"""Realizes a simple plugin collection with multiple command plugins. """ import c4d class FooCommandData(c4d.plugins.CommandData): """Realizes a dummy command plugin. """ PLUGIN_ID: int = 1064229 PLUGIN_NAME: str = "Foo Command" def Execute(self, doc): print(f"Executing {self.PLUGIN_NAME}") return True class BarCommandData(FooCommandData): """Realizes a dummy command plugin. """ PLUGIN_ID: int = 1064230 PLUGIN_NAME: str = "Bar Command" class BazCommandData(FooCommandData): """Realizes a dummy command plugin. """ PLUGIN_ID: int = 1064231 PLUGIN_NAME: str = "Baz Command" def RegisterPlugins() -> None: """Registers the plugins of the plugin collection. """ # When we place this file inside a folder named "myPluginCollection" in the plugins directory, # linked as a plugin folder in Cinema, we will wind up with the following menu structure: # # Extensions # |- ... # |- myPluginCollection # |- Bar Command # |- Baz Command # |- Foo Command # # This is because Cinema will sort by default all entries in the Extensions alphanumerically. for i, hook in (FooCommandData, BazCommandData, BarCommandData): if not c4d.plugins.RegisterCommandPlugin( hook.PLUGIN_ID, hook.PLUGIN_NAME, 0, None ,None, hook()): raise RuntimeError(f"Failed to register {hook.PLUGIN_NAME} plugin.") if __name__ == "__main__": RegisterPlugins()
Listing B
"""Realizes a simple plugin collection with multiple command plugins with a custom menu order. """ import c4d class FooCommandData(c4d.plugins.CommandData): """Realizes a dummy command plugin. """ PLUGIN_ID: int = 1064229 PLUGIN_NAME: str = "Foo Command" def Execute(self, doc): print(f"Executing {self.PLUGIN_NAME}") return True class BarCommandData(FooCommandData): """Realizes a dummy command plugin. """ PLUGIN_ID: int = 1064230 PLUGIN_NAME: str = "Bar Command" class BazCommandData(FooCommandData): """Realizes a dummy command plugin. """ PLUGIN_ID: int = 1064231 PLUGIN_NAME: str = "Baz Command" def RegisterPlugins() -> None: """Registers the plugins of the plugin collection. """ # To sort plugins in a custom order, we must use the "$#00" syntax for the plugin label. The plugin # will not display that prefix in its name, but with the `xy` part in the prefix we can set an position # index for the menu of the plugin. E.g., "#$05Blah Command" means that Cinema 4D will try to put the # "Blah Command" at the 5th position in that menu. for i, hook in enumerate((FooCommandData, BazCommandData, BarCommandData)): # E.g, "#$00Foo Command". It is important to use the zfill method to ensure that the number # has always two digits. We cannot do any sorting beyond 100 plugins within the same menu. label: str = f"#${str(i).zfill(2)}{hook.PLUGIN_NAME}" if not c4d.plugins.RegisterCommandPlugin( hook.PLUGIN_ID, label, 0, None ,None, hook()): raise RuntimeError(f"Failed to register {hook.PLUGIN_NAME} plugin.") if __name__ == "__main__": RegisterPlugins()
-
@ferdinand I'm very sorry, but I was wondering how the separator on the screenshot is implemented? Yes, my question is in the "Extensions" menu
-
Hey, separators are not possible in this menu, as it is dynamically built. When you want separators, you must build your own menu.
-
@ferdinand I don't quite understand what you mean, because the plugin I took the screenshot of is also in the "Extensions" menu, but it has "Group Separator".
-
Hey @treezw,
As it turns out, there is also a special syntax with which you can do that, but it requires you registering a dummy hook (with its own ID) for each separator.
Cheers,
FerdinandResult
Code
"""Realizes a simple plugin collection with multiple command plugins. """ import c4d class FooCommandData(c4d.plugins.CommandData): """Realizes a dummy command plugin. """ PLUGIN_ID: int = 1064229 PLUGIN_NAME: str = "Foo Command" def Execute(self, doc): print(f"Executing {self.PLUGIN_NAME}") return True class BarCommandData(FooCommandData): """Realizes a dummy command plugin. """ PLUGIN_ID: int = 1064230 PLUGIN_NAME: str = "Bar Command" class BazCommandData(FooCommandData): """Realizes a dummy command plugin. """ PLUGIN_ID: int = 1064231 PLUGIN_NAME: str = "Baz Command" class SeparatorCommandData(c4d.plugins.CommandData): """Realizes a dummy command plugin used by separators in menus. Please use a command data plugin as a dummy plugin, as there are not unlimited slots for other plugin types in Python (a user can have only 50 of each NodeData derived plugin type installed). We can have each separator use this type but they all need to have a unique ID. """ SEPARATOR_ID_1: int = 1064234 SEPARATOR_ID_2: int = 1064235 def Execute(self, doc): return True def RegisterPlugins() -> None: """Registers the plugins of the plugin collection. """ # To sort plugins in a custom order, we must use the "$#00" syntax. We can pass here a two-digit # number for the index of the plugin in the menu. The logic we use here is that when #hook is a # an #int, we register there a dummy plugin for a separator. for i, hook in enumerate((FooCommandData, SeparatorCommandData.SEPARATOR_ID_1, BazCommandData, SeparatorCommandData.SEPARATOR_ID_2, BarCommandData)): # Separators can be realized with the special syntax "$#00--", but we need a dummy plugin # for each separator. We can have each separator use the DummyCommandData type but they all # need to have a unique plugin ID. If you want to use separators, please use a command data # plugin use a command data plugin as the dummy plugin, as there are not unlimited slots for # other plugin types. if isinstance(hook, int): label = f"#${str(i).zfill(2)}--" # E.g., "#$01--" will be a separator at index 1 pid: int = hook hook = SeparatorCommandData else: label = f"#${str(i).zfill(2)}{hook.PLUGIN_NAME}" # E.g., "#$00Foo" will be "Foo" at index 0 pid: int = hook.PLUGIN_ID print (f"{label = }") if not c4d.plugins.RegisterCommandPlugin(pid, label, 0, None ,None, hook()): raise RuntimeError(f"Failed to register {hook} plugin.") if __name__ == "__main__": RegisterPlugins()
-
@ferdinand Thank you very much for your help, I have solved the problem!