From 1d2973c6a95f220342a91178dadc139396fe9105 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Sat, 24 Oct 2020 09:57:26 +0200 Subject: [PATCH] #663 - AfterEffects init commit --- pype/hosts/aftereffects/__init__.py | 74 ++++++++++++ .../websocket_server/hosts/aftereffects.py | 64 +++++++++++ .../stubs/aftereffects_server_stub.py | 108 ++++++++++++++++++ 3 files changed, 246 insertions(+) create mode 100644 pype/hosts/aftereffects/__init__.py create mode 100644 pype/modules/websocket_server/hosts/aftereffects.py create mode 100644 pype/modules/websocket_server/stubs/aftereffects_server_stub.py diff --git a/pype/hosts/aftereffects/__init__.py b/pype/hosts/aftereffects/__init__.py new file mode 100644 index 0000000000..184a9a59d6 --- /dev/null +++ b/pype/hosts/aftereffects/__init__.py @@ -0,0 +1,74 @@ +import os +import sys + +from avalon import api, io +from avalon.vendor import Qt +from pype import lib +import pyblish.api + + +def check_inventory(): + if not lib.any_outdated(): + return + + host = api.registered_host() + outdated_containers = [] + for container in host.ls(): + representation = container['representation'] + representation_doc = io.find_one( + { + "_id": io.ObjectId(representation), + "type": "representation" + }, + projection={"parent": True} + ) + if representation_doc and not lib.is_latest(representation_doc): + outdated_containers.append(container) + + # Warn about outdated containers. + print("Starting new QApplication..") + app = Qt.QtWidgets.QApplication(sys.argv) + + message_box = Qt.QtWidgets.QMessageBox() + message_box.setIcon(Qt.QtWidgets.QMessageBox.Warning) + msg = "There are outdated containers in the scene." + message_box.setText(msg) + message_box.exec_() + + # Garbage collect QApplication. + del app + + +def application_launch(): + check_inventory() + + +def install(): + print("Installing Pype config...") + + plugins_directory = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(__file__))), + "plugins", + "aftereffects" + ) + + pyblish.api.register_plugin_path( + os.path.join(plugins_directory, "publish") + ) + api.register_plugin_path( + api.Loader, os.path.join(plugins_directory, "load") + ) + api.register_plugin_path( + api.Creator, os.path.join(plugins_directory, "create") + ) + + pyblish.api.register_callback( + "instanceToggled", on_pyblish_instance_toggled + ) + + api.on("application.launched", application_launch) + + +def on_pyblish_instance_toggled(instance, old_value, new_value): + """Toggle layer visibility on instance toggles.""" + instance[0].Visible = new_value diff --git a/pype/modules/websocket_server/hosts/aftereffects.py b/pype/modules/websocket_server/hosts/aftereffects.py new file mode 100644 index 0000000000..14d2c04338 --- /dev/null +++ b/pype/modules/websocket_server/hosts/aftereffects.py @@ -0,0 +1,64 @@ +from pype.api import Logger +from wsrpc_aiohttp import WebSocketRoute +import functools + +import avalon.aftereffects as aftereffects + +log = Logger().get_logger("WebsocketServer") + + +class AfterEffects(WebSocketRoute): + """ + One route, mimicking external application (like Harmony, etc). + All functions could be called from client. + 'do_notify' function calls function on the client - mimicking + notification after long running job on the server or similar + """ + instance = None + + def init(self, **kwargs): + # Python __init__ must be return "self". + # This method might return anything. + log.debug("someone called AfterEffects route") + self.instance = self + return kwargs + + # server functions + async def ping(self): + log.debug("someone called AfterEffects route ping") + + # This method calls function on the client side + # client functions + + async def read(self): + log.debug("aftereffects.read client calls server server calls " + "aftereffects client") + return await self.socket.call('aftereffects.read') + + # panel routes for tools + async def creator_route(self): + self._tool_route("creator") + + async def workfiles_route(self): + self._tool_route("workfiles") + + async def loader_route(self): + self._tool_route("loader") + + async def publish_route(self): + self._tool_route("publish") + + async def sceneinventory_route(self): + self._tool_route("sceneinventory") + + async def projectmanager_route(self): + self._tool_route("projectmanager") + + def _tool_route(self, tool_name): + """The address accessed when clicking on the buttons.""" + partial_method = functools.partial(aftereffects.show, tool_name) + + aftereffects.execute_in_main_thread(partial_method) + + # Required return statement. + return "nothing" diff --git a/pype/modules/websocket_server/stubs/aftereffects_server_stub.py b/pype/modules/websocket_server/stubs/aftereffects_server_stub.py new file mode 100644 index 0000000000..9a296110f9 --- /dev/null +++ b/pype/modules/websocket_server/stubs/aftereffects_server_stub.py @@ -0,0 +1,108 @@ +from pype.modules.websocket_server import WebSocketServer +""" + Stub handling connection from server to client. + Used anywhere solution is calling client methods. +""" +import json +from collections import namedtuple + + +class AfterEffectsServerStub(): + """ + Stub for calling function on client (Photoshop js) side. + Expects that client is already connected (started when avalon menu + is opened). + 'self.websocketserver.call' is used as async wrapper + """ + + def __init__(self): + self.websocketserver = WebSocketServer.get_instance() + self.client = self.websocketserver.get_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: + res(string): - json representation + """ + try: + layers_data = json.loads(res) + except json.decoder.JSONDecodeError: + raise ValueError("Received broken JSON {}".format(res)) + ret = [] + # convert to namedtuple to use dot donation + if isinstance(layers_data, dict): # TODO refactore + layers_data = [layers_data] + for d in layers_data: + ret.append(namedtuple('Layer', d.keys())(*d.values())) + return ret