mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
refactor of scene inventory view
This commit is contained in:
parent
8366d2e8b4
commit
e598e5fef3
8 changed files with 1146 additions and 1027 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
from .containers import ContainersModel
|
||||
from .sitesync import SiteSyncModel
|
||||
|
||||
|
||||
__all__ = (
|
||||
"ContainersModel",
|
||||
"SiteSyncModel",
|
||||
)
|
||||
|
|
|
|||
343
client/ayon_core/tools/sceneinventory/models/containers.py
Normal file
343
client/ayon_core/tools/sceneinventory/models/containers.py
Normal 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
|
|
@ -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()
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue