mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #1087 from pypeclub/feature/add_look_assigner_to_pype_tools
Add Maya look assigner to pype tools
This commit is contained in:
commit
f68bb4e9bc
9 changed files with 901 additions and 2 deletions
|
|
@ -89,7 +89,7 @@ def override_toolbox_ui():
|
|||
log.warning("Could not import Workfiles tool")
|
||||
|
||||
try:
|
||||
import mayalookassigner
|
||||
from pype.tools import mayalookassigner
|
||||
except Exception:
|
||||
log.warning("Could not import Maya Look assigner tool")
|
||||
|
||||
|
|
|
|||
21
pype/tools/mayalookassigner/LICENSE
Normal file
21
pype/tools/mayalookassigner/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017 Colorbleed
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
9
pype/tools/mayalookassigner/__init__.py
Normal file
9
pype/tools/mayalookassigner/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from .app import (
|
||||
App,
|
||||
show
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"App",
|
||||
"show"]
|
||||
248
pype/tools/mayalookassigner/app.py
Normal file
248
pype/tools/mayalookassigner/app.py
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
import sys
|
||||
import time
|
||||
import logging
|
||||
|
||||
from pype.hosts.maya.api.lib import assign_look_by_version
|
||||
|
||||
from avalon import style, io
|
||||
from avalon.tools import lib
|
||||
from avalon.vendor.Qt import QtWidgets, QtCore
|
||||
|
||||
from maya import cmds
|
||||
# old api for MFileIO
|
||||
import maya.OpenMaya
|
||||
import maya.api.OpenMaya as om
|
||||
|
||||
from . import widgets
|
||||
from . import commands
|
||||
|
||||
module = sys.modules[__name__]
|
||||
module.window = None
|
||||
|
||||
|
||||
class App(QtWidgets.QWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QtWidgets.QWidget.__init__(self, parent=parent)
|
||||
|
||||
self.log = logging.getLogger(__name__)
|
||||
|
||||
# Store callback references
|
||||
self._callbacks = []
|
||||
|
||||
filename = commands.get_workfile()
|
||||
|
||||
self.setObjectName("lookManager")
|
||||
self.setWindowTitle("Look Manager 1.3.0 - [{}]".format(filename))
|
||||
self.setWindowFlags(QtCore.Qt.Window)
|
||||
self.setParent(parent)
|
||||
|
||||
# Force to delete the window on close so it triggers
|
||||
# closeEvent only once. Otherwise it's retriggered when
|
||||
# the widget gets garbage collected.
|
||||
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
|
||||
self.resize(750, 500)
|
||||
|
||||
self.setup_ui()
|
||||
|
||||
self.setup_connections()
|
||||
|
||||
# Force refresh check on initialization
|
||||
self._on_renderlayer_switch()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Build the UI"""
|
||||
|
||||
# Assets (left)
|
||||
asset_outliner = widgets.AssetOutliner()
|
||||
|
||||
# Looks (right)
|
||||
looks_widget = QtWidgets.QWidget()
|
||||
looks_layout = QtWidgets.QVBoxLayout(looks_widget)
|
||||
|
||||
look_outliner = widgets.LookOutliner() # Database look overview
|
||||
|
||||
assign_selected = QtWidgets.QCheckBox("Assign to selected only")
|
||||
assign_selected.setToolTip("Whether to assign only to selected nodes "
|
||||
"or to the full asset")
|
||||
remove_unused_btn = QtWidgets.QPushButton("Remove Unused Looks")
|
||||
|
||||
looks_layout.addWidget(look_outliner)
|
||||
looks_layout.addWidget(assign_selected)
|
||||
looks_layout.addWidget(remove_unused_btn)
|
||||
|
||||
# Footer
|
||||
status = QtWidgets.QStatusBar()
|
||||
status.setSizeGripEnabled(False)
|
||||
status.setFixedHeight(25)
|
||||
warn_layer = QtWidgets.QLabel("Current Layer is not "
|
||||
"defaultRenderLayer")
|
||||
warn_layer.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
warn_layer.setStyleSheet("color: #DD5555; font-weight: bold;")
|
||||
warn_layer.setFixedHeight(25)
|
||||
|
||||
footer = QtWidgets.QHBoxLayout()
|
||||
footer.setContentsMargins(0, 0, 0, 0)
|
||||
footer.addWidget(status)
|
||||
footer.addWidget(warn_layer)
|
||||
|
||||
# Build up widgets
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.setSpacing(0)
|
||||
main_splitter = QtWidgets.QSplitter()
|
||||
main_splitter.setStyleSheet("QSplitter{ border: 0px; }")
|
||||
main_splitter.addWidget(asset_outliner)
|
||||
main_splitter.addWidget(looks_widget)
|
||||
main_splitter.setSizes([350, 200])
|
||||
main_layout.addWidget(main_splitter)
|
||||
main_layout.addLayout(footer)
|
||||
|
||||
# Set column width
|
||||
asset_outliner.view.setColumnWidth(0, 200)
|
||||
look_outliner.view.setColumnWidth(0, 150)
|
||||
|
||||
# Open widgets
|
||||
self.asset_outliner = asset_outliner
|
||||
self.look_outliner = look_outliner
|
||||
self.status = status
|
||||
self.warn_layer = warn_layer
|
||||
|
||||
# Buttons
|
||||
self.remove_unused = remove_unused_btn
|
||||
self.assign_selected = assign_selected
|
||||
|
||||
def setup_connections(self):
|
||||
"""Connect interactive widgets with actions"""
|
||||
|
||||
self.asset_outliner.selection_changed.connect(
|
||||
self.on_asset_selection_changed)
|
||||
|
||||
self.asset_outliner.refreshed.connect(
|
||||
lambda: self.echo("Loaded assets.."))
|
||||
|
||||
self.look_outliner.menu_apply_action.connect(self.on_process_selected)
|
||||
self.remove_unused.clicked.connect(commands.remove_unused_looks)
|
||||
|
||||
# Maya renderlayer switch callback
|
||||
callback = om.MEventMessage.addEventCallback(
|
||||
"renderLayerManagerChange",
|
||||
self._on_renderlayer_switch
|
||||
)
|
||||
self._callbacks.append(callback)
|
||||
|
||||
def closeEvent(self, event):
|
||||
|
||||
# Delete callbacks
|
||||
for callback in self._callbacks:
|
||||
om.MMessage.removeCallback(callback)
|
||||
|
||||
return super(App, self).closeEvent(event)
|
||||
|
||||
def _on_renderlayer_switch(self, *args):
|
||||
"""Callback that updates on Maya renderlayer switch"""
|
||||
|
||||
if maya.OpenMaya.MFileIO.isNewingFile():
|
||||
# Don't perform a check during file open or file new as
|
||||
# the renderlayers will not be in a valid state yet.
|
||||
return
|
||||
|
||||
layer = cmds.editRenderLayerGlobals(query=True,
|
||||
currentRenderLayer=True)
|
||||
if layer != "defaultRenderLayer":
|
||||
self.warn_layer.show()
|
||||
else:
|
||||
self.warn_layer.hide()
|
||||
|
||||
def echo(self, message):
|
||||
self.status.showMessage(message, 1500)
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh the content"""
|
||||
|
||||
# Get all containers and information
|
||||
self.asset_outliner.clear()
|
||||
found_items = self.asset_outliner.get_all_assets()
|
||||
if not found_items:
|
||||
self.look_outliner.clear()
|
||||
|
||||
def on_asset_selection_changed(self):
|
||||
"""Get selected items from asset loader and fill look outliner"""
|
||||
|
||||
items = self.asset_outliner.get_selected_items()
|
||||
self.look_outliner.clear()
|
||||
self.look_outliner.add_items(items)
|
||||
|
||||
def on_process_selected(self):
|
||||
"""Process all selected looks for the selected assets"""
|
||||
|
||||
assets = self.asset_outliner.get_selected_items()
|
||||
assert assets, "No asset selected"
|
||||
|
||||
# Collect the looks we want to apply (by name)
|
||||
look_items = self.look_outliner.get_selected_items()
|
||||
looks = {look["subset"] for look in look_items}
|
||||
|
||||
selection = self.assign_selected.isChecked()
|
||||
asset_nodes = self.asset_outliner.get_nodes(selection=selection)
|
||||
|
||||
start = time.time()
|
||||
for i, (asset, item) in enumerate(asset_nodes.items()):
|
||||
|
||||
# Label prefix
|
||||
prefix = "({}/{})".format(i+1, len(asset_nodes))
|
||||
|
||||
# Assign the first matching look relevant for this asset
|
||||
# (since assigning multiple to the same nodes makes no sense)
|
||||
assign_look = next((subset for subset in item["looks"]
|
||||
if subset["name"] in looks), None)
|
||||
if not assign_look:
|
||||
self.echo("{} No matching selected "
|
||||
"look for {}".format(prefix, asset))
|
||||
continue
|
||||
|
||||
# Get the latest version of this asset's look subset
|
||||
version = io.find_one({"type": "version",
|
||||
"parent": assign_look["_id"]},
|
||||
sort=[("name", -1)])
|
||||
|
||||
subset_name = assign_look["name"]
|
||||
self.echo("{} Assigning {} to {}\t".format(prefix,
|
||||
subset_name,
|
||||
asset))
|
||||
|
||||
# Assign look
|
||||
assign_look_by_version(nodes=item["nodes"],
|
||||
version_id=version["_id"])
|
||||
|
||||
end = time.time()
|
||||
|
||||
self.echo("Finished assigning.. ({0:.3f}s)".format(end - start))
|
||||
|
||||
|
||||
def show():
|
||||
"""Display Loader GUI
|
||||
|
||||
Arguments:
|
||||
debug (bool, optional): Run loader in debug-mode,
|
||||
defaults to False
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
module.window.close()
|
||||
del module.window
|
||||
except (RuntimeError, AttributeError):
|
||||
pass
|
||||
|
||||
# Get Maya main window
|
||||
top_level_widgets = QtWidgets.QApplication.topLevelWidgets()
|
||||
mainwindow = next(widget for widget in top_level_widgets
|
||||
if widget.objectName() == "MayaWindow")
|
||||
|
||||
with lib.application():
|
||||
window = App(parent=mainwindow)
|
||||
window.setStyleSheet(style.load_stylesheet())
|
||||
window.show()
|
||||
|
||||
module.window = window
|
||||
191
pype/tools/mayalookassigner/commands.py
Normal file
191
pype/tools/mayalookassigner/commands.py
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
from collections import defaultdict
|
||||
import logging
|
||||
import os
|
||||
|
||||
import maya.cmds as cmds
|
||||
|
||||
from pype.hosts.maya.api import lib
|
||||
|
||||
from avalon import io, api
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_workfile():
|
||||
path = cmds.file(query=True, sceneName=True) or "untitled"
|
||||
return os.path.basename(path)
|
||||
|
||||
|
||||
def get_workfolder():
|
||||
return os.path.dirname(cmds.file(query=True, sceneName=True))
|
||||
|
||||
|
||||
def select(nodes):
|
||||
cmds.select(nodes)
|
||||
|
||||
|
||||
def get_namespace_from_node(node):
|
||||
"""Get the namespace from the given node
|
||||
|
||||
Args:
|
||||
node (str): name of the node
|
||||
|
||||
Returns:
|
||||
namespace (str)
|
||||
|
||||
"""
|
||||
parts = node.rsplit("|", 1)[-1].rsplit(":", 1)
|
||||
return parts[0] if len(parts) > 1 else u":"
|
||||
|
||||
|
||||
def list_descendents(nodes):
|
||||
"""Include full descendant hierarchy of given nodes.
|
||||
|
||||
This is a workaround to cmds.listRelatives(allDescendents=True) because
|
||||
this way correctly keeps children instance paths (see Maya documentation)
|
||||
|
||||
This fixes LKD-26: assignments not working as expected on instanced shapes.
|
||||
|
||||
Return:
|
||||
list: List of children descendents of nodes
|
||||
|
||||
"""
|
||||
result = []
|
||||
while True:
|
||||
nodes = cmds.listRelatives(nodes,
|
||||
fullPath=True)
|
||||
if nodes:
|
||||
result.extend(nodes)
|
||||
else:
|
||||
return result
|
||||
|
||||
|
||||
def get_selected_nodes():
|
||||
"""Get information from current selection"""
|
||||
|
||||
selection = cmds.ls(selection=True, long=True)
|
||||
hierarchy = list_descendents(selection)
|
||||
nodes = list(set(selection + hierarchy))
|
||||
|
||||
return nodes
|
||||
|
||||
|
||||
def get_all_asset_nodes():
|
||||
"""Get all assets from the scene, container based
|
||||
|
||||
Returns:
|
||||
list: list of dictionaries
|
||||
"""
|
||||
|
||||
host = api.registered_host()
|
||||
|
||||
nodes = []
|
||||
for container in host.ls():
|
||||
# We are not interested in looks but assets!
|
||||
if container["loader"] == "LookLoader":
|
||||
continue
|
||||
|
||||
# Gather all information
|
||||
container_name = container["objectName"]
|
||||
nodes += cmds.sets(container_name, query=True, nodesOnly=True) or []
|
||||
|
||||
return nodes
|
||||
|
||||
|
||||
def create_asset_id_hash(nodes):
|
||||
"""Create a hash based on cbId attribute value
|
||||
Args:
|
||||
nodes (list): a list of nodes
|
||||
|
||||
Returns:
|
||||
dict
|
||||
"""
|
||||
node_id_hash = defaultdict(list)
|
||||
for node in nodes:
|
||||
value = lib.get_id(node)
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
asset_id = value.split(":")[0]
|
||||
node_id_hash[asset_id].append(node)
|
||||
|
||||
return dict(node_id_hash)
|
||||
|
||||
|
||||
def create_items_from_nodes(nodes):
|
||||
"""Create an item for the view based the container and content of it
|
||||
|
||||
It fetches the look document based on the asset ID found in the content.
|
||||
The item will contain all important information for the tool to work.
|
||||
|
||||
If there is an asset ID which is not registered in the project's collection
|
||||
it will log a warning message.
|
||||
|
||||
Args:
|
||||
nodes (list): list of maya nodes
|
||||
|
||||
Returns:
|
||||
list of dicts
|
||||
|
||||
"""
|
||||
|
||||
asset_view_items = []
|
||||
|
||||
id_hashes = create_asset_id_hash(nodes)
|
||||
if not id_hashes:
|
||||
return asset_view_items
|
||||
|
||||
for _id, id_nodes in id_hashes.items():
|
||||
asset = io.find_one({"_id": io.ObjectId(_id)},
|
||||
projection={"name": True})
|
||||
|
||||
# Skip if asset id is not found
|
||||
if not asset:
|
||||
log.warning("Id not found in the database, skipping '%s'." % _id)
|
||||
log.warning("Nodes: %s" % id_nodes)
|
||||
continue
|
||||
|
||||
# Collect available look subsets for this asset
|
||||
looks = lib.list_looks(asset["_id"])
|
||||
|
||||
# Collect namespaces the asset is found in
|
||||
namespaces = set()
|
||||
for node in id_nodes:
|
||||
namespace = get_namespace_from_node(node)
|
||||
namespaces.add(namespace)
|
||||
|
||||
asset_view_items.append({"label": asset["name"],
|
||||
"asset": asset,
|
||||
"looks": looks,
|
||||
"namespaces": namespaces})
|
||||
|
||||
return asset_view_items
|
||||
|
||||
|
||||
def remove_unused_looks():
|
||||
"""Removes all loaded looks for which none of the shaders are used.
|
||||
|
||||
This will cleanup all loaded "LookLoader" containers that are unused in
|
||||
the current scene.
|
||||
|
||||
"""
|
||||
|
||||
host = api.registered_host()
|
||||
|
||||
unused = list()
|
||||
for container in host.ls():
|
||||
if container['loader'] == "LookLoader":
|
||||
members = cmds.sets(container['objectName'], query=True)
|
||||
look_sets = cmds.ls(members, type="objectSet")
|
||||
for look_set in look_sets:
|
||||
# If the set is used than we consider this look *in use*
|
||||
if cmds.sets(look_set, query=True):
|
||||
break
|
||||
else:
|
||||
unused.append(container)
|
||||
|
||||
for container in unused:
|
||||
log.info("Removing unused look container: %s", container['objectName'])
|
||||
api.remove(container)
|
||||
|
||||
log.info("Finished removing unused looks. (see log for details)")
|
||||
120
pype/tools/mayalookassigner/models.py
Normal file
120
pype/tools/mayalookassigner/models.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
from collections import defaultdict
|
||||
from avalon.tools import models
|
||||
|
||||
from avalon.vendor.Qt import QtCore
|
||||
from avalon.vendor import qtawesome
|
||||
from avalon.style import colors
|
||||
|
||||
|
||||
class AssetModel(models.TreeModel):
|
||||
|
||||
Columns = ["label"]
|
||||
|
||||
def add_items(self, items):
|
||||
"""
|
||||
Add items to model with needed data
|
||||
Args:
|
||||
items(list): collection of item data
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
self.beginResetModel()
|
||||
|
||||
# Add the items sorted by label
|
||||
sorter = lambda x: x["label"]
|
||||
|
||||
for item in sorted(items, key=sorter):
|
||||
|
||||
asset_item = models.Item()
|
||||
asset_item.update(item)
|
||||
asset_item["icon"] = "folder"
|
||||
|
||||
# Add namespace children
|
||||
namespaces = item["namespaces"]
|
||||
for namespace in sorted(namespaces):
|
||||
child = models.Item()
|
||||
child.update(item)
|
||||
child.update({
|
||||
"label": (namespace if namespace != ":"
|
||||
else "(no namespace)"),
|
||||
"namespace": namespace,
|
||||
"looks": item["looks"],
|
||||
"icon": "folder-o"
|
||||
})
|
||||
asset_item.add_child(child)
|
||||
|
||||
self.add_child(asset_item)
|
||||
|
||||
self.endResetModel()
|
||||
|
||||
def data(self, index, role):
|
||||
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
if role == models.TreeModel.ItemRole:
|
||||
node = index.internalPointer()
|
||||
return node
|
||||
|
||||
# Add icon
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
if index.column() == 0:
|
||||
node = index.internalPointer()
|
||||
icon = node.get("icon")
|
||||
if icon:
|
||||
return qtawesome.icon("fa.{0}".format(icon),
|
||||
color=colors.default)
|
||||
|
||||
return super(AssetModel, self).data(index, role)
|
||||
|
||||
|
||||
class LookModel(models.TreeModel):
|
||||
"""Model displaying a list of looks and matches for assets"""
|
||||
|
||||
Columns = ["label", "match"]
|
||||
|
||||
def add_items(self, items):
|
||||
"""Add items to model with needed data
|
||||
|
||||
An item exists of:
|
||||
{
|
||||
"subset": 'name of subset',
|
||||
"asset": asset_document
|
||||
}
|
||||
|
||||
Args:
|
||||
items(list): collection of item data
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
self.beginResetModel()
|
||||
|
||||
# Collect the assets per look name (from the items of the AssetModel)
|
||||
look_subsets = defaultdict(list)
|
||||
for asset_item in items:
|
||||
asset = asset_item["asset"]
|
||||
for look in asset_item["looks"]:
|
||||
look_subsets[look["name"]].append(asset)
|
||||
|
||||
for subset, assets in sorted(look_subsets.iteritems()):
|
||||
|
||||
# Define nice label without "look" prefix for readability
|
||||
label = subset if not subset.startswith("look") else subset[4:]
|
||||
|
||||
item_node = models.Item()
|
||||
item_node["label"] = label
|
||||
item_node["subset"] = subset
|
||||
|
||||
# Amount of matching assets for this look
|
||||
item_node["match"] = len(assets)
|
||||
|
||||
# Store the assets that have this subset available
|
||||
item_node["assets"] = assets
|
||||
|
||||
self.add_child(item_node)
|
||||
|
||||
self.endResetModel()
|
||||
50
pype/tools/mayalookassigner/views.py
Normal file
50
pype/tools/mayalookassigner/views.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
from avalon.vendor.Qt import QtWidgets, QtCore
|
||||
|
||||
|
||||
DEFAULT_COLOR = "#fb9c15"
|
||||
|
||||
|
||||
class View(QtWidgets.QTreeView):
|
||||
data_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(View, self).__init__(parent=parent)
|
||||
|
||||
# view settings
|
||||
self.setAlternatingRowColors(False)
|
||||
self.setSortingEnabled(True)
|
||||
self.setSelectionMode(self.ExtendedSelection)
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
|
||||
def get_indices(self):
|
||||
"""Get the selected rows"""
|
||||
selection_model = self.selectionModel()
|
||||
return selection_model.selectedRows()
|
||||
|
||||
def extend_to_children(self, indices):
|
||||
"""Extend the indices to the children indices.
|
||||
|
||||
Top-level indices are extended to its children indices. Sub-items
|
||||
are kept as is.
|
||||
|
||||
:param indices: The indices to extend.
|
||||
:type indices: list
|
||||
|
||||
:return: The children indices
|
||||
:rtype: list
|
||||
"""
|
||||
|
||||
subitems = set()
|
||||
for i in indices:
|
||||
valid_parent = i.parent().isValid()
|
||||
if valid_parent and i not in subitems:
|
||||
subitems.add(i)
|
||||
else:
|
||||
# is top level node
|
||||
model = i.model()
|
||||
rows = model.rowCount(parent=i)
|
||||
for row in range(rows):
|
||||
child = model.index(row, 0, parent=i)
|
||||
subitems.add(child)
|
||||
|
||||
return list(subitems)
|
||||
261
pype/tools/mayalookassigner/widgets.py
Normal file
261
pype/tools/mayalookassigner/widgets.py
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
from avalon.vendor.Qt import QtWidgets, QtCore
|
||||
|
||||
# TODO: expose this better in avalon core
|
||||
from avalon.tools import lib
|
||||
from avalon.tools.models import TreeModel
|
||||
|
||||
from . import models
|
||||
from . import commands
|
||||
from . import views
|
||||
|
||||
from maya import cmds
|
||||
|
||||
MODELINDEX = QtCore.QModelIndex()
|
||||
|
||||
|
||||
class AssetOutliner(QtWidgets.QWidget):
|
||||
|
||||
refreshed = QtCore.Signal()
|
||||
selection_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QtWidgets.QWidget.__init__(self, parent)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
|
||||
title = QtWidgets.QLabel("Assets")
|
||||
title.setAlignment(QtCore.Qt.AlignCenter)
|
||||
title.setStyleSheet("font-weight: bold; font-size: 12px")
|
||||
|
||||
model = models.AssetModel()
|
||||
view = views.View()
|
||||
view.setModel(model)
|
||||
view.customContextMenuRequested.connect(self.right_mouse_menu)
|
||||
view.setSortingEnabled(False)
|
||||
view.setHeaderHidden(True)
|
||||
view.setIndentation(10)
|
||||
|
||||
from_all_asset_btn = QtWidgets.QPushButton("Get All Assets")
|
||||
from_selection_btn = QtWidgets.QPushButton("Get Assets From Selection")
|
||||
|
||||
layout.addWidget(title)
|
||||
layout.addWidget(from_all_asset_btn)
|
||||
layout.addWidget(from_selection_btn)
|
||||
layout.addWidget(view)
|
||||
|
||||
# Build connections
|
||||
from_selection_btn.clicked.connect(self.get_selected_assets)
|
||||
from_all_asset_btn.clicked.connect(self.get_all_assets)
|
||||
|
||||
selection_model = view.selectionModel()
|
||||
selection_model.selectionChanged.connect(self.selection_changed)
|
||||
|
||||
self.view = view
|
||||
self.model = model
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
self.log = logging.getLogger(__name__)
|
||||
|
||||
def clear(self):
|
||||
self.model.clear()
|
||||
|
||||
# fix looks remaining visible when no items present after "refresh"
|
||||
# todo: figure out why this workaround is needed.
|
||||
self.selection_changed.emit()
|
||||
|
||||
def add_items(self, items):
|
||||
"""Add new items to the outliner"""
|
||||
|
||||
self.model.add_items(items)
|
||||
self.refreshed.emit()
|
||||
|
||||
def get_selected_items(self):
|
||||
"""Get current selected items from view
|
||||
|
||||
Returns:
|
||||
list: list of dictionaries
|
||||
"""
|
||||
|
||||
selection_model = self.view.selectionModel()
|
||||
items = [row.data(TreeModel.ItemRole) for row in
|
||||
selection_model.selectedRows(0)]
|
||||
|
||||
return items
|
||||
|
||||
def get_all_assets(self):
|
||||
"""Add all items from the current scene"""
|
||||
|
||||
with lib.preserve_expanded_rows(self.view):
|
||||
with lib.preserve_selection(self.view):
|
||||
self.clear()
|
||||
nodes = commands.get_all_asset_nodes()
|
||||
items = commands.create_items_from_nodes(nodes)
|
||||
self.add_items(items)
|
||||
|
||||
return len(items) > 0
|
||||
|
||||
def get_selected_assets(self):
|
||||
"""Add all selected items from the current scene"""
|
||||
|
||||
with lib.preserve_expanded_rows(self.view):
|
||||
with lib.preserve_selection(self.view):
|
||||
self.clear()
|
||||
nodes = commands.get_selected_nodes()
|
||||
items = commands.create_items_from_nodes(nodes)
|
||||
self.add_items(items)
|
||||
|
||||
def get_nodes(self, selection=False):
|
||||
"""Find the nodes in the current scene per asset."""
|
||||
|
||||
items = self.get_selected_items()
|
||||
|
||||
# Collect all nodes by hash (optimization)
|
||||
if not selection:
|
||||
nodes = cmds.ls(dag=True, long=True)
|
||||
else:
|
||||
nodes = commands.get_selected_nodes()
|
||||
id_nodes = commands.create_asset_id_hash(nodes)
|
||||
|
||||
# Collect the asset item entries per asset
|
||||
# and collect the namespaces we'd like to apply
|
||||
assets = dict()
|
||||
asset_namespaces = defaultdict(set)
|
||||
for item in items:
|
||||
asset_id = str(item["asset"]["_id"])
|
||||
asset_name = item["asset"]["name"]
|
||||
asset_namespaces[asset_name].add(item.get("namespace"))
|
||||
|
||||
if asset_name in assets:
|
||||
continue
|
||||
|
||||
assets[asset_name] = item
|
||||
assets[asset_name]["nodes"] = id_nodes.get(asset_id, [])
|
||||
|
||||
# Filter nodes to namespace (if only namespaces were selected)
|
||||
for asset_name in assets:
|
||||
namespaces = asset_namespaces[asset_name]
|
||||
|
||||
# When None is present there should be no filtering
|
||||
if None in namespaces:
|
||||
continue
|
||||
|
||||
# Else only namespaces are selected and *not* the top entry so
|
||||
# we should filter to only those namespaces.
|
||||
nodes = assets[asset_name]["nodes"]
|
||||
nodes = [node for node in nodes if
|
||||
commands.get_namespace_from_node(node) in namespaces]
|
||||
assets[asset_name]["nodes"] = nodes
|
||||
|
||||
return assets
|
||||
|
||||
def select_asset_from_items(self):
|
||||
"""Select nodes from listed asset"""
|
||||
|
||||
items = self.get_nodes(selection=False)
|
||||
nodes = []
|
||||
for item in items.values():
|
||||
nodes.extend(item["nodes"])
|
||||
|
||||
commands.select(nodes)
|
||||
|
||||
def right_mouse_menu(self, pos):
|
||||
"""Build RMB menu for asset outliner"""
|
||||
|
||||
active = self.view.currentIndex() # index under mouse
|
||||
active = active.sibling(active.row(), 0) # get first column
|
||||
globalpos = self.view.viewport().mapToGlobal(pos)
|
||||
|
||||
menu = QtWidgets.QMenu(self.view)
|
||||
|
||||
# Direct assignment
|
||||
apply_action = QtWidgets.QAction(menu, text="Select nodes")
|
||||
apply_action.triggered.connect(self.select_asset_from_items)
|
||||
|
||||
if not active.isValid():
|
||||
apply_action.setEnabled(False)
|
||||
|
||||
menu.addAction(apply_action)
|
||||
|
||||
menu.exec_(globalpos)
|
||||
|
||||
|
||||
class LookOutliner(QtWidgets.QWidget):
|
||||
|
||||
menu_apply_action = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QtWidgets.QWidget.__init__(self, parent)
|
||||
|
||||
# look manager layout
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(10)
|
||||
|
||||
# Looks from database
|
||||
title = QtWidgets.QLabel("Looks")
|
||||
title.setAlignment(QtCore.Qt.AlignCenter)
|
||||
title.setStyleSheet("font-weight: bold; font-size: 12px")
|
||||
title.setAlignment(QtCore.Qt.AlignCenter)
|
||||
|
||||
model = models.LookModel()
|
||||
|
||||
# Proxy for dynamic sorting
|
||||
proxy = QtCore.QSortFilterProxyModel()
|
||||
proxy.setSourceModel(model)
|
||||
|
||||
view = views.View()
|
||||
view.setModel(proxy)
|
||||
view.setMinimumHeight(180)
|
||||
view.setToolTip("Use right mouse button menu for direct actions")
|
||||
view.customContextMenuRequested.connect(self.right_mouse_menu)
|
||||
view.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
|
||||
layout.addWidget(title)
|
||||
layout.addWidget(view)
|
||||
|
||||
self.view = view
|
||||
self.model = model
|
||||
|
||||
def clear(self):
|
||||
self.model.clear()
|
||||
|
||||
def add_items(self, items):
|
||||
self.model.add_items(items)
|
||||
|
||||
def get_selected_items(self):
|
||||
"""Get current selected items from view
|
||||
|
||||
Returns:
|
||||
list: list of dictionaries
|
||||
"""
|
||||
|
||||
datas = [i.data(TreeModel.ItemRole) for i in self.view.get_indices()]
|
||||
items = [d for d in datas if d is not None] # filter Nones
|
||||
|
||||
return items
|
||||
|
||||
def right_mouse_menu(self, pos):
|
||||
"""Build RMB menu for look view"""
|
||||
|
||||
active = self.view.currentIndex() # index under mouse
|
||||
active = active.sibling(active.row(), 0) # get first column
|
||||
globalpos = self.view.viewport().mapToGlobal(pos)
|
||||
|
||||
if not active.isValid():
|
||||
return
|
||||
|
||||
menu = QtWidgets.QMenu(self.view)
|
||||
|
||||
# Direct assignment
|
||||
apply_action = QtWidgets.QAction(menu, text="Assign looks..")
|
||||
apply_action.triggered.connect(self.menu_apply_action)
|
||||
|
||||
menu.addAction(apply_action)
|
||||
|
||||
menu.exec_(globalpos)
|
||||
|
||||
|
||||
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 7adabe8f0e6858bfe5b6bf0b39bd428ed72d0452
|
||||
Loading…
Add table
Add a link
Reference in a new issue