Merge pull request #2255 from pypeclub/feature/sceneinventory_in_openpype

Tools: SceneInventory in OpenPype
This commit is contained in:
Jakub Trllo 2021-11-16 14:22:04 +01:00 committed by GitHub
commit 3a07b0d0f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 2712 additions and 5 deletions

View file

@ -0,0 +1,9 @@
from .window import (
show,
SceneInventoryWindow
)
__all__ = (
"show",
"SceneInventoryWindow"
)

View file

@ -0,0 +1,82 @@
import os
from openpype_modules import sync_server
from Qt import QtGui
def walk_hierarchy(node):
"""Recursively yield group node."""
for child in node.children():
if child.get("isGroupNode"):
yield child
for _child in walk_hierarchy(child):
yield _child
def get_site_icons():
resource_path = os.path.join(
os.path.dirname(sync_server.sync_server_module.__file__),
"providers",
"resources"
)
icons = {}
# TODO get from sync module
for provider in ["studio", "local_drive", "gdrive"]:
pix_url = "{}/{}.png".format(resource_path, provider)
icons[provider] = QtGui.QIcon(pix_url)
return icons
def get_progress_for_repre(repre_doc, active_site, remote_site):
"""
Calculates average progress for representation.
If site has created_dt >> fully available >> progress == 1
Could be calculated in aggregate if it would be too slow
Args:
repre_doc(dict): representation dict
Returns:
(dict) with active and remote sites progress
{'studio': 1.0, 'gdrive': -1} - gdrive site is not present
-1 is used to highlight the site should be added
{'studio': 1.0, 'gdrive': 0.0} - gdrive site is present, not
uploaded yet
"""
progress = {active_site: -1, remote_site: -1}
if not repre_doc:
return progress
files = {active_site: 0, remote_site: 0}
doc_files = repre_doc.get("files") or []
for doc_file in doc_files:
if not isinstance(doc_file, dict):
continue
sites = doc_file.get("sites") or []
for site in sites:
if (
# Pype 2 compatibility
not isinstance(site, dict)
# Check if site name is one of progress sites
or site["name"] not in progress
):
continue
files[site["name"]] += 1
norm_progress = max(progress[site["name"]], 0)
if site.get("created_dt"):
progress[site["name"]] = norm_progress + 1
elif site.get("progress"):
progress[site["name"]] = norm_progress + site["progress"]
else: # site exists, might be failed, do not add again
progress[site["name"]] = 0
# for example 13 fully avail. files out of 26 >> 13/26 = 0.5
avg_progress = {
active_site: progress[active_site] / max(files[active_site], 1),
remote_site: progress[remote_site] / max(files[remote_site], 1)
}
return avg_progress

View file

@ -0,0 +1,576 @@
import re
import logging
from collections import defaultdict
from Qt import QtCore, QtGui
from avalon import api, io, style, schema
from avalon.vendor import qtawesome
from avalon.lib import HeroVersionType
from avalon.tools.models import TreeModel, Item
from .lib import (
get_site_icons,
walk_hierarchy,
get_progress_for_repre
)
from openpype.modules import ModulesManager
class InventoryModel(TreeModel):
"""The model for the inventory"""
Columns = ["Name", "version", "count", "family", "loader", "objectName"]
OUTDATED_COLOR = QtGui.QColor(235, 30, 30)
CHILD_OUTDATED_COLOR = QtGui.QColor(200, 160, 30)
GRAYOUT_COLOR = QtGui.QColor(160, 160, 160)
UniqueRole = QtCore.Qt.UserRole + 2 # unique label role
def __init__(self, family_config_cache, parent=None):
super(InventoryModel, self).__init__(parent)
self.log = logging.getLogger(self.__class__.__name__)
self.family_config_cache = family_config_cache
self._hierarchy_view = False
manager = ModulesManager()
sync_server = manager.modules_by_name["sync_server"]
self.sync_enabled = sync_server.enabled
self._site_icons = {}
self.active_site = self.remote_site = None
self.active_provider = self.remote_provider = None
if not self.sync_enabled:
return
project_name = io.Session["AVALON_PROJECT"]
active_site = sync_server.get_active_site(project_name)
remote_site = sync_server.get_remote_site(project_name)
active_provider = "studio"
remote_provider = "studio"
if active_site != "studio":
# sanitized for icon
active_provider = sync_server.get_provider_for_site(
project_name, active_site
)
if remote_site != "studio":
remote_provider = sync_server.get_provider_for_site(
project_name, remote_site
)
# self.sync_server = sync_server
self.active_site = active_site
self.active_provider = active_provider
self.remote_site = remote_site
self.remote_provider = remote_provider
self._site_icons = get_site_icons()
if "active_site" not in self.Columns:
self.Columns.append("active_site")
if "remote_site" not in self.Columns:
self.Columns.append("remote_site")
def outdated(self, item):
value = item.get("version")
if isinstance(value, HeroVersionType):
return False
if item.get("version") == item.get("highest_version"):
return False
return True
def data(self, index, role):
if not index.isValid():
return
item = index.internalPointer()
if role == QtCore.Qt.FontRole:
# Make top-level entries bold
if item.get("isGroupNode") or item.get("isNotSet"): # group-item
font = QtGui.QFont()
font.setBold(True)
return font
if role == QtCore.Qt.ForegroundRole:
# Set the text color to the OUTDATED_COLOR when the
# collected version is not the same as the highest version
key = self.Columns[index.column()]
if key == "version": # version
if item.get("isGroupNode"): # group-item
if self.outdated(item):
return self.OUTDATED_COLOR
if self._hierarchy_view:
# If current group is not outdated, check if any
# outdated children.
for _node in walk_hierarchy(item):
if self.outdated(_node):
return self.CHILD_OUTDATED_COLOR
else:
if self._hierarchy_view:
# Although this is not a group item, we still need
# to distinguish which one contain outdated child.
for _node in walk_hierarchy(item):
if self.outdated(_node):
return self.CHILD_OUTDATED_COLOR.darker(150)
return self.GRAYOUT_COLOR
if key == "Name" and not item.get("isGroupNode"):
return self.GRAYOUT_COLOR
# Add icons
if role == QtCore.Qt.DecorationRole:
if index.column() == 0:
# Override color
color = item.get("color", style.colors.default)
if item.get("isGroupNode"): # group-item
return qtawesome.icon("fa.folder", color=color)
if item.get("isNotSet"):
return qtawesome.icon("fa.exclamation-circle", color=color)
return qtawesome.icon("fa.file-o", color=color)
if index.column() == 3:
# Family icon
return item.get("familyIcon", None)
if item.get("isGroupNode"):
column_name = self.Columns[index.column()]
if column_name == "active_site":
provider = item.get("active_site_provider")
return self._site_icons.get(provider)
if column_name == "remote_site":
provider = item.get("remote_site_provider")
return self._site_icons.get(provider)
if role == QtCore.Qt.DisplayRole and item.get("isGroupNode"):
column_name = self.Columns[index.column()]
progress = None
if column_name == 'active_site':
progress = item.get("active_site_progress", 0)
elif column_name == 'remote_site':
progress = item.get("remote_site_progress", 0)
if progress is not None:
return "{}%".format(max(progress, 0) * 100)
if role == self.UniqueRole:
return item["representation"] + item.get("objectName", "<none>")
return super(InventoryModel, self).data(index, role)
def set_hierarchy_view(self, state):
"""Set whether to display subsets in hierarchy view."""
state = bool(state)
if state != self._hierarchy_view:
self._hierarchy_view = state
def refresh(self, selected=None, items=None):
"""Refresh the model"""
host = api.registered_host()
if not items: # for debugging or testing, injecting items from outside
items = host.ls()
self.clear()
if self._hierarchy_view and selected:
if not hasattr(host.pipeline, "update_hierarchy"):
# If host doesn't support hierarchical containers, then
# cherry-pick only.
self.add_items((item for item in items
if item["objectName"] in selected))
# Update hierarchy info for all containers
items_by_name = {item["objectName"]: item
for item in host.pipeline.update_hierarchy(items)}
selected_items = set()
def walk_children(names):
"""Select containers and extend to chlid containers"""
for name in [n for n in names if n not in selected_items]:
selected_items.add(name)
item = items_by_name[name]
yield item
for child in walk_children(item["children"]):
yield child
items = list(walk_children(selected)) # Cherry-picked and extended
# Cut unselected upstream containers
for item in items:
if not item.get("parent") in selected_items:
# Parent not in selection, this is root item.
item["parent"] = None
parents = [self._root_item]
# The length of `items` array is the maximum depth that a
# hierarchy could be.
# Take this as an easiest way to prevent looping forever.
maximum_loop = len(items)
count = 0
while items:
if count > maximum_loop:
self.log.warning("Maximum loop count reached, possible "
"missing parent node.")
break
_parents = list()
for parent in parents:
_unparented = list()
def _children():
"""Child item provider"""
for item in items:
if item.get("parent") == parent.get("objectName"):
# (NOTE)
# Since `self._root_node` has no "objectName"
# entry, it will be paired with root item if
# the value of key "parent" is None, or not
# having the key.
yield item
else:
# Not current parent's child, try next
_unparented.append(item)
self.add_items(_children(), parent)
items[:] = _unparented
# Parents of next level
for group_node in parent.children():
_parents += group_node.children()
parents[:] = _parents
count += 1
else:
self.add_items(items)
def add_items(self, items, parent=None):
"""Add the items to the model.
The items should be formatted similar to `api.ls()` returns, an item
is then represented as:
{"filename_v001.ma": [full/filename/of/loaded/filename_v001.ma,
full/filename/of/loaded/filename_v001.ma],
"nodetype" : "reference",
"node": "referenceNode1"}
Note: When performing an additional call to `add_items` it will *not*
group the new items with previously existing item groups of the
same type.
Args:
items (generator): the items to be processed as returned by `ls()`
parent (Item, optional): Set this item as parent for the added
items when provided. Defaults to the root of the model.
Returns:
node.Item: root node which has children added based on the data
"""
self.beginResetModel()
# Group by representation
grouped = defaultdict(lambda: {"items": list()})
for item in items:
grouped[item["representation"]]["items"].append(item)
# Add to model
not_found = defaultdict(list)
not_found_ids = []
for repre_id, group_dict in sorted(grouped.items()):
group_items = group_dict["items"]
# Get parenthood per group
representation = io.find_one({"_id": io.ObjectId(repre_id)})
if not representation:
not_found["representation"].append(group_items)
not_found_ids.append(repre_id)
continue
version = io.find_one({"_id": representation["parent"]})
if not version:
not_found["version"].append(group_items)
not_found_ids.append(repre_id)
continue
elif version["type"] == "hero_version":
_version = io.find_one({
"_id": version["version_id"]
})
version["name"] = HeroVersionType(_version["name"])
version["data"] = _version["data"]
subset = io.find_one({"_id": version["parent"]})
if not subset:
not_found["subset"].append(group_items)
not_found_ids.append(repre_id)
continue
asset = io.find_one({"_id": subset["parent"]})
if not asset:
not_found["asset"].append(group_items)
not_found_ids.append(repre_id)
continue
grouped[repre_id].update({
"representation": representation,
"version": version,
"subset": subset,
"asset": asset
})
for id in not_found_ids:
grouped.pop(id)
for where, group_items in not_found.items():
# create the group header
group_node = Item()
name = "< NOT FOUND - {} >".format(where)
group_node["Name"] = name
group_node["representation"] = name
group_node["count"] = len(group_items)
group_node["isGroupNode"] = False
group_node["isNotSet"] = True
self.add_child(group_node, parent=parent)
for _group_items in group_items:
item_node = Item()
item_node["Name"] = ", ".join(
[item["objectName"] for item in _group_items]
)
self.add_child(item_node, parent=group_node)
for repre_id, group_dict in sorted(grouped.items()):
group_items = group_dict["items"]
representation = grouped[repre_id]["representation"]
version = grouped[repre_id]["version"]
subset = grouped[repre_id]["subset"]
asset = grouped[repre_id]["asset"]
# Get the primary family
no_family = ""
maj_version, _ = schema.get_schema_version(subset["schema"])
if maj_version < 3:
prim_family = version["data"].get("family")
if not prim_family:
families = version["data"].get("families")
prim_family = families[0] if families else no_family
else:
families = subset["data"].get("families") or []
prim_family = families[0] if families else no_family
# Get the label and icon for the family if in configuration
family_config = self.family_config_cache.family_config(prim_family)
family = family_config.get("label", prim_family)
family_icon = family_config.get("icon", None)
# Store the highest available version so the model can know
# whether current version is currently up-to-date.
highest_version = io.find_one({
"type": "version",
"parent": version["parent"]
}, sort=[("name", -1)])
# create the group header
group_node = Item()
group_node["Name"] = "%s_%s: (%s)" % (asset["name"],
subset["name"],
representation["name"])
group_node["representation"] = repre_id
group_node["version"] = version["name"]
group_node["highest_version"] = highest_version["name"]
group_node["family"] = family
group_node["familyIcon"] = family_icon
group_node["count"] = len(group_items)
group_node["isGroupNode"] = True
if self.sync_enabled:
progress = get_progress_for_repre(
representation, self.active_site, self.remote_site
)
group_node["active_site"] = self.active_site
group_node["active_site_provider"] = self.active_provider
group_node["remote_site"] = self.remote_site
group_node["remote_site_provider"] = self.remote_provider
group_node["active_site_progress"] = progress[self.active_site]
group_node["remote_site_progress"] = progress[self.remote_site]
self.add_child(group_node, parent=parent)
for item in group_items:
item_node = Item()
item_node.update(item)
# store the current version on the item
item_node["version"] = version["name"]
# Remapping namespace to item name.
# Noted that the name key is capital "N", by doing this, we
# can view namespace in GUI without changing container data.
item_node["Name"] = item["namespace"]
self.add_child(item_node, parent=group_node)
self.endResetModel()
return self._root_item
class FilterProxyModel(QtCore.QSortFilterProxyModel):
"""Filter model to where key column's value is in the filtered tags"""
def __init__(self, *args, **kwargs):
super(FilterProxyModel, self).__init__(*args, **kwargs)
self._filter_outdated = False
self._hierarchy_view = False
def filterAcceptsRow(self, row, parent):
model = self.sourceModel()
source_index = model.index(row, self.filterKeyColumn(), parent)
# Always allow bottom entries (individual containers), since their
# parent group hidden if it wouldn't have been validated.
rows = model.rowCount(source_index)
if not rows:
return True
# Filter by regex
if not self.filterRegExp().isEmpty():
pattern = re.escape(self.filterRegExp().pattern())
if not self._matches(row, parent, pattern):
return False
if self._filter_outdated:
# When filtering to outdated we filter the up to date entries
# thus we "allow" them when they are outdated
if not self._is_outdated(row, parent):
return False
return True
def set_filter_outdated(self, state):
"""Set whether to show the outdated entries only."""
state = bool(state)
if state != self._filter_outdated:
self._filter_outdated = bool(state)
self.invalidateFilter()
def set_hierarchy_view(self, state):
state = bool(state)
if state != self._hierarchy_view:
self._hierarchy_view = state
def _is_outdated(self, row, parent):
"""Return whether row is outdated.
A row is considered outdated if it has "version" and "highest_version"
data and in the internal data structure, and they are not of an
equal value.
"""
def outdated(node):
version = node.get("version", None)
highest = node.get("highest_version", None)
# Always allow indices that have no version data at all
if version is None and highest is None:
return True
# If either a version or highest is present but not the other
# consider the item invalid.
if not self._hierarchy_view:
# Skip this check if in hierarchy view, or the child item
# node will be hidden even it's actually outdated.
if version is None or highest is None:
return False
return version != highest
index = self.sourceModel().index(row, self.filterKeyColumn(), parent)
# The scene contents are grouped by "representation", e.g. the same
# "representation" loaded twice is grouped under the same header.
# Since the version check filters these parent groups we skip that
# check for the individual children.
has_parent = index.parent().isValid()
if has_parent and not self._hierarchy_view:
return True
# Filter to those that have the different version numbers
node = index.internalPointer()
if outdated(node):
return True
if self._hierarchy_view:
for _node in walk_hierarchy(node):
if outdated(_node):
return True
return False
def _matches(self, row, parent, pattern):
"""Return whether row matches regex pattern.
Args:
row (int): row number in model
parent (QtCore.QModelIndex): parent index
pattern (regex.pattern): pattern to check for in key
Returns:
bool
"""
model = self.sourceModel()
column = self.filterKeyColumn()
role = self.filterRole()
def matches(row, parent, pattern):
index = model.index(row, column, parent)
key = model.data(index, role)
if re.search(pattern, key, re.IGNORECASE):
return True
if matches(row, parent, pattern):
return True
# Also allow if any of the children matches
source_index = model.index(row, column, parent)
rows = model.rowCount(source_index)
if any(
matches(idx, source_index, pattern)
for idx in range(rows)
):
return True
if not self._hierarchy_view:
return False
for idx in range(rows):
child_index = model.index(idx, column, source_index)
child_rows = model.rowCount(child_index)
return any(
self._matches(child_idx, child_index, pattern)
for child_idx in range(child_rows)
)
return True

View file

@ -0,0 +1,993 @@
import collections
import logging
from Qt import QtWidgets, QtCore
from avalon import io, api
from avalon.vendor import qtawesome
from .widgets import SearchComboBox
log = logging.getLogger("SwitchAssetDialog")
class ValidationState:
def __init__(self):
self.asset_ok = True
self.subset_ok = True
self.repre_ok = True
@property
def all_ok(self):
return (
self.asset_ok
and self.subset_ok
and self.repre_ok
)
class SwitchAssetDialog(QtWidgets.QDialog):
"""Widget to support asset switching"""
MIN_WIDTH = 550
switched = QtCore.Signal()
def __init__(self, parent=None, items=None):
super(SwitchAssetDialog, self).__init__(parent)
self.setWindowTitle("Switch selected items ...")
# Force and keep focus dialog
self.setModal(True)
assets_combox = SearchComboBox(self)
subsets_combox = SearchComboBox(self)
repres_combobox = SearchComboBox(self)
assets_combox.set_placeholder("<asset>")
subsets_combox.set_placeholder("<subset>")
repres_combobox.set_placeholder("<representation>")
asset_label = QtWidgets.QLabel(self)
subset_label = QtWidgets.QLabel(self)
repre_label = QtWidgets.QLabel(self)
current_asset_btn = QtWidgets.QPushButton("Use current asset")
accept_icon = qtawesome.icon("fa.check", color="white")
accept_btn = QtWidgets.QPushButton(self)
accept_btn.setIcon(accept_icon)
main_layout = QtWidgets.QGridLayout(self)
# Asset column
main_layout.addWidget(current_asset_btn, 0, 0)
main_layout.addWidget(assets_combox, 1, 0)
main_layout.addWidget(asset_label, 2, 0)
# Subset column
main_layout.addWidget(subsets_combox, 1, 1)
main_layout.addWidget(subset_label, 2, 1)
# Representation column
main_layout.addWidget(repres_combobox, 1, 2)
main_layout.addWidget(repre_label, 2, 2)
# Btn column
main_layout.addWidget(accept_btn, 1, 3)
main_layout.setColumnStretch(0, 1)
main_layout.setColumnStretch(1, 1)
main_layout.setColumnStretch(2, 1)
main_layout.setColumnStretch(3, 0)
assets_combox.currentIndexChanged.connect(
self._combobox_value_changed
)
subsets_combox.currentIndexChanged.connect(
self._combobox_value_changed
)
repres_combobox.currentIndexChanged.connect(
self._combobox_value_changed
)
accept_btn.clicked.connect(self._on_accept)
current_asset_btn.clicked.connect(self._on_current_asset)
self._current_asset_btn = current_asset_btn
self._assets_box = assets_combox
self._subsets_box = subsets_combox
self._representations_box = repres_combobox
self._asset_label = asset_label
self._subset_label = subset_label
self._repre_label = repre_label
self._accept_btn = accept_btn
self._init_asset_name = None
self._init_subset_name = None
self._init_repre_name = None
self._fill_check = False
self._items = items
self._prepare_content_data()
self.refresh(True)
self.setMinimumWidth(self.MIN_WIDTH)
# Set default focus to accept button so you don't directly type in
# first asset field, this also allows to see the placeholder value.
accept_btn.setFocus()
def _prepare_content_data(self):
repre_ids = [
io.ObjectId(item["representation"])
for item in self._items
]
repres = list(io.find({
"type": {"$in": ["representation", "archived_representation"]},
"_id": {"$in": repre_ids}
}))
repres_by_id = {repre["_id"]: repre for repre in repres}
# stash context values, works only for single representation
if len(repres) == 1:
self._init_asset_name = repres[0]["context"]["asset"]
self._init_subset_name = repres[0]["context"]["subset"]
self._init_repre_name = repres[0]["context"]["representation"]
content_repres = {}
archived_repres = []
missing_repres = []
version_ids = []
for repre_id in repre_ids:
if repre_id not in repres_by_id:
missing_repres.append(repre_id)
elif repres_by_id[repre_id]["type"] == "archived_representation":
repre = repres_by_id[repre_id]
archived_repres.append(repre)
version_ids.append(repre["parent"])
else:
repre = repres_by_id[repre_id]
content_repres[repre_id] = repres_by_id[repre_id]
version_ids.append(repre["parent"])
versions = io.find({
"type": {"$in": ["version", "hero_version"]},
"_id": {"$in": list(set(version_ids))}
})
content_versions = {}
hero_version_ids = set()
for version in versions:
content_versions[version["_id"]] = version
if version["type"] == "hero_version":
hero_version_ids.add(version["_id"])
missing_versions = []
subset_ids = []
for version_id in version_ids:
if version_id not in content_versions:
missing_versions.append(version_id)
else:
subset_ids.append(content_versions[version_id]["parent"])
subsets = io.find({
"type": {"$in": ["subset", "archived_subset"]},
"_id": {"$in": subset_ids}
})
subsets_by_id = {sub["_id"]: sub for sub in subsets}
asset_ids = []
archived_subsets = []
missing_subsets = []
content_subsets = {}
for subset_id in subset_ids:
if subset_id not in subsets_by_id:
missing_subsets.append(subset_id)
elif subsets_by_id[subset_id]["type"] == "archived_subset":
subset = subsets_by_id[subset_id]
asset_ids.append(subset["parent"])
archived_subsets.append(subset)
else:
subset = subsets_by_id[subset_id]
asset_ids.append(subset["parent"])
content_subsets[subset_id] = subset
assets = io.find({
"type": {"$in": ["asset", "archived_asset"]},
"_id": {"$in": list(asset_ids)}
})
assets_by_id = {asset["_id"]: asset for asset in assets}
missing_assets = []
archived_assets = []
content_assets = {}
for asset_id in asset_ids:
if asset_id not in assets_by_id:
missing_assets.append(asset_id)
elif assets_by_id[asset_id]["type"] == "archived_asset":
archived_assets.append(assets_by_id[asset_id])
else:
content_assets[asset_id] = assets_by_id[asset_id]
self.content_assets = content_assets
self.content_subsets = content_subsets
self.content_versions = content_versions
self.content_repres = content_repres
self.hero_version_ids = hero_version_ids
self.missing_assets = missing_assets
self.missing_versions = missing_versions
self.missing_subsets = missing_subsets
self.missing_repres = missing_repres
self.missing_docs = (
bool(missing_assets)
or bool(missing_versions)
or bool(missing_subsets)
or bool(missing_repres)
)
self.archived_assets = archived_assets
self.archived_subsets = archived_subsets
self.archived_repres = archived_repres
def _combobox_value_changed(self, *args, **kwargs):
self.refresh()
def refresh(self, init_refresh=False):
"""Build the need comboboxes with content"""
if not self._fill_check and not init_refresh:
return
self._fill_check = False
if init_refresh:
asset_values = self._get_asset_box_values()
self._fill_combobox(asset_values, "asset")
validation_state = ValidationState()
# Set other comboboxes to empty if any document is missing or any asset
# of loaded representations is archived.
self._is_asset_ok(validation_state)
if validation_state.asset_ok:
subset_values = self._get_subset_box_values()
self._fill_combobox(subset_values, "subset")
self._is_subset_ok(validation_state)
if validation_state.asset_ok and validation_state.subset_ok:
repre_values = sorted(self._representations_box_values())
self._fill_combobox(repre_values, "repre")
self._is_repre_ok(validation_state)
# Fill comboboxes with values
self.set_labels()
self.apply_validations(validation_state)
if init_refresh: # pre select context if possible
self._assets_box.set_valid_value(self._init_asset_name)
self._subsets_box.set_valid_value(self._init_subset_name)
self._representations_box.set_valid_value(self._init_repre_name)
self._fill_check = True
def _get_loaders(self, representations):
if not representations:
return list()
available_loaders = filter(
lambda l: not (hasattr(l, "is_utility") and l.is_utility),
api.discover(api.Loader)
)
loaders = set()
for representation in representations:
for loader in api.loaders_from_representation(
available_loaders,
representation
):
loaders.add(loader)
return loaders
def _fill_combobox(self, values, combobox_type):
if combobox_type == "asset":
combobox_widget = self._assets_box
elif combobox_type == "subset":
combobox_widget = self._subsets_box
elif combobox_type == "repre":
combobox_widget = self._representations_box
else:
return
selected_value = combobox_widget.get_valid_value()
# Fill combobox
if values is not None:
combobox_widget.populate(list(sorted(values)))
if selected_value and selected_value in values:
index = None
for idx in range(combobox_widget.count()):
if selected_value == str(combobox_widget.itemText(idx)):
index = idx
break
if index is not None:
combobox_widget.setCurrentIndex(index)
def set_labels(self):
asset_label = self._assets_box.get_valid_value()
subset_label = self._subsets_box.get_valid_value()
repre_label = self._representations_box.get_valid_value()
default = "*No changes"
self._asset_label.setText(asset_label or default)
self._subset_label.setText(subset_label or default)
self._repre_label.setText(repre_label or default)
def apply_validations(self, validation_state):
error_msg = "*Please select"
error_sheet = "border: 1px solid red;"
success_sheet = "border: 1px solid green;"
asset_sheet = None
subset_sheet = None
repre_sheet = None
accept_sheet = None
if validation_state.asset_ok is False:
asset_sheet = error_sheet
self._asset_label.setText(error_msg)
elif validation_state.subset_ok is False:
subset_sheet = error_sheet
self._subset_label.setText(error_msg)
elif validation_state.repre_ok is False:
repre_sheet = error_sheet
self._repre_label.setText(error_msg)
if validation_state.all_ok:
accept_sheet = success_sheet
self._assets_box.setStyleSheet(asset_sheet or "")
self._subsets_box.setStyleSheet(subset_sheet or "")
self._representations_box.setStyleSheet(repre_sheet or "")
self._accept_btn.setEnabled(validation_state.all_ok)
self._accept_btn.setStyleSheet(accept_sheet or "")
def _get_asset_box_values(self):
asset_docs = io.find(
{"type": "asset"},
{"_id": 1, "name": 1}
)
asset_names_by_id = {
asset_doc["_id"]: asset_doc["name"]
for asset_doc in asset_docs
}
subsets = io.find(
{
"type": "subset",
"parent": {"$in": list(asset_names_by_id.keys())}
},
{
"parent": 1
}
)
filtered_assets = []
for subset in subsets:
asset_name = asset_names_by_id[subset["parent"]]
if asset_name not in filtered_assets:
filtered_assets.append(asset_name)
return sorted(filtered_assets)
def _get_subset_box_values(self):
selected_asset = self._assets_box.get_valid_value()
if selected_asset:
asset_doc = io.find_one({"type": "asset", "name": selected_asset})
asset_ids = [asset_doc["_id"]]
else:
asset_ids = list(self.content_assets.keys())
subsets = io.find(
{
"type": "subset",
"parent": {"$in": asset_ids}
},
{
"parent": 1,
"name": 1
}
)
subset_names_by_parent_id = collections.defaultdict(set)
for subset in subsets:
subset_names_by_parent_id[subset["parent"]].add(subset["name"])
possible_subsets = None
for subset_names in subset_names_by_parent_id.values():
if possible_subsets is None:
possible_subsets = subset_names
else:
possible_subsets = (possible_subsets & subset_names)
if not possible_subsets:
break
return list(possible_subsets or list())
def _representations_box_values(self):
# NOTE hero versions are not used because it is expected that
# hero version has same representations as latests
selected_asset = self._assets_box.currentText()
selected_subset = self._subsets_box.currentText()
# If nothing is selected
# [ ] [ ] [?]
if not selected_asset and not selected_subset:
# Find all representations of selection's subsets
possible_repres = list(io.find(
{
"type": "representation",
"parent": {"$in": list(self.content_versions.keys())}
},
{
"parent": 1,
"name": 1
}
))
possible_repres_by_parent = collections.defaultdict(set)
for repre in possible_repres:
possible_repres_by_parent[repre["parent"]].add(repre["name"])
output_repres = None
for repre_names in possible_repres_by_parent.values():
if output_repres is None:
output_repres = repre_names
else:
output_repres = (output_repres & repre_names)
if not output_repres:
break
return list(output_repres or list())
# [x] [x] [?]
if selected_asset and selected_subset:
asset_doc = io.find_one(
{"type": "asset", "name": selected_asset},
{"_id": 1}
)
subset_doc = io.find_one(
{
"type": "subset",
"name": selected_subset,
"parent": asset_doc["_id"]
},
{"_id": 1}
)
subset_id = subset_doc["_id"]
last_versions_by_subset_id = self.find_last_versions([subset_id])
version_doc = last_versions_by_subset_id.get(subset_id)
repre_docs = io.find(
{
"type": "representation",
"parent": version_doc["_id"]
},
{
"name": 1
}
)
return [
repre_doc["name"]
for repre_doc in repre_docs
]
# [x] [ ] [?]
# If asset only is selected
if selected_asset:
asset_doc = io.find_one(
{"type": "asset", "name": selected_asset},
{"_id": 1}
)
if not asset_doc:
return list()
# Filter subsets by subset names from content
subset_names = set()
for subset_doc in self.content_subsets.values():
subset_names.add(subset_doc["name"])
subset_docs = io.find(
{
"type": "subset",
"parent": asset_doc["_id"],
"name": {"$in": list(subset_names)}
},
{"_id": 1}
)
subset_ids = [
subset_doc["_id"]
for subset_doc in subset_docs
]
if not subset_ids:
return list()
last_versions_by_subset_id = self.find_last_versions(subset_ids)
subset_id_by_version_id = {}
for subset_id, last_version in last_versions_by_subset_id.items():
version_id = last_version["_id"]
subset_id_by_version_id[version_id] = subset_id
if not subset_id_by_version_id:
return list()
repre_docs = list(io.find(
{
"type": "representation",
"parent": {"$in": list(subset_id_by_version_id.keys())}
},
{
"name": 1,
"parent": 1
}
))
if not repre_docs:
return list()
repre_names_by_parent = collections.defaultdict(set)
for repre_doc in repre_docs:
repre_names_by_parent[repre_doc["parent"]].add(
repre_doc["name"]
)
available_repres = None
for repre_names in repre_names_by_parent.values():
if available_repres is None:
available_repres = repre_names
continue
available_repres = available_repres.intersection(repre_names)
return list(available_repres)
# [ ] [x] [?]
subset_docs = list(io.find(
{
"type": "subset",
"parent": {"$in": list(self.content_assets.keys())},
"name": selected_subset
},
{"_id": 1, "parent": 1}
))
if not subset_docs:
return list()
subset_docs_by_id = {
subset_doc["_id"]: subset_doc
for subset_doc in subset_docs
}
last_versions_by_subset_id = self.find_last_versions(
subset_docs_by_id.keys()
)
subset_id_by_version_id = {}
for subset_id, last_version in last_versions_by_subset_id.items():
version_id = last_version["_id"]
subset_id_by_version_id[version_id] = subset_id
if not subset_id_by_version_id:
return list()
repre_docs = list(io.find(
{
"type": "representation",
"parent": {"$in": list(subset_id_by_version_id.keys())}
},
{
"name": 1,
"parent": 1
}
))
if not repre_docs:
return list()
repre_names_by_asset_id = {}
for repre_doc in repre_docs:
subset_id = subset_id_by_version_id[repre_doc["parent"]]
asset_id = subset_docs_by_id[subset_id]["parent"]
if asset_id not in repre_names_by_asset_id:
repre_names_by_asset_id[asset_id] = set()
repre_names_by_asset_id[asset_id].add(repre_doc["name"])
available_repres = None
for repre_names in repre_names_by_asset_id.values():
if available_repres is None:
available_repres = repre_names
continue
available_repres = available_repres.intersection(repre_names)
return list(available_repres)
def _is_asset_ok(self, validation_state):
selected_asset = self._assets_box.get_valid_value()
if (
selected_asset is None
and (self.missing_docs or self.archived_assets)
):
validation_state.asset_ok = False
def _is_subset_ok(self, validation_state):
selected_asset = self._assets_box.get_valid_value()
selected_subset = self._subsets_box.get_valid_value()
# [?] [x] [?]
# If subset is selected then must be ok
if selected_subset is not None:
return
# [ ] [ ] [?]
if selected_asset is None:
# If there were archived subsets and asset is not selected
if self.archived_subsets:
validation_state.subset_ok = False
return
# [x] [ ] [?]
asset_doc = io.find_one(
{"type": "asset", "name": selected_asset},
{"_id": 1}
)
subset_docs = io.find(
{"type": "subset", "parent": asset_doc["_id"]},
{"name": 1}
)
subset_names = set(
subset_doc["name"]
for subset_doc in subset_docs
)
for subset_doc in self.content_subsets.values():
if subset_doc["name"] not in subset_names:
validation_state.subset_ok = False
break
def find_last_versions(self, subset_ids):
_pipeline = [
# Find all versions of those subsets
{"$match": {
"type": "version",
"parent": {"$in": list(subset_ids)}
}},
# Sorting versions all together
{"$sort": {"name": 1}},
# Group them by "parent", but only take the last
{"$group": {
"_id": "$parent",
"_version_id": {"$last": "$_id"},
"type": {"$last": "$type"}
}}
]
last_versions_by_subset_id = dict()
for doc in io.aggregate(_pipeline):
doc["parent"] = doc["_id"]
doc["_id"] = doc.pop("_version_id")
last_versions_by_subset_id[doc["parent"]] = doc
return last_versions_by_subset_id
def _is_repre_ok(self, validation_state):
selected_asset = self._assets_box.get_valid_value()
selected_subset = self._subsets_box.get_valid_value()
selected_repre = self._representations_box.get_valid_value()
# [?] [?] [x]
# If subset is selected then must be ok
if selected_repre is not None:
return
# [ ] [ ] [ ]
if selected_asset is None and selected_subset is None:
if (
self.archived_repres
or self.missing_versions
or self.missing_repres
):
validation_state.repre_ok = False
return
# [x] [x] [ ]
if selected_asset is not None and selected_subset is not None:
asset_doc = io.find_one(
{"type": "asset", "name": selected_asset},
{"_id": 1}
)
subset_doc = io.find_one(
{
"type": "subset",
"parent": asset_doc["_id"],
"name": selected_subset
},
{"_id": 1}
)
last_versions_by_subset_id = self.find_last_versions(
[subset_doc["_id"]]
)
last_version = last_versions_by_subset_id.get(subset_doc["_id"])
if not last_version:
validation_state.repre_ok = False
return
repre_docs = io.find(
{
"type": "representation",
"parent": last_version["_id"]
},
{"name": 1}
)
repre_names = set(
repre_doc["name"]
for repre_doc in repre_docs
)
for repre_doc in self.content_repres.values():
if repre_doc["name"] not in repre_names:
validation_state.repre_ok = False
break
return
# [x] [ ] [ ]
if selected_asset is not None:
asset_doc = io.find_one(
{"type": "asset", "name": selected_asset},
{"_id": 1}
)
subset_docs = list(io.find(
{
"type": "subset",
"parent": asset_doc["_id"]
},
{"_id": 1, "name": 1}
))
subset_name_by_id = {}
subset_ids = set()
for subset_doc in subset_docs:
subset_id = subset_doc["_id"]
subset_ids.add(subset_id)
subset_name_by_id[subset_id] = subset_doc["name"]
last_versions_by_subset_id = self.find_last_versions(subset_ids)
subset_id_by_version_id = {}
for subset_id, last_version in last_versions_by_subset_id.items():
version_id = last_version["_id"]
subset_id_by_version_id[version_id] = subset_id
repre_docs = io.find(
{
"type": "representation",
"parent": {"$in": list(subset_id_by_version_id.keys())}
},
{
"name": 1,
"parent": 1
}
)
repres_by_subset_name = {}
for repre_doc in repre_docs:
subset_id = subset_id_by_version_id[repre_doc["parent"]]
subset_name = subset_name_by_id[subset_id]
if subset_name not in repres_by_subset_name:
repres_by_subset_name[subset_name] = set()
repres_by_subset_name[subset_name].add(repre_doc["name"])
for repre_doc in self.content_repres.values():
version_doc = self.content_versions[repre_doc["parent"]]
subset_doc = self.content_subsets[version_doc["parent"]]
repre_names = (
repres_by_subset_name.get(subset_doc["name"]) or []
)
if repre_doc["name"] not in repre_names:
validation_state.repre_ok = False
break
return
# [ ] [x] [ ]
# Subset documents
subset_docs = io.find(
{
"type": "subset",
"parent": {"$in": list(self.content_assets.keys())},
"name": selected_subset
},
{"_id": 1, "name": 1, "parent": 1}
)
subset_docs_by_id = {}
for subset_doc in subset_docs:
subset_docs_by_id[subset_doc["_id"]] = subset_doc
last_versions_by_subset_id = self.find_last_versions(
subset_docs_by_id.keys()
)
subset_id_by_version_id = {}
for subset_id, last_version in last_versions_by_subset_id.items():
version_id = last_version["_id"]
subset_id_by_version_id[version_id] = subset_id
repre_docs = io.find(
{
"type": "representation",
"parent": {"$in": list(subset_id_by_version_id.keys())}
},
{
"name": 1,
"parent": 1
}
)
repres_by_asset_id = {}
for repre_doc in repre_docs:
subset_id = subset_id_by_version_id[repre_doc["parent"]]
asset_id = subset_docs_by_id[subset_id]["parent"]
if asset_id not in repres_by_asset_id:
repres_by_asset_id[asset_id] = set()
repres_by_asset_id[asset_id].add(repre_doc["name"])
for repre_doc in self.content_repres.values():
version_doc = self.content_versions[repre_doc["parent"]]
subset_doc = self.content_subsets[version_doc["parent"]]
asset_id = subset_doc["parent"]
repre_names = (
repres_by_asset_id.get(asset_id) or []
)
if repre_doc["name"] not in repre_names:
validation_state.repre_ok = False
break
def _on_current_asset(self):
# Set initial asset as current.
asset_name = io.Session["AVALON_ASSET"]
index = self._assets_box.findText(
asset_name, QtCore.Qt.MatchFixedString
)
if index >= 0:
print("Setting asset to {}".format(asset_name))
self._assets_box.setCurrentIndex(index)
def _on_accept(self):
# Use None when not a valid value or when placeholder value
selected_asset = self._assets_box.get_valid_value()
selected_subset = self._subsets_box.get_valid_value()
selected_representation = self._representations_box.get_valid_value()
if selected_asset:
asset_doc = io.find_one({"type": "asset", "name": selected_asset})
asset_docs_by_id = {asset_doc["_id"]: asset_doc}
else:
asset_docs_by_id = self.content_assets
asset_docs_by_name = {
asset_doc["name"]: asset_doc
for asset_doc in asset_docs_by_id.values()
}
asset_ids = list(asset_docs_by_id.keys())
subset_query = {
"type": "subset",
"parent": {"$in": asset_ids}
}
if selected_subset:
subset_query["name"] = selected_subset
subset_docs = list(io.find(subset_query))
subset_ids = []
subset_docs_by_parent_and_name = collections.defaultdict(dict)
for subset in subset_docs:
subset_ids.append(subset["_id"])
parent_id = subset["parent"]
name = subset["name"]
subset_docs_by_parent_and_name[parent_id][name] = subset
# versions
version_docs = list(io.find({
"type": "version",
"parent": {"$in": subset_ids}
}, sort=[("name", -1)]))
hero_version_docs = list(io.find({
"type": "hero_version",
"parent": {"$in": subset_ids}
}))
version_ids = list()
version_docs_by_parent_id = {}
for version_doc in version_docs:
parent_id = version_doc["parent"]
if parent_id not in version_docs_by_parent_id:
version_ids.append(version_doc["_id"])
version_docs_by_parent_id[parent_id] = version_doc
hero_version_docs_by_parent_id = {}
for hero_version_doc in hero_version_docs:
version_ids.append(hero_version_doc["_id"])
parent_id = hero_version_doc["parent"]
hero_version_docs_by_parent_id[parent_id] = hero_version_doc
repre_docs = io.find({
"type": "representation",
"parent": {"$in": version_ids}
})
repre_docs_by_parent_id_by_name = collections.defaultdict(dict)
for repre_doc in repre_docs:
parent_id = repre_doc["parent"]
name = repre_doc["name"]
repre_docs_by_parent_id_by_name[parent_id][name] = repre_doc
for container in self._items:
container_repre_id = io.ObjectId(container["representation"])
container_repre = self.content_repres[container_repre_id]
container_repre_name = container_repre["name"]
container_version_id = container_repre["parent"]
container_version = self.content_versions[container_version_id]
container_subset_id = container_version["parent"]
container_subset = self.content_subsets[container_subset_id]
container_subset_name = container_subset["name"]
container_asset_id = container_subset["parent"]
container_asset = self.content_assets[container_asset_id]
container_asset_name = container_asset["name"]
if selected_asset:
asset_doc = asset_docs_by_name[selected_asset]
else:
asset_doc = asset_docs_by_name[container_asset_name]
subsets_by_name = subset_docs_by_parent_and_name[asset_doc["_id"]]
if selected_subset:
subset_doc = subsets_by_name[selected_subset]
else:
subset_doc = subsets_by_name[container_subset_name]
repre_doc = None
subset_id = subset_doc["_id"]
if container_version["type"] == "hero_version":
hero_version = hero_version_docs_by_parent_id.get(
subset_id
)
if hero_version:
_repres = repre_docs_by_parent_id_by_name.get(
hero_version["_id"]
)
if selected_representation:
repre_doc = _repres.get(selected_representation)
else:
repre_doc = _repres.get(container_repre_name)
if not repre_doc:
version_doc = version_docs_by_parent_id[subset_id]
version_id = version_doc["_id"]
repres_by_name = repre_docs_by_parent_id_by_name[version_id]
if selected_representation:
repre_doc = repres_by_name[selected_representation]
else:
repre_doc = repres_by_name[container_repre_name]
try:
api.switch(container, repre_doc)
except Exception:
msg = (
"Couldn't switch asset."
"See traceback for more information."
)
log.warning(msg, exc_info=True)
dialog = QtWidgets.QMessageBox(self)
dialog.setWindowTitle("Switch asset failed")
dialog.setText(
"Switch asset failed. Search console log for more details"
)
dialog.exec_()
self.switched.emit()
self.close()

View file

@ -0,0 +1,794 @@
import collections
import logging
from functools import partial
from Qt import QtWidgets, QtCore
from avalon import io, api, style
from avalon.vendor import qtawesome
from avalon.lib import HeroVersionType
from avalon.tools import lib as tools_lib
from openpype.modules import ModulesManager
from .switch_dialog import SwitchAssetDialog
from .model import InventoryModel
DEFAULT_COLOR = "#fb9c15"
log = logging.getLogger("SceneInventory")
class SceneInvetoryView(QtWidgets.QTreeView):
data_changed = QtCore.Signal()
hierarchy_view_changed = QtCore.Signal(bool)
def __init__(self, parent=None):
super(SceneInvetoryView, self).__init__(parent=parent)
# view settings
self.setIndentation(12)
self.setAlternatingRowColors(True)
self.setSortingEnabled(True)
self.setSelectionMode(self.ExtendedSelection)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._show_right_mouse_menu)
self._hierarchy_view = False
self._selected = None
manager = ModulesManager()
self.sync_server = manager.modules_by_name["sync_server"]
self.sync_enabled = self.sync_server.enabled
def _set_hierarchy_view(self, enabled):
if enabled == self._hierarchy_view:
return
self._hierarchy_view = enabled
self.hierarchy_view_changed.emit(enabled)
def _enter_hierarchy(self, items):
self._selected = set(i["objectName"] for i in items)
self._set_hierarchy_view(True)
self.data_changed.emit()
self.expandToDepth(1)
self.setStyleSheet("""
QTreeView {
border-color: #fb9c15;
}
""")
def _leave_hierarchy(self):
self._set_hierarchy_view(False)
self.data_changed.emit()
self.setStyleSheet("QTreeView {}")
def _build_item_menu_for_selection(self, items, menu):
if not items:
return
repre_ids = []
for item in items:
item_id = io.ObjectId(item["representation"])
if item_id not in repre_ids:
repre_ids.append(item_id)
repre_docs = io.find(
{
"type": "representation",
"_id": {"$in": repre_ids}
},
{"parent": 1}
)
version_ids = []
for repre_doc in repre_docs:
version_id = repre_doc["parent"]
if version_id not in version_ids:
version_ids.append(version_id)
loaded_versions = io.find({
"_id": {"$in": version_ids},
"type": {"$in": ["version", "hero_version"]}
})
loaded_hero_versions = []
versions_by_parent_id = collections.defaultdict(list)
version_parents = []
for version in loaded_versions:
if version["type"] == "hero_version":
loaded_hero_versions.append(version)
else:
parent_id = version["parent"]
versions_by_parent_id[parent_id].append(version)
if parent_id not in version_parents:
version_parents.append(parent_id)
all_versions = io.find({
"type": {"$in": ["hero_version", "version"]},
"parent": {"$in": version_parents}
})
hero_versions = []
versions = []
for version in all_versions:
if version["type"] == "hero_version":
hero_versions.append(version)
else:
versions.append(version)
has_loaded_hero_versions = len(loaded_hero_versions) > 0
has_available_hero_version = len(hero_versions) > 0
has_outdated = False
for version in versions:
parent_id = version["parent"]
current_versions = versions_by_parent_id[parent_id]
for current_version in current_versions:
if current_version["name"] < version["name"]:
has_outdated = True
break
if has_outdated:
break
switch_to_versioned = None
if has_loaded_hero_versions:
def _on_switch_to_versioned(items):
repre_ids = []
for item in items:
item_id = io.ObjectId(item["representation"])
if item_id not in repre_ids:
repre_ids.append(item_id)
repre_docs = io.find(
{
"type": "representation",
"_id": {"$in": repre_ids}
},
{"parent": 1}
)
version_ids = []
version_id_by_repre_id = {}
for repre_doc in repre_docs:
version_id = repre_doc["parent"]
version_id_by_repre_id[repre_doc["_id"]] = version_id
if version_id not in version_ids:
version_ids.append(version_id)
hero_versions = io.find(
{
"_id": {"$in": version_ids},
"type": "hero_version"
},
{"version_id": 1}
)
version_ids = set()
for hero_version in hero_versions:
version_id = hero_version["version_id"]
version_ids.add(version_id)
hero_version_id = hero_version["_id"]
for _repre_id, current_version_id in (
version_id_by_repre_id.items()
):
if current_version_id == hero_version_id:
version_id_by_repre_id[_repre_id] = version_id
version_docs = io.find(
{
"_id": {"$in": list(version_ids)},
"type": "version"
},
{"name": 1}
)
version_name_by_id = {}
for version_doc in version_docs:
version_name_by_id[version_doc["_id"]] = \
version_doc["name"]
for item in items:
repre_id = io.ObjectId(item["representation"])
version_id = version_id_by_repre_id.get(repre_id)
version_name = version_name_by_id.get(version_id)
if version_name is not None:
try:
api.update(item, version_name)
except AssertionError:
self._show_version_error_dialog(
version_name, [item]
)
log.warning("Update failed", exc_info=True)
self.data_changed.emit()
update_icon = qtawesome.icon(
"fa.asterisk",
color=DEFAULT_COLOR
)
switch_to_versioned = QtWidgets.QAction(
update_icon,
"Switch to versioned",
menu
)
switch_to_versioned.triggered.connect(
lambda: _on_switch_to_versioned(items)
)
update_to_latest_action = None
if has_outdated or has_loaded_hero_versions:
# update to latest version
def _on_update_to_latest(items):
for item in items:
try:
api.update(item, -1)
except AssertionError:
self._show_version_error_dialog(None, [item])
log.warning("Update failed", exc_info=True)
self.data_changed.emit()
update_icon = qtawesome.icon(
"fa.angle-double-up",
color=DEFAULT_COLOR
)
update_to_latest_action = QtWidgets.QAction(
update_icon,
"Update to latest",
menu
)
update_to_latest_action.triggered.connect(
lambda: _on_update_to_latest(items)
)
change_to_hero = None
if has_available_hero_version:
# change to hero version
def _on_update_to_hero(items):
for item in items:
try:
api.update(item, HeroVersionType(-1))
except AssertionError:
self._show_version_error_dialog('hero', [item])
log.warning("Update failed", exc_info=True)
self.data_changed.emit()
# TODO change icon
change_icon = qtawesome.icon(
"fa.asterisk",
color="#00b359"
)
change_to_hero = QtWidgets.QAction(
change_icon,
"Change to hero",
menu
)
change_to_hero.triggered.connect(
lambda: _on_update_to_hero(items)
)
# set version
set_version_icon = qtawesome.icon("fa.hashtag", color=DEFAULT_COLOR)
set_version_action = QtWidgets.QAction(
set_version_icon,
"Set version",
menu
)
set_version_action.triggered.connect(
lambda: self._show_version_dialog(items))
# switch asset
switch_asset_icon = qtawesome.icon("fa.sitemap", color=DEFAULT_COLOR)
switch_asset_action = QtWidgets.QAction(
switch_asset_icon,
"Switch Asset",
menu
)
switch_asset_action.triggered.connect(
lambda: self._show_switch_dialog(items))
# remove
remove_icon = qtawesome.icon("fa.remove", color=DEFAULT_COLOR)
remove_action = QtWidgets.QAction(remove_icon, "Remove items", menu)
remove_action.triggered.connect(
lambda: self._show_remove_warning_dialog(items))
# add the actions
if switch_to_versioned:
menu.addAction(switch_to_versioned)
if update_to_latest_action:
menu.addAction(update_to_latest_action)
if change_to_hero:
menu.addAction(change_to_hero)
menu.addAction(set_version_action)
menu.addAction(switch_asset_action)
menu.addSeparator()
menu.addAction(remove_action)
self._handle_sync_server(menu, repre_ids)
def _handle_sync_server(self, menu, repre_ids):
"""
Adds actions for download/upload when SyncServer is enabled
Args:
menu (OptionMenu)
repre_ids (list) of object_ids
Returns:
(OptionMenu)
"""
if not self.sync_enabled:
return
menu.addSeparator()
download_icon = qtawesome.icon("fa.download", color=DEFAULT_COLOR)
download_active_action = QtWidgets.QAction(
download_icon,
"Download",
menu
)
download_active_action.triggered.connect(
lambda: self._add_sites(repre_ids, 'active_site'))
upload_icon = qtawesome.icon("fa.upload", color=DEFAULT_COLOR)
upload_remote_action = QtWidgets.QAction(
upload_icon,
"Upload",
menu
)
upload_remote_action.triggered.connect(
lambda: self._add_sites(repre_ids, 'remote_site'))
menu.addAction(download_active_action)
menu.addAction(upload_remote_action)
def _add_sites(self, repre_ids, side):
"""
(Re)sync all 'repre_ids' to specific site.
It checks if opposite site has fully available content to limit
accidents. (ReSync active when no remote >> losing active content)
Args:
repre_ids (list)
side (str): 'active_site'|'remote_site'
"""
project_name = io.Session["AVALON_PROJECT"]
active_site = self.sync_server.get_active_site(project_name)
remote_site = self.sync_server.get_remote_site(project_name)
repre_docs = io.find({
"type": "representation",
"_id": {"$in": repre_ids}
})
repre_docs_by_id = {
repre_doc["_id"]: repre_doc
for repre_doc in repre_docs
}
for repre_id in repre_ids:
repre_doc = repre_docs_by_id.get(repre_id)
if not repre_doc:
continue
progress = tools_lib.get_progress_for_repre(
repre_doc,
active_site,
remote_site
)
if side == "active_site":
# check opposite from added site, must be 1 or unable to sync
check_progress = progress[remote_site]
site = active_site
else:
check_progress = progress[active_site]
site = remote_site
if check_progress == 1:
self.sync_server.add_site(
project_name, repre_id, site, force=True
)
self.data_changed.emit()
def _build_item_menu(self, items=None):
"""Create menu for the selected items"""
if not items:
items = []
menu = QtWidgets.QMenu(self)
# add the actions
self._build_item_menu_for_selection(items, menu)
# These two actions should be able to work without selection
# expand all items
expandall_action = QtWidgets.QAction(menu, text="Expand all items")
expandall_action.triggered.connect(self.expandAll)
# collapse all items
collapse_action = QtWidgets.QAction(menu, text="Collapse all items")
collapse_action.triggered.connect(self.collapseAll)
menu.addAction(expandall_action)
menu.addAction(collapse_action)
custom_actions = self._get_custom_actions(containers=items)
if custom_actions:
submenu = QtWidgets.QMenu("Actions", self)
for action in custom_actions:
color = action.color or DEFAULT_COLOR
icon = qtawesome.icon("fa.%s" % action.icon, color=color)
action_item = QtWidgets.QAction(icon, action.label, submenu)
action_item.triggered.connect(
partial(self._process_custom_action, action, items))
submenu.addAction(action_item)
menu.addMenu(submenu)
# go back to flat view
if self._hierarchy_view:
back_to_flat_icon = qtawesome.icon("fa.list", color=DEFAULT_COLOR)
back_to_flat_action = QtWidgets.QAction(
back_to_flat_icon,
"Back to Full-View",
menu
)
back_to_flat_action.triggered.connect(self._leave_hierarchy)
# send items to hierarchy view
enter_hierarchy_icon = qtawesome.icon("fa.indent", color="#d8d8d8")
enter_hierarchy_action = QtWidgets.QAction(
enter_hierarchy_icon,
"Cherry-Pick (Hierarchy)",
menu
)
enter_hierarchy_action.triggered.connect(
lambda: self._enter_hierarchy(items))
if items:
menu.addAction(enter_hierarchy_action)
if self._hierarchy_view:
menu.addAction(back_to_flat_action)
return menu
def _get_custom_actions(self, containers):
"""Get the registered Inventory Actions
Args:
containers(list): collection of containers
Returns:
list: collection of filter and initialized actions
"""
def sorter(Plugin):
"""Sort based on order attribute of the plugin"""
return Plugin.order
# Fedd an empty dict if no selection, this will ensure the compat
# lookup always work, so plugin can interact with Scene Inventory
# reversely.
containers = containers or [dict()]
# Check which action will be available in the menu
Plugins = api.discover(api.InventoryAction)
compatible = [p() for p in Plugins if
any(p.is_compatible(c) for c in containers)]
return sorted(compatible, key=sorter)
def _process_custom_action(self, action, containers):
"""Run action and if results are returned positive update the view
If the result is list or dict, will select view items by the result.
Args:
action (InventoryAction): Inventory Action instance
containers (list): Data of currently selected items
Returns:
None
"""
result = action.process(containers)
if result:
self.data_changed.emit()
if isinstance(result, (list, set)):
self._select_items_by_action(result)
if isinstance(result, dict):
self._select_items_by_action(
result["objectNames"], result["options"]
)
def _select_items_by_action(self, object_names, options=None):
"""Select view items by the result of action
Args:
object_names (list or set): A list/set of container object name
options (dict): GUI operation options.
Returns:
None
"""
options = options or dict()
if options.get("clear", True):
self.clearSelection()
object_names = set(object_names)
if (
self._hierarchy_view
and not self._selected.issuperset(object_names)
):
# If any container not in current cherry-picked view, update
# view before selecting them.
self._selected.update(object_names)
self.data_changed.emit()
model = self.model()
selection_model = self.selectionModel()
select_mode = {
"select": selection_model.Select,
"deselect": selection_model.Deselect,
"toggle": selection_model.Toggle,
}[options.get("mode", "select")]
for item in tools_lib.iter_model_rows(model, 0):
item = item.data(InventoryModel.ItemRole)
if item.get("isGroupNode"):
continue
name = item.get("objectName")
if name in object_names:
self.scrollTo(item) # Ensure item is visible
flags = select_mode | selection_model.Rows
selection_model.select(item, flags)
object_names.remove(name)
if len(object_names) == 0:
break
def _show_right_mouse_menu(self, pos):
"""Display the menu when at the position of the item clicked"""
globalpos = self.viewport().mapToGlobal(pos)
if not self.selectionModel().hasSelection():
print("No selection")
# Build menu without selection, feed an empty list
menu = self._build_item_menu()
menu.exec_(globalpos)
return
active = self.currentIndex() # index under mouse
active = active.sibling(active.row(), 0) # get first column
# move index under mouse
indices = self.get_indices()
if active in indices:
indices.remove(active)
indices.append(active)
# Extend to the sub-items
all_indices = self._extend_to_children(indices)
items = [dict(i.data(InventoryModel.ItemRole)) for i in all_indices
if i.parent().isValid()]
if self._hierarchy_view:
# Ensure no group item
items = [n for n in items if not n.get("isGroupNode")]
menu = self._build_item_menu(items)
menu.exec_(globalpos)
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.
Args:
indices (list): The indices to extend.
Returns:
list: The children indices
"""
def get_children(i):
model = i.model()
rows = model.rowCount(parent=i)
for row in range(rows):
child = model.index(row, 0, parent=i)
yield child
subitems = set()
for i in indices:
valid_parent = i.parent().isValid()
if valid_parent and i not in subitems:
subitems.add(i)
if self._hierarchy_view:
# Assume this is a group item
for child in get_children(i):
subitems.add(child)
else:
# is top level item
for child in get_children(i):
subitems.add(child)
return list(subitems)
def _show_version_dialog(self, items):
"""Create a dialog with the available versions for the selected file
Args:
items (list): list of items to run the "set_version" for
Returns:
None
"""
active = items[-1]
# Get available versions for active representation
representation_id = io.ObjectId(active["representation"])
representation = io.find_one({"_id": representation_id})
version = io.find_one({
"_id": representation["parent"]
})
versions = list(io.find(
{
"parent": version["parent"],
"type": "version"
},
sort=[("name", 1)]
))
hero_version = io.find_one({
"parent": version["parent"],
"type": "hero_version"
})
if hero_version:
_version_id = hero_version["version_id"]
for _version in versions:
if _version["_id"] != _version_id:
continue
hero_version["name"] = HeroVersionType(
_version["name"]
)
hero_version["data"] = _version["data"]
break
# Get index among the listed versions
current_item = None
current_version = active["version"]
if isinstance(current_version, HeroVersionType):
current_item = hero_version
else:
for version in versions:
if version["name"] == current_version:
current_item = version
break
all_versions = []
if hero_version:
all_versions.append(hero_version)
all_versions.extend(reversed(versions))
if current_item:
index = all_versions.index(current_item)
else:
index = 0
versions_by_label = dict()
labels = []
for version in all_versions:
is_hero = version["type"] == "hero_version"
label = tools_lib.format_version(version["name"], is_hero)
labels.append(label)
versions_by_label[label] = version["name"]
label, state = QtWidgets.QInputDialog.getItem(
self,
"Set version..",
"Set version number to",
labels,
current=index,
editable=False
)
if not state:
return
if label:
version = versions_by_label[label]
for item in items:
try:
api.update(item, version)
except AssertionError:
self._show_version_error_dialog(version, [item])
log.warning("Update failed", exc_info=True)
# refresh model when done
self.data_changed.emit()
def _show_switch_dialog(self, items):
"""Display Switch dialog"""
dialog = SwitchAssetDialog(self, items)
dialog.switched.connect(self.data_changed.emit)
dialog.show()
def _show_remove_warning_dialog(self, items):
"""Prompt a dialog to inform the user the action will remove items"""
accept = QtWidgets.QMessageBox.Ok
buttons = accept | QtWidgets.QMessageBox.Cancel
state = QtWidgets.QMessageBox.question(
self,
"Are you sure?",
"Are you sure you want to remove {} item(s)".format(len(items)),
buttons=buttons,
defaultButton=accept
)
if state != accept:
return
for item in items:
api.remove(item)
self.data_changed.emit()
def _show_version_error_dialog(self, version, items):
"""Shows QMessageBox when version switch doesn't work
Args:
version: str or int or None
"""
if not version:
version_str = "latest"
elif version == "hero":
version_str = "hero"
elif isinstance(version, int):
version_str = "v{:03d}".format(version)
else:
version_str = version
dialog = QtWidgets.QMessageBox()
dialog.setIcon(QtWidgets.QMessageBox.Warning)
dialog.setStyleSheet(style.load_stylesheet())
dialog.setWindowTitle("Update failed")
switch_btn = dialog.addButton(
"Switch Asset",
QtWidgets.QMessageBox.ActionRole
)
switch_btn.clicked.connect(lambda: self._show_switch_dialog(items))
dialog.addButton(QtWidgets.QMessageBox.Cancel)
msg = (
"Version update to '{}' failed as representation doesn't exist."
"\n\nPlease update to version with a valid representation"
" OR \n use 'Switch Asset' button to change asset."
).format(version_str)
dialog.setText(msg)
dialog.exec_()

View file

@ -0,0 +1,51 @@
from Qt import QtWidgets, QtCore
class SearchComboBox(QtWidgets.QComboBox):
"""Searchable ComboBox with empty placeholder value as first value"""
def __init__(self, parent=None):
super(SearchComboBox, self).__init__(parent)
self.setEditable(True)
self.setInsertPolicy(self.NoInsert)
# Apply completer settings
completer = self.completer()
completer.setCompletionMode(completer.PopupCompletion)
completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
# Force style sheet on popup menu
# It won't take the parent stylesheet for some reason
# todo: better fix for completer popup stylesheet
# if module.window:
# popup = completer.popup()
# popup.setStyleSheet(module.window.styleSheet())
def set_placeholder(self, placeholder):
self.lineEdit().setPlaceholderText(placeholder)
def populate(self, items):
self.clear()
self.addItems([""]) # ensure first item is placeholder
self.addItems(items)
def get_valid_value(self):
"""Return the current text if it's a valid value else None
Note: The empty placeholder value is valid and returns as ""
"""
text = self.currentText()
lookup = set(self.itemText(i) for i in range(self.count()))
if text not in lookup:
return None
return text or None
def set_valid_value(self, value):
"""Try to locate 'value' and pre-select it in dropdown."""
index = self.findText(value)
if index > -1:
self.setCurrentIndex(index)

View file

@ -0,0 +1,203 @@
import os
import sys
from Qt import QtWidgets, QtCore
from avalon.vendor import qtawesome
from avalon import io, api
from openpype import style
from openpype.tools.utils.delegates import VersionDelegate
from openpype.tools.utils.lib import (
qt_app_context,
preserve_expanded_rows,
preserve_selection,
FamilyConfigCache
)
from .model import (
InventoryModel,
FilterProxyModel
)
from .view import SceneInvetoryView
module = sys.modules[__name__]
module.window = None
class SceneInventoryWindow(QtWidgets.QDialog):
"""Scene Inventory window"""
def __init__(self, parent=None):
super(SceneInventoryWindow, self).__init__(parent)
if not parent:
self.setWindowFlags(
self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint
)
project_name = os.getenv("AVALON_PROJECT") or "<Project not set>"
self.setWindowTitle("Scene Inventory 1.0 - {}".format(project_name))
self.setObjectName("SceneInventory")
# Maya only property
self.setProperty("saveWindowPref", True)
self.resize(1100, 480)
# region control
filter_label = QtWidgets.QLabel("Search", self)
text_filter = QtWidgets.QLineEdit(self)
outdated_only_checkbox = QtWidgets.QCheckBox(
"Filter to outdated", self
)
outdated_only_checkbox.setToolTip("Show outdated files only")
outdated_only_checkbox.setChecked(False)
icon = qtawesome.icon("fa.refresh", color="white")
refresh_button = QtWidgets.QPushButton(self)
refresh_button.setIcon(icon)
control_layout = QtWidgets.QHBoxLayout()
control_layout.addWidget(filter_label)
control_layout.addWidget(text_filter)
control_layout.addWidget(outdated_only_checkbox)
control_layout.addWidget(refresh_button)
# endregion control
family_config_cache = FamilyConfigCache(io)
model = InventoryModel(family_config_cache)
proxy = FilterProxyModel()
proxy.setSourceModel(model)
proxy.setDynamicSortFilter(True)
proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
view = SceneInvetoryView(self)
view.setModel(proxy)
# set some nice default widths for the view
view.setColumnWidth(0, 250) # name
view.setColumnWidth(1, 55) # version
view.setColumnWidth(2, 55) # count
view.setColumnWidth(3, 150) # family
view.setColumnWidth(4, 100) # namespace
# apply delegates
version_delegate = VersionDelegate(io, self)
column = model.Columns.index("version")
view.setItemDelegateForColumn(column, version_delegate)
layout = QtWidgets.QVBoxLayout(self)
layout.addLayout(control_layout)
layout.addWidget(view)
# signals
text_filter.textChanged.connect(self._on_text_filter_change)
outdated_only_checkbox.stateChanged.connect(
self._on_outdated_state_change
)
view.hierarchy_view_changed.connect(
self._on_hiearchy_view_change
)
view.data_changed.connect(self.refresh)
refresh_button.clicked.connect(self.refresh)
self._outdated_only_checkbox = outdated_only_checkbox
self._view = view
self._model = model
self._proxy = proxy
self._version_delegate = version_delegate
self._family_config_cache = family_config_cache
self._first_show = True
family_config_cache.refresh()
def showEvent(self, event):
super(SceneInventoryWindow, self).showEvent(event)
if self._first_show:
self._first_show = False
self.setStyleSheet(style.load_stylesheet())
def keyPressEvent(self, event):
"""Custom keyPressEvent.
Override keyPressEvent to do nothing so that Maya's panels won't
take focus when pressing "SHIFT" whilst mouse is over viewport or
outliner. This way users don't accidently perform Maya commands
whilst trying to name an instance.
"""
def refresh(self, items=None):
with preserve_expanded_rows(
tree_view=self._view,
role=self._model.UniqueRole
):
with preserve_selection(
tree_view=self._view,
role=self._model.UniqueRole,
current_index=False
):
kwargs = {"items": items}
# TODO do not touch view's inner attribute
if self._view._hierarchy_view:
kwargs["selected"] = self._view._selected
self._model.refresh(**kwargs)
def _on_hiearchy_view_change(self, enabled):
self._proxy.set_hierarchy_view(enabled)
self._model.set_hierarchy_view(enabled)
def _on_text_filter_change(self, text_filter):
self._proxy.setFilterRegExp(text_filter)
def _on_outdated_state_change(self):
self._proxy.set_filter_outdated(
self._outdated_only_checkbox.isChecked()
)
def show(root=None, debug=False, parent=None, items=None):
"""Display Scene Inventory GUI
Arguments:
debug (bool, optional): Run in debug-mode,
defaults to False
parent (QtCore.QObject, optional): When provided parent the interface
to this QObject.
items (list) of dictionaries - for injection of items for standalone
testing
"""
try:
module.window.close()
del module.window
except (RuntimeError, AttributeError):
pass
if debug is True:
io.install()
if not os.environ.get("AVALON_PROJECT"):
any_project = next(
project for project in io.projects()
if project.get("active", True) is not False
)
api.Session["AVALON_PROJECT"] = any_project["name"]
else:
api.Session["AVALON_PROJECT"] = os.environ.get("AVALON_PROJECT")
with qt_app_context():
window = SceneInventoryWindow(parent)
window.show()
window.refresh(items=items)
module.window = window
# Pull window to the front.
module.window.raise_()
module.window.activateWindow()

View file

@ -154,21 +154,20 @@ class HostToolsHelper:
def get_scene_inventory_tool(self, parent):
"""Create, cache and return scene inventory tool window."""
if self._scene_inventory_tool is None:
from avalon.tools.sceneinventory.app import Window
from openpype.tools.sceneinventory import SceneInventoryWindow
scene_inventory_window = Window(parent=parent or self._parent)
scene_inventory_window = SceneInventoryWindow(
parent=parent or self._parent
)
self._scene_inventory_tool = scene_inventory_window
return self._scene_inventory_tool
def show_scene_inventory(self, parent=None):
"""Show tool maintain loaded containers."""
from avalon import style
scene_inventory_tool = self.get_scene_inventory_tool(parent)
scene_inventory_tool.show()
scene_inventory_tool.refresh()
scene_inventory_tool.setStyleSheet(style.load_stylesheet())
# Pull window to the front.
scene_inventory_tool.raise_()