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 do I sort the plug-in menu?

    Cinema 4D SDK
    python
    3
    8
    1.3k
    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.
    • treezwT
      treezw
      last edited by

      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:
      Snipaste_2024-09-04_23-29-59.png ,Snipaste_2024-09-04_23-09-09.png
      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 ----------------------------- 关于... ------------------------------------------------------------------------------------------------------------------------
      
      DunhouD 1 Reply Last reply Reply Quote 0
      • DunhouD
        Dunhou @treezw
        last edited by

        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

        https://boghma.com
        https://github.com/DunHouGo

        ferdinandF 1 Reply Last reply Reply Quote 0
        • ferdinandF
          ferdinand @Dunhou
          last edited by ferdinand

          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:

          base.png

          This assumes that there is a {SOMEPATH}\plugins\MyPluginCollection\myPluginCollection.pyp with the code shown in Listing A and that the user has linked {SOMEPATH}\plugins as a plugin path in his or her plugin collection. The sub menu entry MyPluginCollection then comes from the folder of the same name.

          But we can disable this automated sorting with the #$xy{SOMELABEL} syntax, where xy is the two digit index of where the plugin hook should be displayed in its extensions menu. This is being shown in Listing B and will then result in this, where we force a non-alphanumerical order:

          sorted.png

          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:

          28df0feb-ce85-4342-9938-d765b07c935f-image.png

          Will result in this:

          e53f2962-bc45-49c0-916e-cb79de4bc153-image.png

          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,
          Ferdinand

          Code

          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()
          

          MAXON SDK Specialist
          developers.maxon.net

          treezwT 1 Reply Last reply Reply Quote 0
          • treezwT
            treezw @ferdinand
            last edited by

            @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

            1 Reply Last reply Reply Quote 0
            • ferdinandF
              ferdinand
              last edited by

              Hey, separators are not possible in this menu, as it is dynamically built. When you want separators, you must build your own menu.

              MAXON SDK Specialist
              developers.maxon.net

              treezwT 1 Reply Last reply Reply Quote 0
              • treezwT
                treezw @ferdinand
                last edited by

                @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".111111111.png

                1 Reply Last reply Reply Quote 0
                • ferdinandF
                  ferdinand
                  last edited by ferdinand

                  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,
                  Ferdinand

                  Result

                  b047158c-d1b3-4f56-8da1-37ee6312288c-image.png

                  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()
                  

                  MAXON SDK Specialist
                  developers.maxon.net

                  treezwT 1 Reply Last reply Reply Quote 0
                  • treezwT
                    treezw @ferdinand
                    last edited by

                    @ferdinand Thank you very much for your help, I have solved the problem!😄

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