mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 08:24:53 +01:00
Merge branch 'develop' into enhancement/1416-loader-actions
# Conflicts: # client/ayon_core/tools/loader/ui/window.py
This commit is contained in:
commit
30dda67e7c
50 changed files with 1554 additions and 611 deletions
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -35,6 +35,8 @@ body:
|
||||||
label: Version
|
label: Version
|
||||||
description: What version are you running? Look to AYON Tray
|
description: What version are you running? Look to AYON Tray
|
||||||
options:
|
options:
|
||||||
|
- 1.6.9
|
||||||
|
- 1.6.8
|
||||||
- 1.6.7
|
- 1.6.7
|
||||||
- 1.6.6
|
- 1.6.6
|
||||||
- 1.6.5
|
- 1.6.5
|
||||||
|
|
|
||||||
|
|
@ -604,7 +604,11 @@ class EnumDef(AbstractAttrDef):
|
||||||
|
|
||||||
if value is None:
|
if value is None:
|
||||||
return copy.deepcopy(self.default)
|
return copy.deepcopy(self.default)
|
||||||
return list(self._item_values.intersection(value))
|
return [
|
||||||
|
v
|
||||||
|
for v in value
|
||||||
|
if v in self._item_values
|
||||||
|
]
|
||||||
|
|
||||||
def is_value_valid(self, value: Any) -> bool:
|
def is_value_valid(self, value: Any) -> bool:
|
||||||
"""Check if item is available in possible values."""
|
"""Check if item is available in possible values."""
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,15 @@ def deprecated(new_destination):
|
||||||
return _decorator(func)
|
return _decorator(func)
|
||||||
|
|
||||||
|
|
||||||
|
class MissingRGBAChannelsError(ValueError):
|
||||||
|
"""Raised when we can't find channels to use as RGBA for conversion in
|
||||||
|
input media.
|
||||||
|
|
||||||
|
This may be other channels than solely RGBA, like Z-channel. The error is
|
||||||
|
raised when no matching 'reviewable' channel was found.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_transcode_temp_directory():
|
def get_transcode_temp_directory():
|
||||||
"""Creates temporary folder for transcoding.
|
"""Creates temporary folder for transcoding.
|
||||||
|
|
||||||
|
|
@ -388,6 +397,10 @@ def get_review_info_by_layer_name(channel_names):
|
||||||
...
|
...
|
||||||
]
|
]
|
||||||
|
|
||||||
|
This tries to find suitable outputs good for review purposes, by
|
||||||
|
searching for channel names like RGBA, but also XYZ, Z, N, AR, AG, AB
|
||||||
|
channels.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
channel_names (list[str]): List of channel names.
|
channel_names (list[str]): List of channel names.
|
||||||
|
|
||||||
|
|
@ -396,7 +409,6 @@ def get_review_info_by_layer_name(channel_names):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
layer_names_order = []
|
layer_names_order = []
|
||||||
rgba_by_layer_name = collections.defaultdict(dict)
|
|
||||||
channels_by_layer_name = collections.defaultdict(dict)
|
channels_by_layer_name = collections.defaultdict(dict)
|
||||||
|
|
||||||
for channel_name in channel_names:
|
for channel_name in channel_names:
|
||||||
|
|
@ -405,45 +417,95 @@ def get_review_info_by_layer_name(channel_names):
|
||||||
if "." in channel_name:
|
if "." in channel_name:
|
||||||
layer_name, last_part = channel_name.rsplit(".", 1)
|
layer_name, last_part = channel_name.rsplit(".", 1)
|
||||||
|
|
||||||
channels_by_layer_name[layer_name][channel_name] = last_part
|
# R, G, B, A or X, Y, Z, N, AR, AG, AB, RED, GREEN, BLUE, ALPHA
|
||||||
if last_part.lower() not in {
|
channel = last_part.upper()
|
||||||
"r", "red",
|
if channel not in {
|
||||||
"g", "green",
|
# Detect RGBA channels
|
||||||
"b", "blue",
|
"R", "G", "B", "A",
|
||||||
"a", "alpha"
|
# Support fully written out rgba channel names
|
||||||
|
"RED", "GREEN", "BLUE", "ALPHA",
|
||||||
|
# Allow detecting of x, y and z channels, and normal channels
|
||||||
|
"X", "Y", "Z", "N",
|
||||||
|
# red, green and blue alpha/opacity, for colored mattes
|
||||||
|
"AR", "AG", "AB"
|
||||||
}:
|
}:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if layer_name not in layer_names_order:
|
if layer_name not in layer_names_order:
|
||||||
layer_names_order.append(layer_name)
|
layer_names_order.append(layer_name)
|
||||||
# R, G, B or A
|
|
||||||
channel = last_part[0].upper()
|
channels_by_layer_name[layer_name][channel] = channel_name
|
||||||
rgba_by_layer_name[layer_name][channel] = channel_name
|
|
||||||
|
|
||||||
# Put empty layer or 'rgba' to the beginning of the list
|
# Put empty layer or 'rgba' to the beginning of the list
|
||||||
# - if input has R, G, B, A channels they should be used for review
|
# - if input has R, G, B, A channels they should be used for review
|
||||||
# NOTE They are iterated in reversed order because they're inserted to
|
def _sort(_layer_name: str) -> int:
|
||||||
# the beginning of 'layer_names_order' -> last added will be first.
|
# Prioritize "" layer name
|
||||||
for name in reversed(["", "rgba"]):
|
# Prioritize layers with RGB channels
|
||||||
if name in layer_names_order:
|
if _layer_name == "rgba":
|
||||||
layer_names_order.remove(name)
|
return 0
|
||||||
layer_names_order.insert(0, name)
|
|
||||||
|
if _layer_name == "":
|
||||||
|
return 1
|
||||||
|
|
||||||
|
channels = channels_by_layer_name[_layer_name]
|
||||||
|
if all(channel in channels for channel in "RGB"):
|
||||||
|
return 2
|
||||||
|
return 10
|
||||||
|
layer_names_order.sort(key=_sort)
|
||||||
|
|
||||||
output = []
|
output = []
|
||||||
for layer_name in layer_names_order:
|
for layer_name in layer_names_order:
|
||||||
rgba_layer_info = rgba_by_layer_name[layer_name]
|
channel_info = channels_by_layer_name[layer_name]
|
||||||
red = rgba_layer_info.get("R")
|
|
||||||
green = rgba_layer_info.get("G")
|
alpha = channel_info.get("A")
|
||||||
blue = rgba_layer_info.get("B")
|
|
||||||
if not red or not green or not blue:
|
# RGB channels
|
||||||
|
if all(channel in channel_info for channel in "RGB"):
|
||||||
|
rgb = "R", "G", "B"
|
||||||
|
|
||||||
|
# RGB channels using fully written out channel names
|
||||||
|
elif all(
|
||||||
|
channel in channel_info
|
||||||
|
for channel in ("RED", "GREEN", "BLUE")
|
||||||
|
):
|
||||||
|
rgb = "RED", "GREEN", "BLUE"
|
||||||
|
alpha = channel_info.get("ALPHA")
|
||||||
|
|
||||||
|
# XYZ channels (position pass)
|
||||||
|
elif all(channel in channel_info for channel in "XYZ"):
|
||||||
|
rgb = "X", "Y", "Z"
|
||||||
|
|
||||||
|
# Colored mattes (as defined in OpenEXR Channel Name standards)
|
||||||
|
elif all(channel in channel_info for channel in ("AR", "AG", "AB")):
|
||||||
|
rgb = "AR", "AG", "AB"
|
||||||
|
|
||||||
|
# Luminance channel (as defined in OpenEXR Channel Name standards)
|
||||||
|
elif "Y" in channel_info:
|
||||||
|
rgb = "Y", "Y", "Y"
|
||||||
|
|
||||||
|
# Has only Z channel (Z-depth layer)
|
||||||
|
elif "Z" in channel_info:
|
||||||
|
rgb = "Z", "Z", "Z"
|
||||||
|
|
||||||
|
# Has only A channel (Alpha layer)
|
||||||
|
elif "A" in channel_info:
|
||||||
|
rgb = "A", "A", "A"
|
||||||
|
alpha = None
|
||||||
|
|
||||||
|
else:
|
||||||
|
# No reviewable channels found
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
red = channel_info[rgb[0]]
|
||||||
|
green = channel_info[rgb[1]]
|
||||||
|
blue = channel_info[rgb[2]]
|
||||||
output.append({
|
output.append({
|
||||||
"name": layer_name,
|
"name": layer_name,
|
||||||
"review_channels": {
|
"review_channels": {
|
||||||
"R": red,
|
"R": red,
|
||||||
"G": green,
|
"G": green,
|
||||||
"B": blue,
|
"B": blue,
|
||||||
"A": rgba_layer_info.get("A"),
|
"A": alpha,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return output
|
return output
|
||||||
|
|
@ -1467,8 +1529,9 @@ def get_oiio_input_and_channel_args(oiio_input_info, alpha_default=None):
|
||||||
review_channels = get_convert_rgb_channels(channel_names)
|
review_channels = get_convert_rgb_channels(channel_names)
|
||||||
|
|
||||||
if review_channels is None:
|
if review_channels is None:
|
||||||
raise ValueError(
|
raise MissingRGBAChannelsError(
|
||||||
"Couldn't find channels that can be used for conversion."
|
"Couldn't find channels that can be used for conversion "
|
||||||
|
f"among channels: {channel_names}."
|
||||||
)
|
)
|
||||||
|
|
||||||
red, green, blue, alpha = review_channels
|
red, green, blue, alpha = review_channels
|
||||||
|
|
@ -1482,7 +1545,8 @@ def get_oiio_input_and_channel_args(oiio_input_info, alpha_default=None):
|
||||||
channels_arg += ",A={}".format(float(alpha_default))
|
channels_arg += ",A={}".format(float(alpha_default))
|
||||||
input_channels.append("A")
|
input_channels.append("A")
|
||||||
|
|
||||||
input_channels_str = ",".join(input_channels)
|
# Make sure channels are unique, but preserve order to avoid oiiotool crash
|
||||||
|
input_channels_str = ",".join(list(dict.fromkeys(input_channels)))
|
||||||
|
|
||||||
subimages = oiio_input_info.get("subimages")
|
subimages = oiio_input_info.get("subimages")
|
||||||
input_arg = "-i"
|
input_arg = "-i"
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,7 @@ class AttributeValues:
|
||||||
if value is None:
|
if value is None:
|
||||||
continue
|
continue
|
||||||
converted_value = attr_def.convert_value(value)
|
converted_value = attr_def.convert_value(value)
|
||||||
|
# QUESTION Could we just use converted value all the time?
|
||||||
if converted_value == value:
|
if converted_value == value:
|
||||||
self._data[attr_def.key] = value
|
self._data[attr_def.key] = value
|
||||||
|
|
||||||
|
|
@ -245,11 +246,11 @@ class AttributeValues:
|
||||||
|
|
||||||
def _update(self, value):
|
def _update(self, value):
|
||||||
changes = {}
|
changes = {}
|
||||||
for key, value in dict(value).items():
|
for key, key_value in dict(value).items():
|
||||||
if key in self._data and self._data.get(key) == value:
|
if key in self._data and self._data.get(key) == key_value:
|
||||||
continue
|
continue
|
||||||
self._data[key] = value
|
self._data[key] = key_value
|
||||||
changes[key] = value
|
changes[key] = key_value
|
||||||
return changes
|
return changes
|
||||||
|
|
||||||
def _pop(self, key, default):
|
def _pop(self, key, default):
|
||||||
|
|
|
||||||
|
|
@ -1045,7 +1045,9 @@ def get_resources(project_name, version_entity, extension=None):
|
||||||
filtered.append(repre_entity)
|
filtered.append(repre_entity)
|
||||||
|
|
||||||
representation = filtered[0]
|
representation = filtered[0]
|
||||||
directory = get_representation_path(representation)
|
directory = get_representation_path(
|
||||||
|
project_name, representation
|
||||||
|
)
|
||||||
print("Source: ", directory)
|
print("Source: ", directory)
|
||||||
resources = sorted(
|
resources = sorted(
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,8 @@ from .utils import (
|
||||||
get_loader_identifier,
|
get_loader_identifier,
|
||||||
get_loaders_by_name,
|
get_loaders_by_name,
|
||||||
|
|
||||||
get_representation_path_from_context,
|
|
||||||
get_representation_path,
|
get_representation_path,
|
||||||
|
get_representation_path_from_context,
|
||||||
get_representation_path_with_anatomy,
|
get_representation_path_with_anatomy,
|
||||||
|
|
||||||
is_compatible_loader,
|
is_compatible_loader,
|
||||||
|
|
@ -85,8 +85,8 @@ __all__ = (
|
||||||
"get_loader_identifier",
|
"get_loader_identifier",
|
||||||
"get_loaders_by_name",
|
"get_loaders_by_name",
|
||||||
|
|
||||||
"get_representation_path_from_context",
|
|
||||||
"get_representation_path",
|
"get_representation_path",
|
||||||
|
"get_representation_path_from_context",
|
||||||
"get_representation_path_with_anatomy",
|
"get_representation_path_with_anatomy",
|
||||||
|
|
||||||
"is_compatible_loader",
|
"is_compatible_loader",
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
import platform
|
import warnings
|
||||||
import logging
|
import logging
|
||||||
import inspect
|
import inspect
|
||||||
import collections
|
import collections
|
||||||
import numbers
|
import numbers
|
||||||
from typing import Optional, Union, Any
|
import copy
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Optional, Union, Any, overload
|
||||||
|
|
||||||
import ayon_api
|
import ayon_api
|
||||||
|
|
||||||
|
|
@ -14,9 +18,8 @@ from ayon_core.lib import (
|
||||||
StringTemplate,
|
StringTemplate,
|
||||||
TemplateUnsolved,
|
TemplateUnsolved,
|
||||||
)
|
)
|
||||||
from ayon_core.pipeline import (
|
from ayon_core.lib.path_templates import TemplateResult
|
||||||
Anatomy,
|
from ayon_core.pipeline import Anatomy
|
||||||
)
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -644,15 +647,15 @@ def get_representation_path_from_context(context):
|
||||||
|
|
||||||
representation = context["representation"]
|
representation = context["representation"]
|
||||||
project_entity = context.get("project")
|
project_entity = context.get("project")
|
||||||
root = None
|
if project_entity:
|
||||||
if (
|
project_name = project_entity["name"]
|
||||||
project_entity
|
else:
|
||||||
and project_entity["name"] != get_current_project_name()
|
project_name = get_current_project_name()
|
||||||
):
|
return get_representation_path(
|
||||||
anatomy = Anatomy(project_entity["name"])
|
project_name,
|
||||||
root = anatomy.roots
|
representation,
|
||||||
|
project_entity=project_entity,
|
||||||
return get_representation_path(representation, root)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_representation_path_with_anatomy(repre_entity, anatomy):
|
def get_representation_path_with_anatomy(repre_entity, anatomy):
|
||||||
|
|
@ -671,139 +674,248 @@ def get_representation_path_with_anatomy(repre_entity, anatomy):
|
||||||
anatomy (Anatomy): Project anatomy object.
|
anatomy (Anatomy): Project anatomy object.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Union[None, TemplateResult]: None if path can't be received
|
TemplateResult: Resolved representation path.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
InvalidRepresentationContext: When representation data are probably
|
InvalidRepresentationContext: When representation data are probably
|
||||||
invalid or not available.
|
invalid or not available.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
return get_representation_path(
|
||||||
|
anatomy.project_name,
|
||||||
|
repre_entity,
|
||||||
|
anatomy=anatomy,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_representation_path_with_roots(
|
||||||
|
representation: dict[str, Any],
|
||||||
|
roots: dict[str, str],
|
||||||
|
) -> Optional[TemplateResult]:
|
||||||
|
"""Get filename from representation with custom root.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
representation(dict): Representation entity.
|
||||||
|
roots (dict[str, str]): Roots to use.
|
||||||
|
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[TemplateResult]: Resolved representation path.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
template = representation["attrib"]["template"]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
context = representation["context"]
|
||||||
|
|
||||||
|
_fix_representation_context_compatibility(context)
|
||||||
|
|
||||||
|
context["root"] = roots
|
||||||
|
path = StringTemplate.format_strict_template(
|
||||||
|
template, context
|
||||||
|
)
|
||||||
|
except (TemplateUnsolved, KeyError):
|
||||||
|
# Template references unavailable data
|
||||||
|
return None
|
||||||
|
|
||||||
|
return path.normalized()
|
||||||
|
|
||||||
|
|
||||||
|
def _backwards_compatibility_repre_path(func):
|
||||||
|
"""Wrapper handling backwards compatibility of 'get_representation_path'.
|
||||||
|
|
||||||
|
Allows 'get_representation_path' to support old and new signatures of the
|
||||||
|
function. The old signature supported passing in representation entity
|
||||||
|
and optional roots. The new signature requires the project name
|
||||||
|
to be passed. In case custom roots should be used, a dedicated function
|
||||||
|
'get_representation_path_with_roots' is available.
|
||||||
|
|
||||||
|
The wrapper handles passed arguments, and based on kwargs and types
|
||||||
|
of the arguments will call the function which relates to
|
||||||
|
the arguments.
|
||||||
|
|
||||||
|
The function is also marked with an attribute 'version' so other addons
|
||||||
|
can check if the function is using the new signature or is using
|
||||||
|
the old signature. That should allow addons to adapt to new signature.
|
||||||
|
>>> if getattr(get_representation_path, "version", None) == 2:
|
||||||
|
>>> path = get_representation_path(project_name, repre_entity)
|
||||||
|
>>> else:
|
||||||
|
>>> path = get_representation_path(repre_entity)
|
||||||
|
|
||||||
|
The plan to remove backwards compatibility is 1.1.2026.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Add an attribute to the function so addons can check if the new variant
|
||||||
|
# of the function is available.
|
||||||
|
# >>> getattr(get_representation_path, "version", None) == 2
|
||||||
|
# >>> True
|
||||||
|
setattr(func, "version", 2)
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
from ayon_core.pipeline import get_current_project_name
|
||||||
|
|
||||||
|
# Decide which variant of the function based on passed arguments
|
||||||
|
# will be used.
|
||||||
|
if args:
|
||||||
|
arg_1 = args[0]
|
||||||
|
if isinstance(arg_1, str):
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
elif "project_name" in kwargs:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
(
|
||||||
|
"Used deprecated variant of 'get_representation_path'."
|
||||||
|
" Please change used arguments signature to follow"
|
||||||
|
" new definition. Will be removed 1.1.2026."
|
||||||
|
),
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Find out which arguments were passed
|
||||||
|
if args:
|
||||||
|
representation = args[0]
|
||||||
|
else:
|
||||||
|
representation = kwargs.get("representation")
|
||||||
|
|
||||||
|
if len(args) > 1:
|
||||||
|
roots = args[1]
|
||||||
|
else:
|
||||||
|
roots = kwargs.get("root")
|
||||||
|
|
||||||
|
if roots is not None:
|
||||||
|
return get_representation_path_with_roots(
|
||||||
|
representation, roots
|
||||||
|
)
|
||||||
|
|
||||||
|
project_name = (
|
||||||
|
representation["context"].get("project", {}).get("name")
|
||||||
|
)
|
||||||
|
if project_name is None:
|
||||||
|
project_name = get_current_project_name()
|
||||||
|
|
||||||
|
return func(project_name, representation)
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_representation_path(
|
||||||
|
representation: dict[str, Any],
|
||||||
|
root: Optional[dict[str, Any]] = None,
|
||||||
|
) -> TemplateResult:
|
||||||
|
"""DEPRECATED Get filled representation path.
|
||||||
|
|
||||||
|
Use 'get_representation_path' using the new function signature.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
representation (dict[str, Any]): Representation entity.
|
||||||
|
root (Optional[dict[str, Any]): Roots to fill the path.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TemplateResult: Resolved path to representation.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
InvalidRepresentationContext: When representation data are probably
|
||||||
|
invalid or not available.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def get_representation_path(
|
||||||
|
project_name: str,
|
||||||
|
repre_entity: dict[str, Any],
|
||||||
|
*,
|
||||||
|
anatomy: Optional[Anatomy] = None,
|
||||||
|
project_entity: Optional[dict[str, Any]] = None,
|
||||||
|
) -> TemplateResult:
|
||||||
|
"""Get filled representation path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_name (str): Project name.
|
||||||
|
repre_entity (dict[str, Any]): Representation entity.
|
||||||
|
anatomy (Optional[Anatomy]): Project anatomy.
|
||||||
|
project_entity (Optional[dict[str, Any]): Project entity. Is used to
|
||||||
|
initialize Anatomy and is not needed if 'anatomy' is passed in.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TemplateResult: Resolved path to representation.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
InvalidRepresentationContext: When representation data are probably
|
||||||
|
invalid or not available.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@_backwards_compatibility_repre_path
|
||||||
|
def get_representation_path(
|
||||||
|
project_name: str,
|
||||||
|
repre_entity: dict[str, Any],
|
||||||
|
*,
|
||||||
|
anatomy: Optional[Anatomy] = None,
|
||||||
|
project_entity: Optional[dict[str, Any]] = None,
|
||||||
|
) -> TemplateResult:
|
||||||
|
"""Get filled representation path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_name (str): Project name.
|
||||||
|
repre_entity (dict[str, Any]): Representation entity.
|
||||||
|
anatomy (Optional[Anatomy]): Project anatomy.
|
||||||
|
project_entity (Optional[dict[str, Any]): Project entity. Is used to
|
||||||
|
initialize Anatomy and is not needed if 'anatomy' is passed in.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TemplateResult: Resolved path to representation.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
InvalidRepresentationContext: When representation data are probably
|
||||||
|
invalid or not available.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if anatomy is None:
|
||||||
|
anatomy = Anatomy(project_name, project_entity=project_entity)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
template = repre_entity["attrib"]["template"]
|
template = repre_entity["attrib"]["template"]
|
||||||
|
|
||||||
except KeyError:
|
except KeyError as exc:
|
||||||
raise InvalidRepresentationContext((
|
raise InvalidRepresentationContext(
|
||||||
"Representation document does not"
|
"Failed to receive template from representation entity."
|
||||||
" contain template in data ('data.template')"
|
) from exc
|
||||||
))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
context = repre_entity["context"]
|
context = copy.deepcopy(repre_entity["context"])
|
||||||
_fix_representation_context_compatibility(context)
|
_fix_representation_context_compatibility(context)
|
||||||
context["root"] = anatomy.roots
|
context["root"] = anatomy.roots
|
||||||
|
|
||||||
path = StringTemplate.format_strict_template(template, context)
|
path = StringTemplate.format_strict_template(template, context)
|
||||||
|
|
||||||
except TemplateUnsolved as exc:
|
except TemplateUnsolved as exc:
|
||||||
raise InvalidRepresentationContext((
|
raise InvalidRepresentationContext(
|
||||||
"Couldn't resolve representation template with available data."
|
"Failed to resolve representation template with available data."
|
||||||
" Reason: {}".format(str(exc))
|
) from exc
|
||||||
))
|
|
||||||
|
|
||||||
return path.normalized()
|
return path.normalized()
|
||||||
|
|
||||||
|
|
||||||
def get_representation_path(representation, root=None):
|
|
||||||
"""Get filename from representation document
|
|
||||||
|
|
||||||
There are three ways of getting the path from representation which are
|
|
||||||
tried in following sequence until successful.
|
|
||||||
1. Get template from representation['data']['template'] and data from
|
|
||||||
representation['context']. Then format template with the data.
|
|
||||||
2. Get template from project['config'] and format it with default data set
|
|
||||||
3. Get representation['data']['path'] and use it directly
|
|
||||||
|
|
||||||
Args:
|
|
||||||
representation(dict): representation document from the database
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: fullpath of the representation
|
|
||||||
|
|
||||||
"""
|
|
||||||
if root is None:
|
|
||||||
from ayon_core.pipeline import get_current_project_name, Anatomy
|
|
||||||
|
|
||||||
anatomy = Anatomy(get_current_project_name())
|
|
||||||
return get_representation_path_with_anatomy(
|
|
||||||
representation, anatomy
|
|
||||||
)
|
|
||||||
|
|
||||||
def path_from_representation():
|
|
||||||
try:
|
|
||||||
template = representation["attrib"]["template"]
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
context = representation["context"]
|
|
||||||
|
|
||||||
_fix_representation_context_compatibility(context)
|
|
||||||
|
|
||||||
context["root"] = root
|
|
||||||
path = StringTemplate.format_strict_template(
|
|
||||||
template, context
|
|
||||||
)
|
|
||||||
# Force replacing backslashes with forward slashed if not on
|
|
||||||
# windows
|
|
||||||
if platform.system().lower() != "windows":
|
|
||||||
path = path.replace("\\", "/")
|
|
||||||
except (TemplateUnsolved, KeyError):
|
|
||||||
# Template references unavailable data
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not path:
|
|
||||||
return path
|
|
||||||
|
|
||||||
normalized_path = os.path.normpath(path)
|
|
||||||
if os.path.exists(normalized_path):
|
|
||||||
return normalized_path
|
|
||||||
return path
|
|
||||||
|
|
||||||
def path_from_data():
|
|
||||||
if "path" not in representation["attrib"]:
|
|
||||||
return None
|
|
||||||
|
|
||||||
path = representation["attrib"]["path"]
|
|
||||||
# Force replacing backslashes with forward slashed if not on
|
|
||||||
# windows
|
|
||||||
if platform.system().lower() != "windows":
|
|
||||||
path = path.replace("\\", "/")
|
|
||||||
|
|
||||||
if os.path.exists(path):
|
|
||||||
return os.path.normpath(path)
|
|
||||||
|
|
||||||
dir_path, file_name = os.path.split(path)
|
|
||||||
if not os.path.exists(dir_path):
|
|
||||||
return None
|
|
||||||
|
|
||||||
base_name, ext = os.path.splitext(file_name)
|
|
||||||
file_name_items = None
|
|
||||||
if "#" in base_name:
|
|
||||||
file_name_items = [part for part in base_name.split("#") if part]
|
|
||||||
elif "%" in base_name:
|
|
||||||
file_name_items = base_name.split("%")
|
|
||||||
|
|
||||||
if not file_name_items:
|
|
||||||
return None
|
|
||||||
|
|
||||||
filename_start = file_name_items[0]
|
|
||||||
|
|
||||||
for _file in os.listdir(dir_path):
|
|
||||||
if _file.startswith(filename_start) and _file.endswith(ext):
|
|
||||||
return os.path.normpath(path)
|
|
||||||
|
|
||||||
return (
|
|
||||||
path_from_representation() or path_from_data()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_representation_path_by_names(
|
def get_representation_path_by_names(
|
||||||
project_name: str,
|
project_name: str,
|
||||||
folder_path: str,
|
folder_path: str,
|
||||||
product_name: str,
|
product_name: str,
|
||||||
version_name: str,
|
version_name: Union[int, str],
|
||||||
representation_name: str,
|
representation_name: str,
|
||||||
anatomy: Optional[Anatomy] = None) -> Optional[str]:
|
anatomy: Optional[Anatomy] = None
|
||||||
|
) -> Optional[TemplateResult]:
|
||||||
"""Get (latest) filepath for representation for folder and product.
|
"""Get (latest) filepath for representation for folder and product.
|
||||||
|
|
||||||
See `get_representation_by_names` for more details.
|
See `get_representation_by_names` for more details.
|
||||||
|
|
@ -820,22 +932,21 @@ def get_representation_path_by_names(
|
||||||
representation_name
|
representation_name
|
||||||
)
|
)
|
||||||
if not representation:
|
if not representation:
|
||||||
return
|
return None
|
||||||
|
|
||||||
if not anatomy:
|
return get_representation_path(
|
||||||
anatomy = Anatomy(project_name)
|
project_name,
|
||||||
|
representation,
|
||||||
if representation:
|
anatomy=anatomy,
|
||||||
path = get_representation_path_with_anatomy(representation, anatomy)
|
)
|
||||||
return str(path).replace("\\", "/")
|
|
||||||
|
|
||||||
|
|
||||||
def get_representation_by_names(
|
def get_representation_by_names(
|
||||||
project_name: str,
|
project_name: str,
|
||||||
folder_path: str,
|
folder_path: str,
|
||||||
product_name: str,
|
product_name: str,
|
||||||
version_name: Union[int, str],
|
version_name: Union[int, str],
|
||||||
representation_name: str,
|
representation_name: str,
|
||||||
) -> Optional[dict]:
|
) -> Optional[dict]:
|
||||||
"""Get representation entity for asset and subset.
|
"""Get representation entity for asset and subset.
|
||||||
|
|
||||||
|
|
@ -852,7 +963,7 @@ def get_representation_by_names(
|
||||||
folder_entity = ayon_api.get_folder_by_path(
|
folder_entity = ayon_api.get_folder_by_path(
|
||||||
project_name, folder_path, fields=["id"])
|
project_name, folder_path, fields=["id"])
|
||||||
if not folder_entity:
|
if not folder_entity:
|
||||||
return
|
return None
|
||||||
|
|
||||||
if isinstance(product_name, dict) and "name" in product_name:
|
if isinstance(product_name, dict) and "name" in product_name:
|
||||||
# Allow explicitly passing subset document
|
# Allow explicitly passing subset document
|
||||||
|
|
@ -864,7 +975,7 @@ def get_representation_by_names(
|
||||||
folder_id=folder_entity["id"],
|
folder_id=folder_entity["id"],
|
||||||
fields=["id"])
|
fields=["id"])
|
||||||
if not product_entity:
|
if not product_entity:
|
||||||
return
|
return None
|
||||||
|
|
||||||
if version_name == "hero":
|
if version_name == "hero":
|
||||||
version_entity = ayon_api.get_hero_version_by_product_id(
|
version_entity = ayon_api.get_hero_version_by_product_id(
|
||||||
|
|
@ -876,7 +987,7 @@ def get_representation_by_names(
|
||||||
version_entity = ayon_api.get_version_by_name(
|
version_entity = ayon_api.get_version_by_name(
|
||||||
project_name, version_name, product_id=product_entity["id"])
|
project_name, version_name, product_id=product_entity["id"])
|
||||||
if not version_entity:
|
if not version_entity:
|
||||||
return
|
return None
|
||||||
|
|
||||||
return ayon_api.get_representation_by_name(
|
return ayon_api.get_representation_by_name(
|
||||||
project_name, representation_name, version_id=version_entity["id"])
|
project_name, representation_name, version_id=version_entity["id"])
|
||||||
|
|
|
||||||
|
|
@ -300,7 +300,11 @@ class AbstractTemplateBuilder(ABC):
|
||||||
self._loaders_by_name = get_loaders_by_name()
|
self._loaders_by_name = get_loaders_by_name()
|
||||||
return self._loaders_by_name
|
return self._loaders_by_name
|
||||||
|
|
||||||
def get_linked_folder_entities(self, link_type: Optional[str]):
|
def get_linked_folder_entities(
|
||||||
|
self,
|
||||||
|
link_type: Optional[str],
|
||||||
|
folder_path_regex: Optional[str],
|
||||||
|
):
|
||||||
if not link_type:
|
if not link_type:
|
||||||
return []
|
return []
|
||||||
project_name = self.project_name
|
project_name = self.project_name
|
||||||
|
|
@ -317,7 +321,11 @@ class AbstractTemplateBuilder(ABC):
|
||||||
if link["entityType"] == "folder"
|
if link["entityType"] == "folder"
|
||||||
}
|
}
|
||||||
|
|
||||||
return list(get_folders(project_name, folder_ids=linked_folder_ids))
|
return list(get_folders(
|
||||||
|
project_name,
|
||||||
|
folder_path_regex=folder_path_regex,
|
||||||
|
folder_ids=linked_folder_ids,
|
||||||
|
))
|
||||||
|
|
||||||
def _collect_creators(self):
|
def _collect_creators(self):
|
||||||
self._creators_by_name = {
|
self._creators_by_name = {
|
||||||
|
|
@ -1638,7 +1646,10 @@ class PlaceholderLoadMixin(object):
|
||||||
linked_folder_entity["id"]
|
linked_folder_entity["id"]
|
||||||
for linked_folder_entity in (
|
for linked_folder_entity in (
|
||||||
self.builder.get_linked_folder_entities(
|
self.builder.get_linked_folder_entities(
|
||||||
link_type=link_type))
|
link_type=link_type,
|
||||||
|
folder_path_regex=folder_path_regex
|
||||||
|
)
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
if not folder_ids:
|
if not folder_ids:
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ class CreateHeroVersion(load.ProductLoaderPlugin):
|
||||||
msgBox.setStyleSheet(style.load_stylesheet())
|
msgBox.setStyleSheet(style.load_stylesheet())
|
||||||
msgBox.setWindowFlags(
|
msgBox.setWindowFlags(
|
||||||
msgBox.windowFlags() | QtCore.Qt.WindowType.FramelessWindowHint
|
msgBox.windowFlags() | QtCore.Qt.WindowType.FramelessWindowHint
|
||||||
|
| QtCore.Qt.WindowType.WindowStaysOnTopHint
|
||||||
)
|
)
|
||||||
msgBox.exec_()
|
msgBox.exec_()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ class CollectAudio(pyblish.api.ContextPlugin):
|
||||||
context, self.__class__
|
context, self.__class__
|
||||||
):
|
):
|
||||||
# Skip instances that already have audio filled
|
# Skip instances that already have audio filled
|
||||||
if instance.data.get("audio"):
|
if "audio" in instance.data:
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
"Skipping Audio collection. It is already collected"
|
"Skipping Audio collection. It is already collected"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ from ayon_core.lib import (
|
||||||
is_oiio_supported,
|
is_oiio_supported,
|
||||||
)
|
)
|
||||||
from ayon_core.lib.transcoding import (
|
from ayon_core.lib.transcoding import (
|
||||||
|
MissingRGBAChannelsError,
|
||||||
oiio_color_convert,
|
oiio_color_convert,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -111,7 +112,17 @@ class ExtractOIIOTranscode(publish.Extractor):
|
||||||
self.log.warning("Config file doesn't exist, skipping")
|
self.log.warning("Config file doesn't exist, skipping")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Get representation files to convert
|
||||||
|
if isinstance(repre["files"], list):
|
||||||
|
repre_files_to_convert = copy.deepcopy(repre["files"])
|
||||||
|
else:
|
||||||
|
repre_files_to_convert = [repre["files"]]
|
||||||
|
|
||||||
|
# Process each output definition
|
||||||
for output_def in profile_output_defs:
|
for output_def in profile_output_defs:
|
||||||
|
# Local copy to avoid accidental mutable changes
|
||||||
|
files_to_convert = list(repre_files_to_convert)
|
||||||
|
|
||||||
output_name = output_def["name"]
|
output_name = output_def["name"]
|
||||||
new_repre = copy.deepcopy(repre)
|
new_repre = copy.deepcopy(repre)
|
||||||
|
|
||||||
|
|
@ -122,11 +133,6 @@ class ExtractOIIOTranscode(publish.Extractor):
|
||||||
)
|
)
|
||||||
new_repre["stagingDir"] = new_staging_dir
|
new_repre["stagingDir"] = new_staging_dir
|
||||||
|
|
||||||
if isinstance(new_repre["files"], list):
|
|
||||||
files_to_convert = copy.deepcopy(new_repre["files"])
|
|
||||||
else:
|
|
||||||
files_to_convert = [new_repre["files"]]
|
|
||||||
|
|
||||||
output_extension = output_def["extension"]
|
output_extension = output_def["extension"]
|
||||||
output_extension = output_extension.replace('.', '')
|
output_extension = output_extension.replace('.', '')
|
||||||
self._rename_in_representation(new_repre,
|
self._rename_in_representation(new_repre,
|
||||||
|
|
@ -168,30 +174,49 @@ class ExtractOIIOTranscode(publish.Extractor):
|
||||||
additional_command_args = (output_def["oiiotool_args"]
|
additional_command_args = (output_def["oiiotool_args"]
|
||||||
["additional_command_args"])
|
["additional_command_args"])
|
||||||
|
|
||||||
files_to_convert = self._translate_to_sequence(
|
sequence_files = self._translate_to_sequence(files_to_convert)
|
||||||
files_to_convert)
|
self.log.debug("Files to convert: {}".format(sequence_files))
|
||||||
self.log.debug("Files to convert: {}".format(files_to_convert))
|
missing_rgba_review_channels = False
|
||||||
for file_name in files_to_convert:
|
for file_name in sequence_files:
|
||||||
|
if isinstance(file_name, clique.Collection):
|
||||||
|
# Convert to filepath that can be directly converted
|
||||||
|
# by oiio like `frame.1001-1025%04d.exr`
|
||||||
|
file_name: str = file_name.format(
|
||||||
|
"{head}{range}{padding}{tail}"
|
||||||
|
)
|
||||||
|
|
||||||
self.log.debug("Transcoding file: `{}`".format(file_name))
|
self.log.debug("Transcoding file: `{}`".format(file_name))
|
||||||
input_path = os.path.join(original_staging_dir,
|
input_path = os.path.join(original_staging_dir,
|
||||||
file_name)
|
file_name)
|
||||||
output_path = self._get_output_file_path(input_path,
|
output_path = self._get_output_file_path(input_path,
|
||||||
new_staging_dir,
|
new_staging_dir,
|
||||||
output_extension)
|
output_extension)
|
||||||
|
try:
|
||||||
|
oiio_color_convert(
|
||||||
|
input_path=input_path,
|
||||||
|
output_path=output_path,
|
||||||
|
config_path=config_path,
|
||||||
|
source_colorspace=source_colorspace,
|
||||||
|
target_colorspace=target_colorspace,
|
||||||
|
target_display=target_display,
|
||||||
|
target_view=target_view,
|
||||||
|
source_display=source_display,
|
||||||
|
source_view=source_view,
|
||||||
|
additional_command_args=additional_command_args,
|
||||||
|
logger=self.log
|
||||||
|
)
|
||||||
|
except MissingRGBAChannelsError as exc:
|
||||||
|
missing_rgba_review_channels = True
|
||||||
|
self.log.error(exc)
|
||||||
|
self.log.error(
|
||||||
|
"Skipping OIIO Transcode. Unknown RGBA channels"
|
||||||
|
f" for colorspace conversion in file: {input_path}"
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
oiio_color_convert(
|
if missing_rgba_review_channels:
|
||||||
input_path=input_path,
|
# Stop processing this representation
|
||||||
output_path=output_path,
|
break
|
||||||
config_path=config_path,
|
|
||||||
source_colorspace=source_colorspace,
|
|
||||||
target_colorspace=target_colorspace,
|
|
||||||
target_display=target_display,
|
|
||||||
target_view=target_view,
|
|
||||||
source_display=source_display,
|
|
||||||
source_view=source_view,
|
|
||||||
additional_command_args=additional_command_args,
|
|
||||||
logger=self.log
|
|
||||||
)
|
|
||||||
|
|
||||||
# cleanup temporary transcoded files
|
# cleanup temporary transcoded files
|
||||||
for file_name in new_repre["files"]:
|
for file_name in new_repre["files"]:
|
||||||
|
|
@ -217,11 +242,11 @@ class ExtractOIIOTranscode(publish.Extractor):
|
||||||
added_review = True
|
added_review = True
|
||||||
|
|
||||||
# If there is only 1 file outputted then convert list to
|
# If there is only 1 file outputted then convert list to
|
||||||
# string, cause that'll indicate that its not a sequence.
|
# string, because that'll indicate that it is not a sequence.
|
||||||
if len(new_repre["files"]) == 1:
|
if len(new_repre["files"]) == 1:
|
||||||
new_repre["files"] = new_repre["files"][0]
|
new_repre["files"] = new_repre["files"][0]
|
||||||
|
|
||||||
# If the source representation has "review" tag, but its not
|
# If the source representation has "review" tag, but it's not
|
||||||
# part of the output definition tags, then both the
|
# part of the output definition tags, then both the
|
||||||
# representations will be transcoded in ExtractReview and
|
# representations will be transcoded in ExtractReview and
|
||||||
# their outputs will clash in integration.
|
# their outputs will clash in integration.
|
||||||
|
|
@ -271,42 +296,34 @@ class ExtractOIIOTranscode(publish.Extractor):
|
||||||
new_repre["files"] = renamed_files
|
new_repre["files"] = renamed_files
|
||||||
|
|
||||||
def _translate_to_sequence(self, files_to_convert):
|
def _translate_to_sequence(self, files_to_convert):
|
||||||
"""Returns original list or list with filename formatted in single
|
"""Returns original list or a clique.Collection of a sequence.
|
||||||
sequence format.
|
|
||||||
|
|
||||||
Uses clique to find frame sequence, in this case it merges all frames
|
Uses clique to find frame sequence Collection.
|
||||||
into sequence format (FRAMESTART-FRAMEEND#) and returns it.
|
If sequence not found, it returns original list.
|
||||||
If sequence not found, it returns original list
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
files_to_convert (list): list of file names
|
files_to_convert (list): list of file names
|
||||||
Returns:
|
Returns:
|
||||||
(list) of [file.1001-1010#.exr] or [fileA.exr, fileB.exr]
|
list[str | clique.Collection]: List of filepaths or a list
|
||||||
|
of Collections (usually one, unless there are holes)
|
||||||
"""
|
"""
|
||||||
pattern = [clique.PATTERNS["frames"]]
|
pattern = [clique.PATTERNS["frames"]]
|
||||||
collections, _ = clique.assemble(
|
collections, _ = clique.assemble(
|
||||||
files_to_convert, patterns=pattern,
|
files_to_convert, patterns=pattern,
|
||||||
assume_padded_when_ambiguous=True)
|
assume_padded_when_ambiguous=True)
|
||||||
|
|
||||||
if collections:
|
if collections:
|
||||||
if len(collections) > 1:
|
if len(collections) > 1:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Too many collections {}".format(collections))
|
"Too many collections {}".format(collections))
|
||||||
|
|
||||||
collection = collections[0]
|
collection = collections[0]
|
||||||
frames = list(collection.indexes)
|
# TODO: Technically oiiotool supports holes in the sequence as well
|
||||||
if collection.holes().indexes:
|
# using the dedicated --frames argument to specify the frames.
|
||||||
return files_to_convert
|
# We may want to use that too so conversions of sequences with
|
||||||
|
# holes will perform faster as well.
|
||||||
# Get the padding from the collection
|
# Separate the collection so that we have no holes/gaps per
|
||||||
# This is the number of digits used in the frame numbers
|
# collection.
|
||||||
padding = collection.padding
|
return collection.separate()
|
||||||
|
|
||||||
frame_str = "{}-{}%0{}d".format(frames[0], frames[-1], padding)
|
|
||||||
file_name = "{}{}{}".format(collection.head, frame_str,
|
|
||||||
collection.tail)
|
|
||||||
|
|
||||||
files_to_convert = [file_name]
|
|
||||||
|
|
||||||
return files_to_convert
|
return files_to_convert
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,83 @@
|
||||||
|
import collections
|
||||||
|
import hashlib
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pyblish
|
import pyblish
|
||||||
|
from ayon_core.lib import get_ffmpeg_tool_args, run_subprocess
|
||||||
|
|
||||||
from ayon_core.lib import (
|
|
||||||
get_ffmpeg_tool_args,
|
def get_audio_instances(context):
|
||||||
run_subprocess
|
"""Return only instances which are having audio in families
|
||||||
)
|
|
||||||
|
Args:
|
||||||
|
context (pyblish.context): context of publisher
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: list of selected instances
|
||||||
|
"""
|
||||||
|
audio_instances = []
|
||||||
|
for instance in context:
|
||||||
|
if not instance.data.get("parent_instance_id"):
|
||||||
|
continue
|
||||||
|
if (
|
||||||
|
instance.data["productType"] == "audio"
|
||||||
|
or instance.data.get("reviewAudio")
|
||||||
|
):
|
||||||
|
audio_instances.append(instance)
|
||||||
|
return audio_instances
|
||||||
|
|
||||||
|
|
||||||
|
def map_instances_by_parent_id(context):
|
||||||
|
"""Create a mapping of instances by their parent id
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context (pyblish.context): context of publisher
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: mapping of instances by their parent id
|
||||||
|
"""
|
||||||
|
instances_by_parent_id = collections.defaultdict(list)
|
||||||
|
for instance in context:
|
||||||
|
parent_instance_id = instance.data.get("parent_instance_id")
|
||||||
|
if not parent_instance_id:
|
||||||
|
continue
|
||||||
|
instances_by_parent_id[parent_instance_id].append(instance)
|
||||||
|
return instances_by_parent_id
|
||||||
|
|
||||||
|
|
||||||
|
class CollectParentAudioInstanceAttribute(pyblish.api.ContextPlugin):
|
||||||
|
"""Collect audio instance attribute"""
|
||||||
|
|
||||||
|
order = pyblish.api.CollectorOrder
|
||||||
|
label = "Collect Audio Instance Attribute"
|
||||||
|
|
||||||
|
def process(self, context):
|
||||||
|
|
||||||
|
audio_instances = get_audio_instances(context)
|
||||||
|
|
||||||
|
# no need to continue if no audio instances found
|
||||||
|
if not audio_instances:
|
||||||
|
return
|
||||||
|
|
||||||
|
# create mapped instances by parent id
|
||||||
|
instances_by_parent_id = map_instances_by_parent_id(context)
|
||||||
|
|
||||||
|
# distribute audio related attribute
|
||||||
|
for audio_instance in audio_instances:
|
||||||
|
parent_instance_id = audio_instance.data["parent_instance_id"]
|
||||||
|
|
||||||
|
for sibl_instance in instances_by_parent_id[parent_instance_id]:
|
||||||
|
# exclude the same audio instance
|
||||||
|
if sibl_instance.id == audio_instance.id:
|
||||||
|
continue
|
||||||
|
self.log.info(
|
||||||
|
"Adding audio to Sibling instance: "
|
||||||
|
f"{sibl_instance.data['label']}"
|
||||||
|
)
|
||||||
|
sibl_instance.data["audio"] = None
|
||||||
|
|
||||||
|
|
||||||
class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
|
class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
|
||||||
|
|
@ -19,7 +90,8 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
|
||||||
|
|
||||||
order = pyblish.api.ExtractorOrder - 0.44
|
order = pyblish.api.ExtractorOrder - 0.44
|
||||||
label = "Extract OTIO Audio Tracks"
|
label = "Extract OTIO Audio Tracks"
|
||||||
hosts = ["hiero", "resolve", "flame"]
|
|
||||||
|
temp_dir_path = None
|
||||||
|
|
||||||
def process(self, context):
|
def process(self, context):
|
||||||
"""Convert otio audio track's content to audio representations
|
"""Convert otio audio track's content to audio representations
|
||||||
|
|
@ -28,13 +100,14 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
|
||||||
context (pyblish.Context): context of publisher
|
context (pyblish.Context): context of publisher
|
||||||
"""
|
"""
|
||||||
# split the long audio file to peces devided by isntances
|
# split the long audio file to peces devided by isntances
|
||||||
audio_instances = self.get_audio_instances(context)
|
audio_instances = get_audio_instances(context)
|
||||||
self.log.debug("Audio instances: {}".format(len(audio_instances)))
|
|
||||||
|
|
||||||
if len(audio_instances) < 1:
|
# no need to continue if no audio instances found
|
||||||
self.log.info("No audio instances available")
|
if not audio_instances:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.log.debug("Audio instances: {}".format(len(audio_instances)))
|
||||||
|
|
||||||
# get sequence
|
# get sequence
|
||||||
otio_timeline = context.data["otioTimeline"]
|
otio_timeline = context.data["otioTimeline"]
|
||||||
|
|
||||||
|
|
@ -44,8 +117,8 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
|
||||||
if not audio_inputs:
|
if not audio_inputs:
|
||||||
return
|
return
|
||||||
|
|
||||||
# temp file
|
# Convert all available audio into single file for trimming
|
||||||
audio_temp_fpath = self.create_temp_file("audio")
|
audio_temp_fpath = self.create_temp_file("timeline_audio_track")
|
||||||
|
|
||||||
# create empty audio with longest duration
|
# create empty audio with longest duration
|
||||||
empty = self.create_empty(audio_inputs)
|
empty = self.create_empty(audio_inputs)
|
||||||
|
|
@ -59,19 +132,25 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
|
||||||
# remove empty
|
# remove empty
|
||||||
os.remove(empty["mediaPath"])
|
os.remove(empty["mediaPath"])
|
||||||
|
|
||||||
|
# create mapped instances by parent id
|
||||||
|
instances_by_parent_id = map_instances_by_parent_id(context)
|
||||||
|
|
||||||
# cut instance framerange and add to representations
|
# cut instance framerange and add to representations
|
||||||
self.add_audio_to_instances(audio_temp_fpath, audio_instances)
|
self.add_audio_to_instances(
|
||||||
|
audio_temp_fpath, audio_instances, instances_by_parent_id)
|
||||||
|
|
||||||
# remove full mixed audio file
|
# remove full mixed audio file
|
||||||
os.remove(audio_temp_fpath)
|
os.remove(audio_temp_fpath)
|
||||||
|
|
||||||
def add_audio_to_instances(self, audio_file, instances):
|
def add_audio_to_instances(
|
||||||
|
self, audio_file, audio_instances, instances_by_parent_id):
|
||||||
created_files = []
|
created_files = []
|
||||||
for inst in instances:
|
for audio_instance in audio_instances:
|
||||||
name = inst.data["folderPath"]
|
folder_path = audio_instance.data["folderPath"]
|
||||||
|
file_suffix = folder_path.replace("/", "-")
|
||||||
|
|
||||||
recycling_file = [f for f in created_files if name in f]
|
recycling_file = [f for f in created_files if file_suffix in f]
|
||||||
audio_clip = inst.data["otioClip"]
|
audio_clip = audio_instance.data["otioClip"]
|
||||||
audio_range = audio_clip.range_in_parent()
|
audio_range = audio_clip.range_in_parent()
|
||||||
duration = audio_range.duration.to_frames()
|
duration = audio_range.duration.to_frames()
|
||||||
|
|
||||||
|
|
@ -84,68 +163,70 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
|
||||||
start_sec = relative_start_time.to_seconds()
|
start_sec = relative_start_time.to_seconds()
|
||||||
duration_sec = audio_range.duration.to_seconds()
|
duration_sec = audio_range.duration.to_seconds()
|
||||||
|
|
||||||
# temp audio file
|
# shot related audio file
|
||||||
audio_fpath = self.create_temp_file(name)
|
shot_audio_fpath = self.create_temp_file(file_suffix)
|
||||||
|
|
||||||
cmd = get_ffmpeg_tool_args(
|
cmd = get_ffmpeg_tool_args(
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
"-ss", str(start_sec),
|
"-ss", str(start_sec),
|
||||||
"-t", str(duration_sec),
|
"-t", str(duration_sec),
|
||||||
"-i", audio_file,
|
"-i", audio_file,
|
||||||
audio_fpath
|
shot_audio_fpath
|
||||||
)
|
)
|
||||||
|
|
||||||
# run subprocess
|
# run subprocess
|
||||||
self.log.debug("Executing: {}".format(" ".join(cmd)))
|
self.log.debug("Executing: {}".format(" ".join(cmd)))
|
||||||
run_subprocess(cmd, logger=self.log)
|
run_subprocess(cmd, logger=self.log)
|
||||||
else:
|
|
||||||
audio_fpath = recycling_file.pop()
|
|
||||||
|
|
||||||
if "audio" in (
|
# add generated audio file to created files for recycling
|
||||||
inst.data["families"] + [inst.data["productType"]]
|
if shot_audio_fpath not in created_files:
|
||||||
):
|
created_files.append(shot_audio_fpath)
|
||||||
|
else:
|
||||||
|
shot_audio_fpath = recycling_file.pop()
|
||||||
|
|
||||||
|
# audio file needs to be published as representation
|
||||||
|
if audio_instance.data["productType"] == "audio":
|
||||||
# create empty representation attr
|
# create empty representation attr
|
||||||
if "representations" not in inst.data:
|
if "representations" not in audio_instance.data:
|
||||||
inst.data["representations"] = []
|
audio_instance.data["representations"] = []
|
||||||
# add to representations
|
# add to representations
|
||||||
inst.data["representations"].append({
|
audio_instance.data["representations"].append({
|
||||||
"files": os.path.basename(audio_fpath),
|
"files": os.path.basename(shot_audio_fpath),
|
||||||
"name": "wav",
|
"name": "wav",
|
||||||
"ext": "wav",
|
"ext": "wav",
|
||||||
"stagingDir": os.path.dirname(audio_fpath),
|
"stagingDir": os.path.dirname(shot_audio_fpath),
|
||||||
"frameStart": 0,
|
"frameStart": 0,
|
||||||
"frameEnd": duration
|
"frameEnd": duration
|
||||||
})
|
})
|
||||||
|
|
||||||
elif "reviewAudio" in inst.data.keys():
|
# audio file needs to be reviewable too
|
||||||
audio_attr = inst.data.get("audio") or []
|
elif "reviewAudio" in audio_instance.data.keys():
|
||||||
|
audio_attr = audio_instance.data.get("audio") or []
|
||||||
audio_attr.append({
|
audio_attr.append({
|
||||||
"filename": audio_fpath,
|
"filename": shot_audio_fpath,
|
||||||
"offset": 0
|
"offset": 0
|
||||||
})
|
})
|
||||||
inst.data["audio"] = audio_attr
|
audio_instance.data["audio"] = audio_attr
|
||||||
|
|
||||||
# add generated audio file to created files for recycling
|
# Make sure if the audio instance is having siblink instances
|
||||||
if audio_fpath not in created_files:
|
# which needs audio for reviewable media so it is also added
|
||||||
created_files.append(audio_fpath)
|
# to its instance data
|
||||||
|
# Retrieve instance data from parent instance shot instance.
|
||||||
def get_audio_instances(self, context):
|
parent_instance_id = audio_instance.data["parent_instance_id"]
|
||||||
"""Return only instances which are having audio in families
|
for sibl_instance in instances_by_parent_id[parent_instance_id]:
|
||||||
|
# exclude the same audio instance
|
||||||
Args:
|
if sibl_instance.id == audio_instance.id:
|
||||||
context (pyblish.context): context of publisher
|
continue
|
||||||
|
self.log.info(
|
||||||
Returns:
|
"Adding audio to Sibling instance: "
|
||||||
list: list of selected instances
|
f"{sibl_instance.data['label']}"
|
||||||
"""
|
)
|
||||||
return [
|
audio_attr = sibl_instance.data.get("audio") or []
|
||||||
_i for _i in context
|
audio_attr.append({
|
||||||
# filter only those with audio product type or family
|
"filename": shot_audio_fpath,
|
||||||
# and also with reviewAudio data key
|
"offset": 0
|
||||||
if bool("audio" in (
|
})
|
||||||
_i.data.get("families", []) + [_i.data["productType"]])
|
sibl_instance.data["audio"] = audio_attr
|
||||||
) or _i.data.get("reviewAudio")
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_audio_track_items(self, otio_timeline):
|
def get_audio_track_items(self, otio_timeline):
|
||||||
"""Get all audio clips form OTIO audio tracks
|
"""Get all audio clips form OTIO audio tracks
|
||||||
|
|
@ -321,19 +402,23 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
|
||||||
|
|
||||||
os.remove(filters_tmp_filepath)
|
os.remove(filters_tmp_filepath)
|
||||||
|
|
||||||
def create_temp_file(self, name):
|
def create_temp_file(self, file_suffix):
|
||||||
"""Create temp wav file
|
"""Create temp wav file
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str): name to be used in file name
|
file_suffix (str): name to be used in file name
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: temp fpath
|
str: temp fpath
|
||||||
"""
|
"""
|
||||||
name = name.replace("/", "_")
|
extension = ".wav"
|
||||||
return os.path.normpath(
|
# get 8 characters
|
||||||
tempfile.mktemp(
|
hash = hashlib.md5(str(uuid.uuid4()).encode()).hexdigest()[:8]
|
||||||
prefix="pyblish_tmp_{}_".format(name),
|
file_name = f"{hash}_{file_suffix}{extension}"
|
||||||
suffix=".wav"
|
|
||||||
)
|
if not self.temp_dir_path:
|
||||||
)
|
audio_temp_dir_path = tempfile.mkdtemp(prefix="AYON_audio_")
|
||||||
|
self.temp_dir_path = Path(audio_temp_dir_path)
|
||||||
|
self.temp_dir_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
return (self.temp_dir_path / file_name).as_posix()
|
||||||
|
|
|
||||||
|
|
@ -361,14 +361,14 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
||||||
if not filtered_output_defs:
|
if not filtered_output_defs:
|
||||||
self.log.debug((
|
self.log.debug((
|
||||||
"Repre: {} - All output definitions were filtered"
|
"Repre: {} - All output definitions were filtered"
|
||||||
" out by single frame filter. Skipping"
|
" out by single frame filter. Skipped."
|
||||||
).format(repre["name"]))
|
).format(repre["name"]))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Skip if file is not set
|
# Skip if file is not set
|
||||||
if first_input_path is None:
|
if first_input_path is None:
|
||||||
self.log.warning((
|
self.log.warning((
|
||||||
"Representation \"{}\" have empty files. Skipped."
|
"Representation \"{}\" has empty files. Skipped."
|
||||||
).format(repre["name"]))
|
).format(repre["name"]))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ from ayon_core.lib import (
|
||||||
run_subprocess,
|
run_subprocess,
|
||||||
)
|
)
|
||||||
from ayon_core.lib.transcoding import (
|
from ayon_core.lib.transcoding import (
|
||||||
|
MissingRGBAChannelsError,
|
||||||
oiio_color_convert,
|
oiio_color_convert,
|
||||||
get_oiio_input_and_channel_args,
|
get_oiio_input_and_channel_args,
|
||||||
get_oiio_info_for_input,
|
get_oiio_info_for_input,
|
||||||
|
|
@ -477,7 +478,16 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
input_info = get_oiio_info_for_input(src_path, logger=self.log)
|
input_info = get_oiio_info_for_input(src_path, logger=self.log)
|
||||||
input_arg, channels_arg = get_oiio_input_and_channel_args(input_info)
|
try:
|
||||||
|
input_arg, channels_arg = get_oiio_input_and_channel_args(
|
||||||
|
input_info
|
||||||
|
)
|
||||||
|
except MissingRGBAChannelsError:
|
||||||
|
self.log.debug(
|
||||||
|
"Unable to find relevant reviewable channel for thumbnail "
|
||||||
|
"creation"
|
||||||
|
)
|
||||||
|
return False
|
||||||
oiio_cmd = get_oiio_tool_args(
|
oiio_cmd = get_oiio_tool_args(
|
||||||
"oiiotool",
|
"oiiotool",
|
||||||
input_arg, src_path,
|
input_arg, src_path,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import pyblish.api
|
import pyblish.api
|
||||||
|
|
@ -179,6 +180,8 @@ def get_instance_uri_path(
|
||||||
|
|
||||||
# Ensure `None` for now is also a string
|
# Ensure `None` for now is also a string
|
||||||
path = str(path)
|
path = str(path)
|
||||||
|
if platform.system().lower() == "windows":
|
||||||
|
path = path.replace("\\", "/")
|
||||||
|
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import contextlib
|
import contextlib
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
import ayon_api
|
import ayon_api
|
||||||
|
from ayon_api.graphql_queries import projects_graphql_query
|
||||||
|
|
||||||
from ayon_core.style import get_default_entity_icon_color
|
from ayon_core.style import get_default_entity_icon_color
|
||||||
from ayon_core.lib import CacheItem, NestedCacheItem
|
from ayon_core.lib import CacheItem, NestedCacheItem
|
||||||
|
|
@ -275,7 +277,7 @@ class ProductTypeIconMapping:
|
||||||
return self._definitions_by_name
|
return self._definitions_by_name
|
||||||
|
|
||||||
|
|
||||||
def _get_project_items_from_entitiy(
|
def _get_project_items_from_entity(
|
||||||
projects: list[dict[str, Any]]
|
projects: list[dict[str, Any]]
|
||||||
) -> list[ProjectItem]:
|
) -> list[ProjectItem]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -290,6 +292,7 @@ def _get_project_items_from_entitiy(
|
||||||
return [
|
return [
|
||||||
ProjectItem.from_entity(project)
|
ProjectItem.from_entity(project)
|
||||||
for project in projects
|
for project in projects
|
||||||
|
if project["active"]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -538,8 +541,32 @@ class ProjectsModel(object):
|
||||||
self._projects_cache.update_data(project_items)
|
self._projects_cache.update_data(project_items)
|
||||||
return self._projects_cache.get_data()
|
return self._projects_cache.get_data()
|
||||||
|
|
||||||
|
def _fetch_graphql_projects(self) -> list[dict[str, Any]]:
|
||||||
|
"""Fetch projects using GraphQl.
|
||||||
|
|
||||||
|
This method was added because ayon_api had a bug in 'get_projects'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[dict[str, Any]]: List of projects.
|
||||||
|
|
||||||
|
"""
|
||||||
|
api = ayon_api.get_server_api_connection()
|
||||||
|
query = projects_graphql_query({"name", "active", "library", "data"})
|
||||||
|
|
||||||
|
projects = []
|
||||||
|
for parsed_data in query.continuous_query(api):
|
||||||
|
for project in parsed_data["projects"]:
|
||||||
|
project_data = project["data"]
|
||||||
|
if project_data is None:
|
||||||
|
project["data"] = {}
|
||||||
|
elif isinstance(project_data, str):
|
||||||
|
project["data"] = json.loads(project_data)
|
||||||
|
projects.append(project)
|
||||||
|
return projects
|
||||||
|
|
||||||
def _query_projects(self) -> list[ProjectItem]:
|
def _query_projects(self) -> list[ProjectItem]:
|
||||||
projects = ayon_api.get_projects(fields=["name", "active", "library"])
|
projects = self._fetch_graphql_projects()
|
||||||
|
|
||||||
user = ayon_api.get_user()
|
user = ayon_api.get_user()
|
||||||
pinned_projects = (
|
pinned_projects = (
|
||||||
user
|
user
|
||||||
|
|
@ -548,7 +575,7 @@ class ProjectsModel(object):
|
||||||
.get("pinnedProjects")
|
.get("pinnedProjects")
|
||||||
) or []
|
) or []
|
||||||
pinned_projects = set(pinned_projects)
|
pinned_projects = set(pinned_projects)
|
||||||
project_items = _get_project_items_from_entitiy(list(projects))
|
project_items = _get_project_items_from_entity(list(projects))
|
||||||
for project in project_items:
|
for project in project_items:
|
||||||
project.is_pinned = project.name in pinned_projects
|
project.is_pinned = project.name in pinned_projects
|
||||||
return project_items
|
return project_items
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import json
|
import json
|
||||||
import collections
|
import collections
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import ayon_api
|
import ayon_api
|
||||||
from ayon_api.graphql import FIELD_VALUE, GraphQlQuery, fields_to_dict
|
from ayon_api.graphql import FIELD_VALUE, GraphQlQuery, fields_to_dict
|
||||||
|
|
||||||
from ayon_core.lib import NestedCacheItem
|
from ayon_core.lib import NestedCacheItem, get_ayon_username
|
||||||
|
|
||||||
|
NOT_SET = object()
|
||||||
|
|
||||||
|
|
||||||
# --- Implementation that should be in ayon-python-api ---
|
# --- Implementation that should be in ayon-python-api ---
|
||||||
|
|
@ -105,9 +108,18 @@ class UserItem:
|
||||||
|
|
||||||
class UsersModel:
|
class UsersModel:
|
||||||
def __init__(self, controller):
|
def __init__(self, controller):
|
||||||
|
self._current_username = NOT_SET
|
||||||
self._controller = controller
|
self._controller = controller
|
||||||
self._users_cache = NestedCacheItem(default_factory=list)
|
self._users_cache = NestedCacheItem(default_factory=list)
|
||||||
|
|
||||||
|
def get_current_username(self) -> Optional[str]:
|
||||||
|
if self._current_username is NOT_SET:
|
||||||
|
self._current_username = get_ayon_username()
|
||||||
|
return self._current_username
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
self._users_cache.reset()
|
||||||
|
|
||||||
def get_user_items(self, project_name):
|
def get_user_items(self, project_name):
|
||||||
"""Get user items.
|
"""Get user items.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from ayon_core.lib import Logger, get_ayon_username
|
from ayon_core.lib import Logger
|
||||||
from ayon_core.lib.events import QueuedEventSystem
|
from ayon_core.lib.events import QueuedEventSystem
|
||||||
from ayon_core.addon import AddonsManager
|
from ayon_core.addon import AddonsManager
|
||||||
from ayon_core.settings import get_project_settings, get_studio_settings
|
from ayon_core.settings import get_project_settings, get_studio_settings
|
||||||
from ayon_core.tools.common_models import ProjectsModel, HierarchyModel
|
from ayon_core.tools.common_models import (
|
||||||
|
ProjectsModel,
|
||||||
|
HierarchyModel,
|
||||||
|
UsersModel,
|
||||||
|
)
|
||||||
|
|
||||||
from .abstract import (
|
from .abstract import (
|
||||||
AbstractLauncherFrontEnd,
|
AbstractLauncherFrontEnd,
|
||||||
|
|
@ -30,13 +34,12 @@ class BaseLauncherController(
|
||||||
|
|
||||||
self._addons_manager = None
|
self._addons_manager = None
|
||||||
|
|
||||||
self._username = NOT_SET
|
|
||||||
|
|
||||||
self._selection_model = LauncherSelectionModel(self)
|
self._selection_model = LauncherSelectionModel(self)
|
||||||
self._projects_model = ProjectsModel(self)
|
self._projects_model = ProjectsModel(self)
|
||||||
self._hierarchy_model = HierarchyModel(self)
|
self._hierarchy_model = HierarchyModel(self)
|
||||||
self._actions_model = ActionsModel(self)
|
self._actions_model = ActionsModel(self)
|
||||||
self._workfiles_model = WorkfilesModel(self)
|
self._workfiles_model = WorkfilesModel(self)
|
||||||
|
self._users_model = UsersModel(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def log(self):
|
def log(self):
|
||||||
|
|
@ -209,6 +212,7 @@ class BaseLauncherController(
|
||||||
|
|
||||||
self._projects_model.reset()
|
self._projects_model.reset()
|
||||||
self._hierarchy_model.reset()
|
self._hierarchy_model.reset()
|
||||||
|
self._users_model.reset()
|
||||||
|
|
||||||
self._actions_model.refresh()
|
self._actions_model.refresh()
|
||||||
self._projects_model.refresh()
|
self._projects_model.refresh()
|
||||||
|
|
@ -229,8 +233,10 @@ class BaseLauncherController(
|
||||||
|
|
||||||
self._emit_event("controller.refresh.actions.finished")
|
self._emit_event("controller.refresh.actions.finished")
|
||||||
|
|
||||||
def get_my_tasks_entity_ids(self, project_name: str):
|
def get_my_tasks_entity_ids(
|
||||||
username = self._get_my_username()
|
self, project_name: str
|
||||||
|
) -> dict[str, list[str]]:
|
||||||
|
username = self._users_model.get_current_username()
|
||||||
assignees = []
|
assignees = []
|
||||||
if username:
|
if username:
|
||||||
assignees.append(username)
|
assignees.append(username)
|
||||||
|
|
@ -238,10 +244,5 @@ class BaseLauncherController(
|
||||||
project_name, 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):
|
def _emit_event(self, topic, data=None):
|
||||||
self.emit_event(topic, data, "controller")
|
self.emit_event(topic, data, "controller")
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,47 @@ import qtawesome
|
||||||
from qtpy import QtWidgets, QtCore
|
from qtpy import QtWidgets, QtCore
|
||||||
|
|
||||||
from ayon_core.tools.utils import (
|
from ayon_core.tools.utils import (
|
||||||
PlaceholderLineEdit,
|
|
||||||
SquareButton,
|
SquareButton,
|
||||||
RefreshButton,
|
RefreshButton,
|
||||||
ProjectsCombobox,
|
ProjectsCombobox,
|
||||||
FoldersWidget,
|
FoldersWidget,
|
||||||
TasksWidget,
|
TasksWidget,
|
||||||
NiceCheckbox,
|
|
||||||
)
|
)
|
||||||
from ayon_core.tools.utils.lib import checkstate_int_to_enum
|
from ayon_core.tools.utils.folders_widget import FoldersFiltersWidget
|
||||||
|
|
||||||
from .workfiles_page import WorkfilesPage
|
from .workfiles_page import WorkfilesPage
|
||||||
|
|
||||||
|
|
||||||
|
class LauncherFoldersWidget(FoldersWidget):
|
||||||
|
focused_in = QtCore.Signal()
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._folders_view.installEventFilter(self)
|
||||||
|
|
||||||
|
def eventFilter(self, obj, event):
|
||||||
|
if event.type() == QtCore.QEvent.FocusIn:
|
||||||
|
self.focused_in.emit()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class LauncherTasksWidget(TasksWidget):
|
||||||
|
focused_in = QtCore.Signal()
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._tasks_view.installEventFilter(self)
|
||||||
|
|
||||||
|
def deselect(self):
|
||||||
|
sel_model = self._tasks_view.selectionModel()
|
||||||
|
sel_model.clearSelection()
|
||||||
|
|
||||||
|
def eventFilter(self, obj, event):
|
||||||
|
if event.type() == QtCore.QEvent.FocusIn:
|
||||||
|
self.focused_in.emit()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class HierarchyPage(QtWidgets.QWidget):
|
class HierarchyPage(QtWidgets.QWidget):
|
||||||
def __init__(self, controller, parent):
|
def __init__(self, controller, parent):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
@ -46,34 +74,15 @@ class HierarchyPage(QtWidgets.QWidget):
|
||||||
content_body.setOrientation(QtCore.Qt.Horizontal)
|
content_body.setOrientation(QtCore.Qt.Horizontal)
|
||||||
|
|
||||||
# - filters
|
# - filters
|
||||||
filters_widget = QtWidgets.QWidget(self)
|
filters_widget = FoldersFiltersWidget(self)
|
||||||
|
|
||||||
folders_filter_text = PlaceholderLineEdit(filters_widget)
|
|
||||||
folders_filter_text.setPlaceholderText("Filter folders...")
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
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
|
||||||
folders_widget = FoldersWidget(controller, content_body)
|
folders_widget = LauncherFoldersWidget(controller, content_body)
|
||||||
folders_widget.set_header_visible(True)
|
folders_widget.set_header_visible(True)
|
||||||
folders_widget.set_deselectable(True)
|
folders_widget.set_deselectable(True)
|
||||||
|
|
||||||
# - Tasks widget
|
# - Tasks widget
|
||||||
tasks_widget = TasksWidget(controller, content_body)
|
tasks_widget = LauncherTasksWidget(controller, content_body)
|
||||||
|
|
||||||
# - Third page - Workfiles
|
# - Third page - Workfiles
|
||||||
workfiles_page = WorkfilesPage(controller, content_body)
|
workfiles_page = WorkfilesPage(controller, content_body)
|
||||||
|
|
@ -93,17 +102,18 @@ class HierarchyPage(QtWidgets.QWidget):
|
||||||
|
|
||||||
btn_back.clicked.connect(self._on_back_clicked)
|
btn_back.clicked.connect(self._on_back_clicked)
|
||||||
refresh_btn.clicked.connect(self._on_refresh_clicked)
|
refresh_btn.clicked.connect(self._on_refresh_clicked)
|
||||||
folders_filter_text.textChanged.connect(self._on_filter_text_changed)
|
filters_widget.text_changed.connect(self._on_filter_text_changed)
|
||||||
my_tasks_checkbox.stateChanged.connect(
|
filters_widget.my_tasks_changed.connect(
|
||||||
self._on_my_tasks_checkbox_state_changed
|
self._on_my_tasks_checkbox_state_changed
|
||||||
)
|
)
|
||||||
|
folders_widget.focused_in.connect(self._on_folders_focus)
|
||||||
|
tasks_widget.focused_in.connect(self._on_tasks_focus)
|
||||||
|
|
||||||
self._is_visible = False
|
self._is_visible = False
|
||||||
self._controller = controller
|
self._controller = controller
|
||||||
|
|
||||||
self._btn_back = btn_back
|
self._btn_back = btn_back
|
||||||
self._projects_combobox = projects_combobox
|
self._projects_combobox = projects_combobox
|
||||||
self._my_tasks_checkbox = my_tasks_checkbox
|
|
||||||
self._folders_widget = folders_widget
|
self._folders_widget = folders_widget
|
||||||
self._tasks_widget = tasks_widget
|
self._tasks_widget = tasks_widget
|
||||||
self._workfiles_page = workfiles_page
|
self._workfiles_page = workfiles_page
|
||||||
|
|
@ -126,9 +136,6 @@ class HierarchyPage(QtWidgets.QWidget):
|
||||||
self._folders_widget.refresh()
|
self._folders_widget.refresh()
|
||||||
self._tasks_widget.refresh()
|
self._tasks_widget.refresh()
|
||||||
self._workfiles_page.refresh()
|
self._workfiles_page.refresh()
|
||||||
self._on_my_tasks_checkbox_state_changed(
|
|
||||||
self._my_tasks_checkbox.checkState()
|
|
||||||
)
|
|
||||||
|
|
||||||
def _on_back_clicked(self):
|
def _on_back_clicked(self):
|
||||||
self._controller.set_selected_project(None)
|
self._controller.set_selected_project(None)
|
||||||
|
|
@ -139,11 +146,10 @@ class HierarchyPage(QtWidgets.QWidget):
|
||||||
def _on_filter_text_changed(self, text):
|
def _on_filter_text_changed(self, text):
|
||||||
self._folders_widget.set_name_filter(text)
|
self._folders_widget.set_name_filter(text)
|
||||||
|
|
||||||
def _on_my_tasks_checkbox_state_changed(self, state):
|
def _on_my_tasks_checkbox_state_changed(self, enabled: bool) -> None:
|
||||||
folder_ids = None
|
folder_ids = None
|
||||||
task_ids = None
|
task_ids = None
|
||||||
state = checkstate_int_to_enum(state)
|
if enabled:
|
||||||
if state == QtCore.Qt.Checked:
|
|
||||||
entity_ids = self._controller.get_my_tasks_entity_ids(
|
entity_ids = self._controller.get_my_tasks_entity_ids(
|
||||||
self._project_name
|
self._project_name
|
||||||
)
|
)
|
||||||
|
|
@ -151,3 +157,9 @@ class HierarchyPage(QtWidgets.QWidget):
|
||||||
task_ids = entity_ids["task_ids"]
|
task_ids = entity_ids["task_ids"]
|
||||||
self._folders_widget.set_folder_ids_filter(folder_ids)
|
self._folders_widget.set_folder_ids_filter(folder_ids)
|
||||||
self._tasks_widget.set_task_ids_filter(task_ids)
|
self._tasks_widget.set_task_ids_filter(task_ids)
|
||||||
|
|
||||||
|
def _on_folders_focus(self):
|
||||||
|
self._workfiles_page.deselect()
|
||||||
|
|
||||||
|
def _on_tasks_focus(self):
|
||||||
|
self._workfiles_page.deselect()
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from typing import Optional
|
||||||
import ayon_api
|
import ayon_api
|
||||||
from qtpy import QtCore, QtWidgets, QtGui
|
from qtpy import QtCore, QtWidgets, QtGui
|
||||||
|
|
||||||
from ayon_core.tools.utils import get_qt_icon
|
from ayon_core.tools.utils import get_qt_icon, DeselectableTreeView
|
||||||
from ayon_core.tools.launcher.abstract import AbstractLauncherFrontEnd
|
from ayon_core.tools.launcher.abstract import AbstractLauncherFrontEnd
|
||||||
|
|
||||||
VERSION_ROLE = QtCore.Qt.UserRole + 1
|
VERSION_ROLE = QtCore.Qt.UserRole + 1
|
||||||
|
|
@ -127,7 +127,7 @@ class WorkfilesModel(QtGui.QStandardItemModel):
|
||||||
return icon
|
return icon
|
||||||
|
|
||||||
|
|
||||||
class WorkfilesView(QtWidgets.QTreeView):
|
class WorkfilesView(DeselectableTreeView):
|
||||||
def drawBranches(self, painter, rect, index):
|
def drawBranches(self, painter, rect, index):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -165,6 +165,10 @@ class WorkfilesPage(QtWidgets.QWidget):
|
||||||
def refresh(self) -> None:
|
def refresh(self) -> None:
|
||||||
self._workfiles_model.refresh()
|
self._workfiles_model.refresh()
|
||||||
|
|
||||||
|
def deselect(self):
|
||||||
|
sel_model = self._workfiles_view.selectionModel()
|
||||||
|
sel_model.clearSelection()
|
||||||
|
|
||||||
def _on_refresh(self) -> None:
|
def _on_refresh(self) -> None:
|
||||||
self._workfiles_proxy.sort(0, QtCore.Qt.DescendingOrder)
|
self._workfiles_proxy.sort(0, QtCore.Qt.DescendingOrder)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -653,6 +653,21 @@ class FrontendLoaderController(_BaseLoaderController):
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_my_tasks_entity_ids(
|
||||||
|
self, project_name: str
|
||||||
|
) -> dict[str, list[str]]:
|
||||||
|
"""Get entity ids for my tasks.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_name (str): Project name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, list[str]]: Folder and task ids.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_available_tags_by_entity_type(
|
def get_available_tags_by_entity_type(
|
||||||
self, project_name: str
|
self, project_name: str
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,11 @@ import ayon_api
|
||||||
|
|
||||||
from ayon_core.settings import get_project_settings
|
from ayon_core.settings import get_project_settings
|
||||||
from ayon_core.pipeline import get_current_host_name
|
from ayon_core.pipeline import get_current_host_name
|
||||||
from ayon_core.lib import NestedCacheItem, CacheItem, filter_profiles
|
from ayon_core.lib import (
|
||||||
|
NestedCacheItem,
|
||||||
|
CacheItem,
|
||||||
|
filter_profiles,
|
||||||
|
)
|
||||||
from ayon_core.lib.events import QueuedEventSystem
|
from ayon_core.lib.events import QueuedEventSystem
|
||||||
from ayon_core.pipeline import Anatomy, get_current_context
|
from ayon_core.pipeline import Anatomy, get_current_context
|
||||||
from ayon_core.host import ILoadHost
|
from ayon_core.host import ILoadHost
|
||||||
|
|
@ -18,6 +22,7 @@ from ayon_core.tools.common_models import (
|
||||||
ThumbnailsModel,
|
ThumbnailsModel,
|
||||||
TagItem,
|
TagItem,
|
||||||
ProductTypeIconMapping,
|
ProductTypeIconMapping,
|
||||||
|
UsersModel,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .abstract import (
|
from .abstract import (
|
||||||
|
|
@ -33,6 +38,8 @@ from .models import (
|
||||||
SiteSyncModel
|
SiteSyncModel
|
||||||
)
|
)
|
||||||
|
|
||||||
|
NOT_SET = object()
|
||||||
|
|
||||||
|
|
||||||
class ExpectedSelection:
|
class ExpectedSelection:
|
||||||
def __init__(self, controller):
|
def __init__(self, controller):
|
||||||
|
|
@ -125,6 +132,7 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
|
||||||
self._loader_actions_model = LoaderActionsModel(self)
|
self._loader_actions_model = LoaderActionsModel(self)
|
||||||
self._thumbnails_model = ThumbnailsModel()
|
self._thumbnails_model = ThumbnailsModel()
|
||||||
self._sitesync_model = SiteSyncModel(self)
|
self._sitesync_model = SiteSyncModel(self)
|
||||||
|
self._users_model = UsersModel(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def log(self):
|
def log(self):
|
||||||
|
|
@ -161,6 +169,7 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
|
||||||
self._projects_model.reset()
|
self._projects_model.reset()
|
||||||
self._thumbnails_model.reset()
|
self._thumbnails_model.reset()
|
||||||
self._sitesync_model.reset()
|
self._sitesync_model.reset()
|
||||||
|
self._users_model.reset()
|
||||||
|
|
||||||
self._projects_model.refresh()
|
self._projects_model.refresh()
|
||||||
|
|
||||||
|
|
@ -236,6 +245,17 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
|
||||||
output[folder_id] = label
|
output[folder_id] = label
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
def get_my_tasks_entity_ids(
|
||||||
|
self, project_name: str
|
||||||
|
) -> dict[str, list[str]]:
|
||||||
|
username = self._users_model.get_current_username()
|
||||||
|
assignees = []
|
||||||
|
if username:
|
||||||
|
assignees.append(username)
|
||||||
|
return self._hierarchy_model.get_entity_ids_for_assignees(
|
||||||
|
project_name, assignees
|
||||||
|
)
|
||||||
|
|
||||||
def get_available_tags_by_entity_type(
|
def get_available_tags_by_entity_type(
|
||||||
self, project_name: str
|
self, project_name: str
|
||||||
) -> dict[str, list[str]]:
|
) -> dict[str, list[str]]:
|
||||||
|
|
@ -479,20 +499,6 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
|
||||||
def is_standard_projects_filter_enabled(self):
|
def is_standard_projects_filter_enabled(self):
|
||||||
return self._host is not None
|
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")
|
|
||||||
|
|
||||||
def get_product_types_filter(self):
|
def get_product_types_filter(self):
|
||||||
output = ProductTypesFilter(
|
output = ProductTypesFilter(
|
||||||
is_allow_list=False,
|
is_allow_list=False,
|
||||||
|
|
@ -548,3 +554,17 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
|
||||||
product_types=profile["filter_product_types"]
|
product_types=profile["filter_product_types"]
|
||||||
)
|
)
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
def _create_event_system(self):
|
||||||
|
return QueuedEventSystem()
|
||||||
|
|
||||||
|
def _emit_event(self, topic, data=None):
|
||||||
|
self._event_system.emit(topic, data or {}, "controller")
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import qtpy
|
import qtpy
|
||||||
from qtpy import QtWidgets, QtCore, QtGui
|
from qtpy import QtWidgets, QtCore, QtGui
|
||||||
|
|
||||||
from ayon_core.tools.utils import (
|
|
||||||
RecursiveSortFilterProxyModel,
|
|
||||||
DeselectableTreeView,
|
|
||||||
)
|
|
||||||
from ayon_core.style import get_objected_colors
|
from ayon_core.style import get_objected_colors
|
||||||
|
from ayon_core.tools.utils import DeselectableTreeView
|
||||||
|
from ayon_core.tools.utils.folders_widget import FoldersProxyModel
|
||||||
|
|
||||||
from ayon_core.tools.utils import (
|
from ayon_core.tools.utils import (
|
||||||
FoldersQtModel,
|
FoldersQtModel,
|
||||||
|
|
@ -260,7 +260,7 @@ class LoaderFoldersWidget(QtWidgets.QWidget):
|
||||||
QtWidgets.QAbstractItemView.ExtendedSelection)
|
QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||||
|
|
||||||
folders_model = LoaderFoldersModel(controller)
|
folders_model = LoaderFoldersModel(controller)
|
||||||
folders_proxy_model = RecursiveSortFilterProxyModel()
|
folders_proxy_model = FoldersProxyModel()
|
||||||
folders_proxy_model.setSourceModel(folders_model)
|
folders_proxy_model.setSourceModel(folders_model)
|
||||||
folders_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
folders_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||||
|
|
||||||
|
|
@ -314,6 +314,15 @@ class LoaderFoldersWidget(QtWidgets.QWidget):
|
||||||
if name:
|
if name:
|
||||||
self._folders_view.expandAll()
|
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_merged_products_selection(self, items):
|
def set_merged_products_selection(self, items):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import collections
|
import collections
|
||||||
import hashlib
|
import hashlib
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from qtpy import QtWidgets, QtCore, QtGui
|
from qtpy import QtWidgets, QtCore, QtGui
|
||||||
|
|
||||||
from ayon_core.style import get_default_entity_icon_color
|
from ayon_core.style import get_default_entity_icon_color
|
||||||
from ayon_core.tools.utils import (
|
from ayon_core.tools.utils import (
|
||||||
RecursiveSortFilterProxyModel,
|
|
||||||
DeselectableTreeView,
|
DeselectableTreeView,
|
||||||
TasksQtModel,
|
TasksQtModel,
|
||||||
TASKS_MODEL_SENDER_NAME,
|
TASKS_MODEL_SENDER_NAME,
|
||||||
|
|
@ -15,9 +15,11 @@ from ayon_core.tools.utils.tasks_widget import (
|
||||||
ITEM_NAME_ROLE,
|
ITEM_NAME_ROLE,
|
||||||
PARENT_ID_ROLE,
|
PARENT_ID_ROLE,
|
||||||
TASK_TYPE_ROLE,
|
TASK_TYPE_ROLE,
|
||||||
|
TasksProxyModel,
|
||||||
)
|
)
|
||||||
from ayon_core.tools.utils.lib import RefreshThread, get_qt_icon
|
from ayon_core.tools.utils.lib import RefreshThread, get_qt_icon
|
||||||
|
|
||||||
|
|
||||||
# Role that can't clash with default 'tasks_widget' roles
|
# Role that can't clash with default 'tasks_widget' roles
|
||||||
FOLDER_LABEL_ROLE = QtCore.Qt.UserRole + 100
|
FOLDER_LABEL_ROLE = QtCore.Qt.UserRole + 100
|
||||||
NO_TASKS_ID = "--no-task--"
|
NO_TASKS_ID = "--no-task--"
|
||||||
|
|
@ -295,7 +297,7 @@ class LoaderTasksQtModel(TasksQtModel):
|
||||||
return super().data(index, role)
|
return super().data(index, role)
|
||||||
|
|
||||||
|
|
||||||
class LoaderTasksProxyModel(RecursiveSortFilterProxyModel):
|
class LoaderTasksProxyModel(TasksProxyModel):
|
||||||
def lessThan(self, left, right):
|
def lessThan(self, left, right):
|
||||||
if left.data(ITEM_ID_ROLE) == NO_TASKS_ID:
|
if left.data(ITEM_ID_ROLE) == NO_TASKS_ID:
|
||||||
return False
|
return False
|
||||||
|
|
@ -303,6 +305,12 @@ class LoaderTasksProxyModel(RecursiveSortFilterProxyModel):
|
||||||
return True
|
return True
|
||||||
return super().lessThan(left, right)
|
return super().lessThan(left, right)
|
||||||
|
|
||||||
|
def filterAcceptsRow(self, row, parent_index):
|
||||||
|
source_index = self.sourceModel().index(row, 0, parent_index)
|
||||||
|
if source_index.data(ITEM_ID_ROLE) == NO_TASKS_ID:
|
||||||
|
return True
|
||||||
|
return super().filterAcceptsRow(row, parent_index)
|
||||||
|
|
||||||
|
|
||||||
class LoaderTasksWidget(QtWidgets.QWidget):
|
class LoaderTasksWidget(QtWidgets.QWidget):
|
||||||
refreshed = QtCore.Signal()
|
refreshed = QtCore.Signal()
|
||||||
|
|
@ -363,6 +371,15 @@ class LoaderTasksWidget(QtWidgets.QWidget):
|
||||||
if name:
|
if name:
|
||||||
self._tasks_view.expandAll()
|
self._tasks_view.expandAll()
|
||||||
|
|
||||||
|
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 refresh(self):
|
def refresh(self):
|
||||||
self._tasks_model.refresh()
|
self._tasks_model.refresh()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ from ayon_core.resources import get_ayon_icon_filepath
|
||||||
from ayon_core.style import load_stylesheet
|
from ayon_core.style import load_stylesheet
|
||||||
from ayon_core.pipeline.actions import LoaderActionResult
|
from ayon_core.pipeline.actions import LoaderActionResult
|
||||||
from ayon_core.tools.utils import (
|
from ayon_core.tools.utils import (
|
||||||
PlaceholderLineEdit,
|
|
||||||
MessageOverlayObject,
|
MessageOverlayObject,
|
||||||
ErrorMessageBox,
|
ErrorMessageBox,
|
||||||
ThumbnailPainterWidget,
|
ThumbnailPainterWidget,
|
||||||
|
|
@ -16,6 +15,7 @@ from ayon_core.tools.utils import (
|
||||||
GoToCurrentButton,
|
GoToCurrentButton,
|
||||||
ProjectsCombobox,
|
ProjectsCombobox,
|
||||||
get_qt_icon,
|
get_qt_icon,
|
||||||
|
FoldersFiltersWidget,
|
||||||
)
|
)
|
||||||
from ayon_core.tools.attribute_defs import AttributeDefinitionsDialog
|
from ayon_core.tools.attribute_defs import AttributeDefinitionsDialog
|
||||||
from ayon_core.tools.utils.lib import center_window
|
from ayon_core.tools.utils.lib import center_window
|
||||||
|
|
@ -178,15 +178,14 @@ class LoaderWindow(QtWidgets.QWidget):
|
||||||
context_top_layout.addWidget(go_to_current_btn, 0)
|
context_top_layout.addWidget(go_to_current_btn, 0)
|
||||||
context_top_layout.addWidget(refresh_btn, 0)
|
context_top_layout.addWidget(refresh_btn, 0)
|
||||||
|
|
||||||
folders_filter_input = PlaceholderLineEdit(context_widget)
|
filters_widget = FoldersFiltersWidget(context_widget)
|
||||||
folders_filter_input.setPlaceholderText("Folder name filter...")
|
|
||||||
|
|
||||||
folders_widget = LoaderFoldersWidget(controller, context_widget)
|
folders_widget = LoaderFoldersWidget(controller, context_widget)
|
||||||
|
|
||||||
context_layout = QtWidgets.QVBoxLayout(context_widget)
|
context_layout = QtWidgets.QVBoxLayout(context_widget)
|
||||||
context_layout.setContentsMargins(0, 0, 0, 0)
|
context_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
context_layout.addWidget(context_top_widget, 0)
|
context_layout.addWidget(context_top_widget, 0)
|
||||||
context_layout.addWidget(folders_filter_input, 0)
|
context_layout.addWidget(filters_widget, 0)
|
||||||
context_layout.addWidget(folders_widget, 1)
|
context_layout.addWidget(folders_widget, 1)
|
||||||
|
|
||||||
tasks_widget = LoaderTasksWidget(controller, context_widget)
|
tasks_widget = LoaderTasksWidget(controller, context_widget)
|
||||||
|
|
@ -255,9 +254,12 @@ class LoaderWindow(QtWidgets.QWidget):
|
||||||
projects_combobox.refreshed.connect(self._on_projects_refresh)
|
projects_combobox.refreshed.connect(self._on_projects_refresh)
|
||||||
folders_widget.refreshed.connect(self._on_folders_refresh)
|
folders_widget.refreshed.connect(self._on_folders_refresh)
|
||||||
products_widget.refreshed.connect(self._on_products_refresh)
|
products_widget.refreshed.connect(self._on_products_refresh)
|
||||||
folders_filter_input.textChanged.connect(
|
filters_widget.text_changed.connect(
|
||||||
self._on_folder_filter_change
|
self._on_folder_filter_change
|
||||||
)
|
)
|
||||||
|
filters_widget.my_tasks_changed.connect(
|
||||||
|
self._on_my_tasks_checkbox_state_changed
|
||||||
|
)
|
||||||
search_bar.filter_changed.connect(self._on_filter_change)
|
search_bar.filter_changed.connect(self._on_filter_change)
|
||||||
product_group_checkbox.stateChanged.connect(
|
product_group_checkbox.stateChanged.connect(
|
||||||
self._on_product_group_change
|
self._on_product_group_change
|
||||||
|
|
@ -317,7 +319,7 @@ class LoaderWindow(QtWidgets.QWidget):
|
||||||
self._refresh_btn = refresh_btn
|
self._refresh_btn = refresh_btn
|
||||||
self._projects_combobox = projects_combobox
|
self._projects_combobox = projects_combobox
|
||||||
|
|
||||||
self._folders_filter_input = folders_filter_input
|
self._filters_widget = filters_widget
|
||||||
self._folders_widget = folders_widget
|
self._folders_widget = folders_widget
|
||||||
|
|
||||||
self._tasks_widget = tasks_widget
|
self._tasks_widget = tasks_widget
|
||||||
|
|
@ -449,9 +451,21 @@ class LoaderWindow(QtWidgets.QWidget):
|
||||||
self._group_dialog.set_product_ids(project_name, product_ids)
|
self._group_dialog.set_product_ids(project_name, product_ids)
|
||||||
self._group_dialog.show()
|
self._group_dialog.show()
|
||||||
|
|
||||||
def _on_folder_filter_change(self, text):
|
def _on_folder_filter_change(self, text: str) -> None:
|
||||||
self._folders_widget.set_name_filter(text)
|
self._folders_widget.set_name_filter(text)
|
||||||
|
|
||||||
|
def _on_my_tasks_checkbox_state_changed(self, enabled: bool) -> None:
|
||||||
|
folder_ids = None
|
||||||
|
task_ids = None
|
||||||
|
if enabled:
|
||||||
|
entity_ids = self._controller.get_my_tasks_entity_ids(
|
||||||
|
self._selected_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)
|
||||||
|
|
||||||
def _on_product_group_change(self):
|
def _on_product_group_change(self):
|
||||||
self._products_widget.set_enable_grouping(
|
self._products_widget.set_enable_grouping(
|
||||||
self._product_group_checkbox.isChecked()
|
self._product_group_checkbox.isChecked()
|
||||||
|
|
|
||||||
|
|
@ -295,6 +295,21 @@ class AbstractPublisherFrontend(AbstractPublisherCommon):
|
||||||
"""Get folder id from folder path."""
|
"""Get folder id from folder path."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_my_tasks_entity_ids(
|
||||||
|
self, project_name: str
|
||||||
|
) -> dict[str, list[str]]:
|
||||||
|
"""Get entity ids for my tasks.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_name (str): Project name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, list[str]]: Folder and task ids.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
# --- Create ---
|
# --- Create ---
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_creator_items(self) -> Dict[str, "CreatorItem"]:
|
def get_creator_items(self) -> Dict[str, "CreatorItem"]:
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,11 @@ from ayon_core.pipeline import (
|
||||||
registered_host,
|
registered_host,
|
||||||
get_process_id,
|
get_process_id,
|
||||||
)
|
)
|
||||||
from ayon_core.tools.common_models import ProjectsModel, HierarchyModel
|
from ayon_core.tools.common_models import (
|
||||||
|
ProjectsModel,
|
||||||
|
HierarchyModel,
|
||||||
|
UsersModel,
|
||||||
|
)
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
PublishModel,
|
PublishModel,
|
||||||
|
|
@ -101,6 +105,7 @@ class PublisherController(
|
||||||
# Cacher of avalon documents
|
# Cacher of avalon documents
|
||||||
self._projects_model = ProjectsModel(self)
|
self._projects_model = ProjectsModel(self)
|
||||||
self._hierarchy_model = HierarchyModel(self)
|
self._hierarchy_model = HierarchyModel(self)
|
||||||
|
self._users_model = UsersModel(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def log(self):
|
def log(self):
|
||||||
|
|
@ -317,6 +322,17 @@ class PublisherController(
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def get_my_tasks_entity_ids(
|
||||||
|
self, project_name: str
|
||||||
|
) -> dict[str, list[str]]:
|
||||||
|
username = self._users_model.get_current_username()
|
||||||
|
assignees = []
|
||||||
|
if username:
|
||||||
|
assignees.append(username)
|
||||||
|
return self._hierarchy_model.get_entity_ids_for_assignees(
|
||||||
|
project_name, assignees
|
||||||
|
)
|
||||||
|
|
||||||
# --- Publish specific callbacks ---
|
# --- Publish specific callbacks ---
|
||||||
def get_context_title(self):
|
def get_context_title(self):
|
||||||
"""Get context title for artist shown at the top of main window."""
|
"""Get context title for artist shown at the top of main window."""
|
||||||
|
|
@ -359,6 +375,7 @@ class PublisherController(
|
||||||
self._emit_event("controller.reset.started")
|
self._emit_event("controller.reset.started")
|
||||||
|
|
||||||
self._hierarchy_model.reset()
|
self._hierarchy_model.reset()
|
||||||
|
self._users_model.reset()
|
||||||
|
|
||||||
# Publish part must be reset after plugins
|
# Publish part must be reset after plugins
|
||||||
self._create_model.reset()
|
self._create_model.reset()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import copy
|
||||||
from typing import (
|
from typing import (
|
||||||
Union,
|
Union,
|
||||||
List,
|
List,
|
||||||
|
|
@ -1098,7 +1099,7 @@ class CreateModel:
|
||||||
creator_attributes[key] = attr_def.default
|
creator_attributes[key] = attr_def.default
|
||||||
|
|
||||||
elif attr_def.is_value_valid(value):
|
elif attr_def.is_value_valid(value):
|
||||||
creator_attributes[key] = value
|
creator_attributes[key] = copy.deepcopy(value)
|
||||||
|
|
||||||
def _set_instances_publish_attr_values(
|
def _set_instances_publish_attr_values(
|
||||||
self, instance_ids, plugin_name, key, value
|
self, instance_ids, plugin_name, key, value
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@ class ContextCardWidget(CardWidget):
|
||||||
Is not visually under group widget and is always at the top of card view.
|
Is not visually under group widget and is always at the top of card view.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent: QtWidgets.QWidget):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self._id = CONTEXT_ID
|
self._id = CONTEXT_ID
|
||||||
|
|
@ -211,7 +211,7 @@ class ContextCardWidget(CardWidget):
|
||||||
icon_widget = PublishPixmapLabel(None, self)
|
icon_widget = PublishPixmapLabel(None, self)
|
||||||
icon_widget.setObjectName("ProductTypeIconLabel")
|
icon_widget.setObjectName("ProductTypeIconLabel")
|
||||||
|
|
||||||
label_widget = QtWidgets.QLabel(CONTEXT_LABEL, self)
|
label_widget = QtWidgets.QLabel(f"<span>{CONTEXT_LABEL}</span>", self)
|
||||||
|
|
||||||
icon_layout = QtWidgets.QHBoxLayout()
|
icon_layout = QtWidgets.QHBoxLayout()
|
||||||
icon_layout.setContentsMargins(5, 5, 5, 5)
|
icon_layout.setContentsMargins(5, 5, 5, 5)
|
||||||
|
|
@ -288,6 +288,8 @@ class InstanceCardWidget(CardWidget):
|
||||||
self._last_product_name = None
|
self._last_product_name = None
|
||||||
self._last_variant = None
|
self._last_variant = None
|
||||||
self._last_label = None
|
self._last_label = None
|
||||||
|
self._last_folder_path = None
|
||||||
|
self._last_task_name = None
|
||||||
|
|
||||||
icon_widget = IconValuePixmapLabel(group_icon, self)
|
icon_widget = IconValuePixmapLabel(group_icon, self)
|
||||||
icon_widget.setObjectName("ProductTypeIconLabel")
|
icon_widget.setObjectName("ProductTypeIconLabel")
|
||||||
|
|
@ -383,29 +385,54 @@ class InstanceCardWidget(CardWidget):
|
||||||
self._icon_widget.setVisible(valid)
|
self._icon_widget.setVisible(valid)
|
||||||
self._context_warning.setVisible(not valid)
|
self._context_warning.setVisible(not valid)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_card_widget_sub_label(
|
||||||
|
folder_path: Optional[str],
|
||||||
|
task_name: Optional[str],
|
||||||
|
) -> str:
|
||||||
|
sublabel = ""
|
||||||
|
if folder_path:
|
||||||
|
folder_name = folder_path.rsplit("/", 1)[-1]
|
||||||
|
sublabel = f"<b>{folder_name}</b>"
|
||||||
|
if task_name:
|
||||||
|
sublabel += f" - <i>{task_name}</i>"
|
||||||
|
return sublabel
|
||||||
|
|
||||||
def _update_product_name(self):
|
def _update_product_name(self):
|
||||||
variant = self.instance.variant
|
variant = self.instance.variant
|
||||||
product_name = self.instance.product_name
|
product_name = self.instance.product_name
|
||||||
label = self.instance.label
|
label = self.instance.label
|
||||||
|
folder_path = self.instance.folder_path
|
||||||
|
task_name = self.instance.task_name
|
||||||
|
|
||||||
if (
|
if (
|
||||||
variant == self._last_variant
|
variant == self._last_variant
|
||||||
and product_name == self._last_product_name
|
and product_name == self._last_product_name
|
||||||
and label == self._last_label
|
and label == self._last_label
|
||||||
|
and folder_path == self._last_folder_path
|
||||||
|
and task_name == self._last_task_name
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
self._last_variant = variant
|
self._last_variant = variant
|
||||||
self._last_product_name = product_name
|
self._last_product_name = product_name
|
||||||
self._last_label = label
|
self._last_label = label
|
||||||
|
self._last_folder_path = folder_path
|
||||||
|
self._last_task_name = task_name
|
||||||
|
|
||||||
# Make `variant` bold
|
# Make `variant` bold
|
||||||
label = html_escape(self.instance.label)
|
label = html_escape(self.instance.label)
|
||||||
found_parts = set(re.findall(variant, label, re.IGNORECASE))
|
found_parts = set(re.findall(variant, label, re.IGNORECASE))
|
||||||
if found_parts:
|
if found_parts:
|
||||||
for part in found_parts:
|
for part in found_parts:
|
||||||
replacement = "<b>{}</b>".format(part)
|
replacement = f"<b>{part}</b>"
|
||||||
label = label.replace(part, replacement)
|
label = label.replace(part, replacement)
|
||||||
|
|
||||||
|
label = f"<span>{label}</span>"
|
||||||
|
sublabel = self._get_card_widget_sub_label(folder_path, task_name)
|
||||||
|
if sublabel:
|
||||||
|
label += f"<br/><span style=\"font-size: 8pt;\">{sublabel}</span>"
|
||||||
|
|
||||||
self._label_widget.setText(label)
|
self._label_widget.setText(label)
|
||||||
# HTML text will cause that label start catch mouse clicks
|
# HTML text will cause that label start catch mouse clicks
|
||||||
# - disabling with changing interaction flag
|
# - disabling with changing interaction flag
|
||||||
|
|
@ -702,11 +729,9 @@ class InstanceCardView(AbstractInstanceView):
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
"""Refresh instances in view based on CreatedContext."""
|
"""Refresh instances in view based on CreatedContext."""
|
||||||
|
|
||||||
self._make_sure_context_widget_exists()
|
self._make_sure_context_widget_exists()
|
||||||
|
|
||||||
self._update_convertors_group()
|
self._update_convertors_group()
|
||||||
|
|
||||||
context_info_by_id = self._controller.get_instances_context_info()
|
context_info_by_id = self._controller.get_instances_context_info()
|
||||||
|
|
||||||
# Prepare instances by group and identifiers by group
|
# Prepare instances by group and identifiers by group
|
||||||
|
|
@ -814,6 +839,8 @@ class InstanceCardView(AbstractInstanceView):
|
||||||
widget.setVisible(False)
|
widget.setVisible(False)
|
||||||
widget.deleteLater()
|
widget.deleteLater()
|
||||||
|
|
||||||
|
sorted_group_names.insert(0, CONTEXT_GROUP)
|
||||||
|
|
||||||
self._parent_id_by_id = parent_id_by_id
|
self._parent_id_by_id = parent_id_by_id
|
||||||
self._instance_ids_by_parent_id = instance_ids_by_parent_id
|
self._instance_ids_by_parent_id = instance_ids_by_parent_id
|
||||||
self._group_name_by_instance_id = group_by_instance_id
|
self._group_name_by_instance_id = group_by_instance_id
|
||||||
|
|
@ -881,7 +908,7 @@ class InstanceCardView(AbstractInstanceView):
|
||||||
context_info,
|
context_info,
|
||||||
is_parent_active,
|
is_parent_active,
|
||||||
group_icon,
|
group_icon,
|
||||||
group_widget
|
group_widget,
|
||||||
)
|
)
|
||||||
widget.selected.connect(self._on_widget_selection)
|
widget.selected.connect(self._on_widget_selection)
|
||||||
widget.active_changed.connect(self._on_active_changed)
|
widget.active_changed.connect(self._on_active_changed)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
from qtpy import QtWidgets, QtCore
|
from qtpy import QtWidgets, QtCore
|
||||||
|
|
||||||
from ayon_core.lib.events import QueuedEventSystem
|
from ayon_core.lib.events import QueuedEventSystem
|
||||||
from ayon_core.tools.utils import PlaceholderLineEdit, GoToCurrentButton
|
|
||||||
|
|
||||||
from ayon_core.tools.common_models import HierarchyExpectedSelection
|
from ayon_core.tools.common_models import HierarchyExpectedSelection
|
||||||
from ayon_core.tools.utils import FoldersWidget, TasksWidget
|
from ayon_core.tools.utils import (
|
||||||
|
FoldersWidget,
|
||||||
|
TasksWidget,
|
||||||
|
FoldersFiltersWidget,
|
||||||
|
GoToCurrentButton,
|
||||||
|
)
|
||||||
from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend
|
from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -180,8 +184,7 @@ class CreateContextWidget(QtWidgets.QWidget):
|
||||||
|
|
||||||
headers_widget = QtWidgets.QWidget(self)
|
headers_widget = QtWidgets.QWidget(self)
|
||||||
|
|
||||||
folder_filter_input = PlaceholderLineEdit(headers_widget)
|
filters_widget = FoldersFiltersWidget(headers_widget)
|
||||||
folder_filter_input.setPlaceholderText("Filter folders..")
|
|
||||||
|
|
||||||
current_context_btn = GoToCurrentButton(headers_widget)
|
current_context_btn = GoToCurrentButton(headers_widget)
|
||||||
current_context_btn.setToolTip("Go to current context")
|
current_context_btn.setToolTip("Go to current context")
|
||||||
|
|
@ -189,7 +192,8 @@ class CreateContextWidget(QtWidgets.QWidget):
|
||||||
|
|
||||||
headers_layout = QtWidgets.QHBoxLayout(headers_widget)
|
headers_layout = QtWidgets.QHBoxLayout(headers_widget)
|
||||||
headers_layout.setContentsMargins(0, 0, 0, 0)
|
headers_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
headers_layout.addWidget(folder_filter_input, 1)
|
headers_layout.setSpacing(5)
|
||||||
|
headers_layout.addWidget(filters_widget, 1)
|
||||||
headers_layout.addWidget(current_context_btn, 0)
|
headers_layout.addWidget(current_context_btn, 0)
|
||||||
|
|
||||||
hierarchy_controller = CreateHierarchyController(controller)
|
hierarchy_controller = CreateHierarchyController(controller)
|
||||||
|
|
@ -207,15 +211,16 @@ class CreateContextWidget(QtWidgets.QWidget):
|
||||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
main_layout.setSpacing(0)
|
main_layout.setSpacing(0)
|
||||||
main_layout.addWidget(headers_widget, 0)
|
main_layout.addWidget(headers_widget, 0)
|
||||||
|
main_layout.addSpacing(5)
|
||||||
main_layout.addWidget(folders_widget, 2)
|
main_layout.addWidget(folders_widget, 2)
|
||||||
main_layout.addWidget(tasks_widget, 1)
|
main_layout.addWidget(tasks_widget, 1)
|
||||||
|
|
||||||
folders_widget.selection_changed.connect(self._on_folder_change)
|
folders_widget.selection_changed.connect(self._on_folder_change)
|
||||||
tasks_widget.selection_changed.connect(self._on_task_change)
|
tasks_widget.selection_changed.connect(self._on_task_change)
|
||||||
current_context_btn.clicked.connect(self._on_current_context_click)
|
current_context_btn.clicked.connect(self._on_current_context_click)
|
||||||
folder_filter_input.textChanged.connect(self._on_folder_filter_change)
|
filters_widget.text_changed.connect(self._on_folder_filter_change)
|
||||||
|
filters_widget.my_tasks_changed.connect(self._on_my_tasks_change)
|
||||||
|
|
||||||
self._folder_filter_input = folder_filter_input
|
|
||||||
self._current_context_btn = current_context_btn
|
self._current_context_btn = current_context_btn
|
||||||
self._folders_widget = folders_widget
|
self._folders_widget = folders_widget
|
||||||
self._tasks_widget = tasks_widget
|
self._tasks_widget = tasks_widget
|
||||||
|
|
@ -303,5 +308,17 @@ class CreateContextWidget(QtWidgets.QWidget):
|
||||||
self._last_project_name, folder_id, task_name
|
self._last_project_name, folder_id, task_name
|
||||||
)
|
)
|
||||||
|
|
||||||
def _on_folder_filter_change(self, text):
|
def _on_folder_filter_change(self, text: str) -> None:
|
||||||
self._folders_widget.set_name_filter(text)
|
self._folders_widget.set_name_filter(text)
|
||||||
|
|
||||||
|
def _on_my_tasks_change(self, enabled: bool) -> None:
|
||||||
|
folder_ids = None
|
||||||
|
task_ids = None
|
||||||
|
if enabled:
|
||||||
|
entity_ids = self._controller.get_my_tasks_entity_ids(
|
||||||
|
self._last_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)
|
||||||
|
|
|
||||||
|
|
@ -710,11 +710,13 @@ class CreateWidget(QtWidgets.QWidget):
|
||||||
|
|
||||||
def _on_first_show(self):
|
def _on_first_show(self):
|
||||||
width = self.width()
|
width = self.width()
|
||||||
part = int(width / 4)
|
part = int(width / 9)
|
||||||
rem_width = width - part
|
context_width = part * 3
|
||||||
self._main_splitter_widget.setSizes([part, rem_width])
|
create_sel_width = part * 2
|
||||||
rem_width = rem_width - part
|
rem_width = width - context_width
|
||||||
self._creators_splitter.setSizes([part, rem_width])
|
self._main_splitter_widget.setSizes([context_width, rem_width])
|
||||||
|
rem_width -= create_sel_width
|
||||||
|
self._creators_splitter.setSizes([create_sel_width, rem_width])
|
||||||
|
|
||||||
def showEvent(self, event):
|
def showEvent(self, event):
|
||||||
super().showEvent(event)
|
super().showEvent(event)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
from qtpy import QtWidgets
|
from qtpy import QtWidgets
|
||||||
|
|
||||||
from ayon_core.lib.events import QueuedEventSystem
|
from ayon_core.lib.events import QueuedEventSystem
|
||||||
from ayon_core.tools.utils import PlaceholderLineEdit, FoldersWidget
|
from ayon_core.tools.utils import (
|
||||||
|
FoldersWidget,
|
||||||
|
FoldersFiltersWidget,
|
||||||
|
)
|
||||||
from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend
|
from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -43,8 +46,7 @@ class FoldersDialog(QtWidgets.QDialog):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle("Select folder")
|
self.setWindowTitle("Select folder")
|
||||||
|
|
||||||
filter_input = PlaceholderLineEdit(self)
|
filters_widget = FoldersFiltersWidget(self)
|
||||||
filter_input.setPlaceholderText("Filter folders..")
|
|
||||||
|
|
||||||
folders_controller = FoldersDialogController(controller)
|
folders_controller = FoldersDialogController(controller)
|
||||||
folders_widget = FoldersWidget(folders_controller, self)
|
folders_widget = FoldersWidget(folders_controller, self)
|
||||||
|
|
@ -59,7 +61,8 @@ class FoldersDialog(QtWidgets.QDialog):
|
||||||
btns_layout.addWidget(cancel_btn)
|
btns_layout.addWidget(cancel_btn)
|
||||||
|
|
||||||
layout = QtWidgets.QVBoxLayout(self)
|
layout = QtWidgets.QVBoxLayout(self)
|
||||||
layout.addWidget(filter_input, 0)
|
layout.setSpacing(5)
|
||||||
|
layout.addWidget(filters_widget, 0)
|
||||||
layout.addWidget(folders_widget, 1)
|
layout.addWidget(folders_widget, 1)
|
||||||
layout.addLayout(btns_layout, 0)
|
layout.addLayout(btns_layout, 0)
|
||||||
|
|
||||||
|
|
@ -68,12 +71,13 @@ class FoldersDialog(QtWidgets.QDialog):
|
||||||
)
|
)
|
||||||
|
|
||||||
folders_widget.double_clicked.connect(self._on_ok_clicked)
|
folders_widget.double_clicked.connect(self._on_ok_clicked)
|
||||||
filter_input.textChanged.connect(self._on_filter_change)
|
filters_widget.text_changed.connect(self._on_filter_change)
|
||||||
|
filters_widget.my_tasks_changed.connect(self._on_my_tasks_change)
|
||||||
ok_btn.clicked.connect(self._on_ok_clicked)
|
ok_btn.clicked.connect(self._on_ok_clicked)
|
||||||
cancel_btn.clicked.connect(self._on_cancel_clicked)
|
cancel_btn.clicked.connect(self._on_cancel_clicked)
|
||||||
|
|
||||||
self._controller = controller
|
self._controller = controller
|
||||||
self._filter_input = filter_input
|
self._filters_widget = filters_widget
|
||||||
self._ok_btn = ok_btn
|
self._ok_btn = ok_btn
|
||||||
self._cancel_btn = cancel_btn
|
self._cancel_btn = cancel_btn
|
||||||
|
|
||||||
|
|
@ -88,6 +92,49 @@ class FoldersDialog(QtWidgets.QDialog):
|
||||||
self._first_show = True
|
self._first_show = True
|
||||||
self._default_height = 500
|
self._default_height = 500
|
||||||
|
|
||||||
|
self._project_name = None
|
||||||
|
|
||||||
|
def showEvent(self, event):
|
||||||
|
"""Refresh folders widget on show."""
|
||||||
|
super().showEvent(event)
|
||||||
|
if self._first_show:
|
||||||
|
self._first_show = False
|
||||||
|
self._on_first_show()
|
||||||
|
# Refresh on show
|
||||||
|
self.reset(False)
|
||||||
|
|
||||||
|
def reset(self, force=True):
|
||||||
|
"""Reset widget."""
|
||||||
|
if not force and not self._soft_reset_enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._project_name = self._controller.get_current_project_name()
|
||||||
|
if self._soft_reset_enabled:
|
||||||
|
self._soft_reset_enabled = False
|
||||||
|
|
||||||
|
self._folders_widget.set_project_name(self._project_name)
|
||||||
|
|
||||||
|
def get_selected_folder_path(self):
|
||||||
|
"""Get selected folder path."""
|
||||||
|
return self._selected_folder_path
|
||||||
|
|
||||||
|
def set_selected_folders(self, folder_paths: list[str]) -> None:
|
||||||
|
"""Change preselected folder before showing the dialog.
|
||||||
|
|
||||||
|
This also resets model and clean filter.
|
||||||
|
"""
|
||||||
|
self.reset(False)
|
||||||
|
self._filters_widget.set_text("")
|
||||||
|
self._filters_widget.set_my_tasks_checked(False)
|
||||||
|
|
||||||
|
folder_id = None
|
||||||
|
for folder_path in folder_paths:
|
||||||
|
folder_id = self._controller.get_folder_id_from_path(folder_path)
|
||||||
|
if folder_id:
|
||||||
|
break
|
||||||
|
if folder_id:
|
||||||
|
self._folders_widget.set_selected_folder(folder_id)
|
||||||
|
|
||||||
def _on_first_show(self):
|
def _on_first_show(self):
|
||||||
center = self.rect().center()
|
center = self.rect().center()
|
||||||
size = self.size()
|
size = self.size()
|
||||||
|
|
@ -103,27 +150,6 @@ class FoldersDialog(QtWidgets.QDialog):
|
||||||
# Change reset enabled so model is reset on show event
|
# Change reset enabled so model is reset on show event
|
||||||
self._soft_reset_enabled = True
|
self._soft_reset_enabled = True
|
||||||
|
|
||||||
def showEvent(self, event):
|
|
||||||
"""Refresh folders widget on show."""
|
|
||||||
super().showEvent(event)
|
|
||||||
if self._first_show:
|
|
||||||
self._first_show = False
|
|
||||||
self._on_first_show()
|
|
||||||
# Refresh on show
|
|
||||||
self.reset(False)
|
|
||||||
|
|
||||||
def reset(self, force=True):
|
|
||||||
"""Reset widget."""
|
|
||||||
if not force and not self._soft_reset_enabled:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self._soft_reset_enabled:
|
|
||||||
self._soft_reset_enabled = False
|
|
||||||
|
|
||||||
self._folders_widget.set_project_name(
|
|
||||||
self._controller.get_current_project_name()
|
|
||||||
)
|
|
||||||
|
|
||||||
def _on_filter_change(self, text):
|
def _on_filter_change(self, text):
|
||||||
"""Trigger change of filter of folders."""
|
"""Trigger change of filter of folders."""
|
||||||
self._folders_widget.set_name_filter(text)
|
self._folders_widget.set_name_filter(text)
|
||||||
|
|
@ -137,22 +163,11 @@ class FoldersDialog(QtWidgets.QDialog):
|
||||||
)
|
)
|
||||||
self.done(1)
|
self.done(1)
|
||||||
|
|
||||||
def set_selected_folders(self, folder_paths):
|
def _on_my_tasks_change(self, enabled: bool) -> None:
|
||||||
"""Change preselected folder before showing the dialog.
|
folder_ids = None
|
||||||
|
if enabled:
|
||||||
This also resets model and clean filter.
|
entity_ids = self._controller.get_my_tasks_entity_ids(
|
||||||
"""
|
self._project_name
|
||||||
self.reset(False)
|
)
|
||||||
self._filter_input.setText("")
|
folder_ids = entity_ids["folder_ids"]
|
||||||
|
self._folders_widget.set_folder_ids_filter(folder_ids)
|
||||||
folder_id = None
|
|
||||||
for folder_path in folder_paths:
|
|
||||||
folder_id = self._controller.get_folder_id_from_path(folder_path)
|
|
||||||
if folder_id:
|
|
||||||
break
|
|
||||||
if folder_id:
|
|
||||||
self._folders_widget.set_selected_folder(folder_id)
|
|
||||||
|
|
||||||
def get_selected_folder_path(self):
|
|
||||||
"""Get selected folder path."""
|
|
||||||
return self._selected_folder_path
|
|
||||||
|
|
|
||||||
|
|
@ -678,13 +678,8 @@ class PublisherWindow(QtWidgets.QDialog):
|
||||||
self._help_dialog.show()
|
self._help_dialog.show()
|
||||||
|
|
||||||
window = self.window()
|
window = self.window()
|
||||||
if hasattr(QtWidgets.QApplication, "desktop"):
|
screen = window.screen()
|
||||||
desktop = QtWidgets.QApplication.desktop()
|
screen_geo = screen.geometry()
|
||||||
screen_idx = desktop.screenNumber(window)
|
|
||||||
screen_geo = desktop.screenGeometry(screen_idx)
|
|
||||||
else:
|
|
||||||
screen = window.screen()
|
|
||||||
screen_geo = screen.geometry()
|
|
||||||
|
|
||||||
window_geo = window.geometry()
|
window_geo = window.geometry()
|
||||||
dialog_x = window_geo.x() + window_geo.width()
|
dialog_x = window_geo.x() + window_geo.width()
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ class PushToContextController:
|
||||||
self._process_item_id = None
|
self._process_item_id = None
|
||||||
|
|
||||||
self._use_original_name = False
|
self._use_original_name = False
|
||||||
|
self._version_up = False
|
||||||
|
|
||||||
self.set_source(project_name, version_ids)
|
self.set_source(project_name, version_ids)
|
||||||
|
|
||||||
|
|
@ -212,7 +213,7 @@ class PushToContextController:
|
||||||
self._user_values.variant,
|
self._user_values.variant,
|
||||||
comment=self._user_values.comment,
|
comment=self._user_values.comment,
|
||||||
new_folder_name=self._user_values.new_folder_name,
|
new_folder_name=self._user_values.new_folder_name,
|
||||||
dst_version=1,
|
version_up=self._version_up,
|
||||||
use_original_name=self._use_original_name,
|
use_original_name=self._use_original_name,
|
||||||
)
|
)
|
||||||
item_ids.append(item_id)
|
item_ids.append(item_id)
|
||||||
|
|
@ -229,6 +230,9 @@ class PushToContextController:
|
||||||
thread.start()
|
thread.start()
|
||||||
return item_ids
|
return item_ids
|
||||||
|
|
||||||
|
def set_version_up(self, state):
|
||||||
|
self._version_up = state
|
||||||
|
|
||||||
def wait_for_process_thread(self):
|
def wait_for_process_thread(self):
|
||||||
if self._process_thread is None:
|
if self._process_thread is None:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ class ProjectPushItem:
|
||||||
variant,
|
variant,
|
||||||
comment,
|
comment,
|
||||||
new_folder_name,
|
new_folder_name,
|
||||||
dst_version,
|
version_up,
|
||||||
item_id=None,
|
item_id=None,
|
||||||
use_original_name=False
|
use_original_name=False
|
||||||
):
|
):
|
||||||
|
|
@ -100,7 +100,7 @@ class ProjectPushItem:
|
||||||
self.dst_project_name = dst_project_name
|
self.dst_project_name = dst_project_name
|
||||||
self.dst_folder_id = dst_folder_id
|
self.dst_folder_id = dst_folder_id
|
||||||
self.dst_task_name = dst_task_name
|
self.dst_task_name = dst_task_name
|
||||||
self.dst_version = dst_version
|
self.version_up = version_up
|
||||||
self.variant = variant
|
self.variant = variant
|
||||||
self.new_folder_name = new_folder_name
|
self.new_folder_name = new_folder_name
|
||||||
self.comment = comment or ""
|
self.comment = comment or ""
|
||||||
|
|
@ -118,7 +118,7 @@ class ProjectPushItem:
|
||||||
str(self.dst_folder_id),
|
str(self.dst_folder_id),
|
||||||
str(self.new_folder_name),
|
str(self.new_folder_name),
|
||||||
str(self.dst_task_name),
|
str(self.dst_task_name),
|
||||||
str(self.dst_version),
|
str(self.version_up),
|
||||||
self.use_original_name
|
self.use_original_name
|
||||||
])
|
])
|
||||||
return self._repr_value
|
return self._repr_value
|
||||||
|
|
@ -133,7 +133,7 @@ class ProjectPushItem:
|
||||||
"dst_project_name": self.dst_project_name,
|
"dst_project_name": self.dst_project_name,
|
||||||
"dst_folder_id": self.dst_folder_id,
|
"dst_folder_id": self.dst_folder_id,
|
||||||
"dst_task_name": self.dst_task_name,
|
"dst_task_name": self.dst_task_name,
|
||||||
"dst_version": self.dst_version,
|
"version_up": self.version_up,
|
||||||
"variant": self.variant,
|
"variant": self.variant,
|
||||||
"comment": self.comment,
|
"comment": self.comment,
|
||||||
"new_folder_name": self.new_folder_name,
|
"new_folder_name": self.new_folder_name,
|
||||||
|
|
@ -948,10 +948,22 @@ class ProjectPushItemProcess:
|
||||||
self._product_entity = product_entity
|
self._product_entity = product_entity
|
||||||
return product_entity
|
return product_entity
|
||||||
|
|
||||||
|
src_attrib = self._src_product_entity["attrib"]
|
||||||
|
|
||||||
|
dst_attrib = {}
|
||||||
|
for key in {
|
||||||
|
"description",
|
||||||
|
"productGroup",
|
||||||
|
}:
|
||||||
|
value = src_attrib.get(key)
|
||||||
|
if value:
|
||||||
|
dst_attrib[key] = value
|
||||||
|
|
||||||
product_entity = new_product_entity(
|
product_entity = new_product_entity(
|
||||||
product_name,
|
product_name,
|
||||||
product_type,
|
product_type,
|
||||||
folder_id,
|
folder_id,
|
||||||
|
attribs=dst_attrib
|
||||||
)
|
)
|
||||||
self._operations.create_entity(
|
self._operations.create_entity(
|
||||||
project_name, "product", product_entity
|
project_name, "product", product_entity
|
||||||
|
|
@ -962,7 +974,7 @@ class ProjectPushItemProcess:
|
||||||
"""Make sure version document exits in database."""
|
"""Make sure version document exits in database."""
|
||||||
|
|
||||||
project_name = self._item.dst_project_name
|
project_name = self._item.dst_project_name
|
||||||
version = self._item.dst_version
|
version_up = self._item.version_up
|
||||||
src_version_entity = self._src_version_entity
|
src_version_entity = self._src_version_entity
|
||||||
product_entity = self._product_entity
|
product_entity = self._product_entity
|
||||||
product_id = product_entity["id"]
|
product_id = product_entity["id"]
|
||||||
|
|
@ -990,27 +1002,29 @@ class ProjectPushItemProcess:
|
||||||
"description",
|
"description",
|
||||||
"intent",
|
"intent",
|
||||||
}:
|
}:
|
||||||
if key in src_attrib:
|
value = src_attrib.get(key)
|
||||||
dst_attrib[key] = src_attrib[key]
|
if value:
|
||||||
|
dst_attrib[key] = value
|
||||||
|
|
||||||
if version is None:
|
last_version_entity = ayon_api.get_last_version_by_product_id(
|
||||||
last_version_entity = ayon_api.get_last_version_by_product_id(
|
project_name, product_id
|
||||||
project_name, product_id
|
)
|
||||||
|
if last_version_entity is None:
|
||||||
|
dst_version = get_versioning_start(
|
||||||
|
project_name,
|
||||||
|
self.host_name,
|
||||||
|
task_name=self._task_info.get("name"),
|
||||||
|
task_type=self._task_info.get("taskType"),
|
||||||
|
product_type=product_type,
|
||||||
|
product_name=product_entity["name"],
|
||||||
)
|
)
|
||||||
if last_version_entity:
|
else:
|
||||||
version = int(last_version_entity["version"]) + 1
|
dst_version = int(last_version_entity["version"])
|
||||||
else:
|
if version_up:
|
||||||
version = get_versioning_start(
|
dst_version += 1
|
||||||
project_name,
|
|
||||||
self.host_name,
|
|
||||||
task_name=self._task_info.get("name"),
|
|
||||||
task_type=self._task_info.get("taskType"),
|
|
||||||
product_type=product_type,
|
|
||||||
product_name=product_entity["name"],
|
|
||||||
)
|
|
||||||
|
|
||||||
existing_version_entity = ayon_api.get_version_by_name(
|
existing_version_entity = ayon_api.get_version_by_name(
|
||||||
project_name, version, product_id
|
project_name, dst_version, product_id
|
||||||
)
|
)
|
||||||
thumbnail_id = self._copy_version_thumbnail()
|
thumbnail_id = self._copy_version_thumbnail()
|
||||||
|
|
||||||
|
|
@ -1032,7 +1046,7 @@ class ProjectPushItemProcess:
|
||||||
copied_status = self._get_transferable_status(src_version_entity)
|
copied_status = self._get_transferable_status(src_version_entity)
|
||||||
|
|
||||||
version_entity = new_version_entity(
|
version_entity = new_version_entity(
|
||||||
version,
|
dst_version,
|
||||||
product_id,
|
product_id,
|
||||||
author=src_version_entity["author"],
|
author=src_version_entity["author"],
|
||||||
status=copied_status,
|
status=copied_status,
|
||||||
|
|
@ -1380,7 +1394,7 @@ class IntegrateModel:
|
||||||
variant,
|
variant,
|
||||||
comment,
|
comment,
|
||||||
new_folder_name,
|
new_folder_name,
|
||||||
dst_version,
|
version_up,
|
||||||
use_original_name
|
use_original_name
|
||||||
):
|
):
|
||||||
"""Create new item for integration.
|
"""Create new item for integration.
|
||||||
|
|
@ -1394,7 +1408,7 @@ class IntegrateModel:
|
||||||
variant (str): Variant name.
|
variant (str): Variant name.
|
||||||
comment (Union[str, None]): Comment.
|
comment (Union[str, None]): Comment.
|
||||||
new_folder_name (Union[str, None]): New folder name.
|
new_folder_name (Union[str, None]): New folder name.
|
||||||
dst_version (int): Destination version number.
|
version_up (bool): Should destination product be versioned up
|
||||||
use_original_name (bool): If original product names should be used
|
use_original_name (bool): If original product names should be used
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
@ -1411,7 +1425,7 @@ class IntegrateModel:
|
||||||
variant,
|
variant,
|
||||||
comment=comment,
|
comment=comment,
|
||||||
new_folder_name=new_folder_name,
|
new_folder_name=new_folder_name,
|
||||||
dst_version=dst_version,
|
version_up=version_up,
|
||||||
use_original_name=use_original_name
|
use_original_name=use_original_name
|
||||||
)
|
)
|
||||||
process_item = ProjectPushItemProcess(self, item)
|
process_item = ProjectPushItemProcess(self, item)
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,8 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
||||||
variant_input.setPlaceholderText("< Variant >")
|
variant_input.setPlaceholderText("< Variant >")
|
||||||
variant_input.setObjectName("ValidatedLineEdit")
|
variant_input.setObjectName("ValidatedLineEdit")
|
||||||
|
|
||||||
|
version_up_checkbox = NiceCheckbox(True, parent=inputs_widget)
|
||||||
|
|
||||||
comment_input = PlaceholderLineEdit(inputs_widget)
|
comment_input = PlaceholderLineEdit(inputs_widget)
|
||||||
comment_input.setPlaceholderText("< Publish comment >")
|
comment_input.setPlaceholderText("< Publish comment >")
|
||||||
|
|
||||||
|
|
@ -153,7 +155,11 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
||||||
inputs_layout.addRow("New folder name", folder_name_input)
|
inputs_layout.addRow("New folder name", folder_name_input)
|
||||||
inputs_layout.addRow("Variant", variant_input)
|
inputs_layout.addRow("Variant", variant_input)
|
||||||
inputs_layout.addRow(
|
inputs_layout.addRow(
|
||||||
"Use original product names", original_names_checkbox)
|
"Use original product names", original_names_checkbox
|
||||||
|
)
|
||||||
|
inputs_layout.addRow(
|
||||||
|
"Version up existing Product", version_up_checkbox
|
||||||
|
)
|
||||||
inputs_layout.addRow("Comment", comment_input)
|
inputs_layout.addRow("Comment", comment_input)
|
||||||
|
|
||||||
main_splitter.addWidget(context_widget)
|
main_splitter.addWidget(context_widget)
|
||||||
|
|
@ -209,8 +215,11 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
||||||
"Show error detail dialog to copy full error."
|
"Show error detail dialog to copy full error."
|
||||||
)
|
)
|
||||||
original_names_checkbox.setToolTip(
|
original_names_checkbox.setToolTip(
|
||||||
"Required for multi copy, doesn't allow changes "
|
"Required for multi copy, doesn't allow changes variant values."
|
||||||
"variant values."
|
)
|
||||||
|
version_up_checkbox.setToolTip(
|
||||||
|
"Version up existing product. If not selected version will be "
|
||||||
|
"updated."
|
||||||
)
|
)
|
||||||
|
|
||||||
overlay_close_btn = QtWidgets.QPushButton(
|
overlay_close_btn = QtWidgets.QPushButton(
|
||||||
|
|
@ -259,6 +268,8 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
||||||
library_only_checkbox.stateChanged.connect(self._on_library_only_change)
|
library_only_checkbox.stateChanged.connect(self._on_library_only_change)
|
||||||
original_names_checkbox.stateChanged.connect(
|
original_names_checkbox.stateChanged.connect(
|
||||||
self._on_original_names_change)
|
self._on_original_names_change)
|
||||||
|
version_up_checkbox.stateChanged.connect(
|
||||||
|
self._on_version_up_checkbox_change)
|
||||||
|
|
||||||
publish_btn.clicked.connect(self._on_select_click)
|
publish_btn.clicked.connect(self._on_select_click)
|
||||||
cancel_btn.clicked.connect(self._on_close_click)
|
cancel_btn.clicked.connect(self._on_close_click)
|
||||||
|
|
@ -308,6 +319,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
||||||
self._folder_name_input = folder_name_input
|
self._folder_name_input = folder_name_input
|
||||||
self._comment_input = comment_input
|
self._comment_input = comment_input
|
||||||
self._use_original_names_checkbox = original_names_checkbox
|
self._use_original_names_checkbox = original_names_checkbox
|
||||||
|
self._library_only_checkbox = library_only_checkbox
|
||||||
|
|
||||||
self._publish_btn = publish_btn
|
self._publish_btn = publish_btn
|
||||||
|
|
||||||
|
|
@ -328,6 +340,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
||||||
self._new_folder_name_input_text = None
|
self._new_folder_name_input_text = None
|
||||||
self._variant_input_text = None
|
self._variant_input_text = None
|
||||||
self._comment_input_text = None
|
self._comment_input_text = None
|
||||||
|
self._version_up_checkbox = version_up_checkbox
|
||||||
|
|
||||||
self._first_show = True
|
self._first_show = True
|
||||||
self._show_timer = show_timer
|
self._show_timer = show_timer
|
||||||
|
|
@ -344,6 +357,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
||||||
show_detail_btn.setVisible(False)
|
show_detail_btn.setVisible(False)
|
||||||
overlay_close_btn.setVisible(False)
|
overlay_close_btn.setVisible(False)
|
||||||
overlay_try_btn.setVisible(False)
|
overlay_try_btn.setVisible(False)
|
||||||
|
version_up_checkbox.setChecked(False)
|
||||||
|
|
||||||
# Support of public api function of controller
|
# Support of public api function of controller
|
||||||
def set_source(self, project_name, version_ids):
|
def set_source(self, project_name, version_ids):
|
||||||
|
|
@ -376,7 +390,6 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
||||||
self._invalidate_new_folder_name(
|
self._invalidate_new_folder_name(
|
||||||
new_folder_name, user_values["is_new_folder_name_valid"]
|
new_folder_name, user_values["is_new_folder_name_valid"]
|
||||||
)
|
)
|
||||||
self._controller._invalidate()
|
|
||||||
self._projects_combobox.refresh()
|
self._projects_combobox.refresh()
|
||||||
|
|
||||||
def _on_first_show(self):
|
def _on_first_show(self):
|
||||||
|
|
@ -415,14 +428,18 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
||||||
self._comment_input_text = text
|
self._comment_input_text = text
|
||||||
self._user_input_changed_timer.start()
|
self._user_input_changed_timer.start()
|
||||||
|
|
||||||
def _on_library_only_change(self, state: int) -> None:
|
def _on_library_only_change(self) -> None:
|
||||||
"""Change toggle state, reset filter, recalculate dropdown"""
|
"""Change toggle state, reset filter, recalculate dropdown"""
|
||||||
state = bool(state)
|
is_checked = self._library_only_checkbox.isChecked()
|
||||||
self._projects_combobox.set_standard_filter_enabled(state)
|
self._projects_combobox.set_standard_filter_enabled(is_checked)
|
||||||
|
|
||||||
def _on_original_names_change(self, state: int) -> None:
|
def _on_original_names_change(self) -> None:
|
||||||
use_original_name = bool(state)
|
is_checked = self._use_original_names_checkbox.isChecked()
|
||||||
self._invalidate_use_original_names(use_original_name)
|
self._invalidate_use_original_names(is_checked)
|
||||||
|
|
||||||
|
def _on_version_up_checkbox_change(self) -> None:
|
||||||
|
is_checked = self._version_up_checkbox.isChecked()
|
||||||
|
self._controller.set_version_up(is_checked)
|
||||||
|
|
||||||
def _on_user_input_timer(self):
|
def _on_user_input_timer(self):
|
||||||
folder_name_enabled = self._new_folder_name_enabled
|
folder_name_enabled = self._new_folder_name_enabled
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ from .folders_widget import (
|
||||||
FoldersQtModel,
|
FoldersQtModel,
|
||||||
FOLDERS_MODEL_SENDER_NAME,
|
FOLDERS_MODEL_SENDER_NAME,
|
||||||
SimpleFoldersWidget,
|
SimpleFoldersWidget,
|
||||||
|
FoldersFiltersWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .tasks_widget import (
|
from .tasks_widget import (
|
||||||
|
|
@ -160,6 +161,7 @@ __all__ = (
|
||||||
"FoldersQtModel",
|
"FoldersQtModel",
|
||||||
"FOLDERS_MODEL_SENDER_NAME",
|
"FOLDERS_MODEL_SENDER_NAME",
|
||||||
"SimpleFoldersWidget",
|
"SimpleFoldersWidget",
|
||||||
|
"FoldersFiltersWidget",
|
||||||
|
|
||||||
"TasksWidget",
|
"TasksWidget",
|
||||||
"TasksQtModel",
|
"TasksQtModel",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import qtpy
|
|
||||||
from qtpy import QtWidgets, QtCore, QtGui
|
from qtpy import QtWidgets, QtCore, QtGui
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -6,7 +5,7 @@ class PickScreenColorWidget(QtWidgets.QWidget):
|
||||||
color_selected = QtCore.Signal(QtGui.QColor)
|
color_selected = QtCore.Signal(QtGui.QColor)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super(PickScreenColorWidget, self).__init__(parent)
|
super().__init__(parent)
|
||||||
self.labels = []
|
self.labels = []
|
||||||
self.magnification = 2
|
self.magnification = 2
|
||||||
|
|
||||||
|
|
@ -53,7 +52,7 @@ class PickLabel(QtWidgets.QLabel):
|
||||||
close_session = QtCore.Signal()
|
close_session = QtCore.Signal()
|
||||||
|
|
||||||
def __init__(self, pick_widget):
|
def __init__(self, pick_widget):
|
||||||
super(PickLabel, self).__init__()
|
super().__init__()
|
||||||
self.setMouseTracking(True)
|
self.setMouseTracking(True)
|
||||||
|
|
||||||
self.pick_widget = pick_widget
|
self.pick_widget = pick_widget
|
||||||
|
|
@ -74,14 +73,10 @@ class PickLabel(QtWidgets.QLabel):
|
||||||
self.show()
|
self.show()
|
||||||
self.windowHandle().setScreen(screen_obj)
|
self.windowHandle().setScreen(screen_obj)
|
||||||
geo = screen_obj.geometry()
|
geo = screen_obj.geometry()
|
||||||
args = (
|
pix = screen_obj.grabWindow(
|
||||||
QtWidgets.QApplication.desktop().winId(),
|
self.winId(),
|
||||||
geo.x(), geo.y(), geo.width(), geo.height()
|
geo.x(), geo.y(), geo.width(), geo.height()
|
||||||
)
|
)
|
||||||
if qtpy.API in ("pyqt4", "pyside"):
|
|
||||||
pix = QtGui.QPixmap.grabWindow(*args)
|
|
||||||
else:
|
|
||||||
pix = screen_obj.grabWindow(*args)
|
|
||||||
|
|
||||||
if pix.width() > pix.height():
|
if pix.width() > pix.height():
|
||||||
size = pix.height()
|
size = pix.height()
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ from ayon_core.tools.common_models import (
|
||||||
from .models import RecursiveSortFilterProxyModel
|
from .models import RecursiveSortFilterProxyModel
|
||||||
from .views import TreeView
|
from .views import TreeView
|
||||||
from .lib import RefreshThread, get_qt_icon
|
from .lib import RefreshThread, get_qt_icon
|
||||||
|
from .widgets import PlaceholderLineEdit
|
||||||
|
from .nice_checkbox import NiceCheckbox
|
||||||
|
|
||||||
|
|
||||||
FOLDERS_MODEL_SENDER_NAME = "qt_folders_model"
|
FOLDERS_MODEL_SENDER_NAME = "qt_folders_model"
|
||||||
|
|
@ -343,6 +345,8 @@ class FoldersProxyModel(RecursiveSortFilterProxyModel):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
self.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||||
|
|
||||||
self._folder_ids_filter = None
|
self._folder_ids_filter = None
|
||||||
|
|
||||||
def set_folder_ids_filter(self, folder_ids: Optional[list[str]]):
|
def set_folder_ids_filter(self, folder_ids: Optional[list[str]]):
|
||||||
|
|
@ -794,3 +798,47 @@ class SimpleFoldersWidget(FoldersWidget):
|
||||||
event (Event): Triggered event.
|
event (Event): Triggered event.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FoldersFiltersWidget(QtWidgets.QWidget):
|
||||||
|
"""Helper widget for most commonly used filters in context selection."""
|
||||||
|
text_changed = QtCore.Signal(str)
|
||||||
|
my_tasks_changed = QtCore.Signal(bool)
|
||||||
|
|
||||||
|
def __init__(self, parent: QtWidgets.QWidget) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
folders_filter_input = PlaceholderLineEdit(self)
|
||||||
|
folders_filter_input.setPlaceholderText("Folder name filter...")
|
||||||
|
|
||||||
|
my_tasks_tooltip = (
|
||||||
|
"Filter folders and task to only those you are assigned to."
|
||||||
|
)
|
||||||
|
my_tasks_label = QtWidgets.QLabel("My tasks", self)
|
||||||
|
my_tasks_label.setToolTip(my_tasks_tooltip)
|
||||||
|
|
||||||
|
my_tasks_checkbox = NiceCheckbox(self)
|
||||||
|
my_tasks_checkbox.setChecked(False)
|
||||||
|
my_tasks_checkbox.setToolTip(my_tasks_tooltip)
|
||||||
|
|
||||||
|
layout = QtWidgets.QHBoxLayout(self)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.setSpacing(5)
|
||||||
|
layout.addWidget(folders_filter_input, 1)
|
||||||
|
layout.addWidget(my_tasks_label, 0)
|
||||||
|
layout.addWidget(my_tasks_checkbox, 0)
|
||||||
|
|
||||||
|
folders_filter_input.textChanged.connect(self.text_changed)
|
||||||
|
my_tasks_checkbox.stateChanged.connect(self._on_my_tasks_change)
|
||||||
|
|
||||||
|
self._folders_filter_input = folders_filter_input
|
||||||
|
self._my_tasks_checkbox = my_tasks_checkbox
|
||||||
|
|
||||||
|
def set_text(self, text: str) -> None:
|
||||||
|
self._folders_filter_input.setText(text)
|
||||||
|
|
||||||
|
def set_my_tasks_checked(self, checked: bool) -> None:
|
||||||
|
self._my_tasks_checkbox.setChecked(checked)
|
||||||
|
|
||||||
|
def _on_my_tasks_change(self, _state: int) -> None:
|
||||||
|
self.my_tasks_changed.emit(self._my_tasks_checkbox.isChecked())
|
||||||
|
|
|
||||||
|
|
@ -53,14 +53,8 @@ def checkstate_enum_to_int(state):
|
||||||
|
|
||||||
def center_window(window):
|
def center_window(window):
|
||||||
"""Move window to center of it's screen."""
|
"""Move window to center of it's screen."""
|
||||||
|
screen = window.screen()
|
||||||
if hasattr(QtWidgets.QApplication, "desktop"):
|
screen_geo = screen.geometry()
|
||||||
desktop = QtWidgets.QApplication.desktop()
|
|
||||||
screen_idx = desktop.screenNumber(window)
|
|
||||||
screen_geo = desktop.screenGeometry(screen_idx)
|
|
||||||
else:
|
|
||||||
screen = window.screen()
|
|
||||||
screen_geo = screen.geometry()
|
|
||||||
|
|
||||||
geo = window.frameGeometry()
|
geo = window.frameGeometry()
|
||||||
geo.moveCenter(screen_geo.center())
|
geo.moveCenter(screen_geo.center())
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,15 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
import typing
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from ayon_core.style import get_default_entity_icon_color
|
from ayon_core.style import get_default_entity_icon_color
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
from ayon_core.host import PublishedWorkfileInfo
|
||||||
|
|
||||||
|
|
||||||
class FolderItem:
|
class FolderItem:
|
||||||
"""Item representing folder entity on a server.
|
"""Item representing folder entity on a server.
|
||||||
|
|
@ -159,6 +166,17 @@ class WorkareaFilepathResult:
|
||||||
self.filepath = filepath
|
self.filepath = filepath
|
||||||
|
|
||||||
|
|
||||||
|
class PublishedWorkfileWrap:
|
||||||
|
"""Wrapper for workfile info that also contains version comment."""
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
info: Optional[PublishedWorkfileInfo] = None,
|
||||||
|
comment: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
self.info = info
|
||||||
|
self.comment = comment
|
||||||
|
|
||||||
|
|
||||||
class AbstractWorkfilesCommon(ABC):
|
class AbstractWorkfilesCommon(ABC):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def is_host_valid(self):
|
def is_host_valid(self):
|
||||||
|
|
@ -787,6 +805,25 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_published_workfile_info(
|
||||||
|
self,
|
||||||
|
folder_id: Optional[str],
|
||||||
|
representation_id: Optional[str],
|
||||||
|
) -> PublishedWorkfileWrap:
|
||||||
|
"""Get published workfile info by representation ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
folder_id (Optional[str]): Folder id.
|
||||||
|
representation_id (Optional[str]): Representation id.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PublishedWorkfileWrap: Published workfile info or None
|
||||||
|
if not found.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_workfile_info(self, folder_id, task_id, rootless_path):
|
def get_workfile_info(self, folder_id, task_id, rootless_path):
|
||||||
"""Workfile info from database.
|
"""Workfile info from database.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import ayon_api
|
import ayon_api
|
||||||
|
|
||||||
|
|
@ -18,6 +21,7 @@ from ayon_core.tools.common_models import (
|
||||||
from .abstract import (
|
from .abstract import (
|
||||||
AbstractWorkfilesBackend,
|
AbstractWorkfilesBackend,
|
||||||
AbstractWorkfilesFrontend,
|
AbstractWorkfilesFrontend,
|
||||||
|
PublishedWorkfileWrap,
|
||||||
)
|
)
|
||||||
from .models import SelectionModel, WorkfilesModel
|
from .models import SelectionModel, WorkfilesModel
|
||||||
|
|
||||||
|
|
@ -432,6 +436,15 @@ class BaseWorkfileController(
|
||||||
folder_id, task_id
|
folder_id, task_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_published_workfile_info(
|
||||||
|
self,
|
||||||
|
folder_id: Optional[str],
|
||||||
|
representation_id: Optional[str],
|
||||||
|
) -> PublishedWorkfileWrap:
|
||||||
|
return self._workfiles_model.get_published_workfile_info(
|
||||||
|
folder_id, representation_id
|
||||||
|
)
|
||||||
|
|
||||||
def get_workfile_info(self, folder_id, task_id, rootless_path):
|
def get_workfile_info(self, folder_id, task_id, rootless_path):
|
||||||
return self._workfiles_model.get_workfile_info(
|
return self._workfiles_model.get_workfile_info(
|
||||||
folder_id, task_id, rootless_path
|
folder_id, task_id, rootless_path
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ class SelectionModel(object):
|
||||||
self._task_name = None
|
self._task_name = None
|
||||||
self._task_id = None
|
self._task_id = None
|
||||||
self._workfile_path = None
|
self._workfile_path = None
|
||||||
|
self._rootless_workfile_path = None
|
||||||
|
self._workfile_entity_id = None
|
||||||
self._representation_id = None
|
self._representation_id = None
|
||||||
|
|
||||||
def get_selected_folder_id(self):
|
def get_selected_folder_id(self):
|
||||||
|
|
@ -62,39 +64,49 @@ class SelectionModel(object):
|
||||||
def get_selected_workfile_path(self):
|
def get_selected_workfile_path(self):
|
||||||
return self._workfile_path
|
return self._workfile_path
|
||||||
|
|
||||||
|
def get_selected_workfile_data(self):
|
||||||
|
return {
|
||||||
|
"project_name": self._controller.get_current_project_name(),
|
||||||
|
"path": self._workfile_path,
|
||||||
|
"rootless_path": self._rootless_workfile_path,
|
||||||
|
"folder_id": self._folder_id,
|
||||||
|
"task_name": self._task_name,
|
||||||
|
"task_id": self._task_id,
|
||||||
|
"workfile_entity_id": self._workfile_entity_id,
|
||||||
|
}
|
||||||
|
|
||||||
def set_selected_workfile_path(
|
def set_selected_workfile_path(
|
||||||
self, rootless_path, path, workfile_entity_id
|
self, rootless_path, path, workfile_entity_id
|
||||||
):
|
):
|
||||||
if path == self._workfile_path:
|
if path == self._workfile_path:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self._rootless_workfile_path = rootless_path
|
||||||
self._workfile_path = path
|
self._workfile_path = path
|
||||||
|
self._workfile_entity_id = workfile_entity_id
|
||||||
self._controller.emit_event(
|
self._controller.emit_event(
|
||||||
"selection.workarea.changed",
|
"selection.workarea.changed",
|
||||||
{
|
self.get_selected_workfile_data(),
|
||||||
"project_name": self._controller.get_current_project_name(),
|
|
||||||
"path": path,
|
|
||||||
"rootless_path": rootless_path,
|
|
||||||
"folder_id": self._folder_id,
|
|
||||||
"task_name": self._task_name,
|
|
||||||
"task_id": self._task_id,
|
|
||||||
"workfile_entity_id": workfile_entity_id,
|
|
||||||
},
|
|
||||||
self.event_source
|
self.event_source
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_selected_representation_id(self):
|
def get_selected_representation_id(self):
|
||||||
return self._representation_id
|
return self._representation_id
|
||||||
|
|
||||||
|
def get_selected_representation_data(self):
|
||||||
|
return {
|
||||||
|
"project_name": self._controller.get_current_project_name(),
|
||||||
|
"folder_id": self._folder_id,
|
||||||
|
"task_id": self._task_id,
|
||||||
|
"representation_id": self._representation_id,
|
||||||
|
}
|
||||||
|
|
||||||
def set_selected_representation_id(self, representation_id):
|
def set_selected_representation_id(self, representation_id):
|
||||||
if representation_id == self._representation_id:
|
if representation_id == self._representation_id:
|
||||||
return
|
return
|
||||||
self._representation_id = representation_id
|
self._representation_id = representation_id
|
||||||
self._controller.emit_event(
|
self._controller.emit_event(
|
||||||
"selection.representation.changed",
|
"selection.representation.changed",
|
||||||
{
|
self.get_selected_representation_data(),
|
||||||
"project_name": self._controller.get_current_project_name(),
|
|
||||||
"representation_id": representation_id,
|
|
||||||
},
|
|
||||||
self.event_source
|
self.event_source
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ from ayon_core.pipeline.workfile import (
|
||||||
from ayon_core.pipeline.version_start import get_versioning_start
|
from ayon_core.pipeline.version_start import get_versioning_start
|
||||||
from ayon_core.tools.workfiles.abstract import (
|
from ayon_core.tools.workfiles.abstract import (
|
||||||
WorkareaFilepathResult,
|
WorkareaFilepathResult,
|
||||||
|
PublishedWorkfileWrap,
|
||||||
AbstractWorkfilesBackend,
|
AbstractWorkfilesBackend,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -79,6 +80,7 @@ class WorkfilesModel:
|
||||||
|
|
||||||
# Published workfiles
|
# Published workfiles
|
||||||
self._repre_by_id = {}
|
self._repre_by_id = {}
|
||||||
|
self._version_comment_by_id = {}
|
||||||
self._published_workfile_items_cache = NestedCacheItem(
|
self._published_workfile_items_cache = NestedCacheItem(
|
||||||
levels=1, default_factory=list
|
levels=1, default_factory=list
|
||||||
)
|
)
|
||||||
|
|
@ -95,6 +97,7 @@ class WorkfilesModel:
|
||||||
self._workarea_file_items_cache.reset()
|
self._workarea_file_items_cache.reset()
|
||||||
|
|
||||||
self._repre_by_id = {}
|
self._repre_by_id = {}
|
||||||
|
self._version_comment_by_id = {}
|
||||||
self._published_workfile_items_cache.reset()
|
self._published_workfile_items_cache.reset()
|
||||||
|
|
||||||
self._workfile_entities_by_task_id = {}
|
self._workfile_entities_by_task_id = {}
|
||||||
|
|
@ -552,13 +555,13 @@ class WorkfilesModel:
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_published_file_items(
|
def get_published_file_items(
|
||||||
self, folder_id: str, task_id: str
|
self, folder_id: Optional[str], task_id: Optional[str]
|
||||||
) -> list[PublishedWorkfileInfo]:
|
) -> list[PublishedWorkfileInfo]:
|
||||||
"""Published workfiles for passed context.
|
"""Published workfiles for passed context.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
folder_id (str): Folder id.
|
folder_id (Optional[str]): Folder id.
|
||||||
task_id (str): Task id.
|
task_id (Optional[str]): Task id.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[PublishedWorkfileInfo]: List of files for published workfiles.
|
list[PublishedWorkfileInfo]: List of files for published workfiles.
|
||||||
|
|
@ -586,7 +589,7 @@ class WorkfilesModel:
|
||||||
version_entities = list(ayon_api.get_versions(
|
version_entities = list(ayon_api.get_versions(
|
||||||
project_name,
|
project_name,
|
||||||
product_ids=product_ids,
|
product_ids=product_ids,
|
||||||
fields={"id", "author", "taskId"},
|
fields={"id", "author", "taskId", "attrib.comment"},
|
||||||
))
|
))
|
||||||
|
|
||||||
repre_entities = []
|
repre_entities = []
|
||||||
|
|
@ -600,6 +603,13 @@ class WorkfilesModel:
|
||||||
repre_entity["id"]: repre_entity
|
repre_entity["id"]: repre_entity
|
||||||
for repre_entity in repre_entities
|
for repre_entity in repre_entities
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Map versions by representation ID for easy lookup
|
||||||
|
self._version_comment_by_id.update({
|
||||||
|
version_entity["id"]: version_entity["attrib"].get("comment")
|
||||||
|
for version_entity in version_entities
|
||||||
|
})
|
||||||
|
|
||||||
project_entity = self._controller.get_project_entity(project_name)
|
project_entity = self._controller.get_project_entity(project_name)
|
||||||
|
|
||||||
prepared_data = ListPublishedWorkfilesOptionalData(
|
prepared_data = ListPublishedWorkfilesOptionalData(
|
||||||
|
|
@ -626,6 +636,34 @@ class WorkfilesModel:
|
||||||
]
|
]
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
def get_published_workfile_info(
|
||||||
|
self,
|
||||||
|
folder_id: Optional[str],
|
||||||
|
representation_id: Optional[str],
|
||||||
|
) -> PublishedWorkfileWrap:
|
||||||
|
"""Get published workfile info by representation ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
folder_id (Optional[str]): Folder id.
|
||||||
|
representation_id (Optional[str]): Representation id.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PublishedWorkfileWrap: Published workfile info or None
|
||||||
|
if not found.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not representation_id:
|
||||||
|
return PublishedWorkfileWrap()
|
||||||
|
|
||||||
|
# Search through all cached published workfile items
|
||||||
|
for item in self.get_published_file_items(folder_id, None):
|
||||||
|
if item.representation_id == representation_id:
|
||||||
|
comment = self._get_published_workfile_version_comment(
|
||||||
|
representation_id
|
||||||
|
)
|
||||||
|
return PublishedWorkfileWrap(item, comment)
|
||||||
|
return PublishedWorkfileWrap()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _project_name(self) -> str:
|
def _project_name(self) -> str:
|
||||||
return self._controller.get_current_project_name()
|
return self._controller.get_current_project_name()
|
||||||
|
|
@ -642,6 +680,25 @@ class WorkfilesModel:
|
||||||
self._current_username = get_ayon_username()
|
self._current_username = get_ayon_username()
|
||||||
return self._current_username
|
return self._current_username
|
||||||
|
|
||||||
|
def _get_published_workfile_version_comment(
|
||||||
|
self, representation_id: str
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""Get version comment for published workfile.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
representation_id (str): Representation id.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[str]: Version comment or None.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not representation_id:
|
||||||
|
return None
|
||||||
|
repre = self._repre_by_id.get(representation_id)
|
||||||
|
if not repre:
|
||||||
|
return None
|
||||||
|
return self._version_comment_by_id.get(repre["versionId"])
|
||||||
|
|
||||||
# --- Host ---
|
# --- Host ---
|
||||||
def _open_workfile(self, folder_id: str, task_id: str, filepath: str):
|
def _open_workfile(self, folder_id: str, task_id: str, filepath: str):
|
||||||
# TODO move to workfiles pipeline
|
# TODO move to workfiles pipeline
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from qtpy import QtWidgets, QtCore
|
from qtpy import QtCore, QtWidgets
|
||||||
|
|
||||||
|
|
||||||
def file_size_to_string(file_size):
|
def file_size_to_string(file_size):
|
||||||
|
|
@ -8,9 +9,9 @@ def file_size_to_string(file_size):
|
||||||
return "N/A"
|
return "N/A"
|
||||||
size = 0
|
size = 0
|
||||||
size_ending_mapping = {
|
size_ending_mapping = {
|
||||||
"KB": 1024 ** 1,
|
"KB": 1024**1,
|
||||||
"MB": 1024 ** 2,
|
"MB": 1024**2,
|
||||||
"GB": 1024 ** 3
|
"GB": 1024**3,
|
||||||
}
|
}
|
||||||
ending = "B"
|
ending = "B"
|
||||||
for _ending, _size in size_ending_mapping.items():
|
for _ending, _size in size_ending_mapping.items():
|
||||||
|
|
@ -70,7 +71,12 @@ class SidePanelWidget(QtWidgets.QWidget):
|
||||||
btn_description_save.clicked.connect(self._on_save_click)
|
btn_description_save.clicked.connect(self._on_save_click)
|
||||||
|
|
||||||
controller.register_event_callback(
|
controller.register_event_callback(
|
||||||
"selection.workarea.changed", self._on_selection_change
|
"selection.workarea.changed",
|
||||||
|
self._on_workarea_selection_change
|
||||||
|
)
|
||||||
|
controller.register_event_callback(
|
||||||
|
"selection.representation.changed",
|
||||||
|
self._on_representation_selection_change,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._details_input = details_input
|
self._details_input = details_input
|
||||||
|
|
@ -82,12 +88,13 @@ class SidePanelWidget(QtWidgets.QWidget):
|
||||||
self._task_id = None
|
self._task_id = None
|
||||||
self._filepath = None
|
self._filepath = None
|
||||||
self._rootless_path = None
|
self._rootless_path = None
|
||||||
|
self._representation_id = None
|
||||||
self._orig_description = ""
|
self._orig_description = ""
|
||||||
self._controller = controller
|
self._controller = controller
|
||||||
|
|
||||||
self._set_context(None, None, None, None)
|
self._set_context(False, None, None)
|
||||||
|
|
||||||
def set_published_mode(self, published_mode):
|
def set_published_mode(self, published_mode: bool) -> None:
|
||||||
"""Change published mode.
|
"""Change published mode.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -95,14 +102,37 @@ class SidePanelWidget(QtWidgets.QWidget):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self._description_widget.setVisible(not published_mode)
|
self._description_widget.setVisible(not published_mode)
|
||||||
|
# Clear the context when switching modes to avoid showing stale data
|
||||||
|
if published_mode:
|
||||||
|
self._set_publish_context(
|
||||||
|
self._folder_id,
|
||||||
|
self._task_id,
|
||||||
|
self._representation_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._set_workarea_context(
|
||||||
|
self._folder_id,
|
||||||
|
self._task_id,
|
||||||
|
self._rootless_path,
|
||||||
|
self._filepath,
|
||||||
|
)
|
||||||
|
|
||||||
def _on_selection_change(self, event):
|
def _on_workarea_selection_change(self, event):
|
||||||
folder_id = event["folder_id"]
|
folder_id = event["folder_id"]
|
||||||
task_id = event["task_id"]
|
task_id = event["task_id"]
|
||||||
filepath = event["path"]
|
filepath = event["path"]
|
||||||
rootless_path = event["rootless_path"]
|
rootless_path = event["rootless_path"]
|
||||||
|
|
||||||
self._set_context(folder_id, task_id, rootless_path, filepath)
|
self._set_workarea_context(
|
||||||
|
folder_id, task_id, rootless_path, filepath
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_representation_selection_change(self, event):
|
||||||
|
folder_id = event["folder_id"]
|
||||||
|
task_id = event["task_id"]
|
||||||
|
representation_id = event["representation_id"]
|
||||||
|
|
||||||
|
self._set_publish_context(folder_id, task_id, representation_id)
|
||||||
|
|
||||||
def _on_description_change(self):
|
def _on_description_change(self):
|
||||||
text = self._description_input.toPlainText()
|
text = self._description_input.toPlainText()
|
||||||
|
|
@ -118,85 +148,134 @@ class SidePanelWidget(QtWidgets.QWidget):
|
||||||
self._orig_description = description
|
self._orig_description = description
|
||||||
self._btn_description_save.setEnabled(False)
|
self._btn_description_save.setEnabled(False)
|
||||||
|
|
||||||
def _set_context(self, folder_id, task_id, rootless_path, filepath):
|
def _set_workarea_context(
|
||||||
|
self,
|
||||||
|
folder_id: Optional[str],
|
||||||
|
task_id: Optional[str],
|
||||||
|
rootless_path: Optional[str],
|
||||||
|
filepath: Optional[str],
|
||||||
|
) -> None:
|
||||||
|
self._rootless_path = rootless_path
|
||||||
|
self._filepath = filepath
|
||||||
|
|
||||||
workfile_info = None
|
workfile_info = None
|
||||||
# Check if folder, task and file are selected
|
# Check if folder, task and file are selected
|
||||||
if folder_id and task_id and rootless_path:
|
if folder_id and task_id and rootless_path:
|
||||||
workfile_info = self._controller.get_workfile_info(
|
workfile_info = self._controller.get_workfile_info(
|
||||||
folder_id, task_id, rootless_path
|
folder_id, task_id, rootless_path
|
||||||
)
|
)
|
||||||
enabled = workfile_info is not None
|
|
||||||
|
|
||||||
self._details_input.setEnabled(enabled)
|
if workfile_info is None:
|
||||||
self._description_input.setEnabled(enabled)
|
|
||||||
self._btn_description_save.setEnabled(enabled)
|
|
||||||
|
|
||||||
self._folder_id = folder_id
|
|
||||||
self._task_id = task_id
|
|
||||||
self._filepath = filepath
|
|
||||||
self._rootless_path = rootless_path
|
|
||||||
|
|
||||||
# Disable inputs and remove texts if any required arguments are
|
|
||||||
# missing
|
|
||||||
if not enabled:
|
|
||||||
self._orig_description = ""
|
self._orig_description = ""
|
||||||
self._details_input.setPlainText("")
|
|
||||||
self._description_input.setPlainText("")
|
self._description_input.setPlainText("")
|
||||||
|
self._set_context(False, folder_id, task_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
description = workfile_info.description
|
self._set_context(
|
||||||
size_value = file_size_to_string(workfile_info.file_size)
|
True,
|
||||||
|
folder_id,
|
||||||
|
task_id,
|
||||||
|
file_created=workfile_info.file_created,
|
||||||
|
file_modified=workfile_info.file_modified,
|
||||||
|
size_value=workfile_info.file_size,
|
||||||
|
created_by=workfile_info.created_by,
|
||||||
|
updated_by=workfile_info.updated_by,
|
||||||
|
)
|
||||||
|
|
||||||
|
description = workfile_info.description
|
||||||
|
self._orig_description = description
|
||||||
|
self._description_input.setPlainText(description)
|
||||||
|
|
||||||
|
def _set_publish_context(
|
||||||
|
self,
|
||||||
|
folder_id: Optional[str],
|
||||||
|
task_id: Optional[str],
|
||||||
|
representation_id: Optional[str],
|
||||||
|
) -> None:
|
||||||
|
self._representation_id = representation_id
|
||||||
|
published_workfile_wrap = self._controller.get_published_workfile_info(
|
||||||
|
folder_id,
|
||||||
|
representation_id,
|
||||||
|
)
|
||||||
|
info = published_workfile_wrap.info
|
||||||
|
comment = published_workfile_wrap.comment
|
||||||
|
if info is None:
|
||||||
|
self._set_context(False, folder_id, task_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._set_context(
|
||||||
|
True,
|
||||||
|
folder_id,
|
||||||
|
task_id,
|
||||||
|
file_created=info.file_created,
|
||||||
|
file_modified=info.file_modified,
|
||||||
|
size_value=info.file_size,
|
||||||
|
created_by=info.author,
|
||||||
|
comment=comment,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _set_context(
|
||||||
|
self,
|
||||||
|
is_valid: bool,
|
||||||
|
folder_id: Optional[str],
|
||||||
|
task_id: Optional[str],
|
||||||
|
*,
|
||||||
|
file_created: Optional[int] = None,
|
||||||
|
file_modified: Optional[int] = None,
|
||||||
|
size_value: Optional[int] = None,
|
||||||
|
created_by: Optional[str] = None,
|
||||||
|
updated_by: Optional[str] = None,
|
||||||
|
comment: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
self._folder_id = folder_id
|
||||||
|
self._task_id = task_id
|
||||||
|
|
||||||
|
self._details_input.setEnabled(is_valid)
|
||||||
|
self._description_input.setEnabled(is_valid)
|
||||||
|
self._btn_description_save.setEnabled(is_valid)
|
||||||
|
if not is_valid:
|
||||||
|
self._details_input.setPlainText("")
|
||||||
|
return
|
||||||
|
|
||||||
# Append html string
|
|
||||||
datetime_format = "%b %d %Y %H:%M:%S"
|
datetime_format = "%b %d %Y %H:%M:%S"
|
||||||
file_created = workfile_info.file_created
|
|
||||||
modification_time = workfile_info.file_modified
|
|
||||||
if file_created:
|
if file_created:
|
||||||
file_created = datetime.datetime.fromtimestamp(file_created)
|
file_created = datetime.datetime.fromtimestamp(file_created)
|
||||||
|
|
||||||
if modification_time:
|
if file_modified:
|
||||||
modification_time = datetime.datetime.fromtimestamp(
|
file_modified = datetime.datetime.fromtimestamp(
|
||||||
modification_time)
|
file_modified
|
||||||
|
)
|
||||||
|
|
||||||
user_items_by_name = self._controller.get_user_items_by_name()
|
user_items_by_name = self._controller.get_user_items_by_name()
|
||||||
|
|
||||||
def convert_username(username):
|
def convert_username(username_v):
|
||||||
user_item = user_items_by_name.get(username)
|
user_item = user_items_by_name.get(username_v)
|
||||||
if user_item is not None and user_item.full_name:
|
if user_item is not None and user_item.full_name:
|
||||||
return user_item.full_name
|
return user_item.full_name
|
||||||
return username
|
return username_v
|
||||||
|
|
||||||
created_lines = []
|
lines = []
|
||||||
if workfile_info.created_by:
|
if size_value is not None:
|
||||||
created_lines.append(
|
size_value = file_size_to_string(size_value)
|
||||||
convert_username(workfile_info.created_by)
|
lines.append(f"<b>Size:</b><br/>{size_value}")
|
||||||
)
|
|
||||||
if file_created:
|
|
||||||
created_lines.append(file_created.strftime(datetime_format))
|
|
||||||
|
|
||||||
if created_lines:
|
# Add version comment for published workfiles
|
||||||
created_lines.insert(0, "<b>Created:</b>")
|
if comment:
|
||||||
|
lines.append(f"<b>Comment:</b><br/>{comment}")
|
||||||
|
|
||||||
modified_lines = []
|
if created_by or file_created:
|
||||||
if workfile_info.updated_by:
|
lines.append("<b>Created:</b>")
|
||||||
modified_lines.append(
|
if created_by:
|
||||||
convert_username(workfile_info.updated_by)
|
lines.append(convert_username(created_by))
|
||||||
)
|
if file_created:
|
||||||
if modification_time:
|
lines.append(file_created.strftime(datetime_format))
|
||||||
modified_lines.append(
|
|
||||||
modification_time.strftime(datetime_format)
|
|
||||||
)
|
|
||||||
if modified_lines:
|
|
||||||
modified_lines.insert(0, "<b>Modified:</b>")
|
|
||||||
|
|
||||||
lines = (
|
if updated_by or file_modified:
|
||||||
"<b>Size:</b>",
|
lines.append("<b>Modified:</b>")
|
||||||
size_value,
|
if updated_by:
|
||||||
"<br/>".join(created_lines),
|
lines.append(convert_username(updated_by))
|
||||||
"<br/>".join(modified_lines),
|
if file_modified:
|
||||||
)
|
lines.append(file_modified.strftime(datetime_format))
|
||||||
self._orig_description = description
|
|
||||||
self._description_input.setPlainText(description)
|
|
||||||
|
|
||||||
# Set as empty string
|
# Set as empty string
|
||||||
self._details_input.setPlainText("")
|
self._details_input.setPlainText("")
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,11 @@ from ayon_core.tools.utils import (
|
||||||
FoldersWidget,
|
FoldersWidget,
|
||||||
GoToCurrentButton,
|
GoToCurrentButton,
|
||||||
MessageOverlayObject,
|
MessageOverlayObject,
|
||||||
NiceCheckbox,
|
|
||||||
PlaceholderLineEdit,
|
PlaceholderLineEdit,
|
||||||
RefreshButton,
|
RefreshButton,
|
||||||
TasksWidget,
|
TasksWidget,
|
||||||
|
FoldersFiltersWidget,
|
||||||
)
|
)
|
||||||
from ayon_core.tools.utils.lib import checkstate_int_to_enum
|
|
||||||
from ayon_core.tools.workfiles.control import BaseWorkfileController
|
from ayon_core.tools.workfiles.control import BaseWorkfileController
|
||||||
|
|
||||||
from .files_widget import FilesWidget
|
from .files_widget import FilesWidget
|
||||||
|
|
@ -69,7 +68,6 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
|
||||||
self._default_window_flags = flags
|
self._default_window_flags = flags
|
||||||
|
|
||||||
self._folders_widget = None
|
self._folders_widget = None
|
||||||
self._folder_filter_input = None
|
|
||||||
|
|
||||||
self._files_widget = None
|
self._files_widget = None
|
||||||
|
|
||||||
|
|
@ -178,48 +176,33 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
|
||||||
col_widget = QtWidgets.QWidget(parent)
|
col_widget = QtWidgets.QWidget(parent)
|
||||||
header_widget = QtWidgets.QWidget(col_widget)
|
header_widget = QtWidgets.QWidget(col_widget)
|
||||||
|
|
||||||
folder_filter_input = PlaceholderLineEdit(header_widget)
|
filters_widget = FoldersFiltersWidget(header_widget)
|
||||||
folder_filter_input.setPlaceholderText("Filter folders..")
|
|
||||||
|
|
||||||
go_to_current_btn = GoToCurrentButton(header_widget)
|
go_to_current_btn = GoToCurrentButton(header_widget)
|
||||||
refresh_btn = RefreshButton(header_widget)
|
refresh_btn = RefreshButton(header_widget)
|
||||||
|
|
||||||
|
header_layout = QtWidgets.QHBoxLayout(header_widget)
|
||||||
|
header_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
header_layout.addWidget(filters_widget, 1)
|
||||||
|
header_layout.addWidget(go_to_current_btn, 0)
|
||||||
|
header_layout.addWidget(refresh_btn, 0)
|
||||||
|
|
||||||
folder_widget = FoldersWidget(
|
folder_widget = FoldersWidget(
|
||||||
controller, col_widget, handle_expected_selection=True
|
controller, col_widget, handle_expected_selection=True
|
||||||
)
|
)
|
||||||
|
|
||||||
my_tasks_tooltip = (
|
|
||||||
"Filter folders and task to only those you are assigned to."
|
|
||||||
)
|
|
||||||
|
|
||||||
my_tasks_label = QtWidgets.QLabel("My tasks")
|
|
||||||
my_tasks_label.setToolTip(my_tasks_tooltip)
|
|
||||||
|
|
||||||
my_tasks_checkbox = NiceCheckbox(folder_widget)
|
|
||||||
my_tasks_checkbox.setChecked(False)
|
|
||||||
my_tasks_checkbox.setToolTip(my_tasks_tooltip)
|
|
||||||
|
|
||||||
header_layout = QtWidgets.QHBoxLayout(header_widget)
|
|
||||||
header_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
header_layout.addWidget(folder_filter_input, 1)
|
|
||||||
header_layout.addWidget(go_to_current_btn, 0)
|
|
||||||
header_layout.addWidget(refresh_btn, 0)
|
|
||||||
header_layout.addWidget(my_tasks_label, 0)
|
|
||||||
header_layout.addWidget(my_tasks_checkbox, 0)
|
|
||||||
|
|
||||||
col_layout = QtWidgets.QVBoxLayout(col_widget)
|
col_layout = QtWidgets.QVBoxLayout(col_widget)
|
||||||
col_layout.setContentsMargins(0, 0, 0, 0)
|
col_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
col_layout.addWidget(header_widget, 0)
|
col_layout.addWidget(header_widget, 0)
|
||||||
col_layout.addWidget(folder_widget, 1)
|
col_layout.addWidget(folder_widget, 1)
|
||||||
|
|
||||||
folder_filter_input.textChanged.connect(self._on_folder_filter_change)
|
filters_widget.text_changed.connect(self._on_folder_filter_change)
|
||||||
go_to_current_btn.clicked.connect(self._on_go_to_current_clicked)
|
filters_widget.my_tasks_changed.connect(
|
||||||
refresh_btn.clicked.connect(self._on_refresh_clicked)
|
|
||||||
my_tasks_checkbox.stateChanged.connect(
|
|
||||||
self._on_my_tasks_checkbox_state_changed
|
self._on_my_tasks_checkbox_state_changed
|
||||||
)
|
)
|
||||||
|
go_to_current_btn.clicked.connect(self._on_go_to_current_clicked)
|
||||||
|
refresh_btn.clicked.connect(self._on_refresh_clicked)
|
||||||
|
|
||||||
self._folder_filter_input = folder_filter_input
|
|
||||||
self._folders_widget = folder_widget
|
self._folders_widget = folder_widget
|
||||||
|
|
||||||
return col_widget
|
return col_widget
|
||||||
|
|
@ -403,11 +386,10 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
|
||||||
else:
|
else:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def _on_my_tasks_checkbox_state_changed(self, state):
|
def _on_my_tasks_checkbox_state_changed(self, enabled: bool) -> None:
|
||||||
folder_ids = None
|
folder_ids = None
|
||||||
task_ids = None
|
task_ids = None
|
||||||
state = checkstate_int_to_enum(state)
|
if enabled:
|
||||||
if state == QtCore.Qt.Checked:
|
|
||||||
entity_ids = self._controller.get_my_tasks_entity_ids(
|
entity_ids = self._controller.get_my_tasks_entity_ids(
|
||||||
self._project_name
|
self._project_name
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Package declaring AYON addon 'core' version."""
|
"""Package declaring AYON addon 'core' version."""
|
||||||
__version__ = "1.6.7+dev"
|
__version__ = "1.6.9+dev"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
name = "core"
|
name = "core"
|
||||||
title = "Core"
|
title = "Core"
|
||||||
version = "1.6.7+dev"
|
version = "1.6.9+dev"
|
||||||
|
|
||||||
client_dir = "ayon_core"
|
client_dir = "ayon_core"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "ayon-core"
|
name = "ayon-core"
|
||||||
version = "1.6.7+dev"
|
version = "1.6.9+dev"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Ynput Team <team@ynput.io>"]
|
authors = ["Ynput Team <team@ynput.io>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
|
||||||
158
tests/client/ayon_core/lib/test_transcoding.py
Normal file
158
tests/client/ayon_core/lib/test_transcoding.py
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from ayon_core.lib.transcoding import (
|
||||||
|
get_review_info_by_layer_name
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GetReviewInfoByLayerName(unittest.TestCase):
|
||||||
|
"""Test responses from `get_review_info_by_layer_name`"""
|
||||||
|
def test_rgba_channels(self):
|
||||||
|
|
||||||
|
# RGB is supported
|
||||||
|
info = get_review_info_by_layer_name(["R", "G", "B"])
|
||||||
|
self.assertEqual(info, [{
|
||||||
|
"name": "",
|
||||||
|
"review_channels": {
|
||||||
|
"R": "R",
|
||||||
|
"G": "G",
|
||||||
|
"B": "B",
|
||||||
|
"A": None,
|
||||||
|
}
|
||||||
|
}])
|
||||||
|
|
||||||
|
# rgb is supported
|
||||||
|
info = get_review_info_by_layer_name(["r", "g", "b"])
|
||||||
|
self.assertEqual(info, [{
|
||||||
|
"name": "",
|
||||||
|
"review_channels": {
|
||||||
|
"R": "r",
|
||||||
|
"G": "g",
|
||||||
|
"B": "b",
|
||||||
|
"A": None,
|
||||||
|
}
|
||||||
|
}])
|
||||||
|
|
||||||
|
# diffuse.[RGB] is supported
|
||||||
|
info = get_review_info_by_layer_name(
|
||||||
|
["diffuse.R", "diffuse.G", "diffuse.B"]
|
||||||
|
)
|
||||||
|
self.assertEqual(info, [{
|
||||||
|
"name": "diffuse",
|
||||||
|
"review_channels": {
|
||||||
|
"R": "diffuse.R",
|
||||||
|
"G": "diffuse.G",
|
||||||
|
"B": "diffuse.B",
|
||||||
|
"A": None,
|
||||||
|
}
|
||||||
|
}])
|
||||||
|
|
||||||
|
info = get_review_info_by_layer_name(["R", "G", "B", "A"])
|
||||||
|
self.assertEqual(info, [{
|
||||||
|
"name": "",
|
||||||
|
"review_channels": {
|
||||||
|
"R": "R",
|
||||||
|
"G": "G",
|
||||||
|
"B": "B",
|
||||||
|
"A": "A",
|
||||||
|
}
|
||||||
|
}])
|
||||||
|
|
||||||
|
def test_z_channel(self):
|
||||||
|
|
||||||
|
info = get_review_info_by_layer_name(["Z"])
|
||||||
|
self.assertEqual(info, [{
|
||||||
|
"name": "",
|
||||||
|
"review_channels": {
|
||||||
|
"R": "Z",
|
||||||
|
"G": "Z",
|
||||||
|
"B": "Z",
|
||||||
|
"A": None,
|
||||||
|
}
|
||||||
|
}])
|
||||||
|
|
||||||
|
info = get_review_info_by_layer_name(["Z", "A"])
|
||||||
|
self.assertEqual(info, [{
|
||||||
|
"name": "",
|
||||||
|
"review_channels": {
|
||||||
|
"R": "Z",
|
||||||
|
"G": "Z",
|
||||||
|
"B": "Z",
|
||||||
|
"A": "A",
|
||||||
|
}
|
||||||
|
}])
|
||||||
|
|
||||||
|
def test_ar_ag_ab_channels(self):
|
||||||
|
|
||||||
|
info = get_review_info_by_layer_name(["AR", "AG", "AB"])
|
||||||
|
self.assertEqual(info, [{
|
||||||
|
"name": "",
|
||||||
|
"review_channels": {
|
||||||
|
"R": "AR",
|
||||||
|
"G": "AG",
|
||||||
|
"B": "AB",
|
||||||
|
"A": None,
|
||||||
|
}
|
||||||
|
}])
|
||||||
|
|
||||||
|
info = get_review_info_by_layer_name(["AR", "AG", "AB", "A"])
|
||||||
|
self.assertEqual(info, [{
|
||||||
|
"name": "",
|
||||||
|
"review_channels": {
|
||||||
|
"R": "AR",
|
||||||
|
"G": "AG",
|
||||||
|
"B": "AB",
|
||||||
|
"A": "A",
|
||||||
|
}
|
||||||
|
}])
|
||||||
|
|
||||||
|
def test_unknown_channels(self):
|
||||||
|
info = get_review_info_by_layer_name(["hello", "world"])
|
||||||
|
self.assertEqual(info, [])
|
||||||
|
|
||||||
|
def test_rgba_priority(self):
|
||||||
|
"""Ensure main layer, and RGB channels are prioritized
|
||||||
|
|
||||||
|
If both Z and RGB channels are present for a layer name, then RGB
|
||||||
|
should be prioritized and the Z channel should be ignored.
|
||||||
|
|
||||||
|
Also, the alpha channel from another "layer name" is not used. Note
|
||||||
|
how the diffuse response does not take A channel from the main layer.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
info = get_review_info_by_layer_name([
|
||||||
|
"Z",
|
||||||
|
"diffuse.R", "diffuse.G", "diffuse.B",
|
||||||
|
"R", "G", "B", "A",
|
||||||
|
"specular.R", "specular.G", "specular.B", "specular.A",
|
||||||
|
])
|
||||||
|
self.assertEqual(info, [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"review_channels": {
|
||||||
|
"R": "R",
|
||||||
|
"G": "G",
|
||||||
|
"B": "B",
|
||||||
|
"A": "A",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "diffuse",
|
||||||
|
"review_channels": {
|
||||||
|
"R": "diffuse.R",
|
||||||
|
"G": "diffuse.G",
|
||||||
|
"B": "diffuse.B",
|
||||||
|
"A": None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "specular",
|
||||||
|
"review_channels": {
|
||||||
|
"R": "specular.R",
|
||||||
|
"G": "specular.G",
|
||||||
|
"B": "specular.B",
|
||||||
|
"A": "specular.A",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
Loading…
Add table
Add a link
Reference in a new issue