diff --git a/openpype/hosts/nuke/api/__init__.py b/openpype/hosts/nuke/api/__init__.py
index 1af5ff365d..a01f5bda0a 100644
--- a/openpype/hosts/nuke/api/__init__.py
+++ b/openpype/hosts/nuke/api/__init__.py
@@ -50,6 +50,8 @@ from .utils import (
get_colorspace_list
)
+from .actions import SelectInvalidAction
+
__all__ = (
"file_extensions",
"has_unsaved_changes",
@@ -92,5 +94,7 @@ __all__ = (
"create_write_node",
"colorspace_exists_on_node",
- "get_colorspace_list"
+ "get_colorspace_list",
+
+ "SelectInvalidAction",
)
diff --git a/openpype/hosts/nuke/api/actions.py b/openpype/hosts/nuke/api/actions.py
index c955a85acc..ca3c8393ed 100644
--- a/openpype/hosts/nuke/api/actions.py
+++ b/openpype/hosts/nuke/api/actions.py
@@ -20,33 +20,31 @@ class SelectInvalidAction(pyblish.api.Action):
def process(self, context, plugin):
- try:
- import nuke
- except ImportError:
- raise ImportError("Current host is not Nuke")
-
- errored_instances = get_errored_instances_from_context(context,
- plugin=plugin)
+ # Get the errored instances for the plug-in
+ errored_instances = get_errored_instances_from_context(
+ context, plugin)
# Get the invalid nodes for the plug-ins
self.log.info("Finding invalid nodes..")
- invalid = list()
+ invalid_nodes = set()
for instance in errored_instances:
- invalid_nodes = plugin.get_invalid(instance)
+ invalid = plugin.get_invalid(instance)
- if invalid_nodes:
- if isinstance(invalid_nodes, (list, tuple)):
- invalid.append(invalid_nodes[0])
- else:
- self.log.warning("Plug-in returned to be invalid, "
- "but has no selectable nodes.")
+ if not invalid:
+ continue
- # Ensure unique (process each node only once)
- invalid = list(set(invalid))
+ select_node = instance.data.get("transientData", {}).get("node")
+ if not select_node:
+ raise RuntimeError(
+ "No transientData['node'] found on instance: {}".format(
+ instance)
+ )
- if invalid:
- self.log.info("Selecting invalid nodes: {}".format(invalid))
+ invalid_nodes.add(select_node)
+
+ if invalid_nodes:
+ self.log.info("Selecting invalid nodes: {}".format(invalid_nodes))
reset_selection()
- select_nodes(invalid)
+ select_nodes(list(invalid_nodes))
else:
self.log.info("No invalid nodes found.")
diff --git a/openpype/hosts/nuke/plugins/publish/help/validate_asset_context.xml b/openpype/hosts/nuke/plugins/publish/help/validate_asset_context.xml
new file mode 100644
index 0000000000..d9394ae510
--- /dev/null
+++ b/openpype/hosts/nuke/plugins/publish/help/validate_asset_context.xml
@@ -0,0 +1,31 @@
+
+
+
+ Shot/Asset name
+
+## Publishing to a different asset context
+
+There are publish instances present which are publishing into a different asset than your current context.
+
+Usually this is not what you want but there can be cases where you might want to publish into another asset/shot or task.
+
+If that's the case you can disable the validation on the instance to ignore it.
+
+The wrong node's name is: \`{node_name}\`
+
+### Correct context keys and values:
+
+\`{correct_values}\`
+
+### Wrong keys and values:
+
+\`{wrong_values}\`.
+
+
+## How to repair?
+
+1. Use \"Repair\" button.
+2. Hit Reload button on the publisher.
+
+
+
diff --git a/openpype/hosts/nuke/plugins/publish/help/validate_asset_name.xml b/openpype/hosts/nuke/plugins/publish/help/validate_asset_name.xml
deleted file mode 100644
index 0422917e9c..0000000000
--- a/openpype/hosts/nuke/plugins/publish/help/validate_asset_name.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
- Shot/Asset name
-
-## Invalid Shot/Asset name in subset
-
-Following Node with name `{node_name}`:
-Is in context of `{correct_name}` but Node _asset_ knob is set as `{wrong_name}`.
-
-### How to repair?
-
-1. Either use Repair or Select button.
-2. If you chose Select then rename asset knob to correct name.
-3. Hit Reload button on the publisher.
-
-
-
\ No newline at end of file
diff --git a/openpype/hosts/nuke/plugins/publish/validate_asset_context.py b/openpype/hosts/nuke/plugins/publish/validate_asset_context.py
new file mode 100644
index 0000000000..ab62daeaeb
--- /dev/null
+++ b/openpype/hosts/nuke/plugins/publish/validate_asset_context.py
@@ -0,0 +1,112 @@
+# -*- coding: utf-8 -*-
+"""Validate if instance asset is the same as context asset."""
+from __future__ import absolute_import
+
+import pyblish.api
+
+from openpype.pipeline.publish import (
+ RepairAction,
+ ValidateContentsOrder,
+ PublishXmlValidationError,
+ OptionalPyblishPluginMixin
+)
+from openpype.hosts.nuke.api import SelectInvalidAction
+
+
+class ValidateCorrectAssetContext(
+ pyblish.api.InstancePlugin,
+ OptionalPyblishPluginMixin
+):
+ """Validator to check if instance asset context match context asset.
+
+ When working in per-shot style you always publish data in context of
+ current asset (shot). This validator checks if this is so. It is optional
+ so it can be disabled when needed.
+
+ Checking `asset` and `task` keys.
+ """
+ order = ValidateContentsOrder
+ label = "Validate asset context"
+ hosts = ["nuke"]
+ actions = [
+ RepairAction,
+ SelectInvalidAction
+ ]
+ optional = True
+
+ @classmethod
+ def apply_settings(cls, project_settings):
+ """Apply deprecated settings from project settings.
+ """
+ nuke_publish = project_settings["nuke"]["publish"]
+ if "ValidateCorrectAssetName" in nuke_publish:
+ settings = nuke_publish["ValidateCorrectAssetName"]
+ else:
+ settings = nuke_publish["ValidateCorrectAssetContext"]
+
+ cls.enabled = settings["enabled"]
+ cls.optional = settings["optional"]
+ cls.active = settings["active"]
+
+ def process(self, instance):
+ if not self.is_active(instance.data):
+ return
+
+ invalid_keys = self.get_invalid(instance)
+
+ if not invalid_keys:
+ return
+
+ message_values = {
+ "node_name": instance.data["transientData"]["node"].name(),
+ "correct_values": ", ".join([
+ "{} > {}".format(_key, instance.context.data[_key])
+ for _key in invalid_keys
+ ]),
+ "wrong_values": ", ".join([
+ "{} > {}".format(_key, instance.data.get(_key))
+ for _key in invalid_keys
+ ])
+ }
+
+ msg = (
+ "Instance `{node_name}` has wrong context keys:\n"
+ "Correct: `{correct_values}` | Wrong: `{wrong_values}`").format(
+ **message_values)
+
+ self.log.debug(msg)
+
+ raise PublishXmlValidationError(
+ self, msg, formatting_data=message_values
+ )
+
+ @classmethod
+ def get_invalid(cls, instance):
+ """Get invalid keys from instance data and context data."""
+
+ invalid_keys = []
+ testing_keys = ["asset", "task"]
+ for _key in testing_keys:
+ if _key not in instance.data:
+ invalid_keys.append(_key)
+ continue
+ if instance.data[_key] != instance.context.data[_key]:
+ invalid_keys.append(_key)
+
+ return invalid_keys
+
+ @classmethod
+ def repair(cls, instance):
+ """Repair instance data with context data."""
+ invalid_keys = cls.get_invalid(instance)
+
+ create_context = instance.context.data["create_context"]
+
+ instance_id = instance.data.get("instance_id")
+ created_instance = create_context.get_instance_by_id(
+ instance_id
+ )
+ for _key in invalid_keys:
+ created_instance[_key] = instance.context.data[_key]
+
+ create_context.save_changes()
diff --git a/openpype/hosts/nuke/plugins/publish/validate_asset_name.py b/openpype/hosts/nuke/plugins/publish/validate_asset_name.py
deleted file mode 100644
index df05f76a5b..0000000000
--- a/openpype/hosts/nuke/plugins/publish/validate_asset_name.py
+++ /dev/null
@@ -1,138 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Validate if instance asset is the same as context asset."""
-from __future__ import absolute_import
-
-import pyblish.api
-
-import openpype.hosts.nuke.api.lib as nlib
-
-from openpype.pipeline.publish import (
- ValidateContentsOrder,
- PublishXmlValidationError,
- OptionalPyblishPluginMixin
-)
-
-class SelectInvalidInstances(pyblish.api.Action):
- """Select invalid instances in Outliner."""
-
- label = "Select"
- icon = "briefcase"
- on = "failed"
-
- def process(self, context, plugin):
- """Process invalid validators and select invalid instances."""
- # Get the errored instances
- failed = []
- for result in context.data["results"]:
- if (
- result["error"] is None
- or result["instance"] is None
- or result["instance"] in failed
- or result["plugin"] != plugin
- ):
- continue
-
- failed.append(result["instance"])
-
- # Apply pyblish.logic to get the instances for the plug-in
- instances = pyblish.api.instances_by_plugin(failed, plugin)
-
- if instances:
- self.deselect()
- self.log.info(
- "Selecting invalid nodes: %s" % ", ".join(
- [str(x) for x in instances]
- )
- )
- self.select(instances)
- else:
- self.log.info("No invalid nodes found.")
- self.deselect()
-
- def select(self, instances):
- for inst in instances:
- if inst.data.get("transientData", {}).get("node"):
- select_node = inst.data["transientData"]["node"]
- select_node["selected"].setValue(True)
-
- def deselect(self):
- nlib.reset_selection()
-
-
-class RepairSelectInvalidInstances(pyblish.api.Action):
- """Repair the instance asset."""
-
- label = "Repair"
- icon = "wrench"
- on = "failed"
-
- def process(self, context, plugin):
- # Get the errored instances
- failed = []
- for result in context.data["results"]:
- if (
- result["error"] is None
- or result["instance"] is None
- or result["instance"] in failed
- or result["plugin"] != plugin
- ):
- continue
-
- failed.append(result["instance"])
-
- # Apply pyblish.logic to get the instances for the plug-in
- instances = pyblish.api.instances_by_plugin(failed, plugin)
- self.log.debug(instances)
-
- context_asset = context.data["assetEntity"]["name"]
- for instance in instances:
- node = instance.data["transientData"]["node"]
- node_data = nlib.get_node_data(node, nlib.INSTANCE_DATA_KNOB)
- node_data["asset"] = context_asset
- nlib.set_node_data(node, nlib.INSTANCE_DATA_KNOB, node_data)
-
-
-class ValidateCorrectAssetName(
- pyblish.api.InstancePlugin,
- OptionalPyblishPluginMixin
-):
- """Validator to check if instance asset match context asset.
-
- When working in per-shot style you always publish data in context of
- current asset (shot). This validator checks if this is so. It is optional
- so it can be disabled when needed.
-
- Action on this validator will select invalid instances in Outliner.
- """
- order = ValidateContentsOrder
- label = "Validate correct asset name"
- hosts = ["nuke"]
- actions = [
- SelectInvalidInstances,
- RepairSelectInvalidInstances
- ]
- optional = True
-
- def process(self, instance):
- if not self.is_active(instance.data):
- return
-
- asset = instance.data.get("asset")
- context_asset = instance.context.data["assetEntity"]["name"]
- node = instance.data["transientData"]["node"]
-
- msg = (
- "Instance `{}` has wrong shot/asset name:\n"
- "Correct: `{}` | Wrong: `{}`").format(
- instance.name, asset, context_asset)
-
- self.log.debug(msg)
-
- if asset != context_asset:
- raise PublishXmlValidationError(
- self, msg, formatting_data={
- "node_name": node.name(),
- "wrong_name": asset,
- "correct_name": context_asset
- }
- )
diff --git a/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py b/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py
index dbcd216a84..39114c80c8 100644
--- a/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py
+++ b/openpype/hosts/nuke/plugins/publish/validate_output_resolution.py
@@ -23,7 +23,7 @@ class ValidateOutputResolution(
order = pyblish.api.ValidatorOrder
optional = True
families = ["render"]
- label = "Write resolution"
+ label = "Validate Write resolution"
hosts = ["nuke"]
actions = [RepairAction]
diff --git a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py
index 2a925fbeff..9aae53e59d 100644
--- a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py
+++ b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py
@@ -82,12 +82,6 @@ class ValidateNukeWriteNode(
correct_data = get_write_node_template_attr(write_group_node)
check = []
- self.log.debug("__ write_node: {}".format(
- write_node
- ))
- self.log.debug("__ correct_data: {}".format(
- correct_data
- ))
# Collect key values of same type in a list.
values_by_name = defaultdict(list)
@@ -96,9 +90,6 @@ class ValidateNukeWriteNode(
for knob_data in correct_data["knobs"]:
knob_type = knob_data["type"]
- self.log.debug("__ knob_type: {}".format(
- knob_type
- ))
if (
knob_type == "__legacy__"
@@ -134,9 +125,6 @@ class ValidateNukeWriteNode(
fixed_values.append(value)
- self.log.debug("__ key: {} | values: {}".format(
- key, fixed_values
- ))
if (
node_value not in fixed_values
and key != "file"
@@ -144,8 +132,6 @@ class ValidateNukeWriteNode(
):
check.append([key, value, write_node[key].value()])
- self.log.info(check)
-
if check:
self._make_error(check)
diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json
index ad9f46c8ab..3b69ef54fd 100644
--- a/openpype/settings/defaults/project_settings/nuke.json
+++ b/openpype/settings/defaults/project_settings/nuke.json
@@ -341,7 +341,7 @@
"write"
]
},
- "ValidateCorrectAssetName": {
+ "ValidateCorrectAssetContext": {
"enabled": true,
"optional": true,
"active": true
diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json
index fa08e19c63..9e012e560f 100644
--- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json
+++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json
@@ -61,7 +61,7 @@
"name": "template_publish_plugin",
"template_data": [
{
- "key": "ValidateCorrectAssetName",
+ "key": "ValidateCorrectAssetContext",
"label": "Validate Correct Asset Name"
}
]
diff --git a/openpype/tools/ayon_sceneinventory/__init__.py b/openpype/tools/ayon_sceneinventory/__init__.py
new file mode 100644
index 0000000000..5412e2fea2
--- /dev/null
+++ b/openpype/tools/ayon_sceneinventory/__init__.py
@@ -0,0 +1,6 @@
+from .control import SceneInventoryController
+
+
+__all__ = (
+ "SceneInventoryController",
+)
diff --git a/openpype/tools/ayon_sceneinventory/control.py b/openpype/tools/ayon_sceneinventory/control.py
new file mode 100644
index 0000000000..e98b0e307b
--- /dev/null
+++ b/openpype/tools/ayon_sceneinventory/control.py
@@ -0,0 +1,134 @@
+import ayon_api
+
+from openpype.lib.events import QueuedEventSystem
+from openpype.host import ILoadHost
+from openpype.pipeline import (
+ registered_host,
+ get_current_context,
+)
+from openpype.tools.ayon_utils.models import HierarchyModel
+
+from .models import SiteSyncModel
+
+
+class SceneInventoryController:
+ """This is a temporary controller for AYON.
+
+ Goal of this temporary controller is to provide a way to get current
+ context instead of using 'AvalonMongoDB' object (or 'legacy_io').
+
+ Also provides (hopefully) cleaner api for site sync.
+ """
+
+ def __init__(self, host=None):
+ if host is None:
+ host = registered_host()
+ self._host = host
+ self._current_context = None
+ self._current_project = None
+ self._current_folder_id = None
+ self._current_folder_set = False
+
+ self._site_sync_model = SiteSyncModel(self)
+ # Switch dialog requirements
+ self._hierarchy_model = HierarchyModel(self)
+ self._event_system = self._create_event_system()
+
+ def emit_event(self, topic, data=None, source=None):
+ if data is None:
+ data = {}
+ self._event_system.emit(topic, data, source)
+
+ def register_event_callback(self, topic, callback):
+ self._event_system.add_callback(topic, callback)
+
+ def reset(self):
+ self._current_context = None
+ self._current_project = None
+ self._current_folder_id = None
+ self._current_folder_set = False
+
+ self._site_sync_model.reset()
+ self._hierarchy_model.reset()
+
+ def get_current_context(self):
+ if self._current_context is None:
+ if hasattr(self._host, "get_current_context"):
+ self._current_context = self._host.get_current_context()
+ else:
+ self._current_context = get_current_context()
+ return self._current_context
+
+ def get_current_project_name(self):
+ if self._current_project is None:
+ self._current_project = self.get_current_context()["project_name"]
+ return self._current_project
+
+ def get_current_folder_id(self):
+ if self._current_folder_set:
+ return self._current_folder_id
+
+ context = self.get_current_context()
+ project_name = context["project_name"]
+ folder_path = context.get("folder_path")
+ folder_name = context.get("asset_name")
+ folder_id = None
+ if folder_path:
+ folder = ayon_api.get_folder_by_path(project_name, folder_path)
+ if folder:
+ folder_id = folder["id"]
+ elif folder_name:
+ for folder in ayon_api.get_folders(
+ project_name, folder_names=[folder_name]
+ ):
+ folder_id = folder["id"]
+ break
+
+ self._current_folder_id = folder_id
+ self._current_folder_set = True
+ return self._current_folder_id
+
+ def get_containers(self):
+ host = self._host
+ if isinstance(host, ILoadHost):
+ return host.get_containers()
+ elif hasattr(host, "ls"):
+ return host.ls()
+ return []
+
+ # Site Sync methods
+ def is_sync_server_enabled(self):
+ return self._site_sync_model.is_sync_server_enabled()
+
+ def get_sites_information(self):
+ return self._site_sync_model.get_sites_information()
+
+ def get_site_provider_icons(self):
+ return self._site_sync_model.get_site_provider_icons()
+
+ def get_representations_site_progress(self, representation_ids):
+ return self._site_sync_model.get_representations_site_progress(
+ representation_ids
+ )
+
+ def resync_representations(self, representation_ids, site_type):
+ return self._site_sync_model.resync_representations(
+ representation_ids, site_type
+ )
+
+ # Switch dialog methods
+ def get_folder_items(self, project_name, sender=None):
+ return self._hierarchy_model.get_folder_items(project_name, sender)
+
+ def get_folder_label(self, folder_id):
+ if not folder_id:
+ return None
+ project_name = self.get_current_project_name()
+ folder_item = self._hierarchy_model.get_folder_item(
+ project_name, folder_id)
+ if folder_item is None:
+ return None
+ return folder_item.label
+
+ def _create_event_system(self):
+ return QueuedEventSystem()
diff --git a/openpype/tools/ayon_sceneinventory/model.py b/openpype/tools/ayon_sceneinventory/model.py
new file mode 100644
index 0000000000..16924b0a7e
--- /dev/null
+++ b/openpype/tools/ayon_sceneinventory/model.py
@@ -0,0 +1,622 @@
+import collections
+import re
+import logging
+import uuid
+import copy
+
+from collections import defaultdict
+
+from qtpy import QtCore, QtGui
+import qtawesome
+
+from openpype.client import (
+ get_assets,
+ get_subsets,
+ get_versions,
+ get_last_version_by_subset_id,
+ get_representations,
+)
+from openpype.pipeline import (
+ get_current_project_name,
+ schema,
+ HeroVersionType,
+)
+from openpype.style import get_default_entity_icon_color
+from openpype.tools.utils.models import TreeModel, Item
+
+
+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):
+ """The model for the inventory"""
+
+ Columns = [
+ "Name",
+ "version",
+ "count",
+ "family",
+ "group",
+ "loader",
+ "objectName",
+ "active_site",
+ "remote_site",
+ ]
+ active_site_col = Columns.index("active_site")
+ remote_site_col = Columns.index("remote_site")
+
+ 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)
+ self.log = logging.getLogger(self.__class__.__name__)
+
+ self._controller = controller
+
+ self._hierarchy_view = False
+
+ self._default_icon_color = get_default_entity_icon_color()
+
+ site_icons = self._controller.get_site_provider_icons()
+
+ self._site_icons = {
+ provider: QtGui.QIcon(icon_path)
+ for provider, icon_path in site_icons.items()
+ }
+
+ def outdated(self, item):
+ value = item.get("version")
+ if isinstance(value, HeroVersionType):
+ return False
+
+ if item.get("version") == item.get("highest_version"):
+ return False
+ return True
+
+ def data(self, index, role):
+ if not index.isValid():
+ return
+
+ item = index.internalPointer()
+
+ if role == QtCore.Qt.FontRole:
+ # Make top-level entries bold
+ if item.get("isGroupNode") or item.get("isNotSet"): # group-item
+ font = QtGui.QFont()
+ font.setBold(True)
+ return font
+
+ if role == QtCore.Qt.ForegroundRole:
+ # Set the text color to the OUTDATED_COLOR when the
+ # collected version is not the same as the highest version
+ key = self.Columns[index.column()]
+ if key == "version": # version
+ if item.get("isGroupNode"): # group-item
+ if self.outdated(item):
+ return self.OUTDATED_COLOR
+
+ if self._hierarchy_view:
+ # If current group is not outdated, check if any
+ # outdated children.
+ for _node in walk_hierarchy(item):
+ if self.outdated(_node):
+ return self.CHILD_OUTDATED_COLOR
+ else:
+
+ if self._hierarchy_view:
+ # Although this is not a group item, we still need
+ # to distinguish which one contain outdated child.
+ for _node in walk_hierarchy(item):
+ if self.outdated(_node):
+ return self.CHILD_OUTDATED_COLOR.darker(150)
+
+ return self.GRAYOUT_COLOR
+
+ if key == "Name" and not item.get("isGroupNode"):
+ return self.GRAYOUT_COLOR
+
+ # Add icons
+ if role == QtCore.Qt.DecorationRole:
+ if index.column() == 0:
+ # Override color
+ color = item.get("color", 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:
+ # Family icon
+ return item.get("familyIcon", 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", "")
+
+ return super(InventoryModel, self).data(index, role)
+
+ def set_hierarchy_view(self, state):
+ """Set whether to display subsets in hierarchy view."""
+ state = bool(state)
+
+ if state != self._hierarchy_view:
+ self._hierarchy_view = state
+
+ def refresh(self, selected=None, containers=None):
+ """Refresh the model"""
+
+ # 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 = versions_by_id.get(representation["parent"])
+ if not version:
+ not_found["version"].extend(group_containers)
+ not_found_ids.append(repre_id)
+ continue
+
+ product = products_by_id.get(version["parent"])
+ if not product:
+ not_found["product"].extend(group_containers)
+ not_found_ids.append(repre_id)
+ continue
+
+ folder = folders_by_id.get(product["parent"])
+ if not folder:
+ not_found["folder"].extend(group_containers)
+ not_found_ids.append(repre_id)
+ continue
+
+ group_dict.update({
+ "representation": representation,
+ "version": version,
+ "subset": product,
+ "asset": folder
+ })
+
+ 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
+ family_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()
+
+ for repre_id, group_dict in sorted(grouped.items()):
+ group_containers = group_dict["containers"]
+ representation = group_dict["representation"]
+ version = group_dict["version"]
+ subset = group_dict["subset"]
+ asset = group_dict["asset"]
+
+ # Get the primary family
+ maj_version, _ = schema.get_schema_version(subset["schema"])
+ if maj_version < 3:
+ src_doc = version
+ else:
+ src_doc = subset
+
+ prim_family = src_doc["data"].get("family")
+ if not prim_family:
+ families = src_doc["data"].get("families")
+ if families:
+ prim_family = families[0]
+
+ # Store the highest available version so the model can know
+ # whether current version is currently up-to-date.
+ highest_version = get_last_version_by_subset_id(
+ project_name, version["parent"]
+ )
+
+ # create the group header
+ group_node = Item()
+ group_node["Name"] = "{}_{}: ({})".format(
+ asset["name"], subset["name"], representation["name"]
+ )
+ group_node["representation"] = repre_id
+ group_node["version"] = version["name"]
+ group_node["highest_version"] = highest_version["name"]
+ group_node["family"] = prim_family or ""
+ group_node["familyIcon"] = family_icon
+ group_node["count"] = len(group_containers)
+ group_node["isGroupNode"] = True
+ group_node["group"] = subset["data"].get("subsetGroup")
+
+ # 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["name"]
+
+ # Remapping namespace to item name.
+ # Noted that the name key is capital "N", by doing this, we
+ # can view namespace in GUI without changing container data.
+ item_node["Name"] = 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_docs = get_representations(project_name, repre_ids)
+ repres_by_id.update({
+ repre_doc["_id"]: repre_doc
+ for repre_doc in repre_docs
+ })
+ version_ids = {
+ repre_doc["parent"] for repre_doc in repres_by_id.values()
+ }
+ if not version_ids:
+ return output
+
+ version_docs = get_versions(project_name, version_ids, hero=True)
+ versions_by_id.update({
+ version_doc["_id"]: version_doc
+ for version_doc in version_docs
+ })
+ hero_versions_by_subversion_id = collections.defaultdict(list)
+ for version_doc in versions_by_id.values():
+ if version_doc["type"] != "hero_version":
+ continue
+ subversion = version_doc["version_id"]
+ hero_versions_by_subversion_id[subversion].append(version_doc)
+
+ if hero_versions_by_subversion_id:
+ subversion_ids = set(
+ hero_versions_by_subversion_id.keys()
+ )
+ subversion_docs = get_versions(project_name, subversion_ids)
+ for subversion_doc in subversion_docs:
+ subversion_id = subversion_doc["_id"]
+ subversion_ids.discard(subversion_id)
+ h_version_docs = hero_versions_by_subversion_id[subversion_id]
+ for version_doc in h_version_docs:
+ version_doc["name"] = HeroVersionType(
+ subversion_doc["name"]
+ )
+ version_doc["data"] = copy.deepcopy(
+ subversion_doc["data"]
+ )
+
+ for subversion_id in subversion_ids:
+ h_version_docs = hero_versions_by_subversion_id[subversion_id]
+ for version_doc in h_version_docs:
+ versions_by_id.pop(version_doc["_id"])
+
+ product_ids = {
+ version_doc["parent"]
+ for version_doc in versions_by_id.values()
+ }
+ if not product_ids:
+ return output
+ product_docs = get_subsets(project_name, product_ids)
+ products_by_id.update({
+ product_doc["_id"]: product_doc
+ for product_doc in product_docs
+ })
+ folder_ids = {
+ product_doc["parent"]
+ for product_doc in products_by_id.values()
+ }
+ if not folder_ids:
+ return output
+
+ folder_docs = get_assets(project_name, folder_ids)
+ folders_by_id.update({
+ folder_doc["_id"]: folder_doc
+ for folder_doc in folder_docs
+ })
+ return output
+
+
+class FilterProxyModel(QtCore.QSortFilterProxyModel):
+ """Filter model to where key column's value is in the filtered tags"""
+
+ def __init__(self, *args, **kwargs):
+ super(FilterProxyModel, self).__init__(*args, **kwargs)
+ self._filter_outdated = False
+ self._hierarchy_view = False
+
+ def filterAcceptsRow(self, row, parent):
+ model = self.sourceModel()
+ source_index = model.index(row, self.filterKeyColumn(), parent)
+
+ # Always allow bottom entries (individual containers), since their
+ # parent group hidden if it wouldn't have been validated.
+ rows = model.rowCount(source_index)
+ if not rows:
+ return True
+
+ # Filter by regex
+ if 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):
+ return False
+
+ return True
+
+ def set_filter_outdated(self, state):
+ """Set whether to show the outdated entries only."""
+ state = bool(state)
+
+ if state != self._filter_outdated:
+ self._filter_outdated = bool(state)
+ self.invalidateFilter()
+
+ def set_hierarchy_view(self, state):
+ state = bool(state)
+
+ if state != self._hierarchy_view:
+ self._hierarchy_view = state
+
+ def _is_outdated(self, row, parent):
+ """Return whether row is outdated.
+
+ A row is considered outdated if it has "version" and "highest_version"
+ data and in the internal data structure, and they are not of an
+ equal value.
+
+ """
+ def outdated(node):
+ version = node.get("version", None)
+ highest = node.get("highest_version", None)
+
+ # Always allow indices that have no version data at all
+ if version is None and highest is None:
+ return True
+
+ # If either a version or highest is present but not the other
+ # consider the item invalid.
+ if not self._hierarchy_view:
+ # Skip this check if in hierarchy view, or the child item
+ # node will be hidden even it's actually outdated.
+ if version is None or highest is None:
+ return False
+ return version != highest
+
+ index = self.sourceModel().index(row, self.filterKeyColumn(), parent)
+
+ # The scene contents are grouped by "representation", e.g. the same
+ # "representation" loaded twice is grouped under the same header.
+ # Since the version check filters these parent groups we skip that
+ # check for the individual children.
+ has_parent = index.parent().isValid()
+ if has_parent and not self._hierarchy_view:
+ return True
+
+ # Filter to those that have the different version numbers
+ node = index.internalPointer()
+ if outdated(node):
+ return True
+
+ if self._hierarchy_view:
+ for _node in walk_hierarchy(node):
+ if outdated(_node):
+ return True
+
+ return False
+
+ def _matches(self, row, parent, pattern):
+ """Return whether row matches regex pattern.
+
+ Args:
+ row (int): row number in model
+ parent (QtCore.QModelIndex): parent index
+ pattern (regex.pattern): pattern to check for in key
+
+ Returns:
+ bool
+
+ """
+ model = self.sourceModel()
+ column = self.filterKeyColumn()
+ role = self.filterRole()
+
+ def matches(row, parent, pattern):
+ index = model.index(row, column, parent)
+ key = model.data(index, role)
+ if re.search(pattern, key, re.IGNORECASE):
+ return True
+
+ if matches(row, parent, pattern):
+ return True
+
+ # Also allow if any of the children matches
+ source_index = model.index(row, column, parent)
+ rows = model.rowCount(source_index)
+
+ if any(
+ matches(idx, source_index, pattern)
+ for idx in range(rows)
+ ):
+ return True
+
+ if not self._hierarchy_view:
+ return False
+
+ for idx in range(rows):
+ child_index = model.index(idx, column, source_index)
+ child_rows = model.rowCount(child_index)
+ return any(
+ self._matches(child_idx, child_index, pattern)
+ for child_idx in range(child_rows)
+ )
+
+ return True
diff --git a/openpype/tools/ayon_sceneinventory/models/__init__.py b/openpype/tools/ayon_sceneinventory/models/__init__.py
new file mode 100644
index 0000000000..c861d3c1a0
--- /dev/null
+++ b/openpype/tools/ayon_sceneinventory/models/__init__.py
@@ -0,0 +1,6 @@
+from .site_sync import SiteSyncModel
+
+
+__all__ = (
+ "SiteSyncModel",
+)
diff --git a/openpype/tools/ayon_sceneinventory/models/site_sync.py b/openpype/tools/ayon_sceneinventory/models/site_sync.py
new file mode 100644
index 0000000000..b8c9443230
--- /dev/null
+++ b/openpype/tools/ayon_sceneinventory/models/site_sync.py
@@ -0,0 +1,176 @@
+from openpype.client import get_representations
+from openpype.modules import ModulesManager
+
+NOT_SET = object()
+
+
+class SiteSyncModel:
+ def __init__(self, controller):
+ self._controller = controller
+
+ self._sync_server_module = NOT_SET
+ self._sync_server_enabled = None
+ self._active_site = NOT_SET
+ self._remote_site = NOT_SET
+ self._active_site_provider = NOT_SET
+ self._remote_site_provider = NOT_SET
+
+ def reset(self):
+ self._sync_server_module = NOT_SET
+ self._sync_server_enabled = None
+ self._active_site = NOT_SET
+ self._remote_site = NOT_SET
+ self._active_site_provider = NOT_SET
+ self._remote_site_provider = NOT_SET
+
+ def is_sync_server_enabled(self):
+ """Site sync is enabled.
+
+ Returns:
+ bool: Is enabled or not.
+ """
+
+ self._cache_sync_server_module()
+ return self._sync_server_enabled
+
+ def get_site_provider_icons(self):
+ """Icon paths per provider.
+
+ Returns:
+ dict[str, str]: Path by provider name.
+ """
+
+ site_sync = self._get_sync_server_module()
+ if site_sync is None:
+ return {}
+ return site_sync.get_site_icons()
+
+ def get_sites_information(self):
+ return {
+ "active_site": self._get_active_site(),
+ "active_site_provider": self._get_active_site_provider(),
+ "remote_site": self._get_remote_site(),
+ "remote_site_provider": self._get_remote_site_provider()
+ }
+
+ def get_representations_site_progress(self, representation_ids):
+ """Get progress of representations sync."""
+
+ representation_ids = set(representation_ids)
+ output = {
+ repre_id: {
+ "active_site": 0,
+ "remote_site": 0,
+ }
+ for repre_id in representation_ids
+ }
+ if not self.is_sync_server_enabled():
+ return output
+
+ project_name = self._controller.get_current_project_name()
+ site_sync = self._get_sync_server_module()
+ repre_docs = get_representations(project_name, representation_ids)
+ active_site = self._get_active_site()
+ remote_site = self._get_remote_site()
+
+ for repre_doc in repre_docs:
+ repre_output = output[repre_doc["_id"]]
+ result = site_sync.get_progress_for_repre(
+ repre_doc, active_site, remote_site
+ )
+ repre_output["active_site"] = result[active_site]
+ repre_output["remote_site"] = result[remote_site]
+
+ return output
+
+ def resync_representations(self, representation_ids, site_type):
+ """
+
+ Args:
+ representation_ids (Iterable[str]): Representation ids.
+ site_type (Literal[active_site, remote_site]): Site type.
+ """
+
+ project_name = self._controller.get_current_project_name()
+ site_sync = self._get_sync_server_module()
+ active_site = self._get_active_site()
+ remote_site = self._get_remote_site()
+ progress = self.get_representations_site_progress(
+ representation_ids
+ )
+ for repre_id in representation_ids:
+ repre_progress = progress.get(repre_id)
+ if not repre_progress:
+ continue
+
+ if site_type == "active_site":
+ # check opposite from added site, must be 1 or unable to sync
+ check_progress = repre_progress["remote_site"]
+ site = active_site
+ else:
+ check_progress = repre_progress["active_site"]
+ site = remote_site
+
+ if check_progress == 1:
+ site_sync.add_site(
+ project_name, repre_id, site, force=True
+ )
+
+ def _get_sync_server_module(self):
+ self._cache_sync_server_module()
+ return self._sync_server_module
+
+ def _cache_sync_server_module(self):
+ if self._sync_server_module is not NOT_SET:
+ return self._sync_server_module
+ manager = ModulesManager()
+ site_sync = manager.modules_by_name.get("sync_server")
+ sync_enabled = site_sync is not None and site_sync.enabled
+ self._sync_server_module = site_sync
+ self._sync_server_enabled = sync_enabled
+
+ def _get_active_site(self):
+ if self._active_site is NOT_SET:
+ self._cache_sites()
+ return self._active_site
+
+ def _get_remote_site(self):
+ if self._remote_site is NOT_SET:
+ self._cache_sites()
+ return self._remote_site
+
+ def _get_active_site_provider(self):
+ if self._active_site_provider is NOT_SET:
+ self._cache_sites()
+ return self._active_site_provider
+
+ def _get_remote_site_provider(self):
+ if self._remote_site_provider is NOT_SET:
+ self._cache_sites()
+ return self._remote_site_provider
+
+ def _cache_sites(self):
+ site_sync = self._get_sync_server_module()
+ active_site = None
+ remote_site = None
+ active_site_provider = None
+ remote_site_provider = None
+ if site_sync is not None:
+ project_name = self._controller.get_current_project_name()
+ active_site = site_sync.get_active_site(project_name)
+ remote_site = site_sync.get_remote_site(project_name)
+ active_site_provider = "studio"
+ remote_site_provider = "studio"
+ if active_site != "studio":
+ active_site_provider = site_sync.get_active_provider(
+ project_name, active_site
+ )
+ if remote_site != "studio":
+ remote_site_provider = site_sync.get_active_provider(
+ project_name, remote_site
+ )
+
+ self._active_site = active_site
+ self._remote_site = remote_site
+ self._active_site_provider = active_site_provider
+ self._remote_site_provider = remote_site_provider
diff --git a/openpype/tools/ayon_sceneinventory/switch_dialog/__init__.py b/openpype/tools/ayon_sceneinventory/switch_dialog/__init__.py
new file mode 100644
index 0000000000..4c07832829
--- /dev/null
+++ b/openpype/tools/ayon_sceneinventory/switch_dialog/__init__.py
@@ -0,0 +1,6 @@
+from .dialog import SwitchAssetDialog
+
+
+__all__ = (
+ "SwitchAssetDialog",
+)
diff --git a/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py b/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py
new file mode 100644
index 0000000000..2ebed7f89b
--- /dev/null
+++ b/openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py
@@ -0,0 +1,1333 @@
+import collections
+import logging
+
+from qtpy import QtWidgets, QtCore
+import qtawesome
+
+from openpype.client import (
+ get_assets,
+ get_subset_by_name,
+ get_subsets,
+ get_versions,
+ get_hero_versions,
+ get_last_versions,
+ get_representations,
+)
+from openpype.pipeline.load import (
+ discover_loader_plugins,
+ switch_container,
+ get_repres_contexts,
+ loaders_from_repre_context,
+ LoaderSwitchNotImplementedError,
+ IncompatibleLoaderError,
+ LoaderNotFoundError
+)
+
+from .widgets import (
+ ButtonWithMenu,
+ SearchComboBox
+)
+from .folders_input import FoldersField
+
+log = logging.getLogger("SwitchAssetDialog")
+
+
+class ValidationState:
+ def __init__(self):
+ self.folder_ok = True
+ self.product_ok = True
+ self.repre_ok = True
+
+ @property
+ def all_ok(self):
+ return (
+ self.folder_ok
+ and self.product_ok
+ and self.repre_ok
+ )
+
+
+class SwitchAssetDialog(QtWidgets.QDialog):
+ """Widget to support asset switching"""
+
+ MIN_WIDTH = 550
+
+ switched = QtCore.Signal()
+
+ def __init__(self, controller, parent=None, items=None):
+ super(SwitchAssetDialog, self).__init__(parent)
+
+ self.setWindowTitle("Switch selected items ...")
+
+ # Force and keep focus dialog
+ self.setModal(True)
+
+ folders_field = FoldersField(controller, self)
+ products_combox = SearchComboBox(self)
+ repres_combobox = SearchComboBox(self)
+
+ products_combox.set_placeholder("")
+ repres_combobox.set_placeholder("")
+
+ folder_label = QtWidgets.QLabel(self)
+ product_label = QtWidgets.QLabel(self)
+ repre_label = QtWidgets.QLabel(self)
+
+ current_folder_btn = QtWidgets.QPushButton("Use current folder", self)
+
+ accept_icon = qtawesome.icon("fa.check", color="white")
+ accept_btn = ButtonWithMenu(self)
+ accept_btn.setIcon(accept_icon)
+
+ main_layout = QtWidgets.QGridLayout(self)
+ # Folder column
+ main_layout.addWidget(current_folder_btn, 0, 0)
+ main_layout.addWidget(folders_field, 1, 0)
+ main_layout.addWidget(folder_label, 2, 0)
+ # Product column
+ main_layout.addWidget(products_combox, 1, 1)
+ main_layout.addWidget(product_label, 2, 1)
+ # Representation column
+ main_layout.addWidget(repres_combobox, 1, 2)
+ main_layout.addWidget(repre_label, 2, 2)
+ # Btn column
+ main_layout.addWidget(accept_btn, 1, 3)
+ main_layout.setColumnStretch(0, 1)
+ main_layout.setColumnStretch(1, 1)
+ main_layout.setColumnStretch(2, 1)
+ main_layout.setColumnStretch(3, 0)
+
+ show_timer = QtCore.QTimer()
+ show_timer.setInterval(0)
+ show_timer.setSingleShot(False)
+
+ show_timer.timeout.connect(self._on_show_timer)
+ folders_field.value_changed.connect(
+ self._combobox_value_changed
+ )
+ products_combox.currentIndexChanged.connect(
+ self._combobox_value_changed
+ )
+ repres_combobox.currentIndexChanged.connect(
+ self._combobox_value_changed
+ )
+ accept_btn.clicked.connect(self._on_accept)
+ current_folder_btn.clicked.connect(self._on_current_folder)
+
+ self._show_timer = show_timer
+ self._show_counter = 0
+
+ self._current_folder_btn = current_folder_btn
+
+ self._folders_field = folders_field
+ self._products_combox = products_combox
+ self._representations_box = repres_combobox
+
+ self._folder_label = folder_label
+ self._product_label = product_label
+ self._repre_label = repre_label
+
+ self._accept_btn = accept_btn
+
+ self.setMinimumWidth(self.MIN_WIDTH)
+
+ # Set default focus to accept button so you don't directly type in
+ # first asset field, this also allows to see the placeholder value.
+ accept_btn.setFocus()
+
+ self._folder_docs_by_id = {}
+ self._product_docs_by_id = {}
+ self._version_docs_by_id = {}
+ self._repre_docs_by_id = {}
+
+ self._missing_folder_ids = set()
+ self._missing_product_ids = set()
+ self._missing_version_ids = set()
+ self._missing_repre_ids = set()
+ self._missing_docs = False
+
+ self._inactive_folder_ids = set()
+ self._inactive_product_ids = set()
+ self._inactive_repre_ids = set()
+
+ self._init_folder_id = None
+ self._init_product_name = None
+ self._init_repre_name = None
+
+ self._fill_check = False
+
+ self._project_name = controller.get_current_project_name()
+ self._folder_id = controller.get_current_folder_id()
+
+ self._current_folder_btn.setEnabled(self._folder_id is not None)
+
+ self._controller = controller
+
+ self._items = items
+ self._prepare_content_data()
+
+ def showEvent(self, event):
+ super(SwitchAssetDialog, self).showEvent(event)
+ self._show_timer.start()
+
+ def refresh(self, init_refresh=False):
+ """Build the need comboboxes with content"""
+ if not self._fill_check and not init_refresh:
+ return
+
+ self._fill_check = False
+
+ validation_state = ValidationState()
+ self._folders_field.refresh()
+ # Set other comboboxes to empty if any document is missing or
+ # any folder of loaded representations is archived.
+ self._is_folder_ok(validation_state)
+ if validation_state.folder_ok:
+ product_values = self._get_product_box_values()
+ self._fill_combobox(product_values, "product")
+ self._is_product_ok(validation_state)
+
+ if validation_state.folder_ok and validation_state.product_ok:
+ repre_values = sorted(self._representations_box_values())
+ self._fill_combobox(repre_values, "repre")
+ self._is_repre_ok(validation_state)
+
+ # Fill comboboxes with values
+ self.set_labels()
+
+ self.apply_validations(validation_state)
+
+ self._build_loaders_menu()
+
+ if init_refresh:
+ # pre select context if possible
+ self._folders_field.set_selected_item(self._init_folder_id)
+ self._products_combox.set_valid_value(self._init_product_name)
+ self._representations_box.set_valid_value(self._init_repre_name)
+
+ self._fill_check = True
+
+ def set_labels(self):
+ folder_label = self._folders_field.get_selected_folder_label()
+ product_label = self._products_combox.get_valid_value()
+ repre_label = self._representations_box.get_valid_value()
+
+ default = "*No changes"
+ self._folder_label.setText(folder_label or default)
+ self._product_label.setText(product_label or default)
+ self._repre_label.setText(repre_label or default)
+
+ def apply_validations(self, validation_state):
+ error_msg = "*Please select"
+ error_sheet = "border: 1px solid red;"
+
+ product_sheet = None
+ repre_sheet = None
+ accept_state = ""
+ if validation_state.folder_ok is False:
+ self._folder_label.setText(error_msg)
+ elif validation_state.product_ok is False:
+ product_sheet = error_sheet
+ self._product_label.setText(error_msg)
+ elif validation_state.repre_ok is False:
+ repre_sheet = error_sheet
+ self._repre_label.setText(error_msg)
+
+ if validation_state.all_ok:
+ accept_state = "1"
+
+ self._folders_field.set_valid(validation_state.folder_ok)
+ self._products_combox.setStyleSheet(product_sheet or "")
+ self._representations_box.setStyleSheet(repre_sheet or "")
+
+ self._accept_btn.setEnabled(validation_state.all_ok)
+ self._set_style_property(self._accept_btn, "state", accept_state)
+
+ def find_last_versions(self, product_ids):
+ project_name = self._project_name
+ return get_last_versions(
+ project_name,
+ subset_ids=product_ids,
+ fields=["_id", "parent", "type"]
+ )
+
+ def _on_show_timer(self):
+ if self._show_counter == 2:
+ self._show_timer.stop()
+ self.refresh(True)
+ else:
+ self._show_counter += 1
+
+ def _prepare_content_data(self):
+ repre_ids = {
+ item["representation"]
+ for item in self._items
+ }
+
+ project_name = self._project_name
+ repres = list(get_representations(
+ project_name,
+ representation_ids=repre_ids,
+ archived=True,
+ ))
+ repres_by_id = {str(repre["_id"]): repre for repre in repres}
+
+ content_repre_docs_by_id = {}
+ inactive_repre_ids = set()
+ missing_repre_ids = set()
+ version_ids = set()
+ for repre_id in repre_ids:
+ repre_doc = repres_by_id.get(repre_id)
+ if repre_doc is None:
+ missing_repre_ids.add(repre_id)
+ elif repres_by_id[repre_id]["type"] == "archived_representation":
+ inactive_repre_ids.add(repre_id)
+ version_ids.add(repre_doc["parent"])
+ else:
+ content_repre_docs_by_id[repre_id] = repre_doc
+ version_ids.add(repre_doc["parent"])
+
+ version_docs = get_versions(
+ project_name,
+ version_ids=version_ids,
+ hero=True
+ )
+ content_version_docs_by_id = {}
+ for version_doc in version_docs:
+ version_id = version_doc["_id"]
+ content_version_docs_by_id[version_id] = version_doc
+
+ missing_version_ids = set()
+ product_ids = set()
+ for version_id in version_ids:
+ version_doc = content_version_docs_by_id.get(version_id)
+ if version_doc is None:
+ missing_version_ids.add(version_id)
+ else:
+ product_ids.add(version_doc["parent"])
+
+ product_docs = get_subsets(
+ project_name, subset_ids=product_ids, archived=True
+ )
+ product_docs_by_id = {sub["_id"]: sub for sub in product_docs}
+
+ folder_ids = set()
+ inactive_product_ids = set()
+ missing_product_ids = set()
+ content_product_docs_by_id = {}
+ for product_id in product_ids:
+ product_doc = product_docs_by_id.get(product_id)
+ if product_doc is None:
+ missing_product_ids.add(product_id)
+ elif product_doc["type"] == "archived_subset":
+ folder_ids.add(product_doc["parent"])
+ inactive_product_ids.add(product_id)
+ else:
+ folder_ids.add(product_doc["parent"])
+ content_product_docs_by_id[product_id] = product_doc
+
+ folder_docs = get_assets(
+ project_name, asset_ids=folder_ids, archived=True
+ )
+ folder_docs_by_id = {
+ folder_doc["_id"]: folder_doc
+ for folder_doc in folder_docs
+ }
+
+ missing_folder_ids = set()
+ inactive_folder_ids = set()
+ content_folder_docs_by_id = {}
+ for folder_id in folder_ids:
+ folder_doc = folder_docs_by_id.get(folder_id)
+ if folder_doc is None:
+ missing_folder_ids.add(folder_id)
+ elif folder_doc["type"] == "archived_asset":
+ inactive_folder_ids.add(folder_id)
+ else:
+ content_folder_docs_by_id[folder_id] = folder_doc
+
+ # stash context values, works only for single representation
+ init_folder_id = None
+ init_product_name = None
+ init_repre_name = None
+ if len(repres) == 1:
+ init_repre_doc = repres[0]
+ init_version_doc = content_version_docs_by_id.get(
+ init_repre_doc["parent"])
+ init_product_doc = None
+ init_folder_doc = None
+ if init_version_doc:
+ init_product_doc = content_product_docs_by_id.get(
+ init_version_doc["parent"]
+ )
+ if init_product_doc:
+ init_folder_doc = content_folder_docs_by_id.get(
+ init_product_doc["parent"]
+ )
+ if init_folder_doc:
+ init_repre_name = init_repre_doc["name"]
+ init_product_name = init_product_doc["name"]
+ init_folder_id = init_folder_doc["_id"]
+
+ self._init_folder_id = init_folder_id
+ self._init_product_name = init_product_name
+ self._init_repre_name = init_repre_name
+
+ self._folder_docs_by_id = content_folder_docs_by_id
+ self._product_docs_by_id = content_product_docs_by_id
+ self._version_docs_by_id = content_version_docs_by_id
+ self._repre_docs_by_id = content_repre_docs_by_id
+
+ self._missing_folder_ids = missing_folder_ids
+ self._missing_product_ids = missing_product_ids
+ self._missing_version_ids = missing_version_ids
+ self._missing_repre_ids = missing_repre_ids
+ self._missing_docs = (
+ bool(missing_folder_ids)
+ or bool(missing_version_ids)
+ or bool(missing_product_ids)
+ or bool(missing_repre_ids)
+ )
+
+ self._inactive_folder_ids = inactive_folder_ids
+ self._inactive_product_ids = inactive_product_ids
+ self._inactive_repre_ids = inactive_repre_ids
+
+ def _combobox_value_changed(self, *args, **kwargs):
+ self.refresh()
+
+ def _build_loaders_menu(self):
+ repre_ids = self._get_current_output_repre_ids()
+ loaders = self._get_loaders(repre_ids)
+ # Get and destroy the action group
+ self._accept_btn.clear_actions()
+
+ if not loaders:
+ return
+
+ # Build new action group
+ group = QtWidgets.QActionGroup(self._accept_btn)
+
+ for loader in loaders:
+ # Label
+ label = getattr(loader, "label", None)
+ if label is None:
+ label = loader.__name__
+
+ action = group.addAction(label)
+ # action = QtWidgets.QAction(label)
+ action.setData(loader)
+
+ # Support font-awesome icons using the `.icon` and `.color`
+ # attributes on plug-ins.
+ icon = getattr(loader, "icon", None)
+ if icon is not None:
+ try:
+ key = "fa.{0}".format(icon)
+ color = getattr(loader, "color", "white")
+ action.setIcon(qtawesome.icon(key, color=color))
+
+ except Exception as exc:
+ print("Unable to set icon for loader {}: {}".format(
+ loader, str(exc)
+ ))
+
+ self._accept_btn.add_action(action)
+
+ group.triggered.connect(self._on_action_clicked)
+
+ def _on_action_clicked(self, action):
+ loader_plugin = action.data()
+ self._trigger_switch(loader_plugin)
+
+ def _get_loaders(self, repre_ids):
+ repre_contexts = None
+ if repre_ids:
+ repre_contexts = get_repres_contexts(repre_ids)
+
+ if not repre_contexts:
+ return list()
+
+ available_loaders = []
+ for loader_plugin in discover_loader_plugins():
+ # Skip loaders without switch method
+ if not hasattr(loader_plugin, "switch"):
+ continue
+
+ # Skip utility loaders
+ if (
+ hasattr(loader_plugin, "is_utility")
+ and loader_plugin.is_utility
+ ):
+ continue
+ available_loaders.append(loader_plugin)
+
+ loaders = None
+ for repre_context in repre_contexts.values():
+ _loaders = set(loaders_from_repre_context(
+ available_loaders, repre_context
+ ))
+ if loaders is None:
+ loaders = _loaders
+ else:
+ loaders = _loaders.intersection(loaders)
+
+ if not loaders:
+ break
+
+ if loaders is None:
+ loaders = []
+ else:
+ loaders = list(loaders)
+
+ return loaders
+
+ def _fill_combobox(self, values, combobox_type):
+ if combobox_type == "product":
+ combobox_widget = self._products_combox
+ elif combobox_type == "repre":
+ combobox_widget = self._representations_box
+ else:
+ return
+ selected_value = combobox_widget.get_valid_value()
+
+ # Fill combobox
+ if values is not None:
+ combobox_widget.populate(list(sorted(values)))
+ if selected_value and selected_value in values:
+ index = None
+ for idx in range(combobox_widget.count()):
+ if selected_value == str(combobox_widget.itemText(idx)):
+ index = idx
+ break
+ if index is not None:
+ combobox_widget.setCurrentIndex(index)
+
+ def _set_style_property(self, widget, name, value):
+ cur_value = widget.property(name)
+ if cur_value == value:
+ return
+ widget.setProperty(name, value)
+ widget.style().polish(widget)
+
+ def _get_current_output_repre_ids(self):
+ # NOTE hero versions are not used because it is expected that
+ # hero version has same representations as latests
+ selected_folder_id = self._folders_field.get_selected_folder_id()
+ selected_product_name = self._products_combox.currentText()
+ selected_repre = self._representations_box.currentText()
+
+ # Nothing is selected
+ # [ ] [ ] [ ]
+ if (
+ not selected_folder_id
+ and not selected_product_name
+ and not selected_repre
+ ):
+ return list(self._repre_docs_by_id.keys())
+
+ # Everything is selected
+ # [x] [x] [x]
+ if selected_folder_id and selected_product_name and selected_repre:
+ return self._get_current_output_repre_ids_xxx(
+ selected_folder_id, selected_product_name, selected_repre
+ )
+
+ # [x] [x] [ ]
+ # If folder and product is selected
+ if selected_folder_id and selected_product_name:
+ return self._get_current_output_repre_ids_xxo(
+ selected_folder_id, selected_product_name
+ )
+
+ # [x] [ ] [x]
+ # If folder and repre is selected
+ if selected_folder_id and selected_repre:
+ return self._get_current_output_repre_ids_xox(
+ selected_folder_id, selected_repre
+ )
+
+ # [x] [ ] [ ]
+ # If folder and product is selected
+ if selected_folder_id:
+ return self._get_current_output_repre_ids_xoo(selected_folder_id)
+
+ # [ ] [x] [x]
+ if selected_product_name and selected_repre:
+ return self._get_current_output_repre_ids_oxx(
+ selected_product_name, selected_repre
+ )
+
+ # [ ] [x] [ ]
+ if selected_product_name:
+ return self._get_current_output_repre_ids_oxo(
+ selected_product_name
+ )
+
+ # [ ] [ ] [x]
+ return self._get_current_output_repre_ids_oox(selected_repre)
+
+ def _get_current_output_repre_ids_xxx(
+ self, folder_id, selected_product_name, selected_repre
+ ):
+ project_name = self._project_name
+ product_doc = get_subset_by_name(
+ project_name,
+ selected_product_name,
+ folder_id,
+ fields=["_id"]
+ )
+
+ product_id = product_doc["_id"]
+ last_versions_by_product_id = self.find_last_versions([product_id])
+ version_doc = last_versions_by_product_id.get(product_id)
+ if not version_doc:
+ return []
+
+ repre_docs = get_representations(
+ project_name,
+ version_ids=[version_doc["_id"]],
+ representation_names=[selected_repre],
+ fields=["_id"]
+ )
+ return [repre_doc["_id"] for repre_doc in repre_docs]
+
+ def _get_current_output_repre_ids_xxo(self, folder_id, product_name):
+ project_name = self._project_name
+ product_doc = get_subset_by_name(
+ project_name,
+ product_name,
+ folder_id,
+ fields=["_id"]
+ )
+ if not product_doc:
+ return []
+
+ repre_names = set()
+ for repre_doc in self._repre_docs_by_id.values():
+ repre_names.add(repre_doc["name"])
+
+ # TODO where to take version ids?
+ version_ids = []
+ repre_docs = get_representations(
+ project_name,
+ representation_names=repre_names,
+ version_ids=version_ids,
+ fields=["_id"]
+ )
+ return [repre_doc["_id"] for repre_doc in repre_docs]
+
+ def _get_current_output_repre_ids_xox(self, folder_id, selected_repre):
+ product_names = {
+ product_doc["name"]
+ for product_doc in self._product_docs_by_id.values()
+ }
+
+ project_name = self._project_name
+ product_docs = get_subsets(
+ project_name,
+ asset_ids=[folder_id],
+ subset_names=product_names,
+ fields=["_id", "name"]
+ )
+ product_name_by_id = {
+ product_doc["_id"]: product_doc["name"]
+ for product_doc in product_docs
+ }
+ product_ids = list(product_name_by_id.keys())
+ last_versions_by_product_id = self.find_last_versions(product_ids)
+ last_version_id_by_product_name = {}
+ for product_id, last_version in last_versions_by_product_id.items():
+ product_name = product_name_by_id[product_id]
+ last_version_id_by_product_name[product_name] = (
+ last_version["_id"]
+ )
+
+ repre_docs = get_representations(
+ project_name,
+ version_ids=last_version_id_by_product_name.values(),
+ representation_names=[selected_repre],
+ fields=["_id"]
+ )
+ return [repre_doc["_id"] for repre_doc in repre_docs]
+
+ def _get_current_output_repre_ids_xoo(self, folder_id):
+ project_name = self._project_name
+ repres_by_product_name = collections.defaultdict(set)
+ for repre_doc in self._repre_docs_by_id.values():
+ version_doc = self._version_docs_by_id[repre_doc["parent"]]
+ product_doc = self._product_docs_by_id[version_doc["parent"]]
+ product_name = product_doc["name"]
+ repres_by_product_name[product_name].add(repre_doc["name"])
+
+ product_docs = list(get_subsets(
+ project_name,
+ asset_ids=[folder_id],
+ subset_names=repres_by_product_name.keys(),
+ fields=["_id", "name"]
+ ))
+ product_name_by_id = {
+ product_doc["_id"]: product_doc["name"]
+ for product_doc in product_docs
+ }
+ product_ids = list(product_name_by_id.keys())
+ last_versions_by_product_id = self.find_last_versions(product_ids)
+ last_version_id_by_product_name = {}
+ for product_id, last_version in last_versions_by_product_id.items():
+ product_name = product_name_by_id[product_id]
+ last_version_id_by_product_name[product_name] = (
+ last_version["_id"]
+ )
+
+ repre_names_by_version_id = {}
+ for product_name, repre_names in repres_by_product_name.items():
+ version_id = last_version_id_by_product_name.get(product_name)
+ # This should not happen but why to crash?
+ if version_id is not None:
+ repre_names_by_version_id[version_id] = list(repre_names)
+
+ repre_docs = get_representations(
+ project_name,
+ names_by_version_ids=repre_names_by_version_id,
+ fields=["_id"]
+ )
+ return [repre_doc["_id"] for repre_doc in repre_docs]
+
+ def _get_current_output_repre_ids_oxx(
+ self, product_name, selected_repre
+ ):
+ project_name = self._project_name
+ product_docs = get_subsets(
+ project_name,
+ asset_ids=self._folder_docs_by_id.keys(),
+ subset_names=[product_name],
+ fields=["_id"]
+ )
+ product_ids = [product_doc["_id"] for product_doc in product_docs]
+ last_versions_by_product_id = self.find_last_versions(product_ids)
+ last_version_ids = [
+ last_version["_id"]
+ for last_version in last_versions_by_product_id.values()
+ ]
+ repre_docs = get_representations(
+ project_name,
+ version_ids=last_version_ids,
+ representation_names=[selected_repre],
+ fields=["_id"]
+ )
+ return [repre_doc["_id"] for repre_doc in repre_docs]
+
+ def _get_current_output_repre_ids_oxo(self, product_name):
+ project_name = self._project_name
+ product_docs = get_subsets(
+ project_name,
+ asset_ids=self._folder_docs_by_id.keys(),
+ subset_names=[product_name],
+ fields=["_id", "parent"]
+ )
+ product_docs_by_id = {
+ product_doc["_id"]: product_doc
+ for product_doc in product_docs
+ }
+ if not product_docs:
+ return list()
+
+ last_versions_by_product_id = self.find_last_versions(
+ product_docs_by_id.keys()
+ )
+
+ product_id_by_version_id = {}
+ for product_id, last_version in last_versions_by_product_id.items():
+ version_id = last_version["_id"]
+ product_id_by_version_id[version_id] = product_id
+
+ if not product_id_by_version_id:
+ return list()
+
+ repre_names_by_folder_id = collections.defaultdict(set)
+ for repre_doc in self._repre_docs_by_id.values():
+ version_doc = self._version_docs_by_id[repre_doc["parent"]]
+ product_doc = self._product_docs_by_id[version_doc["parent"]]
+ folder_doc = self._folder_docs_by_id[product_doc["parent"]]
+ folder_id = folder_doc["_id"]
+ repre_names_by_folder_id[folder_id].add(repre_doc["name"])
+
+ repre_names_by_version_id = {}
+ for last_version_id, product_id in product_id_by_version_id.items():
+ product_doc = product_docs_by_id[product_id]
+ folder_id = product_doc["parent"]
+ repre_names = repre_names_by_folder_id.get(folder_id)
+ if not repre_names:
+ continue
+ repre_names_by_version_id[last_version_id] = repre_names
+
+ repre_docs = get_representations(
+ project_name,
+ names_by_version_ids=repre_names_by_version_id,
+ fields=["_id"]
+ )
+ return [repre_doc["_id"] for repre_doc in repre_docs]
+
+ def _get_current_output_repre_ids_oox(self, selected_repre):
+ project_name = self._project_name
+ repre_docs = get_representations(
+ project_name,
+ representation_names=[selected_repre],
+ version_ids=self._version_docs_by_id.keys(),
+ fields=["_id"]
+ )
+ return [repre_doc["_id"] for repre_doc in repre_docs]
+
+ def _get_product_box_values(self):
+ project_name = self._project_name
+ selected_folder_id = self._folders_field.get_selected_folder_id()
+ if selected_folder_id:
+ folder_ids = [selected_folder_id]
+ else:
+ folder_ids = list(self._folder_docs_by_id.keys())
+
+ product_docs = get_subsets(
+ project_name,
+ asset_ids=folder_ids,
+ fields=["parent", "name"]
+ )
+
+ product_names_by_parent_id = collections.defaultdict(set)
+ for product_doc in product_docs:
+ product_names_by_parent_id[product_doc["parent"]].add(
+ product_doc["name"]
+ )
+
+ possible_product_names = None
+ for product_names in product_names_by_parent_id.values():
+ if possible_product_names is None:
+ possible_product_names = product_names
+ else:
+ possible_product_names = possible_product_names.intersection(
+ product_names)
+
+ if not possible_product_names:
+ break
+
+ if not possible_product_names:
+ return []
+ return list(possible_product_names)
+
+ def _representations_box_values(self):
+ # NOTE hero versions are not used because it is expected that
+ # hero version has same representations as latests
+ project_name = self._project_name
+ selected_folder_id = self._folders_field.get_selected_folder_id()
+ selected_product_name = self._products_combox.currentText()
+
+ # If nothing is selected
+ # [ ] [ ] [?]
+ if not selected_folder_id and not selected_product_name:
+ # Find all representations of selection's products
+ possible_repres = get_representations(
+ project_name,
+ version_ids=self._version_docs_by_id.keys(),
+ fields=["parent", "name"]
+ )
+
+ possible_repres_by_parent = collections.defaultdict(set)
+ for repre in possible_repres:
+ possible_repres_by_parent[repre["parent"]].add(repre["name"])
+
+ output_repres = None
+ for repre_names in possible_repres_by_parent.values():
+ if output_repres is None:
+ output_repres = repre_names
+ else:
+ output_repres = (output_repres & repre_names)
+
+ if not output_repres:
+ break
+
+ return list(output_repres or list())
+
+ # [x] [x] [?]
+ if selected_folder_id and selected_product_name:
+ product_doc = get_subset_by_name(
+ project_name,
+ selected_product_name,
+ selected_folder_id,
+ fields=["_id"]
+ )
+
+ product_id = product_doc["_id"]
+ last_versions_by_product_id = self.find_last_versions([product_id])
+ version_doc = last_versions_by_product_id.get(product_id)
+ repre_docs = get_representations(
+ project_name,
+ version_ids=[version_doc["_id"]],
+ fields=["name"]
+ )
+ return [
+ repre_doc["name"]
+ for repre_doc in repre_docs
+ ]
+
+ # [x] [ ] [?]
+ # If only folder is selected
+ if selected_folder_id:
+ # Filter products by names from content
+ product_names = {
+ product_doc["name"]
+ for product_doc in self._product_docs_by_id.values()
+ }
+
+ product_docs = get_subsets(
+ project_name,
+ asset_ids=[selected_folder_id],
+ subset_names=product_names,
+ fields=["_id"]
+ )
+ product_ids = {
+ product_doc["_id"]
+ for product_doc in product_docs
+ }
+ if not product_ids:
+ return list()
+
+ last_versions_by_product_id = self.find_last_versions(product_ids)
+ product_id_by_version_id = {}
+ for product_id, last_version in (
+ last_versions_by_product_id.items()
+ ):
+ version_id = last_version["_id"]
+ product_id_by_version_id[version_id] = product_id
+
+ if not product_id_by_version_id:
+ return list()
+
+ repre_docs = list(get_representations(
+ project_name,
+ version_ids=product_id_by_version_id.keys(),
+ fields=["name", "parent"]
+ ))
+ if not repre_docs:
+ return list()
+
+ repre_names_by_parent = collections.defaultdict(set)
+ for repre_doc in repre_docs:
+ repre_names_by_parent[repre_doc["parent"]].add(
+ repre_doc["name"]
+ )
+
+ available_repres = None
+ for repre_names in repre_names_by_parent.values():
+ if available_repres is None:
+ available_repres = repre_names
+ continue
+
+ available_repres = available_repres.intersection(repre_names)
+
+ return list(available_repres)
+
+ # [ ] [x] [?]
+ product_docs = list(get_subsets(
+ project_name,
+ asset_ids=self._folder_docs_by_id.keys(),
+ subset_names=[selected_product_name],
+ fields=["_id", "parent"]
+ ))
+ if not product_docs:
+ return list()
+
+ product_docs_by_id = {
+ product_doc["_id"]: product_doc
+ for product_doc in product_docs
+ }
+ last_versions_by_product_id = self.find_last_versions(
+ product_docs_by_id.keys()
+ )
+
+ product_id_by_version_id = {}
+ for product_id, last_version in last_versions_by_product_id.items():
+ version_id = last_version["_id"]
+ product_id_by_version_id[version_id] = product_id
+
+ if not product_id_by_version_id:
+ return list()
+
+ repre_docs = list(
+ get_representations(
+ project_name,
+ version_ids=product_id_by_version_id.keys(),
+ fields=["name", "parent"]
+ )
+ )
+ if not repre_docs:
+ return list()
+
+ repre_names_by_folder_id = collections.defaultdict(set)
+ for repre_doc in repre_docs:
+ product_id = product_id_by_version_id[repre_doc["parent"]]
+ folder_id = product_docs_by_id[product_id]["parent"]
+ repre_names_by_folder_id[folder_id].add(repre_doc["name"])
+
+ available_repres = None
+ for repre_names in repre_names_by_folder_id.values():
+ if available_repres is None:
+ available_repres = repre_names
+ continue
+
+ available_repres = available_repres.intersection(repre_names)
+
+ return list(available_repres)
+
+ def _is_folder_ok(self, validation_state):
+ selected_folder_id = self._folders_field.get_selected_folder_id()
+ if (
+ selected_folder_id is None
+ and (self._missing_docs or self._inactive_folder_ids)
+ ):
+ validation_state.folder_ok = False
+
+ def _is_product_ok(self, validation_state):
+ selected_folder_id = self._folders_field.get_selected_folder_id()
+ selected_product_name = self._products_combox.get_valid_value()
+
+ # [?] [x] [?]
+ # If product is selected then must be ok
+ if selected_product_name is not None:
+ return
+
+ # [ ] [ ] [?]
+ if selected_folder_id is None:
+ # If there were archived products and folder is not selected
+ if self._inactive_product_ids:
+ validation_state.product_ok = False
+ return
+
+ # [x] [ ] [?]
+ project_name = self._project_name
+ product_docs = get_subsets(
+ project_name, asset_ids=[selected_folder_id], fields=["name"]
+ )
+
+ product_names = set(
+ product_doc["name"]
+ for product_doc in product_docs
+ )
+
+ for product_doc in self._product_docs_by_id.values():
+ if product_doc["name"] not in product_names:
+ validation_state.product_ok = False
+ break
+
+ def _is_repre_ok(self, validation_state):
+ selected_folder_id = self._folders_field.get_selected_folder_id()
+ selected_product_name = self._products_combox.get_valid_value()
+ selected_repre = self._representations_box.get_valid_value()
+
+ # [?] [?] [x]
+ # If product is selected then must be ok
+ if selected_repre is not None:
+ return
+
+ # [ ] [ ] [ ]
+ if selected_folder_id is None and selected_product_name is None:
+ if (
+ self._inactive_repre_ids
+ or self._missing_version_ids
+ or self._missing_repre_ids
+ ):
+ validation_state.repre_ok = False
+ return
+
+ # [x] [x] [ ]
+ project_name = self._project_name
+ if (
+ selected_folder_id is not None
+ and selected_product_name is not None
+ ):
+ product_doc = get_subset_by_name(
+ project_name,
+ selected_product_name,
+ selected_folder_id,
+ fields=["_id"]
+ )
+ product_id = product_doc["_id"]
+ last_versions_by_product_id = self.find_last_versions([product_id])
+ last_version = last_versions_by_product_id.get(product_id)
+ if not last_version:
+ validation_state.repre_ok = False
+ return
+
+ repre_docs = get_representations(
+ project_name,
+ version_ids=[last_version["_id"]],
+ fields=["name"]
+ )
+
+ repre_names = set(
+ repre_doc["name"]
+ for repre_doc in repre_docs
+ )
+ for repre_doc in self._repre_docs_by_id.values():
+ if repre_doc["name"] not in repre_names:
+ validation_state.repre_ok = False
+ break
+ return
+
+ # [x] [ ] [ ]
+ if selected_folder_id is not None:
+ product_docs = list(get_subsets(
+ project_name,
+ asset_ids=[selected_folder_id],
+ fields=["_id", "name"]
+ ))
+
+ product_name_by_id = {}
+ product_ids = set()
+ for product_doc in product_docs:
+ product_id = product_doc["_id"]
+ product_ids.add(product_id)
+ product_name_by_id[product_id] = product_doc["name"]
+
+ last_versions_by_product_id = self.find_last_versions(product_ids)
+
+ product_id_by_version_id = {}
+ for product_id, last_version in (
+ last_versions_by_product_id.items()
+ ):
+ version_id = last_version["_id"]
+ product_id_by_version_id[version_id] = product_id
+
+ repre_docs = get_representations(
+ project_name,
+ version_ids=product_id_by_version_id.keys(),
+ fields=["name", "parent"]
+ )
+ repres_by_product_name = collections.defaultdict(set)
+ for repre_doc in repre_docs:
+ product_id = product_id_by_version_id[repre_doc["parent"]]
+ product_name = product_name_by_id[product_id]
+ repres_by_product_name[product_name].add(repre_doc["name"])
+
+ for repre_doc in self._repre_docs_by_id.values():
+ version_doc = self._version_docs_by_id[repre_doc["parent"]]
+ product_doc = self._product_docs_by_id[version_doc["parent"]]
+ repre_names = repres_by_product_name[product_doc["name"]]
+ if repre_doc["name"] not in repre_names:
+ validation_state.repre_ok = False
+ break
+ return
+
+ # [ ] [x] [ ]
+ # Product documents
+ product_docs = get_subsets(
+ project_name,
+ asset_ids=self._folder_docs_by_id.keys(),
+ subset_names=[selected_product_name],
+ fields=["_id", "name", "parent"]
+ )
+ product_docs_by_id = {}
+ for product_doc in product_docs:
+ product_docs_by_id[product_doc["_id"]] = product_doc
+
+ last_versions_by_product_id = self.find_last_versions(
+ product_docs_by_id.keys()
+ )
+ product_id_by_version_id = {}
+ for product_id, last_version in last_versions_by_product_id.items():
+ version_id = last_version["_id"]
+ product_id_by_version_id[version_id] = product_id
+
+ repre_docs = get_representations(
+ project_name,
+ version_ids=product_id_by_version_id.keys(),
+ fields=["name", "parent"]
+ )
+ repres_by_folder_id = collections.defaultdict(set)
+ for repre_doc in repre_docs:
+ product_id = product_id_by_version_id[repre_doc["parent"]]
+ folder_id = product_docs_by_id[product_id]["parent"]
+ repres_by_folder_id[folder_id].add(repre_doc["name"])
+
+ for repre_doc in self._repre_docs_by_id.values():
+ version_doc = self._version_docs_by_id[repre_doc["parent"]]
+ product_doc = self._product_docs_by_id[version_doc["parent"]]
+ folder_id = product_doc["parent"]
+ repre_names = repres_by_folder_id[folder_id]
+ if repre_doc["name"] not in repre_names:
+ validation_state.repre_ok = False
+ break
+
+ def _on_current_folder(self):
+ # Set initial folder as current.
+ folder_id = self._controller.get_current_folder_id()
+ if not folder_id:
+ return
+
+ selected_folder_id = self._folders_field.get_selected_folder_id()
+ if folder_id == selected_folder_id:
+ return
+
+ self._folders_field.set_selected_item(folder_id)
+ self._combobox_value_changed()
+
+ def _on_accept(self):
+ self._trigger_switch()
+
+ def _trigger_switch(self, loader=None):
+ # Use None when not a valid value or when placeholder value
+ selected_folder_id = self._folders_field.get_selected_folder_id()
+ selected_product_name = self._products_combox.get_valid_value()
+ selected_representation = self._representations_box.get_valid_value()
+
+ project_name = self._project_name
+ if selected_folder_id:
+ folder_ids = {selected_folder_id}
+ else:
+ folder_ids = set(self._folder_docs_by_id.keys())
+
+ product_names = None
+ if selected_product_name:
+ product_names = [selected_product_name]
+
+ product_docs = list(get_subsets(
+ project_name,
+ subset_names=product_names,
+ asset_ids=folder_ids
+ ))
+ product_ids = set()
+ product_docs_by_parent_and_name = collections.defaultdict(dict)
+ for product_doc in product_docs:
+ product_ids.add(product_doc["_id"])
+ folder_id = product_doc["parent"]
+ name = product_doc["name"]
+ product_docs_by_parent_and_name[folder_id][name] = product_doc
+
+ # versions
+ _version_docs = get_versions(project_name, subset_ids=product_ids)
+ version_docs = list(reversed(
+ sorted(_version_docs, key=lambda item: item["name"])
+ ))
+
+ hero_version_docs = list(get_hero_versions(
+ project_name, subset_ids=product_ids
+ ))
+
+ version_ids = set()
+ version_docs_by_parent_id = {}
+ for version_doc in version_docs:
+ parent_id = version_doc["parent"]
+ if parent_id not in version_docs_by_parent_id:
+ version_ids.add(version_doc["_id"])
+ version_docs_by_parent_id[parent_id] = version_doc
+
+ hero_version_docs_by_parent_id = {}
+ for hero_version_doc in hero_version_docs:
+ version_ids.add(hero_version_doc["_id"])
+ parent_id = hero_version_doc["parent"]
+ hero_version_docs_by_parent_id[parent_id] = hero_version_doc
+
+ repre_docs = get_representations(
+ project_name, version_ids=version_ids
+ )
+ repre_docs_by_parent_id_by_name = collections.defaultdict(dict)
+ for repre_doc in repre_docs:
+ parent_id = repre_doc["parent"]
+ name = repre_doc["name"]
+ repre_docs_by_parent_id_by_name[parent_id][name] = repre_doc
+
+ for container in self._items:
+ self._switch_container(
+ container,
+ loader,
+ selected_folder_id,
+ selected_product_name,
+ selected_representation,
+ product_docs_by_parent_and_name,
+ version_docs_by_parent_id,
+ hero_version_docs_by_parent_id,
+ repre_docs_by_parent_id_by_name,
+ )
+
+ self.switched.emit()
+
+ self.close()
+
+ def _switch_container(
+ self,
+ container,
+ loader,
+ selected_folder_id,
+ product_name,
+ selected_representation,
+ product_docs_by_parent_and_name,
+ version_docs_by_parent_id,
+ hero_version_docs_by_parent_id,
+ repre_docs_by_parent_id_by_name,
+ ):
+ container_repre_id = container["representation"]
+ container_repre = self._repre_docs_by_id[container_repre_id]
+ container_repre_name = container_repre["name"]
+ container_version_id = container_repre["parent"]
+
+ container_version = self._version_docs_by_id[container_version_id]
+
+ container_product_id = container_version["parent"]
+ container_product = self._product_docs_by_id[container_product_id]
+
+ if selected_folder_id:
+ folder_id = selected_folder_id
+ else:
+ folder_id = container_product["parent"]
+
+ products_by_name = product_docs_by_parent_and_name[folder_id]
+ if product_name:
+ product_doc = products_by_name[product_name]
+ else:
+ product_doc = products_by_name[container_product["name"]]
+
+ repre_doc = None
+ product_id = product_doc["_id"]
+ if container_version["type"] == "hero_version":
+ hero_version = hero_version_docs_by_parent_id.get(
+ product_id
+ )
+ if hero_version:
+ _repres = repre_docs_by_parent_id_by_name.get(
+ hero_version["_id"]
+ )
+ if selected_representation:
+ repre_doc = _repres.get(selected_representation)
+ else:
+ repre_doc = _repres.get(container_repre_name)
+
+ if not repre_doc:
+ version_doc = version_docs_by_parent_id[product_id]
+ version_id = version_doc["_id"]
+ repres_by_name = repre_docs_by_parent_id_by_name[version_id]
+ if selected_representation:
+ repre_doc = repres_by_name[selected_representation]
+ else:
+ repre_doc = repres_by_name[container_repre_name]
+
+ error = None
+ try:
+ switch_container(container, repre_doc, loader)
+ except (
+ LoaderSwitchNotImplementedError,
+ IncompatibleLoaderError,
+ LoaderNotFoundError,
+ ) as exc:
+ error = str(exc)
+ except Exception:
+ error = (
+ "Switch asset failed. "
+ "Search console log for more details."
+ )
+ if error is not None:
+ log.warning((
+ "Couldn't switch asset."
+ "See traceback for more information."
+ ), exc_info=True)
+ dialog = QtWidgets.QMessageBox(self)
+ dialog.setWindowTitle("Switch asset failed")
+ dialog.setText(error)
+ dialog.exec_()
diff --git a/openpype/tools/ayon_sceneinventory/switch_dialog/folders_input.py b/openpype/tools/ayon_sceneinventory/switch_dialog/folders_input.py
new file mode 100644
index 0000000000..699c62371a
--- /dev/null
+++ b/openpype/tools/ayon_sceneinventory/switch_dialog/folders_input.py
@@ -0,0 +1,307 @@
+from qtpy import QtWidgets, QtCore
+import qtawesome
+
+from openpype.tools.utils import (
+ PlaceholderLineEdit,
+ BaseClickableFrame,
+ set_style_property,
+)
+from openpype.tools.ayon_utils.widgets import FoldersWidget
+
+NOT_SET = object()
+
+
+class ClickableLineEdit(QtWidgets.QLineEdit):
+ """QLineEdit capturing left mouse click.
+
+ Triggers `clicked` signal on mouse click.
+ """
+ clicked = QtCore.Signal()
+
+ def __init__(self, *args, **kwargs):
+ super(ClickableLineEdit, self).__init__(*args, **kwargs)
+ self.setReadOnly(True)
+ self._mouse_pressed = False
+
+ def mousePressEvent(self, event):
+ if event.button() == QtCore.Qt.LeftButton:
+ self._mouse_pressed = True
+ event.accept()
+
+ def mouseMoveEvent(self, event):
+ event.accept()
+
+ def mouseReleaseEvent(self, event):
+ if self._mouse_pressed:
+ self._mouse_pressed = False
+ if self.rect().contains(event.pos()):
+ self.clicked.emit()
+ event.accept()
+
+ def mouseDoubleClickEvent(self, event):
+ event.accept()
+
+
+class ControllerWrap:
+ def __init__(self, controller):
+ self._controller = controller
+ self._selected_folder_id = None
+
+ def emit_event(self, *args, **kwargs):
+ self._controller.emit_event(*args, **kwargs)
+
+ def register_event_callback(self, *args, **kwargs):
+ self._controller.register_event_callback(*args, **kwargs)
+
+ def get_current_project_name(self):
+ return self._controller.get_current_project_name()
+
+ def get_folder_items(self, *args, **kwargs):
+ return self._controller.get_folder_items(*args, **kwargs)
+
+ def set_selected_folder(self, folder_id):
+ self._selected_folder_id = folder_id
+
+ def get_selected_folder_id(self):
+ return self._selected_folder_id
+
+
+class FoldersDialog(QtWidgets.QDialog):
+ """Dialog to select asset for a context of instance."""
+
+ def __init__(self, controller, parent):
+ super(FoldersDialog, self).__init__(parent)
+ self.setWindowTitle("Select folder")
+
+ filter_input = PlaceholderLineEdit(self)
+ filter_input.setPlaceholderText("Filter folders..")
+
+ controller_wrap = ControllerWrap(controller)
+ folders_widget = FoldersWidget(controller_wrap, self)
+ folders_widget.set_deselectable(True)
+
+ ok_btn = QtWidgets.QPushButton("OK", self)
+ cancel_btn = QtWidgets.QPushButton("Cancel", self)
+
+ btns_layout = QtWidgets.QHBoxLayout()
+ btns_layout.addStretch(1)
+ btns_layout.addWidget(ok_btn)
+ btns_layout.addWidget(cancel_btn)
+
+ layout = QtWidgets.QVBoxLayout(self)
+ layout.addWidget(filter_input, 0)
+ layout.addWidget(folders_widget, 1)
+ layout.addLayout(btns_layout, 0)
+
+ folders_widget.double_clicked.connect(self._on_ok_clicked)
+ folders_widget.refreshed.connect(self._on_folders_refresh)
+ filter_input.textChanged.connect(self._on_filter_change)
+ ok_btn.clicked.connect(self._on_ok_clicked)
+ cancel_btn.clicked.connect(self._on_cancel_clicked)
+
+ self._filter_input = filter_input
+ self._ok_btn = ok_btn
+ self._cancel_btn = cancel_btn
+
+ self._folders_widget = folders_widget
+ self._controller_wrap = controller_wrap
+
+ # Set selected folder only when user confirms the dialog
+ self._selected_folder_id = None
+ self._selected_folder_label = None
+
+ self._folder_id_to_select = NOT_SET
+
+ self._first_show = True
+ self._default_height = 500
+
+ def showEvent(self, event):
+ """Refresh asset model on show."""
+ super(FoldersDialog, self).showEvent(event)
+ if self._first_show:
+ self._first_show = False
+ self._on_first_show()
+
+ def refresh(self):
+ project_name = self._controller_wrap.get_current_project_name()
+ self._folders_widget.set_project_name(project_name)
+
+ def _on_first_show(self):
+ center = self.rect().center()
+ size = self.size()
+ size.setHeight(self._default_height)
+
+ self.resize(size)
+ new_pos = self.mapToGlobal(center)
+ new_pos.setX(new_pos.x() - int(self.width() / 2))
+ new_pos.setY(new_pos.y() - int(self.height() / 2))
+ self.move(new_pos)
+
+ def _on_folders_refresh(self):
+ if self._folder_id_to_select is NOT_SET:
+ return
+ self._folders_widget.set_selected_folder(self._folder_id_to_select)
+ self._folder_id_to_select = NOT_SET
+
+ def _on_filter_change(self, text):
+ """Trigger change of filter of folders."""
+
+ self._folders_widget.set_name_filter(text)
+
+ def _on_cancel_clicked(self):
+ self.done(0)
+
+ def _on_ok_clicked(self):
+ self._selected_folder_id = (
+ self._folders_widget.get_selected_folder_id()
+ )
+ self._selected_folder_label = (
+ self._folders_widget.get_selected_folder_label()
+ )
+ self.done(1)
+
+ def set_selected_folder(self, folder_id):
+ """Change preselected folder before showing the dialog.
+
+ This also resets model and clean filter.
+ """
+
+ if (
+ self._folders_widget.is_refreshing
+ or self._folders_widget.get_project_name() is None
+ ):
+ self._folder_id_to_select = folder_id
+ else:
+ self._folders_widget.set_selected_folder(folder_id)
+
+ def get_selected_folder_id(self):
+ """Get selected folder id.
+
+ Returns:
+ Union[str, None]: Selected folder id or None if nothing
+ is selected.
+ """
+ return self._selected_folder_id
+
+ def get_selected_folder_label(self):
+ return self._selected_folder_label
+
+
+class FoldersField(BaseClickableFrame):
+ """Field where asset name of selected instance/s is showed.
+
+ Click on the field will trigger `FoldersDialog`.
+ """
+ value_changed = QtCore.Signal()
+
+ def __init__(self, controller, parent):
+ super(FoldersField, self).__init__(parent)
+ self.setObjectName("AssetNameInputWidget")
+
+ # Don't use 'self' for parent!
+ # - this widget has specific styles
+ dialog = FoldersDialog(controller, parent)
+
+ name_input = ClickableLineEdit(self)
+ name_input.setObjectName("AssetNameInput")
+
+ icon = qtawesome.icon("fa.window-maximize", color="white")
+ icon_btn = QtWidgets.QPushButton(self)
+ icon_btn.setIcon(icon)
+ icon_btn.setObjectName("AssetNameInputButton")
+
+ layout = QtWidgets.QHBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(0)
+ layout.addWidget(name_input, 1)
+ layout.addWidget(icon_btn, 0)
+
+ # Make sure all widgets are vertically extended to highest widget
+ for widget in (
+ name_input,
+ icon_btn
+ ):
+ w_size_policy = widget.sizePolicy()
+ w_size_policy.setVerticalPolicy(
+ QtWidgets.QSizePolicy.MinimumExpanding)
+ widget.setSizePolicy(w_size_policy)
+
+ size_policy = self.sizePolicy()
+ size_policy.setVerticalPolicy(QtWidgets.QSizePolicy.Maximum)
+ self.setSizePolicy(size_policy)
+
+ name_input.clicked.connect(self._mouse_release_callback)
+ icon_btn.clicked.connect(self._mouse_release_callback)
+ dialog.finished.connect(self._on_dialog_finish)
+
+ self._controller = controller
+ self._dialog = dialog
+ self._name_input = name_input
+ self._icon_btn = icon_btn
+
+ self._selected_folder_id = None
+ self._selected_folder_label = None
+ self._selected_items = []
+ self._is_valid = True
+
+ def refresh(self):
+ self._dialog.refresh()
+
+ def is_valid(self):
+ """Is asset valid."""
+ return self._is_valid
+
+ def get_selected_folder_id(self):
+ """Selected asset names."""
+ return self._selected_folder_id
+
+ def get_selected_folder_label(self):
+ return self._selected_folder_label
+
+ def set_text(self, text):
+ """Set text in text field.
+
+ Does not change selected items (assets).
+ """
+ self._name_input.setText(text)
+
+ def set_valid(self, is_valid):
+ state = ""
+ if not is_valid:
+ state = "invalid"
+ self._set_state_property(state)
+
+ def set_selected_item(self, folder_id=None, folder_label=None):
+ """Set folder for selection.
+
+ Args:
+ folder_id (Optional[str]): Folder id to select.
+ folder_label (Optional[str]): Folder label.
+ """
+
+ self._selected_folder_id = folder_id
+ if not folder_id:
+ folder_label = None
+ elif folder_id and not folder_label:
+ folder_label = self._controller.get_folder_label(folder_id)
+ self._selected_folder_label = folder_label
+ self.set_text(folder_label if folder_label else "")
+
+ def _on_dialog_finish(self, result):
+ if not result:
+ return
+
+ folder_id = self._dialog.get_selected_folder_id()
+ folder_label = self._dialog.get_selected_folder_label()
+ self.set_selected_item(folder_id, folder_label)
+
+ self.value_changed.emit()
+
+ def _mouse_release_callback(self):
+ self._dialog.set_selected_folder(self._selected_folder_id)
+ self._dialog.open()
+
+ def _set_state_property(self, state):
+ set_style_property(self, "state", state)
+ set_style_property(self._name_input, "state", state)
+ set_style_property(self._icon_btn, "state", state)
diff --git a/openpype/tools/ayon_sceneinventory/switch_dialog/widgets.py b/openpype/tools/ayon_sceneinventory/switch_dialog/widgets.py
new file mode 100644
index 0000000000..50a49e0ce1
--- /dev/null
+++ b/openpype/tools/ayon_sceneinventory/switch_dialog/widgets.py
@@ -0,0 +1,94 @@
+from qtpy import QtWidgets, QtCore
+
+from openpype import style
+
+
+class ButtonWithMenu(QtWidgets.QToolButton):
+ def __init__(self, parent=None):
+ super(ButtonWithMenu, self).__init__(parent)
+
+ self.setObjectName("ButtonWithMenu")
+
+ self.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
+ menu = QtWidgets.QMenu(self)
+
+ self.setMenu(menu)
+
+ self._menu = menu
+ self._actions = []
+
+ def menu(self):
+ return self._menu
+
+ def clear_actions(self):
+ if self._menu is not None:
+ self._menu.clear()
+ self._actions = []
+
+ def add_action(self, action):
+ self._actions.append(action)
+ self._menu.addAction(action)
+
+ def _on_action_trigger(self):
+ action = self.sender()
+ if action not in self._actions:
+ return
+ action.trigger()
+
+
+class SearchComboBox(QtWidgets.QComboBox):
+ """Searchable ComboBox with empty placeholder value as first value"""
+
+ def __init__(self, parent):
+ super(SearchComboBox, self).__init__(parent)
+
+ self.setEditable(True)
+ self.setInsertPolicy(QtWidgets.QComboBox.NoInsert)
+
+ combobox_delegate = QtWidgets.QStyledItemDelegate(self)
+ self.setItemDelegate(combobox_delegate)
+
+ completer = self.completer()
+ completer.setCompletionMode(
+ QtWidgets.QCompleter.PopupCompletion
+ )
+ completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
+
+ completer_view = completer.popup()
+ completer_view.setObjectName("CompleterView")
+ completer_delegate = QtWidgets.QStyledItemDelegate(completer_view)
+ completer_view.setItemDelegate(completer_delegate)
+ completer_view.setStyleSheet(style.load_stylesheet())
+
+ self._combobox_delegate = combobox_delegate
+
+ self._completer_delegate = completer_delegate
+ self._completer = completer
+
+ def set_placeholder(self, placeholder):
+ self.lineEdit().setPlaceholderText(placeholder)
+
+ def populate(self, items):
+ self.clear()
+ self.addItems([""]) # ensure first item is placeholder
+ self.addItems(items)
+
+ def get_valid_value(self):
+ """Return the current text if it's a valid value else None
+
+ Note: The empty placeholder value is valid and returns as ""
+
+ """
+
+ text = self.currentText()
+ lookup = set(self.itemText(i) for i in range(self.count()))
+ if text not in lookup:
+ return None
+
+ return text or None
+
+ def set_valid_value(self, value):
+ """Try to locate 'value' and pre-select it in dropdown."""
+ index = self.findText(value)
+ if index > -1:
+ self.setCurrentIndex(index)
diff --git a/openpype/tools/ayon_sceneinventory/view.py b/openpype/tools/ayon_sceneinventory/view.py
new file mode 100644
index 0000000000..039b498b1b
--- /dev/null
+++ b/openpype/tools/ayon_sceneinventory/view.py
@@ -0,0 +1,825 @@
+import uuid
+import collections
+import logging
+import itertools
+from functools import partial
+
+from qtpy import QtWidgets, QtCore
+import qtawesome
+
+from openpype.client import (
+ get_version_by_id,
+ get_versions,
+ get_hero_versions,
+ get_representation_by_id,
+ get_representations,
+)
+from openpype import style
+from openpype.pipeline import (
+ HeroVersionType,
+ update_container,
+ remove_container,
+ discover_inventory_actions,
+)
+from openpype.tools.utils.lib import (
+ iter_model_rows,
+ format_version
+)
+
+from .switch_dialog import SwitchAssetDialog
+from .model import InventoryModel
+
+
+DEFAULT_COLOR = "#fb9c15"
+
+log = logging.getLogger("SceneInventory")
+
+
+class SceneInventoryView(QtWidgets.QTreeView):
+ data_changed = QtCore.Signal()
+ hierarchy_view_changed = QtCore.Signal(bool)
+
+ def __init__(self, controller, parent):
+ super(SceneInventoryView, self).__init__(parent=parent)
+
+ # view settings
+ self.setIndentation(12)
+ self.setAlternatingRowColors(True)
+ self.setSortingEnabled(True)
+ self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+ self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+
+ self.customContextMenuRequested.connect(self._show_right_mouse_menu)
+
+ self._hierarchy_view = False
+ self._selected = None
+
+ self._controller = controller
+
+ def _set_hierarchy_view(self, enabled):
+ if enabled == self._hierarchy_view:
+ return
+ self._hierarchy_view = enabled
+ self.hierarchy_view_changed.emit(enabled)
+
+ def _enter_hierarchy(self, items):
+ self._selected = set(i["objectName"] for i in items)
+ self._set_hierarchy_view(True)
+ self.data_changed.emit()
+ self.expandToDepth(1)
+ self.setStyleSheet("""
+ QTreeView {
+ border-color: #fb9c15;
+ }
+ """)
+
+ def _leave_hierarchy(self):
+ self._set_hierarchy_view(False)
+ self.data_changed.emit()
+ self.setStyleSheet("QTreeView {}")
+
+ def _build_item_menu_for_selection(self, items, menu):
+ # Exclude items that are "NOT FOUND" since setting versions, updating
+ # and removal won't work for those items.
+ items = [item for item in items if not item.get("isNotFound")]
+ if not items:
+ return
+
+ # An item might not have a representation, for example when an item
+ # is listed as "NOT FOUND"
+ repre_ids = set()
+ for item in items:
+ repre_id = item["representation"]
+ try:
+ uuid.UUID(repre_id)
+ repre_ids.add(repre_id)
+ except ValueError:
+ pass
+
+ project_name = self._controller.get_current_project_name()
+ repre_docs = get_representations(
+ project_name, representation_ids=repre_ids, fields=["parent"]
+ )
+
+ version_ids = {
+ repre_doc["parent"]
+ for repre_doc in repre_docs
+ }
+
+ loaded_versions = get_versions(
+ project_name, version_ids=version_ids, hero=True
+ )
+
+ loaded_hero_versions = []
+ versions_by_parent_id = collections.defaultdict(list)
+ subset_ids = set()
+ for version in loaded_versions:
+ if version["type"] == "hero_version":
+ loaded_hero_versions.append(version)
+ else:
+ parent_id = version["parent"]
+ versions_by_parent_id[parent_id].append(version)
+ subset_ids.add(parent_id)
+
+ all_versions = get_versions(
+ project_name, subset_ids=subset_ids, hero=True
+ )
+ hero_versions = []
+ versions = []
+ for version in all_versions:
+ if version["type"] == "hero_version":
+ hero_versions.append(version)
+ else:
+ versions.append(version)
+
+ has_loaded_hero_versions = len(loaded_hero_versions) > 0
+ has_available_hero_version = len(hero_versions) > 0
+ has_outdated = False
+
+ for version in versions:
+ parent_id = version["parent"]
+ current_versions = versions_by_parent_id[parent_id]
+ for current_version in current_versions:
+ if current_version["name"] < version["name"]:
+ has_outdated = True
+ break
+
+ if has_outdated:
+ break
+
+ switch_to_versioned = None
+ if has_loaded_hero_versions:
+ def _on_switch_to_versioned(items):
+ repre_ids = {
+ item["representation"]
+ for item in items
+ }
+
+ repre_docs = get_representations(
+ project_name,
+ representation_ids=repre_ids,
+ fields=["parent"]
+ )
+
+ version_ids = set()
+ version_id_by_repre_id = {}
+ for repre_doc in repre_docs:
+ version_id = repre_doc["parent"]
+ repre_id = str(repre_doc["_id"])
+ version_id_by_repre_id[repre_id] = version_id
+ version_ids.add(version_id)
+
+ hero_versions = get_hero_versions(
+ project_name,
+ version_ids=version_ids,
+ fields=["version_id"]
+ )
+
+ hero_src_version_ids = set()
+ for hero_version in hero_versions:
+ version_id = hero_version["version_id"]
+ hero_src_version_ids.add(version_id)
+ hero_version_id = hero_version["_id"]
+ for _repre_id, current_version_id in (
+ version_id_by_repre_id.items()
+ ):
+ if current_version_id == hero_version_id:
+ version_id_by_repre_id[_repre_id] = version_id
+
+ version_docs = get_versions(
+ project_name,
+ version_ids=hero_src_version_ids,
+ fields=["name"]
+ )
+ version_name_by_id = {}
+ for version_doc in version_docs:
+ version_name_by_id[version_doc["_id"]] = \
+ version_doc["name"]
+
+ # Specify version per item to update to
+ update_items = []
+ update_versions = []
+ for item in items:
+ repre_id = item["representation"]
+ version_id = version_id_by_repre_id.get(repre_id)
+ version_name = version_name_by_id.get(version_id)
+ if version_name is not None:
+ update_items.append(item)
+ update_versions.append(version_name)
+ self._update_containers(update_items, update_versions)
+
+ update_icon = qtawesome.icon(
+ "fa.asterisk",
+ color=DEFAULT_COLOR
+ )
+ switch_to_versioned = QtWidgets.QAction(
+ update_icon,
+ "Switch to versioned",
+ menu
+ )
+ switch_to_versioned.triggered.connect(
+ lambda: _on_switch_to_versioned(items)
+ )
+
+ update_to_latest_action = None
+ if has_outdated or has_loaded_hero_versions:
+ update_icon = qtawesome.icon(
+ "fa.angle-double-up",
+ color=DEFAULT_COLOR
+ )
+ update_to_latest_action = QtWidgets.QAction(
+ update_icon,
+ "Update to latest",
+ menu
+ )
+ update_to_latest_action.triggered.connect(
+ lambda: self._update_containers(items, version=-1)
+ )
+
+ change_to_hero = None
+ if has_available_hero_version:
+ # TODO change icon
+ change_icon = qtawesome.icon(
+ "fa.asterisk",
+ color="#00b359"
+ )
+ change_to_hero = QtWidgets.QAction(
+ change_icon,
+ "Change to hero",
+ menu
+ )
+ change_to_hero.triggered.connect(
+ lambda: self._update_containers(items,
+ version=HeroVersionType(-1))
+ )
+
+ # set version
+ set_version_icon = qtawesome.icon("fa.hashtag", color=DEFAULT_COLOR)
+ set_version_action = QtWidgets.QAction(
+ set_version_icon,
+ "Set version",
+ menu
+ )
+ set_version_action.triggered.connect(
+ lambda: self._show_version_dialog(items))
+
+ # switch folder
+ switch_folder_icon = qtawesome.icon("fa.sitemap", color=DEFAULT_COLOR)
+ switch_folder_action = QtWidgets.QAction(
+ switch_folder_icon,
+ "Switch Folder",
+ menu
+ )
+ switch_folder_action.triggered.connect(
+ lambda: self._show_switch_dialog(items))
+
+ # remove
+ remove_icon = qtawesome.icon("fa.remove", color=DEFAULT_COLOR)
+ remove_action = QtWidgets.QAction(remove_icon, "Remove items", menu)
+ remove_action.triggered.connect(
+ lambda: self._show_remove_warning_dialog(items))
+
+ # add the actions
+ if switch_to_versioned:
+ menu.addAction(switch_to_versioned)
+
+ if update_to_latest_action:
+ menu.addAction(update_to_latest_action)
+
+ if change_to_hero:
+ menu.addAction(change_to_hero)
+
+ menu.addAction(set_version_action)
+ menu.addAction(switch_folder_action)
+
+ menu.addSeparator()
+
+ menu.addAction(remove_action)
+
+ self._handle_sync_server(menu, repre_ids)
+
+ def _handle_sync_server(self, menu, repre_ids):
+ """Adds actions for download/upload when SyncServer is enabled
+
+ Args:
+ menu (OptionMenu)
+ repre_ids (list) of object_ids
+
+ Returns:
+ (OptionMenu)
+ """
+
+ if not self._controller.is_sync_server_enabled():
+ return
+
+ menu.addSeparator()
+
+ download_icon = qtawesome.icon("fa.download", color=DEFAULT_COLOR)
+ download_active_action = QtWidgets.QAction(
+ download_icon,
+ "Download",
+ menu
+ )
+ download_active_action.triggered.connect(
+ lambda: self._add_sites(repre_ids, "active_site"))
+
+ upload_icon = qtawesome.icon("fa.upload", color=DEFAULT_COLOR)
+ upload_remote_action = QtWidgets.QAction(
+ upload_icon,
+ "Upload",
+ menu
+ )
+ upload_remote_action.triggered.connect(
+ lambda: self._add_sites(repre_ids, "remote_site"))
+
+ menu.addAction(download_active_action)
+ menu.addAction(upload_remote_action)
+
+ def _add_sites(self, repre_ids, site_type):
+ """(Re)sync all 'repre_ids' to specific site.
+
+ It checks if opposite site has fully available content to limit
+ accidents. (ReSync active when no remote >> losing active content)
+
+ Args:
+ repre_ids (list)
+ site_type (Literal[active_site, remote_site]): Site type.
+ """
+
+ self._controller.resync_representations(repre_ids, site_type)
+
+ self.data_changed.emit()
+
+ def _build_item_menu(self, items=None):
+ """Create menu for the selected items"""
+
+ if not items:
+ items = []
+
+ menu = QtWidgets.QMenu(self)
+
+ # add the actions
+ self._build_item_menu_for_selection(items, menu)
+
+ # These two actions should be able to work without selection
+ # expand all items
+ expandall_action = QtWidgets.QAction(menu, text="Expand all items")
+ expandall_action.triggered.connect(self.expandAll)
+
+ # collapse all items
+ collapse_action = QtWidgets.QAction(menu, text="Collapse all items")
+ collapse_action.triggered.connect(self.collapseAll)
+
+ menu.addAction(expandall_action)
+ menu.addAction(collapse_action)
+
+ custom_actions = self._get_custom_actions(containers=items)
+ if custom_actions:
+ submenu = QtWidgets.QMenu("Actions", self)
+ for action in custom_actions:
+ color = action.color or DEFAULT_COLOR
+ icon = qtawesome.icon("fa.%s" % action.icon, color=color)
+ action_item = QtWidgets.QAction(icon, action.label, submenu)
+ action_item.triggered.connect(
+ partial(self._process_custom_action, action, items))
+
+ submenu.addAction(action_item)
+
+ menu.addMenu(submenu)
+
+ # go back to flat view
+ back_to_flat_action = None
+ if self._hierarchy_view:
+ back_to_flat_icon = qtawesome.icon("fa.list", color=DEFAULT_COLOR)
+ back_to_flat_action = QtWidgets.QAction(
+ back_to_flat_icon,
+ "Back to Full-View",
+ menu
+ )
+ back_to_flat_action.triggered.connect(self._leave_hierarchy)
+
+ # send items to hierarchy view
+ enter_hierarchy_icon = qtawesome.icon("fa.indent", color="#d8d8d8")
+ enter_hierarchy_action = QtWidgets.QAction(
+ enter_hierarchy_icon,
+ "Cherry-Pick (Hierarchy)",
+ menu
+ )
+ enter_hierarchy_action.triggered.connect(
+ lambda: self._enter_hierarchy(items))
+
+ if items:
+ menu.addAction(enter_hierarchy_action)
+
+ if back_to_flat_action is not None:
+ menu.addAction(back_to_flat_action)
+
+ return menu
+
+ def _get_custom_actions(self, containers):
+ """Get the registered Inventory Actions
+
+ Args:
+ containers(list): collection of containers
+
+ Returns:
+ list: collection of filter and initialized actions
+ """
+
+ def sorter(Plugin):
+ """Sort based on order attribute of the plugin"""
+ return Plugin.order
+
+ # Fedd an empty dict if no selection, this will ensure the compat
+ # lookup always work, so plugin can interact with Scene Inventory
+ # reversely.
+ containers = containers or [dict()]
+
+ # Check which action will be available in the menu
+ Plugins = discover_inventory_actions()
+ compatible = [p() for p in Plugins if
+ any(p.is_compatible(c) for c in containers)]
+
+ return sorted(compatible, key=sorter)
+
+ def _process_custom_action(self, action, containers):
+ """Run action and if results are returned positive update the view
+
+ If the result is list or dict, will select view items by the result.
+
+ Args:
+ action (InventoryAction): Inventory Action instance
+ containers (list): Data of currently selected items
+
+ Returns:
+ None
+ """
+
+ result = action.process(containers)
+ if result:
+ self.data_changed.emit()
+
+ if isinstance(result, (list, set)):
+ self._select_items_by_action(result)
+
+ if isinstance(result, dict):
+ self._select_items_by_action(
+ result["objectNames"], result["options"]
+ )
+
+ def _select_items_by_action(self, object_names, options=None):
+ """Select view items by the result of action
+
+ Args:
+ object_names (list or set): A list/set of container object name
+ options (dict): GUI operation options.
+
+ Returns:
+ None
+
+ """
+ options = options or dict()
+
+ if options.get("clear", True):
+ self.clearSelection()
+
+ object_names = set(object_names)
+ if (
+ self._hierarchy_view
+ and not self._selected.issuperset(object_names)
+ ):
+ # If any container not in current cherry-picked view, update
+ # view before selecting them.
+ self._selected.update(object_names)
+ self.data_changed.emit()
+
+ model = self.model()
+ selection_model = self.selectionModel()
+
+ select_mode = {
+ "select": QtCore.QItemSelectionModel.Select,
+ "deselect": QtCore.QItemSelectionModel.Deselect,
+ "toggle": QtCore.QItemSelectionModel.Toggle,
+ }[options.get("mode", "select")]
+
+ for index in iter_model_rows(model, 0):
+ item = index.data(InventoryModel.ItemRole)
+ if item.get("isGroupNode"):
+ continue
+
+ name = item.get("objectName")
+ if name in object_names:
+ self.scrollTo(index) # Ensure item is visible
+ flags = select_mode | QtCore.QItemSelectionModel.Rows
+ selection_model.select(index, flags)
+
+ object_names.remove(name)
+
+ if len(object_names) == 0:
+ break
+
+ def _show_right_mouse_menu(self, pos):
+ """Display the menu when at the position of the item clicked"""
+
+ globalpos = self.viewport().mapToGlobal(pos)
+
+ if not self.selectionModel().hasSelection():
+ print("No selection")
+ # Build menu without selection, feed an empty list
+ menu = self._build_item_menu()
+ menu.exec_(globalpos)
+ return
+
+ active = self.currentIndex() # index under mouse
+ active = active.sibling(active.row(), 0) # get first column
+
+ # move index under mouse
+ indices = self.get_indices()
+ if active in indices:
+ indices.remove(active)
+
+ indices.append(active)
+
+ # Extend to the sub-items
+ all_indices = self._extend_to_children(indices)
+ items = [dict(i.data(InventoryModel.ItemRole)) for i in all_indices
+ if i.parent().isValid()]
+
+ if self._hierarchy_view:
+ # Ensure no group item
+ items = [n for n in items if not n.get("isGroupNode")]
+
+ menu = self._build_item_menu(items)
+ menu.exec_(globalpos)
+
+ def get_indices(self):
+ """Get the selected rows"""
+ selection_model = self.selectionModel()
+ return selection_model.selectedRows()
+
+ def _extend_to_children(self, indices):
+ """Extend the indices to the children indices.
+
+ Top-level indices are extended to its children indices. Sub-items
+ are kept as is.
+
+ Args:
+ indices (list): The indices to extend.
+
+ Returns:
+ list: The children indices
+
+ """
+ def get_children(i):
+ model = i.model()
+ rows = model.rowCount(parent=i)
+ for row in range(rows):
+ child = model.index(row, 0, parent=i)
+ yield child
+
+ subitems = set()
+ for i in indices:
+ valid_parent = i.parent().isValid()
+ if valid_parent and i not in subitems:
+ subitems.add(i)
+
+ if self._hierarchy_view:
+ # Assume this is a group item
+ for child in get_children(i):
+ subitems.add(child)
+ else:
+ # is top level item
+ for child in get_children(i):
+ subitems.add(child)
+
+ return list(subitems)
+
+ def _show_version_dialog(self, items):
+ """Create a dialog with the available versions for the selected file
+
+ Args:
+ items (list): list of items to run the "set_version" for
+
+ Returns:
+ None
+ """
+
+ active = items[-1]
+
+ project_name = self._controller.get_current_project_name()
+ # Get available versions for active representation
+ repre_doc = get_representation_by_id(
+ project_name,
+ active["representation"],
+ fields=["parent"]
+ )
+
+ repre_version_doc = get_version_by_id(
+ project_name,
+ repre_doc["parent"],
+ fields=["parent"]
+ )
+
+ version_docs = list(get_versions(
+ project_name,
+ subset_ids=[repre_version_doc["parent"]],
+ hero=True
+ ))
+ hero_version = None
+ standard_versions = []
+ for version_doc in version_docs:
+ if version_doc["type"] == "hero_version":
+ hero_version = version_doc
+ else:
+ standard_versions.append(version_doc)
+ versions = list(reversed(
+ sorted(standard_versions, key=lambda item: item["name"])
+ ))
+ if hero_version:
+ _version_id = hero_version["version_id"]
+ for _version in versions:
+ if _version["_id"] != _version_id:
+ continue
+
+ hero_version["name"] = HeroVersionType(
+ _version["name"]
+ )
+ hero_version["data"] = _version["data"]
+ break
+
+ # Get index among the listed versions
+ current_item = None
+ current_version = active["version"]
+ if isinstance(current_version, HeroVersionType):
+ current_item = hero_version
+ else:
+ for version in versions:
+ if version["name"] == current_version:
+ current_item = version
+ break
+
+ all_versions = []
+ if hero_version:
+ all_versions.append(hero_version)
+ all_versions.extend(versions)
+
+ if current_item:
+ index = all_versions.index(current_item)
+ else:
+ index = 0
+
+ versions_by_label = dict()
+ labels = []
+ for version in all_versions:
+ is_hero = version["type"] == "hero_version"
+ label = format_version(version["name"], is_hero)
+ labels.append(label)
+ versions_by_label[label] = version["name"]
+
+ label, state = QtWidgets.QInputDialog.getItem(
+ self,
+ "Set version..",
+ "Set version number to",
+ labels,
+ current=index,
+ editable=False
+ )
+ if not state:
+ return
+
+ if label:
+ version = versions_by_label[label]
+ self._update_containers(items, version)
+
+ def _show_switch_dialog(self, items):
+ """Display Switch dialog"""
+ dialog = SwitchAssetDialog(self._controller, self, items)
+ dialog.switched.connect(self.data_changed.emit)
+ dialog.show()
+
+ def _show_remove_warning_dialog(self, items):
+ """Prompt a dialog to inform the user the action will remove items"""
+
+ accept = QtWidgets.QMessageBox.Ok
+ buttons = accept | QtWidgets.QMessageBox.Cancel
+
+ state = QtWidgets.QMessageBox.question(
+ self,
+ "Are you sure?",
+ "Are you sure you want to remove {} item(s)".format(len(items)),
+ buttons=buttons,
+ defaultButton=accept
+ )
+
+ if state != accept:
+ return
+
+ for item in items:
+ remove_container(item)
+ self.data_changed.emit()
+
+ def _show_version_error_dialog(self, version, items):
+ """Shows QMessageBox when version switch doesn't work
+
+ Args:
+ version: str or int or None
+ """
+ if version == -1:
+ version_str = "latest"
+ elif isinstance(version, HeroVersionType):
+ version_str = "hero"
+ elif isinstance(version, int):
+ version_str = "v{:03d}".format(version)
+ else:
+ version_str = version
+
+ dialog = QtWidgets.QMessageBox(self)
+ dialog.setIcon(QtWidgets.QMessageBox.Warning)
+ dialog.setStyleSheet(style.load_stylesheet())
+ dialog.setWindowTitle("Update failed")
+
+ switch_btn = dialog.addButton(
+ "Switch Folder",
+ QtWidgets.QMessageBox.ActionRole
+ )
+ switch_btn.clicked.connect(lambda: self._show_switch_dialog(items))
+
+ dialog.addButton(QtWidgets.QMessageBox.Cancel)
+
+ msg = (
+ "Version update to '{}' failed as representation doesn't exist."
+ "\n\nPlease update to version with a valid representation"
+ " OR \n use 'Switch Folder' button to change folder."
+ ).format(version_str)
+ dialog.setText(msg)
+ dialog.exec_()
+
+ def update_all(self):
+ """Update all items that are currently 'outdated' in the view"""
+ # Get the source model through the proxy model
+ model = self.model().sourceModel()
+
+ # Get all items from outdated groups
+ outdated_items = []
+ for index in iter_model_rows(model,
+ column=0,
+ include_root=False):
+ item = index.data(model.ItemRole)
+
+ if not item.get("isGroupNode"):
+ continue
+
+ # Only the group nodes contain the "highest_version" data and as
+ # such we find only the groups and take its children.
+ if not model.outdated(item):
+ continue
+
+ # Collect all children which we want to update
+ children = item.children()
+ outdated_items.extend(children)
+
+ if not outdated_items:
+ log.info("Nothing to update.")
+ return
+
+ # Trigger update to latest
+ self._update_containers(outdated_items, version=-1)
+
+ def _update_containers(self, items, version):
+ """Helper to update items to given version (or version per item)
+
+ If at least one item is specified this will always try to refresh
+ the inventory even if errors occurred on any of the items.
+
+ Arguments:
+ items (list): Items to update
+ version (int or list): Version to set to.
+ This can be a list specifying a version for each item.
+ Like `update_container` version -1 sets the latest version
+ and HeroTypeVersion instances set the hero version.
+
+ """
+
+ if isinstance(version, (list, tuple)):
+ # We allow a unique version to be specified per item. In that case
+ # the length must match with the items
+ assert len(items) == len(version), (
+ "Number of items mismatches number of versions: "
+ "{} items - {} versions".format(len(items), len(version))
+ )
+ versions = version
+ else:
+ # Repeat the same version infinitely
+ versions = itertools.repeat(version)
+
+ # Trigger update to latest
+ try:
+ for item, item_version in zip(items, versions):
+ try:
+ update_container(item, item_version)
+ except AssertionError:
+ self._show_version_error_dialog(item_version, [item])
+ log.warning("Update failed", exc_info=True)
+ finally:
+ # Always update the scene inventory view, even if errors occurred
+ self.data_changed.emit()
diff --git a/openpype/tools/ayon_sceneinventory/window.py b/openpype/tools/ayon_sceneinventory/window.py
new file mode 100644
index 0000000000..427bf4c50d
--- /dev/null
+++ b/openpype/tools/ayon_sceneinventory/window.py
@@ -0,0 +1,200 @@
+from qtpy import QtWidgets, QtCore, QtGui
+import qtawesome
+
+from openpype import style, resources
+from openpype.tools.utils.delegates import VersionDelegate
+from openpype.tools.utils.lib import (
+ preserve_expanded_rows,
+ preserve_selection,
+)
+from openpype.tools.ayon_sceneinventory import SceneInventoryController
+
+from .model import (
+ InventoryModel,
+ FilterProxyModel
+)
+from .view import SceneInventoryView
+
+
+class ControllerVersionDelegate(VersionDelegate):
+ """Version delegate that uses controller to get project.
+
+ Original VersionDelegate is using 'AvalonMongoDB' object instead. Don't
+ worry about the variable name, object is stored to '_dbcon' attribute.
+ """
+
+ def get_project_name(self):
+ self._dbcon.get_current_project_name()
+
+
+class SceneInventoryWindow(QtWidgets.QDialog):
+ """Scene Inventory window"""
+
+ def __init__(self, controller=None, parent=None):
+ super(SceneInventoryWindow, self).__init__(parent)
+
+ if controller is None:
+ controller = SceneInventoryController()
+
+ project_name = controller.get_current_project_name()
+ icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
+ self.setWindowIcon(icon)
+ self.setWindowTitle("Scene Inventory - {}".format(project_name))
+ self.setObjectName("SceneInventory")
+
+ self.resize(1100, 480)
+
+ # region control
+
+ filter_label = QtWidgets.QLabel("Search", self)
+ text_filter = QtWidgets.QLineEdit(self)
+
+ outdated_only_checkbox = QtWidgets.QCheckBox(
+ "Filter to outdated", self
+ )
+ outdated_only_checkbox.setToolTip("Show outdated files only")
+ outdated_only_checkbox.setChecked(False)
+
+ icon = qtawesome.icon("fa.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)
+
+ icon = qtawesome.icon("fa.refresh", color="white")
+ refresh_button = QtWidgets.QPushButton(self)
+ refresh_button.setToolTip("Refresh")
+ refresh_button.setIcon(icon)
+
+ control_layout = QtWidgets.QHBoxLayout()
+ control_layout.addWidget(filter_label)
+ control_layout.addWidget(text_filter)
+ control_layout.addWidget(outdated_only_checkbox)
+ control_layout.addWidget(update_all_button)
+ control_layout.addWidget(refresh_button)
+
+ model = InventoryModel(controller)
+ proxy = FilterProxyModel()
+ proxy.setSourceModel(model)
+ proxy.setDynamicSortFilter(True)
+ proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
+
+ view = SceneInventoryView(controller, self)
+ view.setModel(proxy)
+
+ sync_enabled = controller.is_sync_server_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) # family
+ view.setColumnWidth(4, 120) # group
+ view.setColumnWidth(5, 150) # loader
+
+ # apply delegates
+ version_delegate = ControllerVersionDelegate(controller, self)
+ column = model.Columns.index("version")
+ view.setItemDelegateForColumn(column, version_delegate)
+
+ layout = QtWidgets.QVBoxLayout(self)
+ layout.addLayout(control_layout)
+ layout.addWidget(view)
+
+ show_timer = QtCore.QTimer()
+ show_timer.setInterval(0)
+ show_timer.setSingleShot(False)
+
+ # signals
+ show_timer.timeout.connect(self._on_show_timer)
+ text_filter.textChanged.connect(self._on_text_filter_change)
+ outdated_only_checkbox.stateChanged.connect(
+ self._on_outdated_state_change
+ )
+ view.hierarchy_view_changed.connect(
+ self._on_hierarchy_view_change
+ )
+ view.data_changed.connect(self._on_refresh_request)
+ refresh_button.clicked.connect(self._on_refresh_request)
+ update_all_button.clicked.connect(self._on_update_all)
+
+ self._show_timer = show_timer
+ self._show_counter = 0
+ self._controller = controller
+ 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)
+ if self._first_show:
+ self._first_show = False
+ self.setStyleSheet(style.load_stylesheet())
+
+ self._show_counter = 0
+ self._show_timer.start()
+
+ def keyPressEvent(self, event):
+ """Custom keyPressEvent.
+
+ Override keyPressEvent to do nothing so that Maya's panels won't
+ take focus when pressing "SHIFT" whilst mouse is over viewport or
+ outliner. This way users don't accidentally perform Maya commands
+ whilst trying to name an instance.
+
+ """
+
+ def _on_refresh_request(self):
+ """Signal callback to trigger 'refresh' without any arguments."""
+
+ self.refresh()
+
+ def refresh(self, containers=None):
+ self._first_refresh = False
+ 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)
+
+ def _on_show_timer(self):
+ if self._show_counter < 3:
+ self._show_counter += 1
+ return
+ self._show_timer.stop()
+ self.refresh()
+
+ def _on_hierarchy_view_change(self, enabled):
+ self._proxy.set_hierarchy_view(enabled)
+ self._model.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)
+
+ def _on_outdated_state_change(self):
+ self._proxy.set_filter_outdated(
+ self._outdated_only_checkbox.isChecked()
+ )
+
+ def _on_update_all(self):
+ self._view.update_all()
diff --git a/openpype/tools/utils/delegates.py b/openpype/tools/utils/delegates.py
index c71c87f9b0..c51323e556 100644
--- a/openpype/tools/utils/delegates.py
+++ b/openpype/tools/utils/delegates.py
@@ -24,9 +24,12 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
lock = False
def __init__(self, dbcon, *args, **kwargs):
- self.dbcon = dbcon
+ self._dbcon = dbcon
super(VersionDelegate, self).__init__(*args, **kwargs)
+ def get_project_name(self):
+ return self._dbcon.active_project()
+
def displayText(self, value, locale):
if isinstance(value, HeroVersionType):
return lib.format_version(value, True)
@@ -120,7 +123,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
"Version is not integer"
)
- project_name = self.dbcon.active_project()
+ project_name = self.get_project_name()
# Add all available versions to the editor
parent_id = item["version_document"]["parent"]
version_docs = [
diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py
index ca23945339..29c8c0ba8e 100644
--- a/openpype/tools/utils/host_tools.py
+++ b/openpype/tools/utils/host_tools.py
@@ -171,14 +171,23 @@ class HostToolsHelper:
def get_scene_inventory_tool(self, parent):
"""Create, cache and return scene inventory tool window."""
if self._scene_inventory_tool is None:
- from openpype.tools.sceneinventory import SceneInventoryWindow
-
host = registered_host()
ILoadHost.validate_load_methods(host)
- scene_inventory_window = SceneInventoryWindow(
- parent=parent or self._parent
- )
+ if AYON_SERVER_ENABLED:
+ from openpype.tools.ayon_sceneinventory.window import (
+ SceneInventoryWindow)
+
+ scene_inventory_window = SceneInventoryWindow(
+ parent=parent or self._parent
+ )
+
+ else:
+ from openpype.tools.sceneinventory import SceneInventoryWindow
+
+ scene_inventory_window = SceneInventoryWindow(
+ parent=parent or self._parent
+ )
self._scene_inventory_tool = scene_inventory_window
return self._scene_inventory_tool
diff --git a/server_addon/nuke/server/settings/publish_plugins.py b/server_addon/nuke/server/settings/publish_plugins.py
index 19206149b6..692b2bd240 100644
--- a/server_addon/nuke/server/settings/publish_plugins.py
+++ b/server_addon/nuke/server/settings/publish_plugins.py
@@ -236,7 +236,7 @@ class PublishPuginsModel(BaseSettingsModel):
default_factory=CollectInstanceDataModel,
section="Collectors"
)
- ValidateCorrectAssetName: OptionalPluginModel = Field(
+ ValidateCorrectAssetContext: OptionalPluginModel = Field(
title="Validate Correct Folder Name",
default_factory=OptionalPluginModel,
section="Validators"
@@ -308,7 +308,7 @@ DEFAULT_PUBLISH_PLUGIN_SETTINGS = {
"write"
]
},
- "ValidateCorrectAssetName": {
+ "ValidateCorrectAssetContext": {
"enabled": True,
"optional": True,
"active": True