[Automated] Merged develop into main
|
|
@ -912,7 +912,6 @@ class BootstrapRepos:
|
||||||
processed_path = file
|
processed_path = file
|
||||||
self._print(f"- processing {processed_path}")
|
self._print(f"- processing {processed_path}")
|
||||||
|
|
||||||
|
|
||||||
checksums.append(
|
checksums.append(
|
||||||
(
|
(
|
||||||
sha256sum(file.as_posix()),
|
sha256sum(file.as_posix()),
|
||||||
|
|
@ -1544,7 +1543,8 @@ class BootstrapRepos:
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
zip_item (Path): Zip file to test.
|
zip_item (Path): Zip file to test.
|
||||||
detected_version (OpenPypeVersion): Pype version detected from name.
|
detected_version (OpenPypeVersion): Pype version detected from
|
||||||
|
name.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if it is valid OpenPype version, False otherwise.
|
True if it is valid OpenPype version, False otherwise.
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ class InstallThread(QThread):
|
||||||
# find local version of OpenPype
|
# find local version of OpenPype
|
||||||
bs = BootstrapRepos(
|
bs = BootstrapRepos(
|
||||||
progress_callback=self.set_progress, message=self.message)
|
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.
|
# if user did entered nothing, we install OpenPype from local version.
|
||||||
# zip content of `repos`, copy it to user data dir and append
|
# zip content of `repos`, copy it to user data dir and append
|
||||||
|
|
|
||||||
66
openpype/hosts/aftereffects/api/README.md
Normal file
|
|
@ -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
|
||||||
|
|
@ -1,115 +1,68 @@
|
||||||
import os
|
"""Public API
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from avalon import io
|
Anything that isn't defined here is INTERNAL and unreliable for external use.
|
||||||
from avalon import api as avalon
|
|
||||||
from Qt import QtWidgets
|
"""
|
||||||
from openpype import lib, api
|
|
||||||
import pyblish.api as pyblish
|
from .launch_logic import (
|
||||||
import openpype.hosts.aftereffects
|
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__))
|
"file_extensions",
|
||||||
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
|
"has_unsaved_changes",
|
||||||
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
|
"save_file",
|
||||||
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
|
"open_file",
|
||||||
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
|
"current_file",
|
||||||
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
|
"work_root",
|
||||||
|
|
||||||
|
# lib
|
||||||
|
"maintained_selection",
|
||||||
|
"get_extension_manifest_path",
|
||||||
|
|
||||||
def check_inventory():
|
# plugin
|
||||||
if not lib.any_outdated():
|
"AfterEffectsLoader"
|
||||||
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
|
|
||||||
|
|
|
||||||
BIN
openpype/hosts/aftereffects/api/extension.zxp
Normal file
32
openpype/hosts/aftereffects/api/extension/.debug
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ExtensionList>
|
||||||
|
<Extension Id="com.openpype.AE.panel">
|
||||||
|
<HostList>
|
||||||
|
|
||||||
|
<!-- Comment Host tags according to the apps you want your panel to support -->
|
||||||
|
|
||||||
|
<!-- Photoshop -->
|
||||||
|
<Host Name="PHXS" Port="8088"/>
|
||||||
|
|
||||||
|
<!-- Illustrator -->
|
||||||
|
<Host Name="ILST" Port="8089"/>
|
||||||
|
|
||||||
|
<!-- InDesign -->
|
||||||
|
<Host Name="IDSN" Port="8090" />
|
||||||
|
|
||||||
|
<!-- Premiere -->
|
||||||
|
<Host Name="PPRO" Port="8091" />
|
||||||
|
|
||||||
|
<!-- AfterEffects -->
|
||||||
|
<Host Name="AEFT" Port="8092" />
|
||||||
|
|
||||||
|
<!-- PRELUDE -->
|
||||||
|
<Host Name="PRLD" Port="8093" />
|
||||||
|
|
||||||
|
<!-- FLASH Pro -->
|
||||||
|
<Host Name="FLPR" Port="8094" />
|
||||||
|
|
||||||
|
</HostList>
|
||||||
|
</Extension>
|
||||||
|
</ExtensionList>
|
||||||
|
|
||||||
79
openpype/hosts/aftereffects/api/extension/CSXS/manifest.xml
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ExtensionManifest Version="8.0" ExtensionBundleId="com.openpype.AE.panel" ExtensionBundleVersion="1.0.21"
|
||||||
|
ExtensionBundleName="openpype" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
<ExtensionList>
|
||||||
|
<Extension Id="com.openpype.AE.panel" Version="1.0" />
|
||||||
|
</ExtensionList>
|
||||||
|
<ExecutionEnvironment>
|
||||||
|
<HostList>
|
||||||
|
<!-- Uncomment Host tags according to the apps you want your panel to support -->
|
||||||
|
<!-- Photoshop -->
|
||||||
|
<!--<Host Name="PHXS" Version="[14.0,19.0]" /> -->
|
||||||
|
<!-- <Host Name="PHSP" Version="[14.0,19.0]" /> -->
|
||||||
|
|
||||||
|
<!-- Illustrator -->
|
||||||
|
<!-- <Host Name="ILST" Version="[18.0,22.0]" /> -->
|
||||||
|
|
||||||
|
<!-- InDesign -->
|
||||||
|
<!-- <Host Name="IDSN" Version="[10.0,13.0]" /> -->
|
||||||
|
|
||||||
|
<!-- Premiere -->
|
||||||
|
<!-- <Host Name="PPRO" Version="[8.0,12.0]" /> -->
|
||||||
|
|
||||||
|
<!-- AfterEffects -->
|
||||||
|
<Host Name="AEFT" Version="[13.0,99.0]" />
|
||||||
|
|
||||||
|
<!-- PRELUDE -->
|
||||||
|
<!-- <Host Name="PRLD" Version="[3.0,7.0]" /> -->
|
||||||
|
|
||||||
|
<!-- FLASH Pro -->
|
||||||
|
<!-- <Host Name="FLPR" Version="[14.0,18.0]" /> -->
|
||||||
|
|
||||||
|
</HostList>
|
||||||
|
<LocaleList>
|
||||||
|
<Locale Code="All" />
|
||||||
|
</LocaleList>
|
||||||
|
<RequiredRuntimeList>
|
||||||
|
<RequiredRuntime Name="CSXS" Version="9.0" />
|
||||||
|
</RequiredRuntimeList>
|
||||||
|
</ExecutionEnvironment>
|
||||||
|
<DispatchInfoList>
|
||||||
|
<Extension Id="com.openpype.AE.panel">
|
||||||
|
<DispatchInfo >
|
||||||
|
<Resources>
|
||||||
|
<MainPath>./index.html</MainPath>
|
||||||
|
<ScriptPath>./jsx/hostscript.jsx</ScriptPath>
|
||||||
|
</Resources>
|
||||||
|
<Lifecycle>
|
||||||
|
<AutoVisible>true</AutoVisible>
|
||||||
|
</Lifecycle>
|
||||||
|
<UI>
|
||||||
|
<Type>Panel</Type>
|
||||||
|
<Menu>OpenPype</Menu>
|
||||||
|
<Geometry>
|
||||||
|
<Size>
|
||||||
|
<Height>200</Height>
|
||||||
|
<Width>100</Width>
|
||||||
|
</Size>
|
||||||
|
<!--<MinSize>
|
||||||
|
<Height>550</Height>
|
||||||
|
<Width>400</Width>
|
||||||
|
</MinSize>
|
||||||
|
<MaxSize>
|
||||||
|
<Height>550</Height>
|
||||||
|
<Width>400</Width>
|
||||||
|
</MaxSize>-->
|
||||||
|
|
||||||
|
</Geometry>
|
||||||
|
<Icons>
|
||||||
|
<Icon Type="Normal">./icons/iconNormal.png</Icon>
|
||||||
|
<Icon Type="RollOver">./icons/iconRollover.png</Icon>
|
||||||
|
<Icon Type="Disabled">./icons/iconDisabled.png</Icon>
|
||||||
|
<Icon Type="DarkNormal">./icons/iconDarkNormal.png</Icon>
|
||||||
|
<Icon Type="DarkRollOver">./icons/iconDarkRollover.png</Icon>
|
||||||
|
</Icons>
|
||||||
|
</UI>
|
||||||
|
</DispatchInfo>
|
||||||
|
</Extension>
|
||||||
|
</DispatchInfoList>
|
||||||
|
</ExtensionManifest>
|
||||||
327
openpype/hosts/aftereffects/api/extension/css/boilerplate.css
Normal file
|
|
@ -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, "メイリオ", "MS Pゴシック", sans-serif; color: #222; }
|
||||||
|
/*
|
||||||
|
* Remove text-shadow in selection highlight: h5bp.com/i
|
||||||
|
* These selection declarations have to be separate
|
||||||
|
* Also: hot pink! (or customize the background color to match your design)
|
||||||
|
*/
|
||||||
|
|
||||||
|
::selection { text-shadow: none; background-color: highlight; color: highlighttext; }
|
||||||
|
|
||||||
|
|
||||||
|
/* =============================================================================
|
||||||
|
Links
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
a { color: #00e; }
|
||||||
|
a:visited { color: #551a8b; }
|
||||||
|
a:hover { color: #06e; }
|
||||||
|
a:focus { outline: thin dotted; }
|
||||||
|
|
||||||
|
/* Improve readability when focused and hovered in all browsers: h5bp.com/h */
|
||||||
|
a:hover, a:active { outline: 0; }
|
||||||
|
|
||||||
|
|
||||||
|
/* =============================================================================
|
||||||
|
Typography
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
abbr[title] { border-bottom: 1px dotted; }
|
||||||
|
|
||||||
|
b, strong { font-weight: bold; }
|
||||||
|
|
||||||
|
blockquote { margin: 1em 40px; }
|
||||||
|
|
||||||
|
dfn { font-style: italic; }
|
||||||
|
|
||||||
|
hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; }
|
||||||
|
|
||||||
|
ins { background: #ff9; color: #000; text-decoration: none; }
|
||||||
|
|
||||||
|
mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; }
|
||||||
|
|
||||||
|
/* Redeclare monospace font family: h5bp.com/j */
|
||||||
|
pre, code, kbd, samp { font-family: monospace, serif; _font-family: 'courier new', monospace; font-size: 1em; }
|
||||||
|
|
||||||
|
/* Improve readability of pre-formatted text in all browsers */
|
||||||
|
pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; }
|
||||||
|
|
||||||
|
q { quotes: none; }
|
||||||
|
q:before, q:after { content: ""; content: none; }
|
||||||
|
|
||||||
|
small { font-size: 85%; }
|
||||||
|
|
||||||
|
/* Position subscript and superscript content without affecting line-height: h5bp.com/k */
|
||||||
|
sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
|
||||||
|
sup { top: -0.5em; }
|
||||||
|
sub { bottom: -0.25em; }
|
||||||
|
|
||||||
|
|
||||||
|
/* =============================================================================
|
||||||
|
Lists
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
ul, ol { margin: 1em 0; padding: 0 0 0 40px; }
|
||||||
|
dd { margin: 0 0 0 40px; }
|
||||||
|
nav ul, nav ol { list-style: none; list-style-image: none; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
|
||||||
|
/* =============================================================================
|
||||||
|
Embedded content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 1. Improve image quality when scaled in IE7: h5bp.com/d
|
||||||
|
* 2. Remove the gap between images and borders on image containers: h5bp.com/e
|
||||||
|
*/
|
||||||
|
|
||||||
|
img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Correct overflow not hidden in IE9
|
||||||
|
*/
|
||||||
|
|
||||||
|
svg:not(:root) { overflow: hidden; }
|
||||||
|
|
||||||
|
|
||||||
|
/* =============================================================================
|
||||||
|
Figures
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
figure { margin: 0; }
|
||||||
|
|
||||||
|
|
||||||
|
/* =============================================================================
|
||||||
|
Forms
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
form { margin: 0; }
|
||||||
|
fieldset { border: 0; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
/* Indicate that 'label' will shift focus to the associated form element */
|
||||||
|
label { cursor: pointer; }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 1. Correct color not inheriting in IE6/7/8/9
|
||||||
|
* 2. Correct alignment displayed oddly in IE6/7
|
||||||
|
*/
|
||||||
|
|
||||||
|
legend { border: 0; *margin-left: -7px; padding: 0; }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 1. Correct font-size not inheriting in all browsers
|
||||||
|
* 2. Remove margins in FF3/4 S5 Chrome
|
||||||
|
* 3. Define consistent vertical alignment display in all browsers
|
||||||
|
*/
|
||||||
|
|
||||||
|
button, input, select, textarea { font-size: 100%; margin: 0; vertical-align: baseline; *vertical-align: middle; }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 1. Define line-height as normal to match FF3/4 (set using !important in the UA stylesheet)
|
||||||
|
*/
|
||||||
|
|
||||||
|
button, input { line-height: normal; }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 1. Display hand cursor for clickable form elements
|
||||||
|
* 2. Allow styling of clickable form elements in iOS
|
||||||
|
* 3. Correct inner spacing displayed oddly in IE7 (doesn't effect IE6)
|
||||||
|
*/
|
||||||
|
|
||||||
|
button, input[type="button"], input[type="reset"], input[type="submit"] { cursor: pointer; -webkit-appearance: button; *overflow: visible; }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Consistent box sizing and appearance
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type="checkbox"], input[type="radio"] { box-sizing: border-box; padding: 0; }
|
||||||
|
input[type="search"] { -webkit-appearance: textfield; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; box-sizing: content-box; }
|
||||||
|
input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Remove inner padding and border in FF3/4: h5bp.com/l
|
||||||
|
*/
|
||||||
|
|
||||||
|
button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 1. Remove default vertical scrollbar in IE6/7/8/9
|
||||||
|
* 2. Allow only vertical resizing
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea { overflow: auto; vertical-align: top; resize: vertical; }
|
||||||
|
|
||||||
|
/* Colors for form validity */
|
||||||
|
input:valid, textarea:valid { }
|
||||||
|
input:invalid, textarea:invalid { background-color: #f0dddd; }
|
||||||
|
|
||||||
|
|
||||||
|
/* =============================================================================
|
||||||
|
Tables
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
table { border-collapse: collapse; border-spacing: 0; }
|
||||||
|
td { vertical-align: top; }
|
||||||
|
|
||||||
|
|
||||||
|
/* ==|== primary styles =====================================================
|
||||||
|
Author:
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/* ==|== media queries ======================================================
|
||||||
|
PLACEHOLDER Media Queries for Responsive Design.
|
||||||
|
These override the primary ('mobile first') styles
|
||||||
|
Modify as content requires.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
@media only screen and (min-width: 480px) {
|
||||||
|
/* Style adjustments for viewports 480px and over go here */
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 768px) {
|
||||||
|
/* Style adjustments for viewports 768px and over go here */
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* ==|== non-semantic helper classes ========================================
|
||||||
|
Please define your styles before this section.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/* For image replacement */
|
||||||
|
.ir { display: block; border: 0; text-indent: -999em; overflow: hidden; background-color: transparent; background-repeat: no-repeat; text-align: left; direction: ltr; }
|
||||||
|
.ir br { display: none; }
|
||||||
|
|
||||||
|
/* Hide from both screenreaders and browsers: h5bp.com/u */
|
||||||
|
.hidden { display: none !important; visibility: hidden; }
|
||||||
|
|
||||||
|
/* Hide only visually, but have it available for screenreaders: h5bp.com/v */
|
||||||
|
.visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; }
|
||||||
|
|
||||||
|
/* Extends the .visuallyhidden class to allow the element to be focusable when navigated to via the keyboard: h5bp.com/p */
|
||||||
|
.visuallyhidden.focusable:active, .visuallyhidden.focusable:focus { clip: auto; height: auto; margin: 0; overflow: visible; position: static; width: auto; }
|
||||||
|
|
||||||
|
/* Hide visually and from screenreaders, but maintain layout */
|
||||||
|
.invisible { visibility: hidden; }
|
||||||
|
|
||||||
|
/* Contain floats: h5bp.com/q */
|
||||||
|
.clearfix:before, .clearfix:after { content: ""; display: table; }
|
||||||
|
.clearfix:after { clear: both; }
|
||||||
|
.clearfix { *zoom: 1; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* ==|== print styles =======================================================
|
||||||
|
Print styles.
|
||||||
|
Inlined to avoid required HTTP connection: h5bp.com/r
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
* { background: transparent !important; color: black !important; box-shadow:none !important; text-shadow: none !important; filter:none !important; -ms-filter: none !important; } /* Black prints faster: h5bp.com/s */
|
||||||
|
a, a:visited { text-decoration: underline; }
|
||||||
|
a[href]:after { content: " (" attr(href) ")"; }
|
||||||
|
abbr[title]:after { content: " (" attr(title) ")"; }
|
||||||
|
.ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } /* Don't show links for images, or javascript/internal links */
|
||||||
|
pre, blockquote { border: 1px solid #999; page-break-inside: avoid; }
|
||||||
|
table { display: table-header-group; } /* h5bp.com/t */
|
||||||
|
tr, img { page-break-inside: avoid; }
|
||||||
|
img { max-width: 100% !important; }
|
||||||
|
@page { margin: 0.5cm; }
|
||||||
|
p, h2, h3 { orphans: 3; widows: 3; }
|
||||||
|
h2, h3 { page-break-after: avoid; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* reflow reset for -webkit-margin-before: 1em */
|
||||||
|
p { margin: 0; }
|
||||||
|
|
||||||
|
html {
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: transparent;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #fff;
|
||||||
|
font: normal 100%;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, div, img, p, button, input, select, textarea {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
cursor: default;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=button] {
|
||||||
|
background-color: #e5e9e8;
|
||||||
|
border: 1px solid #9daca9;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: inset 0 1px #fff;
|
||||||
|
font: inherit;
|
||||||
|
letter-spacing: inherit;
|
||||||
|
text-indent: inherit;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=button]:hover {
|
||||||
|
background-color: #eff1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=button]:active {
|
||||||
|
background-color: #d2d6d6;
|
||||||
|
border: 1px solid #9daca9;
|
||||||
|
box-shadow: inset 0 1px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reset anchor styles to an unstyled default to be in parity with design surface. It
|
||||||
|
is presumed that most link styles in real-world designs are custom (non-default). */
|
||||||
|
a, a:visited, a:hover, a:active {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
51
openpype/hosts/aftereffects/api/extension/css/styles.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
1
openpype/hosts/aftereffects/api/extension/css/topcoat-desktop-dark.min.css
vendored
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
BIN
openpype/hosts/aftereffects/api/extension/icons/iconDisabled.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
openpype/hosts/aftereffects/api/extension/icons/iconNormal.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
openpype/hosts/aftereffects/api/extension/icons/iconRollover.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
136
openpype/hosts/aftereffects/api/extension/index.html
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="css/topcoat-desktop-dark.min.css"/>
|
||||||
|
<link id="hostStyle" rel="stylesheet" href="css/styles.css"/>
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
html, body, iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
button {width: 100%;}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
button {width: 100%;}
|
||||||
|
body {margin:0; padding:0; height: 100%;}
|
||||||
|
html {height: 100%;}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<title></title>
|
||||||
|
<script src="js/libs/jquery-2.0.2.min.js"></script>
|
||||||
|
|
||||||
|
<script type=text/javascript>
|
||||||
|
$(function() {
|
||||||
|
$("a#workfiles-button").bind("click", function() {
|
||||||
|
|
||||||
|
RPC.call('AfterEffects.workfiles_route').then(function (data) {
|
||||||
|
}, function (error) {
|
||||||
|
alert(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type=text/javascript>
|
||||||
|
$(function() {
|
||||||
|
$("a#creator-button").bind("click", function() {
|
||||||
|
RPC.call('AfterEffects.creator_route').then(function (data) {
|
||||||
|
}, function (error) {
|
||||||
|
alert(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type=text/javascript>
|
||||||
|
$(function() {
|
||||||
|
$("a#loader-button").bind("click", function() {
|
||||||
|
RPC.call('AfterEffects.loader_route').then(function (data) {
|
||||||
|
}, function (error) {
|
||||||
|
alert(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type=text/javascript>
|
||||||
|
$(function() {
|
||||||
|
$("a#publish-button").bind("click", function() {
|
||||||
|
RPC.call('AfterEffects.publish_route').then(function (data) {
|
||||||
|
}, function (error) {
|
||||||
|
alert(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type=text/javascript>
|
||||||
|
$(function() {
|
||||||
|
$("a#sceneinventory-button").bind("click", function() {
|
||||||
|
RPC.call('AfterEffects.sceneinventory_route').then(function (data) {
|
||||||
|
}, function (error) {
|
||||||
|
alert(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type=text/javascript>
|
||||||
|
$(function() {
|
||||||
|
$("a#subsetmanager-button").bind("click", function() {
|
||||||
|
RPC.call('AfterEffects.subsetmanager_route').then(function (data) {
|
||||||
|
}, function (error) {
|
||||||
|
alert(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type=text/javascript>
|
||||||
|
$(function() {
|
||||||
|
$("a#experimental-button").bind("click", function() {
|
||||||
|
RPC.call('AfterEffects.experimental_tools_route').then(function (data) {
|
||||||
|
}, function (error) {
|
||||||
|
alert(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="hostElt">
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div></div><a href=# id=workfiles-button><button class="hostFontSize">Workfiles...</button></a></div>
|
||||||
|
<div> <a href=# id=creator-button><button class="hostFontSize">Create...</button></a></div>
|
||||||
|
<div><a href=# id=loader-button><button class="hostFontSize">Load...</button></a></div>
|
||||||
|
<div><a href=# id=publish-button><button class="hostFontSize">Publish...</button></a></div>
|
||||||
|
<div><a href=# id=sceneinventory-button><button class="hostFontSize">Manage...</button></a></div>
|
||||||
|
<div><a href=# id=subsetmanager-button><button class="hostFontSize">Subset Manager...</button></a></div>
|
||||||
|
<div><a href=# id=experimental-button><button class="hostFontSize">Experimental Tools...</button></a></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <script src="js/libs/PlayerDebugMode"></script> -->
|
||||||
|
<script src="js/libs/wsrpc.js"></script>
|
||||||
|
<script src="js/libs/loglevel.min.js"></script>
|
||||||
|
<script src="js/libs/CSInterface.js"></script>
|
||||||
|
|
||||||
|
<script src="js/themeManager.js"></script>
|
||||||
|
<script src="js/main.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1193
openpype/hosts/aftereffects/api/extension/js/libs/CSInterface.js
Normal file
6
openpype/hosts/aftereffects/api/extension/js/libs/jquery-2.0.2.min.js
vendored
Normal file
530
openpype/hosts/aftereffects/api/extension/js/libs/json.js
Normal file
|
|
@ -0,0 +1,530 @@
|
||||||
|
// json2.js
|
||||||
|
// 2017-06-12
|
||||||
|
// Public Domain.
|
||||||
|
// NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
|
||||||
|
|
||||||
|
// USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
|
||||||
|
// NOT CONTROL.
|
||||||
|
|
||||||
|
// This file creates a global JSON object containing two methods: stringify
|
||||||
|
// and parse. This file provides the ES5 JSON capability to ES3 systems.
|
||||||
|
// If a project might run on IE8 or earlier, then this file should be included.
|
||||||
|
// This file does nothing on ES5 systems.
|
||||||
|
|
||||||
|
// JSON.stringify(value, replacer, space)
|
||||||
|
// value any JavaScript value, usually an object or array.
|
||||||
|
// replacer an optional parameter that determines how object
|
||||||
|
// values are stringified for objects. It can be a
|
||||||
|
// function or an array of strings.
|
||||||
|
// space an optional parameter that specifies the indentation
|
||||||
|
// of nested structures. If it is omitted, the text will
|
||||||
|
// be packed without extra whitespace. If it is a number,
|
||||||
|
// it will specify the number of spaces to indent at each
|
||||||
|
// level. If it is a string (such as "\t" or " "),
|
||||||
|
// it contains the characters used to indent at each level.
|
||||||
|
// This method produces a JSON text from a JavaScript value.
|
||||||
|
// When an object value is found, if the object contains a toJSON
|
||||||
|
// method, its toJSON method will be called and the result will be
|
||||||
|
// stringified. A toJSON method does not serialize: it returns the
|
||||||
|
// value represented by the name/value pair that should be serialized,
|
||||||
|
// or undefined if nothing should be serialized. The toJSON method
|
||||||
|
// will be passed the key associated with the value, and this will be
|
||||||
|
// bound to the value.
|
||||||
|
|
||||||
|
// For example, this would serialize Dates as ISO strings.
|
||||||
|
|
||||||
|
// Date.prototype.toJSON = function (key) {
|
||||||
|
// function f(n) {
|
||||||
|
// // Format integers to have at least two digits.
|
||||||
|
// return (n < 10)
|
||||||
|
// ? "0" + n
|
||||||
|
// : n;
|
||||||
|
// }
|
||||||
|
// return this.getUTCFullYear() + "-" +
|
||||||
|
// f(this.getUTCMonth() + 1) + "-" +
|
||||||
|
// f(this.getUTCDate()) + "T" +
|
||||||
|
// f(this.getUTCHours()) + ":" +
|
||||||
|
// f(this.getUTCMinutes()) + ":" +
|
||||||
|
// f(this.getUTCSeconds()) + "Z";
|
||||||
|
// };
|
||||||
|
|
||||||
|
// You can provide an optional replacer method. It will be passed the
|
||||||
|
// key and value of each member, with this bound to the containing
|
||||||
|
// object. The value that is returned from your method will be
|
||||||
|
// serialized. If your method returns undefined, then the member will
|
||||||
|
// be excluded from the serialization.
|
||||||
|
|
||||||
|
// If the replacer parameter is an array of strings, then it will be
|
||||||
|
// used to select the members to be serialized. It filters the results
|
||||||
|
// such that only members with keys listed in the replacer array are
|
||||||
|
// stringified.
|
||||||
|
|
||||||
|
// Values that do not have JSON representations, such as undefined or
|
||||||
|
// functions, will not be serialized. Such values in objects will be
|
||||||
|
// dropped; in arrays they will be replaced with null. You can use
|
||||||
|
// a replacer function to replace those with JSON values.
|
||||||
|
|
||||||
|
// JSON.stringify(undefined) returns undefined.
|
||||||
|
|
||||||
|
// The optional space parameter produces a stringification of the
|
||||||
|
// value that is filled with line breaks and indentation to make it
|
||||||
|
// easier to read.
|
||||||
|
|
||||||
|
// If the space parameter is a non-empty string, then that string will
|
||||||
|
// be used for indentation. If the space parameter is a number, then
|
||||||
|
// the indentation will be that many spaces.
|
||||||
|
|
||||||
|
// Example:
|
||||||
|
|
||||||
|
// text = JSON.stringify(["e", {pluribus: "unum"}]);
|
||||||
|
// // text is '["e",{"pluribus":"unum"}]'
|
||||||
|
|
||||||
|
// text = JSON.stringify(["e", {pluribus: "unum"}], null, "\t");
|
||||||
|
// // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
|
||||||
|
|
||||||
|
// text = JSON.stringify([new Date()], function (key, value) {
|
||||||
|
// return this[key] instanceof Date
|
||||||
|
// ? "Date(" + this[key] + ")"
|
||||||
|
// : value;
|
||||||
|
// });
|
||||||
|
// // text is '["Date(---current time---)"]'
|
||||||
|
|
||||||
|
// JSON.parse(text, reviver)
|
||||||
|
// This method parses a JSON text to produce an object or array.
|
||||||
|
// It can throw a SyntaxError exception.
|
||||||
|
|
||||||
|
// The optional reviver parameter is a function that can filter and
|
||||||
|
// transform the results. It receives each of the keys and values,
|
||||||
|
// and its return value is used instead of the original value.
|
||||||
|
// If it returns what it received, then the structure is not modified.
|
||||||
|
// If it returns undefined then the member is deleted.
|
||||||
|
|
||||||
|
// Example:
|
||||||
|
|
||||||
|
// // Parse the text. Values that look like ISO date strings will
|
||||||
|
// // be converted to Date objects.
|
||||||
|
|
||||||
|
// myData = JSON.parse(text, function (key, value) {
|
||||||
|
// var a;
|
||||||
|
// if (typeof value === "string") {
|
||||||
|
// a =
|
||||||
|
// /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
|
||||||
|
// if (a) {
|
||||||
|
// return new Date(Date.UTC(
|
||||||
|
// +a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6]
|
||||||
|
// ));
|
||||||
|
// }
|
||||||
|
// return value;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// myData = JSON.parse(
|
||||||
|
// "[\"Date(09/09/2001)\"]",
|
||||||
|
// function (key, value) {
|
||||||
|
// var d;
|
||||||
|
// if (
|
||||||
|
// typeof value === "string"
|
||||||
|
// && value.slice(0, 5) === "Date("
|
||||||
|
// && value.slice(-1) === ")"
|
||||||
|
// ) {
|
||||||
|
// d = new Date(value.slice(5, -1));
|
||||||
|
// if (d) {
|
||||||
|
// return d;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return value;
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
|
||||||
|
// This is a reference implementation. You are free to copy, modify, or
|
||||||
|
// redistribute.
|
||||||
|
|
||||||
|
/*jslint
|
||||||
|
eval, for, this
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*property
|
||||||
|
JSON, apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
|
||||||
|
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
|
||||||
|
lastIndex, length, parse, prototype, push, replace, slice, stringify,
|
||||||
|
test, toJSON, toString, valueOf
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// Create a JSON object only if one does not already exist. We create the
|
||||||
|
// methods in a closure to avoid creating global variables.
|
||||||
|
|
||||||
|
if (typeof JSON !== "object") {
|
||||||
|
JSON = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var rx_one = /^[\],:{}\s]*$/;
|
||||||
|
var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
|
||||||
|
var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
|
||||||
|
var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
|
||||||
|
var rx_escapable = /[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
|
||||||
|
var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
|
||||||
|
|
||||||
|
function f(n) {
|
||||||
|
// Format integers to have at least two digits.
|
||||||
|
return (n < 10)
|
||||||
|
? "0" + n
|
||||||
|
: n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function this_value() {
|
||||||
|
return this.valueOf();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof Date.prototype.toJSON !== "function") {
|
||||||
|
|
||||||
|
Date.prototype.toJSON = function () {
|
||||||
|
|
||||||
|
return isFinite(this.valueOf())
|
||||||
|
? (
|
||||||
|
this.getUTCFullYear()
|
||||||
|
+ "-"
|
||||||
|
+ f(this.getUTCMonth() + 1)
|
||||||
|
+ "-"
|
||||||
|
+ f(this.getUTCDate())
|
||||||
|
+ "T"
|
||||||
|
+ f(this.getUTCHours())
|
||||||
|
+ ":"
|
||||||
|
+ f(this.getUTCMinutes())
|
||||||
|
+ ":"
|
||||||
|
+ f(this.getUTCSeconds())
|
||||||
|
+ "Z"
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
Boolean.prototype.toJSON = this_value;
|
||||||
|
Number.prototype.toJSON = this_value;
|
||||||
|
String.prototype.toJSON = this_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var gap;
|
||||||
|
var indent;
|
||||||
|
var meta;
|
||||||
|
var rep;
|
||||||
|
|
||||||
|
|
||||||
|
function quote(string) {
|
||||||
|
|
||||||
|
// If the string contains no control characters, no quote characters, and no
|
||||||
|
// backslash characters, then we can safely slap some quotes around it.
|
||||||
|
// Otherwise we must also replace the offending characters with safe escape
|
||||||
|
// sequences.
|
||||||
|
|
||||||
|
rx_escapable.lastIndex = 0;
|
||||||
|
return rx_escapable.test(string)
|
||||||
|
? "\"" + string.replace(rx_escapable, function (a) {
|
||||||
|
var c = meta[a];
|
||||||
|
return typeof c === "string"
|
||||||
|
? c
|
||||||
|
: "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4);
|
||||||
|
}) + "\""
|
||||||
|
: "\"" + string + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function str(key, holder) {
|
||||||
|
|
||||||
|
// Produce a string from holder[key].
|
||||||
|
|
||||||
|
var i; // The loop counter.
|
||||||
|
var k; // The member key.
|
||||||
|
var v; // The member value.
|
||||||
|
var length;
|
||||||
|
var mind = gap;
|
||||||
|
var partial;
|
||||||
|
var value = holder[key];
|
||||||
|
|
||||||
|
// If the value has a toJSON method, call it to obtain a replacement value.
|
||||||
|
|
||||||
|
if (
|
||||||
|
value
|
||||||
|
&& typeof value === "object"
|
||||||
|
&& typeof value.toJSON === "function"
|
||||||
|
) {
|
||||||
|
value = value.toJSON(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we were called with a replacer function, then call the replacer to
|
||||||
|
// obtain a replacement value.
|
||||||
|
|
||||||
|
if (typeof rep === "function") {
|
||||||
|
value = rep.call(holder, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// What happens next depends on the value's type.
|
||||||
|
|
||||||
|
switch (typeof value) {
|
||||||
|
case "string":
|
||||||
|
return quote(value);
|
||||||
|
|
||||||
|
case "number":
|
||||||
|
|
||||||
|
// JSON numbers must be finite. Encode non-finite numbers as null.
|
||||||
|
|
||||||
|
return (isFinite(value))
|
||||||
|
? String(value)
|
||||||
|
: "null";
|
||||||
|
|
||||||
|
case "boolean":
|
||||||
|
case "null":
|
||||||
|
|
||||||
|
// If the value is a boolean or null, convert it to a string. Note:
|
||||||
|
// typeof null does not produce "null". The case is included here in
|
||||||
|
// the remote chance that this gets fixed someday.
|
||||||
|
|
||||||
|
return String(value);
|
||||||
|
|
||||||
|
// If the type is "object", we might be dealing with an object or an array or
|
||||||
|
// null.
|
||||||
|
|
||||||
|
case "object":
|
||||||
|
|
||||||
|
// Due to a specification blunder in ECMAScript, typeof null is "object",
|
||||||
|
// so watch out for that case.
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make an array to hold the partial results of stringifying this object value.
|
||||||
|
|
||||||
|
gap += indent;
|
||||||
|
partial = [];
|
||||||
|
|
||||||
|
// Is the value an array?
|
||||||
|
|
||||||
|
if (Object.prototype.toString.apply(value) === "[object Array]") {
|
||||||
|
|
||||||
|
// The value is an array. Stringify every element. Use null as a placeholder
|
||||||
|
// for non-JSON values.
|
||||||
|
|
||||||
|
length = value.length;
|
||||||
|
for (i = 0; i < length; i += 1) {
|
||||||
|
partial[i] = str(i, value) || "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join all of the elements together, separated with commas, and wrap them in
|
||||||
|
// brackets.
|
||||||
|
|
||||||
|
v = partial.length === 0
|
||||||
|
? "[]"
|
||||||
|
: gap
|
||||||
|
? (
|
||||||
|
"[\n"
|
||||||
|
+ gap
|
||||||
|
+ partial.join(",\n" + gap)
|
||||||
|
+ "\n"
|
||||||
|
+ mind
|
||||||
|
+ "]"
|
||||||
|
)
|
||||||
|
: "[" + partial.join(",") + "]";
|
||||||
|
gap = mind;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the replacer is an array, use it to select the members to be stringified.
|
||||||
|
|
||||||
|
if (rep && typeof rep === "object") {
|
||||||
|
length = rep.length;
|
||||||
|
for (i = 0; i < length; i += 1) {
|
||||||
|
if (typeof rep[i] === "string") {
|
||||||
|
k = rep[i];
|
||||||
|
v = str(k, value);
|
||||||
|
if (v) {
|
||||||
|
partial.push(quote(k) + (
|
||||||
|
(gap)
|
||||||
|
? ": "
|
||||||
|
: ":"
|
||||||
|
) + v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Otherwise, iterate through all of the keys in the object.
|
||||||
|
|
||||||
|
for (k in value) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
||||||
|
v = str(k, value);
|
||||||
|
if (v) {
|
||||||
|
partial.push(quote(k) + (
|
||||||
|
(gap)
|
||||||
|
? ": "
|
||||||
|
: ":"
|
||||||
|
) + v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join all of the member texts together, separated with commas,
|
||||||
|
// and wrap them in braces.
|
||||||
|
|
||||||
|
v = partial.length === 0
|
||||||
|
? "{}"
|
||||||
|
: gap
|
||||||
|
? "{\n" + gap + partial.join(",\n" + gap) + "\n" + mind + "}"
|
||||||
|
: "{" + partial.join(",") + "}";
|
||||||
|
gap = mind;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the JSON object does not yet have a stringify method, give it one.
|
||||||
|
|
||||||
|
if (typeof JSON.stringify !== "function") {
|
||||||
|
meta = { // table of character substitutions
|
||||||
|
"\b": "\\b",
|
||||||
|
"\t": "\\t",
|
||||||
|
"\n": "\\n",
|
||||||
|
"\f": "\\f",
|
||||||
|
"\r": "\\r",
|
||||||
|
"\"": "\\\"",
|
||||||
|
"\\": "\\\\"
|
||||||
|
};
|
||||||
|
JSON.stringify = function (value, replacer, space) {
|
||||||
|
|
||||||
|
// The stringify method takes a value and an optional replacer, and an optional
|
||||||
|
// space parameter, and returns a JSON text. The replacer can be a function
|
||||||
|
// that can replace values, or an array of strings that will select the keys.
|
||||||
|
// A default replacer method can be provided. Use of the space parameter can
|
||||||
|
// produce text that is more easily readable.
|
||||||
|
|
||||||
|
var i;
|
||||||
|
gap = "";
|
||||||
|
indent = "";
|
||||||
|
|
||||||
|
// If the space parameter is a number, make an indent string containing that
|
||||||
|
// many spaces.
|
||||||
|
|
||||||
|
if (typeof space === "number") {
|
||||||
|
for (i = 0; i < space; i += 1) {
|
||||||
|
indent += " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the space parameter is a string, it will be used as the indent string.
|
||||||
|
|
||||||
|
} else if (typeof space === "string") {
|
||||||
|
indent = space;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is a replacer, it must be a function or an array.
|
||||||
|
// Otherwise, throw an error.
|
||||||
|
|
||||||
|
rep = replacer;
|
||||||
|
if (replacer && typeof replacer !== "function" && (
|
||||||
|
typeof replacer !== "object"
|
||||||
|
|| typeof replacer.length !== "number"
|
||||||
|
)) {
|
||||||
|
throw new Error("JSON.stringify");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a fake root object containing our value under the key of "".
|
||||||
|
// Return the result of stringifying the value.
|
||||||
|
|
||||||
|
return str("", {"": value});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// If the JSON object does not yet have a parse method, give it one.
|
||||||
|
|
||||||
|
if (typeof JSON.parse !== "function") {
|
||||||
|
JSON.parse = function (text, reviver) {
|
||||||
|
|
||||||
|
// The parse method takes a text and an optional reviver function, and returns
|
||||||
|
// a JavaScript value if the text is a valid JSON text.
|
||||||
|
|
||||||
|
var j;
|
||||||
|
|
||||||
|
function walk(holder, key) {
|
||||||
|
|
||||||
|
// The walk method is used to recursively walk the resulting structure so
|
||||||
|
// that modifications can be made.
|
||||||
|
|
||||||
|
var k;
|
||||||
|
var v;
|
||||||
|
var value = holder[key];
|
||||||
|
if (value && typeof value === "object") {
|
||||||
|
for (k in value) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
||||||
|
v = walk(value, k);
|
||||||
|
if (v !== undefined) {
|
||||||
|
value[k] = v;
|
||||||
|
} else {
|
||||||
|
delete value[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reviver.call(holder, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Parsing happens in four stages. In the first stage, we replace certain
|
||||||
|
// Unicode characters with escape sequences. JavaScript handles many characters
|
||||||
|
// incorrectly, either silently deleting them, or treating them as line endings.
|
||||||
|
|
||||||
|
text = String(text);
|
||||||
|
rx_dangerous.lastIndex = 0;
|
||||||
|
if (rx_dangerous.test(text)) {
|
||||||
|
text = text.replace(rx_dangerous, function (a) {
|
||||||
|
return (
|
||||||
|
"\\u"
|
||||||
|
+ ("0000" + a.charCodeAt(0).toString(16)).slice(-4)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the second stage, we run the text against regular expressions that look
|
||||||
|
// for non-JSON patterns. We are especially concerned with "()" and "new"
|
||||||
|
// because they can cause invocation, and "=" because it can cause mutation.
|
||||||
|
// But just to be safe, we want to reject all unexpected forms.
|
||||||
|
|
||||||
|
// We split the second stage into 4 regexp operations in order to work around
|
||||||
|
// crippling inefficiencies in IE's and Safari's regexp engines. First we
|
||||||
|
// replace the JSON backslash pairs with "@" (a non-JSON character). Second, we
|
||||||
|
// replace all simple value tokens with "]" characters. Third, we delete all
|
||||||
|
// open brackets that follow a colon or comma or that begin the text. Finally,
|
||||||
|
// we look to see that the remaining characters are only whitespace or "]" or
|
||||||
|
// "," or ":" or "{" or "}". If that is so, then the text is safe for eval.
|
||||||
|
|
||||||
|
if (
|
||||||
|
rx_one.test(
|
||||||
|
text
|
||||||
|
.replace(rx_two, "@")
|
||||||
|
.replace(rx_three, "]")
|
||||||
|
.replace(rx_four, "")
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
|
||||||
|
// In the third stage we use the eval function to compile the text into a
|
||||||
|
// JavaScript structure. The "{" operator is subject to a syntactic ambiguity
|
||||||
|
// in JavaScript: it can begin a block or an object literal. We wrap the text
|
||||||
|
// in parens to eliminate the ambiguity.
|
||||||
|
|
||||||
|
j = eval("(" + text + ")");
|
||||||
|
|
||||||
|
// In the optional fourth stage, we recursively walk the new structure, passing
|
||||||
|
// each name/value pair to a reviver function for possible transformation.
|
||||||
|
|
||||||
|
return (typeof reviver === "function")
|
||||||
|
? walk({"": j}, "")
|
||||||
|
: j;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the text is not JSON parseable, then a SyntaxError is thrown.
|
||||||
|
|
||||||
|
throw new SyntaxError("JSON.parse");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}());
|
||||||
2
openpype/hosts/aftereffects/api/extension/js/libs/loglevel.min.js
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
/*! loglevel - v1.6.8 - https://github.com/pimterry/loglevel - (c) 2020 Tim Perry - licensed MIT */
|
||||||
|
!function(a,b){"use strict";"function"==typeof define&&define.amd?define(b):"object"==typeof module&&module.exports?module.exports=b():a.log=b()}(this,function(){"use strict";function a(a,b){var c=a[b];if("function"==typeof c.bind)return c.bind(a);try{return Function.prototype.bind.call(c,a)}catch(b){return function(){return Function.prototype.apply.apply(c,[a,arguments])}}}function b(){console.log&&(console.log.apply?console.log.apply(console,arguments):Function.prototype.apply.apply(console.log,[console,arguments])),console.trace&&console.trace()}function c(c){return"debug"===c&&(c="log"),typeof console!==i&&("trace"===c&&j?b:void 0!==console[c]?a(console,c):void 0!==console.log?a(console,"log"):h)}function d(a,b){for(var c=0;c<k.length;c++){var d=k[c];this[d]=c<a?h:this.methodFactory(d,a,b)}this.log=this.debug}function e(a,b,c){return function(){typeof console!==i&&(d.call(this,b,c),this[a].apply(this,arguments))}}function f(a,b,d){return c(a)||e.apply(this,arguments)}function g(a,b,c){function e(a){var b=(k[a]||"silent").toUpperCase();if(typeof window!==i){try{return void(window.localStorage[l]=b)}catch(a){}try{window.document.cookie=encodeURIComponent(l)+"="+b+";"}catch(a){}}}function g(){var a;if(typeof window!==i){try{a=window.localStorage[l]}catch(a){}if(typeof a===i)try{var b=window.document.cookie,c=b.indexOf(encodeURIComponent(l)+"=");-1!==c&&(a=/^([^;]+)/.exec(b.slice(c))[1])}catch(a){}return void 0===j.levels[a]&&(a=void 0),a}}var h,j=this,l="loglevel";a&&(l+=":"+a),j.name=a,j.levels={TRACE:0,DEBUG:1,INFO:2,WARN:3,ERROR:4,SILENT:5},j.methodFactory=c||f,j.getLevel=function(){return h},j.setLevel=function(b,c){if("string"==typeof b&&void 0!==j.levels[b.toUpperCase()]&&(b=j.levels[b.toUpperCase()]),!("number"==typeof b&&b>=0&&b<=j.levels.SILENT))throw"log.setLevel() called with invalid level: "+b;if(h=b,!1!==c&&e(b),d.call(j,b,a),typeof console===i&&b<j.levels.SILENT)return"No console available for logging"},j.setDefaultLevel=function(a){g()||j.setLevel(a,!1)},j.enableAll=function(a){j.setLevel(j.levels.TRACE,a)},j.disableAll=function(a){j.setLevel(j.levels.SILENT,a)};var m=g();null==m&&(m=null==b?"WARN":b),j.setLevel(m,!1)}var h=function(){},i="undefined",j=typeof window!==i&&typeof window.navigator!==i&&/Trident\/|MSIE /.test(window.navigator.userAgent),k=["trace","debug","info","warn","error"],l=new g,m={};l.getLogger=function(a){if("string"!=typeof a||""===a)throw new TypeError("You must supply a name when creating a logger.");var b=m[a];return b||(b=m[a]=new g(a,l.getLevel(),l.methodFactory)),b};var n=typeof window!==i?window.log:void 0;return l.noConflict=function(){return typeof window!==i&&window.log===l&&(window.log=n),l},l.getLoggers=function(){return m},l});
|
||||||
393
openpype/hosts/aftereffects/api/extension/js/libs/wsrpc.js
Normal file
|
|
@ -0,0 +1,393 @@
|
||||||
|
(function (global, factory) {
|
||||||
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||||
|
typeof define === 'function' && define.amd ? define(factory) :
|
||||||
|
(global = global || self, global.WSRPC = factory());
|
||||||
|
}(this, function () { 'use strict';
|
||||||
|
|
||||||
|
function _classCallCheck(instance, Constructor) {
|
||||||
|
if (!(instance instanceof Constructor)) {
|
||||||
|
throw new TypeError("Cannot call a class as a function");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Deferred = function Deferred() {
|
||||||
|
_classCallCheck(this, Deferred);
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
self.resolve = null;
|
||||||
|
self.reject = null;
|
||||||
|
self.done = false;
|
||||||
|
|
||||||
|
function wrapper(func) {
|
||||||
|
return function () {
|
||||||
|
if (self.done) throw new Error('Promise already done');
|
||||||
|
self.done = true;
|
||||||
|
return func.apply(this, arguments);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
self.promise = new Promise(function (resolve, reject) {
|
||||||
|
self.resolve = wrapper(resolve);
|
||||||
|
self.reject = wrapper(reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.promise.isPending = function () {
|
||||||
|
return !self.done;
|
||||||
|
};
|
||||||
|
|
||||||
|
return self;
|
||||||
|
};
|
||||||
|
|
||||||
|
function logGroup(group, level, args) {
|
||||||
|
console.group(group);
|
||||||
|
console[level].apply(this, args);
|
||||||
|
console.groupEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
function log() {
|
||||||
|
if (!WSRPC.DEBUG) return;
|
||||||
|
logGroup('WSRPC.DEBUG', 'trace', arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
function trace(msg) {
|
||||||
|
if (!WSRPC.TRACE) return;
|
||||||
|
var payload = msg;
|
||||||
|
if ('data' in msg) payload = JSON.parse(msg.data);
|
||||||
|
logGroup("WSRPC.TRACE", 'trace', [payload]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAbsoluteWsUrl(url) {
|
||||||
|
if (/^\w+:\/\//.test(url)) return url;
|
||||||
|
if (typeof window == 'undefined' && window.location.host.length < 1) throw new Error("Can not construct absolute URL from ".concat(window.location));
|
||||||
|
var scheme = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
var port = window.location.port === '' ? ":".concat(window.location.port) : '';
|
||||||
|
var host = window.location.host;
|
||||||
|
var path = url.replace(/^\/+/gm, '');
|
||||||
|
return "".concat(scheme, "//").concat(host).concat(port, "/").concat(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
var readyState = Object.freeze({
|
||||||
|
0: 'CONNECTING',
|
||||||
|
1: 'OPEN',
|
||||||
|
2: 'CLOSING',
|
||||||
|
3: 'CLOSED'
|
||||||
|
});
|
||||||
|
|
||||||
|
var WSRPC = function WSRPC(URL) {
|
||||||
|
var reconnectTimeout = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1000;
|
||||||
|
|
||||||
|
_classCallCheck(this, WSRPC);
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
URL = getAbsoluteWsUrl(URL);
|
||||||
|
self.id = 1;
|
||||||
|
self.eventId = 0;
|
||||||
|
self.socketStarted = false;
|
||||||
|
self.eventStore = {
|
||||||
|
onconnect: {},
|
||||||
|
onerror: {},
|
||||||
|
onclose: {},
|
||||||
|
onchange: {}
|
||||||
|
};
|
||||||
|
self.connectionNumber = 0;
|
||||||
|
self.oneTimeEventStore = {
|
||||||
|
onconnect: [],
|
||||||
|
onerror: [],
|
||||||
|
onclose: [],
|
||||||
|
onchange: []
|
||||||
|
};
|
||||||
|
self.callQueue = [];
|
||||||
|
|
||||||
|
function createSocket() {
|
||||||
|
var ws = new WebSocket(URL);
|
||||||
|
|
||||||
|
var rejectQueue = function rejectQueue() {
|
||||||
|
self.connectionNumber++; // rejects incoming calls
|
||||||
|
|
||||||
|
var deferred; //reject all pending calls
|
||||||
|
|
||||||
|
while (0 < self.callQueue.length) {
|
||||||
|
var callObj = self.callQueue.shift();
|
||||||
|
deferred = self.store[callObj.id];
|
||||||
|
delete self.store[callObj.id];
|
||||||
|
|
||||||
|
if (deferred && deferred.promise.isPending()) {
|
||||||
|
deferred.reject('WebSocket error occurred');
|
||||||
|
}
|
||||||
|
} // reject all from the store
|
||||||
|
|
||||||
|
|
||||||
|
for (var key in self.store) {
|
||||||
|
if (!self.store.hasOwnProperty(key)) continue;
|
||||||
|
deferred = self.store[key];
|
||||||
|
|
||||||
|
if (deferred && deferred.promise.isPending()) {
|
||||||
|
deferred.reject('WebSocket error occurred');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function reconnect(callEvents) {
|
||||||
|
setTimeout(function () {
|
||||||
|
try {
|
||||||
|
self.socket = createSocket();
|
||||||
|
self.id = 1;
|
||||||
|
} catch (exc) {
|
||||||
|
callEvents('onerror', exc);
|
||||||
|
delete self.socket;
|
||||||
|
console.error(exc);
|
||||||
|
}
|
||||||
|
}, reconnectTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onclose = function (err) {
|
||||||
|
log('ONCLOSE CALLED', 'STATE', self.public.state());
|
||||||
|
trace(err);
|
||||||
|
|
||||||
|
for (var serial in self.store) {
|
||||||
|
if (!self.store.hasOwnProperty(serial)) continue;
|
||||||
|
|
||||||
|
if (self.store[serial].hasOwnProperty('reject')) {
|
||||||
|
self.store[serial].reject('Connection closed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rejectQueue();
|
||||||
|
callEvents('onclose', err);
|
||||||
|
callEvents('onchange', err);
|
||||||
|
reconnect(callEvents);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = function (err) {
|
||||||
|
log('ONERROR CALLED', 'STATE', self.public.state());
|
||||||
|
trace(err);
|
||||||
|
rejectQueue();
|
||||||
|
callEvents('onerror', err);
|
||||||
|
callEvents('onchange', err);
|
||||||
|
log('WebSocket has been closed by error: ', err);
|
||||||
|
};
|
||||||
|
|
||||||
|
function tryCallEvent(func, event) {
|
||||||
|
try {
|
||||||
|
return func(event);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.hasOwnProperty('stack')) {
|
||||||
|
log(e.stack);
|
||||||
|
} else {
|
||||||
|
log('Event function', func, 'raised unknown error:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function callEvents(evName, event) {
|
||||||
|
while (0 < self.oneTimeEventStore[evName].length) {
|
||||||
|
var deferred = self.oneTimeEventStore[evName].shift();
|
||||||
|
if (deferred.hasOwnProperty('resolve') && deferred.promise.isPending()) deferred.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i in self.eventStore[evName]) {
|
||||||
|
if (!self.eventStore[evName].hasOwnProperty(i)) continue;
|
||||||
|
var cur = self.eventStore[evName][i];
|
||||||
|
tryCallEvent(cur, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onopen = function (ev) {
|
||||||
|
log('ONOPEN CALLED', 'STATE', self.public.state());
|
||||||
|
trace(ev);
|
||||||
|
|
||||||
|
while (0 < self.callQueue.length) {
|
||||||
|
// noinspection JSUnresolvedFunction
|
||||||
|
self.socket.send(JSON.stringify(self.callQueue.shift(), 0, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
callEvents('onconnect', ev);
|
||||||
|
callEvents('onchange', ev);
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleCall(self, data) {
|
||||||
|
if (!self.routes.hasOwnProperty(data.method)) throw new Error('Route not found');
|
||||||
|
var connectionNumber = self.connectionNumber;
|
||||||
|
var deferred = new Deferred();
|
||||||
|
deferred.promise.then(function (result) {
|
||||||
|
if (connectionNumber !== self.connectionNumber) return;
|
||||||
|
self.socket.send(JSON.stringify({
|
||||||
|
id: data.id,
|
||||||
|
result: result
|
||||||
|
}));
|
||||||
|
}, function (error) {
|
||||||
|
if (connectionNumber !== self.connectionNumber) return;
|
||||||
|
self.socket.send(JSON.stringify({
|
||||||
|
id: data.id,
|
||||||
|
error: error
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
var func = self.routes[data.method];
|
||||||
|
if (self.asyncRoutes[data.method]) return func.apply(deferred, [data.params]);
|
||||||
|
|
||||||
|
function badPromise() {
|
||||||
|
throw new Error("You should register route with async flag.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var promiseMock = {
|
||||||
|
resolve: badPromise,
|
||||||
|
reject: badPromise
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
deferred.resolve(func.apply(promiseMock, [data.params]));
|
||||||
|
} catch (e) {
|
||||||
|
deferred.reject(e);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleError(self, data) {
|
||||||
|
if (!self.store.hasOwnProperty(data.id)) return log('Unknown callback');
|
||||||
|
var deferred = self.store[data.id];
|
||||||
|
if (typeof deferred === 'undefined') return log('Confirmation without handler');
|
||||||
|
delete self.store[data.id];
|
||||||
|
log('REJECTING', data.error);
|
||||||
|
deferred.reject(data.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResult(self, data) {
|
||||||
|
var deferred = self.store[data.id];
|
||||||
|
if (typeof deferred === 'undefined') return log('Confirmation without handler');
|
||||||
|
delete self.store[data.id];
|
||||||
|
|
||||||
|
if (data.hasOwnProperty('result')) {
|
||||||
|
return deferred.resolve(data.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return deferred.reject(data.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.onmessage = function (message) {
|
||||||
|
log('ONMESSAGE CALLED', 'STATE', self.public.state());
|
||||||
|
trace(message);
|
||||||
|
if (message.type !== 'message') return;
|
||||||
|
var data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = JSON.parse(message.data);
|
||||||
|
log(data);
|
||||||
|
|
||||||
|
if (data.hasOwnProperty('method')) {
|
||||||
|
return handleCall(self, data);
|
||||||
|
} else if (data.hasOwnProperty('error') && data.error === null) {
|
||||||
|
return handleError(self, data);
|
||||||
|
} else {
|
||||||
|
return handleResult(self, data);
|
||||||
|
}
|
||||||
|
} catch (exception) {
|
||||||
|
var err = {
|
||||||
|
error: exception.message,
|
||||||
|
result: null,
|
||||||
|
id: data ? data.id : null
|
||||||
|
};
|
||||||
|
self.socket.send(JSON.stringify(err));
|
||||||
|
console.error(exception);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return ws;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeCall(func, args, params) {
|
||||||
|
self.id += 2;
|
||||||
|
var deferred = new Deferred();
|
||||||
|
var callObj = Object.freeze({
|
||||||
|
id: self.id,
|
||||||
|
method: func,
|
||||||
|
params: args
|
||||||
|
});
|
||||||
|
var state = self.public.state();
|
||||||
|
|
||||||
|
if (state === 'OPEN') {
|
||||||
|
self.store[self.id] = deferred;
|
||||||
|
self.socket.send(JSON.stringify(callObj));
|
||||||
|
} else if (state === 'CONNECTING') {
|
||||||
|
log('SOCKET IS', state);
|
||||||
|
self.store[self.id] = deferred;
|
||||||
|
self.callQueue.push(callObj);
|
||||||
|
} else {
|
||||||
|
log('SOCKET IS', state);
|
||||||
|
|
||||||
|
if (params && params['noWait']) {
|
||||||
|
deferred.reject("Socket is: ".concat(state));
|
||||||
|
} else {
|
||||||
|
self.store[self.id] = deferred;
|
||||||
|
self.callQueue.push(callObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.asyncRoutes = {};
|
||||||
|
self.routes = {};
|
||||||
|
self.store = {};
|
||||||
|
self.public = Object.freeze({
|
||||||
|
call: function call(func, args, params) {
|
||||||
|
return makeCall(func, args, params);
|
||||||
|
},
|
||||||
|
addRoute: function addRoute(route, callback, isAsync) {
|
||||||
|
self.asyncRoutes[route] = isAsync || false;
|
||||||
|
self.routes[route] = callback;
|
||||||
|
},
|
||||||
|
deleteRoute: function deleteRoute(route) {
|
||||||
|
delete self.asyncRoutes[route];
|
||||||
|
return delete self.routes[route];
|
||||||
|
},
|
||||||
|
addEventListener: function addEventListener(event, func) {
|
||||||
|
var eventId = self.eventId++;
|
||||||
|
self.eventStore[event][eventId] = func;
|
||||||
|
return eventId;
|
||||||
|
},
|
||||||
|
removeEventListener: function removeEventListener(event, index) {
|
||||||
|
if (self.eventStore[event].hasOwnProperty(index)) {
|
||||||
|
delete self.eventStore[event][index];
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onEvent: function onEvent(event) {
|
||||||
|
var deferred = new Deferred();
|
||||||
|
self.oneTimeEventStore[event].push(deferred);
|
||||||
|
return deferred.promise;
|
||||||
|
},
|
||||||
|
destroy: function destroy() {
|
||||||
|
return self.socket.close();
|
||||||
|
},
|
||||||
|
state: function state() {
|
||||||
|
return readyState[this.stateCode()];
|
||||||
|
},
|
||||||
|
stateCode: function stateCode() {
|
||||||
|
if (self.socketStarted && self.socket) return self.socket.readyState;
|
||||||
|
return 3;
|
||||||
|
},
|
||||||
|
connect: function connect() {
|
||||||
|
self.socketStarted = true;
|
||||||
|
self.socket = createSocket();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.public.addRoute('log', function (argsObj) {
|
||||||
|
//console.info("Websocket sent: ".concat(argsObj));
|
||||||
|
});
|
||||||
|
self.public.addRoute('ping', function (data) {
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
return self.public;
|
||||||
|
};
|
||||||
|
|
||||||
|
WSRPC.DEBUG = false;
|
||||||
|
WSRPC.TRACE = false;
|
||||||
|
|
||||||
|
return WSRPC;
|
||||||
|
|
||||||
|
}));
|
||||||
|
//# sourceMappingURL=wsrpc.js.map
|
||||||
1
openpype/hosts/aftereffects/api/extension/js/libs/wsrpc.min.js
vendored
Normal file
347
openpype/hosts/aftereffects/api/extension/js/main.js
Normal file
|
|
@ -0,0 +1,347 @@
|
||||||
|
/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true,
|
||||||
|
indent: 4, maxerr: 50 */
|
||||||
|
/*global $, window, location, CSInterface, SystemPath, themeManager*/
|
||||||
|
|
||||||
|
|
||||||
|
var csInterface = new CSInterface();
|
||||||
|
|
||||||
|
log.warn("script start");
|
||||||
|
|
||||||
|
WSRPC.DEBUG = false;
|
||||||
|
WSRPC.TRACE = false;
|
||||||
|
|
||||||
|
// get websocket server url from environment value
|
||||||
|
async function startUp(url){
|
||||||
|
promis = runEvalScript("getEnv('" + url + "')");
|
||||||
|
|
||||||
|
var res = await promis;
|
||||||
|
log.warn("res: " + res);
|
||||||
|
|
||||||
|
promis = runEvalScript("getEnv('OPENPYPE_DEBUG')");
|
||||||
|
var debug = await promis;
|
||||||
|
log.warn("debug: " + debug);
|
||||||
|
if (debug && debug.toString() == '3'){
|
||||||
|
WSRPC.DEBUG = true;
|
||||||
|
WSRPC.TRACE = true;
|
||||||
|
}
|
||||||
|
// run rest only after resolved promise
|
||||||
|
main(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_extension_version(){
|
||||||
|
/** Returns version number from extension manifest.xml **/
|
||||||
|
log.debug("get_extension_version")
|
||||||
|
var path = csInterface.getSystemPath(SystemPath.EXTENSION);
|
||||||
|
log.debug("extension path " + path);
|
||||||
|
|
||||||
|
var result = window.cep.fs.readFile(path + "/CSXS/manifest.xml");
|
||||||
|
var version = undefined;
|
||||||
|
if(result.err === 0){
|
||||||
|
if (window.DOMParser) {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const xmlDoc = parser.parseFromString(result.data.toString(),
|
||||||
|
'text/xml');
|
||||||
|
const children = xmlDoc.children;
|
||||||
|
|
||||||
|
for (let i = 0; i <= children.length; i++) {
|
||||||
|
if (children[i] &&
|
||||||
|
children[i].getAttribute('ExtensionBundleVersion')) {
|
||||||
|
version =
|
||||||
|
children[i].getAttribute('ExtensionBundleVersion');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '{"result":"' + version + '"}'
|
||||||
|
}
|
||||||
|
|
||||||
|
function main(websocket_url){
|
||||||
|
// creates connection to 'websocket_url', registers routes
|
||||||
|
var default_url = 'ws://localhost:8099/ws/';
|
||||||
|
|
||||||
|
if (websocket_url == ''){
|
||||||
|
websocket_url = default_url;
|
||||||
|
}
|
||||||
|
RPC = new WSRPC(websocket_url, 5000); // spin connection
|
||||||
|
|
||||||
|
RPC.connect();
|
||||||
|
|
||||||
|
log.warn("connected");
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.open', function (data) {
|
||||||
|
log.warn('Server called client route "open":', data);
|
||||||
|
var escapedPath = EscapeStringForJSX(data.path);
|
||||||
|
return runEvalScript("fileOpen('" + escapedPath +"')")
|
||||||
|
.then(function(result){
|
||||||
|
log.warn("open: " + result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.get_metadata', function (data) {
|
||||||
|
log.warn('Server called client route "get_metadata":', data);
|
||||||
|
return runEvalScript("getMetadata()")
|
||||||
|
.then(function(result){
|
||||||
|
log.warn("getMetadata: " + result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.get_active_document_name', function (data) {
|
||||||
|
log.warn('Server called client route ' +
|
||||||
|
'"get_active_document_name":', data);
|
||||||
|
return runEvalScript("getActiveDocumentName()")
|
||||||
|
.then(function(result){
|
||||||
|
log.warn("get_active_document_name: " + result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.get_active_document_full_name', function (data){
|
||||||
|
log.warn('Server called client route ' +
|
||||||
|
'"get_active_document_full_name":', data);
|
||||||
|
return runEvalScript("getActiveDocumentFullName()")
|
||||||
|
.then(function(result){
|
||||||
|
log.warn("get_active_document_full_name: " + result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.get_items', function (data) {
|
||||||
|
log.warn('Server called client route "get_items":', data);
|
||||||
|
return runEvalScript("getItems(" + data.comps + "," +
|
||||||
|
data.folders + "," +
|
||||||
|
data.footages + ")")
|
||||||
|
.then(function(result){
|
||||||
|
log.warn("get_items: " + result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.get_selected_items', function (data) {
|
||||||
|
log.warn('Server called client route "get_selected_items":', data);
|
||||||
|
return runEvalScript("getSelectedItems(" + data.comps + "," +
|
||||||
|
data.folders + "," +
|
||||||
|
data.footages + ")")
|
||||||
|
.then(function(result){
|
||||||
|
log.warn("get_items: " + result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.import_file', function (data) {
|
||||||
|
log.warn('Server called client route "import_file":', data);
|
||||||
|
var escapedPath = EscapeStringForJSX(data.path);
|
||||||
|
return runEvalScript("importFile('" + escapedPath +"', " +
|
||||||
|
"'" + data.item_name + "'," +
|
||||||
|
"'" + JSON.stringify(
|
||||||
|
data.import_options) + "')")
|
||||||
|
.then(function(result){
|
||||||
|
log.warn("importFile: " + result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.replace_item', function (data) {
|
||||||
|
log.warn('Server called client route "replace_item":', data);
|
||||||
|
var escapedPath = EscapeStringForJSX(data.path);
|
||||||
|
return runEvalScript("replaceItem(" + data.item_id + ", " +
|
||||||
|
"'" + escapedPath + "', " +
|
||||||
|
"'" + data.item_name + "')")
|
||||||
|
.then(function(result){
|
||||||
|
log.warn("replaceItem: " + result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.rename_item', function (data) {
|
||||||
|
log.warn('Server called client route "rename_item":', data);
|
||||||
|
return runEvalScript("renameItem(" + data.item_id + ", " +
|
||||||
|
"'" + data.item_name + "')")
|
||||||
|
.then(function(result){
|
||||||
|
log.warn("renameItem: " + result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.delete_item', function (data) {
|
||||||
|
log.warn('Server called client route "delete_item":', data);
|
||||||
|
return runEvalScript("deleteItem(" + data.item_id + ")")
|
||||||
|
.then(function(result){
|
||||||
|
log.warn("deleteItem: " + result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.imprint', function (data) {
|
||||||
|
log.warn('Server called client route "imprint":', data);
|
||||||
|
var escaped = data.payload.replace(/\n/g, "\\n");
|
||||||
|
return runEvalScript("imprint('" + escaped +"')")
|
||||||
|
.then(function(result){
|
||||||
|
log.warn("imprint: " + result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.set_label_color', function (data) {
|
||||||
|
log.warn('Server called client route "set_label_color":', data);
|
||||||
|
return runEvalScript("setLabelColor(" + data.item_id + "," +
|
||||||
|
data.color_idx + ")")
|
||||||
|
.then(function(result){
|
||||||
|
log.warn("imprint: " + result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.get_work_area', function (data) {
|
||||||
|
log.warn('Server called client route "get_work_area":', data);
|
||||||
|
return runEvalScript("getWorkArea(" + data.item_id + ")")
|
||||||
|
.then(function(result){
|
||||||
|
log.warn("getWorkArea: " + result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.set_work_area', function (data) {
|
||||||
|
log.warn('Server called client route "set_work_area":', data);
|
||||||
|
return runEvalScript("setWorkArea(" + data.item_id + ',' +
|
||||||
|
data.start + ',' +
|
||||||
|
data.duration + ',' +
|
||||||
|
data.frame_rate + ")")
|
||||||
|
.then(function(result){
|
||||||
|
log.warn("getWorkArea: " + result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.saveAs', function (data) {
|
||||||
|
log.warn('Server called client route "saveAs":', data);
|
||||||
|
var escapedPath = EscapeStringForJSX(data.image_path);
|
||||||
|
return runEvalScript("saveAs('" + escapedPath + "', " +
|
||||||
|
data.as_copy + ")")
|
||||||
|
.then(function(result){
|
||||||
|
log.warn("saveAs: " + result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.save', function (data) {
|
||||||
|
log.warn('Server called client route "save":', data);
|
||||||
|
return runEvalScript("save()")
|
||||||
|
.then(function(result){
|
||||||
|
log.warn("save: " + result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.get_render_info', function (data) {
|
||||||
|
log.warn('Server called client route "get_render_info":', data);
|
||||||
|
return runEvalScript("getRenderInfo()")
|
||||||
|
.then(function(result){
|
||||||
|
log.warn("get_render_info: " + result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.get_audio_url', function (data) {
|
||||||
|
log.warn('Server called client route "get_audio_url":', data);
|
||||||
|
return runEvalScript("getAudioUrlForComp(" + data.item_id + ")")
|
||||||
|
.then(function(result){
|
||||||
|
log.warn("getAudioUrlForComp: " + result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.import_background', function (data) {
|
||||||
|
log.warn('Server called client route "import_background":', data);
|
||||||
|
return runEvalScript("importBackground(" + data.comp_id + ", " +
|
||||||
|
"'" + data.comp_name + "', " +
|
||||||
|
JSON.stringify(data.files) + ")")
|
||||||
|
.then(function(result){
|
||||||
|
log.warn("importBackground: " + result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.reload_background', function (data) {
|
||||||
|
log.warn('Server called client route "reload_background":', data);
|
||||||
|
return runEvalScript("reloadBackground(" + data.comp_id + ", " +
|
||||||
|
"'" + data.comp_name + "', " +
|
||||||
|
JSON.stringify(data.files) + ")")
|
||||||
|
.then(function(result){
|
||||||
|
log.warn("reloadBackground: " + result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.add_item_as_layer', function (data) {
|
||||||
|
log.warn('Server called client route "add_item_as_layer":', data);
|
||||||
|
return runEvalScript("addItemAsLayerToComp(" + data.comp_id + ", " +
|
||||||
|
data.item_id + "," +
|
||||||
|
" null )")
|
||||||
|
.then(function(result){
|
||||||
|
log.warn("addItemAsLayerToComp: " + result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.render', function (data) {
|
||||||
|
log.warn('Server called client route "render":', data);
|
||||||
|
var escapedPath = EscapeStringForJSX(data.folder_url);
|
||||||
|
return runEvalScript("render('" + escapedPath +"')")
|
||||||
|
.then(function(result){
|
||||||
|
log.warn("render: " + result);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.get_extension_version', function (data) {
|
||||||
|
log.warn('Server called client route "get_extension_version":', data);
|
||||||
|
return get_extension_version();
|
||||||
|
});
|
||||||
|
|
||||||
|
RPC.addRoute('AfterEffects.close', function (data) {
|
||||||
|
log.warn('Server called client route "close":', data);
|
||||||
|
return runEvalScript("close()");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** main entry point **/
|
||||||
|
startUp("WEBSOCKET_URL");
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var csInterface = new CSInterface();
|
||||||
|
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
|
||||||
|
themeManager.init();
|
||||||
|
|
||||||
|
$("#btn_test").click(function () {
|
||||||
|
csInterface.evalScript('sayHello()');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
}());
|
||||||
|
|
||||||
|
function EscapeStringForJSX(str){
|
||||||
|
// Replaces:
|
||||||
|
// \ with \\
|
||||||
|
// ' with \'
|
||||||
|
// " with \"
|
||||||
|
// See: https://stackoverflow.com/a/3967927/5285364
|
||||||
|
return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g,'\\"');
|
||||||
|
}
|
||||||
|
|
||||||
|
function runEvalScript(script) {
|
||||||
|
// because of asynchronous nature of functions in jsx
|
||||||
|
// this waits for response
|
||||||
|
return new Promise(function(resolve, reject){
|
||||||
|
csInterface.evalScript(script, resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
128
openpype/hosts/aftereffects/api/extension/js/themeManager.js
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */
|
||||||
|
/*global window, document, CSInterface*/
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Responsible for overwriting CSS at runtime according to CC app
|
||||||
|
settings as defined by the end user.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
var themeManager = (function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the Color object to string in hexadecimal format;
|
||||||
|
*/
|
||||||
|
function toHex(color, delta) {
|
||||||
|
|
||||||
|
function computeValue(value, delta) {
|
||||||
|
var computedValue = !isNaN(delta) ? value + delta : value;
|
||||||
|
if (computedValue < 0) {
|
||||||
|
computedValue = 0;
|
||||||
|
} else if (computedValue > 255) {
|
||||||
|
computedValue = 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
computedValue = Math.floor(computedValue);
|
||||||
|
|
||||||
|
computedValue = computedValue.toString(16);
|
||||||
|
return computedValue.length === 1 ? "0" + computedValue : computedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hex = "";
|
||||||
|
if (color) {
|
||||||
|
hex = computeValue(color.red, delta) + computeValue(color.green, delta) + computeValue(color.blue, delta);
|
||||||
|
}
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function reverseColor(color, delta) {
|
||||||
|
return toHex({
|
||||||
|
red: Math.abs(255 - color.red),
|
||||||
|
green: Math.abs(255 - color.green),
|
||||||
|
blue: Math.abs(255 - color.blue)
|
||||||
|
},
|
||||||
|
delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function addRule(stylesheetId, selector, rule) {
|
||||||
|
var stylesheet = document.getElementById(stylesheetId);
|
||||||
|
|
||||||
|
if (stylesheet) {
|
||||||
|
stylesheet = stylesheet.sheet;
|
||||||
|
if (stylesheet.addRule) {
|
||||||
|
stylesheet.addRule(selector, rule);
|
||||||
|
} else if (stylesheet.insertRule) {
|
||||||
|
stylesheet.insertRule(selector + ' { ' + rule + ' }', stylesheet.cssRules.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the theme with the AppSkinInfo retrieved from the host product.
|
||||||
|
*/
|
||||||
|
function updateThemeWithAppSkinInfo(appSkinInfo) {
|
||||||
|
|
||||||
|
var panelBgColor = appSkinInfo.panelBackgroundColor.color;
|
||||||
|
var bgdColor = toHex(panelBgColor);
|
||||||
|
|
||||||
|
var darkBgdColor = toHex(panelBgColor, 20);
|
||||||
|
|
||||||
|
var fontColor = "F0F0F0";
|
||||||
|
if (panelBgColor.red > 122) {
|
||||||
|
fontColor = "000000";
|
||||||
|
}
|
||||||
|
var lightBgdColor = toHex(panelBgColor, -100);
|
||||||
|
|
||||||
|
var styleId = "hostStyle";
|
||||||
|
|
||||||
|
addRule(styleId, ".hostElt", "background-color:" + "#" + bgdColor);
|
||||||
|
addRule(styleId, ".hostElt", "font-size:" + appSkinInfo.baseFontSize + "px;");
|
||||||
|
addRule(styleId, ".hostElt", "font-family:" + appSkinInfo.baseFontFamily);
|
||||||
|
addRule(styleId, ".hostElt", "color:" + "#" + fontColor);
|
||||||
|
|
||||||
|
addRule(styleId, ".hostBgd", "background-color:" + "#" + bgdColor);
|
||||||
|
addRule(styleId, ".hostBgdDark", "background-color: " + "#" + darkBgdColor);
|
||||||
|
addRule(styleId, ".hostBgdLight", "background-color: " + "#" + lightBgdColor);
|
||||||
|
addRule(styleId, ".hostFontSize", "font-size:" + appSkinInfo.baseFontSize + "px;");
|
||||||
|
addRule(styleId, ".hostFontFamily", "font-family:" + appSkinInfo.baseFontFamily);
|
||||||
|
addRule(styleId, ".hostFontColor", "color:" + "#" + fontColor);
|
||||||
|
|
||||||
|
addRule(styleId, ".hostFont", "font-size:" + appSkinInfo.baseFontSize + "px;");
|
||||||
|
addRule(styleId, ".hostFont", "font-family:" + appSkinInfo.baseFontFamily);
|
||||||
|
addRule(styleId, ".hostFont", "color:" + "#" + fontColor);
|
||||||
|
|
||||||
|
addRule(styleId, ".hostButton", "background-color:" + "#" + darkBgdColor);
|
||||||
|
addRule(styleId, ".hostButton:hover", "background-color:" + "#" + bgdColor);
|
||||||
|
addRule(styleId, ".hostButton:active", "background-color:" + "#" + darkBgdColor);
|
||||||
|
addRule(styleId, ".hostButton", "border-color: " + "#" + lightBgdColor);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function onAppThemeColorChanged(event) {
|
||||||
|
var skinInfo = JSON.parse(window.__adobe_cep__.getHostEnvironment()).appSkinInfo;
|
||||||
|
updateThemeWithAppSkinInfo(skinInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
|
||||||
|
var csInterface = new CSInterface();
|
||||||
|
|
||||||
|
updateThemeWithAppSkinInfo(csInterface.hostEnvironment.appSkinInfo);
|
||||||
|
|
||||||
|
csInterface.addEventListener(CSInterface.THEME_COLOR_CHANGED_EVENT, onAppThemeColorChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
init: init
|
||||||
|
};
|
||||||
|
|
||||||
|
}());
|
||||||
723
openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx
Normal file
|
|
@ -0,0 +1,723 @@
|
||||||
|
/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true,
|
||||||
|
indent: 4, maxerr: 50 */
|
||||||
|
/*global $, Folder*/
|
||||||
|
#include "../js/libs/json.js";
|
||||||
|
|
||||||
|
/* All public API function should return JSON! */
|
||||||
|
|
||||||
|
app.preferences.savePrefAsBool("General Section", "Show Welcome Screen", false) ;
|
||||||
|
|
||||||
|
if(!Array.prototype.indexOf) {
|
||||||
|
Array.prototype.indexOf = function ( item ) {
|
||||||
|
var index = 0, length = this.length;
|
||||||
|
for ( ; index < length; index++ ) {
|
||||||
|
if ( this[index] === item )
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sayHello(){
|
||||||
|
alert("hello from ExtendScript");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEnv(variable){
|
||||||
|
return $.getenv(variable);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMetadata(){
|
||||||
|
/**
|
||||||
|
* Returns payload in 'Label' field of project's metadata
|
||||||
|
*
|
||||||
|
**/
|
||||||
|
if (ExternalObject.AdobeXMPScript === undefined){
|
||||||
|
ExternalObject.AdobeXMPScript =
|
||||||
|
new ExternalObject('lib:AdobeXMPScript');
|
||||||
|
}
|
||||||
|
|
||||||
|
var proj = app.project;
|
||||||
|
var meta = new XMPMeta(app.project.xmpPacket);
|
||||||
|
var schemaNS = XMPMeta.getNamespaceURI("xmp");
|
||||||
|
var label = "xmp:Label";
|
||||||
|
|
||||||
|
if (meta.doesPropertyExist(schemaNS, label)){
|
||||||
|
var prop = meta.getProperty(schemaNS, label);
|
||||||
|
return prop.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _prepareSingleValue([]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function imprint(payload){
|
||||||
|
/**
|
||||||
|
* Stores payload in 'Label' field of project's metadata
|
||||||
|
*
|
||||||
|
* Args:
|
||||||
|
* payload (string): json content
|
||||||
|
*/
|
||||||
|
if (ExternalObject.AdobeXMPScript === undefined){
|
||||||
|
ExternalObject.AdobeXMPScript =
|
||||||
|
new ExternalObject('lib:AdobeXMPScript');
|
||||||
|
}
|
||||||
|
|
||||||
|
var proj = app.project;
|
||||||
|
var meta = new XMPMeta(app.project.xmpPacket);
|
||||||
|
var schemaNS = XMPMeta.getNamespaceURI("xmp");
|
||||||
|
var label = "xmp:Label";
|
||||||
|
|
||||||
|
meta.setProperty(schemaNS, label, payload);
|
||||||
|
|
||||||
|
app.project.xmpPacket = meta.serialize();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function fileOpen(path){
|
||||||
|
/**
|
||||||
|
* Opens (project) file on 'path'
|
||||||
|
*/
|
||||||
|
fp = new File(path);
|
||||||
|
return _prepareSingleValue(app.open(fp))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveDocumentName(){
|
||||||
|
/**
|
||||||
|
* Returns file name of active document
|
||||||
|
* */
|
||||||
|
var file = app.project.file;
|
||||||
|
|
||||||
|
if (file){
|
||||||
|
return _prepareSingleValue(file.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return _prepareError("No file open currently");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveDocumentFullName(){
|
||||||
|
/**
|
||||||
|
* Returns absolute path to current project
|
||||||
|
* */
|
||||||
|
var file = app.project.file;
|
||||||
|
|
||||||
|
if (file){
|
||||||
|
var f = new File(file.fullName);
|
||||||
|
var path = f.fsName;
|
||||||
|
f.close();
|
||||||
|
|
||||||
|
return _prepareSingleValue(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return _prepareError("No file open currently");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItems(comps, folders, footages){
|
||||||
|
/**
|
||||||
|
* Returns JSON representation of compositions and
|
||||||
|
* if 'collectLayers' then layers in comps too.
|
||||||
|
*
|
||||||
|
* Args:
|
||||||
|
* comps (bool): return selected compositions
|
||||||
|
* folders (bool): return folders
|
||||||
|
* footages (bool): return FootageItem
|
||||||
|
* Returns:
|
||||||
|
* (list) of JSON items
|
||||||
|
*/
|
||||||
|
var items = []
|
||||||
|
for (i = 1; i <= app.project.items.length; ++i){
|
||||||
|
var item = app.project.items[i];
|
||||||
|
if (!item){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var ret = _getItem(item, comps, folders, footages);
|
||||||
|
if (ret){
|
||||||
|
items.push(ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '[' + items.join() + ']';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedItems(comps, folders, footages){
|
||||||
|
/**
|
||||||
|
* Returns list of selected items from Project menu
|
||||||
|
*
|
||||||
|
* Args:
|
||||||
|
* comps (bool): return selected compositions
|
||||||
|
* folders (bool): return folders
|
||||||
|
* footages (bool): return FootageItem
|
||||||
|
* Returns:
|
||||||
|
* (list) of JSON items
|
||||||
|
*/
|
||||||
|
var items = []
|
||||||
|
for (i = 0; i < app.project.selection.length; ++i){
|
||||||
|
var item = app.project.selection[i];
|
||||||
|
if (!item){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var ret = _getItem(item, comps, folders, footages);
|
||||||
|
if (ret){
|
||||||
|
items.push(ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '[' + items.join() + ']';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getItem(item, comps, folders, footages){
|
||||||
|
/**
|
||||||
|
* Auxiliary function as project items and selections
|
||||||
|
* are indexed in different way :/
|
||||||
|
* Refactor
|
||||||
|
*/
|
||||||
|
var item_type = '';
|
||||||
|
if (item instanceof FolderItem){
|
||||||
|
item_type = 'folder';
|
||||||
|
if (!folders){
|
||||||
|
return "{}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (item instanceof FootageItem){
|
||||||
|
item_type = 'footage';
|
||||||
|
if (!footages){
|
||||||
|
return "{}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (item instanceof CompItem){
|
||||||
|
item_type = 'comp';
|
||||||
|
if (!comps){
|
||||||
|
return "{}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = {"name": item.name,
|
||||||
|
"id": item.id,
|
||||||
|
"type": item_type};
|
||||||
|
return JSON.stringify(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
function importFile(path, item_name, import_options){
|
||||||
|
/**
|
||||||
|
* Imports file (image tested for now) as a FootageItem.
|
||||||
|
* Creates new composition
|
||||||
|
*
|
||||||
|
* Args:
|
||||||
|
* path (string): absolute path to image file
|
||||||
|
* item_name (string): label for composition
|
||||||
|
* Returns:
|
||||||
|
* JSON {name, id}
|
||||||
|
*/
|
||||||
|
var comp;
|
||||||
|
var ret = {};
|
||||||
|
try{
|
||||||
|
import_options = JSON.parse(import_options);
|
||||||
|
} catch (e){
|
||||||
|
return _prepareError("Couldn't parse import options " + import_options);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.beginUndoGroup("Import File");
|
||||||
|
fp = new File(path);
|
||||||
|
if (fp.exists){
|
||||||
|
try {
|
||||||
|
im_opt = new ImportOptions(fp);
|
||||||
|
importAsType = import_options["ImportAsType"];
|
||||||
|
|
||||||
|
if ('ImportAsType' in import_options){ // refactor
|
||||||
|
if (importAsType.indexOf('COMP') > 0){
|
||||||
|
im_opt.importAs = ImportAsType.COMP;
|
||||||
|
}
|
||||||
|
if (importAsType.indexOf('FOOTAGE') > 0){
|
||||||
|
im_opt.importAs = ImportAsType.FOOTAGE;
|
||||||
|
}
|
||||||
|
if (importAsType.indexOf('COMP_CROPPED_LAYERS') > 0){
|
||||||
|
im_opt.importAs = ImportAsType.COMP_CROPPED_LAYERS;
|
||||||
|
}
|
||||||
|
if (importAsType.indexOf('PROJECT') > 0){
|
||||||
|
im_opt.importAs = ImportAsType.PROJECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if ('sequence' in import_options){
|
||||||
|
im_opt.sequence = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
comp = app.project.importFile(im_opt);
|
||||||
|
|
||||||
|
if (app.project.selection.length == 2 &&
|
||||||
|
app.project.selection[0] instanceof FolderItem){
|
||||||
|
comp.parentFolder = app.project.selection[0]
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return _prepareError(error.toString() + importOptions.file.fsName);
|
||||||
|
} finally {
|
||||||
|
fp.close();
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
return _prepareError("File " + path + " not found.");
|
||||||
|
}
|
||||||
|
if (comp){
|
||||||
|
comp.name = item_name;
|
||||||
|
comp.label = 9; // Green
|
||||||
|
ret = {"name": comp.name, "id": comp.id}
|
||||||
|
}
|
||||||
|
app.endUndoGroup();
|
||||||
|
|
||||||
|
return JSON.stringify(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLabelColor(comp_id, color_idx){
|
||||||
|
/**
|
||||||
|
* Set item_id label to 'color_idx' color
|
||||||
|
* Args:
|
||||||
|
* item_id (int): item id
|
||||||
|
* color_idx (int): 0-16 index from Label
|
||||||
|
*/
|
||||||
|
var item = app.project.itemByID(comp_id);
|
||||||
|
if (item){
|
||||||
|
item.label = color_idx;
|
||||||
|
}else{
|
||||||
|
return _prepareError("There is no composition with "+ comp_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceItem(comp_id, path, item_name){
|
||||||
|
/**
|
||||||
|
* Replaces loaded file with new file and updates name
|
||||||
|
*
|
||||||
|
* Args:
|
||||||
|
* comp_id (int): id of composition, not a index!
|
||||||
|
* path (string): absolute path to new file
|
||||||
|
* item_name (string): new composition name
|
||||||
|
*/
|
||||||
|
app.beginUndoGroup("Replace File");
|
||||||
|
|
||||||
|
fp = new File(path);
|
||||||
|
if (!fp.exists){
|
||||||
|
return _prepareError("File " + path + " not found.");
|
||||||
|
}
|
||||||
|
var item = app.project.itemByID(comp_id);
|
||||||
|
if (item){
|
||||||
|
try{
|
||||||
|
if (isFileSequence(item)) {
|
||||||
|
item.replaceWithSequence(fp, false);
|
||||||
|
}else{
|
||||||
|
item.replace(fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
item.name = item_name;
|
||||||
|
} catch (error) {
|
||||||
|
return _prepareError(error.toString() + path);
|
||||||
|
} finally {
|
||||||
|
fp.close();
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
return _prepareError("There is no composition with "+ comp_id);
|
||||||
|
}
|
||||||
|
app.endUndoGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renameItem(item_id, new_name){
|
||||||
|
/**
|
||||||
|
* Renames item with 'item_id' to 'new_name'
|
||||||
|
*
|
||||||
|
* Args:
|
||||||
|
* item_id (int): id to search item
|
||||||
|
* new_name (str)
|
||||||
|
*/
|
||||||
|
var item = app.project.itemByID(item_id);
|
||||||
|
if (item){
|
||||||
|
item.name = new_name;
|
||||||
|
}else{
|
||||||
|
return _prepareError("There is no composition with "+ comp_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteItem(item_id){
|
||||||
|
/**
|
||||||
|
* Delete any 'item_id'
|
||||||
|
*
|
||||||
|
* Not restricted only to comp, it could delete
|
||||||
|
* any item with 'id'
|
||||||
|
*/
|
||||||
|
var item = app.project.itemByID(item_id);
|
||||||
|
if (item){
|
||||||
|
item.remove();
|
||||||
|
}else{
|
||||||
|
return _prepareError("There is no composition with "+ comp_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWorkArea(comp_id){
|
||||||
|
/**
|
||||||
|
* Returns information about workarea - are that will be
|
||||||
|
* rendered. All calculation will be done in OpenPype,
|
||||||
|
* easier to modify without redeploy of extension.
|
||||||
|
*
|
||||||
|
* Returns
|
||||||
|
* (dict)
|
||||||
|
*/
|
||||||
|
var item = app.project.itemByID(comp_id);
|
||||||
|
if (item){
|
||||||
|
return JSON.stringify({
|
||||||
|
"workAreaStart": item.displayStartFrame,
|
||||||
|
"workAreaDuration": item.duration,
|
||||||
|
"frameRate": item.frameRate});
|
||||||
|
}else{
|
||||||
|
return _prepareError("There is no composition with "+ comp_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWorkArea(comp_id, workAreaStart, workAreaDuration, frameRate){
|
||||||
|
/**
|
||||||
|
* Sets work area info from outside (from Ftrack via OpenPype)
|
||||||
|
*/
|
||||||
|
var item = app.project.itemByID(comp_id);
|
||||||
|
if (item){
|
||||||
|
item.displayStartTime = workAreaStart;
|
||||||
|
item.duration = workAreaDuration;
|
||||||
|
item.frameRate = frameRate;
|
||||||
|
}else{
|
||||||
|
return _prepareError("There is no composition with "+ comp_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function save(){
|
||||||
|
/**
|
||||||
|
* Saves current project
|
||||||
|
*/
|
||||||
|
app.project.save(); //TODO path is wrong, File instead
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAs(path){
|
||||||
|
/**
|
||||||
|
* Saves current project as 'path'
|
||||||
|
* */
|
||||||
|
app.project.save(fp = new File(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRenderInfo(){
|
||||||
|
/***
|
||||||
|
Get info from render queue.
|
||||||
|
Currently pulls only file name to parse extension and
|
||||||
|
if it is sequence in Python
|
||||||
|
**/
|
||||||
|
try{
|
||||||
|
var render_item = app.project.renderQueue.item(1);
|
||||||
|
if (render_item.status == RQItemStatus.DONE){
|
||||||
|
render_item.duplicate(); // create new, cannot change status if DONE
|
||||||
|
render_item.remove(); // remove existing to limit duplications
|
||||||
|
render_item = app.project.renderQueue.item(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
render_item.render = true; // always set render queue to render
|
||||||
|
var item = render_item.outputModule(1);
|
||||||
|
} catch (error) {
|
||||||
|
return _prepareError("There is no render queue, create one");
|
||||||
|
}
|
||||||
|
var file_url = item.file.toString();
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
"file_name": file_url
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAudioUrlForComp(comp_id){
|
||||||
|
/**
|
||||||
|
* Searches composition for audio layer
|
||||||
|
*
|
||||||
|
* Only single AVLayer is expected!
|
||||||
|
* Used for collecting Audio
|
||||||
|
*
|
||||||
|
* Args:
|
||||||
|
* comp_id (int): id of composition
|
||||||
|
* Return:
|
||||||
|
* (str) with url to audio content
|
||||||
|
*/
|
||||||
|
var item = app.project.itemByID(comp_id);
|
||||||
|
if (item){
|
||||||
|
for (i = 1; i <= item.numLayers; ++i){
|
||||||
|
var layer = item.layers[i];
|
||||||
|
if (layer instanceof AVLayer){
|
||||||
|
return layer.source.file.fsName.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
return _prepareError("There is no composition with "+ comp_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function addItemAsLayerToComp(comp_id, item_id, found_comp){
|
||||||
|
/**
|
||||||
|
* Adds already imported FootageItem ('item_id') as a new
|
||||||
|
* layer to composition ('comp_id').
|
||||||
|
*
|
||||||
|
* Args:
|
||||||
|
* comp_id (int): id of target composition
|
||||||
|
* item_id (int): FootageItem.id
|
||||||
|
* found_comp (CompItem, optional): to limit quering if
|
||||||
|
* comp already found previously
|
||||||
|
*/
|
||||||
|
var comp = found_comp || app.project.itemByID(comp_id);
|
||||||
|
if (comp){
|
||||||
|
item = app.project.itemByID(item_id);
|
||||||
|
if (item){
|
||||||
|
comp.layers.add(item);
|
||||||
|
}else{
|
||||||
|
return _prepareError("There is no item with " + item_id);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
return _prepareError("There is no composition with "+ comp_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function importBackground(comp_id, composition_name, files_to_import){
|
||||||
|
/**
|
||||||
|
* Imports backgrounds images to existing or new composition.
|
||||||
|
*
|
||||||
|
* If comp_id is not provided, new composition is created, basic
|
||||||
|
* values (width, heights, frameRatio) takes from first imported
|
||||||
|
* image.
|
||||||
|
*
|
||||||
|
* Args:
|
||||||
|
* comp_id (int): id of existing composition (null if new)
|
||||||
|
* composition_name (str): used when new composition
|
||||||
|
* files_to_import (list): list of absolute paths to import and
|
||||||
|
* add as layers
|
||||||
|
*
|
||||||
|
* Returns:
|
||||||
|
* (str): json representation (id, name, members)
|
||||||
|
*/
|
||||||
|
var comp;
|
||||||
|
var folder;
|
||||||
|
var imported_ids = [];
|
||||||
|
if (comp_id){
|
||||||
|
comp = app.project.itemByID(comp_id);
|
||||||
|
folder = comp.parentFolder;
|
||||||
|
}else{
|
||||||
|
if (app.project.selection.length > 1){
|
||||||
|
return _prepareError(
|
||||||
|
"Too many items selected, select only target composition!");
|
||||||
|
}else{
|
||||||
|
selected_item = app.project.activeItem;
|
||||||
|
if (selected_item instanceof Folder){
|
||||||
|
comp = selected_item;
|
||||||
|
folder = selected_item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files_to_import){
|
||||||
|
for (i = 0; i < files_to_import.length; ++i){
|
||||||
|
item = _importItem(files_to_import[i]);
|
||||||
|
if (!item){
|
||||||
|
return _prepareError(
|
||||||
|
"No item for " + item_json["id"] +
|
||||||
|
". Import background failed.")
|
||||||
|
}
|
||||||
|
if (!comp){
|
||||||
|
folder = app.project.items.addFolder(composition_name);
|
||||||
|
imported_ids.push(folder.id);
|
||||||
|
comp = app.project.items.addComp(composition_name, item.width,
|
||||||
|
item.height, item.pixelAspect,
|
||||||
|
1, 26.7); // hardcode defaults
|
||||||
|
imported_ids.push(comp.id);
|
||||||
|
comp.parentFolder = folder;
|
||||||
|
}
|
||||||
|
imported_ids.push(item.id)
|
||||||
|
item.parentFolder = folder;
|
||||||
|
|
||||||
|
addItemAsLayerToComp(comp.id, item.id, comp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var item = {"name": comp.name,
|
||||||
|
"id": folder.id,
|
||||||
|
"members": imported_ids};
|
||||||
|
return JSON.stringify(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadBackground(comp_id, composition_name, files_to_import){
|
||||||
|
/**
|
||||||
|
* Reloads existing composition.
|
||||||
|
*
|
||||||
|
* It deletes complete composition with encompassing folder, recreates
|
||||||
|
* from scratch via 'importBackground' functionality.
|
||||||
|
*
|
||||||
|
* Args:
|
||||||
|
* comp_id (int): id of existing composition (null if new)
|
||||||
|
* composition_name (str): used when new composition
|
||||||
|
* files_to_import (list): list of absolute paths to import and
|
||||||
|
* add as layers
|
||||||
|
*
|
||||||
|
* Returns:
|
||||||
|
* (str): json representation (id, name, members)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
var imported_ids = []; // keep track of members of composition
|
||||||
|
comp = app.project.itemByID(comp_id);
|
||||||
|
folder = comp.parentFolder;
|
||||||
|
if (folder){
|
||||||
|
renameItem(folder.id, composition_name);
|
||||||
|
imported_ids.push(folder.id);
|
||||||
|
}
|
||||||
|
if (comp){
|
||||||
|
renameItem(comp.id, composition_name);
|
||||||
|
imported_ids.push(comp.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
var existing_layer_names = [];
|
||||||
|
var existing_layer_ids = []; // because ExtendedScript doesnt have keys()
|
||||||
|
for (i = 1; i <= folder.items.length; ++i){
|
||||||
|
layer = folder.items[i];
|
||||||
|
//because comp.layers[i] doesnt have 'id' accessible
|
||||||
|
if (layer instanceof CompItem){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
existing_layer_names.push(layer.name);
|
||||||
|
existing_layer_ids.push(layer.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
var new_filenames = [];
|
||||||
|
if (files_to_import){
|
||||||
|
for (i = 0; i < files_to_import.length; ++i){
|
||||||
|
file_name = _get_file_name(files_to_import[i]);
|
||||||
|
new_filenames.push(file_name);
|
||||||
|
|
||||||
|
idx = existing_layer_names.indexOf(file_name);
|
||||||
|
if (idx >= 0){ // update
|
||||||
|
var layer_id = existing_layer_ids[idx];
|
||||||
|
replaceItem(layer_id, files_to_import[i], file_name);
|
||||||
|
imported_ids.push(layer_id);
|
||||||
|
}else{ // new layer
|
||||||
|
item = _importItem(files_to_import[i]);
|
||||||
|
if (!item){
|
||||||
|
return _prepareError(
|
||||||
|
"No item for " + files_to_import[i] +
|
||||||
|
". Reload background failed.");
|
||||||
|
}
|
||||||
|
imported_ids.push(item.id);
|
||||||
|
item.parentFolder = folder;
|
||||||
|
addItemAsLayerToComp(comp.id, item.id, comp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_delete_obsolete_items(folder, new_filenames);
|
||||||
|
|
||||||
|
var item = {"name": comp.name,
|
||||||
|
"id": folder.id,
|
||||||
|
"members": imported_ids};
|
||||||
|
|
||||||
|
return JSON.stringify(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _get_file_name(file_url){
|
||||||
|
/**
|
||||||
|
* Returns file name without extension from 'file_url'
|
||||||
|
*
|
||||||
|
* Args:
|
||||||
|
* file_url (str): full absolute url
|
||||||
|
* Returns:
|
||||||
|
* (str)
|
||||||
|
*/
|
||||||
|
fp = new File(file_url);
|
||||||
|
file_name = fp.name.substring(0, fp.name.lastIndexOf("."));
|
||||||
|
return file_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _delete_obsolete_items(folder, new_filenames){
|
||||||
|
/***
|
||||||
|
* Goes through 'folder' and removes layers not in new
|
||||||
|
* background
|
||||||
|
*
|
||||||
|
* Args:
|
||||||
|
* folder (FolderItem)
|
||||||
|
* new_filenames (array): list of layer names in new bg
|
||||||
|
*/
|
||||||
|
// remove items in old, but not in new
|
||||||
|
delete_ids = []
|
||||||
|
for (i = 1; i <= folder.items.length; ++i){
|
||||||
|
layer = folder.items[i];
|
||||||
|
//because comp.layers[i] doesnt have 'id' accessible
|
||||||
|
if (layer instanceof CompItem){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (new_filenames.indexOf(layer.name) < 0){
|
||||||
|
delete_ids.push(layer.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i = 0; i < delete_ids.length; ++i){
|
||||||
|
deleteItem(delete_ids[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _importItem(file_url){
|
||||||
|
/**
|
||||||
|
* Imports 'file_url' as new FootageItem
|
||||||
|
*
|
||||||
|
* Args:
|
||||||
|
* file_url (str): file url with content
|
||||||
|
* Returns:
|
||||||
|
* (FootageItem)
|
||||||
|
*/
|
||||||
|
file_name = _get_file_name(file_url);
|
||||||
|
|
||||||
|
//importFile prepared previously to return json
|
||||||
|
item_json = importFile(file_url, file_name, JSON.stringify({"ImportAsType":"FOOTAGE"}));
|
||||||
|
item_json = JSON.parse(item_json);
|
||||||
|
item = app.project.itemByID(item_json["id"]);
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFileSequence (item){
|
||||||
|
/**
|
||||||
|
* Check that item is a recognizable sequence
|
||||||
|
*/
|
||||||
|
if (item instanceof FootageItem && item.mainSource instanceof FileSource && !(item.mainSource.isStill) && item.hasVideo){
|
||||||
|
var extname = item.mainSource.file.fsName.split('.').pop();
|
||||||
|
|
||||||
|
return extname.match(new RegExp("(ai|bmp|bw|cin|cr2|crw|dcr|dng|dib|dpx|eps|erf|exr|gif|hdr|ico|icb|iff|jpe|jpeg|jpg|mos|mrw|nef|orf|pbm|pef|pct|pcx|pdf|pic|pict|png|ps|psd|pxr|raf|raw|rgb|rgbe|rla|rle|rpf|sgi|srf|tdi|tga|tif|tiff|vda|vst|x3f|xyze)", "i")) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(target_folder){
|
||||||
|
var out_dir = new Folder(target_folder);
|
||||||
|
var out_dir = out_dir.fsName;
|
||||||
|
for (i = 1; i <= app.project.renderQueue.numItems; ++i){
|
||||||
|
var render_item = app.project.renderQueue.item(i);
|
||||||
|
var om1 = app.project.renderQueue.item(i).outputModule(1);
|
||||||
|
var file_name = File.decode( om1.file.name ).replace('℗', ''); // Name contains special character, space?
|
||||||
|
|
||||||
|
var omItem1_settable_str = app.project.renderQueue.item(i).outputModule(1).getSettings( GetSettingsFormat.STRING_SETTABLE );
|
||||||
|
|
||||||
|
if (render_item.status == RQItemStatus.DONE){
|
||||||
|
render_item.duplicate();
|
||||||
|
render_item.remove();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetFolder = new Folder(target_folder);
|
||||||
|
if (!targetFolder.exists) {
|
||||||
|
targetFolder.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
om1.file = new File(targetFolder.fsName + '/' + file_name);
|
||||||
|
}
|
||||||
|
app.project.renderQueue.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(){
|
||||||
|
app.project.close(CloseOptions.DO_NOT_SAVE_CHANGES);
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _prepareSingleValue(value){
|
||||||
|
return JSON.stringify({"result": value})
|
||||||
|
}
|
||||||
|
function _prepareError(error_msg){
|
||||||
|
return JSON.stringify({"error": error_msg})
|
||||||
|
}
|
||||||
319
openpype/hosts/aftereffects/api/launch_logic.py
Normal file
|
|
@ -0,0 +1,319 @@
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import collections
|
||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
import functools
|
||||||
|
|
||||||
|
from wsrpc_aiohttp import (
|
||||||
|
WebSocketRoute,
|
||||||
|
WebSocketAsync
|
||||||
|
)
|
||||||
|
|
||||||
|
from Qt import QtCore
|
||||||
|
|
||||||
|
from openpype.tools.utils import host_tools
|
||||||
|
|
||||||
|
from avalon import api
|
||||||
|
from avalon.tools.webserver.app import WebServerTool
|
||||||
|
|
||||||
|
from .ws_stub import AfterEffectsServerStub
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionNotEstablishedYet(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_stub():
|
||||||
|
"""
|
||||||
|
Convenience function to get server RPC stub to call methods directed
|
||||||
|
for host (Photoshop).
|
||||||
|
It expects already created connection, started from client.
|
||||||
|
Currently created when panel is opened (PS: Window>Extensions>Avalon)
|
||||||
|
:return: <PhotoshopClientStub> where functions could be called from
|
||||||
|
"""
|
||||||
|
ae_stub = AfterEffectsServerStub()
|
||||||
|
if not ae_stub.client:
|
||||||
|
raise ConnectionNotEstablishedYet("Connection is not created yet")
|
||||||
|
|
||||||
|
return ae_stub
|
||||||
|
|
||||||
|
|
||||||
|
def stub():
|
||||||
|
return get_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 = "AfterEffects"
|
||||||
|
_main_thread_callbacks = collections.deque()
|
||||||
|
|
||||||
|
def __init__(self, subprocess_args):
|
||||||
|
self._subprocess_args = subprocess_args
|
||||||
|
self._log = None
|
||||||
|
|
||||||
|
super(ProcessLauncher, self).__init__()
|
||||||
|
|
||||||
|
# Keep track if launcher was alreadu started
|
||||||
|
self._started = False
|
||||||
|
|
||||||
|
self._process = None
|
||||||
|
self._websocket_server = None
|
||||||
|
|
||||||
|
start_process_timer = QtCore.QTimer()
|
||||||
|
start_process_timer.setInterval(100)
|
||||||
|
|
||||||
|
loop_timer = QtCore.QTimer()
|
||||||
|
loop_timer.setInterval(200)
|
||||||
|
|
||||||
|
start_process_timer.timeout.connect(self._on_start_process_timer)
|
||||||
|
loop_timer.timeout.connect(self._on_loop_timer)
|
||||||
|
|
||||||
|
self._start_process_timer = start_process_timer
|
||||||
|
self._loop_timer = loop_timer
|
||||||
|
|
||||||
|
@property
|
||||||
|
def log(self):
|
||||||
|
if self._log is None:
|
||||||
|
from openpype.api import Logger
|
||||||
|
|
||||||
|
self._log = Logger.get_logger("{}-launcher".format(
|
||||||
|
self.route_name))
|
||||||
|
return self._log
|
||||||
|
|
||||||
|
@property
|
||||||
|
def websocket_server_is_running(self):
|
||||||
|
if self._websocket_server is not None:
|
||||||
|
return self._websocket_server.is_running
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_process_running(self):
|
||||||
|
if self._process is not None:
|
||||||
|
return self._process.poll() is None
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_host_connected(self):
|
||||||
|
"""Returns True if connected, False if app is not running at all."""
|
||||||
|
if not self.is_process_running:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
_stub = get_stub()
|
||||||
|
if _stub:
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute_in_main_thread(cls, callback):
|
||||||
|
cls._main_thread_callbacks.append(callback)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
if self._started:
|
||||||
|
return
|
||||||
|
self.log.info("Started launch logic of AfterEffects")
|
||||||
|
self._started = True
|
||||||
|
self._start_process_timer.start()
|
||||||
|
|
||||||
|
def exit(self):
|
||||||
|
""" Exit whole application. """
|
||||||
|
if self._start_process_timer.isActive():
|
||||||
|
self._start_process_timer.stop()
|
||||||
|
if self._loop_timer.isActive():
|
||||||
|
self._loop_timer.stop()
|
||||||
|
|
||||||
|
if self._websocket_server is not None:
|
||||||
|
self._websocket_server.stop()
|
||||||
|
|
||||||
|
if self._process:
|
||||||
|
self._process.kill()
|
||||||
|
self._process.wait()
|
||||||
|
|
||||||
|
QtCore.QCoreApplication.exit()
|
||||||
|
|
||||||
|
def _on_loop_timer(self):
|
||||||
|
# TODO find better way and catch errors
|
||||||
|
# Run only callbacks that are in queue at the moment
|
||||||
|
cls = self.__class__
|
||||||
|
for _ in range(len(cls._main_thread_callbacks)):
|
||||||
|
if cls._main_thread_callbacks:
|
||||||
|
callback = cls._main_thread_callbacks.popleft()
|
||||||
|
callback()
|
||||||
|
|
||||||
|
if not self.is_process_running:
|
||||||
|
self.log.info("Host process is not running. Closing")
|
||||||
|
self.exit()
|
||||||
|
|
||||||
|
elif not self.websocket_server_is_running:
|
||||||
|
self.log.info("Websocket server is not running. Closing")
|
||||||
|
self.exit()
|
||||||
|
|
||||||
|
def _on_start_process_timer(self):
|
||||||
|
# TODO add try except validations for each part in this method
|
||||||
|
# Start server as first thing
|
||||||
|
if self._websocket_server is None:
|
||||||
|
self._init_server()
|
||||||
|
return
|
||||||
|
|
||||||
|
# TODO add waiting time
|
||||||
|
# Wait for webserver
|
||||||
|
if not self.websocket_server_is_running:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Start application process
|
||||||
|
if self._process is None:
|
||||||
|
self._start_process()
|
||||||
|
self.log.info("Waiting for host to connect")
|
||||||
|
return
|
||||||
|
|
||||||
|
# TODO add waiting time
|
||||||
|
# Wait until host is connected
|
||||||
|
if self.is_host_connected:
|
||||||
|
self._start_process_timer.stop()
|
||||||
|
self._loop_timer.start()
|
||||||
|
elif (
|
||||||
|
not self.is_process_running
|
||||||
|
or not self.websocket_server_is_running
|
||||||
|
):
|
||||||
|
self.exit()
|
||||||
|
|
||||||
|
def _init_server(self):
|
||||||
|
if self._websocket_server is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.log.debug(
|
||||||
|
"Initialization of websocket server for host communication"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._websocket_server = websocket_server = WebServerTool()
|
||||||
|
if websocket_server.port_occupied(
|
||||||
|
websocket_server.host_name,
|
||||||
|
websocket_server.port
|
||||||
|
):
|
||||||
|
self.log.info(
|
||||||
|
"Server already running, sending actual context and exit."
|
||||||
|
)
|
||||||
|
asyncio.run(websocket_server.send_context_change(self.route_name))
|
||||||
|
self.exit()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add Websocket route
|
||||||
|
websocket_server.add_route("*", "/ws/", WebSocketAsync)
|
||||||
|
# Add after effects route to websocket handler
|
||||||
|
|
||||||
|
print("Adding {} route".format(self.route_name))
|
||||||
|
WebSocketAsync.add_route(
|
||||||
|
self.route_name, AfterEffectsRoute
|
||||||
|
)
|
||||||
|
self.log.info("Starting websocket server for host communication")
|
||||||
|
websocket_server.start_server()
|
||||||
|
|
||||||
|
def _start_process(self):
|
||||||
|
if self._process is not None:
|
||||||
|
return
|
||||||
|
self.log.info("Starting host process")
|
||||||
|
try:
|
||||||
|
self._process = subprocess.Popen(
|
||||||
|
self._subprocess_args,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
self.log.info("exce", exc_info=True)
|
||||||
|
self.exit()
|
||||||
|
|
||||||
|
|
||||||
|
class AfterEffectsRoute(WebSocketRoute):
|
||||||
|
"""
|
||||||
|
One route, mimicking external application (like Harmony, etc).
|
||||||
|
All functions could be called from client.
|
||||||
|
'do_notify' function calls function on the client - mimicking
|
||||||
|
notification after long running job on the server or similar
|
||||||
|
"""
|
||||||
|
instance = None
|
||||||
|
|
||||||
|
def init(self, **kwargs):
|
||||||
|
# Python __init__ must be return "self".
|
||||||
|
# This method might return anything.
|
||||||
|
log.debug("someone called AfterEffects route")
|
||||||
|
self.instance = self
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
# server functions
|
||||||
|
async def ping(self):
|
||||||
|
log.debug("someone called AfterEffects route ping")
|
||||||
|
|
||||||
|
# This method calls function on the client side
|
||||||
|
# client functions
|
||||||
|
async def set_context(self, project, 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("aftereffects.read client calls server server calls "
|
||||||
|
"aftereffects client")
|
||||||
|
return await self.socket.call('aftereffects.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."""
|
||||||
|
|
||||||
|
partial_method = functools.partial(show_tool_by_name,
|
||||||
|
_tool_name)
|
||||||
|
|
||||||
|
ProcessLauncher.execute_in_main_thread(partial_method)
|
||||||
|
|
||||||
|
# Required return statement.
|
||||||
|
return "nothing"
|
||||||
71
openpype/hosts/aftereffects/api/lib.py
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import contextlib
|
||||||
|
import traceback
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from Qt import QtWidgets
|
||||||
|
|
||||||
|
from openpype.lib.remote_publish import headless_publish
|
||||||
|
|
||||||
|
from openpype.tools.utils import host_tools
|
||||||
|
from .launch_logic import ProcessLauncher, get_stub
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
def safe_excepthook(*args):
|
||||||
|
traceback.print_exception(*args)
|
||||||
|
|
||||||
|
|
||||||
|
def main(*subprocess_args):
|
||||||
|
sys.excepthook = safe_excepthook
|
||||||
|
|
||||||
|
import avalon.api
|
||||||
|
from openpype.hosts.aftereffects import api
|
||||||
|
|
||||||
|
avalon.api.install(api)
|
||||||
|
|
||||||
|
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"):
|
||||||
|
# reusing ConsoleTrayApp approach as it was already implemented
|
||||||
|
launcher.execute_in_main_thread(lambda: headless_publish(
|
||||||
|
log,
|
||||||
|
"CloseAE",
|
||||||
|
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(
|
||||||
|
lambda: host_tools.show_tool_by_name("workfiles", save=save)
|
||||||
|
)
|
||||||
|
|
||||||
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def maintained_selection():
|
||||||
|
"""Maintain selection during context."""
|
||||||
|
selection = get_stub().get_selected_items(True, False, False)
|
||||||
|
try:
|
||||||
|
yield selection
|
||||||
|
finally:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_extension_manifest_path():
|
||||||
|
return os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)),
|
||||||
|
"extension",
|
||||||
|
"CSXS",
|
||||||
|
"manifest.xml"
|
||||||
|
)
|
||||||
BIN
openpype/hosts/aftereffects/api/panel.PNG
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
openpype/hosts/aftereffects/api/panel_failure.PNG
Normal file
|
After Width: | Height: | Size: 13 KiB |
272
openpype/hosts/aftereffects/api/pipeline.py
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from Qt import QtWidgets
|
||||||
|
|
||||||
|
import pyblish.api
|
||||||
|
import avalon.api
|
||||||
|
from avalon import io, pipeline
|
||||||
|
|
||||||
|
from openpype import lib
|
||||||
|
from openpype.api import Logger
|
||||||
|
import openpype.hosts.aftereffects
|
||||||
|
|
||||||
|
from .launch_logic import get_stub
|
||||||
|
|
||||||
|
log = Logger.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
def check_inventory():
|
||||||
|
if not lib.any_outdated():
|
||||||
|
return
|
||||||
|
|
||||||
|
host = pyblish.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..")
|
||||||
|
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_()
|
||||||
|
|
||||||
|
|
||||||
|
def application_launch():
|
||||||
|
check_inventory()
|
||||||
|
|
||||||
|
|
||||||
|
def install():
|
||||||
|
print("Installing Pype config...")
|
||||||
|
|
||||||
|
pyblish.api.register_host("aftereffects")
|
||||||
|
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", 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 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
|
||||||
|
|
||||||
|
return {
|
||||||
|
"fps": fps,
|
||||||
|
"frameStart": frame_start,
|
||||||
|
"frameEnd": frame_end,
|
||||||
|
"handleStart": handle_start,
|
||||||
|
"handleEnd": handle_end,
|
||||||
|
"resolutionWidth": resolution_width,
|
||||||
|
"resolutionHeight": resolution_height,
|
||||||
|
"duration": duration
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def containerise(name,
|
||||||
|
namespace,
|
||||||
|
comp,
|
||||||
|
context,
|
||||||
|
loader=None,
|
||||||
|
suffix="_CON"):
|
||||||
|
"""
|
||||||
|
Containerisation enables a tracking of version, author and origin
|
||||||
|
for loaded assets.
|
||||||
|
|
||||||
|
Creates dictionary payloads that gets saved into file metadata. Each
|
||||||
|
container contains of who loaded (loader) and members (single or multiple
|
||||||
|
in case of background).
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
name (str): Name of resulting assembly
|
||||||
|
namespace (str): Namespace under which to host container
|
||||||
|
comp (Comp): Composition to containerise
|
||||||
|
context (dict): Asset information
|
||||||
|
loader (str, optional): Name of loader used to produce this container.
|
||||||
|
suffix (str, optional): Suffix of container, defaults to `_CON`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
container (str): Name of container assembly
|
||||||
|
"""
|
||||||
|
data = {
|
||||||
|
"schema": "openpype:container-2.0",
|
||||||
|
"id": pipeline.AVALON_CONTAINER_ID,
|
||||||
|
"name": name,
|
||||||
|
"namespace": namespace,
|
||||||
|
"loader": str(loader),
|
||||||
|
"representation": str(context["representation"]["_id"]),
|
||||||
|
"members": comp.members or [comp.id]
|
||||||
|
}
|
||||||
|
|
||||||
|
stub = get_stub()
|
||||||
|
stub.imprint(comp, data)
|
||||||
|
|
||||||
|
return comp
|
||||||
|
|
||||||
|
|
||||||
|
def _get_stub():
|
||||||
|
"""
|
||||||
|
Handle pulling stub from PS to run operations on host
|
||||||
|
Returns:
|
||||||
|
(AEServerStub) or None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
stub = get_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 ls():
|
||||||
|
"""Yields containers from active AfterEffects document.
|
||||||
|
|
||||||
|
This is the host-equivalent of api.ls(), but instead of listing
|
||||||
|
assets on disk, it lists assets already loaded in AE; once loaded
|
||||||
|
they are called 'containers'. Used in Manage tool.
|
||||||
|
|
||||||
|
Containers could be on multiple levels, single images/videos/was as a
|
||||||
|
FootageItem, or multiple items - backgrounds (folder with automatically
|
||||||
|
created composition and all imported layers).
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
dict: container
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
stub = get_stub() # only after AfterEffects is up
|
||||||
|
except lib.ConnectionNotEstablishedYet:
|
||||||
|
print("Not connected yet, ignoring")
|
||||||
|
return
|
||||||
|
|
||||||
|
layers_meta = stub.get_metadata()
|
||||||
|
for item in stub.get_items(comps=True,
|
||||||
|
folders=True,
|
||||||
|
footages=True):
|
||||||
|
data = stub.read(item, layers_meta)
|
||||||
|
# Skip non-tagged layers.
|
||||||
|
if not data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Filter to only containers.
|
||||||
|
if "container" not in data["id"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Append transient data
|
||||||
|
data["objectName"] = item.name.replace(stub.LOADED_ICON, '')
|
||||||
|
data["layer"] = item
|
||||||
|
yield data
|
||||||
|
|
||||||
|
|
||||||
|
def list_instances():
|
||||||
|
"""
|
||||||
|
List all created instances from current workfile which
|
||||||
|
will be published.
|
||||||
|
|
||||||
|
Pulls from File > File Info
|
||||||
|
|
||||||
|
For SubsetManager
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(list) of dictionaries matching instances format
|
||||||
|
"""
|
||||||
|
stub = _get_stub()
|
||||||
|
if not stub:
|
||||||
|
return []
|
||||||
|
|
||||||
|
instances = []
|
||||||
|
layers_meta = stub.get_metadata()
|
||||||
|
|
||||||
|
for instance in layers_meta:
|
||||||
|
if instance.get("schema") and \
|
||||||
|
"container" in instance.get("schema"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
uuid_val = instance.get("uuid")
|
||||||
|
if uuid_val:
|
||||||
|
instance['uuid'] = uuid_val
|
||||||
|
else:
|
||||||
|
instance['uuid'] = instance.get("members")[0] # legacy
|
||||||
|
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"))
|
||||||
|
item = stub.get_item(instance.get("uuid"))
|
||||||
|
if item:
|
||||||
|
stub.rename_item(item.id,
|
||||||
|
item.name.replace(stub.PUBLISH_ICON, ''))
|
||||||
9
openpype/hosts/aftereffects/api/plugin.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import avalon.api
|
||||||
|
from .launch_logic import get_stub
|
||||||
|
|
||||||
|
|
||||||
|
class AfterEffectsLoader(avalon.api.Loader):
|
||||||
|
@staticmethod
|
||||||
|
def get_stub():
|
||||||
|
return get_stub()
|
||||||
|
|
||||||
49
openpype/hosts/aftereffects/api/workio.py
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
"""Host API required Work Files tool"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .launch_logic import get_stub
|
||||||
|
from avalon import api
|
||||||
|
|
||||||
|
|
||||||
|
def _active_document():
|
||||||
|
document_name = get_stub().get_active_document_name()
|
||||||
|
if not document_name:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return document_name
|
||||||
|
|
||||||
|
|
||||||
|
def file_extensions():
|
||||||
|
return api.HOST_WORKFILE_EXTENSIONS["aftereffects"]
|
||||||
|
|
||||||
|
|
||||||
|
def has_unsaved_changes():
|
||||||
|
if _active_document():
|
||||||
|
return not get_stub().is_saved()
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def save_file(filepath):
|
||||||
|
get_stub().saveAs(filepath, True)
|
||||||
|
|
||||||
|
|
||||||
|
def open_file(filepath):
|
||||||
|
get_stub().open(filepath)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def current_file():
|
||||||
|
try:
|
||||||
|
full_name = get_stub().get_active_document_full_name()
|
||||||
|
if full_name and full_name != "null":
|
||||||
|
return os.path.normpath(full_name).replace("\\", "/")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def work_root(session):
|
||||||
|
return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/")
|
||||||
605
openpype/hosts/aftereffects/api/ws_stub.py
Normal file
|
|
@ -0,0 +1,605 @@
|
||||||
|
"""
|
||||||
|
Stub handling connection from server to client.
|
||||||
|
Used anywhere solution is calling client methods.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import attr
|
||||||
|
|
||||||
|
from wsrpc_aiohttp import WebSocketAsync
|
||||||
|
from avalon.tools.webserver.app import WebServerTool
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class AEItem(object):
|
||||||
|
"""
|
||||||
|
Object denoting Item in AE. Each item is created in AE by any Loader,
|
||||||
|
but contains same fields, which are being used in later processing.
|
||||||
|
"""
|
||||||
|
# metadata
|
||||||
|
id = attr.ib() # id created by AE, could be used for querying
|
||||||
|
name = attr.ib() # name of item
|
||||||
|
item_type = attr.ib(default=None) # item type (footage, folder, comp)
|
||||||
|
# all imported elements, single for
|
||||||
|
# regular image, array for Backgrounds
|
||||||
|
members = attr.ib(factory=list)
|
||||||
|
workAreaStart = attr.ib(default=None)
|
||||||
|
workAreaDuration = attr.ib(default=None)
|
||||||
|
frameRate = attr.ib(default=None)
|
||||||
|
file_name = attr.ib(default=None)
|
||||||
|
|
||||||
|
|
||||||
|
class AfterEffectsServerStub():
|
||||||
|
"""
|
||||||
|
Stub for calling function on client (Photoshop js) side.
|
||||||
|
Expects that client is already connected (started when avalon menu
|
||||||
|
is opened).
|
||||||
|
'self.websocketserver.call' is used as async wrapper
|
||||||
|
"""
|
||||||
|
PUBLISH_ICON = '\u2117 '
|
||||||
|
LOADED_ICON = '\u25bc'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.websocketserver = WebServerTool.get_instance()
|
||||||
|
self.client = self.get_client()
|
||||||
|
self.log = logging.getLogger(self.__class__.__name__)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_client():
|
||||||
|
"""
|
||||||
|
Return first connected client to WebSocket
|
||||||
|
TODO implement selection by Route
|
||||||
|
:return: <WebSocketAsync> client
|
||||||
|
"""
|
||||||
|
clients = WebSocketAsync.get_clients()
|
||||||
|
client = None
|
||||||
|
if len(clients) > 0:
|
||||||
|
key = list(clients.keys())[0]
|
||||||
|
client = clients.get(key)
|
||||||
|
|
||||||
|
return client
|
||||||
|
|
||||||
|
def open(self, path):
|
||||||
|
"""
|
||||||
|
Open file located at 'path' (local).
|
||||||
|
Args:
|
||||||
|
path(string): file path locally
|
||||||
|
Returns: None
|
||||||
|
"""
|
||||||
|
res = self.websocketserver.call(self.client.call
|
||||||
|
('AfterEffects.open', path=path))
|
||||||
|
|
||||||
|
return self._handle_return(res)
|
||||||
|
|
||||||
|
def get_metadata(self):
|
||||||
|
"""
|
||||||
|
Get complete stored JSON with metadata from AE.Metadata.Label
|
||||||
|
field.
|
||||||
|
|
||||||
|
It contains containers loaded by any Loader OR instances creted
|
||||||
|
by Creator.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(list)
|
||||||
|
"""
|
||||||
|
res = self.websocketserver.call(self.client.call
|
||||||
|
('AfterEffects.get_metadata'))
|
||||||
|
metadata = self._handle_return(res)
|
||||||
|
|
||||||
|
return metadata or []
|
||||||
|
|
||||||
|
def read(self, item, layers_meta=None):
|
||||||
|
"""
|
||||||
|
Parses item metadata from Label field of active document.
|
||||||
|
Used as filter to pick metadata for specific 'item' only.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item (AEItem): pulled info from AE
|
||||||
|
layers_meta (dict): full list from Headline
|
||||||
|
(load and inject for better performance in loops)
|
||||||
|
Returns:
|
||||||
|
(dict):
|
||||||
|
"""
|
||||||
|
if layers_meta is None:
|
||||||
|
layers_meta = self.get_metadata()
|
||||||
|
for item_meta in layers_meta:
|
||||||
|
if 'container' in item_meta.get('id') and \
|
||||||
|
str(item.id) == str(item_meta.get('members')[0]):
|
||||||
|
return item_meta
|
||||||
|
|
||||||
|
self.log.debug("Couldn't find layer metadata")
|
||||||
|
|
||||||
|
def imprint(self, item, data, all_items=None, items_meta=None):
|
||||||
|
"""
|
||||||
|
Save item metadata to Label field of metadata of active document
|
||||||
|
Args:
|
||||||
|
item (AEItem):
|
||||||
|
data(string): json representation for single layer
|
||||||
|
all_items (list of item): for performance, could be
|
||||||
|
injected for usage in loop, if not, single call will be
|
||||||
|
triggered
|
||||||
|
items_meta(string): json representation from Headline
|
||||||
|
(for performance - provide only if imprint is in
|
||||||
|
loop - value should be same)
|
||||||
|
Returns: None
|
||||||
|
"""
|
||||||
|
if not items_meta:
|
||||||
|
items_meta = self.get_metadata()
|
||||||
|
|
||||||
|
result_meta = []
|
||||||
|
# fix existing
|
||||||
|
is_new = True
|
||||||
|
|
||||||
|
for item_meta in items_meta:
|
||||||
|
if item_meta.get('members') \
|
||||||
|
and str(item.id) == str(item_meta.get('members')[0]):
|
||||||
|
is_new = False
|
||||||
|
if data:
|
||||||
|
item_meta.update(data)
|
||||||
|
result_meta.append(item_meta)
|
||||||
|
else:
|
||||||
|
result_meta.append(item_meta)
|
||||||
|
|
||||||
|
if is_new:
|
||||||
|
result_meta.append(data)
|
||||||
|
|
||||||
|
# Ensure only valid ids are stored.
|
||||||
|
if not all_items:
|
||||||
|
# loaders create FootageItem now
|
||||||
|
all_items = self.get_items(comps=True,
|
||||||
|
folders=True,
|
||||||
|
footages=True)
|
||||||
|
item_ids = [int(item.id) for item in all_items]
|
||||||
|
cleaned_data = []
|
||||||
|
for meta in result_meta:
|
||||||
|
# for creation of instance OR loaded container
|
||||||
|
if 'instance' in meta.get('id') or \
|
||||||
|
int(meta.get('members')[0]) in item_ids:
|
||||||
|
cleaned_data.append(meta)
|
||||||
|
|
||||||
|
payload = json.dumps(cleaned_data, indent=4)
|
||||||
|
|
||||||
|
res = self.websocketserver.call(self.client.call
|
||||||
|
('AfterEffects.imprint',
|
||||||
|
payload=payload))
|
||||||
|
return self._handle_return(res)
|
||||||
|
|
||||||
|
def get_active_document_full_name(self):
|
||||||
|
"""
|
||||||
|
Returns just a name of active document via ws call
|
||||||
|
Returns(string): file name
|
||||||
|
"""
|
||||||
|
res = self.websocketserver.call(self.client.call(
|
||||||
|
'AfterEffects.get_active_document_full_name'))
|
||||||
|
|
||||||
|
return self._handle_return(res)
|
||||||
|
|
||||||
|
def get_active_document_name(self):
|
||||||
|
"""
|
||||||
|
Returns just a name of active document via ws call
|
||||||
|
Returns(string): file name
|
||||||
|
"""
|
||||||
|
res = self.websocketserver.call(self.client.call(
|
||||||
|
'AfterEffects.get_active_document_name'))
|
||||||
|
|
||||||
|
return self._handle_return(res)
|
||||||
|
|
||||||
|
def get_items(self, comps, folders=False, footages=False):
|
||||||
|
"""
|
||||||
|
Get all items from Project panel according to arguments.
|
||||||
|
There are multiple different types:
|
||||||
|
CompItem (could have multiple layers - source for Creator,
|
||||||
|
will be rendered)
|
||||||
|
FolderItem (collection type, currently used for Background
|
||||||
|
loading)
|
||||||
|
FootageItem (imported file - created by Loader)
|
||||||
|
Args:
|
||||||
|
comps (bool): return CompItems
|
||||||
|
folders (bool): return FolderItem
|
||||||
|
footages (bool: return FootageItem
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(list) of namedtuples
|
||||||
|
"""
|
||||||
|
res = self.websocketserver.call(
|
||||||
|
self.client.call('AfterEffects.get_items',
|
||||||
|
comps=comps,
|
||||||
|
folders=folders,
|
||||||
|
footages=footages)
|
||||||
|
)
|
||||||
|
return self._to_records(self._handle_return(res))
|
||||||
|
|
||||||
|
def get_selected_items(self, comps, folders=False, footages=False):
|
||||||
|
"""
|
||||||
|
Same as get_items but using selected items only
|
||||||
|
Args:
|
||||||
|
comps (bool): return CompItems
|
||||||
|
folders (bool): return FolderItem
|
||||||
|
footages (bool: return FootageItem
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(list) of namedtuples
|
||||||
|
|
||||||
|
"""
|
||||||
|
res = self.websocketserver.call(self.client.call
|
||||||
|
('AfterEffects.get_selected_items',
|
||||||
|
comps=comps,
|
||||||
|
folders=folders,
|
||||||
|
footages=footages)
|
||||||
|
)
|
||||||
|
return self._to_records(self._handle_return(res))
|
||||||
|
|
||||||
|
def get_item(self, item_id):
|
||||||
|
"""
|
||||||
|
Returns metadata for particular 'item_id' or None
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item_id (int, or string)
|
||||||
|
"""
|
||||||
|
for item in self.get_items(True, True, True):
|
||||||
|
if str(item.id) == str(item_id):
|
||||||
|
return item
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def import_file(self, path, item_name, import_options=None):
|
||||||
|
"""
|
||||||
|
Imports file as a FootageItem. Used in Loader
|
||||||
|
Args:
|
||||||
|
path (string): absolute path for asset file
|
||||||
|
item_name (string): label for created FootageItem
|
||||||
|
import_options (dict): different files (img vs psd) need different
|
||||||
|
config
|
||||||
|
|
||||||
|
"""
|
||||||
|
res = self.websocketserver.call(
|
||||||
|
self.client.call('AfterEffects.import_file',
|
||||||
|
path=path,
|
||||||
|
item_name=item_name,
|
||||||
|
import_options=import_options)
|
||||||
|
)
|
||||||
|
records = self._to_records(self._handle_return(res))
|
||||||
|
if records:
|
||||||
|
return records.pop()
|
||||||
|
|
||||||
|
def replace_item(self, item_id, path, item_name):
|
||||||
|
""" Replace FootageItem with new file
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item_id (int):
|
||||||
|
path (string):absolute path
|
||||||
|
item_name (string): label on item in Project list
|
||||||
|
|
||||||
|
"""
|
||||||
|
res = self.websocketserver.call(self.client.call
|
||||||
|
('AfterEffects.replace_item',
|
||||||
|
item_id=item_id,
|
||||||
|
path=path, item_name=item_name))
|
||||||
|
|
||||||
|
return self._handle_return(res)
|
||||||
|
|
||||||
|
def rename_item(self, item_id, item_name):
|
||||||
|
""" Replace item with item_name
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item_id (int):
|
||||||
|
item_name (string): label on item in Project list
|
||||||
|
|
||||||
|
"""
|
||||||
|
res = self.websocketserver.call(self.client.call
|
||||||
|
('AfterEffects.rename_item',
|
||||||
|
item_id=item_id,
|
||||||
|
item_name=item_name))
|
||||||
|
|
||||||
|
return self._handle_return(res)
|
||||||
|
|
||||||
|
def delete_item(self, item_id):
|
||||||
|
""" Deletes *Item in a file
|
||||||
|
Args:
|
||||||
|
item_id (int):
|
||||||
|
|
||||||
|
"""
|
||||||
|
res = self.websocketserver.call(self.client.call
|
||||||
|
('AfterEffects.delete_item',
|
||||||
|
item_id=item_id))
|
||||||
|
|
||||||
|
return self._handle_return(res)
|
||||||
|
|
||||||
|
def remove_instance(self, instance_id):
|
||||||
|
"""
|
||||||
|
Removes instance with 'instance_id' from file's metadata and
|
||||||
|
saves them.
|
||||||
|
|
||||||
|
Keep matching item in file though.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
instance_id(string): instance uuid
|
||||||
|
"""
|
||||||
|
cleaned_data = []
|
||||||
|
|
||||||
|
for instance in self.get_metadata():
|
||||||
|
uuid_val = instance.get("uuid")
|
||||||
|
if not uuid_val:
|
||||||
|
uuid_val = instance.get("members")[0] # legacy
|
||||||
|
if uuid_val != instance_id:
|
||||||
|
cleaned_data.append(instance)
|
||||||
|
|
||||||
|
payload = json.dumps(cleaned_data, indent=4)
|
||||||
|
res = self.websocketserver.call(self.client.call
|
||||||
|
('AfterEffects.imprint',
|
||||||
|
payload=payload))
|
||||||
|
|
||||||
|
return self._handle_return(res)
|
||||||
|
|
||||||
|
def is_saved(self):
|
||||||
|
# TODO
|
||||||
|
return True
|
||||||
|
|
||||||
|
def set_label_color(self, item_id, color_idx):
|
||||||
|
"""
|
||||||
|
Used for highlight additional information in Project panel.
|
||||||
|
Green color is loaded asset, blue is created asset
|
||||||
|
Args:
|
||||||
|
item_id (int):
|
||||||
|
color_idx (int): 0-16 Label colors from AE Project view
|
||||||
|
"""
|
||||||
|
res = self.websocketserver.call(self.client.call
|
||||||
|
('AfterEffects.set_label_color',
|
||||||
|
item_id=item_id,
|
||||||
|
color_idx=color_idx))
|
||||||
|
|
||||||
|
return self._handle_return(res)
|
||||||
|
|
||||||
|
def get_work_area(self, item_id):
|
||||||
|
""" Get work are information for render purposes
|
||||||
|
Args:
|
||||||
|
item_id (int):
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(namedtuple)
|
||||||
|
|
||||||
|
"""
|
||||||
|
res = self.websocketserver.call(self.client.call
|
||||||
|
('AfterEffects.get_work_area',
|
||||||
|
item_id=item_id
|
||||||
|
))
|
||||||
|
|
||||||
|
records = self._to_records(self._handle_return(res))
|
||||||
|
if records:
|
||||||
|
return records.pop()
|
||||||
|
|
||||||
|
def set_work_area(self, item, start, duration, frame_rate):
|
||||||
|
"""
|
||||||
|
Set work area to predefined values (from Ftrack).
|
||||||
|
Work area directs what gets rendered.
|
||||||
|
Beware of rounding, AE expects seconds, not frames directly.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item (dict):
|
||||||
|
start (float): workAreaStart in seconds
|
||||||
|
duration (float): in seconds
|
||||||
|
frame_rate (float): frames in seconds
|
||||||
|
"""
|
||||||
|
res = self.websocketserver.call(self.client.call
|
||||||
|
('AfterEffects.set_work_area',
|
||||||
|
item_id=item.id,
|
||||||
|
start=start,
|
||||||
|
duration=duration,
|
||||||
|
frame_rate=frame_rate))
|
||||||
|
return self._handle_return(res)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
"""
|
||||||
|
Saves active document
|
||||||
|
Returns: None
|
||||||
|
"""
|
||||||
|
res = self.websocketserver.call(self.client.call
|
||||||
|
('AfterEffects.save'))
|
||||||
|
|
||||||
|
return self._handle_return(res)
|
||||||
|
|
||||||
|
def saveAs(self, project_path, as_copy):
|
||||||
|
"""
|
||||||
|
Saves active project to aep (copy) or png or jpg
|
||||||
|
Args:
|
||||||
|
project_path(string): full local path
|
||||||
|
as_copy: <boolean>
|
||||||
|
Returns: None
|
||||||
|
"""
|
||||||
|
res = self.websocketserver.call(self.client.call
|
||||||
|
('AfterEffects.saveAs',
|
||||||
|
image_path=project_path,
|
||||||
|
as_copy=as_copy))
|
||||||
|
|
||||||
|
return self._handle_return(res)
|
||||||
|
|
||||||
|
def get_render_info(self):
|
||||||
|
""" Get render queue info for render purposes
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(namedtuple): with 'file_name' field
|
||||||
|
"""
|
||||||
|
res = self.websocketserver.call(self.client.call
|
||||||
|
('AfterEffects.get_render_info'))
|
||||||
|
|
||||||
|
records = self._to_records(self._handle_return(res))
|
||||||
|
if records:
|
||||||
|
return records.pop()
|
||||||
|
|
||||||
|
def get_audio_url(self, item_id):
|
||||||
|
""" Get audio layer absolute url for comp
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item_id (int): composition id
|
||||||
|
Returns:
|
||||||
|
(str): absolute path url
|
||||||
|
"""
|
||||||
|
res = self.websocketserver.call(self.client.call
|
||||||
|
('AfterEffects.get_audio_url',
|
||||||
|
item_id=item_id))
|
||||||
|
|
||||||
|
return self._handle_return(res)
|
||||||
|
|
||||||
|
def import_background(self, comp_id, comp_name, files):
|
||||||
|
"""
|
||||||
|
Imports backgrounds images to existing or new composition.
|
||||||
|
|
||||||
|
If comp_id is not provided, new composition is created, basic
|
||||||
|
values (width, heights, frameRatio) takes from first imported
|
||||||
|
image.
|
||||||
|
|
||||||
|
All images from background json are imported as a FootageItem and
|
||||||
|
separate layer is created for each of them under composition.
|
||||||
|
|
||||||
|
Order of imported 'files' is important.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
comp_id (int): id of existing composition (null if new)
|
||||||
|
comp_name (str): used when new composition
|
||||||
|
files (list): list of absolute paths to import and
|
||||||
|
add as layers
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(AEItem): object with id of created folder, all imported images
|
||||||
|
"""
|
||||||
|
res = self.websocketserver.call(self.client.call
|
||||||
|
('AfterEffects.import_background',
|
||||||
|
comp_id=comp_id,
|
||||||
|
comp_name=comp_name,
|
||||||
|
files=files))
|
||||||
|
|
||||||
|
records = self._to_records(self._handle_return(res))
|
||||||
|
if records:
|
||||||
|
return records.pop()
|
||||||
|
|
||||||
|
def reload_background(self, comp_id, comp_name, files):
|
||||||
|
"""
|
||||||
|
Reloads backgrounds images to existing composition.
|
||||||
|
|
||||||
|
It actually deletes complete folder with imported images and
|
||||||
|
created composition for safety.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
comp_id (int): id of existing composition to be overwritten
|
||||||
|
comp_name (str): new name of composition (could be same as old
|
||||||
|
if version up only)
|
||||||
|
files (list): list of absolute paths to import and
|
||||||
|
add as layers
|
||||||
|
Returns:
|
||||||
|
(AEItem): object with id of created folder, all imported images
|
||||||
|
"""
|
||||||
|
res = self.websocketserver.call(self.client.call
|
||||||
|
('AfterEffects.reload_background',
|
||||||
|
comp_id=comp_id,
|
||||||
|
comp_name=comp_name,
|
||||||
|
files=files))
|
||||||
|
|
||||||
|
records = self._to_records(self._handle_return(res))
|
||||||
|
if records:
|
||||||
|
return records.pop()
|
||||||
|
|
||||||
|
def add_item_as_layer(self, comp_id, item_id):
|
||||||
|
"""
|
||||||
|
Adds already imported FootageItem ('item_id') as a new
|
||||||
|
layer to composition ('comp_id').
|
||||||
|
|
||||||
|
Args:
|
||||||
|
comp_id (int): id of target composition
|
||||||
|
item_id (int): FootageItem.id
|
||||||
|
comp already found previously
|
||||||
|
"""
|
||||||
|
res = self.websocketserver.call(self.client.call
|
||||||
|
('AfterEffects.add_item_as_layer',
|
||||||
|
comp_id=comp_id,
|
||||||
|
item_id=item_id))
|
||||||
|
|
||||||
|
records = self._to_records(self._handle_return(res))
|
||||||
|
if records:
|
||||||
|
return records.pop()
|
||||||
|
|
||||||
|
def render(self, folder_url):
|
||||||
|
"""
|
||||||
|
Render all renderqueueitem to 'folder_url'
|
||||||
|
Args:
|
||||||
|
folder_url(string): local folder path for collecting
|
||||||
|
Returns: None
|
||||||
|
"""
|
||||||
|
res = self.websocketserver.call(self.client.call
|
||||||
|
('AfterEffects.render',
|
||||||
|
folder_url=folder_url))
|
||||||
|
return self._handle_return(res)
|
||||||
|
|
||||||
|
def get_extension_version(self):
|
||||||
|
"""Returns version number of installed extension."""
|
||||||
|
res = self.websocketserver.call(self.client.call(
|
||||||
|
'AfterEffects.get_extension_version'))
|
||||||
|
|
||||||
|
return self._handle_return(res)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
res = self.websocketserver.call(self.client.call('AfterEffects.close'))
|
||||||
|
|
||||||
|
return self._handle_return(res)
|
||||||
|
|
||||||
|
def _handle_return(self, res):
|
||||||
|
"""Wraps return, throws ValueError if 'error' key is present."""
|
||||||
|
if res and isinstance(res, str) and res != "undefined":
|
||||||
|
try:
|
||||||
|
parsed = json.loads(res)
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
raise ValueError("Received broken JSON {}".format(res))
|
||||||
|
|
||||||
|
if not parsed: # empty list
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
first_item = parsed
|
||||||
|
if isinstance(parsed, list):
|
||||||
|
first_item = parsed[0]
|
||||||
|
|
||||||
|
if first_item:
|
||||||
|
if first_item.get("error"):
|
||||||
|
raise ValueError(first_item["error"])
|
||||||
|
# singular values (file name etc)
|
||||||
|
if first_item.get("result") is not None:
|
||||||
|
return first_item["result"]
|
||||||
|
return parsed # parsed
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _to_records(self, payload):
|
||||||
|
"""
|
||||||
|
Converts string json representation into list of AEItem
|
||||||
|
dot notation access to work.
|
||||||
|
Returns: <list of AEItem>
|
||||||
|
payload(dict): - dictionary from json representation, expected to
|
||||||
|
come from _handle_return
|
||||||
|
"""
|
||||||
|
if not payload:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if isinstance(payload, str): # safety fallback
|
||||||
|
try:
|
||||||
|
payload = json.loads(payload)
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
raise ValueError("Received broken JSON {}".format(payload))
|
||||||
|
|
||||||
|
if isinstance(payload, dict):
|
||||||
|
payload = [payload]
|
||||||
|
|
||||||
|
ret = []
|
||||||
|
# convert to AEItem to use dot donation
|
||||||
|
for d in payload:
|
||||||
|
if not d:
|
||||||
|
continue
|
||||||
|
# currently implemented and expected fields
|
||||||
|
item = AEItem(d.get('id'),
|
||||||
|
d.get('name'),
|
||||||
|
d.get('type'),
|
||||||
|
d.get('members'),
|
||||||
|
d.get('workAreaStart'),
|
||||||
|
d.get('workAreaDuration'),
|
||||||
|
d.get('frameRate'),
|
||||||
|
d.get('file_name'))
|
||||||
|
|
||||||
|
ret.append(item)
|
||||||
|
return ret
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
from openpype.hosts.aftereffects.plugins.create import create_render
|
from openpype.hosts.aftereffects.plugins.create import create_render
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class CreateLocalRender(create_render.CreateRender):
|
class CreateLocalRender(create_render.CreateRender):
|
||||||
""" Creator to render locally.
|
""" Creator to render locally.
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
from avalon import aftereffects
|
|
||||||
from avalon.api import CreatorError
|
from avalon.api import CreatorError
|
||||||
|
|
||||||
import openpype.api
|
import openpype.api
|
||||||
|
from openpype.hosts.aftereffects.api import (
|
||||||
import logging
|
get_stub,
|
||||||
|
list_instances
|
||||||
log = logging.getLogger(__name__)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CreateRender(openpype.api.Creator):
|
class CreateRender(openpype.api.Creator):
|
||||||
|
|
@ -22,22 +21,27 @@ class CreateRender(openpype.api.Creator):
|
||||||
family = "render"
|
family = "render"
|
||||||
|
|
||||||
def process(self):
|
def process(self):
|
||||||
stub = aftereffects.stub() # only after After Effects is up
|
stub = get_stub() # only after After Effects is up
|
||||||
if (self.options or {}).get("useSelection"):
|
if (self.options or {}).get("useSelection"):
|
||||||
items = stub.get_selected_items(comps=True,
|
items = stub.get_selected_items(
|
||||||
folders=False,
|
comps=True, folders=False, footages=False
|
||||||
footages=False)
|
)
|
||||||
if len(items) > 1:
|
if len(items) > 1:
|
||||||
raise CreatorError("Please select only single "
|
raise CreatorError(
|
||||||
"composition at time.")
|
"Please select only single composition at time."
|
||||||
|
)
|
||||||
|
|
||||||
if not items:
|
if not items:
|
||||||
raise CreatorError("Nothing to create. Select composition " +
|
raise CreatorError((
|
||||||
"if 'useSelection' or create at least " +
|
"Nothing to create. Select composition "
|
||||||
"one composition.")
|
"if 'useSelection' or create at least "
|
||||||
|
"one composition."
|
||||||
|
))
|
||||||
|
|
||||||
existing_subsets = [instance['subset'].lower()
|
existing_subsets = [
|
||||||
for instance in aftereffects.list_instances()]
|
instance['subset'].lower()
|
||||||
|
for instance in list_instances()
|
||||||
|
]
|
||||||
|
|
||||||
item = items.pop()
|
item = items.pop()
|
||||||
if self.name.lower() in existing_subsets:
|
if self.name.lower() in existing_subsets:
|
||||||
|
|
@ -46,9 +50,11 @@ class CreateRender(openpype.api.Creator):
|
||||||
|
|
||||||
self.data["members"] = [item.id]
|
self.data["members"] = [item.id]
|
||||||
self.data["uuid"] = item.id # for SubsetManager
|
self.data["uuid"] = item.id # for SubsetManager
|
||||||
self.data["subset"] = self.data["subset"]\
|
self.data["subset"] = (
|
||||||
.replace(stub.PUBLISH_ICON, '')\
|
self.data["subset"]
|
||||||
|
.replace(stub.PUBLISH_ICON, '')
|
||||||
.replace(stub.LOADED_ICON, '')
|
.replace(stub.LOADED_ICON, '')
|
||||||
|
)
|
||||||
|
|
||||||
stub.imprint(item, self.data)
|
stub.imprint(item, self.data)
|
||||||
stub.set_label_color(item.id, 14) # Cyan options 0 - 16
|
stub.set_label_color(item.id, 14) # Cyan options 0 - 16
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,18 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from avalon import api, aftereffects
|
import avalon.api
|
||||||
|
|
||||||
from openpype.lib import get_background_layers, get_unique_layer_name
|
from openpype.lib import (
|
||||||
|
get_background_layers,
|
||||||
stub = aftereffects.stub()
|
get_unique_layer_name
|
||||||
|
)
|
||||||
|
from openpype.hosts.aftereffects.api import (
|
||||||
|
AfterEffectsLoader,
|
||||||
|
containerise
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BackgroundLoader(api.Loader):
|
class BackgroundLoader(AfterEffectsLoader):
|
||||||
"""
|
"""
|
||||||
Load images from Background family
|
Load images from Background family
|
||||||
Creates for each background separate folder with all imported images
|
Creates for each background separate folder with all imported images
|
||||||
|
|
@ -21,6 +26,7 @@ class BackgroundLoader(api.Loader):
|
||||||
representations = ["json"]
|
representations = ["json"]
|
||||||
|
|
||||||
def load(self, context, name=None, namespace=None, data=None):
|
def load(self, context, name=None, namespace=None, data=None):
|
||||||
|
stub = self.get_stub()
|
||||||
items = stub.get_items(comps=True)
|
items = stub.get_items(comps=True)
|
||||||
existing_items = [layer.name.replace(stub.LOADED_ICON, '')
|
existing_items = [layer.name.replace(stub.LOADED_ICON, '')
|
||||||
for layer in items]
|
for layer in items]
|
||||||
|
|
@ -43,7 +49,7 @@ class BackgroundLoader(api.Loader):
|
||||||
self[:] = [comp]
|
self[:] = [comp]
|
||||||
namespace = namespace or comp_name
|
namespace = namespace or comp_name
|
||||||
|
|
||||||
return aftereffects.containerise(
|
return containerise(
|
||||||
name,
|
name,
|
||||||
namespace,
|
namespace,
|
||||||
comp,
|
comp,
|
||||||
|
|
@ -53,6 +59,7 @@ class BackgroundLoader(api.Loader):
|
||||||
|
|
||||||
def update(self, container, representation):
|
def update(self, container, representation):
|
||||||
""" Switch asset or change version """
|
""" Switch asset or change version """
|
||||||
|
stub = self.get_stub()
|
||||||
context = representation.get("context", {})
|
context = representation.get("context", {})
|
||||||
_ = container.pop("layer")
|
_ = container.pop("layer")
|
||||||
|
|
||||||
|
|
@ -71,7 +78,7 @@ class BackgroundLoader(api.Loader):
|
||||||
else: # switching version - keep same name
|
else: # switching version - keep same name
|
||||||
comp_name = container["namespace"]
|
comp_name = container["namespace"]
|
||||||
|
|
||||||
path = api.get_representation_path(representation)
|
path = avalon.api.get_representation_path(representation)
|
||||||
|
|
||||||
layers = get_background_layers(path)
|
layers = get_background_layers(path)
|
||||||
comp = stub.reload_background(container["members"][1],
|
comp = stub.reload_background(container["members"][1],
|
||||||
|
|
@ -94,6 +101,7 @@ class BackgroundLoader(api.Loader):
|
||||||
container (dict): container to be removed - used to get layer_id
|
container (dict): container to be removed - used to get layer_id
|
||||||
"""
|
"""
|
||||||
print("!!!! container:: {}".format(container))
|
print("!!!! container:: {}".format(container))
|
||||||
|
stub = self.get_stub()
|
||||||
layer = container.pop("layer")
|
layer = container.pop("layer")
|
||||||
stub.imprint(layer, {})
|
stub.imprint(layer, {})
|
||||||
stub.delete_item(layer.id)
|
stub.delete_item(layer.id)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
from avalon import api, aftereffects
|
|
||||||
from openpype import lib
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
stub = aftereffects.stub()
|
import avalon.api
|
||||||
|
from openpype import lib
|
||||||
|
|
||||||
|
from openpype.hosts.aftereffects.api import (
|
||||||
|
AfterEffectsLoader,
|
||||||
|
containerise
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FileLoader(api.Loader):
|
class FileLoader(AfterEffectsLoader):
|
||||||
"""Load images
|
"""Load images
|
||||||
|
|
||||||
Stores the imported asset in a container named after the asset.
|
Stores the imported asset in a container named after the asset.
|
||||||
|
|
@ -21,6 +25,7 @@ class FileLoader(api.Loader):
|
||||||
representations = ["*"]
|
representations = ["*"]
|
||||||
|
|
||||||
def load(self, context, name=None, namespace=None, data=None):
|
def load(self, context, name=None, namespace=None, data=None):
|
||||||
|
stub = self.get_stub()
|
||||||
layers = stub.get_items(comps=True, folders=True, footages=True)
|
layers = stub.get_items(comps=True, folders=True, footages=True)
|
||||||
existing_layers = [layer.name for layer in layers]
|
existing_layers = [layer.name for layer in layers]
|
||||||
comp_name = lib.get_unique_layer_name(
|
comp_name = lib.get_unique_layer_name(
|
||||||
|
|
@ -60,7 +65,7 @@ class FileLoader(api.Loader):
|
||||||
self[:] = [comp]
|
self[:] = [comp]
|
||||||
namespace = namespace or comp_name
|
namespace = namespace or comp_name
|
||||||
|
|
||||||
return aftereffects.containerise(
|
return containerise(
|
||||||
name,
|
name,
|
||||||
namespace,
|
namespace,
|
||||||
comp,
|
comp,
|
||||||
|
|
@ -70,6 +75,7 @@ class FileLoader(api.Loader):
|
||||||
|
|
||||||
def update(self, container, representation):
|
def update(self, container, representation):
|
||||||
""" Switch asset or change version """
|
""" Switch asset or change version """
|
||||||
|
stub = self.get_stub()
|
||||||
layer = container.pop("layer")
|
layer = container.pop("layer")
|
||||||
|
|
||||||
context = representation.get("context", {})
|
context = representation.get("context", {})
|
||||||
|
|
@ -86,7 +92,7 @@ class FileLoader(api.Loader):
|
||||||
"{}_{}".format(context["asset"], context["subset"]))
|
"{}_{}".format(context["asset"], context["subset"]))
|
||||||
else: # switching version - keep same name
|
else: # switching version - keep same name
|
||||||
layer_name = container["namespace"]
|
layer_name = container["namespace"]
|
||||||
path = api.get_representation_path(representation)
|
path = avalon.api.get_representation_path(representation)
|
||||||
# with aftereffects.maintained_selection(): # TODO
|
# with aftereffects.maintained_selection(): # TODO
|
||||||
stub.replace_item(layer.id, path, stub.LOADED_ICON + layer_name)
|
stub.replace_item(layer.id, path, stub.LOADED_ICON + layer_name)
|
||||||
stub.imprint(
|
stub.imprint(
|
||||||
|
|
@ -101,6 +107,7 @@ class FileLoader(api.Loader):
|
||||||
Args:
|
Args:
|
||||||
container (dict): container to be removed - used to get layer_id
|
container (dict): container to be removed - used to get layer_id
|
||||||
"""
|
"""
|
||||||
|
stub = self.get_stub()
|
||||||
layer = container.pop("layer")
|
layer = container.pop("layer")
|
||||||
stub.imprint(layer, {})
|
stub.imprint(layer, {})
|
||||||
stub.delete_item(layer.id)
|
stub.delete_item(layer.id)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import pyblish.api
|
import pyblish.api
|
||||||
|
|
||||||
from avalon import aftereffects
|
from openpype.hosts.aftereffects.api import get_stub
|
||||||
|
|
||||||
|
|
||||||
class AddPublishHighlight(pyblish.api.InstancePlugin):
|
class AddPublishHighlight(pyblish.api.InstancePlugin):
|
||||||
|
|
@ -15,7 +15,7 @@ class AddPublishHighlight(pyblish.api.InstancePlugin):
|
||||||
optional = True
|
optional = True
|
||||||
|
|
||||||
def process(self, instance):
|
def process(self, instance):
|
||||||
stub = aftereffects.stub()
|
stub = get_stub()
|
||||||
item = instance.data
|
item = instance.data
|
||||||
# comp name contains highlight icon
|
# comp name contains highlight icon
|
||||||
stub.rename_item(item["comp_id"], item["comp_name"])
|
stub.rename_item(item["comp_id"], item["comp_name"])
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"""Close AE after publish. For Webpublishing only."""
|
"""Close AE after publish. For Webpublishing only."""
|
||||||
import pyblish.api
|
import pyblish.api
|
||||||
|
|
||||||
from avalon import aftereffects
|
from openpype.hosts.aftereffects.api import get_stub
|
||||||
|
|
||||||
|
|
||||||
class CloseAE(pyblish.api.ContextPlugin):
|
class CloseAE(pyblish.api.ContextPlugin):
|
||||||
|
|
@ -20,7 +20,7 @@ class CloseAE(pyblish.api.ContextPlugin):
|
||||||
def process(self, context):
|
def process(self, context):
|
||||||
self.log.info("CloseAE")
|
self.log.info("CloseAE")
|
||||||
|
|
||||||
stub = aftereffects.stub()
|
stub = get_stub()
|
||||||
self.log.info("Shutting down AE")
|
self.log.info("Shutting down AE")
|
||||||
stub.save()
|
stub.save()
|
||||||
stub.close()
|
stub.close()
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import os
|
||||||
|
|
||||||
import pyblish.api
|
import pyblish.api
|
||||||
|
|
||||||
from avalon import aftereffects
|
from openpype.hosts.aftereffects.api import get_stub
|
||||||
|
|
||||||
|
|
||||||
class CollectAudio(pyblish.api.ContextPlugin):
|
class CollectAudio(pyblish.api.ContextPlugin):
|
||||||
|
|
@ -21,7 +21,8 @@ class CollectAudio(pyblish.api.ContextPlugin):
|
||||||
comp_id = instance.data["comp_id"]
|
comp_id = instance.data["comp_id"]
|
||||||
if not comp_id:
|
if not comp_id:
|
||||||
self.log.debug("No comp_id filled in instance")
|
self.log.debug("No comp_id filled in instance")
|
||||||
|
# @iLLiCiTiT QUESTION Should return or continue?
|
||||||
return
|
return
|
||||||
context.data["audioFile"] = os.path.normpath(
|
context.data["audioFile"] = os.path.normpath(
|
||||||
aftereffects.stub().get_audio_url(comp_id)
|
get_stub().get_audio_url(comp_id)
|
||||||
).replace("\\", "/")
|
).replace("\\", "/")
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import os
|
||||||
|
|
||||||
import pyblish.api
|
import pyblish.api
|
||||||
|
|
||||||
from avalon import aftereffects
|
from openpype.hosts.aftereffects.api import get_stub
|
||||||
|
|
||||||
|
|
||||||
class CollectCurrentFile(pyblish.api.ContextPlugin):
|
class CollectCurrentFile(pyblish.api.ContextPlugin):
|
||||||
|
|
@ -14,5 +14,5 @@ class CollectCurrentFile(pyblish.api.ContextPlugin):
|
||||||
|
|
||||||
def process(self, context):
|
def process(self, context):
|
||||||
context.data["currentFile"] = os.path.normpath(
|
context.data["currentFile"] = os.path.normpath(
|
||||||
aftereffects.stub().get_active_document_full_name()
|
get_stub().get_active_document_full_name()
|
||||||
).replace("\\", "/")
|
).replace("\\", "/")
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@ import os
|
||||||
import re
|
import re
|
||||||
import pyblish.api
|
import pyblish.api
|
||||||
|
|
||||||
from avalon import aftereffects
|
from openpype.hosts.aftereffects.api import (
|
||||||
|
get_stub,
|
||||||
|
get_extension_manifest_path
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CollectExtensionVersion(pyblish.api.ContextPlugin):
|
class CollectExtensionVersion(pyblish.api.ContextPlugin):
|
||||||
|
|
@ -27,13 +30,12 @@ class CollectExtensionVersion(pyblish.api.ContextPlugin):
|
||||||
active = True
|
active = True
|
||||||
|
|
||||||
def process(self, context):
|
def process(self, context):
|
||||||
installed_version = aftereffects.stub().get_extension_version()
|
installed_version = get_stub().get_extension_version()
|
||||||
|
|
||||||
if not installed_version:
|
if not installed_version:
|
||||||
raise ValueError("Unknown version, probably old extension")
|
raise ValueError("Unknown version, probably old extension")
|
||||||
|
|
||||||
manifest_url = os.path.join(os.path.dirname(aftereffects.__file__),
|
manifest_url = get_extension_manifest_path()
|
||||||
"extension", "CSXS", "manifest.xml")
|
|
||||||
|
|
||||||
if not os.path.exists(manifest_url):
|
if not os.path.exists(manifest_url):
|
||||||
self.log.debug("Unable to locate extension manifest, not checking")
|
self.log.debug("Unable to locate extension manifest, not checking")
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import attr
|
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import attr
|
||||||
|
|
||||||
from avalon import aftereffects
|
from avalon import aftereffects
|
||||||
import pyblish.api
|
import pyblish.api
|
||||||
|
|
@ -10,6 +10,8 @@ from openpype.settings import get_project_settings
|
||||||
from openpype.lib import abstract_collect_render
|
from openpype.lib import abstract_collect_render
|
||||||
from openpype.lib.abstract_collect_render import RenderInstance
|
from openpype.lib.abstract_collect_render import RenderInstance
|
||||||
|
|
||||||
|
from openpype.hosts.aftereffects.api import get_stub
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
class AERenderInstance(RenderInstance):
|
class AERenderInstance(RenderInstance):
|
||||||
|
|
@ -35,7 +37,7 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender):
|
||||||
padding_width = 6
|
padding_width = 6
|
||||||
rendered_extension = 'png'
|
rendered_extension = 'png'
|
||||||
|
|
||||||
stub = aftereffects.stub()
|
stub = get_stub()
|
||||||
|
|
||||||
def get_instances(self, context):
|
def get_instances(self, context):
|
||||||
instances = []
|
instances = []
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import os
|
import os
|
||||||
import six
|
|
||||||
import sys
|
import sys
|
||||||
|
import six
|
||||||
|
|
||||||
import openpype.api
|
import openpype.api
|
||||||
from avalon import aftereffects
|
from openpype.hosts.aftereffects.api import get_stub
|
||||||
|
|
||||||
|
|
||||||
class ExtractLocalRender(openpype.api.Extractor):
|
class ExtractLocalRender(openpype.api.Extractor):
|
||||||
|
|
@ -15,7 +15,7 @@ class ExtractLocalRender(openpype.api.Extractor):
|
||||||
families = ["render"]
|
families = ["render"]
|
||||||
|
|
||||||
def process(self, instance):
|
def process(self, instance):
|
||||||
stub = aftereffects.stub()
|
stub = get_stub()
|
||||||
staging_dir = instance.data["stagingDir"]
|
staging_dir = instance.data["stagingDir"]
|
||||||
self.log.info("staging_dir::{}".format(staging_dir))
|
self.log.info("staging_dir::{}".format(staging_dir))
|
||||||
|
|
||||||
|
|
@ -55,8 +55,7 @@ class ExtractLocalRender(openpype.api.Extractor):
|
||||||
|
|
||||||
ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg")
|
ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg")
|
||||||
# Generate thumbnail.
|
# Generate thumbnail.
|
||||||
thumbnail_path = os.path.join(staging_dir,
|
thumbnail_path = os.path.join(staging_dir, "thumbnail.jpg")
|
||||||
"thumbnail.jpg")
|
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
ffmpeg_path, "-y",
|
ffmpeg_path, "-y",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import openpype.api
|
import openpype.api
|
||||||
from avalon import aftereffects
|
from openpype.hosts.aftereffects.api import get_stub
|
||||||
|
|
||||||
|
|
||||||
class ExtractSaveScene(openpype.api.Extractor):
|
class ExtractSaveScene(openpype.api.Extractor):
|
||||||
|
|
@ -11,5 +11,5 @@ class ExtractSaveScene(openpype.api.Extractor):
|
||||||
families = ["workfile"]
|
families = ["workfile"]
|
||||||
|
|
||||||
def process(self, instance):
|
def process(self, instance):
|
||||||
stub = aftereffects.stub()
|
stub = get_stub()
|
||||||
stub.save()
|
stub.save()
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import pyblish.api
|
||||||
from openpype.action import get_errored_plugins_from_data
|
from openpype.action import get_errored_plugins_from_data
|
||||||
from openpype.lib import version_up
|
from openpype.lib import version_up
|
||||||
|
|
||||||
from avalon import aftereffects
|
from openpype.hosts.aftereffects.api import get_stub
|
||||||
|
|
||||||
|
|
||||||
class IncrementWorkfile(pyblish.api.InstancePlugin):
|
class IncrementWorkfile(pyblish.api.InstancePlugin):
|
||||||
|
|
@ -25,6 +25,6 @@ class IncrementWorkfile(pyblish.api.InstancePlugin):
|
||||||
)
|
)
|
||||||
|
|
||||||
scene_path = version_up(instance.context.data["currentFile"])
|
scene_path = version_up(instance.context.data["currentFile"])
|
||||||
aftereffects.stub().saveAs(scene_path, True)
|
get_stub().saveAs(scene_path, True)
|
||||||
|
|
||||||
self.log.info("Incremented workfile to: {}".format(scene_path))
|
self.log.info("Incremented workfile to: {}".format(scene_path))
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import openpype.api
|
import openpype.api
|
||||||
from avalon import aftereffects
|
from openpype.hosts.aftereffects.api import get_stub
|
||||||
|
|
||||||
|
|
||||||
class RemovePublishHighlight(openpype.api.Extractor):
|
class RemovePublishHighlight(openpype.api.Extractor):
|
||||||
|
|
@ -16,7 +16,7 @@ class RemovePublishHighlight(openpype.api.Extractor):
|
||||||
families = ["render.farm"]
|
families = ["render.farm"]
|
||||||
|
|
||||||
def process(self, instance):
|
def process(self, instance):
|
||||||
stub = aftereffects.stub()
|
stub = get_stub()
|
||||||
self.log.debug("instance::{}".format(instance.data))
|
self.log.debug("instance::{}".format(instance.data))
|
||||||
item = instance.data
|
item = instance.data
|
||||||
comp_name = item["comp_name"].replace(stub.PUBLISH_ICON, '')
|
comp_name = item["comp_name"].replace(stub.PUBLISH_ICON, '')
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from avalon import api
|
from avalon import api
|
||||||
import pyblish.api
|
import pyblish.api
|
||||||
import openpype.api
|
import openpype.api
|
||||||
from avalon import aftereffects
|
from openpype.hosts.aftereffects.api import get_stub
|
||||||
|
|
||||||
|
|
||||||
class ValidateInstanceAssetRepair(pyblish.api.Action):
|
class ValidateInstanceAssetRepair(pyblish.api.Action):
|
||||||
|
|
@ -22,7 +22,7 @@ class ValidateInstanceAssetRepair(pyblish.api.Action):
|
||||||
|
|
||||||
# Apply pyblish.logic to get the instances for the plug-in
|
# Apply pyblish.logic to get the instances for the plug-in
|
||||||
instances = pyblish.api.instances_by_plugin(failed, plugin)
|
instances = pyblish.api.instances_by_plugin(failed, plugin)
|
||||||
stub = aftereffects.stub()
|
stub = get_stub()
|
||||||
for instance in instances:
|
for instance in instances:
|
||||||
data = stub.read(instance[0])
|
data = stub.read(instance[0])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,7 @@ import re
|
||||||
|
|
||||||
import pyblish.api
|
import pyblish.api
|
||||||
|
|
||||||
from avalon import aftereffects
|
from openpype.hosts.aftereffects.api import get_asset_settings
|
||||||
|
|
||||||
import openpype.hosts.aftereffects.api as api
|
|
||||||
|
|
||||||
stub = aftereffects.stub()
|
|
||||||
|
|
||||||
|
|
||||||
class ValidateSceneSettings(pyblish.api.InstancePlugin):
|
class ValidateSceneSettings(pyblish.api.InstancePlugin):
|
||||||
|
|
@ -47,7 +43,7 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin):
|
||||||
resolutionWidth
|
resolutionWidth
|
||||||
resolutionHeight
|
resolutionHeight
|
||||||
TODO support in extension is missing for now
|
TODO support in extension is missing for now
|
||||||
|
|
||||||
By defaults validates duration (how many frames should be published)
|
By defaults validates duration (how many frames should be published)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -62,7 +58,7 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin):
|
||||||
|
|
||||||
def process(self, instance):
|
def process(self, instance):
|
||||||
"""Plugin entry point."""
|
"""Plugin entry point."""
|
||||||
expected_settings = api.get_asset_settings()
|
expected_settings = get_asset_settings()
|
||||||
self.log.info("config from DB::{}".format(expected_settings))
|
self.log.info("config from DB::{}".format(expected_settings))
|
||||||
|
|
||||||
if any(re.search(pattern, os.getenv('AVALON_TASK'))
|
if any(re.search(pattern, os.getenv('AVALON_TASK'))
|
||||||
|
|
|
||||||
|
|
@ -168,9 +168,13 @@ from .editorial import (
|
||||||
make_sequence_collection
|
make_sequence_collection
|
||||||
)
|
)
|
||||||
|
|
||||||
from .pype_info import (
|
from .openpype_version import (
|
||||||
get_openpype_version,
|
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
|
terminal = Terminal
|
||||||
|
|
@ -304,4 +308,8 @@ __all__ = [
|
||||||
|
|
||||||
"get_openpype_version",
|
"get_openpype_version",
|
||||||
"get_build_version",
|
"get_build_version",
|
||||||
|
"get_expected_version",
|
||||||
|
"is_running_from_build",
|
||||||
|
"is_running_staging",
|
||||||
|
"is_current_version_studio_latest",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,69 @@ OpenPype version located in build but versions available in remote versions
|
||||||
repository or locally available.
|
repository or locally available.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
import sys
|
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():
|
def get_OpenPypeVersion():
|
||||||
"""Access to OpenPypeVersion class stored in sys modules."""
|
"""Access to OpenPypeVersion class stored in sys modules."""
|
||||||
return sys.modules.get("OpenPypeVersion")
|
return sys.modules.get("OpenPypeVersion")
|
||||||
|
|
@ -71,15 +131,67 @@ def get_remote_versions(*args, **kwargs):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_latest_version(*args, **kwargs):
|
def get_latest_version(staging=None, local=None, remote=None):
|
||||||
"""Get latest version from repository path."""
|
"""Get latest version from repository path."""
|
||||||
|
if staging is None:
|
||||||
|
staging = is_running_staging()
|
||||||
if op_version_control_available():
|
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
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_expected_studio_version(staging=False):
|
def get_expected_studio_version(staging=None):
|
||||||
"""Expected production or staging version in studio."""
|
"""Expected production or staging version in studio."""
|
||||||
|
if staging is None:
|
||||||
|
staging = is_running_staging()
|
||||||
if op_version_control_available():
|
if op_version_control_available():
|
||||||
return get_OpenPypeVersion().get_expected_studio_version(staging)
|
return get_OpenPypeVersion().get_expected_studio_version(staging)
|
||||||
return None
|
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
|
||||||
|
|
|
||||||
|
|
@ -5,68 +5,13 @@ import platform
|
||||||
import getpass
|
import getpass
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
import openpype.version
|
|
||||||
from openpype.settings.lib import get_local_settings
|
from openpype.settings.lib import get_local_settings
|
||||||
from .execute import get_openpype_execute_args
|
from .execute import get_openpype_execute_args
|
||||||
from .local_settings import get_local_site_id
|
from .local_settings import get_local_site_id
|
||||||
from .python_module_tools import import_filepath
|
from .openpype_version import (
|
||||||
|
is_running_from_build,
|
||||||
|
get_openpype_version
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def get_pype_info():
|
def get_pype_info():
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,14 @@ from openpype_modules.ftrack.ftrack_server.lib import (
|
||||||
TOPIC_STATUS_SERVER_RESULT
|
TOPIC_STATUS_SERVER_RESULT
|
||||||
)
|
)
|
||||||
from openpype.api import Logger
|
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 = (
|
action_identifier = (
|
||||||
"event.server.status" + os.environ["FTRACK_EVENT_SUB_ID"]
|
"event.server.status" + os.environ["FTRACK_EVENT_SUB_ID"]
|
||||||
)
|
)
|
||||||
|
|
@ -203,8 +209,57 @@ class StatusFactory:
|
||||||
})
|
})
|
||||||
return items
|
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 <b>{}</b>."
|
||||||
|
).format(str(get_openpype_version()))
|
||||||
|
})
|
||||||
|
|
||||||
|
elif is_latest is None:
|
||||||
|
items.append({
|
||||||
|
"type": "label",
|
||||||
|
"value": (
|
||||||
|
"Can't determine if OpenPype version is outdated"
|
||||||
|
" <b>{}</b>. 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 <b>{}</b>.".format(
|
||||||
|
str(get_openpype_version())
|
||||||
|
)
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
items.append({
|
||||||
|
"type": "label",
|
||||||
|
"value": (
|
||||||
|
"Using <b>outdated</b> OpenPype version <b>{}</b>."
|
||||||
|
" Expected version is <b>{}</b>."
|
||||||
|
"<br/>- 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):
|
def items(self):
|
||||||
items = []
|
items = []
|
||||||
|
items.extend(self.openpype_version_items())
|
||||||
items.append(self.note_item)
|
items.append(self.note_item)
|
||||||
items.extend(self.bool_items())
|
items.extend(self.bool_items())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import os
|
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__))
|
RESOURCES_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ def main(argv):
|
||||||
if host_name == "photoshop":
|
if host_name == "photoshop":
|
||||||
from openpype.hosts.photoshop.api.lib import main
|
from openpype.hosts.photoshop.api.lib import main
|
||||||
elif host_name == "aftereffects":
|
elif host_name == "aftereffects":
|
||||||
from avalon.aftereffects.lib import main
|
from openpype.hosts.aftereffects.api.lib import main
|
||||||
elif host_name == "harmony":
|
elif host_name == "harmony":
|
||||||
from avalon.harmony.lib import main
|
from avalon.harmony.lib import main
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"admin_password": "",
|
"admin_password": "",
|
||||||
"production_version": "",
|
"production_version": "",
|
||||||
"staging_version": "",
|
"staging_version": "",
|
||||||
|
"version_check_interval": 5,
|
||||||
"environment": {
|
"environment": {
|
||||||
"__environment_keys__": {
|
"__environment_keys__": {
|
||||||
"global": []
|
"global": []
|
||||||
|
|
|
||||||
|
|
@ -469,6 +469,17 @@ class PathInput(InputEntity):
|
||||||
# GUI attributes
|
# GUI attributes
|
||||||
self.placeholder_text = self.schema_data.get("placeholder")
|
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):
|
class RawJsonEntity(InputEntity):
|
||||||
schema_types = ["raw-json"]
|
schema_types = ["raw-json"]
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,19 @@
|
||||||
{
|
{
|
||||||
"type": "splitter"
|
"type": "splitter"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "label",
|
||||||
|
"label": "Trigger validation if running OpenPype is using studio defined version each 'n' <b>minutes</b>. Validation happens in OpenPype tray application."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number",
|
||||||
|
"key": "version_check_interval",
|
||||||
|
"label": "Version check interval",
|
||||||
|
"minimum": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "splitter"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "environment",
|
"key": "environment",
|
||||||
"label": "Environment",
|
"label": "Environment",
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,8 @@
|
||||||
"border-hover": "rgba(168, 175, 189, .3)",
|
"border-hover": "rgba(168, 175, 189, .3)",
|
||||||
"border-focus": "rgb(92, 173, 214)",
|
"border-focus": "rgb(92, 173, 214)",
|
||||||
|
|
||||||
|
"restart-btn-bg": "#458056",
|
||||||
|
|
||||||
"delete-btn-bg": "rgb(201, 54, 54)",
|
"delete-btn-bg": "rgb(201, 54, 54)",
|
||||||
"delete-btn-bg-disabled": "rgba(201, 54, 54, 64)",
|
"delete-btn-bg-disabled": "rgba(201, 54, 54, 64)",
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1228,6 +1228,11 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
||||||
background: #21252B;
|
background: #21252B;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tray */
|
||||||
|
#TrayRestartButton {
|
||||||
|
background: {color:restart-btn-bg};
|
||||||
|
}
|
||||||
|
|
||||||
/* Globally used names */
|
/* Globally used names */
|
||||||
#Separator {
|
#Separator {
|
||||||
background: {color:bg-menu-separator};
|
background: {color:bg-menu-separator};
|
||||||
|
|
|
||||||
BIN
openpype/tools/tray/images/gifts.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
|
|
@ -14,7 +14,14 @@ from openpype.api import (
|
||||||
resources,
|
resources,
|
||||||
get_system_settings
|
get_system_settings
|
||||||
)
|
)
|
||||||
from openpype.lib import get_openpype_execute_args
|
from openpype.lib import (
|
||||||
|
get_openpype_execute_args,
|
||||||
|
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.modules import TrayModulesManager
|
||||||
from openpype import style
|
from openpype import style
|
||||||
from openpype.settings import (
|
from openpype.settings import (
|
||||||
|
|
@ -22,29 +29,177 @@ from openpype.settings import (
|
||||||
ProjectSettings,
|
ProjectSettings,
|
||||||
DefaultsNotDefined
|
DefaultsNotDefined
|
||||||
)
|
)
|
||||||
|
from openpype.tools.utils import (
|
||||||
|
WrappedCallbackItem,
|
||||||
|
paint_image_with_color
|
||||||
|
)
|
||||||
|
|
||||||
from .pype_info_widget import PypeInfoWidget
|
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 <b>{}</b>."
|
||||||
|
" Your production has been updated to version <b>{}</b>."
|
||||||
|
).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:
|
class TrayManager:
|
||||||
"""Cares about context of application.
|
"""Cares about context of application.
|
||||||
|
|
||||||
Load submenus, actions, separators and modules into tray's context.
|
Load submenus, actions, separators and modules into tray's context.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, tray_widget, main_window):
|
def __init__(self, tray_widget, main_window):
|
||||||
self.tray_widget = tray_widget
|
self.tray_widget = tray_widget
|
||||||
self.main_window = main_window
|
self.main_window = main_window
|
||||||
self.pype_info_widget = None
|
self.pype_info_widget = None
|
||||||
|
self._restart_action = None
|
||||||
|
|
||||||
self.log = Logger.get_logger(self.__class__.__name__)
|
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.modules_manager = TrayModulesManager()
|
||||||
|
|
||||||
self.errors = []
|
self.errors = []
|
||||||
|
|
||||||
|
self._version_check_timer = None
|
||||||
|
self._version_dialog = None
|
||||||
|
|
||||||
self.main_thread_timer = None
|
self.main_thread_timer = None
|
||||||
self._main_thread_callbacks = collections.deque()
|
self._main_thread_callbacks = collections.deque()
|
||||||
self._execution_in_progress = None
|
self._execution_in_progress = None
|
||||||
|
|
@ -61,21 +216,73 @@ class TrayManager:
|
||||||
if callback:
|
if callback:
|
||||||
self.execute_in_main_thread(callback)
|
self.execute_in_main_thread(callback)
|
||||||
|
|
||||||
def execute_in_main_thread(self, callback):
|
def _on_version_check_timer(self):
|
||||||
self._main_thread_callbacks.append(callback)
|
# Check if is running from build and stop future validations if yes
|
||||||
|
if not is_running_from_build():
|
||||||
|
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):
|
def _main_thread_execution(self):
|
||||||
if self._execution_in_progress:
|
if self._execution_in_progress:
|
||||||
return
|
return
|
||||||
self._execution_in_progress = True
|
self._execution_in_progress = True
|
||||||
while self._main_thread_callbacks:
|
for _ in range(len(self._main_thread_callbacks)):
|
||||||
try:
|
if self._main_thread_callbacks:
|
||||||
callback = self._main_thread_callbacks.popleft()
|
item = self._main_thread_callbacks.popleft()
|
||||||
callback()
|
item.execute()
|
||||||
except:
|
|
||||||
self.log.warning(
|
|
||||||
"Failed to execute {} in main thread".format(callback),
|
|
||||||
exc_info=True)
|
|
||||||
|
|
||||||
self._execution_in_progress = False
|
self._execution_in_progress = False
|
||||||
|
|
||||||
|
|
@ -119,6 +326,13 @@ class TrayManager:
|
||||||
|
|
||||||
self.main_thread_timer = main_thread_timer
|
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
|
# For storing missing settings dialog
|
||||||
self._settings_validation_dialog = None
|
self._settings_validation_dialog = None
|
||||||
|
|
||||||
|
|
@ -200,24 +414,47 @@ class TrayManager:
|
||||||
|
|
||||||
version_action = QtWidgets.QAction(version_string, self.tray_widget)
|
version_action = QtWidgets.QAction(version_string, self.tray_widget)
|
||||||
version_action.triggered.connect(self._on_version_action)
|
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(version_action)
|
||||||
|
self.tray_widget.menu.addAction(restart_action)
|
||||||
self.tray_widget.menu.addSeparator()
|
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.
|
"""Restart Tray tool.
|
||||||
|
|
||||||
First creates new process with same argument and close current tray.
|
First creates new process with same argument and close current tray.
|
||||||
"""
|
"""
|
||||||
args = get_openpype_execute_args()
|
args = get_openpype_execute_args()
|
||||||
|
kwargs = {
|
||||||
|
"env": dict(os.environ.items())
|
||||||
|
}
|
||||||
|
|
||||||
# Create a copy of sys.argv
|
# Create a copy of sys.argv
|
||||||
additional_args = list(sys.argv)
|
additional_args = list(sys.argv)
|
||||||
# Check last argument from `get_openpype_execute_args`
|
# Check last argument from `get_openpype_execute_args`
|
||||||
# - when running from code it is the same as first from sys.argv
|
# - when running from code it is the same as first from sys.argv
|
||||||
if args[-1] == additional_args[0]:
|
if args[-1] == additional_args[0]:
|
||||||
additional_args.pop(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":
|
if platform.system().lower() == "windows":
|
||||||
flags = (
|
flags = (
|
||||||
subprocess.CREATE_NEW_PROCESS_GROUP
|
subprocess.CREATE_NEW_PROCESS_GROUP
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ from .widgets import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from .error_dialog import ErrorMessageBox
|
from .error_dialog import ErrorMessageBox
|
||||||
|
from .lib import (
|
||||||
|
WrappedCallbackItem,
|
||||||
|
paint_image_with_color
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
|
@ -14,5 +18,8 @@ __all__ = (
|
||||||
"ClickableFrame",
|
"ClickableFrame",
|
||||||
"ExpandBtn",
|
"ExpandBtn",
|
||||||
|
|
||||||
"ErrorMessageBox"
|
"ErrorMessageBox",
|
||||||
|
|
||||||
|
"WrappedCallbackItem",
|
||||||
|
"paint_image_with_color",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,10 @@ import avalon.api
|
||||||
from avalon import style
|
from avalon import style
|
||||||
from avalon.vendor import qtawesome
|
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
|
from openpype.lib import filter_profiles
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -598,3 +601,68 @@ def is_remove_site_loader(loader):
|
||||||
|
|
||||||
def is_add_site_loader(loader):
|
def is_add_site_loader(loader):
|
||||||
return hasattr(loader, "add_site_to_representation")
|
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
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ 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,
|
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).
|
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_link}` to message content. This link might help users to find review easier on their machines.
|
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!)
|
(It won't show a playable preview though!)
|
||||||
|
|
||||||
#### Message
|
#### Message
|
||||||
|
|
|
||||||