mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into enhancement/allow_disabling_removal_of_rendereed_files_on_farm
This commit is contained in:
commit
a3d3865e78
42 changed files with 814 additions and 858 deletions
|
|
@ -1,12 +1,15 @@
|
|||
from ayon_api import get_project, get_folder_by_path, get_task_by_name
|
||||
|
||||
from ayon_core.pipeline import Anatomy
|
||||
from ayon_core.pipeline.anatomy import RootMissingEnv
|
||||
|
||||
from ayon_applications import PreLaunchHook
|
||||
from ayon_applications.exceptions import ApplicationLaunchFailed
|
||||
from ayon_applications.utils import (
|
||||
EnvironmentPrepData,
|
||||
prepare_app_environments,
|
||||
prepare_context_environments
|
||||
)
|
||||
from ayon_core.pipeline import Anatomy
|
||||
|
||||
|
||||
class GlobalHostDataHook(PreLaunchHook):
|
||||
|
|
@ -67,9 +70,12 @@ class GlobalHostDataHook(PreLaunchHook):
|
|||
self.data["project_entity"] = project_entity
|
||||
|
||||
# Anatomy
|
||||
self.data["anatomy"] = Anatomy(
|
||||
project_name, project_entity=project_entity
|
||||
)
|
||||
try:
|
||||
self.data["anatomy"] = Anatomy(
|
||||
project_name, project_entity=project_entity
|
||||
)
|
||||
except RootMissingEnv as exc:
|
||||
raise ApplicationLaunchFailed(str(exc))
|
||||
|
||||
folder_path = self.data.get("folder_path")
|
||||
if not folder_path:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from .exceptions import (
|
||||
ProjectNotSet,
|
||||
RootMissingEnv,
|
||||
RootCombinationError,
|
||||
TemplateMissingKey,
|
||||
AnatomyTemplateUnsolved,
|
||||
|
|
@ -9,6 +10,7 @@ from .anatomy import Anatomy
|
|||
|
||||
__all__ = (
|
||||
"ProjectNotSet",
|
||||
"RootMissingEnv",
|
||||
"RootCombinationError",
|
||||
"TemplateMissingKey",
|
||||
"AnatomyTemplateUnsolved",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@ class ProjectNotSet(Exception):
|
|||
"""Exception raised when is created Anatomy without project name."""
|
||||
|
||||
|
||||
class RootMissingEnv(KeyError):
|
||||
"""Raised when root requires environment variables which is not filled."""
|
||||
pass
|
||||
|
||||
|
||||
class RootCombinationError(Exception):
|
||||
"""This exception is raised when templates has combined root types."""
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ import os
|
|||
import platform
|
||||
import numbers
|
||||
|
||||
from ayon_core.lib import Logger
|
||||
from ayon_core.lib import Logger, StringTemplate
|
||||
from ayon_core.lib.path_templates import FormatObject
|
||||
|
||||
from .exceptions import RootMissingEnv
|
||||
|
||||
|
||||
class RootItem(FormatObject):
|
||||
"""Represents one item or roots.
|
||||
|
|
@ -21,18 +23,36 @@ class RootItem(FormatObject):
|
|||
multi root setup otherwise None value is expected.
|
||||
"""
|
||||
def __init__(self, parent, root_raw_data, name):
|
||||
super(RootItem, self).__init__()
|
||||
super().__init__()
|
||||
self._log = None
|
||||
lowered_platform_keys = {}
|
||||
for key, value in root_raw_data.items():
|
||||
lowered_platform_keys[key.lower()] = value
|
||||
lowered_platform_keys = {
|
||||
key.lower(): value
|
||||
for key, value in root_raw_data.items()
|
||||
}
|
||||
self.raw_data = lowered_platform_keys
|
||||
self.cleaned_data = self._clean_roots(lowered_platform_keys)
|
||||
self.name = name
|
||||
self.parent = parent
|
||||
|
||||
self.available_platforms = set(lowered_platform_keys.keys())
|
||||
self.value = lowered_platform_keys.get(platform.system().lower())
|
||||
|
||||
current_platform = platform.system().lower()
|
||||
# WARNING: Using environment variables in roots is not considered
|
||||
# as production safe. Some features may not work as expected, for
|
||||
# example USD resolver or site sync.
|
||||
try:
|
||||
self.value = lowered_platform_keys[current_platform].format_map(
|
||||
os.environ
|
||||
)
|
||||
except KeyError:
|
||||
result = StringTemplate(self.value).format(os.environ.copy())
|
||||
is_are = "is" if len(result.missing_keys) == 1 else "are"
|
||||
missing_keys = ", ".join(result.missing_keys)
|
||||
raise RootMissingEnv(
|
||||
f"Root \"{name}\" requires environment variable/s"
|
||||
f" {missing_keys} which {is_are} not available."
|
||||
)
|
||||
|
||||
self.clean_value = self._clean_root(self.value)
|
||||
|
||||
def __format__(self, *args, **kwargs):
|
||||
|
|
@ -105,10 +125,10 @@ class RootItem(FormatObject):
|
|||
|
||||
def _clean_roots(self, raw_data):
|
||||
"""Clean all values of raw root item values."""
|
||||
cleaned = {}
|
||||
for key, value in raw_data.items():
|
||||
cleaned[key] = self._clean_root(value)
|
||||
return cleaned
|
||||
return {
|
||||
key: self._clean_root(value)
|
||||
for key, value in raw_data.items()
|
||||
}
|
||||
|
||||
def path_remapper(self, path, dst_platform=None, src_platform=None):
|
||||
"""Remap path for specific platform.
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ from .workfile import (
|
|||
get_workdir,
|
||||
get_custom_workfile_template_by_string_context,
|
||||
get_workfile_template_key_from_context,
|
||||
get_last_workfile
|
||||
get_last_workfile,
|
||||
MissingWorkdirError,
|
||||
)
|
||||
from . import (
|
||||
register_loader_plugin_path,
|
||||
|
|
@ -251,7 +252,7 @@ def uninstall_host():
|
|||
pyblish.api.deregister_discovery_filter(filter_pyblish_plugins)
|
||||
deregister_loader_plugin_path(LOAD_PATH)
|
||||
deregister_inventory_action_path(INVENTORY_PATH)
|
||||
log.info("Global plug-ins unregistred")
|
||||
log.info("Global plug-ins unregistered")
|
||||
|
||||
deregister_host()
|
||||
|
||||
|
|
@ -617,7 +618,18 @@ def version_up_current_workfile():
|
|||
last_workfile_path = get_last_workfile(
|
||||
work_root, file_template, data, extensions, True
|
||||
)
|
||||
new_workfile_path = version_up(last_workfile_path)
|
||||
# `get_last_workfile` will return the first expected file version
|
||||
# if no files exist yet. In that case, if they do not exist we will
|
||||
# want to save v001
|
||||
new_workfile_path = last_workfile_path
|
||||
if os.path.exists(new_workfile_path):
|
||||
new_workfile_path = version_up(new_workfile_path)
|
||||
|
||||
# Raise an error if the parent folder doesn't exist as `host.save_workfile`
|
||||
# is not supposed/able to create missing folders.
|
||||
parent_folder = os.path.dirname(new_workfile_path)
|
||||
if not os.path.exists(parent_folder):
|
||||
raise MissingWorkdirError(
|
||||
f"Work area directory '{parent_folder}' does not exist.")
|
||||
|
||||
host.save_workfile(new_workfile_path)
|
||||
|
|
|
|||
|
|
@ -2303,10 +2303,16 @@ class CreateContext:
|
|||
for plugin_name, plugin_value in item_changes.pop(
|
||||
"publish_attributes"
|
||||
).items():
|
||||
if plugin_value is None:
|
||||
current_publish[plugin_name] = None
|
||||
continue
|
||||
plugin_changes = current_publish.setdefault(
|
||||
plugin_name, {}
|
||||
)
|
||||
plugin_changes.update(plugin_value)
|
||||
if plugin_changes is None:
|
||||
current_publish[plugin_name] = plugin_value
|
||||
else:
|
||||
plugin_changes.update(plugin_value)
|
||||
|
||||
item_values.update(item_changes)
|
||||
|
||||
|
|
|
|||
|
|
@ -160,29 +160,26 @@ class AttributeValues:
|
|||
return self._attr_defs_by_key.get(key, default)
|
||||
|
||||
def update(self, value):
|
||||
changes = {}
|
||||
for _key, _value in dict(value).items():
|
||||
if _key in self._data and self._data.get(_key) == _value:
|
||||
continue
|
||||
self._data[_key] = _value
|
||||
changes[_key] = _value
|
||||
|
||||
changes = self._update(value)
|
||||
if changes:
|
||||
self._parent.attribute_value_changed(self._key, changes)
|
||||
|
||||
def pop(self, key, default=None):
|
||||
has_key = key in self._data
|
||||
value = self._data.pop(key, default)
|
||||
# Remove attribute definition if is 'UnknownDef'
|
||||
# - gives option to get rid of unknown values
|
||||
attr_def = self._attr_defs_by_key.get(key)
|
||||
if isinstance(attr_def, UnknownDef):
|
||||
self._attr_defs_by_key.pop(key)
|
||||
self._attr_defs.remove(attr_def)
|
||||
elif has_key:
|
||||
self._parent.attribute_value_changed(self._key, {key: None})
|
||||
value, changes = self._pop(key, default)
|
||||
if changes:
|
||||
self._parent.attribute_value_changed(self._key, changes)
|
||||
return value
|
||||
|
||||
def set_value(self, value):
|
||||
pop_keys = set(value.keys()) - set(self._data.keys())
|
||||
changes = self._update(value)
|
||||
for key in pop_keys:
|
||||
_, key_changes = self._pop(key, None)
|
||||
changes.update(key_changes)
|
||||
|
||||
if changes:
|
||||
self._parent.attribute_value_changed(self._key, changes)
|
||||
|
||||
def reset_values(self):
|
||||
self._data = {}
|
||||
|
||||
|
|
@ -228,6 +225,29 @@ class AttributeValues:
|
|||
|
||||
return serialize_attr_defs(self._attr_defs)
|
||||
|
||||
def _update(self, value):
|
||||
changes = {}
|
||||
for key, value in dict(value).items():
|
||||
if key in self._data and self._data.get(key) == value:
|
||||
continue
|
||||
self._data[key] = value
|
||||
changes[key] = value
|
||||
return changes
|
||||
|
||||
def _pop(self, key, default):
|
||||
has_key = key in self._data
|
||||
value = self._data.pop(key, default)
|
||||
# Remove attribute definition if is 'UnknownDef'
|
||||
# - gives option to get rid of unknown values
|
||||
attr_def = self._attr_defs_by_key.get(key)
|
||||
changes = {}
|
||||
if isinstance(attr_def, UnknownDef):
|
||||
self._attr_defs_by_key.pop(key)
|
||||
self._attr_defs.remove(attr_def)
|
||||
elif has_key:
|
||||
changes[key] = None
|
||||
return value, changes
|
||||
|
||||
|
||||
class CreatorAttributeValues(AttributeValues):
|
||||
"""Creator specific attribute values of an instance."""
|
||||
|
|
@ -270,6 +290,23 @@ class PublishAttributes:
|
|||
def __getitem__(self, key):
|
||||
return self._data[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Set value for plugin.
|
||||
|
||||
Args:
|
||||
key (str): Plugin name.
|
||||
value (dict[str, Any]): Value to set.
|
||||
|
||||
"""
|
||||
current_value = self._data.get(key)
|
||||
if isinstance(current_value, PublishAttributeValues):
|
||||
current_value.set_value(value)
|
||||
else:
|
||||
self._data[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
self.pop(key)
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self._data
|
||||
|
||||
|
|
|
|||
|
|
@ -226,11 +226,26 @@ class _CacheItems:
|
|||
thumbnails_cache = ThumbnailsCache()
|
||||
|
||||
|
||||
def get_thumbnail_path(project_name, thumbnail_id):
|
||||
def get_thumbnail_path(
|
||||
project_name: str,
|
||||
entity_type: str,
|
||||
entity_id: str,
|
||||
thumbnail_id: str
|
||||
):
|
||||
"""Get path to thumbnail image.
|
||||
|
||||
Thumbnail is cached by thumbnail id but is received using entity type and
|
||||
entity id.
|
||||
|
||||
Notes:
|
||||
Function 'get_thumbnail_by_id' can't be used because does not work
|
||||
for artists. The endpoint can't validate artist permissions.
|
||||
|
||||
Args:
|
||||
project_name (str): Project where thumbnail belongs to.
|
||||
entity_type (str): Entity type "folder", "task", "version"
|
||||
and "workfile".
|
||||
entity_id (str): Entity id.
|
||||
thumbnail_id (Union[str, None]): Thumbnail id.
|
||||
|
||||
Returns:
|
||||
|
|
@ -251,7 +266,7 @@ def get_thumbnail_path(project_name, thumbnail_id):
|
|||
# '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)
|
||||
result = con.get_thumbnail(project_name, entity_type, entity_id)
|
||||
|
||||
if result is not None and result.is_valid:
|
||||
return _CacheItems.thumbnails_cache.store_thumbnail(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from .path_resolving import (
|
|||
from .utils import (
|
||||
should_use_last_workfile_on_launch,
|
||||
should_open_workfiles_tool_on_launch,
|
||||
MissingWorkdirError,
|
||||
)
|
||||
|
||||
from .build_workfile import BuildWorkfile
|
||||
|
|
@ -46,6 +47,7 @@ __all__ = (
|
|||
|
||||
"should_use_last_workfile_on_launch",
|
||||
"should_open_workfiles_tool_on_launch",
|
||||
"MissingWorkdirError",
|
||||
|
||||
"BuildWorkfile",
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@ from ayon_core.lib import filter_profiles
|
|||
from ayon_core.settings import get_project_settings
|
||||
|
||||
|
||||
class MissingWorkdirError(Exception):
|
||||
"""Raised when accessing a work directory not found on disk."""
|
||||
pass
|
||||
|
||||
|
||||
def should_use_last_workfile_on_launch(
|
||||
project_name,
|
||||
host_name,
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ class CollectAudio(pyblish.api.ContextPlugin):
|
|||
"blender",
|
||||
"houdini",
|
||||
"max",
|
||||
"circuit",
|
||||
]
|
||||
|
||||
audio_product_name = "audioMain"
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@ class ExtractBurnin(publish.Extractor):
|
|||
"houdini",
|
||||
"max",
|
||||
"blender",
|
||||
"unreal"
|
||||
"unreal",
|
||||
"circuit",
|
||||
]
|
||||
|
||||
optional = True
|
||||
|
|
|
|||
|
|
@ -91,7 +91,8 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
"webpublisher",
|
||||
"aftereffects",
|
||||
"flame",
|
||||
"unreal"
|
||||
"unreal",
|
||||
"circuit",
|
||||
]
|
||||
|
||||
# Supported extensions
|
||||
|
|
@ -196,7 +197,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
).format(repre_name))
|
||||
continue
|
||||
|
||||
input_ext = repre["ext"]
|
||||
input_ext = repre["ext"].lower()
|
||||
if input_ext.startswith("."):
|
||||
input_ext = input_ext[1:]
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
"nuke",
|
||||
"aftereffects",
|
||||
"unreal",
|
||||
"houdini"
|
||||
"houdini",
|
||||
"circuit",
|
||||
]
|
||||
enabled = False
|
||||
|
||||
|
|
@ -162,9 +163,12 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
# Store new staging to cleanup paths
|
||||
instance.context.data["cleanupFullPaths"].append(dst_staging)
|
||||
|
||||
thumbnail_created = False
|
||||
oiio_supported = is_oiio_supported()
|
||||
repre_thumb_created = False
|
||||
for repre in filtered_repres:
|
||||
# Reset for each iteration to handle cases where multiple
|
||||
# reviewable thumbnails are needed
|
||||
repre_thumb_created = False
|
||||
repre_files = repre["files"]
|
||||
src_staging = os.path.normpath(repre["stagingDir"])
|
||||
if not isinstance(repre_files, (list, tuple)):
|
||||
|
|
@ -213,7 +217,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
)
|
||||
# If the input can read by OIIO then use OIIO method for
|
||||
# conversion otherwise use ffmpeg
|
||||
thumbnail_created = self._create_thumbnail_oiio(
|
||||
repre_thumb_created = self._create_thumbnail_oiio(
|
||||
full_input_path,
|
||||
full_output_path,
|
||||
colorspace_data
|
||||
|
|
@ -222,19 +226,19 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
# Try to use FFMPEG if OIIO is not supported or for cases when
|
||||
# oiiotool isn't available or representation is not having
|
||||
# colorspace data
|
||||
if not thumbnail_created:
|
||||
if not repre_thumb_created:
|
||||
if oiio_supported:
|
||||
self.log.debug(
|
||||
"Converting with FFMPEG because input"
|
||||
" can't be read by OIIO."
|
||||
)
|
||||
|
||||
thumbnail_created = self._create_thumbnail_ffmpeg(
|
||||
repre_thumb_created = self._create_thumbnail_ffmpeg(
|
||||
full_input_path, full_output_path
|
||||
)
|
||||
|
||||
# Skip representation and try next one if wasn't created
|
||||
if not thumbnail_created:
|
||||
if not repre_thumb_created:
|
||||
continue
|
||||
|
||||
if len(explicit_repres) > 1:
|
||||
|
|
@ -290,7 +294,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
# There is no need to create more then one thumbnail
|
||||
break
|
||||
|
||||
if not thumbnail_created:
|
||||
if not repre_thumb_created:
|
||||
self.log.warning("Thumbnail has not been created.")
|
||||
|
||||
def _is_review_instance(self, instance):
|
||||
|
|
@ -449,7 +453,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
# output arguments from presets
|
||||
jpeg_items.extend(ffmpeg_args.get("output") or [])
|
||||
# we just want one frame from movie files
|
||||
jpeg_items.extend(["-vframes", "1"])
|
||||
jpeg_items.extend(["-frames:v", "1"])
|
||||
|
||||
if resolution_arg:
|
||||
jpeg_items.extend(resolution_arg)
|
||||
|
|
@ -497,7 +501,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
"-i", video_file_path,
|
||||
"-analyzeduration", max_int,
|
||||
"-probesize", max_int,
|
||||
"-vframes", "1"
|
||||
"-frames:v", "1"
|
||||
]
|
||||
|
||||
# add output file path
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
|
|||
"-analyzeduration", max_int,
|
||||
"-probesize", max_int,
|
||||
"-i", src_path,
|
||||
"-vframes", "1",
|
||||
"-frames:v", "1",
|
||||
dst_path
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -619,8 +619,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
|
|||
# used for all represe
|
||||
# from temp to final
|
||||
original_directory = (
|
||||
instance.data.get("originalDirname") or instance_stagingdir)
|
||||
|
||||
instance.data.get("originalDirname") or stagingdir)
|
||||
_rootless = self.get_rootless_path(anatomy, original_directory)
|
||||
if _rootless == original_directory:
|
||||
raise KnownPublishError((
|
||||
|
|
|
|||
|
|
@ -27,8 +27,10 @@ import collections
|
|||
|
||||
import pyblish.api
|
||||
import ayon_api
|
||||
from ayon_api import RequestTypes
|
||||
from ayon_api.operations import OperationsSession
|
||||
|
||||
|
||||
InstanceFilterResult = collections.namedtuple(
|
||||
"InstanceFilterResult",
|
||||
["instance", "thumbnail_path", "version_id"]
|
||||
|
|
@ -161,6 +163,30 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin):
|
|||
return None
|
||||
return os.path.normpath(filled_path)
|
||||
|
||||
def _create_thumbnail(self, project_name: str, src_filepath: str) -> str:
|
||||
"""Upload thumbnail to AYON and return its id.
|
||||
|
||||
This is temporary fix of 'create_thumbnail' function in ayon_api to
|
||||
fix jpeg mime type.
|
||||
|
||||
"""
|
||||
mime_type = None
|
||||
with open(src_filepath, "rb") as stream:
|
||||
if b"\xff\xd8\xff" == stream.read(3):
|
||||
mime_type = "image/jpeg"
|
||||
|
||||
if mime_type is None:
|
||||
return ayon_api.create_thumbnail(project_name, src_filepath)
|
||||
|
||||
response = ayon_api.upload_file(
|
||||
f"projects/{project_name}/thumbnails",
|
||||
src_filepath,
|
||||
request_type=RequestTypes.post,
|
||||
headers={"Content-Type": mime_type},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()["id"]
|
||||
|
||||
def _integrate_thumbnails(
|
||||
self,
|
||||
filtered_instance_items,
|
||||
|
|
@ -179,7 +205,7 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin):
|
|||
).format(instance_label))
|
||||
continue
|
||||
|
||||
thumbnail_id = ayon_api.create_thumbnail(
|
||||
thumbnail_id = self._create_thumbnail(
|
||||
project_name, thumbnail_path
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -227,6 +227,9 @@ class HierarchyModel(object):
|
|||
self._tasks_by_id = NestedCacheItem(
|
||||
levels=2, default_factory=dict, lifetime=self.lifetime)
|
||||
|
||||
self._entity_ids_by_assignee = NestedCacheItem(
|
||||
levels=2, default_factory=dict, lifetime=self.lifetime)
|
||||
|
||||
self._folders_refreshing = set()
|
||||
self._tasks_refreshing = set()
|
||||
self._controller = controller
|
||||
|
|
@ -238,6 +241,8 @@ class HierarchyModel(object):
|
|||
self._task_items.reset()
|
||||
self._tasks_by_id.reset()
|
||||
|
||||
self._entity_ids_by_assignee.reset()
|
||||
|
||||
def refresh_project(self, project_name):
|
||||
"""Force to refresh folder items for a project.
|
||||
|
||||
|
|
@ -461,6 +466,54 @@ class HierarchyModel(object):
|
|||
output = self.get_task_entities(project_name, {task_id})
|
||||
return output[task_id]
|
||||
|
||||
def get_entity_ids_for_assignees(
|
||||
self, project_name: str, assignees: list[str]
|
||||
):
|
||||
folder_ids = set()
|
||||
task_ids = set()
|
||||
output = {
|
||||
"folder_ids": folder_ids,
|
||||
"task_ids": task_ids,
|
||||
}
|
||||
assignees = set(assignees)
|
||||
for assignee in tuple(assignees):
|
||||
cache = self._entity_ids_by_assignee[project_name][assignee]
|
||||
if cache.is_valid:
|
||||
assignees.discard(assignee)
|
||||
assignee_data = cache.get_data()
|
||||
folder_ids.update(assignee_data["folder_ids"])
|
||||
task_ids.update(assignee_data["task_ids"])
|
||||
|
||||
if not assignees:
|
||||
return output
|
||||
|
||||
tasks = ayon_api.get_tasks(
|
||||
project_name,
|
||||
assignees_all=assignees,
|
||||
fields={"id", "folderId", "assignees"},
|
||||
)
|
||||
tasks_assignee = {}
|
||||
for task in tasks:
|
||||
folder_ids.add(task["folderId"])
|
||||
task_ids.add(task["id"])
|
||||
for assignee in task["assignees"]:
|
||||
tasks_assignee.setdefault(assignee, []).append(task)
|
||||
|
||||
for assignee, tasks in tasks_assignee.items():
|
||||
cache = self._entity_ids_by_assignee[project_name][assignee]
|
||||
assignee_folder_ids = set()
|
||||
assignee_task_ids = set()
|
||||
assignee_data = {
|
||||
"folder_ids": assignee_folder_ids,
|
||||
"task_ids": assignee_task_ids,
|
||||
}
|
||||
for task in tasks:
|
||||
assignee_folder_ids.add(task["folderId"])
|
||||
assignee_task_ids.add(task["id"])
|
||||
cache.update_data(assignee_data)
|
||||
|
||||
return output
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _folder_refresh_event_manager(self, project_name, sender):
|
||||
self._folders_refreshing.add(project_name)
|
||||
|
|
|
|||
|
|
@ -21,8 +21,49 @@ class ThumbnailsModel:
|
|||
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_thumbnail_paths(
|
||||
self,
|
||||
project_name,
|
||||
entity_type,
|
||||
entity_ids,
|
||||
):
|
||||
output = {
|
||||
entity_id: None
|
||||
for entity_id in entity_ids
|
||||
}
|
||||
if not project_name or not entity_type or not entity_ids:
|
||||
return output
|
||||
|
||||
thumbnail_id_by_entity_id = {}
|
||||
if entity_type == "folder":
|
||||
thumbnail_id_by_entity_id = self.get_folder_thumbnail_ids(
|
||||
project_name, entity_ids
|
||||
)
|
||||
|
||||
elif entity_type == "version":
|
||||
thumbnail_id_by_entity_id = self.get_version_thumbnail_ids(
|
||||
project_name, entity_ids
|
||||
)
|
||||
|
||||
if not thumbnail_id_by_entity_id:
|
||||
return output
|
||||
|
||||
entity_ids_by_thumbnail_id = collections.defaultdict(set)
|
||||
for entity_id, thumbnail_id in thumbnail_id_by_entity_id.items():
|
||||
if not thumbnail_id:
|
||||
continue
|
||||
entity_ids_by_thumbnail_id[thumbnail_id].add(entity_id)
|
||||
|
||||
for thumbnail_id, entity_ids in entity_ids_by_thumbnail_id.items():
|
||||
thumbnail_path = self._get_thumbnail_path(
|
||||
project_name, entity_type, next(iter(entity_ids)), thumbnail_id
|
||||
)
|
||||
if not thumbnail_path:
|
||||
continue
|
||||
for entity_id in entity_ids:
|
||||
output[entity_id] = thumbnail_path
|
||||
|
||||
return output
|
||||
|
||||
def get_folder_thumbnail_ids(self, project_name, folder_ids):
|
||||
project_cache = self._folders_cache[project_name]
|
||||
|
|
@ -56,7 +97,13 @@ class ThumbnailsModel:
|
|||
output[version_id] = cache.get_data()
|
||||
return output
|
||||
|
||||
def _get_thumbnail_path(self, project_name, thumbnail_id):
|
||||
def _get_thumbnail_path(
|
||||
self,
|
||||
project_name,
|
||||
entity_type,
|
||||
entity_id,
|
||||
thumbnail_id
|
||||
):
|
||||
if not thumbnail_id:
|
||||
return None
|
||||
|
||||
|
|
@ -64,7 +111,12 @@ class ThumbnailsModel:
|
|||
if thumbnail_id in project_cache:
|
||||
return project_cache[thumbnail_id]
|
||||
|
||||
filepath = get_thumbnail_path(project_name, thumbnail_id)
|
||||
filepath = get_thumbnail_path(
|
||||
project_name,
|
||||
entity_type,
|
||||
entity_id,
|
||||
thumbnail_id
|
||||
)
|
||||
project_cache[thumbnail_id] = filepath
|
||||
return filepath
|
||||
|
||||
|
|
|
|||
|
|
@ -160,8 +160,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
Returns:
|
||||
list[FolderItem]: Minimum possible information needed
|
||||
for visualisation of folder hierarchy.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -180,8 +180,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
Returns:
|
||||
list[TaskItem]: Minimum possible information needed
|
||||
for visualisation of tasks.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -190,8 +190,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
|
||||
Returns:
|
||||
Union[str, None]: Selected project name.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -200,8 +200,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
|
||||
Returns:
|
||||
Union[str, None]: Selected folder id.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -210,8 +210,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
|
||||
Returns:
|
||||
Union[str, None]: Selected task id.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -220,8 +220,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
|
||||
Returns:
|
||||
Union[str, None]: Selected task name.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -238,8 +238,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
|
||||
Returns:
|
||||
dict[str, Union[str, None]]: Selected context.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -249,8 +249,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
Args:
|
||||
project_name (Union[str, None]): Project nameor None if no project
|
||||
is selected.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -260,8 +260,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
Args:
|
||||
folder_id (Union[str, None]): Folder id or None if no folder
|
||||
is selected.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -273,8 +273,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
is selected.
|
||||
task_name (Union[str, None]): Task name or None if no task
|
||||
is selected.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
# Actions
|
||||
|
|
@ -290,8 +290,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
Returns:
|
||||
list[ActionItem]: List of action items that should be shown
|
||||
for given context.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -303,8 +303,8 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
folder_id (Union[str, None]): Folder id.
|
||||
task_id (Union[str, None]): Task id.
|
||||
action_id (str): Action identifier.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -317,10 +317,10 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
project_name (Union[str, None]): Project name.
|
||||
folder_id (Union[str, None]): Folder id.
|
||||
task_id (Union[str, None]): Task id.
|
||||
action_id (Iterable[str]): Action identifiers.
|
||||
action_ids (Iterable[str]): Action identifiers.
|
||||
enabled (bool): New value of force not open workfile.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -340,5 +340,17 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
Triggers 'controller.refresh.actions.started' event at the beginning
|
||||
and 'controller.refresh.actions.finished' at the end.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_my_tasks_entity_ids(self, project_name: str):
|
||||
"""Get entity ids for my tasks.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
|
||||
Returns:
|
||||
dict[str, Union[list[str]]]: Folder and task ids.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from ayon_core.lib import Logger
|
||||
from ayon_core.lib import Logger, get_ayon_username
|
||||
from ayon_core.lib.events import QueuedEventSystem
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.tools.common_models import ProjectsModel, HierarchyModel
|
||||
|
|
@ -6,6 +6,8 @@ from ayon_core.tools.common_models import ProjectsModel, HierarchyModel
|
|||
from .abstract import AbstractLauncherFrontEnd, AbstractLauncherBackend
|
||||
from .models import LauncherSelectionModel, ActionsModel
|
||||
|
||||
NOT_SET = object()
|
||||
|
||||
|
||||
class BaseLauncherController(
|
||||
AbstractLauncherFrontEnd, AbstractLauncherBackend
|
||||
|
|
@ -15,6 +17,8 @@ class BaseLauncherController(
|
|||
self._event_system = None
|
||||
self._log = None
|
||||
|
||||
self._username = NOT_SET
|
||||
|
||||
self._selection_model = LauncherSelectionModel(self)
|
||||
self._projects_model = ProjectsModel(self)
|
||||
self._hierarchy_model = HierarchyModel(self)
|
||||
|
|
@ -168,5 +172,19 @@ class BaseLauncherController(
|
|||
|
||||
self._emit_event("controller.refresh.actions.finished")
|
||||
|
||||
def get_my_tasks_entity_ids(self, project_name: str):
|
||||
username = self._get_my_username()
|
||||
assignees = []
|
||||
if username:
|
||||
assignees.append(username)
|
||||
return self._hierarchy_model.get_entity_ids_for_assignees(
|
||||
project_name, assignees
|
||||
)
|
||||
|
||||
def _get_my_username(self):
|
||||
if self._username is NOT_SET:
|
||||
self._username = get_ayon_username()
|
||||
return self._username
|
||||
|
||||
def _emit_event(self, topic, data=None):
|
||||
self.emit_event(topic, data, "controller")
|
||||
|
|
|
|||
|
|
@ -5,17 +5,17 @@ from ayon_core.tools.utils import (
|
|||
PlaceholderLineEdit,
|
||||
SquareButton,
|
||||
RefreshButton,
|
||||
)
|
||||
from ayon_core.tools.utils import (
|
||||
ProjectsCombobox,
|
||||
FoldersWidget,
|
||||
TasksWidget,
|
||||
NiceCheckbox,
|
||||
)
|
||||
from ayon_core.tools.utils.lib import checkstate_int_to_enum
|
||||
|
||||
|
||||
class HierarchyPage(QtWidgets.QWidget):
|
||||
def __init__(self, controller, parent):
|
||||
super(HierarchyPage, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
# Header
|
||||
header_widget = QtWidgets.QWidget(self)
|
||||
|
|
@ -43,23 +43,36 @@ class HierarchyPage(QtWidgets.QWidget):
|
|||
)
|
||||
content_body.setOrientation(QtCore.Qt.Horizontal)
|
||||
|
||||
# - Folders widget with filter
|
||||
folders_wrapper = QtWidgets.QWidget(content_body)
|
||||
# - filters
|
||||
filters_widget = QtWidgets.QWidget(self)
|
||||
|
||||
folders_filter_text = PlaceholderLineEdit(folders_wrapper)
|
||||
folders_filter_text = PlaceholderLineEdit(filters_widget)
|
||||
folders_filter_text.setPlaceholderText("Filter folders...")
|
||||
|
||||
folders_widget = FoldersWidget(controller, folders_wrapper)
|
||||
my_tasks_tooltip = (
|
||||
"Filter folders and task to only those you are assigned to."
|
||||
)
|
||||
my_tasks_label = QtWidgets.QLabel("My tasks", filters_widget)
|
||||
my_tasks_label.setToolTip(my_tasks_tooltip)
|
||||
|
||||
folders_wrapper_layout = QtWidgets.QVBoxLayout(folders_wrapper)
|
||||
folders_wrapper_layout.setContentsMargins(0, 0, 0, 0)
|
||||
folders_wrapper_layout.addWidget(folders_filter_text, 0)
|
||||
folders_wrapper_layout.addWidget(folders_widget, 1)
|
||||
my_tasks_checkbox = NiceCheckbox(filters_widget)
|
||||
my_tasks_checkbox.setChecked(False)
|
||||
my_tasks_checkbox.setToolTip(my_tasks_tooltip)
|
||||
|
||||
filters_layout = QtWidgets.QHBoxLayout(filters_widget)
|
||||
filters_layout.setContentsMargins(0, 0, 0, 0)
|
||||
filters_layout.addWidget(folders_filter_text, 1)
|
||||
filters_layout.addWidget(my_tasks_label, 0)
|
||||
filters_layout.addWidget(my_tasks_checkbox, 0)
|
||||
|
||||
# - Folders widget
|
||||
folders_widget = FoldersWidget(controller, content_body)
|
||||
folders_widget.set_header_visible(True)
|
||||
|
||||
# - Tasks widget
|
||||
tasks_widget = TasksWidget(controller, content_body)
|
||||
|
||||
content_body.addWidget(folders_wrapper)
|
||||
content_body.addWidget(folders_widget)
|
||||
content_body.addWidget(tasks_widget)
|
||||
content_body.setStretchFactor(0, 100)
|
||||
content_body.setStretchFactor(1, 65)
|
||||
|
|
@ -67,20 +80,27 @@ class HierarchyPage(QtWidgets.QWidget):
|
|||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.addWidget(header_widget, 0)
|
||||
main_layout.addWidget(filters_widget, 0)
|
||||
main_layout.addWidget(content_body, 1)
|
||||
|
||||
btn_back.clicked.connect(self._on_back_clicked)
|
||||
refresh_btn.clicked.connect(self._on_refresh_clicked)
|
||||
folders_filter_text.textChanged.connect(self._on_filter_text_changed)
|
||||
my_tasks_checkbox.stateChanged.connect(
|
||||
self._on_my_tasks_checkbox_state_changed
|
||||
)
|
||||
|
||||
self._is_visible = False
|
||||
self._controller = controller
|
||||
|
||||
self._btn_back = btn_back
|
||||
self._projects_combobox = projects_combobox
|
||||
self._my_tasks_checkbox = my_tasks_checkbox
|
||||
self._folders_widget = folders_widget
|
||||
self._tasks_widget = tasks_widget
|
||||
|
||||
self._project_name = None
|
||||
|
||||
# Post init
|
||||
projects_combobox.set_listen_to_selection_change(self._is_visible)
|
||||
|
||||
|
|
@ -91,10 +111,14 @@ class HierarchyPage(QtWidgets.QWidget):
|
|||
self._projects_combobox.set_listen_to_selection_change(visible)
|
||||
if visible and project_name:
|
||||
self._projects_combobox.set_selection(project_name)
|
||||
self._project_name = project_name
|
||||
|
||||
def refresh(self):
|
||||
self._folders_widget.refresh()
|
||||
self._tasks_widget.refresh()
|
||||
self._on_my_tasks_checkbox_state_changed(
|
||||
self._my_tasks_checkbox.checkState()
|
||||
)
|
||||
|
||||
def _on_back_clicked(self):
|
||||
self._controller.set_selected_project(None)
|
||||
|
|
@ -104,3 +128,16 @@ class HierarchyPage(QtWidgets.QWidget):
|
|||
|
||||
def _on_filter_text_changed(self, text):
|
||||
self._folders_widget.set_name_filter(text)
|
||||
|
||||
def _on_my_tasks_checkbox_state_changed(self, state):
|
||||
folder_ids = None
|
||||
task_ids = None
|
||||
state = checkstate_int_to_enum(state)
|
||||
if state == QtCore.Qt.Checked:
|
||||
entity_ids = self._controller.get_my_tasks_entity_ids(
|
||||
self._project_name
|
||||
)
|
||||
folder_ids = entity_ids["folder_ids"]
|
||||
task_ids = entity_ids["task_ids"]
|
||||
self._folders_widget.set_folder_ids_filter(folder_ids)
|
||||
self._tasks_widget.set_task_ids_filter(task_ids)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
page_side_anim_interval = 250
|
||||
|
||||
def __init__(self, controller=None, parent=None):
|
||||
super(LauncherWindow, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
if controller is None:
|
||||
controller = BaseLauncherController()
|
||||
|
|
@ -153,14 +153,14 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
self.resize(520, 740)
|
||||
|
||||
def showEvent(self, event):
|
||||
super(LauncherWindow, self).showEvent(event)
|
||||
super().showEvent(event)
|
||||
self._window_is_active = True
|
||||
if not self._actions_refresh_timer.isActive():
|
||||
self._actions_refresh_timer.start()
|
||||
self._controller.refresh()
|
||||
|
||||
def closeEvent(self, event):
|
||||
super(LauncherWindow, self).closeEvent(event)
|
||||
super().closeEvent(event)
|
||||
self._window_is_active = False
|
||||
self._actions_refresh_timer.stop()
|
||||
|
||||
|
|
@ -176,7 +176,7 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
self._on_actions_refresh_timeout()
|
||||
self._actions_refresh_timer.start()
|
||||
|
||||
super(LauncherWindow, self).changeEvent(event)
|
||||
super().changeEvent(event)
|
||||
|
||||
def _on_actions_refresh_timeout(self):
|
||||
# Stop timer if widget is not visible
|
||||
|
|
|
|||
|
|
@ -733,7 +733,12 @@ class FrontendLoaderController(_BaseLoaderController):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_thumbnail_path(self, project_name, thumbnail_id):
|
||||
def get_thumbnail_paths(
|
||||
self,
|
||||
project_name,
|
||||
entity_type,
|
||||
entity_ids
|
||||
):
|
||||
"""Get thumbnail path for thumbnail id.
|
||||
|
||||
This method should get a path to a thumbnail based on thumbnail id.
|
||||
|
|
@ -742,10 +747,11 @@ class FrontendLoaderController(_BaseLoaderController):
|
|||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
thumbnail_id (str): Thumbnail id.
|
||||
entity_type (str): Entity type.
|
||||
entity_ids (set[str]): Entity ids.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Thumbnail path or None if not found.
|
||||
dict[str, Union[str, None]]: Thumbnail path by entity id.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -259,9 +259,14 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
|
|||
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 get_thumbnail_paths(
|
||||
self,
|
||||
project_name,
|
||||
entity_type,
|
||||
entity_ids,
|
||||
):
|
||||
return self._thumbnails_model.get_thumbnail_paths(
|
||||
project_name, entity_type, entity_ids
|
||||
)
|
||||
|
||||
def change_products_group(self, project_name, product_ids, group_name):
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from __future__ import annotations
|
||||
from qtpy import QtGui, QtCore
|
||||
|
||||
from ._multicombobox import (
|
||||
|
|
|
|||
|
|
@ -501,38 +501,29 @@ class LoaderWindow(QtWidgets.QWidget):
|
|||
self._update_thumbnails()
|
||||
|
||||
def _update_thumbnails(self):
|
||||
# TODO make this threaded and show loading animation while running
|
||||
project_name = self._selected_project_name
|
||||
thumbnail_ids = set()
|
||||
entity_type = None
|
||||
entity_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())
|
||||
entity_ids = set(self._selected_version_ids)
|
||||
entity_type = "version"
|
||||
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())
|
||||
entity_ids = set(self._selected_folder_ids)
|
||||
entity_type = "folder"
|
||||
|
||||
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_path_by_entity_id = self._controller.get_thumbnail_paths(
|
||||
project_name, entity_type, entity_ids
|
||||
)
|
||||
thumbnail_paths = set(thumbnail_path_by_entity_id.values())
|
||||
thumbnail_paths.discard(None)
|
||||
self._thumbnails_widget.set_current_thumbnail_paths(thumbnail_paths)
|
||||
|
||||
if thumbnail_paths:
|
||||
self._thumbnails_widget.set_current_thumbnail_paths(
|
||||
thumbnail_paths
|
||||
)
|
||||
else:
|
||||
self._thumbnails_widget.set_current_thumbnails(None)
|
||||
|
||||
def _on_projects_refresh(self):
|
||||
self._refresh_handler.set_project_refreshed()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
from __future__ import annotations
|
||||
import collections
|
||||
from typing import Optional
|
||||
|
||||
from qtpy import QtWidgets, QtGui, QtCore
|
||||
|
||||
|
|
@ -33,7 +35,10 @@ class FoldersQtModel(QtGui.QStandardItemModel):
|
|||
refreshed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller):
|
||||
super(FoldersQtModel, self).__init__()
|
||||
super().__init__()
|
||||
|
||||
self.setColumnCount(1)
|
||||
self.setHeaderData(0, QtCore.Qt.Horizontal, "Folders")
|
||||
|
||||
self._controller = controller
|
||||
self._items_by_id = {}
|
||||
|
|
@ -334,6 +339,29 @@ class FoldersQtModel(QtGui.QStandardItemModel):
|
|||
self.refreshed.emit()
|
||||
|
||||
|
||||
class FoldersProxyModel(RecursiveSortFilterProxyModel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._folder_ids_filter = None
|
||||
|
||||
def set_folder_ids_filter(self, folder_ids: Optional[list[str]]):
|
||||
if self._folder_ids_filter == folder_ids:
|
||||
return
|
||||
self._folder_ids_filter = folder_ids
|
||||
self.invalidateFilter()
|
||||
|
||||
def filterAcceptsRow(self, row, parent_index):
|
||||
if self._folder_ids_filter is not None:
|
||||
if not self._folder_ids_filter:
|
||||
return False
|
||||
source_index = self.sourceModel().index(row, 0, parent_index)
|
||||
folder_id = source_index.data(FOLDER_ID_ROLE)
|
||||
if folder_id not in self._folder_ids_filter:
|
||||
return False
|
||||
return super().filterAcceptsRow(row, parent_index)
|
||||
|
||||
|
||||
class FoldersWidget(QtWidgets.QWidget):
|
||||
"""Folders widget.
|
||||
|
||||
|
|
@ -369,13 +397,13 @@ class FoldersWidget(QtWidgets.QWidget):
|
|||
refreshed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent, handle_expected_selection=False):
|
||||
super(FoldersWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
folders_view = TreeView(self)
|
||||
folders_view.setHeaderHidden(True)
|
||||
|
||||
folders_model = FoldersQtModel(controller)
|
||||
folders_proxy_model = RecursiveSortFilterProxyModel()
|
||||
folders_proxy_model = FoldersProxyModel()
|
||||
folders_proxy_model.setSourceModel(folders_model)
|
||||
folders_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
|
||||
|
|
@ -446,6 +474,18 @@ class FoldersWidget(QtWidgets.QWidget):
|
|||
if name:
|
||||
self._folders_view.expandAll()
|
||||
|
||||
def set_folder_ids_filter(self, folder_ids: Optional[list[str]]):
|
||||
"""Set filter of folder ids.
|
||||
|
||||
Args:
|
||||
folder_ids (list[str]): The list of folder ids.
|
||||
|
||||
"""
|
||||
self._folders_proxy_model.set_folder_ids_filter(folder_ids)
|
||||
|
||||
def set_header_visible(self, visible: bool):
|
||||
self._folders_view.setHeaderHidden(not visible)
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh folders model.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
from __future__ import annotations
|
||||
from typing import Optional
|
||||
|
||||
from qtpy import QtWidgets, QtGui, QtCore
|
||||
|
||||
from ayon_core.style import (
|
||||
|
|
@ -343,6 +346,29 @@ class TasksQtModel(QtGui.QStandardItemModel):
|
|||
return self._has_content
|
||||
|
||||
|
||||
class TasksProxyModel(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._task_ids_filter: Optional[set[str]] = None
|
||||
|
||||
def set_task_ids_filter(self, task_ids: Optional[set[str]]):
|
||||
if self._task_ids_filter == task_ids:
|
||||
return
|
||||
self._task_ids_filter = task_ids
|
||||
self.invalidateFilter()
|
||||
|
||||
def filterAcceptsRow(self, row, parent_index):
|
||||
if self._task_ids_filter is not None:
|
||||
if not self._task_ids_filter:
|
||||
return False
|
||||
source_index = self.sourceModel().index(row, 0, parent_index)
|
||||
task_id = source_index.data(ITEM_ID_ROLE)
|
||||
if task_id is not None and task_id not in self._task_ids_filter:
|
||||
return False
|
||||
return super().filterAcceptsRow(row, parent_index)
|
||||
|
||||
|
||||
class TasksWidget(QtWidgets.QWidget):
|
||||
"""Tasks widget.
|
||||
|
||||
|
|
@ -364,7 +390,7 @@ class TasksWidget(QtWidgets.QWidget):
|
|||
tasks_view.setIndentation(0)
|
||||
|
||||
tasks_model = TasksQtModel(controller)
|
||||
tasks_proxy_model = QtCore.QSortFilterProxyModel()
|
||||
tasks_proxy_model = TasksProxyModel()
|
||||
tasks_proxy_model.setSourceModel(tasks_model)
|
||||
tasks_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
|
||||
|
|
@ -490,6 +516,15 @@ class TasksWidget(QtWidgets.QWidget):
|
|||
)
|
||||
return True
|
||||
|
||||
def set_task_ids_filter(self, task_ids: Optional[list[str]]):
|
||||
"""Set filter of folder ids.
|
||||
|
||||
Args:
|
||||
task_ids (list[str]): The list of folder ids.
|
||||
|
||||
"""
|
||||
self._tasks_proxy_model.set_task_ids_filter(task_ids)
|
||||
|
||||
def _on_tasks_refresh_finished(self, event):
|
||||
"""Tasks were refreshed in controller.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring AYON addon 'core' version."""
|
||||
__version__ = "1.1.5+dev"
|
||||
__version__ = "1.1.8+dev"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue