Solved Custom menu in C4D top menu bar with custom Python commands

I'm looking to set up a custom menu in the Cinema4D's top menu bar. I found the Enhancing the Main Menu example which shows how to add a custom menu yet it skips to to show how to add a bunch of custom commands as menu items.

So basically this is what I found:

import c4d
from c4d import gui
  

main_menu = gui.GetMenuResource('M_EDITOR')            

# Create custom menu
menu = c4d.BaseContainer()
menu.InsData(c4d.MENURESOURCE_SUBTITLE, "MyMenu")

# Add commands
menu.InsData(c4d.MENURESOURCE_COMMAND, "IDM_NEU")
menu.InsData(c4d.MENURESOURCE_SEPERATOR, True)
menu.InsData(c4d.MENURESOURCE_COMMAND, "IDM_NEU")

# Add custom menu to main menu
main_menu.InsData(c4d.MENURESOURCE_STRING, menu)

# Refresh menu bar
gui.UpdateMenus()

Yet now I'm looking to add some menu items that allow me to trigger some custom Python code to show some pipeline tools (Qt widgets). This would be the first step to integrating open-source pipeline Avalon completely into Cinema4D, status for that will be tracked in this Avalon issue.

  1. Say I wanted to add five menu items, each triggering its own tool. How would I do so? I believe I have to register a command and then add it to the menu item. But a quick search didn't bring me to a concrete example on how to do so. I found this topic but it still was unclear how to proceed and make a simple menu item with some custom commands.
  2. Would I need a unique plug-in ID for each single command I'll register? Or can this be done differently?

A short to the point example could would be much appreciated.

Thanks in advance! :)

Hi Bigroy, thanks for reaching out us.

Sorry for the answer taking longer than expected by holiday season didn't helped ;)

  1. Say I wanted to add five menu items, each triggering its own tool. How would I do so? I believe I have to register a command and then add it to the menu item. But a quick search didn't bring me to a concrete example on how to do so. I found this topic but it still was unclear how to proceed and make a simple menu item with some custom commands.

The process is pretty straightforward and your initial finding in our documentation is actually the entry point.
Saving the code below in a .pyp file located in the folder where Cinema 4D looks for plugins should do the trick and show the following entry in the Cinema 4D top-menu bar.
9de7abca-12fd-4098-b719-da6b45c43f34-image.png

import c4d
from c4d import gui

PLUGIN1_ID = 999121031
PLUGIN2_ID = 999121032
PLUGIN3_ID = 999121033
PLUGIN4_ID = 999121034
PLUGIN5_ID = 999121035

class PC_12103_1(c4d.plugins.CommandData):
    def Execute(self, doc):
        print "Execute the 1st command"
        return True

class PC_12103_2(c4d.plugins.CommandData):
    def Execute(self, doc):
        print "Execute the 2nd command"
        return True

class PC_12103_3(c4d.plugins.CommandData):
    def Execute(self, doc):
        print "Execute the 3rd command"
        return True

class PC_12103_4(c4d.plugins.CommandData):
    def Execute(self, doc):
        print "Execute the 4th command"
        return True

class PC_12103_5(c4d.plugins.CommandData):
    def Execute(self, doc):
        print "Execute the 5th command"
        return True

def EnhanceMainMenu():
    mainMenu = gui.GetMenuResource("M_EDITOR")                    # Get main menu resource
    pluginsMenu = gui.SearchPluginMenuResource()                  # Get 'Plugins' main menu resource

    menu = c4d.BaseContainer()                                    # Create a container to hold a new menu information
    menu.InsData(c4d.MENURESOURCE_SUBTITLE, "Your Menu")          # Set the name of the menu
    menu.InsData(c4d.MENURESOURCE_COMMAND, "PLUGIN_CMD_999121031")# Add registered command identified by ID 999121031
    menu.InsData(c4d.MENURESOURCE_COMMAND, "PLUGIN_CMD_999121032")# Add registered command identified by ID 999121032
    menu.InsData(c4d.MENURESOURCE_COMMAND, "PLUGIN_CMD_999121033")# Add registered command identified by ID 999121033
    menu.InsData(c4d.MENURESOURCE_COMMAND, "PLUGIN_CMD_999121034")# Add registered command identified by ID 999121034
    menu.InsData(c4d.MENURESOURCE_COMMAND, "PLUGIN_CMD_999121035")# Add registered command identified by ID 999121035

    menu.InsData(c4d.MENURESOURCE_SEPERATOR, True);               # Add a separator
    menu.InsData(c4d.MENURESOURCE_COMMAND, "PLUGIN_CMD_5159")     # Add command 'Cube' with ID 5159 to the menu

    submenu = c4d.BaseContainer()                                 # Create a new submenu container
    submenu.InsData(c4d.MENURESOURCE_SUBTITLE, "Submenu")         # This is a submenu
    submenu.InsData(c4d.MENURESOURCE_COMMAND, "IDM_NEU")          # Add registered default command 'New Scene' to the menu
    submenu.InsData(c4d.MENURESOURCE_COMMAND, "IDM_SPEICHERN")    # Add registered default command 'Save' to the menu

    menu.InsData(c4d.MENURESOURCE_SUBMENU, submenu)               # Add the submenu

    if pluginsMenu:
        # Insert menu after 'Plugins' menu
        mainMenu.InsDataAfter(c4d.MENURESOURCE_STRING, menu, pluginsMenu)
    else:
        # Insert menu after the last existing menu ('Plugins' menu was not found)
        mainMenu.InsData(c4d.MENURESOURCE_STRING, menu)

def PluginMessage(id, data):
    if id==c4d.C4DPL_BUILDMENU:
        EnhanceMainMenu()


if __name__ == "__main__":
    c4d.plugins.RegisterCommandPlugin(PLUGIN1_ID, "1st Cmd", 0, None, "", PC_12103_1())
    c4d.plugins.RegisterCommandPlugin(PLUGIN2_ID, "2nd Cmd", 0, None, "", PC_12103_2())
    c4d.plugins.RegisterCommandPlugin(PLUGIN3_ID, "3rd Cmd", 0, None, "", PC_12103_3())
    c4d.plugins.RegisterCommandPlugin(PLUGIN4_ID, "4th Cmd", 0, None, "", PC_12103_4())
    c4d.plugins.RegisterCommandPlugin(PLUGIN5_ID, "5th Cmd", 0, None, "", PC_12103_5())
  1. Would I need a unique plug-in ID for each single command I'll register? Or can this be done differently?

Yes, that's the way Cinema 4D registers commands. You can generate your unique plugin IDs on this link.

Looking forward Avalon to be brought on Cinema, give best.
Riccardo

@BigRoy Hi and I am not to sure what you are asking for, but I hope this help you out, I didn't had time to type out a quick code but I hope these pics below help you.
Now from what I know, you have to make a command plugin with a ID to add to your tool to the custom menu.
So basically you need to make it a plugin so c4d can call or load your custom menu that is the (PluginMessage()) when the plugin is loaded. For scripts that's a no.

These are how my studio tools menu look in c4d :
2019-12-26_18-15-21.png
2019-12-26_19-05-52.png

This when you press V key button and the c4d pipe menu comes up :
2019-12-26_18-17-04.png

The code in your .pyp file :
2019-12-26_18-08-26.png
2019-12-26_18-12-52.png

Cheers! :+1: :grinning:
If you anymore questions free to contact me I don't mine helping in the best way I can.
Fackbook: https://www.facebook.com/ap.base.1
Email: [email protected]

Hi Bigroy, thanks for reaching out us.

Sorry for the answer taking longer than expected by holiday season didn't helped ;)

  1. Say I wanted to add five menu items, each triggering its own tool. How would I do so? I believe I have to register a command and then add it to the menu item. But a quick search didn't bring me to a concrete example on how to do so. I found this topic but it still was unclear how to proceed and make a simple menu item with some custom commands.

The process is pretty straightforward and your initial finding in our documentation is actually the entry point.
Saving the code below in a .pyp file located in the folder where Cinema 4D looks for plugins should do the trick and show the following entry in the Cinema 4D top-menu bar.
9de7abca-12fd-4098-b719-da6b45c43f34-image.png

import c4d
from c4d import gui

PLUGIN1_ID = 999121031
PLUGIN2_ID = 999121032
PLUGIN3_ID = 999121033
PLUGIN4_ID = 999121034
PLUGIN5_ID = 999121035

class PC_12103_1(c4d.plugins.CommandData):
    def Execute(self, doc):
        print "Execute the 1st command"
        return True

class PC_12103_2(c4d.plugins.CommandData):
    def Execute(self, doc):
        print "Execute the 2nd command"
        return True

class PC_12103_3(c4d.plugins.CommandData):
    def Execute(self, doc):
        print "Execute the 3rd command"
        return True

class PC_12103_4(c4d.plugins.CommandData):
    def Execute(self, doc):
        print "Execute the 4th command"
        return True

class PC_12103_5(c4d.plugins.CommandData):
    def Execute(self, doc):
        print "Execute the 5th command"
        return True

def EnhanceMainMenu():
    mainMenu = gui.GetMenuResource("M_EDITOR")                    # Get main menu resource
    pluginsMenu = gui.SearchPluginMenuResource()                  # Get 'Plugins' main menu resource

    menu = c4d.BaseContainer()                                    # Create a container to hold a new menu information
    menu.InsData(c4d.MENURESOURCE_SUBTITLE, "Your Menu")          # Set the name of the menu
    menu.InsData(c4d.MENURESOURCE_COMMAND, "PLUGIN_CMD_999121031")# Add registered command identified by ID 999121031
    menu.InsData(c4d.MENURESOURCE_COMMAND, "PLUGIN_CMD_999121032")# Add registered command identified by ID 999121032
    menu.InsData(c4d.MENURESOURCE_COMMAND, "PLUGIN_CMD_999121033")# Add registered command identified by ID 999121033
    menu.InsData(c4d.MENURESOURCE_COMMAND, "PLUGIN_CMD_999121034")# Add registered command identified by ID 999121034
    menu.InsData(c4d.MENURESOURCE_COMMAND, "PLUGIN_CMD_999121035")# Add registered command identified by ID 999121035

    menu.InsData(c4d.MENURESOURCE_SEPERATOR, True);               # Add a separator
    menu.InsData(c4d.MENURESOURCE_COMMAND, "PLUGIN_CMD_5159")     # Add command 'Cube' with ID 5159 to the menu

    submenu = c4d.BaseContainer()                                 # Create a new submenu container
    submenu.InsData(c4d.MENURESOURCE_SUBTITLE, "Submenu")         # This is a submenu
    submenu.InsData(c4d.MENURESOURCE_COMMAND, "IDM_NEU")          # Add registered default command 'New Scene' to the menu
    submenu.InsData(c4d.MENURESOURCE_COMMAND, "IDM_SPEICHERN")    # Add registered default command 'Save' to the menu

    menu.InsData(c4d.MENURESOURCE_SUBMENU, submenu)               # Add the submenu

    if pluginsMenu:
        # Insert menu after 'Plugins' menu
        mainMenu.InsDataAfter(c4d.MENURESOURCE_STRING, menu, pluginsMenu)
    else:
        # Insert menu after the last existing menu ('Plugins' menu was not found)
        mainMenu.InsData(c4d.MENURESOURCE_STRING, menu)

def PluginMessage(id, data):
    if id==c4d.C4DPL_BUILDMENU:
        EnhanceMainMenu()


if __name__ == "__main__":
    c4d.plugins.RegisterCommandPlugin(PLUGIN1_ID, "1st Cmd", 0, None, "", PC_12103_1())
    c4d.plugins.RegisterCommandPlugin(PLUGIN2_ID, "2nd Cmd", 0, None, "", PC_12103_2())
    c4d.plugins.RegisterCommandPlugin(PLUGIN3_ID, "3rd Cmd", 0, None, "", PC_12103_3())
    c4d.plugins.RegisterCommandPlugin(PLUGIN4_ID, "4th Cmd", 0, None, "", PC_12103_4())
    c4d.plugins.RegisterCommandPlugin(PLUGIN5_ID, "5th Cmd", 0, None, "", PC_12103_5())
  1. Would I need a unique plug-in ID for each single command I'll register? Or can this be done differently?

Yes, that's the way Cinema 4D registers commands. You can generate your unique plugin IDs on this link.

Looking forward Avalon to be brought on Cinema, give best.
Riccardo

@r_gigante Thanks, the command examples were exactly what I needed. And someone stating explicitly that I needed to create a command per menu entry made me help realize that too.

I now have an Avalon menu - perfect.

Bug with C4D and PYTHONPATH

I did hit a C4D bug as I started setting up the pipeline integration, whenever there's an empty value in the list on PYTHONPATH then Python fails to run correctly in Cinema4D. More details here

Icons. Are they .tiff only?

I've tried to add an icon to the menu but I failed there. I tried to pass it a .svg icon like so:

bitmap = c4d.bitmaps.BaseBitmap()
bitmap.InitWith(path)

And then providing that to c4d.plugins.RegisterCommandPlugin as the 4th argument (where there's None in your example). However, the menu entry showed no icon.
Should this work?


Opening files in C4D with Python (resolved)

I should be separating this out into a new Topic I suppose. But any pointers on how to save/open scenes in C4D Python?

I've tried this:

import os
import c4d


def file_extensions():
    return [".c4d"]
    
    
def _document():
    return c4d.documents.GetActiveDocument()


def has_unsaved_changes():
    doc = _document()
    if doc:
        return doc.GetChanged()


def save_file(filepath):
    doc = _document()
    if doc:
            return c4d.documents.SaveDocument(doc, 
                                              filepath, 
                                              c4d.SAVEDOCUMENTFLAGS_NONE,
                                              c4d.FORMAT_C4DEXPORT)


def open_file(filepath):
    doc = c4d.documents.LoadDocument(filepath, c4d.SCENEFILTER_0, None)
    if doc:
        c4d.documents.SetActiveDocument(doc)
    return doc


def current_file():
    doc = _document()
    if doc:
        root = doc.GetDocumentPath()
        fname = doc.GetDocumentName()
        if root and fname:
            return os.path.join(root, fname)

But whenever I open_file a document and set it active, then all functionality in C4D's UIs gets disabled and greyed out. Am I misinterpreting how this should work? The other code seems to do exactly what I need.

Edit I should have used c4d.documents.LoadFile for opening a file - that does what I need.

However, it kept failing with:

Traceback (most recent call last):
  File "C:\Users\Roy\AppData\Roaming\Maxon\Maxon Cinema 4D R21_64C2B3BD\library\scripts\untitled.py", line 16, in <module>
doc = c4d.documents.LoadFile(path)
TypeError:unable to convert unicode to @net.maxon.interface.url-C

This was due to the filepath being unicode. That's fixed by forcing it to str using str(path).

@BigRoy said in Custom menu in C4D top menu bar with custom Python commands:

Bug with C4D and PYTHONPATH
I did hit a C4D bug as I started setting up the pipeline integration, whenever there's an empty value in the list on PYTHONPATH then Python fails to run correctly in Cinema4D. More details here

Nice find. This is a bug or maybe a limitation. :)

I've tried to add an icon to the menu but I failed there. I tried to pass it a .svg icon like so:

Yea, that won't work. SVG is not supported. Use TIFF instead (others are possible).

However, it kept failing with:
Traceback (most recent call last):

File "C:\Users\Roy\AppData\Roaming\Maxon\Maxon Cinema 4D R21_64C2B3BD\library\scripts\untitled.py", line 16, in <module>

doc = c4d.documents.LoadFile(path)

TypeError:unable to convert unicode to @net.maxon.interface.url-C

This is most likely because you didn't decode it as UTF-8 when giving it to a python-function. (Assuming you used LoadDialog or similar c4d-specific function).
Python uses unicode, Cinema 4D's default coding is utf8.

@BigRoy In the future, I invite you to open a new topic for new questions no related. This way it helps other readers to understand and find what they need.
It also helps to not pollute a topic with multiple topics.

@BigRoy said in Custom menu in C4D top menu bar with custom Python commands:

Bug with C4D and PYTHONPATH

I did hit a C4D bug as I started setting up the pipeline integration, whenever there's an empty value in the list on PYTHONPATH then Python fails to run correctly in Cinema4D. More details here

Thanks for reporting it, it will be fixed in the next Cinema 4D update.

@BigRoy said in Custom menu in C4D top menu bar with custom Python commands:

Icons. Are they .tiff only?

I've tried to add an icon to the menu but I failed there. I tried to pass it a .svg icon like so:

bitmap = c4d.bitmaps.BaseBitmap()
bitmap.InitWith(path)

SVG is currently not supported so either use tiff, png, jpg whatever else is supported by Cinema 4D.
If you have further questions about it, please open a new topic and don't continue here.

Opening files in C4D with Python (resolved)

I should be separating this out into a new Topic I suppose. But any pointers on how to save/open scenes in C4D Python?

I've tried this:

import os
import c4d


def file_extensions():
    return [".c4d"]
    
    
def _document():
    return c4d.documents.GetActiveDocument()


def has_unsaved_changes():
    doc = _document()
    if doc:
        return doc.GetChanged()


def save_file(filepath):
    doc = _document()
    if doc:
            return c4d.documents.SaveDocument(doc, 
                                              filepath, 
                                              c4d.SAVEDOCUMENTFLAGS_NONE,
                                              c4d.FORMAT_C4DEXPORT)


def open_file(filepath):
    doc = c4d.documents.LoadDocument(filepath, c4d.SCENEFILTER_0, None)
    if doc:
        c4d.documents.SetActiveDocument(doc)
    return doc


def current_file():
    doc = _document()
    if doc:
        root = doc.GetDocumentPath()
        fname = doc.GetDocumentName()
        if root and fname:
            return os.path.join(root, fname)

But whenever I open_file a document and set it active, then all functionality in C4D's UIs gets disabled and greyed out.
Am I misinterpreting how this should work? The other code seems to do exactly what I need.

If LoadFile works for you it's nice, but I would like to confirm with you since normally LoadDocument should work.
So when you said a document, it's a Cinema 4D document (a *.c4d file)?
If that's the LoadDocument let you load a document but you need to tell what to load e.g. Object, or Material?
Then if you want to expose this document to the user, you need to insert it first with c4d.documents.InsertBaseDocument.

# Loads a documents with objects and material in memory
newDoc = c4d.documents.LoadDocuments(filePath, c4d.SCENEFILTER_OBJECTS | c4d.SCENEFILTER_MATERIALS, None)
if newDoc is None:
    raise RuntimeError("Failed to load the document")
	
# Exposes it to the user and set it as active
c4d.documents.InsertBaseDocument(newDoc)
c4d.documents.SetActiveDocument(newDoc)

c4d.EventAdd()

If you have further questions about it, please open a new topic and don't continue here.

And as @mp5gosu pointed, Cinema 4D Classic API expects a string and not Unicode.
Cheers,
Maxime.

The issue about PYTHONPATH is now resolved in R21 Sp2.

Cheers,
Maxime