Merge branch 'develop' into enhancement/OP-6154_Publishing-Luts

This commit is contained in:
Jakub Ježek 2023-10-12 14:22:14 +02:00 committed by GitHub
commit b06b45dbc1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 6916 additions and 112 deletions

View file

@ -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)

View file

@ -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

View file

@ -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)
)

View file

@ -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",

View file

@ -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."""

View file

@ -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
)

View file

@ -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"]))

View file

@ -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

View file

@ -0,0 +1,6 @@
from .control import LoaderController
__all__ = (
"LoaderController",
)

View 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

View 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")

View file

@ -0,0 +1,10 @@
from .selection import SelectionModel
from .products import ProductsModel
from .actions import LoaderActionsModel
__all__ = (
"SelectionModel",
"ProductsModel",
"LoaderActionsModel",
)

View 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

View 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)

View 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,
}
)

View file

@ -0,0 +1,6 @@
from .window import LoaderWindow
__all__ = (
"LoaderWindow",
)

View 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)

View 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)

View 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)

View 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()

View 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)

View 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)

View 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)

View 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()

View 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,
)

View 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()

View file

@ -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",
)

View file

@ -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

View file

@ -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)

View 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"]

View file

@ -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",

View file

@ -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):

View file

@ -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:

View file

@ -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

View file

@ -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",
)

View file

@ -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()

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

View 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

View file

@ -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",

View file

@ -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,