diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml
index 6e1e38d0b2..ac7279117a 100644
--- a/.github/workflows/test_build.yml
+++ b/.github/workflows/test_build.yml
@@ -37,6 +37,7 @@ jobs:
- name: 🔨 Build
shell: pwsh
run: |
+ $env:SKIP_THIRD_PARTY_VALIDATION="1"
./tools/build.ps1
Ubuntu-latest:
@@ -61,6 +62,7 @@ jobs:
- name: 🔨 Build
run: |
+ export SKIP_THIRD_PARTY_VALIDATION="1"
./tools/build.sh
# MacOS-latest:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 20ab087690..d1b390da5e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,48 +1,67 @@
# Changelog
-## [3.8.0-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD)
+## [3.8.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.7.0...HEAD)
+### 📖 Documentation
+
+- Renamed to proper name [\#2546](https://github.com/pypeclub/OpenPype/pull/2546)
+- Slack: Add review to notification message [\#2498](https://github.com/pypeclub/OpenPype/pull/2498)
+
**🆕 New features**
- Flame: OpenTimelineIO Export Modul [\#2398](https://github.com/pypeclub/OpenPype/pull/2398)
**🚀 Enhancements**
+- Settings: PathInput strip passed string [\#2550](https://github.com/pypeclub/OpenPype/pull/2550)
+- General: Validate if current process OpenPype version is requested version [\#2529](https://github.com/pypeclub/OpenPype/pull/2529)
+- General: Be able to use anatomy data in ffmpeg output arguments [\#2525](https://github.com/pypeclub/OpenPype/pull/2525)
+- Expose toggle publish plug-in settings for Maya Look Shading Engine Naming [\#2521](https://github.com/pypeclub/OpenPype/pull/2521)
+- Photoshop: Move implementation to OpenPype [\#2510](https://github.com/pypeclub/OpenPype/pull/2510)
+- TimersManager: Move module one hierarchy higher [\#2501](https://github.com/pypeclub/OpenPype/pull/2501)
+- Slack: notifications are sent with Openpype logo and bot name [\#2499](https://github.com/pypeclub/OpenPype/pull/2499)
- Ftrack: Event handlers settings [\#2496](https://github.com/pypeclub/OpenPype/pull/2496)
+- Flame - create publishable clips [\#2495](https://github.com/pypeclub/OpenPype/pull/2495)
- Tools: Fix style and modality of errors in loader and creator [\#2489](https://github.com/pypeclub/OpenPype/pull/2489)
+- Project Manager: Remove project button cleanup [\#2482](https://github.com/pypeclub/OpenPype/pull/2482)
- Tools: Be able to change models of tasks and assets widgets [\#2475](https://github.com/pypeclub/OpenPype/pull/2475)
- Publish pype: Reduce publish process defering [\#2464](https://github.com/pypeclub/OpenPype/pull/2464)
- Maya: Improve speed of Collect History logic [\#2460](https://github.com/pypeclub/OpenPype/pull/2460)
- Maya: Validate Rig Controllers - fix Error: in script editor [\#2459](https://github.com/pypeclub/OpenPype/pull/2459)
- Maya: Optimize Validate Locked Normals speed for dense polymeshes [\#2457](https://github.com/pypeclub/OpenPype/pull/2457)
+- Fix \#2453 Refactor missing \_get\_reference\_node method [\#2455](https://github.com/pypeclub/OpenPype/pull/2455)
+- Houdini: Remove broken unique name counter [\#2450](https://github.com/pypeclub/OpenPype/pull/2450)
+- Maya: Improve lib.polyConstraint performance when Select tool is not the active tool context [\#2447](https://github.com/pypeclub/OpenPype/pull/2447)
+- General: Validate third party before build [\#2425](https://github.com/pypeclub/OpenPype/pull/2425)
- Maya : add option to not group reference in ReferenceLoader [\#2383](https://github.com/pypeclub/OpenPype/pull/2383)
**🛠Bug fixes**
+- Fix published frame content for sequence starting with 0 [\#2513](https://github.com/pypeclub/OpenPype/pull/2513)
+- Fix \#2497: reset empty string attributes correctly to "" instead of "None" [\#2506](https://github.com/pypeclub/OpenPype/pull/2506)
- General: Settings work if OpenPypeVersion is available [\#2494](https://github.com/pypeclub/OpenPype/pull/2494)
+- General: PYTHONPATH may break OpenPype dependencies [\#2493](https://github.com/pypeclub/OpenPype/pull/2493)
- Workfiles tool: Files widget show files on first show [\#2488](https://github.com/pypeclub/OpenPype/pull/2488)
- General: Custom template paths filter fix [\#2483](https://github.com/pypeclub/OpenPype/pull/2483)
- Loader: Remove always on top flag in tray [\#2480](https://github.com/pypeclub/OpenPype/pull/2480)
- General: Anatomy does not return root envs as unicode [\#2465](https://github.com/pypeclub/OpenPype/pull/2465)
-- Webpublisher: Fix assignment of families of TVpaint instances [\#2373](https://github.com/pypeclub/OpenPype/pull/2373)
+- Maya: Validate Shape Zero do not keep fixed geometry vertices selected/active after repair [\#2456](https://github.com/pypeclub/OpenPype/pull/2456)
**Merged pull requests:**
+- General: Fix install thread in igniter [\#2549](https://github.com/pypeclub/OpenPype/pull/2549)
+- AfterEffects: Move implementation to OpenPype [\#2543](https://github.com/pypeclub/OpenPype/pull/2543)
+- Fix create zip tool - path argument [\#2522](https://github.com/pypeclub/OpenPype/pull/2522)
- General: Modules import function output fix [\#2492](https://github.com/pypeclub/OpenPype/pull/2492)
- AE: fix hiding of alert window below Publish [\#2491](https://github.com/pypeclub/OpenPype/pull/2491)
- Maya: Validate NGONs re-use polyConstraint code from openpype.host.maya.api.lib [\#2458](https://github.com/pypeclub/OpenPype/pull/2458)
-- Version handling [\#2363](https://github.com/pypeclub/OpenPype/pull/2363)
## [3.7.0](https://github.com/pypeclub/OpenPype/tree/3.7.0) (2022-01-04)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.7.0-nightly.14...3.7.0)
-**Deprecated:**
-
-- General: Default modules hierarchy n2 [\#2368](https://github.com/pypeclub/OpenPype/pull/2368)
-
**🚀 Enhancements**
- General: Workdir extra folders [\#2462](https://github.com/pypeclub/OpenPype/pull/2462)
@@ -61,9 +80,6 @@
- Enhancement: Settings: Use project settings values from another project [\#2382](https://github.com/pypeclub/OpenPype/pull/2382)
- Blender 3: Support auto install for new blender version [\#2377](https://github.com/pypeclub/OpenPype/pull/2377)
- Maya add render image path to settings [\#2375](https://github.com/pypeclub/OpenPype/pull/2375)
-- Hiero: python3 compatibility [\#2365](https://github.com/pypeclub/OpenPype/pull/2365)
-- Maya: Add is\_static\_image\_plane and is\_in\_all\_views option in imagePlaneLoader [\#2356](https://github.com/pypeclub/OpenPype/pull/2356)
-- TVPaint: Move implementation to OpenPype [\#2336](https://github.com/pypeclub/OpenPype/pull/2336)
**🛠Bug fixes**
@@ -80,8 +96,7 @@
- hiero: solve custom ocio path [\#2379](https://github.com/pypeclub/OpenPype/pull/2379)
- hiero: fix workio and flatten [\#2378](https://github.com/pypeclub/OpenPype/pull/2378)
- Nuke: fixing menu re-drawing during context change [\#2374](https://github.com/pypeclub/OpenPype/pull/2374)
-- Nuke: fixing node name based on switched asset name [\#2369](https://github.com/pypeclub/OpenPype/pull/2369)
-- Houdini: Fix HDA creation [\#2350](https://github.com/pypeclub/OpenPype/pull/2350)
+- Webpublisher: Fix assignment of families of TVpaint instances [\#2373](https://github.com/pypeclub/OpenPype/pull/2373)
**Merged pull requests:**
@@ -89,7 +104,6 @@
- Maya: Replaced PATH usage with vendored oiio path for maketx utility [\#2405](https://github.com/pypeclub/OpenPype/pull/2405)
- \[Fix\]\[MAYA\] Handle message type attribute within CollectLook [\#2394](https://github.com/pypeclub/OpenPype/pull/2394)
- Add validator to check correct version of extension for PS and AE [\#2387](https://github.com/pypeclub/OpenPype/pull/2387)
-- Linux : flip updating submodules logic [\#2357](https://github.com/pypeclub/OpenPype/pull/2357)
## [3.6.4](https://github.com/pypeclub/OpenPype/tree/3.6.4) (2021-11-23)
diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py
index db62cbbe91..207253cd2d 100644
--- a/igniter/bootstrap_repos.py
+++ b/igniter/bootstrap_repos.py
@@ -762,7 +762,7 @@ class BootstrapRepos:
destination = self._move_zip_to_data_dir(temp_zip)
- return OpenPypeVersion(version=version, path=destination)
+ return OpenPypeVersion(version=version, path=Path(destination))
def _move_zip_to_data_dir(self, zip_file) -> Union[None, Path]:
"""Move zip with OpenPype version to user data directory.
@@ -912,7 +912,6 @@ class BootstrapRepos:
processed_path = file
self._print(f"- processing {processed_path}")
-
checksums.append(
(
sha256sum(file.as_posix()),
@@ -1544,7 +1543,8 @@ class BootstrapRepos:
Args:
zip_item (Path): Zip file to test.
- detected_version (OpenPypeVersion): Pype version detected from name.
+ detected_version (OpenPypeVersion): Pype version detected from
+ name.
Returns:
True if it is valid OpenPype version, False otherwise.
diff --git a/igniter/install_thread.py b/igniter/install_thread.py
index 383012b88b..8e31f8cb8f 100644
--- a/igniter/install_thread.py
+++ b/igniter/install_thread.py
@@ -60,7 +60,7 @@ class InstallThread(QThread):
# find local version of OpenPype
bs = BootstrapRepos(
progress_callback=self.set_progress, message=self.message)
- local_version = bs.get_local_live_version()
+ local_version = OpenPypeVersion.get_installed_version_str()
# if user did entered nothing, we install OpenPype from local version.
# zip content of `repos`, copy it to user data dir and append
diff --git a/openpype/hooks/pre_non_python_host_launch.py b/openpype/hooks/pre_non_python_host_launch.py
index 848ed675a8..29e40d28c8 100644
--- a/openpype/hooks/pre_non_python_host_launch.py
+++ b/openpype/hooks/pre_non_python_host_launch.py
@@ -3,7 +3,7 @@ import subprocess
from openpype.lib import (
PreLaunchHook,
- get_pype_execute_args
+ get_openpype_execute_args
)
from openpype import PACKAGE_DIR as OPENPYPE_DIR
@@ -35,7 +35,7 @@ class NonPythonHostHook(PreLaunchHook):
"non_python_host_launch.py"
)
- new_launch_args = get_pype_execute_args(
+ new_launch_args = get_openpype_execute_args(
"run", script_path, executable_path
)
# Add workfile path if exists
@@ -48,4 +48,3 @@ class NonPythonHostHook(PreLaunchHook):
if remainders:
self.launch_context.launch_args.extend(remainders)
-
diff --git a/openpype/hosts/aftereffects/api/README.md b/openpype/hosts/aftereffects/api/README.md
new file mode 100644
index 0000000000..790c9f859a
--- /dev/null
+++ b/openpype/hosts/aftereffects/api/README.md
@@ -0,0 +1,66 @@
+# 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 avalon-core}\avalon\photoshop\extension.zxp
+```
+OR
+download [Anastasiy’s Extension Manager](https://install.anastasiy.com/)
+
+### Server
+
+The easiest way to get the server and After Effects launch is with:
+
+```
+python -c ^"import avalon.photoshop;avalon.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 > OpenPype`. Once launched you should be presented with a panel like this:
+
+
+
+
+## Developing
+
+### Extension
+When developing the extension you can load it [unsigned](https://github.com/Adobe-CEP/CEP-Resources/blob/master/CEP_9.x/Documentation/CEP%209.0%20HTML%20Extension%20Cookbook.md#debugging-unsigned-extensions).
+
+When signing the extension you can use this [guide](https://github.com/Adobe-CEP/Getting-Started-guides/tree/master/Package%20Distribute%20Install#package-distribute-install-guide).
+
+```
+ZXPSignCmd -selfSignedCert NA NA Avalon Avalon-After-Effects avalon extension.p12
+ZXPSignCmd -sign {path to avalon-core}\avalon\aftereffects\extension {path to avalon-core}\avalon\aftereffects\extension.zxp extension.p12 avalon
+```
+
+### 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\com.openpype.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/openpype/hosts/aftereffects/api/__init__.py b/openpype/hosts/aftereffects/api/__init__.py
index b1edb91a5c..cea1bdc023 100644
--- a/openpype/hosts/aftereffects/api/__init__.py
+++ b/openpype/hosts/aftereffects/api/__init__.py
@@ -1,115 +1,68 @@
-import os
-import sys
-import logging
+"""Public API
-from avalon import io
-from avalon import api as avalon
-from Qt import QtWidgets
-from openpype import lib, api
-import pyblish.api as pyblish
-import openpype.hosts.aftereffects
+Anything that isn't defined here is INTERNAL and unreliable for external use.
+
+"""
+
+from .launch_logic import (
+ get_stub,
+ stub,
+)
+
+from .pipeline import (
+ ls,
+ get_asset_settings,
+ install,
+ uninstall,
+ list_instances,
+ remove_instance,
+ containerise
+)
+
+from .workio import (
+ file_extensions,
+ has_unsaved_changes,
+ save_file,
+ open_file,
+ current_file,
+ work_root,
+)
+
+from .lib import (
+ maintained_selection,
+ get_extension_manifest_path
+)
+
+from .plugin import (
+ AfterEffectsLoader
+)
-log = logging.getLogger("openpype.hosts.aftereffects")
+__all__ = [
+ # launch_logic
+ "get_stub",
+ "stub",
+ # pipeline
+ "ls",
+ "get_asset_settings",
+ "install",
+ "uninstall",
+ "list_instances",
+ "remove_instance",
+ "containerise",
-HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.aftereffects.__file__))
-PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
-PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
-LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
-CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
-INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
+ "file_extensions",
+ "has_unsaved_changes",
+ "save_file",
+ "open_file",
+ "current_file",
+ "work_root",
+ # lib
+ "maintained_selection",
+ "get_extension_manifest_path",
-def check_inventory():
- if not lib.any_outdated():
- return
-
- host = pyblish.registered_host()
- outdated_containers = []
- for container in host.ls():
- representation = container['representation']
- representation_doc = io.find_one(
- {
- "_id": io.ObjectId(representation),
- "type": "representation"
- },
- projection={"parent": True}
- )
- if representation_doc and not lib.is_latest(representation_doc):
- outdated_containers.append(container)
-
- # Warn about outdated containers.
- print("Starting new QApplication..")
- app = QtWidgets.QApplication(sys.argv)
-
- message_box = QtWidgets.QMessageBox()
- message_box.setIcon(QtWidgets.QMessageBox.Warning)
- msg = "There are outdated containers in the scene."
- message_box.setText(msg)
- message_box.exec_()
-
- # Garbage collect QApplication.
- del app
-
-
-def application_launch():
- check_inventory()
-
-
-def install():
- print("Installing Pype config...")
-
- pyblish.register_plugin_path(PUBLISH_PATH)
- avalon.register_plugin_path(avalon.Loader, LOAD_PATH)
- avalon.register_plugin_path(avalon.Creator, CREATE_PATH)
- log.info(PUBLISH_PATH)
-
- pyblish.register_callback(
- "instanceToggled", on_pyblish_instance_toggled
- )
-
- avalon.on("application.launched", application_launch)
-
-
-def uninstall():
- pyblish.deregister_plugin_path(PUBLISH_PATH)
- avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH)
- avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH)
-
-
-def on_pyblish_instance_toggled(instance, old_value, new_value):
- """Toggle layer visibility on instance toggles."""
- instance[0].Visible = new_value
-
-
-def get_asset_settings():
- """Get settings on current asset from database.
-
- Returns:
- dict: Scene data.
-
- """
- asset_data = lib.get_asset()["data"]
- fps = asset_data.get("fps")
- frame_start = asset_data.get("frameStart")
- frame_end = asset_data.get("frameEnd")
- handle_start = asset_data.get("handleStart")
- handle_end = asset_data.get("handleEnd")
- resolution_width = asset_data.get("resolutionWidth")
- resolution_height = asset_data.get("resolutionHeight")
- duration = (frame_end - frame_start + 1) + handle_start + handle_end
- entity_type = asset_data.get("entityType")
-
- scene_data = {
- "fps": fps,
- "frameStart": frame_start,
- "frameEnd": frame_end,
- "handleStart": handle_start,
- "handleEnd": handle_end,
- "resolutionWidth": resolution_width,
- "resolutionHeight": resolution_height,
- "duration": duration
- }
-
- return scene_data
+ # plugin
+ "AfterEffectsLoader"
+]
diff --git a/openpype/hosts/aftereffects/api/extension.zxp b/openpype/hosts/aftereffects/api/extension.zxp
new file mode 100644
index 0000000000..35b0c0fc42
Binary files /dev/null and b/openpype/hosts/aftereffects/api/extension.zxp differ
diff --git a/openpype/hosts/aftereffects/api/extension/.debug b/openpype/hosts/aftereffects/api/extension/.debug
new file mode 100644
index 0000000000..b06ec515dd
--- /dev/null
+++ b/openpype/hosts/aftereffects/api/extension/.debug
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/openpype/hosts/aftereffects/api/extension/CSXS/manifest.xml b/openpype/hosts/aftereffects/api/extension/CSXS/manifest.xml
new file mode 100644
index 0000000000..2480089825
--- /dev/null
+++ b/openpype/hosts/aftereffects/api/extension/CSXS/manifest.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ./index.html
+ ./jsx/hostscript.jsx
+
+
+ true
+
+
+ Panel
+ OpenPype
+
+
+ 200
+ 100
+
+
+
+
+
+ ./icons/iconNormal.png
+ ./icons/iconRollover.png
+ ./icons/iconDisabled.png
+ ./icons/iconDarkNormal.png
+ ./icons/iconDarkRollover.png
+
+
+
+
+
+
\ No newline at end of file
diff --git a/openpype/hosts/aftereffects/api/extension/css/boilerplate.css b/openpype/hosts/aftereffects/api/extension/css/boilerplate.css
new file mode 100644
index 0000000000..d208999b8a
--- /dev/null
+++ b/openpype/hosts/aftereffects/api/extension/css/boilerplate.css
@@ -0,0 +1,327 @@
+/*
+ * HTML5 ✰ Boilerplate
+ *
+ * What follows is the result of much research on cross-browser styling.
+ * Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal,
+ * Kroc Camen, and the H5BP dev community and team.
+ *
+ * Detailed information about this CSS: h5bp.com/css
+ *
+ * ==|== normalize ==========================================================
+ */
+
+
+/* =============================================================================
+ HTML5 display definitions
+ ========================================================================== */
+
+article, aside, details, figcaption, figure, footer, header, hgroup, nav, section { display: block; }
+audio, canvas, video { display: inline-block; *display: inline; *zoom: 1; }
+audio:not([controls]) { display: none; }
+[hidden] { display: none; }
+
+
+/* =============================================================================
+ Base
+ ========================================================================== */
+
+/*
+ * 1. Correct text resizing oddly in IE6/7 when body font-size is set using em units
+ * 2. Force vertical scrollbar in non-IE
+ * 3. Prevent iOS text size adjust on device orientation change, without disabling user zoom: h5bp.com/g
+ */
+
+html { font-size: 100%; overflow-y: scroll; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
+
+body { margin: 0; font-size: 100%; line-height: 1.231; }
+
+body, button, input, select, textarea { font-family: helvetica, arial,"lucida grande", verdana, "メイリオ", "ï¼ï¼³ Pゴシック", sans-serif; color: #222; }
+/*
+ * Remove text-shadow in selection highlight: h5bp.com/i
+ * These selection declarations have to be separate
+ * Also: hot pink! (or customize the background color to match your design)
+ */
+
+::selection { text-shadow: none; background-color: highlight; color: highlighttext; }
+
+
+/* =============================================================================
+ Links
+ ========================================================================== */
+
+a { color: #00e; }
+a:visited { color: #551a8b; }
+a:hover { color: #06e; }
+a:focus { outline: thin dotted; }
+
+/* Improve readability when focused and hovered in all browsers: h5bp.com/h */
+a:hover, a:active { outline: 0; }
+
+
+/* =============================================================================
+ Typography
+ ========================================================================== */
+
+abbr[title] { border-bottom: 1px dotted; }
+
+b, strong { font-weight: bold; }
+
+blockquote { margin: 1em 40px; }
+
+dfn { font-style: italic; }
+
+hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; }
+
+ins { background: #ff9; color: #000; text-decoration: none; }
+
+mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; }
+
+/* Redeclare monospace font family: h5bp.com/j */
+pre, code, kbd, samp { font-family: monospace, serif; _font-family: 'courier new', monospace; font-size: 1em; }
+
+/* Improve readability of pre-formatted text in all browsers */
+pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; }
+
+q { quotes: none; }
+q:before, q:after { content: ""; content: none; }
+
+small { font-size: 85%; }
+
+/* Position subscript and superscript content without affecting line-height: h5bp.com/k */
+sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
+sup { top: -0.5em; }
+sub { bottom: -0.25em; }
+
+
+/* =============================================================================
+ Lists
+ ========================================================================== */
+
+ul, ol { margin: 1em 0; padding: 0 0 0 40px; }
+dd { margin: 0 0 0 40px; }
+nav ul, nav ol { list-style: none; list-style-image: none; margin: 0; padding: 0; }
+
+
+/* =============================================================================
+ Embedded content
+ ========================================================================== */
+
+/*
+ * 1. Improve image quality when scaled in IE7: h5bp.com/d
+ * 2. Remove the gap between images and borders on image containers: h5bp.com/e
+ */
+
+img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; }
+
+/*
+ * Correct overflow not hidden in IE9
+ */
+
+svg:not(:root) { overflow: hidden; }
+
+
+/* =============================================================================
+ Figures
+ ========================================================================== */
+
+figure { margin: 0; }
+
+
+/* =============================================================================
+ Forms
+ ========================================================================== */
+
+form { margin: 0; }
+fieldset { border: 0; margin: 0; padding: 0; }
+
+/* Indicate that 'label' will shift focus to the associated form element */
+label { cursor: pointer; }
+
+/*
+ * 1. Correct color not inheriting in IE6/7/8/9
+ * 2. Correct alignment displayed oddly in IE6/7
+ */
+
+legend { border: 0; *margin-left: -7px; padding: 0; }
+
+/*
+ * 1. Correct font-size not inheriting in all browsers
+ * 2. Remove margins in FF3/4 S5 Chrome
+ * 3. Define consistent vertical alignment display in all browsers
+ */
+
+button, input, select, textarea { font-size: 100%; margin: 0; vertical-align: baseline; *vertical-align: middle; }
+
+/*
+ * 1. Define line-height as normal to match FF3/4 (set using !important in the UA stylesheet)
+ */
+
+button, input { line-height: normal; }
+
+/*
+ * 1. Display hand cursor for clickable form elements
+ * 2. Allow styling of clickable form elements in iOS
+ * 3. Correct inner spacing displayed oddly in IE7 (doesn't effect IE6)
+ */
+
+button, input[type="button"], input[type="reset"], input[type="submit"] { cursor: pointer; -webkit-appearance: button; *overflow: visible; }
+
+/*
+ * Consistent box sizing and appearance
+ */
+
+input[type="checkbox"], input[type="radio"] { box-sizing: border-box; padding: 0; }
+input[type="search"] { -webkit-appearance: textfield; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; box-sizing: content-box; }
+input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; }
+
+/*
+ * Remove inner padding and border in FF3/4: h5bp.com/l
+ */
+
+button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; }
+
+/*
+ * 1. Remove default vertical scrollbar in IE6/7/8/9
+ * 2. Allow only vertical resizing
+ */
+
+textarea { overflow: auto; vertical-align: top; resize: vertical; }
+
+/* Colors for form validity */
+input:valid, textarea:valid { }
+input:invalid, textarea:invalid { background-color: #f0dddd; }
+
+
+/* =============================================================================
+ Tables
+ ========================================================================== */
+
+table { border-collapse: collapse; border-spacing: 0; }
+td { vertical-align: top; }
+
+
+/* ==|== primary styles =====================================================
+ Author:
+ ========================================================================== */
+
+/* ==|== media queries ======================================================
+ PLACEHOLDER Media Queries for Responsive Design.
+ These override the primary ('mobile first') styles
+ Modify as content requires.
+ ========================================================================== */
+
+@media only screen and (min-width: 480px) {
+ /* Style adjustments for viewports 480px and over go here */
+
+}
+
+@media only screen and (min-width: 768px) {
+ /* Style adjustments for viewports 768px and over go here */
+
+}
+
+
+
+/* ==|== non-semantic helper classes ========================================
+ Please define your styles before this section.
+ ========================================================================== */
+
+/* For image replacement */
+.ir { display: block; border: 0; text-indent: -999em; overflow: hidden; background-color: transparent; background-repeat: no-repeat; text-align: left; direction: ltr; }
+.ir br { display: none; }
+
+/* Hide from both screenreaders and browsers: h5bp.com/u */
+.hidden { display: none !important; visibility: hidden; }
+
+/* Hide only visually, but have it available for screenreaders: h5bp.com/v */
+.visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; }
+
+/* Extends the .visuallyhidden class to allow the element to be focusable when navigated to via the keyboard: h5bp.com/p */
+.visuallyhidden.focusable:active, .visuallyhidden.focusable:focus { clip: auto; height: auto; margin: 0; overflow: visible; position: static; width: auto; }
+
+/* Hide visually and from screenreaders, but maintain layout */
+.invisible { visibility: hidden; }
+
+/* Contain floats: h5bp.com/q */
+.clearfix:before, .clearfix:after { content: ""; display: table; }
+.clearfix:after { clear: both; }
+.clearfix { *zoom: 1; }
+
+
+
+/* ==|== print styles =======================================================
+ Print styles.
+ Inlined to avoid required HTTP connection: h5bp.com/r
+ ========================================================================== */
+
+@media print {
+ * { background: transparent !important; color: black !important; box-shadow:none !important; text-shadow: none !important; filter:none !important; -ms-filter: none !important; } /* Black prints faster: h5bp.com/s */
+ a, a:visited { text-decoration: underline; }
+ a[href]:after { content: " (" attr(href) ")"; }
+ abbr[title]:after { content: " (" attr(title) ")"; }
+ .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } /* Don't show links for images, or javascript/internal links */
+ pre, blockquote { border: 1px solid #999; page-break-inside: avoid; }
+ table { display: table-header-group; } /* h5bp.com/t */
+ tr, img { page-break-inside: avoid; }
+ img { max-width: 100% !important; }
+ @page { margin: 0.5cm; }
+ p, h2, h3 { orphans: 3; widows: 3; }
+ h2, h3 { page-break-after: avoid; }
+}
+
+/* reflow reset for -webkit-margin-before: 1em */
+p { margin: 0; }
+
+html {
+ overflow-y: auto;
+ background-color: transparent;
+ height: 100%;
+}
+
+body {
+ background: #fff;
+ font: normal 100%;
+ position: relative;
+ height: 100%;
+}
+
+body, div, img, p, button, input, select, textarea {
+ box-sizing: border-box;
+}
+
+.image {
+ display: block;
+}
+
+input {
+ cursor: default;
+ display: block;
+}
+
+input[type=button] {
+ background-color: #e5e9e8;
+ border: 1px solid #9daca9;
+ border-radius: 4px;
+ box-shadow: inset 0 1px #fff;
+ font: inherit;
+ letter-spacing: inherit;
+ text-indent: inherit;
+ color: inherit;
+}
+
+input[type=button]:hover {
+ background-color: #eff1f1;
+}
+
+input[type=button]:active {
+ background-color: #d2d6d6;
+ border: 1px solid #9daca9;
+ box-shadow: inset 0 1px rgba(0,0,0,0.1);
+}
+
+/* Reset anchor styles to an unstyled default to be in parity with design surface. It
+ is presumed that most link styles in real-world designs are custom (non-default). */
+a, a:visited, a:hover, a:active {
+ color: inherit;
+ text-decoration: inherit;
+}
\ No newline at end of file
diff --git a/openpype/hosts/aftereffects/api/extension/css/styles.css b/openpype/hosts/aftereffects/api/extension/css/styles.css
new file mode 100644
index 0000000000..c9cf2b93ac
--- /dev/null
+++ b/openpype/hosts/aftereffects/api/extension/css/styles.css
@@ -0,0 +1,51 @@
+/*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/openpype/hosts/aftereffects/api/extension/css/topcoat-desktop-dark.min.css b/openpype/hosts/aftereffects/api/extension/css/topcoat-desktop-dark.min.css
new file mode 100644
index 0000000000..6b479def43
--- /dev/null
+++ b/openpype/hosts/aftereffects/api/extension/css/topcoat-desktop-dark.min.css
@@ -0,0 +1 @@
+.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/openpype/hosts/aftereffects/api/extension/icons/iconDarkNormal.png b/openpype/hosts/aftereffects/api/extension/icons/iconDarkNormal.png
new file mode 100644
index 0000000000..b8652a85b8
Binary files /dev/null and b/openpype/hosts/aftereffects/api/extension/icons/iconDarkNormal.png differ
diff --git a/openpype/hosts/aftereffects/api/extension/icons/iconDarkRollover.png b/openpype/hosts/aftereffects/api/extension/icons/iconDarkRollover.png
new file mode 100644
index 0000000000..49edd7ca27
Binary files /dev/null and b/openpype/hosts/aftereffects/api/extension/icons/iconDarkRollover.png differ
diff --git a/openpype/hosts/aftereffects/api/extension/icons/iconDisabled.png b/openpype/hosts/aftereffects/api/extension/icons/iconDisabled.png
new file mode 100644
index 0000000000..49edd7ca27
Binary files /dev/null and b/openpype/hosts/aftereffects/api/extension/icons/iconDisabled.png differ
diff --git a/openpype/hosts/aftereffects/api/extension/icons/iconNormal.png b/openpype/hosts/aftereffects/api/extension/icons/iconNormal.png
new file mode 100644
index 0000000000..199326f2ea
Binary files /dev/null and b/openpype/hosts/aftereffects/api/extension/icons/iconNormal.png differ
diff --git a/openpype/hosts/aftereffects/api/extension/icons/iconRollover.png b/openpype/hosts/aftereffects/api/extension/icons/iconRollover.png
new file mode 100644
index 0000000000..ff62645798
Binary files /dev/null and b/openpype/hosts/aftereffects/api/extension/icons/iconRollover.png differ
diff --git a/openpype/hosts/aftereffects/api/extension/index.html b/openpype/hosts/aftereffects/api/extension/index.html
new file mode 100644
index 0000000000..9e39bf1acc
--- /dev/null
+++ b/openpype/hosts/aftereffects/api/extension/index.html
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/openpype/hosts/aftereffects/api/extension/js/libs/CSInterface.js b/openpype/hosts/aftereffects/api/extension/js/libs/CSInterface.js
new file mode 100644
index 0000000000..4239391efd
--- /dev/null
+++ b/openpype/hosts/aftereffects/api/extension/js/libs/CSInterface.js
@@ -0,0 +1,1193 @@
+/**************************************************************************************************
+*
+* 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:
+ *
+ * Access information about the host application in which an extension is running
+ * Launch an extension
+ * Register interest in event notifications, and dispatch events
+ *
+ *
+ * @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
+ * NO_ERROR - 0 \n
+ * ERR_UNKNOWN - 1 \n
+ * ERR_INVALID_PARAMS - 2 \n
+ * ERR_INVALID_URL - 201 \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
+ * -1.0 when error occurs \n
+ * 1.0 means normal screen \n
+ * >1.0 means HiDPI screen \n
+ * \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/openpype/hosts/aftereffects/api/extension/js/libs/jquery-2.0.2.min.js b/openpype/hosts/aftereffects/api/extension/js/libs/jquery-2.0.2.min.js
new file mode 100644
index 0000000000..73e5218d21
--- /dev/null
+++ b/openpype/hosts/aftereffects/api/extension/js/libs/jquery-2.0.2.min.js
@@ -0,0 +1,6 @@
+/*! 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>$2>");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>$2>")+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("").css("cssText","display:block !important")).appendTo(t.documentElement),t=(xt[0].contentWindow||xt[0].contentDocument).document,t.write(""),t.close(),n=Mt(e,t),xt.detach()),Nt[e]=n),n}function Mt(e,t){var n=x(t.createElement(e)).appendTo(t.body),r=x.css(n[0],"display");return n.remove(),r}x.each(["height","width"],function(e,t){x.cssHooks[t]={get:function(e,n,r){return n?0===e.offsetWidth&&bt.test(x.css(e,"display"))?x.swap(e,Et,function(){return Pt(e,t,r)}):Pt(e,t,r):undefined},set:function(e,n,r){var i=r&&Ht(e);return Ot(e,n,r?Ft(e,t,r,x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,i),i):0)}}}),x(function(){x.support.reliableMarginRight||(x.cssHooks.marginRight={get:function(e,t){return t?x.swap(e,{display:"inline-block"},vt,[e,"marginRight"]):undefined}}),!x.support.pixelPosition&&x.fn.position&&x.each(["top","left"],function(e,t){x.cssHooks[t]={get:function(e,n){return n?(n=vt(e,t),Ct.test(n)?x(e).position()[t]+"px":n):undefined}}})}),x.expr&&x.expr.filters&&(x.expr.filters.hidden=function(e){return 0>=e.offsetWidth&&0>=e.offsetHeight},x.expr.filters.visible=function(e){return!x.expr.filters.hidden(e)}),x.each({margin:"",padding:"",border:"Width"},function(e,t){x.cssHooks[e+t]={expand:function(n){var r=0,i={},o="string"==typeof n?n.split(" "):[n];for(;4>r;r++)i[e+jt[r]+t]=o[r]||o[r-2]||o[0];return i}},wt.test(e)||(x.cssHooks[e+t].set=Ot)});var Wt=/%20/g,$t=/\[\]$/,Bt=/\r?\n/g,It=/^(?:submit|button|image|reset|file)$/i,zt=/^(?:input|select|textarea|keygen)/i;x.fn.extend({serialize:function(){return x.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=x.prop(this,"elements");return e?x.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!x(this).is(":disabled")&&zt.test(this.nodeName)&&!It.test(e)&&(this.checked||!ot.test(e))}).map(function(e,t){var n=x(this).val();return null==n?null:x.isArray(n)?x.map(n,function(e){return{name:t.name,value:e.replace(Bt,"\r\n")}}):{name:t.name,value:n.replace(Bt,"\r\n")}}).get()}}),x.param=function(e,t){var n,r=[],i=function(e,t){t=x.isFunction(t)?t():null==t?"":t,r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(t===undefined&&(t=x.ajaxSettings&&x.ajaxSettings.traditional),x.isArray(e)||e.jquery&&!x.isPlainObject(e))x.each(e,function(){i(this.name,this.value)});else for(n in e)_t(n,e[n],t,i);return r.join("&").replace(Wt,"+")};function _t(e,t,n,r){var i;if(x.isArray(t))x.each(t,function(t,i){n||$t.test(e)?r(e,i):_t(e+"["+("object"==typeof i?t:"")+"]",i,n,r)});else if(n||"object"!==x.type(t))r(e,t);else for(i in t)_t(e+"["+i+"]",t[i],n,r)}x.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){x.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),x.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}});var Xt,Ut,Yt=x.now(),Vt=/\?/,Gt=/#.*$/,Jt=/([?&])_=[^&]*/,Qt=/^(.*?):[ \t]*([^\r\n]*)$/gm,Kt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Zt=/^(?:GET|HEAD)$/,en=/^\/\//,tn=/^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,nn=x.fn.load,rn={},on={},sn="*/".concat("*");
+try{Ut=i.href}catch(an){Ut=o.createElement("a"),Ut.href="",Ut=Ut.href}Xt=tn.exec(Ut.toLowerCase())||[];function un(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(w)||[];if(x.isFunction(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function ln(e,t,n,r){var i={},o=e===on;function s(a){var u;return i[a]=!0,x.each(e[a]||[],function(e,a){var l=a(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):undefined:(t.dataTypes.unshift(l),s(l),!1)}),u}return s(t.dataTypes[0])||!i["*"]&&s("*")}function cn(e,t){var n,r,i=x.ajaxSettings.flatOptions||{};for(n in t)t[n]!==undefined&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&x.extend(!0,e,r),e}x.fn.load=function(e,t,n){if("string"!=typeof e&&nn)return nn.apply(this,arguments);var r,i,o,s=this,a=e.indexOf(" ");return a>=0&&(r=e.slice(a),e=e.slice(0,a)),x.isFunction(t)?(n=t,t=undefined):t&&"object"==typeof t&&(i="POST"),s.length>0&&x.ajax({url:e,type:i,dataType:"html",data:t}).done(function(e){o=arguments,s.html(r?x("").append(x.parseHTML(e)).find(r):e)}).complete(n&&function(e,t){s.each(n,o||[e.responseText,t,e])}),this},x.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){x.fn[t]=function(e){return this.on(t,e)}}),x.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ut,type:"GET",isLocal:Kt.test(Xt[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":sn,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":x.parseJSON,"text xml":x.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?cn(cn(e,x.ajaxSettings),t):cn(x.ajaxSettings,e)},ajaxPrefilter:un(rn),ajaxTransport:un(on),ajax:function(e,t){"object"==typeof e&&(t=e,e=undefined),t=t||{};var n,r,i,o,s,a,u,l,c=x.ajaxSetup({},t),p=c.context||c,f=c.context&&(p.nodeType||p.jquery)?x(p):x.event,h=x.Deferred(),d=x.Callbacks("once memory"),g=c.statusCode||{},m={},y={},v=0,b="canceled",T={readyState:0,getResponseHeader:function(e){var t;if(2===v){if(!o){o={};while(t=Qt.exec(i))o[t[1].toLowerCase()]=t[2]}t=o[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===v?i:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return v||(e=y[n]=y[n]||e,m[e]=t),this},overrideMimeType:function(e){return v||(c.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>v)for(t in e)g[t]=[g[t],e[t]];else T.always(e[T.status]);return this},abort:function(e){var t=e||b;return n&&n.abort(t),k(0,t),this}};if(h.promise(T).complete=d.add,T.success=T.done,T.error=T.fail,c.url=((e||c.url||Ut)+"").replace(Gt,"").replace(en,Xt[1]+"//"),c.type=t.method||t.type||c.method||c.type,c.dataTypes=x.trim(c.dataType||"*").toLowerCase().match(w)||[""],null==c.crossDomain&&(a=tn.exec(c.url.toLowerCase()),c.crossDomain=!(!a||a[1]===Xt[1]&&a[2]===Xt[2]&&(a[3]||("http:"===a[1]?"80":"443"))===(Xt[3]||("http:"===Xt[1]?"80":"443")))),c.data&&c.processData&&"string"!=typeof c.data&&(c.data=x.param(c.data,c.traditional)),ln(rn,c,t,T),2===v)return T;u=c.global,u&&0===x.active++&&x.event.trigger("ajaxStart"),c.type=c.type.toUpperCase(),c.hasContent=!Zt.test(c.type),r=c.url,c.hasContent||(c.data&&(r=c.url+=(Vt.test(r)?"&":"?")+c.data,delete c.data),c.cache===!1&&(c.url=Jt.test(r)?r.replace(Jt,"$1_="+Yt++):r+(Vt.test(r)?"&":"?")+"_="+Yt++)),c.ifModified&&(x.lastModified[r]&&T.setRequestHeader("If-Modified-Since",x.lastModified[r]),x.etag[r]&&T.setRequestHeader("If-None-Match",x.etag[r])),(c.data&&c.hasContent&&c.contentType!==!1||t.contentType)&&T.setRequestHeader("Content-Type",c.contentType),T.setRequestHeader("Accept",c.dataTypes[0]&&c.accepts[c.dataTypes[0]]?c.accepts[c.dataTypes[0]]+("*"!==c.dataTypes[0]?", "+sn+"; q=0.01":""):c.accepts["*"]);for(l in c.headers)T.setRequestHeader(l,c.headers[l]);if(c.beforeSend&&(c.beforeSend.call(p,T,c)===!1||2===v))return T.abort();b="abort";for(l in{success:1,error:1,complete:1})T[l](c[l]);if(n=ln(on,c,t,T)){T.readyState=1,u&&f.trigger("ajaxSend",[T,c]),c.async&&c.timeout>0&&(s=setTimeout(function(){T.abort("timeout")},c.timeout));try{v=1,n.send(m,k)}catch(C){if(!(2>v))throw C;k(-1,C)}}else k(-1,"No Transport");function k(e,t,o,a){var l,m,y,b,w,C=t;2!==v&&(v=2,s&&clearTimeout(s),n=undefined,i=a||"",T.readyState=e>0?4:0,l=e>=200&&300>e||304===e,o&&(b=pn(c,T,o)),b=fn(c,b,T,l),l?(c.ifModified&&(w=T.getResponseHeader("Last-Modified"),w&&(x.lastModified[r]=w),w=T.getResponseHeader("etag"),w&&(x.etag[r]=w)),204===e||"HEAD"===c.type?C="nocontent":304===e?C="notmodified":(C=b.state,m=b.data,y=b.error,l=!y)):(y=C,(e||!C)&&(C="error",0>e&&(e=0))),T.status=e,T.statusText=(t||C)+"",l?h.resolveWith(p,[m,C,T]):h.rejectWith(p,[T,C,y]),T.statusCode(g),g=undefined,u&&f.trigger(l?"ajaxSuccess":"ajaxError",[T,c,l?m:y]),d.fireWith(p,[T,C]),u&&(f.trigger("ajaxComplete",[T,c]),--x.active||x.event.trigger("ajaxStop")))}return T},getJSON:function(e,t,n){return x.get(e,t,n,"json")},getScript:function(e,t){return x.get(e,undefined,t,"script")}}),x.each(["get","post"],function(e,t){x[t]=function(e,n,r,i){return x.isFunction(n)&&(i=i||r,r=n,n=undefined),x.ajax({url:e,type:t,dataType:i,data:n,success:r})}});function pn(e,t,n){var r,i,o,s,a=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),r===undefined&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in a)if(a[i]&&a[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}s||(s=i)}o=o||s}return o?(o!==u[0]&&u.unshift(o),n[o]):undefined}function fn(e,t,n,r){var i,o,s,a,u,l={},c=e.dataTypes.slice();if(c[1])for(s in e.converters)l[s.toLowerCase()]=e.converters[s];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(s=l[u+" "+o]||l["* "+o],!s)for(i in l)if(a=i.split(" "),a[1]===o&&(s=l[u+" "+a[0]]||l["* "+a[0]])){s===!0?s=l[i]:l[i]!==!0&&(o=a[0],c.unshift(a[1]));break}if(s!==!0)if(s&&e["throws"])t=s(t);else try{t=s(t)}catch(p){return{state:"parsererror",error:s?p:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}x.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return x.globalEval(e),e}}}),x.ajaxPrefilter("script",function(e){e.cache===undefined&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),x.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(r,i){t=x("
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Workfiles...
+
Create...
+
Load...
+
Publish...
+
Manage...
+
Subset Manager...
+
Experimental Tools...
+
+
diff --git a/openpype/hosts/photoshop/api/launch_logic.py b/openpype/hosts/photoshop/api/launch_logic.py
new file mode 100644
index 0000000000..16a1d23244
--- /dev/null
+++ b/openpype/hosts/photoshop/api/launch_logic.py
@@ -0,0 +1,365 @@
+import os
+import subprocess
+import collections
+import asyncio
+
+from wsrpc_aiohttp import (
+ WebSocketRoute,
+ WebSocketAsync
+)
+
+from Qt import QtCore
+
+from openpype.api import Logger
+from openpype.tools.utils import host_tools
+
+from avalon import api
+from avalon.tools.webserver.app import WebServerTool
+
+from .ws_stub import PhotoshopServerStub
+
+log = Logger.get_logger(__name__)
+
+
+class ConnectionNotEstablishedYet(Exception):
+ pass
+
+
+class MainThreadItem:
+ """Structure to store information about callback in main thread.
+
+ Item should be used to execute callback in main thread which may be needed
+ for execution of Qt objects.
+
+ Item store callback (callable variable), arguments and keyword arguments
+ for the callback. Item hold information about it's process.
+ """
+ not_set = object()
+
+ def __init__(self, callback, *args, **kwargs):
+ self._done = False
+ self._exception = self.not_set
+ self._result = self.not_set
+ self._callback = callback
+ self._args = args
+ self._kwargs = kwargs
+
+ @property
+ def done(self):
+ return self._done
+
+ @property
+ def exception(self):
+ return self._exception
+
+ @property
+ def result(self):
+ return self._result
+
+ def execute(self):
+ """Execute callback and store it's result.
+
+ Method must be called from main thread. Item is marked as `done`
+ when callback execution finished. Store output of callback of exception
+ information when callback raise one.
+ """
+ log.debug("Executing process in main thread")
+ if self.done:
+ log.warning("- item is already processed")
+ return
+
+ log.info("Running callback: {}".format(str(self._callback)))
+ try:
+ result = self._callback(*self._args, **self._kwargs)
+ self._result = result
+
+ except Exception as exc:
+ self._exception = exc
+
+ finally:
+ self._done = True
+
+
+def stub():
+ """
+ Convenience function to get server RPC stub to call methods directed
+ for host (Photoshop).
+ It expects already created connection, started from client.
+ Currently created when panel is opened (PS: Window>Extensions>Avalon)
+ :return:
where functions could be called from
+ """
+ ps_stub = PhotoshopServerStub()
+ if not ps_stub.client:
+ raise ConnectionNotEstablishedYet("Connection is not created yet")
+
+ return ps_stub
+
+
+def show_tool_by_name(tool_name):
+ kwargs = {}
+ if tool_name == "loader":
+ kwargs["use_context"] = True
+
+ host_tools.show_tool_by_name(tool_name, **kwargs)
+
+
+class ProcessLauncher(QtCore.QObject):
+ route_name = "Photoshop"
+ _main_thread_callbacks = collections.deque()
+
+ def __init__(self, subprocess_args):
+ self._subprocess_args = subprocess_args
+ self._log = None
+
+ super(ProcessLauncher, self).__init__()
+
+ # Keep track if launcher was already started
+ self._started = False
+
+ self._process = None
+ self._websocket_server = None
+
+ start_process_timer = QtCore.QTimer()
+ start_process_timer.setInterval(100)
+
+ loop_timer = QtCore.QTimer()
+ loop_timer.setInterval(200)
+
+ start_process_timer.timeout.connect(self._on_start_process_timer)
+ loop_timer.timeout.connect(self._on_loop_timer)
+
+ self._start_process_timer = start_process_timer
+ self._loop_timer = loop_timer
+
+ @property
+ def log(self):
+ if self._log is None:
+ self._log = Logger.get_logger(
+ "{}-launcher".format(self.route_name)
+ )
+ return self._log
+
+ @property
+ def websocket_server_is_running(self):
+ if self._websocket_server is not None:
+ return self._websocket_server.is_running
+ return False
+
+ @property
+ def is_process_running(self):
+ if self._process is not None:
+ return self._process.poll() is None
+ return False
+
+ @property
+ def is_host_connected(self):
+ """Returns True if connected, False if app is not running at all."""
+ if not self.is_process_running:
+ return False
+
+ try:
+ _stub = stub()
+ if _stub:
+ return True
+ except Exception:
+ pass
+
+ return None
+
+ @classmethod
+ def execute_in_main_thread(cls, callback, *args, **kwargs):
+ item = MainThreadItem(callback, *args, **kwargs)
+ cls._main_thread_callbacks.append(item)
+ return item
+
+ def start(self):
+ if self._started:
+ return
+ self.log.info("Started launch logic of AfterEffects")
+ self._started = True
+ self._start_process_timer.start()
+
+ def exit(self):
+ """ Exit whole application. """
+ if self._start_process_timer.isActive():
+ self._start_process_timer.stop()
+ if self._loop_timer.isActive():
+ self._loop_timer.stop()
+
+ if self._websocket_server is not None:
+ self._websocket_server.stop()
+
+ if self._process:
+ self._process.kill()
+ self._process.wait()
+
+ QtCore.QCoreApplication.exit()
+
+ def _on_loop_timer(self):
+ # TODO find better way and catch errors
+ # Run only callbacks that are in queue at the moment
+ cls = self.__class__
+ for _ in range(len(cls._main_thread_callbacks)):
+ if cls._main_thread_callbacks:
+ item = cls._main_thread_callbacks.popleft()
+ item.execute()
+
+ if not self.is_process_running:
+ self.log.info("Host process is not running. Closing")
+ self.exit()
+
+ elif not self.websocket_server_is_running:
+ self.log.info("Websocket server is not running. Closing")
+ self.exit()
+
+ def _on_start_process_timer(self):
+ # TODO add try except validations for each part in this method
+ # Start server as first thing
+ if self._websocket_server is None:
+ self._init_server()
+ return
+
+ # TODO add waiting time
+ # Wait for webserver
+ if not self.websocket_server_is_running:
+ return
+
+ # Start application process
+ if self._process is None:
+ self._start_process()
+ self.log.info("Waiting for host to connect")
+ return
+
+ # TODO add waiting time
+ # Wait until host is connected
+ if self.is_host_connected:
+ self._start_process_timer.stop()
+ self._loop_timer.start()
+ elif (
+ not self.is_process_running
+ or not self.websocket_server_is_running
+ ):
+ self.exit()
+
+ def _init_server(self):
+ if self._websocket_server is not None:
+ return
+
+ self.log.debug(
+ "Initialization of websocket server for host communication"
+ )
+
+ self._websocket_server = websocket_server = WebServerTool()
+ if websocket_server.port_occupied(
+ websocket_server.host_name,
+ websocket_server.port
+ ):
+ self.log.info(
+ "Server already running, sending actual context and exit."
+ )
+ asyncio.run(websocket_server.send_context_change(self.route_name))
+ self.exit()
+ return
+
+ # Add Websocket route
+ websocket_server.add_route("*", "/ws/", WebSocketAsync)
+ # Add after effects route to websocket handler
+
+ print("Adding {} route".format(self.route_name))
+ WebSocketAsync.add_route(
+ self.route_name, PhotoshopRoute
+ )
+ self.log.info("Starting websocket server for host communication")
+ websocket_server.start_server()
+
+ def _start_process(self):
+ if self._process is not None:
+ return
+ self.log.info("Starting host process")
+ try:
+ self._process = subprocess.Popen(
+ self._subprocess_args,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL
+ )
+ except Exception:
+ self.log.info("exce", exc_info=True)
+ self.exit()
+
+
+class PhotoshopRoute(WebSocketRoute):
+ """
+ One route, mimicking external application (like Harmony, etc).
+ All functions could be called from client.
+ 'do_notify' function calls function on the client - mimicking
+ notification after long running job on the server or similar
+ """
+ instance = None
+
+ def init(self, **kwargs):
+ # Python __init__ must be return "self".
+ # This method might return anything.
+ log.debug("someone called Photoshop route")
+ self.instance = self
+ return kwargs
+
+ # server functions
+ async def ping(self):
+ log.debug("someone called Photoshop route ping")
+
+ # This method calls function on the client side
+ # client functions
+ async def set_context(self, project, asset, task):
+ """
+ Sets 'project' and 'asset' to envs, eg. setting context
+
+ Args:
+ project (str)
+ asset (str)
+ """
+ log.info("Setting context change")
+ log.info("project {} asset {} ".format(project, asset))
+ if project:
+ api.Session["AVALON_PROJECT"] = project
+ os.environ["AVALON_PROJECT"] = project
+ if asset:
+ api.Session["AVALON_ASSET"] = asset
+ os.environ["AVALON_ASSET"] = asset
+ if task:
+ api.Session["AVALON_TASK"] = task
+ os.environ["AVALON_TASK"] = task
+
+ async def read(self):
+ log.debug("photoshop.read client calls server server calls "
+ "photoshop client")
+ return await self.socket.call('photoshop.read')
+
+ # panel routes for tools
+ async def creator_route(self):
+ self._tool_route("creator")
+
+ async def workfiles_route(self):
+ self._tool_route("workfiles")
+
+ async def loader_route(self):
+ self._tool_route("loader")
+
+ async def publish_route(self):
+ self._tool_route("publish")
+
+ async def sceneinventory_route(self):
+ self._tool_route("sceneinventory")
+
+ async def subsetmanager_route(self):
+ self._tool_route("subsetmanager")
+
+ async def experimental_tools_route(self):
+ self._tool_route("experimental_tools")
+
+ def _tool_route(self, _tool_name):
+ """The address accessed when clicking on the buttons."""
+
+ ProcessLauncher.execute_in_main_thread(show_tool_by_name, _tool_name)
+
+ # Required return statement.
+ return "nothing"
diff --git a/openpype/hosts/photoshop/api/lib.py b/openpype/hosts/photoshop/api/lib.py
new file mode 100644
index 0000000000..707cd476c5
--- /dev/null
+++ b/openpype/hosts/photoshop/api/lib.py
@@ -0,0 +1,78 @@
+import os
+import sys
+import contextlib
+import traceback
+
+from Qt import QtWidgets
+
+import avalon.api
+
+from openpype.api import Logger
+from openpype.tools.utils import host_tools
+from openpype.lib.remote_publish import headless_publish
+
+from .launch_logic import ProcessLauncher, stub
+
+log = Logger.get_logger(__name__)
+
+
+def safe_excepthook(*args):
+ traceback.print_exception(*args)
+
+
+def main(*subprocess_args):
+ from openpype.hosts.photoshop import api
+
+ avalon.api.install(api)
+ sys.excepthook = safe_excepthook
+
+ # coloring in ConsoleTrayApp
+ os.environ["OPENPYPE_LOG_NO_COLORS"] = "False"
+ app = QtWidgets.QApplication([])
+ app.setQuitOnLastWindowClosed(False)
+
+ launcher = ProcessLauncher(subprocess_args)
+ launcher.start()
+
+ if os.environ.get("HEADLESS_PUBLISH"):
+ launcher.execute_in_main_thread(
+ headless_publish,
+ log,
+ "ClosePS",
+ os.environ.get("IS_TEST")
+ )
+ elif os.environ.get("AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH", True):
+ save = False
+ if os.getenv("WORKFILES_SAVE_AS"):
+ save = True
+
+ launcher.execute_in_main_thread(
+ host_tools.show_workfiles, save=save
+ )
+
+ sys.exit(app.exec_())
+
+
+@contextlib.contextmanager
+def maintained_selection():
+ """Maintain selection during context."""
+ selection = stub().get_selected_layers()
+ try:
+ yield selection
+ finally:
+ stub().select_layers(selection)
+
+
+@contextlib.contextmanager
+def maintained_visibility():
+ """Maintain visibility during context."""
+ visibility = {}
+ layers = stub().get_layers()
+ for layer in layers:
+ visibility[layer.id] = layer.visible
+ try:
+ yield
+ finally:
+ for layer in layers:
+ stub().set_visible(layer.id, visibility[layer.id])
+ pass
diff --git a/openpype/hosts/photoshop/api/panel.PNG b/openpype/hosts/photoshop/api/panel.PNG
new file mode 100644
index 0000000000..be5db3b8df
Binary files /dev/null and b/openpype/hosts/photoshop/api/panel.PNG differ
diff --git a/openpype/hosts/photoshop/api/panel_failure.PNG b/openpype/hosts/photoshop/api/panel_failure.PNG
new file mode 100644
index 0000000000..67afc4e212
Binary files /dev/null and b/openpype/hosts/photoshop/api/panel_failure.PNG differ
diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py
new file mode 100644
index 0000000000..25983f2471
--- /dev/null
+++ b/openpype/hosts/photoshop/api/pipeline.py
@@ -0,0 +1,229 @@
+import os
+import sys
+from Qt import QtWidgets
+
+import pyblish.api
+import avalon.api
+from avalon import pipeline, io
+
+from openpype.api import Logger
+import openpype.hosts.photoshop
+
+from . import lib
+
+log = Logger.get_logger(__name__)
+
+HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.photoshop.__file__))
+PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
+PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
+LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
+CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
+INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
+
+
+def check_inventory():
+ if not lib.any_outdated():
+ return
+
+ host = avalon.api.registered_host()
+ outdated_containers = []
+ for container in host.ls():
+ representation = container['representation']
+ representation_doc = io.find_one(
+ {
+ "_id": io.ObjectId(representation),
+ "type": "representation"
+ },
+ projection={"parent": True}
+ )
+ if representation_doc and not lib.is_latest(representation_doc):
+ outdated_containers.append(container)
+
+ # Warn about outdated containers.
+ print("Starting new QApplication..")
+
+ message_box = QtWidgets.QMessageBox()
+ message_box.setIcon(QtWidgets.QMessageBox.Warning)
+ msg = "There are outdated containers in the scene."
+ message_box.setText(msg)
+ message_box.exec_()
+
+
+def on_application_launch():
+ check_inventory()
+
+
+def on_pyblish_instance_toggled(instance, old_value, new_value):
+ """Toggle layer visibility on instance toggles."""
+ instance[0].Visible = new_value
+
+
+def install():
+ """Install Photoshop-specific functionality of avalon-core.
+
+ This function is called automatically on calling `api.install(photoshop)`.
+ """
+ log.info("Installing OpenPype Photoshop...")
+ pyblish.api.register_host("photoshop")
+
+ pyblish.api.register_plugin_path(PUBLISH_PATH)
+ avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH)
+ avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH)
+ log.info(PUBLISH_PATH)
+
+ pyblish.api.register_callback(
+ "instanceToggled", on_pyblish_instance_toggled
+ )
+
+ avalon.api.on("application.launched", on_application_launch)
+
+
+def uninstall():
+ pyblish.api.deregister_plugin_path(PUBLISH_PATH)
+ avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH)
+ avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH)
+
+
+def ls():
+ """Yields containers from active Photoshop document
+
+ This is the host-equivalent of api.ls(), but instead of listing
+ assets on disk, it lists assets already loaded in Photoshop; once loaded
+ they are called 'containers'
+
+ Yields:
+ dict: container
+
+ """
+ try:
+ stub = lib.stub() # only after Photoshop is up
+ except lib.ConnectionNotEstablishedYet:
+ print("Not connected yet, ignoring")
+ return
+
+ if not stub.get_active_document_name():
+ return
+
+ layers_meta = stub.get_layers_metadata() # minimalize calls to PS
+ for layer in stub.get_layers():
+ data = stub.read(layer, layers_meta)
+
+ # Skip non-tagged layers.
+ if not data:
+ continue
+
+ # Filter to only containers.
+ if "container" not in data["id"]:
+ continue
+
+ # Append transient data
+ data["objectName"] = layer.name.replace(stub.LOADED_ICON, '')
+ data["layer"] = layer
+
+ yield data
+
+
+def list_instances():
+ """List all created instances to publish from current workfile.
+
+ Pulls from File > File Info
+
+ For SubsetManager
+
+ Returns:
+ (list) of dictionaries matching instances format
+ """
+ stub = _get_stub()
+
+ if not stub:
+ return []
+
+ instances = []
+ layers_meta = stub.get_layers_metadata()
+ if layers_meta:
+ for key, instance in layers_meta.items():
+ schema = instance.get("schema")
+ if schema and "container" in schema:
+ continue
+
+ instance['uuid'] = key
+ instances.append(instance)
+
+ return instances
+
+
+def remove_instance(instance):
+ """Remove instance from current workfile metadata.
+
+ Updates metadata of current file in File > File Info and removes
+ icon highlight on group layer.
+
+ For SubsetManager
+
+ Args:
+ instance (dict): instance representation from subsetmanager model
+ """
+ stub = _get_stub()
+
+ if not stub:
+ return
+
+ stub.remove_instance(instance.get("uuid"))
+ layer = stub.get_layer(instance.get("uuid"))
+ if layer:
+ stub.rename_layer(instance.get("uuid"),
+ layer.name.replace(stub.PUBLISH_ICON, ''))
+
+
+def _get_stub():
+ """Handle pulling stub from PS to run operations on host
+
+ Returns:
+ (PhotoshopServerStub) or None
+ """
+ try:
+ stub = lib.stub() # only after Photoshop is up
+ except lib.ConnectionNotEstablishedYet:
+ print("Not connected yet, ignoring")
+ return
+
+ if not stub.get_active_document_name():
+ return
+
+ return stub
+
+
+def containerise(
+ name, namespace, layer, context, loader=None, suffix="_CON"
+):
+ """Imprint layer with metadata
+
+ Containerisation enables a tracking of version, author and origin
+ for loaded assets.
+
+ Arguments:
+ name (str): Name of resulting assembly
+ namespace (str): Namespace under which to host container
+ layer (PSItem): Layer to containerise
+ context (dict): Asset information
+ loader (str, optional): Name of loader used to produce this container.
+ suffix (str, optional): Suffix of container, defaults to `_CON`.
+
+ Returns:
+ container (str): Name of container assembly
+ """
+ layer.name = name + suffix
+
+ data = {
+ "schema": "openpype:container-2.0",
+ "id": pipeline.AVALON_CONTAINER_ID,
+ "name": name,
+ "namespace": namespace,
+ "loader": str(loader),
+ "representation": str(context["representation"]["_id"]),
+ "members": [str(layer.id)]
+ }
+ stub = lib.stub()
+ stub.imprint(layer, data)
+
+ return layer
diff --git a/openpype/hosts/photoshop/api/plugin.py b/openpype/hosts/photoshop/api/plugin.py
new file mode 100644
index 0000000000..e0db67de2c
--- /dev/null
+++ b/openpype/hosts/photoshop/api/plugin.py
@@ -0,0 +1,69 @@
+import re
+
+import avalon.api
+from .launch_logic import stub
+
+
+def get_unique_layer_name(layers, asset_name, subset_name):
+ """
+ Gets all layer names and if 'asset_name_subset_name' is present, it
+ increases suffix by 1 (eg. creates unique layer name - for Loader)
+ Args:
+ layers (list) of dict with layers info (name, id etc.)
+ asset_name (string):
+ subset_name (string):
+
+ Returns:
+ (string): name_00X (without version)
+ """
+ name = "{}_{}".format(asset_name, subset_name)
+ names = {}
+ for layer in layers:
+ layer_name = re.sub(r'_\d{3}$', '', layer.name)
+ if layer_name in names.keys():
+ names[layer_name] = names[layer_name] + 1
+ else:
+ names[layer_name] = 1
+ occurrences = names.get(name, 0)
+
+ return "{}_{:0>3d}".format(name, occurrences + 1)
+
+
+class PhotoshopLoader(avalon.api.Loader):
+ @staticmethod
+ def get_stub():
+ return stub()
+
+
+class Creator(avalon.api.Creator):
+ """Creator plugin to create instances in Photoshop
+
+ A LayerSet is created to support any number of layers in an instance. If
+ the selection is used, these layers will be added to the LayerSet.
+ """
+
+ def process(self):
+ # Photoshop can have multiple LayerSets with the same name, which does
+ # not work with Avalon.
+ msg = "Instance with name \"{}\" already exists.".format(self.name)
+ stub = lib.stub() # only after Photoshop is up
+ for layer in stub.get_layers():
+ if self.name.lower() == layer.Name.lower():
+ msg = QtWidgets.QMessageBox()
+ msg.setIcon(QtWidgets.QMessageBox.Warning)
+ msg.setText(msg)
+ msg.exec_()
+ return False
+
+ # Store selection because adding a group will change selection.
+ with lib.maintained_selection():
+
+ # Add selection to group.
+ if (self.options or {}).get("useSelection"):
+ group = stub.group_selected_layers(self.name)
+ else:
+ group = stub.create_group(self.name)
+
+ stub.imprint(group, self.data)
+
+ return group
diff --git a/openpype/hosts/photoshop/api/workio.py b/openpype/hosts/photoshop/api/workio.py
new file mode 100644
index 0000000000..0bf3ed2bd9
--- /dev/null
+++ b/openpype/hosts/photoshop/api/workio.py
@@ -0,0 +1,51 @@
+"""Host API required Work Files tool"""
+import os
+
+import avalon.api
+
+from . import lib
+
+
+def _active_document():
+ document_name = lib.stub().get_active_document_name()
+ if not document_name:
+ return None
+
+ return document_name
+
+
+def file_extensions():
+ return avalon.api.HOST_WORKFILE_EXTENSIONS["photoshop"]
+
+
+def has_unsaved_changes():
+ if _active_document():
+ return not lib.stub().is_saved()
+
+ return False
+
+
+def save_file(filepath):
+ _, ext = os.path.splitext(filepath)
+ lib.stub().saveAs(filepath, ext[1:], True)
+
+
+def open_file(filepath):
+ lib.stub().open(filepath)
+
+ return True
+
+
+def current_file():
+ try:
+ full_name = lib.stub().get_active_document_full_name()
+ if full_name and full_name != "null":
+ return os.path.normpath(full_name).replace("\\", "/")
+ except Exception:
+ pass
+
+ return None
+
+
+def work_root(session):
+ return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/")
diff --git a/openpype/hosts/photoshop/api/ws_stub.py b/openpype/hosts/photoshop/api/ws_stub.py
new file mode 100644
index 0000000000..b8f66332c6
--- /dev/null
+++ b/openpype/hosts/photoshop/api/ws_stub.py
@@ -0,0 +1,495 @@
+"""
+ Stub handling connection from server to client.
+ Used anywhere solution is calling client methods.
+"""
+import sys
+import json
+import attr
+from wsrpc_aiohttp import WebSocketAsync
+
+from avalon.tools.webserver.app import WebServerTool
+
+
+@attr.s
+class PSItem(object):
+ """
+ Object denoting layer or group item in PS. Each item is created in
+ PS by any Loader, but contains same fields, which are being used
+ in later processing.
+ """
+ # metadata
+ id = attr.ib() # id created by AE, could be used for querying
+ name = attr.ib() # name of item
+ group = attr.ib(default=None) # item type (footage, folder, comp)
+ parents = attr.ib(factory=list)
+ visible = attr.ib(default=True)
+ type = attr.ib(default=None)
+ # all imported elements, single for
+ members = attr.ib(factory=list)
+ long_name = attr.ib(default=None)
+ color_code = attr.ib(default=None) # color code of layer
+
+
+class PhotoshopServerStub:
+ """
+ Stub for calling function on client (Photoshop js) side.
+ Expects that client is already connected (started when avalon menu
+ is opened).
+ 'self.websocketserver.call' is used as async wrapper
+ """
+ PUBLISH_ICON = '\u2117 '
+ LOADED_ICON = '\u25bc'
+
+ def __init__(self):
+ self.websocketserver = WebServerTool.get_instance()
+ self.client = self.get_client()
+
+ @staticmethod
+ def get_client():
+ """
+ Return first connected client to WebSocket
+ TODO implement selection by Route
+ :return: client
+ """
+ clients = WebSocketAsync.get_clients()
+ client = None
+ if len(clients) > 0:
+ key = list(clients.keys())[0]
+ client = clients.get(key)
+
+ return client
+
+ def open(self, path):
+ """Open file located at 'path' (local).
+
+ Args:
+ path(string): file path locally
+ Returns: None
+ """
+ self.websocketserver.call(
+ self.client.call('Photoshop.open', path=path)
+ )
+
+ def read(self, layer, layers_meta=None):
+ """Parses layer metadata from Headline field of active document.
+
+ Args:
+ layer: (PSItem)
+ layers_meta: full list from Headline (for performance in loops)
+ Returns:
+ """
+ if layers_meta is None:
+ layers_meta = self.get_layers_metadata()
+
+ return layers_meta.get(str(layer.id))
+
+ def imprint(self, layer, data, all_layers=None, layers_meta=None):
+ """Save layer metadata to Headline field of active document
+
+ Stores metadata in format:
+ [{
+ "active":true,
+ "subset":"imageBG",
+ "family":"image",
+ "id":"pyblish.avalon.instance",
+ "asset":"Town",
+ "uuid": "8"
+ }] - for created instances
+ OR
+ [{
+ "schema": "openpype:container-2.0",
+ "id": "pyblish.avalon.instance",
+ "name": "imageMG",
+ "namespace": "Jungle_imageMG_001",
+ "loader": "ImageLoader",
+ "representation": "5fbfc0ee30a946093c6ff18a",
+ "members": [
+ "40"
+ ]
+ }] - for loaded instances
+
+ Args:
+ layer (PSItem):
+ data(string): json representation for single layer
+ all_layers (list of PSItem): for performance, could be
+ injected for usage in loop, if not, single call will be
+ triggered
+ layers_meta(string): json representation from Headline
+ (for performance - provide only if imprint is in
+ loop - value should be same)
+ Returns: None
+ """
+ if not layers_meta:
+ layers_meta = self.get_layers_metadata()
+
+ # json.dumps writes integer values in a dictionary to string, so
+ # anticipating it here.
+ if str(layer.id) in layers_meta and layers_meta[str(layer.id)]:
+ if data:
+ layers_meta[str(layer.id)].update(data)
+ else:
+ layers_meta.pop(str(layer.id))
+ else:
+ layers_meta[str(layer.id)] = data
+
+ # Ensure only valid ids are stored.
+ if not all_layers:
+ all_layers = self.get_layers()
+ layer_ids = [layer.id for layer in all_layers]
+ cleaned_data = []
+
+ for layer_id in layers_meta:
+ if int(layer_id) in layer_ids:
+ cleaned_data.append(layers_meta[layer_id])
+
+ payload = json.dumps(cleaned_data, indent=4)
+
+ self.websocketserver.call(
+ self.client.call('Photoshop.imprint', payload=payload)
+ )
+
+ def get_layers(self):
+ """Returns JSON document with all(?) layers in active document.
+
+ Returns:
+ Format of tuple: { 'id':'123',
+ 'name': 'My Layer 1',
+ 'type': 'GUIDE'|'FG'|'BG'|'OBJ'
+ 'visible': 'true'|'false'
+ """
+ res = self.websocketserver.call(
+ self.client.call('Photoshop.get_layers')
+ )
+
+ return self._to_records(res)
+
+ def get_layer(self, layer_id):
+ """
+ Returns PSItem for specific 'layer_id' or None if not found
+ Args:
+ layer_id (string): unique layer id, stored in 'uuid' field
+
+ Returns:
+ (PSItem) or None
+ """
+ layers = self.get_layers()
+ for layer in layers:
+ if str(layer.id) == str(layer_id):
+ return layer
+
+ def get_layers_in_layers(self, layers):
+ """Return all layers that belong to layers (might be groups).
+
+ Args:
+ layers :
+
+ Returns:
+
+ """
+ all_layers = self.get_layers()
+ ret = []
+ parent_ids = set([lay.id for lay in layers])
+
+ for layer in all_layers:
+ parents = set(layer.parents)
+ if len(parent_ids & parents) > 0:
+ ret.append(layer)
+ if layer.id in parent_ids:
+ ret.append(layer)
+
+ return ret
+
+ def create_group(self, name):
+ """Create new group (eg. LayerSet)
+
+ Returns:
+
+ """
+ enhanced_name = self.PUBLISH_ICON + name
+ ret = self.websocketserver.call(
+ self.client.call('Photoshop.create_group', name=enhanced_name)
+ )
+ # create group on PS is asynchronous, returns only id
+ return PSItem(id=ret, name=name, group=True)
+
+ def group_selected_layers(self, name):
+ """Group selected layers into new LayerSet (eg. group)
+
+ Returns:
+ (Layer)
+ """
+ enhanced_name = self.PUBLISH_ICON + name
+ res = self.websocketserver.call(
+ self.client.call(
+ 'Photoshop.group_selected_layers', name=enhanced_name
+ )
+ )
+ res = self._to_records(res)
+ if res:
+ rec = res.pop()
+ rec.name = rec.name.replace(self.PUBLISH_ICON, '')
+ return rec
+ raise ValueError("No group record returned")
+
+ def get_selected_layers(self):
+ """Get a list of actually selected layers.
+
+ Returns:
+ """
+ res = self.websocketserver.call(
+ self.client.call('Photoshop.get_selected_layers')
+ )
+ return self._to_records(res)
+
+ def select_layers(self, layers):
+ """Selects specified layers in Photoshop by its ids.
+
+ Args:
+ layers:
+ """
+ layers_id = [str(lay.id) for lay in layers]
+ self.websocketserver.call(
+ self.client.call(
+ 'Photoshop.select_layers',
+ layers=json.dumps(layers_id)
+ )
+ )
+
+ def get_active_document_full_name(self):
+ """Returns full name with path of active document via ws call
+
+ Returns(string):
+ full path with name
+ """
+ res = self.websocketserver.call(
+ self.client.call('Photoshop.get_active_document_full_name')
+ )
+
+ return res
+
+ def get_active_document_name(self):
+ """Returns just a name of active document via ws call
+
+ Returns(string):
+ file name
+ """
+ return self.websocketserver.call(
+ self.client.call('Photoshop.get_active_document_name')
+ )
+
+ def is_saved(self):
+ """Returns true if no changes in active document
+
+ Returns:
+
+ """
+ return self.websocketserver.call(
+ self.client.call('Photoshop.is_saved')
+ )
+
+ def save(self):
+ """Saves active document"""
+ self.websocketserver.call(
+ self.client.call('Photoshop.save')
+ )
+
+ def saveAs(self, image_path, ext, as_copy):
+ """Saves active document to psd (copy) or png or jpg
+
+ Args:
+ image_path(string): full local path
+ ext:
+ as_copy:
+ Returns: None
+ """
+ self.websocketserver.call(
+ self.client.call(
+ 'Photoshop.saveAs',
+ image_path=image_path,
+ ext=ext,
+ as_copy=as_copy
+ )
+ )
+
+ def set_visible(self, layer_id, visibility):
+ """Set layer with 'layer_id' to 'visibility'
+
+ Args:
+ layer_id:
+ visibility:
+ Returns: None
+ """
+ self.websocketserver.call(
+ self.client.call(
+ 'Photoshop.set_visible',
+ layer_id=layer_id,
+ visibility=visibility
+ )
+ )
+
+ def get_layers_metadata(self):
+ """Reads layers metadata from Headline from active document in PS.
+ (Headline accessible by File > File Info)
+
+ Returns:
+ (string): - json documents
+ example:
+ {"8":{"active":true,"subset":"imageBG",
+ "family":"image","id":"pyblish.avalon.instance",
+ "asset":"Town"}}
+ 8 is layer(group) id - used for deletion, update etc.
+ """
+ layers_data = {}
+ res = self.websocketserver.call(self.client.call('Photoshop.read'))
+ try:
+ layers_data = json.loads(res)
+ except json.decoder.JSONDecodeError:
+ pass
+ # format of metadata changed from {} to [] because of standardization
+ # keep current implementation logic as its working
+ if not isinstance(layers_data, dict):
+ temp_layers_meta = {}
+ for layer_meta in layers_data:
+ layer_id = layer_meta.get("uuid")
+ if not layer_id:
+ layer_id = layer_meta.get("members")[0]
+
+ temp_layers_meta[layer_id] = layer_meta
+ layers_data = temp_layers_meta
+ else:
+ # legacy version of metadata
+ for layer_id, layer_meta in layers_data.items():
+ if layer_meta.get("schema") != "openpype:container-2.0":
+ layer_meta["uuid"] = str(layer_id)
+ else:
+ layer_meta["members"] = [str(layer_id)]
+
+ return layers_data
+
+ def import_smart_object(self, path, layer_name, as_reference=False):
+ """Import the file at `path` as a smart object to active document.
+
+ Args:
+ path (str): File path to import.
+ layer_name (str): Unique layer name to differentiate how many times
+ same smart object was loaded
+ as_reference (bool): pull in content or reference
+ """
+ enhanced_name = self.LOADED_ICON + layer_name
+ res = self.websocketserver.call(
+ self.client.call(
+ 'Photoshop.import_smart_object',
+ path=path,
+ name=enhanced_name,
+ as_reference=as_reference
+ )
+ )
+ rec = self._to_records(res).pop()
+ if rec:
+ rec.name = rec.name.replace(self.LOADED_ICON, '')
+ return rec
+
+ def replace_smart_object(self, layer, path, layer_name):
+ """Replace the smart object `layer` with file at `path`
+
+ Args:
+ layer (PSItem):
+ path (str): File to import.
+ layer_name (str): Unique layer name to differentiate how many times
+ same smart object was loaded
+ """
+ enhanced_name = self.LOADED_ICON + layer_name
+ self.websocketserver.call(
+ self.client.call(
+ 'Photoshop.replace_smart_object',
+ layer_id=layer.id,
+ path=path,
+ name=enhanced_name
+ )
+ )
+
+ def delete_layer(self, layer_id):
+ """Deletes specific layer by it's id.
+
+ Args:
+ layer_id (int): id of layer to delete
+ """
+ self.websocketserver.call(
+ self.client.call('Photoshop.delete_layer', layer_id=layer_id)
+ )
+
+ def rename_layer(self, layer_id, name):
+ """Renames specific layer by it's id.
+
+ Args:
+ layer_id (int): id of layer to delete
+ name (str): new name
+ """
+ self.websocketserver.call(
+ self.client.call(
+ 'Photoshop.rename_layer',
+ layer_id=layer_id,
+ name=name
+ )
+ )
+
+ def remove_instance(self, instance_id):
+ cleaned_data = {}
+
+ for key, instance in self.get_layers_metadata().items():
+ if key != instance_id:
+ cleaned_data[key] = instance
+
+ payload = json.dumps(cleaned_data, indent=4)
+
+ self.websocketserver.call(
+ self.client.call('Photoshop.imprint', payload=payload)
+ )
+
+ def get_extension_version(self):
+ """Returns version number of installed extension."""
+ return self.websocketserver.call(
+ self.client.call('Photoshop.get_extension_version')
+ )
+
+ def close(self):
+ """Shutting down PS and process too.
+
+ For webpublishing only.
+ """
+ # TODO change client.call to method with checks for client
+ self.websocketserver.call(self.client.call('Photoshop.close'))
+
+ def _to_records(self, res):
+ """Converts string json representation into list of PSItem for
+ dot notation access to work.
+
+ Args:
+ res (string): valid json
+
+ Returns:
+
+ """
+ try:
+ layers_data = json.loads(res)
+ except json.decoder.JSONDecodeError:
+ raise ValueError("Received broken JSON {}".format(res))
+ ret = []
+
+ # convert to AEItem to use dot donation
+ if isinstance(layers_data, dict):
+ layers_data = [layers_data]
+ for d in layers_data:
+ # currently implemented and expected fields
+ ret.append(PSItem(
+ d.get('id'),
+ d.get('name'),
+ d.get('group'),
+ d.get('parents'),
+ d.get('visible'),
+ d.get('type'),
+ d.get('members'),
+ d.get('long_name'),
+ d.get("color_code")
+ ))
+ return ret
diff --git a/openpype/hosts/photoshop/hooks/__init__.py b/openpype/hosts/photoshop/hooks/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/openpype/hosts/photoshop/plugins/__init__.py b/openpype/hosts/photoshop/plugins/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py
index 657d41aa93..cf41bb4020 100644
--- a/openpype/hosts/photoshop/plugins/create/create_image.py
+++ b/openpype/hosts/photoshop/plugins/create/create_image.py
@@ -1,6 +1,6 @@
from Qt import QtWidgets
import openpype.api
-from avalon import photoshop
+from openpype.hosts.photoshop import api as photoshop
class CreateImage(openpype.api.Creator):
diff --git a/openpype/hosts/photoshop/plugins/lib.py b/openpype/hosts/photoshop/plugins/lib.py
deleted file mode 100644
index 74aff06114..0000000000
--- a/openpype/hosts/photoshop/plugins/lib.py
+++ /dev/null
@@ -1,26 +0,0 @@
-import re
-
-
-def get_unique_layer_name(layers, asset_name, subset_name):
- """
- Gets all layer names and if 'asset_name_subset_name' is present, it
- increases suffix by 1 (eg. creates unique layer name - for Loader)
- Args:
- layers (list) of dict with layers info (name, id etc.)
- asset_name (string):
- subset_name (string):
-
- Returns:
- (string): name_00X (without version)
- """
- name = "{}_{}".format(asset_name, subset_name)
- names = {}
- for layer in layers:
- layer_name = re.sub(r'_\d{3}$', '', layer.name)
- if layer_name in names.keys():
- names[layer_name] = names[layer_name] + 1
- else:
- names[layer_name] = 1
- occurrences = names.get(name, 0)
-
- return "{}_{:0>3d}".format(name, occurrences + 1)
diff --git a/openpype/hosts/photoshop/plugins/load/load_image.py b/openpype/hosts/photoshop/plugins/load/load_image.py
index 981a1ed204..3b1cfe9636 100644
--- a/openpype/hosts/photoshop/plugins/load/load_image.py
+++ b/openpype/hosts/photoshop/plugins/load/load_image.py
@@ -1,12 +1,11 @@
import re
-from avalon import api, photoshop
+from avalon import api
+from openpype.hosts.photoshop import api as photoshop
+from openpype.hosts.photoshop.api import get_unique_layer_name
-from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name
-stub = photoshop.stub()
-
-class ImageLoader(api.Loader):
+class ImageLoader(photoshop.PhotoshopLoader):
"""Load images
Stores the imported asset in a container named after the asset.
@@ -16,11 +15,14 @@ class ImageLoader(api.Loader):
representations = ["*"]
def load(self, context, name=None, namespace=None, data=None):
- layer_name = get_unique_layer_name(stub.get_layers(),
- context["asset"]["name"],
- name)
+ stub = self.get_stub()
+ layer_name = get_unique_layer_name(
+ stub.get_layers(),
+ context["asset"]["name"],
+ name
+ )
with photoshop.maintained_selection():
- layer = self.import_layer(self.fname, layer_name)
+ layer = self.import_layer(self.fname, layer_name, stub)
self[:] = [layer]
namespace = namespace or layer_name
@@ -35,6 +37,8 @@ class ImageLoader(api.Loader):
def update(self, container, representation):
""" Switch asset or change version """
+ stub = self.get_stub()
+
layer = container.pop("layer")
context = representation.get("context", {})
@@ -44,9 +48,9 @@ class ImageLoader(api.Loader):
layer_name = "{}_{}".format(context["asset"], context["subset"])
# switching assets
if namespace_from_container != layer_name:
- layer_name = get_unique_layer_name(stub.get_layers(),
- context["asset"],
- context["subset"])
+ layer_name = get_unique_layer_name(
+ stub.get_layers(), context["asset"], context["subset"]
+ )
else: # switching version - keep same name
layer_name = container["namespace"]
@@ -66,6 +70,8 @@ class ImageLoader(api.Loader):
Args:
container (dict): container to be removed - used to get layer_id
"""
+ stub = self.get_stub()
+
layer = container.pop("layer")
stub.imprint(layer, {})
stub.delete_layer(layer.id)
@@ -73,5 +79,5 @@ class ImageLoader(api.Loader):
def switch(self, container, representation):
self.update(container, representation)
- def import_layer(self, file_name, layer_name):
+ def import_layer(self, file_name, layer_name, stub):
return stub.import_smart_object(file_name, layer_name)
diff --git a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py
index 8704627b12..6627aded51 100644
--- a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py
+++ b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py
@@ -1,18 +1,14 @@
import os
-from avalon import api
-from avalon import photoshop
from avalon.pipeline import get_representation_path_from_context
from avalon.vendor import qargparse
-from openpype.lib import Anatomy
-from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name
-
-stub = photoshop.stub()
+from openpype.hosts.photoshop import api as photoshop
+from openpype.hosts.photoshop.api import get_unique_layer_name
-class ImageFromSequenceLoader(api.Loader):
- """ Load specifing image from sequence
+class ImageFromSequenceLoader(photoshop.PhotoshopLoader):
+ """ Load specific image from sequence
Used only as quick load of reference file from a sequence.
@@ -35,15 +31,16 @@ class ImageFromSequenceLoader(api.Loader):
def load(self, context, name=None, namespace=None, data=None):
if data.get("frame"):
- self.fname = os.path.join(os.path.dirname(self.fname),
- data["frame"])
+ self.fname = os.path.join(
+ os.path.dirname(self.fname), data["frame"]
+ )
if not os.path.exists(self.fname):
return
- stub = photoshop.stub()
- layer_name = get_unique_layer_name(stub.get_layers(),
- context["asset"]["name"],
- name)
+ stub = self.get_stub()
+ layer_name = get_unique_layer_name(
+ stub.get_layers(), context["asset"]["name"], name
+ )
with photoshop.maintained_selection():
layer = stub.import_smart_object(self.fname, layer_name)
diff --git a/openpype/hosts/photoshop/plugins/load/load_reference.py b/openpype/hosts/photoshop/plugins/load/load_reference.py
index 0cb4e4a69f..60142d4a1f 100644
--- a/openpype/hosts/photoshop/plugins/load/load_reference.py
+++ b/openpype/hosts/photoshop/plugins/load/load_reference.py
@@ -1,30 +1,30 @@
import re
-from avalon import api, photoshop
+from avalon import api
-from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name
-
-stub = photoshop.stub()
+from openpype.hosts.photoshop import api as photoshop
+from openpype.hosts.photoshop.api import get_unique_layer_name
-class ReferenceLoader(api.Loader):
+class ReferenceLoader(photoshop.PhotoshopLoader):
"""Load reference images
- Stores the imported asset in a container named after the asset.
+ Stores the imported asset in a container named after the asset.
- Inheriting from 'load_image' didn't work because of
- "Cannot write to closing transport", possible refactor.
+ Inheriting from 'load_image' didn't work because of
+ "Cannot write to closing transport", possible refactor.
"""
families = ["image", "render"]
representations = ["*"]
def load(self, context, name=None, namespace=None, data=None):
- layer_name = get_unique_layer_name(stub.get_layers(),
- context["asset"]["name"],
- name)
+ stub = self.get_stub()
+ layer_name = get_unique_layer_name(
+ stub.get_layers(), context["asset"]["name"], name
+ )
with photoshop.maintained_selection():
- layer = self.import_layer(self.fname, layer_name)
+ layer = self.import_layer(self.fname, layer_name, stub)
self[:] = [layer]
namespace = namespace or layer_name
@@ -39,6 +39,7 @@ class ReferenceLoader(api.Loader):
def update(self, container, representation):
""" Switch asset or change version """
+ stub = self.get_stub()
layer = container.pop("layer")
context = representation.get("context", {})
@@ -48,9 +49,9 @@ class ReferenceLoader(api.Loader):
layer_name = "{}_{}".format(context["asset"], context["subset"])
# switching assets
if namespace_from_container != layer_name:
- layer_name = get_unique_layer_name(stub.get_layers(),
- context["asset"],
- context["subset"])
+ layer_name = get_unique_layer_name(
+ stub.get_layers(), context["asset"], context["subset"]
+ )
else: # switching version - keep same name
layer_name = container["namespace"]
@@ -65,11 +66,12 @@ class ReferenceLoader(api.Loader):
)
def remove(self, container):
- """
- Removes element from scene: deletes layer + removes from Headline
+ """Removes element from scene: deletes layer + removes from Headline
+
Args:
container (dict): container to be removed - used to get layer_id
"""
+ stub = self.get_stub()
layer = container.pop("layer")
stub.imprint(layer, {})
stub.delete_layer(layer.id)
@@ -77,6 +79,7 @@ class ReferenceLoader(api.Loader):
def switch(self, container, representation):
self.update(container, representation)
- def import_layer(self, file_name, layer_name):
- return stub.import_smart_object(file_name, layer_name,
- as_reference=True)
+ def import_layer(self, file_name, layer_name, stub):
+ return stub.import_smart_object(
+ file_name, layer_name, as_reference=True
+ )
diff --git a/openpype/hosts/photoshop/plugins/publish/closePS.py b/openpype/hosts/photoshop/plugins/publish/closePS.py
index 2f0eab0ee5..b4ded96001 100644
--- a/openpype/hosts/photoshop/plugins/publish/closePS.py
+++ b/openpype/hosts/photoshop/plugins/publish/closePS.py
@@ -4,7 +4,7 @@ import os
import pyblish.api
-from avalon import photoshop
+from openpype.hosts.photoshop import api as photoshop
class ClosePS(pyblish.api.ContextPlugin):
diff --git a/openpype/hosts/photoshop/plugins/publish/collect_current_file.py b/openpype/hosts/photoshop/plugins/publish/collect_current_file.py
index 4d4829555e..5daf47c6ac 100644
--- a/openpype/hosts/photoshop/plugins/publish/collect_current_file.py
+++ b/openpype/hosts/photoshop/plugins/publish/collect_current_file.py
@@ -2,7 +2,7 @@ import os
import pyblish.api
-from avalon import photoshop
+from openpype.hosts.photoshop import api as photoshop
class CollectCurrentFile(pyblish.api.ContextPlugin):
diff --git a/openpype/hosts/photoshop/plugins/publish/collect_extension_version.py b/openpype/hosts/photoshop/plugins/publish/collect_extension_version.py
index f07ff0b0ff..64c99b4fc1 100644
--- a/openpype/hosts/photoshop/plugins/publish/collect_extension_version.py
+++ b/openpype/hosts/photoshop/plugins/publish/collect_extension_version.py
@@ -2,7 +2,7 @@ import os
import re
import pyblish.api
-from avalon import photoshop
+from openpype.hosts.photoshop import api as photoshop
class CollectExtensionVersion(pyblish.api.ContextPlugin):
diff --git a/openpype/hosts/photoshop/plugins/publish/collect_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_instances.py
index 5390df768b..f67cc0cbac 100644
--- a/openpype/hosts/photoshop/plugins/publish/collect_instances.py
+++ b/openpype/hosts/photoshop/plugins/publish/collect_instances.py
@@ -1,6 +1,6 @@
import pyblish.api
-from avalon import photoshop
+from openpype.hosts.photoshop import api as photoshop
class CollectInstances(pyblish.api.ContextPlugin):
diff --git a/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py
index c76e15484e..e264d04d9f 100644
--- a/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py
+++ b/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py
@@ -1,10 +1,11 @@
-import pyblish.api
import os
import re
-from avalon import photoshop
+import pyblish.api
+
from openpype.lib import prepare_template_data
from openpype.lib.plugin_tools import parse_json
+from openpype.hosts.photoshop import api as photoshop
class CollectRemoteInstances(pyblish.api.ContextPlugin):
diff --git a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py
index 88817c3969..db1ede14d5 100644
--- a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py
+++ b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py
@@ -1,5 +1,5 @@
-import pyblish.api
import os
+import pyblish.api
class CollectWorkfile(pyblish.api.ContextPlugin):
diff --git a/openpype/hosts/photoshop/plugins/publish/extract_image.py b/openpype/hosts/photoshop/plugins/publish/extract_image.py
index ae9892e290..2ba81e0bac 100644
--- a/openpype/hosts/photoshop/plugins/publish/extract_image.py
+++ b/openpype/hosts/photoshop/plugins/publish/extract_image.py
@@ -1,7 +1,7 @@
import os
import openpype.api
-from avalon import photoshop
+from openpype.hosts.photoshop import api as photoshop
class ExtractImage(openpype.api.Extractor):
diff --git a/openpype/hosts/photoshop/plugins/publish/extract_review.py b/openpype/hosts/photoshop/plugins/publish/extract_review.py
index 8c4d05b282..1ad442279a 100644
--- a/openpype/hosts/photoshop/plugins/publish/extract_review.py
+++ b/openpype/hosts/photoshop/plugins/publish/extract_review.py
@@ -2,7 +2,7 @@ import os
import openpype.api
import openpype.lib
-from avalon import photoshop
+from openpype.hosts.photoshop import api as photoshop
class ExtractReview(openpype.api.Extractor):
diff --git a/openpype/hosts/photoshop/plugins/publish/extract_save_scene.py b/openpype/hosts/photoshop/plugins/publish/extract_save_scene.py
index 0180640c90..03086f389f 100644
--- a/openpype/hosts/photoshop/plugins/publish/extract_save_scene.py
+++ b/openpype/hosts/photoshop/plugins/publish/extract_save_scene.py
@@ -1,5 +1,5 @@
import openpype.api
-from avalon import photoshop
+from openpype.hosts.photoshop import api as photoshop
class ExtractSaveScene(openpype.api.Extractor):
diff --git a/openpype/hosts/photoshop/plugins/publish/increment_workfile.py b/openpype/hosts/photoshop/plugins/publish/increment_workfile.py
index 709fb988fc..92132c393b 100644
--- a/openpype/hosts/photoshop/plugins/publish/increment_workfile.py
+++ b/openpype/hosts/photoshop/plugins/publish/increment_workfile.py
@@ -3,7 +3,7 @@ import pyblish.api
from openpype.action import get_errored_plugins_from_data
from openpype.lib import version_up
-from avalon import photoshop
+from openpype.hosts.photoshop import api as photoshop
class IncrementWorkfile(pyblish.api.InstancePlugin):
diff --git a/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py b/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py
index 4dc1972074..ebe9cc21ea 100644
--- a/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py
+++ b/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py
@@ -1,7 +1,7 @@
from avalon import api
import pyblish.api
import openpype.api
-from avalon import photoshop
+from openpype.hosts.photoshop import api as photoshop
class ValidateInstanceAssetRepair(pyblish.api.Action):
diff --git a/openpype/hosts/photoshop/plugins/publish/validate_naming.py b/openpype/hosts/photoshop/plugins/publish/validate_naming.py
index 1635096f4b..b40e44d016 100644
--- a/openpype/hosts/photoshop/plugins/publish/validate_naming.py
+++ b/openpype/hosts/photoshop/plugins/publish/validate_naming.py
@@ -2,7 +2,7 @@ import re
import pyblish.api
import openpype.api
-from avalon import photoshop
+from openpype.hosts.photoshop import api as photoshop
class ValidateNamingRepair(pyblish.api.Action):
diff --git a/openpype/hosts/resolve/README.markdown b/openpype/hosts/resolve/README.markdown
index 50664fbd21..8c9f72fb0c 100644
--- a/openpype/hosts/resolve/README.markdown
+++ b/openpype/hosts/resolve/README.markdown
@@ -4,10 +4,10 @@
- add absolute path to ffmpeg into openpype settings

- install Python 3.6 into `%LOCALAPPDATA%/Programs/Python/Python36` (only respected path by Resolve)
-- install OpenTimelineIO for 3.6 `%LOCALAPPDATA%\Programs\Python\Python36\python.exe -m pip install git+https://github.com/PixarAnimationStudios/OpenTimelineIO.git@5aa24fbe89d615448876948fe4b4900455c9a3e8` and move builded files from `%LOCALAPPDATA%/Programs/Python/Python36/Lib/site-packages/opentimelineio/cxx-libs/bin and lib` to `%LOCALAPPDATA%/Programs/Python/Python36/Lib/site-packages/opentimelineio/`. I was building it on Win10 machine with Visual Studio Community 2019 and
+- install OpenTimelineIO for 3.6 `%LOCALAPPDATA%\Programs\Python\Python36\python.exe -m pip install git+https://github.com/PixarAnimationStudios/OpenTimelineIO.git@5aa24fbe89d615448876948fe4b4900455c9a3e8` and move built files from `%LOCALAPPDATA%/Programs/Python/Python36/Lib/site-packages/opentimelineio/cxx-libs/bin and lib` to `%LOCALAPPDATA%/Programs/Python/Python36/Lib/site-packages/opentimelineio/`. I was building it on Win10 machine with Visual Studio Community 2019 and
 with installed CMake in PATH.
- install PySide2 for 3.6 `%LOCALAPPDATA%\Programs\Python\Python36\python.exe -m pip install PySide2`
-- make sure Resovle Fusion (Fusion Tab/menu/Fusion/Fusion Setings) is set to Python 3.6
+- make sure Resolve Fusion (Fusion Tab/menu/Fusion/Fusion Settings) is set to Python 3.6

#### Editorial setup
diff --git a/openpype/hosts/resolve/RESOLVE_API_README_v16.2.0_up.txt b/openpype/hosts/resolve/RESOLVE_API_README_v16.2.0_up.txt
index a24a053cd7..f1b8b81a71 100644
--- a/openpype/hosts/resolve/RESOLVE_API_README_v16.2.0_up.txt
+++ b/openpype/hosts/resolve/RESOLVE_API_README_v16.2.0_up.txt
@@ -366,7 +366,7 @@ TimelineItem
DeleteTakeByIndex(idx) --> Bool # Deletes a take by index, 1 <= idx <= number of takes.
SelectTakeByIndex(idx) --> Bool # Selects a take by index, 1 <= idx <= number of takes.
FinalizeTake() --> Bool # Finalizes take selection.
- CopyGrades([tgtTimelineItems]) --> Bool # Copies the current grade to all the items in tgtTimelineItems list. Returns True on success and False if any error occured.
+ CopyGrades([tgtTimelineItems]) --> Bool # Copies the current grade to all the items in tgtTimelineItems list. Returns True on success and False if any error occurred.
List and Dict Data Structures
diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py
index aa4b2e7219..22f83c6eed 100644
--- a/openpype/hosts/resolve/api/lib.py
+++ b/openpype/hosts/resolve/api/lib.py
@@ -16,7 +16,7 @@ self = sys.modules[__name__]
self.project_manager = None
self.media_storage = None
-# OpenPype sequencial rename variables
+# OpenPype sequential rename variables
self.rename_index = 0
self.rename_add = 0
@@ -59,7 +59,7 @@ def maintain_current_timeline(to_timeline: object,
project = get_current_project()
working_timeline = from_timeline or project.GetCurrentTimeline()
- # swith to the input timeline
+ # switch to the input timeline
project.SetCurrentTimeline(to_timeline)
try:
@@ -566,7 +566,7 @@ def create_compound_clip(clip_data, name, folder):
mp_in_rc = opentime.RationalTime((ci_l_offset), rate)
mp_out_rc = opentime.RationalTime((ci_l_offset + ci_duration - 1), rate)
- # get frame in and out for clip swaping
+ # get frame in and out for clip swapping
in_frame = opentime.to_frames(mp_in_rc)
out_frame = opentime.to_frames(mp_out_rc)
@@ -628,7 +628,7 @@ def create_compound_clip(clip_data, name, folder):
def swap_clips(from_clip, to_clip, to_in_frame, to_out_frame):
"""
- Swaping clips on timeline in timelineItem
+ Swapping clips on timeline in timelineItem
It will add take and activate it to the frame range which is inputted
@@ -699,7 +699,7 @@ def get_pype_clip_metadata(clip):
def get_clip_attributes(clip):
"""
- Collect basic atrributes from resolve timeline item
+ Collect basic attributes from resolve timeline item
Args:
clip (resolve.TimelineItem): timeline item object
diff --git a/openpype/hosts/resolve/api/pipeline.py b/openpype/hosts/resolve/api/pipeline.py
index ce95cfe02a..8b7e2a6c6a 100644
--- a/openpype/hosts/resolve/api/pipeline.py
+++ b/openpype/hosts/resolve/api/pipeline.py
@@ -64,7 +64,7 @@ def install():
def uninstall():
- """Uninstall all tha was installed
+ """Uninstall all that was installed
This is where you undo everything that was done in `install()`.
That means, removing menus, deregistering families and data
diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py
index f1c55a6180..8612cf82ec 100644
--- a/openpype/hosts/resolve/api/plugin.py
+++ b/openpype/hosts/resolve/api/plugin.py
@@ -133,7 +133,7 @@ class CreatorWidget(QtWidgets.QDialog):
# convert label text to normal capitalized text with spaces
label_text = self.camel_case_split(text)
- # assign the new text to lable widget
+ # assign the new text to label widget
label = QtWidgets.QLabel(label_text)
label.setObjectName("LineLabel")
@@ -367,7 +367,7 @@ class ClipLoader:
def _get_asset_data(self):
""" Get all available asset data
- joint `data` key with asset.data dict into the representaion
+ joint `data` key with asset.data dict into the representation
"""
asset_name = self.context["representation"]["context"]["asset"]
@@ -540,8 +540,8 @@ class PublishClip:
"track": "sequence",
}
- # parents search patern
- parents_search_patern = r"\{([a-z]*?)\}"
+ # parents search pattern
+ parents_search_pattern = r"\{([a-z]*?)\}"
# default templates for non-ui use
rename_default = False
@@ -630,7 +630,7 @@ class PublishClip:
return self.timeline_item
def _populate_timeline_item_default_data(self):
- """ Populate default formating data from track item. """
+ """ Populate default formatting data from track item. """
self.timeline_item_default_data = {
"_folder_": "shots",
@@ -722,7 +722,7 @@ class PublishClip:
# mark review layer
if self.review_track and (
self.review_track not in self.review_track_default):
- # if review layer is defined and not the same as defalut
+ # if review layer is defined and not the same as default
self.review_layer = self.review_track
# shot num calculate
if self.rename_index == 0:
@@ -771,7 +771,7 @@ class PublishClip:
# in case track name and subset name is the same then add
if self.subset_name == self.track_name:
hero_data["subset"] = self.subset
- # assing data to return hierarchy data to tag
+ # assign data to return hierarchy data to tag
tag_hierarchy_data = hero_data
# add data to return data dict
@@ -823,8 +823,8 @@ class PublishClip:
""" Create parents and return it in list. """
self.parents = []
- patern = re.compile(self.parents_search_patern)
- par_split = [patern.findall(t).pop()
+ pattern = re.compile(self.parents_search_pattern)
+ par_split = [pattern.findall(t).pop()
for t in self.hierarchy.split("/")]
for key in par_split:
diff --git a/openpype/hosts/resolve/api/testing_utils.py b/openpype/hosts/resolve/api/testing_utils.py
index 98ad6abcf1..4aac66f4b7 100644
--- a/openpype/hosts/resolve/api/testing_utils.py
+++ b/openpype/hosts/resolve/api/testing_utils.py
@@ -25,12 +25,12 @@ class TestGUI:
ui.Button(
{
"ID": "inputTestSourcesFolder",
- "Text": "Select folder with testing medias",
+ "Text": "Select folder with testing media",
"Weight": 1.25,
"ToolTip": (
"Chose folder with videos, sequences, "
"single images, nested folders with "
- "medias"
+ "media"
),
"Flat": False
}
diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py
index bcb27e24fc..978e3760fd 100644
--- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py
+++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py
@@ -15,7 +15,7 @@ class ResolvePrelaunch(PreLaunchHook):
def execute(self):
# TODO: add OTIO installation from `openpype/requirements.py`
- # making sure pyton 3.6 is installed at provided path
+ # making sure python 3.6 is installed at provided path
py36_dir = os.path.normpath(
self.launch_context.env.get("PYTHON36_RESOLVE", ""))
assert os.path.isdir(py36_dir), (
diff --git a/openpype/hosts/resolve/otio/davinci_export.py b/openpype/hosts/resolve/otio/davinci_export.py
index 2c276d9888..5f11c81fc5 100644
--- a/openpype/hosts/resolve/otio/davinci_export.py
+++ b/openpype/hosts/resolve/otio/davinci_export.py
@@ -306,7 +306,7 @@ def create_otio_timeline(resolve_project):
if index == 0:
otio_track.append(clip)
else:
- # add previouse otio track to timeline
+ # add previous otio track to timeline
otio_timeline.tracks.append(otio_track)
# convert track to otio
otio_track = create_otio_track(
diff --git a/openpype/hosts/resolve/plugins/create/create_shot_clip.py b/openpype/hosts/resolve/plugins/create/create_shot_clip.py
index 41fdbf5c61..62d5557a50 100644
--- a/openpype/hosts/resolve/plugins/create/create_shot_clip.py
+++ b/openpype/hosts/resolve/plugins/create/create_shot_clip.py
@@ -135,7 +135,7 @@ class CreateShotClip(resolve.Creator):
"type": "QComboBox",
"label": "Subset Name",
"target": "ui",
- "toolTip": "chose subset name patern, if is selected, name of track layer will be used", # noqa
+ "toolTip": "chose subset name pattern, if is selected, name of track layer will be used", # noqa
"order": 0},
"subsetFamily": {
"value": ["plate", "take"],
diff --git a/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py b/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py
index b1037a9c93..b0cef1838a 100644
--- a/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py
+++ b/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py
@@ -16,7 +16,7 @@ def main(env):
# activate resolve from openpype
avalon.install(bmdvr)
- log.info(f"Avalon registred hosts: {avalon.registered_host()}")
+ log.info(f"Avalon registered hosts: {avalon.registered_host()}")
bmdvr.launch_pype_menu()
diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py
index 45c6a264dd..d0d36bb717 100644
--- a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py
+++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py
@@ -83,7 +83,7 @@ class CollectInstances(pyblish.api.InstancePlugin):
if isinstance(clip, otio.schema.Gap):
continue
- # skip all generators like black ampty
+ # skip all generators like black empty
if isinstance(
clip.media_reference,
otio.schema.GeneratorReference):
@@ -142,7 +142,7 @@ class CollectInstances(pyblish.api.InstancePlugin):
"item": clip,
"clipName": clip_name,
- # parent time properities
+ # parent time properties
"trackStartFrame": track_start_frame,
"handleStart": handle_start,
"handleEnd": handle_end,
@@ -180,14 +180,14 @@ class CollectInstances(pyblish.api.InstancePlugin):
"families": []
}
})
- for subset, properities in self.subsets.items():
- version = properities.get("version")
+ for subset, properties in self.subsets.items():
+ version = properties.get("version")
if version == 0:
- properities.pop("version")
+ properties.pop("version")
# adding Review-able instance
subset_instance_data = deepcopy(instance_data)
- subset_instance_data.update(deepcopy(properities))
+ subset_instance_data.update(deepcopy(properties))
subset_instance_data.update({
# unique attributes
"name": f"{name}_{subset}",
diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py
index 36bacceb1c..4d7a13fcf2 100644
--- a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py
+++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py
@@ -31,7 +31,7 @@ class CollectInstanceResources(pyblish.api.InstancePlugin):
editorial_source_root = instance.data["editorialSourceRoot"]
editorial_source_path = instance.data["editorialSourcePath"]
- # if `editorial_source_path` then loop trough
+ # if `editorial_source_path` then loop through
if editorial_source_path:
# add family if mov or mp4 found which is longer for
# cutting `trimming` to enable `ExtractTrimmingVideoAudio` plugin
@@ -42,7 +42,7 @@ class CollectInstanceResources(pyblish.api.InstancePlugin):
instance.data["families"] += ["trimming"]
return
- # if template patern in path then fill it with `anatomy_data`
+ # if template pattern in path then fill it with `anatomy_data`
if "{" in editorial_source_root:
editorial_source_root = editorial_source_root.format(
**anatomy_data)
@@ -86,7 +86,7 @@ class CollectInstanceResources(pyblish.api.InstancePlugin):
subset_files.update({clip_dir_path: subset_files_items})
# break the loop if correct_clip_dir was captured
- # no need to cary on if corect folder was found
+ # no need to cary on if correct folder was found
if correct_clip_dir:
break
@@ -113,10 +113,10 @@ class CollectInstanceResources(pyblish.api.InstancePlugin):
unique_subset_names = list()
root_dir = list(subset_files.keys()).pop()
files_list = subset_files[root_dir]
- search_patern = f"({subset}[A-Za-z0-9]+)(?=[\\._\\s])"
+ search_pattern = f"({subset}[A-Za-z0-9]+)(?=[\\._\\s])"
for _file in files_list:
- patern = re.compile(search_patern)
- match = patern.findall(_file)
+ pattern = re.compile(search_pattern)
+ match = pattern.findall(_file)
if not match:
continue
match_subset = match.pop()
@@ -175,7 +175,7 @@ class CollectInstanceResources(pyblish.api.InstancePlugin):
instance_data["representations"] = list()
collection_head_name = None
- # loop trough collections and create representations
+ # loop through collections and create representations
for _collection in collections:
ext = _collection.tail[1:]
collection_head_name = _collection.head
@@ -210,7 +210,7 @@ class CollectInstanceResources(pyblish.api.InstancePlugin):
frames.append(frame_start)
frames.append(frame_end)
- # loop trough reminders and create representations
+ # loop through reminders and create representations
for _reminding_file in remainder:
ext = os.path.splitext(_reminding_file)[-1][1:]
if ext not in instance_data["extensions"]:
diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_hierarchy.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_hierarchy.py
index acad98d784..b2735f3428 100644
--- a/openpype/hosts/standalonepublisher/plugins/publish/collect_hierarchy.py
+++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_hierarchy.py
@@ -99,7 +99,7 @@ class CollectHierarchyInstance(pyblish.api.ContextPlugin):
# in case SP context is set to the same folder
if (_index == 0) and ("folder" in parent_key) \
and (parents[-1]["entity_name"] == parent_filled):
- self.log.debug(f" skiping : {parent_filled}")
+ self.log.debug(f" skipping : {parent_filled}")
continue
# in case first parent is project then start parents from start
@@ -119,7 +119,7 @@ class CollectHierarchyInstance(pyblish.api.ContextPlugin):
# convert hierarchy to string
hierarchy = "/".join(hierarchy)
- # assing to instance data
+ # assign to instance data
instance.data["hierarchy"] = hierarchy
instance.data["parents"] = parents
@@ -202,7 +202,7 @@ class CollectHierarchyInstance(pyblish.api.ContextPlugin):
class CollectHierarchyContext(pyblish.api.ContextPlugin):
- '''Collecting Hierarchy from instaces and building
+ '''Collecting Hierarchy from instances and building
context hierarchy tree
'''
diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_representation_names.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_representation_names.py
index c9063c22ed..82dbba3345 100644
--- a/openpype/hosts/standalonepublisher/plugins/publish/collect_representation_names.py
+++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_representation_names.py
@@ -8,7 +8,7 @@ class CollectRepresentationNames(pyblish.api.InstancePlugin):
Sets the representation names for given families based on RegEx filter
"""
- label = "Collect Representaion Names"
+ label = "Collect Representation Names"
order = pyblish.api.CollectorOrder
families = []
hosts = ["standalonepublisher"]
diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_name.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_name.py
index f210be3631..4bafe81020 100644
--- a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_name.py
+++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_name.py
@@ -16,7 +16,7 @@ class ValidateTextureBatchNaming(pyblish.api.InstancePlugin):
if isinstance(file_name, list):
file_name = file_name[0]
- msg = "Couldnt find asset name in '{}'\n".format(file_name) + \
+ msg = "Couldn't find asset name in '{}'\n".format(file_name) + \
"File name doesn't follow configured pattern.\n" + \
"Please rename the file."
assert "NOT_AVAIL" not in instance.data["asset_build"], msg
diff --git a/openpype/hosts/tvpaint/api/communication_server.py b/openpype/hosts/tvpaint/api/communication_server.py
index 6c8aca5445..c8d6d3b458 100644
--- a/openpype/hosts/tvpaint/api/communication_server.py
+++ b/openpype/hosts/tvpaint/api/communication_server.py
@@ -351,7 +351,7 @@ class QtTVPaintRpc(BaseTVPaintRpc):
async def scene_inventory_tool(self):
"""Open Scene Inventory tool.
- Funciton can't confirm if tool was opened becauise one part of
+ Function can't confirm if tool was opened becauise one part of
SceneInventory initialization is calling websocket request to host but
host can't response because is waiting for response from this call.
"""
@@ -578,7 +578,7 @@ class BaseCommunicator:
# Folder for right windows plugin files
source_plugins_dir = os.path.join(plugin_files_path, subfolder)
- # Path to libraies (.dll) required for plugin library
+ # Path to libraries (.dll) required for plugin library
# - additional libraries can be copied to TVPaint installation folder
# (next to executable) or added to PATH environment variable
additional_libs_folder = os.path.join(
diff --git a/openpype/hosts/tvpaint/api/lib.py b/openpype/hosts/tvpaint/api/lib.py
index 654aff19d8..9e6404e72f 100644
--- a/openpype/hosts/tvpaint/api/lib.py
+++ b/openpype/hosts/tvpaint/api/lib.py
@@ -159,7 +159,7 @@ def get_layers_data(layer_ids=None, communicator=None):
def parse_group_data(data):
- """Paser group data collected in 'get_groups_data'."""
+ """Parse group data collected in 'get_groups_data'."""
output = []
groups_raw = data.split("\n")
for group_raw in groups_raw:
diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py
index e7c5159bbc..6b4632e2f2 100644
--- a/openpype/hosts/tvpaint/api/pipeline.py
+++ b/openpype/hosts/tvpaint/api/pipeline.py
@@ -112,7 +112,7 @@ def containerise(
members (list): List of members that were loaded and belongs
to the container (layer names).
current_containers (list): Preloaded containers. Should be used only
- on update/switch when containers were modified durring the process.
+ on update/switch when containers were modified during the process.
Returns:
dict: Container data stored to workfile metadata.
@@ -166,7 +166,7 @@ def split_metadata_string(text, chunk_length=None):
set to global variable `TVPAINT_CHUNK_LENGTH`.
Returns:
- list: List of strings wil at least one item.
+ list: List of strings with at least one item.
"""
if chunk_length is None:
chunk_length = TVPAINT_CHUNK_LENGTH
diff --git a/openpype/hosts/tvpaint/api/plugin.py b/openpype/hosts/tvpaint/api/plugin.py
index e65c25b8d1..af80c9eae2 100644
--- a/openpype/hosts/tvpaint/api/plugin.py
+++ b/openpype/hosts/tvpaint/api/plugin.py
@@ -35,7 +35,7 @@ class Creator(PypeCreatorMixin, avalon.api.Creator):
def are_instances_same(instance_1, instance_2):
"""Compare instances but skip keys with unique values.
- During compare are skiped keys that will be 100% sure
+ During compare are skipped keys that will be 100% sure
different on new instance, like "id".
Returns:
diff --git a/openpype/hosts/tvpaint/hooks/pre_launch_args.py b/openpype/hosts/tvpaint/hooks/pre_launch_args.py
index 62fd662d79..2a8f49d5b0 100644
--- a/openpype/hosts/tvpaint/hooks/pre_launch_args.py
+++ b/openpype/hosts/tvpaint/hooks/pre_launch_args.py
@@ -4,7 +4,7 @@ import shutil
from openpype.hosts import tvpaint
from openpype.lib import (
PreLaunchHook,
- get_pype_execute_args
+ get_openpype_execute_args
)
import avalon
@@ -30,7 +30,7 @@ class TvpaintPrelaunchHook(PreLaunchHook):
while self.launch_context.launch_args:
remainders.append(self.launch_context.launch_args.pop(0))
- new_launch_args = get_pype_execute_args(
+ new_launch_args = get_openpype_execute_args(
"run", self.launch_script_path(), executable_path
)
diff --git a/openpype/hosts/tvpaint/lib.py b/openpype/hosts/tvpaint/lib.py
index 513bb2d952..715ebb4a9d 100644
--- a/openpype/hosts/tvpaint/lib.py
+++ b/openpype/hosts/tvpaint/lib.py
@@ -278,7 +278,7 @@ def _cleanup_out_range_frames(output_idx_by_frame_idx, range_start, range_end):
}
// Result
{
- 2: 2, // Redirect to self as is first that refence out range
+ 2: 2, // Redirect to self as is first that reference out range
3: 2 // Redirect to first redirected frame
}
```
@@ -593,7 +593,7 @@ def composite_rendered_layers(
transparent_filepaths.add(dst_filepath)
continue
- # Store first destionation filepath to be used for transparent images
+ # Store first destination filepath to be used for transparent images
if first_dst_filepath is None:
first_dst_filepath = dst_filepath
@@ -657,7 +657,7 @@ def rename_filepaths_by_frame_start(
max(range_end, new_frame_end)
)
- # Use differnet ranges based on Mark In and output Frame Start values
+ # Use different ranges based on Mark In and output Frame Start values
# - this is to make sure that filename renaming won't affect files that
# are not renamed yet
if range_start < new_frame_start:
diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py
index 31d2fd1fd5..9cbfb61550 100644
--- a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py
+++ b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py
@@ -77,7 +77,7 @@ class CollectInstances(pyblish.api.ContextPlugin):
# Project name from workfile context
project_name = context.data["workfile_context"]["project"]
- # Host name from environemnt variable
+ # Host name from environment variable
host_name = os.environ["AVALON_APP"]
# Use empty variant value
variant = ""
diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py
index 68ba350a85..89348037d3 100644
--- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py
+++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py
@@ -35,7 +35,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin):
# Project name from workfile context
project_name = context.data["workfile_context"]["project"]
- # Host name from environemnt variable
+ # Host name from environment variable
host_name = os.environ["AVALON_APP"]
# Use empty variant value
variant = ""
diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py
index b6b8bd0d9e..729c545545 100644
--- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py
+++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py
@@ -168,7 +168,7 @@ class ExtractSequence(pyblish.api.Extractor):
if single_file:
repre_files = repre_files[0]
- # Extension is harcoded
+ # Extension is hardcoded
# - changing extension would require change code
new_repre = {
"name": "png",
@@ -235,7 +235,7 @@ class ExtractSequence(pyblish.api.Extractor):
scene_bg_color (list): Bg color set in scene. Result of george
script command `tv_background`.
- Retruns:
+ Returns:
tuple: With 2 items first is list of filenames second is path to
thumbnail.
"""
@@ -311,7 +311,7 @@ class ExtractSequence(pyblish.api.Extractor):
mark_out (int): On which frame index export will end.
layers (list): List of layers to be exported.
- Retruns:
+ Returns:
tuple: With 2 items first is list of filenames second is path to
thumbnail.
"""
diff --git a/openpype/hosts/tvpaint/plugins/publish/increment_workfile_version.py b/openpype/hosts/tvpaint/plugins/publish/increment_workfile_version.py
index c9f2434cef..24d6558168 100644
--- a/openpype/hosts/tvpaint/plugins/publish/increment_workfile_version.py
+++ b/openpype/hosts/tvpaint/plugins/publish/increment_workfile_version.py
@@ -15,7 +15,7 @@ class IncrementWorkfileVersion(pyblish.api.ContextPlugin):
def process(self, context):
assert all(result["success"] for result in context.data["results"]), (
- "Publishing not succesfull so version is not increased.")
+ "Publishing not successful so version is not increased.")
path = context.data["currentFile"]
workio.save_file(version_up(path))
diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_marks.py b/openpype/hosts/tvpaint/plugins/publish/validate_marks.py
index 9d55bb21a9..f45247ceac 100644
--- a/openpype/hosts/tvpaint/plugins/publish/validate_marks.py
+++ b/openpype/hosts/tvpaint/plugins/publish/validate_marks.py
@@ -44,7 +44,7 @@ class ValidateMarks(pyblish.api.ContextPlugin):
handle_start = context.data["handleStart"]
handle_end = context.data["handleEnd"]
- # Calculate expeted Mark out (Mark In + duration - 1)
+ # Calculate expected Mark out (Mark In + duration - 1)
expected_mark_out = (
scene_mark_in
+ (frame_end - frame_start)
diff --git a/openpype/hosts/tvpaint/tvpaint_plugin/plugin_code/README.md b/openpype/hosts/tvpaint/tvpaint_plugin/plugin_code/README.md
index 03b0a31f51..70a96b2919 100644
--- a/openpype/hosts/tvpaint/tvpaint_plugin/plugin_code/README.md
+++ b/openpype/hosts/tvpaint/tvpaint_plugin/plugin_code/README.md
@@ -3,7 +3,7 @@ README for TVPaint Avalon plugin
Introduction
------------
This project is dedicated to integrate Avalon functionality to TVPaint.
-This implementaiton is using TVPaint plugin (C/C++) which can communicate with python process. The communication should allow to trigger tools or pipeline functions from TVPaint and accept requests from python process at the same time.
+This implementation is using TVPaint plugin (C/C++) which can communicate with python process. The communication should allow to trigger tools or pipeline functions from TVPaint and accept requests from python process at the same time.
Current implementation is based on websocket protocol, using json-rpc communication (specification 2.0). Project is in beta stage, tested only on Windows.
diff --git a/openpype/hosts/tvpaint/tvpaint_plugin/plugin_code/library.cpp b/openpype/hosts/tvpaint/tvpaint_plugin/plugin_code/library.cpp
index a57124084b..bb67715cbd 100644
--- a/openpype/hosts/tvpaint/tvpaint_plugin/plugin_code/library.cpp
+++ b/openpype/hosts/tvpaint/tvpaint_plugin/plugin_code/library.cpp
@@ -41,7 +41,7 @@ static struct {
nlohmann::json menuItemsById;
std::list menuItemsIds;
// Messages from server before processing.
- // - messages can't be process at the moment of recieve as client is running in thread
+ // - messages can't be process at the moment of receive as client is running in thread
std::queue messages;
// Responses to requests mapped by request id
std::map responses;
@@ -694,7 +694,7 @@ int newMenuItemsProcess(PIFilter* iFilter) {
return 1;
}
/**************************************************************************************/
-// something happenned that needs our attention.
+// something happened that needs our attention.
// Global variable where current button up data are stored
std::string button_up_item_id_str;
int FAR PASCAL PI_Msg( PIFilter* iFilter, INTPTR iEvent, INTPTR iReq, INTPTR* iArgs )
diff --git a/openpype/hosts/tvpaint/worker/worker_job.py b/openpype/hosts/tvpaint/worker/worker_job.py
index 519d42ce73..1c785ab2ee 100644
--- a/openpype/hosts/tvpaint/worker/worker_job.py
+++ b/openpype/hosts/tvpaint/worker/worker_job.py
@@ -41,7 +41,7 @@ class BaseCommand:
Command also have id which is created on command creation.
The idea is that command is just a data container on sender side send
- througth server to a worker where is replicated one by one, executed and
+ through server to a worker where is replicated one by one, executed and
result sent back to sender through server.
"""
@abstractproperty
@@ -248,7 +248,7 @@ class ExecuteGeorgeScript(BaseCommand):
class CollectSceneData(BaseCommand):
- """Helper command which will collect all usefull info about workfile.
+ """Helper command which will collect all useful info about workfile.
Result is dictionary with all layers data, exposure frames by layer ids
pre/post behavior of layers by their ids, group information and scene data.
diff --git a/openpype/hosts/unreal/api/lib.py b/openpype/hosts/unreal/api/lib.py
index c0fafbb667..61dac46fac 100644
--- a/openpype/hosts/unreal/api/lib.py
+++ b/openpype/hosts/unreal/api/lib.py
@@ -115,7 +115,7 @@ def _darwin_get_engine_version() -> dict:
Returns:
dict: version as a key and path as a value.
- See Aslo:
+ See Also:
:func:`_win_get_engine_versions`.
"""
diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_geometrycache.py b/openpype/hosts/unreal/plugins/load/load_alembic_geometrycache.py
index ad37a7a068..e2023e8b47 100644
--- a/openpype/hosts/unreal/plugins/load/load_alembic_geometrycache.py
+++ b/openpype/hosts/unreal/plugins/load/load_alembic_geometrycache.py
@@ -98,7 +98,7 @@ class PointCacheAlembicLoader(api.Loader):
frame_start = context.get('asset').get('data').get('frameStart')
frame_end = context.get('asset').get('data').get('frameEnd')
- # If frame start and end are the same, we increse the end frame by
+ # If frame start and end are the same, we increase the end frame by
# one, otherwise Unreal will not import it
if frame_start == frame_end:
frame_end += 1
diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_batch_data.py b/openpype/hosts/webpublisher/plugins/publish/collect_batch_data.py
index a710fcb3e8..062c5ce0da 100644
--- a/openpype/hosts/webpublisher/plugins/publish/collect_batch_data.py
+++ b/openpype/hosts/webpublisher/plugins/publish/collect_batch_data.py
@@ -12,7 +12,7 @@ from openpype.lib.plugin_tools import (
parse_json,
get_batch_asset_task_info
)
-from openpype.lib.remote_publish import get_webpublish_conn
+from openpype.lib.remote_publish import get_webpublish_conn, IN_PROGRESS_STATUS
class CollectBatchData(pyblish.api.ContextPlugin):
@@ -74,7 +74,7 @@ class CollectBatchData(pyblish.api.ContextPlugin):
dbcon.update_one(
{
"batch_id": batch_id,
- "status": "in_progress"
+ "status": IN_PROGRESS_STATUS
},
{
"$set": {
diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py
index d2754b3df3..c1b1d66cb8 100644
--- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py
+++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py
@@ -21,6 +21,11 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
This collector will try to find json files in provided
`OPENPYPE_PUBLISH_DATA`. Those files _MUST_ share same context.
+ This covers 'basic' webpublishes, eg artists uses Standalone Publisher to
+ publish rendered frames or assets.
+
+ This is not applicable for 'studio' processing where host application is
+ called to process uploaded workfile and render frames itself.
"""
# must be really early, context values are only in json file
order = pyblish.api.CollectorOrder - 0.490
diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_instances.py b/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_instances.py
index 976a14e808..92f581be5f 100644
--- a/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_instances.py
+++ b/openpype/hosts/webpublisher/plugins/publish/collect_tvpaint_instances.py
@@ -28,7 +28,7 @@ class CollectTVPaintInstances(pyblish.api.ContextPlugin):
render_layer_pass_name = "beauty"
# Set by settings
- # Regex must constain 'layer' and 'variant' groups which are extracted from
+ # Regex must contain 'layer' and 'variant' groups which are extracted from
# name when instances are created
layer_name_regex = r"(?PL[0-9]{3}_\w+)_(?P.+)"
diff --git a/openpype/hosts/webpublisher/plugins/publish/extract_tvpaint_workfile.py b/openpype/hosts/webpublisher/plugins/publish/extract_tvpaint_workfile.py
index 85c8526c83..2142d740a5 100644
--- a/openpype/hosts/webpublisher/plugins/publish/extract_tvpaint_workfile.py
+++ b/openpype/hosts/webpublisher/plugins/publish/extract_tvpaint_workfile.py
@@ -286,7 +286,7 @@ class ExtractTVPaintSequences(pyblish.api.Extractor):
if single_file:
repre_files = repre_files[0]
- # Extension is harcoded
+ # Extension is hardcoded
# - changing extension would require change code
new_repre = {
"name": "png",
@@ -407,7 +407,7 @@ class ExtractTVPaintSequences(pyblish.api.Extractor):
mark_out (int): On which frame index export will end.
layers (list): List of layers to be exported.
- Retruns:
+ Returns:
tuple: With 2 items first is list of filenames second is path to
thumbnail.
"""
diff --git a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py
index 30399a6ba7..e2d041b512 100644
--- a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py
+++ b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py
@@ -11,10 +11,14 @@ from avalon.api import AvalonMongoDB
from openpype.lib import OpenPypeMongoConnection
from openpype_modules.avalon_apps.rest_api import _RestApiEndpoint
-from openpype.lib.remote_publish import get_task_data
from openpype.settings import get_project_settings
from openpype.lib import PypeLogger
+from openpype.lib.remote_publish import (
+ get_task_data,
+ ERROR_STATUS,
+ REPROCESS_STATUS
+)
log = PypeLogger.get_logger("WebServer")
@@ -61,7 +65,7 @@ class OpenPypeRestApiResource(RestApiResource):
self.dbcon = mongo_client[database_name]["webpublishes"]
-class WebpublisherProjectsEndpoint(_RestApiEndpoint):
+class ProjectsEndpoint(_RestApiEndpoint):
"""Returns list of dict with project info (id, name)."""
async def get(self) -> Response:
output = []
@@ -82,7 +86,7 @@ class WebpublisherProjectsEndpoint(_RestApiEndpoint):
)
-class WebpublisherHiearchyEndpoint(_RestApiEndpoint):
+class HiearchyEndpoint(_RestApiEndpoint):
"""Returns dictionary with context tree from assets."""
async def get(self, project_name) -> Response:
query_projection = {
@@ -181,7 +185,7 @@ class TaskNode(Node):
self["attributes"] = {}
-class WebpublisherBatchPublishEndpoint(_RestApiEndpoint):
+class BatchPublishEndpoint(_RestApiEndpoint):
"""Triggers headless publishing of batch."""
async def post(self, request) -> Response:
# Validate existence of openpype executable
@@ -190,7 +194,7 @@ class WebpublisherBatchPublishEndpoint(_RestApiEndpoint):
msg = "Non existent OpenPype executable {}".format(openpype_app)
raise RuntimeError(msg)
- log.info("WebpublisherBatchPublishEndpoint called")
+ log.info("BatchPublishEndpoint called")
content = await request.json()
# Each filter have extensions which are checked on first task item
@@ -286,7 +290,7 @@ class WebpublisherBatchPublishEndpoint(_RestApiEndpoint):
)
-class WebpublisherTaskPublishEndpoint(_RestApiEndpoint):
+class TaskPublishEndpoint(_RestApiEndpoint):
"""Prepared endpoint triggered after each task - for future development."""
async def post(self, request) -> Response:
return Response(
@@ -301,21 +305,37 @@ class BatchStatusEndpoint(_RestApiEndpoint):
async def get(self, batch_id) -> Response:
output = self.dbcon.find_one({"batch_id": batch_id})
+ if output:
+ status = 200
+ else:
+ output = {"msg": "Batch id {} not found".format(batch_id),
+ "status": "queued",
+ "progress": 0}
+ status = 404
+ body = self.resource.encode(output)
return Response(
- status=200,
- body=self.resource.encode(output),
+ status=status,
+ body=body,
content_type="application/json"
)
-class PublishesStatusEndpoint(_RestApiEndpoint):
+class UserReportEndpoint(_RestApiEndpoint):
"""Returns list of dict with batch info for user (email address)."""
async def get(self, user) -> Response:
- output = list(self.dbcon.find({"user": user}))
+ output = list(self.dbcon.find({"user": user},
+ projection={"log": False}))
+
+ if output:
+ status = 200
+ else:
+ output = {"msg": "User {} not found".format(user)}
+ status = 404
+ body = self.resource.encode(output)
return Response(
- status=200,
- body=self.resource.encode(output),
+ status=status,
+ body=body,
content_type="application/json"
)
@@ -335,7 +355,7 @@ class ConfiguredExtensionsEndpoint(_RestApiEndpoint):
configured = {
"file_exts": set(),
"sequence_exts": set(),
- # workfiles that could have "Studio Procesing" hardcoded for now
+ # workfiles that could have "Studio Processing" hardcoded for now
"studio_exts": set(["psd", "psb", "tvpp", "tvp"])
}
collect_conf = sett["webpublisher"]["publish"]["CollectPublishedFiles"]
@@ -351,3 +371,28 @@ class ConfiguredExtensionsEndpoint(_RestApiEndpoint):
body=self.resource.encode(dict(configured)),
content_type="application/json"
)
+
+
+class BatchReprocessEndpoint(_RestApiEndpoint):
+ """Marks latest 'batch_id' for reprocessing, returns 404 if not found."""
+ async def post(self, batch_id) -> Response:
+ batches = self.dbcon.find({"batch_id": batch_id,
+ "status": ERROR_STATUS}).sort("_id", -1)
+
+ if batches:
+ self.dbcon.update_one(
+ {"_id": batches[0]["_id"]},
+ {"$set": {"status": REPROCESS_STATUS}}
+ )
+ output = [{"msg": "Batch id {} set to reprocess".format(batch_id)}]
+ status = 200
+ else:
+ output = [{"msg": "Batch id {} not found".format(batch_id)}]
+ status = 404
+ body = self.resource.encode(output)
+
+ return Response(
+ status=status,
+ body=body,
+ content_type="application/json"
+ )
diff --git a/openpype/hosts/webpublisher/webserver_service/webserver_cli.py b/openpype/hosts/webpublisher/webserver_service/webserver_cli.py
index c96ad8e110..909ea38bc6 100644
--- a/openpype/hosts/webpublisher/webserver_service/webserver_cli.py
+++ b/openpype/hosts/webpublisher/webserver_service/webserver_cli.py
@@ -11,13 +11,19 @@ from openpype.lib import PypeLogger
from .webpublish_routes import (
RestApiResource,
OpenPypeRestApiResource,
- WebpublisherBatchPublishEndpoint,
- WebpublisherTaskPublishEndpoint,
- WebpublisherHiearchyEndpoint,
- WebpublisherProjectsEndpoint,
+ HiearchyEndpoint,
+ ProjectsEndpoint,
+ ConfiguredExtensionsEndpoint,
+ BatchPublishEndpoint,
+ BatchReprocessEndpoint,
BatchStatusEndpoint,
- PublishesStatusEndpoint,
- ConfiguredExtensionsEndpoint
+ TaskPublishEndpoint,
+ UserReportEndpoint
+)
+from openpype.lib.remote_publish import (
+ ERROR_STATUS,
+ REPROCESS_STATUS,
+ SENT_REPROCESSING_STATUS
)
@@ -41,14 +47,14 @@ def run_webserver(*args, **kwargs):
upload_dir=kwargs["upload_dir"],
executable=kwargs["executable"],
studio_task_queue=studio_task_queue)
- projects_endpoint = WebpublisherProjectsEndpoint(resource)
+ projects_endpoint = ProjectsEndpoint(resource)
server_manager.add_route(
"GET",
"/api/projects",
projects_endpoint.dispatch
)
- hiearchy_endpoint = WebpublisherHiearchyEndpoint(resource)
+ hiearchy_endpoint = HiearchyEndpoint(resource)
server_manager.add_route(
"GET",
"/api/hierarchy/{project_name}",
@@ -64,7 +70,7 @@ def run_webserver(*args, **kwargs):
# triggers publish
webpublisher_task_publish_endpoint = \
- WebpublisherBatchPublishEndpoint(resource)
+ BatchPublishEndpoint(resource)
server_manager.add_route(
"POST",
"/api/webpublish/batch",
@@ -72,7 +78,7 @@ def run_webserver(*args, **kwargs):
)
webpublisher_batch_publish_endpoint = \
- WebpublisherTaskPublishEndpoint(resource)
+ TaskPublishEndpoint(resource)
server_manager.add_route(
"POST",
"/api/webpublish/task",
@@ -88,13 +94,21 @@ def run_webserver(*args, **kwargs):
batch_status_endpoint.dispatch
)
- user_status_endpoint = PublishesStatusEndpoint(openpype_resource)
+ user_status_endpoint = UserReportEndpoint(openpype_resource)
server_manager.add_route(
"GET",
"/api/publishes/{user}",
user_status_endpoint.dispatch
)
+ webpublisher_batch_reprocess_endpoint = \
+ BatchReprocessEndpoint(openpype_resource)
+ server_manager.add_route(
+ "POST",
+ "/api/webpublish/reprocess/{batch_id}",
+ webpublisher_batch_reprocess_endpoint.dispatch
+ )
+
server_manager.start_server()
last_reprocessed = time.time()
while True:
@@ -116,8 +130,12 @@ def reprocess_failed(upload_dir, webserver_url):
database_name = os.environ["OPENPYPE_DATABASE_NAME"]
dbcon = mongo_client[database_name]["webpublishes"]
- results = dbcon.find({"status": "reprocess"})
+ results = dbcon.find({"status": REPROCESS_STATUS})
+ reprocessed_batches = set()
for batch in results:
+ if batch["batch_id"] in reprocessed_batches:
+ continue
+
batch_url = os.path.join(upload_dir,
batch["batch_id"],
"manifest.json")
@@ -130,8 +148,8 @@ def reprocess_failed(upload_dir, webserver_url):
{"$set":
{
"finish_date": datetime.now(),
- "status": "error",
- "progress": 1,
+ "status": ERROR_STATUS,
+ "progress": 100,
"log": batch.get("log") + msg
}}
)
@@ -141,18 +159,24 @@ def reprocess_failed(upload_dir, webserver_url):
with open(batch_url) as f:
data = json.loads(f.read())
+ dbcon.update_many(
+ {
+ "batch_id": batch["batch_id"],
+ "status": {"$in": [ERROR_STATUS, REPROCESS_STATUS]}
+ },
+ {
+ "$set": {
+ "finish_date": datetime.now(),
+ "status": SENT_REPROCESSING_STATUS,
+ "progress": 100
+ }
+ }
+ )
+
try:
r = requests.post(server_url, json=data)
log.info("response{}".format(r))
except Exception:
log.info("exception", exc_info=True)
- dbcon.update_one(
- {"_id": batch["_id"]},
- {"$set":
- {
- "finish_date": datetime.now(),
- "status": "sent_for_reprocessing",
- "progress": 1
- }}
- )
+ reprocessed_batches.add(batch["batch_id"])
diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py
index 34926453cb..1c8f7a57af 100644
--- a/openpype/lib/__init__.py
+++ b/openpype/lib/__init__.py
@@ -24,10 +24,13 @@ from .env_tools import (
from .terminal import Terminal
from .execute import (
+ get_openpype_execute_args,
get_pype_execute_args,
get_linux_launcher_args,
execute,
run_subprocess,
+ run_openpype_process,
+ clean_envs_for_openpype_process,
path_to_subprocess_arg,
CREATE_NO_WINDOW
)
@@ -165,18 +168,26 @@ from .editorial import (
make_sequence_collection
)
-from .pype_info import (
+from .openpype_version import (
+ op_version_control_available,
get_openpype_version,
- get_build_version
+ get_build_version,
+ get_expected_version,
+ is_running_from_build,
+ is_running_staging,
+ is_current_version_studio_latest
)
terminal = Terminal
__all__ = [
+ "get_openpype_execute_args",
"get_pype_execute_args",
"get_linux_launcher_args",
"execute",
"run_subprocess",
+ "run_openpype_process",
+ "clean_envs_for_openpype_process",
"path_to_subprocess_arg",
"CREATE_NO_WINDOW",
@@ -296,6 +307,11 @@ __all__ = [
"create_workdir_extra_folders",
"get_project_basic_paths",
+ "op_version_control_available",
"get_openpype_version",
"get_build_version",
+ "get_expected_version",
+ "is_running_from_build",
+ "is_running_staging",
+ "is_current_version_studio_latest",
]
diff --git a/openpype/lib/abstract_collect_render.py b/openpype/lib/abstract_collect_render.py
index 2ac0fe434d..d9c8a0993d 100644
--- a/openpype/lib/abstract_collect_render.py
+++ b/openpype/lib/abstract_collect_render.py
@@ -49,7 +49,7 @@ class RenderInstance(object):
handleStart = attr.ib(default=None) # start frame
handleEnd = attr.ib(default=None) # start frame
- # for softwares (like Harmony) where frame range cannot be set by DB
+ # for software (like Harmony) where frame range cannot be set by DB
# handles need to be propagated if exist
ignoreFrameHandleCheck = attr.ib(default=False)
@@ -57,7 +57,7 @@ class RenderInstance(object):
# With default values
# metadata
renderer = attr.ib(default="") # renderer - can be used in Deadline
- review = attr.ib(default=False) # genereate review from instance (bool)
+ review = attr.ib(default=False) # generate review from instance (bool)
priority = attr.ib(default=50) # job priority on farm
family = attr.ib(default="renderlayer")
diff --git a/openpype/lib/abstract_submit_deadline.py b/openpype/lib/abstract_submit_deadline.py
index 5b6e1743e0..a0925283ac 100644
--- a/openpype/lib/abstract_submit_deadline.py
+++ b/openpype/lib/abstract_submit_deadline.py
@@ -485,7 +485,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin):
def get_aux_files(self):
"""Return list of auxiliary files for Deadline job.
- If needed this should be overriden, otherwise return empty list as
+ If needed this should be overridden, otherwise return empty list as
that field even empty must be present on Deadline submission.
Returns:
diff --git a/openpype/lib/anatomy.py b/openpype/lib/anatomy.py
index 5f7285fe6c..fa81a18ff7 100644
--- a/openpype/lib/anatomy.py
+++ b/openpype/lib/anatomy.py
@@ -125,7 +125,7 @@ class Anatomy:
@staticmethod
def _prepare_anatomy_data(anatomy_data):
- """Prepare anatomy data for futher processing.
+ """Prepare anatomy data for further processing.
Method added to replace `{task}` with `{task[name]}` in templates.
"""
@@ -722,7 +722,7 @@ class Templates:
First is collecting all global keys (keys in top hierarchy where value
is not dictionary). All global keys are set for all group keys (keys
in top hierarchy where value is dictionary). Value of a key is not
- overriden in group if already contain value for the key.
+ overridden in group if already contain value for the key.
In second part all keys with "at" symbol in value are replaced with
value of the key afterward "at" symbol from the group.
@@ -802,7 +802,7 @@ class Templates:
Result:
tuple: Contain origin template without missing optional keys and
- withoud optional keys identificator ("<" and ">"), information
+ without optional keys identificator ("<" and ">"), information
about missing optional keys and invalid types of optional keys.
"""
@@ -1628,7 +1628,7 @@ class Roots:
This property returns roots for current project or default root values.
Warning:
Default roots value may cause issues when project use different
- roots settings. That may happend when project use multiroot
+ roots settings. That may happen when project use multiroot
templates but default roots miss their keys.
"""
if self.project_name != self.loaded_project:
diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py
index d0438e12a6..0e1f44391e 100644
--- a/openpype/lib/applications.py
+++ b/openpype/lib/applications.py
@@ -47,7 +47,7 @@ DEFAULT_ENV_SUBGROUP = "standard"
def parse_environments(env_data, env_group=None, platform_name=None):
- """Parse environment values from settings byt group and platfrom.
+ """Parse environment values from settings byt group and platform.
Data may contain up to 2 hierarchical levels of dictionaries. At the end
of the last level must be string or list. List is joined using platform
@@ -261,7 +261,7 @@ class Application:
data (dict): Data for the version containing information about
executables, variant label or if is enabled.
Only required key is `executables`.
- group (ApplicationGroup): App group object that created the applicaiton
+ group (ApplicationGroup): App group object that created the application
and under which application belongs.
"""
@@ -775,7 +775,7 @@ class PostLaunchHook(LaunchHook):
class ApplicationLaunchContext:
"""Context of launching application.
- Main purpose of context is to prepare launch arguments and keword arguments
+ Main purpose of context is to prepare launch arguments and keyword arguments
for new process. Most important part of keyword arguments preparations
are environment variables.
@@ -969,7 +969,7 @@ class ApplicationLaunchContext:
hook = klass(self)
if not hook.is_valid:
self.log.debug(
- "Hook is not valid for curent launch context."
+ "Hook is not valid for current launch context."
)
continue
@@ -1113,7 +1113,7 @@ class ApplicationLaunchContext:
))
# TODO how to handle errors?
- # - store to variable to let them accesible?
+ # - store to variable to let them accessible?
try:
postlaunch_hook.execute()
@@ -1357,11 +1357,11 @@ def apply_project_environments_value(
):
"""Apply project specific environments on passed environments.
- The enviornments are applied on passed `env` argument value so it is not
+ The environments are applied on passed `env` argument value so it is not
required to apply changes back.
Args:
- project_name (str): Name of project for which environemnts should be
+ project_name (str): Name of project for which environments should be
received.
env (dict): Environment values on which project specific environments
will be applied.
@@ -1391,7 +1391,7 @@ def apply_project_environments_value(
def prepare_context_environments(data, env_group=None):
- """Modify launch environemnts with context data for launched host.
+ """Modify launch environments with context data for launched host.
Args:
data (EnvironmentPrepData): Dictionary where result and intermediate
@@ -1463,7 +1463,7 @@ def prepare_context_environments(data, env_group=None):
"AVALON_WORKDIR": workdir
}
log.debug(
- "Context environemnts set:\n{}".format(
+ "Context environments set:\n{}".format(
json.dumps(context_env, indent=4)
)
)
@@ -1567,7 +1567,7 @@ def should_start_last_workfile(
):
"""Define if host should start last version workfile if possible.
- Default output is `False`. Can be overriden with environment variable
+ Default output is `False`. Can be overridden with environment variable
`AVALON_OPEN_LAST_WORKFILE`, valid values without case sensitivity are
`"0", "1", "true", "false", "yes", "no"`.
@@ -1617,7 +1617,7 @@ def should_workfile_tool_start(
):
"""Define if host should start workfile tool at host launch.
- Default output is `False`. Can be overriden with environment variable
+ Default output is `False`. Can be overridden with environment variable
`OPENPYPE_WORKFILE_TOOL_ON_START`, valid values without case sensitivity are
`"0", "1", "true", "false", "yes", "no"`.
diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py
index 8180e416a9..1254580657 100644
--- a/openpype/lib/avalon_context.py
+++ b/openpype/lib/avalon_context.py
@@ -443,7 +443,7 @@ def get_workfile_template_key(
Function is using profiles from project settings to return right template
for passet task type and host name.
- One of 'project_name' or 'project_settings' must be passed it is preffered
+ One of 'project_name' or 'project_settings' must be passed it is preferred
to pass settings if are already available.
Args:
@@ -545,7 +545,7 @@ def get_workdir_with_workdir_data(
"""Fill workdir path from entered data and project's anatomy.
It is possible to pass only project's name instead of project's anatomy but
- one of them **must** be entered. It is preffered to enter anatomy if is
+ one of them **must** be entered. It is preferred to enter anatomy if is
available as initialization of a new Anatomy object may be time consuming.
Args:
@@ -582,7 +582,7 @@ def get_workdir_with_workdir_data(
)
anatomy_filled = anatomy.format(workdir_data)
- # Output is TemplateResult object which contain usefull data
+ # Output is TemplateResult object which contain useful data
return anatomy_filled[template_key]["folder"]
@@ -604,7 +604,7 @@ def get_workdir(
because workdir template may contain `{app}` key. In `Session`
is stored under `AVALON_APP` key.
anatomy (Anatomy): Optional argument. Anatomy object is created using
- project name from `project_doc`. It is preffered to pass this
+ project name from `project_doc`. It is preferred to pass this
argument as initialization of a new Anatomy object may be time
consuming.
template_key (str): Key of work templates in anatomy templates. Default
@@ -619,7 +619,7 @@ def get_workdir(
workdir_data = get_workdir_data(
project_doc, asset_doc, task_name, host_name
)
- # Output is TemplateResult object which contain usefull data
+ # Output is TemplateResult object which contain useful data
return get_workdir_with_workdir_data(
workdir_data, anatomy, template_key=template_key
)
@@ -1036,7 +1036,7 @@ class BuildWorkfile:
return valid_profiles
def _prepare_profile_for_subsets(self, subsets, profiles):
- """Select profile for each subset byt it's data.
+ """Select profile for each subset by it's data.
Profiles are filtered for each subset individually.
Profile is filtered by subset's family, optionally by name regex and
@@ -1197,7 +1197,7 @@ class BuildWorkfile:
Representations are tried to load by names defined in configuration.
If subset has representation matching representation name each loader
is tried to load it until any is successful. If none of them was
- successful then next reprensentation name is tried.
+ successful then next representation name is tried.
Subset process loop ends when any representation is loaded or
all matching representations were already tried.
@@ -1240,7 +1240,7 @@ class BuildWorkfile:
print("representations", representations)
- # Load ordered reprensentations.
+ # Load ordered representations.
for subset_id, repres in representations_ordered:
subset_name = subsets_by_id[subset_id]["name"]
diff --git a/openpype/lib/editorial.py b/openpype/lib/editorial.py
index 8e8e365bdb..bf868953ea 100644
--- a/openpype/lib/editorial.py
+++ b/openpype/lib/editorial.py
@@ -116,7 +116,7 @@ def range_from_frames(start, duration, fps):
fps (float): frame range
Returns:
- otio._ot._ot.TimeRange: crated range
+ otio._ot._ot.TimeRange: created range
"""
return _ot.TimeRange(
@@ -131,7 +131,7 @@ def frames_to_secons(frames, framerate):
Args:
frames (int): frame
- framerate (flaot): frame rate
+ framerate (float): frame rate
Returns:
float: second value
@@ -257,7 +257,7 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end):
((source_range.duration.value - 1) * abs(
time_scalar)) + offset_out))
- # calculate available hanles
+ # calculate available handles
if (media_in_trimmed - media_in) < handle_start:
handle_start = (media_in_trimmed - media_in)
if (media_out - media_out_trimmed) < handle_end:
diff --git a/openpype/lib/env_tools.py b/openpype/lib/env_tools.py
index ede14e00b2..6521d20f1e 100644
--- a/openpype/lib/env_tools.py
+++ b/openpype/lib/env_tools.py
@@ -28,11 +28,11 @@ def env_value_to_bool(env_key=None, value=None, default=False):
def get_paths_from_environ(env_key=None, env_value=None, return_first=False):
- """Return existing paths from specific envirnment variable.
+ """Return existing paths from specific environment variable.
Args:
env_key (str): Environment key where should look for paths.
- env_value (str): Value of environemnt variable. Argument `env_key` is
+ env_value (str): Value of environment variable. Argument `env_key` is
skipped if this argument is entered.
return_first (bool): Return first found value or return list of found
paths. `None` or empty list returned if nothing found.
diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py
index f97617d906..afde844f2d 100644
--- a/openpype/lib/execute.py
+++ b/openpype/lib/execute.py
@@ -79,7 +79,7 @@ def run_subprocess(*args, **kwargs):
Args:
*args: Variable length arument list passed to Popen.
- **kwargs : Arbitary keyword arguments passed to Popen. Is possible to
+ **kwargs : Arbitrary keyword arguments passed to Popen. Is possible to
pass `logging.Logger` object under "logger" if want to use
different than lib's logger.
@@ -119,7 +119,7 @@ def run_subprocess(*args, **kwargs):
if _stderr:
_stderr = _stderr.decode("utf-8")
- # Add additional line break if output already containt stdout
+ # Add additional line break if output already contains stdout
if full_output:
full_output += "\n"
full_output += _stderr
@@ -138,6 +138,49 @@ def run_subprocess(*args, **kwargs):
return full_output
+def clean_envs_for_openpype_process(env=None):
+ """Modify environemnts that may affect OpenPype process.
+
+ Main reason to implement this function is to pop PYTHONPATH which may be
+ affected by in-host environments.
+ """
+ if env is None:
+ env = os.environ
+ return {
+ key: value
+ for key, value in env.items()
+ if key not in ("PYTHONPATH",)
+ }
+
+
+def run_openpype_process(*args, **kwargs):
+ """Execute OpenPype process with passed arguments and wait.
+
+ Wrapper for 'run_process' which prepends OpenPype executable arguments
+ before passed arguments and define environments if are not passed.
+
+ Values from 'os.environ' are used for environments if are not passed.
+ They are cleaned using 'clean_envs_for_openpype_process' function.
+
+ Example:
+ ```
+ run_openpype_process("run", "")
+ ```
+
+ Args:
+ *args (tuple): OpenPype cli arguments.
+ **kwargs (dict): Keyword arguments for for subprocess.Popen.
+ """
+ args = get_openpype_execute_args(*args)
+ env = kwargs.pop("env", None)
+ # Keep env untouched if are passed and not empty
+ if not env:
+ # Skip envs that can affect OpenPype process
+ # - fill more if you find more
+ env = clean_envs_for_openpype_process(os.environ)
+ return run_subprocess(args, env=env, **kwargs)
+
+
def path_to_subprocess_arg(path):
"""Prepare path for subprocess arguments.
@@ -147,6 +190,18 @@ def path_to_subprocess_arg(path):
def get_pype_execute_args(*args):
+ """Backwards compatible function for 'get_openpype_execute_args'."""
+ import traceback
+
+ log = Logger.get_logger("get_pype_execute_args")
+ stack = "\n".join(traceback.format_stack())
+ log.warning((
+ "Using deprecated function 'get_pype_execute_args'. Called from:\n{}"
+ ).format(stack))
+ return get_openpype_execute_args(*args)
+
+
+def get_openpype_execute_args(*args):
"""Arguments to run pype command.
Arguments for subprocess when need to spawn new pype process. Which may be
diff --git a/openpype/lib/git_progress.py b/openpype/lib/git_progress.py
index e9cf9a12e1..331b7b6745 100644
--- a/openpype/lib/git_progress.py
+++ b/openpype/lib/git_progress.py
@@ -33,7 +33,7 @@ class _GitProgress(git.remote.RemoteProgress):
self._t.close()
def _detroy_tqdm(self):
- """ Used to close tqdm when opration ended.
+ """ Used to close tqdm when operation ended.
"""
if self._t is not None:
diff --git a/openpype/lib/import_utils.py b/openpype/lib/import_utils.py
index 4e72618803..e88c07fca6 100644
--- a/openpype/lib/import_utils.py
+++ b/openpype/lib/import_utils.py
@@ -14,7 +14,7 @@ def discover_host_vendor_module(module_name):
pype_root, "hosts", host, "vendor", main_module)
log.debug(
- "Importing moduel from host vendor path: `{}`".format(module_path))
+ "Importing module from host vendor path: `{}`".format(module_path))
if not os.path.exists(module_path):
log.warning(
diff --git a/openpype/lib/mongo.py b/openpype/lib/mongo.py
index 7e0bd4f796..c08e76c75c 100644
--- a/openpype/lib/mongo.py
+++ b/openpype/lib/mongo.py
@@ -24,7 +24,7 @@ def _decompose_url(url):
validation pass.
"""
# Use first url from passed url
- # - this is beacuse it is possible to pass multiple urls for multiple
+ # - this is because it is possible to pass multiple urls for multiple
# replica sets which would crash on urlparse otherwise
# - please don't use comma in username of password
url = url.split(",")[0]
diff --git a/openpype/lib/openpype_version.py b/openpype/lib/openpype_version.py
index e3a4e1fa3e..201bf646e9 100644
--- a/openpype/lib/openpype_version.py
+++ b/openpype/lib/openpype_version.py
@@ -9,9 +9,69 @@ OpenPype version located in build but versions available in remote versions
repository or locally available.
"""
+import os
import sys
+import openpype.version
+from .python_module_tools import import_filepath
+
+
+# ----------------------------------------
+# Functions independent on OpenPypeVersion
+# ----------------------------------------
+def get_openpype_version():
+ """Version of pype that is currently used."""
+ return openpype.version.__version__
+
+
+def get_build_version():
+ """OpenPype version of build."""
+ # Return OpenPype version if is running from code
+ if not is_running_from_build():
+ return get_openpype_version()
+
+ # Import `version.py` from build directory
+ version_filepath = os.path.join(
+ os.environ["OPENPYPE_ROOT"],
+ "openpype",
+ "version.py"
+ )
+ if not os.path.exists(version_filepath):
+ return None
+
+ module = import_filepath(version_filepath, "openpype_build_version")
+ return getattr(module, "__version__", None)
+
+
+def is_running_from_build():
+ """Determine if current process is running from build or code.
+
+ Returns:
+ bool: True if running from build.
+ """
+ executable_path = os.environ["OPENPYPE_EXECUTABLE"]
+ executable_filename = os.path.basename(executable_path)
+ if "python" in executable_filename.lower():
+ return False
+ return True
+
+
+def is_running_staging():
+ """Currently used OpenPype is staging version.
+
+ Returns:
+ bool: True if openpype version containt 'staging'.
+ """
+ if "staging" in get_openpype_version():
+ return True
+ return False
+
+
+# ----------------------------------------
+# Functions dependent on OpenPypeVersion
+# - Make sense to call only in OpenPype process
+# ----------------------------------------
def get_OpenPypeVersion():
"""Access to OpenPypeVersion class stored in sys modules."""
return sys.modules.get("OpenPypeVersion")
@@ -71,15 +131,67 @@ def get_remote_versions(*args, **kwargs):
return None
-def get_latest_version(*args, **kwargs):
+def get_latest_version(staging=None, local=None, remote=None):
"""Get latest version from repository path."""
+ if staging is None:
+ staging = is_running_staging()
if op_version_control_available():
- return get_OpenPypeVersion().get_latest_version(*args, **kwargs)
+ return get_OpenPypeVersion().get_latest_version(
+ staging=staging,
+ local=local,
+ remote=remote
+ )
return None
-def get_expected_studio_version(staging=False):
+def get_expected_studio_version(staging=None):
"""Expected production or staging version in studio."""
+ if staging is None:
+ staging = is_running_staging()
if op_version_control_available():
return get_OpenPypeVersion().get_expected_studio_version(staging)
return None
+
+
+def get_expected_version(staging=None):
+ expected_version = get_expected_studio_version(staging)
+ if expected_version is None:
+ # Look for latest if expected version is not set in settings
+ expected_version = get_latest_version(
+ staging=staging,
+ remote=True
+ )
+ return expected_version
+
+
+def is_current_version_studio_latest():
+ """Is currently running OpenPype version which is defined by studio.
+
+ It is not recommended to ask in each process as there may be situations
+ when older OpenPype should be used. For example on farm. But it does make
+ sense in processes that can run for a long time.
+
+ Returns:
+ None: Can't determine. e.g. when running from code or the build is
+ too old.
+ bool: True when is using studio
+ """
+ output = None
+ # Skip if is not running from build or build does not support version
+ # control or path to folder with zip files is not accessible
+ if (
+ not is_running_from_build()
+ or not op_version_control_available()
+ or not openpype_path_is_accessible()
+ ):
+ return output
+
+ # Get OpenPypeVersion class
+ OpenPypeVersion = get_OpenPypeVersion()
+ # Convert current version to OpenPypeVersion object
+ current_version = OpenPypeVersion(version=get_openpype_version())
+
+ # Get expected version (from settings)
+ expected_version = get_expected_version()
+ # Check if current version is expected version
+ return current_version == expected_version
diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py
index 12e9e2db9c..c0b78c5724 100644
--- a/openpype/lib/path_tools.py
+++ b/openpype/lib/path_tools.py
@@ -116,10 +116,10 @@ def get_last_version_from_path(path_dir, filter):
filtred_files = list()
# form regex for filtering
- patern = r".*".join(filter)
+ pattern = r".*".join(filter)
for file in os.listdir(path_dir):
- if not re.findall(patern, file):
+ if not re.findall(pattern, file):
continue
filtred_files.append(file)
diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py
index 7c66f9760d..183aad939a 100644
--- a/openpype/lib/plugin_tools.py
+++ b/openpype/lib/plugin_tools.py
@@ -164,7 +164,7 @@ def prepare_template_data(fill_pairs):
"""
Prepares formatted data for filling template.
- It produces mutliple variants of keys (key, Key, KEY) to control
+ It produces multiple variants of keys (key, Key, KEY) to control
format of filled template.
Args:
@@ -288,7 +288,7 @@ def set_plugin_attributes_from_settings(
if project_name is None:
project_name = os.environ.get("AVALON_PROJECT")
- # map plugin superclass to preset json. Currenly suppoted is load and
+ # map plugin superclass to preset json. Currently supported is load and
# create (avalon.api.Loader and avalon.api.Creator)
plugin_type = None
if superclass.__name__.split(".")[-1] in ("Loader", "SubsetLoader"):
diff --git a/openpype/lib/pype_info.py b/openpype/lib/pype_info.py
index 33715e369d..848a505187 100644
--- a/openpype/lib/pype_info.py
+++ b/openpype/lib/pype_info.py
@@ -5,73 +5,18 @@ import platform
import getpass
import socket
-import openpype.version
from openpype.settings.lib import get_local_settings
-from .execute import get_pype_execute_args
+from .execute import get_openpype_execute_args
from .local_settings import get_local_site_id
-from .python_module_tools import import_filepath
-
-
-def get_openpype_version():
- """Version of pype that is currently used."""
- return openpype.version.__version__
-
-
-def get_pype_version():
- """Backwards compatibility. Remove when 100% not used."""
- print((
- "Using deprecated function 'openpype.lib.pype_info.get_pype_version'"
- " replace with 'openpype.lib.pype_info.get_openpype_version'."
- ))
- return get_openpype_version()
-
-
-def get_build_version():
- """OpenPype version of build."""
- # Return OpenPype version if is running from code
- if not is_running_from_build():
- return get_openpype_version()
-
- # Import `version.py` from build directory
- version_filepath = os.path.join(
- os.environ["OPENPYPE_ROOT"],
- "openpype",
- "version.py"
- )
- if not os.path.exists(version_filepath):
- return None
-
- module = import_filepath(version_filepath, "openpype_build_version")
- return getattr(module, "__version__", None)
-
-
-def is_running_from_build():
- """Determine if current process is running from build or code.
-
- Returns:
- bool: True if running from build.
- """
- executable_path = os.environ["OPENPYPE_EXECUTABLE"]
- executable_filename = os.path.basename(executable_path)
- if "python" in executable_filename.lower():
- return False
- return True
-
-
-def is_running_staging():
- """Currently used OpenPype is staging version.
-
- Returns:
- bool: True if openpype version containt 'staging'.
- """
- if "staging" in get_openpype_version():
- return True
- return False
+from .openpype_version import (
+ is_running_from_build,
+ get_openpype_version
+)
def get_pype_info():
"""Information about currently used Pype process."""
- executable_args = get_pype_execute_args()
+ executable_args = get_openpype_execute_args()
if is_running_from_build():
version_type = "build"
else:
diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py
index 8074b2d112..9632e63ea0 100644
--- a/openpype/lib/remote_publish.py
+++ b/openpype/lib/remote_publish.py
@@ -11,6 +11,13 @@ from openpype import uninstall
from openpype.lib.mongo import OpenPypeMongoConnection
from openpype.lib.plugin_tools import parse_json
+ERROR_STATUS = "error"
+IN_PROGRESS_STATUS = "in_progress"
+REPROCESS_STATUS = "reprocess"
+SENT_REPROCESSING_STATUS = "sent_for_reprocessing"
+FINISHED_REPROCESS_STATUS = "republishing_finished"
+FINISHED_OK_STATUS = "finished_ok"
+
def headless_publish(log, close_plugin_name=None, is_test=False):
"""Runs publish in a opened host with a context and closes Python process.
@@ -26,7 +33,7 @@ def headless_publish(log, close_plugin_name=None, is_test=False):
"batch will be unfinished!")
return
- publish_and_log(dbcon, _id, log, close_plugin_name)
+ publish_and_log(dbcon, _id, log, close_plugin_name=close_plugin_name)
else:
publish(log, close_plugin_name)
@@ -52,8 +59,8 @@ def start_webpublish_log(dbcon, batch_id, user):
"batch_id": batch_id,
"start_date": datetime.now(),
"user": user,
- "status": "in_progress",
- "progress": 0.0
+ "status": IN_PROGRESS_STATUS,
+ "progress": 0 # integer 0-100, percentage
}).inserted_id
@@ -84,18 +91,20 @@ def publish(log, close_plugin_name=None):
sys.exit(1)
-def publish_and_log(dbcon, _id, log, close_plugin_name=None):
+def publish_and_log(dbcon, _id, log, close_plugin_name=None, batch_id=None):
"""Loops through all plugins, logs ok and fails into OP DB.
Args:
dbcon (OpenPypeMongoConnection)
- _id (str)
+ _id (str) - id of current job in DB
log (OpenPypeLogger)
+ batch_id (str) - id sent from frontend
close_plugin_name (str): name of plugin with responsibility to
close host app
"""
# Error exit as soon as any error occurs.
- error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}"
+ error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}\n"
+ error_format += "-" * 80 + "\n"
close_plugin = _get_close_plugin(close_plugin_name, log)
@@ -103,21 +112,24 @@ def publish_and_log(dbcon, _id, log, close_plugin_name=None):
_id = ObjectId(_id)
log_lines = []
+ processed = 0
+ log_every = 5
for result in pyblish.util.publish_iter():
for record in result["records"]:
log_lines.append("{}: {}".format(
result["plugin"].label, record.msg))
+ processed += 1
if result["error"]:
log.error(error_format.format(**result))
uninstall()
- log_lines.append(error_format.format(**result))
+ log_lines = [error_format.format(**result)] + log_lines
dbcon.update_one(
{"_id": _id},
{"$set":
{
"finish_date": datetime.now(),
- "status": "error",
+ "status": ERROR_STATUS,
"log": os.linesep.join(log_lines)
}}
@@ -126,26 +138,42 @@ def publish_and_log(dbcon, _id, log, close_plugin_name=None):
context = pyblish.api.Context()
close_plugin().process(context)
sys.exit(1)
- else:
+ elif processed % log_every == 0:
+ # pyblish returns progress in 0.0 - 2.0
+ progress = min(round(result["progress"] / 2 * 100), 99)
dbcon.update_one(
{"_id": _id},
{"$set":
{
- "progress": max(result["progress"], 0.95),
+ "progress": progress,
"log": os.linesep.join(log_lines)
}}
)
# final update
+ if batch_id:
+ dbcon.update_many(
+ {"batch_id": batch_id, "status": SENT_REPROCESSING_STATUS},
+ {
+ "$set":
+ {
+ "finish_date": datetime.now(),
+ "status": FINISHED_REPROCESS_STATUS,
+ }
+ }
+ )
+
dbcon.update_one(
{"_id": _id},
- {"$set":
- {
- "finish_date": datetime.now(),
- "status": "finished_ok",
- "progress": 1,
- "log": os.linesep.join(log_lines)
- }}
+ {
+ "$set":
+ {
+ "finish_date": datetime.now(),
+ "status": FINISHED_OK_STATUS,
+ "progress": 100,
+ "log": os.linesep.join(log_lines)
+ }
+ }
)
@@ -162,7 +190,7 @@ def fail_batch(_id, batches_in_progress, dbcon):
{"$set":
{
"finish_date": datetime.now(),
- "status": "error",
+ "status": ERROR_STATUS,
"log": msg
}}
diff --git a/openpype/lib/terminal.py b/openpype/lib/terminal.py
index ddc917ac4e..bc0744931a 100644
--- a/openpype/lib/terminal.py
+++ b/openpype/lib/terminal.py
@@ -130,7 +130,7 @@ class Terminal:
def _multiple_replace(text, adict):
"""Replace multiple tokens defined in dict.
- Find and replace all occurances of strings defined in dict is
+ Find and replace all occurrences of strings defined in dict is
supplied string.
Args:
diff --git a/openpype/modules/base.py b/openpype/modules/base.py
index b5c491a1c0..d566692439 100644
--- a/openpype/modules/base.py
+++ b/openpype/modules/base.py
@@ -42,6 +42,7 @@ DEFAULT_OPENPYPE_MODULES = (
"settings_action",
"standalonepublish_action",
"job_queue",
+ "timers_manager",
)
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py
index d3cc0ad971..676dd80e93 100644
--- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py
+++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py
@@ -163,24 +163,27 @@ class DeleteAssetSubset(BaseAction):
if not selected_av_entities:
return {
- "success": False,
- "message": "Didn't found entities in avalon"
+ "success": True,
+ "message": (
+ "Didn't found entities in avalon."
+ " You can use Ftrack's Delete button for the selection."
+ )
}
# Remove cached action older than 2 minutes
old_action_ids = []
- for id, data in self.action_data_by_id.items():
+ for action_id, data in self.action_data_by_id.items():
created_at = data.get("created_at")
if not created_at:
- old_action_ids.append(id)
+ old_action_ids.append(action_id)
continue
cur_time = datetime.now()
existing_in_sec = (created_at - cur_time).total_seconds()
if existing_in_sec > 60 * 2:
- old_action_ids.append(id)
+ old_action_ids.append(action_id)
- for id in old_action_ids:
- self.action_data_by_id.pop(id, None)
+ for action_id in old_action_ids:
+ self.action_data_by_id.pop(action_id, None)
# Store data for action id
action_id = str(uuid.uuid1())
@@ -439,7 +442,11 @@ class DeleteAssetSubset(BaseAction):
subsets_to_delete = to_delete.get("subsets") or []
# Convert asset ids to ObjectId obj
- assets_to_delete = [ObjectId(id) for id in assets_to_delete if id]
+ assets_to_delete = [
+ ObjectId(asset_id)
+ for asset_id in assets_to_delete
+ if asset_id
+ ]
subset_ids_by_parent = spec_data["subset_ids_by_parent"]
subset_ids_by_name = spec_data["subset_ids_by_name"]
@@ -468,9 +475,8 @@ class DeleteAssetSubset(BaseAction):
if not ftrack_id:
ftrack_id = asset["data"].get("ftrackId")
- if not ftrack_id:
- continue
- ftrack_ids_to_delete.append(ftrack_id)
+ if ftrack_id:
+ ftrack_ids_to_delete.append(ftrack_id)
children_queue = collections.deque()
for mongo_id in assets_to_delete:
@@ -569,12 +575,12 @@ class DeleteAssetSubset(BaseAction):
exc_info=True
)
- if not_deleted_entities_id:
- joined_not_deleted = ", ".join([
+ if not_deleted_entities_id and asset_names_to_delete:
+ joined_not_deleted = ",".join([
"\"{}\"".format(ftrack_id)
for ftrack_id in not_deleted_entities_id
])
- joined_asset_names = ", ".join([
+ joined_asset_names = ",".join([
"\"{}\"".format(name)
for name in asset_names_to_delete
])
@@ -613,6 +619,25 @@ class DeleteAssetSubset(BaseAction):
joined_ids_to_delete
)
).all()
+ # Find all children entities and add them to list
+ # - Delete tasks first then their parents and continue
+ parent_ids_to_delete = [
+ entity["id"]
+ for entity in to_delete_entities
+ ]
+ while parent_ids_to_delete:
+ joined_parent_ids_to_delete = ",".join([
+ "\"{}\"".format(ftrack_id)
+ for ftrack_id in parent_ids_to_delete
+ ])
+ _to_delete = session.query((
+ "select id, link from TypedContext where parent_id in ({})"
+ ).format(joined_parent_ids_to_delete)).all()
+ parent_ids_to_delete = []
+ for entity in _to_delete:
+ parent_ids_to_delete.append(entity["id"])
+ to_delete_entities.append(entity)
+
entities_by_link_len = collections.defaultdict(list)
for entity in to_delete_entities:
entities_by_link_len[len(entity["link"])].append(entity)
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_djvview.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_djvview.py
index c603a2d200..334519b4bb 100644
--- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_djvview.py
+++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_djvview.py
@@ -1,6 +1,8 @@
import os
+import time
import subprocess
from operator import itemgetter
+from openpype.lib import ApplicationManager
from openpype_modules.ftrack.lib import BaseAction, statics_icon
@@ -23,15 +25,25 @@ class DJVViewAction(BaseAction):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.djv_path = self.find_djv_path()
+ self.application_manager = ApplicationManager()
+ self._last_check = time.time()
+ self._check_interval = 10
- def preregister(self):
- if self.djv_path is None:
- return (
- 'DJV View is not installed'
- ' or paths in presets are not set correctly'
- )
- return True
+ def _get_djv_apps(self):
+ app_group = self.application_manager.app_groups["djvview"]
+
+ output = []
+ for app in app_group:
+ executable = app.find_executable()
+ if executable is not None:
+ output.append(app)
+ return output
+
+ def get_djv_apps(self):
+ cur_time = time.time()
+ if (cur_time - self._last_check) > self._check_interval:
+ self.application_manager.refresh()
+ return self._get_djv_apps()
def discover(self, session, entities, event):
"""Return available actions based on *event*. """
@@ -40,15 +52,13 @@ class DJVViewAction(BaseAction):
return False
entityType = selection[0].get("entityType", None)
- if entityType in ["assetversion", "task"]:
+ if entityType not in ["assetversion", "task"]:
+ return False
+
+ if self.get_djv_apps():
return True
return False
- def find_djv_path(self):
- for path in (os.environ.get("DJV_PATH") or "").split(os.pathsep):
- if os.path.exists(path):
- return path
-
def interface(self, session, entities, event):
if event['data'].get('values', {}):
return
@@ -88,7 +98,37 @@ class DJVViewAction(BaseAction):
'message': 'There are no Asset Versions to open.'
}
- items = []
+ # TODO sort them (somehow?)
+ enum_items = []
+ first_value = None
+ for app in self.get_djv_apps():
+ if first_value is None:
+ first_value = app.full_name
+ enum_items.append({
+ "value": app.full_name,
+ "label": app.full_label
+ })
+
+ if not enum_items:
+ return {
+ "success": False,
+ "message": "Couldn't find DJV executable."
+ }
+
+ items = [
+ {
+ "type": "enumerator",
+ "label": "DJV version:",
+ "name": "djv_app_name",
+ "data": enum_items,
+ "value": first_value
+ },
+ {
+ "type": "label",
+ "value": "---"
+ }
+ ]
+ version_items = []
base_label = "v{0} - {1} - {2}"
default_component = None
last_available = None
@@ -115,11 +155,11 @@ class DJVViewAction(BaseAction):
last_available = file_path
if component['name'] == default_component:
select_value = file_path
- items.append(
+ version_items.append(
{'label': label, 'value': file_path}
)
- if len(items) == 0:
+ if len(version_items) == 0:
return {
'success': False,
'message': (
@@ -132,7 +172,7 @@ class DJVViewAction(BaseAction):
'type': 'enumerator',
'name': 'path',
'data': sorted(
- items,
+ version_items,
key=itemgetter('label'),
reverse=True
)
@@ -142,21 +182,37 @@ class DJVViewAction(BaseAction):
else:
item['value'] = last_available
- return {'items': [item]}
+ items.append(item)
+
+ return {'items': items}
def launch(self, session, entities, event):
"""Callback method for DJVView action."""
# Launching application
- if "values" not in event["data"]:
+ event_data = event["data"]
+ if "values" not in event_data:
return
- filpath = event['data']['values']['path']
+
+ djv_app_name = event_data["djv_app_name"]
+ app = self.applicaion_manager.applications.get(djv_app_name)
+ executable = None
+ if app is not None:
+ executable = app.find_executable()
+
+ if not executable:
+ return {
+ "success": False,
+ "message": "Couldn't find DJV executable."
+ }
+
+ filpath = os.path.normpath(event_data["values"]["path"])
cmd = [
# DJV path
- os.path.normpath(self.djv_path),
+ executable,
# PATH TO COMPONENT
- os.path.normpath(filpath)
+ filpath
]
try:
@@ -164,8 +220,8 @@ class DJVViewAction(BaseAction):
subprocess.Popen(cmd)
except FileNotFoundError:
return {
- 'success': False,
- 'message': 'File "{}" was not found.'.format(
+ "success": False,
+ "message": "File \"{}\" was not found.".format(
os.path.basename(filpath)
)
}
diff --git a/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py b/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py
index 1a76905b38..90ce757242 100644
--- a/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py
+++ b/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py
@@ -14,7 +14,7 @@ import uuid
import ftrack_api
import pymongo
from openpype.lib import (
- get_pype_execute_args,
+ get_openpype_execute_args,
OpenPypeMongoConnection,
get_openpype_version,
get_build_version,
@@ -136,7 +136,7 @@ def legacy_server(ftrack_url):
if subproc is None:
if subproc_failed_count < max_fail_count:
- args = get_pype_execute_args("run", subproc_path)
+ args = get_openpype_execute_args("run", subproc_path)
subproc = subprocess.Popen(
args,
stdout=subprocess.PIPE
@@ -248,7 +248,7 @@ def main_loop(ftrack_url):
["Username", getpass.getuser()],
["Host Name", host_name],
["Host IP", socket.gethostbyname(host_name)],
- ["OpenPype executable", get_pype_execute_args()[-1]],
+ ["OpenPype executable", get_openpype_execute_args()[-1]],
["OpenPype version", get_openpype_version() or "N/A"],
["OpenPype build version", get_build_version() or "N/A"]
]
diff --git a/openpype/modules/default_modules/ftrack/ftrack_server/socket_thread.py b/openpype/modules/default_modules/ftrack/ftrack_server/socket_thread.py
index eb8ec4d06c..f49ca5557e 100644
--- a/openpype/modules/default_modules/ftrack/ftrack_server/socket_thread.py
+++ b/openpype/modules/default_modules/ftrack/ftrack_server/socket_thread.py
@@ -6,7 +6,7 @@ import threading
import traceback
import subprocess
from openpype.api import Logger
-from openpype.lib import get_pype_execute_args
+from openpype.lib import get_openpype_execute_args
class SocketThread(threading.Thread):
@@ -59,7 +59,7 @@ class SocketThread(threading.Thread):
env = os.environ.copy()
env["OPENPYPE_PROCESS_MONGO_ID"] = str(Logger.mongo_process_id)
# OpenPype executable (with path to start script if not build)
- args = get_pype_execute_args(
+ args = get_openpype_execute_args(
# Add `run` command
"run",
self.filepath,
diff --git a/openpype/modules/default_modules/ftrack/plugins/publish/collect_username.py b/openpype/modules/default_modules/ftrack/plugins/publish/collect_username.py
index 7ea1c1f323..303490189b 100644
--- a/openpype/modules/default_modules/ftrack/plugins/publish/collect_username.py
+++ b/openpype/modules/default_modules/ftrack/plugins/publish/collect_username.py
@@ -37,16 +37,27 @@ class CollectUsername(pyblish.api.ContextPlugin):
os.environ["FTRACK_API_USER"] = os.environ["FTRACK_BOT_API_USER"]
os.environ["FTRACK_API_KEY"] = os.environ["FTRACK_BOT_API_KEY"]
- for instance in context:
- email = instance.data["user_email"]
- self.log.info("email:: {}".format(email))
- session = ftrack_api.Session(auto_connect_event_hub=False)
- user = session.query("User where email like '{}'".format(
- email))
+ # for publishes with studio processing
+ user_email = os.environ.get("USER_EMAIL")
+ self.log.debug("Email from env:: {}".format(user_email))
+ if not user_email:
+ # for basic webpublishes
+ for instance in context:
+ user_email = instance.data.get("user_email")
+ self.log.debug("Email from instance:: {}".format(user_email))
+ break
- if not user:
- raise ValueError(
- "Couldnt find user with {} email".format(email))
+ if not user_email:
+ self.log.info("No email found")
+ return
- os.environ["FTRACK_API_USER"] = user[0].get("username")
- break
+ session = ftrack_api.Session(auto_connect_event_hub=False)
+ user = session.query("User where email like '{}'".format(user_email))
+
+ if not user:
+ raise ValueError(
+ "Couldn't find user with {} email".format(user_email))
+
+ username = user[0].get("username")
+ self.log.debug("Resolved ftrack username:: {}".format(username))
+ os.environ["FTRACK_API_USER"] = username
diff --git a/openpype/modules/default_modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/default_modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py
index fbd64d9f70..61892240d7 100644
--- a/openpype/modules/default_modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py
+++ b/openpype/modules/default_modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py
@@ -63,7 +63,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
order = pyblish.api.IntegratorOrder - 0.04
label = 'Integrate Hierarchy To Ftrack'
families = ["shot"]
- hosts = ["hiero", "resolve", "standalonepublisher"]
+ hosts = ["hiero", "resolve", "standalonepublisher", "flame"]
optional = False
def process(self, context):
diff --git a/openpype/modules/default_modules/ftrack/scripts/sub_event_status.py b/openpype/modules/default_modules/ftrack/scripts/sub_event_status.py
index 004f61338c..3163642e3f 100644
--- a/openpype/modules/default_modules/ftrack/scripts/sub_event_status.py
+++ b/openpype/modules/default_modules/ftrack/scripts/sub_event_status.py
@@ -16,8 +16,14 @@ from openpype_modules.ftrack.ftrack_server.lib import (
TOPIC_STATUS_SERVER_RESULT
)
from openpype.api import Logger
+from openpype.lib import (
+ is_current_version_studio_latest,
+ is_running_from_build,
+ get_expected_version,
+ get_openpype_version
+)
-log = Logger().get_logger("Event storer")
+log = Logger.get_logger("Event storer")
action_identifier = (
"event.server.status" + os.environ["FTRACK_EVENT_SUB_ID"]
)
@@ -203,8 +209,57 @@ class StatusFactory:
})
return items
+ def openpype_version_items(self):
+ items = []
+ is_latest = is_current_version_studio_latest()
+ items.append({
+ "type": "label",
+ "value": "# OpenPype version"
+ })
+ if not is_running_from_build():
+ items.append({
+ "type": "label",
+ "value": (
+ "OpenPype event server is running from code {} ."
+ ).format(str(get_openpype_version()))
+ })
+
+ elif is_latest is None:
+ items.append({
+ "type": "label",
+ "value": (
+ "Can't determine if OpenPype version is outdated"
+ " {} . OpenPype build version should be updated."
+ ).format(str(get_openpype_version()))
+ })
+ elif is_latest:
+ items.append({
+ "type": "label",
+ "value": "OpenPype version is up to date {} .".format(
+ str(get_openpype_version())
+ )
+ })
+ else:
+ items.append({
+ "type": "label",
+ "value": (
+ "Using outdated OpenPype version {} ."
+ " Expected version is {} ."
+ " - Please restart event server for automatic"
+ " updates or update manually."
+ ).format(
+ str(get_openpype_version()),
+ str(get_expected_version())
+ )
+ })
+
+ items.append({"type": "label", "value": "---"})
+
+ return items
+
def items(self):
items = []
+ items.extend(self.openpype_version_items())
items.append(self.note_item)
items.extend(self.bool_items())
diff --git a/openpype/modules/slack/manifest.yml b/openpype/modules/slack/manifest.yml
index 37d4669903..7a65cc5915 100644
--- a/openpype/modules/slack/manifest.yml
+++ b/openpype/modules/slack/manifest.yml
@@ -15,8 +15,10 @@ oauth_config:
scopes:
bot:
- chat:write
+ - chat:write.customize
- chat:write.public
- files:write
+ - channels:read
settings:
org_deploy_enabled: false
socket_mode_enabled: false
diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py
index 7b81d3c364..5d014382a3 100644
--- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py
+++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py
@@ -2,8 +2,10 @@ import os
import six
import pyblish.api
import copy
+from datetime import datetime
from openpype.lib.plugin_tools import prepare_template_data
+from openpype.lib import OpenPypeMongoConnection
class IntegrateSlackAPI(pyblish.api.InstancePlugin):
@@ -14,6 +16,8 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin):
Project settings > Slack > Publish plugins > Notification to Slack.
If instance contains 'thumbnail' it uploads it. Bot must be present
in the target channel.
+ If instance contains 'review' it could upload (if configured) or place
+ link with {review_filepath} placeholder.
Message template can contain {} placeholders from anatomyData.
"""
order = pyblish.api.IntegratorOrder + 0.499
@@ -23,44 +27,81 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin):
optional = True
def process(self, instance):
- published_path = self._get_thumbnail_path(instance)
+ thumbnail_path = self._get_thumbnail_path(instance)
+ review_path = self._get_review_path(instance)
+ publish_files = set()
for message_profile in instance.data["slack_channel_message_profiles"]:
message = self._get_filled_message(message_profile["message"],
- instance)
+ instance,
+ review_path)
+ self.log.info("message:: {}".format(message))
if not message:
return
+ if message_profile["upload_thumbnail"] and thumbnail_path:
+ publish_files.add(thumbnail_path)
+
+ if message_profile["upload_review"] and review_path:
+ publish_files.add(review_path)
+
+ project = instance.context.data["anatomyData"]["project"]["code"]
for channel in message_profile["channels"]:
if six.PY2:
- self._python2_call(instance.data["slack_token"],
- channel,
- message,
- published_path,
- message_profile["upload_thumbnail"])
+ msg_id, file_ids = \
+ self._python2_call(instance.data["slack_token"],
+ channel,
+ message,
+ publish_files)
else:
- self._python3_call(instance.data["slack_token"],
- channel,
- message,
- published_path,
- message_profile["upload_thumbnail"])
+ msg_id, file_ids = \
+ self._python3_call(instance.data["slack_token"],
+ channel,
+ message,
+ publish_files)
- def _get_filled_message(self, message_templ, instance):
- """Use message_templ and data from instance to get message content."""
+ msg = {
+ "type": "slack",
+ "msg_id": msg_id,
+ "file_ids": file_ids,
+ "project": project,
+ "created_dt": datetime.now()
+ }
+ mongo_client = OpenPypeMongoConnection.get_mongo_client()
+ database_name = os.environ["OPENPYPE_DATABASE_NAME"]
+ dbcon = mongo_client[database_name]["notification_messages"]
+ dbcon.insert_one(msg)
+
+ def _get_filled_message(self, message_templ, instance, review_path=None):
+ """Use message_templ and data from instance to get message content.
+
+ Reviews might be large, so allow only adding link to message instead of
+ uploading only.
+ """
fill_data = copy.deepcopy(instance.context.data["anatomyData"])
- fill_pairs = (
+ fill_pairs = [
("asset", instance.data.get("asset", fill_data.get("asset"))),
("subset", instance.data.get("subset", fill_data.get("subset"))),
- ("task", instance.data.get("task", fill_data.get("task"))),
("username", instance.data.get("username",
fill_data.get("username"))),
("app", instance.data.get("app", fill_data.get("app"))),
("family", instance.data.get("family", fill_data.get("family"))),
("version", str(instance.data.get("version",
fill_data.get("version"))))
- )
+ ]
+ if review_path:
+ fill_pairs.append(("review_filepath", review_path))
+ task_data = instance.data.get("task")
+ if not task_data:
+ task_data = fill_data.get("task")
+ for key, value in task_data.items():
+ fill_key = "task[{}]".format(key)
+ fill_pairs.append((fill_key, value))
+ fill_pairs.append(("task", task_data["name"]))
+
+ self.log.debug("fill_pairs ::{}".format(fill_pairs))
multiple_case_variants = prepare_template_data(fill_pairs)
fill_data.update(multiple_case_variants)
@@ -79,67 +120,87 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin):
published_path = None
for repre in instance.data['representations']:
if repre.get('thumbnail') or "thumbnail" in repre.get('tags', []):
- repre_files = repre["files"]
- if isinstance(repre_files, (tuple, list, set)):
- filename = repre_files[0]
- else:
- filename = repre_files
-
- published_path = os.path.join(
- repre['stagingDir'], filename
- )
+ if os.path.exists(repre["published_path"]):
+ published_path = repre["published_path"]
break
return published_path
- def _python2_call(self, token, channel, message,
- published_path, upload_thumbnail):
+ def _get_review_path(self, instance):
+ """Returns abs url for review if present in instance repres"""
+ published_path = None
+ for repre in instance.data['representations']:
+ tags = repre.get('tags', [])
+ if (repre.get("review")
+ or "review" in tags
+ or "burnin" in tags):
+ if os.path.exists(repre["published_path"]):
+ published_path = repre["published_path"]
+ if "burnin" in tags: # burnin has precedence if exists
+ break
+ return published_path
+
+ def _python2_call(self, token, channel, message, publish_files):
from slackclient import SlackClient
try:
client = SlackClient(token)
- if upload_thumbnail and \
- published_path and os.path.exists(published_path):
- with open(published_path, 'rb') as pf:
+ attachment_str = "\n\n Attachment links: \n"
+ file_ids = []
+ for p_file in publish_files:
+ with open(p_file, 'rb') as pf:
response = client.api_call(
"files.upload",
- channels=channel,
- initial_comment=message,
file=pf,
- title=os.path.basename(published_path)
+ channel=channel,
+ title=os.path.basename(p_file)
)
- else:
- response = client.api_call(
- "chat.postMessage",
- channel=channel,
- text=message
- )
+ attachment_str += "\n<{}|{}>".format(
+ response["file"]["permalink"],
+ os.path.basename(p_file))
+ file_ids.append(response["file"]["id"])
+ if publish_files:
+ message += attachment_str
+
+ response = client.api_call(
+ "chat.postMessage",
+ channel=channel,
+ text=message
+ )
if response.get("error"):
error_str = self._enrich_error(str(response.get("error")),
channel)
self.log.warning("Error happened: {}".format(error_str))
+ else:
+ return response["ts"], file_ids
except Exception as e:
# You will get a SlackApiError if "ok" is False
error_str = self._enrich_error(str(e), channel)
self.log.warning("Error happened: {}".format(error_str))
- def _python3_call(self, token, channel, message,
- published_path, upload_thumbnail):
+ def _python3_call(self, token, channel, message, publish_files):
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
try:
client = WebClient(token=token)
- if upload_thumbnail and \
- published_path and os.path.exists(published_path):
- _ = client.files_upload(
- channels=channel,
- initial_comment=message,
- file=published_path,
- )
- else:
- _ = client.chat_postMessage(
- channel=channel,
- text=message
- )
+ attachment_str = "\n\n Attachment links: \n"
+ file_ids = []
+ for published_file in publish_files:
+ response = client.files_upload(
+ file=published_file,
+ filename=os.path.basename(published_file))
+ attachment_str += "\n<{}|{}>".format(
+ response["file"]["permalink"],
+ os.path.basename(published_file))
+ file_ids.append(response["file"]["id"])
+
+ if publish_files:
+ message += attachment_str
+
+ response = client.chat_postMessage(
+ channel=channel,
+ text=message
+ )
+ return response.data["ts"], file_ids
except SlackApiError as e:
# You will get a SlackApiError if "ok" is False
error_str = self._enrich_error(str(e.response["error"]), channel)
diff --git a/openpype/modules/slack/resources/openpype_icon.png b/openpype/modules/slack/resources/openpype_icon.png
new file mode 100644
index 0000000000..bb38dcf577
Binary files /dev/null and b/openpype/modules/slack/resources/openpype_icon.png differ
diff --git a/openpype/modules/standalonepublish_action.py b/openpype/modules/standalonepublish_action.py
index 9321a415a9..ba53ce9b9e 100644
--- a/openpype/modules/standalonepublish_action.py
+++ b/openpype/modules/standalonepublish_action.py
@@ -1,7 +1,7 @@
import os
import platform
import subprocess
-from openpype.lib import get_pype_execute_args
+from openpype.lib import get_openpype_execute_args
from openpype.modules import OpenPypeModule
from openpype_interfaces import ITrayAction
@@ -35,7 +35,7 @@ class StandAlonePublishAction(OpenPypeModule, ITrayAction):
self.publish_paths.extend(publish_paths)
def run_standalone_publisher(self):
- args = get_pype_execute_args("standalonepublisher")
+ args = get_openpype_execute_args("standalonepublisher")
kwargs = {}
if platform.system().lower() == "darwin":
new_args = ["open", "-na", args.pop(0), "--args"]
diff --git a/openpype/modules/default_modules/timers_manager/__init__.py b/openpype/modules/timers_manager/__init__.py
similarity index 100%
rename from openpype/modules/default_modules/timers_manager/__init__.py
rename to openpype/modules/timers_manager/__init__.py
diff --git a/openpype/modules/default_modules/timers_manager/exceptions.py b/openpype/modules/timers_manager/exceptions.py
similarity index 100%
rename from openpype/modules/default_modules/timers_manager/exceptions.py
rename to openpype/modules/timers_manager/exceptions.py
diff --git a/openpype/modules/default_modules/timers_manager/idle_threads.py b/openpype/modules/timers_manager/idle_threads.py
similarity index 100%
rename from openpype/modules/default_modules/timers_manager/idle_threads.py
rename to openpype/modules/timers_manager/idle_threads.py
diff --git a/openpype/modules/default_modules/timers_manager/launch_hooks/post_start_timer.py b/openpype/modules/timers_manager/launch_hooks/post_start_timer.py
similarity index 100%
rename from openpype/modules/default_modules/timers_manager/launch_hooks/post_start_timer.py
rename to openpype/modules/timers_manager/launch_hooks/post_start_timer.py
diff --git a/openpype/modules/default_modules/timers_manager/rest_api.py b/openpype/modules/timers_manager/rest_api.py
similarity index 100%
rename from openpype/modules/default_modules/timers_manager/rest_api.py
rename to openpype/modules/timers_manager/rest_api.py
diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/timers_manager/timers_manager.py
similarity index 100%
rename from openpype/modules/default_modules/timers_manager/timers_manager.py
rename to openpype/modules/timers_manager/timers_manager.py
diff --git a/openpype/modules/default_modules/timers_manager/widget_user_idle.py b/openpype/modules/timers_manager/widget_user_idle.py
similarity index 100%
rename from openpype/modules/default_modules/timers_manager/widget_user_idle.py
rename to openpype/modules/timers_manager/widget_user_idle.py
diff --git a/openpype/plugins/publish/collect_hierarchy.py b/openpype/plugins/publish/collect_hierarchy.py
index f7d1c6b4be..efb40407d9 100644
--- a/openpype/plugins/publish/collect_hierarchy.py
+++ b/openpype/plugins/publish/collect_hierarchy.py
@@ -13,9 +13,9 @@ class CollectHierarchy(pyblish.api.ContextPlugin):
"""
label = "Collect Hierarchy"
- order = pyblish.api.CollectorOrder - 0.47
+ order = pyblish.api.CollectorOrder - 0.076
families = ["shot"]
- hosts = ["resolve", "hiero"]
+ hosts = ["resolve", "hiero", "flame"]
def process(self, context):
temp_context = {}
diff --git a/openpype/plugins/publish/collect_otio_frame_ranges.py b/openpype/plugins/publish/collect_otio_frame_ranges.py
index a35ef47e79..ee7b7957ad 100644
--- a/openpype/plugins/publish/collect_otio_frame_ranges.py
+++ b/openpype/plugins/publish/collect_otio_frame_ranges.py
@@ -12,15 +12,15 @@ import openpype.lib
from pprint import pformat
-class CollectOcioFrameRanges(pyblish.api.InstancePlugin):
+class CollectOtioFrameRanges(pyblish.api.InstancePlugin):
"""Getting otio ranges from otio_clip
Adding timeline and source ranges to instance data"""
label = "Collect OTIO Frame Ranges"
- order = pyblish.api.CollectorOrder - 0.48
+ order = pyblish.api.CollectorOrder - 0.08
families = ["shot", "clip"]
- hosts = ["resolve", "hiero"]
+ hosts = ["resolve", "hiero", "flame"]
def process(self, instance):
# get basic variables
diff --git a/openpype/plugins/publish/collect_otio_review.py b/openpype/plugins/publish/collect_otio_review.py
index 10ceafdcca..35c77a24cb 100644
--- a/openpype/plugins/publish/collect_otio_review.py
+++ b/openpype/plugins/publish/collect_otio_review.py
@@ -16,13 +16,13 @@ import pyblish.api
from pprint import pformat
-class CollectOcioReview(pyblish.api.InstancePlugin):
+class CollectOtioReview(pyblish.api.InstancePlugin):
"""Get matching otio track from defined review layer"""
label = "Collect OTIO Review"
- order = pyblish.api.CollectorOrder - 0.47
+ order = pyblish.api.CollectorOrder - 0.078
families = ["clip"]
- hosts = ["resolve", "hiero"]
+ hosts = ["resolve", "hiero", "flame"]
def process(self, instance):
# get basic variables
diff --git a/openpype/plugins/publish/collect_otio_subset_resources.py b/openpype/plugins/publish/collect_otio_subset_resources.py
index 571d0d56a4..7c11462ef0 100644
--- a/openpype/plugins/publish/collect_otio_subset_resources.py
+++ b/openpype/plugins/publish/collect_otio_subset_resources.py
@@ -14,13 +14,13 @@ import openpype
from openpype.lib import editorial
-class CollectOcioSubsetResources(pyblish.api.InstancePlugin):
+class CollectOtioSubsetResources(pyblish.api.InstancePlugin):
"""Get Resources for a subset version"""
label = "Collect OTIO Subset Resources"
- order = pyblish.api.CollectorOrder - 0.47
+ order = pyblish.api.CollectorOrder - 0.077
families = ["clip"]
- hosts = ["resolve", "hiero"]
+ hosts = ["resolve", "hiero", "flame"]
def process(self, instance):
@@ -64,7 +64,7 @@ class CollectOcioSubsetResources(pyblish.api.InstancePlugin):
a_frame_start_h = media_in - handle_start
a_frame_end_h = media_out + handle_end
- # create trimmed ocio time range
+ # create trimmed otio time range
trimmed_media_range_h = editorial.range_from_frames(
a_frame_start_h, (a_frame_end_h - a_frame_start_h + 1),
media_fps
diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py
index df7dc47e17..7ff1b24689 100644
--- a/openpype/plugins/publish/extract_burnin.py
+++ b/openpype/plugins/publish/extract_burnin.py
@@ -13,7 +13,7 @@ import pyblish
import openpype
import openpype.api
from openpype.lib import (
- get_pype_execute_args,
+ run_openpype_process,
get_transcode_temp_directory,
convert_for_ffmpeg,
@@ -48,7 +48,8 @@ class ExtractBurnin(openpype.api.Extractor):
"tvpaint",
"webpublisher",
"aftereffects",
- "photoshop"
+ "photoshop",
+ "flame"
# "resolve"
]
optional = True
@@ -168,9 +169,8 @@ class ExtractBurnin(openpype.api.Extractor):
anatomy = instance.context.data["anatomy"]
scriptpath = self.burnin_script_path()
- # Executable args that will execute the script
- # [pype executable, *pype script, "run"]
- executable_args = get_pype_execute_args("run", scriptpath)
+ # Args that will execute the script
+ executable_args = ["run", scriptpath]
burnins_per_repres = self._get_burnins_per_representations(
instance, burnin_defs
)
@@ -313,7 +313,7 @@ class ExtractBurnin(openpype.api.Extractor):
if platform.system().lower() == "windows":
process_kwargs["creationflags"] = CREATE_NO_WINDOW
- openpype.api.run_subprocess(args, **process_kwargs)
+ run_openpype_process(*args, **process_kwargs)
# Remove the temporary json
os.remove(temporary_json_filepath)
diff --git a/openpype/plugins/publish/extract_otio_audio_tracks.py b/openpype/plugins/publish/extract_otio_audio_tracks.py
index be0bae5cdc..00c1748cdc 100644
--- a/openpype/plugins/publish/extract_otio_audio_tracks.py
+++ b/openpype/plugins/publish/extract_otio_audio_tracks.py
@@ -19,7 +19,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
order = pyblish.api.ExtractorOrder - 0.44
label = "Extract OTIO Audio Tracks"
- hosts = ["hiero", "resolve"]
+ hosts = ["hiero", "resolve", "flame"]
# FFmpeg tools paths
ffmpeg_path = get_ffmpeg_tool_path("ffmpeg")
diff --git a/openpype/plugins/publish/extract_otio_review.py b/openpype/plugins/publish/extract_otio_review.py
index ed2ba017d5..78570488b3 100644
--- a/openpype/plugins/publish/extract_otio_review.py
+++ b/openpype/plugins/publish/extract_otio_review.py
@@ -41,7 +41,7 @@ class ExtractOTIOReview(openpype.api.Extractor):
order = api.ExtractorOrder - 0.45
label = "Extract OTIO review"
families = ["review"]
- hosts = ["resolve", "hiero"]
+ hosts = ["resolve", "hiero", "flame"]
# plugin default attributes
temp_file_head = "tempFile."
@@ -85,6 +85,28 @@ class ExtractOTIOReview(openpype.api.Extractor):
for index, r_otio_cl in enumerate(otio_review_clips):
# QUESTION: what if transition on clip?
+ # check if resolution is the same
+ width = self.to_width
+ height = self.to_height
+ otio_media = r_otio_cl.media_reference
+ media_metadata = otio_media.metadata
+
+ # get from media reference metadata source
+ if media_metadata.get("openpype.source.width"):
+ width = int(media_metadata.get("openpype.source.width"))
+ if media_metadata.get("openpype.source.height"):
+ height = int(media_metadata.get("openpype.source.height"))
+
+ # compare and reset
+ if width != self.to_width:
+ self.to_width = width
+ if height != self.to_height:
+ self.to_height = height
+
+ self.log.debug("> self.to_width x self.to_height: {} x {}".format(
+ self.to_width, self.to_height
+ ))
+
# get frame range values
src_range = r_otio_cl.source_range
start = src_range.start_time.value
diff --git a/openpype/plugins/publish/extract_otio_trimming_video.py b/openpype/plugins/publish/extract_otio_trimming_video.py
index 3e2d39c99c..30b57e2c69 100644
--- a/openpype/plugins/publish/extract_otio_trimming_video.py
+++ b/openpype/plugins/publish/extract_otio_trimming_video.py
@@ -19,7 +19,7 @@ class ExtractOTIOTrimmingVideo(openpype.api.Extractor):
order = api.ExtractorOrder
label = "Extract OTIO trim longer video"
families = ["trim"]
- hosts = ["resolve", "hiero"]
+ hosts = ["resolve", "hiero", "flame"]
def process(self, instance):
self.staging_dir = self.staging_dir(instance)
diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py
index b6c2e49385..d223c31291 100644
--- a/openpype/plugins/publish/extract_review.py
+++ b/openpype/plugins/publish/extract_review.py
@@ -51,7 +51,8 @@ class ExtractReview(pyblish.api.InstancePlugin):
"tvpaint",
"resolve",
"webpublisher",
- "aftereffects"
+ "aftereffects",
+ "flame"
]
# Supported extensions
@@ -187,6 +188,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
outputs_per_repres = self._get_outputs_per_representations(
instance, profile_outputs
)
+ fill_data = copy.deepcopy(instance.data["anatomyData"])
for repre, outputs in outputs_per_repres:
# Check if input should be preconverted before processing
# Store original staging dir (it's value may change)
@@ -291,9 +293,24 @@ class ExtractReview(pyblish.api.InstancePlugin):
temp_data["frame_start"],
temp_data["frame_end"])
+ # create or update outputName
+ output_name = new_repre.get("outputName", "")
+ output_ext = new_repre["ext"]
+ if output_name:
+ output_name += "_"
+ output_name += output_def["filename_suffix"]
+ if temp_data["without_handles"]:
+ output_name += "_noHandles"
+
+ # add outputName to anatomy format fill_data
+ fill_data.update({
+ "output": output_name,
+ "ext": output_ext
+ })
+
try: # temporary until oiiotool is supported cross platform
ffmpeg_args = self._ffmpeg_arguments(
- output_def, instance, new_repre, temp_data
+ output_def, instance, new_repre, temp_data, fill_data
)
except ZeroDivisionError:
if 'exr' in temp_data["origin_repre"]["ext"]:
@@ -316,14 +333,6 @@ class ExtractReview(pyblish.api.InstancePlugin):
for f in files_to_clean:
os.unlink(f)
- output_name = new_repre.get("outputName", "")
- output_ext = new_repre["ext"]
- if output_name:
- output_name += "_"
- output_name += output_def["filename_suffix"]
- if temp_data["without_handles"]:
- output_name += "_noHandles"
-
new_repre.update({
"name": "{}_{}".format(output_name, output_ext),
"outputName": output_name,
@@ -446,7 +455,9 @@ class ExtractReview(pyblish.api.InstancePlugin):
"handles_are_set": handles_are_set
}
- def _ffmpeg_arguments(self, output_def, instance, new_repre, temp_data):
+ def _ffmpeg_arguments(
+ self, output_def, instance, new_repre, temp_data, fill_data
+ ):
"""Prepares ffmpeg arguments for expected extraction.
Prepares input and output arguments based on output definition and
@@ -472,9 +483,6 @@ class ExtractReview(pyblish.api.InstancePlugin):
ffmpeg_input_args = [
value for value in _ffmpeg_input_args if value.strip()
]
- ffmpeg_output_args = [
- value for value in _ffmpeg_output_args if value.strip()
- ]
ffmpeg_video_filters = [
value for value in _ffmpeg_video_filters if value.strip()
]
@@ -482,6 +490,21 @@ class ExtractReview(pyblish.api.InstancePlugin):
value for value in _ffmpeg_audio_filters if value.strip()
]
+ ffmpeg_output_args = []
+ for value in _ffmpeg_output_args:
+ value = value.strip()
+ if not value:
+ continue
+ try:
+ value = value.format(**fill_data)
+ except Exception:
+ self.log.warning(
+ "Failed to format ffmpeg argument: {}".format(value),
+ exc_info=True
+ )
+ pass
+ ffmpeg_output_args.append(value)
+
# Prepare input and output filepaths
self.input_output_paths(new_repre, output_def, temp_data)
diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py
index 1b0b8da2ff..cec2e470b3 100644
--- a/openpype/plugins/publish/integrate_new.py
+++ b/openpype/plugins/publish/integrate_new.py
@@ -580,7 +580,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
if repre.get("outputName"):
representation["context"]["output"] = repre['outputName']
- if sequence_repre and repre.get("frameStart"):
+ if sequence_repre and repre.get("frameStart") is not None:
representation['context']['frame'] = (
dst_padding_exp % int(repre.get("frameStart"))
)
diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py
index e25b56744e..de0336be2b 100644
--- a/openpype/pype_commands.py
+++ b/openpype/pype_commands.py
@@ -14,7 +14,8 @@ from openpype.lib.remote_publish import (
publish_and_log,
fail_batch,
find_variant_key,
- get_task_data
+ get_task_data,
+ IN_PROGRESS_STATUS
)
@@ -161,21 +162,32 @@ class PypeCommands:
log.info("Publish finished.")
@staticmethod
- def remotepublishfromapp(project, batch_dir, host_name,
- user, targets=None):
+ def remotepublishfromapp(project, batch_path, host_name,
+ user_email, targets=None):
"""Opens installed variant of 'host' and run remote publish there.
- Currently implemented and tested for Photoshop where customer
- wants to process uploaded .psd file and publish collected layers
- from there.
+ Currently implemented and tested for Photoshop where customer
+ wants to process uploaded .psd file and publish collected layers
+ from there.
- Checks if no other batches are running (status =='in_progress). If
- so, it sleeps for SLEEP (this is separate process),
- waits for WAIT_FOR seconds altogether.
+ Checks if no other batches are running (status =='in_progress). If
+ so, it sleeps for SLEEP (this is separate process),
+ waits for WAIT_FOR seconds altogether.
- Requires installed host application on the machine.
+ Requires installed host application on the machine.
- Runs publish process as user would, in automatic fashion.
+ Runs publish process as user would, in automatic fashion.
+
+ Args:
+ project (str): project to publish (only single context is expected
+ per call of remotepublish
+ batch_path (str): Path batch folder. Contains subfolders with
+ resources (workfile, another subfolder 'renders' etc.)
+ host_name (str): 'photoshop'
+ user_email (string): email address for webpublisher - used to
+ find Ftrack user with same email
+ targets (list): Pyblish targets
+ (to choose validator for example)
"""
import pyblish.api
from openpype.api import Logger
@@ -185,9 +197,9 @@ class PypeCommands:
log.info("remotepublishphotoshop command")
- task_data = get_task_data(batch_dir)
+ task_data = get_task_data(batch_path)
- workfile_path = os.path.join(batch_dir,
+ workfile_path = os.path.join(batch_path,
task_data["task"],
task_data["files"][0])
@@ -196,9 +208,9 @@ class PypeCommands:
batch_id = task_data["batch"]
dbcon = get_webpublish_conn()
# safer to start logging here, launch might be broken altogether
- _id = start_webpublish_log(dbcon, batch_id, user)
+ _id = start_webpublish_log(dbcon, batch_id, user_email)
- batches_in_progress = list(dbcon.find({"status": "in_progress"}))
+ batches_in_progress = list(dbcon.find({"status": IN_PROGRESS_STATUS}))
if len(batches_in_progress) > 1:
fail_batch(_id, batches_in_progress, dbcon)
print("Another batch running, probably stuck, ask admin for help")
@@ -219,10 +231,11 @@ class PypeCommands:
print("env:: {}".format(env))
os.environ.update(env)
- os.environ["OPENPYPE_PUBLISH_DATA"] = batch_dir
+ os.environ["OPENPYPE_PUBLISH_DATA"] = batch_path
# must pass identifier to update log lines for a batch
os.environ["BATCH_LOG_ID"] = str(_id)
os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib
+ os.environ["USER_EMAIL"] = user_email
pyblish.api.register_host(host_name)
if targets:
@@ -247,7 +260,7 @@ class PypeCommands:
time.sleep(0.5)
@staticmethod
- def remotepublish(project, batch_path, user, targets=None):
+ def remotepublish(project, batch_path, user_email, targets=None):
"""Start headless publishing.
Used to publish rendered assets, workfiles etc.
@@ -259,7 +272,8 @@ class PypeCommands:
per call of remotepublish
batch_path (str): Path batch folder. Contains subfolders with
resources (workfile, another subfolder 'renders' etc.)
- user (string): email address for webpublisher
+ user_email (string): email address for webpublisher - used to
+ find Ftrack user with same email
targets (list): Pyblish targets
(to choose validator for example)
@@ -283,6 +297,7 @@ class PypeCommands:
os.environ["OPENPYPE_PUBLISH_DATA"] = batch_path
os.environ["AVALON_PROJECT"] = project
os.environ["AVALON_APP"] = host_name
+ os.environ["USER_EMAIL"] = user_email
pyblish.api.register_host(host_name)
@@ -298,9 +313,9 @@ class PypeCommands:
_, batch_id = os.path.split(batch_path)
dbcon = get_webpublish_conn()
- _id = start_webpublish_log(dbcon, batch_id, user)
+ _id = start_webpublish_log(dbcon, batch_id, user_email)
- publish_and_log(dbcon, _id, log)
+ publish_and_log(dbcon, _id, log, batch_id=batch_id)
log.info("Publish finished.")
diff --git a/openpype/resources/__init__.py b/openpype/resources/__init__.py
index f463933525..34a833d080 100644
--- a/openpype/resources/__init__.py
+++ b/openpype/resources/__init__.py
@@ -1,5 +1,5 @@
import os
-from openpype.lib.pype_info import is_running_staging
+from openpype.lib.openpype_version import is_running_staging
RESOURCES_DIR = os.path.dirname(os.path.abspath(__file__))
diff --git a/openpype/scripts/non_python_host_launch.py b/openpype/scripts/non_python_host_launch.py
index 32c4b23f4f..1dc9a84993 100644
--- a/openpype/scripts/non_python_host_launch.py
+++ b/openpype/scripts/non_python_host_launch.py
@@ -81,9 +81,9 @@ def main(argv):
host_name = os.environ["AVALON_APP"].lower()
if host_name == "photoshop":
- from avalon.photoshop.lib import main
+ from openpype.hosts.photoshop.api.lib import main
elif host_name == "aftereffects":
- from avalon.aftereffects.lib import main
+ from openpype.hosts.aftereffects.api.lib import main
elif host_name == "harmony":
from avalon.harmony.lib import main
else:
diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py
index 3fc1412e62..639657d68f 100644
--- a/openpype/scripts/otio_burnin.py
+++ b/openpype/scripts/otio_burnin.py
@@ -157,6 +157,16 @@ def _dnxhd_codec_args(stream_data, source_ffmpeg_cmd):
if pix_fmt:
output.extend(["-pix_fmt", pix_fmt])
+ # Use arguments from source if are available source arguments
+ if source_ffmpeg_cmd:
+ copy_args = (
+ "-b:v", "-vb",
+ )
+ args = source_ffmpeg_cmd.split(" ")
+ for idx, arg in enumerate(args):
+ if arg in copy_args:
+ output.extend([arg, args[idx + 1]])
+
output.extend(["-g", "1"])
return output
@@ -716,6 +726,15 @@ def burnins_from_data(
ffmpeg_args.extend(
get_codec_args(burnin.ffprobe_data, source_ffmpeg_cmd)
)
+ # Use arguments from source if are available source arguments
+ if source_ffmpeg_cmd:
+ copy_args = (
+ "-metadata",
+ )
+ args = source_ffmpeg_cmd.split(" ")
+ for idx, arg in enumerate(args):
+ if arg in copy_args:
+ ffmpeg_args.extend([arg, args[idx + 1]])
# Use group one (same as `-intra` argument, which is deprecated)
ffmpeg_args_str = " ".join(ffmpeg_args)
diff --git a/openpype/settings/defaults/project_settings/flame.json b/openpype/settings/defaults/project_settings/flame.json
new file mode 100644
index 0000000000..c81069ef5c
--- /dev/null
+++ b/openpype/settings/defaults/project_settings/flame.json
@@ -0,0 +1,35 @@
+{
+ "create": {
+ "CreateShotClip": {
+ "hierarchy": "{folder}/{sequence}",
+ "clipRename": true,
+ "clipName": "{sequence}{shot}",
+ "segmentIndex": true,
+ "countFrom": 10,
+ "countSteps": 10,
+ "folder": "shots",
+ "episode": "ep01",
+ "sequence": "a",
+ "track": "{_track_}",
+ "shot": "####",
+ "vSyncOn": false,
+ "workfileFrameStart": 1001,
+ "handleStart": 5,
+ "handleEnd": 5
+ }
+ },
+ "publish": {
+ "ExtractSubsetResources": {
+ "keep_original_representation": false,
+ "export_presets_mapping": {
+ "exr16fpdwaa": {
+ "ext": "exr",
+ "xml_preset_dir": "",
+ "xml_preset_file": "OpenEXR (16-bit fp DWAA).xml",
+ "representation_add_range": true,
+ "representation_tags": []
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json
index b3ea77a584..513611ebfb 100644
--- a/openpype/settings/defaults/project_settings/ftrack.json
+++ b/openpype/settings/defaults/project_settings/ftrack.json
@@ -318,6 +318,19 @@
"tasks": [],
"add_ftrack_family": true,
"advanced_filtering": []
+ },
+ {
+ "hosts": [
+ "flame"
+ ],
+ "families": [
+ "plate",
+ "take"
+ ],
+ "task_types": [],
+ "tasks": [],
+ "add_ftrack_family": true,
+ "advanced_filtering": []
}
]
},
diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json
index b75b0168ec..a756071106 100644
--- a/openpype/settings/defaults/project_settings/maya.json
+++ b/openpype/settings/defaults/project_settings/maya.json
@@ -166,6 +166,11 @@
"enabled": false,
"regex": "(?P.*)_(.*)_SHD"
},
+ "ValidateShadingEngine": {
+ "enabled": true,
+ "optional": true,
+ "active": true
+ },
"ValidateAttributes": {
"enabled": false,
"attributes": {}
diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json
index 1cbe09f576..4a8b6d82a2 100644
--- a/openpype/settings/defaults/system_settings/applications.json
+++ b/openpype/settings/defaults/system_settings/applications.json
@@ -129,7 +129,11 @@
"darwin": [],
"linux": []
},
- "environment": {}
+ "environment": {
+ "OPENPYPE_FLAME_PYTHON_EXEC": "/opt/Autodesk/python/2021/bin/python2.7",
+ "OPENPYPE_FLAME_PYTHONPATH": "/opt/Autodesk/flame_2021/python",
+ "OPENPYPE_WIRETAP_TOOLS": "/opt/Autodesk/wiretap/tools/2021"
+ }
},
"__dynamic_keys_labels__": {
"2021": "2021 (Testing Only)"
@@ -142,7 +146,10 @@
"icon": "{}/app_icons/nuke.png",
"host_name": "nuke",
"environment": {
- "NUKE_PATH": ["{NUKE_PATH}", "{OPENPYPE_STUDIO_PLUGINS}/nuke"]
+ "NUKE_PATH": [
+ "{NUKE_PATH}",
+ "{OPENPYPE_STUDIO_PLUGINS}/nuke"
+ ]
},
"variants": {
"13-0": {
@@ -248,7 +255,10 @@
"icon": "{}/app_icons/nuke.png",
"host_name": "nuke",
"environment": {
- "NUKE_PATH": ["{NUKE_PATH}", "{OPENPYPE_STUDIO_PLUGINS}/nuke"]
+ "NUKE_PATH": [
+ "{NUKE_PATH}",
+ "{OPENPYPE_STUDIO_PLUGINS}/nuke"
+ ]
},
"variants": {
"13-0": {
diff --git a/openpype/settings/defaults/system_settings/general.json b/openpype/settings/defaults/system_settings/general.json
index a07152eaf8..7c78de9a5c 100644
--- a/openpype/settings/defaults/system_settings/general.json
+++ b/openpype/settings/defaults/system_settings/general.json
@@ -4,6 +4,7 @@
"admin_password": "",
"production_version": "",
"staging_version": "",
+ "version_check_interval": 5,
"environment": {
"__environment_keys__": {
"global": []
diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py
index ff32df9262..7512d7bfcc 100644
--- a/openpype/settings/entities/input_entities.py
+++ b/openpype/settings/entities/input_entities.py
@@ -469,6 +469,17 @@ class PathInput(InputEntity):
# GUI attributes
self.placeholder_text = self.schema_data.get("placeholder")
+ def set(self, value):
+ # Strip value
+ super(PathInput, self).set(value.strip())
+
+ def set_override_state(self, state, ignore_missing_defaults):
+ super(PathInput, self).set_override_state(
+ state, ignore_missing_defaults
+ )
+ # Strip current value
+ self._current_value = self._current_value.strip()
+
class RawJsonEntity(InputEntity):
schema_types = ["raw-json"]
diff --git a/openpype/settings/entities/schemas/projects_schema/schema_main.json b/openpype/settings/entities/schemas/projects_schema/schema_main.json
index c9eca5dedd..8a2ad451ee 100644
--- a/openpype/settings/entities/schemas/projects_schema/schema_main.json
+++ b/openpype/settings/entities/schemas/projects_schema/schema_main.json
@@ -110,6 +110,10 @@
"type": "schema",
"name": "schema_project_celaction"
},
+ {
+ "type": "schema",
+ "name": "schema_project_flame"
+ },
{
"type": "schema",
"name": "schema_project_resolve"
diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json
new file mode 100644
index 0000000000..76576ebf73
--- /dev/null
+++ b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json
@@ -0,0 +1,194 @@
+{
+ "type": "dict",
+ "collapsible": true,
+ "key": "flame",
+ "label": "Flame",
+ "is_file": true,
+ "children": [
+ {
+ "type": "dict",
+ "collapsible": true,
+ "key": "create",
+ "label": "Create plugins",
+ "children": [
+ {
+ "type": "dict",
+ "collapsible": true,
+ "key": "CreateShotClip",
+ "label": "Create Shot Clip",
+ "is_group": true,
+ "children": [
+ {
+ "type": "collapsible-wrap",
+ "label": "Shot Hierarchy And Rename Settings",
+ "collapsible": false,
+ "children": [
+ {
+ "type": "text",
+ "key": "hierarchy",
+ "label": "Shot parent hierarchy"
+ },
+ {
+ "type": "boolean",
+ "key": "clipRename",
+ "label": "Rename clips"
+ },
+ {
+ "type": "text",
+ "key": "clipName",
+ "label": "Clip name template"
+ },
+ {
+ "type": "boolean",
+ "key": "segmentIndex",
+ "label": "Accept segment order"
+ },
+ {
+ "type": "number",
+ "key": "countFrom",
+ "label": "Count sequence from"
+ },
+ {
+ "type": "number",
+ "key": "countSteps",
+ "label": "Stepping number"
+ }
+ ]
+ },
+ {
+ "type": "collapsible-wrap",
+ "label": "Shot Template Keywords",
+ "collapsible": false,
+ "children": [
+ {
+ "type": "text",
+ "key": "folder",
+ "label": "{folder}"
+ },
+ {
+ "type": "text",
+ "key": "episode",
+ "label": "{episode}"
+ },
+ {
+ "type": "text",
+ "key": "sequence",
+ "label": "{sequence}"
+ },
+ {
+ "type": "text",
+ "key": "track",
+ "label": "{track}"
+ },
+ {
+ "type": "text",
+ "key": "shot",
+ "label": "{shot}"
+ }
+ ]
+ },
+ {
+ "type": "collapsible-wrap",
+ "label": "Vertical Synchronization Of Attributes",
+ "collapsible": false,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "vSyncOn",
+ "label": "Enable Vertical Sync"
+ }
+ ]
+ },
+ {
+ "type": "collapsible-wrap",
+ "label": "Shot Attributes",
+ "collapsible": false,
+ "children": [
+ {
+ "type": "number",
+ "key": "workfileFrameStart",
+ "label": "Workfiles Start Frame"
+ },
+ {
+ "type": "number",
+ "key": "handleStart",
+ "label": "Handle start (head)"
+ },
+ {
+ "type": "number",
+ "key": "handleEnd",
+ "label": "Handle end (tail)"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "dict",
+ "collapsible": true,
+ "key": "publish",
+ "label": "Publish plugins",
+ "children": [
+ {
+ "type": "dict",
+ "collapsible": true,
+ "key": "ExtractSubsetResources",
+ "label": "Extract Subset Resources",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "keep_original_representation",
+ "label": "Publish clip's original media"
+ },
+ {
+ "key": "export_presets_mapping",
+ "label": "Export presets mapping",
+ "type": "dict-modifiable",
+ "highlight_content": true,
+ "object_type": {
+ "type": "dict",
+ "children": [
+ {
+ "key": "ext",
+ "label": "Output extension",
+ "type": "text"
+ },
+ {
+ "key": "xml_preset_file",
+ "label": "XML preset file (with ext)",
+ "type": "text"
+ },
+ {
+ "key": "xml_preset_dir",
+ "label": "XML preset folder (optional)",
+ "type": "text"
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "type": "boolean",
+ "key": "representation_add_range",
+ "label": "Add frame range to representation"
+ },
+ {
+ "type": "list",
+ "key": "representation_tags",
+ "label": "Add representation tags",
+ "object_type": {
+ "type": "text",
+ "multiline": false
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json
index 9ca4e443bd..4e82c991e7 100644
--- a/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json
+++ b/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json
@@ -91,6 +91,11 @@
"key": "upload_thumbnail",
"label": "Upload thumbnail"
},
+ {
+ "type": "boolean",
+ "key": "upload_review",
+ "label": "Upload review"
+ },
{
"type": "text",
"multiline": true,
diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json
index 606dd6c2bb..7c9a5a6b46 100644
--- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json
+++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json
@@ -72,6 +72,17 @@
]
},
+ {
+ "type": "schema_template",
+ "name": "template_publish_plugin",
+ "template_data": [
+ {
+ "key": "ValidateShadingEngine",
+ "label": "Validate Look Shading Engine Naming"
+ }
+ ]
+ },
+
{
"type": "dict",
"collapsible": true,
diff --git a/openpype/settings/entities/schemas/system_schema/schema_general.json b/openpype/settings/entities/schemas/system_schema/schema_general.json
index b4c83fc85f..3af3f5ce35 100644
--- a/openpype/settings/entities/schemas/system_schema/schema_general.json
+++ b/openpype/settings/entities/schemas/system_schema/schema_general.json
@@ -47,6 +47,19 @@
{
"type": "splitter"
},
+ {
+ "type": "label",
+ "label": "Trigger validation if running OpenPype is using studio defined version each 'n' minutes . Validation happens in OpenPype tray application."
+ },
+ {
+ "type": "number",
+ "key": "version_check_interval",
+ "label": "Version check interval",
+ "minimum": 0
+ },
+ {
+ "type": "splitter"
+ },
{
"key": "environment",
"label": "Environment",
diff --git a/openpype/style/data.json b/openpype/style/data.json
index 62573f015e..1db0c732cf 100644
--- a/openpype/style/data.json
+++ b/openpype/style/data.json
@@ -51,6 +51,11 @@
"border-hover": "rgba(168, 175, 189, .3)",
"border-focus": "rgb(92, 173, 214)",
+ "restart-btn-bg": "#458056",
+
+ "delete-btn-bg": "rgb(201, 54, 54)",
+ "delete-btn-bg-disabled": "rgba(201, 54, 54, 64)",
+
"tab-widget": {
"bg": "#21252B",
"bg-selected": "#434a56",
diff --git a/openpype/style/style.css b/openpype/style/style.css
index 3e95ece4b9..d9b0ff7421 100644
--- a/openpype/style/style.css
+++ b/openpype/style/style.css
@@ -734,6 +734,13 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
background: {color:bg-view-hover};
}
+#DeleteButton {
+ background: {color:delete-btn-bg};
+}
+#DeleteButton:disabled {
+ background: {color:delete-btn-bg-disabled};
+}
+
/* Launcher specific stylesheets */
#IconView[mode="icon"] {
/* font size can't be set on items */
@@ -1221,6 +1228,11 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
background: #21252B;
}
+/* Tray */
+#TrayRestartButton {
+ background: {color:restart-btn-bg};
+}
+
/* Globally used names */
#Separator {
background: {color:bg-menu-separator};
diff --git a/openpype/tools/project_manager/project_manager/images/warning.png b/openpype/tools/project_manager/project_manager/images/warning.png
new file mode 100644
index 0000000000..3b4ae861f9
Binary files /dev/null and b/openpype/tools/project_manager/project_manager/images/warning.png differ
diff --git a/openpype/tools/project_manager/project_manager/style.py b/openpype/tools/project_manager/project_manager/style.py
index d3d6857a63..9fa7a5520b 100644
--- a/openpype/tools/project_manager/project_manager/style.py
+++ b/openpype/tools/project_manager/project_manager/style.py
@@ -1,6 +1,7 @@
import os
from Qt import QtCore, QtGui
+from openpype.style import get_objected_colors
from avalon.vendor import qtawesome
@@ -90,6 +91,17 @@ class ResourceCache:
icon.addPixmap(disabled_pix, QtGui.QIcon.Disabled, QtGui.QIcon.Off)
return icon
+ @classmethod
+ def get_warning_pixmap(cls):
+ src_image = get_warning_image()
+ colors = get_objected_colors()
+ color_value = colors["delete-btn-bg"]
+
+ return paint_image_with_color(
+ src_image,
+ color_value.get_qcolor()
+ )
+
def get_remove_image():
image_path = os.path.join(
@@ -100,6 +112,15 @@ def get_remove_image():
return QtGui.QImage(image_path)
+def get_warning_image():
+ image_path = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)),
+ "images",
+ "warning.png"
+ )
+ return QtGui.QImage(image_path)
+
+
def paint_image_with_color(image, color):
"""TODO: This function should be imported from utils.
diff --git a/openpype/tools/project_manager/project_manager/widgets.py b/openpype/tools/project_manager/project_manager/widgets.py
index e4c58a8a2c..4b5aca35ef 100644
--- a/openpype/tools/project_manager/project_manager/widgets.py
+++ b/openpype/tools/project_manager/project_manager/widgets.py
@@ -4,6 +4,7 @@ from .constants import (
NAME_ALLOWED_SYMBOLS,
NAME_REGEX
)
+from .style import ResourceCache
from openpype.lib import (
create_project,
PROJECT_NAME_ALLOWED_SYMBOLS,
@@ -13,7 +14,7 @@ from openpype.style import load_stylesheet
from openpype.tools.utils import PlaceholderLineEdit
from avalon.api import AvalonMongoDB
-from Qt import QtWidgets, QtCore
+from Qt import QtWidgets, QtCore, QtGui
class NameTextEdit(QtWidgets.QLineEdit):
@@ -291,42 +292,41 @@ class CreateProjectDialog(QtWidgets.QDialog):
return project_names, project_codes
-class _SameSizeBtns(QtWidgets.QPushButton):
- """Button that keep width of all button added as related.
+# TODO PixmapLabel should be moved to 'utils' in other future PR so should be
+# imported from there
+class PixmapLabel(QtWidgets.QLabel):
+ """Label resizing image to height of font."""
+ def __init__(self, pixmap, parent):
+ super(PixmapLabel, self).__init__(parent)
+ self._empty_pixmap = QtGui.QPixmap(0, 0)
+ self._source_pixmap = pixmap
- This happens without changing min/max/fix size of button. Which is
- welcomed for multidisplay desktops with different resolution.
- """
- def __init__(self, *args, **kwargs):
- super(_SameSizeBtns, self).__init__(*args, **kwargs)
- self._related_btns = []
+ def set_source_pixmap(self, pixmap):
+ """Change source image."""
+ self._source_pixmap = pixmap
+ self._set_resized_pix()
- def add_related_btn(self, btn):
- """Add related button which should be checked for width.
+ def _get_pix_size(self):
+ size = self.fontMetrics().height() * 4
+ return size, size
- Args:
- btn (_SameSizeBtns): Other object of _SameSizeBtns.
- """
- self._related_btns.append(btn)
+ def _set_resized_pix(self):
+ if self._source_pixmap is None:
+ self.setPixmap(self._empty_pixmap)
+ return
+ width, height = self._get_pix_size()
+ self.setPixmap(
+ self._source_pixmap.scaled(
+ width,
+ height,
+ QtCore.Qt.KeepAspectRatio,
+ QtCore.Qt.SmoothTransformation
+ )
+ )
- def hint_width(self):
- """Get size hint of button not related to others."""
- return super(_SameSizeBtns, self).sizeHint().width()
-
- def sizeHint(self):
- """Calculate size hint based on size hint of this button and related.
-
- If width is lower than any other button it is changed to higher.
- """
- result = super(_SameSizeBtns, self).sizeHint()
- width = result.width()
- for btn in self._related_btns:
- btn_width = btn.hint_width()
- if btn_width > width:
- width = btn_width
-
- result.setWidth(width)
- return result
+ def resizeEvent(self, event):
+ self._set_resized_pix()
+ super(PixmapLabel, self).resizeEvent(event)
class ConfirmProjectDeletion(QtWidgets.QDialog):
@@ -336,35 +336,50 @@ class ConfirmProjectDeletion(QtWidgets.QDialog):
self.setWindowTitle("Delete project?")
- message = (
- "Project \"{}\" with all related data will be"
- " permanently removed from the database (This actions won't remove"
- " any files on disk)."
- ).format(project_name)
- message_label = QtWidgets.QLabel(message, self)
+ top_widget = QtWidgets.QWidget(self)
+
+ warning_pixmap = ResourceCache.get_warning_pixmap()
+ warning_icon_label = PixmapLabel(warning_pixmap, top_widget)
+
+ message_label = QtWidgets.QLabel(top_widget)
message_label.setWordWrap(True)
+ message_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
+ message_label.setText((
+ "WARNING: This cannot be undone. "
+ "Project \"{}\" with all related data will be"
+ " permanently removed from the database. (This action won't remove"
+ " any files on disk.)"
+ ).format(project_name))
+
+ top_layout = QtWidgets.QHBoxLayout(top_widget)
+ top_layout.setContentsMargins(0, 0, 0, 0)
+ top_layout.addWidget(
+ warning_icon_label, 0,
+ QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter
+ )
+ top_layout.addWidget(message_label, 1)
question_label = QtWidgets.QLabel("Are you sure? ", self)
confirm_input = PlaceholderLineEdit(self)
- confirm_input.setPlaceholderText("Type \"Delete\" to confirm...")
+ confirm_input.setPlaceholderText(
+ "Type \"{}\" to confirm...".format(project_name)
+ )
- cancel_btn = _SameSizeBtns("Cancel", self)
+ cancel_btn = QtWidgets.QPushButton("Cancel", self)
cancel_btn.setToolTip("Cancel deletion of the project")
- confirm_btn = _SameSizeBtns("Delete", self)
+ confirm_btn = QtWidgets.QPushButton("Permanently Delete Project", self)
+ confirm_btn.setObjectName("DeleteButton")
confirm_btn.setEnabled(False)
confirm_btn.setToolTip("Confirm deletion")
- cancel_btn.add_related_btn(confirm_btn)
- confirm_btn.add_related_btn(cancel_btn)
-
btns_layout = QtWidgets.QHBoxLayout()
btns_layout.addStretch(1)
btns_layout.addWidget(cancel_btn, 0)
btns_layout.addWidget(confirm_btn, 0)
layout = QtWidgets.QVBoxLayout(self)
- layout.addWidget(message_label, 0)
+ layout.addWidget(top_widget, 0)
layout.addStretch(1)
layout.addWidget(question_label, 0)
layout.addWidget(confirm_input, 0)
@@ -379,6 +394,7 @@ class ConfirmProjectDeletion(QtWidgets.QDialog):
self._confirm_btn = confirm_btn
self._confirm_input = confirm_input
self._result = 0
+ self._project_name = project_name
self.setMinimumWidth(480)
self.setMaximumWidth(650)
@@ -411,5 +427,5 @@ class ConfirmProjectDeletion(QtWidgets.QDialog):
self._on_confirm_click()
def _on_confirm_text_change(self):
- enabled = self._confirm_input.text().lower() == "delete"
+ enabled = self._confirm_input.text() == self._project_name
self._confirm_btn.setEnabled(enabled)
diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py
index a05811e813..0298d565a5 100644
--- a/openpype/tools/project_manager/project_manager/window.py
+++ b/openpype/tools/project_manager/project_manager/window.py
@@ -78,7 +78,9 @@ class ProjectManagerWindow(QtWidgets.QWidget):
)
create_folders_btn.setEnabled(False)
- remove_projects_btn = QtWidgets.QPushButton(project_widget)
+ remove_projects_btn = QtWidgets.QPushButton(
+ "Delete project", project_widget
+ )
remove_projects_btn.setIcon(ResourceCache.get_icon("remove"))
remove_projects_btn.setObjectName("IconBtn")
diff --git a/openpype/tools/pyblish_pype/window.py b/openpype/tools/pyblish_pype/window.py
index fdd2d80e23..edcf6f53b6 100644
--- a/openpype/tools/pyblish_pype/window.py
+++ b/openpype/tools/pyblish_pype/window.py
@@ -909,6 +909,13 @@ class Window(QtWidgets.QDialog):
self.tr("Processing"), plugin_item.data(QtCore.Qt.DisplayRole)
))
+ visibility = True
+ if hasattr(plugin, "hide_ui_on_process") and plugin.hide_ui_on_process:
+ visibility = False
+
+ if self.isVisible() != visibility:
+ self.setVisible(visibility)
+
def on_plugin_action_menu_requested(self, pos):
"""The user right-clicked on a plug-in
__________
diff --git a/openpype/tools/standalonepublish/widgets/widget_components.py b/openpype/tools/standalonepublish/widgets/widget_components.py
index 2ac54af4e3..4d7f94f825 100644
--- a/openpype/tools/standalonepublish/widgets/widget_components.py
+++ b/openpype/tools/standalonepublish/widgets/widget_components.py
@@ -10,7 +10,7 @@ from .constants import HOST_NAME
from avalon import io
from openpype.api import execute, Logger
from openpype.lib import (
- get_pype_execute_args,
+ get_openpype_execute_args,
apply_project_environments_value
)
@@ -193,7 +193,7 @@ def cli_publish(data, publish_paths, gui=True):
project_name = os.environ["AVALON_PROJECT"]
env_copy = apply_project_environments_value(project_name, envcopy)
- args = get_pype_execute_args("run", PUBLISH_SCRIPT_PATH)
+ args = get_openpype_execute_args("run", PUBLISH_SCRIPT_PATH)
result = execute(args, env=envcopy)
result = {}
diff --git a/openpype/tools/tray/images/gifts.png b/openpype/tools/tray/images/gifts.png
new file mode 100644
index 0000000000..57fb3f2863
Binary files /dev/null and b/openpype/tools/tray/images/gifts.png differ
diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py
index 8c6a6d3266..c9b8aaa842 100644
--- a/openpype/tools/tray/pype_tray.py
+++ b/openpype/tools/tray/pype_tray.py
@@ -14,7 +14,15 @@ from openpype.api import (
resources,
get_system_settings
)
-from openpype.lib import get_pype_execute_args
+from openpype.lib import (
+ get_openpype_execute_args,
+ op_version_control_available,
+ is_current_version_studio_latest,
+ is_running_from_build,
+ is_running_staging,
+ get_expected_version,
+ get_openpype_version
+)
from openpype.modules import TrayModulesManager
from openpype import style
from openpype.settings import (
@@ -22,29 +30,177 @@ from openpype.settings import (
ProjectSettings,
DefaultsNotDefined
)
+from openpype.tools.utils import (
+ WrappedCallbackItem,
+ paint_image_with_color
+)
from .pype_info_widget import PypeInfoWidget
+# TODO PixmapLabel should be moved to 'utils' in other future PR so should be
+# imported from there
+class PixmapLabel(QtWidgets.QLabel):
+ """Label resizing image to height of font."""
+ def __init__(self, pixmap, parent):
+ super(PixmapLabel, self).__init__(parent)
+ self._empty_pixmap = QtGui.QPixmap(0, 0)
+ self._source_pixmap = pixmap
+
+ def set_source_pixmap(self, pixmap):
+ """Change source image."""
+ self._source_pixmap = pixmap
+ self._set_resized_pix()
+
+ def _get_pix_size(self):
+ size = self.fontMetrics().height() * 3
+ return size, size
+
+ def _set_resized_pix(self):
+ if self._source_pixmap is None:
+ self.setPixmap(self._empty_pixmap)
+ return
+ width, height = self._get_pix_size()
+ self.setPixmap(
+ self._source_pixmap.scaled(
+ width,
+ height,
+ QtCore.Qt.KeepAspectRatio,
+ QtCore.Qt.SmoothTransformation
+ )
+ )
+
+ def resizeEvent(self, event):
+ self._set_resized_pix()
+ super(PixmapLabel, self).resizeEvent(event)
+
+
+class VersionDialog(QtWidgets.QDialog):
+ restart_requested = QtCore.Signal()
+ ignore_requested = QtCore.Signal()
+
+ _min_width = 400
+ _min_height = 130
+
+ def __init__(self, parent=None):
+ super(VersionDialog, self).__init__(parent)
+ self.setWindowTitle("OpenPype update is needed")
+ icon = QtGui.QIcon(resources.get_openpype_icon_filepath())
+ self.setWindowIcon(icon)
+ self.setWindowFlags(
+ self.windowFlags()
+ | QtCore.Qt.WindowStaysOnTopHint
+ )
+
+ self.setMinimumWidth(self._min_width)
+ self.setMinimumHeight(self._min_height)
+
+ top_widget = QtWidgets.QWidget(self)
+
+ gift_pixmap = self._get_gift_pixmap()
+ gift_icon_label = PixmapLabel(gift_pixmap, top_widget)
+
+ label_widget = QtWidgets.QLabel(top_widget)
+ label_widget.setWordWrap(True)
+
+ top_layout = QtWidgets.QHBoxLayout(top_widget)
+ # top_layout.setContentsMargins(0, 0, 0, 0)
+ top_layout.setSpacing(10)
+ top_layout.addWidget(gift_icon_label, 0, QtCore.Qt.AlignCenter)
+ top_layout.addWidget(label_widget, 1)
+
+ ignore_btn = QtWidgets.QPushButton("Later", self)
+ restart_btn = QtWidgets.QPushButton("Restart && Update", self)
+ restart_btn.setObjectName("TrayRestartButton")
+
+ btns_layout = QtWidgets.QHBoxLayout()
+ btns_layout.addStretch(1)
+ btns_layout.addWidget(ignore_btn, 0)
+ btns_layout.addWidget(restart_btn, 0)
+
+ layout = QtWidgets.QVBoxLayout(self)
+ layout.addWidget(top_widget, 0)
+ layout.addStretch(1)
+ layout.addLayout(btns_layout, 0)
+
+ ignore_btn.clicked.connect(self._on_ignore)
+ restart_btn.clicked.connect(self._on_reset)
+
+ self._label_widget = label_widget
+ self._restart_accepted = False
+
+ self.setStyleSheet(style.load_stylesheet())
+
+ def _get_gift_pixmap(self):
+ image_path = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)),
+ "images",
+ "gifts.png"
+ )
+ src_image = QtGui.QImage(image_path)
+ colors = style.get_objected_colors()
+ color_value = colors["font"]
+
+ return paint_image_with_color(
+ src_image,
+ color_value.get_qcolor()
+ )
+
+ def showEvent(self, event):
+ super().showEvent(event)
+ self._restart_accepted = False
+
+ def closeEvent(self, event):
+ super().closeEvent(event)
+ if not self._restart_accepted:
+ self.ignore_requested.emit()
+
+ def update_versions(self, current_version, expected_version):
+ message = (
+ "Running OpenPype version is {} ."
+ " Your production has been updated to version {} ."
+ ).format(str(current_version), str(expected_version))
+ self._label_widget.setText(message)
+
+ def _on_ignore(self):
+ self.reject()
+
+ def _on_reset(self):
+ self._restart_accepted = True
+ self.restart_requested.emit()
+ self.accept()
+
+
class TrayManager:
"""Cares about context of application.
Load submenus, actions, separators and modules into tray's context.
"""
-
def __init__(self, tray_widget, main_window):
self.tray_widget = tray_widget
self.main_window = main_window
self.pype_info_widget = None
+ self._restart_action = None
self.log = Logger.get_logger(self.__class__.__name__)
- self.module_settings = get_system_settings()["modules"]
+ system_settings = get_system_settings()
+ self.module_settings = system_settings["modules"]
+
+ version_check_interval = system_settings["general"].get(
+ "version_check_interval"
+ )
+ if version_check_interval is None:
+ version_check_interval = 5
+ self._version_check_interval = version_check_interval * 60 * 1000
self.modules_manager = TrayModulesManager()
self.errors = []
+ self._version_check_timer = None
+ self._version_dialog = None
+
self.main_thread_timer = None
self._main_thread_callbacks = collections.deque()
self._execution_in_progress = None
@@ -61,21 +217,73 @@ class TrayManager:
if callback:
self.execute_in_main_thread(callback)
- def execute_in_main_thread(self, callback):
- self._main_thread_callbacks.append(callback)
+ def _on_version_check_timer(self):
+ # Check if is running from build and stop future validations if yes
+ if not is_running_from_build() or not op_version_control_available():
+ self._version_check_timer.stop()
+ return
+
+ self.validate_openpype_version()
+
+ def validate_openpype_version(self):
+ using_requested = is_current_version_studio_latest()
+ self._restart_action.setVisible(not using_requested)
+ if using_requested:
+ if (
+ self._version_dialog is not None
+ and self._version_dialog.isVisible()
+ ):
+ self._version_dialog.close()
+ return
+
+ if self._version_dialog is None:
+ self._version_dialog = VersionDialog()
+ self._version_dialog.restart_requested.connect(
+ self._restart_and_install
+ )
+ self._version_dialog.ignore_requested.connect(
+ self._outdated_version_ignored
+ )
+
+ expected_version = get_expected_version()
+ current_version = get_openpype_version()
+ self._version_dialog.update_versions(
+ current_version, expected_version
+ )
+ self._version_dialog.show()
+ self._version_dialog.raise_()
+ self._version_dialog.activateWindow()
+
+ def _restart_and_install(self):
+ self.restart()
+
+ def _outdated_version_ignored(self):
+ self.show_tray_message(
+ "OpenPype version is outdated",
+ (
+ "Please update your OpenPype as soon as possible."
+ " To update, restart OpenPype Tray application."
+ )
+ )
+
+ def execute_in_main_thread(self, callback, *args, **kwargs):
+ if isinstance(callback, WrappedCallbackItem):
+ item = callback
+ else:
+ item = WrappedCallbackItem(callback, *args, **kwargs)
+
+ self._main_thread_callbacks.append(item)
+
+ return item
def _main_thread_execution(self):
if self._execution_in_progress:
return
self._execution_in_progress = True
- while self._main_thread_callbacks:
- try:
- callback = self._main_thread_callbacks.popleft()
- callback()
- except:
- self.log.warning(
- "Failed to execute {} in main thread".format(callback),
- exc_info=True)
+ for _ in range(len(self._main_thread_callbacks)):
+ if self._main_thread_callbacks:
+ item = self._main_thread_callbacks.popleft()
+ item.execute()
self._execution_in_progress = False
@@ -119,6 +327,13 @@ class TrayManager:
self.main_thread_timer = main_thread_timer
+ version_check_timer = QtCore.QTimer()
+ version_check_timer.timeout.connect(self._on_version_check_timer)
+ if self._version_check_interval > 0:
+ version_check_timer.setInterval(self._version_check_interval)
+ version_check_timer.start()
+ self._version_check_timer = version_check_timer
+
# For storing missing settings dialog
self._settings_validation_dialog = None
@@ -200,24 +415,47 @@ class TrayManager:
version_action = QtWidgets.QAction(version_string, self.tray_widget)
version_action.triggered.connect(self._on_version_action)
+
+ restart_action = QtWidgets.QAction(
+ "Restart && Update", self.tray_widget
+ )
+ restart_action.triggered.connect(self._on_restart_action)
+ restart_action.setVisible(False)
+
self.tray_widget.menu.addAction(version_action)
+ self.tray_widget.menu.addAction(restart_action)
self.tray_widget.menu.addSeparator()
- def restart(self):
+ self._restart_action = restart_action
+
+ def _on_restart_action(self):
+ self.restart()
+
+ def restart(self, reset_version=True):
"""Restart Tray tool.
First creates new process with same argument and close current tray.
"""
- args = get_pype_execute_args()
+ args = get_openpype_execute_args()
+ kwargs = {
+ "env": dict(os.environ.items())
+ }
+
# Create a copy of sys.argv
additional_args = list(sys.argv)
- # Check last argument from `get_pype_execute_args`
+ # Check last argument from `get_openpype_execute_args`
# - when running from code it is the same as first from sys.argv
if args[-1] == additional_args[0]:
additional_args.pop(0)
- args.extend(additional_args)
- kwargs = {}
+ # Pop OPENPYPE_VERSION
+ if reset_version:
+ # Add staging flag if was running from staging
+ if is_running_staging():
+ args.append("--use-staging")
+ kwargs["env"].pop("OPENPYPE_VERSION", None)
+
+ args.extend(additional_args)
if platform.system().lower() == "windows":
flags = (
subprocess.CREATE_NEW_PROCESS_GROUP
diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py
index 4dd6bdd05f..eb0cb1eef5 100644
--- a/openpype/tools/utils/__init__.py
+++ b/openpype/tools/utils/__init__.py
@@ -6,6 +6,10 @@ from .widgets import (
)
from .error_dialog import ErrorMessageBox
+from .lib import (
+ WrappedCallbackItem,
+ paint_image_with_color
+)
__all__ = (
@@ -14,5 +18,8 @@ __all__ = (
"ClickableFrame",
"ExpandBtn",
- "ErrorMessageBox"
+ "ErrorMessageBox",
+
+ "WrappedCallbackItem",
+ "paint_image_with_color",
)
diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py
index 6742df8557..5f3456ae3e 100644
--- a/openpype/tools/utils/lib.py
+++ b/openpype/tools/utils/lib.py
@@ -9,7 +9,10 @@ import avalon.api
from avalon import style
from avalon.vendor import qtawesome
-from openpype.api import get_project_settings
+from openpype.api import (
+ get_project_settings,
+ Logger
+)
from openpype.lib import filter_profiles
@@ -598,3 +601,68 @@ def is_remove_site_loader(loader):
def is_add_site_loader(loader):
return hasattr(loader, "add_site_to_representation")
+
+
+class WrappedCallbackItem:
+ """Structure to store information about callback and args/kwargs for it.
+
+ Item can be used to execute callback in main thread which may be needed
+ for execution of Qt objects.
+
+ Item store callback (callable variable), arguments and keyword arguments
+ for the callback. Item hold information about it's process.
+ """
+ not_set = object()
+ _log = None
+
+ def __init__(self, callback, *args, **kwargs):
+ self._done = False
+ self._exception = self.not_set
+ self._result = self.not_set
+ self._callback = callback
+ self._args = args
+ self._kwargs = kwargs
+
+ def __call__(self):
+ self.execute()
+
+ @property
+ def log(self):
+ cls = self.__class__
+ if cls._log is None:
+ cls._log = Logger.get_logger(cls.__name__)
+ return cls._log
+
+ @property
+ def done(self):
+ return self._done
+
+ @property
+ def exception(self):
+ return self._exception
+
+ @property
+ def result(self):
+ return self._result
+
+ def execute(self):
+ """Execute callback and store it's result.
+
+ Method must be called from main thread. Item is marked as `done`
+ when callback execution finished. Store output of callback of exception
+ information when callback raise one.
+ """
+ if self.done:
+ self.log.warning("- item is already processed")
+ return
+
+ self.log.debug("Running callback: {}".format(str(self._callback)))
+ try:
+ result = self._callback(*self._args, **self._kwargs)
+ self._result = result
+
+ except Exception as exc:
+ self._exception = exc
+
+ finally:
+ self._done = True
diff --git a/openpype/version.py b/openpype/version.py
index ed0a96d4de..121bb01e8f 100644
--- a/openpype/version.py
+++ b/openpype/version.py
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
-__version__ = "3.8.0-nightly.2"
+__version__ = "3.8.0-nightly.5"
diff --git a/pyproject.toml b/pyproject.toml
index 0ef447e0be..04d48401ab 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "OpenPype"
-version = "3.8.0-nightly.2" # OpenPype
+version = "3.8.0-nightly.5" # OpenPype
description = "Open VFX and Animation pipeline with support."
authors = ["OpenPype Team "]
license = "MIT License"
diff --git a/setup.py b/setup.py
index 41ed066693..3ee6ad43ea 100644
--- a/setup.py
+++ b/setup.py
@@ -4,15 +4,66 @@ import os
import sys
import re
import platform
+import distutils.spawn
from pathlib import Path
from cx_Freeze import setup, Executable
from sphinx.setup_command import BuildDoc
-version = {}
-
openpype_root = Path(os.path.dirname(__file__))
+
+def validate_thirdparty_binaries():
+ """Check existence of thirdpart executables."""
+ low_platform = platform.system().lower()
+ binary_vendors_dir = os.path.join(
+ openpype_root,
+ "vendor",
+ "bin"
+ )
+
+ error_msg = (
+ "Missing binary dependency {}. Please fetch thirdparty dependencies."
+ )
+ # Validate existence of FFmpeg
+ ffmpeg_dir = os.path.join(binary_vendors_dir, "ffmpeg", low_platform)
+ if low_platform == "windows":
+ ffmpeg_dir = os.path.join(ffmpeg_dir, "bin")
+ ffmpeg_executable = os.path.join(ffmpeg_dir, "ffmpeg")
+ ffmpeg_result = distutils.spawn.find_executable(ffmpeg_executable)
+ if ffmpeg_result is None:
+ raise RuntimeError(error_msg.format("FFmpeg"))
+
+ # Validate existence of OpenImageIO (not on MacOs)
+ oiio_tool_path = None
+ if low_platform == "linux":
+ oiio_tool_path = os.path.join(
+ binary_vendors_dir,
+ "oiio",
+ low_platform,
+ "bin",
+ "oiiotool"
+ )
+ elif low_platform == "windows":
+ oiio_tool_path = os.path.join(
+ binary_vendors_dir,
+ "oiio",
+ low_platform,
+ "oiiotool"
+ )
+ oiio_result = None
+ if oiio_tool_path is not None:
+ oiio_result = distutils.spawn.find_executable(oiio_tool_path)
+ if oiio_result is None:
+ raise RuntimeError(error_msg.format("OpenImageIO"))
+
+
+# Give ability to skip vaidation
+if not os.getenv("SKIP_THIRD_PARTY_VALIDATION"):
+ validate_thirdparty_binaries()
+
+version = {}
+
with open(openpype_root / "openpype" / "version.py") as fp:
exec(fp.read(), version)
diff --git a/tools/create_zip.py b/tools/create_zip.py
index 32a4d27e8b..2fc351469a 100644
--- a/tools/create_zip.py
+++ b/tools/create_zip.py
@@ -31,7 +31,9 @@ def main(path):
bs = bootstrap_repos.BootstrapRepos(progress_callback=progress)
if path:
out_path = Path(path)
- bs.data_dir = out_path.parent
+ bs.data_dir = out_path
+ if out_path.is_file():
+ bs.data_dir = out_path.parent
_print(f"Creating zip in {bs.data_dir} ...")
repo_file = bs.create_version_from_live_code()
diff --git a/website/docs/module_slack.md b/website/docs/module_slack.md
index f71fcc2bb7..6c621105e5 100644
--- a/website/docs/module_slack.md
+++ b/website/docs/module_slack.md
@@ -20,6 +20,12 @@ Slack application must be installed to company's Slack first.
Please locate `openpype/modules/slack/manifest.yml` file in deployed OpenPype installation and follow instruction at
https://api.slack.com/reference/manifests#using and follow "Creating apps with manifests".
+### App icon
+
+If you would like to enrich bot with an icon, Slack admin must add the icon after app installation.
+
+Go to your Slack app home (something like https://api.slack.com/apps/XXXXXXXX/general?) > Basic information > Display Information.
+You can upload any image you want, or for your convenience locate prepared OpenPype icon in your installed Openpype installation in `openpype\modules\slac\resources`.
## System Settings
@@ -61,16 +67,33 @@ Integration can upload 'thumbnail' file (if present in an instance), for that bo
manually added to target channel by Slack admin!
(In target channel write: ```/invite @OpenPypeNotifier``)
+#### Upload review
+Integration can upload 'review' file (if present in an instance), for that bot must be
+manually added to target channel by Slack admin!
+(In target channel write: ```/invite @OpenPypeNotifier``)
+
+Burnin version (usually .mp4) is preferred if present.
+
+Please be sure that this configuration is viable for your use case. In case of uploading large reviews to Slack,
+all publishes will be slowed down and you might hit a file limit on Slack pretty soon (it is 5GB for Free version of Slack, any file cannot be bigger than 1GB).
+You might try to add `{review_filepath}` to message content. This link might help users to find review easier on their machines.
+(It won't show a playable preview though!)
+
#### Message
Message content can use Templating (see [Available template keys](admin_settings_project_anatomy#available-template-keys)).
Few keys also have Capitalized and UPPERCASE format. Values will be modified accordingly ({Asset} >> "Asset", {FAMILY} >> "RENDER").
-**Available keys:**
-- asset
-- subset
-- task
-- username
-- app
-- family
-- version
+**Additional implemented keys:**
+- review_filepath
+
+##### Message example
+```
+{Subset} was published for {ASSET} in {task[name]} task.
+
+Here you can find review {review_filepath}
+```
+
+#### Message retention
+Currently no purging of old messages is implemented in Openpype. Admins of Slack should set their own retention of messages and files per channel.
+(see https://slack.com/help/articles/203457187-Customize-message-and-file-retention-policies)
diff --git a/website/yarn.lock b/website/yarn.lock
index 89da2289de..098aed5e85 100644
--- a/website/yarn.lock
+++ b/website/yarn.lock
@@ -2250,9 +2250,9 @@ bail@^1.0.0:
integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==
balanced-match@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
- integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+ integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base16@^1.0.0:
version "1.0.0"
@@ -3983,9 +3983,9 @@ flux@^4.0.1:
fbjs "^3.0.0"
follow-redirects@^1.0.0, follow-redirects@^1.14.0:
- version "1.14.4"
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379"
- integrity sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==
+ version "1.14.7"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685"
+ integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==
for-in@^1.0.2:
version "1.0.2"
@@ -4136,9 +4136,9 @@ glob-to-regexp@^0.4.1:
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
glob@^7.0.0, glob@^7.0.3, glob@^7.1.3:
- version "7.1.6"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
- integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
+ integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
@@ -4825,6 +4825,13 @@ is-core-module@^2.2.0:
dependencies:
has "^1.0.3"
+is-core-module@^2.8.0:
+ version "2.8.1"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211"
+ integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==
+ dependencies:
+ has "^1.0.3"
+
is-data-descriptor@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
@@ -6167,7 +6174,7 @@ path-key@^3.0.0, path-key@^3.1.0:
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
-path-parse@^1.0.6:
+path-parse@^1.0.6, path-parse@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
@@ -7208,7 +7215,16 @@ resolve-url@^0.2.1:
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
-resolve@^1.1.6, resolve@^1.14.2, resolve@^1.3.2:
+resolve@^1.1.6:
+ version "1.21.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.21.0.tgz#b51adc97f3472e6a5cf4444d34bc9d6b9037591f"
+ integrity sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==
+ dependencies:
+ is-core-module "^2.8.0"
+ path-parse "^1.0.7"
+ supports-preserve-symlinks-flag "^1.0.0"
+
+resolve@^1.14.2, resolve@^1.3.2:
version "1.20.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
@@ -7533,9 +7549,9 @@ shell-quote@1.7.2:
integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==
shelljs@^0.8.4:
- version "0.8.4"
- resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2"
- integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==
+ version "0.8.5"
+ resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c"
+ integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==
dependencies:
glob "^7.0.0"
interpret "^1.0.0"
@@ -7896,6 +7912,11 @@ supports-color@^7.0.0, supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
+supports-preserve-symlinks-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+ integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
svg-parser@^2.0.2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5"