From 5018a5c71f9e83bcff59ab1a712f09d66e9f562b Mon Sep 17 00:00:00 2001 From: Joji Date: Sat, 30 Sep 2023 10:52:35 -0700 Subject: [PATCH 01/20] The user interface now remembers its previous size, and won't recreate another window --- .../scripts/mgear/rigbits/rbf_manager_ui.py | 50 ++++++++++++++----- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/release/scripts/mgear/rigbits/rbf_manager_ui.py b/release/scripts/mgear/rigbits/rbf_manager_ui.py index 9b5ddde7..889d5a75 100644 --- a/release/scripts/mgear/rigbits/rbf_manager_ui.py +++ b/release/scripts/mgear/rigbits/rbf_manager_ui.py @@ -91,11 +91,13 @@ # ============================================================================= # Constants # ============================================================================= -__version__ = "1.0.2" +__version__ = "1.0.3" _mgear_version = mgear.getVersion() TOOL_NAME = "RBF Manager" TOOL_TITLE = "{} v{} | mGear {}".format(TOOL_NAME, __version__, _mgear_version) +UI_NAME = "RBFManagerUI" +WORK_SPACE_NAME = UI_NAME + "WorkspaceControl" DRIVEN_SUFFIX = rbf_node.DRIVEN_SUFFIX CTL_SUFFIX = rbf_node.CTL_SUFFIX @@ -277,23 +279,33 @@ def VLine(): def show(dockable=True, newSceneCallBack=True, *args): - """To launch the ui and not get the same instance + """To launch the UI and ensure any previously opened instance is closed. Returns: DistributeUI: instance Args: *args: Description + :param newSceneCallBack: + :param dockable: """ - global RBF_UI - if 'RBF_UI' in globals(): - try: - RBF_UI.close() - except TypeError: - pass - RBF_UI = RBFManagerUI(parent=pyqt.maya_main_window(), - newSceneCallBack=newSceneCallBack) - RBF_UI.show(dockable=True) + global RBF_UI # Ensure we have access to the global variable + + # Attempt to close any existing UI with the given name + if mc.workspaceControl(WORK_SPACE_NAME, exists=True): + mc.deleteUI(WORK_SPACE_NAME) + + # Create the UI + RBF_UI = RBFManagerUI(newSceneCallBack=newSceneCallBack) + + # Check if we've saved a size previously and set it + if mc.optionVar(exists='RBF_UI_width') and mc.optionVar(exists='RBF_UI_height'): + saved_width = mc.optionVar(query='RBF_UI_width') + saved_height = mc.optionVar(query='RBF_UI_height') + RBF_UI.resize(saved_width, saved_height) + + # Show the UI. + RBF_UI.show(dockable=dockable) return RBF_UI @@ -474,11 +486,12 @@ class RBFManagerUI(MayaQWidgetDockableMixin, QtWidgets.QMainWindow): mousePosition = QtCore.Signal(int, int) - def __init__(self, parent=None, hideMenuBar=False, newSceneCallBack=True): + def __init__(self, parent=pyqt.maya_main_window(), hideMenuBar=False, newSceneCallBack=True): super(RBFManagerUI, self).__init__(parent=parent) # UI info ------------------------------------------------------------- self.callBackID = None self.setWindowTitle(TOOL_TITLE) + self.setObjectName(UI_NAME) self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) self.genericWidgetHight = 24 # class info ---------------------------------------------------------- @@ -493,9 +506,22 @@ def __init__(self, parent=None, hideMenuBar=False, newSceneCallBack=True): self.connectSignals() # added because the dockableMixin makes the ui appear small self.adjustSize() + self.resize(800, 650) if newSceneCallBack: self.newSceneCallBack() + def closeEvent(self, event): + """Overridden close event to save the size of the UI.""" + width = self.width() + height = self.height() + + # Save the size to Maya's optionVars + mc.optionVar(intValue=('RBF_UI_width', width)) + mc.optionVar(intValue=('RBF_UI_height', height)) + + # Call the parent class's closeEvent + super(RBFManagerUI, self).closeEvent(event) + def callBackFunc(self, *args): """super safe function for trying to refresh the UI, should anything fail. From f5c0df891daef208039d06307027fa41875b1171 Mon Sep 17 00:00:00 2001 From: Joji Date: Sat, 30 Sep 2023 10:55:36 -0700 Subject: [PATCH 02/20] Set 'Display Keyable' as the default, or use 'Display Non-Keyable' if no keyable attributes exist --- .../scripts/mgear/rigbits/rbf_manager_ui.py | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/release/scripts/mgear/rigbits/rbf_manager_ui.py b/release/scripts/mgear/rigbits/rbf_manager_ui.py index 889d5a75..ab79138e 100644 --- a/release/scripts/mgear/rigbits/rbf_manager_ui.py +++ b/release/scripts/mgear/rigbits/rbf_manager_ui.py @@ -92,6 +92,7 @@ # Constants # ============================================================================= __version__ = "1.0.3" +__version__ = "1.0.4" _mgear_version = mgear.getVersion() TOOL_NAME = "RBF Manager" @@ -122,6 +123,7 @@ def testFunctions(*args): def getPlugAttrs(nodes, attrType="all"): +def getPlugAttrs(nodes, attrType="keyable"): """Get a list of attributes to display to the user Args: @@ -699,9 +701,11 @@ def addRBFToSetup(self): else: attrType = "all" - drivenNode_name = drivenNode + drivenType = mc.nodeType(drivenNode) if drivenType in ["transform", "joint"]: drivenNode_name = rbf_node.get_driven_group_name(drivenNode) + else: + drivenNode_name = drivenNode # check if there is an existing rbf node attached if mc.objExists(drivenNode_name): @@ -722,7 +726,6 @@ def addRBFToSetup(self): setupField=False) if not drivenAttrs: return - parentNode = False if drivenType in ["transform", "joint"]: parentNode = True @@ -781,6 +784,22 @@ def refreshAllTables(self): if weightInfo and rbfNode: self.populateDriverInfo(rbfNode, weightInfo) + @staticmethod + def determineAttrType(node): + nodeType = mc.nodeType(node) + if nodeType in ["transform", "joint"]: + keyAttrs = mc.listAttr(node, keyable=True) or [] + requiredAttrs = [ + "{}{}".format(attrType, xyz) + for xyz in "XYZ" + for attrType in ["translate", "rotate", "scale"] + ] + + if not any(attr in keyAttrs for attr in requiredAttrs): + return "cb" + return "keyable" + return "all" + def deletePose(self): """delete a pose from the UI and all the RBFNodes in the setup. @@ -989,7 +1008,8 @@ def updateAttributeDisplay(self, attrListWidget, driverNames, highlight=[], - attrType="all"): + attrType="keyable", + force=False): """update the provided listwidget with the attrs collected from the list of nodes provided @@ -1007,9 +1027,13 @@ def updateAttributeDisplay(self, return elif type(driverNames) != list: driverNames = [driverNames] + + if not force: + attrType = self.determineAttrType(driverNames[0]) + nodeAttrsToDisplay = getPlugAttrs(driverNames, attrType=attrType) attrListWidget.clear() - attrListWidget.addItems(sorted(nodeAttrsToDisplay)) + attrListWidget.addItems(nodeAttrsToDisplay) if highlight: self.highlightListEntries(attrListWidget, highlight) @@ -1342,7 +1366,8 @@ def attrListMenu(self, menu_item_01.triggered.connect(partial(self.updateAttributeDisplay, attributeListWidget, nodeToQuery, - attrType="keyable")) + attrType="keyable", + force=True)) menu2Label = "Display ChannelBox (Non Keyable)" menu_item_02 = self.attrMenu.addAction(menu2Label) menu2tip = "Show attributes in ChannelBox that are not keyable." @@ -1350,13 +1375,15 @@ def attrListMenu(self, menu_item_02.triggered.connect(partial(self.updateAttributeDisplay, attributeListWidget, nodeToQuery, - attrType="cb")) + attrType="cb", + force=True)) menu_item_03 = self.attrMenu.addAction("Display All") menu_item_03.setToolTip("GIVE ME ALL!") menu_item_03.triggered.connect(partial(self.updateAttributeDisplay, attributeListWidget, nodeToQuery, - attrType="all")) + attrType="all", + force=True)) self.attrMenu.move(parentPosition + QPos) self.attrMenu.show() From c97987ae6f38b189737604d87cf45a5f2b75028c Mon Sep 17 00:00:00 2001 From: Joji Date: Tue, 3 Oct 2023 11:18:46 -0700 Subject: [PATCH 03/20] Changed button and selectionNode styles --- .../scripts/mgear/rigbits/rbf_manager_ui.py | 53 ++++++++++++++----- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/release/scripts/mgear/rigbits/rbf_manager_ui.py b/release/scripts/mgear/rigbits/rbf_manager_ui.py index ab79138e..6ae4156b 100644 --- a/release/scripts/mgear/rigbits/rbf_manager_ui.py +++ b/release/scripts/mgear/rigbits/rbf_manager_ui.py @@ -91,8 +91,7 @@ # ============================================================================= # Constants # ============================================================================= -__version__ = "1.0.3" -__version__ = "1.0.4" +__version__ = "1.0.5" _mgear_version = mgear.getVersion() TOOL_NAME = "RBF Manager" @@ -1817,7 +1816,7 @@ def createSetupSelectorWidget(self): setRBFLayout = QtWidgets.QHBoxLayout() rbfLabel = QtWidgets.QLabel("Select RBF Setup:") rbf_cbox = QtWidgets.QComboBox() - rbf_refreshButton = QtWidgets.QPushButton("Refresh") + rbf_refreshButton = self.createCustomButton("Refresh", (60, 25), "Refresh the UI") rbf_cbox.setFixedHeight(self.genericWidgetHight) rbf_refreshButton.setMaximumWidth(80) rbf_refreshButton.setFixedHeight(self.genericWidgetHight - 1) @@ -1826,15 +1825,45 @@ def createSetupSelectorWidget(self): setRBFLayout.addWidget(rbf_refreshButton) return setRBFLayout, rbf_cbox, rbf_refreshButton + @staticmethod + def createCustomButton(label, size=(35, 25), tooltip=""): + stylesheet = ( + "QPushButton {background-color: #5D5D5D; border-radius: 4px;}" + "QPushButton:pressed { background-color: #00A6F3;}" + "QPushButton:hover:!pressed { background-color: #707070;}" + ) + button = QtWidgets.QPushButton(label) + button.setMinimumSize(QtCore.QSize(*size)) + button.setStyleSheet(stylesheet) + button.setToolTip(tooltip) + return button + + @staticmethod + def createSetupSelector2Widget(): + rbfVLayout = QtWidgets.QVBoxLayout() + rbfListWidget = QtWidgets.QListWidget() + rbfVLayout.addWidget(rbfListWidget) + return rbfVLayout, rbfListWidget + def selectNodeWidget(self, label, buttonLabel="Select"): """create a lout with label, lineEdit, QPushbutton for user input """ + stylesheet = ( + "QLineEdit { background-color: #404040;" + "border-radius: 4px;" + "border-color: #505050;" + "border-style: solid;" + "border-width: 1.4px;}" + ) + nodeLayout = QtWidgets.QHBoxLayout() nodeLabel = QtWidgets.QLabel(label) nodeLabel.setFixedWidth(40) nodeLineEdit = ClickableLineEdit() + nodeLineEdit.setStyleSheet(stylesheet) nodeLineEdit.setReadOnly(True) - nodeSelectButton = QtWidgets.QPushButton(buttonLabel) + nodeSelectButton = self.createCustomButton(buttonLabel) + nodeSelectButton.setFixedWidth(40) nodeLineEdit.setFixedHeight(self.genericWidgetHight) nodeSelectButton.setFixedHeight(self.genericWidgetHight) nodeLayout.addWidget(nodeLabel) @@ -1895,6 +1924,8 @@ def createDriverAttributeWidget(self): buttonLabel="Set") controlLineEdit.setToolTip("The node driving the setup. (Click me!)") # -------------------------------------------------------------------- + allButton = self.createCustomButton("All", (20, 53), "") + (attributeLayout, attributeListWidget) = self.labelListWidget(label="Select Attributes", horizontal=False) @@ -1992,20 +2023,16 @@ def createOptionsButtonsWidget(self): list: [QPushButtons] """ optionsLayout = QtWidgets.QHBoxLayout() - addPoseButton = QtWidgets.QPushButton("Add Pose") + optionsLayout.setSpacing(5) addTip = "After positioning all controls in the setup, add new pose." addTip = addTip + "\nEnsure the driver node has a unique position." - addPoseButton.setToolTip(addTip) - addPoseButton.setFixedHeight(self.genericWidgetHight) - EditPoseButton = QtWidgets.QPushButton("Edit Pose") + addPoseButton = self.createCustomButton("Add Pose", (80, 28), addTip) + EditPoseButton = self.createCustomButton("Update Pose", (80, 28), "") EditPoseButton.setToolTip("Recall pose, adjust controls and Edit.") - EditPoseButton.setFixedHeight(self.genericWidgetHight) - EditPoseValuesButton = QtWidgets.QPushButton("Edit Pose Values") + EditPoseValuesButton = self.createCustomButton("Update Pose Values", (80, 28), "") EditPoseValuesButton.setToolTip("Set pose based on values in table") - EditPoseValuesButton.setFixedHeight(self.genericWidgetHight) - deletePoseButton = QtWidgets.QPushButton("Delete Pose") + deletePoseButton = self.createCustomButton("Delete Pose", (80, 28), "") deletePoseButton.setToolTip("Recall pose, then Delete") - deletePoseButton.setFixedHeight(self.genericWidgetHight) optionsLayout.addWidget(addPoseButton) optionsLayout.addWidget(EditPoseButton) optionsLayout.addWidget(EditPoseValuesButton) From 4b3e9f857fb4109a0e510872257e5ee4f855791c Mon Sep 17 00:00:00 2001 From: Joji Date: Tue, 3 Oct 2023 11:27:13 -0700 Subject: [PATCH 04/20] New layout design --- .../scripts/mgear/rigbits/rbf_manager_ui.py | 542 ++++++++---------- 1 file changed, 247 insertions(+), 295 deletions(-) diff --git a/release/scripts/mgear/rigbits/rbf_manager_ui.py b/release/scripts/mgear/rigbits/rbf_manager_ui.py index 6ae4156b..214431df 100644 --- a/release/scripts/mgear/rigbits/rbf_manager_ui.py +++ b/release/scripts/mgear/rigbits/rbf_manager_ui.py @@ -91,7 +91,7 @@ # ============================================================================= # Constants # ============================================================================= -__version__ = "1.0.5" +__version__ = "1.0.6" _mgear_version = mgear.getVersion() TOOL_NAME = "RBF Manager" @@ -376,97 +376,6 @@ def tabSizeHint(self, index): return QtCore.QSize(width, 25) -class RBFSetupInput(QtWidgets.QDialog): - - """Allow the user to select which attrs will drive the rbf nodes in a setup - - Attributes: - drivenListWidget (QListWidget): widget to display attrs to drive setup - okButton (QPushButton): BUTTON - result (list): of selected attrs from listWidget - setupField (bool)): Should the setup lineEdit widget be displayed - setupLineEdit (QLineEdit): name selected by user - """ - - def __init__(self, listValues, setupField=True, parent=None): - """setup the UI widgets - - Args: - listValues (list): attrs to be displayed on the list - setupField (bool, optional): should the setup line edit be shown - parent (QWidget, optional): widget to parent this to - """ - super(RBFSetupInput, self).__init__(parent=parent) - self.setWindowTitle(TOOL_TITLE) - mainLayout = QtWidgets.QVBoxLayout() - self.setLayout(mainLayout) - self.setupField = setupField - self.result = [] - # -------------------------------------------------------------------- - setupLayout = QtWidgets.QHBoxLayout() - setupLabel = QtWidgets.QLabel("Specify Setup Name") - self.setupLineEdit = QtWidgets.QLineEdit() - self.setupLineEdit.setPlaceholderText("_ // skirt_L0") - setupLayout.addWidget(setupLabel) - setupLayout.addWidget(self.setupLineEdit) - if setupField: - mainLayout.addLayout(setupLayout) - # -------------------------------------------------------------------- - drivenLayout = QtWidgets.QVBoxLayout() - drivenLabel = QtWidgets.QLabel("Select Driven Attributes") - self.drivenListWidget = QtWidgets.QListWidget() - self.drivenListWidget.setToolTip("Right Click for sorting!") - selType = QtWidgets.QAbstractItemView.ExtendedSelection - self.drivenListWidget.setSelectionMode(selType) - self.drivenListWidget.addItems(listValues) - self.drivenListWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - drivenLayout.addWidget(drivenLabel) - drivenLayout.addWidget(self.drivenListWidget) - mainLayout.addLayout(drivenLayout) - # -------------------------------------------------------------------- - # buttonLayout = QtWidgets.QHBoxLayout() - self.okButton = QtWidgets.QPushButton("Ok") - self.okButton.clicked.connect(self.onOK) - mainLayout.addWidget(self.okButton) - - def onOK(self): - """collect information from the displayed widgets, userinput, return - - Returns: - list: of user input provided from user - """ - setupName = self.setupLineEdit.text() - if setupName == "" and self.setupField: - genericWarning(self, "Enter Setup Name") - return - selectedAttrs = self.drivenListWidget.selectedItems() - if not selectedAttrs: - genericWarning(self, "Select at least one attribute") - return - driverAttrs = [item.text().split(".")[1] for item in selectedAttrs] - self.result.append(setupName) - self.result.append(driverAttrs) - self.accept() - return self.result - - def getValue(self): - """convenience to get result - - Returns: - TYPE: Description - """ - return self.result - - def exec_(self): - """Convenience - - Returns: - list: [str, [of selected attrs]] - """ - super(RBFSetupInput, self).exec_() - return self.result - - class RBFManagerUI(MayaQWidgetDockableMixin, QtWidgets.QMainWindow): """A manager for creating, mirroring, importing/exporting poses created @@ -500,6 +409,8 @@ def __init__(self, parent=pyqt.maya_main_window(), hideMenuBar=False, newSceneCa self.zeroedDefaults = True self.currentRBFSetupNodes = [] self.allSetupsInfo = None + self.drivenWidget = [] + self.setMenuBar(self.createMenuBar(hideMenuBar=hideMenuBar)) self.setCentralWidget(self.createCentralWidget()) self.centralWidget().setMouseTracking(True) @@ -581,33 +492,6 @@ def getDrivenNodesFromSetup(self): drivenNodes.extend(rbfNode.getDrivenNode) return drivenNodes - def getUserSetupInfo(self, drivenNode, drivenAttrs, setupField=True): - """prompt the user for information needed to create setup or add - rbf node to existing setup - - Args: - drivenAttrs (list): of attrs to display to user to select from - setupField (bool, optional): should the user be asked to input - a name for setup - - Returns: - list: list of selected attrs, name specified - """ - userInputWdgt = RBFSetupInput(drivenAttrs, - setupField=setupField, - parent=self) - partialObj = partial(self.attrListMenu, - userInputWdgt.drivenListWidget, - "", - nodeToQuery=drivenNode) - customMenu = userInputWdgt.drivenListWidget.customContextMenuRequested - customMenu.connect(partialObj) - results = userInputWdgt.exec_() - if results: - return results[0], results[1] - else: - return None, None - def __deleteSetup(self): decision = promptAcceptance(self, "Delete current Setup?", @@ -677,28 +561,11 @@ def addRBFToSetup(self): Returns: TYPE: Description """ - # TODO cut this function down to size - driverNode = self.driverLineEdit.text() - driverControl = self.controlLineEdit.text() - # take every opportunity to return to avoid unneeded processes - if driverNode == "": - return - selectedAttrItems = self.driver_attributes_widget.selectedItems() - if not selectedAttrItems: - return - driverAttrs = [item.text().split(".")[1] for item in selectedAttrItems] - drivenNode = mc.ls(sl=True) - # This does prevents a driver to be its own driven - if not drivenNode or drivenNode[0] == driverNode: - genericWarning(self, "Select Node to be driven!") - return - drivenNode = drivenNode[0] - drivenType = mc.nodeType(drivenNode) - # smart display all when needed - if drivenType in ["transform", "joint"]: - attrType = "keyable" - else: - attrType = "all" + result = self.preValidationCheck() + + driverControl = result["driverControl"] + driverNode, drivenNode = result["driverNode"], result["drivenNode"] + driverAttrs, drivenAttrs = result["driverAttrs"], result["drivenAttrs"] drivenType = mc.nodeType(drivenNode) if drivenType in ["transform", "joint"]: @@ -713,57 +580,81 @@ def addRBFToSetup(self): genericWarning(self, msg) return - availableAttrs = getPlugAttrs([drivenNode], attrType=attrType) setupName, rbfType = self.getSelectedSetup() - # if a setup has already been named or starting new - if setupName is None: - setupName, drivenAttrs = self.getUserSetupInfo(drivenNode, - availableAttrs) - else: - tmpName, drivenAttrs = self.getUserSetupInfo(drivenNode, - availableAttrs, - setupField=False) - if not drivenAttrs: - return + parentNode = False if drivenType in ["transform", "joint"]: parentNode = True drivenNode = rbf_node.addDrivenGroup(drivenNode) - # create RBFNode instance, apply settings + + # Create RBFNode instance, apply settings + if not setupName: + setupName = "{}_WD".format(driverNode) rbfNode = sortRBF(drivenNode, rbfType=rbfType) rbfNode.setSetupName(setupName) rbfNode.setDriverControlAttr(driverControl) rbfNode.setDriverNode(driverNode, driverAttrs) - defaultVals = rbfNode.setDrivenNode(drivenNode, - drivenAttrs, - parent=parentNode) - # Check if there any preexisting nodes in setup, if so copy pose index + defaultVals = rbfNode.setDrivenNode(drivenNode, drivenAttrs, parent=parentNode) + + # Check if there are any preexisting nodes in setup, if so copy pose index if self.currentRBFSetupNodes: currentRbfs = self.currentRBFSetupNodes[0] - print("Syncing poses indices from {} >> {}".format(currentRbfs, - rbfNode)) + print("Syncing poses indices from {} >> {}".format(currentRbfs, rbfNode)) rbfNode.syncPoseIndices(self.currentRBFSetupNodes[0]) else: if self.zeroedDefaults: rbfNode.applyDefaultPose() - else: - poseInputs = rbf_node.getMultipleAttrs(driverNode, driverAttrs) + rbfNode.addPose(poseInput=poseInputs, poseValue=defaultVals[1::2]) - rbfNode.addPose(poseInput=poseInputs, - poseValue=defaultVals[1::2]) self.populateDriverInfo(rbfNode, rbfNode.getNodeInfo()) - # add newly created RBFNode to list of current + self.populateDrivenInfo(rbfNode, rbfNode.getNodeInfo()) + + # Add newly created RBFNode to list of current self.currentRBFSetupNodes.append(rbfNode) - # get info to populate the UI with it - weightInfo = rbfNode.getNodeInfo() - tabDrivenWidget = self.addNewTab(rbfNode) - self.populateDrivenWidgetInfo(tabDrivenWidget, weightInfo, rbfNode) self.refreshRbfSetupList(setToSelection=setupName) self.lockDriverWidgets() - if driverControl: - mc.select(driverControl) + mc.select(driverControl) + + def preValidationCheck(self): + # Fetch data from UI fields + driverNode = self.driverLineEdit.text() + drivenNode = self.drivenLineEdit.text() + driverControl = self.controlLineEdit.text() + driverSelectedAttrItems = self.driverAttributesWidget.selectedItems() + drivenSelectedAttrItems = self.drivenAttributesWidget.selectedItems() + + # Create a default return dictionary with None values + result = { + "driverNode": None, + "drivenNode": None, + "driverControl": None, + "driverAttrs": None, + "drivenAttrs": None + } + + # Ensure driverNode and drivenNode are provided + if not driverNode or not drivenNode: + return result + + # Ensure attributes are selected in the widgets + if not driverSelectedAttrItems or not drivenSelectedAttrItems: + return result + + # Check if the driven node is the same as the driver node + if drivenNode == driverNode: + genericWarning(self, "Select Node to be driven!") + return result + + # Update the result dictionary with the fetched values + result["driverNode"] = driverNode + result["drivenNode"] = drivenNode + result["driverControl"] = driverControl + result["driverAttrs"] = [item.text().split(".")[1] for item in driverSelectedAttrItems] + result["drivenAttrs"] = [item.text().split(".")[1] for item in drivenSelectedAttrItems] + + return result def refreshAllTables(self): """Convenience function to refresh all the tables on all the tabs @@ -975,6 +866,16 @@ def setNodeToField(self, lineEdit, multi=False): mc.select(cl=True) return controlNameData + def setDriverControlLineEdit(self): + selected = mc.ls(sl=True) + if len(selected) == 2: + self.controlLineEdit.setText(selected[0]) + self.driverLineEdit.setText(selected[1]) + elif len(selected) == 1: + self.controlLineEdit.setText(selected[0]) + self.driverLineEdit.setText(selected[0]) + mc.select(cl=True) + def highlightListEntries(self, listWidget, toHighlight): """set the items in a listWidget to be highlighted if they are in list @@ -1068,13 +969,7 @@ def __deleteAssociatedWidgets(self, widget, attrName="associated"): else: setattr(widget, attrName, []) - def syncDriverTableCells(self, - attrEdit, - rbfAttrPlug, - poseIndex, - valueIndex, - attributeName, - *args): + def syncDriverTableCells(self, attrEdit, rbfAttrPlug): """When you edit the driver table, it will update all the sibling rbf nodes in the setup. @@ -1101,44 +996,40 @@ def setDriverTable(self, rbfNode, weightInfo): n/a: n/a """ poses = weightInfo["poses"] + # ensure deletion of associated widgets with this parent widget self.__deleteAssociatedWidgetsMaya(self.driverPoseTableWidget) self.__deleteAssociatedWidgets(self.driverPoseTableWidget) self.driverPoseTableWidget.clear() + + # Configure columns and headers columnLen = len(weightInfo["driverAttrs"]) - self.driverPoseTableWidget.setColumnCount(columnLen) headerNames = weightInfo["driverAttrs"] + self.driverPoseTableWidget.setColumnCount(columnLen) self.driverPoseTableWidget.setHorizontalHeaderLabels(headerNames) + + # Configure rows poseInputLen = len(poses["poseInput"]) self.driverPoseTableWidget.setRowCount(poseInputLen) if poseInputLen == 0: return - verticalLabels = ["Pose {}".format(index) for index - in range(poseInputLen)] + + verticalLabels = ["Pose {}".format(index) for index in range(poseInputLen)] self.driverPoseTableWidget.setVerticalHeaderLabels(verticalLabels) - tmpWidgets = [] - mayaUiItems = [] + + # Populate table cells + tmpWidgets, mayaUiItems = [], [] for rowIndex, poseInput in enumerate(poses["poseInput"]): for columnIndex, pValue in enumerate(poseInput): - # TODO, this is where we get the attrControlGroup - rbfAttrPlug = "{}.poses[{}].poseInput[{}]".format(rbfNode, - rowIndex, - columnIndex) - - attrEdit, mAttrFeild = getControlAttrWidget(rbfAttrPlug, - label="") - func = partial(self.syncDriverTableCells, - attrEdit, - rbfAttrPlug, - rowIndex, - columnIndex, - headerNames[columnIndex]) - self.driverPoseTableWidget.setCellWidget(rowIndex, - columnIndex, - attrEdit) + rbfAttrPlug = "{}.poses[{}].poseInput[{}]".format(rbfNode, rowIndex, columnIndex) + attrEdit, mAttrField = getControlAttrWidget(rbfAttrPlug, label="") + + self.driverPoseTableWidget.setCellWidget(rowIndex, columnIndex, attrEdit) + func = partial(self.syncDriverTableCells, attrEdit, rbfAttrPlug) attrEdit.returnPressed.connect(func) + tmpWidgets.append(attrEdit) - mayaUiItems.append(mAttrFeild) + mayaUiItems.append(mAttrField) setattr(self.driverPoseTableWidget, "associated", tmpWidgets) setattr(self.driverPoseTableWidget, "associatedMaya", mayaUiItems) @@ -1149,10 +1040,13 @@ def lockDriverWidgets(self, lock=True): lock (bool, optional): should it be locked """ self.setDriverButton.blockSignals(lock) + self.setDrivenButton.blockSignals(lock) if lock: - self.driver_attributes_widget.setEnabled(False) + self.driverAttributesWidget.setEnabled(False) + self.drivenAttributesWidget.setEnabled(False) else: - self.driver_attributes_widget.setEnabled(True) + self.driverAttributesWidget.setEnabled(True) + self.drivenAttributesWidget.setEnabled(True) def populateDriverInfo(self, rbfNode, weightInfo): """populate the driver widget, driver, control, driving attrs @@ -1161,19 +1055,41 @@ def populateDriverInfo(self, rbfNode, weightInfo): rbfNode (RBFNode): node for query weightInfo (dict): to pull information from, since we have it """ - driverNode = weightInfo["driverNode"] - if driverNode: - driverNode = driverNode[0] - self.driverLineEdit.setText(driverNode) - driverControl = weightInfo["driverControl"] - # populate control here + driverNode = weightInfo.get("driverNode", [None])[0] + driverControl = weightInfo.get("driverControl", "") + driverAttrs = weightInfo.get("driverAttrs", []) + + self.driverLineEdit.setText(driverNode or "") self.controlLineEdit.setText(driverControl) - self.setAttributeDisplay(self.driver_attributes_widget, - driverNode, - weightInfo["driverAttrs"]) + self.setAttributeDisplay(self.driverAttributesWidget, driverNode, driverAttrs) self.setDriverTable(rbfNode, weightInfo) - def _associateRBFnodeAndWidget(self, tabDrivenWidget, rbfNode): + def populateDrivenInfo(self, rbfNode, weightInfo): + """populate the driver widget, driver, control, driving attrs + + Args: + rbfNode (RBFNode): node for query + weightInfo (dict): to pull information from, since we have it + """ + # Initialize Driven Widget + drivenWidget = self.createAndTagDrivenWidget() + self._associateRBFnodeAndWidget(drivenWidget, rbfNode) + + # Populate Driven Widget Info + self.populateDrivenWidgetInfo(drivenWidget, weightInfo, rbfNode) + + # Set Driven Node and Attributes + drivenNode = weightInfo.get("drivenNode", [None])[0] + self.drivenLineEdit.setText(drivenNode or "") + self.setAttributeDisplay(self.drivenAttributesWidget, drivenNode or "", weightInfo["drivenAttrs"]) + + # Add the driven widget to the tab widget. + self.rbfTabWidget.addTab(drivenWidget, rbfNode.name) + self.rbfTabWidget.setProperty("drivenNode", drivenNode) + self.setDrivenTable(drivenWidget, rbfNode, weightInfo) + + @staticmethod + def _associateRBFnodeAndWidget(tabDrivenWidget, rbfNode): """associates the RBFNode with a widget for convenience when adding, deleting, editing @@ -1183,43 +1099,27 @@ def _associateRBFnodeAndWidget(self, tabDrivenWidget, rbfNode): """ setattr(tabDrivenWidget, "rbfNode", rbfNode) - def createAndTagDrivenWidget(self, weightInfo, lockWidgets=True): + def createAndTagDrivenWidget(self): """create and associate a widget, populated with the information provided by the weightInfo Args: - weightInfo (dict): information to populate the widgets with - lockWidgets (bool, optional): should they be locked from editing Returns: QWidget: parent widget that houses all the information to display """ - drivenWidgetComponents = self.createDrivenAttributeWidget() - drivenWidget = drivenWidgetComponents.pop(-1) - widgetAttrs = ("drivenLineEdit", - "drivenSelectButton", - "attributeListWidget", - "tableWidget") - for component, attr in zip(drivenWidgetComponents, widgetAttrs): - setattr(drivenWidget, attr, component) - if attr == "attributeListWidget" and lockWidgets: - component.setEnabled(False) - # TODO add signal connections here - table = [wdgt for wdgt in drivenWidgetComponents - if type(wdgt) == QtWidgets.QTableWidget][0] - header = table.verticalHeader() - # TODO There was an inconsistency here with signals, potentially - # resolved + drivenWidget, tableWidget = self.createDrivenWidget() + drivenWidget.tableWidget = tableWidget + + # Set up signals for the table + header = tableWidget.verticalHeader() header.sectionClicked.connect(self.setConsistentHeaderSelection) header.sectionClicked.connect(self.recallDriverPose) - selDelFunc = self.setEditDeletePoseEnabled - table.itemSelectionChanged.connect(selDelFunc) - clickWidget = [wdgt for wdgt in drivenWidgetComponents - if type(wdgt) == ClickableLineEdit][0] - clickWidget.clicked.connect(selectNode) + tableWidget.itemSelectionChanged.connect(self.setEditDeletePoseEnabled) return drivenWidget - def setDrivenTable(self, drivenWidget, rbfNode, weightInfo): + @staticmethod + def setDrivenTable(drivenWidget, rbfNode, weightInfo): """set the widgets with information from the weightInfo for dispaly Args: @@ -1228,24 +1128,21 @@ def setDrivenTable(self, drivenWidget, rbfNode, weightInfo): weightInfo (dict): of information to display """ poses = weightInfo["poses"] - drivenWidget.tableWidget.clear() + drivenAttrs = weightInfo["drivenAttrs"] rowCount = len(poses["poseValue"]) + verticalLabels = ["Pose {}".format(index) for index in range(rowCount)] + + drivenWidget.tableWidget.clear() drivenWidget.tableWidget.setRowCount(rowCount) - drivenAttrs = weightInfo["drivenAttrs"] drivenWidget.tableWidget.setColumnCount(len(drivenAttrs)) drivenWidget.tableWidget.setHorizontalHeaderLabels(drivenAttrs) - verticalLabels = ["Pose {}".format(index) for index in range(rowCount)] drivenWidget.tableWidget.setVerticalHeaderLabels(verticalLabels) + for rowIndex, poseInput in enumerate(poses["poseValue"]): for columnIndex, pValue in enumerate(poseInput): - rbfAttrPlug = "{}.poses[{}].poseValue[{}]".format(rbfNode, - rowIndex, - columnIndex) - attrEdit, mAttrFeild = getControlAttrWidget(rbfAttrPlug, - label="") - drivenWidget.tableWidget.setCellWidget(rowIndex, - columnIndex, - attrEdit) + rbfAttrPlug = "{}.poses[{}].poseValue[{}]".format(rbfNode, rowIndex, columnIndex) + attrEdit, mAttrFeild = getControlAttrWidget(rbfAttrPlug, label="") + drivenWidget.tableWidget.setCellWidget(rowIndex, columnIndex, attrEdit) def populateDrivenWidgetInfo(self, drivenWidget, weightInfo, rbfNode): """set the information from the weightInfo to the widgets child of @@ -1406,6 +1303,17 @@ def refreshRbfSetupList(self, setToSelection=False): self.lockDriverWidgets(lock=False) self.rbf_cbox.blockSignals(False) + def clearDriverTabs(self): + """force deletion on tab widgets + """ + toRemove = [] + tabIndicies = self.driverPoseTableWidget.count() + for index in range(tabIndicies): + tabWidget = self.driverPoseTableWidget.widget(index) + toRemove.append(tabWidget) + self.driverPoseTableWidget.clear() + [t.deleteLater() for t in toRemove] + def clearDrivenTabs(self): """force deletion on tab widgets """ @@ -1436,11 +1344,12 @@ def refresh(self, if driverSelection: self.controlLineEdit.clear() self.driverLineEdit.clear() - self.driver_attributes_widget.clear() + self.driverAttributesWidget.clear() self.__deleteAssociatedWidgetsMaya(self.driverPoseTableWidget) self.__deleteAssociatedWidgets(self.driverPoseTableWidget) - self.driverPoseTableWidget.clear() if drivenSelection: + self.drivenLineEdit.clear() + self.drivenAttributesWidget.clear() self.clearDrivenTabs() if currentRBFSetupNodes: self.currentRBFSetupNodes = [] @@ -1756,7 +1665,6 @@ def toggleGetPoseType(self, toggleState): toggleState (bool): default True """ self.absWorld = toggleState - print("Recording poses in world space set to: {}".format(toggleState)) def toggleDefaultType(self, toggleState): """records whether the user wants default poses to be zeroed @@ -1765,42 +1673,55 @@ def toggleDefaultType(self, toggleState): toggleState (bool): default True """ self.zeroedDefaults = toggleState - print("Default poses are zeroed: {}".format(toggleState)) # signal management ------------------------------------------------------- def connectSignals(self): """connect all the signals in the UI Exceptions being MenuBar and Table header signals """ + # RBF ComboBox and Refresh Button self.rbf_cbox.currentIndexChanged.connect(self.displayRBFSetupInfo) - self.rbf_refreshButton.clicked.connect(self.refresh) + # Driver Line Edit and Control Line Edit self.driverLineEdit.clicked.connect(selectNode) self.controlLineEdit.clicked.connect(selectNode) + self.driverLineEdit.textChanged.connect(partial(self.updateAttributeDisplay, self.driverAttributesWidget)) + self.drivenLineEdit.textChanged.connect(partial(self.updateAttributeDisplay, self.drivenAttributesWidget)) + + # Table Widget header = self.driverPoseTableWidget.verticalHeader() header.sectionClicked.connect(self.setConsistentHeaderSelection) header.sectionClicked.connect(self.recallDriverPose) selDelFunc = self.setEditDeletePoseEnabled self.driverPoseTableWidget.itemSelectionChanged.connect(selDelFunc) - self.addRbfButton.clicked.connect(self.addRBFToSetup) + # Buttons Widget + self.addRbfButton.clicked.connect(self.addRBFToSetup) self.addPoseButton.clicked.connect(self.addPose) self.editPoseButton.clicked.connect(self.editPose) self.editPoseValuesButton.clicked.connect(self.editPoseValues) self.deletePoseButton.clicked.connect(self.deletePose) - partialObj = partial(self.setSetupDriverControl, self.controlLineEdit) - self.setControlButton.clicked.connect(partialObj) - self.setDriverButton.clicked.connect(partial(self.setNodeToField, - self.driverLineEdit)) - partialObj = partial(self.updateAttributeDisplay, - self.driver_attributes_widget) - self.driverLineEdit.textChanged.connect(partialObj) - partialObj = partial(self.attrListMenu, - self.driver_attributes_widget, - self.driverLineEdit) - customMenu = self.driver_attributes_widget.customContextMenuRequested - customMenu.connect(partialObj) + self.setControlButton.clicked.connect(partial(self.setSetupDriverControl, self.controlLineEdit)) + self.setDriverButton.clicked.connect(partial(self.setNodeToField, self.driverLineEdit)) + self.allButton.clicked.connect(self.setDriverControlLineEdit) + self.setDrivenButton.clicked.connect(partial(self.setNodeToField, self.drivenLineEdit)) + + # Custom Context Menus + customMenu = self.driverAttributesWidget.customContextMenuRequested + customMenu.connect( + partial(self.attrListMenu, + self.driverAttributesWidget, + self.driverLineEdit) + ) + customMenu = self.drivenAttributesWidget.customContextMenuRequested + customMenu.connect( + partial(self.attrListMenu, + self.drivenAttributesWidget, + self.driverLineEdit) + ) + + # Tab Widget tabBar = self.rbfTabWidget.tabBar() tabBar.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) tabBar.customContextMenuRequested.connect(self.tabConextMenu) @@ -1871,7 +1792,8 @@ def selectNodeWidget(self, label, buttonLabel="Select"): nodeLayout.addWidget(nodeSelectButton) return nodeLayout, nodeLineEdit, nodeSelectButton - def labelListWidget(self, label, horizontal=True): + @staticmethod + def labelListWidget(label, horizontal=True): """create the listAttribute that users can select their driver/driven attributes for the setup @@ -1911,36 +1833,43 @@ def createDriverAttributeWidget(self): list: [of widgets] """ driverMainLayout = QtWidgets.QVBoxLayout() - # -------------------------------------------------------------------- - (driverLayout, - driverLineEdit, - driverSelectButton) = self.selectNodeWidget("Driver", - buttonLabel="Set") - driverLineEdit.setToolTip("The node driving the setup. (Click me!)") + driverDrivenVLayout = QtWidgets.QVBoxLayout() + driverDrivenHLayout = QtWidgets.QHBoxLayout() + + driverDrivenHLayout.setSpacing(3) # -------------------------------------------------------------------- (controlLayout, controlLineEdit, - setControlButton) = self.selectNodeWidget("Control", - buttonLabel="Set") + setControlButton) = self.selectNodeWidget("Control", buttonLabel="Set") controlLineEdit.setToolTip("The node driving the setup. (Click me!)") # -------------------------------------------------------------------- + (driverLayout, + driverLineEdit, + driverSelectButton) = self.selectNodeWidget("Driver", buttonLabel="Set") + driverLineEdit.setToolTip("The node driving the setup. (Click me!)") + # -------------------------------------------------------------------- + allButton = self.createCustomButton("All", (20, 53), "") (attributeLayout, - attributeListWidget) = self.labelListWidget(label="Select Attributes", + attributeListWidget) = self.labelListWidget(label="Select Driver Attributes:", horizontal=False) attributeListWidget.setToolTip("List of attributes driving setup.") selType = QtWidgets.QAbstractItemView.ExtendedSelection attributeListWidget.setSelectionMode(selType) attributeListWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) # -------------------------------------------------------------------- - driverMainLayout.addLayout(driverLayout, 0) - driverMainLayout.addLayout(controlLayout, 0) + driverDrivenVLayout.addLayout(controlLayout, 0) + driverDrivenVLayout.addLayout(driverLayout, 0) + driverDrivenHLayout.addLayout(driverDrivenVLayout, 0) + driverDrivenHLayout.addWidget(allButton, 0) + driverMainLayout.addLayout(driverDrivenHLayout, 0) driverMainLayout.addLayout(attributeLayout, 0) return [controlLineEdit, setControlButton, driverLineEdit, driverSelectButton, + allButton, attributeListWidget, driverMainLayout] @@ -1951,7 +1880,7 @@ def createDrivenAttributeWidget(self): list: [of widgets] """ drivenWidget = QtWidgets.QWidget() - drivenMainLayout = QtWidgets.QHBoxLayout() + drivenMainLayout = QtWidgets.QVBoxLayout() drivenMainLayout.setContentsMargins(0, 10, 0, 10) drivenMainLayout.setSpacing(9) driverSetLayout = QtWidgets.QVBoxLayout() @@ -1960,14 +1889,12 @@ def createDrivenAttributeWidget(self): # -------------------------------------------------------------------- (driverLayout, driverLineEdit, - driverSelectButton) = self.selectNodeWidget("Driven", - buttonLabel="Select") + driverSelectButton) = self.selectNodeWidget("Driven", buttonLabel="Set") drivenTip = "The node being driven by setup. (Click me!)" driverLineEdit.setToolTip(drivenTip) - driverSelectButton.hide() # -------------------------------------------------------------------- (attributeLayout, - attributeListWidget) = self.labelListWidget(label="Attributes", + attributeListWidget) = self.labelListWidget(label="Select Driven Attributes:", horizontal=False) attributeListWidget.setToolTip("Attributes being driven by setup.") attributeLayout.setSpacing(1) @@ -1975,16 +1902,29 @@ def createDrivenAttributeWidget(self): attributeListWidget.setSelectionMode(selType) attributeListWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) # -------------------------------------------------------------------- - tableWidget = self.createTableWidget() - driverSetLayout.addLayout(driverLayout, 0) driverSetLayout.addLayout(attributeLayout, 0) - drivenMainLayout.addWidget(tableWidget, 1) return [driverLineEdit, driverSelectButton, attributeListWidget, - tableWidget, - drivenWidget] + drivenWidget, + drivenMainLayout] + + def createDrivenWidget(self): + """the widget that displays the driven information + + Returns: + list: [of widgets] + """ + drivenWidget = QtWidgets.QWidget() + drivenMainLayout = QtWidgets.QHBoxLayout() + drivenMainLayout.setContentsMargins(0, 10, 0, 10) + drivenMainLayout.setSpacing(9) + drivenWidget.setLayout(drivenMainLayout) + + tableWidget = self.createTableWidget() + drivenMainLayout.addWidget(tableWidget, 1) + return drivenWidget, tableWidget def createTableWidget(self): """create table widget used to display poses, set tooltips and colum @@ -2101,7 +2041,6 @@ def createMenuBar(self, hideMenuBar=False): return mainMenuBar # main assebly ------------------------------------------------------------ - def createCentralWidget(self): """main UI assembly @@ -2110,6 +2049,7 @@ def createCentralWidget(self): """ centralWidget = QtWidgets.QWidget() centralWidgetLayout = QtWidgets.QVBoxLayout() + centralSubWidgetLayout = QtWidgets.QHBoxLayout() centralWidget.setLayout(centralWidgetLayout) (rbfLayout, self.rbf_cbox, @@ -2119,27 +2059,39 @@ def createCentralWidget(self): centralWidgetLayout.addLayout(rbfLayout) centralWidgetLayout.addWidget(HLine()) # -------------------------------------------------------------------- - driverDrivenLayout = QtWidgets.QHBoxLayout() (self.controlLineEdit, self.setControlButton, self.driverLineEdit, self.setDriverButton, - self.driver_attributes_widget, + self.allButton, + self.driverAttributesWidget, driverLayout) = self.createDriverAttributeWidget() + (self.drivenLineEdit, + self.setDrivenButton, + self.drivenAttributesWidget, + self.drivenWidget, + self.drivenMainLayout) = self.createDrivenAttributeWidget() + self.addRbfButton = QtWidgets.QPushButton("New RBF") self.addRbfButton.setToolTip("Select node to be driven by setup.") self.addRbfButton.setFixedHeight(self.genericWidgetHight) self.addRbfButton.setStyleSheet("background-color: rgb(23, 158, 131)") + + driverLayout.addWidget(self.drivenWidget) driverLayout.addWidget(self.addRbfButton) + driverDrivenLayout = QtWidgets.QHBoxLayout() + driverDrivenTableLayout = QtWidgets.QVBoxLayout() self.driverPoseTableWidget = self.createTableWidget() - driverDrivenLayout.addLayout(driverLayout, 0) - driverDrivenLayout.addWidget(self.driverPoseTableWidget, 1) - centralWidgetLayout.addLayout(driverDrivenLayout, 1) - # -------------------------------------------------------------------- self.rbfTabWidget = self.createTabWidget() - centralWidgetLayout.addWidget(self.rbfTabWidget, 1) + + driverDrivenLayout.addLayout(driverLayout, 0) + driverDrivenTableLayout.addWidget(self.driverPoseTableWidget, 1) + driverDrivenTableLayout.addWidget(self.rbfTabWidget, 1) + centralSubWidgetLayout.addLayout(driverDrivenLayout, 1) + centralSubWidgetLayout.addLayout(driverDrivenTableLayout, 2) + centralWidgetLayout.addLayout(centralSubWidgetLayout, 1) # -------------------------------------------------------------------- (optionsLayout, self.addPoseButton, From 990203c08e628d9045efa2a8353435e785e68f2b Mon Sep 17 00:00:00 2001 From: Joji Date: Tue, 3 Oct 2023 11:30:04 -0700 Subject: [PATCH 05/20] Refactored codes --- .../scripts/mgear/rigbits/rbf_manager_ui.py | 58 +++++++++---------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/release/scripts/mgear/rigbits/rbf_manager_ui.py b/release/scripts/mgear/rigbits/rbf_manager_ui.py index 214431df..3cf677e9 100644 --- a/release/scripts/mgear/rigbits/rbf_manager_ui.py +++ b/release/scripts/mgear/rigbits/rbf_manager_ui.py @@ -91,7 +91,6 @@ # ============================================================================= # Constants # ============================================================================= -__version__ = "1.0.6" _mgear_version = mgear.getVersion() TOOL_NAME = "RBF Manager" @@ -121,7 +120,6 @@ def testFunctions(*args): print('!!', args) -def getPlugAttrs(nodes, attrType="all"): def getPlugAttrs(nodes, attrType="keyable"): """Get a list of attributes to display to the user @@ -224,7 +222,7 @@ def selectNode(name): # ============================================================================= def getControlAttrWidget(nodeAttr, label=""): - """get a cmds.attrControlGrp wrapped in a qtWidget, still connected + """Create a cmds.attrControlGrp and wrap it in a qtWidget, preserving its connection to the specified attr Args: @@ -232,16 +230,11 @@ def getControlAttrWidget(nodeAttr, label=""): label (str, optional): name for the attr widget Returns: - QtWidget: qwidget created from attrControlGrp + str: The name of the created Maya attrControlGrp """ - mAttrFeild = mc.attrControlGrp(attribute=nodeAttr, - label=label, - po=True) + mAttrFeild = mc.attrControlGrp(attribute=nodeAttr, label=label, po=True) + ptr = mui.MQtUtil.findControl(mAttrFeild) - if PY2: - controlWidget = QtCompat.wrapInstance(long(ptr), base=QtWidgets.QWidget) - else: - controlWidget = QtCompat.wrapInstance(int(ptr), base=QtWidgets.QWidget) controlWidget.setContentsMargins(0, 0, 0, 0) controlWidget.setMinimumWidth(0) attrEdit = [wdgt for wdgt in controlWidget.children() @@ -298,6 +291,7 @@ def show(dockable=True, newSceneCallBack=True, *args): # Create the UI RBF_UI = RBFManagerUI(newSceneCallBack=newSceneCallBack) + RBF_UI.initializePoseControlWidgets() # Check if we've saved a size previously and set it if mc.optionVar(exists='RBF_UI_width') and mc.optionVar(exists='RBF_UI_height'): @@ -573,7 +567,6 @@ def addRBFToSetup(self): else: drivenNode_name = drivenNode - # check if there is an existing rbf node attached if mc.objExists(drivenNode_name): if existing_rbf_setup(drivenNode_name): msg = "Node is already driven by an RBF Setup." @@ -774,7 +767,6 @@ def editPoseValues(self): rbfNode.forceEvaluation() self.refreshAllTables() - def updateAllFromTables(self): """Update every pose @@ -815,8 +807,6 @@ def updateAllFromTables(self): self.refreshAllTables() - - def addPose(self): """Add pose to rbf nodes in setup. Additional index on all nodes @@ -831,7 +821,7 @@ def addPose(self): poseInputs = rbf_node.getMultipleAttrs(driverNode, driverAttrs) for rbfNode in rbfNodes: poseValues = rbfNode.getPoseValues(resetDriven=True, - absoluteWorld=self.absWorld) + poseValues = rbfNode.getPoseValues(resetDriven=True, absoluteWorld=self.absWorld) rbfNode.addPose(poseInput=poseInputs, poseValue=poseValues) self.refreshAllTables() @@ -938,6 +928,8 @@ def updateAttributeDisplay(self, self.highlightListEntries(attrListWidget, highlight) def __deleteAssociatedWidgetsMaya(self, widget, attrName="associatedMaya"): + @staticmethod + def __deleteAssociatedWidgetsMaya(widget, attrName="associatedMaya"): """delete core ui items 'associated' with the provided widgets Args: @@ -985,6 +977,7 @@ def syncDriverTableCells(self, attrEdit, rbfAttrPlug): mc.setAttr(attrPlug, float(value)) rbfNode.forceEvaluation() + getControlAttrWidget(rbfAttrPlug, label="") def setDriverTable(self, rbfNode, weightInfo): """Set the driverTable widget with the information from the weightInfo @@ -1156,7 +1149,6 @@ def populateDrivenWidgetInfo(self, drivenWidget, weightInfo, rbfNode): Returns: n/a: n/a """ - drivenWidget.drivenLineEdit.clear() driverNode = weightInfo["drivenNode"] if driverNode: driverNode = driverNode[0] @@ -1165,8 +1157,6 @@ def populateDrivenWidgetInfo(self, drivenWidget, weightInfo, rbfNode): drivenWidget.drivenLineEdit.setText(str(driverNode)) self.setAttributeDisplay(drivenWidget.attributeListWidget, - weightInfo["drivenNode"][0], - weightInfo["drivenAttrs"]) self.setDrivenTable(drivenWidget, rbfNode, weightInfo) def addNewTab(self, rbfNode): @@ -1178,7 +1168,7 @@ def addNewTab(self, rbfNode): Returns: QWidget: created widget """ - tabDrivenWidget = self.createAndTagDrivenWidget({}) + tabDrivenWidget = self.createAndTagDrivenWidget() self._associateRBFnodeAndWidget(tabDrivenWidget, rbfNode) self.rbfTabWidget.addTab(tabDrivenWidget, str(rbfNode)) return tabDrivenWidget @@ -1193,7 +1183,7 @@ def recreateDrivenTabs(self, rbfNodes): self.rbfTabWidget.clear() for rbfNode in rbfNodes: weightInfo = rbfNode.getNodeInfo() - drivenWidget = self.createAndTagDrivenWidget(weightInfo) + drivenWidget = self.createAndTagDrivenWidget() self._associateRBFnodeAndWidget(drivenWidget, rbfNode) self.populateDrivenWidgetInfo(drivenWidget, weightInfo, rbfNode) self.rbfTabWidget.addTab(drivenWidget, rbfNode.name) @@ -1214,11 +1204,15 @@ def displayRBFSetupInfo(self, index): self.currentRBFSetupNodes = [] self.lockDriverWidgets(lock=False) return + rbfNodes = self.allSetupsInfo.get(rbfSelection, []) if not rbfNodes: return + + # Display node info in the UI self.currentRBFSetupNodes = rbfNodes weightInfo = rbfNodes[0].getNodeInfo() + self.populateDriverInfo(rbfNodes[0], weightInfo) self.lockDriverWidgets(lock=True) # wrapping the following in try due to what I think is a Qt Bug. @@ -1226,7 +1220,7 @@ def displayRBFSetupInfo(self, index): # File "rbf_manager_ui.py", line 872, in createAndTagDrivenWidget # header.sectionClicked.connect(self.setConsistentHeaderSelection) # AttributeError: 'PySide2.QtWidgets.QListWidgetItem' object has - # no attribute 'sectionClicked' + try: self.recreateDrivenTabs(self.allSetupsInfo[rbfSelection]) except AttributeError: @@ -1290,12 +1284,16 @@ def refreshRbfSetupList(self, setToSelection=False): setToSelection (bool, optional): after refresh, set to desired """ self.rbf_cbox.blockSignals(True) + + # Clear the combo box and populate with new setup options self.rbf_cbox.clear() addNewOfType = ["New {} setup".format(rbf) for rbf in rbf_node.SUPPORTED_RBF_NODES] self.updateAllSetupsInfo() addNewOfType.extend(sorted(self.allSetupsInfo.keys())) self.rbf_cbox.addItems(addNewOfType) + self.rbf_cbox.addItems(newSetupOptions + allSetups) + if setToSelection: selectionIndex = self.rbf_cbox.findText(setToSelection) self.rbf_cbox.setCurrentIndex(selectionIndex) @@ -1429,6 +1427,8 @@ def setSetupDriverControl(self, lineEditWidget): self.setDriverControlOnSetup(controlName) def getRBFNodesInfo(self, rbfNodes): + @staticmethod + def getRBFNodesInfo(rbfNodes): """create a dictionary of all the RBFInfo(referred to as weightNodeInfo a lot) for export @@ -1485,6 +1485,7 @@ def exportNodes(self, allSetups=True): rbf_io.exportRBFs(nodesToExport, filePath) def gatherMirroredInfo(self, rbfNodes): + @staticmethod """gather all the info from the provided nodes and string replace side information for its mirror. Using mGear standard naming convections @@ -1503,27 +1504,20 @@ def gatherMirroredInfo(self, rbfNodes): for pairs in weightInfo["connections"]: mrConnections.append([mString.convertRLName(pairs[0]), mString.convertRLName(pairs[1])]) + weightInfo["connections"] = mrConnections - # drivenControlName ----------------------------------------------- - mrDrvnCtl = mString.convertRLName(weightInfo["drivenControlName"]) - weightInfo["drivenControlName"] = mrDrvnCtl - # drivenNode ------------------------------------------------------ - weightInfo["drivenNode"] = [mString.convertRLName(n) for n - in weightInfo["drivenNode"]] - # driverControl --------------------------------------------------- - mrDrvrCtl = mString.convertRLName(weightInfo["driverControl"]) weightInfo["driverControl"] = mrDrvrCtl # driverNode ------------------------------------------------------ weightInfo["driverNode"] = [mString.convertRLName(n) for n in weightInfo["driverNode"]] + weightInfo["driverControl"] = mString.convertRLName(weightInfo["driverControl"]) + weightInfo["driverNode"] = [mString.convertRLName(n) for n in weightInfo["driverNode"]] # setupName ------------------------------------------------------- mrSetupName = mString.convertRLName(weightInfo["setupName"]) if mrSetupName == weightInfo["setupName"]: mrSetupName = "{}{}".format(mrSetupName, MIRROR_SUFFIX) weightInfo["setupName"] = mrSetupName # transformNode --------------------------------------------------- - # name - # parent tmp = weightInfo["transformNode"]["name"] mrTransformName = mString.convertRLName(tmp) weightInfo["transformNode"]["name"] = mrTransformName From bf91c109a0ee6464f157a0da3895b6da1d330d10 Mon Sep 17 00:00:00 2001 From: Joji Date: Sat, 7 Oct 2023 00:15:35 -0700 Subject: [PATCH 06/20] Added a button to add new driven object, refactored some codes --- .../scripts/mgear/rigbits/rbf_manager_ui.py | 147 ++++++++++++------ 1 file changed, 103 insertions(+), 44 deletions(-) diff --git a/release/scripts/mgear/rigbits/rbf_manager_ui.py b/release/scripts/mgear/rigbits/rbf_manager_ui.py index 3cf677e9..09806a5a 100644 --- a/release/scripts/mgear/rigbits/rbf_manager_ui.py +++ b/release/scripts/mgear/rigbits/rbf_manager_ui.py @@ -81,16 +81,11 @@ from . import rbf_node from .six import PY2 -# debug -# reload(rbf_io) -# reload(rbf_node) - -# from mgear.rigbits import rbf_manager_ui -# rbf_ui = rbf_manager_ui.show() # ============================================================================= # Constants # ============================================================================= +__version__ = "1.0.8" _mgear_version = mgear.getVersion() TOOL_NAME = "RBF Manager" @@ -230,15 +225,20 @@ def getControlAttrWidget(nodeAttr, label=""): label (str, optional): name for the attr widget Returns: + QtWidget.QLineEdit: A Qt widget created from attrControlGrp str: The name of the created Maya attrControlGrp """ mAttrFeild = mc.attrControlGrp(attribute=nodeAttr, label=label, po=True) + # Convert the Maya control to a Qt pointer ptr = mui.MQtUtil.findControl(mAttrFeild) + + # Wrap the Maya control into a Qt widget, considering Python version + controlWidget = QtCompat.wrapInstance(long(ptr) if PY2 else int(ptr), base=QtWidgets.QWidget) controlWidget.setContentsMargins(0, 0, 0, 0) controlWidget.setMinimumWidth(0) - attrEdit = [wdgt for wdgt in controlWidget.children() - if type(wdgt) == QtWidgets.QLineEdit] + + attrEdit = [wdgt for wdgt in controlWidget.children() if type(wdgt) == QtWidgets.QLineEdit] [wdgt.setParent(attrEdit[0]) for wdgt in controlWidget.children() if type(wdgt) == QtCore.QObject] @@ -567,6 +567,7 @@ def addRBFToSetup(self): else: drivenNode_name = drivenNode + # Check if there is an existing rbf node attached if mc.objExists(drivenNode_name): if existing_rbf_setup(drivenNode_name): msg = "Node is already driven by an RBF Setup." @@ -657,7 +658,7 @@ def refreshAllTables(self): rbfNode = None for index in range(self.rbfTabWidget.count()): drivenWidget = self.rbfTabWidget.widget(index) - drivenNodeName = drivenWidget.drivenLineEdit.text() + drivenNodeName = drivenWidget.property("drivenNode") for rbfNode in self.currentRBFSetupNodes: drivenNodes = rbfNode.getDrivenNode() if drivenNodes and drivenNodes[0] != drivenNodeName: @@ -806,7 +807,6 @@ def updateAllFromTables(self): rbfNode.forceEvaluation() self.refreshAllTables() - def addPose(self): """Add pose to rbf nodes in setup. Additional index on all nodes @@ -820,7 +820,6 @@ def addPose(self): driverAttrs = rbfNodes[0].getDriverNodeAttributes() poseInputs = rbf_node.getMultipleAttrs(driverNode, driverAttrs) for rbfNode in rbfNodes: - poseValues = rbfNode.getPoseValues(resetDriven=True, poseValues = rbfNode.getPoseValues(resetDriven=True, absoluteWorld=self.absWorld) rbfNode.addPose(poseInput=poseInputs, poseValue=poseValues) self.refreshAllTables() @@ -927,7 +926,6 @@ def updateAttributeDisplay(self, if highlight: self.highlightListEntries(attrListWidget, highlight) - def __deleteAssociatedWidgetsMaya(self, widget, attrName="associatedMaya"): @staticmethod def __deleteAssociatedWidgetsMaya(widget, attrName="associatedMaya"): """delete core ui items 'associated' with the provided widgets @@ -945,7 +943,8 @@ def __deleteAssociatedWidgetsMaya(widget, attrName="associatedMaya"): else: setattr(widget, attrName, []) - def __deleteAssociatedWidgets(self, widget, attrName="associated"): + @staticmethod + def __deleteAssociatedWidgets(widget, attrName="associated"): """delete widget items 'associated' with the provided widgets Args: @@ -977,7 +976,29 @@ def syncDriverTableCells(self, attrEdit, rbfAttrPlug): mc.setAttr(attrPlug, float(value)) rbfNode.forceEvaluation() + def initializePoseControlWidgets(self): + """Initialize UI widgets for each pose input based on the information from RBF nodes. + This dynamically creates widgets for the control attributes associated with each pose. + """ + # Retrieve all the RBF nodes from the stored setups info + rbfNodes = self.allSetupsInfo.values() + + # Loop through each RBF node to extract its weight information + for rbfNode in rbfNodes: + weightInfo = rbfNode[0].getNodeInfo() + + # Extract pose information from the weight data + poses = weightInfo.get("poses", None) + if not poses: + continue + # Enumerate through each pose input for this RBF node + for rowIndex, poseInput in enumerate(poses["poseInput"]): + for columnIndex, pValue in enumerate(poseInput): + rbfAttrPlug = "{}.poses[{}].poseInput[{}]".format(rbfNode[0], rowIndex, columnIndex) + + # Create a control widget for this pose input attribute getControlAttrWidget(rbfAttrPlug, label="") + def setDriverTable(self, rbfNode, weightInfo): """Set the driverTable widget with the information from the weightInfo @@ -1077,8 +1098,8 @@ def populateDrivenInfo(self, rbfNode, weightInfo): self.setAttributeDisplay(self.drivenAttributesWidget, drivenNode or "", weightInfo["drivenAttrs"]) # Add the driven widget to the tab widget. + drivenWidget.setProperty("drivenNode", drivenNode) self.rbfTabWidget.addTab(drivenWidget, rbfNode.name) - self.rbfTabWidget.setProperty("drivenNode", drivenNode) self.setDrivenTable(drivenWidget, rbfNode, weightInfo) @staticmethod @@ -1154,9 +1175,7 @@ def populateDrivenWidgetInfo(self, drivenWidget, weightInfo, rbfNode): driverNode = driverNode[0] else: return - drivenWidget.drivenLineEdit.setText(str(driverNode)) - self.setAttributeDisplay(drivenWidget.attributeListWidget, self.setDrivenTable(drivenWidget, rbfNode, weightInfo) def addNewTab(self, rbfNode): @@ -1173,6 +1192,20 @@ def addNewTab(self, rbfNode): self.rbfTabWidget.addTab(tabDrivenWidget, str(rbfNode)) return tabDrivenWidget + def addNewDriven(self): + self.refresh( + rbfSelection=False, + driverSelection=False, + drivenSelection=True, + currentRBFSetupNodes=False, + clearDrivenTab=False + ) + + self.setDrivenButton.blockSignals(False) + self.drivenAttributesWidget.setEnabled(True) + + self.addRbfButton.setText("Add new driven to RBF") + def recreateDrivenTabs(self, rbfNodes): """remove tabs and create ones for each node in rbfNodes provided @@ -1196,15 +1229,20 @@ def displayRBFSetupInfo(self, index): """ rbfSelection = str(self.rbf_cbox.currentText()) + + # Refresh UI components self.refresh(rbfSelection=False, driverSelection=True, drivenSelection=True, currentRBFSetupNodes=False) + + # Handle 'New' selection case if rbfSelection.startswith("New "): self.currentRBFSetupNodes = [] self.lockDriverWidgets(lock=False) return + # Fetch RBF nodes for the selected setup rbfNodes = self.allSetupsInfo.get(rbfSelection, []) if not rbfNodes: return @@ -1214,12 +1252,8 @@ def displayRBFSetupInfo(self, index): weightInfo = rbfNodes[0].getNodeInfo() self.populateDriverInfo(rbfNodes[0], weightInfo) + self.populateDrivenInfo(rbfNodes[0], weightInfo) self.lockDriverWidgets(lock=True) - # wrapping the following in try due to what I think is a Qt Bug. - # need to look further into this. - # File "rbf_manager_ui.py", line 872, in createAndTagDrivenWidget - # header.sectionClicked.connect(self.setConsistentHeaderSelection) - # AttributeError: 'PySide2.QtWidgets.QListWidgetItem' object has try: self.recreateDrivenTabs(self.allSetupsInfo[rbfSelection]) @@ -1287,11 +1321,9 @@ def refreshRbfSetupList(self, setToSelection=False): # Clear the combo box and populate with new setup options self.rbf_cbox.clear() - addNewOfType = ["New {} setup".format(rbf) - for rbf in rbf_node.SUPPORTED_RBF_NODES] self.updateAllSetupsInfo() - addNewOfType.extend(sorted(self.allSetupsInfo.keys())) - self.rbf_cbox.addItems(addNewOfType) + allSetups = sorted(self.allSetupsInfo.keys()) + newSetupOptions = ["New {} setup".format(rbf) for rbf in rbf_node.SUPPORTED_RBF_NODES] self.rbf_cbox.addItems(newSetupOptions + allSetups) if setToSelection: @@ -1328,6 +1360,7 @@ def refresh(self, driverSelection=True, drivenSelection=True, currentRBFSetupNodes=True, + clearDrivenTab=True, *args): """Refreshes the UI @@ -1336,7 +1369,9 @@ def refresh(self, driverSelection (bool, optional): desired section to refresh drivenSelection (bool, optional): desired section to refresh currentRBFSetupNodes (bool, optional): desired section to refresh + clearDrivenTab (bool, optional): desired section to refresh """ + self.addRbfButton.setText("New RBF") if rbfSelection: self.refreshRbfSetupList() if driverSelection: @@ -1348,7 +1383,8 @@ def refresh(self, if drivenSelection: self.drivenLineEdit.clear() self.drivenAttributesWidget.clear() - self.clearDrivenTabs() + if clearDrivenTab: + self.clearDrivenTabs() if currentRBFSetupNodes: self.currentRBFSetupNodes = [] @@ -1426,7 +1462,6 @@ def setSetupDriverControl(self, lineEditWidget): controlName = self.setNodeToField(lineEditWidget) self.setDriverControlOnSetup(controlName) - def getRBFNodesInfo(self, rbfNodes): @staticmethod def getRBFNodesInfo(rbfNodes): """create a dictionary of all the RBFInfo(referred to as @@ -1484,8 +1519,8 @@ def exportNodes(self, allSetups=True): return rbf_io.exportRBFs(nodesToExport, filePath) - def gatherMirroredInfo(self, rbfNodes): @staticmethod + def gatherMirroredInfo(rbfNodes): """gather all the info from the provided nodes and string replace side information for its mirror. Using mGear standard naming convections @@ -1506,12 +1541,11 @@ def gatherMirroredInfo(self, rbfNodes): mString.convertRLName(pairs[1])]) weightInfo["connections"] = mrConnections - weightInfo["driverControl"] = mrDrvrCtl - # driverNode ------------------------------------------------------ - weightInfo["driverNode"] = [mString.convertRLName(n) for n - in weightInfo["driverNode"]] + weightInfo["drivenControlName"] = mString.convertRLName(weightInfo["drivenControlName"]) + weightInfo["drivenNode"] = [mString.convertRLName(n) for n in weightInfo["drivenNode"]] weightInfo["driverControl"] = mString.convertRLName(weightInfo["driverControl"]) weightInfo["driverNode"] = [mString.convertRLName(n) for n in weightInfo["driverNode"]] + # setupName ------------------------------------------------------- mrSetupName = mString.convertRLName(weightInfo["setupName"]) if mrSetupName == weightInfo["setupName"]: @@ -1546,8 +1580,7 @@ def getMirroredSetupTargetsInfo(self): drivenControlNode = rbfNode.getConnectedRBFToggleNode() mrDrivenControlNode = mString.convertRLName(drivenControlNode) mrDrivenControlNode = pm.PyNode(mrDrivenControlNode) - setupTargetInfo_dict[pm.PyNode(drivenNode)] = [mrDrivenControlNode, - mrRbfNode] + setupTargetInfo_dict[pm.PyNode(drivenNode)] = [mrDrivenControlNode, mrRbfNode] return setupTargetInfo_dict def mirrorSetup(self): @@ -1700,6 +1733,7 @@ def connectSignals(self): self.setDriverButton.clicked.connect(partial(self.setNodeToField, self.driverLineEdit)) self.allButton.clicked.connect(self.setDriverControlLineEdit) self.setDrivenButton.clicked.connect(partial(self.setNodeToField, self.drivenLineEdit)) + self.addDrivenButton.clicked.connect(self.addNewDriven) # Custom Context Menus customMenu = self.driverAttributesWidget.customContextMenuRequested @@ -1772,6 +1806,8 @@ def selectNodeWidget(self, label, buttonLabel="Select"): ) nodeLayout = QtWidgets.QHBoxLayout() + nodeLayout.setSpacing(4) + nodeLabel = QtWidgets.QLabel(label) nodeLabel.setFixedWidth(40) nodeLineEdit = ClickableLineEdit() @@ -1830,6 +1866,7 @@ def createDriverAttributeWidget(self): driverDrivenVLayout = QtWidgets.QVBoxLayout() driverDrivenHLayout = QtWidgets.QHBoxLayout() + # driverMainLayout.setStyleSheet("QVBoxLayout { background-color: #404040;") driverDrivenHLayout.setSpacing(3) # -------------------------------------------------------------------- (controlLayout, @@ -1875,17 +1912,22 @@ def createDrivenAttributeWidget(self): """ drivenWidget = QtWidgets.QWidget() drivenMainLayout = QtWidgets.QVBoxLayout() + drivenSetLayout = QtWidgets.QVBoxLayout() + drivenMainLayout.setContentsMargins(0, 10, 0, 10) drivenMainLayout.setSpacing(9) - driverSetLayout = QtWidgets.QVBoxLayout() - drivenMainLayout.addLayout(driverSetLayout) + drivenSetLayout = QtWidgets.QVBoxLayout() + drivenMainLayout.addLayout(drivenSetLayout) drivenWidget.setLayout(drivenMainLayout) # -------------------------------------------------------------------- - (driverLayout, - driverLineEdit, - driverSelectButton) = self.selectNodeWidget("Driven", buttonLabel="Set") + (drivenLayout, + drivenLineEdit, + drivenSelectButton) = self.selectNodeWidget("Driven", buttonLabel="Set") drivenTip = "The node being driven by setup. (Click me!)" - driverLineEdit.setToolTip(drivenTip) + drivenLineEdit.setToolTip(drivenTip) + + addDrivenButton = self.createCustomButton("+", (20, 25), "") + addDrivenButton.setToolTip("Add a new driven to the current rbf node") # -------------------------------------------------------------------- (attributeLayout, attributeListWidget) = self.labelListWidget(label="Select Driven Attributes:", @@ -1896,10 +1938,15 @@ def createDrivenAttributeWidget(self): attributeListWidget.setSelectionMode(selType) attributeListWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) # -------------------------------------------------------------------- - driverSetLayout.addLayout(driverLayout, 0) - driverSetLayout.addLayout(attributeLayout, 0) return [driverLineEdit, driverSelectButton, + drivenSetLayout.addLayout(drivenLayout, 0) + drivenSetLayout.addLayout(attributeLayout, 0) + drivenMainLayout.addLayout(drivenSetLayout) + drivenWidget.setLayout(drivenMainLayout) + return [drivenLineEdit, + drivenSelectButton, + addDrivenButton, attributeListWidget, drivenWidget, drivenMainLayout] @@ -2045,6 +2092,13 @@ def createCentralWidget(self): centralWidgetLayout = QtWidgets.QVBoxLayout() centralSubWidgetLayout = QtWidgets.QHBoxLayout() centralWidget.setLayout(centralWidgetLayout) + + driverDrivenWidget = QtWidgets.QWidget() + driverDrivenWidget.setStyleSheet("background-color: rgb(23, 158, 131)") + driverDrivenWidgetLayout = QtWidgets.QVBoxLayout() + driverDrivenWidget.setLayout(driverDrivenWidgetLayout) + + # Setup selector section (rbfLayout, self.rbf_cbox, self.rbf_refreshButton) = self.createSetupSelectorWidget() @@ -2052,7 +2106,8 @@ def createCentralWidget(self): self.rbf_refreshButton.setToolTip("Refresh the UI") centralWidgetLayout.addLayout(rbfLayout) centralWidgetLayout.addWidget(HLine()) - # -------------------------------------------------------------------- + + # Driver section (self.controlLineEdit, self.setControlButton, self.driverLineEdit, @@ -2061,8 +2116,10 @@ def createCentralWidget(self): self.driverAttributesWidget, driverLayout) = self.createDriverAttributeWidget() + # Driven section (self.drivenLineEdit, self.setDrivenButton, + self.addDrivenButton, self.drivenAttributesWidget, self.drivenWidget, self.drivenMainLayout) = self.createDrivenAttributeWidget() @@ -2075,6 +2132,7 @@ def createCentralWidget(self): driverLayout.addWidget(self.drivenWidget) driverLayout.addWidget(self.addRbfButton) + # Setting up the main layout for driver and driven sections driverDrivenLayout = QtWidgets.QHBoxLayout() driverDrivenTableLayout = QtWidgets.QVBoxLayout() self.driverPoseTableWidget = self.createTableWidget() @@ -2086,7 +2144,8 @@ def createCentralWidget(self): centralSubWidgetLayout.addLayout(driverDrivenLayout, 1) centralSubWidgetLayout.addLayout(driverDrivenTableLayout, 2) centralWidgetLayout.addLayout(centralSubWidgetLayout, 1) - # -------------------------------------------------------------------- + + # Options buttons section (optionsLayout, self.addPoseButton, self.editPoseButton, From ddad6037ea9c346fb936b1824b805c22af5328de Mon Sep 17 00:00:00 2001 From: Joji Date: Sat, 7 Oct 2023 00:15:53 -0700 Subject: [PATCH 07/20] Update rbf_manager_ui.py --- release/scripts/mgear/rigbits/rbf_manager_ui.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/release/scripts/mgear/rigbits/rbf_manager_ui.py b/release/scripts/mgear/rigbits/rbf_manager_ui.py index 09806a5a..2cb5ae97 100644 --- a/release/scripts/mgear/rigbits/rbf_manager_ui.py +++ b/release/scripts/mgear/rigbits/rbf_manager_ui.py @@ -1938,8 +1938,9 @@ def createDrivenAttributeWidget(self): attributeListWidget.setSelectionMode(selType) attributeListWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) # -------------------------------------------------------------------- - return [driverLineEdit, - driverSelectButton, + drivenSetLayout.addLayout(drivenLayout, 0) + drivenSetLayout.addLayout(attributeLayout, 0) + drivenLayout.addWidget(addDrivenButton) drivenSetLayout.addLayout(drivenLayout, 0) drivenSetLayout.addLayout(attributeLayout, 0) drivenMainLayout.addLayout(drivenSetLayout) From 64f3421173d3a533f3f6d988cd828d8819dd71a3 Mon Sep 17 00:00:00 2001 From: Joji Date: Sat, 7 Oct 2023 19:01:12 -0700 Subject: [PATCH 08/20] QSplitter was added between attributeWidget and tableWidget. QSplitter is a great widget in PyQt/PySide that allows users to adjust the size of child widgets by dragging the boundary between them. --- .../scripts/mgear/rigbits/rbf_manager_ui.py | 122 +++++++++++------- 1 file changed, 72 insertions(+), 50 deletions(-) diff --git a/release/scripts/mgear/rigbits/rbf_manager_ui.py b/release/scripts/mgear/rigbits/rbf_manager_ui.py index 2cb5ae97..e70d2c67 100644 --- a/release/scripts/mgear/rigbits/rbf_manager_ui.py +++ b/release/scripts/mgear/rigbits/rbf_manager_ui.py @@ -1845,7 +1845,8 @@ def labelListWidget(label, horizontal=True): attributeLayout.addWidget(attributeListWidget) return attributeLayout, attributeListWidget - def addRemoveButtonWidget(self, label1, label2, horizontal=True): + @staticmethod + def addRemoveButtonWidget(label1, label2, horizontal=True): if horizontal: addRemoveLayout = QtWidgets.QHBoxLayout() else: @@ -1862,12 +1863,11 @@ def createDriverAttributeWidget(self): Returns: list: [of widgets] """ - driverMainLayout = QtWidgets.QVBoxLayout() - driverDrivenVLayout = QtWidgets.QVBoxLayout() - driverDrivenHLayout = QtWidgets.QHBoxLayout() + driverControlVLayout = QtWidgets.QVBoxLayout() + driverControlHLayout = QtWidgets.QHBoxLayout() # driverMainLayout.setStyleSheet("QVBoxLayout { background-color: #404040;") - driverDrivenHLayout.setSpacing(3) + driverControlHLayout.setSpacing(3) # -------------------------------------------------------------------- (controlLayout, controlLineEdit, @@ -1879,30 +1879,28 @@ def createDriverAttributeWidget(self): driverSelectButton) = self.selectNodeWidget("Driver", buttonLabel="Set") driverLineEdit.setToolTip("The node driving the setup. (Click me!)") # -------------------------------------------------------------------- - allButton = self.createCustomButton("All", (20, 53), "") - (attributeLayout, - attributeListWidget) = self.labelListWidget(label="Select Driver Attributes:", - horizontal=False) + (attributeLayout, attributeListWidget) = self.labelListWidget( + label="Select Driver Attributes:", horizontal=False) + attributeListWidget.setToolTip("List of attributes driving setup.") selType = QtWidgets.QAbstractItemView.ExtendedSelection attributeListWidget.setSelectionMode(selType) attributeListWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) # -------------------------------------------------------------------- - driverDrivenVLayout.addLayout(controlLayout, 0) - driverDrivenVLayout.addLayout(driverLayout, 0) - driverDrivenHLayout.addLayout(driverDrivenVLayout, 0) - driverDrivenHLayout.addWidget(allButton, 0) - driverMainLayout.addLayout(driverDrivenHLayout, 0) - driverMainLayout.addLayout(attributeLayout, 0) + driverControlVLayout.addLayout(controlLayout, 0) + driverControlVLayout.addLayout(driverLayout, 0) + driverControlHLayout.addLayout(driverControlVLayout, 0) + driverControlHLayout.addWidget(allButton, 0) return [controlLineEdit, setControlButton, driverLineEdit, driverSelectButton, allButton, attributeListWidget, - driverMainLayout] + attributeLayout, + driverControlHLayout] def createDrivenAttributeWidget(self): """the widget that displays the driven information @@ -1912,8 +1910,6 @@ def createDrivenAttributeWidget(self): """ drivenWidget = QtWidgets.QWidget() drivenMainLayout = QtWidgets.QVBoxLayout() - drivenSetLayout = QtWidgets.QVBoxLayout() - drivenMainLayout.setContentsMargins(0, 10, 0, 10) drivenMainLayout.setSpacing(9) drivenSetLayout = QtWidgets.QVBoxLayout() @@ -2082,31 +2078,14 @@ def createMenuBar(self, hideMenuBar=False): self.mousePosition.connect(self.hideMenuBar) return mainMenuBar - # main assebly ------------------------------------------------------------ - def createCentralWidget(self): - """main UI assembly - - Returns: - QtWidget: main UI to be parented to as the centralWidget - """ - centralWidget = QtWidgets.QWidget() - centralWidgetLayout = QtWidgets.QVBoxLayout() - centralSubWidgetLayout = QtWidgets.QHBoxLayout() - centralWidget.setLayout(centralWidgetLayout) - - driverDrivenWidget = QtWidgets.QWidget() - driverDrivenWidget.setStyleSheet("background-color: rgb(23, 158, 131)") - driverDrivenWidgetLayout = QtWidgets.QVBoxLayout() - driverDrivenWidget.setLayout(driverDrivenWidgetLayout) + def createDarkContainerWidget(self): + darkContainer = QtWidgets.QWidget() + driverMainLayout = QtWidgets.QVBoxLayout() + driverMainLayout.setContentsMargins(10, 10, 10, 10) # Adjust margins as needed + driverMainLayout.setSpacing(5) # Adjust spacing between widgets - # Setup selector section - (rbfLayout, - self.rbf_cbox, - self.rbf_refreshButton) = self.createSetupSelectorWidget() - self.rbf_cbox.setToolTip("List of available setups in the scene.") - self.rbf_refreshButton.setToolTip("Refresh the UI") - centralWidgetLayout.addLayout(rbfLayout) - centralWidgetLayout.addWidget(HLine()) + # Setting the dark color (Example: dark gray) + # darkContainer.setStyleSheet("background-color: rgb(40, 40, 40);") # Driver section (self.controlLineEdit, @@ -2115,7 +2094,8 @@ def createCentralWidget(self): self.setDriverButton, self.allButton, self.driverAttributesWidget, - driverLayout) = self.createDriverAttributeWidget() + self.driverAttributesLayout, + driverControlLayout) = self.createDriverAttributeWidget() # Driven section (self.drivenLineEdit, @@ -2130,21 +2110,63 @@ def createCentralWidget(self): self.addRbfButton.setFixedHeight(self.genericWidgetHight) self.addRbfButton.setStyleSheet("background-color: rgb(23, 158, 131)") - driverLayout.addWidget(self.drivenWidget) - driverLayout.addWidget(self.addRbfButton) + # Setting up the main layout for driver and driven sections + driverMainLayout = QtWidgets.QVBoxLayout() + driverMainLayout.addLayout(driverControlLayout) + driverMainLayout.addLayout(self.driverAttributesLayout) + driverMainLayout.addWidget(self.drivenWidget) + driverMainLayout.addWidget(self.addRbfButton) + darkContainer.setLayout(driverMainLayout) + + return darkContainer + + def createDriverDrivenTableWidget(self): + tableContainer = QtWidgets.QWidget() # Setting up the main layout for driver and driven sections - driverDrivenLayout = QtWidgets.QHBoxLayout() driverDrivenTableLayout = QtWidgets.QVBoxLayout() self.driverPoseTableWidget = self.createTableWidget() self.rbfTabWidget = self.createTabWidget() - driverDrivenLayout.addLayout(driverLayout, 0) driverDrivenTableLayout.addWidget(self.driverPoseTableWidget, 1) driverDrivenTableLayout.addWidget(self.rbfTabWidget, 1) - centralSubWidgetLayout.addLayout(driverDrivenLayout, 1) - centralSubWidgetLayout.addLayout(driverDrivenTableLayout, 2) - centralWidgetLayout.addLayout(centralSubWidgetLayout, 1) + tableContainer.setLayout(driverDrivenTableLayout) + return tableContainer + + # main assebly ------------------------------------------------------------ + def createCentralWidget(self): + """main UI assembly + + Returns: + QtWidget: main UI to be parented to as the centralWidget + """ + centralWidget = QtWidgets.QWidget() + centralWidgetLayout = QtWidgets.QVBoxLayout() + centralWidget.setLayout(centralWidgetLayout) + + splitter = QtWidgets.QSplitter() + + # Setup selector section + (rbfLayout, + self.rbf_cbox, + self.rbf_refreshButton) = self.createSetupSelectorWidget() + self.rbf_cbox.setToolTip("List of available setups in the scene.") + self.rbf_refreshButton.setToolTip("Refresh the UI") + + driverDrivenWidget = self.createDarkContainerWidget() + allTableWidget = self.createDriverDrivenTableWidget() + + centralWidgetLayout.addLayout(rbfLayout) + centralWidgetLayout.addWidget(HLine()) + splitter.addWidget(driverDrivenWidget) + splitter.addWidget(allTableWidget) + centralWidgetLayout.addWidget(splitter) + + # Assuming a ratio of 2:1 for settingWidth to tableWidth + totalWidth = splitter.width() + attributeWidth = (1/3) * totalWidth + tableWidth = (2/3) * totalWidth + splitter.setSizes([int(attributeWidth), int(tableWidth)]) # Options buttons section (optionsLayout, From d8ac05adb58166e6e1346eb4a223fb25d08f9391 Mon Sep 17 00:00:00 2001 From: Joji Date: Mon, 9 Oct 2023 18:05:48 -0700 Subject: [PATCH 09/20] Added a new context menu item in QListWidget for both driver and driven. Add a new context menu item in QListWidget for both driver and driven attributes that allows setting your selected attribute to be automatically highlighted up in the next operations(the new setup). --- .../scripts/mgear/rigbits/rbf_manager_ui.py | 57 +++++++++++++++++-- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/release/scripts/mgear/rigbits/rbf_manager_ui.py b/release/scripts/mgear/rigbits/rbf_manager_ui.py index e70d2c67..c9914991 100644 --- a/release/scripts/mgear/rigbits/rbf_manager_ui.py +++ b/release/scripts/mgear/rigbits/rbf_manager_ui.py @@ -404,6 +404,8 @@ def __init__(self, parent=pyqt.maya_main_window(), hideMenuBar=False, newSceneCa self.currentRBFSetupNodes = [] self.allSetupsInfo = None self.drivenWidget = [] + self.driverAutoAttr = [] + self.drivenAutoAttr = [] self.setMenuBar(self.createMenuBar(hideMenuBar=hideMenuBar)) self.setCentralWidget(self.createCentralWidget()) @@ -893,6 +895,24 @@ def setAttributeDisplay(self, attrListWidget, driverName, displayAttrs): attrListWidget.addItems(sorted(nodeAttrsToDisplay)) self.highlightListEntries(attrListWidget, displayAttrs) + def setAttributeToAutoSelect(self, attrListWidget): + selectedItems = attrListWidget.selectedItems() + selectedTexts = [item.text() for item in selectedItems] + attributes = [attrPlug.split(".")[-1] for attrPlug in selectedTexts] + + if "driver" in attrListWidget.objectName(): + self.driverAutoAttr = attributes + elif "driven" in attrListWidget.objectName(): + self.drivenAutoAttr = attributes + print("driverAutoAttr", self.driverAutoAttr) + + @staticmethod + def setSelectedForAutoSelect(attrListWidget, itemTexts): + for i in range(attrListWidget.count()): + item = attrListWidget.item(i) + if item.text() in itemTexts: + item.setSelected(True) + def updateAttributeDisplay(self, attrListWidget, driverNames, @@ -923,6 +943,17 @@ def updateAttributeDisplay(self, nodeAttrsToDisplay = getPlugAttrs(driverNames, attrType=attrType) attrListWidget.clear() attrListWidget.addItems(nodeAttrsToDisplay) + + objName = attrListWidget.objectName() + autoAttrs = { + "driverListWidget": self.driverAutoAttr, "drivenListWidget": self.drivenAutoAttr + } + + if autoAttrs[objName]: + attrPlugs = ["{}.{}".format(driverNames[0], attr) for attr in autoAttrs[objName]] + print(attrPlugs) + self.setSelectedForAutoSelect(attrListWidget, attrPlugs) + if highlight: self.highlightListEntries(attrListWidget, highlight) @@ -1267,6 +1298,7 @@ def displayRBFSetupInfo(self, index): def attrListMenu(self, attributeListWidget, driverLineEdit, + attributeListType, QPos, nodeToQuery=None): """right click menu for queie qlistwidget @@ -1308,6 +1340,13 @@ def attrListMenu(self, nodeToQuery, attrType="all", force=True)) + + self.attrMenu.addSeparator() + + menu_item_04 = self.attrMenu.addAction("Set attribute to auto select") + menu_item_04.setToolTip("Set your attribute to be automatically highlighted up in the next operations") + menu_item_04.triggered.connect(partial(self.setAttributeToAutoSelect, + attributeListWidget)) self.attrMenu.move(parentPosition + QPos) self.attrMenu.show() @@ -1713,8 +1752,10 @@ def connectSignals(self): # Driver Line Edit and Control Line Edit self.driverLineEdit.clicked.connect(selectNode) self.controlLineEdit.clicked.connect(selectNode) - self.driverLineEdit.textChanged.connect(partial(self.updateAttributeDisplay, self.driverAttributesWidget)) - self.drivenLineEdit.textChanged.connect(partial(self.updateAttributeDisplay, self.drivenAttributesWidget)) + self.driverLineEdit.textChanged.connect(partial(self.updateAttributeDisplay, + self.driverAttributesWidget)) + self.drivenLineEdit.textChanged.connect(partial(self.updateAttributeDisplay, + self.drivenAttributesWidget)) # Table Widget header = self.driverPoseTableWidget.verticalHeader() @@ -1740,13 +1781,15 @@ def connectSignals(self): customMenu.connect( partial(self.attrListMenu, self.driverAttributesWidget, - self.driverLineEdit) + self.driverLineEdit, + "driver") ) customMenu = self.drivenAttributesWidget.customContextMenuRequested customMenu.connect( partial(self.attrListMenu, self.drivenAttributesWidget, - self.driverLineEdit) + self.driverLineEdit, + "driven") ) # Tab Widget @@ -1823,7 +1866,7 @@ def selectNodeWidget(self, label, buttonLabel="Select"): return nodeLayout, nodeLineEdit, nodeSelectButton @staticmethod - def labelListWidget(label, horizontal=True): + def labelListWidget(label, attrListType, horizontal=True): """create the listAttribute that users can select their driver/driven attributes for the setup @@ -1841,6 +1884,7 @@ def labelListWidget(label, horizontal=True): attributeLayout = QtWidgets.QVBoxLayout() attributeLabel = QtWidgets.QLabel(label) attributeListWidget = QtWidgets.QListWidget() + attributeListWidget.setObjectName("{}ListWidget".format(attrListType)) attributeLayout.addWidget(attributeLabel) attributeLayout.addWidget(attributeListWidget) return attributeLayout, attributeListWidget @@ -1882,7 +1926,7 @@ def createDriverAttributeWidget(self): allButton = self.createCustomButton("All", (20, 53), "") (attributeLayout, attributeListWidget) = self.labelListWidget( - label="Select Driver Attributes:", horizontal=False) + label="Select Driver Attributes:", attrListType="driver", horizontal=False) attributeListWidget.setToolTip("List of attributes driving setup.") selType = QtWidgets.QAbstractItemView.ExtendedSelection @@ -1927,6 +1971,7 @@ def createDrivenAttributeWidget(self): # -------------------------------------------------------------------- (attributeLayout, attributeListWidget) = self.labelListWidget(label="Select Driven Attributes:", + attrListType="driven", horizontal=False) attributeListWidget.setToolTip("Attributes being driven by setup.") attributeLayout.setSpacing(1) From cfd9cc3cd3d79fffc5765452d382a1b4e5f766ba Mon Sep 17 00:00:00 2001 From: Joji Date: Mon, 9 Oct 2023 18:09:26 -0700 Subject: [PATCH 10/20] Changed the height size of buttons and layout of pose buttons --- .../scripts/mgear/rigbits/rbf_manager_ui.py | 72 ++++++++++++------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/release/scripts/mgear/rigbits/rbf_manager_ui.py b/release/scripts/mgear/rigbits/rbf_manager_ui.py index c9914991..281aa2bd 100644 --- a/release/scripts/mgear/rigbits/rbf_manager_ui.py +++ b/release/scripts/mgear/rigbits/rbf_manager_ui.py @@ -608,6 +608,8 @@ def addRBFToSetup(self): self.populateDrivenInfo(rbfNode, rbfNode.getNodeInfo()) # Add newly created RBFNode to list of current + self.addPoseButton.setEnabled(True) + self.currentRBFSetupNodes.append(rbfNode) self.refreshRbfSetupList(setToSelection=setupName) self.lockDriverWidgets() @@ -1235,7 +1237,7 @@ def addNewDriven(self): self.setDrivenButton.blockSignals(False) self.drivenAttributesWidget.setEnabled(True) - self.addRbfButton.setText("Add new driven to RBF") + self.addRbfButton.setText("Add New Driven") def recreateDrivenTabs(self, rbfNodes): """remove tabs and create ones for each node in rbfNodes provided @@ -1252,6 +1254,8 @@ def recreateDrivenTabs(self, rbfNodes): self.populateDrivenWidgetInfo(drivenWidget, weightInfo, rbfNode) self.rbfTabWidget.addTab(drivenWidget, rbfNode.name) + self.addPoseButton.setEnabled(True) + def displayRBFSetupInfo(self, index): """Display the rbfnodes within the desired setups @@ -1411,6 +1415,7 @@ def refresh(self, clearDrivenTab (bool, optional): desired section to refresh """ self.addRbfButton.setText("New RBF") + self.addPoseButton.setEnabled(False) if rbfSelection: self.refreshRbfSetupList() if driverSelection: @@ -1818,11 +1823,12 @@ def createSetupSelectorWidget(self): return setRBFLayout, rbf_cbox, rbf_refreshButton @staticmethod - def createCustomButton(label, size=(35, 25), tooltip=""): + def createCustomButton(label, size=(35, 27), tooltip=""): stylesheet = ( "QPushButton {background-color: #5D5D5D; border-radius: 4px;}" "QPushButton:pressed { background-color: #00A6F3;}" "QPushButton:hover:!pressed { background-color: #707070;}" + "QPushButton:disabled { background-color: #4a4a4a;}" ) button = QtWidgets.QPushButton(label) button.setMinimumSize(QtCore.QSize(*size)) @@ -1954,7 +1960,7 @@ def createDrivenAttributeWidget(self): """ drivenWidget = QtWidgets.QWidget() drivenMainLayout = QtWidgets.QVBoxLayout() - drivenMainLayout.setContentsMargins(0, 10, 0, 10) + drivenMainLayout.setContentsMargins(0, 10, 0, 2) drivenMainLayout.setSpacing(9) drivenSetLayout = QtWidgets.QVBoxLayout() drivenMainLayout.addLayout(drivenSetLayout) @@ -1966,7 +1972,7 @@ def createDrivenAttributeWidget(self): drivenTip = "The node being driven by setup. (Click me!)" drivenLineEdit.setToolTip(drivenTip) - addDrivenButton = self.createCustomButton("+", (20, 25), "") + addDrivenButton = self.createCustomButton("+", (20, 26), "") addDrivenButton.setToolTip("Add a new driven to the current rbf node") # -------------------------------------------------------------------- (attributeLayout, @@ -2001,7 +2007,7 @@ def createDrivenWidget(self): """ drivenWidget = QtWidgets.QWidget() drivenMainLayout = QtWidgets.QHBoxLayout() - drivenMainLayout.setContentsMargins(0, 10, 0, 10) + drivenMainLayout.setContentsMargins(0, 0, 0, 0) drivenMainLayout.setSpacing(9) drivenWidget.setLayout(drivenMainLayout) @@ -2015,11 +2021,23 @@ def createTableWidget(self): Returns: QTableWidget: QTableWidget """ + stylesheet = """ + QTableWidget QHeaderView::section { + background-color: #3a3b3b; + padding: 2px; + text-align: center; + } + QTableCornerButton::section { + background-color: #3a3b3b; + border: none; + } + """ tableWidget = QtWidgets.QTableWidget() tableWidget.insertColumn(0) tableWidget.insertRow(0) tableWidget.setHorizontalHeaderLabels(["Pose Value"]) tableWidget.setVerticalHeaderLabels(["Pose #0"]) + # tableWidget.setStyleSheet(stylesheet) tableTip = "Live connections to the RBF Node in your setup." tableTip = tableTip + "\nSelect the desired Pose # to recall pose." tableWidget.setToolTip(tableTip) @@ -2049,12 +2067,12 @@ def createOptionsButtonsWidget(self): optionsLayout.setSpacing(5) addTip = "After positioning all controls in the setup, add new pose." addTip = addTip + "\nEnsure the driver node has a unique position." - addPoseButton = self.createCustomButton("Add Pose", (80, 28), addTip) - EditPoseButton = self.createCustomButton("Update Pose", (80, 28), "") + addPoseButton = self.createCustomButton("Add Pose", size=(80, 26), tooltip=addTip) + EditPoseButton = self.createCustomButton("Update Pose", size=(80, 26)) EditPoseButton.setToolTip("Recall pose, adjust controls and Edit.") - EditPoseValuesButton = self.createCustomButton("Update Pose Values", (80, 28), "") + EditPoseValuesButton = self.createCustomButton("Update Pose Values", size=(80, 26)) EditPoseValuesButton.setToolTip("Set pose based on values in table") - deletePoseButton = self.createCustomButton("Delete Pose", (80, 28), "") + deletePoseButton = self.createCustomButton("Delete Pose", size=(80, 26)) deletePoseButton.setToolTip("Recall pose, then Delete") optionsLayout.addWidget(addPoseButton) optionsLayout.addWidget(EditPoseButton) @@ -2130,7 +2148,7 @@ def createDarkContainerWidget(self): driverMainLayout.setSpacing(5) # Adjust spacing between widgets # Setting the dark color (Example: dark gray) - # darkContainer.setStyleSheet("background-color: rgb(40, 40, 40);") + # darkContainer.setStyleSheet("background-color: #323232;") # Driver section (self.controlLineEdit, @@ -2150,10 +2168,13 @@ def createDarkContainerWidget(self): self.drivenWidget, self.drivenMainLayout) = self.createDrivenAttributeWidget() - self.addRbfButton = QtWidgets.QPushButton("New RBF") + self.addRbfButton = self.createCustomButton("New RBF") self.addRbfButton.setToolTip("Select node to be driven by setup.") - self.addRbfButton.setFixedHeight(self.genericWidgetHight) - self.addRbfButton.setStyleSheet("background-color: rgb(23, 158, 131)") + stylesheet = ( + "QPushButton {background-color: #179e83; border-radius: 4px;}" + "QPushButton:hover:!pressed { background-color: #2ea88f;}" + ) + self.addRbfButton.setStyleSheet(stylesheet) # Setting up the main layout for driver and driven sections driverMainLayout = QtWidgets.QVBoxLayout() @@ -2173,9 +2194,22 @@ def createDriverDrivenTableWidget(self): self.driverPoseTableWidget = self.createTableWidget() self.rbfTabWidget = self.createTabWidget() + # Options buttons section + (optionsLayout, + self.addPoseButton, + self.editPoseButton, + self.editPoseValuesButton, + self.deletePoseButton) = self.createOptionsButtonsWidget() + self.addPoseButton.setEnabled(False) + self.editPoseButton.setEnabled(False) + self.editPoseValuesButton.setEnabled(False) + self.deletePoseButton.setEnabled(False) + driverDrivenTableLayout.addWidget(self.driverPoseTableWidget, 1) driverDrivenTableLayout.addWidget(self.rbfTabWidget, 1) + driverDrivenTableLayout.addLayout(optionsLayout) tableContainer.setLayout(driverDrivenTableLayout) + return tableContainer # main assebly ------------------------------------------------------------ @@ -2212,18 +2246,6 @@ def createCentralWidget(self): attributeWidth = (1/3) * totalWidth tableWidth = (2/3) * totalWidth splitter.setSizes([int(attributeWidth), int(tableWidth)]) - - # Options buttons section - (optionsLayout, - self.addPoseButton, - self.editPoseButton, - self.editPoseValuesButton, - self.deletePoseButton) = self.createOptionsButtonsWidget() - self.editPoseButton.setEnabled(False) - self.editPoseValuesButton.setEnabled(False) - self.deletePoseButton.setEnabled(False) - centralWidgetLayout.addWidget(HLine()) - centralWidgetLayout.addLayout(optionsLayout) return centralWidget # overrides --------------------------------------------------------------- From 41458b8b47eebd08e7c580f097eea6935d12c87d Mon Sep 17 00:00:00 2001 From: Joji Date: Mon, 9 Oct 2023 18:11:26 -0700 Subject: [PATCH 11/20] Fixed a bug where a new driven tab couldn't be generated --- release/scripts/mgear/rigbits/rbf_manager_ui.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/release/scripts/mgear/rigbits/rbf_manager_ui.py b/release/scripts/mgear/rigbits/rbf_manager_ui.py index 281aa2bd..93ee2b7d 100644 --- a/release/scripts/mgear/rigbits/rbf_manager_ui.py +++ b/release/scripts/mgear/rigbits/rbf_manager_ui.py @@ -597,6 +597,8 @@ def addRBFToSetup(self): currentRbfs = self.currentRBFSetupNodes[0] print("Syncing poses indices from {} >> {}".format(currentRbfs, rbfNode)) rbfNode.syncPoseIndices(self.currentRBFSetupNodes[0]) + self.addNewTab(currentRbfs, drivenNode) + self.setAttributeDisplay(self.drivenAttributesWidget, drivenNode, drivenAttrs) else: if self.zeroedDefaults: rbfNode.applyDefaultPose() @@ -1211,7 +1213,7 @@ def populateDrivenWidgetInfo(self, drivenWidget, weightInfo, rbfNode): self.setDrivenTable(drivenWidget, rbfNode, weightInfo) - def addNewTab(self, rbfNode): + def addNewTab(self, rbfNode, drivenNode): """Create a new tab in the setup Args: @@ -1220,9 +1222,13 @@ def addNewTab(self, rbfNode): Returns: QWidget: created widget """ + weightInfo = rbfNode.getNodeInfo() tabDrivenWidget = self.createAndTagDrivenWidget() self._associateRBFnodeAndWidget(tabDrivenWidget, rbfNode) - self.rbfTabWidget.addTab(tabDrivenWidget, str(rbfNode)) + self.rbfTabWidget.addTab(tabDrivenWidget, drivenNode) + tabDrivenWidget.setProperty("drivenNode", drivenNode) + self.setDrivenTable(tabDrivenWidget, rbfNode, weightInfo) + return tabDrivenWidget def addNewDriven(self): @@ -1428,7 +1434,9 @@ def refresh(self, self.drivenLineEdit.clear() self.drivenAttributesWidget.clear() if clearDrivenTab: - self.clearDrivenTabs() + self.rbfTabWidget.clear() + self.__deleteAssociatedWidgetsMaya(self.rbfTabWidget) + self.__deleteAssociatedWidgets(self.rbfTabWidget) if currentRBFSetupNodes: self.currentRBFSetupNodes = [] From d1af20ebb26321231d101590e913e70847bd8b91 Mon Sep 17 00:00:00 2001 From: Joji Date: Mon, 9 Oct 2023 18:15:23 -0700 Subject: [PATCH 12/20] Updated to version 1.0.9 --- release/scripts/mgear/rigbits/rbf_manager_ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release/scripts/mgear/rigbits/rbf_manager_ui.py b/release/scripts/mgear/rigbits/rbf_manager_ui.py index 93ee2b7d..ad90d37e 100644 --- a/release/scripts/mgear/rigbits/rbf_manager_ui.py +++ b/release/scripts/mgear/rigbits/rbf_manager_ui.py @@ -85,7 +85,7 @@ # ============================================================================= # Constants # ============================================================================= -__version__ = "1.0.8" +__version__ = "1.0.9" _mgear_version = mgear.getVersion() TOOL_NAME = "RBF Manager" From e09da119d2ab1a6641328cc48da569c3da42f7d5 Mon Sep 17 00:00:00 2001 From: Joji Date: Mon, 9 Oct 2023 23:43:12 -0700 Subject: [PATCH 13/20] Refactored the entire codebase to improve readability and maintainability. --- .../scripts/mgear/rigbits/rbf_manager_ui.py | 1348 +++++++++-------- 1 file changed, 678 insertions(+), 670 deletions(-) diff --git a/release/scripts/mgear/rigbits/rbf_manager_ui.py b/release/scripts/mgear/rigbits/rbf_manager_ui.py index ad90d37e..d580741b 100644 --- a/release/scripts/mgear/rigbits/rbf_manager_ui.py +++ b/release/scripts/mgear/rigbits/rbf_manager_ui.py @@ -126,6 +126,9 @@ def getPlugAttrs(nodes, attrType="keyable"): list: list of attrplugs """ plugAttrs = [] + if len(nodes) >= 2: + print("the number of node is more than two") + for node in nodes: if attrType == "all": attrs = mc.listAttr(node, se=True, u=False) @@ -370,173 +373,582 @@ def tabSizeHint(self, index): return QtCore.QSize(width, 25) -class RBFManagerUI(MayaQWidgetDockableMixin, QtWidgets.QMainWindow): - - """A manager for creating, mirroring, importing/exporting poses created - for RBF type nodes. - - Attributes: - absWorld (bool): Type of pose info look up, world vs local - addRbfButton (QPushButton): button for adding RBFs to setup - allSetupsInfo (dict): setupName:[of all the RBFNodes in scene] - attrMenu (TYPE): Description - currentRBFSetupNodes (list): currently selected setup nodes(userSelect) - driverPoseTableWidget (QTableWidget): poseInfo for the driver node - genericWidgetHight (int): convenience to adjust height of all buttons - mousePosition (QPose): if tracking mouse position on UI - rbfTabWidget (QTabWidget): where the driven table node info is - displayed - """ +class RBFWidget(MayaQWidgetDockableMixin, QtWidgets.QMainWindow): - mousePosition = QtCore.Signal(int, int) + def __init__(self, parent=pyqt.maya_main_window()): + super(RBFWidget, self).__init__(parent=parent) - def __init__(self, parent=pyqt.maya_main_window(), hideMenuBar=False, newSceneCallBack=True): - super(RBFManagerUI, self).__init__(parent=parent) # UI info ------------------------------------------------------------- self.callBackID = None self.setWindowTitle(TOOL_TITLE) self.setObjectName(UI_NAME) self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) self.genericWidgetHight = 24 - # class info ---------------------------------------------------------- - self.absWorld = True - self.zeroedDefaults = True - self.currentRBFSetupNodes = [] - self.allSetupsInfo = None - self.drivenWidget = [] - self.driverAutoAttr = [] - self.drivenAutoAttr = [] - self.setMenuBar(self.createMenuBar(hideMenuBar=hideMenuBar)) - self.setCentralWidget(self.createCentralWidget()) - self.centralWidget().setMouseTracking(True) - self.refreshRbfSetupList() - self.connectSignals() - # added because the dockableMixin makes the ui appear small - self.adjustSize() - self.resize(800, 650) - if newSceneCallBack: - self.newSceneCallBack() + @staticmethod + def deleteAssociatedWidgetsMaya(widget, attrName="associatedMaya"): + """delete core ui items 'associated' with the provided widgets - def closeEvent(self, event): - """Overridden close event to save the size of the UI.""" - width = self.width() - height = self.height() + Args: + widget (QWidget): Widget that has the associated attr set + attrName (str, optional): class attr to query + """ + if hasattr(widget, attrName): + for t in getattr(widget, attrName): + try: + mc.deleteUI(t, ctl=True) + except Exception: + pass + else: + setattr(widget, attrName, []) - # Save the size to Maya's optionVars - mc.optionVar(intValue=('RBF_UI_width', width)) - mc.optionVar(intValue=('RBF_UI_height', height)) + @staticmethod + def deleteAssociatedWidgets(widget, attrName="associated"): + """delete widget items 'associated' with the provided widgets - # Call the parent class's closeEvent - super(RBFManagerUI, self).closeEvent(event) + Args: + widget (QWidget): Widget that has the associated attr set + attrName (str, optional): class attr to query + """ + if hasattr(widget, attrName): + for t in getattr(widget, attrName): + try: + t.deleteLater() + except Exception: + pass + else: + setattr(widget, attrName, []) - def callBackFunc(self, *args): - """super safe function for trying to refresh the UI, should anything - fail. + @staticmethod + def _associateRBFnodeAndWidget(tabDrivenWidget, rbfNode): + """associates the RBFNode with a widget for convenience when adding, + deleting, editing Args: - *args: Description + tabDrivenWidget (QWidget): tab widget + rbfNode (RBFNode): instance to be associated """ - try: - self.refresh() - except Exception: - pass + setattr(tabDrivenWidget, "rbfNode", rbfNode) - def removeSceneCallback(self): - """remove the callback associated witht he UI, quietly fail. - """ - try: - om.MSceneMessage.removeCallback(self.callBackID) - except Exception as e: - print("CallBack removal failure:") - print(e) + @staticmethod + def createCustomButton(label, size=(35, 27), tooltip=""): + stylesheet = ( + "QPushButton {background-color: #5D5D5D; border-radius: 4px;}" + "QPushButton:pressed { background-color: #00A6F3;}" + "QPushButton:hover:!pressed { background-color: #707070;}" + "QPushButton:disabled { background-color: #4a4a4a;}" + ) + button = QtWidgets.QPushButton(label) + button.setMinimumSize(QtCore.QSize(*size)) + button.setStyleSheet(stylesheet) + button.setToolTip(tooltip) + return button - def newSceneCallBack(self): - """create a new scene callback to refresh the UI when scene changes. - """ - callBackType = om.MSceneMessage.kSceneUpdate - try: - func = self.callBackFunc - obj = om.MSceneMessage.addCallback(callBackType, func) - self.callBackID = obj - except Exception as e: - print(e) - self.callBackID = None + @staticmethod + def createSetupSelector2Widget(): + rbfVLayout = QtWidgets.QVBoxLayout() + rbfListWidget = QtWidgets.QListWidget() + rbfVLayout.addWidget(rbfListWidget) + return rbfVLayout, rbfListWidget - # general functions ------------------------------------------------------- - def getSelectedSetup(self): - """return the string name of the selected setup from user and type + @staticmethod + def labelListWidget(label, attrListType, horizontal=True): + """create the listAttribute that users can select their driver/driven + attributes for the setup + + Args: + label (str): to display above the listWidget + horizontal (bool, optional): should the label be above or infront + of the listWidget Returns: - str, str: name, nodeType + list: QLayout, QListWidget """ - selectedSetup = self.rbf_cbox.currentText() - if selectedSetup.startswith("New"): - setupType = selectedSetup.split(" ")[1] - return None, setupType + if horizontal: + attributeLayout = QtWidgets.QHBoxLayout() else: - return selectedSetup, self.currentRBFSetupNodes[0].rbfType + attributeLayout = QtWidgets.QVBoxLayout() + attributeLabel = QtWidgets.QLabel(label) + attributeListWidget = QtWidgets.QListWidget() + attributeListWidget.setObjectName("{}ListWidget".format(attrListType)) + attributeLayout.addWidget(attributeLabel) + attributeLayout.addWidget(attributeListWidget) + return attributeLayout, attributeListWidget - def getDrivenNodesFromSetup(self): - """based on the user selected setup, get the associated RBF nodes + @staticmethod + def addRemoveButtonWidget(label1, label2, horizontal=True): + if horizontal: + addRemoveLayout = QtWidgets.QHBoxLayout() + else: + addRemoveLayout = QtWidgets.QVBoxLayout() + addAttributesButton = QtWidgets.QPushButton(label1) + removeAttributesButton = QtWidgets.QPushButton(label2) + addRemoveLayout.addWidget(addAttributesButton) + addRemoveLayout.addWidget(removeAttributesButton) + return addRemoveLayout, addAttributesButton, removeAttributesButton - Returns: - list: driven rbfnodes + def selectNodeWidget(self, label, buttonLabel="Select"): + """create a lout with label, lineEdit, QPushbutton for user input """ - drivenNodes = [] - for rbfNode in self.currentRBFSetupNodes: - drivenNodes.extend(rbfNode.getDrivenNode) - return drivenNodes + stylesheet = ( + "QLineEdit { background-color: #404040;" + "border-radius: 4px;" + "border-color: #505050;" + "border-style: solid;" + "border-width: 1.4px;}" + ) - def __deleteSetup(self): - decision = promptAcceptance(self, - "Delete current Setup?", - "This will delete all RBF nodes in setup.") - if decision in [QtWidgets.QMessageBox.Discard, - QtWidgets.QMessageBox.Cancel]: - return - self.deleteSetup() + nodeLayout = QtWidgets.QHBoxLayout() + nodeLayout.setSpacing(4) - def deleteSetup(self, setupName=None): - """Delete all the nodes within a setup. + nodeLabel = QtWidgets.QLabel(label) + nodeLabel.setFixedWidth(40) + nodeLineEdit = ClickableLineEdit() + nodeLineEdit.setStyleSheet(stylesheet) + nodeLineEdit.setReadOnly(True) + nodeSelectButton = self.createCustomButton(buttonLabel) + nodeSelectButton.setFixedWidth(40) + nodeLineEdit.setFixedHeight(self.genericWidgetHight) + nodeSelectButton.setFixedHeight(self.genericWidgetHight) + nodeLayout.addWidget(nodeLabel) + nodeLayout.addWidget(nodeLineEdit, 1) + nodeLayout.addWidget(nodeSelectButton) + return nodeLayout, nodeLineEdit, nodeSelectButton - Args: - setupName (None, optional): Description - """ - setupType = None - if setupName is None: - setupName, setupType = self.getSelectedSetup() - nodesToDelete = self.allSetupsInfo.get(setupName, []) - for rbfNode in nodesToDelete: - drivenNode = rbfNode.getDrivenNode() - rbfNode.deleteRBFToggleAttr() - if drivenNode: - rbf_node.removeDrivenGroup(drivenNode[0]) - mc.delete(rbfNode.transformNode) - self.refresh() + def createSetupSelectorWidget(self): + """create the top portion of the weidget, select setup + refresh - def removeRBFFromSetup(self, drivenWidgetIndex): - """remove RBF tab from setup. Delete driven group, attrs and clean up + Returns: + list: QLayout, QCombobox, QPushButton + """ + setRBFLayout = QtWidgets.QHBoxLayout() + rbfLabel = QtWidgets.QLabel("Select RBF Setup:") + rbf_cbox = QtWidgets.QComboBox() + rbf_refreshButton = self.createCustomButton("Refresh", (60, 25), "Refresh the UI") + rbf_cbox.setFixedHeight(self.genericWidgetHight) + rbf_refreshButton.setMaximumWidth(80) + rbf_refreshButton.setFixedHeight(self.genericWidgetHight - 1) + setRBFLayout.addWidget(rbfLabel) + setRBFLayout.addWidget(rbf_cbox, 1) + setRBFLayout.addWidget(rbf_refreshButton) + return setRBFLayout, rbf_cbox, rbf_refreshButton - Args: - drivenWidgetIndex (QWidget): parent widget that houses the contents - and info of the rbf node + def createDriverAttributeWidget(self): + """widget where the user inputs information for the setups Returns: - n/a: n/a + list: [of widgets] """ - decision = promptAcceptance(self, - "Are you sure you want to remove node?", - "This will delete the RBF & driven node.") - if decision in [QtWidgets.QMessageBox.Discard, - QtWidgets.QMessageBox.Cancel]: - return - drivenWidget = self.rbfTabWidget.widget(drivenWidgetIndex) - self.rbfTabWidget.removeTab(drivenWidgetIndex) - rbfNode = getattr(drivenWidget, "rbfNode") - self.__deleteAssociatedWidgets(drivenWidget, attrName="associated") - drivenWidget.deleteLater() - drivenNode = rbfNode.getDrivenNode() + driverControlVLayout = QtWidgets.QVBoxLayout() + driverControlHLayout = QtWidgets.QHBoxLayout() + + # driverMainLayout.setStyleSheet("QVBoxLayout { background-color: #404040;") + driverControlHLayout.setSpacing(3) + # -------------------------------------------------------------------- + (controlLayout, + controlLineEdit, + setControlButton) = self.selectNodeWidget("Control", buttonLabel="Set") + controlLineEdit.setToolTip("The node driving the setup. (Click me!)") + # -------------------------------------------------------------------- + (driverLayout, + driverLineEdit, + driverSelectButton) = self.selectNodeWidget("Driver", buttonLabel="Set") + driverLineEdit.setToolTip("The node driving the setup. (Click me!)") + # -------------------------------------------------------------------- + allButton = self.createCustomButton("All", (20, 53), "") + + (attributeLayout, attributeListWidget) = self.labelListWidget( + label="Select Driver Attributes:", attrListType="driver", horizontal=False) + + attributeListWidget.setToolTip("List of attributes driving setup.") + selType = QtWidgets.QAbstractItemView.ExtendedSelection + attributeListWidget.setSelectionMode(selType) + attributeListWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + # -------------------------------------------------------------------- + driverControlVLayout.addLayout(controlLayout, 0) + driverControlVLayout.addLayout(driverLayout, 0) + driverControlHLayout.addLayout(driverControlVLayout, 0) + driverControlHLayout.addWidget(allButton, 0) + return [controlLineEdit, + setControlButton, + driverLineEdit, + driverSelectButton, + allButton, + attributeListWidget, + attributeLayout, + driverControlHLayout] + + def createDrivenAttributeWidget(self): + """the widget that displays the driven information + + Returns: + list: [of widgets] + """ + drivenWidget = QtWidgets.QWidget() + drivenMainLayout = QtWidgets.QVBoxLayout() + drivenMainLayout.setContentsMargins(0, 10, 0, 2) + drivenMainLayout.setSpacing(9) + drivenSetLayout = QtWidgets.QVBoxLayout() + drivenMainLayout.addLayout(drivenSetLayout) + drivenWidget.setLayout(drivenMainLayout) + # -------------------------------------------------------------------- + (drivenLayout, + drivenLineEdit, + drivenSelectButton) = self.selectNodeWidget("Driven", buttonLabel="Set") + drivenTip = "The node being driven by setup. (Click me!)" + drivenLineEdit.setToolTip(drivenTip) + + addDrivenButton = self.createCustomButton("+", (20, 26), "") + addDrivenButton.setToolTip("Add a new driven to the current rbf node") + # -------------------------------------------------------------------- + (attributeLayout, + attributeListWidget) = self.labelListWidget(label="Select Driven Attributes:", + attrListType="driven", + horizontal=False) + attributeListWidget.setToolTip("Attributes being driven by setup.") + attributeLayout.setSpacing(1) + selType = QtWidgets.QAbstractItemView.ExtendedSelection + attributeListWidget.setSelectionMode(selType) + attributeListWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + # -------------------------------------------------------------------- + drivenSetLayout.addLayout(drivenLayout, 0) + drivenSetLayout.addLayout(attributeLayout, 0) + drivenLayout.addWidget(addDrivenButton) + drivenSetLayout.addLayout(drivenLayout, 0) + drivenSetLayout.addLayout(attributeLayout, 0) + drivenMainLayout.addLayout(drivenSetLayout) + drivenWidget.setLayout(drivenMainLayout) + return [drivenLineEdit, + drivenSelectButton, + addDrivenButton, + attributeListWidget, + drivenWidget, + drivenMainLayout] + + def createDrivenWidget(self): + """the widget that displays the driven information + + Returns: + list: [of widgets] + """ + drivenWidget = QtWidgets.QWidget() + drivenMainLayout = QtWidgets.QHBoxLayout() + drivenMainLayout.setContentsMargins(0, 0, 0, 0) + drivenMainLayout.setSpacing(9) + drivenWidget.setLayout(drivenMainLayout) + + tableWidget = self.createTableWidget() + drivenMainLayout.addWidget(tableWidget, 1) + return drivenWidget, tableWidget + + def createTableWidget(self): + """create table widget used to display poses, set tooltips and colum + + Returns: + QTableWidget: QTableWidget + """ + stylesheet = """ + QTableWidget QHeaderView::section { + background-color: #3a3b3b; + padding: 2px; + text-align: center; + } + QTableCornerButton::section { + background-color: #3a3b3b; + border: none; + } + """ + tableWidget = QtWidgets.QTableWidget() + tableWidget.insertColumn(0) + tableWidget.insertRow(0) + tableWidget.setHorizontalHeaderLabels(["Pose Value"]) + tableWidget.setVerticalHeaderLabels(["Pose #0"]) + # tableWidget.setStyleSheet(stylesheet) + tableTip = "Live connections to the RBF Node in your setup." + tableTip = tableTip + "\nSelect the desired Pose # to recall pose." + tableWidget.setToolTip(tableTip) + return tableWidget + + def createTabWidget(self): + """Tab widget to add driven widgets too. Custom TabBar so the tab is + easier to select + + Returns: + QTabWidget: + """ + tabLayout = QtWidgets.QTabWidget() + tabLayout.setContentsMargins(0, 0, 0, 0) + tabBar = TabBar() + tabLayout.setTabBar(tabBar) + tabBar.setTabsClosable(True) + return tabLayout + + def createOptionsButtonsWidget(self): + """add, edit, delete buttons for modifying rbf setups. + + Returns: + list: [QPushButtons] + """ + optionsLayout = QtWidgets.QHBoxLayout() + optionsLayout.setSpacing(5) + addTip = "After positioning all controls in the setup, add new pose." + addTip = addTip + "\nEnsure the driver node has a unique position." + addPoseButton = self.createCustomButton("Add Pose", size=(80, 26), tooltip=addTip) + EditPoseButton = self.createCustomButton("Update Pose", size=(80, 26)) + EditPoseButton.setToolTip("Recall pose, adjust controls and Edit.") + EditPoseValuesButton = self.createCustomButton("Update Pose Values", size=(80, 26)) + EditPoseValuesButton.setToolTip("Set pose based on values in table") + deletePoseButton = self.createCustomButton("Delete Pose", size=(80, 26)) + deletePoseButton.setToolTip("Recall pose, then Delete") + optionsLayout.addWidget(addPoseButton) + optionsLayout.addWidget(EditPoseButton) + optionsLayout.addWidget(EditPoseValuesButton) + optionsLayout.addWidget(deletePoseButton) + return (optionsLayout, + addPoseButton, + EditPoseButton, + EditPoseValuesButton, + deletePoseButton) + + def createDarkContainerWidget(self): + darkContainer = QtWidgets.QWidget() + driverMainLayout = QtWidgets.QVBoxLayout() + driverMainLayout.setContentsMargins(10, 10, 10, 10) # Adjust margins as needed + driverMainLayout.setSpacing(5) # Adjust spacing between widgets + + # Setting the dark color (Example: dark gray) + # darkContainer.setStyleSheet("background-color: #323232;") + + # Driver section + (self.controlLineEdit, + self.setControlButton, + self.driverLineEdit, + self.setDriverButton, + self.allButton, + self.driverAttributesWidget, + self.driverAttributesLayout, + driverControlLayout) = self.createDriverAttributeWidget() + + # Driven section + (self.drivenLineEdit, + self.setDrivenButton, + self.addDrivenButton, + self.drivenAttributesWidget, + self.drivenWidget, + self.drivenMainLayout) = self.createDrivenAttributeWidget() + + self.addRbfButton = self.createCustomButton("New RBF") + self.addRbfButton.setToolTip("Select node to be driven by setup.") + stylesheet = ( + "QPushButton {background-color: #179e83; border-radius: 4px;}" + "QPushButton:hover:!pressed { background-color: #2ea88f;}" + ) + self.addRbfButton.setStyleSheet(stylesheet) + + # Setting up the main layout for driver and driven sections + driverMainLayout = QtWidgets.QVBoxLayout() + driverMainLayout.addLayout(driverControlLayout) + driverMainLayout.addLayout(self.driverAttributesLayout) + driverMainLayout.addWidget(self.drivenWidget) + driverMainLayout.addWidget(self.addRbfButton) + darkContainer.setLayout(driverMainLayout) + + return darkContainer + + def createDriverDrivenTableWidget(self): + tableContainer = QtWidgets.QWidget() + + # Setting up the main layout for driver and driven sections + driverDrivenTableLayout = QtWidgets.QVBoxLayout() + self.driverPoseTableWidget = self.createTableWidget() + self.rbfTabWidget = self.createTabWidget() + + # Options buttons section + (optionsLayout, + self.addPoseButton, + self.editPoseButton, + self.editPoseValuesButton, + self.deletePoseButton) = self.createOptionsButtonsWidget() + self.addPoseButton.setEnabled(False) + self.editPoseButton.setEnabled(False) + self.editPoseValuesButton.setEnabled(False) + self.deletePoseButton.setEnabled(False) + + driverDrivenTableLayout.addWidget(self.driverPoseTableWidget, 1) + driverDrivenTableLayout.addWidget(self.rbfTabWidget, 1) + driverDrivenTableLayout.addLayout(optionsLayout) + tableContainer.setLayout(driverDrivenTableLayout) + + return tableContainer + + +class RBFTables(RBFWidget): + + def __init__(self): + pass + + +class RBFManagerUI(RBFWidget): + + """A manager for creating, mirroring, importing/exporting poses created + for RBF type nodes. + + Attributes: + absWorld (bool): Type of pose info look up, world vs local + addRbfButton (QPushButton): button for adding RBFs to setup + allSetupsInfo (dict): setupName:[of all the RBFNodes in scene] + attrMenu (TYPE): Description + currentRBFSetupNodes (list): currently selected setup nodes(userSelect) + driverPoseTableWidget (QTableWidget): poseInfo for the driver node + genericWidgetHight (int): convenience to adjust height of all buttons + mousePosition (QPose): if tracking mouse position on UI + rbfTabWidget (QTabWidget): where the driven table node info is + displayed + """ + + mousePosition = QtCore.Signal(int, int) + + def __init__(self, hideMenuBar=False, newSceneCallBack=True): + super(RBFManagerUI, self).__init__() + + self.absWorld = True + self.zeroedDefaults = True + self.currentRBFSetupNodes = [] + self.allSetupsInfo = None + self.drivenWidget = [] + self.driverAutoAttr = [] + self.drivenAutoAttr = [] + + self.setMenuBar(self.createMenuBar(hideMenuBar=hideMenuBar)) + self.setCentralWidget(self.createCentralWidget()) + self.centralWidget().setMouseTracking(True) + self.refreshRbfSetupList() + self.connectSignals() + # added because the dockableMixin makes the ui appear small + self.adjustSize() + self.resize(800, 650) + if newSceneCallBack: + self.newSceneCallBack() + + def closeEvent(self, event): + """Overridden close event to save the size of the UI.""" + width = self.width() + height = self.height() + + # Save the size to Maya's optionVars + mc.optionVar(intValue=('RBF_UI_width', width)) + mc.optionVar(intValue=('RBF_UI_height', height)) + + # Call the parent class's closeEvent + super(RBFManagerUI, self).closeEvent(event) + + def callBackFunc(self, *args): + """super safe function for trying to refresh the UI, should anything + fail. + + Args: + *args: Description + """ + try: + self.refresh() + except Exception: + pass + + def removeSceneCallback(self): + """remove the callback associated witht he UI, quietly fail. + """ + try: + om.MSceneMessage.removeCallback(self.callBackID) + except Exception as e: + print("CallBack removal failure:") + print(e) + + def newSceneCallBack(self): + """create a new scene callback to refresh the UI when scene changes. + """ + callBackType = om.MSceneMessage.kSceneUpdate + try: + func = self.callBackFunc + obj = om.MSceneMessage.addCallback(callBackType, func) + self.callBackID = obj + except Exception as e: + print(e) + self.callBackID = None + + # general functions ------------------------------------------------------- + def getSelectedSetup(self): + """return the string name of the selected setup from user and type + + Returns: + str, str: name, nodeType + """ + selectedSetup = self.rbf_cbox.currentText() + if selectedSetup.startswith("New"): + setupType = selectedSetup.split(" ")[1] + return None, setupType + else: + return selectedSetup, self.currentRBFSetupNodes[0].rbfType + + def getDrivenNodesFromSetup(self): + """based on the user selected setup, get the associated RBF nodes + + Returns: + list: driven rbfnodes + """ + drivenNodes = [] + for rbfNode in self.currentRBFSetupNodes: + drivenNodes.extend(rbfNode.getDrivenNode) + return drivenNodes + + def __deleteSetup(self): + decision = promptAcceptance(self, + "Delete current Setup?", + "This will delete all RBF nodes in setup.") + if decision in [QtWidgets.QMessageBox.Discard, + QtWidgets.QMessageBox.Cancel]: + return + self.deleteSetup() + + def deleteSetup(self, setupName=None): + """Delete all the nodes within a setup. + + Args: + setupName (None, optional): Description + """ + setupType = None + if setupName is None: + setupName, setupType = self.getSelectedSetup() + nodesToDelete = self.allSetupsInfo.get(setupName, []) + for rbfNode in nodesToDelete: + drivenNode = rbfNode.getDrivenNode() + rbfNode.deleteRBFToggleAttr() + if drivenNode: + rbf_node.removeDrivenGroup(drivenNode[0]) + mc.delete(rbfNode.transformNode) + self.refresh() + + def removeRBFFromSetup(self, drivenWidgetIndex): + """remove RBF tab from setup. Delete driven group, attrs and clean up + + Args: + drivenWidgetIndex (QWidget): parent widget that houses the contents + and info of the rbf node + + Returns: + n/a: n/a + """ + decision = promptAcceptance(self, + "Are you sure you want to remove node?", + "This will delete the RBF & driven node.") + if decision in [QtWidgets.QMessageBox.Discard, + QtWidgets.QMessageBox.Cancel]: + return + drivenWidget = self.rbfTabWidget.widget(drivenWidgetIndex) + self.rbfTabWidget.removeTab(drivenWidgetIndex) + rbfNode = getattr(drivenWidget, "rbfNode") + self.deleteAssociatedWidgets(drivenWidget, attrName="associated") + drivenWidget.deleteLater() + drivenNode = rbfNode.getDrivenNode() rbfNode.deleteRBFToggleAttr() if drivenNode and drivenNode[0].endswith(rbf_node.DRIVEN_SUFFIX): rbf_node.removeDrivenGroup(drivenNode[0]) @@ -657,22 +1069,20 @@ def preValidationCheck(self): return result def refreshAllTables(self): - """Convenience function to refresh all the tables on all the tabs - with latest information. + """Refresh all tables on all the tabs with the latest information """ - weightInfo = None - rbfNode = None + # Iterate through each tab in the widget for index in range(self.rbfTabWidget.count()): drivenWidget = self.rbfTabWidget.widget(index) drivenNodeName = drivenWidget.property("drivenNode") + + # Update table if the rbfNode's drivenNode matches the current tab's drivenNode for rbfNode in self.currentRBFSetupNodes: drivenNodes = rbfNode.getDrivenNode() - if drivenNodes and drivenNodes[0] != drivenNodeName: - continue - weightInfo = rbfNode.getNodeInfo() - self.setDrivenTable(drivenWidget, rbfNode, weightInfo) - if weightInfo and rbfNode: - self.populateDriverInfo(rbfNode, weightInfo) + if drivenNodes and drivenNodes[0] == drivenNodeName: + weightInfo = rbfNode.getNodeInfo() + self.setDrivenTable(drivenWidget, rbfNode, weightInfo) + self.populateDriverInfo(rbfNode, weightInfo) @staticmethod def determineAttrType(node): @@ -755,12 +1165,9 @@ def editPoseValues(self): driverNode = rbfNodes[0].getDriverNode()[0] driverAttrs = rbfNodes[0].getDriverNodeAttributes() poseInputs = rbf_node.getMultipleAttrs(driverNode, driverAttrs) - # print("poseInputs: " + str(poseInputs)) - # print("RBF nodes: " + str(rbfNodes)) nColumns = drivenTableWidget.columnCount() entryWidgets = [drivenTableWidget.cellWidget(drivenRow, c) for c in range(nColumns)] newValues = [float(w.text()) for w in entryWidgets] - # print("values: " + str(newValues)) rbfNode = getattr(drivenWidget, "rbfNode") rbfNodes = [rbfNode] for rbfNode in rbfNodes: @@ -775,42 +1182,38 @@ def editPoseValues(self): self.refreshAllTables() def updateAllFromTables(self): - """Update every pose - - Args: - rbfNode (RBFNode): node for query - weightInfo (dict): to pull information from, since we have it + """Update every pose for the RBF nodes based on the values from the tables. """ rbfNodes = self.currentRBFSetupNodes if not rbfNodes: return - for w in self.rbfTabWidget.count(): - drivenWidget = self.rbfTabWidget.widget(w) - drivenTableWidget = getattr(drivenWidget, "tableWidget") - drivenRow = drivenTableWidget.currentRow() + + # Get common data for all RBF nodes driverNode = rbfNodes[0].getDriverNode()[0] driverAttrs = rbfNodes[0].getDriverNodeAttributes() poseInputs = rbf_node.getMultipleAttrs(driverNode, driverAttrs) - # print("poseInputs: " + str(poseInputs)) - # print("RBF nodes: " + str(rbfNodes)) - nColumns = drivenTableWidget.columnCount() - entryWidgets = [drivenTableWidget.cellWidget(drivenRow, c) for c in range(nColumns)] - newValues = [float(w.text()) for w in entryWidgets] - # print("values: " + str(newValues)) - rbfNode = getattr(drivenWidget, "rbfNode") - rbfNodes = [rbfNode] - for rbfNode in rbfNodes: - # poseValues = rbfNode.getPoseValues() - # print("Old pose values: " + str(poseValues)) - # print("New pose values: " + str(newValues)) - print("rbfNode: " + str(rbfNode)) - print("poseInputs: " + str(poseInputs)) - print("New pose values: " + str(newValues)) - print("poseIndex: " + str(drivenRow)) + + # Iterate over all widgets in the tab widget + for idx in range(self.rbfTabWidget.count()): + drivenWidget = self.rbfTabWidget.widget(idx) + + # Fetch the table widget associated with the current driven widget + drivenTableWidget = getattr(drivenWidget, "tableWidget") + drivenRow = drivenTableWidget.currentRow() + + # Extract new pose values from the driven table widget + nColumns = drivenTableWidget.columnCount() + entryWidgets = [drivenTableWidget.cellWidget(drivenRow, c) for c in range(nColumns)] + newValues = [float(widget.text()) for widget in entryWidgets] + + # Update the RBF node associated with the current widget/tab + rbfNode = getattr(drivenWidget, "rbfNode") rbfNode.addPose(poseInput=poseInputs, poseValue=newValues, posesIndex=drivenRow) rbfNode.forceEvaluation() + + # Refresh tables after all updates self.refreshAllTables() def addPose(self): @@ -908,7 +1311,6 @@ def setAttributeToAutoSelect(self, attrListWidget): self.driverAutoAttr = attributes elif "driven" in attrListWidget.objectName(): self.drivenAutoAttr = attributes - print("driverAutoAttr", self.driverAutoAttr) @staticmethod def setSelectedForAutoSelect(attrListWidget, itemTexts): @@ -938,6 +1340,8 @@ def updateAttributeDisplay(self, nodeAttrsToDisplay = [] if not driverNames: return + elif "," in driverNames: + driverNames = driverNames.split(", ") elif type(driverNames) != list: driverNames = [driverNames] @@ -955,46 +1359,11 @@ def updateAttributeDisplay(self, if autoAttrs[objName]: attrPlugs = ["{}.{}".format(driverNames[0], attr) for attr in autoAttrs[objName]] - print(attrPlugs) self.setSelectedForAutoSelect(attrListWidget, attrPlugs) if highlight: self.highlightListEntries(attrListWidget, highlight) - @staticmethod - def __deleteAssociatedWidgetsMaya(widget, attrName="associatedMaya"): - """delete core ui items 'associated' with the provided widgets - - Args: - widget (QWidget): Widget that has the associated attr set - attrName (str, optional): class attr to query - """ - if hasattr(widget, attrName): - for t in getattr(widget, attrName): - try: - mc.deleteUI(t, ctl=True) - except Exception: - pass - else: - setattr(widget, attrName, []) - - @staticmethod - def __deleteAssociatedWidgets(widget, attrName="associated"): - """delete widget items 'associated' with the provided widgets - - Args: - widget (QWidget): Widget that has the associated attr set - attrName (str, optional): class attr to query - """ - if hasattr(widget, attrName): - for t in getattr(widget, attrName): - try: - t.deleteLater() - except Exception: - pass - else: - setattr(widget, attrName, []) - def syncDriverTableCells(self, attrEdit, rbfAttrPlug): """When you edit the driver table, it will update all the sibling rbf nodes in the setup. @@ -1044,41 +1413,43 @@ def setDriverTable(self, rbfNode, weightInfo): Returns: n/a: n/a """ - poses = weightInfo["poses"] + poses = weightInfo.get("poses", {}) - # ensure deletion of associated widgets with this parent widget - self.__deleteAssociatedWidgetsMaya(self.driverPoseTableWidget) - self.__deleteAssociatedWidgets(self.driverPoseTableWidget) + # Clean up existing widgets and prepare for new content + self.deleteAssociatedWidgetsMaya(self.driverPoseTableWidget) + self.deleteAssociatedWidgets(self.driverPoseTableWidget) self.driverPoseTableWidget.clear() - # Configure columns and headers - columnLen = len(weightInfo["driverAttrs"]) - headerNames = weightInfo["driverAttrs"] - self.driverPoseTableWidget.setColumnCount(columnLen) - self.driverPoseTableWidget.setHorizontalHeaderLabels(headerNames) + # Set columns and headers + driverAttrs = weightInfo.get("driverAttrs", []) + self.driverPoseTableWidget.setColumnCount(len(driverAttrs)) + self.driverPoseTableWidget.setHorizontalHeaderLabels(driverAttrs) - # Configure rows - poseInputLen = len(poses["poseInput"]) - self.driverPoseTableWidget.setRowCount(poseInputLen) - if poseInputLen == 0: + # Set rows + poseInputs = poses.get("poseInput", []) + self.driverPoseTableWidget.setRowCount(len(poseInputs)) + if not poseInputs: return - verticalLabels = ["Pose {}".format(index) for index in range(poseInputLen)] + verticalLabels = ["Pose {}".format(index) for index in range(len(poseInputs))] self.driverPoseTableWidget.setVerticalHeaderLabels(verticalLabels) - # Populate table cells + # Populate the table with widgets tmpWidgets, mayaUiItems = [], [] for rowIndex, poseInput in enumerate(poses["poseInput"]): - for columnIndex, pValue in enumerate(poseInput): + for columnIndex, _ in enumerate(poseInput): rbfAttrPlug = "{}.poses[{}].poseInput[{}]".format(rbfNode, rowIndex, columnIndex) attrEdit, mAttrField = getControlAttrWidget(rbfAttrPlug, label="") self.driverPoseTableWidget.setCellWidget(rowIndex, columnIndex, attrEdit) - func = partial(self.syncDriverTableCells, attrEdit, rbfAttrPlug) - attrEdit.returnPressed.connect(func) + attrEdit.returnPressed.connect( + partial(self.syncDriverTableCells, attrEdit, rbfAttrPlug) + ) tmpWidgets.append(attrEdit) mayaUiItems.append(mAttrField) + + # Populate the table with widgets setattr(self.driverPoseTableWidget, "associated", tmpWidgets) setattr(self.driverPoseTableWidget, "associatedMaya", mayaUiItems) @@ -1137,17 +1508,6 @@ def populateDrivenInfo(self, rbfNode, weightInfo): self.rbfTabWidget.addTab(drivenWidget, rbfNode.name) self.setDrivenTable(drivenWidget, rbfNode, weightInfo) - @staticmethod - def _associateRBFnodeAndWidget(tabDrivenWidget, rbfNode): - """associates the RBFNode with a widget for convenience when adding, - deleting, editing - - Args: - tabDrivenWidget (QWidget): tab widget - rbfNode (RBFNode): instance to be associated - """ - setattr(tabDrivenWidget, "rbfNode", rbfNode) - def createAndTagDrivenWidget(self): """create and associate a widget, populated with the information provided by the weightInfo @@ -1428,15 +1788,15 @@ def refresh(self, self.controlLineEdit.clear() self.driverLineEdit.clear() self.driverAttributesWidget.clear() - self.__deleteAssociatedWidgetsMaya(self.driverPoseTableWidget) - self.__deleteAssociatedWidgets(self.driverPoseTableWidget) + self.deleteAssociatedWidgetsMaya(self.driverPoseTableWidget) + self.deleteAssociatedWidgets(self.driverPoseTableWidget) if drivenSelection: self.drivenLineEdit.clear() self.drivenAttributesWidget.clear() if clearDrivenTab: self.rbfTabWidget.clear() - self.__deleteAssociatedWidgetsMaya(self.rbfTabWidget) - self.__deleteAssociatedWidgets(self.rbfTabWidget) + self.deleteAssociatedWidgetsMaya(self.rbfTabWidget) + self.deleteAssociatedWidgets(self.rbfTabWidget) if currentRBFSetupNodes: self.currentRBFSetupNodes = [] @@ -1759,338 +2119,93 @@ def connectSignals(self): Exceptions being MenuBar and Table header signals """ # RBF ComboBox and Refresh Button - self.rbf_cbox.currentIndexChanged.connect(self.displayRBFSetupInfo) - self.rbf_refreshButton.clicked.connect(self.refresh) - - # Driver Line Edit and Control Line Edit - self.driverLineEdit.clicked.connect(selectNode) - self.controlLineEdit.clicked.connect(selectNode) - self.driverLineEdit.textChanged.connect(partial(self.updateAttributeDisplay, - self.driverAttributesWidget)) - self.drivenLineEdit.textChanged.connect(partial(self.updateAttributeDisplay, - self.drivenAttributesWidget)) - - # Table Widget - header = self.driverPoseTableWidget.verticalHeader() - header.sectionClicked.connect(self.setConsistentHeaderSelection) - header.sectionClicked.connect(self.recallDriverPose) - selDelFunc = self.setEditDeletePoseEnabled - self.driverPoseTableWidget.itemSelectionChanged.connect(selDelFunc) - - # Buttons Widget - self.addRbfButton.clicked.connect(self.addRBFToSetup) - self.addPoseButton.clicked.connect(self.addPose) - self.editPoseButton.clicked.connect(self.editPose) - self.editPoseValuesButton.clicked.connect(self.editPoseValues) - self.deletePoseButton.clicked.connect(self.deletePose) - self.setControlButton.clicked.connect(partial(self.setSetupDriverControl, self.controlLineEdit)) - self.setDriverButton.clicked.connect(partial(self.setNodeToField, self.driverLineEdit)) - self.allButton.clicked.connect(self.setDriverControlLineEdit) - self.setDrivenButton.clicked.connect(partial(self.setNodeToField, self.drivenLineEdit)) - self.addDrivenButton.clicked.connect(self.addNewDriven) - - # Custom Context Menus - customMenu = self.driverAttributesWidget.customContextMenuRequested - customMenu.connect( - partial(self.attrListMenu, - self.driverAttributesWidget, - self.driverLineEdit, - "driver") - ) - customMenu = self.drivenAttributesWidget.customContextMenuRequested - customMenu.connect( - partial(self.attrListMenu, - self.drivenAttributesWidget, - self.driverLineEdit, - "driven") - ) - - # Tab Widget - tabBar = self.rbfTabWidget.tabBar() - tabBar.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - tabBar.customContextMenuRequested.connect(self.tabConextMenu) - tabBar.tabCloseRequested.connect(self.removeRBFFromSetup) - - # broken down widgets ----------------------------------------------------- - def createSetupSelectorWidget(self): - """create the top portion of the weidget, select setup + refresh - - Returns: - list: QLayout, QCombobox, QPushButton - """ - setRBFLayout = QtWidgets.QHBoxLayout() - rbfLabel = QtWidgets.QLabel("Select RBF Setup:") - rbf_cbox = QtWidgets.QComboBox() - rbf_refreshButton = self.createCustomButton("Refresh", (60, 25), "Refresh the UI") - rbf_cbox.setFixedHeight(self.genericWidgetHight) - rbf_refreshButton.setMaximumWidth(80) - rbf_refreshButton.setFixedHeight(self.genericWidgetHight - 1) - setRBFLayout.addWidget(rbfLabel) - setRBFLayout.addWidget(rbf_cbox, 1) - setRBFLayout.addWidget(rbf_refreshButton) - return setRBFLayout, rbf_cbox, rbf_refreshButton - - @staticmethod - def createCustomButton(label, size=(35, 27), tooltip=""): - stylesheet = ( - "QPushButton {background-color: #5D5D5D; border-radius: 4px;}" - "QPushButton:pressed { background-color: #00A6F3;}" - "QPushButton:hover:!pressed { background-color: #707070;}" - "QPushButton:disabled { background-color: #4a4a4a;}" - ) - button = QtWidgets.QPushButton(label) - button.setMinimumSize(QtCore.QSize(*size)) - button.setStyleSheet(stylesheet) - button.setToolTip(tooltip) - return button - - @staticmethod - def createSetupSelector2Widget(): - rbfVLayout = QtWidgets.QVBoxLayout() - rbfListWidget = QtWidgets.QListWidget() - rbfVLayout.addWidget(rbfListWidget) - return rbfVLayout, rbfListWidget - - def selectNodeWidget(self, label, buttonLabel="Select"): - """create a lout with label, lineEdit, QPushbutton for user input - """ - stylesheet = ( - "QLineEdit { background-color: #404040;" - "border-radius: 4px;" - "border-color: #505050;" - "border-style: solid;" - "border-width: 1.4px;}" - ) - - nodeLayout = QtWidgets.QHBoxLayout() - nodeLayout.setSpacing(4) - - nodeLabel = QtWidgets.QLabel(label) - nodeLabel.setFixedWidth(40) - nodeLineEdit = ClickableLineEdit() - nodeLineEdit.setStyleSheet(stylesheet) - nodeLineEdit.setReadOnly(True) - nodeSelectButton = self.createCustomButton(buttonLabel) - nodeSelectButton.setFixedWidth(40) - nodeLineEdit.setFixedHeight(self.genericWidgetHight) - nodeSelectButton.setFixedHeight(self.genericWidgetHight) - nodeLayout.addWidget(nodeLabel) - nodeLayout.addWidget(nodeLineEdit, 1) - nodeLayout.addWidget(nodeSelectButton) - return nodeLayout, nodeLineEdit, nodeSelectButton - - @staticmethod - def labelListWidget(label, attrListType, horizontal=True): - """create the listAttribute that users can select their driver/driven - attributes for the setup - - Args: - label (str): to display above the listWidget - horizontal (bool, optional): should the label be above or infront - of the listWidget - - Returns: - list: QLayout, QListWidget - """ - if horizontal: - attributeLayout = QtWidgets.QHBoxLayout() - else: - attributeLayout = QtWidgets.QVBoxLayout() - attributeLabel = QtWidgets.QLabel(label) - attributeListWidget = QtWidgets.QListWidget() - attributeListWidget.setObjectName("{}ListWidget".format(attrListType)) - attributeLayout.addWidget(attributeLabel) - attributeLayout.addWidget(attributeListWidget) - return attributeLayout, attributeListWidget - - @staticmethod - def addRemoveButtonWidget(label1, label2, horizontal=True): - if horizontal: - addRemoveLayout = QtWidgets.QHBoxLayout() - else: - addRemoveLayout = QtWidgets.QVBoxLayout() - addAttributesButton = QtWidgets.QPushButton(label1) - removeAttributesButton = QtWidgets.QPushButton(label2) - addRemoveLayout.addWidget(addAttributesButton) - addRemoveLayout.addWidget(removeAttributesButton) - return addRemoveLayout, addAttributesButton, removeAttributesButton - - def createDriverAttributeWidget(self): - """widget where the user inputs information for the setups - - Returns: - list: [of widgets] - """ - driverControlVLayout = QtWidgets.QVBoxLayout() - driverControlHLayout = QtWidgets.QHBoxLayout() - - # driverMainLayout.setStyleSheet("QVBoxLayout { background-color: #404040;") - driverControlHLayout.setSpacing(3) - # -------------------------------------------------------------------- - (controlLayout, - controlLineEdit, - setControlButton) = self.selectNodeWidget("Control", buttonLabel="Set") - controlLineEdit.setToolTip("The node driving the setup. (Click me!)") - # -------------------------------------------------------------------- - (driverLayout, - driverLineEdit, - driverSelectButton) = self.selectNodeWidget("Driver", buttonLabel="Set") - driverLineEdit.setToolTip("The node driving the setup. (Click me!)") - # -------------------------------------------------------------------- - allButton = self.createCustomButton("All", (20, 53), "") - - (attributeLayout, attributeListWidget) = self.labelListWidget( - label="Select Driver Attributes:", attrListType="driver", horizontal=False) - - attributeListWidget.setToolTip("List of attributes driving setup.") - selType = QtWidgets.QAbstractItemView.ExtendedSelection - attributeListWidget.setSelectionMode(selType) - attributeListWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - # -------------------------------------------------------------------- - driverControlVLayout.addLayout(controlLayout, 0) - driverControlVLayout.addLayout(driverLayout, 0) - driverControlHLayout.addLayout(driverControlVLayout, 0) - driverControlHLayout.addWidget(allButton, 0) - return [controlLineEdit, - setControlButton, - driverLineEdit, - driverSelectButton, - allButton, - attributeListWidget, - attributeLayout, - driverControlHLayout] - - def createDrivenAttributeWidget(self): - """the widget that displays the driven information - - Returns: - list: [of widgets] - """ - drivenWidget = QtWidgets.QWidget() - drivenMainLayout = QtWidgets.QVBoxLayout() - drivenMainLayout.setContentsMargins(0, 10, 0, 2) - drivenMainLayout.setSpacing(9) - drivenSetLayout = QtWidgets.QVBoxLayout() - drivenMainLayout.addLayout(drivenSetLayout) - drivenWidget.setLayout(drivenMainLayout) - # -------------------------------------------------------------------- - (drivenLayout, - drivenLineEdit, - drivenSelectButton) = self.selectNodeWidget("Driven", buttonLabel="Set") - drivenTip = "The node being driven by setup. (Click me!)" - drivenLineEdit.setToolTip(drivenTip) - - addDrivenButton = self.createCustomButton("+", (20, 26), "") - addDrivenButton.setToolTip("Add a new driven to the current rbf node") - # -------------------------------------------------------------------- - (attributeLayout, - attributeListWidget) = self.labelListWidget(label="Select Driven Attributes:", - attrListType="driven", - horizontal=False) - attributeListWidget.setToolTip("Attributes being driven by setup.") - attributeLayout.setSpacing(1) - selType = QtWidgets.QAbstractItemView.ExtendedSelection - attributeListWidget.setSelectionMode(selType) - attributeListWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - # -------------------------------------------------------------------- - drivenSetLayout.addLayout(drivenLayout, 0) - drivenSetLayout.addLayout(attributeLayout, 0) - drivenLayout.addWidget(addDrivenButton) - drivenSetLayout.addLayout(drivenLayout, 0) - drivenSetLayout.addLayout(attributeLayout, 0) - drivenMainLayout.addLayout(drivenSetLayout) - drivenWidget.setLayout(drivenMainLayout) - return [drivenLineEdit, - drivenSelectButton, - addDrivenButton, - attributeListWidget, - drivenWidget, - drivenMainLayout] + self.rbf_cbox.currentIndexChanged.connect(self.displayRBFSetupInfo) + self.rbf_refreshButton.clicked.connect(self.refresh) - def createDrivenWidget(self): - """the widget that displays the driven information + # Driver Line Edit and Control Line Edit + self.driverLineEdit.clicked.connect(selectNode) + self.controlLineEdit.clicked.connect(selectNode) + self.driverLineEdit.textChanged.connect(partial(self.updateAttributeDisplay, + self.driverAttributesWidget)) + self.drivenLineEdit.textChanged.connect(partial(self.updateAttributeDisplay, + self.drivenAttributesWidget)) - Returns: - list: [of widgets] - """ - drivenWidget = QtWidgets.QWidget() - drivenMainLayout = QtWidgets.QHBoxLayout() - drivenMainLayout.setContentsMargins(0, 0, 0, 0) - drivenMainLayout.setSpacing(9) - drivenWidget.setLayout(drivenMainLayout) + # Table Widget + header = self.driverPoseTableWidget.verticalHeader() + header.sectionClicked.connect(self.setConsistentHeaderSelection) + header.sectionClicked.connect(self.recallDriverPose) + selDelFunc = self.setEditDeletePoseEnabled + self.driverPoseTableWidget.itemSelectionChanged.connect(selDelFunc) - tableWidget = self.createTableWidget() - drivenMainLayout.addWidget(tableWidget, 1) - return drivenWidget, tableWidget + # Buttons Widget + self.addRbfButton.clicked.connect(self.addRBFToSetup) + self.addPoseButton.clicked.connect(self.addPose) + self.editPoseButton.clicked.connect(self.editPose) + self.editPoseValuesButton.clicked.connect(self.editPoseValues) + self.deletePoseButton.clicked.connect(self.deletePose) + self.setControlButton.clicked.connect(partial(self.setSetupDriverControl, self.controlLineEdit)) + self.setDriverButton.clicked.connect(partial(self.setNodeToField, self.driverLineEdit)) + self.setDrivenButton.clicked.connect(partial(self.setNodeToField, self.drivenLineEdit, multi=True)) + self.allButton.clicked.connect(self.setDriverControlLineEdit) + self.addDrivenButton.clicked.connect(self.addNewDriven) - def createTableWidget(self): - """create table widget used to display poses, set tooltips and colum + # Custom Context Menus + customMenu = self.driverAttributesWidget.customContextMenuRequested + customMenu.connect( + partial(self.attrListMenu, + self.driverAttributesWidget, + self.driverLineEdit, + "driver") + ) + customMenu = self.drivenAttributesWidget.customContextMenuRequested + customMenu.connect( + partial(self.attrListMenu, + self.drivenAttributesWidget, + self.driverLineEdit, + "driven") + ) - Returns: - QTableWidget: QTableWidget - """ - stylesheet = """ - QTableWidget QHeaderView::section { - background-color: #3a3b3b; - padding: 2px; - text-align: center; - } - QTableCornerButton::section { - background-color: #3a3b3b; - border: none; - } - """ - tableWidget = QtWidgets.QTableWidget() - tableWidget.insertColumn(0) - tableWidget.insertRow(0) - tableWidget.setHorizontalHeaderLabels(["Pose Value"]) - tableWidget.setVerticalHeaderLabels(["Pose #0"]) - # tableWidget.setStyleSheet(stylesheet) - tableTip = "Live connections to the RBF Node in your setup." - tableTip = tableTip + "\nSelect the desired Pose # to recall pose." - tableWidget.setToolTip(tableTip) - return tableWidget + # Tab Widget + tabBar = self.rbfTabWidget.tabBar() + tabBar.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + tabBar.customContextMenuRequested.connect(self.tabConextMenu) + tabBar.tabCloseRequested.connect(self.removeRBFFromSetup) - def createTabWidget(self): - """Tab widget to add driven widgets too. Custom TabBar so the tab is - easier to select + # main assebly ------------------------------------------------------------ + def createCentralWidget(self): + """main UI assembly Returns: - QTabWidget: + QtWidget: main UI to be parented to as the centralWidget """ - tabLayout = QtWidgets.QTabWidget() - tabLayout.setContentsMargins(0, 0, 0, 0) - tabBar = TabBar() - tabLayout.setTabBar(tabBar) - tabBar.setTabsClosable(True) - return tabLayout + centralWidget = QtWidgets.QWidget() + centralWidgetLayout = QtWidgets.QVBoxLayout() + centralWidget.setLayout(centralWidgetLayout) - def createOptionsButtonsWidget(self): - """add, edit, delete buttons for modifying rbf setups. + splitter = QtWidgets.QSplitter() - Returns: - list: [QPushButtons] - """ - optionsLayout = QtWidgets.QHBoxLayout() - optionsLayout.setSpacing(5) - addTip = "After positioning all controls in the setup, add new pose." - addTip = addTip + "\nEnsure the driver node has a unique position." - addPoseButton = self.createCustomButton("Add Pose", size=(80, 26), tooltip=addTip) - EditPoseButton = self.createCustomButton("Update Pose", size=(80, 26)) - EditPoseButton.setToolTip("Recall pose, adjust controls and Edit.") - EditPoseValuesButton = self.createCustomButton("Update Pose Values", size=(80, 26)) - EditPoseValuesButton.setToolTip("Set pose based on values in table") - deletePoseButton = self.createCustomButton("Delete Pose", size=(80, 26)) - deletePoseButton.setToolTip("Recall pose, then Delete") - optionsLayout.addWidget(addPoseButton) - optionsLayout.addWidget(EditPoseButton) - optionsLayout.addWidget(EditPoseValuesButton) - optionsLayout.addWidget(deletePoseButton) - return (optionsLayout, - addPoseButton, - EditPoseButton, - EditPoseValuesButton, - deletePoseButton) + # Setup selector section + (rbfLayout, + self.rbf_cbox, + self.rbf_refreshButton) = self.createSetupSelectorWidget() + self.rbf_cbox.setToolTip("List of available setups in the scene.") + self.rbf_refreshButton.setToolTip("Refresh the UI") + + driverDrivenWidget = self.createDarkContainerWidget() + allTableWidget = self.createDriverDrivenTableWidget() + + centralWidgetLayout.addLayout(rbfLayout) + centralWidgetLayout.addWidget(HLine()) + splitter.addWidget(driverDrivenWidget) + splitter.addWidget(allTableWidget) + centralWidgetLayout.addWidget(splitter) + + # Assuming a ratio of 2:1 for settingWidth to tableWidth + totalWidth = splitter.width() + attributeWidth = (1/3) * totalWidth + tableWidth = (2/3) * totalWidth + splitter.setSizes([int(attributeWidth), int(tableWidth)]) + return centralWidget def createMenuBar(self, hideMenuBar=False): """Create the UI menubar, with option to hide based on mouse input @@ -2149,113 +2264,6 @@ def createMenuBar(self, hideMenuBar=False): self.mousePosition.connect(self.hideMenuBar) return mainMenuBar - def createDarkContainerWidget(self): - darkContainer = QtWidgets.QWidget() - driverMainLayout = QtWidgets.QVBoxLayout() - driverMainLayout.setContentsMargins(10, 10, 10, 10) # Adjust margins as needed - driverMainLayout.setSpacing(5) # Adjust spacing between widgets - - # Setting the dark color (Example: dark gray) - # darkContainer.setStyleSheet("background-color: #323232;") - - # Driver section - (self.controlLineEdit, - self.setControlButton, - self.driverLineEdit, - self.setDriverButton, - self.allButton, - self.driverAttributesWidget, - self.driverAttributesLayout, - driverControlLayout) = self.createDriverAttributeWidget() - - # Driven section - (self.drivenLineEdit, - self.setDrivenButton, - self.addDrivenButton, - self.drivenAttributesWidget, - self.drivenWidget, - self.drivenMainLayout) = self.createDrivenAttributeWidget() - - self.addRbfButton = self.createCustomButton("New RBF") - self.addRbfButton.setToolTip("Select node to be driven by setup.") - stylesheet = ( - "QPushButton {background-color: #179e83; border-radius: 4px;}" - "QPushButton:hover:!pressed { background-color: #2ea88f;}" - ) - self.addRbfButton.setStyleSheet(stylesheet) - - # Setting up the main layout for driver and driven sections - driverMainLayout = QtWidgets.QVBoxLayout() - driverMainLayout.addLayout(driverControlLayout) - driverMainLayout.addLayout(self.driverAttributesLayout) - driverMainLayout.addWidget(self.drivenWidget) - driverMainLayout.addWidget(self.addRbfButton) - darkContainer.setLayout(driverMainLayout) - - return darkContainer - - def createDriverDrivenTableWidget(self): - tableContainer = QtWidgets.QWidget() - - # Setting up the main layout for driver and driven sections - driverDrivenTableLayout = QtWidgets.QVBoxLayout() - self.driverPoseTableWidget = self.createTableWidget() - self.rbfTabWidget = self.createTabWidget() - - # Options buttons section - (optionsLayout, - self.addPoseButton, - self.editPoseButton, - self.editPoseValuesButton, - self.deletePoseButton) = self.createOptionsButtonsWidget() - self.addPoseButton.setEnabled(False) - self.editPoseButton.setEnabled(False) - self.editPoseValuesButton.setEnabled(False) - self.deletePoseButton.setEnabled(False) - - driverDrivenTableLayout.addWidget(self.driverPoseTableWidget, 1) - driverDrivenTableLayout.addWidget(self.rbfTabWidget, 1) - driverDrivenTableLayout.addLayout(optionsLayout) - tableContainer.setLayout(driverDrivenTableLayout) - - return tableContainer - - # main assebly ------------------------------------------------------------ - def createCentralWidget(self): - """main UI assembly - - Returns: - QtWidget: main UI to be parented to as the centralWidget - """ - centralWidget = QtWidgets.QWidget() - centralWidgetLayout = QtWidgets.QVBoxLayout() - centralWidget.setLayout(centralWidgetLayout) - - splitter = QtWidgets.QSplitter() - - # Setup selector section - (rbfLayout, - self.rbf_cbox, - self.rbf_refreshButton) = self.createSetupSelectorWidget() - self.rbf_cbox.setToolTip("List of available setups in the scene.") - self.rbf_refreshButton.setToolTip("Refresh the UI") - - driverDrivenWidget = self.createDarkContainerWidget() - allTableWidget = self.createDriverDrivenTableWidget() - - centralWidgetLayout.addLayout(rbfLayout) - centralWidgetLayout.addWidget(HLine()) - splitter.addWidget(driverDrivenWidget) - splitter.addWidget(allTableWidget) - centralWidgetLayout.addWidget(splitter) - - # Assuming a ratio of 2:1 for settingWidth to tableWidth - totalWidth = splitter.width() - attributeWidth = (1/3) * totalWidth - tableWidth = (2/3) * totalWidth - splitter.setSizes([int(attributeWidth), int(tableWidth)]) - return centralWidget - # overrides --------------------------------------------------------------- def mouseMoveEvent(self, event): """used for tracking the mouse position over the UI, in this case for @@ -2276,8 +2284,8 @@ def closeEvent(self, evnt): Args: evnt (Qt.QEvent): Close event called """ - self.__deleteAssociatedWidgetsMaya(self.driverPoseTableWidget) - self.__deleteAssociatedWidgets(self.driverPoseTableWidget) + self.deleteAssociatedWidgetsMaya(self.driverPoseTableWidget) + self.deleteAssociatedWidgets(self.driverPoseTableWidget) if self.callBackID is not None: self.removeSceneCallback() super(RBFManagerUI, self).closeEvent(evnt) From e7d13ab51a2c5ca37bc1cf38794443f0cf22d96f Mon Sep 17 00:00:00 2001 From: Joji Date: Thu, 12 Oct 2023 00:04:27 -0700 Subject: [PATCH 14/20] Optimized the codebase and fixed a crash when refreshing the UI --- .../scripts/mgear/rigbits/rbf_manager_ui.py | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/release/scripts/mgear/rigbits/rbf_manager_ui.py b/release/scripts/mgear/rigbits/rbf_manager_ui.py index d580741b..a377bd54 100644 --- a/release/scripts/mgear/rigbits/rbf_manager_ui.py +++ b/release/scripts/mgear/rigbits/rbf_manager_ui.py @@ -824,9 +824,10 @@ def __init__(self, hideMenuBar=False, newSceneCallBack=True): self.centralWidget().setMouseTracking(True) self.refreshRbfSetupList() self.connectSignals() + # added because the dockableMixin makes the ui appear small self.adjustSize() - self.resize(800, 650) + # self.resize(800, 650) if newSceneCallBack: self.newSceneCallBack() @@ -839,6 +840,11 @@ def closeEvent(self, event): mc.optionVar(intValue=('RBF_UI_width', width)) mc.optionVar(intValue=('RBF_UI_height', height)) + self.deleteAssociatedWidgetsMaya(self.driverPoseTableWidget) + self.deleteAssociatedWidgets(self.driverPoseTableWidget) + if self.callBackID is not None: + self.removeSceneCallback() + # Call the parent class's closeEvent super(RBFManagerUI, self).closeEvent(event) @@ -1081,8 +1087,8 @@ def refreshAllTables(self): drivenNodes = rbfNode.getDrivenNode() if drivenNodes and drivenNodes[0] == drivenNodeName: weightInfo = rbfNode.getNodeInfo() + self.setDriverTable(rbfNode, weightInfo) self.setDrivenTable(drivenWidget, rbfNode, weightInfo) - self.populateDriverInfo(rbfNode, weightInfo) @staticmethod def determineAttrType(node): @@ -1622,7 +1628,7 @@ def recreateDrivenTabs(self, rbfNodes): self.addPoseButton.setEnabled(True) - def displayRBFSetupInfo(self, index): + def displayRBFSetupInfo(self): """Display the rbfnodes within the desired setups Args: @@ -1788,15 +1794,18 @@ def refresh(self, self.controlLineEdit.clear() self.driverLineEdit.clear() self.driverAttributesWidget.clear() - self.deleteAssociatedWidgetsMaya(self.driverPoseTableWidget) - self.deleteAssociatedWidgets(self.driverPoseTableWidget) + self.driverPoseTableWidget.clear() + + self.driverPoseTableWidget.setRowCount(1) + self.driverPoseTableWidget.setColumnCount(1) + self.driverPoseTableWidget.setHorizontalHeaderLabels(["Pose Value"]) + self.driverPoseTableWidget.setVerticalHeaderLabels(["Pose #0"]) + if drivenSelection: self.drivenLineEdit.clear() self.drivenAttributesWidget.clear() if clearDrivenTab: self.rbfTabWidget.clear() - self.deleteAssociatedWidgetsMaya(self.rbfTabWidget) - self.deleteAssociatedWidgets(self.rbfTabWidget) if currentRBFSetupNodes: self.currentRBFSetupNodes = [] @@ -2277,15 +2286,3 @@ def mouseMoveEvent(self, event): pos = event.pos() self.mousePosition.emit(pos.x(), pos.y()) - def closeEvent(self, evnt): - """on UI close, ensure that all attrControlgrps are destroyed in case - the user is just reopening the UI. Properly severs ties to the attrs - - Args: - evnt (Qt.QEvent): Close event called - """ - self.deleteAssociatedWidgetsMaya(self.driverPoseTableWidget) - self.deleteAssociatedWidgets(self.driverPoseTableWidget) - if self.callBackID is not None: - self.removeSceneCallback() - super(RBFManagerUI, self).closeEvent(evnt) From 7f77b370b929be7968153c84db259b330cbfc19e Mon Sep 17 00:00:00 2001 From: Joji Date: Thu, 12 Oct 2023 20:11:39 -0700 Subject: [PATCH 15/20] Approximate small float values to zero to clean up data and make it more readable after exporting a file. --- .../scripts/mgear/rigbits/rbf_manager_ui.py | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/release/scripts/mgear/rigbits/rbf_manager_ui.py b/release/scripts/mgear/rigbits/rbf_manager_ui.py index a377bd54..96707a83 100644 --- a/release/scripts/mgear/rigbits/rbf_manager_ui.py +++ b/release/scripts/mgear/rigbits/rbf_manager_ui.py @@ -1144,9 +1144,9 @@ def editPose(self): return driverNode = rbfNodes[0].getDriverNode()[0] driverAttrs = rbfNodes[0].getDriverNodeAttributes() - poseInputs = rbf_node.getMultipleAttrs(driverNode, driverAttrs) + poseInputs = self.approximateZeros(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) for rbfNode in rbfNodes: - poseValues = rbfNode.getPoseValues(resetDriven=True) + poseValues = self.approximateZeros(rbfNode.getPoseValues(resetDriven=True)) rbfNode.addPose(poseInput=poseInputs, poseValue=poseValues, posesIndex=drivenRow) @@ -1170,7 +1170,7 @@ def editPoseValues(self): return driverNode = rbfNodes[0].getDriverNode()[0] driverAttrs = rbfNodes[0].getDriverNodeAttributes() - poseInputs = rbf_node.getMultipleAttrs(driverNode, driverAttrs) + poseInputs = self.approximateZeros(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) nColumns = drivenTableWidget.columnCount() entryWidgets = [drivenTableWidget.cellWidget(drivenRow, c) for c in range(nColumns)] newValues = [float(w.text()) for w in entryWidgets] @@ -1222,6 +1222,18 @@ def updateAllFromTables(self): # Refresh tables after all updates self.refreshAllTables() + def approximateZeros(self, values, tolerance=1e-10): + """Approximate small values to zero. + + Args: + values (list of float): The values to approximate. + tolerance (float): The tolerance under which a value is considered zero. + + Returns: + list of float: The approximated values. + """ + return [0 if abs(v) < tolerance else v for v in values] + def addPose(self): """Add pose to rbf nodes in setup. Additional index on all nodes @@ -1233,9 +1245,10 @@ def addPose(self): return driverNode = rbfNodes[0].getDriverNode()[0] driverAttrs = rbfNodes[0].getDriverNodeAttributes() - poseInputs = rbf_node.getMultipleAttrs(driverNode, driverAttrs) + poseInputs = self.approximateZeros(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) for rbfNode in rbfNodes: poseValues = rbfNode.getPoseValues(resetDriven=True, absoluteWorld=self.absWorld) + poseValues = self.approximateZeros(poseValues) rbfNode.addPose(poseInput=poseInputs, poseValue=poseValues) self.refreshAllTables() From 8919a710440251f42edff4e4ee3e1d34cd4d063d Mon Sep 17 00:00:00 2001 From: Joji Date: Tue, 24 Oct 2023 19:30:42 -0700 Subject: [PATCH 16/20] Approximate small values to zero and rounds the others. --- .../scripts/mgear/rigbits/rbf_manager_ui.py | 136 +++++++++++++----- 1 file changed, 103 insertions(+), 33 deletions(-) diff --git a/release/scripts/mgear/rigbits/rbf_manager_ui.py b/release/scripts/mgear/rigbits/rbf_manager_ui.py index 96707a83..34a81c59 100644 --- a/release/scripts/mgear/rigbits/rbf_manager_ui.py +++ b/release/scripts/mgear/rigbits/rbf_manager_ui.py @@ -1025,7 +1025,12 @@ def addRBFToSetup(self): rbfNode.addPose(poseInput=poseInputs, poseValue=defaultVals[1::2]) self.populateDriverInfo(rbfNode, rbfNode.getNodeInfo()) - self.populateDrivenInfo(rbfNode, rbfNode.getNodeInfo()) + drivenWidget = self.populateDrivenInfo(rbfNode, rbfNode.getNodeInfo()) + + # Add the driven widget to the tab widget. + drivenWidget.setProperty("drivenNode", drivenNode) + self.rbfTabWidget.addTab(drivenWidget, rbfNode.name) + self.setDrivenTable(drivenWidget, rbfNode, rbfNode.getNodeInfo()) # Add newly created RBFNode to list of current self.addPoseButton.setEnabled(True) @@ -1144,15 +1149,60 @@ def editPose(self): return driverNode = rbfNodes[0].getDriverNode()[0] driverAttrs = rbfNodes[0].getDriverNodeAttributes() - poseInputs = self.approximateZeros(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) + poseInputs = self.approximateAndRound(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) for rbfNode in rbfNodes: - poseValues = self.approximateZeros(rbfNode.getPoseValues(resetDriven=True)) + poseValues = self.approximateAndRound(rbfNode.getPoseValues(resetDriven=True)) rbfNode.addPose(poseInput=poseInputs, poseValue=poseValues, posesIndex=drivenRow) rbfNode.forceEvaluation() self.refreshAllTables() + def editAutoPoseValues(self, row, column): + """Edit an existing pose based on the values in the table + Returns: + None + """ + print("hello") + + rbfNodes = self.currentRBFSetupNodes + if not rbfNodes: + return + + drivenWidget = self.rbfTabWidget.currentWidget() + drivenTableWidget = getattr(drivenWidget, "tableWidget") + + try: + # Disconnecting the signal to prevent an infinite loop + drivenTableWidget.cellChanged.disconnect(self.hello) + + driverNode = rbfNodes[0].getDriverNode()[0] + driverAttrs = rbfNodes[0].getDriverNodeAttributes() + poseInputs = self.approximateAndRound(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) + + nColumns = drivenTableWidget.columnCount() + entryWidgets = [drivenTableWidget.cellWidget(row, c) for c in range(nColumns)] + newValues = [float(w.text()) if w is not None else 0.0 for w in entryWidgets] # Added check for None widgets + + rbfNode = getattr(drivenWidget, "rbfNode") + rbfNodes = [rbfNode] + for rbfNode in rbfNodes: + print("rbfNode: {}".format(rbfNode)) + print("poseInputs: {}".format(poseInputs)) + print("New pose values: {}".format(newValues)) + print("poseIndex: {}".format(row)) + rbfNode.addPose(poseInput=poseInputs, + poseValue=newValues, + posesIndex=row) + rbfNode.forceEvaluation() + self.refreshAllTables() + + except Exception as e: + print("An error occurred: {}".format(str(e))) + finally: + # Always reconnect the signal in a finally block to ensure it is reconnected even if an error occurs + drivenTableWidget.cellChanged.connect(self.hello) + def editPoseValues(self): """Edit an existing pose based on the values in the table Returns: @@ -1170,7 +1220,7 @@ def editPoseValues(self): return driverNode = rbfNodes[0].getDriverNode()[0] driverAttrs = rbfNodes[0].getDriverNodeAttributes() - poseInputs = self.approximateZeros(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) + poseInputs = self.approximateAndRound(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) nColumns = drivenTableWidget.columnCount() entryWidgets = [drivenTableWidget.cellWidget(drivenRow, c) for c in range(nColumns)] newValues = [float(w.text()) for w in entryWidgets] @@ -1222,18 +1272,25 @@ def updateAllFromTables(self): # Refresh tables after all updates self.refreshAllTables() - def approximateZeros(self, values, tolerance=1e-10): - """Approximate small values to zero. - + def approximateAndRound(self, values, tolerance=1e-10, decimalPlaces=2): + """Approximate small values to zero and rounds the others. + Args: - values (list of float): The values to approximate. + values (list of float): The values to approximate and round. tolerance (float): The tolerance under which a value is considered zero. - + decimalPlaces (int): The number of decimal places to round to. + Returns: - list of float: The approximated values. + list of float: The approximated and rounded values. """ - return [0 if abs(v) < tolerance else v for v in values] - + newValues = [] + for v in values: + if abs(v) < tolerance: + newValues.append(0) + else: + newValues.append(round(v, decimalPlaces)) + return newValues + def addPose(self): """Add pose to rbf nodes in setup. Additional index on all nodes @@ -1245,13 +1302,25 @@ def addPose(self): return driverNode = rbfNodes[0].getDriverNode()[0] driverAttrs = rbfNodes[0].getDriverNodeAttributes() - poseInputs = self.approximateZeros(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) + poseInputs = self.approximateAndRound(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) + + for index in range(self.rbfTabWidget.count()): + drivenWidget = self.rbfTabWidget.widget(index) + try: + drivenWidget.tableWidget.cellChanged.disconnect(self.hello) + except RuntimeError: + pass + for rbfNode in rbfNodes: poseValues = rbfNode.getPoseValues(resetDriven=True, absoluteWorld=self.absWorld) - poseValues = self.approximateZeros(poseValues) + poseValues = self.approximateAndRound(poseValues) rbfNode.addPose(poseInput=poseInputs, poseValue=poseValues) self.refreshAllTables() + for index in range(self.rbfTabWidget.count()): + drivenWidget = self.rbfTabWidget.widget(index) + drivenWidget.tableWidget.cellChanged.connect(self.hello) + def updateAllSetupsInfo(self, includeEmpty=False): """refresh the instance dictionary of all the setps in the scene. @@ -1514,18 +1583,12 @@ def populateDrivenInfo(self, rbfNode, weightInfo): drivenWidget = self.createAndTagDrivenWidget() self._associateRBFnodeAndWidget(drivenWidget, rbfNode) - # Populate Driven Widget Info - self.populateDrivenWidgetInfo(drivenWidget, weightInfo, rbfNode) - # Set Driven Node and Attributes drivenNode = weightInfo.get("drivenNode", [None])[0] self.drivenLineEdit.setText(drivenNode or "") self.setAttributeDisplay(self.drivenAttributesWidget, drivenNode or "", weightInfo["drivenAttrs"]) + return drivenWidget - # Add the driven widget to the tab widget. - drivenWidget.setProperty("drivenNode", drivenNode) - self.rbfTabWidget.addTab(drivenWidget, rbfNode.name) - self.setDrivenTable(drivenWidget, rbfNode, weightInfo) def createAndTagDrivenWidget(self): """create and associate a widget, populated with the information @@ -1546,8 +1609,7 @@ def createAndTagDrivenWidget(self): tableWidget.itemSelectionChanged.connect(self.setEditDeletePoseEnabled) return drivenWidget - @staticmethod - def setDrivenTable(drivenWidget, rbfNode, weightInfo): + def setDrivenTable(self, drivenWidget, rbfNode, weightInfo): """set the widgets with information from the weightInfo for dispaly Args: @@ -1572,6 +1634,12 @@ def setDrivenTable(drivenWidget, rbfNode, weightInfo): attrEdit, mAttrFeild = getControlAttrWidget(rbfAttrPlug, label="") drivenWidget.tableWidget.setCellWidget(rowIndex, columnIndex, attrEdit) + # Adding QTableWidgetItem for the cell and setting the value + tableItem = QtWidgets.QTableWidgetItem() + tableItem.setFlags(tableItem.flags() | QtCore.Qt.ItemIsEditable) + tableItem.setData(QtCore.Qt.DisplayRole, str(pValue)) + drivenWidget.tableWidget.setItem(rowIndex, columnIndex, tableItem) + def populateDrivenWidgetInfo(self, drivenWidget, weightInfo, rbfNode): """set the information from the weightInfo to the widgets child of drivenWidget @@ -1630,14 +1698,17 @@ def recreateDrivenTabs(self, rbfNodes): Args: rbfNodes (list): [of RBFNodes] """ - rbfNodes = sorted(rbfNodes, key=lambda x: x.name) - self.rbfTabWidget.clear() - for rbfNode in rbfNodes: - weightInfo = rbfNode.getNodeInfo() - drivenWidget = self.createAndTagDrivenWidget() - self._associateRBFnodeAndWidget(drivenWidget, rbfNode) - self.populateDrivenWidgetInfo(drivenWidget, weightInfo, rbfNode) - self.rbfTabWidget.addTab(drivenWidget, rbfNode.name) + rbfNode = sorted(rbfNodes, key=lambda x: x.name)[0] + # self.rbfTabWidget.clear() + # for rbfNode in rbfNodes: + weightInfo = rbfNode.getNodeInfo() + drivenWidget = self.createAndTagDrivenWidget() + self._associateRBFnodeAndWidget(drivenWidget, rbfNode) + self.populateDrivenWidgetInfo(drivenWidget, weightInfo, rbfNode) + self.rbfTabWidget.addTab(drivenWidget, rbfNode.name) + is_connected = drivenWidget.tableWidget.cellChanged.connect(self.hello) + print("Signal connected:", is_connected) + drivenWidget.tableWidget.setItem(1, 1, QtWidgets.QTableWidgetItem("Test")) # This should trigger the cellChanged signal self.addPoseButton.setEnabled(True) @@ -2156,8 +2227,7 @@ def connectSignals(self): header = self.driverPoseTableWidget.verticalHeader() header.sectionClicked.connect(self.setConsistentHeaderSelection) header.sectionClicked.connect(self.recallDriverPose) - selDelFunc = self.setEditDeletePoseEnabled - self.driverPoseTableWidget.itemSelectionChanged.connect(selDelFunc) + self.driverPoseTableWidget.itemSelectionChanged.connect(self.setEditDeletePoseEnabled) # Buttons Widget self.addRbfButton.clicked.connect(self.addRBFToSetup) From 5c4a2a7cec521f9774920458f938212e288a058b Mon Sep 17 00:00:00 2001 From: Joji Date: Tue, 24 Oct 2023 19:54:34 -0700 Subject: [PATCH 17/20] Revert "Approximate small values to zero and rounds the others." This reverts commit 8919a710440251f42edff4e4ee3e1d34cd4d063d. --- .../scripts/mgear/rigbits/rbf_manager_ui.py | 136 +++++------------- 1 file changed, 33 insertions(+), 103 deletions(-) diff --git a/release/scripts/mgear/rigbits/rbf_manager_ui.py b/release/scripts/mgear/rigbits/rbf_manager_ui.py index 34a81c59..96707a83 100644 --- a/release/scripts/mgear/rigbits/rbf_manager_ui.py +++ b/release/scripts/mgear/rigbits/rbf_manager_ui.py @@ -1025,12 +1025,7 @@ def addRBFToSetup(self): rbfNode.addPose(poseInput=poseInputs, poseValue=defaultVals[1::2]) self.populateDriverInfo(rbfNode, rbfNode.getNodeInfo()) - drivenWidget = self.populateDrivenInfo(rbfNode, rbfNode.getNodeInfo()) - - # Add the driven widget to the tab widget. - drivenWidget.setProperty("drivenNode", drivenNode) - self.rbfTabWidget.addTab(drivenWidget, rbfNode.name) - self.setDrivenTable(drivenWidget, rbfNode, rbfNode.getNodeInfo()) + self.populateDrivenInfo(rbfNode, rbfNode.getNodeInfo()) # Add newly created RBFNode to list of current self.addPoseButton.setEnabled(True) @@ -1149,60 +1144,15 @@ def editPose(self): return driverNode = rbfNodes[0].getDriverNode()[0] driverAttrs = rbfNodes[0].getDriverNodeAttributes() - poseInputs = self.approximateAndRound(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) + poseInputs = self.approximateZeros(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) for rbfNode in rbfNodes: - poseValues = self.approximateAndRound(rbfNode.getPoseValues(resetDriven=True)) + poseValues = self.approximateZeros(rbfNode.getPoseValues(resetDriven=True)) rbfNode.addPose(poseInput=poseInputs, poseValue=poseValues, posesIndex=drivenRow) rbfNode.forceEvaluation() self.refreshAllTables() - def editAutoPoseValues(self, row, column): - """Edit an existing pose based on the values in the table - Returns: - None - """ - print("hello") - - rbfNodes = self.currentRBFSetupNodes - if not rbfNodes: - return - - drivenWidget = self.rbfTabWidget.currentWidget() - drivenTableWidget = getattr(drivenWidget, "tableWidget") - - try: - # Disconnecting the signal to prevent an infinite loop - drivenTableWidget.cellChanged.disconnect(self.hello) - - driverNode = rbfNodes[0].getDriverNode()[0] - driverAttrs = rbfNodes[0].getDriverNodeAttributes() - poseInputs = self.approximateAndRound(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) - - nColumns = drivenTableWidget.columnCount() - entryWidgets = [drivenTableWidget.cellWidget(row, c) for c in range(nColumns)] - newValues = [float(w.text()) if w is not None else 0.0 for w in entryWidgets] # Added check for None widgets - - rbfNode = getattr(drivenWidget, "rbfNode") - rbfNodes = [rbfNode] - for rbfNode in rbfNodes: - print("rbfNode: {}".format(rbfNode)) - print("poseInputs: {}".format(poseInputs)) - print("New pose values: {}".format(newValues)) - print("poseIndex: {}".format(row)) - rbfNode.addPose(poseInput=poseInputs, - poseValue=newValues, - posesIndex=row) - rbfNode.forceEvaluation() - self.refreshAllTables() - - except Exception as e: - print("An error occurred: {}".format(str(e))) - finally: - # Always reconnect the signal in a finally block to ensure it is reconnected even if an error occurs - drivenTableWidget.cellChanged.connect(self.hello) - def editPoseValues(self): """Edit an existing pose based on the values in the table Returns: @@ -1220,7 +1170,7 @@ def editPoseValues(self): return driverNode = rbfNodes[0].getDriverNode()[0] driverAttrs = rbfNodes[0].getDriverNodeAttributes() - poseInputs = self.approximateAndRound(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) + poseInputs = self.approximateZeros(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) nColumns = drivenTableWidget.columnCount() entryWidgets = [drivenTableWidget.cellWidget(drivenRow, c) for c in range(nColumns)] newValues = [float(w.text()) for w in entryWidgets] @@ -1272,25 +1222,18 @@ def updateAllFromTables(self): # Refresh tables after all updates self.refreshAllTables() - def approximateAndRound(self, values, tolerance=1e-10, decimalPlaces=2): - """Approximate small values to zero and rounds the others. - + def approximateZeros(self, values, tolerance=1e-10): + """Approximate small values to zero. + Args: - values (list of float): The values to approximate and round. + values (list of float): The values to approximate. tolerance (float): The tolerance under which a value is considered zero. - decimalPlaces (int): The number of decimal places to round to. - + Returns: - list of float: The approximated and rounded values. + list of float: The approximated values. """ - newValues = [] - for v in values: - if abs(v) < tolerance: - newValues.append(0) - else: - newValues.append(round(v, decimalPlaces)) - return newValues - + return [0 if abs(v) < tolerance else v for v in values] + def addPose(self): """Add pose to rbf nodes in setup. Additional index on all nodes @@ -1302,25 +1245,13 @@ def addPose(self): return driverNode = rbfNodes[0].getDriverNode()[0] driverAttrs = rbfNodes[0].getDriverNodeAttributes() - poseInputs = self.approximateAndRound(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) - - for index in range(self.rbfTabWidget.count()): - drivenWidget = self.rbfTabWidget.widget(index) - try: - drivenWidget.tableWidget.cellChanged.disconnect(self.hello) - except RuntimeError: - pass - + poseInputs = self.approximateZeros(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) for rbfNode in rbfNodes: poseValues = rbfNode.getPoseValues(resetDriven=True, absoluteWorld=self.absWorld) - poseValues = self.approximateAndRound(poseValues) + poseValues = self.approximateZeros(poseValues) rbfNode.addPose(poseInput=poseInputs, poseValue=poseValues) self.refreshAllTables() - for index in range(self.rbfTabWidget.count()): - drivenWidget = self.rbfTabWidget.widget(index) - drivenWidget.tableWidget.cellChanged.connect(self.hello) - def updateAllSetupsInfo(self, includeEmpty=False): """refresh the instance dictionary of all the setps in the scene. @@ -1583,12 +1514,18 @@ def populateDrivenInfo(self, rbfNode, weightInfo): drivenWidget = self.createAndTagDrivenWidget() self._associateRBFnodeAndWidget(drivenWidget, rbfNode) + # Populate Driven Widget Info + self.populateDrivenWidgetInfo(drivenWidget, weightInfo, rbfNode) + # Set Driven Node and Attributes drivenNode = weightInfo.get("drivenNode", [None])[0] self.drivenLineEdit.setText(drivenNode or "") self.setAttributeDisplay(self.drivenAttributesWidget, drivenNode or "", weightInfo["drivenAttrs"]) - return drivenWidget + # Add the driven widget to the tab widget. + drivenWidget.setProperty("drivenNode", drivenNode) + self.rbfTabWidget.addTab(drivenWidget, rbfNode.name) + self.setDrivenTable(drivenWidget, rbfNode, weightInfo) def createAndTagDrivenWidget(self): """create and associate a widget, populated with the information @@ -1609,7 +1546,8 @@ def createAndTagDrivenWidget(self): tableWidget.itemSelectionChanged.connect(self.setEditDeletePoseEnabled) return drivenWidget - def setDrivenTable(self, drivenWidget, rbfNode, weightInfo): + @staticmethod + def setDrivenTable(drivenWidget, rbfNode, weightInfo): """set the widgets with information from the weightInfo for dispaly Args: @@ -1634,12 +1572,6 @@ def setDrivenTable(self, drivenWidget, rbfNode, weightInfo): attrEdit, mAttrFeild = getControlAttrWidget(rbfAttrPlug, label="") drivenWidget.tableWidget.setCellWidget(rowIndex, columnIndex, attrEdit) - # Adding QTableWidgetItem for the cell and setting the value - tableItem = QtWidgets.QTableWidgetItem() - tableItem.setFlags(tableItem.flags() | QtCore.Qt.ItemIsEditable) - tableItem.setData(QtCore.Qt.DisplayRole, str(pValue)) - drivenWidget.tableWidget.setItem(rowIndex, columnIndex, tableItem) - def populateDrivenWidgetInfo(self, drivenWidget, weightInfo, rbfNode): """set the information from the weightInfo to the widgets child of drivenWidget @@ -1698,17 +1630,14 @@ def recreateDrivenTabs(self, rbfNodes): Args: rbfNodes (list): [of RBFNodes] """ - rbfNode = sorted(rbfNodes, key=lambda x: x.name)[0] - # self.rbfTabWidget.clear() - # for rbfNode in rbfNodes: - weightInfo = rbfNode.getNodeInfo() - drivenWidget = self.createAndTagDrivenWidget() - self._associateRBFnodeAndWidget(drivenWidget, rbfNode) - self.populateDrivenWidgetInfo(drivenWidget, weightInfo, rbfNode) - self.rbfTabWidget.addTab(drivenWidget, rbfNode.name) - is_connected = drivenWidget.tableWidget.cellChanged.connect(self.hello) - print("Signal connected:", is_connected) - drivenWidget.tableWidget.setItem(1, 1, QtWidgets.QTableWidgetItem("Test")) # This should trigger the cellChanged signal + rbfNodes = sorted(rbfNodes, key=lambda x: x.name) + self.rbfTabWidget.clear() + for rbfNode in rbfNodes: + weightInfo = rbfNode.getNodeInfo() + drivenWidget = self.createAndTagDrivenWidget() + self._associateRBFnodeAndWidget(drivenWidget, rbfNode) + self.populateDrivenWidgetInfo(drivenWidget, weightInfo, rbfNode) + self.rbfTabWidget.addTab(drivenWidget, rbfNode.name) self.addPoseButton.setEnabled(True) @@ -2227,7 +2156,8 @@ def connectSignals(self): header = self.driverPoseTableWidget.verticalHeader() header.sectionClicked.connect(self.setConsistentHeaderSelection) header.sectionClicked.connect(self.recallDriverPose) - self.driverPoseTableWidget.itemSelectionChanged.connect(self.setEditDeletePoseEnabled) + selDelFunc = self.setEditDeletePoseEnabled + self.driverPoseTableWidget.itemSelectionChanged.connect(selDelFunc) # Buttons Widget self.addRbfButton.clicked.connect(self.addRBFToSetup) From e351a13618e727ba438432d4f501c4642b433468 Mon Sep 17 00:00:00 2001 From: Joji Date: Fri, 3 Nov 2023 19:13:32 -0700 Subject: [PATCH 18/20] Transitioning to RBFmanager 2.0 and packaging it in a self-contained folder --- release/scripts/mgear/rigbits/menu.py | 6 + .../mgear/rigbits/rbf_manager2/__init__.py | 0 .../rigbits/rbf_manager2/rbf_manager_ui.py | 2320 +++++++++++++++++ .../scripts/mgear/rigbits/rbf_manager_ui.py | 1575 +++++------ 4 files changed, 3013 insertions(+), 888 deletions(-) create mode 100644 release/scripts/mgear/rigbits/rbf_manager2/__init__.py create mode 100644 release/scripts/mgear/rigbits/rbf_manager2/rbf_manager_ui.py diff --git a/release/scripts/mgear/rigbits/menu.py b/release/scripts/mgear/rigbits/menu.py index 4dc524b9..1017947b 100644 --- a/release/scripts/mgear/rigbits/menu.py +++ b/release/scripts/mgear/rigbits/menu.py @@ -30,6 +30,7 @@ def install(): ("Duplicate symmetrical", str_duplicateSym), ("-----", None), ("RBF Manager", str_rbf_manager_ui), + ("RBF Manager2", str_rbf_manager2_ui), ("SDK Manager (BETA)", str_SDK_manager_ui), ("-----", None), ("Space Manager", str_space_manager), @@ -191,6 +192,11 @@ def install_utils_menu(m): rbf_manager_ui.show() """ +str_rbf_manager2_ui = """ +from mgear.rigbits.rbf_manager2 import rbf_manager_ui +rbf_manager_ui.show() +""" + str_SDK_manager_ui = """ from mgear.rigbits.sdk_manager import SDK_manager_ui SDK_manager_ui.show() diff --git a/release/scripts/mgear/rigbits/rbf_manager2/__init__.py b/release/scripts/mgear/rigbits/rbf_manager2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/release/scripts/mgear/rigbits/rbf_manager2/rbf_manager_ui.py b/release/scripts/mgear/rigbits/rbf_manager2/rbf_manager_ui.py new file mode 100644 index 00000000..f64b76ab --- /dev/null +++ b/release/scripts/mgear/rigbits/rbf_manager2/rbf_manager_ui.py @@ -0,0 +1,2320 @@ +#!/usr/bin/env python +""" +A tool to manage a number of rbf type nodes under a user defined setup(name) + +Steps - + set Driver + set Control for driver(optional, recommended) + select attributes to driver RBF nodes + Select Node to be driven in scene(Animation control, transform) + Name newly created setup + select attributes to be driven by the setup + add any additional driven nodes + position driver(via the control) + position the driven node(s) + select add pose + +Add notes - +Please ensure the driver node is NOT in the same position more than once. This +will cause the RBFNode to fail while calculating. This can be fixed by deleting +any two poses with the same input values. + +Edit Notes - +Edit a pose by selecting "pose #" in the table. (which recalls recorded pose) +reposition any controls involved in the setup +select "Edit Pose" + +Delete notes - +select desired "pose #" +select "Delete Pose" + +Mirror notes - +setups/Controls will succefully mirror if they have had their inverseAttrs +configured previously. + +2.0 ------- +LOOK into coloring the pose and how close it is +import replace name support (will work through json manually) +support live connections +settings support for suffix, etc +rename existing setup +newScene callback + +Attributes: + CTL_SUFFIX (str): suffix for anim controls + DRIVEN_SUFFIX (str): suffix for driven group nodes + EXTRA_MODULE_DICT (str): name of the dict which holds additional modules + MGEAR_EXTRA_ENVIRON (str): environment variable to query for paths + TOOL_NAME (str): name of UI + TOOL_TITLE (str): title as it appears in the ui + __version__ (float): UI version + +Deleted Attributes: + RBF_MODULES (dict): of supported rbf modules + +__author__ = "Rafael Villar, Joji Nishimura" +__email__ = "rav@ravrigs.com" +__credits__ = ["Miquel Campos", "Ingo Clemens"] + +""" +# python +import imp +import os +from functools import partial + +# core +import maya.cmds as mc +import pymel.core as pm +import maya.OpenMaya as om +import maya.OpenMayaUI as mui + +# mgear +import mgear +from mgear.core import pyqt +import mgear.core.string as mString +from mgear.core import anim_utils +from mgear.vendor.Qt import QtWidgets, QtCore, QtCompat +from maya.app.general.mayaMixin import MayaQWidgetDockableMixin +from mgear.rigbits.six import PY2 + +# rbf +from mgear.rigbits import rbf_io +from mgear.rigbits import rbf_node + + +# ============================================================================= +# Constants +# ============================================================================= +__version__ = "2.0.0" + +_mgear_version = mgear.getVersion() +TOOL_NAME = "RBF Manager" +TOOL_TITLE = "{} v{} | mGear {}".format(TOOL_NAME, __version__, _mgear_version) +UI_NAME = "RBFManagerUI" +WORK_SPACE_NAME = UI_NAME + "WorkspaceControl" + +DRIVEN_SUFFIX = rbf_node.DRIVEN_SUFFIX +CTL_SUFFIX = rbf_node.CTL_SUFFIX + +MGEAR_EXTRA_ENVIRON = "MGEAR_RBF_EXTRA" +EXTRA_MODULE_DICT = "extraFunc_dict" + +MIRROR_SUFFIX = "_mr" + +# ============================================================================= +# general functions +# ============================================================================= + + +def testFunctions(*args): + """test function for connecting signals during debug + + Args: + *args: Description + """ + print('!!', args) + + +def getPlugAttrs(nodes, attrType="keyable"): + """Get a list of attributes to display to the user + + Args: + nodes (str): name of node to attr query + keyable (bool, optional): should the list only be kayable attrs + + Returns: + list: list of attrplugs + """ + plugAttrs = [] + if len(nodes) >= 2: + print("the number of node is more than two") + + for node in nodes: + if attrType == "all": + attrs = mc.listAttr(node, se=True, u=False) + aliasAttrs = mc.aliasAttr(node, q=True) + if aliasAttrs is not None: + try: + attrs.extend(aliasAttrs[0::2]) + except Exception: + pass + elif attrType == "cb": + attrs = mc.listAttr(node, se=True, u=False, cb=True) + elif attrType == "keyable": + attrs = mc.listAttr(node, se=True, u=False, keyable=True) + if attrs is None: + continue + [plugAttrs.append("{}.{}".format(node, a)) for a in attrs] + return plugAttrs + + +def existing_rbf_setup(node): + """check if there is an existing rbf setup associated with the node + + Args: + node (str): name of the node to query + + Returns: + list: of the rbftype assiociated with the node + """ + connected_nodes = mc.listConnections(node, + destination=True, + shapes=True, + scn=True) or [] + connected_node_types = set(mc.nodeType(x) for x in connected_nodes) + rbf_node_types = set(rbf_io.RBF_MODULES.keys()) + return list(connected_node_types.intersection(rbf_node_types)) + + +def sortRBF(name, rbfType=None): + """Get node wrapped in RBFNode class based on the type of node + + Args: + name (str): name of the RBFNode in scene + rbfType (str, optional): type of RBF to get instance from + + Returns: + RBFNode: instance of RBFNode + """ + if mc.objExists(name) and mc.nodeType(name) in rbf_io.RBF_MODULES: + rbfType = mc.nodeType(name) + return rbf_io.RBF_MODULES[rbfType].RBFNode(name) + elif rbfType is not None: + return rbf_io.RBF_MODULES[rbfType].RBFNode(name) + + +def getEnvironModules(): + """if there are any environment variables set that load additional + modules for the UI, query and return dict + + Returns: + dict: displayName:funcObject + """ + extraModulePath = os.environ.get(MGEAR_EXTRA_ENVIRON, None) + if extraModulePath is None or not os.path.exists(extraModulePath): + return None + exModule = imp.load_source(MGEAR_EXTRA_ENVIRON, + os.path.abspath(extraModulePath)) + additionalFuncDict = getattr(exModule, EXTRA_MODULE_DICT, None) + if additionalFuncDict is None: + mc.warning("'{}' not found in {}".format(EXTRA_MODULE_DICT, + extraModulePath)) + print("No additional menu items added to {}".format(TOOL_NAME)) + return additionalFuncDict + + +def selectNode(name): + """Convenience function, to ensure no errors when selecting nodes in UI + + Args: + name (str): name of node to be selected + """ + if mc.objExists(name): + mc.select(name) + else: + print(name, "No longer exists for selection!") + + +# ============================================================================= +# UI General Functions +# ============================================================================= + +def getControlAttrWidget(nodeAttr, label=""): + """Create a cmds.attrControlGrp and wrap it in a qtWidget, preserving its connection + to the specified attr + + Args: + nodeAttr (str): node.attr, the target for the attrControlGrp + label (str, optional): name for the attr widget + + Returns: + QtWidget.QLineEdit: A Qt widget created from attrControlGrp + str: The name of the created Maya attrControlGrp + """ + mAttrFeild = mc.attrControlGrp(attribute=nodeAttr, label=label, po=True) + + # Convert the Maya control to a Qt pointer + ptr = mui.MQtUtil.findControl(mAttrFeild) + + # Wrap the Maya control into a Qt widget, considering Python version + controlWidget = QtCompat.wrapInstance(long(ptr) if PY2 else int(ptr), base=QtWidgets.QWidget) + controlWidget.setContentsMargins(0, 0, 0, 0) + controlWidget.setMinimumWidth(0) + + attrEdit = [wdgt for wdgt in controlWidget.children() if type(wdgt) == QtWidgets.QLineEdit] + [wdgt.setParent(attrEdit[0]) for wdgt in controlWidget.children() + if type(wdgt) == QtCore.QObject] + + attrEdit[0].setParent(None) + controlWidget.setParent(attrEdit[0]) + controlWidget.setHidden(True) + return attrEdit[0], mAttrFeild + + +def HLine(): + """seporator line for widgets + + Returns: + Qframe: line for seperating UI elements visually + """ + seperatorLine = QtWidgets.QFrame() + seperatorLine.setFrameShape(QtWidgets.QFrame.HLine) + seperatorLine.setFrameShadow(QtWidgets.QFrame.Sunken) + return seperatorLine + + +def VLine(): + """seporator line for widgets + + Returns: + Qframe: line for seperating UI elements visually + """ + seperatorLine = QtWidgets.QFrame() + seperatorLine.setFrameShape(QtWidgets.QFrame.VLine) + seperatorLine.setFrameShadow(QtWidgets.QFrame.Sunken) + return seperatorLine + + +def show(dockable=True, newSceneCallBack=True, *args): + """To launch the UI and ensure any previously opened instance is closed. + + Returns: + DistributeUI: instance + + Args: + *args: Description + :param newSceneCallBack: + :param dockable: + """ + global RBF_UI # Ensure we have access to the global variable + + # Attempt to close any existing UI with the given name + if mc.workspaceControl(WORK_SPACE_NAME, exists=True): + mc.deleteUI(WORK_SPACE_NAME) + + # Create the UI + RBF_UI = RBFManagerUI(newSceneCallBack=newSceneCallBack) + RBF_UI.initializePoseControlWidgets() + + # Check if we've saved a size previously and set it + if mc.optionVar(exists='RBF_UI_width') and mc.optionVar(exists='RBF_UI_height'): + saved_width = mc.optionVar(query='RBF_UI_width') + saved_height = mc.optionVar(query='RBF_UI_height') + RBF_UI.resize(saved_width, saved_height) + + # Show the UI. + RBF_UI.show(dockable=dockable) + return RBF_UI + + +def genericWarning(parent, warningText): + """generic prompt warning with the provided text + + Args: + parent (QWidget): Qwidget to be parented under + warningText (str): information to display to the user + + Returns: + QtCore.Response: of what the user chose. For warnings + """ + selWarning = QtWidgets.QMessageBox(parent) + selWarning.setText(warningText) + results = selWarning.exec_() + return results + + +def promptAcceptance(parent, descriptionA, descriptionB): + """Warn user, asking for permission + + Args: + parent (QWidget): to be parented under + descriptionA (str): info + descriptionB (str): further info + + Returns: + QtCore.Response: accept, deline, reject + """ + msgBox = QtWidgets.QMessageBox(parent) + msgBox.setText(descriptionA) + msgBox.setInformativeText(descriptionB) + msgBox.setStandardButtons(QtWidgets.QMessageBox.Ok | + QtWidgets.QMessageBox.Cancel) + msgBox.setDefaultButton(QtWidgets.QMessageBox.Cancel) + decision = msgBox.exec_() + return decision + + +class ClickableLineEdit(QtWidgets.QLineEdit): + + """subclass to allow for clickable lineEdit, as a button + + Attributes: + clicked (QtCore.Signal): emitted when clicked + """ + + clicked = QtCore.Signal(str) + + def mousePressEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + self.clicked.emit(self.text()) + else: + super(ClickableLineEdit, self).mousePressEvent(event) + + +class TabBar(QtWidgets.QTabBar): + """Subclass to get a taller tab widget, for readability + """ + + def __init__(self): + super(TabBar, self).__init__() + + def tabSizeHint(self, index): + width = QtWidgets.QTabBar.tabSizeHint(self, index).width() + return QtCore.QSize(width, 25) + + +class RBFWidget(MayaQWidgetDockableMixin, QtWidgets.QMainWindow): + + def __init__(self, parent=pyqt.maya_main_window()): + super(RBFWidget, self).__init__(parent=parent) + + # UI info ------------------------------------------------------------- + self.callBackID = None + self.setWindowTitle(TOOL_TITLE) + self.setObjectName(UI_NAME) + self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) + self.genericWidgetHight = 24 + + @staticmethod + def deleteAssociatedWidgetsMaya(widget, attrName="associatedMaya"): + """delete core ui items 'associated' with the provided widgets + + Args: + widget (QWidget): Widget that has the associated attr set + attrName (str, optional): class attr to query + """ + if hasattr(widget, attrName): + for t in getattr(widget, attrName): + try: + mc.deleteUI(t, ctl=True) + except Exception: + pass + else: + setattr(widget, attrName, []) + + @staticmethod + def deleteAssociatedWidgets(widget, attrName="associated"): + """delete widget items 'associated' with the provided widgets + + Args: + widget (QWidget): Widget that has the associated attr set + attrName (str, optional): class attr to query + """ + if hasattr(widget, attrName): + for t in getattr(widget, attrName): + try: + t.deleteLater() + except Exception: + pass + else: + setattr(widget, attrName, []) + + @staticmethod + def _associateRBFnodeAndWidget(tabDrivenWidget, rbfNode): + """associates the RBFNode with a widget for convenience when adding, + deleting, editing + + Args: + tabDrivenWidget (QWidget): tab widget + rbfNode (RBFNode): instance to be associated + """ + setattr(tabDrivenWidget, "rbfNode", rbfNode) + + @staticmethod + def createCustomButton(label, size=(35, 27), tooltip=""): + stylesheet = ( + "QPushButton {background-color: #5D5D5D; border-radius: 4px;}" + "QPushButton:pressed { background-color: #00A6F3;}" + "QPushButton:hover:!pressed { background-color: #707070;}" + "QPushButton:disabled { background-color: #4a4a4a;}" + ) + button = QtWidgets.QPushButton(label) + button.setMinimumSize(QtCore.QSize(*size)) + button.setStyleSheet(stylesheet) + button.setToolTip(tooltip) + return button + + @staticmethod + def createSetupSelector2Widget(): + rbfVLayout = QtWidgets.QVBoxLayout() + rbfListWidget = QtWidgets.QListWidget() + rbfVLayout.addWidget(rbfListWidget) + return rbfVLayout, rbfListWidget + + @staticmethod + def labelListWidget(label, attrListType, horizontal=True): + """create the listAttribute that users can select their driver/driven + attributes for the setup + + Args: + label (str): to display above the listWidget + horizontal (bool, optional): should the label be above or infront + of the listWidget + + Returns: + list: QLayout, QListWidget + """ + if horizontal: + attributeLayout = QtWidgets.QHBoxLayout() + else: + attributeLayout = QtWidgets.QVBoxLayout() + attributeLabel = QtWidgets.QLabel(label) + attributeListWidget = QtWidgets.QListWidget() + attributeListWidget.setObjectName("{}ListWidget".format(attrListType)) + attributeLayout.addWidget(attributeLabel) + attributeLayout.addWidget(attributeListWidget) + return attributeLayout, attributeListWidget + + @staticmethod + def addRemoveButtonWidget(label1, label2, horizontal=True): + if horizontal: + addRemoveLayout = QtWidgets.QHBoxLayout() + else: + addRemoveLayout = QtWidgets.QVBoxLayout() + addAttributesButton = QtWidgets.QPushButton(label1) + removeAttributesButton = QtWidgets.QPushButton(label2) + addRemoveLayout.addWidget(addAttributesButton) + addRemoveLayout.addWidget(removeAttributesButton) + return addRemoveLayout, addAttributesButton, removeAttributesButton + + def selectNodeWidget(self, label, buttonLabel="Select"): + """create a lout with label, lineEdit, QPushbutton for user input + """ + stylesheet = ( + "QLineEdit { background-color: #404040;" + "border-radius: 4px;" + "border-color: #505050;" + "border-style: solid;" + "border-width: 1.4px;}" + ) + + nodeLayout = QtWidgets.QHBoxLayout() + nodeLayout.setSpacing(4) + + nodeLabel = QtWidgets.QLabel(label) + nodeLabel.setFixedWidth(40) + nodeLineEdit = ClickableLineEdit() + nodeLineEdit.setStyleSheet(stylesheet) + nodeLineEdit.setReadOnly(True) + nodeSelectButton = self.createCustomButton(buttonLabel) + nodeSelectButton.setFixedWidth(40) + nodeLineEdit.setFixedHeight(self.genericWidgetHight) + nodeSelectButton.setFixedHeight(self.genericWidgetHight) + nodeLayout.addWidget(nodeLabel) + nodeLayout.addWidget(nodeLineEdit, 1) + nodeLayout.addWidget(nodeSelectButton) + return nodeLayout, nodeLineEdit, nodeSelectButton + + def createSetupSelectorWidget(self): + """create the top portion of the weidget, select setup + refresh + + Returns: + list: QLayout, QCombobox, QPushButton + """ + setRBFLayout = QtWidgets.QHBoxLayout() + rbfLabel = QtWidgets.QLabel("Select RBF Setup:") + rbf_cbox = QtWidgets.QComboBox() + rbf_refreshButton = self.createCustomButton("Refresh", (60, 25), "Refresh the UI") + rbf_cbox.setFixedHeight(self.genericWidgetHight) + rbf_refreshButton.setMaximumWidth(80) + rbf_refreshButton.setFixedHeight(self.genericWidgetHight - 1) + setRBFLayout.addWidget(rbfLabel) + setRBFLayout.addWidget(rbf_cbox, 1) + setRBFLayout.addWidget(rbf_refreshButton) + return setRBFLayout, rbf_cbox, rbf_refreshButton + + def createDriverAttributeWidget(self): + """widget where the user inputs information for the setups + + Returns: + list: [of widgets] + """ + driverControlVLayout = QtWidgets.QVBoxLayout() + driverControlHLayout = QtWidgets.QHBoxLayout() + + # driverMainLayout.setStyleSheet("QVBoxLayout { background-color: #404040;") + driverControlHLayout.setSpacing(3) + # -------------------------------------------------------------------- + (controlLayout, + controlLineEdit, + setControlButton) = self.selectNodeWidget("Control", buttonLabel="Set") + controlLineEdit.setToolTip("The node driving the setup. (Click me!)") + # -------------------------------------------------------------------- + (driverLayout, + driverLineEdit, + driverSelectButton) = self.selectNodeWidget("Driver", buttonLabel="Set") + driverLineEdit.setToolTip("The node driving the setup. (Click me!)") + # -------------------------------------------------------------------- + allButton = self.createCustomButton("All", (20, 53), "") + + (attributeLayout, attributeListWidget) = self.labelListWidget( + label="Select Driver Attributes:", attrListType="driver", horizontal=False) + + attributeListWidget.setToolTip("List of attributes driving setup.") + selType = QtWidgets.QAbstractItemView.ExtendedSelection + attributeListWidget.setSelectionMode(selType) + attributeListWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + # -------------------------------------------------------------------- + driverControlVLayout.addLayout(controlLayout, 0) + driverControlVLayout.addLayout(driverLayout, 0) + driverControlHLayout.addLayout(driverControlVLayout, 0) + driverControlHLayout.addWidget(allButton, 0) + return [controlLineEdit, + setControlButton, + driverLineEdit, + driverSelectButton, + allButton, + attributeListWidget, + attributeLayout, + driverControlHLayout] + + def createDrivenAttributeWidget(self): + """the widget that displays the driven information + + Returns: + list: [of widgets] + """ + drivenWidget = QtWidgets.QWidget() + drivenMainLayout = QtWidgets.QVBoxLayout() + drivenMainLayout.setContentsMargins(0, 10, 0, 2) + drivenMainLayout.setSpacing(9) + drivenSetLayout = QtWidgets.QVBoxLayout() + drivenMainLayout.addLayout(drivenSetLayout) + drivenWidget.setLayout(drivenMainLayout) + # -------------------------------------------------------------------- + (drivenLayout, + drivenLineEdit, + drivenSelectButton) = self.selectNodeWidget("Driven", buttonLabel="Set") + drivenTip = "The node being driven by setup. (Click me!)" + drivenLineEdit.setToolTip(drivenTip) + + addDrivenButton = self.createCustomButton("+", (20, 26), "") + addDrivenButton.setToolTip("Add a new driven to the current rbf node") + # -------------------------------------------------------------------- + (attributeLayout, + attributeListWidget) = self.labelListWidget(label="Select Driven Attributes:", + attrListType="driven", + horizontal=False) + attributeListWidget.setToolTip("Attributes being driven by setup.") + attributeLayout.setSpacing(1) + selType = QtWidgets.QAbstractItemView.ExtendedSelection + attributeListWidget.setSelectionMode(selType) + attributeListWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + # -------------------------------------------------------------------- + drivenSetLayout.addLayout(drivenLayout, 0) + drivenSetLayout.addLayout(attributeLayout, 0) + drivenLayout.addWidget(addDrivenButton) + drivenSetLayout.addLayout(drivenLayout, 0) + drivenSetLayout.addLayout(attributeLayout, 0) + drivenMainLayout.addLayout(drivenSetLayout) + drivenWidget.setLayout(drivenMainLayout) + return [drivenLineEdit, + drivenSelectButton, + addDrivenButton, + attributeListWidget, + drivenWidget, + drivenMainLayout] + + def createDrivenWidget(self): + """the widget that displays the driven information + + Returns: + list: [of widgets] + """ + drivenWidget = QtWidgets.QWidget() + drivenMainLayout = QtWidgets.QHBoxLayout() + drivenMainLayout.setContentsMargins(0, 0, 0, 0) + drivenMainLayout.setSpacing(9) + drivenWidget.setLayout(drivenMainLayout) + + tableWidget = self.createTableWidget() + drivenMainLayout.addWidget(tableWidget, 1) + return drivenWidget, tableWidget + + def createTableWidget(self): + """create table widget used to display poses, set tooltips and colum + + Returns: + QTableWidget: QTableWidget + """ + stylesheet = """ + QTableWidget QHeaderView::section { + background-color: #3a3b3b; + padding: 2px; + text-align: center; + } + QTableCornerButton::section { + background-color: #3a3b3b; + border: none; + } + """ + tableWidget = QtWidgets.QTableWidget() + tableWidget.insertColumn(0) + tableWidget.insertRow(0) + tableWidget.setHorizontalHeaderLabels(["Pose Value"]) + tableWidget.setVerticalHeaderLabels(["Pose #0"]) + # tableWidget.setStyleSheet(stylesheet) + tableTip = "Live connections to the RBF Node in your setup." + tableTip = tableTip + "\nSelect the desired Pose # to recall pose." + tableWidget.setToolTip(tableTip) + return tableWidget + + def createTabWidget(self): + """Tab widget to add driven widgets too. Custom TabBar so the tab is + easier to select + + Returns: + QTabWidget: + """ + tabLayout = QtWidgets.QTabWidget() + tabLayout.setContentsMargins(0, 0, 0, 0) + tabBar = TabBar() + tabLayout.setTabBar(tabBar) + tabBar.setTabsClosable(True) + return tabLayout + + def createOptionsButtonsWidget(self): + """add, edit, delete buttons for modifying rbf setups. + + Returns: + list: [QPushButtons] + """ + optionsLayout = QtWidgets.QHBoxLayout() + optionsLayout.setSpacing(5) + addTip = "After positioning all controls in the setup, add new pose." + addTip = addTip + "\nEnsure the driver node has a unique position." + addPoseButton = self.createCustomButton("Add Pose", size=(80, 26), tooltip=addTip) + EditPoseButton = self.createCustomButton("Update Pose", size=(80, 26)) + EditPoseButton.setToolTip("Recall pose, adjust controls and Edit.") + EditPoseValuesButton = self.createCustomButton("Update Pose Values", size=(80, 26)) + EditPoseValuesButton.setToolTip("Set pose based on values in table") + deletePoseButton = self.createCustomButton("Delete Pose", size=(80, 26)) + deletePoseButton.setToolTip("Recall pose, then Delete") + optionsLayout.addWidget(addPoseButton) + optionsLayout.addWidget(EditPoseButton) + optionsLayout.addWidget(EditPoseValuesButton) + optionsLayout.addWidget(deletePoseButton) + return (optionsLayout, + addPoseButton, + EditPoseButton, + EditPoseValuesButton, + deletePoseButton) + + def createDarkContainerWidget(self): + darkContainer = QtWidgets.QWidget() + driverMainLayout = QtWidgets.QVBoxLayout() + driverMainLayout.setContentsMargins(10, 10, 10, 10) # Adjust margins as needed + driverMainLayout.setSpacing(5) # Adjust spacing between widgets + + # Setting the dark color (Example: dark gray) + # darkContainer.setStyleSheet("background-color: #323232;") + + # Driver section + (self.controlLineEdit, + self.setControlButton, + self.driverLineEdit, + self.setDriverButton, + self.allButton, + self.driverAttributesWidget, + self.driverAttributesLayout, + driverControlLayout) = self.createDriverAttributeWidget() + + # Driven section + (self.drivenLineEdit, + self.setDrivenButton, + self.addDrivenButton, + self.drivenAttributesWidget, + self.drivenWidget, + self.drivenMainLayout) = self.createDrivenAttributeWidget() + + self.addRbfButton = self.createCustomButton("New RBF") + self.addRbfButton.setToolTip("Select node to be driven by setup.") + stylesheet = ( + "QPushButton {background-color: #179e83; border-radius: 4px;}" + "QPushButton:hover:!pressed { background-color: #2ea88f;}" + ) + self.addRbfButton.setStyleSheet(stylesheet) + + # Setting up the main layout for driver and driven sections + driverMainLayout = QtWidgets.QVBoxLayout() + driverMainLayout.addLayout(driverControlLayout) + driverMainLayout.addLayout(self.driverAttributesLayout) + driverMainLayout.addWidget(self.drivenWidget) + driverMainLayout.addWidget(self.addRbfButton) + darkContainer.setLayout(driverMainLayout) + + return darkContainer + + def createDriverDrivenTableWidget(self): + tableContainer = QtWidgets.QWidget() + + # Setting up the main layout for driver and driven sections + driverDrivenTableLayout = QtWidgets.QVBoxLayout() + self.driverPoseTableWidget = self.createTableWidget() + self.rbfTabWidget = self.createTabWidget() + + # Options buttons section + (optionsLayout, + self.addPoseButton, + self.editPoseButton, + self.editPoseValuesButton, + self.deletePoseButton) = self.createOptionsButtonsWidget() + self.addPoseButton.setEnabled(False) + self.editPoseButton.setEnabled(False) + self.editPoseValuesButton.setEnabled(False) + self.deletePoseButton.setEnabled(False) + + driverDrivenTableLayout.addWidget(self.driverPoseTableWidget, 1) + driverDrivenTableLayout.addWidget(self.rbfTabWidget, 1) + driverDrivenTableLayout.addLayout(optionsLayout) + tableContainer.setLayout(driverDrivenTableLayout) + + return tableContainer + + +class RBFTables(RBFWidget): + + def __init__(self): + pass + + +class RBFManagerUI(RBFWidget): + + """A manager for creating, mirroring, importing/exporting poses created + for RBF type nodes. + + Attributes: + absWorld (bool): Type of pose info look up, world vs local + addRbfButton (QPushButton): button for adding RBFs to setup + allSetupsInfo (dict): setupName:[of all the RBFNodes in scene] + attrMenu (TYPE): Description + currentRBFSetupNodes (list): currently selected setup nodes(userSelect) + driverPoseTableWidget (QTableWidget): poseInfo for the driver node + genericWidgetHight (int): convenience to adjust height of all buttons + mousePosition (QPose): if tracking mouse position on UI + rbfTabWidget (QTabWidget): where the driven table node info is + displayed + """ + + mousePosition = QtCore.Signal(int, int) + + def __init__(self, hideMenuBar=False, newSceneCallBack=True): + super(RBFManagerUI, self).__init__() + + self.absWorld = True + self.zeroedDefaults = True + self.currentRBFSetupNodes = [] + self.allSetupsInfo = None + self.drivenWidget = [] + self.driverAutoAttr = [] + self.drivenAutoAttr = [] + + self.setMenuBar(self.createMenuBar(hideMenuBar=hideMenuBar)) + self.setCentralWidget(self.createCentralWidget()) + self.centralWidget().setMouseTracking(True) + self.refreshRbfSetupList() + self.connectSignals() + + # added because the dockableMixin makes the ui appear small + self.adjustSize() + # self.resize(800, 650) + if newSceneCallBack: + self.newSceneCallBack() + + def closeEvent(self, event): + """Overridden close event to save the size of the UI.""" + width = self.width() + height = self.height() + + # Save the size to Maya's optionVars + mc.optionVar(intValue=('RBF_UI_width', width)) + mc.optionVar(intValue=('RBF_UI_height', height)) + + self.deleteAssociatedWidgetsMaya(self.driverPoseTableWidget) + self.deleteAssociatedWidgets(self.driverPoseTableWidget) + if self.callBackID is not None: + self.removeSceneCallback() + + # Call the parent class's closeEvent + super(RBFManagerUI, self).closeEvent(event) + + def callBackFunc(self, *args): + """super safe function for trying to refresh the UI, should anything + fail. + + Args: + *args: Description + """ + try: + self.refresh() + except Exception: + pass + + def removeSceneCallback(self): + """remove the callback associated witht he UI, quietly fail. + """ + try: + om.MSceneMessage.removeCallback(self.callBackID) + except Exception as e: + print("CallBack removal failure:") + print(e) + + def newSceneCallBack(self): + """create a new scene callback to refresh the UI when scene changes. + """ + callBackType = om.MSceneMessage.kSceneUpdate + try: + func = self.callBackFunc + obj = om.MSceneMessage.addCallback(callBackType, func) + self.callBackID = obj + except Exception as e: + print(e) + self.callBackID = None + + # general functions ------------------------------------------------------- + def getSelectedSetup(self): + """return the string name of the selected setup from user and type + + Returns: + str, str: name, nodeType + """ + selectedSetup = self.rbf_cbox.currentText() + if selectedSetup.startswith("New"): + setupType = selectedSetup.split(" ")[1] + return None, setupType + else: + return selectedSetup, self.currentRBFSetupNodes[0].rbfType + + def getDrivenNodesFromSetup(self): + """based on the user selected setup, get the associated RBF nodes + + Returns: + list: driven rbfnodes + """ + drivenNodes = [] + for rbfNode in self.currentRBFSetupNodes: + drivenNodes.extend(rbfNode.getDrivenNode) + return drivenNodes + + def __deleteSetup(self): + decision = promptAcceptance(self, + "Delete current Setup?", + "This will delete all RBF nodes in setup.") + if decision in [QtWidgets.QMessageBox.Discard, + QtWidgets.QMessageBox.Cancel]: + return + self.deleteSetup() + + def deleteSetup(self, setupName=None): + """Delete all the nodes within a setup. + + Args: + setupName (None, optional): Description + """ + setupType = None + if setupName is None: + setupName, setupType = self.getSelectedSetup() + nodesToDelete = self.allSetupsInfo.get(setupName, []) + for rbfNode in nodesToDelete: + drivenNode = rbfNode.getDrivenNode() + rbfNode.deleteRBFToggleAttr() + if drivenNode: + rbf_node.removeDrivenGroup(drivenNode[0]) + mc.delete(rbfNode.transformNode) + self.refresh() + + def removeRBFFromSetup(self, drivenWidgetIndex): + """remove RBF tab from setup. Delete driven group, attrs and clean up + + Args: + drivenWidgetIndex (QWidget): parent widget that houses the contents + and info of the rbf node + + Returns: + n/a: n/a + """ + decision = promptAcceptance(self, + "Are you sure you want to remove node?", + "This will delete the RBF & driven node.") + if decision in [QtWidgets.QMessageBox.Discard, + QtWidgets.QMessageBox.Cancel]: + return + drivenWidget = self.rbfTabWidget.widget(drivenWidgetIndex) + self.rbfTabWidget.removeTab(drivenWidgetIndex) + rbfNode = getattr(drivenWidget, "rbfNode") + self.deleteAssociatedWidgets(drivenWidget, attrName="associated") + drivenWidget.deleteLater() + drivenNode = rbfNode.getDrivenNode() + rbfNode.deleteRBFToggleAttr() + if drivenNode and drivenNode[0].endswith(rbf_node.DRIVEN_SUFFIX): + rbf_node.removeDrivenGroup(drivenNode[0]) + mc.delete(rbfNode.transformNode) + self.currentRBFSetupNodes.remove(rbfNode) + if self.rbfTabWidget.count() == 0: + self.refresh(rbfSelection=True, + driverSelection=True, + drivenSelection=True, + currentRBFSetupNodes=True) + else: + self.refreshAllTables() + + def addRBFToSetup(self): + """query the user in case of a new setup or adding additional RBFs to + existing. + + Returns: + TYPE: Description + """ + result = self.preValidationCheck() + + driverControl = result["driverControl"] + driverNode, drivenNode = result["driverNode"], result["drivenNode"] + driverAttrs, drivenAttrs = result["driverAttrs"], result["drivenAttrs"] + + drivenType = mc.nodeType(drivenNode) + if drivenType in ["transform", "joint"]: + drivenNode_name = rbf_node.get_driven_group_name(drivenNode) + else: + drivenNode_name = drivenNode + + # Check if there is an existing rbf node attached + if mc.objExists(drivenNode_name): + if existing_rbf_setup(drivenNode_name): + msg = "Node is already driven by an RBF Setup." + genericWarning(self, msg) + return + + setupName, rbfType = self.getSelectedSetup() + + parentNode = False + if drivenType in ["transform", "joint"]: + parentNode = True + drivenNode = rbf_node.addDrivenGroup(drivenNode) + + # Create RBFNode instance, apply settings + if not setupName: + setupName = "{}_WD".format(driverNode) + rbfNode = sortRBF(drivenNode, rbfType=rbfType) + rbfNode.setSetupName(setupName) + rbfNode.setDriverControlAttr(driverControl) + rbfNode.setDriverNode(driverNode, driverAttrs) + defaultVals = rbfNode.setDrivenNode(drivenNode, drivenAttrs, parent=parentNode) + + # Check if there are any preexisting nodes in setup, if so copy pose index + if self.currentRBFSetupNodes: + print("Syncing poses indices from {} >> {}".format(self.currentRBFSetupNodes[0], rbfNode)) + rbfNode.syncPoseIndices(self.currentRBFSetupNodes[0]) + self.addNewTab(rbfNode, drivenNode) + self.setAttributeDisplay(self.drivenAttributesWidget, drivenNode, drivenAttrs) + else: + if self.zeroedDefaults: + rbfNode.applyDefaultPose() + else: + poseInputs = rbf_node.getMultipleAttrs(driverNode, driverAttrs) + rbfNode.addPose(poseInput=poseInputs, poseValue=defaultVals[1::2]) + + self.populateDriverInfo(rbfNode, rbfNode.getNodeInfo()) + drivenWidget = self.populateDrivenInfo(rbfNode, rbfNode.getNodeInfo()) + + # Add the driven widget to the tab widget. + drivenWidget.setProperty("drivenNode", drivenNode) + self.rbfTabWidget.addTab(drivenWidget, rbfNode.name) + self.setDrivenTable(drivenWidget, rbfNode, rbfNode.getNodeInfo()) + + # Add newly created RBFNode to list of current + self.addPoseButton.setEnabled(True) + + self.currentRBFSetupNodes.append(rbfNode) + self.refreshRbfSetupList(setToSelection=setupName) + self.lockDriverWidgets() + mc.select(driverControl) + + def preValidationCheck(self): + # Fetch data from UI fields + driverNode = self.driverLineEdit.text() + drivenNode = self.drivenLineEdit.text() + driverControl = self.controlLineEdit.text() + driverSelectedAttrItems = self.driverAttributesWidget.selectedItems() + drivenSelectedAttrItems = self.drivenAttributesWidget.selectedItems() + + # Create a default return dictionary with None values + result = { + "driverNode": None, + "drivenNode": None, + "driverControl": None, + "driverAttrs": None, + "drivenAttrs": None + } + + # Ensure driverNode and drivenNode are provided + if not driverNode or not drivenNode: + return result + + # Ensure attributes are selected in the widgets + if not driverSelectedAttrItems or not drivenSelectedAttrItems: + return result + + # Check if the driven node is the same as the driver node + if drivenNode == driverNode: + genericWarning(self, "Select Node to be driven!") + return result + + # Update the result dictionary with the fetched values + result["driverNode"] = driverNode + result["drivenNode"] = drivenNode + result["driverControl"] = driverControl + result["driverAttrs"] = [item.text().split(".")[1] for item in driverSelectedAttrItems] + result["drivenAttrs"] = [item.text().split(".")[1] for item in drivenSelectedAttrItems] + + return result + + def refreshAllTables(self): + """Refresh all tables on all the tabs with the latest information + """ + # Iterate through each tab in the widget + for index in range(self.rbfTabWidget.count()): + drivenWidget = self.rbfTabWidget.widget(index) + drivenNodeName = drivenWidget.property("drivenNode") + + # Update table if the rbfNode's drivenNode matches the current tab's drivenNode + for rbfNode in self.currentRBFSetupNodes: + drivenNodes = rbfNode.getDrivenNode() + if drivenNodes and drivenNodes[0] == drivenNodeName: + weightInfo = rbfNode.getNodeInfo() + self.setDriverTable(rbfNode, weightInfo) + self.setDrivenTable(drivenWidget, rbfNode, weightInfo) + + @staticmethod + def determineAttrType(node): + nodeType = mc.nodeType(node) + if nodeType in ["transform", "joint"]: + keyAttrs = mc.listAttr(node, keyable=True) or [] + requiredAttrs = [ + "{}{}".format(attrType, xyz) + for xyz in "XYZ" + for attrType in ["translate", "rotate", "scale"] + ] + + if not any(attr in keyAttrs for attr in requiredAttrs): + return "cb" + return "keyable" + return "all" + + def deletePose(self): + """delete a pose from the UI and all the RBFNodes in the setup. + + Returns: + n/a: n/a + """ + driverRow = self.driverPoseTableWidget.currentRow() + drivenWidget = self.rbfTabWidget.currentWidget() + drivenTableWidget = getattr(drivenWidget, "tableWidget") + drivenRow = drivenTableWidget.currentRow() + # TODO if one is allow syncing of nodes of different lengths + # it should be done here + if drivenRow != driverRow or drivenRow == -1: + genericWarning(self, "Select Pose # to be deleted.") + return + + for rbfNode in self.currentRBFSetupNodes: + rbfNode.deletePose(indexToPop=drivenRow) + self.refreshAllTables() + + def editPose(self): + """edit an existing pose. Specify the index + + Returns: + TYPE: Description + """ + rbfNodes = self.currentRBFSetupNodes + if not rbfNodes: + return + driverRow = self.driverPoseTableWidget.currentRow() + drivenWidget = self.rbfTabWidget.currentWidget() + drivenTableWidget = getattr(drivenWidget, "tableWidget") + drivenRow = drivenTableWidget.currentRow() + if drivenRow != driverRow or drivenRow == -1: + genericWarning(self, "Select Pose # to be Edited.") + return + driverNode = rbfNodes[0].getDriverNode()[0] + driverAttrs = rbfNodes[0].getDriverNodeAttributes() + poseInputs = self.approximateAndRound(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) + for rbfNode in rbfNodes: + poseValues = self.approximateAndRound(rbfNode.getPoseValues(resetDriven=True)) + rbfNode.addPose(poseInput=poseInputs, + poseValue=poseValues, + posesIndex=drivenRow) + rbfNode.forceEvaluation() + self.refreshAllTables() + + def editPoseValues(self): + """Edit an existing pose based on the values in the table + Returns: + None + """ + rbfNodes = self.currentRBFSetupNodes + if not rbfNodes: + return + driverRow = self.driverPoseTableWidget.currentRow() # A number + drivenWidget = self.rbfTabWidget.currentWidget() + drivenTableWidget = getattr(drivenWidget, "tableWidget") + drivenRow = drivenTableWidget.currentRow() + if drivenRow != driverRow or drivenRow == -1: + genericWarning(self, "Select Pose # to be Edited.") + return + driverNode = rbfNodes[0].getDriverNode()[0] + driverAttrs = rbfNodes[0].getDriverNodeAttributes() + poseInputs = self.approximateAndRound(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) + nColumns = drivenTableWidget.columnCount() + entryWidgets = [drivenTableWidget.cellWidget(drivenRow, c) for c in range(nColumns)] + newValues = [float(w.text()) for w in entryWidgets] + rbfNode = getattr(drivenWidget, "rbfNode") + rbfNodes = [rbfNode] + for rbfNode in rbfNodes: + print("rbfNode: {}".format(rbfNode)) + print("poseInputs: {}".format(poseInputs)) + print("New pose values: {}".format(newValues)) + print("poseIndex: {}".format(drivenRow)) + rbfNode.addPose(poseInput=poseInputs, + poseValue=newValues, + posesIndex=drivenRow) + rbfNode.forceEvaluation() + self.refreshAllTables() + + def updateAllFromTables(self): + """Update every pose for the RBF nodes based on the values from the tables. + """ + rbfNodes = self.currentRBFSetupNodes + if not rbfNodes: + return + + # Get common data for all RBF nodes + driverNode = rbfNodes[0].getDriverNode()[0] + driverAttrs = rbfNodes[0].getDriverNodeAttributes() + poseInputs = rbf_node.getMultipleAttrs(driverNode, driverAttrs) + + # Iterate over all widgets in the tab widget + for idx in range(self.rbfTabWidget.count()): + drivenWidget = self.rbfTabWidget.widget(idx) + + # Fetch the table widget associated with the current driven widget + drivenTableWidget = getattr(drivenWidget, "tableWidget") + drivenRow = drivenTableWidget.currentRow() + + # Extract new pose values from the driven table widget + nColumns = drivenTableWidget.columnCount() + entryWidgets = [drivenTableWidget.cellWidget(drivenRow, c) for c in range(nColumns)] + newValues = [float(widget.text()) for widget in entryWidgets] + + # Update the RBF node associated with the current widget/tab + rbfNode = getattr(drivenWidget, "rbfNode") + rbfNode.addPose(poseInput=poseInputs, + poseValue=newValues, + posesIndex=drivenRow) + rbfNode.forceEvaluation() + + # Refresh tables after all updates + self.refreshAllTables() + + def approximateAndRound(self, values, tolerance=1e-10, decimalPlaces=2): + """Approximate small values to zero and rounds the others. + + Args: + values (list of float): The values to approximate and round. + tolerance (float): The tolerance under which a value is considered zero. + decimalPlaces (int): The number of decimal places to round to. + + Returns: + list of float: The approximated and rounded values. + """ + newValues = [] + for v in values: + if abs(v) < tolerance: + newValues.append(0) + else: + newValues.append(round(v, decimalPlaces)) + return newValues + + def addPose(self): + """Add pose to rbf nodes in setup. Additional index on all nodes + + Returns: + TYPE: Description + """ + rbfNodes = self.currentRBFSetupNodes + if not rbfNodes: + return + driverNode = rbfNodes[0].getDriverNode()[0] + driverAttrs = rbfNodes[0].getDriverNodeAttributes() + poseInputs = self.approximateAndRound(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) + for rbfNode in rbfNodes: + poseValues = rbfNode.getPoseValues(resetDriven=True, absoluteWorld=self.absWorld) + poseValues = self.approximateAndRound(poseValues) + rbfNode.addPose(poseInput=poseInputs, poseValue=poseValues) + self.refreshAllTables() + + def updateAllSetupsInfo(self, includeEmpty=False): + """refresh the instance dictionary of all the setps in the scene. + + Args: + includeEmpty (bool, optional): there could be rbf nodes with no + setup names. + """ + self.allSetupsInfo = {} + tmp_dict = rbf_node.getRbfSceneSetupsInfo(includeEmpty=includeEmpty) + for setupName, nodes in tmp_dict.items(): + rbfNodes = [sortRBF(n) for n in nodes] + self.allSetupsInfo[setupName] = sorted(rbfNodes, key=lambda rbf: rbf.name) + + def setNodeToField(self, lineEdit, multi=False): + """take the currently selected node and set its name to the lineedit + provided + + Args: + lineEdit (QLineEdit): widget to set the name to + multi (bool, optional): should multiple nodes be supported + + Returns: + str: str set to the lineedit + """ + selected = mc.ls(sl=True) + if not multi: + selected = [selected[0]] + controlNameData = ", ".join(selected) + lineEdit.setText(controlNameData) + mc.select(cl=True) + return controlNameData + + def setDriverControlLineEdit(self): + selected = mc.ls(sl=True) + if len(selected) == 2: + self.controlLineEdit.setText(selected[0]) + self.driverLineEdit.setText(selected[1]) + elif len(selected) == 1: + self.controlLineEdit.setText(selected[0]) + self.driverLineEdit.setText(selected[0]) + mc.select(cl=True) + + def highlightListEntries(self, listWidget, toHighlight): + """set the items in a listWidget to be highlighted if they are in list + + Args: + listWidget (QListWidget): list to highlight items on + toHighlight (list): of things to highlight + """ + toHighlight = list(toHighlight) + scrollToItems = [] + for index in range(listWidget.count()): + # for qt to check for events like keypress + item = listWidget.item(index) + itemText = item.text() + for desired in toHighlight: + if desired in itemText: + item.setSelected(True) + scrollToItems.append(item) + toHighlight.remove(desired) + if scrollToItems: + listWidget.scrollToItem(scrollToItems[0]) + + def setAttributeDisplay(self, attrListWidget, driverName, displayAttrs): + nodeAttrsToDisplay = ["{}.{}".format(driverName, attr) + for attr in displayAttrs] + attrListWidget.clear() + attrListWidget.addItems(sorted(nodeAttrsToDisplay)) + self.highlightListEntries(attrListWidget, displayAttrs) + + def setAttributeToAutoSelect(self, attrListWidget): + selectedItems = attrListWidget.selectedItems() + selectedTexts = [item.text() for item in selectedItems] + attributes = [attrPlug.split(".")[-1] for attrPlug in selectedTexts] + + if "driver" in attrListWidget.objectName(): + self.driverAutoAttr = attributes + elif "driven" in attrListWidget.objectName(): + self.drivenAutoAttr = attributes + + @staticmethod + def setSelectedForAutoSelect(attrListWidget, itemTexts): + for i in range(attrListWidget.count()): + item = attrListWidget.item(i) + if item.text() in itemTexts: + item.setSelected(True) + + def updateAttributeDisplay(self, + attrListWidget, + driverNames, + highlight=[], + attrType="keyable", + force=False): + """update the provided listwidget with the attrs collected from the + list of nodes provided + + Args: + attrListWidget (QListWidget): widget to update + driverNames (list): of nodes to query for attrs to display + highlight (list, optional): of item entries to highlight + keyable (bool, optional): should the displayed attrs be keyable + + Returns: + n/a: n/a + """ + nodeAttrsToDisplay = [] + if not driverNames: + return + elif "," in driverNames: + driverNames = driverNames.split(", ") + elif type(driverNames) != list: + driverNames = [driverNames] + + if not force: + attrType = self.determineAttrType(driverNames[0]) + + nodeAttrsToDisplay = getPlugAttrs(driverNames, attrType=attrType) + attrListWidget.clear() + attrListWidget.addItems(nodeAttrsToDisplay) + + objName = attrListWidget.objectName() + autoAttrs = { + "driverListWidget": self.driverAutoAttr, "drivenListWidget": self.drivenAutoAttr + } + + if autoAttrs[objName]: + attrPlugs = ["{}.{}".format(driverNames[0], attr) for attr in autoAttrs[objName]] + self.setSelectedForAutoSelect(attrListWidget, attrPlugs) + + if highlight: + self.highlightListEntries(attrListWidget, highlight) + + def syncDriverTableCells(self, attrEdit, rbfAttrPlug): + """When you edit the driver table, it will update all the sibling + rbf nodes in the setup. + + Args: + attrEdit (QLineEdit): cell that was edited in the driver table + rbfAttrPlug (str): node.attr the cell represents + *args: signal throws additional args + """ + attr = rbfAttrPlug.partition(".")[2] + value = attrEdit.text() + for rbfNode in self.currentRBFSetupNodes: + attrPlug = "{}.{}".format(rbfNode, attr) + mc.setAttr(attrPlug, float(value)) + rbfNode.forceEvaluation() + + def initializePoseControlWidgets(self): + """Initialize UI widgets for each pose input based on the information from RBF nodes. + This dynamically creates widgets for the control attributes associated with each pose. + """ + # Retrieve all the RBF nodes from the stored setups info + rbfNodes = self.allSetupsInfo.values() + + # Loop through each RBF node to extract its weight information + for rbfNode in rbfNodes: + weightInfo = rbfNode[0].getNodeInfo() + + # Extract pose information from the weight data + poses = weightInfo.get("poses", None) + if not poses: + continue + # Enumerate through each pose input for this RBF node + for rowIndex, poseInput in enumerate(poses["poseInput"]): + for columnIndex, pValue in enumerate(poseInput): + rbfAttrPlug = "{}.poses[{}].poseInput[{}]".format(rbfNode[0], rowIndex, columnIndex) + + # Create a control widget for this pose input attribute + getControlAttrWidget(rbfAttrPlug, label="") + + def setDriverTable(self, rbfNode, weightInfo): + """Set the driverTable widget with the information from the weightInfo + + Args: + rbfNode (RBFNode): node to query additional info from + weightInfo (dict): to pull information from + + Returns: + n/a: n/a + """ + poses = weightInfo.get("poses", {}) + + # Clean up existing widgets and prepare for new content + self.deleteAssociatedWidgetsMaya(self.driverPoseTableWidget) + self.deleteAssociatedWidgets(self.driverPoseTableWidget) + self.driverPoseTableWidget.clear() + + # Set columns and headers + driverAttrs = weightInfo.get("driverAttrs", []) + self.driverPoseTableWidget.setColumnCount(len(driverAttrs)) + self.driverPoseTableWidget.setHorizontalHeaderLabels(driverAttrs) + + # Set rows + poseInputs = poses.get("poseInput", []) + self.driverPoseTableWidget.setRowCount(len(poseInputs)) + if not poseInputs: + return + + verticalLabels = ["Pose {}".format(index) for index in range(len(poseInputs))] + self.driverPoseTableWidget.setVerticalHeaderLabels(verticalLabels) + + # Populate the table with widgets + tmpWidgets, mayaUiItems = [], [] + for rowIndex, poseInput in enumerate(poses["poseInput"]): + for columnIndex, _ in enumerate(poseInput): + rbfAttrPlug = "{}.poses[{}].poseInput[{}]".format(rbfNode, rowIndex, columnIndex) + attrEdit, mAttrField = getControlAttrWidget(rbfAttrPlug, label="") + + self.driverPoseTableWidget.setCellWidget(rowIndex, columnIndex, attrEdit) + attrEdit.returnPressed.connect( + partial(self.syncDriverTableCells, attrEdit, rbfAttrPlug) + ) + + tmpWidgets.append(attrEdit) + mayaUiItems.append(mAttrField) + + # Populate the table with widgets + setattr(self.driverPoseTableWidget, "associated", tmpWidgets) + setattr(self.driverPoseTableWidget, "associatedMaya", mayaUiItems) + + def lockDriverWidgets(self, lock=True): + """toggle the ability to edit widgets after they have been set + + Args: + lock (bool, optional): should it be locked + """ + self.setDriverButton.blockSignals(lock) + self.setDrivenButton.blockSignals(lock) + if lock: + self.driverAttributesWidget.setEnabled(False) + self.drivenAttributesWidget.setEnabled(False) + else: + self.driverAttributesWidget.setEnabled(True) + self.drivenAttributesWidget.setEnabled(True) + + def populateDriverInfo(self, rbfNode, weightInfo): + """populate the driver widget, driver, control, driving attrs + + Args: + rbfNode (RBFNode): node for query + weightInfo (dict): to pull information from, since we have it + """ + driverNode = weightInfo.get("driverNode", [None])[0] + driverControl = weightInfo.get("driverControl", "") + driverAttrs = weightInfo.get("driverAttrs", []) + + self.driverLineEdit.setText(driverNode or "") + self.controlLineEdit.setText(driverControl) + self.setAttributeDisplay(self.driverAttributesWidget, driverNode, driverAttrs) + self.setDriverTable(rbfNode, weightInfo) + + def populateDrivenInfo(self, rbfNode, weightInfo): + """populate the driver widget, driver, control, driving attrs + + Args: + rbfNode (RBFNode): node for query + weightInfo (dict): to pull information from, since we have it + """ + # Initialize Driven Widget + drivenWidget = self.createAndTagDrivenWidget() + self._associateRBFnodeAndWidget(drivenWidget, rbfNode) + + # Populate Driven Widget Info + self.populateDrivenWidgetInfo(drivenWidget, weightInfo, rbfNode) + + # Set Driven Node and Attributes + drivenNode = weightInfo.get("drivenNode", [None])[0] + self.drivenLineEdit.setText(drivenNode or "") + self.setAttributeDisplay(self.drivenAttributesWidget, drivenNode or "", weightInfo["drivenAttrs"]) + + # Add the driven widget to the tab widget. + drivenWidget.setProperty("drivenNode", drivenNode) + self.rbfTabWidget.addTab(drivenWidget, rbfNode.name) + self.setDrivenTable(drivenWidget, rbfNode, weightInfo) + return drivenWidget + + def createAndTagDrivenWidget(self): + """create and associate a widget, populated with the information + provided by the weightInfo + + Args: + + Returns: + QWidget: parent widget that houses all the information to display + """ + drivenWidget, tableWidget = self.createDrivenWidget() + drivenWidget.tableWidget = tableWidget + + # Set up signals for the table + header = tableWidget.verticalHeader() + header.sectionClicked.connect(self.setConsistentHeaderSelection) + header.sectionClicked.connect(self.recallDriverPose) + tableWidget.itemSelectionChanged.connect(self.setEditDeletePoseEnabled) + return drivenWidget + + @staticmethod + def setDrivenTable(drivenWidget, rbfNode, weightInfo): + """set the widgets with information from the weightInfo for dispaly + + Args: + drivenWidget (QWidget): parent widget, the tab to populate + rbfNode (RBFNode): node associated with widget + weightInfo (dict): of information to display + """ + poses = weightInfo["poses"] + drivenAttrs = weightInfo["drivenAttrs"] + rowCount = len(poses["poseValue"]) + verticalLabels = ["Pose {}".format(index) for index in range(rowCount)] + + drivenWidget.tableWidget.clear() + drivenWidget.tableWidget.setRowCount(rowCount) + drivenWidget.tableWidget.setColumnCount(len(drivenAttrs)) + drivenWidget.tableWidget.setHorizontalHeaderLabels(drivenAttrs) + drivenWidget.tableWidget.setVerticalHeaderLabels(verticalLabels) + + for rowIndex, poseInput in enumerate(poses["poseValue"]): + for columnIndex, pValue in enumerate(poseInput): + rbfAttrPlug = "{}.poses[{}].poseValue[{}]".format(rbfNode, rowIndex, columnIndex) + attrEdit, mAttrFeild = getControlAttrWidget(rbfAttrPlug, label="") + drivenWidget.tableWidget.setCellWidget(rowIndex, columnIndex, attrEdit) + + # Adding QTableWidgetItem for the cell and setting the value + tableItem = QtWidgets.QTableWidgetItem() + tableItem.setFlags(tableItem.flags() | QtCore.Qt.ItemIsEditable) + tableItem.setData(QtCore.Qt.DisplayRole, str(pValue)) + drivenWidget.tableWidget.setItem(rowIndex, columnIndex, tableItem) + + def populateDrivenWidgetInfo(self, drivenWidget, weightInfo, rbfNode): + """set the information from the weightInfo to the widgets child of + drivenWidget + + Args: + drivenWidget (QWidget): parent widget + weightInfo (dict): of information to display + rbfNode (RBFNode): instance of the RBFNode + + Returns: + n/a: n/a + """ + driverNode = weightInfo["drivenNode"] + if driverNode: + driverNode = driverNode[0] + else: + return + + self.setDrivenTable(drivenWidget, rbfNode, weightInfo) + + def addNewTab(self, rbfNode, drivenNode): + """Create a new tab in the setup + + Args: + rbfNode (RBFNode): to pull information from + + Returns: + QWidget: created widget + """ + weightInfo = rbfNode.getNodeInfo() + tabDrivenWidget = self.createAndTagDrivenWidget() + self._associateRBFnodeAndWidget(tabDrivenWidget, rbfNode) + self.rbfTabWidget.addTab(tabDrivenWidget, rbfNode.name) + tabDrivenWidget.setProperty("drivenNode", drivenNode) + self.setDrivenTable(tabDrivenWidget, rbfNode, weightInfo) + + return tabDrivenWidget + + def addNewDriven(self): + self.refresh( + rbfSelection=False, + driverSelection=False, + drivenSelection=True, + currentRBFSetupNodes=False, + clearDrivenTab=False + ) + + self.setDrivenButton.blockSignals(False) + self.drivenAttributesWidget.setEnabled(True) + + self.addRbfButton.setText("Add New Driven") + + def recreateDrivenTabs(self, rbfNodes): + """remove tabs and create ones for each node in rbfNodes provided + + Args: + rbfNodes (list): [of RBFNodes] + """ + rbfNodes = sorted(rbfNodes, key=lambda x: x.name) + self.rbfTabWidget.clear() + for rbfNode in rbfNodes: + weightInfo = rbfNode.getNodeInfo() + drivenWidget = self.createAndTagDrivenWidget() + self._associateRBFnodeAndWidget(drivenWidget, rbfNode) + self.populateDrivenWidgetInfo(drivenWidget, weightInfo, rbfNode) + self.rbfTabWidget.addTab(drivenWidget, rbfNode.name) + + self.addPoseButton.setEnabled(True) + + def displayRBFSetupInfo(self): + """Display the rbfnodes within the desired setups + + Args: + index (int): signal information + + """ + rbfSelection = str(self.rbf_cbox.currentText()) + + # Refresh UI components + self.refresh(rbfSelection=False, + driverSelection=True, + drivenSelection=True, + currentRBFSetupNodes=False) + + # Handle 'New' selection case + if rbfSelection.startswith("New "): + self.currentRBFSetupNodes = [] + self.lockDriverWidgets(lock=False) + return + + # Fetch RBF nodes for the selected setup + rbfNodes = self.allSetupsInfo.get(rbfSelection, []) + if not rbfNodes: + return + + # Display node info in the UI + self.currentRBFSetupNodes = rbfNodes + weightInfo = rbfNodes[0].getNodeInfo() + + self.populateDriverInfo(rbfNodes[0], weightInfo) + self.populateDrivenInfo(rbfNodes[0], weightInfo) + self.lockDriverWidgets(lock=True) + + try: + self.recreateDrivenTabs(self.allSetupsInfo[rbfSelection]) + except AttributeError: + print("Forcing refresh on UI due to error.") + self.refresh(rbfSelection=True, + driverSelection=True, + drivenSelection=True, + currentRBFSetupNodes=True) + + def attrListMenu(self, + attributeListWidget, + driverLineEdit, + attributeListType, + QPos, + nodeToQuery=None): + """right click menu for queie qlistwidget + + Args: + attributeListWidget (QListWidget): widget to display menu over + driverLineEdit (QLineEdit): widget to query the attrs from + QPos (QtCore.QPos): due to the signal, used + nodeToQuery (None, optional): To display attrs from this nodes + for menu placement + + No Longer Returned: + n/a: n/a + """ + if nodeToQuery is None: + nodeToQuery = str(driverLineEdit.text()) + self.attrMenu = QtWidgets.QMenu() + parentPosition = attributeListWidget.mapToGlobal(QtCore.QPoint(0, 0)) + menu_item_01 = self.attrMenu.addAction("Display Keyable") + menu_item_01.setToolTip("Show Keyable Attributes") + menu_item_01.triggered.connect(partial(self.updateAttributeDisplay, + attributeListWidget, + nodeToQuery, + attrType="keyable", + force=True)) + menu2Label = "Display ChannelBox (Non Keyable)" + menu_item_02 = self.attrMenu.addAction(menu2Label) + menu2tip = "Show attributes in ChannelBox that are not keyable." + menu_item_02.setToolTip(menu2tip) + menu_item_02.triggered.connect(partial(self.updateAttributeDisplay, + attributeListWidget, + nodeToQuery, + attrType="cb", + force=True)) + menu_item_03 = self.attrMenu.addAction("Display All") + menu_item_03.setToolTip("GIVE ME ALL!") + menu_item_03.triggered.connect(partial(self.updateAttributeDisplay, + attributeListWidget, + nodeToQuery, + attrType="all", + force=True)) + + self.attrMenu.addSeparator() + + menu_item_04 = self.attrMenu.addAction("Set attribute to auto select") + menu_item_04.setToolTip("Set your attribute to be automatically highlighted up in the next operations") + menu_item_04.triggered.connect(partial(self.setAttributeToAutoSelect, + attributeListWidget)) + self.attrMenu.move(parentPosition + QPos) + self.attrMenu.show() + + def refreshRbfSetupList(self, setToSelection=False): + """refresh the list of setups the user may select from + + Args: + setToSelection (bool, optional): after refresh, set to desired + """ + self.rbf_cbox.blockSignals(True) + + # Clear the combo box and populate with new setup options + self.rbf_cbox.clear() + self.updateAllSetupsInfo() + allSetups = sorted(self.allSetupsInfo.keys()) + newSetupOptions = ["New {} setup".format(rbf) for rbf in rbf_node.SUPPORTED_RBF_NODES] + self.rbf_cbox.addItems(newSetupOptions + allSetups) + + if setToSelection: + selectionIndex = self.rbf_cbox.findText(setToSelection) + self.rbf_cbox.setCurrentIndex(selectionIndex) + else: + self.lockDriverWidgets(lock=False) + self.rbf_cbox.blockSignals(False) + + def clearDriverTabs(self): + """force deletion on tab widgets + """ + toRemove = [] + tabIndicies = self.driverPoseTableWidget.count() + for index in range(tabIndicies): + tabWidget = self.driverPoseTableWidget.widget(index) + toRemove.append(tabWidget) + self.driverPoseTableWidget.clear() + [t.deleteLater() for t in toRemove] + + def clearDrivenTabs(self): + """force deletion on tab widgets + """ + toRemove = [] + tabIndicies = self.rbfTabWidget.count() + for index in range(tabIndicies): + tabWidget = self.rbfTabWidget.widget(index) + toRemove.append(tabWidget) + self.rbfTabWidget.clear() + [t.deleteLater() for t in toRemove] + + def refresh(self, + rbfSelection=True, + driverSelection=True, + drivenSelection=True, + currentRBFSetupNodes=True, + clearDrivenTab=True, + *args): + """Refreshes the UI + + Args: + rbfSelection (bool, optional): desired section to refresh + driverSelection (bool, optional): desired section to refresh + drivenSelection (bool, optional): desired section to refresh + currentRBFSetupNodes (bool, optional): desired section to refresh + clearDrivenTab (bool, optional): desired section to refresh + """ + self.addRbfButton.setText("New RBF") + self.addPoseButton.setEnabled(False) + if rbfSelection: + self.refreshRbfSetupList() + if driverSelection: + self.controlLineEdit.clear() + self.driverLineEdit.clear() + self.driverAttributesWidget.clear() + self.driverPoseTableWidget.clear() + + self.driverPoseTableWidget.setRowCount(1) + self.driverPoseTableWidget.setColumnCount(1) + self.driverPoseTableWidget.setHorizontalHeaderLabels(["Pose Value"]) + self.driverPoseTableWidget.setVerticalHeaderLabels(["Pose #0"]) + + if drivenSelection: + self.drivenLineEdit.clear() + self.drivenAttributesWidget.clear() + if clearDrivenTab: + self.rbfTabWidget.clear() + if currentRBFSetupNodes: + self.currentRBFSetupNodes = [] + + def recallDriverPose(self, indexSelected): + """recall a pose recorded from one of the RBFNodes in currentSelection + it should not matter when RBFNode in setup is selected as they + should all be in sync + + Args: + indexSelected (int): index of the pose to recall + + Returns: + n/a: nada + """ + if not self.currentRBFSetupNodes: + return + self.currentRBFSetupNodes[0].recallDriverPose(indexSelected) + + def setConsistentHeaderSelection(self, headerIndex): + """when a pose is selected in one table, ensure the selection in all + other tables, to avoid visual confusion + + Args: + headerIndex (int): desired header to highlight + """ + self.driverPoseTableWidget.blockSignals(True) + self.driverPoseTableWidget.selectRow(headerIndex) + self.driverPoseTableWidget.blockSignals(False) + for index in range(self.rbfTabWidget.count()): + drivenWidget = self.rbfTabWidget.widget(index) + drivenTableWidget = getattr(drivenWidget, "tableWidget") + drivenTableWidget.blockSignals(True) + drivenTableWidget.selectRow(headerIndex) + drivenTableWidget.blockSignals(False) + self.setEditDeletePoseEnabled(enable=True) + + def setEditDeletePoseEnabled(self, enable=False): + """toggle buttons that can or cannot be selected + + Args: + enable (bool, optional): to disable vs not + """ + self.editPoseButton.setEnabled(enable) + self.editPoseValuesButton.setEnabled(enable) + self.deletePoseButton.setEnabled(enable) + + def setDriverControlOnSetup(self, controlName): + """make sure to set the driverControlAttr when the user supplies one + + Args: + controlName (str): name of the control to set in an attr + """ + for rbfNode in self.currentRBFSetupNodes: + rbfNode.setDriverControlAttr(controlName) + + def setSetupDriverControl(self, lineEditWidget): + """should the user wish to set a different driverControl pose setup + creation, prompt them prior to proceeding + + Args: + lineEditWidget (QLineEdit): to query for the name + + Returns: + n/a: nada + """ + if not self.currentRBFSetupNodes: + self.setNodeToField(lineEditWidget) + elif self.currentRBFSetupNodes: + textA = "Do you want to change the Control for setup?" + textB = "This Control that will be used for recalling poses." + decision = promptAcceptance(self, textA, textB) + if decision in [QtWidgets.QMessageBox.Discard, + QtWidgets.QMessageBox.Cancel]: + return + controlName = self.setNodeToField(lineEditWidget) + self.setDriverControlOnSetup(controlName) + + @staticmethod + def getRBFNodesInfo(rbfNodes): + """create a dictionary of all the RBFInfo(referred to as + weightNodeInfo a lot) for export + + Args: + rbfNodes (list): [of RBFNodes] + + Returns: + dict: of all the rbfNodes provided + """ + weightNodeInfo_dict = {} + for rbf in rbfNodes: + weightNodeInfo_dict[rbf.name] = rbf.getNodeInfo() + return weightNodeInfo_dict + + def importNodes(self): + """import a setup(s) from file select by user + + Returns: + n/a: nada + """ + # sceneFilePath = mc.file(sn=True, q=True) + # startDir = os.path.dirname(sceneFilePath) + filePath = rbf_io.fileDialog(mode=1) + if filePath is None: + return + rbf_io.importRBFs(filePath) + mc.select(cl=True) + self.refresh() + print("RBF setups imported: {}".format(filePath)) + + def exportNodes(self, allSetups=True): + """export all nodes or nodes from current setup + + Args: + allSetups (bool, optional): If all or setup + + Returns: + n/a: nada + """ + # TODO WHEN NEW RBF NODE TYPES ARE ADDED, THIS WILL NEED TO BE RETOOLED + nodesToExport = [] + if allSetups: + [nodesToExport.extend(v) for k, v, + in self.allSetupsInfo.items()] + else: + nodesToExport = self.currentRBFSetupNodes + + nodesToExport = [n.name for n in nodesToExport] + # sceneFilePath = mc.file(sn=True, q=True) + # startDir = os.path.dirname(sceneFilePath) + filePath = rbf_io.fileDialog(mode=0) + if filePath is None: + return + rbf_io.exportRBFs(nodesToExport, filePath) + + @staticmethod + def gatherMirroredInfo(rbfNodes): + """gather all the info from the provided nodes and string replace + side information for its mirror. Using mGear standard + naming convections + + Args: + rbfNodes (list): [of RBFNodes] + + Returns: + dict: with all the info mirrored + """ + mirrorWeightInfo = {} + for rbfNode in rbfNodes: + weightInfo = rbfNode.getNodeInfo() + # connections ----------------------------------------------------- + mrConnections = [] + for pairs in weightInfo["connections"]: + mrConnections.append([mString.convertRLName(pairs[0]), + mString.convertRLName(pairs[1])]) + + weightInfo["connections"] = mrConnections + weightInfo["drivenControlName"] = mString.convertRLName(weightInfo["drivenControlName"]) + weightInfo["drivenNode"] = [mString.convertRLName(n) for n in weightInfo["drivenNode"]] + weightInfo["driverControl"] = mString.convertRLName(weightInfo["driverControl"]) + weightInfo["driverNode"] = [mString.convertRLName(n) for n in weightInfo["driverNode"]] + + # setupName ------------------------------------------------------- + mrSetupName = mString.convertRLName(weightInfo["setupName"]) + if mrSetupName == weightInfo["setupName"]: + mrSetupName = "{}{}".format(mrSetupName, MIRROR_SUFFIX) + weightInfo["setupName"] = mrSetupName + # transformNode --------------------------------------------------- + tmp = weightInfo["transformNode"]["name"] + mrTransformName = mString.convertRLName(tmp) + weightInfo["transformNode"]["name"] = mrTransformName + + tmp = weightInfo["transformNode"]["parent"] + if tmp is None: + mrTransformPar = None + else: + mrTransformPar = mString.convertRLName(tmp) + weightInfo["transformNode"]["parent"] = mrTransformPar + # name ------------------------------------------------------------ + mirrorWeightInfo[mString.convertRLName(rbfNode.name)] = weightInfo + return mirrorWeightInfo + + def getMirroredSetupTargetsInfo(self): + """convenience function to get all the mirrored info for the new side + + Returns: + dict: mirrored dict information + """ + setupTargetInfo_dict = {} + for rbfNode in self.currentRBFSetupNodes: + mrRbfNode = mString.convertRLName(rbfNode.name) + mrRbfNode = sortRBF(mrRbfNode) + drivenNode = rbfNode.getDrivenNode()[0] + drivenControlNode = rbfNode.getConnectedRBFToggleNode() + mrDrivenControlNode = mString.convertRLName(drivenControlNode) + mrDrivenControlNode = pm.PyNode(mrDrivenControlNode) + setupTargetInfo_dict[pm.PyNode(drivenNode)] = [mrDrivenControlNode, mrRbfNode] + return setupTargetInfo_dict + + def mirrorSetup(self): + """gather all info on current setup, mirror the info, use the creation + func from that rbf module type to create the nodes in the setup with + mirrored information. + + THE ONLY nodes created will be the ones created during normal + "add pose" creation. Assumption is that all nodes that need drive, + driven by the setup exist. + + Returns: + n/a: nada + """ + if not self.currentRBFSetupNodes: + return + aRbfNode = self.currentRBFSetupNodes[0] + mirrorWeightInfo = self.gatherMirroredInfo(self.currentRBFSetupNodes) + mrRbfType = aRbfNode.rbfType + poseIndices = len(aRbfNode.getPoseInfo()["poseInput"]) + rbfModule = rbf_io.RBF_MODULES[mrRbfType] + rbfModule.createRBFFromInfo(mirrorWeightInfo) + setupTargetInfo_dict = self.getMirroredSetupTargetsInfo() + nameSpace = anim_utils.getNamespace(aRbfNode.name) + mrRbfNodes = [v[1] for k, v in setupTargetInfo_dict.items()] + [v.setToggleRBFAttr(0) for v in mrRbfNodes] + mrDriverNode = mrRbfNodes[0].getDriverNode()[0] + mrDriverAttrs = mrRbfNodes[0].getDriverNodeAttributes() + driverControl = aRbfNode.getDriverControlAttr() + driverControl = pm.PyNode(driverControl) + # Sanity check: make sure mirror attributes are set up properly + for targetInfo in setupTargetInfo_dict.items(): + driven = targetInfo[0] + ctrl = driven.name().replace("_driven", "_ctl") + for attr in ["invTx", "invTy", "invTz", "invRx", "invRy", "invRz"]: + pm.setAttr(driven + "." + attr, pm.getAttr(ctrl + "." + attr)) + for index in range(poseIndices): + # Apply mirror pose to control + aRbfNode.recallDriverPose(index) + anim_utils.mirrorPose(flip=False, nodes=[driverControl]) + mrData = [] + for srcNode, dstValues in setupTargetInfo_dict.items(): + mrData.extend(anim_utils.calculateMirrorDataRBF(srcNode, + dstValues[0])) + for entry in mrData: + anim_utils.applyMirror(nameSpace, entry) + poseInputs = rbf_node.getMultipleAttrs(mrDriverNode, mrDriverAttrs) + for mrRbfNode in mrRbfNodes: + poseValues = mrRbfNode.getPoseValues(resetDriven=False) + mrRbfNode.addPose(poseInput=poseInputs, + poseValue=poseValues, + posesIndex=index) + mrRbfNode.forceEvaluation() + [v.setToggleRBFAttr(1) for v in mrRbfNodes] + setupName, rbfType = self.getSelectedSetup() + self.refreshRbfSetupList(setToSelection=setupName) + for mrRbfNode in mrRbfNodes: + rbf_node.resetDrivenNodes(str(mrRbfNode)) + pm.select(cl=True) + + def hideMenuBar(self, x, y): + """rules to hide/show the menubar when hide is enabled + + Args: + x (int): coord X of the mouse + y (int): coord Y of the mouse + """ + if x < 100 and y < 50: + self.menuBar().show() + else: + self.menuBar().hide() + + def tabConextMenu(self, qPoint): + """create a pop up menu over the tabs when right clicked + + Args: + qPoint (int): the mouse position when menu requested + + Returns: + n/a: diddly + """ + tabIndex = self.rbfTabWidget.tabBar().tabAt(qPoint) + if tabIndex == -1: + return + selWidget = self.rbfTabWidget.widget(tabIndex) + rbfNode = getattr(selWidget, "rbfNode") + tabMenu = QtWidgets.QMenu(self) + parentPosition = self.rbfTabWidget.mapToGlobal(QtCore.QPoint(0, 0)) + menu_item_01 = tabMenu.addAction("Select {}".format(rbfNode)) + menu_item_01.triggered.connect(partial(mc.select, rbfNode)) + partialObj_selWdgt = partial(self.rbfTabWidget.setCurrentWidget, + selWidget) + menu_item_01.triggered.connect(partialObj_selWdgt) + tabMenu.move(parentPosition + qPoint) + tabMenu.show() + + def reevalluateAllNodes(self): + """for evaluation on all nodes in any setup. In case of manual editing + """ + for name, rbfNodes in self.allSetupsInfo.items(): + [rbfNode.forceEvaluation() for rbfNode in rbfNodes] + print("All Nodes have been Re-evaluated") + + def toggleGetPoseType(self, toggleState): + """records whether the user wants poses recorded in worldSpace or check + local space + + Args: + toggleState (bool): default True + """ + self.absWorld = toggleState + + def toggleDefaultType(self, toggleState): + """records whether the user wants default poses to be zeroed + + Args: + toggleState (bool): default True + """ + self.zeroedDefaults = toggleState + + # signal management ------------------------------------------------------- + def connectSignals(self): + """connect all the signals in the UI + Exceptions being MenuBar and Table header signals + """ + # RBF ComboBox and Refresh Button + self.rbf_cbox.currentIndexChanged.connect(self.displayRBFSetupInfo) + self.rbf_refreshButton.clicked.connect(self.refresh) + + # Driver Line Edit and Control Line Edit + self.driverLineEdit.clicked.connect(selectNode) + self.controlLineEdit.clicked.connect(selectNode) + self.driverLineEdit.textChanged.connect(partial(self.updateAttributeDisplay, + self.driverAttributesWidget)) + self.drivenLineEdit.textChanged.connect(partial(self.updateAttributeDisplay, + self.drivenAttributesWidget)) + + # Table Widget + header = self.driverPoseTableWidget.verticalHeader() + header.sectionClicked.connect(self.setConsistentHeaderSelection) + header.sectionClicked.connect(self.recallDriverPose) + selDelFunc = self.setEditDeletePoseEnabled + self.driverPoseTableWidget.itemSelectionChanged.connect(selDelFunc) + + # Buttons Widget + self.addRbfButton.clicked.connect(self.addRBFToSetup) + self.addPoseButton.clicked.connect(self.addPose) + self.editPoseButton.clicked.connect(self.editPose) + self.editPoseValuesButton.clicked.connect(self.editPoseValues) + self.deletePoseButton.clicked.connect(self.deletePose) + self.setControlButton.clicked.connect(partial(self.setSetupDriverControl, self.controlLineEdit)) + self.setDriverButton.clicked.connect(partial(self.setNodeToField, self.driverLineEdit)) + self.setDrivenButton.clicked.connect(partial(self.setNodeToField, self.drivenLineEdit, multi=True)) + self.allButton.clicked.connect(self.setDriverControlLineEdit) + self.addDrivenButton.clicked.connect(self.addNewDriven) + + # Custom Context Menus + customMenu = self.driverAttributesWidget.customContextMenuRequested + customMenu.connect( + partial(self.attrListMenu, + self.driverAttributesWidget, + self.driverLineEdit, + "driver") + ) + customMenu = self.drivenAttributesWidget.customContextMenuRequested + customMenu.connect( + partial(self.attrListMenu, + self.drivenAttributesWidget, + self.driverLineEdit, + "driven") + ) + + # Tab Widget + tabBar = self.rbfTabWidget.tabBar() + tabBar.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + tabBar.customContextMenuRequested.connect(self.tabConextMenu) + tabBar.tabCloseRequested.connect(self.removeRBFFromSetup) + + # main assebly ------------------------------------------------------------ + def createCentralWidget(self): + """main UI assembly + + Returns: + QtWidget: main UI to be parented to as the centralWidget + """ + centralWidget = QtWidgets.QWidget() + centralWidgetLayout = QtWidgets.QVBoxLayout() + centralWidget.setLayout(centralWidgetLayout) + + splitter = QtWidgets.QSplitter() + + # Setup selector section + (rbfLayout, + self.rbf_cbox, + self.rbf_refreshButton) = self.createSetupSelectorWidget() + self.rbf_cbox.setToolTip("List of available setups in the scene.") + self.rbf_refreshButton.setToolTip("Refresh the UI") + + driverDrivenWidget = self.createDarkContainerWidget() + allTableWidget = self.createDriverDrivenTableWidget() + + centralWidgetLayout.addLayout(rbfLayout) + centralWidgetLayout.addWidget(HLine()) + splitter.addWidget(driverDrivenWidget) + splitter.addWidget(allTableWidget) + centralWidgetLayout.addWidget(splitter) + + # Assuming a ratio of 2:1 for settingWidth to tableWidth + totalWidth = splitter.width() + attributeWidth = (1/3) * totalWidth + tableWidth = (2/3) * totalWidth + splitter.setSizes([int(attributeWidth), int(tableWidth)]) + return centralWidget + + def createMenuBar(self, hideMenuBar=False): + """Create the UI menubar, with option to hide based on mouse input + + Args: + hideMenuBar (bool, optional): should it autoHide + + Returns: + QMenuBar: for parenting + """ + mainMenuBar = QtWidgets.QMenuBar() + mainMenuBar.setContentsMargins(0, 0, 0, 0) + file = mainMenuBar.addMenu("File") + menu1 = file.addAction("Re-evaluate Nodes", self.reevalluateAllNodes) + menu1.setToolTip("Force all RBF nodes to re-revaluate.") + file.addAction("Export All", self.exportNodes) + file.addAction("Export current setup", partial(self.exportNodes, + allSetups=False)) + file.addAction("Import RBFs", partial(self.importNodes)) + file.addSeparator() + file.addAction("Delete Current Setup", self.__deleteSetup) + # mirror -------------------------------------------------------------- + mirrorMenu = mainMenuBar.addMenu("Mirror") + mirrorMenu1 = mirrorMenu.addAction("Mirror Setup", self.mirrorSetup) + mirrorMenu1.setToolTip("This will create a new setup.") + + # settings ------------------------------------------------------------ + settingsMenu = mainMenuBar.addMenu("Settings") + menuLabel = "Add poses in worldSpace" + worldSpaceMenuItem = settingsMenu.addAction(menuLabel) + worldSpaceMenuItem.toggled.connect(self.toggleGetPoseType) + + worldSpaceMenuItem.setCheckable(True) + worldSpaceMenuItem.setChecked(True) + toolTip = "When ADDING NEW pose, should it be recorded in worldSpace." + + menuLabel = "Default Poses is Zeroed" + zeroedDefaultsMenuItem = settingsMenu.addAction(menuLabel) + zeroedDefaultsMenuItem.toggled.connect(self.toggleDefaultType) + + zeroedDefaultsMenuItem.setCheckable(True) + zeroedDefaultsMenuItem.setChecked(True) + + worldSpaceMenuItem.setToolTip(toolTip) + + # show override ------------------------------------------------------- + additionalFuncDict = getEnvironModules() + if additionalFuncDict: + showOverridesMenu = mainMenuBar.addMenu("Local Overrides") + for k, v in additionalFuncDict.items(): + showOverridesMenu.addAction(k, v) + + if hideMenuBar: + mainMenuBar.hide() + self.setMouseTracking(True) + self.mousePosition.connect(self.hideMenuBar) + return mainMenuBar + + # overrides --------------------------------------------------------------- + def mouseMoveEvent(self, event): + """used for tracking the mouse position over the UI, in this case for + menu hiding/show + + Args: + event (Qt.QEvent): events to filter + """ + if event.type() == QtCore.QEvent.MouseMove: + if event.buttons() == QtCore.Qt.NoButton: + pos = event.pos() + self.mousePosition.emit(pos.x(), pos.y()) + diff --git a/release/scripts/mgear/rigbits/rbf_manager_ui.py b/release/scripts/mgear/rigbits/rbf_manager_ui.py index 96707a83..9b5ddde7 100644 --- a/release/scripts/mgear/rigbits/rbf_manager_ui.py +++ b/release/scripts/mgear/rigbits/rbf_manager_ui.py @@ -81,17 +81,21 @@ from . import rbf_node from .six import PY2 +# debug +# reload(rbf_io) +# reload(rbf_node) + +# from mgear.rigbits import rbf_manager_ui +# rbf_ui = rbf_manager_ui.show() # ============================================================================= # Constants # ============================================================================= -__version__ = "1.0.9" +__version__ = "1.0.2" _mgear_version = mgear.getVersion() TOOL_NAME = "RBF Manager" TOOL_TITLE = "{} v{} | mGear {}".format(TOOL_NAME, __version__, _mgear_version) -UI_NAME = "RBFManagerUI" -WORK_SPACE_NAME = UI_NAME + "WorkspaceControl" DRIVEN_SUFFIX = rbf_node.DRIVEN_SUFFIX CTL_SUFFIX = rbf_node.CTL_SUFFIX @@ -115,7 +119,7 @@ def testFunctions(*args): print('!!', args) -def getPlugAttrs(nodes, attrType="keyable"): +def getPlugAttrs(nodes, attrType="all"): """Get a list of attributes to display to the user Args: @@ -126,9 +130,6 @@ def getPlugAttrs(nodes, attrType="keyable"): list: list of attrplugs """ plugAttrs = [] - if len(nodes) >= 2: - print("the number of node is more than two") - for node in nodes: if attrType == "all": attrs = mc.listAttr(node, se=True, u=False) @@ -220,7 +221,7 @@ def selectNode(name): # ============================================================================= def getControlAttrWidget(nodeAttr, label=""): - """Create a cmds.attrControlGrp and wrap it in a qtWidget, preserving its connection + """get a cmds.attrControlGrp wrapped in a qtWidget, still connected to the specified attr Args: @@ -228,20 +229,20 @@ def getControlAttrWidget(nodeAttr, label=""): label (str, optional): name for the attr widget Returns: - QtWidget.QLineEdit: A Qt widget created from attrControlGrp - str: The name of the created Maya attrControlGrp + QtWidget: qwidget created from attrControlGrp """ - mAttrFeild = mc.attrControlGrp(attribute=nodeAttr, label=label, po=True) - - # Convert the Maya control to a Qt pointer + mAttrFeild = mc.attrControlGrp(attribute=nodeAttr, + label=label, + po=True) ptr = mui.MQtUtil.findControl(mAttrFeild) - - # Wrap the Maya control into a Qt widget, considering Python version - controlWidget = QtCompat.wrapInstance(long(ptr) if PY2 else int(ptr), base=QtWidgets.QWidget) + if PY2: + controlWidget = QtCompat.wrapInstance(long(ptr), base=QtWidgets.QWidget) + else: + controlWidget = QtCompat.wrapInstance(int(ptr), base=QtWidgets.QWidget) controlWidget.setContentsMargins(0, 0, 0, 0) controlWidget.setMinimumWidth(0) - - attrEdit = [wdgt for wdgt in controlWidget.children() if type(wdgt) == QtWidgets.QLineEdit] + attrEdit = [wdgt for wdgt in controlWidget.children() + if type(wdgt) == QtWidgets.QLineEdit] [wdgt.setParent(attrEdit[0]) for wdgt in controlWidget.children() if type(wdgt) == QtCore.QObject] @@ -276,34 +277,23 @@ def VLine(): def show(dockable=True, newSceneCallBack=True, *args): - """To launch the UI and ensure any previously opened instance is closed. + """To launch the ui and not get the same instance Returns: DistributeUI: instance Args: *args: Description - :param newSceneCallBack: - :param dockable: """ - global RBF_UI # Ensure we have access to the global variable - - # Attempt to close any existing UI with the given name - if mc.workspaceControl(WORK_SPACE_NAME, exists=True): - mc.deleteUI(WORK_SPACE_NAME) - - # Create the UI - RBF_UI = RBFManagerUI(newSceneCallBack=newSceneCallBack) - RBF_UI.initializePoseControlWidgets() - - # Check if we've saved a size previously and set it - if mc.optionVar(exists='RBF_UI_width') and mc.optionVar(exists='RBF_UI_height'): - saved_width = mc.optionVar(query='RBF_UI_width') - saved_height = mc.optionVar(query='RBF_UI_height') - RBF_UI.resize(saved_width, saved_height) - - # Show the UI. - RBF_UI.show(dockable=dockable) + global RBF_UI + if 'RBF_UI' in globals(): + try: + RBF_UI.close() + except TypeError: + pass + RBF_UI = RBFManagerUI(parent=pyqt.maya_main_window(), + newSceneCallBack=newSceneCallBack) + RBF_UI.show(dockable=True) return RBF_UI @@ -373,422 +363,98 @@ def tabSizeHint(self, index): return QtCore.QSize(width, 25) -class RBFWidget(MayaQWidgetDockableMixin, QtWidgets.QMainWindow): +class RBFSetupInput(QtWidgets.QDialog): - def __init__(self, parent=pyqt.maya_main_window()): - super(RBFWidget, self).__init__(parent=parent) + """Allow the user to select which attrs will drive the rbf nodes in a setup - # UI info ------------------------------------------------------------- - self.callBackID = None - self.setWindowTitle(TOOL_TITLE) - self.setObjectName(UI_NAME) - self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) - self.genericWidgetHight = 24 - - @staticmethod - def deleteAssociatedWidgetsMaya(widget, attrName="associatedMaya"): - """delete core ui items 'associated' with the provided widgets - - Args: - widget (QWidget): Widget that has the associated attr set - attrName (str, optional): class attr to query - """ - if hasattr(widget, attrName): - for t in getattr(widget, attrName): - try: - mc.deleteUI(t, ctl=True) - except Exception: - pass - else: - setattr(widget, attrName, []) - - @staticmethod - def deleteAssociatedWidgets(widget, attrName="associated"): - """delete widget items 'associated' with the provided widgets - - Args: - widget (QWidget): Widget that has the associated attr set - attrName (str, optional): class attr to query - """ - if hasattr(widget, attrName): - for t in getattr(widget, attrName): - try: - t.deleteLater() - except Exception: - pass - else: - setattr(widget, attrName, []) - - @staticmethod - def _associateRBFnodeAndWidget(tabDrivenWidget, rbfNode): - """associates the RBFNode with a widget for convenience when adding, - deleting, editing - - Args: - tabDrivenWidget (QWidget): tab widget - rbfNode (RBFNode): instance to be associated - """ - setattr(tabDrivenWidget, "rbfNode", rbfNode) + Attributes: + drivenListWidget (QListWidget): widget to display attrs to drive setup + okButton (QPushButton): BUTTON + result (list): of selected attrs from listWidget + setupField (bool)): Should the setup lineEdit widget be displayed + setupLineEdit (QLineEdit): name selected by user + """ - @staticmethod - def createCustomButton(label, size=(35, 27), tooltip=""): - stylesheet = ( - "QPushButton {background-color: #5D5D5D; border-radius: 4px;}" - "QPushButton:pressed { background-color: #00A6F3;}" - "QPushButton:hover:!pressed { background-color: #707070;}" - "QPushButton:disabled { background-color: #4a4a4a;}" - ) - button = QtWidgets.QPushButton(label) - button.setMinimumSize(QtCore.QSize(*size)) - button.setStyleSheet(stylesheet) - button.setToolTip(tooltip) - return button - - @staticmethod - def createSetupSelector2Widget(): - rbfVLayout = QtWidgets.QVBoxLayout() - rbfListWidget = QtWidgets.QListWidget() - rbfVLayout.addWidget(rbfListWidget) - return rbfVLayout, rbfListWidget - - @staticmethod - def labelListWidget(label, attrListType, horizontal=True): - """create the listAttribute that users can select their driver/driven - attributes for the setup + def __init__(self, listValues, setupField=True, parent=None): + """setup the UI widgets Args: - label (str): to display above the listWidget - horizontal (bool, optional): should the label be above or infront - of the listWidget - - Returns: - list: QLayout, QListWidget - """ - if horizontal: - attributeLayout = QtWidgets.QHBoxLayout() - else: - attributeLayout = QtWidgets.QVBoxLayout() - attributeLabel = QtWidgets.QLabel(label) - attributeListWidget = QtWidgets.QListWidget() - attributeListWidget.setObjectName("{}ListWidget".format(attrListType)) - attributeLayout.addWidget(attributeLabel) - attributeLayout.addWidget(attributeListWidget) - return attributeLayout, attributeListWidget - - @staticmethod - def addRemoveButtonWidget(label1, label2, horizontal=True): - if horizontal: - addRemoveLayout = QtWidgets.QHBoxLayout() - else: - addRemoveLayout = QtWidgets.QVBoxLayout() - addAttributesButton = QtWidgets.QPushButton(label1) - removeAttributesButton = QtWidgets.QPushButton(label2) - addRemoveLayout.addWidget(addAttributesButton) - addRemoveLayout.addWidget(removeAttributesButton) - return addRemoveLayout, addAttributesButton, removeAttributesButton - - def selectNodeWidget(self, label, buttonLabel="Select"): - """create a lout with label, lineEdit, QPushbutton for user input + listValues (list): attrs to be displayed on the list + setupField (bool, optional): should the setup line edit be shown + parent (QWidget, optional): widget to parent this to """ - stylesheet = ( - "QLineEdit { background-color: #404040;" - "border-radius: 4px;" - "border-color: #505050;" - "border-style: solid;" - "border-width: 1.4px;}" - ) - - nodeLayout = QtWidgets.QHBoxLayout() - nodeLayout.setSpacing(4) - - nodeLabel = QtWidgets.QLabel(label) - nodeLabel.setFixedWidth(40) - nodeLineEdit = ClickableLineEdit() - nodeLineEdit.setStyleSheet(stylesheet) - nodeLineEdit.setReadOnly(True) - nodeSelectButton = self.createCustomButton(buttonLabel) - nodeSelectButton.setFixedWidth(40) - nodeLineEdit.setFixedHeight(self.genericWidgetHight) - nodeSelectButton.setFixedHeight(self.genericWidgetHight) - nodeLayout.addWidget(nodeLabel) - nodeLayout.addWidget(nodeLineEdit, 1) - nodeLayout.addWidget(nodeSelectButton) - return nodeLayout, nodeLineEdit, nodeSelectButton - - def createSetupSelectorWidget(self): - """create the top portion of the weidget, select setup + refresh - - Returns: - list: QLayout, QCombobox, QPushButton - """ - setRBFLayout = QtWidgets.QHBoxLayout() - rbfLabel = QtWidgets.QLabel("Select RBF Setup:") - rbf_cbox = QtWidgets.QComboBox() - rbf_refreshButton = self.createCustomButton("Refresh", (60, 25), "Refresh the UI") - rbf_cbox.setFixedHeight(self.genericWidgetHight) - rbf_refreshButton.setMaximumWidth(80) - rbf_refreshButton.setFixedHeight(self.genericWidgetHight - 1) - setRBFLayout.addWidget(rbfLabel) - setRBFLayout.addWidget(rbf_cbox, 1) - setRBFLayout.addWidget(rbf_refreshButton) - return setRBFLayout, rbf_cbox, rbf_refreshButton - - def createDriverAttributeWidget(self): - """widget where the user inputs information for the setups - - Returns: - list: [of widgets] - """ - driverControlVLayout = QtWidgets.QVBoxLayout() - driverControlHLayout = QtWidgets.QHBoxLayout() - - # driverMainLayout.setStyleSheet("QVBoxLayout { background-color: #404040;") - driverControlHLayout.setSpacing(3) - # -------------------------------------------------------------------- - (controlLayout, - controlLineEdit, - setControlButton) = self.selectNodeWidget("Control", buttonLabel="Set") - controlLineEdit.setToolTip("The node driving the setup. (Click me!)") + super(RBFSetupInput, self).__init__(parent=parent) + self.setWindowTitle(TOOL_TITLE) + mainLayout = QtWidgets.QVBoxLayout() + self.setLayout(mainLayout) + self.setupField = setupField + self.result = [] # -------------------------------------------------------------------- - (driverLayout, - driverLineEdit, - driverSelectButton) = self.selectNodeWidget("Driver", buttonLabel="Set") - driverLineEdit.setToolTip("The node driving the setup. (Click me!)") + setupLayout = QtWidgets.QHBoxLayout() + setupLabel = QtWidgets.QLabel("Specify Setup Name") + self.setupLineEdit = QtWidgets.QLineEdit() + self.setupLineEdit.setPlaceholderText("_ // skirt_L0") + setupLayout.addWidget(setupLabel) + setupLayout.addWidget(self.setupLineEdit) + if setupField: + mainLayout.addLayout(setupLayout) # -------------------------------------------------------------------- - allButton = self.createCustomButton("All", (20, 53), "") - - (attributeLayout, attributeListWidget) = self.labelListWidget( - label="Select Driver Attributes:", attrListType="driver", horizontal=False) - - attributeListWidget.setToolTip("List of attributes driving setup.") + drivenLayout = QtWidgets.QVBoxLayout() + drivenLabel = QtWidgets.QLabel("Select Driven Attributes") + self.drivenListWidget = QtWidgets.QListWidget() + self.drivenListWidget.setToolTip("Right Click for sorting!") selType = QtWidgets.QAbstractItemView.ExtendedSelection - attributeListWidget.setSelectionMode(selType) - attributeListWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.drivenListWidget.setSelectionMode(selType) + self.drivenListWidget.addItems(listValues) + self.drivenListWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + drivenLayout.addWidget(drivenLabel) + drivenLayout.addWidget(self.drivenListWidget) + mainLayout.addLayout(drivenLayout) # -------------------------------------------------------------------- - driverControlVLayout.addLayout(controlLayout, 0) - driverControlVLayout.addLayout(driverLayout, 0) - driverControlHLayout.addLayout(driverControlVLayout, 0) - driverControlHLayout.addWidget(allButton, 0) - return [controlLineEdit, - setControlButton, - driverLineEdit, - driverSelectButton, - allButton, - attributeListWidget, - attributeLayout, - driverControlHLayout] + # buttonLayout = QtWidgets.QHBoxLayout() + self.okButton = QtWidgets.QPushButton("Ok") + self.okButton.clicked.connect(self.onOK) + mainLayout.addWidget(self.okButton) - def createDrivenAttributeWidget(self): - """the widget that displays the driven information + def onOK(self): + """collect information from the displayed widgets, userinput, return Returns: - list: [of widgets] + list: of user input provided from user """ - drivenWidget = QtWidgets.QWidget() - drivenMainLayout = QtWidgets.QVBoxLayout() - drivenMainLayout.setContentsMargins(0, 10, 0, 2) - drivenMainLayout.setSpacing(9) - drivenSetLayout = QtWidgets.QVBoxLayout() - drivenMainLayout.addLayout(drivenSetLayout) - drivenWidget.setLayout(drivenMainLayout) - # -------------------------------------------------------------------- - (drivenLayout, - drivenLineEdit, - drivenSelectButton) = self.selectNodeWidget("Driven", buttonLabel="Set") - drivenTip = "The node being driven by setup. (Click me!)" - drivenLineEdit.setToolTip(drivenTip) - - addDrivenButton = self.createCustomButton("+", (20, 26), "") - addDrivenButton.setToolTip("Add a new driven to the current rbf node") - # -------------------------------------------------------------------- - (attributeLayout, - attributeListWidget) = self.labelListWidget(label="Select Driven Attributes:", - attrListType="driven", - horizontal=False) - attributeListWidget.setToolTip("Attributes being driven by setup.") - attributeLayout.setSpacing(1) - selType = QtWidgets.QAbstractItemView.ExtendedSelection - attributeListWidget.setSelectionMode(selType) - attributeListWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - # -------------------------------------------------------------------- - drivenSetLayout.addLayout(drivenLayout, 0) - drivenSetLayout.addLayout(attributeLayout, 0) - drivenLayout.addWidget(addDrivenButton) - drivenSetLayout.addLayout(drivenLayout, 0) - drivenSetLayout.addLayout(attributeLayout, 0) - drivenMainLayout.addLayout(drivenSetLayout) - drivenWidget.setLayout(drivenMainLayout) - return [drivenLineEdit, - drivenSelectButton, - addDrivenButton, - attributeListWidget, - drivenWidget, - drivenMainLayout] + setupName = self.setupLineEdit.text() + if setupName == "" and self.setupField: + genericWarning(self, "Enter Setup Name") + return + selectedAttrs = self.drivenListWidget.selectedItems() + if not selectedAttrs: + genericWarning(self, "Select at least one attribute") + return + driverAttrs = [item.text().split(".")[1] for item in selectedAttrs] + self.result.append(setupName) + self.result.append(driverAttrs) + self.accept() + return self.result - def createDrivenWidget(self): - """the widget that displays the driven information + def getValue(self): + """convenience to get result Returns: - list: [of widgets] + TYPE: Description """ - drivenWidget = QtWidgets.QWidget() - drivenMainLayout = QtWidgets.QHBoxLayout() - drivenMainLayout.setContentsMargins(0, 0, 0, 0) - drivenMainLayout.setSpacing(9) - drivenWidget.setLayout(drivenMainLayout) + return self.result - tableWidget = self.createTableWidget() - drivenMainLayout.addWidget(tableWidget, 1) - return drivenWidget, tableWidget - - def createTableWidget(self): - """create table widget used to display poses, set tooltips and colum + def exec_(self): + """Convenience Returns: - QTableWidget: QTableWidget + list: [str, [of selected attrs]] """ - stylesheet = """ - QTableWidget QHeaderView::section { - background-color: #3a3b3b; - padding: 2px; - text-align: center; - } - QTableCornerButton::section { - background-color: #3a3b3b; - border: none; - } - """ - tableWidget = QtWidgets.QTableWidget() - tableWidget.insertColumn(0) - tableWidget.insertRow(0) - tableWidget.setHorizontalHeaderLabels(["Pose Value"]) - tableWidget.setVerticalHeaderLabels(["Pose #0"]) - # tableWidget.setStyleSheet(stylesheet) - tableTip = "Live connections to the RBF Node in your setup." - tableTip = tableTip + "\nSelect the desired Pose # to recall pose." - tableWidget.setToolTip(tableTip) - return tableWidget + super(RBFSetupInput, self).exec_() + return self.result - def createTabWidget(self): - """Tab widget to add driven widgets too. Custom TabBar so the tab is - easier to select - - Returns: - QTabWidget: - """ - tabLayout = QtWidgets.QTabWidget() - tabLayout.setContentsMargins(0, 0, 0, 0) - tabBar = TabBar() - tabLayout.setTabBar(tabBar) - tabBar.setTabsClosable(True) - return tabLayout - def createOptionsButtonsWidget(self): - """add, edit, delete buttons for modifying rbf setups. - - Returns: - list: [QPushButtons] - """ - optionsLayout = QtWidgets.QHBoxLayout() - optionsLayout.setSpacing(5) - addTip = "After positioning all controls in the setup, add new pose." - addTip = addTip + "\nEnsure the driver node has a unique position." - addPoseButton = self.createCustomButton("Add Pose", size=(80, 26), tooltip=addTip) - EditPoseButton = self.createCustomButton("Update Pose", size=(80, 26)) - EditPoseButton.setToolTip("Recall pose, adjust controls and Edit.") - EditPoseValuesButton = self.createCustomButton("Update Pose Values", size=(80, 26)) - EditPoseValuesButton.setToolTip("Set pose based on values in table") - deletePoseButton = self.createCustomButton("Delete Pose", size=(80, 26)) - deletePoseButton.setToolTip("Recall pose, then Delete") - optionsLayout.addWidget(addPoseButton) - optionsLayout.addWidget(EditPoseButton) - optionsLayout.addWidget(EditPoseValuesButton) - optionsLayout.addWidget(deletePoseButton) - return (optionsLayout, - addPoseButton, - EditPoseButton, - EditPoseValuesButton, - deletePoseButton) - - def createDarkContainerWidget(self): - darkContainer = QtWidgets.QWidget() - driverMainLayout = QtWidgets.QVBoxLayout() - driverMainLayout.setContentsMargins(10, 10, 10, 10) # Adjust margins as needed - driverMainLayout.setSpacing(5) # Adjust spacing between widgets - - # Setting the dark color (Example: dark gray) - # darkContainer.setStyleSheet("background-color: #323232;") - - # Driver section - (self.controlLineEdit, - self.setControlButton, - self.driverLineEdit, - self.setDriverButton, - self.allButton, - self.driverAttributesWidget, - self.driverAttributesLayout, - driverControlLayout) = self.createDriverAttributeWidget() - - # Driven section - (self.drivenLineEdit, - self.setDrivenButton, - self.addDrivenButton, - self.drivenAttributesWidget, - self.drivenWidget, - self.drivenMainLayout) = self.createDrivenAttributeWidget() - - self.addRbfButton = self.createCustomButton("New RBF") - self.addRbfButton.setToolTip("Select node to be driven by setup.") - stylesheet = ( - "QPushButton {background-color: #179e83; border-radius: 4px;}" - "QPushButton:hover:!pressed { background-color: #2ea88f;}" - ) - self.addRbfButton.setStyleSheet(stylesheet) - - # Setting up the main layout for driver and driven sections - driverMainLayout = QtWidgets.QVBoxLayout() - driverMainLayout.addLayout(driverControlLayout) - driverMainLayout.addLayout(self.driverAttributesLayout) - driverMainLayout.addWidget(self.drivenWidget) - driverMainLayout.addWidget(self.addRbfButton) - darkContainer.setLayout(driverMainLayout) - - return darkContainer - - def createDriverDrivenTableWidget(self): - tableContainer = QtWidgets.QWidget() - - # Setting up the main layout for driver and driven sections - driverDrivenTableLayout = QtWidgets.QVBoxLayout() - self.driverPoseTableWidget = self.createTableWidget() - self.rbfTabWidget = self.createTabWidget() - - # Options buttons section - (optionsLayout, - self.addPoseButton, - self.editPoseButton, - self.editPoseValuesButton, - self.deletePoseButton) = self.createOptionsButtonsWidget() - self.addPoseButton.setEnabled(False) - self.editPoseButton.setEnabled(False) - self.editPoseValuesButton.setEnabled(False) - self.deletePoseButton.setEnabled(False) - - driverDrivenTableLayout.addWidget(self.driverPoseTableWidget, 1) - driverDrivenTableLayout.addWidget(self.rbfTabWidget, 1) - driverDrivenTableLayout.addLayout(optionsLayout) - tableContainer.setLayout(driverDrivenTableLayout) - - return tableContainer - - -class RBFTables(RBFWidget): - - def __init__(self): - pass - - -class RBFManagerUI(RBFWidget): +class RBFManagerUI(MayaQWidgetDockableMixin, QtWidgets.QMainWindow): """A manager for creating, mirroring, importing/exporting poses created for RBF type nodes. @@ -808,46 +474,28 @@ class RBFManagerUI(RBFWidget): mousePosition = QtCore.Signal(int, int) - def __init__(self, hideMenuBar=False, newSceneCallBack=True): - super(RBFManagerUI, self).__init__() - + def __init__(self, parent=None, hideMenuBar=False, newSceneCallBack=True): + super(RBFManagerUI, self).__init__(parent=parent) + # UI info ------------------------------------------------------------- + self.callBackID = None + self.setWindowTitle(TOOL_TITLE) + self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) + self.genericWidgetHight = 24 + # class info ---------------------------------------------------------- self.absWorld = True self.zeroedDefaults = True self.currentRBFSetupNodes = [] self.allSetupsInfo = None - self.drivenWidget = [] - self.driverAutoAttr = [] - self.drivenAutoAttr = [] - self.setMenuBar(self.createMenuBar(hideMenuBar=hideMenuBar)) self.setCentralWidget(self.createCentralWidget()) self.centralWidget().setMouseTracking(True) self.refreshRbfSetupList() self.connectSignals() - # added because the dockableMixin makes the ui appear small self.adjustSize() - # self.resize(800, 650) if newSceneCallBack: self.newSceneCallBack() - def closeEvent(self, event): - """Overridden close event to save the size of the UI.""" - width = self.width() - height = self.height() - - # Save the size to Maya's optionVars - mc.optionVar(intValue=('RBF_UI_width', width)) - mc.optionVar(intValue=('RBF_UI_height', height)) - - self.deleteAssociatedWidgetsMaya(self.driverPoseTableWidget) - self.deleteAssociatedWidgets(self.driverPoseTableWidget) - if self.callBackID is not None: - self.removeSceneCallback() - - # Call the parent class's closeEvent - super(RBFManagerUI, self).closeEvent(event) - def callBackFunc(self, *args): """super safe function for trying to refresh the UI, should anything fail. @@ -906,6 +554,33 @@ def getDrivenNodesFromSetup(self): drivenNodes.extend(rbfNode.getDrivenNode) return drivenNodes + def getUserSetupInfo(self, drivenNode, drivenAttrs, setupField=True): + """prompt the user for information needed to create setup or add + rbf node to existing setup + + Args: + drivenAttrs (list): of attrs to display to user to select from + setupField (bool, optional): should the user be asked to input + a name for setup + + Returns: + list: list of selected attrs, name specified + """ + userInputWdgt = RBFSetupInput(drivenAttrs, + setupField=setupField, + parent=self) + partialObj = partial(self.attrListMenu, + userInputWdgt.drivenListWidget, + "", + nodeToQuery=drivenNode) + customMenu = userInputWdgt.drivenListWidget.customContextMenuRequested + customMenu.connect(partialObj) + results = userInputWdgt.exec_() + if results: + return results[0], results[1] + else: + return None, None + def __deleteSetup(self): decision = promptAcceptance(self, "Delete current Setup?", @@ -952,7 +627,7 @@ def removeRBFFromSetup(self, drivenWidgetIndex): drivenWidget = self.rbfTabWidget.widget(drivenWidgetIndex) self.rbfTabWidget.removeTab(drivenWidgetIndex) rbfNode = getattr(drivenWidget, "rbfNode") - self.deleteAssociatedWidgets(drivenWidget, attrName="associated") + self.__deleteAssociatedWidgets(drivenWidget, attrName="associated") drivenWidget.deleteLater() drivenNode = rbfNode.getDrivenNode() rbfNode.deleteRBFToggleAttr() @@ -975,136 +650,110 @@ def addRBFToSetup(self): Returns: TYPE: Description """ - result = self.preValidationCheck() - - driverControl = result["driverControl"] - driverNode, drivenNode = result["driverNode"], result["drivenNode"] - driverAttrs, drivenAttrs = result["driverAttrs"], result["drivenAttrs"] - + # TODO cut this function down to size + driverNode = self.driverLineEdit.text() + driverControl = self.controlLineEdit.text() + # take every opportunity to return to avoid unneeded processes + if driverNode == "": + return + selectedAttrItems = self.driver_attributes_widget.selectedItems() + if not selectedAttrItems: + return + driverAttrs = [item.text().split(".")[1] for item in selectedAttrItems] + drivenNode = mc.ls(sl=True) + # This does prevents a driver to be its own driven + if not drivenNode or drivenNode[0] == driverNode: + genericWarning(self, "Select Node to be driven!") + return + drivenNode = drivenNode[0] drivenType = mc.nodeType(drivenNode) + # smart display all when needed if drivenType in ["transform", "joint"]: - drivenNode_name = rbf_node.get_driven_group_name(drivenNode) + attrType = "keyable" else: - drivenNode_name = drivenNode + attrType = "all" + + drivenNode_name = drivenNode + if drivenType in ["transform", "joint"]: + drivenNode_name = rbf_node.get_driven_group_name(drivenNode) - # Check if there is an existing rbf node attached + # check if there is an existing rbf node attached if mc.objExists(drivenNode_name): if existing_rbf_setup(drivenNode_name): msg = "Node is already driven by an RBF Setup." genericWarning(self, msg) return + availableAttrs = getPlugAttrs([drivenNode], attrType=attrType) setupName, rbfType = self.getSelectedSetup() - + # if a setup has already been named or starting new + if setupName is None: + setupName, drivenAttrs = self.getUserSetupInfo(drivenNode, + availableAttrs) + else: + tmpName, drivenAttrs = self.getUserSetupInfo(drivenNode, + availableAttrs, + setupField=False) + if not drivenAttrs: + return parentNode = False + if drivenType in ["transform", "joint"]: parentNode = True drivenNode = rbf_node.addDrivenGroup(drivenNode) - - # Create RBFNode instance, apply settings - if not setupName: - setupName = "{}_WD".format(driverNode) + # create RBFNode instance, apply settings rbfNode = sortRBF(drivenNode, rbfType=rbfType) rbfNode.setSetupName(setupName) rbfNode.setDriverControlAttr(driverControl) rbfNode.setDriverNode(driverNode, driverAttrs) - defaultVals = rbfNode.setDrivenNode(drivenNode, drivenAttrs, parent=parentNode) - - # Check if there are any preexisting nodes in setup, if so copy pose index + defaultVals = rbfNode.setDrivenNode(drivenNode, + drivenAttrs, + parent=parentNode) + # Check if there any preexisting nodes in setup, if so copy pose index if self.currentRBFSetupNodes: currentRbfs = self.currentRBFSetupNodes[0] - print("Syncing poses indices from {} >> {}".format(currentRbfs, rbfNode)) + print("Syncing poses indices from {} >> {}".format(currentRbfs, + rbfNode)) rbfNode.syncPoseIndices(self.currentRBFSetupNodes[0]) - self.addNewTab(currentRbfs, drivenNode) - self.setAttributeDisplay(self.drivenAttributesWidget, drivenNode, drivenAttrs) else: if self.zeroedDefaults: rbfNode.applyDefaultPose() + else: + poseInputs = rbf_node.getMultipleAttrs(driverNode, driverAttrs) - rbfNode.addPose(poseInput=poseInputs, poseValue=defaultVals[1::2]) + rbfNode.addPose(poseInput=poseInputs, + poseValue=defaultVals[1::2]) self.populateDriverInfo(rbfNode, rbfNode.getNodeInfo()) - self.populateDrivenInfo(rbfNode, rbfNode.getNodeInfo()) - - # Add newly created RBFNode to list of current - self.addPoseButton.setEnabled(True) - + # add newly created RBFNode to list of current self.currentRBFSetupNodes.append(rbfNode) + # get info to populate the UI with it + weightInfo = rbfNode.getNodeInfo() + tabDrivenWidget = self.addNewTab(rbfNode) + self.populateDrivenWidgetInfo(tabDrivenWidget, weightInfo, rbfNode) self.refreshRbfSetupList(setToSelection=setupName) self.lockDriverWidgets() - mc.select(driverControl) - - def preValidationCheck(self): - # Fetch data from UI fields - driverNode = self.driverLineEdit.text() - drivenNode = self.drivenLineEdit.text() - driverControl = self.controlLineEdit.text() - driverSelectedAttrItems = self.driverAttributesWidget.selectedItems() - drivenSelectedAttrItems = self.drivenAttributesWidget.selectedItems() - - # Create a default return dictionary with None values - result = { - "driverNode": None, - "drivenNode": None, - "driverControl": None, - "driverAttrs": None, - "drivenAttrs": None - } - - # Ensure driverNode and drivenNode are provided - if not driverNode or not drivenNode: - return result - - # Ensure attributes are selected in the widgets - if not driverSelectedAttrItems or not drivenSelectedAttrItems: - return result - - # Check if the driven node is the same as the driver node - if drivenNode == driverNode: - genericWarning(self, "Select Node to be driven!") - return result - - # Update the result dictionary with the fetched values - result["driverNode"] = driverNode - result["drivenNode"] = drivenNode - result["driverControl"] = driverControl - result["driverAttrs"] = [item.text().split(".")[1] for item in driverSelectedAttrItems] - result["drivenAttrs"] = [item.text().split(".")[1] for item in drivenSelectedAttrItems] - - return result + if driverControl: + mc.select(driverControl) def refreshAllTables(self): - """Refresh all tables on all the tabs with the latest information + """Convenience function to refresh all the tables on all the tabs + with latest information. """ - # Iterate through each tab in the widget + weightInfo = None + rbfNode = None for index in range(self.rbfTabWidget.count()): drivenWidget = self.rbfTabWidget.widget(index) - drivenNodeName = drivenWidget.property("drivenNode") - - # Update table if the rbfNode's drivenNode matches the current tab's drivenNode + drivenNodeName = drivenWidget.drivenLineEdit.text() for rbfNode in self.currentRBFSetupNodes: drivenNodes = rbfNode.getDrivenNode() - if drivenNodes and drivenNodes[0] == drivenNodeName: - weightInfo = rbfNode.getNodeInfo() - self.setDriverTable(rbfNode, weightInfo) - self.setDrivenTable(drivenWidget, rbfNode, weightInfo) - - @staticmethod - def determineAttrType(node): - nodeType = mc.nodeType(node) - if nodeType in ["transform", "joint"]: - keyAttrs = mc.listAttr(node, keyable=True) or [] - requiredAttrs = [ - "{}{}".format(attrType, xyz) - for xyz in "XYZ" - for attrType in ["translate", "rotate", "scale"] - ] - - if not any(attr in keyAttrs for attr in requiredAttrs): - return "cb" - return "keyable" - return "all" + if drivenNodes and drivenNodes[0] != drivenNodeName: + continue + weightInfo = rbfNode.getNodeInfo() + self.setDrivenTable(drivenWidget, rbfNode, weightInfo) + if weightInfo and rbfNode: + self.populateDriverInfo(rbfNode, weightInfo) def deletePose(self): """delete a pose from the UI and all the RBFNodes in the setup. @@ -1144,9 +793,9 @@ def editPose(self): return driverNode = rbfNodes[0].getDriverNode()[0] driverAttrs = rbfNodes[0].getDriverNodeAttributes() - poseInputs = self.approximateZeros(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) + poseInputs = rbf_node.getMultipleAttrs(driverNode, driverAttrs) for rbfNode in rbfNodes: - poseValues = self.approximateZeros(rbfNode.getPoseValues(resetDriven=True)) + poseValues = rbfNode.getPoseValues(resetDriven=True) rbfNode.addPose(poseInput=poseInputs, poseValue=poseValues, posesIndex=drivenRow) @@ -1170,10 +819,13 @@ def editPoseValues(self): return driverNode = rbfNodes[0].getDriverNode()[0] driverAttrs = rbfNodes[0].getDriverNodeAttributes() - poseInputs = self.approximateZeros(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) + poseInputs = rbf_node.getMultipleAttrs(driverNode, driverAttrs) + # print("poseInputs: " + str(poseInputs)) + # print("RBF nodes: " + str(rbfNodes)) nColumns = drivenTableWidget.columnCount() entryWidgets = [drivenTableWidget.cellWidget(drivenRow, c) for c in range(nColumns)] newValues = [float(w.text()) for w in entryWidgets] + # print("values: " + str(newValues)) rbfNode = getattr(drivenWidget, "rbfNode") rbfNodes = [rbfNode] for rbfNode in rbfNodes: @@ -1187,52 +839,48 @@ def editPoseValues(self): rbfNode.forceEvaluation() self.refreshAllTables() + def updateAllFromTables(self): - """Update every pose for the RBF nodes based on the values from the tables. + """Update every pose + + Args: + rbfNode (RBFNode): node for query + weightInfo (dict): to pull information from, since we have it """ rbfNodes = self.currentRBFSetupNodes if not rbfNodes: return - - # Get common data for all RBF nodes + for w in self.rbfTabWidget.count(): + drivenWidget = self.rbfTabWidget.widget(w) + drivenTableWidget = getattr(drivenWidget, "tableWidget") + drivenRow = drivenTableWidget.currentRow() driverNode = rbfNodes[0].getDriverNode()[0] driverAttrs = rbfNodes[0].getDriverNodeAttributes() poseInputs = rbf_node.getMultipleAttrs(driverNode, driverAttrs) - - # Iterate over all widgets in the tab widget - for idx in range(self.rbfTabWidget.count()): - drivenWidget = self.rbfTabWidget.widget(idx) - - # Fetch the table widget associated with the current driven widget - drivenTableWidget = getattr(drivenWidget, "tableWidget") - drivenRow = drivenTableWidget.currentRow() - - # Extract new pose values from the driven table widget - nColumns = drivenTableWidget.columnCount() - entryWidgets = [drivenTableWidget.cellWidget(drivenRow, c) for c in range(nColumns)] - newValues = [float(widget.text()) for widget in entryWidgets] - - # Update the RBF node associated with the current widget/tab - rbfNode = getattr(drivenWidget, "rbfNode") + # print("poseInputs: " + str(poseInputs)) + # print("RBF nodes: " + str(rbfNodes)) + nColumns = drivenTableWidget.columnCount() + entryWidgets = [drivenTableWidget.cellWidget(drivenRow, c) for c in range(nColumns)] + newValues = [float(w.text()) for w in entryWidgets] + # print("values: " + str(newValues)) + rbfNode = getattr(drivenWidget, "rbfNode") + rbfNodes = [rbfNode] + for rbfNode in rbfNodes: + # poseValues = rbfNode.getPoseValues() + # print("Old pose values: " + str(poseValues)) + # print("New pose values: " + str(newValues)) + print("rbfNode: " + str(rbfNode)) + print("poseInputs: " + str(poseInputs)) + print("New pose values: " + str(newValues)) + print("poseIndex: " + str(drivenRow)) rbfNode.addPose(poseInput=poseInputs, poseValue=newValues, posesIndex=drivenRow) rbfNode.forceEvaluation() - - # Refresh tables after all updates self.refreshAllTables() - def approximateZeros(self, values, tolerance=1e-10): - """Approximate small values to zero. - Args: - values (list of float): The values to approximate. - tolerance (float): The tolerance under which a value is considered zero. - Returns: - list of float: The approximated values. - """ - return [0 if abs(v) < tolerance else v for v in values] def addPose(self): """Add pose to rbf nodes in setup. Additional index on all nodes @@ -1245,10 +893,10 @@ def addPose(self): return driverNode = rbfNodes[0].getDriverNode()[0] driverAttrs = rbfNodes[0].getDriverNodeAttributes() - poseInputs = self.approximateZeros(rbf_node.getMultipleAttrs(driverNode, driverAttrs)) + poseInputs = rbf_node.getMultipleAttrs(driverNode, driverAttrs) for rbfNode in rbfNodes: - poseValues = rbfNode.getPoseValues(resetDriven=True, absoluteWorld=self.absWorld) - poseValues = self.approximateZeros(poseValues) + poseValues = rbfNode.getPoseValues(resetDriven=True, + absoluteWorld=self.absWorld) rbfNode.addPose(poseInput=poseInputs, poseValue=poseValues) self.refreshAllTables() @@ -1283,16 +931,6 @@ def setNodeToField(self, lineEdit, multi=False): mc.select(cl=True) return controlNameData - def setDriverControlLineEdit(self): - selected = mc.ls(sl=True) - if len(selected) == 2: - self.controlLineEdit.setText(selected[0]) - self.driverLineEdit.setText(selected[1]) - elif len(selected) == 1: - self.controlLineEdit.setText(selected[0]) - self.driverLineEdit.setText(selected[0]) - mc.select(cl=True) - def highlightListEntries(self, listWidget, toHighlight): """set the items in a listWidget to be highlighted if they are in list @@ -1321,29 +959,11 @@ def setAttributeDisplay(self, attrListWidget, driverName, displayAttrs): attrListWidget.addItems(sorted(nodeAttrsToDisplay)) self.highlightListEntries(attrListWidget, displayAttrs) - def setAttributeToAutoSelect(self, attrListWidget): - selectedItems = attrListWidget.selectedItems() - selectedTexts = [item.text() for item in selectedItems] - attributes = [attrPlug.split(".")[-1] for attrPlug in selectedTexts] - - if "driver" in attrListWidget.objectName(): - self.driverAutoAttr = attributes - elif "driven" in attrListWidget.objectName(): - self.drivenAutoAttr = attributes - - @staticmethod - def setSelectedForAutoSelect(attrListWidget, itemTexts): - for i in range(attrListWidget.count()): - item = attrListWidget.item(i) - if item.text() in itemTexts: - item.setSelected(True) - def updateAttributeDisplay(self, attrListWidget, driverNames, highlight=[], - attrType="keyable", - force=False): + attrType="all"): """update the provided listwidget with the attrs collected from the list of nodes provided @@ -1359,31 +979,53 @@ def updateAttributeDisplay(self, nodeAttrsToDisplay = [] if not driverNames: return - elif "," in driverNames: - driverNames = driverNames.split(", ") elif type(driverNames) != list: driverNames = [driverNames] - - if not force: - attrType = self.determineAttrType(driverNames[0]) - nodeAttrsToDisplay = getPlugAttrs(driverNames, attrType=attrType) attrListWidget.clear() - attrListWidget.addItems(nodeAttrsToDisplay) + attrListWidget.addItems(sorted(nodeAttrsToDisplay)) + if highlight: + self.highlightListEntries(attrListWidget, highlight) - objName = attrListWidget.objectName() - autoAttrs = { - "driverListWidget": self.driverAutoAttr, "drivenListWidget": self.drivenAutoAttr - } + def __deleteAssociatedWidgetsMaya(self, widget, attrName="associatedMaya"): + """delete core ui items 'associated' with the provided widgets + + Args: + widget (QWidget): Widget that has the associated attr set + attrName (str, optional): class attr to query + """ + if hasattr(widget, attrName): + for t in getattr(widget, attrName): + try: + mc.deleteUI(t, ctl=True) + except Exception: + pass + else: + setattr(widget, attrName, []) - if autoAttrs[objName]: - attrPlugs = ["{}.{}".format(driverNames[0], attr) for attr in autoAttrs[objName]] - self.setSelectedForAutoSelect(attrListWidget, attrPlugs) + def __deleteAssociatedWidgets(self, widget, attrName="associated"): + """delete widget items 'associated' with the provided widgets - if highlight: - self.highlightListEntries(attrListWidget, highlight) + Args: + widget (QWidget): Widget that has the associated attr set + attrName (str, optional): class attr to query + """ + if hasattr(widget, attrName): + for t in getattr(widget, attrName): + try: + t.deleteLater() + except Exception: + pass + else: + setattr(widget, attrName, []) - def syncDriverTableCells(self, attrEdit, rbfAttrPlug): + def syncDriverTableCells(self, + attrEdit, + rbfAttrPlug, + poseIndex, + valueIndex, + attributeName, + *args): """When you edit the driver table, it will update all the sibling rbf nodes in the setup. @@ -1399,29 +1041,6 @@ def syncDriverTableCells(self, attrEdit, rbfAttrPlug): mc.setAttr(attrPlug, float(value)) rbfNode.forceEvaluation() - def initializePoseControlWidgets(self): - """Initialize UI widgets for each pose input based on the information from RBF nodes. - This dynamically creates widgets for the control attributes associated with each pose. - """ - # Retrieve all the RBF nodes from the stored setups info - rbfNodes = self.allSetupsInfo.values() - - # Loop through each RBF node to extract its weight information - for rbfNode in rbfNodes: - weightInfo = rbfNode[0].getNodeInfo() - - # Extract pose information from the weight data - poses = weightInfo.get("poses", None) - if not poses: - continue - # Enumerate through each pose input for this RBF node - for rowIndex, poseInput in enumerate(poses["poseInput"]): - for columnIndex, pValue in enumerate(poseInput): - rbfAttrPlug = "{}.poses[{}].poseInput[{}]".format(rbfNode[0], rowIndex, columnIndex) - - # Create a control widget for this pose input attribute - getControlAttrWidget(rbfAttrPlug, label="") - def setDriverTable(self, rbfNode, weightInfo): """Set the driverTable widget with the information from the weightInfo @@ -1432,43 +1051,45 @@ def setDriverTable(self, rbfNode, weightInfo): Returns: n/a: n/a """ - poses = weightInfo.get("poses", {}) - - # Clean up existing widgets and prepare for new content - self.deleteAssociatedWidgetsMaya(self.driverPoseTableWidget) - self.deleteAssociatedWidgets(self.driverPoseTableWidget) + poses = weightInfo["poses"] + # ensure deletion of associated widgets with this parent widget + self.__deleteAssociatedWidgetsMaya(self.driverPoseTableWidget) + self.__deleteAssociatedWidgets(self.driverPoseTableWidget) self.driverPoseTableWidget.clear() - - # Set columns and headers - driverAttrs = weightInfo.get("driverAttrs", []) - self.driverPoseTableWidget.setColumnCount(len(driverAttrs)) - self.driverPoseTableWidget.setHorizontalHeaderLabels(driverAttrs) - - # Set rows - poseInputs = poses.get("poseInput", []) - self.driverPoseTableWidget.setRowCount(len(poseInputs)) - if not poseInputs: + columnLen = len(weightInfo["driverAttrs"]) + self.driverPoseTableWidget.setColumnCount(columnLen) + headerNames = weightInfo["driverAttrs"] + self.driverPoseTableWidget.setHorizontalHeaderLabels(headerNames) + poseInputLen = len(poses["poseInput"]) + self.driverPoseTableWidget.setRowCount(poseInputLen) + if poseInputLen == 0: return - - verticalLabels = ["Pose {}".format(index) for index in range(len(poseInputs))] + verticalLabels = ["Pose {}".format(index) for index + in range(poseInputLen)] self.driverPoseTableWidget.setVerticalHeaderLabels(verticalLabels) - - # Populate the table with widgets - tmpWidgets, mayaUiItems = [], [] + tmpWidgets = [] + mayaUiItems = [] for rowIndex, poseInput in enumerate(poses["poseInput"]): - for columnIndex, _ in enumerate(poseInput): - rbfAttrPlug = "{}.poses[{}].poseInput[{}]".format(rbfNode, rowIndex, columnIndex) - attrEdit, mAttrField = getControlAttrWidget(rbfAttrPlug, label="") - - self.driverPoseTableWidget.setCellWidget(rowIndex, columnIndex, attrEdit) - attrEdit.returnPressed.connect( - partial(self.syncDriverTableCells, attrEdit, rbfAttrPlug) - ) - + for columnIndex, pValue in enumerate(poseInput): + # TODO, this is where we get the attrControlGroup + rbfAttrPlug = "{}.poses[{}].poseInput[{}]".format(rbfNode, + rowIndex, + columnIndex) + + attrEdit, mAttrFeild = getControlAttrWidget(rbfAttrPlug, + label="") + func = partial(self.syncDriverTableCells, + attrEdit, + rbfAttrPlug, + rowIndex, + columnIndex, + headerNames[columnIndex]) + self.driverPoseTableWidget.setCellWidget(rowIndex, + columnIndex, + attrEdit) + attrEdit.returnPressed.connect(func) tmpWidgets.append(attrEdit) - mayaUiItems.append(mAttrField) - - # Populate the table with widgets + mayaUiItems.append(mAttrFeild) setattr(self.driverPoseTableWidget, "associated", tmpWidgets) setattr(self.driverPoseTableWidget, "associatedMaya", mayaUiItems) @@ -1479,13 +1100,10 @@ def lockDriverWidgets(self, lock=True): lock (bool, optional): should it be locked """ self.setDriverButton.blockSignals(lock) - self.setDrivenButton.blockSignals(lock) if lock: - self.driverAttributesWidget.setEnabled(False) - self.drivenAttributesWidget.setEnabled(False) + self.driver_attributes_widget.setEnabled(False) else: - self.driverAttributesWidget.setEnabled(True) - self.drivenAttributesWidget.setEnabled(True) + self.driver_attributes_widget.setEnabled(True) def populateDriverInfo(self, rbfNode, weightInfo): """populate the driver widget, driver, control, driving attrs @@ -1494,60 +1112,65 @@ def populateDriverInfo(self, rbfNode, weightInfo): rbfNode (RBFNode): node for query weightInfo (dict): to pull information from, since we have it """ - driverNode = weightInfo.get("driverNode", [None])[0] - driverControl = weightInfo.get("driverControl", "") - driverAttrs = weightInfo.get("driverAttrs", []) - - self.driverLineEdit.setText(driverNode or "") + driverNode = weightInfo["driverNode"] + if driverNode: + driverNode = driverNode[0] + self.driverLineEdit.setText(driverNode) + driverControl = weightInfo["driverControl"] + # populate control here self.controlLineEdit.setText(driverControl) - self.setAttributeDisplay(self.driverAttributesWidget, driverNode, driverAttrs) + self.setAttributeDisplay(self.driver_attributes_widget, + driverNode, + weightInfo["driverAttrs"]) self.setDriverTable(rbfNode, weightInfo) - def populateDrivenInfo(self, rbfNode, weightInfo): - """populate the driver widget, driver, control, driving attrs + def _associateRBFnodeAndWidget(self, tabDrivenWidget, rbfNode): + """associates the RBFNode with a widget for convenience when adding, + deleting, editing Args: - rbfNode (RBFNode): node for query - weightInfo (dict): to pull information from, since we have it + tabDrivenWidget (QWidget): tab widget + rbfNode (RBFNode): instance to be associated """ - # Initialize Driven Widget - drivenWidget = self.createAndTagDrivenWidget() - self._associateRBFnodeAndWidget(drivenWidget, rbfNode) - - # Populate Driven Widget Info - self.populateDrivenWidgetInfo(drivenWidget, weightInfo, rbfNode) - - # Set Driven Node and Attributes - drivenNode = weightInfo.get("drivenNode", [None])[0] - self.drivenLineEdit.setText(drivenNode or "") - self.setAttributeDisplay(self.drivenAttributesWidget, drivenNode or "", weightInfo["drivenAttrs"]) - - # Add the driven widget to the tab widget. - drivenWidget.setProperty("drivenNode", drivenNode) - self.rbfTabWidget.addTab(drivenWidget, rbfNode.name) - self.setDrivenTable(drivenWidget, rbfNode, weightInfo) + setattr(tabDrivenWidget, "rbfNode", rbfNode) - def createAndTagDrivenWidget(self): + def createAndTagDrivenWidget(self, weightInfo, lockWidgets=True): """create and associate a widget, populated with the information provided by the weightInfo Args: + weightInfo (dict): information to populate the widgets with + lockWidgets (bool, optional): should they be locked from editing Returns: QWidget: parent widget that houses all the information to display """ - drivenWidget, tableWidget = self.createDrivenWidget() - drivenWidget.tableWidget = tableWidget - - # Set up signals for the table - header = tableWidget.verticalHeader() + drivenWidgetComponents = self.createDrivenAttributeWidget() + drivenWidget = drivenWidgetComponents.pop(-1) + widgetAttrs = ("drivenLineEdit", + "drivenSelectButton", + "attributeListWidget", + "tableWidget") + for component, attr in zip(drivenWidgetComponents, widgetAttrs): + setattr(drivenWidget, attr, component) + if attr == "attributeListWidget" and lockWidgets: + component.setEnabled(False) + # TODO add signal connections here + table = [wdgt for wdgt in drivenWidgetComponents + if type(wdgt) == QtWidgets.QTableWidget][0] + header = table.verticalHeader() + # TODO There was an inconsistency here with signals, potentially + # resolved header.sectionClicked.connect(self.setConsistentHeaderSelection) header.sectionClicked.connect(self.recallDriverPose) - tableWidget.itemSelectionChanged.connect(self.setEditDeletePoseEnabled) + selDelFunc = self.setEditDeletePoseEnabled + table.itemSelectionChanged.connect(selDelFunc) + clickWidget = [wdgt for wdgt in drivenWidgetComponents + if type(wdgt) == ClickableLineEdit][0] + clickWidget.clicked.connect(selectNode) return drivenWidget - @staticmethod - def setDrivenTable(drivenWidget, rbfNode, weightInfo): + def setDrivenTable(self, drivenWidget, rbfNode, weightInfo): """set the widgets with information from the weightInfo for dispaly Args: @@ -1556,21 +1179,24 @@ def setDrivenTable(drivenWidget, rbfNode, weightInfo): weightInfo (dict): of information to display """ poses = weightInfo["poses"] - drivenAttrs = weightInfo["drivenAttrs"] - rowCount = len(poses["poseValue"]) - verticalLabels = ["Pose {}".format(index) for index in range(rowCount)] - drivenWidget.tableWidget.clear() + rowCount = len(poses["poseValue"]) drivenWidget.tableWidget.setRowCount(rowCount) + drivenAttrs = weightInfo["drivenAttrs"] drivenWidget.tableWidget.setColumnCount(len(drivenAttrs)) drivenWidget.tableWidget.setHorizontalHeaderLabels(drivenAttrs) + verticalLabels = ["Pose {}".format(index) for index in range(rowCount)] drivenWidget.tableWidget.setVerticalHeaderLabels(verticalLabels) - for rowIndex, poseInput in enumerate(poses["poseValue"]): for columnIndex, pValue in enumerate(poseInput): - rbfAttrPlug = "{}.poses[{}].poseValue[{}]".format(rbfNode, rowIndex, columnIndex) - attrEdit, mAttrFeild = getControlAttrWidget(rbfAttrPlug, label="") - drivenWidget.tableWidget.setCellWidget(rowIndex, columnIndex, attrEdit) + rbfAttrPlug = "{}.poses[{}].poseValue[{}]".format(rbfNode, + rowIndex, + columnIndex) + attrEdit, mAttrFeild = getControlAttrWidget(rbfAttrPlug, + label="") + drivenWidget.tableWidget.setCellWidget(rowIndex, + columnIndex, + attrEdit) def populateDrivenWidgetInfo(self, drivenWidget, weightInfo, rbfNode): """set the information from the weightInfo to the widgets child of @@ -1584,15 +1210,20 @@ def populateDrivenWidgetInfo(self, drivenWidget, weightInfo, rbfNode): Returns: n/a: n/a """ + drivenWidget.drivenLineEdit.clear() driverNode = weightInfo["drivenNode"] if driverNode: driverNode = driverNode[0] else: return + drivenWidget.drivenLineEdit.setText(str(driverNode)) + self.setAttributeDisplay(drivenWidget.attributeListWidget, + weightInfo["drivenNode"][0], + weightInfo["drivenAttrs"]) self.setDrivenTable(drivenWidget, rbfNode, weightInfo) - def addNewTab(self, rbfNode, drivenNode): + def addNewTab(self, rbfNode): """Create a new tab in the setup Args: @@ -1601,29 +1232,11 @@ def addNewTab(self, rbfNode, drivenNode): Returns: QWidget: created widget """ - weightInfo = rbfNode.getNodeInfo() - tabDrivenWidget = self.createAndTagDrivenWidget() + tabDrivenWidget = self.createAndTagDrivenWidget({}) self._associateRBFnodeAndWidget(tabDrivenWidget, rbfNode) - self.rbfTabWidget.addTab(tabDrivenWidget, drivenNode) - tabDrivenWidget.setProperty("drivenNode", drivenNode) - self.setDrivenTable(tabDrivenWidget, rbfNode, weightInfo) - + self.rbfTabWidget.addTab(tabDrivenWidget, str(rbfNode)) return tabDrivenWidget - def addNewDriven(self): - self.refresh( - rbfSelection=False, - driverSelection=False, - drivenSelection=True, - currentRBFSetupNodes=False, - clearDrivenTab=False - ) - - self.setDrivenButton.blockSignals(False) - self.drivenAttributesWidget.setEnabled(True) - - self.addRbfButton.setText("Add New Driven") - def recreateDrivenTabs(self, rbfNodes): """remove tabs and create ones for each node in rbfNodes provided @@ -1634,14 +1247,12 @@ def recreateDrivenTabs(self, rbfNodes): self.rbfTabWidget.clear() for rbfNode in rbfNodes: weightInfo = rbfNode.getNodeInfo() - drivenWidget = self.createAndTagDrivenWidget() + drivenWidget = self.createAndTagDrivenWidget(weightInfo) self._associateRBFnodeAndWidget(drivenWidget, rbfNode) self.populateDrivenWidgetInfo(drivenWidget, weightInfo, rbfNode) self.rbfTabWidget.addTab(drivenWidget, rbfNode.name) - self.addPoseButton.setEnabled(True) - - def displayRBFSetupInfo(self): + def displayRBFSetupInfo(self, index): """Display the rbfnodes within the desired setups Args: @@ -1649,32 +1260,27 @@ def displayRBFSetupInfo(self): """ rbfSelection = str(self.rbf_cbox.currentText()) - - # Refresh UI components self.refresh(rbfSelection=False, driverSelection=True, drivenSelection=True, currentRBFSetupNodes=False) - - # Handle 'New' selection case if rbfSelection.startswith("New "): self.currentRBFSetupNodes = [] self.lockDriverWidgets(lock=False) return - - # Fetch RBF nodes for the selected setup rbfNodes = self.allSetupsInfo.get(rbfSelection, []) if not rbfNodes: return - - # Display node info in the UI self.currentRBFSetupNodes = rbfNodes weightInfo = rbfNodes[0].getNodeInfo() - self.populateDriverInfo(rbfNodes[0], weightInfo) - self.populateDrivenInfo(rbfNodes[0], weightInfo) self.lockDriverWidgets(lock=True) - + # wrapping the following in try due to what I think is a Qt Bug. + # need to look further into this. + # File "rbf_manager_ui.py", line 872, in createAndTagDrivenWidget + # header.sectionClicked.connect(self.setConsistentHeaderSelection) + # AttributeError: 'PySide2.QtWidgets.QListWidgetItem' object has + # no attribute 'sectionClicked' try: self.recreateDrivenTabs(self.allSetupsInfo[rbfSelection]) except AttributeError: @@ -1687,7 +1293,6 @@ def displayRBFSetupInfo(self): def attrListMenu(self, attributeListWidget, driverLineEdit, - attributeListType, QPos, nodeToQuery=None): """right click menu for queie qlistwidget @@ -1711,8 +1316,7 @@ def attrListMenu(self, menu_item_01.triggered.connect(partial(self.updateAttributeDisplay, attributeListWidget, nodeToQuery, - attrType="keyable", - force=True)) + attrType="keyable")) menu2Label = "Display ChannelBox (Non Keyable)" menu_item_02 = self.attrMenu.addAction(menu2Label) menu2tip = "Show attributes in ChannelBox that are not keyable." @@ -1720,22 +1324,13 @@ def attrListMenu(self, menu_item_02.triggered.connect(partial(self.updateAttributeDisplay, attributeListWidget, nodeToQuery, - attrType="cb", - force=True)) + attrType="cb")) menu_item_03 = self.attrMenu.addAction("Display All") menu_item_03.setToolTip("GIVE ME ALL!") menu_item_03.triggered.connect(partial(self.updateAttributeDisplay, attributeListWidget, nodeToQuery, - attrType="all", - force=True)) - - self.attrMenu.addSeparator() - - menu_item_04 = self.attrMenu.addAction("Set attribute to auto select") - menu_item_04.setToolTip("Set your attribute to be automatically highlighted up in the next operations") - menu_item_04.triggered.connect(partial(self.setAttributeToAutoSelect, - attributeListWidget)) + attrType="all")) self.attrMenu.move(parentPosition + QPos) self.attrMenu.show() @@ -1746,14 +1341,12 @@ def refreshRbfSetupList(self, setToSelection=False): setToSelection (bool, optional): after refresh, set to desired """ self.rbf_cbox.blockSignals(True) - - # Clear the combo box and populate with new setup options self.rbf_cbox.clear() + addNewOfType = ["New {} setup".format(rbf) + for rbf in rbf_node.SUPPORTED_RBF_NODES] self.updateAllSetupsInfo() - allSetups = sorted(self.allSetupsInfo.keys()) - newSetupOptions = ["New {} setup".format(rbf) for rbf in rbf_node.SUPPORTED_RBF_NODES] - self.rbf_cbox.addItems(newSetupOptions + allSetups) - + addNewOfType.extend(sorted(self.allSetupsInfo.keys())) + self.rbf_cbox.addItems(addNewOfType) if setToSelection: selectionIndex = self.rbf_cbox.findText(setToSelection) self.rbf_cbox.setCurrentIndex(selectionIndex) @@ -1761,17 +1354,6 @@ def refreshRbfSetupList(self, setToSelection=False): self.lockDriverWidgets(lock=False) self.rbf_cbox.blockSignals(False) - def clearDriverTabs(self): - """force deletion on tab widgets - """ - toRemove = [] - tabIndicies = self.driverPoseTableWidget.count() - for index in range(tabIndicies): - tabWidget = self.driverPoseTableWidget.widget(index) - toRemove.append(tabWidget) - self.driverPoseTableWidget.clear() - [t.deleteLater() for t in toRemove] - def clearDrivenTabs(self): """force deletion on tab widgets """ @@ -1788,7 +1370,6 @@ def refresh(self, driverSelection=True, drivenSelection=True, currentRBFSetupNodes=True, - clearDrivenTab=True, *args): """Refreshes the UI @@ -1797,28 +1378,18 @@ def refresh(self, driverSelection (bool, optional): desired section to refresh drivenSelection (bool, optional): desired section to refresh currentRBFSetupNodes (bool, optional): desired section to refresh - clearDrivenTab (bool, optional): desired section to refresh """ - self.addRbfButton.setText("New RBF") - self.addPoseButton.setEnabled(False) if rbfSelection: self.refreshRbfSetupList() if driverSelection: self.controlLineEdit.clear() self.driverLineEdit.clear() - self.driverAttributesWidget.clear() + self.driver_attributes_widget.clear() + self.__deleteAssociatedWidgetsMaya(self.driverPoseTableWidget) + self.__deleteAssociatedWidgets(self.driverPoseTableWidget) self.driverPoseTableWidget.clear() - - self.driverPoseTableWidget.setRowCount(1) - self.driverPoseTableWidget.setColumnCount(1) - self.driverPoseTableWidget.setHorizontalHeaderLabels(["Pose Value"]) - self.driverPoseTableWidget.setVerticalHeaderLabels(["Pose #0"]) - if drivenSelection: - self.drivenLineEdit.clear() - self.drivenAttributesWidget.clear() - if clearDrivenTab: - self.rbfTabWidget.clear() + self.clearDrivenTabs() if currentRBFSetupNodes: self.currentRBFSetupNodes = [] @@ -1896,8 +1467,7 @@ def setSetupDriverControl(self, lineEditWidget): controlName = self.setNodeToField(lineEditWidget) self.setDriverControlOnSetup(controlName) - @staticmethod - def getRBFNodesInfo(rbfNodes): + def getRBFNodesInfo(self, rbfNodes): """create a dictionary of all the RBFInfo(referred to as weightNodeInfo a lot) for export @@ -1953,8 +1523,7 @@ def exportNodes(self, allSetups=True): return rbf_io.exportRBFs(nodesToExport, filePath) - @staticmethod - def gatherMirroredInfo(rbfNodes): + def gatherMirroredInfo(self, rbfNodes): """gather all the info from the provided nodes and string replace side information for its mirror. Using mGear standard naming convections @@ -1973,19 +1542,27 @@ def gatherMirroredInfo(rbfNodes): for pairs in weightInfo["connections"]: mrConnections.append([mString.convertRLName(pairs[0]), mString.convertRLName(pairs[1])]) - weightInfo["connections"] = mrConnections - weightInfo["drivenControlName"] = mString.convertRLName(weightInfo["drivenControlName"]) - weightInfo["drivenNode"] = [mString.convertRLName(n) for n in weightInfo["drivenNode"]] - weightInfo["driverControl"] = mString.convertRLName(weightInfo["driverControl"]) - weightInfo["driverNode"] = [mString.convertRLName(n) for n in weightInfo["driverNode"]] - + # drivenControlName ----------------------------------------------- + mrDrvnCtl = mString.convertRLName(weightInfo["drivenControlName"]) + weightInfo["drivenControlName"] = mrDrvnCtl + # drivenNode ------------------------------------------------------ + weightInfo["drivenNode"] = [mString.convertRLName(n) for n + in weightInfo["drivenNode"]] + # driverControl --------------------------------------------------- + mrDrvrCtl = mString.convertRLName(weightInfo["driverControl"]) + weightInfo["driverControl"] = mrDrvrCtl + # driverNode ------------------------------------------------------ + weightInfo["driverNode"] = [mString.convertRLName(n) for n + in weightInfo["driverNode"]] # setupName ------------------------------------------------------- mrSetupName = mString.convertRLName(weightInfo["setupName"]) if mrSetupName == weightInfo["setupName"]: mrSetupName = "{}{}".format(mrSetupName, MIRROR_SUFFIX) weightInfo["setupName"] = mrSetupName # transformNode --------------------------------------------------- + # name + # parent tmp = weightInfo["transformNode"]["name"] mrTransformName = mString.convertRLName(tmp) weightInfo["transformNode"]["name"] = mrTransformName @@ -2014,7 +1591,8 @@ def getMirroredSetupTargetsInfo(self): drivenControlNode = rbfNode.getConnectedRBFToggleNode() mrDrivenControlNode = mString.convertRLName(drivenControlNode) mrDrivenControlNode = pm.PyNode(mrDrivenControlNode) - setupTargetInfo_dict[pm.PyNode(drivenNode)] = [mrDrivenControlNode, mrRbfNode] + setupTargetInfo_dict[pm.PyNode(drivenNode)] = [mrDrivenControlNode, + mrRbfNode] return setupTargetInfo_dict def mirrorSetup(self): @@ -2126,6 +1704,7 @@ def toggleGetPoseType(self, toggleState): toggleState (bool): default True """ self.absWorld = toggleState + print("Recording poses in world space set to: {}".format(toggleState)) def toggleDefaultType(self, toggleState): """records whether the user wants default poses to be zeroed @@ -2134,100 +1713,255 @@ def toggleDefaultType(self, toggleState): toggleState (bool): default True """ self.zeroedDefaults = toggleState + print("Default poses are zeroed: {}".format(toggleState)) # signal management ------------------------------------------------------- def connectSignals(self): """connect all the signals in the UI Exceptions being MenuBar and Table header signals """ - # RBF ComboBox and Refresh Button self.rbf_cbox.currentIndexChanged.connect(self.displayRBFSetupInfo) + self.rbf_refreshButton.clicked.connect(self.refresh) - # Driver Line Edit and Control Line Edit self.driverLineEdit.clicked.connect(selectNode) self.controlLineEdit.clicked.connect(selectNode) - self.driverLineEdit.textChanged.connect(partial(self.updateAttributeDisplay, - self.driverAttributesWidget)) - self.drivenLineEdit.textChanged.connect(partial(self.updateAttributeDisplay, - self.drivenAttributesWidget)) - - # Table Widget header = self.driverPoseTableWidget.verticalHeader() header.sectionClicked.connect(self.setConsistentHeaderSelection) header.sectionClicked.connect(self.recallDriverPose) selDelFunc = self.setEditDeletePoseEnabled self.driverPoseTableWidget.itemSelectionChanged.connect(selDelFunc) - - # Buttons Widget self.addRbfButton.clicked.connect(self.addRBFToSetup) + self.addPoseButton.clicked.connect(self.addPose) self.editPoseButton.clicked.connect(self.editPose) self.editPoseValuesButton.clicked.connect(self.editPoseValues) self.deletePoseButton.clicked.connect(self.deletePose) - self.setControlButton.clicked.connect(partial(self.setSetupDriverControl, self.controlLineEdit)) - self.setDriverButton.clicked.connect(partial(self.setNodeToField, self.driverLineEdit)) - self.setDrivenButton.clicked.connect(partial(self.setNodeToField, self.drivenLineEdit, multi=True)) - self.allButton.clicked.connect(self.setDriverControlLineEdit) - self.addDrivenButton.clicked.connect(self.addNewDriven) - - # Custom Context Menus - customMenu = self.driverAttributesWidget.customContextMenuRequested - customMenu.connect( - partial(self.attrListMenu, - self.driverAttributesWidget, - self.driverLineEdit, - "driver") - ) - customMenu = self.drivenAttributesWidget.customContextMenuRequested - customMenu.connect( - partial(self.attrListMenu, - self.drivenAttributesWidget, - self.driverLineEdit, - "driven") - ) - - # Tab Widget + partialObj = partial(self.setSetupDriverControl, self.controlLineEdit) + self.setControlButton.clicked.connect(partialObj) + self.setDriverButton.clicked.connect(partial(self.setNodeToField, + self.driverLineEdit)) + partialObj = partial(self.updateAttributeDisplay, + self.driver_attributes_widget) + self.driverLineEdit.textChanged.connect(partialObj) + partialObj = partial(self.attrListMenu, + self.driver_attributes_widget, + self.driverLineEdit) + customMenu = self.driver_attributes_widget.customContextMenuRequested + customMenu.connect(partialObj) tabBar = self.rbfTabWidget.tabBar() tabBar.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) tabBar.customContextMenuRequested.connect(self.tabConextMenu) tabBar.tabCloseRequested.connect(self.removeRBFFromSetup) - # main assebly ------------------------------------------------------------ - def createCentralWidget(self): - """main UI assembly + # broken down widgets ----------------------------------------------------- + def createSetupSelectorWidget(self): + """create the top portion of the weidget, select setup + refresh Returns: - QtWidget: main UI to be parented to as the centralWidget + list: QLayout, QCombobox, QPushButton """ - centralWidget = QtWidgets.QWidget() - centralWidgetLayout = QtWidgets.QVBoxLayout() - centralWidget.setLayout(centralWidgetLayout) + setRBFLayout = QtWidgets.QHBoxLayout() + rbfLabel = QtWidgets.QLabel("Select RBF Setup:") + rbf_cbox = QtWidgets.QComboBox() + rbf_refreshButton = QtWidgets.QPushButton("Refresh") + rbf_cbox.setFixedHeight(self.genericWidgetHight) + rbf_refreshButton.setMaximumWidth(80) + rbf_refreshButton.setFixedHeight(self.genericWidgetHight - 1) + setRBFLayout.addWidget(rbfLabel) + setRBFLayout.addWidget(rbf_cbox, 1) + setRBFLayout.addWidget(rbf_refreshButton) + return setRBFLayout, rbf_cbox, rbf_refreshButton + + def selectNodeWidget(self, label, buttonLabel="Select"): + """create a lout with label, lineEdit, QPushbutton for user input + """ + nodeLayout = QtWidgets.QHBoxLayout() + nodeLabel = QtWidgets.QLabel(label) + nodeLabel.setFixedWidth(40) + nodeLineEdit = ClickableLineEdit() + nodeLineEdit.setReadOnly(True) + nodeSelectButton = QtWidgets.QPushButton(buttonLabel) + nodeLineEdit.setFixedHeight(self.genericWidgetHight) + nodeSelectButton.setFixedHeight(self.genericWidgetHight) + nodeLayout.addWidget(nodeLabel) + nodeLayout.addWidget(nodeLineEdit, 1) + nodeLayout.addWidget(nodeSelectButton) + return nodeLayout, nodeLineEdit, nodeSelectButton - splitter = QtWidgets.QSplitter() + def labelListWidget(self, label, horizontal=True): + """create the listAttribute that users can select their driver/driven + attributes for the setup - # Setup selector section - (rbfLayout, - self.rbf_cbox, - self.rbf_refreshButton) = self.createSetupSelectorWidget() - self.rbf_cbox.setToolTip("List of available setups in the scene.") - self.rbf_refreshButton.setToolTip("Refresh the UI") + Args: + label (str): to display above the listWidget + horizontal (bool, optional): should the label be above or infront + of the listWidget - driverDrivenWidget = self.createDarkContainerWidget() - allTableWidget = self.createDriverDrivenTableWidget() + Returns: + list: QLayout, QListWidget + """ + if horizontal: + attributeLayout = QtWidgets.QHBoxLayout() + else: + attributeLayout = QtWidgets.QVBoxLayout() + attributeLabel = QtWidgets.QLabel(label) + attributeListWidget = QtWidgets.QListWidget() + attributeLayout.addWidget(attributeLabel) + attributeLayout.addWidget(attributeListWidget) + return attributeLayout, attributeListWidget - centralWidgetLayout.addLayout(rbfLayout) - centralWidgetLayout.addWidget(HLine()) - splitter.addWidget(driverDrivenWidget) - splitter.addWidget(allTableWidget) - centralWidgetLayout.addWidget(splitter) - - # Assuming a ratio of 2:1 for settingWidth to tableWidth - totalWidth = splitter.width() - attributeWidth = (1/3) * totalWidth - tableWidth = (2/3) * totalWidth - splitter.setSizes([int(attributeWidth), int(tableWidth)]) - return centralWidget + def addRemoveButtonWidget(self, label1, label2, horizontal=True): + if horizontal: + addRemoveLayout = QtWidgets.QHBoxLayout() + else: + addRemoveLayout = QtWidgets.QVBoxLayout() + addAttributesButton = QtWidgets.QPushButton(label1) + removeAttributesButton = QtWidgets.QPushButton(label2) + addRemoveLayout.addWidget(addAttributesButton) + addRemoveLayout.addWidget(removeAttributesButton) + return addRemoveLayout, addAttributesButton, removeAttributesButton + + def createDriverAttributeWidget(self): + """widget where the user inputs information for the setups + + Returns: + list: [of widgets] + """ + driverMainLayout = QtWidgets.QVBoxLayout() + # -------------------------------------------------------------------- + (driverLayout, + driverLineEdit, + driverSelectButton) = self.selectNodeWidget("Driver", + buttonLabel="Set") + driverLineEdit.setToolTip("The node driving the setup. (Click me!)") + # -------------------------------------------------------------------- + (controlLayout, + controlLineEdit, + setControlButton) = self.selectNodeWidget("Control", + buttonLabel="Set") + controlLineEdit.setToolTip("The node driving the setup. (Click me!)") + # -------------------------------------------------------------------- + (attributeLayout, + attributeListWidget) = self.labelListWidget(label="Select Attributes", + horizontal=False) + attributeListWidget.setToolTip("List of attributes driving setup.") + selType = QtWidgets.QAbstractItemView.ExtendedSelection + attributeListWidget.setSelectionMode(selType) + attributeListWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + # -------------------------------------------------------------------- + driverMainLayout.addLayout(driverLayout, 0) + driverMainLayout.addLayout(controlLayout, 0) + driverMainLayout.addLayout(attributeLayout, 0) + return [controlLineEdit, + setControlButton, + driverLineEdit, + driverSelectButton, + attributeListWidget, + driverMainLayout] + + def createDrivenAttributeWidget(self): + """the widget that displays the driven information + + Returns: + list: [of widgets] + """ + drivenWidget = QtWidgets.QWidget() + drivenMainLayout = QtWidgets.QHBoxLayout() + drivenMainLayout.setContentsMargins(0, 10, 0, 10) + drivenMainLayout.setSpacing(9) + driverSetLayout = QtWidgets.QVBoxLayout() + drivenMainLayout.addLayout(driverSetLayout) + drivenWidget.setLayout(drivenMainLayout) + # -------------------------------------------------------------------- + (driverLayout, + driverLineEdit, + driverSelectButton) = self.selectNodeWidget("Driven", + buttonLabel="Select") + drivenTip = "The node being driven by setup. (Click me!)" + driverLineEdit.setToolTip(drivenTip) + driverSelectButton.hide() + # -------------------------------------------------------------------- + (attributeLayout, + attributeListWidget) = self.labelListWidget(label="Attributes", + horizontal=False) + attributeListWidget.setToolTip("Attributes being driven by setup.") + attributeLayout.setSpacing(1) + selType = QtWidgets.QAbstractItemView.ExtendedSelection + attributeListWidget.setSelectionMode(selType) + attributeListWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + # -------------------------------------------------------------------- + tableWidget = self.createTableWidget() + + driverSetLayout.addLayout(driverLayout, 0) + driverSetLayout.addLayout(attributeLayout, 0) + drivenMainLayout.addWidget(tableWidget, 1) + return [driverLineEdit, + driverSelectButton, + attributeListWidget, + tableWidget, + drivenWidget] + + def createTableWidget(self): + """create table widget used to display poses, set tooltips and colum + + Returns: + QTableWidget: QTableWidget + """ + tableWidget = QtWidgets.QTableWidget() + tableWidget.insertColumn(0) + tableWidget.insertRow(0) + tableWidget.setHorizontalHeaderLabels(["Pose Value"]) + tableWidget.setVerticalHeaderLabels(["Pose #0"]) + tableTip = "Live connections to the RBF Node in your setup." + tableTip = tableTip + "\nSelect the desired Pose # to recall pose." + tableWidget.setToolTip(tableTip) + return tableWidget + + def createTabWidget(self): + """Tab widget to add driven widgets too. Custom TabBar so the tab is + easier to select + + Returns: + QTabWidget: + """ + tabLayout = QtWidgets.QTabWidget() + tabLayout.setContentsMargins(0, 0, 0, 0) + tabBar = TabBar() + tabLayout.setTabBar(tabBar) + tabBar.setTabsClosable(True) + return tabLayout + + def createOptionsButtonsWidget(self): + """add, edit, delete buttons for modifying rbf setups. + + Returns: + list: [QPushButtons] + """ + optionsLayout = QtWidgets.QHBoxLayout() + addPoseButton = QtWidgets.QPushButton("Add Pose") + addTip = "After positioning all controls in the setup, add new pose." + addTip = addTip + "\nEnsure the driver node has a unique position." + addPoseButton.setToolTip(addTip) + addPoseButton.setFixedHeight(self.genericWidgetHight) + EditPoseButton = QtWidgets.QPushButton("Edit Pose") + EditPoseButton.setToolTip("Recall pose, adjust controls and Edit.") + EditPoseButton.setFixedHeight(self.genericWidgetHight) + EditPoseValuesButton = QtWidgets.QPushButton("Edit Pose Values") + EditPoseValuesButton.setToolTip("Set pose based on values in table") + EditPoseValuesButton.setFixedHeight(self.genericWidgetHight) + deletePoseButton = QtWidgets.QPushButton("Delete Pose") + deletePoseButton.setToolTip("Recall pose, then Delete") + deletePoseButton.setFixedHeight(self.genericWidgetHight) + optionsLayout.addWidget(addPoseButton) + optionsLayout.addWidget(EditPoseButton) + optionsLayout.addWidget(EditPoseValuesButton) + optionsLayout.addWidget(deletePoseButton) + return (optionsLayout, + addPoseButton, + EditPoseButton, + EditPoseValuesButton, + deletePoseButton) def createMenuBar(self, hideMenuBar=False): """Create the UI menubar, with option to hide based on mouse input @@ -2286,6 +2020,59 @@ def createMenuBar(self, hideMenuBar=False): self.mousePosition.connect(self.hideMenuBar) return mainMenuBar + # main assebly ------------------------------------------------------------ + + def createCentralWidget(self): + """main UI assembly + + Returns: + QtWidget: main UI to be parented to as the centralWidget + """ + centralWidget = QtWidgets.QWidget() + centralWidgetLayout = QtWidgets.QVBoxLayout() + centralWidget.setLayout(centralWidgetLayout) + (rbfLayout, + self.rbf_cbox, + self.rbf_refreshButton) = self.createSetupSelectorWidget() + self.rbf_cbox.setToolTip("List of available setups in the scene.") + self.rbf_refreshButton.setToolTip("Refresh the UI") + centralWidgetLayout.addLayout(rbfLayout) + centralWidgetLayout.addWidget(HLine()) + # -------------------------------------------------------------------- + driverDrivenLayout = QtWidgets.QHBoxLayout() + (self.controlLineEdit, + self.setControlButton, + self.driverLineEdit, + self.setDriverButton, + self.driver_attributes_widget, + driverLayout) = self.createDriverAttributeWidget() + + self.addRbfButton = QtWidgets.QPushButton("New RBF") + self.addRbfButton.setToolTip("Select node to be driven by setup.") + self.addRbfButton.setFixedHeight(self.genericWidgetHight) + self.addRbfButton.setStyleSheet("background-color: rgb(23, 158, 131)") + driverLayout.addWidget(self.addRbfButton) + + self.driverPoseTableWidget = self.createTableWidget() + driverDrivenLayout.addLayout(driverLayout, 0) + driverDrivenLayout.addWidget(self.driverPoseTableWidget, 1) + centralWidgetLayout.addLayout(driverDrivenLayout, 1) + # -------------------------------------------------------------------- + self.rbfTabWidget = self.createTabWidget() + centralWidgetLayout.addWidget(self.rbfTabWidget, 1) + # -------------------------------------------------------------------- + (optionsLayout, + self.addPoseButton, + self.editPoseButton, + self.editPoseValuesButton, + self.deletePoseButton) = self.createOptionsButtonsWidget() + self.editPoseButton.setEnabled(False) + self.editPoseValuesButton.setEnabled(False) + self.deletePoseButton.setEnabled(False) + centralWidgetLayout.addWidget(HLine()) + centralWidgetLayout.addLayout(optionsLayout) + return centralWidget + # overrides --------------------------------------------------------------- def mouseMoveEvent(self, event): """used for tracking the mouse position over the UI, in this case for @@ -2299,3 +2086,15 @@ def mouseMoveEvent(self, event): pos = event.pos() self.mousePosition.emit(pos.x(), pos.y()) + def closeEvent(self, evnt): + """on UI close, ensure that all attrControlgrps are destroyed in case + the user is just reopening the UI. Properly severs ties to the attrs + + Args: + evnt (Qt.QEvent): Close event called + """ + self.__deleteAssociatedWidgetsMaya(self.driverPoseTableWidget) + self.__deleteAssociatedWidgets(self.driverPoseTableWidget) + if self.callBackID is not None: + self.removeSceneCallback() + super(RBFManagerUI, self).closeEvent(evnt) From 7344691e159ac466ec90daba2e93fe457d96d978 Mon Sep 17 00:00:00 2001 From: Joji Date: Sun, 5 Nov 2023 14:35:18 -0800 Subject: [PATCH 19/20] Add icons --- .../mgear/rigbits/rbf_manager2/rbf_manager_ui.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/release/scripts/mgear/rigbits/rbf_manager2/rbf_manager_ui.py b/release/scripts/mgear/rigbits/rbf_manager2/rbf_manager_ui.py index f64b76ab..8a51d226 100644 --- a/release/scripts/mgear/rigbits/rbf_manager2/rbf_manager_ui.py +++ b/release/scripts/mgear/rigbits/rbf_manager2/rbf_manager_ui.py @@ -73,7 +73,7 @@ from mgear.core import pyqt import mgear.core.string as mString from mgear.core import anim_utils -from mgear.vendor.Qt import QtWidgets, QtCore, QtCompat +from mgear.vendor.Qt import QtWidgets, QtCore, QtCompat, QtGui from maya.app.general.mayaMixin import MayaQWidgetDockableMixin from mgear.rigbits.six import PY2 @@ -431,17 +431,15 @@ def _associateRBFnodeAndWidget(tabDrivenWidget, rbfNode): setattr(tabDrivenWidget, "rbfNode", rbfNode) @staticmethod - def createCustomButton(label, size=(35, 27), tooltip=""): + def createCustomButton(label, size=(35, 27), icon=None, iconSize=None, tooltip=""): stylesheet = ( - "QPushButton {background-color: #5D5D5D; border-radius: 4px;}" "QPushButton:pressed { background-color: #00A6F3;}" - "QPushButton:hover:!pressed { background-color: #707070;}" - "QPushButton:disabled { background-color: #4a4a4a;}" ) button = QtWidgets.QPushButton(label) button.setMinimumSize(QtCore.QSize(*size)) button.setStyleSheet(stylesheet) button.setToolTip(tooltip) + button.setIcon(pyqt.get_icon(icon, iconSize)) return button @staticmethod @@ -524,7 +522,9 @@ def createSetupSelectorWidget(self): setRBFLayout = QtWidgets.QHBoxLayout() rbfLabel = QtWidgets.QLabel("Select RBF Setup:") rbf_cbox = QtWidgets.QComboBox() - rbf_refreshButton = self.createCustomButton("Refresh", (60, 25), "Refresh the UI") + rbf_refreshButton = self.createCustomButton( + "", (35, 25), icon="mgear_refresh-cw", iconSize=16, tooltip="Refresh the UI" + ) rbf_cbox.setFixedHeight(self.genericWidgetHight) rbf_refreshButton.setMaximumWidth(80) rbf_refreshButton.setFixedHeight(self.genericWidgetHight - 1) @@ -555,7 +555,7 @@ def createDriverAttributeWidget(self): driverSelectButton) = self.selectNodeWidget("Driver", buttonLabel="Set") driverLineEdit.setToolTip("The node driving the setup. (Click me!)") # -------------------------------------------------------------------- - allButton = self.createCustomButton("All", (20, 53), "") + allButton = self.createCustomButton("", (20, 52), tooltip="", icon="mgear_rewind", iconSize=15) (attributeLayout, attributeListWidget) = self.labelListWidget( label="Select Driver Attributes:", attrListType="driver", horizontal=False) @@ -598,7 +598,7 @@ def createDrivenAttributeWidget(self): drivenTip = "The node being driven by setup. (Click me!)" drivenLineEdit.setToolTip(drivenTip) - addDrivenButton = self.createCustomButton("+", (20, 26), "") + addDrivenButton = self.createCustomButton("", (20, 25), icon="mgear_plus", iconSize=16, tooltip="") addDrivenButton.setToolTip("Add a new driven to the current rbf node") # -------------------------------------------------------------------- (attributeLayout, From ce71d1fe3a2fb4c71d4c1c9569455e30466fac0e Mon Sep 17 00:00:00 2001 From: Joji Date: Sat, 11 Nov 2023 00:43:45 -0800 Subject: [PATCH 20/20] Created widget.py and refactored the code --- .../rigbits/rbf_manager2/rbf_manager_ui.py | 779 +++--------------- .../mgear/rigbits/rbf_manager2/widget.py | 560 +++++++++++++ 2 files changed, 689 insertions(+), 650 deletions(-) create mode 100644 release/scripts/mgear/rigbits/rbf_manager2/widget.py diff --git a/release/scripts/mgear/rigbits/rbf_manager2/rbf_manager_ui.py b/release/scripts/mgear/rigbits/rbf_manager2/rbf_manager_ui.py index 8a51d226..633a06ea 100644 --- a/release/scripts/mgear/rigbits/rbf_manager2/rbf_manager_ui.py +++ b/release/scripts/mgear/rigbits/rbf_manager2/rbf_manager_ui.py @@ -43,9 +43,9 @@ Attributes: CTL_SUFFIX (str): suffix for anim controls DRIVEN_SUFFIX (str): suffix for driven group nodes - EXTRA_MODULE_DICT (str): name of the dict which holds additional modules - MGEAR_EXTRA_ENVIRON (str): environment variable to query for paths - TOOL_NAME (str): name of UI + widget.EXTRA_MODULE_DICT (str): name of the dict which holds additional modules + widget.MGEAR_EXTRA_ENVIRON (str): environment variable to query for paths + widget.TOOL_NAME (str): name of UI TOOL_TITLE (str): title as it appears in the ui __version__ (float): UI version @@ -80,41 +80,14 @@ # rbf from mgear.rigbits import rbf_io from mgear.rigbits import rbf_node +from . import widget -# ============================================================================= -# Constants -# ============================================================================= -__version__ = "2.0.0" - -_mgear_version = mgear.getVersion() -TOOL_NAME = "RBF Manager" -TOOL_TITLE = "{} v{} | mGear {}".format(TOOL_NAME, __version__, _mgear_version) -UI_NAME = "RBFManagerUI" -WORK_SPACE_NAME = UI_NAME + "WorkspaceControl" - -DRIVEN_SUFFIX = rbf_node.DRIVEN_SUFFIX -CTL_SUFFIX = rbf_node.CTL_SUFFIX - -MGEAR_EXTRA_ENVIRON = "MGEAR_RBF_EXTRA" -EXTRA_MODULE_DICT = "extraFunc_dict" - -MIRROR_SUFFIX = "_mr" - # ============================================================================= # general functions # ============================================================================= -def testFunctions(*args): - """test function for connecting signals during debug - - Args: - *args: Description - """ - print('!!', args) - - def getPlugAttrs(nodes, attrType="keyable"): """Get a list of attributes to display to the user @@ -190,16 +163,16 @@ def getEnvironModules(): Returns: dict: displayName:funcObject """ - extraModulePath = os.environ.get(MGEAR_EXTRA_ENVIRON, None) + extraModulePath = os.environ.get(widget.MGEAR_EXTRA_ENVIRON, None) if extraModulePath is None or not os.path.exists(extraModulePath): return None - exModule = imp.load_source(MGEAR_EXTRA_ENVIRON, + exModule = imp.load_source(widget.MGEAR_EXTRA_ENVIRON, os.path.abspath(extraModulePath)) - additionalFuncDict = getattr(exModule, EXTRA_MODULE_DICT, None) + additionalFuncDict = getattr(exModule, widget.EXTRA_MODULE_DICT, None) if additionalFuncDict is None: - mc.warning("'{}' not found in {}".format(EXTRA_MODULE_DICT, + mc.warning("'{}' not found in {}".format(widget.EXTRA_MODULE_DICT, extraModulePath)) - print("No additional menu items added to {}".format(TOOL_NAME)) + print("No additional menu items added to {}".format(widget.TOOL_NAME)) return additionalFuncDict @@ -215,66 +188,6 @@ def selectNode(name): print(name, "No longer exists for selection!") -# ============================================================================= -# UI General Functions -# ============================================================================= - -def getControlAttrWidget(nodeAttr, label=""): - """Create a cmds.attrControlGrp and wrap it in a qtWidget, preserving its connection - to the specified attr - - Args: - nodeAttr (str): node.attr, the target for the attrControlGrp - label (str, optional): name for the attr widget - - Returns: - QtWidget.QLineEdit: A Qt widget created from attrControlGrp - str: The name of the created Maya attrControlGrp - """ - mAttrFeild = mc.attrControlGrp(attribute=nodeAttr, label=label, po=True) - - # Convert the Maya control to a Qt pointer - ptr = mui.MQtUtil.findControl(mAttrFeild) - - # Wrap the Maya control into a Qt widget, considering Python version - controlWidget = QtCompat.wrapInstance(long(ptr) if PY2 else int(ptr), base=QtWidgets.QWidget) - controlWidget.setContentsMargins(0, 0, 0, 0) - controlWidget.setMinimumWidth(0) - - attrEdit = [wdgt for wdgt in controlWidget.children() if type(wdgt) == QtWidgets.QLineEdit] - [wdgt.setParent(attrEdit[0]) for wdgt in controlWidget.children() - if type(wdgt) == QtCore.QObject] - - attrEdit[0].setParent(None) - controlWidget.setParent(attrEdit[0]) - controlWidget.setHidden(True) - return attrEdit[0], mAttrFeild - - -def HLine(): - """seporator line for widgets - - Returns: - Qframe: line for seperating UI elements visually - """ - seperatorLine = QtWidgets.QFrame() - seperatorLine.setFrameShape(QtWidgets.QFrame.HLine) - seperatorLine.setFrameShadow(QtWidgets.QFrame.Sunken) - return seperatorLine - - -def VLine(): - """seporator line for widgets - - Returns: - Qframe: line for seperating UI elements visually - """ - seperatorLine = QtWidgets.QFrame() - seperatorLine.setFrameShape(QtWidgets.QFrame.VLine) - seperatorLine.setFrameShadow(QtWidgets.QFrame.Sunken) - return seperatorLine - - def show(dockable=True, newSceneCallBack=True, *args): """To launch the UI and ensure any previously opened instance is closed. @@ -289,8 +202,8 @@ def show(dockable=True, newSceneCallBack=True, *args): global RBF_UI # Ensure we have access to the global variable # Attempt to close any existing UI with the given name - if mc.workspaceControl(WORK_SPACE_NAME, exists=True): - mc.deleteUI(WORK_SPACE_NAME) + if mc.workspaceControl(widget.WORK_SPACE_NAME, exists=True): + mc.deleteUI(widget.WORK_SPACE_NAME) # Create the UI RBF_UI = RBFManagerUI(newSceneCallBack=newSceneCallBack) @@ -307,489 +220,86 @@ def show(dockable=True, newSceneCallBack=True, *args): return RBF_UI -def genericWarning(parent, warningText): - """generic prompt warning with the provided text - - Args: - parent (QWidget): Qwidget to be parented under - warningText (str): information to display to the user - - Returns: - QtCore.Response: of what the user chose. For warnings - """ - selWarning = QtWidgets.QMessageBox(parent) - selWarning.setText(warningText) - results = selWarning.exec_() - return results - - -def promptAcceptance(parent, descriptionA, descriptionB): - """Warn user, asking for permission - - Args: - parent (QWidget): to be parented under - descriptionA (str): info - descriptionB (str): further info - - Returns: - QtCore.Response: accept, deline, reject - """ - msgBox = QtWidgets.QMessageBox(parent) - msgBox.setText(descriptionA) - msgBox.setInformativeText(descriptionB) - msgBox.setStandardButtons(QtWidgets.QMessageBox.Ok | - QtWidgets.QMessageBox.Cancel) - msgBox.setDefaultButton(QtWidgets.QMessageBox.Cancel) - decision = msgBox.exec_() - return decision - - -class ClickableLineEdit(QtWidgets.QLineEdit): - - """subclass to allow for clickable lineEdit, as a button - - Attributes: - clicked (QtCore.Signal): emitted when clicked - """ - - clicked = QtCore.Signal(str) - - def mousePressEvent(self, event): - if event.button() == QtCore.Qt.LeftButton: - self.clicked.emit(self.text()) - else: - super(ClickableLineEdit, self).mousePressEvent(event) - - -class TabBar(QtWidgets.QTabBar): - """Subclass to get a taller tab widget, for readability - """ +class RBFTables(widget.RBFWidget): def __init__(self): - super(TabBar, self).__init__() - - def tabSizeHint(self, index): - width = QtWidgets.QTabBar.tabSizeHint(self, index).width() - return QtCore.QSize(width, 25) - + pass -class RBFWidget(MayaQWidgetDockableMixin, QtWidgets.QMainWindow): - def __init__(self, parent=pyqt.maya_main_window()): - super(RBFWidget, self).__init__(parent=parent) +class RBFMenuFunction: - # UI info ------------------------------------------------------------- - self.callBackID = None - self.setWindowTitle(TOOL_TITLE) - self.setObjectName(UI_NAME) - self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) - self.genericWidgetHight = 24 + def __init__(self, rbfInstance): + self.rbfInstance = rbfInstance - @staticmethod - def deleteAssociatedWidgetsMaya(widget, attrName="associatedMaya"): - """delete core ui items 'associated' with the provided widgets + def deleteSetup(self, setupName=None, allSetup=False): + """Delete all the nodes within a setup. Args: - widget (QWidget): Widget that has the associated attr set - attrName (str, optional): class attr to query + setupName (None, optional): Description """ - if hasattr(widget, attrName): - for t in getattr(widget, attrName): - try: - mc.deleteUI(t, ctl=True) - except Exception: - pass - else: - setattr(widget, attrName, []) + decision = widget.promptAcceptance(self.rbfInstance, + "Delete current Setup?", + "This will delete all RBF nodes in setup.") + if decision in [QtWidgets.QMessageBox.Discard, + QtWidgets.QMessageBox.Cancel]: + return - @staticmethod - def deleteAssociatedWidgets(widget, attrName="associated"): - """delete widget items 'associated' with the provided widgets + nodesToDelete = None + if setupName is None: + if not allSetup: + setupName, _ = self.rbfInstance.getSelectedSetup() + nodesToDelete = self.rbfInstance.allSetupsInfo.get(setupName, []) + else: + nodesToDelete = list(self.rbfInstance.allSetupsInfo.values()) + nodesToDelete = [item for sublist in nodesToDelete for item in sublist] - Args: - widget (QWidget): Widget that has the associated attr set - attrName (str, optional): class attr to query - """ - if hasattr(widget, attrName): - for t in getattr(widget, attrName): - try: - t.deleteLater() - except Exception: - pass - else: - setattr(widget, attrName, []) + for rbfNode in nodesToDelete: + drivenNode = rbfNode.getDrivenNode() + rbfNode.deleteRBFToggleAttr() + if drivenNode: + rbf_node.removeDrivenGroup(drivenNode[0]) + mc.delete(rbfNode.transformNode) + self.rbfInstance.refresh() - @staticmethod - def _associateRBFnodeAndWidget(tabDrivenWidget, rbfNode): - """associates the RBFNode with a widget for convenience when adding, - deleting, editing + def importNodes(self): + """import a setup(s) from file select by user - Args: - tabDrivenWidget (QWidget): tab widget - rbfNode (RBFNode): instance to be associated + Returns: + n/a: nada """ - setattr(tabDrivenWidget, "rbfNode", rbfNode) - - @staticmethod - def createCustomButton(label, size=(35, 27), icon=None, iconSize=None, tooltip=""): - stylesheet = ( - "QPushButton:pressed { background-color: #00A6F3;}" - ) - button = QtWidgets.QPushButton(label) - button.setMinimumSize(QtCore.QSize(*size)) - button.setStyleSheet(stylesheet) - button.setToolTip(tooltip) - button.setIcon(pyqt.get_icon(icon, iconSize)) - return button - - @staticmethod - def createSetupSelector2Widget(): - rbfVLayout = QtWidgets.QVBoxLayout() - rbfListWidget = QtWidgets.QListWidget() - rbfVLayout.addWidget(rbfListWidget) - return rbfVLayout, rbfListWidget + filePath = rbf_io.fileDialog(mode=1) + if filePath is None: + return + rbf_io.importRBFs(filePath) + mc.select(cl=True) + self.rbfInstance.refresh() + print("RBF setups imported: {}".format(filePath)) - @staticmethod - def labelListWidget(label, attrListType, horizontal=True): - """create the listAttribute that users can select their driver/driven - attributes for the setup + def exportNodes(self, allSetups=True): + """export all nodes or nodes from current setup Args: - label (str): to display above the listWidget - horizontal (bool, optional): should the label be above or infront - of the listWidget + allSetups (bool, optional): If all or setup Returns: - list: QLayout, QListWidget + n/a: nada """ - if horizontal: - attributeLayout = QtWidgets.QHBoxLayout() + # TODO WHEN NEW RBF NODE TYPES ARE ADDED, THIS WILL NEED TO BE RETOOLED + nodesToExport = [] + if allSetups: + [nodesToExport.extend(v) for k, v, + in self.rbfInstance.allSetupsInfo.items()] else: - attributeLayout = QtWidgets.QVBoxLayout() - attributeLabel = QtWidgets.QLabel(label) - attributeListWidget = QtWidgets.QListWidget() - attributeListWidget.setObjectName("{}ListWidget".format(attrListType)) - attributeLayout.addWidget(attributeLabel) - attributeLayout.addWidget(attributeListWidget) - return attributeLayout, attributeListWidget - - @staticmethod - def addRemoveButtonWidget(label1, label2, horizontal=True): - if horizontal: - addRemoveLayout = QtWidgets.QHBoxLayout() - else: - addRemoveLayout = QtWidgets.QVBoxLayout() - addAttributesButton = QtWidgets.QPushButton(label1) - removeAttributesButton = QtWidgets.QPushButton(label2) - addRemoveLayout.addWidget(addAttributesButton) - addRemoveLayout.addWidget(removeAttributesButton) - return addRemoveLayout, addAttributesButton, removeAttributesButton - - def selectNodeWidget(self, label, buttonLabel="Select"): - """create a lout with label, lineEdit, QPushbutton for user input - """ - stylesheet = ( - "QLineEdit { background-color: #404040;" - "border-radius: 4px;" - "border-color: #505050;" - "border-style: solid;" - "border-width: 1.4px;}" - ) - - nodeLayout = QtWidgets.QHBoxLayout() - nodeLayout.setSpacing(4) - - nodeLabel = QtWidgets.QLabel(label) - nodeLabel.setFixedWidth(40) - nodeLineEdit = ClickableLineEdit() - nodeLineEdit.setStyleSheet(stylesheet) - nodeLineEdit.setReadOnly(True) - nodeSelectButton = self.createCustomButton(buttonLabel) - nodeSelectButton.setFixedWidth(40) - nodeLineEdit.setFixedHeight(self.genericWidgetHight) - nodeSelectButton.setFixedHeight(self.genericWidgetHight) - nodeLayout.addWidget(nodeLabel) - nodeLayout.addWidget(nodeLineEdit, 1) - nodeLayout.addWidget(nodeSelectButton) - return nodeLayout, nodeLineEdit, nodeSelectButton - - def createSetupSelectorWidget(self): - """create the top portion of the weidget, select setup + refresh - - Returns: - list: QLayout, QCombobox, QPushButton - """ - setRBFLayout = QtWidgets.QHBoxLayout() - rbfLabel = QtWidgets.QLabel("Select RBF Setup:") - rbf_cbox = QtWidgets.QComboBox() - rbf_refreshButton = self.createCustomButton( - "", (35, 25), icon="mgear_refresh-cw", iconSize=16, tooltip="Refresh the UI" - ) - rbf_cbox.setFixedHeight(self.genericWidgetHight) - rbf_refreshButton.setMaximumWidth(80) - rbf_refreshButton.setFixedHeight(self.genericWidgetHight - 1) - setRBFLayout.addWidget(rbfLabel) - setRBFLayout.addWidget(rbf_cbox, 1) - setRBFLayout.addWidget(rbf_refreshButton) - return setRBFLayout, rbf_cbox, rbf_refreshButton - - def createDriverAttributeWidget(self): - """widget where the user inputs information for the setups - - Returns: - list: [of widgets] - """ - driverControlVLayout = QtWidgets.QVBoxLayout() - driverControlHLayout = QtWidgets.QHBoxLayout() - - # driverMainLayout.setStyleSheet("QVBoxLayout { background-color: #404040;") - driverControlHLayout.setSpacing(3) - # -------------------------------------------------------------------- - (controlLayout, - controlLineEdit, - setControlButton) = self.selectNodeWidget("Control", buttonLabel="Set") - controlLineEdit.setToolTip("The node driving the setup. (Click me!)") - # -------------------------------------------------------------------- - (driverLayout, - driverLineEdit, - driverSelectButton) = self.selectNodeWidget("Driver", buttonLabel="Set") - driverLineEdit.setToolTip("The node driving the setup. (Click me!)") - # -------------------------------------------------------------------- - allButton = self.createCustomButton("", (20, 52), tooltip="", icon="mgear_rewind", iconSize=15) - - (attributeLayout, attributeListWidget) = self.labelListWidget( - label="Select Driver Attributes:", attrListType="driver", horizontal=False) - - attributeListWidget.setToolTip("List of attributes driving setup.") - selType = QtWidgets.QAbstractItemView.ExtendedSelection - attributeListWidget.setSelectionMode(selType) - attributeListWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - # -------------------------------------------------------------------- - driverControlVLayout.addLayout(controlLayout, 0) - driverControlVLayout.addLayout(driverLayout, 0) - driverControlHLayout.addLayout(driverControlVLayout, 0) - driverControlHLayout.addWidget(allButton, 0) - return [controlLineEdit, - setControlButton, - driverLineEdit, - driverSelectButton, - allButton, - attributeListWidget, - attributeLayout, - driverControlHLayout] - - def createDrivenAttributeWidget(self): - """the widget that displays the driven information - - Returns: - list: [of widgets] - """ - drivenWidget = QtWidgets.QWidget() - drivenMainLayout = QtWidgets.QVBoxLayout() - drivenMainLayout.setContentsMargins(0, 10, 0, 2) - drivenMainLayout.setSpacing(9) - drivenSetLayout = QtWidgets.QVBoxLayout() - drivenMainLayout.addLayout(drivenSetLayout) - drivenWidget.setLayout(drivenMainLayout) - # -------------------------------------------------------------------- - (drivenLayout, - drivenLineEdit, - drivenSelectButton) = self.selectNodeWidget("Driven", buttonLabel="Set") - drivenTip = "The node being driven by setup. (Click me!)" - drivenLineEdit.setToolTip(drivenTip) - - addDrivenButton = self.createCustomButton("", (20, 25), icon="mgear_plus", iconSize=16, tooltip="") - addDrivenButton.setToolTip("Add a new driven to the current rbf node") - # -------------------------------------------------------------------- - (attributeLayout, - attributeListWidget) = self.labelListWidget(label="Select Driven Attributes:", - attrListType="driven", - horizontal=False) - attributeListWidget.setToolTip("Attributes being driven by setup.") - attributeLayout.setSpacing(1) - selType = QtWidgets.QAbstractItemView.ExtendedSelection - attributeListWidget.setSelectionMode(selType) - attributeListWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - # -------------------------------------------------------------------- - drivenSetLayout.addLayout(drivenLayout, 0) - drivenSetLayout.addLayout(attributeLayout, 0) - drivenLayout.addWidget(addDrivenButton) - drivenSetLayout.addLayout(drivenLayout, 0) - drivenSetLayout.addLayout(attributeLayout, 0) - drivenMainLayout.addLayout(drivenSetLayout) - drivenWidget.setLayout(drivenMainLayout) - return [drivenLineEdit, - drivenSelectButton, - addDrivenButton, - attributeListWidget, - drivenWidget, - drivenMainLayout] - - def createDrivenWidget(self): - """the widget that displays the driven information - - Returns: - list: [of widgets] - """ - drivenWidget = QtWidgets.QWidget() - drivenMainLayout = QtWidgets.QHBoxLayout() - drivenMainLayout.setContentsMargins(0, 0, 0, 0) - drivenMainLayout.setSpacing(9) - drivenWidget.setLayout(drivenMainLayout) - - tableWidget = self.createTableWidget() - drivenMainLayout.addWidget(tableWidget, 1) - return drivenWidget, tableWidget - - def createTableWidget(self): - """create table widget used to display poses, set tooltips and colum - - Returns: - QTableWidget: QTableWidget - """ - stylesheet = """ - QTableWidget QHeaderView::section { - background-color: #3a3b3b; - padding: 2px; - text-align: center; - } - QTableCornerButton::section { - background-color: #3a3b3b; - border: none; - } - """ - tableWidget = QtWidgets.QTableWidget() - tableWidget.insertColumn(0) - tableWidget.insertRow(0) - tableWidget.setHorizontalHeaderLabels(["Pose Value"]) - tableWidget.setVerticalHeaderLabels(["Pose #0"]) - # tableWidget.setStyleSheet(stylesheet) - tableTip = "Live connections to the RBF Node in your setup." - tableTip = tableTip + "\nSelect the desired Pose # to recall pose." - tableWidget.setToolTip(tableTip) - return tableWidget - - def createTabWidget(self): - """Tab widget to add driven widgets too. Custom TabBar so the tab is - easier to select - - Returns: - QTabWidget: - """ - tabLayout = QtWidgets.QTabWidget() - tabLayout.setContentsMargins(0, 0, 0, 0) - tabBar = TabBar() - tabLayout.setTabBar(tabBar) - tabBar.setTabsClosable(True) - return tabLayout - - def createOptionsButtonsWidget(self): - """add, edit, delete buttons for modifying rbf setups. - - Returns: - list: [QPushButtons] - """ - optionsLayout = QtWidgets.QHBoxLayout() - optionsLayout.setSpacing(5) - addTip = "After positioning all controls in the setup, add new pose." - addTip = addTip + "\nEnsure the driver node has a unique position." - addPoseButton = self.createCustomButton("Add Pose", size=(80, 26), tooltip=addTip) - EditPoseButton = self.createCustomButton("Update Pose", size=(80, 26)) - EditPoseButton.setToolTip("Recall pose, adjust controls and Edit.") - EditPoseValuesButton = self.createCustomButton("Update Pose Values", size=(80, 26)) - EditPoseValuesButton.setToolTip("Set pose based on values in table") - deletePoseButton = self.createCustomButton("Delete Pose", size=(80, 26)) - deletePoseButton.setToolTip("Recall pose, then Delete") - optionsLayout.addWidget(addPoseButton) - optionsLayout.addWidget(EditPoseButton) - optionsLayout.addWidget(EditPoseValuesButton) - optionsLayout.addWidget(deletePoseButton) - return (optionsLayout, - addPoseButton, - EditPoseButton, - EditPoseValuesButton, - deletePoseButton) - - def createDarkContainerWidget(self): - darkContainer = QtWidgets.QWidget() - driverMainLayout = QtWidgets.QVBoxLayout() - driverMainLayout.setContentsMargins(10, 10, 10, 10) # Adjust margins as needed - driverMainLayout.setSpacing(5) # Adjust spacing between widgets - - # Setting the dark color (Example: dark gray) - # darkContainer.setStyleSheet("background-color: #323232;") - - # Driver section - (self.controlLineEdit, - self.setControlButton, - self.driverLineEdit, - self.setDriverButton, - self.allButton, - self.driverAttributesWidget, - self.driverAttributesLayout, - driverControlLayout) = self.createDriverAttributeWidget() - - # Driven section - (self.drivenLineEdit, - self.setDrivenButton, - self.addDrivenButton, - self.drivenAttributesWidget, - self.drivenWidget, - self.drivenMainLayout) = self.createDrivenAttributeWidget() - - self.addRbfButton = self.createCustomButton("New RBF") - self.addRbfButton.setToolTip("Select node to be driven by setup.") - stylesheet = ( - "QPushButton {background-color: #179e83; border-radius: 4px;}" - "QPushButton:hover:!pressed { background-color: #2ea88f;}" - ) - self.addRbfButton.setStyleSheet(stylesheet) - - # Setting up the main layout for driver and driven sections - driverMainLayout = QtWidgets.QVBoxLayout() - driverMainLayout.addLayout(driverControlLayout) - driverMainLayout.addLayout(self.driverAttributesLayout) - driverMainLayout.addWidget(self.drivenWidget) - driverMainLayout.addWidget(self.addRbfButton) - darkContainer.setLayout(driverMainLayout) - - return darkContainer - - def createDriverDrivenTableWidget(self): - tableContainer = QtWidgets.QWidget() - - # Setting up the main layout for driver and driven sections - driverDrivenTableLayout = QtWidgets.QVBoxLayout() - self.driverPoseTableWidget = self.createTableWidget() - self.rbfTabWidget = self.createTabWidget() - - # Options buttons section - (optionsLayout, - self.addPoseButton, - self.editPoseButton, - self.editPoseValuesButton, - self.deletePoseButton) = self.createOptionsButtonsWidget() - self.addPoseButton.setEnabled(False) - self.editPoseButton.setEnabled(False) - self.editPoseValuesButton.setEnabled(False) - self.deletePoseButton.setEnabled(False) - - driverDrivenTableLayout.addWidget(self.driverPoseTableWidget, 1) - driverDrivenTableLayout.addWidget(self.rbfTabWidget, 1) - driverDrivenTableLayout.addLayout(optionsLayout) - tableContainer.setLayout(driverDrivenTableLayout) - - return tableContainer - - -class RBFTables(RBFWidget): - - def __init__(self): - pass + nodesToExport = self.rbfInstance.currentRBFSetupNodes + nodesToExport = [n.name for n in nodesToExport] + filePath = rbf_io.fileDialog(mode=0) + if filePath is None: + return + rbf_io.exportRBFs(nodesToExport, filePath) -class RBFManagerUI(RBFWidget): +class RBFManagerUI(widget.RBFWidget): """A manager for creating, mirroring, importing/exporting poses created for RBF type nodes. @@ -819,6 +329,8 @@ def __init__(self, hideMenuBar=False, newSceneCallBack=True): self.driverAutoAttr = [] self.drivenAutoAttr = [] + self.menuFunc = RBFMenuFunction(self) + self.setMenuBar(self.createMenuBar(hideMenuBar=hideMenuBar)) self.setCentralWidget(self.createCentralWidget()) self.centralWidget().setMouseTracking(True) @@ -906,33 +418,6 @@ def getDrivenNodesFromSetup(self): drivenNodes.extend(rbfNode.getDrivenNode) return drivenNodes - def __deleteSetup(self): - decision = promptAcceptance(self, - "Delete current Setup?", - "This will delete all RBF nodes in setup.") - if decision in [QtWidgets.QMessageBox.Discard, - QtWidgets.QMessageBox.Cancel]: - return - self.deleteSetup() - - def deleteSetup(self, setupName=None): - """Delete all the nodes within a setup. - - Args: - setupName (None, optional): Description - """ - setupType = None - if setupName is None: - setupName, setupType = self.getSelectedSetup() - nodesToDelete = self.allSetupsInfo.get(setupName, []) - for rbfNode in nodesToDelete: - drivenNode = rbfNode.getDrivenNode() - rbfNode.deleteRBFToggleAttr() - if drivenNode: - rbf_node.removeDrivenGroup(drivenNode[0]) - mc.delete(rbfNode.transformNode) - self.refresh() - def removeRBFFromSetup(self, drivenWidgetIndex): """remove RBF tab from setup. Delete driven group, attrs and clean up @@ -943,9 +428,9 @@ def removeRBFFromSetup(self, drivenWidgetIndex): Returns: n/a: n/a """ - decision = promptAcceptance(self, - "Are you sure you want to remove node?", - "This will delete the RBF & driven node.") + decision = widget.promptAcceptance(self, + "Are you sure you want to remove node?", + "This will delete the RBF & driven node.") if decision in [QtWidgets.QMessageBox.Discard, QtWidgets.QMessageBox.Cancel]: return @@ -991,7 +476,7 @@ def addRBFToSetup(self): if mc.objExists(drivenNode_name): if existing_rbf_setup(drivenNode_name): msg = "Node is already driven by an RBF Setup." - genericWarning(self, msg) + widget.genericWarning(self, msg) return setupName, rbfType = self.getSelectedSetup() @@ -1066,7 +551,7 @@ def preValidationCheck(self): # Check if the driven node is the same as the driver node if drivenNode == driverNode: - genericWarning(self, "Select Node to be driven!") + widget.genericWarning(self, "Select Node to be driven!") return result # Update the result dictionary with the fetched values @@ -1123,7 +608,7 @@ def deletePose(self): # TODO if one is allow syncing of nodes of different lengths # it should be done here if drivenRow != driverRow or drivenRow == -1: - genericWarning(self, "Select Pose # to be deleted.") + widget.genericWarning(self, "Select Pose # to be deleted.") return for rbfNode in self.currentRBFSetupNodes: @@ -1144,7 +629,7 @@ def editPose(self): drivenTableWidget = getattr(drivenWidget, "tableWidget") drivenRow = drivenTableWidget.currentRow() if drivenRow != driverRow or drivenRow == -1: - genericWarning(self, "Select Pose # to be Edited.") + widget.genericWarning(self, "Select Pose # to be Edited.") return driverNode = rbfNodes[0].getDriverNode()[0] driverAttrs = rbfNodes[0].getDriverNodeAttributes() @@ -1165,12 +650,12 @@ def editPoseValues(self): rbfNodes = self.currentRBFSetupNodes if not rbfNodes: return - driverRow = self.driverPoseTableWidget.currentRow() # A number + driverRow = self.driverPoseTableWidget.currentRow() # A number drivenWidget = self.rbfTabWidget.currentWidget() drivenTableWidget = getattr(drivenWidget, "tableWidget") drivenRow = drivenTableWidget.currentRow() if drivenRow != driverRow or drivenRow == -1: - genericWarning(self, "Select Pose # to be Edited.") + widget.genericWarning(self, "Select Pose # to be Edited.") return driverNode = rbfNodes[0].getDriverNode()[0] driverAttrs = rbfNodes[0].getDriverNodeAttributes() @@ -1432,7 +917,7 @@ def initializePoseControlWidgets(self): rbfAttrPlug = "{}.poses[{}].poseInput[{}]".format(rbfNode[0], rowIndex, columnIndex) # Create a control widget for this pose input attribute - getControlAttrWidget(rbfAttrPlug, label="") + widget.getControlAttrWidget(rbfAttrPlug, label="") def setDriverTable(self, rbfNode, weightInfo): """Set the driverTable widget with the information from the weightInfo @@ -1470,7 +955,15 @@ def setDriverTable(self, rbfNode, weightInfo): for rowIndex, poseInput in enumerate(poses["poseInput"]): for columnIndex, _ in enumerate(poseInput): rbfAttrPlug = "{}.poses[{}].poseInput[{}]".format(rbfNode, rowIndex, columnIndex) - attrEdit, mAttrField = getControlAttrWidget(rbfAttrPlug, label="") + attrEdit, mAttrField = widget.getControlAttrWidget(rbfAttrPlug, label="") + + # Get the current width of the column after resizing to contents + self.driverPoseTableWidget.resizeColumnToContents(columnIndex) + currentWidth = self.driverPoseTableWidget.columnWidth(columnIndex) + + # Add some extra space (e.g., 10 pixels) + extraSpace = 20 + self.driverPoseTableWidget.setColumnWidth(columnIndex, currentWidth + extraSpace) self.driverPoseTableWidget.setCellWidget(rowIndex, columnIndex, attrEdit) attrEdit.returnPressed.connect( @@ -1582,9 +1075,17 @@ def setDrivenTable(drivenWidget, rbfNode, weightInfo): for rowIndex, poseInput in enumerate(poses["poseValue"]): for columnIndex, pValue in enumerate(poseInput): rbfAttrPlug = "{}.poses[{}].poseValue[{}]".format(rbfNode, rowIndex, columnIndex) - attrEdit, mAttrFeild = getControlAttrWidget(rbfAttrPlug, label="") + attrEdit, mAttrFeild = widget.getControlAttrWidget(rbfAttrPlug, label="") drivenWidget.tableWidget.setCellWidget(rowIndex, columnIndex, attrEdit) + # Get the current width of the column after resizing to contents + drivenWidget.tableWidget.resizeColumnToContents(columnIndex) + currentWidth = drivenWidget.tableWidget.columnWidth(columnIndex) + + # Add some extra space (e.g., 10 pixels) + extraSpace = 20 + drivenWidget.tableWidget.setColumnWidth(columnIndex, currentWidth + extraSpace) + # Adding QTableWidgetItem for the cell and setting the value tableItem = QtWidgets.QTableWidgetItem() tableItem.setFlags(tableItem.flags() | QtCore.Qt.ItemIsEditable) @@ -1636,7 +1137,7 @@ def addNewDriven(self): drivenSelection=True, currentRBFSetupNodes=False, clearDrivenTab=False - ) + ) self.setDrivenButton.blockSignals(False) self.drivenAttributesWidget.setEnabled(True) @@ -1908,13 +1409,31 @@ def setSetupDriverControl(self, lineEditWidget): elif self.currentRBFSetupNodes: textA = "Do you want to change the Control for setup?" textB = "This Control that will be used for recalling poses." - decision = promptAcceptance(self, textA, textB) + decision = widget.promptAcceptance(self, textA, textB) if decision in [QtWidgets.QMessageBox.Discard, QtWidgets.QMessageBox.Cancel]: return controlName = self.setNodeToField(lineEditWidget) self.setDriverControlOnSetup(controlName) + def setDrivenInfo(self, tabIndex): + self.refresh(rbfSelection=False, + driverSelection=False, + drivenSelection=True, + currentRBFSetupNodes=False, + clearDrivenTab=False) + + tabWidget = self.rbfTabWidget.widget(tabIndex) + rbfNode = getattr(tabWidget, "rbfNode") + weightInfo = rbfNode.getNodeInfo() + + # Set Driven Node and Attributes + drivenNode = weightInfo.get("drivenNode", [None])[0] + self.drivenLineEdit.setText(drivenNode or "") + self.setAttributeDisplay(self.drivenAttributesWidget, drivenNode or "", weightInfo["drivenAttrs"]) + + self.addPoseButton.setEnabled(True) + @staticmethod def getRBFNodesInfo(rbfNodes): """create a dictionary of all the RBFInfo(referred to as @@ -1931,47 +1450,6 @@ def getRBFNodesInfo(rbfNodes): weightNodeInfo_dict[rbf.name] = rbf.getNodeInfo() return weightNodeInfo_dict - def importNodes(self): - """import a setup(s) from file select by user - - Returns: - n/a: nada - """ - # sceneFilePath = mc.file(sn=True, q=True) - # startDir = os.path.dirname(sceneFilePath) - filePath = rbf_io.fileDialog(mode=1) - if filePath is None: - return - rbf_io.importRBFs(filePath) - mc.select(cl=True) - self.refresh() - print("RBF setups imported: {}".format(filePath)) - - def exportNodes(self, allSetups=True): - """export all nodes or nodes from current setup - - Args: - allSetups (bool, optional): If all or setup - - Returns: - n/a: nada - """ - # TODO WHEN NEW RBF NODE TYPES ARE ADDED, THIS WILL NEED TO BE RETOOLED - nodesToExport = [] - if allSetups: - [nodesToExport.extend(v) for k, v, - in self.allSetupsInfo.items()] - else: - nodesToExport = self.currentRBFSetupNodes - - nodesToExport = [n.name for n in nodesToExport] - # sceneFilePath = mc.file(sn=True, q=True) - # startDir = os.path.dirname(sceneFilePath) - filePath = rbf_io.fileDialog(mode=0) - if filePath is None: - return - rbf_io.exportRBFs(nodesToExport, filePath) - @staticmethod def gatherMirroredInfo(rbfNodes): """gather all the info from the provided nodes and string replace @@ -2002,7 +1480,7 @@ def gatherMirroredInfo(rbfNodes): # setupName ------------------------------------------------------- mrSetupName = mString.convertRLName(weightInfo["setupName"]) if mrSetupName == weightInfo["setupName"]: - mrSetupName = "{}{}".format(mrSetupName, MIRROR_SUFFIX) + mrSetupName = "{}{}".format(mrSetupName, widget.MIRROR_SUFFIX) weightInfo["setupName"] = mrSetupName # transformNode --------------------------------------------------- tmp = weightInfo["transformNode"]["name"] @@ -2077,7 +1555,7 @@ def mirrorSetup(self): mrData = [] for srcNode, dstValues in setupTargetInfo_dict.items(): mrData.extend(anim_utils.calculateMirrorDataRBF(srcNode, - dstValues[0])) + dstValues[0])) for entry in mrData: anim_utils.applyMirror(nameSpace, entry) poseInputs = rbf_node.getMultipleAttrs(mrDriverNode, mrDriverAttrs) @@ -2166,6 +1644,7 @@ def connectSignals(self): # Driver Line Edit and Control Line Edit self.driverLineEdit.clicked.connect(selectNode) self.controlLineEdit.clicked.connect(selectNode) + self.drivenLineEdit.clicked.connect(selectNode) self.driverLineEdit.textChanged.connect(partial(self.updateAttributeDisplay, self.driverAttributesWidget)) self.drivenLineEdit.textChanged.connect(partial(self.updateAttributeDisplay, @@ -2210,6 +1689,7 @@ def connectSignals(self): tabBar = self.rbfTabWidget.tabBar() tabBar.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) tabBar.customContextMenuRequested.connect(self.tabConextMenu) + tabBar.tabBarClicked.connect(self.setDrivenInfo) tabBar.tabCloseRequested.connect(self.removeRBFFromSetup) # main assebly ------------------------------------------------------------ @@ -2236,15 +1716,15 @@ def createCentralWidget(self): allTableWidget = self.createDriverDrivenTableWidget() centralWidgetLayout.addLayout(rbfLayout) - centralWidgetLayout.addWidget(HLine()) + centralWidgetLayout.addWidget(widget.HLine()) splitter.addWidget(driverDrivenWidget) splitter.addWidget(allTableWidget) centralWidgetLayout.addWidget(splitter) # Assuming a ratio of 2:1 for settingWidth to tableWidth totalWidth = splitter.width() - attributeWidth = (1/3) * totalWidth - tableWidth = (2/3) * totalWidth + attributeWidth = (1 / 3) * totalWidth + tableWidth = (2 / 3) * totalWidth splitter.setSizes([int(attributeWidth), int(tableWidth)]) return centralWidget @@ -2262,12 +1742,12 @@ def createMenuBar(self, hideMenuBar=False): file = mainMenuBar.addMenu("File") menu1 = file.addAction("Re-evaluate Nodes", self.reevalluateAllNodes) menu1.setToolTip("Force all RBF nodes to re-revaluate.") - file.addAction("Export All", self.exportNodes) - file.addAction("Export current setup", partial(self.exportNodes, - allSetups=False)) - file.addAction("Import RBFs", partial(self.importNodes)) file.addSeparator() - file.addAction("Delete Current Setup", self.__deleteSetup) + file.addAction("Import RBFs", partial(self.menuFunc.importNodes)) + file.addAction("Export RBFs", self.menuFunc.exportNodes) + file.addSeparator() + file.addAction("Delete Current Setup", partial(self.menuFunc.deleteSetup, allSetup=False)) + file.addAction("Delete All Setup", partial(self.menuFunc.deleteSetup, allSetup=True)) # mirror -------------------------------------------------------------- mirrorMenu = mainMenuBar.addMenu("Mirror") mirrorMenu1 = mirrorMenu.addAction("Mirror Setup", self.mirrorSetup) @@ -2317,4 +1797,3 @@ def mouseMoveEvent(self, event): if event.buttons() == QtCore.Qt.NoButton: pos = event.pos() self.mousePosition.emit(pos.x(), pos.y()) - diff --git a/release/scripts/mgear/rigbits/rbf_manager2/widget.py b/release/scripts/mgear/rigbits/rbf_manager2/widget.py new file mode 100644 index 00000000..b9ea79ef --- /dev/null +++ b/release/scripts/mgear/rigbits/rbf_manager2/widget.py @@ -0,0 +1,560 @@ +# core +import maya.cmds as mc +import maya.OpenMayaUI as mui + +# mgear +import mgear +from mgear.core import pyqt +from mgear.vendor.Qt import QtWidgets, QtCore, QtCompat, QtGui +from maya.app.general.mayaMixin import MayaQWidgetDockableMixin +from mgear.rigbits.six import PY2 + +__version__ = "2.0.0" +TOOL_NAME = "RBF Manager" +TOOL_TITLE = "{} v{} | mGear {}".format(TOOL_NAME, __version__, mgear.getVersion()) +UI_NAME = "RBFManagerUI" +WORK_SPACE_NAME = UI_NAME + "WorkspaceControl" + +MGEAR_EXTRA_ENVIRON = "MGEAR_RBF_EXTRA" +EXTRA_MODULE_DICT = "extraFunc_dict" + +MIRROR_SUFFIX = "_mr" + + +class ClickableLineEdit(QtWidgets.QLineEdit): + """subclass to allow for clickable lineEdit, as a button + + Attributes: + clicked (QtCore.Signal): emitted when clicked + """ + + clicked = QtCore.Signal(str) + + def mousePressEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + self.clicked.emit(self.text()) + else: + super(ClickableLineEdit, self).mousePressEvent(event) + + +class TabBar(QtWidgets.QTabBar): + """Subclass to get a taller tab widget, for readability + """ + + def __init__(self): + super(TabBar, self).__init__() + + def tabSizeHint(self, index): + width = QtWidgets.QTabBar.tabSizeHint(self, index).width() + return QtCore.QSize(width, 25) + + +class RBFWidget(MayaQWidgetDockableMixin, QtWidgets.QMainWindow): + + def __init__(self, parent=pyqt.maya_main_window()): + super(RBFWidget, self).__init__(parent=parent) + + # UI info ------------------------------------------------------------- + self.callBackID = None + self.setWindowTitle(TOOL_TITLE) + self.setObjectName(UI_NAME) + self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) + self.genericWidgetHight = 24 + + @staticmethod + def deleteAssociatedWidgetsMaya(widget, attrName="associatedMaya"): + """delete core ui items 'associated' with the provided widgets + + Args: + widget (QWidget): Widget that has the associated attr set + attrName (str, optional): class attr to query + """ + if hasattr(widget, attrName): + for t in getattr(widget, attrName): + try: + mc.deleteUI(t, ctl=True) + except Exception: + pass + else: + setattr(widget, attrName, []) + + @staticmethod + def deleteAssociatedWidgets(widget, attrName="associated"): + """delete widget items 'associated' with the provided widgets + + Args: + widget (QWidget): Widget that has the associated attr set + attrName (str, optional): class attr to query + """ + if hasattr(widget, attrName): + for t in getattr(widget, attrName): + try: + t.deleteLater() + except Exception: + pass + else: + setattr(widget, attrName, []) + + @staticmethod + def _associateRBFnodeAndWidget(tabDrivenWidget, rbfNode): + """associates the RBFNode with a widget for convenience when adding, + deleting, editing + + Args: + tabDrivenWidget (QWidget): tab widget + rbfNode (RBFNode): instance to be associated + """ + setattr(tabDrivenWidget, "rbfNode", rbfNode) + + @staticmethod + def createCustomButton(label, size=(35, 27), icon=None, iconSize=None, tooltip=""): + stylesheet = ( + "QPushButton {background-color: #5D5D5D; border-radius: 4px;}" + "QPushButton:pressed { background-color: #00A6F3;}" + "QPushButton:hover:!pressed { background-color: #707070;}" + ) + button = QtWidgets.QPushButton(label) + button.setMinimumSize(QtCore.QSize(*size)) + button.setStyleSheet(stylesheet) + button.setToolTip(tooltip) + button.setIcon(pyqt.get_icon(icon, iconSize)) + return button + + @staticmethod + def createSetupSelector2Widget(): + rbfVLayout = QtWidgets.QVBoxLayout() + rbfListWidget = QtWidgets.QListWidget() + rbfVLayout.addWidget(rbfListWidget) + return rbfVLayout, rbfListWidget + + @staticmethod + def labelListWidget(label, attrListType, horizontal=True): + """create the listAttribute that users can select their driver/driven + attributes for the setup + + Args: + label (str): to display above the listWidget + horizontal (bool, optional): should the label be above or infront + of the listWidget + + Returns: + list: QLayout, QListWidget + """ + if horizontal: + attributeLayout = QtWidgets.QHBoxLayout() + else: + attributeLayout = QtWidgets.QVBoxLayout() + attributeLabel = QtWidgets.QLabel(label) + attributeListWidget = QtWidgets.QListWidget() + attributeListWidget.setObjectName("{}ListWidget".format(attrListType)) + attributeLayout.addWidget(attributeLabel) + attributeLayout.addWidget(attributeListWidget) + return attributeLayout, attributeListWidget + + @staticmethod + def addRemoveButtonWidget(label1, label2, horizontal=True): + if horizontal: + addRemoveLayout = QtWidgets.QHBoxLayout() + else: + addRemoveLayout = QtWidgets.QVBoxLayout() + addAttributesButton = QtWidgets.QPushButton(label1) + removeAttributesButton = QtWidgets.QPushButton(label2) + addRemoveLayout.addWidget(addAttributesButton) + addRemoveLayout.addWidget(removeAttributesButton) + return addRemoveLayout, addAttributesButton, removeAttributesButton + + def selectNodeWidget(self, label, buttonLabel="Select"): + """create a lout with label, lineEdit, QPushbutton for user input + """ + stylesheet = ( + "QLineEdit { background-color: #404040;" + "border-radius: 4px;" + "border-color: #505050;" + "border-style: solid;" + "border-width: 1.4px;}" + ) + + nodeLayout = QtWidgets.QHBoxLayout() + nodeLayout.setSpacing(4) + + nodeLabel = QtWidgets.QLabel(label) + nodeLabel.setFixedWidth(40) + nodeLineEdit = ClickableLineEdit() + nodeLineEdit.setStyleSheet(stylesheet) + nodeLineEdit.setReadOnly(True) + nodeSelectButton = self.createCustomButton(buttonLabel) + nodeSelectButton.setFixedWidth(40) + nodeLineEdit.setFixedHeight(self.genericWidgetHight) + nodeSelectButton.setFixedHeight(self.genericWidgetHight) + nodeLayout.addWidget(nodeLabel) + nodeLayout.addWidget(nodeLineEdit, 1) + nodeLayout.addWidget(nodeSelectButton) + return nodeLayout, nodeLineEdit, nodeSelectButton + + def createSetupSelectorWidget(self): + """create the top portion of the weidget, select setup + refresh + + Returns: + list: QLayout, QCombobox, QPushButton + """ + setRBFLayout = QtWidgets.QHBoxLayout() + rbfLabel = QtWidgets.QLabel("Select RBF Setup:") + rbf_cbox = QtWidgets.QComboBox() + rbf_refreshButton = self.createCustomButton( + "", (35, 25), icon="mgear_refresh-cw", iconSize=16, tooltip="Refresh the UI" + ) + rbf_cbox.setFixedHeight(self.genericWidgetHight) + rbf_refreshButton.setMaximumWidth(80) + rbf_refreshButton.setFixedHeight(self.genericWidgetHight - 1) + setRBFLayout.addWidget(rbfLabel) + setRBFLayout.addWidget(rbf_cbox, 1) + setRBFLayout.addWidget(rbf_refreshButton) + return setRBFLayout, rbf_cbox, rbf_refreshButton + + def createDriverAttributeWidget(self): + """widget where the user inputs information for the setups + + Returns: + list: [of widgets] + """ + driverControlVLayout = QtWidgets.QVBoxLayout() + driverControlHLayout = QtWidgets.QHBoxLayout() + + # driverMainLayout.setStyleSheet("QVBoxLayout { background-color: #404040;") + driverControlHLayout.setSpacing(3) + # -------------------------------------------------------------------- + (controlLayout, + controlLineEdit, + setControlButton) = self.selectNodeWidget("Control", buttonLabel="Set") + controlLineEdit.setToolTip("The node driving the setup. (Click me!)") + # -------------------------------------------------------------------- + (driverLayout, + driverLineEdit, + driverSelectButton) = self.selectNodeWidget("Driver", buttonLabel="Set") + driverLineEdit.setToolTip("The node driving the setup. (Click me!)") + # -------------------------------------------------------------------- + tooltipMsg = "Set Control and Driver : Select exactly two objects.\n" \ + "The first selected object will be set as the Control, and the second as the Driver." + allButton = self.createCustomButton("", (20, 52), tooltip=tooltipMsg, icon="mgear_rewind", iconSize=15) + + (attributeLayout, attributeListWidget) = self.labelListWidget( + label="Select Driver Attributes:", attrListType="driver", horizontal=False) + + attributeListWidget.setToolTip("List of attributes driving setup.") + selType = QtWidgets.QAbstractItemView.ExtendedSelection + attributeListWidget.setSelectionMode(selType) + attributeListWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + # -------------------------------------------------------------------- + driverControlVLayout.addLayout(controlLayout, 0) + driverControlVLayout.addLayout(driverLayout, 0) + driverControlHLayout.addLayout(driverControlVLayout, 0) + driverControlHLayout.addWidget(allButton, 0) + return [controlLineEdit, + setControlButton, + driverLineEdit, + driverSelectButton, + allButton, + attributeListWidget, + attributeLayout, + driverControlHLayout] + + def createDrivenAttributeWidget(self): + """the widget that displays the driven information + + Returns: + list: [of widgets] + """ + drivenWidget = QtWidgets.QWidget() + drivenMainLayout = QtWidgets.QVBoxLayout() + drivenMainLayout.setContentsMargins(0, 10, 0, 2) + drivenMainLayout.setSpacing(9) + drivenSetLayout = QtWidgets.QVBoxLayout() + drivenMainLayout.addLayout(drivenSetLayout) + drivenWidget.setLayout(drivenMainLayout) + # -------------------------------------------------------------------- + (drivenLayout, + drivenLineEdit, + drivenSelectButton) = self.selectNodeWidget("Driven", buttonLabel="Set") + drivenTip = "The node being driven by setup. (Click me!)" + drivenLineEdit.setToolTip(drivenTip) + + addDrivenButton = self.createCustomButton("", (20, 25), icon="mgear_plus", iconSize=16, tooltip="") + addDrivenButton.setToolTip("Add a new driven to the current rbf node") + # -------------------------------------------------------------------- + (attributeLayout, + attributeListWidget) = self.labelListWidget(label="Select Driven Attributes:", + attrListType="driven", + horizontal=False) + attributeListWidget.setToolTip("Attributes being driven by setup.") + attributeLayout.setSpacing(1) + selType = QtWidgets.QAbstractItemView.ExtendedSelection + attributeListWidget.setSelectionMode(selType) + attributeListWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + # -------------------------------------------------------------------- + drivenSetLayout.addLayout(drivenLayout, 0) + drivenSetLayout.addLayout(attributeLayout, 0) + drivenLayout.addWidget(addDrivenButton) + drivenSetLayout.addLayout(drivenLayout, 0) + drivenSetLayout.addLayout(attributeLayout, 0) + drivenMainLayout.addLayout(drivenSetLayout) + drivenWidget.setLayout(drivenMainLayout) + return [drivenLineEdit, + drivenSelectButton, + addDrivenButton, + attributeListWidget, + drivenWidget, + drivenMainLayout] + + def createDrivenWidget(self): + """the widget that displays the driven information + + Returns: + list: [of widgets] + """ + drivenWidget = QtWidgets.QWidget() + drivenMainLayout = QtWidgets.QHBoxLayout() + drivenMainLayout.setContentsMargins(0, 0, 0, 0) + drivenMainLayout.setSpacing(9) + drivenWidget.setLayout(drivenMainLayout) + + tableWidget = self.createTableWidget() + drivenMainLayout.addWidget(tableWidget, 1) + return drivenWidget, tableWidget + + def createTableWidget(self): + """create table widget used to display poses, set tooltips and colum + + Returns: + QTableWidget: QTableWidget + """ + stylesheet = """ + QTableWidget QHeaderView::section { + background-color: #3a3b3b; + padding: 2px; + text-align: center; + } + QTableCornerButton::section { + background-color: #3a3b3b; + border: none; + } + """ + tableWidget = QtWidgets.QTableWidget() + tableWidget.insertColumn(0) + tableWidget.insertRow(0) + tableWidget.setHorizontalHeaderLabels(["Pose Value"]) + tableWidget.setVerticalHeaderLabels(["Pose #0"]) + + # tableWidget.setStyleSheet(stylesheet) + tableTip = "Live connections to the RBF Node in your setup." + tableTip = tableTip + "\nSelect the desired Pose # to recall pose." + tableWidget.setToolTip(tableTip) + return tableWidget + + def createTabWidget(self): + """Tab widget to add driven widgets too. Custom TabBar so the tab is + easier to select + + Returns: + QTabWidget: + """ + tabLayout = QtWidgets.QTabWidget() + tabLayout.setContentsMargins(0, 0, 0, 0) + tabBar = TabBar() + tabLayout.setTabBar(tabBar) + tabBar.setTabsClosable(True) + return tabLayout + + def createOptionsButtonsWidget(self): + """add, edit, delete buttons for modifying rbf setups. + + Returns: + list: [QPushButtons] + """ + optionsLayout = QtWidgets.QHBoxLayout() + optionsLayout.setSpacing(5) + addTip = "After positioning all controls in the setup, add new pose." + addTip = addTip + "\nEnsure the driver node has a unique position." + addPoseButton = self.createCustomButton("Add Pose", size=(80, 26), tooltip=addTip) + EditPoseButton = self.createCustomButton("Update Pose", size=(80, 26)) + EditPoseButton.setToolTip("Recall pose, adjust controls and Edit.") + EditPoseValuesButton = self.createCustomButton("Update Pose Values", size=(80, 26)) + EditPoseValuesButton.setToolTip("Set pose based on values in table") + deletePoseButton = self.createCustomButton("Delete Pose", size=(80, 26)) + deletePoseButton.setToolTip("Recall pose, then Delete") + optionsLayout.addWidget(addPoseButton) + optionsLayout.addWidget(EditPoseButton) + optionsLayout.addWidget(EditPoseValuesButton) + optionsLayout.addWidget(deletePoseButton) + return (optionsLayout, + addPoseButton, + EditPoseButton, + EditPoseValuesButton, + deletePoseButton) + + def createDarkContainerWidget(self): + darkContainer = QtWidgets.QWidget() + driverMainLayout = QtWidgets.QVBoxLayout() + driverMainLayout.setContentsMargins(10, 10, 10, 10) # Adjust margins as needed + driverMainLayout.setSpacing(5) # Adjust spacing between widgets + + # Setting the dark color (Example: dark gray) + # darkContainer.setStyleSheet("background-color: #323232;") + + # Driver section + (self.controlLineEdit, + self.setControlButton, + self.driverLineEdit, + self.setDriverButton, + self.allButton, + self.driverAttributesWidget, + self.driverAttributesLayout, + driverControlLayout) = self.createDriverAttributeWidget() + + # Driven section + (self.drivenLineEdit, + self.setDrivenButton, + self.addDrivenButton, + self.drivenAttributesWidget, + self.drivenWidget, + self.drivenMainLayout) = self.createDrivenAttributeWidget() + + self.addRbfButton = self.createCustomButton("New RBF") + self.addRbfButton.setToolTip("Select node to be driven by setup.") + stylesheet = ( + "QPushButton {background-color: #179e83; border-radius: 4px;}" + "QPushButton:hover:!pressed { background-color: #2ea88f;}" + ) + self.addRbfButton.setStyleSheet(stylesheet) + + # Setting up the main layout for driver and driven sections + driverMainLayout = QtWidgets.QVBoxLayout() + driverMainLayout.addLayout(driverControlLayout) + driverMainLayout.addLayout(self.driverAttributesLayout) + driverMainLayout.addWidget(self.drivenWidget) + driverMainLayout.addWidget(self.addRbfButton) + darkContainer.setLayout(driverMainLayout) + + return darkContainer + + def createDriverDrivenTableWidget(self): + tableContainer = QtWidgets.QWidget() + + # Setting up the main layout for driver and driven sections + driverDrivenTableLayout = QtWidgets.QVBoxLayout() + self.driverPoseTableWidget = self.createTableWidget() + self.rbfTabWidget = self.createTabWidget() + + # Options buttons section + (optionsLayout, + self.addPoseButton, + self.editPoseButton, + self.editPoseValuesButton, + self.deletePoseButton) = self.createOptionsButtonsWidget() + self.addPoseButton.setEnabled(False) + self.editPoseButton.setEnabled(False) + self.editPoseValuesButton.setEnabled(False) + self.deletePoseButton.setEnabled(False) + + driverDrivenTableLayout.addWidget(self.driverPoseTableWidget, 1) + driverDrivenTableLayout.addWidget(self.rbfTabWidget, 1) + driverDrivenTableLayout.addLayout(optionsLayout) + tableContainer.setLayout(driverDrivenTableLayout) + + return tableContainer + + +# ============================================================================= +# UI General Functions +# ============================================================================= + +def getControlAttrWidget(nodeAttr, label=""): + """Create a cmds.attrControlGrp and wrap it in a qtWidget, preserving its connection + to the specified attr + + Args: + nodeAttr (str): node.attr, the target for the attrControlGrp + label (str, optional): name for the attr widget + + Returns: + QtWidget.QLineEdit: A Qt widget created from attrControlGrp + str: The name of the created Maya attrControlGrp + """ + mAttrFeild = mc.attrControlGrp(attribute=nodeAttr, label=label, po=True) + + # Convert the Maya control to a Qt pointer + ptr = mui.MQtUtil.findControl(mAttrFeild) + + # Wrap the Maya control into a Qt widget, considering Python version + controlWidget = QtCompat.wrapInstance(long(ptr) if PY2 else int(ptr), base=QtWidgets.QWidget) + controlWidget.setContentsMargins(0, 0, 0, 0) + controlWidget.setMinimumWidth(0) + + attrEdit = [wdgt for wdgt in controlWidget.children() if type(wdgt) == QtWidgets.QLineEdit] + [wdgt.setParent(attrEdit[0]) for wdgt in controlWidget.children() + if type(wdgt) == QtCore.QObject] + + attrEdit[0].setParent(None) + controlWidget.setParent(attrEdit[0]) + controlWidget.setHidden(True) + return attrEdit[0], mAttrFeild + + +def HLine(): + """seporator line for widgets + + Returns: + Qframe: line for seperating UI elements visually + """ + seperatorLine = QtWidgets.QFrame() + seperatorLine.setFrameShape(QtWidgets.QFrame.HLine) + seperatorLine.setFrameShadow(QtWidgets.QFrame.Sunken) + return seperatorLine + + +def VLine(): + """seporator line for widgets + + Returns: + Qframe: line for seperating UI elements visually + """ + seperatorLine = QtWidgets.QFrame() + seperatorLine.setFrameShape(QtWidgets.QFrame.VLine) + seperatorLine.setFrameShadow(QtWidgets.QFrame.Sunken) + return seperatorLine + + +def genericWarning(parent, warningText): + """generic prompt warning with the provided text + + Args: + parent (QWidget): Qwidget to be parented under + warningText (str): information to display to the user + + Returns: + QtCore.Response: of what the user chose. For warnings + """ + selWarning = QtWidgets.QMessageBox(parent) + selWarning.setText(warningText) + results = selWarning.exec_() + return results + + +def promptAcceptance(parent, descriptionA, descriptionB): + """Warn user, asking for permission + + Args: + parent (QWidget): to be parented under + descriptionA (str): info + descriptionB (str): further info + + Returns: + QtCore.Response: accept, deline, reject + """ + msgBox = QtWidgets.QMessageBox(parent) + msgBox.setText(descriptionA) + msgBox.setInformativeText(descriptionB) + msgBox.setStandardButtons(QtWidgets.QMessageBox.Ok | + QtWidgets.QMessageBox.Cancel) + msgBox.setDefaultButton(QtWidgets.QMessageBox.Cancel) + decision = msgBox.exec_() + return decision