diff --git a/W_hotbox.py b/W_hotbox.py old mode 100755 new mode 100644 index fe6350d..74e4c84 --- a/W_hotbox.py +++ b/W_hotbox.py @@ -1,13 +1,11 @@ #---------------------------------------------------------------------------------------------------------- # Wouter Gilsing # woutergilsing@hotmail.com -version = '1.7' -releaseDate = 'June 26 2017' +version = '1.8' +releaseDate = 'April 23 2018' #---------------------------------------------------------------------------------------------------------- -# #LICENSE -# #---------------------------------------------------------------------------------------------------------- ''' @@ -36,6 +34,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ''' +#---------------------------------------------------------------------------------------------------------- +#modules #---------------------------------------------------------------------------------------------------------- import nuke @@ -60,15 +60,16 @@ #---------------------------------------------------------------------------------------------------------- -class hotbox(QtWidgets.QWidget): +class Hotbox(QtWidgets.QWidget): ''' The main class for the hotbox ''' def __init__(self, subMenuMode = False, path = '', name = '', position = ''): - super(hotbox, self).__init__() + super(Hotbox, self).__init__() self.active = True + self.activeButton = None self.triggerMode = preferencesNode.knob('hotboxTriggerDropdown').getValue() @@ -78,25 +79,25 @@ def __init__(self, subMenuMode = False, path = '', name = '', position = ''): self.setAttribute(QtCore.Qt.WA_TranslucentBackground) #enable transparency on Linux - - if operatingSystem not in ['Darwin','Windows']: + if operatingSystem not in ['Darwin','Windows'] and nuke.NUKE_VERSION_MAJOR < 11: self.setAttribute(QtCore.Qt.WA_PaintOnScreen) masterLayout = QtWidgets.QVBoxLayout() self.setLayout(masterLayout) - self.selection = nuke.selectedNodes() - + #-------------------------------------------------------------------------------------------------- + #context + #-------------------------------------------------------------------------------------------------- + + self.selection = nuke.selectedNodes() #check whether selection in group self.groupRoot = 'root' - if len(self.selection) != 0: + if self.selection: nodeRoot = self.selection[0].fullName() - if nodeRoot.count('.') > 0: - self.groupRoot = '.'.join(nodeRoot.split('.')[:-1]) - - self.activeButton = None + if nodeRoot.count('.'): + self.groupRoot = '.'.join([self.groupRoot] + nodeRoot.split('.')[:-1]) #-------------------------------------------------------------------------------------------------- #main hotbox @@ -104,33 +105,29 @@ def __init__(self, subMenuMode = False, path = '', name = '', position = ''): if not subMenuMode: - if len(self.selection) > 1: + self.mode = 'Single' - if len(list(set([i.Class() for i in nuke.selectedNodes()]))) == 1: - self.mode = 'Single' - else: + if len(self.selection) > 1: + if len(list(set([node.Class() for node in nuke.selectedNodes()]))) > 1: self.mode = 'Multiple' - else: - self.mode = 'Single' - #Layouts centerLayout = QtWidgets.QHBoxLayout() - centerLayout.addStretch() - centerLayout.addWidget(hotboxButton('Reveal in %s'%getFileBrowser(),'revealInBrowser()')) + centerLayout.addWidget(HotboxButton('Reveal in %s'%getFileBrowser(),'revealInBrowser()')) centerLayout.addSpacing(25) - centerLayout.addWidget(hotboxCenter()) + centerLayout.addWidget(HotboxCenter()) centerLayout.addSpacing(25) - centerLayout.addWidget(hotboxButton('Hotbox Manager','showHotboxManager()')) + centerLayout.addWidget(HotboxButton('Hotbox Manager','showHotboxManager()')) centerLayout.addStretch() - self.topLayout = nodeButtons() - self.bottomLayout = nodeButtons('bottom') + self.topLayout = NodeButtons() + self.bottomLayout = NodeButtons('bottom') + spacing = 12 #-------------------------------------------------------------------------------------------------- - #submenu + #submenu mode #-------------------------------------------------------------------------------------------------- else: @@ -147,23 +144,23 @@ def __init__(self, subMenuMode = False, path = '', name = '', position = ''): else: lists[index%2].insert(0,item) - #Stretch layout centerLayout = QtWidgets.QHBoxLayout() centerLayout.addStretch() for index, item in enumerate(centerItems): - centerLayout.addWidget(hotboxButton(item)) + centerLayout.addWidget(HotboxButton(item)) if index == 0: - centerLayout.addWidget(hotboxCenter(False,path)) + centerLayout.addWidget(HotboxCenter(False,path)) if len(centerItems) == 1: centerLayout.addSpacing(105) centerLayout.addStretch() - self.topLayout = nodeButtons('SubMenuTop',lists[0]) - self.bottomLayout = nodeButtons('SubMenuBottom',lists[1]) + self.topLayout = NodeButtons('SubMenuTop',lists[0]) + self.bottomLayout = NodeButtons('SubMenuBottom',lists[1]) + spacing = 0 #-------------------------------------------------------------------------------------------------- @@ -214,6 +211,9 @@ def __init__(self, subMenuMode = False, path = '', name = '', position = ''): def closeHotbox(self, hotkey = False): #if the execute on close function is turned on, the hotbox will execute the selected button upon close + + + if hotkey: if preferencesNode.knob('hotboxExecuteOnClose').value(): if self.activeButton != None: @@ -257,15 +257,15 @@ def eventFilter(self, object, event): #Button field #---------------------------------------------------------------------------------------------------------- -class nodeButtons(QtWidgets.QVBoxLayout): +class NodeButtons(QtWidgets.QVBoxLayout): ''' Create QLayout filled with buttons ''' + def __init__(self, mode = '', allItems = ''): - super(nodeButtons, self).__init__() + super(NodeButtons, self).__init__() selectedNodes = nuke.selectedNodes() - mirrored = True #-------------------------------------------------------------------------------------------------- #submenu @@ -274,8 +274,7 @@ def __init__(self, mode = '', allItems = ''): if 'submenu' in mode.lower(): self.rowMaxAmount = 3 - if 'top' in mode.lower(): - mirrored = False + mirrored = ('top' not in mode.lower()) #-------------------------------------------------------------------------------------------------- #main hotbox @@ -283,6 +282,14 @@ def __init__(self, mode = '', allItems = ''): else: + mirrored = True + + mode = (mode == 'bottom') + + if preferencesNode.knob('hotboxMirroredLayout').value(): + mode = 1 - mode + mirrored = 1 - mirrored + self.path = preferencesNode.knob('hotboxLocation').value().replace('\\','/') if self.path[-1] != '/': self.path = self.path + '/' @@ -293,88 +300,161 @@ def __init__(self, mode = '', allItems = ''): self.folderList = [] - - if mode == 'bottom': + #---------------------------------------------------------------------------------------------- + #noncontextual + #---------------------------------------------------------------------------------------------- - for repository in self.allRepositories: - self.folderList.append(repository + 'All/') + if mode: - else: - mirrored = False - self.rowMaxAmount = int(preferencesNode.knob('hotboxRowAmountSelection').value()) + self.folderList += [repository + 'All' for repository in self.allRepositories] - nodeClasses = list(set([node.Class() for node in selectedNodes])) + #---------------------------------------------------------------------------------------------- + #contextual + #---------------------------------------------------------------------------------------------- + + else: - if len(nodeClasses) == 0: - nodeClasses = ['No Selection'] + mirrored = 1 - mirrored - else: - - #check if group, if so take the name of the group, as well as the class - groupNodes = [] - if 'Group' in nodeClasses: - for node in selectedNodes: - if node.Class() == 'Group': - groupName = node.name() - while groupName[-1] in [str(i) for i in range(10)]: - groupName = groupName[:-1] - if groupName not in groupNodes and groupName != 'Group': - groupNodes.append(groupName) + self.rowMaxAmount = int(preferencesNode.knob('hotboxRowAmountSelection').value()) - if len(groupNodes) > 0: - groupNodes = [nodeClass for nodeClass in nodeClasses if nodeClass != 'Group'] + groupNodes + #------------------------------------------------------------------------------------------ + #rules + #------------------------------------------------------------------------------------------ - if len(nodeClasses) > 1: - nodeClasses = [nodeClasses] - if len(groupNodes) > 1: - groupNodes = [groupNodes] + #collect all folders storing buttons for applicable rules - nodeClasses = nodeClasses + groupNodes + ignoreClasses = False + tag = '# IGNORE CLASSES: ' - ''' - Check which defined class combinations on disk are applicable to the current selection. - ''' + allRulePaths = [] for repository in self.allRepositories: - for nodeClass in nodeClasses: - if isinstance(nodeClass,list): + + rulesFolder = repository + 'Rules' + if not os.path.exists(rulesFolder): + continue + + rules = ['/'.join([rulesFolder,rule]) for rule in os.listdir(rulesFolder) if rule[0] not in ['_','.'] and rule[-1] != '_'] + + #validate rules + for rule in rules: + + ruleFile = rule + '/_rule.py' + + if os.path.exists(ruleFile): + + if self.validateRule(ruleFile): + allRulePaths.append(rule) + + #read ruleFile to check if ignoreClasses was enabled. + if not ignoreClasses: + + for line in open(ruleFile).readlines(): + #no point in checking boyond the header + if not line.startswith('#'): + break + #if proper tag is found, check its value + if line.startswith(tag): + ignoreClasses = bool(int(line.split(tag)[-1].replace('\n',''))) + break + + #------------------------------------------------------------------------------------------ + #classes + #------------------------------------------------------------------------------------------ + + #collect all folders storing buttons for applicable classes + + if not ignoreClasses: + + allClassPaths = [] + + nodeClasses = list(set([node.Class() for node in selectedNodes])) + + #if nothing selected + if len(nodeClasses) == 0: + nodeClasses = ['No Selection'] + + #if selection + else: + #check if group, if so take the name of the group, as well as the class + groupNodes = [] + if 'Group' in nodeClasses: + for node in selectedNodes: + if node.Class() == 'Group': + groupName = node.name() + while groupName[-1] in [str(i) for i in range(10)]: + groupName = groupName[:-1] + if groupName not in groupNodes and groupName != 'Group': + groupNodes.append(groupName) + + if len(groupNodes) > 0: + groupNodes = [nodeClass for nodeClass in nodeClasses if nodeClass != 'Group'] + groupNodes + + if len(nodeClasses) > 1: + nodeClasses = [nodeClasses] + if len(groupNodes) > 1: + groupNodes = [groupNodes] + + nodeClasses = nodeClasses + groupNodes + + #Check which defined class combinations on disk are applicable to the current selection. + for repository in self.allRepositories: + for nodeClass in nodeClasses: + + if isinstance(nodeClass, list): + for managerNodeClasses in [i for i in os.listdir(repository + 'Multiple') if i[0] not in ['_','.']]: + managerNodeClassesList = managerNodeClasses.split('-') + match = list(set(nodeClass).intersection(managerNodeClassesList)) + + if len(match) >= len(nodeClass): + allClassPaths.append(repository + 'Multiple/' + managerNodeClasses) + else: + allClassPaths.append(repository + 'Single/' + nodeClass) + + allClassPaths = list(set(allClassPaths)) + allClassPaths = [path for path in allClassPaths if os.path.exists(path)] + + #------------------------------------------------------------------------------------------ + #combine classes and rules + #------------------------------------------------------------------------------------------ + + if ignoreClasses: + self.folderList = allRulePaths - for managerNodeClasses in [i for i in os.listdir(repository + 'Multiple') if i[0] not in ['_','.']]: - managerNodeClassesList = managerNodeClasses.split('-') - match = list(set(nodeClass).intersection(managerNodeClassesList)) + else: + self.folderList = allClassPaths + allRulePaths - if len(match) >= len(nodeClass): - self.folderList.append(repository + 'Multiple/' + managerNodeClasses) - else: - self.folderList.append(repository + 'Single/' + nodeClass) + if preferencesNode.knob('hotboxRuleClassOrder').getValue(): + self.folderList.reverse() + + #---------------------------------------------------------------------------------------------- + #files on disk representing items + #---------------------------------------------------------------------------------------------- allItems = [] - self.folderList = list(set(self.folderList)) for folder in self.folderList: - #check if path exists - if os.path.exists(folder): - for i in sorted(os.listdir(folder)): - if i[0] not in ['.','_'] and len(i) in [3,6]: - if folder[-1] != '/': - folder += '/' - allItems.append(folder + i) + for file in sorted(os.listdir(folder)): + if file[0] not in ['.','_'] and len(file) in [3,6]: + allItems.append('/'.join([folder, file])) #-------------------------------------------------------------------------------------------------- #devide in rows based on the row maximum - + #-------------------------------------------------------------------------------------------------- + allRows = [] row = [] - for i in range(len(allItems)): - currentItem = allItems[i] + for item in allItems: if preferencesNode.knob('hotboxButtonSpawnMode').value(): if len(row) %2: - row.append(currentItem) + row.append(item) else: - row.insert(0,currentItem) + row.insert(0,item) else: - row.append(currentItem) + row.append(item) + #when a row reaches its full capacity, add the row to the allRows list #and start a new one. Increase rowcapacity to get a triangular shape if len(row) == self.rowMaxAmount: @@ -386,29 +466,65 @@ def __init__(self, mode = '', allItems = ''): if len(row) != 0: allRows.append(row) - if mirrored: - rows = allRows - else: - rows = allRows[::-1] + if not mirrored: + allRows.reverse() #nodeHotboxLayout - for row in rows: + for row in allRows: self.rowLayout = QtWidgets.QHBoxLayout() self.rowLayout.addStretch() for button in row: - buttonObject = hotboxButton(button) + buttonObject = HotboxButton(button) self.rowLayout.addWidget(buttonObject) self.rowLayout.addStretch() self.addLayout(self.rowLayout) - self.rowAmount = len(rows) + self.rowAmount = len(allRows) + + def validateRule(self, ruleFile): + ''' + Run the rule, return True or False. + ''' + + error = False + + #read from file + ruleString = open(ruleFile).read() + + #quick sanity check + if not 'ret=' in ruleString.replace(' ',''): + error = "RuleError: rule must contain variable named 'ret'" + + else: + + #prepend the rulestring with a nuke import statement and make it return False by default + prefix = 'import nuke\nret = False\n' + ruleString = prefix + ruleString + + #run rule + try: + results = {} + exec(ruleString, {}, results) + + if 'ret' in results.keys(): + result = bool(results['ret']) + except: + error = traceback.format_exc() + + #run error + if error: + printError(error, buttonName = os.path.basename(os.path.dirname(ruleFile)), rule = True) + result = False + + #return the result of the rule + return result #---------------------------------------------------------------------------------------------------------- -class hotboxCenter(QtWidgets.QLabel): +class HotboxCenter(QtWidgets.QLabel): ''' Center button of the hotbox. If the 'color nodes' is set to True in the preferences panel, the button will take over the color and @@ -417,7 +533,7 @@ class hotboxCenter(QtWidgets.QLabel): ''' def __init__(self, node = True, name = ''): - super ( hotboxCenter ,self ).__init__() + super(HotboxCenter, self).__init__() self.node = node @@ -427,7 +543,6 @@ def __init__(self, node = True, name = ''): selectedNodes = nuke.selectedNodes() if node: - #if no node selected if len(selectedNodes) == 0: name = 'W_hotbox' @@ -496,6 +611,9 @@ def enterEvent(self, event): return True def leaveEvent(self,event): + ''' + Change color of the button when the mouse starts hovering over it + ''' if not self.node: self.setSelectionStatus() return True @@ -512,14 +630,14 @@ def mouseReleaseEvent(self,event): #Buttons #---------------------------------------------------------------------------------------------------------- -class hotboxButton(QtWidgets.QLabel): +class HotboxButton(QtWidgets.QLabel): ''' Button class ''' def __init__(self, name, function = None): - super(hotboxButton, self).__init__() + super(HotboxButton, self).__init__() self.menuButton = False self.filePath = name @@ -613,59 +731,19 @@ def invokeButton(self): ''' Execute script attached to button ''' + with nuke.toNode(hotboxInstance.groupRoot): + try: exec self.function except: - self.printError(traceback.format_exc()) + printError(traceback.format_exc(), self.filePath, self.text()) #if 'close on click' is ticked, close the hotbox if not self.menuButton: if preferencesNode.knob('hotboxCloseOnClick').value() and preferencesNode.knob('hotboxTriggerDropdown').getValue(): hotboxInstance.closeHotbox() - def printError(self, error): - - fullError = error.splitlines() - - lineNumber = 'error determining line' - - for index, line in enumerate(reversed(fullError)): - if line.startswith(' File "<'): - - for i in line.split(','): - if i.startswith(' line '): - lineNumber = i - - index = len(fullError)-index - break - - fullError = fullError[index:] - - errorDescription = '\n'.join(fullError) - - scriptFolder = os.path.dirname(self.filePath) - scriptFolderName = os.path.basename(scriptFolder) - - buttonName = [self.text()] - - while len(scriptFolderName) == 3 and scriptFolderName.isdigit(): - - name = open(scriptFolder+'/_name.json').read() - buttonName.insert(0, name) - scriptFolder = os.path.dirname(scriptFolder) - scriptFolderName = os.path.basename(scriptFolder) - - for i in range(2): - buttonName.insert(0, os.path.basename(scriptFolder)) - scriptFolder = os.path.dirname(scriptFolder) - - hotboxError = '\nW_HOTBOX ERROR: %s -%s:\n\n%s'%('/'.join(buttonName),lineNumber,errorDescription) - - #print error - print hotboxError - nuke.tprint(hotboxError) - def setSelectionStatus(self, selected = False): ''' Define the style of the button for different states @@ -723,10 +801,12 @@ def mouseReleaseEvent(self,event): Execute the buttons' self.function (str) ''' if self.selected: + nuke.Undo().name(self.text()) nuke.Undo().begin() self.invokeButton() + nuke.Undo().end() return True @@ -796,181 +876,217 @@ def addPreferences(): Add knobs to the preferences needed for this module to work properly. ''' - homeFolder = os.getenv('HOME').replace('\\','/') + '/.nuke' - addToPreferences(nuke.Tab_Knob('hotboxLabel','W_hotbox')) addToPreferences(nuke.Text_Knob('hotboxGeneralLabel','General')) #version knob to check whether the hotbox was updated - versionKnob = nuke.String_Knob('hotboxVersion','version') - versionKnob.setValue(version) - addToPreferences(versionKnob) + knob = nuke.String_Knob('hotboxVersion','version') + knob.setValue(version) + addToPreferences(knob) preferencesNode.knob('hotboxVersion').setVisible(False) #location knob - locationKnob = nuke.File_Knob('hotboxLocation','Hotbox location') + knob = nuke.File_Knob('hotboxLocation','Hotbox location') tooltip = "The folder on disk the Hotbox uses to store the Hotbox buttons. Make sure this path links to the folder containing the 'All','Single' and 'Multiple' folders." - locationKnobAdded = addToPreferences(locationKnob, tooltip) - - if locationKnobAdded != None: - locationKnob.setValue(homeFolder + '/W_hotbox') + locationKnobAdded = addToPreferences(knob, tooltip) #icons knob - iconLocationKnob = nuke.File_Knob('hotboxIconLocation','Icons location') - iconLocationKnob.setValue(homeFolder +'/icons/W_hotbox') + knob = nuke.File_Knob('hotboxIconLocation','Icons location') + knob.setValue(homeFolder +'/icons/W_hotbox') tooltip = "The folder on disk the where the Hotbox related icons are stored. Make sure this path links to the folder containing the PNG files." - addToPreferences(iconLocationKnob, tooltip) + addToPreferences(knob, tooltip) #open manager button - openManagerKnob = nuke.PyScript_Knob('hotboxOpenManager','open hotbox manager','W_hotboxManager.showHotboxManager()') - openManagerKnob.setFlag(nuke.STARTLINE) + knob = nuke.PyScript_Knob('hotboxOpenManager','open hotbox manager','W_hotboxManager.showHotboxManager()') + knob.setFlag(nuke.STARTLINE) tooltip = "Open the Hotbox Manager." - addToPreferences(openManagerKnob, tooltip) + addToPreferences(knob, tooltip) #open in file system button knob - openFolderKnob = nuke.PyScript_Knob('hotboxOpenFolder','open hotbox folder','W_hotbox.revealInBrowser(True)') + knob = nuke.PyScript_Knob('hotboxOpenFolder','open hotbox folder','W_hotbox.revealInBrowser(True)') tooltip = "Open the folder containing the files that store the Hotbox buttons. It's advised not to mess around in this folder unless you understand what you're doing." - addToPreferences(openFolderKnob, tooltip) + addToPreferences(knob, tooltip) #delete preferences button knob - deletePreferencesKnob = nuke.PyScript_Knob('hotboxDeletePreferences','delete preferences','W_hotbox.deletePreferences()') + knob = nuke.PyScript_Knob('hotboxDeletePreferences','delete preferences','W_hotbox.deletePreferences()') tooltip = "Delete all the Hotbox related knobs from the Preferences Panel. After clicking this button the Preferences Panel should be closed by clicking the 'cancel' button." - addToPreferences(deletePreferencesKnob, tooltip) + addToPreferences(knob, tooltip) #Launch Label knob addToPreferences(nuke.Text_Knob('hotboxLaunchLabel','Launch')) #shortcut knob - shortcutKnob = nuke.String_Knob('hotboxShortcut','Shortcut') - shortcutKnob.setValue('`') + knob = nuke.String_Knob('hotboxShortcut','Shortcut') + knob.setValue('`') - tooltip = "The key that triggers the Hotbox. Should be set to a single key without any modifier keys. Spacebar can be defined as 'space'. Nuke needs be restarted in order for the changes to take effect." + tooltip = ("The key that triggers the Hotbox. Should be set to a single key without any modifier keys. " + "Spacebar can be defined as 'space'. Nuke needs be restarted in order for the changes to take effect.") - addToPreferences(shortcutKnob, tooltip) + addToPreferences(knob, tooltip) global shortcut shortcut = preferencesNode.knob('hotboxShortcut').value() + #reset shortcut knob + knob = nuke.PyScript_Knob('hotboxResetShortcut','set', 'W_hotbox.resetMenuItems()') + knob.clearFlag(nuke.STARTLINE) + tooltip = "Apply new shortcut." + + addToPreferences(knob, tooltip) + #trigger mode knob - triggerDropdownKnob = nuke.Enumeration_Knob('hotboxTriggerDropdown', 'Launch mode',['Press and Hold','Single Tap']) + knob = nuke.Enumeration_Knob('hotboxTriggerDropdown', 'Launch mode',['Press and Hold','Single Tap']) - tooltip = "The way the hotbox is launched. When set to 'Press and Hold' the Hotbox will appear whenever the shortcut is pressed and disappear as soon as the user releases the key. When set to 'Single Tap' the shortcut will toggle the Hotbox on and off." + tooltip = ("The way the hotbox is launched. When set to 'Press and Hold' the Hotbox will appear whenever the shortcut is pressed and disappear as soon as the user releases the key. " + "When set to 'Single Tap' the shortcut will toggle the Hotbox on and off.") - addToPreferences(triggerDropdownKnob, tooltip) + addToPreferences(knob, tooltip) #close on click - closeAfterClickKnob = nuke.Boolean_Knob('hotboxCloseOnClick','Close on button click') - closeAfterClickKnob.setValue(False) - closeAfterClickKnob.clearFlag(nuke.STARTLINE) + knob = nuke.Boolean_Knob('hotboxCloseOnClick','Close on button click') + knob.setValue(False) + knob.clearFlag(nuke.STARTLINE) tooltip = "Close the Hotbox whenever a button is clicked (excluding submenus obviously). This option will only take effect when the launch mode is set to 'Single Tap'." - addToPreferences(closeAfterClickKnob, tooltip) + addToPreferences(knob, tooltip) #execute on close - executeWithoutClickKnob = nuke.Boolean_Knob('hotboxExecuteOnClose','Execute button without click') - executeWithoutClickKnob.setValue(False) - executeWithoutClickKnob.clearFlag(nuke.STARTLINE) + knob = nuke.Boolean_Knob('hotboxExecuteOnClose','Execute button without click') + knob.setValue(False) + knob.clearFlag(nuke.STARTLINE) tooltip = "Execute the button underneath the cursor whenever the Hotbox is closed." - addToPreferences(executeWithoutClickKnob, tooltip) + addToPreferences(knob, tooltip) + + #Rule/Class order + knob = nuke.Enumeration_Knob('hotboxRuleClassOrder', 'Order',['Class - Rule', 'Rule - Class']) + tooltip = "The order in which the buttons will be loaded." + + addToPreferences(knob, tooltip) + + #Manager startup default + knob = nuke.Enumeration_Knob('hotboxOpenManagerOptions', 'Manager startup default',['Contextual','All','Rules', 'Contextual/All', 'Contextual/Rules']) + knob.clearFlag(nuke.STARTLINE) + + tooltip = ("The section of the Manager that will be opened on startup.\n" + "\nContextual Open the 'Single' or 'Multiple' section, depending on selection." + "\nAll Open the 'All' section." + "\nRules Open the 'Rules' section." + "\nContextual/All Contextual if the selection matches a button in the 'Single' or 'Multiple' section, otherwise the 'All' section will be opened." + "\nContextual/Rules Contextual if the selection matches a button in the 'Single' or 'Multiple' section, otherwise the 'Rules' section will be opened.") + + addToPreferences(knob, tooltip) #Appearence knob addToPreferences(nuke.Text_Knob('hotboxAppearanceLabel','Appearance')) #color dropdown knob - colorDropdownKnob = nuke.Enumeration_Knob('hotboxColorDropdown', 'Color scheme',['Maya','Nuke','Custom']) + knob = nuke.Boolean_Knob('hotboxMirroredLayout', 'Mirrored') - tooltip = "The color of the buttons when selected.\n\nMaya Autodesk Maya's muted blue.\nNuke Nuke's bright orange.\nCustom which lets the user pick a color." + tooltip = ("By default the contextual buttons will appear at the top of the hotbox and the non contextual buttons at the bottom.") - addToPreferences(colorDropdownKnob, tooltip) + addToPreferences(knob, tooltip) + + #color dropdown knob + knob = nuke.Enumeration_Knob('hotboxColorDropdown', 'Color scheme',['Maya','Nuke','Custom']) + + tooltip = ("The color of the buttons when selected.\n" + "\nMaya Autodesk Maya's muted blue." + "\nNuke Nuke's bright orange." + "\nCustom which lets the user pick a color.") + + addToPreferences(knob, tooltip) #custom color knob - colorCustomKnob = nuke.ColorChip_Knob('hotboxColorCustom','') - colorCustomKnob.clearFlag(nuke.STARTLINE) + knob = nuke.ColorChip_Knob('hotboxColorCustom','') + knob.clearFlag(nuke.STARTLINE) tooltip = "The color of the buttons when selected, when the color dropdown is set to 'Custom'." - addToPreferences(colorCustomKnob, tooltip) + addToPreferences(knob, tooltip) #hotbox center knob - colorHotboxCenterKnob = nuke.Boolean_Knob('hotboxColorCenter','Colorize hotbox center') - colorHotboxCenterKnob.setValue(True) - colorHotboxCenterKnob.clearFlag(nuke.STARTLINE) + knob = nuke.Boolean_Knob('hotboxColorCenter','Colorize hotbox center') + knob.setValue(True) + knob.clearFlag(nuke.STARTLINE) tooltip = "Color the center button of the hotbox depending on the current selection. When unticked the center button will be colored a lighter tone of grey." - addToPreferences(colorHotboxCenterKnob, tooltip) + addToPreferences(knob, tooltip) #auto color text - autoTextColorKnob = nuke.Boolean_Knob('hotboxAutoTextColor','Auto adjust text color') - autoTextColorKnob.setValue(True) - autoTextColorKnob.clearFlag(nuke.STARTLINE) + knob = nuke.Boolean_Knob('hotboxAutoTextColor','Auto adjust text color') + knob.setValue(True) + knob.clearFlag(nuke.STARTLINE) tooltip = "Automatically adjust the color of a button's text to its background color in order to keep enough of a difference to remain readable." - addToPreferences(autoTextColorKnob, tooltip) + addToPreferences(knob, tooltip) #fontsize knob - fontSizeKnob = nuke.Int_Knob('hotboxFontSize','Font size') - fontSizeKnob.setValue(9) + knob = nuke.Int_Knob('hotboxFontSize','Font size') + knob.setValue(8) tooltip = "The font size of the text that appears in the hotbox buttons, unless defined differently on a per-button level." - addToPreferences(fontSizeKnob, tooltip) + addToPreferences(knob, tooltip) #fontsize manager's script editor knob - fontSizeScriptEditorKnob = nuke.Int_Knob('hotboxScriptEditorFontSize','Font size script editor') - fontSizeScriptEditorKnob.setValue(11) - fontSizeScriptEditorKnob.clearFlag(nuke.STARTLINE) + knob = nuke.Int_Knob('hotboxScriptEditorFontSize','Font size script editor') + knob.setValue(11) + knob.clearFlag(nuke.STARTLINE) tooltip = "The font size of the text that appears in the hotbox manager's script editor." - addToPreferences(fontSizeScriptEditorKnob, tooltip) + addToPreferences(knob, tooltip) addToPreferences(nuke.Text_Knob('hotboxItemsLabel','Items per Row')) #row amount selection knob - rowAmountSelectionKnob = nuke.Int_Knob('hotboxRowAmountSelection', 'Selection specific') - rowAmountSelectionKnob.setValue(3) + knob = nuke.Int_Knob('hotboxRowAmountSelection', 'Selection specific') + knob.setValue(3) - tooltip = "The maximum amount of buttons a row in the upper half of the Hotbox can contain. When the row's maximum capacity is reached a new row will be started. This new row's maximum capacity will be incremented by the step size." + tooltip = ("The maximum amount of buttons a row in the upper half of the Hotbox can contain. " + "When the row's maximum capacity is reached a new row will be started. This new row's maximum capacity will be incremented by the step size.") - addToPreferences(rowAmountSelectionKnob, tooltip) + addToPreferences(knob, tooltip) #row amount all knob - rowAmountSelectionAll = nuke.Int_Knob('hotboxRowAmountAll','All') - rowAmountSelectionAll.setValue(3) + knob = nuke.Int_Knob('hotboxRowAmountAll','All') + knob.setValue(3) - tooltip = "The maximum amount of buttons a row in the lower half of the Hotbox can contain. When the row's maximum capacity is reached a new row will be started.This new row's maximum capacity will be incremented by the step size." + tooltip = ("The maximum amount of buttons a row in the lower half of the Hotbox can contain. " + "When the row's maximum capacity is reached a new row will be started.This new row's maximum capacity will be incremented by the step size.") - addToPreferences(rowAmountSelectionAll, tooltip) + addToPreferences(knob, tooltip) #stepsize knob - stepSizeKnob = nuke.Int_Knob('hotboxRowStepSize','Step size') - stepSizeKnob.setValue(1) + knob = nuke.Int_Knob('hotboxRowStepSize','Step size') + knob.setValue(1) - tooltip = "The amount a buttons every new row's maximum capacity will be increased by. Having a number unequal to zero will result in a triangular shape when having multiple rows of buttons." + tooltip = ("The amount a buttons every new row's maximum capacity will be increased by. " + "Having a number unequal to zero will result in a triangular shape when having multiple rows of buttons.") - addToPreferences(stepSizeKnob, tooltip) + addToPreferences(knob, tooltip) #spawnmode knob - spawnModeKnob = nuke.Boolean_Knob('hotboxButtonSpawnMode','Add new buttons to the sides') - spawnModeKnob.setValue(True) - spawnModeKnob.setFlag(nuke.STARTLINE) + knob = nuke.Boolean_Knob('hotboxButtonSpawnMode','Add new buttons to the sides') + knob.setValue(True) + knob.setFlag(nuke.STARTLINE) tooltip = "Add new buttons left and right of the row alternately, instead of to the right, in order to preserve muscle memory." - addToPreferences(spawnModeKnob, tooltip) + addToPreferences(knob, tooltip) #hide the iconLocation knob if environment varible called 'W_HOTBOX_HIDE_ICON_LOC' is set to 'true' or '1' preferencesNode.knob('hotboxIconLocation').setVisible(True) @@ -985,7 +1101,6 @@ def updatePreferences(): Check whether the hotbox was updated since the last launch. If so refresh the preferences. ''' - allKnobs = preferencesNode.knobs().keys() #Older versions of the hotbox had a knob called 'iconLocation'. @@ -1126,6 +1241,7 @@ def revealInBrowser(startFolder = False): else: subprocess.Popen(["xdg-open", path]) + def getFileBrowser(): ''' Determine the name of the file browser on the current system. @@ -1141,6 +1257,64 @@ def getFileBrowser(): return fileBrowser #---------------------------------------------------------------------------------------------------------- +#error catching +#---------------------------------------------------------------------------------------------------------- + + +def printError(error, path = '', buttonName = '', rule = False): + ''' + Format error message and print it to the scripteditor and shell. + ''' + + fullError = error.splitlines() + + buttonName = [buttonName] + + #line number + lineNumber = '' + for index, line in enumerate(reversed(fullError)): + + if line.startswith(' File "<'): + for i in line.split(','): + if i.startswith(' line '): + lineNumber = i + + index = len(fullError)-index + break + + lineNumber = (' -' + lineNumber) * bool(lineNumber) + + fullError = fullError[index:] + errorDescription = '\n'.join(fullError) + + #button + if not rule: + + scriptFolder = os.path.dirname(path) + scriptFolderName = os.path.basename(scriptFolder) + + while len(scriptFolderName) == 3 and scriptFolderName.isdigit(): + + name = open(scriptFolder + '/_name.json').read() + buttonName.insert(0, name) + scriptFolder = os.path.dirname(scriptFolder) + scriptFolderName = os.path.basename(scriptFolder) + + for i in range(2): + buttonName.insert(0, os.path.basename(scriptFolder)) + scriptFolder = os.path.dirname(scriptFolder) + + #buttonName = [buttonName] + + hotboxError = '\nW_HOTBOX %sERROR: %s%s:\n%s'%('RULE '*int(bool(rule)), '/'.join(buttonName), lineNumber, errorDescription) + + #print error + print hotboxError + nuke.tprint(hotboxError) + +#---------------------------------------------------------------------------------------------------------- +#launch hotbox +#---------------------------------------------------------------------------------------------------------- def showHotbox(force = False, resetPosition = True): @@ -1161,14 +1335,14 @@ def showHotbox(force = False, resetPosition = True): lastPosition = '' if hotboxInstance == None or not hotboxInstance.active: - hotboxInstance = hotbox(position = lastPosition) + hotboxInstance = Hotbox(position = lastPosition) hotboxInstance.show() def showHotboxSubMenu(path, name): global hotboxInstance hotboxInstance.active = False if hotboxInstance == None or not hotboxInstance.active: - hotboxInstance = hotbox(True, path, name) + hotboxInstance = Hotbox(True, path, name) hotboxInstance.show() def showHotboxManager(): @@ -1179,22 +1353,62 @@ def showHotboxManager(): W_hotboxManager.showHotboxManager() #---------------------------------------------------------------------------------------------------------- +#menu items +#---------------------------------------------------------------------------------------------------------- + +def addMenuItems(): + ''' + Add items to the Nuke menu + ''' + editMenu.addCommand('W_hotbox/Open W_hotbox', showHotbox, shortcut) + editMenu.addCommand('W_hotbox/-', '', '') + editMenu.addCommand('W_hotbox/Open Hotbox Manager', 'W_hotboxManager.showHotboxManager()') + editMenu.addCommand('W_hotbox/Open in %s'%getFileBrowser(), revealInBrowser) + editMenu.addCommand('W_hotbox/-', '', '') + editMenu.addCommand('W_hotbox/Repair', 'W_hotboxManager.repairHotbox()') + editMenu.addCommand('W_hotbox/Clear/Clear Everything', 'W_hotboxManager.clearHotboxManager()') + editMenu.addCommand('W_hotbox/Clear/Clear Section/Single', 'W_hotboxManager.clearHotboxManager(["Single"])') + editMenu.addCommand('W_hotbox/Clear/Clear Section/Multiple', 'W_hotboxManager.clearHotboxManager(["Multiple"])') + editMenu.addCommand('W_hotbox/Clear/Clear Section/All', 'W_hotboxManager.clearHotboxManager(["All"])') + editMenu.addCommand('W_hotbox/Clear/Clear Section/-', '', '') + editMenu.addCommand('W_hotbox/Clear/Clear Section/Templates', 'W_hotboxManager.clearHotboxManager(["Templates"])') + +def resetMenuItems(): + ''' + Remove and readd all items to the Nuke menu. Used to change the shotcut + ''' + + global shortcut + shortcut = preferencesNode.knob('hotboxShortcut').value() + if editMenu.findItem('W_hotbox'): + editMenu.removeItem('W_hotbox') + + addMenuItems() + +#---------------------------------------------------------------------------------------------------------- #add knobs to preferences preferencesNode = nuke.toNode('preferences') +homeFolder = os.getenv('HOME').replace('\\','/') + '/.nuke' + updatePreferences() addPreferences() #---------------------------------------------------------------------------------------------------------- #make sure the archive folders are present, if not, create them +hotboxLocationPathKnob = preferencesNode.knob('hotboxLocation') +hotboxLocationPath = hotboxLocationPathKnob.value().replace('\\','/') + +if not hotboxLocationPath: + hotboxLocationPath = homeFolder + '/W_hotbox' + hotboxLocationPathKnob.setValue(hotboxLocationPath) -hotboxLocationPath = preferencesNode.knob('hotboxLocation').value().replace('\\','/') if hotboxLocationPath[-1] != '/': hotboxLocationPath += '/' -for subFolder in ['','Single','Multiple','All','Single/No Selection','Templates']: +for subFolder in ['', 'Single', 'Multiple', 'All', 'Rules', 'Single/No Selection', 'Templates']: subFolderPath = hotboxLocationPath + subFolder if not os.path.isdir(subFolderPath): try: @@ -1203,24 +1417,11 @@ def showHotboxManager(): pass #---------------------------------------------------------------------------------------------------------- -# MENU ITEMS -#---------------------------------------------------------------------------------------------------------- -menubar = nuke.menu('Nuke') - -menubar.addCommand('Edit/-', '', '') -menubar.addCommand('Edit/W_hotbox/Open W_hotbox',showHotbox, shortcut) -menubar.addCommand('Edit/W_hotbox/-', '', '') -menubar.addCommand('Edit/W_hotbox/Open Hotbox Manager', 'W_hotboxManager.showHotboxManager()') -menubar.addCommand('Edit/W_hotbox/Open in %s'%getFileBrowser(), revealInBrowser) -menubar.addCommand('Edit/W_hotbox/-', '', '') -menubar.addCommand('Edit/W_hotbox/Repair', 'W_hotboxManager.repairHotbox()') -menubar.addCommand('Edit/W_hotbox/Clear/Clear Everything', 'W_hotboxManager.clearHotboxManager()') -menubar.addCommand('Edit/W_hotbox/Clear/Clear Section/Single', 'W_hotboxManager.clearHotboxManager(["Single"])') -menubar.addCommand('Edit/W_hotbox/Clear/Clear Section/Multiple', 'W_hotboxManager.clearHotboxManager(["Multiple"])') -menubar.addCommand('Edit/W_hotbox/Clear/Clear Section/All', 'W_hotboxManager.clearHotboxManager(["All"])') -menubar.addCommand('Edit/W_hotbox/Clear/Clear Section/-', '', '') -menubar.addCommand('Edit/W_hotbox/Clear/Clear Section/Templates', 'W_hotboxManager.clearHotboxManager(["Templates"])') +#menu items +editMenu = nuke.menu('Nuke').findItem('Edit') +editMenu.addCommand('-', '', '') +addMenuItems() #---------------------------------------------------------------------------------------------------------- # EXTRA REPOSTITORIES @@ -1254,9 +1455,9 @@ def showHotboxManager(): if len(extraRepositories) > 0: - menubar.addCommand('Edit/W_hotbox/-', '', '') - for i in extraRepositories: - menubar.addCommand('Edit/W_hotbox/Special/Open Hotbox Manager - %s'%i[0], 'W_hotboxManager.showHotboxManager(path="%s")'%i[1]) + menubar.addCommand('W_hotbox/-', '', '') + for repo in extraRepositories: + menubar.addCommand('W_hotbox/Special/Open Hotbox Manager - %s'%repo[0], 'W_hotboxManager.showHotboxManager(path="%s")'%repo[1]) #---------------------------------------------------------------------------------------------------------- @@ -1265,4 +1466,4 @@ def showHotboxManager(): #---------------------------------------------------------------------------------------------------------- -nuke.tprint('W_hotbox v%s, built %s.\nCopyright (c) 2016 Wouter Gilsing. All Rights Reserved.'%(version,releaseDate)) +nuke.tprint('W_hotbox v%s, built %s.\nCopyright (c) 2016-%s Wouter Gilsing. All Rights Reserved.'%(version, releaseDate, releaseDate.split()[-1])) diff --git a/W_hotboxManager.py b/W_hotboxManager.py old mode 100755 new mode 100644 index 718c937..894d028 --- a/W_hotboxManager.py +++ b/W_hotboxManager.py @@ -1,13 +1,11 @@ #---------------------------------------------------------------------------------------------------------- # Wouter Gilsing # woutergilsing@hotmail.com -version = '1.7' -releaseDate = 'June 26 2017' +version = '1.8' +releaseDate = 'April 23 2018' #---------------------------------------------------------------------------------------------------------- -# #LICENSE -# #---------------------------------------------------------------------------------------------------------- ''' @@ -36,6 +34,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ''' +#---------------------------------------------------------------------------------------------------------- +#modules #---------------------------------------------------------------------------------------------------------- import nuke @@ -62,9 +62,9 @@ #---------------------------------------------------------------------------------------------------------- -class hotboxManager(QtWidgets.QWidget): +class HotboxManager(QtWidgets.QWidget): def __init__(self, path = ''): - super(hotboxManager, self).__init__() + super(HotboxManager, self).__init__() #-------------------------------------------------------------------------------------------------- #main widget @@ -90,14 +90,18 @@ def __init__(self, path = ''): self.rootLocation = path.replace('\\','/') + #-------------------------------------------------------------------------------------------------- + #create folders + #-------------------------------------------------------------------------------------------------- #If the manager is launched for the default repository, make sure the current archive exists. + preferencesLocation = preferencesNode.knob('hotboxLocation').value() if preferencesLocation[-1] != '/': preferencesLocation += '/' if self.rootLocation == preferencesLocation: - for subFolder in ['','Single','Multiple','All','Single/No Selection','Templates']: + for subFolder in ['','Single', 'Multiple', 'All', 'Single/No Selection', 'Rules', 'Templates']: subFolderPath = self.rootLocation + subFolder if not os.path.isdir(subFolderPath): try: @@ -108,17 +112,20 @@ def __init__(self, path = ''): self.templateLocation = self.rootLocation + 'Templates/' #-------------------------------------------------------------------------------------------------- - #classes list + #left column - classes list #-------------------------------------------------------------------------------------------------- self.classesListLayout = QtWidgets.QVBoxLayout() + #scope dropdown self.scopeComboBox = QtWidgets.QComboBox() - self.scopeComboBoxItems = ['Single','Multiple','All'] + self.scopeComboBoxItems = ['Single','Multiple','All','Rules'] self.scopeComboBox.addItems(self.scopeComboBoxItems) + self.scopeComboBox.insertSeparator(3) self.scopeComboBox.currentIndexChanged.connect(self.buildClassesList) + #list column self.classesList = QListWidgetCustom(self) self.classesList.setFixedWidth(150) @@ -132,7 +139,7 @@ def __init__(self, path = ''): self.classesListRemoveButton = QLabelButton('remove',self.classesList) self.classesListRenameButton = QLabelButton('rename',self.classesList) - #wire up + #connect self.classesListAddButton.clicked.connect(self.addClass) self.classesListRemoveButton.clicked.connect(self.removeClass) self.classesListRenameButton.clicked.connect(self.renameClass) @@ -145,7 +152,7 @@ def __init__(self, path = ''): self.classesListButtonsLayout.addStretch() #-------------------------------------------------------------------------------------------------- - #hotbox items tree + #right column - hotbox items tree #-------------------------------------------------------------------------------------------------- self.hotboxItemsTree = QTreeViewCustom(self) @@ -153,11 +160,8 @@ def __init__(self, path = ''): self.rootPath = nuke.toNode('preferences').knob('hotboxLocation').value() self.classesList.itemSelectionChanged.connect(self.hotboxItemsTree.populateTree) - - #-------------------------------------------------------------------------------------------------- - #hotbox items tree actions - #-------------------------------------------------------------------------------------------------- - + + #actions self.hotboxItemsTreeButtonsLayout = QtWidgets.QVBoxLayout() self.hotboxItemsTreeAddButton = QLabelButton('add',self.hotboxItemsTree) @@ -171,7 +175,7 @@ def __init__(self, path = ''): self.hotboxItemsTreeMoveDown = QLabelButton('moveDown',self.hotboxItemsTree) self.hotboxItemsTreeMoveUpLevel = QLabelButton('moveUpLevel',self.hotboxItemsTree) - #wire up + #connect self.hotboxItemsTreeAddButton.clicked.connect(self.hotboxItemsTree.addItem) self.hotboxItemsTreeAddFolderButton.clicked.connect(lambda: self.hotboxItemsTree.addItem(True)) self.hotboxItemsTreeRemoveButton.clicked.connect(self.hotboxItemsTree.removeItem) @@ -208,7 +212,7 @@ def __init__(self, path = ''): #-------------------------------------------------------------------------------------------------- #create buttons - self.clipboardArchive = QtWidgets.QRadioButton('Clipboard') + self.clipboardArchive = QtWidgets.QCheckBox('Clipboard') self.importArchiveButton = QtWidgets.QPushButton('Import Archive') self.exportArchiveButton = QtWidgets.QPushButton('Export Archive') @@ -218,9 +222,9 @@ def __init__(self, path = ''): #tooltips tooltip = 'Make use of the clipboard to import/export an archive, rather than saving a file to disk.' self.clipboardArchive.setToolTip(tooltip) - tooltip = 'Export the current set of buttons as an archive.' - self.importArchiveButton.setToolTip(tooltip) tooltip = 'Import a button archive. This will append the current set of buttons and overwrite any buttons with the same name.' + self.importArchiveButton.setToolTip(tooltip) + tooltip = 'Export the current set of buttons as an archive.' self.exportArchiveButton.setToolTip(tooltip) #wire up @@ -238,8 +242,8 @@ def __init__(self, path = ''): #scriptEditor #-------------------------------------------------------------------------------------------------- + self.ignoreSave = False self.loadedScript = None - self.scriptEditorLayout = QtWidgets.QVBoxLayout() #buttons @@ -256,7 +260,7 @@ def __init__(self, path = ''): self.scriptEditorImportButton = QtWidgets.QPushButton('Import') self.scriptEditorImportButton.clicked.connect(self.importScriptEditor) - self.scriptEditorTemplateMenu = scriptEditorTemplateMenu(self) + self.scriptEditorTemplateMenu = ScriptEditorTemplateMenu(self) self.scriptEditorTemplateButton.setMenu(self.scriptEditorTemplateMenu) self.exitTemplateModeButton.clicked.connect(self.toggleTemplateMode) @@ -271,17 +275,17 @@ def __init__(self, path = ''): self.scriptEditorNameLayout = QtWidgets.QHBoxLayout() self.scriptEditorNameLabel = QtWidgets.QLabel('Name') - self.scriptEditorName = scriptEditorNameWidget() + self.scriptEditorName = ScriptEditorNameWidget() self.scriptEditorName.setAlignment(QtCore.Qt.AlignLeft) self.scriptEditorName.editingFinished.connect(self.saveScriptEditor) #color swatches self.colorSwatchButtonLabel = QtWidgets.QLabel('Button') - self.colorSwatchButton = colorSwatch('#525252') + self.colorSwatchButton = ColorSwatch('#525252') self.colorSwatchTextLabel = QtWidgets.QLabel('Text') - self.colorSwatchText = colorSwatch('#eeeeee') + self.colorSwatchText = ColorSwatch('#eeeeee') self.colorSwatchButton.setChild(self.colorSwatchText) @@ -289,20 +293,35 @@ def __init__(self, path = ''): self.colorSwatchButton.save.connect(self.saveScriptEditor) self.colorSwatchText.save.connect(self.saveScriptEditor) - for widget in [self.scriptEditorNameLabel,self.scriptEditorName,self.colorSwatchButtonLabel,self.colorSwatchButton,self.colorSwatchTextLabel,self.colorSwatchText]: + self.scriptEditorNameWidgets = [self.scriptEditorNameLabel, self.scriptEditorName, self.colorSwatchButtonLabel, self.colorSwatchButton, self.colorSwatchTextLabel, self.colorSwatchText] + + #rules + self.rulesFlagCheckbox = QtWidgets.QCheckBox('Ignore classes') + self.rulesFlagCheckbox.setLayoutDirection(QtCore.Qt.RightToLeft) + self.rulesFlagCheckbox.stateChanged.connect(lambda: self.saveScriptEditor()) + + #label to make sure the checkbox is aligned to the right + self.rulesFlagLabel = QtWidgets.QLabel('') + self.rulesFlagWidgets = [self.rulesFlagCheckbox, self.rulesFlagLabel] + + for widget in self.rulesFlagWidgets: + widget.setVisible(False) + + #assemble layout + for widget in self.rulesFlagWidgets + self.scriptEditorNameWidgets: self.scriptEditorNameLayout.addWidget(widget) #script - self.scriptEditorScript = scriptEditorWidget() + self.scriptEditorScript = ScriptEditorWidget() self.scriptEditorScript.setMinimumHeight(200) self.scriptEditorScript.setMinimumWidth(500) self.scriptEditorScript.save.connect(self.saveScriptEditor) - scriptEditorHighlighter(self.scriptEditorScript.document()) + ScriptEditorHighlighter(self.scriptEditorScript.document()) scriptEditorFont = QtGui.QFont() - scriptEditorFont.setFamily("Courier") + scriptEditorFont.setFamily('Courier') scriptEditorFont.setStyleHint(QtGui.QFont.Monospace) scriptEditorFont.setFixedPitch(True) scriptEditorFont.setPointSize(preferencesNode.knob('hotboxScriptEditorFontSize').value()) @@ -311,7 +330,6 @@ def __init__(self, path = ''): self.scriptEditorScript.setTabStopWidth(4 * QtGui.QFontMetrics(scriptEditorFont).width(' ')) #assemble - self.scriptEditorLayout.addLayout(self.archiveButtonsLayout) self.scriptEditorLayout.addLayout(self.scriptEditorNameLayout) self.scriptEditorLayout.addWidget(self.scriptEditorScript) @@ -366,7 +384,7 @@ def __init__(self, path = ''): self.move(QtCore.QPoint(screenRes.width()/2,screenRes.height()/2)-QtCore.QPoint((self.width()/2),(self.height()/2))) #-------------------------------------------------------------------------------------------------- - #set hotbox to current selection + #set values #-------------------------------------------------------------------------------------------------- self.enableScriptEditor(False,False) @@ -375,15 +393,46 @@ def __init__(self, path = ''): self.scopeComboBox.setCurrentIndex(0) self.scopeComboBoxLastIndex = 0 - selection = nuke.selectedNodes() - if len(selection) > 0: - classes = set(sorted([i.Class() for i in selection])) - self.scopeComboBox.setCurrentIndex(max(min(len(classes)-1,1),0)) + #-------------------------------------------------------------------------------------------------- + #set hotbox to current selection + #-------------------------------------------------------------------------------------------------- + + launchMode = preferencesNode.knob('hotboxOpenManagerOptions').value() + launchMode = launchMode.replace('Contextual','Single/Multiple') + launchMode = launchMode.split('/') + + found = False + + #contextual + if len(launchMode) > 1: + + selection = nuke.selectedNodes() + + classes = sorted(set([node.Class() for node in selection])) + + #single/multiple + self.scopeComboBox.setCurrentIndex(len(classes) > 1) + for index in range(self.classesList.count()): - if self.classesList.item(index).text() == '-'.join(classes): + itemClasses = self.classesList.item(index).text().split('-') + if all([nodeClass in itemClasses for nodeClass in classes]): self.classesList.setCurrentRow(index) + found = True break + if len(launchMode) == 2: + found = True + + else: + found *= bool(selection) + + #all or rules (2 or 4) + if not found: + + item = launchMode[-1] + index = (int(item == 'Rules')*2) + 2 + self.scopeComboBox.setCurrentIndex(index) + #-------------------------------------------------------------------------------------------------- #classes list #-------------------------------------------------------------------------------------------------- @@ -397,9 +446,12 @@ def buildClassesList(self, selectItem = None): if isinstance(selectItem, bool) and selectItem: itemIndex = self.classesList.currentRow() - mode = self.scopeComboBox.currentText() + self.mode = self.scopeComboBox.currentText() + + self.contextual = self.mode not in ['All','Templates'] - self.selectionSpecific = mode not in ['All','Templates'] + #turn this variable on, to prevent the itemChanged signal from emitting + self.classesList.buildClassesList = True #clear selection self.classesList.clearSelection() @@ -410,25 +462,47 @@ def buildClassesList(self, selectItem = None): #clear list self.classesList.clear() - self.path = self.rootLocation + mode + #disable scripteditor + self.enableScriptEditor(False, False) + + self.path = self.rootLocation + self.mode #color color = self.activeColor #disable if templates or all mode - if self.selectionSpecific: + if self.contextual: self.classesList.setEnabled() else: self.classesList.setEnabled(False) - if self.selectionSpecific: + if self.contextual: - #sort classes found on disk - allClasses =sorted(os.listdir(self.path), key=lambda s: s.lower()) - allClasses = [folder for folder in allClasses if os.path.isdir(self.path + '/' + folder) and folder[0] not in ['.','_']] + #sort items found on disk + allItems = sorted(os.listdir(self.path), key=lambda s: s.lower()) + allItems = [folder for folder in allItems if os.path.isdir(self.path + '/' + folder) and folder[0] not in ['.','_']] #add items - self.classesList.addItems(allClasses) + self.classesList.addItems(allItems) + + #add checkbox to item if in rule mode + if self.mode == 'Rules': + + checkedStates = [QtCore.Qt.Unchecked, QtCore.Qt.Checked] + for index in range(self.classesList.count()): + + item = self.classesList.item(index) + itemText = item.text() + + item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable) + + #check if supposed to be enabled according to name + checkedState = itemText[-1] != '_' + item.setCheckState(checkedStates[checkedState]) + if not checkedState: + item.setText(itemText[:-1]) + + self.toggleRulesMode(False) #populate buttons tree self.hotboxItemsTree.populateTree() @@ -450,22 +524,37 @@ def buildClassesList(self, selectItem = None): self.classesList.setCurrentRow(itemIndex) + #turn this variable back off + self.classesList.buildClassesList = False + def addClass(self): ''' Add a new nodeclass ''' + defaultName = 'NewClass' - newClass = 'NewClass' + if self.mode == 'Rules': + defaultName = 'NewRule' + name = defaultName + + #in case name allready exists counter = 1 - while os.path.isdir(self.path + '/' + newClass): - newClass = 'NewClass' + str(counter) + while os.path.isdir(self.path + '/' + name): + name = defaultName + str(counter) counter += 1 - os.mkdir(self.path + '/' + newClass) + #create folder on disk + folderPath = '/'.join([self.path, name]) + os.mkdir(folderPath) - self.buildClassesList(newClass) + #create rule file + if self.mode == 'Rules': + ruleFile = open(folderPath + '/_rule.py', 'w') + ruleFile.write(FileHeader('0', rule = True).getHeader()) + ruleFile.close() + self.buildClassesList(name) self.renameClass(True) def removeClass(self, className = None): @@ -473,15 +562,17 @@ def removeClass(self, className = None): Remove the selected nodeclass ''' - if className: selectedClass = className + else: - if not self.classesList.itemSelected(): - return - selectedClass = self.classesList.currentItem().text() + selectedClass = self.getSelectedClass() + if not selectedClass: + return + - oldFolder = self.path + '/_old' + #move to old folder + oldFolder = '/'.join([self.path, '_old']) if not os.path.isdir(oldFolder): os.mkdir(oldFolder) @@ -494,10 +585,9 @@ def renameClass(self, new = False): Rename the selected nodeclass ''' - if not self.classesList.itemSelected(): - return - - currentClass = self.classesList.currentItem().text() + selectedClass = self.getSelectedClass() + if not selectedClass: + return #kill any existing instances global renameDialogInstance @@ -505,14 +595,31 @@ def renameClass(self, new = False): renameDialogInstance.closeRenameDialog() #spawn new - renameDialogInstance = renameDialog(currentClass,new) + renameDialogInstance = RenameDialog(selectedClass, new) renameDialogInstance.show() + def getSelectedClass(self): + ''' + Return the name of the selected class + ''' + + if not self.classesList.itemSelected(): + return None + + selectedItem = self.classesList.currentItem() + selectedClass = selectedItem.text() + + #if rule, check if item enabled + if self.mode == 'Rules': + selectedClass += '_' * (1 - bool(selectedItem.checkState())) + + return selectedClass + #-------------------------------------------------------------------------------------------------- #scriptEditor #-------------------------------------------------------------------------------------------------- - def loadScriptEditor(self): + def loadScriptEditor(self, rule = False): ''' Fill the fields of the the script editor with the information read from the currently selected file. @@ -520,32 +627,57 @@ def loadScriptEditor(self): self.scriptEditorScript.savedText = '' - if len(self.hotboxItemsTree.selectedItems) != 0: + #check if items selected + itemsSelected = True + if not rule: + itemsSelected = bool(self.hotboxItemsTree.selectedItems) + - self.selectedItem = self.hotboxItemsTree.selectedItems[0] - self.loadedScript = self.selectedItem.path + if itemsSelected: + + if not rule: + self.selectedItem = self.hotboxItemsTree.selectedItems[0] + self.loadedScript = self.selectedItem.path + + #if rule mode + else: + item = self.classesList.currentItem() + itemState = 1 - bool(item.checkState()) + self.loadedScript = '/'.join([self.path, item.text() + '_' * itemState, '_rule.py']) + #if item (not submenu) - if self.selectedItem.path.endswith('.py'): + if self.loadedScript.endswith('.py'): self.enableScriptEditor() - #set attributes - name = getAttributeFromFile(self.loadedScript) - self.scriptEditorName.setText(name) + if not rule: + + + #set attributes + name = getAttributeFromFile(self.loadedScript) + self.scriptEditorName.setText(name) - #make sure the colorswatches will remain disabled in template mode - if not self.exitTemplateModeButton.isVisible(): + #make sure the colorswatches will remain disabled in template mode + if not self.exitTemplateModeButton.isVisible(): - textColor = getAttributeFromFile(self.loadedScript, 'textColor') - self.colorSwatchText.setColor(textColor, adjustChild = False, indirect = True) + textColor = getAttributeFromFile(self.loadedScript, 'textColor') + self.colorSwatchText.setColor(textColor, adjustChild = False, indirect = True) - color = getAttributeFromFile(self.loadedScript, 'color') - self.colorSwatchButton.setColor(color, adjustChild = False, indirect = True) + color = getAttributeFromFile(self.loadedScript, 'color') + self.colorSwatchButton.setColor(color, adjustChild = False, indirect = True) + + #rule + else: + + ignoreClasses = int(getAttributeFromFile(self.loadedScript, 'ignore classes')) + + self.ignoreSave = True + self.rulesFlagCheckbox.setChecked(ignoreClasses) + self.ignoreSave = False #set script text = getScriptFromFile(self.loadedScript) - self.scriptEditorScript.setPlainText(text) self.scriptEditorScript.updateSavedText() @@ -561,6 +693,7 @@ def loadScriptEditor(self): self.loadedScript = None self.enableScriptEditor(False, False) + def enableScriptEditor(self, editor = True, name = True): ''' Enable/Disable widgets based on selection. @@ -578,7 +711,7 @@ def enableScriptEditor(self, editor = True, name = True): #make sure the buttons are colorswatches are always disabled in template mode editor = editor * (1-self.exitTemplateModeButton.isVisible()) - for colorSwatch in [self.colorSwatchButton,self.colorSwatchText,self.colorSwatchButtonLabel,self.colorSwatchTextLabel]: + for colorSwatch in [self.colorSwatchButton, self.colorSwatchText, self.colorSwatchButtonLabel, self.colorSwatchTextLabel]: colorSwatch.setEnabled(editor) #name @@ -605,12 +738,17 @@ def importScriptEditor(self): self.scriptEditorScript.setPlainText(text) self.scriptEditorScript.setFocus() - def saveScriptEditor(self, template = False): ''' Save the current content of the script editor ''' + #dont save whenever this function is triggered while ignoreSave is on. + if self.ignoreSave: + return + + rule = self.rulesFlagCheckbox.isVisible() + if not self.scriptEditorName.isReadOnly(): name = self.scriptEditorName.text() @@ -620,18 +758,23 @@ def saveScriptEditor(self, template = False): path += '.py' else: - path = self.selectedItem.path + path = self.loadedScript #file if path.endswith('.py'): text = self.scriptEditorScript.toPlainText() - #header - color = self.colorSwatchButton.isNonDefault(True) - textColor = self.colorSwatchText.isNonDefault(True) + if not rule: + + #header + color = self.colorSwatchButton.isNonDefault(True) + textColor = self.colorSwatchText.isNonDefault(True) + + newFileContent = FileHeader(name, color, textColor).getHeader() + text - newFileContent = fileHeader(name, color, textColor).getHeader() + text + else: + newFileContent = FileHeader(int(self.rulesFlagCheckbox.isChecked()), rule = True).getHeader() + text #save to disk currentFile = open(path, 'w') @@ -644,15 +787,39 @@ def saveScriptEditor(self, template = False): #menu else: #save to disk - currentFile = open(self.selectedItem.path+'/_name.json', 'w') + currentFile = open(self.loadedScript + '/_name.json', 'w') currentFile.write(name) currentFile.close() - self.selectedItem.setText(name) + if not rule: + self.selectedItem.setText(name) - #update template menu - if path.startswith(self.templateLocation): - self.scriptEditorTemplateMenu.initMenu() + if template: + #update template menu + if path.startswith(self.templateLocation): + self.scriptEditorTemplateMenu.initMenu() + + #-------------------------------------------------------------------------------------------------- + #Rules mode + #-------------------------------------------------------------------------------------------------- + + def toggleRulesMode(self, mode = True): + ''' + Toggle rule mode on and off. + ''' + + #if triggered by item selection + if mode: + if self.mode != 'Rules': + return + + #apply change + for index, widgetList in enumerate([self.rulesFlagWidgets, self.scriptEditorNameWidgets][::mode*2-1]): + for widget in widgetList: + widget.setVisible(1-index) + + if mode: + self.loadScriptEditor(rule = True) #-------------------------------------------------------------------------------------------------- #Template mode @@ -729,11 +896,15 @@ def toggleTemplateMode(self): self.scriptEditorTemplateMenu.enableMenuItems() #-------------------------------------------------------------------------------------------------- - #Import/Export functions + #import/export functions #-------------------------------------------------------------------------------------------------- + #export def exportHotboxArchive(self): - + ''' + A method to export a set of buttons to an external current archive. + ''' + #create zip nukeFolder = os.getenv('HOME').replace('\\','/') + '/.nuke/' currentDate = dt.now().strftime('%Y%m%d%H%M') @@ -821,17 +992,16 @@ def indexArchive(self, location, dict = False): fileList.append( [level + '/' + file , newLevel + '/' + newFile ]) return fileList + #import def importHotboxArchive(self): ''' - A method to import a set of button to append the current archive with. + A method to import a set of buttons to append the current archive with. If you're actually reading this, I apologise in advance for what's coming. I had trouble getting the code to work on Windows and it turned out it had to do with (back)slashes. I ended up trowing in a lot of ".replace('\\','/')". I works, but it turned kinda messy... ''' - - nukeFolder = os.getenv('HOME').replace('\\','/') + '/.nuke/' currentDate = dt.now().strftime('%Y%m%d%H%M') tempFolder = nukeFolder + 'W_hotboxArchiveImportTemp_%s/'%currentDate @@ -878,7 +1048,7 @@ def importHotboxArchive(self): #Make sure the current archive is healthy for i in ['Single','Multiple','All']: - repairHotbox(self.rootLocation + i, message = False) + RepairHotbox(self.rootLocation + i, message = False) #Copy stuff from extracted archive to current hotbox location @@ -979,21 +1149,23 @@ def openAboutDialog(self): global aboutDialogInstance if aboutDialogInstance != None: aboutDialogInstance.close() - aboutDialogInstance = aboutDialog() + aboutDialogInstance = AboutDialog() aboutDialogInstance.show() - #------------------------------------------------------------------------------------------------------ #Classes List #------------------------------------------------------------------------------------------------------ class QListWidgetCustom(QtWidgets.QListWidget): - def __init__(self, parentClass): + def __init__(self, hotboxManager): super(QListWidgetCustom, self).__init__() self.enabled = False - self.parentClass = parentClass + self.hotboxManager = hotboxManager + + self.buildClassesList = False + self.itemChanged.connect(self.catchCheckboxChange) def setEnabled(self, mode = True): @@ -1003,33 +1175,74 @@ def setEnabled(self, mode = True): self.enabled = mode - #change color - color = [self.parentClass.lockedColor,self.parentClass.activeColor][int(mode)] + color = [self.hotboxManager.lockedColor, self.hotboxManager.activeColor][int(mode)] self.setStyleSheet('background-color : %s'%color) def itemSelected(self): return bool(self.currentItem()) + def allItemNames(self): + ''' + Return a list of all the items (text) + ''' + return [self.item(index).text() for index in range(self.count())] + + def catchCheckboxChange(self): + ''' + Function that get executed whenever an item is changed. + ''' + + #Only un when in Rules mode and when not building the list (so just when I user ticks the checkbox.) + if self.hotboxManager.mode != 'Rules' or self.buildClassesList: + return + + for index in range(self.count()): + + item = self.item(index) + fileName = item.text() + + checkState = int(item.checkState()) + + newRulePath, origRulePath = [self.hotboxManager.path + '/' + fileName + ('_' * index) for index in range(2)][::(checkState - 1)] + + if not os.path.exists(newRulePath): + os.rename(origRulePath, newRulePath) + break + + def focusInEvent(self, event): + ''' + Actions executed when widget gains focus. + ''' + + #inherit default behaviour + QtWidgets.QListWidget.focusOutEvent(self, event) + + if self.hotboxManager.mode == 'Rules' and not self.hotboxManager.rulesFlagCheckbox.isVisible(): + self.clearSelection() + + return True #------------------------------------------------------------------------------------------------------ #Color Swatch #------------------------------------------------------------------------------------------------------ -class colorSwatch(QtWidgets.QLabel): +class ColorSwatch(QtWidgets.QLabel): #signals save = QtCore.Signal() def __init__(self, defaultColor): - super(colorSwatch, self).__init__() + super(ColorSwatch, self).__init__() self.color = None self.enabled = False self.active = False + + self.child = None self.parent = None @@ -1162,8 +1375,35 @@ def mouseReleaseEvent(self,event): return False + def dragEnterEvent(self, e): + + #check if color + if e.mimeData().hasFormat('application/x-color') and self.enabled: + e.accept() + else: + e.ignore() + + + def dropEvent(self, e): + + print e.mimeData().colorData().rgb() + + #find color + node = nuke.toNode(nuke.tcl('stack 0')) + + interfaceColor = node.knob('tile_color').value() + + if interfaceColor == 0: + interfaceColor = nuke.defaultNodeColor(node.Class()) + + rgbColor = W_hotbox.interface2rgb(interfaceColor) + color = W_hotbox.rgb2hex(rgbColor) + + self.setColor(color) + + #-------------------------------------------------------------------------------------------------- - # Color + #Color #-------------------------------------------------------------------------------------------------- def setEnabled(self, mode): ''' @@ -1171,6 +1411,7 @@ def setEnabled(self, mode): ''' self.enabled = mode + self.setAcceptDrops(mode) self.setColor(adjustChild = False, indirect = True) def getColor(self): @@ -1245,7 +1486,6 @@ def setChildColor(self): self.child.setColor(indirect = True) return True - #parent color rgbParentColor = W_hotbox.hex2rgb(self.color) hsvParentColor = colorsys.rgb_to_hsv(rgbParentColor[0],rgbParentColor[1],rgbParentColor[2]) @@ -1293,7 +1533,7 @@ def setChild(self, child): ''' ''' - if isinstance(child, colorSwatch): + if isinstance(child, ColorSwatch): self.child = child self.child.parent = self self.child.assignToolTip(True) @@ -1389,7 +1629,7 @@ def paintEvent(self, event): #File Name #------------------------------------------------------------------------------------------------------ -class scriptEditorNameWidget(QtWidgets.QLineEdit): +class ScriptEditorNameWidget(QtWidgets.QLineEdit): ''' Subclassed QLineEdit. Added some functionality to check whether the text was changed and to save. @@ -1399,7 +1639,7 @@ class scriptEditorNameWidget(QtWidgets.QLineEdit): save = QtCore.Signal() def __init__(self): - super(scriptEditorNameWidget, self).__init__() + super(ScriptEditorNameWidget, self).__init__() self.savedText = '' self.editingFinished.connect(self.saveEvent) @@ -1440,7 +1680,7 @@ def setText(self,text): #Script Editor #------------------------------------------------------------------------------------------------------ -class scriptEditorWidget(QtWidgets.QPlainTextEdit): +class ScriptEditorWidget(QtWidgets.QPlainTextEdit): ''' Script editor widget. ''' @@ -1449,7 +1689,7 @@ class scriptEditorWidget(QtWidgets.QPlainTextEdit): save = QtCore.Signal() def __init__(self): - super(scriptEditorWidget, self).__init__() + super(ScriptEditorWidget, self).__init__() self.savedText = '' self.savedName = '' @@ -1500,7 +1740,6 @@ def keyPressEvent(self, event): #if Shift+Tab remove indent elif event.key() == 16777218: - self.indentation('unindent') #if BackSpace try to snap to previous indent level @@ -1508,10 +1747,16 @@ def keyPressEvent(self, event): if not self.unindentBackspace(): QtWidgets.QPlainTextEdit.keyPressEvent(self, event) + #if shift+/ comment out current line(s) (toggle) + elif event.key() == 47 and QtWidgets.QApplication.keyboardModifiers() == QtCore.Qt.ControlModifier: + #QtWidgets.QPlainTextEdit.keyPressEvent(self, event) + self.toggleComment() + #if enter or return, match indent level elif event.key() in [16777220 ,16777221]: #QtWidgets.QPlainTextEdit.keyPressEvent(self, event) self.indentNewLine() + else: QtWidgets.QPlainTextEdit.keyPressEvent(self, event) @@ -1545,7 +1790,7 @@ def updateSavedText(self): #thefoundry.co.uk/products/nuke/developers/100/pythonreference/nukescripts.blinkscripteditor-pysrc.html #I stripped and modified the useful bits of the line number related parts of the code #and implemented it in the Hotbox Manager. Credits to theFoundry for writing the blinkscripteditor, - #best example code I could wish for. + #best example code I could have wished for. #-------------------------------------------------------------------------------------------------- def lineNumberAreaWidth(self): @@ -1628,12 +1873,14 @@ def getCursorInfo(self): self.originalPosition = self.cursor.position() self.cursorBlockPos = self.cursor.positionInBlock() + #-------------------------------------------------------------------------------------------------- def unindentBackspace(self): ''' - #snap to previous indent level + snap to previous indent level ''' + self.getCursorInfo() if not self.noSelection or self.cursorBlockPos == 0: @@ -1652,6 +1899,9 @@ def unindentBackspace(self): self.cursor.deletePreviousChar() def indentNewLine(self): + ''' + Auto indent a new line + ''' #in case selection covers multiple line, make it one line first self.insertPlainText('') @@ -1693,11 +1943,15 @@ def indentNewLine(self): self.insertPlainText(' '*(4*indentLevel)) def indentation(self, mode): + ''' + Indent selected + ''' self.getCursorInfo() #if nothing is selected and mode is set to indent, simply insert as many #space as needed to reach the next indentation level. + if self.noSelection and mode == 'indent': remainingSpaces = 4 - (self.cursorBlockPos%4) @@ -1721,10 +1975,43 @@ def indentation(self, mode): self.clear() self.setPlainText(combinedText) + self.restoreSelection() + + def toggleComment(self): + ''' + Disable a line by putting a # in front of it. + ''' + + self.getCursorInfo() + + selectedBlocks = self.findBlocks(self.firstChar, self.lastChar) + beforeBlocks = self.findBlocks(last = self.firstChar -1, exclude = selectedBlocks) + afterBlocks = self.findBlocks(first = self.lastChar + 1, exclude = selectedBlocks) + + beforeBlocksText = self.blocks2list(beforeBlocks) + selectedBlocksText = self.blocks2list(selectedBlocks, 'comment') + afterBlocksText = self.blocks2list(afterBlocks) + + combinedText = '\n'.join(beforeBlocksText + selectedBlocksText + afterBlocksText) + + #make sure the line count stays the same + originalBlockCount = len(self.toPlainText().split('\n')) + combinedText = '\n'.join(combinedText.split('\n')[:originalBlockCount]) + + self.clear() + self.setPlainText(combinedText) + + self.restoreSelection() + + def restoreSelection(self): + ''' + Restore the original selection and cursor posiftion modifing the text. + ''' + if self.noSelection: self.cursor.setPosition(self.lastChar) - #check whether the the orignal selection was from top to bottom or vice versa + #check whether the the original selection was from top to bottom or vice versa else: if self.originalPosition == self.firstChar: first = self.lastChar @@ -1738,13 +2025,18 @@ def indentation(self, mode): lastBlockSnap = QtGui.QTextCursor.EndOfBlock self.cursor.setPosition(first) - self.cursor.movePosition(firstBlockSnap,QtGui.QTextCursor.MoveAnchor) - self.cursor.setPosition(last,QtGui.QTextCursor.KeepAnchor) - self.cursor.movePosition(lastBlockSnap,QtGui.QTextCursor.KeepAnchor) + self.cursor.movePosition(firstBlockSnap, QtGui.QTextCursor.MoveAnchor) + self.cursor.setPosition(last, QtGui.QTextCursor.KeepAnchor) + self.cursor.movePosition(lastBlockSnap, QtGui.QTextCursor.KeepAnchor) - self.setTextCursor(self.cursor) + self.setTextCursor(self.cursor) + #-------------------------------------------------------------------------------------------------- def findBlocks(self, first = 0, last = None, exclude = []): + ''' + Divide text in blocks + ''' + blocks = [] if last == None: last = self.document().characterCount() @@ -1755,21 +2047,59 @@ def findBlocks(self, first = 0, last = None, exclude = []): return blocks def blocks2list(self, blocks, mode = None): + ''' + Convert a block to a string. + If a mode is specified, preform custom modification to the text. + ''' + text = [] + + toggle = None + for block in blocks: + blockText = block.text() + + #------------------------------------------------------------------------------------------ + if mode == 'unindent': + if blockText.startswith(' '*4): blockText = blockText[4:] self.lastChar -= 4 + elif blockText.startswith('\t'): blockText = blockText[1:] self.lastChar -= 1 + #------------------------------------------------------------------------------------------ + elif mode == 'indent': blockText = ' '*4 + blockText self.lastChar += 4 + #------------------------------------------------------------------------------------------ + + elif mode == 'comment': + + unindentedBlockText = blockText.lstrip() + indents = len(blockText) - len(unindentedBlockText) + + if toggle is None: + toggle = not unindentedBlockText.startswith('#') + + #kill comment + if unindentedBlockText.startswith('# '): + unindentedBlockText = unindentedBlockText[2:] + + elif unindentedBlockText.startswith('#'): + unindentedBlockText = unindentedBlockText[1:] + + #combine + blockText = (' ' * indents) + ('# ' * int(toggle)) + unindentedBlockText + + #------------------------------------------------------------------------------------------ + text.append(blockText) return text @@ -1782,6 +2112,7 @@ def highlightCurrentLine(self): ''' Highlight currently selected line ''' + extraSelections = [] selection = QtWidgets.QTextEdit.ExtraSelection() @@ -1801,6 +2132,7 @@ def highlightCurrentLine(self): self.setExtraSelections(extraSelections) class LineNumberArea(QtWidgets.QWidget): + def __init__(self, scriptEditor): super(LineNumberArea, self).__init__(scriptEditor) @@ -1811,7 +2143,7 @@ def paintEvent(self, event): self.scriptEditor.lineNumberAreaPaintEvent(event) return -class scriptEditorHighlighter(QtGui.QSyntaxHighlighter): +class ScriptEditorHighlighter(QtGui.QSyntaxHighlighter): ''' Modified, simplified version of some code found I found when researching: wiki.python.org/moin/PyQt/Python%20syntax%20highlighting @@ -1821,7 +2153,7 @@ class scriptEditorHighlighter(QtGui.QSyntaxHighlighter): def __init__(self, document): - super(scriptEditorHighlighter, self).__init__(document) + super(ScriptEditorHighlighter, self).__init__(document) self.styles = { 'keyword': self.format([238,117,181],'bold'), @@ -1912,13 +2244,13 @@ def highlightBlock(self, text): self.setCurrentBlockState(0) # Do multi-line strings - in_multiline = self.match_multiline(text, *self.tri_single) + in_multiline = self.matchMultiline(text, *self.tri_single) if not in_multiline: - in_multiline = self.match_multiline(text, *self.tri_double) + in_multiline = self.matchMultiline(text, *self.tri_double) - def match_multiline(self, text, delimiter, in_state, style): + def matchMultiline(self, text, delimiter, in_state, style): ''' - Check whether highlighting reuires multiple lines. + Check whether highlighting requires multiple lines. ''' # If inside triple-single quotes, start at 0 if self.previousBlockState() == in_state: @@ -1947,7 +2279,7 @@ def match_multiline(self, text, delimiter, in_state, style): # Look for the next match start = delimiter.indexIn(text, start + length) - # Return True if still inside a multi-line string, False otherwise + # Return True if still inside a multi-line string, False Otherwise if self.currentBlockState() == in_state: return True else: @@ -1957,11 +2289,11 @@ def match_multiline(self, text, delimiter, in_state, style): #Template Button #------------------------------------------------------------------------------------------------------ -class scriptEditorTemplateMenu(QtWidgets.QMenu): +class ScriptEditorTemplateMenu(QtWidgets.QMenu): def __init__(self, parentObject): - super(scriptEditorTemplateMenu,self).__init__() + super(ScriptEditorTemplateMenu,self).__init__() self.hotbox = parentObject @@ -2147,6 +2479,7 @@ def setModel(self, model): self.connect(self.selectionModel(),QtCore.SIGNAL("selectionChanged(QItemSelection, QItemSelection)"), self.setSelectedItems) #-------------------------------------------------------------------------------------------------- + def setEnabled(self, mode = True): self.enabled = mode @@ -2161,15 +2494,13 @@ def populateTree(self): Fill the QTreeView with items associated with the selected nodeclass ''' - #---------------------------------------------------------------------------------------------- - #store current scope as previous scope self.previousScope = self.scope self.setEnabled(True) #find current scope - if not self.parentClass.selectionSpecific: + if not self.parentClass.contextual: self.scope = self.parentClass.path + '/' else: @@ -2179,9 +2510,14 @@ def populateTree(self): self.setEnabled(False) return - classItem = classItems[0].text() + '/' + classItem = classItems[0] + classItemText = classItem.text() + + if self.parentClass.mode == 'Rules': + if not int(classItem.checkState()): + classItemText += '_' - self.scope = self.parentClass.path + '/' + classItem + self.scope = self.parentClass.path + '/' + classItemText + '/' if self.previousScope == self.scope: self.update = True @@ -2206,7 +2542,7 @@ def populateTree(self): self.clearTree() #Fill the buttonstree if there is an item selected in the classescolumn, or the mode is set to all. - if not self.parentClass.selectionSpecific or self.parentClass.classesList.selectedItems() != 0: + if not self.parentClass.contextual or self.parentClass.classesList.selectedItems() != 0: self.addChild(self.root,self.scope) #Expand/Collapse @@ -2220,10 +2556,12 @@ def populateTree(self): index = self.dataModel.indexFromItem(button) self.collapse(index) + self.parentClass.toggleRulesMode() + def clearTree(self): ''' empty the tree - self.dataModel.clear() #unfortunately this crashes Nuke + self.dataModel.clear() unfortunately this Nuke ''' for row in range(self.dataModel.rowCount()): self.dataModel.takeRow(0) @@ -2257,11 +2595,13 @@ def addChild(self, parent, path): def setSelectedItems(self): - + ''' + Run when items gets selected + ''' self.selectedItems = [index.model().itemFromIndex(index) for index in self.selectedIndexes()] self.selectedItemsPaths = set([i.path for i in self.selectedItems]) - self.parentClass.loadScriptEditor() + self.parentClass.toggleRulesMode(False) def moveItem(self, direction): ''' @@ -2527,16 +2867,17 @@ def addItem(self, folder = False): folderPath = os.path.dirname(selectedItem.path) + '/' #make sure all the files inside the folder are named correctly - repairHotbox(folder = folderPath, recursive = False, message = False) + RepairHotbox(folder = folderPath, recursive = False, message = False) #loop over content of folder to find an appropriate name for the new item itemPath = getFirstAvailableFilePath(folderPath) if not folder: + itemName = 'New Item' itemPath += '.py' - newFileContent = fileHeader(itemName).getHeader() + newFileContent = FileHeader(itemName).getHeader() currentFile = open(itemPath, 'w') currentFile.write(newFileContent) currentFile.close() @@ -2590,7 +2931,7 @@ def removeItem(self): #make sure all the files inside the folder are named correctly changedFolder = os.path.dirname(currentItem.path) - repairHotbox(folder = changedFolder, recursive = False, message = False) + RepairHotbox(folder = changedFolder, recursive = False, message = False) self.populateTree() @@ -2617,7 +2958,7 @@ def pasteItem(self): if len(self.clipboard) > 0: #make sure all the files inside the folder are named correctly - repairHotbox(folder = self.scope, recursive = False, message = False) + RepairHotbox(folder = self.scope, recursive = False, message = False) for path in self.clipboard: @@ -2785,14 +3126,14 @@ def updateIcon(self, mode = 'neutral'): #rename dialog #------------------------------------------------------------------------------------------------------ -class renameDialog(QtWidgets.QDialog): +class RenameDialog(QtWidgets.QDialog): ''' Dialog that will pop up when the rename button in the manager is clicked. ''' def __init__(self, currentName, new = False): - super(renameDialog, self).__init__() + super(RenameDialog, self).__init__() self.currentName = currentName @@ -2802,14 +3143,23 @@ def __init__(self, currentName, new = False): self.hotboxManager = hotboxManagerInstance + #list of all items currently in list + self.allItems = self.hotboxManager.classesList.allItemNames() + if self.currentName in self.allItems: + self.allItems.remove(self.currentName) + + enitity = 'Class' + if self.hotboxManager.mode == 'Rules': + enitity = 'Rule' + #window title if self.new: renameButtonLabel = 'Create' - self.setWindowTitle('New class') + self.setWindowTitle('New ' + enitity) else: renameButtonLabel = 'Rename' - self.setWindowTitle('Rename class') + self.setWindowTitle('Rename ' + enitity) #layout masterLayout = QtWidgets.QVBoxLayout() @@ -2819,14 +3169,16 @@ def __init__(self, currentName, new = False): self.newNameLineEdit.setText(self.currentName) self.newNameLineEdit.selectAll() - renameButton = QtWidgets.QPushButton(renameButtonLabel) + self.newNameLineEdit.textChanged.connect(self.validateName) + + self.renameButton = QtWidgets.QPushButton(renameButtonLabel) cancelButton = QtWidgets.QPushButton('Cancel') - renameButton.clicked.connect(self.renameButtonClicked) + self.renameButton.clicked.connect(self.renameButtonClicked) cancelButton.clicked.connect(self.cancelRenameDialog) - buttonsLayout.addWidget(renameButton) - buttonsLayout.addWidget(cancelButton) + for button in [self.renameButton, cancelButton]: + buttonsLayout.addWidget(button) masterLayout.addWidget(self.newNameLineEdit) masterLayout.addLayout(buttonsLayout) @@ -2834,7 +3186,7 @@ def __init__(self, currentName, new = False): #shortcuts self.enterAction = QtWidgets.QAction(self) - self.enterAction.setShortcut(QtWidgets.QKeySequence(QtCore.Qt.Key_Return)) + self.enterAction.setShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Return)) self.enterAction.triggered.connect(self.renameButtonClicked) self.addAction(self.enterAction) @@ -2843,6 +3195,25 @@ def __init__(self, currentName, new = False): screenRes = QtWidgets.QDesktopWidget().screenGeometry() self.move(QtCore.QPoint(screenRes.width()/2,screenRes.height()/2)-QtCore.QPoint((self.width()/2),(self.height()/2))) + def validateName(self): + ''' + Check the imput name and disable the 'Rename button' accordingly. + ''' + text = self.newNameLineEdit.text() + valid = True + + try: + + valid *= text not in self.allItems + valid *= len(text) > 0 + valid *= text[-1] != '_' + valid *= text[0] != '_' + except: + + valid = False + + self.renameButton.setEnabled(valid) + def renameButtonClicked(self): currentPath = self.hotboxManager.path + '/' + self.currentName @@ -2883,14 +3254,14 @@ def closeRenameDialog(self): #Dialog with contact informaton #------------------------------------------------------------------------------------------------------ -class aboutDialog(QtWidgets.QFrame): +class AboutDialog(QtWidgets.QFrame): ''' Dialog that will show some information about the current version of the Hotbox. ''' def __init__(self): - super(aboutDialog, self).__init__() + super(AboutDialog, self).__init__() self.setWindowFlags(QtCore.Qt.ToolTip) @@ -3029,10 +3400,11 @@ def mouseReleaseEvent(self,event): #Top portion of the files that will be generated #------------------------------------------------------------------------------------------------------ -class fileHeader(): - def __init__(self, name, color = None, textColor = None): +class FileHeader(): + def __init__(self, name, color = None, textColor = None, rule = False): dividerLine = '-'*106 + text = ['#%s'%dividerLine, '#', '# AUTOMATICALLY GENERATED FILE TO BE USED BY W_HOTBOX', @@ -3041,6 +3413,9 @@ def __init__(self, name, color = None, textColor = None): '#', '#%s\n\n'%dividerLine] + if rule: + text[4] = text[4].replace('# NAME:','# IGNORE CLASSES:') + # add extra attributes if available if textColor: text.insert(5,'# TEXTCOLOR: %s'%textColor) @@ -3057,7 +3432,7 @@ def getHeader(self): #Repair #------------------------------------------------------------------------------------------------------ -class repairHotbox(): +class RepairHotbox(): #-------------------------------------------------------------------------------------------------- def __init__(self, folder = None, recursive = True, message = True): @@ -3112,7 +3487,7 @@ def __init__(self, folder = None, recursive = True, message = True): self.repairFolder(i) if message: - nuke.message('Reparation succesfully') + nuke.message('Succesfully repaired') #-------------------------------------------------------------------------------------------------- @@ -3159,7 +3534,7 @@ def repairFolder(self, folderPath): #-------------------------------------------------------------------------------------------------- -def clearHotboxManager(sections = ['Single','Multiple','All']): +def clearHotboxManager(sections = ['Single','Multiple','All','Rules']): ''' Clear the buttons of the section specified. By default all buttons will be erased. ''' @@ -3207,6 +3582,7 @@ def clearHotboxManager(sections = ['Single','Multiple','All']): # Commenly used functions #-------------------------------------------------------------------------------------------------- + def getAttributeFromFile(path, attribute = 'name'): ''' Scan file for the appropriate attribute. @@ -3273,7 +3649,9 @@ def showHotboxManager(path = ''): ''' Launch an instance of the hotbox manager ''' + global hotboxManagerInstance + #check if the manager is opened already, if so close that instance. if hotboxManagerInstance != None: hotboxManagerInstance.close() @@ -3284,5 +3662,5 @@ def showHotboxManager(path = ''): if path[-1] != '/': path += '/' - hotboxManagerInstance = hotboxManager(path) + hotboxManagerInstance = HotboxManager(path) hotboxManagerInstance.show() \ No newline at end of file diff --git a/W_hotbox_UserGuide_v1.7.pdf b/W_hotbox_UserGuide_v1.8.pdf similarity index 95% rename from W_hotbox_UserGuide_v1.7.pdf rename to W_hotbox_UserGuide_v1.8.pdf index f489be5..e9861ee 100644 Binary files a/W_hotbox_UserGuide_v1.7.pdf and b/W_hotbox_UserGuide_v1.8.pdf differ