# Univeral Tool Template v011.0 tpl_ver = 11.16 tpl_date = 191025 print("tpl_ver: {0}-{1}".format(tpl_ver, tpl_date)) # by ying - https://github.com/shiningdesign/universal_tool_template.py import importlib import sys # ---- hostMode ---- hostMode = '' hostModeList = [ ['maya', {'mui':'maya.OpenMayaUI', 'cmds':'maya.cmds'} ], ['nuke', {'nuke':'nuke', 'nukescripts':'nukescripts'} ], ['fusion', {'fs':'fusionscript'} ], ['houdini', {'hou':'hou'} ], ['blender', {'bpy':'bpy'} ], ['npp', {'Npp':'Npp'} ], ] for name, libs in hostModeList: try: for x in libs.keys(): globals()[x] = importlib.import_module(libs[x]) hostMode = name break except ImportError: pass if hostMode == '': hostMode = 'desktop' print('Host: {0}'.format(hostMode)) # ---- qtMode ---- qtMode = 0 # 0: PySide; 1 : PyQt, 2: PySide2, 3: PyQt5 qtModeList = ('PySide', 'PyQt4', 'PySide2', 'PyQt5') try: from PySide import QtGui, QtCore import PySide.QtGui as QtWidgets qtMode = 0 if hostMode == "maya": import shiboken except ImportError: try: from PySide2 import QtCore, QtGui, QtWidgets qtMode = 2 if hostMode == "maya": import shiboken2 as shiboken except ImportError: try: from PyQt4 import QtGui,QtCore import PyQt4.QtGui as QtWidgets import sip qtMode = 1 except ImportError: from PyQt5 import QtGui,QtCore,QtWidgets import sip qtMode = 3 print('Qt: {0}'.format(qtModeList[qtMode])) # ---- pyMode ---- # python 2,3 support unicode function try: UNICODE_EXISTS = bool(type(unicode)) except NameError: # lambda s: str(s) # this works for function but not for class check unicode = str if sys.version_info[:3][0]>=3: reload = importlib.reload # add reload pyMode = '.'.join([ str(n) for n in sys.version_info[:3] ]) print("Python: {0}".format(pyMode)) # ---- osMode ---- osMode = 'other' if sys.platform in ['win32','win64']: osMode = 'win' elif sys.platform == 'darwin': osMode = 'mac' elif sys.platform == 'linux2': osMode = 'linux' print("OS: {0}".format(osMode)) # ---- template module list ---- import os # for path and language code from functools import partial # for partial function creation import json # for ascii data output if sys.version_info[:3][0]<3: import cPickle # for binary data output else: import _pickle as cPickle import re # for name pattern import ctypes # for windows instance detection import subprocess # for cmd call #======================================= # UniversalToolUI template class #======================================= class UniversalToolUI(QtWidgets.QMainWindow): def __init__(self, parent=None, mode=0): QtWidgets.QMainWindow.__init__(self, parent) #------------------------------ # class variables #------------------------------ self.version = '0.1' self.date = '2017.01.01' self.log = 'no version log in user class' self.help = 'no help guide in user class' self.hotkey = {} self.uiList={} # for ui obj storage self.memoData = {} # key based variable data storage self.memoData['font_size_default'] = QtGui.QFont().pointSize() self.memoData['font_size'] = self.memoData['font_size_default'] self.memoData['last_export'] = '' self.memoData['last_import'] = '' self.name = self.__class__.__name__ self.location = '' if getattr(sys, 'frozen', False): # frozen - cx_freeze self.location = sys.executable else: # unfrozen self.location = os.path.realpath(sys.modules[self.__class__.__module__].__file__) self.iconPath = os.path.join(os.path.dirname(self.location),'icons',self.name+'.png') self.iconPix = QtGui.QPixmap(self.iconPath) self.icon = QtGui.QIcon(self.iconPath) self.fileType='.{0}_EXT'.format(self.name) #------------------------------ # core function variable #------------------------------ self.qui_core_dict = { 'vbox': 'QVBoxLayout','hbox':'QHBoxLayout','grid':'QGridLayout', 'form':'QFormLayout', 'split': 'QSplitter', 'grp':'QGroupBox', 'tab':'QTabWidget', 'btn':'QPushButton', 'btnMsg':'QPushButton', 'label':'QLabel', 'input':'QLineEdit', 'check':'QCheckBox', 'choice':'QComboBox', 'txt': 'QTextEdit', 'list': 'QListWidget', 'tree': 'QTreeWidget', 'table': 'QTableWidget', 'space': 'QSpacerItem', 'menu' : 'QMenu', 'menubar' : 'QMenuBar', } self.qui_user_dict = {} def setupStyle(self): # global app style setting for desktop if hostMode == "desktop": QtWidgets.QApplication.setStyle(QtWidgets.QStyleFactory.create('Cleanlooks')) self.setStyleSheet("QLineEdit:disabled{background-color: gray;}") def setupMenu(self): # global help menu if 'help_menu' in self.uiList.keys(): # for info review self.qui_atn('helpHostMode_atnNone','Host Mode - {}'.format(hostMode),'Host Running.') self.qui_atn('helpPyMode_atnNone','Python Mode - {}'.format(pyMode),'Python Library Running.') self.qui_atn('helpQtMode_atnNone','Qt Mode - {}'.format(qtModeList[qtMode]),'Qt Library Running.') self.qui_atn('helpTemplate_atnNone','Universal Tool Teamplate - {0}.{1}'.format(tpl_ver, tpl_date),'based on Univeral Tool Template v{0} by Shining Ying - https://github.com/shiningdesign/universal{1}tool{1}template.py'.format(tpl_ver,'_')) self.uiList['helpGuide_msg'] = self.help self.qui_atn('helpGuide_atnMsg','Usage Guide','How to Usge Guide.') self.uiList['helpLog_msg'] = self.log self.qui_atn('helpLog_atnMsg','About v{0} - {1}'.format(self.version, self.date),'Vesion Log.') self.qui_menu('helpHostMode_atnNone | helpPyMode_atnNone | helpQtMode_atnNone | helpTemplate_atnNone | _ | helpGuide_atnMsg | helpLog_atnMsg', 'help_menu') def setupWin(self): self.setWindowTitle(self.name + " - v" + self.version + " - host: " + hostMode) self.setWindowIcon(self.icon) self.drag_position=QtGui.QCursor.pos() # initial win drag position def setupUI(self, layout='grid'): main_widget = QtWidgets.QWidget() self.setCentralWidget(main_widget) main_layout = self.quickLayout(layout, 'main_layout') # grid for auto fill window size main_widget.setLayout(main_layout) def Establish_Connections(self): for ui_name in self.uiList.keys(): prefix = ui_name.rsplit('_', 1)[0] if ui_name.endswith('_btn'): self.uiList[ui_name].clicked.connect(getattr(self, prefix+"_action", partial(self.default_action,ui_name))) elif ui_name.endswith('_atn'): self.uiList[ui_name].triggered.connect(getattr(self, prefix+"_action", partial(self.default_action,ui_name))) elif ui_name.endswith('_btnMsg'): self.uiList[ui_name].clicked.connect(getattr(self, prefix+"_message", partial(self.default_message,ui_name))) elif ui_name.endswith('_atnMsg'): self.uiList[ui_name].triggered.connect(getattr(self, prefix+"_message", partial(self.default_message,ui_name))) #======================================= # ui response functions #======================================= def default_action(self, ui_name, *argv): print("No action defined for this UI element: "+ui_name) def default_message(self, ui_name): prefix = ui_name.rsplit('_', 1)[0] msgName = prefix+"_msg" msg_txt = msgName + " is not defined in uiList." if msgName in self.uiList: msg_txt = self.uiList[msgName] self.quickMsg(msg_txt) def default_menu_call(self, ui_name, point): if ui_name in self.uiList.keys() and ui_name+'_menu' in self.uiList.keys(): self.uiList[ui_name+'_menu'].exec_(self.uiList[ui_name].mapToGlobal(point)) def toggleTop_action(self): self.setWindowFlags(self.windowFlags() ^ QtCore.Qt.WindowStaysOnTopHint) self.show() def hotkey_action(self): txt_list = [] for each_key in sorted(self.hotkey.keys()): txt_list.append(each_key+' : '+unicode(self.hotkey[each_key].key().toString())) self.quickMsg('\n'.join(txt_list)) #======================================= # ui feedback functions #======================================= def ____ui_feedback_functions____(): pass def quickInfo(self, info, force=0): if hasattr( self.window(), "quickInfo") and force == 0: self.window().statusBar().showMessage(info) else: self.statusBar().showMessage(info) def quickMsg(self, msg, block=1, ask=0): tmpMsg = QtWidgets.QMessageBox(self) # for simple msg that no need for translation tmpMsg.setWindowTitle("Info") lineCnt = len(msg.split('\n')) if lineCnt > 25: scroll = QtWidgets.QScrollArea() scroll.setWidgetResizable(1) content = QtWidgets.QWidget() scroll.setWidget(content) layout = QtWidgets.QVBoxLayout(content) tmpLabel = QtWidgets.QLabel(msg) tmpLabel.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) layout.addWidget(tmpLabel) tmpMsg.layout().addWidget(scroll, 0, 0, 1, tmpMsg.layout().columnCount()) tmpMsg.setStyleSheet("QScrollArea{min-width:600 px; min-height: 400px}") else: tmpMsg.setText(msg) if block == 0: tmpMsg.setWindowModality( QtCore.Qt.NonModal ) if ask==0: tmpMsg.addButton("OK",QtWidgets.QMessageBox.YesRole) else: tmpMsg.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel) if block: value = tmpMsg.exec_() if value == QtWidgets.QMessageBox.Ok: return 1 else: return 0 else: tmpMsg.show() return 0 def quickMsgAsk(self, msg, mode=0, choice=[]): # getItem, getInteger, getDouble, getText modeOpt = (QtWidgets.QLineEdit.Normal, QtWidgets.QLineEdit.NoEcho, QtWidgets.QLineEdit.Password, QtWidgets.QLineEdit.PasswordEchoOnEdit) # option: QtWidgets.QInputDialog.UseListViewForComboBoxItems if len(choice)==0: txt, ok = QtWidgets.QInputDialog.getText(self, "Input", msg, modeOpt[mode]) return (unicode(txt), ok) else: txt, ok = QtWidgets.QInputDialog.getItem(self, "Input", msg, choice, 0, 0) return (unicode(txt), ok) def quickModKeyAsk(self): modifiers = QtWidgets.QApplication.queryKeyboardModifiers() clickMode = 0 # basic mode if modifiers == QtCore.Qt.ControlModifier: clickMode = 1 # ctrl elif modifiers == QtCore.Qt.ShiftModifier: clickMode = 2 # shift elif modifiers == QtCore.Qt.AltModifier: clickMode = 3 # alt elif modifiers == QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier | QtCore.Qt.AltModifier: clickMode = 4 # ctrl+shift+alt elif modifiers == QtCore.Qt.ControlModifier | QtCore.Qt.AltModifier: clickMode = 5 # ctrl+alt elif modifiers == QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier: clickMode = 6 # ctrl+shift elif modifiers == QtCore.Qt.AltModifier | QtCore.Qt.ShiftModifier: clickMode = 7 # alt+shift return clickMode def quickFileAsk(self, type, ext=None, dir=None): if ext == None: ext = "RAW data (*.json);;RAW binary data (*.dat);;Format Txt (*{0});;AllFiles (*.*)".format(self.fileType) elif isinstance(ext, (str,unicode)): if ';;' not in ext: if ext == '': ext = 'AllFiles (*.*)' else: ext = self.extFormat(ext) + ';;AllFiles (*.*)' elif isinstance(ext, (tuple,list)): if len(ext) > 0 and isinstance(ext[0], (tuple,list)): tmp_list = [self.extFormat(x) for x in ext] tmp_list.append('AllFiles (*.*)') ext = ';;'.join(tmp_list) else: ext = ';;'.join([self.extFormat(x) for x in ext].append('AllFiles(*.*)')) elif isinstance(ext, dict): tmp_list = [self.extFormat(x) for x in ext.items()] tmp_list.append('AllFiles (*.*)') ext = ';;'.join(tmp_list) else: ext = "AllFiles (*.*)" file = '' if type == 'export': if dir == None: dir = self.memoData['last_export'] file = QtWidgets.QFileDialog.getSaveFileName(self, "Save File",dir,ext) elif type == 'import': if dir == None: dir = self.memoData['last_import'] file = QtWidgets.QFileDialog.getOpenFileName(self, "Open File",dir,ext) if isinstance(file, (list, tuple)): file = file[0] # for deal with pyside case else: file = unicode(file) # for deal with pyqt case # save last dir in memoData if file != '': if type == 'export': self.memoData['last_export'] = os.path.dirname(file) #QFileInfo().path() elif type == 'import': self.memoData['last_import'] = os.path.dirname(file) return file def extFormat(self, ext): if isinstance(ext, (tuple,list)): ext = '{0} (*.{1})'.format(ext[1],ext[0]) else: if ext.startswith('.'): ext = ext[1:] ext = '{0} (*.{0})'.format(ext) return ext def quickFolderAsk(self,dir=None): if dir == None: dir = self.memoData['last_browse'] if self.parent is not None and hasattr(self.parent, 'memoData'): dir = self.parent.memoData['last_browse'] return unicode(QtWidgets.QFileDialog.getExistingDirectory(self, "Select Directory",dir)) def openFolder(self, folderPath): if os.path.isfile(folderPath): folderPath = os.path.dirname(folderPath) if os.path.isdir(folderPath): cmd_list = None if sys.platform == 'darwin': cmd_list = ['open', '--', folderPath] elif sys.platform == 'linux2': cmd_list = ['xdg-open', folderPath] elif sys.platform in ['win32','win64']: cmd_list = ['explorer', folderPath.replace('/','\\')] if cmd_list != None: try: subprocess.check_call(cmd_list) except subprocess.CalledProcessError: pass # handle errors in the called executable except OSError: pass # executable not found def openFile(self, filePath): if sys.platform in ['win32','win64']: os.startfile(filePath) elif sys.platform == 'darwin': os.open(filePath) elif sys.platform == 'linux2': os.xdg-open(filePath) def newFolder(self, parentPath, name=None): if os.path.isfile(parentPath): parentPath = os.path.dirname(parentPath) created = 0 if name == None: name, ok = self.quickMsgAsk('Enter the folder name:') if not ok or name=='': return create_path = os.path.join(parentPath, name) if os.path.isdir(create_path): self.quickMsg('Already Exists') else: try: os.makedirs(create_path) created = 1 except OSError: self.quickMsg('Error on creation user data folder') return created #======================================= # ui info functions #======================================= def ____ui_info_functions____(): pass def input_text(self, input_name, msg=''): text = unicode(self.uiList[input_name].text()) if text == '': print("Please define the text. {0}".format(msg)) return return text def input_int(self, input_name, min=None, max=None, msg=''): input_txt = str(self.uiList[input_name].text()) result = None # int valid if not input_txt.isdigit(): print("Please enter a valid int. {0}".format(msg)) return result = int(input_txt) # min if min != None: if result < min: print("Please enter a valid int number >= {0}. {1}".format(min, msg)) return # max if max != None: if result > max: print("Please enter a valid int number <= {0}. {1}".format(max, msg)) return return result def input_float(self, input_name, min=None, max=None, msg=''): input_txt = str(self.uiList[input_name].text()) result = None try: result = float(input_txt) except (ValueError, TypeError): return # min if min != None: if result < min: print("Please enter a valid int number >= {0}. {1}".format(min, msg)) return # max if max != None: if result > max: print("Please enter a valid int number <= {0}. {1}".format(max, msg)) return return result def input_choice(self, ui_name): if ui_name in self.uiList.keys(): return self.uiList[ui_name].currentIndex() else: return def input_check(self, ui_name): if ui_name in self.uiList.keys(): return self.uiList[ui_name].isChecked() else: return def output_text(self, ui_name, text): if ui_name in self.uiList.keys(): self.uiList[ui_name].setText(text) #======================================= # file data functions #======================================= def ____file_functions____(): pass def readDataFile(self,file,binary=0): with open(file) as f: if binary == 0: data = json.load(f) else: data = cPickle.load(f) return data def writeDataFile(self,data,file,binary=0): with open(file, 'w') as f: if binary == 0: json.dump(data, f) else: cPickle.dump(data, f) def readTextFile(self, file): with open(file) as f: txt = f.read() return txt def writeTextFile(self, txt, file, b=0): if b==0: b='' else: b = 'b' with open(file, 'w'+b) as f: f.write(txt) def dict_merge(self, default_dict, extra_dict, addKey=0): # dictionary merge, with optional adding extra data from extra_dict new_dict = {} for key in default_dict.keys(): if not isinstance( default_dict[key], dict ): # value case if key in extra_dict.keys(): is_same_text_type = isinstance(extra_dict[key], (str,unicode)) and isinstance(default_dict[key], (str,unicode)) is_same_non_text_type = type(extra_dict[key]) is type(default_dict[key]) if is_same_text_type or is_same_non_text_type: print('use config file value for key: '+key) new_dict[key] = extra_dict[key] else: new_dict[key] = default_dict[key] else: new_dict[key] = default_dict[key] else: # dictionary case if key in extra_dict.keys() and isinstance( extra_dict[key], dict ): new_dict[key] = self.dict_merge( default_dict[key], extra_dict[key], addKey ) else: new_dict[key] = default_dict[key] # optional, add additional keys if addKey == 1: for key in [ x for x in extra_dict.keys() if x not in default_dict.keys() ]: new_dict[key] = extra_dict[key] return new_dict #======================================= # ui text functions #======================================= def ____ui_text_functions____(): pass def fontNormal_action(self, uiClass_list=[]): if len(uiClass_list) == 0: uiClass_list = 'QLabel,QPushButton'.split(',') self.memoData['font_size'] = self.memoData['font_size_default'] self.setStyleSheet( "{0} { font-size: {1}pt;}".format(','.join(uiClass_list), self.memoData['font_size']) ) def fontUp_action(self, uiClass_list=[]): if len(uiClass_list) == 0: uiClass_list = 'QLabel,QPushButton'.split(',') self.memoData['font_size'] += 2 self.setStyleSheet( "{0} { font-size: {1}pt;}".format(','.join(uiClass_list), self.memoData['font_size']) ) def fontDown_action(self, uiClass_list=[]): if len(uiClass_list) == 0: uiClass_list = 'QLabel,QPushButton'.split(',') if self.memoData['font_size'] >= self.memoData['font_size_default']: self.memoData['font_size'] -= 2 self.setStyleSheet( "{0} { font-size: {1}pt;}".format(','.join(uiClass_list), self.memoData['font_size']) ) def loadLang(self, build_menu=1): # store default language self.memoData['lang']={} self.memoData['lang']['default']={} for ui_name in self.uiList.keys(): ui_element = self.uiList[ui_name] if isinstance(ui_element, (QtWidgets.QLabel, QtWidgets.QPushButton, QtWidgets.QAction, QtWidgets.QCheckBox) ): # uiType: QLabel, QPushButton, QAction(menuItem), QCheckBox self.memoData['lang']['default'][ui_name] = unicode(ui_element.text()) elif isinstance(ui_element, (QtWidgets.QGroupBox, QtWidgets.QMenu) ): # uiType: QMenu, QGroupBox self.memoData['lang']['default'][ui_name] = unicode(ui_element.title()) elif isinstance(ui_element, QtWidgets.QTabWidget): # uiType: QTabWidget tabCnt = ui_element.count() tabNameList = [] for i in range(tabCnt): tabNameList.append(unicode(ui_element.tabText(i))) self.memoData['lang']['default'][ui_name]=';'.join(tabNameList) elif isinstance(ui_element, QtWidgets.QComboBox): # uiType: QComboBox itemCnt = ui_element.count() itemNameList = [] for i in range(itemCnt): itemNameList.append(unicode(ui_element.itemText(i))) self.memoData['lang']['default'][ui_name]=';'.join(itemNameList) elif isinstance(ui_element, QtWidgets.QTreeWidget): # uiType: QTreeWidget labelCnt = ui_element.headerItem().columnCount() labelList = [] for i in range(labelCnt): labelList.append(unicode(ui_element.headerItem().text(i))) self.memoData['lang']['default'][ui_name]=';'.join(labelList) elif isinstance(ui_element, QtWidgets.QTableWidget): # uiType: QTableWidget colCnt = ui_element.columnCount() headerList = [] for i in range(colCnt): if ui_element.horizontalHeaderItem(i): headerList.append( unicode(ui_element.horizontalHeaderItem(i).text()) ) else: headerList.append('') self.memoData['lang']['default'][ui_name]=';'.join(headerList) elif isinstance(ui_element, (str, unicode) ): # uiType: string for msg self.memoData['lang']['default'][ui_name] = self.uiList[ui_name] # language menu lang_menu = 'language_menu' if build_menu == 1: self.qui_menubar('language_menu;&Language') self.qui_menu('langDefault_atnLang;Default | _', lang_menu) self.uiList['langDefault_atnLang'].triggered.connect(partial(self.setLang,'default')) # scan for language file lang_path = os.path.dirname(self.location) baseName = os.path.splitext( os.path.basename(self.location) )[0] for file in self.getPathChild(lang_path, pattern=baseName+'_lang_[a-zA-Z]+.json', isfile=1): langName = re.findall(baseName+'_lang_(.+)\.json', file) if len(langName) == 1: langName = langName[0].upper() self.memoData['lang'][ langName ] = self.readDataFile( os.path.join(lang_path, file) ) if build_menu == 1: self.qui_menu('{0}_atnLang;{0}'.format(langName), lang_menu) self.uiList[langName+'_atnLang'].triggered.connect(partial(self.setLang,langName)) # if no language file detected, add export default language option if build_menu == 1: if isinstance(self, QtWidgets.QMainWindow) and len(self.memoData['lang']) == 1: self.qui_menu('langExport_atnLang;Export Default Language', lang_menu) self.uiList['langExport_atnLang'].triggered.connect(self.exportLang) def setLang(self, langName): lang_data = self.memoData['lang'][langName] for ui_name in lang_data.keys(): if ui_name in self.uiList.keys() and lang_data[ui_name] != '': ui_element = self.uiList[ui_name] # '' means no translation availdanle in that data file if isinstance(ui_element, (QtWidgets.QLabel, QtWidgets.QPushButton, QtWidgets.QAction, QtWidgets.QCheckBox) ): # uiType: QLabel, QPushButton, QAction(menuItem), QCheckBox ui_element.setText(lang_data[ui_name]) elif isinstance(ui_element, (QtWidgets.QGroupBox, QtWidgets.QMenu) ): # uiType: QMenu, QGroupBox ui_element.setTitle(lang_data[ui_name]) elif isinstance(ui_element, QtWidgets.QTabWidget): # uiType: QTabWidget tabCnt = ui_element.count() tabNameList = lang_data[ui_name].split(';') if len(tabNameList) == tabCnt: for i in range(tabCnt): if tabNameList[i] != '': ui_element.setTabText(i,tabNameList[i]) elif isinstance(ui_element, QtWidgets.QComboBox): # uiType: QComboBox itemCnt = ui_element.count() itemNameList = lang_data[ui_name].split(';') ui_element.clear() ui_element.addItems(itemNameList) elif isinstance(ui_element, QtWidgets.QTreeWidget): # uiType: QTreeWidget labelCnt = ui_element.headerItem().columnCount() labelList = lang_data[ui_name].split(';') ui_element.setHeaderLabels(labelList) elif isinstance(ui_element, QtWidgets.QTableWidget): # uiType: QTableWidget colCnt = ui_element.columnCount() headerList = lang_data[ui_name].split(';') cur_table.setHorizontalHeaderLabels( headerList ) elif isinstance(ui_element, (str, unicode) ): # uiType: string for msg self.uiList[ui_name] = lang_data[ui_name] def exportLang(self): file = self.quickFileAsk('export', ext='json') if file != '': self.writeDataFile( self.memoData['lang']['default'], file ) self.quickMsg("Languge File created: '"+file) #======================================= # qui functions #======================================= def ____ui_creation_functions____(): pass def setAsUI(self): # turn win to widget self.setWindowFlags(QtCore.Qt.Widget) self.statusBar().hide() self.uiList['main_layout'].setContentsMargins(0, 0, 0, 0) def qui(self, ui_list_string, parent_ui_string='', insert_opt=''): ui_creation_list = [ x.strip() for x in ui_list_string.split('|') if x.strip() !=''] ui_creation_quickUI_list = [] # ------------ # - ui list # ------------ for ui_creation in ui_creation_list: arg_list = ui_creation.split(';') uiName = arg_list[0].split('@')[0] # ------------ # continue if ui is already created. pass as ui reference if uiName in self.uiList.keys(): ui_creation_quickUI_list.append(self.uiList[uiName]) continue # ------------ # create quickUI string # - expand short name for Class uiClass = uiName.rsplit('_',1)[-1] if uiClass == 'layout' and len(arg_list)>1: uiClass = arg_list[1] arg_list = [ arg_list[0] ] if uiClass in self.qui_user_dict: uiClass = self.qui_user_dict[uiClass] # first, try user dict elif uiClass in self.qui_core_dict: uiClass = self.qui_core_dict[uiClass] # then, try default core dict # - check it is valid Qt class or a user class if hasattr(QtWidgets, uiClass) or uiClass in sys.modules: pass # uiClass is valid for Qt class, user module else: print("WARNING: ({0}) is not defined in self.qui_user_dict and it is not a Qt widget class or User class; Item {1} Ignored.".format(uiClass, uiName)) continue # - set quickUI creation format arg_list[0] = arg_list[0] +';'+uiClass if len(arg_list)==1: if uiClass in ('QPushButton','QLabel'): arg_list.append(uiName) # give empty button and label a place holder name ui_creation_quickUI_list.append(';'.join(arg_list)) # ------------ # - ui parent # ------------ parent_creation_quickUI_input = '' parent_arg_list = parent_ui_string.split(';') parent_uiName = parent_arg_list[0] # - continue if parent ui is already created. pass as ui reference if parent_uiName in self.uiList.keys(): parent_creation_quickUI_input = self.uiList[parent_uiName] else: parent_uiClass = parent_uiName.rsplit('_',1)[-1] if parent_uiClass == 'layout' and len(parent_arg_list)>1: parent_uiClass = parent_arg_list[1] parent_arg_list = [ parent_arg_list[0] ] if parent_uiClass in self.qui_user_dict: parent_uiClass = self.qui_user_dict[parent_uiClass] # first, try user dict elif parent_uiClass in self.qui_core_dict: parent_uiClass = self.qui_core_dict[parent_uiClass] # then, try default core dict # - check it is valid Qt class or a user class if hasattr(QtWidgets, parent_uiClass) or parent_uiClass in sys.modules: pass # uiClass is valid for Qt class, user module else: print("WARNING: ({0}) is not defined in self.qui_user_dict and it is not a Qt widget class or User class; Item {1} Ignored.".format(parent_uiClass, parent_uiName)) return # - set quickUI creation format parent_arg_list[0] = parent_arg_list[0] +';'+parent_uiClass parent_creation_quickUI_input = ';'.join(parent_arg_list) self.quickUI(ui_creation_quickUI_list, parent_creation_quickUI_input, insert_opt) return parent_uiName def qui_menu(self, action_list_str, menu_str): # qui menu creation # syntax: self.qui_menu('right_menu_createFolder_atn;Create Folder,Ctrl+D | right_menu_openFolder_atn;Open Folder', 'right_menu') if menu_str not in self.uiList.keys(): self.uiList[menu_str] = QtWidgets.QMenu() create_opt_list = [ x.strip() for x in action_list_str.split('|') ] for each_creation in create_opt_list: ui_info = [ x.strip() for x in each_creation.split(';') ] atn_name = ui_info[0] atn_title = '' atn_hotkey = '' if len(ui_info) > 1: options = ui_info[1].split(',') atn_title = '' if len(options) < 1 else options[0] atn_hotkey = '' if len(options) < 2 else options[1] if atn_name != '': if atn_name == '_': self.uiList[menu_str].addSeparator() else: if atn_name not in self.uiList.keys(): self.uiList[atn_name] = QtWidgets.QAction(atn_title, self) if atn_hotkey != '': self.uiList[atn_name].setShortcut(QtGui.QKeySequence(atn_hotkey)) self.uiList[menu_str].addAction(self.uiList[atn_name]) def qui_atn(self, ui_name, title, tip=None, icon=None, parent=None, key=None): self.uiList[ui_name] = QtWidgets.QAction(title, self) if icon!=None: self.uiList[ui_name].setIcon(QtGui.QIcon(icon)) if tip !=None: self.uiList[ui_name].setStatusTip(tip) if key != None: self.uiList[ui_name].setShortcut(QtGui.QKeySequence(key)) if parent !=None: if isinstance(parent, (str, unicode)) and parent in self.uiList.keys(): self.uiList[parent].addAction(self.uiList[ui_name]) elif isinstance(parent, QtWidgets.QMenu): parent.addAction(self.uiList[ui_name]) return ui_name def qui_key(self, key_name, key_combo, func): self.hotkey[key_name] = QtWidgets.QShortcut(QtGui.QKeySequence(key_combo), self) self.hotkey[key_name].activated.connect( func ) def qui_menubar(self, menu_list_str): if not isinstance(self, QtWidgets.QMainWindow): print("Warning: Only QMainWindow can have menu bar.") return menubar = self.menuBar() create_opt_list = [ x.strip() for x in menu_list_str.split('|') ] for each_creation in create_opt_list: ui_info = [ x.strip() for x in each_creation.split(';') ] menu_name = ui_info[0] menu_title = '' if len(ui_info) > 1: menu_title = ui_info[1] if menu_name not in self.uiList.keys(): self.uiList[menu_name] = QtWidgets.QMenu(menu_title) menubar.addMenu(self.uiList[menu_name]) #======================================= # ui creation functions #======================================= def quickLayout(self, type, ui_name=""): the_layout = '' if type in ("form", "QFormLayout"): the_layout = QtWidgets.QFormLayout() the_layout.setLabelAlignment(QtCore.Qt.AlignLeft) the_layout.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow) elif type in ("grid", "QGridLayout"): the_layout = QtWidgets.QGridLayout() elif type in ("hbox", "QHBoxLayout"): the_layout = QtWidgets.QHBoxLayout() the_layout.setAlignment(QtCore.Qt.AlignTop) else: the_layout = QtWidgets.QVBoxLayout() the_layout.setAlignment(QtCore.Qt.AlignTop) if ui_name != "": self.uiList[ui_name] = the_layout return the_layout def quickUI(self, part_list, parentObject="", insert_opt=""): # part_list contains: # -- 1. string (strings for widget/space, layout, container[group, tab, splitter]) # -- 2. object (widget/space, layout, container[group, tab, splitter]) # -- 3. object list # -- 4. [object list, label_object list] # parentObject contains: # -- 1. string (strings for layout, container[group, tab, splitter]) # -- 2. object (layout, container[group, tab, splitter]) # insert_opt: # -- insert into grid layout, h, v # -- insert into tab, titles if not isinstance(part_list, (list, tuple)): part_list = [part_list] # func variable ui_list = [] ui_label_list = [] form_type = 0 # flag for store whether ui_list need a label widget list for form layout creation # 1. convert string to object and flatten part_list for each_part in part_list: # 1.1 string if isinstance(each_part, str): # - string : get part info partInfo = each_part.split(';') uiNameLabel = partInfo[0].split('@') uiName = uiNameLabel[0] uiLabel = '' if len(uiNameLabel) > 1: uiLabel = uiNameLabel[1] form_type = 1 uiType = partInfo[1] if len(partInfo) > 1 else "" uiArgs = partInfo[2] if len(partInfo) > 2 else "" # - string : valid info if uiType == "": print("Warning (QuickUI): uiType is empty for "+each_part) else: # - string : to object creation ui_create_state = 0 # flag to track creation success if not uiType[0] == 'Q': # -- 3rd ui type, create like UI_Class.UI_Class() self.uiList[uiName] = getattr(sys.modules[uiType], uiType)() # getattr(eval(uiType), uiType)() ui_list.append(self.uiList[uiName]) ui_create_state = 1 else: # -- Qt ui if uiType in ('QVBoxLayout', 'QHBoxLayout', 'QFormLayout', 'QGridLayout'): # --- Qt Layout creation preset func ui_list.append(self.quickLayout(uiType, uiName)) ui_create_state = 1 elif uiType in ('QSplitter', 'QTabWidget', 'QGroupBox'): # --- Qt container creation if uiType == 'QSplitter': # ---- QSplitter as element split_type = QtCore.Qt.Horizontal if uiArgs == 'v': split_type = QtCore.Qt.Vertical self.uiList[uiName]=QtWidgets.QSplitter(split_type) ui_list.append(self.uiList[uiName]) ui_create_state = 1 elif uiType == 'QTabWidget': # ---- QTabWidget as element, no tab label need for input self.uiList[uiName]=QtWidgets.QTabWidget() self.uiList[uiName].setStyleSheet("QTabWidget::tab-bar{alignment:center;}QTabBar::tab { min-width: 100px; }") ui_list.append(self.uiList[uiName]) ui_create_state = 1 elif uiType == 'QGroupBox': # ---- QGroupBox as element, with layout type and optional title arg_list = [x.strip() for x in uiArgs.split(',')] grp_layout = arg_list[0] if arg_list[0]!='' else 'vbox' grp_title = arg_list[1] if len(arg_list)>1 else uiName # create layout and set grp layout grp_layout = self.quickLayout(grp_layout, uiName+"_layout" ) self.uiList[uiName] = QtWidgets.QGroupBox(grp_title) self.uiList[uiName].setLayout(grp_layout) ui_list.append(self.uiList[uiName]) ui_create_state = 1 else: # --- Qt widget creation if uiArgs == "": # ---- widget with no uiArgs self.uiList[uiName] = getattr(QtWidgets, uiType)() ui_list.append(self.uiList[uiName]) ui_create_state = 1 else: # ---- widget with uiArgs if not ( uiArgs.startswith("(") and uiArgs.endswith(")") ): # ----- with string arg self.uiList[uiName] = getattr(QtWidgets, uiType)(uiArgs) ui_list.append(self.uiList[uiName]) ui_create_state = 1 else: # ----- with array arg arg_list = uiArgs.replace('(','').replace(')','').split(',') if uiType == 'QComboBox': self.uiList[uiName] = QtWidgets.QComboBox() self.uiList[uiName].addItems(arg_list) ui_list.append(self.uiList[uiName]) ui_create_state = 1 elif uiType == 'QTreeWidget': self.uiList[uiName] = QtWidgets.QTreeWidget() self.uiList[uiName].setHeaderLabels(arg_list) ui_list.append(self.uiList[uiName]) ui_create_state = 1 elif uiType == 'QSpacerItem': policyList = ( QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Ignored) # 0 = fixed; 1 > min; 2 < max; 3 = prefered; 4 = <expanding>; 5 = expanding> Aggresive; 6=4 ignored size input # factors in fighting for space: horizontalStretch # extra space: setContentsMargins and setSpacing # ref: http://www.cnblogs.com/alleyonline/p/4903337.html arg_list = [ int(x) for x in arg_list ] self.uiList[uiName] = QtWidgets.QSpacerItem(arg_list[0],arg_list[1], policyList[arg_list[2]], policyList[arg_list[3]] ) ui_list.append(self.uiList[uiName]) ui_create_state = 1 else: print("Warning (QuickUI): uiType don't support array arg for "+each_part) # - string : Qt widget label for form element creation if ui_create_state == 1: if uiLabel != '': ui_label_list.append((uiName,uiLabel)) else: ui_label_list.append('') ui_create_state = 0 else: # 1.2 other part like: object, object list, [object, label object] if isinstance(each_part, (QtWidgets.QWidget, QtWidgets.QLayout, QtWidgets.QSpacerItem)): # - object ui_list.append(each_part) ui_label_list.append('') elif isinstance(each_part, (tuple, list)): # - object list, [object, label object] if len(each_part) != 0: if isinstance(each_part[0], (tuple, list)) and len(each_part)==2: # -- [object, label object] ui_list.extend(each_part[0]) ui_label_list.extend(each_part[1]) else: # -- object list ui_list.extend(each_part) ui_label_list.extend(['']*len(each_part)) # 2 parentObject part if parentObject == '': # - if no parentObject, return object list or [object list, label_object list] if form_type == 1: return [ui_list, ui_label_list] else: return ui_list else: if isinstance(parentObject, str): # - if parentObject, convert string to parentObject parentName = '' parentType = '' parentArgs = '' layout_type_list = ( 'QVBoxLayout', 'QHBoxLayout', 'QFormLayout', 'QGridLayout', 'vbox', 'hbox', 'grid', 'form', 'QSplitter', 'QTabWidget', 'QGroupBox', 'split', 'tab', 'grp', ) # get options parentOpt = parentObject.split(';') if len(parentOpt) == 1: # -- only 1 arg case: strict name format, eg. conf_QHBoxLayout, config_hbox parentName = parentOpt[0] # 1 para case: strict name endfix format parentType = parentName.rsplit('_',1)[-1] elif len(parentOpt)==2: # -- only 2 arg case: # a. flexible name format + type eg. conf_layout;QGridLayout, conf_layout;hbox # b. strict name format, + setting eg. conf_QGridLayout;h, config_grid;h parentName = parentOpt[0] if parentOpt[1] in layout_type_list: parentType = parentOpt[1] # a else: parentType = parentName.rsplit('_',1)[-1] parentArgs = parentOpt[1] # b elif len(parentOpt)>=3: # -- 3 arg case: # flexible name format + type + settings eg. conf_layout;QGridLayout;h parentName = parentOpt[0] parentType = parentOpt[1] parentArgs = parentOpt[2] # - validate layout options if parentName=='' or (parentType not in layout_type_list): print("Warning (QuickUI): quickUI not support parent layout as "+parentObject) return else: # - create layout if parentType in ('QVBoxLayout', 'QHBoxLayout', 'QFormLayout', 'QGridLayout', 'vbox', 'hbox', 'grid', 'form'): # -- layout object case parentObject = self.quickLayout(parentType, parentName) elif parentType in ('QSplitter', 'QTabWidget', 'QGroupBox', 'split', 'tab', 'grp'): # --- Qt container creation if parentType in ('QSplitter', 'split'): # ---- QSplitter as element split_type = QtCore.Qt.Horizontal if parentArgs == 'v': split_type = QtCore.Qt.Vertical self.uiList[parentName]=QtWidgets.QSplitter(split_type) parentObject = self.uiList[parentName] elif parentType in ('QTabWidget', 'tab'): # ---- QTabWidget as element, no tab label need for input self.uiList[parentName]=QtWidgets.QTabWidget() self.uiList[parentName].setStyleSheet("QTabWidget::tab-bar{alignment:center;}QTabBar::tab { min-width: 100px; }") parentObject = self.uiList[parentName] elif parentType in ('QGroupBox', 'grp'): # ---- QGroupBox as element, with layout type and optional title arg_list = [x.strip() for x in parentArgs.split(',')] grp_layout = arg_list[0] if arg_list[0]!='' else 'vbox' grp_title = arg_list[1] if len(arg_list)>1 else parentName # create layout and set grp layout grp_layout = self.quickLayout(grp_layout, parentName+"_layout" ) self.uiList[parentName] = QtWidgets.QGroupBox(grp_title) self.uiList[parentName].setLayout(grp_layout) parentObject = self.uiList[parentName] # 3. get parentLayout inside parentObject parentLayout = '' if isinstance(parentObject, QtWidgets.QLayout): parentLayout = parentObject elif isinstance(parentObject, QtWidgets.QGroupBox): parentLayout = parentObject.layout() # 3.1 insert part_list into parentLayout for layout and groupbox if isinstance(parentLayout, QtWidgets.QBoxLayout): for each_ui in ui_list: if isinstance(each_ui, QtWidgets.QWidget): parentLayout.addWidget(each_ui) elif isinstance(each_ui, QtWidgets.QSpacerItem): parentLayout.addItem(each_ui) elif isinstance(each_ui, QtWidgets.QLayout): parentLayout.addLayout(each_ui) elif isinstance(parentLayout, QtWidgets.QGridLayout): # one row/colume operation only insertRow = parentLayout.rowCount() insertCol = parentLayout.columnCount() for i in range(len(ui_list)): each_ui = ui_list[i] x = insertRow if insert_opt=="h" else i y = i if insert_opt=="h" else insertCol if isinstance(each_ui, QtWidgets.QWidget): parentLayout.addWidget(each_ui,x,y) elif isinstance(each_ui, QtWidgets.QSpacerItem): parentLayout.addItem(each_ui,x,y) elif isinstance(each_ui, QtWidgets.QLayout): parentLayout.addLayout(each_ui,x,y) elif isinstance(parentLayout, QtWidgets.QFormLayout): for i in range(len(ui_list)): each_ui = ui_list[i] if isinstance(each_ui, QtWidgets.QWidget) or isinstance(each_ui, QtWidgets.QLayout): # create and add label: (uiName, uiLabel) if ui_label_list[i] != '': uiLabelName = ui_label_list[i][0] + "_label" uiLabelText = ui_label_list[i][1] self.uiList[uiLabelName] = QtWidgets.QLabel(uiLabelText) parentLayout.addRow(self.uiList[uiLabelName], each_ui) else: parentLayout.addRow(each_ui) else: # 3.2 insert for empty parentLayout for split, and tab if isinstance(parentObject, QtWidgets.QSplitter): for each_ui in ui_list: if isinstance(each_ui, QtWidgets.QWidget): parentObject.addWidget(each_ui) else: tmp_holder = QtWidgets.QWidget() tmp_holder.setLayout(each_ui) parentObject.addWidget(tmp_holder) elif isinstance(parentObject, QtWidgets.QTabWidget): tab_names = insert_opt.replace('(','').replace(')','').split(',') for i in range( len(ui_list) ): each_tab = ui_list[i] each_name = 'tab_'+str(i) if i < len(tab_names): if tab_names[i] != '': each_name = tab_names[i] if isinstance(each_tab, QtWidgets.QWidget): parentObject.addTab(each_tab, each_name) else: tmp_holder = QtWidgets.QWidget() tmp_holder.setLayout(each_tab) parentObject.addTab(tmp_holder, each_name) return parentObject def quickSplitUI(self, name, part_list, type): split_type = QtCore.Qt.Horizontal if type == 'v': split_type = QtCore.Qt.Vertical self.uiList[name]=QtWidgets.QSplitter(split_type) for each_part in part_list: if isinstance(each_part, QtWidgets.QWidget): self.uiList[name].addWidget(each_part) else: tmp_holder = QtWidgets.QWidget() tmp_holder.setLayout(each_part) self.uiList[name].addWidget(tmp_holder) return self.uiList[name] def quickTabUI(self, name, tab_list, tab_names): self.uiList[name]=QtWidgets.QTabWidget() self.uiList[name].setStyleSheet("QTabWidget::tab-bar{alignment:center;}QTabBar::tab { min-width: 100px; }") for i in range( len(tab_list) ): each_tab = tab_list[i] each_name = tab_names[i] if isinstance(each_tab, QtWidgets.QWidget): self.uiList[name].addTab(each_tab, each_name) else: tmp_holder = QtWidgets.QWidget() tmp_holder.setLayout(each_tab) self.uiList[name].addTab(tmp_holder, each_name) return self.uiList[name] def quickGrpUI(self, ui_name, ui_label, ui_layout): self.uiList[ui_name] = QtWidgets.QGroupBox(ui_label) if isinstance(ui_layout, QtWidgets.QLayout): self.uiList[ui_name].setLayout(ui_layout) elif isinstance(ui_layout, str): ui_layout = self.quickLayout(ui_name+"_layout", ui_layout) self.uiList[ui_name].setLayout(ui_layout) return [self.uiList[ui_name], ui_layout] def quickPolicy(self, ui_list, w, h): if not isinstance(ui_list, (list, tuple)): ui_list = [ui_list] # 0 = fixed; 1 > min; 2 < max; 3 = prefered; 4 = <expanding>; 5 = expanding> Aggresive; 6=4 ignored size input policyList = ( QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Ignored) for each_ui in ui_list: if isinstance(each_ui, str): each_ui = self.uiList[each_ui] each_ui.setSizePolicy(policyList[w],policyList[h]) def mui_to_qt(self, mui_name): if hostMode != "maya": return ptr = mui.MQtUtil.findControl(mui_name) if ptr is None: ptr = mui.MQtUtil.findLayout(mui_name) if ptr is None: ptr = mui.MQtUtil.findMenuItem(mui_name) if ptr is not None: if qtMode in (0,2): # ==== for pyside ==== return shiboken.wrapInstance(long(ptr), QtWidgets.QWidget) elif qtMode in (1,3): # ==== for PyQt==== return sip.wrapinstance(long(ptr), QtCore.QObject) def qt_to_mui(self, qt_obj): if hostMode != "maya": return ref = None if qtMode in (0,2): # ==== for pyside ==== ref = long(shiboken.getCppPointer(qt_obj)[0]) elif qtMode in (1,3): # ==== for PyQt==== ref = long(sip.unwrapinstance(qt_obj)) if ref is not None: return mui.MQtUtil.fullName(ref) #======================================= # widget specific functions #======================================= def ____TreeWidget_Process_Functions____(): pass def path_pattern_to_task(self, path_pattern): # break config text into section of sub-directory search task # each task: 'sub_directory_path_to/content_list', 'content_dir_variable_name' # also, 'content_dir_variable_name' also is the key to its filter pattern # example: [('/VFX/assets/models', 'category'), ('', 'asset'), ('/Mesh/publish', 'model_file')]) part_list = path_pattern.split('/') task_config = [] task_pattern = re.compile('{.+}') # grab variable name in path_pattern with {variable} format sub = '' for each in part_list: if task_pattern.match(each): task_config.append( (sub,each[1:-1]) ) sub = '' else: sub=sub+'/'+each return task_config def getPathChild(self, scanPath, pattern='', isfile=0): resultList =[] if not os.path.isdir(scanPath): return resultList if isfile == 0: resultList = [x for x in os.listdir(scanPath) if os.path.isdir(os.path.join(scanPath,x))] elif isfile == 1: resultList = [x for x in os.listdir(scanPath) if os.path.isfile(os.path.join(scanPath,x))] else: resultList = os.listdir(scanPath) if pattern != '': cur_pattern = re.compile(pattern) resultList = [x for x in resultList if cur_pattern.match(x)] resultList.sort() return resultList def path_info(self, scanPath, file_pattern='', folder_pattern='', file=0, folder=0): # alternative method of getPathChild file_list = [] folder_list = [] # prepare filter cur_file_pattern = None if file_pattern != '': cur_file_pattern = file_pattern # re object if isinstance(file_pattern, (unicode,str)): cur_file_pattern = re.compile(file_pattern) cur_folder_pattern = None if folder_pattern != '': cur_folder_pattern = folder_pattern # re object if isinstance(folder_pattern, (unicode,str)): cur_folder_pattern = re.compile(folder_pattern) # category file and folder for x in os.listdir(scanPath): if os.path.isdir(os.path.join(scanPath,x)): folder_list.append(x) elif os.path.isfile(os.path.join(scanPath,x)): file_list.append(x) file_list.sort() folder_list.sort() # filter result result = [] if file == 1: if cur_file_pattern is None: result.append(file_list) else: result.append( [x for x in file_list if cur_file_pattern.match(x)] ) if folder == 1: if cur_folder_pattern is None: result.append(folder_list) else: result.append( [x for x in folder_list if cur_folder_pattern.match(x)] ) if len(result) == 1: result = result[0] return result def DirToData(self, scanPath, task_config, pattern_config, currentTag=''): ''' [ node_info node_child ] ''' if not isinstance(task_config, (tuple, list)): return ( [], [] ) else: if len(task_config)== 0: return ( [], [] ) task_list = task_config # 1. get path if at least 1 task cur_task = task_list[0] rest_task = [] if len(task_list)==1 else task_list[1:] scanPath = scanPath.replace('\\','/') if cur_task[0] != '': scanPath = scanPath+cur_task[0] # note join path with /startswith/ will goto top path if not os.path.isdir(scanPath): print('Error: path not exists: {}'.format(scanPath)) return ( [], [] ) # 2. get list and filter list cur_pattern = '' if cur_task[1] not in pattern_config.keys() else pattern_config[cur_task[1]] isfile = 0 # folder only if cur_task[1].endswith('_file'): isfile = 1 # file only if cur_task[1].endswith('_all'): isfile = 2 # folder and file node_name = os.path.basename(scanPath) node_info = ['', '', scanPath ] if currentTag == '' else [node_name, currentTag, scanPath ] node_info_child = [] parentTag = currentTag for each_name in self.getPathChild(scanPath, cur_pattern, isfile): cur_path = os.path.join(scanPath, each_name).replace('\\','/') cur_tag = each_name if parentTag == '' else parentTag+':'+each_name if os.path.isdir(cur_path): if len(rest_task) > 0: # go next level task node_info_child.append( self.DirToData(cur_path, rest_task, pattern_config, cur_tag) ) else: node_info_child.append( ( [os.path.basename(cur_path), cur_tag, cur_path ], [] ) ) else: node_info_child.append( ( [os.path.basename(cur_path), '', cur_path ], [] ) ) return (node_info, node_info_child) def DirToTree(self, cur_tree, parentNode, scanPath, task_config, pattern_config): if not isinstance(task_config, (tuple, list)): return else: if len(task_config)== 0: return task_list = task_config # 1. get path if at least 1 task cur_task = task_list[0] rest_task = [] if len(task_list)==1 else task_list[1:] scanPath = scanPath.replace('\\','/') if cur_task[0] != '': # because join path with /startswith/ will goto top path scanPath = scanPath+cur_task[0] if not os.path.isdir(scanPath): print('Error: path not exists: {}'.format(scanPath)) return # 2. get list and filter list cur_pattern = '' if cur_task[1] not in pattern_config.keys() else pattern_config[cur_task[1]] isfile = 0 # folder only if cur_task[1].endswith('_file'): isfile = 1 # file only if cur_task[1].endswith('_all'): isfile = 2 # folder and file child_list = self.getPathChild(scanPath, cur_pattern, isfile) node_list = {} # 3. create node in normal style parentNode_info = unicode(parentNode.text(1)) if isfile == 2: group_dict = {} for each_name in child_list: if os.path.isdir(os.path.join(scanPath, each_name)): new_node = QtWidgets.QTreeWidgetItem() new_node.setText(0, each_name) new_node.setText(2, os.path.join(scanPath,each_name).replace('\\','/') ) parentNode.addChild(new_node) node_list[each_name]=new_node else: prefix, ext = os.path.splitext(each_name) # file type fileType = ext[1:] # file version version_txt = "" possible_version_list = re.findall(r'_v([\d]+)[_\.]', each_name) # last _v999.ext or _v999_xxx.ext if len(possible_version_list) > 0: version_txt = possible_version_list[-1] # file prefix if version_txt != "": prefix = each_name.rsplit("_v"+version_txt, 1)[0] # file group group_name = prefix+':'+fileType if group_name not in group_dict.keys(): group_dict[group_name] = [] group_dict[group_name].append(each_name) # add group node first for group_name in sorted(group_dict.keys()): group_dict[group_name].sort(reverse=1) group_item_list = group_dict[group_name] fileType = group_name.split(':')[1] group_node = QtWidgets.QTreeWidgetItem() group_node_top_name = group_item_list[0] cur_filePath = os.path.join(scanPath,group_node_top_name).replace("\\","/") group_node.setText(0, group_node_top_name) group_node.setText(1, fileType) group_node.setText(2, cur_filePath) parentNode.addChild(group_node) # add sub version to the tree if len(group_item_list) == 1: node_list[group_node_top_name]=group_node if len(group_item_list) > 1: for each_name in group_item_list: sub_node = QtWidgets.QTreeWidgetItem() cur_filePath = os.path.join(scanPath,each_name).replace("\\","/") sub_node.setText(0, each_name) sub_node.setText(1, fileType) sub_node.setText(2, cur_filePath) group_node.addChild(sub_node) node_list[each_name]=sub_node elif isfile == 0: for each_name in child_list: new_node = QtWidgets.QTreeWidgetItem() new_node.setText(0, each_name) if parentNode_info == '': new_node.setText(1, each_name) else: new_node.setText(1, parentNode_info+':'+each_name) new_node.setText(2, os.path.join(scanPath,each_name).replace('\\','/') ) parentNode.addChild(new_node) node_list[each_name]=new_node elif isfile == 1: # 3. create node in combine style #-- group similar group_dict = {} for each_name in child_list: prefix, ext = os.path.splitext(each_name) # file type fileType = ext[1:] # file version version_txt = "" possible_version_list = re.findall(r'_v([\d]+)[_\.]', each_name) # last _v999.ext or _v999_xxx.ext if len(possible_version_list) > 0: version_txt = possible_version_list[-1] # file prefix if version_txt != "": prefix = each_name.rsplit("_v"+version_txt, 1)[0] # file group group_name = prefix+':'+fileType if group_name not in group_dict.keys(): group_dict[group_name] = [] group_dict[group_name].append(each_name) # add group node first for group_name in sorted(group_dict.keys()): group_dict[group_name].sort(reverse=1) group_item_list = group_dict[group_name] fileType = group_name.split(':')[1] group_node = QtWidgets.QTreeWidgetItem() group_node_top_name = group_item_list[0] cur_filePath = os.path.join(scanPath,group_node_top_name).replace("\\","/") group_node.setText(0, group_node_top_name) group_node.setText(1, fileType) group_node.setText(2, cur_filePath) parentNode.addChild(group_node) # add sub version to the tree if len(group_item_list) == 1: node_list[group_node_top_name]=group_node if len(group_item_list) > 1: for each_name in group_item_list: sub_node = QtWidgets.QTreeWidgetItem() cur_filePath = os.path.join(scanPath,each_name).replace("\\","/") sub_node.setText(0, each_name) sub_node.setText(1, fileType) sub_node.setText(2, cur_filePath) group_node.addChild(sub_node) node_list[each_name]=sub_node # go next level task if len(rest_task) > 0: for each_name in child_list: cur_parentPath = os.path.join(scanPath, each_name).replace('\\', '/') if os.path.isdir(cur_parentPath): self.DirToTree(cur_tree, node_list[each_name], cur_parentPath, rest_task, pattern_config) def TreeToData(self, tree, cur_node): # now take widghet col count instead tree column count with hidden ones child_count = cur_node.childCount() node_info = [ unicode( cur_node.text(i) ) for i in range(cur_node.columnCount()) ] node_info_child = [] for i in range(child_count): node_info_child.append( self.TreeToData(tree, cur_node.child(i) ) ) return (node_info, node_info_child) def DataToTree(self, tree, cur_node, data, filter='', col=0): node_info = data[0] node_info_child = data[1] [cur_node.setText(i, node_info[i]) for i in range(len(node_info))] # re filter if filter != '' and isinstance(filter, (str, unicode)): filter = re.compile(filter, re.IGNORECASE) for sub_data in node_info_child: if filter == '': new_node = QtWidgets.QTreeWidgetItem() cur_node.addChild(new_node) self.DataToTree(tree, new_node, sub_data) else: if not filter.search(sub_data[0][col]) and not self.DataChildCheck(sub_data[1], filter, col): pass else: new_node = QtWidgets.QTreeWidgetItem() cur_node.addChild(new_node) new_node.setExpanded(1) self.DataToTree(tree, new_node, sub_data, filter, col) def DataChildCheck(self, DataChild, filter, col): ok_cnt = 0 if isinstance(filter, (str, unicode)): filter = re.compile(filter, re.IGNORECASE) for sub_data in DataChild: if filter.search(sub_data[0][col]) or self.DataChildCheck(sub_data[1], filter, col): ok_cnt +=1 return ok_cnt def TreeExport(self, tree_name, file): # export process ui_data = self.TreeToData(self.uiList[tree_name], self.uiList[tree_name].invisibleRootItem()) # file process if file.endswith('.dat'): self.writeDataFile(ui_data, file, binary=1) else: self.writeDataFile(ui_data, file) self.quickInfo("File: '"+file+"' creation finished.") def TreeImport(self, tree_name, file): # import process ui_data = "" if file.endswith('.dat'): ui_data = self.readDataFile(file, binary=1) else: ui_data = self.readDataFile(file) self.uiList['dir_tree'].clear() self.DataToTree(self.uiList['dir_tree'], self.uiList['dir_tree'].invisibleRootItem(), ui_data) self.quickInfo("File: '"+file+"' loading finished.") # tree operation -- tmp v1.0 def quickTree(self, cur_node, node_data, editable=0): # not per-column control on editable if not isinstance(node_data, (list, tuple)): node_data = [node_data] # 1. create a new node new_node = QtWidgets.QTreeWidgetItem() for i,name in enumerate(node_data): new_node.setText(i, name) if editable == 1: new_node.setFlags(new_node.flags()|QtCore.Qt.ItemIsEditable) # 2. add it cur_node.addChild(new_node) # 3. expand it cur_node.setExpanded(1) def quickTreeRemove(self, cur_tree): root = cur_tree.invisibleRootItem() for item in cur_tree.selectedItems(): (item.parent() or root).removeChild(item) # -- end tree operation def cache_tree(self, cur_tree_name, force=1): cur_tree = self.uiList[cur_tree_name] if 'cache' not in self.memoData: self.memoData['cache'] = {} if force == 1: self.memoData['cache'][cur_tree_name] = self.TreeToData(cur_tree, cur_tree.invisibleRootItem()) else: if cur_tree_name not in self.memoData['cache']: self.memoData['cache'][cur_tree_name] = self.TreeToData(cur_tree, cur_tree.invisibleRootItem()) def filter_tree(self, cur_tree_name, word): self.filter_tree_col(cur_tree_name, 0, word) def filter_tree_col(self, cur_tree_name, col, word): word = unicode(word) cur_tree = self.uiList[cur_tree_name] parentNode = cur_tree.invisibleRootItem() # read cache, if no cache, create cache self.cache_tree(cur_tree_name, force = 0) # filter and show, reset back to cache cur_tree.clear() if word != '': self.DataToTree(cur_tree, parentNode, self.memoData['cache'][cur_tree_name], filter=word, col=col) else: self.DataToTree(cur_tree, parentNode, self.memoData['cache'][cur_tree_name]) ############################################# # User Class creation ############################################# version = '0.1' date = '2017.01.01' log = ''' #------------------------------ # How to Use: # 1. global replace class name "UserClassUI" to "YourToolName" in your editor, # - in icons folder, the Tool GUI icon should name as "YourToolName.png" # 2. change file name "universal_tool_template.py" to "YourPythonFileName.py", # - in icons folder, the Maya shelf icon should name as "YourPythonFileName.png", if you name all name the same, then 1 icon is enough # 3. load it up and run #------------------------------ ''' help = ''' # loading template - Run in python panel myPath='/path_to_universal_tool_or_custom_name/' import sys;myPath in sys.path or sys.path.append(myPath); import universal_tool_template universal_tool_template.main() # loading template - Run in system command console python universal_tool_template.py ''' # -------------------- # user module list # -------------------- class UserClassUI(UniversalToolUI): def __init__(self, parent=None, mode=0): UniversalToolUI.__init__(self, parent) # class variables self.version= version self.date = date self.log = log self.help = help # mode: example for receive extra user input as parameter self.mode = 0 if mode in [0,1]: self.mode = mode # mode validator # Custom user variable #------------------------------ # initial data #------------------------------ self.memoData['data']=[] self.memoData['settingUI']=[] self.qui_user_dict = {} # e.g: 'edit': 'LNTextEdit', self.setupStyle() if isinstance(self, QtWidgets.QMainWindow): self.setupMenu() self.setupWin() self.setupUI() self.Establish_Connections() self.loadLang() self.loadData() #------------------------------ # overwrite functions #------------------------------ def setupMenu(self): self.qui_menubar('file_menu;&File | setting_menu;&Setting | help_menu;&Help') info_list = ['export', 'import','user'] info_item_list = ['{0}Config_atn;{1} Config (&{2}),Ctrl+{2}'.format(info,info.title(),info.title()[0]) for info in info_list]+['_'] self.qui_menu('|'.join(info_item_list), 'setting_menu') # toggle on top self.qui_menu('toggleTop_atn;Toggle Always-On-Top', 'setting_menu') super(self.__class__,self).setupMenu() def setupWin(self): super(self.__class__,self).setupWin() self.setGeometry(500, 300, 250, 110) # self.resize(250,250) def setupUI(self): super(self.__class__,self).setupUI('grid') #------------------------------ # user ui creation part #------------------------------ # + template: qui version since universal tool template v7 # - no extra variable name, all text based creation and reference self.qui('box_btn;Box | sphere_btn;Sphere | ring_btn;Ring', 'my_layout;grid', 'h') self.qui('box2_btn;Box2 | sphere2_btn;Sphere2 | ring2_btn;Ring2', 'my_layout', 'h') self.qui('cat_btn;Cat | dog_btn;Dog | pig_btn;Pig', 'pet_layout;grid', 'v') self.qui('cat2_btn;Cat2 | dog2_btn;Dog2 | pig2_btn;Pig2', 'pet_layout', 'v') self.qui('name_input@Name:;John | email_input@Email:;test@test.com', 'entry_form') self.qui('user2_btn;User2 | info2_btn;Info2', 'my_grp;vbox,Personal Data') self.qui('source_txt | process_btn;Process and Update', 'upper_vbox') self.qui('upper_vbox | result_txt', 'input_split;v') self.qui('filePath_input | fileLoad_btn;Load | fileExport_btn;Export', 'fileBtn_layout;hbox') self.qui('my_layout | my_table | input_split | entry_form | fileBtn_layout | pet_layout | my_grp', 'main_layout') cur_table = self.uiList['my_table'] cur_table.setRowCount(0) cur_table.setColumnCount(1) cur_table.insertColumn(cur_table.columnCount()) cur_item = QtWidgets.QTableWidgetItem('ok') #QtWidgets.QPushButton('Cool') # cur_table.insertRow(0) cur_table.setItem(0,1, cur_item) #setCellWidget(0,0,cur_item) cur_table.setHorizontalHeaderLabels(('a','b')) ''' self.qui('source_txt | process_btn;Process and Update', 'upper_vbox') self.qui('upper_vbox | result_txt', 'input_split;v') self.qui('filePath_input | fileLoad_btn;Load | fileExport_btn;Export', 'fileBtn_layout;hbox') self.qui('input_split | fileBtn_layout', 'main_layout') ''' self.memoData['settingUI']=[] #------------- end ui creation -------------------- keep_margin_layout = ['main_layout'] keep_margin_layout_obj = [] # add tab layouts for each in self.uiList.values(): if isinstance(each, QtWidgets.QTabWidget): for i in range(each.count()): keep_margin_layout_obj.append( each.widget(i).layout() ) for name, each in self.uiList.items(): if isinstance(each, QtWidgets.QLayout) and name not in keep_margin_layout and not name.endswith('_grp_layout') and each not in keep_margin_layout_obj: each.setContentsMargins(0, 0, 0, 0) self.quickInfo('Ready') # self.statusBar().hide() def Establish_Connections(self): super(self.__class__,self).Establish_Connections() # custom ui response # shortcut connection self.hotkey = {} # self.hotkey['my_key'] = QtWidgets.QShortcut(QtGui.QKeySequence( "Ctrl+1" ), self) # self.hotkey['my_key'].activated.connect(self.my_key_func) # ---- user response list ---- def loadData(self): print("Load data") # load config config = {} config['root_name'] = 'root_default_name' # overload config file if exists next to it # then, save merged config into self.memoData['config'] prefix, ext = os.path.splitext(self.location) config_file = prefix+'_config.json' if os.path.isfile(config_file): external_config = self.readDataFile(config_file) print('info: External config file found.') if isinstance( external_config, dict ): self.memoData['config'] = self.dict_merge(config, external_config, addKey=1) print('info: External config merged.') else: self.memoData['config'] = config print('info: External config is not a dict and ignored.') else: self.memoData['config'] = config # load user setting user_setting = {} if self.mode == 0: # for standalone mode only user_dirPath = os.path.join(os.path.expanduser('~'), 'Tool_Config', self.__class__.__name__) user_setting_filePath = os.path.join(user_dirPath, 'setting.json') if os.path.isfile(user_setting_filePath): user_setting = self.readDataFile(user_setting_filePath) if 'sizeInfo' in user_setting: self.setGeometry(*user_setting['sizeInfo']) # custome setting loading here preset = {} for ui in self.memoData['settingUI']: if ui in user_setting: preset[ui]=user_setting[ui] #self.updateUI(preset) def closeEvent(self, event): if self.mode == 0: # for standalone mode only user_dirPath = os.path.join(os.path.expanduser('~'), 'Tool_Config', self.__class__.__name__) if not os.path.isdir(user_dirPath): try: os.makedirs(user_dirPath) except OSError: print('Error on creation user data folder') if not os.path.isdir(user_dirPath): print('Fail to create user dir.') return # save setting user_setting = {} geoInfo = self.geometry() user_setting['sizeInfo'] = [geoInfo.x(), geoInfo.y(), geoInfo.width(), geoInfo.height()] # custome setting saving here for ui in self.memoData['settingUI']: if ui.endswith('_choice'): user_setting[ui] = unicode(self.uiList[ui].currentText()) elif ui.endswith('_input'): user_setting[ui] = unicode(self.uiList[ui].text()) user_setting_filePath = os.path.join(user_dirPath, 'setting.json') self.writeDataFile(user_setting, user_setting_filePath) # - example button functions def updateUI(self,preset={}): show_list = [] # update based on setting preset value cur_preset_ui = 'root_choice' if cur_preset_ui in preset: if preset[cur_preset_ui] != '' and preset[cur_preset_ui] in show_list: self.uiList[cur_preset_ui].setCurrentIndex(show_list.index(preset[cur_preset_ui])) def process_action(self): # (optional) config = self.memoData['config'] print("Process ....") source_txt = unicode(self.uiList['source_txt'].toPlainText()) # 2: update memory self.memoData['data'] = [row.strip() for row in source_txt.split('\n')] print("Update Result") txt=config['root_name']+'\n'+'\n'.join([('>>: '+row) for row in self.memoData['data']]) self.uiList['result_txt'].setText(txt) # - example file io function def exportConfig_action(self): file= self.quickFileAsk('export', {'json':'JSON data file', 'xdat':'Pickle binary file'}) if file == "": return # export process ui_data = self.memoData['config'] # file process if file.endswith('.xdat'): self.writeDataFile(ui_data, file, binary=1) else: self.writeDataFile(ui_data, file) self.quickInfo("File: '"+file+"' creation finished.") def importConfig_action(self): file= self.quickFileAsk('import',{'json':'JSON data file', 'xdat':'Pickle binary file'}) if file == "": return # import process ui_data = "" if file.endswith('.xdat'): ui_data = self.readDataFile(file, binary=1) else: ui_data = self.readDataFile(file) self.memoData['config'] = ui_data self.quickInfo("File: '"+file+"' loading finished.") def userConfig_action(self): user_dirPath = os.path.join(os.path.expanduser('~'), 'Tool_Config', self.__class__.__name__) self.openFolder(user_dirPath) #======================================= # window instance creation #======================================= single_UserClassUI = None app_UserClassUI = None def main(mode=0): # get parent window in Maya parentWin = None if hostMode == "maya": if qtMode in (0,2): # pyside parentWin = shiboken.wrapInstance(long(mui.MQtUtil.mainWindow()), QtWidgets.QWidget) elif qtMode in (1,3): # PyQt parentWin = sip.wrapinstance(long(mui.MQtUtil.mainWindow()), QtCore.QObject) # create app object for certain host global app_UserClassUI if hostMode in ('desktop', 'blender', 'npp', 'fusion'): # single instance app mode on windows if osMode == 'win': # check if already open for single desktop instance from ctypes import wintypes order_list = [] result_list = [] top = ctypes.windll.user32.GetTopWindow(None) if top: length = ctypes.windll.user32.GetWindowTextLengthW(top) buff = ctypes.create_unicode_buffer(length + 1) ctypes.windll.user32.GetWindowTextW(top, buff, length + 1) class_name = ctypes.create_string_buffer(200) ctypes.windll.user32.GetClassNameA(top, ctypes.byref(class_name), 200) result_list.append( [buff.value, class_name.value, top ]) order_list.append(top) while True: next = ctypes.windll.user32.GetWindow(order_list[-1], 2) # win32con.GW_HWNDNEXT if not next: break length = ctypes.windll.user32.GetWindowTextLengthW(next) buff = ctypes.create_unicode_buffer(length + 1) ctypes.windll.user32.GetWindowTextW(next, buff, length + 1) class_name = ctypes.create_string_buffer(200) ctypes.windll.user32.GetClassNameA(next, ctypes.byref(class_name), 200) result_list.append( [buff.value, class_name.value, next] ) order_list.append(next) # result_list: [(title, class, hwnd int)] winTitle = 'UserClassUI' # os.path.basename(os.path.dirname(__file__)) is_opened = 0 for each in result_list: if re.match(winTitle+' - v[0-9.]* - host: desktop',each[0]) and each[1] == 'QWidget': is_opened += 1 if is_opened == 1: ctypes.windll.user32.SetForegroundWindow(each[2]) return if hostMode in ('npp','fusion'): app_UserClassUI = QtWidgets.QApplication([]) elif hostMode in ('houdini'): pass else: app_UserClassUI = QtWidgets.QApplication(sys.argv) #-------------------------- # ui instance #-------------------------- # template 1 - Keep only one copy of windows ui in Maya global single_UserClassUI if single_UserClassUI is None: if hostMode == 'maya': single_UserClassUI = UserClassUI(parentWin, mode) elif hostMode == 'nuke': single_UserClassUI = UserClassUI(QtWidgets.QApplication.activeWindow(), mode) else: single_UserClassUI = UserClassUI() # extra note: in Maya () for no parent; (parentWin,0) for extra mode input single_UserClassUI.show() ui = single_UserClassUI if hostMode != 'desktop': ui.activateWindow() # template 2 - allow loading multiple windows of same UI in Maya ''' if hostMode == "maya": ui = UserClassUI(parentWin) ui.show() else: pass # extra note: in Maya () for no parent; (parentWin,0) for extra mode input ''' # loop app object for certain host if hostMode in ('desktop'): sys.exit(app_UserClassUI.exec_()) elif hostMode in ('npp','fusion'): app_UserClassUI.exec_() return ui if __name__ == "__main__": main()