Merge branch 'develop' of https://github.com/ynput/ayon-core into enhancement/create_context_typing
# Conflicts: # server_addon/jobqueue/client/ayon_jobqueue/addon.py
|
|
@ -87,7 +87,9 @@ class IntegrateHeroVersion(
|
|||
]
|
||||
# QUESTION/TODO this process should happen on server if crashed due to
|
||||
# permissions error on files (files were used or user didn't have perms)
|
||||
# *but all other plugins must be sucessfully completed
|
||||
# *but all other plugins must be successfully completed
|
||||
|
||||
use_hardlinks = False
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
|
|
@ -617,24 +619,32 @@ class IntegrateHeroVersion(
|
|||
|
||||
self.log.debug("Folder already exists: \"{}\"".format(dirname))
|
||||
|
||||
if self.use_hardlinks:
|
||||
# First try hardlink and copy if paths are cross drive
|
||||
self.log.debug("Hardlinking file \"{}\" to \"{}\"".format(
|
||||
src_path, dst_path
|
||||
))
|
||||
try:
|
||||
create_hard_link(src_path, dst_path)
|
||||
# Return when successful
|
||||
return
|
||||
|
||||
except OSError as exc:
|
||||
# re-raise exception if different than
|
||||
# EXDEV - cross drive path
|
||||
# EINVAL - wrong format, must be NTFS
|
||||
self.log.debug(
|
||||
"Hardlink failed with errno:'{}'".format(exc.errno))
|
||||
if exc.errno not in [errno.EXDEV, errno.EINVAL]:
|
||||
raise
|
||||
|
||||
self.log.debug(
|
||||
"Hardlinking failed, falling back to regular copy...")
|
||||
|
||||
self.log.debug("Copying file \"{}\" to \"{}\"".format(
|
||||
src_path, dst_path
|
||||
))
|
||||
|
||||
# First try hardlink and copy if paths are cross drive
|
||||
try:
|
||||
create_hard_link(src_path, dst_path)
|
||||
# Return when successful
|
||||
return
|
||||
|
||||
except OSError as exc:
|
||||
# re-raise exception if different than
|
||||
# EXDEV - cross drive path
|
||||
# EINVAL - wrong format, must be NTFS
|
||||
self.log.debug("Hardlink failed with errno:'{}'".format(exc.errno))
|
||||
if exc.errno not in [errno.EXDEV, errno.EINVAL]:
|
||||
raise
|
||||
|
||||
shutil.copy(src_path, dst_path)
|
||||
|
||||
def version_from_representations(self, project_name, repres):
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ from .projects import (
|
|||
ProjectItem,
|
||||
ProjectsModel,
|
||||
PROJECTS_MODEL_SENDER,
|
||||
FolderTypeItem,
|
||||
TaskTypeItem,
|
||||
)
|
||||
from .hierarchy import (
|
||||
FolderItem,
|
||||
|
|
@ -28,6 +30,8 @@ __all__ = (
|
|||
"ProjectItem",
|
||||
"ProjectsModel",
|
||||
"PROJECTS_MODEL_SENDER",
|
||||
"FolderTypeItem",
|
||||
"TaskTypeItem",
|
||||
|
||||
"FolderItem",
|
||||
"TaskItem",
|
||||
|
|
|
|||
587
client/ayon_core/tools/publisher/abstract.py
Normal file
|
|
@ -0,0 +1,587 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import (
|
||||
Optional,
|
||||
Dict,
|
||||
List,
|
||||
Tuple,
|
||||
Any,
|
||||
Callable,
|
||||
Union,
|
||||
Iterable,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from ayon_core.lib import AbstractAttrDef
|
||||
from ayon_core.host import HostBase
|
||||
from ayon_core.pipeline.create import CreateContext, CreatedInstance
|
||||
from ayon_core.pipeline.create.context import ConvertorItem
|
||||
from ayon_core.tools.common_models import (
|
||||
FolderItem,
|
||||
TaskItem,
|
||||
FolderTypeItem,
|
||||
TaskTypeItem,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .models import CreatorItem
|
||||
|
||||
|
||||
class CardMessageTypes:
|
||||
standard = None
|
||||
info = "info"
|
||||
error = "error"
|
||||
|
||||
|
||||
class AbstractPublisherCommon(ABC):
|
||||
@abstractmethod
|
||||
def register_event_callback(self, topic, callback):
|
||||
"""Register event callback.
|
||||
|
||||
Listen for events with given topic.
|
||||
|
||||
Args:
|
||||
topic (str): Name of topic.
|
||||
callback (Callable): Callback that will be called when event
|
||||
is triggered.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def emit_event(
|
||||
self, topic: str,
|
||||
data: Optional[Dict[str, Any]] = None,
|
||||
source: Optional[str] = None
|
||||
):
|
||||
"""Emit event.
|
||||
|
||||
Args:
|
||||
topic (str): Event topic used for callbacks filtering.
|
||||
data (Optional[dict[str, Any]]): Event data.
|
||||
source (Optional[str]): Event source.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def emit_card_message(
|
||||
self,
|
||||
message: str,
|
||||
message_type: Optional[str] = CardMessageTypes.standard
|
||||
):
|
||||
"""Emit a card message which can have a lifetime.
|
||||
|
||||
This is for UI purposes. Method can be extended to more arguments
|
||||
in future e.g. different message timeout or type (color).
|
||||
|
||||
Args:
|
||||
message (str): Message that will be showed.
|
||||
message_type (Optional[str]): Message type.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_current_project_name(self) -> Union[str, None]:
|
||||
"""Current context project name.
|
||||
|
||||
Returns:
|
||||
str: Name of project.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_current_folder_path(self) -> Union[str, None]:
|
||||
"""Current context folder path.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Folder path.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_current_task_name(self) -> Union[str, None]:
|
||||
"""Current context task name.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Name of task.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def host_context_has_changed(self) -> bool:
|
||||
"""Host context changed after last reset.
|
||||
|
||||
'CreateContext' has this option available using 'context_has_changed'.
|
||||
|
||||
Returns:
|
||||
bool: Context has changed.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def reset(self):
|
||||
"""Reset whole controller.
|
||||
|
||||
This should reset create context, publish context and all variables
|
||||
that are related to it.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AbstractPublisherBackend(AbstractPublisherCommon):
|
||||
@abstractmethod
|
||||
def is_headless(self) -> bool:
|
||||
"""Controller is in headless mode.
|
||||
|
||||
Notes:
|
||||
Not sure if this method is relevant in UI tool?
|
||||
|
||||
Returns:
|
||||
bool: Headless mode.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_host(self) -> HostBase:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_create_context(self) -> CreateContext:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_task_item_by_name(
|
||||
self,
|
||||
project_name: str,
|
||||
folder_id: str,
|
||||
task_name: str,
|
||||
sender: Optional[str] = None
|
||||
) -> Union[TaskItem, None]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_folder_entity(
|
||||
self, project_name: str, folder_id: str
|
||||
) -> Union[Dict[str, Any], None]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_folder_item_by_path(
|
||||
self, project_name: str, folder_path: str
|
||||
) -> Union[FolderItem, None]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_task_entity(
|
||||
self, project_name: str, task_id: str
|
||||
) -> Union[Dict[str, Any], None]:
|
||||
pass
|
||||
|
||||
|
||||
class AbstractPublisherFrontend(AbstractPublisherCommon):
|
||||
@abstractmethod
|
||||
def register_event_callback(self, topic: str, callback: Callable):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_host_valid(self) -> bool:
|
||||
"""Host is valid for creation part.
|
||||
|
||||
Host must have implemented certain functionality to be able create
|
||||
in Publisher tool.
|
||||
|
||||
Returns:
|
||||
bool: Host can handle creation of instances.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_context_title(self) -> Union[str, None]:
|
||||
"""Get context title for artist shown at the top of main window.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Context title for window or None. In case of None
|
||||
a warning is displayed (not nice for artists).
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_task_items_by_folder_paths(
|
||||
self, folder_paths: Iterable[str]
|
||||
) -> Dict[str, List[TaskItem]]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_folder_items(
|
||||
self, project_name: str, sender: Optional[str] = None
|
||||
) -> List[FolderItem]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_task_items(
|
||||
self, project_name: str, folder_id: str, sender: Optional[str] = None
|
||||
) -> List[TaskItem]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_folder_type_items(
|
||||
self, project_name: str, sender: Optional[str] = None
|
||||
) -> List[FolderTypeItem]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_task_type_items(
|
||||
self, project_name: str, sender: Optional[str] = None
|
||||
) -> List[TaskTypeItem]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def are_folder_paths_valid(self, folder_paths: Iterable[str]) -> bool:
|
||||
"""Folder paths do exist in project.
|
||||
|
||||
Args:
|
||||
folder_paths (Iterable[str]): List of folder paths.
|
||||
|
||||
Returns:
|
||||
bool: All folder paths exist in project.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
# --- Create ---
|
||||
@abstractmethod
|
||||
def get_creator_items(self) -> Dict[str, "CreatorItem"]:
|
||||
"""Creator items by identifier.
|
||||
|
||||
Returns:
|
||||
Dict[str, CreatorItem]: Creator items that will be shown to user.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_creator_icon(
|
||||
self, identifier: str
|
||||
) -> Union[str, Dict[str, Any], None]:
|
||||
"""Receive creator's icon by identifier.
|
||||
|
||||
Todos:
|
||||
Icon should be part of 'CreatorItem'.
|
||||
|
||||
Args:
|
||||
identifier (str): Creator's identifier.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Creator's icon string.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_convertor_items(self) -> Dict[str, ConvertorItem]:
|
||||
"""Convertor items by identifier.
|
||||
|
||||
Returns:
|
||||
Dict[str, ConvertorItem]: Convertor items that can be triggered
|
||||
by user.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_instances(self) -> List[CreatedInstance]:
|
||||
"""Collected/created instances.
|
||||
|
||||
Returns:
|
||||
List[CreatedInstance]: List of created instances.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_instances_by_id(
|
||||
self, instance_ids: Optional[Iterable[str]] = None
|
||||
) -> Dict[str, Union[CreatedInstance, None]]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_existing_product_names(self, folder_path: str) -> List[str]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_creator_attribute_definitions(
|
||||
self, instances: List[CreatedInstance]
|
||||
) -> List[Tuple[AbstractAttrDef, List[CreatedInstance], List[Any]]]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_publish_attribute_definitions(
|
||||
self,
|
||||
instances: List[CreatedInstance],
|
||||
include_context: bool
|
||||
) -> List[Tuple[
|
||||
str,
|
||||
List[AbstractAttrDef],
|
||||
Dict[str, List[Tuple[CreatedInstance, Any]]]
|
||||
]]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_product_name(
|
||||
self,
|
||||
creator_identifier: str,
|
||||
variant: str,
|
||||
task_name: Union[str, None],
|
||||
folder_path: Union[str, None],
|
||||
instance_id: Optional[str] = None
|
||||
):
|
||||
"""Get product name based on passed data.
|
||||
|
||||
Args:
|
||||
creator_identifier (str): Identifier of creator which should be
|
||||
responsible for product name creation.
|
||||
variant (str): Variant value from user's input.
|
||||
task_name (str): Name of task for which is instance created.
|
||||
folder_path (str): Folder path for which is instance created.
|
||||
instance_id (Union[str, None]): Existing instance id when product
|
||||
name is updated.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def create(
|
||||
self,
|
||||
creator_identifier: str,
|
||||
product_name: str,
|
||||
instance_data: Dict[str, Any],
|
||||
options: Dict[str, Any],
|
||||
):
|
||||
"""Trigger creation by creator identifier.
|
||||
|
||||
Should also trigger refresh of instanes.
|
||||
|
||||
Args:
|
||||
creator_identifier (str): Identifier of Creator plugin.
|
||||
product_name (str): Calculated product name.
|
||||
instance_data (Dict[str, Any]): Base instance data with variant,
|
||||
folder path and task name.
|
||||
options (Dict[str, Any]): Data from pre-create attributes.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def trigger_convertor_items(self, convertor_identifiers: List[str]):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def remove_instances(self, instance_ids: Iterable[str]):
|
||||
"""Remove list of instances from create context."""
|
||||
# TODO expect instance ids
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def save_changes(self) -> bool:
|
||||
"""Save changes in create context.
|
||||
|
||||
Save can crash because of unexpected errors.
|
||||
|
||||
Returns:
|
||||
bool: Save was successful.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
# --- Publish ---
|
||||
@abstractmethod
|
||||
def publish(self):
|
||||
"""Trigger publishing without any order limitations."""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def validate(self):
|
||||
"""Trigger publishing which will stop after validation order."""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def stop_publish(self):
|
||||
"""Stop publishing can be also used to pause publishing.
|
||||
|
||||
Pause of publishing is possible only if all plugins successfully
|
||||
finished.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def run_action(self, plugin_id: str, action_id: str):
|
||||
"""Trigger pyblish action on a plugin.
|
||||
|
||||
Args:
|
||||
plugin_id (str): Id of publish plugin.
|
||||
action_id (str): Id of publish action.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def publish_has_started(self) -> bool:
|
||||
"""Has publishing finished.
|
||||
|
||||
Returns:
|
||||
bool: If publishing finished and all plugins were iterated.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def publish_has_finished(self) -> bool:
|
||||
"""Has publishing finished.
|
||||
|
||||
Returns:
|
||||
bool: If publishing finished and all plugins were iterated.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def publish_is_running(self) -> bool:
|
||||
"""Publishing is running right now.
|
||||
|
||||
Returns:
|
||||
bool: If publishing is in progress.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def publish_has_validated(self) -> bool:
|
||||
"""Publish validation passed.
|
||||
|
||||
Returns:
|
||||
bool: If publishing passed last possible validation order.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def publish_can_continue(self):
|
||||
"""Publish has still plugins to process and did not crash yet.
|
||||
|
||||
Returns:
|
||||
bool: Publishing can continue in processing.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def publish_has_crashed(self) -> bool:
|
||||
"""Publishing crashed for any reason.
|
||||
|
||||
Returns:
|
||||
bool: Publishing crashed.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def publish_has_validation_errors(self) -> bool:
|
||||
"""During validation happened at least one validation error.
|
||||
|
||||
Returns:
|
||||
bool: Validation error was raised during validation.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_publish_progress(self) -> int:
|
||||
"""Current progress number.
|
||||
|
||||
Returns:
|
||||
int: Current progress value from 0 to 'publish_max_progress'.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_publish_max_progress(self) -> int:
|
||||
"""Get maximum possible progress number.
|
||||
|
||||
Returns:
|
||||
int: Number that can be used as 100% of publish progress bar.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_publish_error_msg(self) -> Union[str, None]:
|
||||
"""Current error message which cause fail of publishing.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Message which will be showed to artist or
|
||||
None.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_publish_report(self) -> Dict[str, Any]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_validation_errors(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_comment(self, comment: str):
|
||||
"""Set comment on pyblish context.
|
||||
|
||||
Set "comment" key on current pyblish.api.Context data.
|
||||
|
||||
Args:
|
||||
comment (str): Artist's comment.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_thumbnail_paths_for_instances(
|
||||
self, instance_ids: List[str]
|
||||
) -> Dict[str, Union[str, None]]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_thumbnail_paths_for_instances(
|
||||
self, thumbnail_path_mapping: Dict[str, Optional[str]]
|
||||
):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_thumbnail_temp_dir_path(self) -> str:
|
||||
"""Return path to directory where thumbnails can be temporary stored.
|
||||
|
||||
Returns:
|
||||
str: Path to a directory.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def clear_thumbnail_temp_dir_path(self):
|
||||
"""Remove content of thumbnail temp directory."""
|
||||
|
||||
pass
|
||||
|
|
@ -37,6 +37,9 @@ __all__ = (
|
|||
"CONTEXT_ID",
|
||||
"CONTEXT_LABEL",
|
||||
|
||||
"CONTEXT_GROUP",
|
||||
"CONVERTOR_ITEM_GROUP",
|
||||
|
||||
"VARIANT_TOOLTIP",
|
||||
|
||||
"INPUTS_LAYOUT_HSPACING",
|
||||
|
|
|
|||
|
|
@ -1,16 +1,23 @@
|
|||
import collections
|
||||
from abc import abstractmethod, abstractproperty
|
||||
|
||||
from qtpy import QtCore
|
||||
|
||||
from ayon_core.lib.events import Event
|
||||
from ayon_core.pipeline.create import CreatedInstance
|
||||
from .control import PublisherController
|
||||
|
||||
from .control import (
|
||||
MainThreadItem,
|
||||
PublisherController,
|
||||
BasePublisherController,
|
||||
)
|
||||
|
||||
class MainThreadItem:
|
||||
"""Callback with args and kwargs."""
|
||||
|
||||
def __init__(self, callback, *args, **kwargs):
|
||||
self.callback = callback
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
def __call__(self):
|
||||
self.process()
|
||||
|
||||
def process(self):
|
||||
self.callback(*self.args, **self.kwargs)
|
||||
|
||||
|
||||
class MainThreadProcess(QtCore.QObject):
|
||||
|
|
@ -24,7 +31,7 @@ class MainThreadProcess(QtCore.QObject):
|
|||
count_timeout = 2
|
||||
|
||||
def __init__(self):
|
||||
super(MainThreadProcess, self).__init__()
|
||||
super().__init__()
|
||||
self._items_to_process = collections.deque()
|
||||
|
||||
timer = QtCore.QTimer()
|
||||
|
|
@ -33,11 +40,6 @@ class MainThreadProcess(QtCore.QObject):
|
|||
timer.timeout.connect(self._execute)
|
||||
|
||||
self._timer = timer
|
||||
self._switch_counter = self.count_timeout
|
||||
|
||||
def process(self, func, *args, **kwargs):
|
||||
item = MainThreadItem(func, *args, **kwargs)
|
||||
self.add_item(item)
|
||||
|
||||
def add_item(self, item):
|
||||
self._items_to_process.append(item)
|
||||
|
|
@ -46,12 +48,6 @@ class MainThreadProcess(QtCore.QObject):
|
|||
if not self._items_to_process:
|
||||
return
|
||||
|
||||
if self._switch_counter > 0:
|
||||
self._switch_counter -= 1
|
||||
return
|
||||
|
||||
self._switch_counter = self.count_timeout
|
||||
|
||||
item = self._items_to_process.popleft()
|
||||
item.process()
|
||||
|
||||
|
|
@ -73,18 +69,34 @@ class QtPublisherController(PublisherController):
|
|||
def __init__(self, *args, **kwargs):
|
||||
self._main_thread_processor = MainThreadProcess()
|
||||
|
||||
super(QtPublisherController, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.event_system.add_callback(
|
||||
self.register_event_callback(
|
||||
"publish.process.started", self._qt_on_publish_start
|
||||
)
|
||||
self.event_system.add_callback(
|
||||
self.register_event_callback(
|
||||
"publish.process.stopped", self._qt_on_publish_stop
|
||||
)
|
||||
|
||||
def _reset_publish(self):
|
||||
super(QtPublisherController, self)._reset_publish()
|
||||
def reset(self):
|
||||
self._main_thread_processor.clear()
|
||||
super().reset()
|
||||
|
||||
def _start_publish(self, up_validation):
|
||||
self._publish_model.set_publish_up_validation(up_validation)
|
||||
self._publish_model.start_publish(wait=False)
|
||||
self._process_main_thread_item(
|
||||
MainThreadItem(self._next_publish_item_process)
|
||||
)
|
||||
|
||||
def _next_publish_item_process(self):
|
||||
if not self._publish_model.is_running():
|
||||
return
|
||||
func = self._publish_model.get_next_process_func()
|
||||
self._process_main_thread_item(MainThreadItem(func))
|
||||
self._process_main_thread_item(
|
||||
MainThreadItem(self._next_publish_item_process)
|
||||
)
|
||||
|
||||
def _process_main_thread_item(self, item):
|
||||
self._main_thread_processor.add_item(item)
|
||||
|
|
@ -94,347 +106,3 @@ class QtPublisherController(PublisherController):
|
|||
|
||||
def _qt_on_publish_stop(self):
|
||||
self._main_thread_processor.stop()
|
||||
|
||||
|
||||
class QtRemotePublishController(BasePublisherController):
|
||||
"""Abstract Remote controller for Qt UI.
|
||||
|
||||
This controller should be used in process where UI is running and should
|
||||
listen and ask for data on a client side.
|
||||
|
||||
All objects that are used during UI processing should be able to convert
|
||||
on client side to json serializable data and then recreated here. Keep in
|
||||
mind that all changes made here should be send back to client controller
|
||||
before critical actions.
|
||||
|
||||
ATM Was not tested and will require some changes. All code written here is
|
||||
based on theoretical idea how it could work.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._created_instances = {}
|
||||
self._thumbnail_paths_by_instance_id = None
|
||||
|
||||
def _reset_attributes(self):
|
||||
super()._reset_attributes()
|
||||
self._thumbnail_paths_by_instance_id = None
|
||||
|
||||
@abstractmethod
|
||||
def _get_serialized_instances(self):
|
||||
"""Receive serialized instances from client process.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Serialized instances.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def _on_create_instance_change(self):
|
||||
serialized_instances = self._get_serialized_instances()
|
||||
|
||||
created_instances = {}
|
||||
for serialized_data in serialized_instances:
|
||||
item = CreatedInstance.deserialize_on_remote(serialized_data)
|
||||
created_instances[item.id] = item
|
||||
|
||||
self._created_instances = created_instances
|
||||
self._emit_event("instances.refresh.finished")
|
||||
|
||||
def remote_events_handler(self, event_data):
|
||||
event = Event.from_data(event_data)
|
||||
|
||||
# Topics that cause "replication" of controller changes
|
||||
if event.topic == "publish.max_progress.changed":
|
||||
self.publish_max_progress = event["value"]
|
||||
return
|
||||
|
||||
if event.topic == "publish.progress.changed":
|
||||
self.publish_progress = event["value"]
|
||||
return
|
||||
|
||||
if event.topic == "publish.has_validated.changed":
|
||||
self.publish_has_validated = event["value"]
|
||||
return
|
||||
|
||||
if event.topic == "publish.is_running.changed":
|
||||
self.publish_is_running = event["value"]
|
||||
return
|
||||
|
||||
if event.topic == "publish.publish_error.changed":
|
||||
self.publish_error_msg = event["value"]
|
||||
return
|
||||
|
||||
if event.topic == "publish.has_crashed.changed":
|
||||
self.publish_has_crashed = event["value"]
|
||||
return
|
||||
|
||||
if event.topic == "publish.has_validation_errors.changed":
|
||||
self.publish_has_validation_errors = event["value"]
|
||||
return
|
||||
|
||||
if event.topic == "publish.finished.changed":
|
||||
self.publish_has_finished = event["value"]
|
||||
return
|
||||
|
||||
if event.topic == "publish.host_is_valid.changed":
|
||||
self.host_is_valid = event["value"]
|
||||
return
|
||||
|
||||
# Don't skip because UI want know about it too
|
||||
if event.topic == "instance.thumbnail.changed":
|
||||
for instance_id, path in event["mapping"].items():
|
||||
self.thumbnail_paths_by_instance_id[instance_id] = path
|
||||
|
||||
# Topics that can be just passed by because are not affecting
|
||||
# controller itself
|
||||
# - "show.card.message"
|
||||
# - "show.detailed.help"
|
||||
# - "publish.reset.finished"
|
||||
# - "instances.refresh.finished"
|
||||
# - "plugins.refresh.finished"
|
||||
# - "controller.reset.finished"
|
||||
# - "publish.process.started"
|
||||
# - "publish.process.stopped"
|
||||
# - "publish.process.plugin.changed"
|
||||
# - "publish.process.instance.changed"
|
||||
self.event_system.emit_event(event)
|
||||
|
||||
@abstractproperty
|
||||
def project_name(self):
|
||||
"""Current context project name from client.
|
||||
|
||||
Returns:
|
||||
str: Name of project.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractproperty
|
||||
def current_folder_path(self):
|
||||
"""Current context folder path from host.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Folder path.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractproperty
|
||||
def current_task_name(self):
|
||||
"""Current context task name from client.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Name of task.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@property
|
||||
def instances(self):
|
||||
"""Collected/created instances.
|
||||
|
||||
Returns:
|
||||
List[CreatedInstance]: List of created instances.
|
||||
"""
|
||||
|
||||
return self._created_instances
|
||||
|
||||
def get_context_title(self):
|
||||
"""Get context title for artist shown at the top of main window.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Context title for window or None. In case of None
|
||||
a warning is displayed (not nice for artists).
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def get_existing_product_names(self, folder_path):
|
||||
pass
|
||||
|
||||
@property
|
||||
def thumbnail_paths_by_instance_id(self):
|
||||
if self._thumbnail_paths_by_instance_id is None:
|
||||
self._thumbnail_paths_by_instance_id = (
|
||||
self._collect_thumbnail_paths_by_instance_id()
|
||||
)
|
||||
return self._thumbnail_paths_by_instance_id
|
||||
|
||||
def get_thumbnail_path_for_instance(self, instance_id):
|
||||
return self.thumbnail_paths_by_instance_id.get(instance_id)
|
||||
|
||||
def set_thumbnail_path_for_instance(self, instance_id, thumbnail_path):
|
||||
self._set_thumbnail_path_on_context(self, instance_id, thumbnail_path)
|
||||
|
||||
@abstractmethod
|
||||
def _collect_thumbnail_paths_by_instance_id(self):
|
||||
"""Collect thumbnail paths by instance id in remote controller.
|
||||
|
||||
These should be collected from 'CreatedContext' there.
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: Mapping of thumbnail path by instance id.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _set_thumbnail_path_on_context(self, instance_id, thumbnail_path):
|
||||
"""Send change of thumbnail path in remote controller.
|
||||
|
||||
That should trigger event 'instance.thumbnail.changed' which is
|
||||
captured and handled in default implementation in this class.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_product_name(
|
||||
self,
|
||||
creator_identifier,
|
||||
variant,
|
||||
task_name,
|
||||
folder_path,
|
||||
instance_id=None
|
||||
):
|
||||
"""Get product name based on passed data.
|
||||
|
||||
Args:
|
||||
creator_identifier (str): Identifier of creator which should be
|
||||
responsible for product name creation.
|
||||
variant (str): Variant value from user's input.
|
||||
task_name (str): Name of task for which is instance created.
|
||||
folder_path (str): Folder path for which is instance created.
|
||||
instance_id (Union[str, None]): Existing instance id when product
|
||||
name is updated.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def create(
|
||||
self, creator_identifier, product_name, instance_data, options
|
||||
):
|
||||
"""Trigger creation by creator identifier.
|
||||
|
||||
Should also trigger refresh of instanes.
|
||||
|
||||
Args:
|
||||
creator_identifier (str): Identifier of Creator plugin.
|
||||
product_name (str): Calculated product name.
|
||||
instance_data (Dict[str, Any]): Base instance data with variant,
|
||||
folder path and task name.
|
||||
options (Dict[str, Any]): Data from pre-create attributes.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def _get_instance_changes_for_client(self):
|
||||
"""Preimplemented method to receive instance changes for client."""
|
||||
|
||||
created_instance_changes = {}
|
||||
for instance_id, instance in self._created_instances.items():
|
||||
created_instance_changes[instance_id] = (
|
||||
instance.remote_changes()
|
||||
)
|
||||
return created_instance_changes
|
||||
|
||||
@abstractmethod
|
||||
def _send_instance_changes_to_client(self):
|
||||
# TODO Implement to send 'instance_changes' value to client
|
||||
# instance_changes = self._get_instance_changes_for_client()
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def save_changes(self):
|
||||
"""Save changes happened during creation."""
|
||||
|
||||
self._send_instance_changes_to_client()
|
||||
|
||||
@abstractmethod
|
||||
def remove_instances(self, instance_ids):
|
||||
"""Remove list of instances from create context."""
|
||||
# TODO add Args:
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_publish_report(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_validation_errors(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def reset(self):
|
||||
"""Reset whole controller.
|
||||
|
||||
This should reset create context, publish context and all variables
|
||||
that are related to it.
|
||||
"""
|
||||
|
||||
self._send_instance_changes_to_client()
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def publish(self):
|
||||
"""Trigger publishing without any order limitations."""
|
||||
|
||||
self._send_instance_changes_to_client()
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def validate(self):
|
||||
"""Trigger publishing which will stop after validation order."""
|
||||
|
||||
self._send_instance_changes_to_client()
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def stop_publish(self):
|
||||
"""Stop publishing can be also used to pause publishing.
|
||||
|
||||
Pause of publishing is possible only if all plugins successfully
|
||||
finished.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def run_action(self, plugin_id, action_id):
|
||||
"""Trigger pyblish action on a plugin.
|
||||
|
||||
Args:
|
||||
plugin_id (str): Id of publish plugin.
|
||||
action_id (str): Id of publish action.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_comment(self, comment):
|
||||
"""Set comment on pyblish context.
|
||||
|
||||
Set "comment" key on current pyblish.api.Context data.
|
||||
|
||||
Args:
|
||||
comment (str): Artist's comment.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def emit_card_message(self, message):
|
||||
"""Emit a card message which can have a lifetime.
|
||||
|
||||
This is for UI purposes. Method can be extended to more arguments
|
||||
in future e.g. different message timeout or type (color).
|
||||
|
||||
Args:
|
||||
message (str): Message that will be showed.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
|
|||
10
client/ayon_core/tools/publisher/models/__init__.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from .create import CreateModel, CreatorItem
|
||||
from .publish import PublishModel
|
||||
|
||||
|
||||
__all__ = (
|
||||
"CreateModel",
|
||||
"CreatorItem",
|
||||
|
||||
"PublishModel",
|
||||
)
|
||||
758
client/ayon_core/tools/publisher/models/create.py
Normal file
|
|
@ -0,0 +1,758 @@
|
|||
import logging
|
||||
import re
|
||||
from typing import Union, List, Dict, Tuple, Any, Optional, Iterable, Pattern
|
||||
|
||||
from ayon_core.lib.attribute_definitions import (
|
||||
serialize_attr_defs,
|
||||
deserialize_attr_defs,
|
||||
AbstractAttrDef,
|
||||
)
|
||||
from ayon_core.lib.profiles_filtering import filter_profiles
|
||||
from ayon_core.lib.attribute_definitions import UIDef
|
||||
from ayon_core.pipeline.create import (
|
||||
BaseCreator,
|
||||
AutoCreator,
|
||||
HiddenCreator,
|
||||
Creator,
|
||||
CreateContext,
|
||||
CreatedInstance,
|
||||
)
|
||||
from ayon_core.pipeline.create.context import (
|
||||
CreatorsOperationFailed,
|
||||
ConvertorsOperationFailed,
|
||||
ConvertorItem,
|
||||
)
|
||||
from ayon_core.tools.publisher.abstract import (
|
||||
AbstractPublisherBackend,
|
||||
CardMessageTypes,
|
||||
)
|
||||
CREATE_EVENT_SOURCE = "publisher.create.model"
|
||||
|
||||
|
||||
class CreatorType:
|
||||
def __init__(self, name: str):
|
||||
self.name: str = name
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.name == str(other)
|
||||
|
||||
def __ne__(self, other):
|
||||
# This is implemented only because of Python 2
|
||||
return not self == other
|
||||
|
||||
|
||||
class CreatorTypes:
|
||||
base = CreatorType("base")
|
||||
auto = CreatorType("auto")
|
||||
hidden = CreatorType("hidden")
|
||||
artist = CreatorType("artist")
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, value: str) -> CreatorType:
|
||||
for creator_type in (
|
||||
cls.base,
|
||||
cls.auto,
|
||||
cls.hidden,
|
||||
cls.artist
|
||||
):
|
||||
if value == creator_type:
|
||||
return creator_type
|
||||
raise ValueError("Unknown type \"{}\"".format(str(value)))
|
||||
|
||||
|
||||
class CreatorItem:
|
||||
"""Wrapper around Creator plugin.
|
||||
|
||||
Object can be serialized and recreated.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
identifier: str,
|
||||
creator_type: CreatorType,
|
||||
product_type: str,
|
||||
label: str,
|
||||
group_label: str,
|
||||
icon: Union[str, Dict[str, Any], None],
|
||||
description: Union[str, None],
|
||||
detailed_description: Union[str, None],
|
||||
default_variant: Union[str, None],
|
||||
default_variants: Union[List[str], None],
|
||||
create_allow_context_change: Union[bool, None],
|
||||
create_allow_thumbnail: Union[bool, None],
|
||||
show_order: int,
|
||||
pre_create_attributes_defs: List[AbstractAttrDef],
|
||||
):
|
||||
self.identifier: str = identifier
|
||||
self.creator_type: CreatorType = creator_type
|
||||
self.product_type: str = product_type
|
||||
self.label: str = label
|
||||
self.group_label: str = group_label
|
||||
self.icon: Union[str, Dict[str, Any], None] = icon
|
||||
self.description: Union[str, None] = description
|
||||
self.detailed_description: Union[bool, None] = detailed_description
|
||||
self.default_variant: Union[bool, None] = default_variant
|
||||
self.default_variants: Union[List[str], None] = default_variants
|
||||
self.create_allow_context_change: Union[bool, None] = (
|
||||
create_allow_context_change
|
||||
)
|
||||
self.create_allow_thumbnail: Union[bool, None] = create_allow_thumbnail
|
||||
self.show_order: int = show_order
|
||||
self.pre_create_attributes_defs: List[AbstractAttrDef] = (
|
||||
pre_create_attributes_defs
|
||||
)
|
||||
|
||||
def get_group_label(self) -> str:
|
||||
return self.group_label
|
||||
|
||||
@classmethod
|
||||
def from_creator(cls, creator: BaseCreator):
|
||||
creator_type: CreatorType = CreatorTypes.base
|
||||
if isinstance(creator, AutoCreator):
|
||||
creator_type = CreatorTypes.auto
|
||||
elif isinstance(creator, HiddenCreator):
|
||||
creator_type = CreatorTypes.hidden
|
||||
elif isinstance(creator, Creator):
|
||||
creator_type = CreatorTypes.artist
|
||||
|
||||
description = None
|
||||
detail_description = None
|
||||
default_variant = None
|
||||
default_variants = None
|
||||
pre_create_attr_defs = None
|
||||
create_allow_context_change = None
|
||||
create_allow_thumbnail = None
|
||||
show_order = creator.order
|
||||
if creator_type is CreatorTypes.artist:
|
||||
description = creator.get_description()
|
||||
detail_description = creator.get_detail_description()
|
||||
default_variant = creator.get_default_variant()
|
||||
default_variants = creator.get_default_variants()
|
||||
pre_create_attr_defs = creator.get_pre_create_attr_defs()
|
||||
create_allow_context_change = creator.create_allow_context_change
|
||||
create_allow_thumbnail = creator.create_allow_thumbnail
|
||||
show_order = creator.show_order
|
||||
|
||||
identifier = creator.identifier
|
||||
return cls(
|
||||
identifier,
|
||||
creator_type,
|
||||
creator.product_type,
|
||||
creator.label or identifier,
|
||||
creator.get_group_label(),
|
||||
creator.get_icon(),
|
||||
description,
|
||||
detail_description,
|
||||
default_variant,
|
||||
default_variants,
|
||||
create_allow_context_change,
|
||||
create_allow_thumbnail,
|
||||
show_order,
|
||||
pre_create_attr_defs,
|
||||
)
|
||||
|
||||
def to_data(self) -> Dict[str, Any]:
|
||||
pre_create_attributes_defs = None
|
||||
if self.pre_create_attributes_defs is not None:
|
||||
pre_create_attributes_defs = serialize_attr_defs(
|
||||
self.pre_create_attributes_defs
|
||||
)
|
||||
|
||||
return {
|
||||
"identifier": self.identifier,
|
||||
"creator_type": str(self.creator_type),
|
||||
"product_type": self.product_type,
|
||||
"label": self.label,
|
||||
"group_label": self.group_label,
|
||||
"icon": self.icon,
|
||||
"description": self.description,
|
||||
"detailed_description": self.detailed_description,
|
||||
"default_variant": self.default_variant,
|
||||
"default_variants": self.default_variants,
|
||||
"create_allow_context_change": self.create_allow_context_change,
|
||||
"create_allow_thumbnail": self.create_allow_thumbnail,
|
||||
"show_order": self.show_order,
|
||||
"pre_create_attributes_defs": pre_create_attributes_defs,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: Dict[str, Any]) -> "CreatorItem":
|
||||
pre_create_attributes_defs = data["pre_create_attributes_defs"]
|
||||
if pre_create_attributes_defs is not None:
|
||||
data["pre_create_attributes_defs"] = deserialize_attr_defs(
|
||||
pre_create_attributes_defs
|
||||
)
|
||||
|
||||
data["creator_type"] = CreatorTypes.from_str(data["creator_type"])
|
||||
return cls(**data)
|
||||
|
||||
|
||||
class CreateModel:
|
||||
def __init__(self, controller: AbstractPublisherBackend):
|
||||
self._log = None
|
||||
self._controller: AbstractPublisherBackend = controller
|
||||
|
||||
self._create_context = CreateContext(
|
||||
controller.get_host(),
|
||||
headless=controller.is_headless(),
|
||||
reset=False
|
||||
)
|
||||
# State flags to prevent executing method which is already in progress
|
||||
self._creator_items = None
|
||||
|
||||
@property
|
||||
def log(self) -> logging.Logger:
|
||||
if self._log is None:
|
||||
self._log = logging.getLogger(self.__class__.__name__)
|
||||
return self._log
|
||||
|
||||
def is_host_valid(self) -> bool:
|
||||
return self._create_context.host_is_valid
|
||||
|
||||
def get_create_context(self) -> CreateContext:
|
||||
return self._create_context
|
||||
|
||||
def get_current_project_name(self) -> Union[str, None]:
|
||||
"""Current project context defined by host.
|
||||
|
||||
Returns:
|
||||
str: Project name.
|
||||
|
||||
"""
|
||||
return self._create_context.get_current_project_name()
|
||||
|
||||
def get_current_folder_path(self) -> Union[str, None]:
|
||||
"""Current context folder path defined by host.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Folder path or None if folder is not set.
|
||||
"""
|
||||
|
||||
return self._create_context.get_current_folder_path()
|
||||
|
||||
def get_current_task_name(self) -> Union[str, None]:
|
||||
"""Current context task name defined by host.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Task name or None if task is not set.
|
||||
"""
|
||||
|
||||
return self._create_context.get_current_task_name()
|
||||
|
||||
def host_context_has_changed(self) -> bool:
|
||||
return self._create_context.context_has_changed
|
||||
|
||||
def reset(self):
|
||||
self._create_context.reset_preparation()
|
||||
|
||||
# Reset current context
|
||||
self._create_context.reset_current_context()
|
||||
|
||||
self._create_context.reset_plugins()
|
||||
# Reset creator items
|
||||
self._creator_items = None
|
||||
|
||||
self._reset_instances()
|
||||
self._create_context.reset_finalization()
|
||||
|
||||
def get_creator_items(self) -> Dict[str, CreatorItem]:
|
||||
"""Creators that can be shown in create dialog."""
|
||||
if self._creator_items is None:
|
||||
self._creator_items = self._collect_creator_items()
|
||||
return self._creator_items
|
||||
|
||||
def get_creator_item_by_id(
|
||||
self, identifier: str
|
||||
) -> Union[CreatorItem, None]:
|
||||
items = self.get_creator_items()
|
||||
return items.get(identifier)
|
||||
|
||||
def get_creator_icon(
|
||||
self, identifier: str
|
||||
) -> Union[str, Dict[str, Any], None]:
|
||||
"""Function to receive icon for creator identifier.
|
||||
|
||||
Args:
|
||||
identifier (str): Creator's identifier for which should
|
||||
be icon returned.
|
||||
|
||||
"""
|
||||
creator_item = self.get_creator_item_by_id(identifier)
|
||||
if creator_item is not None:
|
||||
return creator_item.icon
|
||||
return None
|
||||
|
||||
def get_instances(self) -> List[CreatedInstance]:
|
||||
"""Current instances in create context."""
|
||||
return list(self._create_context.instances_by_id.values())
|
||||
|
||||
def get_instance_by_id(
|
||||
self, instance_id: str
|
||||
) -> Union[CreatedInstance, None]:
|
||||
return self._create_context.instances_by_id.get(instance_id)
|
||||
|
||||
def get_instances_by_id(
|
||||
self, instance_ids: Optional[Iterable[str]] = None
|
||||
) -> Dict[str, Union[CreatedInstance, None]]:
|
||||
if instance_ids is None:
|
||||
instance_ids = self._create_context.instances_by_id.keys()
|
||||
return {
|
||||
instance_id: self.get_instance_by_id(instance_id)
|
||||
for instance_id in instance_ids
|
||||
}
|
||||
|
||||
def get_convertor_items(self) -> Dict[str, ConvertorItem]:
|
||||
return self._create_context.convertor_items_by_id
|
||||
|
||||
def get_product_name(
|
||||
self,
|
||||
creator_identifier: str,
|
||||
variant: str,
|
||||
task_name: Union[str, None],
|
||||
folder_path: Union[str, None],
|
||||
instance_id: Optional[str] = None
|
||||
) -> str:
|
||||
"""Get product name based on passed data.
|
||||
|
||||
Args:
|
||||
creator_identifier (str): Identifier of creator which should be
|
||||
responsible for product name creation.
|
||||
variant (str): Variant value from user's input.
|
||||
task_name (str): Name of task for which is instance created.
|
||||
folder_path (str): Folder path for which is instance created.
|
||||
instance_id (Union[str, None]): Existing instance id when product
|
||||
name is updated.
|
||||
"""
|
||||
|
||||
creator = self._creators[creator_identifier]
|
||||
|
||||
instance = None
|
||||
if instance_id:
|
||||
instance = self.get_instance_by_id(instance_id)
|
||||
|
||||
project_name = self._controller.get_current_project_name()
|
||||
folder_item = self._controller.get_folder_item_by_path(
|
||||
project_name, folder_path
|
||||
)
|
||||
folder_entity = None
|
||||
task_item = None
|
||||
task_entity = None
|
||||
if folder_item is not None:
|
||||
folder_entity = self._controller.get_folder_entity(
|
||||
project_name, folder_item.entity_id
|
||||
)
|
||||
task_item = self._controller.get_task_item_by_name(
|
||||
project_name,
|
||||
folder_item.entity_id,
|
||||
task_name,
|
||||
CREATE_EVENT_SOURCE
|
||||
)
|
||||
|
||||
if task_item is not None:
|
||||
task_entity = self._controller.get_task_entity(
|
||||
project_name, task_item.task_id
|
||||
)
|
||||
|
||||
return creator.get_product_name(
|
||||
project_name,
|
||||
folder_entity,
|
||||
task_entity,
|
||||
variant,
|
||||
instance=instance
|
||||
)
|
||||
|
||||
def create(
|
||||
self,
|
||||
creator_identifier: str,
|
||||
product_name: str,
|
||||
instance_data: Dict[str, Any],
|
||||
options: Dict[str, Any],
|
||||
):
|
||||
"""Trigger creation and refresh of instances in UI."""
|
||||
|
||||
success = True
|
||||
try:
|
||||
self._create_context.create_with_unified_error(
|
||||
creator_identifier, product_name, instance_data, options
|
||||
)
|
||||
|
||||
except CreatorsOperationFailed as exc:
|
||||
success = False
|
||||
self._emit_event(
|
||||
"instances.create.failed",
|
||||
{
|
||||
"title": "Creation failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
self._on_create_instance_change()
|
||||
return success
|
||||
|
||||
def trigger_convertor_items(self, convertor_identifiers: List[str]):
|
||||
"""Trigger legacy item convertors.
|
||||
|
||||
This functionality requires to save and reset CreateContext. The reset
|
||||
is needed so Creators can collect converted items.
|
||||
|
||||
Args:
|
||||
convertor_identifiers (list[str]): Identifiers of convertor
|
||||
plugins.
|
||||
"""
|
||||
|
||||
success = True
|
||||
try:
|
||||
self._create_context.run_convertors(convertor_identifiers)
|
||||
|
||||
except ConvertorsOperationFailed as exc:
|
||||
success = False
|
||||
self._emit_event(
|
||||
"convertors.convert.failed",
|
||||
{
|
||||
"title": "Conversion failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
if success:
|
||||
self._controller.emit_card_message(
|
||||
"Conversion finished"
|
||||
)
|
||||
else:
|
||||
self._controller.emit_card_message(
|
||||
"Conversion failed",
|
||||
CardMessageTypes.error
|
||||
)
|
||||
|
||||
def save_changes(self, show_message: Optional[bool] = True) -> bool:
|
||||
"""Save changes happened during creation.
|
||||
|
||||
Trigger save of changes using host api. This functionality does not
|
||||
validate anything. It is required to do checks before this method is
|
||||
called to be able to give user actionable response e.g. check of
|
||||
context using 'host_context_has_changed'.
|
||||
|
||||
Args:
|
||||
show_message (bool): Show message that changes were
|
||||
saved successfully.
|
||||
|
||||
Returns:
|
||||
bool: Save of changes was successful.
|
||||
"""
|
||||
|
||||
if not self._create_context.host_is_valid:
|
||||
# TODO remove
|
||||
# Fake success save when host is not valid for CreateContext
|
||||
# this is for testing as experimental feature
|
||||
return True
|
||||
|
||||
try:
|
||||
self._create_context.save_changes()
|
||||
if show_message:
|
||||
self._controller.emit_card_message("Saved changes..")
|
||||
return True
|
||||
|
||||
except CreatorsOperationFailed as exc:
|
||||
self._emit_event(
|
||||
"instances.save.failed",
|
||||
{
|
||||
"title": "Instances save failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
def remove_instances(self, instance_ids: List[str]):
|
||||
"""Remove instances based on instance ids.
|
||||
|
||||
Args:
|
||||
instance_ids (List[str]): List of instance ids to remove.
|
||||
"""
|
||||
|
||||
# QUESTION Expect that instances are really removed? In that case reset
|
||||
# is not required.
|
||||
self._remove_instances_from_context(instance_ids)
|
||||
|
||||
self._on_create_instance_change()
|
||||
|
||||
def get_creator_attribute_definitions(
|
||||
self, instances: List[CreatedInstance]
|
||||
) -> List[Tuple[AbstractAttrDef, List[CreatedInstance], List[Any]]]:
|
||||
"""Collect creator attribute definitions for multuple instances.
|
||||
|
||||
Args:
|
||||
instances (List[CreatedInstance]): List of created instances for
|
||||
which should be attribute definitions returned.
|
||||
"""
|
||||
|
||||
# NOTE it would be great if attrdefs would have hash method implemented
|
||||
# so they could be used as keys in dictionary
|
||||
output = []
|
||||
_attr_defs = {}
|
||||
for instance in instances:
|
||||
for attr_def in instance.creator_attribute_defs:
|
||||
found_idx = None
|
||||
for idx, _attr_def in _attr_defs.items():
|
||||
if attr_def == _attr_def:
|
||||
found_idx = idx
|
||||
break
|
||||
|
||||
value = None
|
||||
if attr_def.is_value_def:
|
||||
value = instance.creator_attributes[attr_def.key]
|
||||
if found_idx is None:
|
||||
idx = len(output)
|
||||
output.append((attr_def, [instance], [value]))
|
||||
_attr_defs[idx] = attr_def
|
||||
else:
|
||||
item = output[found_idx]
|
||||
item[1].append(instance)
|
||||
item[2].append(value)
|
||||
return output
|
||||
|
||||
def get_publish_attribute_definitions(
|
||||
self,
|
||||
instances: List[CreatedInstance],
|
||||
include_context: bool
|
||||
) -> List[Tuple[
|
||||
str,
|
||||
List[AbstractAttrDef],
|
||||
Dict[str, List[Tuple[CreatedInstance, Any]]]
|
||||
]]:
|
||||
"""Collect publish attribute definitions for passed instances.
|
||||
|
||||
Args:
|
||||
instances (list[CreatedInstance]): List of created instances for
|
||||
which should be attribute definitions returned.
|
||||
include_context (bool): Add context specific attribute definitions.
|
||||
|
||||
"""
|
||||
_tmp_items = []
|
||||
if include_context:
|
||||
_tmp_items.append(self._create_context)
|
||||
|
||||
for instance in instances:
|
||||
_tmp_items.append(instance)
|
||||
|
||||
all_defs_by_plugin_name = {}
|
||||
all_plugin_values = {}
|
||||
for item in _tmp_items:
|
||||
for plugin_name, attr_val in item.publish_attributes.items():
|
||||
attr_defs = attr_val.attr_defs
|
||||
if not attr_defs:
|
||||
continue
|
||||
|
||||
if plugin_name not in all_defs_by_plugin_name:
|
||||
all_defs_by_plugin_name[plugin_name] = attr_val.attr_defs
|
||||
|
||||
plugin_values = all_plugin_values.setdefault(plugin_name, {})
|
||||
|
||||
for attr_def in attr_defs:
|
||||
if isinstance(attr_def, UIDef):
|
||||
continue
|
||||
|
||||
attr_values = plugin_values.setdefault(attr_def.key, [])
|
||||
|
||||
value = attr_val[attr_def.key]
|
||||
attr_values.append((item, value))
|
||||
|
||||
output = []
|
||||
for plugin in self._create_context.plugins_with_defs:
|
||||
plugin_name = plugin.__name__
|
||||
if plugin_name not in all_defs_by_plugin_name:
|
||||
continue
|
||||
output.append((
|
||||
plugin_name,
|
||||
all_defs_by_plugin_name[plugin_name],
|
||||
all_plugin_values
|
||||
))
|
||||
return output
|
||||
|
||||
def get_thumbnail_paths_for_instances(
|
||||
self, instance_ids: List[str]
|
||||
) -> Dict[str, Union[str, None]]:
|
||||
thumbnail_paths_by_instance_id = (
|
||||
self._create_context.thumbnail_paths_by_instance_id
|
||||
)
|
||||
return {
|
||||
instance_id: thumbnail_paths_by_instance_id.get(instance_id)
|
||||
for instance_id in instance_ids
|
||||
}
|
||||
|
||||
def set_thumbnail_paths_for_instances(
|
||||
self, thumbnail_path_mapping: Dict[str, str]
|
||||
):
|
||||
thumbnail_paths_by_instance_id = (
|
||||
self._create_context.thumbnail_paths_by_instance_id
|
||||
)
|
||||
for instance_id, thumbnail_path in thumbnail_path_mapping.items():
|
||||
thumbnail_paths_by_instance_id[instance_id] = thumbnail_path
|
||||
|
||||
self._emit_event(
|
||||
"instance.thumbnail.changed",
|
||||
{
|
||||
"mapping": thumbnail_path_mapping
|
||||
}
|
||||
)
|
||||
|
||||
def _emit_event(self, topic: str, data: Optional[Dict[str, Any]] = None):
|
||||
self._controller.emit_event(topic, data)
|
||||
|
||||
def _get_current_project_settings(self) -> Dict[str, Any]:
|
||||
"""Current project settings.
|
||||
|
||||
Returns:
|
||||
dict
|
||||
"""
|
||||
|
||||
return self._create_context.get_current_project_settings()
|
||||
|
||||
@property
|
||||
def _creators(self) -> Dict[str, BaseCreator]:
|
||||
"""All creators loaded in create context."""
|
||||
|
||||
return self._create_context.creators
|
||||
|
||||
def _reset_instances(self):
|
||||
"""Reset create instances."""
|
||||
|
||||
self._create_context.reset_context_data()
|
||||
with self._create_context.bulk_instances_collection():
|
||||
try:
|
||||
self._create_context.reset_instances()
|
||||
except CreatorsOperationFailed as exc:
|
||||
self._emit_event(
|
||||
"instances.collection.failed",
|
||||
{
|
||||
"title": "Instance collection failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
self._create_context.find_convertor_items()
|
||||
except ConvertorsOperationFailed as exc:
|
||||
self._emit_event(
|
||||
"convertors.find.failed",
|
||||
{
|
||||
"title": "Collection of unsupported product failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
self._create_context.execute_autocreators()
|
||||
|
||||
except CreatorsOperationFailed as exc:
|
||||
self._emit_event(
|
||||
"instances.create.failed",
|
||||
{
|
||||
"title": "AutoCreation failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
self._on_create_instance_change()
|
||||
|
||||
def _remove_instances_from_context(self, instance_ids: List[str]):
|
||||
instances_by_id = self._create_context.instances_by_id
|
||||
instances = [
|
||||
instances_by_id[instance_id]
|
||||
for instance_id in instance_ids
|
||||
]
|
||||
try:
|
||||
self._create_context.remove_instances(instances)
|
||||
except CreatorsOperationFailed as exc:
|
||||
self._emit_event(
|
||||
"instances.remove.failed",
|
||||
{
|
||||
"title": "Instance removement failed",
|
||||
"failed_info": exc.failed_info
|
||||
}
|
||||
)
|
||||
|
||||
def _on_create_instance_change(self):
|
||||
self._emit_event("instances.refresh.finished")
|
||||
|
||||
def _collect_creator_items(self) -> Dict[str, CreatorItem]:
|
||||
# TODO add crashed initialization of create plugins to report
|
||||
output = {}
|
||||
allowed_creator_pattern = self._get_allowed_creators_pattern()
|
||||
for identifier, creator in self._create_context.creators.items():
|
||||
try:
|
||||
if self._is_label_allowed(
|
||||
creator.label, allowed_creator_pattern
|
||||
):
|
||||
output[identifier] = CreatorItem.from_creator(creator)
|
||||
continue
|
||||
self.log.debug(f"{creator.label} not allowed for context")
|
||||
except Exception:
|
||||
self.log.error(
|
||||
"Failed to create creator item for '%s'",
|
||||
identifier,
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
return output
|
||||
|
||||
def _get_allowed_creators_pattern(self) -> Union[Pattern, None]:
|
||||
"""Provide regex pattern for configured creator labels in this context
|
||||
|
||||
If no profile matches current context, it shows all creators.
|
||||
Support usage of regular expressions for configured values.
|
||||
Returns:
|
||||
(re.Pattern)[optional]: None or regex compiled patterns
|
||||
into single one ('Render|Image.*')
|
||||
"""
|
||||
|
||||
task_type = self._create_context.get_current_task_type()
|
||||
project_settings = self._get_current_project_settings()
|
||||
|
||||
filter_creator_profiles = (
|
||||
project_settings
|
||||
["core"]
|
||||
["tools"]
|
||||
["creator"]
|
||||
["filter_creator_profiles"]
|
||||
)
|
||||
filtering_criteria = {
|
||||
"task_names": self.get_current_task_name(),
|
||||
"task_types": task_type,
|
||||
"host_names": self._create_context.host_name
|
||||
}
|
||||
profile = filter_profiles(
|
||||
filter_creator_profiles,
|
||||
filtering_criteria,
|
||||
logger=self.log
|
||||
)
|
||||
|
||||
allowed_creator_pattern = None
|
||||
if profile:
|
||||
allowed_creator_labels = {
|
||||
label
|
||||
for label in profile["creator_labels"]
|
||||
if label
|
||||
}
|
||||
self.log.debug(f"Only allowed `{allowed_creator_labels}` creators")
|
||||
allowed_creator_pattern = (
|
||||
re.compile("|".join(allowed_creator_labels)))
|
||||
return allowed_creator_pattern
|
||||
|
||||
def _is_label_allowed(
|
||||
self,
|
||||
label: str,
|
||||
allowed_labels_regex: Union[Pattern, None]
|
||||
) -> bool:
|
||||
"""Implement regex support for allowed labels.
|
||||
|
||||
Args:
|
||||
label (str): Label of creator - shown in Publisher
|
||||
allowed_labels_regex (re.Pattern): compiled regular expression
|
||||
"""
|
||||
if not allowed_labels_regex:
|
||||
return True
|
||||
return bool(allowed_labels_regex.match(label))
|
||||
1266
client/ayon_core/tools/publisher/models/publish.py
Normal file
|
|
@ -17,7 +17,7 @@ from .constants import (
|
|||
|
||||
class InstancesModel(QtGui.QStandardItemModel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(InstancesModel, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._items_by_id = {}
|
||||
self._plugin_items_by_id = {}
|
||||
|
|
@ -83,7 +83,7 @@ class InstancesModel(QtGui.QStandardItemModel):
|
|||
|
||||
class InstanceProxyModel(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(InstanceProxyModel, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._ignore_removed = True
|
||||
|
||||
|
|
@ -116,7 +116,7 @@ class PluginsModel(QtGui.QStandardItemModel):
|
|||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PluginsModel, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._items_by_id = {}
|
||||
self._plugin_items_by_id = {}
|
||||
|
|
@ -185,7 +185,7 @@ class PluginsModel(QtGui.QStandardItemModel):
|
|||
|
||||
class PluginProxyModel(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PluginProxyModel, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._ignore_skipped = True
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class PluginLoadReportModel(QtGui.QStandardItemModel):
|
|||
|
||||
class DetailWidget(QtWidgets.QTextEdit):
|
||||
def __init__(self, text, *args, **kwargs):
|
||||
super(DetailWidget, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setReadOnly(True)
|
||||
self.setHtml(text)
|
||||
|
|
@ -73,7 +73,7 @@ class DetailWidget(QtWidgets.QTextEdit):
|
|||
|
||||
class PluginLoadReportWidget(QtWidgets.QWidget):
|
||||
def __init__(self, parent):
|
||||
super(PluginLoadReportWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
view = QtWidgets.QTreeView(self)
|
||||
view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||
|
|
@ -101,11 +101,11 @@ class PluginLoadReportWidget(QtWidgets.QWidget):
|
|||
self._create_widget(child_index)
|
||||
|
||||
def showEvent(self, event):
|
||||
super(PluginLoadReportWidget, self).showEvent(event)
|
||||
super().showEvent(event)
|
||||
self._update_widgets_size_hints()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(PluginLoadReportWidget, self).resizeEvent(event)
|
||||
super().resizeEvent(event)
|
||||
self._update_widgets_size_hints()
|
||||
|
||||
def _update_widgets_size_hints(self):
|
||||
|
|
@ -146,7 +146,7 @@ class ZoomPlainText(QtWidgets.QPlainTextEdit):
|
|||
max_point_size = 200.0
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ZoomPlainText, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
anim_timer = QtCore.QTimer()
|
||||
anim_timer.setInterval(20)
|
||||
|
|
@ -160,7 +160,7 @@ class ZoomPlainText(QtWidgets.QPlainTextEdit):
|
|||
def wheelEvent(self, event):
|
||||
modifiers = QtWidgets.QApplication.keyboardModifiers()
|
||||
if modifiers != QtCore.Qt.ControlModifier:
|
||||
super(ZoomPlainText, self).wheelEvent(event)
|
||||
super().wheelEvent(event)
|
||||
return
|
||||
|
||||
if hasattr(event, "angleDelta"):
|
||||
|
|
@ -219,7 +219,7 @@ class ZoomPlainText(QtWidgets.QPlainTextEdit):
|
|||
|
||||
class DetailsWidget(QtWidgets.QWidget):
|
||||
def __init__(self, parent):
|
||||
super(DetailsWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
output_widget = ZoomPlainText(self)
|
||||
output_widget.setObjectName("PublishLogConsole")
|
||||
|
|
@ -327,7 +327,7 @@ class DetailsPopup(QtWidgets.QDialog):
|
|||
closed = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent, center_widget):
|
||||
super(DetailsPopup, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Report Details")
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
|
||||
|
|
@ -338,19 +338,19 @@ class DetailsPopup(QtWidgets.QDialog):
|
|||
def showEvent(self, event):
|
||||
layout = self.layout()
|
||||
layout.insertWidget(0, self._center_widget)
|
||||
super(DetailsPopup, self).showEvent(event)
|
||||
super().showEvent(event)
|
||||
if self._first_show:
|
||||
self._first_show = False
|
||||
self.resize(700, 400)
|
||||
|
||||
def closeEvent(self, event):
|
||||
super(DetailsPopup, self).closeEvent(event)
|
||||
super().closeEvent(event)
|
||||
self.closed.emit()
|
||||
|
||||
|
||||
class PublishReportViewerWidget(QtWidgets.QFrame):
|
||||
def __init__(self, parent=None):
|
||||
super(PublishReportViewerWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
instances_model = InstancesModel()
|
||||
instances_proxy = InstanceProxyModel()
|
||||
|
|
|
|||
|
|
@ -302,7 +302,7 @@ class LoadedFilesModel(QtGui.QStandardItemModel):
|
|||
header_labels = ("Reports", "Created")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LoadedFilesModel, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Column count must be set before setting header data
|
||||
self.setColumnCount(len(self.header_labels))
|
||||
|
|
@ -350,7 +350,7 @@ class LoadedFilesModel(QtGui.QStandardItemModel):
|
|||
if col != 0:
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
|
||||
return super(LoadedFilesModel, self).data(index, role)
|
||||
return super().data(index, role)
|
||||
|
||||
def setData(self, index, value, role=None):
|
||||
if role is None:
|
||||
|
|
@ -364,13 +364,13 @@ class LoadedFilesModel(QtGui.QStandardItemModel):
|
|||
report_item.save()
|
||||
value = report_item.label
|
||||
|
||||
return super(LoadedFilesModel, self).setData(index, value, role)
|
||||
return super().setData(index, value, role)
|
||||
|
||||
def flags(self, index):
|
||||
# Allow editable flag only for first column
|
||||
if index.column() > 0:
|
||||
return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
|
||||
return super(LoadedFilesModel, self).flags(index)
|
||||
return super().flags(index)
|
||||
|
||||
def _create_item(self, report_item):
|
||||
if report_item.id in self._items_by_id:
|
||||
|
|
@ -451,7 +451,7 @@ class LoadedFilesView(QtWidgets.QTreeView):
|
|||
selection_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LoadedFilesView, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.setEditTriggers(
|
||||
QtWidgets.QAbstractItemView.EditKeyPressed
|
||||
| QtWidgets.QAbstractItemView.SelectedClicked
|
||||
|
|
@ -502,11 +502,11 @@ class LoadedFilesView(QtWidgets.QTreeView):
|
|||
self._update_remove_btn()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(LoadedFilesView, self).resizeEvent(event)
|
||||
super().resizeEvent(event)
|
||||
self._update_remove_btn()
|
||||
|
||||
def showEvent(self, event):
|
||||
super(LoadedFilesView, self).showEvent(event)
|
||||
super().showEvent(event)
|
||||
self._model.refresh()
|
||||
header = self.header()
|
||||
header.resizeSections(QtWidgets.QHeaderView.ResizeToContents)
|
||||
|
|
@ -548,7 +548,7 @@ class LoadedFilesWidget(QtWidgets.QWidget):
|
|||
report_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent):
|
||||
super(LoadedFilesWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
|
|
@ -598,7 +598,7 @@ class PublishReportViewerWindow(QtWidgets.QWidget):
|
|||
default_height = 600
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(PublishReportViewerWindow, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Publish report viewer")
|
||||
icon = QtGui.QIcon(get_ayon_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class _VLineWidget(QtWidgets.QWidget):
|
|||
It is expected that parent widget will set width.
|
||||
"""
|
||||
def __init__(self, color, line_size, left, parent):
|
||||
super(_VLineWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self._color = color
|
||||
self._left = left
|
||||
self._line_size = line_size
|
||||
|
|
@ -69,7 +69,7 @@ class _HBottomLineWidget(QtWidgets.QWidget):
|
|||
It is expected that parent widget will set height and radius.
|
||||
"""
|
||||
def __init__(self, color, line_size, parent):
|
||||
super(_HBottomLineWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self._color = color
|
||||
self._radius = 0
|
||||
self._line_size = line_size
|
||||
|
|
@ -128,7 +128,7 @@ class _HTopCornerLineWidget(QtWidgets.QWidget):
|
|||
"""
|
||||
|
||||
def __init__(self, color, line_size, left_side, parent):
|
||||
super(_HTopCornerLineWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self._left_side = left_side
|
||||
self._line_size = line_size
|
||||
self._color = color
|
||||
|
|
@ -192,7 +192,7 @@ class BorderedLabelWidget(QtWidgets.QFrame):
|
|||
└──────────────────────┘
|
||||
"""
|
||||
def __init__(self, label, parent):
|
||||
super(BorderedLabelWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
color_value = get_objected_colors("border")
|
||||
color = None
|
||||
if color_value:
|
||||
|
|
@ -269,7 +269,7 @@ class BorderedLabelWidget(QtWidgets.QFrame):
|
|||
self._recalculate_sizes()
|
||||
|
||||
def showEvent(self, event):
|
||||
super(BorderedLabelWidget, self).showEvent(event)
|
||||
super().showEvent(event)
|
||||
self._recalculate_sizes()
|
||||
|
||||
def _recalculate_sizes(self):
|
||||
|
|
|
|||
|
|
@ -29,18 +29,21 @@ from ayon_core.tools.utils import NiceCheckbox
|
|||
|
||||
from ayon_core.tools.utils import BaseClickableFrame
|
||||
from ayon_core.tools.utils.lib import html_escape
|
||||
|
||||
from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend
|
||||
from ayon_core.tools.publisher.constants import (
|
||||
CONTEXT_ID,
|
||||
CONTEXT_LABEL,
|
||||
CONTEXT_GROUP,
|
||||
CONVERTOR_ITEM_GROUP,
|
||||
)
|
||||
|
||||
from .widgets import (
|
||||
AbstractInstanceView,
|
||||
ContextWarningLabel,
|
||||
IconValuePixmapLabel,
|
||||
PublishPixmapLabel
|
||||
)
|
||||
from ..constants import (
|
||||
CONTEXT_ID,
|
||||
CONTEXT_LABEL,
|
||||
CONTEXT_GROUP,
|
||||
CONVERTOR_ITEM_GROUP,
|
||||
)
|
||||
|
||||
|
||||
class SelectionTypes:
|
||||
|
|
@ -55,7 +58,7 @@ class BaseGroupWidget(QtWidgets.QWidget):
|
|||
double_clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, group_name, parent):
|
||||
super(BaseGroupWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
label_widget = QtWidgets.QLabel(group_name, self)
|
||||
|
||||
|
|
@ -207,7 +210,7 @@ class InstanceGroupWidget(BaseGroupWidget):
|
|||
active_changed = QtCore.Signal(str, str, bool)
|
||||
|
||||
def __init__(self, group_icons, *args, **kwargs):
|
||||
super(InstanceGroupWidget, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._group_icons = group_icons
|
||||
|
||||
|
|
@ -277,14 +280,14 @@ class CardWidget(BaseClickableFrame):
|
|||
double_clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent):
|
||||
super(CardWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self.setObjectName("CardViewWidget")
|
||||
|
||||
self._selected = False
|
||||
self._id = None
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
super(CardWidget, self).mouseDoubleClickEvent(event)
|
||||
super().mouseDoubleClickEvent(event)
|
||||
if self._is_valid_double_click(event):
|
||||
self.double_clicked.emit()
|
||||
|
||||
|
|
@ -332,7 +335,7 @@ class ContextCardWidget(CardWidget):
|
|||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
super(ContextCardWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self._id = CONTEXT_ID
|
||||
self._group_identifier = CONTEXT_GROUP
|
||||
|
|
@ -362,7 +365,7 @@ class ConvertorItemCardWidget(CardWidget):
|
|||
"""
|
||||
|
||||
def __init__(self, item, parent):
|
||||
super(ConvertorItemCardWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self._id = item.id
|
||||
self.identifier = item.identifier
|
||||
|
|
@ -395,7 +398,7 @@ class InstanceCardWidget(CardWidget):
|
|||
active_changed = QtCore.Signal(str, bool)
|
||||
|
||||
def __init__(self, instance, group_icon, parent):
|
||||
super(InstanceCardWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self._id = instance.id
|
||||
self._group_identifier = instance.group_label
|
||||
|
|
@ -558,9 +561,9 @@ class InstanceCardView(AbstractInstanceView):
|
|||
double_clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(InstanceCardView, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self._controller = controller
|
||||
self._controller: AbstractPublisherFrontend = controller
|
||||
|
||||
scroll_area = QtWidgets.QScrollArea(self)
|
||||
scroll_area.setWidgetResizable(True)
|
||||
|
|
@ -610,7 +613,7 @@ class InstanceCardView(AbstractInstanceView):
|
|||
+ scroll_bar.sizeHint().width()
|
||||
)
|
||||
|
||||
result = super(InstanceCardView, self).sizeHint()
|
||||
result = super().sizeHint()
|
||||
result.setWidth(width)
|
||||
return result
|
||||
|
||||
|
|
@ -651,7 +654,7 @@ class InstanceCardView(AbstractInstanceView):
|
|||
self._toggle_instances(1)
|
||||
return True
|
||||
|
||||
return super(InstanceCardView, self).keyPressEvent(event)
|
||||
return super().keyPressEvent(event)
|
||||
|
||||
def _get_selected_widgets(self):
|
||||
output = []
|
||||
|
|
@ -694,7 +697,7 @@ class InstanceCardView(AbstractInstanceView):
|
|||
# Prepare instances by group and identifiers by group
|
||||
instances_by_group = collections.defaultdict(list)
|
||||
identifiers_by_group = collections.defaultdict(set)
|
||||
for instance in self._controller.instances.values():
|
||||
for instance in self._controller.get_instances():
|
||||
group_name = instance.group_label
|
||||
instances_by_group[group_name].append(instance)
|
||||
identifiers_by_group[group_name].add(
|
||||
|
|
@ -787,7 +790,7 @@ class InstanceCardView(AbstractInstanceView):
|
|||
self._content_layout.insertWidget(0, widget)
|
||||
|
||||
def _update_convertor_items_group(self):
|
||||
convertor_items = self._controller.convertor_items
|
||||
convertor_items = self._controller.get_convertor_items()
|
||||
if not convertor_items and self._convertor_items_group is None:
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from ayon_core.tools.utils import PlaceholderLineEdit, GoToCurrentButton
|
|||
|
||||
from ayon_core.tools.common_models import HierarchyExpectedSelection
|
||||
from ayon_core.tools.utils import FoldersWidget, TasksWidget
|
||||
from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend
|
||||
|
||||
|
||||
class CreateSelectionModel(object):
|
||||
|
|
@ -18,8 +19,8 @@ class CreateSelectionModel(object):
|
|||
|
||||
event_source = "publisher.create.selection.model"
|
||||
|
||||
def __init__(self, controller):
|
||||
self._controller = controller
|
||||
def __init__(self, controller: "CreateHierarchyController"):
|
||||
self._controller: CreateHierarchyController = controller
|
||||
|
||||
self._project_name = None
|
||||
self._folder_id = None
|
||||
|
|
@ -94,9 +95,9 @@ class CreateHierarchyController:
|
|||
controller (PublisherController): Publisher controller.
|
||||
|
||||
"""
|
||||
def __init__(self, controller):
|
||||
def __init__(self, controller: AbstractPublisherFrontend):
|
||||
self._event_system = QueuedEventSystem()
|
||||
self._controller = controller
|
||||
self._controller: AbstractPublisherFrontend = controller
|
||||
self._selection_model = CreateSelectionModel(self)
|
||||
self._expected_selection = HierarchyExpectedSelection(
|
||||
self, handle_project=False
|
||||
|
|
@ -118,7 +119,7 @@ class CreateHierarchyController:
|
|||
self.event_system.add_callback(topic, callback)
|
||||
|
||||
def get_project_name(self):
|
||||
return self._controller.project_name
|
||||
return self._controller.get_current_project_name()
|
||||
|
||||
def get_folder_items(self, project_name, sender=None):
|
||||
return self._controller.get_folder_items(project_name, sender)
|
||||
|
|
@ -168,10 +169,10 @@ class CreateContextWidget(QtWidgets.QWidget):
|
|||
folder_changed = QtCore.Signal()
|
||||
task_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(CreateContextWidget, self).__init__(parent)
|
||||
def __init__(self, controller: AbstractPublisherFrontend, parent):
|
||||
super().__init__(parent)
|
||||
|
||||
self._controller = controller
|
||||
self._controller: AbstractPublisherFrontend = controller
|
||||
self._enabled = True
|
||||
self._last_project_name = None
|
||||
self._last_folder_id = None
|
||||
|
|
@ -234,12 +235,12 @@ class CreateContextWidget(QtWidgets.QWidget):
|
|||
|
||||
def update_current_context_btn(self):
|
||||
# Hide set current folder if there is no one
|
||||
folder_path = self._controller.current_folder_path
|
||||
folder_path = self._controller.get_current_folder_path()
|
||||
self._current_context_btn.setVisible(bool(folder_path))
|
||||
|
||||
def set_selected_context(self, folder_id, task_name):
|
||||
self._hierarchy_controller.set_expected_selection(
|
||||
self._controller.project_name,
|
||||
self._controller.get_current_project_name(),
|
||||
folder_id,
|
||||
task_name
|
||||
)
|
||||
|
|
@ -270,13 +271,13 @@ class CreateContextWidget(QtWidgets.QWidget):
|
|||
)
|
||||
|
||||
def refresh(self):
|
||||
self._last_project_name = self._controller.project_name
|
||||
self._last_project_name = self._controller.get_current_project_name()
|
||||
folder_id = self._last_folder_id
|
||||
task_name = self._last_selected_task_name
|
||||
if folder_id is None:
|
||||
folder_path = self._controller.current_folder_path
|
||||
folder_path = self._controller.get_current_folder_path()
|
||||
folder_id = self._controller.get_folder_id_from_path(folder_path)
|
||||
task_name = self._controller.current_task_name
|
||||
task_name = self._controller.get_current_task_name()
|
||||
self._hierarchy_controller.set_selected_project(
|
||||
self._last_project_name
|
||||
)
|
||||
|
|
@ -295,8 +296,8 @@ class CreateContextWidget(QtWidgets.QWidget):
|
|||
self.task_changed.emit()
|
||||
|
||||
def _on_current_context_click(self):
|
||||
folder_path = self._controller.current_folder_path
|
||||
task_name = self._controller.current_task_name
|
||||
folder_path = self._controller.get_current_folder_path()
|
||||
task_name = self._controller.get_current_task_name()
|
||||
folder_id = self._controller.get_folder_id_from_path(folder_path)
|
||||
self._hierarchy_controller.set_expected_selection(
|
||||
self._last_project_name, folder_id, task_name
|
||||
|
|
|
|||
|
|
@ -9,14 +9,8 @@ from ayon_core.pipeline.create import (
|
|||
TaskNotSetError,
|
||||
)
|
||||
|
||||
from .thumbnail_widget import ThumbnailWidget
|
||||
from .widgets import (
|
||||
IconValuePixmapLabel,
|
||||
CreateBtn,
|
||||
)
|
||||
from .create_context_widgets import CreateContextWidget
|
||||
from .precreate_widget import PreCreateWidget
|
||||
from ..constants import (
|
||||
from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend
|
||||
from ayon_core.tools.publisher.constants import (
|
||||
VARIANT_TOOLTIP,
|
||||
PRODUCT_TYPE_ROLE,
|
||||
CREATOR_IDENTIFIER_ROLE,
|
||||
|
|
@ -26,6 +20,14 @@ from ..constants import (
|
|||
INPUTS_LAYOUT_VSPACING,
|
||||
)
|
||||
|
||||
from .thumbnail_widget import ThumbnailWidget
|
||||
from .widgets import (
|
||||
IconValuePixmapLabel,
|
||||
CreateBtn,
|
||||
)
|
||||
from .create_context_widgets import CreateContextWidget
|
||||
from .precreate_widget import PreCreateWidget
|
||||
|
||||
SEPARATORS = ("---separator---", "---")
|
||||
|
||||
|
||||
|
|
@ -33,14 +35,14 @@ class ResizeControlWidget(QtWidgets.QWidget):
|
|||
resized = QtCore.Signal()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(ResizeControlWidget, self).resizeEvent(event)
|
||||
super().resizeEvent(event)
|
||||
self.resized.emit()
|
||||
|
||||
|
||||
# TODO add creator identifier/label to details
|
||||
class CreatorShortDescWidget(QtWidgets.QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super(CreatorShortDescWidget, self).__init__(parent=parent)
|
||||
super().__init__(parent=parent)
|
||||
|
||||
# --- Short description widget ---
|
||||
icon_widget = IconValuePixmapLabel(None, self)
|
||||
|
|
@ -98,15 +100,15 @@ class CreatorsProxyModel(QtCore.QSortFilterProxyModel):
|
|||
l_show_order = left.data(CREATOR_SORT_ROLE)
|
||||
r_show_order = right.data(CREATOR_SORT_ROLE)
|
||||
if l_show_order == r_show_order:
|
||||
return super(CreatorsProxyModel, self).lessThan(left, right)
|
||||
return super().lessThan(left, right)
|
||||
return l_show_order < r_show_order
|
||||
|
||||
|
||||
class CreateWidget(QtWidgets.QWidget):
|
||||
def __init__(self, controller, parent=None):
|
||||
super(CreateWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self._controller = controller
|
||||
self._controller: AbstractPublisherFrontend = controller
|
||||
|
||||
self._folder_path = None
|
||||
self._product_names = None
|
||||
|
|
@ -274,11 +276,11 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create)
|
||||
thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear)
|
||||
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"main.window.closed", self._on_main_window_close
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"plugins.refresh.finished", self._on_plugins_refresh
|
||||
controller.register_event_callback(
|
||||
"controller.reset.finished", self._on_controler_reset
|
||||
)
|
||||
|
||||
self._main_splitter_widget = main_splitter_widget
|
||||
|
|
@ -313,13 +315,11 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
self._last_current_context_task = None
|
||||
self._use_current_context = True
|
||||
|
||||
@property
|
||||
def current_folder_path(self):
|
||||
return self._controller.current_folder_path
|
||||
def get_current_folder_path(self):
|
||||
return self._controller.get_current_folder_path()
|
||||
|
||||
@property
|
||||
def current_task_name(self):
|
||||
return self._controller.current_task_name
|
||||
def get_current_task_name(self):
|
||||
return self._controller.get_current_task_name()
|
||||
|
||||
def _context_change_is_enabled(self):
|
||||
return self._context_widget.is_enabled()
|
||||
|
|
@ -330,7 +330,7 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
folder_path = self._context_widget.get_selected_folder_path()
|
||||
|
||||
if folder_path is None:
|
||||
folder_path = self.current_folder_path
|
||||
folder_path = self.get_current_folder_path()
|
||||
return folder_path or None
|
||||
|
||||
def _get_folder_id(self):
|
||||
|
|
@ -348,7 +348,7 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
task_name = self._context_widget.get_selected_task_name()
|
||||
|
||||
if not task_name:
|
||||
task_name = self.current_task_name
|
||||
task_name = self.get_current_task_name()
|
||||
return task_name
|
||||
|
||||
def _set_context_enabled(self, enabled):
|
||||
|
|
@ -364,8 +364,8 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
self._use_current_context = True
|
||||
|
||||
def refresh(self):
|
||||
current_folder_path = self._controller.current_folder_path
|
||||
current_task_name = self._controller.current_task_name
|
||||
current_folder_path = self._controller.get_current_folder_path()
|
||||
current_task_name = self._controller.get_current_task_name()
|
||||
|
||||
# Get context before refresh to keep selection of folder and
|
||||
# task widgets
|
||||
|
|
@ -481,7 +481,7 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
|
||||
# Add new create plugins
|
||||
new_creators = set()
|
||||
creator_items_by_identifier = self._controller.creator_items
|
||||
creator_items_by_identifier = self._controller.get_creator_items()
|
||||
for identifier, creator_item in creator_items_by_identifier.items():
|
||||
if creator_item.creator_type != "artist":
|
||||
continue
|
||||
|
|
@ -531,7 +531,7 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
|
||||
self._set_creator(create_item)
|
||||
|
||||
def _on_plugins_refresh(self):
|
||||
def _on_controler_reset(self):
|
||||
# Trigger refresh only if is visible
|
||||
self.refresh()
|
||||
|
||||
|
|
@ -562,7 +562,7 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
description = ""
|
||||
if creator_item is not None:
|
||||
description = creator_item.detailed_description or description
|
||||
self._controller.event_system.emit(
|
||||
self._controller.emit_event(
|
||||
"show.detailed.help",
|
||||
{
|
||||
"message": description
|
||||
|
|
@ -571,7 +571,7 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
)
|
||||
|
||||
def _set_creator_by_identifier(self, identifier):
|
||||
creator_item = self._controller.creator_items.get(identifier)
|
||||
creator_item = self._controller.get_creator_item_by_id(identifier)
|
||||
self._set_creator(creator_item)
|
||||
|
||||
def _set_creator(self, creator_item):
|
||||
|
|
@ -755,7 +755,7 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
self._creators_splitter.setSizes([part, rem_width])
|
||||
|
||||
def showEvent(self, event):
|
||||
super(CreateWidget, self).showEvent(event)
|
||||
super().showEvent(event)
|
||||
if self._first_show:
|
||||
self._first_show = False
|
||||
self._on_first_show()
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ from qtpy import QtWidgets
|
|||
|
||||
from ayon_core.lib.events import QueuedEventSystem
|
||||
from ayon_core.tools.utils import PlaceholderLineEdit, FoldersWidget
|
||||
from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend
|
||||
|
||||
|
||||
class FoldersDialogController:
|
||||
def __init__(self, controller):
|
||||
def __init__(self, controller: AbstractPublisherFrontend):
|
||||
self._event_system = QueuedEventSystem()
|
||||
self._controller = controller
|
||||
self._controller: AbstractPublisherFrontend = controller
|
||||
|
||||
@property
|
||||
def event_system(self):
|
||||
|
|
@ -39,7 +40,7 @@ class FoldersDialog(QtWidgets.QDialog):
|
|||
"""Dialog to select folder for a context of instance."""
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(FoldersDialog, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Select folder")
|
||||
|
||||
filter_input = PlaceholderLineEdit(self)
|
||||
|
|
@ -62,7 +63,7 @@ class FoldersDialog(QtWidgets.QDialog):
|
|||
layout.addWidget(folders_widget, 1)
|
||||
layout.addLayout(btns_layout, 0)
|
||||
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"controller.reset.finished", self._on_controller_reset
|
||||
)
|
||||
|
||||
|
|
@ -104,7 +105,7 @@ class FoldersDialog(QtWidgets.QDialog):
|
|||
|
||||
def showEvent(self, event):
|
||||
"""Refresh folders widget on show."""
|
||||
super(FoldersDialog, self).showEvent(event)
|
||||
super().showEvent(event)
|
||||
if self._first_show:
|
||||
self._first_show = False
|
||||
self._on_first_show()
|
||||
|
|
@ -119,7 +120,9 @@ class FoldersDialog(QtWidgets.QDialog):
|
|||
if self._soft_reset_enabled:
|
||||
self._soft_reset_enabled = False
|
||||
|
||||
self._folders_widget.set_project_name(self._controller.project_name)
|
||||
self._folders_widget.set_project_name(
|
||||
self._controller.get_current_project_name()
|
||||
)
|
||||
|
||||
def _on_filter_change(self, text):
|
||||
"""Trigger change of filter of folders."""
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ except Exception:
|
|||
|
||||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend
|
||||
|
||||
|
||||
class HelpButton(QtWidgets.QPushButton):
|
||||
"""Button used to trigger help dialog."""
|
||||
|
||||
def __init__(self, parent):
|
||||
super(HelpButton, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self.setObjectName("CreateDialogHelpButton")
|
||||
self.setText("?")
|
||||
|
||||
|
|
@ -19,7 +21,7 @@ class HelpWidget(QtWidgets.QWidget):
|
|||
"""Widget showing help for single functionality."""
|
||||
|
||||
def __init__(self, parent):
|
||||
super(HelpWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
# TODO add hints what to help with?
|
||||
detail_description_input = QtWidgets.QTextEdit(self)
|
||||
|
|
@ -54,8 +56,10 @@ class HelpDialog(QtWidgets.QDialog):
|
|||
default_width = 530
|
||||
default_height = 340
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(HelpDialog, self).__init__(parent)
|
||||
def __init__(
|
||||
self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setWindowTitle("Help dialog")
|
||||
|
||||
|
|
@ -64,11 +68,11 @@ class HelpDialog(QtWidgets.QDialog):
|
|||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.addWidget(help_content, 1)
|
||||
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"show.detailed.help", self._on_help_request
|
||||
)
|
||||
|
||||
self._controller = controller
|
||||
self._controller: AbstractPublisherFrontend = controller
|
||||
|
||||
self._help_content = help_content
|
||||
|
||||
|
|
@ -80,5 +84,5 @@ class HelpDialog(QtWidgets.QDialog):
|
|||
self._help_content.set_detailed_text(text)
|
||||
|
||||
def showEvent(self, event):
|
||||
super(HelpDialog, self).showEvent(event)
|
||||
super().showEvent(event)
|
||||
self.resize(self.default_width, self.default_height)
|
||||
|
|
|
|||
|
|
@ -29,8 +29,9 @@ from qtpy import QtWidgets, QtCore, QtGui
|
|||
from ayon_core.style import get_objected_colors
|
||||
from ayon_core.tools.utils import NiceCheckbox
|
||||
from ayon_core.tools.utils.lib import html_escape, checkstate_int_to_enum
|
||||
from .widgets import AbstractInstanceView
|
||||
from ..constants import (
|
||||
|
||||
from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend
|
||||
from ayon_core.tools.publisher.constants import (
|
||||
INSTANCE_ID_ROLE,
|
||||
SORT_VALUE_ROLE,
|
||||
IS_GROUP_ROLE,
|
||||
|
|
@ -41,6 +42,8 @@ from ..constants import (
|
|||
CONVERTOR_ITEM_GROUP,
|
||||
)
|
||||
|
||||
from .widgets import AbstractInstanceView
|
||||
|
||||
|
||||
class ListItemDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""Generic delegate for instance group.
|
||||
|
|
@ -55,7 +58,7 @@ class ListItemDelegate(QtWidgets.QStyledItemDelegate):
|
|||
radius_ratio = 0.3
|
||||
|
||||
def __init__(self, parent):
|
||||
super(ListItemDelegate, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
group_color_info = get_objected_colors("publisher", "list-view-group")
|
||||
|
||||
|
|
@ -68,7 +71,7 @@ class ListItemDelegate(QtWidgets.QStyledItemDelegate):
|
|||
if index.data(IS_GROUP_ROLE):
|
||||
self.group_item_paint(painter, option, index)
|
||||
else:
|
||||
super(ListItemDelegate, self).paint(painter, option, index)
|
||||
super().paint(painter, option, index)
|
||||
|
||||
def group_item_paint(self, painter, option, index):
|
||||
"""Paint group item."""
|
||||
|
|
@ -113,7 +116,7 @@ class InstanceListItemWidget(QtWidgets.QWidget):
|
|||
double_clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, instance, parent):
|
||||
super(InstanceListItemWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self.instance = instance
|
||||
|
||||
|
|
@ -152,7 +155,7 @@ class InstanceListItemWidget(QtWidgets.QWidget):
|
|||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
widget = self.childAt(event.pos())
|
||||
super(InstanceListItemWidget, self).mouseDoubleClickEvent(event)
|
||||
super().mouseDoubleClickEvent(event)
|
||||
if widget is not self._active_checkbox:
|
||||
self.double_clicked.emit()
|
||||
|
||||
|
|
@ -219,7 +222,7 @@ class ListContextWidget(QtWidgets.QFrame):
|
|||
double_clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent):
|
||||
super(ListContextWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
label_widget = QtWidgets.QLabel(CONTEXT_LABEL, self)
|
||||
|
||||
|
|
@ -235,7 +238,7 @@ class ListContextWidget(QtWidgets.QFrame):
|
|||
self.label_widget = label_widget
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
super(ListContextWidget, self).mouseDoubleClickEvent(event)
|
||||
super().mouseDoubleClickEvent(event)
|
||||
self.double_clicked.emit()
|
||||
|
||||
|
||||
|
|
@ -249,7 +252,7 @@ class InstanceListGroupWidget(QtWidgets.QFrame):
|
|||
toggle_requested = QtCore.Signal(str, int)
|
||||
|
||||
def __init__(self, group_name, parent):
|
||||
super(InstanceListGroupWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self.setObjectName("InstanceListGroupWidget")
|
||||
|
||||
self.group_name = group_name
|
||||
|
|
@ -333,7 +336,7 @@ class InstanceTreeView(QtWidgets.QTreeView):
|
|||
double_clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(InstanceTreeView, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setObjectName("InstanceListView")
|
||||
self.setHeaderHidden(True)
|
||||
|
|
@ -384,7 +387,7 @@ class InstanceTreeView(QtWidgets.QTreeView):
|
|||
self.toggle_requested.emit(1)
|
||||
return True
|
||||
|
||||
return super(InstanceTreeView, self).event(event)
|
||||
return super().event(event)
|
||||
|
||||
def _mouse_press(self, event):
|
||||
"""Store index of pressed group.
|
||||
|
|
@ -404,11 +407,11 @@ class InstanceTreeView(QtWidgets.QTreeView):
|
|||
|
||||
def mousePressEvent(self, event):
|
||||
self._mouse_press(event)
|
||||
super(InstanceTreeView, self).mousePressEvent(event)
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
self._mouse_press(event)
|
||||
super(InstanceTreeView, self).mouseDoubleClickEvent(event)
|
||||
super().mouseDoubleClickEvent(event)
|
||||
|
||||
def _mouse_release(self, event, pressed_index):
|
||||
if event.button() != QtCore.Qt.LeftButton:
|
||||
|
|
@ -431,7 +434,7 @@ class InstanceTreeView(QtWidgets.QTreeView):
|
|||
self._pressed_group_index = None
|
||||
result = self._mouse_release(event, pressed_index)
|
||||
if not result:
|
||||
super(InstanceTreeView, self).mouseReleaseEvent(event)
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
|
||||
class InstanceListView(AbstractInstanceView):
|
||||
|
|
@ -442,10 +445,12 @@ class InstanceListView(AbstractInstanceView):
|
|||
|
||||
double_clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(InstanceListView, self).__init__(parent)
|
||||
def __init__(
|
||||
self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
self._controller = controller
|
||||
self._controller: AbstractPublisherFrontend = controller
|
||||
|
||||
instance_view = InstanceTreeView(self)
|
||||
instance_delegate = ListItemDelegate(instance_view)
|
||||
|
|
@ -581,7 +586,7 @@ class InstanceListView(AbstractInstanceView):
|
|||
# Prepare instances by their groups
|
||||
instances_by_group_name = collections.defaultdict(list)
|
||||
group_names = set()
|
||||
for instance in self._controller.instances.values():
|
||||
for instance in self._controller.get_instances():
|
||||
group_label = instance.group_label
|
||||
group_names.add(group_label)
|
||||
instances_by_group_name[group_label].append(instance)
|
||||
|
|
@ -745,7 +750,7 @@ class InstanceListView(AbstractInstanceView):
|
|||
|
||||
def _update_convertor_items_group(self):
|
||||
created_new_items = False
|
||||
convertor_items_by_id = self._controller.convertor_items
|
||||
convertor_items_by_id = self._controller.get_convertor_items()
|
||||
group_item = self._convertor_group_item
|
||||
if not convertor_items_by_id and group_item is None:
|
||||
return created_new_items
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
from .border_label_widget import BorderedLabelWidget
|
||||
from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend
|
||||
|
||||
from .border_label_widget import BorderedLabelWidget
|
||||
from .card_view_widgets import InstanceCardView
|
||||
from .list_view_widgets import InstanceListView
|
||||
from .widgets import (
|
||||
|
|
@ -23,11 +24,13 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
anim_end_value = 200
|
||||
anim_duration = 200
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(OverviewWidget, self).__init__(parent)
|
||||
def __init__(
|
||||
self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
self._refreshing_instances = False
|
||||
self._controller = controller
|
||||
self._controller: AbstractPublisherFrontend = controller
|
||||
|
||||
product_content_widget = QtWidgets.QWidget(self)
|
||||
|
||||
|
|
@ -139,16 +142,16 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
)
|
||||
|
||||
# --- Controller callbacks ---
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"publish.process.started", self._on_publish_start
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"controller.reset.started", self._on_controller_reset_start
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"publish.reset.finished", self._on_publish_reset
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"instances.refresh.finished", self._on_instances_refresh
|
||||
)
|
||||
|
||||
|
|
@ -291,7 +294,7 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
# Disable delete button if nothing is selected
|
||||
self._delete_btn.setEnabled(len(instance_ids) > 0)
|
||||
|
||||
instances_by_id = self._controller.instances
|
||||
instances_by_id = self._controller.get_instances_by_id(instance_ids)
|
||||
instances = [
|
||||
instances_by_id[instance_id]
|
||||
for instance_id in instance_ids
|
||||
|
|
@ -454,7 +457,9 @@ class OverviewWidget(QtWidgets.QFrame):
|
|||
|
||||
self._create_btn.setEnabled(True)
|
||||
self._product_attributes_wrap.setEnabled(True)
|
||||
self._product_content_widget.setEnabled(self._controller.host_is_valid)
|
||||
self._product_content_widget.setEnabled(
|
||||
self._controller.is_host_valid()
|
||||
)
|
||||
|
||||
def _on_instances_refresh(self):
|
||||
"""Controller refreshed instances."""
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from ..constants import INPUTS_LAYOUT_HSPACING, INPUTS_LAYOUT_VSPACING
|
|||
|
||||
class PreCreateWidget(QtWidgets.QWidget):
|
||||
def __init__(self, parent):
|
||||
super(PreCreateWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
# Precreate attribute defininitions of Creator
|
||||
scroll_area = QtWidgets.QScrollArea(self)
|
||||
|
|
@ -79,7 +79,7 @@ class PreCreateWidget(QtWidgets.QWidget):
|
|||
|
||||
class AttributesWidget(QtWidgets.QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super(AttributesWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
layout = QtWidgets.QGridLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend
|
||||
|
||||
from .widgets import (
|
||||
StopBtn,
|
||||
ResetBtn,
|
||||
|
|
@ -31,8 +33,13 @@ class PublishFrame(QtWidgets.QWidget):
|
|||
|
||||
details_page_requested = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, borders, parent):
|
||||
super(PublishFrame, self).__init__(parent)
|
||||
def __init__(
|
||||
self,
|
||||
controller: AbstractPublisherFrontend,
|
||||
borders: int,
|
||||
parent: QtWidgets.QWidget
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
# Bottom part of widget where process and callback buttons are showed
|
||||
# - QFrame used to be able set background using stylesheets easily
|
||||
|
|
@ -157,29 +164,29 @@ class PublishFrame(QtWidgets.QWidget):
|
|||
shrunk_anim.valueChanged.connect(self._on_shrunk_anim)
|
||||
shrunk_anim.finished.connect(self._on_shrunk_anim_finish)
|
||||
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"publish.reset.finished", self._on_publish_reset
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"publish.process.started", self._on_publish_start
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"publish.has_validated.changed", self._on_publish_validated_change
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"publish.process.stopped", self._on_publish_stop
|
||||
)
|
||||
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"publish.process.instance.changed", self._on_instance_change
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"publish.process.plugin.changed", self._on_plugin_change
|
||||
)
|
||||
|
||||
self._shrunk_anim = shrunk_anim
|
||||
|
||||
self._controller = controller
|
||||
self._controller: AbstractPublisherFrontend = controller
|
||||
|
||||
self._content_frame = content_frame
|
||||
self._content_layout = content_layout
|
||||
|
|
@ -208,7 +215,7 @@ class PublishFrame(QtWidgets.QWidget):
|
|||
self._last_plugin_label = None
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
super(PublishFrame, self).mouseReleaseEvent(event)
|
||||
super().mouseReleaseEvent(event)
|
||||
self._change_shrunk_state()
|
||||
|
||||
def _change_shrunk_state(self):
|
||||
|
|
@ -314,8 +321,12 @@ class PublishFrame(QtWidgets.QWidget):
|
|||
self._validate_btn.setEnabled(True)
|
||||
self._publish_btn.setEnabled(True)
|
||||
|
||||
self._progress_bar.setValue(self._controller.publish_progress)
|
||||
self._progress_bar.setMaximum(self._controller.publish_max_progress)
|
||||
self._progress_bar.setValue(
|
||||
self._controller.get_publish_progress()
|
||||
)
|
||||
self._progress_bar.setMaximum(
|
||||
self._controller.get_publish_max_progress()
|
||||
)
|
||||
|
||||
def _on_publish_start(self):
|
||||
if self._last_plugin_label:
|
||||
|
|
@ -351,12 +362,12 @@ class PublishFrame(QtWidgets.QWidget):
|
|||
"""Change plugin label when instance is going to be processed."""
|
||||
|
||||
self._last_plugin_label = event["plugin_label"]
|
||||
self._progress_bar.setValue(self._controller.publish_progress)
|
||||
self._progress_bar.setValue(self._controller.get_publish_progress())
|
||||
self._plugin_label.setText(event["plugin_label"])
|
||||
QtWidgets.QApplication.processEvents()
|
||||
|
||||
def _on_publish_stop(self):
|
||||
self._progress_bar.setValue(self._controller.publish_progress)
|
||||
self._progress_bar.setValue(self._controller.get_publish_progress())
|
||||
|
||||
self._reset_btn.setEnabled(True)
|
||||
self._stop_btn.setEnabled(False)
|
||||
|
|
@ -364,31 +375,21 @@ class PublishFrame(QtWidgets.QWidget):
|
|||
self._instance_label.setText("")
|
||||
self._plugin_label.setText("")
|
||||
|
||||
validate_enabled = not self._controller.publish_has_crashed
|
||||
publish_enabled = not self._controller.publish_has_crashed
|
||||
if validate_enabled:
|
||||
validate_enabled = not self._controller.publish_has_validated
|
||||
if publish_enabled:
|
||||
if (
|
||||
self._controller.publish_has_validated
|
||||
and self._controller.publish_has_validation_errors
|
||||
):
|
||||
publish_enabled = False
|
||||
|
||||
else:
|
||||
publish_enabled = not self._controller.publish_has_finished
|
||||
|
||||
publish_enabled = self._controller.publish_can_continue()
|
||||
validate_enabled = (
|
||||
publish_enabled and not self._controller.publish_has_validated()
|
||||
)
|
||||
self._validate_btn.setEnabled(validate_enabled)
|
||||
self._publish_btn.setEnabled(publish_enabled)
|
||||
|
||||
if self._controller.publish_has_crashed:
|
||||
if self._controller.publish_has_crashed():
|
||||
self._set_error_msg()
|
||||
|
||||
elif self._controller.publish_has_validation_errors:
|
||||
elif self._controller.publish_has_validation_errors():
|
||||
self._set_progress_visibility(False)
|
||||
self._set_validation_errors()
|
||||
|
||||
elif self._controller.publish_has_finished:
|
||||
elif self._controller.publish_has_finished():
|
||||
self._set_finished()
|
||||
|
||||
else:
|
||||
|
|
@ -411,7 +412,9 @@ class PublishFrame(QtWidgets.QWidget):
|
|||
|
||||
self._set_main_label("Error happened")
|
||||
|
||||
self._message_label_top.setText(self._controller.publish_error_msg)
|
||||
self._message_label_top.setText(
|
||||
self._controller.get_publish_error_msg()
|
||||
)
|
||||
|
||||
self._set_success_property(1)
|
||||
|
||||
|
|
@ -467,11 +470,11 @@ class PublishFrame(QtWidgets.QWidget):
|
|||
|
||||
def _on_report_triggered(self, identifier):
|
||||
if identifier == "export_report":
|
||||
self._controller.event_system.emit(
|
||||
self._controller.emit_event(
|
||||
"export_report.request", {}, "publish_frame")
|
||||
|
||||
elif identifier == "copy_report":
|
||||
self._controller.event_system.emit(
|
||||
self._controller.emit_event(
|
||||
"copy_report.request", {}, "publish_frame")
|
||||
|
||||
elif identifier == "go_to_report":
|
||||
|
|
|
|||
|
|
@ -19,16 +19,18 @@ from ayon_core.tools.utils import (
|
|||
paint_image_with_color,
|
||||
SeparatorWidget,
|
||||
)
|
||||
from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend
|
||||
from ayon_core.tools.publisher.constants import (
|
||||
INSTANCE_ID_ROLE,
|
||||
CONTEXT_ID,
|
||||
CONTEXT_LABEL,
|
||||
)
|
||||
|
||||
from .widgets import IconValuePixmapLabel
|
||||
from .icons import (
|
||||
get_pixmap,
|
||||
get_image,
|
||||
)
|
||||
from ..constants import (
|
||||
INSTANCE_ID_ROLE,
|
||||
CONTEXT_ID,
|
||||
CONTEXT_LABEL,
|
||||
)
|
||||
|
||||
LOG_DEBUG_VISIBLE = 1 << 0
|
||||
LOG_INFO_VISIBLE = 1 << 1
|
||||
|
|
@ -50,7 +52,7 @@ class VerticalScrollArea(QtWidgets.QScrollArea):
|
|||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(VerticalScrollArea, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
|
||||
|
|
@ -80,7 +82,7 @@ class VerticalScrollArea(QtWidgets.QScrollArea):
|
|||
if old_widget:
|
||||
old_widget.removeEventFilter(self)
|
||||
|
||||
super(VerticalScrollArea, self).setVerticalScrollBar(widget)
|
||||
super().setVerticalScrollBar(widget)
|
||||
if widget:
|
||||
widget.installEventFilter(self)
|
||||
|
||||
|
|
@ -89,7 +91,7 @@ class VerticalScrollArea(QtWidgets.QScrollArea):
|
|||
if old_widget:
|
||||
old_widget.removeEventFilter(self)
|
||||
|
||||
super(VerticalScrollArea, self).setWidget(widget)
|
||||
super().setWidget(widget)
|
||||
if widget:
|
||||
widget.installEventFilter(self)
|
||||
|
||||
|
|
@ -105,7 +107,7 @@ class VerticalScrollArea(QtWidgets.QScrollArea):
|
|||
and (obj is self.widget() or obj is self.verticalScrollBar())
|
||||
):
|
||||
self._size_changed_timer.start()
|
||||
return super(VerticalScrollArea, self).eventFilter(obj, event)
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
|
||||
# --- Publish actions widget ---
|
||||
|
|
@ -122,7 +124,7 @@ class ActionButton(BaseClickableFrame):
|
|||
action_clicked = QtCore.Signal(str, str)
|
||||
|
||||
def __init__(self, plugin_action_item, parent):
|
||||
super(ActionButton, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self.setObjectName("ValidationActionButton")
|
||||
|
||||
|
|
@ -159,8 +161,10 @@ class ValidateActionsWidget(QtWidgets.QFrame):
|
|||
Change actions based on selected validation error.
|
||||
"""
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(ValidateActionsWidget, self).__init__(parent)
|
||||
def __init__(
|
||||
self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
|
|
@ -172,7 +176,7 @@ class ValidateActionsWidget(QtWidgets.QFrame):
|
|||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(content_widget)
|
||||
|
||||
self._controller = controller
|
||||
self._controller: AbstractPublisherFrontend = controller
|
||||
self._content_widget = content_widget
|
||||
self._content_layout = content_layout
|
||||
|
||||
|
|
@ -246,7 +250,7 @@ class ValidationErrorInstanceList(QtWidgets.QListView):
|
|||
Instances are collected per plugin's validation error title.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ValidationErrorInstanceList, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setObjectName("ValidationErrorInstanceList")
|
||||
|
||||
|
|
@ -257,7 +261,7 @@ class ValidationErrorInstanceList(QtWidgets.QListView):
|
|||
return self.sizeHint()
|
||||
|
||||
def sizeHint(self):
|
||||
result = super(ValidationErrorInstanceList, self).sizeHint()
|
||||
result = super().sizeHint()
|
||||
row_count = self.model().rowCount()
|
||||
height = 0
|
||||
if row_count > 0:
|
||||
|
|
@ -280,7 +284,7 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
|
|||
instance_changed = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, title_id, error_info, parent):
|
||||
super(ValidationErrorTitleWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self._title_id = title_id
|
||||
self._error_info = error_info
|
||||
|
|
@ -371,7 +375,7 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
|
|||
self._expanded = False
|
||||
|
||||
def sizeHint(self):
|
||||
result = super(ValidationErrorTitleWidget, self).sizeHint()
|
||||
result = super().sizeHint()
|
||||
expected_width = max(
|
||||
self._view_widget.minimumSizeHint().width(),
|
||||
self._view_widget.sizeHint().width()
|
||||
|
|
@ -475,7 +479,7 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
|
|||
|
||||
class ValidationArtistMessage(QtWidgets.QWidget):
|
||||
def __init__(self, message, parent):
|
||||
super(ValidationArtistMessage, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
artist_msg_label = QtWidgets.QLabel(message, self)
|
||||
artist_msg_label.setAlignment(QtCore.Qt.AlignCenter)
|
||||
|
|
@ -491,7 +495,7 @@ class ValidationErrorsView(QtWidgets.QWidget):
|
|||
selection_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent):
|
||||
super(ValidationErrorsView, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
errors_scroll = VerticalScrollArea(self)
|
||||
errors_scroll.setWidgetResizable(True)
|
||||
|
|
@ -715,7 +719,7 @@ class _InstanceItem:
|
|||
|
||||
class FamilyGroupLabel(QtWidgets.QWidget):
|
||||
def __init__(self, family, parent):
|
||||
super(FamilyGroupLabel, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self.setLayoutDirection(QtCore.Qt.LeftToRight)
|
||||
|
||||
|
|
@ -742,8 +746,8 @@ class PublishInstanceCardWidget(BaseClickableFrame):
|
|||
_success_pix = None
|
||||
_in_progress_pix = None
|
||||
|
||||
def __init__(self, instance, icon, publish_finished, parent):
|
||||
super(PublishInstanceCardWidget, self).__init__(parent)
|
||||
def __init__(self, instance, icon, publish_can_continue, parent):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setObjectName("CardViewWidget")
|
||||
|
||||
|
|
@ -756,10 +760,10 @@ class PublishInstanceCardWidget(BaseClickableFrame):
|
|||
state_pix = self.get_error_pix()
|
||||
elif instance.warned:
|
||||
state_pix = self.get_warning_pix()
|
||||
elif publish_finished:
|
||||
state_pix = self.get_success_pix()
|
||||
else:
|
||||
elif publish_can_continue:
|
||||
state_pix = self.get_in_progress_pix()
|
||||
else:
|
||||
state_pix = self.get_success_pix()
|
||||
|
||||
state_label = IconValuePixmapLabel(state_pix, self)
|
||||
|
||||
|
|
@ -874,8 +878,10 @@ class PublishInstancesViewWidget(QtWidgets.QWidget):
|
|||
_min_width_measure_string = 24 * "O"
|
||||
selection_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(PublishInstancesViewWidget, self).__init__(parent)
|
||||
def __init__(
|
||||
self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
scroll_area = VerticalScrollArea(self)
|
||||
scroll_area.setWidgetResizable(True)
|
||||
|
|
@ -898,7 +904,7 @@ class PublishInstancesViewWidget(QtWidgets.QWidget):
|
|||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(scroll_area, 1)
|
||||
|
||||
self._controller = controller
|
||||
self._controller: AbstractPublisherFrontend = controller
|
||||
self._scroll_area = scroll_area
|
||||
self._instance_view = instance_view
|
||||
self._instance_layout = instance_layout
|
||||
|
|
@ -927,7 +933,7 @@ class PublishInstancesViewWidget(QtWidgets.QWidget):
|
|||
+ scroll_bar.sizeHint().width()
|
||||
)
|
||||
|
||||
result = super(PublishInstancesViewWidget, self).sizeHint()
|
||||
result = super().sizeHint()
|
||||
result.setWidth(width)
|
||||
return result
|
||||
|
||||
|
|
@ -970,11 +976,7 @@ class PublishInstancesViewWidget(QtWidgets.QWidget):
|
|||
widgets = []
|
||||
group_widgets = []
|
||||
|
||||
publish_finished = (
|
||||
self._controller.publish_has_crashed
|
||||
or self._controller.publish_has_validation_errors
|
||||
or self._controller.publish_has_finished
|
||||
)
|
||||
publish_can_continue = self._controller.publish_can_continue()
|
||||
instances_by_family = collections.defaultdict(list)
|
||||
for instance_item in instance_items:
|
||||
if not instance_item.exists:
|
||||
|
|
@ -996,7 +998,10 @@ class PublishInstancesViewWidget(QtWidgets.QWidget):
|
|||
icon = identifier_icons[instance_item.creator_identifier]
|
||||
|
||||
widget = PublishInstanceCardWidget(
|
||||
instance_item, icon, publish_finished, self._instance_view
|
||||
instance_item,
|
||||
icon,
|
||||
publish_can_continue,
|
||||
self._instance_view
|
||||
)
|
||||
widget.selection_requested.connect(self._on_selection_request)
|
||||
self._instance_layout.addWidget(widget, 0)
|
||||
|
|
@ -1040,7 +1045,7 @@ class LogIconFrame(QtWidgets.QFrame):
|
|||
_validation_error_pix = None
|
||||
|
||||
def __init__(self, parent, log_type, log_level, is_validation_error):
|
||||
super(LogIconFrame, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
|
|
@ -1108,7 +1113,7 @@ class LogItemWidget(QtWidgets.QWidget):
|
|||
}
|
||||
|
||||
def __init__(self, log, parent):
|
||||
super(LogItemWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
type_flag, level_n = self._get_log_info(log)
|
||||
icon_label = LogIconFrame(
|
||||
|
|
@ -1185,7 +1190,7 @@ class LogsWithIconsView(QtWidgets.QWidget):
|
|||
"""
|
||||
|
||||
def __init__(self, logs, parent):
|
||||
super(LogsWithIconsView, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
logs_layout = QtWidgets.QVBoxLayout(self)
|
||||
|
|
@ -1265,7 +1270,7 @@ class InstanceLogsWidget(QtWidgets.QWidget):
|
|||
"""
|
||||
|
||||
def __init__(self, instance, parent):
|
||||
super(InstanceLogsWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
|
|
@ -1296,7 +1301,7 @@ class InstancesLogsView(QtWidgets.QFrame):
|
|||
"""Publish instances logs view widget."""
|
||||
|
||||
def __init__(self, parent):
|
||||
super(InstancesLogsView, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self.setObjectName("InstancesLogsView")
|
||||
|
||||
scroll_area = QtWidgets.QScrollArea(self)
|
||||
|
|
@ -1349,16 +1354,16 @@ class InstancesLogsView(QtWidgets.QFrame):
|
|||
self._plugin_ids_filter = None
|
||||
|
||||
def showEvent(self, event):
|
||||
super(InstancesLogsView, self).showEvent(event)
|
||||
super().showEvent(event)
|
||||
self._is_showed = True
|
||||
self._update_instances()
|
||||
|
||||
def hideEvent(self, event):
|
||||
super(InstancesLogsView, self).hideEvent(event)
|
||||
super().hideEvent(event)
|
||||
self._is_showed = False
|
||||
|
||||
def closeEvent(self, event):
|
||||
super(InstancesLogsView, self).closeEvent(event)
|
||||
super().closeEvent(event)
|
||||
self._is_showed = False
|
||||
|
||||
def _update_instances(self):
|
||||
|
|
@ -1456,8 +1461,10 @@ class CrashWidget(QtWidgets.QWidget):
|
|||
actions.
|
||||
"""
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(CrashWidget, self).__init__(parent)
|
||||
def __init__(
|
||||
self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
main_label = QtWidgets.QLabel("This is not your fault", self)
|
||||
main_label.setAlignment(QtCore.Qt.AlignCenter)
|
||||
|
|
@ -1499,20 +1506,20 @@ class CrashWidget(QtWidgets.QWidget):
|
|||
copy_clipboard_btn.clicked.connect(self._on_copy_to_clipboard)
|
||||
save_to_disk_btn.clicked.connect(self._on_save_to_disk_click)
|
||||
|
||||
self._controller = controller
|
||||
self._controller: AbstractPublisherFrontend = controller
|
||||
|
||||
def _on_copy_to_clipboard(self):
|
||||
self._controller.event_system.emit(
|
||||
self._controller.emit_event(
|
||||
"copy_report.request", {}, "report_page")
|
||||
|
||||
def _on_save_to_disk_click(self):
|
||||
self._controller.event_system.emit(
|
||||
self._controller.emit_event(
|
||||
"export_report.request", {}, "report_page")
|
||||
|
||||
|
||||
class ErrorDetailsWidget(QtWidgets.QWidget):
|
||||
def __init__(self, parent):
|
||||
super(ErrorDetailsWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
inputs_widget = QtWidgets.QWidget(self)
|
||||
# Error 'Description' input
|
||||
|
|
@ -1624,8 +1631,10 @@ class ReportsWidget(QtWidgets.QWidget):
|
|||
└──────┴─────────┴─────────┘
|
||||
"""
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(ReportsWidget, self).__init__(parent)
|
||||
def __init__(
|
||||
self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
# Instances view
|
||||
views_widget = QtWidgets.QWidget(self)
|
||||
|
|
@ -1709,7 +1718,7 @@ class ReportsWidget(QtWidgets.QWidget):
|
|||
self._detail_input_scroll = detail_input_scroll
|
||||
self._crash_widget = crash_widget
|
||||
|
||||
self._controller = controller
|
||||
self._controller: AbstractPublisherFrontend = controller
|
||||
|
||||
self._validation_errors_by_id = {}
|
||||
|
||||
|
|
@ -1744,8 +1753,8 @@ class ReportsWidget(QtWidgets.QWidget):
|
|||
view = self._instances_view
|
||||
validation_error_mode = False
|
||||
if (
|
||||
not self._controller.publish_has_crashed
|
||||
and self._controller.publish_has_validation_errors
|
||||
not self._controller.publish_has_crashed()
|
||||
and self._controller.publish_has_validation_errors()
|
||||
):
|
||||
view = self._validation_error_view
|
||||
validation_error_mode = True
|
||||
|
|
@ -1755,8 +1764,9 @@ class ReportsWidget(QtWidgets.QWidget):
|
|||
self._detail_input_scroll.setVisible(validation_error_mode)
|
||||
self._views_layout.setCurrentWidget(view)
|
||||
|
||||
self._crash_widget.setVisible(self._controller.publish_has_crashed)
|
||||
self._logs_view.setVisible(not self._controller.publish_has_crashed)
|
||||
is_crashed = self._controller.publish_has_crashed()
|
||||
self._crash_widget.setVisible(is_crashed)
|
||||
self._logs_view.setVisible(not is_crashed)
|
||||
|
||||
# Instance view & logs update
|
||||
instance_items = self._get_instance_items()
|
||||
|
|
@ -1818,8 +1828,10 @@ class ReportPageWidget(QtWidgets.QFrame):
|
|||
and validation error detail with possible actions (repair).
|
||||
"""
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(ReportPageWidget, self).__init__(parent)
|
||||
def __init__(
|
||||
self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
header_label = QtWidgets.QLabel(self)
|
||||
header_label.setAlignment(QtCore.Qt.AlignCenter)
|
||||
|
|
@ -1832,30 +1844,30 @@ class ReportPageWidget(QtWidgets.QFrame):
|
|||
layout.addWidget(header_label, 0)
|
||||
layout.addWidget(publish_instances_widget, 0)
|
||||
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"publish.process.started", self._on_publish_start
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"publish.reset.finished", self._on_publish_reset
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"publish.process.stopped", self._on_publish_stop
|
||||
)
|
||||
|
||||
self._header_label = header_label
|
||||
self._publish_instances_widget = publish_instances_widget
|
||||
|
||||
self._controller = controller
|
||||
self._controller: AbstractPublisherFrontend = controller
|
||||
|
||||
def _update_label(self):
|
||||
if not self._controller.publish_has_started:
|
||||
if not self._controller.publish_has_started():
|
||||
# This probably never happen when this widget is visible
|
||||
header_label = "Nothing to report until you run publish"
|
||||
elif self._controller.publish_has_crashed:
|
||||
elif self._controller.publish_has_crashed():
|
||||
header_label = "Publish error report"
|
||||
elif self._controller.publish_has_validation_errors:
|
||||
elif self._controller.publish_has_validation_errors():
|
||||
header_label = "Publish validation report"
|
||||
elif self._controller.publish_has_finished:
|
||||
elif self._controller.publish_has_finished():
|
||||
header_label = "Publish success report"
|
||||
else:
|
||||
header_label = "Publish report"
|
||||
|
|
@ -1863,7 +1875,7 @@ class ReportPageWidget(QtWidgets.QFrame):
|
|||
|
||||
def _update_state(self):
|
||||
self._update_label()
|
||||
publish_started = self._controller.publish_has_started
|
||||
publish_started = self._controller.publish_has_started()
|
||||
self._publish_instances_widget.setVisible(publish_started)
|
||||
if publish_started:
|
||||
self._publish_instances_widget.update_data()
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class ScreenMarquee(QtWidgets.QDialog):
|
|||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ScreenMarquee, self).__init__(parent=parent)
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.Window
|
||||
|
|
@ -138,7 +138,7 @@ class ScreenMarquee(QtWidgets.QDialog):
|
|||
event.accept()
|
||||
self.close()
|
||||
return
|
||||
return super(ScreenMarquee, self).keyPressEvent(event)
|
||||
return super().keyPressEvent(event)
|
||||
|
||||
def showEvent(self, event):
|
||||
self._fit_screen_geometry()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ class PublisherTabBtn(QtWidgets.QPushButton):
|
|||
tab_clicked = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, identifier, label, parent):
|
||||
super(PublisherTabBtn, self).__init__(label, parent)
|
||||
super().__init__(label, parent)
|
||||
self._identifier = identifier
|
||||
self._active = False
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ class PublisherTabsWidget(QtWidgets.QFrame):
|
|||
tab_changed = QtCore.Signal(str, str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(PublisherTabsWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
btns_widget = QtWidgets.QWidget(self)
|
||||
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
from typing import Optional
|
||||
|
||||
from qtpy import QtCore, QtGui
|
||||
|
||||
from ayon_core.style import get_default_entity_icon_color
|
||||
from ayon_core.tools.utils import get_qt_icon
|
||||
from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend
|
||||
|
||||
TASK_NAME_ROLE = QtCore.Qt.UserRole + 1
|
||||
TASK_TYPE_ROLE = QtCore.Qt.UserRole + 2
|
||||
|
|
@ -19,14 +22,19 @@ class TasksModel(QtGui.QStandardItemModel):
|
|||
tasks with same names then model is empty too.
|
||||
|
||||
Args:
|
||||
controller (PublisherController): Controller which handles creation and
|
||||
controller (AbstractPublisherFrontend): Controller which handles creation and
|
||||
publishing.
|
||||
|
||||
"""
|
||||
def __init__(self, controller, allow_empty_task=False):
|
||||
super(TasksModel, self).__init__()
|
||||
def __init__(
|
||||
self,
|
||||
controller: AbstractPublisherFrontend,
|
||||
allow_empty_task: Optional[bool] = False
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self._allow_empty_task = allow_empty_task
|
||||
self._controller = controller
|
||||
self._controller: AbstractPublisherFrontend = controller
|
||||
self._items_by_name = {}
|
||||
self._folder_paths = []
|
||||
self._task_names_by_folder_path = {}
|
||||
|
|
@ -135,7 +143,7 @@ class TasksModel(QtGui.QStandardItemModel):
|
|||
task_type_items = {
|
||||
task_type_item.name: task_type_item
|
||||
for task_type_item in self._controller.get_task_type_items(
|
||||
self._controller.project_name
|
||||
self._controller.get_current_project_name()
|
||||
)
|
||||
}
|
||||
icon_name_by_task_name = {}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,10 @@ from ayon_core.tools.utils import (
|
|||
paint_image_with_color,
|
||||
PixmapButton,
|
||||
)
|
||||
from ayon_core.tools.publisher.control import CardMessageTypes
|
||||
from ayon_core.tools.publisher.abstract import (
|
||||
CardMessageTypes,
|
||||
AbstractPublisherFrontend,
|
||||
)
|
||||
|
||||
from .icons import get_image
|
||||
from .screenshot_widget import capture_to_file
|
||||
|
|
@ -34,7 +37,7 @@ class ThumbnailPainterWidget(QtWidgets.QWidget):
|
|||
checker_boxes_count = 20
|
||||
|
||||
def __init__(self, parent):
|
||||
super(ThumbnailPainterWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
border_color = get_objected_colors("bg-buttons").get_qcolor()
|
||||
thumbnail_bg_color = get_objected_colors("bg-view").get_qcolor()
|
||||
|
|
@ -299,10 +302,12 @@ class ThumbnailWidget(QtWidgets.QWidget):
|
|||
thumbnail_created = QtCore.Signal(str)
|
||||
thumbnail_cleared = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
def __init__(
|
||||
self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget
|
||||
):
|
||||
# Missing implementation for thumbnail
|
||||
# - widget kept to make a visial offset of global attr widget offset
|
||||
super(ThumbnailWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
thumbnail_painter = ThumbnailPainterWidget(self)
|
||||
|
|
@ -355,7 +360,7 @@ class ThumbnailWidget(QtWidgets.QWidget):
|
|||
paste_btn.clicked.connect(self._on_paste_from_clipboard)
|
||||
browse_btn.clicked.connect(self._on_browse_clicked)
|
||||
|
||||
self._controller = controller
|
||||
self._controller: AbstractPublisherFrontend = controller
|
||||
self._output_dir = controller.get_thumbnail_temp_dir_path()
|
||||
|
||||
self._review_extensions = set(IMAGE_EXTENSIONS) | set(VIDEO_EXTENSIONS)
|
||||
|
|
@ -570,12 +575,12 @@ class ThumbnailWidget(QtWidgets.QWidget):
|
|||
)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(ThumbnailWidget, self).resizeEvent(event)
|
||||
super().resizeEvent(event)
|
||||
self._adapt_to_size()
|
||||
self._update_buttons_position()
|
||||
|
||||
def showEvent(self, event):
|
||||
super(ThumbnailWidget, self).showEvent(event)
|
||||
super().showEvent(event)
|
||||
self._adapt_to_size()
|
||||
self._update_buttons_position()
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ from qtpy import QtWidgets, QtCore, QtGui
|
|||
import qtawesome
|
||||
|
||||
from ayon_core.lib.attribute_definitions import UnknownDef
|
||||
from ayon_core.style import get_objected_colors
|
||||
from ayon_core.pipeline.create import (
|
||||
PRODUCT_NAME_ALLOWED_SYMBOLS,
|
||||
TaskNotSetError,
|
||||
)
|
||||
from ayon_core.tools.attribute_defs import create_widget_for_attr_def
|
||||
from ayon_core.tools import resources
|
||||
from ayon_core.tools.flickcharm import FlickCharm
|
||||
|
|
@ -20,11 +25,14 @@ from ayon_core.tools.utils import (
|
|||
BaseClickableFrame,
|
||||
set_style_property,
|
||||
)
|
||||
from ayon_core.style import get_objected_colors
|
||||
from ayon_core.pipeline.create import (
|
||||
PRODUCT_NAME_ALLOWED_SYMBOLS,
|
||||
TaskNotSetError,
|
||||
from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend
|
||||
from ayon_core.tools.publisher.constants import (
|
||||
VARIANT_TOOLTIP,
|
||||
ResetKeySequence,
|
||||
INPUTS_LAYOUT_HSPACING,
|
||||
INPUTS_LAYOUT_VSPACING,
|
||||
)
|
||||
|
||||
from .thumbnail_widget import ThumbnailWidget
|
||||
from .folders_dialog import FoldersDialog
|
||||
from .tasks_model import TasksModel
|
||||
|
|
@ -33,13 +41,6 @@ from .icons import (
|
|||
get_icon_path
|
||||
)
|
||||
|
||||
from ..constants import (
|
||||
VARIANT_TOOLTIP,
|
||||
ResetKeySequence,
|
||||
INPUTS_LAYOUT_HSPACING,
|
||||
INPUTS_LAYOUT_VSPACING,
|
||||
)
|
||||
|
||||
FA_PREFIXES = ["", "fa.", "fa5.", "fa5b.", "fa5s.", "ei.", "mdi."]
|
||||
|
||||
|
||||
|
|
@ -94,7 +95,7 @@ class IconValuePixmapLabel(PublishPixmapLabel):
|
|||
def __init__(self, icon_def, parent):
|
||||
source_pixmap = self._parse_icon_def(icon_def)
|
||||
|
||||
super(IconValuePixmapLabel, self).__init__(source_pixmap, parent)
|
||||
super().__init__(source_pixmap, parent)
|
||||
|
||||
def set_icon_def(self, icon_def):
|
||||
"""Set icon by it's definition name.
|
||||
|
|
@ -122,7 +123,7 @@ class ContextWarningLabel(PublishPixmapLabel):
|
|||
def __init__(self, parent):
|
||||
pix = get_pixmap("warning")
|
||||
|
||||
super(ContextWarningLabel, self).__init__(pix, parent)
|
||||
super().__init__(pix, parent)
|
||||
|
||||
self.setToolTip(
|
||||
"Contain invalid context. Please check details."
|
||||
|
|
@ -145,7 +146,7 @@ class PublishIconBtn(IconButton):
|
|||
"""
|
||||
|
||||
def __init__(self, pixmap_path, *args, **kwargs):
|
||||
super(PublishIconBtn, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
colors = get_objected_colors()
|
||||
icon = self.generate_icon(
|
||||
|
|
@ -208,7 +209,7 @@ class CreateBtn(PublishIconBtn):
|
|||
|
||||
def __init__(self, parent=None):
|
||||
icon_path = get_icon_path("create")
|
||||
super(CreateBtn, self).__init__(icon_path, "Create", parent)
|
||||
super().__init__(icon_path, "Create", parent)
|
||||
self.setToolTip("Create new product/s")
|
||||
self.setLayoutDirection(QtCore.Qt.RightToLeft)
|
||||
|
||||
|
|
@ -217,7 +218,7 @@ class SaveBtn(PublishIconBtn):
|
|||
"""Save context and instances information."""
|
||||
def __init__(self, parent=None):
|
||||
icon_path = get_icon_path("save")
|
||||
super(SaveBtn, self).__init__(icon_path, parent)
|
||||
super().__init__(icon_path, parent)
|
||||
self.setToolTip(
|
||||
"Save changes ({})".format(
|
||||
QtGui.QKeySequence(QtGui.QKeySequence.Save).toString()
|
||||
|
|
@ -229,7 +230,7 @@ class ResetBtn(PublishIconBtn):
|
|||
"""Publish reset button."""
|
||||
def __init__(self, parent=None):
|
||||
icon_path = get_icon_path("refresh")
|
||||
super(ResetBtn, self).__init__(icon_path, parent)
|
||||
super().__init__(icon_path, parent)
|
||||
self.setToolTip(
|
||||
"Reset & discard changes ({})".format(ResetKeySequence.toString())
|
||||
)
|
||||
|
|
@ -239,7 +240,7 @@ class StopBtn(PublishIconBtn):
|
|||
"""Publish stop button."""
|
||||
def __init__(self, parent):
|
||||
icon_path = get_icon_path("stop")
|
||||
super(StopBtn, self).__init__(icon_path, parent)
|
||||
super().__init__(icon_path, parent)
|
||||
self.setToolTip("Stop/Pause publishing")
|
||||
|
||||
|
||||
|
|
@ -247,7 +248,7 @@ class ValidateBtn(PublishIconBtn):
|
|||
"""Publish validate button."""
|
||||
def __init__(self, parent=None):
|
||||
icon_path = get_icon_path("validate")
|
||||
super(ValidateBtn, self).__init__(icon_path, parent)
|
||||
super().__init__(icon_path, parent)
|
||||
self.setToolTip("Validate")
|
||||
|
||||
|
||||
|
|
@ -255,7 +256,7 @@ class PublishBtn(PublishIconBtn):
|
|||
"""Publish start publish button."""
|
||||
def __init__(self, parent=None):
|
||||
icon_path = get_icon_path("play")
|
||||
super(PublishBtn, self).__init__(icon_path, "Publish", parent)
|
||||
super().__init__(icon_path, "Publish", parent)
|
||||
self.setToolTip("Publish")
|
||||
|
||||
|
||||
|
|
@ -263,7 +264,7 @@ class CreateInstanceBtn(PublishIconBtn):
|
|||
"""Create add button."""
|
||||
def __init__(self, parent=None):
|
||||
icon_path = get_icon_path("add")
|
||||
super(CreateInstanceBtn, self).__init__(icon_path, parent)
|
||||
super().__init__(icon_path, parent)
|
||||
self.setToolTip("Create new instance")
|
||||
|
||||
|
||||
|
|
@ -274,7 +275,7 @@ class PublishReportBtn(PublishIconBtn):
|
|||
|
||||
def __init__(self, parent=None):
|
||||
icon_path = get_icon_path("view_report")
|
||||
super(PublishReportBtn, self).__init__(icon_path, parent)
|
||||
super().__init__(icon_path, parent)
|
||||
self.setToolTip("Copy report")
|
||||
self._actions = []
|
||||
|
||||
|
|
@ -287,7 +288,7 @@ class PublishReportBtn(PublishIconBtn):
|
|||
self.triggered.emit(identifier)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
super(PublishReportBtn, self).mouseReleaseEvent(event)
|
||||
super().mouseReleaseEvent(event)
|
||||
menu = QtWidgets.QMenu(self)
|
||||
actions = []
|
||||
for item in self._actions:
|
||||
|
|
@ -305,7 +306,7 @@ class RemoveInstanceBtn(PublishIconBtn):
|
|||
"""Create remove button."""
|
||||
def __init__(self, parent=None):
|
||||
icon_path = resources.get_icon_path("delete")
|
||||
super(RemoveInstanceBtn, self).__init__(icon_path, parent)
|
||||
super().__init__(icon_path, parent)
|
||||
self.setToolTip("Remove selected instances")
|
||||
|
||||
|
||||
|
|
@ -313,7 +314,7 @@ class ChangeViewBtn(PublishIconBtn):
|
|||
"""Create toggle view button."""
|
||||
def __init__(self, parent=None):
|
||||
icon_path = get_icon_path("change_view")
|
||||
super(ChangeViewBtn, self).__init__(icon_path, parent)
|
||||
super().__init__(icon_path, parent)
|
||||
self.setToolTip("Swap between views")
|
||||
|
||||
|
||||
|
|
@ -363,7 +364,9 @@ class AbstractInstanceView(QtWidgets.QWidget):
|
|||
"{} Method 'get_selected_items' is not implemented."
|
||||
).format(self.__class__.__name__))
|
||||
|
||||
def set_selected_items(self, instance_ids, context_selected):
|
||||
def set_selected_items(
|
||||
self, instance_ids, context_selected, convertor_identifiers
|
||||
):
|
||||
"""Change selection for instances and context.
|
||||
|
||||
Used to applying selection from one view to other.
|
||||
|
|
@ -371,8 +374,9 @@ class AbstractInstanceView(QtWidgets.QWidget):
|
|||
Args:
|
||||
instance_ids (List[str]): Selected instance ids.
|
||||
context_selected (bool): Context is selected.
|
||||
"""
|
||||
convertor_identifiers (List[str]): Selected convertor identifiers.
|
||||
|
||||
"""
|
||||
raise NotImplementedError((
|
||||
"{} Method 'set_selected_items' is not implemented."
|
||||
).format(self.__class__.__name__))
|
||||
|
|
@ -399,7 +403,7 @@ class ClickableLineEdit(QtWidgets.QLineEdit):
|
|||
clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ClickableLineEdit, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.setReadOnly(True)
|
||||
self._mouse_pressed = False
|
||||
|
||||
|
|
@ -429,8 +433,10 @@ class FoldersFields(BaseClickableFrame):
|
|||
"""
|
||||
value_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(FoldersFields, self).__init__(parent)
|
||||
def __init__(
|
||||
self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("FolderPathInputWidget")
|
||||
|
||||
# Don't use 'self' for parent!
|
||||
|
|
@ -465,7 +471,7 @@ class FoldersFields(BaseClickableFrame):
|
|||
icon_btn.clicked.connect(self._mouse_release_callback)
|
||||
dialog.finished.connect(self._on_dialog_finish)
|
||||
|
||||
self._controller = controller
|
||||
self._controller: AbstractPublisherFrontend = controller
|
||||
self._dialog = dialog
|
||||
self._name_input = name_input
|
||||
self._icon_btn = icon_btn
|
||||
|
|
@ -582,7 +588,7 @@ class FoldersFields(BaseClickableFrame):
|
|||
|
||||
class TasksComboboxProxy(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TasksComboboxProxy, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self._filter_empty = False
|
||||
|
||||
def set_filter_empty(self, filter_empty):
|
||||
|
|
@ -613,8 +619,10 @@ class TasksCombobox(QtWidgets.QComboBox):
|
|||
"""
|
||||
value_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(TasksCombobox, self).__init__(parent)
|
||||
def __init__(
|
||||
self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("TasksCombobox")
|
||||
|
||||
# Set empty delegate to propagate stylesheet to a combobox
|
||||
|
|
@ -892,7 +900,7 @@ class VariantInputWidget(PlaceholderLineEdit):
|
|||
value_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent):
|
||||
super(VariantInputWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
self.setObjectName("VariantInput")
|
||||
self.setToolTip(VARIANT_TOOLTIP)
|
||||
|
|
@ -1003,7 +1011,7 @@ class MultipleItemWidget(QtWidgets.QWidget):
|
|||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
super(MultipleItemWidget, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
model = QtGui.QStandardItemModel()
|
||||
|
||||
|
|
@ -1043,7 +1051,7 @@ class MultipleItemWidget(QtWidgets.QWidget):
|
|||
self.setMaximumHeight(height + (2 * self._view.spacing()))
|
||||
|
||||
def showEvent(self, event):
|
||||
super(MultipleItemWidget, self).showEvent(event)
|
||||
super().showEvent(event)
|
||||
tmp_item = None
|
||||
if not self._value:
|
||||
# Add temp item to be able calculate maximum height of widget
|
||||
|
|
@ -1055,7 +1063,7 @@ class MultipleItemWidget(QtWidgets.QWidget):
|
|||
self._model.clear()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(MultipleItemWidget, self).resizeEvent(event)
|
||||
super().resizeEvent(event)
|
||||
self._update_size()
|
||||
|
||||
def set_value(self, value=None):
|
||||
|
|
@ -1095,10 +1103,12 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
|
|||
multiselection_text = "< Multiselection >"
|
||||
unknown_value = "N/A"
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(GlobalAttrsWidget, self).__init__(parent)
|
||||
def __init__(
|
||||
self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
self._controller = controller
|
||||
self._controller: AbstractPublisherFrontend = controller
|
||||
self._current_instances = []
|
||||
|
||||
variant_input = VariantInputWidget(self)
|
||||
|
|
@ -1338,8 +1348,10 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
|||
widgets are merged into one (different label does not count).
|
||||
"""
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(CreatorAttrsWidget, self).__init__(parent)
|
||||
def __init__(
|
||||
self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
scroll_area = QtWidgets.QScrollArea(self)
|
||||
scroll_area.setWidgetResizable(True)
|
||||
|
|
@ -1351,7 +1363,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
|||
|
||||
self._main_layout = main_layout
|
||||
|
||||
self._controller = controller
|
||||
self._controller: AbstractPublisherFrontend = controller
|
||||
self._scroll_area = scroll_area
|
||||
|
||||
self._attr_def_id_to_instances = {}
|
||||
|
|
@ -1476,8 +1488,10 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
|||
does not count).
|
||||
"""
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(PublishPluginAttrsWidget, self).__init__(parent)
|
||||
def __init__(
|
||||
self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
scroll_area = QtWidgets.QScrollArea(self)
|
||||
scroll_area.setWidgetResizable(True)
|
||||
|
|
@ -1489,7 +1503,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
|||
|
||||
self._main_layout = main_layout
|
||||
|
||||
self._controller = controller
|
||||
self._controller: AbstractPublisherFrontend = controller
|
||||
self._scroll_area = scroll_area
|
||||
|
||||
self._attr_def_id_to_instances = {}
|
||||
|
|
@ -1635,8 +1649,10 @@ class ProductAttributesWidget(QtWidgets.QWidget):
|
|||
instance_context_changed = QtCore.Signal()
|
||||
convert_requested = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(ProductAttributesWidget, self).__init__(parent)
|
||||
def __init__(
|
||||
self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
# TOP PART
|
||||
top_widget = QtWidgets.QWidget(self)
|
||||
|
|
@ -1734,11 +1750,11 @@ class ProductAttributesWidget(QtWidgets.QWidget):
|
|||
thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create)
|
||||
thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear)
|
||||
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"instance.thumbnail.changed", self._on_thumbnail_changed
|
||||
)
|
||||
|
||||
self._controller = controller
|
||||
self._controller: AbstractPublisherFrontend = controller
|
||||
|
||||
self._convert_widget = convert_widget
|
||||
|
||||
|
|
@ -1877,7 +1893,7 @@ class CreateNextPageOverlay(QtWidgets.QWidget):
|
|||
clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent):
|
||||
super(CreateNextPageOverlay, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self.setCursor(QtCore.Qt.PointingHandCursor)
|
||||
self._arrow_color = (
|
||||
get_objected_colors("font").get_qcolor()
|
||||
|
|
@ -1967,7 +1983,7 @@ class CreateNextPageOverlay(QtWidgets.QWidget):
|
|||
def mousePressEvent(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self._mouse_pressed = True
|
||||
super(CreateNextPageOverlay, self).mousePressEvent(event)
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self._mouse_pressed:
|
||||
|
|
@ -1975,7 +1991,7 @@ class CreateNextPageOverlay(QtWidgets.QWidget):
|
|||
if self.rect().contains(event.pos()):
|
||||
self.clicked.emit()
|
||||
|
||||
super(CreateNextPageOverlay, self).mouseReleaseEvent(event)
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QtGui.QPainter()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import json
|
|||
import time
|
||||
import collections
|
||||
import copy
|
||||
from typing import Optional
|
||||
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
from ayon_core import (
|
||||
|
|
@ -19,7 +21,7 @@ from ayon_core.tools.utils.lib import center_window
|
|||
|
||||
from .constants import ResetKeySequence
|
||||
from .publish_report_viewer import PublishReportViewerWidget
|
||||
from .control import CardMessageTypes
|
||||
from .abstract import CardMessageTypes, AbstractPublisherFrontend
|
||||
from .control_qt import QtPublisherController
|
||||
from .widgets import (
|
||||
OverviewWidget,
|
||||
|
|
@ -48,8 +50,13 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
footer_border = 8
|
||||
publish_footer_spacer = 2
|
||||
|
||||
def __init__(self, parent=None, controller=None, reset_on_show=None):
|
||||
super(PublisherWindow, self).__init__(parent)
|
||||
def __init__(
|
||||
self,
|
||||
parent: Optional[QtWidgets.QWidget] = None,
|
||||
controller: Optional[AbstractPublisherFrontend] = None,
|
||||
reset_on_show: Optional[bool] = None
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setObjectName("PublishWindow")
|
||||
|
||||
|
|
@ -273,55 +280,55 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._on_create_overlay_button_click
|
||||
)
|
||||
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"instances.refresh.finished", self._on_instances_refresh
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"publish.reset.finished", self._on_publish_reset
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"controller.reset.finished", self._on_controller_reset
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"publish.process.started", self._on_publish_start
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"publish.has_validated.changed", self._on_publish_validated_change
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"publish.finished.changed", self._on_publish_finished_change
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"publish.process.stopped", self._on_publish_stop
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"show.card.message", self._on_overlay_message
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"instances.collection.failed", self._on_creator_error
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"instances.save.failed", self._on_creator_error
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"instances.remove.failed", self._on_creator_error
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"instances.create.failed", self._on_creator_error
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"convertors.convert.failed", self._on_convertor_error
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"convertors.find.failed", self._on_convertor_error
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"publish.action.failed", self._on_action_error
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"export_report.request", self._export_report
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
controller.register_event_callback(
|
||||
"copy_report.request", self._copy_report
|
||||
)
|
||||
|
||||
|
|
@ -362,7 +369,7 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
|
||||
self._overlay_object = overlay_object
|
||||
|
||||
self._controller = controller
|
||||
self._controller: AbstractPublisherFrontend = controller
|
||||
|
||||
self._first_show = True
|
||||
self._first_reset = True
|
||||
|
|
@ -386,7 +393,8 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._window_is_visible = False
|
||||
|
||||
@property
|
||||
def controller(self):
|
||||
def controller(self) -> AbstractPublisherFrontend:
|
||||
"""Kept for compatibility with traypublisher."""
|
||||
return self._controller
|
||||
|
||||
def show_and_publish(self, comment=None):
|
||||
|
|
@ -437,7 +445,7 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
|
||||
def showEvent(self, event):
|
||||
self._window_is_visible = True
|
||||
super(PublisherWindow, self).showEvent(event)
|
||||
super().showEvent(event)
|
||||
if self._first_show:
|
||||
self._first_show = False
|
||||
self._on_first_show()
|
||||
|
|
@ -445,7 +453,7 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._show_timer.start()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(PublisherWindow, self).resizeEvent(event)
|
||||
super().resizeEvent(event)
|
||||
self._update_publish_frame_rect()
|
||||
self._update_create_overlay_size()
|
||||
|
||||
|
|
@ -453,24 +461,24 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._window_is_visible = False
|
||||
self._uninstall_app_event_listener()
|
||||
# TODO capture changes and ask user if wants to save changes on close
|
||||
if not self._controller.host_context_has_changed:
|
||||
if not self._controller.host_context_has_changed():
|
||||
self._save_changes(False)
|
||||
self._comment_input.setText("") # clear comment
|
||||
self._reset_on_show = True
|
||||
self._controller.clear_thumbnail_temp_dir_path()
|
||||
# Trigger custom event that should be captured only in UI
|
||||
# - backend (controller) must not be dependent on this event topic!!!
|
||||
self._controller.event_system.emit("main.window.closed", {}, "window")
|
||||
super(PublisherWindow, self).closeEvent(event)
|
||||
self._controller.emit_event("main.window.closed", {}, "window")
|
||||
super().closeEvent(event)
|
||||
|
||||
def leaveEvent(self, event):
|
||||
super(PublisherWindow, self).leaveEvent(event)
|
||||
super().leaveEvent(event)
|
||||
self._update_create_overlay_visibility()
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
if event.type() == QtCore.QEvent.MouseMove:
|
||||
self._update_create_overlay_visibility(event.globalPos())
|
||||
return super(PublisherWindow, self).eventFilter(obj, event)
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
def _install_app_event_listener(self):
|
||||
if self._app_event_listener_installed:
|
||||
|
|
@ -520,12 +528,12 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
)
|
||||
|
||||
if reset_match_result == QtGui.QKeySequence.ExactMatch:
|
||||
if not self.controller.publish_is_running:
|
||||
if not self._controller.publish_is_running:
|
||||
self.reset()
|
||||
event.accept()
|
||||
return
|
||||
|
||||
super(PublisherWindow, self).keyPressEvent(event)
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def _on_overlay_message(self, event):
|
||||
self._overlay_object.add_message(
|
||||
|
|
@ -574,7 +582,7 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
bool: Save can happen.
|
||||
"""
|
||||
|
||||
if not self._controller.host_context_has_changed:
|
||||
if not self._controller.host_context_has_changed():
|
||||
return True
|
||||
|
||||
title = "Host context changed"
|
||||
|
|
@ -643,7 +651,7 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
if not force and not self._is_on_details_tab():
|
||||
return
|
||||
|
||||
report_data = self.controller.get_publish_report()
|
||||
report_data = self._controller.get_publish_report()
|
||||
self._publish_details_widget.set_report_data(report_data)
|
||||
|
||||
def _on_help_click(self):
|
||||
|
|
@ -831,7 +839,6 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._set_comment_input_visiblity(True)
|
||||
self._set_publish_overlay_visibility(False)
|
||||
self._set_publish_visibility(False)
|
||||
self._set_footer_enabled(False)
|
||||
self._update_publish_details_widget()
|
||||
|
||||
def _on_controller_reset(self):
|
||||
|
|
@ -885,24 +892,13 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._set_publish_overlay_visibility(False)
|
||||
self._reset_btn.setEnabled(True)
|
||||
self._stop_btn.setEnabled(False)
|
||||
publish_has_crashed = self._controller.publish_has_crashed
|
||||
validate_enabled = not publish_has_crashed
|
||||
publish_enabled = not publish_has_crashed
|
||||
if self._is_on_publish_tab():
|
||||
self._go_to_report_tab()
|
||||
|
||||
if validate_enabled:
|
||||
validate_enabled = not self._controller.publish_has_validated
|
||||
if publish_enabled:
|
||||
if (
|
||||
self._controller.publish_has_validated
|
||||
and self._controller.publish_has_validation_errors
|
||||
):
|
||||
publish_enabled = False
|
||||
|
||||
else:
|
||||
publish_enabled = not self._controller.publish_has_finished
|
||||
|
||||
publish_enabled = self._controller.publish_can_continue()
|
||||
validate_enabled = (
|
||||
publish_enabled and not self._controller.publish_has_validated()
|
||||
)
|
||||
self._validate_btn.setEnabled(validate_enabled)
|
||||
self._publish_btn.setEnabled(publish_enabled)
|
||||
|
||||
|
|
@ -912,12 +908,12 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._update_publish_details_widget()
|
||||
|
||||
def _validate_create_instances(self):
|
||||
if not self._controller.host_is_valid:
|
||||
if not self._controller.is_host_valid():
|
||||
self._set_footer_enabled(True)
|
||||
return
|
||||
|
||||
all_valid = None
|
||||
for instance in self._controller.instances.values():
|
||||
for instance in self._controller.get_instances():
|
||||
if not instance["active"]:
|
||||
continue
|
||||
|
||||
|
|
@ -933,7 +929,7 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
def _on_instances_refresh(self):
|
||||
self._validate_create_instances()
|
||||
|
||||
context_title = self.controller.get_context_title()
|
||||
context_title = self._controller.get_context_title()
|
||||
self.set_context_label(context_title)
|
||||
self._update_publish_details_widget()
|
||||
|
||||
|
|
@ -1091,7 +1087,7 @@ class ErrorsMessageBox(ErrorMessageBox):
|
|||
self._tabs_widget = None
|
||||
self._stack_layout = None
|
||||
|
||||
super(ErrorsMessageBox, self).__init__(error_title, parent)
|
||||
super().__init__(error_title, parent)
|
||||
|
||||
layout = self.layout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ class FoldersQtModel(QtGui.QStandardItemModel):
|
|||
Union[str, None]: Folder id or None if folder is not available.
|
||||
|
||||
"""
|
||||
for folder_id, item in self._items_by_id.values():
|
||||
for folder_id, item in self._items_by_id.items():
|
||||
if item.data(FOLDER_PATH_ROLE) == folder_path:
|
||||
return folder_id
|
||||
return None
|
||||
|
|
@ -165,7 +165,7 @@ class FoldersQtModel(QtGui.QStandardItemModel):
|
|||
folder_items = self._controller.get_folder_items(
|
||||
project_name, FOLDERS_MODEL_SENDER_NAME
|
||||
)
|
||||
folder_type_items = {}
|
||||
folder_type_items = []
|
||||
if hasattr(self._controller, "get_folder_type_items"):
|
||||
folder_type_items = self._controller.get_folder_type_items(
|
||||
project_name, FOLDERS_MODEL_SENDER_NAME
|
||||
|
|
@ -194,7 +194,7 @@ class FoldersQtModel(QtGui.QStandardItemModel):
|
|||
return
|
||||
if thread.failed:
|
||||
# TODO visualize that refresh failed
|
||||
folder_items, folder_type_items = {}, {}
|
||||
folder_items, folder_type_items = {}, []
|
||||
else:
|
||||
folder_items, folder_type_items = thread.get_result()
|
||||
self._fill_items(folder_items, folder_type_items)
|
||||
|
|
|
|||
|
|
@ -743,6 +743,14 @@ class IntegrateHeroVersionModel(BaseSettingsModel):
|
|||
optional: bool = SettingsField(False, title="Optional")
|
||||
active: bool = SettingsField(True, title="Active")
|
||||
families: list[str] = SettingsField(default_factory=list, title="Families")
|
||||
use_hardlinks: bool = SettingsField(
|
||||
False, title="Use Hardlinks",
|
||||
description="When enabled first try to make a hardlink of the version "
|
||||
"instead of a copy. This helps reduce disk usage, but may "
|
||||
"create issues.\nFor example there are known issues on "
|
||||
"Windows being unable to delete any of the hardlinks if "
|
||||
"any of the links is in use creating issues with updating "
|
||||
"hero versions.")
|
||||
|
||||
|
||||
class CleanUpModel(BaseSettingsModel):
|
||||
|
|
@ -1136,7 +1144,8 @@ DEFAULT_PUBLISH_VALUES = {
|
|||
"layout",
|
||||
"mayaScene",
|
||||
"simpleUnrealTexture"
|
||||
]
|
||||
],
|
||||
"use_hardlinks": False
|
||||
},
|
||||
"CleanUp": {
|
||||
"paterns": [],
|
||||
|
|
|
|||
|
|
@ -1,202 +0,0 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
AfterEffects Addon
|
||||
===============
|
||||
|
||||
Integration with Adobe AfterEffects.
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
from .version import __version__
|
||||
from .addon import (
|
||||
AFTEREFFECTS_ADDON_ROOT,
|
||||
AfterEffectsAddon,
|
||||
get_launch_script_path,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"__version__",
|
||||
|
||||
"AFTEREFFECTS_ADDON_ROOT",
|
||||
"AfterEffectsAddon",
|
||||
"get_launch_script_path",
|
||||
)
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import os
|
||||
|
||||
from ayon_core.addon import AYONAddon, IHostAddon
|
||||
|
||||
from .version import __version__
|
||||
|
||||
AFTEREFFECTS_ADDON_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
class AfterEffectsAddon(AYONAddon, IHostAddon):
|
||||
name = "aftereffects"
|
||||
version = __version__
|
||||
host_name = "aftereffects"
|
||||
|
||||
def add_implementation_envs(self, env, _app):
|
||||
"""Modify environments to contain all required for implementation."""
|
||||
defaults = {
|
||||
"AYON_LOG_NO_COLORS": "1",
|
||||
"WEBSOCKET_URL": "ws://localhost:8097/ws/"
|
||||
}
|
||||
for key, value in defaults.items():
|
||||
if not env.get(key):
|
||||
env[key] = value
|
||||
|
||||
def get_workfile_extensions(self):
|
||||
return [".aep"]
|
||||
|
||||
def get_launch_hook_paths(self, app):
|
||||
if app.host_name != self.host_name:
|
||||
return []
|
||||
return [
|
||||
os.path.join(AFTEREFFECTS_ADDON_ROOT, "hooks")
|
||||
]
|
||||
|
||||
|
||||
def get_launch_script_path():
|
||||
return os.path.join(
|
||||
AFTEREFFECTS_ADDON_ROOT, "api", "launch_script.py"
|
||||
)
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
# AfterEffects Integration
|
||||
|
||||
Requirements: This extension requires use of Javascript engine, which is
|
||||
available since CC 16.0.
|
||||
Please check your File>Project Settings>Expressions>Expressions Engine
|
||||
|
||||
## Setup
|
||||
|
||||
The After Effects integration requires two components to work; `extension` and `server`.
|
||||
|
||||
### Extension
|
||||
|
||||
To install the extension download [Extension Manager Command Line tool (ExManCmd)](https://github.com/Adobe-CEP/Getting-Started-guides/tree/master/Package%20Distribute%20Install#option-2---exmancmd).
|
||||
|
||||
```
|
||||
ExManCmd /install {path to addon}/api/extension.zxp
|
||||
```
|
||||
OR
|
||||
download [Anastasiy’s Extension Manager](https://install.anastasiy.com/)
|
||||
|
||||
`{path to addon}` will be most likely in your AppData (on Windows, in your user data folder in Linux and MacOS.)
|
||||
|
||||
### Server
|
||||
|
||||
The easiest way to get the server and After Effects launch is with:
|
||||
|
||||
```
|
||||
python -c ^"import ayon_core.hosts.photoshop;ayon_aftereffects.launch(""c:\Program Files\Adobe\Adobe After Effects 2020\Support Files\AfterFX.exe"")^"
|
||||
```
|
||||
|
||||
`avalon.aftereffects.launch` launches the application and server, and also closes the server when After Effects exists.
|
||||
|
||||
## Usage
|
||||
|
||||
The After Effects extension can be found under `Window > Extensions > AYON`. Once launched you should be presented with a panel like this:
|
||||
|
||||

|
||||
|
||||
|
||||
## Developing
|
||||
|
||||
### Extension
|
||||
When developing the extension you can load it [unsigned](https://github.com/Adobe-CEP/CEP-Resources/blob/master/CEP_9.x/Documentation/CEP%209.0%20HTML%20Extension%20Cookbook.md#debugging-unsigned-extensions).
|
||||
|
||||
When signing the extension you can use this [guide](https://github.com/Adobe-CEP/Getting-Started-guides/tree/master/Package%20Distribute%20Install#package-distribute-install-guide).
|
||||
|
||||
```
|
||||
ZXPSignCmd -selfSignedCert NA NA Ayon Avalon-After-Effects Ayon extension.p12
|
||||
ZXPSignCmd -sign {path to addon}/api/extension {path to addon}/api/extension.zxp extension.p12 Ayon
|
||||
```
|
||||
|
||||
### Plugin Examples
|
||||
|
||||
These plugins were made with the [polly config](https://github.com/mindbender-studio/config). To fully integrate and load, you will have to use this config and add `image` to the [integration plugin](https://github.com/mindbender-studio/config/blob/master/polly/plugins/publish/integrate_asset.py).
|
||||
|
||||
Expected deployed extension location on default Windows:
|
||||
`c:\Program Files (x86)\Common Files\Adobe\CEP\extensions\io.ynput.AE.panel`
|
||||
|
||||
For easier debugging of Javascript:
|
||||
https://community.adobe.com/t5/download-install/adobe-extension-debuger-problem/td-p/10911704?page=1
|
||||
Add (optional) --enable-blink-features=ShadowDOMV0,CustomElementsV0 when starting Chrome
|
||||
then localhost:8092
|
||||
|
||||
Or use Visual Studio Code https://medium.com/adobetech/extendscript-debugger-for-visual-studio-code-public-release-a2ff6161fa01
|
||||
## Resources
|
||||
- https://javascript-tools-guide.readthedocs.io/introduction/index.html
|
||||
- https://github.com/Adobe-CEP/Getting-Started-guides
|
||||
- https://github.com/Adobe-CEP/CEP-Resources
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
"""Public API
|
||||
|
||||
Anything that isn't defined here is INTERNAL and unreliable for external use.
|
||||
|
||||
"""
|
||||
|
||||
from .ws_stub import (
|
||||
get_stub,
|
||||
)
|
||||
|
||||
from .pipeline import (
|
||||
AfterEffectsHost,
|
||||
ls,
|
||||
containerise
|
||||
)
|
||||
|
||||
from .lib import (
|
||||
maintained_selection,
|
||||
get_extension_manifest_path,
|
||||
get_folder_settings,
|
||||
set_settings
|
||||
)
|
||||
|
||||
from .plugin import (
|
||||
AfterEffectsLoader
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
# ws_stub
|
||||
"get_stub",
|
||||
|
||||
# pipeline
|
||||
"AfterEffectsHost",
|
||||
"ls",
|
||||
"containerise",
|
||||
|
||||
# lib
|
||||
"maintained_selection",
|
||||
"get_extension_manifest_path",
|
||||
"get_folder_settings",
|
||||
"set_settings",
|
||||
|
||||
# plugin
|
||||
"AfterEffectsLoader"
|
||||
]
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ExtensionList>
|
||||
<Extension Id="io.ynput.AE.panel">
|
||||
<HostList>
|
||||
|
||||
<!-- Comment Host tags according to the apps you want your panel to support -->
|
||||
|
||||
<!-- Photoshop -->
|
||||
<Host Name="PHXS" Port="8088"/>
|
||||
|
||||
<!-- Illustrator -->
|
||||
<Host Name="ILST" Port="8089"/>
|
||||
|
||||
<!-- InDesign -->
|
||||
<Host Name="IDSN" Port="8090" />
|
||||
|
||||
<!-- Premiere -->
|
||||
<Host Name="PPRO" Port="8091" />
|
||||
|
||||
<!-- AfterEffects -->
|
||||
<Host Name="AEFT" Port="8092" />
|
||||
|
||||
<!-- PRELUDE -->
|
||||
<Host Name="PRLD" Port="8093" />
|
||||
|
||||
<!-- FLASH Pro -->
|
||||
<Host Name="FLPR" Port="8094" />
|
||||
|
||||
</HostList>
|
||||
</Extension>
|
||||
</ExtensionList>
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ExtensionManifest Version="8.0" ExtensionBundleId="io.ynput.AE.panel" ExtensionBundleVersion="1.1.0"
|
||||
ExtensionBundleName="io.ynput.AE.panel" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<ExtensionList>
|
||||
<Extension Id="io.ynput.AE.panel" Version="1.0" />
|
||||
</ExtensionList>
|
||||
<ExecutionEnvironment>
|
||||
<HostList>
|
||||
<!-- Uncomment Host tags according to the apps you want your panel to support -->
|
||||
<!-- Photoshop -->
|
||||
<!--<Host Name="PHXS" Version="[14.0,19.0]" /> -->
|
||||
<!-- <Host Name="PHSP" Version="[14.0,19.0]" /> -->
|
||||
|
||||
<!-- Illustrator -->
|
||||
<!-- <Host Name="ILST" Version="[18.0,22.0]" /> -->
|
||||
|
||||
<!-- InDesign -->
|
||||
<!-- <Host Name="IDSN" Version="[10.0,13.0]" /> -->
|
||||
|
||||
<!-- Premiere -->
|
||||
<!-- <Host Name="PPRO" Version="[8.0,12.0]" /> -->
|
||||
|
||||
<!-- AfterEffects -->
|
||||
<Host Name="AEFT" Version="[13.0,99.0]" />
|
||||
|
||||
<!-- PRELUDE -->
|
||||
<!-- <Host Name="PRLD" Version="[3.0,7.0]" /> -->
|
||||
|
||||
<!-- FLASH Pro -->
|
||||
<!-- <Host Name="FLPR" Version="[14.0,18.0]" /> -->
|
||||
|
||||
</HostList>
|
||||
<LocaleList>
|
||||
<Locale Code="All" />
|
||||
</LocaleList>
|
||||
<RequiredRuntimeList>
|
||||
<RequiredRuntime Name="CSXS" Version="9.0" />
|
||||
</RequiredRuntimeList>
|
||||
</ExecutionEnvironment>
|
||||
<DispatchInfoList>
|
||||
<Extension Id="io.ynput.AE.panel">
|
||||
<DispatchInfo >
|
||||
<Resources>
|
||||
<MainPath>./index.html</MainPath>
|
||||
<ScriptPath>./jsx/hostscript.jsx</ScriptPath>
|
||||
</Resources>
|
||||
<Lifecycle>
|
||||
<AutoVisible>true</AutoVisible>
|
||||
</Lifecycle>
|
||||
<UI>
|
||||
<Type>Panel</Type>
|
||||
<Menu>AYON</Menu>
|
||||
<Geometry>
|
||||
<Size>
|
||||
<Height>200</Height>
|
||||
<Width>100</Width>
|
||||
</Size>
|
||||
<!--<MinSize>
|
||||
<Height>550</Height>
|
||||
<Width>400</Width>
|
||||
</MinSize>
|
||||
<MaxSize>
|
||||
<Height>550</Height>
|
||||
<Width>400</Width>
|
||||
</MaxSize>-->
|
||||
|
||||
</Geometry>
|
||||
<Icons>
|
||||
<Icon Type="Normal">./icons/ayon_logo.png</Icon>
|
||||
<Icon Type="RollOver">./icons/iconRollover.png</Icon>
|
||||
<Icon Type="Disabled">./icons/iconDisabled.png</Icon>
|
||||
<Icon Type="DarkNormal">./icons/iconDarkNormal.png</Icon>
|
||||
<Icon Type="DarkRollOver">./icons/iconDarkRollover.png</Icon>
|
||||
</Icons>
|
||||
</UI>
|
||||
</DispatchInfo>
|
||||
</Extension>
|
||||
</DispatchInfoList>
|
||||
</ExtensionManifest>
|
||||
|
|
@ -1,327 +0,0 @@
|
|||
/*
|
||||
* HTML5 ✰ Boilerplate
|
||||
*
|
||||
* What follows is the result of much research on cross-browser styling.
|
||||
* Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal,
|
||||
* Kroc Camen, and the H5BP dev community and team.
|
||||
*
|
||||
* Detailed information about this CSS: h5bp.com/css
|
||||
*
|
||||
* ==|== normalize ==========================================================
|
||||
*/
|
||||
|
||||
|
||||
/* =============================================================================
|
||||
HTML5 display definitions
|
||||
========================================================================== */
|
||||
|
||||
article, aside, details, figcaption, figure, footer, header, hgroup, nav, section { display: block; }
|
||||
audio, canvas, video { display: inline-block; *display: inline; *zoom: 1; }
|
||||
audio:not([controls]) { display: none; }
|
||||
[hidden] { display: none; }
|
||||
|
||||
|
||||
/* =============================================================================
|
||||
Base
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* 1. Correct text resizing oddly in IE6/7 when body font-size is set using em units
|
||||
* 2. Force vertical scrollbar in non-IE
|
||||
* 3. Prevent iOS text size adjust on device orientation change, without disabling user zoom: h5bp.com/g
|
||||
*/
|
||||
|
||||
html { font-size: 100%; overflow-y: scroll; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
|
||||
|
||||
body { margin: 0; font-size: 100%; line-height: 1.231; }
|
||||
|
||||
body, button, input, select, textarea { font-family: helvetica, arial,"lucida grande", verdana, "メイリオ", "MS Pゴシック", sans-serif; color: #222; }
|
||||
/*
|
||||
* Remove text-shadow in selection highlight: h5bp.com/i
|
||||
* These selection declarations have to be separate
|
||||
* Also: hot pink! (or customize the background color to match your design)
|
||||
*/
|
||||
|
||||
::selection { text-shadow: none; background-color: highlight; color: highlighttext; }
|
||||
|
||||
|
||||
/* =============================================================================
|
||||
Links
|
||||
========================================================================== */
|
||||
|
||||
a { color: #00e; }
|
||||
a:visited { color: #551a8b; }
|
||||
a:hover { color: #06e; }
|
||||
a:focus { outline: thin dotted; }
|
||||
|
||||
/* Improve readability when focused and hovered in all browsers: h5bp.com/h */
|
||||
a:hover, a:active { outline: 0; }
|
||||
|
||||
|
||||
/* =============================================================================
|
||||
Typography
|
||||
========================================================================== */
|
||||
|
||||
abbr[title] { border-bottom: 1px dotted; }
|
||||
|
||||
b, strong { font-weight: bold; }
|
||||
|
||||
blockquote { margin: 1em 40px; }
|
||||
|
||||
dfn { font-style: italic; }
|
||||
|
||||
hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; }
|
||||
|
||||
ins { background: #ff9; color: #000; text-decoration: none; }
|
||||
|
||||
mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; }
|
||||
|
||||
/* Redeclare monospace font family: h5bp.com/j */
|
||||
pre, code, kbd, samp { font-family: monospace, serif; _font-family: 'courier new', monospace; font-size: 1em; }
|
||||
|
||||
/* Improve readability of pre-formatted text in all browsers */
|
||||
pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; }
|
||||
|
||||
q { quotes: none; }
|
||||
q:before, q:after { content: ""; content: none; }
|
||||
|
||||
small { font-size: 85%; }
|
||||
|
||||
/* Position subscript and superscript content without affecting line-height: h5bp.com/k */
|
||||
sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
|
||||
sup { top: -0.5em; }
|
||||
sub { bottom: -0.25em; }
|
||||
|
||||
|
||||
/* =============================================================================
|
||||
Lists
|
||||
========================================================================== */
|
||||
|
||||
ul, ol { margin: 1em 0; padding: 0 0 0 40px; }
|
||||
dd { margin: 0 0 0 40px; }
|
||||
nav ul, nav ol { list-style: none; list-style-image: none; margin: 0; padding: 0; }
|
||||
|
||||
|
||||
/* =============================================================================
|
||||
Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* 1. Improve image quality when scaled in IE7: h5bp.com/d
|
||||
* 2. Remove the gap between images and borders on image containers: h5bp.com/e
|
||||
*/
|
||||
|
||||
img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; }
|
||||
|
||||
/*
|
||||
* Correct overflow not hidden in IE9
|
||||
*/
|
||||
|
||||
svg:not(:root) { overflow: hidden; }
|
||||
|
||||
|
||||
/* =============================================================================
|
||||
Figures
|
||||
========================================================================== */
|
||||
|
||||
figure { margin: 0; }
|
||||
|
||||
|
||||
/* =============================================================================
|
||||
Forms
|
||||
========================================================================== */
|
||||
|
||||
form { margin: 0; }
|
||||
fieldset { border: 0; margin: 0; padding: 0; }
|
||||
|
||||
/* Indicate that 'label' will shift focus to the associated form element */
|
||||
label { cursor: pointer; }
|
||||
|
||||
/*
|
||||
* 1. Correct color not inheriting in IE6/7/8/9
|
||||
* 2. Correct alignment displayed oddly in IE6/7
|
||||
*/
|
||||
|
||||
legend { border: 0; *margin-left: -7px; padding: 0; }
|
||||
|
||||
/*
|
||||
* 1. Correct font-size not inheriting in all browsers
|
||||
* 2. Remove margins in FF3/4 S5 Chrome
|
||||
* 3. Define consistent vertical alignment display in all browsers
|
||||
*/
|
||||
|
||||
button, input, select, textarea { font-size: 100%; margin: 0; vertical-align: baseline; *vertical-align: middle; }
|
||||
|
||||
/*
|
||||
* 1. Define line-height as normal to match FF3/4 (set using !important in the UA stylesheet)
|
||||
*/
|
||||
|
||||
button, input { line-height: normal; }
|
||||
|
||||
/*
|
||||
* 1. Display hand cursor for clickable form elements
|
||||
* 2. Allow styling of clickable form elements in iOS
|
||||
* 3. Correct inner spacing displayed oddly in IE7 (doesn't effect IE6)
|
||||
*/
|
||||
|
||||
button, input[type="button"], input[type="reset"], input[type="submit"] { cursor: pointer; -webkit-appearance: button; *overflow: visible; }
|
||||
|
||||
/*
|
||||
* Consistent box sizing and appearance
|
||||
*/
|
||||
|
||||
input[type="checkbox"], input[type="radio"] { box-sizing: border-box; padding: 0; }
|
||||
input[type="search"] { -webkit-appearance: textfield; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; box-sizing: content-box; }
|
||||
input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; }
|
||||
|
||||
/*
|
||||
* Remove inner padding and border in FF3/4: h5bp.com/l
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; }
|
||||
|
||||
/*
|
||||
* 1. Remove default vertical scrollbar in IE6/7/8/9
|
||||
* 2. Allow only vertical resizing
|
||||
*/
|
||||
|
||||
textarea { overflow: auto; vertical-align: top; resize: vertical; }
|
||||
|
||||
/* Colors for form validity */
|
||||
input:valid, textarea:valid { }
|
||||
input:invalid, textarea:invalid { background-color: #f0dddd; }
|
||||
|
||||
|
||||
/* =============================================================================
|
||||
Tables
|
||||
========================================================================== */
|
||||
|
||||
table { border-collapse: collapse; border-spacing: 0; }
|
||||
td { vertical-align: top; }
|
||||
|
||||
|
||||
/* ==|== primary styles =====================================================
|
||||
Author:
|
||||
========================================================================== */
|
||||
|
||||
/* ==|== media queries ======================================================
|
||||
PLACEHOLDER Media Queries for Responsive Design.
|
||||
These override the primary ('mobile first') styles
|
||||
Modify as content requires.
|
||||
========================================================================== */
|
||||
|
||||
@media only screen and (min-width: 480px) {
|
||||
/* Style adjustments for viewports 480px and over go here */
|
||||
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 768px) {
|
||||
/* Style adjustments for viewports 768px and over go here */
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ==|== non-semantic helper classes ========================================
|
||||
Please define your styles before this section.
|
||||
========================================================================== */
|
||||
|
||||
/* For image replacement */
|
||||
.ir { display: block; border: 0; text-indent: -999em; overflow: hidden; background-color: transparent; background-repeat: no-repeat; text-align: left; direction: ltr; }
|
||||
.ir br { display: none; }
|
||||
|
||||
/* Hide from both screenreaders and browsers: h5bp.com/u */
|
||||
.hidden { display: none !important; visibility: hidden; }
|
||||
|
||||
/* Hide only visually, but have it available for screenreaders: h5bp.com/v */
|
||||
.visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; }
|
||||
|
||||
/* Extends the .visuallyhidden class to allow the element to be focusable when navigated to via the keyboard: h5bp.com/p */
|
||||
.visuallyhidden.focusable:active, .visuallyhidden.focusable:focus { clip: auto; height: auto; margin: 0; overflow: visible; position: static; width: auto; }
|
||||
|
||||
/* Hide visually and from screenreaders, but maintain layout */
|
||||
.invisible { visibility: hidden; }
|
||||
|
||||
/* Contain floats: h5bp.com/q */
|
||||
.clearfix:before, .clearfix:after { content: ""; display: table; }
|
||||
.clearfix:after { clear: both; }
|
||||
.clearfix { *zoom: 1; }
|
||||
|
||||
|
||||
|
||||
/* ==|== print styles =======================================================
|
||||
Print styles.
|
||||
Inlined to avoid required HTTP connection: h5bp.com/r
|
||||
========================================================================== */
|
||||
|
||||
@media print {
|
||||
* { background: transparent !important; color: black !important; box-shadow:none !important; text-shadow: none !important; filter:none !important; -ms-filter: none !important; } /* Black prints faster: h5bp.com/s */
|
||||
a, a:visited { text-decoration: underline; }
|
||||
a[href]:after { content: " (" attr(href) ")"; }
|
||||
abbr[title]:after { content: " (" attr(title) ")"; }
|
||||
.ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } /* Don't show links for images, or javascript/internal links */
|
||||
pre, blockquote { border: 1px solid #999; page-break-inside: avoid; }
|
||||
table { display: table-header-group; } /* h5bp.com/t */
|
||||
tr, img { page-break-inside: avoid; }
|
||||
img { max-width: 100% !important; }
|
||||
@page { margin: 0.5cm; }
|
||||
p, h2, h3 { orphans: 3; widows: 3; }
|
||||
h2, h3 { page-break-after: avoid; }
|
||||
}
|
||||
|
||||
/* reflow reset for -webkit-margin-before: 1em */
|
||||
p { margin: 0; }
|
||||
|
||||
html {
|
||||
overflow-y: auto;
|
||||
background-color: transparent;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #fff;
|
||||
font: normal 100%;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body, div, img, p, button, input, select, textarea {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.image {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input {
|
||||
cursor: default;
|
||||
display: block;
|
||||
}
|
||||
|
||||
input[type=button] {
|
||||
background-color: #e5e9e8;
|
||||
border: 1px solid #9daca9;
|
||||
border-radius: 4px;
|
||||
box-shadow: inset 0 1px #fff;
|
||||
font: inherit;
|
||||
letter-spacing: inherit;
|
||||
text-indent: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
input[type=button]:hover {
|
||||
background-color: #eff1f1;
|
||||
}
|
||||
|
||||
input[type=button]:active {
|
||||
background-color: #d2d6d6;
|
||||
border: 1px solid #9daca9;
|
||||
box-shadow: inset 0 1px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* Reset anchor styles to an unstyled default to be in parity with design surface. It
|
||||
is presumed that most link styles in real-world designs are custom (non-default). */
|
||||
a, a:visited, a:hover, a:active {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
/*Your styles*/
|
||||
|
||||
body {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
|
||||
#content {
|
||||
margin-right:auto;
|
||||
margin-left:auto;
|
||||
vertical-align:middle;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
|
||||
#btn_test{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Those classes will be edited at runtime with values specified
|
||||
by the settings of the CC application
|
||||
*/
|
||||
.hostFontColor{}
|
||||
.hostFontFamily{}
|
||||
.hostFontSize{}
|
||||
|
||||
/*font family, color and size*/
|
||||
.hostFont{}
|
||||
/*background color*/
|
||||
.hostBgd{}
|
||||
/*lighter background color*/
|
||||
.hostBgdLight{}
|
||||
/*darker background color*/
|
||||
.hostBgdDark{}
|
||||
/*background color and font*/
|
||||
.hostElt{}
|
||||
|
||||
|
||||
.hostButton{
|
||||
border:1px solid;
|
||||
border-radius:2px;
|
||||
height:20px;
|
||||
vertical-align:bottom;
|
||||
font-family:inherit;
|
||||
color:inherit;
|
||||
font-size:inherit;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
|
@ -1,187 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<link rel="stylesheet" href="css/topcoat-desktop-dark.min.css"/>
|
||||
<link id="hostStyle" rel="stylesheet" href="css/styles.css"/>
|
||||
|
||||
<style type="text/css">
|
||||
html, body, iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0px;
|
||||
margin: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
button {width: 100%;}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
button {width: 100%;}
|
||||
body {margin:0; padding:0; height: 100%;}
|
||||
html {height: 100%;}
|
||||
</style>
|
||||
|
||||
<title></title>
|
||||
<script src="js/libs/jquery-2.0.2.min.js"></script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#workfiles-button").bind("click", function() {
|
||||
|
||||
RPC.call('AfterEffects.workfiles_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#loader-button").bind("click", function() {
|
||||
RPC.call('AfterEffects.loader_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#publish-button").bind("click", function() {
|
||||
RPC.call('AfterEffects.publish_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#sceneinventory-button").bind("click", function() {
|
||||
RPC.call('AfterEffects.sceneinventory_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#setresolution-button").bind("click", function() {
|
||||
RPC.call('AfterEffects.setresolution_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#setframes-button").bind("click", function() {
|
||||
RPC.call('AfterEffects.setframes_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#setall-button").bind("click", function() {
|
||||
RPC.call('AfterEffects.setall_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#create-placeholder-button").bind("click", function() {
|
||||
RPC.call('AfterEffects.create_placeholder_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#update-placeholder-button").bind("click", function() {
|
||||
RPC.call('AfterEffects.update_placeholder_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#build-workfile-button").bind("click", function() {
|
||||
RPC.call('AfterEffects.build_workfile_template_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#experimental-button").bind("click", function() {
|
||||
RPC.call('AfterEffects.experimental_tools_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
<body class="hostElt">
|
||||
|
||||
<div id="content">
|
||||
|
||||
<div>
|
||||
<div></div><a href=# id=workfiles-button><button class="hostFontSize">Workfiles...</button></a></div>
|
||||
<div><a href=# id=loader-button><button class="hostFontSize">Load...</button></a></div>
|
||||
<div><a href=# id=publish-button><button class="hostFontSize">Publish...</button></a></div>
|
||||
<div><a href=# id=sceneinventory-button><button class="hostFontSize">Manage...</button></a></div>
|
||||
<div><a href=# id=separator0><button class="hostFontSize"> </button></a></div>
|
||||
<div><a href=# id=setresolution-button><button class="hostFontSize">Set Resolution</button></a></div>
|
||||
<div><a href=# id=setframes-button><button class="hostFontSize">Set Frame Range</button></a></div>
|
||||
<div><a href=# id=setall-button><button class="hostFontSize">Apply All Settings</button></a></div>
|
||||
<div><a href=# id=separator1><button class="hostFontSize"> </button></a></div>
|
||||
<div><a href=# id=create-placeholder-button><button class="hostFontSize">Create placeholder</button></a></div>
|
||||
<div><a href=# id=update-placeholder-button><button class="hostFontSize">Update placeholder</button></a></div>
|
||||
<div><a href=# id=build-workfile-button><button class="hostFontSize">Build Workfile from template</button></a></div>
|
||||
<div><a href=# id=separator3><button class="hostFontSize"> </button></a></div>
|
||||
<div><a href=# id=experimental-button><button class="hostFontSize">Experimental Tools...</button></a></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- <script src="js/libs/PlayerDebugMode"></script> -->
|
||||
<script src="js/libs/wsrpc.js"></script>
|
||||
<script src="js/libs/loglevel.min.js"></script>
|
||||
<script src="js/libs/CSInterface.js"></script>
|
||||
|
||||
<script src="js/themeManager.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,530 +0,0 @@
|
|||
// json2.js
|
||||
// 2017-06-12
|
||||
// Public Domain.
|
||||
// NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
|
||||
|
||||
// USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
|
||||
// NOT CONTROL.
|
||||
|
||||
// This file creates a global JSON object containing two methods: stringify
|
||||
// and parse. This file provides the ES5 JSON capability to ES3 systems.
|
||||
// If a project might run on IE8 or earlier, then this file should be included.
|
||||
// This file does nothing on ES5 systems.
|
||||
|
||||
// JSON.stringify(value, replacer, space)
|
||||
// value any JavaScript value, usually an object or array.
|
||||
// replacer an optional parameter that determines how object
|
||||
// values are stringified for objects. It can be a
|
||||
// function or an array of strings.
|
||||
// space an optional parameter that specifies the indentation
|
||||
// of nested structures. If it is omitted, the text will
|
||||
// be packed without extra whitespace. If it is a number,
|
||||
// it will specify the number of spaces to indent at each
|
||||
// level. If it is a string (such as "\t" or " "),
|
||||
// it contains the characters used to indent at each level.
|
||||
// This method produces a JSON text from a JavaScript value.
|
||||
// When an object value is found, if the object contains a toJSON
|
||||
// method, its toJSON method will be called and the result will be
|
||||
// stringified. A toJSON method does not serialize: it returns the
|
||||
// value represented by the name/value pair that should be serialized,
|
||||
// or undefined if nothing should be serialized. The toJSON method
|
||||
// will be passed the key associated with the value, and this will be
|
||||
// bound to the value.
|
||||
|
||||
// For example, this would serialize Dates as ISO strings.
|
||||
|
||||
// Date.prototype.toJSON = function (key) {
|
||||
// function f(n) {
|
||||
// // Format integers to have at least two digits.
|
||||
// return (n < 10)
|
||||
// ? "0" + n
|
||||
// : n;
|
||||
// }
|
||||
// return this.getUTCFullYear() + "-" +
|
||||
// f(this.getUTCMonth() + 1) + "-" +
|
||||
// f(this.getUTCDate()) + "T" +
|
||||
// f(this.getUTCHours()) + ":" +
|
||||
// f(this.getUTCMinutes()) + ":" +
|
||||
// f(this.getUTCSeconds()) + "Z";
|
||||
// };
|
||||
|
||||
// You can provide an optional replacer method. It will be passed the
|
||||
// key and value of each member, with this bound to the containing
|
||||
// object. The value that is returned from your method will be
|
||||
// serialized. If your method returns undefined, then the member will
|
||||
// be excluded from the serialization.
|
||||
|
||||
// If the replacer parameter is an array of strings, then it will be
|
||||
// used to select the members to be serialized. It filters the results
|
||||
// such that only members with keys listed in the replacer array are
|
||||
// stringified.
|
||||
|
||||
// Values that do not have JSON representations, such as undefined or
|
||||
// functions, will not be serialized. Such values in objects will be
|
||||
// dropped; in arrays they will be replaced with null. You can use
|
||||
// a replacer function to replace those with JSON values.
|
||||
|
||||
// JSON.stringify(undefined) returns undefined.
|
||||
|
||||
// The optional space parameter produces a stringification of the
|
||||
// value that is filled with line breaks and indentation to make it
|
||||
// easier to read.
|
||||
|
||||
// If the space parameter is a non-empty string, then that string will
|
||||
// be used for indentation. If the space parameter is a number, then
|
||||
// the indentation will be that many spaces.
|
||||
|
||||
// Example:
|
||||
|
||||
// text = JSON.stringify(["e", {pluribus: "unum"}]);
|
||||
// // text is '["e",{"pluribus":"unum"}]'
|
||||
|
||||
// text = JSON.stringify(["e", {pluribus: "unum"}], null, "\t");
|
||||
// // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
|
||||
|
||||
// text = JSON.stringify([new Date()], function (key, value) {
|
||||
// return this[key] instanceof Date
|
||||
// ? "Date(" + this[key] + ")"
|
||||
// : value;
|
||||
// });
|
||||
// // text is '["Date(---current time---)"]'
|
||||
|
||||
// JSON.parse(text, reviver)
|
||||
// This method parses a JSON text to produce an object or array.
|
||||
// It can throw a SyntaxError exception.
|
||||
|
||||
// The optional reviver parameter is a function that can filter and
|
||||
// transform the results. It receives each of the keys and values,
|
||||
// and its return value is used instead of the original value.
|
||||
// If it returns what it received, then the structure is not modified.
|
||||
// If it returns undefined then the member is deleted.
|
||||
|
||||
// Example:
|
||||
|
||||
// // Parse the text. Values that look like ISO date strings will
|
||||
// // be converted to Date objects.
|
||||
|
||||
// myData = JSON.parse(text, function (key, value) {
|
||||
// var a;
|
||||
// if (typeof value === "string") {
|
||||
// a =
|
||||
// /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
|
||||
// if (a) {
|
||||
// return new Date(Date.UTC(
|
||||
// +a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6]
|
||||
// ));
|
||||
// }
|
||||
// return value;
|
||||
// }
|
||||
// });
|
||||
|
||||
// myData = JSON.parse(
|
||||
// "[\"Date(09/09/2001)\"]",
|
||||
// function (key, value) {
|
||||
// var d;
|
||||
// if (
|
||||
// typeof value === "string"
|
||||
// && value.slice(0, 5) === "Date("
|
||||
// && value.slice(-1) === ")"
|
||||
// ) {
|
||||
// d = new Date(value.slice(5, -1));
|
||||
// if (d) {
|
||||
// return d;
|
||||
// }
|
||||
// }
|
||||
// return value;
|
||||
// }
|
||||
// );
|
||||
|
||||
// This is a reference implementation. You are free to copy, modify, or
|
||||
// redistribute.
|
||||
|
||||
/*jslint
|
||||
eval, for, this
|
||||
*/
|
||||
|
||||
/*property
|
||||
JSON, apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
|
||||
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
|
||||
lastIndex, length, parse, prototype, push, replace, slice, stringify,
|
||||
test, toJSON, toString, valueOf
|
||||
*/
|
||||
|
||||
|
||||
// Create a JSON object only if one does not already exist. We create the
|
||||
// methods in a closure to avoid creating global variables.
|
||||
|
||||
if (typeof JSON !== "object") {
|
||||
JSON = {};
|
||||
}
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var rx_one = /^[\],:{}\s]*$/;
|
||||
var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
|
||||
var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
|
||||
var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
|
||||
var rx_escapable = /[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
|
||||
var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
|
||||
|
||||
function f(n) {
|
||||
// Format integers to have at least two digits.
|
||||
return (n < 10)
|
||||
? "0" + n
|
||||
: n;
|
||||
}
|
||||
|
||||
function this_value() {
|
||||
return this.valueOf();
|
||||
}
|
||||
|
||||
if (typeof Date.prototype.toJSON !== "function") {
|
||||
|
||||
Date.prototype.toJSON = function () {
|
||||
|
||||
return isFinite(this.valueOf())
|
||||
? (
|
||||
this.getUTCFullYear()
|
||||
+ "-"
|
||||
+ f(this.getUTCMonth() + 1)
|
||||
+ "-"
|
||||
+ f(this.getUTCDate())
|
||||
+ "T"
|
||||
+ f(this.getUTCHours())
|
||||
+ ":"
|
||||
+ f(this.getUTCMinutes())
|
||||
+ ":"
|
||||
+ f(this.getUTCSeconds())
|
||||
+ "Z"
|
||||
)
|
||||
: null;
|
||||
};
|
||||
|
||||
Boolean.prototype.toJSON = this_value;
|
||||
Number.prototype.toJSON = this_value;
|
||||
String.prototype.toJSON = this_value;
|
||||
}
|
||||
|
||||
var gap;
|
||||
var indent;
|
||||
var meta;
|
||||
var rep;
|
||||
|
||||
|
||||
function quote(string) {
|
||||
|
||||
// If the string contains no control characters, no quote characters, and no
|
||||
// backslash characters, then we can safely slap some quotes around it.
|
||||
// Otherwise we must also replace the offending characters with safe escape
|
||||
// sequences.
|
||||
|
||||
rx_escapable.lastIndex = 0;
|
||||
return rx_escapable.test(string)
|
||||
? "\"" + string.replace(rx_escapable, function (a) {
|
||||
var c = meta[a];
|
||||
return typeof c === "string"
|
||||
? c
|
||||
: "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4);
|
||||
}) + "\""
|
||||
: "\"" + string + "\"";
|
||||
}
|
||||
|
||||
|
||||
function str(key, holder) {
|
||||
|
||||
// Produce a string from holder[key].
|
||||
|
||||
var i; // The loop counter.
|
||||
var k; // The member key.
|
||||
var v; // The member value.
|
||||
var length;
|
||||
var mind = gap;
|
||||
var partial;
|
||||
var value = holder[key];
|
||||
|
||||
// If the value has a toJSON method, call it to obtain a replacement value.
|
||||
|
||||
if (
|
||||
value
|
||||
&& typeof value === "object"
|
||||
&& typeof value.toJSON === "function"
|
||||
) {
|
||||
value = value.toJSON(key);
|
||||
}
|
||||
|
||||
// If we were called with a replacer function, then call the replacer to
|
||||
// obtain a replacement value.
|
||||
|
||||
if (typeof rep === "function") {
|
||||
value = rep.call(holder, key, value);
|
||||
}
|
||||
|
||||
// What happens next depends on the value's type.
|
||||
|
||||
switch (typeof value) {
|
||||
case "string":
|
||||
return quote(value);
|
||||
|
||||
case "number":
|
||||
|
||||
// JSON numbers must be finite. Encode non-finite numbers as null.
|
||||
|
||||
return (isFinite(value))
|
||||
? String(value)
|
||||
: "null";
|
||||
|
||||
case "boolean":
|
||||
case "null":
|
||||
|
||||
// If the value is a boolean or null, convert it to a string. Note:
|
||||
// typeof null does not produce "null". The case is included here in
|
||||
// the remote chance that this gets fixed someday.
|
||||
|
||||
return String(value);
|
||||
|
||||
// If the type is "object", we might be dealing with an object or an array or
|
||||
// null.
|
||||
|
||||
case "object":
|
||||
|
||||
// Due to a specification blunder in ECMAScript, typeof null is "object",
|
||||
// so watch out for that case.
|
||||
|
||||
if (!value) {
|
||||
return "null";
|
||||
}
|
||||
|
||||
// Make an array to hold the partial results of stringifying this object value.
|
||||
|
||||
gap += indent;
|
||||
partial = [];
|
||||
|
||||
// Is the value an array?
|
||||
|
||||
if (Object.prototype.toString.apply(value) === "[object Array]") {
|
||||
|
||||
// The value is an array. Stringify every element. Use null as a placeholder
|
||||
// for non-JSON values.
|
||||
|
||||
length = value.length;
|
||||
for (i = 0; i < length; i += 1) {
|
||||
partial[i] = str(i, value) || "null";
|
||||
}
|
||||
|
||||
// Join all of the elements together, separated with commas, and wrap them in
|
||||
// brackets.
|
||||
|
||||
v = partial.length === 0
|
||||
? "[]"
|
||||
: gap
|
||||
? (
|
||||
"[\n"
|
||||
+ gap
|
||||
+ partial.join(",\n" + gap)
|
||||
+ "\n"
|
||||
+ mind
|
||||
+ "]"
|
||||
)
|
||||
: "[" + partial.join(",") + "]";
|
||||
gap = mind;
|
||||
return v;
|
||||
}
|
||||
|
||||
// If the replacer is an array, use it to select the members to be stringified.
|
||||
|
||||
if (rep && typeof rep === "object") {
|
||||
length = rep.length;
|
||||
for (i = 0; i < length; i += 1) {
|
||||
if (typeof rep[i] === "string") {
|
||||
k = rep[i];
|
||||
v = str(k, value);
|
||||
if (v) {
|
||||
partial.push(quote(k) + (
|
||||
(gap)
|
||||
? ": "
|
||||
: ":"
|
||||
) + v);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// Otherwise, iterate through all of the keys in the object.
|
||||
|
||||
for (k in value) {
|
||||
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
||||
v = str(k, value);
|
||||
if (v) {
|
||||
partial.push(quote(k) + (
|
||||
(gap)
|
||||
? ": "
|
||||
: ":"
|
||||
) + v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Join all of the member texts together, separated with commas,
|
||||
// and wrap them in braces.
|
||||
|
||||
v = partial.length === 0
|
||||
? "{}"
|
||||
: gap
|
||||
? "{\n" + gap + partial.join(",\n" + gap) + "\n" + mind + "}"
|
||||
: "{" + partial.join(",") + "}";
|
||||
gap = mind;
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
// If the JSON object does not yet have a stringify method, give it one.
|
||||
|
||||
if (typeof JSON.stringify !== "function") {
|
||||
meta = { // table of character substitutions
|
||||
"\b": "\\b",
|
||||
"\t": "\\t",
|
||||
"\n": "\\n",
|
||||
"\f": "\\f",
|
||||
"\r": "\\r",
|
||||
"\"": "\\\"",
|
||||
"\\": "\\\\"
|
||||
};
|
||||
JSON.stringify = function (value, replacer, space) {
|
||||
|
||||
// The stringify method takes a value and an optional replacer, and an optional
|
||||
// space parameter, and returns a JSON text. The replacer can be a function
|
||||
// that can replace values, or an array of strings that will select the keys.
|
||||
// A default replacer method can be provided. Use of the space parameter can
|
||||
// produce text that is more easily readable.
|
||||
|
||||
var i;
|
||||
gap = "";
|
||||
indent = "";
|
||||
|
||||
// If the space parameter is a number, make an indent string containing that
|
||||
// many spaces.
|
||||
|
||||
if (typeof space === "number") {
|
||||
for (i = 0; i < space; i += 1) {
|
||||
indent += " ";
|
||||
}
|
||||
|
||||
// If the space parameter is a string, it will be used as the indent string.
|
||||
|
||||
} else if (typeof space === "string") {
|
||||
indent = space;
|
||||
}
|
||||
|
||||
// If there is a replacer, it must be a function or an array.
|
||||
// Otherwise, throw an error.
|
||||
|
||||
rep = replacer;
|
||||
if (replacer && typeof replacer !== "function" && (
|
||||
typeof replacer !== "object"
|
||||
|| typeof replacer.length !== "number"
|
||||
)) {
|
||||
throw new Error("JSON.stringify");
|
||||
}
|
||||
|
||||
// Make a fake root object containing our value under the key of "".
|
||||
// Return the result of stringifying the value.
|
||||
|
||||
return str("", {"": value});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// If the JSON object does not yet have a parse method, give it one.
|
||||
|
||||
if (typeof JSON.parse !== "function") {
|
||||
JSON.parse = function (text, reviver) {
|
||||
|
||||
// The parse method takes a text and an optional reviver function, and returns
|
||||
// a JavaScript value if the text is a valid JSON text.
|
||||
|
||||
var j;
|
||||
|
||||
function walk(holder, key) {
|
||||
|
||||
// The walk method is used to recursively walk the resulting structure so
|
||||
// that modifications can be made.
|
||||
|
||||
var k;
|
||||
var v;
|
||||
var value = holder[key];
|
||||
if (value && typeof value === "object") {
|
||||
for (k in value) {
|
||||
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
||||
v = walk(value, k);
|
||||
if (v !== undefined) {
|
||||
value[k] = v;
|
||||
} else {
|
||||
delete value[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return reviver.call(holder, key, value);
|
||||
}
|
||||
|
||||
|
||||
// Parsing happens in four stages. In the first stage, we replace certain
|
||||
// Unicode characters with escape sequences. JavaScript handles many characters
|
||||
// incorrectly, either silently deleting them, or treating them as line endings.
|
||||
|
||||
text = String(text);
|
||||
rx_dangerous.lastIndex = 0;
|
||||
if (rx_dangerous.test(text)) {
|
||||
text = text.replace(rx_dangerous, function (a) {
|
||||
return (
|
||||
"\\u"
|
||||
+ ("0000" + a.charCodeAt(0).toString(16)).slice(-4)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// In the second stage, we run the text against regular expressions that look
|
||||
// for non-JSON patterns. We are especially concerned with "()" and "new"
|
||||
// because they can cause invocation, and "=" because it can cause mutation.
|
||||
// But just to be safe, we want to reject all unexpected forms.
|
||||
|
||||
// We split the second stage into 4 regexp operations in order to work around
|
||||
// crippling inefficiencies in IE's and Safari's regexp engines. First we
|
||||
// replace the JSON backslash pairs with "@" (a non-JSON character). Second, we
|
||||
// replace all simple value tokens with "]" characters. Third, we delete all
|
||||
// open brackets that follow a colon or comma or that begin the text. Finally,
|
||||
// we look to see that the remaining characters are only whitespace or "]" or
|
||||
// "," or ":" or "{" or "}". If that is so, then the text is safe for eval.
|
||||
|
||||
if (
|
||||
rx_one.test(
|
||||
text
|
||||
.replace(rx_two, "@")
|
||||
.replace(rx_three, "]")
|
||||
.replace(rx_four, "")
|
||||
)
|
||||
) {
|
||||
|
||||
// In the third stage we use the eval function to compile the text into a
|
||||
// JavaScript structure. The "{" operator is subject to a syntactic ambiguity
|
||||
// in JavaScript: it can begin a block or an object literal. We wrap the text
|
||||
// in parens to eliminate the ambiguity.
|
||||
|
||||
j = eval("(" + text + ")");
|
||||
|
||||
// In the optional fourth stage, we recursively walk the new structure, passing
|
||||
// each name/value pair to a reviver function for possible transformation.
|
||||
|
||||
return (typeof reviver === "function")
|
||||
? walk({"": j}, "")
|
||||
: j;
|
||||
}
|
||||
|
||||
// If the text is not JSON parseable, then a SyntaxError is thrown.
|
||||
|
||||
throw new SyntaxError("JSON.parse");
|
||||
};
|
||||
}
|
||||
}());
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
/*! loglevel - v1.6.8 - https://github.com/pimterry/loglevel - (c) 2020 Tim Perry - licensed MIT */
|
||||
!function(a,b){"use strict";"function"==typeof define&&define.amd?define(b):"object"==typeof module&&module.exports?module.exports=b():a.log=b()}(this,function(){"use strict";function a(a,b){var c=a[b];if("function"==typeof c.bind)return c.bind(a);try{return Function.prototype.bind.call(c,a)}catch(b){return function(){return Function.prototype.apply.apply(c,[a,arguments])}}}function b(){console.log&&(console.log.apply?console.log.apply(console,arguments):Function.prototype.apply.apply(console.log,[console,arguments])),console.trace&&console.trace()}function c(c){return"debug"===c&&(c="log"),typeof console!==i&&("trace"===c&&j?b:void 0!==console[c]?a(console,c):void 0!==console.log?a(console,"log"):h)}function d(a,b){for(var c=0;c<k.length;c++){var d=k[c];this[d]=c<a?h:this.methodFactory(d,a,b)}this.log=this.debug}function e(a,b,c){return function(){typeof console!==i&&(d.call(this,b,c),this[a].apply(this,arguments))}}function f(a,b,d){return c(a)||e.apply(this,arguments)}function g(a,b,c){function e(a){var b=(k[a]||"silent").toUpperCase();if(typeof window!==i){try{return void(window.localStorage[l]=b)}catch(a){}try{window.document.cookie=encodeURIComponent(l)+"="+b+";"}catch(a){}}}function g(){var a;if(typeof window!==i){try{a=window.localStorage[l]}catch(a){}if(typeof a===i)try{var b=window.document.cookie,c=b.indexOf(encodeURIComponent(l)+"=");-1!==c&&(a=/^([^;]+)/.exec(b.slice(c))[1])}catch(a){}return void 0===j.levels[a]&&(a=void 0),a}}var h,j=this,l="loglevel";a&&(l+=":"+a),j.name=a,j.levels={TRACE:0,DEBUG:1,INFO:2,WARN:3,ERROR:4,SILENT:5},j.methodFactory=c||f,j.getLevel=function(){return h},j.setLevel=function(b,c){if("string"==typeof b&&void 0!==j.levels[b.toUpperCase()]&&(b=j.levels[b.toUpperCase()]),!("number"==typeof b&&b>=0&&b<=j.levels.SILENT))throw"log.setLevel() called with invalid level: "+b;if(h=b,!1!==c&&e(b),d.call(j,b,a),typeof console===i&&b<j.levels.SILENT)return"No console available for logging"},j.setDefaultLevel=function(a){g()||j.setLevel(a,!1)},j.enableAll=function(a){j.setLevel(j.levels.TRACE,a)},j.disableAll=function(a){j.setLevel(j.levels.SILENT,a)};var m=g();null==m&&(m=null==b?"WARN":b),j.setLevel(m,!1)}var h=function(){},i="undefined",j=typeof window!==i&&typeof window.navigator!==i&&/Trident\/|MSIE /.test(window.navigator.userAgent),k=["trace","debug","info","warn","error"],l=new g,m={};l.getLogger=function(a){if("string"!=typeof a||""===a)throw new TypeError("You must supply a name when creating a logger.");var b=m[a];return b||(b=m[a]=new g(a,l.getLevel(),l.methodFactory)),b};var n=typeof window!==i?window.log:void 0;return l.noConflict=function(){return typeof window!==i&&window.log===l&&(window.log=n),l},l.getLoggers=function(){return m},l});
|
||||
|
|
@ -1,393 +0,0 @@
|
|||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global = global || self, global.WSRPC = factory());
|
||||
}(this, function () { 'use strict';
|
||||
|
||||
function _classCallCheck(instance, Constructor) {
|
||||
if (!(instance instanceof Constructor)) {
|
||||
throw new TypeError("Cannot call a class as a function");
|
||||
}
|
||||
}
|
||||
|
||||
var Deferred = function Deferred() {
|
||||
_classCallCheck(this, Deferred);
|
||||
|
||||
var self = this;
|
||||
self.resolve = null;
|
||||
self.reject = null;
|
||||
self.done = false;
|
||||
|
||||
function wrapper(func) {
|
||||
return function () {
|
||||
if (self.done) throw new Error('Promise already done');
|
||||
self.done = true;
|
||||
return func.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
self.promise = new Promise(function (resolve, reject) {
|
||||
self.resolve = wrapper(resolve);
|
||||
self.reject = wrapper(reject);
|
||||
});
|
||||
|
||||
self.promise.isPending = function () {
|
||||
return !self.done;
|
||||
};
|
||||
|
||||
return self;
|
||||
};
|
||||
|
||||
function logGroup(group, level, args) {
|
||||
console.group(group);
|
||||
console[level].apply(this, args);
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
function log() {
|
||||
if (!WSRPC.DEBUG) return;
|
||||
logGroup('WSRPC.DEBUG', 'trace', arguments);
|
||||
}
|
||||
|
||||
function trace(msg) {
|
||||
if (!WSRPC.TRACE) return;
|
||||
var payload = msg;
|
||||
if ('data' in msg) payload = JSON.parse(msg.data);
|
||||
logGroup("WSRPC.TRACE", 'trace', [payload]);
|
||||
}
|
||||
|
||||
function getAbsoluteWsUrl(url) {
|
||||
if (/^\w+:\/\//.test(url)) return url;
|
||||
if (typeof window == 'undefined' && window.location.host.length < 1) throw new Error("Can not construct absolute URL from ".concat(window.location));
|
||||
var scheme = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
var port = window.location.port === '' ? ":".concat(window.location.port) : '';
|
||||
var host = window.location.host;
|
||||
var path = url.replace(/^\/+/gm, '');
|
||||
return "".concat(scheme, "//").concat(host).concat(port, "/").concat(path);
|
||||
}
|
||||
|
||||
var readyState = Object.freeze({
|
||||
0: 'CONNECTING',
|
||||
1: 'OPEN',
|
||||
2: 'CLOSING',
|
||||
3: 'CLOSED'
|
||||
});
|
||||
|
||||
var WSRPC = function WSRPC(URL) {
|
||||
var reconnectTimeout = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1000;
|
||||
|
||||
_classCallCheck(this, WSRPC);
|
||||
|
||||
var self = this;
|
||||
URL = getAbsoluteWsUrl(URL);
|
||||
self.id = 1;
|
||||
self.eventId = 0;
|
||||
self.socketStarted = false;
|
||||
self.eventStore = {
|
||||
onconnect: {},
|
||||
onerror: {},
|
||||
onclose: {},
|
||||
onchange: {}
|
||||
};
|
||||
self.connectionNumber = 0;
|
||||
self.oneTimeEventStore = {
|
||||
onconnect: [],
|
||||
onerror: [],
|
||||
onclose: [],
|
||||
onchange: []
|
||||
};
|
||||
self.callQueue = [];
|
||||
|
||||
function createSocket() {
|
||||
var ws = new WebSocket(URL);
|
||||
|
||||
var rejectQueue = function rejectQueue() {
|
||||
self.connectionNumber++; // rejects incoming calls
|
||||
|
||||
var deferred; //reject all pending calls
|
||||
|
||||
while (0 < self.callQueue.length) {
|
||||
var callObj = self.callQueue.shift();
|
||||
deferred = self.store[callObj.id];
|
||||
delete self.store[callObj.id];
|
||||
|
||||
if (deferred && deferred.promise.isPending()) {
|
||||
deferred.reject('WebSocket error occurred');
|
||||
}
|
||||
} // reject all from the store
|
||||
|
||||
|
||||
for (var key in self.store) {
|
||||
if (!self.store.hasOwnProperty(key)) continue;
|
||||
deferred = self.store[key];
|
||||
|
||||
if (deferred && deferred.promise.isPending()) {
|
||||
deferred.reject('WebSocket error occurred');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function reconnect(callEvents) {
|
||||
setTimeout(function () {
|
||||
try {
|
||||
self.socket = createSocket();
|
||||
self.id = 1;
|
||||
} catch (exc) {
|
||||
callEvents('onerror', exc);
|
||||
delete self.socket;
|
||||
console.error(exc);
|
||||
}
|
||||
}, reconnectTimeout);
|
||||
}
|
||||
|
||||
ws.onclose = function (err) {
|
||||
log('ONCLOSE CALLED', 'STATE', self.public.state());
|
||||
trace(err);
|
||||
|
||||
for (var serial in self.store) {
|
||||
if (!self.store.hasOwnProperty(serial)) continue;
|
||||
|
||||
if (self.store[serial].hasOwnProperty('reject')) {
|
||||
self.store[serial].reject('Connection closed');
|
||||
}
|
||||
}
|
||||
|
||||
rejectQueue();
|
||||
callEvents('onclose', err);
|
||||
callEvents('onchange', err);
|
||||
reconnect(callEvents);
|
||||
};
|
||||
|
||||
ws.onerror = function (err) {
|
||||
log('ONERROR CALLED', 'STATE', self.public.state());
|
||||
trace(err);
|
||||
rejectQueue();
|
||||
callEvents('onerror', err);
|
||||
callEvents('onchange', err);
|
||||
log('WebSocket has been closed by error: ', err);
|
||||
};
|
||||
|
||||
function tryCallEvent(func, event) {
|
||||
try {
|
||||
return func(event);
|
||||
} catch (e) {
|
||||
if (e.hasOwnProperty('stack')) {
|
||||
log(e.stack);
|
||||
} else {
|
||||
log('Event function', func, 'raised unknown error:', e);
|
||||
}
|
||||
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function callEvents(evName, event) {
|
||||
while (0 < self.oneTimeEventStore[evName].length) {
|
||||
var deferred = self.oneTimeEventStore[evName].shift();
|
||||
if (deferred.hasOwnProperty('resolve') && deferred.promise.isPending()) deferred.resolve();
|
||||
}
|
||||
|
||||
for (var i in self.eventStore[evName]) {
|
||||
if (!self.eventStore[evName].hasOwnProperty(i)) continue;
|
||||
var cur = self.eventStore[evName][i];
|
||||
tryCallEvent(cur, event);
|
||||
}
|
||||
}
|
||||
|
||||
ws.onopen = function (ev) {
|
||||
log('ONOPEN CALLED', 'STATE', self.public.state());
|
||||
trace(ev);
|
||||
|
||||
while (0 < self.callQueue.length) {
|
||||
// noinspection JSUnresolvedFunction
|
||||
self.socket.send(JSON.stringify(self.callQueue.shift(), 0, 1));
|
||||
}
|
||||
|
||||
callEvents('onconnect', ev);
|
||||
callEvents('onchange', ev);
|
||||
};
|
||||
|
||||
function handleCall(self, data) {
|
||||
if (!self.routes.hasOwnProperty(data.method)) throw new Error('Route not found');
|
||||
var connectionNumber = self.connectionNumber;
|
||||
var deferred = new Deferred();
|
||||
deferred.promise.then(function (result) {
|
||||
if (connectionNumber !== self.connectionNumber) return;
|
||||
self.socket.send(JSON.stringify({
|
||||
id: data.id,
|
||||
result: result
|
||||
}));
|
||||
}, function (error) {
|
||||
if (connectionNumber !== self.connectionNumber) return;
|
||||
self.socket.send(JSON.stringify({
|
||||
id: data.id,
|
||||
error: error
|
||||
}));
|
||||
});
|
||||
var func = self.routes[data.method];
|
||||
if (self.asyncRoutes[data.method]) return func.apply(deferred, [data.params]);
|
||||
|
||||
function badPromise() {
|
||||
throw new Error("You should register route with async flag.");
|
||||
}
|
||||
|
||||
var promiseMock = {
|
||||
resolve: badPromise,
|
||||
reject: badPromise
|
||||
};
|
||||
|
||||
try {
|
||||
deferred.resolve(func.apply(promiseMock, [data.params]));
|
||||
} catch (e) {
|
||||
deferred.reject(e);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function handleError(self, data) {
|
||||
if (!self.store.hasOwnProperty(data.id)) return log('Unknown callback');
|
||||
var deferred = self.store[data.id];
|
||||
if (typeof deferred === 'undefined') return log('Confirmation without handler');
|
||||
delete self.store[data.id];
|
||||
log('REJECTING', data.error);
|
||||
deferred.reject(data.error);
|
||||
}
|
||||
|
||||
function handleResult(self, data) {
|
||||
var deferred = self.store[data.id];
|
||||
if (typeof deferred === 'undefined') return log('Confirmation without handler');
|
||||
delete self.store[data.id];
|
||||
|
||||
if (data.hasOwnProperty('result')) {
|
||||
return deferred.resolve(data.result);
|
||||
}
|
||||
|
||||
return deferred.reject(data.error);
|
||||
}
|
||||
|
||||
ws.onmessage = function (message) {
|
||||
log('ONMESSAGE CALLED', 'STATE', self.public.state());
|
||||
trace(message);
|
||||
if (message.type !== 'message') return;
|
||||
var data;
|
||||
|
||||
try {
|
||||
data = JSON.parse(message.data);
|
||||
log(data);
|
||||
|
||||
if (data.hasOwnProperty('method')) {
|
||||
return handleCall(self, data);
|
||||
} else if (data.hasOwnProperty('error') && data.error === null) {
|
||||
return handleError(self, data);
|
||||
} else {
|
||||
return handleResult(self, data);
|
||||
}
|
||||
} catch (exception) {
|
||||
var err = {
|
||||
error: exception.message,
|
||||
result: null,
|
||||
id: data ? data.id : null
|
||||
};
|
||||
self.socket.send(JSON.stringify(err));
|
||||
console.error(exception);
|
||||
}
|
||||
};
|
||||
|
||||
return ws;
|
||||
}
|
||||
|
||||
function makeCall(func, args, params) {
|
||||
self.id += 2;
|
||||
var deferred = new Deferred();
|
||||
var callObj = Object.freeze({
|
||||
id: self.id,
|
||||
method: func,
|
||||
params: args
|
||||
});
|
||||
var state = self.public.state();
|
||||
|
||||
if (state === 'OPEN') {
|
||||
self.store[self.id] = deferred;
|
||||
self.socket.send(JSON.stringify(callObj));
|
||||
} else if (state === 'CONNECTING') {
|
||||
log('SOCKET IS', state);
|
||||
self.store[self.id] = deferred;
|
||||
self.callQueue.push(callObj);
|
||||
} else {
|
||||
log('SOCKET IS', state);
|
||||
|
||||
if (params && params['noWait']) {
|
||||
deferred.reject("Socket is: ".concat(state));
|
||||
} else {
|
||||
self.store[self.id] = deferred;
|
||||
self.callQueue.push(callObj);
|
||||
}
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
self.asyncRoutes = {};
|
||||
self.routes = {};
|
||||
self.store = {};
|
||||
self.public = Object.freeze({
|
||||
call: function call(func, args, params) {
|
||||
return makeCall(func, args, params);
|
||||
},
|
||||
addRoute: function addRoute(route, callback, isAsync) {
|
||||
self.asyncRoutes[route] = isAsync || false;
|
||||
self.routes[route] = callback;
|
||||
},
|
||||
deleteRoute: function deleteRoute(route) {
|
||||
delete self.asyncRoutes[route];
|
||||
return delete self.routes[route];
|
||||
},
|
||||
addEventListener: function addEventListener(event, func) {
|
||||
var eventId = self.eventId++;
|
||||
self.eventStore[event][eventId] = func;
|
||||
return eventId;
|
||||
},
|
||||
removeEventListener: function removeEventListener(event, index) {
|
||||
if (self.eventStore[event].hasOwnProperty(index)) {
|
||||
delete self.eventStore[event][index];
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
onEvent: function onEvent(event) {
|
||||
var deferred = new Deferred();
|
||||
self.oneTimeEventStore[event].push(deferred);
|
||||
return deferred.promise;
|
||||
},
|
||||
destroy: function destroy() {
|
||||
return self.socket.close();
|
||||
},
|
||||
state: function state() {
|
||||
return readyState[this.stateCode()];
|
||||
},
|
||||
stateCode: function stateCode() {
|
||||
if (self.socketStarted && self.socket) return self.socket.readyState;
|
||||
return 3;
|
||||
},
|
||||
connect: function connect() {
|
||||
self.socketStarted = true;
|
||||
self.socket = createSocket();
|
||||
}
|
||||
});
|
||||
self.public.addRoute('log', function (argsObj) {
|
||||
//console.info("Websocket sent: ".concat(argsObj));
|
||||
});
|
||||
self.public.addRoute('ping', function (data) {
|
||||
return data;
|
||||
});
|
||||
return self.public;
|
||||
};
|
||||
|
||||
WSRPC.DEBUG = false;
|
||||
WSRPC.TRACE = false;
|
||||
|
||||
return WSRPC;
|
||||
|
||||
}));
|
||||
//# sourceMappingURL=wsrpc.js.map
|
||||
|
|
@ -1,412 +0,0 @@
|
|||
/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true,
|
||||
indent: 4, maxerr: 50 */
|
||||
/*global $, window, location, CSInterface, SystemPath, themeManager*/
|
||||
|
||||
|
||||
var csInterface = new CSInterface();
|
||||
|
||||
log.warn("script start");
|
||||
|
||||
WSRPC.DEBUG = false;
|
||||
WSRPC.TRACE = false;
|
||||
|
||||
// get websocket server url from environment value
|
||||
async function startUp(url){
|
||||
promis = runEvalScript("getEnv('" + url + "')");
|
||||
|
||||
var res = await promis;
|
||||
log.warn("res: " + res);
|
||||
|
||||
promis = runEvalScript("getEnv('AYON_DEBUG')");
|
||||
var debug = await promis;
|
||||
log.warn("debug: " + debug);
|
||||
if (debug && debug.toString() == '3'){
|
||||
WSRPC.DEBUG = true;
|
||||
WSRPC.TRACE = true;
|
||||
}
|
||||
// run rest only after resolved promise
|
||||
main(res);
|
||||
}
|
||||
|
||||
function get_extension_version(){
|
||||
/** Returns version number from extension manifest.xml **/
|
||||
log.debug("get_extension_version")
|
||||
var path = csInterface.getSystemPath(SystemPath.EXTENSION);
|
||||
log.debug("extension path " + path);
|
||||
|
||||
var result = window.cep.fs.readFile(path + "/CSXS/manifest.xml");
|
||||
var version = undefined;
|
||||
if(result.err === 0){
|
||||
if (window.DOMParser) {
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(result.data.toString(),
|
||||
'text/xml');
|
||||
const children = xmlDoc.children;
|
||||
|
||||
for (let i = 0; i <= children.length; i++) {
|
||||
if (children[i] &&
|
||||
children[i].getAttribute('ExtensionBundleVersion')) {
|
||||
version =
|
||||
children[i].getAttribute('ExtensionBundleVersion');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return '{"result":"' + version + '"}'
|
||||
}
|
||||
|
||||
function main(websocket_url){
|
||||
// creates connection to 'websocket_url', registers routes
|
||||
var default_url = 'ws://localhost:8099/ws/';
|
||||
|
||||
if (websocket_url == ''){
|
||||
websocket_url = default_url;
|
||||
}
|
||||
RPC = new WSRPC(websocket_url, 5000); // spin connection
|
||||
|
||||
RPC.connect();
|
||||
|
||||
log.warn("connected");
|
||||
|
||||
RPC.addRoute('AfterEffects.open', function (data) {
|
||||
log.warn('Server called client route "open":', data);
|
||||
var escapedPath = EscapeStringForJSX(data.path);
|
||||
return runEvalScript("fileOpen('" + escapedPath +"')")
|
||||
.then(function(result){
|
||||
log.warn("open: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.get_metadata', function (data) {
|
||||
log.warn('Server called client route "get_metadata":', data);
|
||||
return runEvalScript("getMetadata()")
|
||||
.then(function(result){
|
||||
log.warn("getMetadata: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.get_active_document_name', function (data) {
|
||||
log.warn('Server called client route ' +
|
||||
'"get_active_document_name":', data);
|
||||
return runEvalScript("getActiveDocumentName()")
|
||||
.then(function(result){
|
||||
log.warn("get_active_document_name: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.get_active_document_full_name', function (data){
|
||||
log.warn('Server called client route ' +
|
||||
'"get_active_document_full_name":', data);
|
||||
return runEvalScript("getActiveDocumentFullName()")
|
||||
.then(function(result){
|
||||
log.warn("get_active_document_full_name: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.add_item', function (data) {
|
||||
log.warn('Server called client route "add_item":', data);
|
||||
var escapedName = EscapeStringForJSX(data.name);
|
||||
return runEvalScript("addItem('" + escapedName +"', " +
|
||||
"'" + data.item_type + "')")
|
||||
.then(function(result){
|
||||
log.warn("get_items: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.get_items', function (data) {
|
||||
log.warn('Server called client route "get_items":', data);
|
||||
return runEvalScript("getItems(" + data.comps + "," +
|
||||
data.folders + "," +
|
||||
data.footages + ")")
|
||||
.then(function(result){
|
||||
log.warn("get_items: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.select_items', function (data) {
|
||||
log.warn('Server called client route "select_items":', data);
|
||||
return runEvalScript("selectItems(" + JSON.stringify(data.items) + ")")
|
||||
.then(function(result){
|
||||
log.warn("select_items: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
RPC.addRoute('AfterEffects.get_selected_items', function (data) {
|
||||
log.warn('Server called client route "get_selected_items":', data);
|
||||
return runEvalScript("getSelectedItems(" + data.comps + "," +
|
||||
data.folders + "," +
|
||||
data.footages + ")")
|
||||
.then(function(result){
|
||||
log.warn("get_items: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.import_file', function (data) {
|
||||
log.warn('Server called client route "import_file":', data);
|
||||
var escapedPath = EscapeStringForJSX(data.path);
|
||||
return runEvalScript("importFile('" + escapedPath +"', " +
|
||||
"'" + data.item_name + "'," +
|
||||
"'" + JSON.stringify(
|
||||
data.import_options) + "')")
|
||||
.then(function(result){
|
||||
log.warn("importFile: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.replace_item', function (data) {
|
||||
log.warn('Server called client route "replace_item":', data);
|
||||
var escapedPath = EscapeStringForJSX(data.path);
|
||||
return runEvalScript("replaceItem(" + data.item_id + ", " +
|
||||
"'" + escapedPath + "', " +
|
||||
"'" + data.item_name + "')")
|
||||
.then(function(result){
|
||||
log.warn("replaceItem: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.rename_item', function (data) {
|
||||
log.warn('Server called client route "rename_item":', data);
|
||||
return runEvalScript("renameItem(" + data.item_id + ", " +
|
||||
"'" + data.item_name + "')")
|
||||
.then(function(result){
|
||||
log.warn("renameItem: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.delete_item', function (data) {
|
||||
log.warn('Server called client route "delete_item":', data);
|
||||
return runEvalScript("deleteItem(" + data.item_id + ")")
|
||||
.then(function(result){
|
||||
log.warn("deleteItem: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.imprint', function (data) {
|
||||
log.warn('Server called client route "imprint":', data);
|
||||
var escaped = data.payload.replace(/\n/g, "\\n");
|
||||
return runEvalScript("imprint('" + escaped +"')")
|
||||
.then(function(result){
|
||||
log.warn("imprint: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.set_label_color', function (data) {
|
||||
log.warn('Server called client route "set_label_color":', data);
|
||||
return runEvalScript("setLabelColor(" + data.item_id + "," +
|
||||
data.color_idx + ")")
|
||||
.then(function(result){
|
||||
log.warn("imprint: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.get_comp_properties', function (data) {
|
||||
log.warn('Server called client route "get_comp_properties":', data);
|
||||
return runEvalScript("getCompProperties(" + data.item_id + ")")
|
||||
.then(function(result){
|
||||
log.warn("get_comp_properties: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.set_comp_properties', function (data) {
|
||||
log.warn('Server called client route "set_work_area":', data);
|
||||
return runEvalScript("setCompProperties(" + data.item_id + ',' +
|
||||
data.start + ',' +
|
||||
data.duration + ',' +
|
||||
data.frame_rate + ',' +
|
||||
data.width + ',' +
|
||||
data.height + ")")
|
||||
.then(function(result){
|
||||
log.warn("set_comp_properties: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.saveAs', function (data) {
|
||||
log.warn('Server called client route "saveAs":', data);
|
||||
var escapedPath = EscapeStringForJSX(data.image_path);
|
||||
return runEvalScript("saveAs('" + escapedPath + "', " +
|
||||
data.as_copy + ")")
|
||||
.then(function(result){
|
||||
log.warn("saveAs: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.save', function (data) {
|
||||
log.warn('Server called client route "save":', data);
|
||||
return runEvalScript("save()")
|
||||
.then(function(result){
|
||||
log.warn("save: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.get_render_info', function (data) {
|
||||
log.warn('Server called client route "get_render_info":', data);
|
||||
return runEvalScript("getRenderInfo(" + data.comp_id +")")
|
||||
.then(function(result){
|
||||
log.warn("get_render_info: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.get_audio_url', function (data) {
|
||||
log.warn('Server called client route "get_audio_url":', data);
|
||||
return runEvalScript("getAudioUrlForComp(" + data.item_id + ")")
|
||||
.then(function(result){
|
||||
log.warn("getAudioUrlForComp: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.import_background', function (data) {
|
||||
log.warn('Server called client route "import_background":', data);
|
||||
return runEvalScript("importBackground(" + data.comp_id + ", " +
|
||||
"'" + data.comp_name + "', " +
|
||||
JSON.stringify(data.files) + ")")
|
||||
.then(function(result){
|
||||
log.warn("importBackground: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.reload_background', function (data) {
|
||||
log.warn('Server called client route "reload_background":', data);
|
||||
return runEvalScript("reloadBackground(" + data.comp_id + ", " +
|
||||
"'" + data.comp_name + "', " +
|
||||
JSON.stringify(data.files) + ")")
|
||||
.then(function(result){
|
||||
log.warn("reloadBackground: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.add_item_as_layer', function (data) {
|
||||
log.warn('Server called client route "add_item_as_layer":', data);
|
||||
return runEvalScript("addItemAsLayerToComp(" + data.comp_id + ", " +
|
||||
data.item_id + "," +
|
||||
" null )")
|
||||
.then(function(result){
|
||||
log.warn("addItemAsLayerToComp: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.add_item_instead_placeholder', function (data) {
|
||||
log.warn('Server called client route "add_item_instead_placeholder":', data);
|
||||
return runEvalScript("addItemInstead(" + data.placeholder_item_id + ", " +
|
||||
data.item_id + ")")
|
||||
.then(function(result){
|
||||
log.warn("add_item_instead_placeholder: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.render', function (data) {
|
||||
log.warn('Server called client route "render":', data);
|
||||
var escapedPath = EscapeStringForJSX(data.folder_url);
|
||||
return runEvalScript("render('" + escapedPath +"', " + data.comp_id + ")")
|
||||
.then(function(result){
|
||||
log.warn("render: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.get_extension_version', function (data) {
|
||||
log.warn('Server called client route "get_extension_version":', data);
|
||||
return get_extension_version();
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.get_app_version', function (data) {
|
||||
log.warn('Server called client route "get_app_version":', data);
|
||||
return runEvalScript("getAppVersion()")
|
||||
.then(function(result){
|
||||
log.warn("get_app_version: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.add_placeholder', function (data) {
|
||||
log.warn('Server called client route "add_placeholder":', data);
|
||||
var escapedName = EscapeStringForJSX(data.name);
|
||||
return runEvalScript("addPlaceholder('" + escapedName +"',"+
|
||||
data.width + ',' +
|
||||
data.height + ',' +
|
||||
data.fps + ',' +
|
||||
data.duration + ")")
|
||||
.then(function(result){
|
||||
log.warn("add_placeholder: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.close', function (data) {
|
||||
log.warn('Server called client route "close":', data);
|
||||
return runEvalScript("close()");
|
||||
});
|
||||
|
||||
RPC.addRoute('AfterEffects.print_msg', function (data) {
|
||||
log.warn('Server called client route "print_msg":', data);
|
||||
var escaped_msg = EscapeStringForJSX(data.msg);
|
||||
return runEvalScript("printMsg('" + escaped_msg +"')")
|
||||
.then(function(result){
|
||||
log.warn("print_msg: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** main entry point **/
|
||||
startUp("WEBSOCKET_URL");
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var csInterface = new CSInterface();
|
||||
|
||||
|
||||
function init() {
|
||||
|
||||
themeManager.init();
|
||||
|
||||
$("#btn_test").click(function () {
|
||||
csInterface.evalScript('sayHello()');
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
}());
|
||||
|
||||
function EscapeStringForJSX(str){
|
||||
// Replaces:
|
||||
// \ with \\
|
||||
// ' with \'
|
||||
// " with \"
|
||||
// See: https://stackoverflow.com/a/3967927/5285364
|
||||
return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g,'\\"');
|
||||
}
|
||||
|
||||
function runEvalScript(script) {
|
||||
// because of asynchronous nature of functions in jsx
|
||||
// this waits for response
|
||||
return new Promise(function(resolve, reject){
|
||||
csInterface.evalScript(script, resolve);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */
|
||||
/*global window, document, CSInterface*/
|
||||
|
||||
|
||||
/*
|
||||
|
||||
Responsible for overwriting CSS at runtime according to CC app
|
||||
settings as defined by the end user.
|
||||
|
||||
*/
|
||||
|
||||
var themeManager = (function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Convert the Color object to string in hexadecimal format;
|
||||
*/
|
||||
function toHex(color, delta) {
|
||||
|
||||
function computeValue(value, delta) {
|
||||
var computedValue = !isNaN(delta) ? value + delta : value;
|
||||
if (computedValue < 0) {
|
||||
computedValue = 0;
|
||||
} else if (computedValue > 255) {
|
||||
computedValue = 255;
|
||||
}
|
||||
|
||||
computedValue = Math.floor(computedValue);
|
||||
|
||||
computedValue = computedValue.toString(16);
|
||||
return computedValue.length === 1 ? "0" + computedValue : computedValue;
|
||||
}
|
||||
|
||||
var hex = "";
|
||||
if (color) {
|
||||
hex = computeValue(color.red, delta) + computeValue(color.green, delta) + computeValue(color.blue, delta);
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
|
||||
function reverseColor(color, delta) {
|
||||
return toHex({
|
||||
red: Math.abs(255 - color.red),
|
||||
green: Math.abs(255 - color.green),
|
||||
blue: Math.abs(255 - color.blue)
|
||||
},
|
||||
delta);
|
||||
}
|
||||
|
||||
|
||||
function addRule(stylesheetId, selector, rule) {
|
||||
var stylesheet = document.getElementById(stylesheetId);
|
||||
|
||||
if (stylesheet) {
|
||||
stylesheet = stylesheet.sheet;
|
||||
if (stylesheet.addRule) {
|
||||
stylesheet.addRule(selector, rule);
|
||||
} else if (stylesheet.insertRule) {
|
||||
stylesheet.insertRule(selector + ' { ' + rule + ' }', stylesheet.cssRules.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Update the theme with the AppSkinInfo retrieved from the host product.
|
||||
*/
|
||||
function updateThemeWithAppSkinInfo(appSkinInfo) {
|
||||
|
||||
var panelBgColor = appSkinInfo.panelBackgroundColor.color;
|
||||
var bgdColor = toHex(panelBgColor);
|
||||
|
||||
var darkBgdColor = toHex(panelBgColor, 20);
|
||||
|
||||
var fontColor = "F0F0F0";
|
||||
if (panelBgColor.red > 122) {
|
||||
fontColor = "000000";
|
||||
}
|
||||
var lightBgdColor = toHex(panelBgColor, -100);
|
||||
|
||||
var styleId = "hostStyle";
|
||||
|
||||
addRule(styleId, ".hostElt", "background-color:" + "#" + bgdColor);
|
||||
addRule(styleId, ".hostElt", "font-size:" + appSkinInfo.baseFontSize + "px;");
|
||||
addRule(styleId, ".hostElt", "font-family:" + appSkinInfo.baseFontFamily);
|
||||
addRule(styleId, ".hostElt", "color:" + "#" + fontColor);
|
||||
|
||||
addRule(styleId, ".hostBgd", "background-color:" + "#" + bgdColor);
|
||||
addRule(styleId, ".hostBgdDark", "background-color: " + "#" + darkBgdColor);
|
||||
addRule(styleId, ".hostBgdLight", "background-color: " + "#" + lightBgdColor);
|
||||
addRule(styleId, ".hostFontSize", "font-size:" + appSkinInfo.baseFontSize + "px;");
|
||||
addRule(styleId, ".hostFontFamily", "font-family:" + appSkinInfo.baseFontFamily);
|
||||
addRule(styleId, ".hostFontColor", "color:" + "#" + fontColor);
|
||||
|
||||
addRule(styleId, ".hostFont", "font-size:" + appSkinInfo.baseFontSize + "px;");
|
||||
addRule(styleId, ".hostFont", "font-family:" + appSkinInfo.baseFontFamily);
|
||||
addRule(styleId, ".hostFont", "color:" + "#" + fontColor);
|
||||
|
||||
addRule(styleId, ".hostButton", "background-color:" + "#" + darkBgdColor);
|
||||
addRule(styleId, ".hostButton:hover", "background-color:" + "#" + bgdColor);
|
||||
addRule(styleId, ".hostButton:active", "background-color:" + "#" + darkBgdColor);
|
||||
addRule(styleId, ".hostButton", "border-color: " + "#" + lightBgdColor);
|
||||
|
||||
}
|
||||
|
||||
|
||||
function onAppThemeColorChanged(event) {
|
||||
var skinInfo = JSON.parse(window.__adobe_cep__.getHostEnvironment()).appSkinInfo;
|
||||
updateThemeWithAppSkinInfo(skinInfo);
|
||||
}
|
||||
|
||||
|
||||
function init() {
|
||||
|
||||
var csInterface = new CSInterface();
|
||||
|
||||
updateThemeWithAppSkinInfo(csInterface.hostEnvironment.appSkinInfo);
|
||||
|
||||
csInterface.addEventListener(CSInterface.THEME_COLOR_CHANGED_EVENT, onAppThemeColorChanged);
|
||||
}
|
||||
|
||||
return {
|
||||
init: init
|
||||
};
|
||||
|
||||
}());
|
||||
|
|
@ -1,946 +0,0 @@
|
|||
/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true,
|
||||
indent: 4, maxerr: 50 */
|
||||
/*global $, Folder*/
|
||||
//@include "../js/libs/json.js"
|
||||
|
||||
/* All public API function should return JSON! */
|
||||
|
||||
app.preferences.savePrefAsBool("General Section", "Show Welcome Screen", false) ;
|
||||
|
||||
if(!Array.prototype.indexOf) {
|
||||
Array.prototype.indexOf = function ( item ) {
|
||||
var index = 0, length = this.length;
|
||||
for ( ; index < length; index++ ) {
|
||||
if ( this[index] === item )
|
||||
return index;
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
}
|
||||
|
||||
function sayHello(){
|
||||
alert("hello from ExtendScript");
|
||||
}
|
||||
|
||||
function getEnv(variable){
|
||||
return $.getenv(variable);
|
||||
}
|
||||
|
||||
function getMetadata(){
|
||||
/**
|
||||
* Returns payload in 'Label' field of project's metadata
|
||||
*
|
||||
**/
|
||||
if (ExternalObject.AdobeXMPScript === undefined){
|
||||
ExternalObject.AdobeXMPScript =
|
||||
new ExternalObject('lib:AdobeXMPScript');
|
||||
}
|
||||
|
||||
var proj = app.project;
|
||||
var meta = new XMPMeta(app.project.xmpPacket);
|
||||
var schemaNS = XMPMeta.getNamespaceURI("xmp");
|
||||
var label = "xmp:Label";
|
||||
|
||||
if (meta.doesPropertyExist(schemaNS, label)){
|
||||
var prop = meta.getProperty(schemaNS, label);
|
||||
return prop.value;
|
||||
}
|
||||
|
||||
return _prepareSingleValue([]);
|
||||
|
||||
}
|
||||
|
||||
function imprint(payload){
|
||||
/**
|
||||
* Stores payload in 'Label' field of project's metadata
|
||||
*
|
||||
* Args:
|
||||
* payload (string): json content
|
||||
*/
|
||||
if (ExternalObject.AdobeXMPScript === undefined){
|
||||
ExternalObject.AdobeXMPScript =
|
||||
new ExternalObject('lib:AdobeXMPScript');
|
||||
}
|
||||
|
||||
var proj = app.project;
|
||||
var meta = new XMPMeta(app.project.xmpPacket);
|
||||
var schemaNS = XMPMeta.getNamespaceURI("xmp");
|
||||
var label = "xmp:Label";
|
||||
|
||||
meta.setProperty(schemaNS, label, payload);
|
||||
|
||||
app.project.xmpPacket = meta.serialize();
|
||||
|
||||
}
|
||||
|
||||
|
||||
function fileOpen(path){
|
||||
/**
|
||||
* Opens (project) file on 'path'
|
||||
*/
|
||||
fp = new File(path);
|
||||
return _prepareSingleValue(app.open(fp))
|
||||
}
|
||||
|
||||
function getActiveDocumentName(){
|
||||
/**
|
||||
* Returns file name of active document
|
||||
* */
|
||||
var file = app.project.file;
|
||||
|
||||
if (file){
|
||||
return _prepareSingleValue(file.name)
|
||||
}
|
||||
|
||||
return _prepareError("No file open currently");
|
||||
}
|
||||
|
||||
function getActiveDocumentFullName(){
|
||||
/**
|
||||
* Returns absolute path to current project
|
||||
* */
|
||||
var file = app.project.file;
|
||||
|
||||
if (file){
|
||||
var f = new File(file.fullName);
|
||||
var path = f.fsName;
|
||||
f.close();
|
||||
|
||||
return _prepareSingleValue(path)
|
||||
}
|
||||
|
||||
return _prepareError("No file open currently");
|
||||
}
|
||||
|
||||
|
||||
function addItem(name, item_type){
|
||||
/**
|
||||
* Adds comp or folder to project items.
|
||||
*
|
||||
* Could be called when creating publishable instance to prepare
|
||||
* composition (and render queue).
|
||||
*
|
||||
* Args:
|
||||
* name (str): composition name
|
||||
* item_type (str): COMP|FOLDER
|
||||
* Returns:
|
||||
* SingleItemValue: eg {"result": VALUE}
|
||||
*/
|
||||
if (item_type == "COMP"){
|
||||
// dummy values, will be rewritten later
|
||||
item = app.project.items.addComp(name, 1920, 1060, 1, 10, 25);
|
||||
}else if (item_type == "FOLDER"){
|
||||
item = app.project.items.addFolder(name);
|
||||
}else{
|
||||
return _prepareError("Only 'COMP' or 'FOLDER' can be created");
|
||||
}
|
||||
return _prepareSingleValue(item.id);
|
||||
|
||||
}
|
||||
|
||||
function getItems(comps, folders, footages){
|
||||
/**
|
||||
* Returns JSON representation of compositions and
|
||||
* if 'collectLayers' then layers in comps too.
|
||||
*
|
||||
* Args:
|
||||
* comps (bool): return selected compositions
|
||||
* folders (bool): return folders
|
||||
* footages (bool): return FootageItem
|
||||
* Returns:
|
||||
* (list) of JSON items
|
||||
*/
|
||||
var items = []
|
||||
for (i = 1; i <= app.project.items.length; ++i){
|
||||
var item = app.project.items[i];
|
||||
if (!item){
|
||||
continue;
|
||||
}
|
||||
var ret = _getItem(item, comps, folders, footages);
|
||||
if (ret){
|
||||
items.push(ret);
|
||||
}
|
||||
}
|
||||
return '[' + items.join() + ']';
|
||||
|
||||
}
|
||||
|
||||
function selectItems(items){
|
||||
/**
|
||||
* Select all items from `items`, deselect other.
|
||||
*
|
||||
* Args:
|
||||
* items (list)
|
||||
*/
|
||||
for (i = 1; i <= app.project.items.length; ++i){
|
||||
item = app.project.items[i];
|
||||
if (items.indexOf(item.id) > -1){
|
||||
item.selected = true;
|
||||
}else{
|
||||
item.selected = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function getSelectedItems(comps, folders, footages){
|
||||
/**
|
||||
* Returns list of selected items from Project menu
|
||||
*
|
||||
* Args:
|
||||
* comps (bool): return selected compositions
|
||||
* folders (bool): return folders
|
||||
* footages (bool): return FootageItem
|
||||
* Returns:
|
||||
* (list) of JSON items
|
||||
*/
|
||||
var items = []
|
||||
for (i = 0; i < app.project.selection.length; ++i){
|
||||
var item = app.project.selection[i];
|
||||
if (!item){
|
||||
continue;
|
||||
}
|
||||
var ret = _getItem(item, comps, folders, footages);
|
||||
if (ret){
|
||||
items.push(ret);
|
||||
}
|
||||
}
|
||||
return '[' + items.join() + ']';
|
||||
}
|
||||
|
||||
function _getItem(item, comps, folders, footages){
|
||||
/**
|
||||
* Auxiliary function as project items and selections
|
||||
* are indexed in different way :/
|
||||
* Refactor
|
||||
*/
|
||||
var item_type = '';
|
||||
var path = '';
|
||||
var containing_comps = [];
|
||||
if (item instanceof FolderItem){
|
||||
item_type = 'folder';
|
||||
if (!folders){
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
if (item instanceof FootageItem){
|
||||
if (!footages){
|
||||
return "{}";
|
||||
}
|
||||
item_type = 'footage';
|
||||
if (item.file){
|
||||
path = item.file.fsName;
|
||||
}
|
||||
if (item.usedIn){
|
||||
for (j = 0; j < item.usedIn.length; ++j){
|
||||
containing_comps.push(item.usedIn[j].id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (item instanceof CompItem){
|
||||
item_type = 'comp';
|
||||
if (!comps){
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
|
||||
var item = {"name": item.name,
|
||||
"id": item.id,
|
||||
"type": item_type,
|
||||
"path": path,
|
||||
"containing_comps": containing_comps};
|
||||
return JSON.stringify(item);
|
||||
}
|
||||
|
||||
function importFile(path, item_name, import_options){
|
||||
/**
|
||||
* Imports file (image tested for now) as a FootageItem.
|
||||
* Creates new composition
|
||||
*
|
||||
* Args:
|
||||
* path (string): absolute path to image file
|
||||
* item_name (string): label for composition
|
||||
* Returns:
|
||||
* JSON {name, id}
|
||||
*/
|
||||
var comp;
|
||||
var ret = {};
|
||||
try{
|
||||
import_options = JSON.parse(import_options);
|
||||
} catch (e){
|
||||
return _prepareError("Couldn't parse import options " + import_options);
|
||||
}
|
||||
|
||||
app.beginUndoGroup("Import File");
|
||||
fp = new File(path);
|
||||
if (fp.exists){
|
||||
try {
|
||||
im_opt = new ImportOptions(fp);
|
||||
importAsType = import_options["ImportAsType"];
|
||||
|
||||
if ('ImportAsType' in import_options){ // refactor
|
||||
if (importAsType.indexOf('COMP') > 0){
|
||||
im_opt.importAs = ImportAsType.COMP;
|
||||
}
|
||||
if (importAsType.indexOf('FOOTAGE') > 0){
|
||||
im_opt.importAs = ImportAsType.FOOTAGE;
|
||||
}
|
||||
if (importAsType.indexOf('COMP_CROPPED_LAYERS') > 0){
|
||||
im_opt.importAs = ImportAsType.COMP_CROPPED_LAYERS;
|
||||
}
|
||||
if (importAsType.indexOf('PROJECT') > 0){
|
||||
im_opt.importAs = ImportAsType.PROJECT;
|
||||
}
|
||||
|
||||
}
|
||||
if ('sequence' in import_options){
|
||||
im_opt.sequence = true;
|
||||
}
|
||||
|
||||
comp = app.project.importFile(im_opt);
|
||||
|
||||
if (app.project.selection.length == 2 &&
|
||||
app.project.selection[0] instanceof FolderItem){
|
||||
comp.parentFolder = app.project.selection[0]
|
||||
}
|
||||
} catch (error) {
|
||||
return _prepareError(error.toString() + importOptions.file.fsName);
|
||||
} finally {
|
||||
fp.close();
|
||||
}
|
||||
}else{
|
||||
return _prepareError("File " + path + " not found.");
|
||||
}
|
||||
if (comp){
|
||||
comp.name = item_name;
|
||||
comp.label = 9; // Green
|
||||
ret = {"name": comp.name, "id": comp.id}
|
||||
}
|
||||
app.endUndoGroup();
|
||||
|
||||
return JSON.stringify(ret);
|
||||
}
|
||||
|
||||
function setLabelColor(comp_id, color_idx){
|
||||
/**
|
||||
* Set item_id label to 'color_idx' color
|
||||
* Args:
|
||||
* item_id (int): item id
|
||||
* color_idx (int): 0-16 index from Label
|
||||
*/
|
||||
var item = app.project.itemByID(comp_id);
|
||||
if (item){
|
||||
item.label = color_idx;
|
||||
}else{
|
||||
return _prepareError("There is no composition with "+ comp_id);
|
||||
}
|
||||
}
|
||||
|
||||
function replaceItem(item_id, path, item_name){
|
||||
/**
|
||||
* Replaces loaded file with new file and updates name
|
||||
*
|
||||
* Args:
|
||||
* item_id (int): id of composition, not a index!
|
||||
* path (string): absolute path to new file
|
||||
* item_name (string): new composition name
|
||||
*/
|
||||
app.beginUndoGroup("Replace File");
|
||||
|
||||
fp = new File(path);
|
||||
if (!fp.exists){
|
||||
return _prepareError("File " + path + " not found.");
|
||||
}
|
||||
var item = app.project.itemByID(item_id);
|
||||
if (item){
|
||||
try{
|
||||
if (isFileSequence(item)) {
|
||||
item.replaceWithSequence(fp, false);
|
||||
}else{
|
||||
item.replace(fp);
|
||||
}
|
||||
|
||||
item.name = item_name;
|
||||
} catch (error) {
|
||||
return _prepareError(error.toString() + path);
|
||||
} finally {
|
||||
fp.close();
|
||||
}
|
||||
}else{
|
||||
return _prepareError("There is no item with "+ item_id);
|
||||
}
|
||||
app.endUndoGroup();
|
||||
}
|
||||
|
||||
function renameItem(item_id, new_name){
|
||||
/**
|
||||
* Renames item with 'item_id' to 'new_name'
|
||||
*
|
||||
* Args:
|
||||
* item_id (int): id to search item
|
||||
* new_name (str)
|
||||
*/
|
||||
var item = app.project.itemByID(item_id);
|
||||
if (item){
|
||||
item.name = new_name;
|
||||
}else{
|
||||
return _prepareError("There is no composition with "+ comp_id);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteItem(item_id){
|
||||
/**
|
||||
* Delete any 'item_id'
|
||||
*
|
||||
* Not restricted only to comp, it could delete
|
||||
* any item with 'id'
|
||||
*/
|
||||
var item = app.project.itemByID(item_id);
|
||||
if (item){
|
||||
item.remove();
|
||||
}else{
|
||||
return _prepareError("There is no composition with "+ comp_id);
|
||||
}
|
||||
}
|
||||
|
||||
function getCompProperties(comp_id){
|
||||
/**
|
||||
* Returns information about composition - are that will be
|
||||
* rendered.
|
||||
*
|
||||
* Returns
|
||||
* (dict)
|
||||
*/
|
||||
var comp = app.project.itemByID(comp_id);
|
||||
if (!comp){
|
||||
return _prepareError("There is no composition with "+ comp_id);
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
"id": comp.id,
|
||||
"name": comp.name,
|
||||
"frameStart": comp.displayStartFrame,
|
||||
"framesDuration": comp.duration * comp.frameRate,
|
||||
"frameRate": comp.frameRate,
|
||||
"width": comp.width,
|
||||
"height": comp.height});
|
||||
}
|
||||
|
||||
function setCompProperties(comp_id, frameStart, framesCount, frameRate,
|
||||
width, height){
|
||||
/**
|
||||
* Sets work area info from outside (from Ftrack via OpenPype)
|
||||
*/
|
||||
var comp = app.project.itemByID(comp_id);
|
||||
if (!comp){
|
||||
return _prepareError("There is no composition with "+ comp_id);
|
||||
}
|
||||
|
||||
app.beginUndoGroup('change comp properties');
|
||||
if (frameStart && framesCount && frameRate){
|
||||
comp.displayStartFrame = frameStart;
|
||||
comp.duration = framesCount / frameRate;
|
||||
comp.frameRate = frameRate;
|
||||
}
|
||||
if (width && height){
|
||||
var widthOld = comp.width;
|
||||
var widthNew = width;
|
||||
var widthDelta = widthNew - widthOld;
|
||||
|
||||
var heightOld = comp.height;
|
||||
var heightNew = height;
|
||||
var heightDelta = heightNew - heightOld;
|
||||
|
||||
var offset = [widthDelta / 2, heightDelta / 2];
|
||||
|
||||
comp.width = widthNew;
|
||||
comp.height = heightNew;
|
||||
|
||||
for (var i = 1, il = comp.numLayers; i <= il; i++) {
|
||||
var layer = comp.layer(i);
|
||||
var positionProperty = layer.property('ADBE Transform Group').property('ADBE Position');
|
||||
|
||||
if (positionProperty.numKeys > 0) {
|
||||
for (var j = 1, jl = positionProperty.numKeys; j <= jl; j++) {
|
||||
var keyValue = positionProperty.keyValue(j);
|
||||
positionProperty.setValueAtKey(j, keyValue + offset);
|
||||
}
|
||||
} else {
|
||||
var positionValue = positionProperty.value;
|
||||
positionProperty.setValue(positionValue + offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.endUndoGroup();
|
||||
}
|
||||
|
||||
function save(){
|
||||
/**
|
||||
* Saves current project
|
||||
*/
|
||||
app.project.save(); //TODO path is wrong, File instead
|
||||
}
|
||||
|
||||
function saveAs(path){
|
||||
/**
|
||||
* Saves current project as 'path'
|
||||
* */
|
||||
app.project.save(fp = new File(path));
|
||||
}
|
||||
|
||||
function getRenderInfo(comp_id){
|
||||
/***
|
||||
Get info from render queue.
|
||||
Currently pulls only file name to parse extension and
|
||||
if it is sequence in Python
|
||||
Args:
|
||||
comp_id (int): id of composition
|
||||
Return:
|
||||
(list) [{file_name:"xx.png", width:00, height:00}]
|
||||
**/
|
||||
var item = app.project.itemByID(comp_id);
|
||||
if (!item){
|
||||
return _prepareError("Composition with '" + comp_id + "' wasn't found! Recreate publishable instance(s)")
|
||||
}
|
||||
|
||||
var comp_name = item.name;
|
||||
var output_metadata = []
|
||||
try{
|
||||
// render_item.duplicate() should create new item on renderQueue
|
||||
// BUT it works only sometimes, there are some weird synchronization issue
|
||||
// this method will be called always before render, so prepare items here
|
||||
// for render to spare the hassle
|
||||
for (i = 1; i <= app.project.renderQueue.numItems; ++i){
|
||||
var render_item = app.project.renderQueue.item(i);
|
||||
if (render_item.comp.id != comp_id){
|
||||
continue;
|
||||
}
|
||||
|
||||
if (render_item.status == RQItemStatus.DONE){
|
||||
render_item.duplicate(); // create new, cannot change status if DONE
|
||||
render_item.remove(); // remove existing to limit duplications
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// properly validate as `numItems` won't change magically
|
||||
var comp_id_count = 0;
|
||||
for (i = 1; i <= app.project.renderQueue.numItems; ++i){
|
||||
var render_item = app.project.renderQueue.item(i);
|
||||
if (render_item.comp.id != comp_id){
|
||||
continue;
|
||||
}
|
||||
comp_id_count += 1;
|
||||
var item = render_item.outputModule(1);
|
||||
|
||||
for (j = 1; j<= render_item.numOutputModules; ++j){
|
||||
var file_url = item.file.toString();
|
||||
output_metadata.push(
|
||||
JSON.stringify({
|
||||
"file_name": file_url,
|
||||
"width": render_item.comp.width,
|
||||
"height": render_item.comp.height
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return _prepareError("There is no render queue, create one");
|
||||
}
|
||||
|
||||
if (comp_id_count > 1){
|
||||
return _prepareError("There cannot be more items in Render Queue for '" + comp_name + "'!")
|
||||
}
|
||||
|
||||
if (comp_id_count == 0){
|
||||
return _prepareError("There is no item in Render Queue for '" + comp_name + "'! Add composition to Render Queue.")
|
||||
}
|
||||
|
||||
return '[' + output_metadata.join() + ']';
|
||||
}
|
||||
|
||||
function getAudioUrlForComp(comp_id){
|
||||
/**
|
||||
* Searches composition for audio layer
|
||||
*
|
||||
* Only single AVLayer is expected!
|
||||
* Used for collecting Audio
|
||||
*
|
||||
* Args:
|
||||
* comp_id (int): id of composition
|
||||
* Return:
|
||||
* (str) with url to audio content
|
||||
*/
|
||||
var item = app.project.itemByID(comp_id);
|
||||
if (item){
|
||||
for (i = 1; i <= item.numLayers; ++i){
|
||||
var layer = item.layers[i];
|
||||
if (layer instanceof AVLayer){
|
||||
if (layer.hasAudio){
|
||||
source_url = layer.source.file.fsName.toString()
|
||||
return _prepareSingleValue(source_url);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}else{
|
||||
return _prepareError("There is no composition with "+ comp_id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function addItemAsLayerToComp(comp_id, item_id, found_comp){
|
||||
/**
|
||||
* Adds already imported FootageItem ('item_id') as a new
|
||||
* layer to composition ('comp_id').
|
||||
*
|
||||
* Args:
|
||||
* comp_id (int): id of target composition
|
||||
* item_id (int): FootageItem.id
|
||||
* found_comp (CompItem, optional): to limit quering if
|
||||
* comp already found previously
|
||||
*/
|
||||
var comp = found_comp || app.project.itemByID(comp_id);
|
||||
if (comp){
|
||||
item = app.project.itemByID(item_id);
|
||||
if (item){
|
||||
comp.layers.add(item);
|
||||
}else{
|
||||
return _prepareError("There is no item with " + item_id);
|
||||
}
|
||||
}else{
|
||||
return _prepareError("There is no composition with "+ comp_id);
|
||||
}
|
||||
}
|
||||
|
||||
function importBackground(comp_id, composition_name, files_to_import){
|
||||
/**
|
||||
* Imports backgrounds images to existing or new composition.
|
||||
*
|
||||
* If comp_id is not provided, new composition is created, basic
|
||||
* values (width, heights, frameRatio) takes from first imported
|
||||
* image.
|
||||
*
|
||||
* Args:
|
||||
* comp_id (int): id of existing composition (null if new)
|
||||
* composition_name (str): used when new composition
|
||||
* files_to_import (list): list of absolute paths to import and
|
||||
* add as layers
|
||||
*
|
||||
* Returns:
|
||||
* (str): json representation (id, name, members)
|
||||
*/
|
||||
var comp;
|
||||
var folder;
|
||||
var imported_ids = [];
|
||||
if (comp_id){
|
||||
comp = app.project.itemByID(comp_id);
|
||||
folder = comp.parentFolder;
|
||||
}else{
|
||||
if (app.project.selection.length > 1){
|
||||
return _prepareError(
|
||||
"Too many items selected, select only target composition!");
|
||||
}else{
|
||||
selected_item = app.project.activeItem;
|
||||
if (selected_item instanceof Folder){
|
||||
comp = selected_item;
|
||||
folder = selected_item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (files_to_import){
|
||||
for (i = 0; i < files_to_import.length; ++i){
|
||||
item = _importItem(files_to_import[i]);
|
||||
if (!item){
|
||||
return _prepareError(
|
||||
"No item for " + item_json["id"] +
|
||||
". Import background failed.")
|
||||
}
|
||||
if (!comp){
|
||||
folder = app.project.items.addFolder(composition_name);
|
||||
imported_ids.push(folder.id);
|
||||
comp = app.project.items.addComp(composition_name, item.width,
|
||||
item.height, item.pixelAspect,
|
||||
1, 26.7); // hardcode defaults
|
||||
imported_ids.push(comp.id);
|
||||
comp.parentFolder = folder;
|
||||
}
|
||||
imported_ids.push(item.id)
|
||||
item.parentFolder = folder;
|
||||
|
||||
addItemAsLayerToComp(comp.id, item.id, comp);
|
||||
}
|
||||
}
|
||||
var item = {"name": comp.name,
|
||||
"id": folder.id,
|
||||
"members": imported_ids};
|
||||
return JSON.stringify(item);
|
||||
}
|
||||
|
||||
function reloadBackground(comp_id, composition_name, files_to_import){
|
||||
/**
|
||||
* Reloads existing composition.
|
||||
*
|
||||
* It deletes complete composition with encompassing folder, recreates
|
||||
* from scratch via 'importBackground' functionality.
|
||||
*
|
||||
* Args:
|
||||
* comp_id (int): id of existing composition (null if new)
|
||||
* composition_name (str): used when new composition
|
||||
* files_to_import (list): list of absolute paths to import and
|
||||
* add as layers
|
||||
*
|
||||
* Returns:
|
||||
* (str): json representation (id, name, members)
|
||||
*
|
||||
*/
|
||||
var imported_ids = []; // keep track of members of composition
|
||||
comp = app.project.itemByID(comp_id);
|
||||
folder = comp.parentFolder;
|
||||
if (folder){
|
||||
renameItem(folder.id, composition_name);
|
||||
imported_ids.push(folder.id);
|
||||
}
|
||||
if (comp){
|
||||
renameItem(comp.id, composition_name);
|
||||
imported_ids.push(comp.id);
|
||||
}
|
||||
|
||||
var existing_layer_names = [];
|
||||
var existing_layer_ids = []; // because ExtendedScript doesnt have keys()
|
||||
for (i = 1; i <= folder.items.length; ++i){
|
||||
layer = folder.items[i];
|
||||
//because comp.layers[i] doesnt have 'id' accessible
|
||||
if (layer instanceof CompItem){
|
||||
continue;
|
||||
}
|
||||
existing_layer_names.push(layer.name);
|
||||
existing_layer_ids.push(layer.id);
|
||||
}
|
||||
|
||||
var new_filenames = [];
|
||||
if (files_to_import){
|
||||
for (i = 0; i < files_to_import.length; ++i){
|
||||
file_name = _get_file_name(files_to_import[i]);
|
||||
new_filenames.push(file_name);
|
||||
|
||||
idx = existing_layer_names.indexOf(file_name);
|
||||
if (idx >= 0){ // update
|
||||
var layer_id = existing_layer_ids[idx];
|
||||
replaceItem(layer_id, files_to_import[i], file_name);
|
||||
imported_ids.push(layer_id);
|
||||
}else{ // new layer
|
||||
item = _importItem(files_to_import[i]);
|
||||
if (!item){
|
||||
return _prepareError(
|
||||
"No item for " + files_to_import[i] +
|
||||
". Reload background failed.");
|
||||
}
|
||||
imported_ids.push(item.id);
|
||||
item.parentFolder = folder;
|
||||
addItemAsLayerToComp(comp.id, item.id, comp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_delete_obsolete_items(folder, new_filenames);
|
||||
|
||||
var item = {"name": comp.name,
|
||||
"id": folder.id,
|
||||
"members": imported_ids};
|
||||
|
||||
return JSON.stringify(item);
|
||||
}
|
||||
|
||||
function _get_file_name(file_url){
|
||||
/**
|
||||
* Returns file name without extension from 'file_url'
|
||||
*
|
||||
* Args:
|
||||
* file_url (str): full absolute url
|
||||
* Returns:
|
||||
* (str)
|
||||
*/
|
||||
fp = new File(file_url);
|
||||
file_name = fp.name.substring(0, fp.name.lastIndexOf("."));
|
||||
return file_name;
|
||||
}
|
||||
|
||||
function _delete_obsolete_items(folder, new_filenames){
|
||||
/***
|
||||
* Goes through 'folder' and removes layers not in new
|
||||
* background
|
||||
*
|
||||
* Args:
|
||||
* folder (FolderItem)
|
||||
* new_filenames (array): list of layer names in new bg
|
||||
*/
|
||||
// remove items in old, but not in new
|
||||
delete_ids = []
|
||||
for (i = 1; i <= folder.items.length; ++i){
|
||||
layer = folder.items[i];
|
||||
//because comp.layers[i] doesnt have 'id' accessible
|
||||
if (layer instanceof CompItem){
|
||||
continue;
|
||||
}
|
||||
if (new_filenames.indexOf(layer.name) < 0){
|
||||
delete_ids.push(layer.id);
|
||||
}
|
||||
}
|
||||
for (i = 0; i < delete_ids.length; ++i){
|
||||
deleteItem(delete_ids[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function _importItem(file_url){
|
||||
/**
|
||||
* Imports 'file_url' as new FootageItem
|
||||
*
|
||||
* Args:
|
||||
* file_url (str): file url with content
|
||||
* Returns:
|
||||
* (FootageItem)
|
||||
*/
|
||||
file_name = _get_file_name(file_url);
|
||||
|
||||
//importFile prepared previously to return json
|
||||
item_json = importFile(file_url, file_name, JSON.stringify({"ImportAsType":"FOOTAGE"}));
|
||||
item_json = JSON.parse(item_json);
|
||||
item = app.project.itemByID(item_json["id"]);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
function isFileSequence (item){
|
||||
/**
|
||||
* Check that item is a recognizable sequence
|
||||
*/
|
||||
if (item instanceof FootageItem && item.mainSource instanceof FileSource && !(item.mainSource.isStill) && item.hasVideo){
|
||||
var extname = item.mainSource.file.fsName.split('.').pop();
|
||||
|
||||
return extname.match(new RegExp("(ai|bmp|bw|cin|cr2|crw|dcr|dng|dib|dpx|eps|erf|exr|gif|hdr|ico|icb|iff|jpe|jpeg|jpg|mos|mrw|nef|orf|pbm|pef|pct|pcx|pdf|pic|pict|png|ps|psd|pxr|raf|raw|rgb|rgbe|rla|rle|rpf|sgi|srf|tdi|tga|tif|tiff|vda|vst|x3f|xyze)", "i")) !== null;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function render(target_folder, comp_id){
|
||||
var out_dir = new Folder(target_folder);
|
||||
var out_dir = out_dir.fsName;
|
||||
for (i = 1; i <= app.project.renderQueue.numItems; ++i){
|
||||
var render_item = app.project.renderQueue.item(i);
|
||||
var composition = render_item.comp;
|
||||
if (composition.id == comp_id){
|
||||
if (render_item.status == RQItemStatus.DONE){
|
||||
var new_item = render_item.duplicate();
|
||||
render_item.remove();
|
||||
render_item = new_item;
|
||||
}
|
||||
|
||||
render_item.render = true;
|
||||
|
||||
var om1 = app.project.renderQueue.item(i).outputModule(1);
|
||||
var file_name = File.decode( om1.file.name ).replace('℗', ''); // Name contains special character, space?
|
||||
|
||||
var omItem1_settable_str = app.project.renderQueue.item(i).outputModule(1).getSettings( GetSettingsFormat.STRING_SETTABLE );
|
||||
|
||||
var targetFolder = new Folder(target_folder);
|
||||
if (!targetFolder.exists) {
|
||||
targetFolder.create();
|
||||
}
|
||||
|
||||
om1.file = new File(targetFolder.fsName + '/' + file_name);
|
||||
}else{
|
||||
if (render_item.status != RQItemStatus.DONE){
|
||||
render_item.render = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
app.beginSuppressDialogs();
|
||||
app.project.renderQueue.render();
|
||||
app.endSuppressDialogs(false);
|
||||
}
|
||||
|
||||
function close(){
|
||||
app.project.close(CloseOptions.DO_NOT_SAVE_CHANGES);
|
||||
app.quit();
|
||||
}
|
||||
|
||||
function getAppVersion(){
|
||||
return _prepareSingleValue(app.version);
|
||||
}
|
||||
|
||||
function printMsg(msg){
|
||||
alert(msg);
|
||||
}
|
||||
|
||||
function addPlaceholder(name, width, height, fps, duration){
|
||||
/** Add AE PlaceholderItem to Project list.
|
||||
*
|
||||
* PlaceholderItem chosen as it doesn't require existing file and
|
||||
* might potentially allow nice functionality in the future.
|
||||
*
|
||||
*/
|
||||
app.beginUndoGroup('change comp properties');
|
||||
try{
|
||||
item = app.project.importPlaceholder(name, width, height,
|
||||
fps, duration);
|
||||
|
||||
return _prepareSingleValue(item.id);
|
||||
}catch (error) {
|
||||
writeLn(_prepareError("Cannot add placeholder " + error.toString()));
|
||||
}
|
||||
app.endUndoGroup();
|
||||
}
|
||||
|
||||
function addItemInstead(placeholder_item_id, item_id){
|
||||
/** Add new loaded item in place of load placeholder.
|
||||
*
|
||||
* Each placeholder could be placed multiple times into multiple
|
||||
* composition. This loops through all compositions and
|
||||
* places loaded item under placeholder.
|
||||
* Placeholder item gets deleted later separately according
|
||||
* to configuration in Settings.
|
||||
*
|
||||
* Args:
|
||||
* placeholder_item_id (int)
|
||||
* item_id (int)
|
||||
*/
|
||||
var item = app.project.itemByID(item_id);
|
||||
if (!item){
|
||||
return _prepareError("There is no item with "+ item_id);
|
||||
}
|
||||
|
||||
app.beginUndoGroup('Add loaded items');
|
||||
for (i = 1; i <= app.project.items.length; ++i){
|
||||
var comp = app.project.items[i];
|
||||
if (!(comp instanceof CompItem)){
|
||||
continue
|
||||
}
|
||||
|
||||
var i = 1;
|
||||
while (i <= comp.numLayers) {
|
||||
var layer = comp.layer(i);
|
||||
var layer_source = layer.source;
|
||||
if (layer_source && layer_source.id == placeholder_item_id){
|
||||
var new_layer = comp.layers.add(item);
|
||||
new_layer.moveAfter(layer);
|
||||
// copy all(?) properties to new layer
|
||||
layer.property("ADBE Transform Group").copyToComp(new_layer);
|
||||
i = i + 1;
|
||||
}
|
||||
i = i + 1;
|
||||
}
|
||||
}
|
||||
app.endUndoGroup();
|
||||
}
|
||||
|
||||
function _prepareSingleValue(value){
|
||||
return JSON.stringify({"result": value})
|
||||
}
|
||||
function _prepareError(error_msg){
|
||||
return JSON.stringify({"error": error_msg})
|
||||
}
|
||||
|
|
@ -1,385 +0,0 @@
|
|||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import collections
|
||||
import logging
|
||||
import asyncio
|
||||
import functools
|
||||
import traceback
|
||||
|
||||
from wsrpc_aiohttp import (
|
||||
WebSocketRoute,
|
||||
WebSocketAsync
|
||||
)
|
||||
|
||||
from qtpy import QtCore
|
||||
|
||||
from ayon_core.lib import Logger, is_in_tests
|
||||
from ayon_core.pipeline import install_host
|
||||
from ayon_core.addon import AddonsManager
|
||||
from ayon_core.tools.utils import host_tools, get_ayon_qt_app
|
||||
|
||||
from .webserver import WebServerTool
|
||||
from .ws_stub import get_stub
|
||||
from .lib import set_settings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
def safe_excepthook(*args):
|
||||
traceback.print_exception(*args)
|
||||
|
||||
|
||||
def main(*subprocess_args):
|
||||
"""Main entrypoint to AE launching, called from pre hook."""
|
||||
sys.excepthook = safe_excepthook
|
||||
|
||||
from ayon_aftereffects.api import AfterEffectsHost
|
||||
|
||||
host = AfterEffectsHost()
|
||||
install_host(host)
|
||||
|
||||
os.environ["AYON_LOG_NO_COLORS"] = "0"
|
||||
app = get_ayon_qt_app()
|
||||
app.setQuitOnLastWindowClosed(False)
|
||||
|
||||
launcher = ProcessLauncher(subprocess_args)
|
||||
launcher.start()
|
||||
|
||||
if os.environ.get("HEADLESS_PUBLISH"):
|
||||
manager = AddonsManager()
|
||||
webpublisher_addon = manager["webpublisher"]
|
||||
|
||||
launcher.execute_in_main_thread(
|
||||
functools.partial(
|
||||
webpublisher_addon.headless_publish,
|
||||
log,
|
||||
"CloseAE",
|
||||
is_in_tests()
|
||||
)
|
||||
)
|
||||
|
||||
elif os.environ.get("AVALON_AFTEREFFECTS_WORKFILES_ON_LAUNCH", True):
|
||||
save = False
|
||||
if os.getenv("WORKFILES_SAVE_AS"):
|
||||
save = True
|
||||
|
||||
launcher.execute_in_main_thread(
|
||||
lambda: host_tools.show_tool_by_name("workfiles", save=save)
|
||||
)
|
||||
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
def show_tool_by_name(tool_name):
|
||||
kwargs = {}
|
||||
if tool_name == "loader":
|
||||
kwargs["use_context"] = True
|
||||
|
||||
host_tools.show_tool_by_name(tool_name, **kwargs)
|
||||
|
||||
|
||||
class ProcessLauncher(QtCore.QObject):
|
||||
"""Launches webserver, connects to it, runs main thread."""
|
||||
route_name = "AfterEffects"
|
||||
_main_thread_callbacks = collections.deque()
|
||||
|
||||
def __init__(self, subprocess_args):
|
||||
self._subprocess_args = subprocess_args
|
||||
self._log = None
|
||||
|
||||
super(ProcessLauncher, self).__init__()
|
||||
|
||||
# Keep track if launcher was alreadu started
|
||||
self._started = False
|
||||
|
||||
self._process = None
|
||||
self._websocket_server = None
|
||||
|
||||
start_process_timer = QtCore.QTimer()
|
||||
start_process_timer.setInterval(100)
|
||||
|
||||
loop_timer = QtCore.QTimer()
|
||||
loop_timer.setInterval(200)
|
||||
|
||||
start_process_timer.timeout.connect(self._on_start_process_timer)
|
||||
loop_timer.timeout.connect(self._on_loop_timer)
|
||||
|
||||
self._start_process_timer = start_process_timer
|
||||
self._loop_timer = loop_timer
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
if self._log is None:
|
||||
self._log = Logger.get_logger("{}-launcher".format(
|
||||
self.route_name))
|
||||
return self._log
|
||||
|
||||
@property
|
||||
def websocket_server_is_running(self):
|
||||
if self._websocket_server is not None:
|
||||
return self._websocket_server.is_running
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_process_running(self):
|
||||
if self._process is not None:
|
||||
return self._process.poll() is None
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_host_connected(self):
|
||||
"""Returns True if connected, False if app is not running at all."""
|
||||
if not self.is_process_running:
|
||||
return False
|
||||
|
||||
try:
|
||||
|
||||
_stub = get_stub()
|
||||
if _stub:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def execute_in_main_thread(cls, callback):
|
||||
cls._main_thread_callbacks.append(callback)
|
||||
|
||||
def start(self):
|
||||
if self._started:
|
||||
return
|
||||
self.log.info("Started launch logic of AfterEffects")
|
||||
self._started = True
|
||||
self._start_process_timer.start()
|
||||
|
||||
def exit(self):
|
||||
""" Exit whole application. """
|
||||
if self._start_process_timer.isActive():
|
||||
self._start_process_timer.stop()
|
||||
if self._loop_timer.isActive():
|
||||
self._loop_timer.stop()
|
||||
|
||||
if self._websocket_server is not None:
|
||||
self._websocket_server.stop()
|
||||
|
||||
if self._process:
|
||||
self._process.kill()
|
||||
self._process.wait()
|
||||
|
||||
QtCore.QCoreApplication.exit()
|
||||
|
||||
def _on_loop_timer(self):
|
||||
# TODO find better way and catch errors
|
||||
# Run only callbacks that are in queue at the moment
|
||||
cls = self.__class__
|
||||
for _ in range(len(cls._main_thread_callbacks)):
|
||||
if cls._main_thread_callbacks:
|
||||
callback = cls._main_thread_callbacks.popleft()
|
||||
callback()
|
||||
|
||||
if not self.is_process_running:
|
||||
self.log.info("Host process is not running. Closing")
|
||||
self.exit()
|
||||
|
||||
elif not self.websocket_server_is_running:
|
||||
self.log.info("Websocket server is not running. Closing")
|
||||
self.exit()
|
||||
|
||||
def _on_start_process_timer(self):
|
||||
# TODO add try except validations for each part in this method
|
||||
# Start server as first thing
|
||||
if self._websocket_server is None:
|
||||
self._init_server()
|
||||
return
|
||||
|
||||
# TODO add waiting time
|
||||
# Wait for webserver
|
||||
if not self.websocket_server_is_running:
|
||||
return
|
||||
|
||||
# Start application process
|
||||
if self._process is None:
|
||||
self._start_process()
|
||||
self.log.info("Waiting for host to connect")
|
||||
return
|
||||
|
||||
# TODO add waiting time
|
||||
# Wait until host is connected
|
||||
if self.is_host_connected:
|
||||
self._start_process_timer.stop()
|
||||
self._loop_timer.start()
|
||||
elif (
|
||||
not self.is_process_running
|
||||
or not self.websocket_server_is_running
|
||||
):
|
||||
self.exit()
|
||||
|
||||
def _init_server(self):
|
||||
if self._websocket_server is not None:
|
||||
return
|
||||
|
||||
self.log.debug(
|
||||
"Initialization of websocket server for host communication"
|
||||
)
|
||||
|
||||
self._websocket_server = websocket_server = WebServerTool()
|
||||
if websocket_server.port_occupied(
|
||||
websocket_server.host_name,
|
||||
websocket_server.port
|
||||
):
|
||||
self.log.info(
|
||||
"Server already running, sending actual context and exit."
|
||||
)
|
||||
asyncio.run(websocket_server.send_context_change(self.route_name))
|
||||
self.exit()
|
||||
return
|
||||
|
||||
# Add Websocket route
|
||||
websocket_server.add_route("*", "/ws/", WebSocketAsync)
|
||||
# Add after effects route to websocket handler
|
||||
|
||||
print("Adding {} route".format(self.route_name))
|
||||
WebSocketAsync.add_route(
|
||||
self.route_name, AfterEffectsRoute
|
||||
)
|
||||
self.log.info("Starting websocket server for host communication")
|
||||
websocket_server.start_server()
|
||||
|
||||
def _start_process(self):
|
||||
if self._process is not None:
|
||||
return
|
||||
self.log.info("Starting host process")
|
||||
try:
|
||||
self._process = subprocess.Popen(
|
||||
self._subprocess_args,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
except Exception:
|
||||
self.log.info("exce", exc_info=True)
|
||||
self.exit()
|
||||
|
||||
|
||||
class AfterEffectsRoute(WebSocketRoute):
|
||||
"""
|
||||
One route, mimicking external application (like Harmony, etc).
|
||||
All functions could be called from client.
|
||||
'do_notify' function calls function on the client - mimicking
|
||||
notification after long running job on the server or similar
|
||||
"""
|
||||
instance = None
|
||||
|
||||
def init(self, **kwargs):
|
||||
# Python __init__ must be return "self".
|
||||
# This method might return anything.
|
||||
log.debug("someone called AfterEffects route")
|
||||
self.instance = self
|
||||
return kwargs
|
||||
|
||||
# server functions
|
||||
async def ping(self):
|
||||
log.debug("someone called AfterEffects route ping")
|
||||
|
||||
# This method calls function on the client side
|
||||
# client functions
|
||||
async def set_context(self, project, folder, task):
|
||||
"""
|
||||
Sets 'project', 'folder' and 'task' to envs, eg. setting context
|
||||
|
||||
Args:
|
||||
project (str)
|
||||
folder (str)
|
||||
task (str)
|
||||
"""
|
||||
log.info("Setting context change")
|
||||
log.info("project {} folder {} ".format(project, folder))
|
||||
if project:
|
||||
os.environ["AYON_PROJECT_NAME"] = project
|
||||
if folder:
|
||||
os.environ["AYON_FOLDER_PATH"] = folder
|
||||
if task:
|
||||
os.environ["AYON_TASK_NAME"] = task
|
||||
|
||||
async def read(self):
|
||||
log.debug("aftereffects.read client calls server server calls "
|
||||
"aftereffects client")
|
||||
return await self.socket.call('aftereffects.read')
|
||||
|
||||
# panel routes for tools
|
||||
async def workfiles_route(self):
|
||||
self._tool_route("workfiles")
|
||||
|
||||
async def loader_route(self):
|
||||
self._tool_route("loader")
|
||||
|
||||
async def publish_route(self):
|
||||
self._tool_route("publisher")
|
||||
|
||||
async def sceneinventory_route(self):
|
||||
self._tool_route("sceneinventory")
|
||||
|
||||
async def setresolution_route(self):
|
||||
self._settings_route(False, True)
|
||||
|
||||
async def setframes_route(self):
|
||||
self._settings_route(True, False)
|
||||
|
||||
async def setall_route(self):
|
||||
self._settings_route(True, True)
|
||||
|
||||
async def experimental_tools_route(self):
|
||||
self._tool_route("experimental_tools")
|
||||
|
||||
def _tool_route(self, _tool_name):
|
||||
"""The address accessed when clicking on the buttons."""
|
||||
|
||||
partial_method = functools.partial(show_tool_by_name,
|
||||
_tool_name)
|
||||
|
||||
ProcessLauncher.execute_in_main_thread(partial_method)
|
||||
|
||||
# Required return statement.
|
||||
return "nothing"
|
||||
|
||||
def _settings_route(self, frames, resolution):
|
||||
partial_method = functools.partial(set_settings,
|
||||
frames,
|
||||
resolution)
|
||||
|
||||
ProcessLauncher.execute_in_main_thread(partial_method)
|
||||
|
||||
# Required return statement.
|
||||
return "nothing"
|
||||
|
||||
def create_placeholder_route(self):
|
||||
from ayon_aftereffects.api.workfile_template_builder import \
|
||||
create_placeholder
|
||||
partial_method = functools.partial(create_placeholder)
|
||||
|
||||
ProcessLauncher.execute_in_main_thread(partial_method)
|
||||
|
||||
# Required return statement.
|
||||
return "nothing"
|
||||
|
||||
def update_placeholder_route(self):
|
||||
from ayon_aftereffects.api.workfile_template_builder import \
|
||||
update_placeholder
|
||||
partial_method = functools.partial(update_placeholder)
|
||||
|
||||
ProcessLauncher.execute_in_main_thread(partial_method)
|
||||
|
||||
# Required return statement.
|
||||
return "nothing"
|
||||
|
||||
def build_workfile_template_route(self):
|
||||
from ayon_aftereffects.api.workfile_template_builder import \
|
||||
build_workfile_template
|
||||
partial_method = functools.partial(build_workfile_template)
|
||||
|
||||
ProcessLauncher.execute_in_main_thread(partial_method)
|
||||
|
||||
# Required return statement.
|
||||
return "nothing"
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
"""Script wraps launch mechanism of AfterEffects implementations.
|
||||
|
||||
Arguments passed to the script are passed to launch function in host
|
||||
implementation. In all cases requires host app executable and may contain
|
||||
workfile or others.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ayon_aftereffects.api.launch_logic import main as host_main
|
||||
|
||||
# Get current file to locate start point of sys.argv
|
||||
CURRENT_FILE = os.path.abspath(__file__)
|
||||
|
||||
|
||||
def show_error_messagebox(title, message, detail_message=None):
|
||||
"""Function will show message and process ends after closing it."""
|
||||
from qtpy import QtWidgets, QtCore
|
||||
from ayon_core import style
|
||||
|
||||
app = QtWidgets.QApplication([])
|
||||
app.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
msgbox = QtWidgets.QMessageBox()
|
||||
msgbox.setWindowTitle(title)
|
||||
msgbox.setText(message)
|
||||
|
||||
if detail_message:
|
||||
msgbox.setDetailedText(detail_message)
|
||||
|
||||
msgbox.setWindowModality(QtCore.Qt.ApplicationModal)
|
||||
msgbox.show()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
def on_invalid_args(script_not_found):
|
||||
"""Show to user message box saying that something went wrong.
|
||||
|
||||
Tell user that arguments to launch implementation are invalid with
|
||||
arguments details.
|
||||
|
||||
Args:
|
||||
script_not_found (bool): Use different message based on this value.
|
||||
"""
|
||||
|
||||
title = "Invalid arguments"
|
||||
joined_args = ", ".join("\"{}\"".format(arg) for arg in sys.argv)
|
||||
if script_not_found:
|
||||
submsg = "Where couldn't find script path:\n\"{}\""
|
||||
else:
|
||||
submsg = "Expected Host executable after script path:\n\"{}\""
|
||||
|
||||
message = "BUG: Got invalid arguments so can't launch Host application."
|
||||
detail_message = "Process was launched with arguments:\n{}\n\n{}".format(
|
||||
joined_args,
|
||||
submsg.format(CURRENT_FILE)
|
||||
)
|
||||
|
||||
show_error_messagebox(title, message, detail_message)
|
||||
|
||||
|
||||
def main(argv):
|
||||
# Modify current file path to find match in sys.argv which may be different
|
||||
# on windows (different letter cases and slashes).
|
||||
modified_current_file = CURRENT_FILE.replace("\\", "/").lower()
|
||||
|
||||
# Create a copy of sys argv
|
||||
sys_args = list(argv)
|
||||
after_script_idx = None
|
||||
# Find script path in sys.argv to know index of argv where host
|
||||
# executable should be.
|
||||
for idx, item in enumerate(sys_args):
|
||||
if item.replace("\\", "/").lower() == modified_current_file:
|
||||
after_script_idx = idx + 1
|
||||
break
|
||||
|
||||
# Validate that there is at least one argument after script path
|
||||
launch_args = None
|
||||
if after_script_idx is not None:
|
||||
launch_args = sys_args[after_script_idx:]
|
||||
|
||||
if launch_args:
|
||||
# Launch host implementation
|
||||
host_main(*launch_args)
|
||||
else:
|
||||
# Show message box
|
||||
on_invalid_args(after_script_idx is None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv)
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
import os
|
||||
import re
|
||||
import json
|
||||
import contextlib
|
||||
import logging
|
||||
|
||||
import ayon_api
|
||||
|
||||
from ayon_core.pipeline.context_tools import get_current_context
|
||||
|
||||
from .ws_stub import get_stub
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def maintained_selection():
|
||||
"""Maintain selection during context."""
|
||||
selection = get_stub().get_selected_items(True, False, False)
|
||||
try:
|
||||
yield selection
|
||||
finally:
|
||||
pass
|
||||
|
||||
|
||||
def get_extension_manifest_path():
|
||||
return os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"extension",
|
||||
"CSXS",
|
||||
"manifest.xml"
|
||||
)
|
||||
|
||||
|
||||
def get_unique_layer_name(layers, name):
|
||||
"""
|
||||
Gets all layer names and if 'name' is present in them, increases
|
||||
suffix by 1 (eg. creates unique layer name - for Loader)
|
||||
Args:
|
||||
layers (list): of strings, names only
|
||||
name (string): checked value
|
||||
|
||||
Returns:
|
||||
(string): name_00X (without version)
|
||||
"""
|
||||
names = {}
|
||||
for layer in layers:
|
||||
layer_name = re.sub(r'_\d{3}$', '', layer)
|
||||
if layer_name in names.keys():
|
||||
names[layer_name] = names[layer_name] + 1
|
||||
else:
|
||||
names[layer_name] = 1
|
||||
occurrences = names.get(name, 0)
|
||||
|
||||
return "{}_{:0>3d}".format(name, occurrences + 1)
|
||||
|
||||
|
||||
def get_background_layers(file_url):
|
||||
"""
|
||||
Pulls file name from background json file, enrich with folder url for
|
||||
AE to be able import files.
|
||||
|
||||
Order is important, follows order in json.
|
||||
|
||||
Args:
|
||||
file_url (str): abs url of background json
|
||||
|
||||
Returns:
|
||||
(list): of abs paths to images
|
||||
"""
|
||||
with open(file_url) as json_file:
|
||||
data = json.load(json_file)
|
||||
|
||||
layers = list()
|
||||
bg_folder = os.path.dirname(file_url)
|
||||
for child in data['children']:
|
||||
if child.get("filename"):
|
||||
layers.append(os.path.join(bg_folder, child.get("filename")).
|
||||
replace("\\", "/"))
|
||||
else:
|
||||
for layer in child['children']:
|
||||
if layer.get("filename"):
|
||||
layers.append(os.path.join(bg_folder,
|
||||
layer.get("filename")).
|
||||
replace("\\", "/"))
|
||||
return layers
|
||||
|
||||
|
||||
def get_folder_settings(folder_entity):
|
||||
"""Get settings of current folder.
|
||||
|
||||
Returns:
|
||||
dict: Scene data.
|
||||
|
||||
"""
|
||||
folder_attributes = folder_entity["attrib"]
|
||||
fps = folder_attributes.get("fps", 0)
|
||||
frame_start = folder_attributes.get("frameStart", 0)
|
||||
frame_end = folder_attributes.get("frameEnd", 0)
|
||||
handle_start = folder_attributes.get("handleStart", 0)
|
||||
handle_end = folder_attributes.get("handleEnd", 0)
|
||||
resolution_width = folder_attributes.get("resolutionWidth", 0)
|
||||
resolution_height = folder_attributes.get("resolutionHeight", 0)
|
||||
duration = (frame_end - frame_start + 1) + handle_start + handle_end
|
||||
|
||||
return {
|
||||
"fps": fps,
|
||||
"frameStart": frame_start,
|
||||
"frameEnd": frame_end,
|
||||
"handleStart": handle_start,
|
||||
"handleEnd": handle_end,
|
||||
"resolutionWidth": resolution_width,
|
||||
"resolutionHeight": resolution_height,
|
||||
"duration": duration
|
||||
}
|
||||
|
||||
|
||||
def set_settings(frames, resolution, comp_ids=None, print_msg=True):
|
||||
"""Sets number of frames and resolution to selected comps.
|
||||
|
||||
Args:
|
||||
frames (bool): True if set frame info
|
||||
resolution (bool): True if set resolution
|
||||
comp_ids (list): specific composition ids, if empty
|
||||
it tries to look for currently selected
|
||||
print_msg (bool): True throw JS alert with msg
|
||||
"""
|
||||
frame_start = frames_duration = fps = width = height = None
|
||||
current_context = get_current_context()
|
||||
|
||||
folder_entity = ayon_api.get_folder_by_path(
|
||||
current_context["project_name"],
|
||||
current_context["folder_path"]
|
||||
)
|
||||
settings = get_folder_settings(folder_entity)
|
||||
|
||||
msg = ''
|
||||
if frames:
|
||||
frame_start = settings["frameStart"] - settings["handleStart"]
|
||||
frames_duration = settings["duration"]
|
||||
fps = settings["fps"]
|
||||
msg += f"frame start:{frame_start}, duration:{frames_duration}, "\
|
||||
f"fps:{fps}"
|
||||
if resolution:
|
||||
width = settings["resolutionWidth"]
|
||||
height = settings["resolutionHeight"]
|
||||
msg += f"width:{width} and height:{height}"
|
||||
|
||||
stub = get_stub()
|
||||
if not comp_ids:
|
||||
comps = stub.get_selected_items(True, False, False)
|
||||
comp_ids = [comp.id for comp in comps]
|
||||
if not comp_ids:
|
||||
stub.print_msg("Select at least one composition to apply settings.")
|
||||
return
|
||||
|
||||
for comp_id in comp_ids:
|
||||
msg = f"Setting for comp {comp_id} " + msg
|
||||
log.debug(msg)
|
||||
stub.set_comp_properties(comp_id, frame_start, frames_duration,
|
||||
fps, width, height)
|
||||
if print_msg:
|
||||
stub.print_msg(msg)
|
||||
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
|
@ -1,286 +0,0 @@
|
|||
import os
|
||||
|
||||
from qtpy import QtWidgets
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from ayon_core.lib import Logger, register_event_callback
|
||||
from ayon_core.pipeline import (
|
||||
register_loader_plugin_path,
|
||||
register_creator_plugin_path,
|
||||
register_workfile_build_plugin_path,
|
||||
AVALON_CONTAINER_ID,
|
||||
AVALON_INSTANCE_ID,
|
||||
AYON_INSTANCE_ID,
|
||||
)
|
||||
from ayon_core.pipeline.load import any_outdated_containers
|
||||
from ayon_core.host import (
|
||||
HostBase,
|
||||
IWorkfileHost,
|
||||
ILoadHost,
|
||||
IPublishHost
|
||||
)
|
||||
from ayon_core.tools.utils import get_ayon_qt_app
|
||||
from ayon_aftereffects import AFTEREFFECTS_ADDON_ROOT
|
||||
|
||||
from .launch_logic import get_stub
|
||||
from .ws_stub import ConnectionNotEstablishedYet
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
|
||||
|
||||
PLUGINS_DIR = os.path.join(AFTEREFFECTS_ADDON_ROOT, "plugins")
|
||||
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
|
||||
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
|
||||
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
|
||||
WORKFILE_BUILD_PATH = os.path.join(PLUGINS_DIR, "workfile_build")
|
||||
|
||||
|
||||
class AfterEffectsHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
|
||||
name = "aftereffects"
|
||||
|
||||
def __init__(self):
|
||||
self._stub = None
|
||||
super(AfterEffectsHost, self).__init__()
|
||||
|
||||
@property
|
||||
def stub(self):
|
||||
"""
|
||||
Handle pulling stub from PS to run operations on host
|
||||
Returns:
|
||||
(AEServerStub) or None
|
||||
"""
|
||||
if self._stub:
|
||||
return self._stub
|
||||
|
||||
try:
|
||||
stub = get_stub() # only after Photoshop is up
|
||||
except ConnectionNotEstablishedYet:
|
||||
print("Not connected yet, ignoring")
|
||||
return
|
||||
|
||||
self._stub = stub
|
||||
return self._stub
|
||||
|
||||
def install(self):
|
||||
print("Installing Pype config...")
|
||||
|
||||
pyblish.api.register_host("aftereffects")
|
||||
pyblish.api.register_plugin_path(PUBLISH_PATH)
|
||||
|
||||
register_loader_plugin_path(LOAD_PATH)
|
||||
register_creator_plugin_path(CREATE_PATH)
|
||||
register_workfile_build_plugin_path(WORKFILE_BUILD_PATH)
|
||||
|
||||
register_event_callback("application.launched", application_launch)
|
||||
|
||||
def get_workfile_extensions(self):
|
||||
return [".aep"]
|
||||
|
||||
def save_workfile(self, dst_path=None):
|
||||
self.stub.saveAs(dst_path, True)
|
||||
|
||||
def open_workfile(self, filepath):
|
||||
self.stub.open(filepath)
|
||||
|
||||
return True
|
||||
|
||||
def get_current_workfile(self):
|
||||
try:
|
||||
full_name = get_stub().get_active_document_full_name()
|
||||
if full_name and full_name != "null":
|
||||
return os.path.normpath(full_name).replace("\\", "/")
|
||||
except ValueError:
|
||||
print("Nothing opened")
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def get_containers(self):
|
||||
return ls()
|
||||
|
||||
def get_context_data(self):
|
||||
meta = self.stub.get_metadata()
|
||||
for item in meta:
|
||||
if item.get("id") == "publish_context":
|
||||
item.pop("id")
|
||||
return item
|
||||
|
||||
return {}
|
||||
|
||||
def update_context_data(self, data, changes):
|
||||
item = data
|
||||
item["id"] = "publish_context"
|
||||
self.stub.imprint(item["id"], item)
|
||||
|
||||
# created instances section
|
||||
def list_instances(self):
|
||||
"""List all created instances from current workfile which
|
||||
will be published.
|
||||
|
||||
Pulls from File > File Info
|
||||
|
||||
For SubsetManager
|
||||
|
||||
Returns:
|
||||
(list) of dictionaries matching instances format
|
||||
"""
|
||||
stub = self.stub
|
||||
if not stub:
|
||||
return []
|
||||
|
||||
instances = []
|
||||
layers_meta = stub.get_metadata()
|
||||
|
||||
for instance in layers_meta:
|
||||
if instance.get("id") in {
|
||||
AYON_INSTANCE_ID, AVALON_INSTANCE_ID
|
||||
}:
|
||||
instances.append(instance)
|
||||
return instances
|
||||
|
||||
def remove_instance(self, instance):
|
||||
"""Remove instance from current workfile metadata.
|
||||
|
||||
Updates metadata of current file in File > File Info and removes
|
||||
icon highlight on group layer.
|
||||
|
||||
For SubsetManager
|
||||
|
||||
Args:
|
||||
instance (dict): instance representation from subsetmanager model
|
||||
"""
|
||||
stub = self.stub
|
||||
|
||||
if not stub:
|
||||
return
|
||||
|
||||
inst_id = instance.get("instance_id") or instance.get("uuid") # legacy
|
||||
if not inst_id:
|
||||
log.warning("No instance identifier for {}".format(instance))
|
||||
return
|
||||
|
||||
stub.remove_instance(inst_id)
|
||||
|
||||
if instance.get("members"):
|
||||
item = stub.get_item(instance["members"][0])
|
||||
if item:
|
||||
stub.rename_item(item.id,
|
||||
item.name.replace(stub.PUBLISH_ICON, ''))
|
||||
|
||||
|
||||
def application_launch():
|
||||
"""Triggered after start of app"""
|
||||
check_inventory()
|
||||
|
||||
|
||||
def ls():
|
||||
"""Yields containers from active AfterEffects document.
|
||||
|
||||
This is the host-equivalent of api.ls(), but instead of listing
|
||||
assets on disk, it lists assets already loaded in AE; once loaded
|
||||
they are called 'containers'. Used in Manage tool.
|
||||
|
||||
Containers could be on multiple levels, single images/videos/was as a
|
||||
FootageItem, or multiple items - backgrounds (folder with automatically
|
||||
created composition and all imported layers).
|
||||
|
||||
Yields:
|
||||
dict: container
|
||||
|
||||
"""
|
||||
try:
|
||||
stub = get_stub() # only after AfterEffects is up
|
||||
except ConnectionNotEstablishedYet:
|
||||
print("Not connected yet, ignoring")
|
||||
return
|
||||
|
||||
layers_meta = stub.get_metadata()
|
||||
for item in stub.get_items(comps=True,
|
||||
folders=True,
|
||||
footages=True):
|
||||
data = stub.read(item, layers_meta)
|
||||
# Skip non-tagged layers.
|
||||
if not data:
|
||||
continue
|
||||
|
||||
# Filter to only containers.
|
||||
if "container" not in data["id"]:
|
||||
continue
|
||||
|
||||
# Append transient data
|
||||
data["objectName"] = item.name.replace(stub.LOADED_ICON, '')
|
||||
data["layer"] = item
|
||||
yield data
|
||||
|
||||
|
||||
def check_inventory():
|
||||
"""Checks loaded containers if they are of highest version"""
|
||||
if not any_outdated_containers():
|
||||
return
|
||||
|
||||
# Warn about outdated containers.
|
||||
_app = get_ayon_qt_app()
|
||||
|
||||
message_box = QtWidgets.QMessageBox()
|
||||
message_box.setIcon(QtWidgets.QMessageBox.Warning)
|
||||
msg = "There are outdated containers in the scene."
|
||||
message_box.setText(msg)
|
||||
message_box.exec_()
|
||||
|
||||
|
||||
def containerise(name,
|
||||
namespace,
|
||||
comp,
|
||||
context,
|
||||
loader=None,
|
||||
suffix="_CON"):
|
||||
"""
|
||||
Containerisation enables a tracking of version, author and origin
|
||||
for loaded assets.
|
||||
|
||||
Creates dictionary payloads that gets saved into file metadata. Each
|
||||
container contains of who loaded (loader) and members (single or multiple
|
||||
in case of background).
|
||||
|
||||
Arguments:
|
||||
name (str): Name of resulting assembly
|
||||
namespace (str): Namespace under which to host container
|
||||
comp (AEItem): Composition to containerise
|
||||
context (dict): Asset information
|
||||
loader (str, optional): Name of loader used to produce this container.
|
||||
suffix (str, optional): Suffix of container, defaults to `_CON`.
|
||||
|
||||
Returns:
|
||||
container (str): Name of container assembly
|
||||
"""
|
||||
data = {
|
||||
"schema": "openpype:container-2.0",
|
||||
"id": AVALON_CONTAINER_ID,
|
||||
"name": name,
|
||||
"namespace": namespace,
|
||||
"loader": str(loader),
|
||||
"representation": context["representation"]["id"],
|
||||
"members": comp.members or [comp.id]
|
||||
}
|
||||
|
||||
stub = get_stub()
|
||||
stub.imprint(comp.id, data)
|
||||
|
||||
return comp
|
||||
|
||||
|
||||
def cache_and_get_instances(creator):
|
||||
"""Cache instances in shared data.
|
||||
|
||||
Storing all instances as a list as legacy instances might be still present.
|
||||
Args:
|
||||
creator (Creator): Plugin which would like to get instances from host.
|
||||
Returns:
|
||||
List[]: list of all instances stored in metadata
|
||||
"""
|
||||
shared_key = "openpype.photoshop.instances"
|
||||
if shared_key not in creator.collection_shared_data:
|
||||
creator.collection_shared_data[shared_key] = \
|
||||
creator.host.list_instances()
|
||||
return creator.collection_shared_data[shared_key]
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import six
|
||||
from abc import ABCMeta
|
||||
|
||||
from ayon_core.pipeline import LoaderPlugin
|
||||
from .launch_logic import get_stub
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class AfterEffectsLoader(LoaderPlugin):
|
||||
@staticmethod
|
||||
def get_stub():
|
||||
return get_stub()
|
||||
|
|
@ -1,241 +0,0 @@
|
|||
"""Webserver for communication with AfterEffects.
|
||||
|
||||
Aiohttp (Asyncio) based websocket server used for communication with host
|
||||
application.
|
||||
|
||||
This webserver is started in spawned Python process that opens DCC during
|
||||
its launch, waits for connection from DCC and handles communication going
|
||||
forward. Server is closed before Python process is killed.
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
import urllib
|
||||
import threading
|
||||
import asyncio
|
||||
import socket
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from wsrpc_aiohttp import WSRPCClient
|
||||
|
||||
from ayon_core.pipeline import get_global_context
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WebServerTool:
|
||||
"""
|
||||
Basic POC implementation of asychronic websocket RPC server.
|
||||
Uses class in external_app_1.py to mimic implementation for single
|
||||
external application.
|
||||
'test_client' folder contains two test implementations of client
|
||||
"""
|
||||
_instance = None
|
||||
|
||||
def __init__(self):
|
||||
WebServerTool._instance = self
|
||||
|
||||
self.client = None
|
||||
self.handlers = {}
|
||||
self.on_stop_callbacks = []
|
||||
|
||||
port = None
|
||||
host_name = "localhost"
|
||||
websocket_url = os.getenv("WEBSOCKET_URL")
|
||||
if websocket_url:
|
||||
parsed = urllib.parse.urlparse(websocket_url)
|
||||
port = parsed.port
|
||||
host_name = parsed.netloc.split(":")[0]
|
||||
if not port:
|
||||
port = 8098 # fallback
|
||||
|
||||
self.port = port
|
||||
self.host_name = host_name
|
||||
|
||||
self.app = web.Application()
|
||||
|
||||
# add route with multiple methods for single "external app"
|
||||
self.webserver_thread = WebServerThread(self, self.port)
|
||||
|
||||
def add_route(self, *args, **kwargs):
|
||||
self.app.router.add_route(*args, **kwargs)
|
||||
|
||||
def add_static(self, *args, **kwargs):
|
||||
self.app.router.add_static(*args, **kwargs)
|
||||
|
||||
def start_server(self):
|
||||
if self.webserver_thread and not self.webserver_thread.is_alive():
|
||||
self.webserver_thread.start()
|
||||
|
||||
def stop_server(self):
|
||||
self.stop()
|
||||
|
||||
async def send_context_change(self, host):
|
||||
"""
|
||||
Calls running webserver to inform about context change
|
||||
|
||||
Used when new PS/AE should be triggered,
|
||||
but one already running, without
|
||||
this publish would point to old context.
|
||||
"""
|
||||
client = WSRPCClient(os.getenv("WEBSOCKET_URL"),
|
||||
loop=asyncio.get_event_loop())
|
||||
await client.connect()
|
||||
|
||||
context = get_global_context()
|
||||
project_name = context["project_name"]
|
||||
folder_path = context["folder_path"]
|
||||
task_name = context["task_name"]
|
||||
log.info("Sending context change to {}{}/{}".format(
|
||||
project_name, folder_path, task_name
|
||||
))
|
||||
|
||||
await client.call(
|
||||
'{}.set_context'.format(host),
|
||||
project=project_name,
|
||||
folder=folder_path,
|
||||
task=task_name
|
||||
)
|
||||
await client.close()
|
||||
|
||||
def port_occupied(self, host_name, port):
|
||||
"""
|
||||
Check if 'url' is already occupied.
|
||||
|
||||
This could mean, that app is already running and we are trying open it
|
||||
again. In that case, use existing running webserver.
|
||||
Check here is easier than capturing exception from thread.
|
||||
"""
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as con:
|
||||
result = con.connect_ex((host_name, port)) == 0
|
||||
|
||||
if result:
|
||||
print(f"Port {port} is already in use")
|
||||
return result
|
||||
|
||||
def call(self, func):
|
||||
log.debug("websocket.call {}".format(func))
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
func,
|
||||
self.webserver_thread.loop
|
||||
)
|
||||
result = future.result()
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_instance():
|
||||
if WebServerTool._instance is None:
|
||||
WebServerTool()
|
||||
return WebServerTool._instance
|
||||
|
||||
@property
|
||||
def is_running(self):
|
||||
if not self.webserver_thread:
|
||||
return False
|
||||
return self.webserver_thread.is_running
|
||||
|
||||
def stop(self):
|
||||
if not self.is_running:
|
||||
return
|
||||
try:
|
||||
log.debug("Stopping websocket server")
|
||||
self.webserver_thread.is_running = False
|
||||
self.webserver_thread.stop()
|
||||
except Exception:
|
||||
log.warning(
|
||||
"Error has happened during Killing websocket server",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
def thread_stopped(self):
|
||||
for callback in self.on_stop_callbacks:
|
||||
callback()
|
||||
|
||||
|
||||
class WebServerThread(threading.Thread):
|
||||
""" Listener for websocket rpc requests.
|
||||
|
||||
It would be probably better to "attach" this to main thread (as for
|
||||
example Harmony needs to run something on main thread), but currently
|
||||
it creates separate thread and separate asyncio event loop
|
||||
"""
|
||||
def __init__(self, module, port):
|
||||
super(WebServerThread, self).__init__()
|
||||
|
||||
self.is_running = False
|
||||
self.port = port
|
||||
self.module = module
|
||||
self.loop = None
|
||||
self.runner = None
|
||||
self.site = None
|
||||
self.tasks = []
|
||||
|
||||
def run(self):
|
||||
self.is_running = True
|
||||
|
||||
try:
|
||||
log.info("Starting web server")
|
||||
self.loop = asyncio.new_event_loop() # create new loop for thread
|
||||
asyncio.set_event_loop(self.loop)
|
||||
|
||||
self.loop.run_until_complete(self.start_server())
|
||||
|
||||
websocket_url = "ws://localhost:{}/ws".format(self.port)
|
||||
|
||||
log.debug(
|
||||
"Running Websocket server on URL: \"{}\"".format(websocket_url)
|
||||
)
|
||||
|
||||
asyncio.ensure_future(self.check_shutdown(), loop=self.loop)
|
||||
self.loop.run_forever()
|
||||
except Exception:
|
||||
self.is_running = False
|
||||
log.warning(
|
||||
"Websocket Server service has failed", exc_info=True
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
self.loop.close() # optional
|
||||
|
||||
self.is_running = False
|
||||
self.module.thread_stopped()
|
||||
log.info("Websocket server stopped")
|
||||
|
||||
async def start_server(self):
|
||||
""" Starts runner and TCPsite """
|
||||
self.runner = web.AppRunner(self.module.app)
|
||||
await self.runner.setup()
|
||||
self.site = web.TCPSite(self.runner, 'localhost', self.port)
|
||||
await self.site.start()
|
||||
|
||||
def stop(self):
|
||||
"""Sets is_running flag to false, 'check_shutdown' shuts server down"""
|
||||
self.is_running = False
|
||||
|
||||
async def check_shutdown(self):
|
||||
""" Future that is running and checks if server should be running
|
||||
periodically.
|
||||
"""
|
||||
while self.is_running:
|
||||
while self.tasks:
|
||||
task = self.tasks.pop(0)
|
||||
log.debug("waiting for task {}".format(task))
|
||||
await task
|
||||
log.debug("returned value {}".format(task.result))
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
log.debug("Starting shutdown")
|
||||
await self.site.stop()
|
||||
log.debug("Site stopped")
|
||||
await self.runner.cleanup()
|
||||
log.debug("Runner stopped")
|
||||
tasks = [task for task in asyncio.all_tasks() if
|
||||
task is not asyncio.current_task()]
|
||||
list(map(lambda task: task.cancel(), tasks)) # cancel all the tasks
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
log.debug(f'Finished awaiting cancelled tasks, results: {results}...')
|
||||
await self.loop.shutdown_asyncgens()
|
||||
# to really make sure everything else has time to stop
|
||||
await asyncio.sleep(0.07)
|
||||
self.loop.stop()
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
import os.path
|
||||
import uuid
|
||||
import shutil
|
||||
from abc import abstractmethod
|
||||
|
||||
from ayon_core.pipeline import registered_host
|
||||
from ayon_core.tools.workfile_template_build import (
|
||||
WorkfileBuildPlaceholderDialog,
|
||||
)
|
||||
from ayon_core.pipeline.workfile.workfile_template_builder import (
|
||||
AbstractTemplateBuilder,
|
||||
PlaceholderPlugin,
|
||||
PlaceholderItem
|
||||
)
|
||||
from ayon_aftereffects.api import get_stub
|
||||
|
||||
PLACEHOLDER_SET = "PLACEHOLDERS_SET"
|
||||
PLACEHOLDER_ID = "openpype.placeholder"
|
||||
|
||||
|
||||
class AETemplateBuilder(AbstractTemplateBuilder):
|
||||
"""Concrete implementation of AbstractTemplateBuilder for AE"""
|
||||
|
||||
def import_template(self, path):
|
||||
"""Import template into current scene.
|
||||
Block if a template is already loaded.
|
||||
|
||||
Args:
|
||||
path (str): A path to current template (usually given by
|
||||
get_template_preset implementation)
|
||||
|
||||
Returns:
|
||||
bool: Whether the template was successfully imported or not
|
||||
"""
|
||||
stub = get_stub()
|
||||
if not os.path.exists(path):
|
||||
stub.print_msg(f"Template file on {path} doesn't exist.")
|
||||
return
|
||||
|
||||
stub.save()
|
||||
workfile_path = stub.get_active_document_full_name()
|
||||
shutil.copy2(path, workfile_path)
|
||||
stub.open(workfile_path)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AEPlaceholderPlugin(PlaceholderPlugin):
|
||||
"""Contains generic methods for all PlaceholderPlugins."""
|
||||
|
||||
@abstractmethod
|
||||
def _create_placeholder_item(self, item_data: dict) -> PlaceholderItem:
|
||||
pass
|
||||
|
||||
def collect_placeholders(self):
|
||||
"""Collect info from file metadata about created placeholders.
|
||||
|
||||
Returns:
|
||||
(list) (LoadPlaceholderItem)
|
||||
"""
|
||||
output = []
|
||||
scene_placeholders = self._collect_scene_placeholders()
|
||||
for item in scene_placeholders:
|
||||
if item.get("plugin_identifier") != self.identifier:
|
||||
continue
|
||||
|
||||
item = self._create_placeholder_item(item)
|
||||
output.append(item)
|
||||
|
||||
return output
|
||||
|
||||
def update_placeholder(self, placeholder_item, placeholder_data):
|
||||
"""Resave changed properties for placeholders"""
|
||||
item_id, metadata_item = self._get_item(placeholder_item)
|
||||
stub = get_stub()
|
||||
if not item_id:
|
||||
stub.print_msg("Cannot find item for "
|
||||
f"{placeholder_item.scene_identifier}")
|
||||
return
|
||||
metadata_item["data"] = placeholder_data
|
||||
stub.imprint(item_id, metadata_item)
|
||||
|
||||
def _get_item(self, placeholder_item):
|
||||
"""Returns item id and item metadata for placeholder from file meta"""
|
||||
stub = get_stub()
|
||||
placeholder_uuid = placeholder_item.scene_identifier
|
||||
for metadata_item in stub.get_metadata():
|
||||
if not metadata_item.get("is_placeholder"):
|
||||
continue
|
||||
if placeholder_uuid in metadata_item.get("uuid"):
|
||||
return metadata_item["members"][0], metadata_item
|
||||
return None, None
|
||||
|
||||
def _collect_scene_placeholders(self):
|
||||
"""Cache placeholder data to shared data.
|
||||
Returns:
|
||||
(list) of dicts
|
||||
"""
|
||||
placeholder_items = self.builder.get_shared_populate_data(
|
||||
"placeholder_items"
|
||||
)
|
||||
if not placeholder_items:
|
||||
placeholder_items = []
|
||||
for item in get_stub().get_metadata():
|
||||
if not item.get("is_placeholder"):
|
||||
continue
|
||||
placeholder_items.append(item)
|
||||
|
||||
self.builder.set_shared_populate_data(
|
||||
"placeholder_items", placeholder_items
|
||||
)
|
||||
return placeholder_items
|
||||
|
||||
def _imprint_item(self, item_id, name, placeholder_data, stub):
|
||||
if not item_id:
|
||||
raise ValueError("Couldn't create a placeholder")
|
||||
container_data = {
|
||||
"id": "openpype.placeholder",
|
||||
"name": name,
|
||||
"is_placeholder": True,
|
||||
"plugin_identifier": self.identifier,
|
||||
"uuid": str(uuid.uuid4()), # scene_identifier
|
||||
"data": placeholder_data,
|
||||
"members": [item_id]
|
||||
}
|
||||
stub.imprint(item_id, container_data)
|
||||
|
||||
|
||||
def build_workfile_template(*args, **kwargs):
|
||||
builder = AETemplateBuilder(registered_host())
|
||||
builder.build_template(*args, **kwargs)
|
||||
|
||||
|
||||
def update_workfile_template(*args):
|
||||
builder = AETemplateBuilder(registered_host())
|
||||
builder.rebuild_template()
|
||||
|
||||
|
||||
def create_placeholder(*args):
|
||||
"""Called when new workile placeholder should be created."""
|
||||
host = registered_host()
|
||||
builder = AETemplateBuilder(host)
|
||||
window = WorkfileBuildPlaceholderDialog(host, builder)
|
||||
window.exec_()
|
||||
|
||||
|
||||
def update_placeholder(*args):
|
||||
"""Called after placeholder item is selected to modify it."""
|
||||
host = registered_host()
|
||||
builder = AETemplateBuilder(host)
|
||||
|
||||
stub = get_stub()
|
||||
selected_items = stub.get_selected_items(True, True, True)
|
||||
|
||||
if len(selected_items) != 1:
|
||||
stub.print_msg("Please select just 1 placeholder")
|
||||
return
|
||||
|
||||
selected_id = selected_items[0].id
|
||||
placeholder_item = None
|
||||
|
||||
placeholder_items_by_id = {
|
||||
placeholder_item.scene_identifier: placeholder_item
|
||||
for placeholder_item in builder.get_placeholders()
|
||||
}
|
||||
for metadata_item in stub.get_metadata():
|
||||
if not metadata_item.get("is_placeholder"):
|
||||
continue
|
||||
if selected_id in metadata_item.get("members"):
|
||||
placeholder_item = placeholder_items_by_id.get(
|
||||
metadata_item["uuid"])
|
||||
break
|
||||
|
||||
if not placeholder_item:
|
||||
stub.print_msg("Didn't find placeholder metadata. "
|
||||
"Remove and re-create placeholder.")
|
||||
return
|
||||
|
||||
window = WorkfileBuildPlaceholderDialog(host, builder)
|
||||
window.set_update_mode(placeholder_item)
|
||||
window.exec_()
|
||||
|
|
@ -1,732 +0,0 @@
|
|||
"""
|
||||
Stub handling connection from server to client.
|
||||
Used anywhere solution is calling client methods.
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
|
||||
import attr
|
||||
|
||||
from wsrpc_aiohttp import WebSocketAsync
|
||||
|
||||
from .webserver import WebServerTool
|
||||
|
||||
|
||||
class ConnectionNotEstablishedYet(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@attr.s
|
||||
class AEItem(object):
|
||||
"""
|
||||
Object denoting Item in AE. Each item is created in AE by any Loader,
|
||||
but contains same fields, which are being used in later processing.
|
||||
"""
|
||||
# metadata
|
||||
id = attr.ib() # id created by AE, could be used for querying
|
||||
name = attr.ib() # name of item
|
||||
item_type = attr.ib(default=None) # item type (footage, folder, comp)
|
||||
# all imported elements, single for
|
||||
# regular image, array for Backgrounds
|
||||
members = attr.ib(factory=list)
|
||||
frameStart = attr.ib(default=None)
|
||||
framesDuration = attr.ib(default=None)
|
||||
frameRate = attr.ib(default=None)
|
||||
file_name = attr.ib(default=None)
|
||||
instance_id = attr.ib(default=None) # New Publisher
|
||||
width = attr.ib(default=None)
|
||||
height = attr.ib(default=None)
|
||||
is_placeholder = attr.ib(default=False)
|
||||
uuid = attr.ib(default=False)
|
||||
path = attr.ib(default=False) # path to FootageItem to validate
|
||||
# list of composition Footage is in
|
||||
containing_comps = attr.ib(factory=list)
|
||||
|
||||
|
||||
class AfterEffectsServerStub():
|
||||
"""
|
||||
Stub for calling function on client (Photoshop js) side.
|
||||
Expects that client is already connected (started when avalon menu
|
||||
is opened).
|
||||
'self.websocketserver.call' is used as async wrapper
|
||||
"""
|
||||
PUBLISH_ICON = '\u2117 '
|
||||
LOADED_ICON = '\u25bc'
|
||||
|
||||
def __init__(self):
|
||||
self.websocketserver = WebServerTool.get_instance()
|
||||
self.client = self.get_client()
|
||||
self.log = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
@staticmethod
|
||||
def get_client():
|
||||
"""
|
||||
Return first connected client to WebSocket
|
||||
TODO implement selection by Route
|
||||
:return: <WebSocketAsync> client
|
||||
"""
|
||||
clients = WebSocketAsync.get_clients()
|
||||
client = None
|
||||
if len(clients) > 0:
|
||||
key = list(clients.keys())[0]
|
||||
client = clients.get(key)
|
||||
|
||||
return client
|
||||
|
||||
def open(self, path):
|
||||
"""
|
||||
Open file located at 'path' (local).
|
||||
Args:
|
||||
path(string): file path locally
|
||||
Returns: None
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.open', path=path))
|
||||
|
||||
return self._handle_return(res)
|
||||
|
||||
def get_metadata(self):
|
||||
"""
|
||||
Get complete stored JSON with metadata from AE.Metadata.Label
|
||||
field.
|
||||
|
||||
It contains containers loaded by any Loader OR instances created
|
||||
by Creator.
|
||||
|
||||
Returns:
|
||||
(list)
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.get_metadata'))
|
||||
metadata = self._handle_return(res)
|
||||
|
||||
return metadata or []
|
||||
|
||||
def read(self, item, layers_meta=None):
|
||||
"""
|
||||
Parses item metadata from Label field of active document.
|
||||
Used as filter to pick metadata for specific 'item' only.
|
||||
|
||||
Args:
|
||||
item (AEItem): pulled info from AE
|
||||
layers_meta (dict): full list from Headline
|
||||
(load and inject for better performance in loops)
|
||||
Returns:
|
||||
(dict):
|
||||
"""
|
||||
if layers_meta is None:
|
||||
layers_meta = self.get_metadata()
|
||||
for item_meta in layers_meta:
|
||||
if 'container' in item_meta.get('id') and \
|
||||
str(item.id) == str(item_meta.get('members')[0]):
|
||||
return item_meta
|
||||
|
||||
self.log.debug("Couldn't find layer metadata")
|
||||
|
||||
def imprint(self, item_id, data, all_items=None, items_meta=None):
|
||||
"""
|
||||
Save item metadata to Label field of metadata of active document
|
||||
Args:
|
||||
item_id (int|str): id of FootageItem or instance_id for workfiles
|
||||
data(string): json representation for single layer
|
||||
all_items (list of item): for performance, could be
|
||||
injected for usage in loop, if not, single call will be
|
||||
triggered
|
||||
items_meta(string): json representation from Headline
|
||||
(for performance - provide only if imprint is in
|
||||
loop - value should be same)
|
||||
Returns: None
|
||||
"""
|
||||
if not items_meta:
|
||||
items_meta = self.get_metadata()
|
||||
|
||||
result_meta = []
|
||||
# fix existing
|
||||
is_new = True
|
||||
|
||||
for item_meta in items_meta:
|
||||
if ((item_meta.get('members') and
|
||||
str(item_id) == str(item_meta.get('members')[0])) or
|
||||
item_meta.get("instance_id") == item_id):
|
||||
is_new = False
|
||||
if data:
|
||||
item_meta.update(data)
|
||||
result_meta.append(item_meta)
|
||||
else:
|
||||
result_meta.append(item_meta)
|
||||
|
||||
if is_new:
|
||||
result_meta.append(data)
|
||||
|
||||
# Ensure only valid ids are stored.
|
||||
if not all_items:
|
||||
# loaders create FootageItem now
|
||||
all_items = self.get_items(comps=True,
|
||||
folders=True,
|
||||
footages=True)
|
||||
item_ids = [int(item.id) for item in all_items]
|
||||
cleaned_data = []
|
||||
for meta in result_meta:
|
||||
# do not added instance with nonexistend item id
|
||||
if meta.get("members"):
|
||||
if int(meta["members"][0]) not in item_ids:
|
||||
continue
|
||||
|
||||
cleaned_data.append(meta)
|
||||
|
||||
payload = json.dumps(cleaned_data, indent=4)
|
||||
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.imprint',
|
||||
payload=payload))
|
||||
return self._handle_return(res)
|
||||
|
||||
def get_active_document_full_name(self):
|
||||
"""
|
||||
Returns absolute path of active document via ws call
|
||||
Returns(string): file name
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call(
|
||||
'AfterEffects.get_active_document_full_name'))
|
||||
|
||||
return self._handle_return(res)
|
||||
|
||||
def get_active_document_name(self):
|
||||
"""
|
||||
Returns just a name of active document via ws call
|
||||
Returns(string): file name
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call(
|
||||
'AfterEffects.get_active_document_name'))
|
||||
|
||||
return self._handle_return(res)
|
||||
|
||||
def get_items(self, comps, folders=False, footages=False):
|
||||
"""
|
||||
Get all items from Project panel according to arguments.
|
||||
There are multiple different types:
|
||||
CompItem (could have multiple layers - source for Creator,
|
||||
will be rendered)
|
||||
FolderItem (collection type, currently used for Background
|
||||
loading)
|
||||
FootageItem (imported file - created by Loader)
|
||||
Args:
|
||||
comps (bool): return CompItems
|
||||
folders (bool): return FolderItem
|
||||
footages (bool: return FootageItem
|
||||
|
||||
Returns:
|
||||
(list) of namedtuples
|
||||
"""
|
||||
res = self.websocketserver.call(
|
||||
self.client.call('AfterEffects.get_items',
|
||||
comps=comps,
|
||||
folders=folders,
|
||||
footages=footages)
|
||||
)
|
||||
return self._to_records(self._handle_return(res))
|
||||
|
||||
def select_items(self, items):
|
||||
"""
|
||||
Select items in Project list
|
||||
Args:
|
||||
items (list): of int item ids
|
||||
"""
|
||||
self.websocketserver.call(
|
||||
self.client.call('AfterEffects.select_items', items=items))
|
||||
|
||||
|
||||
def get_selected_items(self, comps, folders=False, footages=False):
|
||||
"""
|
||||
Same as get_items but using selected items only
|
||||
Args:
|
||||
comps (bool): return CompItems
|
||||
folders (bool): return FolderItem
|
||||
footages (bool: return FootageItem
|
||||
|
||||
Returns:
|
||||
(list) of namedtuples
|
||||
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.get_selected_items',
|
||||
comps=comps,
|
||||
folders=folders,
|
||||
footages=footages)
|
||||
)
|
||||
return self._to_records(self._handle_return(res))
|
||||
|
||||
def add_item(self, name, item_type):
|
||||
"""
|
||||
Adds either composition or folder to project item list.
|
||||
|
||||
Args:
|
||||
name (str)
|
||||
item_type (str): COMP|FOLDER
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.add_item',
|
||||
name=name,
|
||||
item_type=item_type))
|
||||
|
||||
return self._handle_return(res)
|
||||
|
||||
def get_item(self, item_id):
|
||||
"""
|
||||
Returns metadata for particular 'item_id' or None
|
||||
|
||||
Args:
|
||||
item_id (int, or string)
|
||||
"""
|
||||
for item in self.get_items(True, True, True):
|
||||
if str(item.id) == str(item_id):
|
||||
return item
|
||||
|
||||
return None
|
||||
|
||||
def import_file(self, path, item_name, import_options=None):
|
||||
"""
|
||||
Imports file as a FootageItem. Used in Loader
|
||||
Args:
|
||||
path (string): absolute path for asset file
|
||||
item_name (string): label for created FootageItem
|
||||
import_options (dict): different files (img vs psd) need different
|
||||
config
|
||||
|
||||
"""
|
||||
res = self.websocketserver.call(
|
||||
self.client.call('AfterEffects.import_file',
|
||||
path=path,
|
||||
item_name=item_name,
|
||||
import_options=import_options)
|
||||
)
|
||||
records = self._to_records(self._handle_return(res))
|
||||
if records:
|
||||
return records.pop()
|
||||
|
||||
def replace_item(self, item_id, path, item_name):
|
||||
""" Replace FootageItem with new file
|
||||
|
||||
Args:
|
||||
item_id (int):
|
||||
path (string):absolute path
|
||||
item_name (string): label on item in Project list
|
||||
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.replace_item',
|
||||
item_id=item_id,
|
||||
path=path, item_name=item_name))
|
||||
|
||||
return self._handle_return(res)
|
||||
|
||||
def rename_item(self, item_id, item_name):
|
||||
""" Replace item with item_name
|
||||
|
||||
Args:
|
||||
item_id (int):
|
||||
item_name (string): label on item in Project list
|
||||
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.rename_item',
|
||||
item_id=item_id,
|
||||
item_name=item_name))
|
||||
|
||||
return self._handle_return(res)
|
||||
|
||||
def delete_item(self, item_id):
|
||||
""" Deletes *Item in a file
|
||||
Args:
|
||||
item_id (int):
|
||||
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.delete_item',
|
||||
item_id=item_id))
|
||||
|
||||
return self._handle_return(res)
|
||||
|
||||
def remove_instance(self, instance_id, metadata=None):
|
||||
"""
|
||||
Removes instance with 'instance_id' from file's metadata and
|
||||
saves them.
|
||||
|
||||
Keep matching item in file though.
|
||||
|
||||
Args:
|
||||
instance_id(string): instance id
|
||||
"""
|
||||
cleaned_data = []
|
||||
|
||||
if metadata is None:
|
||||
metadata = self.get_metadata()
|
||||
|
||||
for instance in metadata:
|
||||
inst_id = instance.get("instance_id") or instance.get("uuid")
|
||||
if inst_id != instance_id:
|
||||
cleaned_data.append(instance)
|
||||
|
||||
payload = json.dumps(cleaned_data, indent=4)
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.imprint',
|
||||
payload=payload))
|
||||
|
||||
return self._handle_return(res)
|
||||
|
||||
def is_saved(self):
|
||||
# TODO
|
||||
return True
|
||||
|
||||
def set_label_color(self, item_id, color_idx):
|
||||
"""
|
||||
Used for highlight additional information in Project panel.
|
||||
Green color is loaded asset, blue is created asset
|
||||
Args:
|
||||
item_id (int):
|
||||
color_idx (int): 0-16 Label colors from AE Project view
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.set_label_color',
|
||||
item_id=item_id,
|
||||
color_idx=color_idx))
|
||||
|
||||
return self._handle_return(res)
|
||||
|
||||
def get_comp_properties(self, comp_id):
|
||||
""" Get composition information for render purposes
|
||||
|
||||
Returns startFrame, frameDuration, fps, width, height.
|
||||
|
||||
Args:
|
||||
comp_id (int):
|
||||
|
||||
Returns:
|
||||
(AEItem)
|
||||
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.get_comp_properties',
|
||||
item_id=comp_id
|
||||
))
|
||||
|
||||
records = self._to_records(self._handle_return(res))
|
||||
if records:
|
||||
return records.pop()
|
||||
|
||||
def set_comp_properties(self, comp_id, start, duration, frame_rate,
|
||||
width, height):
|
||||
"""
|
||||
Set work area to predefined values (from Ftrack).
|
||||
Work area directs what gets rendered.
|
||||
Beware of rounding, AE expects seconds, not frames directly.
|
||||
|
||||
Args:
|
||||
comp_id (int):
|
||||
start (int): workAreaStart in frames
|
||||
duration (int): in frames
|
||||
frame_rate (float): frames in seconds
|
||||
width (int): resolution width
|
||||
height (int): resolution height
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.set_comp_properties',
|
||||
item_id=comp_id,
|
||||
start=start,
|
||||
duration=duration,
|
||||
frame_rate=frame_rate,
|
||||
width=width,
|
||||
height=height))
|
||||
return self._handle_return(res)
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Saves active document
|
||||
Returns: None
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.save'))
|
||||
|
||||
return self._handle_return(res)
|
||||
|
||||
def saveAs(self, project_path, as_copy):
|
||||
"""
|
||||
Saves active project to aep (copy) or png or jpg
|
||||
Args:
|
||||
project_path(string): full local path
|
||||
as_copy: <boolean>
|
||||
Returns: None
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.saveAs',
|
||||
image_path=project_path,
|
||||
as_copy=as_copy))
|
||||
|
||||
return self._handle_return(res)
|
||||
|
||||
def get_render_info(self, comp_id):
|
||||
""" Get render queue info for render purposes
|
||||
|
||||
Returns:
|
||||
(list) of (AEItem): with 'file_name' field
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.get_render_info',
|
||||
comp_id=comp_id))
|
||||
|
||||
records = self._to_records(self._handle_return(res))
|
||||
return records
|
||||
|
||||
def get_audio_url(self, item_id):
|
||||
""" Get audio layer absolute url for comp
|
||||
|
||||
Args:
|
||||
item_id (int): composition id
|
||||
Returns:
|
||||
(str): absolute path url
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.get_audio_url',
|
||||
item_id=item_id))
|
||||
|
||||
return self._handle_return(res)
|
||||
|
||||
def import_background(self, comp_id, comp_name, files):
|
||||
"""
|
||||
Imports backgrounds images to existing or new composition.
|
||||
|
||||
If comp_id is not provided, new composition is created, basic
|
||||
values (width, heights, frameRatio) takes from first imported
|
||||
image.
|
||||
|
||||
All images from background json are imported as a FootageItem and
|
||||
separate layer is created for each of them under composition.
|
||||
|
||||
Order of imported 'files' is important.
|
||||
|
||||
Args:
|
||||
comp_id (int): id of existing composition (null if new)
|
||||
comp_name (str): used when new composition
|
||||
files (list): list of absolute paths to import and
|
||||
add as layers
|
||||
|
||||
Returns:
|
||||
(AEItem): object with id of created folder, all imported images
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.import_background',
|
||||
comp_id=comp_id,
|
||||
comp_name=comp_name,
|
||||
files=files))
|
||||
|
||||
records = self._to_records(self._handle_return(res))
|
||||
if records:
|
||||
return records.pop()
|
||||
|
||||
def reload_background(self, comp_id, comp_name, files):
|
||||
"""
|
||||
Reloads backgrounds images to existing composition.
|
||||
|
||||
It actually deletes complete folder with imported images and
|
||||
created composition for safety.
|
||||
|
||||
Args:
|
||||
comp_id (int): id of existing composition to be overwritten
|
||||
comp_name (str): new name of composition (could be same as old
|
||||
if version up only)
|
||||
files (list): list of absolute paths to import and
|
||||
add as layers
|
||||
Returns:
|
||||
(AEItem): object with id of created folder, all imported images
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.reload_background',
|
||||
comp_id=comp_id,
|
||||
comp_name=comp_name,
|
||||
files=files))
|
||||
|
||||
records = self._to_records(self._handle_return(res))
|
||||
if records:
|
||||
return records.pop()
|
||||
|
||||
def add_item_as_layer(self, comp_id, item_id):
|
||||
"""
|
||||
Adds already imported FootageItem ('item_id') as a new
|
||||
layer to composition ('comp_id').
|
||||
|
||||
Args:
|
||||
comp_id (int): id of target composition
|
||||
item_id (int): FootageItem.id
|
||||
comp already found previously
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.add_item_as_layer',
|
||||
comp_id=comp_id,
|
||||
item_id=item_id))
|
||||
|
||||
records = self._to_records(self._handle_return(res))
|
||||
if records:
|
||||
return records.pop()
|
||||
|
||||
def add_item_instead_placeholder(self, placeholder_item_id, item_id):
|
||||
"""
|
||||
Adds item_id to layers where plaeholder_item_id is present.
|
||||
|
||||
1 placeholder could result in multiple loaded containers (eg items)
|
||||
|
||||
Args:
|
||||
placeholder_item_id (int): id of placeholder item
|
||||
item_id (int): loaded FootageItem id
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.add_item_instead_placeholder', # noqa
|
||||
placeholder_item_id=placeholder_item_id, # noqa
|
||||
item_id=item_id))
|
||||
|
||||
return self._handle_return(res)
|
||||
|
||||
def add_placeholder(self, name, width, height, fps, duration):
|
||||
"""
|
||||
Adds new FootageItem as a placeholder for workfile builder
|
||||
|
||||
Placeholder requires width etc, currently probably only hardcoded
|
||||
values.
|
||||
|
||||
Args:
|
||||
name (str)
|
||||
width (int)
|
||||
height (int)
|
||||
fps (float)
|
||||
duration (int)
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.add_placeholder',
|
||||
name=name,
|
||||
width=width,
|
||||
height=height,
|
||||
fps=fps,
|
||||
duration=duration))
|
||||
|
||||
return self._handle_return(res)
|
||||
|
||||
def render(self, folder_url, comp_id):
|
||||
"""
|
||||
Render all renderqueueitem to 'folder_url'
|
||||
Args:
|
||||
folder_url(string): local folder path for collecting
|
||||
Returns: None
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('AfterEffects.render',
|
||||
folder_url=folder_url,
|
||||
comp_id=comp_id))
|
||||
return self._handle_return(res)
|
||||
|
||||
def get_extension_version(self):
|
||||
"""Returns version number of installed extension."""
|
||||
res = self.websocketserver.call(self.client.call(
|
||||
'AfterEffects.get_extension_version'))
|
||||
|
||||
return self._handle_return(res)
|
||||
|
||||
def get_app_version(self):
|
||||
"""Returns version number of installed application (17.5...)."""
|
||||
res = self.websocketserver.call(self.client.call(
|
||||
'AfterEffects.get_app_version'))
|
||||
|
||||
return self._handle_return(res)
|
||||
|
||||
def close(self):
|
||||
res = self.websocketserver.call(self.client.call('AfterEffects.close'))
|
||||
|
||||
return self._handle_return(res)
|
||||
|
||||
def print_msg(self, msg):
|
||||
"""Triggers Javascript alert dialog."""
|
||||
self.websocketserver.call(self.client.call
|
||||
('AfterEffects.print_msg',
|
||||
msg=msg))
|
||||
|
||||
def _handle_return(self, res):
|
||||
"""Wraps return, throws ValueError if 'error' key is present."""
|
||||
if res and isinstance(res, str) and res != "undefined":
|
||||
try:
|
||||
parsed = json.loads(res)
|
||||
except json.decoder.JSONDecodeError:
|
||||
raise ValueError("Received broken JSON {}".format(res))
|
||||
|
||||
if not parsed: # empty list
|
||||
return parsed
|
||||
|
||||
first_item = parsed
|
||||
if isinstance(parsed, list):
|
||||
first_item = parsed[0]
|
||||
|
||||
if first_item:
|
||||
if first_item.get("error"):
|
||||
raise ValueError(first_item["error"])
|
||||
# singular values (file name etc)
|
||||
if first_item.get("result") is not None:
|
||||
return first_item["result"]
|
||||
return parsed # parsed
|
||||
return res
|
||||
|
||||
def _to_records(self, payload):
|
||||
"""
|
||||
Converts string json representation into list of AEItem
|
||||
dot notation access to work.
|
||||
Returns: <list of AEItem>
|
||||
payload(dict): - dictionary from json representation, expected to
|
||||
come from _handle_return
|
||||
"""
|
||||
if not payload:
|
||||
return []
|
||||
|
||||
if isinstance(payload, str): # safety fallback
|
||||
try:
|
||||
payload = json.loads(payload)
|
||||
except json.decoder.JSONDecodeError:
|
||||
raise ValueError("Received broken JSON {}".format(payload))
|
||||
|
||||
if isinstance(payload, dict):
|
||||
payload = [payload]
|
||||
|
||||
ret = []
|
||||
# convert to AEItem to use dot donation
|
||||
for d in payload:
|
||||
if not d:
|
||||
continue
|
||||
# currently implemented and expected fields
|
||||
item = AEItem(d.get('id'),
|
||||
d.get('name'),
|
||||
d.get('type'),
|
||||
d.get('members'),
|
||||
d.get('frameStart'),
|
||||
d.get('framesDuration'),
|
||||
d.get('frameRate'),
|
||||
d.get('file_name'),
|
||||
d.get("instance_id"),
|
||||
d.get("width"),
|
||||
d.get("height"),
|
||||
d.get("is_placeholder"),
|
||||
d.get("uuid"),
|
||||
d.get("path"),
|
||||
d.get("containing_comps"),)
|
||||
|
||||
ret.append(item)
|
||||
return ret
|
||||
|
||||
|
||||
def get_stub():
|
||||
"""
|
||||
Convenience function to get server RPC stub to call methods directed
|
||||
for host (Photoshop).
|
||||
It expects already created connection, started from client.
|
||||
Currently created when panel is opened (PS: Window>Extensions>Avalon)
|
||||
:return: <PhotoshopClientStub> where functions could be called from
|
||||
"""
|
||||
ae_stub = AfterEffectsServerStub()
|
||||
if not ae_stub.client:
|
||||
raise ConnectionNotEstablishedYet("Connection is not created yet")
|
||||
|
||||
return ae_stub
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
|
||||
from ayon_core.lib import (
|
||||
get_ayon_launcher_args,
|
||||
is_using_ayon_console,
|
||||
)
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_aftereffects import get_launch_script_path
|
||||
|
||||
|
||||
def get_launch_kwargs(kwargs):
|
||||
"""Explicit setting of kwargs for Popen for AfterEffects.
|
||||
|
||||
Expected behavior
|
||||
- ayon_console opens window with logs
|
||||
- ayon has stdout/stderr available for capturing
|
||||
|
||||
Args:
|
||||
kwargs (Union[dict, None]): Current kwargs or None.
|
||||
|
||||
"""
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
|
||||
if platform.system().lower() != "windows":
|
||||
return kwargs
|
||||
|
||||
if is_using_ayon_console():
|
||||
kwargs.update({
|
||||
"creationflags": subprocess.CREATE_NEW_CONSOLE
|
||||
})
|
||||
else:
|
||||
kwargs.update({
|
||||
"creationflags": subprocess.CREATE_NO_WINDOW,
|
||||
"stdout": subprocess.DEVNULL,
|
||||
"stderr": subprocess.DEVNULL
|
||||
})
|
||||
return kwargs
|
||||
|
||||
|
||||
class AEPrelaunchHook(PreLaunchHook):
|
||||
"""Launch arguments preparation.
|
||||
|
||||
Hook add python executable and script path to AE implementation before
|
||||
AE executable and add last workfile path to launch arguments.
|
||||
|
||||
Existence of last workfile is checked. If workfile does not exists tries
|
||||
to copy templated workfile from predefined path.
|
||||
"""
|
||||
app_groups = {"aftereffects"}
|
||||
|
||||
order = 20
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
# Pop executable
|
||||
executable_path = self.launch_context.launch_args.pop(0)
|
||||
|
||||
# Pop rest of launch arguments - There should not be other arguments!
|
||||
remainders = []
|
||||
while self.launch_context.launch_args:
|
||||
remainders.append(self.launch_context.launch_args.pop(0))
|
||||
|
||||
script_path = get_launch_script_path()
|
||||
|
||||
new_launch_args = get_ayon_launcher_args(
|
||||
"run", script_path, executable_path
|
||||
)
|
||||
# Add workfile path if exists
|
||||
workfile_path = self.data["last_workfile_path"]
|
||||
if (
|
||||
self.data.get("start_last_workfile")
|
||||
and workfile_path
|
||||
and os.path.exists(workfile_path)
|
||||
):
|
||||
new_launch_args.append(workfile_path)
|
||||
|
||||
# Append as whole list as these arguments should not be separated
|
||||
self.launch_context.launch_args.append(new_launch_args)
|
||||
|
||||
if remainders:
|
||||
self.launch_context.launch_args.extend(remainders)
|
||||
|
||||
self.launch_context.kwargs = get_launch_kwargs(
|
||||
self.launch_context.kwargs
|
||||
)
|
||||
|
|
@ -1,260 +0,0 @@
|
|||
import re
|
||||
|
||||
from ayon_core import resources
|
||||
from ayon_core.lib import BoolDef, UISeparatorDef
|
||||
from ayon_core.pipeline import (
|
||||
Creator,
|
||||
CreatedInstance,
|
||||
CreatorError
|
||||
)
|
||||
from ayon_core.lib import prepare_template_data
|
||||
from ayon_core.pipeline.create import PRODUCT_NAME_ALLOWED_SYMBOLS
|
||||
from ayon_aftereffects import api
|
||||
from ayon_aftereffects.api.pipeline import cache_and_get_instances
|
||||
from ayon_aftereffects.api.lib import set_settings
|
||||
|
||||
|
||||
class RenderCreator(Creator):
|
||||
"""Creates 'render' instance for publishing.
|
||||
|
||||
Result of 'render' instance is video or sequence of images for particular
|
||||
composition based of configuration in its RenderQueue.
|
||||
"""
|
||||
identifier = "render"
|
||||
label = "Render"
|
||||
product_type = "render"
|
||||
description = "Render creator"
|
||||
|
||||
create_allow_context_change = True
|
||||
|
||||
# Settings
|
||||
mark_for_review = True
|
||||
force_setting_values = True
|
||||
|
||||
def create(self, product_name, data, pre_create_data):
|
||||
stub = api.get_stub() # only after After Effects is up
|
||||
|
||||
try:
|
||||
_ = stub.get_active_document_full_name()
|
||||
except ValueError:
|
||||
raise CreatorError(
|
||||
"Please save workfile via Workfile app first!"
|
||||
)
|
||||
|
||||
if pre_create_data.get("use_selection"):
|
||||
comps = stub.get_selected_items(
|
||||
comps=True, folders=False, footages=False
|
||||
)
|
||||
else:
|
||||
comps = stub.get_items(comps=True, folders=False, footages=False)
|
||||
|
||||
if not comps:
|
||||
raise CreatorError(
|
||||
"Nothing to create. Select composition in Project Bin if "
|
||||
"'Use selection' is toggled or create at least "
|
||||
"one composition."
|
||||
)
|
||||
use_composition_name = (pre_create_data.get("use_composition_name") or
|
||||
len(comps) > 1)
|
||||
for comp in comps:
|
||||
composition_name = re.sub(
|
||||
"[^{}]+".format(PRODUCT_NAME_ALLOWED_SYMBOLS),
|
||||
"",
|
||||
comp.name
|
||||
)
|
||||
if use_composition_name:
|
||||
if "{composition}" not in product_name.lower():
|
||||
product_name += "{Composition}"
|
||||
|
||||
dynamic_fill = prepare_template_data({"composition":
|
||||
composition_name})
|
||||
comp_product_name = product_name.format(**dynamic_fill)
|
||||
data["composition_name"] = composition_name
|
||||
else:
|
||||
comp_product_name = re.sub(
|
||||
r"\{composition\}",
|
||||
"",
|
||||
product_name,
|
||||
flags=re.IGNORECASE
|
||||
)
|
||||
|
||||
for inst in self.create_context.instances:
|
||||
if comp_product_name == inst.product_name:
|
||||
raise CreatorError("{} already exists".format(
|
||||
inst.product_name))
|
||||
|
||||
data["members"] = [comp.id]
|
||||
data["orig_comp_name"] = composition_name
|
||||
|
||||
new_instance = CreatedInstance(
|
||||
self.product_type, comp_product_name, data, self
|
||||
)
|
||||
if "farm" in pre_create_data:
|
||||
use_farm = pre_create_data["farm"]
|
||||
new_instance.creator_attributes["farm"] = use_farm
|
||||
|
||||
review = pre_create_data["mark_for_review"]
|
||||
new_instance.creator_attributes["mark_for_review"] = review
|
||||
|
||||
api.get_stub().imprint(new_instance.id,
|
||||
new_instance.data_to_store())
|
||||
self._add_instance_to_context(new_instance)
|
||||
|
||||
stub.rename_item(comp.id, comp_product_name)
|
||||
if self.force_setting_values:
|
||||
set_settings(True, True, [comp.id], print_msg=False)
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
output = [
|
||||
BoolDef("use_selection",
|
||||
tooltip="Composition for publishable instance should be "
|
||||
"selected by default.",
|
||||
default=True, label="Use selection"),
|
||||
BoolDef("use_composition_name",
|
||||
label="Use composition name in product"),
|
||||
UISeparatorDef(),
|
||||
BoolDef("farm", label="Render on farm"),
|
||||
BoolDef(
|
||||
"mark_for_review",
|
||||
label="Review",
|
||||
default=self.mark_for_review
|
||||
)
|
||||
]
|
||||
return output
|
||||
|
||||
def get_instance_attr_defs(self):
|
||||
return [
|
||||
BoolDef("farm", label="Render on farm"),
|
||||
BoolDef(
|
||||
"mark_for_review",
|
||||
label="Review",
|
||||
default=False
|
||||
)
|
||||
]
|
||||
|
||||
def get_icon(self):
|
||||
return resources.get_openpype_splash_filepath()
|
||||
|
||||
def collect_instances(self):
|
||||
for instance_data in cache_and_get_instances(self):
|
||||
# legacy instances have product_type=='render' or 'renderLocal', use them
|
||||
creator_id = instance_data.get("creator_identifier")
|
||||
if not creator_id:
|
||||
# NOTE this is for backwards compatibility but probably can be
|
||||
# removed
|
||||
creator_id = instance_data.get("family", "")
|
||||
creator_id = creator_id.replace("Local", "")
|
||||
|
||||
if creator_id == self.identifier:
|
||||
instance_data = self._handle_legacy(instance_data)
|
||||
instance = CreatedInstance.from_existing(
|
||||
instance_data, self
|
||||
)
|
||||
self._add_instance_to_context(instance)
|
||||
|
||||
def update_instances(self, update_list):
|
||||
for created_inst, _changes in update_list:
|
||||
api.get_stub().imprint(created_inst.get("instance_id"),
|
||||
created_inst.data_to_store())
|
||||
name_change = _changes.get("productName")
|
||||
if name_change:
|
||||
api.get_stub().rename_item(created_inst.data["members"][0],
|
||||
name_change.new_value)
|
||||
|
||||
def remove_instances(self, instances):
|
||||
"""Removes metadata and renames to original comp name if available."""
|
||||
for instance in instances:
|
||||
self._remove_instance_from_context(instance)
|
||||
self.host.remove_instance(instance)
|
||||
|
||||
comp_id = instance.data["members"][0]
|
||||
comp = api.get_stub().get_item(comp_id)
|
||||
orig_comp_name = instance.data.get("orig_comp_name")
|
||||
if comp:
|
||||
if orig_comp_name:
|
||||
new_comp_name = orig_comp_name
|
||||
else:
|
||||
new_comp_name = "dummyCompName"
|
||||
api.get_stub().rename_item(comp_id,
|
||||
new_comp_name)
|
||||
|
||||
def apply_settings(self, project_settings):
|
||||
plugin_settings = (
|
||||
project_settings["aftereffects"]["create"]["RenderCreator"]
|
||||
)
|
||||
|
||||
self.mark_for_review = plugin_settings["mark_for_review"]
|
||||
self.default_variants = plugin_settings.get(
|
||||
"default_variants",
|
||||
plugin_settings.get("defaults") or []
|
||||
)
|
||||
|
||||
def get_detail_description(self):
|
||||
return """Creator for Render instances
|
||||
|
||||
Main publishable item in AfterEffects will be of `render` product type.
|
||||
Result of this item (instance) is picture sequence or video that could
|
||||
be a final delivery product or loaded and used in another DCCs.
|
||||
|
||||
Select single composition and create instance of 'render' product type
|
||||
or turn off 'Use selection' to create instance for all compositions.
|
||||
|
||||
'Use composition name in product' allows to explicitly add composition
|
||||
name into created product name.
|
||||
|
||||
Position of composition name could be set in
|
||||
`project_settings/global/tools/creator/product_name_profiles` with
|
||||
some form of '{composition}' placeholder.
|
||||
|
||||
Composition name will be used implicitly if multiple composition should
|
||||
be handled at same time.
|
||||
|
||||
If {composition} placeholder is not us 'product_name_profiles'
|
||||
composition name will be capitalized and set at the end of
|
||||
product name if necessary.
|
||||
|
||||
If composition name should be used, it will be cleaned up of characters
|
||||
that would cause an issue in published file names.
|
||||
"""
|
||||
|
||||
def get_dynamic_data(
|
||||
self,
|
||||
project_name,
|
||||
folder_entity,
|
||||
task_entity,
|
||||
variant,
|
||||
host_name,
|
||||
instance
|
||||
):
|
||||
dynamic_data = {}
|
||||
if instance is not None:
|
||||
composition_name = instance.get("composition_name")
|
||||
if composition_name:
|
||||
dynamic_data["composition"] = composition_name
|
||||
else:
|
||||
dynamic_data["composition"] = "{composition}"
|
||||
|
||||
return dynamic_data
|
||||
|
||||
def _handle_legacy(self, instance_data):
|
||||
"""Converts old instances to new format."""
|
||||
if not instance_data.get("members"):
|
||||
instance_data["members"] = [instance_data.get("uuid")]
|
||||
|
||||
if instance_data.get("uuid"):
|
||||
# uuid not needed, replaced with unique instance_id
|
||||
api.get_stub().remove_instance(instance_data.get("uuid"))
|
||||
instance_data.pop("uuid")
|
||||
|
||||
if not instance_data.get("task"):
|
||||
instance_data["task"] = self.create_context.get_current_task_name()
|
||||
|
||||
if not instance_data.get("creator_attributes"):
|
||||
is_old_farm = instance_data.get("family") != "renderLocal"
|
||||
instance_data["creator_attributes"] = {"farm": is_old_farm}
|
||||
instance_data["productType"] = self.product_type
|
||||
|
||||
if instance_data["creator_attributes"].get("mark_for_review") is None:
|
||||
instance_data["creator_attributes"]["mark_for_review"] = True
|
||||
|
||||
return instance_data
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
import ayon_api
|
||||
|
||||
from ayon_core.pipeline import (
|
||||
AutoCreator,
|
||||
CreatedInstance
|
||||
)
|
||||
from ayon_aftereffects import api
|
||||
from ayon_aftereffects.api.pipeline import cache_and_get_instances
|
||||
|
||||
|
||||
class AEWorkfileCreator(AutoCreator):
|
||||
identifier = "workfile"
|
||||
product_type = "workfile"
|
||||
|
||||
default_variant = "Main"
|
||||
|
||||
def get_instance_attr_defs(self):
|
||||
return []
|
||||
|
||||
def collect_instances(self):
|
||||
for instance_data in cache_and_get_instances(self):
|
||||
creator_id = instance_data.get("creator_identifier")
|
||||
if creator_id == self.identifier:
|
||||
product_name = instance_data["productName"]
|
||||
instance = CreatedInstance(
|
||||
self.product_type, product_name, instance_data, self
|
||||
)
|
||||
self._add_instance_to_context(instance)
|
||||
|
||||
def update_instances(self, update_list):
|
||||
# nothing to change on workfiles
|
||||
pass
|
||||
|
||||
def create(self, options=None):
|
||||
existing_instance = None
|
||||
for instance in self.create_context.instances:
|
||||
if instance.product_type == self.product_type:
|
||||
existing_instance = instance
|
||||
break
|
||||
|
||||
context = self.create_context
|
||||
project_name = context.get_current_project_name()
|
||||
folder_path = context.get_current_folder_path()
|
||||
task_name = context.get_current_task_name()
|
||||
host_name = context.host_name
|
||||
|
||||
existing_folder_path = None
|
||||
if existing_instance is not None:
|
||||
existing_folder_path = existing_instance.get("folderPath")
|
||||
|
||||
if existing_instance is None:
|
||||
folder_entity = ayon_api.get_folder_by_path(
|
||||
project_name, folder_path
|
||||
)
|
||||
task_entity = ayon_api.get_task_by_name(
|
||||
project_name, folder_entity["id"], task_name
|
||||
)
|
||||
product_name = self.get_product_name(
|
||||
project_name,
|
||||
folder_entity,
|
||||
task_entity,
|
||||
self.default_variant,
|
||||
host_name,
|
||||
)
|
||||
data = {
|
||||
"folderPath": folder_path,
|
||||
"task": task_name,
|
||||
"variant": self.default_variant,
|
||||
}
|
||||
data.update(self.get_dynamic_data(
|
||||
project_name,
|
||||
folder_entity,
|
||||
task_entity,
|
||||
self.default_variant,
|
||||
host_name,
|
||||
None,
|
||||
))
|
||||
|
||||
new_instance = CreatedInstance(
|
||||
self.product_type, product_name, data, self
|
||||
)
|
||||
self._add_instance_to_context(new_instance)
|
||||
|
||||
api.get_stub().imprint(new_instance.get("instance_id"),
|
||||
new_instance.data_to_store())
|
||||
|
||||
elif (
|
||||
existing_folder_path != folder_path
|
||||
or existing_instance["task"] != task_name
|
||||
):
|
||||
folder_entity = ayon_api.get_folder_by_path(
|
||||
project_name, folder_path
|
||||
)
|
||||
task_entity = ayon_api.get_task_by_name(
|
||||
project_name, folder_entity["id"], task_name
|
||||
)
|
||||
product_name = self.get_product_name(
|
||||
project_name,
|
||||
folder_entity,
|
||||
task_entity,
|
||||
self.default_variant,
|
||||
host_name,
|
||||
)
|
||||
existing_instance["folderPath"] = folder_path
|
||||
existing_instance["task"] = task_name
|
||||
existing_instance["productName"] = product_name
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
import re
|
||||
|
||||
from ayon_core.pipeline import get_representation_path
|
||||
|
||||
from ayon_aftereffects import api
|
||||
from ayon_aftereffects.api.lib import (
|
||||
get_background_layers,
|
||||
get_unique_layer_name,
|
||||
)
|
||||
|
||||
|
||||
class BackgroundLoader(api.AfterEffectsLoader):
|
||||
"""
|
||||
Load images from Background product type
|
||||
Creates for each background separate folder with all imported images
|
||||
from background json AND automatically created composition with layers,
|
||||
each layer for separate image.
|
||||
|
||||
For each load container is created and stored in project (.aep)
|
||||
metadata
|
||||
"""
|
||||
label = "Load JSON Background"
|
||||
product_types = {"background"}
|
||||
representations = {"json"}
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
stub = self.get_stub()
|
||||
items = stub.get_items(comps=True)
|
||||
existing_items = [layer.name.replace(stub.LOADED_ICON, '')
|
||||
for layer in items]
|
||||
|
||||
comp_name = get_unique_layer_name(
|
||||
existing_items,
|
||||
"{}_{}".format(context["folder"]["name"], name))
|
||||
|
||||
path = self.filepath_from_context(context)
|
||||
layers = get_background_layers(path)
|
||||
if not layers:
|
||||
raise ValueError("No layers found in {}".format(path))
|
||||
|
||||
comp = stub.import_background(None, stub.LOADED_ICON + comp_name,
|
||||
layers)
|
||||
|
||||
if not comp:
|
||||
raise ValueError("Import background failed. "
|
||||
"Please contact support")
|
||||
|
||||
self[:] = [comp]
|
||||
namespace = namespace or comp_name
|
||||
|
||||
return api.containerise(
|
||||
name,
|
||||
namespace,
|
||||
comp,
|
||||
context,
|
||||
self.__class__.__name__
|
||||
)
|
||||
|
||||
def update(self, container, context):
|
||||
""" Switch asset or change version """
|
||||
stub = self.get_stub()
|
||||
folder_name = context["folder"]["name"]
|
||||
product_name = context["product"]["name"]
|
||||
repre_entity = context["representation"]
|
||||
|
||||
_ = container.pop("layer")
|
||||
|
||||
# without iterator number (_001, 002...)
|
||||
namespace_from_container = re.sub(r'_\d{3}$', '',
|
||||
container["namespace"])
|
||||
comp_name = "{}_{}".format(folder_name, product_name)
|
||||
|
||||
# switching assets
|
||||
if namespace_from_container != comp_name:
|
||||
items = stub.get_items(comps=True)
|
||||
existing_items = [layer.name for layer in items]
|
||||
comp_name = get_unique_layer_name(
|
||||
existing_items,
|
||||
"{}_{}".format(folder_name, product_name))
|
||||
else: # switching version - keep same name
|
||||
comp_name = container["namespace"]
|
||||
|
||||
path = get_representation_path(repre_entity)
|
||||
|
||||
layers = get_background_layers(path)
|
||||
comp = stub.reload_background(container["members"][1],
|
||||
stub.LOADED_ICON + comp_name,
|
||||
layers)
|
||||
|
||||
# update container
|
||||
container["representation"] = repre_entity["id"]
|
||||
container["name"] = product_name
|
||||
container["namespace"] = comp_name
|
||||
container["members"] = comp.members
|
||||
|
||||
stub.imprint(comp.id, container)
|
||||
|
||||
def remove(self, container):
|
||||
"""
|
||||
Removes element from scene: deletes layer + removes from file
|
||||
metadata.
|
||||
Args:
|
||||
container (dict): container to be removed - used to get layer_id
|
||||
"""
|
||||
stub = self.get_stub()
|
||||
layer = container.pop("layer")
|
||||
stub.imprint(layer.id, {})
|
||||
stub.delete_item(layer.id)
|
||||
|
||||
def switch(self, container, context):
|
||||
self.update(container, context)
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
import re
|
||||
import os
|
||||
from ayon_core.pipeline import get_representation_path
|
||||
from ayon_aftereffects import api
|
||||
from ayon_aftereffects.api.lib import get_unique_layer_name
|
||||
|
||||
|
||||
class FileLoader(api.AfterEffectsLoader):
|
||||
"""Load images
|
||||
|
||||
Stores the imported asset in a container named after the asset.
|
||||
"""
|
||||
label = "Load file"
|
||||
|
||||
product_types = {
|
||||
"image",
|
||||
"plate",
|
||||
"render",
|
||||
"prerender",
|
||||
"review",
|
||||
"audio",
|
||||
}
|
||||
representations = {"*"}
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
stub = self.get_stub()
|
||||
selected_folders = stub.get_selected_items(
|
||||
comps=False, folders=True, footages=False)
|
||||
if selected_folders:
|
||||
stub.select_items([folder.id for folder in selected_folders])
|
||||
layers = stub.get_items(comps=True, folders=True, footages=True)
|
||||
existing_layers = [layer.name for layer in layers]
|
||||
comp_name = get_unique_layer_name(
|
||||
existing_layers, "{}_{}".format(
|
||||
context["folder"]["name"], name
|
||||
)
|
||||
)
|
||||
|
||||
import_options = {}
|
||||
|
||||
path = self.filepath_from_context(context)
|
||||
|
||||
if len(context["representation"]["files"]) > 1:
|
||||
import_options['sequence'] = True
|
||||
|
||||
if not path:
|
||||
repr_id = context["representation"]["id"]
|
||||
self.log.warning(
|
||||
"Representation id `{}` is failing to load".format(repr_id))
|
||||
return
|
||||
|
||||
path = path.replace("\\", "/")
|
||||
if '.psd' in path:
|
||||
import_options['ImportAsType'] = 'ImportAsType.COMP'
|
||||
|
||||
comp = stub.import_file(path, stub.LOADED_ICON + comp_name,
|
||||
import_options)
|
||||
if not comp:
|
||||
self.log.warning(
|
||||
"Representation `{}` is failing to load".format(path))
|
||||
self.log.warning("Check host app for alert error.")
|
||||
return
|
||||
|
||||
self[:] = [comp]
|
||||
namespace = namespace or comp_name
|
||||
return api.containerise(
|
||||
name,
|
||||
namespace,
|
||||
comp,
|
||||
context,
|
||||
self.__class__.__name__
|
||||
)
|
||||
|
||||
def update(self, container, context):
|
||||
""" Switch asset or change version """
|
||||
stub = self.get_stub()
|
||||
layer = container.pop("layer")
|
||||
|
||||
folder_name = context["folder"]["name"]
|
||||
product_name = context["product"]["name"]
|
||||
repre_entity = context["representation"]
|
||||
|
||||
namespace_from_container = re.sub(r'_\d{3}$', '',
|
||||
container["namespace"])
|
||||
layer_name = "{}_{}".format(folder_name, product_name)
|
||||
# switching assets
|
||||
if namespace_from_container != layer_name:
|
||||
layers = stub.get_items(comps=True)
|
||||
existing_layers = [layer.name for layer in layers]
|
||||
layer_name = get_unique_layer_name(
|
||||
existing_layers,
|
||||
"{}_{}".format(folder_name, product_name))
|
||||
else: # switching version - keep same name
|
||||
layer_name = container["namespace"]
|
||||
path = get_representation_path(repre_entity)
|
||||
|
||||
if len(repre_entity["files"]) > 1:
|
||||
path = os.path.dirname(path)
|
||||
# with aftereffects.maintained_selection(): # TODO
|
||||
stub.replace_item(layer.id, path, stub.LOADED_ICON + layer_name)
|
||||
stub.imprint(
|
||||
layer.id, {"representation": repre_entity["id"],
|
||||
"name": product_name,
|
||||
"namespace": layer_name}
|
||||
)
|
||||
|
||||
def remove(self, container):
|
||||
"""
|
||||
Removes element from scene: deletes layer + removes from Headline
|
||||
Args:
|
||||
container (dict): container to be removed - used to get layer_id
|
||||
"""
|
||||
stub = self.get_stub()
|
||||
layer = container.pop("layer")
|
||||
stub.imprint(layer.id, {})
|
||||
stub.delete_item(layer.id)
|
||||
|
||||
def switch(self, container, context):
|
||||
self.update(container, context)
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import pyblish.api
|
||||
|
||||
from ayon_aftereffects.api import get_stub
|
||||
|
||||
|
||||
class AddPublishHighlight(pyblish.api.InstancePlugin):
|
||||
"""
|
||||
Revert back rendered comp name and add publish highlight
|
||||
"""
|
||||
|
||||
label = "Add render highlight"
|
||||
order = pyblish.api.IntegratorOrder + 8.0
|
||||
hosts = ["aftereffects"]
|
||||
families = ["render.farm"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
stub = get_stub()
|
||||
item = instance.data
|
||||
# comp name contains highlight icon
|
||||
stub.rename_item(item["comp_id"], item["comp_name"])
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Close AE after publish. For Webpublishing only."""
|
||||
import pyblish.api
|
||||
|
||||
from ayon_aftereffects.api import get_stub
|
||||
|
||||
|
||||
class CloseAE(pyblish.api.ContextPlugin):
|
||||
"""Close AE after publish. For Webpublishing only.
|
||||
"""
|
||||
|
||||
order = pyblish.api.IntegratorOrder + 14
|
||||
label = "Close AE"
|
||||
optional = True
|
||||
active = True
|
||||
|
||||
hosts = ["aftereffects"]
|
||||
targets = ["automated"]
|
||||
|
||||
def process(self, context):
|
||||
self.log.info("CloseAE")
|
||||
|
||||
stub = get_stub()
|
||||
self.log.info("Shutting down AE")
|
||||
stub.save()
|
||||
stub.close()
|
||||
self.log.info("AE closed")
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from ayon_aftereffects.api import get_stub
|
||||
|
||||
|
||||
class CollectAudio(pyblish.api.ContextPlugin):
|
||||
"""Inject audio file url for rendered composition into context.
|
||||
Needs to run AFTER 'collect_render'. Use collected comp_id to check
|
||||
if there is an AVLayer in this composition
|
||||
"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.499
|
||||
label = "Collect Audio"
|
||||
hosts = ["aftereffects"]
|
||||
|
||||
def process(self, context):
|
||||
for instance in context:
|
||||
if 'render.farm' in instance.data.get("families", []):
|
||||
comp_id = instance.data["comp_id"]
|
||||
if not comp_id:
|
||||
self.log.debug("No comp_id filled in instance")
|
||||
continue
|
||||
context.data["audioFile"] = os.path.normpath(
|
||||
get_stub().get_audio_url(comp_id)
|
||||
).replace("\\", "/")
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from ayon_aftereffects.api import get_stub
|
||||
|
||||
|
||||
class CollectCurrentFile(pyblish.api.ContextPlugin):
|
||||
"""Inject the current working file into context"""
|
||||
|
||||
order = pyblish.api.CollectorOrder - 0.49
|
||||
label = "Current File"
|
||||
hosts = ["aftereffects"]
|
||||
|
||||
def process(self, context):
|
||||
context.data["currentFile"] = os.path.normpath(
|
||||
get_stub().get_active_document_full_name()
|
||||
).replace("\\", "/")
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
import os
|
||||
import re
|
||||
import pyblish.api
|
||||
|
||||
from ayon_aftereffects.api import (
|
||||
get_stub,
|
||||
get_extension_manifest_path
|
||||
)
|
||||
|
||||
|
||||
class CollectExtensionVersion(pyblish.api.ContextPlugin):
|
||||
""" Pulls and compares version of installed extension.
|
||||
|
||||
It is recommended to use same extension as in provided Openpype code.
|
||||
|
||||
Please use Anastasiy’s Extension Manager or ZXPInstaller to update
|
||||
extension in case of an error.
|
||||
|
||||
You can locate extension.zxp in your installed Openpype code in
|
||||
`repos/avalon-core/avalon/aftereffects`
|
||||
"""
|
||||
# This technically should be a validator, but other collectors might be
|
||||
# impacted with usage of obsolete extension, so collector that runs first
|
||||
# was chosen
|
||||
order = pyblish.api.CollectorOrder - 0.5
|
||||
label = "Collect extension version"
|
||||
hosts = ["aftereffects"]
|
||||
|
||||
optional = True
|
||||
active = True
|
||||
|
||||
def process(self, context):
|
||||
installed_version = get_stub().get_extension_version()
|
||||
|
||||
if not installed_version:
|
||||
raise ValueError("Unknown version, probably old extension")
|
||||
|
||||
manifest_url = get_extension_manifest_path()
|
||||
|
||||
if not os.path.exists(manifest_url):
|
||||
self.log.debug("Unable to locate extension manifest, not checking")
|
||||
return
|
||||
|
||||
expected_version = None
|
||||
with open(manifest_url) as fp:
|
||||
content = fp.read()
|
||||
found = re.findall(r'(ExtensionBundleVersion=")([0-9\.]+)(")',
|
||||
content)
|
||||
if found:
|
||||
expected_version = found[0][1]
|
||||
|
||||
if expected_version != installed_version:
|
||||
msg = (
|
||||
"Expected version '{}' found '{}'\n Please update"
|
||||
" your installed extension, it might not work properly."
|
||||
).format(expected_version, installed_version)
|
||||
|
||||
raise ValueError(msg)
|
||||
|
|
@ -1,234 +0,0 @@
|
|||
import os
|
||||
import tempfile
|
||||
|
||||
import attr
|
||||
import pyblish.api
|
||||
|
||||
from ayon_core.pipeline import publish
|
||||
from ayon_core.pipeline.publish import RenderInstance
|
||||
|
||||
from ayon_aftereffects.api import get_stub
|
||||
|
||||
@attr.s
|
||||
class AERenderInstance(RenderInstance):
|
||||
# extend generic, composition name is needed
|
||||
comp_name = attr.ib(default=None)
|
||||
comp_id = attr.ib(default=None)
|
||||
fps = attr.ib(default=None)
|
||||
projectEntity = attr.ib(default=None)
|
||||
stagingDir = attr.ib(default=None)
|
||||
app_version = attr.ib(default=None)
|
||||
publish_attributes = attr.ib(default={})
|
||||
file_names = attr.ib(default=[])
|
||||
|
||||
|
||||
class CollectAERender(publish.AbstractCollectRender):
|
||||
"""Prepares RenderInstance.
|
||||
|
||||
RenderInstance is meant to replace simple dictionaries to provide code
|
||||
assist and typing. (Currently used only in AE, Harmony though.)
|
||||
|
||||
This must run after `collect_review`, but before Deadline plugins (which
|
||||
should be run only on renderable instances.)
|
||||
"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.125
|
||||
label = "Collect After Effects Render Layers"
|
||||
hosts = ["aftereffects"]
|
||||
|
||||
padding_width = 6
|
||||
rendered_extension = 'png'
|
||||
|
||||
_stub = None
|
||||
|
||||
@classmethod
|
||||
def get_stub(cls):
|
||||
if not cls._stub:
|
||||
cls._stub = get_stub()
|
||||
return cls._stub
|
||||
|
||||
def get_instances(self, context):
|
||||
instances = []
|
||||
|
||||
app_version = CollectAERender.get_stub().get_app_version()
|
||||
app_version = app_version[0:4]
|
||||
|
||||
current_file = context.data["currentFile"]
|
||||
version = context.data["version"]
|
||||
|
||||
project_entity = context.data["projectEntity"]
|
||||
|
||||
compositions = CollectAERender.get_stub().get_items(True)
|
||||
compositions_by_id = {item.id: item for item in compositions}
|
||||
for inst in context:
|
||||
if not inst.data.get("active", True):
|
||||
continue
|
||||
|
||||
product_type = inst.data["productType"]
|
||||
if product_type not in ["render", "renderLocal"]: # legacy
|
||||
continue
|
||||
|
||||
comp_id = int(inst.data["members"][0])
|
||||
|
||||
comp_info = CollectAERender.get_stub().get_comp_properties(
|
||||
comp_id)
|
||||
|
||||
if not comp_info:
|
||||
self.log.warning("Orphaned instance, deleting metadata")
|
||||
inst_id = inst.data.get("instance_id") or str(comp_id)
|
||||
CollectAERender.get_stub().remove_instance(inst_id)
|
||||
continue
|
||||
|
||||
frame_start = comp_info.frameStart
|
||||
frame_end = round(comp_info.frameStart +
|
||||
comp_info.framesDuration) - 1
|
||||
fps = comp_info.frameRate
|
||||
# TODO add resolution when supported by extension
|
||||
|
||||
task_name = inst.data.get("task")
|
||||
|
||||
render_q = CollectAERender.get_stub().get_render_info(comp_id)
|
||||
if not render_q:
|
||||
raise ValueError("No file extension set in Render Queue")
|
||||
render_item = render_q[0]
|
||||
|
||||
product_type = "render"
|
||||
instance_families = inst.data.get("families", [])
|
||||
instance_families.append(product_type)
|
||||
product_name = inst.data["productName"]
|
||||
instance = AERenderInstance(
|
||||
productType=product_type,
|
||||
family=product_type,
|
||||
families=instance_families,
|
||||
version=version,
|
||||
time="",
|
||||
source=current_file,
|
||||
label="{} - {}".format(product_name, product_type),
|
||||
productName=product_name,
|
||||
folderPath=inst.data["folderPath"],
|
||||
task=task_name,
|
||||
attachTo=False,
|
||||
setMembers='',
|
||||
publish=True,
|
||||
name=product_name,
|
||||
resolutionWidth=render_item.width,
|
||||
resolutionHeight=render_item.height,
|
||||
pixelAspect=1,
|
||||
tileRendering=False,
|
||||
tilesX=0,
|
||||
tilesY=0,
|
||||
review="review" in instance_families,
|
||||
frameStart=frame_start,
|
||||
frameEnd=frame_end,
|
||||
frameStep=1,
|
||||
fps=fps,
|
||||
app_version=app_version,
|
||||
publish_attributes=inst.data.get("publish_attributes", {}),
|
||||
file_names=[item.file_name for item in render_q],
|
||||
|
||||
# The source instance this render instance replaces
|
||||
source_instance=inst
|
||||
)
|
||||
|
||||
comp = compositions_by_id.get(comp_id)
|
||||
if not comp:
|
||||
raise ValueError("There is no composition for item {}".
|
||||
format(comp_id))
|
||||
instance.outputDir = self._get_output_dir(instance)
|
||||
instance.comp_name = comp.name
|
||||
instance.comp_id = comp_id
|
||||
|
||||
is_local = "renderLocal" in inst.data["family"] # legacy
|
||||
if inst.data.get("creator_attributes"):
|
||||
is_local = not inst.data["creator_attributes"].get("farm")
|
||||
if is_local:
|
||||
# for local renders
|
||||
instance = self._update_for_local(instance, project_entity)
|
||||
else:
|
||||
fam = "render.farm"
|
||||
if fam not in instance.families:
|
||||
instance.families.append(fam)
|
||||
instance.renderer = "aerender"
|
||||
instance.farm = True # to skip integrate
|
||||
if "review" in instance.families:
|
||||
# to skip ExtractReview locally
|
||||
instance.families.remove("review")
|
||||
instance.deadline = inst.data.get("deadline")
|
||||
|
||||
instances.append(instance)
|
||||
|
||||
return instances
|
||||
|
||||
def get_expected_files(self, render_instance):
|
||||
"""
|
||||
Returns list of rendered files that should be created by
|
||||
Deadline. These are not published directly, they are source
|
||||
for later 'submit_publish_job'.
|
||||
|
||||
Args:
|
||||
render_instance (RenderInstance): to pull anatomy and parts used
|
||||
in url
|
||||
|
||||
Returns:
|
||||
(list) of absolute urls to rendered file
|
||||
"""
|
||||
start = render_instance.frameStart
|
||||
end = render_instance.frameEnd
|
||||
|
||||
base_dir = self._get_output_dir(render_instance)
|
||||
expected_files = []
|
||||
for file_name in render_instance.file_names:
|
||||
_, ext = os.path.splitext(os.path.basename(file_name))
|
||||
ext = ext.replace('.', '')
|
||||
version_str = "v{:03d}".format(render_instance.version)
|
||||
if "#" not in file_name: # single frame (mov)
|
||||
file_name = "{}_{}.{}".format(
|
||||
render_instance.productName,
|
||||
version_str,
|
||||
ext
|
||||
)
|
||||
file_path = os.path.join(base_dir, file_name)
|
||||
expected_files.append(file_path)
|
||||
else:
|
||||
for frame in range(start, end + 1):
|
||||
file_name = "{}_{}.{}.{}".format(
|
||||
render_instance.productName,
|
||||
version_str,
|
||||
str(frame).zfill(self.padding_width),
|
||||
ext
|
||||
)
|
||||
|
||||
file_path = os.path.join(base_dir, file_name)
|
||||
expected_files.append(file_path)
|
||||
return expected_files
|
||||
|
||||
def _get_output_dir(self, render_instance):
|
||||
"""
|
||||
Returns dir path of rendered files, used in submit_publish_job
|
||||
for metadata.json location.
|
||||
Should be in separate folder inside of work area.
|
||||
|
||||
Args:
|
||||
render_instance (RenderInstance):
|
||||
|
||||
Returns:
|
||||
(str): absolute path to rendered files
|
||||
"""
|
||||
# render to folder of workfile
|
||||
base_dir = os.path.dirname(render_instance.source)
|
||||
file_name, _ = os.path.splitext(
|
||||
os.path.basename(render_instance.source))
|
||||
base_dir = os.path.join(base_dir, 'renders', 'aftereffects', file_name)
|
||||
|
||||
# for submit_publish_job
|
||||
return base_dir
|
||||
|
||||
def _update_for_local(self, instance, project_entity):
|
||||
"""Update old saved instances to current publishing format"""
|
||||
instance.stagingDir = tempfile.mkdtemp()
|
||||
instance.projectEntity = project_entity
|
||||
fam = "render.local"
|
||||
if fam not in instance.families:
|
||||
instance.families.append(fam)
|
||||
|
||||
return instance
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
"""
|
||||
Requires:
|
||||
None
|
||||
|
||||
Provides:
|
||||
instance -> families ("review")
|
||||
"""
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectReview(pyblish.api.ContextPlugin):
|
||||
"""Add review to families if instance created with 'mark_for_review' flag
|
||||
"""
|
||||
label = "Collect Review"
|
||||
hosts = ["aftereffects"]
|
||||
order = pyblish.api.CollectorOrder + 0.1
|
||||
settings_category = "aftereffects"
|
||||
|
||||
def process(self, context):
|
||||
for instance in context:
|
||||
creator_attributes = instance.data.get("creator_attributes") or {}
|
||||
if (
|
||||
creator_attributes.get("mark_for_review")
|
||||
and "review" not in instance.data["families"]
|
||||
):
|
||||
instance.data["families"].append("review")
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectWorkfile(pyblish.api.ContextPlugin):
|
||||
""" Adds the AE render instances """
|
||||
|
||||
label = "Collect After Effects Workfile Instance"
|
||||
order = pyblish.api.CollectorOrder + 0.1
|
||||
|
||||
default_variant = "Main"
|
||||
|
||||
def process(self, context):
|
||||
workfile_instance = None
|
||||
for instance in context:
|
||||
if instance.data["productType"] == "workfile":
|
||||
self.log.debug("Workfile instance found")
|
||||
workfile_instance = instance
|
||||
break
|
||||
|
||||
current_file = context.data["currentFile"]
|
||||
staging_dir = os.path.dirname(current_file)
|
||||
scene_file = os.path.basename(current_file)
|
||||
if workfile_instance is None:
|
||||
self.log.debug("Workfile instance not found. Skipping")
|
||||
return
|
||||
|
||||
# creating representation
|
||||
workfile_instance.data["representations"].append({
|
||||
"name": "aep",
|
||||
"ext": "aep",
|
||||
"files": scene_file,
|
||||
"stagingDir": staging_dir,
|
||||
})
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
import os
|
||||
|
||||
from ayon_core.pipeline import publish
|
||||
|
||||
from ayon_aftereffects.api import get_stub
|
||||
|
||||
class ExtractLocalRender(publish.Extractor):
|
||||
"""Render RenderQueue locally."""
|
||||
|
||||
order = publish.Extractor.order - 0.47
|
||||
label = "Extract Local Render"
|
||||
hosts = ["aftereffects"]
|
||||
families = ["renderLocal", "render.local"]
|
||||
|
||||
def process(self, instance):
|
||||
stub = get_stub()
|
||||
staging_dir = instance.data["stagingDir"]
|
||||
self.log.debug("staging_dir::{}".format(staging_dir))
|
||||
|
||||
# pull file name collected value from Render Queue Output module
|
||||
if not instance.data["file_names"]:
|
||||
raise ValueError("No file extension set in Render Queue")
|
||||
|
||||
comp_id = instance.data['comp_id']
|
||||
stub.render(staging_dir, comp_id)
|
||||
|
||||
representations = []
|
||||
for file_name in instance.data["file_names"]:
|
||||
_, ext = os.path.splitext(os.path.basename(file_name))
|
||||
ext = ext[1:]
|
||||
|
||||
first_file_path = None
|
||||
files = []
|
||||
for found_file_name in os.listdir(staging_dir):
|
||||
if not found_file_name.endswith(ext):
|
||||
continue
|
||||
|
||||
files.append(found_file_name)
|
||||
if first_file_path is None:
|
||||
first_file_path = os.path.join(staging_dir,
|
||||
found_file_name)
|
||||
|
||||
if not files:
|
||||
self.log.info("no files")
|
||||
return
|
||||
|
||||
# single file cannot be wrapped in array
|
||||
resulting_files = files
|
||||
if len(files) == 1:
|
||||
resulting_files = files[0]
|
||||
|
||||
repre_data = {
|
||||
"frameStart": instance.data["frameStart"],
|
||||
"frameEnd": instance.data["frameEnd"],
|
||||
"name": ext,
|
||||
"ext": ext,
|
||||
"files": resulting_files,
|
||||
"stagingDir": staging_dir
|
||||
}
|
||||
first_repre = not representations
|
||||
if instance.data["review"] and first_repre:
|
||||
repre_data["tags"] = ["review"]
|
||||
# TODO return back when Extract from source same as regular
|
||||
# thumbnail_path = os.path.join(staging_dir, files[0])
|
||||
# instance.data["thumbnailSource"] = thumbnail_path
|
||||
|
||||
representations.append(repre_data)
|
||||
|
||||
instance.data["representations"] = representations
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import pyblish.api
|
||||
|
||||
from ayon_core.pipeline import publish
|
||||
from ayon_aftereffects.api import get_stub
|
||||
|
||||
|
||||
class ExtractSaveScene(pyblish.api.ContextPlugin):
|
||||
"""Save scene before extraction."""
|
||||
|
||||
order = publish.Extractor.order - 0.48
|
||||
label = "Extract Save Scene"
|
||||
hosts = ["aftereffects"]
|
||||
|
||||
def process(self, context):
|
||||
stub = get_stub()
|
||||
stub.save()
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<root>
|
||||
<error id="main">
|
||||
<title>Footage item missing</title>
|
||||
<description>
|
||||
## Footage item missing
|
||||
|
||||
FootageItem `{name}` contains missing `{path}`. Render will not produce any frames and AE will stop react to any integration
|
||||
### How to repair?
|
||||
|
||||
Remove `{name}` or provide missing file.
|
||||
</description>
|
||||
</error>
|
||||
</root>
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<root>
|
||||
<error id="main">
|
||||
<title>Product context</title>
|
||||
<description>
|
||||
## Invalid product context
|
||||
|
||||
Context of the given product doesn't match your current scene.
|
||||
|
||||
### How to repair?
|
||||
|
||||
You can fix this with "repair" button on the right and refresh Publish at the bottom right.
|
||||
</description>
|
||||
<detail>
|
||||
### __Detailed Info__ (optional)
|
||||
|
||||
This might happen if you are reuse old workfile and open it in different context.
|
||||
(Eg. you created product name "renderCompositingDefault" from folder "Robot' in "your_project_Robot_compositing.aep", now you opened this workfile in a context "Sloth" but existing product for "Robot" folder stayed in the workfile.)
|
||||
</detail>
|
||||
</error>
|
||||
</root>
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<root>
|
||||
<error id="main">
|
||||
<title>Scene setting</title>
|
||||
<description>
|
||||
## Invalid scene setting found
|
||||
|
||||
One of the settings in a scene doesn't match to folder settings in database.
|
||||
|
||||
{invalid_setting_str}
|
||||
|
||||
### How to repair?
|
||||
|
||||
Change values for {invalid_keys_str} in the scene OR change them in the folder database if they are wrong there.
|
||||
|
||||
In the scene it is right mouse click on published composition > `Composition Settings`.
|
||||
</description>
|
||||
<detail>
|
||||
### __Detailed Info__ (optional)
|
||||
|
||||
This error is shown when for example resolution in the scene doesn't match to resolution set on the folder in the database.
|
||||
Either value in the database or in the scene is wrong.
|
||||
</detail>
|
||||
</error>
|
||||
<error id="file_not_found">
|
||||
<title>Scene file doesn't exist</title>
|
||||
<description>
|
||||
## Scene file doesn't exist
|
||||
|
||||
Collected scene {scene_url} doesn't exist.
|
||||
|
||||
### How to repair?
|
||||
|
||||
Re-save file, start publish from the beginning again.
|
||||
</description>
|
||||
</error>
|
||||
</root>
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import pyblish.api
|
||||
from ayon_core.lib import version_up
|
||||
from ayon_core.pipeline.publish import get_errored_plugins_from_context
|
||||
|
||||
from ayon_aftereffects.api import get_stub
|
||||
|
||||
|
||||
class IncrementWorkfile(pyblish.api.InstancePlugin):
|
||||
"""Increment the current workfile.
|
||||
|
||||
Saves the current scene with an increased version number.
|
||||
"""
|
||||
|
||||
label = "Increment Workfile"
|
||||
order = pyblish.api.IntegratorOrder + 9.0
|
||||
hosts = ["aftereffects"]
|
||||
families = ["workfile"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
errored_plugins = get_errored_plugins_from_context(instance.context)
|
||||
if errored_plugins:
|
||||
raise RuntimeError(
|
||||
"Skipping incrementing current file because publishing failed."
|
||||
)
|
||||
|
||||
scene_path = version_up(instance.context.data["currentFile"])
|
||||
get_stub().saveAs(scene_path, True)
|
||||
|
||||
self.log.info("Incremented workfile to: {}".format(scene_path))
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
from ayon_core.pipeline import publish
|
||||
from ayon_aftereffects.api import get_stub
|
||||
|
||||
|
||||
class RemovePublishHighlight(publish.Extractor):
|
||||
"""Clean utf characters which are not working in DL
|
||||
|
||||
Published compositions are marked with unicode icon which causes
|
||||
problems on specific render environments. Clean it first, sent to
|
||||
rendering, add it later back to avoid confusion.
|
||||
"""
|
||||
|
||||
order = publish.Extractor.order - 0.49 # just before save
|
||||
label = "Clean render comp"
|
||||
hosts = ["aftereffects"]
|
||||
families = ["render.farm"]
|
||||
|
||||
def process(self, instance):
|
||||
stub = get_stub()
|
||||
self.log.debug("instance::{}".format(instance.data))
|
||||
item = instance.data
|
||||
comp_name = item["comp_name"].replace(stub.PUBLISH_ICON, '')
|
||||
stub.rename_item(item["comp_id"], comp_name)
|
||||
instance.data["comp_name"] = comp_name
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Validate presence of footage items in composition
|
||||
Requires:
|
||||
"""
|
||||
import os
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from ayon_core.pipeline import (
|
||||
PublishXmlValidationError
|
||||
)
|
||||
from ayon_aftereffects.api import get_stub
|
||||
|
||||
|
||||
class ValidateFootageItems(pyblish.api.InstancePlugin):
|
||||
"""
|
||||
Validates if FootageItems contained in composition exist.
|
||||
|
||||
AE fails silently and doesn't render anything if footage item file is
|
||||
missing. This will result in nonresponsiveness of AE UI as it expects
|
||||
reaction from user, but it will not provide dialog.
|
||||
This validator tries to check existence of the files.
|
||||
It will not protect from missing frame in multiframes though
|
||||
(as AE api doesn't provide this information and it cannot be told how many
|
||||
frames should be there easily). Missing frame is replaced by placeholder.
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
label = "Validate Footage Items"
|
||||
families = ["render.farm", "render.local", "render"]
|
||||
hosts = ["aftereffects"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
"""Plugin entry point."""
|
||||
|
||||
comp_id = instance.data["comp_id"]
|
||||
for footage_item in get_stub().get_items(comps=False, folders=False,
|
||||
footages=True):
|
||||
self.log.info(footage_item)
|
||||
if comp_id not in footage_item.containing_comps:
|
||||
continue
|
||||
|
||||
path = footage_item.path
|
||||
if path and not os.path.exists(path):
|
||||
msg = f"File {path} not found."
|
||||
formatting = {"name": footage_item.name, "path": path}
|
||||
raise PublishXmlValidationError(self, msg,
|
||||
formatting_data=formatting)
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import pyblish.api
|
||||
|
||||
from ayon_core.pipeline import get_current_folder_path
|
||||
from ayon_core.pipeline.publish import (
|
||||
ValidateContentsOrder,
|
||||
PublishXmlValidationError,
|
||||
)
|
||||
from ayon_aftereffects.api import get_stub
|
||||
|
||||
|
||||
class ValidateInstanceFolderRepair(pyblish.api.Action):
|
||||
"""Repair the instance folder with value from Context."""
|
||||
|
||||
label = "Repair"
|
||||
icon = "wrench"
|
||||
on = "failed"
|
||||
|
||||
def process(self, context, plugin):
|
||||
|
||||
# Get the errored instances
|
||||
failed = []
|
||||
for result in context.data["results"]:
|
||||
if (result["error"] is not None and result["instance"] is not None
|
||||
and result["instance"] not in failed):
|
||||
failed.append(result["instance"])
|
||||
|
||||
# Apply pyblish.logic to get the instances for the plug-in
|
||||
instances = pyblish.api.instances_by_plugin(failed, plugin)
|
||||
stub = get_stub()
|
||||
for instance in instances:
|
||||
data = stub.read(instance[0])
|
||||
|
||||
data["folderPath"] = get_current_folder_path()
|
||||
stub.imprint(instance[0].instance_id, data)
|
||||
|
||||
|
||||
class ValidateInstanceFolder(pyblish.api.InstancePlugin):
|
||||
"""Validate the instance folder is the current selected context folder.
|
||||
|
||||
As it might happen that multiple worfiles are opened at same time,
|
||||
switching between them would mess with selected context. (From Launcher
|
||||
or Ftrack).
|
||||
|
||||
In that case outputs might be output under wrong folder!
|
||||
|
||||
Repair action will use Context folder value (from Workfiles or Launcher)
|
||||
Closing and reopening with Workfiles will refresh Context value.
|
||||
"""
|
||||
|
||||
label = "Validate Instance Folder"
|
||||
hosts = ["aftereffects"]
|
||||
actions = [ValidateInstanceFolderRepair]
|
||||
order = ValidateContentsOrder
|
||||
|
||||
def process(self, instance):
|
||||
instance_folder = instance.data["folderPath"]
|
||||
current_folder = get_current_folder_path()
|
||||
msg = (
|
||||
f"Instance folder {instance_folder} is not the same "
|
||||
f"as current context {current_folder}."
|
||||
)
|
||||
|
||||
if instance_folder != current_folder:
|
||||
raise PublishXmlValidationError(self, msg)
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Validate scene settings.
|
||||
Requires:
|
||||
instance -> folderEntity
|
||||
instance -> anatomyData
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from ayon_core.pipeline import (
|
||||
PublishXmlValidationError,
|
||||
OptionalPyblishPluginMixin
|
||||
)
|
||||
from ayon_aftereffects.api import get_folder_settings
|
||||
|
||||
|
||||
class ValidateSceneSettings(OptionalPyblishPluginMixin,
|
||||
pyblish.api.InstancePlugin):
|
||||
"""
|
||||
Ensures that Composition Settings (right mouse on comp) are same as
|
||||
in FTrack on task.
|
||||
|
||||
By default checks only duration - how many frames should be rendered.
|
||||
Compares:
|
||||
Frame start - Frame end + 1 from FTrack
|
||||
against
|
||||
Duration in Composition Settings.
|
||||
|
||||
If this complains:
|
||||
Check error message where is discrepancy.
|
||||
Check FTrack task 'pype' section of task attributes for expected
|
||||
values.
|
||||
Check/modify rendered Composition Settings.
|
||||
|
||||
If you know what you are doing run publishing again, uncheck this
|
||||
validation before Validation phase.
|
||||
"""
|
||||
|
||||
"""
|
||||
Dev docu:
|
||||
Could be configured by 'presets/plugins/aftereffects/publish'
|
||||
|
||||
skip_timelines_check - fill task name for which skip validation of
|
||||
frameStart
|
||||
frameEnd
|
||||
fps
|
||||
handleStart
|
||||
handleEnd
|
||||
skip_resolution_check - fill entity type ('folder') to skip validation
|
||||
resolutionWidth
|
||||
resolutionHeight
|
||||
TODO support in extension is missing for now
|
||||
|
||||
By defaults validates duration (how many frames should be published)
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
label = "Validate Scene Settings"
|
||||
families = ["render.farm", "render.local", "render"]
|
||||
hosts = ["aftereffects"]
|
||||
settings_category = "aftereffects"
|
||||
optional = True
|
||||
|
||||
skip_timelines_check = [".*"] # * >> skip for all
|
||||
skip_resolution_check = [".*"]
|
||||
|
||||
def process(self, instance):
|
||||
"""Plugin entry point."""
|
||||
# Skip the instance if is not active by data on the instance
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
folder_entity = instance.data["folderEntity"]
|
||||
expected_settings = get_folder_settings(folder_entity)
|
||||
self.log.info("config from DB::{}".format(expected_settings))
|
||||
|
||||
task_name = instance.data["task"]
|
||||
if any(re.search(pattern, task_name)
|
||||
for pattern in self.skip_resolution_check):
|
||||
expected_settings.pop("resolutionWidth")
|
||||
expected_settings.pop("resolutionHeight")
|
||||
|
||||
if any(re.search(pattern, task_name)
|
||||
for pattern in self.skip_timelines_check):
|
||||
expected_settings.pop('fps', None)
|
||||
expected_settings.pop('frameStart', None)
|
||||
expected_settings.pop('frameEnd', None)
|
||||
expected_settings.pop('handleStart', None)
|
||||
expected_settings.pop('handleEnd', None)
|
||||
|
||||
# handle case where ftrack uses only two decimal places
|
||||
# 23.976023976023978 vs. 23.98
|
||||
fps = instance.data.get("fps")
|
||||
if fps:
|
||||
if isinstance(fps, float):
|
||||
fps = float(
|
||||
"{:.2f}".format(fps))
|
||||
expected_settings["fps"] = fps
|
||||
|
||||
duration = instance.data.get("frameEndHandle") - \
|
||||
instance.data.get("frameStartHandle") + 1
|
||||
|
||||
self.log.debug("validated items::{}".format(expected_settings))
|
||||
|
||||
current_settings = {
|
||||
"fps": fps,
|
||||
"frameStart": instance.data.get("frameStart"),
|
||||
"frameEnd": instance.data.get("frameEnd"),
|
||||
"handleStart": instance.data.get("handleStart"),
|
||||
"handleEnd": instance.data.get("handleEnd"),
|
||||
"frameStartHandle": instance.data.get("frameStartHandle"),
|
||||
"frameEndHandle": instance.data.get("frameEndHandle"),
|
||||
"resolutionWidth": instance.data.get("resolutionWidth"),
|
||||
"resolutionHeight": instance.data.get("resolutionHeight"),
|
||||
"duration": duration
|
||||
}
|
||||
self.log.info("current_settings:: {}".format(current_settings))
|
||||
|
||||
invalid_settings = []
|
||||
invalid_keys = set()
|
||||
for key, value in expected_settings.items():
|
||||
if value != current_settings[key]:
|
||||
msg = "'{}' expected: '{}' found: '{}'".format(
|
||||
key, value, current_settings[key])
|
||||
|
||||
if key == "duration" and expected_settings.get("handleStart"):
|
||||
msg += "Handles included in calculation. Remove " \
|
||||
"handles in DB or extend frame range in " \
|
||||
"Composition Setting."
|
||||
|
||||
invalid_settings.append(msg)
|
||||
invalid_keys.add(key)
|
||||
|
||||
if invalid_settings:
|
||||
msg = "Found invalid settings:\n{}".format(
|
||||
"\n".join(invalid_settings)
|
||||
)
|
||||
|
||||
invalid_keys_str = ",".join(invalid_keys)
|
||||
break_str = "<br/>"
|
||||
invalid_setting_str = "<b>Found invalid settings:</b><br/>{}".\
|
||||
format(break_str.join(invalid_settings))
|
||||
|
||||
formatting_data = {
|
||||
"invalid_setting_str": invalid_setting_str,
|
||||
"invalid_keys_str": invalid_keys_str
|
||||
}
|
||||
raise PublishXmlValidationError(self, msg,
|
||||
formatting_data=formatting_data)
|
||||
|
||||
if not os.path.exists(instance.data.get("source")):
|
||||
scene_url = instance.data.get("source")
|
||||
msg = "Scene file {} not found (saved under wrong name)".format(
|
||||
scene_url
|
||||
)
|
||||
formatting_data = {
|
||||
"scene_url": scene_url
|
||||
}
|
||||
raise PublishXmlValidationError(self, msg, key="file_not_found",
|
||||
formatting_data=formatting_data)
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
from ayon_core.pipeline.workfile.workfile_template_builder import (
|
||||
CreatePlaceholderItem,
|
||||
PlaceholderCreateMixin
|
||||
)
|
||||
from ayon_aftereffects.api import (
|
||||
get_stub,
|
||||
workfile_template_builder as wtb,
|
||||
)
|
||||
from ayon_aftereffects.api.lib import set_settings
|
||||
|
||||
|
||||
class AEPlaceholderCreatePlugin(wtb.AEPlaceholderPlugin,
|
||||
PlaceholderCreateMixin):
|
||||
"""Adds Create placeholder.
|
||||
|
||||
This adds composition and runs Create
|
||||
"""
|
||||
identifier = "aftereffects.create"
|
||||
label = "AfterEffects create"
|
||||
|
||||
def _create_placeholder_item(self, item_data) -> CreatePlaceholderItem:
|
||||
return CreatePlaceholderItem(
|
||||
scene_identifier=item_data["uuid"],
|
||||
data=item_data["data"],
|
||||
plugin=self
|
||||
)
|
||||
|
||||
def create_placeholder(self, placeholder_data):
|
||||
stub = get_stub()
|
||||
name = "CREATEPLACEHOLDER"
|
||||
item_id = stub.add_item(name, "COMP")
|
||||
|
||||
self._imprint_item(item_id, name, placeholder_data, stub)
|
||||
|
||||
def populate_placeholder(self, placeholder):
|
||||
"""Replace 'placeholder' with publishable instance.
|
||||
|
||||
Renames prepared composition name, creates publishable instance, sets
|
||||
frame/duration settings according to DB.
|
||||
"""
|
||||
pre_create_data = {"use_selection": True}
|
||||
item_id, item = self._get_item(placeholder)
|
||||
get_stub().select_items([item_id])
|
||||
self.populate_create_placeholder(placeholder, pre_create_data)
|
||||
|
||||
# apply settings for populated composition
|
||||
item_id, metadata_item = self._get_item(placeholder)
|
||||
set_settings(True, True, [item_id])
|
||||
|
||||
def get_placeholder_options(self, options=None):
|
||||
return self.get_create_plugin_options(options)
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
from ayon_core.pipeline.workfile.workfile_template_builder import (
|
||||
LoadPlaceholderItem,
|
||||
PlaceholderLoadMixin
|
||||
)
|
||||
from ayon_aftereffects.api import (
|
||||
get_stub,
|
||||
workfile_template_builder as wtb,
|
||||
)
|
||||
|
||||
|
||||
class AEPlaceholderLoadPlugin(wtb.AEPlaceholderPlugin, PlaceholderLoadMixin):
|
||||
identifier = "aftereffects.load"
|
||||
label = "AfterEffects load"
|
||||
|
||||
def _create_placeholder_item(self, item_data) -> LoadPlaceholderItem:
|
||||
return LoadPlaceholderItem(
|
||||
scene_identifier=item_data["uuid"],
|
||||
data=item_data["data"],
|
||||
plugin=self
|
||||
)
|
||||
|
||||
def create_placeholder(self, placeholder_data):
|
||||
"""Creates AE's Placeholder item in Project items list.
|
||||
|
||||
Sets dummy resolution/duration/fps settings, will be replaced when
|
||||
populated.
|
||||
"""
|
||||
stub = get_stub()
|
||||
name = "LOADERPLACEHOLDER"
|
||||
item_id = stub.add_placeholder(name, 1920, 1060, 25, 10)
|
||||
|
||||
self._imprint_item(item_id, name, placeholder_data, stub)
|
||||
|
||||
def populate_placeholder(self, placeholder):
|
||||
"""Use Openpype Loader from `placeholder` to create new FootageItems
|
||||
|
||||
New FootageItems are created, files are imported.
|
||||
"""
|
||||
self.populate_load_placeholder(placeholder)
|
||||
errors = placeholder.get_errors()
|
||||
stub = get_stub()
|
||||
if errors:
|
||||
stub.print_msg("\n".join(errors))
|
||||
else:
|
||||
if not placeholder.data["keep_placeholder"]:
|
||||
metadata = stub.get_metadata()
|
||||
for item in metadata:
|
||||
if not item.get("is_placeholder"):
|
||||
continue
|
||||
scene_identifier = item.get("uuid")
|
||||
if (scene_identifier and
|
||||
scene_identifier == placeholder.scene_identifier):
|
||||
stub.delete_item(item["members"][0])
|
||||
stub.remove_instance(placeholder.scene_identifier, metadata)
|
||||
|
||||
def get_placeholder_options(self, options=None):
|
||||
return self.get_load_plugin_options(options)
|
||||
|
||||
def load_succeed(self, placeholder, container):
|
||||
placeholder_item_id, _ = self._get_item(placeholder)
|
||||
item_id = container.id
|
||||
get_stub().add_item_instead_placeholder(placeholder_item_id, item_id)
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring AYON addon 'aftereffects' version."""
|
||||
__version__ = "0.2.2"
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
[project]
|
||||
name="aftereffects"
|
||||
description="AYON AfterEffects addon."
|
||||
|
||||
[ayon.runtimeDependencies]
|
||||
wsrpc_aiohttp = "^3.1.1" # websocket server
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
name = "aftereffects"
|
||||
title = "AfterEffects"
|
||||
version = "0.2.2"
|
||||
|
||||
client_dir = "ayon_aftereffects"
|
||||
|
||||
ayon_required_addons = {
|
||||
"core": ">0.3.2",
|
||||
}
|
||||
ayon_compatible_addons = {}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .settings import AfterEffectsSettings, DEFAULT_AFTEREFFECTS_SETTING
|
||||
|
||||
|
||||
class AfterEffects(BaseServerAddon):
|
||||
settings_model = AfterEffectsSettings
|
||||
|
||||
async def get_default_settings(self):
|
||||
settings_model_cls = self.get_settings_model()
|
||||
return settings_model_cls(**DEFAULT_AFTEREFFECTS_SETTING)
|
||||