diff --git a/openpype/hooks/pre_non_python_host_launch.py b/openpype/hooks/pre_non_python_host_launch.py
index 848ed675a8..29e40d28c8 100644
--- a/openpype/hooks/pre_non_python_host_launch.py
+++ b/openpype/hooks/pre_non_python_host_launch.py
@@ -3,7 +3,7 @@ import subprocess
from openpype.lib import (
PreLaunchHook,
- get_pype_execute_args
+ get_openpype_execute_args
)
from openpype import PACKAGE_DIR as OPENPYPE_DIR
@@ -35,7 +35,7 @@ class NonPythonHostHook(PreLaunchHook):
"non_python_host_launch.py"
)
- new_launch_args = get_pype_execute_args(
+ new_launch_args = get_openpype_execute_args(
"run", script_path, executable_path
)
# Add workfile path if exists
@@ -48,4 +48,3 @@ class NonPythonHostHook(PreLaunchHook):
if remainders:
self.launch_context.launch_args.extend(remainders)
-
diff --git a/openpype/hosts/houdini/plugins/load/load_alembic.py b/openpype/hosts/houdini/plugins/load/load_alembic.py
index cd0f0f0d2d..df66d56008 100644
--- a/openpype/hosts/houdini/plugins/load/load_alembic.py
+++ b/openpype/hosts/houdini/plugins/load/load_alembic.py
@@ -1,6 +1,6 @@
from avalon import api
-from avalon.houdini import pipeline, lib
+from avalon.houdini import pipeline
class AbcLoader(api.Loader):
@@ -25,16 +25,9 @@ class AbcLoader(api.Loader):
# Get the root node
obj = hou.node("/obj")
- # Create a unique name
- counter = 1
+ # Define node name
namespace = namespace if namespace else context["asset"]["name"]
- formatted = "{}_{}".format(namespace, name) if namespace else name
- node_name = "{0}_{1:03d}".format(formatted, counter)
-
- children = lib.children_as_string(hou.node("/obj"))
- while node_name in children:
- counter += 1
- node_name = "{0}_{1:03d}".format(formatted, counter)
+ node_name = "{}_{}".format(namespace, name) if namespace else name
# Create a new geo node
container = obj.createNode("geo", node_name=node_name)
diff --git a/openpype/hosts/houdini/plugins/load/load_camera.py b/openpype/hosts/houdini/plugins/load/load_camera.py
index 83246b7d97..8b98b7c05e 100644
--- a/openpype/hosts/houdini/plugins/load/load_camera.py
+++ b/openpype/hosts/houdini/plugins/load/load_camera.py
@@ -1,5 +1,5 @@
from avalon import api
-from avalon.houdini import pipeline, lib
+from avalon.houdini import pipeline
ARCHIVE_EXPRESSION = ('__import__("_alembic_hom_extensions")'
@@ -97,18 +97,9 @@ class CameraLoader(api.Loader):
# Get the root node
obj = hou.node("/obj")
- # Create a unique name
- counter = 1
- asset_name = context["asset"]["name"]
-
- namespace = namespace or asset_name
- formatted = "{}_{}".format(namespace, name) if namespace else name
- node_name = "{0}_{1:03d}".format(formatted, counter)
-
- children = lib.children_as_string(hou.node("/obj"))
- while node_name in children:
- counter += 1
- node_name = "{0}_{1:03d}".format(formatted, counter)
+ # Define node name
+ namespace = namespace if namespace else context["asset"]["name"]
+ node_name = "{}_{}".format(namespace, name) if namespace else name
# Create a archive node
container = self.create_and_connect(obj, "alembicarchive", node_name)
diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py
index 52ebcaff64..bd83b13b06 100644
--- a/openpype/hosts/maya/api/lib.py
+++ b/openpype/hosts/maya/api/lib.py
@@ -745,6 +745,33 @@ def namespaced(namespace, new=True):
cmds.namespace(set=original)
+@contextlib.contextmanager
+def maintained_selection_api():
+ """Maintain selection using the Maya Python API.
+
+ Warning: This is *not* added to the undo stack.
+
+ """
+ original = om.MGlobal.getActiveSelectionList()
+ try:
+ yield
+ finally:
+ om.MGlobal.setActiveSelectionList(original)
+
+
+@contextlib.contextmanager
+def tool(context):
+ """Set a tool context during the context manager.
+
+ """
+ original = cmds.currentCtx()
+ try:
+ cmds.setToolTo(context)
+ yield
+ finally:
+ cmds.setToolTo(original)
+
+
def polyConstraint(components, *args, **kwargs):
"""Return the list of *components* with the constraints applied.
@@ -763,17 +790,25 @@ def polyConstraint(components, *args, **kwargs):
kwargs.pop('mode', None)
with no_undo(flush=False):
- with maya.maintained_selection():
- # Apply constraint using mode=2 (current and next) so
- # it applies to the selection made before it; because just
- # a `maya.cmds.select()` call will not trigger the constraint.
- with reset_polySelectConstraint():
- cmds.select(components, r=1, noExpand=True)
- cmds.polySelectConstraint(*args, mode=2, **kwargs)
- result = cmds.ls(selection=True)
- cmds.select(clear=True)
-
- return result
+ # Reverting selection to the original selection using
+ # `maya.cmds.select` can be slow in rare cases where previously
+ # `maya.cmds.polySelectConstraint` had set constrain to "All and Next"
+ # and the "Random" setting was activated. To work around this we
+ # revert to the original selection using the Maya API. This is safe
+ # since we're not generating any undo change anyway.
+ with tool("selectSuperContext"):
+ # Selection can be very slow when in a manipulator mode.
+ # So we force the selection context which is fast.
+ with maintained_selection_api():
+ # Apply constraint using mode=2 (current and next) so
+ # it applies to the selection made before it; because just
+ # a `maya.cmds.select()` call will not trigger the constraint.
+ with reset_polySelectConstraint():
+ cmds.select(components, r=1, noExpand=True)
+ cmds.polySelectConstraint(*args, mode=2, **kwargs)
+ result = cmds.ls(selection=True)
+ cmds.select(clear=True)
+ return result
@contextlib.contextmanager
diff --git a/openpype/hosts/maya/plugins/load/load_look.py b/openpype/hosts/maya/plugins/load/load_look.py
index fca612eff4..8e14778fd2 100644
--- a/openpype/hosts/maya/plugins/load/load_look.py
+++ b/openpype/hosts/maya/plugins/load/load_look.py
@@ -8,6 +8,8 @@ from collections import defaultdict
from openpype.widgets.message_window import ScrollMessageBox
from Qt import QtWidgets
+from openpype.hosts.maya.api.plugin import get_reference_node
+
class LookLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
"""Specific loader for lookdev"""
@@ -70,7 +72,7 @@ class LookLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
# Get reference node from container members
members = cmds.sets(node, query=True, nodesOnly=True)
- reference_node = self._get_reference_node(members)
+ reference_node = get_reference_node(members, log=self.log)
shader_nodes = cmds.ls(members, type='shadingEngine')
orig_nodes = set(self._get_nodes_with_shader(shader_nodes))
diff --git a/openpype/hosts/photoshop/api/README.md b/openpype/hosts/photoshop/api/README.md
new file mode 100644
index 0000000000..b958f53803
--- /dev/null
+++ b/openpype/hosts/photoshop/api/README.md
@@ -0,0 +1,255 @@
+# Photoshop Integration
+
+## Setup
+
+The Photoshop 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
+```
+
+### Server
+
+The easiest way to get the server and Photoshop launch is with:
+
+```
+python -c ^"import avalon.photoshop;avalon.photoshop.launch(""C:\Program Files\Adobe\Adobe Photoshop 2020\Photoshop.exe"")^"
+```
+
+`avalon.photoshop.launch` launches the application and server, and also closes the server when Photoshop exists.
+
+## Usage
+
+The Photoshop extension can be found under `Window > Extensions > Avalon`. 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-Photoshop avalon extension.p12
+ZXPSignCmd -sign {path to avalon-core}\avalon\photoshop\extension {path to avalon-core}\avalon\photoshop\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).
+
+#### Creator Plugin
+```python
+from avalon import photoshop
+
+
+class CreateImage(photoshop.Creator):
+ """Image folder for publish."""
+
+ name = "imageDefault"
+ label = "Image"
+ family = "image"
+
+ def __init__(self, *args, **kwargs):
+ super(CreateImage, self).__init__(*args, **kwargs)
+```
+
+#### Collector Plugin
+```python
+import pythoncom
+
+import pyblish.api
+
+
+class CollectInstances(pyblish.api.ContextPlugin):
+ """Gather instances by LayerSet and file metadata
+
+ This collector takes into account assets that are associated with
+ an LayerSet and marked with a unique identifier;
+
+ Identifier:
+ id (str): "pyblish.avalon.instance"
+ """
+
+ label = "Instances"
+ order = pyblish.api.CollectorOrder
+ hosts = ["photoshop"]
+ families_mapping = {
+ "image": []
+ }
+
+ def process(self, context):
+ # Necessary call when running in a different thread which pyblish-qml
+ # can be.
+ pythoncom.CoInitialize()
+
+ photoshop_client = PhotoshopClientStub()
+ layers = photoshop_client.get_layers()
+ layers_meta = photoshop_client.get_layers_metadata()
+ for layer in layers:
+ layer_data = photoshop_client.read(layer, layers_meta)
+
+ # Skip layers without metadata.
+ if layer_data is None:
+ continue
+
+ # Skip containers.
+ if "container" in layer_data["id"]:
+ continue
+
+ # child_layers = [*layer.Layers]
+ # self.log.debug("child_layers {}".format(child_layers))
+ # if not child_layers:
+ # self.log.info("%s skipped, it was empty." % layer.Name)
+ # continue
+
+ instance = context.create_instance(layer.name)
+ instance.append(layer)
+ instance.data.update(layer_data)
+ instance.data["families"] = self.families_mapping[
+ layer_data["family"]
+ ]
+ instance.data["publish"] = layer.visible
+
+ # Produce diagnostic message for any graphical
+ # user interface interested in visualising it.
+ self.log.info("Found: \"%s\" " % instance.data["name"])
+```
+
+#### Extractor Plugin
+```python
+import os
+
+import openpype.api
+from avalon import photoshop
+
+
+class ExtractImage(openpype.api.Extractor):
+ """Produce a flattened image file from instance
+
+ This plug-in takes into account only the layers in the group.
+ """
+
+ label = "Extract Image"
+ hosts = ["photoshop"]
+ families = ["image"]
+ formats = ["png", "jpg"]
+
+ def process(self, instance):
+
+ staging_dir = self.staging_dir(instance)
+ self.log.info("Outputting image to {}".format(staging_dir))
+
+ # Perform extraction
+ stub = photoshop.stub()
+ files = {}
+ with photoshop.maintained_selection():
+ self.log.info("Extracting %s" % str(list(instance)))
+ with photoshop.maintained_visibility():
+ # Hide all other layers.
+ extract_ids = set([ll.id for ll in stub.
+ get_layers_in_layers([instance[0]])])
+
+ for layer in stub.get_layers():
+ # limit unnecessary calls to client
+ if layer.visible and layer.id not in extract_ids:
+ stub.set_visible(layer.id, False)
+
+ save_options = []
+ if "png" in self.formats:
+ save_options.append('png')
+ if "jpg" in self.formats:
+ save_options.append('jpg')
+
+ file_basename = os.path.splitext(
+ stub.get_active_document_name()
+ )[0]
+ for extension in save_options:
+ _filename = "{}.{}".format(file_basename, extension)
+ files[extension] = _filename
+
+ full_filename = os.path.join(staging_dir, _filename)
+ stub.saveAs(full_filename, extension, True)
+
+ representations = []
+ for extension, filename in files.items():
+ representations.append({
+ "name": extension,
+ "ext": extension,
+ "files": filename,
+ "stagingDir": staging_dir
+ })
+ instance.data["representations"] = representations
+ instance.data["stagingDir"] = staging_dir
+
+ self.log.info(f"Extracted {instance} to {staging_dir}")
+```
+
+#### Loader Plugin
+```python
+from avalon import api, photoshop
+
+stub = photoshop.stub()
+
+
+class ImageLoader(api.Loader):
+ """Load images
+
+ Stores the imported asset in a container named after the asset.
+ """
+
+ families = ["image"]
+ representations = ["*"]
+
+ def load(self, context, name=None, namespace=None, data=None):
+ with photoshop.maintained_selection():
+ layer = stub.import_smart_object(self.fname)
+
+ self[:] = [layer]
+
+ return photoshop.containerise(
+ name,
+ namespace,
+ layer,
+ context,
+ self.__class__.__name__
+ )
+
+ def update(self, container, representation):
+ layer = container.pop("layer")
+
+ with photoshop.maintained_selection():
+ stub.replace_smart_object(
+ layer, api.get_representation_path(representation)
+ )
+
+ stub.imprint(
+ layer, {"representation": str(representation["_id"])}
+ )
+
+ def remove(self, container):
+ container["layer"].Delete()
+
+ def switch(self, container, representation):
+ self.update(container, representation)
+```
+For easier debugging of Javascript:
+https://community.adobe.com/t5/download-install/adobe-extension-debuger-problem/td-p/10911704?page=1
+Add --enable-blink-features=ShadowDOMV0,CustomElementsV0 when starting Chrome
+then localhost:8078 (port set in `photoshop\extension\.debug`)
+
+Or use Visual Studio Code https://medium.com/adobetech/extendscript-debugger-for-visual-studio-code-public-release-a2ff6161fa01
+
+Or install CEF client from https://github.com/Adobe-CEP/CEP-Resources/tree/master/CEP_9.x
+## Resources
+ - https://github.com/lohriialo/photoshop-scripting-python
+ - https://www.adobe.com/devnet/photoshop/scripting.html
+ - https://github.com/Adobe-CEP/Getting-Started-guides
+ - https://github.com/Adobe-CEP/CEP-Resources
diff --git a/openpype/hosts/photoshop/api/__init__.py b/openpype/hosts/photoshop/api/__init__.py
index d978d6ecc1..4cc2aa2c78 100644
--- a/openpype/hosts/photoshop/api/__init__.py
+++ b/openpype/hosts/photoshop/api/__init__.py
@@ -1,79 +1,63 @@
-import os
-import sys
-import logging
+"""Public API
-from Qt import QtWidgets
+Anything that isn't defined here is INTERNAL and unreliable for external use.
-from avalon import io
-from avalon import api as avalon
-from openpype import lib
-from pyblish import api as pyblish
-import openpype.hosts.photoshop
+"""
-log = logging.getLogger("openpype.hosts.photoshop")
+from .launch_logic import stub
-HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.photoshop.__file__))
-PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
-PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
-LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
-CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
-INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
+from .pipeline import (
+ ls,
+ list_instances,
+ remove_instance,
+ install,
+ uninstall,
+ containerise
+)
+from .plugin import (
+ PhotoshopLoader,
+ Creator,
+ get_unique_layer_name
+)
+from .workio import (
+ file_extensions,
+ has_unsaved_changes,
+ save_file,
+ open_file,
+ current_file,
+ work_root,
+)
-def check_inventory():
- if not lib.any_outdated():
- return
+from .lib import (
+ maintained_selection,
+ maintained_visibility
+)
- host = avalon.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)
+__all__ = [
+ # launch_logic
+ "stub",
- # Warn about outdated containers.
- print("Starting new QApplication..")
- app = QtWidgets.QApplication(sys.argv)
+ # pipeline
+ "ls",
+ "list_instances",
+ "remove_instance",
+ "install",
+ "containerise",
- 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_()
+ # Plugin
+ "PhotoshopLoader",
+ "Creator",
+ "get_unique_layer_name",
- # Garbage collect QApplication.
- del app
+ # workfiles
+ "file_extensions",
+ "has_unsaved_changes",
+ "save_file",
+ "open_file",
+ "current_file",
+ "work_root",
-
-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
+ # lib
+ "maintained_selection",
+ "maintained_visibility",
+]
diff --git a/openpype/hosts/photoshop/api/extension.zxp b/openpype/hosts/photoshop/api/extension.zxp
new file mode 100644
index 0000000000..a25ec96e7d
Binary files /dev/null and b/openpype/hosts/photoshop/api/extension.zxp differ
diff --git a/openpype/hosts/photoshop/api/extension/.debug b/openpype/hosts/photoshop/api/extension/.debug
new file mode 100644
index 0000000000..a0e2f3c9e0
--- /dev/null
+++ b/openpype/hosts/photoshop/api/extension/.debug
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/openpype/hosts/photoshop/api/extension/CSXS/manifest.xml b/openpype/hosts/photoshop/api/extension/CSXS/manifest.xml
new file mode 100644
index 0000000000..6396cd2412
--- /dev/null
+++ b/openpype/hosts/photoshop/api/extension/CSXS/manifest.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ./index.html
+
+
+
+ true
+
+
+ applicationActivate
+ com.adobe.csxs.events.ApplicationInitialized
+
+
+
+ Panel
+
+
+
+ 300
+ 140
+
+
+ 400
+ 200
+
+
+
+ ./icons/avalon-logo-48.png
+
+
+
+
+
+
diff --git a/openpype/hosts/photoshop/api/extension/client/CSInterface.js b/openpype/hosts/photoshop/api/extension/client/CSInterface.js
new file mode 100644
index 0000000000..4239391efd
--- /dev/null
+++ b/openpype/hosts/photoshop/api/extension/client/CSInterface.js
@@ -0,0 +1,1193 @@
+/**************************************************************************************************
+*
+* ADOBE SYSTEMS INCORPORATED
+* Copyright 2013 Adobe Systems Incorporated
+* All Rights Reserved.
+*
+* NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the
+* terms of the Adobe license agreement accompanying it. If you have received this file from a
+* source other than Adobe, then your use, modification, or distribution of it requires the prior
+* written permission of Adobe.
+*
+**************************************************************************************************/
+
+/** CSInterface - v8.0.0 */
+
+/**
+ * Stores constants for the window types supported by the CSXS infrastructure.
+ */
+function CSXSWindowType()
+{
+}
+
+/** Constant for the CSXS window type Panel. */
+CSXSWindowType._PANEL = "Panel";
+
+/** Constant for the CSXS window type Modeless. */
+CSXSWindowType._MODELESS = "Modeless";
+
+/** Constant for the CSXS window type ModalDialog. */
+CSXSWindowType._MODAL_DIALOG = "ModalDialog";
+
+/** EvalScript error message */
+EvalScript_ErrMessage = "EvalScript error.";
+
+/**
+ * @class Version
+ * Defines a version number with major, minor, micro, and special
+ * components. The major, minor and micro values are numeric; the special
+ * value can be any string.
+ *
+ * @param major The major version component, a positive integer up to nine digits long.
+ * @param minor The minor version component, a positive integer up to nine digits long.
+ * @param micro The micro version component, a positive integer up to nine digits long.
+ * @param special The special version component, an arbitrary string.
+ *
+ * @return A new \c Version object.
+ */
+function Version(major, minor, micro, special)
+{
+ this.major = major;
+ this.minor = minor;
+ this.micro = micro;
+ this.special = special;
+}
+
+/**
+ * The maximum value allowed for a numeric version component.
+ * This reflects the maximum value allowed in PlugPlug and the manifest schema.
+ */
+Version.MAX_NUM = 999999999;
+
+/**
+ * @class VersionBound
+ * Defines a boundary for a version range, which associates a \c Version object
+ * with a flag for whether it is an inclusive or exclusive boundary.
+ *
+ * @param version The \c #Version object.
+ * @param inclusive True if this boundary is inclusive, false if it is exclusive.
+ *
+ * @return A new \c VersionBound object.
+ */
+function VersionBound(version, inclusive)
+{
+ this.version = version;
+ this.inclusive = inclusive;
+}
+
+/**
+ * @class VersionRange
+ * Defines a range of versions using a lower boundary and optional upper boundary.
+ *
+ * @param lowerBound The \c #VersionBound object.
+ * @param upperBound The \c #VersionBound object, or null for a range with no upper boundary.
+ *
+ * @return A new \c VersionRange object.
+ */
+function VersionRange(lowerBound, upperBound)
+{
+ this.lowerBound = lowerBound;
+ this.upperBound = upperBound;
+}
+
+/**
+ * @class Runtime
+ * Represents a runtime related to the CEP infrastructure.
+ * Extensions can declare dependencies on particular
+ * CEP runtime versions in the extension manifest.
+ *
+ * @param name The runtime name.
+ * @param version A \c #VersionRange object that defines a range of valid versions.
+ *
+ * @return A new \c Runtime object.
+ */
+function Runtime(name, versionRange)
+{
+ this.name = name;
+ this.versionRange = versionRange;
+}
+
+/**
+* @class Extension
+* Encapsulates a CEP-based extension to an Adobe application.
+*
+* @param id The unique identifier of this extension.
+* @param name The localizable display name of this extension.
+* @param mainPath The path of the "index.html" file.
+* @param basePath The base path of this extension.
+* @param windowType The window type of the main window of this extension.
+ Valid values are defined by \c #CSXSWindowType.
+* @param width The default width in pixels of the main window of this extension.
+* @param height The default height in pixels of the main window of this extension.
+* @param minWidth The minimum width in pixels of the main window of this extension.
+* @param minHeight The minimum height in pixels of the main window of this extension.
+* @param maxWidth The maximum width in pixels of the main window of this extension.
+* @param maxHeight The maximum height in pixels of the main window of this extension.
+* @param defaultExtensionDataXml The extension data contained in the default \c ExtensionDispatchInfo section of the extension manifest.
+* @param specialExtensionDataXml The extension data contained in the application-specific \c ExtensionDispatchInfo section of the extension manifest.
+* @param requiredRuntimeList An array of \c Runtime objects for runtimes required by this extension.
+* @param isAutoVisible True if this extension is visible on loading.
+* @param isPluginExtension True if this extension has been deployed in the Plugins folder of the host application.
+*
+* @return A new \c Extension object.
+*/
+function Extension(id, name, mainPath, basePath, windowType, width, height, minWidth, minHeight, maxWidth, maxHeight,
+ defaultExtensionDataXml, specialExtensionDataXml, requiredRuntimeList, isAutoVisible, isPluginExtension)
+{
+ this.id = id;
+ this.name = name;
+ this.mainPath = mainPath;
+ this.basePath = basePath;
+ this.windowType = windowType;
+ this.width = width;
+ this.height = height;
+ this.minWidth = minWidth;
+ this.minHeight = minHeight;
+ this.maxWidth = maxWidth;
+ this.maxHeight = maxHeight;
+ this.defaultExtensionDataXml = defaultExtensionDataXml;
+ this.specialExtensionDataXml = specialExtensionDataXml;
+ this.requiredRuntimeList = requiredRuntimeList;
+ this.isAutoVisible = isAutoVisible;
+ this.isPluginExtension = isPluginExtension;
+}
+
+/**
+ * @class CSEvent
+ * A standard JavaScript event, the base class for CEP events.
+ *
+ * @param type The name of the event type.
+ * @param scope The scope of event, can be "GLOBAL" or "APPLICATION".
+ * @param appId The unique identifier of the application that generated the event.
+ * @param extensionId The unique identifier of the extension that generated the event.
+ *
+ * @return A new \c CSEvent object
+ */
+function CSEvent(type, scope, appId, extensionId)
+{
+ this.type = type;
+ this.scope = scope;
+ this.appId = appId;
+ this.extensionId = extensionId;
+}
+
+/** Event-specific data. */
+CSEvent.prototype.data = "";
+
+/**
+ * @class SystemPath
+ * Stores operating-system-specific location constants for use in the
+ * \c #CSInterface.getSystemPath() method.
+ * @return A new \c SystemPath object.
+ */
+function SystemPath()
+{
+}
+
+/** The path to user data. */
+SystemPath.USER_DATA = "userData";
+
+/** The path to common files for Adobe applications. */
+SystemPath.COMMON_FILES = "commonFiles";
+
+/** The path to the user's default document folder. */
+SystemPath.MY_DOCUMENTS = "myDocuments";
+
+/** @deprecated. Use \c #SystemPath.Extension. */
+SystemPath.APPLICATION = "application";
+
+/** The path to current extension. */
+SystemPath.EXTENSION = "extension";
+
+/** The path to hosting application's executable. */
+SystemPath.HOST_APPLICATION = "hostApplication";
+
+/**
+ * @class ColorType
+ * Stores color-type constants.
+ */
+function ColorType()
+{
+}
+
+/** RGB color type. */
+ColorType.RGB = "rgb";
+
+/** Gradient color type. */
+ColorType.GRADIENT = "gradient";
+
+/** Null color type. */
+ColorType.NONE = "none";
+
+/**
+ * @class RGBColor
+ * Stores an RGB color with red, green, blue, and alpha values.
+ * All values are in the range [0.0 to 255.0]. Invalid numeric values are
+ * converted to numbers within this range.
+ *
+ * @param red The red value, in the range [0.0 to 255.0].
+ * @param green The green value, in the range [0.0 to 255.0].
+ * @param blue The blue value, in the range [0.0 to 255.0].
+ * @param alpha The alpha (transparency) value, in the range [0.0 to 255.0].
+ * The default, 255.0, means that the color is fully opaque.
+ *
+ * @return A new RGBColor object.
+ */
+function RGBColor(red, green, blue, alpha)
+{
+ this.red = red;
+ this.green = green;
+ this.blue = blue;
+ this.alpha = alpha;
+}
+
+/**
+ * @class Direction
+ * A point value in which the y component is 0 and the x component
+ * is positive or negative for a right or left direction,
+ * or the x component is 0 and the y component is positive or negative for
+ * an up or down direction.
+ *
+ * @param x The horizontal component of the point.
+ * @param y The vertical component of the point.
+ *
+ * @return A new \c Direction object.
+ */
+function Direction(x, y)
+{
+ this.x = x;
+ this.y = y;
+}
+
+/**
+ * @class GradientStop
+ * Stores gradient stop information.
+ *
+ * @param offset The offset of the gradient stop, in the range [0.0 to 1.0].
+ * @param rgbColor The color of the gradient at this point, an \c #RGBColor object.
+ *
+ * @return GradientStop object.
+ */
+function GradientStop(offset, rgbColor)
+{
+ this.offset = offset;
+ this.rgbColor = rgbColor;
+}
+
+/**
+ * @class GradientColor
+ * Stores gradient color information.
+ *
+ * @param type The gradient type, must be "linear".
+ * @param direction A \c #Direction object for the direction of the gradient
+ (up, down, right, or left).
+ * @param numStops The number of stops in the gradient.
+ * @param gradientStopList An array of \c #GradientStop objects.
+ *
+ * @return A new \c GradientColor object.
+ */
+function GradientColor(type, direction, numStops, arrGradientStop)
+{
+ this.type = type;
+ this.direction = direction;
+ this.numStops = numStops;
+ this.arrGradientStop = arrGradientStop;
+}
+
+/**
+ * @class UIColor
+ * Stores color information, including the type, anti-alias level, and specific color
+ * values in a color object of an appropriate type.
+ *
+ * @param type The color type, 1 for "rgb" and 2 for "gradient".
+ The supplied color object must correspond to this type.
+ * @param antialiasLevel The anti-alias level constant.
+ * @param color A \c #RGBColor or \c #GradientColor object containing specific color information.
+ *
+ * @return A new \c UIColor object.
+ */
+function UIColor(type, antialiasLevel, color)
+{
+ this.type = type;
+ this.antialiasLevel = antialiasLevel;
+ this.color = color;
+}
+
+/**
+ * @class AppSkinInfo
+ * Stores window-skin properties, such as color and font. All color parameter values are \c #UIColor objects except that systemHighlightColor is \c #RGBColor object.
+ *
+ * @param baseFontFamily The base font family of the application.
+ * @param baseFontSize The base font size of the application.
+ * @param appBarBackgroundColor The application bar background color.
+ * @param panelBackgroundColor The background color of the extension panel.
+ * @param appBarBackgroundColorSRGB The application bar background color, as sRGB.
+ * @param panelBackgroundColorSRGB The background color of the extension panel, as sRGB.
+ * @param systemHighlightColor The highlight color of the extension panel, if provided by the host application. Otherwise, the operating-system highlight color.
+ *
+ * @return AppSkinInfo object.
+ */
+function AppSkinInfo(baseFontFamily, baseFontSize, appBarBackgroundColor, panelBackgroundColor, appBarBackgroundColorSRGB, panelBackgroundColorSRGB, systemHighlightColor)
+{
+ this.baseFontFamily = baseFontFamily;
+ this.baseFontSize = baseFontSize;
+ this.appBarBackgroundColor = appBarBackgroundColor;
+ this.panelBackgroundColor = panelBackgroundColor;
+ this.appBarBackgroundColorSRGB = appBarBackgroundColorSRGB;
+ this.panelBackgroundColorSRGB = panelBackgroundColorSRGB;
+ this.systemHighlightColor = systemHighlightColor;
+}
+
+/**
+ * @class HostEnvironment
+ * Stores information about the environment in which the extension is loaded.
+ *
+ * @param appName The application's name.
+ * @param appVersion The application's version.
+ * @param appLocale The application's current license locale.
+ * @param appUILocale The application's current UI locale.
+ * @param appId The application's unique identifier.
+ * @param isAppOnline True if the application is currently online.
+ * @param appSkinInfo An \c #AppSkinInfo object containing the application's default color and font styles.
+ *
+ * @return A new \c HostEnvironment object.
+ */
+function HostEnvironment(appName, appVersion, appLocale, appUILocale, appId, isAppOnline, appSkinInfo)
+{
+ this.appName = appName;
+ this.appVersion = appVersion;
+ this.appLocale = appLocale;
+ this.appUILocale = appUILocale;
+ this.appId = appId;
+ this.isAppOnline = isAppOnline;
+ this.appSkinInfo = appSkinInfo;
+}
+
+/**
+ * @class HostCapabilities
+ * Stores information about the host capabilities.
+ *
+ * @param EXTENDED_PANEL_MENU True if the application supports panel menu.
+ * @param EXTENDED_PANEL_ICONS True if the application supports panel icon.
+ * @param DELEGATE_APE_ENGINE True if the application supports delegated APE engine.
+ * @param SUPPORT_HTML_EXTENSIONS True if the application supports HTML extensions.
+ * @param DISABLE_FLASH_EXTENSIONS True if the application disables FLASH extensions.
+ *
+ * @return A new \c HostCapabilities object.
+ */
+function HostCapabilities(EXTENDED_PANEL_MENU, EXTENDED_PANEL_ICONS, DELEGATE_APE_ENGINE, SUPPORT_HTML_EXTENSIONS, DISABLE_FLASH_EXTENSIONS)
+{
+ this.EXTENDED_PANEL_MENU = EXTENDED_PANEL_MENU;
+ this.EXTENDED_PANEL_ICONS = EXTENDED_PANEL_ICONS;
+ this.DELEGATE_APE_ENGINE = DELEGATE_APE_ENGINE;
+ this.SUPPORT_HTML_EXTENSIONS = SUPPORT_HTML_EXTENSIONS;
+ this.DISABLE_FLASH_EXTENSIONS = DISABLE_FLASH_EXTENSIONS; // Since 5.0.0
+}
+
+/**
+ * @class ApiVersion
+ * Stores current api version.
+ *
+ * Since 4.2.0
+ *
+ * @param major The major version
+ * @param minor The minor version.
+ * @param micro The micro version.
+ *
+ * @return ApiVersion object.
+ */
+function ApiVersion(major, minor, micro)
+{
+ this.major = major;
+ this.minor = minor;
+ this.micro = micro;
+}
+
+/**
+ * @class MenuItemStatus
+ * Stores flyout menu item status
+ *
+ * Since 5.2.0
+ *
+ * @param menuItemLabel The menu item label.
+ * @param enabled True if user wants to enable the menu item.
+ * @param checked True if user wants to check the menu item.
+ *
+ * @return MenuItemStatus object.
+ */
+function MenuItemStatus(menuItemLabel, enabled, checked)
+{
+ this.menuItemLabel = menuItemLabel;
+ this.enabled = enabled;
+ this.checked = checked;
+}
+
+/**
+ * @class ContextMenuItemStatus
+ * Stores the status of the context menu item.
+ *
+ * Since 5.2.0
+ *
+ * @param menuItemID The menu item id.
+ * @param enabled True if user wants to enable the menu item.
+ * @param checked True if user wants to check the menu item.
+ *
+ * @return MenuItemStatus object.
+ */
+function ContextMenuItemStatus(menuItemID, enabled, checked)
+{
+ this.menuItemID = menuItemID;
+ this.enabled = enabled;
+ this.checked = checked;
+}
+//------------------------------ CSInterface ----------------------------------
+
+/**
+ * @class CSInterface
+ * This is the entry point to the CEP extensibility infrastructure.
+ * Instantiate this object and use it to:
+ *
+ *
Access information about the host application in which an extension is running
+ *
Launch an extension
+ *
Register interest in event notifications, and dispatch events
+ *
+ *
+ * @return A new \c CSInterface object
+ */
+function CSInterface()
+{
+}
+
+/**
+ * User can add this event listener to handle native application theme color changes.
+ * Callback function gives extensions ability to fine-tune their theme color after the
+ * global theme color has been changed.
+ * The callback function should be like below:
+ *
+ * @example
+ * // event is a CSEvent object, but user can ignore it.
+ * function OnAppThemeColorChanged(event)
+ * {
+ * // Should get a latest HostEnvironment object from application.
+ * var skinInfo = JSON.parse(window.__adobe_cep__.getHostEnvironment()).appSkinInfo;
+ * // Gets the style information such as color info from the skinInfo,
+ * // and redraw all UI controls of your extension according to the style info.
+ * }
+ */
+CSInterface.THEME_COLOR_CHANGED_EVENT = "com.adobe.csxs.events.ThemeColorChanged";
+
+/** The host environment data object. */
+CSInterface.prototype.hostEnvironment = window.__adobe_cep__ ? JSON.parse(window.__adobe_cep__.getHostEnvironment()) : null;
+
+/** Retrieves information about the host environment in which the
+ * extension is currently running.
+ *
+ * @return A \c #HostEnvironment object.
+ */
+CSInterface.prototype.getHostEnvironment = function()
+{
+ this.hostEnvironment = JSON.parse(window.__adobe_cep__.getHostEnvironment());
+ return this.hostEnvironment;
+};
+
+/** Closes this extension. */
+CSInterface.prototype.closeExtension = function()
+{
+ window.__adobe_cep__.closeExtension();
+};
+
+/**
+ * Retrieves a path for which a constant is defined in the system.
+ *
+ * @param pathType The path-type constant defined in \c #SystemPath ,
+ *
+ * @return The platform-specific system path string.
+ */
+CSInterface.prototype.getSystemPath = function(pathType)
+{
+ var path = decodeURI(window.__adobe_cep__.getSystemPath(pathType));
+ var OSVersion = this.getOSInformation();
+ if (OSVersion.indexOf("Windows") >= 0)
+ {
+ path = path.replace("file:///", "");
+ }
+ else if (OSVersion.indexOf("Mac") >= 0)
+ {
+ path = path.replace("file://", "");
+ }
+ return path;
+};
+
+/**
+ * Evaluates a JavaScript script, which can use the JavaScript DOM
+ * of the host application.
+ *
+ * @param script The JavaScript script.
+ * @param callback Optional. A callback function that receives the result of execution.
+ * If execution fails, the callback function receives the error message \c EvalScript_ErrMessage.
+ */
+CSInterface.prototype.evalScript = function(script, callback)
+{
+ if(callback === null || callback === undefined)
+ {
+ callback = function(result){};
+ }
+ window.__adobe_cep__.evalScript(script, callback);
+};
+
+/**
+ * Retrieves the unique identifier of the application.
+ * in which the extension is currently running.
+ *
+ * @return The unique ID string.
+ */
+CSInterface.prototype.getApplicationID = function()
+{
+ var appId = this.hostEnvironment.appId;
+ return appId;
+};
+
+/**
+ * Retrieves host capability information for the application
+ * in which the extension is currently running.
+ *
+ * @return A \c #HostCapabilities object.
+ */
+CSInterface.prototype.getHostCapabilities = function()
+{
+ var hostCapabilities = JSON.parse(window.__adobe_cep__.getHostCapabilities() );
+ return hostCapabilities;
+};
+
+/**
+ * Triggers a CEP event programmatically. Yoy can use it to dispatch
+ * an event of a predefined type, or of a type you have defined.
+ *
+ * @param event A \c CSEvent object.
+ */
+CSInterface.prototype.dispatchEvent = function(event)
+{
+ if (typeof event.data == "object")
+ {
+ event.data = JSON.stringify(event.data);
+ }
+
+ window.__adobe_cep__.dispatchEvent(event);
+};
+
+/**
+ * Registers an interest in a CEP event of a particular type, and
+ * assigns an event handler.
+ * The event infrastructure notifies your extension when events of this type occur,
+ * passing the event object to the registered handler function.
+ *
+ * @param type The name of the event type of interest.
+ * @param listener The JavaScript handler function or method.
+ * @param obj Optional, the object containing the handler method, if any.
+ * Default is null.
+ */
+CSInterface.prototype.addEventListener = function(type, listener, obj)
+{
+ window.__adobe_cep__.addEventListener(type, listener, obj);
+};
+
+/**
+ * Removes a registered event listener.
+ *
+ * @param type The name of the event type of interest.
+ * @param listener The JavaScript handler function or method that was registered.
+ * @param obj Optional, the object containing the handler method, if any.
+ * Default is null.
+ */
+CSInterface.prototype.removeEventListener = function(type, listener, obj)
+{
+ window.__adobe_cep__.removeEventListener(type, listener, obj);
+};
+
+/**
+ * Loads and launches another extension, or activates the extension if it is already loaded.
+ *
+ * @param extensionId The extension's unique identifier.
+ * @param startupParams Not currently used, pass "".
+ *
+ * @example
+ * To launch the extension "help" with ID "HLP" from this extension, call:
+ * requestOpenExtension("HLP", "");
+ *
+ */
+CSInterface.prototype.requestOpenExtension = function(extensionId, params)
+{
+ window.__adobe_cep__.requestOpenExtension(extensionId, params);
+};
+
+/**
+ * Retrieves the list of extensions currently loaded in the current host application.
+ * The extension list is initialized once, and remains the same during the lifetime
+ * of the CEP session.
+ *
+ * @param extensionIds Optional, an array of unique identifiers for extensions of interest.
+ * If omitted, retrieves data for all extensions.
+ *
+ * @return Zero or more \c #Extension objects.
+ */
+CSInterface.prototype.getExtensions = function(extensionIds)
+{
+ var extensionIdsStr = JSON.stringify(extensionIds);
+ var extensionsStr = window.__adobe_cep__.getExtensions(extensionIdsStr);
+
+ var extensions = JSON.parse(extensionsStr);
+ return extensions;
+};
+
+/**
+ * Retrieves network-related preferences.
+ *
+ * @return A JavaScript object containing network preferences.
+ */
+CSInterface.prototype.getNetworkPreferences = function()
+{
+ var result = window.__adobe_cep__.getNetworkPreferences();
+ var networkPre = JSON.parse(result);
+
+ return networkPre;
+};
+
+/**
+ * Initializes the resource bundle for this extension with property values
+ * for the current application and locale.
+ * To support multiple locales, you must define a property file for each locale,
+ * containing keyed display-string values for that locale.
+ * See localization documentation for Extension Builder and related products.
+ *
+ * Keys can be in the
+ * form key.value="localized string", for use in HTML text elements.
+ * For example, in this input element, the localized \c key.value string is displayed
+ * instead of the empty \c value string:
+ *
+ *
+ *
+ * @return An object containing the resource bundle information.
+ */
+CSInterface.prototype.initResourceBundle = function()
+{
+ var resourceBundle = JSON.parse(window.__adobe_cep__.initResourceBundle());
+ var resElms = document.querySelectorAll('[data-locale]');
+ for (var n = 0; n < resElms.length; n++)
+ {
+ var resEl = resElms[n];
+ // Get the resource key from the element.
+ var resKey = resEl.getAttribute('data-locale');
+ if (resKey)
+ {
+ // Get all the resources that start with the key.
+ for (var key in resourceBundle)
+ {
+ if (key.indexOf(resKey) === 0)
+ {
+ var resValue = resourceBundle[key];
+ if (key.length == resKey.length)
+ {
+ resEl.innerHTML = resValue;
+ }
+ else if ('.' == key.charAt(resKey.length))
+ {
+ var attrKey = key.substring(resKey.length + 1);
+ resEl[attrKey] = resValue;
+ }
+ }
+ }
+ }
+ }
+ return resourceBundle;
+};
+
+/**
+ * Writes installation information to a file.
+ *
+ * @return The file path.
+ */
+CSInterface.prototype.dumpInstallationInfo = function()
+{
+ return window.__adobe_cep__.dumpInstallationInfo();
+};
+
+/**
+ * Retrieves version information for the current Operating System,
+ * See http://www.useragentstring.com/pages/Chrome/ for Chrome \c navigator.userAgent values.
+ *
+ * @return A string containing the OS version, or "unknown Operation System".
+ * If user customizes the User Agent by setting CEF command parameter "--user-agent", only
+ * "Mac OS X" or "Windows" will be returned.
+ */
+CSInterface.prototype.getOSInformation = function()
+{
+ var userAgent = navigator.userAgent;
+
+ if ((navigator.platform == "Win32") || (navigator.platform == "Windows"))
+ {
+ var winVersion = "Windows";
+ var winBit = "";
+ if (userAgent.indexOf("Windows") > -1)
+ {
+ if (userAgent.indexOf("Windows NT 5.0") > -1)
+ {
+ winVersion = "Windows 2000";
+ }
+ else if (userAgent.indexOf("Windows NT 5.1") > -1)
+ {
+ winVersion = "Windows XP";
+ }
+ else if (userAgent.indexOf("Windows NT 5.2") > -1)
+ {
+ winVersion = "Windows Server 2003";
+ }
+ else if (userAgent.indexOf("Windows NT 6.0") > -1)
+ {
+ winVersion = "Windows Vista";
+ }
+ else if (userAgent.indexOf("Windows NT 6.1") > -1)
+ {
+ winVersion = "Windows 7";
+ }
+ else if (userAgent.indexOf("Windows NT 6.2") > -1)
+ {
+ winVersion = "Windows 8";
+ }
+ else if (userAgent.indexOf("Windows NT 6.3") > -1)
+ {
+ winVersion = "Windows 8.1";
+ }
+ else if (userAgent.indexOf("Windows NT 10") > -1)
+ {
+ winVersion = "Windows 10";
+ }
+
+ if (userAgent.indexOf("WOW64") > -1 || userAgent.indexOf("Win64") > -1)
+ {
+ winBit = " 64-bit";
+ }
+ else
+ {
+ winBit = " 32-bit";
+ }
+ }
+
+ return winVersion + winBit;
+ }
+ else if ((navigator.platform == "MacIntel") || (navigator.platform == "Macintosh"))
+ {
+ var result = "Mac OS X";
+
+ if (userAgent.indexOf("Mac OS X") > -1)
+ {
+ result = userAgent.substring(userAgent.indexOf("Mac OS X"), userAgent.indexOf(")"));
+ result = result.replace(/_/g, ".");
+ }
+
+ return result;
+ }
+
+ return "Unknown Operation System";
+};
+
+/**
+ * Opens a page in the default system browser.
+ *
+ * Since 4.2.0
+ *
+ * @param url The URL of the page/file to open, or the email address.
+ * Must use HTTP/HTTPS/file/mailto protocol. For example:
+ * "http://www.adobe.com"
+ * "https://github.com"
+ * "file:///C:/log.txt"
+ * "mailto:test@adobe.com"
+ *
+ * @return One of these error codes:\n
+ *
\n
+ *
NO_ERROR - 0
\n
+ *
ERR_UNKNOWN - 1
\n
+ *
ERR_INVALID_PARAMS - 2
\n
+ *
ERR_INVALID_URL - 201
\n
+ *
\n
+ */
+CSInterface.prototype.openURLInDefaultBrowser = function(url)
+{
+ return cep.util.openURLInDefaultBrowser(url);
+};
+
+/**
+ * Retrieves extension ID.
+ *
+ * Since 4.2.0
+ *
+ * @return extension ID.
+ */
+CSInterface.prototype.getExtensionID = function()
+{
+ return window.__adobe_cep__.getExtensionId();
+};
+
+/**
+ * Retrieves the scale factor of screen.
+ * On Windows platform, the value of scale factor might be different from operating system's scale factor,
+ * since host application may use its self-defined scale factor.
+ *
+ * Since 4.2.0
+ *
+ * @return One of the following float number.
+ *
\n
+ *
-1.0 when error occurs
\n
+ *
1.0 means normal screen
\n
+ *
>1.0 means HiDPI screen
\n
+ *
\n
+ */
+CSInterface.prototype.getScaleFactor = function()
+{
+ return window.__adobe_cep__.getScaleFactor();
+};
+
+/**
+ * Set a handler to detect any changes of scale factor. This only works on Mac.
+ *
+ * Since 4.2.0
+ *
+ * @param handler The function to be called when scale factor is changed.
+ *
+ */
+CSInterface.prototype.setScaleFactorChangedHandler = function(handler)
+{
+ window.__adobe_cep__.setScaleFactorChangedHandler(handler);
+};
+
+/**
+ * Retrieves current API version.
+ *
+ * Since 4.2.0
+ *
+ * @return ApiVersion object.
+ *
+ */
+CSInterface.prototype.getCurrentApiVersion = function()
+{
+ var apiVersion = JSON.parse(window.__adobe_cep__.getCurrentApiVersion());
+ return apiVersion;
+};
+
+/**
+ * Set panel flyout menu by an XML.
+ *
+ * Since 5.2.0
+ *
+ * Register a callback function for "com.adobe.csxs.events.flyoutMenuClicked" to get notified when a
+ * menu item is clicked.
+ * The "data" attribute of event is an object which contains "menuId" and "menuName" attributes.
+ *
+ * Register callback functions for "com.adobe.csxs.events.flyoutMenuOpened" and "com.adobe.csxs.events.flyoutMenuClosed"
+ * respectively to get notified when flyout menu is opened or closed.
+ *
+ * @param menu A XML string which describes menu structure.
+ * An example menu XML:
+ *
+ *
+ */
+CSInterface.prototype.setPanelFlyoutMenu = function(menu)
+{
+ if ("string" != typeof menu)
+ {
+ return;
+ }
+
+ window.__adobe_cep__.invokeSync("setPanelFlyoutMenu", menu);
+};
+
+/**
+ * Updates a menu item in the extension window's flyout menu, by setting the enabled
+ * and selection status.
+ *
+ * Since 5.2.0
+ *
+ * @param menuItemLabel The menu item label.
+ * @param enabled True to enable the item, false to disable it (gray it out).
+ * @param checked True to select the item, false to deselect it.
+ *
+ * @return false when the host application does not support this functionality (HostCapabilities.EXTENDED_PANEL_MENU is false).
+ * Fails silently if menu label is invalid.
+ *
+ * @see HostCapabilities.EXTENDED_PANEL_MENU
+ */
+CSInterface.prototype.updatePanelMenuItem = function(menuItemLabel, enabled, checked)
+{
+ var ret = false;
+ if (this.getHostCapabilities().EXTENDED_PANEL_MENU)
+ {
+ var itemStatus = new MenuItemStatus(menuItemLabel, enabled, checked);
+ ret = window.__adobe_cep__.invokeSync("updatePanelMenuItem", JSON.stringify(itemStatus));
+ }
+ return ret;
+};
+
+
+/**
+ * Set context menu by XML string.
+ *
+ * Since 5.2.0
+ *
+ * There are a number of conventions used to communicate what type of menu item to create and how it should be handled.
+ * - an item without menu ID or menu name is disabled and is not shown.
+ * - if the item name is "---" (three hyphens) then it is treated as a separator. The menu ID in this case will always be NULL.
+ * - Checkable attribute takes precedence over Checked attribute.
+ * - a PNG icon. For optimal display results please supply a 16 x 16px icon as larger dimensions will increase the size of the menu item.
+ The Chrome extension contextMenus API was taken as a reference.
+ https://developer.chrome.com/extensions/contextMenus
+ * - the items with icons and checkable items cannot coexist on the same menu level. The former take precedences over the latter.
+ *
+ * @param menu A XML string which describes menu structure.
+ * @param callback The callback function which is called when a menu item is clicked. The only parameter is the returned ID of clicked menu item.
+ *
+ * @description An example menu XML:
+ *
+ */
+CSInterface.prototype.setContextMenu = function(menu, callback)
+{
+ if ("string" != typeof menu)
+ {
+ return;
+ }
+
+ window.__adobe_cep__.invokeAsync("setContextMenu", menu, callback);
+};
+
+/**
+ * Set context menu by JSON string.
+ *
+ * Since 6.0.0
+ *
+ * There are a number of conventions used to communicate what type of menu item to create and how it should be handled.
+ * - an item without menu ID or menu name is disabled and is not shown.
+ * - if the item label is "---" (three hyphens) then it is treated as a separator. The menu ID in this case will always be NULL.
+ * - Checkable attribute takes precedence over Checked attribute.
+ * - a PNG icon. For optimal display results please supply a 16 x 16px icon as larger dimensions will increase the size of the menu item.
+ The Chrome extension contextMenus API was taken as a reference.
+ * - the items with icons and checkable items cannot coexist on the same menu level. The former take precedences over the latter.
+ https://developer.chrome.com/extensions/contextMenus
+ *
+ * @param menu A JSON string which describes menu structure.
+ * @param callback The callback function which is called when a menu item is clicked. The only parameter is the returned ID of clicked menu item.
+ *
+ * @description An example menu JSON:
+ *
+ * {
+ * "menu": [
+ * {
+ * "id": "menuItemId1",
+ * "label": "testExample1",
+ * "enabled": true,
+ * "checkable": true,
+ * "checked": false,
+ * "icon": "./image/small_16X16.png"
+ * },
+ * {
+ * "id": "menuItemId2",
+ * "label": "testExample2",
+ * "menu": [
+ * {
+ * "id": "menuItemId2-1",
+ * "label": "testExample2-1",
+ * "menu": [
+ * {
+ * "id": "menuItemId2-1-1",
+ * "label": "testExample2-1-1",
+ * "enabled": false,
+ * "checkable": true,
+ * "checked": true
+ * }
+ * ]
+ * },
+ * {
+ * "id": "menuItemId2-2",
+ * "label": "testExample2-2",
+ * "enabled": true,
+ * "checkable": true,
+ * "checked": true
+ * }
+ * ]
+ * },
+ * {
+ * "label": "---"
+ * },
+ * {
+ * "id": "menuItemId3",
+ * "label": "testExample3",
+ * "enabled": false,
+ * "checkable": true,
+ * "checked": false
+ * }
+ * ]
+ * }
+ *
+ */
+CSInterface.prototype.setContextMenuByJSON = function(menu, callback)
+{
+ if ("string" != typeof menu)
+ {
+ return;
+ }
+
+ window.__adobe_cep__.invokeAsync("setContextMenuByJSON", menu, callback);
+};
+
+/**
+ * Updates a context menu item by setting the enabled and selection status.
+ *
+ * Since 5.2.0
+ *
+ * @param menuItemID The menu item ID.
+ * @param enabled True to enable the item, false to disable it (gray it out).
+ * @param checked True to select the item, false to deselect it.
+ */
+CSInterface.prototype.updateContextMenuItem = function(menuItemID, enabled, checked)
+{
+ var itemStatus = new ContextMenuItemStatus(menuItemID, enabled, checked);
+ ret = window.__adobe_cep__.invokeSync("updateContextMenuItem", JSON.stringify(itemStatus));
+};
+
+/**
+ * Get the visibility status of an extension window.
+ *
+ * Since 6.0.0
+ *
+ * @return true if the extension window is visible; false if the extension window is hidden.
+ */
+CSInterface.prototype.isWindowVisible = function()
+{
+ return window.__adobe_cep__.invokeSync("isWindowVisible", "");
+};
+
+/**
+ * Resize extension's content to the specified dimensions.
+ * 1. Works with modal and modeless extensions in all Adobe products.
+ * 2. Extension's manifest min/max size constraints apply and take precedence.
+ * 3. For panel extensions
+ * 3.1 This works in all Adobe products except:
+ * * Premiere Pro
+ * * Prelude
+ * * After Effects
+ * 3.2 When the panel is in certain states (especially when being docked),
+ * it will not change to the desired dimensions even when the
+ * specified size satisfies min/max constraints.
+ *
+ * Since 6.0.0
+ *
+ * @param width The new width
+ * @param height The new height
+ */
+CSInterface.prototype.resizeContent = function(width, height)
+{
+ window.__adobe_cep__.resizeContent(width, height);
+};
+
+/**
+ * Register the invalid certificate callback for an extension.
+ * This callback will be triggered when the extension tries to access the web site that contains the invalid certificate on the main frame.
+ * But if the extension does not call this function and tries to access the web site containing the invalid certificate, a default error page will be shown.
+ *
+ * Since 6.1.0
+ *
+ * @param callback the callback function
+ */
+CSInterface.prototype.registerInvalidCertificateCallback = function(callback)
+{
+ return window.__adobe_cep__.registerInvalidCertificateCallback(callback);
+};
+
+/**
+ * Register an interest in some key events to prevent them from being sent to the host application.
+ *
+ * This function works with modeless extensions and panel extensions.
+ * Generally all the key events will be sent to the host application for these two extensions if the current focused element
+ * is not text input or dropdown,
+ * If you want to intercept some key events and want them to be handled in the extension, please call this function
+ * in advance to prevent them being sent to the host application.
+ *
+ * Since 6.1.0
+ *
+ * @param keyEventsInterest A JSON string describing those key events you are interested in. A null object or
+ an empty string will lead to removing the interest
+ *
+ * This JSON string should be an array, each object has following keys:
+ *
+ * keyCode: [Required] represents an OS system dependent virtual key code identifying
+ * the unmodified value of the pressed key.
+ * ctrlKey: [optional] a Boolean that indicates if the control key was pressed (true) or not (false) when the event occurred.
+ * altKey: [optional] a Boolean that indicates if the alt key was pressed (true) or not (false) when the event occurred.
+ * shiftKey: [optional] a Boolean that indicates if the shift key was pressed (true) or not (false) when the event occurred.
+ * metaKey: [optional] (Mac Only) a Boolean that indicates if the Meta key was pressed (true) or not (false) when the event occurred.
+ * On Macintosh keyboards, this is the command key. To detect Windows key on Windows, please use keyCode instead.
+ * An example JSON string:
+ *
+ * [
+ * {
+ * "keyCode": 48
+ * },
+ * {
+ * "keyCode": 123,
+ * "ctrlKey": true
+ * },
+ * {
+ * "keyCode": 123,
+ * "ctrlKey": true,
+ * "metaKey": true
+ * }
+ * ]
+ *
+ */
+CSInterface.prototype.registerKeyEventsInterest = function(keyEventsInterest)
+{
+ return window.__adobe_cep__.registerKeyEventsInterest(keyEventsInterest);
+};
+
+/**
+ * Set the title of the extension window.
+ * This function works with modal and modeless extensions in all Adobe products, and panel extensions in Photoshop, InDesign, InCopy, Illustrator, Flash Pro and Dreamweaver.
+ *
+ * Since 6.1.0
+ *
+ * @param title The window title.
+ */
+CSInterface.prototype.setWindowTitle = function(title)
+{
+ window.__adobe_cep__.invokeSync("setWindowTitle", title);
+};
+
+/**
+ * Get the title of the extension window.
+ * This function works with modal and modeless extensions in all Adobe products, and panel extensions in Photoshop, InDesign, InCopy, Illustrator, Flash Pro and Dreamweaver.
+ *
+ * Since 6.1.0
+ *
+ * @return The window title.
+ */
+CSInterface.prototype.getWindowTitle = function()
+{
+ return window.__adobe_cep__.invokeSync("getWindowTitle", "");
+};
diff --git a/openpype/hosts/photoshop/api/extension/client/client.js b/openpype/hosts/photoshop/api/extension/client/client.js
new file mode 100644
index 0000000000..f4ba4cfe47
--- /dev/null
+++ b/openpype/hosts/photoshop/api/extension/client/client.js
@@ -0,0 +1,300 @@
+ // client facing part of extension, creates WSRPC client (jsx cannot
+ // do that)
+ // consumes RPC calls from server (OpenPype) calls ./host/index.jsx and
+ // returns values back (in json format)
+
+ var logReturn = function(result){ log.warn('Result: ' + result);};
+
+ var csInterface = new CSInterface();
+
+ log.warn("script start");
+
+ WSRPC.DEBUG = false;
+ WSRPC.TRACE = false;
+
+ function myCallBack(){
+ log.warn("Triggered index.jsx");
+ }
+ // importing through manifest.xml isn't working because relative paths
+ // possibly TODO
+ jsx.evalFile('./host/index.jsx', myCallBack);
+
+ 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);
+ });
+ }
+
+ /** main entry point **/
+ startUp("WEBSOCKET_URL");
+
+ // get websocket server url from environment value
+ async function startUp(url){
+ log.warn("url", url);
+ promis = runEvalScript("getEnv('" + url + "')");
+
+ var res = await promis;
+ // 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 version
+ }
+
+ function main(websocket_url){
+ // creates connection to 'websocket_url', registers routes
+ log.warn("websocket_url", websocket_url);
+ var default_url = 'ws://localhost:8099/ws/';
+
+ if (websocket_url == ''){
+ websocket_url = default_url;
+ }
+ log.warn("connecting to:", websocket_url);
+ RPC = new WSRPC(websocket_url, 5000); // spin connection
+
+ RPC.connect();
+
+ log.warn("connected");
+
+ function EscapeStringForJSX(str){
+ // Replaces:
+ // \ with \\
+ // ' with \'
+ // " with \"
+ // See: https://stackoverflow.com/a/3967927/5285364
+ return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"');
+ }
+
+ RPC.addRoute('Photoshop.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('Photoshop.read', function (data) {
+ log.warn('Server called client route "read":', data);
+ return runEvalScript("getHeadline()")
+ .then(function(result){
+ log.warn("getHeadline: " + result);
+ return result;
+ });
+ });
+
+ RPC.addRoute('Photoshop.get_layers', function (data) {
+ log.warn('Server called client route "get_layers":', data);
+ return runEvalScript("getLayers()")
+ .then(function(result){
+ log.warn("getLayers: " + result);
+ return result;
+ });
+ });
+
+ RPC.addRoute('Photoshop.set_visible', function (data) {
+ log.warn('Server called client route "set_visible":', data);
+ return runEvalScript("setVisible(" + data.layer_id + ", " +
+ data.visibility + ")")
+ .then(function(result){
+ log.warn("setVisible: " + result);
+ return result;
+ });
+ });
+
+ RPC.addRoute('Photoshop.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("save: " + result);
+ return result;
+ });
+ });
+
+ RPC.addRoute('Photoshop.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("save: " + result);
+ return result;
+ });
+ });
+
+ RPC.addRoute('Photoshop.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('Photoshop.get_selected_layers', function (data) {
+ log.warn('Server called client route "get_selected_layers":', data);
+
+ return runEvalScript("getSelectedLayers()")
+ .then(function(result){
+ log.warn("get_selected_layers: " + result);
+ return result;
+ });
+ });
+
+ RPC.addRoute('Photoshop.create_group', function (data) {
+ log.warn('Server called client route "create_group":', data);
+
+ return runEvalScript("createGroup('" + data.name + "')")
+ .then(function(result){
+ log.warn("createGroup: " + result);
+ return result;
+ });
+ });
+
+ RPC.addRoute('Photoshop.group_selected_layers', function (data) {
+ log.warn('Server called client route "group_selected_layers":',
+ data);
+
+ return runEvalScript("groupSelectedLayers(null, "+
+ "'" + data.name +"')")
+ .then(function(result){
+ log.warn("group_selected_layers: " + result);
+ return result;
+ });
+ });
+
+ RPC.addRoute('Photoshop.import_smart_object', function (data) {
+ log.warn('Server called client "import_smart_object":', data);
+ var escapedPath = EscapeStringForJSX(data.path);
+ return runEvalScript("importSmartObject('" + escapedPath +"', " +
+ "'"+ data.name +"',"+
+ + data.as_reference +")")
+ .then(function(result){
+ log.warn("import_smart_object: " + result);
+ return result;
+ });
+ });
+
+ RPC.addRoute('Photoshop.replace_smart_object', function (data) {
+ log.warn('Server called route "replace_smart_object":', data);
+ var escapedPath = EscapeStringForJSX(data.path);
+ return runEvalScript("replaceSmartObjects("+data.layer_id+"," +
+ "'" + escapedPath +"',"+
+ "'"+ data.name +"')")
+ .then(function(result){
+ log.warn("replaceSmartObjects: " + result);
+ return result;
+ });
+ });
+
+ RPC.addRoute('Photoshop.delete_layer', function (data) {
+ log.warn('Server called route "delete_layer":', data);
+ return runEvalScript("deleteLayer("+data.layer_id+")")
+ .then(function(result){
+ log.warn("delete_layer: " + result);
+ return result;
+ });
+ });
+
+ RPC.addRoute('Photoshop.rename_layer', function (data) {
+ log.warn('Server called route "rename_layer":', data);
+ return runEvalScript("renameLayer("+data.layer_id+", " +
+ "'"+ data.name +"')")
+ .then(function(result){
+ log.warn("rename_layer: " + result);
+ return result;
+ });
+});
+
+ RPC.addRoute('Photoshop.select_layers', function (data) {
+ log.warn('Server called client route "select_layers":', data);
+
+ return runEvalScript("selectLayers('" + data.layers +"')")
+ .then(function(result){
+ log.warn("select_layers: " + result);
+ return result;
+ });
+ });
+
+ RPC.addRoute('Photoshop.is_saved', function (data) {
+ log.warn('Server called client route "is_saved":', data);
+
+ return runEvalScript("isSaved()")
+ .then(function(result){
+ log.warn("is_saved: " + result);
+ return result;
+ });
+ });
+
+ RPC.addRoute('Photoshop.saveAs', function (data) {
+ log.warn('Server called client route "saveAsJPEG":', data);
+ var escapedPath = EscapeStringForJSX(data.image_path);
+ return runEvalScript("saveAs('" + escapedPath + "', " +
+ "'" + data.ext + "', " +
+ data.as_copy + ")")
+ .then(function(result){
+ log.warn("save: " + result);
+ return result;
+ });
+ });
+
+ RPC.addRoute('Photoshop.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('Photoshop.get_extension_version', function (data) {
+ log.warn('Server called client route "get_extension_version":', data);
+ return get_extension_version();
+ });
+
+ RPC.addRoute('Photoshop.close', function (data) {
+ log.warn('Server called client route "close":', data);
+ return runEvalScript("close()");
+ });
+
+ RPC.call('Photoshop.ping').then(function (data) {
+ log.warn('Result for calling server route "ping": ', data);
+ return runEvalScript("ping()")
+ .then(function(result){
+ log.warn("ping: " + result);
+ return result;
+ });
+
+ }, function (error) {
+ log.warn(error);
+ });
+
+ }
+
+ log.warn("end script");
diff --git a/openpype/hosts/photoshop/api/extension/client/loglevel.min.js b/openpype/hosts/photoshop/api/extension/client/loglevel.min.js
new file mode 100644
index 0000000000..648d7e9ff6
--- /dev/null
+++ b/openpype/hosts/photoshop/api/extension/client/loglevel.min.js
@@ -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=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 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
diff --git a/openpype/hosts/photoshop/api/extension/client/wsrpc.min.js b/openpype/hosts/photoshop/api/extension/client/wsrpc.min.js
new file mode 100644
index 0000000000..f1264b91c4
--- /dev/null
+++ b/openpype/hosts/photoshop/api/extension/client/wsrpc.min.js
@@ -0,0 +1 @@
+!function(global,factory){"object"==typeof exports&&"undefined"!=typeof module?module.exports=factory():"function"==typeof define&&define.amd?define(factory):(global=global||self).WSRPC=factory()}(this,function(){"use strict";function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function Deferred(){_classCallCheck(this,Deferred);var self=this;function wrapper(func){return function(){if(!self.done)return self.done=!0,func.apply(this,arguments);console.error(new Error("Promise already done"))}}return self.resolve=null,self.reject=null,self.done=!1,self.promise=new Promise(function(resolve,reject){self.resolve=wrapper(resolve),self.reject=wrapper(reject)}),self.promise.isPending=function(){return!self.done},self}function logGroup(group,level,args){console.group(group),console[level].apply(this,args),console.groupEnd()}function log(){WSRPC.DEBUG&&logGroup("WSRPC.DEBUG","trace",arguments)}function trace(msg){if(WSRPC.TRACE){var payload=msg;"data"in msg&&(payload=JSON.parse(msg.data)),logGroup("WSRPC.TRACE","trace",[payload])}}var readyState=Object.freeze({0:"CONNECTING",1:"OPEN",2:"CLOSING",3:"CLOSED"}),WSRPC=function WSRPC(URL){var reconnectTimeout=1 //
+// forceEval is now by default true //
+// It wraps the scripts in a try catch and an eval providing useful error handling //
+// One can set in the jsx engine $.includeStack = true to return the call stack in the event of an error //
+///////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////
+// JSX.js for calling jsx code from the js engine //
+// 2 methods included //
+// 1) jsx.evalScript AKA jsx.eval //
+// 2) jsx.evalFile AKA jsx.file //
+// Special features //
+// 1) Allows all changes in your jsx code to be reloaded into your extension at the click of a button //
+// 2) Can enable the $.fileName property to work and provides a $.__fileName() method as an alternative //
+// 3) Can force a callBack result from InDesign //
+// 4) No more csInterface.evalScript('alert("hello "' + title + " " + name + '");') //
+// use jsx.evalScript('alert("hello __title__ __name__");', {title: title, name: name}); //
+// 5) execute jsx files from your jsx folder like this jsx.evalFile('myFabJsxScript.jsx'); //
+// or from a relative path jsx.evalFile('../myFabScripts/myFabJsxScript.jsx'); //
+// or from an absolute url jsx.evalFile('/Path/to/my/FabJsxScript.jsx'); (mac) //
+// or from an absolute url jsx.evalFile('C:Path/to/my/FabJsxScript.jsx'); (windows) //
+// 6) Parameter can be entered in the from of a parameter list which can be in any order or as an object //
+// 7) Not camelCase sensitive (very useful for the illiterate) //
+// Dead easy to use BUT SPEND THE 3 TO 5 MINUTES IT SHOULD TAKE TO READ THE INSTRUCTIONS //
+///////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/* jshint undef:true, unused:true, esversion:6 */
+
+//////////////////////////////////////
+// jsx is the interface for the API //
+//////////////////////////////////////
+
+var jsx;
+
+// Wrap everything in an anonymous function to prevent leeks
+(function() {
+ /////////////////////////////////////////////////////////////////////
+ // Substitute some CSInterface functions to avoid dependency on it //
+ /////////////////////////////////////////////////////////////////////
+
+ var __dirname = (function() {
+ var path, isMac;
+ path = decodeURI(window.__adobe_cep__.getSystemPath('extension'));
+ isMac = navigator.platform[0] === 'M'; // [M]ac
+ path = path.replace('file://' + (isMac ? '' : '/'), '');
+ return path;
+ })();
+
+ var evalScript = function(script, callback) {
+ callback = callback || function() {};
+ window.__adobe_cep__.evalScript(script, callback);
+ };
+
+
+ ////////////////////////////////////////////
+ // In place of using the node path module //
+ ////////////////////////////////////////////
+
+ // jshint undef: true, unused: true
+
+ // A very minified version of the NodeJs Path module!!
+ // For use outside of NodeJs
+ // Majorly nicked by Trevor from Joyent
+ var path = (function() {
+
+ var isString = function(arg) {
+ return typeof arg === 'string';
+ };
+
+ // var isObject = function(arg) {
+ // return typeof arg === 'object' && arg !== null;
+ // };
+
+ var basename = function(path) {
+ if (!isString(path)) {
+ throw new TypeError('Argument to path.basename must be a string');
+ }
+ var bits = path.split(/[\/\\]/g);
+ return bits[bits.length - 1];
+ };
+
+ // jshint undef: true
+ // Regex to split a windows path into three parts: [*, device, slash,
+ // tail] windows-only
+ var splitDeviceRe =
+ /^([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/]+[^\\\/]+)?([\\\/])?([\s\S]*?)$/;
+
+ // Regex to split the tail part of the above into [*, dir, basename, ext]
+ // var splitTailRe =
+ // /^([\s\S]*?)((?:\.{1,2}|[^\\\/]+?|)(\.[^.\/\\]*|))(?:[\\\/]*)$/;
+
+ var win32 = {};
+ // Function to split a filename into [root, dir, basename, ext]
+ // var win32SplitPath = function(filename) {
+ // // Separate device+slash from tail
+ // var result = splitDeviceRe.exec(filename),
+ // device = (result[1] || '') + (result[2] || ''),
+ // tail = result[3] || '';
+ // // Split the tail into dir, basename and extension
+ // var result2 = splitTailRe.exec(tail),
+ // dir = result2[1],
+ // basename = result2[2],
+ // ext = result2[3];
+ // return [device, dir, basename, ext];
+ // };
+
+ var win32StatPath = function(path) {
+ var result = splitDeviceRe.exec(path),
+ device = result[1] || '',
+ isUnc = !!device && device[1] !== ':';
+ return {
+ device: device,
+ isUnc: isUnc,
+ isAbsolute: isUnc || !!result[2], // UNC paths are always absolute
+ tail: result[3]
+ };
+ };
+
+ var normalizeUNCRoot = function(device) {
+ return '\\\\' + device.replace(/^[\\\/]+/, '').replace(/[\\\/]+/g, '\\');
+ };
+
+ var normalizeArray = function(parts, allowAboveRoot) {
+ var res = [];
+ for (var i = 0; i < parts.length; i++) {
+ var p = parts[i];
+
+ // ignore empty parts
+ if (!p || p === '.')
+ continue;
+
+ if (p === '..') {
+ if (res.length && res[res.length - 1] !== '..') {
+ res.pop();
+ } else if (allowAboveRoot) {
+ res.push('..');
+ }
+ } else {
+ res.push(p);
+ }
+ }
+
+ return res;
+ };
+
+ win32.normalize = function(path) {
+ var result = win32StatPath(path),
+ device = result.device,
+ isUnc = result.isUnc,
+ isAbsolute = result.isAbsolute,
+ tail = result.tail,
+ trailingSlash = /[\\\/]$/.test(tail);
+
+ // Normalize the tail path
+ tail = normalizeArray(tail.split(/[\\\/]+/), !isAbsolute).join('\\');
+
+ if (!tail && !isAbsolute) {
+ tail = '.';
+ }
+ if (tail && trailingSlash) {
+ tail += '\\';
+ }
+
+ // Convert slashes to backslashes when `device` points to an UNC root.
+ // Also squash multiple slashes into a single one where appropriate.
+ if (isUnc) {
+ device = normalizeUNCRoot(device);
+ }
+
+ return device + (isAbsolute ? '\\' : '') + tail;
+ };
+ win32.join = function() {
+ var paths = [];
+ for (var i = 0; i < arguments.length; i++) {
+ var arg = arguments[i];
+ if (!isString(arg)) {
+ throw new TypeError('Arguments to path.join must be strings');
+ }
+ if (arg) {
+ paths.push(arg);
+ }
+ }
+
+ var joined = paths.join('\\');
+
+ // Make sure that the joined path doesn't start with two slashes, because
+ // normalize() will mistake it for an UNC path then.
+ //
+ // This step is skipped when it is very clear that the user actually
+ // intended to point at an UNC path. This is assumed when the first
+ // non-empty string arguments starts with exactly two slashes followed by
+ // at least one more non-slash character.
+ //
+ // Note that for normalize() to treat a path as an UNC path it needs to
+ // have at least 2 components, so we don't filter for that here.
+ // This means that the user can use join to construct UNC paths from
+ // a server name and a share name; for example:
+ // path.join('//server', 'share') -> '\\\\server\\share\')
+ if (!/^[\\\/]{2}[^\\\/]/.test(paths[0])) {
+ joined = joined.replace(/^[\\\/]{2,}/, '\\');
+ }
+ return win32.normalize(joined);
+ };
+
+ var posix = {};
+
+ // posix version
+ posix.join = function() {
+ var path = '';
+ for (var i = 0; i < arguments.length; i++) {
+ var segment = arguments[i];
+ if (!isString(segment)) {
+ throw new TypeError('Arguments to path.join must be strings');
+ }
+ if (segment) {
+ if (!path) {
+ path += segment;
+ } else {
+ path += '/' + segment;
+ }
+ }
+ }
+ return posix.normalize(path);
+ };
+
+ // path.normalize(path)
+ // posix version
+ posix.normalize = function(path) {
+ var isAbsolute = path.charAt(0) === '/',
+ trailingSlash = path && path[path.length - 1] === '/';
+
+ // Normalize the path
+ path = normalizeArray(path.split('/'), !isAbsolute).join('/');
+
+ if (!path && !isAbsolute) {
+ path = '.';
+ }
+ if (path && trailingSlash) {
+ path += '/';
+ }
+
+ return (isAbsolute ? '/' : '') + path;
+ };
+
+ win32.basename = posix.basename = basename;
+
+ this.win32 = win32;
+ this.posix = posix;
+ return (navigator.platform[0] === 'M') ? posix : win32;
+ })();
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // The is the "main" function which is to be prototyped //
+ // It run a small snippet in the jsx engine that //
+ // 1) Assigns $.__dirname with the value of the extensions __dirname base path //
+ // 2) Sets up a method $.__fileName() for retrieving from within the jsx script it's $.fileName value //
+ // more on that method later //
+ // At the end of the script the global declaration jsx = new Jsx(); has been made. //
+ // If you like you can remove that and include in your relevant functions //
+ // var jsx = new Jsx(); You would never call the Jsx function without the "new" declaration //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////
+ var Jsx = function() {
+ var jsxScript;
+ // Setup jsx function to enable the jsx scripts to easily retrieve their file location
+ jsxScript = [
+ '$.level = 0;',
+ 'if(!$.__fileNames){',
+ ' $.__fileNames = {};',
+ ' $.__dirname = "__dirname__";'.replace('__dirname__', __dirname),
+ ' $.__fileName = function(name){',
+ ' name = name || $.fileName;',
+ ' return ($.__fileNames && $.__fileNames[name]) || $.fileName;',
+ ' };',
+ '}'
+ ].join('');
+ evalScript(jsxScript);
+ return this;
+ };
+
+ /**
+ * [evalScript] For calling jsx scripts from the js engine
+ *
+ * The jsx.evalScript method is used for calling jsx scripts directly from the js engine
+ * Allows for easy replacement i.e. variable insertions and for forcing eval.
+ * For convenience jsx.eval or jsx.script or jsx.evalscript can be used instead of calling jsx.evalScript
+ *
+ * @param {String} jsxScript
+ * The string that makes up the jsx script
+ * it can contain a simple template like syntax for replacements
+ * 'alert("__foo__");'
+ * the __foo__ will be replaced as per the replacements parameter
+ *
+ * @param {Function} callback
+ * The callback function you want the jsx script to trigger on completion
+ * The result of the jsx script is passed as the argument to that function
+ * The function can exist in some other file.
+ * Note that InDesign does not automatically pass the callBack as a string.
+ * Either write your InDesign in a way that it returns a sting the form of
+ * return 'this is my result surrounded by quotes'
+ * or use the force eval option
+ * [Optional DEFAULT no callBack]
+ *
+ * @param {Object} replacements
+ * The replacements to make on the jsx script
+ * given the following script (template)
+ * 'alert("__message__: " + __val__);'
+ * and we want to change the script to
+ * 'alert("I was born in the year: " + 1234);'
+ * we would pass the following object
+ * {"message": 'I was born in the year', "val": 1234}
+ * or if not using reserved words like do we can leave out the key quotes
+ * {message: 'I was born in the year', val: 1234}
+ * [Optional DEFAULT no replacements]
+ *
+ * @param {Bolean} forceEval
+ * If the script should be wrapped in an eval and try catch
+ * This will 1) provide useful error feedback if heaven forbid it is needed
+ * 2) The result will be a string which is required for callback results in InDesign
+ * [Optional DEFAULT true]
+ *
+ * Note 1) The order of the parameters is irrelevant
+ * Note 2) One can pass the arguments as an object if desired
+ * jsx.evalScript(myCallBackFunction, 'alert("__myMessage__");', true);
+ * is the same as
+ * jsx.evalScript({
+ * script: 'alert("__myMessage__");',
+ * replacements: {myMessage: 'Hi there'},
+ * callBack: myCallBackFunction,
+ * eval: true
+ * });
+ * note that either lower or camelCase key names are valid
+ * i.e. both callback or callBack will work
+ *
+ * The following keys are the same jsx || script || jsxScript || jsxscript || file
+ * The following keys are the same callBack || callback
+ * The following keys are the same replacements || replace
+ * The following keys are the same eval || forceEval || forceeval
+ * The following keys are the same forceEvalScript || forceevalscript || evalScript || evalscript;
+ *
+ * @return {Boolean} if the jsxScript was executed or not
+ */
+
+ Jsx.prototype.evalScript = function() {
+ var arg, i, key, replaceThis, withThis, args, callback, forceEval, replacements, jsxScript, isBin;
+
+ //////////////////////////////////////////////////////////////////////////////////////
+ // sort out order which arguments into jsxScript, callback, replacements, forceEval //
+ //////////////////////////////////////////////////////////////////////////////////////
+
+ args = arguments;
+
+ // Detect if the parameters were passed as an object and if so allow for various keys
+ if (args.length === 1 && (arg = args[0]) instanceof Object) {
+ jsxScript = arg.jsxScript || arg.jsx || arg.script || arg.file || arg.jsxscript;
+ callback = arg.callBack || arg.callback;
+ replacements = arg.replacements || arg.replace;
+ forceEval = arg.eval || arg.forceEval || arg.forceeval;
+ } else {
+ for (i = 0; i < 4; i++) {
+ arg = args[i];
+ if (arg === undefined) {
+ continue;
+ }
+ if (arg.constructor === String) {
+ jsxScript = arg;
+ continue;
+ }
+ if (arg.constructor === Object) {
+ replacements = arg;
+ continue;
+ }
+ if (arg.constructor === Function) {
+ callback = arg;
+ continue;
+ }
+ if (arg === false) {
+ forceEval = false;
+ }
+ }
+ }
+
+ // If no script provide then not too much to do!
+ if (!jsxScript) {
+ return false;
+ }
+
+ // Have changed the forceEval default to be true as I prefer the error handling
+ if (forceEval !== false) {
+ forceEval = true;
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // On Illustrator and other apps the result of the jsx script is automatically passed as a string //
+ // if you have a "script" containing the single number 1 and nothing else then the callBack will register as "1" //
+ // On InDesign that same script will provide a blank callBack //
+ // Let's say we have a callBack function var callBack = function(result){alert(result);} //
+ // On Ai your see the 1 in the alert //
+ // On ID your just see a blank alert //
+ // To see the 1 in the alert you need to convert the result to a string and then it will show //
+ // So if we rewrite out 1 byte script to '1' i.e. surround the 1 in quotes then the call back alert will show 1 //
+ // If the scripts planed one can make sure that the results always passed as a string (including errors) //
+ // otherwise one can wrap the script in an eval and then have the result passed as a string //
+ // I have not gone through all the apps but can say //
+ // for Ai you never need to set the forceEval to true //
+ // for ID you if you have not coded your script appropriately and your want to send a result to the callBack then set forceEval to true //
+ // I changed this that even on Illustrator it applies the try catch, Note the try catch will fail if $.level is set to 1 //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ if (forceEval) {
+
+ isBin = (jsxScript.substring(0, 10) === '@JSXBIN@ES') ? '' : '\n';
+ jsxScript = (
+ // "\n''') + '';} catch(e){(function(e){var n, a=[]; for (n in e){a.push(n + ': ' + e[n])}; return a.join('\n')})(e)}");
+ // "\n''') + '';} catch(e){e + (e.line ? ('\\nLine ' + (+e.line - 1)) : '')}");
+ [
+ "$.level = 0;",
+ "try{eval('''" + isBin, // need to add an extra line otherwise #targetengine doesn't work ;-]
+ jsxScript.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"') + "\n''') + '';",
+ "} catch (e) {",
+ " (function(e) {",
+ " var line, sourceLine, name, description, ErrorMessage, fileName, start, end, bug;",
+ " line = +e.line" + (isBin === '' ? ';' : ' - 1;'), // To take into account the extra line added
+ " fileName = File(e.fileName).fsName;",
+ " sourceLine = line && e.source.split(/[\\r\\n]/)[line];",
+ " name = e.name;",
+ " description = e.description;",
+ " ErrorMessage = name + ' ' + e.number + ': ' + description;",
+ " if (fileName.length && !(/[\\/\\\\]\\d+$/.test(fileName))) {",
+ " ErrorMessage += '\\nFile: ' + fileName;",
+ " line++;",
+ " }",
+ " if (line){",
+ " ErrorMessage += '\\nLine: ' + line +",
+ " '-> ' + ((sourceLine.length < 300) ? sourceLine : sourceLine.substring(0,300) + '...');",
+ " }",
+ " if (e.start) {ErrorMessage += '\\nBug: ' + e.source.substring(e.start - 1, e.end)}",
+ " if ($.includeStack) {ErrorMessage += '\\nStack:' + $.stack;}",
+ " return ErrorMessage;",
+ " })(e);",
+ "}"
+ ].join('')
+ );
+
+ }
+
+ /////////////////////////////////////////////////////////////
+ // deal with the replacements //
+ // Note it's probably better to use ${template} `literals` //
+ /////////////////////////////////////////////////////////////
+
+ if (replacements) {
+ for (key in replacements) {
+ if (replacements.hasOwnProperty(key)) {
+ replaceThis = new RegExp('__' + key + '__', 'g');
+ withThis = replacements[key];
+ jsxScript = jsxScript.replace(replaceThis, withThis + '');
+ }
+ }
+ }
+
+
+ try {
+ evalScript(jsxScript, callback);
+ return true;
+ } catch (err) {
+ ////////////////////////////////////////////////
+ // Do whatever error handling you want here ! //
+ ////////////////////////////////////////////////
+ var newErr;
+ newErr = new Error(err);
+ alert('Error Eek: ' + newErr.stack);
+ return false;
+ }
+
+ };
+
+
+ /**
+ * [evalFile] For calling jsx scripts from the js engine
+ *
+ * The jsx.evalFiles method is used for executing saved jsx scripts
+ * where the jsxScript parameter is a string of the jsx scripts file location.
+ * For convenience jsx.file or jsx.evalfile can be used instead of jsx.evalFile
+ *
+ * @param {String} file
+ * The path to jsx script
+ * If only the base name is provided then the path will be presumed to be the
+ * To execute files stored in the jsx folder located in the __dirname folder use
+ * jsx.evalFile('myFabJsxScript.jsx');
+ * To execute files stored in the a folder myFabScripts located in the __dirname folder use
+ * jsx.evalFile('./myFabScripts/myFabJsxScript.jsx');
+ * To execute files stored in the a folder myFabScripts located at an absolute url use
+ * jsx.evalFile('/Path/to/my/FabJsxScript.jsx'); (mac)
+ * or jsx.evalFile('C:Path/to/my/FabJsxScript.jsx'); (windows)
+ *
+ * @param {Function} callback
+ * The callback function you want the jsx script to trigger on completion
+ * The result of the jsx script is passed as the argument to that function
+ * The function can exist in some other file.
+ * Note that InDesign does not automatically pass the callBack as a string.
+ * Either write your InDesign in a way that it returns a sting the form of
+ * return 'this is my result surrounded by quotes'
+ * or use the force eval option
+ * [Optional DEFAULT no callBack]
+ *
+ * @param {Object} replacements
+ * The replacements to make on the jsx script
+ * give the following script (template)
+ * 'alert("__message__: " + __val__);'
+ * and we want to change the script to
+ * 'alert("I was born in the year: " + 1234);'
+ * we would pass the following object
+ * {"message": 'I was born in the year', "val": 1234}
+ * or if not using reserved words like do we can leave out the key quotes
+ * {message: 'I was born in the year', val: 1234}
+ * By default when possible the forceEvalScript will be set to true
+ * The forceEvalScript option cannot be true when there are replacements
+ * To force the forceEvalScript to be false you can send a blank set of replacements
+ * jsx.evalFile('myFabScript.jsx', {}); Will NOT be executed using the $.evalScript method
+ * jsx.evalFile('myFabScript.jsx'); Will YES be executed using the $.evalScript method
+ * see the forceEvalScript parameter for details on this
+ * [Optional DEFAULT no replacements]
+ *
+ * @param {Bolean} forceEval
+ * If the script should be wrapped in an eval and try catch
+ * This will 1) provide useful error feedback if heaven forbid it is needed
+ * 2) The result will be a string which is required for callback results in InDesign
+ * [Optional DEFAULT true]
+ *
+ * If no replacements are needed then the jsx script is be executed by using the $.evalFile method
+ * This exposes the true value of the $.fileName property
+ * In such a case it's best to avoid using the $.__fileName() with no base name as it won't work
+ * BUT one can still use the $.__fileName('baseName') method which is more accurate than the standard $.fileName property
+ * Let's say you have a Drive called "Graphics" AND YOU HAVE a root folder on your "main" drive called "Graphics"
+ * You call a script jsx.evalFile('/Volumes/Graphics/myFabScript.jsx');
+ * $.fileName will give you '/Graphics/myFabScript.jsx' which is wrong
+ * $.__fileName('myFabScript.jsx') will give you '/Volumes/Graphics/myFabScript.jsx' which is correct
+ * $.__fileName() will not give you a reliable result
+ * Note that if your calling multiple versions of myFabScript.jsx stored in multiple folders then you can get stuffed!
+ * i.e. if the fileName is important to you then don't do that.
+ * It also will force the result of the jsx file as a string which is particularly useful for InDesign callBacks
+ *
+ * Note 1) The order of the parameters is irrelevant
+ * Note 2) One can pass the arguments as an object if desired
+ * jsx.evalScript(myCallBackFunction, 'alert("__myMessage__");', true);
+ * is the same as
+ * jsx.evalScript({
+ * script: 'alert("__myMessage__");',
+ * replacements: {myMessage: 'Hi there'},
+ * callBack: myCallBackFunction,
+ * eval: false,
+ * });
+ * note that either lower or camelCase key names or valid
+ * i.e. both callback or callBack will work
+ *
+ * The following keys are the same file || jsx || script || jsxScript || jsxscript
+ * The following keys are the same callBack || callback
+ * The following keys are the same replacements || replace
+ * The following keys are the same eval || forceEval || forceeval
+ *
+ * @return {Boolean} if the jsxScript was executed or not
+ */
+
+ Jsx.prototype.evalFile = function() {
+ var arg, args, callback, fileName, fileNameScript, forceEval, forceEvalScript,
+ i, jsxFolder, jsxScript, newLine, replacements, success;
+
+ success = true; // optimistic
+ args = arguments;
+
+ jsxFolder = path.join(__dirname, 'jsx');
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // $.fileName does not return it's correct path in the jsx engine for files called from the js engine //
+ // In Illustrator it returns an integer in InDesign it returns an empty string //
+ // This script injection allows for the script to know it's path by calling //
+ // $.__fileName(); //
+ // on Illustrator this works pretty well //
+ // on InDesign it's best to use with a bit of care //
+ // If the a second script has been called the InDesing will "forget" the path to the first script //
+ // 2 work-arounds for this //
+ // 1) at the beginning of your script add var thePathToMeIs = $.fileName(); //
+ // thePathToMeIs will not be forgotten after running the second script //
+ // 2) $.__fileName('myBaseName.jsx'); //
+ // for example you have file with the following path //
+ // /path/to/me.jsx //
+ // Call $.__fileName('me.jsx') and you will get /path/to/me.jsx even after executing a second script //
+ // Note When the forceEvalScript option is used then you just use the regular $.fileName property //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////
+ fileNameScript = [
+ // The if statement should not normally be executed
+ 'if(!$.__fileNames){',
+ ' $.__fileNames = {};',
+ ' $.__dirname = "__dirname__";'.replace('__dirname__', __dirname),
+ ' $.__fileName = function(name){',
+ ' name = name || $.fileName;',
+ ' return ($.__fileNames && $.__fileNames[name]) || $.fileName;',
+ ' };',
+ '}',
+ '$.__fileNames["__basename__"] = $.__fileNames["" + $.fileName] = "__fileName__";'
+ ].join('');
+
+ //////////////////////////////////////////////////////////////////////////////////////
+ // sort out order which arguments into jsxScript, callback, replacements, forceEval //
+ //////////////////////////////////////////////////////////////////////////////////////
+
+
+ // Detect if the parameters were passed as an object and if so allow for various keys
+ if (args.length === 1 && (arg = args[0]) instanceof Object) {
+ jsxScript = arg.jsxScript || arg.jsx || arg.script || arg.file || arg.jsxscript;
+ callback = arg.callBack || arg.callback;
+ replacements = arg.replacements || arg.replace;
+ forceEval = arg.eval || arg.forceEval || arg.forceeval;
+ } else {
+ for (i = 0; i < 5; i++) {
+ arg = args[i];
+ if (arg === undefined) {
+ continue;
+ }
+ if (arg.constructor.name === 'String') {
+ jsxScript = arg;
+ continue;
+ }
+ if (arg.constructor.name === 'Object') {
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // If no replacements are provided then the $.evalScript method will be used //
+ // This will allow directly for the $.fileName property to be used //
+ // If one does not want the $.evalScript method to be used then //
+ // either send a blank object as the replacements {} //
+ // or explicitly set the forceEvalScript option to false //
+ // This can only be done if the parameters are passed as an object //
+ // i.e. jsx.evalFile({file:'myFabScript.jsx', forceEvalScript: false}); //
+ // if the file was called using //
+ // i.e. jsx.evalFile('myFabScript.jsx'); //
+ // then the following jsx code is called $.evalFile(new File('Path/to/myFabScript.jsx', 10000000000)) + ''; //
+ // forceEval is never needed if the forceEvalScript is triggered //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ replacements = arg;
+ continue;
+ }
+ if (arg.constructor === Function) {
+ callback = arg;
+ continue;
+ }
+ if (arg === false) {
+ forceEval = false;
+ }
+ }
+ }
+
+ // If no script provide then not too much to do!
+ if (!jsxScript) {
+ return false;
+ }
+
+ forceEvalScript = !replacements;
+
+
+ //////////////////////////////////////////////////////
+ // Get path of script //
+ // Check if it's literal, relative or in jsx folder //
+ //////////////////////////////////////////////////////
+
+ if (/^\/|[a-zA-Z]+:/.test(jsxScript)) { // absolute path Mac | Windows
+ jsxScript = path.normalize(jsxScript);
+ } else if (/^\.+\//.test(jsxScript)) {
+ jsxScript = path.join(__dirname, jsxScript); // relative path
+ } else {
+ jsxScript = path.join(jsxFolder, jsxScript); // files in the jsxFolder
+ }
+
+ if (forceEvalScript) {
+ jsxScript = jsxScript.replace(/"/g, '\\"');
+ // Check that the path exist, should change this to asynchronous at some point
+ if (!window.cep.fs.stat(jsxScript).err) {
+ jsxScript = fileNameScript.replace(/__fileName__/, jsxScript).replace(/__basename__/, path.basename(jsxScript)) +
+ '$.evalFile(new File("' + jsxScript.replace(/\\/g, '\\\\') + '")) + "";';
+ return this.evalScript(jsxScript, callback, forceEval);
+ } else {
+ throw new Error(`The file: {jsxScript} could not be found / read`);
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ // Replacements made so we can't use $.evalFile and need to read the jsx script for ourselves //
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+
+ fileName = jsxScript.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
+ try {
+ jsxScript = window.cep.fs.readFile(jsxScript).data;
+ } catch (er) {
+ throw new Error(`The file: ${fileName} could not be read`);
+ }
+ // It is desirable that the injected fileNameScript is on the same line as the 1st line of the script
+ // This is so that the $.line or error.line returns the same value as the actual file
+ // However if the 1st line contains a # directive then we need to insert a new line and stuff the above problem
+ // When possible i.e. when there's no replacements then $.evalFile will be used and then the whole issue is avoided
+ newLine = /^\s*#/.test(jsxScript) ? '\n' : '';
+ jsxScript = fileNameScript.replace(/__fileName__/, fileName).replace(/__basename__/, path.basename(fileName)) + newLine + jsxScript;
+
+ try {
+ // evalScript(jsxScript, callback);
+ return this.evalScript(jsxScript, callback, replacements, forceEval);
+ } catch (err) {
+ ////////////////////////////////////////////////
+ // Do whatever error handling you want here ! //
+ ////////////////////////////////////////////////
+ var newErr;
+ newErr = new Error(err);
+ alert('Error Eek: ' + newErr.stack);
+ return false;
+ }
+
+ return success; // success should be an array but for now it's a Boolean
+ };
+
+
+ ////////////////////////////////////
+ // Setup alternative method names //
+ ////////////////////////////////////
+ Jsx.prototype.eval = Jsx.prototype.script = Jsx.prototype.evalscript = Jsx.prototype.evalScript;
+ Jsx.prototype.file = Jsx.prototype.evalfile = Jsx.prototype.evalFile;
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // Examples //
+ // jsx.evalScript('alert("foo");'); //
+ // jsx.evalFile('foo.jsx'); // where foo.jsx is stored in the jsx folder at the base of the extensions directory //
+ // jsx.evalFile('../myFolder/foo.jsx'); // where a relative or absolute file path is given //
+ // //
+ // using conventional methods one would use in the case were the values to swap were supplied by variables //
+ // csInterface.evalScript('var q = "' + name + '"; alert("' + myString + '" ' + myOp + ' q);q;', callback); //
+ // Using all the '' + foo + '' is very error prone //
+ // jsx.evalScript('var q = "__name__"; alert(__string__ __opp__ q);q;',{'name':'Fred', 'string':'Hello ', 'opp':'+'}, callBack); //
+ // is much simpler and less error prone //
+ // //
+ // more readable to use object //
+ // jsx.evalFile({ //
+ // file: 'yetAnotherFabScript.jsx', //
+ // replacements: {"this": foo, That: bar, and: "&&", the: foo2, other: bar2}, //
+ // eval: true //
+ // }) //
+ // Enjoy //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+ jsx = new Jsx();
+})();
diff --git a/openpype/hosts/photoshop/api/extension/host/index.jsx b/openpype/hosts/photoshop/api/extension/host/index.jsx
new file mode 100644
index 0000000000..2acec1ebc1
--- /dev/null
+++ b/openpype/hosts/photoshop/api/extension/host/index.jsx
@@ -0,0 +1,484 @@
+#include "json.js";
+#target photoshop
+
+var LogFactory=function(file,write,store,level,defaultStatus,continuing){if(file&&(file.constructor===String||file.constructor===File)){file={file:file};}else if(!file)file={file:{}};write=(file.write!==undefined)?file.write:write;if(write===undefined){write=true;}store=(file.store!==undefined)?file.store||false:store||false;level=(file.level!==undefined)?file.level:level;defaultStatus=(file.defaultStatus!==undefined)?file.defaultStatus:defaultStatus;if(defaultStatus===undefined){defaultStatus='LOG';}continuing=(file.continuing!==undefined)?file.continuing:continuing||false;file=file.file||{};var stack,times,logTime,logPoint,icons,statuses,LOG_LEVEL,LOG_STATUS;stack=[];times=[];logTime=new Date();logPoint='Log Factory Start';icons={"1":"\ud83d\udd50","130":"\ud83d\udd5c","2":"\ud83d\udd51","230":"\ud83d\udd5d","3":"\ud83d\udd52","330":"\ud83d\udd5e","4":"\ud83d\udd53","430":"\ud83d\udd5f","5":"\ud83d\udd54","530":"\ud83d\udd60","6":"\ud83d\udd55","630":"\ud83d\udd61","7":"\ud83d\udd56","730":"\ud83d\udd62","8":"\ud83d\udd57","830":"\ud83d\udd63","9":"\ud83d\udd58","930":"\ud83d\udd64","10":"\ud83d\udd59","1030":"\ud83d\udd65","11":"\ud83d\udd5a","1130":"\ud83d\udd66","12":"\ud83d\udd5b","1230":"\ud83d\udd67","AIRPLANE":"\ud83d\udee9","ALARM":"\u23f0","AMBULANCE":"\ud83d\ude91","ANCHOR":"\u2693","ANGRY":"\ud83d\ude20","ANGUISHED":"\ud83d\ude27","ANT":"\ud83d\udc1c","ANTENNA":"\ud83d\udce1","APPLE":"\ud83c\udf4f","APPLE2":"\ud83c\udf4e","ATM":"\ud83c\udfe7","ATOM":"\u269b","BABYBOTTLE":"\ud83c\udf7c","BAD:":"\ud83d\udc4e","BANANA":"\ud83c\udf4c","BANDAGE":"\ud83e\udd15","BANK":"\ud83c\udfe6","BATTERY":"\ud83d\udd0b","BED":"\ud83d\udecf","BEE":"\ud83d\udc1d","BEER":"\ud83c\udf7a","BELL":"\ud83d\udd14","BELLOFF":"\ud83d\udd15","BIRD":"\ud83d\udc26","BLACKFLAG":"\ud83c\udff4","BLUSH":"\ud83d\ude0a","BOMB":"\ud83d\udca3","BOOK":"\ud83d\udcd5","BOOKMARK":"\ud83d\udd16","BOOKS":"\ud83d\udcda","BOW":"\ud83c\udff9","BOWLING":"\ud83c\udfb3","BRIEFCASE":"\ud83d\udcbc","BROKEN":"\ud83d\udc94","BUG":"\ud83d\udc1b","BUILDING":"\ud83c\udfdb","BUILDINGS":"\ud83c\udfd8","BULB":"\ud83d\udca1","BUS":"\ud83d\ude8c","CACTUS":"\ud83c\udf35","CALENDAR":"\ud83d\udcc5","CAMEL":"\ud83d\udc2a","CAMERA":"\ud83d\udcf7","CANDLE":"\ud83d\udd6f","CAR":"\ud83d\ude98","CAROUSEL":"\ud83c\udfa0","CASTLE":"\ud83c\udff0","CATEYES":"\ud83d\ude3b","CATJOY":"\ud83d\ude39","CATMOUTH":"\ud83d\ude3a","CATSMILE":"\ud83d\ude3c","CD":"\ud83d\udcbf","CHECK":"\u2714","CHEQFLAG":"\ud83c\udfc1","CHICK":"\ud83d\udc25","CHICKEN":"\ud83d\udc14","CHICKHEAD":"\ud83d\udc24","CIRCLEBLACK":"\u26ab","CIRCLEBLUE":"\ud83d\udd35","CIRCLERED":"\ud83d\udd34","CIRCLEWHITE":"\u26aa","CIRCUS":"\ud83c\udfaa","CLAPPER":"\ud83c\udfac","CLAPPING":"\ud83d\udc4f","CLIP":"\ud83d\udcce","CLIPBOARD":"\ud83d\udccb","CLOUD":"\ud83c\udf28","CLOVER":"\ud83c\udf40","CLOWN":"\ud83e\udd21","COLDSWEAT":"\ud83d\ude13","COLDSWEAT2":"\ud83d\ude30","COMPRESS":"\ud83d\udddc","CONFOUNDED":"\ud83d\ude16","CONFUSED":"\ud83d\ude15","CONSTRUCTION":"\ud83d\udea7","CONTROL":"\ud83c\udf9b","COOKIE":"\ud83c\udf6a","COOKING":"\ud83c\udf73","COOL":"\ud83d\ude0e","COOLBOX":"\ud83c\udd92","COPYRIGHT":"\u00a9","CRANE":"\ud83c\udfd7","CRAYON":"\ud83d\udd8d","CREDITCARD":"\ud83d\udcb3","CROSS":"\u2716","CROSSBOX:":"\u274e","CRY":"\ud83d\ude22","CRYCAT":"\ud83d\ude3f","CRYSTALBALL":"\ud83d\udd2e","CUSTOMS":"\ud83d\udec3","DELICIOUS":"\ud83d\ude0b","DERELICT":"\ud83c\udfda","DESKTOP":"\ud83d\udda5","DIAMONDLB":"\ud83d\udd37","DIAMONDLO":"\ud83d\udd36","DIAMONDSB":"\ud83d\udd39","DIAMONDSO":"\ud83d\udd38","DICE":"\ud83c\udfb2","DISAPPOINTED":"\ud83d\ude1e","CRY2":"\ud83d\ude25","DIVISION":"\u2797","DIZZY":"\ud83d\ude35","DOLLAR":"\ud83d\udcb5","DOLLAR2":"\ud83d\udcb2","DOWNARROW":"\u2b07","DVD":"\ud83d\udcc0","EJECT":"\u23cf","ELEPHANT":"\ud83d\udc18","EMAIL":"\ud83d\udce7","ENVELOPE":"\ud83d\udce8","ENVELOPE2":"\u2709","ENVELOPE_DOWN":"\ud83d\udce9","EURO":"\ud83d\udcb6","EVIL":"\ud83d\ude08","EXPRESSIONLESS":"\ud83d\ude11","EYES":"\ud83d\udc40","FACTORY":"\ud83c\udfed","FAX":"\ud83d\udce0","FEARFUL":"\ud83d\ude28","FILEBOX":"\ud83d\uddc3","FILECABINET":"\ud83d\uddc4","FIRE":"\ud83d\udd25","FIREENGINE":"\ud83d\ude92","FIST":"\ud83d\udc4a","FLOWER":"\ud83c\udf37","FLOWER2":"\ud83c\udf38","FLUSHED":"\ud83d\ude33","FOLDER":"\ud83d\udcc1","FOLDER2":"\ud83d\udcc2","FREE":"\ud83c\udd93","FROG":"\ud83d\udc38","FROWN":"\ud83d\ude41","GEAR":"\u2699","GLOBE":"\ud83c\udf0d","GLOWINGSTAR":"\ud83c\udf1f","GOOD:":"\ud83d\udc4d","GRIMACING":"\ud83d\ude2c","GRIN":"\ud83d\ude00","GRINNINGCAT":"\ud83d\ude38","HALO":"\ud83d\ude07","HAMMER":"\ud83d\udd28","HAMSTER":"\ud83d\udc39","HAND":"\u270b","HANDDOWN":"\ud83d\udc47","HANDLEFT":"\ud83d\udc48","HANDRIGHT":"\ud83d\udc49","HANDUP":"\ud83d\udc46","HATCHING":"\ud83d\udc23","HAZARD":"\u2623","HEADPHONE":"\ud83c\udfa7","HEARNOEVIL":"\ud83d\ude49","HEARTBLUE":"\ud83d\udc99","HEARTEYES":"\ud83d\ude0d","HEARTGREEN":"\ud83d\udc9a","HEARTYELLOW":"\ud83d\udc9b","HELICOPTER":"\ud83d\ude81","HERB":"\ud83c\udf3f","HIGH_BRIGHTNESS":"\ud83d\udd06","HIGHVOLTAGE":"\u26a1","HIT":"\ud83c\udfaf","HONEY":"\ud83c\udf6f","HOT":"\ud83c\udf36","HOURGLASS":"\u23f3","HOUSE":"\ud83c\udfe0","HUGGINGFACE":"\ud83e\udd17","HUNDRED":"\ud83d\udcaf","HUSHED":"\ud83d\ude2f","ID":"\ud83c\udd94","INBOX":"\ud83d\udce5","INDEX":"\ud83d\uddc2","JOY":"\ud83d\ude02","KEY":"\ud83d\udd11","KISS":"\ud83d\ude18","KISS2":"\ud83d\ude17","KISS3":"\ud83d\ude19","KISS4":"\ud83d\ude1a","KISSINGCAT":"\ud83d\ude3d","KNIFE":"\ud83d\udd2a","LABEL":"\ud83c\udff7","LADYBIRD":"\ud83d\udc1e","LANDING":"\ud83d\udeec","LAPTOP":"\ud83d\udcbb","LEFTARROW":"\u2b05","LEMON":"\ud83c\udf4b","LIGHTNINGCLOUD":"\ud83c\udf29","LINK":"\ud83d\udd17","LITTER":"\ud83d\udeae","LOCK":"\ud83d\udd12","LOLLIPOP":"\ud83c\udf6d","LOUDSPEAKER":"\ud83d\udce2","LOW_BRIGHTNESS":"\ud83d\udd05","MAD":"\ud83d\ude1c","MAGNIFYING_GLASS":"\ud83d\udd0d","MASK":"\ud83d\ude37","MEDAL":"\ud83c\udf96","MEMO":"\ud83d\udcdd","MIC":"\ud83c\udfa4","MICROSCOPE":"\ud83d\udd2c","MINUS":"\u2796","MOBILE":"\ud83d\udcf1","MONEY":"\ud83d\udcb0","MONEYMOUTH":"\ud83e\udd11","MONKEY":"\ud83d\udc35","MOUSE":"\ud83d\udc2d","MOUSE2":"\ud83d\udc01","MOUTHLESS":"\ud83d\ude36","MOVIE":"\ud83c\udfa5","MUGS":"\ud83c\udf7b","NERD":"\ud83e\udd13","NEUTRAL":"\ud83d\ude10","NEW":"\ud83c\udd95","NOENTRY":"\ud83d\udeab","NOTEBOOK":"\ud83d\udcd4","NOTEPAD":"\ud83d\uddd2","NUTANDBOLT":"\ud83d\udd29","O":"\u2b55","OFFICE":"\ud83c\udfe2","OK":"\ud83c\udd97","OKHAND":"\ud83d\udc4c","OLDKEY":"\ud83d\udddd","OPENLOCK":"\ud83d\udd13","OPENMOUTH":"\ud83d\ude2e","OUTBOX":"\ud83d\udce4","PACKAGE":"\ud83d\udce6","PAGE":"\ud83d\udcc4","PAINTBRUSH":"\ud83d\udd8c","PALETTE":"\ud83c\udfa8","PANDA":"\ud83d\udc3c","PASSPORT":"\ud83d\udec2","PAWS":"\ud83d\udc3e","PEN":"\ud83d\udd8a","PEN2":"\ud83d\udd8b","PENSIVE":"\ud83d\ude14","PERFORMING":"\ud83c\udfad","PHONE":"\ud83d\udcde","PILL":"\ud83d\udc8a","PING":"\u2757","PLATE":"\ud83c\udf7d","PLUG":"\ud83d\udd0c","PLUS":"\u2795","POLICE":"\ud83d\ude93","POLICELIGHT":"\ud83d\udea8","POSTOFFICE":"\ud83c\udfe4","POUND":"\ud83d\udcb7","POUTING":"\ud83d\ude21","POUTINGCAT":"\ud83d\ude3e","PRESENT":"\ud83c\udf81","PRINTER":"\ud83d\udda8","PROJECTOR":"\ud83d\udcfd","PUSHPIN":"\ud83d\udccc","QUESTION":"\u2753","RABBIT":"\ud83d\udc30","RADIOACTIVE":"\u2622","RADIOBUTTON":"\ud83d\udd18","RAINCLOUD":"\ud83c\udf27","RAT":"\ud83d\udc00","RECYCLE":"\u267b","REGISTERED":"\u00ae","RELIEVED":"\ud83d\ude0c","ROBOT":"\ud83e\udd16","ROCKET":"\ud83d\ude80","ROLLING":"\ud83d\ude44","ROOSTER":"\ud83d\udc13","RULER":"\ud83d\udccf","SATELLITE":"\ud83d\udef0","SAVE":"\ud83d\udcbe","SCHOOL":"\ud83c\udfeb","SCISSORS":"\u2702","SCREAMING":"\ud83d\ude31","SCROLL":"\ud83d\udcdc","SEAT":"\ud83d\udcba","SEEDLING":"\ud83c\udf31","SEENOEVIL":"\ud83d\ude48","SHIELD":"\ud83d\udee1","SHIP":"\ud83d\udea2","SHOCKED":"\ud83d\ude32","SHOWER":"\ud83d\udebf","SLEEPING":"\ud83d\ude34","SLEEPY":"\ud83d\ude2a","SLIDER":"\ud83c\udf9a","SLOT":"\ud83c\udfb0","SMILE":"\ud83d\ude42","SMILING":"\ud83d\ude03","SMILINGCLOSEDEYES":"\ud83d\ude06","SMILINGEYES":"\ud83d\ude04","SMILINGSWEAT":"\ud83d\ude05","SMIRK":"\ud83d\ude0f","SNAIL":"\ud83d\udc0c","SNAKE":"\ud83d\udc0d","SOCCER":"\u26bd","SOS":"\ud83c\udd98","SPEAKER":"\ud83d\udd08","SPEAKEROFF":"\ud83d\udd07","SPEAKNOEVIL":"\ud83d\ude4a","SPIDER":"\ud83d\udd77","SPIDERWEB":"\ud83d\udd78","STAR":"\u2b50","STOP":"\u26d4","STOPWATCH":"\u23f1","SULK":"\ud83d\ude26","SUNFLOWER":"\ud83c\udf3b","SUNGLASSES":"\ud83d\udd76","SYRINGE":"\ud83d\udc89","TAKEOFF":"\ud83d\udeeb","TAXI":"\ud83d\ude95","TELESCOPE":"\ud83d\udd2d","TEMPORATURE":"\ud83e\udd12","TENNIS":"\ud83c\udfbe","THERMOMETER":"\ud83c\udf21","THINKING":"\ud83e\udd14","THUNDERCLOUD":"\u26c8","TICKBOX":"\u2705","TICKET":"\ud83c\udf9f","TIRED":"\ud83d\ude2b","TOILET":"\ud83d\udebd","TOMATO":"\ud83c\udf45","TONGUE":"\ud83d\ude1b","TOOLS":"\ud83d\udee0","TORCH":"\ud83d\udd26","TORNADO":"\ud83c\udf2a","TOUNG2":"\ud83d\ude1d","TRADEMARK":"\u2122","TRAFFICLIGHT":"\ud83d\udea6","TRASH":"\ud83d\uddd1","TREE":"\ud83c\udf32","TRIANGLE_LEFT":"\u25c0","TRIANGLE_RIGHT":"\u25b6","TRIANGLEDOWN":"\ud83d\udd3b","TRIANGLEUP":"\ud83d\udd3a","TRIANGULARFLAG":"\ud83d\udea9","TROPHY":"\ud83c\udfc6","TRUCK":"\ud83d\ude9a","TRUMPET":"\ud83c\udfba","TURKEY":"\ud83e\udd83","TURTLE":"\ud83d\udc22","UMBRELLA":"\u26f1","UNAMUSED":"\ud83d\ude12","UPARROW":"\u2b06","UPSIDEDOWN":"\ud83d\ude43","WARNING":"\u26a0","WATCH":"\u231a","WAVING":"\ud83d\udc4b","WEARY":"\ud83d\ude29","WEARYCAT":"\ud83d\ude40","WHITEFLAG":"\ud83c\udff3","WINEGLASS":"\ud83c\udf77","WINK":"\ud83d\ude09","WORRIED":"\ud83d\ude1f","WRENCH":"\ud83d\udd27","X":"\u274c","YEN":"\ud83d\udcb4","ZIPPERFACE":"\ud83e\udd10","UNDEFINED":"","":""};statuses={F:'FATAL',B:'BUG',C:'CRITICAL',E:'ERROR',W:'WARNING',I:'INFO',IM:'IMPORTANT',D:'DEBUG',L:'LOG',CO:'CONSTANT',FU:'FUNCTION',R:'RETURN',V:'VARIABLE',S:'STACK',RE:'RESULT',ST:'STOPPER',TI:'TIMER',T:'TRACE'};LOG_LEVEL={NONE:7,OFF:7,FATAL:6,ERROR:5,WARN:4,INFO:3,UNDEFINED:2,'':2,DEFAULT:2,DEBUG:2,TRACE:1,ON:0,ALL:0,};LOG_STATUS={OFF:LOG_LEVEL.OFF,NONE:LOG_LEVEL.OFF,NO:LOG_LEVEL.OFF,NOPE:LOG_LEVEL.OFF,FALSE:LOG_LEVEL.OFF,FATAL:LOG_LEVEL.FATAL,BUG:LOG_LEVEL.ERROR,CRITICAL:LOG_LEVEL.ERROR,ERROR:LOG_LEVEL.ERROR,WARNING:LOG_LEVEL.WARN,INFO:LOG_LEVEL.INFO,IMPORTANT:LOG_LEVEL.INFO,DEBUG:LOG_LEVEL.DEBUG,LOG:LOG_LEVEL.DEBUG,STACK:LOG_LEVEL.DEBUG,CONSTANT:LOG_LEVEL.DEBUG,FUNCTION:LOG_LEVEL.DEBUG,VARIABLE:LOG_LEVEL.DEBUG,RETURN:LOG_LEVEL.DEBUG,RESULT:LOG_LEVEL.TRACE,STOPPER:LOG_LEVEL.TRACE,TIMER:LOG_LEVEL.TRACE,TRACE:LOG_LEVEL.TRACE,ALL:LOG_LEVEL.ALL,YES:LOG_LEVEL.ALL,YEP:LOG_LEVEL.ALL,TRUE:LOG_LEVEL.ALL};var logFile,logFolder;var LOG=function(message,status,icon){if(LOG.level!==LOG_LEVEL.OFF&&(LOG.write||LOG.store)&&LOG.arguments.length)return LOG.addMessage(message,status,icon);};LOG.logDecodeLevel=function(level){if(level==~~level)return Math.abs(level);var lev;level+='';level=level.toUpperCase();if(level in statuses){level=statuses[level];}lev=LOG_LEVEL[level];if(lev!==undefined)return lev;lev=LOG_STATUS[level];if(lev!==undefined)return lev;return LOG_LEVEL.DEFAULT;};LOG.write=write;LOG.store=store;LOG.level=LOG.logDecodeLevel(level);LOG.status=defaultStatus;LOG.addMessage=function(message,status,icon){var date=new Date(),count,bool,logStatus;if(status&&status.constructor.name==='String'){status=status.toUpperCase();status=statuses[status]||status;}else status=LOG.status;logStatus=LOG_STATUS[status]||LOG_STATUS.ALL;if(logStatus999)?'['+LOG.count+'] ':(' ['+LOG.count+'] ').slice(-7);message=count+status+icon+(message instanceof Object?message.toSource():message)+date;if(LOG.store){stack.push(message);}if(LOG.write){bool=file&&file.writable&&logFile.writeln(message);if(!bool){file.writable=true;LOG.setFile(logFile);logFile.writeln(message);}}LOG.count++;return true;};var logNewFile=function(file,isCookie,overwrite){file.encoding='UTF-8';file.lineFeed=($.os[0]=='M')?'Macintosh':' Windows';if(isCookie)return file.open(overwrite?'w':'e')&&file;file.writable=LOG.write;logFile=file;logFolder=file.parent;if(continuing){LOG.count=LOG.setCount(file);}return(!LOG.write&&file||(file.open('a')&&file));};LOG.setFile=function(file,isCookie,overwrite){var bool,folder,fileName,suffix,newFileName,f,d,safeFileName;d=new Date();f=$.stack.split("\n")[0].replace(/^\[\(?/,'').replace(/\)?\]$/,'');if(f==~~f){f=$.fileName.replace(/[^\/]+\//g,'');}safeFileName=File.encode((isCookie?'/COOKIE_':'/LOG_')+f.replace(/^\//,'')+'_'+(1900+d.getYear())+(''+d).replace(/...(...)(..).+/,'_$1_$2')+(isCookie?'.txt':'.log'));if(file&&file.constructor.name=='String'){file=(file.match('/'))?new File(file):new File((logFolder||Folder.temp)+'/'+file);}if(file instanceof File){folder=file.parent;bool=folder.exists||folder.create();if(!bool)folder=Folder.temp;fileName=File.decode(file.name);suffix=fileName.match(/\.[^.]+$/);suffix=suffix?suffix[0]:'';fileName='/'+fileName;newFileName=fileName.replace(/\.[^.]+$/,'')+'_'+(+(new Date())+suffix);f=logNewFile(file,isCookie,overwrite);if(f)return f;f=logNewFile(new File(folder+newFileName),isCookie,overwrite);if(f)return f;f=logNewFile(new File(folder+safeFileName),isCookie,overwrite);if(f)return f;if(folder!=Folder.temp){f=logNewFile(new File(Folder.temp+fileName),isCookie,overwrite);if(f)return f;f=logNewFile(new File(Folder.temp+safeFileName),isCookie,overwrite);return f||new File(Folder.temp+safeFileName);}}return LOG.setFile(((logFile&&!isCookie)?new File(logFile):new File(Folder.temp+safeFileName)),isCookie,overwrite );};LOG.setCount=function(file){if(~~file===file){LOG.count=file;return LOG.count;}if(file===undefined){file=logFile;}if(file&&file.constructor===String){file=new File(file);}var logNumbers,contents;if(!file.length||!file.exists){LOG.count=1;return 1;}file.open('r');file.encoding='utf-8';file.seek(10000,2);contents='\n'+file.read();logNumbers=contents.match(/\n{0,3}\[\d+\] \[\w+\]+/g);if(logNumbers){logNumbers=+logNumbers[logNumbers.length-1].match(/\d+/)+1;file.close();LOG.count=logNumbers;return logNumbers;}if(file.length<10001){file.close();LOG.count=1;return 1;}file.seek(10000000,2);contents='\n'+file.read();logNumbers=contents.match(/\n{0,3}\[\d+\] \[\w+\]+/g);if(logNumbers){logNumbers=+logNumbers[logNumbers.length-1].match(/\d+/)+1;file.close();LOG.count=logNumbers;return logNumbers;}file.close();LOG.count=1;return 1;};LOG.setLevel=function(level){LOG.level=LOG.logDecodeLevel(level);return LOG.level;};LOG.setStatus=function(status){status=(''+status).toUpperCase();LOG.status=statuses[status]||status;return LOG.status;};LOG.cookie=function(file,level,overwrite,setLevel){var log,cookie;if(!file){file={file:file};}if(file&&(file.constructor===String||file.constructor===File)){file={file:file};}log=file;if(log.level===undefined){log.level=(level!==undefined)?level:'NONE';}if(log.overwrite===undefined){log.overwrite=(overwrite!==undefined)?overwrite:false;}if(log.setLevel===undefined){log.setLevel=(setLevel!==undefined)?setLevel:true;}setLevel=log.setLevel;overwrite=log.overwrite;level=log.level;file=log.file;file=LOG.setFile(file,true,overwrite);if(overwrite){file.write(level);}else{cookie=file.read();if(cookie.length){level=cookie;}else{file.write(level);}}file.close();if(setLevel){LOG.setLevel(level);}return{path:file,level:level};};LOG.args=function(args,funct,line){if(LOG.level>LOG_STATUS.FUNCTION)return;if(!(args&&(''+args.constructor).replace(/\s+/g,'')==='functionObject(){[nativecode]}'))return;if(!LOG.args.STRIP_COMMENTS){LOG.args.STRIP_COMMENTS=/((\/.*$)|(\/\*[\s\S]*?\*\/))/mg;}if(!LOG.args.ARGUMENT_NAMES){LOG.args.ARGUMENT_NAMES=/([^\s,]+)/g;}if(!LOG.args.OUTER_BRACKETS){LOG.args.OUTER_BRACKETS=/^\((.+)?\)$/;}if(!LOG.args.NEW_SOMETHING){LOG.args.NEW_SOMETHING=/^new \w+\((.+)?\)$/;}var functionString,argumentNames,stackInfo,report,functionName,arg,argsL,n,argName,argValue,argsTotal;if(funct===~~funct){line=funct;}if(!(funct instanceof Function)){funct=args.callee;}if(!(funct instanceof Function))return;functionName=funct.name;functionString=(''+funct).replace(LOG.args.STRIP_COMMENTS,'');argumentNames=functionString.slice(functionString.indexOf('(')+1,functionString.indexOf(')')).match(LOG.args.ARGUMENT_NAMES);argumentNames=argumentNames||[];report=[];report.push('--------------');report.push('Function Data:');report.push('--------------');report.push('Function Name:'+functionName);argsL=args.length;stackInfo=$.stack.split(/[\n\r]/);stackInfo.pop();stackInfo=stackInfo.join('\n ');report.push('Call stack:'+stackInfo);if(line){report.push('Function Line around:'+line);}report.push('Arguments Provided:'+argsL);report.push('Named Arguments:'+argumentNames.length);if(argumentNames.length){report.push('Arguments Names:'+argumentNames.join(','));}if(argsL){report.push('----------------');report.push('Argument Values:');report.push('----------------');}argsTotal=Math.max(argsL,argumentNames.length);for(n=0;n=argsL){argValue='NO VALUE PROVIDED';}else if(arg===undefined){argValue='undefined';}else if(arg===null){argValue='null';}else{argValue=arg.toSource().replace(LOG.args.OUTER_BRACKETS,'$1').replace(LOG.args.NEW_SOMETHING,'$1');}report.push((argName?argName:'arguments['+n+']')+':'+argValue);}report.push('');report=report.join('\n ');LOG(report,'f');return report;};LOG.stack=function(reverse){var st=$.stack.split('\n');st.pop();st.pop();if(reverse){st.reverse();}return LOG(st.join('\n '),'s');};LOG.values=function(values){var n,value,map=[];if(!(values instanceof Object||values instanceof Array)){return;}if(!LOG.values.OUTER_BRACKETS){LOG.values.OUTER_BRACKETS=/^\((.+)?\)$/;}if(!LOG.values.NEW_SOMETHING){LOG.values.NEW_SOMETHING=/^new \w+\((.+)?\)$/;}for(n in values){try{value=values[n];if(value===undefined){value='undefined';}else if(value===null){value='null';}else{value=value.toSource().replace(LOG.values.OUTER_BRACKETS,'$1').replace(LOG.values.NEW_SOMETHING,'$1');}}catch(e){value='\uD83D\uDEAB '+e;}map.push(n+':'+value);}if(map.length){map=map.join('\n ')+'\n ';return LOG(map,'v');}};LOG.reset=function(all){stack.length=0;LOG.count=1;if(all!==false){if(logFile instanceof File){logFile.close();}logFile=LOG.store=LOG.writeToFile=undefined;LOG.write=true;logFolder=Folder.temp;logTime=new Date();logPoint='After Log Reset';}};LOG.stopper=function(message){var newLogTime,t,m,newLogPoint;newLogTime=new Date();newLogPoint=(LOG.count!==undefined)?'LOG#'+LOG.count:'BEFORE LOG#1';LOG.time=t=newLogTime-logTime;if(message===false){return;}message=message||'Stopper start point';t=LOG.prettyTime(t);m=message+'\n '+'From '+logPoint+' to '+newLogPoint+' took '+t+' Starting '+logTime+' '+logTime.getMilliseconds()+'ms'+' Ending '+newLogTime+' '+newLogTime.getMilliseconds()+'ms';LOG(m,'st');logPoint=newLogPoint;logTime=newLogTime;return m;};LOG.start=function(message){var t=new Date();times.push([t,(message!==undefined)?message+'':'']);};LOG.stop=function(message){if(!times.length)return;message=(message)?message+' ':'';var nt,startLog,ot,om,td,m;nt=new Date();startLog=times.pop();ot=startLog[0];om=startLog[1];td=nt-ot;if(om.length){om+=' ';}m=om+'STARTED ['+ot+' '+ot.getMilliseconds()+'ms]\n '+message+'FINISHED ['+nt+' '+nt.getMilliseconds()+'ms]\n TOTAL TIME ['+LOG.prettyTime(td)+']';LOG(m,'ti');return m;};LOG.prettyTime=function(t){var h,m,s,ms;h=Math.floor(t / 3600000);m=Math.floor((t % 3600000)/ 60000);s=Math.floor((t % 60000)/ 1000);ms=t % 1000;t=(!t)?'<1ms':((h)?h+' hours ':'')+((m)?m+' minutes ':'')+((s)?s+' seconds ':'')+((ms&&(h||m||s))?'&':'')+((ms)?ms+'ms':'');return t;};LOG.get=function(){if(!stack.length)return 'THE LOG IS NOT SET TO STORE';var a=fetchLogLines(arguments);return a?'\n'+a.join('\n'):'NO LOGS AVAILABLE';};var fetchLogLines=function(){var args=arguments[0];if(!args.length)return stack;var c,n,l,a=[],ln,start,end,j,sl;l=args.length;sl=stack.length-1;n=0;for(c=0;cln)?sl+ln+1:ln-1;if(ln>=0&&ln<=sl)a[n++]=stack[ln];}else if(ln instanceof Array&&ln.length===2){start=ln[0];end=ln[1];if(!(~~start===start&&~~end===end))continue;start=(0>start)?sl+start+1:start-1;end=(0>end)?sl+end+1:end-1;start=Math.max(Math.min(sl,start),0);end=Math.min(Math.max(end,0),sl);if(start<=end)for(j=start;j<=end;j++)a[n++]=stack[j];else for(j=start;j>=end;j--)a[n++]=stack[j];}}return(n)?a:false;};LOG.file=function(){return logFile;};LOG.openFolder=function(){if(logFolder)return logFolder.execute();};LOG.show=LOG.execute=function(){if(logFile)return logFile.execute();};LOG.close=function(){if(logFile)return logFile.close();};LOG.setFile(file);if(!$.summary.difference){$.summary.difference=function(){return $.summary().replace(/ *([0-9]+)([^ ]+)(\n?)/g,$.summary.updateSnapshot );};}if(!$.summary.updateSnapshot){$.summary.updateSnapshot=function(full,count,name,lf){var snapshot=$.summary.snapshot;count=Number(count);var prev=snapshot[name]?snapshot[name]:0;snapshot[name]=count;var diff=count-prev;if(diff===0)return "";return " ".substring(String(diff).length)+diff+" "+name+lf;};}if(!$.summary.snapshot){$.summary.snapshot=[];$.summary.difference();}$.gc();$.gc();$.summary.difference();LOG.sumDiff=function(message){$.gc();$.gc();var diff=$.summary.difference();if(diff.length<8){diff=' - NONE -';}if(message===undefined){message='';}message+=diff;return LOG('$.summary.difference():'+message,'v');};return LOG;};
+
+var log = new LogFactory('myLog.log'); // =>; creates the new log factory - put full path where
+
+function getEnv(variable){
+ return $.getenv(variable);
+}
+
+function fileOpen(path){
+ return app.open(new File(path));
+}
+
+function getLayerTypeWithName(layerName) {
+ var type = 'NA';
+ var nameParts = layerName.split('_');
+ var namePrefix = nameParts[0];
+ namePrefix = namePrefix.toLowerCase();
+ switch (namePrefix) {
+ case 'guide':
+ case 'tl':
+ case 'tr':
+ case 'bl':
+ case 'br':
+ type = 'GUIDE';
+ break;
+ case 'fg':
+ type = 'FG';
+ break;
+ case 'bg':
+ type = 'BG';
+ break;
+ case 'obj':
+ default:
+ type = 'OBJ';
+ break;
+ }
+
+ return type;
+}
+
+function getLayers() {
+ /**
+ * Get json representation of list of layers.
+ * Much faster this way than in DOM traversal (2s vs 45s on same file)
+ *
+ * Format of single layer info:
+ * id : number
+ * name: string
+ * group: boolean - true if layer is a group
+ * parents:array - list of ids of parent groups, useful for selection
+ * all children layers from parent layerSet (eg. group)
+ * type: string - type of layer guessed from its name
+ * visible:boolean - true if visible
+ **/
+ if (documents.length == 0){
+ return '[]';
+ }
+ var ref1 = new ActionReference();
+ ref1.putEnumerated(charIDToTypeID('Dcmn'), charIDToTypeID('Ordn'),
+ charIDToTypeID('Trgt'));
+ var count = executeActionGet(ref1).getInteger(charIDToTypeID('NmbL'));
+
+ // get all layer names
+ var layers = [];
+ var layer = {};
+
+ var parents = [];
+ for (var i = count; i >= 1; i--) {
+ var layer = {};
+ var ref2 = new ActionReference();
+ ref2.putIndex(charIDToTypeID('Lyr '), i);
+
+ var desc = executeActionGet(ref2); // Access layer index #i
+ var layerSection = typeIDToStringID(desc.getEnumerationValue(
+ stringIDToTypeID('layerSection')));
+
+ layer.id = desc.getInteger(stringIDToTypeID("layerID"));
+ layer.name = desc.getString(stringIDToTypeID("name"));
+ layer.color_code = typeIDToStringID(desc.getEnumerationValue(stringIDToTypeID('color')));
+ layer.group = false;
+ layer.parents = parents.slice();
+ layer.type = getLayerTypeWithName(layer.name);
+ layer.visible = desc.getBoolean(stringIDToTypeID("visible"));
+ //log(" name: " + layer.name + " groupId " + layer.groupId +
+ //" group " + layer.group);
+ if (layerSection == 'layerSectionStart') { // Group start and end
+ parents.push(layer.id);
+ layer.group = true;
+ }
+ if (layerSection == 'layerSectionEnd') {
+ parents.pop();
+ continue;
+ }
+ layers.push(JSON.stringify(layer));
+ }
+ try{
+ var bck = activeDocument.backgroundLayer;
+ layer.id = bck.id;
+ layer.name = bck.name;
+ layer.group = false;
+ layer.parents = [];
+ layer.type = 'background';
+ layer.visible = bck.visible;
+ layers.push(JSON.stringify(layer));
+ }catch(e){
+ // do nothing, no background layer
+ };
+ //log("layers " + layers);
+ return '[' + layers + ']';
+}
+
+function setVisible(layer_id, visibility){
+ /**
+ * Sets particular 'layer_id' to 'visibility' if true > show
+ **/
+ var desc = new ActionDescriptor();
+ var ref = new ActionReference();
+ ref.putIdentifier(stringIDToTypeID("layer"), layer_id);
+ desc.putReference(stringIDToTypeID("null"), ref);
+
+ executeAction(visibility?stringIDToTypeID("show"):stringIDToTypeID("hide"),
+ desc, DialogModes.NO);
+
+}
+
+function getHeadline(){
+ /**
+ * Returns headline of current document with metadata
+ *
+ **/
+ if (documents.length == 0){
+ return '';
+ }
+ var headline = app.activeDocument.info.headline;
+
+ return headline;
+}
+
+function isSaved(){
+ return app.activeDocument.saved;
+}
+
+function save(){
+ /** Saves active document **/
+ return app.activeDocument.save();
+}
+
+function saveAs(output_path, ext, as_copy){
+ /** Exports scene to various formats
+ *
+ * Currently implemented: 'jpg', 'png', 'psd'
+ *
+ * output_path - escaped file path on local system
+ * ext - extension for export
+ * as_copy - create copy, do not overwrite
+ *
+ * */
+ var saveName = output_path;
+ var saveOptions;
+ if (ext == 'jpg'){
+ saveOptions = new JPEGSaveOptions();
+ saveOptions.quality = 12;
+ saveOptions.embedColorProfile = true;
+ saveOptions.formatOptions = FormatOptions.PROGRESSIVE;
+ if(saveOptions.formatOptions == FormatOptions.PROGRESSIVE){
+ saveOptions.scans = 5};
+ saveOptions.matte = MatteType.NONE;
+ }
+ if (ext == 'png'){
+ saveOptions = new PNGSaveOptions();
+ saveOptions.interlaced = true;
+ saveOptions.transparency = true;
+ }
+ if (ext == 'psd'){
+ saveOptions = null;
+ return app.activeDocument.saveAs(new File(saveName));
+ }
+ if (ext == 'psb'){
+ return savePSB(output_path);
+ }
+
+ return app.activeDocument.saveAs(new File(saveName), saveOptions, as_copy);
+
+}
+
+function getActiveDocumentName(){
+ /**
+ * Returns file name of active document
+ * */
+ if (documents.length == 0){
+ return null;
+ }
+ return app.activeDocument.name;
+}
+
+function getActiveDocumentFullName(){
+ /**
+ * Returns file name of active document with file path.
+ * activeDocument.fullName returns path in URI (eg /c/.. insted of c:/)
+ * */
+ if (documents.length == 0){
+ return null;
+ }
+ var f = new File(app.activeDocument.fullName);
+ var path = f.fsName;
+ f.close();
+ return path;
+}
+
+function imprint(payload){
+ /**
+ * Sets headline content of current document with metadata. Stores
+ * information about assets created through Avalon.
+ * Content accessible in PS through File > File Info
+ *
+ **/
+ app.activeDocument.info.headline = payload;
+}
+
+function getSelectedLayers(doc) {
+ /**
+ * Returns json representation of currently selected layers.
+ * Works in three steps - 1) creates new group with selected layers
+ * 2) traverses this group
+ * 3) deletes newly created group, not neede
+ * Bit weird, but Adobe..
+ **/
+ if (doc == null){
+ doc = app.activeDocument;
+ }
+
+ var selLayers = [];
+ _grp = groupSelectedLayers(doc);
+
+ var group = doc.activeLayer;
+ var layers = group.layers;
+
+ // // group is fake at this point
+ // var itself_name = '';
+ // if (layers){
+ // itself_name = layers[0].name;
+ // }
+
+
+ for (var i = 0; i < layers.length; i++) {
+ var layer = {};
+ layer.id = layers[i].id;
+ layer.name = layers[i].name;
+ long_names =_get_parents_names(group.parent, layers[i].name);
+ var t = layers[i].kind;
+ if ((typeof t !== 'undefined') &&
+ (layers[i].kind.toString() == 'LayerKind.NORMAL')){
+ layer.group = false;
+ }else{
+ layer.group = true;
+ }
+ layer.long_name = long_names;
+
+ selLayers.push(layer);
+ }
+
+ _undo();
+
+ return JSON.stringify(selLayers);
+};
+
+function selectLayers(selectedLayers){
+ /**
+ * Selects layers from list of ids
+ **/
+ selectedLayers = JSON.parse(selectedLayers);
+ var layers = new Array();
+ var id54 = charIDToTypeID( "slct" );
+ var desc12 = new ActionDescriptor();
+ var id55 = charIDToTypeID( "null" );
+ var ref9 = new ActionReference();
+
+ var existing_layers = JSON.parse(getLayers());
+ var existing_ids = [];
+ for (var y = 0; y < existing_layers.length; y++){
+ existing_ids.push(existing_layers[y]["id"]);
+ }
+ for (var i = 0; i < selectedLayers.length; i++) {
+ // a check to see if the id stil exists
+ var id = selectedLayers[i];
+ if(existing_ids.toString().indexOf(id)>=0){
+ layers[i] = charIDToTypeID( "Lyr " );
+ ref9.putIdentifier(layers[i], id);
+ }
+ }
+ desc12.putReference( id55, ref9 );
+ var id58 = charIDToTypeID( "MkVs" );
+ desc12.putBoolean( id58, false );
+ executeAction( id54, desc12, DialogModes.NO );
+}
+
+function groupSelectedLayers(doc, name) {
+ /**
+ * Groups selected layers into new group.
+ * Returns json representation of Layer for server to consume
+ *
+ * Args:
+ * doc(activeDocument)
+ * name (str): new name of created group
+ **/
+ if (doc == null){
+ doc = app.activeDocument;
+ }
+
+ var desc = new ActionDescriptor();
+ var ref = new ActionReference();
+ ref.putClass( stringIDToTypeID('layerSection') );
+ desc.putReference( charIDToTypeID('null'), ref );
+ var lref = new ActionReference();
+ lref.putEnumerated( charIDToTypeID('Lyr '), charIDToTypeID('Ordn'),
+ charIDToTypeID('Trgt') );
+ desc.putReference( charIDToTypeID('From'), lref);
+ executeAction( charIDToTypeID('Mk '), desc, DialogModes.NO );
+
+ var group = doc.activeLayer;
+ if (name){
+ // Add special character to highlight group that will be published
+ group.name = name;
+ }
+ var layer = {};
+ layer.id = group.id;
+ layer.name = name; // keep name clean
+ layer.group = true;
+
+ layer.long_name = _get_parents_names(group, name);
+
+ return JSON.stringify(layer);
+};
+
+function importSmartObject(path, name, link){
+ /**
+ * Creates new layer with an image from 'path'
+ *
+ * path: absolute path to loaded file
+ * name: sets name of newly created laye
+ *
+ **/
+ var desc1 = new ActionDescriptor();
+ desc1.putPath( app.charIDToTypeID("null"), new File(path) );
+ link = link || false;
+ if (link) {
+ desc1.putBoolean( app.charIDToTypeID('Lnkd'), true );
+ }
+
+ desc1.putEnumerated(app.charIDToTypeID("FTcs"), app.charIDToTypeID("QCSt"),
+ app.charIDToTypeID("Qcsa"));
+ var desc2 = new ActionDescriptor();
+ desc2.putUnitDouble(app.charIDToTypeID("Hrzn"),
+ app.charIDToTypeID("#Pxl"), 0.0);
+ desc2.putUnitDouble(app.charIDToTypeID("Vrtc"),
+ app.charIDToTypeID("#Pxl"), 0.0);
+
+ desc1.putObject(charIDToTypeID("Ofst"), charIDToTypeID("Ofst"), desc2);
+ executeAction(charIDToTypeID("Plc " ), desc1, DialogModes.NO);
+
+ var docRef = app.activeDocument
+ var currentActivelayer = app.activeDocument.activeLayer;
+ if (name){
+ currentActivelayer.name = name;
+ }
+ var layer = {}
+ layer.id = currentActivelayer.id;
+ layer.name = currentActivelayer.name;
+ return JSON.stringify(layer);
+}
+
+function replaceSmartObjects(layer_id, path, name){
+ /**
+ * Updates content of 'layer' with an image from 'path'
+ *
+ **/
+
+ var desc = new ActionDescriptor();
+ var ref = new ActionReference();
+ ref.putIdentifier(stringIDToTypeID("layer"), layer_id);
+ desc.putReference(stringIDToTypeID("null"), ref);
+
+ desc.putPath(charIDToTypeID('null'), new File(path) );
+ desc.putInteger(charIDToTypeID("PgNm"), 1);
+
+ executeAction(stringIDToTypeID('placedLayerReplaceContents'),
+ desc, DialogModes.NO );
+ var currentActivelayer = app.activeDocument.activeLayer;
+ if (name){
+ currentActivelayer.name = name;
+ }
+}
+
+function createGroup(name){
+ /**
+ * Creates new group with a 'name'
+ * Because of asynchronous nature, only group.id is available
+ **/
+ group = app.activeDocument.layerSets.add();
+ // Add special character to highlight group that will be published
+ group.name = name;
+
+ return group.id; // only id available at this time :|
+}
+
+function deleteLayer(layer_id){
+ /***
+ * Deletes layer by its layer_id
+ *
+ * layer_id (int)
+ **/
+ var d = new ActionDescriptor();
+ var r = new ActionReference();
+
+ r.putIdentifier(stringIDToTypeID("layer"), layer_id);
+ d.putReference(stringIDToTypeID("null"), r);
+ executeAction(stringIDToTypeID("delete"), d, DialogModes.NO);
+}
+
+function _undo() {
+ executeAction(charIDToTypeID("undo", undefined, DialogModes.NO));
+};
+
+function savePSB(output_path){
+ /***
+ * Saves file as .psb to 'output_path'
+ *
+ * output_path (str)
+ **/
+ var desc1 = new ActionDescriptor();
+ var desc2 = new ActionDescriptor();
+ desc2.putBoolean( stringIDToTypeID('maximizeCompatibility'), true );
+ desc1.putObject( charIDToTypeID('As '), charIDToTypeID('Pht8'), desc2 );
+ desc1.putPath( charIDToTypeID('In '), new File(output_path) );
+ desc1.putBoolean( charIDToTypeID('LwCs'), true );
+ executeAction( charIDToTypeID('save'), desc1, DialogModes.NO );
+}
+
+function close(){
+ executeAction(stringIDToTypeID("quit"), undefined, DialogModes.NO );
+}
+
+function renameLayer(layer_id, new_name){
+ /***
+ * Renames 'layer_id' to 'new_name'
+ *
+ * Via Action (fast)
+ *
+ * Args:
+ * layer_id(int)
+ * new_name(str)
+ *
+ * output_path (str)
+ **/
+ doc = app.activeDocument;
+ selectLayers('['+layer_id+']');
+
+ doc.activeLayer.name = new_name;
+}
+
+function _get_parents_names(layer, itself_name){
+ var long_names = [itself_name];
+ while (layer.parent){
+ if (layer.typename != "LayerSet"){
+ break;
+ }
+ long_names.push(layer.name);
+ layer = layer.parent;
+ }
+ return long_names;
+}
+
+// triggers when panel is opened, good for debugging
+//log(getActiveDocumentName());
+// log.show();
+// var a = app.activeDocument.activeLayer;
+// log(a);
+//getSelectedLayers();
+// importSmartObject("c:/projects/test.jpg", "a aaNewLayer", true);
+// log("dpc");
+// replaceSmartObjects(153, "â–¼Jungle_imageTest_001", "c:/projects/test_project_test_asset_TestTask_v001.png");
\ No newline at end of file
diff --git a/openpype/hosts/photoshop/api/extension/host/json.js b/openpype/hosts/photoshop/api/extension/host/json.js
new file mode 100644
index 0000000000..397349bbfd
--- /dev/null
+++ b/openpype/hosts/photoshop/api/extension/host/json.js
@@ -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");
+ };
+ }
+}());
\ No newline at end of file
diff --git a/openpype/hosts/photoshop/api/extension/icons/avalon-logo-48.png b/openpype/hosts/photoshop/api/extension/icons/avalon-logo-48.png
new file mode 100644
index 0000000000..33fe2a606b
Binary files /dev/null and b/openpype/hosts/photoshop/api/extension/icons/avalon-logo-48.png differ
diff --git a/openpype/hosts/photoshop/api/extension/index.html b/openpype/hosts/photoshop/api/extension/index.html
new file mode 100644
index 0000000000..501e753c0b
--- /dev/null
+++ b/openpype/hosts/photoshop/api/extension/index.html
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openpype/hosts/photoshop/api/launch_logic.py b/openpype/hosts/photoshop/api/launch_logic.py
new file mode 100644
index 0000000000..16a1d23244
--- /dev/null
+++ b/openpype/hosts/photoshop/api/launch_logic.py
@@ -0,0 +1,365 @@
+import os
+import subprocess
+import collections
+import asyncio
+
+from wsrpc_aiohttp import (
+ WebSocketRoute,
+ WebSocketAsync
+)
+
+from Qt import QtCore
+
+from openpype.api import Logger
+from openpype.tools.utils import host_tools
+
+from avalon import api
+from avalon.tools.webserver.app import WebServerTool
+
+from .ws_stub import PhotoshopServerStub
+
+log = Logger.get_logger(__name__)
+
+
+class ConnectionNotEstablishedYet(Exception):
+ pass
+
+
+class MainThreadItem:
+ """Structure to store information about callback in main thread.
+
+ Item should be used to execute callback in main thread which may be needed
+ for execution of Qt objects.
+
+ Item store callback (callable variable), arguments and keyword arguments
+ for the callback. Item hold information about it's process.
+ """
+ not_set = object()
+
+ def __init__(self, callback, *args, **kwargs):
+ self._done = False
+ self._exception = self.not_set
+ self._result = self.not_set
+ self._callback = callback
+ self._args = args
+ self._kwargs = kwargs
+
+ @property
+ def done(self):
+ return self._done
+
+ @property
+ def exception(self):
+ return self._exception
+
+ @property
+ def result(self):
+ return self._result
+
+ def execute(self):
+ """Execute callback and store it's result.
+
+ Method must be called from main thread. Item is marked as `done`
+ when callback execution finished. Store output of callback of exception
+ information when callback raise one.
+ """
+ log.debug("Executing process in main thread")
+ if self.done:
+ log.warning("- item is already processed")
+ return
+
+ log.info("Running callback: {}".format(str(self._callback)))
+ try:
+ result = self._callback(*self._args, **self._kwargs)
+ self._result = result
+
+ except Exception as exc:
+ self._exception = exc
+
+ finally:
+ self._done = True
+
+
+def stub():
+ """
+ Convenience function to get server RPC stub to call methods directed
+ for host (Photoshop).
+ It expects already created connection, started from client.
+ Currently created when panel is opened (PS: Window>Extensions>Avalon)
+ :return: where functions could be called from
+ """
+ ps_stub = PhotoshopServerStub()
+ if not ps_stub.client:
+ raise ConnectionNotEstablishedYet("Connection is not created yet")
+
+ return ps_stub
+
+
+def show_tool_by_name(tool_name):
+ kwargs = {}
+ if tool_name == "loader":
+ kwargs["use_context"] = True
+
+ host_tools.show_tool_by_name(tool_name, **kwargs)
+
+
+class ProcessLauncher(QtCore.QObject):
+ route_name = "Photoshop"
+ _main_thread_callbacks = collections.deque()
+
+ def __init__(self, subprocess_args):
+ self._subprocess_args = subprocess_args
+ self._log = None
+
+ super(ProcessLauncher, self).__init__()
+
+ # Keep track if launcher was already started
+ self._started = False
+
+ self._process = None
+ self._websocket_server = None
+
+ start_process_timer = QtCore.QTimer()
+ start_process_timer.setInterval(100)
+
+ loop_timer = QtCore.QTimer()
+ loop_timer.setInterval(200)
+
+ start_process_timer.timeout.connect(self._on_start_process_timer)
+ loop_timer.timeout.connect(self._on_loop_timer)
+
+ self._start_process_timer = start_process_timer
+ self._loop_timer = loop_timer
+
+ @property
+ def log(self):
+ if self._log is None:
+ self._log = Logger.get_logger(
+ "{}-launcher".format(self.route_name)
+ )
+ return self._log
+
+ @property
+ def websocket_server_is_running(self):
+ if self._websocket_server is not None:
+ return self._websocket_server.is_running
+ return False
+
+ @property
+ def is_process_running(self):
+ if self._process is not None:
+ return self._process.poll() is None
+ return False
+
+ @property
+ def is_host_connected(self):
+ """Returns True if connected, False if app is not running at all."""
+ if not self.is_process_running:
+ return False
+
+ try:
+ _stub = stub()
+ if _stub:
+ return True
+ except Exception:
+ pass
+
+ return None
+
+ @classmethod
+ def execute_in_main_thread(cls, callback, *args, **kwargs):
+ item = MainThreadItem(callback, *args, **kwargs)
+ cls._main_thread_callbacks.append(item)
+ return item
+
+ def start(self):
+ if self._started:
+ return
+ self.log.info("Started launch logic of AfterEffects")
+ self._started = True
+ self._start_process_timer.start()
+
+ def exit(self):
+ """ Exit whole application. """
+ if self._start_process_timer.isActive():
+ self._start_process_timer.stop()
+ if self._loop_timer.isActive():
+ self._loop_timer.stop()
+
+ if self._websocket_server is not None:
+ self._websocket_server.stop()
+
+ if self._process:
+ self._process.kill()
+ self._process.wait()
+
+ QtCore.QCoreApplication.exit()
+
+ def _on_loop_timer(self):
+ # TODO find better way and catch errors
+ # Run only callbacks that are in queue at the moment
+ cls = self.__class__
+ for _ in range(len(cls._main_thread_callbacks)):
+ if cls._main_thread_callbacks:
+ item = cls._main_thread_callbacks.popleft()
+ item.execute()
+
+ if not self.is_process_running:
+ self.log.info("Host process is not running. Closing")
+ self.exit()
+
+ elif not self.websocket_server_is_running:
+ self.log.info("Websocket server is not running. Closing")
+ self.exit()
+
+ def _on_start_process_timer(self):
+ # TODO add try except validations for each part in this method
+ # Start server as first thing
+ if self._websocket_server is None:
+ self._init_server()
+ return
+
+ # TODO add waiting time
+ # Wait for webserver
+ if not self.websocket_server_is_running:
+ return
+
+ # Start application process
+ if self._process is None:
+ self._start_process()
+ self.log.info("Waiting for host to connect")
+ return
+
+ # TODO add waiting time
+ # Wait until host is connected
+ if self.is_host_connected:
+ self._start_process_timer.stop()
+ self._loop_timer.start()
+ elif (
+ not self.is_process_running
+ or not self.websocket_server_is_running
+ ):
+ self.exit()
+
+ def _init_server(self):
+ if self._websocket_server is not None:
+ return
+
+ self.log.debug(
+ "Initialization of websocket server for host communication"
+ )
+
+ self._websocket_server = websocket_server = WebServerTool()
+ if websocket_server.port_occupied(
+ websocket_server.host_name,
+ websocket_server.port
+ ):
+ self.log.info(
+ "Server already running, sending actual context and exit."
+ )
+ asyncio.run(websocket_server.send_context_change(self.route_name))
+ self.exit()
+ return
+
+ # Add Websocket route
+ websocket_server.add_route("*", "/ws/", WebSocketAsync)
+ # Add after effects route to websocket handler
+
+ print("Adding {} route".format(self.route_name))
+ WebSocketAsync.add_route(
+ self.route_name, PhotoshopRoute
+ )
+ self.log.info("Starting websocket server for host communication")
+ websocket_server.start_server()
+
+ def _start_process(self):
+ if self._process is not None:
+ return
+ self.log.info("Starting host process")
+ try:
+ self._process = subprocess.Popen(
+ self._subprocess_args,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL
+ )
+ except Exception:
+ self.log.info("exce", exc_info=True)
+ self.exit()
+
+
+class PhotoshopRoute(WebSocketRoute):
+ """
+ One route, mimicking external application (like Harmony, etc).
+ All functions could be called from client.
+ 'do_notify' function calls function on the client - mimicking
+ notification after long running job on the server or similar
+ """
+ instance = None
+
+ def init(self, **kwargs):
+ # Python __init__ must be return "self".
+ # This method might return anything.
+ log.debug("someone called Photoshop route")
+ self.instance = self
+ return kwargs
+
+ # server functions
+ async def ping(self):
+ log.debug("someone called Photoshop route ping")
+
+ # This method calls function on the client side
+ # client functions
+ async def set_context(self, project, asset, task):
+ """
+ Sets 'project' and 'asset' to envs, eg. setting context
+
+ Args:
+ project (str)
+ asset (str)
+ """
+ log.info("Setting context change")
+ log.info("project {} asset {} ".format(project, asset))
+ if project:
+ api.Session["AVALON_PROJECT"] = project
+ os.environ["AVALON_PROJECT"] = project
+ if asset:
+ api.Session["AVALON_ASSET"] = asset
+ os.environ["AVALON_ASSET"] = asset
+ if task:
+ api.Session["AVALON_TASK"] = task
+ os.environ["AVALON_TASK"] = task
+
+ async def read(self):
+ log.debug("photoshop.read client calls server server calls "
+ "photoshop client")
+ return await self.socket.call('photoshop.read')
+
+ # panel routes for tools
+ async def creator_route(self):
+ self._tool_route("creator")
+
+ async def workfiles_route(self):
+ self._tool_route("workfiles")
+
+ async def loader_route(self):
+ self._tool_route("loader")
+
+ async def publish_route(self):
+ self._tool_route("publish")
+
+ async def sceneinventory_route(self):
+ self._tool_route("sceneinventory")
+
+ async def subsetmanager_route(self):
+ self._tool_route("subsetmanager")
+
+ async def experimental_tools_route(self):
+ self._tool_route("experimental_tools")
+
+ def _tool_route(self, _tool_name):
+ """The address accessed when clicking on the buttons."""
+
+ ProcessLauncher.execute_in_main_thread(show_tool_by_name, _tool_name)
+
+ # Required return statement.
+ return "nothing"
diff --git a/openpype/hosts/photoshop/api/lib.py b/openpype/hosts/photoshop/api/lib.py
new file mode 100644
index 0000000000..707cd476c5
--- /dev/null
+++ b/openpype/hosts/photoshop/api/lib.py
@@ -0,0 +1,78 @@
+import os
+import sys
+import contextlib
+import traceback
+
+from Qt import QtWidgets
+
+import avalon.api
+
+from openpype.api import Logger
+from openpype.tools.utils import host_tools
+from openpype.lib.remote_publish import headless_publish
+
+from .launch_logic import ProcessLauncher, stub
+
+log = Logger.get_logger(__name__)
+
+
+def safe_excepthook(*args):
+ traceback.print_exception(*args)
+
+
+def main(*subprocess_args):
+ from openpype.hosts.photoshop import api
+
+ avalon.api.install(api)
+ sys.excepthook = safe_excepthook
+
+ # coloring in ConsoleTrayApp
+ os.environ["OPENPYPE_LOG_NO_COLORS"] = "False"
+ app = QtWidgets.QApplication([])
+ app.setQuitOnLastWindowClosed(False)
+
+ launcher = ProcessLauncher(subprocess_args)
+ launcher.start()
+
+ if os.environ.get("HEADLESS_PUBLISH"):
+ launcher.execute_in_main_thread(
+ headless_publish,
+ log,
+ "ClosePS",
+ os.environ.get("IS_TEST")
+ )
+ elif os.environ.get("AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH", True):
+ save = False
+ if os.getenv("WORKFILES_SAVE_AS"):
+ save = True
+
+ launcher.execute_in_main_thread(
+ host_tools.show_workfiles, save=save
+ )
+
+ sys.exit(app.exec_())
+
+
+@contextlib.contextmanager
+def maintained_selection():
+ """Maintain selection during context."""
+ selection = stub().get_selected_layers()
+ try:
+ yield selection
+ finally:
+ stub().select_layers(selection)
+
+
+@contextlib.contextmanager
+def maintained_visibility():
+ """Maintain visibility during context."""
+ visibility = {}
+ layers = stub().get_layers()
+ for layer in layers:
+ visibility[layer.id] = layer.visible
+ try:
+ yield
+ finally:
+ for layer in layers:
+ stub().set_visible(layer.id, visibility[layer.id])
+ pass
diff --git a/openpype/hosts/photoshop/api/panel.PNG b/openpype/hosts/photoshop/api/panel.PNG
new file mode 100644
index 0000000000..be5db3b8df
Binary files /dev/null and b/openpype/hosts/photoshop/api/panel.PNG differ
diff --git a/openpype/hosts/photoshop/api/panel_failure.PNG b/openpype/hosts/photoshop/api/panel_failure.PNG
new file mode 100644
index 0000000000..67afc4e212
Binary files /dev/null and b/openpype/hosts/photoshop/api/panel_failure.PNG differ
diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py
new file mode 100644
index 0000000000..25983f2471
--- /dev/null
+++ b/openpype/hosts/photoshop/api/pipeline.py
@@ -0,0 +1,229 @@
+import os
+import sys
+from Qt import QtWidgets
+
+import pyblish.api
+import avalon.api
+from avalon import pipeline, io
+
+from openpype.api import Logger
+import openpype.hosts.photoshop
+
+from . import lib
+
+log = Logger.get_logger(__name__)
+
+HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.photoshop.__file__))
+PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
+PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
+LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
+CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
+INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
+
+
+def check_inventory():
+ if not lib.any_outdated():
+ return
+
+ host = avalon.api.registered_host()
+ outdated_containers = []
+ for container in host.ls():
+ representation = container['representation']
+ representation_doc = io.find_one(
+ {
+ "_id": io.ObjectId(representation),
+ "type": "representation"
+ },
+ projection={"parent": True}
+ )
+ if representation_doc and not lib.is_latest(representation_doc):
+ outdated_containers.append(container)
+
+ # Warn about outdated containers.
+ print("Starting new QApplication..")
+
+ message_box = QtWidgets.QMessageBox()
+ message_box.setIcon(QtWidgets.QMessageBox.Warning)
+ msg = "There are outdated containers in the scene."
+ message_box.setText(msg)
+ message_box.exec_()
+
+
+def on_application_launch():
+ check_inventory()
+
+
+def on_pyblish_instance_toggled(instance, old_value, new_value):
+ """Toggle layer visibility on instance toggles."""
+ instance[0].Visible = new_value
+
+
+def install():
+ """Install Photoshop-specific functionality of avalon-core.
+
+ This function is called automatically on calling `api.install(photoshop)`.
+ """
+ log.info("Installing OpenPype Photoshop...")
+ pyblish.api.register_host("photoshop")
+
+ pyblish.api.register_plugin_path(PUBLISH_PATH)
+ avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH)
+ avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH)
+ log.info(PUBLISH_PATH)
+
+ pyblish.api.register_callback(
+ "instanceToggled", on_pyblish_instance_toggled
+ )
+
+ avalon.api.on("application.launched", on_application_launch)
+
+
+def uninstall():
+ pyblish.api.deregister_plugin_path(PUBLISH_PATH)
+ avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH)
+ avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH)
+
+
+def ls():
+ """Yields containers from active Photoshop document
+
+ This is the host-equivalent of api.ls(), but instead of listing
+ assets on disk, it lists assets already loaded in Photoshop; once loaded
+ they are called 'containers'
+
+ Yields:
+ dict: container
+
+ """
+ try:
+ stub = lib.stub() # only after Photoshop is up
+ except lib.ConnectionNotEstablishedYet:
+ print("Not connected yet, ignoring")
+ return
+
+ if not stub.get_active_document_name():
+ return
+
+ layers_meta = stub.get_layers_metadata() # minimalize calls to PS
+ for layer in stub.get_layers():
+ data = stub.read(layer, layers_meta)
+
+ # Skip non-tagged layers.
+ if not data:
+ continue
+
+ # Filter to only containers.
+ if "container" not in data["id"]:
+ continue
+
+ # Append transient data
+ data["objectName"] = layer.name.replace(stub.LOADED_ICON, '')
+ data["layer"] = layer
+
+ yield data
+
+
+def list_instances():
+ """List all created instances to publish from current workfile.
+
+ Pulls from File > File Info
+
+ For SubsetManager
+
+ Returns:
+ (list) of dictionaries matching instances format
+ """
+ stub = _get_stub()
+
+ if not stub:
+ return []
+
+ instances = []
+ layers_meta = stub.get_layers_metadata()
+ if layers_meta:
+ for key, instance in layers_meta.items():
+ schema = instance.get("schema")
+ if schema and "container" in schema:
+ continue
+
+ instance['uuid'] = key
+ instances.append(instance)
+
+ return instances
+
+
+def remove_instance(instance):
+ """Remove instance from current workfile metadata.
+
+ Updates metadata of current file in File > File Info and removes
+ icon highlight on group layer.
+
+ For SubsetManager
+
+ Args:
+ instance (dict): instance representation from subsetmanager model
+ """
+ stub = _get_stub()
+
+ if not stub:
+ return
+
+ stub.remove_instance(instance.get("uuid"))
+ layer = stub.get_layer(instance.get("uuid"))
+ if layer:
+ stub.rename_layer(instance.get("uuid"),
+ layer.name.replace(stub.PUBLISH_ICON, ''))
+
+
+def _get_stub():
+ """Handle pulling stub from PS to run operations on host
+
+ Returns:
+ (PhotoshopServerStub) or None
+ """
+ try:
+ stub = lib.stub() # only after Photoshop is up
+ except lib.ConnectionNotEstablishedYet:
+ print("Not connected yet, ignoring")
+ return
+
+ if not stub.get_active_document_name():
+ return
+
+ return stub
+
+
+def containerise(
+ name, namespace, layer, context, loader=None, suffix="_CON"
+):
+ """Imprint layer with metadata
+
+ Containerisation enables a tracking of version, author and origin
+ for loaded assets.
+
+ Arguments:
+ name (str): Name of resulting assembly
+ namespace (str): Namespace under which to host container
+ layer (PSItem): Layer to containerise
+ context (dict): Asset information
+ loader (str, optional): Name of loader used to produce this container.
+ suffix (str, optional): Suffix of container, defaults to `_CON`.
+
+ Returns:
+ container (str): Name of container assembly
+ """
+ layer.name = name + suffix
+
+ data = {
+ "schema": "openpype:container-2.0",
+ "id": pipeline.AVALON_CONTAINER_ID,
+ "name": name,
+ "namespace": namespace,
+ "loader": str(loader),
+ "representation": str(context["representation"]["_id"]),
+ "members": [str(layer.id)]
+ }
+ stub = lib.stub()
+ stub.imprint(layer, data)
+
+ return layer
diff --git a/openpype/hosts/photoshop/api/plugin.py b/openpype/hosts/photoshop/api/plugin.py
new file mode 100644
index 0000000000..e0db67de2c
--- /dev/null
+++ b/openpype/hosts/photoshop/api/plugin.py
@@ -0,0 +1,69 @@
+import re
+
+import avalon.api
+from .launch_logic import stub
+
+
+def get_unique_layer_name(layers, asset_name, subset_name):
+ """
+ Gets all layer names and if 'asset_name_subset_name' is present, it
+ increases suffix by 1 (eg. creates unique layer name - for Loader)
+ Args:
+ layers (list) of dict with layers info (name, id etc.)
+ asset_name (string):
+ subset_name (string):
+
+ Returns:
+ (string): name_00X (without version)
+ """
+ name = "{}_{}".format(asset_name, subset_name)
+ names = {}
+ for layer in layers:
+ layer_name = re.sub(r'_\d{3}$', '', layer.name)
+ if layer_name in names.keys():
+ names[layer_name] = names[layer_name] + 1
+ else:
+ names[layer_name] = 1
+ occurrences = names.get(name, 0)
+
+ return "{}_{:0>3d}".format(name, occurrences + 1)
+
+
+class PhotoshopLoader(avalon.api.Loader):
+ @staticmethod
+ def get_stub():
+ return stub()
+
+
+class Creator(avalon.api.Creator):
+ """Creator plugin to create instances in Photoshop
+
+ A LayerSet is created to support any number of layers in an instance. If
+ the selection is used, these layers will be added to the LayerSet.
+ """
+
+ def process(self):
+ # Photoshop can have multiple LayerSets with the same name, which does
+ # not work with Avalon.
+ msg = "Instance with name \"{}\" already exists.".format(self.name)
+ stub = lib.stub() # only after Photoshop is up
+ for layer in stub.get_layers():
+ if self.name.lower() == layer.Name.lower():
+ msg = QtWidgets.QMessageBox()
+ msg.setIcon(QtWidgets.QMessageBox.Warning)
+ msg.setText(msg)
+ msg.exec_()
+ return False
+
+ # Store selection because adding a group will change selection.
+ with lib.maintained_selection():
+
+ # Add selection to group.
+ if (self.options or {}).get("useSelection"):
+ group = stub.group_selected_layers(self.name)
+ else:
+ group = stub.create_group(self.name)
+
+ stub.imprint(group, self.data)
+
+ return group
diff --git a/openpype/hosts/photoshop/api/workio.py b/openpype/hosts/photoshop/api/workio.py
new file mode 100644
index 0000000000..0bf3ed2bd9
--- /dev/null
+++ b/openpype/hosts/photoshop/api/workio.py
@@ -0,0 +1,51 @@
+"""Host API required Work Files tool"""
+import os
+
+import avalon.api
+
+from . import lib
+
+
+def _active_document():
+ document_name = lib.stub().get_active_document_name()
+ if not document_name:
+ return None
+
+ return document_name
+
+
+def file_extensions():
+ return avalon.api.HOST_WORKFILE_EXTENSIONS["photoshop"]
+
+
+def has_unsaved_changes():
+ if _active_document():
+ return not lib.stub().is_saved()
+
+ return False
+
+
+def save_file(filepath):
+ _, ext = os.path.splitext(filepath)
+ lib.stub().saveAs(filepath, ext[1:], True)
+
+
+def open_file(filepath):
+ lib.stub().open(filepath)
+
+ return True
+
+
+def current_file():
+ try:
+ full_name = lib.stub().get_active_document_full_name()
+ if full_name and full_name != "null":
+ return os.path.normpath(full_name).replace("\\", "/")
+ except Exception:
+ pass
+
+ return None
+
+
+def work_root(session):
+ return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/")
diff --git a/openpype/hosts/photoshop/api/ws_stub.py b/openpype/hosts/photoshop/api/ws_stub.py
new file mode 100644
index 0000000000..b8f66332c6
--- /dev/null
+++ b/openpype/hosts/photoshop/api/ws_stub.py
@@ -0,0 +1,495 @@
+"""
+ Stub handling connection from server to client.
+ Used anywhere solution is calling client methods.
+"""
+import sys
+import json
+import attr
+from wsrpc_aiohttp import WebSocketAsync
+
+from avalon.tools.webserver.app import WebServerTool
+
+
+@attr.s
+class PSItem(object):
+ """
+ Object denoting layer or group item in PS. Each item is created in
+ PS by any Loader, but contains same fields, which are being used
+ in later processing.
+ """
+ # metadata
+ id = attr.ib() # id created by AE, could be used for querying
+ name = attr.ib() # name of item
+ group = attr.ib(default=None) # item type (footage, folder, comp)
+ parents = attr.ib(factory=list)
+ visible = attr.ib(default=True)
+ type = attr.ib(default=None)
+ # all imported elements, single for
+ members = attr.ib(factory=list)
+ long_name = attr.ib(default=None)
+ color_code = attr.ib(default=None) # color code of layer
+
+
+class PhotoshopServerStub:
+ """
+ Stub for calling function on client (Photoshop js) side.
+ Expects that client is already connected (started when avalon menu
+ is opened).
+ 'self.websocketserver.call' is used as async wrapper
+ """
+ PUBLISH_ICON = '\u2117 '
+ LOADED_ICON = '\u25bc'
+
+ def __init__(self):
+ self.websocketserver = WebServerTool.get_instance()
+ self.client = self.get_client()
+
+ @staticmethod
+ def get_client():
+ """
+ Return first connected client to WebSocket
+ TODO implement selection by Route
+ :return: client
+ """
+ clients = WebSocketAsync.get_clients()
+ client = None
+ if len(clients) > 0:
+ key = list(clients.keys())[0]
+ client = clients.get(key)
+
+ return client
+
+ def open(self, path):
+ """Open file located at 'path' (local).
+
+ Args:
+ path(string): file path locally
+ Returns: None
+ """
+ self.websocketserver.call(
+ self.client.call('Photoshop.open', path=path)
+ )
+
+ def read(self, layer, layers_meta=None):
+ """Parses layer metadata from Headline field of active document.
+
+ Args:
+ layer: (PSItem)
+ layers_meta: full list from Headline (for performance in loops)
+ Returns:
+ """
+ if layers_meta is None:
+ layers_meta = self.get_layers_metadata()
+
+ return layers_meta.get(str(layer.id))
+
+ def imprint(self, layer, data, all_layers=None, layers_meta=None):
+ """Save layer metadata to Headline field of active document
+
+ Stores metadata in format:
+ [{
+ "active":true,
+ "subset":"imageBG",
+ "family":"image",
+ "id":"pyblish.avalon.instance",
+ "asset":"Town",
+ "uuid": "8"
+ }] - for created instances
+ OR
+ [{
+ "schema": "openpype:container-2.0",
+ "id": "pyblish.avalon.instance",
+ "name": "imageMG",
+ "namespace": "Jungle_imageMG_001",
+ "loader": "ImageLoader",
+ "representation": "5fbfc0ee30a946093c6ff18a",
+ "members": [
+ "40"
+ ]
+ }] - for loaded instances
+
+ Args:
+ layer (PSItem):
+ data(string): json representation for single layer
+ all_layers (list of PSItem): for performance, could be
+ injected for usage in loop, if not, single call will be
+ triggered
+ layers_meta(string): json representation from Headline
+ (for performance - provide only if imprint is in
+ loop - value should be same)
+ Returns: None
+ """
+ if not layers_meta:
+ layers_meta = self.get_layers_metadata()
+
+ # json.dumps writes integer values in a dictionary to string, so
+ # anticipating it here.
+ if str(layer.id) in layers_meta and layers_meta[str(layer.id)]:
+ if data:
+ layers_meta[str(layer.id)].update(data)
+ else:
+ layers_meta.pop(str(layer.id))
+ else:
+ layers_meta[str(layer.id)] = data
+
+ # Ensure only valid ids are stored.
+ if not all_layers:
+ all_layers = self.get_layers()
+ layer_ids = [layer.id for layer in all_layers]
+ cleaned_data = []
+
+ for layer_id in layers_meta:
+ if int(layer_id) in layer_ids:
+ cleaned_data.append(layers_meta[layer_id])
+
+ payload = json.dumps(cleaned_data, indent=4)
+
+ self.websocketserver.call(
+ self.client.call('Photoshop.imprint', payload=payload)
+ )
+
+ def get_layers(self):
+ """Returns JSON document with all(?) layers in active document.
+
+ Returns:
+ Format of tuple: { 'id':'123',
+ 'name': 'My Layer 1',
+ 'type': 'GUIDE'|'FG'|'BG'|'OBJ'
+ 'visible': 'true'|'false'
+ """
+ res = self.websocketserver.call(
+ self.client.call('Photoshop.get_layers')
+ )
+
+ return self._to_records(res)
+
+ def get_layer(self, layer_id):
+ """
+ Returns PSItem for specific 'layer_id' or None if not found
+ Args:
+ layer_id (string): unique layer id, stored in 'uuid' field
+
+ Returns:
+ (PSItem) or None
+ """
+ layers = self.get_layers()
+ for layer in layers:
+ if str(layer.id) == str(layer_id):
+ return layer
+
+ def get_layers_in_layers(self, layers):
+ """Return all layers that belong to layers (might be groups).
+
+ Args:
+ layers :
+
+ Returns:
+
+ """
+ all_layers = self.get_layers()
+ ret = []
+ parent_ids = set([lay.id for lay in layers])
+
+ for layer in all_layers:
+ parents = set(layer.parents)
+ if len(parent_ids & parents) > 0:
+ ret.append(layer)
+ if layer.id in parent_ids:
+ ret.append(layer)
+
+ return ret
+
+ def create_group(self, name):
+ """Create new group (eg. LayerSet)
+
+ Returns:
+
+ """
+ enhanced_name = self.PUBLISH_ICON + name
+ ret = self.websocketserver.call(
+ self.client.call('Photoshop.create_group', name=enhanced_name)
+ )
+ # create group on PS is asynchronous, returns only id
+ return PSItem(id=ret, name=name, group=True)
+
+ def group_selected_layers(self, name):
+ """Group selected layers into new LayerSet (eg. group)
+
+ Returns:
+ (Layer)
+ """
+ enhanced_name = self.PUBLISH_ICON + name
+ res = self.websocketserver.call(
+ self.client.call(
+ 'Photoshop.group_selected_layers', name=enhanced_name
+ )
+ )
+ res = self._to_records(res)
+ if res:
+ rec = res.pop()
+ rec.name = rec.name.replace(self.PUBLISH_ICON, '')
+ return rec
+ raise ValueError("No group record returned")
+
+ def get_selected_layers(self):
+ """Get a list of actually selected layers.
+
+ Returns:
+ """
+ res = self.websocketserver.call(
+ self.client.call('Photoshop.get_selected_layers')
+ )
+ return self._to_records(res)
+
+ def select_layers(self, layers):
+ """Selects specified layers in Photoshop by its ids.
+
+ Args:
+ layers:
+ """
+ layers_id = [str(lay.id) for lay in layers]
+ self.websocketserver.call(
+ self.client.call(
+ 'Photoshop.select_layers',
+ layers=json.dumps(layers_id)
+ )
+ )
+
+ def get_active_document_full_name(self):
+ """Returns full name with path of active document via ws call
+
+ Returns(string):
+ full path with name
+ """
+ res = self.websocketserver.call(
+ self.client.call('Photoshop.get_active_document_full_name')
+ )
+
+ return res
+
+ def get_active_document_name(self):
+ """Returns just a name of active document via ws call
+
+ Returns(string):
+ file name
+ """
+ return self.websocketserver.call(
+ self.client.call('Photoshop.get_active_document_name')
+ )
+
+ def is_saved(self):
+ """Returns true if no changes in active document
+
+ Returns:
+
+ """
+ return self.websocketserver.call(
+ self.client.call('Photoshop.is_saved')
+ )
+
+ def save(self):
+ """Saves active document"""
+ self.websocketserver.call(
+ self.client.call('Photoshop.save')
+ )
+
+ def saveAs(self, image_path, ext, as_copy):
+ """Saves active document to psd (copy) or png or jpg
+
+ Args:
+ image_path(string): full local path
+ ext:
+ as_copy:
+ Returns: None
+ """
+ self.websocketserver.call(
+ self.client.call(
+ 'Photoshop.saveAs',
+ image_path=image_path,
+ ext=ext,
+ as_copy=as_copy
+ )
+ )
+
+ def set_visible(self, layer_id, visibility):
+ """Set layer with 'layer_id' to 'visibility'
+
+ Args:
+ layer_id:
+ visibility:
+ Returns: None
+ """
+ self.websocketserver.call(
+ self.client.call(
+ 'Photoshop.set_visible',
+ layer_id=layer_id,
+ visibility=visibility
+ )
+ )
+
+ def get_layers_metadata(self):
+ """Reads layers metadata from Headline from active document in PS.
+ (Headline accessible by File > File Info)
+
+ Returns:
+ (string): - json documents
+ example:
+ {"8":{"active":true,"subset":"imageBG",
+ "family":"image","id":"pyblish.avalon.instance",
+ "asset":"Town"}}
+ 8 is layer(group) id - used for deletion, update etc.
+ """
+ layers_data = {}
+ res = self.websocketserver.call(self.client.call('Photoshop.read'))
+ try:
+ layers_data = json.loads(res)
+ except json.decoder.JSONDecodeError:
+ pass
+ # format of metadata changed from {} to [] because of standardization
+ # keep current implementation logic as its working
+ if not isinstance(layers_data, dict):
+ temp_layers_meta = {}
+ for layer_meta in layers_data:
+ layer_id = layer_meta.get("uuid")
+ if not layer_id:
+ layer_id = layer_meta.get("members")[0]
+
+ temp_layers_meta[layer_id] = layer_meta
+ layers_data = temp_layers_meta
+ else:
+ # legacy version of metadata
+ for layer_id, layer_meta in layers_data.items():
+ if layer_meta.get("schema") != "openpype:container-2.0":
+ layer_meta["uuid"] = str(layer_id)
+ else:
+ layer_meta["members"] = [str(layer_id)]
+
+ return layers_data
+
+ def import_smart_object(self, path, layer_name, as_reference=False):
+ """Import the file at `path` as a smart object to active document.
+
+ Args:
+ path (str): File path to import.
+ layer_name (str): Unique layer name to differentiate how many times
+ same smart object was loaded
+ as_reference (bool): pull in content or reference
+ """
+ enhanced_name = self.LOADED_ICON + layer_name
+ res = self.websocketserver.call(
+ self.client.call(
+ 'Photoshop.import_smart_object',
+ path=path,
+ name=enhanced_name,
+ as_reference=as_reference
+ )
+ )
+ rec = self._to_records(res).pop()
+ if rec:
+ rec.name = rec.name.replace(self.LOADED_ICON, '')
+ return rec
+
+ def replace_smart_object(self, layer, path, layer_name):
+ """Replace the smart object `layer` with file at `path`
+
+ Args:
+ layer (PSItem):
+ path (str): File to import.
+ layer_name (str): Unique layer name to differentiate how many times
+ same smart object was loaded
+ """
+ enhanced_name = self.LOADED_ICON + layer_name
+ self.websocketserver.call(
+ self.client.call(
+ 'Photoshop.replace_smart_object',
+ layer_id=layer.id,
+ path=path,
+ name=enhanced_name
+ )
+ )
+
+ def delete_layer(self, layer_id):
+ """Deletes specific layer by it's id.
+
+ Args:
+ layer_id (int): id of layer to delete
+ """
+ self.websocketserver.call(
+ self.client.call('Photoshop.delete_layer', layer_id=layer_id)
+ )
+
+ def rename_layer(self, layer_id, name):
+ """Renames specific layer by it's id.
+
+ Args:
+ layer_id (int): id of layer to delete
+ name (str): new name
+ """
+ self.websocketserver.call(
+ self.client.call(
+ 'Photoshop.rename_layer',
+ layer_id=layer_id,
+ name=name
+ )
+ )
+
+ def remove_instance(self, instance_id):
+ cleaned_data = {}
+
+ for key, instance in self.get_layers_metadata().items():
+ if key != instance_id:
+ cleaned_data[key] = instance
+
+ payload = json.dumps(cleaned_data, indent=4)
+
+ self.websocketserver.call(
+ self.client.call('Photoshop.imprint', payload=payload)
+ )
+
+ def get_extension_version(self):
+ """Returns version number of installed extension."""
+ return self.websocketserver.call(
+ self.client.call('Photoshop.get_extension_version')
+ )
+
+ def close(self):
+ """Shutting down PS and process too.
+
+ For webpublishing only.
+ """
+ # TODO change client.call to method with checks for client
+ self.websocketserver.call(self.client.call('Photoshop.close'))
+
+ def _to_records(self, res):
+ """Converts string json representation into list of PSItem for
+ dot notation access to work.
+
+ Args:
+ res (string): valid json
+
+ Returns:
+
+ """
+ try:
+ layers_data = json.loads(res)
+ except json.decoder.JSONDecodeError:
+ raise ValueError("Received broken JSON {}".format(res))
+ ret = []
+
+ # convert to AEItem to use dot donation
+ if isinstance(layers_data, dict):
+ layers_data = [layers_data]
+ for d in layers_data:
+ # currently implemented and expected fields
+ ret.append(PSItem(
+ d.get('id'),
+ d.get('name'),
+ d.get('group'),
+ d.get('parents'),
+ d.get('visible'),
+ d.get('type'),
+ d.get('members'),
+ d.get('long_name'),
+ d.get("color_code")
+ ))
+ return ret
diff --git a/openpype/hosts/photoshop/hooks/__init__.py b/openpype/hosts/photoshop/hooks/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/openpype/hosts/photoshop/plugins/__init__.py b/openpype/hosts/photoshop/plugins/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py
index 657d41aa93..cf41bb4020 100644
--- a/openpype/hosts/photoshop/plugins/create/create_image.py
+++ b/openpype/hosts/photoshop/plugins/create/create_image.py
@@ -1,6 +1,6 @@
from Qt import QtWidgets
import openpype.api
-from avalon import photoshop
+from openpype.hosts.photoshop import api as photoshop
class CreateImage(openpype.api.Creator):
diff --git a/openpype/hosts/photoshop/plugins/lib.py b/openpype/hosts/photoshop/plugins/lib.py
deleted file mode 100644
index 74aff06114..0000000000
--- a/openpype/hosts/photoshop/plugins/lib.py
+++ /dev/null
@@ -1,26 +0,0 @@
-import re
-
-
-def get_unique_layer_name(layers, asset_name, subset_name):
- """
- Gets all layer names and if 'asset_name_subset_name' is present, it
- increases suffix by 1 (eg. creates unique layer name - for Loader)
- Args:
- layers (list) of dict with layers info (name, id etc.)
- asset_name (string):
- subset_name (string):
-
- Returns:
- (string): name_00X (without version)
- """
- name = "{}_{}".format(asset_name, subset_name)
- names = {}
- for layer in layers:
- layer_name = re.sub(r'_\d{3}$', '', layer.name)
- if layer_name in names.keys():
- names[layer_name] = names[layer_name] + 1
- else:
- names[layer_name] = 1
- occurrences = names.get(name, 0)
-
- return "{}_{:0>3d}".format(name, occurrences + 1)
diff --git a/openpype/hosts/photoshop/plugins/load/load_image.py b/openpype/hosts/photoshop/plugins/load/load_image.py
index 981a1ed204..3b1cfe9636 100644
--- a/openpype/hosts/photoshop/plugins/load/load_image.py
+++ b/openpype/hosts/photoshop/plugins/load/load_image.py
@@ -1,12 +1,11 @@
import re
-from avalon import api, photoshop
+from avalon import api
+from openpype.hosts.photoshop import api as photoshop
+from openpype.hosts.photoshop.api import get_unique_layer_name
-from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name
-stub = photoshop.stub()
-
-class ImageLoader(api.Loader):
+class ImageLoader(photoshop.PhotoshopLoader):
"""Load images
Stores the imported asset in a container named after the asset.
@@ -16,11 +15,14 @@ class ImageLoader(api.Loader):
representations = ["*"]
def load(self, context, name=None, namespace=None, data=None):
- layer_name = get_unique_layer_name(stub.get_layers(),
- context["asset"]["name"],
- name)
+ stub = self.get_stub()
+ layer_name = get_unique_layer_name(
+ stub.get_layers(),
+ context["asset"]["name"],
+ name
+ )
with photoshop.maintained_selection():
- layer = self.import_layer(self.fname, layer_name)
+ layer = self.import_layer(self.fname, layer_name, stub)
self[:] = [layer]
namespace = namespace or layer_name
@@ -35,6 +37,8 @@ class ImageLoader(api.Loader):
def update(self, container, representation):
""" Switch asset or change version """
+ stub = self.get_stub()
+
layer = container.pop("layer")
context = representation.get("context", {})
@@ -44,9 +48,9 @@ class ImageLoader(api.Loader):
layer_name = "{}_{}".format(context["asset"], context["subset"])
# switching assets
if namespace_from_container != layer_name:
- layer_name = get_unique_layer_name(stub.get_layers(),
- context["asset"],
- context["subset"])
+ layer_name = get_unique_layer_name(
+ stub.get_layers(), context["asset"], context["subset"]
+ )
else: # switching version - keep same name
layer_name = container["namespace"]
@@ -66,6 +70,8 @@ class ImageLoader(api.Loader):
Args:
container (dict): container to be removed - used to get layer_id
"""
+ stub = self.get_stub()
+
layer = container.pop("layer")
stub.imprint(layer, {})
stub.delete_layer(layer.id)
@@ -73,5 +79,5 @@ class ImageLoader(api.Loader):
def switch(self, container, representation):
self.update(container, representation)
- def import_layer(self, file_name, layer_name):
+ def import_layer(self, file_name, layer_name, stub):
return stub.import_smart_object(file_name, layer_name)
diff --git a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py
index 8704627b12..ab4682e63e 100644
--- a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py
+++ b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py
@@ -1,17 +1,13 @@
import os
-from avalon import api
-from avalon import photoshop
from avalon.pipeline import get_representation_path_from_context
from avalon.vendor import qargparse
-from openpype.lib import Anatomy
-from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name
-
-stub = photoshop.stub()
+from openpype.hosts.photoshop import api as photoshop
+from openpype.hosts.photoshop.api import get_unique_layer_name
-class ImageFromSequenceLoader(api.Loader):
+class ImageFromSequenceLoader(photoshop.PhotoshopLoader):
""" Load specifing image from sequence
Used only as quick load of reference file from a sequence.
@@ -35,15 +31,16 @@ class ImageFromSequenceLoader(api.Loader):
def load(self, context, name=None, namespace=None, data=None):
if data.get("frame"):
- self.fname = os.path.join(os.path.dirname(self.fname),
- data["frame"])
+ self.fname = os.path.join(
+ os.path.dirname(self.fname), data["frame"]
+ )
if not os.path.exists(self.fname):
return
- stub = photoshop.stub()
- layer_name = get_unique_layer_name(stub.get_layers(),
- context["asset"]["name"],
- name)
+ stub = self.get_stub()
+ layer_name = get_unique_layer_name(
+ stub.get_layers(), context["asset"]["name"], name
+ )
with photoshop.maintained_selection():
layer = stub.import_smart_object(self.fname, layer_name)
@@ -95,4 +92,3 @@ class ImageFromSequenceLoader(api.Loader):
def remove(self, container):
"""No update possible, not containerized."""
pass
-
diff --git a/openpype/hosts/photoshop/plugins/load/load_reference.py b/openpype/hosts/photoshop/plugins/load/load_reference.py
index 0cb4e4a69f..60142d4a1f 100644
--- a/openpype/hosts/photoshop/plugins/load/load_reference.py
+++ b/openpype/hosts/photoshop/plugins/load/load_reference.py
@@ -1,30 +1,30 @@
import re
-from avalon import api, photoshop
+from avalon import api
-from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name
-
-stub = photoshop.stub()
+from openpype.hosts.photoshop import api as photoshop
+from openpype.hosts.photoshop.api import get_unique_layer_name
-class ReferenceLoader(api.Loader):
+class ReferenceLoader(photoshop.PhotoshopLoader):
"""Load reference images
- Stores the imported asset in a container named after the asset.
+ Stores the imported asset in a container named after the asset.
- Inheriting from 'load_image' didn't work because of
- "Cannot write to closing transport", possible refactor.
+ Inheriting from 'load_image' didn't work because of
+ "Cannot write to closing transport", possible refactor.
"""
families = ["image", "render"]
representations = ["*"]
def load(self, context, name=None, namespace=None, data=None):
- layer_name = get_unique_layer_name(stub.get_layers(),
- context["asset"]["name"],
- name)
+ stub = self.get_stub()
+ layer_name = get_unique_layer_name(
+ stub.get_layers(), context["asset"]["name"], name
+ )
with photoshop.maintained_selection():
- layer = self.import_layer(self.fname, layer_name)
+ layer = self.import_layer(self.fname, layer_name, stub)
self[:] = [layer]
namespace = namespace or layer_name
@@ -39,6 +39,7 @@ class ReferenceLoader(api.Loader):
def update(self, container, representation):
""" Switch asset or change version """
+ stub = self.get_stub()
layer = container.pop("layer")
context = representation.get("context", {})
@@ -48,9 +49,9 @@ class ReferenceLoader(api.Loader):
layer_name = "{}_{}".format(context["asset"], context["subset"])
# switching assets
if namespace_from_container != layer_name:
- layer_name = get_unique_layer_name(stub.get_layers(),
- context["asset"],
- context["subset"])
+ layer_name = get_unique_layer_name(
+ stub.get_layers(), context["asset"], context["subset"]
+ )
else: # switching version - keep same name
layer_name = container["namespace"]
@@ -65,11 +66,12 @@ class ReferenceLoader(api.Loader):
)
def remove(self, container):
- """
- Removes element from scene: deletes layer + removes from Headline
+ """Removes element from scene: deletes layer + removes from Headline
+
Args:
container (dict): container to be removed - used to get layer_id
"""
+ stub = self.get_stub()
layer = container.pop("layer")
stub.imprint(layer, {})
stub.delete_layer(layer.id)
@@ -77,6 +79,7 @@ class ReferenceLoader(api.Loader):
def switch(self, container, representation):
self.update(container, representation)
- def import_layer(self, file_name, layer_name):
- return stub.import_smart_object(file_name, layer_name,
- as_reference=True)
+ def import_layer(self, file_name, layer_name, stub):
+ return stub.import_smart_object(
+ file_name, layer_name, as_reference=True
+ )
diff --git a/openpype/hosts/photoshop/plugins/publish/closePS.py b/openpype/hosts/photoshop/plugins/publish/closePS.py
index 2f0eab0ee5..b4ded96001 100644
--- a/openpype/hosts/photoshop/plugins/publish/closePS.py
+++ b/openpype/hosts/photoshop/plugins/publish/closePS.py
@@ -4,7 +4,7 @@ import os
import pyblish.api
-from avalon import photoshop
+from openpype.hosts.photoshop import api as photoshop
class ClosePS(pyblish.api.ContextPlugin):
diff --git a/openpype/hosts/photoshop/plugins/publish/collect_current_file.py b/openpype/hosts/photoshop/plugins/publish/collect_current_file.py
index 4d4829555e..5daf47c6ac 100644
--- a/openpype/hosts/photoshop/plugins/publish/collect_current_file.py
+++ b/openpype/hosts/photoshop/plugins/publish/collect_current_file.py
@@ -2,7 +2,7 @@ import os
import pyblish.api
-from avalon import photoshop
+from openpype.hosts.photoshop import api as photoshop
class CollectCurrentFile(pyblish.api.ContextPlugin):
diff --git a/openpype/hosts/photoshop/plugins/publish/collect_extension_version.py b/openpype/hosts/photoshop/plugins/publish/collect_extension_version.py
index f07ff0b0ff..64c99b4fc1 100644
--- a/openpype/hosts/photoshop/plugins/publish/collect_extension_version.py
+++ b/openpype/hosts/photoshop/plugins/publish/collect_extension_version.py
@@ -2,7 +2,7 @@ import os
import re
import pyblish.api
-from avalon import photoshop
+from openpype.hosts.photoshop import api as photoshop
class CollectExtensionVersion(pyblish.api.ContextPlugin):
diff --git a/openpype/hosts/photoshop/plugins/publish/collect_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_instances.py
index 5390df768b..f67cc0cbac 100644
--- a/openpype/hosts/photoshop/plugins/publish/collect_instances.py
+++ b/openpype/hosts/photoshop/plugins/publish/collect_instances.py
@@ -1,6 +1,6 @@
import pyblish.api
-from avalon import photoshop
+from openpype.hosts.photoshop import api as photoshop
class CollectInstances(pyblish.api.ContextPlugin):
diff --git a/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py
index c76e15484e..e264d04d9f 100644
--- a/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py
+++ b/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py
@@ -1,10 +1,11 @@
-import pyblish.api
import os
import re
-from avalon import photoshop
+import pyblish.api
+
from openpype.lib import prepare_template_data
from openpype.lib.plugin_tools import parse_json
+from openpype.hosts.photoshop import api as photoshop
class CollectRemoteInstances(pyblish.api.ContextPlugin):
diff --git a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py
index 88817c3969..db1ede14d5 100644
--- a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py
+++ b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py
@@ -1,5 +1,5 @@
-import pyblish.api
import os
+import pyblish.api
class CollectWorkfile(pyblish.api.ContextPlugin):
diff --git a/openpype/hosts/photoshop/plugins/publish/extract_image.py b/openpype/hosts/photoshop/plugins/publish/extract_image.py
index ae9892e290..2ba81e0bac 100644
--- a/openpype/hosts/photoshop/plugins/publish/extract_image.py
+++ b/openpype/hosts/photoshop/plugins/publish/extract_image.py
@@ -1,7 +1,7 @@
import os
import openpype.api
-from avalon import photoshop
+from openpype.hosts.photoshop import api as photoshop
class ExtractImage(openpype.api.Extractor):
diff --git a/openpype/hosts/photoshop/plugins/publish/extract_review.py b/openpype/hosts/photoshop/plugins/publish/extract_review.py
index 8c4d05b282..1ad442279a 100644
--- a/openpype/hosts/photoshop/plugins/publish/extract_review.py
+++ b/openpype/hosts/photoshop/plugins/publish/extract_review.py
@@ -2,7 +2,7 @@ import os
import openpype.api
import openpype.lib
-from avalon import photoshop
+from openpype.hosts.photoshop import api as photoshop
class ExtractReview(openpype.api.Extractor):
diff --git a/openpype/hosts/photoshop/plugins/publish/extract_save_scene.py b/openpype/hosts/photoshop/plugins/publish/extract_save_scene.py
index 0180640c90..03086f389f 100644
--- a/openpype/hosts/photoshop/plugins/publish/extract_save_scene.py
+++ b/openpype/hosts/photoshop/plugins/publish/extract_save_scene.py
@@ -1,5 +1,5 @@
import openpype.api
-from avalon import photoshop
+from openpype.hosts.photoshop import api as photoshop
class ExtractSaveScene(openpype.api.Extractor):
diff --git a/openpype/hosts/photoshop/plugins/publish/increment_workfile.py b/openpype/hosts/photoshop/plugins/publish/increment_workfile.py
index 709fb988fc..92132c393b 100644
--- a/openpype/hosts/photoshop/plugins/publish/increment_workfile.py
+++ b/openpype/hosts/photoshop/plugins/publish/increment_workfile.py
@@ -3,7 +3,7 @@ import pyblish.api
from openpype.action import get_errored_plugins_from_data
from openpype.lib import version_up
-from avalon import photoshop
+from openpype.hosts.photoshop import api as photoshop
class IncrementWorkfile(pyblish.api.InstancePlugin):
diff --git a/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py b/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py
index 4dc1972074..ebe9cc21ea 100644
--- a/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py
+++ b/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py
@@ -1,7 +1,7 @@
from avalon import api
import pyblish.api
import openpype.api
-from avalon import photoshop
+from openpype.hosts.photoshop import api as photoshop
class ValidateInstanceAssetRepair(pyblish.api.Action):
diff --git a/openpype/hosts/photoshop/plugins/publish/validate_naming.py b/openpype/hosts/photoshop/plugins/publish/validate_naming.py
index 1635096f4b..b40e44d016 100644
--- a/openpype/hosts/photoshop/plugins/publish/validate_naming.py
+++ b/openpype/hosts/photoshop/plugins/publish/validate_naming.py
@@ -2,7 +2,7 @@ import re
import pyblish.api
import openpype.api
-from avalon import photoshop
+from openpype.hosts.photoshop import api as photoshop
class ValidateNamingRepair(pyblish.api.Action):
diff --git a/openpype/hosts/tvpaint/hooks/pre_launch_args.py b/openpype/hosts/tvpaint/hooks/pre_launch_args.py
index 62fd662d79..2a8f49d5b0 100644
--- a/openpype/hosts/tvpaint/hooks/pre_launch_args.py
+++ b/openpype/hosts/tvpaint/hooks/pre_launch_args.py
@@ -4,7 +4,7 @@ import shutil
from openpype.hosts import tvpaint
from openpype.lib import (
PreLaunchHook,
- get_pype_execute_args
+ get_openpype_execute_args
)
import avalon
@@ -30,7 +30,7 @@ class TvpaintPrelaunchHook(PreLaunchHook):
while self.launch_context.launch_args:
remainders.append(self.launch_context.launch_args.pop(0))
- new_launch_args = get_pype_execute_args(
+ new_launch_args = get_openpype_execute_args(
"run", self.launch_script_path(), executable_path
)
diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py
index 34926453cb..12e47a8961 100644
--- a/openpype/lib/__init__.py
+++ b/openpype/lib/__init__.py
@@ -24,10 +24,13 @@ from .env_tools import (
from .terminal import Terminal
from .execute import (
+ get_openpype_execute_args,
get_pype_execute_args,
get_linux_launcher_args,
execute,
run_subprocess,
+ run_openpype_process,
+ clean_envs_for_openpype_process,
path_to_subprocess_arg,
CREATE_NO_WINDOW
)
@@ -173,10 +176,13 @@ from .pype_info import (
terminal = Terminal
__all__ = [
+ "get_openpype_execute_args",
"get_pype_execute_args",
"get_linux_launcher_args",
"execute",
"run_subprocess",
+ "run_openpype_process",
+ "clean_envs_for_openpype_process",
"path_to_subprocess_arg",
"CREATE_NO_WINDOW",
diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py
index f97617d906..3cf67a379c 100644
--- a/openpype/lib/execute.py
+++ b/openpype/lib/execute.py
@@ -138,6 +138,49 @@ def run_subprocess(*args, **kwargs):
return full_output
+def clean_envs_for_openpype_process(env=None):
+ """Modify environemnts that may affect OpenPype process.
+
+ Main reason to implement this function is to pop PYTHONPATH which may be
+ affected by in-host environments.
+ """
+ if env is None:
+ env = os.environ
+ return {
+ key: value
+ for key, value in env.items()
+ if key not in ("PYTHONPATH",)
+ }
+
+
+def run_openpype_process(*args, **kwargs):
+ """Execute OpenPype process with passed arguments and wait.
+
+ Wrapper for 'run_process' which prepends OpenPype executable arguments
+ before passed arguments and define environments if are not passed.
+
+ Values from 'os.environ' are used for environments if are not passed.
+ They are cleaned using 'clean_envs_for_openpype_process' function.
+
+ Example:
+ ```
+ run_openpype_process("run", "")
+ ```
+
+ Args:
+ *args (tuple): OpenPype cli arguments.
+ **kwargs (dict): Keyword arguments for for subprocess.Popen.
+ """
+ args = get_openpype_execute_args(*args)
+ env = kwargs.pop("env", None)
+ # Keep env untouched if are passed and not empty
+ if not env:
+ # Skip envs that can affect OpenPype process
+ # - fill more if you find more
+ env = clean_envs_for_openpype_process(os.environ)
+ return run_subprocess(args, env=env, **kwargs)
+
+
def path_to_subprocess_arg(path):
"""Prepare path for subprocess arguments.
@@ -147,6 +190,18 @@ def path_to_subprocess_arg(path):
def get_pype_execute_args(*args):
+ """Backwards compatible function for 'get_openpype_execute_args'."""
+ import traceback
+
+ log = Logger.get_logger("get_pype_execute_args")
+ stack = "\n".join(traceback.format_stack())
+ log.warning((
+ "Using deprecated function 'get_pype_execute_args'. Called from:\n{}"
+ ).format(stack))
+ return get_openpype_execute_args(*args)
+
+
+def get_openpype_execute_args(*args):
"""Arguments to run pype command.
Arguments for subprocess when need to spawn new pype process. Which may be
diff --git a/openpype/lib/pype_info.py b/openpype/lib/pype_info.py
index 33715e369d..15856bfb19 100644
--- a/openpype/lib/pype_info.py
+++ b/openpype/lib/pype_info.py
@@ -7,7 +7,7 @@ import socket
import openpype.version
from openpype.settings.lib import get_local_settings
-from .execute import get_pype_execute_args
+from .execute import get_openpype_execute_args
from .local_settings import get_local_site_id
from .python_module_tools import import_filepath
@@ -71,7 +71,7 @@ def is_running_staging():
def get_pype_info():
"""Information about currently used Pype process."""
- executable_args = get_pype_execute_args()
+ executable_args = get_openpype_execute_args()
if is_running_from_build():
version_type = "build"
else:
diff --git a/openpype/modules/base.py b/openpype/modules/base.py
index b5c491a1c0..d566692439 100644
--- a/openpype/modules/base.py
+++ b/openpype/modules/base.py
@@ -42,6 +42,7 @@ DEFAULT_OPENPYPE_MODULES = (
"settings_action",
"standalonepublish_action",
"job_queue",
+ "timers_manager",
)
diff --git a/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py b/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py
index 1a76905b38..90ce757242 100644
--- a/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py
+++ b/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py
@@ -14,7 +14,7 @@ import uuid
import ftrack_api
import pymongo
from openpype.lib import (
- get_pype_execute_args,
+ get_openpype_execute_args,
OpenPypeMongoConnection,
get_openpype_version,
get_build_version,
@@ -136,7 +136,7 @@ def legacy_server(ftrack_url):
if subproc is None:
if subproc_failed_count < max_fail_count:
- args = get_pype_execute_args("run", subproc_path)
+ args = get_openpype_execute_args("run", subproc_path)
subproc = subprocess.Popen(
args,
stdout=subprocess.PIPE
@@ -248,7 +248,7 @@ def main_loop(ftrack_url):
["Username", getpass.getuser()],
["Host Name", host_name],
["Host IP", socket.gethostbyname(host_name)],
- ["OpenPype executable", get_pype_execute_args()[-1]],
+ ["OpenPype executable", get_openpype_execute_args()[-1]],
["OpenPype version", get_openpype_version() or "N/A"],
["OpenPype build version", get_build_version() or "N/A"]
]
diff --git a/openpype/modules/default_modules/ftrack/ftrack_server/socket_thread.py b/openpype/modules/default_modules/ftrack/ftrack_server/socket_thread.py
index eb8ec4d06c..f49ca5557e 100644
--- a/openpype/modules/default_modules/ftrack/ftrack_server/socket_thread.py
+++ b/openpype/modules/default_modules/ftrack/ftrack_server/socket_thread.py
@@ -6,7 +6,7 @@ import threading
import traceback
import subprocess
from openpype.api import Logger
-from openpype.lib import get_pype_execute_args
+from openpype.lib import get_openpype_execute_args
class SocketThread(threading.Thread):
@@ -59,7 +59,7 @@ class SocketThread(threading.Thread):
env = os.environ.copy()
env["OPENPYPE_PROCESS_MONGO_ID"] = str(Logger.mongo_process_id)
# OpenPype executable (with path to start script if not build)
- args = get_pype_execute_args(
+ args = get_openpype_execute_args(
# Add `run` command
"run",
self.filepath,
diff --git a/openpype/modules/standalonepublish_action.py b/openpype/modules/standalonepublish_action.py
index 9321a415a9..ba53ce9b9e 100644
--- a/openpype/modules/standalonepublish_action.py
+++ b/openpype/modules/standalonepublish_action.py
@@ -1,7 +1,7 @@
import os
import platform
import subprocess
-from openpype.lib import get_pype_execute_args
+from openpype.lib import get_openpype_execute_args
from openpype.modules import OpenPypeModule
from openpype_interfaces import ITrayAction
@@ -35,7 +35,7 @@ class StandAlonePublishAction(OpenPypeModule, ITrayAction):
self.publish_paths.extend(publish_paths)
def run_standalone_publisher(self):
- args = get_pype_execute_args("standalonepublisher")
+ args = get_openpype_execute_args("standalonepublisher")
kwargs = {}
if platform.system().lower() == "darwin":
new_args = ["open", "-na", args.pop(0), "--args"]
diff --git a/openpype/modules/default_modules/timers_manager/__init__.py b/openpype/modules/timers_manager/__init__.py
similarity index 100%
rename from openpype/modules/default_modules/timers_manager/__init__.py
rename to openpype/modules/timers_manager/__init__.py
diff --git a/openpype/modules/default_modules/timers_manager/exceptions.py b/openpype/modules/timers_manager/exceptions.py
similarity index 100%
rename from openpype/modules/default_modules/timers_manager/exceptions.py
rename to openpype/modules/timers_manager/exceptions.py
diff --git a/openpype/modules/default_modules/timers_manager/idle_threads.py b/openpype/modules/timers_manager/idle_threads.py
similarity index 100%
rename from openpype/modules/default_modules/timers_manager/idle_threads.py
rename to openpype/modules/timers_manager/idle_threads.py
diff --git a/openpype/modules/default_modules/timers_manager/launch_hooks/post_start_timer.py b/openpype/modules/timers_manager/launch_hooks/post_start_timer.py
similarity index 100%
rename from openpype/modules/default_modules/timers_manager/launch_hooks/post_start_timer.py
rename to openpype/modules/timers_manager/launch_hooks/post_start_timer.py
diff --git a/openpype/modules/default_modules/timers_manager/rest_api.py b/openpype/modules/timers_manager/rest_api.py
similarity index 100%
rename from openpype/modules/default_modules/timers_manager/rest_api.py
rename to openpype/modules/timers_manager/rest_api.py
diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/timers_manager/timers_manager.py
similarity index 100%
rename from openpype/modules/default_modules/timers_manager/timers_manager.py
rename to openpype/modules/timers_manager/timers_manager.py
diff --git a/openpype/modules/default_modules/timers_manager/widget_user_idle.py b/openpype/modules/timers_manager/widget_user_idle.py
similarity index 100%
rename from openpype/modules/default_modules/timers_manager/widget_user_idle.py
rename to openpype/modules/timers_manager/widget_user_idle.py
diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py
index df7dc47e17..459c66ee43 100644
--- a/openpype/plugins/publish/extract_burnin.py
+++ b/openpype/plugins/publish/extract_burnin.py
@@ -13,7 +13,7 @@ import pyblish
import openpype
import openpype.api
from openpype.lib import (
- get_pype_execute_args,
+ run_openpype_process,
get_transcode_temp_directory,
convert_for_ffmpeg,
@@ -168,9 +168,8 @@ class ExtractBurnin(openpype.api.Extractor):
anatomy = instance.context.data["anatomy"]
scriptpath = self.burnin_script_path()
- # Executable args that will execute the script
- # [pype executable, *pype script, "run"]
- executable_args = get_pype_execute_args("run", scriptpath)
+ # Args that will execute the script
+ executable_args = ["run", scriptpath]
burnins_per_repres = self._get_burnins_per_representations(
instance, burnin_defs
)
@@ -313,7 +312,7 @@ class ExtractBurnin(openpype.api.Extractor):
if platform.system().lower() == "windows":
process_kwargs["creationflags"] = CREATE_NO_WINDOW
- openpype.api.run_subprocess(args, **process_kwargs)
+ run_openpype_process(*args, **process_kwargs)
# Remove the temporary json
os.remove(temporary_json_filepath)
diff --git a/openpype/scripts/non_python_host_launch.py b/openpype/scripts/non_python_host_launch.py
index 32c4b23f4f..6b17e6a037 100644
--- a/openpype/scripts/non_python_host_launch.py
+++ b/openpype/scripts/non_python_host_launch.py
@@ -81,7 +81,7 @@ def main(argv):
host_name = os.environ["AVALON_APP"].lower()
if host_name == "photoshop":
- from avalon.photoshop.lib import main
+ from openpype.hosts.photoshop.api.lib import main
elif host_name == "aftereffects":
from avalon.aftereffects.lib import main
elif host_name == "harmony":
diff --git a/openpype/style/data.json b/openpype/style/data.json
index 62573f015e..b3dffd7c71 100644
--- a/openpype/style/data.json
+++ b/openpype/style/data.json
@@ -51,6 +51,9 @@
"border-hover": "rgba(168, 175, 189, .3)",
"border-focus": "rgb(92, 173, 214)",
+ "delete-btn-bg": "rgb(201, 54, 54)",
+ "delete-btn-bg-disabled": "rgba(201, 54, 54, 64)",
+
"tab-widget": {
"bg": "#21252B",
"bg-selected": "#434a56",
diff --git a/openpype/style/style.css b/openpype/style/style.css
index 3e95ece4b9..7f7f30e2bc 100644
--- a/openpype/style/style.css
+++ b/openpype/style/style.css
@@ -734,6 +734,13 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
background: {color:bg-view-hover};
}
+#DeleteButton {
+ background: {color:delete-btn-bg};
+}
+#DeleteButton:disabled {
+ background: {color:delete-btn-bg-disabled};
+}
+
/* Launcher specific stylesheets */
#IconView[mode="icon"] {
/* font size can't be set on items */
diff --git a/openpype/tools/project_manager/project_manager/images/warning.png b/openpype/tools/project_manager/project_manager/images/warning.png
new file mode 100644
index 0000000000..3b4ae861f9
Binary files /dev/null and b/openpype/tools/project_manager/project_manager/images/warning.png differ
diff --git a/openpype/tools/project_manager/project_manager/style.py b/openpype/tools/project_manager/project_manager/style.py
index d3d6857a63..9fa7a5520b 100644
--- a/openpype/tools/project_manager/project_manager/style.py
+++ b/openpype/tools/project_manager/project_manager/style.py
@@ -1,6 +1,7 @@
import os
from Qt import QtCore, QtGui
+from openpype.style import get_objected_colors
from avalon.vendor import qtawesome
@@ -90,6 +91,17 @@ class ResourceCache:
icon.addPixmap(disabled_pix, QtGui.QIcon.Disabled, QtGui.QIcon.Off)
return icon
+ @classmethod
+ def get_warning_pixmap(cls):
+ src_image = get_warning_image()
+ colors = get_objected_colors()
+ color_value = colors["delete-btn-bg"]
+
+ return paint_image_with_color(
+ src_image,
+ color_value.get_qcolor()
+ )
+
def get_remove_image():
image_path = os.path.join(
@@ -100,6 +112,15 @@ def get_remove_image():
return QtGui.QImage(image_path)
+def get_warning_image():
+ image_path = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)),
+ "images",
+ "warning.png"
+ )
+ return QtGui.QImage(image_path)
+
+
def paint_image_with_color(image, color):
"""TODO: This function should be imported from utils.
diff --git a/openpype/tools/project_manager/project_manager/widgets.py b/openpype/tools/project_manager/project_manager/widgets.py
index e4c58a8a2c..4b5aca35ef 100644
--- a/openpype/tools/project_manager/project_manager/widgets.py
+++ b/openpype/tools/project_manager/project_manager/widgets.py
@@ -4,6 +4,7 @@ from .constants import (
NAME_ALLOWED_SYMBOLS,
NAME_REGEX
)
+from .style import ResourceCache
from openpype.lib import (
create_project,
PROJECT_NAME_ALLOWED_SYMBOLS,
@@ -13,7 +14,7 @@ from openpype.style import load_stylesheet
from openpype.tools.utils import PlaceholderLineEdit
from avalon.api import AvalonMongoDB
-from Qt import QtWidgets, QtCore
+from Qt import QtWidgets, QtCore, QtGui
class NameTextEdit(QtWidgets.QLineEdit):
@@ -291,42 +292,41 @@ class CreateProjectDialog(QtWidgets.QDialog):
return project_names, project_codes
-class _SameSizeBtns(QtWidgets.QPushButton):
- """Button that keep width of all button added as related.
+# TODO PixmapLabel should be moved to 'utils' in other future PR so should be
+# imported from there
+class PixmapLabel(QtWidgets.QLabel):
+ """Label resizing image to height of font."""
+ def __init__(self, pixmap, parent):
+ super(PixmapLabel, self).__init__(parent)
+ self._empty_pixmap = QtGui.QPixmap(0, 0)
+ self._source_pixmap = pixmap
- This happens without changing min/max/fix size of button. Which is
- welcomed for multidisplay desktops with different resolution.
- """
- def __init__(self, *args, **kwargs):
- super(_SameSizeBtns, self).__init__(*args, **kwargs)
- self._related_btns = []
+ def set_source_pixmap(self, pixmap):
+ """Change source image."""
+ self._source_pixmap = pixmap
+ self._set_resized_pix()
- def add_related_btn(self, btn):
- """Add related button which should be checked for width.
+ def _get_pix_size(self):
+ size = self.fontMetrics().height() * 4
+ return size, size
- Args:
- btn (_SameSizeBtns): Other object of _SameSizeBtns.
- """
- self._related_btns.append(btn)
+ def _set_resized_pix(self):
+ if self._source_pixmap is None:
+ self.setPixmap(self._empty_pixmap)
+ return
+ width, height = self._get_pix_size()
+ self.setPixmap(
+ self._source_pixmap.scaled(
+ width,
+ height,
+ QtCore.Qt.KeepAspectRatio,
+ QtCore.Qt.SmoothTransformation
+ )
+ )
- def hint_width(self):
- """Get size hint of button not related to others."""
- return super(_SameSizeBtns, self).sizeHint().width()
-
- def sizeHint(self):
- """Calculate size hint based on size hint of this button and related.
-
- If width is lower than any other button it is changed to higher.
- """
- result = super(_SameSizeBtns, self).sizeHint()
- width = result.width()
- for btn in self._related_btns:
- btn_width = btn.hint_width()
- if btn_width > width:
- width = btn_width
-
- result.setWidth(width)
- return result
+ def resizeEvent(self, event):
+ self._set_resized_pix()
+ super(PixmapLabel, self).resizeEvent(event)
class ConfirmProjectDeletion(QtWidgets.QDialog):
@@ -336,35 +336,50 @@ class ConfirmProjectDeletion(QtWidgets.QDialog):
self.setWindowTitle("Delete project?")
- message = (
- "Project \"{}\" with all related data will be"
- " permanently removed from the database (This actions won't remove"
- " any files on disk)."
- ).format(project_name)
- message_label = QtWidgets.QLabel(message, self)
+ top_widget = QtWidgets.QWidget(self)
+
+ warning_pixmap = ResourceCache.get_warning_pixmap()
+ warning_icon_label = PixmapLabel(warning_pixmap, top_widget)
+
+ message_label = QtWidgets.QLabel(top_widget)
message_label.setWordWrap(True)
+ message_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
+ message_label.setText((
+ "WARNING: This cannot be undone.
"
+ "Project \"{}\" with all related data will be"
+ " permanently removed from the database. (This action won't remove"
+ " any files on disk.)"
+ ).format(project_name))
+
+ top_layout = QtWidgets.QHBoxLayout(top_widget)
+ top_layout.setContentsMargins(0, 0, 0, 0)
+ top_layout.addWidget(
+ warning_icon_label, 0,
+ QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter
+ )
+ top_layout.addWidget(message_label, 1)
question_label = QtWidgets.QLabel("Are you sure?", self)
confirm_input = PlaceholderLineEdit(self)
- confirm_input.setPlaceholderText("Type \"Delete\" to confirm...")
+ confirm_input.setPlaceholderText(
+ "Type \"{}\" to confirm...".format(project_name)
+ )
- cancel_btn = _SameSizeBtns("Cancel", self)
+ cancel_btn = QtWidgets.QPushButton("Cancel", self)
cancel_btn.setToolTip("Cancel deletion of the project")
- confirm_btn = _SameSizeBtns("Delete", self)
+ confirm_btn = QtWidgets.QPushButton("Permanently Delete Project", self)
+ confirm_btn.setObjectName("DeleteButton")
confirm_btn.setEnabled(False)
confirm_btn.setToolTip("Confirm deletion")
- cancel_btn.add_related_btn(confirm_btn)
- confirm_btn.add_related_btn(cancel_btn)
-
btns_layout = QtWidgets.QHBoxLayout()
btns_layout.addStretch(1)
btns_layout.addWidget(cancel_btn, 0)
btns_layout.addWidget(confirm_btn, 0)
layout = QtWidgets.QVBoxLayout(self)
- layout.addWidget(message_label, 0)
+ layout.addWidget(top_widget, 0)
layout.addStretch(1)
layout.addWidget(question_label, 0)
layout.addWidget(confirm_input, 0)
@@ -379,6 +394,7 @@ class ConfirmProjectDeletion(QtWidgets.QDialog):
self._confirm_btn = confirm_btn
self._confirm_input = confirm_input
self._result = 0
+ self._project_name = project_name
self.setMinimumWidth(480)
self.setMaximumWidth(650)
@@ -411,5 +427,5 @@ class ConfirmProjectDeletion(QtWidgets.QDialog):
self._on_confirm_click()
def _on_confirm_text_change(self):
- enabled = self._confirm_input.text().lower() == "delete"
+ enabled = self._confirm_input.text() == self._project_name
self._confirm_btn.setEnabled(enabled)
diff --git a/openpype/tools/project_manager/project_manager/window.py b/openpype/tools/project_manager/project_manager/window.py
index a05811e813..0298d565a5 100644
--- a/openpype/tools/project_manager/project_manager/window.py
+++ b/openpype/tools/project_manager/project_manager/window.py
@@ -78,7 +78,9 @@ class ProjectManagerWindow(QtWidgets.QWidget):
)
create_folders_btn.setEnabled(False)
- remove_projects_btn = QtWidgets.QPushButton(project_widget)
+ remove_projects_btn = QtWidgets.QPushButton(
+ "Delete project", project_widget
+ )
remove_projects_btn.setIcon(ResourceCache.get_icon("remove"))
remove_projects_btn.setObjectName("IconBtn")
diff --git a/openpype/tools/standalonepublish/widgets/widget_components.py b/openpype/tools/standalonepublish/widgets/widget_components.py
index 2ac54af4e3..4d7f94f825 100644
--- a/openpype/tools/standalonepublish/widgets/widget_components.py
+++ b/openpype/tools/standalonepublish/widgets/widget_components.py
@@ -10,7 +10,7 @@ from .constants import HOST_NAME
from avalon import io
from openpype.api import execute, Logger
from openpype.lib import (
- get_pype_execute_args,
+ get_openpype_execute_args,
apply_project_environments_value
)
@@ -193,7 +193,7 @@ def cli_publish(data, publish_paths, gui=True):
project_name = os.environ["AVALON_PROJECT"]
env_copy = apply_project_environments_value(project_name, envcopy)
- args = get_pype_execute_args("run", PUBLISH_SCRIPT_PATH)
+ args = get_openpype_execute_args("run", PUBLISH_SCRIPT_PATH)
result = execute(args, env=envcopy)
result = {}
diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py
index 8c6a6d3266..df0238c848 100644
--- a/openpype/tools/tray/pype_tray.py
+++ b/openpype/tools/tray/pype_tray.py
@@ -14,7 +14,7 @@ from openpype.api import (
resources,
get_system_settings
)
-from openpype.lib import get_pype_execute_args
+from openpype.lib import get_openpype_execute_args
from openpype.modules import TrayModulesManager
from openpype import style
from openpype.settings import (
@@ -208,10 +208,10 @@ class TrayManager:
First creates new process with same argument and close current tray.
"""
- args = get_pype_execute_args()
+ args = get_openpype_execute_args()
# Create a copy of sys.argv
additional_args = list(sys.argv)
- # Check last argument from `get_pype_execute_args`
+ # Check last argument from `get_openpype_execute_args`
# - when running from code it is the same as first from sys.argv
if args[-1] == additional_args[0]:
additional_args.pop(0)