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: + +![Avalon Panel](panel.PNG "Avalon Panel") + + +## Developing + +### Extension +When developing the extension you can load it [unsigned](https://github.com/Adobe-CEP/CEP-Resources/blob/master/CEP_9.x/Documentation/CEP%209.0%20HTML%20Extension%20Cookbook.md#debugging-unsigned-extensions). + +When signing the extension you can use this [guide](https://github.com/Adobe-CEP/Getting-Started-guides/tree/master/Package%20Distribute%20Install#package-distribute-install-guide). + +``` +ZXPSignCmd -selfSignedCert NA NA 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 + OpenPype + + + 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: + * + * + * @return A new \c CSInterface object + */ +function CSInterface() +{ +} + +/** + * User can add this event listener to handle native application theme color changes. + * Callback function gives extensions ability to fine-tune their theme color after the + * global theme color has been changed. + * The callback function should be like below: + * + * @example + * // event is a CSEvent object, but user can ignore it. + * function OnAppThemeColorChanged(event) + * { + * // Should get a latest HostEnvironment object from application. + * var skinInfo = JSON.parse(window.__adobe_cep__.getHostEnvironment()).appSkinInfo; + * // Gets the style information such as color info from the skinInfo, + * // and redraw all UI controls of your extension according to the style info. + * } + */ +CSInterface.THEME_COLOR_CHANGED_EVENT = "com.adobe.csxs.events.ThemeColorChanged"; + +/** The host environment data object. */ +CSInterface.prototype.hostEnvironment = window.__adobe_cep__ ? JSON.parse(window.__adobe_cep__.getHostEnvironment()) : null; + +/** Retrieves information about the host environment in which the + * extension is currently running. + * + * @return A \c #HostEnvironment object. + */ +CSInterface.prototype.getHostEnvironment = function() +{ + this.hostEnvironment = JSON.parse(window.__adobe_cep__.getHostEnvironment()); + return this.hostEnvironment; +}; + +/** Closes this extension. */ +CSInterface.prototype.closeExtension = function() +{ + window.__adobe_cep__.closeExtension(); +}; + +/** + * Retrieves a path for which a constant is defined in the system. + * + * @param pathType The path-type constant defined in \c #SystemPath , + * + * @return The platform-specific system path string. + */ +CSInterface.prototype.getSystemPath = function(pathType) +{ + var path = decodeURI(window.__adobe_cep__.getSystemPath(pathType)); + var OSVersion = this.getOSInformation(); + if (OSVersion.indexOf("Windows") >= 0) + { + path = path.replace("file:///", ""); + } + else if (OSVersion.indexOf("Mac") >= 0) + { + path = path.replace("file://", ""); + } + return path; +}; + +/** + * Evaluates a JavaScript script, which can use the JavaScript DOM + * of the host application. + * + * @param script The JavaScript script. + * @param callback Optional. A callback function that receives the result of execution. + * If execution fails, the callback function receives the error message \c EvalScript_ErrMessage. + */ +CSInterface.prototype.evalScript = function(script, callback) +{ + if(callback === null || callback === undefined) + { + callback = function(result){}; + } + window.__adobe_cep__.evalScript(script, callback); +}; + +/** + * Retrieves the unique identifier of the application. + * in which the extension is currently running. + * + * @return The unique ID string. + */ +CSInterface.prototype.getApplicationID = function() +{ + var appId = this.hostEnvironment.appId; + return appId; +}; + +/** + * Retrieves host capability information for the application + * in which the extension is currently running. + * + * @return A \c #HostCapabilities object. + */ +CSInterface.prototype.getHostCapabilities = function() +{ + var hostCapabilities = JSON.parse(window.__adobe_cep__.getHostCapabilities() ); + return hostCapabilities; +}; + +/** + * Triggers a CEP event programmatically. Yoy can use it to dispatch + * an event of a predefined type, or of a type you have defined. + * + * @param event A \c CSEvent object. + */ +CSInterface.prototype.dispatchEvent = function(event) +{ + if (typeof event.data == "object") + { + event.data = JSON.stringify(event.data); + } + + window.__adobe_cep__.dispatchEvent(event); +}; + +/** + * Registers an interest in a CEP event of a particular type, and + * assigns an event handler. + * The event infrastructure notifies your extension when events of this type occur, + * passing the event object to the registered handler function. + * + * @param type The name of the event type of interest. + * @param listener The JavaScript handler function or method. + * @param obj Optional, the object containing the handler method, if any. + * Default is null. + */ +CSInterface.prototype.addEventListener = function(type, listener, obj) +{ + window.__adobe_cep__.addEventListener(type, listener, obj); +}; + +/** + * Removes a registered event listener. + * + * @param type The name of the event type of interest. + * @param listener The JavaScript handler function or method that was registered. + * @param obj Optional, the object containing the handler method, if any. + * Default is null. + */ +CSInterface.prototype.removeEventListener = function(type, listener, obj) +{ + window.__adobe_cep__.removeEventListener(type, listener, obj); +}; + +/** + * Loads and launches another extension, or activates the extension if it is already loaded. + * + * @param extensionId The extension's unique identifier. + * @param startupParams Not currently used, pass "". + * + * @example + * To launch the extension "help" with ID "HLP" from this extension, call: + * requestOpenExtension("HLP", ""); + * + */ +CSInterface.prototype.requestOpenExtension = function(extensionId, params) +{ + window.__adobe_cep__.requestOpenExtension(extensionId, params); +}; + +/** + * Retrieves the list of extensions currently loaded in the current host application. + * The extension list is initialized once, and remains the same during the lifetime + * of the CEP session. + * + * @param extensionIds Optional, an array of unique identifiers for extensions of interest. + * If omitted, retrieves data for all extensions. + * + * @return Zero or more \c #Extension objects. + */ +CSInterface.prototype.getExtensions = function(extensionIds) +{ + var extensionIdsStr = JSON.stringify(extensionIds); + var extensionsStr = window.__adobe_cep__.getExtensions(extensionIdsStr); + + var extensions = JSON.parse(extensionsStr); + return extensions; +}; + +/** + * Retrieves network-related preferences. + * + * @return A JavaScript object containing network preferences. + */ +CSInterface.prototype.getNetworkPreferences = function() +{ + var result = window.__adobe_cep__.getNetworkPreferences(); + var networkPre = JSON.parse(result); + + return networkPre; +}; + +/** + * Initializes the resource bundle for this extension with property values + * for the current application and locale. + * To support multiple locales, you must define a property file for each locale, + * containing keyed display-string values for that locale. + * See localization documentation for Extension Builder and related products. + * + * Keys can be in the + * form key.value="localized string", for use in HTML text elements. + * For example, in this input element, the localized \c key.value string is displayed + * instead of the empty \c value string: + * + * + * + * @return An object containing the resource bundle information. + */ +CSInterface.prototype.initResourceBundle = function() +{ + var resourceBundle = JSON.parse(window.__adobe_cep__.initResourceBundle()); + var resElms = document.querySelectorAll('[data-locale]'); + for (var n = 0; n < resElms.length; n++) + { + var resEl = resElms[n]; + // Get the resource key from the element. + var resKey = resEl.getAttribute('data-locale'); + if (resKey) + { + // Get all the resources that start with the key. + for (var key in resourceBundle) + { + if (key.indexOf(resKey) === 0) + { + var resValue = resourceBundle[key]; + if (key.length == resKey.length) + { + resEl.innerHTML = resValue; + } + else if ('.' == key.charAt(resKey.length)) + { + var attrKey = key.substring(resKey.length + 1); + resEl[attrKey] = resValue; + } + } + } + } + } + return resourceBundle; +}; + +/** + * Writes installation information to a file. + * + * @return The file path. + */ +CSInterface.prototype.dumpInstallationInfo = function() +{ + return window.__adobe_cep__.dumpInstallationInfo(); +}; + +/** + * Retrieves version information for the current Operating System, + * See http://www.useragentstring.com/pages/Chrome/ for Chrome \c navigator.userAgent values. + * + * @return A string containing the OS version, or "unknown Operation System". + * If user customizes the User Agent by setting CEF command parameter "--user-agent", only + * "Mac OS X" or "Windows" will be returned. + */ +CSInterface.prototype.getOSInformation = function() +{ + var userAgent = navigator.userAgent; + + if ((navigator.platform == "Win32") || (navigator.platform == "Windows")) + { + var winVersion = "Windows"; + var winBit = ""; + if (userAgent.indexOf("Windows") > -1) + { + if (userAgent.indexOf("Windows NT 5.0") > -1) + { + winVersion = "Windows 2000"; + } + else if (userAgent.indexOf("Windows NT 5.1") > -1) + { + winVersion = "Windows XP"; + } + else if (userAgent.indexOf("Windows NT 5.2") > -1) + { + winVersion = "Windows Server 2003"; + } + else if (userAgent.indexOf("Windows NT 6.0") > -1) + { + winVersion = "Windows Vista"; + } + else if (userAgent.indexOf("Windows NT 6.1") > -1) + { + winVersion = "Windows 7"; + } + else if (userAgent.indexOf("Windows NT 6.2") > -1) + { + winVersion = "Windows 8"; + } + else if (userAgent.indexOf("Windows NT 6.3") > -1) + { + winVersion = "Windows 8.1"; + } + else if (userAgent.indexOf("Windows NT 10") > -1) + { + winVersion = "Windows 10"; + } + + if (userAgent.indexOf("WOW64") > -1 || userAgent.indexOf("Win64") > -1) + { + winBit = " 64-bit"; + } + else + { + winBit = " 32-bit"; + } + } + + return winVersion + winBit; + } + else if ((navigator.platform == "MacIntel") || (navigator.platform == "Macintosh")) + { + var result = "Mac OS X"; + + if (userAgent.indexOf("Mac OS X") > -1) + { + result = userAgent.substring(userAgent.indexOf("Mac OS X"), userAgent.indexOf(")")); + result = result.replace(/_/g, "."); + } + + return result; + } + + return "Unknown Operation System"; +}; + +/** + * Opens a page in the default system browser. + * + * Since 4.2.0 + * + * @param url The URL of the page/file to open, or the email address. + * Must use HTTP/HTTPS/file/mailto protocol. For example: + * "http://www.adobe.com" + * "https://github.com" + * "file:///C:/log.txt" + * "mailto:test@adobe.com" + * + * @return One of these error codes:\n + * \n + */ +CSInterface.prototype.openURLInDefaultBrowser = function(url) +{ + return cep.util.openURLInDefaultBrowser(url); +}; + +/** + * Retrieves extension ID. + * + * Since 4.2.0 + * + * @return extension ID. + */ +CSInterface.prototype.getExtensionID = function() +{ + return window.__adobe_cep__.getExtensionId(); +}; + +/** + * Retrieves the scale factor of screen. + * On Windows platform, the value of scale factor might be different from operating system's scale factor, + * since host application may use its self-defined scale factor. + * + * Since 4.2.0 + * + * @return One of the following float number. + * \n + */ +CSInterface.prototype.getScaleFactor = function() +{ + return window.__adobe_cep__.getScaleFactor(); +}; + +/** + * Set a handler to detect any changes of scale factor. This only works on Mac. + * + * Since 4.2.0 + * + * @param handler The function to be called when scale factor is changed. + * + */ +CSInterface.prototype.setScaleFactorChangedHandler = function(handler) +{ + window.__adobe_cep__.setScaleFactorChangedHandler(handler); +}; + +/** + * Retrieves current API version. + * + * Since 4.2.0 + * + * @return ApiVersion object. + * + */ +CSInterface.prototype.getCurrentApiVersion = function() +{ + var apiVersion = JSON.parse(window.__adobe_cep__.getCurrentApiVersion()); + return apiVersion; +}; + +/** + * Set panel flyout menu by an XML. + * + * Since 5.2.0 + * + * Register a callback function for "com.adobe.csxs.events.flyoutMenuClicked" to get notified when a + * menu item is clicked. + * The "data" attribute of event is an object which contains "menuId" and "menuName" attributes. + * + * Register callback functions for "com.adobe.csxs.events.flyoutMenuOpened" and "com.adobe.csxs.events.flyoutMenuClosed" + * respectively to get notified when flyout menu is opened or closed. + * + * @param menu A XML string which describes menu structure. + * An example menu XML: + * + * + * + * + * + * + * + * + * + * + * + * + */ +CSInterface.prototype.setPanelFlyoutMenu = function(menu) +{ + if ("string" != typeof menu) + { + return; + } + + window.__adobe_cep__.invokeSync("setPanelFlyoutMenu", menu); +}; + +/** + * Updates a menu item in the extension window's flyout menu, by setting the enabled + * and selection status. + * + * Since 5.2.0 + * + * @param menuItemLabel The menu item label. + * @param enabled True to enable the item, false to disable it (gray it out). + * @param checked True to select the item, false to deselect it. + * + * @return false when the host application does not support this functionality (HostCapabilities.EXTENDED_PANEL_MENU is false). + * Fails silently if menu label is invalid. + * + * @see HostCapabilities.EXTENDED_PANEL_MENU + */ +CSInterface.prototype.updatePanelMenuItem = function(menuItemLabel, enabled, checked) +{ + var ret = false; + if (this.getHostCapabilities().EXTENDED_PANEL_MENU) + { + var itemStatus = new MenuItemStatus(menuItemLabel, enabled, checked); + ret = window.__adobe_cep__.invokeSync("updatePanelMenuItem", JSON.stringify(itemStatus)); + } + return ret; +}; + + +/** + * Set context menu by XML string. + * + * Since 5.2.0 + * + * There are a number of conventions used to communicate what type of menu item to create and how it should be handled. + * - an item without menu ID or menu name is disabled and is not shown. + * - if the item name is "---" (three hyphens) then it is treated as a separator. The menu ID in this case will always be NULL. + * - Checkable attribute takes precedence over Checked attribute. + * - a PNG icon. For optimal display results please supply a 16 x 16px icon as larger dimensions will increase the size of the menu item. + The Chrome extension contextMenus API was taken as a reference. + https://developer.chrome.com/extensions/contextMenus + * - the items with icons and checkable items cannot coexist on the same menu level. The former take precedences over the latter. + * + * @param menu A XML string which describes menu structure. + * @param callback The callback function which is called when a menu item is clicked. The only parameter is the returned ID of clicked menu item. + * + * @description An example menu XML: + * + * + * + * + * + * + * + * + * + * + * + */ +CSInterface.prototype.setContextMenu = function(menu, callback) +{ + if ("string" != typeof menu) + { + return; + } + + window.__adobe_cep__.invokeAsync("setContextMenu", menu, callback); +}; + +/** + * Set context menu by JSON string. + * + * Since 6.0.0 + * + * There are a number of conventions used to communicate what type of menu item to create and how it should be handled. + * - an item without menu ID or menu name is disabled and is not shown. + * - if the item label is "---" (three hyphens) then it is treated as a separator. The menu ID in this case will always be NULL. + * - Checkable attribute takes precedence over Checked attribute. + * - a PNG icon. For optimal display results please supply a 16 x 16px icon as larger dimensions will increase the size of the menu item. + The Chrome extension contextMenus API was taken as a reference. + * - the items with icons and checkable items cannot coexist on the same menu level. The former take precedences over the latter. + https://developer.chrome.com/extensions/contextMenus + * + * @param menu A JSON string which describes menu structure. + * @param callback The callback function which is called when a menu item is clicked. The only parameter is the returned ID of clicked menu item. + * + * @description An example menu JSON: + * + * { + * "menu": [ + * { + * "id": "menuItemId1", + * "label": "testExample1", + * "enabled": true, + * "checkable": true, + * "checked": false, + * "icon": "./image/small_16X16.png" + * }, + * { + * "id": "menuItemId2", + * "label": "testExample2", + * "menu": [ + * { + * "id": "menuItemId2-1", + * "label": "testExample2-1", + * "menu": [ + * { + * "id": "menuItemId2-1-1", + * "label": "testExample2-1-1", + * "enabled": false, + * "checkable": true, + * "checked": true + * } + * ] + * }, + * { + * "id": "menuItemId2-2", + * "label": "testExample2-2", + * "enabled": true, + * "checkable": true, + * "checked": true + * } + * ] + * }, + * { + * "label": "---" + * }, + * { + * "id": "menuItemId3", + * "label": "testExample3", + * "enabled": false, + * "checkable": true, + * "checked": false + * } + * ] + * } + * + */ +CSInterface.prototype.setContextMenuByJSON = function(menu, callback) +{ + if ("string" != typeof menu) + { + return; + } + + window.__adobe_cep__.invokeAsync("setContextMenuByJSON", menu, callback); +}; + +/** + * Updates a context menu item by setting the enabled and selection status. + * + * Since 5.2.0 + * + * @param menuItemID The menu item ID. + * @param enabled True to enable the item, false to disable it (gray it out). + * @param checked True to select the item, false to deselect it. + */ +CSInterface.prototype.updateContextMenuItem = function(menuItemID, enabled, checked) +{ + var itemStatus = new ContextMenuItemStatus(menuItemID, enabled, checked); + ret = window.__adobe_cep__.invokeSync("updateContextMenuItem", JSON.stringify(itemStatus)); +}; + +/** + * Get the visibility status of an extension window. + * + * Since 6.0.0 + * + * @return true if the extension window is visible; false if the extension window is hidden. + */ +CSInterface.prototype.isWindowVisible = function() +{ + return window.__adobe_cep__.invokeSync("isWindowVisible", ""); +}; + +/** + * Resize extension's content to the specified dimensions. + * 1. Works with modal and modeless extensions in all Adobe products. + * 2. Extension's manifest min/max size constraints apply and take precedence. + * 3. For panel extensions + * 3.1 This works in all Adobe products except: + * * Premiere Pro + * * Prelude + * * After Effects + * 3.2 When the panel is in certain states (especially when being docked), + * it will not change to the desired dimensions even when the + * specified size satisfies min/max constraints. + * + * Since 6.0.0 + * + * @param width The new width + * @param height The new height + */ +CSInterface.prototype.resizeContent = function(width, height) +{ + window.__adobe_cep__.resizeContent(width, height); +}; + +/** + * Register the invalid certificate callback for an extension. + * This callback will be triggered when the extension tries to access the web site that contains the invalid certificate on the main frame. + * But if the extension does not call this function and tries to access the web site containing the invalid certificate, a default error page will be shown. + * + * Since 6.1.0 + * + * @param callback the callback function + */ +CSInterface.prototype.registerInvalidCertificateCallback = function(callback) +{ + return window.__adobe_cep__.registerInvalidCertificateCallback(callback); +}; + +/** + * Register an interest in some key events to prevent them from being sent to the host application. + * + * This function works with modeless extensions and panel extensions. + * Generally all the key events will be sent to the host application for these two extensions if the current focused element + * is not text input or dropdown, + * If you want to intercept some key events and want them to be handled in the extension, please call this function + * in advance to prevent them being sent to the host application. + * + * Since 6.1.0 + * + * @param keyEventsInterest A JSON string describing those key events you are interested in. A null object or + an empty string will lead to removing the interest + * + * This JSON string should be an array, each object has following keys: + * + * keyCode: [Required] represents an OS system dependent virtual key code identifying + * the unmodified value of the pressed key. + * ctrlKey: [optional] a Boolean that indicates if the control key was pressed (true) or not (false) when the event occurred. + * altKey: [optional] a Boolean that indicates if the alt key was pressed (true) or not (false) when the event occurred. + * shiftKey: [optional] a Boolean that indicates if the shift key was pressed (true) or not (false) when the event occurred. + * metaKey: [optional] (Mac Only) a Boolean that indicates if the Meta key was pressed (true) or not (false) when the event occurred. + * On Macintosh keyboards, this is the command key. To detect Windows key on Windows, please use keyCode instead. + * An example JSON string: + * + * [ + * { + * "keyCode": 48 + * }, + * { + * "keyCode": 123, + * "ctrlKey": true + * }, + * { + * "keyCode": 123, + * "ctrlKey": true, + * "metaKey": true + * } + * ] + * + */ +CSInterface.prototype.registerKeyEventsInterest = function(keyEventsInterest) +{ + return window.__adobe_cep__.registerKeyEventsInterest(keyEventsInterest); +}; + +/** + * Set the title of the extension window. + * This function works with modal and modeless extensions in all Adobe products, and panel extensions in Photoshop, InDesign, InCopy, Illustrator, Flash Pro and Dreamweaver. + * + * Since 6.1.0 + * + * @param title The window title. + */ +CSInterface.prototype.setWindowTitle = function(title) +{ + window.__adobe_cep__.invokeSync("setWindowTitle", title); +}; + +/** + * Get the title of the extension window. + * This function works with modal and modeless extensions in all Adobe products, and panel extensions in Photoshop, InDesign, InCopy, Illustrator, Flash Pro and Dreamweaver. + * + * Since 6.1.0 + * + * @return The window title. + */ +CSInterface.prototype.getWindowTitle = function() +{ + return window.__adobe_cep__.invokeSync("getWindowTitle", ""); +}; diff --git a/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)