mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
[Automated] Merged develop into main
This commit is contained in:
commit
a3f300aeb3
43 changed files with 6920 additions and 116 deletions
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -35,6 +35,7 @@ body:
|
|||
label: Version
|
||||
description: What version are you running? Look to OpenPype Tray
|
||||
options:
|
||||
- 3.17.2-nightly.4
|
||||
- 3.17.2-nightly.3
|
||||
- 3.17.2-nightly.2
|
||||
- 3.17.2-nightly.1
|
||||
|
|
@ -134,7 +135,6 @@ body:
|
|||
- 3.14.11-nightly.1
|
||||
- 3.14.10
|
||||
- 3.14.10-nightly.9
|
||||
- 3.14.10-nightly.8
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
|||
|
|
@ -75,9 +75,9 @@ def _get_subsets(
|
|||
):
|
||||
fields.add(key)
|
||||
|
||||
active = None
|
||||
active = True
|
||||
if archived:
|
||||
active = False
|
||||
active = None
|
||||
|
||||
for subset in con.get_products(
|
||||
project_name,
|
||||
|
|
@ -196,7 +196,7 @@ def get_assets(
|
|||
|
||||
active = True
|
||||
if archived:
|
||||
active = False
|
||||
active = None
|
||||
|
||||
con = get_server_api_connection()
|
||||
fields = folder_fields_v3_to_v4(fields, con)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from openpype.hosts.nuke import api as napi
|
|||
from openpype.hosts.nuke.api.lib import set_node_knobs_from_settings
|
||||
|
||||
|
||||
# Python 2/3 compatibility
|
||||
if sys.version_info[0] >= 3:
|
||||
unicode = str
|
||||
|
||||
|
|
@ -45,11 +46,12 @@ class ExtractThumbnail(publish.Extractor):
|
|||
for o_name, o_data in instance.data["bakePresets"].items():
|
||||
self.render_thumbnail(instance, o_name, **o_data)
|
||||
else:
|
||||
viewer_process_swithes = {
|
||||
viewer_process_switches = {
|
||||
"bake_viewer_process": True,
|
||||
"bake_viewer_input_process": True
|
||||
}
|
||||
self.render_thumbnail(instance, None, **viewer_process_swithes)
|
||||
self.render_thumbnail(
|
||||
instance, None, **viewer_process_switches)
|
||||
|
||||
def render_thumbnail(self, instance, output_name=None, **kwargs):
|
||||
first_frame = instance.data["frameStartHandle"]
|
||||
|
|
@ -61,8 +63,6 @@ class ExtractThumbnail(publish.Extractor):
|
|||
|
||||
# solve output name if any is set
|
||||
output_name = output_name or ""
|
||||
if output_name:
|
||||
output_name = "_" + output_name
|
||||
|
||||
bake_viewer_process = kwargs["bake_viewer_process"]
|
||||
bake_viewer_input_process_node = kwargs[
|
||||
|
|
@ -166,26 +166,42 @@ class ExtractThumbnail(publish.Extractor):
|
|||
previous_node = dag_node
|
||||
temporary_nodes.append(dag_node)
|
||||
|
||||
thumb_name = "thumbnail"
|
||||
# only add output name and
|
||||
# if there are more than one bake preset
|
||||
if (
|
||||
output_name
|
||||
and len(instance.data.get("bakePresets", {}).keys()) > 1
|
||||
):
|
||||
thumb_name = "{}_{}".format(output_name, thumb_name)
|
||||
|
||||
# create write node
|
||||
write_node = nuke.createNode("Write")
|
||||
file = fhead[:-1] + output_name + ".jpg"
|
||||
name = "thumbnail"
|
||||
path = os.path.join(staging_dir, file).replace("\\", "/")
|
||||
instance.data["thumbnail"] = path
|
||||
write_node["file"].setValue(path)
|
||||
file = fhead[:-1] + thumb_name + ".jpg"
|
||||
thumb_path = os.path.join(staging_dir, file).replace("\\", "/")
|
||||
|
||||
# add thumbnail to cleanup
|
||||
instance.context.data["cleanupFullPaths"].append(thumb_path)
|
||||
|
||||
# make sure only one thumbnail path is set
|
||||
# and it is existing file
|
||||
instance_thumb_path = instance.data.get("thumbnailPath")
|
||||
if not instance_thumb_path or not os.path.isfile(instance_thumb_path):
|
||||
instance.data["thumbnailPath"] = thumb_path
|
||||
|
||||
write_node["file"].setValue(thumb_path)
|
||||
write_node["file_type"].setValue("jpg")
|
||||
write_node["raw"].setValue(1)
|
||||
write_node.setInput(0, previous_node)
|
||||
temporary_nodes.append(write_node)
|
||||
tags = ["thumbnail", "publish_on_farm"]
|
||||
|
||||
repre = {
|
||||
'name': name,
|
||||
'name': thumb_name,
|
||||
'ext': "jpg",
|
||||
"outputName": "thumb",
|
||||
"outputName": thumb_name,
|
||||
'files': file,
|
||||
"stagingDir": staging_dir,
|
||||
"tags": tags
|
||||
"tags": ["thumbnail", "publish_on_farm", "delete"]
|
||||
}
|
||||
instance.data["representations"].append(repre)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import os
|
||||
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from openpype.modules import OpenPypeModule, ITrayModule
|
||||
|
||||
|
||||
|
|
@ -75,20 +76,11 @@ class AvalonModule(OpenPypeModule, ITrayModule):
|
|||
|
||||
def show_library_loader(self):
|
||||
if self._library_loader_window is None:
|
||||
from qtpy import QtCore
|
||||
from openpype.tools.libraryloader import LibraryLoaderWindow
|
||||
from openpype.pipeline import install_openpype_plugins
|
||||
|
||||
libraryloader = LibraryLoaderWindow(
|
||||
show_projects=True,
|
||||
show_libraries=True
|
||||
)
|
||||
# Remove always on top flag for tray
|
||||
window_flags = libraryloader.windowFlags()
|
||||
if window_flags | QtCore.Qt.WindowStaysOnTopHint:
|
||||
window_flags ^= QtCore.Qt.WindowStaysOnTopHint
|
||||
libraryloader.setWindowFlags(window_flags)
|
||||
self._library_loader_window = libraryloader
|
||||
if AYON_SERVER_ENABLED:
|
||||
self._init_ayon_loader()
|
||||
else:
|
||||
self._init_library_loader()
|
||||
|
||||
install_openpype_plugins()
|
||||
|
||||
|
|
@ -106,3 +98,25 @@ class AvalonModule(OpenPypeModule, ITrayModule):
|
|||
if self.tray_initialized:
|
||||
from .rest_api import AvalonRestApiResource
|
||||
self.rest_api_obj = AvalonRestApiResource(self, server_manager)
|
||||
|
||||
def _init_library_loader(self):
|
||||
from qtpy import QtCore
|
||||
from openpype.tools.libraryloader import LibraryLoaderWindow
|
||||
|
||||
libraryloader = LibraryLoaderWindow(
|
||||
show_projects=True,
|
||||
show_libraries=True
|
||||
)
|
||||
# Remove always on top flag for tray
|
||||
window_flags = libraryloader.windowFlags()
|
||||
if window_flags | QtCore.Qt.WindowStaysOnTopHint:
|
||||
window_flags ^= QtCore.Qt.WindowStaysOnTopHint
|
||||
libraryloader.setWindowFlags(window_flags)
|
||||
self._library_loader_window = libraryloader
|
||||
|
||||
def _init_ayon_loader(self):
|
||||
from openpype.tools.ayon_loader.ui import LoaderWindow
|
||||
|
||||
libraryloader = LoaderWindow()
|
||||
|
||||
self._library_loader_window = libraryloader
|
||||
|
|
|
|||
|
|
@ -14,6 +14,13 @@ class TaskNotSetError(KeyError):
|
|||
super(TaskNotSetError, self).__init__(msg)
|
||||
|
||||
|
||||
class TemplateFillError(Exception):
|
||||
def __init__(self, msg=None):
|
||||
if not msg:
|
||||
msg = "Creator's subset name template is missing key value."
|
||||
super(TemplateFillError, self).__init__(msg)
|
||||
|
||||
|
||||
def get_subset_name_template(
|
||||
project_name,
|
||||
family,
|
||||
|
|
@ -112,6 +119,10 @@ def get_subset_name(
|
|||
for project. Settings are queried if not passed.
|
||||
family_filter (Optional[str]): Use different family for subset template
|
||||
filtering. Value of 'family' is used when not passed.
|
||||
|
||||
Raises:
|
||||
TemplateFillError: If filled template contains placeholder key which is not
|
||||
collected.
|
||||
"""
|
||||
|
||||
if not family:
|
||||
|
|
@ -154,4 +165,10 @@ def get_subset_name(
|
|||
for key, value in dynamic_data.items():
|
||||
fill_pairs[key] = value
|
||||
|
||||
return template.format(**prepare_template_data(fill_pairs))
|
||||
try:
|
||||
return template.format(**prepare_template_data(fill_pairs))
|
||||
except KeyError as exp:
|
||||
raise TemplateFillError(
|
||||
"Value for {} key is missing in template '{}'."
|
||||
" Available values are {}".format(str(exp), template, fill_pairs)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ from .utils import (
|
|||
|
||||
loaders_from_repre_context,
|
||||
loaders_from_representation,
|
||||
filter_repre_contexts_by_loader,
|
||||
|
||||
any_outdated_containers,
|
||||
get_outdated_containers,
|
||||
|
|
@ -85,6 +86,7 @@ __all__ = (
|
|||
|
||||
"loaders_from_repre_context",
|
||||
"loaders_from_representation",
|
||||
"filter_repre_contexts_by_loader",
|
||||
|
||||
"any_outdated_containers",
|
||||
"get_outdated_containers",
|
||||
|
|
|
|||
|
|
@ -790,6 +790,24 @@ def loaders_from_repre_context(loaders, repre_context):
|
|||
]
|
||||
|
||||
|
||||
def filter_repre_contexts_by_loader(repre_contexts, loader):
|
||||
"""Filter representation contexts for loader.
|
||||
|
||||
Args:
|
||||
repre_contexts (list[dict[str, Ant]]): Representation context.
|
||||
loader (LoaderPlugin): Loader plugin to filter contexts for.
|
||||
|
||||
Returns:
|
||||
list[dict[str, Any]]: Filtered representation contexts.
|
||||
"""
|
||||
|
||||
return [
|
||||
repre_context
|
||||
for repre_context in repre_contexts
|
||||
if is_compatible_loader(loader, repre_context)
|
||||
]
|
||||
|
||||
|
||||
def loaders_from_representation(loaders, representation):
|
||||
"""Return all compatible loaders for a representation."""
|
||||
|
||||
|
|
|
|||
|
|
@ -166,8 +166,12 @@ class ServerThumbnailResolver(ThumbnailResolver):
|
|||
|
||||
# This is new way how thumbnails can be received from server
|
||||
# - output is 'ThumbnailContent' object
|
||||
if hasattr(ayon_api, "get_thumbnail_by_id"):
|
||||
result = ayon_api.get_thumbnail_by_id(thumbnail_id)
|
||||
# NOTE Use 'get_server_api_connection' because public function
|
||||
# 'get_thumbnail_by_id' does not return output of 'ServerAPI'
|
||||
# method.
|
||||
con = ayon_api.get_server_api_connection()
|
||||
if hasattr(con, "get_thumbnail_by_id"):
|
||||
result = con.get_thumbnail_by_id(thumbnail_id)
|
||||
if result.is_valid:
|
||||
filepath = cache.store_thumbnail(
|
||||
project_name,
|
||||
|
|
@ -178,7 +182,7 @@ class ServerThumbnailResolver(ThumbnailResolver):
|
|||
else:
|
||||
# Backwards compatibility for ayon api where 'get_thumbnail_by_id'
|
||||
# is not implemented and output is filepath
|
||||
filepath = ayon_api.get_thumbnail(
|
||||
filepath = con.get_thumbnail(
|
||||
project_name, entity_type, entity_id, thumbnail_id
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -29,13 +29,12 @@ class PreIntegrateThumbnails(pyblish.api.InstancePlugin):
|
|||
if not repres:
|
||||
return
|
||||
|
||||
thumbnail_repre = None
|
||||
thumbnail_repres = []
|
||||
for repre in repres:
|
||||
if repre["name"] == "thumbnail":
|
||||
thumbnail_repre = repre
|
||||
break
|
||||
if "thumbnail" in repre.get("tags", []):
|
||||
thumbnail_repres.append(repre)
|
||||
|
||||
if not thumbnail_repre:
|
||||
if not thumbnail_repres:
|
||||
return
|
||||
|
||||
family = instance.data["family"]
|
||||
|
|
@ -60,14 +59,15 @@ class PreIntegrateThumbnails(pyblish.api.InstancePlugin):
|
|||
if not found_profile:
|
||||
return
|
||||
|
||||
thumbnail_repre.setdefault("tags", [])
|
||||
for thumbnail_repre in thumbnail_repres:
|
||||
thumbnail_repre.setdefault("tags", [])
|
||||
|
||||
if not found_profile["integrate_thumbnail"]:
|
||||
if "delete" not in thumbnail_repre["tags"]:
|
||||
thumbnail_repre["tags"].append("delete")
|
||||
else:
|
||||
if "delete" in thumbnail_repre["tags"]:
|
||||
thumbnail_repre["tags"].remove("delete")
|
||||
if not found_profile["integrate_thumbnail"]:
|
||||
if "delete" not in thumbnail_repre["tags"]:
|
||||
thumbnail_repre["tags"].append("delete")
|
||||
else:
|
||||
if "delete" in thumbnail_repre["tags"]:
|
||||
thumbnail_repre["tags"].remove("delete")
|
||||
|
||||
self.log.debug(
|
||||
"Thumbnail repre tags {}".format(thumbnail_repre["tags"]))
|
||||
self.log.debug(
|
||||
"Thumbnail repre tags {}".format(thumbnail_repre["tags"]))
|
||||
|
|
|
|||
|
|
@ -1164,19 +1164,19 @@ def _convert_global_project_settings(ayon_settings, output, default_settings):
|
|||
for profile in extract_oiio_transcode_profiles:
|
||||
new_outputs = {}
|
||||
name_counter = {}
|
||||
for output in profile["outputs"]:
|
||||
if "name" in output:
|
||||
name = output.pop("name")
|
||||
for profile_output in profile["outputs"]:
|
||||
if "name" in profile_output:
|
||||
name = profile_output.pop("name")
|
||||
else:
|
||||
# Backwards compatibility for setting without 'name' in model
|
||||
name = output["extension"]
|
||||
name = profile_output["extension"]
|
||||
if name in new_outputs:
|
||||
name_counter[name] += 1
|
||||
name = "{}_{}".format(name, name_counter[name])
|
||||
else:
|
||||
name_counter[name] = 0
|
||||
|
||||
new_outputs[name] = output
|
||||
new_outputs[name] = profile_output
|
||||
profile["outputs"] = new_outputs
|
||||
|
||||
# Extract Burnin plugin
|
||||
|
|
|
|||
6
openpype/tools/ayon_loader/__init__.py
Normal file
6
openpype/tools/ayon_loader/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from .control import LoaderController
|
||||
|
||||
|
||||
__all__ = (
|
||||
"LoaderController",
|
||||
)
|
||||
851
openpype/tools/ayon_loader/abstract.py
Normal file
851
openpype/tools/ayon_loader/abstract.py
Normal file
|
|
@ -0,0 +1,851 @@
|
|||
from abc import ABCMeta, abstractmethod
|
||||
import six
|
||||
|
||||
from openpype.lib.attribute_definitions import (
|
||||
AbstractAttrDef,
|
||||
serialize_attr_defs,
|
||||
deserialize_attr_defs,
|
||||
)
|
||||
|
||||
|
||||
class ProductTypeItem:
|
||||
"""Item representing product type.
|
||||
|
||||
Args:
|
||||
name (str): Product type name.
|
||||
icon (dict[str, Any]): Product type icon definition.
|
||||
checked (bool): Is product type checked for filtering.
|
||||
"""
|
||||
|
||||
def __init__(self, name, icon, checked):
|
||||
self.name = name
|
||||
self.icon = icon
|
||||
self.checked = checked
|
||||
|
||||
def to_data(self):
|
||||
return {
|
||||
"name": self.name,
|
||||
"icon": self.icon,
|
||||
"checked": self.checked,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data):
|
||||
return cls(**data)
|
||||
|
||||
|
||||
class ProductItem:
|
||||
"""Product item with it versions.
|
||||
|
||||
Args:
|
||||
product_id (str): Product id.
|
||||
product_type (str): Product type.
|
||||
product_name (str): Product name.
|
||||
product_icon (dict[str, Any]): Product icon definition.
|
||||
product_type_icon (dict[str, Any]): Product type icon definition.
|
||||
product_in_scene (bool): Is product in scene (only when used in DCC).
|
||||
group_name (str): Group name.
|
||||
folder_id (str): Folder id.
|
||||
folder_label (str): Folder label.
|
||||
version_items (dict[str, VersionItem]): Version items by id.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
product_id,
|
||||
product_type,
|
||||
product_name,
|
||||
product_icon,
|
||||
product_type_icon,
|
||||
product_in_scene,
|
||||
group_name,
|
||||
folder_id,
|
||||
folder_label,
|
||||
version_items,
|
||||
):
|
||||
self.product_id = product_id
|
||||
self.product_type = product_type
|
||||
self.product_name = product_name
|
||||
self.product_icon = product_icon
|
||||
self.product_type_icon = product_type_icon
|
||||
self.product_in_scene = product_in_scene
|
||||
self.group_name = group_name
|
||||
self.folder_id = folder_id
|
||||
self.folder_label = folder_label
|
||||
self.version_items = version_items
|
||||
|
||||
def to_data(self):
|
||||
return {
|
||||
"product_id": self.product_id,
|
||||
"product_type": self.product_type,
|
||||
"product_name": self.product_name,
|
||||
"product_icon": self.product_icon,
|
||||
"product_type_icon": self.product_type_icon,
|
||||
"product_in_scene": self.product_in_scene,
|
||||
"group_name": self.group_name,
|
||||
"folder_id": self.folder_id,
|
||||
"folder_label": self.folder_label,
|
||||
"version_items": {
|
||||
version_id: version_item.to_data()
|
||||
for version_id, version_item in self.version_items.items()
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data):
|
||||
version_items = {
|
||||
version_id: VersionItem.from_data(version)
|
||||
for version_id, version in data["version_items"].items()
|
||||
}
|
||||
data["version_items"] = version_items
|
||||
return cls(**data)
|
||||
|
||||
|
||||
class VersionItem:
|
||||
"""Version item.
|
||||
|
||||
Object have implemented comparison operators to be sortable.
|
||||
|
||||
Args:
|
||||
version_id (str): Version id.
|
||||
version (int): Version. Can be negative when is hero version.
|
||||
is_hero (bool): Is hero version.
|
||||
product_id (str): Product id.
|
||||
thumbnail_id (Union[str, None]): Thumbnail id.
|
||||
published_time (Union[str, None]): Published time in format
|
||||
'%Y%m%dT%H%M%SZ'.
|
||||
author (Union[str, None]): Author.
|
||||
frame_range (Union[str, None]): Frame range.
|
||||
duration (Union[int, None]): Duration.
|
||||
handles (Union[str, None]): Handles.
|
||||
step (Union[int, None]): Step.
|
||||
comment (Union[str, None]): Comment.
|
||||
source (Union[str, None]): Source.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
version_id,
|
||||
version,
|
||||
is_hero,
|
||||
product_id,
|
||||
thumbnail_id,
|
||||
published_time,
|
||||
author,
|
||||
frame_range,
|
||||
duration,
|
||||
handles,
|
||||
step,
|
||||
comment,
|
||||
source
|
||||
):
|
||||
self.version_id = version_id
|
||||
self.product_id = product_id
|
||||
self.thumbnail_id = thumbnail_id
|
||||
self.version = version
|
||||
self.is_hero = is_hero
|
||||
self.published_time = published_time
|
||||
self.author = author
|
||||
self.frame_range = frame_range
|
||||
self.duration = duration
|
||||
self.handles = handles
|
||||
self.step = step
|
||||
self.comment = comment
|
||||
self.source = source
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, VersionItem):
|
||||
return False
|
||||
return (
|
||||
self.is_hero == other.is_hero
|
||||
and self.version == other.version
|
||||
and self.version_id == other.version_id
|
||||
and self.product_id == other.product_id
|
||||
)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __gt__(self, other):
|
||||
if not isinstance(other, VersionItem):
|
||||
return False
|
||||
if (
|
||||
other.version == self.version
|
||||
and self.is_hero
|
||||
):
|
||||
return True
|
||||
return other.version < self.version
|
||||
|
||||
def to_data(self):
|
||||
return {
|
||||
"version_id": self.version_id,
|
||||
"product_id": self.product_id,
|
||||
"thumbnail_id": self.thumbnail_id,
|
||||
"version": self.version,
|
||||
"is_hero": self.is_hero,
|
||||
"published_time": self.published_time,
|
||||
"author": self.author,
|
||||
"frame_range": self.frame_range,
|
||||
"duration": self.duration,
|
||||
"handles": self.handles,
|
||||
"step": self.step,
|
||||
"comment": self.comment,
|
||||
"source": self.source,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data):
|
||||
return cls(**data)
|
||||
|
||||
|
||||
class RepreItem:
|
||||
"""Representation item.
|
||||
|
||||
Args:
|
||||
representation_id (str): Representation id.
|
||||
representation_name (str): Representation name.
|
||||
representation_icon (dict[str, Any]): Representation icon definition.
|
||||
product_name (str): Product name.
|
||||
folder_label (str): Folder label.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
representation_id,
|
||||
representation_name,
|
||||
representation_icon,
|
||||
product_name,
|
||||
folder_label,
|
||||
):
|
||||
self.representation_id = representation_id
|
||||
self.representation_name = representation_name
|
||||
self.representation_icon = representation_icon
|
||||
self.product_name = product_name
|
||||
self.folder_label = folder_label
|
||||
|
||||
def to_data(self):
|
||||
return {
|
||||
"representation_id": self.representation_id,
|
||||
"representation_name": self.representation_name,
|
||||
"representation_icon": self.representation_icon,
|
||||
"product_name": self.product_name,
|
||||
"folder_label": self.folder_label,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data):
|
||||
return cls(**data)
|
||||
|
||||
|
||||
class ActionItem:
|
||||
"""Action item that can be triggered.
|
||||
|
||||
Action item is defined for a specific context. To trigger the action
|
||||
use 'identifier' and context, it necessary also use 'options'.
|
||||
|
||||
Args:
|
||||
identifier (str): Action identifier.
|
||||
label (str): Action label.
|
||||
icon (dict[str, Any]): Action icon definition.
|
||||
tooltip (str): Action tooltip.
|
||||
options (Union[list[AbstractAttrDef], list[qargparse.QArgument]]):
|
||||
Action options. Note: 'qargparse' is considered as deprecated.
|
||||
order (int): Action order.
|
||||
project_name (str): Project name.
|
||||
folder_ids (list[str]): Folder ids.
|
||||
product_ids (list[str]): Product ids.
|
||||
version_ids (list[str]): Version ids.
|
||||
representation_ids (list[str]): Representation ids.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
identifier,
|
||||
label,
|
||||
icon,
|
||||
tooltip,
|
||||
options,
|
||||
order,
|
||||
project_name,
|
||||
folder_ids,
|
||||
product_ids,
|
||||
version_ids,
|
||||
representation_ids,
|
||||
):
|
||||
self.identifier = identifier
|
||||
self.label = label
|
||||
self.icon = icon
|
||||
self.tooltip = tooltip
|
||||
self.options = options
|
||||
self.order = order
|
||||
self.project_name = project_name
|
||||
self.folder_ids = folder_ids
|
||||
self.product_ids = product_ids
|
||||
self.version_ids = version_ids
|
||||
self.representation_ids = representation_ids
|
||||
|
||||
def _options_to_data(self):
|
||||
options = self.options
|
||||
if not options:
|
||||
return options
|
||||
if isinstance(options[0], AbstractAttrDef):
|
||||
return serialize_attr_defs(options)
|
||||
# NOTE: Data conversion is not used by default in loader tool. But for
|
||||
# future development of detached UI tools it would be better to be
|
||||
# prepared for it.
|
||||
raise NotImplementedError(
|
||||
"{}.to_data is not implemented. Use Attribute definitions"
|
||||
" from 'openpype.lib' instead of 'qargparse'.".format(
|
||||
self.__class__.__name__
|
||||
)
|
||||
)
|
||||
|
||||
def to_data(self):
|
||||
options = self._options_to_data()
|
||||
return {
|
||||
"identifier": self.identifier,
|
||||
"label": self.label,
|
||||
"icon": self.icon,
|
||||
"tooltip": self.tooltip,
|
||||
"options": options,
|
||||
"order": self.order,
|
||||
"project_name": self.project_name,
|
||||
"folder_ids": self.folder_ids,
|
||||
"product_ids": self.product_ids,
|
||||
"version_ids": self.version_ids,
|
||||
"representation_ids": self.representation_ids,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data):
|
||||
options = data["options"]
|
||||
if options:
|
||||
options = deserialize_attr_defs(options)
|
||||
data["options"] = options
|
||||
return cls(**data)
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class _BaseLoaderController(object):
|
||||
"""Base loader controller abstraction.
|
||||
|
||||
Abstract base class that is required for both frontend and backed.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_current_context(self):
|
||||
"""Current context is a context of the current scene.
|
||||
|
||||
Example output:
|
||||
{
|
||||
"project_name": "MyProject",
|
||||
"folder_id": "0011223344-5566778-99",
|
||||
"task_name": "Compositing",
|
||||
}
|
||||
|
||||
Returns:
|
||||
dict[str, Union[str, None]]: Context data.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def reset(self):
|
||||
"""Reset all cached data to reload everything.
|
||||
|
||||
Triggers events "controller.reset.started" and
|
||||
"controller.reset.finished".
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
# Model wrappers
|
||||
@abstractmethod
|
||||
def get_folder_items(self, project_name, sender=None):
|
||||
"""Folder items for a project.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
sender (Optional[str]): Sender who requested the name.
|
||||
|
||||
Returns:
|
||||
list[FolderItem]: Folder items for the project.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
# Expected selection helpers
|
||||
@abstractmethod
|
||||
def get_expected_selection_data(self):
|
||||
"""Full expected selection information.
|
||||
|
||||
Expected selection is a selection that may not be yet selected in UI
|
||||
e.g. because of refreshing, this data tell the UI what should be
|
||||
selected when they finish their refresh.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Expected selection data.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_expected_selection(self, project_name, folder_id):
|
||||
"""Set expected selection.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of project to be selected.
|
||||
folder_id (str): Id of folder to be selected.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BackendLoaderController(_BaseLoaderController):
|
||||
"""Backend loader controller abstraction.
|
||||
|
||||
What backend logic requires from a controller for proper logic.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def emit_event(self, topic, data=None, source=None):
|
||||
"""Emit event with a certain topic, data and source.
|
||||
|
||||
The event should be sent to both frontend and backend.
|
||||
|
||||
Args:
|
||||
topic (str): Event topic name.
|
||||
data (Optional[dict[str, Any]]): Event data.
|
||||
source (Optional[str]): Event source.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_loaded_product_ids(self):
|
||||
"""Return set of loaded product ids.
|
||||
|
||||
Returns:
|
||||
set[str]: Set of loaded product ids.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class FrontendLoaderController(_BaseLoaderController):
|
||||
@abstractmethod
|
||||
def register_event_callback(self, topic, callback):
|
||||
"""Register callback for an event topic.
|
||||
|
||||
Args:
|
||||
topic (str): Event topic name.
|
||||
callback (func): Callback triggered when the event is emitted.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
# Expected selection helpers
|
||||
@abstractmethod
|
||||
def expected_project_selected(self, project_name):
|
||||
"""Expected project was selected in frontend.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def expected_folder_selected(self, folder_id):
|
||||
"""Expected folder was selected in frontend.
|
||||
|
||||
Args:
|
||||
folder_id (str): Folder id.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
# Model wrapper calls
|
||||
@abstractmethod
|
||||
def get_project_items(self, sender=None):
|
||||
"""Items for all projects available on server.
|
||||
|
||||
Triggers event topics "projects.refresh.started" and
|
||||
"projects.refresh.finished" with data:
|
||||
{
|
||||
"sender": sender
|
||||
}
|
||||
|
||||
Notes:
|
||||
Filtering of projects is done in UI.
|
||||
|
||||
Args:
|
||||
sender (Optional[str]): Sender who requested the items.
|
||||
|
||||
Returns:
|
||||
list[ProjectItem]: List of project items.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_product_items(self, project_name, folder_ids, sender=None):
|
||||
"""Product items for folder ids.
|
||||
|
||||
Triggers event topics "products.refresh.started" and
|
||||
"products.refresh.finished" with data:
|
||||
{
|
||||
"project_name": project_name,
|
||||
"folder_ids": folder_ids,
|
||||
"sender": sender
|
||||
}
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
folder_ids (Iterable[str]): Folder ids.
|
||||
sender (Optional[str]): Sender who requested the items.
|
||||
|
||||
Returns:
|
||||
list[ProductItem]: List of product items.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_product_item(self, project_name, product_id):
|
||||
"""Receive single product item.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
product_id (str): Product id.
|
||||
|
||||
Returns:
|
||||
Union[ProductItem, None]: Product info or None if not found.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_product_type_items(self, project_name):
|
||||
"""Product type items for a project.
|
||||
|
||||
Product types have defined if are checked for filtering or not.
|
||||
|
||||
Returns:
|
||||
list[ProductTypeItem]: List of product type items for a project.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_representation_items(
|
||||
self, project_name, version_ids, sender=None
|
||||
):
|
||||
"""Representation items for version ids.
|
||||
|
||||
Triggers event topics "model.representations.refresh.started" and
|
||||
"model.representations.refresh.finished" with data:
|
||||
{
|
||||
"project_name": project_name,
|
||||
"version_ids": version_ids,
|
||||
"sender": sender
|
||||
}
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
version_ids (Iterable[str]): Version ids.
|
||||
sender (Optional[str]): Sender who requested the items.
|
||||
|
||||
Returns:
|
||||
list[RepreItem]: List of representation items.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_version_thumbnail_ids(self, project_name, version_ids):
|
||||
"""Get thumbnail ids for version ids.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
version_ids (Iterable[str]): Version ids.
|
||||
|
||||
Returns:
|
||||
dict[str, Union[str, Any]]: Thumbnail id by version id.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_folder_thumbnail_ids(self, project_name, folder_ids):
|
||||
"""Get thumbnail ids for folder ids.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
folder_ids (Iterable[str]): Folder ids.
|
||||
|
||||
Returns:
|
||||
dict[str, Union[str, Any]]: Thumbnail id by folder id.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_thumbnail_path(self, project_name, thumbnail_id):
|
||||
"""Get thumbnail path for thumbnail id.
|
||||
|
||||
This method should get a path to a thumbnail based on thumbnail id.
|
||||
Which probably means to download the thumbnail from server and store
|
||||
it locally.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
thumbnail_id (str): Thumbnail id.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Thumbnail path or None if not found.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
# Selection model wrapper calls
|
||||
@abstractmethod
|
||||
def get_selected_project_name(self):
|
||||
"""Get selected project name.
|
||||
|
||||
The information is based on last selection from UI.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Selected project name.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_selected_folder_ids(self):
|
||||
"""Get selected folder ids.
|
||||
|
||||
The information is based on last selection from UI.
|
||||
|
||||
Returns:
|
||||
list[str]: Selected folder ids.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_selected_version_ids(self):
|
||||
"""Get selected version ids.
|
||||
|
||||
The information is based on last selection from UI.
|
||||
|
||||
Returns:
|
||||
list[str]: Selected version ids.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_selected_representation_ids(self):
|
||||
"""Get selected representation ids.
|
||||
|
||||
The information is based on last selection from UI.
|
||||
|
||||
Returns:
|
||||
list[str]: Selected representation ids.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_selected_project(self, project_name):
|
||||
"""Set selected project.
|
||||
|
||||
Project selection changed in UI. Method triggers event with topic
|
||||
"selection.project.changed" with data:
|
||||
{
|
||||
"project_name": self._project_name
|
||||
}
|
||||
|
||||
Args:
|
||||
project_name (Union[str, None]): Selected project name.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_selected_folders(self, folder_ids):
|
||||
"""Set selected folders.
|
||||
|
||||
Folder selection changed in UI. Method triggers event with topic
|
||||
"selection.folders.changed" with data:
|
||||
{
|
||||
"project_name": project_name,
|
||||
"folder_ids": folder_ids
|
||||
}
|
||||
|
||||
Args:
|
||||
folder_ids (Iterable[str]): Selected folder ids.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_selected_versions(self, version_ids):
|
||||
"""Set selected versions.
|
||||
|
||||
Version selection changed in UI. Method triggers event with topic
|
||||
"selection.versions.changed" with data:
|
||||
{
|
||||
"project_name": project_name,
|
||||
"folder_ids": folder_ids,
|
||||
"version_ids": version_ids
|
||||
}
|
||||
|
||||
Args:
|
||||
version_ids (Iterable[str]): Selected version ids.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_selected_representations(self, repre_ids):
|
||||
"""Set selected representations.
|
||||
|
||||
Representation selection changed in UI. Method triggers event with
|
||||
topic "selection.representations.changed" with data:
|
||||
{
|
||||
"project_name": project_name,
|
||||
"folder_ids": folder_ids,
|
||||
"version_ids": version_ids,
|
||||
"representation_ids": representation_ids
|
||||
}
|
||||
|
||||
Args:
|
||||
repre_ids (Iterable[str]): Selected representation ids.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
# Load action items
|
||||
@abstractmethod
|
||||
def get_versions_action_items(self, project_name, version_ids):
|
||||
"""Action items for versions selection.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
version_ids (Iterable[str]): Version ids.
|
||||
|
||||
Returns:
|
||||
list[ActionItem]: List of action items.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_representations_action_items(
|
||||
self, project_name, representation_ids
|
||||
):
|
||||
"""Action items for representations selection.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
representation_ids (Iterable[str]): Representation ids.
|
||||
|
||||
Returns:
|
||||
list[ActionItem]: List of action items.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def trigger_action_item(
|
||||
self,
|
||||
identifier,
|
||||
options,
|
||||
project_name,
|
||||
version_ids,
|
||||
representation_ids
|
||||
):
|
||||
"""Trigger action item.
|
||||
|
||||
Triggers event "load.started" with data:
|
||||
{
|
||||
"identifier": identifier,
|
||||
"id": <Random UUID>,
|
||||
}
|
||||
|
||||
And triggers "load.finished" with data:
|
||||
{
|
||||
"identifier": identifier,
|
||||
"id": <Random UUID>,
|
||||
"error_info": [...],
|
||||
}
|
||||
|
||||
Args:
|
||||
identifier (str): Action identifier.
|
||||
options (dict[str, Any]): Action option values from UI.
|
||||
project_name (str): Project name.
|
||||
version_ids (Iterable[str]): Version ids.
|
||||
representation_ids (Iterable[str]): Representation ids.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def change_products_group(self, project_name, product_ids, group_name):
|
||||
"""Change group of products.
|
||||
|
||||
Triggers event "products.group.changed" with data:
|
||||
{
|
||||
"project_name": project_name,
|
||||
"folder_ids": folder_ids,
|
||||
"product_ids": product_ids,
|
||||
"group_name": group_name,
|
||||
}
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
product_ids (Iterable[str]): Product ids.
|
||||
group_name (str): New group name.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def fill_root_in_source(self, source):
|
||||
"""Fill root in source path.
|
||||
|
||||
Args:
|
||||
source (Union[str, None]): Source of a published version. Usually
|
||||
rootless workfile path.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
# NOTE: Methods 'is_loaded_products_supported' and
|
||||
# 'is_standard_projects_filter_enabled' are both based on being in host
|
||||
# or not. Maybe we could implement only single method 'is_in_host'?
|
||||
@abstractmethod
|
||||
def is_loaded_products_supported(self):
|
||||
"""Is capable to get information about loaded products.
|
||||
|
||||
Returns:
|
||||
bool: True if it is supported.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_standard_projects_filter_enabled(self):
|
||||
"""Is standard projects filter enabled.
|
||||
|
||||
This is used for filtering out when loader tool is used in a host. In
|
||||
that case only current project and library projects should be shown.
|
||||
|
||||
Returns:
|
||||
bool: Frontend should filter out non-library projects, except
|
||||
current context project.
|
||||
"""
|
||||
|
||||
pass
|
||||
343
openpype/tools/ayon_loader/control.py
Normal file
343
openpype/tools/ayon_loader/control.py
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
import logging
|
||||
|
||||
import ayon_api
|
||||
|
||||
from openpype.lib.events import QueuedEventSystem
|
||||
from openpype.pipeline import Anatomy, get_current_context
|
||||
from openpype.host import ILoadHost
|
||||
from openpype.tools.ayon_utils.models import (
|
||||
ProjectsModel,
|
||||
HierarchyModel,
|
||||
NestedCacheItem,
|
||||
CacheItem,
|
||||
ThumbnailsModel,
|
||||
)
|
||||
|
||||
from .abstract import BackendLoaderController, FrontendLoaderController
|
||||
from .models import SelectionModel, ProductsModel, LoaderActionsModel
|
||||
|
||||
|
||||
class ExpectedSelection:
|
||||
def __init__(self, controller):
|
||||
self._project_name = None
|
||||
self._folder_id = None
|
||||
|
||||
self._project_selected = True
|
||||
self._folder_selected = True
|
||||
|
||||
self._controller = controller
|
||||
|
||||
def _emit_change(self):
|
||||
self._controller.emit_event(
|
||||
"expected_selection_changed",
|
||||
self.get_expected_selection_data(),
|
||||
)
|
||||
|
||||
def set_expected_selection(self, project_name, folder_id):
|
||||
self._project_name = project_name
|
||||
self._folder_id = folder_id
|
||||
|
||||
self._project_selected = False
|
||||
self._folder_selected = False
|
||||
self._emit_change()
|
||||
|
||||
def get_expected_selection_data(self):
|
||||
project_current = False
|
||||
folder_current = False
|
||||
if not self._project_selected:
|
||||
project_current = True
|
||||
elif not self._folder_selected:
|
||||
folder_current = True
|
||||
return {
|
||||
"project": {
|
||||
"name": self._project_name,
|
||||
"current": project_current,
|
||||
"selected": self._project_selected,
|
||||
},
|
||||
"folder": {
|
||||
"id": self._folder_id,
|
||||
"current": folder_current,
|
||||
"selected": self._folder_selected,
|
||||
},
|
||||
}
|
||||
|
||||
def is_expected_project_selected(self, project_name):
|
||||
return project_name == self._project_name and self._project_selected
|
||||
|
||||
def is_expected_folder_selected(self, folder_id):
|
||||
return folder_id == self._folder_id and self._folder_selected
|
||||
|
||||
def expected_project_selected(self, project_name):
|
||||
if project_name != self._project_name:
|
||||
return False
|
||||
self._project_selected = True
|
||||
self._emit_change()
|
||||
return True
|
||||
|
||||
def expected_folder_selected(self, folder_id):
|
||||
if folder_id != self._folder_id:
|
||||
return False
|
||||
self._folder_selected = True
|
||||
self._emit_change()
|
||||
return True
|
||||
|
||||
|
||||
class LoaderController(BackendLoaderController, FrontendLoaderController):
|
||||
"""
|
||||
|
||||
Args:
|
||||
host (Optional[AbstractHost]): Host object. Defaults to None.
|
||||
"""
|
||||
|
||||
def __init__(self, host=None):
|
||||
self._log = None
|
||||
self._host = host
|
||||
|
||||
self._event_system = self._create_event_system()
|
||||
|
||||
self._project_anatomy_cache = NestedCacheItem(
|
||||
levels=1, lifetime=60)
|
||||
self._loaded_products_cache = CacheItem(
|
||||
default_factory=set, lifetime=60)
|
||||
|
||||
self._selection_model = SelectionModel(self)
|
||||
self._expected_selection = ExpectedSelection(self)
|
||||
self._projects_model = ProjectsModel(self)
|
||||
self._hierarchy_model = HierarchyModel(self)
|
||||
self._products_model = ProductsModel(self)
|
||||
self._loader_actions_model = LoaderActionsModel(self)
|
||||
self._thumbnails_model = ThumbnailsModel()
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
if self._log is None:
|
||||
self._log = logging.getLogger(self.__class__.__name__)
|
||||
return self._log
|
||||
|
||||
# ---------------------------------
|
||||
# Implementation of abstract methods
|
||||
# ---------------------------------
|
||||
# Events system
|
||||
def emit_event(self, topic, data=None, source=None):
|
||||
"""Use implemented event system to trigger event."""
|
||||
|
||||
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._emit_event("controller.reset.started")
|
||||
|
||||
project_name = self.get_selected_project_name()
|
||||
folder_ids = self.get_selected_folder_ids()
|
||||
|
||||
self._project_anatomy_cache.reset()
|
||||
self._loaded_products_cache.reset()
|
||||
|
||||
self._products_model.reset()
|
||||
self._hierarchy_model.reset()
|
||||
self._loader_actions_model.reset()
|
||||
self._projects_model.reset()
|
||||
self._thumbnails_model.reset()
|
||||
|
||||
self._projects_model.refresh()
|
||||
|
||||
if not project_name and not folder_ids:
|
||||
context = self.get_current_context()
|
||||
project_name = context["project_name"]
|
||||
folder_id = context["folder_id"]
|
||||
self.set_expected_selection(project_name, folder_id)
|
||||
|
||||
self._emit_event("controller.reset.finished")
|
||||
|
||||
# Expected selection helpers
|
||||
def get_expected_selection_data(self):
|
||||
return self._expected_selection.get_expected_selection_data()
|
||||
|
||||
def set_expected_selection(self, project_name, folder_id):
|
||||
self._expected_selection.set_expected_selection(
|
||||
project_name, folder_id
|
||||
)
|
||||
|
||||
def expected_project_selected(self, project_name):
|
||||
self._expected_selection.expected_project_selected(project_name)
|
||||
|
||||
def expected_folder_selected(self, folder_id):
|
||||
self._expected_selection.expected_folder_selected(folder_id)
|
||||
|
||||
# Entity model wrappers
|
||||
def get_project_items(self, sender=None):
|
||||
return self._projects_model.get_project_items(sender)
|
||||
|
||||
def get_folder_items(self, project_name, sender=None):
|
||||
return self._hierarchy_model.get_folder_items(project_name, sender)
|
||||
|
||||
def get_product_items(self, project_name, folder_ids, sender=None):
|
||||
return self._products_model.get_product_items(
|
||||
project_name, folder_ids, sender)
|
||||
|
||||
def get_product_item(self, project_name, product_id):
|
||||
return self._products_model.get_product_item(
|
||||
project_name, product_id
|
||||
)
|
||||
|
||||
def get_product_type_items(self, project_name):
|
||||
return self._products_model.get_product_type_items(project_name)
|
||||
|
||||
def get_representation_items(
|
||||
self, project_name, version_ids, sender=None
|
||||
):
|
||||
return self._products_model.get_repre_items(
|
||||
project_name, version_ids, sender
|
||||
)
|
||||
|
||||
def get_folder_thumbnail_ids(self, project_name, folder_ids):
|
||||
return self._thumbnails_model.get_folder_thumbnail_ids(
|
||||
project_name, folder_ids)
|
||||
|
||||
def get_version_thumbnail_ids(self, project_name, version_ids):
|
||||
return self._thumbnails_model.get_version_thumbnail_ids(
|
||||
project_name, version_ids)
|
||||
|
||||
def get_thumbnail_path(self, project_name, thumbnail_id):
|
||||
return self._thumbnails_model.get_thumbnail_path(
|
||||
project_name, thumbnail_id
|
||||
)
|
||||
|
||||
def change_products_group(self, project_name, product_ids, group_name):
|
||||
self._products_model.change_products_group(
|
||||
project_name, product_ids, group_name
|
||||
)
|
||||
|
||||
def get_versions_action_items(self, project_name, version_ids):
|
||||
return self._loader_actions_model.get_versions_action_items(
|
||||
project_name, version_ids)
|
||||
|
||||
def get_representations_action_items(
|
||||
self, project_name, representation_ids):
|
||||
return self._loader_actions_model.get_representations_action_items(
|
||||
project_name, representation_ids)
|
||||
|
||||
def trigger_action_item(
|
||||
self,
|
||||
identifier,
|
||||
options,
|
||||
project_name,
|
||||
version_ids,
|
||||
representation_ids
|
||||
):
|
||||
self._loader_actions_model.trigger_action_item(
|
||||
identifier,
|
||||
options,
|
||||
project_name,
|
||||
version_ids,
|
||||
representation_ids
|
||||
)
|
||||
|
||||
# Selection model wrappers
|
||||
def get_selected_project_name(self):
|
||||
return self._selection_model.get_selected_project_name()
|
||||
|
||||
def set_selected_project(self, project_name):
|
||||
self._selection_model.set_selected_project(project_name)
|
||||
|
||||
# Selection model wrappers
|
||||
def get_selected_folder_ids(self):
|
||||
return self._selection_model.get_selected_folder_ids()
|
||||
|
||||
def set_selected_folders(self, folder_ids):
|
||||
self._selection_model.set_selected_folders(folder_ids)
|
||||
|
||||
def get_selected_version_ids(self):
|
||||
return self._selection_model.get_selected_version_ids()
|
||||
|
||||
def set_selected_versions(self, version_ids):
|
||||
self._selection_model.set_selected_versions(version_ids)
|
||||
|
||||
def get_selected_representation_ids(self):
|
||||
return self._selection_model.get_selected_representation_ids()
|
||||
|
||||
def set_selected_representations(self, repre_ids):
|
||||
self._selection_model.set_selected_representations(repre_ids)
|
||||
|
||||
def fill_root_in_source(self, source):
|
||||
project_name = self.get_selected_project_name()
|
||||
anatomy = self._get_project_anatomy(project_name)
|
||||
if anatomy is None:
|
||||
return source
|
||||
|
||||
try:
|
||||
return anatomy.fill_root(source)
|
||||
except Exception:
|
||||
return source
|
||||
|
||||
def get_current_context(self):
|
||||
if self._host is None:
|
||||
return {
|
||||
"project_name": None,
|
||||
"folder_id": None,
|
||||
"task_name": None,
|
||||
}
|
||||
if hasattr(self._host, "get_current_context"):
|
||||
context = self._host.get_current_context()
|
||||
else:
|
||||
context = get_current_context()
|
||||
folder_id = None
|
||||
project_name = context.get("project_name")
|
||||
asset_name = context.get("asset_name")
|
||||
if project_name and asset_name:
|
||||
folder = ayon_api.get_folder_by_name(
|
||||
project_name, asset_name, fields=["id"]
|
||||
)
|
||||
if folder:
|
||||
folder_id = folder["id"]
|
||||
return {
|
||||
"project_name": project_name,
|
||||
"folder_id": folder_id,
|
||||
"task_name": context.get("task_name"),
|
||||
}
|
||||
|
||||
def get_loaded_product_ids(self):
|
||||
if self._host is None:
|
||||
return set()
|
||||
|
||||
context = self.get_current_context()
|
||||
project_name = context["project_name"]
|
||||
if not project_name:
|
||||
return set()
|
||||
|
||||
if not self._loaded_products_cache.is_valid:
|
||||
if isinstance(self._host, ILoadHost):
|
||||
containers = self._host.get_containers()
|
||||
else:
|
||||
containers = self._host.ls()
|
||||
repre_ids = {c.get("representation") for c in containers}
|
||||
repre_ids.discard(None)
|
||||
product_ids = self._products_model.get_product_ids_by_repre_ids(
|
||||
project_name, repre_ids
|
||||
)
|
||||
self._loaded_products_cache.update_data(product_ids)
|
||||
return self._loaded_products_cache.get_data()
|
||||
|
||||
def is_loaded_products_supported(self):
|
||||
return self._host is not None
|
||||
|
||||
def is_standard_projects_filter_enabled(self):
|
||||
return self._host is not None
|
||||
|
||||
def _get_project_anatomy(self, project_name):
|
||||
if not project_name:
|
||||
return None
|
||||
cache = self._project_anatomy_cache[project_name]
|
||||
if not cache.is_valid:
|
||||
cache.update_data(Anatomy(project_name))
|
||||
return cache.get_data()
|
||||
|
||||
def _create_event_system(self):
|
||||
return QueuedEventSystem()
|
||||
|
||||
def _emit_event(self, topic, data=None):
|
||||
self._event_system.emit(topic, data or {}, "controller")
|
||||
10
openpype/tools/ayon_loader/models/__init__.py
Normal file
10
openpype/tools/ayon_loader/models/__init__.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from .selection import SelectionModel
|
||||
from .products import ProductsModel
|
||||
from .actions import LoaderActionsModel
|
||||
|
||||
|
||||
__all__ = (
|
||||
"SelectionModel",
|
||||
"ProductsModel",
|
||||
"LoaderActionsModel",
|
||||
)
|
||||
870
openpype/tools/ayon_loader/models/actions.py
Normal file
870
openpype/tools/ayon_loader/models/actions.py
Normal file
|
|
@ -0,0 +1,870 @@
|
|||
import sys
|
||||
import traceback
|
||||
import inspect
|
||||
import copy
|
||||
import collections
|
||||
import uuid
|
||||
|
||||
from openpype.client import (
|
||||
get_project,
|
||||
get_assets,
|
||||
get_subsets,
|
||||
get_versions,
|
||||
get_representations,
|
||||
)
|
||||
from openpype.pipeline.load import (
|
||||
discover_loader_plugins,
|
||||
SubsetLoaderPlugin,
|
||||
filter_repre_contexts_by_loader,
|
||||
get_loader_identifier,
|
||||
load_with_repre_context,
|
||||
load_with_subset_context,
|
||||
load_with_subset_contexts,
|
||||
LoadError,
|
||||
IncompatibleLoaderError,
|
||||
)
|
||||
from openpype.tools.ayon_utils.models import NestedCacheItem
|
||||
from openpype.tools.ayon_loader.abstract import ActionItem
|
||||
|
||||
ACTIONS_MODEL_SENDER = "actions.model"
|
||||
NOT_SET = object()
|
||||
|
||||
|
||||
class LoaderActionsModel:
|
||||
"""Model for loader actions.
|
||||
|
||||
This is probably only part of models that requires to use codebase from
|
||||
'openpype.client' because of backwards compatibility with loaders logic
|
||||
which are expecting mongo documents.
|
||||
|
||||
TODOs:
|
||||
Deprecate 'qargparse' usage in loaders and implement conversion
|
||||
of 'ActionItem' to data (and 'from_data').
|
||||
Use controller to get entities (documents) -> possible only when
|
||||
loaders are able to handle AYON vs. OpenPype logic.
|
||||
Add missing site sync logic, and if possible remove it from loaders.
|
||||
Implement loader actions to replace load plugins.
|
||||
Ask loader actions to return action items instead of guessing them.
|
||||
"""
|
||||
|
||||
# Cache loader plugins for some time
|
||||
# NOTE Set to '0' for development
|
||||
loaders_cache_lifetime = 30
|
||||
|
||||
def __init__(self, controller):
|
||||
self._controller = controller
|
||||
self._current_context_project = NOT_SET
|
||||
self._loaders_by_identifier = NestedCacheItem(
|
||||
levels=1, lifetime=self.loaders_cache_lifetime)
|
||||
self._product_loaders = NestedCacheItem(
|
||||
levels=1, lifetime=self.loaders_cache_lifetime)
|
||||
self._repre_loaders = NestedCacheItem(
|
||||
levels=1, lifetime=self.loaders_cache_lifetime)
|
||||
|
||||
def reset(self):
|
||||
"""Reset the model with all cached items."""
|
||||
|
||||
self._current_context_project = NOT_SET
|
||||
self._loaders_by_identifier.reset()
|
||||
self._product_loaders.reset()
|
||||
self._repre_loaders.reset()
|
||||
|
||||
def get_versions_action_items(self, project_name, version_ids):
|
||||
"""Get action items for given version ids.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
version_ids (Iterable[str]): Version ids.
|
||||
|
||||
Returns:
|
||||
list[ActionItem]: List of action items.
|
||||
"""
|
||||
|
||||
(
|
||||
version_context_by_id,
|
||||
repre_context_by_id
|
||||
) = self._contexts_for_versions(
|
||||
project_name,
|
||||
version_ids
|
||||
)
|
||||
return self._get_action_items_for_contexts(
|
||||
project_name,
|
||||
version_context_by_id,
|
||||
repre_context_by_id
|
||||
)
|
||||
|
||||
def get_representations_action_items(
|
||||
self, project_name, representation_ids
|
||||
):
|
||||
"""Get action items for given representation ids.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
representation_ids (Iterable[str]): Representation ids.
|
||||
|
||||
Returns:
|
||||
list[ActionItem]: List of action items.
|
||||
"""
|
||||
|
||||
(
|
||||
product_context_by_id,
|
||||
repre_context_by_id
|
||||
) = self._contexts_for_representations(
|
||||
project_name,
|
||||
representation_ids
|
||||
)
|
||||
return self._get_action_items_for_contexts(
|
||||
project_name,
|
||||
product_context_by_id,
|
||||
repre_context_by_id
|
||||
)
|
||||
|
||||
def trigger_action_item(
|
||||
self,
|
||||
identifier,
|
||||
options,
|
||||
project_name,
|
||||
version_ids,
|
||||
representation_ids
|
||||
):
|
||||
"""Trigger action by identifier.
|
||||
|
||||
Triggers the action by identifier for given contexts.
|
||||
|
||||
Triggers events "load.started" and "load.finished". Finished event
|
||||
also contains "error_info" key with error information if any
|
||||
happened.
|
||||
|
||||
Args:
|
||||
identifier (str): Loader identifier.
|
||||
options (dict[str, Any]): Loader option values.
|
||||
project_name (str): Project name.
|
||||
version_ids (Iterable[str]): Version ids.
|
||||
representation_ids (Iterable[str]): Representation ids.
|
||||
"""
|
||||
|
||||
event_data = {
|
||||
"identifier": identifier,
|
||||
"id": uuid.uuid4().hex,
|
||||
}
|
||||
self._controller.emit_event(
|
||||
"load.started",
|
||||
event_data,
|
||||
ACTIONS_MODEL_SENDER,
|
||||
)
|
||||
loader = self._get_loader_by_identifier(project_name, identifier)
|
||||
if representation_ids is not None:
|
||||
error_info = self._trigger_representation_loader(
|
||||
loader,
|
||||
options,
|
||||
project_name,
|
||||
representation_ids,
|
||||
)
|
||||
elif version_ids is not None:
|
||||
error_info = self._trigger_version_loader(
|
||||
loader,
|
||||
options,
|
||||
project_name,
|
||||
version_ids,
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Invalid arguments to trigger action item")
|
||||
|
||||
event_data["error_info"] = error_info
|
||||
self._controller.emit_event(
|
||||
"load.finished",
|
||||
event_data,
|
||||
ACTIONS_MODEL_SENDER,
|
||||
)
|
||||
|
||||
def _get_current_context_project(self):
|
||||
"""Get current context project name.
|
||||
|
||||
The value is based on controller (host) and cached.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Current context project.
|
||||
"""
|
||||
|
||||
if self._current_context_project is NOT_SET:
|
||||
context = self._controller.get_current_context()
|
||||
self._current_context_project = context["project_name"]
|
||||
return self._current_context_project
|
||||
|
||||
def _get_action_label(self, loader, representation=None):
|
||||
"""Pull label info from loader class.
|
||||
|
||||
Args:
|
||||
loader (LoaderPlugin): Plugin class.
|
||||
representation (Optional[dict[str, Any]]): Representation data.
|
||||
|
||||
Returns:
|
||||
str: Action label.
|
||||
"""
|
||||
|
||||
label = getattr(loader, "label", None)
|
||||
if label is None:
|
||||
label = loader.__name__
|
||||
if representation:
|
||||
# Add the representation as suffix
|
||||
label = "{} ({})".format(label, representation["name"])
|
||||
return label
|
||||
|
||||
def _get_action_icon(self, loader):
|
||||
"""Pull icon info from loader class.
|
||||
|
||||
Args:
|
||||
loader (LoaderPlugin): Plugin class.
|
||||
|
||||
Returns:
|
||||
Union[dict[str, Any], None]: Icon definition based on
|
||||
loader plugin.
|
||||
"""
|
||||
|
||||
# Support font-awesome icons using the `.icon` and `.color`
|
||||
# attributes on plug-ins.
|
||||
icon = getattr(loader, "icon", None)
|
||||
if icon is not None and not isinstance(icon, dict):
|
||||
icon = {
|
||||
"type": "awesome-font",
|
||||
"name": icon,
|
||||
"color": getattr(loader, "color", None) or "white"
|
||||
}
|
||||
return icon
|
||||
|
||||
def _get_action_tooltip(self, loader):
|
||||
"""Pull tooltip info from loader class.
|
||||
|
||||
Args:
|
||||
loader (LoaderPlugin): Plugin class.
|
||||
|
||||
Returns:
|
||||
str: Action tooltip.
|
||||
"""
|
||||
|
||||
# Add tooltip and statustip from Loader docstring
|
||||
return inspect.getdoc(loader)
|
||||
|
||||
def _filter_loaders_by_tool_name(self, project_name, loaders):
|
||||
"""Filter loaders by tool name.
|
||||
|
||||
Tool names are based on OpenPype tools loader tool and library
|
||||
loader tool. The new tool merged both into one tool and the difference
|
||||
is based only on current project name.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
loaders (list[LoaderPlugin]): List of loader plugins.
|
||||
|
||||
Returns:
|
||||
list[LoaderPlugin]: Filtered list of loader plugins.
|
||||
"""
|
||||
|
||||
# Keep filtering by tool name
|
||||
# - if current context project name is same as project name we do
|
||||
# expect the tool is used as OpenPype loader tool, otherwise
|
||||
# as library loader tool.
|
||||
if project_name == self._get_current_context_project():
|
||||
tool_name = "loader"
|
||||
else:
|
||||
tool_name = "library_loader"
|
||||
filtered_loaders = []
|
||||
for loader in loaders:
|
||||
tool_names = getattr(loader, "tool_names", None)
|
||||
if (
|
||||
tool_names is None
|
||||
or "*" in tool_names
|
||||
or tool_name in tool_names
|
||||
):
|
||||
filtered_loaders.append(loader)
|
||||
return filtered_loaders
|
||||
|
||||
def _create_loader_action_item(
|
||||
self,
|
||||
loader,
|
||||
contexts,
|
||||
project_name,
|
||||
folder_ids=None,
|
||||
product_ids=None,
|
||||
version_ids=None,
|
||||
representation_ids=None,
|
||||
repre_name=None,
|
||||
):
|
||||
label = self._get_action_label(loader)
|
||||
if repre_name:
|
||||
label = "{} ({})".format(label, repre_name)
|
||||
return ActionItem(
|
||||
get_loader_identifier(loader),
|
||||
label=label,
|
||||
icon=self._get_action_icon(loader),
|
||||
tooltip=self._get_action_tooltip(loader),
|
||||
options=loader.get_options(contexts),
|
||||
order=loader.order,
|
||||
project_name=project_name,
|
||||
folder_ids=folder_ids,
|
||||
product_ids=product_ids,
|
||||
version_ids=version_ids,
|
||||
representation_ids=representation_ids,
|
||||
)
|
||||
|
||||
def _get_loaders(self, project_name):
|
||||
"""Loaders with loaded settings for a project.
|
||||
|
||||
Questions:
|
||||
Project name is required because of settings. Should we actually
|
||||
pass in current project name instead of project name where
|
||||
we want to show loaders for?
|
||||
|
||||
Returns:
|
||||
tuple[list[SubsetLoaderPlugin], list[LoaderPlugin]]: Discovered
|
||||
loader plugins.
|
||||
"""
|
||||
|
||||
loaders_by_identifier_c = self._loaders_by_identifier[project_name]
|
||||
product_loaders_c = self._product_loaders[project_name]
|
||||
repre_loaders_c = self._repre_loaders[project_name]
|
||||
if loaders_by_identifier_c.is_valid:
|
||||
return product_loaders_c.get_data(), repre_loaders_c.get_data()
|
||||
|
||||
# Get all representation->loader combinations available for the
|
||||
# index under the cursor, so we can list the user the options.
|
||||
available_loaders = self._filter_loaders_by_tool_name(
|
||||
project_name, discover_loader_plugins(project_name)
|
||||
)
|
||||
|
||||
repre_loaders = []
|
||||
product_loaders = []
|
||||
loaders_by_identifier = {}
|
||||
for loader_cls in available_loaders:
|
||||
if not loader_cls.enabled:
|
||||
continue
|
||||
|
||||
identifier = get_loader_identifier(loader_cls)
|
||||
loaders_by_identifier[identifier] = loader_cls
|
||||
if issubclass(loader_cls, SubsetLoaderPlugin):
|
||||
product_loaders.append(loader_cls)
|
||||
else:
|
||||
repre_loaders.append(loader_cls)
|
||||
|
||||
loaders_by_identifier_c.update_data(loaders_by_identifier)
|
||||
product_loaders_c.update_data(product_loaders)
|
||||
repre_loaders_c.update_data(repre_loaders)
|
||||
return product_loaders, repre_loaders
|
||||
|
||||
def _get_loader_by_identifier(self, project_name, identifier):
|
||||
if not self._loaders_by_identifier[project_name].is_valid:
|
||||
self._get_loaders(project_name)
|
||||
loaders_by_identifier_c = self._loaders_by_identifier[project_name]
|
||||
loaders_by_identifier = loaders_by_identifier_c.get_data()
|
||||
return loaders_by_identifier.get(identifier)
|
||||
|
||||
def _actions_sorter(self, action_item):
|
||||
"""Sort the Loaders by their order and then their name.
|
||||
|
||||
Returns:
|
||||
tuple[int, str]: Sort keys.
|
||||
"""
|
||||
|
||||
return action_item.order, action_item.label
|
||||
|
||||
def _get_version_docs(self, project_name, version_ids):
|
||||
"""Get version documents for given version ids.
|
||||
|
||||
This function also handles hero versions and copies data from
|
||||
source version to it.
|
||||
|
||||
Todos:
|
||||
Remove this function when this is completely rewritten to
|
||||
use AYON calls.
|
||||
"""
|
||||
|
||||
version_docs = list(get_versions(
|
||||
project_name, version_ids=version_ids, hero=True
|
||||
))
|
||||
hero_versions_by_src_id = collections.defaultdict(list)
|
||||
src_hero_version = set()
|
||||
for version_doc in version_docs:
|
||||
if version_doc["type"] != "hero":
|
||||
continue
|
||||
version_id = ""
|
||||
src_hero_version.add(version_id)
|
||||
hero_versions_by_src_id[version_id].append(version_doc)
|
||||
|
||||
src_versions = []
|
||||
if src_hero_version:
|
||||
src_versions = get_versions(project_name, version_ids=version_ids)
|
||||
for src_version in src_versions:
|
||||
src_version_id = src_version["_id"]
|
||||
for hero_version in hero_versions_by_src_id[src_version_id]:
|
||||
hero_version["data"] = copy.deepcopy(src_version["data"])
|
||||
|
||||
return version_docs
|
||||
|
||||
def _contexts_for_versions(self, project_name, version_ids):
|
||||
"""Get contexts for given version ids.
|
||||
|
||||
Prepare version contexts for 'SubsetLoaderPlugin' and representation
|
||||
contexts for 'LoaderPlugin' for all children representations of
|
||||
given versions.
|
||||
|
||||
This method is very similar to '_contexts_for_representations' but the
|
||||
queries of documents are called in a different order.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
version_ids (Iterable[str]): Version ids.
|
||||
|
||||
Returns:
|
||||
tuple[list[dict[str, Any]], list[dict[str, Any]]]: Version and
|
||||
representation contexts.
|
||||
"""
|
||||
|
||||
# TODO fix hero version
|
||||
version_context_by_id = {}
|
||||
repre_context_by_id = {}
|
||||
if not project_name and not version_ids:
|
||||
return version_context_by_id, repre_context_by_id
|
||||
|
||||
version_docs = self._get_version_docs(project_name, version_ids)
|
||||
version_docs_by_id = {}
|
||||
version_docs_by_product_id = collections.defaultdict(list)
|
||||
for version_doc in version_docs:
|
||||
version_id = version_doc["_id"]
|
||||
product_id = version_doc["parent"]
|
||||
version_docs_by_id[version_id] = version_doc
|
||||
version_docs_by_product_id[product_id].append(version_doc)
|
||||
|
||||
_product_ids = set(version_docs_by_product_id.keys())
|
||||
_product_docs = get_subsets(project_name, subset_ids=_product_ids)
|
||||
product_docs_by_id = {p["_id"]: p for p in _product_docs}
|
||||
|
||||
_folder_ids = {p["parent"] for p in product_docs_by_id.values()}
|
||||
_folder_docs = get_assets(project_name, asset_ids=_folder_ids)
|
||||
folder_docs_by_id = {f["_id"]: f for f in _folder_docs}
|
||||
|
||||
project_doc = get_project(project_name)
|
||||
project_doc["code"] = project_doc["data"]["code"]
|
||||
|
||||
for version_doc in version_docs:
|
||||
product_id = version_doc["parent"]
|
||||
product_doc = product_docs_by_id[product_id]
|
||||
folder_id = product_doc["parent"]
|
||||
folder_doc = folder_docs_by_id[folder_id]
|
||||
version_context_by_id[product_id] = {
|
||||
"project": project_doc,
|
||||
"asset": folder_doc,
|
||||
"subset": product_doc,
|
||||
"version": version_doc,
|
||||
}
|
||||
|
||||
repre_docs = get_representations(
|
||||
project_name, version_ids=version_ids)
|
||||
for repre_doc in repre_docs:
|
||||
version_id = repre_doc["parent"]
|
||||
version_doc = version_docs_by_id[version_id]
|
||||
product_id = version_doc["parent"]
|
||||
product_doc = product_docs_by_id[product_id]
|
||||
folder_id = product_doc["parent"]
|
||||
folder_doc = folder_docs_by_id[folder_id]
|
||||
|
||||
repre_context_by_id[repre_doc["_id"]] = {
|
||||
"project": project_doc,
|
||||
"asset": folder_doc,
|
||||
"subset": product_doc,
|
||||
"version": version_doc,
|
||||
"representation": repre_doc,
|
||||
}
|
||||
|
||||
return version_context_by_id, repre_context_by_id
|
||||
|
||||
def _contexts_for_representations(self, project_name, repre_ids):
|
||||
"""Get contexts for given representation ids.
|
||||
|
||||
Prepare version contexts for 'SubsetLoaderPlugin' and representation
|
||||
contexts for 'LoaderPlugin' for all children representations of
|
||||
given versions.
|
||||
|
||||
This method is very similar to '_contexts_for_versions' but the
|
||||
queries of documents are called in a different order.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
repre_ids (Iterable[str]): Representation ids.
|
||||
|
||||
Returns:
|
||||
tuple[list[dict[str, Any]], list[dict[str, Any]]]: Version and
|
||||
representation contexts.
|
||||
"""
|
||||
|
||||
product_context_by_id = {}
|
||||
repre_context_by_id = {}
|
||||
if not project_name and not repre_ids:
|
||||
return product_context_by_id, repre_context_by_id
|
||||
|
||||
repre_docs = list(get_representations(
|
||||
project_name, representation_ids=repre_ids
|
||||
))
|
||||
version_ids = {r["parent"] for r in repre_docs}
|
||||
version_docs = self._get_version_docs(project_name, version_ids)
|
||||
version_docs_by_id = {
|
||||
v["_id"]: v for v in version_docs
|
||||
}
|
||||
|
||||
product_ids = {v["parent"] for v in version_docs_by_id.values()}
|
||||
product_docs = get_subsets(project_name, subset_ids=product_ids)
|
||||
product_docs_by_id = {
|
||||
p["_id"]: p for p in product_docs
|
||||
}
|
||||
|
||||
folder_ids = {p["parent"] for p in product_docs_by_id.values()}
|
||||
folder_docs = get_assets(project_name, asset_ids=folder_ids)
|
||||
folder_docs_by_id = {
|
||||
f["_id"]: f for f in folder_docs
|
||||
}
|
||||
|
||||
project_doc = get_project(project_name)
|
||||
project_doc["code"] = project_doc["data"]["code"]
|
||||
|
||||
for product_id, product_doc in product_docs_by_id.items():
|
||||
folder_id = product_doc["parent"]
|
||||
folder_doc = folder_docs_by_id[folder_id]
|
||||
product_context_by_id[product_id] = {
|
||||
"project": project_doc,
|
||||
"asset": folder_doc,
|
||||
"subset": product_doc,
|
||||
}
|
||||
|
||||
for repre_doc in repre_docs:
|
||||
version_id = repre_doc["parent"]
|
||||
version_doc = version_docs_by_id[version_id]
|
||||
product_id = version_doc["parent"]
|
||||
product_doc = product_docs_by_id[product_id]
|
||||
folder_id = product_doc["parent"]
|
||||
folder_doc = folder_docs_by_id[folder_id]
|
||||
|
||||
repre_context_by_id[repre_doc["_id"]] = {
|
||||
"project": project_doc,
|
||||
"asset": folder_doc,
|
||||
"subset": product_doc,
|
||||
"version": version_doc,
|
||||
"representation": repre_doc,
|
||||
}
|
||||
return product_context_by_id, repre_context_by_id
|
||||
|
||||
def _get_action_items_for_contexts(
|
||||
self,
|
||||
project_name,
|
||||
version_context_by_id,
|
||||
repre_context_by_id
|
||||
):
|
||||
"""Prepare action items based on contexts.
|
||||
|
||||
Actions are prepared based on discovered loader plugins and contexts.
|
||||
The context must be valid for the loader plugin.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
version_context_by_id (dict[str, dict[str, Any]]): Version
|
||||
contexts by version id.
|
||||
repre_context_by_id (dict[str, dict[str, Any]]): Representation
|
||||
"""
|
||||
|
||||
action_items = []
|
||||
if not version_context_by_id and not repre_context_by_id:
|
||||
return action_items
|
||||
|
||||
product_loaders, repre_loaders = self._get_loaders(project_name)
|
||||
|
||||
repre_contexts_by_name = collections.defaultdict(list)
|
||||
for repre_context in repre_context_by_id.values():
|
||||
repre_name = repre_context["representation"]["name"]
|
||||
repre_contexts_by_name[repre_name].append(repre_context)
|
||||
|
||||
for loader in repre_loaders:
|
||||
# # do not allow download whole repre, select specific repre
|
||||
# if tools_lib.is_sync_loader(loader):
|
||||
# continue
|
||||
|
||||
for repre_name, repre_contexts in repre_contexts_by_name.items():
|
||||
filtered_repre_contexts = filter_repre_contexts_by_loader(
|
||||
repre_contexts, loader)
|
||||
if not filtered_repre_contexts:
|
||||
continue
|
||||
|
||||
repre_ids = set()
|
||||
repre_version_ids = set()
|
||||
repre_product_ids = set()
|
||||
repre_folder_ids = set()
|
||||
for repre_context in filtered_repre_contexts:
|
||||
repre_ids.add(repre_context["representation"]["_id"])
|
||||
repre_product_ids.add(repre_context["subset"]["_id"])
|
||||
repre_version_ids.add(repre_context["version"]["_id"])
|
||||
repre_folder_ids.add(repre_context["asset"]["_id"])
|
||||
|
||||
item = self._create_loader_action_item(
|
||||
loader,
|
||||
repre_contexts,
|
||||
project_name=project_name,
|
||||
folder_ids=repre_folder_ids,
|
||||
product_ids=repre_product_ids,
|
||||
version_ids=repre_version_ids,
|
||||
representation_ids=repre_ids,
|
||||
repre_name=repre_name,
|
||||
)
|
||||
action_items.append(item)
|
||||
|
||||
# Subset Loaders.
|
||||
version_ids = set(version_context_by_id.keys())
|
||||
product_folder_ids = set()
|
||||
product_ids = set()
|
||||
for product_context in version_context_by_id.values():
|
||||
product_ids.add(product_context["subset"]["_id"])
|
||||
product_folder_ids.add(product_context["asset"]["_id"])
|
||||
|
||||
version_contexts = list(version_context_by_id.values())
|
||||
for loader in product_loaders:
|
||||
item = self._create_loader_action_item(
|
||||
loader,
|
||||
version_contexts,
|
||||
project_name=project_name,
|
||||
folder_ids=product_folder_ids,
|
||||
product_ids=product_ids,
|
||||
version_ids=version_ids,
|
||||
)
|
||||
action_items.append(item)
|
||||
|
||||
action_items.sort(key=self._actions_sorter)
|
||||
return action_items
|
||||
|
||||
def _trigger_version_loader(
|
||||
self,
|
||||
loader,
|
||||
options,
|
||||
project_name,
|
||||
version_ids,
|
||||
):
|
||||
"""Trigger version loader.
|
||||
|
||||
This triggers 'load' method of 'SubsetLoaderPlugin' for given version
|
||||
ids.
|
||||
|
||||
Note:
|
||||
Even when the plugin is 'SubsetLoaderPlugin' it actually expects
|
||||
versions and should be named 'VersionLoaderPlugin'. Because it
|
||||
is planned to refactor load system and introduce
|
||||
'LoaderAction' plugins it is not relevant to change it
|
||||
anymore.
|
||||
|
||||
Args:
|
||||
loader (SubsetLoaderPlugin): Loader plugin to use.
|
||||
options (dict): Option values for loader.
|
||||
project_name (str): Project name.
|
||||
version_ids (Iterable[str]): Version ids.
|
||||
"""
|
||||
|
||||
project_doc = get_project(project_name)
|
||||
project_doc["code"] = project_doc["data"]["code"]
|
||||
|
||||
version_docs = self._get_version_docs(project_name, version_ids)
|
||||
product_ids = {v["parent"] for v in version_docs}
|
||||
product_docs = get_subsets(project_name, subset_ids=product_ids)
|
||||
product_docs_by_id = {f["_id"]: f for f in product_docs}
|
||||
folder_ids = {p["parent"] for p in product_docs_by_id.values()}
|
||||
folder_docs = get_assets(project_name, asset_ids=folder_ids)
|
||||
folder_docs_by_id = {f["_id"]: f for f in folder_docs}
|
||||
product_contexts = []
|
||||
for version_doc in version_docs:
|
||||
product_id = version_doc["parent"]
|
||||
product_doc = product_docs_by_id[product_id]
|
||||
folder_id = product_doc["parent"]
|
||||
folder_doc = folder_docs_by_id[folder_id]
|
||||
product_contexts.append({
|
||||
"project": project_doc,
|
||||
"asset": folder_doc,
|
||||
"subset": product_doc,
|
||||
"version": version_doc,
|
||||
})
|
||||
|
||||
return self._load_products_by_loader(
|
||||
loader, product_contexts, options
|
||||
)
|
||||
|
||||
def _trigger_representation_loader(
|
||||
self,
|
||||
loader,
|
||||
options,
|
||||
project_name,
|
||||
representation_ids,
|
||||
):
|
||||
"""Trigger representation loader.
|
||||
|
||||
This triggers 'load' method of 'LoaderPlugin' for given representation
|
||||
ids. For that are prepared contexts for each representation, with
|
||||
all parent documents.
|
||||
|
||||
Args:
|
||||
loader (LoaderPlugin): Loader plugin to use.
|
||||
options (dict): Option values for loader.
|
||||
project_name (str): Project name.
|
||||
representation_ids (Iterable[str]): Representation ids.
|
||||
"""
|
||||
|
||||
project_doc = get_project(project_name)
|
||||
project_doc["code"] = project_doc["data"]["code"]
|
||||
repre_docs = list(get_representations(
|
||||
project_name, representation_ids=representation_ids
|
||||
))
|
||||
version_ids = {r["parent"] for r in repre_docs}
|
||||
version_docs = self._get_version_docs(project_name, version_ids)
|
||||
version_docs_by_id = {v["_id"]: v for v in version_docs}
|
||||
product_ids = {v["parent"] for v in version_docs_by_id.values()}
|
||||
product_docs = get_subsets(project_name, subset_ids=product_ids)
|
||||
product_docs_by_id = {p["_id"]: p for p in product_docs}
|
||||
folder_ids = {p["parent"] for p in product_docs_by_id.values()}
|
||||
folder_docs = get_assets(project_name, asset_ids=folder_ids)
|
||||
folder_docs_by_id = {f["_id"]: f for f in folder_docs}
|
||||
repre_contexts = []
|
||||
for repre_doc in repre_docs:
|
||||
version_id = repre_doc["parent"]
|
||||
version_doc = version_docs_by_id[version_id]
|
||||
product_id = version_doc["parent"]
|
||||
product_doc = product_docs_by_id[product_id]
|
||||
folder_id = product_doc["parent"]
|
||||
folder_doc = folder_docs_by_id[folder_id]
|
||||
repre_contexts.append({
|
||||
"project": project_doc,
|
||||
"asset": folder_doc,
|
||||
"subset": product_doc,
|
||||
"version": version_doc,
|
||||
"representation": repre_doc,
|
||||
})
|
||||
|
||||
return self._load_representations_by_loader(
|
||||
loader, repre_contexts, options
|
||||
)
|
||||
|
||||
def _load_representations_by_loader(self, loader, repre_contexts, options):
|
||||
"""Loops through list of repre_contexts and loads them with one loader
|
||||
|
||||
Args:
|
||||
loader (LoaderPlugin): Loader plugin to use.
|
||||
repre_contexts (list[dict]): Full info about selected
|
||||
representations, containing repre, version, subset, asset and
|
||||
project documents.
|
||||
options (dict): Data from options.
|
||||
"""
|
||||
|
||||
error_info = []
|
||||
for repre_context in repre_contexts:
|
||||
version_doc = repre_context["version"]
|
||||
if version_doc["type"] == "hero_version":
|
||||
version_name = "Hero"
|
||||
else:
|
||||
version_name = version_doc.get("name")
|
||||
try:
|
||||
load_with_repre_context(
|
||||
loader,
|
||||
repre_context,
|
||||
options=options
|
||||
)
|
||||
|
||||
except IncompatibleLoaderError as exc:
|
||||
print(exc)
|
||||
error_info.append((
|
||||
"Incompatible Loader",
|
||||
None,
|
||||
repre_context["representation"]["name"],
|
||||
repre_context["subset"]["name"],
|
||||
version_name
|
||||
))
|
||||
|
||||
except Exception as exc:
|
||||
formatted_traceback = None
|
||||
if not isinstance(exc, LoadError):
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
formatted_traceback = "".join(traceback.format_exception(
|
||||
exc_type, exc_value, exc_traceback
|
||||
))
|
||||
|
||||
error_info.append((
|
||||
str(exc),
|
||||
formatted_traceback,
|
||||
repre_context["representation"]["name"],
|
||||
repre_context["subset"]["name"],
|
||||
version_name
|
||||
))
|
||||
return error_info
|
||||
|
||||
def _load_products_by_loader(self, loader, version_contexts, options):
|
||||
"""Triggers load with SubsetLoader type of loaders.
|
||||
|
||||
Warning:
|
||||
Plugin is named 'SubsetLoader' but version is passed to context
|
||||
too.
|
||||
|
||||
Args:
|
||||
loader (SubsetLoder): Loader used to load.
|
||||
version_contexts (list[dict[str, Any]]): For context for each
|
||||
version.
|
||||
options (dict[str, Any]): Options for loader that user could fill.
|
||||
"""
|
||||
|
||||
error_info = []
|
||||
if loader.is_multiple_contexts_compatible:
|
||||
subset_names = []
|
||||
for context in version_contexts:
|
||||
subset_name = context.get("subset", {}).get("name") or "N/A"
|
||||
subset_names.append(subset_name)
|
||||
try:
|
||||
load_with_subset_contexts(
|
||||
loader,
|
||||
version_contexts,
|
||||
options=options
|
||||
)
|
||||
|
||||
except Exception as exc:
|
||||
formatted_traceback = None
|
||||
if not isinstance(exc, LoadError):
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
formatted_traceback = "".join(traceback.format_exception(
|
||||
exc_type, exc_value, exc_traceback
|
||||
))
|
||||
error_info.append((
|
||||
str(exc),
|
||||
formatted_traceback,
|
||||
None,
|
||||
", ".join(subset_names),
|
||||
None
|
||||
))
|
||||
else:
|
||||
for version_context in version_contexts:
|
||||
subset_name = (
|
||||
version_context.get("subset", {}).get("name") or "N/A"
|
||||
)
|
||||
try:
|
||||
load_with_subset_context(
|
||||
loader,
|
||||
version_context,
|
||||
options=options
|
||||
)
|
||||
|
||||
except Exception as exc:
|
||||
formatted_traceback = None
|
||||
if not isinstance(exc, LoadError):
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
formatted_traceback = "".join(
|
||||
traceback.format_exception(
|
||||
exc_type, exc_value, exc_traceback
|
||||
)
|
||||
)
|
||||
|
||||
error_info.append((
|
||||
str(exc),
|
||||
formatted_traceback,
|
||||
None,
|
||||
subset_name,
|
||||
None
|
||||
))
|
||||
|
||||
return error_info
|
||||
682
openpype/tools/ayon_loader/models/products.py
Normal file
682
openpype/tools/ayon_loader/models/products.py
Normal file
|
|
@ -0,0 +1,682 @@
|
|||
import collections
|
||||
import contextlib
|
||||
|
||||
import arrow
|
||||
import ayon_api
|
||||
from ayon_api.operations import OperationsSession
|
||||
|
||||
from openpype.style import get_default_entity_icon_color
|
||||
from openpype.tools.ayon_utils.models import NestedCacheItem
|
||||
from openpype.tools.ayon_loader.abstract import (
|
||||
ProductTypeItem,
|
||||
ProductItem,
|
||||
VersionItem,
|
||||
RepreItem,
|
||||
)
|
||||
|
||||
PRODUCTS_MODEL_SENDER = "products.model"
|
||||
|
||||
|
||||
def version_item_from_entity(version):
|
||||
version_attribs = version["attrib"]
|
||||
frame_start = version_attribs.get("frameStart")
|
||||
frame_end = version_attribs.get("frameEnd")
|
||||
handle_start = version_attribs.get("handleStart")
|
||||
handle_end = version_attribs.get("handleEnd")
|
||||
step = version_attribs.get("step")
|
||||
comment = version_attribs.get("comment")
|
||||
source = version_attribs.get("source")
|
||||
|
||||
frame_range = None
|
||||
duration = None
|
||||
handles = None
|
||||
if frame_start is not None and frame_end is not None:
|
||||
# Remove superfluous zeros from numbers (3.0 -> 3) to improve
|
||||
# readability for most frame ranges
|
||||
frame_start = int(frame_start)
|
||||
frame_end = int(frame_end)
|
||||
frame_range = "{}-{}".format(frame_start, frame_end)
|
||||
duration = frame_end - frame_start + 1
|
||||
|
||||
if handle_start is not None and handle_end is not None:
|
||||
handles = "{}-{}".format(int(handle_start), int(handle_end))
|
||||
|
||||
# NOTE There is also 'updatedAt', should be used that instead?
|
||||
# TODO skip conversion - converting to '%Y%m%dT%H%M%SZ' is because
|
||||
# 'PrettyTimeDelegate' expects it
|
||||
created_at = arrow.get(version["createdAt"])
|
||||
published_time = created_at.strftime("%Y%m%dT%H%M%SZ")
|
||||
author = version["author"]
|
||||
version_num = version["version"]
|
||||
is_hero = version_num < 0
|
||||
|
||||
return VersionItem(
|
||||
version_id=version["id"],
|
||||
version=version_num,
|
||||
is_hero=is_hero,
|
||||
product_id=version["productId"],
|
||||
thumbnail_id=version["thumbnailId"],
|
||||
published_time=published_time,
|
||||
author=author,
|
||||
frame_range=frame_range,
|
||||
duration=duration,
|
||||
handles=handles,
|
||||
step=step,
|
||||
comment=comment,
|
||||
source=source,
|
||||
)
|
||||
|
||||
|
||||
def product_item_from_entity(
|
||||
product_entity,
|
||||
version_entities,
|
||||
product_type_items_by_name,
|
||||
folder_label,
|
||||
product_in_scene,
|
||||
):
|
||||
product_attribs = product_entity["attrib"]
|
||||
group = product_attribs.get("productGroup")
|
||||
product_type = product_entity["productType"]
|
||||
product_type_item = product_type_items_by_name[product_type]
|
||||
product_type_icon = product_type_item.icon
|
||||
|
||||
product_icon = {
|
||||
"type": "awesome-font",
|
||||
"name": "fa.file-o",
|
||||
"color": get_default_entity_icon_color(),
|
||||
}
|
||||
version_items = {
|
||||
version_entity["id"]: version_item_from_entity(version_entity)
|
||||
for version_entity in version_entities
|
||||
}
|
||||
|
||||
return ProductItem(
|
||||
product_id=product_entity["id"],
|
||||
product_type=product_type,
|
||||
product_name=product_entity["name"],
|
||||
product_icon=product_icon,
|
||||
product_type_icon=product_type_icon,
|
||||
product_in_scene=product_in_scene,
|
||||
group_name=group,
|
||||
folder_id=product_entity["folderId"],
|
||||
folder_label=folder_label,
|
||||
version_items=version_items,
|
||||
)
|
||||
|
||||
|
||||
def product_type_item_from_data(product_type_data):
|
||||
# TODO implement icon implementation
|
||||
# icon = product_type_data["icon"]
|
||||
# color = product_type_data["color"]
|
||||
icon = {
|
||||
"type": "awesome-font",
|
||||
"name": "fa.folder",
|
||||
"color": "#0091B2",
|
||||
}
|
||||
# TODO implement checked logic
|
||||
return ProductTypeItem(product_type_data["name"], icon, True)
|
||||
|
||||
|
||||
class ProductsModel:
|
||||
"""Model for products, version and representation.
|
||||
|
||||
All of the entities are product based. This model prepares data for UI
|
||||
and caches it for faster access.
|
||||
|
||||
Note:
|
||||
Data are not used for actions model because that would require to
|
||||
break OpenPype compatibility of 'LoaderPlugin's.
|
||||
"""
|
||||
|
||||
lifetime = 60 # In seconds (minute by default)
|
||||
|
||||
def __init__(self, controller):
|
||||
self._controller = controller
|
||||
|
||||
# Mapping helpers
|
||||
# NOTE - mapping must be cleaned up with cache cleanup
|
||||
self._product_item_by_id = collections.defaultdict(dict)
|
||||
self._version_item_by_id = collections.defaultdict(dict)
|
||||
self._product_folder_ids_mapping = collections.defaultdict(dict)
|
||||
|
||||
# Cache helpers
|
||||
self._product_type_items_cache = NestedCacheItem(
|
||||
levels=1, default_factory=list, lifetime=self.lifetime)
|
||||
self._product_items_cache = NestedCacheItem(
|
||||
levels=2, default_factory=dict, lifetime=self.lifetime)
|
||||
self._repre_items_cache = NestedCacheItem(
|
||||
levels=2, default_factory=dict, lifetime=self.lifetime)
|
||||
|
||||
def reset(self):
|
||||
"""Reset model with all cached data."""
|
||||
|
||||
self._product_item_by_id.clear()
|
||||
self._version_item_by_id.clear()
|
||||
self._product_folder_ids_mapping.clear()
|
||||
|
||||
self._product_type_items_cache.reset()
|
||||
self._product_items_cache.reset()
|
||||
self._repre_items_cache.reset()
|
||||
|
||||
def get_product_type_items(self, project_name):
|
||||
"""Product type items for project.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
|
||||
Returns:
|
||||
list[ProductTypeItem]: Product type items.
|
||||
"""
|
||||
|
||||
cache = self._product_type_items_cache[project_name]
|
||||
if not cache.is_valid:
|
||||
product_types = ayon_api.get_project_product_types(project_name)
|
||||
cache.update_data([
|
||||
product_type_item_from_data(product_type)
|
||||
for product_type in product_types
|
||||
])
|
||||
return cache.get_data()
|
||||
|
||||
def get_product_items(self, project_name, folder_ids, sender):
|
||||
"""Product items with versions for project and folder ids.
|
||||
|
||||
Product items also contain version items. They're directly connected
|
||||
to product items in the UI and the separation is not needed.
|
||||
|
||||
Args:
|
||||
project_name (Union[str, None]): Project name.
|
||||
folder_ids (Iterable[str]): Folder ids.
|
||||
sender (Union[str, None]): Who triggered the method.
|
||||
|
||||
Returns:
|
||||
list[ProductItem]: Product items.
|
||||
"""
|
||||
|
||||
if not project_name or not folder_ids:
|
||||
return []
|
||||
|
||||
project_cache = self._product_items_cache[project_name]
|
||||
output = []
|
||||
folder_ids_to_update = set()
|
||||
for folder_id in folder_ids:
|
||||
cache = project_cache[folder_id]
|
||||
if cache.is_valid:
|
||||
output.extend(cache.get_data().values())
|
||||
else:
|
||||
folder_ids_to_update.add(folder_id)
|
||||
|
||||
self._refresh_product_items(
|
||||
project_name, folder_ids_to_update, sender)
|
||||
|
||||
for folder_id in folder_ids_to_update:
|
||||
cache = project_cache[folder_id]
|
||||
output.extend(cache.get_data().values())
|
||||
return output
|
||||
|
||||
def get_product_item(self, project_name, product_id):
|
||||
"""Get product item based on passed product id.
|
||||
|
||||
This method is using cached items, but if cache is not valid it also
|
||||
can query the item.
|
||||
|
||||
Args:
|
||||
project_name (Union[str, None]): Where to look for product.
|
||||
product_id (Union[str, None]): Product id to receive.
|
||||
|
||||
Returns:
|
||||
Union[ProductItem, None]: Product item or 'None' if not found.
|
||||
"""
|
||||
|
||||
if not any((project_name, product_id)):
|
||||
return None
|
||||
|
||||
product_items_by_id = self._product_item_by_id[project_name]
|
||||
product_item = product_items_by_id.get(product_id)
|
||||
if product_item is not None:
|
||||
return product_item
|
||||
for product_item in self._query_product_items_by_ids(
|
||||
project_name, product_ids=[product_id]
|
||||
).values():
|
||||
return product_item
|
||||
|
||||
def get_product_ids_by_repre_ids(self, project_name, repre_ids):
|
||||
"""Get product ids based on passed representation ids.
|
||||
|
||||
Args:
|
||||
project_name (str): Where to look for representations.
|
||||
repre_ids (Iterable[str]): Representation ids.
|
||||
|
||||
Returns:
|
||||
set[str]: Product ids for passed representation ids.
|
||||
"""
|
||||
|
||||
# TODO look out how to use single server call
|
||||
if not repre_ids:
|
||||
return set()
|
||||
repres = ayon_api.get_representations(
|
||||
project_name, repre_ids, fields=["versionId"]
|
||||
)
|
||||
version_ids = {repre["versionId"] for repre in repres}
|
||||
if not version_ids:
|
||||
return set()
|
||||
versions = ayon_api.get_versions(
|
||||
project_name, version_ids=version_ids, fields=["productId"]
|
||||
)
|
||||
return {v["productId"] for v in versions}
|
||||
|
||||
def get_repre_items(self, project_name, version_ids, sender):
|
||||
"""Get representation items for passed version ids.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
version_ids (Iterable[str]): Version ids.
|
||||
sender (Union[str, None]): Who triggered the method.
|
||||
|
||||
Returns:
|
||||
list[RepreItem]: Representation items.
|
||||
"""
|
||||
|
||||
output = []
|
||||
if not any((project_name, version_ids)):
|
||||
return output
|
||||
|
||||
invalid_version_ids = set()
|
||||
project_cache = self._repre_items_cache[project_name]
|
||||
for version_id in version_ids:
|
||||
version_cache = project_cache[version_id]
|
||||
if version_cache.is_valid:
|
||||
output.extend(version_cache.get_data().values())
|
||||
else:
|
||||
invalid_version_ids.add(version_id)
|
||||
|
||||
if invalid_version_ids:
|
||||
self.refresh_representation_items(
|
||||
project_name, invalid_version_ids, sender
|
||||
)
|
||||
|
||||
for version_id in invalid_version_ids:
|
||||
version_cache = project_cache[version_id]
|
||||
output.extend(version_cache.get_data().values())
|
||||
|
||||
return output
|
||||
|
||||
def change_products_group(self, project_name, product_ids, group_name):
|
||||
"""Change group name for passed product ids.
|
||||
|
||||
Group name is stored in 'attrib' of product entity and is used in UI
|
||||
to group items.
|
||||
|
||||
Method triggers "products.group.changed" event with data:
|
||||
{
|
||||
"project_name": project_name,
|
||||
"folder_ids": folder_ids,
|
||||
"product_ids": product_ids,
|
||||
"group_name": group_name
|
||||
}
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
product_ids (Iterable[str]): Product ids to change group name for.
|
||||
group_name (str): Group name to set.
|
||||
"""
|
||||
|
||||
if not product_ids:
|
||||
return
|
||||
|
||||
product_items = self._get_product_items_by_id(
|
||||
project_name, product_ids
|
||||
)
|
||||
if not product_items:
|
||||
return
|
||||
|
||||
session = OperationsSession()
|
||||
folder_ids = set()
|
||||
for product_item in product_items.values():
|
||||
session.update_entity(
|
||||
project_name,
|
||||
"product",
|
||||
product_item.product_id,
|
||||
{"attrib": {"productGroup": group_name}}
|
||||
)
|
||||
folder_ids.add(product_item.folder_id)
|
||||
product_item.group_name = group_name
|
||||
|
||||
session.commit()
|
||||
self._controller.emit_event(
|
||||
"products.group.changed",
|
||||
{
|
||||
"project_name": project_name,
|
||||
"folder_ids": folder_ids,
|
||||
"product_ids": product_ids,
|
||||
"group_name": group_name,
|
||||
},
|
||||
PRODUCTS_MODEL_SENDER
|
||||
)
|
||||
|
||||
def _get_product_items_by_id(self, project_name, product_ids):
|
||||
product_item_by_id = self._product_item_by_id[project_name]
|
||||
missing_product_ids = set()
|
||||
output = {}
|
||||
for product_id in product_ids:
|
||||
product_item = product_item_by_id.get(product_id)
|
||||
if product_item is not None:
|
||||
output[product_id] = product_item
|
||||
else:
|
||||
missing_product_ids.add(product_id)
|
||||
|
||||
output.update(
|
||||
self._query_product_items_by_ids(
|
||||
project_name, missing_product_ids
|
||||
)
|
||||
)
|
||||
return output
|
||||
|
||||
def _get_version_items_by_id(self, project_name, version_ids):
|
||||
version_item_by_id = self._version_item_by_id[project_name]
|
||||
missing_version_ids = set()
|
||||
output = {}
|
||||
for version_id in version_ids:
|
||||
version_item = version_item_by_id.get(version_id)
|
||||
if version_item is not None:
|
||||
output[version_id] = version_item
|
||||
else:
|
||||
missing_version_ids.add(version_id)
|
||||
|
||||
output.update(
|
||||
self._query_version_items_by_ids(
|
||||
project_name, missing_version_ids
|
||||
)
|
||||
)
|
||||
return output
|
||||
|
||||
def _create_product_items(
|
||||
self,
|
||||
project_name,
|
||||
products,
|
||||
versions,
|
||||
folder_items=None,
|
||||
product_type_items=None,
|
||||
):
|
||||
if folder_items is None:
|
||||
folder_items = self._controller.get_folder_items(project_name)
|
||||
|
||||
if product_type_items is None:
|
||||
product_type_items = self.get_product_type_items(project_name)
|
||||
|
||||
loaded_product_ids = self._controller.get_loaded_product_ids()
|
||||
|
||||
versions_by_product_id = collections.defaultdict(list)
|
||||
for version in versions:
|
||||
versions_by_product_id[version["productId"]].append(version)
|
||||
product_type_items_by_name = {
|
||||
product_type_item.name: product_type_item
|
||||
for product_type_item in product_type_items
|
||||
}
|
||||
output = {}
|
||||
for product in products:
|
||||
product_id = product["id"]
|
||||
folder_id = product["folderId"]
|
||||
folder_item = folder_items.get(folder_id)
|
||||
if not folder_item:
|
||||
continue
|
||||
versions = versions_by_product_id[product_id]
|
||||
if not versions:
|
||||
continue
|
||||
product_item = product_item_from_entity(
|
||||
product,
|
||||
versions,
|
||||
product_type_items_by_name,
|
||||
folder_item.label,
|
||||
product_id in loaded_product_ids,
|
||||
)
|
||||
output[product_id] = product_item
|
||||
return output
|
||||
|
||||
def _query_product_items_by_ids(
|
||||
self,
|
||||
project_name,
|
||||
folder_ids=None,
|
||||
product_ids=None,
|
||||
folder_items=None
|
||||
):
|
||||
"""Query product items.
|
||||
|
||||
This method does get from, or store to, cache attributes.
|
||||
|
||||
One of 'product_ids' or 'folder_ids' must be passed to the method.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
folder_ids (Optional[Iterable[str]]): Folder ids under which are
|
||||
products.
|
||||
product_ids (Optional[Iterable[str]]): Product ids to use.
|
||||
folder_items (Optional[Dict[str, FolderItem]]): Prepared folder
|
||||
items from controller.
|
||||
|
||||
Returns:
|
||||
dict[str, ProductItem]: Product items by product id.
|
||||
"""
|
||||
|
||||
if not folder_ids and not product_ids:
|
||||
return {}
|
||||
|
||||
kwargs = {}
|
||||
if folder_ids is not None:
|
||||
kwargs["folder_ids"] = folder_ids
|
||||
|
||||
if product_ids is not None:
|
||||
kwargs["product_ids"] = product_ids
|
||||
|
||||
products = list(ayon_api.get_products(project_name, **kwargs))
|
||||
product_ids = {product["id"] for product in products}
|
||||
|
||||
versions = ayon_api.get_versions(
|
||||
project_name, product_ids=product_ids
|
||||
)
|
||||
|
||||
return self._create_product_items(
|
||||
project_name, products, versions, folder_items=folder_items
|
||||
)
|
||||
|
||||
def _query_version_items_by_ids(self, project_name, version_ids):
|
||||
versions = list(ayon_api.get_versions(
|
||||
project_name, version_ids=version_ids
|
||||
))
|
||||
product_ids = {version["productId"] for version in versions}
|
||||
products = list(ayon_api.get_products(
|
||||
project_name, product_ids=product_ids
|
||||
))
|
||||
product_items = self._create_product_items(
|
||||
project_name, products, versions
|
||||
)
|
||||
version_items = {}
|
||||
for product_item in product_items.values():
|
||||
version_items.update(product_item.version_items)
|
||||
return version_items
|
||||
|
||||
def _clear_product_version_items(self, project_name, folder_ids):
|
||||
"""Clear product and version items from memory.
|
||||
|
||||
When products are re-queried for a folders, the old product and version
|
||||
items in '_product_item_by_id' and '_version_item_by_id' should
|
||||
be cleaned up from memory. And mapping in stored in
|
||||
'_product_folder_ids_mapping' is not relevant either.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of project.
|
||||
folder_ids (Iterable[str]): Folder ids which are being refreshed.
|
||||
"""
|
||||
|
||||
project_mapping = self._product_folder_ids_mapping[project_name]
|
||||
if not project_mapping:
|
||||
return
|
||||
|
||||
product_item_by_id = self._product_item_by_id[project_name]
|
||||
version_item_by_id = self._version_item_by_id[project_name]
|
||||
for folder_id in folder_ids:
|
||||
product_ids = project_mapping.pop(folder_id, None)
|
||||
if not product_ids:
|
||||
continue
|
||||
|
||||
for product_id in product_ids:
|
||||
product_item = product_item_by_id.pop(product_id, None)
|
||||
if product_item is None:
|
||||
continue
|
||||
for version_item in product_item.version_items.values():
|
||||
version_item_by_id.pop(version_item.version_id, None)
|
||||
|
||||
def _refresh_product_items(self, project_name, folder_ids, sender):
|
||||
"""Refresh product items and store them in cache.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of project.
|
||||
folder_ids (Iterable[str]): Folder ids which are being refreshed.
|
||||
sender (Union[str, None]): Who triggered the refresh.
|
||||
"""
|
||||
|
||||
if not project_name or not folder_ids:
|
||||
return
|
||||
|
||||
self._clear_product_version_items(project_name, folder_ids)
|
||||
|
||||
project_mapping = self._product_folder_ids_mapping[project_name]
|
||||
product_item_by_id = self._product_item_by_id[project_name]
|
||||
version_item_by_id = self._version_item_by_id[project_name]
|
||||
|
||||
for folder_id in folder_ids:
|
||||
project_mapping[folder_id] = set()
|
||||
|
||||
with self._product_refresh_event_manager(
|
||||
project_name, folder_ids, sender
|
||||
):
|
||||
folder_items = self._controller.get_folder_items(project_name)
|
||||
items_by_folder_id = {
|
||||
folder_id: {}
|
||||
for folder_id in folder_ids
|
||||
}
|
||||
product_items_by_id = self._query_product_items_by_ids(
|
||||
project_name,
|
||||
folder_ids=folder_ids,
|
||||
folder_items=folder_items
|
||||
)
|
||||
for product_id, product_item in product_items_by_id.items():
|
||||
folder_id = product_item.folder_id
|
||||
items_by_folder_id[product_item.folder_id][product_id] = (
|
||||
product_item
|
||||
)
|
||||
|
||||
project_mapping[folder_id].add(product_id)
|
||||
product_item_by_id[product_id] = product_item
|
||||
for version_id, version_item in (
|
||||
product_item.version_items.items()
|
||||
):
|
||||
version_item_by_id[version_id] = version_item
|
||||
|
||||
project_cache = self._product_items_cache[project_name]
|
||||
for folder_id, product_items in items_by_folder_id.items():
|
||||
project_cache[folder_id].update_data(product_items)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _product_refresh_event_manager(
|
||||
self, project_name, folder_ids, sender
|
||||
):
|
||||
self._controller.emit_event(
|
||||
"products.refresh.started",
|
||||
{
|
||||
"project_name": project_name,
|
||||
"folder_ids": folder_ids,
|
||||
"sender": sender,
|
||||
},
|
||||
PRODUCTS_MODEL_SENDER
|
||||
)
|
||||
try:
|
||||
yield
|
||||
|
||||
finally:
|
||||
self._controller.emit_event(
|
||||
"products.refresh.finished",
|
||||
{
|
||||
"project_name": project_name,
|
||||
"folder_ids": folder_ids,
|
||||
"sender": sender,
|
||||
},
|
||||
PRODUCTS_MODEL_SENDER
|
||||
)
|
||||
|
||||
def refresh_representation_items(
|
||||
self, project_name, version_ids, sender
|
||||
):
|
||||
if not any((project_name, version_ids)):
|
||||
return
|
||||
self._controller.emit_event(
|
||||
"model.representations.refresh.started",
|
||||
{
|
||||
"project_name": project_name,
|
||||
"version_ids": version_ids,
|
||||
"sender": sender,
|
||||
},
|
||||
PRODUCTS_MODEL_SENDER
|
||||
)
|
||||
failed = False
|
||||
try:
|
||||
self._refresh_representation_items(project_name, version_ids)
|
||||
except Exception:
|
||||
# TODO add more information about failed refresh
|
||||
failed = True
|
||||
|
||||
self._controller.emit_event(
|
||||
"model.representations.refresh.finished",
|
||||
{
|
||||
"project_name": project_name,
|
||||
"version_ids": version_ids,
|
||||
"sender": sender,
|
||||
"failed": failed,
|
||||
},
|
||||
PRODUCTS_MODEL_SENDER
|
||||
)
|
||||
|
||||
def _refresh_representation_items(self, project_name, version_ids):
|
||||
representations = list(ayon_api.get_representations(
|
||||
project_name,
|
||||
version_ids=version_ids,
|
||||
fields=["id", "name", "versionId"]
|
||||
))
|
||||
|
||||
version_items_by_id = self._get_version_items_by_id(
|
||||
project_name, version_ids
|
||||
)
|
||||
product_ids = {
|
||||
version_item.product_id
|
||||
for version_item in version_items_by_id.values()
|
||||
}
|
||||
product_items_by_id = self._get_product_items_by_id(
|
||||
project_name, product_ids
|
||||
)
|
||||
repre_icon = {
|
||||
"type": "awesome-font",
|
||||
"name": "fa.file-o",
|
||||
"color": get_default_entity_icon_color(),
|
||||
}
|
||||
repre_items_by_version_id = collections.defaultdict(dict)
|
||||
for representation in representations:
|
||||
version_id = representation["versionId"]
|
||||
version_item = version_items_by_id.get(version_id)
|
||||
if version_item is None:
|
||||
continue
|
||||
product_item = product_items_by_id.get(version_item.product_id)
|
||||
if product_item is None:
|
||||
continue
|
||||
repre_id = representation["id"]
|
||||
repre_item = RepreItem(
|
||||
repre_id,
|
||||
representation["name"],
|
||||
repre_icon,
|
||||
product_item.product_name,
|
||||
product_item.folder_label,
|
||||
)
|
||||
repre_items_by_version_id[version_id][repre_id] = repre_item
|
||||
|
||||
project_cache = self._repre_items_cache[project_name]
|
||||
for version_id, repre_items in repre_items_by_version_id.items():
|
||||
version_cache = project_cache[version_id]
|
||||
version_cache.update_data(repre_items)
|
||||
85
openpype/tools/ayon_loader/models/selection.py
Normal file
85
openpype/tools/ayon_loader/models/selection.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
class SelectionModel(object):
|
||||
"""Model handling selection changes.
|
||||
|
||||
Triggering events:
|
||||
- "selection.project.changed"
|
||||
- "selection.folders.changed"
|
||||
- "selection.versions.changed"
|
||||
"""
|
||||
|
||||
event_source = "selection.model"
|
||||
|
||||
def __init__(self, controller):
|
||||
self._controller = controller
|
||||
|
||||
self._project_name = None
|
||||
self._folder_ids = set()
|
||||
self._version_ids = set()
|
||||
self._representation_ids = set()
|
||||
|
||||
def get_selected_project_name(self):
|
||||
return self._project_name
|
||||
|
||||
def set_selected_project(self, project_name):
|
||||
if self._project_name == project_name:
|
||||
return
|
||||
|
||||
self._project_name = project_name
|
||||
self._controller.emit_event(
|
||||
"selection.project.changed",
|
||||
{"project_name": self._project_name},
|
||||
self.event_source
|
||||
)
|
||||
|
||||
def get_selected_folder_ids(self):
|
||||
return self._folder_ids
|
||||
|
||||
def set_selected_folders(self, folder_ids):
|
||||
if folder_ids == self._folder_ids:
|
||||
return
|
||||
|
||||
self._folder_ids = folder_ids
|
||||
self._controller.emit_event(
|
||||
"selection.folders.changed",
|
||||
{
|
||||
"project_name": self._project_name,
|
||||
"folder_ids": folder_ids,
|
||||
},
|
||||
self.event_source
|
||||
)
|
||||
|
||||
def get_selected_version_ids(self):
|
||||
return self._version_ids
|
||||
|
||||
def set_selected_versions(self, version_ids):
|
||||
if version_ids == self._version_ids:
|
||||
return
|
||||
|
||||
self._version_ids = version_ids
|
||||
self._controller.emit_event(
|
||||
"selection.versions.changed",
|
||||
{
|
||||
"project_name": self._project_name,
|
||||
"folder_ids": self._folder_ids,
|
||||
"version_ids": self._version_ids,
|
||||
},
|
||||
self.event_source
|
||||
)
|
||||
|
||||
def get_selected_representation_ids(self):
|
||||
return self._representation_ids
|
||||
|
||||
def set_selected_representations(self, repre_ids):
|
||||
if repre_ids == self._representation_ids:
|
||||
return
|
||||
|
||||
self._representation_ids = repre_ids
|
||||
self._controller.emit_event(
|
||||
"selection.representations.changed",
|
||||
{
|
||||
"project_name": self._project_name,
|
||||
"folder_ids": self._folder_ids,
|
||||
"version_ids": self._version_ids,
|
||||
"representation_ids": self._representation_ids,
|
||||
}
|
||||
)
|
||||
6
openpype/tools/ayon_loader/ui/__init__.py
Normal file
6
openpype/tools/ayon_loader/ui/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from .window import LoaderWindow
|
||||
|
||||
|
||||
__all__ = (
|
||||
"LoaderWindow",
|
||||
)
|
||||
118
openpype/tools/ayon_loader/ui/actions_utils.py
Normal file
118
openpype/tools/ayon_loader/ui/actions_utils.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import uuid
|
||||
|
||||
from qtpy import QtWidgets, QtGui
|
||||
import qtawesome
|
||||
|
||||
from openpype.lib.attribute_definitions import AbstractAttrDef
|
||||
from openpype.tools.attribute_defs import AttributeDefinitionsDialog
|
||||
from openpype.tools.utils.widgets import (
|
||||
OptionalMenu,
|
||||
OptionalAction,
|
||||
OptionDialog,
|
||||
)
|
||||
from openpype.tools.ayon_utils.widgets import get_qt_icon
|
||||
|
||||
|
||||
def show_actions_menu(action_items, global_point, one_item_selected, parent):
|
||||
selected_action_item = None
|
||||
selected_options = None
|
||||
|
||||
if not action_items:
|
||||
menu = QtWidgets.QMenu(parent)
|
||||
action = _get_no_loader_action(menu, one_item_selected)
|
||||
menu.addAction(action)
|
||||
menu.exec_(global_point)
|
||||
return selected_action_item, selected_options
|
||||
|
||||
menu = OptionalMenu(parent)
|
||||
|
||||
action_items_by_id = {}
|
||||
for action_item in action_items:
|
||||
item_id = uuid.uuid4().hex
|
||||
action_items_by_id[item_id] = action_item
|
||||
item_options = action_item.options
|
||||
icon = get_qt_icon(action_item.icon)
|
||||
use_option = bool(item_options)
|
||||
action = OptionalAction(
|
||||
action_item.label,
|
||||
icon,
|
||||
use_option,
|
||||
menu
|
||||
)
|
||||
if use_option:
|
||||
# Add option box tip
|
||||
action.set_option_tip(item_options)
|
||||
|
||||
tip = action_item.tooltip
|
||||
if tip:
|
||||
action.setToolTip(tip)
|
||||
action.setStatusTip(tip)
|
||||
|
||||
action.setData(item_id)
|
||||
|
||||
menu.addAction(action)
|
||||
|
||||
action = menu.exec_(global_point)
|
||||
if action is not None:
|
||||
item_id = action.data()
|
||||
selected_action_item = action_items_by_id.get(item_id)
|
||||
|
||||
if selected_action_item is not None:
|
||||
selected_options = _get_options(action, selected_action_item, parent)
|
||||
|
||||
return selected_action_item, selected_options
|
||||
|
||||
|
||||
def _get_options(action, action_item, parent):
|
||||
"""Provides dialog to select value from loader provided options.
|
||||
|
||||
Loader can provide static or dynamically created options based on
|
||||
AttributeDefinitions, and for backwards compatibility qargparse.
|
||||
|
||||
Args:
|
||||
action (OptionalAction) - Action object in menu.
|
||||
action_item (ActionItem) - Action item with context information.
|
||||
parent (QtCore.QObject) - Parent object for dialog.
|
||||
|
||||
Returns:
|
||||
Union[dict[str, Any], None]: Selected value from attributes or
|
||||
'None' if dialog was cancelled.
|
||||
"""
|
||||
|
||||
# Pop option dialog
|
||||
options = action_item.options
|
||||
if not getattr(action, "optioned", False) or not options:
|
||||
return {}
|
||||
|
||||
if isinstance(options[0], AbstractAttrDef):
|
||||
qargparse_options = False
|
||||
dialog = AttributeDefinitionsDialog(options, parent)
|
||||
else:
|
||||
qargparse_options = True
|
||||
dialog = OptionDialog(parent)
|
||||
dialog.create(options)
|
||||
|
||||
dialog.setWindowTitle(action.label + " Options")
|
||||
|
||||
if not dialog.exec_():
|
||||
return None
|
||||
|
||||
# Get option
|
||||
if qargparse_options:
|
||||
return dialog.parse()
|
||||
return dialog.get_values()
|
||||
|
||||
|
||||
def _get_no_loader_action(menu, one_item_selected):
|
||||
"""Creates dummy no loader option in 'menu'"""
|
||||
|
||||
if one_item_selected:
|
||||
submsg = "this version."
|
||||
else:
|
||||
submsg = "your selection."
|
||||
msg = "No compatible loaders for {}".format(submsg)
|
||||
icon = qtawesome.icon(
|
||||
"fa.exclamation",
|
||||
color=QtGui.QColor(255, 51, 0)
|
||||
)
|
||||
return QtWidgets.QAction(icon, ("*" + msg), menu)
|
||||
416
openpype/tools/ayon_loader/ui/folders_widget.py
Normal file
416
openpype/tools/ayon_loader/ui/folders_widget.py
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
import qtpy
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype.tools.utils import (
|
||||
RecursiveSortFilterProxyModel,
|
||||
DeselectableTreeView,
|
||||
)
|
||||
from openpype.style import get_objected_colors
|
||||
|
||||
from openpype.tools.ayon_utils.widgets import (
|
||||
FoldersModel,
|
||||
FOLDERS_MODEL_SENDER_NAME,
|
||||
)
|
||||
from openpype.tools.ayon_utils.widgets.folders_widget import ITEM_ID_ROLE
|
||||
|
||||
if qtpy.API == "pyside":
|
||||
from PySide.QtGui import QStyleOptionViewItemV4
|
||||
elif qtpy.API == "pyqt4":
|
||||
from PyQt4.QtGui import QStyleOptionViewItemV4
|
||||
|
||||
UNDERLINE_COLORS_ROLE = QtCore.Qt.UserRole + 4
|
||||
|
||||
|
||||
class UnderlinesFolderDelegate(QtWidgets.QItemDelegate):
|
||||
"""Item delegate drawing bars under folder label.
|
||||
|
||||
This is used in loader tool. Multiselection of folders
|
||||
may group products by name under colored groups. Selected color groups are
|
||||
then propagated back to selected folders as underlines.
|
||||
"""
|
||||
bar_height = 3
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(UnderlinesFolderDelegate, self).__init__(*args, **kwargs)
|
||||
colors = get_objected_colors("loader", "asset-view")
|
||||
self._selected_color = colors["selected"].get_qcolor()
|
||||
self._hover_color = colors["hover"].get_qcolor()
|
||||
self._selected_hover_color = colors["selected-hover"].get_qcolor()
|
||||
|
||||
def sizeHint(self, option, index):
|
||||
"""Add bar height to size hint."""
|
||||
result = super(UnderlinesFolderDelegate, self).sizeHint(option, index)
|
||||
height = result.height()
|
||||
result.setHeight(height + self.bar_height)
|
||||
|
||||
return result
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
"""Replicate painting of an item and draw color bars if needed."""
|
||||
# Qt4 compat
|
||||
if qtpy.API in ("pyside", "pyqt4"):
|
||||
option = QStyleOptionViewItemV4(option)
|
||||
|
||||
painter.save()
|
||||
|
||||
item_rect = QtCore.QRect(option.rect)
|
||||
item_rect.setHeight(option.rect.height() - self.bar_height)
|
||||
|
||||
subset_colors = index.data(UNDERLINE_COLORS_ROLE) or []
|
||||
|
||||
subset_colors_width = 0
|
||||
if subset_colors:
|
||||
subset_colors_width = option.rect.width() / len(subset_colors)
|
||||
|
||||
subset_rects = []
|
||||
counter = 0
|
||||
for subset_c in subset_colors:
|
||||
new_color = None
|
||||
new_rect = None
|
||||
if subset_c:
|
||||
new_color = QtGui.QColor(subset_c)
|
||||
|
||||
new_rect = QtCore.QRect(
|
||||
option.rect.left() + (counter * subset_colors_width),
|
||||
option.rect.top() + (
|
||||
option.rect.height() - self.bar_height
|
||||
),
|
||||
subset_colors_width,
|
||||
self.bar_height
|
||||
)
|
||||
subset_rects.append((new_color, new_rect))
|
||||
counter += 1
|
||||
|
||||
# Background
|
||||
if option.state & QtWidgets.QStyle.State_Selected:
|
||||
if len(subset_colors) == 0:
|
||||
item_rect.setTop(item_rect.top() + (self.bar_height / 2))
|
||||
|
||||
if option.state & QtWidgets.QStyle.State_MouseOver:
|
||||
bg_color = self._selected_hover_color
|
||||
else:
|
||||
bg_color = self._selected_color
|
||||
else:
|
||||
item_rect.setTop(item_rect.top() + (self.bar_height / 2))
|
||||
if option.state & QtWidgets.QStyle.State_MouseOver:
|
||||
bg_color = self._hover_color
|
||||
else:
|
||||
bg_color = QtGui.QColor()
|
||||
bg_color.setAlpha(0)
|
||||
|
||||
# When not needed to do a rounded corners (easier and without
|
||||
# painter restore):
|
||||
painter.fillRect(
|
||||
option.rect,
|
||||
QtGui.QBrush(bg_color)
|
||||
)
|
||||
|
||||
if option.state & QtWidgets.QStyle.State_Selected:
|
||||
for color, subset_rect in subset_rects:
|
||||
if not color or not subset_rect:
|
||||
continue
|
||||
painter.fillRect(subset_rect, QtGui.QBrush(color))
|
||||
|
||||
# Icon
|
||||
icon_index = index.model().index(
|
||||
index.row(), index.column(), index.parent()
|
||||
)
|
||||
# - Default icon_rect if not icon
|
||||
icon_rect = QtCore.QRect(
|
||||
item_rect.left(),
|
||||
item_rect.top(),
|
||||
# To make sure it's same size all the time
|
||||
option.rect.height() - self.bar_height,
|
||||
option.rect.height() - self.bar_height
|
||||
)
|
||||
icon = index.model().data(icon_index, QtCore.Qt.DecorationRole)
|
||||
|
||||
if icon:
|
||||
mode = QtGui.QIcon.Normal
|
||||
if not (option.state & QtWidgets.QStyle.State_Enabled):
|
||||
mode = QtGui.QIcon.Disabled
|
||||
elif option.state & QtWidgets.QStyle.State_Selected:
|
||||
mode = QtGui.QIcon.Selected
|
||||
|
||||
if isinstance(icon, QtGui.QPixmap):
|
||||
icon = QtGui.QIcon(icon)
|
||||
option.decorationSize = icon.size() / icon.devicePixelRatio()
|
||||
|
||||
elif isinstance(icon, QtGui.QColor):
|
||||
pixmap = QtGui.QPixmap(option.decorationSize)
|
||||
pixmap.fill(icon)
|
||||
icon = QtGui.QIcon(pixmap)
|
||||
|
||||
elif isinstance(icon, QtGui.QImage):
|
||||
icon = QtGui.QIcon(QtGui.QPixmap.fromImage(icon))
|
||||
option.decorationSize = icon.size() / icon.devicePixelRatio()
|
||||
|
||||
elif isinstance(icon, QtGui.QIcon):
|
||||
state = QtGui.QIcon.Off
|
||||
if option.state & QtWidgets.QStyle.State_Open:
|
||||
state = QtGui.QIcon.On
|
||||
actual_size = option.icon.actualSize(
|
||||
option.decorationSize, mode, state
|
||||
)
|
||||
option.decorationSize = QtCore.QSize(
|
||||
min(option.decorationSize.width(), actual_size.width()),
|
||||
min(option.decorationSize.height(), actual_size.height())
|
||||
)
|
||||
|
||||
state = QtGui.QIcon.Off
|
||||
if option.state & QtWidgets.QStyle.State_Open:
|
||||
state = QtGui.QIcon.On
|
||||
|
||||
icon.paint(
|
||||
painter, icon_rect,
|
||||
QtCore.Qt.AlignLeft, mode, state
|
||||
)
|
||||
|
||||
# Text
|
||||
text_rect = QtCore.QRect(
|
||||
icon_rect.left() + icon_rect.width() + 2,
|
||||
item_rect.top(),
|
||||
item_rect.width(),
|
||||
item_rect.height()
|
||||
)
|
||||
|
||||
painter.drawText(
|
||||
text_rect, QtCore.Qt.AlignVCenter,
|
||||
index.data(QtCore.Qt.DisplayRole)
|
||||
)
|
||||
|
||||
painter.restore()
|
||||
|
||||
|
||||
class LoaderFoldersModel(FoldersModel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LoaderFoldersModel, self).__init__(*args, **kwargs)
|
||||
|
||||
self._colored_items = set()
|
||||
|
||||
def _fill_item_data(self, item, folder_item):
|
||||
"""
|
||||
|
||||
Args:
|
||||
item (QtGui.QStandardItem): Item to fill data.
|
||||
folder_item (FolderItem): Folder item.
|
||||
"""
|
||||
|
||||
super(LoaderFoldersModel, self)._fill_item_data(item, folder_item)
|
||||
|
||||
def set_merged_products_selection(self, items):
|
||||
changes = {
|
||||
folder_id: None
|
||||
for folder_id in self._colored_items
|
||||
}
|
||||
|
||||
all_folder_ids = set()
|
||||
for item in items:
|
||||
folder_ids = item["folder_ids"]
|
||||
all_folder_ids.update(folder_ids)
|
||||
|
||||
for folder_id in all_folder_ids:
|
||||
changes[folder_id] = []
|
||||
|
||||
for item in items:
|
||||
item_color = item["color"]
|
||||
item_folder_ids = item["folder_ids"]
|
||||
for folder_id in all_folder_ids:
|
||||
folder_color = (
|
||||
item_color
|
||||
if folder_id in item_folder_ids
|
||||
else None
|
||||
)
|
||||
changes[folder_id].append(folder_color)
|
||||
|
||||
for folder_id, color_value in changes.items():
|
||||
item = self._items_by_id.get(folder_id)
|
||||
if item is not None:
|
||||
item.setData(color_value, UNDERLINE_COLORS_ROLE)
|
||||
|
||||
self._colored_items = all_folder_ids
|
||||
|
||||
|
||||
class LoaderFoldersWidget(QtWidgets.QWidget):
|
||||
"""Folders widget.
|
||||
|
||||
Widget that handles folders view, model and selection.
|
||||
|
||||
Expected selection handling is disabled by default. If enabled, the
|
||||
widget will handle the expected in predefined way. Widget is listening
|
||||
to event 'expected_selection_changed' with expected event data below,
|
||||
the same data must be available when called method
|
||||
'get_expected_selection_data' on controller.
|
||||
|
||||
{
|
||||
"folder": {
|
||||
"current": bool, # Folder is what should be set now
|
||||
"folder_id": Union[str, None], # Folder id that should be selected
|
||||
},
|
||||
...
|
||||
}
|
||||
|
||||
Selection is confirmed by calling method 'expected_folder_selected' on
|
||||
controller.
|
||||
|
||||
|
||||
Args:
|
||||
controller (AbstractWorkfilesFrontend): The control object.
|
||||
parent (QtWidgets.QWidget): The parent widget.
|
||||
handle_expected_selection (bool): If True, the widget will handle
|
||||
the expected selection. Defaults to False.
|
||||
"""
|
||||
|
||||
refreshed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent, handle_expected_selection=False):
|
||||
super(LoaderFoldersWidget, self).__init__(parent)
|
||||
|
||||
folders_view = DeselectableTreeView(self)
|
||||
folders_view.setHeaderHidden(True)
|
||||
folders_view.setSelectionMode(
|
||||
QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
|
||||
folders_model = LoaderFoldersModel(controller)
|
||||
folders_proxy_model = RecursiveSortFilterProxyModel()
|
||||
folders_proxy_model.setSourceModel(folders_model)
|
||||
folders_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
|
||||
folders_label_delegate = UnderlinesFolderDelegate(folders_view)
|
||||
|
||||
folders_view.setModel(folders_proxy_model)
|
||||
folders_view.setItemDelegate(folders_label_delegate)
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(folders_view, 1)
|
||||
|
||||
controller.register_event_callback(
|
||||
"selection.project.changed",
|
||||
self._on_project_selection_change,
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"folders.refresh.finished",
|
||||
self._on_folders_refresh_finished
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"controller.refresh.finished",
|
||||
self._on_controller_refresh
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"expected_selection_changed",
|
||||
self._on_expected_selection_change
|
||||
)
|
||||
|
||||
selection_model = folders_view.selectionModel()
|
||||
selection_model.selectionChanged.connect(self._on_selection_change)
|
||||
|
||||
folders_model.refreshed.connect(self._on_model_refresh)
|
||||
|
||||
self._controller = controller
|
||||
self._folders_view = folders_view
|
||||
self._folders_model = folders_model
|
||||
self._folders_proxy_model = folders_proxy_model
|
||||
self._folders_label_delegate = folders_label_delegate
|
||||
|
||||
self._handle_expected_selection = handle_expected_selection
|
||||
self._expected_selection = None
|
||||
|
||||
def set_name_filer(self, name):
|
||||
"""Set filter of folder name.
|
||||
|
||||
Args:
|
||||
name (str): The string filter.
|
||||
"""
|
||||
|
||||
self._folders_proxy_model.setFilterFixedString(name)
|
||||
|
||||
def set_merged_products_selection(self, items):
|
||||
"""
|
||||
|
||||
Args:
|
||||
items (list[dict[str, Any]]): List of merged items with folder
|
||||
ids.
|
||||
"""
|
||||
|
||||
self._folders_model.set_merged_products_selection(items)
|
||||
|
||||
def refresh(self):
|
||||
self._folders_model.refresh()
|
||||
|
||||
def _on_project_selection_change(self, event):
|
||||
project_name = event["project_name"]
|
||||
self._set_project_name(project_name)
|
||||
|
||||
def _set_project_name(self, project_name):
|
||||
self._folders_model.set_project_name(project_name)
|
||||
|
||||
def _clear(self):
|
||||
self._folders_model.clear()
|
||||
|
||||
def _on_folders_refresh_finished(self, event):
|
||||
if event["sender"] != FOLDERS_MODEL_SENDER_NAME:
|
||||
self._set_project_name(event["project_name"])
|
||||
|
||||
def _on_controller_refresh(self):
|
||||
self._update_expected_selection()
|
||||
|
||||
def _on_model_refresh(self):
|
||||
if self._expected_selection:
|
||||
self._set_expected_selection()
|
||||
self._folders_proxy_model.sort(0)
|
||||
self.refreshed.emit()
|
||||
|
||||
def _get_selected_item_ids(self):
|
||||
selection_model = self._folders_view.selectionModel()
|
||||
item_ids = []
|
||||
for index in selection_model.selectedIndexes():
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
if item_id is not None:
|
||||
item_ids.append(item_id)
|
||||
return item_ids
|
||||
|
||||
def _on_selection_change(self):
|
||||
item_ids = self._get_selected_item_ids()
|
||||
self._controller.set_selected_folders(item_ids)
|
||||
|
||||
# Expected selection handling
|
||||
def _on_expected_selection_change(self, event):
|
||||
self._update_expected_selection(event.data)
|
||||
|
||||
def _update_expected_selection(self, expected_data=None):
|
||||
if not self._handle_expected_selection:
|
||||
return
|
||||
|
||||
if expected_data is None:
|
||||
expected_data = self._controller.get_expected_selection_data()
|
||||
|
||||
folder_data = expected_data.get("folder")
|
||||
if not folder_data or not folder_data["current"]:
|
||||
return
|
||||
|
||||
folder_id = folder_data["id"]
|
||||
self._expected_selection = folder_id
|
||||
if not self._folders_model.is_refreshing:
|
||||
self._set_expected_selection()
|
||||
|
||||
def _set_expected_selection(self):
|
||||
if not self._handle_expected_selection:
|
||||
return
|
||||
|
||||
folder_id = self._expected_selection
|
||||
selected_ids = self._get_selected_item_ids()
|
||||
self._expected_selection = None
|
||||
skip_selection = (
|
||||
folder_id is None
|
||||
or (
|
||||
folder_id in selected_ids
|
||||
and len(selected_ids) == 1
|
||||
)
|
||||
)
|
||||
if not skip_selection:
|
||||
index = self._folders_model.get_index_by_id(folder_id)
|
||||
if index.isValid():
|
||||
proxy_index = self._folders_proxy_model.mapFromSource(index)
|
||||
self._folders_view.setCurrentIndex(proxy_index)
|
||||
self._controller.expected_folder_selected(folder_id)
|
||||
141
openpype/tools/ayon_loader/ui/info_widget.py
Normal file
141
openpype/tools/ayon_loader/ui/info_widget.py
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import datetime
|
||||
|
||||
from qtpy import QtWidgets
|
||||
|
||||
from openpype.tools.utils.lib import format_version
|
||||
|
||||
|
||||
class VersionTextEdit(QtWidgets.QTextEdit):
|
||||
"""QTextEdit that displays version specific information.
|
||||
|
||||
This also overrides the context menu to add actions like copying
|
||||
source path to clipboard or copying the raw data of the version
|
||||
to clipboard.
|
||||
|
||||
"""
|
||||
def __init__(self, controller, parent):
|
||||
super(VersionTextEdit, self).__init__(parent=parent)
|
||||
|
||||
self._version_item = None
|
||||
self._product_item = None
|
||||
|
||||
self._controller = controller
|
||||
|
||||
# Reset
|
||||
self.set_current_item()
|
||||
|
||||
def set_current_item(self, product_item=None, version_item=None):
|
||||
"""
|
||||
|
||||
Args:
|
||||
product_item (Union[ProductItem, None]): Product item.
|
||||
version_item (Union[VersionItem, None]): Version item to display.
|
||||
"""
|
||||
|
||||
self._product_item = product_item
|
||||
self._version_item = version_item
|
||||
|
||||
if version_item is None:
|
||||
# Reset state to empty
|
||||
self.setText("")
|
||||
return
|
||||
|
||||
version_label = format_version(abs(version_item.version))
|
||||
if version_item.version < 0:
|
||||
version_label = "Hero version {}".format(version_label)
|
||||
|
||||
# Define readable creation timestamp
|
||||
created = version_item.published_time
|
||||
created = datetime.datetime.strptime(created, "%Y%m%dT%H%M%SZ")
|
||||
created = datetime.datetime.strftime(created, "%b %d %Y %H:%M")
|
||||
|
||||
comment = version_item.comment or "No comment"
|
||||
source = version_item.source or "No source"
|
||||
|
||||
self.setHtml(
|
||||
(
|
||||
"<h2>{product_name}</h2>"
|
||||
"<h3>{version_label}</h3>"
|
||||
"<b>Comment</b><br>"
|
||||
"{comment}<br><br>"
|
||||
|
||||
"<b>Created</b><br>"
|
||||
"{created}<br><br>"
|
||||
|
||||
"<b>Source</b><br>"
|
||||
"{source}"
|
||||
).format(
|
||||
product_name=product_item.product_name,
|
||||
version_label=version_label,
|
||||
comment=comment,
|
||||
created=created,
|
||||
source=source,
|
||||
)
|
||||
)
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
"""Context menu with additional actions"""
|
||||
menu = self.createStandardContextMenu()
|
||||
|
||||
# Add additional actions when any text, so we can assume
|
||||
# the version is set.
|
||||
source = None
|
||||
if self._version_item is not None:
|
||||
source = self._version_item.source
|
||||
|
||||
if source:
|
||||
menu.addSeparator()
|
||||
action = QtWidgets.QAction(
|
||||
"Copy source path to clipboard", menu
|
||||
)
|
||||
action.triggered.connect(self._on_copy_source)
|
||||
menu.addAction(action)
|
||||
|
||||
menu.exec_(event.globalPos())
|
||||
|
||||
def _on_copy_source(self):
|
||||
"""Copy formatted source path to clipboard."""
|
||||
|
||||
source = self._version_item.source
|
||||
if not source:
|
||||
return
|
||||
|
||||
filled_source = self._controller.fill_root_in_source(source)
|
||||
clipboard = QtWidgets.QApplication.clipboard()
|
||||
clipboard.setText(filled_source)
|
||||
|
||||
|
||||
class InfoWidget(QtWidgets.QWidget):
|
||||
"""A Widget that display information about a specific version"""
|
||||
def __init__(self, controller, parent):
|
||||
super(InfoWidget, self).__init__(parent=parent)
|
||||
|
||||
label_widget = QtWidgets.QLabel("Version Info", self)
|
||||
info_text_widget = VersionTextEdit(controller, self)
|
||||
info_text_widget.setReadOnly(True)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(label_widget, 0)
|
||||
layout.addWidget(info_text_widget, 1)
|
||||
|
||||
self._controller = controller
|
||||
|
||||
self._info_text_widget = info_text_widget
|
||||
self._label_widget = label_widget
|
||||
|
||||
def set_selected_version_info(self, project_name, items):
|
||||
if not items or not project_name:
|
||||
self._info_text_widget.set_current_item()
|
||||
return
|
||||
first_item = next(iter(items))
|
||||
product_item = self._controller.get_product_item(
|
||||
project_name,
|
||||
first_item["product_id"],
|
||||
)
|
||||
version_id = first_item["version_id"]
|
||||
version_item = None
|
||||
if product_item is not None:
|
||||
version_item = product_item.version_items.get(version_id)
|
||||
|
||||
self._info_text_widget.set_current_item(product_item, version_item)
|
||||
45
openpype/tools/ayon_loader/ui/product_group_dialog.py
Normal file
45
openpype/tools/ayon_loader/ui/product_group_dialog.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
from qtpy import QtWidgets
|
||||
|
||||
from openpype.tools.utils import PlaceholderLineEdit
|
||||
|
||||
|
||||
class ProductGroupDialog(QtWidgets.QDialog):
|
||||
def __init__(self, controller, parent):
|
||||
super(ProductGroupDialog, self).__init__(parent)
|
||||
self.setWindowTitle("Grouping products")
|
||||
self.setMinimumWidth(250)
|
||||
self.setModal(True)
|
||||
|
||||
main_label = QtWidgets.QLabel("Group Name", self)
|
||||
|
||||
group_name_input = PlaceholderLineEdit(self)
|
||||
group_name_input.setPlaceholderText("Remain blank to ungroup..")
|
||||
|
||||
group_btn = QtWidgets.QPushButton("Apply", self)
|
||||
group_btn.setAutoDefault(True)
|
||||
group_btn.setDefault(True)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(main_label, 0)
|
||||
layout.addWidget(group_name_input, 0)
|
||||
layout.addWidget(group_btn, 0)
|
||||
|
||||
group_btn.clicked.connect(self._on_apply_click)
|
||||
|
||||
self._project_name = None
|
||||
self._product_ids = set()
|
||||
|
||||
self._controller = controller
|
||||
self._group_btn = group_btn
|
||||
self._group_name_input = group_name_input
|
||||
|
||||
def set_product_ids(self, project_name, product_ids):
|
||||
self._project_name = project_name
|
||||
self._product_ids = product_ids
|
||||
|
||||
def _on_apply_click(self):
|
||||
group_name = self._group_name_input.text().strip() or None
|
||||
self._controller.change_products_group(
|
||||
self._project_name, self._product_ids, group_name
|
||||
)
|
||||
self.close()
|
||||
220
openpype/tools/ayon_loader/ui/product_types_widget.py
Normal file
220
openpype/tools/ayon_loader/ui/product_types_widget.py
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
from qtpy import QtWidgets, QtGui, QtCore
|
||||
|
||||
from openpype.tools.ayon_utils.widgets import get_qt_icon
|
||||
|
||||
PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 1
|
||||
|
||||
|
||||
class ProductTypesQtModel(QtGui.QStandardItemModel):
|
||||
refreshed = QtCore.Signal()
|
||||
filter_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller):
|
||||
super(ProductTypesQtModel, self).__init__()
|
||||
self._controller = controller
|
||||
|
||||
self._refreshing = False
|
||||
self._bulk_change = False
|
||||
self._items_by_name = {}
|
||||
|
||||
def is_refreshing(self):
|
||||
return self._refreshing
|
||||
|
||||
def get_filter_info(self):
|
||||
"""Product types filtering info.
|
||||
|
||||
Returns:
|
||||
dict[str, bool]: Filtering value by product type name. False value
|
||||
means to hide product type.
|
||||
"""
|
||||
|
||||
return {
|
||||
name: item.checkState() == QtCore.Qt.Checked
|
||||
for name, item in self._items_by_name.items()
|
||||
}
|
||||
|
||||
def refresh(self, project_name):
|
||||
self._refreshing = True
|
||||
product_type_items = self._controller.get_product_type_items(
|
||||
project_name)
|
||||
|
||||
items_to_remove = set(self._items_by_name.keys())
|
||||
new_items = []
|
||||
for product_type_item in product_type_items:
|
||||
name = product_type_item.name
|
||||
items_to_remove.discard(name)
|
||||
item = self._items_by_name.get(product_type_item.name)
|
||||
if item is None:
|
||||
item = QtGui.QStandardItem(name)
|
||||
item.setData(name, PRODUCT_TYPE_ROLE)
|
||||
item.setEditable(False)
|
||||
item.setCheckable(True)
|
||||
new_items.append(item)
|
||||
self._items_by_name[name] = item
|
||||
|
||||
item.setCheckState(
|
||||
QtCore.Qt.Checked
|
||||
if product_type_item.checked
|
||||
else QtCore.Qt.Unchecked
|
||||
)
|
||||
icon = get_qt_icon(product_type_item.icon)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
|
||||
root_item = self.invisibleRootItem()
|
||||
if new_items:
|
||||
root_item.appendRows(new_items)
|
||||
|
||||
for name in items_to_remove:
|
||||
item = self._items_by_name.pop(name)
|
||||
root_item.removeRow(item.row())
|
||||
|
||||
self._refreshing = False
|
||||
self.refreshed.emit()
|
||||
|
||||
def setData(self, index, value, role=None):
|
||||
checkstate_changed = False
|
||||
if role is None:
|
||||
role = QtCore.Qt.EditRole
|
||||
elif role == QtCore.Qt.CheckStateRole:
|
||||
checkstate_changed = True
|
||||
output = super(ProductTypesQtModel, self).setData(index, value, role)
|
||||
if checkstate_changed and not self._bulk_change:
|
||||
self.filter_changed.emit()
|
||||
return output
|
||||
|
||||
def change_state_for_all(self, checked):
|
||||
if self._items_by_name:
|
||||
self.change_states(checked, self._items_by_name.keys())
|
||||
|
||||
def change_states(self, checked, product_types):
|
||||
product_types = set(product_types)
|
||||
if not product_types:
|
||||
return
|
||||
|
||||
if checked is None:
|
||||
state = None
|
||||
elif checked:
|
||||
state = QtCore.Qt.Checked
|
||||
else:
|
||||
state = QtCore.Qt.Unchecked
|
||||
|
||||
self._bulk_change = True
|
||||
|
||||
changed = False
|
||||
for product_type in product_types:
|
||||
item = self._items_by_name.get(product_type)
|
||||
if item is None:
|
||||
continue
|
||||
new_state = state
|
||||
item_checkstate = item.checkState()
|
||||
if new_state is None:
|
||||
if item_checkstate == QtCore.Qt.Checked:
|
||||
new_state = QtCore.Qt.Unchecked
|
||||
else:
|
||||
new_state = QtCore.Qt.Checked
|
||||
elif item_checkstate == new_state:
|
||||
continue
|
||||
changed = True
|
||||
item.setCheckState(new_state)
|
||||
|
||||
self._bulk_change = False
|
||||
|
||||
if changed:
|
||||
self.filter_changed.emit()
|
||||
|
||||
|
||||
class ProductTypesView(QtWidgets.QListView):
|
||||
filter_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(ProductTypesView, self).__init__(parent)
|
||||
|
||||
self.setSelectionMode(
|
||||
QtWidgets.QAbstractItemView.ExtendedSelection
|
||||
)
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
|
||||
product_types_model = ProductTypesQtModel(controller)
|
||||
product_types_proxy_model = QtCore.QSortFilterProxyModel()
|
||||
product_types_proxy_model.setSourceModel(product_types_model)
|
||||
|
||||
self.setModel(product_types_proxy_model)
|
||||
|
||||
product_types_model.refreshed.connect(self._on_refresh_finished)
|
||||
product_types_model.filter_changed.connect(self._on_filter_change)
|
||||
self.customContextMenuRequested.connect(self._on_context_menu)
|
||||
|
||||
controller.register_event_callback(
|
||||
"selection.project.changed",
|
||||
self._on_project_change
|
||||
)
|
||||
|
||||
self._controller = controller
|
||||
|
||||
self._product_types_model = product_types_model
|
||||
self._product_types_proxy_model = product_types_proxy_model
|
||||
|
||||
def get_filter_info(self):
|
||||
return self._product_types_model.get_filter_info()
|
||||
|
||||
def _on_project_change(self, event):
|
||||
project_name = event["project_name"]
|
||||
self._product_types_model.refresh(project_name)
|
||||
|
||||
def _on_refresh_finished(self):
|
||||
self.filter_changed.emit()
|
||||
|
||||
def _on_filter_change(self):
|
||||
if not self._product_types_model.is_refreshing():
|
||||
self.filter_changed.emit()
|
||||
|
||||
def _change_selection_state(self, checkstate):
|
||||
selection_model = self.selectionModel()
|
||||
product_types = {
|
||||
index.data(PRODUCT_TYPE_ROLE)
|
||||
for index in selection_model.selectedIndexes()
|
||||
}
|
||||
product_types.discard(None)
|
||||
self._product_types_model.change_states(checkstate, product_types)
|
||||
|
||||
def _on_enable_all(self):
|
||||
self._product_types_model.change_state_for_all(True)
|
||||
|
||||
def _on_disable_all(self):
|
||||
self._product_types_model.change_state_for_all(False)
|
||||
|
||||
def _on_context_menu(self, pos):
|
||||
menu = QtWidgets.QMenu(self)
|
||||
|
||||
# Add enable all action
|
||||
action_check_all = QtWidgets.QAction(menu)
|
||||
action_check_all.setText("Enable All")
|
||||
action_check_all.triggered.connect(self._on_enable_all)
|
||||
# Add disable all action
|
||||
action_uncheck_all = QtWidgets.QAction(menu)
|
||||
action_uncheck_all.setText("Disable All")
|
||||
action_uncheck_all.triggered.connect(self._on_disable_all)
|
||||
|
||||
menu.addAction(action_check_all)
|
||||
menu.addAction(action_uncheck_all)
|
||||
|
||||
# Get mouse position
|
||||
global_pos = self.viewport().mapToGlobal(pos)
|
||||
menu.exec_(global_pos)
|
||||
|
||||
def event(self, event):
|
||||
if event.type() == QtCore.QEvent.KeyPress:
|
||||
if event.key() == QtCore.Qt.Key_Space:
|
||||
self._change_selection_state(None)
|
||||
return True
|
||||
|
||||
if event.key() == QtCore.Qt.Key_Backspace:
|
||||
self._change_selection_state(False)
|
||||
return True
|
||||
|
||||
if event.key() == QtCore.Qt.Key_Return:
|
||||
self._change_selection_state(True)
|
||||
return True
|
||||
|
||||
return super(ProductTypesView, self).event(event)
|
||||
191
openpype/tools/ayon_loader/ui/products_delegates.py
Normal file
191
openpype/tools/ayon_loader/ui/products_delegates.py
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import numbers
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype.tools.utils.lib import format_version
|
||||
|
||||
from .products_model import (
|
||||
PRODUCT_ID_ROLE,
|
||||
VERSION_NAME_EDIT_ROLE,
|
||||
VERSION_ID_ROLE,
|
||||
PRODUCT_IN_SCENE_ROLE,
|
||||
)
|
||||
|
||||
|
||||
class VersionComboBox(QtWidgets.QComboBox):
|
||||
value_changed = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, product_id, parent):
|
||||
super(VersionComboBox, self).__init__(parent)
|
||||
self._product_id = product_id
|
||||
self._items_by_id = {}
|
||||
|
||||
self._current_id = None
|
||||
|
||||
self.currentIndexChanged.connect(self._on_index_change)
|
||||
|
||||
def update_versions(self, version_items, current_version_id):
|
||||
model = self.model()
|
||||
root_item = model.invisibleRootItem()
|
||||
version_items = list(reversed(version_items))
|
||||
version_ids = [
|
||||
version_item.version_id
|
||||
for version_item in version_items
|
||||
]
|
||||
if current_version_id not in version_ids and version_ids:
|
||||
current_version_id = version_ids[0]
|
||||
self._current_id = current_version_id
|
||||
|
||||
to_remove = set(self._items_by_id.keys()) - set(version_ids)
|
||||
for item_id in to_remove:
|
||||
item = self._items_by_id.pop(item_id)
|
||||
root_item.removeRow(item.row())
|
||||
|
||||
for idx, version_item in enumerate(version_items):
|
||||
version_id = version_item.version_id
|
||||
|
||||
item = self._items_by_id.get(version_id)
|
||||
if item is None:
|
||||
label = format_version(
|
||||
abs(version_item.version), version_item.is_hero
|
||||
)
|
||||
item = QtGui.QStandardItem(label)
|
||||
item.setData(version_id, QtCore.Qt.UserRole)
|
||||
self._items_by_id[version_id] = item
|
||||
|
||||
if item.row() != idx:
|
||||
root_item.insertRow(idx, item)
|
||||
|
||||
index = version_ids.index(current_version_id)
|
||||
if self.currentIndex() != index:
|
||||
self.setCurrentIndex(index)
|
||||
|
||||
def _on_index_change(self):
|
||||
idx = self.currentIndex()
|
||||
value = self.itemData(idx)
|
||||
if value == self._current_id:
|
||||
return
|
||||
self._current_id = value
|
||||
self.value_changed.emit(self._product_id)
|
||||
|
||||
|
||||
class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""A delegate that display version integer formatted as version string."""
|
||||
|
||||
version_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(VersionDelegate, self).__init__(*args, **kwargs)
|
||||
self._editor_by_product_id = {}
|
||||
|
||||
def displayText(self, value, locale):
|
||||
if not isinstance(value, numbers.Integral):
|
||||
return "N/A"
|
||||
return format_version(abs(value), value < 0)
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
fg_color = index.data(QtCore.Qt.ForegroundRole)
|
||||
if fg_color:
|
||||
if isinstance(fg_color, QtGui.QBrush):
|
||||
fg_color = fg_color.color()
|
||||
elif isinstance(fg_color, QtGui.QColor):
|
||||
pass
|
||||
else:
|
||||
fg_color = None
|
||||
|
||||
if not fg_color:
|
||||
return super(VersionDelegate, self).paint(painter, option, index)
|
||||
|
||||
if option.widget:
|
||||
style = option.widget.style()
|
||||
else:
|
||||
style = QtWidgets.QApplication.style()
|
||||
|
||||
style.drawControl(
|
||||
style.CE_ItemViewItem, option, painter, option.widget
|
||||
)
|
||||
|
||||
painter.save()
|
||||
|
||||
text = self.displayText(
|
||||
index.data(QtCore.Qt.DisplayRole), option.locale
|
||||
)
|
||||
pen = painter.pen()
|
||||
pen.setColor(fg_color)
|
||||
painter.setPen(pen)
|
||||
|
||||
text_rect = style.subElementRect(style.SE_ItemViewItemText, option)
|
||||
text_margin = style.proxy().pixelMetric(
|
||||
style.PM_FocusFrameHMargin, option, option.widget
|
||||
) + 1
|
||||
|
||||
painter.drawText(
|
||||
text_rect.adjusted(text_margin, 0, - text_margin, 0),
|
||||
option.displayAlignment,
|
||||
text
|
||||
)
|
||||
|
||||
painter.restore()
|
||||
|
||||
def createEditor(self, parent, option, index):
|
||||
product_id = index.data(PRODUCT_ID_ROLE)
|
||||
if not product_id:
|
||||
return
|
||||
|
||||
editor = VersionComboBox(product_id, parent)
|
||||
self._editor_by_product_id[product_id] = editor
|
||||
editor.value_changed.connect(self._on_editor_change)
|
||||
|
||||
return editor
|
||||
|
||||
def _on_editor_change(self, product_id):
|
||||
editor = self._editor_by_product_id[product_id]
|
||||
|
||||
# Update model data
|
||||
self.commitData.emit(editor)
|
||||
# Display model data
|
||||
self.version_changed.emit()
|
||||
|
||||
def setEditorData(self, editor, index):
|
||||
editor.clear()
|
||||
|
||||
# Current value of the index
|
||||
versions = index.data(VERSION_NAME_EDIT_ROLE) or []
|
||||
version_id = index.data(VERSION_ID_ROLE)
|
||||
editor.update_versions(versions, version_id)
|
||||
|
||||
def setModelData(self, editor, model, index):
|
||||
"""Apply the integer version back in the model"""
|
||||
|
||||
version_id = editor.itemData(editor.currentIndex())
|
||||
model.setData(index, version_id, VERSION_NAME_EDIT_ROLE)
|
||||
|
||||
|
||||
class LoadedInSceneDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""Delegate for Loaded in Scene state columns.
|
||||
|
||||
Shows "Yes" or "No" for 1 or 0 values, or "N/A" for other values.
|
||||
Colorizes green or dark grey based on values.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LoadedInSceneDelegate, self).__init__(*args, **kwargs)
|
||||
self._colors = {
|
||||
1: QtGui.QColor(80, 170, 80),
|
||||
0: QtGui.QColor(90, 90, 90),
|
||||
}
|
||||
self._default_color = QtGui.QColor(90, 90, 90)
|
||||
|
||||
def displayText(self, value, locale):
|
||||
if value == 0:
|
||||
return "No"
|
||||
elif value == 1:
|
||||
return "Yes"
|
||||
return "N/A"
|
||||
|
||||
def initStyleOption(self, option, index):
|
||||
super(LoadedInSceneDelegate, self).initStyleOption(option, index)
|
||||
|
||||
# Colorize based on value
|
||||
value = index.data(PRODUCT_IN_SCENE_ROLE)
|
||||
color = self._colors.get(value, self._default_color)
|
||||
option.palette.setBrush(QtGui.QPalette.Text, color)
|
||||
590
openpype/tools/ayon_loader/ui/products_model.py
Normal file
590
openpype/tools/ayon_loader/ui/products_model.py
Normal file
|
|
@ -0,0 +1,590 @@
|
|||
import collections
|
||||
|
||||
import qtawesome
|
||||
from qtpy import QtGui, QtCore
|
||||
|
||||
from openpype.style import get_default_entity_icon_color
|
||||
from openpype.tools.ayon_utils.widgets import get_qt_icon
|
||||
|
||||
PRODUCTS_MODEL_SENDER_NAME = "qt_products_model"
|
||||
|
||||
GROUP_TYPE_ROLE = QtCore.Qt.UserRole + 1
|
||||
MERGED_COLOR_ROLE = QtCore.Qt.UserRole + 2
|
||||
FOLDER_LABEL_ROLE = QtCore.Qt.UserRole + 3
|
||||
FOLDER_ID_ROLE = QtCore.Qt.UserRole + 4
|
||||
PRODUCT_ID_ROLE = QtCore.Qt.UserRole + 5
|
||||
PRODUCT_NAME_ROLE = QtCore.Qt.UserRole + 6
|
||||
PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 7
|
||||
PRODUCT_TYPE_ICON_ROLE = QtCore.Qt.UserRole + 8
|
||||
PRODUCT_IN_SCENE_ROLE = QtCore.Qt.UserRole + 9
|
||||
VERSION_ID_ROLE = QtCore.Qt.UserRole + 10
|
||||
VERSION_HERO_ROLE = QtCore.Qt.UserRole + 11
|
||||
VERSION_NAME_ROLE = QtCore.Qt.UserRole + 12
|
||||
VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 13
|
||||
VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 14
|
||||
VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 15
|
||||
VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 16
|
||||
VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 17
|
||||
VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 18
|
||||
VERSION_STEP_ROLE = QtCore.Qt.UserRole + 19
|
||||
VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 20
|
||||
VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 21
|
||||
|
||||
|
||||
class ProductsModel(QtGui.QStandardItemModel):
|
||||
refreshed = QtCore.Signal()
|
||||
version_changed = QtCore.Signal()
|
||||
column_labels = [
|
||||
"Product name",
|
||||
"Product type",
|
||||
"Folder",
|
||||
"Version",
|
||||
"Time",
|
||||
"Author",
|
||||
"Frames",
|
||||
"Duration",
|
||||
"Handles",
|
||||
"Step",
|
||||
"In scene",
|
||||
"Availability",
|
||||
]
|
||||
merged_items_colors = [
|
||||
("#{0:02x}{1:02x}{2:02x}".format(*c), QtGui.QColor(*c))
|
||||
for c in [
|
||||
(55, 161, 222), # Light Blue
|
||||
(231, 176, 0), # Yellow
|
||||
(154, 13, 255), # Purple
|
||||
(130, 184, 30), # Light Green
|
||||
(211, 79, 63), # Light Red
|
||||
(179, 181, 182), # Grey
|
||||
(194, 57, 179), # Pink
|
||||
(0, 120, 215), # Dark Blue
|
||||
(0, 204, 106), # Dark Green
|
||||
(247, 99, 12), # Orange
|
||||
]
|
||||
]
|
||||
|
||||
version_col = column_labels.index("Version")
|
||||
published_time_col = column_labels.index("Time")
|
||||
folders_label_col = column_labels.index("Folder")
|
||||
in_scene_col = column_labels.index("In scene")
|
||||
|
||||
def __init__(self, controller):
|
||||
super(ProductsModel, self).__init__()
|
||||
self.setColumnCount(len(self.column_labels))
|
||||
for idx, label in enumerate(self.column_labels):
|
||||
self.setHeaderData(idx, QtCore.Qt.Horizontal, label)
|
||||
self._controller = controller
|
||||
|
||||
# Variables to store 'QStandardItem'
|
||||
self._items_by_id = {}
|
||||
self._group_items_by_name = {}
|
||||
self._merged_items_by_id = {}
|
||||
|
||||
# product item objects (they have version information)
|
||||
self._product_items_by_id = {}
|
||||
self._grouping_enabled = True
|
||||
self._reset_merge_color = False
|
||||
self._color_iterator = self._color_iter()
|
||||
self._group_icon = None
|
||||
|
||||
self._last_project_name = None
|
||||
self._last_folder_ids = []
|
||||
|
||||
def get_product_item_indexes(self):
|
||||
return [
|
||||
item.index()
|
||||
for item in self._items_by_id.values()
|
||||
]
|
||||
|
||||
def get_product_item_by_id(self, product_id):
|
||||
"""
|
||||
|
||||
Args:
|
||||
product_id (str): Product id.
|
||||
|
||||
Returns:
|
||||
Union[ProductItem, None]: Product item with version information.
|
||||
"""
|
||||
|
||||
return self._product_items_by_id.get(product_id)
|
||||
|
||||
def set_enable_grouping(self, enable_grouping):
|
||||
if enable_grouping is self._grouping_enabled:
|
||||
return
|
||||
self._grouping_enabled = enable_grouping
|
||||
# Ignore change if groups are not available
|
||||
self.refresh(self._last_project_name, self._last_folder_ids)
|
||||
|
||||
def flags(self, index):
|
||||
# Make the version column editable
|
||||
if index.column() == self.version_col and index.data(PRODUCT_ID_ROLE):
|
||||
return (
|
||||
QtCore.Qt.ItemIsEnabled
|
||||
| QtCore.Qt.ItemIsSelectable
|
||||
| QtCore.Qt.ItemIsEditable
|
||||
)
|
||||
if index.column() != 0:
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
return super(ProductsModel, self).flags(index)
|
||||
|
||||
def data(self, index, role=None):
|
||||
if role is None:
|
||||
role = QtCore.Qt.DisplayRole
|
||||
|
||||
if not index.isValid():
|
||||
return None
|
||||
|
||||
col = index.column()
|
||||
if col == 0:
|
||||
return super(ProductsModel, self).data(index, role)
|
||||
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
if col == 1:
|
||||
role = PRODUCT_TYPE_ICON_ROLE
|
||||
else:
|
||||
return None
|
||||
|
||||
if (
|
||||
role == VERSION_NAME_EDIT_ROLE
|
||||
or (role == QtCore.Qt.EditRole and col == self.version_col)
|
||||
):
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
product_id = index.data(PRODUCT_ID_ROLE)
|
||||
product_item = self._product_items_by_id.get(product_id)
|
||||
if product_item is None:
|
||||
return None
|
||||
return list(product_item.version_items.values())
|
||||
|
||||
if role == QtCore.Qt.EditRole:
|
||||
return None
|
||||
|
||||
if role == QtCore.Qt.DisplayRole:
|
||||
if not index.data(PRODUCT_ID_ROLE):
|
||||
return None
|
||||
if col == self.version_col:
|
||||
role = VERSION_NAME_ROLE
|
||||
elif col == 1:
|
||||
role = PRODUCT_TYPE_ROLE
|
||||
elif col == 2:
|
||||
role = FOLDER_LABEL_ROLE
|
||||
elif col == 4:
|
||||
role = VERSION_PUBLISH_TIME_ROLE
|
||||
elif col == 5:
|
||||
role = VERSION_AUTHOR_ROLE
|
||||
elif col == 6:
|
||||
role = VERSION_FRAME_RANGE_ROLE
|
||||
elif col == 7:
|
||||
role = VERSION_DURATION_ROLE
|
||||
elif col == 8:
|
||||
role = VERSION_HANDLES_ROLE
|
||||
elif col == 9:
|
||||
role = VERSION_STEP_ROLE
|
||||
elif col == 10:
|
||||
role = PRODUCT_IN_SCENE_ROLE
|
||||
elif col == 11:
|
||||
role = VERSION_AVAILABLE_ROLE
|
||||
else:
|
||||
return None
|
||||
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
|
||||
return super(ProductsModel, self).data(index, role)
|
||||
|
||||
def setData(self, index, value, role=None):
|
||||
if not index.isValid():
|
||||
return False
|
||||
|
||||
if role is None:
|
||||
role = QtCore.Qt.EditRole
|
||||
|
||||
col = index.column()
|
||||
if col == self.version_col and role == QtCore.Qt.EditRole:
|
||||
role = VERSION_NAME_EDIT_ROLE
|
||||
|
||||
if role == VERSION_NAME_EDIT_ROLE:
|
||||
if col != 0:
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
product_id = index.data(PRODUCT_ID_ROLE)
|
||||
product_item = self._product_items_by_id[product_id]
|
||||
final_version_item = None
|
||||
for v_id, version_item in product_item.version_items.items():
|
||||
if v_id == value:
|
||||
final_version_item = version_item
|
||||
break
|
||||
|
||||
if final_version_item is None:
|
||||
return False
|
||||
if index.data(VERSION_ID_ROLE) == final_version_item.version_id:
|
||||
return True
|
||||
item = self.itemFromIndex(index)
|
||||
self._set_version_data_to_product_item(item, final_version_item)
|
||||
self.version_changed.emit()
|
||||
return True
|
||||
return super(ProductsModel, self).setData(index, value, role)
|
||||
|
||||
def _get_next_color(self):
|
||||
return next(self._color_iterator)
|
||||
|
||||
def _color_iter(self):
|
||||
while True:
|
||||
for color in self.merged_items_colors:
|
||||
if self._reset_merge_color:
|
||||
self._reset_merge_color = False
|
||||
break
|
||||
yield color
|
||||
|
||||
def _clear(self):
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.removeRows(0, root_item.rowCount())
|
||||
|
||||
self._items_by_id = {}
|
||||
self._group_items_by_name = {}
|
||||
self._merged_items_by_id = {}
|
||||
self._product_items_by_id = {}
|
||||
self._reset_merge_color = True
|
||||
|
||||
def _get_group_icon(self):
|
||||
if self._group_icon is None:
|
||||
self._group_icon = qtawesome.icon(
|
||||
"fa.object-group",
|
||||
color=get_default_entity_icon_color()
|
||||
)
|
||||
return self._group_icon
|
||||
|
||||
def _get_group_model_item(self, group_name):
|
||||
model_item = self._group_items_by_name.get(group_name)
|
||||
if model_item is None:
|
||||
model_item = QtGui.QStandardItem(group_name)
|
||||
model_item.setData(
|
||||
self._get_group_icon(), QtCore.Qt.DecorationRole
|
||||
)
|
||||
model_item.setData(0, GROUP_TYPE_ROLE)
|
||||
model_item.setEditable(False)
|
||||
model_item.setColumnCount(self.columnCount())
|
||||
self._group_items_by_name[group_name] = model_item
|
||||
return model_item
|
||||
|
||||
def _get_merged_model_item(self, path, count, hex_color):
|
||||
model_item = self._merged_items_by_id.get(path)
|
||||
if model_item is None:
|
||||
model_item = QtGui.QStandardItem()
|
||||
model_item.setData(1, GROUP_TYPE_ROLE)
|
||||
model_item.setData(hex_color, MERGED_COLOR_ROLE)
|
||||
model_item.setEditable(False)
|
||||
model_item.setColumnCount(self.columnCount())
|
||||
self._merged_items_by_id[path] = model_item
|
||||
label = "{} ({})".format(path, count)
|
||||
model_item.setData(label, QtCore.Qt.DisplayRole)
|
||||
return model_item
|
||||
|
||||
def _set_version_data_to_product_item(self, model_item, version_item):
|
||||
"""
|
||||
|
||||
Args:
|
||||
model_item (QtGui.QStandardItem): Item which should have values
|
||||
from version item.
|
||||
version_item (VersionItem): Item from entities model with
|
||||
information about version.
|
||||
"""
|
||||
|
||||
model_item.setData(version_item.version_id, VERSION_ID_ROLE)
|
||||
model_item.setData(version_item.version, VERSION_NAME_ROLE)
|
||||
model_item.setData(version_item.version_id, VERSION_ID_ROLE)
|
||||
model_item.setData(version_item.is_hero, VERSION_HERO_ROLE)
|
||||
model_item.setData(
|
||||
version_item.published_time, VERSION_PUBLISH_TIME_ROLE
|
||||
)
|
||||
model_item.setData(version_item.author, VERSION_AUTHOR_ROLE)
|
||||
model_item.setData(version_item.frame_range, VERSION_FRAME_RANGE_ROLE)
|
||||
model_item.setData(version_item.duration, VERSION_DURATION_ROLE)
|
||||
model_item.setData(version_item.handles, VERSION_HANDLES_ROLE)
|
||||
model_item.setData(version_item.step, VERSION_STEP_ROLE)
|
||||
model_item.setData(
|
||||
version_item.thumbnail_id, VERSION_THUMBNAIL_ID_ROLE)
|
||||
|
||||
def _get_product_model_item(self, product_item):
|
||||
model_item = self._items_by_id.get(product_item.product_id)
|
||||
versions = list(product_item.version_items.values())
|
||||
versions.sort()
|
||||
last_version = versions[-1]
|
||||
if model_item is None:
|
||||
product_id = product_item.product_id
|
||||
model_item = QtGui.QStandardItem(product_item.product_name)
|
||||
model_item.setEditable(False)
|
||||
icon = get_qt_icon(product_item.product_icon)
|
||||
product_type_icon = get_qt_icon(product_item.product_type_icon)
|
||||
model_item.setColumnCount(self.columnCount())
|
||||
model_item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
model_item.setData(product_id, PRODUCT_ID_ROLE)
|
||||
model_item.setData(product_item.product_name, PRODUCT_NAME_ROLE)
|
||||
model_item.setData(product_item.product_type, PRODUCT_TYPE_ROLE)
|
||||
model_item.setData(product_type_icon, PRODUCT_TYPE_ICON_ROLE)
|
||||
model_item.setData(product_item.folder_id, FOLDER_ID_ROLE)
|
||||
|
||||
self._product_items_by_id[product_id] = product_item
|
||||
self._items_by_id[product_id] = model_item
|
||||
|
||||
model_item.setData(product_item.folder_label, FOLDER_LABEL_ROLE)
|
||||
in_scene = 1 if product_item.product_in_scene else 0
|
||||
model_item.setData(in_scene, PRODUCT_IN_SCENE_ROLE)
|
||||
|
||||
self._set_version_data_to_product_item(model_item, last_version)
|
||||
return model_item
|
||||
|
||||
def get_last_project_name(self):
|
||||
return self._last_project_name
|
||||
|
||||
def refresh(self, project_name, folder_ids):
|
||||
self._clear()
|
||||
|
||||
self._last_project_name = project_name
|
||||
self._last_folder_ids = folder_ids
|
||||
|
||||
product_items = self._controller.get_product_items(
|
||||
project_name,
|
||||
folder_ids,
|
||||
sender=PRODUCTS_MODEL_SENDER_NAME
|
||||
)
|
||||
product_items_by_id = {
|
||||
product_item.product_id: product_item
|
||||
for product_item in product_items
|
||||
}
|
||||
|
||||
# Prepare product groups
|
||||
product_name_matches_by_group = collections.defaultdict(dict)
|
||||
for product_item in product_items_by_id.values():
|
||||
group_name = None
|
||||
if self._grouping_enabled:
|
||||
group_name = product_item.group_name
|
||||
|
||||
product_name = product_item.product_name
|
||||
group = product_name_matches_by_group[group_name]
|
||||
if product_name not in group:
|
||||
group[product_name] = [product_item]
|
||||
continue
|
||||
group[product_name].append(product_item)
|
||||
|
||||
group_names = set(product_name_matches_by_group.keys())
|
||||
|
||||
root_item = self.invisibleRootItem()
|
||||
new_root_items = []
|
||||
merged_paths = set()
|
||||
for group_name in group_names:
|
||||
key_parts = []
|
||||
if group_name:
|
||||
key_parts.append(group_name)
|
||||
|
||||
groups = product_name_matches_by_group[group_name]
|
||||
merged_product_items = {}
|
||||
top_items = []
|
||||
group_product_types = set()
|
||||
for product_name, product_items in groups.items():
|
||||
group_product_types |= {p.product_type for p in product_items}
|
||||
if len(product_items) == 1:
|
||||
top_items.append(product_items[0])
|
||||
else:
|
||||
path = "/".join(key_parts + [product_name])
|
||||
merged_paths.add(path)
|
||||
merged_product_items[path] = (
|
||||
product_name,
|
||||
product_items,
|
||||
)
|
||||
|
||||
parent_item = None
|
||||
if group_name:
|
||||
parent_item = self._get_group_model_item(group_name)
|
||||
parent_item.setData(
|
||||
"|".join(group_product_types), PRODUCT_TYPE_ROLE)
|
||||
|
||||
new_items = []
|
||||
if parent_item is not None and parent_item.row() < 0:
|
||||
new_root_items.append(parent_item)
|
||||
|
||||
for product_item in top_items:
|
||||
item = self._get_product_model_item(product_item)
|
||||
new_items.append(item)
|
||||
|
||||
for path_info in merged_product_items.values():
|
||||
product_name, product_items = path_info
|
||||
(merged_color_hex, merged_color_qt) = self._get_next_color()
|
||||
merged_color = qtawesome.icon(
|
||||
"fa.circle", color=merged_color_qt)
|
||||
merged_item = self._get_merged_model_item(
|
||||
product_name, len(product_items), merged_color_hex)
|
||||
merged_item.setData(merged_color, QtCore.Qt.DecorationRole)
|
||||
new_items.append(merged_item)
|
||||
|
||||
merged_product_types = set()
|
||||
new_merged_items = []
|
||||
for product_item in product_items:
|
||||
item = self._get_product_model_item(product_item)
|
||||
new_merged_items.append(item)
|
||||
merged_product_types.add(product_item.product_type)
|
||||
|
||||
merged_item.setData(
|
||||
"|".join(merged_product_types), PRODUCT_TYPE_ROLE)
|
||||
if new_merged_items:
|
||||
merged_item.appendRows(new_merged_items)
|
||||
|
||||
if not new_items:
|
||||
continue
|
||||
|
||||
if parent_item is None:
|
||||
new_root_items.extend(new_items)
|
||||
else:
|
||||
parent_item.appendRows(new_items)
|
||||
|
||||
if new_root_items:
|
||||
root_item.appendRows(new_root_items)
|
||||
|
||||
self.refreshed.emit()
|
||||
# ---------------------------------
|
||||
# This implementation does not call '_clear' at the start
|
||||
# but is more complex and probably slower
|
||||
# ---------------------------------
|
||||
# def _remove_items(self, items):
|
||||
# if not items:
|
||||
# return
|
||||
# root_item = self.invisibleRootItem()
|
||||
# for item in items:
|
||||
# row = item.row()
|
||||
# if row < 0:
|
||||
# continue
|
||||
# parent = item.parent()
|
||||
# if parent is None:
|
||||
# parent = root_item
|
||||
# parent.removeRow(row)
|
||||
#
|
||||
# def _remove_group_items(self, group_names):
|
||||
# group_items = [
|
||||
# self._group_items_by_name.pop(group_name)
|
||||
# for group_name in group_names
|
||||
# ]
|
||||
# self._remove_items(group_items)
|
||||
#
|
||||
# def _remove_merged_items(self, paths):
|
||||
# merged_items = [
|
||||
# self._merged_items_by_id.pop(path)
|
||||
# for path in paths
|
||||
# ]
|
||||
# self._remove_items(merged_items)
|
||||
#
|
||||
# def _remove_product_items(self, product_ids):
|
||||
# product_items = []
|
||||
# for product_id in product_ids:
|
||||
# self._product_items_by_id.pop(product_id)
|
||||
# product_items.append(self._items_by_id.pop(product_id))
|
||||
# self._remove_items(product_items)
|
||||
#
|
||||
# def _add_to_new_items(self, item, parent_item, new_items, root_item):
|
||||
# if item.row() < 0:
|
||||
# new_items.append(item)
|
||||
# else:
|
||||
# item_parent = item.parent()
|
||||
# if item_parent is not parent_item:
|
||||
# if item_parent is None:
|
||||
# item_parent = root_item
|
||||
# item_parent.takeRow(item.row())
|
||||
# new_items.append(item)
|
||||
|
||||
# def refresh(self, project_name, folder_ids):
|
||||
# product_items = self._controller.get_product_items(
|
||||
# project_name,
|
||||
# folder_ids,
|
||||
# sender=PRODUCTS_MODEL_SENDER_NAME
|
||||
# )
|
||||
# product_items_by_id = {
|
||||
# product_item.product_id: product_item
|
||||
# for product_item in product_items
|
||||
# }
|
||||
# # Remove product items that are not available
|
||||
# product_ids_to_remove = (
|
||||
# set(self._items_by_id.keys()) - set(product_items_by_id.keys())
|
||||
# )
|
||||
# self._remove_product_items(product_ids_to_remove)
|
||||
#
|
||||
# # Prepare product groups
|
||||
# product_name_matches_by_group = collections.defaultdict(dict)
|
||||
# for product_item in product_items_by_id.values():
|
||||
# group_name = None
|
||||
# if self._grouping_enabled:
|
||||
# group_name = product_item.group_name
|
||||
#
|
||||
# product_name = product_item.product_name
|
||||
# group = product_name_matches_by_group[group_name]
|
||||
# if product_name not in group:
|
||||
# group[product_name] = [product_item]
|
||||
# continue
|
||||
# group[product_name].append(product_item)
|
||||
#
|
||||
# group_names = set(product_name_matches_by_group.keys())
|
||||
#
|
||||
# root_item = self.invisibleRootItem()
|
||||
# new_root_items = []
|
||||
# merged_paths = set()
|
||||
# for group_name in group_names:
|
||||
# key_parts = []
|
||||
# if group_name:
|
||||
# key_parts.append(group_name)
|
||||
#
|
||||
# groups = product_name_matches_by_group[group_name]
|
||||
# merged_product_items = {}
|
||||
# top_items = []
|
||||
# for product_name, product_items in groups.items():
|
||||
# if len(product_items) == 1:
|
||||
# top_items.append(product_items[0])
|
||||
# else:
|
||||
# path = "/".join(key_parts + [product_name])
|
||||
# merged_paths.add(path)
|
||||
# merged_product_items[path] = product_items
|
||||
#
|
||||
# parent_item = None
|
||||
# if group_name:
|
||||
# parent_item = self._get_group_model_item(group_name)
|
||||
#
|
||||
# new_items = []
|
||||
# if parent_item is not None and parent_item.row() < 0:
|
||||
# new_root_items.append(parent_item)
|
||||
#
|
||||
# for product_item in top_items:
|
||||
# item = self._get_product_model_item(product_item)
|
||||
# self._add_to_new_items(
|
||||
# item, parent_item, new_items, root_item
|
||||
# )
|
||||
#
|
||||
# for path, product_items in merged_product_items.items():
|
||||
# merged_item = self._get_merged_model_item(path)
|
||||
# self._add_to_new_items(
|
||||
# merged_item, parent_item, new_items, root_item
|
||||
# )
|
||||
#
|
||||
# new_merged_items = []
|
||||
# for product_item in product_items:
|
||||
# item = self._get_product_model_item(product_item)
|
||||
# self._add_to_new_items(
|
||||
# item, merged_item, new_merged_items, root_item
|
||||
# )
|
||||
#
|
||||
# if new_merged_items:
|
||||
# merged_item.appendRows(new_merged_items)
|
||||
#
|
||||
# if not new_items:
|
||||
# continue
|
||||
#
|
||||
# if parent_item is not None:
|
||||
# parent_item.appendRows(new_items)
|
||||
# continue
|
||||
#
|
||||
# new_root_items.extend(new_items)
|
||||
#
|
||||
# root_item.appendRows(new_root_items)
|
||||
#
|
||||
# merged_item_ids_to_remove = (
|
||||
# set(self._merged_items_by_id.keys()) - merged_paths
|
||||
# )
|
||||
# group_names_to_remove = (
|
||||
# set(self._group_items_by_name.keys()) - set(group_names)
|
||||
# )
|
||||
# self._remove_merged_items(merged_item_ids_to_remove)
|
||||
# self._remove_group_items(group_names_to_remove)
|
||||
400
openpype/tools/ayon_loader/ui/products_widget.py
Normal file
400
openpype/tools/ayon_loader/ui/products_widget.py
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
import collections
|
||||
|
||||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
from openpype.tools.utils import (
|
||||
RecursiveSortFilterProxyModel,
|
||||
DeselectableTreeView,
|
||||
)
|
||||
from openpype.tools.utils.delegates import PrettyTimeDelegate
|
||||
|
||||
from .products_model import (
|
||||
ProductsModel,
|
||||
PRODUCTS_MODEL_SENDER_NAME,
|
||||
PRODUCT_TYPE_ROLE,
|
||||
GROUP_TYPE_ROLE,
|
||||
MERGED_COLOR_ROLE,
|
||||
FOLDER_ID_ROLE,
|
||||
PRODUCT_ID_ROLE,
|
||||
VERSION_ID_ROLE,
|
||||
VERSION_THUMBNAIL_ID_ROLE,
|
||||
)
|
||||
from .products_delegates import VersionDelegate, LoadedInSceneDelegate
|
||||
from .actions_utils import show_actions_menu
|
||||
|
||||
|
||||
class ProductsProxyModel(RecursiveSortFilterProxyModel):
|
||||
def __init__(self, parent=None):
|
||||
super(ProductsProxyModel, self).__init__(parent)
|
||||
|
||||
self._product_type_filters = {}
|
||||
self._ascending_sort = True
|
||||
|
||||
def set_product_type_filters(self, product_type_filters):
|
||||
self._product_type_filters = product_type_filters
|
||||
self.invalidateFilter()
|
||||
|
||||
def filterAcceptsRow(self, source_row, source_parent):
|
||||
source_model = self.sourceModel()
|
||||
index = source_model.index(source_row, 0, source_parent)
|
||||
product_types_s = source_model.data(index, PRODUCT_TYPE_ROLE)
|
||||
product_types = []
|
||||
if product_types_s:
|
||||
product_types = product_types_s.split("|")
|
||||
|
||||
for product_type in product_types:
|
||||
if not self._product_type_filters.get(product_type, True):
|
||||
return False
|
||||
return super(ProductsProxyModel, self).filterAcceptsRow(
|
||||
source_row, source_parent)
|
||||
|
||||
def lessThan(self, left, right):
|
||||
l_model = left.model()
|
||||
r_model = right.model()
|
||||
left_group_type = l_model.data(left, GROUP_TYPE_ROLE)
|
||||
right_group_type = r_model.data(right, GROUP_TYPE_ROLE)
|
||||
# Groups are always on top, merged product types are below
|
||||
# and items without group at the bottom
|
||||
# QUESTION Do we need to do it this way?
|
||||
if left_group_type != right_group_type:
|
||||
if left_group_type is None:
|
||||
output = False
|
||||
elif right_group_type is None:
|
||||
output = True
|
||||
else:
|
||||
output = left_group_type < right_group_type
|
||||
if not self._ascending_sort:
|
||||
output = not output
|
||||
return output
|
||||
return super(ProductsProxyModel, self).lessThan(left, right)
|
||||
|
||||
def sort(self, column, order=None):
|
||||
if order is None:
|
||||
order = QtCore.Qt.AscendingOrder
|
||||
self._ascending_sort = order == QtCore.Qt.AscendingOrder
|
||||
super(ProductsProxyModel, self).sort(column, order)
|
||||
|
||||
|
||||
class ProductsWidget(QtWidgets.QWidget):
|
||||
refreshed = QtCore.Signal()
|
||||
merged_products_selection_changed = QtCore.Signal()
|
||||
selection_changed = QtCore.Signal()
|
||||
version_changed = QtCore.Signal()
|
||||
default_widths = (
|
||||
200, # Product name
|
||||
90, # Product type
|
||||
130, # Folder label
|
||||
60, # Version
|
||||
125, # Time
|
||||
75, # Author
|
||||
75, # Frames
|
||||
60, # Duration
|
||||
55, # Handles
|
||||
10, # Step
|
||||
25, # Loaded in scene
|
||||
65, # Site info (maybe?)
|
||||
)
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(ProductsWidget, self).__init__(parent)
|
||||
|
||||
self._controller = controller
|
||||
|
||||
products_view = DeselectableTreeView(self)
|
||||
# TODO - define custom object name in style
|
||||
products_view.setObjectName("SubsetView")
|
||||
products_view.setSelectionMode(
|
||||
QtWidgets.QAbstractItemView.ExtendedSelection
|
||||
)
|
||||
products_view.setAllColumnsShowFocus(True)
|
||||
# TODO - add context menu
|
||||
products_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
products_view.setSortingEnabled(True)
|
||||
# Sort by product type
|
||||
products_view.sortByColumn(1, QtCore.Qt.AscendingOrder)
|
||||
products_view.setAlternatingRowColors(True)
|
||||
|
||||
products_model = ProductsModel(controller)
|
||||
products_proxy_model = ProductsProxyModel()
|
||||
products_proxy_model.setSourceModel(products_model)
|
||||
|
||||
products_view.setModel(products_proxy_model)
|
||||
|
||||
for idx, width in enumerate(self.default_widths):
|
||||
products_view.setColumnWidth(idx, width)
|
||||
|
||||
version_delegate = VersionDelegate()
|
||||
products_view.setItemDelegateForColumn(
|
||||
products_model.version_col, version_delegate)
|
||||
|
||||
time_delegate = PrettyTimeDelegate()
|
||||
products_view.setItemDelegateForColumn(
|
||||
products_model.published_time_col, time_delegate)
|
||||
|
||||
in_scene_delegate = LoadedInSceneDelegate()
|
||||
products_view.setItemDelegateForColumn(
|
||||
products_model.in_scene_col, in_scene_delegate)
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(products_view, 1)
|
||||
|
||||
products_proxy_model.rowsInserted.connect(self._on_rows_inserted)
|
||||
products_proxy_model.rowsMoved.connect(self._on_rows_moved)
|
||||
products_model.refreshed.connect(self._on_refresh)
|
||||
products_view.customContextMenuRequested.connect(
|
||||
self._on_context_menu)
|
||||
products_view.selectionModel().selectionChanged.connect(
|
||||
self._on_selection_change)
|
||||
products_model.version_changed.connect(self._on_version_change)
|
||||
|
||||
controller.register_event_callback(
|
||||
"selection.folders.changed",
|
||||
self._on_folders_selection_change,
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"products.refresh.finished",
|
||||
self._on_products_refresh_finished
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"products.group.changed",
|
||||
self._on_group_changed
|
||||
)
|
||||
|
||||
self._products_view = products_view
|
||||
self._products_model = products_model
|
||||
self._products_proxy_model = products_proxy_model
|
||||
|
||||
self._version_delegate = version_delegate
|
||||
self._time_delegate = time_delegate
|
||||
|
||||
self._selected_project_name = None
|
||||
self._selected_folder_ids = set()
|
||||
|
||||
self._selected_merged_products = []
|
||||
self._selected_versions_info = []
|
||||
|
||||
# Set initial state of widget
|
||||
# - Hide folders column
|
||||
self._update_folders_label_visible()
|
||||
# - Hide in scene column if is not supported (this won't change)
|
||||
products_view.setColumnHidden(
|
||||
products_model.in_scene_col,
|
||||
not controller.is_loaded_products_supported()
|
||||
)
|
||||
|
||||
def set_name_filer(self, name):
|
||||
"""Set filter of product name.
|
||||
|
||||
Args:
|
||||
name (str): The string filter.
|
||||
"""
|
||||
|
||||
self._products_proxy_model.setFilterFixedString(name)
|
||||
|
||||
def set_product_type_filter(self, product_type_filters):
|
||||
"""
|
||||
|
||||
Args:
|
||||
product_type_filters (dict[str, bool]): The filter of product
|
||||
types.
|
||||
"""
|
||||
|
||||
self._products_proxy_model.set_product_type_filters(
|
||||
product_type_filters
|
||||
)
|
||||
|
||||
def set_enable_grouping(self, enable_grouping):
|
||||
self._products_model.set_enable_grouping(enable_grouping)
|
||||
|
||||
def get_selected_merged_products(self):
|
||||
return self._selected_merged_products
|
||||
|
||||
def get_selected_version_info(self):
|
||||
return self._selected_versions_info
|
||||
|
||||
def refresh(self):
|
||||
self._refresh_model()
|
||||
|
||||
def _fill_version_editor(self):
|
||||
model = self._products_proxy_model
|
||||
index_queue = collections.deque()
|
||||
for row in range(model.rowCount()):
|
||||
index_queue.append((row, None))
|
||||
|
||||
version_col = self._products_model.version_col
|
||||
while index_queue:
|
||||
(row, parent_index) = index_queue.popleft()
|
||||
args = [row, 0]
|
||||
if parent_index is not None:
|
||||
args.append(parent_index)
|
||||
index = model.index(*args)
|
||||
rows = model.rowCount(index)
|
||||
for row in range(rows):
|
||||
index_queue.append((row, index))
|
||||
|
||||
product_id = model.data(index, PRODUCT_ID_ROLE)
|
||||
if product_id is not None:
|
||||
args[1] = version_col
|
||||
v_index = model.index(*args)
|
||||
self._products_view.openPersistentEditor(v_index)
|
||||
|
||||
def _on_refresh(self):
|
||||
self._fill_version_editor()
|
||||
self.refreshed.emit()
|
||||
|
||||
def _on_rows_inserted(self):
|
||||
self._fill_version_editor()
|
||||
|
||||
def _on_rows_moved(self):
|
||||
self._fill_version_editor()
|
||||
|
||||
def _refresh_model(self):
|
||||
self._products_model.refresh(
|
||||
self._selected_project_name,
|
||||
self._selected_folder_ids
|
||||
)
|
||||
|
||||
def _on_context_menu(self, point):
|
||||
selection_model = self._products_view.selectionModel()
|
||||
model = self._products_view.model()
|
||||
project_name = self._products_model.get_last_project_name()
|
||||
|
||||
version_ids = set()
|
||||
indexes_queue = collections.deque()
|
||||
indexes_queue.extend(selection_model.selectedIndexes())
|
||||
while indexes_queue:
|
||||
index = indexes_queue.popleft()
|
||||
for row in range(model.rowCount(index)):
|
||||
child_index = model.index(row, 0, index)
|
||||
indexes_queue.append(child_index)
|
||||
version_id = model.data(index, VERSION_ID_ROLE)
|
||||
if version_id is not None:
|
||||
version_ids.add(version_id)
|
||||
|
||||
action_items = self._controller.get_versions_action_items(
|
||||
project_name, version_ids)
|
||||
|
||||
# Prepare global point where to show the menu
|
||||
global_point = self._products_view.mapToGlobal(point)
|
||||
|
||||
result = show_actions_menu(
|
||||
action_items,
|
||||
global_point,
|
||||
len(version_ids) == 1,
|
||||
self
|
||||
)
|
||||
action_item, options = result
|
||||
if action_item is None or options is None:
|
||||
return
|
||||
|
||||
self._controller.trigger_action_item(
|
||||
action_item.identifier,
|
||||
options,
|
||||
action_item.project_name,
|
||||
version_ids=action_item.version_ids,
|
||||
representation_ids=action_item.representation_ids,
|
||||
)
|
||||
|
||||
def _on_selection_change(self):
|
||||
selected_merged_products = []
|
||||
selection_model = self._products_view.selectionModel()
|
||||
model = self._products_view.model()
|
||||
indexes_queue = collections.deque()
|
||||
indexes_queue.extend(selection_model.selectedIndexes())
|
||||
|
||||
# Helper for 'version_items' to avoid duplicated items
|
||||
all_product_ids = set()
|
||||
selected_version_ids = set()
|
||||
# Version items contains information about selected version items
|
||||
selected_versions_info = []
|
||||
while indexes_queue:
|
||||
index = indexes_queue.popleft()
|
||||
if index.column() != 0:
|
||||
continue
|
||||
|
||||
group_type = model.data(index, GROUP_TYPE_ROLE)
|
||||
if group_type is None:
|
||||
product_id = model.data(index, PRODUCT_ID_ROLE)
|
||||
# Skip duplicates - when group and item are selected the item
|
||||
# would be in the loop multiple times
|
||||
if product_id in all_product_ids:
|
||||
continue
|
||||
|
||||
all_product_ids.add(product_id)
|
||||
|
||||
version_id = model.data(index, VERSION_ID_ROLE)
|
||||
selected_version_ids.add(version_id)
|
||||
|
||||
thumbnail_id = model.data(index, VERSION_THUMBNAIL_ID_ROLE)
|
||||
selected_versions_info.append({
|
||||
"folder_id": model.data(index, FOLDER_ID_ROLE),
|
||||
"product_id": product_id,
|
||||
"version_id": version_id,
|
||||
"thumbnail_id": thumbnail_id,
|
||||
})
|
||||
continue
|
||||
|
||||
if group_type == 0:
|
||||
for row in range(model.rowCount(index)):
|
||||
child_index = model.index(row, 0, index)
|
||||
indexes_queue.append(child_index)
|
||||
continue
|
||||
|
||||
if group_type != 1:
|
||||
continue
|
||||
|
||||
item_folder_ids = set()
|
||||
for row in range(model.rowCount(index)):
|
||||
child_index = model.index(row, 0, index)
|
||||
indexes_queue.append(child_index)
|
||||
|
||||
folder_id = model.data(child_index, FOLDER_ID_ROLE)
|
||||
item_folder_ids.add(folder_id)
|
||||
|
||||
if not item_folder_ids:
|
||||
continue
|
||||
|
||||
hex_color = model.data(index, MERGED_COLOR_ROLE)
|
||||
item_data = {
|
||||
"color": hex_color,
|
||||
"folder_ids": item_folder_ids
|
||||
}
|
||||
selected_merged_products.append(item_data)
|
||||
|
||||
prev_selected_merged_products = self._selected_merged_products
|
||||
self._selected_merged_products = selected_merged_products
|
||||
self._selected_versions_info = selected_versions_info
|
||||
|
||||
if selected_merged_products != prev_selected_merged_products:
|
||||
self.merged_products_selection_changed.emit()
|
||||
self.selection_changed.emit()
|
||||
self._controller.set_selected_versions(selected_version_ids)
|
||||
|
||||
def _on_version_change(self):
|
||||
self._on_selection_change()
|
||||
|
||||
def _on_folders_selection_change(self, event):
|
||||
self._selected_project_name = event["project_name"]
|
||||
self._selected_folder_ids = event["folder_ids"]
|
||||
self._refresh_model()
|
||||
self._update_folders_label_visible()
|
||||
|
||||
def _update_folders_label_visible(self):
|
||||
folders_label_hidden = len(self._selected_folder_ids) <= 1
|
||||
self._products_view.setColumnHidden(
|
||||
self._products_model.folders_label_col,
|
||||
folders_label_hidden
|
||||
)
|
||||
|
||||
def _on_products_refresh_finished(self, event):
|
||||
if event["sender"] != PRODUCTS_MODEL_SENDER_NAME:
|
||||
self._refresh_model()
|
||||
|
||||
def _on_group_changed(self, event):
|
||||
if event["project_name"] != self._selected_project_name:
|
||||
return
|
||||
folder_ids = event["folder_ids"]
|
||||
if not set(folder_ids).intersection(set(self._selected_folder_ids)):
|
||||
return
|
||||
self.refresh()
|
||||
338
openpype/tools/ayon_loader/ui/repres_widget.py
Normal file
338
openpype/tools/ayon_loader/ui/repres_widget.py
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
import collections
|
||||
|
||||
from qtpy import QtWidgets, QtGui, QtCore
|
||||
import qtawesome
|
||||
|
||||
from openpype.style import get_default_entity_icon_color
|
||||
from openpype.tools.ayon_utils.widgets import get_qt_icon
|
||||
from openpype.tools.utils import DeselectableTreeView
|
||||
|
||||
from .actions_utils import show_actions_menu
|
||||
|
||||
REPRESENTAION_NAME_ROLE = QtCore.Qt.UserRole + 1
|
||||
REPRESENTATION_ID_ROLE = QtCore.Qt.UserRole + 2
|
||||
PRODUCT_NAME_ROLE = QtCore.Qt.UserRole + 3
|
||||
FOLDER_LABEL_ROLE = QtCore.Qt.UserRole + 4
|
||||
GROUP_TYPE_ROLE = QtCore.Qt.UserRole + 5
|
||||
|
||||
|
||||
class RepresentationsModel(QtGui.QStandardItemModel):
|
||||
refreshed = QtCore.Signal()
|
||||
colums_info = [
|
||||
("Name", 120),
|
||||
("Product name", 125),
|
||||
("Folder", 125),
|
||||
# ("Active site", 85),
|
||||
# ("Remote site", 85)
|
||||
]
|
||||
column_labels = [label for label, _ in colums_info]
|
||||
column_widths = [width for _, width in colums_info]
|
||||
folder_column = column_labels.index("Product name")
|
||||
|
||||
def __init__(self, controller):
|
||||
super(RepresentationsModel, self).__init__()
|
||||
|
||||
self.setColumnCount(len(self.column_labels))
|
||||
|
||||
for idx, label in enumerate(self.column_labels):
|
||||
self.setHeaderData(idx, QtCore.Qt.Horizontal, label)
|
||||
|
||||
controller.register_event_callback(
|
||||
"selection.project.changed",
|
||||
self._on_project_change
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"selection.versions.changed",
|
||||
self._on_version_change
|
||||
)
|
||||
self._selected_project_name = None
|
||||
self._selected_version_ids = None
|
||||
|
||||
self._group_icon = None
|
||||
|
||||
self._items_by_id = {}
|
||||
self._groups_items_by_name = {}
|
||||
|
||||
self._controller = controller
|
||||
|
||||
def refresh(self):
|
||||
repre_items = self._controller.get_representation_items(
|
||||
self._selected_project_name, self._selected_version_ids
|
||||
)
|
||||
self._fill_items(repre_items)
|
||||
self.refreshed.emit()
|
||||
|
||||
def data(self, index, role=None):
|
||||
if role is None:
|
||||
role = QtCore.Qt.DisplayRole
|
||||
|
||||
col = index.column()
|
||||
if col != 0:
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
return None
|
||||
|
||||
if role == QtCore.Qt.DisplayRole:
|
||||
if col == 1:
|
||||
role = PRODUCT_NAME_ROLE
|
||||
elif col == 2:
|
||||
role = FOLDER_LABEL_ROLE
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
return super(RepresentationsModel, self).data(index, role)
|
||||
|
||||
def setData(self, index, value, role=None):
|
||||
if role is None:
|
||||
role = QtCore.Qt.EditRole
|
||||
return super(RepresentationsModel, self).setData(index, value, role)
|
||||
|
||||
def _clear_items(self):
|
||||
self._items_by_id = {}
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.removeRows(0, root_item.rowCount())
|
||||
|
||||
def _get_repre_item(self, repre_item):
|
||||
repre_id = repre_item.representation_id
|
||||
repre_name = repre_item.representation_name
|
||||
repre_icon = repre_item.representation_icon
|
||||
item = self._items_by_id.get(repre_id)
|
||||
is_new_item = False
|
||||
if item is None:
|
||||
is_new_item = True
|
||||
item = QtGui.QStandardItem()
|
||||
self._items_by_id[repre_id] = item
|
||||
item.setColumnCount(self.columnCount())
|
||||
item.setEditable(False)
|
||||
|
||||
icon = get_qt_icon(repre_icon)
|
||||
item.setData(repre_name, QtCore.Qt.DisplayRole)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
item.setData(repre_name, REPRESENTAION_NAME_ROLE)
|
||||
item.setData(repre_id, REPRESENTATION_ID_ROLE)
|
||||
item.setData(repre_item.product_name, PRODUCT_NAME_ROLE)
|
||||
item.setData(repre_item.folder_label, FOLDER_LABEL_ROLE)
|
||||
return is_new_item, item
|
||||
|
||||
def _get_group_icon(self):
|
||||
if self._group_icon is None:
|
||||
self._group_icon = qtawesome.icon(
|
||||
"fa.folder",
|
||||
color=get_default_entity_icon_color()
|
||||
)
|
||||
return self._group_icon
|
||||
|
||||
def _get_group_item(self, repre_name):
|
||||
item = self._groups_items_by_name.get(repre_name)
|
||||
if item is not None:
|
||||
return False, item
|
||||
|
||||
# TODO add color
|
||||
item = QtGui.QStandardItem()
|
||||
item.setColumnCount(self.columnCount())
|
||||
item.setData(repre_name, QtCore.Qt.DisplayRole)
|
||||
item.setData(self._get_group_icon(), QtCore.Qt.DecorationRole)
|
||||
item.setData(0, GROUP_TYPE_ROLE)
|
||||
item.setEditable(False)
|
||||
self._groups_items_by_name[repre_name] = item
|
||||
return True, item
|
||||
|
||||
def _fill_items(self, repre_items):
|
||||
items_to_remove = set(self._items_by_id.keys())
|
||||
repre_items_by_name = collections.defaultdict(list)
|
||||
for repre_item in repre_items:
|
||||
items_to_remove.discard(repre_item.representation_id)
|
||||
repre_name = repre_item.representation_name
|
||||
repre_items_by_name[repre_name].append(repre_item)
|
||||
|
||||
root_item = self.invisibleRootItem()
|
||||
for repre_id in items_to_remove:
|
||||
item = self._items_by_id.pop(repre_id)
|
||||
parent_item = item.parent()
|
||||
if parent_item is None:
|
||||
parent_item = root_item
|
||||
parent_item.removeRow(item.row())
|
||||
|
||||
group_names = set()
|
||||
new_root_items = []
|
||||
for repre_name, repre_name_items in repre_items_by_name.items():
|
||||
group_item = None
|
||||
parent_is_group = False
|
||||
if len(repre_name_items) > 1:
|
||||
group_names.add(repre_name)
|
||||
is_new_group, group_item = self._get_group_item(repre_name)
|
||||
if is_new_group:
|
||||
new_root_items.append(group_item)
|
||||
parent_is_group = True
|
||||
|
||||
new_group_items = []
|
||||
for repre_item in repre_name_items:
|
||||
is_new_item, item = self._get_repre_item(repre_item)
|
||||
item_parent = item.parent()
|
||||
if item_parent is None:
|
||||
item_parent = root_item
|
||||
|
||||
if not is_new_item:
|
||||
if parent_is_group:
|
||||
if item_parent is group_item:
|
||||
continue
|
||||
elif item_parent is root_item:
|
||||
continue
|
||||
item_parent.takeRow(item.row())
|
||||
is_new_item = True
|
||||
|
||||
if is_new_item:
|
||||
new_group_items.append(item)
|
||||
|
||||
if not new_group_items:
|
||||
continue
|
||||
|
||||
if group_item is not None:
|
||||
group_item.appendRows(new_group_items)
|
||||
else:
|
||||
new_root_items.extend(new_group_items)
|
||||
|
||||
if new_root_items:
|
||||
root_item.appendRows(new_root_items)
|
||||
|
||||
for group_name in set(self._groups_items_by_name) - group_names:
|
||||
item = self._groups_items_by_name.pop(group_name)
|
||||
parent_item = item.parent()
|
||||
if parent_item is None:
|
||||
parent_item = root_item
|
||||
parent_item.removeRow(item.row())
|
||||
|
||||
def _on_project_change(self, event):
|
||||
self._selected_project_name = event["project_name"]
|
||||
|
||||
def _on_version_change(self, event):
|
||||
self._selected_version_ids = event["version_ids"]
|
||||
self.refresh()
|
||||
|
||||
|
||||
class RepresentationsWidget(QtWidgets.QWidget):
|
||||
def __init__(self, controller, parent):
|
||||
super(RepresentationsWidget, self).__init__(parent)
|
||||
|
||||
repre_view = DeselectableTreeView(self)
|
||||
repre_view.setSelectionMode(
|
||||
QtWidgets.QAbstractItemView.ExtendedSelection
|
||||
)
|
||||
repre_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
repre_view.setSortingEnabled(True)
|
||||
repre_view.setAlternatingRowColors(True)
|
||||
|
||||
repre_model = RepresentationsModel(controller)
|
||||
repre_proxy_model = QtCore.QSortFilterProxyModel()
|
||||
repre_proxy_model.setSourceModel(repre_model)
|
||||
repre_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
repre_view.setModel(repre_proxy_model)
|
||||
|
||||
for idx, width in enumerate(repre_model.column_widths):
|
||||
repre_view.setColumnWidth(idx, width)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(repre_view, 1)
|
||||
|
||||
repre_view.customContextMenuRequested.connect(
|
||||
self._on_context_menu)
|
||||
repre_view.selectionModel().selectionChanged.connect(
|
||||
self._on_selection_change)
|
||||
repre_model.refreshed.connect(self._on_model_refresh)
|
||||
|
||||
controller.register_event_callback(
|
||||
"selection.project.changed",
|
||||
self._on_project_change
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"selection.folders.changed",
|
||||
self._on_folder_change
|
||||
)
|
||||
|
||||
self._controller = controller
|
||||
self._selected_project_name = None
|
||||
self._selected_multiple_folders = None
|
||||
|
||||
self._repre_view = repre_view
|
||||
self._repre_model = repre_model
|
||||
self._repre_proxy_model = repre_proxy_model
|
||||
|
||||
self._set_multiple_folders_selected(False)
|
||||
|
||||
def refresh(self):
|
||||
self._repre_model.refresh()
|
||||
|
||||
def _on_folder_change(self, event):
|
||||
self._set_multiple_folders_selected(len(event["folder_ids"]) > 1)
|
||||
|
||||
def _on_project_change(self, event):
|
||||
self._selected_project_name = event["project_name"]
|
||||
|
||||
def _set_multiple_folders_selected(self, selected_multiple_folders):
|
||||
if selected_multiple_folders == self._selected_multiple_folders:
|
||||
return
|
||||
self._selected_multiple_folders = selected_multiple_folders
|
||||
self._repre_view.setColumnHidden(
|
||||
self._repre_model.folder_column,
|
||||
not self._selected_multiple_folders
|
||||
)
|
||||
|
||||
def _on_model_refresh(self):
|
||||
self._repre_proxy_model.sort(0)
|
||||
|
||||
def _get_selected_repre_indexes(self):
|
||||
selection_model = self._repre_view.selectionModel()
|
||||
model = self._repre_view.model()
|
||||
indexes_queue = collections.deque()
|
||||
indexes_queue.extend(selection_model.selectedIndexes())
|
||||
|
||||
selected_indexes = []
|
||||
while indexes_queue:
|
||||
index = indexes_queue.popleft()
|
||||
if index.column() != 0:
|
||||
continue
|
||||
|
||||
group_type = model.data(index, GROUP_TYPE_ROLE)
|
||||
if group_type is None:
|
||||
selected_indexes.append(index)
|
||||
|
||||
elif group_type == 0:
|
||||
for row in range(model.rowCount(index)):
|
||||
child_index = model.index(row, 0, index)
|
||||
indexes_queue.append(child_index)
|
||||
|
||||
return selected_indexes
|
||||
|
||||
def _get_selected_repre_ids(self):
|
||||
repre_ids = {
|
||||
index.data(REPRESENTATION_ID_ROLE)
|
||||
for index in self._get_selected_repre_indexes()
|
||||
}
|
||||
repre_ids.discard(None)
|
||||
return repre_ids
|
||||
|
||||
def _on_selection_change(self):
|
||||
selected_repre_ids = self._get_selected_repre_ids()
|
||||
self._controller.set_selected_representations(selected_repre_ids)
|
||||
|
||||
def _on_context_menu(self, point):
|
||||
repre_ids = self._get_selected_repre_ids()
|
||||
action_items = self._controller.get_representations_action_items(
|
||||
self._selected_project_name, repre_ids
|
||||
)
|
||||
global_point = self._repre_view.mapToGlobal(point)
|
||||
result = show_actions_menu(
|
||||
action_items,
|
||||
global_point,
|
||||
len(repre_ids) == 1,
|
||||
self
|
||||
)
|
||||
action_item, options = result
|
||||
if action_item is None or options is None:
|
||||
return
|
||||
|
||||
self._controller.trigger_action_item(
|
||||
action_item.identifier,
|
||||
options,
|
||||
action_item.project_name,
|
||||
version_ids=action_item.version_ids,
|
||||
representation_ids=action_item.representation_ids,
|
||||
)
|
||||
511
openpype/tools/ayon_loader/ui/window.py
Normal file
511
openpype/tools/ayon_loader/ui/window.py
Normal file
|
|
@ -0,0 +1,511 @@
|
|||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype.resources import get_openpype_icon_filepath
|
||||
from openpype.style import load_stylesheet
|
||||
from openpype.tools.utils import (
|
||||
PlaceholderLineEdit,
|
||||
ErrorMessageBox,
|
||||
ThumbnailPainterWidget,
|
||||
RefreshButton,
|
||||
GoToCurrentButton,
|
||||
)
|
||||
from openpype.tools.utils.lib import center_window
|
||||
from openpype.tools.ayon_utils.widgets import ProjectsCombobox
|
||||
from openpype.tools.ayon_loader.control import LoaderController
|
||||
|
||||
from .folders_widget import LoaderFoldersWidget
|
||||
from .products_widget import ProductsWidget
|
||||
from .product_types_widget import ProductTypesView
|
||||
from .product_group_dialog import ProductGroupDialog
|
||||
from .info_widget import InfoWidget
|
||||
from .repres_widget import RepresentationsWidget
|
||||
|
||||
|
||||
class LoadErrorMessageBox(ErrorMessageBox):
|
||||
def __init__(self, messages, parent=None):
|
||||
self._messages = messages
|
||||
super(LoadErrorMessageBox, self).__init__("Loading failed", parent)
|
||||
|
||||
def _create_top_widget(self, parent_widget):
|
||||
label_widget = QtWidgets.QLabel(parent_widget)
|
||||
label_widget.setText(
|
||||
"<span style='font-size:18pt;'>Failed to load items</span>"
|
||||
)
|
||||
return label_widget
|
||||
|
||||
def _get_report_data(self):
|
||||
report_data = []
|
||||
for exc_msg, tb_text, repre, product, version in self._messages:
|
||||
report_message = (
|
||||
"During load error happened on Product: \"{product}\""
|
||||
" Representation: \"{repre}\" Version: {version}"
|
||||
"\n\nError message: {message}"
|
||||
).format(
|
||||
product=product,
|
||||
repre=repre,
|
||||
version=version,
|
||||
message=exc_msg
|
||||
)
|
||||
if tb_text:
|
||||
report_message += "\n\n{}".format(tb_text)
|
||||
report_data.append(report_message)
|
||||
return report_data
|
||||
|
||||
def _create_content(self, content_layout):
|
||||
item_name_template = (
|
||||
"<span style='font-weight:bold;'>Product:</span> {}<br>"
|
||||
"<span style='font-weight:bold;'>Version:</span> {}<br>"
|
||||
"<span style='font-weight:bold;'>Representation:</span> {}<br>"
|
||||
)
|
||||
exc_msg_template = "<span style='font-weight:bold'>{}</span>"
|
||||
|
||||
for exc_msg, tb_text, repre, product, version in self._messages:
|
||||
line = self._create_line()
|
||||
content_layout.addWidget(line)
|
||||
|
||||
item_name = item_name_template.format(product, version, repre)
|
||||
item_name_widget = QtWidgets.QLabel(
|
||||
item_name.replace("\n", "<br>"), self
|
||||
)
|
||||
item_name_widget.setWordWrap(True)
|
||||
content_layout.addWidget(item_name_widget)
|
||||
|
||||
exc_msg = exc_msg_template.format(exc_msg.replace("\n", "<br>"))
|
||||
message_label_widget = QtWidgets.QLabel(exc_msg, self)
|
||||
message_label_widget.setWordWrap(True)
|
||||
content_layout.addWidget(message_label_widget)
|
||||
|
||||
if tb_text:
|
||||
line = self._create_line()
|
||||
tb_widget = self._create_traceback_widget(tb_text, self)
|
||||
content_layout.addWidget(line)
|
||||
content_layout.addWidget(tb_widget)
|
||||
|
||||
|
||||
class RefreshHandler:
|
||||
def __init__(self):
|
||||
self._project_refreshed = False
|
||||
self._folders_refreshed = False
|
||||
self._products_refreshed = False
|
||||
|
||||
@property
|
||||
def project_refreshed(self):
|
||||
return self._products_refreshed
|
||||
|
||||
@property
|
||||
def folders_refreshed(self):
|
||||
return self._folders_refreshed
|
||||
|
||||
@property
|
||||
def products_refreshed(self):
|
||||
return self._products_refreshed
|
||||
|
||||
def reset(self):
|
||||
self._project_refreshed = False
|
||||
self._folders_refreshed = False
|
||||
self._products_refreshed = False
|
||||
|
||||
def set_project_refreshed(self):
|
||||
self._project_refreshed = True
|
||||
|
||||
def set_folders_refreshed(self):
|
||||
self._folders_refreshed = True
|
||||
|
||||
def set_products_refreshed(self):
|
||||
self._products_refreshed = True
|
||||
|
||||
|
||||
class LoaderWindow(QtWidgets.QWidget):
|
||||
def __init__(self, controller=None, parent=None):
|
||||
super(LoaderWindow, self).__init__(parent)
|
||||
|
||||
icon = QtGui.QIcon(get_openpype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
self.setWindowTitle("AYON Loader")
|
||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
self.setAttribute(QtCore.Qt.WA_DeleteOnClose, False)
|
||||
self.setWindowFlags(self.windowFlags() | QtCore.Qt.Window)
|
||||
|
||||
if controller is None:
|
||||
controller = LoaderController()
|
||||
|
||||
main_splitter = QtWidgets.QSplitter(self)
|
||||
|
||||
context_splitter = QtWidgets.QSplitter(main_splitter)
|
||||
context_splitter.setOrientation(QtCore.Qt.Vertical)
|
||||
|
||||
# Context selection widget
|
||||
context_widget = QtWidgets.QWidget(context_splitter)
|
||||
|
||||
context_top_widget = QtWidgets.QWidget(context_widget)
|
||||
projects_combobox = ProjectsCombobox(
|
||||
controller,
|
||||
context_top_widget,
|
||||
handle_expected_selection=True
|
||||
)
|
||||
projects_combobox.set_select_item_visible(True)
|
||||
projects_combobox.set_libraries_separator_visible(True)
|
||||
projects_combobox.set_standard_filter_enabled(
|
||||
controller.is_standard_projects_filter_enabled()
|
||||
)
|
||||
|
||||
go_to_current_btn = GoToCurrentButton(context_top_widget)
|
||||
refresh_btn = RefreshButton(context_top_widget)
|
||||
|
||||
context_top_layout = QtWidgets.QHBoxLayout(context_top_widget)
|
||||
context_top_layout.setContentsMargins(0, 0, 0, 0,)
|
||||
context_top_layout.addWidget(projects_combobox, 1)
|
||||
context_top_layout.addWidget(go_to_current_btn, 0)
|
||||
context_top_layout.addWidget(refresh_btn, 0)
|
||||
|
||||
folders_filter_input = PlaceholderLineEdit(context_widget)
|
||||
folders_filter_input.setPlaceholderText("Folder name filter...")
|
||||
|
||||
folders_widget = LoaderFoldersWidget(controller, context_widget)
|
||||
|
||||
product_types_widget = ProductTypesView(controller, context_splitter)
|
||||
|
||||
context_layout = QtWidgets.QVBoxLayout(context_widget)
|
||||
context_layout.setContentsMargins(0, 0, 0, 0)
|
||||
context_layout.addWidget(context_top_widget, 0)
|
||||
context_layout.addWidget(folders_filter_input, 0)
|
||||
context_layout.addWidget(folders_widget, 1)
|
||||
|
||||
context_splitter.addWidget(context_widget)
|
||||
context_splitter.addWidget(product_types_widget)
|
||||
context_splitter.setStretchFactor(0, 65)
|
||||
context_splitter.setStretchFactor(1, 35)
|
||||
|
||||
# Product + version selection item
|
||||
products_wrap_widget = QtWidgets.QWidget(main_splitter)
|
||||
|
||||
products_inputs_widget = QtWidgets.QWidget(products_wrap_widget)
|
||||
|
||||
products_filter_input = PlaceholderLineEdit(products_inputs_widget)
|
||||
products_filter_input.setPlaceholderText("Product name filter...")
|
||||
product_group_checkbox = QtWidgets.QCheckBox(
|
||||
"Enable grouping", products_inputs_widget)
|
||||
product_group_checkbox.setChecked(True)
|
||||
|
||||
products_widget = ProductsWidget(controller, products_wrap_widget)
|
||||
|
||||
products_inputs_layout = QtWidgets.QHBoxLayout(products_inputs_widget)
|
||||
products_inputs_layout.setContentsMargins(0, 0, 0, 0)
|
||||
products_inputs_layout.addWidget(products_filter_input, 1)
|
||||
products_inputs_layout.addWidget(product_group_checkbox, 0)
|
||||
|
||||
products_wrap_layout = QtWidgets.QVBoxLayout(products_wrap_widget)
|
||||
products_wrap_layout.setContentsMargins(0, 0, 0, 0)
|
||||
products_wrap_layout.addWidget(products_inputs_widget, 0)
|
||||
products_wrap_layout.addWidget(products_widget, 1)
|
||||
|
||||
right_panel_splitter = QtWidgets.QSplitter(main_splitter)
|
||||
right_panel_splitter.setOrientation(QtCore.Qt.Vertical)
|
||||
|
||||
thumbnails_widget = ThumbnailPainterWidget(right_panel_splitter)
|
||||
thumbnails_widget.set_use_checkboard(False)
|
||||
|
||||
info_widget = InfoWidget(controller, right_panel_splitter)
|
||||
|
||||
repre_widget = RepresentationsWidget(controller, right_panel_splitter)
|
||||
|
||||
right_panel_splitter.addWidget(thumbnails_widget)
|
||||
right_panel_splitter.addWidget(info_widget)
|
||||
right_panel_splitter.addWidget(repre_widget)
|
||||
|
||||
right_panel_splitter.setStretchFactor(0, 1)
|
||||
right_panel_splitter.setStretchFactor(1, 1)
|
||||
right_panel_splitter.setStretchFactor(2, 2)
|
||||
|
||||
main_splitter.addWidget(context_splitter)
|
||||
main_splitter.addWidget(products_wrap_widget)
|
||||
main_splitter.addWidget(right_panel_splitter)
|
||||
|
||||
main_splitter.setStretchFactor(0, 4)
|
||||
main_splitter.setStretchFactor(1, 6)
|
||||
main_splitter.setStretchFactor(2, 1)
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.addWidget(main_splitter)
|
||||
|
||||
show_timer = QtCore.QTimer()
|
||||
show_timer.setInterval(1)
|
||||
|
||||
show_timer.timeout.connect(self._on_show_timer)
|
||||
|
||||
projects_combobox.refreshed.connect(self._on_projects_refresh)
|
||||
folders_widget.refreshed.connect(self._on_folders_refresh)
|
||||
products_widget.refreshed.connect(self._on_products_refresh)
|
||||
folders_filter_input.textChanged.connect(
|
||||
self._on_folder_filter_change
|
||||
)
|
||||
product_types_widget.filter_changed.connect(
|
||||
self._on_product_type_filter_change
|
||||
)
|
||||
products_filter_input.textChanged.connect(
|
||||
self._on_product_filter_change
|
||||
)
|
||||
product_group_checkbox.stateChanged.connect(
|
||||
self._on_product_group_change
|
||||
)
|
||||
products_widget.merged_products_selection_changed.connect(
|
||||
self._on_merged_products_selection_change
|
||||
)
|
||||
products_widget.selection_changed.connect(
|
||||
self._on_products_selection_change
|
||||
)
|
||||
go_to_current_btn.clicked.connect(
|
||||
self._on_go_to_current_context_click
|
||||
)
|
||||
refresh_btn.clicked.connect(
|
||||
self._on_refresh_click
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"load.finished",
|
||||
self._on_load_finished,
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"selection.project.changed",
|
||||
self._on_project_selection_changed,
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"selection.folders.changed",
|
||||
self._on_folders_selection_changed,
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"selection.versions.changed",
|
||||
self._on_versions_selection_changed,
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"controller.reset.started",
|
||||
self._on_controller_reset_start,
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"controller.reset.finished",
|
||||
self._on_controller_reset_finish,
|
||||
)
|
||||
|
||||
self._group_dialog = ProductGroupDialog(controller, self)
|
||||
|
||||
self._main_splitter = main_splitter
|
||||
|
||||
self._go_to_current_btn = go_to_current_btn
|
||||
self._refresh_btn = refresh_btn
|
||||
self._projects_combobox = projects_combobox
|
||||
|
||||
self._folders_filter_input = folders_filter_input
|
||||
self._folders_widget = folders_widget
|
||||
|
||||
self._product_types_widget = product_types_widget
|
||||
|
||||
self._products_filter_input = products_filter_input
|
||||
self._product_group_checkbox = product_group_checkbox
|
||||
self._products_widget = products_widget
|
||||
|
||||
self._right_panel_splitter = right_panel_splitter
|
||||
self._thumbnails_widget = thumbnails_widget
|
||||
self._info_widget = info_widget
|
||||
self._repre_widget = repre_widget
|
||||
|
||||
self._controller = controller
|
||||
self._refresh_handler = RefreshHandler()
|
||||
self._first_show = True
|
||||
self._reset_on_show = True
|
||||
self._show_counter = 0
|
||||
self._show_timer = show_timer
|
||||
self._selected_project_name = None
|
||||
self._selected_folder_ids = set()
|
||||
self._selected_version_ids = set()
|
||||
|
||||
self._products_widget.set_enable_grouping(
|
||||
self._product_group_checkbox.isChecked()
|
||||
)
|
||||
|
||||
def refresh(self):
|
||||
self._controller.reset()
|
||||
|
||||
def showEvent(self, event):
|
||||
super(LoaderWindow, self).showEvent(event)
|
||||
|
||||
if self._first_show:
|
||||
self._on_first_show()
|
||||
|
||||
self._show_timer.start()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
modifiers = event.modifiers()
|
||||
ctrl_pressed = QtCore.Qt.ControlModifier & modifiers
|
||||
|
||||
# Grouping products on pressing Ctrl + G
|
||||
if (
|
||||
ctrl_pressed
|
||||
and event.key() == QtCore.Qt.Key_G
|
||||
and not event.isAutoRepeat()
|
||||
):
|
||||
self._show_group_dialog()
|
||||
event.setAccepted(True)
|
||||
return
|
||||
|
||||
super(LoaderWindow, self).keyPressEvent(event)
|
||||
|
||||
def _on_first_show(self):
|
||||
self._first_show = False
|
||||
# width, height = 1800, 900
|
||||
width, height = 1500, 750
|
||||
|
||||
self.resize(width, height)
|
||||
|
||||
mid_width = int(width / 1.8)
|
||||
sides_width = int((width - mid_width) * 0.5)
|
||||
self._main_splitter.setSizes(
|
||||
[sides_width, mid_width, sides_width]
|
||||
)
|
||||
|
||||
thumbnail_height = int(height / 3.6)
|
||||
info_height = int((height - thumbnail_height) * 0.5)
|
||||
self._right_panel_splitter.setSizes(
|
||||
[thumbnail_height, info_height, info_height]
|
||||
)
|
||||
self.setStyleSheet(load_stylesheet())
|
||||
center_window(self)
|
||||
|
||||
def _on_show_timer(self):
|
||||
if self._show_counter < 2:
|
||||
self._show_counter += 1
|
||||
return
|
||||
|
||||
self._show_counter = 0
|
||||
self._show_timer.stop()
|
||||
|
||||
if self._reset_on_show:
|
||||
self._reset_on_show = False
|
||||
self._controller.reset()
|
||||
|
||||
def _show_group_dialog(self):
|
||||
project_name = self._projects_combobox.get_current_project_name()
|
||||
if not project_name:
|
||||
return
|
||||
|
||||
product_ids = {
|
||||
i["product_id"]
|
||||
for i in self._products_widget.get_selected_version_info()
|
||||
}
|
||||
if not product_ids:
|
||||
return
|
||||
|
||||
self._group_dialog.set_product_ids(project_name, product_ids)
|
||||
self._group_dialog.show()
|
||||
|
||||
def _on_folder_filter_change(self, text):
|
||||
self._folders_widget.set_name_filer(text)
|
||||
|
||||
def _on_product_group_change(self):
|
||||
self._products_widget.set_enable_grouping(
|
||||
self._product_group_checkbox.isChecked()
|
||||
)
|
||||
|
||||
def _on_product_filter_change(self, text):
|
||||
self._products_widget.set_name_filer(text)
|
||||
|
||||
def _on_product_type_filter_change(self):
|
||||
self._products_widget.set_product_type_filter(
|
||||
self._product_types_widget.get_filter_info()
|
||||
)
|
||||
|
||||
def _on_merged_products_selection_change(self):
|
||||
items = self._products_widget.get_selected_merged_products()
|
||||
self._folders_widget.set_merged_products_selection(items)
|
||||
|
||||
def _on_products_selection_change(self):
|
||||
items = self._products_widget.get_selected_version_info()
|
||||
self._info_widget.set_selected_version_info(
|
||||
self._projects_combobox.get_current_project_name(),
|
||||
items
|
||||
)
|
||||
|
||||
def _on_go_to_current_context_click(self):
|
||||
context = self._controller.get_current_context()
|
||||
self._controller.set_expected_selection(
|
||||
context["project_name"],
|
||||
context["folder_id"],
|
||||
)
|
||||
|
||||
def _on_refresh_click(self):
|
||||
self._controller.reset()
|
||||
|
||||
def _on_controller_reset_start(self):
|
||||
self._refresh_handler.reset()
|
||||
|
||||
def _on_controller_reset_finish(self):
|
||||
context = self._controller.get_current_context()
|
||||
project_name = context["project_name"]
|
||||
self._go_to_current_btn.setVisible(bool(project_name))
|
||||
self._projects_combobox.set_current_context_project(project_name)
|
||||
if not self._refresh_handler.project_refreshed:
|
||||
self._projects_combobox.refresh()
|
||||
|
||||
def _on_load_finished(self, event):
|
||||
error_info = event["error_info"]
|
||||
if not error_info:
|
||||
return
|
||||
|
||||
box = LoadErrorMessageBox(error_info, self)
|
||||
box.show()
|
||||
|
||||
def _on_project_selection_changed(self, event):
|
||||
self._selected_project_name = event["project_name"]
|
||||
|
||||
def _on_folders_selection_changed(self, event):
|
||||
self._selected_folder_ids = set(event["folder_ids"])
|
||||
self._update_thumbnails()
|
||||
|
||||
def _on_versions_selection_changed(self, event):
|
||||
self._selected_version_ids = set(event["version_ids"])
|
||||
self._update_thumbnails()
|
||||
|
||||
def _update_thumbnails(self):
|
||||
project_name = self._selected_project_name
|
||||
thumbnail_ids = set()
|
||||
if self._selected_version_ids:
|
||||
thumbnail_id_by_entity_id = (
|
||||
self._controller.get_version_thumbnail_ids(
|
||||
project_name,
|
||||
self._selected_version_ids
|
||||
)
|
||||
)
|
||||
thumbnail_ids = set(thumbnail_id_by_entity_id.values())
|
||||
elif self._selected_folder_ids:
|
||||
thumbnail_id_by_entity_id = (
|
||||
self._controller.get_folder_thumbnail_ids(
|
||||
project_name,
|
||||
self._selected_folder_ids
|
||||
)
|
||||
)
|
||||
thumbnail_ids = set(thumbnail_id_by_entity_id.values())
|
||||
|
||||
thumbnail_ids.discard(None)
|
||||
|
||||
if not thumbnail_ids:
|
||||
self._thumbnails_widget.set_current_thumbnails(None)
|
||||
return
|
||||
|
||||
thumbnail_paths = set()
|
||||
for thumbnail_id in thumbnail_ids:
|
||||
thumbnail_path = self._controller.get_thumbnail_path(
|
||||
project_name, thumbnail_id)
|
||||
thumbnail_paths.add(thumbnail_path)
|
||||
thumbnail_paths.discard(None)
|
||||
self._thumbnails_widget.set_current_thumbnail_paths(thumbnail_paths)
|
||||
|
||||
def _on_projects_refresh(self):
|
||||
self._refresh_handler.set_project_refreshed()
|
||||
if not self._refresh_handler.folders_refreshed:
|
||||
self._folders_widget.refresh()
|
||||
|
||||
def _on_folders_refresh(self):
|
||||
self._refresh_handler.set_folders_refreshed()
|
||||
if not self._refresh_handler.products_refreshed:
|
||||
self._products_widget.refresh()
|
||||
|
||||
def _on_products_refresh(self):
|
||||
self._refresh_handler.set_products_refreshed()
|
||||
|
|
@ -12,6 +12,7 @@ from .hierarchy import (
|
|||
HierarchyModel,
|
||||
HIERARCHY_MODEL_SENDER,
|
||||
)
|
||||
from .thumbnails import ThumbnailsModel
|
||||
|
||||
|
||||
__all__ = (
|
||||
|
|
@ -26,4 +27,6 @@ __all__ = (
|
|||
"TaskItem",
|
||||
"HierarchyModel",
|
||||
"HIERARCHY_MODEL_SENDER",
|
||||
|
||||
"ThumbnailsModel",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,9 +29,8 @@ class FolderItem:
|
|||
parent_id (Union[str, None]): Parent folder id. If 'None' then project
|
||||
is parent.
|
||||
name (str): Name of folder.
|
||||
label (str): Folder label.
|
||||
icon_name (str): Name of icon from font awesome.
|
||||
icon_color (str): Hex color string that will be used for icon.
|
||||
label (Union[str, None]): Folder label.
|
||||
icon (Union[dict[str, Any], None]): Icon definition.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -240,23 +239,65 @@ class HierarchyModel(object):
|
|||
self._refresh_tasks_cache(project_name, folder_id, sender)
|
||||
return task_cache.get_data()
|
||||
|
||||
def get_folder_entities(self, project_name, folder_ids):
|
||||
"""Get folder entities by ids.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
folder_ids (Iterable[str]): Folder ids.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Folder entities by id.
|
||||
"""
|
||||
|
||||
output = {}
|
||||
folder_ids = set(folder_ids)
|
||||
if not project_name or not folder_ids:
|
||||
return output
|
||||
|
||||
folder_ids_to_query = set()
|
||||
for folder_id in folder_ids:
|
||||
cache = self._folders_by_id[project_name][folder_id]
|
||||
if cache.is_valid:
|
||||
output[folder_id] = cache.get_data()
|
||||
elif folder_id:
|
||||
folder_ids_to_query.add(folder_id)
|
||||
else:
|
||||
output[folder_id] = None
|
||||
self._query_folder_entities(project_name, folder_ids_to_query)
|
||||
for folder_id in folder_ids_to_query:
|
||||
cache = self._folders_by_id[project_name][folder_id]
|
||||
output[folder_id] = cache.get_data()
|
||||
return output
|
||||
|
||||
def get_folder_entity(self, project_name, folder_id):
|
||||
cache = self._folders_by_id[project_name][folder_id]
|
||||
if not cache.is_valid:
|
||||
entity = None
|
||||
if folder_id:
|
||||
entity = ayon_api.get_folder_by_id(project_name, folder_id)
|
||||
cache.update_data(entity)
|
||||
return cache.get_data()
|
||||
output = self.get_folder_entities(project_name, {folder_id})
|
||||
return output[folder_id]
|
||||
|
||||
def get_task_entities(self, project_name, task_ids):
|
||||
output = {}
|
||||
task_ids = set(task_ids)
|
||||
if not project_name or not task_ids:
|
||||
return output
|
||||
|
||||
task_ids_to_query = set()
|
||||
for task_id in task_ids:
|
||||
cache = self._tasks_by_id[project_name][task_id]
|
||||
if cache.is_valid:
|
||||
output[task_id] = cache.get_data()
|
||||
elif task_id:
|
||||
task_ids_to_query.add(task_id)
|
||||
else:
|
||||
output[task_id] = None
|
||||
self._query_task_entities(project_name, task_ids_to_query)
|
||||
for task_id in task_ids_to_query:
|
||||
cache = self._tasks_by_id[project_name][task_id]
|
||||
output[task_id] = cache.get_data()
|
||||
return output
|
||||
|
||||
def get_task_entity(self, project_name, task_id):
|
||||
cache = self._tasks_by_id[project_name][task_id]
|
||||
if not cache.is_valid:
|
||||
entity = None
|
||||
if task_id:
|
||||
entity = ayon_api.get_task_by_id(project_name, task_id)
|
||||
cache.update_data(entity)
|
||||
return cache.get_data()
|
||||
output = self.get_task_entities(project_name, {task_id})
|
||||
return output[task_id]
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _folder_refresh_event_manager(self, project_name, sender):
|
||||
|
|
@ -326,6 +367,25 @@ class HierarchyModel(object):
|
|||
hierachy_queue.extend(item["children"] or [])
|
||||
return folder_items
|
||||
|
||||
def _query_folder_entities(self, project_name, folder_ids):
|
||||
if not project_name or not folder_ids:
|
||||
return
|
||||
project_cache = self._folders_by_id[project_name]
|
||||
folders = ayon_api.get_folders(project_name, folder_ids=folder_ids)
|
||||
for folder in folders:
|
||||
folder_id = folder["id"]
|
||||
project_cache[folder_id].update_data(folder)
|
||||
|
||||
def _query_task_entities(self, project_name, task_ids):
|
||||
if not project_name or not task_ids:
|
||||
return
|
||||
|
||||
project_cache = self._tasks_by_id[project_name]
|
||||
tasks = ayon_api.get_tasks(project_name, task_ids=task_ids)
|
||||
for task in tasks:
|
||||
task_id = task["id"]
|
||||
project_cache[task_id].update_data(task)
|
||||
|
||||
def _refresh_tasks_cache(self, project_name, folder_id, sender=None):
|
||||
if folder_id in self._tasks_refreshing:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -29,13 +29,14 @@ class ProjectItem:
|
|||
is parent.
|
||||
"""
|
||||
|
||||
def __init__(self, name, active, icon=None):
|
||||
def __init__(self, name, active, is_library, icon=None):
|
||||
self.name = name
|
||||
self.active = active
|
||||
self.is_library = is_library
|
||||
if icon is None:
|
||||
icon = {
|
||||
"type": "awesome-font",
|
||||
"name": "fa.map",
|
||||
"name": "fa.book" if is_library else "fa.map",
|
||||
"color": get_default_entity_icon_color(),
|
||||
}
|
||||
self.icon = icon
|
||||
|
|
@ -50,6 +51,7 @@ class ProjectItem:
|
|||
return {
|
||||
"name": self.name,
|
||||
"active": self.active,
|
||||
"is_library": self.is_library,
|
||||
"icon": self.icon,
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +80,7 @@ def _get_project_items_from_entitiy(projects):
|
|||
"""
|
||||
|
||||
return [
|
||||
ProjectItem(project["name"], project["active"])
|
||||
ProjectItem(project["name"], project["active"], project["library"])
|
||||
for project in projects
|
||||
]
|
||||
|
||||
|
|
@ -141,5 +143,5 @@ class ProjectsModel(object):
|
|||
self._projects_cache.update_data(project_items)
|
||||
|
||||
def _query_projects(self):
|
||||
projects = ayon_api.get_projects(fields=["name", "active"])
|
||||
projects = ayon_api.get_projects(fields=["name", "active", "library"])
|
||||
return _get_project_items_from_entitiy(projects)
|
||||
|
|
|
|||
118
openpype/tools/ayon_utils/models/thumbnails.py
Normal file
118
openpype/tools/ayon_utils/models/thumbnails.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import collections
|
||||
|
||||
import ayon_api
|
||||
|
||||
from openpype.client.server.thumbnails import AYONThumbnailCache
|
||||
|
||||
from .cache import NestedCacheItem
|
||||
|
||||
|
||||
class ThumbnailsModel:
|
||||
entity_cache_lifetime = 240 # In seconds
|
||||
|
||||
def __init__(self):
|
||||
self._thumbnail_cache = AYONThumbnailCache()
|
||||
self._paths_cache = collections.defaultdict(dict)
|
||||
self._folders_cache = NestedCacheItem(
|
||||
levels=2, lifetime=self.entity_cache_lifetime)
|
||||
self._versions_cache = NestedCacheItem(
|
||||
levels=2, lifetime=self.entity_cache_lifetime)
|
||||
|
||||
def reset(self):
|
||||
self._paths_cache = collections.defaultdict(dict)
|
||||
self._folders_cache.reset()
|
||||
self._versions_cache.reset()
|
||||
|
||||
def get_thumbnail_path(self, project_name, thumbnail_id):
|
||||
return self._get_thumbnail_path(project_name, thumbnail_id)
|
||||
|
||||
def get_folder_thumbnail_ids(self, project_name, folder_ids):
|
||||
project_cache = self._folders_cache[project_name]
|
||||
output = {}
|
||||
missing_cache = set()
|
||||
for folder_id in folder_ids:
|
||||
cache = project_cache[folder_id]
|
||||
if cache.is_valid:
|
||||
output[folder_id] = cache.get_data()
|
||||
else:
|
||||
missing_cache.add(folder_id)
|
||||
self._query_folder_thumbnail_ids(project_name, missing_cache)
|
||||
for folder_id in missing_cache:
|
||||
cache = project_cache[folder_id]
|
||||
output[folder_id] = cache.get_data()
|
||||
return output
|
||||
|
||||
def get_version_thumbnail_ids(self, project_name, version_ids):
|
||||
project_cache = self._versions_cache[project_name]
|
||||
output = {}
|
||||
missing_cache = set()
|
||||
for version_id in version_ids:
|
||||
cache = project_cache[version_id]
|
||||
if cache.is_valid:
|
||||
output[version_id] = cache.get_data()
|
||||
else:
|
||||
missing_cache.add(version_id)
|
||||
self._query_version_thumbnail_ids(project_name, missing_cache)
|
||||
for version_id in missing_cache:
|
||||
cache = project_cache[version_id]
|
||||
output[version_id] = cache.get_data()
|
||||
return output
|
||||
|
||||
def _get_thumbnail_path(self, project_name, thumbnail_id):
|
||||
if not thumbnail_id:
|
||||
return None
|
||||
|
||||
project_cache = self._paths_cache[project_name]
|
||||
if thumbnail_id in project_cache:
|
||||
return project_cache[thumbnail_id]
|
||||
|
||||
filepath = self._thumbnail_cache.get_thumbnail_filepath(
|
||||
project_name, thumbnail_id
|
||||
)
|
||||
if filepath is not None:
|
||||
project_cache[thumbnail_id] = filepath
|
||||
return filepath
|
||||
|
||||
# 'ayon_api' had a bug, public function
|
||||
# 'get_thumbnail_by_id' did not return output of
|
||||
# 'ServerAPI' method.
|
||||
con = ayon_api.get_server_api_connection()
|
||||
result = con.get_thumbnail_by_id(project_name, thumbnail_id)
|
||||
if result is None:
|
||||
pass
|
||||
|
||||
elif result.is_valid:
|
||||
filepath = self._thumbnail_cache.store_thumbnail(
|
||||
project_name,
|
||||
thumbnail_id,
|
||||
result.content,
|
||||
result.content_type
|
||||
)
|
||||
project_cache[thumbnail_id] = filepath
|
||||
return filepath
|
||||
|
||||
def _query_folder_thumbnail_ids(self, project_name, folder_ids):
|
||||
if not project_name or not folder_ids:
|
||||
return
|
||||
|
||||
folders = ayon_api.get_folders(
|
||||
project_name,
|
||||
folder_ids=folder_ids,
|
||||
fields=["id", "thumbnailId"]
|
||||
)
|
||||
project_cache = self._folders_cache[project_name]
|
||||
for folder in folders:
|
||||
project_cache[folder["id"]] = folder["thumbnailId"]
|
||||
|
||||
def _query_version_thumbnail_ids(self, project_name, version_ids):
|
||||
if not project_name or not version_ids:
|
||||
return
|
||||
|
||||
versions = ayon_api.get_versions(
|
||||
project_name,
|
||||
version_ids=version_ids,
|
||||
fields=["id", "thumbnailId"]
|
||||
)
|
||||
project_cache = self._versions_cache[project_name]
|
||||
for version in versions:
|
||||
project_cache[version["id"]] = version["thumbnailId"]
|
||||
|
|
@ -8,11 +8,13 @@ from .projects_widget import (
|
|||
from .folders_widget import (
|
||||
FoldersWidget,
|
||||
FoldersModel,
|
||||
FOLDERS_MODEL_SENDER_NAME,
|
||||
)
|
||||
|
||||
from .tasks_widget import (
|
||||
TasksWidget,
|
||||
TasksModel,
|
||||
TASKS_MODEL_SENDER_NAME,
|
||||
)
|
||||
from .utils import (
|
||||
get_qt_icon,
|
||||
|
|
@ -28,9 +30,11 @@ __all__ = (
|
|||
|
||||
"FoldersWidget",
|
||||
"FoldersModel",
|
||||
"FOLDERS_MODEL_SENDER_NAME",
|
||||
|
||||
"TasksWidget",
|
||||
"TasksModel",
|
||||
"TASKS_MODEL_SENDER_NAME",
|
||||
|
||||
"get_qt_icon",
|
||||
"RefreshThread",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from openpype.tools.utils import (
|
|||
|
||||
from .utils import RefreshThread, get_qt_icon
|
||||
|
||||
SENDER_NAME = "qt_folders_model"
|
||||
FOLDERS_MODEL_SENDER_NAME = "qt_folders_model"
|
||||
ITEM_ID_ROLE = QtCore.Qt.UserRole + 1
|
||||
ITEM_NAME_ROLE = QtCore.Qt.UserRole + 2
|
||||
|
||||
|
|
@ -112,7 +112,7 @@ class FoldersModel(QtGui.QStandardItemModel):
|
|||
project_name,
|
||||
self._controller.get_folder_items,
|
||||
project_name,
|
||||
SENDER_NAME
|
||||
FOLDERS_MODEL_SENDER_NAME
|
||||
)
|
||||
self._current_refresh_thread = thread
|
||||
self._refresh_threads[thread.id] = thread
|
||||
|
|
@ -142,6 +142,21 @@ class FoldersModel(QtGui.QStandardItemModel):
|
|||
|
||||
self._fill_items(thread.get_result())
|
||||
|
||||
def _fill_item_data(self, item, folder_item):
|
||||
"""
|
||||
|
||||
Args:
|
||||
item (QtGui.QStandardItem): Item to fill data.
|
||||
folder_item (FolderItem): Folder item.
|
||||
"""
|
||||
|
||||
icon = get_qt_icon(folder_item.icon)
|
||||
item.setData(folder_item.entity_id, ITEM_ID_ROLE)
|
||||
item.setData(folder_item.name, ITEM_NAME_ROLE)
|
||||
item.setData(folder_item.label, QtCore.Qt.DisplayRole)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
|
||||
|
||||
def _fill_items(self, folder_items_by_id):
|
||||
if not folder_items_by_id:
|
||||
if folder_items_by_id is not None:
|
||||
|
|
@ -195,11 +210,7 @@ class FoldersModel(QtGui.QStandardItemModel):
|
|||
else:
|
||||
is_new = self._parent_id_by_id[item_id] != parent_id
|
||||
|
||||
icon = get_qt_icon(folder_item.icon)
|
||||
item.setData(item_id, ITEM_ID_ROLE)
|
||||
item.setData(folder_item.name, ITEM_NAME_ROLE)
|
||||
item.setData(folder_item.label, QtCore.Qt.DisplayRole)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
self._fill_item_data(item, folder_item)
|
||||
if is_new:
|
||||
new_items.append(item)
|
||||
self._items_by_id[item_id] = item
|
||||
|
|
@ -320,7 +331,7 @@ class FoldersWidget(QtWidgets.QWidget):
|
|||
self._folders_model.set_project_name(project_name)
|
||||
|
||||
def _on_folders_refresh_finished(self, event):
|
||||
if event["sender"] != SENDER_NAME:
|
||||
if event["sender"] != FOLDERS_MODEL_SENDER_NAME:
|
||||
self._set_project_name(event["project_name"])
|
||||
|
||||
def _on_controller_refresh(self):
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ from .utils import RefreshThread, get_qt_icon
|
|||
|
||||
PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1
|
||||
PROJECT_IS_ACTIVE_ROLE = QtCore.Qt.UserRole + 2
|
||||
PROJECT_IS_LIBRARY_ROLE = QtCore.Qt.UserRole + 3
|
||||
PROJECT_IS_CURRENT_ROLE = QtCore.Qt.UserRole + 4
|
||||
LIBRARY_PROJECT_SEPARATOR_ROLE = QtCore.Qt.UserRole + 5
|
||||
|
||||
|
||||
class ProjectsModel(QtGui.QStandardItemModel):
|
||||
|
|
@ -15,10 +18,23 @@ class ProjectsModel(QtGui.QStandardItemModel):
|
|||
self._controller = controller
|
||||
|
||||
self._project_items = {}
|
||||
self._has_libraries = False
|
||||
|
||||
self._empty_item = None
|
||||
self._empty_item_added = False
|
||||
|
||||
self._select_item = None
|
||||
self._select_item_added = False
|
||||
self._select_item_visible = None
|
||||
|
||||
self._libraries_sep_item = None
|
||||
self._libraries_sep_item_added = False
|
||||
self._libraries_sep_item_visible = False
|
||||
|
||||
self._current_context_project = None
|
||||
|
||||
self._selected_project = None
|
||||
|
||||
self._is_refreshing = False
|
||||
self._refresh_thread = None
|
||||
|
||||
|
|
@ -32,21 +48,63 @@ class ProjectsModel(QtGui.QStandardItemModel):
|
|||
def has_content(self):
|
||||
return len(self._project_items) > 0
|
||||
|
||||
def set_select_item_visible(self, visible):
|
||||
if self._select_item_visible is visible:
|
||||
return
|
||||
self._select_item_visible = visible
|
||||
|
||||
if self._selected_project is None:
|
||||
self._add_select_item()
|
||||
|
||||
def set_libraries_separator_visible(self, visible):
|
||||
if self._libraries_sep_item_visible is visible:
|
||||
return
|
||||
self._libraries_sep_item_visible = visible
|
||||
|
||||
def set_selected_project(self, project_name):
|
||||
if not self._select_item_visible:
|
||||
return
|
||||
|
||||
self._selected_project = project_name
|
||||
if project_name is None:
|
||||
self._add_select_item()
|
||||
else:
|
||||
self._remove_select_item()
|
||||
|
||||
def set_current_context_project(self, project_name):
|
||||
if project_name == self._current_context_project:
|
||||
return
|
||||
self._unset_current_context_project(self._current_context_project)
|
||||
self._current_context_project = project_name
|
||||
self._set_current_context_project(project_name)
|
||||
|
||||
def _set_current_context_project(self, project_name):
|
||||
item = self._project_items.get(project_name)
|
||||
if item is None:
|
||||
return
|
||||
item.setData(True, PROJECT_IS_CURRENT_ROLE)
|
||||
|
||||
def _unset_current_context_project(self, project_name):
|
||||
item = self._project_items.get(project_name)
|
||||
if item is None:
|
||||
return
|
||||
item.setData(False, PROJECT_IS_CURRENT_ROLE)
|
||||
|
||||
def _add_empty_item(self):
|
||||
if self._empty_item_added:
|
||||
return
|
||||
self._empty_item_added = True
|
||||
item = self._get_empty_item()
|
||||
if not self._empty_item_added:
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.appendRow(item)
|
||||
self._empty_item_added = True
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.appendRow(item)
|
||||
|
||||
def _remove_empty_item(self):
|
||||
if not self._empty_item_added:
|
||||
return
|
||||
|
||||
self._empty_item_added = False
|
||||
root_item = self.invisibleRootItem()
|
||||
item = self._get_empty_item()
|
||||
root_item.takeRow(item.row())
|
||||
self._empty_item_added = False
|
||||
|
||||
def _get_empty_item(self):
|
||||
if self._empty_item is None:
|
||||
|
|
@ -55,6 +113,61 @@ class ProjectsModel(QtGui.QStandardItemModel):
|
|||
self._empty_item = item
|
||||
return self._empty_item
|
||||
|
||||
def _get_library_sep_item(self):
|
||||
if self._libraries_sep_item is not None:
|
||||
return self._libraries_sep_item
|
||||
|
||||
item = QtGui.QStandardItem()
|
||||
item.setData("Libraries", QtCore.Qt.DisplayRole)
|
||||
item.setData(True, LIBRARY_PROJECT_SEPARATOR_ROLE)
|
||||
item.setFlags(QtCore.Qt.NoItemFlags)
|
||||
self._libraries_sep_item = item
|
||||
return item
|
||||
|
||||
def _add_library_sep_item(self):
|
||||
if (
|
||||
not self._libraries_sep_item_visible
|
||||
or self._libraries_sep_item_added
|
||||
):
|
||||
return
|
||||
self._libraries_sep_item_added = True
|
||||
item = self._get_library_sep_item()
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.appendRow(item)
|
||||
|
||||
def _remove_library_sep_item(self):
|
||||
if (
|
||||
not self._libraries_sep_item_added
|
||||
):
|
||||
return
|
||||
self._libraries_sep_item_added = False
|
||||
item = self._get_library_sep_item()
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.takeRow(item.row())
|
||||
|
||||
def _add_select_item(self):
|
||||
if self._select_item_added:
|
||||
return
|
||||
self._select_item_added = True
|
||||
item = self._get_select_item()
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.appendRow(item)
|
||||
|
||||
def _remove_select_item(self):
|
||||
if not self._select_item_added:
|
||||
return
|
||||
self._select_item_added = False
|
||||
root_item = self.invisibleRootItem()
|
||||
item = self._get_select_item()
|
||||
root_item.takeRow(item.row())
|
||||
|
||||
def _get_select_item(self):
|
||||
if self._select_item is None:
|
||||
item = QtGui.QStandardItem("< Select project >")
|
||||
item.setEditable(False)
|
||||
self._select_item = item
|
||||
return self._select_item
|
||||
|
||||
def _refresh(self):
|
||||
if self._is_refreshing:
|
||||
return
|
||||
|
|
@ -80,44 +193,118 @@ class ProjectsModel(QtGui.QStandardItemModel):
|
|||
self.refreshed.emit()
|
||||
|
||||
def _fill_items(self, project_items):
|
||||
items_to_remove = set(self._project_items.keys())
|
||||
new_project_names = {
|
||||
project_item.name
|
||||
for project_item in project_items
|
||||
}
|
||||
|
||||
# Handle "Select item" visibility
|
||||
if self._select_item_visible:
|
||||
# Add select project. if previously selected project is not in
|
||||
# project items
|
||||
if self._selected_project not in new_project_names:
|
||||
self._add_select_item()
|
||||
else:
|
||||
self._remove_select_item()
|
||||
|
||||
root_item = self.invisibleRootItem()
|
||||
|
||||
items_to_remove = set(self._project_items.keys()) - new_project_names
|
||||
for project_name in items_to_remove:
|
||||
item = self._project_items.pop(project_name)
|
||||
root_item.takeRow(item.row())
|
||||
|
||||
has_library_project = False
|
||||
new_items = []
|
||||
for project_item in project_items:
|
||||
project_name = project_item.name
|
||||
items_to_remove.discard(project_name)
|
||||
item = self._project_items.get(project_name)
|
||||
if project_item.is_library:
|
||||
has_library_project = True
|
||||
if item is None:
|
||||
item = QtGui.QStandardItem()
|
||||
item.setEditable(False)
|
||||
new_items.append(item)
|
||||
icon = get_qt_icon(project_item.icon)
|
||||
item.setData(project_name, QtCore.Qt.DisplayRole)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
item.setData(project_name, PROJECT_NAME_ROLE)
|
||||
item.setData(project_item.active, PROJECT_IS_ACTIVE_ROLE)
|
||||
item.setData(project_item.is_library, PROJECT_IS_LIBRARY_ROLE)
|
||||
is_current = project_name == self._current_context_project
|
||||
item.setData(is_current, PROJECT_IS_CURRENT_ROLE)
|
||||
self._project_items[project_name] = item
|
||||
|
||||
root_item = self.invisibleRootItem()
|
||||
self._set_current_context_project(self._current_context_project)
|
||||
|
||||
self._has_libraries = has_library_project
|
||||
|
||||
if new_items:
|
||||
root_item.appendRows(new_items)
|
||||
|
||||
for project_name in items_to_remove:
|
||||
item = self._project_items.pop(project_name)
|
||||
root_item.removeRow(item.row())
|
||||
|
||||
if self.has_content():
|
||||
# Make sure "No projects" item is removed
|
||||
self._remove_empty_item()
|
||||
if has_library_project:
|
||||
self._add_library_sep_item()
|
||||
else:
|
||||
self._remove_library_sep_item()
|
||||
else:
|
||||
# Keep only "No projects" item
|
||||
self._add_empty_item()
|
||||
self._remove_select_item()
|
||||
self._remove_library_sep_item()
|
||||
|
||||
|
||||
class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ProjectSortFilterProxy, self).__init__(*args, **kwargs)
|
||||
self._filter_inactive = True
|
||||
self._filter_standard = False
|
||||
self._filter_library = False
|
||||
self._sort_by_type = True
|
||||
# Disable case sensitivity
|
||||
self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
|
||||
def _type_sort(self, l_index, r_index):
|
||||
if not self._sort_by_type:
|
||||
return None
|
||||
|
||||
l_is_library = l_index.data(PROJECT_IS_LIBRARY_ROLE)
|
||||
r_is_library = r_index.data(PROJECT_IS_LIBRARY_ROLE)
|
||||
# Both hare project items
|
||||
if l_is_library is not None and r_is_library is not None:
|
||||
if l_is_library is r_is_library:
|
||||
return None
|
||||
if l_is_library:
|
||||
return False
|
||||
return True
|
||||
|
||||
if l_index.data(LIBRARY_PROJECT_SEPARATOR_ROLE):
|
||||
if r_is_library is None:
|
||||
return False
|
||||
return r_is_library
|
||||
|
||||
if r_index.data(LIBRARY_PROJECT_SEPARATOR_ROLE):
|
||||
if l_is_library is None:
|
||||
return True
|
||||
return l_is_library
|
||||
return None
|
||||
|
||||
def lessThan(self, left_index, right_index):
|
||||
# Current project always on top
|
||||
# - make sure this is always first, before any other sorting
|
||||
# e.g. type sort would move the item lower
|
||||
if left_index.data(PROJECT_IS_CURRENT_ROLE):
|
||||
return True
|
||||
if right_index.data(PROJECT_IS_CURRENT_ROLE):
|
||||
return False
|
||||
|
||||
# Library separator should be before library projects
|
||||
result = self._type_sort(left_index, right_index)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
if left_index.data(PROJECT_NAME_ROLE) is None:
|
||||
return True
|
||||
|
||||
|
|
@ -137,21 +324,43 @@ class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel):
|
|||
|
||||
def filterAcceptsRow(self, source_row, source_parent):
|
||||
index = self.sourceModel().index(source_row, 0, source_parent)
|
||||
project_name = index.data(PROJECT_NAME_ROLE)
|
||||
if project_name is None:
|
||||
return True
|
||||
|
||||
string_pattern = self.filterRegularExpression().pattern()
|
||||
if string_pattern:
|
||||
return string_pattern.lower() in project_name.lower()
|
||||
|
||||
# Current project keep always visible
|
||||
default = super(ProjectSortFilterProxy, self).filterAcceptsRow(
|
||||
source_row, source_parent
|
||||
)
|
||||
if not default:
|
||||
return default
|
||||
|
||||
# Make sure current project is visible
|
||||
if index.data(PROJECT_IS_CURRENT_ROLE):
|
||||
return True
|
||||
|
||||
if (
|
||||
self._filter_inactive
|
||||
and not index.data(PROJECT_IS_ACTIVE_ROLE)
|
||||
):
|
||||
return False
|
||||
|
||||
if string_pattern:
|
||||
project_name = index.data(PROJECT_IS_ACTIVE_ROLE)
|
||||
if project_name is not None:
|
||||
return string_pattern.lower() in project_name.lower()
|
||||
if (
|
||||
self._filter_standard
|
||||
and not index.data(PROJECT_IS_LIBRARY_ROLE)
|
||||
):
|
||||
return False
|
||||
|
||||
return super(ProjectSortFilterProxy, self).filterAcceptsRow(
|
||||
source_row, source_parent
|
||||
)
|
||||
if (
|
||||
self._filter_library
|
||||
and index.data(PROJECT_IS_LIBRARY_ROLE)
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _custom_index_filter(self, index):
|
||||
return bool(index.data(PROJECT_IS_ACTIVE_ROLE))
|
||||
|
|
@ -159,14 +368,34 @@ class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel):
|
|||
def is_active_filter_enabled(self):
|
||||
return self._filter_inactive
|
||||
|
||||
def set_active_filter_enabled(self, value):
|
||||
if self._filter_inactive == value:
|
||||
def set_active_filter_enabled(self, enabled):
|
||||
if self._filter_inactive == enabled:
|
||||
return
|
||||
self._filter_inactive = value
|
||||
self._filter_inactive = enabled
|
||||
self.invalidateFilter()
|
||||
|
||||
def set_library_filter_enabled(self, enabled):
|
||||
if self._filter_library == enabled:
|
||||
return
|
||||
self._filter_library = enabled
|
||||
self.invalidateFilter()
|
||||
|
||||
def set_standard_filter_enabled(self, enabled):
|
||||
if self._filter_standard == enabled:
|
||||
return
|
||||
self._filter_standard = enabled
|
||||
self.invalidateFilter()
|
||||
|
||||
def set_sort_by_type(self, enabled):
|
||||
if self._sort_by_type is enabled:
|
||||
return
|
||||
self._sort_by_type = enabled
|
||||
self.invalidate()
|
||||
|
||||
|
||||
class ProjectsCombobox(QtWidgets.QWidget):
|
||||
refreshed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent, handle_expected_selection=False):
|
||||
super(ProjectsCombobox, self).__init__(parent)
|
||||
|
||||
|
|
@ -203,6 +432,7 @@ class ProjectsCombobox(QtWidgets.QWidget):
|
|||
|
||||
self._controller = controller
|
||||
self._listen_selection_change = True
|
||||
self._select_item_visible = False
|
||||
|
||||
self._handle_expected_selection = handle_expected_selection
|
||||
self._expected_selection = None
|
||||
|
|
@ -264,17 +494,56 @@ class ProjectsCombobox(QtWidgets.QWidget):
|
|||
return None
|
||||
return self._projects_combobox.itemData(idx, PROJECT_NAME_ROLE)
|
||||
|
||||
def set_current_context_project(self, project_name):
|
||||
self._projects_model.set_current_context_project(project_name)
|
||||
self._projects_proxy_model.invalidateFilter()
|
||||
|
||||
def _update_select_item_visiblity(self, **kwargs):
|
||||
if not self._select_item_visible:
|
||||
return
|
||||
if "project_name" not in kwargs:
|
||||
project_name = self.get_current_project_name()
|
||||
else:
|
||||
project_name = kwargs.get("project_name")
|
||||
|
||||
# Hide the item if a project is selected
|
||||
self._projects_model.set_selected_project(project_name)
|
||||
|
||||
def set_select_item_visible(self, visible):
|
||||
self._select_item_visible = visible
|
||||
self._projects_model.set_select_item_visible(visible)
|
||||
self._update_select_item_visiblity()
|
||||
|
||||
def set_libraries_separator_visible(self, visible):
|
||||
self._projects_model.set_libraries_separator_visible(visible)
|
||||
|
||||
def is_active_filter_enabled(self):
|
||||
return self._projects_proxy_model.is_active_filter_enabled()
|
||||
|
||||
def set_active_filter_enabled(self, enabled):
|
||||
return self._projects_proxy_model.set_active_filter_enabled(enabled)
|
||||
|
||||
def set_standard_filter_enabled(self, enabled):
|
||||
return self._projects_proxy_model.set_standard_filter_enabled(enabled)
|
||||
|
||||
def set_library_filter_enabled(self, enabled):
|
||||
return self._projects_proxy_model.set_library_filter_enabled(enabled)
|
||||
|
||||
def _on_current_index_changed(self, idx):
|
||||
if not self._listen_selection_change:
|
||||
return
|
||||
project_name = self._projects_combobox.itemData(
|
||||
idx, PROJECT_NAME_ROLE)
|
||||
self._update_select_item_visiblity(project_name=project_name)
|
||||
self._controller.set_selected_project(project_name)
|
||||
|
||||
def _on_model_refresh(self):
|
||||
self._projects_proxy_model.sort(0)
|
||||
self._projects_proxy_model.invalidateFilter()
|
||||
if self._expected_selection:
|
||||
self._set_expected_selection()
|
||||
self._update_select_item_visiblity()
|
||||
self.refreshed.emit()
|
||||
|
||||
def _on_projects_refresh_finished(self, event):
|
||||
if event["sender"] != PROJECTS_MODEL_SENDER:
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from openpype.tools.utils import DeselectableTreeView
|
|||
|
||||
from .utils import RefreshThread, get_qt_icon
|
||||
|
||||
SENDER_NAME = "qt_tasks_model"
|
||||
TASKS_MODEL_SENDER_NAME = "qt_tasks_model"
|
||||
ITEM_ID_ROLE = QtCore.Qt.UserRole + 1
|
||||
PARENT_ID_ROLE = QtCore.Qt.UserRole + 2
|
||||
ITEM_NAME_ROLE = QtCore.Qt.UserRole + 3
|
||||
|
|
@ -362,7 +362,7 @@ class TasksWidget(QtWidgets.QWidget):
|
|||
|
||||
# Refresh only if current folder id is the same
|
||||
if (
|
||||
event["sender"] == SENDER_NAME
|
||||
event["sender"] == TASKS_MODEL_SENDER_NAME
|
||||
or event["folder_id"] != self._selected_folder_id
|
||||
):
|
||||
return
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ from .overlay_messages import (
|
|||
MessageOverlayObject,
|
||||
)
|
||||
from .multiselection_combobox import MultiSelectionComboBox
|
||||
from .thumbnail_paint_widget import ThumbnailPainterWidget
|
||||
|
||||
|
||||
__all__ = (
|
||||
|
|
@ -90,4 +91,6 @@ __all__ = (
|
|||
"MessageOverlayObject",
|
||||
|
||||
"MultiSelectionComboBox",
|
||||
|
||||
"ThumbnailPainterWidget",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -86,12 +86,22 @@ class HostToolsHelper:
|
|||
def get_loader_tool(self, parent):
|
||||
"""Create, cache and return loader tool window."""
|
||||
if self._loader_tool is None:
|
||||
from openpype.tools.loader import LoaderWindow
|
||||
|
||||
host = registered_host()
|
||||
ILoadHost.validate_load_methods(host)
|
||||
if AYON_SERVER_ENABLED:
|
||||
from openpype.tools.ayon_loader.ui import LoaderWindow
|
||||
from openpype.tools.ayon_loader import LoaderController
|
||||
|
||||
loader_window = LoaderWindow(parent=parent or self._parent)
|
||||
controller = LoaderController(host=host)
|
||||
loader_window = LoaderWindow(
|
||||
controller=controller,
|
||||
parent=parent or self._parent
|
||||
)
|
||||
|
||||
else:
|
||||
from openpype.tools.loader import LoaderWindow
|
||||
|
||||
loader_window = LoaderWindow(parent=parent or self._parent)
|
||||
self._loader_tool = loader_window
|
||||
|
||||
return self._loader_tool
|
||||
|
|
@ -109,7 +119,7 @@ class HostToolsHelper:
|
|||
if use_context is None:
|
||||
use_context = False
|
||||
|
||||
if use_context:
|
||||
if not AYON_SERVER_ENABLED and use_context:
|
||||
context = {"asset": get_current_asset_name()}
|
||||
loader_tool.set_context(context, refresh=True)
|
||||
else:
|
||||
|
|
@ -187,6 +197,9 @@ class HostToolsHelper:
|
|||
|
||||
def get_library_loader_tool(self, parent):
|
||||
"""Create, cache and return library loader tool window."""
|
||||
if AYON_SERVER_ENABLED:
|
||||
return self.get_loader_tool(parent)
|
||||
|
||||
if self._library_loader_tool is None:
|
||||
from openpype.tools.libraryloader import LibraryLoaderWindow
|
||||
|
||||
|
|
@ -199,6 +212,9 @@ class HostToolsHelper:
|
|||
|
||||
def show_library_loader(self, parent=None):
|
||||
"""Loader tool for loading representations from library project."""
|
||||
if AYON_SERVER_ENABLED:
|
||||
return self.show_loader(parent)
|
||||
|
||||
with qt_app_context():
|
||||
library_loader_tool = self.get_library_loader_tool(parent)
|
||||
library_loader_tool.show()
|
||||
|
|
|
|||
56
openpype/tools/utils/images/__init__.py
Normal file
56
openpype/tools/utils/images/__init__.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import os
|
||||
from qtpy import QtGui
|
||||
|
||||
IMAGES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def get_image_path(filename):
|
||||
"""Get image path from './images'.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Path to image file or None if not found.
|
||||
"""
|
||||
|
||||
path = os.path.join(IMAGES_DIR, filename)
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def get_image(filename):
|
||||
"""Load image from './images' as QImage.
|
||||
|
||||
Returns:
|
||||
Union[QtGui.QImage, None]: QImage or None if not found.
|
||||
"""
|
||||
|
||||
path = get_image_path(filename)
|
||||
if path:
|
||||
return QtGui.QImage(path)
|
||||
return None
|
||||
|
||||
|
||||
def get_pixmap(filename):
|
||||
"""Load image from './images' as QPixmap.
|
||||
|
||||
Returns:
|
||||
Union[QtGui.QPixmap, None]: QPixmap or None if not found.
|
||||
"""
|
||||
|
||||
path = get_image_path(filename)
|
||||
if path:
|
||||
return QtGui.QPixmap(path)
|
||||
return None
|
||||
|
||||
|
||||
def get_icon(filename):
|
||||
"""Load image from './images' as QIcon.
|
||||
|
||||
Returns:
|
||||
Union[QtGui.QIcon, None]: QIcon or None if not found.
|
||||
"""
|
||||
|
||||
pix = get_pixmap(filename)
|
||||
if pix:
|
||||
return QtGui.QIcon(pix)
|
||||
return None
|
||||
BIN
openpype/tools/utils/images/thumbnail.png
Normal file
BIN
openpype/tools/utils/images/thumbnail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5 KiB |
366
openpype/tools/utils/thumbnail_paint_widget.py
Normal file
366
openpype/tools/utils/thumbnail_paint_widget.py
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype.style import get_objected_colors
|
||||
|
||||
from .lib import paint_image_with_color
|
||||
from .images import get_image
|
||||
|
||||
|
||||
class ThumbnailPainterWidget(QtWidgets.QWidget):
|
||||
"""Widget for painting of thumbnails.
|
||||
|
||||
The widget use is to paint thumbnail or multiple thumbnails in a defined
|
||||
area. Is not meant to show them in a grid but in overlay.
|
||||
|
||||
It is expected that there is a logic that will provide thumbnails to
|
||||
paint and set them using 'set_current_thumbnails' or
|
||||
'set_current_thumbnail_paths'.
|
||||
"""
|
||||
|
||||
width_ratio = 3.0
|
||||
height_ratio = 2.0
|
||||
border_width = 1
|
||||
max_thumbnails = 3
|
||||
offset_sep = 4
|
||||
checker_boxes_count = 20
|
||||
|
||||
def __init__(self, parent):
|
||||
super(ThumbnailPainterWidget, self).__init__(parent)
|
||||
|
||||
border_color = get_objected_colors("bg-buttons").get_qcolor()
|
||||
thumbnail_bg_color = get_objected_colors("bg-view").get_qcolor()
|
||||
|
||||
default_image = get_image("thumbnail.png")
|
||||
default_pix = paint_image_with_color(default_image, border_color)
|
||||
|
||||
self._border_color = border_color
|
||||
self._thumbnail_bg_color = thumbnail_bg_color
|
||||
self._default_pix = default_pix
|
||||
|
||||
self._cached_pix = None
|
||||
self._current_pixes = None
|
||||
self._has_pixes = False
|
||||
|
||||
self._bg_color = QtCore.Qt.transparent
|
||||
self._use_checker = True
|
||||
self._checker_color_1 = QtGui.QColor(89, 89, 89)
|
||||
self._checker_color_2 = QtGui.QColor(188, 187, 187)
|
||||
|
||||
def set_background_color(self, color):
|
||||
self._bg_color = color
|
||||
self.repaint()
|
||||
|
||||
def set_use_checkboard(self, use_checker):
|
||||
if self._use_checker is use_checker:
|
||||
return
|
||||
self._use_checker = use_checker
|
||||
self.repaint()
|
||||
|
||||
def set_checker_colors(self, color_1, color_2):
|
||||
self._checker_color_1 = color_1
|
||||
self._checker_color_2 = color_2
|
||||
self.repaint()
|
||||
|
||||
def set_border_color(self, color):
|
||||
"""Change border color.
|
||||
|
||||
Args:
|
||||
color (QtGui.QColor): Color to set.
|
||||
"""
|
||||
|
||||
self._border_color = color
|
||||
self._default_pix = None
|
||||
self.clear_cache()
|
||||
|
||||
def set_thumbnail_bg_color(self, color):
|
||||
"""Change background color.
|
||||
|
||||
Args:
|
||||
color (QtGui.QColor): Color to set.
|
||||
"""
|
||||
|
||||
self._thumbnail_bg_color = color
|
||||
self.clear_cache()
|
||||
|
||||
@property
|
||||
def has_pixes(self):
|
||||
"""Has set thumbnails.
|
||||
|
||||
Returns:
|
||||
bool: True if widget has thumbnails to paint.
|
||||
"""
|
||||
|
||||
return self._has_pixes
|
||||
|
||||
def clear_cache(self):
|
||||
"""Clear cache of resized thumbnails and repaint widget."""
|
||||
|
||||
self._cached_pix = None
|
||||
self.repaint()
|
||||
|
||||
def set_current_thumbnails(self, pixmaps=None):
|
||||
"""Set current thumbnails.
|
||||
|
||||
Args:
|
||||
pixmaps (Optional[List[QtGui.QPixmap]]): List of pixmaps.
|
||||
"""
|
||||
|
||||
self._current_pixes = pixmaps or None
|
||||
self._has_pixes = self._current_pixes is not None
|
||||
self.clear_cache()
|
||||
|
||||
def set_current_thumbnail_paths(self, thumbnail_paths=None):
|
||||
"""Set current thumbnails.
|
||||
|
||||
Set current thumbnails using paths to a files.
|
||||
|
||||
Args:
|
||||
thumbnail_paths (Optional[List[str]]): List of paths to thumbnail
|
||||
sources.
|
||||
"""
|
||||
|
||||
pixes = []
|
||||
if thumbnail_paths:
|
||||
for thumbnail_path in thumbnail_paths:
|
||||
pixes.append(QtGui.QPixmap(thumbnail_path))
|
||||
|
||||
self.set_current_thumbnails(pixes)
|
||||
|
||||
def paintEvent(self, event):
|
||||
if self._cached_pix is None:
|
||||
self._cache_pix()
|
||||
|
||||
painter = QtGui.QPainter()
|
||||
painter.begin(self)
|
||||
painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
painter.drawPixmap(0, 0, self._cached_pix)
|
||||
painter.end()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
self._cached_pix = None
|
||||
super(ThumbnailPainterWidget, self).resizeEvent(event)
|
||||
|
||||
def _get_default_pix(self):
|
||||
if self._default_pix is None:
|
||||
default_image = get_image("thumbnail")
|
||||
default_pix = paint_image_with_color(
|
||||
default_image, self._border_color)
|
||||
self._default_pix = default_pix
|
||||
return self._default_pix
|
||||
|
||||
def _paint_tile(self, width, height):
|
||||
if not self._use_checker:
|
||||
tile_pix = QtGui.QPixmap(width, width)
|
||||
tile_pix.fill(self._bg_color)
|
||||
return tile_pix
|
||||
|
||||
checker_size = int(float(width) / self.checker_boxes_count)
|
||||
if checker_size < 1:
|
||||
checker_size = 1
|
||||
|
||||
checker_pix = QtGui.QPixmap(checker_size * 2, checker_size * 2)
|
||||
checker_pix.fill(QtCore.Qt.transparent)
|
||||
checker_painter = QtGui.QPainter()
|
||||
checker_painter.begin(checker_pix)
|
||||
checker_painter.setPen(QtCore.Qt.NoPen)
|
||||
checker_painter.setBrush(self._checker_color_1)
|
||||
checker_painter.drawRect(
|
||||
0, 0, checker_pix.width(), checker_pix.height()
|
||||
)
|
||||
checker_painter.setBrush(self._checker_color_2)
|
||||
checker_painter.drawRect(
|
||||
0, 0, checker_size, checker_size
|
||||
)
|
||||
checker_painter.drawRect(
|
||||
checker_size, checker_size, checker_size, checker_size
|
||||
)
|
||||
checker_painter.end()
|
||||
return checker_pix
|
||||
|
||||
def _paint_default_pix(self, pix_width, pix_height):
|
||||
full_border_width = 2 * self.border_width
|
||||
width = pix_width - full_border_width
|
||||
height = pix_height - full_border_width
|
||||
if width > 100:
|
||||
width = int(width * 0.6)
|
||||
height = int(height * 0.6)
|
||||
|
||||
scaled_pix = self._get_default_pix().scaled(
|
||||
width,
|
||||
height,
|
||||
QtCore.Qt.KeepAspectRatio,
|
||||
QtCore.Qt.SmoothTransformation
|
||||
)
|
||||
pos_x = int(
|
||||
(pix_width - scaled_pix.width()) / 2
|
||||
)
|
||||
pos_y = int(
|
||||
(pix_height - scaled_pix.height()) / 2
|
||||
)
|
||||
new_pix = QtGui.QPixmap(pix_width, pix_height)
|
||||
new_pix.fill(QtCore.Qt.transparent)
|
||||
pix_painter = QtGui.QPainter()
|
||||
pix_painter.begin(new_pix)
|
||||
render_hints = (
|
||||
QtGui.QPainter.Antialiasing
|
||||
| QtGui.QPainter.SmoothPixmapTransform
|
||||
)
|
||||
if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
|
||||
render_hints |= QtGui.QPainter.HighQualityAntialiasing
|
||||
|
||||
pix_painter.setRenderHints(render_hints)
|
||||
pix_painter.drawPixmap(pos_x, pos_y, scaled_pix)
|
||||
pix_painter.end()
|
||||
return new_pix
|
||||
|
||||
def _draw_thumbnails(self, thumbnails, pix_width, pix_height):
|
||||
full_border_width = 2 * self.border_width
|
||||
|
||||
checker_pix = self._paint_tile(pix_width, pix_height)
|
||||
|
||||
backgrounded_images = []
|
||||
for src_pix in thumbnails:
|
||||
scaled_pix = src_pix.scaled(
|
||||
pix_width - full_border_width,
|
||||
pix_height - full_border_width,
|
||||
QtCore.Qt.KeepAspectRatio,
|
||||
QtCore.Qt.SmoothTransformation
|
||||
)
|
||||
pos_x = int(
|
||||
(pix_width - scaled_pix.width()) / 2
|
||||
)
|
||||
pos_y = int(
|
||||
(pix_height - scaled_pix.height()) / 2
|
||||
)
|
||||
|
||||
new_pix = QtGui.QPixmap(pix_width, pix_height)
|
||||
new_pix.fill(QtCore.Qt.transparent)
|
||||
pix_painter = QtGui.QPainter()
|
||||
pix_painter.begin(new_pix)
|
||||
render_hints = (
|
||||
QtGui.QPainter.Antialiasing
|
||||
| QtGui.QPainter.SmoothPixmapTransform
|
||||
)
|
||||
if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
|
||||
render_hints |= QtGui.QPainter.HighQualityAntialiasing
|
||||
pix_painter.setRenderHints(render_hints)
|
||||
|
||||
tiled_rect = QtCore.QRectF(
|
||||
pos_x, pos_y, scaled_pix.width(), scaled_pix.height()
|
||||
)
|
||||
pix_painter.drawTiledPixmap(
|
||||
tiled_rect,
|
||||
checker_pix,
|
||||
QtCore.QPointF(0.0, 0.0)
|
||||
)
|
||||
pix_painter.drawPixmap(pos_x, pos_y, scaled_pix)
|
||||
pix_painter.end()
|
||||
backgrounded_images.append(new_pix)
|
||||
return backgrounded_images
|
||||
|
||||
def _paint_dash_line(self, painter, rect):
|
||||
pen = QtGui.QPen()
|
||||
pen.setWidth(1)
|
||||
pen.setBrush(QtCore.Qt.darkGray)
|
||||
pen.setStyle(QtCore.Qt.DashLine)
|
||||
|
||||
new_rect = rect.adjusted(1, 1, -1, -1)
|
||||
painter.setPen(pen)
|
||||
painter.setBrush(QtCore.Qt.transparent)
|
||||
# painter.drawRect(rect)
|
||||
painter.drawRect(new_rect)
|
||||
|
||||
def _cache_pix(self):
|
||||
rect = self.rect()
|
||||
rect_width = rect.width()
|
||||
rect_height = rect.height()
|
||||
|
||||
pix_x_offset = 0
|
||||
pix_y_offset = 0
|
||||
expected_height = int(
|
||||
(rect_width / self.width_ratio) * self.height_ratio
|
||||
)
|
||||
if expected_height > rect_height:
|
||||
expected_height = rect_height
|
||||
expected_width = int(
|
||||
(rect_height / self.height_ratio) * self.width_ratio
|
||||
)
|
||||
pix_x_offset = (rect_width - expected_width) / 2
|
||||
else:
|
||||
expected_width = rect_width
|
||||
pix_y_offset = (rect_height - expected_height) / 2
|
||||
|
||||
if self._current_pixes is None:
|
||||
used_default_pix = True
|
||||
pixes_to_draw = None
|
||||
pixes_len = 1
|
||||
else:
|
||||
used_default_pix = False
|
||||
pixes_to_draw = self._current_pixes
|
||||
if len(pixes_to_draw) > self.max_thumbnails:
|
||||
pixes_to_draw = pixes_to_draw[:-self.max_thumbnails]
|
||||
pixes_len = len(pixes_to_draw)
|
||||
|
||||
width_offset, height_offset = self._get_pix_offset_size(
|
||||
expected_width, expected_height, pixes_len
|
||||
)
|
||||
pix_width = expected_width - width_offset
|
||||
pix_height = expected_height - height_offset
|
||||
|
||||
if used_default_pix:
|
||||
thumbnail_images = [self._paint_default_pix(pix_width, pix_height)]
|
||||
else:
|
||||
thumbnail_images = self._draw_thumbnails(
|
||||
pixes_to_draw, pix_width, pix_height
|
||||
)
|
||||
|
||||
if pixes_len == 1:
|
||||
width_offset_part = 0
|
||||
height_offset_part = 0
|
||||
else:
|
||||
width_offset_part = int(float(width_offset) / (pixes_len - 1))
|
||||
height_offset_part = int(float(height_offset) / (pixes_len - 1))
|
||||
full_width_offset = width_offset + pix_x_offset
|
||||
|
||||
final_pix = QtGui.QPixmap(rect_width, rect_height)
|
||||
final_pix.fill(QtCore.Qt.transparent)
|
||||
|
||||
bg_pen = QtGui.QPen()
|
||||
bg_pen.setWidth(self.border_width)
|
||||
bg_pen.setColor(self._border_color)
|
||||
|
||||
final_painter = QtGui.QPainter()
|
||||
final_painter.begin(final_pix)
|
||||
render_hints = (
|
||||
QtGui.QPainter.Antialiasing
|
||||
| QtGui.QPainter.SmoothPixmapTransform
|
||||
)
|
||||
if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
|
||||
render_hints |= QtGui.QPainter.HighQualityAntialiasing
|
||||
|
||||
final_painter.setRenderHints(render_hints)
|
||||
|
||||
final_painter.setBrush(QtGui.QBrush(self._thumbnail_bg_color))
|
||||
final_painter.setPen(bg_pen)
|
||||
final_painter.drawRect(rect)
|
||||
|
||||
for idx, pix in enumerate(thumbnail_images):
|
||||
x_offset = full_width_offset - (width_offset_part * idx)
|
||||
y_offset = (height_offset_part * idx) + pix_y_offset
|
||||
final_painter.drawPixmap(x_offset, y_offset, pix)
|
||||
|
||||
# Draw drop enabled dashes
|
||||
if used_default_pix:
|
||||
self._paint_dash_line(final_painter, rect)
|
||||
|
||||
final_painter.end()
|
||||
|
||||
self._cached_pix = final_pix
|
||||
|
||||
def _get_pix_offset_size(self, width, height, image_count):
|
||||
if image_count == 1:
|
||||
return 0, 0
|
||||
|
||||
part_width = width / self.offset_sep
|
||||
part_height = height / self.offset_sep
|
||||
return part_width, part_height
|
||||
|
|
@ -68,7 +68,7 @@ class TestPublishInNuke(NukeLocalPublishTestClass):
|
|||
name="workfileTest_task"))
|
||||
|
||||
failures.append(
|
||||
DBAssert.count_of_types(dbcon, "representation", 4))
|
||||
DBAssert.count_of_types(dbcon, "representation", 3))
|
||||
|
||||
additional_args = {"context.subset": "workfileTest_task",
|
||||
"context.ext": "nk"}
|
||||
|
|
@ -85,7 +85,7 @@ class TestPublishInNuke(NukeLocalPublishTestClass):
|
|||
additional_args = {"context.subset": "renderTest_taskMain",
|
||||
"name": "thumbnail"}
|
||||
failures.append(
|
||||
DBAssert.count_of_types(dbcon, "representation", 1,
|
||||
DBAssert.count_of_types(dbcon, "representation", 0,
|
||||
additional_args=additional_args))
|
||||
|
||||
additional_args = {"context.subset": "renderTest_taskMain",
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ class ModuleUnitTest(BaseTest):
|
|||
yield path
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def env_var(self, monkeypatch_session, download_test_data):
|
||||
def env_var(self, monkeypatch_session, download_test_data, mongo_url):
|
||||
"""Sets temporary env vars from json file."""
|
||||
env_url = os.path.join(download_test_data, "input",
|
||||
"env_vars", "env_var.json")
|
||||
|
|
@ -129,6 +129,9 @@ class ModuleUnitTest(BaseTest):
|
|||
monkeypatch_session.setenv(key, str(value))
|
||||
|
||||
#reset connection to openpype DB with new env var
|
||||
if mongo_url:
|
||||
monkeypatch_session.setenv("OPENPYPE_MONGO", mongo_url)
|
||||
|
||||
import openpype.settings.lib as sett_lib
|
||||
sett_lib._SETTINGS_HANDLER = None
|
||||
sett_lib._LOCAL_SETTINGS_HANDLER = None
|
||||
|
|
@ -150,8 +153,7 @@ class ModuleUnitTest(BaseTest):
|
|||
request, mongo_url):
|
||||
"""Restore prepared MongoDB dumps into selected DB."""
|
||||
backup_dir = os.path.join(download_test_data, "input", "dumps")
|
||||
|
||||
uri = mongo_url or os.environ.get("OPENPYPE_MONGO")
|
||||
uri = os.environ.get("OPENPYPE_MONGO")
|
||||
db_handler = DBHandler(uri)
|
||||
db_handler.setup_from_dump(self.TEST_DB_NAME, backup_dir,
|
||||
overwrite=True,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue