diff --git a/__init__.py b/__init__.py
index dcd7d2e..c248234 100644
--- a/__init__.py
+++ b/__init__.py
@@ -95,11 +95,26 @@ def __init__(self, moduleItem, attributes, *, mainWindow=None, **kwargs):
layout.setColumnStretch(1, 1)
self.setLayout(layout)
- globEnv = self.mainWindow.getModuleGlobalEnv()
- globEnv.update({"module": ModuleWrapper(self.moduleItem.module), "ch": self.moduleItem.module.ch, "chset": self.moduleItem.module.chset})
+ def executor(cmd, env=None):
+ envUI = self.mainWindow.getEnvUI()
+ RuntimeModule.env = envUI # update environment for runtime modules
+
+ localEnv = dict(envUI)
+ localEnv.update(self.moduleItem.module.getEnv())
+ localEnv.update(env or {})
+
+ with captureOutput(self.mainWindow.logWidget):
+ try:
+ exec(cmd, localEnv)
+ except Exception as e:
+ print("Error: "+str(e))
+ self.mainWindow.showLog()
+ else:
+ self.updateWidgets()
+ return localEnv
for a in attributes:
- templateWidget = widgets.TemplateWidgets[a.template](env=globEnv)
+ templateWidget = widgets.TemplateWidgets[a.template](executor=executor)
nameWidget = QLabel(a.name)
self._attributeAndWidgets.append((a, nameWidget, templateWidget))
@@ -109,7 +124,6 @@ def __init__(self, moduleItem, attributes, *, mainWindow=None, **kwargs):
self.updateWidgetStyle(idx)
templateWidget.somethingChanged.connect(lambda idx=idx: self.widgetOnChange(idx))
- templateWidget.needUpdateUI.connect(self.updateWidgets)
nameWidget.setAlignment(Qt.AlignRight)
nameWidget.setStyleSheet("QLabel:hover:!pressed{ background-color: #666666; }")
@@ -157,10 +171,17 @@ def nameContextMenuEvent(self, event, attrWidgetIndex):
if attr.connect:
menu.addAction("Break connection", Callback(self.disconnectAttr, attrWidgetIndex))
- menu.addSeparator()
+
+ menu.addSeparator()
menu.addAction("Edit data", Callback(self.editData, attrWidgetIndex))
+ menu.addSeparator()
menu.addAction("Edit expression", Callback(self.editExpression, attrWidgetIndex))
+
+ if attr.expression:
+ menu.addAction("Evaluate expression", Callback(self.updateWidget, attrWidgetIndex))
+ menu.addAction("Clear expression", Callback(self.clearExpression, attrWidgetIndex))
+
menu.addSeparator()
menu.addAction("Expose", Callback(self.exposeAttr, attrWidgetIndex))
menu.addSeparator()
@@ -168,46 +189,59 @@ def nameContextMenuEvent(self, event, attrWidgetIndex):
menu.popup(event.globalPos())
+ def _wrapper(f):
+ def inner(self, attrWidgetIndex, *args, **kwargs):
+ attr, _, _ = self._attributeAndWidgets[attrWidgetIndex]
+ with captureOutput(self.mainWindow.logWidget):
+ try:
+ return f(self, attrWidgetIndex, *args, **kwargs)
+
+ except Exception as e:
+ print("Error: {}.{}: {}".format(self.moduleItem.module.name, attr.name, str(e)))
+ self.mainWindow.showLog()
+
+ return inner
+
+ @_wrapper
def widgetOnChange(self, attrWidgetIndex):
attr, _, widget = self._attributeAndWidgets[attrWidgetIndex]
- newData = widget.getJsonData()
- oldData = copyJson(attr.data)
- attr.data = newData
+ previousAttrsData = {id(otherAttr): copyJson(otherAttr.data) for otherAttr in self.moduleItem.module.getAttributes()}
- if attr.connect:
- _, srcAttr = self.moduleItem.module.findConnectionSourceForAttribute(attr)
- if srcAttr:
- srcAttr.updateFromAttribute(attr)
- else:
- if not Attribute.isDataSame(oldData, newData): # compare without value and expression
- self.moduleItem.module.modified = True
- self.moduleItem.emitDataChanged()
+ runtimeAttr = RuntimeAttribute(self.moduleItem.module, attr)
+ widgetData = widget.getJsonData()
+ runtimeAttr.data = copyJson(widgetData)
+ runtimeAttr.push()
+
+ modifiedAttrs = []
+ for otherAttr in self.moduleItem.module.getAttributes():
+ RuntimeAttribute(self.moduleItem.module, otherAttr).pull()
+
+ if otherAttr.data != previousAttrsData[id(otherAttr)]:
+ otherAttr.modified = True
+ modifiedAttrs.append(otherAttr)
- if oldData != newData and not attr.expression: # compare with default value
- attr.modified = True
- self.updateWidgetStyle(attrWidgetIndex)
+ for idx, (otherAttr, _, _) in enumerate(self._attributeAndWidgets): # update attributes' widgets
+ if otherAttr in modifiedAttrs:
+ self.updateWidget(idx)
+ self.updateWidgetStyle(idx)
+ if attr.data != widgetData:
+ widget.blockSignals(True)
+ widget.setJsonData(attr.data)
+ widget.blockSignals(False)
+ self.updateWidgetStyle(attrWidgetIndex)
+
+ @_wrapper
def updateWidget(self, attrWidgetIndex):
attr, _, widget = self._attributeAndWidgets[attrWidgetIndex]
- with captureOutput(self.mainWindow.logWidget):
- error = None
- try:
- self.moduleItem.module.resolveExpression(attr)
- self.moduleItem.module.resolveConnection(attr)
- widget.setJsonData(attr.data)
- except AttributeExpressionError as e:
- error = "Error: " + str(e)
- except Exception as e:
- error = "Error: '{}' has invalid or incompatible JSON data".format(attr.name)
-
- if error:
- print(error)
- attr.data = widget.getDefaultData()
- widget.setJsonData(attr.data)
- self.mainWindow.showLog()
- self.mainWindow.logWidget.ensureCursorVisible()
+ runtimeAttr = RuntimeAttribute(self.moduleItem.module, attr)
+ runtimeAttr.pull()
+
+ widget.blockSignals(True)
+ widget.setJsonData(runtimeAttr.data)
+ widget.blockSignals(False)
def updateWidgets(self):
for i in range(len(self._attributeAndWidgets)):
@@ -221,15 +255,16 @@ def updateWidgetStyle(self, attrWidgetIndex):
if attr.connect:
tooltip.append("Connect: "+attr.connect)
if attr.expression:
- tooltip.append("Expression:\n"+attr.expression)
+ tooltip.append("Expression:\n" + attr.expression)
- if attr.connect:
- style = "TemplateWidget {border: 4px solid #6e6e39; background-color: #6e6e39}"
- elif attr.expression:
- style = "TemplateWidget {border: 4px solid #632094; background-color: #632094}"
-
- if attr.expression and attr.connect: # both, invalid
- style = "TemplateWidget {border: 4px solid #781656; background-color: #781656}"
+ if attr.connect and not attr.expression: # only connection
+ style = "TemplateWidget { border: 4px solid #6e6e39; background-color: #6e6e39 }"
+
+ elif attr.expression and not attr.connect: # only expression
+ style = "TemplateWidget { border: 4px solid #632094; background-color: #632094 }"
+
+ elif attr.expression and attr.connect: # both
+ style = "TemplateWidget { border: 4px solid rgb(0,0,0,0); background: QLinearGradient( x1: 0, y1: 0, x2: 1, y2:0, stop: 0 #6e6e39, stop: 1 #632094);}"
nameWidget.setText(attr.name+("*" if attr.modified else ""))
@@ -271,23 +306,29 @@ def save(data):
def editExpression(self, attrWidgetIndex):
def save(text):
attr.expression = text
- self.updateWidget(attrWidgetIndex)
+ self.updateWidgets()
self.updateWidgetStyle(attrWidgetIndex)
attr, _, _ = self._attributeAndWidgets[attrWidgetIndex]
+ words = set(self.mainWindow.getEnvUI().keys()) | set(self.moduleItem.module.getEnv().keys())
placeholder = '# Example: value = ch("../someAttr") + 1 or data["items"] = [1,2,3]'
- w = widgets.EditTextDialog(attr.expression, title="Edit expression for '{}'".format(attr.name), placeholder=placeholder, python=True)
+ w = widgets.EditTextDialog(attr.expression, title="Edit expression for '{}'".format(attr.name), placeholder=placeholder, words=words, python=True)
w.saved.connect(save)
w.show()
+ def clearExpression(self, attrWidgetIndex):
+ attr, _, _ = self._attributeAndWidgets[attrWidgetIndex]
+ attr.expression = ""
+ self.updateWidgetStyle(attrWidgetIndex)
+
def resetAttr(self, attrWidgetIndex):
- attr, _, widget = self._attributeAndWidgets[attrWidgetIndex]
+ attr, _, _ = self._attributeAndWidgets[attrWidgetIndex]
tmp = widgets.TemplateWidgets[attr.template]()
attr.data = tmp.getDefaultData()
attr.connect = ""
- widget.setJsonData(attr.data)
+ self.updateWidget(attrWidgetIndex)
self.updateWidgetStyle(attrWidgetIndex)
def disconnectAttr(self, attrWidgetIndex):
@@ -296,10 +337,9 @@ def disconnectAttr(self, attrWidgetIndex):
self.updateWidgetStyle(attrWidgetIndex)
def connectAttr(self, connect, attrWidgetIndex):
- attr, _, widget = self._attributeAndWidgets[attrWidgetIndex]
+ attr, _, _ = self._attributeAndWidgets[attrWidgetIndex]
attr.connect = connect
- self.moduleItem.module.resolveConnection(attr)
- widget.setJsonData(attr.data)
+ self.updateWidget(attrWidgetIndex)
self.updateWidgetStyle(attrWidgetIndex)
class AttributesTabWidget(QTabWidget):
@@ -562,7 +602,10 @@ def __init__(self, module, **kwargs):
self.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable | Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled)
def clone(self):
- return ModuleItem(self.module.copy())
+ item = ModuleItem(self.module.copy())
+ for i in range(self.childCount()):
+ item.addChild(self.child(i).clone())
+ return item
def data(self, column, role):
if column == 0: # name
@@ -650,6 +693,9 @@ def setData(self, column, role, value):
if column == 0:
if role == Qt.EditRole:
newName = replaceSpecialChars(value).strip()
+ if self.parent():
+ existingNames = set([ch.name for ch in self.parent().module.getChildren()])
+ newName = findUniqueName(newName, existingNames)
connections = self._saveConnections(self.module) # rename in connections
self.module.name = newName
@@ -732,7 +778,6 @@ def dragEnterEvent(self, event):
if event.mouseButtons() == Qt.MiddleButton:
self.dragItems = self.selectedItems()
- self.dragParents = [item.parent() for item in self.dragItems]
else:
event.ignore()
@@ -743,6 +788,11 @@ def dragMoveEvent(self, event):
event.setDropAction(Qt.CopyAction)
def dropEvent(self, event):
+ for item in self.dragItems:
+ if item.parent():
+ item.parent().module.modified = True
+ item.parent().emitDataChanged()
+
super().dropEvent(event)
if event.mimeData().hasUrls():
@@ -760,23 +810,20 @@ def dropEvent(self, event):
print(e)
print("Error '{}': invalid module".format(path))
self.mainWindow.showLog()
- self.mainWindow.logWidget.ensureCursorVisible()
-
- if self.dragItems:
- for oldParent, item in zip(self.dragParents, self.dragItems):
- if oldParent:
- oldParent.module.removeChild(item.module)
+ else:
+ for item in self.dragItems:
+ if item.module.parent: # remove from old parent
+ item.module.parent.removeChild(item.module)
newParent = item.parent()
if newParent:
- if item.module in newParent.module.getChildren():
- newParent.module.removeChild(item.module)
-
idx = newParent.indexOfChild(item)
newParent.module.insertChild(idx, item.module)
+
+ newParent.module.modified = True
+ newParent.emitDataChanged()
self.dragItems = []
- self.dragParents = []
def makeItemFromModule(self, module):
item = ModuleItem(module)
@@ -844,7 +891,6 @@ def importModule(self):
except ET.ParseError:
print("Error '{}': invalid module".format(filePath))
self.mainWindow.showLog()
- self.mainWindow.logWidget.ensureCursorVisible()
def saveModule(self):
selectedItems = self.selectedItems()
@@ -942,10 +988,12 @@ def muteModule(self):
def duplicateModule(self):
newItems = []
for item in self.selectedItems():
- parent = item.parent()
-
newItem = self.makeItemFromModule(item.module.copy())
+ if item.parent():
+ existingNames = set([ch.name for ch in item.parent().module.getChildren()])
+ newItem.module.name = findUniqueName(item.module.name, existingNames)
+ parent = item.parent()
if parent:
parent.addChild(newItem)
parent.module.addChild(newItem.module)
@@ -973,6 +1021,9 @@ def removeModule(self):
if parent:
parent.removeChild(item)
parent.module.removeChild(item.module)
+
+ parent.module.modified = True
+ parent.emitDataChanged()
else:
self.invisibleRootItem().removeChild(item)
@@ -1150,6 +1201,7 @@ def downBtnClicked(self):
w.nameWidget.setText(self.nameWidget.text())
w.attrConnect = self.attrConnect
w.attrExpression = self.attrExpression
+ w.attrCallback = self.attrCallback
w.attrModified = self.attrModified
self.deleteLater()
@@ -1162,6 +1214,7 @@ def upBtnClicked(self):
w.nameWidget.setText(self.nameWidget.text())
w.attrConnect = self.attrConnect
w.attrExpression = self.attrExpression
+ w.attrCallback = self.attrCallback
w.attrModified = self.attrModified
self.deleteLater()
@@ -1417,14 +1470,14 @@ def generateCompletionWords(self):
if not self.moduleItem:
return
- words = list(self.mainWindow.getModuleGlobalEnv().keys())
- words.extend(list(widgets.WidgetsAPI.keys()))
+ words = set(self.mainWindow.getEnvUI().keys()) | set(self.moduleItem.module.getEnv().keys())
for a in self.moduleItem.module.getAttributes():
- words.append("@" + a.name)
- words.append("@set_" + a.name)
+ words.add("@" + a.name)
+ words.add("@" + a.name + "_data")
+ words.add("@set_" + a.name)
- self.editorWidget.words = set(words)
+ self.editorWidget.words = words
class LogHighligher(QSyntaxHighlighter):
def __init__(self, parent):
@@ -1567,7 +1620,7 @@ def __init__(self):
self.runBtn = QPushButton("Run!")
self.runBtn.setStyleSheet("background-color: #3e4f89")
- self.runBtn.clicked.connect(self.runModulesBtnClicked)
+ self.runBtn.clicked.connect(self.runModuleClicked)
self.runBtn.hide()
self.infoWidget = QTextBrowser()
@@ -1636,7 +1689,7 @@ def getMenu(self):
helpMenu.addAction("Documentation", self.showDocumenation)
return menu
-
+
def copyToolCode(self):
selectedItems = self.treeWidget.selectedItems()
if selectedItems:
@@ -1745,30 +1798,23 @@ def showLog(self):
if sizes[-1] < 10:
sizes[-1] = 200
self.vsplitter.setSizes(sizes)
+ self.logWidget.ensureCursorVisible()
- def getModuleGlobalEnv(self):
- env = {"beginProgress": self.progressBarWidget.beginProgress,
- "stepProgress": self.progressBarWidget.stepProgress,
- "endProgress": self.progressBarWidget.endProgress,
- "currentTabIndex": self.attributesTabWidget.currentIndex()}
-
- for k,v in getModuleDefaultEnv().items():
- env[k] = v
-
- for k, f in widgets.WidgetsAPI.items():
- env[k] = f
-
- return env
+ def getEnvUI(self):
+ return {"beginProgress": self.progressBarWidget.beginProgress,
+ "stepProgress": self.progressBarWidget.stepProgress,
+ "endProgress": self.progressBarWidget.endProgress,
+ "currentTabIndex": self.attributesTabWidget.currentIndex()}
- def runModulesBtnClicked(self):
+ def runModuleClicked(self):
if DCC == "maya":
cmds.undoInfo(ock=True) # run in undo chunk
- self.runModules()
+ self.runModule()
cmds.undoInfo(cck=True)
else:
- self.runModules()
+ self.runModule()
- def runModules(self):
+ def runModule(self):
def uiCallback(mod):
self.progressBarWidget.stepProgress(self.progressCounter, mod.getPath())
self.progressCounter += 1
@@ -1806,8 +1852,10 @@ def getChildrenCount(item):
muted = currentItem.module.muted
currentItem.module.muted = False
+ RuntimeModule.env = self.getEnvUI() # for runtime module
+
try:
- currentItem.module.run(self.getModuleGlobalEnv(), uiCallback=uiCallback)
+ currentItem.module.run(env=self.getEnvUI(), uiCallback=uiCallback)
except Exception:
printErrorStack()
finally:
@@ -1838,7 +1886,7 @@ def RigBuilderTool(spec, child=None, *, size=None): # spec can be full path, rel
w.setWindowTitle("Rig Builder Tool - {}".format(module.getPath()))
w.treeWidget.addTopLevelItem(w.treeWidget.makeItemFromModule(module))
w.treeWidget.setCurrentItem(w.treeWidget.topLevelItem(0))
-
+
w.codeEditorWidget.hide()
w.treeWidget.hide()
@@ -1850,9 +1898,11 @@ def RigBuilderTool(spec, child=None, *, size=None): # spec can be full path, rel
w.resize(size[0], size[1])
else: # auto size
w.adjustSize()
-
+
return w
+ModulesAPI.update(widgets.WidgetsAPI)
+
if not os.path.exists(RigBuilderLocalPath):
os.makedirs(RigBuilderLocalPath+"/modules")
diff --git a/classes.py b/classes.py
index 3bce61b..c22d664 100644
--- a/classes.py
+++ b/classes.py
@@ -5,6 +5,9 @@
import json
import uuid
import xml.etree.ElementTree as ET
+from .utils import copyJson
+
+ModulesAPI = {} # updated at the end
if sys.version_info.major > 2:
RigBuilderPath = os.path.dirname(__file__)
@@ -24,36 +27,6 @@ def getUidFromFile(path):
if r:
return r.group(1)
-def smartConversion(x):
- v = None
- try:
- v = int(x)
- except ValueError:
- try:
- v = float(x)
- except ValueError:
- v = str(x)
- return v
-
-def copyJson(data):
- if data is None:
- return None
-
- elif type(data) in [list, tuple]:
- return [copyJson(x) for x in data]
-
- elif type(data) == dict:
- return {k:copyJson(data[k]) for k in data}
-
- elif type(data) in [int, float, bool, str]:
- return data
-
- elif sys.version_info.major < 3 and type(data) is unicode: # compatibility with python 2.7
- return data
-
- else:
- raise TypeError("Data of {} type is not JSON compatible: {}".format(type(data), str(data)))
-
def calculateRelativePath(path, root):
path = os.path.normpath(path)
path = path.replace(os.path.normpath(root)+"\\", "")
@@ -103,46 +76,13 @@ def __init__(self, name, data={}, category="", template="", connect="", expressi
self.modified = False # used by UI
def copy(self):
- return Attribute(self.name, copyJson(self.data), self.category, self.template, self.connect, self.expression)
-
- def __eq__(self, other):
- if not isinstance(other, Attribute):
- return False
-
- return self.name == other.name and\
- self.data == other.data and\
- self.category == other.category and\
- self.template == other.template # don't compare connections
-
- def hasDefault(self):
- return "default" in self.data
+ return Attribute(self.name, self.data, self.category, self.template, self.connect, self.expression)
def getDefaultValue(self):
- if self.hasDefault():
- return self.data[self.data["default"]]
+ return self.data[self.data["default"]]
def setDefaultValue(self, v):
- if self.hasDefault():
- self.data[self.data["default"]] = v
-
- @staticmethod
- def isDataSame(a, b):
- a = dict(a)
- b = dict(b)
-
- if "default" in a and "default" in b:
- a.pop(a["default"]) # remove default value
- b.pop(b["default"])
-
- if a == b:
- return True
-
- def updateFromAttribute(self, otherAttr):
- # copy default value if any
- if self.hasDefault() and otherAttr.hasDefault():
- self.setDefaultValue(copyJson(otherAttr.getDefaultValue()))
- else:
- self.data = copyJson(otherAttr.data)
+ self.data[self.data["default"]] = v
def toXml(self, *, keepConnection=True):
attrs = [("name", self.name),
@@ -153,9 +93,9 @@ def toXml(self, *, keepConnection=True):
attrsStr = " ".join(["{}=\"{}\"".format(k, v) for k, v in attrs])
data = dict(self.data) # here data can have additional keys for storing custom data
- if self.expression and keepConnection: # expressions are the part of neighbor modules, save them as connections
+ if self.expression:
data["_expression"] = self.expression
-
+
header = ""
return header.format(attribs=attrsStr, data=json.dumps(data))
@@ -208,14 +148,6 @@ def copy(self):
module.muted = self.muted
return module
- def __eq__(self, other):
- if not isinstance(other, Module):
- return False
- return self.uid == other.uid and\
- self.name == other.name and\
- self.runCode == other.runCode and\
- self.loadedFrom == other.loadedFrom
-
def clearChildren(self):
self._children = []
@@ -366,6 +298,7 @@ def update(self):
origAttr.data = foundAttr.data
origAttr.connect = foundAttr.connect
+ origAttr.expression = foundAttr.expression
newAttributes.append(origAttr)
@@ -472,8 +405,7 @@ def _listConnections(currentModule):
def findConnectionSourceForAttribute(self, attr):
if self.parent and attr.connect:
srcModule, srcAttr = self.parent.findModuleAndAttributeByPath(attr.connect)
- if srcAttr:
- return srcModule.findConnectionSourceForAttribute(srcAttr)
+ return srcModule, srcAttr
return self, attr
@@ -481,7 +413,7 @@ def findModuleAndAttributeByPath(self, path):
'''
Returns (module, attribute) by path, where path is /a/b/c, where c is attr, a/b is a parent relative path
'''
- *moduleList, attr = path.split("/")
+ *moduleList, attrName = path.split("/")
currentParent = self
for module in moduleList:
@@ -495,94 +427,55 @@ def findModuleAndAttributeByPath(self, path):
elif module == ".":
continue
- found = currentParent.findChild(module)
- if found:
- currentParent = found
+ ch = currentParent.findChild(module)
+ if ch:
+ currentParent = ch
else:
- return (None, None)
-
- found = currentParent.findAttribute(attr)
- return (currentParent, found) if found else (currentParent, None)
-
- def resolveConnection(self, attr):
- if not attr.connect:
- return
+ raise AttributeResolverError("Cannot resolve '{}' path".format(path))
- srcMod, srcAttr = self.findConnectionSourceForAttribute(attr)
+ attr = currentParent.findAttribute(attrName)
+ if not attr:
+ raise AttributeResolverError("Cannot find '{}' attribute".format(path))
- if srcAttr is not attr:
- if attr.template != srcAttr.template:
- raise AttributeResolverError("{}: '{}' has incompatible connection template".format(self.name, attr.name))
+ return currentParent, attr
- try:
- srcMod.resolveExpression(srcAttr)
- attr.updateFromAttribute(srcAttr)
-
- except TypeError:
- raise AttributeResolverError("{}: '{}' data is not JSON serializable".format(self.name, attr.name))
-
- else:
- raise AttributeResolverError("{}: cannot resolve connection for '{}' which is '{}'".format(self.name, attr.name, attr.connect))
-
- def resolveExpression(self, attr):
+ def executeExpression(self, attr):
if not attr.expression:
return
- env = {"module": ModuleWrapper(self), "ch": self.ch, "data": attr.data, "value": copyJson(attr.getDefaultValue())}
+ localEnv = dict(self.getEnv())
+ localEnv.update({"data": attr.data, "value": attr.getDefaultValue()})
+
try:
- exec(attr.expression, env)
+ exec(attr.expression, localEnv)
except Exception as e:
- raise AttributeExpressionError("{}: '{}' has invalid expression: {}".format(self.name, attr.name, str(e)))
+ raise AttributeExpressionError("Invalid expression: {}".format(str(e)))
else:
- attr.setDefaultValue(env["value"])
+ attr.setDefaultValue(localEnv["value"])
- def ch(self, path, key=None):
- mod, attr = self.findModuleAndAttributeByPath(path)
- if attr:
- _, attr = mod.findConnectionSourceForAttribute(attr)
- if not key:
- return copyJson(attr.getDefaultValue())
- else:
- return AttributeWrapper(self, attr).data().get(key)
- else:
- raise AttributeResolverError("Attribute '{}' not found".format(path))
+ def getEnv(self):
+ mod = RuntimeModule(self)
- def chset(self, path, value, key=None):
- mod, attr = self.findModuleAndAttributeByPath(path)
- if attr:
- _, attr = mod.findConnectionSourceForAttribute(attr)
- if not key:
- attr.setDefaultValue(value)
- else:
- AttributeWrapper(self, attr).data()[key] = value
- else:
- raise AttributeResolverError("Attribute '{}' not found".format(path))
+ env = dict(ModulesAPI)
+ env.update({"module": mod,
+ "ch": mod.ch,
+ "chdata": mod.chdata,
+ "chset": mod.chset})
+ return env
- def run(self, env, *, uiCallback=None):
+ def run(self, *, env=None, uiCallback=None):
if self.muted:
return
- localEnv = {
- "module": ModuleWrapper(self),
- "ch": self.ch,
- "chset": self.chset
- }
-
- for k in env:
- if k not in localEnv: # don't overwrite locals
- localEnv[k] = env[k]
-
- ModuleWrapper.env = dict(env) # update environment for runtime modules
+ localEnv = dict(env or {})
+ localEnv.update(self.getEnv())
attrPrefix = "attr_"
for attr in self._attributes:
- self.resolveExpression(attr)
- self.resolveConnection(attr) # connection rewrites data
-
- attrWrapper = AttributeWrapper(self, attr)
- localEnv[attrPrefix+attr.name] = attrWrapper.get()
- localEnv[attrPrefix+"set_"+attr.name] = attrWrapper.set
- localEnv[attrPrefix+attr.name+"_data"] = attrWrapper.data()
+ runtimeAttr = RuntimeAttribute(self, attr)
+ localEnv[attrPrefix+attr.name] = runtimeAttr.get()
+ localEnv[attrPrefix+"set_"+attr.name] = runtimeAttr.set
+ localEnv[attrPrefix+attr.name+"_data"] = DataAccessor(RuntimeAttribute(self, attr))
print("{} is running...".format(self.getPath()))
@@ -595,7 +488,7 @@ def run(self, env, *, uiCallback=None):
pass
for ch in self._children:
- ch.run(env, uiCallback=uiCallback)
+ ch.run(env=env, uiCallback=uiCallback)
return localEnv
@@ -621,7 +514,7 @@ def findUids(path):
return uids
-# used inside modules in scripts
+# Runtime classes
class Dict(dict):
def __init__(self):
pass
@@ -632,34 +525,68 @@ def __getattr__(self, name):
def __setattr__(self, name, value):
self[name] = value
-class AttributeWrapper(object):
+class RuntimeAttribute(object):
def __init__(self, module, attr):
self._attr = attr
self._module = module
- def set(self, v):
- try:
- vcopy = copyJson(v)
- except TypeError:
- raise CopyJsonError("Cannot set non-JSON data for '{}' attribute (got {})".format(self._attr.name, v))
+ def pull(self):
+ if self._attr.connect:
+ srcMod, srcAttr = self._module.findConnectionSourceForAttribute(self._attr)
- srcAttr = self._attr
- if self._attr.connect and self._module.parent:
- _, srcAttr = self._module.findConnectionSourceForAttribute(self._attr)
+ RuntimeAttribute(srcMod, srcAttr).pull()
+ srcMod.executeExpression(srcAttr)
+ self._attr.setDefaultValue(copyJson(srcAttr.getDefaultValue()))
- default = srcAttr.data.get("default")
- if default:
- srcAttr.data[default] = vcopy
- else:
- srcAttr.data = vcopy
+ self._module.executeExpression(self._attr) # run expression on top of connection
+
+ def push(self):
+ if self._attr.connect:
+ srcMod, srcAttr = self._module.findConnectionSourceForAttribute(self._attr)
+
+ srcAttr.setDefaultValue(copyJson(self._attr.getDefaultValue()))
+ RuntimeAttribute(srcMod, srcAttr).push()
def get(self):
- default = self._attr.data.get("default")
- return copyJson(self._attr.data[default] if default else self._attr.data)
+ self.pull()
+ return copyJson(self._attr.getDefaultValue())
- def data(self):
- return self._attr.data
+ def set(self, value):
+ try:
+ valueCopy = copyJson(value)
+ except TypeError:
+ raise CopyJsonError("Cannot set non-JSON data (got {})".format(value))
+
+ self._attr.setDefaultValue(valueCopy)
+ self.push()
+ @property
+ def data(self): # data is local
+ return self._attr.data
+
+ @data.setter
+ def data(self, value): # data is local
+ self._attr.data = value
+
+class DataAccessor(): # for accessing data with @_data suffix inside a module's code
+ def __init__(self, runtimeAttr):
+ self._runtimeAttr = runtimeAttr
+
+ def __getitem__(self, name):
+ self._runtimeAttr.pull()
+ return self._runtimeAttr.data[name]
+
+ def __setitem__(self, name, value):
+ self._runtimeAttr.data[name] = value
+ self._runtimeAttr.push()
+
+ def __str__(self):
+ items = []
+ for k in self._runtimeAttr.data:
+ self._runtimeAttr.pull()
+ items.append("{}: {}".format(k, self._runtimeAttr.data[k]))
+ return "\n".join(items)
+
class AttrsWrapper(object): # attributes getter/setter
def __init__(self, module):
self._module = module
@@ -668,7 +595,7 @@ def __getattr__(self, name):
module = object.__getattribute__(self, "_module")
attr = module.findAttribute(name)
if attr:
- return AttributeWrapper(self._module, attr)
+ return RuntimeAttribute(self._module, attr)
else:
raise AttributeError("Attribute '{}' not found".format(name))
@@ -679,19 +606,11 @@ def __setattr__(self, name, value):
module = object.__getattribute__(self, "_module")
attr = module.findAttribute(name)
if attr:
- AttributeWrapper(self._module, attr).set(value)
+ RuntimeAttribute(self._module, attr).set(value)
else:
raise AttributeError("Attribute '{}' not found".format(name))
-'''
-How to use wrappers inside scripts.
-module.attr.someAttr.set(10)
-module.attr.someAttr = 5
-module.parent().attr.someAttr.set(20)
-print(@attr) # module.attr.attr.get()
-@set_attr(30) # module.attr.attr.set(30)
-'''
-class ModuleWrapper(object):
+class RuntimeModule(object):
glob = Dict() # global memory
env = {} # default environment for module scripts
@@ -704,30 +623,25 @@ def __init__(self, specOrModule): # spec is path or module
self.attr = AttrsWrapper(self._module)
- def __eq__(self, other):
- if not isinstance(other, ModuleWrapper):
- return False
- return self._module == other._module
-
def name(self):
return self._module.name
def child(self, nameOrIndex):
if type(nameOrIndex) == int:
- return ModuleWrapper(self._module.getChildren()[nameOrIndex])
+ return RuntimeModule(self._module.getChildren()[nameOrIndex])
elif type(nameOrIndex) == str:
m = self._module.findChild(nameOrIndex)
if m:
- return ModuleWrapper(m)
+ return RuntimeModule(m)
else:
raise ModuleNotFoundError("Child module '{}' not found".format(nameOrIndex))
def children(self):
- return [ModuleWrapper(ch) for ch in self._module.getChildren()]
+ return [RuntimeModule(ch) for ch in self._module.getChildren()]
def parent(self):
- return ModuleWrapper(self._module.parent) if self._module.parent else None
+ return RuntimeModule(self._module.parent) if self._module.parent else None
def muted(self):
return self._module.muted
@@ -742,10 +656,22 @@ def path(self):
return self._module.getPath()
def ch(self, path, key=None):
- return self._module.ch(path, key)
-
+ mod, attr = self._module.findModuleAndAttributeByPath(path)
+ if key:
+ return RuntimeAttribute(mod, attr).data[key]
+ else:
+ return RuntimeAttribute(mod, attr).get()
+
+ def chdata(self, path):
+ mod, attr = self._module.findModuleAndAttributeByPath(path)
+ return copyJson(RuntimeAttribute(mod, attr).data)
+
def chset(self, path, value, key=None):
- self._module.chset(path, value, key)
+ mod, attr = self._module.findModuleAndAttributeByPath(path)
+ if not key:
+ RuntimeAttribute(mod, attr).set(value)
+ else:
+ RuntimeAttribute(mod, attr).data[key] = value
def run(self):
muted = self._module.muted
@@ -753,32 +679,31 @@ def run(self):
env = {}
try:
- env = self._module.run(ModuleWrapper.env)
+ env = self._module.run(env=RuntimeModule.env)
except:
raise
finally:
self._module.muted = muted
return env
-def getModuleDefaultEnv():
- def printError(msg):
- raise RuntimeError(msg)
-
- def printWarning(msg):
- print("Warning: "+msg)
-
- def exitModule():
- raise ExitModuleException()
-
- env = {"module":None, # setup in Module.run
- "Module": ModuleWrapper,
- "ch": None, # setup in Module.run
- "chset": None, # setup in Module.run
- "copyJson": copyJson,
- "exit": exitModule,
- "error": printError,
- "warning": printWarning}
-
- return env
+def printError(msg):
+ raise RuntimeError(msg)
+
+def printWarning(msg):
+ print("Warning: "+msg)
+
+def exitModule():
+ raise ExitModuleException()
+
+ModulesAPI.update({
+ "module":None, # updated at runtime
+ "Module": RuntimeModule,
+ "ch": None, # updated at runtime
+ "chdata": None, # updated at runtime
+ "chset": None, # updated at runtime
+ "copyJson": copyJson,
+ "exit": exitModule,
+ "error": printError,
+ "warning": printWarning})
Module.updateUidsCache()
diff --git a/jsonWidget.py b/jsonWidget.py
index 258d3cd..16166f9 100644
--- a/jsonWidget.py
+++ b/jsonWidget.py
@@ -5,7 +5,7 @@
import re
import os
-from .utils import clamp, getActions, centerWindow, setActionsLocalShortcut, SimpleUndo, SearchReplaceDialog, JsonColors
+from .utils import clamp, getActions, centerWindow, setActionsLocalShortcut, SimpleUndo, SearchReplaceDialog, JsonColors, findUniqueName
RootDirectory = os.path.dirname(__file__)
@@ -158,18 +158,6 @@ def data(self, _, role):
else:
return super().data(0, role)
- def findUniqueKey(self, key="key"):
- def keyExists(k):
- return any([self.child(i).data(0, JsonItem.KeyRole) == k for i in range(self.childCount())])
-
- keyNoNum = re.sub(r"\d+$", "", key) # remove trailing numbers
- newKey = key
- i = 1
- while keyExists(newKey):
- newKey = keyNoNum + str(i)
- i += 1
- return newKey
-
def getPath(self, path=""):
parent = self.parent()
if not parent:
@@ -234,7 +222,7 @@ def __init__(self, data=None, **kwargs):
self.setReadOnly(self._readOnly)
if data:
- self.fromJsonList([data])
+ self.loadFromJsonList(data if type(data) == list else [data])
def getMenu(self):
menu = QMenu(self)
@@ -376,7 +364,9 @@ def editKey(self):
if key is not None:
newKey, ok = QInputDialog.getText(self, "Edit key", "Key:", text=key)
if ok:
- item.setData(0, item.KeyRole, item.parent().findUniqueKey(newKey))
+ existingKeys = set([item.parent().child(i).data(0, item.KeyRole) for i in range(item.parent().childCount())])
+ newKey = findUniqueName(newKey, existingKeys)
+ item.setData(0, item.KeyRole, newKey)
# undo
def f():
@@ -420,14 +410,14 @@ def loadFromFile(self):
self.clear()
with open(path, "r") as f:
d = json.load(f)
- self.fromJsonList([d])
+ self.loadFromJsonList([d])
def importFile(self):
path, _ = QFileDialog.getOpenFileName(self, "Import JSON", "", "JSON (*.json)")
if path:
with open(path, "r") as f:
d = json.load(f)
- self.fromJsonList([d])
+ self.loadFromJsonList([d])
def setRootItem(self, item=None):
if item and item.jsonType in [item.ListType, item.DictType]:
@@ -466,23 +456,43 @@ def revealSelected(self):
self.scrollToItem(selectedItems[-1], QAbstractItemView.PositionAtCenter)
def editItemData(self, item=None):
- def saveCallback(item, data):
- newItem = self.setItemJson(item, data)
- self.itemChanged.emit(item, 0)
+ def saveCallback(item, newData):
+ if item:
+ newItem = self.setItemJson(item, newData)
+ self.itemChanged.emit(item, 0)
- # undo
- def f():
- parent = newItem.parent() or self.invisibleRootItem()
- idx = parent.indexOfChild(newItem)
- parent.removeChild(newItem)
- parent.insertChild(idx, item.clone())
- self._undoSystem.push("EditData", f)
+ # undo
+ def f():
+ parent = newItem.parent() or self.invisibleRootItem()
+ idx = parent.indexOfChild(newItem)
+ parent.removeChild(newItem)
+ parent.insertChild(idx, item.clone())
+ self._undoSystem.push("EditData", f)
+
+ else:
+ if type(newData) != list:
+ newData = [newData]
+
+ oldData = self.toJsonList()
+ self.clear()
+ for d in newData:
+ item = self.itemFromJson(d)
+ self.addTopLevelItem(item)
+ self.itemChanged.emit(item, 0)
+
+ # undo
+ def f():
+ self.clear()
+ for d in oldData:
+ self.addTopLevelItem(self.itemFromJson(d))
+ self._undoSystem.push("EditData", f)
item = item or self.selectedItem()
- if item:
- dlg = EditJsonTextWindow(self.itemToJson(item), readOnly=self._readOnly)
- dlg.saved.connect(lambda data: saveCallback(item, data))
- dlg.exec_()
+ data = self.itemToJson(item) if item else self.toJsonList()
+
+ dlg = EditJsonTextWindow(data, readOnly=self._readOnly)
+ dlg.saved.connect(lambda data: saveCallback(item, data))
+ dlg.exec_()
def moveItem(self, direction):
selectedItems = self.selectedItems()
@@ -551,7 +561,8 @@ def addItem(self, json, parentItem=None, *, insertIndex=None):
if parentItem and parentItem is not self.invisibleRootItem():
if parentItem.jsonType == parentItem.DictType:
- key = parentItem.findUniqueKey()
+ existingKeys = set([parentItem.child(i).data(0, parentItem.KeyRole) for i in range(parentItem.childCount())])
+ key = findUniqueName("key", existingKeys)
item.setData(0, item.KeyRole, key)
elif parentItem.jsonType != parentItem.ListType:
return
@@ -624,14 +635,26 @@ def toJsonList(self):
return [self.itemToJson(self.topLevelItem(i)) for i in range(self.topLevelItemCount())]
def fromJsonList(self, dataList):
+ items = []
for d in dataList:
- self.addTopLevelItem(self.itemFromJson(d))
+ item = self.itemFromJson(d)
+ self.addTopLevelItem(item)
+ items.append(item)
+ return items
- self.expandItem(self.invisibleRootItem(), True)
+ def loadFromJsonList(self, dataList):
+ newItems = self.fromJsonList(dataList)
+ for item in newItems:
+ self.expandItem(item, True)
+
+ # undo
+ def f():
+ for item in newItems:
+ self.invisibleRootItem().removeChild(item)
+ self._undoSystem.push("Load", f)
+
self.dataLoaded.emit()
- self._undoSystem.flush()
-
def itemToJson(self, item):
if item.jsonType == item.ListType:
json = []
diff --git a/modules/example.xml b/modules/example.xml
index 513d0c5..bb28a91 100644
--- a/modules/example.xml
+++ b/modules/example.xml
@@ -5,10 +5,12 @@ print("lineAttr:", @lineAttr, type(@lineAttr))
beginProgress("Some slow operation", 10)
for i in range(10):
stepProgress(i)
- time.sleep(0.2)
+ time.sleep(0.1)
endProgress()
-@set_out_time(time.strftime(time.ctime()))
+@set_out_other("Hello world")
+
+chset("/out_lst", [1,2,3])
# children access
ch = module.child("child") # or module.child(0)
@@ -16,26 +18,36 @@ ch.run()
]]>
-
+
You can use html markup here
", "default": "text"}]]>
-
-
+
+
+
+
+
+
+print("lineAttr = {}".format(ch("../lineAttr")))]]>
-
+
+
+
+
+
-
-
+
+
+
+
+
+
diff --git a/runStandalone.py b/runStandalone.py
new file mode 100644
index 0000000..0041116
--- /dev/null
+++ b/runStandalone.py
@@ -0,0 +1,17 @@
+import sys
+import os
+
+from PySide2.QtCore import *
+from PySide2.QtGui import *
+from PySide2.QtWidgets import *
+
+app = QApplication([])
+
+os.environ["RIG_BUILDER_DCC"] = "standalone"
+import rigBuilder
+
+with open(rigBuilder.RigBuilderPath+"/stylesheet.css") as r:
+ app.setStyleSheet(r.read())
+
+rigBuilder.mainWindow.show()
+app.exec_()
diff --git a/utils.py b/utils.py
index ce68950..2af8716 100644
--- a/utils.py
+++ b/utils.py
@@ -1,12 +1,13 @@
import sys
import re
from contextlib import contextmanager
+import json
from PySide2.QtGui import *
from PySide2.QtCore import *
from PySide2.QtWidgets import *
-JsonColors = {"none": QColor("#000000"),
+JsonColors = {"none": QColor("#AAAAAA"),
"bool": QColor("#CDEB8B"),
"true": QColor("#82C777"),
"false": QColor("#CC6666"),
@@ -43,11 +44,51 @@ def clamp(val, low, high):
def replaceSpecialChars(text):
return re.sub("[^a-zA-Z0-9_]", "_", text)
+def findUniqueName(name, existingNames):
+ nameNoNum = re.sub(r"\d+$", "", name) # remove trailing numbers
+ newName = name
+ i = 1
+ while newName in existingNames:
+ newName = nameNoNum + str(i)
+ i += 1
+ return newName
+
def replacePairs(pairs, text):
for k, v in pairs:
text = re.sub(k, v, text)
return text
+def smartConversion(x):
+ try:
+ return json.loads(x)
+ except ValueError:
+ return str(x)
+
+def fromSmartConversion(x):
+ if sys.version_info.major > 2:
+ return json.dumps(x) if not isinstance(x, str) else x
+ else:
+ return json.dumps(x) if type(x) not in [str, unicode] else x
+
+def copyJson(data):
+ if data is None:
+ return None
+
+ elif type(data) in [list, tuple]:
+ return [copyJson(x) for x in data]
+
+ elif type(data) == dict:
+ return {k:copyJson(data[k]) for k in data}
+
+ elif type(data) in [int, float, bool, str]:
+ return data
+
+ elif sys.version_info.major < 3 and type(data) is unicode: # compatibility with python 2.7
+ return data
+
+ else:
+ raise TypeError("Data of '{}' type is not JSON compatible: {}".format(type(data), str(data)))
+
@contextmanager
def captureOutput(stream):
default_stdout = sys.stdout
diff --git a/widgets.py b/widgets.py
index 2242957..d573630 100644
--- a/widgets.py
+++ b/widgets.py
@@ -20,25 +20,18 @@
if DCC == "maya":
import maya.cmds as cmds
-def smartConversion(x):
- try:
- return json.loads(x)
- except ValueError:
- return str(x)
-
-def fromSmartConversion(x):
- if sys.version_info.major > 2:
- return json.dumps(x) if not isinstance(x, str) else x
- else:
- return json.dumps(x) if type(x) not in [str, unicode] else x
-
class TemplateWidget(QFrame):
somethingChanged = Signal()
- needUpdateUI = Signal()
- def __init__(self, env=None, **kwargs):
- super(TemplateWidget, self).__init__(**kwargs)
- self.env = env or {} # used to pass data to widgets
+ def __init__(self, *, executor=None, **kwargs):
+ super().__init__(**kwargs)
+ self.executor = executor or self._defaultExecutor # used to execute commands
+
+ def _defaultExecutor(self, cmd, env=None):
+ localEnv = dict(WidgetsAPI)
+ localEnv.update(env or {})
+ exec(cmd, localEnv)
+ return localEnv
def getDefaultData(self):
return self.getJsonData()
@@ -52,7 +45,7 @@ def setJsonData(self, data):
class EditTextDialog(QDialog):
saved = Signal(str) # emitted when user clicks OK
- def __init__(self, text="", *, title="Edit", placeholder="", python=False):
+ def __init__(self, text="", *, title="Edit", placeholder="", words=None, python=False):
super().__init__(parent=QApplication.activeWindow())
self.setWindowTitle(title)
@@ -68,6 +61,7 @@ def __init__(self, text="", *, title="Edit", placeholder="", python=False):
self.textWidget.setWordWrapMode(QTextOption.NoWrap)
else:
self.textWidget = CodeEditorWidget()
+ self.textWidget.words = words or []
self.textWidget.setPlaceholderText(placeholder)
self.textWidget.setPlainText(text)
@@ -159,22 +153,21 @@ def save(text):
self.buttonCommand = text
self.somethingChanged.emit()
- editText = EditTextDialog(self.buttonCommand, title="Edit command", placeholder='chset("/someAttr", 1)', python=True)
+ words = list(self.executor("").keys())
+
+ editText = EditTextDialog(self.buttonCommand, title="Edit command", placeholder='chset("/someAttr", 1)', words=words, python=True)
editText.saved.connect(save)
editText.show()
def buttonClicked(self):
if self.buttonCommand:
- localEnv = dict(self.env)
-
def f():
- exec(self.buttonCommand, localEnv)
- self.needUpdateUI.emit() # update UI
+ self.executor(self.buttonCommand)
f()
def getDefaultData(self):
- return {"command": "module.attr.someAttr.set(1)",
+ return {"command": 'chset("/someAttr", 1)',
"label": "Press me",
"default": "label"}
@@ -233,11 +226,9 @@ def editItems(self):
items = ";".join([self.comboBox.itemText(i) for i in range(self.comboBox.count())])
newItems, ok = QInputDialog.getText(self, "Rig Builder", "Items separated with ';'", QLineEdit.Normal, items)
if ok and newItems:
- self.comboBox.clear()
- for i, item in enumerate(newItems.split(";")):
- self.comboBox.addItem(item.strip())
- idx = self.comboBox.count()-1
- self.comboBox.setItemData(idx, jsonColor(smartConversion(item)), Qt.ForegroundRole)
+ value = self.getJsonData()
+ value["items"] = [smartConversion(item.strip()) for item in newItems.split(";") if item.strip()]
+ self.setJsonData(value)
self.somethingChanged.emit()
def clearItems(self):
@@ -267,9 +258,15 @@ def getJsonData(self):
def setJsonData(self, value):
self.comboBox.clear()
+ skip = []
for i, item in enumerate(value["items"]):
+ if item in skip: # don't add duplicates
+ continue
+
self.comboBox.addItem(fromSmartConversion(item))
self.comboBox.setItemData(i, jsonColor(item), Qt.ForegroundRole)
+ skip.append(item)
+
if value["current"] in value["items"]:
self.comboBox.setCurrentIndex(value["items"].index(value["current"]))
@@ -320,6 +317,7 @@ def __init__(self, **kwargs):
self.minValue = 0
self.maxValue = 100
self.validator = 0
+ self.value = ""
layout = QHBoxLayout()
self.setLayout(layout)
@@ -337,28 +335,30 @@ def __init__(self, **kwargs):
layout.addWidget(self.textWidget)
layout.addWidget(self.sliderWidget)
- def colorizeText(self):
- text = self.textWidget.text().strip()
- color = jsonColor(smartConversion(text))
- self.textWidget.setStyleSheet("color: {}".format(color.name()))
+ def colorizeValue(self):
+ color = jsonColor(self.value)
+ self.textWidget.setStyleSheet("QLineEdit {{ color: {} }}".format(color.name()))
def textChanged(self):
text = self.textWidget.text().strip()
if self.validator:
self.sliderWidget.setValue(float(text)*100)
- self.colorizeText()
+ self.value = smartConversion(text)
+ self.colorizeValue()
self.somethingChanged.emit()
def sliderValueChanged(self, v):
v /= 100.0
if self.validator == 1: # int
v = round(v)
+ self.value = v
self.textWidget.setText(str(v))
self.somethingChanged.emit()
def textContextMenuEvent(self, event):
menu = self.textWidget.createStandardContextMenu()
+ menu.addSeparator()
menu.addAction("Options...", self.optionsClicked)
menu.popup(event.globalPos())
@@ -373,7 +373,7 @@ def optionsClicked(self):
self.setJsonData(self.getJsonData())
def getJsonData(self):
- return {"value": smartConversion(self.textWidget.text().strip()),
+ return {"value": self.value,
"default": "value",
"min": self.minValue,
"max": self.maxValue,
@@ -383,6 +383,7 @@ def setJsonData(self, data):
self.validator = data.get("validator", 0)
self.minValue = int(data.get("min") or LineEditTemplateWidget.defaultMin)
self.maxValue = int(data.get("max") or LineEditTemplateWidget.defaultMax)
+ self.value = data.get("value", "")
if self.validator == 1:
self.textWidget.setValidator(QIntValidator())
@@ -396,13 +397,13 @@ def setJsonData(self, data):
if self.maxValue:
self.sliderWidget.setMaximum(self.maxValue*100)
- if data["value"]:
- self.sliderWidget.setValue(float(data["value"])*100)
+ if self.value:
+ self.sliderWidget.setValue(float(self.value)*100)
else:
self.sliderWidget.hide()
- self.textWidget.setText(fromSmartConversion(data["value"]))
- self.colorizeText()
+ self.textWidget.setText(fromSmartConversion(self.value))
+ self.colorizeValue()
class LineEditAndButtonTemplateWidget(TemplateWidget):
def __init__(self, **kwargs):
@@ -428,6 +429,7 @@ def __init__(self, **kwargs):
value = path or value'''}
self.buttonCommand = defaultCmd["command"]
+ self.value = ""
layout = QHBoxLayout()
self.setLayout(layout)
@@ -443,13 +445,13 @@ def __init__(self, **kwargs):
layout.addWidget(self.textWidget)
layout.addWidget(self.buttonWidget)
- def colorizeText(self):
- text = self.textWidget.text().strip()
- color = jsonColor(smartConversion(text))
- self.textWidget.setStyleSheet("color: {}".format(color.name()))
+ def colorizeValue(self):
+ color = jsonColor(self.value)
+ self.textWidget.setStyleSheet("QLineEdit {{ color: {} }}".format(color.name()))
def textChanged(self):
- self.colorizeText()
+ self.value = smartConversion(self.textWidget.text().strip())
+ self.colorizeValue()
self.somethingChanged.emit()
def buttonContextMenuEvent(self, event):
@@ -481,37 +483,71 @@ def editCommand(self):
def save(text):
self.buttonCommand = text
self.somethingChanged.emit()
-
- editText = EditTextDialog(self.buttonCommand, title="Edit command", placeholder="Your python command...", python=True)
+
+ words = list(self.executor("").keys())
+
+ editText = EditTextDialog(self.buttonCommand, title="Edit command", placeholder="Your python command...", words=words, python=True)
editText.saved.connect(save)
editText.show()
def buttonClicked(self):
if self.buttonCommand:
- env = dict(self.env)
- env["value"] = smartConversion(self.textWidget.text().strip())
-
def f():
- exec(self.buttonCommand, env)
- self.textWidget.setText(fromSmartConversion(env["value"]))
+ env = {"value": smartConversion(self.textWidget.text().strip())}
+ outEnv = self.executor(self.buttonCommand, env)
+ self.value = outEnv["value"]
+ self.textWidget.setText(fromSmartConversion(self.value))
self.somethingChanged.emit()
f()
def getJsonData(self):
- return {"value": smartConversion(self.textWidget.text().strip()),
+ return {"value": self.value,
"buttonCommand": self.buttonCommand,
"buttonLabel": self.buttonWidget.text(),
"default": "value"}
- def setCustomText(self, value):
- self.textWidget.setText(fromSmartConversion(value))
-
def setJsonData(self, data):
- self.textWidget.setText(fromSmartConversion(data["value"]))
+ self.value = data["value"]
+ self.textWidget.setText(fromSmartConversion(self.value))
self.buttonCommand = data["buttonCommand"]
self.buttonWidget.setText(data["buttonLabel"])
- self.colorizeText()
+ self.colorizeValue()
+
+class ListBoxItem(QListWidgetItem):
+ ValueRole = Qt.UserRole + 1
+
+ def __init__(self, value):
+ super().__init__()
+ self._value = value
+ self.setFlags(self.flags() | Qt.ItemIsEditable)
+
+ def clone(self):
+ return ListBoxItem(copyJson(self._value))
+
+ def data(self, role):
+ if role == Qt.ForegroundRole:
+ return jsonColor(self._value)
+
+ elif role == Qt.DisplayRole:
+ return fromSmartConversion(self._value)
+
+ elif role == Qt.EditRole:
+ return fromSmartConversion(self._value) # always edit as string
+
+ elif role == ListBoxItem.ValueRole:
+ return self._value
+
+ return super().data(role)
+
+ def setData(self, role, value):
+ if role == Qt.EditRole:
+ self._value = smartConversion(value)
+
+ elif role == ListBoxItem.ValueRole:
+ self._value = value
+
+ super().setData(role, value)
class ListBoxTemplateWidget(TemplateWidget):
def __init__(self, **kwargs):
@@ -522,7 +558,9 @@ def __init__(self, **kwargs):
layout.setContentsMargins(QMargins())
self.listWidget = QListWidget()
- self.listWidget.itemDoubleClicked.connect(self.itemDoubleClicked)
+ self.listWidget.setSelectionMode(QAbstractItemView.ExtendedSelection)
+ self.listWidget.itemSelectionChanged.connect(self.somethingChanged)
+ self.listWidget.itemChanged.connect(self.itemChanged)
self.listWidget.contextMenuEvent = self.listContextMenuEvent
layout.addWidget(self.listWidget, alignment=Qt.AlignLeft|Qt.AlignTop)
@@ -533,19 +571,31 @@ def listContextMenuEvent(self, event):
menu.addAction("Append", self.appendItem)
menu.addAction("Remove", self.removeItem)
menu.addAction("Edit", self.editItem)
- menu.addAction("Sort", self.listWidget.sortItems)
+
+ def f():
+ self.listWidget.sortItems()
+ self.somethingChanged.emit()
+ menu.addAction("Sort", f)
+
menu.addSeparator()
if DCC in ["maya"]:
dccLabel = DCC.capitalize()
menu.addAction("Get selected from "+dccLabel, Callback(self.getFromDCC, False))
menu.addAction("Add selected from "+dccLabel, Callback(self.getFromDCC, True))
- menu.addAction("Select in "+dccLabel, self.selectInDCC)
+ menu.addAction("Select in "+dccLabel, Callback(self.selectInDCC, False))
+ menu.addAction("Select all in "+dccLabel, self.selectInDCC)
+ menu.addSeparator()
menu.addAction("Clear", self.clearItems)
menu.popup(event.globalPos())
+ def itemChanged(self, item):
+ self.listWidget.closePersistentEditor(item)
+ self.somethingChanged.emit()
+ self.resizeWidget()
+
def resizeWidget(self):
width = self.listWidget.sizeHintForColumn(0) + 50
height = 0
@@ -560,14 +610,13 @@ def editItem(self):
if ok and newItems:
self.listWidget.clear()
for x in newItems.split(";"):
- item = QListWidgetItem(x.strip())
- item.setForeground(jsonColor(smartConversion(x)))
+ item = ListBoxItem(smartConversion(x.strip()))
self.listWidget.addItem(item)
self.somethingChanged.emit()
self.resizeWidget()
- def selectInDCC(self):
- items = [self.listWidget.item(i).text() for i in range(self.listWidget.count())]
+ def selectInDCC(self, allItems=True):
+ items = [self.listWidget.item(i).text() for i in range(self.listWidget.count()) if allItems or self.listWidget.item(i).isSelected()]
if DCC == "maya":
cmds.select(items)
@@ -578,9 +627,7 @@ def getFromDCC(self, add=False):
def updateUI(nodes):
for n in nodes:
- item = QListWidgetItem(n)
- item.setForeground(jsonColor(n))
- self.listWidget.addItem(item)
+ self.listWidget.addItem(ListBoxItem(n))
self.resizeWidget()
self.somethingChanged.emit()
@@ -589,15 +636,17 @@ def updateUI(nodes):
updateUI(nodes)
def clearItems(self):
- self.listWidget.clear()
- self.resizeWidget()
- self.somethingChanged.emit()
+ ok = QMessageBox.question(self, "Rig Builder", "Really clear all items?", QMessageBox.Yes and QMessageBox.No, QMessageBox.Yes) == QMessageBox.Yes
+ if ok:
+ self.listWidget.blockSignals(True) # avoid emitting signals by selecting items
+ self.listWidget.clear()
+ self.listWidget.blockSignals(False)
+ self.somethingChanged.emit()
+ self.resizeWidget()
def appendItem(self):
- text = "newItem%d"%(self.listWidget.count()+1)
- self.listWidget.addItem(text)
- item = self.listWidget.item(self.listWidget.count()-1)
- item.setForeground(jsonColor(text))
+ text = "item%d"%(self.listWidget.count()+1)
+ self.listWidget.addItem(ListBoxItem(text))
self.resizeWidget()
self.somethingChanged.emit()
@@ -606,27 +655,25 @@ def removeItem(self):
self.resizeWidget()
self.somethingChanged.emit()
- def itemDoubleClicked(self, item):
- newText, ok = QInputDialog.getText(self, "Rig Builder", "New text", QLineEdit.Normal, item.text())
- if ok:
- item.setText(newText)
- item.setForeground(jsonColor(smartConversion(newText)))
- self.resizeWidget()
- self.somethingChanged.emit()
-
def getDefaultData(self):
- return {"items": ["a", "b"], "default": "items"}
+ return {"items": ["a", "b"], "current":0, "selected":[], "default": "items"}
def getJsonData(self):
- return {"items": [smartConversion(self.listWidget.item(i).text()) for i in range(self.listWidget.count())],
+ return {"items": [self.listWidget.item(i).data(ListBoxItem.ValueRole) for i in range(self.listWidget.count())],
+ "selected": [self.listWidget.row(item) for item in self.listWidget.selectedItems()],
"default": "items"}
def setJsonData(self, value):
self.listWidget.clear()
+
for v in value["items"]:
- item = QListWidgetItem(fromSmartConversion(v))
- item.setForeground(jsonColor(v))
- self.listWidget.addItem(item)
+ self.listWidget.addItem(ListBoxItem(v))
+
+ for i in value.get("selected", []):
+ item = self.listWidget.item(i)
+ if item:
+ item.setSelected(True)
+
self.resizeWidget()
class RadioButtonTemplateWidget(TemplateWidget):
@@ -653,7 +700,10 @@ def contextMenuEvent(self, event):
columnsMenu = QMenu("Columns", self)
for n in RadioButtonTemplateWidget.Columns:
- columnsMenu.addAction(str(n) + " columns", Callback(self.setColumns, n))
+ action = columnsMenu.addAction(str(n) + " columns", Callback(self.setColumns, n))
+ if n == self.numColumns:
+ action.setCheckable(True)
+ action.setChecked(True)
menu.addMenu(columnsMenu)
menu.popup(event.globalPos())
@@ -666,7 +716,7 @@ def setColumns(self, n):
def colorizeButtons(self):
for b in self.buttonsGroupWidget.buttons():
- b.setStyleSheet("background-color: #2a6931" if b.isChecked() else "")
+ b.setStyleSheet("QRadioButton {background-color: #2a6931}" if b.isChecked() else "")
def buttonClicked(self, b):
self.colorizeButtons()
@@ -698,25 +748,20 @@ def getJsonData(self):
def setJsonData(self, value):
gridLayout = self.layout()
-
self.clearButtons()
self.numColumns = value["columns"]
gridLayout.setDefaultPositioning(self.numColumns, Qt.Horizontal)
- row = 0
- column = 0
for i, item in enumerate(value["items"]):
- if i % self.numColumns == 0 and i > 0:
- row += 1
- column = 0
-
button = QRadioButton(item)
- gridLayout.addWidget(button, row, column)
+ gridLayout.addWidget(button, i//self.numColumns, i%self.numColumns)
self.buttonsGroupWidget.addButton(button)
self.buttonsGroupWidget.setId(button, i)
- column += 1
+
+ if value["current"] not in range(len(value["items"])):
+ value["current"] = 0
self.buttonsGroupWidget.buttons()[value["current"]].setChecked(True)
self.colorizeButtons()
@@ -822,8 +867,7 @@ def resizeWidget(self):
self.tableWidget.setFixedHeight(clamp(height, headerHeight+100, 500))
def clearAll(self):
- ok = QMessageBox.question(self, "Rig Builder", "Really remove all elements?",
- QMessageBox.Yes and QMessageBox.No, QMessageBox.Yes) == QMessageBox.Yes
+ ok = QMessageBox.question(self, "Rig Builder", "Really remove all items?", QMessageBox.Yes and QMessageBox.No, QMessageBox.Yes) == QMessageBox.Yes
if ok:
self.tableWidget.clearContents()
self.tableWidget.setRowCount(1)
@@ -940,33 +984,103 @@ class VectorTemplateWidget(TemplateWidget):
def __init__(self, **kwargs):
super().__init__(**kwargs)
- layout = QHBoxLayout()
+ self.vectorDim = 3
+ self.numColumns = 3
+ self.precision = 4
+ self.widgets = []
+
+ layout = QGridLayout()
self.setLayout(layout)
layout.setContentsMargins(QMargins())
- self.xWidget = QLineEdit()
- self.xWidget.setValidator(QDoubleValidator())
- self.xWidget.editingFinished.connect(self.somethingChanged.emit)
-
- self.yWidget = QLineEdit()
- self.yWidget.setValidator(QDoubleValidator())
- self.yWidget.editingFinished.connect(self.somethingChanged.emit)
+ self.setJsonData(self.getDefaultData())
+
+ def createMenu(self):
+ menu = QMenu(self)
- self.zWidget = QLineEdit()
- self.zWidget.setValidator(QDoubleValidator())
- self.zWidget.editingFinished.connect(self.somethingChanged.emit)
+ vectorDimMenu = menu.addMenu("Dimension")
+ for size in range(2, 17):
+ action = vectorDimMenu.addAction(str(size)+"D", Callback(self.setSizes, size, self.numColumns))
+ if size == self.vectorDim:
+ action.setCheckable(True)
+ action.setChecked(True)
+
+ numColumnsMenu = menu.addMenu("Columns")
+ for size in range(2, 8):
+ action = numColumnsMenu.addAction(str(size), Callback(self.setSizes, self.vectorDim, size))
+ if size == self.numColumns:
+ action.setCheckable(True)
+ action.setChecked(True)
+
+ precisionMenu = menu.addMenu("Precision")
+ for n in range(0, 8):
+ action = precisionMenu.addAction(str(n), Callback(self.setPrecision, n))
+ if n == self.precision:
+ action.setCheckable(True)
+ action.setChecked(True)
+
+ return menu
- layout.addWidget(self.xWidget)
- layout.addWidget(self.yWidget)
- layout.addWidget(self.zWidget)
+ def contextMenuEvent(self, event):
+ menu = self.createMenu()
+ menu.popup(event.globalPos())
+
+ def setSizes(self, vectorDim, numColumns):
+ self.vectorDim = vectorDim
+ self.numColumns = numColumns
+ self.setJsonData(self.getJsonData())
+ self.somethingChanged.emit()
+
+ def setPrecision(self, prec):
+ self.precision = prec
+ self.setJsonData(self.getJsonData())
+ self.somethingChanged.emit()
+
+ def getDefaultData(self):
+ return {"value": [0.0, 0.0, 0.0], "default": "value", "dimension": self.vectorDim, "columns": self.numColumns, "precision": self.precision}
def getJsonData(self):
- return {"value": [float(self.xWidget.text() or 0), float(self.yWidget.text() or 0), float(self.zWidget.text() or 0)], "default": "value"}
+ return {"value": [float(w.text() or 0.0) for w in self.widgets],
+ "default": "value",
+ "dimension": self.vectorDim,
+ "precision": self.precision,
+ "columns": self.numColumns}
def setJsonData(self, value):
- self.xWidget.setText(str(value["value"][0]))
- self.yWidget.setText(str(value["value"][1]))
- self.zWidget.setText(str(value["value"][2]))
+ def widgetContextMenu(event, w):
+ stdMenu = w.createStandardContextMenu()
+ stdMenu.addSeparator()
+
+ menu = self.createMenu()
+ for a in menu.actions():
+ stdMenu.addAction(a)
+ stdMenu.popup(event.globalPos())
+
+ for w in self.widgets:
+ w.hide()
+
+ self.widgets = []
+
+ layout = self.layout()
+ clearLayout(layout)
+
+ self.numColumns = value.get("columns", self.numColumns)
+ layout.setDefaultPositioning(self.numColumns, Qt.Horizontal)
+
+ self.vectorDim = value.get("dimension", self.vectorDim)
+ self.precision = value.get("precision", self.precision)
+
+ validator = QDoubleValidator()
+ validator.setDecimals(self.precision)
+
+ for i in range(self.vectorDim):
+ v = value["value"][i] if i < len(value["value"]) else 0.0
+ widget = QLineEdit(str(round(v, self.precision)))
+ widget.setValidator(validator)
+ widget.editingFinished.connect(self.somethingChanged)
+ widget.contextMenuEvent = lambda event, w=widget: widgetContextMenu(event, w)
+ layout.addWidget(widget, i//self.numColumns, i%self.numColumns)
+ self.widgets.append(widget)
def listLerp(lst1, lst2, coeff):
return [p1*(1-coeff) + p2*coeff for p1, p2 in zip(lst1, lst2)]
@@ -1088,13 +1202,6 @@ def itemChange(self, change, value):
elif value.y() < CurveScene.MaxY:
value.setY(CurveScene.MaxY)
- if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
- scene = self.scene()
- scene.calculateCVs()
- for view in scene.views():
- if type(view) == CurveView:
- view.somethingChanged.emit()
-
return super().itemChange(change, value)
class CurveScene(QGraphicsScene):
@@ -1139,31 +1246,28 @@ def mouseDoubleClickEvent(self, event):
item.setPos(pos)
self.addItem(item)
- self.calculateCVs()
-
- for view in self.views():
- if type(view) == CurveView:
- view.somethingChanged.emit()
-
def mousePressEvent(self, event):
+ self._oldCvs = self.cvs[:]
+
if event.button() == Qt.RightButton:
- somethingChanged = False
for item in self.selectedItems():
if item.fixedX is None: # don't remove tips
self.removeItem(item)
- somethingChanged = True
-
- if somethingChanged:
- self.calculateCVs()
-
- for view in self.views():
- if type(view) == CurveView:
- view.somethingChanged.emit()
event.accept()
else:
super().mousePressEvent(event)
+ def mouseReleaseEvent(self, event):
+ super().mouseReleaseEvent(event)
+
+ self.calculateCVs()
+
+ if self.cvs != self._oldCvs:
+ for view in self.views():
+ if type(view) == CurveView:
+ view.somethingChanged.emit()
+
def calculateCVs(self):
self.cvs = []
@@ -1187,7 +1291,7 @@ def calculateCVs(self):
else:
d1 = abs(y - prevy)
d2 = abs(y - nexty)
- s = d1 + d2
+ s = d1 + d2 + 1e-5
w1 = d1 / s
w2 = d2 / s
w = max(w1, w2)*2 - 1 # from 0 to 1, because max(w1,w2) is always >= 0.5
@@ -1391,7 +1495,7 @@ def getJsonData(self):
def setJsonData(self, value):
self.jsonWidget.setFixedHeight(value["height"])
self.jsonWidget.clear()
- self.jsonWidget.fromJsonList(value["data"])
+ self.jsonWidget.loadFromJsonList(value["data"])
self.jsonWidget.setReadOnly(value["readonly"])
TemplateWidgets = {
@@ -1409,11 +1513,39 @@ def setJsonData(self, value):
"text": TextTemplateWidget,
"vector": VectorTemplateWidget}
+def curve_evaluate(data, param):
+ return evaluateBezierCurve(data["cvs"], param)
+
+def curve_evaluateFromX(data, param):
+ return evaluateBezierCurveFromX(data["cvs"], param)
+
+def listBox_setSelected(data, indices):
+ data["selected"] = indices
+
+def listBox_selected(data):
+ return [data["items"][idx] for idx in data["selected"] if idx < len(data["items"])]
+
+def comboBox_items(data):
+ return data["items"]
+
+def comboBox_setItems(data, items):
+ data["items"] = items
+
WidgetsAPI = {
- "evaluateBezierCurve": evaluateBezierCurve,
- "evaluateBezierCurveFromX": evaluateBezierCurveFromX,
"listLerp": listLerp,
"clamp": clamp,
"smartConversion": smartConversion,
"fromSmartConversion": fromSmartConversion,
+
+ # data based
+ "curve_evaluate": curve_evaluate,
+ "curve_evaluateFromX": curve_evaluateFromX,
+ "listBox_selected": listBox_selected,
+ "listBox_setSelected": listBox_setSelected,
+ "comboBox_items": comboBox_items,
+ "comboBox_setItems": comboBox_setItems,
+
+ # obsolete
+ "evaluateBezierCurve": evaluateBezierCurve,
+ "evaluateBezierCurveFromX": evaluateBezierCurveFromX,
}
\ No newline at end of file