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