diff --git a/__init__.py b/__init__.py
index 6f23a93..fab1dab 100644
--- a/__init__.py
+++ b/__init__.py
@@ -33,28 +33,20 @@ def sendToServer(module):
'''
module.sendToServer()
return True
-
+
def updateFilesFromServer():
+ '''
+ Update files from server with SVN, Git, Perforce or other VCS.
+ '''
+
def update():
- '''
- Update files from server with SVN, Git, Perforce or other VCS.
- '''
pass
-
+
global updateFilesThread
if not updateFilesThread or not updateFilesThread.isRunning():
updateFilesThread = MyThread(update)
updateFilesThread.start()
-def widgetOnChange(widget, module, attr):
- data = widget.getJsonData()
- attr.data = data
-
- if attr.connect:
- srcAttr = module.findConnectionSourceForAttribute(attr)
- if srcAttr:
- srcAttr.updateFromAttribute(attr)
-
class MyThread(QThread):
def __init__(self, runFunction):
super().__init__()
@@ -63,54 +55,66 @@ def __init__(self, runFunction):
def run(self):
self.runFunction()
-class TabAttributesWidget(QWidget):
- needUpdateUI = Signal()
+class EditJsonDialog(QDialog):
+ saved = Signal(dict)
+
+ def __init__(self, data, *, title="Edit"):
+ super().__init__(parent=QApplication.activeWindow())
+
+ self.setWindowTitle(title)
+ self.setGeometry(0, 0, 600, 400)
- def __init__(self, module, attributes, *, mainWindow=None, **kwargs):
- super(TabAttributesWidget, self).__init__(**kwargs)
+ layout = QVBoxLayout()
+ self.setLayout(layout)
+
+ self.jsonWidget = widgets.JsonWidget(data)
+
+ okBtn = QPushButton("OK")
+ okBtn.clicked.connect(self.saveAndClose)
+
+ layout.addWidget(self.jsonWidget)
+ layout.addWidget(okBtn)
+ centerWindow(self)
+
+ def saveAndClose(self):
+ dataList = self.jsonWidget.toJsonList()
+ if dataList:
+ self.saved.emit(dataList[0]) # keep the first item only
+ self.accept()
+
+class AttributesWidget(QWidget):
+ def __init__(self, moduleItem, attributes, *, mainWindow=None, **kwargs):
+ super().__init__(**kwargs)
self.mainWindow = mainWindow
- self.module = module
+ self.moduleItem = moduleItem
+
+ self._attributeAndWidgets = [] # [attribute, nameWidget, templateWidget]
layout = QGridLayout()
layout.setDefaultPositioning(2, Qt.Horizontal)
layout.setColumnStretch(1, 1)
self.setLayout(layout)
- if self.module:
- with captureOutput(self.mainWindow.logWidget):
- try:
- for attr in self.module.getAttributes():
- self.module.resolveConnection(attr)
- except Exception as err:
- print("Error: " + str(err))
- self.mainWindow.showLog()
- self.mainWindow.logWidget.ensureCursorVisible()
-
globEnv = self.mainWindow.getModuleGlobalEnv()
- globEnv.update({"module": ModuleWrapper(self.module), "ch": self.module.ch, "chset": self.module.chset})
+ globEnv.update({"module": ModuleWrapper(self.moduleItem.module), "ch": self.moduleItem.module.ch, "chset": self.moduleItem.module.chset})
for a in attributes:
templateWidget = widgets.TemplateWidgets[a.template](env=globEnv)
- with captureOutput(self.mainWindow.logWidget):
- try:
- templateWidget.setJsonData(a.data)
- except:
- print("Error: invalid JSON data for attribute '{}'".format(a.name))
- a.data = templateWidget.getDefaultData()
- self.mainWindow.showLog()
- self.mainWindow.logWidget.ensureCursorVisible()
-
- templateWidget.somethingChanged.connect(lambda w=templateWidget, e=module, a=a: widgetOnChange(w, e, a))
- templateWidget.needUpdateUI.connect(self.needUpdateUI.emit)
- self.setWidgetStyle(templateWidget, a)
-
nameWidget = QLabel(a.name)
+
+ self._attributeAndWidgets.append((a, nameWidget, templateWidget))
+ idx = len(self._attributeAndWidgets) - 1 # index of widgets
+
+ self.updateWidget(idx)
+ 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; }")
-
- nameWidget.contextMenuEvent = lambda event, a=a, w=templateWidget: self.nameContextMenuEvent(event, a, w)
- nameWidget.attribute = a
+ nameWidget.contextMenuEvent = lambda event, idx=idx: self.nameContextMenuEvent(event, idx)
layout.addWidget(nameWidget)
layout.addWidget(templateWidget)
@@ -118,153 +122,195 @@ def __init__(self, module, attributes, *, mainWindow=None, **kwargs):
layout.addWidget(QLabel())
layout.setRowStretch(layout.rowCount(), 1)
- def connectionMenu(self, menu, module, attr, widget, path="/"):
+ def connectionMenu(self, menu, module, attrWidgetIndex, path="/"):
+ attr, _, _ = self._attributeAndWidgets[attrWidgetIndex]
+
subMenu = QMenu(module.name)
for a in module.getAttributes():
if a.template == attr.template:
- subMenu.addAction(a.name, Callback(self.connectAttr, path+module.name+"/"+a.name, attr, widget))
+ subMenu.addAction(a.name, Callback(self.connectAttr, path+module.name+"/"+a.name, attrWidgetIndex))
for ch in module.getChildren():
- self.connectionMenu(subMenu, ch, attr, widget, path+module.name+"/")
+ self.connectionMenu(subMenu, ch, attrWidgetIndex, path+module.name+"/")
if subMenu.actions():
menu.addMenu(subMenu)
- def nameContextMenuEvent(self, event, attr, widget):
+ def nameContextMenuEvent(self, event, attrWidgetIndex):
+ attr, _, _ = self._attributeAndWidgets[attrWidgetIndex]
+
menu = QMenu(self)
- if self.module and self.module.parent:
+ if self.moduleItem and self.moduleItem.parent():
makeConnectionMenu = QMenu("Make connection")
- for a in self.module.parent.getAttributes():
+ for a in self.moduleItem.module.parent.getAttributes():
if a.template == attr.template:
- makeConnectionMenu.addAction(a.name, Callback(self.connectAttr, "/"+a.name, attr, widget))
+ makeConnectionMenu.addAction(a.name, Callback(self.connectAttr, "/"+a.name, attrWidgetIndex))
- for ch in self.module.parent.getChildren():
- if ch is self.module:
+ for ch in self.moduleItem.module.parent.getChildren():
+ if ch is self.moduleItem.module:
continue
- self.connectionMenu(makeConnectionMenu, ch, attr, widget)
+ self.connectionMenu(makeConnectionMenu, ch, attrWidgetIndex)
menu.addMenu(makeConnectionMenu)
if attr.connect:
- menu.addAction("Break connection", Callback(self.disconnectAttr, attr, widget))
+ menu.addAction("Break connection", Callback(self.disconnectAttr, attrWidgetIndex))
+ menu.addSeparator()
- menu.addAction("Set data", Callback(self.setData, attr, widget))
- menu.addAction("Reset", Callback(self.resetAttr, attr, widget))
+ menu.addAction("Edit data", Callback(self.editData, attrWidgetIndex))
+ menu.addAction("Edit expression", Callback(self.editExpression, attrWidgetIndex))
menu.addSeparator()
- menu.addAction("Expose", Callback(self.exposeAttr, attr, widget))
+ menu.addAction("Expose", Callback(self.exposeAttr, attrWidgetIndex))
+ menu.addSeparator()
+ menu.addAction("Reset", Callback(self.resetAttr, attrWidgetIndex))
menu.popup(event.globalPos())
- def setWidgetStyle(self, widget, attr):
- tooltip = ""
- background = ""
+ def widgetOnChange(self, attrWidgetIndex):
+ attr, _, widget = self._attributeAndWidgets[attrWidgetIndex]
+
+ newData = widget.getJsonData()
+ oldData = copyJson(attr.data)
+ attr.data = newData
+
+ 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()
+
+ if oldData != newData and not attr.expression: # compare with default value
+ attr.modified = True
+ self.updateWidgetStyle(attrWidgetIndex)
+
+ 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()
+
+ def updateWidgets(self):
+ for i in range(len(self._attributeAndWidgets)):
+ self.updateWidget(i)
+
+ def updateWidgetStyle(self, attrWidgetIndex):
+ attr, nameWidget, widget = self._attributeAndWidgets[attrWidgetIndex]
+
+ style = ""
+ tooltip = []
+ if attr.connect:
+ tooltip.append("Connect: "+attr.connect)
+ if attr.expression:
+ tooltip.append("Expression:\n"+attr.expression)
+
if attr.connect:
- tooltip = "Connect: "+attr.connect
- background = "#6e6e39"
+ 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}"
+
+ nameWidget.setText(attr.name+("*" if attr.modified else ""))
+
+ widget.setStyleSheet(style)
+ widget.setToolTip("\n".join(tooltip))
- widget.setToolTip(tooltip)
- widget.setStyleSheet("background-color:"+background)
+ def updateWidgetStyles(self):
+ for i in range(len(self._attributeAndWidgets)):
+ self.updateWidgetStyle(i)
- def exposeAttr(self, attr, widget):
- if not self.module.parent:
+ def exposeAttr(self, attrWidgetIndex):
+ attr, _, _ = self._attributeAndWidgets[attrWidgetIndex]
+
+ if not self.moduleItem.module.parent:
QMessageBox.warning(self, "Rig Builder", "Can't expose attribute to parent: no parent module")
return
- if self.module.parent.findAttribute(attr.name):
+ if self.moduleItem.module.parent.findAttribute(attr.name):
QMessageBox.warning(self, "Rig Builder", "Can't expose attribute to parent: attribute already exists")
return
doUsePrefix = QMessageBox.question(self, "Rig Builder", "Use prefix for the exposed attribute name?", QMessageBox.Yes and QMessageBox.No, QMessageBox.Yes) == QMessageBox.Yes
- prefix = self.module.name + "_" if doUsePrefix else ""
+ prefix = self.moduleItem.module.name + "_" if doUsePrefix else ""
expAttr = attr.copy()
expAttr.name = prefix + expAttr.name
- self.module.parent.addAttribute(expAttr)
- self.connectAttr("/"+expAttr.name, attr, widget)
-
- def setData(self, attr, widget):
- text = json.dumps(attr.data, indent=4).replace("'", "\"")
- editText = widgets.EditTextDialog(text, title="Set data", parent=mainWindow)
- editText.exec_()
- if editText.result():
- with captureOutput(self.mainWindow.logWidget):
- try:
- data = json.loads(editText.outputText)
- tmp = widgets.TemplateWidgets[attr.template]() # also we need check for widget compatibility
- tmp.setJsonData(data)
-
- except:
- print("Error: invalid or incompatible JSON data")
- self.mainWindow.showLog()
- self.mainWindow.logWidget.ensureCursorVisible()
-
- else:
- attr.data = data
- widget.setJsonData(data)
-
- def resetAttr(self, attr, widget):
- tmp = widgets.TemplateWidgets[attr.template]()
- attr.data = tmp.getDefaultData()
- attr.connect = ""
- widget.setJsonData(attr.data)
- self.setWidgetStyle(widget, attr)
+ self.moduleItem.module.parent.addAttribute(expAttr)
+ self.connectAttr("/"+expAttr.name, attrWidgetIndex)
- def disconnectAttr(self, attr, widget):
- attr.connect = ""
- self.setWidgetStyle(widget, attr)
-
- def connectAttr(self, connect, destAttr, widget):
- destAttr.connect = connect
- self.module.resolveConnection(destAttr)
- widget.setJsonData(destAttr.data)
- self.setWidgetStyle(widget, destAttr)
-
-class SearchReplaceDialog(QDialog):
- onReplace = Signal(str, str, dict) # old, new, options
+ def editData(self, attrWidgetIndex):
+ def save(data):
+ attr.data = data
+ self.updateWidget(attrWidgetIndex)
- def __init__(self, options=[], **kwargs):
- super(SearchReplaceDialog, self).__init__(**kwargs)
+ attr, _, _ = self._attributeAndWidgets[attrWidgetIndex]
+ w = EditJsonDialog(attr.data, title="Edit data")
+ w.saved.connect(save)
+ w.show()
- self.optionsWidgets = {}
-
- self.setWindowTitle("Search/Replace")
- layout = QVBoxLayout()
- self.setLayout(layout)
+ def editExpression(self, attrWidgetIndex):
+ def save(text):
+ attr.expression = text
+ self.updateWidget(attrWidgetIndex)
+ self.updateWidgetStyle(attrWidgetIndex)
- self.searchWidget = QLineEdit("L_")
- self.replaceWidget = QLineEdit("R_")
+ attr, _, _ = self._attributeAndWidgets[attrWidgetIndex]
- btn = QPushButton("Replace")
- btn.clicked.connect(self.replaceClicked)
+ 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.saved.connect(save)
+ w.show()
- gridLayout = QGridLayout()
- gridLayout.addWidget(QLabel("Search"),0,0)
- gridLayout.addWidget(self.searchWidget,0,1)
- gridLayout.addWidget(QLabel("Replace"),1,0)
- gridLayout.addWidget(self.replaceWidget,1,1)
- layout.addLayout(gridLayout)
+ def resetAttr(self, attrWidgetIndex):
+ attr, _, widget = self._attributeAndWidgets[attrWidgetIndex]
- for opt in options:
- w = QCheckBox(opt)
- self.optionsWidgets[opt] = w
- layout.addWidget(w)
+ tmp = widgets.TemplateWidgets[attr.template]()
+ attr.data = tmp.getDefaultData()
+ attr.connect = ""
+ widget.setJsonData(attr.data)
+ self.updateWidgetStyle(attrWidgetIndex)
- layout.addWidget(btn)
+ def disconnectAttr(self, attrWidgetIndex):
+ attr, _, _ = self._attributeAndWidgets[attrWidgetIndex]
+ attr.connect = ""
+ self.updateWidgetStyle(attrWidgetIndex)
- def replaceClicked(self):
- opts = {l:w.isChecked() for l,w in self.optionsWidgets.items()}
- self.onReplace.emit(self.searchWidget.text(), self.replaceWidget.text(), opts)
- self.accept()
+ def connectAttr(self, connect, attrWidgetIndex):
+ attr, _, widget = self._attributeAndWidgets[attrWidgetIndex]
+ attr.connect = connect
+ self.moduleItem.module.resolveConnection(attr)
+ widget.setJsonData(attr.data)
+ self.updateWidgetStyle(attrWidgetIndex)
class AttributesTabWidget(QTabWidget):
- def __init__(self, module=None, *, mainWindow=None, **kwargs):
- super(AttributesTabWidget, self).__init__(**kwargs)
+ def __init__(self, moduleItem, *, mainWindow=None, **kwargs):
+ super().__init__(**kwargs)
self.mainWindow = mainWindow
- self.module = module
+ self.moduleItem = moduleItem
self.tabsAttributes = {}
+ self._attributesWidget = None
self.searchAndReplaceDialog = SearchReplaceDialog(["In all tabs"])
self.searchAndReplaceDialog.onReplace.connect(self.onReplace)
@@ -275,7 +321,7 @@ def __init__(self, module=None, *, mainWindow=None, **kwargs):
def contextMenuEvent(self, event):
menu = QMenu(self)
- if self.module:
+ if self.moduleItem:
menu.addAction("Edit attributes", self.editAttributes)
menu.addSeparator()
menu.addAction("Replace in values", self.searchAndReplaceDialog.exec_)
@@ -283,7 +329,7 @@ def contextMenuEvent(self, event):
menu.popup(event.globalPos())
def editAttributes(self):
- dialog = EditAttributesDialog(self.module, self.currentIndex(), parent=mainWindow)
+ dialog = EditAttributesDialog(self.moduleItem, self.currentIndex(), parent=mainWindow)
dialog.exec_()
self.mainWindow.codeEditorWidget.updateState()
@@ -318,24 +364,24 @@ def tabChanged(self, idx):
title = self.tabText(idx)
scrollArea = self.widget(idx)
- w = TabAttributesWidget(self.module, self.tabsAttributes[title], mainWindow=self.mainWindow)
- w.needUpdateUI.connect(self.updateTabs)
- scrollArea.setWidget(w)
+ self._attributesWidget = AttributesWidget(self.moduleItem, self.tabsAttributes[title], mainWindow=self.mainWindow)
+ scrollArea.setWidget(self._attributesWidget)
self.setCurrentIndex(idx)
def updateTabs(self):
oldIndex = self.currentIndex()
oldCount = self.count()
+ self._attributesWidget = None
self.tabsAttributes.clear()
- if not self.module:
+ if not self.moduleItem:
return
self.blockSignals(True)
tabTitlesInOrder = []
- for a in self.module.getAttributes():
+ for a in self.moduleItem.module.getAttributes():
if a.category not in self.tabsAttributes:
self.tabsAttributes[a.category] = []
tabTitlesInOrder.append(a.category)
@@ -362,14 +408,18 @@ def updateTabs(self):
self.tabChanged(oldIndex)
self.blockSignals(False)
+ def updateWidgetStyles(self):
+ if self._attributesWidget:
+ self._attributesWidget.updateWidgetStyles()
+
class ModuleListDialog(QDialog):
+ moduleSelected = Signal(str) # file path
+
def __init__(self, **kwargs):
- super(ModuleListDialog, self).__init__(**kwargs)
+ super().__init__(**kwargs)
self.setWindowTitle("Module Selector")
- self.selectedFileName = ""
-
layout = QVBoxLayout()
self.setLayout(layout)
@@ -419,8 +469,6 @@ def showEvent(self, event):
pos = self.mapToParent(self.mapFromGlobal(QCursor.pos()))
self.setGeometry(pos.x(), pos.y(), 600, 400)
- self.selectedFileName = ""
-
# update files from server
self.loadingLabel.show()
updateFilesFromServer()
@@ -444,7 +492,7 @@ def browseModuleDirectory(self):
def treeItemActivated(self, item, _):
if item.childCount() == 0:
- self.selectedFileName = item.filePath
+ self.moduleSelected.emit(item.filePath)
self.done(0)
def updateSource(self):
@@ -507,116 +555,166 @@ def findChildByText(text, parent, column=0):
dirItem.addChild(item)
dirItem.setExpanded(True if mask else False)
-class TreeWidget(QTreeWidget):
- def __init__(self, *, mainWindow=None, **kwargs):
- super(TreeWidget, self).__init__(**kwargs)
+class ModuleItem(QTreeWidgetItem):
+ def __init__(self, module, **kwargs):
+ super().__init__(**kwargs)
+ self.module = module
- self.mainWindow = mainWindow
- self.dragItems = [] # using in drag & drop
+ self.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable | Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled)
- self.moduleListDialog = ModuleListDialog()
+ def clone(self):
+ return ModuleItem(self.module.copy())
- self.setHeaderLabels(["Name", "Path", "Source", "UID"])
- self.setSelectionMode(QAbstractItemView.ExtendedSelection) # ExtendedSelection
+ def data(self, column, role):
+ if column == 0: # name
+ if role == Qt.EditRole:
+ return self.module.name
- self.header().setSectionResizeMode(QHeaderView.ResizeToContents)
+ elif role == Qt.DisplayRole:
+ return self.module.name + ("*" if self.module.modified else " ")
- self.setDragEnabled(True)
- self.setDragDropMode(QAbstractItemView.InternalMove)
- self.setDropIndicatorShown(True)
- self.setAcceptDrops(True)
+ elif role == Qt.ForegroundRole:
+ isParentMuted = False
+ isParentReferenced = False
- self.setIndentation(30)
+ parent = self.parent()
+ while parent:
+ isParentMuted = isParentMuted or parent.module.muted
+ isParentReferenced = isParentReferenced or parent.module.uid
+ parent = parent.parent()
- self.setMouseTracking(True)
- self.itemDoubleClicked.connect(self.treeItemDoubleClicked)
+ color = QColor(200, 200, 200)
- def drawRow(self, painter, options, modelIdx):
- painter.save()
+ if isParentReferenced:
+ color = QColor(140, 140, 180)
- rect = self.visualRect(modelIdx)
- item = self.itemFromIndex(modelIdx)
+ if self.module.muted or isParentMuted:
+ color = QColor(100, 100, 100)
- indent = self.indentation()
+ return color
- if rect.width() < 0:
- return
+ elif role == Qt.BackgroundRole:
+ if not re.match("\\w*", self.module.name):
+ return QColor(170, 50, 50)
- isParentMuted = False
- isParentReferenced = False
-
- parent = item.parent()
- while parent:
- isParentMuted = isParentMuted or parent.module.muted
- isParentReferenced = isParentReferenced or parent.module.uid
- parent = parent.parent()
-
- painter.setPen(QPen(QColor(60, 60, 60), 1, Qt.SolidLine))
- numberBranch = int(rect.x() / indent)
- if numberBranch > 1:
- for i in range(1, numberBranch):
- plusInt = i * indent + 10
- x = rect.x() - plusInt
- painter.drawLine(x, rect.y(), x, rect.y() + rect.height())
-
- if item.childCount() and rect.x() + rect.width() > rect.x():
- painter.setPen(QPen(QColor(100, 100, 100), 1, Qt.SolidLine))
- painter.fillRect(QRect(rect.x() - 16, rect.y() + 2, 12, 12), QColor(45, 45, 45))
- painter.drawRect(rect.x() - 16, rect.y() + 2, 12, 12)
- painter.setPen(QPen(QColor(120, 120, 120), 1, Qt.SolidLine))
- if item.isExpanded():
- painter.drawLine(rect.x() - 7, rect.y() + 8, rect.x() - 13, rect.y() + 8)
- else:
- painter.drawLine(rect.x() - 10, rect.y() + 5, rect.x() - 10, rect.y() + 12)
- painter.drawLine(rect.x() - 7, rect.y() + 8, rect.x() - 13, rect.y() + 8)
+ itemParent = self.parent()
+ if itemParent and len([ch for ch in itemParent.module.getChildren() if ch.name == self.module.name]) > 1:
+ return QColor(170, 50, 50)
+
+ return super().data(column, role)
- nameRect = self.visualRect(modelIdx.sibling(modelIdx.row(), 0))
- pathRect = self.visualRect(modelIdx.sibling(modelIdx.row(), 1))
- sourceRect = self.visualRect(modelIdx.sibling(modelIdx.row(), 2))
- uidRect = self.visualRect(modelIdx.sibling(modelIdx.row(), 3))
+ elif column == 1: # path
+ if role == Qt.DisplayRole:
+ return self.module.getRelativeLoadedPathString().replace("\\", "/") + " "
- if not re.match("\\w*", item.module.name):
- painter.fillRect(nameRect, QBrush(QColor(170, 50, 50)))
+ elif role == Qt.EditRole:
+ return "(not editable)"
- itemParent = item.parent()
- if itemParent and len([ch for ch in itemParent.module.getChildren() if ch.name == item.module.name]) > 1:
- painter.fillRect(nameRect, QBrush(QColor(170, 50, 50)))
+ elif role == Qt.FontRole:
+ font = QFont()
+ font.setItalic(True)
+ return font
- # set selected style
- if modelIdx in self.selectedIndexes():
- width = nameRect.width() + pathRect.width() + sourceRect.width() + uidRect.width()
- painter.fillRect(rect.x()-1, rect.y(), width, rect.height(), QColor(80, 96, 154, 60))
- painter.setPen(QColor(73, 146, 158, 200))
- painter.drawRect(rect.x()-1, rect.y()+1, width, rect.height()-3)
+ elif role == Qt.ForegroundRole:
+ return QColor(125, 125, 125)
- painter.setPen(QColor(200, 200, 200))
+ elif column == 2: # source
+ source = ""
+ if self.module.isLoadedFromLocal():
+ source = "local"
+ elif self.module.isLoadedFromServer():
+ source = "server"
- if isParentReferenced:
- painter.setPen(QColor(140, 140, 180))
+ if role == Qt.DisplayRole:
+ return source + " "
- if item.module.muted or isParentMuted:
- painter.setPen(QColor(90, 90, 90))
+ elif role == Qt.EditRole:
+ return "(not editable)"
- modifiedSuffix = "*" if item.module.modified else ""
- painter.drawText(nameRect, Qt.AlignLeft | Qt.AlignVCenter, item.module.name+modifiedSuffix)
+ elif role == Qt.ForegroundRole:
+ if source == "local":
+ return QColor(120, 220, 120)
+ elif source == "server":
+ return QColor(120, 120, 120)
- painter.setPen(QColor(120, 120, 120))
- painter.drawText(pathRect, Qt.AlignLeft | Qt.AlignVCenter, item.text(1))
+ elif column == 3: # uid
+ if role == Qt.DisplayRole:
+ return self.module.uid[:8]
+ elif role == Qt.EditRole:
+ return "(not editable)"
+ elif role == Qt.ForegroundRole:
+ return QColor(125, 125, 170)
+ else:
+ return super().data(column, role)
+
+ def setData(self, column, role, value):
+ if column == 0:
+ if role == Qt.EditRole:
+ newName = replaceSpecialChars(value).strip()
- if item.module.isLoadedFromLocal():
- painter.setPen(QColor(120, 220, 120))
- painter.drawText(sourceRect, "local")
+ connections = self._saveConnections(self.module) # rename in connections
+ self.module.name = newName
+ self.treeWidget().resizeColumnToContents(column)
+ self._updateConnections(connections)
+ else:
+ return super().setData(column, role, value)
+
+ def clearModifiedFlag(self, *, attrFlag=True, moduleFlag=True, children=True): # clear modified flag on embeded modules
+ if moduleFlag:
+ self.module.modified = False
+ self.emitDataChanged()
+
+ if attrFlag:
+ for a in self.module.getAttributes():
+ a.modified = False
+
+ if children:
+ for ch in self.module.getChildren():
+ if not ch.uid: # embeded module
+ ch.clearModifiedFlag(attrFlag=True, moduleFlag=True, children=True)
+ else: # only direct children
+ ch.clearModifiedFlag(attrFlag=True, moduleFlag=False, children=False)
+
+ def _saveConnections(self, currentModule):
+ connections = []
+ for a in currentModule.getAttributes():
+ connections.append({"attr":a, "module": currentModule, "connections":currentModule.listConnections(a)})
+
+ for ch in currentModule.getChildren():
+ connections += self._saveConnections(ch)
+ return connections
+
+ def _updateConnections(self, connections):
+ for data in connections:
+ srcAttr = data["attr"]
+ module = data["module"]
+ for m, a in data["connections"]:
+ a.connect = module.getPath().replace(m.getPath(inclusive=False), "") + "/" + srcAttr.name # update connection path
+
+class TreeWidget(QTreeWidget):
+ def __init__(self, *, mainWindow=None, **kwargs):
+ super().__init__(**kwargs)
- elif item.module.isLoadedFromServer():
- painter.setPen(QColor(120, 120, 120))
- painter.drawText(sourceRect, "server")
+ self.mainWindow = mainWindow
+ self.dragItems = [] # using in drag & drop
- painter.setPen(QColor(120, 120, 120))
- painter.drawText(uidRect, item.module.uid[:8])
- painter.restore()
+ self.moduleListDialog = ModuleListDialog()
+ self.moduleListDialog.moduleSelected.connect(self.addModuleFromBrowser)
+
+ self.setHeaderLabels(["Name", "Path", "Source", "UID"])
+ self.setSelectionMode(QAbstractItemView.ExtendedSelection) # ExtendedSelection
+
+ self.header().setSectionResizeMode(QHeaderView.ResizeToContents)
+
+ self.setDragEnabled(True)
+ self.setDragDropMode(QAbstractItemView.InternalMove)
+ self.setDropIndicatorShown(True)
+ self.setAcceptDrops(True)
+
+ self.setIndentation(30)
def paintEvent(self, event):
- super(TreeWidget, self).paintEvent(event)
+ super().paintEvent(event)
label = "Press TAB to load modules"
fontMetrics = QFontMetrics(self.font())
@@ -627,7 +725,7 @@ def paintEvent(self, event):
painter.drawText(viewport.width() - fontMetrics.width(label)-10, viewport.height()-10, label)
def dragEnterEvent(self, event):
- QTreeWidget.dragEnterEvent(self, event)
+ super().dragEnterEvent(event)
if event.mimeData().hasUrls():
event.accept()
@@ -639,13 +737,13 @@ def dragEnterEvent(self, event):
event.ignore()
def dragMoveEvent(self, event):
- QTreeWidget.dragMoveEvent(self, event)
+ super().dragMoveEvent(event)
if event.mimeData().hasUrls():
event.setDropAction(Qt.CopyAction)
def dropEvent(self, event):
- QTreeWidget.dropEvent(self, event)
+ super().dropEvent(event)
if event.mimeData().hasUrls():
event.setDropAction(Qt.CopyAction)
@@ -680,41 +778,8 @@ def dropEvent(self, event):
self.dragItems = []
self.dragParents = []
- def treeItemDoubleClicked(self, item, column):
- def _keepConnections(currentModule):
- connections = []
- for a in currentModule.getAttributes():
- connections.append({"attr":a, "module": currentModule, "connections":currentModule.listConnections(a)})
-
- for ch in currentModule.getChildren():
- connections += _keepConnections(ch)
- return connections
-
- if column == 0: # name
- newName, ok = QInputDialog.getText(self, "Rig Builder", "New name", QLineEdit.Normal, item.module.name)
- if ok and newName:
- newName = replaceSpecialChars(newName).strip()
-
- # rename in connections
- connections = _keepConnections(item.module)
-
- item.module.name = newName
- item.setText(0, item.module.name + " ")
-
- # update connections
- for data in connections:
- srcAttr = data["attr"]
- module = data["module"]
- for m, a in data["connections"]:
- a.connect = module.getPath().replace(m.getPath(inclusive=False), "") + "/" + srcAttr.name # update connection path
-
- item.setExpanded(not item.isExpanded()) # revert expand on double click
-
def makeItemFromModule(self, module):
- item = QTreeWidgetItem([module.name+" ", module.getRelativeLoadedPathString()+" ", " ", module.uid])
- item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled)
- item.module = module
- item.module.modified = False
+ item = ModuleItem(module)
for ch in module.getChildren():
item.addChild(self.makeItemFromModule(ch))
@@ -723,7 +788,7 @@ def makeItemFromModule(self, module):
def contextMenuEvent(self, event):
menu = QMenu(self)
- for m in self.mainWindow.menuBar.findChildren(QMenu):
+ for m in self.mainWindow.menu.findChildren(QMenu):
if m.title():
for a in m.actions():
menu.addAction(a)
@@ -734,7 +799,7 @@ def sendModuleToServer(self):
selectedItems = self.selectedItems()
if not selectedItems:
return
-
+
msg = "\n".join([item.module.name for item in selectedItems])
if QMessageBox.question(self, "Rig Builder", "Send modules to server?\n"+msg, QMessageBox.Yes and QMessageBox.No, QMessageBox.Yes) != QMessageBox.Yes:
@@ -764,32 +829,24 @@ def importModule(self):
if DCC == "maya":
sceneDir = os.path.dirname(om.MFileIO.currentFile())
- path, _ = QFileDialog.getOpenFileName(mainWindow, "Import", sceneDir, "*.xml")
+ filePath, _ = QFileDialog.getOpenFileName(mainWindow, "Import", sceneDir, "*.xml")
- if not path:
+ if not filePath:
return
Module.updateUidsCache()
try:
- m = Module.loadFromFile(path)
+ m = Module.loadFromFile(filePath)
m.update()
-
- item = self.makeItemFromModule(m)
- self.addTopLevelItem(item)
+ self.addTopLevelItem(self.makeItemFromModule(m))
except ET.ParseError:
- print("Error '{}': invalid module".format(path))
+ print("Error '{}': invalid module".format(filePath))
self.mainWindow.showLog()
self.mainWindow.logWidget.ensureCursorVisible()
def saveModule(self):
- def clearModifiedFlag(module): # clear modified flag on embeded modules
- module.modified = False
- for ch in module.getChildren():
- if not ch.uid:
- clearModifiedFlag(ch)
-
selectedItems = self.selectedItems()
if not selectedItems:
return
@@ -798,7 +855,7 @@ def clearModifiedFlag(module): # clear modified flag on embeded modules
if QMessageBox.question(self, "Rig Builder", "Save modules?\n"+msg, QMessageBox.Yes and QMessageBox.No, QMessageBox.Yes) != QMessageBox.Yes:
return
-
+
for item in selectedItems:
outputPath = item.module.getSavePath()
@@ -811,9 +868,9 @@ def clearModifiedFlag(module): # clear modified flag on embeded modules
os.makedirs(dirname)
item.module.saveToFile(outputPath)
- clearModifiedFlag(item.module)
-
- item.setText(1, item.module.getRelativeLoadedPathString()+" ") # update path string
+ item.emitDataChanged() # path changed
+ item.clearModifiedFlag()
+ self.mainWindow.attributesTabWidget.updateWidgetStyles()
def saveAsModule(self):
for item in self.selectedItems():
@@ -823,7 +880,9 @@ def saveAsModule(self):
if outputPath:
item.module.uid = generateUid()
item.module.saveToFile(outputPath)
- item.setText(1, item.module.getRelativeLoadedPathString()+" ") # update path string
+ item.emitDataChanged() # path and uid changed
+ item.clearModifiedFlag()
+ self.mainWindow.attributesTabWidget.updateWidgetStyles()
def embedModule(self):
selectedItems = self.selectedItems()
@@ -834,13 +893,11 @@ def embedModule(self):
if QMessageBox.question(self, "Rig Builder", "Embed modules?\n"+msg, QMessageBox.Yes and QMessageBox.No, QMessageBox.Yes) != QMessageBox.Yes:
return
-
+
for item in selectedItems:
item.module.uid = ""
item.module.loadedFrom = ""
-
- for i in range(1,4): # clear path, source and uid
- item.setText(i, "") # update path string
+ item.emitDataChanged() # path and uid changed
def updateModule(self):
selectedItems = self.selectedItems()
@@ -852,7 +909,7 @@ def updateModule(self):
msg = "\n".join([item.module.name for item in selectedItems])
if QMessageBox.question(self, "Rig Builder", "Update modules?\n"+msg, QMessageBox.Yes and QMessageBox.No, QMessageBox.Yes) != QMessageBox.Yes:
return
-
+
for item in selectedItems:
item.module.update()
@@ -880,8 +937,7 @@ def updateModule(self):
def muteModule(self):
for item in self.selectedItems():
item.module.muted = not item.module.muted
-
- self.repaint()
+ item.emitDataChanged()
def duplicateModule(self):
newItems = []
@@ -920,6 +976,19 @@ def removeModule(self):
else:
self.invisibleRootItem().removeChild(item)
+ def addModuleFromBrowser(self, modulePath):
+ m = Module.loadFromFile(modulePath)
+ m.update()
+ self.addTopLevelItem(self.makeItemFromModule(m))
+
+ # add to recent modules
+ if m not in self.mainWindow.infoWidget.recentModules:
+ self.mainWindow.infoWidget.recentModules.insert(0, m)
+ if len(self.mainWindow.infoWidget.recentModules) > 10:
+ self.mainWindow.infoWidget.recentModules.pop()
+
+ return m
+
def browseModuleSelector(self, *, mask=None, updateSource=None, modulesFrom=None):
if mask:
self.moduleListDialog.maskWidget.setText(mask)
@@ -932,19 +1001,6 @@ def browseModuleSelector(self, *, mask=None, updateSource=None, modulesFrom=None
self.moduleListDialog.exec_()
- if self.moduleListDialog.selectedFileName:
- m = Module.loadFromFile(self.moduleListDialog.selectedFileName)
- m.update()
- self.addTopLevelItem(self.makeItemFromModule(m))
-
- # add to recent modules
- if m not in self.mainWindow.infoWidget.recentModules:
- self.mainWindow.infoWidget.recentModules.insert(0, m)
- if len(self.mainWindow.infoWidget.recentModules) > 10:
- self.mainWindow.infoWidget.recentModules.pop()
-
- return m
-
def event(self, event):
if event.type() == QEvent.KeyPress:
if event.key() == Qt.Key_Tab:
@@ -956,10 +1012,10 @@ def event(self, event):
return QTreeWidget.event(self, event)
class TemplateSelectorDialog(QDialog):
- def __init__(self, **kwargs):
- super(TemplateSelectorDialog, self).__init__(**kwargs)
+ selectedTemplate = Signal(str)
- self.selectedTemplate = None
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
self.setWindowTitle("Template Selector")
self.setGeometry(0, 0, 700, 500)
@@ -989,7 +1045,7 @@ def __init__(self, **kwargs):
centerWindow(self)
def selectTemplate(self, t):
- self.selectedTemplate = t
+ self.selectedTemplate.emit(t)
self.done(0)
def updateTemplates(self):
@@ -1013,10 +1069,11 @@ class EditTemplateWidget(QWidget):
nameChanged = Signal(str, str)
def __init__(self, name, template, **kwargs):
- super(EditTemplateWidget, self).__init__(**kwargs)
+ super().__init__(**kwargs)
self.template = template
- self.connectedTo = ""
+ self.attrConnect = ""
+ self.attrExpression = ""
layout = QHBoxLayout()
layout.setContentsMargins(0,0,0,0)
@@ -1090,7 +1147,8 @@ def downBtnClicked(self):
w = editAttrsWidget.insertCustomWidget(self.template, idx+2)
w.templateWidget.setJsonData(self.templateWidget.getJsonData())
w.nameWidget.setText(self.nameWidget.text())
- w.connectedTo = self.connectedTo
+ w.attrConnect = self.attrConnect
+ w.attrExpression = self.attrExpression
self.deleteLater()
def upBtnClicked(self):
@@ -1100,16 +1158,17 @@ def upBtnClicked(self):
w = editAttrsWidget.insertCustomWidget(self.template, idx-1)
w.templateWidget.setJsonData(self.templateWidget.getJsonData())
w.nameWidget.setText(self.nameWidget.text())
- w.connectedTo = self.connectedTo
+ w.attrConnect = self.attrConnect
+ w.attrExpression = self.attrExpression
self.deleteLater()
class EditAttributesWidget(QWidget):
nameChanged = Signal(str, str)
- def __init__(self, module, category, **kwargs):
- super(EditAttributesWidget, self).__init__(**kwargs)
+ def __init__(self, moduleItem, category, **kwargs):
+ super().__init__(**kwargs)
- self.module = module
+ self.moduleItem = moduleItem
self.category = category
layout = QVBoxLayout()
@@ -1117,12 +1176,13 @@ def __init__(self, module, category, **kwargs):
self.attributesLayout = QVBoxLayout()
- for a in self.module.getAttributes():
+ for a in self.moduleItem.module.getAttributes():
if a.category == self.category:
w = self.insertCustomWidget(a.template)
w.nameWidget.setText(a.name)
w.templateWidget.setJsonData(a.data)
- w.connectedTo = a.connect
+ w.attrConnect = a.connect
+ w.attrExpression = a.expression
layout.addLayout(self.attributesLayout)
layout.addStretch()
@@ -1156,9 +1216,8 @@ def pasteAttribute(self):
def addTemplateAttribute(self):
selector = TemplateSelectorDialog(parent=mainWindow)
+ selector.selectedTemplate.connect(lambda t: self.insertCustomWidget(t))
selector.exec_()
- if selector.selectedTemplate:
- self.insertCustomWidget(selector.selectedTemplate)
def insertCustomWidget(self, template, row=None):
if not widgets.TemplateWidgets.get(template):
@@ -1179,11 +1238,11 @@ def resizeNameFields(self):
w.nameWidget.setFixedWidth(maxWidth)
class EditAttributesTabWidget(QTabWidget):
- def __init__(self, module, currentIndex=0, **kwargs):
- super(EditAttributesTabWidget, self).__init__(**kwargs)
+ def __init__(self, moduleItem, currentIndex=0, **kwargs):
+ super().__init__(**kwargs)
- self.module = module
- self.tempRunCode = module.runCode
+ self.moduleItem = moduleItem
+ self.tempRunCode = moduleItem.module.runCode
self.setTabBar(QTabBar())
self.setMovable(True)
@@ -1192,7 +1251,7 @@ def __init__(self, module, currentIndex=0, **kwargs):
self.tabCloseRequested.connect(self.tabCloseRequest)
tabTitlesInOrder = []
- for a in self.module.getAttributes():
+ for a in self.moduleItem.module.getAttributes():
if a.category not in tabTitlesInOrder:
tabTitlesInOrder.append(a.category)
@@ -1205,7 +1264,7 @@ def __init__(self, module, currentIndex=0, **kwargs):
self.setCurrentIndex(currentIndex)
def addTabCategory(self, category):
- w = EditAttributesWidget(self.module, category)
+ w = EditAttributesWidget(self.moduleItem, category)
w.nameChanged.connect(self.nameChangedCallback)
scrollArea = QScrollArea()
@@ -1223,11 +1282,11 @@ def nameChangedCallback(self, oldName, newName):
self.tempRunCode = replacePairs(pairs, self.tempRunCode)
# rename in connections
- for m, a in self.module.listConnections(self.module.findAttribute(oldName)):
- a.connect = self.module.getPath().replace(m.getPath(inclusive=False), "") + "/" + newName # update connection path
+ for m, a in self.moduleItem.module.listConnections(self.moduleItem.module.findAttribute(oldName)):
+ a.connect = self.moduleItem.module.getPath().replace(m.getPath(inclusive=False), "") + "/" + newName # update connection path
def tabBarMouseDoubleClickEvent(self, event):
- super(EditAttributesTabWidget, self).mouseDoubleClickEvent(event)
+ super().mouseDoubleClickEvent(event)
idx = self.currentIndex()
newName, ok = QInputDialog.getText(self, "Rig Builder", "New name", QLineEdit.Normal, self.tabText(idx))
@@ -1254,18 +1313,18 @@ def clearTabs(self):
self.clear()
class EditAttributesDialog(QDialog):
- def __init__(self, module, currentIndex=0, **kwargs):
- super(EditAttributesDialog, self).__init__(**kwargs)
+ def __init__(self, moduleItem, currentIndex=0, **kwargs):
+ super().__init__(**kwargs)
- self.module = module
+ self.moduleItem = moduleItem
- self.setWindowTitle("Edit Attributes - " + self.module.name)
+ self.setWindowTitle("Edit Attributes - " + self.moduleItem.module.name)
self.setGeometry(0, 0, 800, 600)
layout = QVBoxLayout()
self.setLayout(layout)
- self.tabWidget = EditAttributesTabWidget(self.module, currentIndex)
+ self.tabWidget = EditAttributesTabWidget(self.moduleItem, currentIndex)
okBtn = QPushButton("Ok")
okBtn.clicked.connect(self.saveAttributes)
@@ -1282,7 +1341,7 @@ def __init__(self, module, currentIndex=0, **kwargs):
centerWindow(self)
def saveAttributes(self):
- self.module.clearAttributes()
+ self.moduleItem.module.clearAttributes()
for i in range(self.tabWidget.count()):
attrsLayout = self.tabWidget.widget(i).widget().attributesLayout # tab/scrollArea/EditAttributesWidget
@@ -1294,57 +1353,59 @@ def saveAttributes(self):
a.data = w.templateWidget.getJsonData()
a.template = w.template
a.category = self.tabWidget.tabText(i)
- a.connect = w.connectedTo
- self.module.addAttribute(a)
+ a.connect = w.attrConnect
+ a.expression = w.attrExpression
+ self.moduleItem.module.addAttribute(a)
- self.module.runCode = self.tabWidget.tempRunCode
- self.module.modified = True
+ self.moduleItem.module.runCode = self.tabWidget.tempRunCode
+ self.moduleItem.module.modified = True
+ self.moduleItem.emitDataChanged()
self.accept()
class CodeEditorWidget(CodeEditorWithNumbersWidget):
- def __init__(self, module=None, *, mainWindow=None, **kwargs):
- super(CodeEditorWidget, self).__init__(**kwargs)
+ def __init__(self, moduleItem=None, *, mainWindow=None, **kwargs):
+ super().__init__(**kwargs)
self.mainWindow = mainWindow
- self.module = module
+ self.moduleItem = moduleItem
self._skipSaving = False
- self.editorWidget.syntax = PythonHighlighter(self.editorWidget.document())
self.editorWidget.textChanged.connect(self.codeChanged)
self.updateState()
def codeChanged(self):
- if not self.module or self._skipSaving:
+ if not self.moduleItem or self._skipSaving:
return
- self.module.runCode = self.editorWidget.toPlainText()
- self.module.modified = True
+ self.moduleItem.module.runCode = self.editorWidget.toPlainText()
+ self.moduleItem.module.modified = True
+ self.moduleItem.emitDataChanged()
def updateState(self):
- if not self.module:
+ if not self.moduleItem:
return
self.editorWidget.ignoreStates = True
self._skipSaving = True
- self.editorWidget.setText(self.module.runCode)
+ self.editorWidget.setText(self.moduleItem.module.runCode)
self._skipSaving = False
self.editorWidget.ignoreStates = False
self.editorWidget.document().clearUndoRedoStacks()
self.generateCompletionWords()
- self.editorWidget.preset = id(self.module)
+ self.editorWidget.preset = id(self.moduleItem)
self.editorWidget.loadState()
def generateCompletionWords(self):
- if not self.module:
+ if not self.moduleItem:
return
words = list(self.mainWindow.getModuleGlobalEnv().keys())
words.extend(list(widgets.WidgetsAPI.keys()))
- for a in self.module.getAttributes():
+ for a in self.moduleItem.module.getAttributes():
words.append("@" + a.name)
words.append("@set_" + a.name)
@@ -1352,7 +1413,7 @@ def generateCompletionWords(self):
class LogHighligher(QSyntaxHighlighter):
def __init__(self, parent):
- super(LogHighligher, self).__init__(parent)
+ super().__init__(parent)
self.highlightingRules = []
@@ -1384,7 +1445,7 @@ def highlightBlock(self, text):
class LogWidget(QTextEdit):
def __init__(self, **kwargs):
- super(LogWidget, self).__init__(**kwargs)
+ super().__init__(**kwargs)
self.syntax = LogHighligher(self.document())
self.setPlaceholderText("Output and errors or warnings...")
@@ -1396,7 +1457,7 @@ def write(self, txt):
class WideSplitterHandle(QSplitterHandle):
def __init__(self, orientation, parent, **kwargs):
- super(WideSplitterHandle, self).__init__(orientation, parent, **kwargs)
+ super().__init__(orientation, parent, **kwargs)
def paintEvent(self, event):
painter = QPainter(self)
@@ -1407,15 +1468,15 @@ def paintEvent(self, event):
class WideSplitter(QSplitter):
def __init__(self, orientation, **kwargs):
- super(WideSplitter, self).__init__(orientation, **kwargs)
+ super().__init__(orientation, **kwargs)
self.setHandleWidth(16)
def createHandle(self):
return WideSplitterHandle(self.orientation(), self)
-class MyProgressBar(QWidget):
+class MyProgressBar(QWidget):
def __init__(self, **kwargs):
- super(MyProgressBar, self).__init__(**kwargs)
+ super().__init__(**kwargs)
self.queue = []
self.labelSize = 25
@@ -1466,7 +1527,7 @@ def endProgress(self):
class RigBuilderWindow(QFrame):
def __init__(self):
- super(RigBuilderWindow, self).__init__(parent=ParentWindow)
+ super().__init__(parent=ParentWindow)
self.setWindowTitle("Rig Builder")
self.setGeometry(0, 0, 1300, 700)
@@ -1479,7 +1540,7 @@ def __init__(self):
self.logWidget = LogWidget()
self.logWidget.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
- self.attributesTabWidget = AttributesTabWidget(mainWindow=self)
+ self.attributesTabWidget = AttributesTabWidget(None, mainWindow=self)
self.attributesTabWidget.hide()
self.treeWidget = TreeWidget(mainWindow=self)
@@ -1523,27 +1584,29 @@ def __init__(self):
self.progressBarWidget = MyProgressBar()
self.progressBarWidget.hide()
- self.menuBar = self.createMenu()
- layout.setMenuBar(self.menuBar)
+ self.menu = self.getMenu()
+ self.treeWidget.addActions(getActions(self.menu))
+ setActionsLocalShortcut(self.treeWidget)
layout.addWidget(self.vsplitter)
layout.addWidget(self.progressBarWidget)
centerWindow(self)
- def createMenu(self):
- menuBar = QMenuBar(self)
+ def getMenu(self):
+ menu = QMenu(self)
- fileMenu = menuBar.addMenu("File")
+ fileMenu = menu.addMenu("File")
fileMenu.addAction("New", self.treeWidget.insertModule, "Insert")
fileMenu.addAction("Import", self.treeWidget.importModule, "Ctrl+I")
fileMenu.addSeparator()
fileMenu.addAction("Save", self.treeWidget.saveModule, "Ctrl+S")
fileMenu.addAction("Save as", self.treeWidget.saveAsModule)
fileMenu.addSeparator()
- fileMenu.addAction("Locate file", self.locateModuleFile)
+ fileMenu.addAction("Locate file", self.locateModuleFile)
+ fileMenu.addAction("Copy tool code", self.copyToolCode)
- editMenu = menuBar.addMenu("Edit")
+ editMenu = menu.addMenu("Edit")
editMenu.addAction("Duplicate", self.treeWidget.duplicateModule, "Ctrl+D")
editMenu.addSeparator()
editMenu.addAction("Update", self.treeWidget.updateModule, "Ctrl+U")
@@ -1554,10 +1617,17 @@ def createMenu(self):
editMenu.addAction("Remove", self.treeWidget.removeModule, "Delete")
editMenu.addAction("Clear all", self.clearAllModules)
- helpMenu = menuBar.addMenu("Help")
+ helpMenu = menu.addMenu("Help")
helpMenu.addAction("Documentation", self.showDocumenation)
- return menuBar
+ return menu
+
+ def copyToolCode(self):
+ selectedItems = self.treeWidget.selectedItems()
+ if selectedItems:
+ item = selectedItems[0]
+ code = '''import rigBuilder;rigBuilder.RigBuilderTool(r"{}").show()'''.format(item.module.getRelativePath())
+ QApplication.clipboard().setText(code)
def clearAllModules(self):
if QMessageBox.question(self, "Rig Builder", "Remove all modules?", QMessageBox.Yes and QMessageBox.No, QMessageBox.Yes) == QMessageBox.Yes:
@@ -1572,21 +1642,21 @@ def locateModuleFile(self):
subprocess.call("explorer /select,\"{}\"".format(os.path.normpath(item.module.loadedFrom)))
def treeItemSelectionChanged(self):
- selected = self.treeWidget.selectedItems()
- en = True if selected else False
+ selectedItems = self.treeWidget.selectedItems()
+ en = True if selectedItems else False
self.attributesTabWidget.setVisible(en)
self.runBtn.setVisible(en)
self.infoWidget.setVisible(not en)
self.codeEditorWidget.setEnabled(en and not self.isCodeEditorHidden())
- if selected:
- item = selected[0]
+ if selectedItems:
+ item = selectedItems[0]
- self.attributesTabWidget.module = item.module
+ self.attributesTabWidget.moduleItem = item
self.attributesTabWidget.updateTabs()
if self.codeEditorWidget.isEnabled():
- self.codeEditorWidget.module = item.module
+ self.codeEditorWidget.moduleItem = item
self.codeEditorWidget.updateState()
def infoLinkClicked(self, url):
@@ -1638,23 +1708,17 @@ def displayFiles(files, *, local):
self.infoWidget.insertHtml("".join(template))
self.infoWidget.moveCursor(QTextCursor.Start)
- def getCurrentModule(self):
- selected = self.treeWidget.selectedItems()
- if not selected:
- return
- return selected[0].module
-
def isCodeEditorHidden(self):
return self.vsplitter.sizes()[1] == 0 # code section size
def codeSplitterMoved(self, sz, n):
- currentModule = self.getCurrentModule()
+ selectedItems = self.treeWidget.selectedItems()
if self.isCodeEditorHidden():
self.codeEditorWidget.setEnabled(False)
- elif not self.codeEditorWidget.isEnabled() and currentModule:
- self.codeEditorWidget.module = currentModule
+ elif not self.codeEditorWidget.isEnabled() and selectedItems:
+ self.codeEditorWidget.moduleItem = selectedItems[0]
self.codeEditorWidget.updateState()
self.codeEditorWidget.setEnabled(True)
@@ -1735,25 +1799,36 @@ def getChildrenCount(item):
self.progressBarWidget.endProgress()
self.attributesTabWidget.updateTabs()
-def RigBuilderTool(spec, child=None): # spec can be full path, relative path, uid
+def RigBuilderTool(spec, child=None, *, size=None): # spec can be full path, relative path, uid
module = Module.loadModule(spec)
if not module:
print("Cannot load '{}' module".format(spec))
return
- if child:
- module = module.findChild(child)
+ if child is not None:
+ if type(child) == str:
+ module = module.findChild(child)
+
+ elif type(child) == int:
+ module = module.getChildren()[child]
+
if not module:
print("Cannot find '{}' child".format(child))
return
w = RigBuilderWindow()
- w.menuBar.hide()
+ w.setWindowTitle("Rig Builder Tool - {}".format(module.getPath()))
w.treeWidget.addTopLevelItem(w.treeWidget.makeItemFromModule(module))
w.treeWidget.setCurrentItem(w.treeWidget.topLevelItem(0))
- w.setWindowTitle("Rig Builder Tool - {}".format(module.getPath()))
- w.attributesTabWidget.adjustSize()
- w.resize(w.attributesTabWidget.size() + QSize(50, 100))
+
+ if size:
+ if type(size) in [int, float]:
+ size = [size, size]
+ w.resize(size[0], size[1])
+ else: # auto size
+ w.attributesTabWidget.adjustSize()
+ w.resize(w.attributesTabWidget.size() + QSize(50, 100))
+
w.codeEditorWidget.hide()
w.treeWidget.hide()
centerWindow(w)
diff --git a/classes.py b/classes.py
index eaac602..3bce61b 100644
--- a/classes.py
+++ b/classes.py
@@ -87,19 +87,23 @@ def categorizeFilesByModTime(files):
class ExitModuleException(Exception):pass
class AttributeResolverError(Exception):pass
+class AttributeExpressionError(Exception):pass
class ModuleNotFoundError(Exception):pass
class CopyJsonError(Exception):pass
class Attribute(object):
- def __init__(self, name, data={}, category="", template="", connect=""):
+ def __init__(self, name, data={}, category="", template="", connect="", expression=""):
self.name = name
self.data = copyJson(data) # as json
self.category = category
self.template = template
self.connect = connect # attribute connection, format: /a/b/c, where c is attr, a/b is a parent relative path
+ self.expression = expression # python code
+
+ self.modified = False # used by UI
def copy(self):
- return Attribute(self.name, copyJson(self.data), self.category, self.template, self.connect)
+ return Attribute(self.name, copyJson(self.data), self.category, self.template, self.connect, self.expression)
def __eq__(self, other):
if not isinstance(other, Attribute):
@@ -121,6 +125,18 @@ 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():
@@ -136,17 +152,24 @@ def toXml(self, *, keepConnection=True):
attrsStr = " ".join(["{}=\"{}\"".format(k, v) for k, v in attrs])
- header = ""
- return header.format(attribs=attrsStr, data=json.dumps(self.data))
+ 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
+ data["_expression"] = self.expression
+ header = ""
+ return header.format(attribs=attrsStr, data=json.dumps(data))
+
@staticmethod
def fromXml(root):
attr = Attribute("")
attr.name = root.attrib["name"]
attr.template = root.attrib["template"]
attr.category = root.attrib["category"]
- attr.connect = root.attrib["connect"]
+ attr.connect = root.attrib["connect"]
attr.data = json.loads(root.text)
+
+ # additional data
+ attr.expression = attr.data.pop("_expression", "")
return attr
class Module(object):
@@ -168,6 +191,8 @@ def __init__(self, name):
self.muted = False
self.loadedFrom = ""
+ self.modified = False # used by UI
+
def copy(self):
module = Module(self.name)
module.uid = self.uid
@@ -450,7 +475,7 @@ def findConnectionSourceForAttribute(self, attr):
if srcAttr:
return srcModule.findConnectionSourceForAttribute(srcAttr)
- return attr
+ return self, attr
def findModuleAndAttributeByPath(self, path):
'''
@@ -483,12 +508,14 @@ def resolveConnection(self, attr):
if not attr.connect:
return
- srcAttr = self.findConnectionSourceForAttribute(attr)
+ srcMod, srcAttr = self.findConnectionSourceForAttribute(attr)
+
if srcAttr is not attr:
if attr.template != srcAttr.template:
raise AttributeResolverError("{}: '{}' has incompatible connection template".format(self.name, attr.name))
try:
+ srcMod.resolveExpression(srcAttr)
attr.updateFromAttribute(srcAttr)
except TypeError:
@@ -497,10 +524,22 @@ def resolveConnection(self, attr):
else:
raise AttributeResolverError("{}: cannot resolve connection for '{}' which is '{}'".format(self.name, attr.name, attr.connect))
+ def resolveExpression(self, attr):
+ if not attr.expression:
+ return
+
+ env = {"module": ModuleWrapper(self), "ch": self.ch, "data": attr.data, "value": copyJson(attr.getDefaultValue())}
+ try:
+ exec(attr.expression, env)
+ except Exception as e:
+ raise AttributeExpressionError("{}: '{}' has invalid expression: {}".format(self.name, attr.name, str(e)))
+ else:
+ attr.setDefaultValue(env["value"])
+
def ch(self, path, key=None):
mod, attr = self.findModuleAndAttributeByPath(path)
if attr:
- attr = mod.findConnectionSourceForAttribute(attr)
+ _, attr = mod.findConnectionSourceForAttribute(attr)
if not key:
return copyJson(attr.getDefaultValue())
else:
@@ -511,7 +550,7 @@ def ch(self, path, key=None):
def chset(self, path, value, key=None):
mod, attr = self.findModuleAndAttributeByPath(path)
if attr:
- attr = mod.findConnectionSourceForAttribute(attr)
+ _, attr = mod.findConnectionSourceForAttribute(attr)
if not key:
attr.setDefaultValue(value)
else:
@@ -537,7 +576,8 @@ def run(self, env, *, uiCallback=None):
attrPrefix = "attr_"
for attr in self._attributes:
- self.resolveConnection(attr)
+ self.resolveExpression(attr)
+ self.resolveConnection(attr) # connection rewrites data
attrWrapper = AttributeWrapper(self, attr)
localEnv[attrPrefix+attr.name] = attrWrapper.get()
@@ -605,7 +645,7 @@ def set(self, v):
srcAttr = self._attr
if self._attr.connect and self._module.parent:
- srcAttr = self._module.findConnectionSourceForAttribute(self._attr)
+ _, srcAttr = self._module.findConnectionSourceForAttribute(self._attr)
default = srcAttr.data.get("default")
if default:
diff --git a/editor.py b/editor.py
index 88ba0ab..4befd64 100644
--- a/editor.py
+++ b/editor.py
@@ -3,11 +3,11 @@
from PySide2.QtWidgets import *
import re
-from .utils import clamp
+from .utils import clamp, getActions, setActionsLocalShortcut, wordAtCursor, findBracketSpans
class PythonHighlighter(QSyntaxHighlighter):
def __init__(self, parent):
- super(PythonHighlighter, self).__init__(parent)
+ super().__init__(parent)
self.highlightingRules = []
@@ -127,6 +127,8 @@ def match_multiline(self, text, delimiter, in_state, style):
else:
return False
+
+
def fontSize(font):
return font.pixelSize() if font.pixelSize() > 0 else font.pointSize()
@@ -159,7 +161,7 @@ def highlightLine(widget, line=None, *, clear=False):
class SwoopHighligher(QSyntaxHighlighter):
def __init__(self, parent):
- super(SwoopHighligher, self).__init__(parent)
+ super().__init__(parent)
self.highlightingRules = []
@@ -200,18 +202,12 @@ def highlightBlock(self, text):
self.setCurrentBlockState(0)
class SwoopSearchDialog(QDialog):
- def __init__(self, edit, **kwargs):
- super(SwoopSearchDialog, self).__init__(**kwargs)
-
- self.edit = edit
+ def __init__(self, textWidget, **kwargs):
+ super().__init__(**kwargs)
- self.useWordBoundary = False
- self.findInsideBrackets = False
- self.caseSensitive = True
- self.downOnly = False
+ self.textWidget = textWidget
self.replaceMode = False
-
self.replacePattern = None
self.previousLines = []
@@ -224,10 +220,28 @@ def __init__(self, edit, **kwargs):
self.setLayout(layout)
self.filterWidget = QLineEdit()
- self.filterWidget.setToolTip("Ctrl-C - case sensitive
Ctrl-W - word boundary
Ctrl-B - find inside brackets
Ctrl-D - down only
Ctrl-R - replace mode")
self.filterWidget.textChanged.connect(self.filterTextChanged)
self.filterWidget.keyPressEvent = self.filterKeyPressEvent
+ self.replaceModeBtn = QPushButton("Replace")
+ self.replaceModeBtn.clicked.connect(lambda _:self.switchReplaceMode())
+
+ filterLayout = QHBoxLayout()
+ filterLayout.addWidget(self.filterWidget)
+ filterLayout.addWidget(self.replaceModeBtn)
+
+ settingsLayout = QHBoxLayout()
+ self.caseSensitiveWidget = QCheckBox("Case sensitive")
+ self.caseSensitiveWidget.setChecked(True)
+ self.caseSensitiveWidget.stateChanged.connect(self.filterTextChanged)
+ self.wholeWordWidget = QCheckBox("Whole word")
+ self.wholeWordWidget.stateChanged.connect(self.filterTextChanged)
+ self.indentDeeperWidget = QCheckBox("Indent deeper")
+ self.indentDeeperWidget.stateChanged.connect(self.filterTextChanged)
+ settingsLayout.addWidget(self.caseSensitiveWidget)
+ settingsLayout.addWidget(self.wholeWordWidget)
+ settingsLayout.addWidget(self.indentDeeperWidget)
+
self.resultsWidget = QTextEdit()
self.resultsWidget.setReadOnly(True)
self.resultsWidget.setWordWrapMode(QTextOption.NoWrap)
@@ -235,41 +249,30 @@ def __init__(self, edit, **kwargs):
self.resultsWidget.mousePressEvent = self.resultsMousePressEvent
self.resultsWidget.keyPressEvent = self.filterWidget.keyPressEvent
- self.statusWidget = QLabel()
- self.statusWidget.hide()
-
- layout.addWidget(self.filterWidget)
+ layout.addLayout(filterLayout)
+ layout.addLayout(settingsLayout)
layout.addWidget(self.resultsWidget)
- layout.addWidget(self.statusWidget)
self.rejected.connect(self.whenRejected)
- # save initial state
- self.text = self.edit.toPlainText()
- cursor = self.edit.textCursor()
-
- self.updateSavedCursor()
+ def showEvent(self, event):
+ cursor = self.textWidget.textCursor()
+ text = self.textWidget.toPlainText()
- self.savedSettings["lines"] = self.text.split("\n")
+ self.savedSettings["cursor"] = cursor
+ self.savedSettings["scroll"] = self.textWidget.verticalScrollBar().value()
+ self.savedSettings["lines"] = text.split("\n")
findText = cursor.selectedText()
if not findText:
findText = wordAtCursor(cursor)[0]
- self.filterWidget.setText(findText)
-
- def updateSavedCursor(self):
- cursor = self.edit.textCursor()
- brackets = findBracketSpans(self.text, cursor.position())
- self.savedSettings["cursor"] = cursor
- self.savedSettings["scroll"] = self.edit.verticalScrollBar().value()
- self.savedSettings["brackets"] = brackets
-
- self.findInsideBrackets = brackets[0]!=brackets[1] and self.findInsideBrackets
+ if findText == self.filterWidget.text():
+ self.filterTextChanged()
+ else:
+ self.filterWidget.setText(findText)
- def showEvent(self, event):
- self.updateSavedCursor()
+ self.switchReplaceMode(False)
self.reposition()
- self.updateStatus()
def resultsMousePressEvent(self, event):
cursor = self.resultsWidget.cursorForPosition(event.pos())
@@ -278,73 +281,66 @@ def resultsMousePressEvent(self, event):
self.resultsLineChanged()
def reposition(self):
- c = self.edit.mapToGlobal(self.edit.cursorRect().topLeft())
+ c = self.textWidget.mapToGlobal(self.textWidget.cursorRect().topLeft())
w = self.resultsWidget.document().idealWidth() + 30
h = self.resultsWidget.document().blockCount()*self.resultsWidget.cursorRect().height() + 110
- self.setGeometry(c.x(), c.y() + fontSize(self.edit.font())+5, clamp(w, 0, 500), clamp(h, 0, 400))
+ self.setGeometry(c.x(), c.y() + fontSize(self.textWidget.font())+5, clamp(w, 0, 500), clamp(h, 0, 400))
+
+ def switchReplaceMode(self, value=None):
+ self.replaceMode = not self.replaceMode if value is None else value
+
+ if self.replaceMode:
+ self.filterWidget.setStyleSheet("background-color: #433567")
+ self.replacePattern = self.getFilterPattern()
+ self.replaceModeBtn.setText("Cancel")
+ else:
+ self.filterWidget.setStyleSheet("")
+ self.replaceModeBtn.setText("Replace")
+ self.filterTextChanged()
+
+ self.caseSensitiveWidget.setEnabled(not self.replaceMode)
+ self.wholeWordWidget.setEnabled(not self.replaceMode)
+ self.indentDeeperWidget.setEnabled(not self.replaceMode)
def resultsLineChanged(self):
if self.replaceMode:
return
+
+ caseSensitive = self.caseSensitiveWidget.isChecked()
resultsLine = self.resultsWidget.textCursor().block().text()
if not resultsLine:
return
lineNumber = re.search("^(\\d+)", resultsLine).group()
- self.edit.gotoLine(int(lineNumber))
+ self.textWidget.gotoLine(int(lineNumber))
currentFilter = self.getFilterPattern()
+ cursor = self.textWidget.textCursor()
+ currentLine = cursor.block().text()
- currentLine = self.edit.textCursor().block().text()
-
- r = re.search(currentFilter, currentLine, re.IGNORECASE if not self.caseSensitive else 0)
+ r = re.search(currentFilter, currentLine, re.IGNORECASE if not caseSensitive else 0)
if r:
- cursor = self.edit.textCursor()
pos = cursor.block().position() + r.start()
if pos > 0:
cursor.setPosition(pos)
- self.edit.setTextCursor(cursor)
+ self.textWidget.setTextCursor(cursor)
- cursorY = self.edit.cursorRect().top()
- scrollBar = self.edit.verticalScrollBar()
- scrollBar.setValue(scrollBar.value() + cursorY - self.edit.geometry().height()/2)
+ cursorY = self.textWidget.cursorRect().top()
+ scrollBar = self.textWidget.verticalScrollBar()
+ scrollBar.setValue(scrollBar.value() + cursorY - self.textWidget.geometry().height()/2)
self.reposition()
- def updateStatus(self):
- items = []
-
- if self.useWordBoundary:
- items.append("[word]")
-
- if self.caseSensitive:
- items.append("[case]")
-
- if self.findInsideBrackets:
- items.append("[brackets]")
-
- if self.downOnly:
- items.append("[down]")
-
- if self.replaceMode:
- items.append("[REPLACE '%s']"%self.replacePattern)
-
- if items:
- self.statusWidget.setText(" ".join(items))
- self.statusWidget.show()
- else:
- self.statusWidget.hide()
-
def filterKeyPressEvent(self, event):
ctrl = event.modifiers() & Qt.ControlModifier
- rw = self.resultsWidget
- line = rw.textCursor().block().blockNumber()
- lineCount = rw.document().blockCount()-1
-
if event.key() in [Qt.Key_Down, Qt.Key_Up, Qt.Key_PageDown, Qt.Key_PageUp]:
+ rw = self.resultsWidget
+ line = rw.textCursor().block().blockNumber()
+ lineCount = rw.document().blockCount()-1
+
highlightLine(rw, clamp(line, 0, lineCount), clear=True)
if event.key() == Qt.Key_Down:
highlightLine(rw, clamp(line+1, 0, lineCount))
@@ -359,57 +355,14 @@ def filterKeyPressEvent(self, event):
self.resultsLineChanged()
- elif ctrl and event.key() == Qt.Key_W: # use word boundary
- if not self.replaceMode:
- self.useWordBoundary = not self.useWordBoundary
- self.updateStatus()
- self.filterTextChanged()
-
- elif ctrl and event.key() == Qt.Key_B: # find inside brackets
- if not self.replaceMode:
- self.findInsideBrackets = not self.findInsideBrackets
- self.updateSavedCursor()
- self.updateStatus()
- self.filterTextChanged()
-
- elif ctrl and event.key() == Qt.Key_D: # down only
- if not self.replaceMode:
- self.downOnly = not self.downOnly
- self.updateSavedCursor()
- self.updateStatus()
- self.filterTextChanged()
-
- elif ctrl and event.key() == Qt.Key_C: # case sensitive
- if self.filterWidget.selectedText():
- self.filterWidget.copy()
- else:
- if not self.replaceMode:
- self.caseSensitive = not self.caseSensitive
- self.updateStatus()
- self.filterTextChanged()
-
- elif ctrl and event.key() == Qt.Key_R: # replace mode
- self.replaceMode = not self.replaceMode
- if self.replaceMode:
- self.filterWidget.setStyleSheet("background-color: #433567")
- self.replacePattern = self.getFilterPattern()
- else:
- self.filterWidget.setStyleSheet("")
- self.filterTextChanged()
-
- self.updateStatus()
-
- elif event.key() == Qt.Key_F3:
- self.accept()
-
elif event.key() == Qt.Key_Return: # accept
if self.replaceMode:
- cursor = self.edit.textCursor()
+ cursor = self.textWidget.textCursor()
savedBlock = self.savedSettings["cursor"].block()
savedColumn = self.savedSettings["cursor"].positionInBlock()
- doc = self.edit.document()
+ doc = self.textWidget.document()
getIndent = lambda s: s[:len(s) - len(s.lstrip())]
@@ -432,26 +385,28 @@ def filterKeyPressEvent(self, event):
cursor.endEditBlock()
cursor.setPosition(savedBlock.position() + savedColumn)
- self.edit.setTextCursor(cursor)
- self.edit.verticalScrollBar().setValue(self.savedSettings["scroll"])
+ self.textWidget.setTextCursor(cursor)
+ self.textWidget.verticalScrollBar().setValue(self.savedSettings["scroll"])
- self.edit.setFocus()
+ self.textWidget.setFocus()
self.accept()
else:
QLineEdit.keyPressEvent(self.filterWidget, event)
def whenRejected(self):
- self.edit.setTextCursor(self.savedSettings["cursor"])
- self.edit.verticalScrollBar().setValue(self.savedSettings["scroll"])
- self.edit.setFocus()
+ self.textWidget.setTextCursor(self.savedSettings["cursor"])
+ self.textWidget.verticalScrollBar().setValue(self.savedSettings["scroll"])
+ self.textWidget.setFocus()
def getFilterPattern(self):
currentFilter = re.escape(self.filterWidget.text())
+ useWordBoundary = self.wholeWordWidget.isChecked()
+
if not currentFilter:
return ""
- if self.useWordBoundary:
+ if useWordBoundary:
currentFilter = "\\b" + currentFilter + "\\b"
return currentFilter
@@ -460,6 +415,11 @@ def filterTextChanged(self):
self.resultsWidget.clear()
self.resultsWidget.setCurrentCharFormat(QTextCharFormat())
+ caseSensitive = self.caseSensitiveWidget.isChecked()
+ deeperOnly = self.indentDeeperWidget.isChecked()
+
+ getIndent = lambda s: s[:len(s) - len(s.lstrip())]
+
if self.replaceMode: # replace mode
replaceString = self.filterWidget.text()
pattern = self.getFilterPattern()
@@ -467,8 +427,7 @@ def filterTextChanged(self):
lines = []
for line in self.previousLines:
n, text = re.search("^(\\d+)\\s*(.*)$", line).groups()
-
- text = re.sub(self.replacePattern, replaceString, text, 0, re.IGNORECASE if not self.caseSensitive else 0)
+ text = re.sub(self.replacePattern, replaceString, text, 0, re.IGNORECASE if not caseSensitive else 0)
lines.append("{0:<5} {1}".format(n, text.strip()))
self.resultsWidget.setText("\n".join(lines))
@@ -476,41 +435,30 @@ def filterTextChanged(self):
self.resultsWidget.syntax.rehighlight()
else: # search mode
- startBlock, endBlock = 0, 0
-
- if self.findInsideBrackets:
- cursor = QTextCursor(self.savedSettings["cursor"])
- cursor.setPosition(self.savedSettings["brackets"][0])
- startBlock = cursor.block().blockNumber()
- cursor.setPosition(self.savedSettings["brackets"][1])
- endBlock = cursor.block().blockNumber()
-
- if self.downOnly:
- cursor = QTextCursor(self.savedSettings["cursor"])
- startBlock = cursor.block().blockNumber()
-
currentFilter = self.getFilterPattern()
- currentBlock = self.edit.textCursor().block().blockNumber()
+ currentBlockNumber = self.savedSettings["cursor"].block().blockNumber()
- self.previousLines = []
+ indent = getIndent(self.savedSettings["cursor"].block().text())
counter = 0
currentIndex = 0
+ self.previousLines = []
for i, line in enumerate(self.savedSettings["lines"]):
if not line.strip():
continue
- if self.findInsideBrackets and (i < startBlock or i > endBlock):
- continue
+ if deeperOnly: # works down and indent deeper only
+ if i < currentBlockNumber: # skip previous lines
+ continue
- if self.downOnly and i < startBlock:
- continue
+ elif getIndent(line) < indent:
+ break
- if i == currentBlock:
+ if i == currentBlockNumber:
currentIndex = counter
- r = re.search(currentFilter, line, re.IGNORECASE if not self.caseSensitive else 0)
+ r = re.search(currentFilter, line, re.IGNORECASE if not caseSensitive else 0)
if r:
self.previousLines.append("{0:<5} {1}".format(i+1, line.strip()))
counter += 1
@@ -523,17 +471,90 @@ def filterTextChanged(self):
highlightLine(self.resultsWidget, currentIndex)
self.resultsLineChanged()
+class CompletionWidget(QTextEdit):
+ def __init__(self, items, **kwargs):
+ super().__init__(**kwargs)
+
+ self._prevLine = 0
+
+ self.setWindowFlags(Qt.FramelessWindowHint)
+ self.setAttribute(Qt.WA_ShowWithoutActivating)
+
+ self.setReadOnly(True)
+ self.setWordWrapMode(QTextOption.NoWrap)
+
+ self.updateItems([])
+
+ def currentLine(self):
+ return self.textCursor().block().blockNumber()
+
+ def lineCount(self):
+ return self.document().blockCount()
+
+ def gotoLine(self, line):
+ line = clamp(line, 0, self.lineCount()-1)
+ highlightLine(self, self._prevLine, clear=True)
+ self._prevLine = line
+ highlightLine(self, self._prevLine)
+
+ def mousePressEvent(self, event):
+ super().mousePressEvent(event)
+ self.gotoLine(self.currentLine())
+
+ def keyPressEvent(self, event):
+ if event.key() == Qt.Key_Return:
+ super().keyPressEvent(event)
+ else:
+ lineCount = self.lineCount()
+
+ keyMove = {Qt.Key_Down: 1, Qt.Key_Up: -1, Qt.Key_PageDown: 10, Qt.Key_PageUp: -10}
+ offset = keyMove.get(event.key(), 0)
+ if offset != 0:
+ self.gotoLine(clamp(self._prevLine+offset, 0, lineCount))
+
+ def updateItems(self, items):
+ if not items:
+ return
+
+ self.clear()
+ self.setCurrentCharFormat(QTextCharFormat())
+
+ lines = []
+ for line in items:
+ lines.append(line)
+
+ self.setText("\n".join(lines))
+
+ highlightLine(self, 0)
+ self._prevLine = 0
+
+ self.autoResize()
+
+ def autoResize(self):
+ w = self.document().idealWidth() + 10
+ h = self.document().blockCount()*self.cursorRect().height() + 30
+
+ maxWidth = self.parent().width() - self.parent().cursorRect().left() - 30
+ maxHeight = self.parent().height() - self.parent().cursorRect().top() - 30
+
+ self.setFixedSize(clamp(w, 0,maxWidth), clamp(h, 0, maxHeight))
+
+ def showEvent(self, event):
+ self.autoResize()
+
class CodeEditorWidget(QTextEdit):
editorState = {}
TabSpaces = 4
def __init__(self, **kwargs):
- super(CodeEditorWidget, self).__init__(**kwargs)
+ super().__init__(**kwargs)
self.preset = "default"
self.commentChar = "#"
self.ignoreStates = False # don't save/load states
+ self.syntax = PythonHighlighter(self.document())
+
self._editorState = {}
self._canShowCompletions = True
@@ -543,6 +564,7 @@ def __init__(self, **kwargs):
self._searchStartWord = ("", 0, 0)
self._prevCursorPosition = 0
+ self.swoopSearchDialog = SwoopSearchDialog(self, parent=self)
self.completionWidget = CompletionWidget([], parent=self)
self.completionWidget.hide()
@@ -552,9 +574,12 @@ def __init__(self, **kwargs):
self.setWordWrapMode(QTextOption.NoWrap)
self.cursorPositionChanged.connect(self.editorCursorPositionChanged)
- self.verticalScrollBar().valueChanged.connect(lambda _: self.saveState(cursor=False, scroll=True, bookmarks=False))
+ self.verticalScrollBar().valueChanged.connect(self.scrollBarChanged)
self.textChanged.connect(self.editorTextChanged)
+ self.addActions(getActions(self.getMenu()))
+ setActionsLocalShortcut(self)
+
def event(self, event):
if event.type() == QEvent.KeyPress:
if event.key() == Qt.Key_Tab:
@@ -582,7 +607,12 @@ def event(self, event):
event.accept()
return True
- return super(CodeEditorWidget, self).event(event)
+ return super().event(event)
+
+ def scrollBarChanged(self, _):
+ self.saveState(cursor=False, scroll=True, bookmarks=False)
+ if self.completionWidget.isVisible():
+ self.completionWidget.hide()
def setBookmark(self, line=-1):
if line == -1:
@@ -604,20 +634,24 @@ def setBookmark(self, line=-1):
block.setUserData(blockData)
self.saveState(cursor=False, scroll=False, bookmarks=True)
- def gotoNextBookmark(self, start=-1):
+ def gotoNextBookmark(self):
doc = self.document()
- if start == -1:
- start = self.textCursor().block().blockNumber()+1
+ def gotoBookmark(startLine):
+ for i in range(startLine, doc.blockCount()):
+ b = doc.findBlockByNumber(i)
+
+ blockData = b.userData()
+ if blockData and blockData.hasBookmark:
+ self.setTextCursor(QTextCursor(b))
+ self.centerLine()
+ break
- for i in range(start, doc.blockCount()):
- b = doc.findBlockByNumber(i)
+ startLine = self.textCursor().block().blockNumber()
+ gotoBookmark(startLine + 1)
- blockData = b.userData()
- if blockData and blockData.hasBookmark:
- self.setTextCursor(QTextCursor(b))
- self.centerLine()
- break
+ if startLine == self.textCursor().block().blockNumber():
+ gotoBookmark(0)
def loadState(self, cursor=True, scroll=True, bookmarks=True):
if self.ignoreStates:
@@ -672,14 +706,16 @@ def saveState(self, cursor=True, scroll=True, bookmarks=False):
data = b.userData()
if data and data.hasBookmark:
state["bookmarks"].append(i)
-
- def contextMenuEvent(self, event):
+
+ def getMenu(self):
menu = QMenu(self)
-
menu.addAction("Swoop search", self.swoopSearch, "F3")
menu.addAction("Highlight selected", self.highlightSelected, "Ctrl+H")
menu.addSeparator()
menu.addAction("Goto line", self.gotoLine, "Ctrl+G")
+ menu.addAction("Duplicate line", self.duplicateLine, "Ctrl+D")
+ menu.addAction("Move line up", lambda: self.moveLine("up"), "Alt+Up")
+ menu.addAction("Move line down", lambda: self.moveLine("down"), "Alt+Down")
menu.addAction("Remove line", self.removeLines, "Ctrl+K")
menu.addAction("Comment line", self.toggleCommentBlock, "Ctrl+;")
menu.addSeparator()
@@ -687,7 +723,10 @@ def contextMenuEvent(self, event):
menu.addAction("Next bookmark", self.gotoNextBookmark, "F2")
menu.addSeparator()
menu.addAction("Select All", self.selectAll, "Ctrl+A")
+ return menu
+ def contextMenuEvent(self, event):
+ menu = self.getMenu()
menu.popup(event.globalPos())
def wheelEvent(self, event):
@@ -704,7 +743,7 @@ def wheelEvent(self, event):
self.parent().numberBarWidget.updateState()
else:
- QTextEdit.wheelEvent(self, event)
+ super().wheelEvent(event)
def keyPressEvent(self, event):
shift = event.modifiers() & Qt.ShiftModifier
@@ -713,33 +752,12 @@ def keyPressEvent(self, event):
key = event.key()
if ctrl and alt and key == Qt.Key_Space:
- cursor = self.textCursor()
- pos = cursor.position()
- start, end = findBracketSpans(self.toPlainText(), pos)
- if start is not None and end is not None:
- cursor.setPosition(start+1)
- cursor.setPosition(end, QTextCursor.KeepAnchor)
- self.setTextCursor(cursor)
+ self.selectInBracket()
elif key in [Qt.Key_Left, Qt.Key_Right]:
- QTextEdit.keyPressEvent(self, event)
+ super().keyPressEvent(event)
self.completionWidget.hide()
- elif alt and key == Qt.Key_F2: # set bookmark
- self.setBookmark()
-
- elif key == Qt.Key_F2: # next bookmark
- n = self.textCursor().block().blockNumber()
- self.gotoNextBookmark()
- if self.textCursor().block().blockNumber() == n:
- self.gotoNextBookmark(0)
-
- elif key == Qt.Key_F3: # emacs swoop
- self.swoopSearch()
-
- elif ctrl and key == Qt.Key_G: # goto line
- self.gotoLine()
-
elif key == Qt.Key_Escape:
self.completionWidget.hide()
@@ -753,42 +771,14 @@ def keyPressEvent(self, event):
block = cursor.block().text()
spc = re.search("^(\\s*)", block).groups("")[0]
- QTextEdit.keyPressEvent(self, event)
+ super().keyPressEvent(event)
if spc:
cursor.insertText(spc)
self.setTextCursor(cursor)
elif key == Qt.Key_Backtab:
- cursor = self.textCursor()
- tabSpaces = " "*CodeEditorWidget.TabSpaces
- start, end = cursor.selectionStart(), cursor.selectionEnd()
- cursor.clearSelection()
-
- cursor.setPosition(start)
-
- cursor.beginEditBlock()
- while cursor.position() < end:
- cursor.movePosition(QTextCursor.StartOfLine)
- cursor.movePosition(QTextCursor.NextWord, QTextCursor.KeepAnchor)
- selText = cursor.selectedText()
-
- # if the text starts with the tab_char, replace it
- if selText.startswith(tabSpaces):
- text = selText.replace(tabSpaces, "", 1)
- end -= len(tabSpaces)
- cursor.insertText(text)
-
- if not cursor.movePosition(QTextCursor.Down):
- break
-
- cursor.endEditBlock()
-
- elif alt and key == Qt.Key_Up: # move line up
- self.moveLineUp()
-
- elif alt and key == Qt.Key_Down: # move line down
- self.moveLineDown()
+ self.decreaseIndent()
elif key in [Qt.Key_Up, Qt.Key_Down, Qt.Key_PageDown, Qt.Key_PageUp]:
if self.completionWidget.isVisible():
@@ -799,50 +789,58 @@ def keyPressEvent(self, event):
else:
super().keyPressEvent(event)
- elif ctrl and key == Qt.Key_H: # highlight selected
- self.highlightSelected()
+ else:
+ super().keyPressEvent(event)
- elif ctrl and key == Qt.Key_K: # remove line
- self.removeLines()
+ def decreaseIndent(self):
+ cursor = self.textCursor()
+ tabSpaces = " "*CodeEditorWidget.TabSpaces
+ start, end = cursor.selectionStart(), cursor.selectionEnd()
+ cursor.clearSelection()
- elif ctrl and key == Qt.Key_Semicolon: # comment
- self.toggleCommentBlock()
+ cursor.setPosition(start)
- else:
- QTextEdit.keyPressEvent(self, event)
+ cursor.beginEditBlock()
+ while cursor.position() < end:
+ cursor.movePosition(QTextCursor.StartOfLine)
+ cursor.movePosition(QTextCursor.NextWord, QTextCursor.KeepAnchor)
+ selText = cursor.selectedText()
+
+ # if the text starts with the tab_char, replace it
+ if selText.startswith(tabSpaces):
+ text = selText.replace(tabSpaces, "", 1)
+ end -= len(tabSpaces)
+ cursor.insertText(text)
+
+ if not cursor.movePosition(QTextCursor.Down):
+ break
- def swoopSearch(self):
- swoopSearchDialog = SwoopSearchDialog(self, parent=self)
- swoopSearchDialog.exec_()
+ cursor.endEditBlock()
- def moveLineUp(self):
+ def selectInBracket(self):
cursor = self.textCursor()
- if not cursor.block().previous().isValid() or cursor.selectedText():
- return
+ pos = cursor.position()
+ start, end = findBracketSpans(self.toPlainText(), pos)
+ if start is not None and end is not None:
+ cursor.setPosition(start+1)
+ cursor.setPosition(end, QTextCursor.KeepAnchor)
+ self.setTextCursor(cursor)
- text = cursor.block().text()
- pos = cursor.positionInBlock()
+ def swoopSearch(self):
+ self.swoopSearchDialog.exec_()
+ def duplicateLine(self):
+ cursor = self.textCursor()
+ line = cursor.block().text()
+ cursor.movePosition(QTextCursor.EndOfBlock)
cursor.beginEditBlock()
- cursor.movePosition(QTextCursor.StartOfBlock)
- cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor)
- cursor.removeSelectedText()
- cursor.deletePreviousChar()
- cursor.movePosition(QTextCursor.StartOfBlock)
- cursor.insertText(text)
cursor.insertBlock()
+ cursor.insertText(line)
cursor.endEditBlock()
+ self.setTextCursor(cursor)
- cursor.movePosition(QTextCursor.Up)
- cursor.movePosition(QTextCursor.StartOfBlock)
- cursor.movePosition(QTextCursor.Right, n=pos)
-
- self.setTextCursor(cursor)
-
- def moveLineDown(self):
+ def moveLine(self, direction):
cursor = self.textCursor()
- if not cursor.block().next().isValid() or cursor.selectedText():
- return
text = cursor.block().text()
pos = cursor.positionInBlock()
@@ -851,12 +849,21 @@ def moveLineDown(self):
cursor.movePosition(QTextCursor.StartOfBlock)
cursor.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor)
cursor.removeSelectedText()
- cursor.deleteChar()
- cursor.movePosition(QTextCursor.EndOfBlock)
- cursor.insertBlock()
- cursor.insertText(text)
- cursor.endEditBlock()
+ if direction == "up":
+ cursor.deletePreviousChar()
+ cursor.movePosition(QTextCursor.StartOfBlock)
+ cursor.insertText(text)
+ cursor.insertBlock()
+ cursor.movePosition(QTextCursor.Up)
+
+ elif direction == "down":
+ cursor.deleteChar()
+ cursor.movePosition(QTextCursor.EndOfBlock)
+ cursor.insertBlock()
+ cursor.insertText(text)
+
+ cursor.endEditBlock()
cursor.movePosition(QTextCursor.StartOfBlock)
cursor.movePosition(QTextCursor.Right, n=pos)
@@ -1078,186 +1085,35 @@ def showCompletions(self, items):
self.completionWidget.show()
-def findOpeningBracketPosition(text, offset, brackets="{(["):
- openingBrackets = "{(["
- closingBrackets = "})]"
- stack = [0 for i in range(len(openingBrackets))] # for each bracket set 0 as default
-
- if offset < 0 or offset >= len(text):
- return None
-
- if text[offset] in closingBrackets:
- offset -= 1
-
- for i in range(offset, -1, -1):
- c = text[i]
-
- if c in brackets and c in openingBrackets and stack[openingBrackets.index(c)] == 0:
- return i
-
- elif c in openingBrackets:
- stack[openingBrackets.index(c)] += 1
-
- elif c in closingBrackets:
- stack[closingBrackets.index(c)] -= 1
-
-def findClosingBracketPosition(text, offset, brackets="})]"):
- openingBrackets = "{(["
- closingBrackets = "})]"
- stack = [0 for i in range(len(openingBrackets))] # for each bracket set 0 as default
-
- if offset < 0 or offset >= len(text):
- return None
-
- if text[offset] in openingBrackets:
- offset += 1
-
- for i in range(offset, len(text)):
- c = text[i]
-
- if c in brackets and c in closingBrackets and stack[closingBrackets.index(c)] == 0:
- return i
-
- elif c in openingBrackets:
- stack[openingBrackets.index(c)] += 1
-
- elif c in closingBrackets:
- stack[closingBrackets.index(c)] -= 1
-
-def findBracketSpans(text, offset):
- s = findOpeningBracketPosition(text, offset, "{([")
- if s is not None:
- matchingClosingBracket = {"{":"}", "(":")", "[":"]"}[text[s]]
- e = findClosingBracketPosition(text, offset, matchingClosingBracket)
- else:
- e = findClosingBracketPosition(text, offset, "})]")
- return (s,e)
-
-def wordAtCursor(cursor):
- cursor = QTextCursor(cursor)
- pos = cursor.position()
-
- lpart = ""
- start = pos-1
- ch = cursor.document().characterAt(start)
- while ch and re.match("[@\\w]", ch):
- lpart += ch
- start -= 1
-
- if ch == "@": # @ can be the first character only
- break
-
- ch = cursor.document().characterAt(start)
-
- rpart = ""
- end = pos
- ch = cursor.document().characterAt(end)
- while ch and re.match("[\\w]", ch):
- rpart += ch
- end += 1
- ch = cursor.document().characterAt(end)
-
- return (lpart[::-1]+rpart, start+1, end)
-
-class CompletionWidget(QTextEdit):
- def __init__(self, items, **kwargs):
- super(CompletionWidget, self).__init__(**kwargs)
-
- self._prevLine = 0
-
- self.setWindowFlags(Qt.FramelessWindowHint)
- self.setAttribute(Qt.WA_ShowWithoutActivating)
-
- self.setReadOnly(True)
- self.setWordWrapMode(QTextOption.NoWrap)
-
- self.updateItems([])
-
- def currentLine(self):
- return self.textCursor().block().blockNumber()
-
- def lineCount(self):
- return self.document().blockCount()
-
- def gotoLine(self, line):
- highlightLine(self, self._prevLine, clear=True)
- self._prevLine = line
- highlightLine(self, self._prevLine)
-
- def mousePressEvent(self, event):
- super().mousePressEvent(event)
- self.gotoLine(self.currentLine())
-
- def keyPressEvent(self, event):
- if event.key() == Qt.Key_Return:
- super().keyPressEvent(event)
- else:
- lineCount = self.lineCount()
-
- keyMove = {Qt.Key_Down: 1, Qt.Key_Up: -1, Qt.Key_PageDown: 10, Qt.Key_PageUp: -10}
- offset = keyMove.get(event.key(), 0)
- if offset != 0:
- self.gotoLine(clamp(self._prevLine+offset, 0, lineCount))
-
- def updateItems(self, items):
- if not items:
- return
-
- self.clear()
- self.setCurrentCharFormat(QTextCharFormat())
-
- lines = []
- for line in items:
- lines.append(line)
-
- self.setText("\n".join(lines))
-
- highlightLine(self, 0)
- self._prevLine = 0
-
- self.autoResize()
-
- def autoResize(self):
- w = self.document().idealWidth() + 10
- h = self.document().blockCount()*self.cursorRect().height() + 30
-
- maxWidth = self.parent().width() - self.parent().cursorRect().left() - 30
- maxHeight = self.parent().height() - self.parent().cursorRect().top() - 30
-
- self.setFixedSize(clamp(w, 0,maxWidth), clamp(h, 0, maxHeight))
-
- def showEvent(self, event):
- self.autoResize()
-
class NumberBarWidget(QWidget):
- def __init__(self, edit, *kwargs):
- super(NumberBarWidget, self).__init__(*kwargs)
- self.edit = edit
+ def __init__(self, textWidget, *kwargs):
+ super().__init__(*kwargs)
+ self.textWidget = textWidget
self.highest_line = 0
def updateState(self, *args):
- self.setFont(self.edit.font())
+ self.setFont(self.textWidget.font())
width = self.fontMetrics().width(str(self.highest_line)) + 19
self.setFixedWidth(width)
self.update()
def paintEvent(self, event):
- contents_y = self.edit.verticalScrollBar().value()
- page_bottom = contents_y + self.edit.viewport().height()
+ contents_y = self.textWidget.verticalScrollBar().value()
+ page_bottom = contents_y + self.textWidget.viewport().height()
font_metrics = self.fontMetrics()
- current_block = self.edit.document().findBlock(self.edit.textCursor().position())
+ current_block = self.textWidget.document().findBlock(self.textWidget.textCursor().position())
painter = QPainter(self)
line_count = 0
# Iterate over all text blocks in the document.
- block = self.edit.document().begin()
+ block = self.textWidget.document().begin()
while block.isValid():
line_count += 1
# The top left position of the block in the document
- position = self.edit.document().documentLayout().blockBoundingRect(block).topLeft()
+ position = self.textWidget.document().documentLayout().blockBoundingRect(block).topLeft()
# Check if the position of the block is out side of the visible
# area.
@@ -1273,19 +1129,19 @@ def paintEvent(self, event):
block = block.next()
- self.highest_line = self.edit.document().blockCount()
+ self.highest_line = self.textWidget.document().blockCount()
painter.end()
QWidget.paintEvent(self, event)
class TextBlockData(QTextBlockUserData):
def __init__(self):
- super(TextBlockData, self).__init__()
+ super().__init__()
self.hasBookmark = False
class CodeEditorWithNumbersWidget(QWidget):
def __init__(self, **kwargs):
- super(CodeEditorWithNumbersWidget, self).__init__(**kwargs)
+ super().__init__(**kwargs)
self.editorWidget = CodeEditorWidget()
diff --git a/jsonWidget.py b/jsonWidget.py
new file mode 100644
index 0000000..7510581
--- /dev/null
+++ b/jsonWidget.py
@@ -0,0 +1,670 @@
+from PySide2.QtWidgets import *
+from PySide2.QtCore import *
+from PySide2.QtGui import *
+import json
+import re
+import os
+
+from .utils import clamp, getActions, centerWindow, setActionsLocalShortcut, SimpleUndo, SearchReplaceDialog
+
+RootDirectory = os.path.dirname(__file__)
+
+FloatType = 6 # QMetaType.Double
+
+class EditJsonTextWindow(QDialog):
+ saved = Signal(object)
+
+ def __init__(self, data, *, readOnly=False, **kwargs):
+ super().__init__(**kwargs)
+
+ self.setWindowTitle("Edit JSON")
+ self.setGeometry(100, 100, 600, 500)
+
+ layout = QVBoxLayout()
+ layout.setMargin(0)
+ self.setLayout(layout)
+
+ self.prettyPrintWidget = QCheckBox("Pretty print")
+ self.prettyPrintWidget.toggled.connect(self.prettyPrintToggled)
+
+ self.textWidget = QTextEdit()
+ self.textWidget.setPlainText(json.dumps(data))
+ self.textWidget.setReadOnly(readOnly)
+ self.textWidget.setTabStopWidth(16)
+ self.textWidget.setAcceptRichText(False)
+ self.textWidget.setWordWrapMode(QTextOption.NoWrap)
+
+ btn = QPushButton("Save" if not readOnly else "Close")
+ btn.clicked.connect(self.saveAndClose if not readOnly else self.accept)
+
+ layout.addWidget(self.prettyPrintWidget)
+ layout.addWidget(self.textWidget)
+ layout.addWidget(btn)
+
+ centerWindow(self)
+
+ self.prettyPrintWidget.setChecked(True)
+
+ def prettyPrintToggled(self, value):
+ try:
+ data = json.loads(self.textWidget.toPlainText())
+ self.textWidget.setPlainText(json.dumps(data, indent=4 if value else None))
+ except:
+ pass
+
+ def saveAndClose(self):
+ try:
+ data = json.loads(self.textWidget.toPlainText())
+ self.saved.emit(data)
+ self.accept()
+ except:
+ QMessageBox.critical(self, "DemBones Tool", "Invalid JSON")
+
+class JsonItem(QTreeWidgetItem):
+ NoneType = 0
+ BoolType = 1
+ IntType = 2
+ FloatType = 3
+ StringType = 4
+ ListType = 5
+ DictType = 6
+
+ KeyRole = Qt.UserRole + 1
+
+ def __init__(self, jsonType, data=None):
+ super().__init__()
+
+ self._editValue = None
+
+ self.jsonType = jsonType
+
+ self.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
+
+ if jsonType == self.ListType:
+ self.setFlags(self.flags() | Qt.ItemIsDropEnabled)
+
+ elif jsonType in [self.BoolType, self.IntType, self.FloatType, self.StringType]:
+ self.setFlags(self.flags() | Qt.ItemIsEditable)
+
+ colors = {self.BoolType: QColor("#CDEB8B"),
+ self.IntType: QColor("#B85E28"),
+ self.FloatType: QColor("#BF8208"),
+ self.StringType: QColor("#AAAA33"),
+ self.ListType: QColor("#008A00"),
+ self.DictType: QColor("#28A6E3")}
+
+ self.setForeground(0, QBrush(colors.get(jsonType, Qt.gray)))
+
+ self.setData(0, Qt.EditRole, data)
+
+ def clone(self):
+ item = JsonItem(self.jsonType)
+ item._editValue = self._editValue
+ item.setText(0, self.text(0))
+ item.setData(0, self.KeyRole, self.data(0, self.KeyRole))
+ for i in range(self.childCount()):
+ item.addChild(self.child(i).clone())
+ return item
+
+ def setData(self, _, role, value):
+ if role == Qt.EditRole:
+ self._editValue = value
+
+ super().setData(0, role, value)
+
+ def data(self, _, role):
+ if role == Qt.ToolTipRole:
+ return str(self._editValue or "")
+
+ elif role == Qt.EditRole:
+ return self._editValue
+
+ elif role == Qt.DisplayRole:
+ key = self.data(0, self.KeyRole)
+
+ if self.jsonType == self.ListType:
+ key = key or ""
+ return key + "[%d]"%self.childCount()
+
+ elif self.jsonType == self.DictType:
+ key = key or ""
+
+ childCount = self.childCount()
+ maxChildKeys = 10
+ children = []
+ for i in range(childCount):
+ if i >= maxChildKeys:
+ break
+ k = self.child(i).data(0, self.KeyRole)
+ children.append(k)
+
+ items = ",".join(children)
+ suffix = "..." if childCount > maxChildKeys else ""
+ return key + "{%s%s}"%(items, suffix)
+
+ else:
+ value = self._editValue
+ if self.jsonType == self.StringType:
+ if len(value) > 25:
+ value = value[:25] + "..."
+ value = "\"{}\"".format(value)
+
+ if key:
+ return key + ":" + str(value)
+ else:
+ return str(value)
+ 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:
+ idx = self.treeWidget().invisibleRootItem().indexOfChild(self)
+ return path + "[%d]"%idx
+
+ if parent.jsonType == parent.ListType:
+ idx = parent.indexOfChild(self)
+ return parent.getPath(path) + "[%d]"%idx
+
+ elif parent.jsonType == parent.DictType:
+ key = self.data(0, self.KeyRole)
+ return parent.getPath(path) + "[\"%s\"]"%key
+
+ return path
+
+class FloatEditor(QDoubleSpinBox):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setDecimals(6)
+
+class FloatEditorCreator(QItemEditorCreatorBase):
+ def __init__(self):
+ super().__init__()
+
+ def createWidget(self, parent):
+ return FloatEditor(parent)
+
+class JsonItemFactor(QItemEditorFactory):
+ def __init__(self):
+ super().__init__()
+ self.registerEditor(FloatType, FloatEditorCreator())
+
+class JsonWidget(QTreeWidget):
+ itemMoved = Signal(QTreeWidgetItem)
+ itemAdded = Signal(QTreeWidgetItem)
+ itemRemoved = Signal(QTreeWidgetItem)
+ dataLoaded = Signal()
+ cleared = Signal()
+ readOnlyChanged = Signal(bool)
+ rootChanged = Signal(QTreeWidgetItem)
+
+ def __init__(self, data=None, **kwargs):
+ super().__init__(**kwargs)
+
+ self._clipboard = []
+ self._readOnly = False
+ self._undoSystem = SimpleUndo()
+
+ self._searchReplaceDialog = SearchReplaceDialog(["Keys"])
+ self._searchReplaceDialog.onReplace.connect(self.onSearchReplace)
+
+ self.header().hide()
+ self.setDragDropMode(QAbstractItemView.InternalMove)
+ self.setDragEnabled(True)
+ self.setDropIndicatorShown(True)
+ self.setSelectionMode(QAbstractItemView.ExtendedSelection)
+ self.setIndentation(32)
+
+ self.itemDelegate().setItemEditorFactory(JsonItemFactor())
+
+ self.setReadOnly(self._readOnly)
+
+ if data:
+ self.fromJsonList([data])
+
+ def getMenu(self):
+ menu = QMenu(self)
+
+ fileMenu = menu.addMenu("File")
+ fileMenu.addAction("Save all", self.saveToFile)
+ fileMenu.addAction("Export selected", lambda: self.saveToFile(item=self.selectedItem()))
+
+ if not self._readOnly:
+ fileMenu.addAction("Load", self.loadFromFile)
+ fileMenu.addAction("Import", self.importFile)
+
+ menu.addSeparator()
+ menu.addAction("Edit JSON", self.editItemData, "Return")
+ menu.addAction("Edit key", self.editKey, "Ctrl+Return")
+
+ undoLabel = "Undo"
+ if not self._undoSystem.isEmpty():
+ undoLabel += " '{}'".format(self._undoSystem.getLastOperationName())
+ undoAction = menu.addAction(undoLabel, self._undoSystem.undo)
+ undoAction.setEnabled(not self._undoSystem.isEmpty())
+ menu.addSeparator()
+
+ addMenu = menu.addMenu("Add")
+ addGroup = QActionGroup(self)
+ addGroup.setExclusive(True)
+ for l, d, key in [("none", None, ""), ("bool", True, "1"), ("int", 0, "2"), ("float", 0.0, "3"), ("string", "", "4"), ("list", [], "5"), ("dict", {}, "6")]:
+ action = addGroup.addAction(l)
+ action.setData(d)
+ action.setShortcut(key)
+
+ addGroup.triggered.connect(lambda action :self.addItem(action.data(), self.selectedItem()))
+ addMenu.addActions(addGroup.actions())
+
+ menu.addAction("Remove", self.removeItem, "Delete")
+
+ def f():
+ if QMessageBox.question(self, "Rig Builder", "Remove all items?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes:
+ self.clear()
+ self.cleared.emit()
+ self._undoSystem.flush()
+ menu.addAction("Clear", f)
+
+ menu.addSeparator()
+
+ moveMenu = menu.addMenu("Move")
+ moveMenu.addAction("Top", lambda:self.moveItem(-9999999), "Ctrl+Shift+Up")
+ moveMenu.addAction("Up", lambda:self.moveItem(-1), "Shift+Up")
+ moveMenu.addAction("Down", lambda:self.moveItem(1), "Shift+Down")
+ moveMenu.addAction("Bottom", lambda:self.moveItem(9999999), "Ctrl+Shift+Down")
+
+ menu.addAction("Duplicate", self.duplicateItem, "Ctrl+D")
+
+ menu.addAction("Copy", self.copyItem, "Ctrl+C")
+ menu.addAction("Cut", self.cutItem, "Ctrl+X")
+ menu.addAction("Paste", self.pasteItem, "Ctrl+V")
+
+ menu.addAction("Replace text", self._searchReplaceDialog.exec_, "Ctrl+R")
+
+ else:
+ menu.addAction("View JSON", self.editItemData, "Return")
+
+ menu.addSeparator()
+ expandMenu = menu.addMenu("Expand")
+ expandMenu.addAction("Toggle", lambda:self.expandItem(self.selectedItem(), recursive=False), "Space")
+ expandMenu.addAction("All", lambda:self.expandItem(self.selectedItem(), True))
+ expandMenu.addAction("Collapse all", lambda:self.expandItem(self.selectedItem(), False))
+ expandMenu.addAction("Toggle all", lambda:self.expandItem(self.selectedItem()), "Shift+Space")
+
+ menu.addAction("Reveal", self.revealSelected, "F")
+ menu.addAction("Set as root", lambda: self.setRootItem(self.selectedItem()), "Ctrl+Space")
+ menu.addAction("Reset root", self.setRootItem, "Escape")
+ menu.addAction("Copy path", self.copyPath)
+
+ menu.addSeparator()
+
+ readOnlyItem = menu.addAction("Read only", lambda: self.setReadOnly(not self._readOnly))
+ readOnlyItem.setCheckable(True)
+ readOnlyItem.setChecked(self._readOnly)
+ menu.addSeparator()
+
+ return menu
+
+ def contextMenuEvent(self, event):
+ menu = self.getMenu()
+ menu.popup(event.globalPos())
+
+ def findItemsByType(self, jsonTypes, parent=None, *, recursive=True):
+ items = []
+ parent = parent or self.invisibleRootItem()
+ for i in range(parent.childCount()):
+ item = parent.child(i)
+ if not jsonTypes or item.jsonType in jsonTypes:
+ items.append(item)
+ if recursive:
+ items.extend(self.findItemsByType(jsonTypes, item, recursive=True))
+ return items
+
+ def copyPath(self):
+ item = self.selectedItem()
+ if item:
+ QApplication.clipboard().setText(item.getPath())
+
+ def onSearchReplace(self, old, new, opts):
+ doReplaceKeys = opts.get("Keys")
+
+ items = self.findItemsByType([], self.selectedItem())
+ changedItems = []
+ for item in items:
+ if item.jsonType == item.StringType:
+ value = item.data(0, Qt.EditRole)
+ if old in value:
+ item.setData(0, Qt.EditRole, value.replace(old, new))
+ changedItems.append(item)
+
+ if doReplaceKeys:
+ key = item.data(0, item.KeyRole)
+ if key and old in key:
+ item.setData(0, item.KeyRole, key.replace(old, new))
+ changedItems.append(item)
+
+ # undo
+ def f():
+ for item in changedItems:
+ if item.jsonType == item.StringType:
+ value = item.data(0, Qt.EditRole)
+ item.setData(0, Qt.EditRole, value.replace(new, old))
+
+ if doReplaceKeys:
+ key = item.data(0, item.KeyRole)
+ item.setData(0, item.KeyRole, key.replace(new, old))
+
+ self._undoSystem.push("ReplaceText", f)
+
+ def editKey(self):
+ item = self.selectedItem()
+ if item:
+ key = item.data(0, item.KeyRole)
+ 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))
+
+ # undo
+ def f():
+ item.setData(0, item.KeyRole, key)
+ self._undoSystem.push("EditKey", f)
+
+ def isReadOnly(self):
+ return self._readOnly
+
+ def setReadOnly(self, value):
+ for a in list(self.actions()):
+ self.removeAction(a)
+
+ self._readOnly = value
+
+ self.addActions(getActions(self.getMenu()))
+ setActionsLocalShortcut(self)
+
+ self.readOnlyChanged.emit(value)
+
+ def selectedItem(self):
+ selectedItems = self.selectedItems()
+ return selectedItems[-1] if selectedItems else None
+
+ def saveToFile(self, *, path=None, item=None):
+ if not path:
+ path, _ = QFileDialog.getSaveFileName(self, "Save JSON", "", "JSON (*.json)")
+
+ if path:
+ if item:
+ data = self.itemToJson(item)
+ else:
+ data = self.toJsonList()
+
+ with open(path, "w") as f:
+ json.dump(data, f)
+
+ def loadFromFile(self):
+ path, _ = QFileDialog.getOpenFileName(self, "Load JSON", "", "JSON (*.json)")
+ if path:
+ self.clear()
+ with open(path, "r") as f:
+ d = json.load(f)
+ self.fromJsonList([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])
+
+ def setRootItem(self, item=None):
+ if item and item.jsonType in [item.ListType, item.DictType]:
+ self.setRootIndex(self.indexFromItem(item))
+ elif item is None:
+ self.setRootIndex(QModelIndex())
+ else:
+ return
+
+ self.rootChanged.emit(item)
+
+ def setItemJson(self, item, data):
+ parentItem = item.parent()
+
+ newItem = self.itemFromJson(data)
+ newItem.setData(0, newItem.KeyRole, item.data(0, item.KeyRole))
+ if parentItem:
+ if parentItem.jsonType == parentItem.DictType:
+ newItem.setData(0, Qt.DisplayRole, item.text(0))
+ newItem.setFlags(item.flags())
+ else:
+ parentItem = self.invisibleRootItem()
+
+ idx = parentItem.indexOfChild(item)
+ isSelected = item.isSelected()
+ isExpanded = item.isExpanded()
+ parentItem.insertChild(idx, newItem)
+ parentItem.removeChild(item)
+ newItem.setExpanded(isExpanded)
+ newItem.setSelected(isSelected)
+ return newItem
+
+ def revealSelected(self):
+ selectedItems = self.selectedItems()
+ if selectedItems:
+ 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)
+
+ # 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)
+
+ 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_()
+
+ def moveItem(self, direction):
+ selectedItems = self.selectedItems()
+ sortedItems = sorted(selectedItems, key=lambda x: -direction*(x.parent() or self.invisibleRootItem()).indexOfChild(x)) # the lowest first
+
+ # undo
+ _items = [((item.parent() or self.invisibleRootItem()).indexOfChild(item), item) for item in sortedItems] # save indices
+ def f():
+ for idx, item in _items if direction > 0 else _items[::-1]:
+ parentItem = item.parent() or self.invisibleRootItem()
+ parentItem.takeChild(parentItem.indexOfChild(item)) # remove
+ parentItem.insertChild(idx, item)
+ self._undoSystem.push("Move", f)
+
+ for item in sortedItems:
+ parentItem = item.parent() or self.invisibleRootItem()
+
+ expand = item.isExpanded()
+ idx = parentItem.indexOfChild(item)
+ parentItem.takeChild(idx)
+ parentItem.insertChild(clamp(idx+direction, 0, parentItem.childCount()), item)
+ item.setSelected(True)
+ item.setExpanded(expand)
+
+ self.itemMoved.emit(item)
+
+ def duplicateItem(self):
+ selectedItems = self.selectedItems()
+
+ self._undoSystem.beginEditBlock("Duplicate")
+
+ for item in selectedItems:
+ data = self.itemToJson(item)
+ parentItem = item.parent() or self.invisibleRootItem()
+ idx = parentItem.indexOfChild(item)
+ newItem = self.addItem(data, parentItem, insertIndex=idx+1 if len(selectedItems) == 1 else None)
+ item.setSelected(False)
+
+ self.itemAdded.emit(newItem)
+
+ self._undoSystem.endEditBlock()
+
+ def copyItem(self):
+ self._clipboard = []
+ for item in self.selectedItems():
+ self._clipboard.append(self.itemToJson(item))
+
+ def cutItem(self):
+ self.copyItem()
+ self._undoSystem.beginEditBlock("Cut")
+ self.removeItem()
+ self._undoSystem.endEditBlock()
+
+ def pasteItem(self):
+ parentItem = self.selectedItem()
+ self._undoSystem.beginEditBlock("Paste")
+ for json in self._clipboard:
+ self.addItem(json, parentItem)
+ self._undoSystem.endEditBlock()
+
+ def addItem(self, json, parentItem=None, *, insertIndex=None):
+ if not parentItem:
+ parentItem = self.itemFromIndex(self.rootIndex())
+
+ item = self.itemFromJson(json)
+
+ if parentItem and parentItem is not self.invisibleRootItem():
+ if parentItem.jsonType == parentItem.DictType:
+ key = parentItem.findUniqueKey()
+ item.setData(0, item.KeyRole, key)
+ elif parentItem.jsonType != parentItem.ListType:
+ return
+ else:
+ parentItem = self.invisibleRootItem()
+
+ if insertIndex is None:
+ parentItem.addChild(item)
+ else:
+ parentItem.insertChild(insertIndex, item)
+
+ item.setSelected(True)
+ self.itemAdded.emit(item)
+
+ # undo
+ def f():
+ parentItem.removeChild(item)
+ self._undoSystem.push("Add", f)
+
+ return item
+
+ def removeItem(self):
+ selectedItems = self.selectedItems()
+
+ # add undo
+ _undoData = []
+ for item in selectedItems:
+ parent = item.parent() or self.invisibleRootItem()
+ idx = parent.indexOfChild(item)
+ _undoData.append([self.getPathIndex(parent), item.clone(), idx])
+
+ def f():
+ for parentIdx, item, idx in _undoData:
+ parent = self.findItemByPathIndex(parentIdx)
+ parent.insertChild(idx, item)
+ self._undoSystem.push("Remove", f)
+
+ for item in selectedItems:
+ (item.parent() or self.invisibleRootItem()).removeChild(item)
+ self.itemRemoved.emit(item)
+
+ def getPathIndex(self, item): # auxiliary function for undo system
+ parent = item.parent()
+ if not parent:
+ return [self.invisibleRootItem().indexOfChild(item)]
+
+ idx = parent.indexOfChild(item)
+ return self.getPathIndex(parent) + [idx]
+
+ def findItemByPathIndex(self, pathIndex, parent=None):
+ parent = parent or self.invisibleRootItem()
+ if not pathIndex:
+ return parent
+
+ idx = pathIndex[0]
+ if idx < parent.childCount():
+ return self.findItemByPathIndex(pathIndex[1:], parent.child(idx))
+
+ def expandItem(self, item, value=None, *, recursive=True):
+ v = not item.isExpanded() if value is None else value
+ if item and item is not self.invisibleRootItem():
+ if item.jsonType == item.DictType or (item.jsonType == item.ListType and item.childCount() < 10):
+ item.setExpanded(v)
+
+ if recursive:
+ for i in range(item.childCount()):
+ self.expandItem(item.child(i), v, recursive=True)
+
+ def toJsonList(self):
+ return [self.itemToJson(self.topLevelItem(i)) for i in range(self.topLevelItemCount())]
+
+ def fromJsonList(self, dataList):
+ for d in dataList:
+ self.addTopLevelItem(self.itemFromJson(d))
+
+ self.expandItem(self.invisibleRootItem(), True)
+ self.dataLoaded.emit()
+
+ self._undoSystem.flush()
+
+ def itemToJson(self, item):
+ if item.jsonType == item.ListType:
+ json = []
+ for i in range(item.childCount()):
+ json.append(self.itemToJson(item.child(i)))
+ return json
+
+ elif item.jsonType == item.DictType:
+ json = {}
+ for i in range(item.childCount()):
+ keyItem = item.child(i)
+ key = keyItem.data(0, keyItem.KeyRole)
+ json[key] = self.itemToJson(keyItem)
+ return json
+
+ else:
+ return item.data(0, Qt.EditRole)
+
+ def itemFromJson(self, data):
+ if type(data) == list:
+ item = JsonItem(JsonItem.ListType)
+ for k in data:
+ chItem = self.itemFromJson(k)
+ chItem.setFlags(chItem.flags() | Qt.ItemIsDragEnabled)
+ item.addChild(chItem)
+
+ elif type(data) == dict:
+ item = JsonItem(JsonItem.DictType)
+ for k,v in data.items():
+ keyItem = self.itemFromJson(v)
+ keyItem.setData(0, keyItem.KeyRole, k)
+ item.addChild(keyItem)
+ else:
+ types = {bool: JsonItem.BoolType, int: JsonItem.IntType, float: JsonItem.FloatType, str: JsonItem.StringType}
+ jsonType = types.get(type(data), JsonItem.NoneType)
+ item = JsonItem(jsonType, data)
+
+ return item
diff --git a/utils.py b/utils.py
index 1383d32..998bbba 100644
--- a/utils.py
+++ b/utils.py
@@ -67,4 +67,211 @@ def clearLayout(layout):
widget.setParent(None)
else:
clearLayout(item.layout())
-
\ No newline at end of file
+
+def getActions(menu, recursive=True):
+ actions = []
+ for action in menu.actions():
+ if action.menu() and recursive:
+ actions.extend(getActions(action.menu(), True))
+ else:
+ actions.append(action)
+ return actions
+
+def setActionsLocalShortcut(widget):
+ for a in getActions(widget):
+ a.setShortcutContext(Qt.WidgetShortcut)
+
+def findOpeningBracketPosition(text, offset, brackets="{(["):
+ openingBrackets = "{(["
+ closingBrackets = "})]"
+ stack = [0 for i in range(len(openingBrackets))] # for each bracket set 0 as default
+
+ if offset < 0 or offset >= len(text):
+ return None
+
+ if text[offset] in closingBrackets:
+ offset -= 1
+
+ for i in range(offset, -1, -1):
+ c = text[i]
+
+ if c in brackets and c in openingBrackets and stack[openingBrackets.index(c)] == 0:
+ return i
+
+ elif c in openingBrackets:
+ stack[openingBrackets.index(c)] += 1
+
+ elif c in closingBrackets:
+ stack[closingBrackets.index(c)] -= 1
+
+def findClosingBracketPosition(text, offset, brackets="})]"):
+ openingBrackets = "{(["
+ closingBrackets = "})]"
+ stack = [0 for _ in range(len(openingBrackets))] # for each bracket set 0 as default
+
+ if offset < 0 or offset >= len(text):
+ return None
+
+ if text[offset] in openingBrackets:
+ offset += 1
+
+ for i in range(offset, len(text)):
+ c = text[i]
+
+ if c in brackets and c in closingBrackets and stack[closingBrackets.index(c)] == 0:
+ return i
+
+ elif c in openingBrackets:
+ stack[openingBrackets.index(c)] += 1
+
+ elif c in closingBrackets:
+ stack[closingBrackets.index(c)] -= 1
+
+def findBracketSpans(text, offset):
+ s = findOpeningBracketPosition(text, offset, "{([")
+ if s is not None:
+ matchingClosingBracket = {"{":"}", "(":")", "[":"]"}[text[s]]
+ e = findClosingBracketPosition(text, offset, matchingClosingBracket)
+ else:
+ e = findClosingBracketPosition(text, offset, "})]")
+ return (s,e)
+
+def wordAtCursor(cursor):
+ cursor = QTextCursor(cursor)
+ pos = cursor.position()
+
+ lpart = ""
+ start = pos-1
+ ch = cursor.document().characterAt(start)
+ while ch and re.match("[@\\w]", ch):
+ lpart += ch
+ start -= 1
+
+ if ch == "@": # @ can be the first character only
+ break
+
+ ch = cursor.document().characterAt(start)
+
+ rpart = ""
+ end = pos
+ ch = cursor.document().characterAt(end)
+ while ch and re.match("[\\w]", ch):
+ rpart += ch
+ end += 1
+ ch = cursor.document().characterAt(end)
+
+ return (lpart[::-1]+rpart, start+1, end)
+
+class SearchReplaceDialog(QDialog):
+ onReplace = Signal(str, str, dict) # old, new, options
+
+ def __init__(self, options=[], **kwargs):
+ super().__init__(**kwargs)
+
+ self.optionsWidgets = {}
+
+ self.setWindowTitle("Search/Replace")
+ layout = QVBoxLayout()
+ self.setLayout(layout)
+
+ self.searchWidget = QLineEdit("L_")
+ self.replaceWidget = QLineEdit("R_")
+
+ btn = QPushButton("Replace")
+ btn.clicked.connect(self.replaceClicked)
+
+ gridLayout = QGridLayout()
+ gridLayout.addWidget(QLabel("Search"),0,0)
+ gridLayout.addWidget(self.searchWidget,0,1)
+ gridLayout.addWidget(QLabel("Replace"),1,0)
+ gridLayout.addWidget(self.replaceWidget,1,1)
+ layout.addLayout(gridLayout)
+
+ for opt in options:
+ w = QCheckBox(opt)
+ self.optionsWidgets[opt] = w
+ layout.addWidget(w)
+
+ layout.addWidget(btn)
+
+ def replaceClicked(self):
+ opts = {l:w.isChecked() for l,w in self.optionsWidgets.items()}
+ self.onReplace.emit(self.searchWidget.text(), self.replaceWidget.text(), opts)
+ self.accept()
+
+class SimpleUndo():
+ def __init__(self):
+ self.undoEnabled = True
+
+ self._undoStack = []
+ self._undoTempStack = []
+ self._tempEditBlockName = ""
+ self._undoOrder = 0 # undo inc/dec this
+
+ def isEmpty(self):
+ return not self._undoStack
+
+ def flush(self):
+ self._undoStack = []
+ self._undoTempStack = []
+
+ def isInEditBlock(self):
+ return self._undoOrder > 0
+
+ def beginEditBlock(self, name="temp"):
+ self._tempEditBlockName = name
+ self._undoOrder += 1
+
+ def endEditBlock(self):
+ self._undoOrder -= 1
+
+ # append all temporary operations as a single undo function
+ if self._undoTempStack and not self.isInEditBlock():
+ def f(stack=self._undoTempStack):
+ for _, func in stack:
+ func()
+
+ self.push(self._tempEditBlockName, f)
+ self._undoTempStack = []
+
+ def getLastOperationName(self):
+ if not self._undoStack:
+ return
+ cmd = self._undoStack[-1][0]
+ return re.match(r"(.+)\s+#", cmd).group(1)
+
+ def push(self, name, undoFunc, operationId=None):
+ def _getLastOperation():
+ if self.isInEditBlock():
+ return self._undoTempStack[-1] if self._undoTempStack else None
+ else:
+ return self._undoStack[-1] if self._undoStack else None
+
+ if not self.undoEnabled:
+ return
+
+ lastOp = _getLastOperation()
+
+ cmd = "{} #{}".format(name, operationId) # generate unique command name
+ if operationId is not None and lastOp and lastOp[0] == cmd: # the same operation, do not add
+ pass
+ else:
+ if self.isInEditBlock():
+ self._undoTempStack.append((cmd, undoFunc))
+ else:
+ self._undoStack.append((cmd, undoFunc))
+
+ def undo(self):
+ if not self._undoStack:
+ print("Nothing to undo")
+ else:
+ self.undoEnabled = False # prevent undoing while undoing
+
+ while True and self._undoStack:
+ _, undoFunc = self._undoStack.pop()
+
+ if callable(undoFunc):
+ undoFunc()
+ break
+
+ self.undoEnabled = True
\ No newline at end of file
diff --git a/widgets.py b/widgets.py
index 28b4257..309ff1c 100644
--- a/widgets.py
+++ b/widgets.py
@@ -7,6 +7,8 @@
import json
import math
from .utils import *
+from .editor import *
+from .jsonWidget import JsonWidget
DCC = os.getenv("RIG_BUILDER_DCC") or "maya"
@@ -30,7 +32,7 @@ def fromSmartConversion(x):
else:
return json.dumps(x) if type(x) not in [str, unicode] else x
-class TemplateWidget(QWidget):
+class TemplateWidget(QFrame):
somethingChanged = Signal()
needUpdateUI = Signal()
@@ -46,83 +48,91 @@ def getJsonData(self):
def setJsonData(self, data):
raise Exception("setJsonData must be implemented")
-
+
class EditTextDialog(QDialog):
- def __init__(self, text="", *, title="Edit", placeholder="", **kwargs):
- super(EditTextDialog, self).__init__(**kwargs)
+ saved = Signal(str) # emitted when user clicks OK
- self.outputText = text
+ def __init__(self, text="", *, title="Edit", placeholder="", python=False):
+ super().__init__(parent=QApplication.activeWindow())
self.setWindowTitle(title)
+ self.setGeometry(0, 0, 600, 400)
layout = QVBoxLayout()
self.setLayout(layout)
- self.textWidget = QTextEdit()
- self.textWidget.setPlainText(text)
- self.textWidget.setTabStopWidth(16)
- self.textWidget.setAcceptRichText(False)
- self.textWidget.setWordWrapMode(QTextOption.NoWrap)
+ if not python:
+ self.textWidget = QTextEdit()
+ self.textWidget.setTabStopWidth(16)
+ self.textWidget.setAcceptRichText(False)
+ self.textWidget.setWordWrapMode(QTextOption.NoWrap)
+ else:
+ self.textWidget = CodeEditorWidget()
+
self.textWidget.setPlaceholderText(placeholder)
+ self.textWidget.setPlainText(text)
okBtn = QPushButton("OK")
- okBtn.clicked.connect(self.okBtnClicked)
+ okBtn.clicked.connect(self.saveAndClose)
layout.addWidget(self.textWidget)
layout.addWidget(okBtn)
- def okBtnClicked(self):
- self.outputText = self.textWidget.toPlainText()
+ centerWindow(self)
+
+ def saveAndClose(self):
+ self.saved.emit(self.textWidget.toPlainText())
self.accept()
class LabelTemplateWidget(TemplateWidget):
def __init__(self, **kwargs):
- super(LabelTemplateWidget, self).__init__(**kwargs)
+ super().__init__(**kwargs)
- self.actualText = ""
+ self._actualText = ""
layout = QVBoxLayout()
self.setLayout(layout)
- layout.setMargin(0)
+ #layout.setContentsMargins(QMargins())
self.label = QLabel()
self.label.setCursor(Qt.PointingHandCursor)
self.label.setWordWrap(True)
- self.label.setToolTip("You can use $ROOT as a path to Rig Builder's root directory, like $ROOT/images/icons")
self.label.mouseDoubleClickEvent = self.labelDoubleClickEvent
layout.addWidget(self.label)
- def setText(self, text):
- self.actualText = text
- self.label.setText(self.actualText.replace("$ROOT", RootPath))
+ def setLabelText(self, text):
+ self._actualText = text
+ self.label.setText(self._actualText.replace("$ROOT", RootPath))
def labelDoubleClickEvent(self, event):
- editTextDialog = EditTextDialog(self.actualText, title="Edit text", placeholder="You can use HTML here...", parent=QApplication.activeWindow())
- editTextDialog.exec_()
-
- if editTextDialog.result():
- self.setText(editTextDialog.outputText)
+ def save(text):
+ self.setLabelText(text)
self.somethingChanged.emit()
+ placeholder = 'Description'
+ editTextDialog = EditTextDialog(self._actualText, title="Edit text", placeholder=placeholder)
+ editTextDialog.saved.connect(save)
+ editTextDialog.show()
+
def getDefaultData(self):
return {"text": "Description", "default": "text"}
def getJsonData(self):
- return {"text": self.actualText, "default": "text"}
+ return {"text": self._actualText, "default": "text"}
def setJsonData(self, value):
- self.setText(value["text"])
+ self.setLabelText(value["text"])
class ButtonTemplateWidget(TemplateWidget):
def __init__(self, **kwargs):
- super(ButtonTemplateWidget, self).__init__(**kwargs)
+ super().__init__(**kwargs)
- self.buttonCommand = "module.attr.someAttr.set(1)"
+ self.buttonCommand = ""
layout = QHBoxLayout()
self.setLayout(layout)
- layout.setMargin(0)
+ layout.setContentsMargins(QMargins())
self.buttonWidget = QPushButton("Press me")
self.buttonWidget.clicked.connect(self.buttonClicked)
@@ -145,10 +155,13 @@ def editLabel(self):
self.somethingChanged.emit()
def editCommand(self):
- editText = EditTextDialog(self.buttonCommand, title="Edit command", placeholder="Your python command...", parent=QApplication.activeWindow())
- editText.exec_()
- self.buttonCommand = editText.outputText
- self.somethingChanged.emit()
+ def save(text):
+ self.buttonCommand = text
+ self.somethingChanged.emit()
+
+ editText = EditTextDialog(self.buttonCommand, title="Edit command", placeholder='chset("/someAttr", 1)', python=True)
+ editText.saved.connect(save)
+ editText.show()
def buttonClicked(self):
if self.buttonCommand:
@@ -176,11 +189,11 @@ def setJsonData(self, data):
class CheckBoxTemplateWidget(TemplateWidget):
def __init__(self, **kwargs):
- super(CheckBoxTemplateWidget, self).__init__(**kwargs)
+ super().__init__(**kwargs)
layout = QVBoxLayout()
self.setLayout(layout)
- layout.setMargin(0)
+ layout.setContentsMargins(QMargins())
self.checkBox = QCheckBox()
self.checkBox.stateChanged.connect(self.somethingChanged)
@@ -194,11 +207,11 @@ def setJsonData(self, value):
class ComboBoxTemplateWidget(TemplateWidget):
def __init__(self, **kwargs):
- super(ComboBoxTemplateWidget, self).__init__(**kwargs)
+ super().__init__(**kwargs)
layout = QVBoxLayout()
self.setLayout(layout)
- layout.setMargin(0)
+ layout.setContentsMargins(QMargins())
self.comboBox = QComboBox()
self.comboBox.currentIndexChanged.connect(self.somethingChanged)
@@ -257,17 +270,14 @@ def setJsonData(self, value):
class LineEditOptionsDialog(QDialog):
def __init__(self, **kwargs):
- super(LineEditOptionsDialog, self).__init__(**kwargs)
+ super().__init__(**kwargs)
self.setWindowTitle("Edit options")
layout = QVBoxLayout()
self.setLayout(layout)
- glayout = QGridLayout()
- glayout.setDefaultPositioning(2, Qt.Horizontal)
- glayout.setColumnStretch(1, 1)
-
+ formLayout = QFormLayout()
self.validatorWidget = QComboBox()
self.validatorWidget.addItems(["Default", "Int", "Double"])
self.validatorWidget.currentIndexChanged.connect(self.validatorIndexChanged)
@@ -283,16 +293,11 @@ def __init__(self, **kwargs):
okBtn.clicked.connect(self.accept)
okBtn.setAutoDefault(False)
- glayout.addWidget(QLabel("Validator"))
- glayout.addWidget(self.validatorWidget)
-
- glayout.addWidget(QLabel("Min"))
- glayout.addWidget(self.minWidget)
+ formLayout.addRow("Validator", self.validatorWidget)
+ formLayout.addRow("Min", self.minWidget)
+ formLayout.addRow("Max", self.maxWidget)
- glayout.addWidget(QLabel("Max"))
- glayout.addWidget(self.maxWidget)
-
- layout.addLayout(glayout)
+ layout.addLayout(formLayout)
layout.addWidget(okBtn)
def validatorIndexChanged(self, idx):
@@ -300,17 +305,20 @@ def validatorIndexChanged(self, idx):
self.maxWidget.setEnabled(idx!=0)
class LineEditTemplateWidget(TemplateWidget):
+ defaultMin = 0
+ defaultMax = 100
+
def __init__(self, **kwargs):
- super(LineEditTemplateWidget, self).__init__(**kwargs)
+ super().__init__(**kwargs)
self.optionsDialog = LineEditOptionsDialog(parent=self)
- self.minValue = ""
- self.maxValue = ""
+ self.minValue = 0
+ self.maxValue = 100
self.validator = 0
layout = QHBoxLayout()
self.setLayout(layout)
- layout.setMargin(0)
+ layout.setContentsMargins(QMargins())
self.textWidget = QLineEdit()
self.textWidget.editingFinished.connect(self.textChanged)
@@ -343,12 +351,12 @@ def textContextMenuEvent(self, event):
menu.popup(event.globalPos())
def optionsClicked(self):
- self.optionsDialog.minWidget.setText(self.minValue)
- self.optionsDialog.maxWidget.setText(self.maxValue)
+ self.optionsDialog.minWidget.setText(str(self.minValue))
+ self.optionsDialog.maxWidget.setText(str(self.maxValue))
self.optionsDialog.validatorWidget.setCurrentIndex(self.validator)
self.optionsDialog.exec_()
- self.minValue = self.optionsDialog.minWidget.text()
- self.maxValue = self.optionsDialog.maxWidget.text()
+ self.minValue = int(self.optionsDialog.minWidget.text() or LineEditTemplateWidget.defaultMin)
+ self.maxValue = int(self.optionsDialog.maxWidget.text() or LineEditTemplateWidget.defaultMax)
self.validator = self.optionsDialog.validatorWidget.currentIndex()
self.setJsonData(self.getJsonData())
@@ -360,58 +368,61 @@ def getJsonData(self):
"validator": self.validator}
def setJsonData(self, data):
- self.textWidget.setText(fromSmartConversion(data["value"]))
-
self.validator = data.get("validator", 0)
- self.minValue = data.get("min")
- self.maxValue = data.get("max")
+ self.minValue = int(data.get("min") or LineEditTemplateWidget.defaultMin)
+ self.maxValue = int(data.get("max") or LineEditTemplateWidget.defaultMax)
if self.validator == 1:
self.textWidget.setValidator(QIntValidator())
elif self.validator == 2:
self.textWidget.setValidator(QDoubleValidator())
- if self.validator and self.minValue and self.maxValue:
+ if self.validator:
self.sliderWidget.show()
-
- self.sliderWidget.setMinimum(int(self.minValue)*100) # slider values are int, so mult by 100
- self.sliderWidget.setMaximum(int(self.maxValue)*100)
+ if self.minValue:
+ self.sliderWidget.setMinimum(self.minValue*100) # slider values are int, so mult by 100
+ if self.maxValue:
+ self.sliderWidget.setMaximum(self.maxValue*100)
if data["value"]:
self.sliderWidget.setValue(float(data["value"])*100)
else:
self.sliderWidget.hide()
+ self.textWidget.setText(fromSmartConversion(data["value"]))
+
class LineEditAndButtonTemplateWidget(TemplateWidget):
def __init__(self, **kwargs):
- super(LineEditAndButtonTemplateWidget, self).__init__(**kwargs)
+ super().__init__(**kwargs)
self.templates = {}
if DCC == "maya":
- self.templates["Get selected"] = "import maya.cmds as cmds\nls = cmds.ls(sl=True)\nif ls: value = ls[0]"
+ self.templates["Get selected"] = {"label": "<", "command":"import maya.cmds as cmds\nls = cmds.ls(sl=True)\nif ls: value = ls[0]"}
- self.templates["Get open file"] = '''from PySide2.QtWidgets import QFileDialog;import os
+ self.templates["Get open file"] = {"label": "...", "command":'''from PySide2.QtWidgets import QFileDialog;import os
path,_ = QFileDialog.getOpenFileName(None, "Open file", os.path.expandvars(value))
-value = path or value'''
+value = path or value'''}
- self.templates["Get save file"] = '''from PySide2.QtWidgets import QFileDialog;import os
+ self.templates["Get save file"] = {"label": "...", "command":'''from PySide2.QtWidgets import QFileDialog;import os
path,_ = QFileDialog.getSaveFileName(None, "Save file", os.path.expandvars(value))
-value = path or value'''
+value = path or value'''}
- self.templates["Get existing directory"] = '''from PySide2.QtWidgets import QFileDialog;import os
+ self.templates["Get existing directory"] = {"label": "...", "command":'''from PySide2.QtWidgets import QFileDialog;import os
path = QFileDialog.getExistingDirectory(None, "Select directory", os.path.expandvars(value))
-value = path or value'''
+value = path or value'''}
+
+ defaultCmd = self.templates["Get selected"]
- self.buttonCommand = self.templates.get("Get selected", "value = 'something'")
+ self.buttonCommand = defaultCmd["command"]
layout = QHBoxLayout()
self.setLayout(layout)
- layout.setMargin(0)
+ layout.setContentsMargins(QMargins())
self.textWidget = QLineEdit()
self.textWidget.editingFinished.connect(self.somethingChanged)
- self.buttonWidget = QPushButton("<")
+ self.buttonWidget = QPushButton(defaultCmd["label"])
self.buttonWidget.clicked.connect(self.buttonClicked)
self.buttonWidget.contextMenuEvent = self.buttonContextMenuEvent
@@ -425,13 +436,14 @@ def buttonContextMenuEvent(self, event):
menu.addAction("Edit command", self.editCommand)
if self.templates:
- def setText(cmd):
- self.buttonCommand = cmd
+ def setCommand(cmd):
+ self.buttonWidget.setText(cmd["label"])
+ self.buttonCommand = cmd["command"]
self.somethingChanged.emit()
templatesMenu = QMenu("Templates", self)
- for k in self.templates:
- templatesMenu.addAction(k, lambda cmd=self.templates[k]:setText(cmd))
+ for k, cmd in self.templates.items():
+ templatesMenu.addAction(k, lambda cmd=cmd:setCommand(cmd))
menu.addMenu(templatesMenu)
menu.popup(event.globalPos())
@@ -443,10 +455,13 @@ def editLabel(self):
self.somethingChanged.emit()
def editCommand(self):
- editText = EditTextDialog(self.buttonCommand, title="Edit command", placeholder="Your python command...", parent=QApplication.activeWindow())
- editText.exec_()
- self.buttonCommand = editText.outputText
- self.somethingChanged.emit()
+ def save(text):
+ self.buttonCommand = text
+ self.somethingChanged.emit()
+
+ editText = EditTextDialog(self.buttonCommand, title="Edit command", placeholder="Your python command...", python=True)
+ editText.saved.connect(save)
+ editText.show()
def buttonClicked(self):
if self.buttonCommand:
@@ -476,11 +491,11 @@ def setJsonData(self, data):
class ListBoxTemplateWidget(TemplateWidget):
def __init__(self, **kwargs):
- super(ListBoxTemplateWidget, self).__init__(**kwargs)
+ super().__init__(**kwargs)
layout = QVBoxLayout()
self.setLayout(layout)
- layout.setMargin(0)
+ layout.setContentsMargins(QMargins())
self.listWidget = QListWidget()
self.listWidget.itemDoubleClicked.connect(self.itemDoubleClicked)
@@ -504,11 +519,11 @@ def listContextMenuEvent(self, event):
menu.popup(event.globalPos())
def resizeWidget(self):
- width = self.listWidget.sizeHintForColumn(0) + 25
+ width = self.listWidget.sizeHintForColumn(0) + 50
height = 0
for i in range(self.listWidget.count()):
height += self.listWidget.sizeHintForRow(i)
- height += 2*self.listWidget.frameWidth() + 25
+ height += 2*self.listWidget.frameWidth() + 50
self.listWidget.setFixedSize(clamp(width, 100, 500), clamp(height, 100, 500))
def editItem(self):
@@ -577,11 +592,13 @@ class RadioButtonTemplateWidget(TemplateWidget):
Columns = [2,3,4,5]
def __init__(self, **kwargs):
- super(RadioButtonTemplateWidget, self).__init__(**kwargs)
+ super().__init__(**kwargs)
+
+ self.numColumns = 3
layout = QGridLayout()
self.setLayout(layout)
- layout.setMargin(0)
+ layout.setContentsMargins(QMargins())
self.buttonsGroupWidget = QButtonGroup()
self.buttonsGroupWidget.buttonClicked.connect(self.buttonClicked)
@@ -615,8 +632,7 @@ def buttonClicked(self, b):
self.somethingChanged.emit()
def clearButtons(self):
- gridLayout = self.layout()
- clearLayout(gridLayout)
+ clearLayout(self.layout())
for b in self.buttonsGroupWidget.buttons():
self.buttonsGroupWidget.removeButton(b)
@@ -631,12 +647,12 @@ def editClicked(self):
self.somethingChanged.emit()
def getDefaultData(self):
- return {"items": ["Helpers", "Run"], "current": 0, "default": "current", "columns":3}
+ return {"items": ["Helpers", "Run"], "current": 0, "default": "current", "columns": self.numColumns}
def getJsonData(self):
return {"items": [b.text() for b in self.buttonsGroupWidget.buttons()],
"current": self.buttonsGroupWidget.checkedId(),
- "columns": self.layout().columnCount(),
+ "columns": self.numColumns,
"default": "current"}
def setJsonData(self, value):
@@ -644,13 +660,13 @@ def setJsonData(self, value):
self.clearButtons()
- columns = value.get("columns", 3)
- gridLayout.setDefaultPositioning(columns, Qt.Horizontal)
+ self.numColumns = value["columns"]
+ gridLayout.setDefaultPositioning(self.numColumns, Qt.Horizontal)
row = 0
column = 0
for i, item in enumerate(value["items"]):
- if i % columns == 0 and i > 0:
+ if i % self.numColumns == 0 and i > 0:
row += 1
column = 0
@@ -666,11 +682,11 @@ def setJsonData(self, value):
class TableTemplateWidget(TemplateWidget):
def __init__(self, **kwargs):
- super(TableTemplateWidget, self).__init__(**kwargs)
+ super().__init__(**kwargs)
layout = QVBoxLayout()
self.setLayout(layout)
- layout.setMargin(0)
+ layout.setContentsMargins(QMargins())
self.tableWidget = QTableWidget()
self.tableWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
@@ -832,51 +848,77 @@ def setJsonData(self, value):
class TextTemplateWidget(TemplateWidget):
def __init__(self, **kwargs):
- super(TextTemplateWidget, self).__init__(**kwargs)
+ super().__init__(**kwargs)
layout = QVBoxLayout()
self.setLayout(layout)
- layout.setMargin(0)
+ layout.setContentsMargins(QMargins())
self.textWidget = QTextEdit()
self.textWidget.textChanged.connect(self.somethingChanged)
+ incSizeBtn = QPushButton("+")
+ incSizeBtn.setFixedSize(25, 25)
+ incSizeBtn.clicked.connect(self.incSize)
+ decSizeBtn = QPushButton("-")
+ decSizeBtn.setFixedSize(25, 25)
+ decSizeBtn.clicked.connect(self.decSize)
+
+ hlayout = QHBoxLayout()
+ hlayout.addWidget(decSizeBtn)
+ hlayout.addWidget(incSizeBtn)
+ hlayout.addStretch()
+
+ layout.addLayout(hlayout)
layout.addWidget(self.textWidget)
+ layout.addStretch()
+
+ def incSize(self):
+ self.textWidget.setFixedHeight(self.textWidget.height() + 50)
+ self.somethingChanged.emit()
+
+ def decSize(self):
+ self.textWidget.setFixedHeight(self.textWidget.height() - 50)
+ self.somethingChanged.emit()
+
+ def getDefaultData(self):
+ return {"text": "", "height": 200, "default": "text"}
def getJsonData(self):
return {"text": self.textWidget.toPlainText().strip(),
+ "height": self.textWidget.height(),
"default": "text"}
def setJsonData(self, data):
self.textWidget.setPlainText(data["text"])
+ self.textWidget.setFixedHeight(data.get("height", self.getDefaultData()["height"]))
class VectorTemplateWidget(TemplateWidget):
def __init__(self, **kwargs):
- super(VectorTemplateWidget, self).__init__(**kwargs)
+ super().__init__(**kwargs)
layout = QHBoxLayout()
self.setLayout(layout)
- layout.setMargin(0)
+ layout.setContentsMargins(QMargins())
- self.xWidget = QLineEdit("0")
+ self.xWidget = QLineEdit()
self.xWidget.setValidator(QDoubleValidator())
- self.xWidget.editingFinished.connect(self.somethingChanged)
-
- self.yWidget = QLineEdit("0")
+ self.xWidget.editingFinished.connect(self.somethingChanged.emit)
+
+ self.yWidget = QLineEdit()
self.yWidget.setValidator(QDoubleValidator())
- self.yWidget.editingFinished.connect(self.somethingChanged)
+ self.yWidget.editingFinished.connect(self.somethingChanged.emit)
- self.zWidget = QLineEdit("0")
+ self.zWidget = QLineEdit()
self.zWidget.setValidator(QDoubleValidator())
- self.zWidget.editingFinished.connect(self.somethingChanged)
+ self.zWidget.editingFinished.connect(self.somethingChanged.emit)
layout.addWidget(self.xWidget)
layout.addWidget(self.yWidget)
layout.addWidget(self.zWidget)
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(self.xWidget.text() or 0), float(self.yWidget.text() or 0), float(self.zWidget.text() or 0)], "default": "value"}
def setJsonData(self, value):
self.xWidget.setText(str(value["value"][0]))
@@ -943,12 +985,12 @@ def evaluateBezierCurveFromX(cvs, x):
def normalizedPoint(p, minX, maxX, minY, maxY):
x = (p[0] - minX) / (maxX - minX)
y = (p[1] - minY) / (maxY - minY)
- return (x, y)
+ return [x, y]
class CurvePointItem(QGraphicsItem):
Size = 10
def __init__(self, **kwargs):
- super(CurvePointItem, self).__init__(**kwargs)
+ super().__init__(**kwargs)
self.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemSendsGeometryChanges)
@@ -969,7 +1011,7 @@ def paint(self, painter, option, widget):
def itemChange(self, change, value):
if not self.scene():
- return super(CurvePointItem, self).itemChange(change, value)
+ return super().itemChange(change, value)
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
if self.fixedX is not None:
@@ -1010,14 +1052,14 @@ def itemChange(self, change, value):
if type(view) == CurveView:
view.somethingChanged.emit()
- return super(CurvePointItem, self).itemChange(change, value)
+ return super().itemChange(change, value)
class CurveScene(QGraphicsScene):
MaxX = 300
MaxY = -100
DrawCurveSamples = 33
def __init__(self, **kwargs):
- super(CurveScene, self).__init__(**kwargs)
+ super().__init__(**kwargs)
self.cvs = []
@@ -1077,7 +1119,7 @@ def mousePressEvent(self, event):
event.accept()
else:
- super(CurveScene,self).mousePressEvent(event)
+ super().mousePressEvent(event)
def calculateCVs(self):
self.cvs = []
@@ -1108,7 +1150,7 @@ def calculateCVs(self):
w = max(w1, w2)*2 - 1 # from 0 to 1, because max(w1,w2) is always >= 0.5
w = w ** 4
tg = QVector2D(items[i+1].pos() - items[i-1].pos()).normalized() * (1-w) + QVector2D(1, 0) * w
-
+
tangents.append(tg)
for i, _ in enumerate(items):
@@ -1192,7 +1234,7 @@ class CurveView(QGraphicsView):
somethingChanged = Signal()
def __init__(self, **kwargs):
- super(CurveView, self).__init__(**kwargs)
+ super().__init__(**kwargs)
self.setRenderHint(QPainter.Antialiasing, True)
self.setRenderHint(QPainter.TextAntialiasing, True)
@@ -1211,10 +1253,10 @@ def resizeEvent(self, event):
class CurveTemplateWidget(TemplateWidget):
def __init__(self, **kwargs):
- super(CurveTemplateWidget, self).__init__(**kwargs)
+ super().__init__(**kwargs)
layout = QVBoxLayout()
- layout.setMargin(0)
+ layout.setContentsMargins(QMargins())
self.setLayout(layout)
self.curveView = CurveView()
@@ -1222,7 +1264,9 @@ def __init__(self, **kwargs):
layout.addWidget(self.curveView)
def getDefaultData(self):
- return {'default': 'cvs', 'cvs': [(0.0, 1.0), (0.13973423457023273, 0.722154453101879), (0.3352803473835302, -0.0019584480764515554), (0.5029205210752953, -0.0), (0.6686136807168636, 0.0019357021806590401), (0.8623842449806401, 0.7231513901834298), (1.0, 1.0)]}
+ return {'default': 'cvs', 'cvs': [[0.0, 1.0], [0.13973423457023273, 0.722154453101879],
+ [0.3352803473835302, -0.0019584480764515554], [0.5029205210752953, -0.0],
+ [0.6686136807168636, 0.0019357021806590401], [0.8623842449806401, 0.7231513901834298], [1.0, 1.0]]}
def getJsonData(self):
return {"cvs": self.curveView.scene().cvs, "default": "cvs"}
@@ -1240,11 +1284,81 @@ def setJsonData(self, value):
if i == 0 or i == len(value["cvs"]) - 1:
item.fixedX = item.pos().x()
+class JsonTemplateWidget(TemplateWidget):
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+
+ layout = QVBoxLayout()
+ self.setLayout(layout)
+ layout.setContentsMargins(QMargins())
+
+ self.jsonWidget = JsonWidget()
+ self.jsonWidget.itemChanged.connect(lambda _,__:self.somethingChanged.emit())
+ self.jsonWidget.itemMoved.connect(lambda _:self.somethingChanged.emit())
+ self.jsonWidget.itemAdded.connect(lambda _:self.somethingChanged.emit())
+ self.jsonWidget.itemRemoved.connect(lambda _:self.somethingChanged.emit())
+ self.jsonWidget.dataLoaded.connect(self.somethingChanged.emit)
+ self.jsonWidget.cleared.connect(self.somethingChanged.emit)
+ self.jsonWidget.readOnlyChanged.connect(lambda _: self.somethingChanged.emit())
+ self.jsonWidget.rootChanged.connect(lambda _: self.updateInfoLabel())
+ self.jsonWidget.itemClicked.connect(lambda _: self.updateInfoLabel())
+
+ incSizeBtn = QPushButton("+")
+ incSizeBtn.setFixedSize(25, 25)
+ incSizeBtn.clicked.connect(self.incSize)
+ decSizeBtn = QPushButton("-")
+ decSizeBtn.setFixedSize(25, 25)
+ decSizeBtn.clicked.connect(self.decSize)
+
+ self.infoLabel = QLabel()
+
+ hlayout = QHBoxLayout()
+ hlayout.addWidget(decSizeBtn)
+ hlayout.addWidget(incSizeBtn)
+ hlayout.addStretch()
+ hlayout.addWidget(self.infoLabel)
+
+ layout.addLayout(hlayout)
+ layout.addWidget(self.jsonWidget)
+ layout.addStretch()
+
+ def updateInfoLabel(self):
+ rootIndex = self.jsonWidget.rootIndex()
+ root = self.jsonWidget.itemFromIndex(rootIndex).getPath() if rootIndex != QModelIndex() else ""
+
+ item = self.jsonWidget.selectedItem()
+ path = item.getPath() if item else ""
+ self.infoLabel.setText("Root:{} Path:{}".format(root, path.replace(root,"")))
+
+ def incSize(self):
+ self.jsonWidget.setFixedHeight(self.jsonWidget.height() + 50)
+ self.somethingChanged.emit()
+
+ def decSize(self):
+ self.jsonWidget.setFixedHeight(self.jsonWidget.height() - 50)
+ self.somethingChanged.emit()
+
+ def getDefaultData(self):
+ return {"data": [{"a": 1, "b": 2}], "height":200, "readonly": False, "default": "data"}
+
+ def getJsonData(self):
+ return {"data": self.jsonWidget.toJsonList(),
+ "height":self.jsonWidget.height(),
+ "readonly": self.jsonWidget.isReadOnly(),
+ "default": "data"}
+
+ def setJsonData(self, value):
+ self.jsonWidget.setFixedHeight(value["height"])
+ self.jsonWidget.clear()
+ self.jsonWidget.fromJsonList(value["data"])
+ self.jsonWidget.setReadOnly(value["readonly"])
+
TemplateWidgets = {
"button": ButtonTemplateWidget,
"checkBox": CheckBoxTemplateWidget,
"comboBox": ComboBoxTemplateWidget,
"curve": CurveTemplateWidget,
+ "json": JsonTemplateWidget,
"label": LabelTemplateWidget,
"lineEdit": LineEditTemplateWidget,
"lineEditAndButton": LineEditAndButtonTemplateWidget,