Merge branch 'develop' of https://github.com/ynput/ayon-core into enhancement/create_context_typing

# Conflicts:
#	server_addon/jobqueue/client/ayon_jobqueue/addon.py
This commit is contained in:
Roy Nieterau 2024-06-27 18:13:25 +02:00
commit ad208251a3
1060 changed files with 3640 additions and 181796 deletions

View file

@ -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):

View file

@ -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",

View 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

View file

@ -37,6 +37,9 @@ __all__ = (
"CONTEXT_ID",
"CONTEXT_LABEL",
"CONTEXT_GROUP",
"CONVERTOR_ITEM_GROUP",
"VARIANT_TOOLTIP",
"INPUTS_LAYOUT_HSPACING",

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -0,0 +1,10 @@
from .create import CreateModel, CreatorItem
from .publish import PublishModel
__all__ = (
"CreateModel",
"CreatorItem",
"PublishModel",
)

View 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))

File diff suppressed because it is too large Load diff

View 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

View file

@ -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()

View file

@ -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)

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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."""

View file

@ -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)

View file

@ -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

View file

@ -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."""

View file

@ -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)

View file

@ -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":

View file

@ -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()

View file

@ -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()

View file

@ -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)

View file

@ -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 = {}

View file

@ -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()

View file

@ -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()

View file

@ -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)

View file

@ -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)

View file

@ -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": [],

View file

@ -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.

View file

@ -1,4 +0,0 @@
AfterEffects Addon
===============
Integration with Adobe AfterEffects.

View file

@ -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",
)

View file

@ -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"
)

View file

@ -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 [Anastasiys 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:
![Ayon Panel](panel.png "Ayon Panel")
## 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

View file

@ -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"
]

View file

@ -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>

View file

@ -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>

View file

@ -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, "メイリオ", " 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;
}

View file

@ -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;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View file

@ -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">&nbsp;</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">&nbsp;</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">&nbsp;</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>

File diff suppressed because one or more lines are too long

View file

@ -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 "&nbsp;"),
// 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");
};
}
}());

View file

@ -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});

View file

@ -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

File diff suppressed because one or more lines are too long

View file

@ -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);
});
}

View file

@ -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
};
}());

View file

@ -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})
}

View file

@ -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"

View file

@ -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)

View file

@ -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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View file

@ -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]

View file

@ -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()

View file

@ -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()

View file

@ -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_()

View file

@ -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

View file

@ -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
)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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"])

View file

@ -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")

View file

@ -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("\\", "/")

View file

@ -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("\\", "/")

View file

@ -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 Anastasiys 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)

View file

@ -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

View file

@ -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")

View file

@ -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,
})

View file

@ -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

View file

@ -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()

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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))

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
"""Package declaring AYON addon 'aftereffects' version."""
__version__ = "0.2.2"

View file

@ -1,6 +0,0 @@
[project]
name="aftereffects"
description="AYON AfterEffects addon."
[ayon.runtimeDependencies]
wsrpc_aiohttp = "^3.1.1" # websocket server

View file

@ -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 = {}

View file

@ -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)

Some files were not shown because too many files have changed in this diff Show more