From fb1f3d26f10e475cd11304a5ff0809c80c5bd92b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Sep 2020 19:18:06 +0200 Subject: [PATCH 1/4] Fix #237 - Updating a look where the shader name changed Added cleanup of references with failed reference edits --- pype/plugins/maya/load/load_look.py | 52 +++++++++++++++++++-- pype/widgets/message_window.py | 71 ++++++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 4 deletions(-) diff --git a/pype/plugins/maya/load/load_look.py b/pype/plugins/maya/load/load_look.py index b9c0d81104..d82978b1a1 100644 --- a/pype/plugins/maya/load/load_look.py +++ b/pype/plugins/maya/load/load_look.py @@ -3,6 +3,8 @@ from avalon import api, io import json import pype.hosts.maya.lib from collections import defaultdict +from pype.widgets.message_window import ScrollMessageBox +from Qt import QtWidgets class LookLoader(pype.hosts.maya.plugin.ReferenceLoader): @@ -44,12 +46,24 @@ class LookLoader(pype.hosts.maya.plugin.ReferenceLoader): self.update(container, representation) def update(self, container, representation): + """ + Called by Scene Inventory when look should be updated to current + version. + If any reference edits cannot be applied, eg. shader renamed and + material not present, reference is unloaded and cleaned. + All failed edits are highlighted to the user via message box. + Args: + container: object that has look to be updated + representation: (dict): relationship data to get proper + representation from DB and persisted + data in .json + Returns: + None + """ import os from maya import cmds - node = container["objectName"] - path = api.get_representation_path(representation) # Get reference node from container members @@ -127,13 +141,45 @@ class LookLoader(pype.hosts.maya.plugin.ReferenceLoader): with open(shader_relation, "r") as f: relationships = json.load(f) + # update of reference could result in failed edits - material is not + # present because of renaming etc. + failed_edits = cmds.referenceQuery(reference_node, + editStrings=True, + failedEdits=True, + successfulEdits=False) + + # highlight failed edits to user + if failed_edits: + shader_data = relationships.get("relationships", {}) + for rel in shader_data.values(): + for member in rel["members"]: + nodes.add(member['name']) + + # clean references - removes failed reference edits + cmds.file(unloadReference=reference_node) + cmds.file(cr=reference_node) # cleanReference + cmds.file(loadReference=reference_node) + + # reapply shading groups from json representation + pype.hosts.maya.lib.apply_shaders(relationships, + shader_nodes, + nodes) + + msg = ["During reference update some edits failed.", + "All successful edits were kept intact.\n", + "Failed and removed edits:"] + msg.extend(failed_edits) + msg = ScrollMessageBox(QtWidgets.QMessageBox.Warning, + "Some reference edit failed", + msg) + msg.exec_() + attributes = relationships.get("attributes", []) # region compute lookup nodes_by_id = defaultdict(list) for n in nodes: nodes_by_id[pype.hosts.maya.lib.get_id(n)].append(n) - pype.hosts.maya.lib.apply_attributes(attributes, nodes_by_id) # Update metadata diff --git a/pype/widgets/message_window.py b/pype/widgets/message_window.py index 41c709b933..f909c60710 100644 --- a/pype/widgets/message_window.py +++ b/pype/widgets/message_window.py @@ -1,4 +1,4 @@ -from Qt import QtWidgets +from Qt import QtWidgets, QtCore import sys import logging @@ -49,6 +49,17 @@ class Window(QtWidgets.QWidget): def message(title=None, message=None, level="info", parent=None): + """ + Produces centered dialog with specific level denoting severity + Args: + title: (string) dialog title + message: (string) message + level: (string) info|warning|critical + parent: (QtWidgets.QApplication) + + Returns: + None + """ app = parent if not app: app = QtWidgets.QApplication(sys.argv) @@ -68,3 +79,61 @@ def message(title=None, message=None, level="info", parent=None): # skip all possible issues that may happen feature is not crutial log.warning("Couldn't center message.", exc_info=True) # sys.exit(app.exec_()) + + +class ScrollMessageBox(QtWidgets.QDialog): + """ + Basic version of scrollable QMessageBox. No other existing dialog + implementation is scrollable. + Args: + icon: + title: + messages: of messages + cancelable: - True if Cancel button should be added + """ + def __init__(self, icon, title, messages, cancelable=False, + *args, **kwargs): + super(ScrollMessageBox, self).__init__() + self.setWindowTitle(title) + self.icon = icon + + self.setWindowFlags(QtCore.Qt.WindowTitleHint) + + layout = QtWidgets.QVBoxLayout(self) + + scroll_widget = QtWidgets.QScrollArea(self) + scroll_widget.setWidgetResizable(True) + content_widget = QtWidgets.QWidget(self) + scroll_widget.setWidget(content_widget) + + max_len = 0 + content_layout = QtWidgets.QVBoxLayout(content_widget) + for message in messages: + label_widget = QtWidgets.QLabel(message, content_widget) + content_layout.addWidget(label_widget) + max_len = max(max_len, len(message)) + + # guess size of scrollable area + max_width = QtWidgets.QApplication.desktop().availableGeometry().width + scroll_widget.setMinimumWidth(min(max_width, max_len * 6)) + layout.addWidget(scroll_widget) + + if not cancelable: # if no specific buttons OK only + buttons = QtWidgets.QDialogButtonBox.Ok + else: + buttons = QtWidgets.QDialogButtonBox.Ok | \ + QtWidgets.QDialogButtonBox.Cancel + + btn_box = QtWidgets.QDialogButtonBox(buttons) + btn_box.accepted.connect(self.accept) + + if cancelable: + btn_box.reject.connect(self.reject) + + btn = QtWidgets.QPushButton('Copy to clipboard') + btn.clicked.connect(lambda: QtWidgets.QApplication. + clipboard().setText("\n".join(messages))) + btn_box.addButton(btn, QtWidgets.QDialogButtonBox.NoRole) + + layout.addWidget(btn_box) + self.show() From 22f8084a8d393a93b4264e8292833f70bd7bd9b0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 10 Sep 2020 12:53:41 +0200 Subject: [PATCH 2/4] Fix #237 - Reworked reference cleanup --- pype/plugins/maya/load/load_look.py | 121 +++++++++++++++++----------- 1 file changed, 73 insertions(+), 48 deletions(-) diff --git a/pype/plugins/maya/load/load_look.py b/pype/plugins/maya/load/load_look.py index d82978b1a1..cb5b6fa2e8 100644 --- a/pype/plugins/maya/load/load_look.py +++ b/pype/plugins/maya/load/load_look.py @@ -70,6 +70,9 @@ class LookLoader(pype.hosts.maya.plugin.ReferenceLoader): members = cmds.sets(node, query=True, nodesOnly=True) reference_node = self._get_reference_node(members) + shader_nodes = cmds.ls(members, type='shadingEngine') + orig_nodes = set(self._get_nodes_with_shader(shader_nodes)) + file_type = { "ma": "mayaAscii", "mb": "mayaBinary", @@ -80,35 +83,7 @@ class LookLoader(pype.hosts.maya.plugin.ReferenceLoader): assert os.path.exists(path), "%s does not exist." % path - try: - content = cmds.file(path, - loadReference=reference_node, - type=file_type, - returnNewNodes=True) - except RuntimeError as exc: - # When changing a reference to a file that has load errors the - # command will raise an error even if the file is still loaded - # correctly (e.g. when raising errors on Arnold attributes) - # When the file is loaded and has content, we consider it's fine. - if not cmds.referenceQuery(reference_node, isLoaded=True): - raise - - content = cmds.referenceQuery(reference_node, - nodes=True, - dagPath=True) - if not content: - raise - - self.log.warning("Ignoring file read error:\n%s", exc) - - # Fix PLN-40 for older containers created with Avalon that had the - # `.verticesOnlySet` set to True. - if cmds.getAttr("{}.verticesOnlySet".format(node)): - self.log.info("Setting %s.verticesOnlySet to False", node) - cmds.setAttr("{}.verticesOnlySet".format(node), False) - - # Add new nodes of the reference to the container - cmds.sets(content, forceElement=node) + self._load_reference(file_type, node, path, reference_node) # Remove any placeHolderList attribute entries from the set that # are remaining from nodes being removed from the referenced file. @@ -117,18 +92,9 @@ class LookLoader(pype.hosts.maya.plugin.ReferenceLoader): if invalid: cmds.sets(invalid, remove=node) - # Get container members + # get new applied shaders and nodes from new version shader_nodes = cmds.ls(members, type='shadingEngine') - - nodes_list = [] - for shader in shader_nodes: - connections = cmds.listConnections(cmds.listHistory(shader, f=1), - type='mesh') - if connections: - for connection in connections: - nodes_list.extend(cmds.listRelatives(connection, - shapes=True)) - nodes = set(nodes_list) + nodes = set(self._get_nodes_with_shader(shader_nodes)) json_representation = io.find_one({ "type": "representation", @@ -150,20 +116,16 @@ class LookLoader(pype.hosts.maya.plugin.ReferenceLoader): # highlight failed edits to user if failed_edits: - shader_data = relationships.get("relationships", {}) - for rel in shader_data.values(): - for member in rel["members"]: - nodes.add(member['name']) - # clean references - removes failed reference edits cmds.file(unloadReference=reference_node) cmds.file(cr=reference_node) # cleanReference - cmds.file(loadReference=reference_node) + # reload reference, now it shouldn't fail + self._load_reference(file_type, node, path, reference_node) - # reapply shading groups from json representation + # reapply shading groups from json representation on orig nodes pype.hosts.maya.lib.apply_shaders(relationships, shader_nodes, - nodes) + orig_nodes) msg = ["During reference update some edits failed.", "All successful edits were kept intact.\n", @@ -186,3 +148,66 @@ class LookLoader(pype.hosts.maya.plugin.ReferenceLoader): cmds.setAttr("{}.representation".format(node), str(representation["_id"]), type="string") + + def _get_nodes_with_shader(self, shader_nodes): + """ + Returns list of nodes belonging to specific shaders + Args: + shader_nodes: of Shader groups + Returns + node names + """ + import maya.cmds as cmds + # Get container members + + nodes_list = [] + for shader in shader_nodes: + connections = cmds.listConnections(cmds.listHistory(shader, f=1), + type='mesh') + if connections: + for connection in connections: + nodes_list.extend(cmds.listRelatives(connection, + shapes=True)) + return nodes_list + + def _load_reference(self, file_type, node, path, reference_node): + """ + Load reference from 'path' on 'reference_node'. Used when change + of look (version/update) is triggered. + Args: + file_type: extension of referenced file + node: + path: (string) location of referenced file + reference_node: (string) - name of node that should be applied + on + Returns: + None + """ + import maya.cmds as cmds + try: + content = cmds.file(path, + loadReference=reference_node, + type=file_type, + returnNewNodes=True) + except RuntimeError as exc: + # When changing a reference to a file that has load errors the + # command will raise an error even if the file is still loaded + # correctly (e.g. when raising errors on Arnold attributes) + # When the file is loaded and has content, we consider it's fine. + if not cmds.referenceQuery(reference_node, isLoaded=True): + raise + + content = cmds.referenceQuery(reference_node, + nodes=True, + dagPath=True) + if not content: + raise + + self.log.warning("Ignoring file read error:\n%s", exc) + # Fix PLN-40 for older containers created with Avalon that had the + # `.verticesOnlySet` set to True. + if cmds.getAttr("{}.verticesOnlySet".format(node)): + self.log.info("Setting %s.verticesOnlySet to False", node) + cmds.setAttr("{}.verticesOnlySet".format(node), False) + # Add new nodes of the reference to the container + cmds.sets(content, forceElement=node) From adb87fa9dd3fc4dcab07f0c2c1a055a2803a13a5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 10 Sep 2020 13:13:52 +0200 Subject: [PATCH 3/4] Hound --- pype/widgets/message_window.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pype/widgets/message_window.py b/pype/widgets/message_window.py index f909c60710..969d6ccdd1 100644 --- a/pype/widgets/message_window.py +++ b/pype/widgets/message_window.py @@ -91,8 +91,7 @@ class ScrollMessageBox(QtWidgets.QDialog): messages: of messages cancelable: - True if Cancel button should be added """ - def __init__(self, icon, title, messages, cancelable=False, - *args, **kwargs): + def __init__(self, icon, title, messages, cancelable=False): super(ScrollMessageBox, self).__init__() self.setWindowTitle(title) self.icon = icon From 2e9be7332681097fa05998dbd6d41567c0d96d48 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 1 Oct 2020 19:20:00 +0200 Subject: [PATCH 4/4] Removed unload + reload to keep changes to untouched shaders --- pype/plugins/maya/load/load_look.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pype/plugins/maya/load/load_look.py b/pype/plugins/maya/load/load_look.py index cb5b6fa2e8..c5b58c9bd5 100644 --- a/pype/plugins/maya/load/load_look.py +++ b/pype/plugins/maya/load/load_look.py @@ -117,10 +117,7 @@ class LookLoader(pype.hosts.maya.plugin.ReferenceLoader): # highlight failed edits to user if failed_edits: # clean references - removes failed reference edits - cmds.file(unloadReference=reference_node) cmds.file(cr=reference_node) # cleanReference - # reload reference, now it shouldn't fail - self._load_reference(file_type, node, path, reference_node) # reapply shading groups from json representation on orig nodes pype.hosts.maya.lib.apply_shaders(relationships,