diff --git a/pype/plugins/maya/load/load_look.py b/pype/plugins/maya/load/load_look.py index b9c0d81104..c5b58c9bd5 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,18 +46,33 @@ 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 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", @@ -66,6 +83,104 @@ class LookLoader(pype.hosts.maya.plugin.ReferenceLoader): assert os.path.exists(path), "%s does not exist." % path + 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. + members = cmds.sets(node, query=True) + invalid = [x for x in members if ".placeHolderList" in x] + if invalid: + cmds.sets(invalid, remove=node) + + # get new applied shaders and nodes from new version + shader_nodes = cmds.ls(members, type='shadingEngine') + nodes = set(self._get_nodes_with_shader(shader_nodes)) + + json_representation = io.find_one({ + "type": "representation", + "parent": representation['parent'], + "name": "json" + }) + + # Load relationships + shader_relation = api.get_representation_path(json_representation) + 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: + # clean references - removes failed reference edits + cmds.file(cr=reference_node) # cleanReference + + # reapply shading groups from json representation on orig nodes + pype.hosts.maya.lib.apply_shaders(relationships, + shader_nodes, + orig_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 + 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, @@ -86,57 +201,10 @@ class LookLoader(pype.hosts.maya.plugin.ReferenceLoader): 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) - - # Remove any placeHolderList attribute entries from the set that - # are remaining from nodes being removed from the referenced file. - members = cmds.sets(node, query=True) - invalid = [x for x in members if ".placeHolderList" in x] - if invalid: - cmds.sets(invalid, remove=node) - - # Get container members - 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) - - json_representation = io.find_one({ - "type": "representation", - "parent": representation['parent'], - "name": "json" - }) - - # Load relationships - shader_relation = api.get_representation_path(json_representation) - with open(shader_relation, "r") as f: - relationships = json.load(f) - - 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 - cmds.setAttr("{}.representation".format(node), - str(representation["_id"]), - type="string") diff --git a/pype/widgets/message_window.py b/pype/widgets/message_window.py index 41c709b933..969d6ccdd1 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,60 @@ 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): + 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()