refactor of scene inventory view

This commit is contained in:
Jakub Trllo 2024-05-29 14:57:00 +02:00
parent 8366d2e8b4
commit e598e5fef3
8 changed files with 1146 additions and 1027 deletions

View file

@ -1,14 +1,14 @@
import ayon_api
from ayon_core.lib.events import QueuedEventSystem
from ayon_core.host import ILoadHost
from ayon_core.host import HostBase, ILoadHost
from ayon_core.pipeline import (
registered_host,
get_current_context,
)
from ayon_core.tools.common_models import HierarchyModel
from .models import SiteSyncModel
from .models import SiteSyncModel, ContainersModel
class SceneInventoryController:
@ -28,11 +28,15 @@ class SceneInventoryController:
self._current_folder_id = None
self._current_folder_set = False
self._containers_model = ContainersModel(self)
self._sitesync_model = SiteSyncModel(self)
# Switch dialog requirements
self._hierarchy_model = HierarchyModel(self)
self._event_system = self._create_event_system()
def get_host(self) -> HostBase:
return self._host
def emit_event(self, topic, data=None, source=None):
if data is None:
data = {}
@ -47,6 +51,7 @@ class SceneInventoryController:
self._current_folder_id = None
self._current_folder_set = False
self._containers_model.reset()
self._sitesync_model.reset()
self._hierarchy_model.reset()
@ -80,13 +85,26 @@ class SceneInventoryController:
self._current_folder_set = True
return self._current_folder_id
# Containers methods
def get_containers(self):
host = self._host
if isinstance(host, ILoadHost):
return list(host.get_containers())
elif hasattr(host, "ls"):
return list(host.ls())
return []
return self._containers_model.get_containers()
def get_containers_by_item_ids(self, item_ids):
return self._containers_model.get_containers_by_item_ids(item_ids)
def get_container_items(self):
return self._containers_model.get_container_items()
def get_container_items_by_id(self, item_ids):
return self._containers_model.get_container_items_by_id(item_ids)
def get_representation_info_items(self, representation_ids):
return self._containers_model.get_representation_info_items(
representation_ids
)
def get_version_items(self, product_ids):
return self._containers_model.get_version_items(product_ids)
# Site Sync methods
def is_sitesync_enabled(self):

View file

@ -1,38 +1,10 @@
import numbers
import ayon_api
from ayon_core.pipeline import HeroVersionType
from ayon_core.tools.utils.models import TreeModel
from ayon_core.tools.utils.lib import format_version
from qtpy import QtWidgets, QtCore, QtGui
from .model import VERSION_LABEL_ROLE
class VersionDelegate(QtWidgets.QStyledItemDelegate):
"""A delegate that display version integer formatted as version string."""
version_changed = QtCore.Signal()
first_run = False
lock = False
def __init__(self, controller, *args, **kwargs):
self._controller = controller
super(VersionDelegate, self).__init__(*args, **kwargs)
def get_project_name(self):
return self._controller.get_current_project_name()
def displayText(self, value, locale):
if isinstance(value, HeroVersionType):
return format_version(value)
if not isinstance(value, numbers.Integral):
# For cases where no version is resolved like NOT FOUND cases
# where a representation might not exist in current database
return
return format_version(value)
def paint(self, painter, option, index):
fg_color = index.data(QtCore.Qt.ForegroundRole)
if fg_color:
@ -44,7 +16,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
fg_color = None
if not fg_color:
return super(VersionDelegate, self).paint(painter, option, index)
return super().paint(painter, option, index)
if option.widget:
style = option.widget.style()
@ -60,9 +32,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
painter.save()
text = self.displayText(
index.data(QtCore.Qt.DisplayRole), option.locale
)
text = index.data(VERSION_LABEL_ROLE)
pen = painter.pen()
pen.setColor(fg_color)
painter.setPen(pen)
@ -82,77 +52,3 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
)
painter.restore()
def createEditor(self, parent, option, index):
item = index.data(TreeModel.ItemRole)
if item.get("isGroup") or item.get("isMerged"):
return
editor = QtWidgets.QComboBox(parent)
def commit_data():
if not self.first_run:
self.commitData.emit(editor) # Update model data
self.version_changed.emit() # Display model data
editor.currentIndexChanged.connect(commit_data)
self.first_run = True
self.lock = False
return editor
def setEditorData(self, editor, index):
if self.lock:
# Only set editor data once per delegation
return
editor.clear()
# Current value of the index
item = index.data(TreeModel.ItemRole)
value = index.data(QtCore.Qt.DisplayRole)
project_name = self.get_project_name()
# Add all available versions to the editor
product_id = item["version_entity"]["productId"]
version_entities = list(sorted(
ayon_api.get_versions(
project_name, product_ids={product_id}, active=True
),
key=lambda item: abs(item["version"])
))
selected = None
items = []
is_hero_version = value < 0
for version_entity in version_entities:
version = version_entity["version"]
label = format_version(version)
item = QtGui.QStandardItem(label)
item.setData(version_entity, QtCore.Qt.UserRole)
items.append(item)
if (
version == value
or is_hero_version and version < 0
):
selected = item
# Reverse items so latest versions be upper
items.reverse()
for item in items:
editor.model().appendRow(item)
index = 0
if selected:
index = selected.row()
# Will trigger index-change signal
editor.setCurrentIndex(index)
self.first_run = False
self.lock = True
def setModelData(self, editor, model, index):
"""Apply the integer version back in the model"""
version = editor.itemData(editor.currentIndex())
model.setData(index, version["name"])

View file

@ -1,57 +1,110 @@
import re
import logging
import uuid
from collections import defaultdict
import collections
import ayon_api
from qtpy import QtCore, QtGui
import qtawesome
from ayon_core.pipeline import (
get_current_project_name,
HeroVersionType,
)
from ayon_core.style import get_default_entity_icon_color
from ayon_core.tools.utils import get_qt_icon
from ayon_core.tools.utils.models import TreeModel, Item
from ayon_core.tools.utils.lib import format_version
ITEM_ID_ROLE = QtCore.Qt.UserRole + 1
NAME_COLOR_ROLE = QtCore.Qt.UserRole + 2
COUNT_ROLE = QtCore.Qt.UserRole + 3
IS_CONTAINER_ITEM_ROLE = QtCore.Qt.UserRole + 4
VERSION_IS_LATEST_ROLE = QtCore.Qt.UserRole + 5
VERSION_VALUE_ROLE = QtCore.Qt.UserRole + 6
VERSION_LABEL_ROLE = QtCore.Qt.UserRole + 7
VERSION_COLOR_ROLE = QtCore.Qt.UserRole + 8
STATUS_NAME_ROLE = QtCore.Qt.UserRole + 9
STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 10
PRODUCT_ID_ROLE = QtCore.Qt.UserRole + 11
PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 12
PRODUCT_TYPE_ICON_ROLE = QtCore.Qt.UserRole + 13
PRODUCT_GROUP_NAME_ROLE = QtCore.Qt.UserRole + 14
PRODUCT_GROUP_ICON_ROLE = QtCore.Qt.UserRole + 15
LOADER_NAME_ROLE = QtCore.Qt.UserRole + 16
OBJECT_NAME_ROLE = QtCore.Qt.UserRole + 17
ACTIVE_SITE_PROGRESS_ROLE = QtCore.Qt.UserRole + 18
REMOTE_SITE_PROGRESS_ROLE = QtCore.Qt.UserRole + 19
ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 20
REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 21
# This value hold unique value of container that should be used to identify
# containers inbetween refresh.
ITEM_UNIQUE_NAME_ROLE = QtCore.Qt.UserRole + 22
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
class InventoryModel(TreeModel):
class InventoryModel(QtGui.QStandardItemModel):
"""The model for the inventory"""
Columns = [
column_labels = [
"Name",
"version",
"count",
"productType",
"group",
"loader",
"objectName",
"active_site",
"remote_site",
"Version",
"Count",
"Product type",
"Group",
"Loader",
"Object name",
"Active site",
"Remote site",
]
active_site_col = Columns.index("active_site")
remote_site_col = Columns.index("remote_site")
name_col = column_labels.index("Name")
version_col = column_labels.index("Version")
count_col = column_labels.index("Count")
product_type_col = column_labels.index("Product type")
product_group_col = column_labels.index("Group")
loader_col = column_labels.index("Loader")
object_name_col = column_labels.index("Object name")
active_site_col = column_labels.index("Active site")
remote_site_col = column_labels.index("Remote site")
display_role_by_column = {
name_col: QtCore.Qt.DisplayRole,
version_col: VERSION_LABEL_ROLE,
count_col: COUNT_ROLE,
# 3: STATUS_NAME_ROLE,
product_type_col: PRODUCT_TYPE_ROLE,
product_group_col: PRODUCT_GROUP_NAME_ROLE,
loader_col: LOADER_NAME_ROLE,
object_name_col: OBJECT_NAME_ROLE,
active_site_col: ACTIVE_SITE_PROGRESS_ROLE,
remote_site_col: REMOTE_SITE_PROGRESS_ROLE,
}
decoration_role_by_column = {
name_col: QtCore.Qt.DecorationRole,
product_type_col: PRODUCT_TYPE_ICON_ROLE,
product_group_col: PRODUCT_GROUP_ICON_ROLE,
active_site_col: ACTIVE_SITE_ICON_ROLE,
remote_site_col: REMOTE_SITE_ICON_ROLE,
}
foreground_role_by_column = {
version_col: VERSION_COLOR_ROLE,
name_col: NAME_COLOR_ROLE,
}
width_by_column = {
name_col: 250,
version_col: 55,
count_col: 55,
product_type_col: 150,
product_group_col: 120,
loader_col: 150,
}
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, controller, parent=None):
super(InventoryModel, self).__init__(parent)
super().__init__(parent)
self.setColumnCount(len(self.column_labels))
for idx, label in enumerate(self.column_labels):
self.setHeaderData(idx, QtCore.Qt.Horizontal, label)
self.log = logging.getLogger(self.__class__.__name__)
self._controller = controller
@ -60,103 +113,201 @@ class InventoryModel(TreeModel):
self._default_icon_color = get_default_entity_icon_color()
site_icons = self._controller.get_site_provider_icons()
self._site_icons = {
provider: get_qt_icon(icon_def)
for provider, icon_def in site_icons.items()
}
def outdated(self, item):
return item.get("isOutdated", True)
def refresh(self, selected=None):
"""Refresh the model"""
# for debugging or testing, injecting items from outside
container_items = self._controller.get_container_items()
self._clear_items()
items_by_repre_id = {}
for container_item in container_items:
# if (
# selected is not None
# and container_item.item_id not in selected
# ):
# continue
repre_id = container_item.representation_id
items = items_by_repre_id.setdefault(repre_id, [])
items.append(container_item)
repre_id = set(items_by_repre_id.keys())
repre_info_by_id = self._controller.get_representation_info_items(
repre_id
)
product_ids = {
repre_info.product_id
for repre_info in repre_info_by_id.values()
}
version_items_by_product_id = self._controller.get_version_items(
product_ids
)
# SiteSync addon information
progress_by_id = self._controller.get_representations_site_progress(
repre_id
)
sites_info = self._controller.get_sites_information()
site_icons = {
provider: get_qt_icon(icon_def)
for provider, icon_def in (
self._controller.get_site_provider_icons().items()
)
}
group_item_icon = qtawesome.icon(
"fa.folder", color=self._default_icon_color
)
valid_item_icon = qtawesome.icon(
"fa.file-o", color=self._default_icon_color
)
invalid_item_icon = qtawesome.icon(
"fa.exclamation-circle", color=self._default_icon_color
)
group_icon = qtawesome.icon(
"fa.object-group", color=self._default_icon_color
)
product_type_icon = qtawesome.icon(
"fa.folder", color="#0091B2"
)
group_item_font = QtGui.QFont()
group_item_font.setBold(True)
active_site_icon = site_icons.get(sites_info["active_site_provider"])
remote_site_icon = site_icons.get(sites_info["remote_site_provider"])
root_item = self.invisibleRootItem()
group_items = []
for repre_id, container_items in items_by_repre_id.items():
repre_info = repre_info_by_id[repre_id]
version_label = "N/A"
version_color = None
version_value = None
is_latest = False
if not repre_info.is_valid:
group_name = "< Entity N/A >"
item_icon = invalid_item_icon
else:
group_name = "{}_{}: ({})".format(
repre_info.folder_path.rsplit("/")[-1],
repre_info.product_name,
repre_info.representation_name
)
item_icon = valid_item_icon
version_items = (
version_items_by_product_id[repre_info.product_id]
)
version_item = version_items[repre_info.version_id]
version_value = version_item.version
if version_value < 0:
version_value = HeroVersionType(version_value)
version_label = format_version(version_value)
is_latest = version_item.is_latest
if not is_latest:
version_color = self.OUTDATED_COLOR
container_model_items = []
for container_item in container_items:
unique_name = (
repre_info.representation_name
+ container_item.object_name or "<none>"
)
item = QtGui.QStandardItem()
item.setColumnCount(root_item.columnCount())
item.setData(container_item.namespace, QtCore.Qt.DisplayRole)
item.setData(self.GRAYOUT_COLOR, NAME_COLOR_ROLE)
item.setData(self.GRAYOUT_COLOR, VERSION_COLOR_ROLE)
item.setData(item_icon, QtCore.Qt.DecorationRole)
item.setData(repre_info.product_id, PRODUCT_ID_ROLE)
item.setData(container_item.item_id, ITEM_ID_ROLE)
item.setData(version_value, VERSION_VALUE_ROLE)
item.setData(version_label, VERSION_LABEL_ROLE)
item.setData(True, IS_CONTAINER_ITEM_ROLE)
item.setData(unique_name, ITEM_UNIQUE_NAME_ROLE)
container_model_items.append(item)
if not container_model_items:
continue
progress = progress_by_id[repre_id]
active_site_progress = "{}%".format(
max(progress["active_site"], 0) * 100
)
remote_site_progress = "{}%".format(
max(progress["remote_site"], 0) * 100
)
group_item = QtGui.QStandardItem()
group_item.setColumnCount(root_item.columnCount())
group_item.setData(group_name, QtCore.Qt.DisplayRole)
group_item.setData(group_name, ITEM_UNIQUE_NAME_ROLE)
group_item.setData(group_item_icon, QtCore.Qt.DecorationRole)
group_item.setData(group_item_font, QtCore.Qt.FontRole)
group_item.setData(repre_info.product_id, PRODUCT_ID_ROLE)
group_item.setData(repre_info.product_type, PRODUCT_TYPE_ROLE)
group_item.setData(product_type_icon, PRODUCT_TYPE_ICON_ROLE)
group_item.setData(is_latest, VERSION_IS_LATEST_ROLE)
group_item.setData(version_label, VERSION_LABEL_ROLE)
group_item.setData(len(container_items), COUNT_ROLE)
group_item.setData(
active_site_progress, ACTIVE_SITE_PROGRESS_ROLE
)
group_item.setData(
remote_site_progress, REMOTE_SITE_PROGRESS_ROLE
)
group_item.setData(active_site_icon, ACTIVE_SITE_ICON_ROLE)
group_item.setData(remote_site_icon, REMOTE_SITE_ICON_ROLE)
group_item.setData(False, IS_CONTAINER_ITEM_ROLE)
if version_color is not None:
group_item.setData(version_color, VERSION_COLOR_ROLE)
if repre_info.product_group:
group_item.setData(
repre_info.product_group, PRODUCT_GROUP_NAME_ROLE
)
group_item.setData(group_icon, PRODUCT_GROUP_ICON_ROLE)
group_item.appendRows(container_model_items)
group_items.append(group_item)
if group_items:
root_item.appendRows(group_items)
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def data(self, index, role):
if not index.isValid():
return
item = index.internalPointer()
col = index.column()
if role == QtCore.Qt.DisplayRole:
role = self.display_role_by_column.get(col)
if role is None:
print(col, role)
return None
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
elif role == QtCore.Qt.DecorationRole:
role = self.decoration_role_by_column.get(col)
if role is None:
return None
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
elif role == QtCore.Qt.ForegroundRole:
role = self.foreground_role_by_column.get(col)
if role is None:
return None
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 col != 0:
index = self.index(index.row(), 0, index.parent())
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", self._default_icon_color)
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:
# Product type icon
return item.get("productTypeIcon", None)
column_name = self.Columns[index.column()]
if column_name == "group" and item.get("group"):
return qtawesome.icon("fa.object-group",
color=get_default_entity_icon_color())
if item.get("isGroupNode"):
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)
return super().data(index, role)
def set_hierarchy_view(self, state):
"""Set whether to display products in hierarchy view."""
@ -165,299 +316,21 @@ class InventoryModel(TreeModel):
if state != self._hierarchy_view:
self._hierarchy_view = state
def refresh(self, selected=None, containers=None):
"""Refresh the model"""
def get_outdated_item_ids(self):
return set()
# for debugging or testing, injecting items from outside
if containers is None:
containers = self._controller.get_containers()
self.clear()
if not selected or not self._hierarchy_view:
self._add_containers(containers)
return
# Filter by cherry-picked items
self._add_containers((
container
for container in containers
if container["objectName"] in selected
))
def _add_containers(self, containers, 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:
containers (generator): Container items.
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
"""
project_name = get_current_project_name()
self.beginResetModel()
# Group by representation
grouped = defaultdict(lambda: {"containers": list()})
for container in containers:
repre_id = container["representation"]
grouped[repre_id]["containers"].append(container)
(
repres_by_id,
versions_by_id,
products_by_id,
folders_by_id,
) = self._query_entities(project_name, set(grouped.keys()))
# Add to model
not_found = defaultdict(list)
not_found_ids = []
for repre_id, group_dict in sorted(grouped.items()):
group_containers = group_dict["containers"]
representation = repres_by_id.get(repre_id)
if not representation:
not_found["representation"].extend(group_containers)
not_found_ids.append(repre_id)
continue
version_entity = versions_by_id.get(representation["versionId"])
if not version_entity:
not_found["version"].extend(group_containers)
not_found_ids.append(repre_id)
continue
product_entity = products_by_id.get(version_entity["productId"])
if not product_entity:
not_found["product"].extend(group_containers)
not_found_ids.append(repre_id)
continue
folder_entity = folders_by_id.get(product_entity["folderId"])
if not folder_entity:
not_found["folder"].extend(group_containers)
not_found_ids.append(repre_id)
continue
group_dict.update({
"representation": representation,
"version": version_entity,
"product": product_entity,
"folder": folder_entity
})
for _repre_id in not_found_ids:
grouped.pop(_repre_id)
for where, group_containers 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_containers)
group_node["isGroupNode"] = False
group_node["isNotSet"] = True
self.add_child(group_node, parent=parent)
for container in group_containers:
item_node = Item()
item_node.update(container)
item_node["Name"] = container.get("objectName", "NO NAME")
item_node["isNotFound"] = True
self.add_child(item_node, parent=group_node)
# TODO Use product icons
product_type_icon = qtawesome.icon(
"fa.folder", color="#0091B2"
)
# Prepare site sync specific data
progress_by_id = self._controller.get_representations_site_progress(
set(grouped.keys())
)
sites_info = self._controller.get_sites_information()
# Query the highest available version so the model can know
# whether current version is currently up-to-date.
highest_version_by_product_id = ayon_api.get_last_versions(
project_name,
product_ids={
group["version"]["productId"] for group in grouped.values()
},
fields={"productId", "version"}
)
# Map value to `version` key
highest_version_by_product_id = {
product_id: version["version"]
for product_id, version in highest_version_by_product_id.items()
}
for repre_id, group_dict in sorted(grouped.items()):
group_containers = group_dict["containers"]
repre_entity = group_dict["representation"]
version_entity = group_dict["version"]
folder_entity = group_dict["folder"]
product_entity = group_dict["product"]
product_type = product_entity["productType"]
# create the group header
group_node = Item()
group_node["Name"] = "{}_{}: ({})".format(
folder_entity["name"],
product_entity["name"],
repre_entity["name"]
)
group_node["representation"] = repre_id
# Detect hero version type
version = version_entity["version"]
if version < 0:
version = HeroVersionType(version)
group_node["version"] = version
# Check if the version is outdated.
# Hero versions are never considered to be outdated.
is_outdated = False
if not isinstance(version, HeroVersionType):
last_version = highest_version_by_product_id.get(
version_entity["productId"])
if last_version is not None:
is_outdated = version_entity["version"] != last_version
group_node["isOutdated"] = is_outdated
group_node["productType"] = product_type or ""
group_node["productTypeIcon"] = product_type_icon
group_node["count"] = len(group_containers)
group_node["isGroupNode"] = True
group_node["group"] = product_entity["attrib"].get("productGroup")
# Site sync specific data
progress = progress_by_id[repre_id]
group_node.update(sites_info)
group_node["active_site_progress"] = progress["active_site"]
group_node["remote_site_progress"] = progress["remote_site"]
self.add_child(group_node, parent=parent)
for container in group_containers:
item_node = Item()
item_node.update(container)
# store the current version on the item
item_node["version"] = version_entity["version"]
item_node["version_entity"] = version_entity
# 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"] = container["namespace"]
self.add_child(item_node, parent=group_node)
self.endResetModel()
return self._root_item
def _query_entities(self, project_name, repre_ids):
"""Query entities for representations from containers.
Returns:
tuple[dict, dict, dict, dict]: Representation, version, product
and folder documents by id.
"""
repres_by_id = {}
versions_by_id = {}
products_by_id = {}
folders_by_id = {}
output = (
repres_by_id,
versions_by_id,
products_by_id,
folders_by_id,
)
filtered_repre_ids = set()
for repre_id in repre_ids:
# Filter out invalid representation ids
# NOTE: This is added because scenes from OpenPype did contain
# ObjectId from mongo.
try:
uuid.UUID(repre_id)
filtered_repre_ids.add(repre_id)
except ValueError:
continue
if not filtered_repre_ids:
return output
repre_entities = ayon_api.get_representations(project_name, repre_ids)
repres_by_id.update({
repre_entity["id"]: repre_entity
for repre_entity in repre_entities
})
version_ids = {
repre_entity["versionId"]
for repre_entity in repres_by_id.values()
}
if not version_ids:
return output
versions_by_id.update({
version_entity["id"]: version_entity
for version_entity in ayon_api.get_versions(
project_name, version_ids=version_ids
)
})
product_ids = {
version_entity["productId"]
for version_entity in versions_by_id.values()
}
if not product_ids:
return output
products_by_id.update({
product_entity["id"]: product_entity
for product_entity in ayon_api.get_products(
project_name, product_ids=product_ids
)
})
folder_ids = {
product_entity["folderId"]
for product_entity in products_by_id.values()
}
if not folder_ids:
return output
folders_by_id.update({
folder_entity["id"]: folder_entity
for folder_entity in ayon_api.get_folders(
project_name, folder_ids=folder_ids
)
})
return output
def _clear_items(self):
root_item = self.invisibleRootItem()
root_item.removeRows(0, root_item.rowCount())
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)
super().__init__(*args, **kwargs)
self.setDynamicSortFilter(True)
self.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
self._filter_outdated = False
self._hierarchy_view = False
@ -467,28 +340,23 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel):
# 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:
if source_index.data(IS_CONTAINER_ITEM_ROLE):
return True
# Filter by regex
if hasattr(self, "filterRegExp"):
regex = self.filterRegExp()
else:
regex = self.filterRegularExpression()
pattern = regex.pattern()
if pattern:
pattern = re.escape(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):
if source_index.data(VERSION_IS_LATEST_ROLE):
return False
# Filter by regex
if hasattr(self, "filterRegularExpression"):
regex = self.filterRegularExpression()
else:
regex = self.filterRegExp()
if not self._matches(row, parent, regex.pattern()):
return False
return True
def set_filter_outdated(self, state):
@ -505,37 +373,6 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel):
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 `isOutdated` data is true or not set.
"""
def outdated(node):
return node.get("isOutdated", True)
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.
@ -548,38 +385,31 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel):
bool
"""
if not pattern:
return True
flags = 0
if self.sortCaseSensitivity() == QtCore.Qt.CaseInsensitive:
flags = re.IGNORECASE
regex = re.compile(re.escape(pattern), flags=flags)
model = self.sourceModel()
column = self.filterKeyColumn()
role = self.filterRole()
def matches(row, parent, pattern):
matches_queue = collections.deque()
matches_queue.append((row, parent))
while matches_queue:
queue_item = matches_queue.popleft()
row, parent = queue_item
index = model.index(row, column, parent)
key = model.data(index, role)
if re.search(pattern, key, re.IGNORECASE):
value = model.data(index, role)
if regex.search(value):
return True
if matches(row, parent, pattern):
return True
for idx in range(model.rowCount(index)):
matches_queue.append((idx, index))
# 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
return False

View file

@ -1,6 +1,8 @@
from .containers import ContainersModel
from .sitesync import SiteSyncModel
__all__ = (
"ContainersModel",
"SiteSyncModel",
)

View file

@ -0,0 +1,343 @@
import uuid
import collections
import ayon_api
from ayon_api.graphql import GraphQlQuery
from ayon_core.host import ILoadHost
# --- Implementation that should be in ayon-python-api ---
# The implementation is not available in all versions of ayon-python-api.
RepresentationHierarchy = collections.namedtuple(
"RepresentationHierarchy",
("folder", "product", "version", "representation")
)
def representations_parent_ids_qraphql_query():
query = GraphQlQuery("RepresentationsHierarchyQuery")
project_name_var = query.add_variable("projectName", "String!")
repre_ids_var = query.add_variable("representationIds", "[String!]")
project_field = query.add_field("project")
project_field.set_filter("name", project_name_var)
repres_field = project_field.add_field_with_edges("representations")
repres_field.add_field("id")
repres_field.add_field("name")
repres_field.set_filter("ids", repre_ids_var)
version_field = repres_field.add_field("version")
version_field.add_field("id")
product_field = version_field.add_field("product")
product_field.add_field("id")
product_field.add_field("name")
product_field.add_field("productType")
product_attrib_field = product_field.add_field("attrib")
product_attrib_field.add_field("productGroup")
folder_field = product_field.add_field("folder")
folder_field.add_field("id")
folder_field.add_field("path")
return query
def get_representations_hierarchy(project_name, representation_ids):
"""Find representations parents by representation id.
Representation parent entities up to project.
Args:
project_name (str): Project where to look for entities.
representation_ids (Iterable[str]): Representation ids.
Returns:
dict[str, RepresentationParents]: Parent entities by
representation id.
"""
if not representation_ids:
return {}
repre_ids = set(representation_ids)
output = {
repre_id: RepresentationHierarchy(None, None, None, None)
for repre_id in representation_ids
}
query = representations_parent_ids_qraphql_query()
query.set_variable_value("projectName", project_name)
query.set_variable_value("representationIds", list(repre_ids))
con = ayon_api.get_server_api_connection()
parsed_data = query.query(con)
for repre in parsed_data["project"]["representations"]:
repre_id = repre["id"]
version = repre.pop("version")
product = version.pop("product")
folder = product.pop("folder")
output[repre_id] = RepresentationHierarchy(
folder, product, version, repre
)
return output
# --- END of ayon-python-api implementation ---
class ContainerItem:
def __init__(
self,
representation_id,
loader_name,
namespace,
name,
object_name,
item_id
):
self.representation_id = representation_id
self.loader_name = loader_name
self.object_name = object_name
self.namespace = namespace
self.name = name
self.item_id = item_id
@classmethod
def from_container_data(cls, container):
return cls(
representation_id=container["representation"],
loader_name=container["loader"],
namespace=container["namespace"],
name=container["name"],
object_name=container["objectName"],
item_id=uuid.uuid4().hex,
)
class RepresentationInfo:
def __init__(
self,
folder_id,
folder_path,
product_id,
product_name,
product_type,
product_group,
version_id,
representation_name,
):
self.folder_id = folder_id
self.folder_path = folder_path
self.product_id = product_id
self.product_name = product_name
self.product_type = product_type
self.product_group = product_group
self.version_id = version_id
self.representation_name = representation_name
self._is_valid = None
@property
def is_valid(self):
if self._is_valid is None:
self._is_valid = (
self.folder_id is not None
and self.product_id is not None
and self.version_id is not None
and self.representation_name is not None
)
return self._is_valid
@classmethod
def new_invalid(cls):
return cls(None, None, None, None, None, None, None, None)
class VersionItem:
def __init__(self, version_id, product_id, version, status, is_latest):
self.version = version
self.version_id = version_id
self.product_id = product_id
self.version = version
self.status = status
self.is_latest = is_latest
@property
def is_hero(self):
return self.version < 0
@classmethod
def from_entity(cls, version_entity, is_latest):
return cls(
version_id=version_entity["id"],
product_id=version_entity["productId"],
version=version_entity["version"],
status=version_entity["status"],
is_latest=is_latest,
)
class ContainersModel:
def __init__(self, controller):
self._controller = controller
self._items_cache = None
self._containers_by_id = {}
self._container_items_by_id = {}
self._version_items_by_product_id = {}
self._repre_info_by_id = {}
def reset(self):
self._items_cache = None
self._containers_by_id = {}
self._container_items_by_id = {}
self._version_items_by_product_id = {}
self._repre_info_by_id = {}
def get_containers(self):
self._update_cache()
return list(self._containers_by_id.values())
def get_containers_by_item_ids(self, item_ids):
return {
item_id: self._containers_by_id.get(item_id)
for item_id in item_ids
}
def get_container_items(self):
self._update_cache()
return list(self._items_cache)
def get_container_items_by_id(self, item_ids):
return {
item_id: self._container_items_by_id.get(item_id)
for item_id in item_ids
}
def get_representation_info_items(self, representation_ids):
output = {}
missing_repre_ids = set()
for repre_id in representation_ids:
try:
uuid.UUID(repre_id)
except ValueError:
output[repre_id] = RepresentationInfo.new_invalid()
continue
repre_info = self._repre_info_by_id.get(repre_id)
if repre_info is None:
missing_repre_ids.add(repre_id)
else:
output[repre_id] = repre_info
if not missing_repre_ids:
return output
project_name = self._controller.get_current_project_name()
repre_hierarchy_by_id = get_representations_hierarchy(
project_name, missing_repre_ids
)
for repre_id, repre_hierarchy in repre_hierarchy_by_id.items():
kwargs = {
"folder_id": None,
"folder_path": None,
"product_id": None,
"product_name": None,
"product_type": None,
"product_group": None,
"version_id": None,
"representation_name": None,
}
folder = repre_hierarchy.folder
product = repre_hierarchy.product
version = repre_hierarchy.version
repre = repre_hierarchy.representation
if folder:
kwargs["folder_id"] = folder["id"]
kwargs["folder_path"] = folder["path"]
if product:
group = product["attrib"]["productGroup"]
kwargs["product_id"] = product["id"]
kwargs["product_name"] = product["name"]
kwargs["product_type"] = product["productType"]
kwargs["product_group"] = group
if version:
kwargs["version_id"] = version["id"]
if repre:
kwargs["representation_name"] = repre["name"]
repre_info = RepresentationInfo(**kwargs)
self._repre_info_by_id[repre_id] = repre_info
output[repre_id] = repre_info
return output
def get_version_items(self, product_ids):
if not product_ids:
return {}
missing_ids = {
product_id
for product_id in product_ids
if product_id not in self._version_items_by_product_id
}
if missing_ids:
def version_sorted(entity):
return entity["version"]
project_name = self._controller.get_current_project_name()
version_entities_by_product_id = {
product_id: []
for product_id in missing_ids
}
version_entities = list(ayon_api.get_versions(
project_name,
product_ids=missing_ids,
fields={"id", "version", "productId", "status"}
))
version_entities.sort(key=version_sorted)
for version_entity in version_entities:
product_id = version_entity["productId"]
version_entities_by_product_id[product_id].append(
version_entity
)
for product_id, version_entities in (
version_entities_by_product_id.items()
):
last_version = abs(version_entities[-1]["version"])
version_items_by_id = {
entity["id"]: VersionItem.from_entity(
entity, abs(entity["version"]) == last_version
)
for entity in version_entities
}
self._version_items_by_product_id[product_id] = (
version_items_by_id
)
return {
product_id: dict(self._version_items_by_product_id[product_id])
for product_id in product_ids
}
def _update_cache(self):
if self._items_cache is not None:
return
host = self._controller.get_host()
if isinstance(host, ILoadHost):
containers = list(host.get_containers())
elif hasattr(host, "ls"):
containers = list(host.ls())
else:
containers = []
container_items = []
containers_by_id = {}
container_items_by_id = {}
for container in containers:
item = ContainerItem.from_container_data(container)
containers_by_id[item.item_id] = container
container_items_by_id[item.item_id] = item
container_items.append(item)
self._containers_by_id = containers_by_id
self._container_items_by_id = container_items_by_id
self._items_cache = container_items

File diff suppressed because it is too large Load diff

View file

@ -2,17 +2,10 @@ from qtpy import QtWidgets, QtCore, QtGui
import qtawesome
from ayon_core import style, resources
from ayon_core.tools.utils.lib import (
preserve_expanded_rows,
preserve_selection,
)
from ayon_core.tools.utils import PlaceholderLineEdit
from ayon_core.tools.sceneinventory import SceneInventoryController
from .delegates import VersionDelegate
from .model import (
InventoryModel,
FilterProxyModel
)
from .view import SceneInventoryView
@ -20,7 +13,7 @@ class SceneInventoryWindow(QtWidgets.QDialog):
"""Scene Inventory window"""
def __init__(self, controller=None, parent=None):
super(SceneInventoryWindow, self).__init__(parent)
super().__init__(parent)
if controller is None:
controller = SceneInventoryController()
@ -33,10 +26,9 @@ class SceneInventoryWindow(QtWidgets.QDialog):
self.resize(1100, 480)
# region control
filter_label = QtWidgets.QLabel("Search", self)
text_filter = QtWidgets.QLineEdit(self)
text_filter = PlaceholderLineEdit(self)
text_filter.setPlaceholderText("Filter by name...")
outdated_only_checkbox = QtWidgets.QCheckBox(
"Filter to outdated", self
@ -44,52 +36,30 @@ class SceneInventoryWindow(QtWidgets.QDialog):
outdated_only_checkbox.setToolTip("Show outdated files only")
outdated_only_checkbox.setChecked(False)
icon = qtawesome.icon("fa.arrow-up", color="white")
update_all_icon = qtawesome.icon("fa.arrow-up", color="white")
update_all_button = QtWidgets.QPushButton(self)
update_all_button.setToolTip("Update all outdated to latest version")
update_all_button.setIcon(icon)
update_all_button.setIcon(update_all_icon)
icon = qtawesome.icon("fa.refresh", color="white")
refresh_icon = qtawesome.icon("fa.refresh", color="white")
refresh_button = QtWidgets.QPushButton(self)
refresh_button.setToolTip("Refresh")
refresh_button.setIcon(icon)
refresh_button.setIcon(refresh_icon)
control_layout = QtWidgets.QHBoxLayout()
control_layout.addWidget(filter_label)
control_layout.addWidget(text_filter)
control_layout.addWidget(outdated_only_checkbox)
control_layout.addWidget(update_all_button)
control_layout.addWidget(refresh_button)
model = InventoryModel(controller)
proxy = FilterProxyModel()
proxy.setSourceModel(model)
proxy.setDynamicSortFilter(True)
proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
headers_widget = QtWidgets.QWidget(self)
headers_layout = QtWidgets.QHBoxLayout(headers_widget)
headers_layout.setContentsMargins(0, 0, 0, 0)
headers_layout.addWidget(filter_label, 0)
headers_layout.addWidget(text_filter, 1)
headers_layout.addWidget(outdated_only_checkbox, 0)
headers_layout.addWidget(update_all_button, 0)
headers_layout.addWidget(refresh_button, 0)
view = SceneInventoryView(controller, self)
view.setModel(proxy)
sync_enabled = controller.is_sitesync_enabled()
view.setColumnHidden(model.active_site_col, not sync_enabled)
view.setColumnHidden(model.remote_site_col, not sync_enabled)
# 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) # product type
view.setColumnWidth(4, 120) # group
view.setColumnWidth(5, 150) # loader
# apply delegates
version_delegate = VersionDelegate(controller, self)
column = model.Columns.index("version")
view.setItemDelegateForColumn(column, version_delegate)
layout = QtWidgets.QVBoxLayout(self)
layout.addLayout(control_layout)
layout.addWidget(view)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.addWidget(headers_widget, 0)
main_layout.addWidget(view, 1)
show_timer = QtCore.QTimer()
show_timer.setInterval(0)
@ -114,12 +84,8 @@ class SceneInventoryWindow(QtWidgets.QDialog):
self._update_all_button = update_all_button
self._outdated_only_checkbox = outdated_only_checkbox
self._view = view
self._model = model
self._proxy = proxy
self._version_delegate = version_delegate
self._first_show = True
self._first_refresh = True
def showEvent(self, event):
super(SceneInventoryWindow, self).showEvent(event)
@ -139,29 +105,16 @@ class SceneInventoryWindow(QtWidgets.QDialog):
whilst trying to name an instance.
"""
pass
def _on_refresh_request(self):
"""Signal callback to trigger 'refresh' without any arguments."""
self.refresh()
def refresh(self, containers=None):
self._first_refresh = False
def refresh(self):
self._controller.reset()
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 = {"containers": containers}
# TODO do not touch view's inner attribute
if self._view._hierarchy_view:
kwargs["selected"] = self._view._selected
self._model.refresh(**kwargs)
self._view.refresh()
def _on_show_timer(self):
if self._show_counter < 3:
@ -171,17 +124,13 @@ class SceneInventoryWindow(QtWidgets.QDialog):
self.refresh()
def _on_hierarchy_view_change(self, enabled):
self._proxy.set_hierarchy_view(enabled)
self._model.set_hierarchy_view(enabled)
self._view.set_hierarchy_view(enabled)
def _on_text_filter_change(self, text_filter):
if hasattr(self._proxy, "setFilterRegExp"):
self._proxy.setFilterRegExp(text_filter)
else:
self._proxy.setFilterRegularExpression(text_filter)
self._view.set_text_filter(text_filter)
def _on_outdated_state_change(self):
self._proxy.set_filter_outdated(
self._view.set_filter_outdated(
self._outdated_only_checkbox.isChecked()
)

View file

@ -1,6 +1,7 @@
import os
import sys
import contextlib
import collections
from functools import partial
from qtpy import QtWidgets, QtCore, QtGui
@ -196,16 +197,16 @@ def get_openpype_qt_app():
return get_ayon_qt_app()
def iter_model_rows(model, column, include_root=False):
def iter_model_rows(model, column=0, include_root=False):
"""Iterate over all row indices in a model"""
indices = [QtCore.QModelIndex()] # start iteration at root
for index in indices:
indexes_queue = collections.deque()
# start iteration at root
indexes_queue.append(QtCore.QModelIndex())
while indexes_queue:
index = indexes_queue.popleft()
# Add children to the iterations
child_rows = model.rowCount(index)
for child_row in range(child_rows):
child_index = model.index(child_row, column, index)
indices.append(child_index)
for child_row in range(model.rowCount(index)):
indexes_queue.append(model.index(child_row, column, index))
if not include_root and not index.isValid():
continue