Menu items without RegisterCommandPlugin?



  • Is it possible to set up custom menus with menu items that execute python code without setting up a "plugin" and registering it with RegisterCommandPlugin?

    We have a facility standard menu framework that creates a custom menu populated with items that correspond to python scripts found on disk under an artist's current working project. One of the menu items is a "Refresh" command that re-builds the menu, used for when scripts are added/deleted while the user is in C4D (and we don't want to force them to exit their session and restart C4D just to have this menu re-built).

    This mechanism works in all our other DCCs, such as Maya, Nuke, etc., but the need to register every single menu item as a plugin that can only be registered with C4D at startup makes the Refresh mechanism useless under C4D.

    Suggestions?



  • Hi John,

    you should be able to enhance the menu using a simple python-script (i.e. refresh command), but I've haven't tried it myself. Here's a small example on enhancing the menu:

    Enhance Menu Example

    From there you can list your local python-scripts, however you need a simple function to run() those "external" scripts.

    I'm currently using this function for one of my plugins which more or less does the same:

        def execute_script(self, filename):
            fl = open(filename, 'rb')
            code = compile(fl.read(), filename, 'exec')
            doc = c4d.documents.GetActiveDocument()
            scope = {
            '__file__': filename,
            '__name__': '__main__',
            'doc': doc,
            }
            exec code in scope
    

    There are some downsides to using this function but it might get you started...

    Cheers,
    Lasse



  • Hi @jcooper, as @lasselauch pointed you, usually to build menu its recommended to create a plugin that will react to the C4DPL_BUILDMENU message.
    However, if you want a one time action this is possible.

    Now regarding your question to have a custom folder, this is not properly supported by Cinema 4D so either you will have to use @lasselauch workaround but then I don't really see how to make dynamic CommandData for each entry since in Python there is no way to dynamically creates CommandData within the script manager since plugins can only be registered in a pyp file.

    Or you can put them in the Cinema 4D script folder located in:

    • Windows: %APPDATA%/MAXON/{cinemaversion}/library/scripts.
    • MacOS: ~/Library/Preferences/MAXON/{cinemaversion}/library/scripts.
      or defined by the enviroment variable C4D_SCRIPTS_DIR

    The benefice of the last decision is that Cinema 4D will load these scripts and create a CommandData for each of them dynamically. This way you can retrieve this commanddata and build the menu. Note that to retrieve the associated CommandData id, I search by plugin name, so be sure to have a unique name for your script.

    Here an example, that will build a menu ( I have 2 scripts, testScript and createMenu):

    import c4d
    
    def GetScriptIdByName(name):
        pluginList = c4d.plugins.FilterPluginList(c4d.PLUGINTYPE_COMMAND, True)
        for plugin in pluginList:
            if plugin.GetName() == name:
                return plugin.GetID()
    
        return None
    
    def GetMenuContainer(name):
        mainMenu = c4d.gui.GetMenuResource("M_EDITOR")
    
        customMenu = c4d.BaseContainer()
        for bcMenuId, bcMenu in mainMenu:
            if bcMenu[c4d.MENURESOURCE_SUBTITLE] == name:
                customMenu = mainMenu.GetContainerInstance(bcMenuId)
                break
    
        if customMenu is not None:
            customMenu.FlushAll()
    
        customMenu.InsData(c4d.MENURESOURCE_SUBTITLE, name)
    
        return customMenu
    
    def AddsMenuToC4DMenu(menu):
        mainMenu = c4d.gui.GetMenuResource("M_EDITOR")
        for bcMenuId, bcMenu in mainMenu:
            if bcMenu[c4d.MENURESOURCE_SUBTITLE] == menu[c4d.MENURESOURCE_SUBTITLE]:
                return
    
        mainMenu = c4d.gui.GetMenuResource("M_EDITOR")
        mainMenu.InsData(c4d.MENURESOURCE_STRING, menu)
    
    def AddEntry(menuContainer, scriptName):
        scriptId = GetScriptIdByName(scriptName)
        if scriptId is None:
            return
    
        menuContainer.InsData(c4d.MENURESOURCE_COMMAND, "PLUGIN_CMD_{0}".format(scriptId))
    
    
    # Main function
    def main():
        menuName = "Custom Python menu"
        menuContainer = GetMenuContainer(menuName)
    
        AddEntry(menuContainer, "testScript")
        AddEntry(menuContainer, "createMenu")
    
        AddsMenuToC4DMenu(menuContainer)
    
        c4d.gui.UpdateMenus()
    
    # Execute main()
    if __name__=='__main__':
        main()
    

    Cheers,
    Maxime.



  • Nice, thanks for the example @m_adam!
    I always thought you have to restart c4d to reload all the scripts located in the scripts folder..!?



  • @lasselauch Correct but the idea is to have all the scripts already loaded.



  • Well, one request was to "Refresh" the menu when scripts are added/deleted while the user is in C4D, this is sadly a limitation and not possible with your solution... am I right?!



  • @lasselauch correct.



  • As @lasselauch points out, the issue is the functionality of the "Refresh" command.

    I've added C4D support for our menu generation framework and it creates a hierarchical menu matching the python scripts it finds on disk just fine. Execution of those scripts works too.

    The only issue is the inability of the Refresh menu item to rebuild the menu when it encounters a new script that didn't exist when C4D was launched.

    I guess my only recourse is to pop up a MessageDialog telling the user that the Refresh command simply doesn't work in Cinema 4D.



  • Hm, but it would be possible to Flush the whole Menu, rebuild it and reinitialize the updated Menu via c4d.gui.UpdateMenus(), wouldn't it!?



  • My menugen code is able to rebuild the menu.

    However, when it tries to use RegisterCommandPlugin() for the new python scripts it finds on disk, C4D issues an error indicating it can't find a .pyp file. Apparently this is because RegisterCommandPlugin() can't be used at any time except at application launch. And without the ability to RegisterCommandPlugin() for the new scripts, I can't add them to the newly built menu as menu items.



  • Hi, I'm sorry for the delay, but I can only confirm what you said.
    In C++ it's possible to call RegisterCommandPlugin but not in Python at runtime.

    So I guess the best approach is to have as you suggested a menu entry (a pre-registered c4d script or CommandData) that will then create a PopuDialog with a list of all scripts, and then it's up to you to execute them with the code @lasselauch provided.
    So here an example of how to implement it.

    import c4d
    import os
    
    def main():
        # Gets all python script of a folder
        searchPath = r"%appdata%\Roaming\Maxon\Maxon Cinema 4D R21_115_XXXXXX\library\scripts"
        pythonFiles = [os.path.join(folder, f) for f in os.listdir(folder) if os.path.isfile(os.path.join(folder, f)) and f.endswith(".py")]
    
        # Build the menu for all the entries
        menu = c4d.BaseContainer()
        for pythonFileId, pythonFile in enumerate(pythonFiles):
            menuId = c4d.FIRST_POPUP_ID + pythonFileId
            filename = os.path.basename(pythonFile)
            menu.InsData(menuId, filename)
    
        # Example to also list regular command.
        # Uses POPUP_EXECUTECOMMANDS in ShowPopupDialog flag so if its a command its executed directly
        menu.InsData(c4d.Ocube, "CMD")
    
        # Display the PopupDialog
        result = c4d.gui.ShowPopupDialog(cd=None, bc=menu, x=c4d.MOUSEPOS, y=c4d.MOUSEPOS, flags=c4d.POPUP_EXECUTECOMMANDS | c4d.POPUP_BELOW | c4d.POPUP_CENTERHORIZ)
        
        # If result is bigger than FIRST_POPUP_ID it means user selected something
        if result >= c4d.FIRST_POPUP_ID:
            
            # Retrieves the selected python file
            scriptId = result - c4d.FIRST_POPUP_ID
            pythonFile = pythonFiles[scriptId]
            
            # Execute it and copy the global to it ( so doc, op are accessible as well)
            fl = open(pythonFile, 'rb')
            code = compile(fl.read(), pythonFile, 'exec')
            exec(code, globals())
    
    # Execute main()
    if __name__=='__main__':
        main()
    ```
    
    Cheers,
    Maxime.

Log in to reply