mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge branch 'develop' into feature/ci-mkdocs
This commit is contained in:
commit
2f64832491
84 changed files with 3752 additions and 3288 deletions
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -35,6 +35,10 @@ body:
|
|||
label: Version
|
||||
description: What version are you running? Look to AYON Tray
|
||||
options:
|
||||
- 1.6.0
|
||||
- 1.5.3
|
||||
- 1.5.2
|
||||
- 1.5.1
|
||||
- 1.5.0
|
||||
- 1.4.1
|
||||
- 1.4.0
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import inspect
|
|||
import logging
|
||||
import threading
|
||||
import collections
|
||||
import warnings
|
||||
from uuid import uuid4
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
|
|
@ -155,18 +156,33 @@ def load_addons(force=False):
|
|||
|
||||
|
||||
def _get_ayon_bundle_data():
|
||||
studio_bundle_name = os.environ.get("AYON_STUDIO_BUNDLE_NAME")
|
||||
project_bundle_name = os.getenv("AYON_BUNDLE_NAME")
|
||||
bundles = ayon_api.get_bundles()["bundles"]
|
||||
|
||||
bundle_name = os.getenv("AYON_BUNDLE_NAME")
|
||||
|
||||
return next(
|
||||
project_bundle = next(
|
||||
(
|
||||
bundle
|
||||
for bundle in bundles
|
||||
if bundle["name"] == bundle_name
|
||||
if bundle["name"] == project_bundle_name
|
||||
),
|
||||
None
|
||||
)
|
||||
studio_bundle = None
|
||||
if studio_bundle_name and project_bundle_name != studio_bundle_name:
|
||||
studio_bundle = next(
|
||||
(
|
||||
bundle
|
||||
for bundle in bundles
|
||||
if bundle["name"] == studio_bundle_name
|
||||
),
|
||||
None
|
||||
)
|
||||
|
||||
if project_bundle and studio_bundle:
|
||||
addons = copy.deepcopy(studio_bundle["addons"])
|
||||
addons.update(project_bundle["addons"])
|
||||
project_bundle["addons"] = addons
|
||||
return project_bundle
|
||||
|
||||
|
||||
def _get_ayon_addons_information(bundle_info):
|
||||
|
|
@ -286,6 +302,11 @@ def _load_ayon_addons(log):
|
|||
milestone_version = MOVED_ADDON_MILESTONE_VERSIONS.get(addon_name)
|
||||
if use_dev_path:
|
||||
addon_dir = dev_addon_info["path"]
|
||||
if addon_dir:
|
||||
addon_dir = os.path.expandvars(
|
||||
addon_dir.format_map(os.environ)
|
||||
)
|
||||
|
||||
if not addon_dir or not os.path.exists(addon_dir):
|
||||
log.warning((
|
||||
"Dev addon {} {} path does not exists. Path \"{}\""
|
||||
|
|
@ -815,10 +836,26 @@ class AddonsManager:
|
|||
|
||||
Unknown keys are logged out.
|
||||
|
||||
Deprecated:
|
||||
Use targeted methods 'collect_launcher_action_paths',
|
||||
'collect_create_plugin_paths', 'collect_load_plugin_paths',
|
||||
'collect_publish_plugin_paths' and
|
||||
'collect_inventory_action_paths' to collect plugin paths.
|
||||
|
||||
Returns:
|
||||
dict: Output is dictionary with keys "publish", "create", "load",
|
||||
"actions" and "inventory" each containing list of paths.
|
||||
|
||||
"""
|
||||
warnings.warn(
|
||||
"Used deprecated method 'collect_plugin_paths'. Please use"
|
||||
" targeted methods 'collect_launcher_action_paths',"
|
||||
" 'collect_create_plugin_paths', 'collect_load_plugin_paths'"
|
||||
" 'collect_publish_plugin_paths' and"
|
||||
" 'collect_inventory_action_paths'",
|
||||
DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
# Output structure
|
||||
output = {
|
||||
"publish": [],
|
||||
|
|
@ -874,24 +911,28 @@ class AddonsManager:
|
|||
if not isinstance(addon, IPluginPaths):
|
||||
continue
|
||||
|
||||
paths = None
|
||||
method = getattr(addon, method_name)
|
||||
try:
|
||||
paths = method(*args, **kwargs)
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
(
|
||||
"Failed to get plugin paths from addon"
|
||||
" '{}' using '{}'."
|
||||
).format(addon.__class__.__name__, method_name),
|
||||
"Failed to get plugin paths from addon"
|
||||
f" '{addon.name}' using '{method_name}'.",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
if not paths:
|
||||
continue
|
||||
|
||||
if paths:
|
||||
# Convert to list if value is not list
|
||||
if not isinstance(paths, (list, tuple, set)):
|
||||
paths = [paths]
|
||||
output.extend(paths)
|
||||
if isinstance(paths, str):
|
||||
paths = [paths]
|
||||
self.log.warning(
|
||||
f"Addon '{addon.name}' returned invalid output type"
|
||||
f" from '{method_name}'."
|
||||
f" Got 'str' expected 'list[str]'."
|
||||
)
|
||||
output.extend(paths)
|
||||
return output
|
||||
|
||||
def collect_launcher_action_paths(self):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""Addon interfaces for AYON."""
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from typing import TYPE_CHECKING, Callable, Optional, Type
|
||||
|
||||
|
|
@ -39,26 +40,29 @@ class AYONInterface(metaclass=_AYONInterfaceMeta):
|
|||
|
||||
|
||||
class IPluginPaths(AYONInterface):
|
||||
"""Addon has plugin paths to return.
|
||||
"""Addon wants to register plugin paths."""
|
||||
|
||||
Expected result is dictionary with keys "publish", "create", "load",
|
||||
"actions" or "inventory" and values as list or string.
|
||||
{
|
||||
"publish": ["path/to/publish_plugins"]
|
||||
}
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_plugin_paths(self) -> dict[str, list[str]]:
|
||||
"""Return plugin paths for addon.
|
||||
|
||||
This method was abstract (required) in the past, so raise the required
|
||||
'core' addon version when 'get_plugin_paths' is removed from
|
||||
addon.
|
||||
|
||||
Deprecated:
|
||||
Please implement specific methods 'get_create_plugin_paths',
|
||||
'get_load_plugin_paths', 'get_inventory_action_paths' and
|
||||
'get_publish_plugin_paths' to return plugin paths.
|
||||
|
||||
Returns:
|
||||
dict[str, list[str]]: Plugin paths for addon.
|
||||
|
||||
"""
|
||||
return {}
|
||||
|
||||
def _get_plugin_paths_by_type(
|
||||
self, plugin_type: str) -> list[str]:
|
||||
self, plugin_type: str
|
||||
) -> list[str]:
|
||||
"""Get plugin paths by type.
|
||||
|
||||
Args:
|
||||
|
|
@ -78,6 +82,24 @@ class IPluginPaths(AYONInterface):
|
|||
|
||||
if not isinstance(paths, (list, tuple, set)):
|
||||
paths = [paths]
|
||||
|
||||
new_function_name = "get_launcher_action_paths"
|
||||
if plugin_type == "create":
|
||||
new_function_name = "get_create_plugin_paths"
|
||||
elif plugin_type == "load":
|
||||
new_function_name = "get_load_plugin_paths"
|
||||
elif plugin_type == "publish":
|
||||
new_function_name = "get_publish_plugin_paths"
|
||||
elif plugin_type == "inventory":
|
||||
new_function_name = "get_inventory_action_paths"
|
||||
|
||||
warnings.warn(
|
||||
f"Addon '{self.name}' returns '{plugin_type}' paths using"
|
||||
" 'get_plugin_paths' method. Please implement"
|
||||
f" '{new_function_name}' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
return paths
|
||||
|
||||
def get_launcher_action_paths(self) -> list[str]:
|
||||
|
|
|
|||
|
|
@ -27,25 +27,40 @@ from ayon_core.lib.env_tools import (
|
|||
|
||||
@click.group(invoke_without_command=True)
|
||||
@click.pass_context
|
||||
@click.option("--use-staging", is_flag=True,
|
||||
expose_value=False, help="use staging variants")
|
||||
@click.option("--debug", is_flag=True, expose_value=False,
|
||||
help="Enable debug")
|
||||
@click.option("--verbose", expose_value=False,
|
||||
help=("Change AYON log level (debug - critical or 0-50)"))
|
||||
@click.option("--force", is_flag=True, hidden=True)
|
||||
def main_cli(ctx, force):
|
||||
@click.option(
|
||||
"--use-staging",
|
||||
is_flag=True,
|
||||
expose_value=False,
|
||||
help="use staging variants")
|
||||
@click.option(
|
||||
"--debug",
|
||||
is_flag=True,
|
||||
expose_value=False,
|
||||
help="Enable debug")
|
||||
@click.option(
|
||||
"--project",
|
||||
help="Project name")
|
||||
@click.option(
|
||||
"--verbose",
|
||||
expose_value=False,
|
||||
help="Change AYON log level (debug - critical or 0-50)")
|
||||
@click.option(
|
||||
"--use-dev",
|
||||
is_flag=True,
|
||||
expose_value=False,
|
||||
help="use dev bundle")
|
||||
def main_cli(ctx, *_args, **_kwargs):
|
||||
"""AYON is main command serving as entry point to pipeline system.
|
||||
|
||||
It wraps different commands together.
|
||||
"""
|
||||
|
||||
if ctx.invoked_subcommand is None:
|
||||
# Print help if headless mode is used
|
||||
if os.getenv("AYON_HEADLESS_MODE") == "1":
|
||||
print(ctx.get_help())
|
||||
sys.exit(0)
|
||||
else:
|
||||
ctx.params.pop("project")
|
||||
ctx.forward(tray)
|
||||
|
||||
|
||||
|
|
@ -60,7 +75,6 @@ def tray(force):
|
|||
Default action of AYON command is to launch tray widget to control basic
|
||||
aspects of AYON. See documentation for more information.
|
||||
"""
|
||||
|
||||
from ayon_core.tools.tray import main
|
||||
|
||||
main(force)
|
||||
|
|
@ -306,6 +320,43 @@ def _add_addons(addons_manager):
|
|||
)
|
||||
|
||||
|
||||
def _cleanup_project_args():
|
||||
rem_args = list(sys.argv[1:])
|
||||
if "--project" not in rem_args:
|
||||
return
|
||||
|
||||
cmd = None
|
||||
current_ctx = None
|
||||
parent_name = "ayon"
|
||||
parent_cmd = main_cli
|
||||
while hasattr(parent_cmd, "resolve_command"):
|
||||
if current_ctx is None:
|
||||
current_ctx = main_cli.make_context(parent_name, rem_args)
|
||||
else:
|
||||
current_ctx = parent_cmd.make_context(
|
||||
parent_name,
|
||||
rem_args,
|
||||
parent=current_ctx
|
||||
)
|
||||
if not rem_args:
|
||||
break
|
||||
cmd_name, cmd, rem_args = parent_cmd.resolve_command(
|
||||
current_ctx, rem_args
|
||||
)
|
||||
parent_name = cmd_name
|
||||
parent_cmd = cmd
|
||||
|
||||
if cmd is None:
|
||||
return
|
||||
|
||||
param_names = {param.name for param in cmd.params}
|
||||
if "project" in param_names:
|
||||
return
|
||||
idx = sys.argv.index("--project")
|
||||
sys.argv.pop(idx)
|
||||
sys.argv.pop(idx)
|
||||
|
||||
|
||||
def main(*args, **kwargs):
|
||||
logging.basicConfig()
|
||||
|
||||
|
|
@ -332,10 +383,14 @@ def main(*args, **kwargs):
|
|||
addons_manager = AddonsManager()
|
||||
_set_addons_environments(addons_manager)
|
||||
_add_addons(addons_manager)
|
||||
|
||||
_cleanup_project_args()
|
||||
|
||||
try:
|
||||
main_cli(
|
||||
prog_name="ayon",
|
||||
obj={"addons_manager": addons_manager},
|
||||
args=(sys.argv[1:]),
|
||||
)
|
||||
except Exception: # noqa
|
||||
exc_info = sys.exc_info()
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class OCIOEnvHook(PreLaunchHook):
|
|||
"fusion",
|
||||
"blender",
|
||||
"aftereffects",
|
||||
"3dsmax",
|
||||
"max",
|
||||
"houdini",
|
||||
"maya",
|
||||
"nuke",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
from .constants import ContextChangeReason
|
||||
from .abstract import AbstractHost
|
||||
from .host import (
|
||||
HostBase,
|
||||
ContextChangeData,
|
||||
)
|
||||
|
||||
from .interfaces import (
|
||||
|
|
@ -18,7 +20,10 @@ from .dirmap import HostDirmap
|
|||
__all__ = (
|
||||
"ContextChangeReason",
|
||||
|
||||
"AbstractHost",
|
||||
|
||||
"HostBase",
|
||||
"ContextChangeData",
|
||||
|
||||
"IWorkfileHost",
|
||||
"WorkfileInfo",
|
||||
|
|
|
|||
96
client/ayon_core/host/abstract.py
Normal file
96
client/ayon_core/host/abstract.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
import typing
|
||||
from typing import Optional, Any
|
||||
|
||||
from .constants import ContextChangeReason
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from ayon_core.pipeline import Anatomy
|
||||
|
||||
from .typing import HostContextData
|
||||
|
||||
|
||||
class AbstractHost(ABC):
|
||||
"""Abstract definition of host implementation."""
|
||||
@property
|
||||
@abstractmethod
|
||||
def log(self) -> logging.Logger:
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self) -> str:
|
||||
"""Host name."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_current_context(self) -> HostContextData:
|
||||
"""Get the current context of the host.
|
||||
|
||||
Current context is defined by project name, folder path and task name.
|
||||
|
||||
Returns:
|
||||
HostContextData: The current context of the host.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_current_context(
|
||||
self,
|
||||
folder_entity: dict[str, Any],
|
||||
task_entity: dict[str, Any],
|
||||
*,
|
||||
reason: ContextChangeReason = ContextChangeReason.undefined,
|
||||
project_entity: Optional[dict[str, Any]] = None,
|
||||
anatomy: Optional[Anatomy] = None,
|
||||
) -> HostContextData:
|
||||
"""Change context of the host.
|
||||
|
||||
Args:
|
||||
folder_entity (dict[str, Any]): Folder entity.
|
||||
task_entity (dict[str, Any]): Task entity.
|
||||
reason (ContextChangeReason): Reason for change.
|
||||
project_entity (dict[str, Any]): Project entity.
|
||||
anatomy (Anatomy): Anatomy entity.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_current_project_name(self) -> str:
|
||||
"""Get the current project name.
|
||||
|
||||
Returns:
|
||||
Optional[str]: The current project name.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_current_folder_path(self) -> Optional[str]:
|
||||
"""Get the current folder path.
|
||||
|
||||
Returns:
|
||||
Optional[str]: The current folder path.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_current_task_name(self) -> Optional[str]:
|
||||
"""Get the current task name.
|
||||
|
||||
Returns:
|
||||
Optional[str]: The current task name.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_context_title(self) -> str:
|
||||
"""Get the context title used in UIs."""
|
||||
pass
|
||||
|
|
@ -3,26 +3,21 @@ from __future__ import annotations
|
|||
import os
|
||||
import logging
|
||||
import contextlib
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
import typing
|
||||
from typing import Optional, Any
|
||||
from dataclasses import dataclass
|
||||
|
||||
import ayon_api
|
||||
|
||||
from ayon_core.lib import emit_event
|
||||
|
||||
from .constants import ContextChangeReason
|
||||
from .abstract import AbstractHost
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from ayon_core.pipeline import Anatomy
|
||||
|
||||
from typing import TypedDict
|
||||
|
||||
class HostContextData(TypedDict):
|
||||
project_name: str
|
||||
folder_path: Optional[str]
|
||||
task_name: Optional[str]
|
||||
from .typing import HostContextData
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -34,7 +29,7 @@ class ContextChangeData:
|
|||
anatomy: Anatomy
|
||||
|
||||
|
||||
class HostBase(ABC):
|
||||
class HostBase(AbstractHost):
|
||||
"""Base of host implementation class.
|
||||
|
||||
Host is pipeline implementation of DCC application. This class should help
|
||||
|
|
@ -109,48 +104,41 @@ class HostBase(ABC):
|
|||
|
||||
It is called automatically when 'ayon_core.pipeline.install_host' is
|
||||
triggered.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
def log(self) -> logging.Logger:
|
||||
if self._log is None:
|
||||
self._log = logging.getLogger(self.__class__.__name__)
|
||||
return self._log
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self) -> str:
|
||||
"""Host name."""
|
||||
|
||||
pass
|
||||
|
||||
def get_current_project_name(self):
|
||||
def get_current_project_name(self) -> str:
|
||||
"""
|
||||
Returns:
|
||||
Union[str, None]: Current project name.
|
||||
"""
|
||||
str: Current project name.
|
||||
|
||||
return os.environ.get("AYON_PROJECT_NAME")
|
||||
"""
|
||||
return os.environ["AYON_PROJECT_NAME"]
|
||||
|
||||
def get_current_folder_path(self) -> Optional[str]:
|
||||
"""
|
||||
Returns:
|
||||
Union[str, None]: Current asset name.
|
||||
"""
|
||||
Optional[str]: Current asset name.
|
||||
|
||||
"""
|
||||
return os.environ.get("AYON_FOLDER_PATH")
|
||||
|
||||
def get_current_task_name(self) -> Optional[str]:
|
||||
"""
|
||||
Returns:
|
||||
Union[str, None]: Current task name.
|
||||
"""
|
||||
Optional[str]: Current task name.
|
||||
|
||||
"""
|
||||
return os.environ.get("AYON_TASK_NAME")
|
||||
|
||||
def get_current_context(self) -> "HostContextData":
|
||||
def get_current_context(self) -> HostContextData:
|
||||
"""Get current context information.
|
||||
|
||||
This method should be used to get current context of host. Usage of
|
||||
|
|
@ -159,10 +147,10 @@ class HostBase(ABC):
|
|||
can't be caught properly.
|
||||
|
||||
Returns:
|
||||
Dict[str, Union[str, None]]: Context with 3 keys 'project_name',
|
||||
'folder_path' and 'task_name'. All of them can be 'None'.
|
||||
"""
|
||||
HostContextData: Current context with 'project_name',
|
||||
'folder_path' and 'task_name'.
|
||||
|
||||
"""
|
||||
return {
|
||||
"project_name": self.get_current_project_name(),
|
||||
"folder_path": self.get_current_folder_path(),
|
||||
|
|
@ -177,7 +165,7 @@ class HostBase(ABC):
|
|||
reason: ContextChangeReason = ContextChangeReason.undefined,
|
||||
project_entity: Optional[dict[str, Any]] = None,
|
||||
anatomy: Optional[Anatomy] = None,
|
||||
) -> "HostContextData":
|
||||
) -> HostContextData:
|
||||
"""Set current context information.
|
||||
|
||||
This method should be used to set current context of host. Usage of
|
||||
|
|
@ -290,7 +278,7 @@ class HostBase(ABC):
|
|||
project_name: str,
|
||||
folder_path: Optional[str],
|
||||
task_name: Optional[str],
|
||||
) -> "HostContextData":
|
||||
) -> HostContextData:
|
||||
"""Emit context change event.
|
||||
|
||||
Args:
|
||||
|
|
@ -302,7 +290,7 @@ class HostBase(ABC):
|
|||
HostContextData: Data send to context change event.
|
||||
|
||||
"""
|
||||
data = {
|
||||
data: HostContextData = {
|
||||
"project_name": project_name,
|
||||
"folder_path": folder_path,
|
||||
"task_name": task_name,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
from abc import abstractmethod
|
||||
|
||||
from ayon_core.host.abstract import AbstractHost
|
||||
|
||||
from .exceptions import MissingMethodsError
|
||||
|
||||
|
||||
class ILoadHost:
|
||||
class ILoadHost(AbstractHost):
|
||||
"""Implementation requirements to be able use reference of representations.
|
||||
|
||||
The load plugins can do referencing even without implementation of methods
|
||||
|
|
@ -24,7 +26,7 @@ class ILoadHost:
|
|||
loading. Checks only existence of methods.
|
||||
|
||||
Args:
|
||||
Union[ModuleType, HostBase]: Object of host where to look for
|
||||
Union[ModuleType, AbstractHost]: Object of host where to look for
|
||||
required methods.
|
||||
|
||||
Returns:
|
||||
|
|
@ -46,7 +48,7 @@ class ILoadHost:
|
|||
"""Validate implemented methods of "old type" host for load workflow.
|
||||
|
||||
Args:
|
||||
Union[ModuleType, HostBase]: Object of host to validate.
|
||||
Union[ModuleType, AbstractHost]: Object of host to validate.
|
||||
|
||||
Raises:
|
||||
MissingMethodsError: If there are missing methods on host
|
||||
|
|
@ -83,7 +85,7 @@ class ILoadHost:
|
|||
return self.get_containers()
|
||||
|
||||
|
||||
class IPublishHost:
|
||||
class IPublishHost(AbstractHost):
|
||||
"""Functions related to new creation system in new publisher.
|
||||
|
||||
New publisher is not storing information only about each created instance
|
||||
|
|
@ -99,7 +101,7 @@ class IPublishHost:
|
|||
new publish creation. Checks only existence of methods.
|
||||
|
||||
Args:
|
||||
Union[ModuleType, HostBase]: Host module where to look for
|
||||
Union[ModuleType, AbstractHost]: Host module where to look for
|
||||
required methods.
|
||||
|
||||
Returns:
|
||||
|
|
@ -127,7 +129,7 @@ class IPublishHost:
|
|||
"""Validate implemented methods of "old type" host.
|
||||
|
||||
Args:
|
||||
Union[ModuleType, HostBase]: Host module to validate.
|
||||
Union[ModuleType, AbstractHost]: Host module to validate.
|
||||
|
||||
Raises:
|
||||
MissingMethodsError: If there are missing methods on host
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import arrow
|
|||
|
||||
from ayon_core.lib import emit_event
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.host.abstract import AbstractHost
|
||||
from ayon_core.host.constants import ContextChangeReason
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
|
|
@ -821,7 +822,7 @@ class PublishedWorkfileInfo:
|
|||
return PublishedWorkfileInfo(**data)
|
||||
|
||||
|
||||
class IWorkfileHost:
|
||||
class IWorkfileHost(AbstractHost):
|
||||
"""Implementation requirements to be able to use workfiles utils and tool.
|
||||
|
||||
Some of the methods are pre-implemented as they generally do the same in
|
||||
|
|
@ -944,6 +945,8 @@ class IWorkfileHost:
|
|||
self._emit_workfile_save_event(event_data, after_save=False)
|
||||
|
||||
workdir = os.path.dirname(filepath)
|
||||
if not os.path.exists(workdir):
|
||||
os.makedirs(workdir, exist_ok=True)
|
||||
|
||||
# Set 'AYON_WORKDIR' environment variable
|
||||
os.environ["AYON_WORKDIR"] = workdir
|
||||
|
|
@ -1072,10 +1075,13 @@ class IWorkfileHost:
|
|||
prepared_data=prepared_data,
|
||||
)
|
||||
|
||||
workfile_entities_by_path = {
|
||||
workfile_entity["path"]: workfile_entity
|
||||
for workfile_entity in list_workfiles_context.workfile_entities
|
||||
}
|
||||
workfile_entities_by_path = {}
|
||||
for workfile_entity in list_workfiles_context.workfile_entities:
|
||||
rootless_path = workfile_entity["path"]
|
||||
path = os.path.normpath(
|
||||
list_workfiles_context.anatomy.fill_root(rootless_path)
|
||||
)
|
||||
workfile_entities_by_path[path] = workfile_entity
|
||||
|
||||
workdir_data = get_template_data(
|
||||
list_workfiles_context.project_entity,
|
||||
|
|
@ -1114,10 +1120,10 @@ class IWorkfileHost:
|
|||
|
||||
rootless_path = f"{rootless_workdir}/{filename}"
|
||||
workfile_entity = workfile_entities_by_path.pop(
|
||||
rootless_path, None
|
||||
filepath, None
|
||||
)
|
||||
version = comment = None
|
||||
if workfile_entity:
|
||||
if workfile_entity is not None:
|
||||
_data = workfile_entity["data"]
|
||||
version = _data.get("version")
|
||||
comment = _data.get("comment")
|
||||
|
|
@ -1137,7 +1143,7 @@ class IWorkfileHost:
|
|||
)
|
||||
items.append(item)
|
||||
|
||||
for workfile_entity in workfile_entities_by_path.values():
|
||||
for filepath, workfile_entity in workfile_entities_by_path.items():
|
||||
# Workfile entity is not in the filesystem
|
||||
# but it is in the database
|
||||
rootless_path = workfile_entity["path"]
|
||||
|
|
@ -1154,13 +1160,13 @@ class IWorkfileHost:
|
|||
version = parsed_data.version
|
||||
comment = parsed_data.comment
|
||||
|
||||
filepath = list_workfiles_context.anatomy.fill_root(rootless_path)
|
||||
available = os.path.exists(filepath)
|
||||
items.append(WorkfileInfo.new(
|
||||
filepath,
|
||||
rootless_path,
|
||||
version=version,
|
||||
comment=comment,
|
||||
available=False,
|
||||
available=available,
|
||||
workfile_entity=workfile_entity,
|
||||
))
|
||||
|
||||
|
|
|
|||
7
client/ayon_core/host/typing.py
Normal file
7
client/ayon_core/host/typing.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from typing import Optional, TypedDict
|
||||
|
||||
|
||||
class HostContextData(TypedDict):
|
||||
project_name: str
|
||||
folder_path: Optional[str]
|
||||
task_name: Optional[str]
|
||||
|
|
@ -8,6 +8,7 @@ import warnings
|
|||
from datetime import datetime
|
||||
from abc import ABC, abstractmethod
|
||||
from functools import lru_cache
|
||||
from typing import Optional, Any
|
||||
|
||||
import platformdirs
|
||||
import ayon_api
|
||||
|
|
@ -15,26 +16,31 @@ import ayon_api
|
|||
_PLACEHOLDER = object()
|
||||
|
||||
|
||||
# TODO should use 'KeyError' or 'Exception' as base
|
||||
class RegistryItemNotFound(ValueError):
|
||||
"""Raised when the item is not found in the keyring."""
|
||||
|
||||
|
||||
class _Cache:
|
||||
username = None
|
||||
|
||||
|
||||
def _get_ayon_appdirs(*args):
|
||||
def _get_ayon_appdirs(*args: str) -> str:
|
||||
return os.path.join(
|
||||
platformdirs.user_data_dir("AYON", "Ynput"),
|
||||
*args
|
||||
)
|
||||
|
||||
|
||||
def get_ayon_appdirs(*args):
|
||||
def get_ayon_appdirs(*args: str) -> str:
|
||||
"""Local app data directory of AYON client.
|
||||
|
||||
Deprecated:
|
||||
Use 'get_launcher_local_dir' or 'get_launcher_storage_dir' based on
|
||||
use-case. Deprecation added 24/08/09 (0.4.4-dev.1).
|
||||
a use-case. Deprecation added 24/08/09 (0.4.4-dev.1).
|
||||
|
||||
Args:
|
||||
*args (Iterable[str]): Subdirectories/files in local app data dir.
|
||||
*args (Iterable[str]): Subdirectories/files in the local app data dir.
|
||||
|
||||
Returns:
|
||||
str: Path to directory/file in local app data dir.
|
||||
|
|
@ -52,7 +58,7 @@ def get_ayon_appdirs(*args):
|
|||
|
||||
|
||||
def get_launcher_storage_dir(*subdirs: str) -> str:
|
||||
"""Get storage directory for launcher.
|
||||
"""Get a storage directory for launcher.
|
||||
|
||||
Storage directory is used for storing shims, addons, dependencies, etc.
|
||||
|
||||
|
|
@ -77,14 +83,14 @@ def get_launcher_storage_dir(*subdirs: str) -> str:
|
|||
|
||||
|
||||
def get_launcher_local_dir(*subdirs: str) -> str:
|
||||
"""Get local directory for launcher.
|
||||
"""Get a local directory for launcher.
|
||||
|
||||
Local directory is used for storing machine or user specific data.
|
||||
Local directory is used for storing machine or user-specific data.
|
||||
|
||||
The location is user specific.
|
||||
The location is user-specific.
|
||||
|
||||
Note:
|
||||
This function should be called at least once on bootstrap.
|
||||
This function should be called at least once on the bootstrap.
|
||||
|
||||
Args:
|
||||
*subdirs (str): Subdirectories relative to local dir.
|
||||
|
|
@ -101,7 +107,7 @@ def get_launcher_local_dir(*subdirs: str) -> str:
|
|||
|
||||
|
||||
def get_addons_resources_dir(addon_name: str, *args) -> str:
|
||||
"""Get directory for storing resources for addons.
|
||||
"""Get a directory for storing resources for addons.
|
||||
|
||||
Some addons might need to store ad-hoc resources that are not part of
|
||||
addon client package (e.g. because of size). Studio might define
|
||||
|
|
@ -111,7 +117,7 @@ def get_addons_resources_dir(addon_name: str, *args) -> str:
|
|||
|
||||
Args:
|
||||
addon_name (str): Addon name.
|
||||
*args (str): Subfolders in resources directory.
|
||||
*args (str): Subfolders in the resources directory.
|
||||
|
||||
Returns:
|
||||
str: Path to resources directory.
|
||||
|
|
@ -124,6 +130,10 @@ def get_addons_resources_dir(addon_name: str, *args) -> str:
|
|||
return os.path.join(addons_resources_dir, addon_name, *args)
|
||||
|
||||
|
||||
class _FakeException(Exception):
|
||||
"""Placeholder exception used if real exception is not available."""
|
||||
|
||||
|
||||
class AYONSecureRegistry:
|
||||
"""Store information using keyring.
|
||||
|
||||
|
|
@ -134,9 +144,10 @@ class AYONSecureRegistry:
|
|||
identify which data were created by AYON.
|
||||
|
||||
Args:
|
||||
name(str): Name of registry used as identifier for data.
|
||||
name(str): Name of registry used as the identifier for data.
|
||||
|
||||
"""
|
||||
def __init__(self, name):
|
||||
def __init__(self, name: str) -> None:
|
||||
try:
|
||||
import keyring
|
||||
|
||||
|
|
@ -152,13 +163,12 @@ class AYONSecureRegistry:
|
|||
keyring.set_keyring(Windows.WinVaultKeyring())
|
||||
|
||||
# Force "AYON" prefix
|
||||
self._name = "/".join(("AYON", name))
|
||||
self._name = f"AYON/{name}"
|
||||
|
||||
def set_item(self, name, value):
|
||||
# type: (str, str) -> None
|
||||
"""Set sensitive item into system's keyring.
|
||||
def set_item(self, name: str, value: str) -> None:
|
||||
"""Set sensitive item into the system's keyring.
|
||||
|
||||
This uses `Keyring module`_ to save sensitive stuff into system's
|
||||
This uses `Keyring module`_ to save sensitive stuff into the system's
|
||||
keyring.
|
||||
|
||||
Args:
|
||||
|
|
@ -172,22 +182,26 @@ class AYONSecureRegistry:
|
|||
import keyring
|
||||
|
||||
keyring.set_password(self._name, name, value)
|
||||
self.get_item.cache_clear()
|
||||
|
||||
@lru_cache(maxsize=32)
|
||||
def get_item(self, name, default=_PLACEHOLDER):
|
||||
"""Get value of sensitive item from system's keyring.
|
||||
def get_item(
|
||||
self, name: str, default: Any = _PLACEHOLDER
|
||||
) -> Optional[str]:
|
||||
"""Get value of sensitive item from the system's keyring.
|
||||
|
||||
See also `Keyring module`_
|
||||
|
||||
Args:
|
||||
name (str): Name of the item.
|
||||
default (Any): Default value if item is not available.
|
||||
default (Any): Default value if the item is not available.
|
||||
|
||||
Returns:
|
||||
value (str): Value of the item.
|
||||
|
||||
Raises:
|
||||
ValueError: If item doesn't exist and default is not defined.
|
||||
RegistryItemNotFound: If the item doesn't exist and default
|
||||
is not defined.
|
||||
|
||||
.. _Keyring module:
|
||||
https://github.com/jaraco/keyring
|
||||
|
|
@ -195,21 +209,29 @@ class AYONSecureRegistry:
|
|||
"""
|
||||
import keyring
|
||||
|
||||
value = keyring.get_password(self._name, name)
|
||||
# Capture 'ItemNotFoundException' exception (on linux)
|
||||
try:
|
||||
from secretstorage.exceptions import ItemNotFoundException
|
||||
except ImportError:
|
||||
ItemNotFoundException = _FakeException
|
||||
|
||||
try:
|
||||
value = keyring.get_password(self._name, name)
|
||||
except ItemNotFoundException:
|
||||
value = None
|
||||
|
||||
if value is not None:
|
||||
return value
|
||||
|
||||
if default is not _PLACEHOLDER:
|
||||
return default
|
||||
|
||||
# NOTE Should raise `KeyError`
|
||||
raise ValueError(
|
||||
"Item {}:{} does not exist in keyring.".format(self._name, name)
|
||||
raise RegistryItemNotFound(
|
||||
f"Item {self._name}:{name} not found in keyring."
|
||||
)
|
||||
|
||||
def delete_item(self, name):
|
||||
# type: (str) -> None
|
||||
"""Delete value stored in system's keyring.
|
||||
def delete_item(self, name: str) -> None:
|
||||
"""Delete value stored in the system's keyring.
|
||||
|
||||
See also `Keyring module`_
|
||||
|
||||
|
|
@ -227,47 +249,38 @@ class AYONSecureRegistry:
|
|||
|
||||
|
||||
class ASettingRegistry(ABC):
|
||||
"""Abstract class defining structure of **SettingRegistry** class.
|
||||
|
||||
It is implementing methods to store secure items into keyring, otherwise
|
||||
mechanism for storing common items must be implemented in abstract
|
||||
methods.
|
||||
|
||||
Attributes:
|
||||
_name (str): Registry names.
|
||||
"""Abstract class to defining structure of registry class.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
# type: (str) -> ASettingRegistry
|
||||
super(ASettingRegistry, self).__init__()
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
self._name = name
|
||||
self._items = {}
|
||||
|
||||
def set_item(self, name, value):
|
||||
# type: (str, str) -> None
|
||||
"""Set item to settings registry.
|
||||
|
||||
Args:
|
||||
name (str): Name of the item.
|
||||
value (str): Value of the item.
|
||||
|
||||
"""
|
||||
self._set_item(name, value)
|
||||
|
||||
@abstractmethod
|
||||
def _set_item(self, name, value):
|
||||
# type: (str, str) -> None
|
||||
# Implement it
|
||||
pass
|
||||
def _get_item(self, name: str) -> Any:
|
||||
"""Get item value from registry."""
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
self._items[name] = value
|
||||
@abstractmethod
|
||||
def _set_item(self, name: str, value: str) -> None:
|
||||
"""Set item value to registry."""
|
||||
|
||||
@abstractmethod
|
||||
def _delete_item(self, name: str) -> None:
|
||||
"""Delete item from registry."""
|
||||
|
||||
def __getitem__(self, name: str) -> Any:
|
||||
return self._get_item(name)
|
||||
|
||||
def __setitem__(self, name: str, value: str) -> None:
|
||||
self._set_item(name, value)
|
||||
|
||||
def get_item(self, name):
|
||||
# type: (str) -> str
|
||||
def __delitem__(self, name: str) -> None:
|
||||
self._delete_item(name)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
def get_item(self, name: str) -> str:
|
||||
"""Get item from settings registry.
|
||||
|
||||
Args:
|
||||
|
|
@ -277,22 +290,22 @@ class ASettingRegistry(ABC):
|
|||
value (str): Value of the item.
|
||||
|
||||
Raises:
|
||||
ValueError: If item doesn't exist.
|
||||
RegistryItemNotFound: If the item doesn't exist.
|
||||
|
||||
"""
|
||||
return self._get_item(name)
|
||||
|
||||
@abstractmethod
|
||||
def _get_item(self, name):
|
||||
# type: (str) -> str
|
||||
# Implement it
|
||||
pass
|
||||
def set_item(self, name: str, value: str) -> None:
|
||||
"""Set item to settings registry.
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self._get_item(name)
|
||||
Args:
|
||||
name (str): Name of the item.
|
||||
value (str): Value of the item.
|
||||
|
||||
def delete_item(self, name):
|
||||
# type: (str) -> None
|
||||
"""
|
||||
self._set_item(name, value)
|
||||
|
||||
def delete_item(self, name: str) -> None:
|
||||
"""Delete item from settings registry.
|
||||
|
||||
Args:
|
||||
|
|
@ -301,16 +314,6 @@ class ASettingRegistry(ABC):
|
|||
"""
|
||||
self._delete_item(name)
|
||||
|
||||
@abstractmethod
|
||||
def _delete_item(self, name):
|
||||
# type: (str) -> None
|
||||
"""Delete item from settings."""
|
||||
pass
|
||||
|
||||
def __delitem__(self, name):
|
||||
del self._items[name]
|
||||
self._delete_item(name)
|
||||
|
||||
|
||||
class IniSettingRegistry(ASettingRegistry):
|
||||
"""Class using :mod:`configparser`.
|
||||
|
|
@ -318,20 +321,17 @@ class IniSettingRegistry(ASettingRegistry):
|
|||
This class is using :mod:`configparser` (ini) files to store items.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name, path):
|
||||
# type: (str, str) -> IniSettingRegistry
|
||||
super(IniSettingRegistry, self).__init__(name)
|
||||
def __init__(self, name: str, path: str) -> None:
|
||||
super().__init__(name)
|
||||
# get registry file
|
||||
self._registry_file = os.path.join(path, "{}.ini".format(name))
|
||||
self._registry_file = os.path.join(path, f"{name}.ini")
|
||||
if not os.path.exists(self._registry_file):
|
||||
with open(self._registry_file, mode="w") as cfg:
|
||||
print("# Settings registry", cfg)
|
||||
now = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
|
||||
print("# {}".format(now), cfg)
|
||||
print(f"# {now}", cfg)
|
||||
|
||||
def set_item_section(self, section, name, value):
|
||||
# type: (str, str, str) -> None
|
||||
def set_item_section(self, section: str, name: str, value: str) -> None:
|
||||
"""Set item to specific section of ini registry.
|
||||
|
||||
If section doesn't exists, it is created.
|
||||
|
|
@ -354,12 +354,10 @@ class IniSettingRegistry(ASettingRegistry):
|
|||
with open(self._registry_file, mode="w") as cfg:
|
||||
config.write(cfg)
|
||||
|
||||
def _set_item(self, name, value):
|
||||
# type: (str, str) -> None
|
||||
def _set_item(self, name: str, value: str) -> None:
|
||||
self.set_item_section("MAIN", name, value)
|
||||
|
||||
def set_item(self, name, value):
|
||||
# type: (str, str) -> None
|
||||
def set_item(self, name: str, value: str) -> None:
|
||||
"""Set item to settings ini file.
|
||||
|
||||
This saves item to ``DEFAULT`` section of ini as each item there
|
||||
|
|
@ -372,10 +370,9 @@ class IniSettingRegistry(ASettingRegistry):
|
|||
"""
|
||||
# this does the some, overridden just for different docstring.
|
||||
# we cast value to str as ini options values must be strings.
|
||||
super(IniSettingRegistry, self).set_item(name, str(value))
|
||||
super().set_item(name, str(value))
|
||||
|
||||
def get_item(self, name):
|
||||
# type: (str) -> str
|
||||
def get_item(self, name: str) -> str:
|
||||
"""Gets item from settings ini file.
|
||||
|
||||
This gets settings from ``DEFAULT`` section of ini file as each item
|
||||
|
|
@ -388,19 +385,18 @@ class IniSettingRegistry(ASettingRegistry):
|
|||
str: Value of item.
|
||||
|
||||
Raises:
|
||||
ValueError: If value doesn't exist.
|
||||
RegistryItemNotFound: If value doesn't exist.
|
||||
|
||||
"""
|
||||
return super(IniSettingRegistry, self).get_item(name)
|
||||
return super().get_item(name)
|
||||
|
||||
@lru_cache(maxsize=32)
|
||||
def get_item_from_section(self, section, name):
|
||||
# type: (str, str) -> str
|
||||
def get_item_from_section(self, section: str, name: str) -> str:
|
||||
"""Get item from section of ini file.
|
||||
|
||||
This will read ini file and try to get item value from specified
|
||||
section. If that section or item doesn't exist, :exc:`ValueError`
|
||||
is risen.
|
||||
section. If that section or item doesn't exist,
|
||||
:exc:`RegistryItemNotFound` is risen.
|
||||
|
||||
Args:
|
||||
section (str): Name of ini section.
|
||||
|
|
@ -410,7 +406,7 @@ class IniSettingRegistry(ASettingRegistry):
|
|||
str: Item value.
|
||||
|
||||
Raises:
|
||||
ValueError: If value doesn't exist.
|
||||
RegistryItemNotFound: If value doesn't exist.
|
||||
|
||||
"""
|
||||
config = configparser.ConfigParser()
|
||||
|
|
@ -418,16 +414,15 @@ class IniSettingRegistry(ASettingRegistry):
|
|||
try:
|
||||
value = config[section][name]
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
"Registry doesn't contain value {}:{}".format(section, name))
|
||||
raise RegistryItemNotFound(
|
||||
f"Registry doesn't contain value {section}:{name}"
|
||||
)
|
||||
return value
|
||||
|
||||
def _get_item(self, name):
|
||||
# type: (str) -> str
|
||||
def _get_item(self, name: str) -> str:
|
||||
return self.get_item_from_section("MAIN", name)
|
||||
|
||||
def delete_item_from_section(self, section, name):
|
||||
# type: (str, str) -> None
|
||||
def delete_item_from_section(self, section: str, name: str) -> None:
|
||||
"""Delete item from section in ini file.
|
||||
|
||||
Args:
|
||||
|
|
@ -435,7 +430,7 @@ class IniSettingRegistry(ASettingRegistry):
|
|||
name (str): Name of the item.
|
||||
|
||||
Raises:
|
||||
ValueError: If item doesn't exist.
|
||||
RegistryItemNotFound: If the item doesn't exist.
|
||||
|
||||
"""
|
||||
self.get_item_from_section.cache_clear()
|
||||
|
|
@ -444,8 +439,9 @@ class IniSettingRegistry(ASettingRegistry):
|
|||
try:
|
||||
_ = config[section][name]
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
"Registry doesn't contain value {}:{}".format(section, name))
|
||||
raise RegistryItemNotFound(
|
||||
f"Registry doesn't contain value {section}:{name}"
|
||||
)
|
||||
config.remove_option(section, name)
|
||||
|
||||
# if section is empty, delete it
|
||||
|
|
@ -461,29 +457,28 @@ class IniSettingRegistry(ASettingRegistry):
|
|||
|
||||
|
||||
class JSONSettingRegistry(ASettingRegistry):
|
||||
"""Class using json file as storage."""
|
||||
"""Class using a json file as storage."""
|
||||
|
||||
def __init__(self, name, path):
|
||||
# type: (str, str) -> JSONSettingRegistry
|
||||
super(JSONSettingRegistry, self).__init__(name)
|
||||
#: str: name of registry file
|
||||
self._registry_file = os.path.join(path, "{}.json".format(name))
|
||||
def __init__(self, name: str, path: str) -> None:
|
||||
super().__init__(name)
|
||||
self._registry_file = os.path.join(path, f"{name}.json")
|
||||
now = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
|
||||
header = {
|
||||
"__metadata__": {"generated": now},
|
||||
"registry": {}
|
||||
}
|
||||
|
||||
if not os.path.exists(os.path.dirname(self._registry_file)):
|
||||
os.makedirs(os.path.dirname(self._registry_file), exist_ok=True)
|
||||
# Use 'os.path.dirname' in case someone uses slashes in 'name'
|
||||
dirpath = os.path.dirname(self._registry_file)
|
||||
if not os.path.exists(dirpath):
|
||||
os.makedirs(dirpath, exist_ok=True)
|
||||
if not os.path.exists(self._registry_file):
|
||||
with open(self._registry_file, mode="w") as cfg:
|
||||
json.dump(header, cfg, indent=4)
|
||||
|
||||
@lru_cache(maxsize=32)
|
||||
def _get_item(self, name):
|
||||
# type: (str) -> object
|
||||
"""Get item value from registry json.
|
||||
def _get_item(self, name: str) -> str:
|
||||
"""Get item value from the registry.
|
||||
|
||||
Note:
|
||||
See :meth:`ayon_core.lib.JSONSettingRegistry.get_item`
|
||||
|
|
@ -494,29 +489,13 @@ class JSONSettingRegistry(ASettingRegistry):
|
|||
try:
|
||||
value = data["registry"][name]
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
"Registry doesn't contain value {}".format(name))
|
||||
raise RegistryItemNotFound(
|
||||
f"Registry doesn't contain value {name}"
|
||||
)
|
||||
return value
|
||||
|
||||
def get_item(self, name):
|
||||
# type: (str) -> object
|
||||
"""Get item value from registry json.
|
||||
|
||||
Args:
|
||||
name (str): Name of the item.
|
||||
|
||||
Returns:
|
||||
value of the item
|
||||
|
||||
Raises:
|
||||
ValueError: If item is not found in registry file.
|
||||
|
||||
"""
|
||||
return self._get_item(name)
|
||||
|
||||
def _set_item(self, name, value):
|
||||
# type: (str, object) -> None
|
||||
"""Set item value to registry json.
|
||||
def _set_item(self, name: str, value: str) -> None:
|
||||
"""Set item value to the registry.
|
||||
|
||||
Note:
|
||||
See :meth:`ayon_core.lib.JSONSettingRegistry.set_item`
|
||||
|
|
@ -528,41 +507,39 @@ class JSONSettingRegistry(ASettingRegistry):
|
|||
cfg.truncate(0)
|
||||
cfg.seek(0)
|
||||
json.dump(data, cfg, indent=4)
|
||||
|
||||
def set_item(self, name, value):
|
||||
# type: (str, object) -> None
|
||||
"""Set item and its value into json registry file.
|
||||
|
||||
Args:
|
||||
name (str): name of the item.
|
||||
value (Any): value of the item.
|
||||
|
||||
"""
|
||||
self._set_item(name, value)
|
||||
|
||||
def _delete_item(self, name):
|
||||
# type: (str) -> None
|
||||
self._get_item.cache_clear()
|
||||
|
||||
def _delete_item(self, name: str) -> None:
|
||||
with open(self._registry_file, "r+") as cfg:
|
||||
data = json.load(cfg)
|
||||
del data["registry"][name]
|
||||
cfg.truncate(0)
|
||||
cfg.seek(0)
|
||||
json.dump(data, cfg, indent=4)
|
||||
self._get_item.cache_clear()
|
||||
|
||||
|
||||
class AYONSettingsRegistry(JSONSettingRegistry):
|
||||
"""Class handling AYON general settings registry.
|
||||
|
||||
Args:
|
||||
name (Optional[str]): Name of the registry.
|
||||
"""
|
||||
name (Optional[str]): Name of the registry. Using 'None' or not
|
||||
passing name is deprecated.
|
||||
|
||||
def __init__(self, name=None):
|
||||
"""
|
||||
def __init__(self, name: Optional[str] = None) -> None:
|
||||
if not name:
|
||||
name = "AYON_settings"
|
||||
warnings.warn(
|
||||
(
|
||||
"Used 'AYONSettingsRegistry' without 'name' argument."
|
||||
" The argument will be required in future versions."
|
||||
),
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
path = get_launcher_storage_dir()
|
||||
super(AYONSettingsRegistry, self).__init__(name, path)
|
||||
super().__init__(name, path)
|
||||
|
||||
|
||||
def get_local_site_id():
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import collections
|
|||
import tempfile
|
||||
import subprocess
|
||||
import platform
|
||||
import warnings
|
||||
import functools
|
||||
from typing import Optional
|
||||
|
||||
import xml.etree.ElementTree
|
||||
|
|
@ -67,6 +69,47 @@ VIDEO_EXTENSIONS = {
|
|||
}
|
||||
|
||||
|
||||
def deprecated(new_destination):
|
||||
"""Mark functions as deprecated.
|
||||
|
||||
It will result in a warning being emitted when the function is used.
|
||||
"""
|
||||
|
||||
func = None
|
||||
if callable(new_destination):
|
||||
func = new_destination
|
||||
new_destination = None
|
||||
|
||||
def _decorator(decorated_func):
|
||||
if new_destination is None:
|
||||
warning_message = (
|
||||
" Please check content of deprecated function to figure out"
|
||||
" possible replacement."
|
||||
)
|
||||
else:
|
||||
warning_message = " Please replace your usage with '{}'.".format(
|
||||
new_destination
|
||||
)
|
||||
|
||||
@functools.wraps(decorated_func)
|
||||
def wrapper(*args, **kwargs):
|
||||
warnings.simplefilter("always", DeprecationWarning)
|
||||
warnings.warn(
|
||||
(
|
||||
"Call to deprecated function '{}'"
|
||||
"\nFunction was moved or removed.{}"
|
||||
).format(decorated_func.__name__, warning_message),
|
||||
category=DeprecationWarning,
|
||||
stacklevel=4
|
||||
)
|
||||
return decorated_func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
if func is None:
|
||||
return _decorator
|
||||
return _decorator(func)
|
||||
|
||||
|
||||
def get_transcode_temp_directory():
|
||||
"""Creates temporary folder for transcoding.
|
||||
|
||||
|
|
@ -966,6 +1009,8 @@ def convert_ffprobe_fps_to_float(value):
|
|||
return dividend / divisor
|
||||
|
||||
|
||||
# --- Deprecated functions ---
|
||||
@deprecated("oiio_color_convert")
|
||||
def convert_colorspace(
|
||||
input_path,
|
||||
output_path,
|
||||
|
|
@ -977,7 +1022,62 @@ def convert_colorspace(
|
|||
additional_command_args=None,
|
||||
logger=None,
|
||||
):
|
||||
"""Convert source file from one color space to another.
|
||||
"""DEPRECATED function use `oiio_color_convert` instead
|
||||
|
||||
Args:
|
||||
input_path (str): Path to input file that should be converted.
|
||||
output_path (str): Path to output file where result will be stored.
|
||||
config_path (str): Path to OCIO config file.
|
||||
source_colorspace (str): OCIO valid color space of source files.
|
||||
target_colorspace (str, optional): OCIO valid target color space.
|
||||
If filled, 'view' and 'display' must be empty.
|
||||
view (str, optional): Name for target viewer space (OCIO valid).
|
||||
Both 'view' and 'display' must be filled
|
||||
(if not 'target_colorspace').
|
||||
display (str, optional): Name for target display-referred
|
||||
reference space. Both 'view' and 'display' must be filled
|
||||
(if not 'target_colorspace').
|
||||
additional_command_args (list, optional): Additional arguments
|
||||
for oiiotool (like binary depth for .dpx).
|
||||
logger (logging.Logger, optional): Logger used for logging.
|
||||
|
||||
Returns:
|
||||
None: Function returns None.
|
||||
|
||||
Raises:
|
||||
ValueError: If parameters are misconfigured.
|
||||
"""
|
||||
return oiio_color_convert(
|
||||
input_path,
|
||||
output_path,
|
||||
config_path,
|
||||
source_colorspace,
|
||||
target_colorspace=target_colorspace,
|
||||
target_display=display,
|
||||
target_view=view,
|
||||
additional_command_args=additional_command_args,
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
|
||||
def oiio_color_convert(
|
||||
input_path,
|
||||
output_path,
|
||||
config_path,
|
||||
source_colorspace,
|
||||
source_display=None,
|
||||
source_view=None,
|
||||
target_colorspace=None,
|
||||
target_display=None,
|
||||
target_view=None,
|
||||
additional_command_args=None,
|
||||
logger=None,
|
||||
):
|
||||
"""Transcode source file to other with colormanagement.
|
||||
|
||||
Oiiotool also support additional arguments for transcoding.
|
||||
For more information, see the official documentation:
|
||||
https://openimageio.readthedocs.io/en/latest/oiiotool.html
|
||||
|
||||
Args:
|
||||
input_path (str): Path that should be converted. It is expected that
|
||||
|
|
@ -989,17 +1089,26 @@ def convert_colorspace(
|
|||
sequence in 'file.FRAMESTART-FRAMEEND#.ext', `output.1-3#.tif`)
|
||||
config_path (str): path to OCIO config file
|
||||
source_colorspace (str): ocio valid color space of source files
|
||||
source_display (str, optional): name for source display-referred
|
||||
reference space (ocio valid). If provided, source_view must also be
|
||||
provided, and source_colorspace will be ignored
|
||||
source_view (str, optional): name for source viewer space (ocio valid)
|
||||
If provided, source_display must also be provided, and
|
||||
source_colorspace will be ignored
|
||||
target_colorspace (str): ocio valid target color space
|
||||
if filled, 'view' and 'display' must be empty
|
||||
view (str): name for viewer space (ocio valid)
|
||||
both 'view' and 'display' must be filled (if 'target_colorspace')
|
||||
display (str): name for display-referred reference space (ocio valid)
|
||||
target_display (str): name for target display-referred reference space
|
||||
(ocio valid) both 'view' and 'display' must be filled (if
|
||||
'target_colorspace')
|
||||
target_view (str): name for target viewer space (ocio valid)
|
||||
both 'view' and 'display' must be filled (if 'target_colorspace')
|
||||
additional_command_args (list): arguments for oiiotool (like binary
|
||||
depth for .dpx)
|
||||
logger (logging.Logger): Logger used for logging.
|
||||
|
||||
Raises:
|
||||
ValueError: if misconfigured
|
||||
|
||||
"""
|
||||
if logger is None:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -1024,23 +1133,82 @@ def convert_colorspace(
|
|||
"--ch", channels_arg
|
||||
])
|
||||
|
||||
if all([target_colorspace, view, display]):
|
||||
raise ValueError("Colorspace and both screen and display"
|
||||
" cannot be set together."
|
||||
"Choose colorspace or screen and display")
|
||||
if not target_colorspace and not all([view, display]):
|
||||
raise ValueError("Both screen and display must be set.")
|
||||
# Validate input parameters
|
||||
if target_colorspace and target_view and target_display:
|
||||
raise ValueError(
|
||||
"Colorspace and both view and display cannot be set together."
|
||||
"Choose colorspace or screen and display"
|
||||
)
|
||||
|
||||
if not target_colorspace and not target_view and not target_display:
|
||||
raise ValueError(
|
||||
"Both view and display must be set if target_colorspace is not "
|
||||
"provided."
|
||||
)
|
||||
|
||||
if (
|
||||
(source_view and not source_display)
|
||||
or (source_display and not source_view)
|
||||
):
|
||||
raise ValueError(
|
||||
"Both source_view and source_display must be provided if using "
|
||||
"display/view inputs."
|
||||
)
|
||||
|
||||
if source_view and source_display and source_colorspace:
|
||||
logger.warning(
|
||||
"Both source display/view and source_colorspace provided. "
|
||||
"Using source display/view pair and ignoring source_colorspace."
|
||||
)
|
||||
|
||||
if additional_command_args:
|
||||
oiio_cmd.extend(additional_command_args)
|
||||
|
||||
if target_colorspace:
|
||||
oiio_cmd.extend(["--colorconvert:subimages=0",
|
||||
source_colorspace,
|
||||
target_colorspace])
|
||||
if view and display:
|
||||
oiio_cmd.extend(["--iscolorspace", source_colorspace])
|
||||
oiio_cmd.extend(["--ociodisplay:subimages=0", display, view])
|
||||
# Handle the different conversion cases
|
||||
# Source view and display are known
|
||||
if source_view and source_display:
|
||||
if target_colorspace:
|
||||
# This is a two-step conversion process since there's no direct
|
||||
# display/view to colorspace command
|
||||
# This could be a config parameter or determined from OCIO config
|
||||
# Use temporarty role space 'scene_linear'
|
||||
color_convert_args = ("scene_linear", target_colorspace)
|
||||
elif source_display != target_display or source_view != target_view:
|
||||
# Complete display/view pair conversion
|
||||
# - go through a reference space
|
||||
color_convert_args = (target_display, target_view)
|
||||
else:
|
||||
color_convert_args = None
|
||||
logger.debug(
|
||||
"Source and target display/view pairs are identical."
|
||||
" No color conversion needed."
|
||||
)
|
||||
|
||||
if color_convert_args:
|
||||
oiio_cmd.extend([
|
||||
"--ociodisplay:inverse=1:subimages=0",
|
||||
source_display,
|
||||
source_view,
|
||||
"--colorconvert:subimages=0",
|
||||
*color_convert_args
|
||||
])
|
||||
|
||||
elif target_colorspace:
|
||||
# Standard color space to color space conversion
|
||||
oiio_cmd.extend([
|
||||
"--colorconvert:subimages=0",
|
||||
source_colorspace,
|
||||
target_colorspace,
|
||||
])
|
||||
else:
|
||||
# Standard conversion from colorspace to display/view
|
||||
oiio_cmd.extend([
|
||||
"--iscolorspace",
|
||||
source_colorspace,
|
||||
"--ociodisplay:subimages=0",
|
||||
target_display,
|
||||
target_view,
|
||||
])
|
||||
|
||||
oiio_cmd.extend(["-o", output_path])
|
||||
|
||||
|
|
|
|||
|
|
@ -19,11 +19,7 @@ from .create import (
|
|||
CreatedInstance,
|
||||
CreatorError,
|
||||
|
||||
LegacyCreator,
|
||||
legacy_create,
|
||||
|
||||
discover_creator_plugins,
|
||||
discover_legacy_creator_plugins,
|
||||
register_creator_plugin,
|
||||
deregister_creator_plugin,
|
||||
register_creator_plugin_path,
|
||||
|
|
@ -141,12 +137,7 @@ __all__ = (
|
|||
|
||||
"CreatorError",
|
||||
|
||||
# - legacy creation
|
||||
"LegacyCreator",
|
||||
"legacy_create",
|
||||
|
||||
"discover_creator_plugins",
|
||||
"discover_legacy_creator_plugins",
|
||||
"register_creator_plugin",
|
||||
"deregister_creator_plugin",
|
||||
"register_creator_plugin_path",
|
||||
|
|
|
|||
|
|
@ -1403,7 +1403,12 @@ def _get_display_view_colorspace_name(config_path, display, view):
|
|||
|
||||
"""
|
||||
config = _get_ocio_config(config_path)
|
||||
return config.getDisplayViewColorSpaceName(display, view)
|
||||
colorspace = config.getDisplayViewColorSpaceName(display, view)
|
||||
# Special token. See https://opencolorio.readthedocs.io/en/latest/guides/authoring/authoring.html#shared-views # noqa
|
||||
if colorspace == "<USE_DISPLAY_NAME>":
|
||||
colorspace = display
|
||||
|
||||
return colorspace
|
||||
|
||||
|
||||
def _get_ocio_config_colorspaces(config_path):
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import pyblish.api
|
|||
from pyblish.lib import MessageHandler
|
||||
|
||||
from ayon_core import AYON_CORE_ROOT
|
||||
from ayon_core.host import HostBase
|
||||
from ayon_core.host import AbstractHost
|
||||
from ayon_core.lib import (
|
||||
is_in_tests,
|
||||
initialize_ayon_connection,
|
||||
|
|
@ -100,16 +100,16 @@ def registered_root():
|
|||
return _registered_root["_"]
|
||||
|
||||
|
||||
def install_host(host: HostBase) -> None:
|
||||
def install_host(host: AbstractHost) -> None:
|
||||
"""Install `host` into the running Python session.
|
||||
|
||||
Args:
|
||||
host (HostBase): A host interface object.
|
||||
host (AbstractHost): A host interface object.
|
||||
|
||||
"""
|
||||
if not isinstance(host, HostBase):
|
||||
if not isinstance(host, AbstractHost):
|
||||
log.error(
|
||||
f"Host must be a subclass of 'HostBase', got '{type(host)}'."
|
||||
f"Host must be a subclass of 'AbstractHost', got '{type(host)}'."
|
||||
)
|
||||
|
||||
global _is_installed
|
||||
|
|
@ -310,7 +310,7 @@ def get_current_host_name():
|
|||
"""
|
||||
|
||||
host = registered_host()
|
||||
if isinstance(host, HostBase):
|
||||
if isinstance(host, AbstractHost):
|
||||
return host.name
|
||||
return os.environ.get("AYON_HOST_NAME")
|
||||
|
||||
|
|
@ -346,28 +346,28 @@ def get_global_context():
|
|||
|
||||
def get_current_context():
|
||||
host = registered_host()
|
||||
if isinstance(host, HostBase):
|
||||
if isinstance(host, AbstractHost):
|
||||
return host.get_current_context()
|
||||
return get_global_context()
|
||||
|
||||
|
||||
def get_current_project_name():
|
||||
host = registered_host()
|
||||
if isinstance(host, HostBase):
|
||||
if isinstance(host, AbstractHost):
|
||||
return host.get_current_project_name()
|
||||
return get_global_context()["project_name"]
|
||||
|
||||
|
||||
def get_current_folder_path():
|
||||
host = registered_host()
|
||||
if isinstance(host, HostBase):
|
||||
if isinstance(host, AbstractHost):
|
||||
return host.get_current_folder_path()
|
||||
return get_global_context()["folder_path"]
|
||||
|
||||
|
||||
def get_current_task_name():
|
||||
host = registered_host()
|
||||
if isinstance(host, HostBase):
|
||||
if isinstance(host, AbstractHost):
|
||||
return host.get_current_task_name()
|
||||
return get_global_context()["task_name"]
|
||||
|
||||
|
|
|
|||
|
|
@ -21,12 +21,14 @@ from .exceptions import (
|
|||
TemplateFillError,
|
||||
)
|
||||
from .structures import (
|
||||
ParentFlags,
|
||||
CreatedInstance,
|
||||
ConvertorItem,
|
||||
AttributeValues,
|
||||
CreatorAttributeValues,
|
||||
PublishAttributeValues,
|
||||
PublishAttributes,
|
||||
InstanceContextInfo,
|
||||
)
|
||||
from .utils import (
|
||||
get_last_versions_for_instances,
|
||||
|
|
@ -44,9 +46,6 @@ from .creator_plugins import (
|
|||
AutoCreator,
|
||||
HiddenCreator,
|
||||
|
||||
discover_legacy_creator_plugins,
|
||||
get_legacy_creator_by_name,
|
||||
|
||||
discover_creator_plugins,
|
||||
register_creator_plugin,
|
||||
deregister_creator_plugin,
|
||||
|
|
@ -58,11 +57,6 @@ from .creator_plugins import (
|
|||
|
||||
from .context import CreateContext
|
||||
|
||||
from .legacy_create import (
|
||||
LegacyCreator,
|
||||
legacy_create,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"PRODUCT_NAME_ALLOWED_SYMBOLS",
|
||||
|
|
@ -85,12 +79,14 @@ __all__ = (
|
|||
"TaskNotSetError",
|
||||
"TemplateFillError",
|
||||
|
||||
"ParentFlags",
|
||||
"CreatedInstance",
|
||||
"ConvertorItem",
|
||||
"AttributeValues",
|
||||
"CreatorAttributeValues",
|
||||
"PublishAttributeValues",
|
||||
"PublishAttributes",
|
||||
"InstanceContextInfo",
|
||||
|
||||
"get_last_versions_for_instances",
|
||||
"get_next_versions_for_instances",
|
||||
|
|
@ -105,9 +101,6 @@ __all__ = (
|
|||
"AutoCreator",
|
||||
"HiddenCreator",
|
||||
|
||||
"discover_legacy_creator_plugins",
|
||||
"get_legacy_creator_by_name",
|
||||
|
||||
"discover_creator_plugins",
|
||||
"register_creator_plugin",
|
||||
"deregister_creator_plugin",
|
||||
|
|
@ -117,7 +110,4 @@ __all__ = (
|
|||
"cache_and_get_instances",
|
||||
|
||||
"CreateContext",
|
||||
|
||||
"LegacyCreator",
|
||||
"legacy_create",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -41,7 +41,12 @@ from .exceptions import (
|
|||
HostMissRequiredMethod,
|
||||
)
|
||||
from .changes import TrackChangesItem
|
||||
from .structures import PublishAttributes, ConvertorItem, InstanceContextInfo
|
||||
from .structures import (
|
||||
PublishAttributes,
|
||||
ConvertorItem,
|
||||
InstanceContextInfo,
|
||||
ParentFlags,
|
||||
)
|
||||
from .creator_plugins import (
|
||||
Creator,
|
||||
AutoCreator,
|
||||
|
|
@ -49,15 +54,12 @@ from .creator_plugins import (
|
|||
discover_convertor_plugins,
|
||||
)
|
||||
if typing.TYPE_CHECKING:
|
||||
from ayon_core.host import HostBase
|
||||
from ayon_core.lib import AbstractAttrDef
|
||||
from ayon_core.lib.events import EventCallback, Event
|
||||
|
||||
from .structures import CreatedInstance
|
||||
from .creator_plugins import BaseCreator
|
||||
|
||||
class PublishHost(HostBase, IPublishHost):
|
||||
pass
|
||||
|
||||
# Import of functions and classes that were moved to different file
|
||||
# TODO Should be removed in future release - Added 24/08/28, 0.4.3-dev.1
|
||||
|
|
@ -80,6 +82,7 @@ INSTANCE_ADDED_TOPIC = "instances.added"
|
|||
INSTANCE_REMOVED_TOPIC = "instances.removed"
|
||||
VALUE_CHANGED_TOPIC = "values.changed"
|
||||
INSTANCE_REQUIREMENT_CHANGED_TOPIC = "instance.requirement.changed"
|
||||
INSTANCE_PARENT_CHANGED_TOPIC = "instance.parent.changed"
|
||||
PRE_CREATE_ATTR_DEFS_CHANGED_TOPIC = "pre.create.attr.defs.changed"
|
||||
CREATE_ATTR_DEFS_CHANGED_TOPIC = "create.attr.defs.changed"
|
||||
PUBLISH_ATTR_DEFS_CHANGED_TOPIC = "publish.attr.defs.changed"
|
||||
|
|
@ -163,7 +166,7 @@ class CreateContext:
|
|||
context which should be handled by host.
|
||||
|
||||
Args:
|
||||
host (PublishHost): Host implementation which handles implementation
|
||||
host (IPublishHost): Host implementation which handles implementation
|
||||
and global metadata.
|
||||
headless (bool): Context is created out of UI (Current not used).
|
||||
reset (bool): Reset context on initialization.
|
||||
|
|
@ -173,7 +176,7 @@ class CreateContext:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
host: "PublishHost",
|
||||
host: IPublishHost,
|
||||
headless: bool = False,
|
||||
reset: bool = True,
|
||||
discover_publish_plugins: bool = True,
|
||||
|
|
@ -262,6 +265,8 @@ class CreateContext:
|
|||
# - right now used only for 'mandatory' but can be extended
|
||||
# in future
|
||||
"requirement_change": BulkInfo(),
|
||||
# Instance parent changed
|
||||
"parent_change": BulkInfo(),
|
||||
}
|
||||
self._bulk_order = []
|
||||
|
||||
|
|
@ -1083,6 +1088,35 @@ class CreateContext:
|
|||
INSTANCE_REQUIREMENT_CHANGED_TOPIC, callback
|
||||
)
|
||||
|
||||
def add_instance_parent_change_callback(
|
||||
self, callback: Callable
|
||||
) -> "EventCallback":
|
||||
"""Register callback to listen to instance parent changes.
|
||||
|
||||
Instance changed parent or parent flags.
|
||||
|
||||
Data structure of event:
|
||||
|
||||
```python
|
||||
{
|
||||
"instances": [CreatedInstance, ...],
|
||||
"create_context": CreateContext
|
||||
}
|
||||
```
|
||||
|
||||
Args:
|
||||
callback (Callable): Callback function that will be called when
|
||||
instance requirement changed.
|
||||
|
||||
Returns:
|
||||
EventCallback: Created callback object which can be used to
|
||||
stop listening.
|
||||
|
||||
"""
|
||||
return self._event_hub.add_callback(
|
||||
INSTANCE_PARENT_CHANGED_TOPIC, callback
|
||||
)
|
||||
|
||||
def context_data_to_store(self) -> dict[str, Any]:
|
||||
"""Data that should be stored by host function.
|
||||
|
||||
|
|
@ -1364,6 +1398,13 @@ class CreateContext:
|
|||
) as bulk_info:
|
||||
yield bulk_info
|
||||
|
||||
@contextmanager
|
||||
def bulk_instance_parent_change(self, sender: Optional[str] = None):
|
||||
with self._bulk_context(
|
||||
"parent_change", sender
|
||||
) as bulk_info:
|
||||
yield bulk_info
|
||||
|
||||
@contextmanager
|
||||
def bulk_publish_attr_defs_change(self, sender: Optional[str] = None):
|
||||
with self._bulk_context("publish_attrs_change", sender) as bulk_info:
|
||||
|
|
@ -1444,6 +1485,19 @@ class CreateContext:
|
|||
with self.bulk_instance_requirement_change() as bulk_item:
|
||||
bulk_item.append(instance_id)
|
||||
|
||||
def instance_parent_changed(self, instance_id: str) -> None:
|
||||
"""Instance parent changed.
|
||||
|
||||
Triggered by `CreatedInstance`.
|
||||
|
||||
Args:
|
||||
instance_id (Optional[str]): Instance id.
|
||||
|
||||
"""
|
||||
if self._is_instance_events_ready(instance_id):
|
||||
with self.bulk_instance_parent_change() as bulk_item:
|
||||
bulk_item.append(instance_id)
|
||||
|
||||
# --- context change callbacks ---
|
||||
def publish_attribute_value_changed(
|
||||
self, plugin_name: str, value: dict[str, Any]
|
||||
|
|
@ -2046,63 +2100,97 @@ class CreateContext:
|
|||
sender (Optional[str]): Sender of the event.
|
||||
|
||||
"""
|
||||
instance_ids_by_parent_id = collections.defaultdict(set)
|
||||
for instance in self.instances:
|
||||
instance_ids_by_parent_id[instance.parent_instance_id].add(
|
||||
instance.id
|
||||
)
|
||||
|
||||
instances_to_remove = list(instances)
|
||||
ids_to_remove = {
|
||||
instance.id
|
||||
for instance in instances_to_remove
|
||||
}
|
||||
_queue = collections.deque()
|
||||
_queue.extend(instances_to_remove)
|
||||
# Add children with parent lifetime flag
|
||||
while _queue:
|
||||
instance = _queue.popleft()
|
||||
ids_to_remove.add(instance.id)
|
||||
children_ids = instance_ids_by_parent_id[instance.id]
|
||||
for children_id in children_ids:
|
||||
if children_id in ids_to_remove:
|
||||
continue
|
||||
instance = self._instances_by_id[children_id]
|
||||
if instance.parent_flags & ParentFlags.parent_lifetime:
|
||||
instances_to_remove.append(instance)
|
||||
ids_to_remove.add(instance.id)
|
||||
_queue.append(instance)
|
||||
|
||||
instances_by_identifier = collections.defaultdict(list)
|
||||
for instance in instances:
|
||||
for instance in instances_to_remove:
|
||||
identifier = instance.creator_identifier
|
||||
instances_by_identifier[identifier].append(instance)
|
||||
|
||||
# Just remove instances from context if creator is not available
|
||||
missing_creators = set(instances_by_identifier) - set(self.creators)
|
||||
instances = []
|
||||
miss_creator_instances = []
|
||||
for identifier in missing_creators:
|
||||
instances.extend(
|
||||
instance
|
||||
for instance in instances_by_identifier[identifier]
|
||||
)
|
||||
miss_creator_instances.extend(instances_by_identifier[identifier])
|
||||
|
||||
self._remove_instances(instances, sender)
|
||||
with self.bulk_remove_instances(sender):
|
||||
self._remove_instances(miss_creator_instances, sender)
|
||||
|
||||
error_message = "Instances removement of creator \"{}\" failed. {}"
|
||||
failed_info = []
|
||||
# Remove instances by creator plugin order
|
||||
for creator in self.get_sorted_creators(
|
||||
instances_by_identifier.keys()
|
||||
):
|
||||
identifier = creator.identifier
|
||||
creator_instances = instances_by_identifier[identifier]
|
||||
error_message = "Instances removement of creator \"{}\" failed. {}"
|
||||
failed_info = []
|
||||
# Remove instances by creator plugin order
|
||||
for creator in self.get_sorted_creators(
|
||||
instances_by_identifier.keys()
|
||||
):
|
||||
identifier = creator.identifier
|
||||
# Filter instances by current state of 'CreateContext'
|
||||
# - in case instances were already removed as subroutine of
|
||||
# previous create plugin.
|
||||
creator_instances = [
|
||||
instance
|
||||
for instance in instances_by_identifier[identifier]
|
||||
if instance.id in self._instances_by_id
|
||||
]
|
||||
if not creator_instances:
|
||||
continue
|
||||
|
||||
label = creator.label
|
||||
failed = False
|
||||
add_traceback = False
|
||||
exc_info = None
|
||||
try:
|
||||
creator.remove_instances(creator_instances)
|
||||
label = creator.label
|
||||
failed = False
|
||||
add_traceback = False
|
||||
exc_info = None
|
||||
try:
|
||||
creator.remove_instances(creator_instances)
|
||||
|
||||
except CreatorError:
|
||||
failed = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
error_message.format(identifier, exc_info[1])
|
||||
)
|
||||
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
|
||||
except: # noqa: E722
|
||||
failed = True
|
||||
add_traceback = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
error_message.format(identifier, ""),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
if failed:
|
||||
failed_info.append(
|
||||
prepare_failed_creator_operation_info(
|
||||
identifier, label, exc_info, add_traceback
|
||||
except CreatorError:
|
||||
failed = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
error_message.format(identifier, exc_info[1])
|
||||
)
|
||||
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
|
||||
except: # noqa: E722
|
||||
failed = True
|
||||
add_traceback = True
|
||||
exc_info = sys.exc_info()
|
||||
self.log.warning(
|
||||
error_message.format(identifier, ""),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
if failed:
|
||||
failed_info.append(
|
||||
prepare_failed_creator_operation_info(
|
||||
identifier, label, exc_info, add_traceback
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if failed_info:
|
||||
raise CreatorsRemoveFailed(failed_info)
|
||||
|
|
@ -2305,6 +2393,8 @@ class CreateContext:
|
|||
self._bulk_publish_attrs_change_finished(data, sender)
|
||||
elif key == "requirement_change":
|
||||
self._bulk_instance_requirement_change_finished(data, sender)
|
||||
elif key == "parent_change":
|
||||
self._bulk_instance_parent_change_finished(data, sender)
|
||||
|
||||
def _bulk_add_instances_finished(
|
||||
self,
|
||||
|
|
@ -2518,3 +2608,22 @@ class CreateContext:
|
|||
{"instances": instances},
|
||||
sender,
|
||||
)
|
||||
|
||||
def _bulk_instance_parent_change_finished(
|
||||
self,
|
||||
instance_ids: list[str],
|
||||
sender: Optional[str],
|
||||
):
|
||||
if not instance_ids:
|
||||
return
|
||||
|
||||
instances = [
|
||||
self.get_instance_by_id(instance_id)
|
||||
for instance_id in set(instance_ids)
|
||||
]
|
||||
|
||||
self._emit_event(
|
||||
INSTANCE_PARENT_CHANGED_TOPIC,
|
||||
{"instances": instances},
|
||||
sender,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ from typing import TYPE_CHECKING, Optional, Dict, Any
|
|||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.lib import Logger, get_version_from_path
|
||||
from ayon_core.pipeline.plugin_discover import (
|
||||
discover,
|
||||
|
|
@ -20,7 +19,6 @@ from ayon_core.pipeline.staging_dir import get_staging_dir_info, StagingDir
|
|||
from .constants import DEFAULT_VARIANT_VALUE
|
||||
from .product_name import get_product_name
|
||||
from .utils import get_next_versions_for_instances
|
||||
from .legacy_create import LegacyCreator
|
||||
from .structures import CreatedInstance
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -975,62 +973,10 @@ def discover_convertor_plugins(*args, **kwargs):
|
|||
return discover(ProductConvertorPlugin, *args, **kwargs)
|
||||
|
||||
|
||||
def discover_legacy_creator_plugins():
|
||||
from ayon_core.pipeline import get_current_project_name
|
||||
|
||||
log = Logger.get_logger("CreatorDiscover")
|
||||
|
||||
plugins = discover(LegacyCreator)
|
||||
project_name = get_current_project_name()
|
||||
project_settings = get_project_settings(project_name)
|
||||
for plugin in plugins:
|
||||
try:
|
||||
plugin.apply_settings(project_settings)
|
||||
except Exception:
|
||||
log.warning(
|
||||
"Failed to apply settings to creator {}".format(
|
||||
plugin.__name__
|
||||
),
|
||||
exc_info=True
|
||||
)
|
||||
return plugins
|
||||
|
||||
|
||||
def get_legacy_creator_by_name(creator_name, case_sensitive=False):
|
||||
"""Find creator plugin by name.
|
||||
|
||||
Args:
|
||||
creator_name (str): Name of creator class that should be returned.
|
||||
case_sensitive (bool): Match of creator plugin name is case sensitive.
|
||||
Set to `False` by default.
|
||||
|
||||
Returns:
|
||||
Creator: Return first matching plugin or `None`.
|
||||
"""
|
||||
|
||||
# Lower input creator name if is not case sensitive
|
||||
if not case_sensitive:
|
||||
creator_name = creator_name.lower()
|
||||
|
||||
for creator_plugin in discover_legacy_creator_plugins():
|
||||
_creator_name = creator_plugin.__name__
|
||||
|
||||
# Lower creator plugin name if is not case sensitive
|
||||
if not case_sensitive:
|
||||
_creator_name = _creator_name.lower()
|
||||
|
||||
if _creator_name == creator_name:
|
||||
return creator_plugin
|
||||
return None
|
||||
|
||||
|
||||
def register_creator_plugin(plugin):
|
||||
if issubclass(plugin, BaseCreator):
|
||||
register_plugin(BaseCreator, plugin)
|
||||
|
||||
elif issubclass(plugin, LegacyCreator):
|
||||
register_plugin(LegacyCreator, plugin)
|
||||
|
||||
elif issubclass(plugin, ProductConvertorPlugin):
|
||||
register_plugin(ProductConvertorPlugin, plugin)
|
||||
|
||||
|
|
@ -1039,22 +985,17 @@ def deregister_creator_plugin(plugin):
|
|||
if issubclass(plugin, BaseCreator):
|
||||
deregister_plugin(BaseCreator, plugin)
|
||||
|
||||
elif issubclass(plugin, LegacyCreator):
|
||||
deregister_plugin(LegacyCreator, plugin)
|
||||
|
||||
elif issubclass(plugin, ProductConvertorPlugin):
|
||||
deregister_plugin(ProductConvertorPlugin, plugin)
|
||||
|
||||
|
||||
def register_creator_plugin_path(path):
|
||||
register_plugin_path(BaseCreator, path)
|
||||
register_plugin_path(LegacyCreator, path)
|
||||
register_plugin_path(ProductConvertorPlugin, path)
|
||||
|
||||
|
||||
def deregister_creator_plugin_path(path):
|
||||
deregister_plugin_path(BaseCreator, path)
|
||||
deregister_plugin_path(LegacyCreator, path)
|
||||
deregister_plugin_path(ProductConvertorPlugin, path)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,216 +0,0 @@
|
|||
"""Create workflow moved from avalon-core repository.
|
||||
|
||||
Renamed classes and functions
|
||||
- 'Creator' -> 'LegacyCreator'
|
||||
- 'create' -> 'legacy_create'
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import collections
|
||||
|
||||
from ayon_core.pipeline.constants import AYON_INSTANCE_ID
|
||||
|
||||
from .product_name import get_product_name
|
||||
|
||||
|
||||
class LegacyCreator:
|
||||
"""Determine how assets are created"""
|
||||
label = None
|
||||
product_type = None
|
||||
defaults = None
|
||||
maintain_selection = True
|
||||
enabled = True
|
||||
|
||||
dynamic_product_name_keys = []
|
||||
|
||||
log = logging.getLogger("LegacyCreator")
|
||||
log.propagate = True
|
||||
|
||||
def __init__(self, name, folder_path, options=None, data=None):
|
||||
self.name = name # For backwards compatibility
|
||||
self.options = options
|
||||
|
||||
# Default data
|
||||
self.data = collections.OrderedDict()
|
||||
# TODO use 'AYON_INSTANCE_ID' when all hosts support it
|
||||
self.data["id"] = AYON_INSTANCE_ID
|
||||
self.data["productType"] = self.product_type
|
||||
self.data["folderPath"] = folder_path
|
||||
self.data["productName"] = name
|
||||
self.data["active"] = True
|
||||
|
||||
self.data.update(data or {})
|
||||
|
||||
@classmethod
|
||||
def apply_settings(cls, project_settings):
|
||||
"""Apply AYON settings to a plugin class."""
|
||||
|
||||
host_name = os.environ.get("AYON_HOST_NAME")
|
||||
plugin_type = "create"
|
||||
plugin_type_settings = (
|
||||
project_settings
|
||||
.get(host_name, {})
|
||||
.get(plugin_type, {})
|
||||
)
|
||||
global_type_settings = (
|
||||
project_settings
|
||||
.get("core", {})
|
||||
.get(plugin_type, {})
|
||||
)
|
||||
if not global_type_settings and not plugin_type_settings:
|
||||
return
|
||||
|
||||
plugin_name = cls.__name__
|
||||
|
||||
plugin_settings = None
|
||||
# Look for plugin settings in host specific settings
|
||||
if plugin_name in plugin_type_settings:
|
||||
plugin_settings = plugin_type_settings[plugin_name]
|
||||
|
||||
# Look for plugin settings in global settings
|
||||
elif plugin_name in global_type_settings:
|
||||
plugin_settings = global_type_settings[plugin_name]
|
||||
|
||||
if not plugin_settings:
|
||||
return
|
||||
|
||||
cls.log.debug(">>> We have preset for {}".format(plugin_name))
|
||||
for option, value in plugin_settings.items():
|
||||
if option == "enabled" and value is False:
|
||||
cls.log.debug(" - is disabled by preset")
|
||||
else:
|
||||
cls.log.debug(" - setting `{}`: `{}`".format(option, value))
|
||||
setattr(cls, option, value)
|
||||
|
||||
def process(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def get_dynamic_data(
|
||||
cls, project_name, folder_entity, task_entity, variant, host_name
|
||||
):
|
||||
"""Return dynamic data for current Creator plugin.
|
||||
|
||||
By default return keys from `dynamic_product_name_keys` attribute
|
||||
as mapping to keep formatted template unchanged.
|
||||
|
||||
```
|
||||
dynamic_product_name_keys = ["my_key"]
|
||||
---
|
||||
output = {
|
||||
"my_key": "{my_key}"
|
||||
}
|
||||
```
|
||||
|
||||
Dynamic keys may override default Creator keys (productType, task,
|
||||
folderPath, ...) but do it wisely if you need.
|
||||
|
||||
All of keys will be converted into 3 variants unchanged, capitalized
|
||||
and all upper letters. Because of that are all keys lowered.
|
||||
|
||||
This method can be modified to prefill some values just keep in mind it
|
||||
is class method.
|
||||
|
||||
Args:
|
||||
project_name (str): Context's project name.
|
||||
folder_entity (dict[str, Any]): Folder entity.
|
||||
task_entity (dict[str, Any]): Task entity.
|
||||
variant (str): What is entered by user in creator tool.
|
||||
host_name (str): Name of host.
|
||||
|
||||
Returns:
|
||||
dict: Fill data for product name template.
|
||||
"""
|
||||
dynamic_data = {}
|
||||
for key in cls.dynamic_product_name_keys:
|
||||
key = key.lower()
|
||||
dynamic_data[key] = "{" + key + "}"
|
||||
return dynamic_data
|
||||
|
||||
@classmethod
|
||||
def get_product_name(
|
||||
cls, project_name, folder_entity, task_entity, variant, host_name=None
|
||||
):
|
||||
"""Return product name created with entered arguments.
|
||||
|
||||
Logic extracted from Creator tool. This method should give ability
|
||||
to get product name without the tool.
|
||||
|
||||
TODO: Maybe change `variant` variable.
|
||||
|
||||
By default is output concatenated product type with variant.
|
||||
|
||||
Args:
|
||||
project_name (str): Context's project name.
|
||||
folder_entity (dict[str, Any]): Folder entity.
|
||||
task_entity (dict[str, Any]): Task entity.
|
||||
variant (str): What is entered by user in creator tool.
|
||||
host_name (str): Name of host.
|
||||
|
||||
Returns:
|
||||
str: Formatted product name with entered arguments. Should match
|
||||
config's logic.
|
||||
"""
|
||||
|
||||
dynamic_data = cls.get_dynamic_data(
|
||||
project_name, folder_entity, task_entity, variant, host_name
|
||||
)
|
||||
task_name = task_type = None
|
||||
if task_entity:
|
||||
task_name = task_entity["name"]
|
||||
task_type = task_entity["taskType"]
|
||||
return get_product_name(
|
||||
project_name,
|
||||
task_name,
|
||||
task_type,
|
||||
host_name,
|
||||
cls.product_type,
|
||||
variant,
|
||||
dynamic_data=dynamic_data
|
||||
)
|
||||
|
||||
|
||||
def legacy_create(
|
||||
Creator, product_name, folder_path, options=None, data=None
|
||||
):
|
||||
"""Create a new instance
|
||||
|
||||
Associate nodes with a product name and type. These nodes are later
|
||||
validated, according to their `product type`, and integrated into the
|
||||
shared environment, relative their `productName`.
|
||||
|
||||
Data relative each product type, along with default data, are imprinted
|
||||
into the resulting objectSet. This data is later used by extractors
|
||||
and finally asset browsers to help identify the origin of the asset.
|
||||
|
||||
Arguments:
|
||||
Creator (Creator): Class of creator.
|
||||
product_name (str): Name of product.
|
||||
folder_path (str): Folder path.
|
||||
options (dict, optional): Additional options from GUI.
|
||||
data (dict, optional): Additional data from GUI.
|
||||
|
||||
Raises:
|
||||
NameError on `productName` already exists
|
||||
KeyError on invalid dynamic property
|
||||
RuntimeError on host error
|
||||
|
||||
Returns:
|
||||
Name of instance
|
||||
|
||||
"""
|
||||
from ayon_core.pipeline import registered_host
|
||||
|
||||
host = registered_host()
|
||||
plugin = Creator(product_name, folder_path, options, data)
|
||||
|
||||
if plugin.maintain_selection is True:
|
||||
with host.maintained_selection():
|
||||
print("Running %s with maintained selection" % plugin)
|
||||
instance = plugin.process()
|
||||
return instance
|
||||
|
||||
print("Running %s" % plugin)
|
||||
instance = plugin.process()
|
||||
return instance
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import copy
|
||||
import collections
|
||||
from uuid import uuid4
|
||||
from enum import Enum
|
||||
import typing
|
||||
from typing import Optional, Dict, List, Any
|
||||
|
||||
|
|
@ -22,6 +23,23 @@ if typing.TYPE_CHECKING:
|
|||
from .creator_plugins import BaseCreator
|
||||
|
||||
|
||||
class IntEnum(int, Enum):
|
||||
"""An int-based Enum class that allows for int comparison."""
|
||||
|
||||
def __int__(self) -> int:
|
||||
return self.value
|
||||
|
||||
|
||||
class ParentFlags(IntEnum):
|
||||
# Delete instance if parent is deleted
|
||||
parent_lifetime = 1
|
||||
# Active state is propagated from parent to children
|
||||
# - the active state is propagated in collection phase
|
||||
# NOTE It might be helpful to have a function that would return "real"
|
||||
# active state for instances
|
||||
share_active = 1 << 1
|
||||
|
||||
|
||||
class ConvertorItem:
|
||||
"""Item representing convertor plugin.
|
||||
|
||||
|
|
@ -507,7 +525,9 @@ class CreatedInstance:
|
|||
if transient_data is None:
|
||||
transient_data = {}
|
||||
self._transient_data = transient_data
|
||||
self._is_mandatory = False
|
||||
self._is_mandatory: bool = False
|
||||
self._parent_instance_id: Optional[str] = None
|
||||
self._parent_flags: int = 0
|
||||
|
||||
# Create a copy of passed data to avoid changing them on the fly
|
||||
data = copy.deepcopy(data or {})
|
||||
|
|
@ -752,6 +772,39 @@ class CreatedInstance:
|
|||
self["active"] = True
|
||||
self._create_context.instance_requirement_changed(self.id)
|
||||
|
||||
@property
|
||||
def parent_instance_id(self) -> Optional[str]:
|
||||
return self._parent_instance_id
|
||||
|
||||
@property
|
||||
def parent_flags(self) -> int:
|
||||
return self._parent_flags
|
||||
|
||||
def set_parent(
|
||||
self, instance_id: Optional[str], flags: int
|
||||
) -> None:
|
||||
"""Set parent instance id and parenting flags.
|
||||
|
||||
Args:
|
||||
instance_id (Optional[str]): Parent instance id.
|
||||
flags (int): Parenting flags.
|
||||
|
||||
"""
|
||||
changed = False
|
||||
if instance_id != self._parent_instance_id:
|
||||
changed = True
|
||||
self._parent_instance_id = instance_id
|
||||
|
||||
if flags is None:
|
||||
flags = 0
|
||||
|
||||
if self._parent_flags != flags:
|
||||
self._parent_flags = flags
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
self._create_context.instance_parent_changed(self.id)
|
||||
|
||||
def changes(self):
|
||||
"""Calculate and return changes."""
|
||||
|
||||
|
|
|
|||
|
|
@ -373,7 +373,7 @@ def discover_loader_plugins(project_name=None):
|
|||
if not project_name:
|
||||
project_name = get_current_project_name()
|
||||
project_settings = get_project_settings(project_name)
|
||||
plugins = discover(LoaderPlugin)
|
||||
plugins = discover(LoaderPlugin, allow_duplicates=False)
|
||||
hooks = discover(LoaderHookPlugin)
|
||||
sorted_hooks = sorted(hooks, key=lambda hook: hook.order)
|
||||
for plugin in plugins:
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import re
|
|||
import collections
|
||||
import copy
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
|
||||
import ayon_api
|
||||
from ayon_api import (
|
||||
|
|
@ -29,7 +30,7 @@ from ayon_api import (
|
|||
)
|
||||
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.host import IWorkfileHost, HostBase
|
||||
from ayon_core.host import IWorkfileHost, AbstractHost
|
||||
from ayon_core.lib import (
|
||||
Logger,
|
||||
StringTemplate,
|
||||
|
|
@ -53,7 +54,6 @@ from ayon_core.pipeline.plugin_discover import (
|
|||
)
|
||||
|
||||
from ayon_core.pipeline.create import (
|
||||
discover_legacy_creator_plugins,
|
||||
CreateContext,
|
||||
HiddenCreator,
|
||||
)
|
||||
|
|
@ -126,15 +126,14 @@ class AbstractTemplateBuilder(ABC):
|
|||
placeholder population.
|
||||
|
||||
Args:
|
||||
host (Union[HostBase, ModuleType]): Implementation of host.
|
||||
host (Union[AbstractHost, ModuleType]): Implementation of host.
|
||||
"""
|
||||
|
||||
_log = None
|
||||
use_legacy_creators = False
|
||||
|
||||
def __init__(self, host):
|
||||
# Get host name
|
||||
if isinstance(host, HostBase):
|
||||
if isinstance(host, AbstractHost):
|
||||
host_name = host.name
|
||||
else:
|
||||
host_name = os.environ.get("AYON_HOST_NAME")
|
||||
|
|
@ -162,24 +161,24 @@ class AbstractTemplateBuilder(ABC):
|
|||
|
||||
@property
|
||||
def project_name(self):
|
||||
if isinstance(self._host, HostBase):
|
||||
if isinstance(self._host, AbstractHost):
|
||||
return self._host.get_current_project_name()
|
||||
return os.getenv("AYON_PROJECT_NAME")
|
||||
|
||||
@property
|
||||
def current_folder_path(self):
|
||||
if isinstance(self._host, HostBase):
|
||||
if isinstance(self._host, AbstractHost):
|
||||
return self._host.get_current_folder_path()
|
||||
return os.getenv("AYON_FOLDER_PATH")
|
||||
|
||||
@property
|
||||
def current_task_name(self):
|
||||
if isinstance(self._host, HostBase):
|
||||
if isinstance(self._host, AbstractHost):
|
||||
return self._host.get_current_task_name()
|
||||
return os.getenv("AYON_TASK_NAME")
|
||||
|
||||
def get_current_context(self):
|
||||
if isinstance(self._host, HostBase):
|
||||
if isinstance(self._host, AbstractHost):
|
||||
return self._host.get_current_context()
|
||||
return {
|
||||
"project_name": self.project_name,
|
||||
|
|
@ -201,12 +200,6 @@ class AbstractTemplateBuilder(ABC):
|
|||
)
|
||||
return self._current_folder_entity
|
||||
|
||||
@property
|
||||
def linked_folder_entities(self):
|
||||
if self._linked_folder_entities is _NOT_SET:
|
||||
self._linked_folder_entities = self._get_linked_folder_entities()
|
||||
return self._linked_folder_entities
|
||||
|
||||
@property
|
||||
def current_task_entity(self):
|
||||
if self._current_task_entity is _NOT_SET:
|
||||
|
|
@ -261,7 +254,7 @@ class AbstractTemplateBuilder(ABC):
|
|||
"""Access to host implementation.
|
||||
|
||||
Returns:
|
||||
Union[HostBase, ModuleType]: Implementation of host.
|
||||
Union[AbstractHost, ModuleType]: Implementation of host.
|
||||
"""
|
||||
|
||||
return self._host
|
||||
|
|
@ -307,13 +300,16 @@ class AbstractTemplateBuilder(ABC):
|
|||
self._loaders_by_name = get_loaders_by_name()
|
||||
return self._loaders_by_name
|
||||
|
||||
def _get_linked_folder_entities(self):
|
||||
def get_linked_folder_entities(self, link_type: Optional[str]):
|
||||
if not link_type:
|
||||
return []
|
||||
project_name = self.project_name
|
||||
folder_entity = self.current_folder_entity
|
||||
if not folder_entity:
|
||||
return []
|
||||
links = get_folder_links(
|
||||
project_name, folder_entity["id"], link_direction="in"
|
||||
project_name,
|
||||
folder_entity["id"], link_types=[link_type], link_direction="in"
|
||||
)
|
||||
linked_folder_ids = {
|
||||
link["entityId"]
|
||||
|
|
@ -323,19 +319,6 @@ class AbstractTemplateBuilder(ABC):
|
|||
|
||||
return list(get_folders(project_name, folder_ids=linked_folder_ids))
|
||||
|
||||
def _collect_legacy_creators(self):
|
||||
creators_by_name = {}
|
||||
for creator in discover_legacy_creator_plugins():
|
||||
if not creator.enabled:
|
||||
continue
|
||||
creator_name = creator.__name__
|
||||
if creator_name in creators_by_name:
|
||||
raise KeyError(
|
||||
"Duplicated creator name {} !".format(creator_name)
|
||||
)
|
||||
creators_by_name[creator_name] = creator
|
||||
self._creators_by_name = creators_by_name
|
||||
|
||||
def _collect_creators(self):
|
||||
self._creators_by_name = {
|
||||
identifier: creator
|
||||
|
|
@ -347,10 +330,7 @@ class AbstractTemplateBuilder(ABC):
|
|||
|
||||
def get_creators_by_name(self):
|
||||
if self._creators_by_name is None:
|
||||
if self.use_legacy_creators:
|
||||
self._collect_legacy_creators()
|
||||
else:
|
||||
self._collect_creators()
|
||||
self._collect_creators()
|
||||
|
||||
return self._creators_by_name
|
||||
|
||||
|
|
@ -1429,10 +1409,27 @@ class PlaceholderLoadMixin(object):
|
|||
|
||||
builder_type_enum_items = [
|
||||
{"label": "Current folder", "value": "context_folder"},
|
||||
# TODO implement linked folders
|
||||
# {"label": "Linked folders", "value": "linked_folders"},
|
||||
{"label": "Linked folders", "value": "linked_folders"},
|
||||
{"label": "All folders", "value": "all_folders"},
|
||||
]
|
||||
|
||||
link_types = ayon_api.get_link_types(self.builder.project_name)
|
||||
|
||||
# Filter link types for folder to folder links
|
||||
link_types_enum_items = [
|
||||
{"label": link_type["name"], "value": link_type["linkType"]}
|
||||
for link_type in link_types
|
||||
if (
|
||||
link_type["inputType"] == "folder"
|
||||
and link_type["outputType"] == "folder"
|
||||
)
|
||||
]
|
||||
|
||||
if not link_types_enum_items:
|
||||
link_types_enum_items.append(
|
||||
{"label": "<No link types>", "value": None}
|
||||
)
|
||||
|
||||
build_type_label = "Folder Builder Type"
|
||||
build_type_help = (
|
||||
"Folder Builder Type\n"
|
||||
|
|
@ -1461,6 +1458,16 @@ class PlaceholderLoadMixin(object):
|
|||
items=builder_type_enum_items,
|
||||
tooltip=build_type_help
|
||||
),
|
||||
attribute_definitions.EnumDef(
|
||||
"link_type",
|
||||
label="Link Type",
|
||||
items=link_types_enum_items,
|
||||
tooltip=(
|
||||
"Link Type\n"
|
||||
"\nDefines what type of link will be used to"
|
||||
" link the asset to the current folder."
|
||||
)
|
||||
),
|
||||
attribute_definitions.EnumDef(
|
||||
"product_type",
|
||||
label="Product type",
|
||||
|
|
@ -1607,10 +1614,7 @@ class PlaceholderLoadMixin(object):
|
|||
|
||||
builder_type = placeholder.data["builder_type"]
|
||||
folder_ids = []
|
||||
if builder_type == "context_folder":
|
||||
folder_ids = [current_folder_entity["id"]]
|
||||
|
||||
elif builder_type == "all_folders":
|
||||
if builder_type == "all_folders":
|
||||
folder_ids = {
|
||||
folder_entity["id"]
|
||||
for folder_entity in get_folders(
|
||||
|
|
@ -1620,6 +1624,23 @@ class PlaceholderLoadMixin(object):
|
|||
)
|
||||
}
|
||||
|
||||
elif builder_type == "context_folder":
|
||||
folder_ids = [current_folder_entity["id"]]
|
||||
|
||||
elif builder_type == "linked_folders":
|
||||
# link type from placeholder data or default to "template"
|
||||
link_type = placeholder.data.get("link_type", "template")
|
||||
# Get all linked folders for the current folder
|
||||
if hasattr(self, "builder") and isinstance(
|
||||
self.builder, AbstractTemplateBuilder):
|
||||
# self.builder: AbstractTemplateBuilder
|
||||
folder_ids = [
|
||||
linked_folder_entity["id"]
|
||||
for linked_folder_entity in (
|
||||
self.builder.get_linked_folder_entities(
|
||||
link_type=link_type))
|
||||
]
|
||||
|
||||
if not folder_ids:
|
||||
return []
|
||||
|
||||
|
|
@ -1899,8 +1920,6 @@ class PlaceholderCreateMixin(object):
|
|||
pre_create_data (dict): dictionary of configuration from Creator
|
||||
configuration in UI
|
||||
"""
|
||||
|
||||
legacy_create = self.builder.use_legacy_creators
|
||||
creator_name = placeholder.data["creator"]
|
||||
create_variant = placeholder.data["create_variant"]
|
||||
active = placeholder.data.get("active")
|
||||
|
|
@ -1940,20 +1959,14 @@ class PlaceholderCreateMixin(object):
|
|||
|
||||
# compile product name from variant
|
||||
try:
|
||||
if legacy_create:
|
||||
creator_instance = creator_plugin(
|
||||
product_name,
|
||||
folder_path
|
||||
).process()
|
||||
else:
|
||||
creator_instance = self.builder.create_context.create(
|
||||
creator_plugin.identifier,
|
||||
create_variant,
|
||||
folder_entity,
|
||||
task_entity,
|
||||
pre_create_data=pre_create_data,
|
||||
active=active
|
||||
)
|
||||
creator_instance = self.builder.create_context.create(
|
||||
creator_plugin.identifier,
|
||||
create_variant,
|
||||
folder_entity,
|
||||
task_entity,
|
||||
pre_create_data=pre_create_data,
|
||||
active=active
|
||||
)
|
||||
|
||||
except: # noqa: E722
|
||||
failed = True
|
||||
|
|
|
|||
630
client/ayon_core/plugins/load/create_hero_version.py
Normal file
630
client/ayon_core/plugins/load/create_hero_version.py
Normal file
|
|
@ -0,0 +1,630 @@
|
|||
"""Plugin to create hero version from selected context."""
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import copy
|
||||
import shutil
|
||||
import errno
|
||||
import itertools
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Any, Optional
|
||||
|
||||
from speedcopy import copyfile
|
||||
import clique
|
||||
import ayon_api
|
||||
from ayon_api.operations import OperationsSession, new_version_entity
|
||||
from ayon_api.utils import create_entity_id
|
||||
from qtpy import QtWidgets, QtCore
|
||||
from ayon_core import style
|
||||
from ayon_core.pipeline import load, Anatomy
|
||||
from ayon_core.lib import create_hard_link, source_hash, StringTemplate
|
||||
from ayon_core.lib.file_transaction import wait_for_future_errors
|
||||
from ayon_core.pipeline.publish import get_publish_template_name
|
||||
from ayon_core.pipeline.template_data import get_template_data
|
||||
|
||||
|
||||
def prepare_changes(old_entity: dict, new_entity: dict) -> dict:
|
||||
"""Prepare changes dict for update entity operation.
|
||||
|
||||
Args:
|
||||
old_entity (dict): Existing entity data from database.
|
||||
new_entity (dict): New entity data to compare against old.
|
||||
|
||||
Returns:
|
||||
dict: Changes to apply to old entity to make it like new entity.
|
||||
|
||||
"""
|
||||
changes = {}
|
||||
for key in set(new_entity.keys()):
|
||||
if key == "attrib":
|
||||
continue
|
||||
if key in new_entity and new_entity[key] != old_entity.get(key):
|
||||
changes[key] = new_entity[key]
|
||||
attrib_changes = {}
|
||||
if "attrib" in new_entity:
|
||||
for key, value in new_entity["attrib"].items():
|
||||
if value != old_entity["attrib"].get(key):
|
||||
attrib_changes[key] = value
|
||||
if attrib_changes:
|
||||
changes["attrib"] = attrib_changes
|
||||
return changes
|
||||
|
||||
|
||||
class CreateHeroVersion(load.ProductLoaderPlugin):
|
||||
"""Create hero version from selected context."""
|
||||
|
||||
is_multiple_contexts_compatible = False
|
||||
representations = {"*"}
|
||||
product_types = {"*"}
|
||||
label = "Create Hero Version"
|
||||
order = 36
|
||||
icon = "star"
|
||||
color = "#ffd700"
|
||||
|
||||
ignored_representation_names: list[str] = []
|
||||
db_representation_context_keys = [
|
||||
"project", "folder", "asset", "hierarchy", "task", "product",
|
||||
"subset", "family", "representation", "username", "user", "output"
|
||||
]
|
||||
use_hardlinks = False
|
||||
|
||||
@staticmethod
|
||||
def message(text: str) -> None:
|
||||
"""Show message box with text."""
|
||||
msgBox = QtWidgets.QMessageBox()
|
||||
msgBox.setText(text)
|
||||
msgBox.setStyleSheet(style.load_stylesheet())
|
||||
msgBox.setWindowFlags(
|
||||
msgBox.windowFlags() | QtCore.Qt.WindowType.FramelessWindowHint
|
||||
)
|
||||
msgBox.exec_()
|
||||
|
||||
def load(self, context, name=None, namespace=None, options=None) -> None:
|
||||
"""Load hero version from context (dict as in context.py)."""
|
||||
success = True
|
||||
errors = []
|
||||
|
||||
# Extract project, product, version, folder from context
|
||||
project = context.get("project")
|
||||
product = context.get("product")
|
||||
version = context.get("version")
|
||||
folder = context.get("folder")
|
||||
task_entity = ayon_api.get_task_by_id(
|
||||
task_id=version.get("taskId"), project_name=project["name"]
|
||||
)
|
||||
|
||||
anatomy = Anatomy(project["name"])
|
||||
|
||||
version_id = version["id"]
|
||||
project_name = project["name"]
|
||||
repres = list(
|
||||
ayon_api.get_representations(
|
||||
project_name, version_ids={version_id}
|
||||
)
|
||||
)
|
||||
anatomy_data = get_template_data(
|
||||
project_entity=project,
|
||||
folder_entity=folder,
|
||||
task_entity=task_entity,
|
||||
)
|
||||
anatomy_data["product"] = {
|
||||
"name": product["name"],
|
||||
"type": product["productType"],
|
||||
}
|
||||
anatomy_data["version"] = version["version"]
|
||||
published_representations = {}
|
||||
for repre in repres:
|
||||
repre_anatomy = copy.deepcopy(anatomy_data)
|
||||
if "ext" not in repre_anatomy:
|
||||
repre_anatomy["ext"] = repre.get("context", {}).get("ext", "")
|
||||
published_representations[repre["id"]] = {
|
||||
"representation": repre,
|
||||
"published_files": [f["path"] for f in repre.get("files", [])],
|
||||
"anatomy_data": repre_anatomy
|
||||
}
|
||||
# get the publish directory
|
||||
publish_template_key = get_publish_template_name(
|
||||
project_name,
|
||||
context.get("hostName"),
|
||||
product["productType"],
|
||||
task_name=anatomy_data.get("task", {}).get("name"),
|
||||
task_type=anatomy_data.get("task", {}).get("type"),
|
||||
project_settings=context.get("project_settings", {}),
|
||||
logger=self.log
|
||||
)
|
||||
published_template_obj = anatomy.get_template_item(
|
||||
"publish", publish_template_key, "directory"
|
||||
)
|
||||
published_dir = os.path.normpath(
|
||||
published_template_obj.format_strict(anatomy_data)
|
||||
)
|
||||
instance_data = {
|
||||
"productName": product["name"],
|
||||
"productType": product["productType"],
|
||||
"anatomyData": anatomy_data,
|
||||
"publishDir": published_dir,
|
||||
"published_representations": published_representations,
|
||||
"versionEntity": version,
|
||||
}
|
||||
|
||||
try:
|
||||
self.create_hero_version(instance_data, anatomy, context)
|
||||
except Exception as exc:
|
||||
success = False
|
||||
errors.append(str(exc))
|
||||
if success:
|
||||
self.message("Hero version created successfully.")
|
||||
else:
|
||||
self.message(
|
||||
f"Failed to create hero version:\n{chr(10).join(errors)}")
|
||||
|
||||
def create_hero_version(
|
||||
self,
|
||||
instance_data: dict[str, Any],
|
||||
anatomy: Anatomy,
|
||||
context: dict[str, Any]) -> None:
|
||||
"""Create hero version from instance data.
|
||||
|
||||
Args:
|
||||
instance_data (dict): Instance data with keys:
|
||||
- productName (str): Name of the product.
|
||||
- productType (str): Type of the product.
|
||||
- anatomyData (dict): Anatomy data for templates.
|
||||
- publishDir (str): Directory where the product is published.
|
||||
- published_representations (dict): Published representations.
|
||||
- versionEntity (dict, optional): Source version entity.
|
||||
anatomy (Anatomy): Anatomy object for the project.
|
||||
context (dict): Context data with keys:
|
||||
- hostName (str): Name of the host application.
|
||||
- project_settings (dict): Project settings.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If any required data is missing or an error occurs
|
||||
during the hero version creation process.
|
||||
|
||||
"""
|
||||
published_repres = instance_data.get("published_representations")
|
||||
if not published_repres:
|
||||
raise RuntimeError("No published representations found.")
|
||||
|
||||
project_name = anatomy.project_name
|
||||
template_key = get_publish_template_name(
|
||||
project_name,
|
||||
context.get("hostName"),
|
||||
instance_data.get("productType"),
|
||||
instance_data.get("anatomyData", {}).get("task", {}).get("name"),
|
||||
instance_data.get("anatomyData", {}).get("task", {}).get("type"),
|
||||
project_settings=context.get("project_settings", {}),
|
||||
hero=True,
|
||||
)
|
||||
hero_template = anatomy.get_template_item(
|
||||
"hero", template_key, "path", default=None
|
||||
)
|
||||
if hero_template is None:
|
||||
raise RuntimeError("Project anatomy does not have hero "
|
||||
f"template key: {template_key}")
|
||||
|
||||
self.log.info(f"Hero template: {hero_template.template}")
|
||||
|
||||
hero_publish_dir = self.get_publish_dir(
|
||||
instance_data, anatomy, template_key
|
||||
)
|
||||
|
||||
self.log.info(f"Hero publish dir: {hero_publish_dir}")
|
||||
|
||||
src_version_entity = instance_data.get("versionEntity")
|
||||
filtered_repre_ids = []
|
||||
for repre_id, repre_info in published_repres.items():
|
||||
repre = repre_info["representation"]
|
||||
if repre["name"].lower() in self.ignored_representation_names:
|
||||
filtered_repre_ids.append(repre_id)
|
||||
for repre_id in filtered_repre_ids:
|
||||
published_repres.pop(repre_id, None)
|
||||
if not published_repres:
|
||||
raise RuntimeError(
|
||||
"All published representations were filtered by name."
|
||||
)
|
||||
|
||||
if src_version_entity is None:
|
||||
src_version_entity = self.version_from_representations(
|
||||
project_name, published_repres)
|
||||
if not src_version_entity:
|
||||
raise RuntimeError("Can't find origin version in database.")
|
||||
if src_version_entity["version"] == 0:
|
||||
raise RuntimeError("Version 0 cannot have hero version.")
|
||||
|
||||
all_copied_files = []
|
||||
transfers = instance_data.get("transfers", [])
|
||||
for _src, dst in transfers:
|
||||
dst = os.path.normpath(dst)
|
||||
if dst not in all_copied_files:
|
||||
all_copied_files.append(dst)
|
||||
hardlinks = instance_data.get("hardlinks", [])
|
||||
for _src, dst in hardlinks:
|
||||
dst = os.path.normpath(dst)
|
||||
if dst not in all_copied_files:
|
||||
all_copied_files.append(dst)
|
||||
|
||||
all_repre_file_paths = []
|
||||
for repre_info in published_repres.values():
|
||||
published_files = repre_info.get("published_files") or []
|
||||
for file_path in published_files:
|
||||
file_path = os.path.normpath(file_path)
|
||||
if file_path not in all_repre_file_paths:
|
||||
all_repre_file_paths.append(file_path)
|
||||
|
||||
publish_dir = instance_data.get("publishDir", "")
|
||||
if not publish_dir:
|
||||
raise RuntimeError(
|
||||
"publishDir is empty in instance_data, cannot continue."
|
||||
)
|
||||
instance_publish_dir = os.path.normpath(publish_dir)
|
||||
other_file_paths_mapping = []
|
||||
for file_path in all_copied_files:
|
||||
if not file_path.startswith(instance_publish_dir):
|
||||
continue
|
||||
if file_path in all_repre_file_paths:
|
||||
continue
|
||||
dst_filepath = file_path.replace(
|
||||
instance_publish_dir, hero_publish_dir
|
||||
)
|
||||
other_file_paths_mapping.append((file_path, dst_filepath))
|
||||
|
||||
old_version, old_repres = self.current_hero_ents(
|
||||
project_name, src_version_entity
|
||||
)
|
||||
inactive_old_repres_by_name = {}
|
||||
old_repres_by_name = {}
|
||||
for repre in old_repres:
|
||||
low_name = repre["name"].lower()
|
||||
if repre["active"]:
|
||||
old_repres_by_name[low_name] = repre
|
||||
else:
|
||||
inactive_old_repres_by_name[low_name] = repre
|
||||
|
||||
op_session = OperationsSession()
|
||||
entity_id = old_version["id"] if old_version else None
|
||||
new_hero_version = new_version_entity(
|
||||
-src_version_entity["version"],
|
||||
src_version_entity["productId"],
|
||||
task_id=src_version_entity.get("taskId"),
|
||||
data=copy.deepcopy(src_version_entity["data"]),
|
||||
attribs=copy.deepcopy(src_version_entity["attrib"]),
|
||||
entity_id=entity_id,
|
||||
)
|
||||
if old_version:
|
||||
update_data = prepare_changes(old_version, new_hero_version)
|
||||
op_session.update_entity(
|
||||
project_name, "version", old_version["id"], update_data
|
||||
)
|
||||
else:
|
||||
op_session.create_entity(project_name, "version", new_hero_version)
|
||||
|
||||
# Store hero entity to instance_data
|
||||
instance_data["heroVersionEntity"] = new_hero_version
|
||||
|
||||
old_repres_to_replace = {}
|
||||
for repre_info in published_repres.values():
|
||||
repre = repre_info["representation"]
|
||||
repre_name_low = repre["name"].lower()
|
||||
if repre_name_low in old_repres_by_name:
|
||||
old_repres_to_replace[repre_name_low] = (
|
||||
old_repres_by_name.pop(repre_name_low)
|
||||
)
|
||||
old_repres_to_delete = old_repres_by_name or {}
|
||||
backup_hero_publish_dir = None
|
||||
if os.path.exists(hero_publish_dir):
|
||||
base_backup_dir = f"{hero_publish_dir}.BACKUP"
|
||||
max_idx = 10
|
||||
# Find the first available backup directory name
|
||||
for idx in range(max_idx + 1):
|
||||
if idx == 0:
|
||||
candidate_backup_dir = base_backup_dir
|
||||
else:
|
||||
candidate_backup_dir = f"{base_backup_dir}{idx}"
|
||||
if not os.path.exists(candidate_backup_dir):
|
||||
backup_hero_publish_dir = candidate_backup_dir
|
||||
break
|
||||
else:
|
||||
raise AssertionError(
|
||||
f"Backup folders are fully occupied to max index {max_idx}"
|
||||
)
|
||||
|
||||
try:
|
||||
os.rename(hero_publish_dir, backup_hero_publish_dir)
|
||||
except PermissionError as e:
|
||||
raise AssertionError(
|
||||
"Could not create hero version because it is "
|
||||
"not possible to replace current hero files."
|
||||
) from e
|
||||
|
||||
try:
|
||||
src_to_dst_file_paths = []
|
||||
repre_integrate_data = []
|
||||
path_template_obj = anatomy.get_template_item(
|
||||
"hero", template_key, "path")
|
||||
anatomy_root = {"root": anatomy.roots}
|
||||
for repre_info in published_repres.values():
|
||||
published_files = repre_info["published_files"]
|
||||
if len(published_files) == 0:
|
||||
continue
|
||||
anatomy_data = copy.deepcopy(repre_info["anatomy_data"])
|
||||
anatomy_data.pop("version", None)
|
||||
template_filled = path_template_obj.format_strict(anatomy_data)
|
||||
repre_context = template_filled.used_values
|
||||
for key in self.db_representation_context_keys:
|
||||
value = anatomy_data.get(key)
|
||||
if value is not None:
|
||||
repre_context[key] = value
|
||||
repre_entity = copy.deepcopy(repre_info["representation"])
|
||||
repre_entity.pop("id", None)
|
||||
repre_entity["versionId"] = new_hero_version["id"]
|
||||
repre_entity["context"] = repre_context
|
||||
repre_entity["attrib"] = {
|
||||
"path": str(template_filled),
|
||||
"template": hero_template.template
|
||||
}
|
||||
dst_paths = []
|
||||
|
||||
if len(published_files) == 1:
|
||||
dst_paths.append(str(template_filled))
|
||||
mapped_published_file = StringTemplate(
|
||||
published_files[0]).format_strict(
|
||||
anatomy_root
|
||||
)
|
||||
src_to_dst_file_paths.append(
|
||||
(mapped_published_file, template_filled)
|
||||
)
|
||||
self.log.info(
|
||||
f"Single published file: {mapped_published_file} -> "
|
||||
f"{template_filled}"
|
||||
)
|
||||
else:
|
||||
collections, remainders = clique.assemble(published_files)
|
||||
if remainders or not collections or len(collections) > 1:
|
||||
raise RuntimeError(
|
||||
(
|
||||
"Integrity error. Files of published "
|
||||
"representation is combination of frame "
|
||||
"collections and single files."
|
||||
)
|
||||
)
|
||||
src_col = collections[0]
|
||||
frame_splitter = "_-_FRAME_SPLIT_-_"
|
||||
anatomy_data["frame"] = frame_splitter
|
||||
_template_filled = path_template_obj.format_strict(
|
||||
anatomy_data
|
||||
)
|
||||
head, tail = _template_filled.split(frame_splitter)
|
||||
padding = anatomy.templates_obj.frame_padding
|
||||
dst_col = clique.Collection(
|
||||
head=head, padding=padding, tail=tail
|
||||
)
|
||||
dst_col.indexes.clear()
|
||||
dst_col.indexes.update(src_col.indexes)
|
||||
for src_file, dst_file in zip(src_col, dst_col):
|
||||
src_file = StringTemplate(src_file).format_strict(
|
||||
anatomy_root
|
||||
)
|
||||
src_to_dst_file_paths.append((src_file, dst_file))
|
||||
dst_paths.append(dst_file)
|
||||
self.log.info(
|
||||
f"Collection published file: {src_file} "
|
||||
f"-> {dst_file}"
|
||||
)
|
||||
repre_integrate_data.append((repre_entity, dst_paths))
|
||||
|
||||
# Copy files
|
||||
with ThreadPoolExecutor(max_workers=8) as executor:
|
||||
futures = [
|
||||
executor.submit(self.copy_file, src_path, dst_path)
|
||||
for src_path, dst_path in itertools.chain(
|
||||
src_to_dst_file_paths, other_file_paths_mapping)
|
||||
]
|
||||
wait_for_future_errors(executor, futures)
|
||||
|
||||
# Update/create representations
|
||||
for repre_entity, dst_paths in repre_integrate_data:
|
||||
repre_files = self.get_files_info(dst_paths, anatomy)
|
||||
repre_entity["files"] = repre_files
|
||||
repre_name_low = repre_entity["name"].lower()
|
||||
if repre_name_low in old_repres_to_replace:
|
||||
old_repre = old_repres_to_replace.pop(repre_name_low)
|
||||
repre_entity["id"] = old_repre["id"]
|
||||
update_data = prepare_changes(old_repre, repre_entity)
|
||||
op_session.update_entity(
|
||||
project_name,
|
||||
"representation",
|
||||
old_repre["id"],
|
||||
update_data
|
||||
)
|
||||
elif repre_name_low in inactive_old_repres_by_name:
|
||||
inactive_repre = inactive_old_repres_by_name.pop(
|
||||
repre_name_low
|
||||
)
|
||||
repre_entity["id"] = inactive_repre["id"]
|
||||
update_data = prepare_changes(inactive_repre, repre_entity)
|
||||
op_session.update_entity(
|
||||
project_name,
|
||||
"representation",
|
||||
inactive_repre["id"],
|
||||
update_data
|
||||
)
|
||||
else:
|
||||
op_session.create_entity(
|
||||
project_name,
|
||||
"representation",
|
||||
repre_entity
|
||||
)
|
||||
|
||||
for repre in old_repres_to_delete.values():
|
||||
op_session.update_entity(
|
||||
project_name,
|
||||
"representation",
|
||||
repre["id"],
|
||||
{"active": False}
|
||||
)
|
||||
|
||||
op_session.commit()
|
||||
|
||||
if backup_hero_publish_dir is not None and os.path.exists(
|
||||
backup_hero_publish_dir
|
||||
):
|
||||
shutil.rmtree(backup_hero_publish_dir)
|
||||
|
||||
except Exception:
|
||||
if backup_hero_publish_dir is not None and os.path.exists(
|
||||
backup_hero_publish_dir):
|
||||
if os.path.exists(hero_publish_dir):
|
||||
shutil.rmtree(hero_publish_dir)
|
||||
os.rename(backup_hero_publish_dir, hero_publish_dir)
|
||||
raise
|
||||
|
||||
def get_files_info(
|
||||
self, filepaths: list[str], anatomy: Anatomy) -> list[dict]:
|
||||
"""Get list of file info dictionaries for given file paths.
|
||||
|
||||
Args:
|
||||
filepaths (list[str]): List of absolute file paths.
|
||||
anatomy (Anatomy): Anatomy object for the project.
|
||||
|
||||
Returns:
|
||||
list[dict]: List of file info dictionaries.
|
||||
|
||||
"""
|
||||
file_infos = []
|
||||
for filepath in filepaths:
|
||||
file_info = self.prepare_file_info(filepath, anatomy)
|
||||
file_infos.append(file_info)
|
||||
return file_infos
|
||||
|
||||
def prepare_file_info(self, path: str, anatomy: Anatomy) -> dict:
|
||||
"""Prepare file info dictionary for given path.
|
||||
|
||||
Args:
|
||||
path (str): Absolute file path.
|
||||
anatomy (Anatomy): Anatomy object for the project.
|
||||
|
||||
Returns:
|
||||
dict: File info dictionary with keys:
|
||||
- id (str): Unique identifier for the file.
|
||||
- name (str): Base name of the file.
|
||||
- path (str): Rootless file path.
|
||||
- size (int): Size of the file in bytes.
|
||||
- hash (str): Hash of the file content.
|
||||
- hash_type (str): Type of the hash used.
|
||||
|
||||
"""
|
||||
return {
|
||||
"id": create_entity_id(),
|
||||
"name": os.path.basename(path),
|
||||
"path": self.get_rootless_path(anatomy, path),
|
||||
"size": os.path.getsize(path),
|
||||
"hash": source_hash(path),
|
||||
"hash_type": "op3",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_publish_dir(
|
||||
instance_data: dict,
|
||||
anatomy: Anatomy,
|
||||
template_key: str) -> str:
|
||||
"""Get publish directory from instance data and anatomy.
|
||||
|
||||
Args:
|
||||
instance_data (dict): Instance data with "anatomyData" key.
|
||||
anatomy (Anatomy): Anatomy object for the project.
|
||||
template_key (str): Template key for the hero template.
|
||||
|
||||
Returns:
|
||||
str: Normalized publish directory path.
|
||||
|
||||
"""
|
||||
template_data = copy.deepcopy(instance_data.get("anatomyData", {}))
|
||||
if "originalBasename" in instance_data:
|
||||
template_data["originalBasename"] = (
|
||||
instance_data["originalBasename"]
|
||||
)
|
||||
template_obj = anatomy.get_template_item(
|
||||
"hero", template_key, "directory"
|
||||
)
|
||||
return os.path.normpath(template_obj.format_strict(template_data))
|
||||
|
||||
@staticmethod
|
||||
def get_rootless_path(anatomy: Anatomy, path: str) -> str:
|
||||
"""Get rootless path from absolute path.
|
||||
|
||||
Args:
|
||||
anatomy (Anatomy): Anatomy object for the project.
|
||||
path (str): Absolute file path.
|
||||
|
||||
Returns:
|
||||
str: Rootless file path if root found, else original path.
|
||||
|
||||
"""
|
||||
success, rootless_path = anatomy.find_root_template_from_path(path)
|
||||
if success:
|
||||
path = rootless_path
|
||||
return path
|
||||
|
||||
def copy_file(self, src_path: str, dst_path: str) -> None:
|
||||
"""Copy file from src to dst with creating directories.
|
||||
|
||||
Args:
|
||||
src_path (str): Source file path.
|
||||
dst_path (str): Destination file path.
|
||||
|
||||
Raises:
|
||||
OSError: If copying or linking fails.
|
||||
|
||||
"""
|
||||
dirname = os.path.dirname(dst_path)
|
||||
try:
|
||||
os.makedirs(dirname)
|
||||
except OSError as exc:
|
||||
if exc.errno != errno.EEXIST:
|
||||
raise
|
||||
if self.use_hardlinks:
|
||||
try:
|
||||
create_hard_link(src_path, dst_path)
|
||||
return
|
||||
except OSError as exc:
|
||||
if exc.errno not in [errno.EXDEV, errno.EINVAL]:
|
||||
raise
|
||||
copyfile(src_path, dst_path)
|
||||
|
||||
@staticmethod
|
||||
def version_from_representations(
|
||||
project_name: str, repres: dict) -> Optional[dict[str, Any]]:
|
||||
"""Find version from representations.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of the project.
|
||||
repres (dict): Dictionary of representations info.
|
||||
|
||||
Returns:
|
||||
Optional[dict]: Version entity if found, else None.
|
||||
|
||||
"""
|
||||
for repre_info in repres.values():
|
||||
version = ayon_api.get_version_by_id(
|
||||
project_name, repre_info["representation"]["versionId"]
|
||||
)
|
||||
if version:
|
||||
return version
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def current_hero_ents(
|
||||
project_name: str,
|
||||
version: dict[str, Any]) -> tuple[Any, list[dict[str, Any]]]:
|
||||
hero_version = ayon_api.get_hero_version_by_product_id(
|
||||
project_name, version["productId"]
|
||||
)
|
||||
if not hero_version:
|
||||
return None, []
|
||||
hero_repres = list(
|
||||
ayon_api.get_representations(
|
||||
project_name, version_ids={hero_version["id"]}
|
||||
)
|
||||
)
|
||||
return hero_version, hero_repres
|
||||
|
|
@ -6,15 +6,15 @@ from ayon_core.pipeline import load
|
|||
from ayon_core.pipeline.load import LoadError
|
||||
|
||||
|
||||
class PushToLibraryProject(load.ProductLoaderPlugin):
|
||||
"""Export selected versions to folder structure from Template"""
|
||||
class PushToProject(load.ProductLoaderPlugin):
|
||||
"""Export selected versions to different project"""
|
||||
|
||||
is_multiple_contexts_compatible = True
|
||||
|
||||
representations = {"*"}
|
||||
product_types = {"*"}
|
||||
|
||||
label = "Push to Library project"
|
||||
label = "Push to project"
|
||||
order = 35
|
||||
icon = "send"
|
||||
color = "#d8d8d8"
|
||||
|
|
@ -28,10 +28,12 @@ class PushToLibraryProject(load.ProductLoaderPlugin):
|
|||
if not filtered_contexts:
|
||||
raise LoadError("Nothing to push for your selection")
|
||||
|
||||
if len(filtered_contexts) > 1:
|
||||
raise LoadError("Please select only one item")
|
||||
|
||||
context = tuple(filtered_contexts)[0]
|
||||
folder_ids = set(
|
||||
context["folder"]["id"]
|
||||
for context in filtered_contexts
|
||||
)
|
||||
if len(folder_ids) > 1:
|
||||
raise LoadError("Please select products from single folder")
|
||||
|
||||
push_tool_script_path = os.path.join(
|
||||
AYON_CORE_ROOT,
|
||||
|
|
@ -39,14 +41,16 @@ class PushToLibraryProject(load.ProductLoaderPlugin):
|
|||
"push_to_project",
|
||||
"main.py"
|
||||
)
|
||||
project_name = filtered_contexts[0]["project"]["name"]
|
||||
|
||||
project_name = context["project"]["name"]
|
||||
version_id = context["version"]["id"]
|
||||
version_ids = {
|
||||
context["version"]["id"]
|
||||
for context in filtered_contexts
|
||||
}
|
||||
|
||||
args = get_ayon_launcher_args(
|
||||
"run",
|
||||
push_tool_script_path,
|
||||
"--project", project_name,
|
||||
"--version", version_id
|
||||
"--versions", ",".join(version_ids)
|
||||
)
|
||||
run_detached_process(args)
|
||||
|
|
@ -39,7 +39,7 @@ class CollectAudio(pyblish.api.ContextPlugin):
|
|||
"blender",
|
||||
"houdini",
|
||||
"max",
|
||||
"circuit",
|
||||
"batchdelivery",
|
||||
]
|
||||
settings_category = "core"
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@
|
|||
|
||||
"""
|
||||
import os
|
||||
import collections
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from ayon_core.host import IPublishHost
|
||||
from ayon_core.pipeline import registered_host
|
||||
from ayon_core.pipeline.create import CreateContext
|
||||
from ayon_core.pipeline.create import CreateContext, ParentFlags
|
||||
|
||||
|
||||
class CollectFromCreateContext(pyblish.api.ContextPlugin):
|
||||
|
|
@ -36,18 +38,51 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin):
|
|||
if project_name:
|
||||
context.data["projectName"] = project_name
|
||||
|
||||
# Separate root instances and parented instances
|
||||
instances_by_parent_id = collections.defaultdict(list)
|
||||
root_instances = []
|
||||
for created_instance in create_context.instances:
|
||||
parent_id = created_instance.parent_instance_id
|
||||
if parent_id is None:
|
||||
root_instances.append(created_instance)
|
||||
else:
|
||||
instances_by_parent_id[parent_id].append(created_instance)
|
||||
|
||||
# Traverse instances from top to bottom
|
||||
# - All instances without an existing parent are automatically
|
||||
# eliminated
|
||||
filtered_instances = []
|
||||
_queue = collections.deque()
|
||||
_queue.append((root_instances, True))
|
||||
while _queue:
|
||||
created_instances, parent_is_active = _queue.popleft()
|
||||
for created_instance in created_instances:
|
||||
is_active = created_instance["active"]
|
||||
# Use a parent's active state if parent flags defines that
|
||||
if (
|
||||
created_instance.parent_flags & ParentFlags.share_active
|
||||
and is_active
|
||||
):
|
||||
is_active = parent_is_active
|
||||
|
||||
if is_active:
|
||||
filtered_instances.append(created_instance)
|
||||
|
||||
children = instances_by_parent_id[created_instance.id]
|
||||
if children:
|
||||
_queue.append((children, is_active))
|
||||
|
||||
for created_instance in filtered_instances:
|
||||
instance_data = created_instance.data_to_store()
|
||||
if instance_data["active"]:
|
||||
thumbnail_path = thumbnail_paths_by_instance_id.get(
|
||||
created_instance.id
|
||||
)
|
||||
self.create_instance(
|
||||
context,
|
||||
instance_data,
|
||||
created_instance.transient_data,
|
||||
thumbnail_path
|
||||
)
|
||||
thumbnail_path = thumbnail_paths_by_instance_id.get(
|
||||
created_instance.id
|
||||
)
|
||||
self.create_instance(
|
||||
context,
|
||||
instance_data,
|
||||
created_instance.transient_data,
|
||||
thumbnail_path
|
||||
)
|
||||
|
||||
# Update global data to context
|
||||
context.data.update(create_context.context_data_to_store())
|
||||
|
|
|
|||
|
|
@ -8,13 +8,7 @@ This module contains a unified plugin that handles:
|
|||
|
||||
from pprint import pformat
|
||||
|
||||
import opentimelineio as otio
|
||||
import pyblish.api
|
||||
from ayon_core.pipeline.editorial import (
|
||||
get_media_range_with_retimes,
|
||||
otio_range_to_frame_range,
|
||||
otio_range_with_handles,
|
||||
)
|
||||
|
||||
|
||||
def validate_otio_clip(instance, logger):
|
||||
|
|
@ -74,6 +68,8 @@ class CollectOtioRanges(pyblish.api.InstancePlugin):
|
|||
if not validate_otio_clip(instance, self.log):
|
||||
return
|
||||
|
||||
import opentimelineio as otio
|
||||
|
||||
otio_clip = instance.data["otioClip"]
|
||||
|
||||
# Collect timeline ranges if workfile start frame is available
|
||||
|
|
@ -100,6 +96,11 @@ class CollectOtioRanges(pyblish.api.InstancePlugin):
|
|||
|
||||
def _collect_timeline_ranges(self, instance, otio_clip):
|
||||
"""Collect basic timeline frame ranges."""
|
||||
from ayon_core.pipeline.editorial import (
|
||||
otio_range_to_frame_range,
|
||||
otio_range_with_handles,
|
||||
)
|
||||
|
||||
workfile_start = instance.data["workfileFrameStart"]
|
||||
|
||||
# Get timeline ranges
|
||||
|
|
@ -129,6 +130,8 @@ class CollectOtioRanges(pyblish.api.InstancePlugin):
|
|||
|
||||
def _collect_source_ranges(self, instance, otio_clip):
|
||||
"""Collect source media frame ranges."""
|
||||
import opentimelineio as otio
|
||||
|
||||
# Get source ranges
|
||||
otio_src_range = otio_clip.source_range
|
||||
otio_available_range = otio_clip.available_range()
|
||||
|
|
@ -178,6 +181,8 @@ class CollectOtioRanges(pyblish.api.InstancePlugin):
|
|||
|
||||
def _collect_retimed_ranges(self, instance, otio_clip):
|
||||
"""Handle retimed clip frame ranges."""
|
||||
from ayon_core.pipeline.editorial import get_media_range_with_retimes
|
||||
|
||||
retimed_attributes = get_media_range_with_retimes(otio_clip, 0, 0)
|
||||
self.log.debug(f"Retimed attributes: {retimed_attributes}")
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class ExtractBurnin(publish.Extractor):
|
|||
"max",
|
||||
"blender",
|
||||
"unreal",
|
||||
"circuit",
|
||||
"batchdelivery",
|
||||
]
|
||||
settings_category = "core"
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from ayon_core.lib import (
|
|||
is_oiio_supported,
|
||||
)
|
||||
from ayon_core.lib.transcoding import (
|
||||
convert_colorspace,
|
||||
oiio_color_convert,
|
||||
)
|
||||
|
||||
from ayon_core.lib.profiles_filtering import filter_profiles
|
||||
|
|
@ -87,6 +87,14 @@ class ExtractOIIOTranscode(publish.Extractor):
|
|||
new_representations = []
|
||||
repres = instance.data["representations"]
|
||||
for idx, repre in enumerate(list(repres)):
|
||||
# target space, display and view might be defined upstream
|
||||
# TODO: address https://github.com/ynput/ayon-core/pull/1268#discussion_r2156555474
|
||||
# Implement upstream logic to handle target_colorspace,
|
||||
# target_display, target_view in other DCCs
|
||||
target_colorspace = False
|
||||
target_display = instance.data.get("colorspaceDisplay")
|
||||
target_view = instance.data.get("colorspaceView")
|
||||
|
||||
self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"]))
|
||||
if not self._repre_is_valid(repre):
|
||||
continue
|
||||
|
|
@ -96,6 +104,8 @@ class ExtractOIIOTranscode(publish.Extractor):
|
|||
|
||||
colorspace_data = repre["colorspaceData"]
|
||||
source_colorspace = colorspace_data["colorspace"]
|
||||
source_display = colorspace_data.get("display")
|
||||
source_view = colorspace_data.get("view")
|
||||
config_path = colorspace_data.get("config", {}).get("path")
|
||||
if not config_path or not os.path.exists(config_path):
|
||||
self.log.warning("Config file doesn't exist, skipping")
|
||||
|
|
@ -126,7 +136,6 @@ class ExtractOIIOTranscode(publish.Extractor):
|
|||
|
||||
transcoding_type = output_def["transcoding_type"]
|
||||
|
||||
target_colorspace = view = display = None
|
||||
# NOTE: we use colorspace_data as the fallback values for
|
||||
# the target colorspace.
|
||||
if transcoding_type == "colorspace":
|
||||
|
|
@ -138,18 +147,20 @@ class ExtractOIIOTranscode(publish.Extractor):
|
|||
colorspace_data.get("colorspace"))
|
||||
elif transcoding_type == "display_view":
|
||||
display_view = output_def["display_view"]
|
||||
view = display_view["view"] or colorspace_data.get("view")
|
||||
display = (
|
||||
target_view = (
|
||||
display_view["view"]
|
||||
or colorspace_data.get("view"))
|
||||
target_display = (
|
||||
display_view["display"]
|
||||
or colorspace_data.get("display")
|
||||
)
|
||||
|
||||
# both could be already collected by DCC,
|
||||
# but could be overwritten when transcoding
|
||||
if view:
|
||||
new_repre["colorspaceData"]["view"] = view
|
||||
if display:
|
||||
new_repre["colorspaceData"]["display"] = display
|
||||
if target_view:
|
||||
new_repre["colorspaceData"]["view"] = target_view
|
||||
if target_display:
|
||||
new_repre["colorspaceData"]["display"] = target_display
|
||||
if target_colorspace:
|
||||
new_repre["colorspaceData"]["colorspace"] = \
|
||||
target_colorspace
|
||||
|
|
@ -168,16 +179,18 @@ class ExtractOIIOTranscode(publish.Extractor):
|
|||
new_staging_dir,
|
||||
output_extension)
|
||||
|
||||
convert_colorspace(
|
||||
input_path,
|
||||
output_path,
|
||||
config_path,
|
||||
source_colorspace,
|
||||
target_colorspace,
|
||||
view,
|
||||
display,
|
||||
additional_command_args,
|
||||
self.log
|
||||
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
|
||||
)
|
||||
|
||||
# cleanup temporary transcoded files
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ from ayon_core.lib import (
|
|||
get_ffmpeg_tool_args,
|
||||
run_subprocess
|
||||
)
|
||||
from ayon_core.pipeline import editorial
|
||||
|
||||
|
||||
class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
|
||||
|
|
@ -159,6 +158,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
|
|||
"""
|
||||
# Not all hosts can import this module.
|
||||
import opentimelineio as otio
|
||||
from ayon_core.pipeline.editorial import OTIO_EPSILON
|
||||
|
||||
output = []
|
||||
# go trough all audio tracks
|
||||
|
|
@ -177,7 +177,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
|
|||
# Avoid rounding issue on media available range.
|
||||
if clip_start.almost_equal(
|
||||
conformed_av_start,
|
||||
editorial.OTIO_EPSILON
|
||||
OTIO_EPSILON
|
||||
):
|
||||
conformed_av_start = clip_start
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ from ayon_core.lib import (
|
|||
)
|
||||
from ayon_core.pipeline import (
|
||||
KnownPublishError,
|
||||
editorial,
|
||||
publish,
|
||||
)
|
||||
|
||||
|
|
@ -359,6 +358,7 @@ class ExtractOTIOReview(
|
|||
import opentimelineio as otio
|
||||
from ayon_core.pipeline.editorial import (
|
||||
trim_media_range,
|
||||
OTIO_EPSILON,
|
||||
)
|
||||
|
||||
def _round_to_frame(rational_time):
|
||||
|
|
@ -380,7 +380,7 @@ class ExtractOTIOReview(
|
|||
# Avoid rounding issue on media available range.
|
||||
if start.almost_equal(
|
||||
avl_start,
|
||||
editorial.OTIO_EPSILON
|
||||
OTIO_EPSILON
|
||||
):
|
||||
avl_start = start
|
||||
|
||||
|
|
@ -406,7 +406,7 @@ class ExtractOTIOReview(
|
|||
# Avoid rounding issue on media available range.
|
||||
if end_point.almost_equal(
|
||||
avl_end_point,
|
||||
editorial.OTIO_EPSILON
|
||||
OTIO_EPSILON
|
||||
):
|
||||
avl_end_point = end_point
|
||||
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
"aftereffects",
|
||||
"flame",
|
||||
"unreal",
|
||||
"circuit",
|
||||
"batchdelivery",
|
||||
"photoshop"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from ayon_core.lib import (
|
|||
path_to_subprocess_arg,
|
||||
run_subprocess,
|
||||
)
|
||||
from ayon_core.lib.transcoding import convert_colorspace
|
||||
from ayon_core.lib.transcoding import oiio_color_convert
|
||||
|
||||
from ayon_core.lib.transcoding import VIDEO_EXTENSIONS, IMAGE_EXTENSIONS
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
"photoshop",
|
||||
"unreal",
|
||||
"houdini",
|
||||
"circuit",
|
||||
"batchdelivery",
|
||||
]
|
||||
settings_category = "core"
|
||||
enabled = False
|
||||
|
|
@ -433,13 +433,15 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
oiio_default_view = display_and_view["view"]
|
||||
|
||||
try:
|
||||
convert_colorspace(
|
||||
oiio_color_convert(
|
||||
src_path,
|
||||
dst_path,
|
||||
colorspace_data["config"]["path"],
|
||||
colorspace_data["colorspace"],
|
||||
display=repre_display or oiio_default_display,
|
||||
view=repre_view or oiio_default_view,
|
||||
source_display=colorspace_data.get("display"),
|
||||
source_view=colorspace_data.get("view"),
|
||||
target_display=repre_display or oiio_default_display,
|
||||
target_view=repre_view or oiio_default_view,
|
||||
target_colorspace=oiio_default_colorspace,
|
||||
additional_command_args=resolution_arg,
|
||||
logger=self.log,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import collections
|
|||
import copy
|
||||
import time
|
||||
import warnings
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import ayon_api
|
||||
|
||||
|
|
@ -36,6 +37,37 @@ class CacheItem:
|
|||
return time.time() > self._outdate_time
|
||||
|
||||
|
||||
def _get_addons_settings(
|
||||
studio_bundle_name,
|
||||
project_bundle_name,
|
||||
variant,
|
||||
project_name=None,
|
||||
):
|
||||
"""Modified version of `ayon_api.get_addons_settings` function."""
|
||||
query_values = {
|
||||
key: value
|
||||
for key, value in (
|
||||
("bundle_name", studio_bundle_name),
|
||||
("variant", variant),
|
||||
("project_name", project_name),
|
||||
)
|
||||
if value
|
||||
}
|
||||
if project_bundle_name != studio_bundle_name:
|
||||
query_values["project_bundle_name"] = project_bundle_name
|
||||
|
||||
site_id = ayon_api.get_site_id()
|
||||
if site_id:
|
||||
query_values["site_id"] = site_id
|
||||
|
||||
response = ayon_api.get(f"settings?{urlencode(query_values)}")
|
||||
response.raise_for_status()
|
||||
return {
|
||||
addon["name"]: addon["settings"]
|
||||
for addon in response.data["addons"]
|
||||
}
|
||||
|
||||
|
||||
class _AyonSettingsCache:
|
||||
use_bundles = None
|
||||
variant = None
|
||||
|
|
@ -68,53 +100,70 @@ class _AyonSettingsCache:
|
|||
return _AyonSettingsCache.variant
|
||||
|
||||
@classmethod
|
||||
def _get_bundle_name(cls):
|
||||
def _get_studio_bundle_name(cls):
|
||||
bundle_name = os.environ.get("AYON_STUDIO_BUNDLE_NAME")
|
||||
if bundle_name:
|
||||
return bundle_name
|
||||
return os.environ["AYON_BUNDLE_NAME"]
|
||||
|
||||
@classmethod
|
||||
def _get_project_bundle_name(cls):
|
||||
return os.environ["AYON_BUNDLE_NAME"]
|
||||
|
||||
@classmethod
|
||||
def get_value_by_project(cls, project_name):
|
||||
cache_item = _AyonSettingsCache.cache_by_project_name[project_name]
|
||||
if cache_item.is_outdated:
|
||||
if cls._use_bundles():
|
||||
value = ayon_api.get_addons_settings(
|
||||
bundle_name=cls._get_bundle_name(),
|
||||
cache_item.update_value(
|
||||
_get_addons_settings(
|
||||
studio_bundle_name=cls._get_studio_bundle_name(),
|
||||
project_bundle_name=cls._get_project_bundle_name(),
|
||||
project_name=project_name,
|
||||
variant=cls._get_variant()
|
||||
variant=cls._get_variant(),
|
||||
)
|
||||
else:
|
||||
value = ayon_api.get_addons_settings(project_name)
|
||||
cache_item.update_value(value)
|
||||
)
|
||||
return cache_item.get_value()
|
||||
|
||||
@classmethod
|
||||
def _get_addon_versions_from_bundle(cls):
|
||||
expected_bundle = cls._get_bundle_name()
|
||||
studio_bundle_name = cls._get_studio_bundle_name()
|
||||
project_bundle_name = cls._get_project_bundle_name()
|
||||
bundles = ayon_api.get_bundles()["bundles"]
|
||||
bundle = next(
|
||||
project_bundle = next(
|
||||
(
|
||||
bundle
|
||||
for bundle in bundles
|
||||
if bundle["name"] == expected_bundle
|
||||
if bundle["name"] == project_bundle_name
|
||||
),
|
||||
None
|
||||
)
|
||||
if bundle is not None:
|
||||
return bundle["addons"]
|
||||
studio_bundle = None
|
||||
if studio_bundle_name and project_bundle_name != studio_bundle_name:
|
||||
studio_bundle = next(
|
||||
(
|
||||
bundle
|
||||
for bundle in bundles
|
||||
if bundle["name"] == studio_bundle_name
|
||||
),
|
||||
None
|
||||
)
|
||||
|
||||
if studio_bundle and project_bundle:
|
||||
addons = copy.deepcopy(studio_bundle["addons"])
|
||||
addons.update(project_bundle["addons"])
|
||||
project_bundle["addons"] = addons
|
||||
|
||||
if project_bundle is not None:
|
||||
return project_bundle["addons"]
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def get_addon_versions(cls):
|
||||
cache_item = _AyonSettingsCache.addon_versions
|
||||
if cache_item.is_outdated:
|
||||
if cls._use_bundles():
|
||||
addons = cls._get_addon_versions_from_bundle()
|
||||
else:
|
||||
settings_data = ayon_api.get_addons_settings(
|
||||
only_values=False,
|
||||
variant=cls._get_variant()
|
||||
)
|
||||
addons = settings_data["versions"]
|
||||
cache_item.update_value(addons)
|
||||
cache_item.update_value(
|
||||
cls._get_addon_versions_from_bundle()
|
||||
)
|
||||
|
||||
return cache_item.get_value()
|
||||
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@
|
|||
},
|
||||
"publisher": {
|
||||
"error": "#AA5050",
|
||||
"disabled": "#5b6779",
|
||||
"crash": "#FF6432",
|
||||
"success": "#458056",
|
||||
"warning": "#ffc671",
|
||||
|
|
|
|||
|
|
@ -1153,6 +1153,10 @@ PixmapButton:disabled {
|
|||
color: {color:publisher:error};
|
||||
}
|
||||
|
||||
#ListViewProductName[state="disabled"] {
|
||||
color: {color:publisher:disabled};
|
||||
}
|
||||
|
||||
#PublishInfoFrame {
|
||||
background: {color:bg};
|
||||
border-radius: 0.3em;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from .projects import (
|
|||
PROJECTS_MODEL_SENDER,
|
||||
FolderTypeItem,
|
||||
TaskTypeItem,
|
||||
ProductTypeIconMapping,
|
||||
)
|
||||
from .hierarchy import (
|
||||
FolderItem,
|
||||
|
|
@ -34,6 +35,7 @@ __all__ = (
|
|||
"PROJECTS_MODEL_SENDER",
|
||||
"FolderTypeItem",
|
||||
"TaskTypeItem",
|
||||
"ProductTypeIconMapping",
|
||||
|
||||
"FolderItem",
|
||||
"TaskItem",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import contextlib
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Any
|
||||
from typing import Any, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
import ayon_api
|
||||
|
|
@ -51,7 +51,7 @@ class StatusItem:
|
|||
self.icon: str = icon
|
||||
self.state: str = state
|
||||
|
||||
def to_data(self) -> Dict[str, Any]:
|
||||
def to_data(self) -> dict[str, Any]:
|
||||
return {
|
||||
"name": self.name,
|
||||
"color": self.color,
|
||||
|
|
@ -125,16 +125,24 @@ class TaskTypeItem:
|
|||
icon (str): Icon name in MaterialIcons ("fiber_new").
|
||||
|
||||
"""
|
||||
def __init__(self, name, short, icon):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
short: str,
|
||||
icon: str,
|
||||
color: Optional[str],
|
||||
):
|
||||
self.name = name
|
||||
self.short = short
|
||||
self.icon = icon
|
||||
self.color = color
|
||||
|
||||
def to_data(self):
|
||||
return {
|
||||
"name": self.name,
|
||||
"short": self.short,
|
||||
"icon": self.icon,
|
||||
"color": self.color,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
|
@ -147,6 +155,7 @@ class TaskTypeItem:
|
|||
name=task_type_data["name"],
|
||||
short=task_type_data["shortName"],
|
||||
icon=task_type_data["icon"],
|
||||
color=task_type_data.get("color"),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -218,6 +227,54 @@ class ProjectItem:
|
|||
return cls(**data)
|
||||
|
||||
|
||||
class ProductTypeIconMapping:
|
||||
def __init__(
|
||||
self,
|
||||
default: Optional[dict[str, str]] = None,
|
||||
definitions: Optional[list[dict[str, str]]] = None,
|
||||
):
|
||||
self._default = default or {}
|
||||
self._definitions = definitions or []
|
||||
|
||||
self._default_def = None
|
||||
self._definitions_by_name = None
|
||||
|
||||
def get_icon(
|
||||
self,
|
||||
product_base_type: Optional[str] = None,
|
||||
product_type: Optional[str] = None,
|
||||
) -> dict[str, str]:
|
||||
defs = self._get_defs_by_name()
|
||||
icon = defs.get(product_type)
|
||||
if icon is None:
|
||||
icon = defs.get(product_base_type)
|
||||
if icon is None:
|
||||
icon = self._get_default_def()
|
||||
return icon.copy()
|
||||
|
||||
def _get_default_def(self) -> dict[str, str]:
|
||||
if self._default_def is None:
|
||||
self._default_def = {
|
||||
"type": "material-symbols",
|
||||
"name": self._default.get("icon", "deployed_code"),
|
||||
"color": self._default.get("color", "#cccccc"),
|
||||
}
|
||||
|
||||
return self._default_def
|
||||
|
||||
def _get_defs_by_name(self) -> dict[str, dict[str, str]]:
|
||||
if self._definitions_by_name is None:
|
||||
self._definitions_by_name = {
|
||||
product_base_type_def["name"]: {
|
||||
"type": "material-symbols",
|
||||
"name": product_base_type_def.get("icon", "deployed_code"),
|
||||
"color": product_base_type_def.get("color", "#cccccc"),
|
||||
}
|
||||
for product_base_type_def in self._definitions
|
||||
}
|
||||
return self._definitions_by_name
|
||||
|
||||
|
||||
def _get_project_items_from_entitiy(
|
||||
projects: list[dict[str, Any]]
|
||||
) -> list[ProjectItem]:
|
||||
|
|
@ -242,6 +299,9 @@ class ProjectsModel(object):
|
|||
self._projects_by_name = NestedCacheItem(
|
||||
levels=1, default_factory=list
|
||||
)
|
||||
self._product_type_icons_mapping = NestedCacheItem(
|
||||
levels=1, default_factory=ProductTypeIconMapping
|
||||
)
|
||||
self._project_statuses_cache = {}
|
||||
self._folder_types_cache = {}
|
||||
self._task_types_cache = {}
|
||||
|
|
@ -255,6 +315,7 @@ class ProjectsModel(object):
|
|||
self._task_types_cache = {}
|
||||
self._projects_cache.reset()
|
||||
self._projects_by_name.reset()
|
||||
self._product_type_icons_mapping.reset()
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh project items.
|
||||
|
|
@ -390,6 +451,27 @@ class ProjectsModel(object):
|
|||
self._task_type_items_getter,
|
||||
)
|
||||
|
||||
def get_product_type_icons_mapping(
|
||||
self, project_name: Optional[str]
|
||||
) -> ProductTypeIconMapping:
|
||||
cache = self._product_type_icons_mapping[project_name]
|
||||
if cache.is_valid:
|
||||
return cache.get_data()
|
||||
|
||||
project_entity = self.get_project_entity(project_name)
|
||||
icons_mapping = ProductTypeIconMapping()
|
||||
if project_entity:
|
||||
product_base_types = (
|
||||
project_entity["config"].get("productBaseTypes", {})
|
||||
)
|
||||
icons_mapping = ProductTypeIconMapping(
|
||||
product_base_types.get("default"),
|
||||
product_base_types.get("definitions")
|
||||
)
|
||||
|
||||
cache.update_data(icons_mapping)
|
||||
return icons_mapping
|
||||
|
||||
def _get_project_items(
|
||||
self, project_name, sender, item_type, cache_obj, getter
|
||||
):
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
from .window import (
|
||||
show,
|
||||
CreatorWindow
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"show",
|
||||
"CreatorWindow"
|
||||
)
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
from qtpy import QtCore
|
||||
|
||||
|
||||
PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 1
|
||||
ITEM_ID_ROLE = QtCore.Qt.UserRole + 2
|
||||
|
||||
SEPARATOR = "---"
|
||||
SEPARATORS = {"---", "---separator---"}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
import uuid
|
||||
from qtpy import QtGui, QtCore
|
||||
|
||||
from ayon_core.pipeline import discover_legacy_creator_plugins
|
||||
|
||||
from . constants import (
|
||||
PRODUCT_TYPE_ROLE,
|
||||
ITEM_ID_ROLE
|
||||
)
|
||||
|
||||
|
||||
class CreatorsModel(QtGui.QStandardItemModel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CreatorsModel, self).__init__(*args, **kwargs)
|
||||
|
||||
self._creators_by_id = {}
|
||||
|
||||
def reset(self):
|
||||
# TODO change to refresh when clearing is not needed
|
||||
self.clear()
|
||||
self._creators_by_id = {}
|
||||
|
||||
items = []
|
||||
creators = discover_legacy_creator_plugins()
|
||||
for creator in creators:
|
||||
if not creator.enabled:
|
||||
continue
|
||||
item_id = str(uuid.uuid4())
|
||||
self._creators_by_id[item_id] = creator
|
||||
|
||||
label = creator.label or creator.product_type
|
||||
item = QtGui.QStandardItem(label)
|
||||
item.setEditable(False)
|
||||
item.setData(item_id, ITEM_ID_ROLE)
|
||||
item.setData(creator.product_type, PRODUCT_TYPE_ROLE)
|
||||
items.append(item)
|
||||
|
||||
if not items:
|
||||
item = QtGui.QStandardItem("No registered create plugins")
|
||||
item.setEnabled(False)
|
||||
item.setData(False, QtCore.Qt.ItemIsEnabled)
|
||||
items.append(item)
|
||||
|
||||
items.sort(key=lambda item: item.text())
|
||||
self.invisibleRootItem().appendRows(items)
|
||||
|
||||
def get_creator_by_id(self, item_id):
|
||||
return self._creators_by_id.get(item_id)
|
||||
|
||||
def get_indexes_by_product_type(self, product_type):
|
||||
indexes = []
|
||||
for row in range(self.rowCount()):
|
||||
index = self.index(row, 0)
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
creator_plugin = self._creators_by_id.get(item_id)
|
||||
if creator_plugin and (
|
||||
creator_plugin.label.lower() == product_type.lower()
|
||||
or creator_plugin.product_type.lower() == product_type.lower()
|
||||
):
|
||||
indexes.append(index)
|
||||
return indexes
|
||||
|
|
@ -1,275 +0,0 @@
|
|||
import re
|
||||
import inspect
|
||||
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
import qtawesome
|
||||
|
||||
from ayon_core.pipeline.create import PRODUCT_NAME_ALLOWED_SYMBOLS
|
||||
from ayon_core.tools.utils import ErrorMessageBox
|
||||
|
||||
if hasattr(QtGui, "QRegularExpressionValidator"):
|
||||
RegularExpressionValidatorClass = QtGui.QRegularExpressionValidator
|
||||
RegularExpressionClass = QtCore.QRegularExpression
|
||||
else:
|
||||
RegularExpressionValidatorClass = QtGui.QRegExpValidator
|
||||
RegularExpressionClass = QtCore.QRegExp
|
||||
|
||||
|
||||
class CreateErrorMessageBox(ErrorMessageBox):
|
||||
def __init__(
|
||||
self,
|
||||
product_type,
|
||||
product_name,
|
||||
folder_path,
|
||||
exc_msg,
|
||||
formatted_traceback,
|
||||
parent
|
||||
):
|
||||
self._product_type = product_type
|
||||
self._product_name = product_name
|
||||
self._folder_path = folder_path
|
||||
self._exc_msg = exc_msg
|
||||
self._formatted_traceback = formatted_traceback
|
||||
super(CreateErrorMessageBox, self).__init__("Creation failed", parent)
|
||||
|
||||
def _create_top_widget(self, parent_widget):
|
||||
label_widget = QtWidgets.QLabel(parent_widget)
|
||||
label_widget.setText(
|
||||
"<span style='font-size:18pt;'>Failed to create</span>"
|
||||
)
|
||||
return label_widget
|
||||
|
||||
def _get_report_data(self):
|
||||
report_message = (
|
||||
"Failed to create Product: \"{product_name}\""
|
||||
" Type: \"{product_type}\""
|
||||
" in Folder: \"{folder_path}\""
|
||||
"\n\nError: {message}"
|
||||
).format(
|
||||
product_name=self._product_name,
|
||||
product_type=self._product_type,
|
||||
folder_path=self._folder_path,
|
||||
message=self._exc_msg
|
||||
)
|
||||
if self._formatted_traceback:
|
||||
report_message += "\n\n{}".format(self._formatted_traceback)
|
||||
return [report_message]
|
||||
|
||||
def _create_content(self, content_layout):
|
||||
item_name_template = (
|
||||
"<span style='font-weight:bold;'>{}:</span> {{}}<br>"
|
||||
"<span style='font-weight:bold;'>{}:</span> {{}}<br>"
|
||||
"<span style='font-weight:bold;'>{}:</span> {{}}<br>"
|
||||
).format(
|
||||
"Product type",
|
||||
"Product name",
|
||||
"Folder"
|
||||
)
|
||||
exc_msg_template = "<span style='font-weight:bold'>{}</span>"
|
||||
|
||||
line = self._create_line()
|
||||
content_layout.addWidget(line)
|
||||
|
||||
item_name_widget = QtWidgets.QLabel(self)
|
||||
item_name_widget.setText(
|
||||
item_name_template.format(
|
||||
self._product_type, self._product_name, self._folder_path
|
||||
)
|
||||
)
|
||||
content_layout.addWidget(item_name_widget)
|
||||
|
||||
message_label_widget = QtWidgets.QLabel(self)
|
||||
message_label_widget.setText(
|
||||
exc_msg_template.format(self.convert_text_for_html(self._exc_msg))
|
||||
)
|
||||
content_layout.addWidget(message_label_widget)
|
||||
|
||||
if self._formatted_traceback:
|
||||
line_widget = self._create_line()
|
||||
tb_widget = self._create_traceback_widget(
|
||||
self._formatted_traceback
|
||||
)
|
||||
content_layout.addWidget(line_widget)
|
||||
content_layout.addWidget(tb_widget)
|
||||
|
||||
|
||||
class ProductNameValidator(RegularExpressionValidatorClass):
|
||||
invalid = QtCore.Signal(set)
|
||||
pattern = "^[{}]*$".format(PRODUCT_NAME_ALLOWED_SYMBOLS)
|
||||
|
||||
def __init__(self):
|
||||
reg = RegularExpressionClass(self.pattern)
|
||||
super(ProductNameValidator, self).__init__(reg)
|
||||
|
||||
def validate(self, text, pos):
|
||||
results = super(ProductNameValidator, self).validate(text, pos)
|
||||
if results[0] == RegularExpressionValidatorClass.Invalid:
|
||||
self.invalid.emit(self.invalid_chars(text))
|
||||
return results
|
||||
|
||||
def invalid_chars(self, text):
|
||||
invalid = set()
|
||||
re_valid = re.compile(self.pattern)
|
||||
for char in text:
|
||||
if char == " ":
|
||||
invalid.add("' '")
|
||||
continue
|
||||
if not re_valid.match(char):
|
||||
invalid.add(char)
|
||||
return invalid
|
||||
|
||||
|
||||
class VariantLineEdit(QtWidgets.QLineEdit):
|
||||
report = QtCore.Signal(str)
|
||||
colors = {
|
||||
"empty": (QtGui.QColor("#78879b"), ""),
|
||||
"exists": (QtGui.QColor("#4E76BB"), "border-color: #4E76BB;"),
|
||||
"new": (QtGui.QColor("#7AAB8F"), "border-color: #7AAB8F;"),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(VariantLineEdit, self).__init__(*args, **kwargs)
|
||||
|
||||
validator = ProductNameValidator()
|
||||
self.setValidator(validator)
|
||||
self.setToolTip("Only alphanumeric characters (A-Z a-z 0-9), "
|
||||
"'_' and '.' are allowed.")
|
||||
|
||||
self._status_color = self.colors["empty"][0]
|
||||
|
||||
anim = QtCore.QPropertyAnimation()
|
||||
anim.setTargetObject(self)
|
||||
anim.setPropertyName(b"status_color")
|
||||
anim.setEasingCurve(QtCore.QEasingCurve.InCubic)
|
||||
anim.setDuration(300)
|
||||
anim.setStartValue(QtGui.QColor("#C84747")) # `Invalid` status color
|
||||
self.animation = anim
|
||||
|
||||
validator.invalid.connect(self.on_invalid)
|
||||
|
||||
def on_invalid(self, invalid):
|
||||
message = "Invalid character: %s" % ", ".join(invalid)
|
||||
self.report.emit(message)
|
||||
self.animation.stop()
|
||||
self.animation.start()
|
||||
|
||||
def as_empty(self):
|
||||
self._set_border("empty")
|
||||
self.report.emit("Empty product name ..")
|
||||
|
||||
def as_exists(self):
|
||||
self._set_border("exists")
|
||||
self.report.emit("Existing product, appending next version.")
|
||||
|
||||
def as_new(self):
|
||||
self._set_border("new")
|
||||
self.report.emit("New product, creating first version.")
|
||||
|
||||
def _set_border(self, status):
|
||||
qcolor, style = self.colors[status]
|
||||
self.animation.setEndValue(qcolor)
|
||||
self.setStyleSheet(style)
|
||||
|
||||
def _get_status_color(self):
|
||||
return self._status_color
|
||||
|
||||
def _set_status_color(self, color):
|
||||
self._status_color = color
|
||||
self.setStyleSheet("border-color: %s;" % color.name())
|
||||
|
||||
status_color = QtCore.Property(
|
||||
QtGui.QColor, _get_status_color, _set_status_color
|
||||
)
|
||||
|
||||
|
||||
class ProductTypeDescriptionWidget(QtWidgets.QWidget):
|
||||
"""A product type description widget.
|
||||
|
||||
Shows a product type icon, name and a help description.
|
||||
Used in creator header.
|
||||
|
||||
_______________________
|
||||
| ____ |
|
||||
| |icon| PRODUCT TYPE |
|
||||
| |____| help |
|
||||
|_______________________|
|
||||
|
||||
"""
|
||||
|
||||
SIZE = 35
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ProductTypeDescriptionWidget, self).__init__(parent=parent)
|
||||
|
||||
icon_label = QtWidgets.QLabel(self)
|
||||
icon_label.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.Maximum,
|
||||
QtWidgets.QSizePolicy.Maximum
|
||||
)
|
||||
|
||||
# Add 4 pixel padding to avoid icon being cut off
|
||||
icon_label.setFixedWidth(self.SIZE + 4)
|
||||
icon_label.setFixedHeight(self.SIZE + 4)
|
||||
|
||||
label_layout = QtWidgets.QVBoxLayout()
|
||||
label_layout.setSpacing(0)
|
||||
|
||||
product_type_label = QtWidgets.QLabel(self)
|
||||
product_type_label.setObjectName("CreatorProductTypeLabel")
|
||||
product_type_label.setAlignment(
|
||||
QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft
|
||||
)
|
||||
|
||||
help_label = QtWidgets.QLabel(self)
|
||||
help_label.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft)
|
||||
|
||||
label_layout.addWidget(product_type_label)
|
||||
label_layout.addWidget(help_label)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(5)
|
||||
layout.addWidget(icon_label)
|
||||
layout.addLayout(label_layout)
|
||||
|
||||
self._help_label = help_label
|
||||
self._product_type_label = product_type_label
|
||||
self._icon_label = icon_label
|
||||
|
||||
def set_item(self, creator_plugin):
|
||||
"""Update elements to display information of a product type item.
|
||||
|
||||
Args:
|
||||
creator_plugin (dict): A product type item as registered with
|
||||
name, help and icon.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
if not creator_plugin:
|
||||
self._icon_label.setPixmap(None)
|
||||
self._product_type_label.setText("")
|
||||
self._help_label.setText("")
|
||||
return
|
||||
|
||||
# Support a font-awesome icon
|
||||
icon_name = getattr(creator_plugin, "icon", None) or "info-circle"
|
||||
try:
|
||||
icon = qtawesome.icon("fa.{}".format(icon_name), color="white")
|
||||
pixmap = icon.pixmap(self.SIZE, self.SIZE)
|
||||
except Exception:
|
||||
print("BUG: Couldn't load icon \"fa.{}\"".format(str(icon_name)))
|
||||
# Create transparent pixmap
|
||||
pixmap = QtGui.QPixmap()
|
||||
pixmap.fill(QtCore.Qt.transparent)
|
||||
pixmap = pixmap.scaled(self.SIZE, self.SIZE)
|
||||
|
||||
# Parse a clean line from the Creator's docstring
|
||||
docstring = inspect.getdoc(creator_plugin)
|
||||
creator_help = docstring.splitlines()[0] if docstring else ""
|
||||
|
||||
self._icon_label.setPixmap(pixmap)
|
||||
self._product_type_label.setText(creator_plugin.product_type)
|
||||
self._help_label.setText(creator_help)
|
||||
|
|
@ -1,508 +0,0 @@
|
|||
import sys
|
||||
import traceback
|
||||
import re
|
||||
|
||||
import ayon_api
|
||||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
from ayon_core import style
|
||||
from ayon_core.settings import get_current_project_settings
|
||||
from ayon_core.tools.utils.lib import qt_app_context
|
||||
from ayon_core.pipeline import (
|
||||
get_current_project_name,
|
||||
get_current_folder_path,
|
||||
get_current_task_name,
|
||||
)
|
||||
from ayon_core.pipeline.create import (
|
||||
PRODUCT_NAME_ALLOWED_SYMBOLS,
|
||||
legacy_create,
|
||||
CreatorError,
|
||||
)
|
||||
|
||||
from .model import CreatorsModel
|
||||
from .widgets import (
|
||||
CreateErrorMessageBox,
|
||||
VariantLineEdit,
|
||||
ProductTypeDescriptionWidget
|
||||
)
|
||||
from .constants import (
|
||||
ITEM_ID_ROLE,
|
||||
SEPARATOR,
|
||||
SEPARATORS
|
||||
)
|
||||
|
||||
module = sys.modules[__name__]
|
||||
module.window = None
|
||||
|
||||
|
||||
class CreatorWindow(QtWidgets.QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super(CreatorWindow, self).__init__(parent)
|
||||
self.setWindowTitle("Instance Creator")
|
||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
if not parent:
|
||||
self.setWindowFlags(
|
||||
self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint
|
||||
)
|
||||
|
||||
creator_info = ProductTypeDescriptionWidget(self)
|
||||
|
||||
creators_model = CreatorsModel()
|
||||
|
||||
creators_proxy = QtCore.QSortFilterProxyModel()
|
||||
creators_proxy.setSourceModel(creators_model)
|
||||
|
||||
creators_view = QtWidgets.QListView(self)
|
||||
creators_view.setObjectName("CreatorsView")
|
||||
creators_view.setModel(creators_proxy)
|
||||
|
||||
folder_path_input = QtWidgets.QLineEdit(self)
|
||||
variant_input = VariantLineEdit(self)
|
||||
product_name_input = QtWidgets.QLineEdit(self)
|
||||
product_name_input.setEnabled(False)
|
||||
|
||||
variants_btn = QtWidgets.QPushButton()
|
||||
variants_btn.setFixedWidth(18)
|
||||
variants_menu = QtWidgets.QMenu(variants_btn)
|
||||
variants_btn.setMenu(variants_menu)
|
||||
|
||||
name_layout = QtWidgets.QHBoxLayout()
|
||||
name_layout.addWidget(variant_input)
|
||||
name_layout.addWidget(variants_btn)
|
||||
name_layout.setSpacing(3)
|
||||
name_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
body_layout = QtWidgets.QVBoxLayout()
|
||||
body_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
body_layout.addWidget(creator_info, 0)
|
||||
body_layout.addWidget(QtWidgets.QLabel("Product type", self), 0)
|
||||
body_layout.addWidget(creators_view, 1)
|
||||
body_layout.addWidget(QtWidgets.QLabel("Folder path", self), 0)
|
||||
body_layout.addWidget(folder_path_input, 0)
|
||||
body_layout.addWidget(QtWidgets.QLabel("Product name", self), 0)
|
||||
body_layout.addLayout(name_layout, 0)
|
||||
body_layout.addWidget(product_name_input, 0)
|
||||
|
||||
useselection_chk = QtWidgets.QCheckBox("Use selection", self)
|
||||
useselection_chk.setCheckState(QtCore.Qt.Checked)
|
||||
|
||||
create_btn = QtWidgets.QPushButton("Create", self)
|
||||
# Need to store error_msg to prevent garbage collection
|
||||
msg_label = QtWidgets.QLabel(self)
|
||||
|
||||
footer_layout = QtWidgets.QVBoxLayout()
|
||||
footer_layout.addWidget(create_btn, 0)
|
||||
footer_layout.addWidget(msg_label, 0)
|
||||
footer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addLayout(body_layout, 1)
|
||||
layout.addWidget(useselection_chk, 0, QtCore.Qt.AlignLeft)
|
||||
layout.addLayout(footer_layout, 0)
|
||||
|
||||
msg_timer = QtCore.QTimer()
|
||||
msg_timer.setSingleShot(True)
|
||||
msg_timer.setInterval(5000)
|
||||
|
||||
validation_timer = QtCore.QTimer()
|
||||
validation_timer.setSingleShot(True)
|
||||
validation_timer.setInterval(300)
|
||||
|
||||
msg_timer.timeout.connect(self._on_msg_timer)
|
||||
validation_timer.timeout.connect(self._on_validation_timer)
|
||||
|
||||
create_btn.clicked.connect(self._on_create)
|
||||
variant_input.returnPressed.connect(self._on_create)
|
||||
variant_input.textChanged.connect(self._on_data_changed)
|
||||
variant_input.report.connect(self.echo)
|
||||
folder_path_input.textChanged.connect(self._on_data_changed)
|
||||
creators_view.selectionModel().currentChanged.connect(
|
||||
self._on_selection_changed
|
||||
)
|
||||
|
||||
# Store valid states and
|
||||
self._is_valid = False
|
||||
create_btn.setEnabled(self._is_valid)
|
||||
|
||||
self._first_show = True
|
||||
|
||||
# Message dialog when something goes wrong during creation
|
||||
self._message_dialog = None
|
||||
|
||||
self._creator_info = creator_info
|
||||
self._create_btn = create_btn
|
||||
self._useselection_chk = useselection_chk
|
||||
self._variant_input = variant_input
|
||||
self._product_name_input = product_name_input
|
||||
self._folder_path_input = folder_path_input
|
||||
|
||||
self._creators_model = creators_model
|
||||
self._creators_proxy = creators_proxy
|
||||
self._creators_view = creators_view
|
||||
|
||||
self._variants_btn = variants_btn
|
||||
self._variants_menu = variants_menu
|
||||
|
||||
self._msg_label = msg_label
|
||||
|
||||
self._validation_timer = validation_timer
|
||||
self._msg_timer = msg_timer
|
||||
|
||||
# Defaults
|
||||
self.resize(300, 500)
|
||||
variant_input.setFocus()
|
||||
|
||||
def _set_valid_state(self, valid):
|
||||
if self._is_valid == valid:
|
||||
return
|
||||
self._is_valid = valid
|
||||
self._create_btn.setEnabled(valid)
|
||||
|
||||
def _build_menu(self, default_names=None):
|
||||
"""Create optional predefined variants.
|
||||
|
||||
Args:
|
||||
default_names(list): all predefined names
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
if not default_names:
|
||||
default_names = []
|
||||
|
||||
menu = self._variants_menu
|
||||
button = self._variants_btn
|
||||
|
||||
# Get and destroy the action group
|
||||
group = button.findChild(QtWidgets.QActionGroup)
|
||||
if group:
|
||||
group.deleteLater()
|
||||
|
||||
state = any(default_names)
|
||||
button.setEnabled(state)
|
||||
if state is False:
|
||||
return
|
||||
|
||||
# Build new action group
|
||||
group = QtWidgets.QActionGroup(button)
|
||||
for name in default_names:
|
||||
if name in SEPARATORS:
|
||||
menu.addSeparator()
|
||||
continue
|
||||
action = group.addAction(name)
|
||||
menu.addAction(action)
|
||||
|
||||
group.triggered.connect(self._on_action_clicked)
|
||||
|
||||
def _on_action_clicked(self, action):
|
||||
self._variant_input.setText(action.text())
|
||||
|
||||
def _on_data_changed(self, *args):
|
||||
# Set invalid state until it's reconfirmed to be valid by the
|
||||
# scheduled callback so any form of creation is held back until
|
||||
# valid again
|
||||
self._set_valid_state(False)
|
||||
|
||||
self._validation_timer.start()
|
||||
|
||||
def _on_validation_timer(self):
|
||||
index = self._creators_view.currentIndex()
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
creator_plugin = self._creators_model.get_creator_by_id(item_id)
|
||||
user_input_text = self._variant_input.text()
|
||||
folder_path = self._folder_path_input.text()
|
||||
|
||||
# Early exit if no folder path
|
||||
if not folder_path:
|
||||
self._build_menu()
|
||||
self.echo("Folder is required ..")
|
||||
self._set_valid_state(False)
|
||||
return
|
||||
|
||||
project_name = get_current_project_name()
|
||||
folder_entity = None
|
||||
if creator_plugin:
|
||||
# Get the folder from the database which match with the name
|
||||
folder_entity = ayon_api.get_folder_by_path(
|
||||
project_name, folder_path, fields={"id"}
|
||||
)
|
||||
|
||||
# Get plugin
|
||||
if not folder_entity or not creator_plugin:
|
||||
self._build_menu()
|
||||
|
||||
if not creator_plugin:
|
||||
self.echo("No registered product types ..")
|
||||
else:
|
||||
self.echo("Folder '{}' not found ..".format(folder_path))
|
||||
self._set_valid_state(False)
|
||||
return
|
||||
|
||||
folder_id = folder_entity["id"]
|
||||
|
||||
task_name = get_current_task_name()
|
||||
task_entity = ayon_api.get_task_by_name(
|
||||
project_name, folder_id, task_name
|
||||
)
|
||||
|
||||
# Calculate product name with Creator plugin
|
||||
product_name = creator_plugin.get_product_name(
|
||||
project_name, folder_entity, task_entity, user_input_text
|
||||
)
|
||||
# Force replacement of prohibited symbols
|
||||
# QUESTION should Creator care about this and here should be only
|
||||
# validated with schema regex?
|
||||
|
||||
# Allow curly brackets in product name for dynamic keys
|
||||
curly_left = "__cbl__"
|
||||
curly_right = "__cbr__"
|
||||
tmp_product_name = (
|
||||
product_name
|
||||
.replace("{", curly_left)
|
||||
.replace("}", curly_right)
|
||||
)
|
||||
# Replace prohibited symbols
|
||||
tmp_product_name = re.sub(
|
||||
"[^{}]+".format(PRODUCT_NAME_ALLOWED_SYMBOLS),
|
||||
"",
|
||||
tmp_product_name
|
||||
)
|
||||
product_name = (
|
||||
tmp_product_name
|
||||
.replace(curly_left, "{")
|
||||
.replace(curly_right, "}")
|
||||
)
|
||||
self._product_name_input.setText(product_name)
|
||||
|
||||
# Get all products of the current folder
|
||||
product_entities = ayon_api.get_products(
|
||||
project_name, folder_ids={folder_id}, fields={"name"}
|
||||
)
|
||||
existing_product_names = {
|
||||
product_entity["name"]
|
||||
for product_entity in product_entities
|
||||
}
|
||||
existing_product_names_low = set(
|
||||
_name.lower()
|
||||
for _name in existing_product_names
|
||||
)
|
||||
|
||||
# Defaults to dropdown
|
||||
defaults = []
|
||||
# Check if Creator plugin has set defaults
|
||||
if (
|
||||
creator_plugin.defaults
|
||||
and isinstance(creator_plugin.defaults, (list, tuple, set))
|
||||
):
|
||||
defaults = list(creator_plugin.defaults)
|
||||
|
||||
# Replace
|
||||
compare_regex = re.compile(re.sub(
|
||||
user_input_text, "(.+)", product_name, flags=re.IGNORECASE
|
||||
))
|
||||
variant_hints = set()
|
||||
if user_input_text:
|
||||
for _name in existing_product_names:
|
||||
_result = compare_regex.search(_name)
|
||||
if _result:
|
||||
variant_hints |= set(_result.groups())
|
||||
|
||||
if variant_hints:
|
||||
if defaults:
|
||||
defaults.append(SEPARATOR)
|
||||
defaults.extend(variant_hints)
|
||||
self._build_menu(defaults)
|
||||
|
||||
# Indicate product existence
|
||||
if not user_input_text:
|
||||
self._variant_input.as_empty()
|
||||
elif product_name.lower() in existing_product_names_low:
|
||||
# validate existence of product name with lowered text
|
||||
# - "renderMain" vs. "rensermain" mean same path item for
|
||||
# windows
|
||||
self._variant_input.as_exists()
|
||||
else:
|
||||
self._variant_input.as_new()
|
||||
|
||||
# Update the valid state
|
||||
valid = product_name.strip() != ""
|
||||
|
||||
self._set_valid_state(valid)
|
||||
|
||||
def _on_selection_changed(self, old_idx, new_idx):
|
||||
index = self._creators_view.currentIndex()
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
|
||||
creator_plugin = self._creators_model.get_creator_by_id(item_id)
|
||||
|
||||
self._creator_info.set_item(creator_plugin)
|
||||
|
||||
if creator_plugin is None:
|
||||
return
|
||||
|
||||
default = None
|
||||
if hasattr(creator_plugin, "get_default_variant"):
|
||||
default = creator_plugin.get_default_variant()
|
||||
|
||||
if not default:
|
||||
if (
|
||||
creator_plugin.defaults
|
||||
and isinstance(creator_plugin.defaults, list)
|
||||
):
|
||||
default = creator_plugin.defaults[0]
|
||||
else:
|
||||
default = "Default"
|
||||
|
||||
self._variant_input.setText(default)
|
||||
|
||||
self._on_data_changed()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""Custom keyPressEvent.
|
||||
|
||||
Override keyPressEvent to do nothing so that Maya's panels won't
|
||||
take focus when pressing "SHIFT" whilst mouse is over viewport or
|
||||
outliner. This way users don't accidentally perform Maya commands
|
||||
whilst trying to name an instance.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def showEvent(self, event):
|
||||
super(CreatorWindow, self).showEvent(event)
|
||||
if self._first_show:
|
||||
self._first_show = False
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
def refresh(self):
|
||||
self._folder_path_input.setText(get_current_folder_path())
|
||||
|
||||
self._creators_model.reset()
|
||||
|
||||
product_types_smart_select = (
|
||||
get_current_project_settings()
|
||||
["core"]
|
||||
["tools"]
|
||||
["creator"]
|
||||
["product_types_smart_select"]
|
||||
)
|
||||
current_index = None
|
||||
product_type = None
|
||||
task_name = get_current_task_name() or None
|
||||
lowered_task_name = task_name.lower()
|
||||
if task_name:
|
||||
for smart_item in product_types_smart_select:
|
||||
_low_task_names = {
|
||||
name.lower() for name in smart_item["task_names"]
|
||||
}
|
||||
for _task_name in _low_task_names:
|
||||
if _task_name in lowered_task_name:
|
||||
product_type = smart_item["name"]
|
||||
break
|
||||
if product_type:
|
||||
break
|
||||
|
||||
if product_type:
|
||||
indexes = self._creators_model.get_indexes_by_product_type(
|
||||
product_type
|
||||
)
|
||||
if indexes:
|
||||
index = indexes[0]
|
||||
current_index = self._creators_proxy.mapFromSource(index)
|
||||
|
||||
if current_index is None or not current_index.isValid():
|
||||
current_index = self._creators_proxy.index(0, 0)
|
||||
|
||||
self._creators_view.setCurrentIndex(current_index)
|
||||
|
||||
def _on_create(self):
|
||||
# Do not allow creation in an invalid state
|
||||
if not self._is_valid:
|
||||
return
|
||||
|
||||
index = self._creators_view.currentIndex()
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
creator_plugin = self._creators_model.get_creator_by_id(item_id)
|
||||
if creator_plugin is None:
|
||||
return
|
||||
|
||||
product_name = self._product_name_input.text()
|
||||
folder_path = self._folder_path_input.text()
|
||||
use_selection = self._useselection_chk.isChecked()
|
||||
|
||||
variant = self._variant_input.text()
|
||||
|
||||
error_info = None
|
||||
try:
|
||||
legacy_create(
|
||||
creator_plugin,
|
||||
product_name,
|
||||
folder_path,
|
||||
options={"useSelection": use_selection},
|
||||
data={"variant": variant}
|
||||
)
|
||||
|
||||
except CreatorError as exc:
|
||||
self.echo("Creator error: {}".format(str(exc)))
|
||||
error_info = (str(exc), None)
|
||||
|
||||
except Exception as exc:
|
||||
self.echo("Program error: %s" % str(exc))
|
||||
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
formatted_traceback = "".join(traceback.format_exception(
|
||||
exc_type, exc_value, exc_traceback
|
||||
))
|
||||
error_info = (str(exc), formatted_traceback)
|
||||
|
||||
if error_info:
|
||||
box = CreateErrorMessageBox(
|
||||
creator_plugin.product_type,
|
||||
product_name,
|
||||
folder_path,
|
||||
*error_info,
|
||||
parent=self
|
||||
)
|
||||
box.show()
|
||||
# Store dialog so is not garbage collected before is shown
|
||||
self._message_dialog = box
|
||||
|
||||
else:
|
||||
self.echo("Created %s .." % product_name)
|
||||
|
||||
def _on_msg_timer(self):
|
||||
self._msg_label.setText("")
|
||||
|
||||
def echo(self, message):
|
||||
self._msg_label.setText(str(message))
|
||||
self._msg_timer.start()
|
||||
|
||||
|
||||
def show(parent=None):
|
||||
"""Display product creator GUI
|
||||
|
||||
Arguments:
|
||||
debug (bool, optional): Run loader in debug-mode,
|
||||
defaults to False
|
||||
parent (QtCore.QObject, optional): When provided parent the interface
|
||||
to this QObject.
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
module.window.close()
|
||||
del module.window
|
||||
except (AttributeError, RuntimeError):
|
||||
pass
|
||||
|
||||
with qt_app_context():
|
||||
window = CreatorWindow(parent)
|
||||
window.refresh()
|
||||
window.show()
|
||||
|
||||
module.window = window
|
||||
|
||||
# Pull window to the front.
|
||||
module.window.raise_()
|
||||
module.window.activateWindow()
|
||||
|
|
@ -517,7 +517,12 @@ class ActionsModel:
|
|||
uri = payload["uri"]
|
||||
else:
|
||||
uri = data["uri"]
|
||||
run_detached_ayon_launcher_process(uri)
|
||||
|
||||
# Remove bundles from environment variables
|
||||
env = os.environ.copy()
|
||||
env.pop("AYON_BUNDLE_NAME", None)
|
||||
env.pop("AYON_STUDIO_BUNDLE_NAME", None)
|
||||
run_detached_ayon_launcher_process(uri, env=env)
|
||||
|
||||
elif response_type in ("query", "navigate"):
|
||||
response.error_message = (
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ from ayon_core.lib.attribute_definitions import (
|
|||
deserialize_attr_defs,
|
||||
serialize_attr_defs,
|
||||
)
|
||||
from ayon_core.tools.common_models import TaskItem, TagItem
|
||||
from ayon_core.tools.common_models import (
|
||||
TaskItem,
|
||||
TagItem,
|
||||
ProductTypeIconMapping,
|
||||
)
|
||||
|
||||
|
||||
class ProductTypeItem:
|
||||
|
|
@ -78,7 +82,6 @@ class ProductItem:
|
|||
product_type (str): Product type.
|
||||
product_name (str): Product name.
|
||||
product_icon (dict[str, Any]): Product icon definition.
|
||||
product_type_icon (dict[str, Any]): Product type icon definition.
|
||||
product_in_scene (bool): Is product in scene (only when used in DCC).
|
||||
group_name (str): Group name.
|
||||
folder_id (str): Folder id.
|
||||
|
|
@ -93,8 +96,6 @@ class ProductItem:
|
|||
product_base_type: str,
|
||||
product_name: str,
|
||||
product_icon: dict[str, Any],
|
||||
product_type_icon: dict[str, Any],
|
||||
product_base_type_icon: dict[str, Any],
|
||||
group_name: str,
|
||||
folder_id: str,
|
||||
folder_label: str,
|
||||
|
|
@ -106,8 +107,6 @@ class ProductItem:
|
|||
self.product_base_type = product_base_type
|
||||
self.product_name = product_name
|
||||
self.product_icon = product_icon
|
||||
self.product_type_icon = product_type_icon
|
||||
self.product_base_type_icon = product_base_type_icon
|
||||
self.product_in_scene = product_in_scene
|
||||
self.group_name = group_name
|
||||
self.folder_id = folder_id
|
||||
|
|
@ -121,8 +120,6 @@ class ProductItem:
|
|||
"product_base_type": self.product_base_type,
|
||||
"product_name": self.product_name,
|
||||
"product_icon": self.product_icon,
|
||||
"product_type_icon": self.product_type_icon,
|
||||
"product_base_type_icon": self.product_base_type_icon,
|
||||
"product_in_scene": self.product_in_scene,
|
||||
"group_name": self.group_name,
|
||||
"folder_id": self.folder_id,
|
||||
|
|
@ -499,8 +496,8 @@ class BackendLoaderController(_BaseLoaderController):
|
|||
topic (str): Event topic name.
|
||||
data (Optional[dict[str, Any]]): Event data.
|
||||
source (Optional[str]): Event source.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -509,8 +506,20 @@ class BackendLoaderController(_BaseLoaderController):
|
|||
|
||||
Returns:
|
||||
set[str]: Set of loaded product ids.
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_product_type_icons_mapping(
|
||||
self, project_name: Optional[str]
|
||||
) -> ProductTypeIconMapping:
|
||||
"""Product type icons mapping.
|
||||
|
||||
Returns:
|
||||
ProductTypeIconMapping: Product type icons mapping.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
import ayon_api
|
||||
|
||||
|
|
@ -16,6 +17,7 @@ from ayon_core.tools.common_models import (
|
|||
HierarchyModel,
|
||||
ThumbnailsModel,
|
||||
TagItem,
|
||||
ProductTypeIconMapping,
|
||||
)
|
||||
|
||||
from .abstract import (
|
||||
|
|
@ -198,6 +200,13 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
|
|||
project_name, sender
|
||||
)
|
||||
|
||||
def get_product_type_icons_mapping(
|
||||
self, project_name: Optional[str]
|
||||
) -> ProductTypeIconMapping:
|
||||
return self._projects_model.get_product_type_icons_mapping(
|
||||
project_name
|
||||
)
|
||||
|
||||
def get_folder_items(self, project_name, sender=None):
|
||||
return self._hierarchy_model.get_folder_items(project_name, sender)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ import arrow
|
|||
import ayon_api
|
||||
from ayon_api.operations import OperationsSession
|
||||
|
||||
|
||||
from ayon_core.lib import NestedCacheItem
|
||||
from ayon_core.style import get_default_entity_icon_color
|
||||
from ayon_core.tools.common_models import ProductTypeIconMapping
|
||||
from ayon_core.tools.loader.abstract import (
|
||||
ProductTypeItem,
|
||||
ProductBaseTypeItem,
|
||||
|
|
@ -21,8 +21,11 @@ from ayon_core.tools.loader.abstract import (
|
|||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ayon_api.typing import ProductBaseTypeDict, ProductDict, VersionDict
|
||||
|
||||
from ayon_api.typing import (
|
||||
ProductBaseTypeDict,
|
||||
ProductDict,
|
||||
VersionDict,
|
||||
)
|
||||
|
||||
PRODUCTS_MODEL_SENDER = "products.model"
|
||||
|
||||
|
|
@ -84,42 +87,18 @@ def version_item_from_entity(version):
|
|||
def product_item_from_entity(
|
||||
product_entity: ProductDict,
|
||||
version_entities,
|
||||
product_type_items_by_name: dict[str, ProductTypeItem],
|
||||
product_base_type_items_by_name: dict[str, ProductBaseTypeItem],
|
||||
folder_label,
|
||||
icons_mapping,
|
||||
product_in_scene,
|
||||
):
|
||||
product_attribs = product_entity["attrib"]
|
||||
group = product_attribs.get("productGroup")
|
||||
product_type = product_entity["productType"]
|
||||
product_type_item = product_type_items_by_name.get(product_type)
|
||||
# NOTE This is needed for cases when products were not created on server
|
||||
# using api functions. In that case product type item may not be
|
||||
# available and we need to create a default.
|
||||
if product_type_item is None:
|
||||
product_type_item = create_default_product_type_item(product_type)
|
||||
# Cache the item for future use
|
||||
product_type_items_by_name[product_type] = product_type_item
|
||||
|
||||
product_base_type = product_entity.get("productBaseType")
|
||||
product_base_type_item = product_base_type_items_by_name.get(
|
||||
product_base_type)
|
||||
# Same as for product type item above. Not sure if this is still needed
|
||||
# though.
|
||||
if product_base_type_item is None:
|
||||
product_base_type_item = create_default_product_base_type_item(
|
||||
product_base_type)
|
||||
# Cache the item for future use
|
||||
product_base_type_items_by_name[product_base_type] = (
|
||||
product_base_type_item)
|
||||
|
||||
product_type_icon = product_type_item.icon
|
||||
product_base_type_icon = product_base_type_item.icon
|
||||
product_icon = {
|
||||
"type": "awesome-font",
|
||||
"name": "fa.file-o",
|
||||
"color": get_default_entity_icon_color(),
|
||||
}
|
||||
product_icon = icons_mapping.get_icon(
|
||||
product_base_type, product_type
|
||||
)
|
||||
version_items = {
|
||||
version_entity["id"]: version_item_from_entity(version_entity)
|
||||
for version_entity in version_entities
|
||||
|
|
@ -131,8 +110,6 @@ def product_item_from_entity(
|
|||
product_base_type=product_base_type,
|
||||
product_name=product_entity["name"],
|
||||
product_icon=product_icon,
|
||||
product_type_icon=product_type_icon,
|
||||
product_base_type_icon=product_base_type_icon,
|
||||
product_in_scene=product_in_scene,
|
||||
group_name=group,
|
||||
folder_id=product_entity["folderId"],
|
||||
|
|
@ -141,22 +118,8 @@ def product_item_from_entity(
|
|||
)
|
||||
|
||||
|
||||
def product_type_item_from_data(
|
||||
product_type_data: ProductDict) -> ProductTypeItem:
|
||||
# TODO implement icon implementation
|
||||
# icon = product_type_data["icon"]
|
||||
# color = product_type_data["color"]
|
||||
icon = {
|
||||
"type": "awesome-font",
|
||||
"name": "fa.folder",
|
||||
"color": "#0091B2",
|
||||
}
|
||||
# TODO implement checked logic
|
||||
return ProductTypeItem(product_type_data["name"], icon)
|
||||
|
||||
|
||||
def product_base_type_item_from_data(
|
||||
product_base_type_data: ProductBaseTypeDict
|
||||
product_base_type_data: ProductBaseTypeDict
|
||||
) -> ProductBaseTypeItem:
|
||||
"""Create product base type item from data.
|
||||
|
||||
|
|
@ -174,34 +137,8 @@ def product_base_type_item_from_data(
|
|||
}
|
||||
return ProductBaseTypeItem(
|
||||
name=product_base_type_data["name"],
|
||||
icon=icon)
|
||||
|
||||
|
||||
def create_default_product_type_item(product_type: str) -> ProductTypeItem:
|
||||
icon = {
|
||||
"type": "awesome-font",
|
||||
"name": "fa.folder",
|
||||
"color": "#0091B2",
|
||||
}
|
||||
return ProductTypeItem(product_type, icon)
|
||||
|
||||
|
||||
def create_default_product_base_type_item(
|
||||
product_base_type: str) -> ProductBaseTypeItem:
|
||||
"""Create default product base type item.
|
||||
|
||||
Args:
|
||||
product_base_type (str): Product base type name.
|
||||
|
||||
Returns:
|
||||
ProductBaseTypeItem: Default product base type item.
|
||||
"""
|
||||
icon = {
|
||||
"type": "awesome-font",
|
||||
"name": "fa.folder",
|
||||
"color": "#0091B2",
|
||||
}
|
||||
return ProductBaseTypeItem(product_base_type, icon)
|
||||
icon=icon
|
||||
)
|
||||
|
||||
|
||||
class ProductsModel:
|
||||
|
|
@ -247,7 +184,9 @@ class ProductsModel:
|
|||
self._product_items_cache.reset()
|
||||
self._repre_items_cache.reset()
|
||||
|
||||
def get_product_type_items(self, project_name):
|
||||
def get_product_type_items(
|
||||
self, project_name: Optional[str]
|
||||
) -> list[ProductTypeItem]:
|
||||
"""Product type items for project.
|
||||
|
||||
Args:
|
||||
|
|
@ -255,25 +194,33 @@ class ProductsModel:
|
|||
|
||||
Returns:
|
||||
list[ProductTypeItem]: Product type items.
|
||||
"""
|
||||
|
||||
"""
|
||||
if not project_name:
|
||||
return []
|
||||
|
||||
cache = self._product_type_items_cache[project_name]
|
||||
if not cache.is_valid:
|
||||
icons_mapping = self._get_product_type_icons(project_name)
|
||||
product_types = ayon_api.get_project_product_types(project_name)
|
||||
cache.update_data([
|
||||
product_type_item_from_data(product_type)
|
||||
ProductTypeItem(
|
||||
product_type["name"],
|
||||
icons_mapping.get_icon(product_type=product_type["name"]),
|
||||
)
|
||||
for product_type in product_types
|
||||
])
|
||||
return cache.get_data()
|
||||
|
||||
def get_product_base_type_items(
|
||||
self,
|
||||
project_name: Optional[str]) -> list[ProductBaseTypeItem]:
|
||||
self, project_name: Optional[str]
|
||||
) -> list[ProductBaseTypeItem]:
|
||||
"""Product base type items for the project.
|
||||
|
||||
Notes:
|
||||
This will be used for filtering product types in UI when
|
||||
product base types are fully implemented.
|
||||
|
||||
Args:
|
||||
project_name (optional, str): Project name.
|
||||
|
||||
|
|
@ -286,6 +233,7 @@ class ProductsModel:
|
|||
|
||||
cache = self._product_base_type_items_cache[project_name]
|
||||
if not cache.is_valid:
|
||||
icons_mapping = self._get_product_type_icons(project_name)
|
||||
product_base_types = []
|
||||
# TODO add temp implementation here when it is actually
|
||||
# implemented and available on server.
|
||||
|
|
@ -294,7 +242,10 @@ class ProductsModel:
|
|||
project_name
|
||||
)
|
||||
cache.update_data([
|
||||
product_base_type_item_from_data(product_base_type)
|
||||
ProductBaseTypeItem(
|
||||
product_base_type["name"],
|
||||
icons_mapping.get_icon(product_base_type["name"]),
|
||||
)
|
||||
for product_base_type in product_base_types
|
||||
])
|
||||
return cache.get_data()
|
||||
|
|
@ -511,6 +462,11 @@ class ProductsModel:
|
|||
PRODUCTS_MODEL_SENDER
|
||||
)
|
||||
|
||||
def _get_product_type_icons(
|
||||
self, project_name: Optional[str]
|
||||
) -> ProductTypeIconMapping:
|
||||
return self._controller.get_product_type_icons_mapping(project_name)
|
||||
|
||||
def _get_product_items_by_id(self, project_name, product_ids):
|
||||
product_item_by_id = self._product_item_by_id[project_name]
|
||||
missing_product_ids = set()
|
||||
|
|
@ -524,7 +480,7 @@ class ProductsModel:
|
|||
|
||||
output.update(
|
||||
self._query_product_items_by_ids(
|
||||
project_name, missing_product_ids
|
||||
project_name, product_ids=missing_product_ids
|
||||
)
|
||||
)
|
||||
return output
|
||||
|
|
@ -553,36 +509,18 @@ class ProductsModel:
|
|||
products: Iterable[ProductDict],
|
||||
versions: Iterable[VersionDict],
|
||||
folder_items=None,
|
||||
product_type_items=None,
|
||||
product_base_type_items: Optional[Iterable[ProductBaseTypeItem]] = None
|
||||
):
|
||||
if folder_items is None:
|
||||
folder_items = self._controller.get_folder_items(project_name)
|
||||
|
||||
if product_type_items is None:
|
||||
product_type_items = self.get_product_type_items(project_name)
|
||||
|
||||
if product_base_type_items is None:
|
||||
product_base_type_items = self.get_product_base_type_items(
|
||||
project_name
|
||||
)
|
||||
|
||||
loaded_product_ids = self._controller.get_loaded_product_ids()
|
||||
|
||||
versions_by_product_id = collections.defaultdict(list)
|
||||
for version in versions:
|
||||
versions_by_product_id[version["productId"]].append(version)
|
||||
product_type_items_by_name = {
|
||||
product_type_item.name: product_type_item
|
||||
for product_type_item in product_type_items
|
||||
}
|
||||
|
||||
product_base_type_items_by_name: dict[str, ProductBaseTypeItem] = {
|
||||
product_base_type_item.name: product_base_type_item
|
||||
for product_base_type_item in product_base_type_items
|
||||
}
|
||||
|
||||
output: dict[str, ProductItem] = {}
|
||||
icons_mapping = self._get_product_type_icons(project_name)
|
||||
for product in products:
|
||||
product_id = product["id"]
|
||||
folder_id = product["folderId"]
|
||||
|
|
@ -595,9 +533,8 @@ class ProductsModel:
|
|||
product_item = product_item_from_entity(
|
||||
product,
|
||||
versions,
|
||||
product_type_items_by_name,
|
||||
product_base_type_items_by_name,
|
||||
folder_item.label,
|
||||
icons_mapping,
|
||||
product_id in loaded_product_ids,
|
||||
)
|
||||
output[product_id] = product_item
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ PRODUCT_ID_ROLE = QtCore.Qt.UserRole + 6
|
|||
PRODUCT_NAME_ROLE = QtCore.Qt.UserRole + 7
|
||||
PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 8
|
||||
PRODUCT_BASE_TYPE_ROLE = QtCore.Qt.UserRole + 9
|
||||
PRODUCT_TYPE_ICON_ROLE = QtCore.Qt.UserRole + 10
|
||||
PRODUCT_IN_SCENE_ROLE = QtCore.Qt.UserRole + 11
|
||||
VERSION_ID_ROLE = QtCore.Qt.UserRole + 12
|
||||
VERSION_HERO_ROLE = QtCore.Qt.UserRole + 13
|
||||
|
|
@ -228,10 +227,7 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
return super().data(index, role)
|
||||
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
if col == 1:
|
||||
role = PRODUCT_TYPE_ICON_ROLE
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
if (
|
||||
role == VERSION_NAME_EDIT_ROLE
|
||||
|
|
@ -455,7 +451,6 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
model_item = QtGui.QStandardItem(product_item.product_name)
|
||||
model_item.setEditable(False)
|
||||
icon = get_qt_icon(product_item.product_icon)
|
||||
product_type_icon = get_qt_icon(product_item.product_type_icon)
|
||||
model_item.setColumnCount(self.columnCount())
|
||||
model_item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
model_item.setData(product_id, PRODUCT_ID_ROLE)
|
||||
|
|
@ -464,7 +459,6 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
product_item.product_base_type, PRODUCT_BASE_TYPE_ROLE
|
||||
)
|
||||
model_item.setData(product_item.product_type, PRODUCT_TYPE_ROLE)
|
||||
model_item.setData(product_type_icon, PRODUCT_TYPE_ICON_ROLE)
|
||||
model_item.setData(product_item.folder_id, FOLDER_ID_ROLE)
|
||||
|
||||
self._product_items_by_id[product_id] = product_item
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from typing import (
|
|||
)
|
||||
|
||||
from ayon_core.lib import AbstractAttrDef
|
||||
from ayon_core.host import HostBase
|
||||
from ayon_core.host import AbstractHost
|
||||
from ayon_core.pipeline.create import (
|
||||
CreateContext,
|
||||
ConvertorItem,
|
||||
|
|
@ -176,7 +176,7 @@ class AbstractPublisherBackend(AbstractPublisherCommon):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_host(self) -> HostBase:
|
||||
def get_host(self) -> AbstractHost:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
|
|
|||
|
|
@ -219,6 +219,8 @@ class InstanceItem:
|
|||
is_active: bool,
|
||||
is_mandatory: bool,
|
||||
has_promised_context: bool,
|
||||
parent_instance_id: Optional[str],
|
||||
parent_flags: int,
|
||||
):
|
||||
self._instance_id: str = instance_id
|
||||
self._creator_identifier: str = creator_identifier
|
||||
|
|
@ -232,6 +234,8 @@ class InstanceItem:
|
|||
self._is_active: bool = is_active
|
||||
self._is_mandatory: bool = is_mandatory
|
||||
self._has_promised_context: bool = has_promised_context
|
||||
self._parent_instance_id: Optional[str] = parent_instance_id
|
||||
self._parent_flags: int = parent_flags
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
|
|
@ -261,6 +265,14 @@ class InstanceItem:
|
|||
def has_promised_context(self):
|
||||
return self._has_promised_context
|
||||
|
||||
@property
|
||||
def parent_instance_id(self):
|
||||
return self._parent_instance_id
|
||||
|
||||
@property
|
||||
def parent_flags(self) -> int:
|
||||
return self._parent_flags
|
||||
|
||||
def get_variant(self):
|
||||
return self._variant
|
||||
|
||||
|
|
@ -312,6 +324,8 @@ class InstanceItem:
|
|||
instance["active"],
|
||||
instance.is_mandatory,
|
||||
instance.has_promised_context,
|
||||
instance.parent_instance_id,
|
||||
instance.parent_flags,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -486,6 +500,9 @@ class CreateModel:
|
|||
self._create_context.add_instance_requirement_change_callback(
|
||||
self._cc_instance_requirement_changed
|
||||
)
|
||||
self._create_context.add_instance_parent_change_callback(
|
||||
self._cc_instance_parent_changed
|
||||
)
|
||||
|
||||
self._create_context.reset_finalization()
|
||||
|
||||
|
|
@ -566,15 +583,21 @@ class CreateModel:
|
|||
def set_instances_active_state(
|
||||
self, active_state_by_id: Dict[str, bool]
|
||||
):
|
||||
changed_ids = set()
|
||||
with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE):
|
||||
for instance_id, active in active_state_by_id.items():
|
||||
instance = self._create_context.get_instance_by_id(instance_id)
|
||||
instance["active"] = active
|
||||
if instance["active"] is not active:
|
||||
instance["active"] = active
|
||||
changed_ids.add(instance_id)
|
||||
|
||||
if not changed_ids:
|
||||
return
|
||||
|
||||
self._emit_event(
|
||||
"create.model.instances.context.changed",
|
||||
{
|
||||
"instance_ids": set(active_state_by_id.keys())
|
||||
"instance_ids": changed_ids
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -1191,6 +1214,16 @@ class CreateModel:
|
|||
{"instance_ids": instance_ids},
|
||||
)
|
||||
|
||||
def _cc_instance_parent_changed(self, event):
|
||||
instance_ids = {
|
||||
instance.id
|
||||
for instance in event.data["instances"]
|
||||
}
|
||||
self._emit_event(
|
||||
"create.model.instance.parent.changed",
|
||||
{"instance_ids": instance_ids},
|
||||
)
|
||||
|
||||
def _get_allowed_creators_pattern(self) -> Union[Pattern, None]:
|
||||
"""Provide regex pattern for configured creator labels in this context
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,3 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Generator
|
||||
|
||||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend
|
||||
|
|
@ -6,6 +10,7 @@ from .border_label_widget import BorderedLabelWidget
|
|||
from .card_view_widgets import InstanceCardView
|
||||
from .list_view_widgets import InstanceListView
|
||||
from .widgets import (
|
||||
AbstractInstanceView,
|
||||
CreateInstanceBtn,
|
||||
RemoveInstanceBtn,
|
||||
ChangeViewBtn,
|
||||
|
|
@ -43,10 +48,16 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
|
||||
product_view_cards = InstanceCardView(controller, product_views_widget)
|
||||
product_list_view = InstanceListView(controller, product_views_widget)
|
||||
product_list_view.set_parent_grouping(False)
|
||||
product_list_view_grouped = InstanceListView(
|
||||
controller, product_views_widget
|
||||
)
|
||||
product_list_view_grouped.set_parent_grouping(True)
|
||||
|
||||
product_views_layout = QtWidgets.QStackedLayout()
|
||||
product_views_layout.addWidget(product_view_cards)
|
||||
product_views_layout.addWidget(product_list_view)
|
||||
product_views_layout.addWidget(product_list_view_grouped)
|
||||
product_views_layout.setCurrentWidget(product_view_cards)
|
||||
|
||||
# Buttons at the bottom of product view
|
||||
|
|
@ -118,6 +129,12 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
product_list_view.double_clicked.connect(
|
||||
self.publish_tab_requested
|
||||
)
|
||||
product_list_view_grouped.selection_changed.connect(
|
||||
self._on_product_change
|
||||
)
|
||||
product_list_view_grouped.double_clicked.connect(
|
||||
self.publish_tab_requested
|
||||
)
|
||||
product_view_cards.selection_changed.connect(
|
||||
self._on_product_change
|
||||
)
|
||||
|
|
@ -159,16 +176,22 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
"create.model.instance.requirement.changed",
|
||||
self._on_instance_requirement_changed
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"create.model.instance.parent.changed",
|
||||
self._on_instance_parent_changed
|
||||
)
|
||||
|
||||
self._product_content_widget = product_content_widget
|
||||
self._product_content_layout = product_content_layout
|
||||
|
||||
self._product_view_cards = product_view_cards
|
||||
self._product_list_view = product_list_view
|
||||
self._product_list_view_grouped = product_list_view_grouped
|
||||
self._product_views_layout = product_views_layout
|
||||
|
||||
self._create_btn = create_btn
|
||||
self._delete_btn = delete_btn
|
||||
self._change_view_btn = change_view_btn
|
||||
|
||||
self._product_attributes_widget = product_attributes_widget
|
||||
self._create_widget = create_widget
|
||||
|
|
@ -246,7 +269,7 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
)
|
||||
|
||||
def has_items(self):
|
||||
view = self._product_views_layout.currentWidget()
|
||||
view = self._get_current_view()
|
||||
return view.has_items()
|
||||
|
||||
def _on_create_clicked(self):
|
||||
|
|
@ -361,17 +384,18 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
def _on_instance_requirement_changed(self, event):
|
||||
self._refresh_instance_states(event["instance_ids"])
|
||||
|
||||
def _refresh_instance_states(self, instance_ids):
|
||||
current_idx = self._product_views_layout.currentIndex()
|
||||
for idx in range(self._product_views_layout.count()):
|
||||
if idx == current_idx:
|
||||
continue
|
||||
widget = self._product_views_layout.widget(idx)
|
||||
if widget.refreshed:
|
||||
widget.set_refreshed(False)
|
||||
def _on_instance_parent_changed(self, event):
|
||||
self._refresh_instance_states(event["instance_ids"])
|
||||
|
||||
current_widget = self._product_views_layout.widget(current_idx)
|
||||
current_widget.refresh_instance_states(instance_ids)
|
||||
def _refresh_instance_states(self, instance_ids):
|
||||
current_view = self._get_current_view()
|
||||
for view in self._iter_views():
|
||||
if view is current_view:
|
||||
current_view = view
|
||||
elif view.refreshed:
|
||||
view.set_refreshed(False)
|
||||
|
||||
current_view.refresh_instance_states(instance_ids)
|
||||
|
||||
def _on_convert_requested(self):
|
||||
self.convert_requested.emit()
|
||||
|
|
@ -385,7 +409,7 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
convertor plugins.
|
||||
"""
|
||||
|
||||
view = self._product_views_layout.currentWidget()
|
||||
view = self._get_current_view()
|
||||
return view.get_selected_items()
|
||||
|
||||
def get_selected_legacy_convertors(self):
|
||||
|
|
@ -400,12 +424,12 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
return convertor_identifiers
|
||||
|
||||
def _change_view_type(self):
|
||||
old_view = self._get_current_view()
|
||||
|
||||
idx = self._product_views_layout.currentIndex()
|
||||
new_idx = (idx + 1) % self._product_views_layout.count()
|
||||
|
||||
old_view = self._product_views_layout.currentWidget()
|
||||
new_view = self._product_views_layout.widget(new_idx)
|
||||
|
||||
new_view = self._get_view_by_idx(new_idx)
|
||||
if not new_view.refreshed:
|
||||
new_view.refresh()
|
||||
new_view.set_refreshed(True)
|
||||
|
|
@ -418,22 +442,52 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
new_view.set_selected_items(
|
||||
instance_ids, context_selected, convertor_identifiers
|
||||
)
|
||||
view_type = "list"
|
||||
if new_view is self._product_list_view_grouped:
|
||||
view_type = "card"
|
||||
elif new_view is self._product_list_view:
|
||||
view_type = "list-parent-grouping"
|
||||
|
||||
self._change_view_btn.set_view_type(view_type)
|
||||
self._product_views_layout.setCurrentIndex(new_idx)
|
||||
|
||||
self._on_product_change()
|
||||
|
||||
def _iter_views(self) -> Generator[AbstractInstanceView, None, None]:
|
||||
for idx in range(self._product_views_layout.count()):
|
||||
widget = self._product_views_layout.widget(idx)
|
||||
if not isinstance(widget, AbstractInstanceView):
|
||||
raise TypeError(
|
||||
"Current widget is not instance of 'AbstractInstanceView'"
|
||||
)
|
||||
yield widget
|
||||
|
||||
def _get_current_view(self) -> AbstractInstanceView:
|
||||
widget = self._product_views_layout.currentWidget()
|
||||
if isinstance(widget, AbstractInstanceView):
|
||||
return widget
|
||||
raise TypeError(
|
||||
"Current widget is not instance of 'AbstractInstanceView'"
|
||||
)
|
||||
|
||||
def _get_view_by_idx(self, idx: int) -> AbstractInstanceView:
|
||||
widget = self._product_views_layout.widget(idx)
|
||||
if isinstance(widget, AbstractInstanceView):
|
||||
return widget
|
||||
raise TypeError(
|
||||
"Current widget is not instance of 'AbstractInstanceView'"
|
||||
)
|
||||
|
||||
def _refresh_instances(self):
|
||||
if self._refreshing_instances:
|
||||
return
|
||||
|
||||
self._refreshing_instances = True
|
||||
|
||||
for idx in range(self._product_views_layout.count()):
|
||||
widget = self._product_views_layout.widget(idx)
|
||||
widget.set_refreshed(False)
|
||||
for view in self._iter_views():
|
||||
view.set_refreshed(False)
|
||||
|
||||
view = self._product_views_layout.currentWidget()
|
||||
view = self._get_current_view()
|
||||
view.refresh()
|
||||
view.set_refreshed(True)
|
||||
|
||||
|
|
@ -444,25 +498,22 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
|
||||
# Give a change to process Resize Request
|
||||
QtWidgets.QApplication.processEvents()
|
||||
# Trigger update geometry of
|
||||
widget = self._product_views_layout.currentWidget()
|
||||
widget.updateGeometry()
|
||||
# Trigger update geometry
|
||||
view.updateGeometry()
|
||||
|
||||
def _on_publish_start(self):
|
||||
"""Publish started."""
|
||||
|
||||
self._create_btn.setEnabled(False)
|
||||
self._product_attributes_wrap.setEnabled(False)
|
||||
for idx in range(self._product_views_layout.count()):
|
||||
widget = self._product_views_layout.widget(idx)
|
||||
widget.set_active_toggle_enabled(False)
|
||||
for view in self._iter_views():
|
||||
view.set_active_toggle_enabled(False)
|
||||
|
||||
def _on_controller_reset_start(self):
|
||||
"""Controller reset started."""
|
||||
|
||||
for idx in range(self._product_views_layout.count()):
|
||||
widget = self._product_views_layout.widget(idx)
|
||||
widget.set_active_toggle_enabled(True)
|
||||
for view in self._iter_views():
|
||||
view.set_active_toggle_enabled(True)
|
||||
|
||||
def _on_publish_reset(self):
|
||||
"""Context in controller has been reseted."""
|
||||
|
|
@ -477,7 +528,19 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
self._refresh_instances()
|
||||
|
||||
def _on_instances_added(self):
|
||||
view = self._get_current_view()
|
||||
is_card_view = False
|
||||
count = 0
|
||||
if isinstance(view, InstanceCardView):
|
||||
is_card_view = True
|
||||
count = view.get_current_instance_count()
|
||||
|
||||
self._refresh_instances()
|
||||
|
||||
if is_card_view and count < 10:
|
||||
new_count = view.get_current_instance_count()
|
||||
if new_count > count and new_count >= 10:
|
||||
self._change_view_type()
|
||||
|
||||
def _on_instances_removed(self):
|
||||
self._refresh_instances()
|
||||
|
|
|
|||
|
|
@ -1147,6 +1147,8 @@ class LogItemMessage(QtWidgets.QTextEdit):
|
|||
QtWidgets.QSizePolicy.Preferred,
|
||||
QtWidgets.QSizePolicy.Maximum
|
||||
)
|
||||
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
document = self.document()
|
||||
document.documentLayout().documentSizeChanged.connect(
|
||||
self._adjust_minimum_size
|
||||
|
|
|
|||
|
|
@ -146,19 +146,19 @@ class TasksModel(QtGui.QStandardItemModel):
|
|||
self._controller.get_current_project_name()
|
||||
)
|
||||
}
|
||||
icon_name_by_task_name = {}
|
||||
type_item_by_task_name = {}
|
||||
for task_items in task_items_by_folder_path.values():
|
||||
for task_item in task_items:
|
||||
task_name = task_item.name
|
||||
if (
|
||||
task_name not in new_task_names
|
||||
or task_name in icon_name_by_task_name
|
||||
or task_name in type_item_by_task_name
|
||||
):
|
||||
continue
|
||||
task_type_name = task_item.task_type
|
||||
task_type_item = task_type_items.get(task_type_name)
|
||||
if task_type_item:
|
||||
icon_name_by_task_name[task_name] = task_type_item.icon
|
||||
type_item_by_task_name[task_name] = task_type_item
|
||||
|
||||
for task_name in new_task_names:
|
||||
item = self._items_by_name.get(task_name)
|
||||
|
|
@ -171,13 +171,18 @@ class TasksModel(QtGui.QStandardItemModel):
|
|||
if not task_name:
|
||||
continue
|
||||
|
||||
icon_name = icon_name_by_task_name.get(task_name)
|
||||
icon = None
|
||||
icon = icon_name = icon_color = None
|
||||
task_type_item = type_item_by_task_name.get(task_name)
|
||||
if task_type_item is not None:
|
||||
icon_name = task_type_item.icon
|
||||
icon_color = task_type_item.color
|
||||
if icon_name:
|
||||
if not icon_color:
|
||||
icon_color = get_default_entity_icon_color()
|
||||
icon = get_qt_icon({
|
||||
"type": "material-symbols",
|
||||
"name": icon_name,
|
||||
"color": get_default_entity_icon_color(),
|
||||
"color": icon_color,
|
||||
})
|
||||
if icon is None:
|
||||
icon = default_icon
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from ayon_core.tools.flickcharm import FlickCharm
|
|||
from ayon_core.tools.utils import (
|
||||
IconButton,
|
||||
PixmapLabel,
|
||||
get_qt_icon,
|
||||
)
|
||||
from ayon_core.tools.publisher.constants import ResetKeySequence
|
||||
|
||||
|
|
@ -287,12 +288,32 @@ class RemoveInstanceBtn(PublishIconBtn):
|
|||
self.setToolTip("Remove selected instances")
|
||||
|
||||
|
||||
class ChangeViewBtn(PublishIconBtn):
|
||||
"""Create toggle view button."""
|
||||
class ChangeViewBtn(IconButton):
|
||||
"""Toggle views button."""
|
||||
def __init__(self, parent=None):
|
||||
icon_path = get_icon_path("change_view")
|
||||
super().__init__(icon_path, parent)
|
||||
self.setToolTip("Swap between views")
|
||||
super().__init__(parent)
|
||||
self.set_view_type("list")
|
||||
|
||||
def set_view_type(self, view_type):
|
||||
if view_type == "list":
|
||||
# icon_name = "data_table"
|
||||
icon_name = "dehaze"
|
||||
tooltip = "Change to list view"
|
||||
elif view_type == "card":
|
||||
icon_name = "view_agenda"
|
||||
tooltip = "Change to card view"
|
||||
else:
|
||||
icon_name = "segment"
|
||||
tooltip = "Change to parent grouping view"
|
||||
|
||||
# "format_align_right"
|
||||
# "segment"
|
||||
icon = get_qt_icon({
|
||||
"type": "material-symbols",
|
||||
"name": icon_name,
|
||||
})
|
||||
self.setIcon(icon)
|
||||
self.setToolTip(tooltip)
|
||||
|
||||
|
||||
class AbstractInstanceView(QtWidgets.QWidget):
|
||||
|
|
@ -370,6 +391,20 @@ class AbstractInstanceView(QtWidgets.QWidget):
|
|||
"{} Method 'set_active_toggle_enabled' is not implemented."
|
||||
).format(self.__class__.__name__))
|
||||
|
||||
def refresh_instance_states(self, instance_ids=None):
|
||||
"""Refresh instance states.
|
||||
|
||||
Args:
|
||||
instance_ids: Optional[Iterable[str]]: Instance ids to refresh.
|
||||
If not passed then all instances are refreshed.
|
||||
|
||||
"""
|
||||
|
||||
raise NotImplementedError(
|
||||
f"{self.__class__.__name__} Method 'refresh_instance_states'"
|
||||
" is not implemented."
|
||||
)
|
||||
|
||||
|
||||
class ClickableLineEdit(QtWidgets.QLineEdit):
|
||||
"""QLineEdit capturing left mouse click.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import threading
|
||||
from typing import Dict
|
||||
|
||||
import ayon_api
|
||||
|
||||
|
|
@ -13,10 +14,11 @@ from .models import (
|
|||
UserPublishValuesModel,
|
||||
IntegrateModel,
|
||||
)
|
||||
from .models.integrate import ProjectPushItemProcess
|
||||
|
||||
|
||||
class PushToContextController:
|
||||
def __init__(self, project_name=None, version_id=None):
|
||||
def __init__(self, project_name=None, version_ids=None):
|
||||
self._event_system = self._create_event_system()
|
||||
|
||||
self._projects_model = ProjectsModel(self)
|
||||
|
|
@ -27,18 +29,20 @@ class PushToContextController:
|
|||
self._user_values = UserPublishValuesModel(self)
|
||||
|
||||
self._src_project_name = None
|
||||
self._src_version_id = None
|
||||
self._src_version_ids = []
|
||||
self._src_folder_entity = None
|
||||
self._src_folder_task_entities = {}
|
||||
self._src_product_entity = None
|
||||
self._src_version_entity = None
|
||||
self._src_version_entities = []
|
||||
self._src_product_entities = {}
|
||||
self._src_label = None
|
||||
|
||||
self._submission_enabled = False
|
||||
self._process_thread = None
|
||||
self._process_item_id = None
|
||||
|
||||
self.set_source(project_name, version_id)
|
||||
self._use_original_name = False
|
||||
|
||||
self.set_source(project_name, version_ids)
|
||||
|
||||
# Events system
|
||||
def emit_event(self, topic, data=None, source=None):
|
||||
|
|
@ -51,38 +55,47 @@ class PushToContextController:
|
|||
def register_event_callback(self, topic, callback):
|
||||
self._event_system.add_callback(topic, callback)
|
||||
|
||||
def set_source(self, project_name, version_id):
|
||||
def set_source(self, project_name, version_ids):
|
||||
"""Set source project and version.
|
||||
|
||||
There is currently assumption that tool is working on products of same
|
||||
folder.
|
||||
|
||||
Args:
|
||||
project_name (Union[str, None]): Source project name.
|
||||
version_id (Union[str, None]): Source version id.
|
||||
version_ids (Optional[list[str]]): Version ids.
|
||||
"""
|
||||
|
||||
if not project_name or not version_ids:
|
||||
return
|
||||
if (
|
||||
project_name == self._src_project_name
|
||||
and version_id == self._src_version_id
|
||||
and version_ids == self._src_version_ids
|
||||
):
|
||||
return
|
||||
|
||||
self._src_project_name = project_name
|
||||
self._src_version_id = version_id
|
||||
self._src_version_ids = version_ids
|
||||
self._src_label = None
|
||||
folder_entity = None
|
||||
task_entities = {}
|
||||
product_entity = None
|
||||
version_entity = None
|
||||
if project_name and version_id:
|
||||
version_entity = ayon_api.get_version_by_id(
|
||||
project_name, version_id
|
||||
product_entities = []
|
||||
version_entities = []
|
||||
if project_name and self._src_version_ids:
|
||||
version_entities = list(ayon_api.get_versions(
|
||||
project_name, version_ids=self._src_version_ids))
|
||||
|
||||
if version_entities:
|
||||
product_ids = [
|
||||
version_entity["productId"]
|
||||
for version_entity in version_entities
|
||||
]
|
||||
product_entities = list(ayon_api.get_products(
|
||||
project_name, product_ids=product_ids)
|
||||
)
|
||||
|
||||
if version_entity:
|
||||
product_entity = ayon_api.get_product_by_id(
|
||||
project_name, version_entity["productId"]
|
||||
)
|
||||
|
||||
if product_entity:
|
||||
if product_entities:
|
||||
# all products for same folder
|
||||
product_entity = product_entities[0]
|
||||
folder_entity = ayon_api.get_folder_by_id(
|
||||
project_name, product_entity["folderId"]
|
||||
)
|
||||
|
|
@ -97,15 +110,18 @@ class PushToContextController:
|
|||
|
||||
self._src_folder_entity = folder_entity
|
||||
self._src_folder_task_entities = task_entities
|
||||
self._src_product_entity = product_entity
|
||||
self._src_version_entity = version_entity
|
||||
self._src_version_entities = version_entities
|
||||
self._src_product_entities = {
|
||||
product["id"]: product
|
||||
for product in product_entities
|
||||
}
|
||||
if folder_entity:
|
||||
self._user_values.set_new_folder_name(folder_entity["name"])
|
||||
variant = self._get_src_variant()
|
||||
if variant:
|
||||
self._user_values.set_variant(variant)
|
||||
|
||||
comment = version_entity["attrib"].get("comment")
|
||||
comment = version_entities[0]["attrib"].get("comment")
|
||||
if comment:
|
||||
self._user_values.set_comment(comment)
|
||||
|
||||
|
|
@ -113,7 +129,7 @@ class PushToContextController:
|
|||
"source.changed",
|
||||
{
|
||||
"project_name": project_name,
|
||||
"version_id": version_id
|
||||
"version_ids": self._src_version_ids
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -142,6 +158,14 @@ class PushToContextController:
|
|||
def get_user_values(self):
|
||||
return self._user_values.get_data()
|
||||
|
||||
def original_names_required(self):
|
||||
"""Checks if original product names must be used.
|
||||
|
||||
Currently simple check if multiple versions, but if multiple products
|
||||
with different product_type were used, it wouldn't be necessary.
|
||||
"""
|
||||
return len(self._src_version_entities) > 1
|
||||
|
||||
def set_user_value_folder_name(self, folder_name):
|
||||
self._user_values.set_new_folder_name(folder_name)
|
||||
self._invalidate()
|
||||
|
|
@ -165,8 +189,9 @@ class PushToContextController:
|
|||
def set_selected_task(self, task_id, task_name):
|
||||
self._selection_model.set_selected_task(task_id, task_name)
|
||||
|
||||
def get_process_item_status(self, item_id):
|
||||
return self._integrate_model.get_item_status(item_id)
|
||||
def get_process_items(self) -> Dict[str, ProjectPushItemProcess]:
|
||||
"""Returns dict of all ProjectPushItemProcess items """
|
||||
return self._integrate_model.get_items()
|
||||
|
||||
# Processing methods
|
||||
def submit(self, wait=True):
|
||||
|
|
@ -176,29 +201,33 @@ class PushToContextController:
|
|||
if self._process_thread is not None:
|
||||
return
|
||||
|
||||
item_id = self._integrate_model.create_process_item(
|
||||
self._src_project_name,
|
||||
self._src_version_id,
|
||||
self._selection_model.get_selected_project_name(),
|
||||
self._selection_model.get_selected_folder_id(),
|
||||
self._selection_model.get_selected_task_name(),
|
||||
self._user_values.variant,
|
||||
comment=self._user_values.comment,
|
||||
new_folder_name=self._user_values.new_folder_name,
|
||||
dst_version=1
|
||||
)
|
||||
item_ids = []
|
||||
for src_version_entity in self._src_version_entities:
|
||||
item_id = self._integrate_model.create_process_item(
|
||||
self._src_project_name,
|
||||
src_version_entity["id"],
|
||||
self._selection_model.get_selected_project_name(),
|
||||
self._selection_model.get_selected_folder_id(),
|
||||
self._selection_model.get_selected_task_name(),
|
||||
self._user_values.variant,
|
||||
comment=self._user_values.comment,
|
||||
new_folder_name=self._user_values.new_folder_name,
|
||||
dst_version=1,
|
||||
use_original_name=self._use_original_name,
|
||||
)
|
||||
item_ids.append(item_id)
|
||||
|
||||
self._process_item_id = item_id
|
||||
self._process_item_ids = item_ids
|
||||
self._emit_event("submit.started")
|
||||
if wait:
|
||||
self._submit_callback()
|
||||
self._process_item_id = None
|
||||
self._process_item_ids = []
|
||||
return item_id
|
||||
|
||||
thread = threading.Thread(target=self._submit_callback)
|
||||
self._process_thread = thread
|
||||
thread.start()
|
||||
return item_id
|
||||
return item_ids
|
||||
|
||||
def wait_for_process_thread(self):
|
||||
if self._process_thread is None:
|
||||
|
|
@ -207,7 +236,7 @@ class PushToContextController:
|
|||
self._process_thread = None
|
||||
|
||||
def _prepare_source_label(self):
|
||||
if not self._src_project_name or not self._src_version_id:
|
||||
if not self._src_project_name or not self._src_version_ids:
|
||||
return "Source is not defined"
|
||||
|
||||
folder_entity = self._src_folder_entity
|
||||
|
|
@ -215,14 +244,21 @@ class PushToContextController:
|
|||
return "Source is invalid"
|
||||
|
||||
folder_path = folder_entity["path"]
|
||||
product_entity = self._src_product_entity
|
||||
version_entity = self._src_version_entity
|
||||
return "Source: {}{}/{}/v{:0>3}".format(
|
||||
self._src_project_name,
|
||||
folder_path,
|
||||
product_entity["name"],
|
||||
version_entity["version"]
|
||||
)
|
||||
src_labels = []
|
||||
for version_entity in self._src_version_entities:
|
||||
product_entity = self._src_product_entities.get(
|
||||
version_entity["productId"]
|
||||
)
|
||||
src_labels.append(
|
||||
"Source: {}{}/{}/v{:0>3}".format(
|
||||
self._src_project_name,
|
||||
folder_path,
|
||||
product_entity["name"],
|
||||
version_entity["version"],
|
||||
)
|
||||
)
|
||||
|
||||
return "\n".join(src_labels)
|
||||
|
||||
def _get_task_info_from_repre_entities(
|
||||
self, task_entities, repre_entities
|
||||
|
|
@ -256,7 +292,8 @@ class PushToContextController:
|
|||
|
||||
def _get_src_variant(self):
|
||||
project_name = self._src_project_name
|
||||
version_entity = self._src_version_entity
|
||||
# parse variant only from first version
|
||||
version_entity = self._src_version_entities[0]
|
||||
task_entities = self._src_folder_task_entities
|
||||
repre_entities = ayon_api.get_representations(
|
||||
project_name, version_ids={version_entity["id"]}
|
||||
|
|
@ -264,9 +301,12 @@ class PushToContextController:
|
|||
task_name, task_type = self._get_task_info_from_repre_entities(
|
||||
task_entities, repre_entities
|
||||
)
|
||||
product_entity = self._src_product_entities.get(
|
||||
version_entity["productId"]
|
||||
)
|
||||
|
||||
project_settings = get_project_settings(project_name)
|
||||
product_type = self._src_product_entity["productType"]
|
||||
product_type = product_entity["productType"]
|
||||
template = get_product_name_template(
|
||||
self._src_project_name,
|
||||
product_type,
|
||||
|
|
@ -300,7 +340,7 @@ class PushToContextController:
|
|||
print("Failed format", exc)
|
||||
return ""
|
||||
|
||||
product_name = self._src_product_entity["name"]
|
||||
product_name = product_entity["name"]
|
||||
if (
|
||||
(product_s and not product_name.startswith(product_s))
|
||||
or (product_e and not product_name.endswith(product_e))
|
||||
|
|
@ -314,9 +354,6 @@ class PushToContextController:
|
|||
return product_name
|
||||
|
||||
def _check_submit_validations(self):
|
||||
if not self._user_values.is_valid:
|
||||
return False
|
||||
|
||||
if not self._selection_model.get_selected_project_name():
|
||||
return False
|
||||
|
||||
|
|
@ -325,6 +362,13 @@ class PushToContextController:
|
|||
and not self._selection_model.get_selected_folder_id()
|
||||
):
|
||||
return False
|
||||
|
||||
if self._use_original_name:
|
||||
return True
|
||||
|
||||
if not self._user_values.is_valid:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _invalidate(self):
|
||||
|
|
@ -338,13 +382,14 @@ class PushToContextController:
|
|||
)
|
||||
|
||||
def _submit_callback(self):
|
||||
process_item_id = self._process_item_id
|
||||
if process_item_id is None:
|
||||
return
|
||||
self._integrate_model.integrate_item(process_item_id)
|
||||
process_item_ids = self._process_item_ids
|
||||
for process_item_id in process_item_ids:
|
||||
self._integrate_model.integrate_item(process_item_id)
|
||||
|
||||
self._emit_event("submit.finished", {})
|
||||
if process_item_id == self._process_item_id:
|
||||
self._process_item_id = None
|
||||
|
||||
if process_item_ids is self._process_item_ids:
|
||||
self._process_item_ids = []
|
||||
|
||||
def _emit_event(self, topic, data=None):
|
||||
if data is None:
|
||||
|
|
|
|||
|
|
@ -4,28 +4,28 @@ from ayon_core.tools.utils import get_ayon_qt_app
|
|||
from ayon_core.tools.push_to_project.ui import PushToContextSelectWindow
|
||||
|
||||
|
||||
def main_show(project_name, version_id):
|
||||
def main_show(project_name, version_ids):
|
||||
app = get_ayon_qt_app()
|
||||
|
||||
window = PushToContextSelectWindow()
|
||||
window.show()
|
||||
window.set_source(project_name, version_id)
|
||||
window.set_source(project_name, version_ids)
|
||||
|
||||
app.exec_()
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("--project", help="Source project name")
|
||||
@click.option("--version", help="Source version id")
|
||||
def main(project, version):
|
||||
@click.option("--versions", help="Source version ids")
|
||||
def main(project, versions):
|
||||
"""Run PushToProject tool to integrate version in different project.
|
||||
|
||||
Args:
|
||||
project (str): Source project name.
|
||||
version (str): Version id.
|
||||
versions (str): comma separated versions for same context
|
||||
"""
|
||||
|
||||
main_show(project, version)
|
||||
main_show(project, versions.split(","))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import itertools
|
|||
import sys
|
||||
import traceback
|
||||
import uuid
|
||||
from typing import Optional, Dict
|
||||
|
||||
import ayon_api
|
||||
from ayon_api.utils import create_entity_id
|
||||
|
|
@ -21,6 +22,7 @@ from ayon_core.lib import (
|
|||
source_hash,
|
||||
)
|
||||
from ayon_core.lib.file_transaction import FileTransaction
|
||||
from ayon_core.pipeline.thumbnails import get_thumbnail_path
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.pipeline import Anatomy
|
||||
from ayon_core.pipeline.version_start import get_versioning_start
|
||||
|
|
@ -88,6 +90,7 @@ class ProjectPushItem:
|
|||
new_folder_name,
|
||||
dst_version,
|
||||
item_id=None,
|
||||
use_original_name=False
|
||||
):
|
||||
if not item_id:
|
||||
item_id = uuid.uuid4().hex
|
||||
|
|
@ -102,6 +105,7 @@ class ProjectPushItem:
|
|||
self.comment = comment or ""
|
||||
self.item_id = item_id
|
||||
self._repr_value = None
|
||||
self.use_original_name = use_original_name
|
||||
|
||||
@property
|
||||
def _repr(self):
|
||||
|
|
@ -113,7 +117,8 @@ class ProjectPushItem:
|
|||
str(self.dst_folder_id),
|
||||
str(self.new_folder_name),
|
||||
str(self.dst_task_name),
|
||||
str(self.dst_version)
|
||||
str(self.dst_version),
|
||||
self.use_original_name
|
||||
])
|
||||
return self._repr_value
|
||||
|
||||
|
|
@ -132,6 +137,7 @@ class ProjectPushItem:
|
|||
"comment": self.comment,
|
||||
"new_folder_name": self.new_folder_name,
|
||||
"item_id": self.item_id,
|
||||
"use_original_name": self.use_original_name
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
|
@ -311,7 +317,7 @@ class ProjectPushRepreItem:
|
|||
if self._src_files is not None:
|
||||
return self._src_files, self._resource_files
|
||||
|
||||
repre_context = self._repre_entity["context"]
|
||||
repre_context = self.repre_entity["context"]
|
||||
if "frame" in repre_context or "udim" in repre_context:
|
||||
src_files, resource_files = self._get_source_files_with_frames()
|
||||
else:
|
||||
|
|
@ -328,7 +334,7 @@ class ProjectPushRepreItem:
|
|||
udim_placeholder = "__udim__"
|
||||
src_files = []
|
||||
resource_files = []
|
||||
template = self._repre_entity["attrib"]["template"]
|
||||
template = self.repre_entity["attrib"]["template"]
|
||||
# Remove padding from 'udim' and 'frame' formatting keys
|
||||
# - "{frame:0>4}" -> "{frame}"
|
||||
for key in ("udim", "frame"):
|
||||
|
|
@ -336,7 +342,7 @@ class ProjectPushRepreItem:
|
|||
replacement = "{{{}}}".format(key)
|
||||
template = re.sub(sub_part, replacement, template)
|
||||
|
||||
repre_context = self._repre_entity["context"]
|
||||
repre_context = self.repre_entity["context"]
|
||||
fill_repre_context = copy.deepcopy(repre_context)
|
||||
if "frame" in fill_repre_context:
|
||||
fill_repre_context["frame"] = frame_placeholder
|
||||
|
|
@ -357,7 +363,7 @@ class ProjectPushRepreItem:
|
|||
.replace(udim_placeholder, "(?P<udim>[0-9]+)")
|
||||
)
|
||||
src_basename_regex = re.compile("^{}$".format(src_basename))
|
||||
for file_info in self._repre_entity["files"]:
|
||||
for file_info in self.repre_entity["files"]:
|
||||
filepath_template = self._clean_path(file_info["path"])
|
||||
filepath = self._clean_path(
|
||||
filepath_template.format(root=self._roots)
|
||||
|
|
@ -371,7 +377,6 @@ class ProjectPushRepreItem:
|
|||
resource_files.append(ResourceFile(filepath, relative_path))
|
||||
continue
|
||||
|
||||
filepath = os.path.join(src_dirpath, basename)
|
||||
frame = None
|
||||
udim = None
|
||||
for item in src_basename_regex.finditer(basename):
|
||||
|
|
@ -389,8 +394,8 @@ class ProjectPushRepreItem:
|
|||
def _get_source_files(self):
|
||||
src_files = []
|
||||
resource_files = []
|
||||
template = self._repre_entity["attrib"]["template"]
|
||||
repre_context = self._repre_entity["context"]
|
||||
template = self.repre_entity["attrib"]["template"]
|
||||
repre_context = self.repre_entity["context"]
|
||||
fill_repre_context = copy.deepcopy(repre_context)
|
||||
fill_roots = fill_repre_context["root"]
|
||||
for root_name in tuple(fill_roots.keys()):
|
||||
|
|
@ -399,7 +404,7 @@ class ProjectPushRepreItem:
|
|||
fill_repre_context)
|
||||
repre_path = self._clean_path(repre_path)
|
||||
src_dirpath = os.path.dirname(repre_path)
|
||||
for file_info in self._repre_entity["files"]:
|
||||
for file_info in self.repre_entity["files"]:
|
||||
filepath_template = self._clean_path(file_info["path"])
|
||||
filepath = self._clean_path(
|
||||
filepath_template.format(root=self._roots))
|
||||
|
|
@ -492,8 +497,11 @@ class ProjectPushItemProcess:
|
|||
|
||||
except Exception as exc:
|
||||
_exc, _value, _tb = sys.exc_info()
|
||||
product_name = self._src_product_entity["name"]
|
||||
self._status.set_failed(
|
||||
"Unhandled error happened: {}".format(str(exc)),
|
||||
"Unhandled error happened for `{}`: {}".format(
|
||||
product_name, str(exc)
|
||||
),
|
||||
(_exc, _value, _tb)
|
||||
)
|
||||
|
||||
|
|
@ -816,31 +824,34 @@ class ProjectPushItemProcess:
|
|||
self._template_name = template_name
|
||||
|
||||
def _determine_product_name(self):
|
||||
product_type = self._product_type
|
||||
task_info = self._task_info
|
||||
task_name = task_type = None
|
||||
if task_info:
|
||||
task_name = task_info["name"]
|
||||
task_type = task_info["taskType"]
|
||||
if self._item.use_original_name:
|
||||
product_name = self._src_product_entity["name"]
|
||||
else:
|
||||
product_type = self._product_type
|
||||
task_info = self._task_info
|
||||
task_name = task_type = None
|
||||
if task_info:
|
||||
task_name = task_info["name"]
|
||||
task_type = task_info["taskType"]
|
||||
|
||||
try:
|
||||
product_name = get_product_name(
|
||||
self._item.dst_project_name,
|
||||
task_name,
|
||||
task_type,
|
||||
self.host_name,
|
||||
product_type,
|
||||
self._item.variant,
|
||||
project_settings=self._project_settings
|
||||
)
|
||||
except TaskNotSetError:
|
||||
self._status.set_failed(
|
||||
"Target product name template requires task name. To continue"
|
||||
" you have to select target task or change settings"
|
||||
" <b>ayon+settings://core/tools/creator/product_name_profiles"
|
||||
f"?project={self._item.dst_project_name}</b>."
|
||||
)
|
||||
raise PushToProjectError(self._status.fail_reason)
|
||||
try:
|
||||
product_name = get_product_name(
|
||||
self._item.dst_project_name,
|
||||
task_name,
|
||||
task_type,
|
||||
self.host_name,
|
||||
product_type,
|
||||
self._item.variant,
|
||||
project_settings=self._project_settings
|
||||
)
|
||||
except TaskNotSetError:
|
||||
self._status.set_failed(
|
||||
"Target product name template requires task name. To "
|
||||
"continue you have to select target task or change settings " # noqa: E501
|
||||
" <b>ayon+settings://core/tools/creator/product_name_profiles" # noqa: E501
|
||||
f"?project={self._item.dst_project_name}</b>."
|
||||
)
|
||||
raise PushToProjectError(self._status.fail_reason)
|
||||
|
||||
self._log_info(
|
||||
f"Push will be integrating to product with name '{product_name}'"
|
||||
|
|
@ -917,14 +928,19 @@ class ProjectPushItemProcess:
|
|||
task_name=self._task_info["name"],
|
||||
task_type=self._task_info["taskType"],
|
||||
product_type=product_type,
|
||||
product_name=product_entity["name"]
|
||||
product_name=product_entity["name"],
|
||||
)
|
||||
|
||||
existing_version_entity = ayon_api.get_version_by_name(
|
||||
project_name, version, product_id
|
||||
)
|
||||
thumbnail_id = self._copy_version_thumbnail()
|
||||
|
||||
# Update existing version
|
||||
if existing_version_entity:
|
||||
updata_data = {"attrib": dst_attrib}
|
||||
if thumbnail_id:
|
||||
updata_data["thumbnailId"] = thumbnail_id
|
||||
self._operations.update_entity(
|
||||
project_name,
|
||||
"version",
|
||||
|
|
@ -939,6 +955,7 @@ class ProjectPushItemProcess:
|
|||
version,
|
||||
product_id,
|
||||
attribs=dst_attrib,
|
||||
thumbnail_id=thumbnail_id,
|
||||
)
|
||||
self._operations.create_entity(
|
||||
project_name, "version", version_entity
|
||||
|
|
@ -1005,10 +1022,18 @@ class ProjectPushItemProcess:
|
|||
self, anatomy, template_name, formatting_data, file_template
|
||||
):
|
||||
processed_repre_items = []
|
||||
repre_context = None
|
||||
for repre_item in self._src_repre_items:
|
||||
repre_entity = repre_item.repre_entity
|
||||
repre_name = repre_entity["name"]
|
||||
repre_format_data = copy.deepcopy(formatting_data)
|
||||
|
||||
if not repre_context:
|
||||
repre_context = self._update_repre_context(
|
||||
copy.deepcopy(repre_entity),
|
||||
formatting_data
|
||||
)
|
||||
|
||||
repre_format_data["representation"] = repre_name
|
||||
for src_file in repre_item.src_files:
|
||||
ext = os.path.splitext(src_file.path)[-1]
|
||||
|
|
@ -1024,7 +1049,6 @@ class ProjectPushItemProcess:
|
|||
"publish", template_name, "directory"
|
||||
)
|
||||
folder_path = template_obj.format_strict(formatting_data)
|
||||
repre_context = folder_path.used_values
|
||||
folder_path_rootless = folder_path.rootless
|
||||
repre_filepaths = []
|
||||
published_path = None
|
||||
|
|
@ -1047,7 +1071,6 @@ class ProjectPushItemProcess:
|
|||
)
|
||||
if published_path is None or frame == repre_item.frame:
|
||||
published_path = dst_filepath
|
||||
repre_context.update(filename.used_values)
|
||||
|
||||
repre_filepaths.append((dst_filepath, dst_rootless_path))
|
||||
self._file_transaction.add(src_file.path, dst_filepath)
|
||||
|
|
@ -1134,7 +1157,7 @@ class ProjectPushItemProcess:
|
|||
self._item.dst_project_name,
|
||||
"representation",
|
||||
entity_id,
|
||||
changes
|
||||
changes,
|
||||
)
|
||||
|
||||
existing_repre_names = set(existing_repres_by_low_name.keys())
|
||||
|
|
@ -1147,6 +1170,45 @@ class ProjectPushItemProcess:
|
|||
{"active": False}
|
||||
)
|
||||
|
||||
def _copy_version_thumbnail(self) -> Optional[str]:
|
||||
thumbnail_id = self._src_version_entity["thumbnailId"]
|
||||
if not thumbnail_id:
|
||||
return None
|
||||
path = get_thumbnail_path(
|
||||
self._item.src_project_name,
|
||||
"version",
|
||||
self._src_version_entity["id"],
|
||||
thumbnail_id
|
||||
)
|
||||
if not path:
|
||||
return None
|
||||
return ayon_api.create_thumbnail(
|
||||
self._item.dst_project_name,
|
||||
path
|
||||
)
|
||||
|
||||
def _update_repre_context(self, repre_entity, formatting_data):
|
||||
"""Replace old context value with new ones.
|
||||
|
||||
Folder might change, project definitely changes etc.
|
||||
"""
|
||||
repre_context = repre_entity["context"]
|
||||
for context_key, context_value in repre_context.items():
|
||||
if context_value and isinstance(context_value, dict):
|
||||
for context_sub_key in context_value.keys():
|
||||
value_to_update = formatting_data.get(context_key, {}).get(
|
||||
context_sub_key)
|
||||
if value_to_update:
|
||||
repre_context[context_key][
|
||||
context_sub_key] = value_to_update
|
||||
else:
|
||||
value_to_update = formatting_data.get(context_key)
|
||||
if value_to_update:
|
||||
repre_context[context_key] = value_to_update
|
||||
if "task" not in formatting_data:
|
||||
repre_context.pop("task")
|
||||
return repre_context
|
||||
|
||||
|
||||
class IntegrateModel:
|
||||
def __init__(self, controller):
|
||||
|
|
@ -1170,6 +1232,7 @@ class IntegrateModel:
|
|||
comment,
|
||||
new_folder_name,
|
||||
dst_version,
|
||||
use_original_name
|
||||
):
|
||||
"""Create new item for integration.
|
||||
|
||||
|
|
@ -1183,6 +1246,7 @@ class IntegrateModel:
|
|||
comment (Union[str, None]): Comment.
|
||||
new_folder_name (Union[str, None]): New folder name.
|
||||
dst_version (int): Destination version number.
|
||||
use_original_name (bool): If original product names should be used
|
||||
|
||||
Returns:
|
||||
str: Item id. The id can be used to trigger integration or get
|
||||
|
|
@ -1198,7 +1262,8 @@ class IntegrateModel:
|
|||
variant,
|
||||
comment=comment,
|
||||
new_folder_name=new_folder_name,
|
||||
dst_version=dst_version
|
||||
dst_version=dst_version,
|
||||
use_original_name=use_original_name
|
||||
)
|
||||
process_item = ProjectPushItemProcess(self, item)
|
||||
self._process_items[item.item_id] = process_item
|
||||
|
|
@ -1216,17 +1281,6 @@ class IntegrateModel:
|
|||
return
|
||||
item.integrate()
|
||||
|
||||
def get_item_status(self, item_id):
|
||||
"""Status of an item.
|
||||
|
||||
Args:
|
||||
item_id (str): Item id for which status should be returned.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Status data.
|
||||
"""
|
||||
|
||||
item = self._process_items.get(item_id)
|
||||
if item is not None:
|
||||
return item.get_status_data()
|
||||
return None
|
||||
def get_items(self) -> Dict[str, ProjectPushItemProcess]:
|
||||
"""Returns dict of all ProjectPushItemProcess items """
|
||||
return self._process_items
|
||||
|
|
|
|||
|
|
@ -85,6 +85,13 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
|
||||
header_widget = QtWidgets.QWidget(main_context_widget)
|
||||
|
||||
library_only_label = QtWidgets.QLabel(
|
||||
"Show only libraries",
|
||||
header_widget
|
||||
)
|
||||
library_only_checkbox = NiceCheckbox(
|
||||
True, parent=header_widget)
|
||||
|
||||
header_label = QtWidgets.QLabel(
|
||||
controller.get_source_label(),
|
||||
header_widget
|
||||
|
|
@ -92,7 +99,9 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
|
||||
header_layout = QtWidgets.QHBoxLayout(header_widget)
|
||||
header_layout.setContentsMargins(0, 0, 0, 0)
|
||||
header_layout.addWidget(header_label)
|
||||
header_layout.addWidget(header_label, 1)
|
||||
header_layout.addWidget(library_only_label, 0)
|
||||
header_layout.addWidget(library_only_checkbox, 0)
|
||||
|
||||
main_splitter = QtWidgets.QSplitter(
|
||||
QtCore.Qt.Horizontal, main_context_widget
|
||||
|
|
@ -124,6 +133,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
inputs_widget = QtWidgets.QWidget(main_splitter)
|
||||
|
||||
new_folder_checkbox = NiceCheckbox(True, parent=inputs_widget)
|
||||
original_names_checkbox = NiceCheckbox(False, parent=inputs_widget)
|
||||
|
||||
folder_name_input = PlaceholderLineEdit(inputs_widget)
|
||||
folder_name_input.setPlaceholderText("< Name of new folder >")
|
||||
|
|
@ -142,6 +152,8 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
inputs_layout.addRow("Create new folder", new_folder_checkbox)
|
||||
inputs_layout.addRow("New folder name", folder_name_input)
|
||||
inputs_layout.addRow("Variant", variant_input)
|
||||
inputs_layout.addRow(
|
||||
"Use original product names", original_names_checkbox)
|
||||
inputs_layout.addRow("Comment", comment_input)
|
||||
|
||||
main_splitter.addWidget(context_widget)
|
||||
|
|
@ -196,6 +208,10 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
show_detail_btn.setToolTip(
|
||||
"Show error detail dialog to copy full error."
|
||||
)
|
||||
original_names_checkbox.setToolTip(
|
||||
"Required for multi copy, doesn't allow changes "
|
||||
"variant values."
|
||||
)
|
||||
|
||||
overlay_close_btn = QtWidgets.QPushButton(
|
||||
"Close", overlay_btns_widget
|
||||
|
|
@ -240,6 +256,9 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
folder_name_input.textChanged.connect(self._on_new_folder_change)
|
||||
variant_input.textChanged.connect(self._on_variant_change)
|
||||
comment_input.textChanged.connect(self._on_comment_change)
|
||||
library_only_checkbox.stateChanged.connect(self._on_library_only_change)
|
||||
original_names_checkbox.stateChanged.connect(
|
||||
self._on_original_names_change)
|
||||
|
||||
publish_btn.clicked.connect(self._on_select_click)
|
||||
cancel_btn.clicked.connect(self._on_close_click)
|
||||
|
|
@ -288,6 +307,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
self._new_folder_checkbox = new_folder_checkbox
|
||||
self._folder_name_input = folder_name_input
|
||||
self._comment_input = comment_input
|
||||
self._use_original_names_checkbox = original_names_checkbox
|
||||
|
||||
self._publish_btn = publish_btn
|
||||
|
||||
|
|
@ -316,7 +336,6 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
self._main_thread_timer = main_thread_timer
|
||||
self._main_thread_timer_can_stop = True
|
||||
self._last_submit_message = None
|
||||
self._process_item_id = None
|
||||
|
||||
self._variant_is_valid = None
|
||||
self._folder_is_valid = None
|
||||
|
|
@ -327,17 +346,17 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
overlay_try_btn.setVisible(False)
|
||||
|
||||
# Support of public api function of controller
|
||||
def set_source(self, project_name, version_id):
|
||||
def set_source(self, project_name, version_ids):
|
||||
"""Set source project and version.
|
||||
|
||||
Call the method on controller.
|
||||
|
||||
Args:
|
||||
project_name (Union[str, None]): Name of project.
|
||||
version_id (Union[str, None]): Version id.
|
||||
version_ids (Union[str, None]): comma separated Version ids.
|
||||
"""
|
||||
|
||||
self._controller.set_source(project_name, version_id)
|
||||
self._controller.set_source(project_name, version_ids)
|
||||
|
||||
def showEvent(self, event):
|
||||
super(PushToContextSelectWindow, self).showEvent(event)
|
||||
|
|
@ -352,10 +371,12 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
self._folder_name_input.setText(new_folder_name or "")
|
||||
self._variant_input.setText(variant or "")
|
||||
self._invalidate_variant(user_values["is_variant_valid"])
|
||||
self._invalidate_use_original_names(
|
||||
self._use_original_names_checkbox.isChecked())
|
||||
self._invalidate_new_folder_name(
|
||||
new_folder_name, user_values["is_new_folder_name_valid"]
|
||||
)
|
||||
|
||||
self._controller._invalidate()
|
||||
self._projects_combobox.refresh()
|
||||
|
||||
def _on_first_show(self):
|
||||
|
|
@ -394,6 +415,15 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
self._comment_input_text = text
|
||||
self._user_input_changed_timer.start()
|
||||
|
||||
def _on_library_only_change(self, state: int) -> None:
|
||||
"""Change toggle state, reset filter, recalculate dropdown"""
|
||||
state = bool(state)
|
||||
self._projects_combobox.set_standard_filter_enabled(state)
|
||||
|
||||
def _on_original_names_change(self, state: int) -> None:
|
||||
use_original_name = bool(state)
|
||||
self._invalidate_use_original_names(use_original_name)
|
||||
|
||||
def _on_user_input_timer(self):
|
||||
folder_name_enabled = self._new_folder_name_enabled
|
||||
folder_name = self._new_folder_name_input_text
|
||||
|
|
@ -456,17 +486,27 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
state = ""
|
||||
if folder_name is not None:
|
||||
state = "valid" if is_valid else "invalid"
|
||||
set_style_property(
|
||||
self._folder_name_input, "state", state
|
||||
)
|
||||
set_style_property(self._folder_name_input, "state", state)
|
||||
|
||||
def _invalidate_variant(self, is_valid):
|
||||
if self._variant_is_valid is is_valid:
|
||||
return
|
||||
self._variant_is_valid = is_valid
|
||||
state = "valid" if is_valid else "invalid"
|
||||
set_style_property(self._variant_input, "state", state)
|
||||
|
||||
def _invalidate_use_original_names(self, use_original_names):
|
||||
"""Checks if original names must be used.
|
||||
|
||||
Invalidates Variant if necessary
|
||||
"""
|
||||
if self._controller.original_names_required():
|
||||
use_original_names = True
|
||||
|
||||
self._variant_input.setEnabled(not use_original_names)
|
||||
self._invalidate_variant(not use_original_names)
|
||||
|
||||
self._controller._use_original_name = use_original_names
|
||||
self._use_original_names_checkbox.setChecked(use_original_names)
|
||||
|
||||
def _on_submission_change(self, event):
|
||||
self._publish_btn.setEnabled(event["enabled"])
|
||||
|
||||
|
|
@ -495,31 +535,43 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
self._overlay_label.setText(self._last_submit_message)
|
||||
self._last_submit_message = None
|
||||
|
||||
process_status = self._controller.get_process_item_status(
|
||||
self._process_item_id
|
||||
)
|
||||
push_failed = process_status["failed"]
|
||||
fail_traceback = process_status["full_traceback"]
|
||||
failed_pushes = []
|
||||
fail_tracebacks = []
|
||||
for process_item in self._controller.get_process_items().values():
|
||||
process_status = process_item.get_status_data()
|
||||
if process_status["failed"]:
|
||||
failed_pushes.append(process_status)
|
||||
# push_failed = process_status["failed"]
|
||||
# fail_traceback = process_status["full_traceback"]
|
||||
if self._main_thread_timer_can_stop:
|
||||
self._main_thread_timer.stop()
|
||||
self._overlay_close_btn.setVisible(True)
|
||||
if push_failed:
|
||||
if failed_pushes:
|
||||
self._overlay_try_btn.setVisible(True)
|
||||
if fail_traceback:
|
||||
fail_tracebacks = [
|
||||
process_status["full_traceback"]
|
||||
for process_status in failed_pushes
|
||||
if process_status["full_traceback"]
|
||||
]
|
||||
if fail_tracebacks:
|
||||
self._show_detail_btn.setVisible(True)
|
||||
|
||||
if push_failed:
|
||||
reason = process_status["fail_reason"]
|
||||
if fail_traceback:
|
||||
if failed_pushes:
|
||||
reasons = [
|
||||
process_status["fail_reason"]
|
||||
for process_status in failed_pushes
|
||||
]
|
||||
if fail_tracebacks:
|
||||
reason = "\n".join(reasons)
|
||||
message = (
|
||||
"Unhandled error happened."
|
||||
" Check error detail for more information."
|
||||
)
|
||||
self._error_detail_dialog.set_detail(
|
||||
reason, fail_traceback
|
||||
reason, "\n".join(fail_tracebacks)
|
||||
)
|
||||
else:
|
||||
message = f"Push Failed:\n{reason}"
|
||||
message = f"Push Failed:\n{reasons}"
|
||||
|
||||
self._overlay_label.setText(message)
|
||||
set_style_property(self._overlay_close_btn, "state", "error")
|
||||
|
|
@ -534,7 +586,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget):
|
|||
self._main_thread_timer_can_stop = False
|
||||
self._main_thread_timer.start()
|
||||
self._main_layout.setCurrentWidget(self._overlay_widget)
|
||||
self._overlay_label.setText("Submittion started")
|
||||
self._overlay_label.setText("Submission started")
|
||||
|
||||
def _on_controller_submit_end(self):
|
||||
self._main_thread_timer_can_stop = True
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
from typing import Optional
|
||||
|
||||
import ayon_api
|
||||
|
||||
from ayon_core.lib.events import QueuedEventSystem
|
||||
from ayon_core.host import HostBase
|
||||
from ayon_core.host import ILoadHost
|
||||
from ayon_core.pipeline import (
|
||||
registered_host,
|
||||
get_current_context,
|
||||
)
|
||||
from ayon_core.tools.common_models import HierarchyModel, ProjectsModel
|
||||
from ayon_core.tools.common_models import (
|
||||
HierarchyModel,
|
||||
ProjectsModel,
|
||||
ProductTypeIconMapping,
|
||||
)
|
||||
|
||||
from .models import SiteSyncModel, ContainersModel
|
||||
|
||||
|
|
@ -35,7 +41,7 @@ class SceneInventoryController:
|
|||
self._projects_model = ProjectsModel(self)
|
||||
self._event_system = self._create_event_system()
|
||||
|
||||
def get_host(self) -> HostBase:
|
||||
def get_host(self) -> ILoadHost:
|
||||
return self._host
|
||||
|
||||
def emit_event(self, topic, data=None, source=None):
|
||||
|
|
@ -93,6 +99,13 @@ class SceneInventoryController:
|
|||
project_name, None
|
||||
)
|
||||
|
||||
def get_product_type_icons_mapping(
|
||||
self, project_name: Optional[str]
|
||||
) -> ProductTypeIconMapping:
|
||||
return self._projects_model.get_product_type_icons_mapping(
|
||||
project_name
|
||||
)
|
||||
|
||||
# Containers methods
|
||||
def get_containers(self):
|
||||
return self._containers_model.get_containers()
|
||||
|
|
|
|||
|
|
@ -214,9 +214,6 @@ class InventoryModel(QtGui.QStandardItemModel):
|
|||
group_icon = qtawesome.icon(
|
||||
"fa.object-group", color=self._default_icon_color
|
||||
)
|
||||
product_type_icon = qtawesome.icon(
|
||||
"fa.folder", color="#0091B2"
|
||||
)
|
||||
group_item_font = QtGui.QFont()
|
||||
group_item_font.setBold(True)
|
||||
|
||||
|
|
@ -303,7 +300,7 @@ class InventoryModel(QtGui.QStandardItemModel):
|
|||
remote_site_progress = "{}%".format(
|
||||
max(progress["remote_site"], 0) * 100
|
||||
)
|
||||
|
||||
product_type_icon = get_qt_icon(repre_info.product_type_icon)
|
||||
group_item = QtGui.QStandardItem()
|
||||
group_item.setColumnCount(root_item.columnCount())
|
||||
group_item.setData(group_name, QtCore.Qt.DisplayRole)
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ class RepresentationInfo:
|
|||
product_id,
|
||||
product_name,
|
||||
product_type,
|
||||
product_type_icon,
|
||||
product_group,
|
||||
version_id,
|
||||
representation_name,
|
||||
|
|
@ -135,6 +136,7 @@ class RepresentationInfo:
|
|||
self.product_id = product_id
|
||||
self.product_name = product_name
|
||||
self.product_type = product_type
|
||||
self.product_type_icon = product_type_icon
|
||||
self.product_group = product_group
|
||||
self.version_id = version_id
|
||||
self.representation_name = representation_name
|
||||
|
|
@ -153,7 +155,17 @@ class RepresentationInfo:
|
|||
|
||||
@classmethod
|
||||
def new_invalid(cls):
|
||||
return cls(None, None, None, None, None, None, None, None)
|
||||
return cls(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
class VersionItem:
|
||||
|
|
@ -229,6 +241,9 @@ class ContainersModel:
|
|||
def get_representation_info_items(self, project_name, representation_ids):
|
||||
output = {}
|
||||
missing_repre_ids = set()
|
||||
icons_mapping = self._controller.get_product_type_icons_mapping(
|
||||
project_name
|
||||
)
|
||||
for repre_id in representation_ids:
|
||||
try:
|
||||
uuid.UUID(repre_id)
|
||||
|
|
@ -253,6 +268,7 @@ class ContainersModel:
|
|||
"product_id": None,
|
||||
"product_name": None,
|
||||
"product_type": None,
|
||||
"product_type_icon": None,
|
||||
"product_group": None,
|
||||
"version_id": None,
|
||||
"representation_name": None,
|
||||
|
|
@ -265,10 +281,17 @@ class ContainersModel:
|
|||
kwargs["folder_id"] = folder["id"]
|
||||
kwargs["folder_path"] = folder["path"]
|
||||
if product:
|
||||
product_type = product["productType"]
|
||||
product_base_type = product.get("productBaseType")
|
||||
icon = icons_mapping.get_icon(
|
||||
product_base_type=product_base_type,
|
||||
product_type=product_type,
|
||||
)
|
||||
group = product["attrib"]["productGroup"]
|
||||
kwargs["product_id"] = product["id"]
|
||||
kwargs["product_name"] = product["name"]
|
||||
kwargs["product_type"] = product["productType"]
|
||||
kwargs["product_type_icon"] = icon
|
||||
kwargs["product_group"] = group
|
||||
if version:
|
||||
kwargs["version_id"] = version["id"]
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ class SelectVersionComboBox(QtWidgets.QComboBox):
|
|||
status_text_rect.setLeft(icon_rect.right() + 2)
|
||||
|
||||
if status_text_rect.width() <= 0:
|
||||
painter.restore()
|
||||
return
|
||||
|
||||
if status_text_rect.width() < metrics.width(status_name):
|
||||
|
|
@ -144,6 +145,7 @@ class SelectVersionComboBox(QtWidgets.QComboBox):
|
|||
QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,
|
||||
status_name
|
||||
)
|
||||
painter.restore()
|
||||
|
||||
def set_current_index(self, index):
|
||||
model = self._combo_view.model()
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
Subset manager
|
||||
--------------
|
||||
|
||||
Simple UI showing list of created subset that will be published via Pyblish.
|
||||
Useful for applications (Photoshop, AfterEffects, TVPaint, Harmony) which are
|
||||
storing metadata about instance hidden from user.
|
||||
|
||||
This UI allows listing all created subset and removal of them if needed (
|
||||
in case use doesn't want to publish anymore, its using workfile as a starting
|
||||
file for different task and instances should be completely different etc.
|
||||
)
|
||||
|
||||
Host is expected to implemented:
|
||||
- `list_instances` - returning list of dictionaries (instances), must contain
|
||||
unique uuid field
|
||||
example:
|
||||
```[{"uuid":"15","active":true,"subset":"imageBG","family":"image","id":"ayon.create.instance","asset":"Town"}]```
|
||||
- `remove_instance(instance)` - removes instance from file's metadata
|
||||
instance is a dictionary, with uuid field
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
from .window import (
|
||||
show,
|
||||
SubsetManagerWindow
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"show",
|
||||
"SubsetManagerWindow"
|
||||
)
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
import uuid
|
||||
|
||||
from qtpy import QtCore, QtGui
|
||||
|
||||
from ayon_core.pipeline import registered_host
|
||||
|
||||
ITEM_ID_ROLE = QtCore.Qt.UserRole + 1
|
||||
|
||||
|
||||
class InstanceModel(QtGui.QStandardItemModel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(InstanceModel, self).__init__(*args, **kwargs)
|
||||
self._instances_by_item_id = {}
|
||||
|
||||
def get_instance_by_id(self, item_id):
|
||||
return self._instances_by_item_id.get(item_id)
|
||||
|
||||
def refresh(self):
|
||||
self.clear()
|
||||
|
||||
self._instances_by_item_id = {}
|
||||
|
||||
instances = None
|
||||
host = registered_host()
|
||||
list_instances = getattr(host, "list_instances", None)
|
||||
if list_instances:
|
||||
instances = list_instances()
|
||||
|
||||
if not instances:
|
||||
return
|
||||
|
||||
items = []
|
||||
for instance_data in instances:
|
||||
item_id = str(uuid.uuid4())
|
||||
product_name = (
|
||||
instance_data.get("productName")
|
||||
or instance_data.get("subset")
|
||||
)
|
||||
label = instance_data.get("label") or product_name
|
||||
item = QtGui.QStandardItem(label)
|
||||
item.setEnabled(True)
|
||||
item.setEditable(False)
|
||||
item.setData(item_id, ITEM_ID_ROLE)
|
||||
items.append(item)
|
||||
self._instances_by_item_id[item_id] = instance_data
|
||||
|
||||
if items:
|
||||
self.invisibleRootItem().appendRows(items)
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if role == QtCore.Qt.DisplayRole and section == 0:
|
||||
return "Instance"
|
||||
|
||||
return super(InstanceModel, self).headerData(
|
||||
section, orientation, role
|
||||
)
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
import json
|
||||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
|
||||
class InstanceDetail(QtWidgets.QWidget):
|
||||
save_triggered = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(InstanceDetail, self).__init__(parent)
|
||||
|
||||
details_widget = QtWidgets.QPlainTextEdit(self)
|
||||
details_widget.setObjectName("SubsetManagerDetailsText")
|
||||
|
||||
save_btn = QtWidgets.QPushButton("Save", self)
|
||||
|
||||
self._block_changes = False
|
||||
self._editable = False
|
||||
self._item_id = None
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(details_widget, 1)
|
||||
layout.addWidget(save_btn, 0, QtCore.Qt.AlignRight)
|
||||
|
||||
save_btn.clicked.connect(self._on_save_clicked)
|
||||
details_widget.textChanged.connect(self._on_text_change)
|
||||
|
||||
self._details_widget = details_widget
|
||||
self._save_btn = save_btn
|
||||
|
||||
self.set_editable(False)
|
||||
|
||||
def _on_save_clicked(self):
|
||||
if self.is_valid():
|
||||
self.save_triggered.emit()
|
||||
|
||||
def set_editable(self, enabled=True):
|
||||
self._editable = enabled
|
||||
self.update_state()
|
||||
|
||||
def update_state(self, valid=None):
|
||||
editable = self._editable
|
||||
if not self._item_id:
|
||||
editable = False
|
||||
|
||||
self._save_btn.setVisible(editable)
|
||||
self._details_widget.setReadOnly(not editable)
|
||||
if valid is None:
|
||||
valid = self.is_valid()
|
||||
|
||||
self._save_btn.setEnabled(valid)
|
||||
self._set_invalid_detail(valid)
|
||||
|
||||
def _set_invalid_detail(self, valid):
|
||||
state = ""
|
||||
if not valid:
|
||||
state = "invalid"
|
||||
|
||||
current_state = self._details_widget.property("state")
|
||||
if current_state != state:
|
||||
self._details_widget.setProperty("state", state)
|
||||
self._details_widget.style().polish(self._details_widget)
|
||||
|
||||
def set_details(self, container, item_id):
|
||||
self._item_id = item_id
|
||||
|
||||
text = "Nothing selected"
|
||||
if item_id:
|
||||
try:
|
||||
text = json.dumps(container, indent=4)
|
||||
except Exception:
|
||||
text = str(container)
|
||||
|
||||
self._block_changes = True
|
||||
self._details_widget.setPlainText(text)
|
||||
self._block_changes = False
|
||||
|
||||
self.update_state()
|
||||
|
||||
def instance_data_from_text(self):
|
||||
try:
|
||||
jsoned = json.loads(self._details_widget.toPlainText())
|
||||
except Exception:
|
||||
jsoned = None
|
||||
return jsoned
|
||||
|
||||
def item_id(self):
|
||||
return self._item_id
|
||||
|
||||
def is_valid(self):
|
||||
if not self._item_id:
|
||||
return True
|
||||
|
||||
value = self._details_widget.toPlainText()
|
||||
valid = False
|
||||
try:
|
||||
jsoned = json.loads(value)
|
||||
if jsoned and isinstance(jsoned, dict):
|
||||
valid = True
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
return valid
|
||||
|
||||
def _on_text_change(self):
|
||||
if self._block_changes or not self._item_id:
|
||||
return
|
||||
|
||||
valid = self.is_valid()
|
||||
self.update_state(valid)
|
||||
|
|
@ -1,218 +0,0 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
from qtpy import QtWidgets, QtCore
|
||||
import qtawesome
|
||||
|
||||
from ayon_core import style
|
||||
from ayon_core.pipeline import registered_host
|
||||
from ayon_core.tools.utils import PlaceholderLineEdit
|
||||
from ayon_core.tools.utils.lib import (
|
||||
iter_model_rows,
|
||||
qt_app_context
|
||||
)
|
||||
from ayon_core.tools.utils.models import RecursiveSortFilterProxyModel
|
||||
from .model import (
|
||||
InstanceModel,
|
||||
ITEM_ID_ROLE
|
||||
)
|
||||
from .widgets import InstanceDetail
|
||||
|
||||
|
||||
module = sys.modules[__name__]
|
||||
module.window = None
|
||||
|
||||
|
||||
class SubsetManagerWindow(QtWidgets.QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super(SubsetManagerWindow, self).__init__(parent=parent)
|
||||
self.setWindowTitle("Subset Manager 0.1")
|
||||
self.setObjectName("SubsetManager")
|
||||
if not parent:
|
||||
self.setWindowFlags(
|
||||
self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint
|
||||
)
|
||||
|
||||
self.resize(780, 430)
|
||||
|
||||
# Trigger refresh on first called show
|
||||
self._first_show = True
|
||||
|
||||
left_side_widget = QtWidgets.QWidget(self)
|
||||
|
||||
# Header part
|
||||
header_widget = QtWidgets.QWidget(left_side_widget)
|
||||
|
||||
# Filter input
|
||||
filter_input = PlaceholderLineEdit(header_widget)
|
||||
filter_input.setPlaceholderText("Filter products..")
|
||||
|
||||
# Refresh button
|
||||
icon = qtawesome.icon("fa.refresh", color="white")
|
||||
refresh_btn = QtWidgets.QPushButton(header_widget)
|
||||
refresh_btn.setIcon(icon)
|
||||
|
||||
header_layout = QtWidgets.QHBoxLayout(header_widget)
|
||||
header_layout.setContentsMargins(0, 0, 0, 0)
|
||||
header_layout.addWidget(filter_input)
|
||||
header_layout.addWidget(refresh_btn)
|
||||
|
||||
# Instances view
|
||||
view = QtWidgets.QTreeView(left_side_widget)
|
||||
view.setIndentation(0)
|
||||
view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
|
||||
model = InstanceModel(view)
|
||||
proxy = RecursiveSortFilterProxyModel()
|
||||
proxy.setSourceModel(model)
|
||||
proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
|
||||
view.setModel(proxy)
|
||||
|
||||
left_side_layout = QtWidgets.QVBoxLayout(left_side_widget)
|
||||
left_side_layout.setContentsMargins(0, 0, 0, 0)
|
||||
left_side_layout.addWidget(header_widget)
|
||||
left_side_layout.addWidget(view)
|
||||
|
||||
details_widget = InstanceDetail(self)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.addWidget(left_side_widget, 0)
|
||||
layout.addWidget(details_widget, 1)
|
||||
|
||||
filter_input.textChanged.connect(proxy.setFilterFixedString)
|
||||
refresh_btn.clicked.connect(self._on_refresh_clicked)
|
||||
view.clicked.connect(self._on_activated)
|
||||
view.customContextMenuRequested.connect(self.on_context_menu)
|
||||
details_widget.save_triggered.connect(self._on_save)
|
||||
|
||||
self._model = model
|
||||
self._proxy = proxy
|
||||
self._view = view
|
||||
self._details_widget = details_widget
|
||||
self._refresh_btn = refresh_btn
|
||||
|
||||
def _on_refresh_clicked(self):
|
||||
self.refresh()
|
||||
|
||||
def _on_activated(self, index):
|
||||
container = None
|
||||
item_id = None
|
||||
if index.isValid():
|
||||
item_id = index.data(ITEM_ID_ROLE)
|
||||
container = self._model.get_instance_by_id(item_id)
|
||||
|
||||
self._details_widget.set_details(container, item_id)
|
||||
|
||||
def _on_save(self):
|
||||
host = registered_host()
|
||||
if not hasattr(host, "save_instances"):
|
||||
print("BUG: Host does not have \"save_instances\" method")
|
||||
return
|
||||
|
||||
current_index = self._view.selectionModel().currentIndex()
|
||||
if not current_index.isValid():
|
||||
return
|
||||
|
||||
item_id = current_index.data(ITEM_ID_ROLE)
|
||||
if item_id != self._details_widget.item_id():
|
||||
return
|
||||
|
||||
item_data = self._details_widget.instance_data_from_text()
|
||||
new_instances = []
|
||||
for index in iter_model_rows(self._model, 0):
|
||||
_item_id = index.data(ITEM_ID_ROLE)
|
||||
if _item_id == item_id:
|
||||
instance_data = item_data
|
||||
else:
|
||||
instance_data = self._model.get_instance_by_id(item_id)
|
||||
new_instances.append(instance_data)
|
||||
|
||||
host.save_instances(new_instances)
|
||||
|
||||
def on_context_menu(self, point):
|
||||
point_index = self._view.indexAt(point)
|
||||
item_id = point_index.data(ITEM_ID_ROLE)
|
||||
instance_data = self._model.get_instance_by_id(item_id)
|
||||
if instance_data is None:
|
||||
return
|
||||
|
||||
# Prepare menu
|
||||
menu = QtWidgets.QMenu(self)
|
||||
actions = []
|
||||
host = registered_host()
|
||||
if hasattr(host, "remove_instance"):
|
||||
action = QtWidgets.QAction("Remove instance", menu)
|
||||
action.setData(host.remove_instance)
|
||||
actions.append(action)
|
||||
|
||||
if hasattr(host, "select_instance"):
|
||||
action = QtWidgets.QAction("Select instance", menu)
|
||||
action.setData(host.select_instance)
|
||||
actions.append(action)
|
||||
|
||||
if not actions:
|
||||
actions.append(QtWidgets.QAction("* Nothing to do", menu))
|
||||
|
||||
for action in actions:
|
||||
menu.addAction(action)
|
||||
|
||||
# Show menu under mouse
|
||||
global_point = self._view.mapToGlobal(point)
|
||||
action = menu.exec_(global_point)
|
||||
if not action or not action.data():
|
||||
return
|
||||
|
||||
# Process action
|
||||
# TODO catch exceptions
|
||||
function = action.data()
|
||||
function(instance_data)
|
||||
|
||||
# Reset modified data
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
self._details_widget.set_details(None, None)
|
||||
self._model.refresh()
|
||||
|
||||
host = registered_host()
|
||||
dev_mode = os.environ.get("AVALON_DEVELOP_MODE") or ""
|
||||
editable = False
|
||||
if dev_mode.lower() in ("1", "yes", "true", "on"):
|
||||
editable = hasattr(host, "save_instances")
|
||||
self._details_widget.set_editable(editable)
|
||||
|
||||
def showEvent(self, *args, **kwargs):
|
||||
super(SubsetManagerWindow, self).showEvent(*args, **kwargs)
|
||||
if self._first_show:
|
||||
self._first_show = False
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
self.refresh()
|
||||
|
||||
|
||||
def show(root=None, debug=False, parent=None):
|
||||
"""Display Scene Inventory GUI
|
||||
|
||||
Arguments:
|
||||
debug (bool, optional): Run in debug-mode,
|
||||
defaults to False
|
||||
parent (QtCore.QObject, optional): When provided parent the interface
|
||||
to this QObject.
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
module.window.close()
|
||||
del module.window
|
||||
except (RuntimeError, AttributeError):
|
||||
pass
|
||||
|
||||
with qt_app_context():
|
||||
window = SubsetManagerWindow(parent)
|
||||
window.show()
|
||||
|
||||
module.window = window
|
||||
|
||||
# Pull window to the front.
|
||||
module.window.raise_()
|
||||
module.window.activateWindow()
|
||||
|
|
@ -240,6 +240,16 @@ class TrayManager:
|
|||
self.log.warning("Other tray started meanwhile. Exiting.")
|
||||
self.exit()
|
||||
|
||||
project_bundle = os.getenv("AYON_BUNDLE_NAME")
|
||||
studio_bundle = os.getenv("AYON_STUDIO_BUNDLE_NAME")
|
||||
if studio_bundle and project_bundle != studio_bundle:
|
||||
self.log.info(
|
||||
f"Project bundle '{project_bundle}' is defined, but tray"
|
||||
" cannot be running in project scope. Restarting tray to use"
|
||||
" studio bundle."
|
||||
)
|
||||
self.restart()
|
||||
|
||||
def get_services_submenu(self):
|
||||
return self._services_submenu
|
||||
|
||||
|
|
@ -270,11 +280,18 @@ class TrayManager:
|
|||
elif is_staging_enabled():
|
||||
additional_args.append("--use-staging")
|
||||
|
||||
if "--project" in additional_args:
|
||||
idx = additional_args.index("--project")
|
||||
additional_args.pop(idx)
|
||||
additional_args.pop(idx)
|
||||
|
||||
args.extend(additional_args)
|
||||
|
||||
envs = dict(os.environ.items())
|
||||
for key in {
|
||||
"AYON_BUNDLE_NAME",
|
||||
"AYON_STUDIO_BUNDLE_NAME",
|
||||
"AYON_PROJECT_NAME",
|
||||
}:
|
||||
envs.pop(key, None)
|
||||
|
||||
|
|
@ -329,6 +346,7 @@ class TrayManager:
|
|||
return json_response({
|
||||
"username": self._cached_username,
|
||||
"bundle": os.getenv("AYON_BUNDLE_NAME"),
|
||||
"studio_bundle": os.getenv("AYON_STUDIO_BUNDLE_NAME"),
|
||||
"dev_mode": is_dev_mode_enabled(),
|
||||
"staging_mode": is_staging_enabled(),
|
||||
"addons": {
|
||||
|
|
@ -516,6 +534,8 @@ class TrayManager:
|
|||
"AYON_SERVER_URL",
|
||||
"AYON_API_KEY",
|
||||
"AYON_BUNDLE_NAME",
|
||||
"AYON_STUDIO_BUNDLE_NAME",
|
||||
"AYON_PROJECT_NAME",
|
||||
}:
|
||||
os.environ.pop(key, None)
|
||||
self.restart()
|
||||
|
|
@ -549,6 +569,8 @@ class TrayManager:
|
|||
envs = dict(os.environ.items())
|
||||
for key in {
|
||||
"AYON_BUNDLE_NAME",
|
||||
"AYON_STUDIO_BUNDLE_NAME",
|
||||
"AYON_PROJECT_NAME",
|
||||
}:
|
||||
envs.pop(key, None)
|
||||
|
||||
|
|
|
|||
|
|
@ -186,8 +186,15 @@ class StatusDelegate(QtWidgets.QStyledItemDelegate):
|
|||
)
|
||||
fm = QtGui.QFontMetrics(option.font)
|
||||
if text_rect.width() < fm.width(text):
|
||||
text = self._get_status_short_name(index)
|
||||
if text_rect.width() < fm.width(text):
|
||||
short_text = self._get_status_short_name(index)
|
||||
if short_text:
|
||||
text = short_text
|
||||
|
||||
text = fm.elidedText(
|
||||
text, QtCore.Qt.ElideRight, text_rect.width()
|
||||
)
|
||||
# Allow at least one character
|
||||
if len(text) < 2:
|
||||
text = ""
|
||||
|
||||
fg_color = self._get_status_color(index)
|
||||
|
|
|
|||
|
|
@ -31,9 +31,7 @@ class HostToolsHelper:
|
|||
# Prepare attributes for all tools
|
||||
self._workfiles_tool = None
|
||||
self._loader_tool = None
|
||||
self._creator_tool = None
|
||||
self._publisher_tool = None
|
||||
self._subset_manager_tool = None
|
||||
self._scene_inventory_tool = None
|
||||
self._experimental_tools_dialog = None
|
||||
|
||||
|
|
@ -96,49 +94,6 @@ class HostToolsHelper:
|
|||
|
||||
loader_tool.refresh()
|
||||
|
||||
def get_creator_tool(self, parent):
|
||||
"""Create, cache and return creator tool window."""
|
||||
if self._creator_tool is None:
|
||||
from ayon_core.tools.creator import CreatorWindow
|
||||
|
||||
creator_window = CreatorWindow(parent=parent or self._parent)
|
||||
self._creator_tool = creator_window
|
||||
|
||||
return self._creator_tool
|
||||
|
||||
def show_creator(self, parent=None):
|
||||
"""Show tool to create new instantes for publishing."""
|
||||
with qt_app_context():
|
||||
creator_tool = self.get_creator_tool(parent)
|
||||
creator_tool.refresh()
|
||||
creator_tool.show()
|
||||
|
||||
# Pull window to the front.
|
||||
creator_tool.raise_()
|
||||
creator_tool.activateWindow()
|
||||
|
||||
def get_subset_manager_tool(self, parent):
|
||||
"""Create, cache and return subset manager tool window."""
|
||||
if self._subset_manager_tool is None:
|
||||
from ayon_core.tools.subsetmanager import SubsetManagerWindow
|
||||
|
||||
subset_manager_window = SubsetManagerWindow(
|
||||
parent=parent or self._parent
|
||||
)
|
||||
self._subset_manager_tool = subset_manager_window
|
||||
|
||||
return self._subset_manager_tool
|
||||
|
||||
def show_subset_manager(self, parent=None):
|
||||
"""Show tool display/remove existing created instances."""
|
||||
with qt_app_context():
|
||||
subset_manager_tool = self.get_subset_manager_tool(parent)
|
||||
subset_manager_tool.show()
|
||||
|
||||
# Pull window to the front.
|
||||
subset_manager_tool.raise_()
|
||||
subset_manager_tool.activateWindow()
|
||||
|
||||
def get_scene_inventory_tool(self, parent):
|
||||
"""Create, cache and return scene inventory tool window."""
|
||||
if self._scene_inventory_tool is None:
|
||||
|
|
@ -261,35 +216,29 @@ class HostToolsHelper:
|
|||
if tool_name == "workfiles":
|
||||
return self.get_workfiles_tool(parent, *args, **kwargs)
|
||||
|
||||
elif tool_name == "loader":
|
||||
if tool_name == "loader":
|
||||
return self.get_loader_tool(parent, *args, **kwargs)
|
||||
|
||||
elif tool_name == "libraryloader":
|
||||
if tool_name == "libraryloader":
|
||||
return self.get_library_loader_tool(parent, *args, **kwargs)
|
||||
|
||||
elif tool_name == "creator":
|
||||
return self.get_creator_tool(parent, *args, **kwargs)
|
||||
|
||||
elif tool_name == "subsetmanager":
|
||||
return self.get_subset_manager_tool(parent, *args, **kwargs)
|
||||
|
||||
elif tool_name == "sceneinventory":
|
||||
if tool_name == "sceneinventory":
|
||||
return self.get_scene_inventory_tool(parent, *args, **kwargs)
|
||||
|
||||
elif tool_name == "publish":
|
||||
self.log.info("Can't return publish tool window.")
|
||||
|
||||
# "new" publisher
|
||||
elif tool_name == "publisher":
|
||||
if tool_name == "publisher":
|
||||
return self.get_publisher_tool(parent, *args, **kwargs)
|
||||
|
||||
elif tool_name == "experimental_tools":
|
||||
if tool_name == "experimental_tools":
|
||||
return self.get_experimental_tools_dialog(parent, *args, **kwargs)
|
||||
|
||||
else:
|
||||
self.log.warning(
|
||||
"Can't show unknown tool name: \"{}\"".format(tool_name)
|
||||
)
|
||||
if tool_name == "publish":
|
||||
self.log.info("Can't return publish tool window.")
|
||||
return None
|
||||
|
||||
self.log.warning(
|
||||
"Can't show unknown tool name: \"{}\"".format(tool_name)
|
||||
)
|
||||
return None
|
||||
|
||||
def show_tool_by_name(self, tool_name, parent=None, *args, **kwargs):
|
||||
"""Show tool by it's name.
|
||||
|
|
@ -305,12 +254,6 @@ class HostToolsHelper:
|
|||
elif tool_name == "libraryloader":
|
||||
self.show_library_loader(parent, *args, **kwargs)
|
||||
|
||||
elif tool_name == "creator":
|
||||
self.show_creator(parent, *args, **kwargs)
|
||||
|
||||
elif tool_name == "subsetmanager":
|
||||
self.show_subset_manager(parent, *args, **kwargs)
|
||||
|
||||
elif tool_name == "sceneinventory":
|
||||
self.show_scene_inventory(parent, *args, **kwargs)
|
||||
|
||||
|
|
@ -379,14 +322,6 @@ def show_library_loader(parent=None):
|
|||
_SingletonPoint.show_tool_by_name("libraryloader", parent)
|
||||
|
||||
|
||||
def show_creator(parent=None):
|
||||
_SingletonPoint.show_tool_by_name("creator", parent)
|
||||
|
||||
|
||||
def show_subset_manager(parent=None):
|
||||
_SingletonPoint.show_tool_by_name("subsetmanager", parent)
|
||||
|
||||
|
||||
def show_scene_inventory(parent=None):
|
||||
_SingletonPoint.show_tool_by_name("sceneinventory", parent)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from math import floor, sqrt, ceil
|
||||
from math import floor, ceil
|
||||
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
from ayon_core.style import get_objected_colors
|
||||
|
|
@ -9,12 +10,15 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
clicked = QtCore.Signal()
|
||||
|
||||
_checked_bg_color = None
|
||||
_checked_bg_color_disabled = None
|
||||
_unchecked_bg_color = None
|
||||
_unchecked_bg_color_disabled = None
|
||||
_checker_color = None
|
||||
_checker_color_disabled = None
|
||||
_checker_hover_color = None
|
||||
|
||||
def __init__(self, checked=False, draw_icons=False, parent=None):
|
||||
super(NiceCheckbox, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self.setObjectName("NiceCheckbox")
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
|
@ -48,8 +52,6 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
self._pressed = False
|
||||
self._under_mouse = False
|
||||
|
||||
self.icon_scale_factor = sqrt(2) / 2
|
||||
|
||||
icon_path_stroker = QtGui.QPainterPathStroker()
|
||||
icon_path_stroker.setCapStyle(QtCore.Qt.RoundCap)
|
||||
icon_path_stroker.setJoinStyle(QtCore.Qt.RoundJoin)
|
||||
|
|
@ -61,35 +63,6 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
self._base_size = QtCore.QSize(90, 50)
|
||||
self._load_colors()
|
||||
|
||||
@classmethod
|
||||
def _load_colors(cls):
|
||||
if cls._checked_bg_color is not None:
|
||||
return
|
||||
|
||||
colors_info = get_objected_colors("nice-checkbox")
|
||||
|
||||
cls._checked_bg_color = colors_info["bg-checked"].get_qcolor()
|
||||
cls._unchecked_bg_color = colors_info["bg-unchecked"].get_qcolor()
|
||||
|
||||
cls._checker_color = colors_info["bg-checker"].get_qcolor()
|
||||
cls._checker_hover_color = colors_info["bg-checker-hover"].get_qcolor()
|
||||
|
||||
@property
|
||||
def checked_bg_color(self):
|
||||
return self._checked_bg_color
|
||||
|
||||
@property
|
||||
def unchecked_bg_color(self):
|
||||
return self._unchecked_bg_color
|
||||
|
||||
@property
|
||||
def checker_color(self):
|
||||
return self._checker_color
|
||||
|
||||
@property
|
||||
def checker_hover_color(self):
|
||||
return self._checker_hover_color
|
||||
|
||||
def setTristate(self, tristate=True):
|
||||
if self._is_tristate != tristate:
|
||||
self._is_tristate = tristate
|
||||
|
|
@ -121,14 +94,14 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
|
||||
def setFixedHeight(self, *args, **kwargs):
|
||||
self._fixed_height_set = True
|
||||
super(NiceCheckbox, self).setFixedHeight(*args, **kwargs)
|
||||
super().setFixedHeight(*args, **kwargs)
|
||||
if not self._fixed_width_set:
|
||||
width = self.get_width_hint_by_height(self.height())
|
||||
self.setFixedWidth(width)
|
||||
|
||||
def setFixedWidth(self, *args, **kwargs):
|
||||
self._fixed_width_set = True
|
||||
super(NiceCheckbox, self).setFixedWidth(*args, **kwargs)
|
||||
super().setFixedWidth(*args, **kwargs)
|
||||
if not self._fixed_height_set:
|
||||
height = self.get_height_hint_by_width(self.width())
|
||||
self.setFixedHeight(height)
|
||||
|
|
@ -136,7 +109,7 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
def setFixedSize(self, *args, **kwargs):
|
||||
self._fixed_height_set = True
|
||||
self._fixed_width_set = True
|
||||
super(NiceCheckbox, self).setFixedSize(*args, **kwargs)
|
||||
super().setFixedSize(*args, **kwargs)
|
||||
|
||||
def steps(self):
|
||||
return self._steps
|
||||
|
|
@ -242,7 +215,7 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
if event.buttons() & QtCore.Qt.LeftButton:
|
||||
self._pressed = True
|
||||
self.repaint()
|
||||
super(NiceCheckbox, self).mousePressEvent(event)
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self._pressed and not event.buttons() & QtCore.Qt.LeftButton:
|
||||
|
|
@ -252,7 +225,7 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
self.clicked.emit()
|
||||
event.accept()
|
||||
return
|
||||
super(NiceCheckbox, self).mouseReleaseEvent(event)
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if self._pressed:
|
||||
|
|
@ -261,19 +234,19 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
self._under_mouse = under_mouse
|
||||
self.repaint()
|
||||
|
||||
super(NiceCheckbox, self).mouseMoveEvent(event)
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
def enterEvent(self, event):
|
||||
self._under_mouse = True
|
||||
if self.isEnabled():
|
||||
self.repaint()
|
||||
super(NiceCheckbox, self).enterEvent(event)
|
||||
super().enterEvent(event)
|
||||
|
||||
def leaveEvent(self, event):
|
||||
self._under_mouse = False
|
||||
if self.isEnabled():
|
||||
self.repaint()
|
||||
super(NiceCheckbox, self).leaveEvent(event)
|
||||
super().leaveEvent(event)
|
||||
|
||||
def _on_animation_timeout(self):
|
||||
if self._checkstate == QtCore.Qt.Checked:
|
||||
|
|
@ -302,24 +275,13 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
|
||||
@staticmethod
|
||||
def steped_color(color1, color2, offset_ratio):
|
||||
red_dif = (
|
||||
color1.red() - color2.red()
|
||||
)
|
||||
green_dif = (
|
||||
color1.green() - color2.green()
|
||||
)
|
||||
blue_dif = (
|
||||
color1.blue() - color2.blue()
|
||||
)
|
||||
red = int(color2.red() + (
|
||||
red_dif * offset_ratio
|
||||
))
|
||||
green = int(color2.green() + (
|
||||
green_dif * offset_ratio
|
||||
))
|
||||
blue = int(color2.blue() + (
|
||||
blue_dif * offset_ratio
|
||||
))
|
||||
red_dif = color1.red() - color2.red()
|
||||
green_dif = color1.green() - color2.green()
|
||||
blue_dif = color1.blue() - color2.blue()
|
||||
|
||||
red = int(color2.red() + (red_dif * offset_ratio))
|
||||
green = int(color2.green() + (green_dif * offset_ratio))
|
||||
blue = int(color2.blue() + (blue_dif * offset_ratio))
|
||||
|
||||
return QtGui.QColor(red, green, blue)
|
||||
|
||||
|
|
@ -334,20 +296,28 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
painter = QtGui.QPainter(self)
|
||||
|
||||
painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
painter.setPen(QtCore.Qt.NoPen)
|
||||
|
||||
# Draw inner background
|
||||
if self._current_step == self._steps:
|
||||
bg_color = self.checked_bg_color
|
||||
if not self.isEnabled():
|
||||
bg_color = (
|
||||
self._checked_bg_color_disabled
|
||||
if self._current_step == self._steps
|
||||
else self._unchecked_bg_color_disabled
|
||||
)
|
||||
|
||||
elif self._current_step == self._steps:
|
||||
bg_color = self._checked_bg_color
|
||||
|
||||
elif self._current_step == 0:
|
||||
bg_color = self.unchecked_bg_color
|
||||
bg_color = self._unchecked_bg_color
|
||||
|
||||
else:
|
||||
offset_ratio = float(self._current_step) / self._steps
|
||||
# Animation bg
|
||||
bg_color = self.steped_color(
|
||||
self.checked_bg_color,
|
||||
self.unchecked_bg_color,
|
||||
self._checked_bg_color,
|
||||
self._unchecked_bg_color,
|
||||
offset_ratio
|
||||
)
|
||||
|
||||
|
|
@ -378,14 +348,20 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
-margin_size_c, -margin_size_c
|
||||
)
|
||||
|
||||
if checkbox_rect.width() > checkbox_rect.height():
|
||||
radius = floor(checkbox_rect.height() * 0.5)
|
||||
else:
|
||||
radius = floor(checkbox_rect.width() * 0.5)
|
||||
slider_rect = QtCore.QRect(checkbox_rect)
|
||||
slider_offset = int(
|
||||
ceil(min(slider_rect.width(), slider_rect.height())) * 0.08
|
||||
)
|
||||
if slider_offset < 1:
|
||||
slider_offset = 1
|
||||
slider_rect.adjust(
|
||||
slider_offset, slider_offset,
|
||||
-slider_offset, -slider_offset
|
||||
)
|
||||
radius = floor(min(slider_rect.width(), slider_rect.height()) * 0.5)
|
||||
|
||||
painter.setPen(QtCore.Qt.NoPen)
|
||||
painter.setBrush(bg_color)
|
||||
painter.drawRoundedRect(checkbox_rect, radius, radius)
|
||||
painter.drawRoundedRect(slider_rect, radius, radius)
|
||||
|
||||
# Draw checker
|
||||
checker_size = size_without_margins - (margin_size_c * 2)
|
||||
|
|
@ -394,9 +370,8 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
- (margin_size_c * 2)
|
||||
- checker_size
|
||||
)
|
||||
if self._current_step == 0:
|
||||
x_offset = 0
|
||||
else:
|
||||
x_offset = 0
|
||||
if self._current_step != 0:
|
||||
x_offset = (float(area_width) / self._steps) * self._current_step
|
||||
|
||||
pos_x = checkbox_rect.x() + x_offset + margin_size_c
|
||||
|
|
@ -404,55 +379,80 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
|
||||
checker_rect = QtCore.QRect(pos_x, pos_y, checker_size, checker_size)
|
||||
|
||||
under_mouse = self.isEnabled() and self._under_mouse
|
||||
if under_mouse:
|
||||
checker_color = self.checker_hover_color
|
||||
else:
|
||||
checker_color = self.checker_color
|
||||
checker_color = self._checker_color
|
||||
if not self.isEnabled():
|
||||
checker_color = self._checker_color_disabled
|
||||
elif self._under_mouse:
|
||||
checker_color = self._checker_hover_color
|
||||
|
||||
painter.setBrush(checker_color)
|
||||
painter.drawEllipse(checker_rect)
|
||||
|
||||
if self._draw_icons:
|
||||
painter.setBrush(bg_color)
|
||||
icon_path = self._get_icon_path(painter, checker_rect)
|
||||
icon_path = self._get_icon_path(checker_rect)
|
||||
painter.drawPath(icon_path)
|
||||
|
||||
# Draw shadow overlay
|
||||
if not self.isEnabled():
|
||||
level = 33
|
||||
alpha = 127
|
||||
painter.setPen(QtCore.Qt.transparent)
|
||||
painter.setBrush(QtGui.QColor(level, level, level, alpha))
|
||||
painter.drawRoundedRect(checkbox_rect, radius, radius)
|
||||
|
||||
painter.end()
|
||||
|
||||
def _get_icon_path(self, painter, checker_rect):
|
||||
@classmethod
|
||||
def _load_colors(cls):
|
||||
if cls._checked_bg_color is not None:
|
||||
return
|
||||
|
||||
colors_info = get_objected_colors("nice-checkbox")
|
||||
|
||||
disabled_color = QtGui.QColor(33, 33, 33, 127)
|
||||
|
||||
cls._checked_bg_color = colors_info["bg-checked"].get_qcolor()
|
||||
cls._checked_bg_color_disabled = cls._merge_colors(
|
||||
cls._checked_bg_color, disabled_color
|
||||
)
|
||||
cls._unchecked_bg_color = colors_info["bg-unchecked"].get_qcolor()
|
||||
cls._unchecked_bg_color_disabled = cls._merge_colors(
|
||||
cls._unchecked_bg_color, disabled_color
|
||||
)
|
||||
|
||||
cls._checker_color = colors_info["bg-checker"].get_qcolor()
|
||||
cls._checker_color_disabled = cls._merge_colors(
|
||||
cls._checker_color, disabled_color
|
||||
)
|
||||
cls._checker_hover_color = colors_info["bg-checker-hover"].get_qcolor()
|
||||
|
||||
@staticmethod
|
||||
def _merge_colors(color_1, color_2):
|
||||
a = color_2.alphaF()
|
||||
return QtGui.QColor(
|
||||
floor((color_1.red() + (color_2.red() * a)) * 0.5),
|
||||
floor((color_1.green() + (color_2.green() * a)) * 0.5),
|
||||
floor((color_1.blue() + (color_2.blue() * a)) * 0.5),
|
||||
color_1.alpha()
|
||||
)
|
||||
|
||||
def _get_icon_path(self, checker_rect):
|
||||
self.icon_path_stroker.setWidth(checker_rect.height() / 5)
|
||||
|
||||
if self._current_step == self._steps:
|
||||
return self._get_enabled_icon_path(painter, checker_rect)
|
||||
return self._get_enabled_icon_path(checker_rect)
|
||||
|
||||
if self._current_step == 0:
|
||||
return self._get_disabled_icon_path(painter, checker_rect)
|
||||
return self._get_disabled_icon_path(checker_rect)
|
||||
|
||||
if self._current_step == self._middle_step:
|
||||
return self._get_middle_circle_path(painter, checker_rect)
|
||||
return self._get_middle_circle_path(checker_rect)
|
||||
|
||||
disabled_step = self._steps - self._current_step
|
||||
enabled_step = self._steps - disabled_step
|
||||
half_steps = self._steps + 1 - ((self._steps + 1) % 2)
|
||||
if enabled_step > disabled_step:
|
||||
return self._get_enabled_icon_path(
|
||||
painter, checker_rect, enabled_step, half_steps
|
||||
)
|
||||
else:
|
||||
return self._get_disabled_icon_path(
|
||||
painter, checker_rect, disabled_step, half_steps
|
||||
checker_rect, enabled_step, half_steps
|
||||
)
|
||||
return self._get_disabled_icon_path(
|
||||
checker_rect, disabled_step, half_steps
|
||||
)
|
||||
|
||||
def _get_middle_circle_path(self, painter, checker_rect):
|
||||
def _get_middle_circle_path(self, checker_rect):
|
||||
width = self.icon_path_stroker.width()
|
||||
path = QtGui.QPainterPath()
|
||||
path.addEllipse(checker_rect.center(), width, width)
|
||||
|
|
@ -460,7 +460,7 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
return path
|
||||
|
||||
def _get_enabled_icon_path(
|
||||
self, painter, checker_rect, step=None, half_steps=None
|
||||
self, checker_rect, step=None, half_steps=None
|
||||
):
|
||||
fifteenth = float(checker_rect.height()) / 15
|
||||
# Left point
|
||||
|
|
@ -509,7 +509,7 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
return self.icon_path_stroker.createStroke(path)
|
||||
|
||||
def _get_disabled_icon_path(
|
||||
self, painter, checker_rect, step=None, half_steps=None
|
||||
self, checker_rect, step=None, half_steps=None
|
||||
):
|
||||
center_point = QtCore.QPointF(
|
||||
float(checker_rect.width()) / 2,
|
||||
|
|
|
|||
|
|
@ -234,10 +234,11 @@ class TasksQtModel(QtGui.QStandardItemModel):
|
|||
)
|
||||
icon = None
|
||||
if task_type_item is not None:
|
||||
color = task_type_item.color or get_default_entity_icon_color()
|
||||
icon = get_qt_icon({
|
||||
"type": "material-symbols",
|
||||
"name": task_type_item.icon,
|
||||
"color": get_default_entity_icon_color()
|
||||
"color": color,
|
||||
})
|
||||
|
||||
if icon is None:
|
||||
|
|
|
|||
|
|
@ -418,7 +418,7 @@ class ExpandingTextEdit(QtWidgets.QTextEdit):
|
|||
"""QTextEdit which does not have sroll area but expands height."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ExpandingTextEdit, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
size_policy = self.sizePolicy()
|
||||
size_policy.setHeightForWidth(True)
|
||||
|
|
@ -441,14 +441,18 @@ class ExpandingTextEdit(QtWidgets.QTextEdit):
|
|||
margins = self.contentsMargins()
|
||||
|
||||
document_width = 0
|
||||
if width >= margins.left() + margins.right():
|
||||
document_width = width - margins.left() - margins.right()
|
||||
margins_size = margins.left() + margins.right()
|
||||
if width >= margins_size:
|
||||
document_width = width - margins_size
|
||||
|
||||
document = self.document().clone()
|
||||
document.setTextWidth(document_width)
|
||||
|
||||
return math.ceil(
|
||||
margins.top() + document.size().height() + margins.bottom()
|
||||
margins.top()
|
||||
+ document.size().height()
|
||||
+ margins.bottom()
|
||||
+ 2
|
||||
)
|
||||
|
||||
def sizeHint(self):
|
||||
|
|
|
|||
|
|
@ -3,25 +3,26 @@ import os
|
|||
import ayon_api
|
||||
|
||||
from ayon_core.host import IWorkfileHost
|
||||
from ayon_core.lib import Logger
|
||||
from ayon_core.lib import Logger, get_ayon_username
|
||||
from ayon_core.lib.events import QueuedEventSystem
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.pipeline import Anatomy, registered_host
|
||||
from ayon_core.pipeline.context_tools import get_global_context
|
||||
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.tools.common_models import (
|
||||
HierarchyModel,
|
||||
HierarchyExpectedSelection,
|
||||
HierarchyModel,
|
||||
ProjectsModel,
|
||||
UsersModel,
|
||||
)
|
||||
|
||||
from .abstract import (
|
||||
AbstractWorkfilesFrontend,
|
||||
AbstractWorkfilesBackend,
|
||||
AbstractWorkfilesFrontend,
|
||||
)
|
||||
from .models import SelectionModel, WorkfilesModel
|
||||
|
||||
NOT_SET = object()
|
||||
|
||||
|
||||
class WorkfilesToolExpectedSelection(HierarchyExpectedSelection):
|
||||
def __init__(self, controller):
|
||||
|
|
@ -143,6 +144,7 @@ class BaseWorkfileController(
|
|||
self._project_settings = None
|
||||
self._event_system = None
|
||||
self._log = None
|
||||
self._username = NOT_SET
|
||||
|
||||
self._current_project_name = None
|
||||
self._current_folder_path = None
|
||||
|
|
@ -588,6 +590,20 @@ class BaseWorkfileController(
|
|||
description,
|
||||
)
|
||||
|
||||
def get_my_tasks_entity_ids(self, project_name: str):
|
||||
username = self._get_my_username()
|
||||
assignees = []
|
||||
if username:
|
||||
assignees.append(username)
|
||||
return self._hierarchy_model.get_entity_ids_for_assignees(
|
||||
project_name, assignees
|
||||
)
|
||||
|
||||
def _get_my_username(self):
|
||||
if self._username is NOT_SET:
|
||||
self._username = get_ayon_username()
|
||||
return self._username
|
||||
|
||||
def _emit_event(self, topic, data=None):
|
||||
self.emit_event(topic, data, "controller")
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ from ayon_core.lib import (
|
|||
Logger,
|
||||
)
|
||||
from ayon_core.host import (
|
||||
HostBase,
|
||||
IWorkfileHost,
|
||||
WorkfileInfo,
|
||||
PublishedWorkfileInfo,
|
||||
|
|
@ -49,19 +48,15 @@ if typing.TYPE_CHECKING:
|
|||
_NOT_SET = object()
|
||||
|
||||
|
||||
class HostType(HostBase, IWorkfileHost):
|
||||
pass
|
||||
|
||||
|
||||
class WorkfilesModel:
|
||||
"""Workfiles model."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: HostType,
|
||||
host: IWorkfileHost,
|
||||
controller: AbstractWorkfilesBackend
|
||||
):
|
||||
self._host: HostType = host
|
||||
self._host: IWorkfileHost = host
|
||||
self._controller: AbstractWorkfilesBackend = controller
|
||||
|
||||
self._log = Logger.get_logger("WorkfilesModel")
|
||||
|
|
|
|||
|
|
@ -287,10 +287,11 @@ class FilesWidget(QtWidgets.QWidget):
|
|||
def _update_published_btns_state(self):
|
||||
enabled = (
|
||||
self._valid_representation_id
|
||||
and self._valid_selected_context
|
||||
and self._is_save_enabled
|
||||
)
|
||||
self._published_btn_copy_n_open.setEnabled(enabled)
|
||||
self._published_btn_copy_n_open.setEnabled(
|
||||
enabled and self._valid_selected_context
|
||||
)
|
||||
self._published_btn_change_context.setEnabled(enabled)
|
||||
|
||||
def _update_workarea_btns_state(self):
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
from qtpy import QtCore, QtWidgets, QtGui
|
||||
|
||||
from ayon_core import style, resources
|
||||
from ayon_core.tools.utils import (
|
||||
PlaceholderLineEdit,
|
||||
MessageOverlayObject,
|
||||
)
|
||||
from qtpy import QtCore, QtGui, QtWidgets
|
||||
|
||||
from ayon_core.tools.workfiles.control import BaseWorkfileController
|
||||
from ayon_core import resources, style
|
||||
from ayon_core.tools.utils import (
|
||||
GoToCurrentButton,
|
||||
RefreshButton,
|
||||
FoldersWidget,
|
||||
GoToCurrentButton,
|
||||
MessageOverlayObject,
|
||||
NiceCheckbox,
|
||||
PlaceholderLineEdit,
|
||||
RefreshButton,
|
||||
TasksWidget,
|
||||
)
|
||||
from ayon_core.tools.utils.lib import checkstate_int_to_enum
|
||||
from ayon_core.tools.workfiles.control import BaseWorkfileController
|
||||
|
||||
from .side_panel import SidePanelWidget
|
||||
from .files_widget import FilesWidget
|
||||
from .side_panel import SidePanelWidget
|
||||
from .utils import BaseOverlayFrame
|
||||
|
||||
|
||||
|
|
@ -107,7 +107,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
|
|||
split_widget.addWidget(tasks_widget)
|
||||
split_widget.addWidget(col_3_widget)
|
||||
split_widget.addWidget(side_panel)
|
||||
split_widget.setSizes([255, 175, 550, 190])
|
||||
split_widget.setSizes([350, 175, 550, 190])
|
||||
|
||||
body_layout.addWidget(split_widget)
|
||||
|
||||
|
|
@ -157,6 +157,8 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
|
|||
self._home_body_widget = home_body_widget
|
||||
self._split_widget = split_widget
|
||||
|
||||
self._project_name = self._controller.get_current_project_name()
|
||||
|
||||
self._tasks_widget = tasks_widget
|
||||
self._side_panel = side_panel
|
||||
|
||||
|
|
@ -186,11 +188,24 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
|
|||
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.setContentsMargins(0, 0, 0, 0)
|
||||
|
|
@ -200,6 +215,9 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
|
|||
folder_filter_input.textChanged.connect(self._on_folder_filter_change)
|
||||
go_to_current_btn.clicked.connect(self._on_go_to_current_clicked)
|
||||
refresh_btn.clicked.connect(self._on_refresh_clicked)
|
||||
my_tasks_checkbox.stateChanged.connect(
|
||||
self._on_my_tasks_checkbox_state_changed
|
||||
)
|
||||
|
||||
self._folder_filter_input = folder_filter_input
|
||||
self._folders_widget = folder_widget
|
||||
|
|
@ -385,3 +403,16 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
|
|||
)
|
||||
else:
|
||||
self.close()
|
||||
|
||||
def _on_my_tasks_checkbox_state_changed(self, state):
|
||||
folder_ids = None
|
||||
task_ids = None
|
||||
state = checkstate_int_to_enum(state)
|
||||
if state == QtCore.Qt.Checked:
|
||||
entity_ids = self._controller.get_my_tasks_entity_ids(
|
||||
self._project_name
|
||||
)
|
||||
folder_ids = entity_ids["folder_ids"]
|
||||
task_ids = entity_ids["task_ids"]
|
||||
self._folders_widget.set_folder_ids_filter(folder_ids)
|
||||
self._tasks_widget.set_task_ids_filter(task_ids)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring AYON addon 'core' version."""
|
||||
__version__ = "1.5.0+dev"
|
||||
__version__ = "1.6.0+dev"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ qtawesome = "0.7.3"
|
|||
[ayon.runtimeDependencies]
|
||||
aiohttp-middlewares = "^2.0.0"
|
||||
Click = "^8"
|
||||
OpenTimelineIO = "0.16.0"
|
||||
OpenTimelineIO = "0.17.0"
|
||||
otio-burnins-adapter = "1.0.0"
|
||||
opencolorio = "^2.3.2,<2.4.0"
|
||||
Pillow = "9.5.0"
|
||||
websocket-client = ">=0.40.0,<2"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
name = "core"
|
||||
title = "Core"
|
||||
version = "1.5.0+dev"
|
||||
version = "1.6.0+dev"
|
||||
|
||||
client_dir = "ayon_core"
|
||||
|
||||
plugin_for = ["ayon_server"]
|
||||
|
||||
project_can_override_addon_version = True
|
||||
|
||||
ayon_server_version = ">=1.8.4,<2.0.0"
|
||||
ayon_launcher_version = ">=1.0.2"
|
||||
ayon_required_addons = {}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
[tool.poetry]
|
||||
name = "ayon-core"
|
||||
version = "1.5.0+dev"
|
||||
version = "1.6.0+dev"
|
||||
description = ""
|
||||
authors = ["Ynput Team <team@ynput.io>"]
|
||||
readme = "README.md"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue