mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into enhancement/OP-7069_Validate-Containers
This commit is contained in:
commit
d1dced43e4
24 changed files with 3899 additions and 203 deletions
|
|
@ -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",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<root>
|
||||
<error id="main">
|
||||
<title>Shot/Asset name</title>
|
||||
<description>
|
||||
## 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.
|
||||
</description>
|
||||
</error>
|
||||
</root>
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<root>
|
||||
<error id="main">
|
||||
<title>Shot/Asset name</title>
|
||||
<description>
|
||||
## 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.
|
||||
</description>
|
||||
</error>
|
||||
</root>
|
||||
112
openpype/hosts/nuke/plugins/publish/validate_asset_context.py
Normal file
112
openpype/hosts/nuke/plugins/publish/validate_asset_context.py
Normal file
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -341,7 +341,7 @@
|
|||
"write"
|
||||
]
|
||||
},
|
||||
"ValidateCorrectAssetName": {
|
||||
"ValidateCorrectAssetContext": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
"active": true
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@
|
|||
"name": "template_publish_plugin",
|
||||
"template_data": [
|
||||
{
|
||||
"key": "ValidateCorrectAssetName",
|
||||
"key": "ValidateCorrectAssetContext",
|
||||
"label": "Validate Correct Asset Name"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
6
openpype/tools/ayon_sceneinventory/__init__.py
Normal file
6
openpype/tools/ayon_sceneinventory/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from .control import SceneInventoryController
|
||||
|
||||
|
||||
__all__ = (
|
||||
"SceneInventoryController",
|
||||
)
|
||||
134
openpype/tools/ayon_sceneinventory/control.py
Normal file
134
openpype/tools/ayon_sceneinventory/control.py
Normal file
|
|
@ -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()
|
||||
622
openpype/tools/ayon_sceneinventory/model.py
Normal file
622
openpype/tools/ayon_sceneinventory/model.py
Normal file
|
|
@ -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", "<none>")
|
||||
|
||||
return super(InventoryModel, self).data(index, role)
|
||||
|
||||
def set_hierarchy_view(self, state):
|
||||
"""Set whether to display subsets in hierarchy view."""
|
||||
state = bool(state)
|
||||
|
||||
if state != self._hierarchy_view:
|
||||
self._hierarchy_view = state
|
||||
|
||||
def refresh(self, selected=None, 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
|
||||
6
openpype/tools/ayon_sceneinventory/models/__init__.py
Normal file
6
openpype/tools/ayon_sceneinventory/models/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from .site_sync import SiteSyncModel
|
||||
|
||||
|
||||
__all__ = (
|
||||
"SiteSyncModel",
|
||||
)
|
||||
176
openpype/tools/ayon_sceneinventory/models/site_sync.py
Normal file
176
openpype/tools/ayon_sceneinventory/models/site_sync.py
Normal file
|
|
@ -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
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from .dialog import SwitchAssetDialog
|
||||
|
||||
|
||||
__all__ = (
|
||||
"SwitchAssetDialog",
|
||||
)
|
||||
1333
openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py
Normal file
1333
openpype/tools/ayon_sceneinventory/switch_dialog/dialog.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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 "<folder>")
|
||||
|
||||
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)
|
||||
94
openpype/tools/ayon_sceneinventory/switch_dialog/widgets.py
Normal file
94
openpype/tools/ayon_sceneinventory/switch_dialog/widgets.py
Normal file
|
|
@ -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)
|
||||
825
openpype/tools/ayon_sceneinventory/view.py
Normal file
825
openpype/tools/ayon_sceneinventory/view.py
Normal file
|
|
@ -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()
|
||||
200
openpype/tools/ayon_sceneinventory/window.py
Normal file
200
openpype/tools/ayon_sceneinventory/window.py
Normal file
|
|
@ -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()
|
||||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue