From 065266cf40bb541b05a8e0caa71baa6f2bcc45a9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 5 Mar 2021 12:03:58 +0100 Subject: [PATCH] PS - added highlight with icon for publishable instances Changed structure of metadata from {} to [] Added rename_layer method Switched to attr instead of namedtuple (same as in AE) --- .../stubs/photoshop_server_stub.py | 163 +++++++++++++++--- 1 file changed, 135 insertions(+), 28 deletions(-) diff --git a/pype/modules/websocket_server/stubs/photoshop_server_stub.py b/pype/modules/websocket_server/stubs/photoshop_server_stub.py index 9677fa61a8..79a486a20c 100644 --- a/pype/modules/websocket_server/stubs/photoshop_server_stub.py +++ b/pype/modules/websocket_server/stubs/photoshop_server_stub.py @@ -4,16 +4,37 @@ from pype.modules.websocket_server import WebSocketServer Used anywhere solution is calling client methods. """ import json -from collections import namedtuple +import attr -class PhotoshopServerStub(): +@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) + + +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 = WebSocketServer.get_instance() @@ -34,7 +55,7 @@ class PhotoshopServerStub(): """ Parses layer metadata from Headline field of active document Args: - layer: + Returns: Format of tuple: { 'id':'123', 'name': 'My Layer 1', 'type': 'GUIDE'|'FG'|'BG'|'OBJ' @@ -100,12 +145,26 @@ class PhotoshopServerStub(): 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: + layers : + Returns: """ all_layers = self.get_layers() ret = [] @@ -123,28 +182,30 @@ class PhotoshopServerStub(): def create_group(self, name): """ Create new group (eg. LayerSet) - Returns: + Returns: """ + enhanced_name = self.PUBLISH_ICON + name ret = self.websocketserver.call(self.client.call ('Photoshop.create_group', - name=name)) + name=enhanced_name)) # create group on PS is asynchronous, returns only id - layer = {"id": ret, "name": name, "group": True} - return namedtuple('Layer', layer.keys())(*layer.values()) + 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=name) + name=enhanced_name) ) res = self._to_records(res) - if res: - return res.pop() + rec = res.pop() + rec.name = rec.name.replace(self.PUBLISH_ICON, '') + return rec raise ValueError("No group record returned") def get_selected_layers(self): @@ -253,6 +314,23 @@ class PhotoshopServerStub(): 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") or \ + (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") != "avalon-core: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): @@ -264,11 +342,14 @@ class PhotoshopServerStub(): layer_name (str): Unique layer name to differentiate how many times same smart object was loaded """ + enhanced_name = self.LOADED_ICON + layer_name res = self.websocketserver.call(self.client.call ('Photoshop.import_smart_object', - path=path, name=layer_name)) - - return self._to_records(res).pop() + path=path, name=enhanced_name)) + 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): """ @@ -277,13 +358,14 @@ class PhotoshopServerStub(): same smart object was loaded Args: - layer (namedTuple): Layer("id":XX, "name":"YY"..). + layer (PSItem): path (str): File to import. """ + enhanced_name = self.LOADED_ICON + layer_name self.websocketserver.call(self.client.call ('Photoshop.replace_smart_object', layer_id=layer.id, - path=path, name=layer_name)) + path=path, name=enhanced_name)) def delete_layer(self, layer_id): """ @@ -295,6 +377,18 @@ class PhotoshopServerStub(): ('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 = {} @@ -313,19 +407,32 @@ class PhotoshopServerStub(): def _to_records(self, res): """ - Converts string json representation into list of named tuples for + Converts string json representation into list of PSItem for dot notation access to work. - Returns: - res(string): - json representation + 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 namedtuple to use dot donation - if isinstance(layers_data, dict): # TODO refactore + + # convert to AEItem to use dot donation + if isinstance(layers_data, dict): layers_data = [layers_data] for d in layers_data: - ret.append(namedtuple('Layer', d.keys())(*d.values())) + # currently implemented and expected fields + item = 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')) + + ret.append(item) return ret