mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge branch 'develop' into enhancement/copy_multiple_library_project
This commit is contained in:
commit
5db9efb95c
36 changed files with 1697 additions and 2568 deletions
|
|
@ -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
|
||||
|
|
|
|||
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]
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -30,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,
|
||||
|
|
@ -54,7 +54,6 @@ from ayon_core.pipeline.plugin_discover import (
|
|||
)
|
||||
|
||||
from ayon_core.pipeline.create import (
|
||||
discover_legacy_creator_plugins,
|
||||
CreateContext,
|
||||
HiddenCreator,
|
||||
)
|
||||
|
|
@ -127,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")
|
||||
|
|
@ -163,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,
|
||||
|
|
@ -256,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
|
||||
|
|
@ -321,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
|
||||
|
|
@ -345,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
|
||||
|
||||
|
|
@ -1938,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")
|
||||
|
|
@ -1979,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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,7 +1,7 @@
|
|||
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,
|
||||
|
|
@ -35,7 +35,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):
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue