diff --git a/client/ayon_core/plugins/publish/integrate_hero_version.py b/client/ayon_core/plugins/publish/integrate_hero_version.py index 8c36719b77..4fb8b886a9 100644 --- a/client/ayon_core/plugins/publish/integrate_hero_version.py +++ b/client/ayon_core/plugins/publish/integrate_hero_version.py @@ -87,7 +87,9 @@ class IntegrateHeroVersion( ] # QUESTION/TODO this process should happen on server if crashed due to # permissions error on files (files were used or user didn't have perms) - # *but all other plugins must be sucessfully completed + # *but all other plugins must be successfully completed + + use_hardlinks = False def process(self, instance): if not self.is_active(instance.data): @@ -617,24 +619,32 @@ class IntegrateHeroVersion( self.log.debug("Folder already exists: \"{}\"".format(dirname)) + if self.use_hardlinks: + # First try hardlink and copy if paths are cross drive + self.log.debug("Hardlinking file \"{}\" to \"{}\"".format( + src_path, dst_path + )) + try: + create_hard_link(src_path, dst_path) + # Return when successful + return + + except OSError as exc: + # re-raise exception if different than + # EXDEV - cross drive path + # EINVAL - wrong format, must be NTFS + self.log.debug( + "Hardlink failed with errno:'{}'".format(exc.errno)) + if exc.errno not in [errno.EXDEV, errno.EINVAL]: + raise + + self.log.debug( + "Hardlinking failed, falling back to regular copy...") + self.log.debug("Copying file \"{}\" to \"{}\"".format( src_path, dst_path )) - # First try hardlink and copy if paths are cross drive - try: - create_hard_link(src_path, dst_path) - # Return when successful - return - - except OSError as exc: - # re-raise exception if different than - # EXDEV - cross drive path - # EINVAL - wrong format, must be NTFS - self.log.debug("Hardlink failed with errno:'{}'".format(exc.errno)) - if exc.errno not in [errno.EXDEV, errno.EINVAL]: - raise - shutil.copy(src_path, dst_path) def version_from_representations(self, project_name, repres): diff --git a/client/ayon_core/tools/common_models/__init__.py b/client/ayon_core/tools/common_models/__init__.py index f09edfeab2..ece189fdc6 100644 --- a/client/ayon_core/tools/common_models/__init__.py +++ b/client/ayon_core/tools/common_models/__init__.py @@ -2,9 +2,13 @@ from .cache import CacheItem, NestedCacheItem from .projects import ( + StatusItem, + StatusStates, ProjectItem, ProjectsModel, PROJECTS_MODEL_SENDER, + FolderTypeItem, + TaskTypeItem, ) from .hierarchy import ( FolderItem, @@ -21,9 +25,13 @@ __all__ = ( "CacheItem", "NestedCacheItem", + "StatusItem", + "StatusStates", "ProjectItem", "ProjectsModel", "PROJECTS_MODEL_SENDER", + "FolderTypeItem", + "TaskTypeItem", "FolderItem", "TaskItem", diff --git a/client/ayon_core/tools/common_models/projects.py b/client/ayon_core/tools/common_models/projects.py index 4e8925388d..7ec941e6bd 100644 --- a/client/ayon_core/tools/common_models/projects.py +++ b/client/ayon_core/tools/common_models/projects.py @@ -1,8 +1,8 @@ import contextlib -from abc import ABCMeta, abstractmethod +from abc import ABC, abstractmethod +from typing import Dict, Any import ayon_api -import six from ayon_core.style import get_default_entity_icon_color from ayon_core.lib import CacheItem, NestedCacheItem @@ -10,8 +10,14 @@ from ayon_core.lib import CacheItem, NestedCacheItem PROJECTS_MODEL_SENDER = "projects.model" -@six.add_metaclass(ABCMeta) -class AbstractHierarchyController: +class StatusStates: + not_started = "not_started" + in_progress = "in_progress" + done = "done" + blocked = "blocked" + + +class AbstractHierarchyController(ABC): @abstractmethod def emit_event(self, topic, data, source): pass @@ -25,18 +31,24 @@ class StatusItem: color (str): Status color in hex ("#434a56"). short (str): Short status name ("NRD"). icon (str): Icon name in MaterialIcons ("fiber_new"). - state (Literal["not_started", "in_progress", "done", "blocked"]): - Status state. + state (str): Status state. """ - def __init__(self, name, color, short, icon, state): - self.name = name - self.color = color - self.short = short - self.icon = icon - self.state = state + def __init__( + self, + name: str, + color: str, + short: str, + icon: str, + state: str + ): + self.name: str = name + self.color: str = color + self.short: str = short + self.icon: str = icon + self.state: str = state - def to_data(self): + def to_data(self) -> Dict[str, Any]: return { "name": self.name, "color": self.color, diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py new file mode 100644 index 0000000000..a9142396f5 --- /dev/null +++ b/client/ayon_core/tools/publisher/abstract.py @@ -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 diff --git a/client/ayon_core/tools/publisher/constants.py b/client/ayon_core/tools/publisher/constants.py index 6676f14c3d..285724727d 100644 --- a/client/ayon_core/tools/publisher/constants.py +++ b/client/ayon_core/tools/publisher/constants.py @@ -37,6 +37,9 @@ __all__ = ( "CONTEXT_ID", "CONTEXT_LABEL", + "CONTEXT_GROUP", + "CONVERTOR_ITEM_GROUP", + "VARIANT_TOOLTIP", "INPUTS_LAYOUT_HSPACING", diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index 4a8ec72e4f..f26f8fc524 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -1,1339 +1,83 @@ import os -import copy import logging -import traceback -import collections -import uuid import tempfile import shutil -import inspect -from abc import ABCMeta, abstractmethod -import re -import six -import arrow -import pyblish.api import ayon_api from ayon_core.lib.events import QueuedEventSystem -from ayon_core.lib.attribute_definitions import ( - UIDef, - serialize_attr_defs, - deserialize_attr_defs, -) + from ayon_core.pipeline import ( - PublishValidationError, - KnownPublishError, registered_host, get_process_id, - OptionalPyblishPluginMixin, ) -from ayon_core.pipeline.create import ( - CreateContext, - AutoCreator, - HiddenCreator, - Creator, -) -from ayon_core.pipeline.create.context import ( - CreatorsOperationFailed, - ConvertorsOperationFailed, -) -from ayon_core.pipeline.publish import get_publish_instance_label from ayon_core.tools.common_models import ProjectsModel, HierarchyModel -from ayon_core.lib.profiles_filtering import filter_profiles -# Define constant for plugin orders offset -PLUGIN_ORDER_OFFSET = 0.5 +from .models import ( + PublishModel, + CreateModel, +) +from .abstract import ( + AbstractPublisherBackend, + AbstractPublisherFrontend, + CardMessageTypes +) -class CardMessageTypes: - standard = None - info = "info" - error = "error" +class PublisherController( + AbstractPublisherBackend, + AbstractPublisherFrontend, +): + """Middleware between UI, CreateContext and publish Context. + Handle both creation and publishing parts. -class MainThreadItem: - """Callback with args and kwargs.""" - - def __init__(self, callback, *args, **kwargs): - self.callback = callback - self.args = args - self.kwargs = kwargs - - def process(self): - self.callback(*self.args, **self.kwargs) - - -class PublishReportMaker: - """Report for single publishing process. - - Report keeps current state of publishing and currently processed plugin. - """ - - def __init__(self, controller): - self.controller = controller - self._create_discover_result = None - self._convert_discover_result = None - self._publish_discover_result = None - - self._plugin_data_by_id = {} - self._current_plugin = None - self._current_plugin_data = {} - self._all_instances_by_id = {} - self._current_context = None - - def reset(self, context, create_context): - """Reset report and clear all data.""" - - self._create_discover_result = create_context.creator_discover_result - self._convert_discover_result = ( - create_context.convertor_discover_result - ) - self._publish_discover_result = create_context.publish_discover_result - - self._plugin_data_by_id = {} - self._current_plugin = None - self._current_plugin_data = {} - self._all_instances_by_id = {} - self._current_context = context - - for plugin in create_context.publish_plugins_mismatch_targets: - plugin_data = self._add_plugin_data_item(plugin) - plugin_data["skipped"] = True - - def add_plugin_iter(self, plugin, context): - """Add report about single iteration of plugin.""" - for instance in context: - self._all_instances_by_id[instance.id] = instance - - if self._current_plugin_data: - self._current_plugin_data["passed"] = True - - self._current_plugin = plugin - self._current_plugin_data = self._add_plugin_data_item(plugin) - - def _add_plugin_data_item(self, plugin): - if plugin.id in self._plugin_data_by_id: - # A plugin would be processed more than once. What can cause it: - # - there is a bug in controller - # - plugin class is imported into multiple files - # - this can happen even with base classes from 'pyblish' - raise ValueError( - "Plugin '{}' is already stored".format(str(plugin))) - - plugin_data_item = self._create_plugin_data_item(plugin) - self._plugin_data_by_id[plugin.id] = plugin_data_item - - return plugin_data_item - - def _create_plugin_data_item(self, plugin): - label = None - if hasattr(plugin, "label"): - label = plugin.label - - return { - "id": plugin.id, - "name": plugin.__name__, - "label": label, - "order": plugin.order, - "targets": list(plugin.targets), - "instances_data": [], - "actions_data": [], - "skipped": False, - "passed": False - } - - def set_plugin_skipped(self): - """Set that current plugin has been skipped.""" - self._current_plugin_data["skipped"] = True - - def add_result(self, result): - """Handle result of one plugin and it's instance.""" - - instance = result["instance"] - instance_id = None - if instance is not None: - instance_id = instance.id - self._current_plugin_data["instances_data"].append({ - "id": instance_id, - "logs": self._extract_instance_log_items(result), - "process_time": result["duration"] - }) - - def add_action_result(self, action, result): - """Add result of single action.""" - plugin = result["plugin"] - - store_item = self._plugin_data_by_id.get(plugin.id) - if store_item is None: - store_item = self._add_plugin_data_item(plugin) - - action_name = action.__name__ - action_label = action.label or action_name - log_items = self._extract_log_items(result) - store_item["actions_data"].append({ - "success": result["success"], - "name": action_name, - "label": action_label, - "logs": log_items - }) - - def get_report(self, publish_plugins=None): - """Report data with all details of current state.""" - - now = arrow.utcnow().to("local") - instances_details = {} - for instance in self._all_instances_by_id.values(): - instances_details[instance.id] = self._extract_instance_data( - instance, instance in self._current_context - ) - - plugins_data_by_id = copy.deepcopy( - self._plugin_data_by_id - ) - - # Ensure the current plug-in is marked as `passed` in the result - # so that it shows on reports for paused publishes - if self._current_plugin is not None: - current_plugin_data = plugins_data_by_id.get( - self._current_plugin.id - ) - if current_plugin_data and not current_plugin_data["passed"]: - current_plugin_data["passed"] = True - - if publish_plugins: - for plugin in publish_plugins: - if plugin.id not in plugins_data_by_id: - plugins_data_by_id[plugin.id] = \ - self._create_plugin_data_item(plugin) - - reports = [] - if self._create_discover_result is not None: - reports.append(self._create_discover_result) - - if self._convert_discover_result is not None: - reports.append(self._convert_discover_result) - - if self._publish_discover_result is not None: - reports.append(self._publish_discover_result) - - crashed_file_paths = {} - for report in reports: - items = report.crashed_file_paths.items() - for filepath, exc_info in items: - crashed_file_paths[filepath] = "".join( - traceback.format_exception(*exc_info) - ) - - return { - "plugins_data": list(plugins_data_by_id.values()), - "instances": instances_details, - "context": self._extract_context_data(self._current_context), - "crashed_file_paths": crashed_file_paths, - "id": uuid.uuid4().hex, - "created_at": now.isoformat(), - "report_version": "1.0.1", - } - - def _extract_context_data(self, context): - context_label = "Context" - if context is not None: - context_label = context.data.get("label") - return { - "label": context_label - } - - def _extract_instance_data(self, instance, exists): - return { - "name": instance.data.get("name"), - "label": get_publish_instance_label(instance), - "product_type": instance.data.get("productType"), - "family": instance.data.get("family"), - "families": instance.data.get("families") or [], - "exists": exists, - "creator_identifier": instance.data.get("creator_identifier"), - "instance_id": instance.data.get("instance_id"), - } - - def _extract_instance_log_items(self, result): - instance = result["instance"] - instance_id = None - if instance: - instance_id = instance.id - - log_items = self._extract_log_items(result) - for item in log_items: - item["instance_id"] = instance_id - return log_items - - def _extract_log_items(self, result): - output = [] - records = result.get("records") or [] - for record in records: - record_exc_info = record.exc_info - if record_exc_info is not None: - record_exc_info = "".join( - traceback.format_exception(*record_exc_info) - ) - - try: - msg = record.getMessage() - except Exception: - msg = str(record.msg) - - output.append({ - "type": "record", - "msg": msg, - "name": record.name, - "lineno": record.lineno, - "levelno": record.levelno, - "levelname": record.levelname, - "threadName": record.threadName, - "filename": record.filename, - "pathname": record.pathname, - "msecs": record.msecs, - "exc_info": record_exc_info - }) - - exception = result.get("error") - if exception: - fname, line_no, func, exc = exception.traceback - - # Conversion of exception into string may crash - try: - msg = str(exception) - except BaseException: - msg = ( - "Publisher Controller: ERROR" - " - Failed to get exception message" - ) - - # Action result does not have 'is_validation_error' - is_validation_error = result.get("is_validation_error", False) - output.append({ - "type": "error", - "is_validation_error": is_validation_error, - "msg": msg, - "filename": str(fname), - "lineno": str(line_no), - "func": str(func), - "traceback": exception.formatted_traceback - }) - - return output - - -class PublishPluginsProxy: - """Wrapper around publish plugin. - - Prepare mapping for publish plugins and actions. Also can create - serializable data for plugin actions so UI don't have to have access to - them. - - This object is created in process where publishing is actually running. - - Notes: - Actions have id but single action can be used on multiple plugins so - to run an action is needed combination of plugin and action. + Known topics: + "show.detailed.help" - Detailed help requested (UI related). + "show.card.message" - Show card message request (UI related). + "instances.refresh.finished" - Instances are refreshed. + "plugins.refresh.finished" - Plugins refreshed. + "publish.reset.finished" - Reset finished. + "controller.reset.started" - Controller reset started. + "controller.reset.finished" - Controller reset finished. + "publish.process.started" - Publishing started. Can be started from + paused state. + "publish.process.stopped" - Publishing stopped/paused process. + "publish.process.plugin.changed" - Plugin state has changed. + "publish.process.instance.changed" - Instance state has changed. + "publish.has_validated.changed" - Attr 'publish_has_validated' + changed. + "publish.is_running.changed" - Attr 'publish_is_running' changed. + "publish.has_crashed.changed" - Attr 'publish_has_crashed' changed. + "publish.publish_error.changed" - Attr 'publish_error' + "publish.has_validation_errors.changed" - Attr + 'has_validation_errors' changed. + "publish.max_progress.changed" - Attr 'publish_max_progress' + changed. + "publish.progress.changed" - Attr 'publish_progress' changed. + "publish.finished.changed" - Attr 'publish_has_finished' changed. Args: - plugins [List[pyblish.api.Plugin]]: Discovered plugins that will be - processed. + headless (bool): Headless publishing. ATM not implemented or used. + """ + _log = None - def __init__(self, plugins): - plugins_by_id = {} - actions_by_plugin_id = {} - action_ids_by_plugin_id = {} - for plugin in plugins: - plugin_id = plugin.id - plugins_by_id[plugin_id] = plugin + def __init__(self, headless=False): + super().__init__() - action_ids = [] - actions_by_id = {} - action_ids_by_plugin_id[plugin_id] = action_ids - actions_by_plugin_id[plugin_id] = actions_by_id - - actions = getattr(plugin, "actions", None) or [] - for action in actions: - action_id = action.id - action_ids.append(action_id) - actions_by_id[action_id] = action - - self._plugins_by_id = plugins_by_id - self._actions_by_plugin_id = actions_by_plugin_id - self._action_ids_by_plugin_id = action_ids_by_plugin_id - - def get_action(self, plugin_id, action_id): - return self._actions_by_plugin_id[plugin_id][action_id] - - def get_plugin(self, plugin_id): - return self._plugins_by_id[plugin_id] - - def get_plugin_id(self, plugin): - """Get id of plugin based on plugin object. - - It's used for validation errors report. - - Args: - plugin (pyblish.api.Plugin): Publish plugin for which id should be - returned. - - Returns: - str: Plugin id. - """ - - return plugin.id - - def get_plugin_action_items(self, plugin_id): - """Get plugin action items for plugin by its id. - - Args: - plugin_id (str): Publish plugin id. - - Returns: - List[PublishPluginActionItem]: Items with information about publish - plugin actions. - """ - - return [ - self._create_action_item( - self.get_action(plugin_id, action_id), plugin_id - ) - for action_id in self._action_ids_by_plugin_id[plugin_id] - ] - - def _create_action_item(self, action, plugin_id): - label = action.label or action.__name__ - icon = getattr(action, "icon", None) - return PublishPluginActionItem( - action.id, - plugin_id, - action.active, - action.on, - label, - icon - ) - - -class PublishPluginActionItem: - """Representation of publish plugin action. - - Data driven object which is used as proxy for controller and UI. - - Args: - action_id (str): Action id. - plugin_id (str): Plugin id. - active (bool): Action is active. - on_filter (str): Actions have 'on' attribte which define when can be - action triggered (e.g. 'all', 'failed', ...). - label (str): Action's label. - icon (Union[str, None]) Action's icon. - """ - - def __init__(self, action_id, plugin_id, active, on_filter, label, icon): - self.action_id = action_id - self.plugin_id = plugin_id - self.active = active - self.on_filter = on_filter - self.label = label - self.icon = icon - - def to_data(self): - """Serialize object to dictionary. - - Returns: - Dict[str, Union[str,bool,None]]: Serialized object. - """ - - return { - "action_id": self.action_id, - "plugin_id": self.plugin_id, - "active": self.active, - "on_filter": self.on_filter, - "label": self.label, - "icon": self.icon - } - - @classmethod - def from_data(cls, data): - """Create object from data. - - Args: - data (Dict[str, Union[str,bool,None]]): Data used to recreate - object. - - Returns: - PublishPluginActionItem: Object created using data. - """ - - return cls(**data) - - -class ValidationErrorItem: - """Data driven validation error item. - - Prepared data container with information about validation error and it's - source plugin. - - Can be converted to raw data and recreated should be used for controller - and UI connection. - - Args: - instance_id (str): Id of pyblish instance to which is validation error - connected. - instance_label (str): Prepared instance label. - plugin_id (str): Id of pyblish Plugin which triggered the validation - error. Id is generated using 'PublishPluginsProxy'. - """ - - def __init__( - self, - instance_id, - instance_label, - plugin_id, - context_validation, - title, - description, - detail - ): - self.instance_id = instance_id - self.instance_label = instance_label - self.plugin_id = plugin_id - self.context_validation = context_validation - self.title = title - self.description = description - self.detail = detail - - def to_data(self): - """Serialize object to dictionary. - - Returns: - Dict[str, Union[str, bool, None]]: Serialized object data. - """ - - return { - "instance_id": self.instance_id, - "instance_label": self.instance_label, - "plugin_id": self.plugin_id, - "context_validation": self.context_validation, - "title": self.title, - "description": self.description, - "detail": self.detail, - } - - @classmethod - def from_result(cls, plugin_id, error, instance): - """Create new object based on resukt from controller. - - Returns: - ValidationErrorItem: New object with filled data. - """ - - instance_label = None - instance_id = None - if instance is not None: - instance_label = ( - instance.data.get("label") or instance.data.get("name") - ) - instance_id = instance.id - - return cls( - instance_id, - instance_label, - plugin_id, - instance is None, - error.title, - error.description, - error.detail, - ) - - @classmethod - def from_data(cls, data): - return cls(**data) - - -class PublishValidationErrorsReport: - """Publish validation errors report that can be parsed to raw data. - - Args: - error_items (List[ValidationErrorItem]): List of validation errors. - plugin_action_items (Dict[str, PublishPluginActionItem]): Action items - by plugin id. - """ - - def __init__(self, error_items, plugin_action_items): - self._error_items = error_items - self._plugin_action_items = plugin_action_items - - def __iter__(self): - for item in self._error_items: - yield item - - def group_items_by_title(self): - """Group errors by plugin and their titles. - - Items are grouped by plugin and title -> same title from different - plugin is different item. Items are ordered by plugin order. - - Returns: - List[Dict[str, Any]]: List where each item title, instance - information related to title and possible plugin actions. - """ - - ordered_plugin_ids = [] - error_items_by_plugin_id = collections.defaultdict(list) - for error_item in self._error_items: - plugin_id = error_item.plugin_id - if plugin_id not in ordered_plugin_ids: - ordered_plugin_ids.append(plugin_id) - error_items_by_plugin_id[plugin_id].append(error_item) - - grouped_error_items = [] - for plugin_id in ordered_plugin_ids: - plugin_action_items = self._plugin_action_items[plugin_id] - error_items = error_items_by_plugin_id[plugin_id] - - titles = [] - error_items_by_title = collections.defaultdict(list) - for error_item in error_items: - title = error_item.title - if title not in titles: - titles.append(error_item.title) - error_items_by_title[title].append(error_item) - - for title in titles: - grouped_error_items.append({ - "id": uuid.uuid4().hex, - "plugin_id": plugin_id, - "plugin_action_items": list(plugin_action_items), - "error_items": error_items_by_title[title], - "title": title - }) - return grouped_error_items - - def to_data(self): - """Serialize object to dictionary. - - Returns: - Dict[str, Any]: Serialized data. - """ - - error_items = [ - item.to_data() - for item in self._error_items - ] - - plugin_action_items = { - plugin_id: [ - action_item.to_data() - for action_item in action_items - ] - for plugin_id, action_items in self._plugin_action_items.items() - } - - return { - "error_items": error_items, - "plugin_action_items": plugin_action_items - } - - @classmethod - def from_data(cls, data): - """Recreate object from data. - - Args: - data (dict[str, Any]): Data to recreate object. Can be created - using 'to_data' method. - - Returns: - PublishValidationErrorsReport: New object based on data. - """ - - error_items = [ - ValidationErrorItem.from_data(error_item) - for error_item in data["error_items"] - ] - plugin_action_items = [ - PublishPluginActionItem.from_data(action_item) - for action_item in data["plugin_action_items"] - ] - return cls(error_items, plugin_action_items) - - -class PublishValidationErrors: - """Object to keep track about validation errors by plugin.""" - - def __init__(self): - self._plugins_proxy = None - self._error_items = [] - self._plugin_action_items = {} - - def __bool__(self): - return self.has_errors - - @property - def has_errors(self): - """At least one error was added.""" - - return bool(self._error_items) - - def reset(self, plugins_proxy): - """Reset object to default state. - - Args: - plugins_proxy (PublishPluginsProxy): Proxy which store plugins, - actions by ids and create mapping of action ids by plugin ids. - """ - - self._plugins_proxy = plugins_proxy - self._error_items = [] - self._plugin_action_items = {} - - def create_report(self): - """Create report based on currently existing errors. - - Returns: - PublishValidationErrorsReport: Validation error report with all - error information and publish plugin action items. - """ - - return PublishValidationErrorsReport( - self._error_items, self._plugin_action_items - ) - - def add_error(self, plugin, error, instance): - """Add error from pyblish result. - - Args: - plugin (pyblish.api.Plugin): Plugin which triggered error. - error (ValidationException): Validation error. - instance (Union[pyblish.api.Instance, None]): Instance on which was - error raised or None if was raised on context. - """ - - # Make sure the cached report is cleared - plugin_id = self._plugins_proxy.get_plugin_id(plugin) - if not error.title: - if hasattr(plugin, "label") and plugin.label: - plugin_label = plugin.label - else: - plugin_label = plugin.__name__ - error.title = plugin_label - - self._error_items.append( - ValidationErrorItem.from_result(plugin_id, error, instance) - ) - if plugin_id in self._plugin_action_items: - return - - plugin_actions = self._plugins_proxy.get_plugin_action_items( - plugin_id - ) - self._plugin_action_items[plugin_id] = plugin_actions - - -class CreatorType: - def __init__(self, name): - self.name = 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): - 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, - creator_type, - product_type, - label, - group_label, - icon, - description, - detailed_description, - default_variant, - default_variants, - create_allow_context_change, - create_allow_thumbnail, - show_order, - pre_create_attributes_defs, - ): - self.identifier = identifier - self.creator_type = creator_type - self.product_type = product_type - self.label = label - self.group_label = group_label - self.icon = icon - self.description = description - self.detailed_description = detailed_description - self.default_variant = default_variant - self.default_variants = default_variants - self.create_allow_context_change = create_allow_context_change - self.create_allow_thumbnail = create_allow_thumbnail - self.show_order = show_order - self.pre_create_attributes_defs = pre_create_attributes_defs - - def get_group_label(self): - return self.group_label - - @classmethod - def from_creator(cls, creator): - if isinstance(creator, AutoCreator): - creator_type = CreatorTypes.auto - elif isinstance(creator, HiddenCreator): - creator_type = CreatorTypes.hidden - elif isinstance(creator, Creator): - creator_type = CreatorTypes.artist - else: - creator_type = CreatorTypes.base - - 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): - 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): - 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) - - -@six.add_metaclass(ABCMeta) -class AbstractPublisherController(object): - """Publisher tool controller. - - Define what must be implemented to be able use Publisher functionality. - - Goal is to have "data driven" controller that can be used to control UI - running in different process. That lead to some disadvantages like UI can't - access objects directly but by using wrappers that can be serialized. - """ - - @property - @abstractmethod - def log(self): - """Controller's logger object. - - Returns: - logging.Logger: Logger object that can be used for logging. - """ - - pass - - @property - @abstractmethod - def event_system(self): - """Inner event system for publisher controller.""" - - pass - - @property - @abstractmethod - def project_name(self): - """Current context project name. - - Returns: - str: Name of project. - """ - - pass - - @property - @abstractmethod - def current_folder_path(self): - """Current context folder path. - - Returns: - Union[str, None]: Folder path. - - """ - pass - - @property - @abstractmethod - def current_task_name(self): - """Current context task name. - - Returns: - Union[str, None]: Name of task. - """ - - pass - - @property - @abstractmethod - def host_context_has_changed(self): - """Host context changed after last reset. - - 'CreateContext' has this option available using 'context_has_changed'. - - Returns: - bool: Context has changed. - """ - - pass - - @property - @abstractmethod - def host_is_valid(self): - """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 - - @property - @abstractmethod - def instances(self): - """Collected/created instances. - - Returns: - List[CreatedInstance]: List of created instances. - """ - - pass - - @abstractmethod - 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 - - @abstractmethod - def get_existing_product_names(self, folder_path): - pass - - @abstractmethod - def reset(self): - """Reset whole controller. - - This should reset create context, publish context and all variables - that are related to it. - """ - - pass - - @abstractmethod - def get_creator_attribute_definitions(self, instances): - pass - - @abstractmethod - def get_publish_attribute_definitions(self, instances, include_context): - pass - - @abstractmethod - def get_creator_icon(self, identifier): - """Receive creator's icon by identifier. - - Args: - identifier (str): Creator's identifier. - - Returns: - Union[str, None]: Creator's icon string. - """ - - 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 - - @abstractmethod - def save_changes(self): - """Save changes in create context. - - Save can crash because of unexpected errors. - - Returns: - bool: Save was successful. - """ - - pass - - @abstractmethod - def remove_instances(self, instance_ids): - """Remove list of instances from create context.""" - # TODO expect instance ids - - pass - - @property - @abstractmethod - def publish_has_started(self): - """Has publishing finished. - - Returns: - bool: If publishing finished and all plugins were iterated. - """ - - pass - - @property - @abstractmethod - def publish_has_finished(self): - """Has publishing finished. - - Returns: - bool: If publishing finished and all plugins were iterated. - """ - - pass - - @property - @abstractmethod - def publish_is_running(self): - """Publishing is running right now. - - Returns: - bool: If publishing is in progress. - """ - - pass - - @property - @abstractmethod - def publish_has_validated(self): - """Publish validation passed. - - Returns: - bool: If publishing passed last possible validation order. - """ - - pass - - @property - @abstractmethod - def publish_has_crashed(self): - """Publishing crashed for any reason. - - Returns: - bool: Publishing crashed. - """ - - pass - - @property - @abstractmethod - def publish_has_validation_errors(self): - """During validation happened at least one validation error. - - Returns: - bool: Validation error was raised during validation. - """ - - pass - - @property - @abstractmethod - def publish_max_progress(self): - """Get maximum possible progress number. - - Returns: - int: Number that can be used as 100% of publish progress bar. - """ - - pass - - @property - @abstractmethod - def publish_progress(self): - """Current progress number. - - Returns: - int: Current progress value from 0 to 'publish_max_progress'. - """ - - pass - - @property - @abstractmethod - def publish_error_msg(self): - """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): - pass - - @abstractmethod - def get_validation_errors(self): - pass - - @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, action_id): - """Trigger pyblish action on a plugin. - - Args: - plugin_id (str): Id of publish plugin. - action_id (str): Id of publish action. - """ - - pass - - @property - @abstractmethod - def convertor_items(self): - pass - - @abstractmethod - def trigger_convertor_items(self, convertor_identifiers): - pass - - @abstractmethod - def get_thumbnail_paths_for_instances(self, instance_ids): - pass - - @abstractmethod - def set_thumbnail_paths_for_instances(self, thumbnail_path_mapping): - 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, message_type=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. - """ - - pass - - @abstractmethod - def get_thumbnail_temp_dir_path(self): - """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 - - -class BasePublisherController(AbstractPublisherController): - """Implement common logic for controllers. - - Implement event system, logger and common attributes. Attributes are - triggering value changes so anyone can listen to their topics. - - Prepare implementation for creator items. Controller must implement just - their filling by '_collect_creator_items'. - - All prepared implementation is based on calling super '__init__'. - """ - - def __init__(self): self._log = None - self._event_system = None + self._event_system = self._create_event_system() - # Host is valid for creation - self._host_is_valid = False + self._host = registered_host() + self._headless = headless - # Any other exception that happened during publishing - self._publish_error_msg = None - # Publishing is in progress - self._publish_is_running = False - # Publishing is over validation order - self._publish_has_validated = False + self._create_model = CreateModel(self) + self._publish_model = PublishModel(self) - self._publish_has_validation_errors = False - self._publish_has_crashed = False - # All publish plugins are processed - self._publish_has_started = False - self._publish_has_finished = False - self._publish_max_progress = 0 - self._publish_progress = 0 - - # Controller must '_collect_creator_items' to fill the value - self._creator_items = None + # Cacher of avalon documents + self._projects_model = ProjectsModel(self) + self._hierarchy_model = HierarchyModel(self) @property def log(self): @@ -1347,45 +91,17 @@ class BasePublisherController(AbstractPublisherController): self._log = logging.getLogger(self.__class__.__name__) return self._log - @property - def event_system(self): - """Inner event system for publisher controller. + def is_headless(self): + return self._headless - Is used for communication with UI. Event system is autocreated. + def get_host(self): + return self._host - Known topics: - "show.detailed.help" - Detailed help requested (UI related). - "show.card.message" - Show card message request (UI related). - "instances.refresh.finished" - Instances are refreshed. - "plugins.refresh.finished" - Plugins refreshed. - "publish.reset.finished" - Reset finished. - "controller.reset.started" - Controller reset started. - "controller.reset.finished" - Controller reset finished. - "publish.process.started" - Publishing started. Can be started from - paused state. - "publish.process.stopped" - Publishing stopped/paused process. - "publish.process.plugin.changed" - Plugin state has changed. - "publish.process.instance.changed" - Instance state has changed. - "publish.has_validated.changed" - Attr 'publish_has_validated' - changed. - "publish.is_running.changed" - Attr 'publish_is_running' changed. - "publish.has_crashed.changed" - Attr 'publish_has_crashed' changed. - "publish.publish_error.changed" - Attr 'publish_error' - "publish.has_validation_errors.changed" - Attr - 'has_validation_errors' changed. - "publish.max_progress.changed" - Attr 'publish_max_progress' - changed. - "publish.progress.changed" - Attr 'publish_progress' changed. - "publish.host_is_valid.changed" - Attr 'host_is_valid' changed. - "publish.finished.changed" - Attr 'publish_has_finished' changed. + def get_create_context(self): + return self._create_model.get_create_context() - Returns: - EventSystem: Event system which can trigger callbacks for topics. - """ - - if self._event_system is None: - self._event_system = QueuedEventSystem() - return self._event_system + def is_host_valid(self) -> bool: + return self._create_model.is_host_valid() # Events system def emit_event(self, topic, data=None, source=None): @@ -1393,310 +109,89 @@ class BasePublisherController(AbstractPublisherController): if data is None: data = {} - self.event_system.emit(topic, data, source) + self._event_system.emit(topic, data, source) + + def emit_card_message( + self, message, message_type=CardMessageTypes.standard + ): + self._emit_event( + "show.card.message", + { + "message": message, + "message_type": message_type + } + ) def register_event_callback(self, topic, callback): - self.event_system.add_callback(topic, callback) + self._event_system.add_callback(topic, callback) - def _emit_event(self, topic, data=None): - self.emit_event(topic, data, "controller") - - def _get_host_is_valid(self): - return self._host_is_valid - - def _set_host_is_valid(self, value): - if self._host_is_valid != value: - self._host_is_valid = value - self._emit_event( - "publish.host_is_valid.changed", {"value": value} - ) - - def _get_publish_has_started(self): - return self._publish_has_started - - def _set_publish_has_started(self, value): - if value != self._publish_has_started: - self._publish_has_started = value - - def _get_publish_has_finished(self): - return self._publish_has_finished - - def _set_publish_has_finished(self, value): - if self._publish_has_finished != value: - self._publish_has_finished = value - self._emit_event("publish.finished.changed", {"value": value}) - - def _get_publish_is_running(self): - return self._publish_is_running - - def _set_publish_is_running(self, value): - if self._publish_is_running != value: - self._publish_is_running = value - self._emit_event("publish.is_running.changed", {"value": value}) - - def _get_publish_has_validated(self): - return self._publish_has_validated - - def _set_publish_has_validated(self, value): - if self._publish_has_validated != value: - self._publish_has_validated = value - self._emit_event( - "publish.has_validated.changed", {"value": value} - ) - - def _get_publish_has_crashed(self): - return self._publish_has_crashed - - def _set_publish_has_crashed(self, value): - if self._publish_has_crashed != value: - self._publish_has_crashed = value - self._emit_event("publish.has_crashed.changed", {"value": value}) - - def _get_publish_has_validation_errors(self): - return self._publish_has_validation_errors - - def _set_publish_has_validation_errors(self, value): - if self._publish_has_validation_errors != value: - self._publish_has_validation_errors = value - self._emit_event( - "publish.has_validation_errors.changed", - {"value": value} - ) - - def _get_publish_max_progress(self): - return self._publish_max_progress - - def _set_publish_max_progress(self, value): - if self._publish_max_progress != value: - self._publish_max_progress = value - self._emit_event("publish.max_progress.changed", {"value": value}) - - def _get_publish_progress(self): - return self._publish_progress - - def _set_publish_progress(self, value): - if self._publish_progress != value: - self._publish_progress = value - self._emit_event("publish.progress.changed", {"value": value}) - - def _get_publish_error_msg(self): - return self._publish_error_msg - - def _set_publish_error_msg(self, value): - if self._publish_error_msg != value: - self._publish_error_msg = value - self._emit_event("publish.publish_error.changed", {"value": value}) - - host_is_valid = property( - _get_host_is_valid, _set_host_is_valid - ) - publish_has_started = property( - _get_publish_has_started, _set_publish_has_started - ) - publish_has_finished = property( - _get_publish_has_finished, _set_publish_has_finished - ) - publish_is_running = property( - _get_publish_is_running, _set_publish_is_running - ) - publish_has_validated = property( - _get_publish_has_validated, _set_publish_has_validated - ) - publish_has_crashed = property( - _get_publish_has_crashed, _set_publish_has_crashed - ) - publish_has_validation_errors = property( - _get_publish_has_validation_errors, _set_publish_has_validation_errors - ) - publish_max_progress = property( - _get_publish_max_progress, _set_publish_max_progress - ) - publish_progress = property( - _get_publish_progress, _set_publish_progress - ) - publish_error_msg = property( - _get_publish_error_msg, _set_publish_error_msg - ) - - def _reset_attributes(self): - """Reset most of attributes that can be reset.""" - - self.publish_is_running = False - self.publish_has_started = False - self.publish_has_validated = False - self.publish_has_crashed = False - self.publish_has_validation_errors = False - self.publish_has_finished = False - - self.publish_error_msg = None - self.publish_progress = 0 - - @property - def creator_items(self): - """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 - - @abstractmethod - def _collect_creator_items(self): - """Receive CreatorItems to work with. - - Returns: - Dict[str, CreatorItem]: Creator items by their identifier. - """ - - pass - - def get_creator_icon(self, identifier): - """Function to receive icon for creator identifier. - - Args: - str: Creator's identifier for which should be icon returned. - """ - - creator_item = self.creator_items.get(identifier) - if creator_item is not None: - return creator_item.icon - return None - - def get_thumbnail_temp_dir_path(self): - """Return path to directory where thumbnails can be temporary stored. - - Returns: - str: Path to a directory. - """ - - return os.path.join( - tempfile.gettempdir(), - "publisher_thumbnails", - get_process_id() - ) - - def clear_thumbnail_temp_dir_path(self): - """Remove content of thumbnail temp directory.""" - - dirpath = self.get_thumbnail_temp_dir_path() - if os.path.exists(dirpath): - shutil.rmtree(dirpath) - - -class PublisherController(BasePublisherController): - """Middleware between UI, CreateContext and publish Context. - - Handle both creation and publishing parts. - - Args: - headless (bool): Headless publishing. ATM not implemented or used. - """ - - _log = None - - def __init__(self, headless=False): - super(PublisherController, self).__init__() - - self._host = registered_host() - self._headless = headless - - self._create_context = CreateContext( - self._host, headless=headless, reset=False - ) - - self._publish_plugins_proxy = None - - # pyblish.api.Context - self._publish_context = None - # Pyblish report - self._publish_report = PublishReportMaker(self) - # Store exceptions of validation error - self._publish_validation_errors = PublishValidationErrors() - - # Publishing should stop at validation stage - self._publish_up_validation = False - # This information is not much important for controller but for widget - # which can change (and set) the comment. - self._publish_comment_is_set = False - - # Validation order - # - plugin with order same or higher than this value is extractor or - # higher - self._validation_order = ( - pyblish.api.ValidatorOrder + PLUGIN_ORDER_OFFSET - ) - - # Plugin iterator - self._main_thread_iter = None - - # State flags to prevent executing method which is already in progress - self._resetting_plugins = False - self._resetting_instances = False - - # Cacher of avalon documents - self._projects_model = ProjectsModel(self) - self._hierarchy_model = HierarchyModel(self) - - @property - def project_name(self): + def get_current_project_name(self): """Current project context defined by host. Returns: str: Project name. + """ + return self._create_model.get_current_project_name() - return self._create_context.get_current_project_name() - - @property - def current_folder_path(self): + def get_current_folder_path(self): """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() + return self._create_model.get_current_folder_path() - @property - def current_task_name(self): + def get_current_task_name(self): """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() + return self._create_model.get_current_task_name() - @property def host_context_has_changed(self): - return self._create_context.context_has_changed + return self._create_model.host_context_has_changed() + + def get_creator_items(self): + """Creators that can be shown in create dialog.""" + return self._create_model.get_creator_items() + + def get_creator_item_by_id(self, identifier): + return self._create_model.get_creator_item_by_id(identifier) + + def get_creator_icon(self, identifier): + """Function to receive icon for creator identifier. + + Args: + identifier (str): Creator's identifier for which should + be icon returned. + + """ + return self._create_model.get_creator_icon(identifier) @property def instances(self): - """Current instances in create context.""" - return self._create_context.instances_by_id + """Current instances in create context. - @property - def convertor_items(self): - return self._create_context.convertor_items_by_id + Deprecated: + Use 'get_instances' instead. Kept for backwards compatibility with + traypublisher. - @property - def _creators(self): - """All creators loaded in create context.""" - - return self._create_context.creators - - @property - def _publish_plugins(self): - """Publish plugins.""" - return self._create_context.publish_plugins - - def _get_current_project_settings(self): - """Current project settings. - - Returns: - dict """ + return self.get_instances() - return self._create_context.get_current_project_settings() + def get_instances(self): + """Current instances in create context.""" + return self._create_model.get_instances() + + def get_instances_by_id(self, instance_ids=None): + return self._create_model.get_instances_by_id(instance_ids) + + def get_convertor_items(self): + return self._create_model.get_convertor_items() def get_folder_type_items(self, project_name, sender=None): return self._projects_model.get_folder_type_items( @@ -1725,12 +220,24 @@ class PublisherController(BasePublisherController): def get_task_entity(self, project_name, task_id): return self._hierarchy_model.get_task_entity(project_name, task_id) + def get_folder_item_by_path(self, project_name, folder_path): + return self._hierarchy_model.get_folder_item_by_path( + project_name, folder_path + ) + + def get_task_item_by_name( + self, project_name, folder_id, task_name, sender=None + ): + return self._hierarchy_model.get_task_item_by_name( + project_name, folder_id, task_name, sender + ) + # Publisher custom method def get_folder_id_from_path(self, folder_path): if not folder_path: return None - folder_item = self._hierarchy_model.get_folder_item_by_path( - self.project_name, folder_path + folder_item = self.get_folder_item_by_path( + self.get_current_project_name(), folder_path ) if folder_item: return folder_item.entity_id @@ -1739,19 +246,23 @@ class PublisherController(BasePublisherController): def get_task_items_by_folder_paths(self, folder_paths): if not folder_paths: return {} + folder_items = self._hierarchy_model.get_folder_items_by_paths( - self.project_name, folder_paths + self.get_current_project_name(), folder_paths ) + output = { folder_path: [] for folder_path in folder_paths } - project_name = self.project_name - for folder_item in folder_items.values(): - task_items = self._hierarchy_model.get_task_items( - project_name, folder_item.entity_id, None - ) - output[folder_item.path] = task_items + project_name = self.get_current_project_name() + for folder_path, folder_item in folder_items.items(): + task_items = [] + if folder_item is not None: + task_items = self._hierarchy_model.get_task_items( + project_name, folder_item.entity_id, None + ) + output[folder_path] = task_items return output @@ -1760,7 +271,7 @@ class PublisherController(BasePublisherController): return True folder_paths = set(folder_paths) folder_items = self._hierarchy_model.get_folder_items_by_paths( - self.project_name, folder_paths + self.get_current_project_name(), folder_paths ) for folder_item in folder_items.values(): if folder_item is None: @@ -1785,7 +296,7 @@ class PublisherController(BasePublisherController): def get_existing_product_names(self, folder_path): if not folder_path: return None - project_name = self.project_name + project_name = self.get_current_project_name() folder_item = self._hierarchy_model.get_folder_item_by_path( project_name, folder_path ) @@ -1808,228 +319,57 @@ class PublisherController(BasePublisherController): self._emit_event("controller.reset.started") - self.host_is_valid = self._create_context.host_is_valid - - self._create_context.reset_preparation() - - # Reset current context - self._create_context.reset_current_context() - self._hierarchy_model.reset() - self._reset_plugins() # Publish part must be reset after plugins - self._reset_publish() - self._reset_instances() - - self._create_context.reset_finalization() + self._create_model.reset() + self._publish_model.reset() self._emit_event("controller.reset.finished") self.emit_card_message("Refreshed..") - def _reset_plugins(self): - """Reset to initial state.""" - if self._resetting_plugins: - return - - self._resetting_plugins = True - - self._create_context.reset_plugins() - # Reset creator items - self._creator_items = None - - self._resetting_plugins = False - - self._emit_event("plugins.refresh.finished") - - def _collect_creator_items(self): - # 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 (not self._is_label_allowed( - creator.label, allowed_creator_pattern)): - self.log.debug(f"{creator.label} not allowed for context") - continue - output[identifier] = CreatorItem.from_creator(creator) - except Exception: - self.log.error( - "Failed to create creator item for '%s'", - identifier, - exc_info=True - ) - - return output - - def _get_allowed_creators_pattern(self): - """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.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, allowed_labels_regex): - """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)) - - def _reset_instances(self): - """Reset create instances.""" - if self._resetting_instances: - return - - self._resetting_instances = True - - 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._resetting_instances = False - - self._on_create_instance_change() - def get_thumbnail_paths_for_instances(self, instance_ids): - thumbnail_paths_by_instance_id = ( - self._create_context.thumbnail_paths_by_instance_id + return self._create_model.get_thumbnail_paths_for_instances( + instance_ids ) - 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): - 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 - } + self._create_model.set_thumbnail_paths_for_instances( + thumbnail_path_mapping ) - def emit_card_message( - self, message, message_type=CardMessageTypes.standard - ): - self._emit_event( - "show.card.message", - { - "message": message, - "message_type": message_type - } + def get_thumbnail_temp_dir_path(self): + """Return path to directory where thumbnails can be temporary stored. + + Returns: + str: Path to a directory. + """ + + return os.path.join( + tempfile.gettempdir(), + "publisher_thumbnails", + get_process_id() ) + def clear_thumbnail_temp_dir_path(self): + """Remove content of thumbnail temp directory.""" + + dirpath = self.get_thumbnail_temp_dir_path() + if os.path.exists(dirpath): + shutil.rmtree(dirpath) + def get_creator_attribute_definitions(self, instances): """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 + return self._create_model.get_creator_attribute_definitions( + instances + ) def get_publish_attribute_definitions(self, instances, include_context): """Collect publish attribute definitions for passed instances. @@ -2038,52 +378,11 @@ class PublisherController(BasePublisherController): instances(list): 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 - - if plugin_name not in all_plugin_values: - all_plugin_values[plugin_name] = {} - - plugin_values = all_plugin_values[plugin_name] - - for attr_def in attr_defs: - if isinstance(attr_def, UIDef): - continue - if attr_def.key not in plugin_values: - plugin_values[attr_def.key] = [] - attr_values = plugin_values[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 + return self._create_model.get_publish_attribute_definitions( + instances, include_context + ) def get_product_name( self, @@ -2105,38 +404,12 @@ class PublisherController(BasePublisherController): name is updated. """ - creator = self._creators[creator_identifier] - - instance = None - if instance_id: - instance = self.instances[instance_id] - - project_name = self.project_name - folder_item = self._hierarchy_model.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._hierarchy_model.get_folder_entity( - project_name, folder_item.entity_id - ) - task_item = self._hierarchy_model.get_task_item_by_name( - project_name, folder_item.entity_id, task_name, "controller" - ) - - if task_item is not None: - task_entity = self._hierarchy_model.get_task_entity( - project_name, task_item.task_id - ) - - return creator.get_product_name( - project_name, - folder_entity, - task_entity, + return self._create_model.get_product_name( + creator_identifier, variant, - instance=instance + task_name, + folder_path, + instance_id=None ) def trigger_convertor_items(self, convertor_identifiers): @@ -2150,24 +423,7 @@ class PublisherController(BasePublisherController): 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.emit_card_message("Conversion finished") - else: - self.emit_card_message("Conversion failed", CardMessageTypes.error) + self._create_model.trigger_convertor_items(convertor_identifiers) self.reset() @@ -2176,24 +432,9 @@ class PublisherController(BasePublisherController): ): """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 + return self._create_model.create( + creator_identifier, product_name, instance_data, options + ) def save_changes(self, show_message=True): """Save changes happened during creation. @@ -2209,96 +450,54 @@ class PublisherController(BasePublisherController): 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.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 + return self._create_model.save_changes(show_message) def remove_instances(self, instance_ids): """Remove instances based on instance ids. Args: instance_ids (List[str]): List of instance ids to remove. + """ + self._create_model.remove_instances(instance_ids) - # QUESTION Expect that instances are really removed? In that case reset - # is not required. - self._remove_instances_from_context(instance_ids) + def publish_has_started(self): + return self._publish_model.has_started() - self._on_create_instance_change() + def publish_has_finished(self): + return self._publish_model.has_finished() - def _remove_instances_from_context(self, instance_ids): - 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 publish_is_running(self): + return self._publish_model.is_running() - def _on_create_instance_change(self): - self._emit_event("instances.refresh.finished") + def publish_has_validated(self): + return self._publish_model.has_validated() + + def publish_has_crashed(self): + return self._publish_model.is_crashed() + + def publish_has_validation_errors(self): + return self._publish_model.has_validation_errors() + + def publish_can_continue(self): + return self._publish_model.publish_can_continue() + + def get_publish_max_progress(self): + return self._publish_model.get_max_progress() + + def get_publish_progress(self): + return self._publish_model.get_progress() + + def get_publish_error_msg(self): + return self._publish_model.get_error_msg() def get_publish_report(self): - return self._publish_report.get_report(self._publish_plugins) + return self._publish_model.get_publish_report() def get_validation_errors(self): - return self._publish_validation_errors.create_report() - - def _reset_publish(self): - self._reset_attributes() - - self._publish_up_validation = False - self._publish_comment_is_set = False - - self._main_thread_iter = self._publish_iterator() - self._publish_context = pyblish.api.Context() - # Make sure "comment" is set on publish context - self._publish_context.data["comment"] = "" - # Add access to create context during publishing - # - must not be used for changing CreatedInstances during publishing! - # QUESTION - # - pop the key after first collector using it would be safest option? - self._publish_context.data["create_context"] = self._create_context - - self._publish_plugins_proxy = PublishPluginsProxy( - self._publish_plugins - ) - - self._publish_report.reset(self._publish_context, self._create_context) - self._publish_validation_errors.reset(self._publish_plugins_proxy) - - self.publish_max_progress = len(self._publish_plugins) - - self._emit_event("publish.reset.finished") + return self._publish_model.get_validation_errors() def set_comment(self, comment): """Set comment from ui to pyblish context. @@ -2308,9 +507,7 @@ class PublisherController(BasePublisherController): '_publish_comment_is_set' is used to keep track about the information. """ - if not self._publish_comment_is_set: - self._publish_context.data["comment"] = comment - self._publish_comment_is_set = True + self._publish_model.set_comment(comment) def publish(self): """Run publishing. @@ -2318,9 +515,7 @@ class PublisherController(BasePublisherController): Make sure all changes are saved before method is called (Call 'save_changes' and check output). """ - - self._publish_up_validation = False - self._start_publish() + self._start_publish(False) def validate(self): """Run publishing and stop after Validation. @@ -2328,292 +523,21 @@ class PublisherController(BasePublisherController): Make sure all changes are saved before method is called (Call 'save_changes' and check output). """ - - if self.publish_has_validated: - return - self._publish_up_validation = True - self._start_publish() - - def _start_publish(self): - """Start or continue in publishing.""" - if self.publish_is_running: - return - - self.publish_is_running = True - self.publish_has_started = True - - self._emit_event("publish.process.started") - - self._publish_next_process() - - def _stop_publish(self): - """Stop or pause publishing.""" - self.publish_is_running = False - - self._emit_event("publish.process.stopped") + self._start_publish(True) def stop_publish(self): """Stop publishing process (any reason).""" - - if self.publish_is_running: - self._stop_publish() + self._publish_model.stop_publish() def run_action(self, plugin_id, action_id): - # TODO handle result in UI - plugin = self._publish_plugins_proxy.get_plugin(plugin_id) - action = self._publish_plugins_proxy.get_action(plugin_id, action_id) + self._publish_model.run_action(plugin_id, action_id) - result = pyblish.plugin.process( - plugin, self._publish_context, None, action.id - ) - exception = result.get("error") - if exception: - self._emit_event( - "publish.action.failed", - { - "title": "Action failed", - "message": "Action failed.", - "traceback": "".join( - traceback.format_exception( - type(exception), - exception, - exception.__traceback__ - ) - ), - "label": action.__name__, - "identifier": action.id - } - ) + def _create_event_system(self): + return QueuedEventSystem() - self._publish_report.add_action_result(action, result) + def _emit_event(self, topic, data=None): + self.emit_event(topic, data, "controller") - self.emit_card_message("Action finished.") - - def _publish_next_process(self): - # Validations of progress before using iterator - # - same conditions may be inside iterator but they may be used - # only in specific cases (e.g. when it happens for a first time) - - # There are validation errors and validation is passed - # - can't do any progree - if ( - self.publish_has_validated - and self.publish_has_validation_errors - ): - item = MainThreadItem(self.stop_publish) - - # Any unexpected error happened - # - everything should stop - elif self.publish_has_crashed: - item = MainThreadItem(self.stop_publish) - - # Everything is ok so try to get new processing item - else: - item = next(self._main_thread_iter) - - self._process_main_thread_item(item) - - def _process_main_thread_item(self, item): - item() - - def _is_publish_plugin_active(self, plugin): - """Decide if publish plugin is active. - - This is hack because 'active' is mis-used in mixin - 'OptionalPyblishPluginMixin' where 'active' is used for default value - of optional plugins. Because of that is 'active' state of plugin - which inherit from 'OptionalPyblishPluginMixin' ignored. That affects - headless publishing inside host, potentially remote publishing. - - We have to change that to match pyblish base, but we can do that - only when all hosts use Publisher because the change requires - change of settings schemas. - - Args: - plugin (pyblish.Plugin): Plugin which should be checked if is - active. - - Returns: - bool: Is plugin active. - """ - - if plugin.active: - return True - - if not plugin.optional: - return False - - if OptionalPyblishPluginMixin in inspect.getmro(plugin): - return True - return False - - def _publish_iterator(self): - """Main logic center of publishing. - - Iterator returns `MainThreadItem` objects with callbacks that should be - processed in main thread (threaded in future?). Cares about changing - states of currently processed publish plugin and instance. Also - change state of processed orders like validation order has passed etc. - - Also stops publishing, if should stop on validation. - """ - - for idx, plugin in enumerate(self._publish_plugins): - self._publish_progress = idx - - # Check if plugin is over validation order - if not self.publish_has_validated: - self.publish_has_validated = ( - plugin.order >= self._validation_order - ) - - # Stop if plugin is over validation order and process - # should process up to validation. - if self._publish_up_validation and self.publish_has_validated: - yield MainThreadItem(self.stop_publish) - - # Stop if validation is over and validation errors happened - if ( - self.publish_has_validated - and self.publish_has_validation_errors - ): - yield MainThreadItem(self.stop_publish) - - # Add plugin to publish report - self._publish_report.add_plugin_iter( - plugin, self._publish_context) - - # WARNING This is hack fix for optional plugins - if not self._is_publish_plugin_active(plugin): - self._publish_report.set_plugin_skipped() - continue - - # Trigger callback that new plugin is going to be processed - plugin_label = plugin.__name__ - if hasattr(plugin, "label") and plugin.label: - plugin_label = plugin.label - self._emit_event( - "publish.process.plugin.changed", - {"plugin_label": plugin_label} - ) - - # Plugin is instance plugin - if plugin.__instanceEnabled__: - instances = pyblish.logic.instances_by_plugin( - self._publish_context, plugin - ) - if not instances: - self._publish_report.set_plugin_skipped() - continue - - for instance in instances: - if instance.data.get("publish") is False: - continue - - instance_label = ( - instance.data.get("label") - or instance.data["name"] - ) - self._emit_event( - "publish.process.instance.changed", - {"instance_label": instance_label} - ) - - yield MainThreadItem( - self._process_and_continue, plugin, instance - ) - else: - families = collect_families_from_instances( - self._publish_context, only_active=True - ) - plugins = pyblish.logic.plugins_by_families( - [plugin], families - ) - if plugins: - instance_label = ( - self._publish_context.data.get("label") - or self._publish_context.data.get("name") - or "Context" - ) - self._emit_event( - "publish.process.instance.changed", - {"instance_label": instance_label} - ) - yield MainThreadItem( - self._process_and_continue, plugin, None - ) - else: - self._publish_report.set_plugin_skipped() - - # Cleanup of publishing process - self.publish_has_finished = True - self.publish_progress = self.publish_max_progress - yield MainThreadItem(self.stop_publish) - - def _add_validation_error(self, result): - self.publish_has_validation_errors = True - self._publish_validation_errors.add_error( - result["plugin"], - result["error"], - result["instance"] - ) - - def _process_and_continue(self, plugin, instance): - result = pyblish.plugin.process( - plugin, self._publish_context, instance - ) - - exception = result.get("error") - if exception: - has_validation_error = False - if ( - isinstance(exception, PublishValidationError) - and not self.publish_has_validated - ): - has_validation_error = True - self._add_validation_error(result) - - else: - if isinstance(exception, KnownPublishError): - msg = str(exception) - else: - msg = ( - "Something went wrong. Send report" - " to your supervisor or Ynput team." - ) - self.publish_error_msg = msg - self.publish_has_crashed = True - - result["is_validation_error"] = has_validation_error - - self._publish_report.add_result(result) - - self._publish_next_process() - - -def collect_families_from_instances(instances, only_active=False): - """Collect all families for passed publish instances. - - Args: - instances(list): List of publish instances from - which are families collected. - only_active(bool): Return families only for active instances. - - Returns: - list[str]: Families available on instances. - """ - - all_families = set() - for instance in instances: - if only_active: - if instance.data.get("publish") is False: - continue - family = instance.data.get("family") - if family: - all_families.add(family) - - families = instance.data.get("families") or tuple() - for family in families: - all_families.add(family) - - return list(all_families) + def _start_publish(self, up_validation): + self._publish_model.set_publish_up_validation(up_validation) + self._publish_model.start_publish(wait=True) diff --git a/client/ayon_core/tools/publisher/control_qt.py b/client/ayon_core/tools/publisher/control_qt.py index bef3a5af3b..b42b9afea3 100644 --- a/client/ayon_core/tools/publisher/control_qt.py +++ b/client/ayon_core/tools/publisher/control_qt.py @@ -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 diff --git a/client/ayon_core/tools/publisher/models/__init__.py b/client/ayon_core/tools/publisher/models/__init__.py new file mode 100644 index 0000000000..bd593be29b --- /dev/null +++ b/client/ayon_core/tools/publisher/models/__init__.py @@ -0,0 +1,10 @@ +from .create import CreateModel, CreatorItem +from .publish import PublishModel + + +__all__ = ( + "CreateModel", + "CreatorItem", + + "PublishModel", +) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py new file mode 100644 index 0000000000..6da3a51a31 --- /dev/null +++ b/client/ayon_core/tools/publisher/models/create.py @@ -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)) diff --git a/client/ayon_core/tools/publisher/models/publish.py b/client/ayon_core/tools/publisher/models/publish.py new file mode 100644 index 0000000000..da7b64ceae --- /dev/null +++ b/client/ayon_core/tools/publisher/models/publish.py @@ -0,0 +1,1273 @@ +import uuid +import copy +import inspect +import traceback +import collections +from functools import partial +from typing import Optional, Dict, List, Union, Any, Iterable, Literal + +import arrow +import pyblish.plugin + +from ayon_core.pipeline import ( + PublishValidationError, + KnownPublishError, + OptionalPyblishPluginMixin, +) +from ayon_core.pipeline.plugin_discover import DiscoverResult +from ayon_core.pipeline.publish import get_publish_instance_label +from ayon_core.tools.publisher.abstract import AbstractPublisherBackend + +PUBLISH_EVENT_SOURCE = "publisher.publish.model" +# Define constant for plugin orders offset +PLUGIN_ORDER_OFFSET = 0.5 + +ActionFilterType = Literal[ + "all", + "notProcessed", + "processed", + "failed", + "warning", + "failedOrWarning", + "succeeded" +] + + +class PublishReportMaker: + """Report for single publishing process. + + Report keeps current state of publishing and currently processed plugin. + """ + + def __init__( + self, + creator_discover_result: Optional[DiscoverResult] = None, + convertor_discover_result: Optional[DiscoverResult] = None, + publish_discover_result: Optional[DiscoverResult] = None, + ): + self._create_discover_result: Union[DiscoverResult, None] = None + self._convert_discover_result: Union[DiscoverResult, None] = None + self._publish_discover_result: Union[DiscoverResult, None] = None + + self._all_instances_by_id: Dict[str, pyblish.api.Instance] = {} + self._plugin_data_by_id: Dict[str, Any] = {} + self._current_plugin_id: Optional[str] = None + + self.reset( + creator_discover_result, + convertor_discover_result, + publish_discover_result, + ) + + def reset( + self, + creator_discover_result: Union[DiscoverResult, None], + convertor_discover_result: Union[DiscoverResult, None], + publish_discover_result: Union[DiscoverResult, None], + ): + """Reset report and clear all data.""" + + self._create_discover_result = creator_discover_result + self._convert_discover_result = convertor_discover_result + self._publish_discover_result = publish_discover_result + + self._all_instances_by_id = {} + self._plugin_data_by_id = {} + self._current_plugin_id = None + + publish_plugins = [] + if publish_discover_result is not None: + publish_plugins = publish_discover_result.plugins + + for plugin in publish_plugins: + self._add_plugin_data_item(plugin) + + def add_plugin_iter(self, plugin_id: str, context: pyblish.api.Context): + """Add report about single iteration of plugin.""" + for instance in context: + self._all_instances_by_id[instance.id] = instance + + self._current_plugin_id = plugin_id + + def set_plugin_passed(self, plugin_id: str): + plugin_data = self._plugin_data_by_id[plugin_id] + plugin_data["passed"] = True + + def set_plugin_skipped(self, plugin_id: str): + """Set that current plugin has been skipped.""" + plugin_data = self._plugin_data_by_id[plugin_id] + plugin_data["skipped"] = True + + def add_result(self, plugin_id: str, result: Dict[str, Any]): + """Handle result of one plugin and it's instance.""" + + instance = result["instance"] + instance_id = None + if instance is not None: + instance_id = instance.id + plugin_data = self._plugin_data_by_id[plugin_id] + plugin_data["instances_data"].append({ + "id": instance_id, + "logs": self._extract_instance_log_items(result), + "process_time": result["duration"] + }) + + def add_action_result( + self, action: pyblish.api.Action, result: Dict[str, Any] + ): + """Add result of single action.""" + plugin = result["plugin"] + + store_item = self._plugin_data_by_id[plugin.id] + + action_name = action.__name__ + action_label = action.label or action_name + log_items = self._extract_log_items(result) + store_item["actions_data"].append({ + "success": result["success"], + "name": action_name, + "label": action_label, + "logs": log_items + }) + + def get_report( + self, publish_context: pyblish.api.Context + ) -> Dict[str, Any]: + """Report data with all details of current state.""" + + now = arrow.utcnow().to("local") + instances_details = { + instance.id: self._extract_instance_data( + instance, instance in publish_context + ) + for instance in self._all_instances_by_id.values() + } + + plugins_data_by_id = copy.deepcopy( + self._plugin_data_by_id + ) + + # Ensure the current plug-in is marked as `passed` in the result + # so that it shows on reports for paused publishes + if self._current_plugin_id is not None: + current_plugin_data = plugins_data_by_id.get( + self._current_plugin_id + ) + if current_plugin_data and not current_plugin_data["passed"]: + current_plugin_data["passed"] = True + + reports = [] + if self._create_discover_result is not None: + reports.append(self._create_discover_result) + + if self._convert_discover_result is not None: + reports.append(self._convert_discover_result) + + if self._publish_discover_result is not None: + reports.append(self._publish_discover_result) + + crashed_file_paths = {} + for report in reports: + items = report.crashed_file_paths.items() + for filepath, exc_info in items: + crashed_file_paths[filepath] = "".join( + traceback.format_exception(*exc_info) + ) + + return { + "plugins_data": list(plugins_data_by_id.values()), + "instances": instances_details, + "context": self._extract_context_data(publish_context), + "crashed_file_paths": crashed_file_paths, + "id": uuid.uuid4().hex, + "created_at": now.isoformat(), + "report_version": "1.0.1", + } + + def _add_plugin_data_item(self, plugin: pyblish.api.Plugin): + if plugin.id in self._plugin_data_by_id: + # A plugin would be processed more than once. What can cause it: + # - there is a bug in controller + # - plugin class is imported into multiple files + # - this can happen even with base classes from 'pyblish' + raise ValueError( + "Plugin '{}' is already stored".format(str(plugin))) + + plugin_data_item = self._create_plugin_data_item(plugin) + self._plugin_data_by_id[plugin.id] = plugin_data_item + + def _create_plugin_data_item( + self, plugin: pyblish.api.Plugin + ) -> Dict[str, Any]: + label = None + if hasattr(plugin, "label"): + label = plugin.label + + return { + "id": plugin.id, + "name": plugin.__name__, + "label": label, + "order": plugin.order, + "targets": list(plugin.targets), + "instances_data": [], + "actions_data": [], + "skipped": False, + "passed": False + } + + def _extract_context_data( + self, context: pyblish.api.Context + ) -> Dict[str, Any]: + context_label = "Context" + if context is not None: + context_label = context.data.get("label") + return { + "label": context_label + } + + def _extract_instance_data( + self, instance: pyblish.api.Instance, exists: bool + ) -> Dict[str, Any]: + return { + "name": instance.data.get("name"), + "label": get_publish_instance_label(instance), + "product_type": instance.data.get("productType"), + "family": instance.data.get("family"), + "families": instance.data.get("families") or [], + "exists": exists, + "creator_identifier": instance.data.get("creator_identifier"), + "instance_id": instance.data.get("instance_id"), + } + + def _extract_instance_log_items( + self, result: Dict[str, Any] + ) -> List[Dict[str, Any]]: + instance = result["instance"] + instance_id = None + if instance: + instance_id = instance.id + + log_items = self._extract_log_items(result) + for item in log_items: + item["instance_id"] = instance_id + return log_items + + def _extract_log_items(self, result): + output = [] + records = result.get("records") or [] + for record in records: + record_exc_info = record.exc_info + if record_exc_info is not None: + record_exc_info = "".join( + traceback.format_exception(*record_exc_info) + ) + + try: + msg = record.getMessage() + except Exception: + msg = str(record.msg) + + output.append({ + "type": "record", + "msg": msg, + "name": record.name, + "lineno": record.lineno, + "levelno": record.levelno, + "levelname": record.levelname, + "threadName": record.threadName, + "filename": record.filename, + "pathname": record.pathname, + "msecs": record.msecs, + "exc_info": record_exc_info + }) + + exception = result.get("error") + if exception: + fname, line_no, func, exc = exception.traceback + + # Conversion of exception into string may crash + try: + msg = str(exception) + except BaseException: + msg = ( + "Publisher Controller: ERROR" + " - Failed to get exception message" + ) + + # Action result does not have 'is_validation_error' + is_validation_error = result.get("is_validation_error", False) + output.append({ + "type": "error", + "is_validation_error": is_validation_error, + "msg": msg, + "filename": str(fname), + "lineno": str(line_no), + "func": str(func), + "traceback": exception.formatted_traceback + }) + + return output + + +class PublishPluginActionItem: + """Representation of publish plugin action. + + Data driven object which is used as proxy for controller and UI. + + Args: + action_id (str): Action id. + plugin_id (str): Plugin id. + active (bool): Action is active. + on_filter (ActionFilterType): Actions have 'on' attribute which define + when can be action triggered (e.g. 'all', 'failed', ...). + label (str): Action's label. + icon (Optional[str]) Action's icon. + """ + + def __init__( + self, + action_id: str, + plugin_id: str, + active: bool, + on_filter: ActionFilterType, + label: str, + icon: Optional[str], + ): + self.action_id: str = action_id + self.plugin_id: str = plugin_id + self.active: bool = active + self.on_filter: ActionFilterType = on_filter + self.label: str = label + self.icon: Optional[str] = icon + + def to_data(self) -> Dict[str, Any]: + """Serialize object to dictionary. + + Returns: + Dict[str, Union[str,bool,None]]: Serialized object. + """ + + return { + "action_id": self.action_id, + "plugin_id": self.plugin_id, + "active": self.active, + "on_filter": self.on_filter, + "label": self.label, + "icon": self.icon + } + + @classmethod + def from_data(cls, data: Dict[str, Any]) -> "PublishPluginActionItem": + """Create object from data. + + Args: + data (Dict[str, Union[str,bool,None]]): Data used to recreate + object. + + Returns: + PublishPluginActionItem: Object created using data. + """ + + return cls(**data) + + +class PublishPluginsProxy: + """Wrapper around publish plugin. + + Prepare mapping for publish plugins and actions. Also can create + serializable data for plugin actions so UI don't have to have access to + them. + + This object is created in process where publishing is actually running. + + Notes: + Actions have id but single action can be used on multiple plugins so + to run an action is needed combination of plugin and action. + + Args: + plugins [List[pyblish.api.Plugin]]: Discovered plugins that will be + processed. + """ + + def __init__(self, plugins: List[pyblish.api.Plugin]): + plugins_by_id: Dict[str, pyblish.api.Plugin] = {} + actions_by_plugin_id: Dict[str, Dict[str, pyblish.api.Action]] = {} + action_ids_by_plugin_id: Dict[str, List[str]] = {} + for plugin in plugins: + plugin_id = plugin.id + plugins_by_id[plugin_id] = plugin + + action_ids = [] + actions_by_id = {} + action_ids_by_plugin_id[plugin_id] = action_ids + actions_by_plugin_id[plugin_id] = actions_by_id + + actions = getattr(plugin, "actions", None) or [] + for action in actions: + action_id = action.id + action_ids.append(action_id) + actions_by_id[action_id] = action + + self._plugins_by_id: Dict[str, pyblish.api.Plugin] = plugins_by_id + self._actions_by_plugin_id: Dict[ + str, Dict[str, pyblish.api.Action] + ] = actions_by_plugin_id + self._action_ids_by_plugin_id: Dict[str, List[str]] = ( + action_ids_by_plugin_id + ) + + def get_action( + self, plugin_id: str, action_id: str + ) -> pyblish.api.Action: + return self._actions_by_plugin_id[plugin_id][action_id] + + def get_plugin(self, plugin_id: str) -> pyblish.api.Plugin: + return self._plugins_by_id[plugin_id] + + def get_plugin_id(self, plugin: pyblish.api.Plugin) -> str: + """Get id of plugin based on plugin object. + + It's used for validation errors report. + + Args: + plugin (pyblish.api.Plugin): Publish plugin for which id should be + returned. + + Returns: + str: Plugin id. + """ + + return plugin.id + + def get_plugin_action_items( + self, plugin_id: str + ) -> List[PublishPluginActionItem]: + """Get plugin action items for plugin by its id. + + Args: + plugin_id (str): Publish plugin id. + + Returns: + List[PublishPluginActionItem]: Items with information about publish + plugin actions. + """ + + return [ + self._create_action_item( + self.get_action(plugin_id, action_id), plugin_id + ) + for action_id in self._action_ids_by_plugin_id[plugin_id] + ] + + def _create_action_item( + self, action: pyblish.api.Action, plugin_id: str + ) -> PublishPluginActionItem: + label = action.label or action.__name__ + icon = getattr(action, "icon", None) + return PublishPluginActionItem( + action.id, + plugin_id, + action.active, + action.on, + label, + icon + ) + + +class ValidationErrorItem: + """Data driven validation error item. + + Prepared data container with information about validation error and it's + source plugin. + + Can be converted to raw data and recreated should be used for controller + and UI connection. + + Args: + instance_id (Optional[str]): Pyblish instance id to which is + validation error connected. + instance_label (Optional[str]): Prepared instance label. + plugin_id (str): Pyblish plugin id which triggered the validation + error. Id is generated using 'PublishPluginsProxy'. + context_validation (bool): Error happened on context. + title (str): Error title. + description (str): Error description. + detail (str): Error detail. + + """ + def __init__( + self, + instance_id: Optional[str], + instance_label: Optional[str], + plugin_id: str, + context_validation: bool, + title: str, + description: str, + detail: str + ): + self.instance_id: Optional[str] = instance_id + self.instance_label: Optional[str] = instance_label + self.plugin_id: str = plugin_id + self.context_validation: bool = context_validation + self.title: str = title + self.description: str = description + self.detail: str = detail + + def to_data(self) -> Dict[str, Any]: + """Serialize object to dictionary. + + Returns: + Dict[str, Union[str, bool, None]]: Serialized object data. + """ + + return { + "instance_id": self.instance_id, + "instance_label": self.instance_label, + "plugin_id": self.plugin_id, + "context_validation": self.context_validation, + "title": self.title, + "description": self.description, + "detail": self.detail, + } + + @classmethod + def from_result( + cls, + plugin_id: str, + error: PublishValidationError, + instance: Union[pyblish.api.Instance, None] + ): + """Create new object based on resukt from controller. + + Returns: + ValidationErrorItem: New object with filled data. + """ + + instance_label = None + instance_id = None + if instance is not None: + instance_label = ( + instance.data.get("label") or instance.data.get("name") + ) + instance_id = instance.id + + return cls( + instance_id, + instance_label, + plugin_id, + instance is None, + error.title, + error.description, + error.detail, + ) + + @classmethod + def from_data(cls, data): + return cls(**data) + + +class PublishValidationErrorsReport: + """Publish validation errors report that can be parsed to raw data. + + Args: + error_items (List[ValidationErrorItem]): List of validation errors. + plugin_action_items (Dict[str, List[PublishPluginActionItem]]): Action + items by plugin id. + + """ + def __init__(self, error_items, plugin_action_items): + self._error_items = error_items + self._plugin_action_items = plugin_action_items + + def __iter__(self) -> Iterable[ValidationErrorItem]: + for item in self._error_items: + yield item + + def group_items_by_title(self) -> List[Dict[str, Any]]: + """Group errors by plugin and their titles. + + Items are grouped by plugin and title -> same title from different + plugin is different item. Items are ordered by plugin order. + + Returns: + List[Dict[str, Any]]: List where each item title, instance + information related to title and possible plugin actions. + """ + + ordered_plugin_ids = [] + error_items_by_plugin_id = collections.defaultdict(list) + for error_item in self._error_items: + plugin_id = error_item.plugin_id + if plugin_id not in ordered_plugin_ids: + ordered_plugin_ids.append(plugin_id) + error_items_by_plugin_id[plugin_id].append(error_item) + + grouped_error_items = [] + for plugin_id in ordered_plugin_ids: + plugin_action_items = self._plugin_action_items[plugin_id] + error_items = error_items_by_plugin_id[plugin_id] + + titles = [] + error_items_by_title = collections.defaultdict(list) + for error_item in error_items: + title = error_item.title + if title not in titles: + titles.append(error_item.title) + error_items_by_title[title].append(error_item) + + for title in titles: + grouped_error_items.append({ + "id": uuid.uuid4().hex, + "plugin_id": plugin_id, + "plugin_action_items": list(plugin_action_items), + "error_items": error_items_by_title[title], + "title": title + }) + return grouped_error_items + + def to_data(self): + """Serialize object to dictionary. + + Returns: + Dict[str, Any]: Serialized data. + """ + + error_items = [ + item.to_data() + for item in self._error_items + ] + + plugin_action_items = { + plugin_id: [ + action_item.to_data() + for action_item in action_items + ] + for plugin_id, action_items in self._plugin_action_items.items() + } + + return { + "error_items": error_items, + "plugin_action_items": plugin_action_items + } + + @classmethod + def from_data( + cls, data: Dict[str, Any] + ) -> "PublishValidationErrorsReport": + """Recreate object from data. + + Args: + data (dict[str, Any]): Data to recreate object. Can be created + using 'to_data' method. + + Returns: + PublishValidationErrorsReport: New object based on data. + """ + + error_items = [ + ValidationErrorItem.from_data(error_item) + for error_item in data["error_items"] + ] + plugin_action_items = {} + for action_item in data["plugin_action_items"]: + item = PublishPluginActionItem.from_data(action_item) + action_items = plugin_action_items.setdefault(item.plugin_id, []) + action_items.append(item) + + return cls(error_items, plugin_action_items) + + +class PublishValidationErrors: + """Object to keep track about validation errors by plugin.""" + + def __init__(self): + self._plugins_proxy: Union[PublishPluginsProxy, None] = None + self._error_items: List[ValidationErrorItem] = [] + self._plugin_action_items: Dict[ + str, List[PublishPluginActionItem] + ] = {} + + def __bool__(self): + return self.has_errors + + @property + def has_errors(self) -> bool: + """At least one error was added.""" + + return bool(self._error_items) + + def reset(self, plugins_proxy: PublishPluginsProxy): + """Reset object to default state. + + Args: + plugins_proxy (PublishPluginsProxy): Proxy which store plugins, + actions by ids and create mapping of action ids by plugin ids. + """ + + self._plugins_proxy = plugins_proxy + self._error_items = [] + self._plugin_action_items = {} + + def create_report(self) -> PublishValidationErrorsReport: + """Create report based on currently existing errors. + + Returns: + PublishValidationErrorsReport: Validation error report with all + error information and publish plugin action items. + """ + + return PublishValidationErrorsReport( + self._error_items, self._plugin_action_items + ) + + def add_error( + self, + plugin: pyblish.api.Plugin, + error: PublishValidationError, + instance: Union[pyblish.api.Instance, None] + ): + """Add error from pyblish result. + + Args: + plugin (pyblish.api.Plugin): Plugin which triggered error. + error (PublishValidationError): Validation error. + instance (Union[pyblish.api.Instance, None]): Instance on which was + error raised or None if was raised on context. + """ + + # Make sure the cached report is cleared + plugin_id = self._plugins_proxy.get_plugin_id(plugin) + if not error.title: + if hasattr(plugin, "label") and plugin.label: + plugin_label = plugin.label + else: + plugin_label = plugin.__name__ + error.title = plugin_label + + self._error_items.append( + ValidationErrorItem.from_result(plugin_id, error, instance) + ) + if plugin_id in self._plugin_action_items: + return + + plugin_actions = self._plugins_proxy.get_plugin_action_items( + plugin_id + ) + self._plugin_action_items[plugin_id] = plugin_actions + + +def collect_families_from_instances( + instances: List[pyblish.api.Instance], + only_active: Optional[bool] = False +) -> List[str]: + """Collect all families for passed publish instances. + + Args: + instances (list[pyblish.api.Instance]): List of publish instances from + which are families collected. + only_active (bool): Return families only for active instances. + + Returns: + list[str]: Families available on instances. + + """ + all_families = set() + for instance in instances: + if only_active: + if instance.data.get("publish") is False: + continue + family = instance.data.get("family") + if family: + all_families.add(family) + + families = instance.data.get("families") or tuple() + for family in families: + all_families.add(family) + + return list(all_families) + + +class PublishModel: + def __init__(self, controller: AbstractPublisherBackend): + self._controller = controller + + # Publishing should stop at validation stage + self._publish_up_validation: bool = False + self._publish_comment_is_set: bool = False + + # Any other exception that happened during publishing + self._publish_error_msg: Optional[str] = None + # Publishing is in progress + self._publish_is_running: bool = False + # Publishing is over validation order + self._publish_has_validated: bool = False + + self._publish_has_validation_errors: bool = False + self._publish_has_crashed: bool = False + # All publish plugins are processed + self._publish_has_started: bool = False + self._publish_has_finished: bool = False + self._publish_max_progress: int = 0 + self._publish_progress: int = 0 + + self._publish_plugins: List[pyblish.api.Plugin] = [] + self._publish_plugins_proxy: PublishPluginsProxy = ( + PublishPluginsProxy([]) + ) + + # pyblish.api.Context + self._publish_context = None + # Pyblish report + self._publish_report: PublishReportMaker = PublishReportMaker() + # Store exceptions of validation error + self._publish_validation_errors: PublishValidationErrors = ( + PublishValidationErrors() + ) + + # This information is not much important for controller but for widget + # which can change (and set) the comment. + self._publish_comment_is_set: bool = False + + # Validation order + # - plugin with order same or higher than this value is extractor or + # higher + self._validation_order: int = ( + pyblish.api.ValidatorOrder + PLUGIN_ORDER_OFFSET + ) + + # Plugin iterator + self._main_thread_iter: Iterable[partial] = [] + + def reset(self): + create_context = self._controller.get_create_context() + self._publish_up_validation = False + self._publish_comment_is_set = False + self._publish_has_started = False + + self._set_publish_error_msg(None) + self._set_progress(0) + self._set_is_running(False) + self._set_has_validated(False) + self._set_is_crashed(False) + self._set_has_validation_errors(False) + self._set_finished(False) + + self._main_thread_iter = self._publish_iterator() + self._publish_context = pyblish.api.Context() + # Make sure "comment" is set on publish context + self._publish_context.data["comment"] = "" + # Add access to create context during publishing + # - must not be used for changing CreatedInstances during publishing! + # QUESTION + # - pop the key after first collector using it would be safest option? + self._publish_context.data["create_context"] = create_context + publish_plugins = create_context.publish_plugins + self._publish_plugins = publish_plugins + self._publish_plugins_proxy = PublishPluginsProxy( + publish_plugins + ) + + self._publish_report.reset( + create_context.creator_discover_result, + create_context.convertor_discover_result, + create_context.publish_discover_result, + ) + for plugin in create_context.publish_plugins_mismatch_targets: + self._publish_report.set_plugin_skipped(plugin.id) + self._publish_validation_errors.reset(self._publish_plugins_proxy) + + self._set_max_progress(len(publish_plugins)) + + self._emit_event("publish.reset.finished") + + def set_publish_up_validation(self, value: bool): + self._publish_up_validation = value + + def start_publish(self, wait: Optional[bool] = True): + """Run publishing. + + Make sure all changes are saved before method is called (Call + 'save_changes' and check output). + """ + if self._publish_up_validation and self._publish_has_validated: + return + + self._start_publish() + + if not wait: + return + + while self.is_running(): + func = self.get_next_process_func() + func() + + def get_next_process_func(self) -> partial: + # Validations of progress before using iterator + # - same conditions may be inside iterator but they may be used + # only in specific cases (e.g. when it happens for a first time) + + if ( + self._main_thread_iter is None + # There are validation errors and validation is passed + # - can't do any progree + or ( + self._publish_has_validated + and self._publish_has_validation_errors + ) + # Any unexpected error happened + # - everything should stop + or self._publish_has_crashed + ): + item = partial(self.stop_publish) + + # Everything is ok so try to get new processing item + else: + item = next(self._main_thread_iter) + + return item + + def stop_publish(self): + if self._publish_is_running: + self._stop_publish() + + def is_running(self) -> bool: + return self._publish_is_running + + def is_crashed(self) -> bool: + return self._publish_has_crashed + + def has_started(self) -> bool: + return self._publish_has_started + + def has_finished(self) -> bool: + return self._publish_has_finished + + def has_validated(self) -> bool: + return self._publish_has_validated + + def has_validation_errors(self) -> bool: + return self._publish_has_validation_errors + + def publish_can_continue(self) -> bool: + return ( + not self._publish_has_crashed + and not self._publish_has_validation_errors + and not self._publish_has_finished + ) + + def get_progress(self) -> int: + return self._publish_progress + + def get_max_progress(self) -> int: + return self._publish_max_progress + + def get_publish_report(self) -> Dict[str, Any]: + return self._publish_report.get_report( + self._publish_context + ) + + def get_validation_errors(self) -> PublishValidationErrorsReport: + return self._publish_validation_errors.create_report() + + def get_error_msg(self) -> Optional[str]: + return self._publish_error_msg + + def set_comment(self, comment: str): + # Ignore change of comment when publishing started + if self._publish_has_started: + return + self._publish_context.data["comment"] = comment + self._publish_comment_is_set = True + + def run_action(self, plugin_id: str, action_id: str): + # TODO handle result in UI + plugin = self._publish_plugins_proxy.get_plugin(plugin_id) + action = self._publish_plugins_proxy.get_action(plugin_id, action_id) + + result = pyblish.plugin.process( + plugin, self._publish_context, None, action.id + ) + exception = result.get("error") + if exception: + self._emit_event( + "publish.action.failed", + { + "title": "Action failed", + "message": "Action failed.", + "traceback": "".join( + traceback.format_exception( + type(exception), + exception, + exception.__traceback__ + ) + ), + "label": action.__name__, + "identifier": action.id + } + ) + + self._publish_report.add_action_result(action, result) + + self._controller.emit_card_message("Action finished.") + + def _emit_event(self, topic: str, data: Optional[Dict[str, Any]] = None): + self._controller.emit_event(topic, data, PUBLISH_EVENT_SOURCE) + + def _set_finished(self, value: bool): + if self._publish_has_finished != value: + self._publish_has_finished = value + self._emit_event( + "publish.finished.changed", + {"value": value} + ) + + def _set_is_running(self, value: bool): + if self._publish_is_running != value: + self._publish_is_running = value + self._emit_event( + "publish.is_running.changed", + {"value": value} + ) + + def _set_has_validated(self, value: bool): + if self._publish_has_validated != value: + self._publish_has_validated = value + self._emit_event( + "publish.has_validated.changed", + {"value": value} + ) + + def _set_is_crashed(self, value: bool): + if self._publish_has_crashed != value: + self._publish_has_crashed = value + self._emit_event( + "publish.has_crashed.changed", + {"value": value} + ) + + def _set_has_validation_errors(self, value: bool): + if self._publish_has_validation_errors != value: + self._publish_has_validation_errors = value + self._emit_event( + "publish.has_validation_errors.changed", + {"value": value} + ) + + def _set_max_progress(self, value: int): + if self._publish_max_progress != value: + self._publish_max_progress = value + self._emit_event( + "publish.max_progress.changed", + {"value": value} + ) + + def _set_progress(self, value: int): + if self._publish_progress != value: + self._publish_progress = value + self._emit_event( + "publish.progress.changed", + {"value": value} + ) + + def _set_publish_error_msg(self, value: Optional[str]): + if self._publish_error_msg != value: + self._publish_error_msg = value + self._emit_event( + "publish.publish_error.changed", + {"value": value} + ) + + def _start_publish(self): + """Start or continue in publishing.""" + if self._publish_is_running: + return + + self._set_is_running(True) + self._publish_has_started = True + + self._emit_event("publish.process.started") + + def _stop_publish(self): + """Stop or pause publishing.""" + self._set_is_running(False) + + self._emit_event("publish.process.stopped") + + def _publish_iterator(self) -> Iterable[partial]: + """Main logic center of publishing. + + Iterator returns `partial` objects with callbacks that should be + processed in main thread (threaded in future?). Cares about changing + states of currently processed publish plugin and instance. Also + change state of processed orders like validation order has passed etc. + + Also stops publishing, if should stop on validation. + """ + + for idx, plugin in enumerate(self._publish_plugins): + self._publish_progress = idx + + # Check if plugin is over validation order + if not self._publish_has_validated: + self._set_has_validated( + plugin.order >= self._validation_order + ) + + # Stop if plugin is over validation order and process + # should process up to validation. + if self._publish_up_validation and self._publish_has_validated: + yield partial(self.stop_publish) + + # Stop if validation is over and validation errors happened + if ( + self._publish_has_validated + and self.has_validation_errors() + ): + yield partial(self.stop_publish) + + # Add plugin to publish report + self._publish_report.add_plugin_iter( + plugin.id, self._publish_context) + + # WARNING This is hack fix for optional plugins + if not self._is_publish_plugin_active(plugin): + self._publish_report.set_plugin_skipped(plugin.id) + continue + + # Trigger callback that new plugin is going to be processed + plugin_label = plugin.__name__ + if hasattr(plugin, "label") and plugin.label: + plugin_label = plugin.label + self._emit_event( + "publish.process.plugin.changed", + {"plugin_label": plugin_label} + ) + + # Plugin is instance plugin + if plugin.__instanceEnabled__: + instances = pyblish.logic.instances_by_plugin( + self._publish_context, plugin + ) + if not instances: + self._publish_report.set_plugin_skipped(plugin.id) + continue + + for instance in instances: + if instance.data.get("publish") is False: + continue + + instance_label = ( + instance.data.get("label") + or instance.data["name"] + ) + self._emit_event( + "publish.process.instance.changed", + {"instance_label": instance_label} + ) + + yield partial( + self._process_and_continue, plugin, instance + ) + else: + families = collect_families_from_instances( + self._publish_context, only_active=True + ) + plugins = pyblish.logic.plugins_by_families( + [plugin], families + ) + if not plugins: + self._publish_report.set_plugin_skipped(plugin.id) + continue + + instance_label = ( + self._publish_context.data.get("label") + or self._publish_context.data.get("name") + or "Context" + ) + self._emit_event( + "publish.process.instance.changed", + {"instance_label": instance_label} + ) + yield partial( + self._process_and_continue, plugin, None + ) + + self._publish_report.set_plugin_passed(plugin.id) + + # Cleanup of publishing process + self._set_finished(True) + self._set_progress(self._publish_max_progress) + yield partial(self.stop_publish) + + def _process_and_continue( + self, + plugin: pyblish.api.Plugin, + instance: pyblish.api.Instance + ): + result = pyblish.plugin.process( + plugin, self._publish_context, instance + ) + + exception = result.get("error") + if exception: + has_validation_error = False + if ( + isinstance(exception, PublishValidationError) + and not self._publish_has_validated + ): + has_validation_error = True + self._add_validation_error(result) + + else: + if isinstance(exception, KnownPublishError): + msg = str(exception) + else: + msg = ( + "Something went wrong. Send report" + " to your supervisor or Ynput team." + ) + self._set_publish_error_msg(msg) + self._set_is_crashed(True) + + result["is_validation_error"] = has_validation_error + + self._publish_report.add_result(plugin.id, result) + + def _add_validation_error(self, result: Dict[str, Any]): + self._set_has_validation_errors(True) + self._publish_validation_errors.add_error( + result["plugin"], + result["error"], + result["instance"] + ) + + def _is_publish_plugin_active(self, plugin: pyblish.api.Plugin) -> bool: + """Decide if publish plugin is active. + + This is hack because 'active' is mis-used in mixin + 'OptionalPyblishPluginMixin' where 'active' is used for default value + of optional plugins. Because of that is 'active' state of plugin + which inherit from 'OptionalPyblishPluginMixin' ignored. That affects + headless publishing inside host, potentially remote publishing. + + We have to change that to match pyblish base, but we can do that + only when all hosts use Publisher because the change requires + change of settings schemas. + + Args: + plugin (pyblish.Plugin): Plugin which should be checked if is + active. + + Returns: + bool: Is plugin active. + """ + + if plugin.active: + return True + + if not plugin.optional: + return False + + if OptionalPyblishPluginMixin in inspect.getmro(plugin): + return True + return False diff --git a/client/ayon_core/tools/publisher/publish_report_viewer/model.py b/client/ayon_core/tools/publisher/publish_report_viewer/model.py index 9ed1bf555d..0bd2f9292a 100644 --- a/client/ayon_core/tools/publisher/publish_report_viewer/model.py +++ b/client/ayon_core/tools/publisher/publish_report_viewer/model.py @@ -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 diff --git a/client/ayon_core/tools/publisher/publish_report_viewer/widgets.py b/client/ayon_core/tools/publisher/publish_report_viewer/widgets.py index 544d45ce89..61a52533ba 100644 --- a/client/ayon_core/tools/publisher/publish_report_viewer/widgets.py +++ b/client/ayon_core/tools/publisher/publish_report_viewer/widgets.py @@ -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() diff --git a/client/ayon_core/tools/publisher/publish_report_viewer/window.py b/client/ayon_core/tools/publisher/publish_report_viewer/window.py index 6427b915a8..3ee986e6f7 100644 --- a/client/ayon_core/tools/publisher/publish_report_viewer/window.py +++ b/client/ayon_core/tools/publisher/publish_report_viewer/window.py @@ -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) diff --git a/client/ayon_core/tools/publisher/widgets/border_label_widget.py b/client/ayon_core/tools/publisher/widgets/border_label_widget.py index 324c70df34..244f9260ff 100644 --- a/client/ayon_core/tools/publisher/widgets/border_label_widget.py +++ b/client/ayon_core/tools/publisher/widgets/border_label_widget.py @@ -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): diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index 4e34f9b58c..d67252e302 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -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 diff --git a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py index 18df798bf0..faf2248181 100644 --- a/client/ayon_core/tools/publisher/widgets/create_context_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/create_context_widgets.py @@ -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 diff --git a/client/ayon_core/tools/publisher/widgets/create_widget.py b/client/ayon_core/tools/publisher/widgets/create_widget.py index 2e4ca34138..479a63ebc9 100644 --- a/client/ayon_core/tools/publisher/widgets/create_widget.py +++ b/client/ayon_core/tools/publisher/widgets/create_widget.py @@ -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() diff --git a/client/ayon_core/tools/publisher/widgets/folders_dialog.py b/client/ayon_core/tools/publisher/widgets/folders_dialog.py index 28bdce37b1..d2eb68310e 100644 --- a/client/ayon_core/tools/publisher/widgets/folders_dialog.py +++ b/client/ayon_core/tools/publisher/widgets/folders_dialog.py @@ -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.""" diff --git a/client/ayon_core/tools/publisher/widgets/help_widget.py b/client/ayon_core/tools/publisher/widgets/help_widget.py index 5d474613df..40f8b255dd 100644 --- a/client/ayon_core/tools/publisher/widgets/help_widget.py +++ b/client/ayon_core/tools/publisher/widgets/help_widget.py @@ -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) diff --git a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py index 71be0ab1a4..930d6bb88c 100644 --- a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py @@ -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 diff --git a/client/ayon_core/tools/publisher/widgets/overview_widget.py b/client/ayon_core/tools/publisher/widgets/overview_widget.py index cedf52ae01..52a45d0881 100644 --- a/client/ayon_core/tools/publisher/widgets/overview_widget.py +++ b/client/ayon_core/tools/publisher/widgets/overview_widget.py @@ -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.""" diff --git a/client/ayon_core/tools/publisher/widgets/precreate_widget.py b/client/ayon_core/tools/publisher/widgets/precreate_widget.py index ae0deb8410..5ad203d370 100644 --- a/client/ayon_core/tools/publisher/widgets/precreate_widget.py +++ b/client/ayon_core/tools/publisher/widgets/precreate_widget.py @@ -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) diff --git a/client/ayon_core/tools/publisher/widgets/publish_frame.py b/client/ayon_core/tools/publisher/widgets/publish_frame.py index ee65c69c19..6eaeb6daf2 100644 --- a/client/ayon_core/tools/publisher/widgets/publish_frame.py +++ b/client/ayon_core/tools/publisher/widgets/publish_frame.py @@ -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": diff --git a/client/ayon_core/tools/publisher/widgets/report_page.py b/client/ayon_core/tools/publisher/widgets/report_page.py index 7475b39f52..ecf1376ec0 100644 --- a/client/ayon_core/tools/publisher/widgets/report_page.py +++ b/client/ayon_core/tools/publisher/widgets/report_page.py @@ -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() diff --git a/client/ayon_core/tools/publisher/widgets/screenshot_widget.py b/client/ayon_core/tools/publisher/widgets/screenshot_widget.py index 37b958c1c7..08a0a790b7 100644 --- a/client/ayon_core/tools/publisher/widgets/screenshot_widget.py +++ b/client/ayon_core/tools/publisher/widgets/screenshot_widget.py @@ -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() diff --git a/client/ayon_core/tools/publisher/widgets/tabs_widget.py b/client/ayon_core/tools/publisher/widgets/tabs_widget.py index e484dc8681..cd2f927f8c 100644 --- a/client/ayon_core/tools/publisher/widgets/tabs_widget.py +++ b/client/ayon_core/tools/publisher/widgets/tabs_widget.py @@ -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) diff --git a/client/ayon_core/tools/publisher/widgets/tasks_model.py b/client/ayon_core/tools/publisher/widgets/tasks_model.py index 03fb95a310..16a4111f59 100644 --- a/client/ayon_core/tools/publisher/widgets/tasks_model.py +++ b/client/ayon_core/tools/publisher/widgets/tasks_model.py @@ -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 = {} diff --git a/client/ayon_core/tools/publisher/widgets/thumbnail_widget.py b/client/ayon_core/tools/publisher/widgets/thumbnail_widget.py index 07dc532534..261dcfb43d 100644 --- a/client/ayon_core/tools/publisher/widgets/thumbnail_widget.py +++ b/client/ayon_core/tools/publisher/widgets/thumbnail_widget.py @@ -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() diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index 12c03c7eeb..1f782ddc67 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -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() diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py index 1b13ced317..1218221420 100644 --- a/client/ayon_core/tools/publisher/window.py +++ b/client/ayon_core/tools/publisher/window.py @@ -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) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 3e0c361535..335df87b95 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -217,7 +217,9 @@ class InventoryModel(QtGui.QStandardItemModel): version_label = format_version(version_item.version) is_hero = version_item.version < 0 is_latest = version_item.is_latest - if not is_latest: + # TODO maybe use different colors for last approved and last + # version? Or don't care about color at all? + if not is_latest and not version_item.is_last_approved: version_color = self.OUTDATED_COLOR status_name = version_item.status diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index 95c5322343..c3881ea40d 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -3,7 +3,9 @@ import collections import ayon_api from ayon_api.graphql import GraphQlQuery + from ayon_core.host import ILoadHost +from ayon_core.tools.common_models.projects import StatusStates # --- Implementation that should be in ayon-python-api --- @@ -149,26 +151,35 @@ class RepresentationInfo: class VersionItem: - def __init__(self, version_id, product_id, version, status, is_latest): - self.version = version - self.version_id = version_id - self.product_id = product_id - self.version = version - self.status = status - self.is_latest = is_latest + def __init__( + self, + version_id: str, + product_id: str, + version: int, + status: str, + is_latest: bool, + is_last_approved: bool, + ): + self.version_id: str = version_id + self.product_id: str = product_id + self.version: int = version + self.status: str = status + self.is_latest: bool = is_latest + self.is_last_approved: bool = is_last_approved @property def is_hero(self): return self.version < 0 @classmethod - def from_entity(cls, version_entity, is_latest): + def from_entity(cls, version_entity, is_latest, is_last_approved): return cls( version_id=version_entity["id"], product_id=version_entity["productId"], version=version_entity["version"], status=version_entity["status"], is_latest=is_latest, + is_last_approved=is_last_approved, ) @@ -275,6 +286,11 @@ class ContainersModel: if product_id not in self._version_items_by_product_id } if missing_ids: + status_items_by_name = { + status_item.name: status_item + for status_item in self._controller.get_project_status_items() + } + def version_sorted(entity): return entity["version"] @@ -300,9 +316,21 @@ class ContainersModel: version_entities_by_product_id.items() ): last_version = abs(version_entities[-1]["version"]) + last_approved_id = None + for version_entity in version_entities: + status_item = status_items_by_name.get( + version_entity["status"] + ) + if status_item is None: + continue + if status_item.state == StatusStates.done: + last_approved_id = version_entity["id"] + version_items_by_id = { entity["id"]: VersionItem.from_entity( - entity, abs(entity["version"]) == last_version + entity, + abs(entity["version"]) == last_version, + entity["id"] == last_approved_id ) for entity in version_entities } diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index c8cc3299a2..22ba15fda8 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -233,19 +233,38 @@ class SceneInventoryView(QtWidgets.QTreeView): has_outdated = False has_loaded_hero_versions = False has_available_hero_version = False - for version_items_by_id in version_items_by_product_id.values(): + has_outdated_approved = False + last_version_by_product_id = {} + for product_id, version_items_by_id in ( + version_items_by_product_id.items() + ): + _has_outdated_approved = False + _last_approved_version_item = None for version_item in version_items_by_id.values(): if version_item.is_hero: has_available_hero_version = True + elif version_item.is_last_approved: + _last_approved_version_item = version_item + _has_outdated_approved = True + if version_item.version_id not in version_ids: continue + if version_item.is_hero: has_loaded_hero_versions = True - elif not version_item.is_latest: has_outdated = True + if ( + _has_outdated_approved + and _last_approved_version_item is not None + ): + last_version_by_product_id[product_id] = ( + _last_approved_version_item + ) + has_outdated_approved = True + switch_to_versioned = None if has_loaded_hero_versions: update_icon = qtawesome.icon( @@ -261,6 +280,42 @@ class SceneInventoryView(QtWidgets.QTreeView): lambda: self._on_switch_to_versioned(item_ids) ) + update_to_last_approved_action = None + approved_version_by_item_id = {} + if has_outdated_approved: + for container_item in container_items_by_id.values(): + repre_id = container_item.representation_id + repre_info = repre_info_by_id.get(repre_id) + if not repre_info or not repre_info.is_valid: + continue + version_item = last_version_by_product_id.get( + repre_info.product_id + ) + if ( + version_item is None + or version_item.version_id == repre_info.version_id + ): + continue + approved_version_by_item_id[container_item.item_id] = ( + version_item.version + ) + + if approved_version_by_item_id: + update_icon = qtawesome.icon( + "fa.angle-double-up", + color="#00f0b4" + ) + update_to_last_approved_action = QtWidgets.QAction( + update_icon, + "Update to last approved", + menu + ) + update_to_last_approved_action.triggered.connect( + lambda: self._update_containers_to_approved_versions( + approved_version_by_item_id + ) + ) + update_to_latest_action = None if has_outdated or has_loaded_hero_versions: update_icon = qtawesome.icon( @@ -299,7 +354,9 @@ class SceneInventoryView(QtWidgets.QTreeView): # set version set_version_action = None if active_repre_id is not None: - set_version_icon = qtawesome.icon("fa.hashtag", color=DEFAULT_COLOR) + set_version_icon = qtawesome.icon( + "fa.hashtag", color=DEFAULT_COLOR + ) set_version_action = QtWidgets.QAction( set_version_icon, "Set version", @@ -323,6 +380,9 @@ class SceneInventoryView(QtWidgets.QTreeView): if switch_to_versioned: menu.addAction(switch_to_versioned) + if update_to_last_approved_action: + menu.addAction(update_to_last_approved_action) + if update_to_latest_action: menu.addAction(update_to_latest_action) @@ -970,3 +1030,24 @@ class SceneInventoryView(QtWidgets.QTreeView): """ versions = [version for _ in range(len(item_ids))] self._update_containers(item_ids, versions) + + def _update_containers_to_approved_versions( + self, approved_version_by_item_id + ): + """Helper to update items to given version (or version per item) + + If at least one item is specified this will always try to refresh + the inventory even if errors occurred on any of the items. + + Arguments: + approved_version_by_item_id (Dict[str, int]): Version to set by + item id. + + """ + versions = [] + item_ids = [] + for item_id, version in approved_version_by_item_id.items(): + item_ids.append(item_id) + versions.append(version) + + self._update_containers(item_ids, versions) diff --git a/client/ayon_core/tools/utils/folders_widget.py b/client/ayon_core/tools/utils/folders_widget.py index bb75f3b6e5..965f4cc8a7 100644 --- a/client/ayon_core/tools/utils/folders_widget.py +++ b/client/ayon_core/tools/utils/folders_widget.py @@ -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) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index b37be1afe6..1b3d382f01 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -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": [], diff --git a/server/settings/tools.py b/server/settings/tools.py index 1cb070e2af..3ed12d3d0a 100644 --- a/server/settings/tools.py +++ b/server/settings/tools.py @@ -448,6 +448,17 @@ DEFAULT_TOOLS_VALUES = { "task_types": [], "tasks": [], "template": "SK_{folder[name]}{variant}" + }, + { + "product_types": [ + "hda" + ], + "hosts": [ + "houdini" + ], + "task_types": [], + "tasks": [], + "template": "{folder[name]}_{variant}" } ], "filter_creator_profiles": [] diff --git a/server_addon/aftereffects/LICENSE b/server_addon/aftereffects/LICENSE deleted file mode 100644 index d645695673..0000000000 --- a/server_addon/aftereffects/LICENSE +++ /dev/null @@ -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. diff --git a/server_addon/aftereffects/README.md b/server_addon/aftereffects/README.md deleted file mode 100644 index b2f34f3407..0000000000 --- a/server_addon/aftereffects/README.md +++ /dev/null @@ -1,4 +0,0 @@ -AfterEffects Addon -=============== - -Integration with Adobe AfterEffects. diff --git a/server_addon/aftereffects/client/ayon_aftereffects/__init__.py b/server_addon/aftereffects/client/ayon_aftereffects/__init__.py deleted file mode 100644 index e8de12e42e..0000000000 --- a/server_addon/aftereffects/client/ayon_aftereffects/__init__.py +++ /dev/null @@ -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", -) diff --git a/server_addon/aftereffects/client/ayon_aftereffects/addon.py b/server_addon/aftereffects/client/ayon_aftereffects/addon.py deleted file mode 100644 index f659eba1cb..0000000000 --- a/server_addon/aftereffects/client/ayon_aftereffects/addon.py +++ /dev/null @@ -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" - ) diff --git a/server_addon/aftereffects/client/ayon_aftereffects/api/README.md b/server_addon/aftereffects/client/ayon_aftereffects/api/README.md deleted file mode 100644 index ca7d5b9b13..0000000000 --- a/server_addon/aftereffects/client/ayon_aftereffects/api/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# AfterEffects Integration - -Requirements: This extension requires use of Javascript engine, which is -available since CC 16.0. -Please check your File>Project Settings>Expressions>Expressions Engine - -## Setup - -The After Effects integration requires two components to work; `extension` and `server`. - -### Extension - -To install the extension download [Extension Manager Command Line tool (ExManCmd)](https://github.com/Adobe-CEP/Getting-Started-guides/tree/master/Package%20Distribute%20Install#option-2---exmancmd). - -``` -ExManCmd /install {path to addon}/api/extension.zxp -``` -OR -download [Anastasiy’s Extension Manager](https://install.anastasiy.com/) - -`{path to addon}` will be most likely in your AppData (on Windows, in your user data folder in Linux and MacOS.) - -### Server - -The easiest way to get the server and After Effects launch is with: - -``` -python -c ^"import ayon_core.hosts.photoshop;ayon_aftereffects.launch(""c:\Program Files\Adobe\Adobe After Effects 2020\Support Files\AfterFX.exe"")^" -``` - -`avalon.aftereffects.launch` launches the application and server, and also closes the server when After Effects exists. - -## Usage - -The After Effects extension can be found under `Window > Extensions > AYON`. Once launched you should be presented with a panel like this: - -![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 diff --git a/server_addon/aftereffects/client/ayon_aftereffects/api/__init__.py b/server_addon/aftereffects/client/ayon_aftereffects/api/__init__.py deleted file mode 100644 index b1d83c5ad9..0000000000 --- a/server_addon/aftereffects/client/ayon_aftereffects/api/__init__.py +++ /dev/null @@ -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" -] diff --git a/server_addon/aftereffects/client/ayon_aftereffects/api/extension.zxp b/server_addon/aftereffects/client/ayon_aftereffects/api/extension.zxp deleted file mode 100644 index 104a5c9e99..0000000000 Binary files a/server_addon/aftereffects/client/ayon_aftereffects/api/extension.zxp and /dev/null differ diff --git a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/.debug b/server_addon/aftereffects/client/ayon_aftereffects/api/extension/.debug deleted file mode 100644 index 20a6713ab2..0000000000 --- a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/.debug +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/CSXS/manifest.xml b/server_addon/aftereffects/client/ayon_aftereffects/api/extension/CSXS/manifest.xml deleted file mode 100644 index cf6ba67f44..0000000000 --- a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/CSXS/manifest.xml +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ./index.html - ./jsx/hostscript.jsx - - - true - - - Panel - AYON - - - 200 - 100 - - - - - - ./icons/ayon_logo.png - ./icons/iconRollover.png - ./icons/iconDisabled.png - ./icons/iconDarkNormal.png - ./icons/iconDarkRollover.png - - - - - - diff --git a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/css/boilerplate.css b/server_addon/aftereffects/client/ayon_aftereffects/api/extension/css/boilerplate.css deleted file mode 100644 index d208999b8a..0000000000 --- a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/css/boilerplate.css +++ /dev/null @@ -1,327 +0,0 @@ -/* - * HTML5 ✰ Boilerplate - * - * What follows is the result of much research on cross-browser styling. - * Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal, - * Kroc Camen, and the H5BP dev community and team. - * - * Detailed information about this CSS: h5bp.com/css - * - * ==|== normalize ========================================================== - */ - - -/* ============================================================================= - HTML5 display definitions - ========================================================================== */ - -article, aside, details, figcaption, figure, footer, header, hgroup, nav, section { display: block; } -audio, canvas, video { display: inline-block; *display: inline; *zoom: 1; } -audio:not([controls]) { display: none; } -[hidden] { display: none; } - - -/* ============================================================================= - Base - ========================================================================== */ - -/* - * 1. Correct text resizing oddly in IE6/7 when body font-size is set using em units - * 2. Force vertical scrollbar in non-IE - * 3. Prevent iOS text size adjust on device orientation change, without disabling user zoom: h5bp.com/g - */ - -html { font-size: 100%; overflow-y: scroll; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } - -body { margin: 0; font-size: 100%; line-height: 1.231; } - -body, button, input, select, textarea { font-family: helvetica, arial,"lucida grande", verdana, "メイリオ", "MS Pゴシック", sans-serif; color: #222; } -/* - * Remove text-shadow in selection highlight: h5bp.com/i - * These selection declarations have to be separate - * Also: hot pink! (or customize the background color to match your design) - */ - -::selection { text-shadow: none; background-color: highlight; color: highlighttext; } - - -/* ============================================================================= - Links - ========================================================================== */ - -a { color: #00e; } -a:visited { color: #551a8b; } -a:hover { color: #06e; } -a:focus { outline: thin dotted; } - -/* Improve readability when focused and hovered in all browsers: h5bp.com/h */ -a:hover, a:active { outline: 0; } - - -/* ============================================================================= - Typography - ========================================================================== */ - -abbr[title] { border-bottom: 1px dotted; } - -b, strong { font-weight: bold; } - -blockquote { margin: 1em 40px; } - -dfn { font-style: italic; } - -hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; } - -ins { background: #ff9; color: #000; text-decoration: none; } - -mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; } - -/* Redeclare monospace font family: h5bp.com/j */ -pre, code, kbd, samp { font-family: monospace, serif; _font-family: 'courier new', monospace; font-size: 1em; } - -/* Improve readability of pre-formatted text in all browsers */ -pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; } - -q { quotes: none; } -q:before, q:after { content: ""; content: none; } - -small { font-size: 85%; } - -/* Position subscript and superscript content without affecting line-height: h5bp.com/k */ -sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } -sup { top: -0.5em; } -sub { bottom: -0.25em; } - - -/* ============================================================================= - Lists - ========================================================================== */ - -ul, ol { margin: 1em 0; padding: 0 0 0 40px; } -dd { margin: 0 0 0 40px; } -nav ul, nav ol { list-style: none; list-style-image: none; margin: 0; padding: 0; } - - -/* ============================================================================= - Embedded content - ========================================================================== */ - -/* - * 1. Improve image quality when scaled in IE7: h5bp.com/d - * 2. Remove the gap between images and borders on image containers: h5bp.com/e - */ - -img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; } - -/* - * Correct overflow not hidden in IE9 - */ - -svg:not(:root) { overflow: hidden; } - - -/* ============================================================================= - Figures - ========================================================================== */ - -figure { margin: 0; } - - -/* ============================================================================= - Forms - ========================================================================== */ - -form { margin: 0; } -fieldset { border: 0; margin: 0; padding: 0; } - -/* Indicate that 'label' will shift focus to the associated form element */ -label { cursor: pointer; } - -/* - * 1. Correct color not inheriting in IE6/7/8/9 - * 2. Correct alignment displayed oddly in IE6/7 - */ - -legend { border: 0; *margin-left: -7px; padding: 0; } - -/* - * 1. Correct font-size not inheriting in all browsers - * 2. Remove margins in FF3/4 S5 Chrome - * 3. Define consistent vertical alignment display in all browsers - */ - -button, input, select, textarea { font-size: 100%; margin: 0; vertical-align: baseline; *vertical-align: middle; } - -/* - * 1. Define line-height as normal to match FF3/4 (set using !important in the UA stylesheet) - */ - -button, input { line-height: normal; } - -/* - * 1. Display hand cursor for clickable form elements - * 2. Allow styling of clickable form elements in iOS - * 3. Correct inner spacing displayed oddly in IE7 (doesn't effect IE6) - */ - -button, input[type="button"], input[type="reset"], input[type="submit"] { cursor: pointer; -webkit-appearance: button; *overflow: visible; } - -/* - * Consistent box sizing and appearance - */ - -input[type="checkbox"], input[type="radio"] { box-sizing: border-box; padding: 0; } -input[type="search"] { -webkit-appearance: textfield; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; box-sizing: content-box; } -input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } - -/* - * Remove inner padding and border in FF3/4: h5bp.com/l - */ - -button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; } - -/* - * 1. Remove default vertical scrollbar in IE6/7/8/9 - * 2. Allow only vertical resizing - */ - -textarea { overflow: auto; vertical-align: top; resize: vertical; } - -/* Colors for form validity */ -input:valid, textarea:valid { } -input:invalid, textarea:invalid { background-color: #f0dddd; } - - -/* ============================================================================= - Tables - ========================================================================== */ - -table { border-collapse: collapse; border-spacing: 0; } -td { vertical-align: top; } - - -/* ==|== primary styles ===================================================== - Author: - ========================================================================== */ - -/* ==|== media queries ====================================================== - PLACEHOLDER Media Queries for Responsive Design. - These override the primary ('mobile first') styles - Modify as content requires. - ========================================================================== */ - -@media only screen and (min-width: 480px) { - /* Style adjustments for viewports 480px and over go here */ - -} - -@media only screen and (min-width: 768px) { - /* Style adjustments for viewports 768px and over go here */ - -} - - - -/* ==|== non-semantic helper classes ======================================== - Please define your styles before this section. - ========================================================================== */ - -/* For image replacement */ -.ir { display: block; border: 0; text-indent: -999em; overflow: hidden; background-color: transparent; background-repeat: no-repeat; text-align: left; direction: ltr; } -.ir br { display: none; } - -/* Hide from both screenreaders and browsers: h5bp.com/u */ -.hidden { display: none !important; visibility: hidden; } - -/* Hide only visually, but have it available for screenreaders: h5bp.com/v */ -.visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; } - -/* Extends the .visuallyhidden class to allow the element to be focusable when navigated to via the keyboard: h5bp.com/p */ -.visuallyhidden.focusable:active, .visuallyhidden.focusable:focus { clip: auto; height: auto; margin: 0; overflow: visible; position: static; width: auto; } - -/* Hide visually and from screenreaders, but maintain layout */ -.invisible { visibility: hidden; } - -/* Contain floats: h5bp.com/q */ -.clearfix:before, .clearfix:after { content: ""; display: table; } -.clearfix:after { clear: both; } -.clearfix { *zoom: 1; } - - - -/* ==|== print styles ======================================================= - Print styles. - Inlined to avoid required HTTP connection: h5bp.com/r - ========================================================================== */ - -@media print { - * { background: transparent !important; color: black !important; box-shadow:none !important; text-shadow: none !important; filter:none !important; -ms-filter: none !important; } /* Black prints faster: h5bp.com/s */ - a, a:visited { text-decoration: underline; } - a[href]:after { content: " (" attr(href) ")"; } - abbr[title]:after { content: " (" attr(title) ")"; } - .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } /* Don't show links for images, or javascript/internal links */ - pre, blockquote { border: 1px solid #999; page-break-inside: avoid; } - table { display: table-header-group; } /* h5bp.com/t */ - tr, img { page-break-inside: avoid; } - img { max-width: 100% !important; } - @page { margin: 0.5cm; } - p, h2, h3 { orphans: 3; widows: 3; } - h2, h3 { page-break-after: avoid; } -} - -/* reflow reset for -webkit-margin-before: 1em */ -p { margin: 0; } - -html { - overflow-y: auto; - background-color: transparent; - height: 100%; -} - -body { - background: #fff; - font: normal 100%; - position: relative; - height: 100%; -} - -body, div, img, p, button, input, select, textarea { - box-sizing: border-box; -} - -.image { - display: block; -} - -input { - cursor: default; - display: block; -} - -input[type=button] { - background-color: #e5e9e8; - border: 1px solid #9daca9; - border-radius: 4px; - box-shadow: inset 0 1px #fff; - font: inherit; - letter-spacing: inherit; - text-indent: inherit; - color: inherit; -} - -input[type=button]:hover { - background-color: #eff1f1; -} - -input[type=button]:active { - background-color: #d2d6d6; - border: 1px solid #9daca9; - box-shadow: inset 0 1px rgba(0,0,0,0.1); -} - -/* Reset anchor styles to an unstyled default to be in parity with design surface. It - is presumed that most link styles in real-world designs are custom (non-default). */ -a, a:visited, a:hover, a:active { - color: inherit; - text-decoration: inherit; -} \ No newline at end of file diff --git a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/css/styles.css b/server_addon/aftereffects/client/ayon_aftereffects/api/extension/css/styles.css deleted file mode 100644 index c9cf2b93ac..0000000000 --- a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/css/styles.css +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/css/topcoat-desktop-dark.min.css b/server_addon/aftereffects/client/ayon_aftereffects/api/extension/css/topcoat-desktop-dark.min.css deleted file mode 100644 index 6b479def43..0000000000 --- a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/css/topcoat-desktop-dark.min.css +++ /dev/null @@ -1 +0,0 @@ -.button-bar{display:table;table-layout:fixed;white-space:nowrap;margin:0;padding:0}.button-bar__item{display:table-cell;width:auto;border-radius:0}.button-bar__item>input{position:absolute;overflow:hidden;padding:0;border:0;opacity:.001;z-index:1;vertical-align:top;outline:0}.button-bar__button{border-radius:inherit}.button-bar__item:disabled{opacity:.3;cursor:default;pointer-events:none}.button,.topcoat-button,.topcoat-button--quiet,.topcoat-button--large,.topcoat-button--large--quiet,.topcoat-button--cta,.topcoat-button--large--cta,.topcoat-button-bar__button,.topcoat-button-bar__button--large{position:relative;display:inline-block;vertical-align:top;-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;padding:0;margin:0;font:inherit;color:inherit;background:transparent;border:0;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;text-decoration:none}.button--quiet{background:transparent;border:1px solid transparent;box-shadow:none}.button--disabled,.topcoat-button:disabled,.topcoat-button--quiet:disabled,.topcoat-button--large:disabled,.topcoat-button--large--quiet:disabled,.topcoat-button--cta:disabled,.topcoat-button--large--cta:disabled,.topcoat-button-bar__button:disabled,.topcoat-button-bar__button--large:disabled{opacity:.3;cursor:default;pointer-events:none}.topcoat-button,.topcoat-button--quiet,.topcoat-button--large,.topcoat-button--large--quiet,.topcoat-button--cta,.topcoat-button--large--cta,.topcoat-button-bar__button,.topcoat-button-bar__button--large{padding:0 .563rem;font-size:12px;line-height:1.313rem;letter-spacing:0;color:#c6c8c8;text-shadow:0 -1px rgba(0,0,0,.69);vertical-align:top;background-color:#595b5b;box-shadow:inset 0 1px #737373;border:1px solid #333434;border-radius:4px}.topcoat-button:hover,.topcoat-button--quiet:hover,.topcoat-button--large:hover,.topcoat-button--large--quiet:hover,.topcoat-button-bar__button:hover,.topcoat-button-bar__button--large:hover{background-color:#626465}.topcoat-button:focus,.topcoat-button--quiet:focus,.topcoat-button--quiet:hover:focus,.topcoat-button--large:focus,.topcoat-button--large--quiet:focus,.topcoat-button--large--quiet:hover:focus,.topcoat-button--cta:focus,.topcoat-button--large--cta:focus,.topcoat-button-bar__button:focus,.topcoat-button-bar__button--large:focus{border:1px solid #0036ff;box-shadow:inset 0 1px rgba(255,255,255,.36),0 0 0 2px #6fb5f1;outline:0}.topcoat-button:active,.topcoat-button--large:active,.topcoat-button-bar__button:active,.topcoat-button-bar__button--large:active,:checked+.topcoat-button-bar__button{border:1px solid #333434;background-color:#3f4041;box-shadow:inset 0 1px rgba(0,0,0,.05)}.topcoat-button--quiet{background:transparent;border:1px solid transparent;box-shadow:none}.topcoat-button--quiet:hover,.topcoat-button--large--quiet:hover{text-shadow:0 -1px rgba(0,0,0,.69);border:1px solid #333434;box-shadow:inset 0 1px #737373}.topcoat-button--quiet:active,.topcoat-button--quiet:focus:active,.topcoat-button--large--quiet:active,.topcoat-button--large--quiet:focus:active{color:#c6c8c8;text-shadow:0 -1px rgba(0,0,0,.69);background-color:#3f4041;border:1px solid #333434;box-shadow:inset 0 1px rgba(0,0,0,.05)}.topcoat-button--large,.topcoat-button--large--quiet,.topcoat-button-bar__button--large{font-size:.875rem;font-weight:600;line-height:1.688rem;padding:0 .875rem}.topcoat-button--large--quiet{background:transparent;border:1px solid transparent;box-shadow:none}.topcoat-button--cta,.topcoat-button--large--cta{border:1px solid #134f7f;background-color:#288edf;box-shadow:inset 0 1px rgba(255,255,255,.36);color:#fff;font-weight:500;text-shadow:0 -1px rgba(0,0,0,.36)}.topcoat-button--cta:hover,.topcoat-button--large--cta:hover{background-color:#4ca1e4}.topcoat-button--cta:active,.topcoat-button--large--cta:active{background-color:#1e7dc8;box-shadow:inset 0 1px rgba(0,0,0,.12)}.topcoat-button--large--cta{font-size:.875rem;font-weight:600;line-height:1.688rem;padding:0 .875rem}.button-bar,.topcoat-button-bar{display:table;table-layout:fixed;white-space:nowrap;margin:0;padding:0}.button-bar__item,.topcoat-button-bar__item{display:table-cell;width:auto;border-radius:0}.button-bar__item>input,.topcoat-button-bar__item>input{position:absolute;overflow:hidden;padding:0;border:0;opacity:.001;z-index:1;vertical-align:top;outline:0}.button-bar__button{border-radius:inherit}.button-bar__item:disabled{opacity:.3;cursor:default;pointer-events:none}.topcoat-button-bar>.topcoat-button-bar__item:first-child{border-top-left-radius:4px;border-bottom-left-radius:4px}.topcoat-button-bar>.topcoat-button-bar__item:last-child{border-top-right-radius:4px;border-bottom-right-radius:4px}.topcoat-button-bar__item:first-child>.topcoat-button-bar__button,.topcoat-button-bar__item:first-child>.topcoat-button-bar__button--large{border-right:0}.topcoat-button-bar__item:last-child>.topcoat-button-bar__button,.topcoat-button-bar__item:last-child>.topcoat-button-bar__button--large{border-left:0}.topcoat-button-bar__button{border-radius:inherit}.topcoat-button-bar__button:focus,.topcoat-button-bar__button--large:focus{z-index:1}.topcoat-button-bar__button--large{border-radius:inherit}.button{position:relative;display:inline-block;vertical-align:top;-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;padding:0;margin:0;font:inherit;color:inherit;background:transparent;border:0;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;text-decoration:none}.button--quiet{background:transparent;border:1px solid transparent;box-shadow:none}.button--disabled{opacity:.3;cursor:default;pointer-events:none}.button,.topcoat-button,.topcoat-button--quiet,.topcoat-button--large,.topcoat-button--large--quiet,.topcoat-button--cta,.topcoat-button--large--cta{position:relative;display:inline-block;vertical-align:top;-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;padding:0;margin:0;font:inherit;color:inherit;background:transparent;border:0;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;text-decoration:none}.button--quiet{background:transparent;border:1px solid transparent;box-shadow:none}.button--disabled,.topcoat-button:disabled,.topcoat-button--quiet:disabled,.topcoat-button--large:disabled,.topcoat-button--large--quiet:disabled,.topcoat-button--cta:disabled,.topcoat-button--large--cta:disabled{opacity:.3;cursor:default;pointer-events:none}.topcoat-button,.topcoat-button--quiet,.topcoat-button--large,.topcoat-button--large--quiet,.topcoat-button--cta,.topcoat-button--large--cta{padding:0 .563rem;font-size:12px;line-height:1.313rem;letter-spacing:0;color:#c6c8c8;text-shadow:0 -1px rgba(0,0,0,.69);vertical-align:top;background-color:#595b5b;box-shadow:inset 0 1px #737373;border:1px solid #333434;border-radius:4px}.topcoat-button:hover,.topcoat-button--quiet:hover,.topcoat-button--large:hover,.topcoat-button--large--quiet:hover{background-color:#626465}.topcoat-button:focus,.topcoat-button--quiet:focus,.topcoat-button--quiet:hover:focus,.topcoat-button--large:focus,.topcoat-button--large--quiet:focus,.topcoat-button--large--quiet:hover:focus,.topcoat-button--cta:focus,.topcoat-button--large--cta:focus{border:1px solid #0036ff;box-shadow:inset 0 1px rgba(255,255,255,.36),0 0 0 2px #6fb5f1;outline:0}.topcoat-button:active,.topcoat-button--large:active{border:1px solid #333434;background-color:#3f4041;box-shadow:inset 0 1px rgba(0,0,0,.05)}.topcoat-button--quiet{background:transparent;border:1px solid transparent;box-shadow:none}.topcoat-button--quiet:hover,.topcoat-button--large--quiet:hover{text-shadow:0 -1px rgba(0,0,0,.69);border:1px solid #333434;box-shadow:inset 0 1px #737373}.topcoat-button--quiet:active,.topcoat-button--quiet:focus:active,.topcoat-button--large--quiet:active,.topcoat-button--large--quiet:focus:active{color:#c6c8c8;text-shadow:0 -1px rgba(0,0,0,.69);background-color:#3f4041;border:1px solid #333434;box-shadow:inset 0 1px rgba(0,0,0,.05)}.topcoat-button--large,.topcoat-button--large--quiet{font-size:.875rem;font-weight:600;line-height:1.688rem;padding:0 .875rem}.topcoat-button--large--quiet{background:transparent;border:1px solid transparent;box-shadow:none}.topcoat-button--cta,.topcoat-button--large--cta{border:1px solid #134f7f;background-color:#288edf;box-shadow:inset 0 1px rgba(255,255,255,.36);color:#fff;font-weight:500;text-shadow:0 -1px rgba(0,0,0,.36)}.topcoat-button--cta:hover,.topcoat-button--large--cta:hover{background-color:#4ca1e4}.topcoat-button--cta:active,.topcoat-button--large--cta:active{background-color:#1e7dc8;box-shadow:inset 0 1px rgba(0,0,0,.12)}.topcoat-button--large--cta{font-size:.875rem;font-weight:600;line-height:1.688rem;padding:0 .875rem}input[type=checkbox]{position:absolute;overflow:hidden;padding:0;border:0;opacity:.001;z-index:1;vertical-align:top;outline:0}.checkbox{-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;position:relative;display:inline-block;vertical-align:top;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.checkbox__label{position:relative;display:inline-block;vertical-align:top;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.checkbox--disabled{opacity:.3;cursor:default;pointer-events:none}.checkbox:before,.checkbox:after{content:'';position:absolute}.checkbox:before{-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box}input[type=checkbox]{position:absolute;overflow:hidden;padding:0;border:0;opacity:.001;z-index:1;vertical-align:top;outline:0}.checkbox,.topcoat-checkbox__checkmark{-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;position:relative;display:inline-block;vertical-align:top;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.checkbox__label,.topcoat-checkbox{position:relative;display:inline-block;vertical-align:top;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.checkbox--disabled,input[type=checkbox]:disabled+.topcoat-checkbox__checkmark{opacity:.3;cursor:default;pointer-events:none}.checkbox:before,.checkbox:after,.topcoat-checkbox__checkmark:before,.topcoat-checkbox__checkmark:after{content:'';position:absolute}.checkbox:before,.topcoat-checkbox__checkmark:before{-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box}.topcoat-checkbox__checkmark{height:1rem}input[type=checkbox]{height:1rem;width:1rem;margin-top:0;margin-right:-1rem;margin-bottom:-1rem;margin-left:0}input[type=checkbox]:checked+.topcoat-checkbox__checkmark:after{opacity:1}.topcoat-checkbox{line-height:1rem}.topcoat-checkbox__checkmark:before{width:1rem;height:1rem;background:#595b5b;border:1px solid #333434;border-radius:3px;box-shadow:inset 0 1px #737373}.topcoat-checkbox__checkmark{width:1rem;height:1rem}.topcoat-checkbox__checkmark:after{top:2px;left:1px;opacity:0;width:14px;height:4px;background:transparent;border:7px solid #c6c8c8;border-width:3px;border-top:0;border-right:0;border-radius:1px;-webkit-transform:rotate(-50deg);-ms-transform:rotate(-50deg);transform:rotate(-50deg)}input[type=checkbox]:focus+.topcoat-checkbox__checkmark:before{border:1px solid #0036ff;box-shadow:inset 0 1px rgba(255,255,255,.36),0 0 0 2px #6fb5f1}input[type=checkbox]:active+.topcoat-checkbox__checkmark:before{border:1px solid #333434;background-color:#3f4041;box-shadow:inset 0 1px rgba(0,0,0,.05)}input[type=checkbox]:disabled:active+.topcoat-checkbox__checkmark:before{border:1px solid #333434;background:#595b5b;box-shadow:inset 0 1px #737373}.button,.topcoat-icon-button,.topcoat-icon-button--quiet,.topcoat-icon-button--large,.topcoat-icon-button--large--quiet{position:relative;display:inline-block;vertical-align:top;-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;padding:0;margin:0;font:inherit;color:inherit;background:transparent;border:0;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;text-decoration:none}.button--quiet{background:transparent;border:1px solid transparent;box-shadow:none}.button--disabled,.topcoat-icon-button:disabled,.topcoat-icon-button--quiet:disabled,.topcoat-icon-button--large:disabled,.topcoat-icon-button--large--quiet:disabled{opacity:.3;cursor:default;pointer-events:none}.topcoat-icon-button,.topcoat-icon-button--quiet,.topcoat-icon-button--large,.topcoat-icon-button--large--quiet{padding:0 .25rem;line-height:1.313rem;letter-spacing:0;color:#c6c8c8;text-shadow:0 -1px rgba(0,0,0,.69);vertical-align:baseline;background-color:#595b5b;box-shadow:inset 0 1px #737373;border:1px solid #333434;border-radius:4px}.topcoat-icon-button:hover,.topcoat-icon-button--quiet:hover,.topcoat-icon-button--large:hover,.topcoat-icon-button--large--quiet:hover{background-color:#626465}.topcoat-icon-button:focus,.topcoat-icon-button--quiet:focus,.topcoat-icon-button--quiet:hover:focus,.topcoat-icon-button--large:focus,.topcoat-icon-button--large--quiet:focus,.topcoat-icon-button--large--quiet:hover:focus{border:1px solid #0036ff;box-shadow:inset 0 1px rgba(255,255,255,.36),0 0 0 2px #6fb5f1;outline:0}.topcoat-icon-button:active,.topcoat-icon-button--large:active{border:1px solid #333434;background-color:#3f4041;box-shadow:inset 0 1px rgba(0,0,0,.05)}.topcoat-icon-button--quiet{background:transparent;border:1px solid transparent;box-shadow:none}.topcoat-icon-button--quiet:hover,.topcoat-icon-button--large--quiet:hover{text-shadow:0 -1px rgba(0,0,0,.69);border:1px solid #333434;box-shadow:inset 0 1px #737373}.topcoat-icon-button--quiet:active,.topcoat-icon-button--quiet:focus:active,.topcoat-icon-button--large--quiet:active,.topcoat-icon-button--large--quiet:focus:active{color:#c6c8c8;text-shadow:0 -1px rgba(0,0,0,.69);background-color:#3f4041;border:1px solid #333434;box-shadow:inset 0 1px rgba(0,0,0,.05)}.topcoat-icon-button--large,.topcoat-icon-button--large--quiet{width:1.688rem;height:1.688rem;line-height:1.688rem}.topcoat-icon-button--large--quiet{background:transparent;border:1px solid transparent;box-shadow:none}.topcoat-icon,.topcoat-icon--large{position:relative;display:inline-block;vertical-align:top;overflow:hidden;width:.81406rem;height:.81406rem;vertical-align:middle;top:-1px}.topcoat-icon--large{width:1.06344rem;height:1.06344rem;top:-2px}.input{padding:0;margin:0;font:inherit;color:inherit;background:transparent;border:0;-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;vertical-align:top;outline:0}.input:disabled{opacity:.3;cursor:default;pointer-events:none}.list{padding:0;margin:0;font:inherit;color:inherit;background:transparent;border:0;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:auto;-webkit-overflow-scrolling:touch}.list__header{margin:0}.list__container{padding:0;margin:0;list-style-type:none}.list__item{margin:0;padding:0}.navigation-bar{-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;white-space:nowrap;overflow:hidden;word-spacing:0;padding:0;margin:0;font:inherit;color:inherit;background:transparent;border:0;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.navigation-bar__item{-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;position:relative;display:inline-block;vertical-align:top;padding:0;margin:0;font:inherit;color:inherit;background:transparent;border:0}.navigation-bar__title{padding:0;margin:0;font:inherit;color:inherit;background:transparent;border:0;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.notification{position:relative;display:inline-block;vertical-align:top;-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;padding:0;margin:0;font:inherit;color:inherit;background:transparent;border:0;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;text-decoration:none}.notification,.topcoat-notification{position:relative;display:inline-block;vertical-align:top;-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;padding:0;margin:0;font:inherit;color:inherit;background:transparent;border:0;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;text-decoration:none}.topcoat-notification{padding:.15em .5em .2em;border-radius:2px;background-color:#ec514e;color:#fff}input[type=radio]{position:absolute;overflow:hidden;padding:0;border:0;opacity:.001;z-index:1;vertical-align:top;outline:0}.radio-button{-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;position:relative;display:inline-block;vertical-align:top;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.radio-button__label{position:relative;display:inline-block;vertical-align:top;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.radio-button:before,.radio-button:after{content:'';position:absolute;border-radius:100%}.radio-button:after{top:50%;left:50%;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.radio-button:before{-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box}.radio-button--disabled{opacity:.3;cursor:default;pointer-events:none}input[type=radio]{position:absolute;overflow:hidden;padding:0;border:0;opacity:.001;z-index:1;vertical-align:top;outline:0}.radio-button,.topcoat-radio-button__checkmark{-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;position:relative;display:inline-block;vertical-align:top;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.radio-button__label,.topcoat-radio-button{position:relative;display:inline-block;vertical-align:top;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.radio-button:before,.radio-button:after,.topcoat-radio-button__checkmark:before,.topcoat-radio-button__checkmark:after{content:'';position:absolute;border-radius:100%}.radio-button:after,.topcoat-radio-button__checkmark:after{top:50%;left:50%;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.radio-button:before,.topcoat-radio-button__checkmark:before{-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box}.radio-button--disabled,input[type=radio]:disabled+.topcoat-radio-button__checkmark{opacity:.3;cursor:default;pointer-events:none}input[type=radio]{height:1.063rem;width:1.063rem;margin-top:0;margin-right:-1.063rem;margin-bottom:-1.063rem;margin-left:0}input[type=radio]:checked+.topcoat-radio-button__checkmark:after{opacity:1}.topcoat-radio-button{color:#c6c8c8;line-height:1.063rem}.topcoat-radio-button__checkmark:before{width:1.063rem;height:1.063rem;background:#595b5b;border:1px solid #333434;box-shadow:inset 0 1px #737373}.topcoat-radio-button__checkmark{position:relative;width:1.063rem;height:1.063rem}.topcoat-radio-button__checkmark:after{opacity:0;width:.313rem;height:.313rem;background:#c6c8c8;border:1px solid rgba(0,0,0,.05);box-shadow:0 1px rgba(255,255,255,.1);-webkit-transform:none;-ms-transform:none;transform:none;top:.313rem;left:.313rem}input[type=radio]:focus+.topcoat-radio-button__checkmark:before{border:1px solid #0036ff;box-shadow:inset 0 1px rgba(255,255,255,.36),0 0 0 2px #6fb5f1}input[type=radio]:active+.topcoat-radio-button__checkmark:before{border:1px solid #333434;background-color:#3f4041;box-shadow:inset 0 1px rgba(0,0,0,.05)}input[type=radio]:disabled:active+.topcoat-radio-button__checkmark:before{border:1px solid #333434;background:#595b5b;box-shadow:inset 0 1px #737373}.range{padding:0;margin:0;font:inherit;color:inherit;background:transparent;border:0;-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;vertical-align:top;outline:0;-webkit-appearance:none}.range__thumb{cursor:pointer}.range__thumb--webkit{cursor:pointer;-webkit-appearance:none}.range:disabled{opacity:.3;cursor:default;pointer-events:none}.range,.topcoat-range{padding:0;margin:0;font:inherit;color:inherit;background:transparent;border:0;-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;vertical-align:top;outline:0;-webkit-appearance:none}.range__thumb,.topcoat-range::-moz-range-thumb{cursor:pointer}.range__thumb--webkit,.topcoat-range::-webkit-slider-thumb{cursor:pointer;-webkit-appearance:none}.range:disabled,.topcoat-range:disabled{opacity:.3;cursor:default;pointer-events:none}.topcoat-range{border-radius:4px;border:1px solid #333434;background-color:#454646;height:.5rem;border-radius:15px}.topcoat-range::-moz-range-track{border-radius:4px;border:1px solid #333434;background-color:#454646;height:.5rem;border-radius:15px}.topcoat-range::-webkit-slider-thumb{height:1.313rem;width:.75rem;background-color:#595b5b;border:1px solid #333434;border-radius:4px;box-shadow:inset 0 1px #737373}.topcoat-range::-moz-range-thumb{height:1.313rem;width:.75rem;background-color:#595b5b;border:1px solid #333434;border-radius:4px;box-shadow:inset 0 1px #737373}.topcoat-range:focus::-webkit-slider-thumb{border:1px solid #0036ff;box-shadow:inset 0 1px rgba(255,255,255,.36),0 0 0 2px #6fb5f1}.topcoat-range:focus::-moz-range-thumb{border:1px solid #0036ff;box-shadow:inset 0 1px rgba(255,255,255,.36),0 0 0 2px #6fb5f1}.topcoat-range:active::-webkit-slider-thumb{border:1px solid #333434;box-shadow:inset 0 1px #737373}.topcoat-range:active::-moz-range-thumb{border:1px solid #333434;box-shadow:inset 0 1px #737373}.search-input{padding:0;margin:0;font:inherit;color:inherit;background:transparent;border:0;-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;vertical-align:top;outline:0;-webkit-appearance:none}input[type=search]::-webkit-search-cancel-button{-webkit-appearance:none}.search-input:disabled{opacity:.3;cursor:default;pointer-events:none}.search-input,.topcoat-search-input,.topcoat-search-input--large{padding:0;margin:0;font:inherit;color:inherit;background:transparent;border:0;-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;vertical-align:top;outline:0;-webkit-appearance:none}input[type=search]::-webkit-search-cancel-button{-webkit-appearance:none}.search-input:disabled,.topcoat-search-input:disabled,.topcoat-search-input--large:disabled{opacity:.3;cursor:default;pointer-events:none}.topcoat-search-input,.topcoat-search-input--large{line-height:1.313rem;height:1.313rem;font-size:12px;border:1px solid #333434;background-color:#454646;box-shadow:inset 0 1px 0 rgba(0,0,0,.23);color:#c6c8c8;padding:0 0 0 1.3rem;border-radius:15px;background-image:url(../img/search.svg);background-position:1rem center;background-repeat:no-repeat;background-size:12px}.topcoat-search-input:focus,.topcoat-search-input--large:focus{background-color:#595b5b;color:#fff;border:1px solid #0036ff;box-shadow:inset 0 1px 0 rgba(0,0,0,.23),0 0 0 2px #6fb5f1}.topcoat-search-input::-webkit-search-cancel-button,.topcoat-search-input::-webkit-search-decoration,.topcoat-search-input--large::-webkit-search-cancel-button,.topcoat-search-input--large::-webkit-search-decoration{margin-right:5px}.topcoat-search-input:focus::-webkit-input-placeholder,.topcoat-search-input:focus::-webkit-input-placeholder{color:#c6c8c8}.topcoat-search-input:disabled::-webkit-input-placeholder{color:#fff}.topcoat-search-input:disabled::-moz-placeholder{color:#fff}.topcoat-search-input:disabled:-ms-input-placeholder{color:#fff}.topcoat-search-input--large{line-height:1.688rem;height:1.688rem;font-size:.875rem;font-weight:400;padding:0 0 0 1.8rem;border-radius:25px;background-position:1.2rem center;background-size:.875rem}.topcoat-search-input--large:disabled{color:#fff}.topcoat-search-input--large:disabled::-webkit-input-placeholder{color:#fff}.topcoat-search-input--large:disabled::-moz-placeholder{color:#fff}.topcoat-search-input--large:disabled:-ms-input-placeholder{color:#fff}.switch{position:relative;display:inline-block;vertical-align:top;-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box}.switch__input{position:absolute;overflow:hidden;padding:0;border:0;opacity:.001;z-index:1;vertical-align:top;outline:0}.switch__toggle{position:relative;display:inline-block;vertical-align:top;-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;padding:0;margin:0;font:inherit;color:inherit;background:transparent;border:0;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.switch__toggle:before,.switch__toggle:after{content:'';position:absolute;z-index:-1;-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box}.switch--disabled{opacity:.3;cursor:default;pointer-events:none}.switch,.topcoat-switch{position:relative;display:inline-block;vertical-align:top;-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box}.switch__input,.topcoat-switch__input{position:absolute;overflow:hidden;padding:0;border:0;opacity:.001;z-index:1;vertical-align:top;outline:0}.switch__toggle,.topcoat-switch__toggle{position:relative;display:inline-block;vertical-align:top;-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;padding:0;margin:0;font:inherit;color:inherit;background:transparent;border:0;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.switch__toggle:before,.switch__toggle:after,.topcoat-switch__toggle:before,.topcoat-switch__toggle:after{content:'';position:absolute;z-index:-1;-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box}.switch--disabled,.topcoat-switch__input:disabled+.topcoat-switch__toggle{opacity:.3;cursor:default;pointer-events:none}.topcoat-switch{font-size:12px;padding:0 .563rem;border-radius:4px;border:1px solid #333434;overflow:hidden;width:3.5rem}.topcoat-switch__toggle:before,.topcoat-switch__toggle:after{top:-1px;width:2.6rem}.topcoat-switch__toggle:before{content:'ON';color:#288edf;background-color:#3f4041;right:.8rem;padding-left:.75rem}.topcoat-switch__toggle{line-height:1.313rem;height:1.313rem;width:1rem;border-radius:4px;color:#c6c8c8;text-shadow:0 -1px rgba(0,0,0,.69);background-color:#595b5b;border:1px solid #333434;margin-left:-.6rem;margin-bottom:-1px;margin-top:-1px;box-shadow:inset 0 1px #737373;-webkit-transition:margin-left .05s ease-in-out;transition:margin-left .05s ease-in-out}.topcoat-switch__toggle:after{content:'OFF';background-color:#3f4041;left:.8rem;padding-left:.6rem}.topcoat-switch__input:checked+.topcoat-switch__toggle{margin-left:1.85rem}.topcoat-switch__input:active+.topcoat-switch__toggle{border:1px solid #333434;box-shadow:inset 0 1px #737373}.topcoat-switch__input:focus+.topcoat-switch__toggle{border:1px solid #0036ff;box-shadow:0 0 0 2px #6fb5f1}.topcoat-switch__input:disabled+.topcoat-switch__toggle:after,.topcoat-switch__input:disabled+.topcoat-switch__toggle:before{background:transparent}.button,.topcoat-tab-bar__button{position:relative;display:inline-block;vertical-align:top;-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;padding:0;margin:0;font:inherit;color:inherit;background:transparent;border:0;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;text-decoration:none}.button--quiet{background:transparent;border:1px solid transparent;box-shadow:none}.button--disabled,.topcoat-tab-bar__button:disabled{opacity:.3;cursor:default;pointer-events:none}.button-bar,.topcoat-tab-bar{display:table;table-layout:fixed;white-space:nowrap;margin:0;padding:0}.button-bar__item,.topcoat-tab-bar__item{display:table-cell;width:auto;border-radius:0}.button-bar__item>input,.topcoat-tab-bar__item>input{position:absolute;overflow:hidden;padding:0;border:0;opacity:.001;z-index:1;vertical-align:top;outline:0}.button-bar__button{border-radius:inherit}.button-bar__item:disabled{opacity:.3;cursor:default;pointer-events:none}.topcoat-tab-bar__button{padding:0 .563rem;height:1.313rem;line-height:1.313rem;letter-spacing:0;color:#c6c8c8;text-shadow:0 -1px rgba(0,0,0,.69);vertical-align:top;background-color:#595b5b;box-shadow:inset 0 1px #737373;border-top:1px solid #333434}.topcoat-tab-bar__button:active,.topcoat-tab-bar__button--large:active,:checked+.topcoat-tab-bar__button{color:#288edf;background-color:#3f4041;box-shadow:inset 0 0 1px rgba(0,0,0,.05)}.topcoat-tab-bar__button:focus,.topcoat-tab-bar__button--large:focus{z-index:1;box-shadow:inset 0 1px rgba(255,255,255,.36),0 0 0 2px #6fb5f1;outline:0}.input,.topcoat-text-input,.topcoat-text-input--large{padding:0;margin:0;font:inherit;color:inherit;background:transparent;border:0;-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;vertical-align:top;outline:0}.input:disabled,.topcoat-text-input:disabled,.topcoat-text-input--large:disabled{opacity:.3;cursor:default;pointer-events:none}.topcoat-text-input,.topcoat-text-input--large{line-height:1.313rem;font-size:12px;letter-spacing:0;padding:0 .563rem;border:1px solid #333434;border-radius:4px;background-color:#454646;box-shadow:inset 0 1px rgba(0,0,0,.05);color:#c6c8c8;vertical-align:top}.topcoat-text-input:focus,.topcoat-text-input--large:focus{background-color:#595b5b;color:#fff;border:1px solid #0036ff;box-shadow:0 0 0 2px #6fb5f1}.topcoat-text-input:disabled::-webkit-input-placeholder{color:#fff}.topcoat-text-input:disabled::-moz-placeholder{color:#fff}.topcoat-text-input:disabled:-ms-input-placeholder{color:#fff}.topcoat-text-input:invalid{border:1px solid #ec514e}.topcoat-text-input--large{line-height:1.688rem;font-size:.875rem}.topcoat-text-input--large:disabled{color:#fff}.topcoat-text-input--large:disabled::-webkit-input-placeholder{color:#fff}.topcoat-text-input--large:disabled::-moz-placeholder{color:#fff}.topcoat-text-input--large:disabled:-ms-input-placeholder{color:#fff}.topcoat-text-input--large:invalid{border:1px solid #ec514e}.textarea{-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;padding:0;margin:0;font:inherit;color:inherit;background:transparent;border:0;vertical-align:top;resize:none;outline:0}.textarea:disabled{opacity:.3;cursor:default;pointer-events:none}.textarea,.topcoat-textarea,.topcoat-textarea--large{-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;padding:0;margin:0;font:inherit;color:inherit;background:transparent;border:0;vertical-align:top;resize:none;outline:0}.textarea:disabled,.topcoat-textarea:disabled,.topcoat-textarea--large:disabled{opacity:.3;cursor:default;pointer-events:none}.topcoat-textarea,.topcoat-textarea--large{padding:1rem;font-size:1rem;font-weight:400;border-radius:4px;line-height:1.313rem;border:1px solid #333434;background-color:#454646;box-shadow:inset 0 1px rgba(0,0,0,.05);color:#c6c8c8;letter-spacing:0}.topcoat-textarea:focus,.topcoat-textarea--large:focus{background-color:#595b5b;color:#fff;border:1px solid #0036ff;box-shadow:0 0 0 2px #6fb5f1}.topcoat-textarea:disabled::-webkit-input-placeholder{color:#fff}.topcoat-textarea:disabled::-moz-placeholder{color:#fff}.topcoat-textarea:disabled:-ms-input-placeholder{color:#fff}.topcoat-textarea--large{font-size:1.3rem;line-height:1.688rem}.topcoat-textarea--large:disabled{color:#fff}.topcoat-textarea--large:disabled::-webkit-input-placeholder{color:#fff}.topcoat-textarea--large:disabled::-moz-placeholder{color:#fff}.topcoat-textarea--large:disabled:-ms-input-placeholder{color:#fff}@font-face{font-family:"Source Sans";src:url(../font/SourceSansPro-Regular.otf)}@font-face{font-family:"Source Sans";src:url(../font/SourceSansPro-Light.otf);font-weight:200}@font-face{font-family:"Source Sans";src:url(../font/SourceSansPro-Semibold.otf);font-weight:600}body{margin:0;padding:0;background:#4b4d4e;color:#000;font:16px "Source Sans",helvetica,arial,sans-serif;font-weight:400}:focus{outline-color:transparent;outline-style:none}.topcoat-icon--menu-stack{background:url(../img/hamburger_light.svg) no-repeat;background-size:cover}.quarter{width:25%}.half{width:50%}.three-quarters{width:75%}.third{width:33.333%}.two-thirds{width:66.666%}.full{width:100%}.left{text-align:left}.center{text-align:center}.right{text-align:right}.reset-ui{-moz-box-sizing:border-box;box-sizing:border-box;background-clip:padding-box;position:relative;display:inline-block;vertical-align:top;padding:0;margin:0;font:inherit;color:inherit;background:transparent;border:0;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;text-overflow:ellipsis;white-space:nowrap;overflow:hidden} \ No newline at end of file diff --git a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/icons/ayon_logo.png b/server_addon/aftereffects/client/ayon_aftereffects/api/extension/icons/ayon_logo.png deleted file mode 100644 index 3a96f8e2b4..0000000000 Binary files a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/icons/ayon_logo.png and /dev/null differ diff --git a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/icons/iconDarkNormal.png b/server_addon/aftereffects/client/ayon_aftereffects/api/extension/icons/iconDarkNormal.png deleted file mode 100644 index b8652a85b8..0000000000 Binary files a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/icons/iconDarkNormal.png and /dev/null differ diff --git a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/icons/iconDarkRollover.png b/server_addon/aftereffects/client/ayon_aftereffects/api/extension/icons/iconDarkRollover.png deleted file mode 100644 index 49edd7ca27..0000000000 Binary files a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/icons/iconDarkRollover.png and /dev/null differ diff --git a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/icons/iconDisabled.png b/server_addon/aftereffects/client/ayon_aftereffects/api/extension/icons/iconDisabled.png deleted file mode 100644 index 49edd7ca27..0000000000 Binary files a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/icons/iconDisabled.png and /dev/null differ diff --git a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/icons/iconNormal.png b/server_addon/aftereffects/client/ayon_aftereffects/api/extension/icons/iconNormal.png deleted file mode 100644 index 199326f2ea..0000000000 Binary files a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/icons/iconNormal.png and /dev/null differ diff --git a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/icons/iconRollover.png b/server_addon/aftereffects/client/ayon_aftereffects/api/extension/icons/iconRollover.png deleted file mode 100644 index ff62645798..0000000000 Binary files a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/icons/iconRollover.png and /dev/null differ diff --git a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/index.html b/server_addon/aftereffects/client/ayon_aftereffects/api/extension/index.html deleted file mode 100644 index 480b814a57..0000000000 --- a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/index.html +++ /dev/null @@ -1,187 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/js/libs/CSInterface.js b/server_addon/aftereffects/client/ayon_aftereffects/api/extension/js/libs/CSInterface.js deleted file mode 100644 index 4239391efd..0000000000 --- a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/js/libs/CSInterface.js +++ /dev/null @@ -1,1193 +0,0 @@ -/************************************************************************************************** -* -* ADOBE SYSTEMS INCORPORATED -* Copyright 2013 Adobe Systems Incorporated -* All Rights Reserved. -* -* NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the -* terms of the Adobe license agreement accompanying it. If you have received this file from a -* source other than Adobe, then your use, modification, or distribution of it requires the prior -* written permission of Adobe. -* -**************************************************************************************************/ - -/** CSInterface - v8.0.0 */ - -/** - * Stores constants for the window types supported by the CSXS infrastructure. - */ -function CSXSWindowType() -{ -} - -/** Constant for the CSXS window type Panel. */ -CSXSWindowType._PANEL = "Panel"; - -/** Constant for the CSXS window type Modeless. */ -CSXSWindowType._MODELESS = "Modeless"; - -/** Constant for the CSXS window type ModalDialog. */ -CSXSWindowType._MODAL_DIALOG = "ModalDialog"; - -/** EvalScript error message */ -EvalScript_ErrMessage = "EvalScript error."; - -/** - * @class Version - * Defines a version number with major, minor, micro, and special - * components. The major, minor and micro values are numeric; the special - * value can be any string. - * - * @param major The major version component, a positive integer up to nine digits long. - * @param minor The minor version component, a positive integer up to nine digits long. - * @param micro The micro version component, a positive integer up to nine digits long. - * @param special The special version component, an arbitrary string. - * - * @return A new \c Version object. - */ -function Version(major, minor, micro, special) -{ - this.major = major; - this.minor = minor; - this.micro = micro; - this.special = special; -} - -/** - * The maximum value allowed for a numeric version component. - * This reflects the maximum value allowed in PlugPlug and the manifest schema. - */ -Version.MAX_NUM = 999999999; - -/** - * @class VersionBound - * Defines a boundary for a version range, which associates a \c Version object - * with a flag for whether it is an inclusive or exclusive boundary. - * - * @param version The \c #Version object. - * @param inclusive True if this boundary is inclusive, false if it is exclusive. - * - * @return A new \c VersionBound object. - */ -function VersionBound(version, inclusive) -{ - this.version = version; - this.inclusive = inclusive; -} - -/** - * @class VersionRange - * Defines a range of versions using a lower boundary and optional upper boundary. - * - * @param lowerBound The \c #VersionBound object. - * @param upperBound The \c #VersionBound object, or null for a range with no upper boundary. - * - * @return A new \c VersionRange object. - */ -function VersionRange(lowerBound, upperBound) -{ - this.lowerBound = lowerBound; - this.upperBound = upperBound; -} - -/** - * @class Runtime - * Represents a runtime related to the CEP infrastructure. - * Extensions can declare dependencies on particular - * CEP runtime versions in the extension manifest. - * - * @param name The runtime name. - * @param version A \c #VersionRange object that defines a range of valid versions. - * - * @return A new \c Runtime object. - */ -function Runtime(name, versionRange) -{ - this.name = name; - this.versionRange = versionRange; -} - -/** -* @class Extension -* Encapsulates a CEP-based extension to an Adobe application. -* -* @param id The unique identifier of this extension. -* @param name The localizable display name of this extension. -* @param mainPath The path of the "index.html" file. -* @param basePath The base path of this extension. -* @param windowType The window type of the main window of this extension. - Valid values are defined by \c #CSXSWindowType. -* @param width The default width in pixels of the main window of this extension. -* @param height The default height in pixels of the main window of this extension. -* @param minWidth The minimum width in pixels of the main window of this extension. -* @param minHeight The minimum height in pixels of the main window of this extension. -* @param maxWidth The maximum width in pixels of the main window of this extension. -* @param maxHeight The maximum height in pixels of the main window of this extension. -* @param defaultExtensionDataXml The extension data contained in the default \c ExtensionDispatchInfo section of the extension manifest. -* @param specialExtensionDataXml The extension data contained in the application-specific \c ExtensionDispatchInfo section of the extension manifest. -* @param requiredRuntimeList An array of \c Runtime objects for runtimes required by this extension. -* @param isAutoVisible True if this extension is visible on loading. -* @param isPluginExtension True if this extension has been deployed in the Plugins folder of the host application. -* -* @return A new \c Extension object. -*/ -function Extension(id, name, mainPath, basePath, windowType, width, height, minWidth, minHeight, maxWidth, maxHeight, - defaultExtensionDataXml, specialExtensionDataXml, requiredRuntimeList, isAutoVisible, isPluginExtension) -{ - this.id = id; - this.name = name; - this.mainPath = mainPath; - this.basePath = basePath; - this.windowType = windowType; - this.width = width; - this.height = height; - this.minWidth = minWidth; - this.minHeight = minHeight; - this.maxWidth = maxWidth; - this.maxHeight = maxHeight; - this.defaultExtensionDataXml = defaultExtensionDataXml; - this.specialExtensionDataXml = specialExtensionDataXml; - this.requiredRuntimeList = requiredRuntimeList; - this.isAutoVisible = isAutoVisible; - this.isPluginExtension = isPluginExtension; -} - -/** - * @class CSEvent - * A standard JavaScript event, the base class for CEP events. - * - * @param type The name of the event type. - * @param scope The scope of event, can be "GLOBAL" or "APPLICATION". - * @param appId The unique identifier of the application that generated the event. - * @param extensionId The unique identifier of the extension that generated the event. - * - * @return A new \c CSEvent object - */ -function CSEvent(type, scope, appId, extensionId) -{ - this.type = type; - this.scope = scope; - this.appId = appId; - this.extensionId = extensionId; -} - -/** Event-specific data. */ -CSEvent.prototype.data = ""; - -/** - * @class SystemPath - * Stores operating-system-specific location constants for use in the - * \c #CSInterface.getSystemPath() method. - * @return A new \c SystemPath object. - */ -function SystemPath() -{ -} - -/** The path to user data. */ -SystemPath.USER_DATA = "userData"; - -/** The path to common files for Adobe applications. */ -SystemPath.COMMON_FILES = "commonFiles"; - -/** The path to the user's default document folder. */ -SystemPath.MY_DOCUMENTS = "myDocuments"; - -/** @deprecated. Use \c #SystemPath.Extension. */ -SystemPath.APPLICATION = "application"; - -/** The path to current extension. */ -SystemPath.EXTENSION = "extension"; - -/** The path to hosting application's executable. */ -SystemPath.HOST_APPLICATION = "hostApplication"; - -/** - * @class ColorType - * Stores color-type constants. - */ -function ColorType() -{ -} - -/** RGB color type. */ -ColorType.RGB = "rgb"; - -/** Gradient color type. */ -ColorType.GRADIENT = "gradient"; - -/** Null color type. */ -ColorType.NONE = "none"; - -/** - * @class RGBColor - * Stores an RGB color with red, green, blue, and alpha values. - * All values are in the range [0.0 to 255.0]. Invalid numeric values are - * converted to numbers within this range. - * - * @param red The red value, in the range [0.0 to 255.0]. - * @param green The green value, in the range [0.0 to 255.0]. - * @param blue The blue value, in the range [0.0 to 255.0]. - * @param alpha The alpha (transparency) value, in the range [0.0 to 255.0]. - * The default, 255.0, means that the color is fully opaque. - * - * @return A new RGBColor object. - */ -function RGBColor(red, green, blue, alpha) -{ - this.red = red; - this.green = green; - this.blue = blue; - this.alpha = alpha; -} - -/** - * @class Direction - * A point value in which the y component is 0 and the x component - * is positive or negative for a right or left direction, - * or the x component is 0 and the y component is positive or negative for - * an up or down direction. - * - * @param x The horizontal component of the point. - * @param y The vertical component of the point. - * - * @return A new \c Direction object. - */ -function Direction(x, y) -{ - this.x = x; - this.y = y; -} - -/** - * @class GradientStop - * Stores gradient stop information. - * - * @param offset The offset of the gradient stop, in the range [0.0 to 1.0]. - * @param rgbColor The color of the gradient at this point, an \c #RGBColor object. - * - * @return GradientStop object. - */ -function GradientStop(offset, rgbColor) -{ - this.offset = offset; - this.rgbColor = rgbColor; -} - -/** - * @class GradientColor - * Stores gradient color information. - * - * @param type The gradient type, must be "linear". - * @param direction A \c #Direction object for the direction of the gradient - (up, down, right, or left). - * @param numStops The number of stops in the gradient. - * @param gradientStopList An array of \c #GradientStop objects. - * - * @return A new \c GradientColor object. - */ -function GradientColor(type, direction, numStops, arrGradientStop) -{ - this.type = type; - this.direction = direction; - this.numStops = numStops; - this.arrGradientStop = arrGradientStop; -} - -/** - * @class UIColor - * Stores color information, including the type, anti-alias level, and specific color - * values in a color object of an appropriate type. - * - * @param type The color type, 1 for "rgb" and 2 for "gradient". - The supplied color object must correspond to this type. - * @param antialiasLevel The anti-alias level constant. - * @param color A \c #RGBColor or \c #GradientColor object containing specific color information. - * - * @return A new \c UIColor object. - */ -function UIColor(type, antialiasLevel, color) -{ - this.type = type; - this.antialiasLevel = antialiasLevel; - this.color = color; -} - -/** - * @class AppSkinInfo - * Stores window-skin properties, such as color and font. All color parameter values are \c #UIColor objects except that systemHighlightColor is \c #RGBColor object. - * - * @param baseFontFamily The base font family of the application. - * @param baseFontSize The base font size of the application. - * @param appBarBackgroundColor The application bar background color. - * @param panelBackgroundColor The background color of the extension panel. - * @param appBarBackgroundColorSRGB The application bar background color, as sRGB. - * @param panelBackgroundColorSRGB The background color of the extension panel, as sRGB. - * @param systemHighlightColor The highlight color of the extension panel, if provided by the host application. Otherwise, the operating-system highlight color. - * - * @return AppSkinInfo object. - */ -function AppSkinInfo(baseFontFamily, baseFontSize, appBarBackgroundColor, panelBackgroundColor, appBarBackgroundColorSRGB, panelBackgroundColorSRGB, systemHighlightColor) -{ - this.baseFontFamily = baseFontFamily; - this.baseFontSize = baseFontSize; - this.appBarBackgroundColor = appBarBackgroundColor; - this.panelBackgroundColor = panelBackgroundColor; - this.appBarBackgroundColorSRGB = appBarBackgroundColorSRGB; - this.panelBackgroundColorSRGB = panelBackgroundColorSRGB; - this.systemHighlightColor = systemHighlightColor; -} - -/** - * @class HostEnvironment - * Stores information about the environment in which the extension is loaded. - * - * @param appName The application's name. - * @param appVersion The application's version. - * @param appLocale The application's current license locale. - * @param appUILocale The application's current UI locale. - * @param appId The application's unique identifier. - * @param isAppOnline True if the application is currently online. - * @param appSkinInfo An \c #AppSkinInfo object containing the application's default color and font styles. - * - * @return A new \c HostEnvironment object. - */ -function HostEnvironment(appName, appVersion, appLocale, appUILocale, appId, isAppOnline, appSkinInfo) -{ - this.appName = appName; - this.appVersion = appVersion; - this.appLocale = appLocale; - this.appUILocale = appUILocale; - this.appId = appId; - this.isAppOnline = isAppOnline; - this.appSkinInfo = appSkinInfo; -} - -/** - * @class HostCapabilities - * Stores information about the host capabilities. - * - * @param EXTENDED_PANEL_MENU True if the application supports panel menu. - * @param EXTENDED_PANEL_ICONS True if the application supports panel icon. - * @param DELEGATE_APE_ENGINE True if the application supports delegated APE engine. - * @param SUPPORT_HTML_EXTENSIONS True if the application supports HTML extensions. - * @param DISABLE_FLASH_EXTENSIONS True if the application disables FLASH extensions. - * - * @return A new \c HostCapabilities object. - */ -function HostCapabilities(EXTENDED_PANEL_MENU, EXTENDED_PANEL_ICONS, DELEGATE_APE_ENGINE, SUPPORT_HTML_EXTENSIONS, DISABLE_FLASH_EXTENSIONS) -{ - this.EXTENDED_PANEL_MENU = EXTENDED_PANEL_MENU; - this.EXTENDED_PANEL_ICONS = EXTENDED_PANEL_ICONS; - this.DELEGATE_APE_ENGINE = DELEGATE_APE_ENGINE; - this.SUPPORT_HTML_EXTENSIONS = SUPPORT_HTML_EXTENSIONS; - this.DISABLE_FLASH_EXTENSIONS = DISABLE_FLASH_EXTENSIONS; // Since 5.0.0 -} - -/** - * @class ApiVersion - * Stores current api version. - * - * Since 4.2.0 - * - * @param major The major version - * @param minor The minor version. - * @param micro The micro version. - * - * @return ApiVersion object. - */ -function ApiVersion(major, minor, micro) -{ - this.major = major; - this.minor = minor; - this.micro = micro; -} - -/** - * @class MenuItemStatus - * Stores flyout menu item status - * - * Since 5.2.0 - * - * @param menuItemLabel The menu item label. - * @param enabled True if user wants to enable the menu item. - * @param checked True if user wants to check the menu item. - * - * @return MenuItemStatus object. - */ -function MenuItemStatus(menuItemLabel, enabled, checked) -{ - this.menuItemLabel = menuItemLabel; - this.enabled = enabled; - this.checked = checked; -} - -/** - * @class ContextMenuItemStatus - * Stores the status of the context menu item. - * - * Since 5.2.0 - * - * @param menuItemID The menu item id. - * @param enabled True if user wants to enable the menu item. - * @param checked True if user wants to check the menu item. - * - * @return MenuItemStatus object. - */ -function ContextMenuItemStatus(menuItemID, enabled, checked) -{ - this.menuItemID = menuItemID; - this.enabled = enabled; - this.checked = checked; -} -//------------------------------ CSInterface ---------------------------------- - -/** - * @class CSInterface - * This is the entry point to the CEP extensibility infrastructure. - * Instantiate this object and use it to: - * - * - * @return A new \c CSInterface object - */ -function CSInterface() -{ -} - -/** - * User can add this event listener to handle native application theme color changes. - * Callback function gives extensions ability to fine-tune their theme color after the - * global theme color has been changed. - * The callback function should be like below: - * - * @example - * // event is a CSEvent object, but user can ignore it. - * function OnAppThemeColorChanged(event) - * { - * // Should get a latest HostEnvironment object from application. - * var skinInfo = JSON.parse(window.__adobe_cep__.getHostEnvironment()).appSkinInfo; - * // Gets the style information such as color info from the skinInfo, - * // and redraw all UI controls of your extension according to the style info. - * } - */ -CSInterface.THEME_COLOR_CHANGED_EVENT = "com.adobe.csxs.events.ThemeColorChanged"; - -/** The host environment data object. */ -CSInterface.prototype.hostEnvironment = window.__adobe_cep__ ? JSON.parse(window.__adobe_cep__.getHostEnvironment()) : null; - -/** Retrieves information about the host environment in which the - * extension is currently running. - * - * @return A \c #HostEnvironment object. - */ -CSInterface.prototype.getHostEnvironment = function() -{ - this.hostEnvironment = JSON.parse(window.__adobe_cep__.getHostEnvironment()); - return this.hostEnvironment; -}; - -/** Closes this extension. */ -CSInterface.prototype.closeExtension = function() -{ - window.__adobe_cep__.closeExtension(); -}; - -/** - * Retrieves a path for which a constant is defined in the system. - * - * @param pathType The path-type constant defined in \c #SystemPath , - * - * @return The platform-specific system path string. - */ -CSInterface.prototype.getSystemPath = function(pathType) -{ - var path = decodeURI(window.__adobe_cep__.getSystemPath(pathType)); - var OSVersion = this.getOSInformation(); - if (OSVersion.indexOf("Windows") >= 0) - { - path = path.replace("file:///", ""); - } - else if (OSVersion.indexOf("Mac") >= 0) - { - path = path.replace("file://", ""); - } - return path; -}; - -/** - * Evaluates a JavaScript script, which can use the JavaScript DOM - * of the host application. - * - * @param script The JavaScript script. - * @param callback Optional. A callback function that receives the result of execution. - * If execution fails, the callback function receives the error message \c EvalScript_ErrMessage. - */ -CSInterface.prototype.evalScript = function(script, callback) -{ - if(callback === null || callback === undefined) - { - callback = function(result){}; - } - window.__adobe_cep__.evalScript(script, callback); -}; - -/** - * Retrieves the unique identifier of the application. - * in which the extension is currently running. - * - * @return The unique ID string. - */ -CSInterface.prototype.getApplicationID = function() -{ - var appId = this.hostEnvironment.appId; - return appId; -}; - -/** - * Retrieves host capability information for the application - * in which the extension is currently running. - * - * @return A \c #HostCapabilities object. - */ -CSInterface.prototype.getHostCapabilities = function() -{ - var hostCapabilities = JSON.parse(window.__adobe_cep__.getHostCapabilities() ); - return hostCapabilities; -}; - -/** - * Triggers a CEP event programmatically. Yoy can use it to dispatch - * an event of a predefined type, or of a type you have defined. - * - * @param event A \c CSEvent object. - */ -CSInterface.prototype.dispatchEvent = function(event) -{ - if (typeof event.data == "object") - { - event.data = JSON.stringify(event.data); - } - - window.__adobe_cep__.dispatchEvent(event); -}; - -/** - * Registers an interest in a CEP event of a particular type, and - * assigns an event handler. - * The event infrastructure notifies your extension when events of this type occur, - * passing the event object to the registered handler function. - * - * @param type The name of the event type of interest. - * @param listener The JavaScript handler function or method. - * @param obj Optional, the object containing the handler method, if any. - * Default is null. - */ -CSInterface.prototype.addEventListener = function(type, listener, obj) -{ - window.__adobe_cep__.addEventListener(type, listener, obj); -}; - -/** - * Removes a registered event listener. - * - * @param type The name of the event type of interest. - * @param listener The JavaScript handler function or method that was registered. - * @param obj Optional, the object containing the handler method, if any. - * Default is null. - */ -CSInterface.prototype.removeEventListener = function(type, listener, obj) -{ - window.__adobe_cep__.removeEventListener(type, listener, obj); -}; - -/** - * Loads and launches another extension, or activates the extension if it is already loaded. - * - * @param extensionId The extension's unique identifier. - * @param startupParams Not currently used, pass "". - * - * @example - * To launch the extension "help" with ID "HLP" from this extension, call: - * requestOpenExtension("HLP", ""); - * - */ -CSInterface.prototype.requestOpenExtension = function(extensionId, params) -{ - window.__adobe_cep__.requestOpenExtension(extensionId, params); -}; - -/** - * Retrieves the list of extensions currently loaded in the current host application. - * The extension list is initialized once, and remains the same during the lifetime - * of the CEP session. - * - * @param extensionIds Optional, an array of unique identifiers for extensions of interest. - * If omitted, retrieves data for all extensions. - * - * @return Zero or more \c #Extension objects. - */ -CSInterface.prototype.getExtensions = function(extensionIds) -{ - var extensionIdsStr = JSON.stringify(extensionIds); - var extensionsStr = window.__adobe_cep__.getExtensions(extensionIdsStr); - - var extensions = JSON.parse(extensionsStr); - return extensions; -}; - -/** - * Retrieves network-related preferences. - * - * @return A JavaScript object containing network preferences. - */ -CSInterface.prototype.getNetworkPreferences = function() -{ - var result = window.__adobe_cep__.getNetworkPreferences(); - var networkPre = JSON.parse(result); - - return networkPre; -}; - -/** - * Initializes the resource bundle for this extension with property values - * for the current application and locale. - * To support multiple locales, you must define a property file for each locale, - * containing keyed display-string values for that locale. - * See localization documentation for Extension Builder and related products. - * - * Keys can be in the - * form key.value="localized string", for use in HTML text elements. - * For example, in this input element, the localized \c key.value string is displayed - * instead of the empty \c value string: - * - * - * - * @return An object containing the resource bundle information. - */ -CSInterface.prototype.initResourceBundle = function() -{ - var resourceBundle = JSON.parse(window.__adobe_cep__.initResourceBundle()); - var resElms = document.querySelectorAll('[data-locale]'); - for (var n = 0; n < resElms.length; n++) - { - var resEl = resElms[n]; - // Get the resource key from the element. - var resKey = resEl.getAttribute('data-locale'); - if (resKey) - { - // Get all the resources that start with the key. - for (var key in resourceBundle) - { - if (key.indexOf(resKey) === 0) - { - var resValue = resourceBundle[key]; - if (key.length == resKey.length) - { - resEl.innerHTML = resValue; - } - else if ('.' == key.charAt(resKey.length)) - { - var attrKey = key.substring(resKey.length + 1); - resEl[attrKey] = resValue; - } - } - } - } - } - return resourceBundle; -}; - -/** - * Writes installation information to a file. - * - * @return The file path. - */ -CSInterface.prototype.dumpInstallationInfo = function() -{ - return window.__adobe_cep__.dumpInstallationInfo(); -}; - -/** - * Retrieves version information for the current Operating System, - * See http://www.useragentstring.com/pages/Chrome/ for Chrome \c navigator.userAgent values. - * - * @return A string containing the OS version, or "unknown Operation System". - * If user customizes the User Agent by setting CEF command parameter "--user-agent", only - * "Mac OS X" or "Windows" will be returned. - */ -CSInterface.prototype.getOSInformation = function() -{ - var userAgent = navigator.userAgent; - - if ((navigator.platform == "Win32") || (navigator.platform == "Windows")) - { - var winVersion = "Windows"; - var winBit = ""; - if (userAgent.indexOf("Windows") > -1) - { - if (userAgent.indexOf("Windows NT 5.0") > -1) - { - winVersion = "Windows 2000"; - } - else if (userAgent.indexOf("Windows NT 5.1") > -1) - { - winVersion = "Windows XP"; - } - else if (userAgent.indexOf("Windows NT 5.2") > -1) - { - winVersion = "Windows Server 2003"; - } - else if (userAgent.indexOf("Windows NT 6.0") > -1) - { - winVersion = "Windows Vista"; - } - else if (userAgent.indexOf("Windows NT 6.1") > -1) - { - winVersion = "Windows 7"; - } - else if (userAgent.indexOf("Windows NT 6.2") > -1) - { - winVersion = "Windows 8"; - } - else if (userAgent.indexOf("Windows NT 6.3") > -1) - { - winVersion = "Windows 8.1"; - } - else if (userAgent.indexOf("Windows NT 10") > -1) - { - winVersion = "Windows 10"; - } - - if (userAgent.indexOf("WOW64") > -1 || userAgent.indexOf("Win64") > -1) - { - winBit = " 64-bit"; - } - else - { - winBit = " 32-bit"; - } - } - - return winVersion + winBit; - } - else if ((navigator.platform == "MacIntel") || (navigator.platform == "Macintosh")) - { - var result = "Mac OS X"; - - if (userAgent.indexOf("Mac OS X") > -1) - { - result = userAgent.substring(userAgent.indexOf("Mac OS X"), userAgent.indexOf(")")); - result = result.replace(/_/g, "."); - } - - return result; - } - - return "Unknown Operation System"; -}; - -/** - * Opens a page in the default system browser. - * - * Since 4.2.0 - * - * @param url The URL of the page/file to open, or the email address. - * Must use HTTP/HTTPS/file/mailto protocol. For example: - * "http://www.adobe.com" - * "https://github.com" - * "file:///C:/log.txt" - * "mailto:test@adobe.com" - * - * @return One of these error codes:\n - * \n - */ -CSInterface.prototype.openURLInDefaultBrowser = function(url) -{ - return cep.util.openURLInDefaultBrowser(url); -}; - -/** - * Retrieves extension ID. - * - * Since 4.2.0 - * - * @return extension ID. - */ -CSInterface.prototype.getExtensionID = function() -{ - return window.__adobe_cep__.getExtensionId(); -}; - -/** - * Retrieves the scale factor of screen. - * On Windows platform, the value of scale factor might be different from operating system's scale factor, - * since host application may use its self-defined scale factor. - * - * Since 4.2.0 - * - * @return One of the following float number. - * \n - */ -CSInterface.prototype.getScaleFactor = function() -{ - return window.__adobe_cep__.getScaleFactor(); -}; - -/** - * Set a handler to detect any changes of scale factor. This only works on Mac. - * - * Since 4.2.0 - * - * @param handler The function to be called when scale factor is changed. - * - */ -CSInterface.prototype.setScaleFactorChangedHandler = function(handler) -{ - window.__adobe_cep__.setScaleFactorChangedHandler(handler); -}; - -/** - * Retrieves current API version. - * - * Since 4.2.0 - * - * @return ApiVersion object. - * - */ -CSInterface.prototype.getCurrentApiVersion = function() -{ - var apiVersion = JSON.parse(window.__adobe_cep__.getCurrentApiVersion()); - return apiVersion; -}; - -/** - * Set panel flyout menu by an XML. - * - * Since 5.2.0 - * - * Register a callback function for "com.adobe.csxs.events.flyoutMenuClicked" to get notified when a - * menu item is clicked. - * The "data" attribute of event is an object which contains "menuId" and "menuName" attributes. - * - * Register callback functions for "com.adobe.csxs.events.flyoutMenuOpened" and "com.adobe.csxs.events.flyoutMenuClosed" - * respectively to get notified when flyout menu is opened or closed. - * - * @param menu A XML string which describes menu structure. - * An example menu XML: - * - * - * - * - * - * - * - * - * - * - * - * - */ -CSInterface.prototype.setPanelFlyoutMenu = function(menu) -{ - if ("string" != typeof menu) - { - return; - } - - window.__adobe_cep__.invokeSync("setPanelFlyoutMenu", menu); -}; - -/** - * Updates a menu item in the extension window's flyout menu, by setting the enabled - * and selection status. - * - * Since 5.2.0 - * - * @param menuItemLabel The menu item label. - * @param enabled True to enable the item, false to disable it (gray it out). - * @param checked True to select the item, false to deselect it. - * - * @return false when the host application does not support this functionality (HostCapabilities.EXTENDED_PANEL_MENU is false). - * Fails silently if menu label is invalid. - * - * @see HostCapabilities.EXTENDED_PANEL_MENU - */ -CSInterface.prototype.updatePanelMenuItem = function(menuItemLabel, enabled, checked) -{ - var ret = false; - if (this.getHostCapabilities().EXTENDED_PANEL_MENU) - { - var itemStatus = new MenuItemStatus(menuItemLabel, enabled, checked); - ret = window.__adobe_cep__.invokeSync("updatePanelMenuItem", JSON.stringify(itemStatus)); - } - return ret; -}; - - -/** - * Set context menu by XML string. - * - * Since 5.2.0 - * - * There are a number of conventions used to communicate what type of menu item to create and how it should be handled. - * - an item without menu ID or menu name is disabled and is not shown. - * - if the item name is "---" (three hyphens) then it is treated as a separator. The menu ID in this case will always be NULL. - * - Checkable attribute takes precedence over Checked attribute. - * - a PNG icon. For optimal display results please supply a 16 x 16px icon as larger dimensions will increase the size of the menu item. - The Chrome extension contextMenus API was taken as a reference. - https://developer.chrome.com/extensions/contextMenus - * - the items with icons and checkable items cannot coexist on the same menu level. The former take precedences over the latter. - * - * @param menu A XML string which describes menu structure. - * @param callback The callback function which is called when a menu item is clicked. The only parameter is the returned ID of clicked menu item. - * - * @description An example menu XML: - * - * - * - * - * - * - * - * - * - * - * - */ -CSInterface.prototype.setContextMenu = function(menu, callback) -{ - if ("string" != typeof menu) - { - return; - } - - window.__adobe_cep__.invokeAsync("setContextMenu", menu, callback); -}; - -/** - * Set context menu by JSON string. - * - * Since 6.0.0 - * - * There are a number of conventions used to communicate what type of menu item to create and how it should be handled. - * - an item without menu ID or menu name is disabled and is not shown. - * - if the item label is "---" (three hyphens) then it is treated as a separator. The menu ID in this case will always be NULL. - * - Checkable attribute takes precedence over Checked attribute. - * - a PNG icon. For optimal display results please supply a 16 x 16px icon as larger dimensions will increase the size of the menu item. - The Chrome extension contextMenus API was taken as a reference. - * - the items with icons and checkable items cannot coexist on the same menu level. The former take precedences over the latter. - https://developer.chrome.com/extensions/contextMenus - * - * @param menu A JSON string which describes menu structure. - * @param callback The callback function which is called when a menu item is clicked. The only parameter is the returned ID of clicked menu item. - * - * @description An example menu JSON: - * - * { - * "menu": [ - * { - * "id": "menuItemId1", - * "label": "testExample1", - * "enabled": true, - * "checkable": true, - * "checked": false, - * "icon": "./image/small_16X16.png" - * }, - * { - * "id": "menuItemId2", - * "label": "testExample2", - * "menu": [ - * { - * "id": "menuItemId2-1", - * "label": "testExample2-1", - * "menu": [ - * { - * "id": "menuItemId2-1-1", - * "label": "testExample2-1-1", - * "enabled": false, - * "checkable": true, - * "checked": true - * } - * ] - * }, - * { - * "id": "menuItemId2-2", - * "label": "testExample2-2", - * "enabled": true, - * "checkable": true, - * "checked": true - * } - * ] - * }, - * { - * "label": "---" - * }, - * { - * "id": "menuItemId3", - * "label": "testExample3", - * "enabled": false, - * "checkable": true, - * "checked": false - * } - * ] - * } - * - */ -CSInterface.prototype.setContextMenuByJSON = function(menu, callback) -{ - if ("string" != typeof menu) - { - return; - } - - window.__adobe_cep__.invokeAsync("setContextMenuByJSON", menu, callback); -}; - -/** - * Updates a context menu item by setting the enabled and selection status. - * - * Since 5.2.0 - * - * @param menuItemID The menu item ID. - * @param enabled True to enable the item, false to disable it (gray it out). - * @param checked True to select the item, false to deselect it. - */ -CSInterface.prototype.updateContextMenuItem = function(menuItemID, enabled, checked) -{ - var itemStatus = new ContextMenuItemStatus(menuItemID, enabled, checked); - ret = window.__adobe_cep__.invokeSync("updateContextMenuItem", JSON.stringify(itemStatus)); -}; - -/** - * Get the visibility status of an extension window. - * - * Since 6.0.0 - * - * @return true if the extension window is visible; false if the extension window is hidden. - */ -CSInterface.prototype.isWindowVisible = function() -{ - return window.__adobe_cep__.invokeSync("isWindowVisible", ""); -}; - -/** - * Resize extension's content to the specified dimensions. - * 1. Works with modal and modeless extensions in all Adobe products. - * 2. Extension's manifest min/max size constraints apply and take precedence. - * 3. For panel extensions - * 3.1 This works in all Adobe products except: - * * Premiere Pro - * * Prelude - * * After Effects - * 3.2 When the panel is in certain states (especially when being docked), - * it will not change to the desired dimensions even when the - * specified size satisfies min/max constraints. - * - * Since 6.0.0 - * - * @param width The new width - * @param height The new height - */ -CSInterface.prototype.resizeContent = function(width, height) -{ - window.__adobe_cep__.resizeContent(width, height); -}; - -/** - * Register the invalid certificate callback for an extension. - * This callback will be triggered when the extension tries to access the web site that contains the invalid certificate on the main frame. - * But if the extension does not call this function and tries to access the web site containing the invalid certificate, a default error page will be shown. - * - * Since 6.1.0 - * - * @param callback the callback function - */ -CSInterface.prototype.registerInvalidCertificateCallback = function(callback) -{ - return window.__adobe_cep__.registerInvalidCertificateCallback(callback); -}; - -/** - * Register an interest in some key events to prevent them from being sent to the host application. - * - * This function works with modeless extensions and panel extensions. - * Generally all the key events will be sent to the host application for these two extensions if the current focused element - * is not text input or dropdown, - * If you want to intercept some key events and want them to be handled in the extension, please call this function - * in advance to prevent them being sent to the host application. - * - * Since 6.1.0 - * - * @param keyEventsInterest A JSON string describing those key events you are interested in. A null object or - an empty string will lead to removing the interest - * - * This JSON string should be an array, each object has following keys: - * - * keyCode: [Required] represents an OS system dependent virtual key code identifying - * the unmodified value of the pressed key. - * ctrlKey: [optional] a Boolean that indicates if the control key was pressed (true) or not (false) when the event occurred. - * altKey: [optional] a Boolean that indicates if the alt key was pressed (true) or not (false) when the event occurred. - * shiftKey: [optional] a Boolean that indicates if the shift key was pressed (true) or not (false) when the event occurred. - * metaKey: [optional] (Mac Only) a Boolean that indicates if the Meta key was pressed (true) or not (false) when the event occurred. - * On Macintosh keyboards, this is the command key. To detect Windows key on Windows, please use keyCode instead. - * An example JSON string: - * - * [ - * { - * "keyCode": 48 - * }, - * { - * "keyCode": 123, - * "ctrlKey": true - * }, - * { - * "keyCode": 123, - * "ctrlKey": true, - * "metaKey": true - * } - * ] - * - */ -CSInterface.prototype.registerKeyEventsInterest = function(keyEventsInterest) -{ - return window.__adobe_cep__.registerKeyEventsInterest(keyEventsInterest); -}; - -/** - * Set the title of the extension window. - * This function works with modal and modeless extensions in all Adobe products, and panel extensions in Photoshop, InDesign, InCopy, Illustrator, Flash Pro and Dreamweaver. - * - * Since 6.1.0 - * - * @param title The window title. - */ -CSInterface.prototype.setWindowTitle = function(title) -{ - window.__adobe_cep__.invokeSync("setWindowTitle", title); -}; - -/** - * Get the title of the extension window. - * This function works with modal and modeless extensions in all Adobe products, and panel extensions in Photoshop, InDesign, InCopy, Illustrator, Flash Pro and Dreamweaver. - * - * Since 6.1.0 - * - * @return The window title. - */ -CSInterface.prototype.getWindowTitle = function() -{ - return window.__adobe_cep__.invokeSync("getWindowTitle", ""); -}; diff --git a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/js/libs/jquery-2.0.2.min.js b/server_addon/aftereffects/client/ayon_aftereffects/api/extension/js/libs/jquery-2.0.2.min.js deleted file mode 100644 index 73e5218d21..0000000000 --- a/server_addon/aftereffects/client/ayon_aftereffects/api/extension/js/libs/jquery-2.0.2.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! jQuery v2.0.2 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license -//@ sourceMappingURL=jquery-2.0.2.min.map -*/ -(function(e,undefined){var t,n,r=typeof undefined,i=e.location,o=e.document,s=o.documentElement,a=e.jQuery,u=e.$,l={},c=[],p="2.0.2",f=c.concat,h=c.push,d=c.slice,g=c.indexOf,m=l.toString,y=l.hasOwnProperty,v=p.trim,x=function(e,n){return new x.fn.init(e,n,t)},b=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,w=/\S+/g,T=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,k=/^-ms-/,N=/-([\da-z])/gi,E=function(e,t){return t.toUpperCase()},S=function(){o.removeEventListener("DOMContentLoaded",S,!1),e.removeEventListener("load",S,!1),x.ready()};x.fn=x.prototype={jquery:p,constructor:x,init:function(e,t,n){var r,i;if(!e)return this;if("string"==typeof e){if(r="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:T.exec(e),!r||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof x?t[0]:t,x.merge(this,x.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:o,!0)),C.test(r[1])&&x.isPlainObject(t))for(r in t)x.isFunction(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return i=o.getElementById(r[2]),i&&i.parentNode&&(this.length=1,this[0]=i),this.context=o,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?n.ready(e):(e.selector!==undefined&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return d.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,t,n,r,i,o,s=arguments[0]||{},a=1,u=arguments.length,l=!1;for("boolean"==typeof s&&(l=s,s=arguments[1]||{},a=2),"object"==typeof s||x.isFunction(s)||(s={}),u===a&&(s=this,--a);u>a;a++)if(null!=(e=arguments[a]))for(t in e)n=s[t],r=e[t],s!==r&&(l&&r&&(x.isPlainObject(r)||(i=x.isArray(r)))?(i?(i=!1,o=n&&x.isArray(n)?n:[]):o=n&&x.isPlainObject(n)?n:{},s[t]=x.extend(l,o,r)):r!==undefined&&(s[t]=r));return s},x.extend({expando:"jQuery"+(p+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=a),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){(e===!0?--x.readyWait:x.isReady)||(x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(o,[x]),x.fn.trigger&&x(o).trigger("ready").off("ready")))},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray,isWindow:function(e){return null!=e&&e===e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[m.call(e)]||"object":typeof e},isPlainObject:function(e){if("object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!y.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(t){return!1}return!0},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||o;var r=C.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:JSON.parse,parseXML:function(e){var t,n;if(!e||"string"!=typeof e)return null;try{n=new DOMParser,t=n.parseFromString(e,"text/xml")}catch(r){t=undefined}return(!t||t.getElementsByTagName("parsererror").length)&&x.error("Invalid XML: "+e),t},noop:function(){},globalEval:function(e){var t,n=eval;e=x.trim(e),e&&(1===e.indexOf("use strict")?(t=o.createElement("script"),t.text=e,o.head.appendChild(t).parentNode.removeChild(t)):n(e))},camelCase:function(e){return e.replace(k,"ms-").replace(N,E)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,s=j(e);if(n){if(s){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(s){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:function(e){return null==e?"":v.call(e)},makeArray:function(e,t){var n=t||[];return null!=e&&(j(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:g.call(t,e,n)},merge:function(e,t){var n=t.length,r=e.length,i=0;if("number"==typeof n)for(;n>i;i++)e[r++]=t[i];else while(t[i]!==undefined)e[r++]=t[i++];return e.length=r,e},grep:function(e,t,n){var r,i=[],o=0,s=e.length;for(n=!!n;s>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,s=j(e),a=[];if(s)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(a[a.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(a[a.length]=r);return f.apply([],a)},guid:1,proxy:function(e,t){var n,r,i;return"string"==typeof t&&(n=e[t],t=e,e=n),x.isFunction(e)?(r=d.call(arguments,2),i=function(){return e.apply(t||this,r.concat(d.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):undefined},access:function(e,t,n,r,i,o,s){var a=0,u=e.length,l=null==n;if("object"===x.type(n)){i=!0;for(a in n)x.access(e,t,a,n[a],!0,o,s)}else if(r!==undefined&&(i=!0,x.isFunction(r)||(s=!0),l&&(s?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(x(e),n)})),t))for(;u>a;a++)t(e[a],n,s?r:r.call(e[a],a,t(e[a],n)));return i?e:l?t.call(e):u?t(e[0],n):o},now:Date.now,swap:function(e,t,n,r){var i,o,s={};for(o in t)s[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=s[o];return i}}),x.ready.promise=function(t){return n||(n=x.Deferred(),"complete"===o.readyState?setTimeout(x.ready):(o.addEventListener("DOMContentLoaded",S,!1),e.addEventListener("load",S,!1))),n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){l["[object "+t+"]"]=t.toLowerCase()});function j(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}t=x(o),function(e,undefined){var t,n,r,i,o,s,a,u,l,c,p,f,h,d,g,m,y,v="sizzle"+-new Date,b=e.document,w=0,T=0,C=at(),k=at(),N=at(),E=!1,S=function(){return 0},j=typeof undefined,D=1<<31,A={}.hasOwnProperty,L=[],H=L.pop,q=L.push,O=L.push,F=L.slice,P=L.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},R="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",W="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",$=W.replace("w","w#"),B="\\["+M+"*("+W+")"+M+"*(?:([*^$|!~]?=)"+M+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+$+")|)|)"+M+"*\\]",I=":("+W+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+B.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),_=RegExp("^"+M+"*,"+M+"*"),X=RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=RegExp(M+"*[+~]"),Y=RegExp("="+M+"*([^\\]'\"]*)"+M+"*\\]","g"),V=RegExp(I),G=RegExp("^"+$+"$"),J={ID:RegExp("^#("+W+")"),CLASS:RegExp("^\\.("+W+")"),TAG:RegExp("^("+W.replace("w","w*")+")"),ATTR:RegExp("^"+B),PSEUDO:RegExp("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:RegExp("^(?:"+R+")$","i"),needsContext:RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Q=/^[^{]+\{\s*\[native \w/,K=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,Z=/^(?:input|select|textarea|button)$/i,et=/^h\d$/i,tt=/'|\\/g,nt=RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),rt=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{O.apply(L=F.call(b.childNodes),b.childNodes),L[b.childNodes.length].nodeType}catch(it){O={apply:L.length?function(e,t){q.apply(e,F.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function ot(e,t,r,i){var o,s,a,u,l,f,g,m,x,w;if((t?t.ownerDocument||t:b)!==p&&c(t),t=t||p,r=r||[],!e||"string"!=typeof e)return r;if(1!==(u=t.nodeType)&&9!==u)return[];if(h&&!i){if(o=K.exec(e))if(a=o[1]){if(9===u){if(s=t.getElementById(a),!s||!s.parentNode)return r;if(s.id===a)return r.push(s),r}else if(t.ownerDocument&&(s=t.ownerDocument.getElementById(a))&&y(t,s)&&s.id===a)return r.push(s),r}else{if(o[2])return O.apply(r,t.getElementsByTagName(e)),r;if((a=o[3])&&n.getElementsByClassName&&t.getElementsByClassName)return O.apply(r,t.getElementsByClassName(a)),r}if(n.qsa&&(!d||!d.test(e))){if(m=g=v,x=t,w=9===u&&e,1===u&&"object"!==t.nodeName.toLowerCase()){f=vt(e),(g=t.getAttribute("id"))?m=g.replace(tt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",l=f.length;while(l--)f[l]=m+xt(f[l]);x=U.test(e)&&t.parentNode||t,w=f.join(",")}if(w)try{return O.apply(r,x.querySelectorAll(w)),r}catch(T){}finally{g||t.removeAttribute("id")}}}return St(e.replace(z,"$1"),t,r,i)}function st(e){return Q.test(e+"")}function at(){var e=[];function t(n,r){return e.push(n+=" ")>i.cacheLength&&delete t[e.shift()],t[n]=r}return t}function ut(e){return e[v]=!0,e}function lt(e){var t=p.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function ct(e,t,n){e=e.split("|");var r,o=e.length,s=n?null:t;while(o--)(r=i.attrHandle[e[o]])&&r!==t||(i.attrHandle[e[o]]=s)}function pt(e,t){var n=e.getAttributeNode(t);return n&&n.specified?n.value:e[t]===!0?t.toLowerCase():null}function ft(e,t){return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}function ht(e){return"input"===e.nodeName.toLowerCase()?e.defaultValue:undefined}function dt(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function gt(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function mt(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function yt(e){return ut(function(t){return t=+t,ut(function(n,r){var i,o=e([],n.length,t),s=o.length;while(s--)n[i=o[s]]&&(n[i]=!(r[i]=n[i]))})})}s=ot.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},n=ot.support={},c=ot.setDocument=function(e){var t=e?e.ownerDocument||e:b,r=t.parentWindow;return t!==p&&9===t.nodeType&&t.documentElement?(p=t,f=t.documentElement,h=!s(t),r&&r.frameElement&&r.attachEvent("onbeforeunload",function(){c()}),n.attributes=lt(function(e){return e.innerHTML="",ct("type|href|height|width",ft,"#"===e.firstChild.getAttribute("href")),ct(R,pt,null==e.getAttribute("disabled")),e.className="i",!e.getAttribute("className")}),n.input=lt(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")}),ct("value",ht,n.attributes&&n.input),n.getElementsByTagName=lt(function(e){return e.appendChild(t.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=lt(function(e){return e.innerHTML="
",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),n.getById=lt(function(e){return f.appendChild(e).id=v,!t.getElementsByName||!t.getElementsByName(v).length}),n.getById?(i.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},i.filter.ID=function(e){var t=e.replace(nt,rt);return function(e){return e.getAttribute("id")===t}}):(delete i.find.ID,i.filter.ID=function(e){var t=e.replace(nt,rt);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),i.find.TAG=n.getElementsByTagName?function(e,t){return typeof t.getElementsByTagName!==j?t.getElementsByTagName(e):undefined}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},i.find.CLASS=n.getElementsByClassName&&function(e,t){return typeof t.getElementsByClassName!==j&&h?t.getElementsByClassName(e):undefined},g=[],d=[],(n.qsa=st(t.querySelectorAll))&&(lt(function(e){e.innerHTML="",e.querySelectorAll("[selected]").length||d.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll(":checked").length||d.push(":checked")}),lt(function(e){var n=t.createElement("input");n.setAttribute("type","hidden"),e.appendChild(n).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&d.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||d.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),d.push(",.*:")})),(n.matchesSelector=st(m=f.webkitMatchesSelector||f.mozMatchesSelector||f.oMatchesSelector||f.msMatchesSelector))&<(function(e){n.disconnectedMatch=m.call(e,"div"),m.call(e,"[s!='']:x"),g.push("!=",I)}),d=d.length&&RegExp(d.join("|")),g=g.length&&RegExp(g.join("|")),y=st(f.contains)||f.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},n.sortDetached=lt(function(e){return 1&e.compareDocumentPosition(t.createElement("div"))}),S=f.compareDocumentPosition?function(e,r){if(e===r)return E=!0,0;var i=r.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(r);return i?1&i||!n.sortDetached&&r.compareDocumentPosition(e)===i?e===t||y(b,e)?-1:r===t||y(b,r)?1:l?P.call(l,e)-P.call(l,r):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,n){var r,i=0,o=e.parentNode,s=n.parentNode,a=[e],u=[n];if(e===n)return E=!0,0;if(!o||!s)return e===t?-1:n===t?1:o?-1:s?1:l?P.call(l,e)-P.call(l,n):0;if(o===s)return dt(e,n);r=e;while(r=r.parentNode)a.unshift(r);r=n;while(r=r.parentNode)u.unshift(r);while(a[i]===u[i])i++;return i?dt(a[i],u[i]):a[i]===b?-1:u[i]===b?1:0},t):p},ot.matches=function(e,t){return ot(e,null,null,t)},ot.matchesSelector=function(e,t){if((e.ownerDocument||e)!==p&&c(e),t=t.replace(Y,"='$1']"),!(!n.matchesSelector||!h||g&&g.test(t)||d&&d.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(i){}return ot(t,p,null,[e]).length>0},ot.contains=function(e,t){return(e.ownerDocument||e)!==p&&c(e),y(e,t)},ot.attr=function(e,t){(e.ownerDocument||e)!==p&&c(e);var r=i.attrHandle[t.toLowerCase()],o=r&&A.call(i.attrHandle,t.toLowerCase())?r(e,t,!h):undefined;return o===undefined?n.attributes||!h?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null:o},ot.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},ot.uniqueSort=function(e){var t,r=[],i=0,o=0;if(E=!n.detectDuplicates,l=!n.sortStable&&e.slice(0),e.sort(S),E){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return e},o=ot.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=o(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=o(t);return n},i=ot.selectors={cacheLength:50,createPseudo:ut,match:J,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(nt,rt),e[3]=(e[4]||e[5]||"").replace(nt,rt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||ot.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&ot.error(e[0]),e},PSEUDO:function(e){var t,n=!e[5]&&e[2];return J.CHILD.test(e[0])?null:(e[3]&&e[4]!==undefined?e[2]=e[4]:n&&V.test(n)&&(t=vt(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(nt,rt).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=C[e+" "];return t||(t=RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&C(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=ot.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),s="last"!==e.slice(-4),a="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,p,f,h,d,g=o!==s?"nextSibling":"previousSibling",m=t.parentNode,y=a&&t.nodeName.toLowerCase(),x=!u&&!a;if(m){if(o){while(g){p=t;while(p=p[g])if(a?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;d=g="only"===e&&!d&&"nextSibling"}return!0}if(d=[s?m.firstChild:m.lastChild],s&&x){c=m[v]||(m[v]={}),l=c[e]||[],h=l[0]===w&&l[1],f=l[0]===w&&l[2],p=h&&m.childNodes[h];while(p=++h&&p&&p[g]||(f=h=0)||d.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[w,h,f];break}}else if(x&&(l=(t[v]||(t[v]={}))[e])&&l[0]===w)f=l[1];else while(p=++h&&p&&p[g]||(f=h=0)||d.pop())if((a?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(x&&((p[v]||(p[v]={}))[e]=[w,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=i.pseudos[e]||i.setFilters[e.toLowerCase()]||ot.error("unsupported pseudo: "+e);return r[v]?r(t):r.length>1?(n=[e,e,"",t],i.setFilters.hasOwnProperty(e.toLowerCase())?ut(function(e,n){var i,o=r(e,t),s=o.length;while(s--)i=P.call(e,o[s]),e[i]=!(n[i]=o[s])}):function(e){return r(e,0,n)}):r}},pseudos:{not:ut(function(e){var t=[],n=[],r=a(e.replace(z,"$1"));return r[v]?ut(function(e,t,n,i){var o,s=r(e,null,i,[]),a=e.length;while(a--)(o=s[a])&&(e[a]=!(t[a]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:ut(function(e){return function(t){return ot(e,t).length>0}}),contains:ut(function(e){return function(t){return(t.textContent||t.innerText||o(t)).indexOf(e)>-1}}),lang:ut(function(e){return G.test(e||"")||ot.error("unsupported lang: "+e),e=e.replace(nt,rt).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===f},focus:function(e){return e===p.activeElement&&(!p.hasFocus||p.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!i.pseudos.empty(e)},header:function(e){return et.test(e.nodeName)},input:function(e){return Z.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:yt(function(){return[0]}),last:yt(function(e,t){return[t-1]}),eq:yt(function(e,t,n){return[0>n?n+t:n]}),even:yt(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:yt(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:yt(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:yt(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}};for(t in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})i.pseudos[t]=gt(t);for(t in{submit:!0,reset:!0})i.pseudos[t]=mt(t);function vt(e,t){var n,r,o,s,a,u,l,c=k[e+" "];if(c)return t?0:c.slice(0);a=e,u=[],l=i.preFilter;while(a){(!n||(r=_.exec(a)))&&(r&&(a=a.slice(r[0].length)||a),u.push(o=[])),n=!1,(r=X.exec(a))&&(n=r.shift(),o.push({value:n,type:r[0].replace(z," ")}),a=a.slice(n.length));for(s in i.filter)!(r=J[s].exec(a))||l[s]&&!(r=l[s](r))||(n=r.shift(),o.push({value:n,type:s,matches:r}),a=a.slice(n.length));if(!n)break}return t?a.length:a?ot.error(e):k(e,u).slice(0)}function xt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function bt(e,t,n){var i=t.dir,o=n&&"parentNode"===i,s=T++;return t.first?function(t,n,r){while(t=t[i])if(1===t.nodeType||o)return e(t,n,r)}:function(t,n,a){var u,l,c,p=w+" "+s;if(a){while(t=t[i])if((1===t.nodeType||o)&&e(t,n,a))return!0}else while(t=t[i])if(1===t.nodeType||o)if(c=t[v]||(t[v]={}),(l=c[i])&&l[0]===p){if((u=l[1])===!0||u===r)return u===!0}else if(l=c[i]=[p],l[1]=e(t,n,a)||r,l[1]===!0)return!0}}function wt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function Tt(e,t,n,r,i){var o,s=[],a=0,u=e.length,l=null!=t;for(;u>a;a++)(o=e[a])&&(!n||n(o,r,i))&&(s.push(o),l&&t.push(a));return s}function Ct(e,t,n,r,i,o){return r&&!r[v]&&(r=Ct(r)),i&&!i[v]&&(i=Ct(i,o)),ut(function(o,s,a,u){var l,c,p,f=[],h=[],d=s.length,g=o||Et(t||"*",a.nodeType?[a]:a,[]),m=!e||!o&&t?g:Tt(g,f,e,a,u),y=n?i||(o?e:d||r)?[]:s:m;if(n&&n(m,y,a,u),r){l=Tt(y,h),r(l,[],a,u),c=l.length;while(c--)(p=l[c])&&(y[h[c]]=!(m[h[c]]=p))}if(o){if(i||e){if(i){l=[],c=y.length;while(c--)(p=y[c])&&l.push(m[c]=p);i(null,y=[],l,u)}c=y.length;while(c--)(p=y[c])&&(l=i?P.call(o,p):f[c])>-1&&(o[l]=!(s[l]=p))}}else y=Tt(y===s?y.splice(d,y.length):y),i?i(null,s,y,u):O.apply(s,y)})}function kt(e){var t,n,r,o=e.length,s=i.relative[e[0].type],a=s||i.relative[" "],l=s?1:0,c=bt(function(e){return e===t},a,!0),p=bt(function(e){return P.call(t,e)>-1},a,!0),f=[function(e,n,r){return!s&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;o>l;l++)if(n=i.relative[e[l].type])f=[bt(wt(f),n)];else{if(n=i.filter[e[l].type].apply(null,e[l].matches),n[v]){for(r=++l;o>r;r++)if(i.relative[e[r].type])break;return Ct(l>1&&wt(f),l>1&&xt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&kt(e.slice(l,r)),o>r&&kt(e=e.slice(r)),o>r&&xt(e))}f.push(n)}return wt(f)}function Nt(e,t){var n=0,o=t.length>0,s=e.length>0,a=function(a,l,c,f,h){var d,g,m,y=[],v=0,x="0",b=a&&[],T=null!=h,C=u,k=a||s&&i.find.TAG("*",h&&l.parentNode||l),N=w+=null==C?1:Math.random()||.1;for(T&&(u=l!==p&&l,r=n);null!=(d=k[x]);x++){if(s&&d){g=0;while(m=e[g++])if(m(d,l,c)){f.push(d);break}T&&(w=N,r=++n)}o&&((d=!m&&d)&&v--,a&&b.push(d))}if(v+=x,o&&x!==v){g=0;while(m=t[g++])m(b,y,l,c);if(a){if(v>0)while(x--)b[x]||y[x]||(y[x]=H.call(f));y=Tt(y)}O.apply(f,y),T&&!a&&y.length>0&&v+t.length>1&&ot.uniqueSort(f)}return T&&(w=N,u=C),b};return o?ut(a):a}a=ot.compile=function(e,t){var n,r=[],i=[],o=N[e+" "];if(!o){t||(t=vt(e)),n=t.length;while(n--)o=kt(t[n]),o[v]?r.push(o):i.push(o);o=N(e,Nt(i,r))}return o};function Et(e,t,n){var r=0,i=t.length;for(;i>r;r++)ot(e,t[r],n);return n}function St(e,t,r,o){var s,u,l,c,p,f=vt(e);if(!o&&1===f.length){if(u=f[0]=f[0].slice(0),u.length>2&&"ID"===(l=u[0]).type&&n.getById&&9===t.nodeType&&h&&i.relative[u[1].type]){if(t=(i.find.ID(l.matches[0].replace(nt,rt),t)||[])[0],!t)return r;e=e.slice(u.shift().value.length)}s=J.needsContext.test(e)?0:u.length;while(s--){if(l=u[s],i.relative[c=l.type])break;if((p=i.find[c])&&(o=p(l.matches[0].replace(nt,rt),U.test(u[0].type)&&t.parentNode||t))){if(u.splice(s,1),e=o.length&&xt(u),!e)return O.apply(r,o),r;break}}}return a(e,f)(o,t,!h,r,U.test(e)),r}i.pseudos.nth=i.pseudos.eq;function jt(){}jt.prototype=i.filters=i.pseudos,i.setFilters=new jt,n.sortStable=v.split("").sort(S).join("")===v,c(),[0,0].sort(S),n.detectDuplicates=E,x.find=ot,x.expr=ot.selectors,x.expr[":"]=x.expr.pseudos,x.unique=ot.uniqueSort,x.text=ot.getText,x.isXMLDoc=ot.isXML,x.contains=ot.contains}(e);var D={};function A(e){var t=D[e]={};return x.each(e.match(w)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?D[e]||A(e):x.extend({},e);var t,n,r,i,o,s,a=[],u=!e.once&&[],l=function(p){for(t=e.memory&&p,n=!0,s=i||0,i=0,o=a.length,r=!0;a&&o>s;s++)if(a[s].apply(p[0],p[1])===!1&&e.stopOnFalse){t=!1;break}r=!1,a&&(u?u.length&&l(u.shift()):t?a=[]:c.disable())},c={add:function(){if(a){var n=a.length;(function s(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&c.has(n)||a.push(n):n&&n.length&&"string"!==r&&s(n)})})(arguments),r?o=a.length:t&&(i=n,l(t))}return this},remove:function(){return a&&x.each(arguments,function(e,t){var n;while((n=x.inArray(t,a,n))>-1)a.splice(n,1),r&&(o>=n&&o--,s>=n&&s--)}),this},has:function(e){return e?x.inArray(e,a)>-1:!(!a||!a.length)},empty:function(){return a=[],o=0,this},disable:function(){return a=u=t=undefined,this},disabled:function(){return!a},lock:function(){return u=undefined,t||c.disable(),this},locked:function(){return!u},fireWith:function(e,t){return t=t||[],t=[e,t.slice?t.slice():t],!a||n&&!u||(r?u.push(t):l(t)),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!n}};return c},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var s=o[0],a=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=a&&a.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[s+"With"](this===r?n.promise():this,a?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var s=o[2],a=o[3];r[o[1]]=s.add,a&&s.add(function(){n=a},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=s.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=d.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),s=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?d.call(arguments):r,n===a?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},a,u,l;if(r>1)for(a=Array(r),u=Array(r),l=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(s(t,l,n)).fail(o.reject).progress(s(t,u,a)):--i;return i||o.resolveWith(l,n),o.promise()}}),x.support=function(t){var n=o.createElement("input"),r=o.createDocumentFragment(),i=o.createElement("div"),s=o.createElement("select"),a=s.appendChild(o.createElement("option"));return n.type?(n.type="checkbox",t.checkOn=""!==n.value,t.optSelected=a.selected,t.reliableMarginRight=!0,t.boxSizingReliable=!0,t.pixelPosition=!1,n.checked=!0,t.noCloneChecked=n.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!a.disabled,n=o.createElement("input"),n.value="t",n.type="radio",t.radioValue="t"===n.value,n.setAttribute("checked","t"),n.setAttribute("name","t"),r.appendChild(n),t.checkClone=r.cloneNode(!0).cloneNode(!0).lastChild.checked,t.focusinBubbles="onfocusin"in e,i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===i.style.backgroundClip,x(function(){var n,r,s="padding:0;margin:0;border:0;display:block;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box",a=o.getElementsByTagName("body")[0];a&&(n=o.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",a.appendChild(n).appendChild(i),i.innerHTML="",i.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%",x.swap(a,null!=a.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===i.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(i,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(i,null)||{width:"4px"}).width,r=i.appendChild(o.createElement("div")),r.style.cssText=i.style.cssText=s,r.style.marginRight=r.style.width="0",i.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),a.removeChild(n))}),t):t}({});var L,H,q=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,O=/([A-Z])/g;function F(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=x.expando+Math.random()}F.uid=1,F.accepts=function(e){return e.nodeType?1===e.nodeType||9===e.nodeType:!0},F.prototype={key:function(e){if(!F.accepts(e))return 0;var t={},n=e[this.expando];if(!n){n=F.uid++;try{t[this.expando]={value:n},Object.defineProperties(e,t)}catch(r){t[this.expando]=n,x.extend(e,t)}}return this.cache[n]||(this.cache[n]={}),n},set:function(e,t,n){var r,i=this.key(e),o=this.cache[i];if("string"==typeof t)o[t]=n;else if(x.isEmptyObject(o))x.extend(this.cache[i],t);else for(r in t)o[r]=t[r];return o},get:function(e,t){var n=this.cache[this.key(e)];return t===undefined?n:n[t]},access:function(e,t,n){return t===undefined||t&&"string"==typeof t&&n===undefined?this.get(e,t):(this.set(e,t,n),n!==undefined?n:t)},remove:function(e,t){var n,r,i,o=this.key(e),s=this.cache[o];if(t===undefined)this.cache[o]={};else{x.isArray(t)?r=t.concat(t.map(x.camelCase)):(i=x.camelCase(t),t in s?r=[t,i]:(r=i,r=r in s?[r]:r.match(w)||[])),n=r.length;while(n--)delete s[r[n]]}},hasData:function(e){return!x.isEmptyObject(this.cache[e[this.expando]]||{})},discard:function(e){e[this.expando]&&delete this.cache[e[this.expando]]}},L=new F,H=new F,x.extend({acceptData:F.accepts,hasData:function(e){return L.hasData(e)||H.hasData(e)},data:function(e,t,n){return L.access(e,t,n)},removeData:function(e,t){L.remove(e,t)},_data:function(e,t,n){return H.access(e,t,n)},_removeData:function(e,t){H.remove(e,t)}}),x.fn.extend({data:function(e,t){var n,r,i=this[0],o=0,s=null;if(e===undefined){if(this.length&&(s=L.get(i),1===i.nodeType&&!H.get(i,"hasDataAttrs"))){for(n=i.attributes;n.length>o;o++)r=n[o].name,0===r.indexOf("data-")&&(r=x.camelCase(r.slice(5)),P(i,r,s[r]));H.set(i,"hasDataAttrs",!0)}return s}return"object"==typeof e?this.each(function(){L.set(this,e)}):x.access(this,function(t){var n,r=x.camelCase(e);if(i&&t===undefined){if(n=L.get(i,e),n!==undefined)return n;if(n=L.get(i,r),n!==undefined)return n;if(n=P(i,r,undefined),n!==undefined)return n}else this.each(function(){var n=L.get(this,r);L.set(this,r,t),-1!==e.indexOf("-")&&n!==undefined&&L.set(this,e,t)})},null,t,arguments.length>1,null,!0)},removeData:function(e){return this.each(function(){L.remove(this,e)})}});function P(e,t,n){var r;if(n===undefined&&1===e.nodeType)if(r="data-"+t.replace(O,"-$1").toLowerCase(),n=e.getAttribute(r),"string"==typeof n){try{n="true"===n?!0:"false"===n?!1:"null"===n?null:+n+""===n?+n:q.test(n)?JSON.parse(n):n}catch(i){}L.set(e,t,n)}else n=undefined;return n}x.extend({queue:function(e,t,n){var r;return e?(t=(t||"fx")+"queue",r=H.get(e,t),n&&(!r||x.isArray(n)?r=H.access(e,t,x.makeArray(n)):r.push(n)),r||[]):undefined},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),s=function(){x.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,s,o)),!r&&o&&o.empty.fire() -},_queueHooks:function(e,t){var n=t+"queueHooks";return H.get(e,n)||H.access(e,n,{empty:x.Callbacks("once memory").add(function(){H.remove(e,[t+"queue",n])})})}}),x.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),n>arguments.length?x.queue(this[0],e):t===undefined?this:this.each(function(){var n=x.queue(this,e,t);x._queueHooks(this,e),"fx"===e&&"inprogress"!==n[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=x.Deferred(),o=this,s=this.length,a=function(){--r||i.resolveWith(o,[o])};"string"!=typeof e&&(t=e,e=undefined),e=e||"fx";while(s--)n=H.get(o[s],e+"queueHooks"),n&&n.empty&&(r++,n.empty.add(a));return a(),i.promise(t)}});var R,M,W=/[\t\r\n\f]/g,$=/\r/g,B=/^(?:input|select|textarea|button)$/i;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[x.propFix[e]||e]})},addClass:function(e){var t,n,r,i,o,s=0,a=this.length,u="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];a>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(W," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,s=0,a=this.length,u=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];a>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(W," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e,i="boolean"==typeof t;return x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var o,s=0,a=x(this),u=t,l=e.match(w)||[];while(o=l[s++])u=i?u:!a.hasClass(o),a[u?"addClass":"removeClass"](o)}else(n===r||"boolean"===n)&&(this.className&&H.set(this,"__className__",this.className),this.className=this.className||e===!1?"":H.get(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(W," ").indexOf(t)>=0)return!0;return!1},val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=x.isFunction(e),this.each(function(n){var i;1===this.nodeType&&(i=r?e.call(this,n,x(this).val()):e,null==i?i="":"number"==typeof i?i+="":x.isArray(i)&&(i=x.map(i,function(e){return null==e?"":e+""})),t=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],t&&"set"in t&&t.set(this,i,"value")!==undefined||(this.value=i))});if(i)return t=x.valHooks[i.type]||x.valHooks[i.nodeName.toLowerCase()],t&&"get"in t&&(n=t.get(i,"value"))!==undefined?n:(n=i.value,"string"==typeof n?n.replace($,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=e.attributes.value;return!t||t.specified?e.value:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,s=o?null:[],a=o?i+1:r.length,u=0>i?a:o?i:0;for(;a>u;u++)if(n=r[u],!(!n.selected&&u!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;s.push(t)}return s},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),s=i.length;while(s--)r=i[s],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,t,n){var i,o,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===r?x.prop(e,t,n):(1===s&&x.isXMLDoc(e)||(t=t.toLowerCase(),i=x.attrHooks[t]||(x.expr.match.bool.test(t)?M:R)),n===undefined?i&&"get"in i&&null!==(o=i.get(e,t))?o:(o=x.find.attr(e,t),null==o?undefined:o):null!==n?i&&"set"in i&&(o=i.set(e,n,t))!==undefined?o:(e.setAttribute(t,n+""),n):(x.removeAttr(e,t),undefined))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(w);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)&&(e[r]=!1),e.removeAttribute(n)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,t,n){var r,i,o,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return o=1!==s||!x.isXMLDoc(e),o&&(t=x.propFix[t]||t,i=x.propHooks[t]),n!==undefined?i&&"set"in i&&(r=i.set(e,n,t))!==undefined?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){return e.hasAttribute("tabindex")||B.test(e.nodeName)||e.href?e.tabIndex:-1}}}}),M={set:function(e,t,n){return t===!1?x.removeAttr(e,n):e.setAttribute(n,n),n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,t){var n=x.expr.attrHandle[t]||x.find.attr;x.expr.attrHandle[t]=function(e,t,r){var i=x.expr.attrHandle[t],o=r?undefined:(x.expr.attrHandle[t]=undefined)!=n(e,t,r)?t.toLowerCase():null;return x.expr.attrHandle[t]=i,o}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,t){return x.isArray(t)?e.checked=x.inArray(x(e).val(),t)>=0:undefined}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var I=/^key/,z=/^(?:mouse|contextmenu)|click/,_=/^(?:focusinfocus|focusoutblur)$/,X=/^([^.]*)(?:\.(.+)|)$/;function U(){return!0}function Y(){return!1}function V(){try{return o.activeElement}catch(e){}}x.event={global:{},add:function(e,t,n,i,o){var s,a,u,l,c,p,f,h,d,g,m,y=H.get(e);if(y){n.handler&&(s=n,n=s.handler,o=s.selector),n.guid||(n.guid=x.guid++),(l=y.events)||(l=y.events={}),(a=y.handle)||(a=y.handle=function(e){return typeof x===r||e&&x.event.triggered===e.type?undefined:x.event.dispatch.apply(a.elem,arguments)},a.elem=e),t=(t||"").match(w)||[""],c=t.length;while(c--)u=X.exec(t[c])||[],d=m=u[1],g=(u[2]||"").split(".").sort(),d&&(f=x.event.special[d]||{},d=(o?f.delegateType:f.bindType)||d,f=x.event.special[d]||{},p=x.extend({type:d,origType:m,data:i,handler:n,guid:n.guid,selector:o,needsContext:o&&x.expr.match.needsContext.test(o),namespace:g.join(".")},s),(h=l[d])||(h=l[d]=[],h.delegateCount=0,f.setup&&f.setup.call(e,i,g,a)!==!1||e.addEventListener&&e.addEventListener(d,a,!1)),f.add&&(f.add.call(e,p),p.handler.guid||(p.handler.guid=n.guid)),o?h.splice(h.delegateCount++,0,p):h.push(p),x.event.global[d]=!0);e=null}},remove:function(e,t,n,r,i){var o,s,a,u,l,c,p,f,h,d,g,m=H.hasData(e)&&H.get(e);if(m&&(u=m.events)){t=(t||"").match(w)||[""],l=t.length;while(l--)if(a=X.exec(t[l])||[],h=g=a[1],d=(a[2]||"").split(".").sort(),h){p=x.event.special[h]||{},h=(r?p.delegateType:p.bindType)||h,f=u[h]||[],a=a[2]&&RegExp("(^|\\.)"+d.join("\\.(?:.*\\.|)")+"(\\.|$)"),s=o=f.length;while(o--)c=f[o],!i&&g!==c.origType||n&&n.guid!==c.guid||a&&!a.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(f.splice(o,1),c.selector&&f.delegateCount--,p.remove&&p.remove.call(e,c));s&&!f.length&&(p.teardown&&p.teardown.call(e,d,m.handle)!==!1||x.removeEvent(e,h,m.handle),delete u[h])}else for(h in u)x.event.remove(e,h+t[l],n,r,!0);x.isEmptyObject(u)&&(delete m.handle,H.remove(e,"events"))}},trigger:function(t,n,r,i){var s,a,u,l,c,p,f,h=[r||o],d=y.call(t,"type")?t.type:t,g=y.call(t,"namespace")?t.namespace.split("."):[];if(a=u=r=r||o,3!==r.nodeType&&8!==r.nodeType&&!_.test(d+x.event.triggered)&&(d.indexOf(".")>=0&&(g=d.split("."),d=g.shift(),g.sort()),c=0>d.indexOf(":")&&"on"+d,t=t[x.expando]?t:new x.Event(d,"object"==typeof t&&t),t.isTrigger=i?2:3,t.namespace=g.join("."),t.namespace_re=t.namespace?RegExp("(^|\\.)"+g.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=undefined,t.target||(t.target=r),n=null==n?[t]:x.makeArray(n,[t]),f=x.event.special[d]||{},i||!f.trigger||f.trigger.apply(r,n)!==!1)){if(!i&&!f.noBubble&&!x.isWindow(r)){for(l=f.delegateType||d,_.test(l+d)||(a=a.parentNode);a;a=a.parentNode)h.push(a),u=a;u===(r.ownerDocument||o)&&h.push(u.defaultView||u.parentWindow||e)}s=0;while((a=h[s++])&&!t.isPropagationStopped())t.type=s>1?l:f.bindType||d,p=(H.get(a,"events")||{})[t.type]&&H.get(a,"handle"),p&&p.apply(a,n),p=c&&a[c],p&&x.acceptData(a)&&p.apply&&p.apply(a,n)===!1&&t.preventDefault();return t.type=d,i||t.isDefaultPrevented()||f._default&&f._default.apply(h.pop(),n)!==!1||!x.acceptData(r)||c&&x.isFunction(r[d])&&!x.isWindow(r)&&(u=r[c],u&&(r[c]=null),x.event.triggered=d,r[d](),x.event.triggered=undefined,u&&(r[c]=u)),t.result}},dispatch:function(e){e=x.event.fix(e);var t,n,r,i,o,s=[],a=d.call(arguments),u=(H.get(this,"events")||{})[e.type]||[],l=x.event.special[e.type]||{};if(a[0]=e,e.delegateTarget=this,!l.preDispatch||l.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),t=0;while((i=s[t++])&&!e.isPropagationStopped()){e.currentTarget=i.elem,n=0;while((o=i.handlers[n++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(o.namespace))&&(e.handleObj=o,e.data=o.data,r=((x.event.special[o.origType]||{}).handle||o.handler).apply(i.elem,a),r!==undefined&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return l.postDispatch&&l.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,i,o,s=[],a=t.delegateCount,u=e.target;if(a&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!==this;u=u.parentNode||this)if(u.disabled!==!0||"click"!==e.type){for(r=[],n=0;a>n;n++)o=t[n],i=o.selector+" ",r[i]===undefined&&(r[i]=o.needsContext?x(i,this).index(u)>=0:x.find(i,this,null,[u]).length),r[i]&&r.push(o);r.length&&s.push({elem:u,handlers:r})}return t.length>a&&s.push({elem:this,handlers:t.slice(a)}),s},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,t){var n,r,i,s=t.button;return null==e.pageX&&null!=t.clientX&&(n=e.target.ownerDocument||o,r=n.documentElement,i=n.body,e.pageX=t.clientX+(r&&r.scrollLeft||i&&i.scrollLeft||0)-(r&&r.clientLeft||i&&i.clientLeft||0),e.pageY=t.clientY+(r&&r.scrollTop||i&&i.scrollTop||0)-(r&&r.clientTop||i&&i.clientTop||0)),e.which||s===undefined||(e.which=1&s?1:2&s?3:4&s?2:0),e}},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,s=e,a=this.fixHooks[i];a||(this.fixHooks[i]=a=z.test(i)?this.mouseHooks:I.test(i)?this.keyHooks:{}),r=a.props?this.props.concat(a.props):this.props,e=new x.Event(s),t=r.length;while(t--)n=r[t],e[n]=s[n];return e.target||(e.target=o),3===e.target.nodeType&&(e.target=e.target.parentNode),a.filter?a.filter(e,s):e},special:{load:{noBubble:!0},focus:{trigger:function(){return this!==V()&&this.focus?(this.focus(),!1):undefined},delegateType:"focusin"},blur:{trigger:function(){return this===V()&&this.blur?(this.blur(),!1):undefined},delegateType:"focusout"},click:{trigger:function(){return"checkbox"===this.type&&this.click&&x.nodeName(this,"input")?(this.click(),!1):undefined},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==undefined&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)},x.Event=function(e,t){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.getPreventDefault&&e.getPreventDefault()?U:Y):this.type=e,t&&x.extend(this,t),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,undefined):new x.Event(e,t)},x.Event.prototype={isDefaultPrevented:Y,isPropagationStopped:Y,isImmediatePropagationStopped:Y,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=U,e&&e.preventDefault&&e.preventDefault()},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=U,e&&e.stopPropagation&&e.stopPropagation()},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=U,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&o.addEventListener(e,r,!0)},teardown:function(){0===--n&&o.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,t,n,r,i){var o,s;if("object"==typeof e){"string"!=typeof t&&(n=n||t,t=undefined);for(s in e)this.on(s,t,n,e[s],i);return this}if(null==n&&null==r?(r=t,n=t=undefined):null==r&&("string"==typeof t?(r=n,n=undefined):(r=n,n=t,t=undefined)),r===!1)r=Y;else if(!r)return this;return 1===i&&(o=r,r=function(e){return x().off(e),o.apply(this,arguments)},r.guid=o.guid||(o.guid=x.guid++)),this.each(function(){x.event.add(this,e,r,n,t)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,x(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return(t===!1||"function"==typeof t)&&(n=t,t=undefined),n===!1&&(n=Y),this.each(function(){x.event.remove(this,e,n,t)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];return n?x.event.trigger(e,t,n,!0):undefined}});var G=/^.[^:#\[\.,]*$/,J=/^(?:parents|prev(?:Until|All))/,Q=x.expr.match.needsContext,K={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t=x(e,this),n=t.length;return this.filter(function(){var e=0;for(;n>e;e++)if(x.contains(this,t[e]))return!0})},not:function(e){return this.pushStack(et(this,e||[],!0))},filter:function(e){return this.pushStack(et(this,e||[],!1))},is:function(e){return!!et(this,"string"==typeof e&&Q.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],s=Q.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(s?s.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?g.call(x(e),this[0]):g.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function Z(e,t){while((e=e[t])&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return Z(e,"nextSibling")},prev:function(e){return Z(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return e.contentDocument||x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(K[e]||x.unique(i),J.test(e)&&i.reverse()),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,t,n){var r=[],i=n!==undefined;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&x(e).is(n))break;r.push(e)}return r},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function et(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(G.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return g.call(t,e)>=0!==n})}var tt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,nt=/<([\w:]+)/,rt=/<|&#?\w+;/,it=/<(?:script|style|link)/i,ot=/^(?:checkbox|radio)$/i,st=/checked\s*(?:[^=]|=\s*.checked.)/i,at=/^$|\/(?:java|ecma)script/i,ut=/^true\/(.*)/,lt=/^\s*\s*$/g,ct={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ct.optgroup=ct.option,ct.tbody=ct.tfoot=ct.colgroup=ct.caption=ct.thead,ct.th=ct.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===undefined?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||o).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=pt(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=pt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(mt(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&dt(mt(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++)1===e.nodeType&&(x.cleanData(mt(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var t=this[0]||{},n=0,r=this.length;if(e===undefined&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!it.test(e)&&!ct[(nt.exec(e)||["",""])[1].toLowerCase()]){e=e.replace(tt,"<$1>");try{for(;r>n;n++)t=this[n]||{},1===t.nodeType&&(x.cleanData(mt(t,!1)),t.innerHTML=e);t=0}catch(i){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=f.apply([],e);var r,i,o,s,a,u,l=0,c=this.length,p=this,h=c-1,d=e[0],g=x.isFunction(d);if(g||!(1>=c||"string"!=typeof d||x.support.checkClone)&&st.test(d))return this.each(function(r){var i=p.eq(r);g&&(e[0]=d.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(r=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),i=r.firstChild,1===r.childNodes.length&&(r=i),i)){for(o=x.map(mt(r,"script"),ft),s=o.length;c>l;l++)a=r,l!==h&&(a=x.clone(a,!0,!0),s&&x.merge(o,mt(a,"script"))),t.call(this[l],a,l);if(s)for(u=o[o.length-1].ownerDocument,x.map(o,ht),l=0;s>l;l++)a=o[l],at.test(a.type||"")&&!H.access(a,"globalEval")&&x.contains(u,a)&&(a.src?x._evalUrl(a.src):x.globalEval(a.textContent.replace(lt,"")))}return this}}),x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=[],i=x(e),o=i.length-1,s=0;for(;o>=s;s++)n=s===o?this:this.clone(!0),x(i[s])[t](n),h.apply(r,n.get());return this.pushStack(r)}}),x.extend({clone:function(e,t,n){var r,i,o,s,a=e.cloneNode(!0),u=x.contains(e.ownerDocument,e);if(!(x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(s=mt(a),o=mt(e),r=0,i=o.length;i>r;r++)yt(o[r],s[r]);if(t)if(n)for(o=o||mt(e),s=s||mt(a),r=0,i=o.length;i>r;r++)gt(o[r],s[r]);else gt(e,a);return s=mt(a,"script"),s.length>0&&dt(s,!u&&mt(e,"script")),a},buildFragment:function(e,t,n,r){var i,o,s,a,u,l,c=0,p=e.length,f=t.createDocumentFragment(),h=[];for(;p>c;c++)if(i=e[c],i||0===i)if("object"===x.type(i))x.merge(h,i.nodeType?[i]:i);else if(rt.test(i)){o=o||f.appendChild(t.createElement("div")),s=(nt.exec(i)||["",""])[1].toLowerCase(),a=ct[s]||ct._default,o.innerHTML=a[1]+i.replace(tt,"<$1>")+a[2],l=a[0];while(l--)o=o.firstChild;x.merge(h,o.childNodes),o=f.firstChild,o.textContent=""}else h.push(t.createTextNode(i));f.textContent="",c=0;while(i=h[c++])if((!r||-1===x.inArray(i,r))&&(u=x.contains(i.ownerDocument,i),o=mt(f.appendChild(i),"script"),u&&dt(o),n)){l=0;while(i=o[l++])at.test(i.type||"")&&n.push(i)}return f},cleanData:function(e){var t,n,r,i,o,s,a=x.event.special,u=0;for(;(n=e[u])!==undefined;u++){if(F.accepts(n)&&(o=n[H.expando],o&&(t=H.cache[o]))){if(r=Object.keys(t.events||{}),r.length)for(s=0;(i=r[s])!==undefined;s++)a[i]?x.event.remove(n,i):x.removeEvent(n,i,t.handle);H.cache[o]&&delete H.cache[o]}delete L.cache[n[L.expando]]}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})}});function pt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function ft(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function ht(e){var t=ut.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function dt(e,t){var n=e.length,r=0;for(;n>r;r++)H.set(e[r],"globalEval",!t||H.get(t[r],"globalEval"))}function gt(e,t){var n,r,i,o,s,a,u,l;if(1===t.nodeType){if(H.hasData(e)&&(o=H.access(e),s=H.set(t,o),l=o.events)){delete s.handle,s.events={};for(i in l)for(n=0,r=l[i].length;r>n;n++)x.event.add(t,i,l[i][n])}L.hasData(e)&&(a=L.access(e),u=x.extend({},a),L.set(t,u))}}function mt(e,t){var n=e.getElementsByTagName?e.getElementsByTagName(t||"*"):e.querySelectorAll?e.querySelectorAll(t||"*"):[];return t===undefined||t&&x.nodeName(e,t)?x.merge([e],n):n}function yt(e,t){var n=t.nodeName.toLowerCase();"input"===n&&ot.test(e.type)?t.checked=e.checked:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}x.fn.extend({wrapAll:function(e){var t;return x.isFunction(e)?this.each(function(t){x(this).wrapAll(e.call(this,t))}):(this[0]&&(t=x(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this)},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var vt,xt,bt=/^(none|table(?!-c[ea]).+)/,wt=/^margin/,Tt=RegExp("^("+b+")(.*)$","i"),Ct=RegExp("^("+b+")(?!px)[a-z%]+$","i"),kt=RegExp("^([+-])=("+b+")","i"),Nt={BODY:"block"},Et={position:"absolute",visibility:"hidden",display:"block"},St={letterSpacing:0,fontWeight:400},jt=["Top","Right","Bottom","Left"],Dt=["Webkit","O","Moz","ms"];function At(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=Dt.length;while(i--)if(t=Dt[i]+n,t in e)return t;return r}function Lt(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function Ht(t){return e.getComputedStyle(t,null)}function qt(e,t){var n,r,i,o=[],s=0,a=e.length;for(;a>s;s++)r=e[s],r.style&&(o[s]=H.get(r,"olddisplay"),n=r.style.display,t?(o[s]||"none"!==n||(r.style.display=""),""===r.style.display&&Lt(r)&&(o[s]=H.access(r,"olddisplay",Rt(r.nodeName)))):o[s]||(i=Lt(r),(n&&"none"!==n||!i)&&H.set(r,"olddisplay",i?n:x.css(r,"display"))));for(s=0;a>s;s++)r=e[s],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[s]||"":"none"));return e}x.fn.extend({css:function(e,t){return x.access(this,function(e,t,n){var r,i,o={},s=0;if(x.isArray(t)){for(r=Ht(e),i=t.length;i>s;s++)o[t[s]]=x.css(e,t[s],!1,r);return o}return n!==undefined?x.style(e,t,n):x.css(e,t)},e,t,arguments.length>1)},show:function(){return qt(this,!0)},hide:function(){return qt(this)},toggle:function(e){var t="boolean"==typeof e;return this.each(function(){(t?e:Lt(this))?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=vt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,s,a=x.camelCase(t),u=e.style;return t=x.cssProps[a]||(x.cssProps[a]=At(u,a)),s=x.cssHooks[t]||x.cssHooks[a],n===undefined?s&&"get"in s&&(i=s.get(e,!1,r))!==undefined?i:u[t]:(o=typeof n,"string"===o&&(i=kt.exec(n))&&(n=(i[1]+1)*i[2]+parseFloat(x.css(e,t)),o="number"),null==n||"number"===o&&isNaN(n)||("number"!==o||x.cssNumber[a]||(n+="px"),x.support.clearCloneStyle||""!==n||0!==t.indexOf("background")||(u[t]="inherit"),s&&"set"in s&&(n=s.set(e,n,r))===undefined||(u[t]=n)),undefined)}},css:function(e,t,n,r){var i,o,s,a=x.camelCase(t);return t=x.cssProps[a]||(x.cssProps[a]=At(e.style,a)),s=x.cssHooks[t]||x.cssHooks[a],s&&"get"in s&&(i=s.get(e,!0,n)),i===undefined&&(i=vt(e,t,r)),"normal"===i&&t in St&&(i=St[t]),""===n||n?(o=parseFloat(i),n===!0||x.isNumeric(o)?o||0:i):i}}),vt=function(e,t,n){var r,i,o,s=n||Ht(e),a=s?s.getPropertyValue(t)||s[t]:undefined,u=e.style;return s&&(""!==a||x.contains(e.ownerDocument,e)||(a=x.style(e,t)),Ct.test(a)&&wt.test(t)&&(r=u.width,i=u.minWidth,o=u.maxWidth,u.minWidth=u.maxWidth=u.width=a,a=s.width,u.width=r,u.minWidth=i,u.maxWidth=o)),a};function Ot(e,t,n){var r=Tt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function Ft(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,s=0;for(;4>o;o+=2)"margin"===n&&(s+=x.css(e,n+jt[o],!0,i)),r?("content"===n&&(s-=x.css(e,"padding"+jt[o],!0,i)),"margin"!==n&&(s-=x.css(e,"border"+jt[o]+"Width",!0,i))):(s+=x.css(e,"padding"+jt[o],!0,i),"padding"!==n&&(s+=x.css(e,"border"+jt[o]+"Width",!0,i)));return s}function Pt(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Ht(e),s=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=vt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Ct.test(i))return i;r=s&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+Ft(e,t,n||(s?"border":"content"),r,o)+"px"}function Rt(e){var t=o,n=Nt[e];return n||(n=Mt(e,t),"none"!==n&&n||(xt=(xt||x("