James Dunlop Homepage



om2-NodeEditorMenuManger

INFO: Used to mange nodeEditor right click menus

USAGE: Please see the commented code at the bottom for usage

LAST UPDATED: 02-19-2019 - More than 1 subMenu

Disclaimer:
  • All code here is provided as is without support.
  • Use at your own risk! These posts assume you have some knowledge of import/running python script in maya

  • ! Updated for submenus !

    GitHub Repo WIP Code

    So the nodeEditor in Maya can register commands on Nodes using customInclusiveNodeItemMenuCallbacks: Part01

    The problem I found with this is it’s a bit hellish to manage (especially when developing). So this is a little post for about me mashing together an approach to facilitate managing adding commands to the nodeEditor via a little menuManager of sorts. Why? Well reloading these menus over and over does not remove the previous load!

    So you have to remove them before the reload. Having a little manager to keep track of what is loaded etc can be pretty helpful with this.

    The idea

    The idea here is to be able to add as many commands as I like to my code base without having to maintain the manager `seeing’ this code. To do this I’m going to add a little factory module in to scan for a specific class in a path, and from that create a cache of all the commands I want to create menus for. Then the manager itself leverages that cache to add the menus to the NodeEditor in Maya.

    defaultMenus
    customMenus

    The Factory -Menu Cache

    import os
    import importlib, inspect
    import logging
    logging.basicConfig()
    logger = logging.getLogger(__name__)
    
    BASE = os.path.dirname(__file__)
    MENUSPATH = "{}/menus".format(BASE)
    MENUCACHE = {}
    
    
    def createMenuCache(path=MENUSPATH, pkg="menus"):
        """
        Recurisvely fetch all the .py modules in the menus folder and any
        classes defined as a menu and add these to the cache
    
    
        :param path: `str` path to the root menus folder
        :param pkg: `str` .separated path for the importlib.import_module to use
        """
        for module in os.listdir(path):
            if module == '__init__.py' or module.endswith(".pyc") or module == "base.py":
                continue
    
            if module.endswith(".py"):
                mod = importlib.import_module(name=".{}".format(module[:-3]), package=pkg)
                for eachMenu in inspect.getmembers(mod, inspect.isclass):
                    if not eachMenu[1].ISSUBMENU:
                        menu = eachMenu[1]()
                        if menu.menufunction() is not None:
                            MENUCACHE[menu.id()] = menu
    
                        if menu.hasSubMenu():
                            for eachChild in menu.subMenus():
                                MENUCACHE[eachChild.id()] = eachChild
    
            else:
                createMenuCache(path="{}/{}".format(path, module), pkg="{}.{}".format(pkg, module))

    What this is doing is setting the base path to /menus relative to the current filepath the factory.py lives in. Scan subfolders for the .py files and looks to import the class(es) in each file. Now this works because I have dictated a specfic structure for each command to uphold.

    Assumption(s):

    Menu base is the base menu class used by the neMenuManager to create the menus as expected in Maya.

    from maya import cmds
    import logging
    logging.basicConfig()
    logger = logging.getLogger(__name__)
    
    
    class MenuBase(object):
        ID = None
        MENUNAME = None
        NODENAME = None
        FUNCTION = None
        ISSUBMENU = False
        SUBMENUS = list()
    
        def __init__(self, isRadial=False, radialPos="", hasSubMenu=False, lastSubMenu=False):
            self.__isRadial = isRadial
            self.__radialPos = radialPos
            self.__func = None
            self.__hasSubMenu = hasSubMenu
            self.__lastSubMenu = lastSubMenu
            # Set the cmd instance on init we want to create only ONE instance of the menuCmd.
            # So well force that now and reuse it thereafter
            self.menufunction()
    
        def id(self):
            return self.ID
    
        def name(self):
            return self.MENUNAME
    
        def nodeType(self):
            return self.NODENAME
    
        def isRadial(self):
            return self.__isRadial
    
        def radialPos(self):
            return self.__radialPos
    
        def createMenuItem(self):
            if self.FUNCTION is None:
                return
    
            if self.isRadial():
                self._menuItem = cmds.menuItem(label=self.MENUNAME, c=self.FUNCTION,
                                               subMenu=self.hasSubMenu(),
                                               radialPosition=self.radialPos(),
                                               )
                if self.ISSUBMENU and self.__lastSubMenu:
                    # Reset the maya internal parent so we don't end up
                    # with all subsequent menus parented under this one!!
                    cmds.setParent("..", menu=True)
            else:
                self._menuItem = cmds.menuItem(label=self.MENUNAME, c=self.FUNCTION,
                                               subMenu=self.hasSubMenu(),
                                               )
                if self.ISSUBMENU and self.__lastSubMenu:
                    # Reset the maya internal parent so we don't end up
                    # with all subsequent menus parented under this one!!
                    cmds.setParent("..", menu=True)
            return self._menuItem
    
        def hasSubMenu(self):
            return self.__hasSubMenu
    
        def subMenus(self):
            return self.SUBMENUS
    
        def subMenus(self):
            return self.SUBMENUS
    
        def menufunction(self):
            if self.FUNCTION is None:
                return
    
            if self.__func is None:
                if self.NODENAME is not None:
                    # Create the menu command for the node we have rightClicked over in Maya
                    def menuCmd(ned, node):
                        if cmds.nodeType(node) == self.NODENAME:
                            self.createMenuItem()
                            return True
                        else:
                            return False
                else:
                    # Add a general menu to all nodes. Take care with Radial positions here as you might clash with a node
                    # based menu item!
                    def menuCmd(ned, node):
                        self.createMenuItem()
                        return True
    
                self.__func = menuCmd
                return menuCmd
            else:
                return self.__func

    Creating a menu.py

    So inheriting / overloading this class is then fairly straight forward for me to use when adding a new menu item to the nodeEditor. eg: gitHub example
    If you check the latest version of the file you can see each menu gets a unique typeID, Name, and a function to call.

    Note; No args are handled atm. And each class has a related mayaNode that they are linked to when you invoke a menu call in maya by right clicking over a node in the nodeEditor

    The Manager

    From there The manager only needs to leverage the MENUCACHE to generate all the menus and provide a way to handle this information eg: adding/removing menus from the nodeEditor as expected.

    from maya.app.general import nodeEditorMenus
    import menuFactory as ne_factory
    import logging
    logging.basicConfig()
    logger = logging.getLogger(__name__)
    """
    usage:
    import sys
    path = "T://software//neMenuManager"
    if path not in sys.path:
        sys.path.append(path)
    
    import neMenuManager as neMM
    nedMenuManager = neMM.NodeEditorMenuManager(autoLoadMenus=True, reload=False)
    for id, e in nedMenuManager.iterMenuItems():
        print(id)
    """
    
    
    class NodeEditorMenuManager(object):
        def __init__(self, autoLoadMenus=True, reload=False):
            self.menus = nodeEditorMenus.customInclusiveNodeItemMenuCallbacks
            self._ids = []
            if reload:
                logger.warning("Resetting ne_factory.MENUCACHE now!")
                ne_factory.MENUCACHE = {}
    
            if ne_factory.MENUCACHE:
                # We should REMOVE any previously appeded functions in maya or we end up with duplicates!
                self._ids = ne_factory.MENUCACHE.keys()
                self.removeAll()
            else:
                logger.warning("Creating ne_factory.MENUCACHE now!")
                ne_factory.createMenuCache()
    
            if autoLoadMenus:
                # Maya sucks at editing the menus, so we're going to iter twice. First for the mainMenu items
                # Then again for anything flagged as a subMenu of something else.
                # This means we only EVER go 1 level deep menu|subMenu NOT menu|menu|subMenu
    
                for id, menu in ne_factory.MENUCACHE.iteritems():
                    self.addMenu(menu=menu)
                    self._ids.append(id)
    
        def addMenu(self, menu):
            """
            :param menu: `MenuBase` instance
            """
            if menu.menufunction() is not None:
                self.menus.append(menu.menufunction())
                logger.info("Added: {} id:{} func: {}".format(menu.name(), menu.id(), menu.menufunction()))
    
        def iterMenuItems(self):
            """
            :return: `int` `MenuBase() Instance`
            """
            for id, data in ne_factory.MENUCACHE.iteritems():
                yield id, data
    
        def removeMenu(self, menuid):
            """
            :param menuid: `int`
            :return: `bool`
            """
            if menuid in ne_factory.MENUCACHE.keys():
                menu = ne_factory.MENUCACHE[menuid]
                try:
                    self.menus.remove(menu.menufunction())
                except ValueError:
                    logger.warning("Failed to remove {} from maya's internal callback list!".format(menu.name()))
                    return False
    
                ne_factory.MENUCACHE.pop(menuid, None)
                self._ids.remove(menuid)
    
                logger.info("Successfully removed menu {}".format(menu.name()))
                return True
    
            return False
    
        def removeAll(self):
            """Remove all the menus before a reload!"""
            while self._ids:
                for eachID in self._ids:
                    self.removeMenu(menuid=eachID)
    
        def currentCache(self):
            return ne_factory.MENUCACHE
    
        def __repr__(self):
            str = "menuItems:\n"
            for k, v in self.iterMenuItems():
                str += "\t id: {}".format(k)
                str += "\t name: {}".format(v.name())
                str += "\t nodeType: {}".format(v.nodeType())
                str += "\t isradial: {}".format(v.isRadial())
                str += "\t pos: {}".format(v.radialPos())
                str += "\t func: {}\n".format(v.menufunction())
    
            return str