# -= ml_toolbox.py =- # __ by Morgan Loomis # ____ ___ / / http://morganloomis.com # / __ `__ \/ / Revision 6 # / / / / / / / 2018-02-17 # /_/ /_/ /_/_/ _________ # /_________/ # # ______________ # - -/__ License __/- - - - - - - - - - - - - - - - - - - - - - - - - - - - - # # Copyright 2018 Morgan Loomis # # Permission is hereby granted, free of charge, to any person obtaining a copy of # this software and associated documentation files (the "Software"), to deal in # the Software without restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the # Software, and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # # ___________________ # - -/__ Installation __/- - - - - - - - - - - - - - - - - - - - - - - - - - # # Copy this file into your maya scripts directory, for example: # C:/Documents and Settings/user/My Documents/maya/scripts/ml_toolbox.py # # Run the tool in a python shell or shelf button by importing the module, # and then calling the primary function: # # import ml_toolbox # ml_toolbox.main() # # # __________________ # - -/__ Description __/- - - - - - - - - - - - - - - - - - - - - - - - - - - # # Create custom menus in Maya's main menu bar for organizing scripts, just by # putting them into directories. Insert tools into the existing maya menus, or add # entirely new menus. # # ____________ # - -/__ Usage __/- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # # Make sure the ml_toolbox_menu folder is in your scripts directory or some other # python path. Run the command mentioned above, or for best results, add the # command to your scripts/userSetup.mel file, like so: python('import # ml_toolbox;ml_toolbox.main()'); To add a script to the menu, create folders and # sub-folders in the ml_toolbox_menu directory. If you add a folder that is named # the same as a maya menu, the tools in that folder will be added to the maya # menu, rather than a new menu. For MEL scripts to work, the main global proc # needs to be the same name as the file. For python scripts, the main function # needs to be "main" or "ui" # # _______________ # - -/__ Advanced __/- - - - - - - - - - - - - - - - - - - - - - - - - - - - # # If you have large menus and you don't like all the printing (if it's slowing # startup), you can remove the verbose flag from Toolbox call in the main() # function to suppress it. For menu matching diagnostics, set environment variable # ML_TOOLBOX_INVESTIGATE=1 (or pass verbose=True to toolbox()) or call # investigate_main_window_menus(menus_path). For python scripts, there's a few extra things you can # do to get the most out of this tool. If you want any additional functions to be # searched for besides main() etc, add them to the functionList variable below. # They are sorted in search order. You can add an optional insertAfter variable to # a script to have its menu item inserted after an existing menuItem in a maya # menu. See the bundled scripts for examples of this. If you'd like to # automatically set up a hotkey for a tool, add a hotkey variable to script file # with the value set to the modifier+hotkey button. (See the bundled # create/snapLocator for an example of this.) # # # __________ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /_ Enjoy! _/- - - __author__ = 'Morgan Loomis' __license__ = 'MIT' __category__ = 'None' __revision__ = 6 import maya.cmds as mc import maya.mel as mm import os, re, sys, imp, posixpath from collections import defaultdict from maya import OpenMaya MENU_ITEM_PREFIX = '*' #change this if you want a different prefix for custom menu SUB_MENU_PREFIX = '> ' #change this if you want a different prefix for custom menu MAIN_MENU_NAME_PREFIX = 'ml_mainWindowMenu_' #These are the functions that this script will look for when building the menu, in this order. #Add to this list if you want to support scripts with a different primary function. functionList = ['main','ui','drag'] def main(): try: import ml_toolbox_menu except: raise ImportError toolbox(ml_toolbox_menu) def toolbox(module, verbose=True): if is_batch_mode(): print('Skipping menu creation in batch mode.') return tb = Toolbox(module, verbose=verbose) tb.createMainMenus() def customMenu(module, name=None, verbose=True): if is_batch_mode(): print('Skipping menu creation in batch mode.') return tb = Toolbox(module, verbose=verbose) tb.createCustomMenu(module.__path__[0], label=name, mainMenu=True) def is_batch_mode(): return os.environ.get('MAYA_BATCH', '0') != '0' def format_label_key_current(label): ''' Match key used when pairing Maya menu labels to top-level ml_toolbox_menu folders. Exposed for investigation output; behavior matches createMainMenus / appendMayaMenu. ''' if not label: return '' return label.replace(' ', '').lower() def resolve_menu_ui_for_duplicate_label(ui_names_ordered): ''' Maya sometimes exposes more than one main-bar menu with the same -label (e.g. Deform in different menu sets). Pick one UI name: prefer objects whose name contains "Rig" (e.g. mainRigDeformationsMenu over mainDeformMenu). ui_names_ordered: bar order, first wins when heuristics tie. ''' if not ui_names_ordered: return None if len(ui_names_ordered) == 1: return ui_names_ordered[0] lower = [(n, n.lower()) for n in ui_names_ordered] with_rig = [n for n, low in lower if 'rig' in low] if len(with_rig) == 1: return with_rig[0] if len(with_rig) > 1: with_deform = [n for n in with_rig if 'deform' in n.lower()] if len(with_deform) == 1: return with_deform[0] if with_deform: return sorted(with_deform)[0] return sorted(with_rig)[0] return ui_names_ordered[0] def build_main_menus_label_map(main_window_menus): ''' Map normalized label key -> chosen Maya menu UI name for MayaWindow menus. Collides duplicate labels via resolve_menu_ui_for_duplicate_label. ''' buckets = defaultdict(list) for name in main_window_menus: label = mc.menu(name, query=True, label=True) k = format_label_key_current(label) buckets[k].append(name) return {k: resolve_menu_ui_for_duplicate_label(names) for k, names in buckets.items()} def investigate_main_window_menus(menus_path=None): ''' Print Maya main-window menus and optional folder↔key alignment (Script Editor helper). menus_path: e.g. ml_toolbox_menu.__path__[0] — if omitted, only Maya menus are listed. ''' if is_batch_mode(): print('ml_toolbox.investigate_main_window_menus: skipped in batch mode.') return main_window_menus = mc.window('MayaWindow', query=True, menuArray=True) if not main_window_menus: print('ml_toolbox.investigate_main_window_menus: no menus under MayaWindow.') return print('\n[ml_toolbox investigate] MayaWindow menus (ui name, label, match key):') by_key = {} for ui_name in main_window_menus: lb = mc.menu(ui_name, query=True, label=True) key = format_label_key_current(lb) by_key.setdefault(key, []).append(ui_name) print(' {!r} label={!r} key={!r}'.format(ui_name, lb, key)) main_menus = build_main_menus_label_map(main_window_menus) for key, names in sorted(by_key.items()): if len(names) > 1: print(' [duplicate label key {!r}] candidates={} -> using {!r}'.format( key, names, main_menus.get(key))) if not menus_path: print('[ml_toolbox investigate] Pass menus_path=ml_toolbox_menu.__path__[0] to check folder keys.\n') return print('\n[ml_toolbox investigate] Top-level folders under {!r}:'.format(menus_path)) for folder in sorted(os.listdir(menus_path)): if folder.startswith('.') or folder.startswith('__'): continue tool_dir = posixpath.join(menus_path, folder) if not os.path.isdir(tool_dir): continue menu_label = labelFromPath(tool_dir) key = format_label_key_current(menu_label) matched = key in main_menus and not main_menus[key].startswith(MAIN_MENU_NAME_PREFIX) if matched: print(' folder={!r} menuLabel={!r} key={!r} -> Maya menu {!r}'.format( folder, menu_label, key, main_menus[key])) else: print(' folder={!r} menuLabel={!r} key={!r} -> NO match (would add custom top-level menu)'.format( folder, menu_label, key)) print('') def labelFromPath(path): ''' generate a label based on a path ''' pattern = re.compile('((?=[A-Z][a-z])|(?<=[a-z])(?=[A-Z]))') label = os.path.basename(path) if '.' in label: label = os.path.splitext(label)[0] if label.startswith('ml_'): label = label[3:] label = pattern.sub(' ', label) label = label.replace('_', ' ') label = label.title() return label def printMenu(menuLabel, depth=0): print(depth*'\t'+' _'+(len(menuLabel)-1)*'_') print(depth*'\t'+'_|'+menuLabel+'|'+(20-len(menuLabel))*'_') class Toolbox(object): def __init__(self, module, verbose=False): self.rootModule = module self.namespace = self.rootModule.__name__ self.menusPath = self.rootModule.__path__[0].replace('\\','/') self.gMainWindow = 'MayaWindow' self.tools = [] self.hasHotkeys = [] self.mayaMenus = [] self.customMenus = {} self.verbose = verbose def createMainMenus(self): #get all the menus that are children of the main menu mainWindowMenus = mc.window(self.gMainWindow, query=True, menuArray=True) if not mainWindowMenus: return #get the label for each of the menus #this will be matched against tool directories (duplicate labels resolved below) self.mainMenus = build_main_menus_label_map(mainWindowMenus) investigate = self.verbose or (os.environ.get('ML_TOOLBOX_INVESTIGATE', '').strip() not in ('', '0', 'false', 'False')) if investigate: print('\n[ml_toolbox investigate] MayaWindow menus (ui, label, match key):') by_key = {} for name in mainWindowMenus: lb = mc.menu(name, query=True, label=True) k = format_label_key_current(lb) by_key.setdefault(k, []).append(name) print(' {!r} label={!r} key={!r}'.format(name, lb, k)) for key, names in sorted(by_key.items()): if len(names) > 1: print(' [duplicate label key {!r}] candidates={} -> using {!r}'.format( key, names, self.mainMenus.get(key))) mayaMenuDirectories = [] customMenuDirectories = [] for folder in os.listdir(self.menusPath): if folder.startswith('.'): continue if folder.startswith('__'): continue toolDirectory = posixpath.join(self.menusPath,folder) #only directories for this first level if not os.path.isdir(toolDirectory): if not folder.startswith('__init__') and self.verbose: print('Root level file being ignored, move this to a sub-directory: {}'.format(toolDirectory)) continue menuLabel = labelFromPath(toolDirectory) formatLabel = format_label_key_current(menuLabel) if formatLabel in self.mainMenus and not self.mainMenus[formatLabel].startswith(MAIN_MENU_NAME_PREFIX): #maya menus mayaMenuDirectories.append(toolDirectory) else: #custom menu customMenuDirectories.append(toolDirectory) if investigate: matched = toolDirectory in mayaMenuDirectories if matched: print(' [folder] {!r} menuLabel={!r} key={!r} -> merge into Maya menu {!r}'.format( folder, menuLabel, formatLabel, self.mainMenus[formatLabel])) else: print(' [folder] {!r} menuLabel={!r} key={!r} -> custom top-level (no Maya key match)'.format( folder, menuLabel, formatLabel)) if mayaMenuDirectories: for d in mayaMenuDirectories: self.appendMayaMenu(d) if customMenuDirectories: for d in customMenuDirectories: self.createCustomMenu(d, parent=self.gMainWindow, mainMenu=True) self.setHotkeys() def classifyTool(self, tool): #initialize tool self.tools.append(tool) if tool.isPython: #python only if hasattr(tool,'hotkey'): self.hasHotkeys.append(tool) def appendMayaMenu(self, path): ''' Add tools to the maya menus ''' menuLabel = labelFromPath(path) formatLabel = format_label_key_current(menuLabel) maya_menu = self.mainMenus[formatLabel] menuItemArray = mc.menu(maya_menu, query=True, itemArray=True) investigate = self.verbose or (os.environ.get('ML_TOOLBOX_INVESTIGATE', '').strip() not in ('', '0', 'false', 'False')) def _item_count(arr): return len(arr) if arr else 0 if investigate: print('\n[ml_toolbox investigate] appendMayaMenu path={!r} menuLabel={!r} maya_menu={!r}'.format( path, menuLabel, maya_menu)) print(' itemArray count (before postMenuCommand): {}'.format(_item_count(menuItemArray))) #if this menu hasn't been built yet, run the post menu command to build it #that took a long time to figure out. if not menuItemArray: if self.verbose: print('pre-building menu: {}'.format(menuLabel)) pmc = mc.menu(maya_menu, query=True, postMenuCommand=True) if investigate: print(' postMenuCommand: {!r}'.format(pmc)) if pmc: mm.eval(pmc) menuItemArray = mc.menu(maya_menu, query=True, itemArray=True) if investigate: print(' itemArray count (after postMenuCommand): {}'.format(_item_count(menuItemArray))) if _item_count(menuItemArray) == 0: print(' NOTE: still empty — items may be created only when the menu is opened in the UI,') print(' or postMenuCommand did not populate queryable items.') #get all the menu items in the menu menuItems = {} for each in menuItemArray: eachLabel = mc.menuItem(each, query=True, label=True) menuItems[eachLabel] = each #subMenu subDirs = [posixpath.join(path,x) for x in os.listdir(path) if os.path.isdir(posixpath.join(path,x))] if subDirs: for each in subDirs: self.createCustomMenu(each, parent=maya_menu, label=labelFromPath(each), depth=1) subItems = [posixpath.join(path,x) for x in os.listdir(path) if (x.endswith('.py') or x.endswith('.mel')) and x != '__init__.py'] if subItems: for path in subItems: tool = Tool(path, self.namespace) self.classifyTool(tool) if not tool.errors: tool.createMenuItem(parent=maya_menu, labelPrefix=MENU_ITEM_PREFIX+' ', italicized=True) def createCustomMenu(self, path, parent=None, label=None, depth=0, mainMenu=False): ''' Recurse through a tool directory, creating tools and menus ''' if not os.path.isdir(path): return if os.path.split(path)[-1].startswith('_'): return if not parent: parent = self.gMainWindow if not parent: return # recursive subMenu subItems = os.listdir(path) #if there's stuff in the folder, create a menu item if not subItems: return #if there are python files in this directory, but no __init__ file, create one. if not '__init__.py' in subItems and [x for x in subItems if x.endswith('.py')]: print('Creating required __init__.py file in {}'.format(path)) with open(os.path.join(path,'__init__.py'), 'w') as f: f.write('#Generated by ml_toolbox') subItems = sorted(subItems) #do directories first, then files #generate menu name menuPrefix = 'mlSubMenu' if mainMenu: menuPrefix = MAIN_MENU_NAME_PREFIX menuName = os.path.basename(path) menuName = menuPrefix+menuName if mc.menu(menuName, exists=True): mc.deleteUI(menuName) #generate menu label if not label: label = labelFromPath(path) if self.verbose: printMenu(label,depth) #create the menu if mainMenu: menuName = mc.menu(menuName, parent=parent, to=True, label=label, allowOptionBoxes=True) else: menuName = mc.menuItem(menuName, label=SUB_MENU_PREFIX+label, subMenu=True, parent=parent, allowOptionBoxes=True, italicized=True) #and recurse! #do directories first filePaths = list() for each in subItems: eachPath = posixpath.join(path,each) if os.path.isdir(eachPath): if each.startswith('_'): continue #if its a directory, recurse self.createCustomMenu(eachPath, parent=menuName, depth=depth+1) else: filePaths.append(eachPath) #now go through the files for each in filePaths: if not each.endswith('__init__.py') and ((each.endswith('.py') or each.endswith('.mel'))): tool = Tool(each, self.namespace, depth=depth) self.classifyTool(tool) if not tool.errors: if self.verbose: print((depth+1)*'\t'+tool.label) tool.createMenuItem(parent=menuName, italicized=False) elif self.verbose: print((depth+1)*'\t'+tool.label+' <-- has errors and was unable to be imported.') mc.setParent('..', menu=True) def setHotkeys(self): ''' Set hotkeys for all the tools ''' if not self.hasHotkeys: return doPrint = True for each in self.hasHotkeys: if each.hotkey: if doPrint and self.verbose: print() print('Set Hotkeys') doPrint = False each.hotkey.create(self.verbose) class Tool(object): def __init__(self, path, namespace=None, depth=0, verbose=False): self.path = os.path.normpath(path).replace('\\','/') self.namespace = namespace #this doesn't allow period in name self.name = os.path.basename(self.path).split('.')[0] self.label = labelFromPath(self.path) self.depth = depth self.errors = False self.verbose = verbose self.module = None self.hotkey = None self.markingMenu = None self.command = None self.isPython = False self.isMel = False if self.path.endswith('.py'): self.initPython() elif self.path.endswith('.mel'): self.initMel() else: self.errors = True if self.module and self.isPython: #initialize hotkeys and markingMenus, python only hotkey = self.Hotkey(self.module, self.command, self.name) if hotkey.keys: self.hotkey = hotkey def initPython(self): self.isPython = True #check for __init__ file, create one if there isn't any in this directory directory = os.path.dirname(self.path) initPath = posixpath.join(directory,'__init__.py') if not os.path.exists(initPath): print('Creating required __init__.py file: {}'.format(initPath)) with open(initPath, 'w') as f: f.write('#Generated by ml_toolbox') fromRoot = self.path.rsplit(self.namespace.replace('.','/'), 1)[-1] #fromRoot = self.path.replace(self.rootPath,'') noExt = fromRoot.split('.')[0] dotPath = noExt.replace('/','.') self.moduleName = self.namespace+dotPath #try to import the module, catch ones with errors try: __import__(self.moduleName) self.module = sys.modules[self.moduleName] #find the main command to use, otherwise skip for f in functionList: if f in self.module.__dict__: self.command = 'import '+self.module.__name__+';'+self.module.__name__+'.'+f+'()' break else: self.command = 'from maya import OpenMaya;OpenMaya.MGlobal.displayWarning("No command found, make sure this tool as a main() function or that its primary function is in the functionList variable in ml_toolbox.py")' except Exception as err: if self.verbose: print(err) print('!! '+((self.depth-1)*'\t')+self.label+' < -- Errors found.') self.errors = True def initMel(self): self.isMel = True self.command = 'import maya.mel;maya.mel.eval("source \\\"'+self.path+'\\\";'+self.name+'")' def createMenuItem(self, parent=None, labelPrefix='', italicized=False): if self.isPython: menuName = 'mlMenu_'+self.module.__name__.replace('.','_') else: menuName = 'mlMenu_'+self.name #Create the label and print the tool label = labelPrefix+self.label #keyword args for the menu command kwargs = {'italicized':italicized} if self.hotkey: if len(self.hotkey.keys) == 1: kwargs['altModifier'] = self.hotkey.altModifier[0] kwargs['ctrlModifier'] = self.hotkey.ctrlModifier[0] if self.hotkey.keys[0].isupper(): kwargs['shiftModifier'] = True kwargs['keyEquivalent'] = self.hotkey.keys[0] if self.verbose: print(self.depth*'\t'+label) if mc.menuItem(menuName, exists=True): mc.deleteUI(menuName) if self.isPython and hasattr(self.module,'insertAfter'): menuItemArray = mc.menu(parent, query=True, itemArray=True) if menuItemArray: menuItems = dict() for each in menuItemArray: eachLabel = mc.menuItem(each, query=True, label=True) menuItems[eachLabel] = each if self.module.insertAfter in menuItems: kwargs['insertAfter'] = menuItems[self.module.insertAfter] mc.setParent(parent, menu=True) menuName = mc.menuItem(menuName, label=label, command=self.command, **kwargs) class Hotkey(object): def __init__(self, module, command, name): self.keys = list() self.altModifier = list() self.ctrlModifier = list() self.commands = list() self.functions = list() self.name = name #check that the module has a hotkey attribute if not hasattr(module,'hotkey'): return hotkeys = list() commands = list() if isinstance(module.hotkey, str): hotkeys.append(module.hotkey) commands.append(command) self.functions.append('main') elif isinstance(module.hotkey, dict): hotkeys = list(module.hotkey.keys()) for h in hotkeys: func = module.hotkey[h].split('(')[0] self.functions.append(func) commands.append('import '+module.__name__+';'+module.__name__+'.'+module.hotkey[h]) for i, (h,c) in enumerate(zip(hotkeys,commands)): self.keys.append(None) self.altModifier.append(False) self.ctrlModifier.append(False) self.commands.append(c) hotkeySplit = h.split('+') if len(hotkeySplit) == 1: self.keys[i] = hotkeySplit[0] else: for each in hotkeySplit: low = each.lower() if low == 'ctrl' or low == 'control' or low == 'cmd' or low == 'command': self.ctrlModifier[i] = True if low == 'alt' or low == 'alternate': self.altModifier[i] = True self.keys[i] = hotkeySplit[-1] def create(self, verbose=False): if not self.keys: return for i,k in enumerate(self.keys): name = self.name+'.'+self.functions[i] if verbose: hotkeyPrint = '\t' if self.altModifier[i]: hotkeyPrint+='[Alt]+' if self.ctrlModifier[i]: hotkeyPrint+='[Ctrl]+' hotkeyPrint = hotkeyPrint+'['+k+'] :\t'+name print(hotkeyPrint) rtc = 'ml_hk_'+self.name+'_'+self.functions[i] nc = rtc+'_NC' if not mc.runTimeCommand(rtc, exists=True): rtc = mc.runTimeCommand(rtc, default=True, annotation=name+' Hotkey generated by ml_toolbox', category='User', command=self.commands[i]) nc = mc.nameCommand(nc, default=True, annotation=name+' nameCommand generated by ml_toolbox', command=rtc) mc.hotkey(keyShortcut=k, alt=self.altModifier[i], ctl=self.ctrlModifier[i], name=nc, releaseName='') if __name__ == '__main__': main()