From 68822216ca15a263130b71dc6711d8ec1605477b Mon Sep 17 00:00:00 2001 From: "petr.kalis" Date: Tue, 11 Aug 2020 10:23:54 +0200 Subject: [PATCH 01/92] Added client support --- .../websocket_server/websocket_server.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/pype/modules/websocket_server/websocket_server.py b/pype/modules/websocket_server/websocket_server.py index 56e71ea895..9d0d01d156 100644 --- a/pype/modules/websocket_server/websocket_server.py +++ b/pype/modules/websocket_server/websocket_server.py @@ -22,12 +22,16 @@ class WebSocketServer(): WIP """ + _instance = None def __init__(self): self.qaction = None self.failed_icon = None self._is_running = False default_port = 8099 + WebSocketServer._instance = self + self.client = None + self.handlers = {} try: self.presets = config.get_presets()["services"]["websocket_server"] @@ -76,8 +80,26 @@ class WebSocketServer(): module = importlib.import_module(module_name) cls = getattr(module, class_name) WebSocketAsync.add_route(class_name, cls) + self.handlers[class_name] = cls() # TODO refactor sys.path.pop() + def call(self, func): + log.debug("websocket.call {}".format(func)) + return self.websocket_thread.call_async(func) + + def task_finished(self, task): + print("task finished {}".format(task.result)) + print("client socket {}".format(self.client.client.socket)) + + def get_routes(self): + WebSocketAsync.get_routes() + + @staticmethod + def get_instance(): + if WebSocketServer._instance == None: + WebSocketServer() + return WebSocketServer._instance + def tray_start(self): self.websocket_thread.start() @@ -124,6 +146,7 @@ class WebsocketServerThread(threading.Thread): self.loop = None self.runner = None self.site = None + self.tasks = [] def run(self): self.is_running = True @@ -153,6 +176,20 @@ class WebsocketServerThread(threading.Thread): self.module.thread_stopped() log.info("Websocket server stopped") + def call_async(self, func): + # log.debug("call async") + # print("call aysnc") + # log.debug("my loop {}".format(self.loop)) + # task = self.loop.create_task(func) + # print("waitning") + # log.debug("waiting for task {}".format(func)) + # self.loop.run_until_complete(task) + # log.debug("returned value {}".format(task.result)) + # return task.result + task = self.loop.create_task(func) + task.add_done_callback(self.module.task_finished) + self.tasks.append(task) + async def start_server(self): """ Starts runner and TCPsite """ self.runner = web.AppRunner(self.module.app) @@ -169,6 +206,12 @@ class WebsocketServerThread(threading.Thread): periodically. """ while self.is_running: + while self.tasks: + task = self.tasks.pop(0) + log.debug("waiting for task {}".format(task)) + await task + log.debug("returned value {}".format(task.result)) + await asyncio.sleep(0.5) log.debug("Starting shutdown") From 41c101695238e3667c6392e155a5996dd335f85f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 18 Aug 2020 18:03:35 +0200 Subject: [PATCH 02/92] Working implementation of collect_instances\n extract_review, etract_image --- .../clients/photoshop_client.py | 102 ++++++++++++++++++ .../websocket_server/hosts/photoshop.py | 34 ++++++ .../websocket_server/websocket_server.py | 35 +++--- .../photoshop/publish/collect_instances.py | 23 ++-- .../photoshop/publish/extract_image.py | 43 +++++--- .../photoshop/publish/extract_review.py | 62 +++++++---- .../photoshop/publish/extract_save_scene.py | 5 +- 7 files changed, 240 insertions(+), 64 deletions(-) create mode 100644 pype/modules/websocket_server/clients/photoshop_client.py create mode 100644 pype/modules/websocket_server/hosts/photoshop.py diff --git a/pype/modules/websocket_server/clients/photoshop_client.py b/pype/modules/websocket_server/clients/photoshop_client.py new file mode 100644 index 0000000000..0090c10db2 --- /dev/null +++ b/pype/modules/websocket_server/clients/photoshop_client.py @@ -0,0 +1,102 @@ +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 PhotoshopClientStub(): + + def __init__(self): + self.websocketserver = WebSocketServer.get_instance() + self.client = self.websocketserver.get_client() + + def read(self, layer): + layers_data = {} + res = self.websocketserver.call(self.client.call('Photoshop.read')) + try: + layers_data = json.loads(res) + except json.decoder.JSONDecodeError: + pass + + return layers_data.get(str(layer.id)) + + def get_layers(self): + """ + Returns JSON document with all(?) layers in active document. + + :return: + Format of tuple: { 'id':'123', + 'name': 'My Layer 1', + 'type': 'GUIDE'|'FG'|'BG'|'OBJ' + 'visible': 'true'|'false' + """ + layers = {} + res = self.websocketserver.call(self.client.call + ('Photoshop.get_layers')) + print("get_layers:: {}".format(res)) + 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 + for d in layers_data: + ret.append(namedtuple('Layer', d.keys())(*d.values())) + + return ret + + def get_layers_in_layers(self, layers): + """ + Return all layers that belong to layers (might be groups). + :param layers: + :return: + """ + all_layers = self.get_layers() + print("get_layers_in_layers {}".format(layers)) + print("get_layers_in_layers len {}".format(len(layers))) + print("get_layers_in_layers type {}".format(type(layers))) + ret = [] + layer_ids = [lay.id for lay in layers] + layer_group_ids = [ll.groupId for ll in layers if ll.group] + for layer in all_layers: + if layer.groupId in layer_group_ids: # all from group + ret.append(layer) + if layer.id in layer_ids: + ret.append(layer) + + return ret + + + def select_layers(self, layers): + layer_ids = [layer.id for layer in layers] + + res = self.websocketserver.call(self.client.call + ('Photoshop.get_layers', + layers=layer_ids) + ) + + def get_active_document_name(self): + res = self.websocketserver.call(self.client.call + ('Photoshop.get_active_document_name')) + + return res + + def set_visible(self, layer_id, visibility): + print("set_visible {}, {}".format(layer_id, visibility)) + res = self.websocketserver.call(self.client.call + ('Photoshop.set_visible', + layer_id=layer_id, + visibility=visibility)) + + def saveAs(self, image_path, ext, as_copy): + res = self.websocketserver.call(self.client.call + ('Photoshop.saveAs', + image_path=image_path, + ext=ext, + as_copy=as_copy)) + + def close(self): + self.client.close() + diff --git a/pype/modules/websocket_server/hosts/photoshop.py b/pype/modules/websocket_server/hosts/photoshop.py new file mode 100644 index 0000000000..9092530e48 --- /dev/null +++ b/pype/modules/websocket_server/hosts/photoshop.py @@ -0,0 +1,34 @@ +import asyncio + +from pype.api import Logger +from wsrpc_aiohttp import WebSocketRoute + +log = Logger().get_logger("WebsocketServer") + + +class Photoshop(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 read(self): + log.debug("photoshop.read client calls server server calls Photo client") + return await self.socket.call('Photoshop.read') diff --git a/pype/modules/websocket_server/websocket_server.py b/pype/modules/websocket_server/websocket_server.py index 9d0d01d156..f9be7c88a9 100644 --- a/pype/modules/websocket_server/websocket_server.py +++ b/pype/modules/websocket_server/websocket_server.py @@ -80,19 +80,26 @@ class WebSocketServer(): module = importlib.import_module(module_name) cls = getattr(module, class_name) WebSocketAsync.add_route(class_name, cls) - self.handlers[class_name] = cls() # TODO refactor sys.path.pop() def call(self, func): log.debug("websocket.call {}".format(func)) - return self.websocket_thread.call_async(func) + future = asyncio.run_coroutine_threadsafe(func, + self.websocket_thread.loop) + result = future.result() + return result - def task_finished(self, task): - print("task finished {}".format(task.result)) - print("client socket {}".format(self.client.client.socket)) + def get_client(self): + """ + Return first connected client to WebSocket + TODO implement selection by Route + :return: client + """ + clients = WebSocketAsync.get_clients() + key = list(clients.keys())[0] + client = clients.get(key) - def get_routes(self): - WebSocketAsync.get_routes() + return client @staticmethod def get_instance(): @@ -176,20 +183,6 @@ class WebsocketServerThread(threading.Thread): self.module.thread_stopped() log.info("Websocket server stopped") - def call_async(self, func): - # log.debug("call async") - # print("call aysnc") - # log.debug("my loop {}".format(self.loop)) - # task = self.loop.create_task(func) - # print("waitning") - # log.debug("waiting for task {}".format(func)) - # self.loop.run_until_complete(task) - # log.debug("returned value {}".format(task.result)) - # return task.result - task = self.loop.create_task(func) - task.add_done_callback(self.module.task_finished) - self.tasks.append(task) - async def start_server(self): """ Starts runner and TCPsite """ self.runner = web.AppRunner(self.module.app) diff --git a/pype/plugins/photoshop/publish/collect_instances.py b/pype/plugins/photoshop/publish/collect_instances.py index 4937f2a1e4..cc7341f384 100644 --- a/pype/plugins/photoshop/publish/collect_instances.py +++ b/pype/plugins/photoshop/publish/collect_instances.py @@ -4,6 +4,7 @@ from avalon import photoshop import pyblish.api +from pype.modules.websocket_server.clients.photoshop_client import PhotoshopClientStub class CollectInstances(pyblish.api.ContextPlugin): """Gather instances by LayerSet and file metadata @@ -27,8 +28,13 @@ class CollectInstances(pyblish.api.ContextPlugin): # can be. pythoncom.CoInitialize() - for layer in photoshop.get_layers_in_document(): - layer_data = photoshop.read(layer) + from datetime import datetime + start = datetime.now() + # for timing + photoshop_client = PhotoshopClientStub() + layers = photoshop_client.get_layers() + for layer in layers: + layer_data = photoshop_client.read(layer) # Skip layers without metadata. if layer_data is None: @@ -38,18 +44,19 @@ class CollectInstances(pyblish.api.ContextPlugin): if "container" in layer_data["id"]: continue - child_layers = [*layer.Layers] - if not child_layers: - self.log.info("%s skipped, it was empty." % layer.Name) - 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 = 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 + instance.data["publish"] = layer.visible # Produce diagnostic message for any graphical # user interface interested in visualising it. diff --git a/pype/plugins/photoshop/publish/extract_image.py b/pype/plugins/photoshop/publish/extract_image.py index 6dfccdc4f2..0451308ef1 100644 --- a/pype/plugins/photoshop/publish/extract_image.py +++ b/pype/plugins/photoshop/publish/extract_image.py @@ -3,6 +3,8 @@ import os import pype.api from avalon import photoshop +from pype.modules.websocket_server.clients.photoshop_client import \ + PhotoshopClientStub class ExtractImage(pype.api.Extractor): """Produce a flattened image file from instance @@ -20,36 +22,49 @@ class ExtractImage(pype.api.Extractor): staging_dir = self.staging_dir(instance) self.log.info("Outputting image to {}".format(staging_dir)) + layers = [] + for image_instance in instance.context: + if image_instance.data["family"] != "image": + continue + layers.append(image_instance[0]) + # Perform extraction + photoshop_client = PhotoshopClientStub() files = {} with photoshop.maintained_selection(): self.log.info("Extracting %s" % str(list(instance))) with photoshop.maintained_visibility(): # Hide all other layers. - extract_ids = [ - x.id for x in photoshop.get_layers_in_layers([instance[0]]) - ] - for layer in photoshop.get_layers_in_document(): - if layer.id not in extract_ids: - layer.Visible = False + extract_ids = set([ll.id for ll in photoshop_client. + get_layers_in_layers(layers)]) - save_options = {} + for layer in photoshop_client.get_layers(): + # limit unnecessary calls to client + if layer.visible and layer.id not in extract_ids: + photoshop_client.set_visible(layer.id, + False) + if not layer.visible and layer.id in extract_ids: + photoshop_client.set_visible(layer.id, + True) + + save_options = [] if "png" in self.formats: - save_options["png"] = photoshop.com_objects.PNGSaveOptions() + save_options.append('png') if "jpg" in self.formats: - save_options["jpg"] = photoshop.com_objects.JPEGSaveOptions() + save_options.append('jpg') file_basename = os.path.splitext( - photoshop.app().ActiveDocument.Name + photoshop_client.get_active_document_name() )[0] - for extension, save_option in save_options.items(): + for extension in save_options: _filename = "{}.{}".format(file_basename, extension) files[extension] = _filename full_filename = os.path.join(staging_dir, _filename) - photoshop.app().ActiveDocument.SaveAs( - full_filename, save_option, True - ) + photoshop_client.saveAs(full_filename, + extension, + True) + representations = [] for extension, filename in files.items(): diff --git a/pype/plugins/photoshop/publish/extract_review.py b/pype/plugins/photoshop/publish/extract_review.py index 078ee53899..1c3aeaffb5 100644 --- a/pype/plugins/photoshop/publish/extract_review.py +++ b/pype/plugins/photoshop/publish/extract_review.py @@ -4,6 +4,10 @@ import pype.api import pype.lib from avalon import photoshop +from datetime import datetime +from pype.modules.websocket_server.clients.photoshop_client import \ + PhotoshopClientStub + class ExtractReview(pype.api.Extractor): """Produce a flattened image file from all instances.""" @@ -13,10 +17,12 @@ class ExtractReview(pype.api.Extractor): families = ["review"] def process(self, instance): - + start = datetime.now() staging_dir = self.staging_dir(instance) self.log.info("Outputting image to {}".format(staging_dir)) + photoshop_client = PhotoshopClientStub() + layers = [] for image_instance in instance.context: if image_instance.data["family"] != "image": @@ -25,26 +31,39 @@ class ExtractReview(pype.api.Extractor): # Perform extraction output_image = "{}.jpg".format( - os.path.splitext(photoshop.app().ActiveDocument.Name)[0] + os.path.splitext(photoshop_client.get_active_document_name())[0] ) output_image_path = os.path.join(staging_dir, output_image) + self.log.info( + "first part took {}".format(datetime.now() - start)) with photoshop.maintained_visibility(): # Hide all other layers. - extract_ids = [ - x.id for x in photoshop.get_layers_in_layers(layers) - ] - for layer in photoshop.get_layers_in_document(): - if layer.id in extract_ids: - layer.Visible = True - else: - layer.Visible = False + start = datetime.now() + extract_ids = set([ll.id for ll in photoshop_client. + get_layers_in_layers(layers)]) + self.log.info("extract_ids {}".format(extract_ids)) - photoshop.app().ActiveDocument.SaveAs( - output_image_path, - photoshop.com_objects.JPEGSaveOptions(), - True - ) + for layer in photoshop_client.get_layers(): + # limit unnecessary calls to client + if layer.visible and layer.id not in extract_ids: + photoshop_client.set_visible(layer.id, + False) + if not layer.visible and layer.id in extract_ids: + photoshop_client.set_visible(layer.id, + True) + self.log.info( + "get_layers_in_layers took {}".format(datetime.now() - start)) + start = datetime.now() + + self.log.info("output_image_path {}".format(output_image_path)) + photoshop_client.saveAs(output_image_path, + 'jpg', + True) + self.log.info( + "saveAs {} took {}".format('JPG', datetime.now() - start)) + + start = datetime.now() ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") instance.data["representations"].append({ @@ -65,7 +84,8 @@ class ExtractReview(pype.api.Extractor): thumbnail_path ] output = pype.lib._subprocess(args) - + self.log.info( + "thumbnail {} took {}".format('JPG', datetime.now() - start)) self.log.debug(output) instance.data["representations"].append({ @@ -75,7 +95,7 @@ class ExtractReview(pype.api.Extractor): "stagingDir": staging_dir, "tags": ["thumbnail"] }) - + start = datetime.now() # Generate mov. mov_path = os.path.join(staging_dir, "review.mov") args = [ @@ -86,9 +106,10 @@ class ExtractReview(pype.api.Extractor): mov_path ] output = pype.lib._subprocess(args) - + self.log.info( + "review {} took {}".format('JPG', datetime.now() - start)) self.log.debug(output) - + start = datetime.now() instance.data["representations"].append({ "name": "mov", "ext": "mov", @@ -105,5 +126,6 @@ class ExtractReview(pype.api.Extractor): instance.data["frameStart"] = 1 instance.data["frameEnd"] = 1 instance.data["fps"] = 25 - + self.log.info( + "end {} took {}".format('JPG', datetime.now() - start)) self.log.info(f"Extracted {instance} to {staging_dir}") diff --git a/pype/plugins/photoshop/publish/extract_save_scene.py b/pype/plugins/photoshop/publish/extract_save_scene.py index b3d4f0e447..e2068501db 100644 --- a/pype/plugins/photoshop/publish/extract_save_scene.py +++ b/pype/plugins/photoshop/publish/extract_save_scene.py @@ -1,7 +1,7 @@ import pype.api from avalon import photoshop - +from datetime import datetime class ExtractSaveScene(pype.api.Extractor): """Save scene before extraction.""" @@ -11,4 +11,7 @@ class ExtractSaveScene(pype.api.Extractor): families = ["workfile"] def process(self, instance): + start = datetime.now() photoshop.app().ActiveDocument.Save() + self.log.info( + "ExtractSaveScene took {}".format(datetime.now() - start)) From a9f146e2fca896a8558c3c8d65d6df1a0f3af3ec Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 19 Aug 2020 12:54:42 +0200 Subject: [PATCH 03/92] Fixed fullName, implemented imprint --- .../clients/photoshop_client.py | 73 ++++++++++++++++--- .../photoshop/publish/collect_current_file.py | 6 +- .../photoshop/publish/collect_instances.py | 6 ++ .../photoshop/publish/extract_save_scene.py | 6 +- .../photoshop/publish/increment_workfile.py | 5 +- .../publish/validate_instance_asset.py | 11 ++- .../photoshop/publish/validate_naming.py | 9 ++- 7 files changed, 96 insertions(+), 20 deletions(-) diff --git a/pype/modules/websocket_server/clients/photoshop_client.py b/pype/modules/websocket_server/clients/photoshop_client.py index 0090c10db2..6d3615e1a4 100644 --- a/pype/modules/websocket_server/clients/photoshop_client.py +++ b/pype/modules/websocket_server/clients/photoshop_client.py @@ -13,15 +13,34 @@ class PhotoshopClientStub(): self.client = self.websocketserver.get_client() def read(self, layer): - layers_data = {} - res = self.websocketserver.call(self.client.call('Photoshop.read')) - try: - layers_data = json.loads(res) - except json.decoder.JSONDecodeError: - pass + layers_data = self._get_layers_metadata() return layers_data.get(str(layer.id)) + def imprint(self, layer, data): + layers_data = self._get_layers_metadata() + # json.dumps writes integer values in a dictionary to string, so + # anticipating it here. + if str(layer.id) in layers_data: + layers_data[str(layer.id)].update(data) + else: + layers_data[str(layer.id)] = data + + # Ensure only valid ids are stored. + layer_ids = [layer.id for layer in self.get_layers()] + cleaned_data = {} + + for id in layers_data: + if int(id) in layer_ids: + cleaned_data[id] = layers_data[id] + + payload = json.dumps(cleaned_data, indent=4) + + res = self.websocketserver.call(self.client.call + ('Photoshop.imprint', + payload=payload) + ) + def get_layers(self): """ Returns JSON document with all(?) layers in active document. @@ -77,18 +96,34 @@ class PhotoshopClientStub(): layers=layer_ids) ) + def get_active_document_full_name(self): + """ + Returns full name with path of active document via ws call + :return: 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 + :return: file name + """ res = self.websocketserver.call(self.client.call ('Photoshop.get_active_document_name')) return res - def set_visible(self, layer_id, visibility): - print("set_visible {}, {}".format(layer_id, visibility)) + def save(self): + """ + Saves active document + :return: None + """ res = self.websocketserver.call(self.client.call - ('Photoshop.set_visible', - layer_id=layer_id, - visibility=visibility)) + ('Photoshop.save')) + def saveAs(self, image_path, ext, as_copy): res = self.websocketserver.call(self.client.call @@ -97,6 +132,22 @@ class PhotoshopClientStub(): ext=ext, as_copy=as_copy)) + def set_visible(self, layer_id, visibility): + print("set_visible {}, {}".format(layer_id, visibility)) + res = self.websocketserver.call(self.client.call + ('Photoshop.set_visible', + layer_id=layer_id, + visibility=visibility)) + + def _get_layers_metadata(self): + layers_data = {} + res = self.websocketserver.call(self.client.call('Photoshop.read')) + try: + layers_data = json.loads(res) + except json.decoder.JSONDecodeError: + pass + return layers_data + def close(self): self.client.close() diff --git a/pype/plugins/photoshop/publish/collect_current_file.py b/pype/plugins/photoshop/publish/collect_current_file.py index 4308588559..bb81718bcc 100644 --- a/pype/plugins/photoshop/publish/collect_current_file.py +++ b/pype/plugins/photoshop/publish/collect_current_file.py @@ -3,6 +3,9 @@ import os import pyblish.api from avalon import photoshop +from pype.modules.websocket_server.clients.photoshop_client import \ + PhotoshopClientStub + class CollectCurrentFile(pyblish.api.ContextPlugin): """Inject the current working file into context""" @@ -12,6 +15,7 @@ class CollectCurrentFile(pyblish.api.ContextPlugin): hosts = ["photoshop"] def process(self, context): + photoshop_client = PhotoshopClientStub() context.data["currentFile"] = os.path.normpath( - photoshop.app().ActiveDocument.FullName + photoshop_client.get_active_document_full_name() ).replace("\\", "/") diff --git a/pype/plugins/photoshop/publish/collect_instances.py b/pype/plugins/photoshop/publish/collect_instances.py index cc7341f384..d94adde00b 100644 --- a/pype/plugins/photoshop/publish/collect_instances.py +++ b/pype/plugins/photoshop/publish/collect_instances.py @@ -33,8 +33,14 @@ class CollectInstances(pyblish.api.ContextPlugin): # for timing photoshop_client = PhotoshopClientStub() layers = photoshop_client.get_layers() + for layer in layers: layer_data = photoshop_client.read(layer) + self.log.info("layer_data {}".format(layer_data)) + + photoshop_client.imprint(layer, layer_data) + new_layer_data = photoshop_client.read(layer) + assert layer_data == new_layer_data # Skip layers without metadata. if layer_data is None: diff --git a/pype/plugins/photoshop/publish/extract_save_scene.py b/pype/plugins/photoshop/publish/extract_save_scene.py index e2068501db..ea7bdda9af 100644 --- a/pype/plugins/photoshop/publish/extract_save_scene.py +++ b/pype/plugins/photoshop/publish/extract_save_scene.py @@ -1,6 +1,9 @@ import pype.api from avalon import photoshop +from pype.modules.websocket_server.clients.photoshop_client import \ + PhotoshopClientStub + from datetime import datetime class ExtractSaveScene(pype.api.Extractor): """Save scene before extraction.""" @@ -11,7 +14,8 @@ class ExtractSaveScene(pype.api.Extractor): families = ["workfile"] def process(self, instance): + photoshop_client = PhotoshopClientStub() start = datetime.now() - photoshop.app().ActiveDocument.Save() + photoshop_client.save() self.log.info( "ExtractSaveScene took {}".format(datetime.now() - start)) diff --git a/pype/plugins/photoshop/publish/increment_workfile.py b/pype/plugins/photoshop/publish/increment_workfile.py index ba9ab8606a..0ae7e9772f 100644 --- a/pype/plugins/photoshop/publish/increment_workfile.py +++ b/pype/plugins/photoshop/publish/increment_workfile.py @@ -3,6 +3,8 @@ from pype.action import get_errored_plugins_from_data from pype.lib import version_up from avalon import photoshop +from pype.modules.websocket_server.clients.photoshop_client import \ + PhotoshopClientStub class IncrementWorkfile(pyblish.api.InstancePlugin): """Increment the current workfile. @@ -24,6 +26,7 @@ class IncrementWorkfile(pyblish.api.InstancePlugin): ) scene_path = version_up(instance.context.data["currentFile"]) - photoshop.app().ActiveDocument.SaveAs(scene_path) + photoshop_client = PhotoshopClientStub() + photoshop_client.saveAs(scene_path, 'psd', True) self.log.info("Incremented workfile to: {}".format(scene_path)) diff --git a/pype/plugins/photoshop/publish/validate_instance_asset.py b/pype/plugins/photoshop/publish/validate_instance_asset.py index ab1d02269f..6a0a408878 100644 --- a/pype/plugins/photoshop/publish/validate_instance_asset.py +++ b/pype/plugins/photoshop/publish/validate_instance_asset.py @@ -4,6 +4,8 @@ import pyblish.api import pype.api from avalon import photoshop +from pype.modules.websocket_server.clients.photoshop_client import \ + PhotoshopClientStub class ValidateInstanceAssetRepair(pyblish.api.Action): """Repair the instance asset.""" @@ -23,11 +25,14 @@ class ValidateInstanceAssetRepair(pyblish.api.Action): # Apply pyblish.logic to get the instances for the plug-in instances = pyblish.api.instances_by_plugin(failed, plugin) - + photoshop_client = PhotoshopClientStub() for instance in instances: - data = photoshop.read(instance[0]) + self.log.info("validate_instance_asset instance[0] {}".format(instance[0])) + self.log.info("validate_instance_asset instance {}".format(instance)) + data = photoshop_client.read(instance[0]) + data["asset"] = os.environ["AVALON_ASSET"] - photoshop.imprint(instance[0], data) + photoshop_client.imprint(instance[0], data) class ValidateInstanceAsset(pyblish.api.InstancePlugin): diff --git a/pype/plugins/photoshop/publish/validate_naming.py b/pype/plugins/photoshop/publish/validate_naming.py index 51e00da352..7734a0e5a0 100644 --- a/pype/plugins/photoshop/publish/validate_naming.py +++ b/pype/plugins/photoshop/publish/validate_naming.py @@ -2,6 +2,8 @@ import pyblish.api import pype.api from avalon import photoshop +from pype.modules.websocket_server.clients.photoshop_client import \ + PhotoshopClientStub class ValidateNamingRepair(pyblish.api.Action): """Repair the instance asset.""" @@ -21,13 +23,14 @@ class ValidateNamingRepair(pyblish.api.Action): # Apply pyblish.logic to get the instances for the plug-in instances = pyblish.api.instances_by_plugin(failed, plugin) - + photoshop_client = PhotoshopClientStub() for instance in instances: + self.log.info("validate_naming instance {}".format(instance)) name = instance.data["name"].replace(" ", "_") instance[0].Name = name - data = photoshop.read(instance[0]) + data = photoshop_client.read(instance[0]) data["subset"] = "image" + name - photoshop.imprint(instance[0], data) + photoshop_client.imprint(instance[0], data) return True From c161a637433bf8b7b0b211dced4c4a99c919ad47 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 19 Aug 2020 18:05:03 +0100 Subject: [PATCH 04/92] Fix alembic settings being reset when updating reference. --- pype/hosts/maya/plugin.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pype/hosts/maya/plugin.py b/pype/hosts/maya/plugin.py index ed244d56df..3b002eed10 100644 --- a/pype/hosts/maya/plugin.py +++ b/pype/hosts/maya/plugin.py @@ -174,6 +174,18 @@ class ReferenceLoader(api.Loader): assert os.path.exists(path), "%s does not exist." % path + # Need to save alembic settings and reapply, cause referencing resets + # them to incoming data. + alembic_attrs = ["speed", "offset", "cycleType"] + alembic_data = {} + if representation["name"] == "abc": + alembic_node = cmds.ls( + cmds.sets(node, query=True), type="AlembicNode" + )[0] + for attr in alembic_attrs: + node_attr = "{}.{}".format(alembic_node, attr) + alembic_data[attr] = cmds.getAttr(node_attr) + try: content = cmds.file(path, loadReference=reference_node, @@ -195,6 +207,21 @@ class ReferenceLoader(api.Loader): self.log.warning("Ignoring file read error:\n%s", exc) + # Reapply alembic settings. + if representation["name"] == "abc": + alembic_node = None + for member in cmds.sets(node, query=True): + shapes = cmds.listRelatives(member, shapes=True) + if shapes: + nodes = cmds.listConnections(shapes[0], type="AlembicNode") + if nodes: + alembic_node = nodes[0] + break + + for attr in alembic_attrs: + value = alembic_data[attr] + cmds.setAttr("{}.{}".format(alembic_node, attr), value) + # Fix PLN-40 for older containers created with Avalon that had the # `.verticesOnlySet` set to True. if cmds.getAttr("{}.verticesOnlySet".format(node)): From fe3cfc24422580c99f837c6bf02de029e80d54a9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 19 Aug 2020 20:50:14 +0200 Subject: [PATCH 05/92] Added group_selected_layers, get_selected_layers, import_smart_object, replace_smart_object Fixed imprint for performance --- .../clients/photoshop_client.py | 157 ++++++++++++++---- .../websocket_server/websocket_server.py | 6 +- pype/plugins/photoshop/load/load_image.py | 9 +- .../photoshop/publish/collect_instances.py | 6 +- 4 files changed, 138 insertions(+), 40 deletions(-) diff --git a/pype/modules/websocket_server/clients/photoshop_client.py b/pype/modules/websocket_server/clients/photoshop_client.py index 6d3615e1a4..eea297954f 100644 --- a/pype/modules/websocket_server/clients/photoshop_client.py +++ b/pype/modules/websocket_server/clients/photoshop_client.py @@ -6,18 +6,38 @@ from pype.modules.websocket_server import WebSocketServer import json from collections import namedtuple + class PhotoshopClientStub(): + """ + Stub for calling function on client (Photoshop js) side. + Expects that client is already connected (started when avalon menu + is opened). + """ def __init__(self): self.websocketserver = WebSocketServer.get_instance() self.client = self.websocketserver.get_client() def read(self, layer): + """ + Parses layer metadata from Headline field of active document + :param layer: + :return: + """ layers_data = self._get_layers_metadata() return layers_data.get(str(layer.id)) - def imprint(self, layer, data): + def imprint(self, layer, data, all_layers=None): + """ + Save layer metadata to Headline field of active document + :param layer: Layer("id": XXX, "name":'YYY') + :param data: json representation for single layer + :param all_layers: - for performance, could be + injected for usage in loop, if not, single call will be + triggered + :return: None + """ layers_data = self._get_layers_metadata() # json.dumps writes integer values in a dictionary to string, so # anticipating it here. @@ -27,7 +47,9 @@ class PhotoshopClientStub(): layers_data[str(layer.id)] = data # Ensure only valid ids are stored. - layer_ids = [layer.id for layer in self.get_layers()] + if not all_layers: + all_layers = self.get_layers() + layer_ids = [layer.id for layer in all_layers] cleaned_data = {} for id in layers_data: @@ -36,10 +58,9 @@ class PhotoshopClientStub(): payload = json.dumps(cleaned_data, indent=4) - res = self.websocketserver.call(self.client.call - ('Photoshop.imprint', - payload=payload) - ) + self.websocketserver.call(self.client.call + ('Photoshop.imprint', payload=payload) + ) def get_layers(self): """ @@ -51,20 +72,10 @@ class PhotoshopClientStub(): 'type': 'GUIDE'|'FG'|'BG'|'OBJ' 'visible': 'true'|'false' """ - layers = {} res = self.websocketserver.call(self.client.call ('Photoshop.get_layers')) - print("get_layers:: {}".format(res)) - 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 - for d in layers_data: - ret.append(namedtuple('Layer', d.keys())(*d.values())) - return ret + return self._to_records(res) def get_layers_in_layers(self, layers): """ @@ -87,14 +98,35 @@ class PhotoshopClientStub(): return ret + def group_selected_layers(self): + """ + Group selected layers into new layer + :return: + """ + self.websocketserver.call(self.client.call + ('Photoshop.group_selected_layers')) + + def get_selected_layers(self): + """ + Get a list of actually selected layers + :return: + """ + res = self.websocketserver.call(self.client.call + ('Photoshop.get_selected_layers')) + return self._to_records(res) def select_layers(self, layers): + """ + Selecte specified layers in Photoshop + :param layers: + :return: None + """ layer_ids = [layer.id for layer in layers] - res = self.websocketserver.call(self.client.call - ('Photoshop.get_layers', - layers=layer_ids) - ) + self.websocketserver.call(self.client.call + ('Photoshop.get_layers', + layers=layer_ids) + ) def get_active_document_full_name(self): """ @@ -121,25 +153,41 @@ class PhotoshopClientStub(): Saves active document :return: None """ - res = self.websocketserver.call(self.client.call - ('Photoshop.save')) - + self.websocketserver.call(self.client.call + ('Photoshop.save')) def saveAs(self, image_path, ext, as_copy): - res = self.websocketserver.call(self.client.call - ('Photoshop.saveAs', - image_path=image_path, - ext=ext, - as_copy=as_copy)) + """ + Saves active document to psd (copy) or png or jpg + :param image_path: full local path + :param ext: + :param as_copy: + :return: 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): - print("set_visible {}, {}".format(layer_id, visibility)) - res = self.websocketserver.call(self.client.call - ('Photoshop.set_visible', - layer_id=layer_id, - visibility=visibility)) + """ + Set layer with 'layer_id' to 'visibility' + :param layer_id: + :param visibility: + :return: 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) + :return: - json documents + """ layers_data = {} res = self.websocketserver.call(self.client.call('Photoshop.read')) try: @@ -148,6 +196,47 @@ class PhotoshopClientStub(): pass return layers_data + def import_smart_object(self, path): + """ + Import the file at `path` as a smart object to active document. + + Args: + path (str): File path to import. + """ + + def replace_smart_object(self, layer, path): + """ + Replace the smart object `layer` with file at `path` + + Args: + layer (namedTuple): Layer("id":XX, "name":"YY"..). + path (str): File to import. + """ + self.websocketserver.call(self.client.call + ('Photoshop.replace_smart_object', + layer=layer, + path=path)) + def close(self): self.client.close() + def _to_records(self, res): + """ + Converts string json representation into list of named tuples for + dot notation access to work. + :return: + :param res: - 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 + for d in layers_data: + ret.append(namedtuple('Layer', d.keys())(*d.values())) + return ret + + + + diff --git a/pype/modules/websocket_server/websocket_server.py b/pype/modules/websocket_server/websocket_server.py index f9be7c88a9..02fde4d56a 100644 --- a/pype/modules/websocket_server/websocket_server.py +++ b/pype/modules/websocket_server/websocket_server.py @@ -96,8 +96,10 @@ class WebSocketServer(): :return: client """ clients = WebSocketAsync.get_clients() - key = list(clients.keys())[0] - client = clients.get(key) + client = None + if len(clients) > 0: + key = list(clients.keys())[0] + client = clients.get(key) return client diff --git a/pype/plugins/photoshop/load/load_image.py b/pype/plugins/photoshop/load/load_image.py index 18efe750d5..0e437b15ba 100644 --- a/pype/plugins/photoshop/load/load_image.py +++ b/pype/plugins/photoshop/load/load_image.py @@ -1,5 +1,10 @@ from avalon import api, photoshop +from pype.modules.websocket_server.clients.photoshop_client \ + import PhotoshopClientStub + +photoshopClient = PhotoshopClientStub() + class ImageLoader(api.Loader): """Load images @@ -28,11 +33,11 @@ class ImageLoader(api.Loader): layer = container.pop("layer") with photoshop.maintained_selection(): - photoshop.replace_smart_object( + photoshopClient.replace_smart_object( layer, api.get_representation_path(representation) ) - photoshop.imprint( + photoshopClient.imprint( layer, {"representation": str(representation["_id"])} ) diff --git a/pype/plugins/photoshop/publish/collect_instances.py b/pype/plugins/photoshop/publish/collect_instances.py index d94adde00b..f2d1c141fd 100644 --- a/pype/plugins/photoshop/publish/collect_instances.py +++ b/pype/plugins/photoshop/publish/collect_instances.py @@ -4,7 +4,9 @@ from avalon import photoshop import pyblish.api -from pype.modules.websocket_server.clients.photoshop_client import PhotoshopClientStub +from pype.modules.websocket_server.clients.photoshop_client \ + import PhotoshopClientStub + class CollectInstances(pyblish.api.ContextPlugin): """Gather instances by LayerSet and file metadata @@ -38,7 +40,7 @@ class CollectInstances(pyblish.api.ContextPlugin): layer_data = photoshop_client.read(layer) self.log.info("layer_data {}".format(layer_data)) - photoshop_client.imprint(layer, layer_data) + photoshop_client.imprint(layer, layer_data, layers) new_layer_data = photoshop_client.read(layer) assert layer_data == new_layer_data From 180035731b329fe617460389bebdc995e79260ab Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 19 Aug 2020 21:10:51 +0200 Subject: [PATCH 06/92] Hound --- .../websocket_server/hosts/photoshop.py | 5 ++-- .../websocket_server/websocket_server.py | 2 +- pype/plugins/photoshop/load/load_image.py | 2 +- .../photoshop/publish/collect_current_file.py | 3 +- .../photoshop/publish/collect_instances.py | 7 +---- .../photoshop/publish/extract_image.py | 4 +-- .../photoshop/publish/extract_review.py | 28 ++----------------- .../photoshop/publish/extract_save_scene.py | 7 ++--- .../photoshop/publish/increment_workfile.py | 4 +-- .../publish/validate_instance_asset.py | 2 -- .../photoshop/publish/validate_naming.py | 3 +- 11 files changed, 17 insertions(+), 50 deletions(-) diff --git a/pype/modules/websocket_server/hosts/photoshop.py b/pype/modules/websocket_server/hosts/photoshop.py index 9092530e48..c63f66865e 100644 --- a/pype/modules/websocket_server/hosts/photoshop.py +++ b/pype/modules/websocket_server/hosts/photoshop.py @@ -1,5 +1,3 @@ -import asyncio - from pype.api import Logger from wsrpc_aiohttp import WebSocketRoute @@ -30,5 +28,6 @@ class Photoshop(WebSocketRoute): # client functions async def read(self): - log.debug("photoshop.read client calls server server calls Photo client") + log.debug("photoshop.read client calls server server calls " + "Photo client") return await self.socket.call('Photoshop.read') diff --git a/pype/modules/websocket_server/websocket_server.py b/pype/modules/websocket_server/websocket_server.py index 02fde4d56a..6b730d4eb3 100644 --- a/pype/modules/websocket_server/websocket_server.py +++ b/pype/modules/websocket_server/websocket_server.py @@ -105,7 +105,7 @@ class WebSocketServer(): @staticmethod def get_instance(): - if WebSocketServer._instance == None: + if WebSocketServer._instance is None: WebSocketServer() return WebSocketServer._instance diff --git a/pype/plugins/photoshop/load/load_image.py b/pype/plugins/photoshop/load/load_image.py index 0e437b15ba..a24280553c 100644 --- a/pype/plugins/photoshop/load/load_image.py +++ b/pype/plugins/photoshop/load/load_image.py @@ -1,7 +1,7 @@ from avalon import api, photoshop from pype.modules.websocket_server.clients.photoshop_client \ - import PhotoshopClientStub + import PhotoshopClientStub photoshopClient = PhotoshopClientStub() diff --git a/pype/plugins/photoshop/publish/collect_current_file.py b/pype/plugins/photoshop/publish/collect_current_file.py index bb81718bcc..604ce97f89 100644 --- a/pype/plugins/photoshop/publish/collect_current_file.py +++ b/pype/plugins/photoshop/publish/collect_current_file.py @@ -1,10 +1,9 @@ import os import pyblish.api -from avalon import photoshop from pype.modules.websocket_server.clients.photoshop_client import \ - PhotoshopClientStub + PhotoshopClientStub class CollectCurrentFile(pyblish.api.ContextPlugin): diff --git a/pype/plugins/photoshop/publish/collect_instances.py b/pype/plugins/photoshop/publish/collect_instances.py index f2d1c141fd..82a0c35311 100644 --- a/pype/plugins/photoshop/publish/collect_instances.py +++ b/pype/plugins/photoshop/publish/collect_instances.py @@ -1,11 +1,9 @@ import pythoncom -from avalon import photoshop - import pyblish.api from pype.modules.websocket_server.clients.photoshop_client \ - import PhotoshopClientStub + import PhotoshopClientStub class CollectInstances(pyblish.api.ContextPlugin): @@ -30,9 +28,6 @@ class CollectInstances(pyblish.api.ContextPlugin): # can be. pythoncom.CoInitialize() - from datetime import datetime - start = datetime.now() - # for timing photoshop_client = PhotoshopClientStub() layers = photoshop_client.get_layers() diff --git a/pype/plugins/photoshop/publish/extract_image.py b/pype/plugins/photoshop/publish/extract_image.py index 0451308ef1..4cbac38ce7 100644 --- a/pype/plugins/photoshop/publish/extract_image.py +++ b/pype/plugins/photoshop/publish/extract_image.py @@ -4,7 +4,8 @@ import pype.api from avalon import photoshop from pype.modules.websocket_server.clients.photoshop_client import \ - PhotoshopClientStub + PhotoshopClientStub + class ExtractImage(pype.api.Extractor): """Produce a flattened image file from instance @@ -65,7 +66,6 @@ class ExtractImage(pype.api.Extractor): extension, True) - representations = [] for extension, filename in files.items(): representations.append({ diff --git a/pype/plugins/photoshop/publish/extract_review.py b/pype/plugins/photoshop/publish/extract_review.py index 1c3aeaffb5..1e8d726720 100644 --- a/pype/plugins/photoshop/publish/extract_review.py +++ b/pype/plugins/photoshop/publish/extract_review.py @@ -4,9 +4,8 @@ import pype.api import pype.lib from avalon import photoshop -from datetime import datetime from pype.modules.websocket_server.clients.photoshop_client import \ - PhotoshopClientStub + PhotoshopClientStub class ExtractReview(pype.api.Extractor): @@ -17,7 +16,6 @@ class ExtractReview(pype.api.Extractor): families = ["review"] def process(self, instance): - start = datetime.now() staging_dir = self.staging_dir(instance) self.log.info("Outputting image to {}".format(staging_dir)) @@ -34,14 +32,10 @@ class ExtractReview(pype.api.Extractor): os.path.splitext(photoshop_client.get_active_document_name())[0] ) output_image_path = os.path.join(staging_dir, output_image) - self.log.info( - "first part took {}".format(datetime.now() - start)) with photoshop.maintained_visibility(): # Hide all other layers. - start = datetime.now() extract_ids = set([ll.id for ll in photoshop_client. - get_layers_in_layers(layers)]) - self.log.info("extract_ids {}".format(extract_ids)) + get_layers_in_layers(layers)]) for layer in photoshop_client.get_layers(): # limit unnecessary calls to client @@ -51,19 +45,11 @@ class ExtractReview(pype.api.Extractor): if not layer.visible and layer.id in extract_ids: photoshop_client.set_visible(layer.id, True) - self.log.info( - "get_layers_in_layers took {}".format(datetime.now() - start)) - start = datetime.now() - - self.log.info("output_image_path {}".format(output_image_path)) photoshop_client.saveAs(output_image_path, 'jpg', True) - self.log.info( - "saveAs {} took {}".format('JPG', datetime.now() - start)) - start = datetime.now() ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") instance.data["representations"].append({ @@ -84,9 +70,6 @@ class ExtractReview(pype.api.Extractor): thumbnail_path ] output = pype.lib._subprocess(args) - self.log.info( - "thumbnail {} took {}".format('JPG', datetime.now() - start)) - self.log.debug(output) instance.data["representations"].append({ "name": "thumbnail", @@ -95,7 +78,6 @@ class ExtractReview(pype.api.Extractor): "stagingDir": staging_dir, "tags": ["thumbnail"] }) - start = datetime.now() # Generate mov. mov_path = os.path.join(staging_dir, "review.mov") args = [ @@ -106,10 +88,7 @@ class ExtractReview(pype.api.Extractor): mov_path ] output = pype.lib._subprocess(args) - self.log.info( - "review {} took {}".format('JPG', datetime.now() - start)) self.log.debug(output) - start = datetime.now() instance.data["representations"].append({ "name": "mov", "ext": "mov", @@ -126,6 +105,5 @@ class ExtractReview(pype.api.Extractor): instance.data["frameStart"] = 1 instance.data["frameEnd"] = 1 instance.data["fps"] = 25 - self.log.info( - "end {} took {}".format('JPG', datetime.now() - start)) + self.log.info(f"Extracted {instance} to {staging_dir}") diff --git a/pype/plugins/photoshop/publish/extract_save_scene.py b/pype/plugins/photoshop/publish/extract_save_scene.py index ea7bdda9af..3357a05f24 100644 --- a/pype/plugins/photoshop/publish/extract_save_scene.py +++ b/pype/plugins/photoshop/publish/extract_save_scene.py @@ -2,9 +2,9 @@ import pype.api from avalon import photoshop from pype.modules.websocket_server.clients.photoshop_client import \ - PhotoshopClientStub + PhotoshopClientStub + -from datetime import datetime class ExtractSaveScene(pype.api.Extractor): """Save scene before extraction.""" @@ -15,7 +15,4 @@ class ExtractSaveScene(pype.api.Extractor): def process(self, instance): photoshop_client = PhotoshopClientStub() - start = datetime.now() photoshop_client.save() - self.log.info( - "ExtractSaveScene took {}".format(datetime.now() - start)) diff --git a/pype/plugins/photoshop/publish/increment_workfile.py b/pype/plugins/photoshop/publish/increment_workfile.py index 0ae7e9772f..4298eb8e77 100644 --- a/pype/plugins/photoshop/publish/increment_workfile.py +++ b/pype/plugins/photoshop/publish/increment_workfile.py @@ -1,10 +1,10 @@ import pyblish.api from pype.action import get_errored_plugins_from_data from pype.lib import version_up -from avalon import photoshop from pype.modules.websocket_server.clients.photoshop_client import \ - PhotoshopClientStub + PhotoshopClientStub + class IncrementWorkfile(pyblish.api.InstancePlugin): """Increment the current workfile. diff --git a/pype/plugins/photoshop/publish/validate_instance_asset.py b/pype/plugins/photoshop/publish/validate_instance_asset.py index 6a0a408878..4bbea69eb4 100644 --- a/pype/plugins/photoshop/publish/validate_instance_asset.py +++ b/pype/plugins/photoshop/publish/validate_instance_asset.py @@ -27,8 +27,6 @@ class ValidateInstanceAssetRepair(pyblish.api.Action): instances = pyblish.api.instances_by_plugin(failed, plugin) photoshop_client = PhotoshopClientStub() for instance in instances: - self.log.info("validate_instance_asset instance[0] {}".format(instance[0])) - self.log.info("validate_instance_asset instance {}".format(instance)) data = photoshop_client.read(instance[0]) data["asset"] = os.environ["AVALON_ASSET"] diff --git a/pype/plugins/photoshop/publish/validate_naming.py b/pype/plugins/photoshop/publish/validate_naming.py index 7734a0e5a0..ba8a3e997e 100644 --- a/pype/plugins/photoshop/publish/validate_naming.py +++ b/pype/plugins/photoshop/publish/validate_naming.py @@ -3,7 +3,8 @@ import pype.api from avalon import photoshop from pype.modules.websocket_server.clients.photoshop_client import \ - PhotoshopClientStub + PhotoshopClientStub + class ValidateNamingRepair(pyblish.api.Action): """Repair the instance asset.""" From 91e65b1f15a3b7b5f795d0e6fbbbc6755a957391 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 20 Aug 2020 00:09:28 +0100 Subject: [PATCH 07/92] Safer alembic node search. --- pype/hosts/maya/plugin.py | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/pype/hosts/maya/plugin.py b/pype/hosts/maya/plugin.py index 3b002eed10..a5c57f1ab8 100644 --- a/pype/hosts/maya/plugin.py +++ b/pype/hosts/maya/plugin.py @@ -179,12 +179,19 @@ class ReferenceLoader(api.Loader): alembic_attrs = ["speed", "offset", "cycleType"] alembic_data = {} if representation["name"] == "abc": - alembic_node = cmds.ls( - cmds.sets(node, query=True), type="AlembicNode" - )[0] - for attr in alembic_attrs: - node_attr = "{}.{}".format(alembic_node, attr) - alembic_data[attr] = cmds.getAttr(node_attr) + alembic_nodes = cmds.ls( + "{}:*".format(members[0].split(":")[0]), type="AlembicNode" + ) + if alembic_nodes: + for attr in alembic_attrs: + node_attr = "{}.{}".format(alembic_nodes[0], attr) + alembic_data[attr] = cmds.getAttr(node_attr) + else: + cmds.warning( + "No alembic nodes found in {}".format( + cmds.ls("{}:*".format(members[0].split(":")[0])) + ) + ) try: content = cmds.file(path, @@ -209,18 +216,13 @@ class ReferenceLoader(api.Loader): # Reapply alembic settings. if representation["name"] == "abc": - alembic_node = None - for member in cmds.sets(node, query=True): - shapes = cmds.listRelatives(member, shapes=True) - if shapes: - nodes = cmds.listConnections(shapes[0], type="AlembicNode") - if nodes: - alembic_node = nodes[0] - break - - for attr in alembic_attrs: - value = alembic_data[attr] - cmds.setAttr("{}.{}".format(alembic_node, attr), value) + alembic_nodes = cmds.ls( + "{}:*".format(members[0].split(":")[0]), type="AlembicNode" + ) + if alembic_nodes: + for attr in alembic_attrs: + value = alembic_data[attr] + cmds.setAttr("{}.{}".format(alembic_nodes[0], attr), value) # Fix PLN-40 for older containers created with Avalon that had the # `.verticesOnlySet` set to True. From de9aca5f8f3ecdb3e635079c4cbd37c79b1c91a7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 20 Aug 2020 10:52:18 +0200 Subject: [PATCH 08/92] Speedup of collect_instances.py --- .../clients/photoshop_client.py | 25 +++++++++++-------- .../photoshop/publish/collect_instances.py | 14 ++++------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/pype/modules/websocket_server/clients/photoshop_client.py b/pype/modules/websocket_server/clients/photoshop_client.py index eea297954f..bf72c1bc5a 100644 --- a/pype/modules/websocket_server/clients/photoshop_client.py +++ b/pype/modules/websocket_server/clients/photoshop_client.py @@ -18,17 +18,19 @@ class PhotoshopClientStub(): self.websocketserver = WebSocketServer.get_instance() self.client = self.websocketserver.get_client() - def read(self, layer): + def read(self, layer, layers_meta=None): """ Parses layer metadata from Headline field of active document - :param layer: + :param layer: Layer("id": XXX, "name":'YYY') @@ -38,13 +40,14 @@ class PhotoshopClientStub(): triggered :return: None """ - layers_data = self._get_layers_metadata() + 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_data: - layers_data[str(layer.id)].update(data) + if str(layer.id) in layers_meta and layers_meta[str(layer.id)]: + layers_meta[str(layer.id)].update(data) else: - layers_data[str(layer.id)] = data + layers_meta[str(layer.id)] = data # Ensure only valid ids are stored. if not all_layers: @@ -52,9 +55,9 @@ class PhotoshopClientStub(): layer_ids = [layer.id for layer in all_layers] cleaned_data = {} - for id in layers_data: + for id in layers_meta: if int(id) in layer_ids: - cleaned_data[id] = layers_data[id] + cleaned_data[id] = layers_meta[id] payload = json.dumps(cleaned_data, indent=4) diff --git a/pype/plugins/photoshop/publish/collect_instances.py b/pype/plugins/photoshop/publish/collect_instances.py index 82a0c35311..47584272d2 100644 --- a/pype/plugins/photoshop/publish/collect_instances.py +++ b/pype/plugins/photoshop/publish/collect_instances.py @@ -2,8 +2,9 @@ import pythoncom import pyblish.api -from pype.modules.websocket_server.clients.photoshop_client \ - import PhotoshopClientStub +from pype.modules.websocket_server.clients.photoshop_client import ( + PhotoshopClientStub +) class CollectInstances(pyblish.api.ContextPlugin): @@ -30,14 +31,9 @@ class CollectInstances(pyblish.api.ContextPlugin): 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) - self.log.info("layer_data {}".format(layer_data)) - - photoshop_client.imprint(layer, layer_data, layers) - new_layer_data = photoshop_client.read(layer) - assert layer_data == new_layer_data + layer_data = photoshop_client.read(layer, layers_meta) # Skip layers without metadata. if layer_data is None: From e551039c124cfe82252479dcf6b506aac17cd31d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 20 Aug 2020 15:42:43 +0200 Subject: [PATCH 09/92] Speedup of collect_instances.py --- .../websocket_server/clients/photoshop_client.py | 13 ++++++++----- pype/plugins/photoshop/publish/extract_review.py | 7 ++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pype/modules/websocket_server/clients/photoshop_client.py b/pype/modules/websocket_server/clients/photoshop_client.py index bf72c1bc5a..00e5355786 100644 --- a/pype/modules/websocket_server/clients/photoshop_client.py +++ b/pype/modules/websocket_server/clients/photoshop_client.py @@ -91,14 +91,17 @@ class PhotoshopClientStub(): print("get_layers_in_layers len {}".format(len(layers))) print("get_layers_in_layers type {}".format(type(layers))) ret = [] - layer_ids = [lay.id for lay in layers] - layer_group_ids = [ll.groupId for ll in layers if ll.group] + parent_ids = set([lay.id for lay in layers]) + print("parent_ids ".format(parent_ids)) for layer in all_layers: - if layer.groupId in layer_group_ids: # all from group + print("layer {}".format(layer)) + parents = set(layer.parents) + print("parents {}".format(layer)) + if len(parent_ids & parents) > 0: ret.append(layer) - if layer.id in layer_ids: + if layer.id in parent_ids: ret.append(layer) - + print("ret {}".format(ret)) return ret def group_selected_layers(self): diff --git a/pype/plugins/photoshop/publish/extract_review.py b/pype/plugins/photoshop/publish/extract_review.py index 1e8d726720..806b59341b 100644 --- a/pype/plugins/photoshop/publish/extract_review.py +++ b/pype/plugins/photoshop/publish/extract_review.py @@ -4,8 +4,9 @@ import pype.api import pype.lib from avalon import photoshop -from pype.modules.websocket_server.clients.photoshop_client import \ - PhotoshopClientStub +from pype.modules.websocket_server.clients.photoshop_client import ( + PhotoshopClientStub +) class ExtractReview(pype.api.Extractor): @@ -36,7 +37,7 @@ class ExtractReview(pype.api.Extractor): # Hide all other layers. extract_ids = set([ll.id for ll in photoshop_client. get_layers_in_layers(layers)]) - + self.log.info("extract_ids {}".format(extract_ids)) for layer in photoshop_client.get_layers(): # limit unnecessary calls to client if layer.visible and layer.id not in extract_ids: From 41e2149d1295d5f89363648bd64da9cd11ec9f5c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 20 Aug 2020 17:15:36 +0200 Subject: [PATCH 10/92] Fix select correct layers for multiple images --- .../clients/photoshop_client.py | 29 ++++++++----------- .../photoshop/publish/collect_instances.py | 2 +- .../photoshop/publish/extract_image.py | 13 +++------ 3 files changed, 17 insertions(+), 27 deletions(-) diff --git a/pype/modules/websocket_server/clients/photoshop_client.py b/pype/modules/websocket_server/clients/photoshop_client.py index 00e5355786..330c2ceff0 100644 --- a/pype/modules/websocket_server/clients/photoshop_client.py +++ b/pype/modules/websocket_server/clients/photoshop_client.py @@ -26,7 +26,7 @@ class PhotoshopClientStub(): :return: """ if layers_meta is None: - layers_meta = self._get_layers_metadata() + layers_meta = self.get_layers_metadata() return layers_meta.get(str(layer.id)) @@ -38,10 +38,13 @@ class PhotoshopClientStub(): :param all_layers: - for performance, could be injected for usage in loop, if not, single call will be triggered + :param layers_meta: json representation from Headline + (for performance - provide only if imprint is in + loop - value should be same) :return: None """ if not layers_meta: - layers_meta = self._get_layers_metadata() + 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)]: @@ -83,25 +86,20 @@ class PhotoshopClientStub(): def get_layers_in_layers(self, layers): """ Return all layers that belong to layers (might be groups). - :param layers: - :return: + :param layers: + :return: """ all_layers = self.get_layers() - print("get_layers_in_layers {}".format(layers)) - print("get_layers_in_layers len {}".format(len(layers))) - print("get_layers_in_layers type {}".format(type(layers))) ret = [] parent_ids = set([lay.id for lay in layers]) - print("parent_ids ".format(parent_ids)) + for layer in all_layers: - print("layer {}".format(layer)) parents = set(layer.parents) - print("parents {}".format(layer)) if len(parent_ids & parents) > 0: ret.append(layer) if layer.id in parent_ids: ret.append(layer) - print("ret {}".format(ret)) + return ret def group_selected_layers(self): @@ -140,7 +138,8 @@ class PhotoshopClientStub(): :return: full path with name """ res = self.websocketserver.call( - self.client.call('Photoshop.get_active_document_full_name')) + self.client.call + ('Photoshop.get_active_document_full_name')) return res @@ -188,7 +187,7 @@ class PhotoshopClientStub(): layer_id=layer_id, visibility=visibility)) - def _get_layers_metadata(self): + def get_layers_metadata(self): """ Reads layers metadata from Headline from active document in PS. (Headline accessible by File > File Info) @@ -242,7 +241,3 @@ class PhotoshopClientStub(): for d in layers_data: ret.append(namedtuple('Layer', d.keys())(*d.values())) return ret - - - - diff --git a/pype/plugins/photoshop/publish/collect_instances.py b/pype/plugins/photoshop/publish/collect_instances.py index 47584272d2..7e433bc92f 100644 --- a/pype/plugins/photoshop/publish/collect_instances.py +++ b/pype/plugins/photoshop/publish/collect_instances.py @@ -31,7 +31,7 @@ class CollectInstances(pyblish.api.ContextPlugin): photoshop_client = PhotoshopClientStub() layers = photoshop_client.get_layers() - layers_meta = photoshop_client._get_layers_metadata() + layers_meta = photoshop_client.get_layers_metadata() for layer in layers: layer_data = photoshop_client.read(layer, layers_meta) diff --git a/pype/plugins/photoshop/publish/extract_image.py b/pype/plugins/photoshop/publish/extract_image.py index 4cbac38ce7..e32444c641 100644 --- a/pype/plugins/photoshop/publish/extract_image.py +++ b/pype/plugins/photoshop/publish/extract_image.py @@ -3,8 +3,9 @@ import os import pype.api from avalon import photoshop -from pype.modules.websocket_server.clients.photoshop_client import \ - PhotoshopClientStub +from pype.modules.websocket_server.clients.photoshop_client import ( + PhotoshopClientStub +) class ExtractImage(pype.api.Extractor): @@ -23,12 +24,6 @@ class ExtractImage(pype.api.Extractor): staging_dir = self.staging_dir(instance) self.log.info("Outputting image to {}".format(staging_dir)) - layers = [] - for image_instance in instance.context: - if image_instance.data["family"] != "image": - continue - layers.append(image_instance[0]) - # Perform extraction photoshop_client = PhotoshopClientStub() files = {} @@ -37,7 +32,7 @@ class ExtractImage(pype.api.Extractor): with photoshop.maintained_visibility(): # Hide all other layers. extract_ids = set([ll.id for ll in photoshop_client. - get_layers_in_layers(layers)]) + get_layers_in_layers([instance[0]])]) for layer in photoshop_client.get_layers(): # limit unnecessary calls to client From dce5de49380c618b89cf899424e356e7bfe82c90 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 20 Aug 2020 17:31:20 +0200 Subject: [PATCH 11/92] Finish loader --- pype/modules/websocket_server/clients/photoshop_client.py | 3 +++ pype/plugins/photoshop/load/load_image.py | 7 ++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pype/modules/websocket_server/clients/photoshop_client.py b/pype/modules/websocket_server/clients/photoshop_client.py index 330c2ceff0..a81870a4ee 100644 --- a/pype/modules/websocket_server/clients/photoshop_client.py +++ b/pype/modules/websocket_server/clients/photoshop_client.py @@ -208,6 +208,9 @@ class PhotoshopClientStub(): Args: path (str): File path to import. """ + self.websocketserver.call(self.client.call + ('Photoshop.import_smart_object', + path=path)) def replace_smart_object(self, layer, path): """ diff --git a/pype/plugins/photoshop/load/load_image.py b/pype/plugins/photoshop/load/load_image.py index a24280553c..1856155b2a 100644 --- a/pype/plugins/photoshop/load/load_image.py +++ b/pype/plugins/photoshop/load/load_image.py @@ -1,7 +1,8 @@ from avalon import api, photoshop -from pype.modules.websocket_server.clients.photoshop_client \ - import PhotoshopClientStub +from pype.modules.websocket_server.clients.photoshop_client import ( + PhotoshopClientStub +) photoshopClient = PhotoshopClientStub() @@ -17,7 +18,7 @@ class ImageLoader(api.Loader): def load(self, context, name=None, namespace=None, data=None): with photoshop.maintained_selection(): - layer = photoshop.import_smart_object(self.fname) + layer = photoshopClient.import_smart_object(self.fname) self[:] = [layer] From fc2018e22b5a3e69a34577b123c07b7d522254c4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 20 Aug 2020 20:12:03 +0200 Subject: [PATCH 12/92] Finished Creator --- .../clients/photoshop_client.py | 27 +++++++++++++--- pype/plugins/photoshop/create/create_image.py | 32 ++++++++++--------- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/pype/modules/websocket_server/clients/photoshop_client.py b/pype/modules/websocket_server/clients/photoshop_client.py index a81870a4ee..4f0bca99cc 100644 --- a/pype/modules/websocket_server/clients/photoshop_client.py +++ b/pype/modules/websocket_server/clients/photoshop_client.py @@ -102,13 +102,28 @@ class PhotoshopClientStub(): return ret - def group_selected_layers(self): + def create_group(self, name): """ - Group selected layers into new layer - :return: + Create new group (eg. LayerSet) + :return: """ - self.websocketserver.call(self.client.call - ('Photoshop.group_selected_layers')) + ret = self.websocketserver.call(self.client.call + ('Photoshop.create_group', + name=name)) + # create group on PS is asynchronous, returns only id + layer = {"id": ret, "name": name, "group": True} + return namedtuple('Layer', layer.keys())(*layer.values()) + + def group_selected_layers(self, name): + """ + Group selected layers into new LayerSet (eg. group) + :return: + """ + res = self.websocketserver.call(self.client.call + ('Photoshop.group_selected_layers', + name=name) + ) + return self._to_records(res) def get_selected_layers(self): """ @@ -241,6 +256,8 @@ class PhotoshopClientStub(): 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 diff --git a/pype/plugins/photoshop/create/create_image.py b/pype/plugins/photoshop/create/create_image.py index 5b2f9f7981..6b54b2a036 100644 --- a/pype/plugins/photoshop/create/create_image.py +++ b/pype/plugins/photoshop/create/create_image.py @@ -1,5 +1,8 @@ -from avalon import api, photoshop +from avalon import api from avalon.vendor import Qt +from pype.modules.websocket_server.clients.photoshop_client import ( + PhotoshopClientStub +) class CreateImage(api.Creator): @@ -13,11 +16,12 @@ class CreateImage(api.Creator): groups = [] layers = [] create_group = False - group_constant = photoshop.get_com_objects().constants().psLayerSet + + photoshopClient = PhotoshopClientStub() if (self.options or {}).get("useSelection"): multiple_instances = False - selection = photoshop.get_selected_layers() - + selection = photoshopClient.get_selected_layers() + self.log.info("selection {}".format(selection)) if len(selection) > 1: # Ask user whether to create one image or image per selected # item. @@ -40,19 +44,19 @@ class CreateImage(api.Creator): if multiple_instances: for item in selection: - if item.LayerType == group_constant: + if item.group: groups.append(item) else: layers.append(item) else: - group = photoshop.group_selected_layers() - group.Name = self.name + group = photoshopClient.group_selected_layers() + group.name = self.name groups.append(group) elif len(selection) == 1: # One selected item. Use group if its a LayerSet (group), else # create a new group. - if selection[0].LayerType == group_constant: + if selection[0].group: groups.append(selection[0]) else: layers.append(selection[0]) @@ -63,16 +67,14 @@ class CreateImage(api.Creator): create_group = True if create_group: - group = photoshop.app().ActiveDocument.LayerSets.Add() - group.Name = self.name + group = photoshopClient.create_group(self.name) groups.append(group) for layer in layers: - photoshop.select_layers([layer]) - group = photoshop.group_selected_layers() - group.Name = layer.Name + photoshopClient.select_layers([layer]) + group = photoshopClient.group_selected_layers(layer.name) groups.append(group) for group in groups: - self.data.update({"subset": "image" + group.Name}) - photoshop.imprint(group, self.data) + self.data.update({"subset": "image" + group.name}) + photoshopClient.imprint(group, self.data) From 4fb557c8376ed44280e1a73f50ba10a5c2e0d1e1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 20 Aug 2020 20:15:09 +0200 Subject: [PATCH 13/92] Hound --- pype/plugins/photoshop/publish/collect_current_file.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pype/plugins/photoshop/publish/collect_current_file.py b/pype/plugins/photoshop/publish/collect_current_file.py index 604ce97f89..7877caa137 100644 --- a/pype/plugins/photoshop/publish/collect_current_file.py +++ b/pype/plugins/photoshop/publish/collect_current_file.py @@ -2,8 +2,9 @@ import os import pyblish.api -from pype.modules.websocket_server.clients.photoshop_client import \ - PhotoshopClientStub +from pype.modules.websocket_server.clients.photoshop_client import ( + PhotoshopClientStub +) class CollectCurrentFile(pyblish.api.ContextPlugin): From 7f5fc953ce98167b5f70127d6eb0506ec2d05cd3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 20 Aug 2020 20:15:09 +0200 Subject: [PATCH 14/92] Hound --- pype/plugins/photoshop/publish/collect_current_file.py | 5 +++-- pype/plugins/photoshop/publish/extract_save_scene.py | 5 +++-- pype/plugins/photoshop/publish/increment_workfile.py | 5 +++-- pype/plugins/photoshop/publish/validate_instance_asset.py | 6 ++++-- pype/plugins/photoshop/publish/validate_naming.py | 5 +++-- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/pype/plugins/photoshop/publish/collect_current_file.py b/pype/plugins/photoshop/publish/collect_current_file.py index 604ce97f89..7877caa137 100644 --- a/pype/plugins/photoshop/publish/collect_current_file.py +++ b/pype/plugins/photoshop/publish/collect_current_file.py @@ -2,8 +2,9 @@ import os import pyblish.api -from pype.modules.websocket_server.clients.photoshop_client import \ - PhotoshopClientStub +from pype.modules.websocket_server.clients.photoshop_client import ( + PhotoshopClientStub +) class CollectCurrentFile(pyblish.api.ContextPlugin): diff --git a/pype/plugins/photoshop/publish/extract_save_scene.py b/pype/plugins/photoshop/publish/extract_save_scene.py index 3357a05f24..c56e5418c9 100644 --- a/pype/plugins/photoshop/publish/extract_save_scene.py +++ b/pype/plugins/photoshop/publish/extract_save_scene.py @@ -1,8 +1,9 @@ import pype.api from avalon import photoshop -from pype.modules.websocket_server.clients.photoshop_client import \ - PhotoshopClientStub +from pype.modules.websocket_server.clients.photoshop_client import ( + PhotoshopClientStub +) class ExtractSaveScene(pype.api.Extractor): diff --git a/pype/plugins/photoshop/publish/increment_workfile.py b/pype/plugins/photoshop/publish/increment_workfile.py index 4298eb8e77..af8ba0b6ae 100644 --- a/pype/plugins/photoshop/publish/increment_workfile.py +++ b/pype/plugins/photoshop/publish/increment_workfile.py @@ -2,8 +2,9 @@ import pyblish.api from pype.action import get_errored_plugins_from_data from pype.lib import version_up -from pype.modules.websocket_server.clients.photoshop_client import \ - PhotoshopClientStub +from pype.modules.websocket_server.clients.photoshop_client import ( + PhotoshopClientStub +) class IncrementWorkfile(pyblish.api.InstancePlugin): diff --git a/pype/plugins/photoshop/publish/validate_instance_asset.py b/pype/plugins/photoshop/publish/validate_instance_asset.py index 4bbea69eb4..aa8d2661ff 100644 --- a/pype/plugins/photoshop/publish/validate_instance_asset.py +++ b/pype/plugins/photoshop/publish/validate_instance_asset.py @@ -4,8 +4,10 @@ import pyblish.api import pype.api from avalon import photoshop -from pype.modules.websocket_server.clients.photoshop_client import \ - PhotoshopClientStub +from pype.modules.websocket_server.clients.photoshop_client import ( + PhotoshopClientStub +) + class ValidateInstanceAssetRepair(pyblish.api.Action): """Repair the instance asset.""" diff --git a/pype/plugins/photoshop/publish/validate_naming.py b/pype/plugins/photoshop/publish/validate_naming.py index ba8a3e997e..c612270802 100644 --- a/pype/plugins/photoshop/publish/validate_naming.py +++ b/pype/plugins/photoshop/publish/validate_naming.py @@ -2,8 +2,9 @@ import pyblish.api import pype.api from avalon import photoshop -from pype.modules.websocket_server.clients.photoshop_client import \ - PhotoshopClientStub +from pype.modules.websocket_server.clients.photoshop_client import ( + PhotoshopClientStub +) class ValidateNamingRepair(pyblish.api.Action): From 690bb9f8b16b85c36616ae0031a39874322827c5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 21 Aug 2020 10:54:38 +0200 Subject: [PATCH 15/92] Fix missed providing name to group_selected_layers --- pype/plugins/photoshop/create/create_image.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pype/plugins/photoshop/create/create_image.py b/pype/plugins/photoshop/create/create_image.py index 6b54b2a036..0a019fe2f8 100644 --- a/pype/plugins/photoshop/create/create_image.py +++ b/pype/plugins/photoshop/create/create_image.py @@ -49,8 +49,7 @@ class CreateImage(api.Creator): else: layers.append(item) else: - group = photoshopClient.group_selected_layers() - group.name = self.name + group = photoshopClient.group_selected_layers(self.name) groups.append(group) elif len(selection) == 1: From f8743e3b80dedfb37da392fd4c7108e5680ae2f6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 21 Aug 2020 15:23:35 +0200 Subject: [PATCH 16/92] Removed usage of http_server --- .../websocket_server/hosts/photoshop.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/pype/modules/websocket_server/hosts/photoshop.py b/pype/modules/websocket_server/hosts/photoshop.py index c63f66865e..ae72963b1b 100644 --- a/pype/modules/websocket_server/hosts/photoshop.py +++ b/pype/modules/websocket_server/hosts/photoshop.py @@ -1,5 +1,8 @@ from pype.api import Logger from wsrpc_aiohttp import WebSocketRoute +import functools + +import avalon.photoshop as photoshop log = Logger().get_logger("WebsocketServer") @@ -31,3 +34,31 @@ class Photoshop(WebSocketRoute): log.debug("photoshop.read client calls server server calls " "Photo 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 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(photoshop.show, tool_name) + + photoshop.execute_in_main_thread(partial_method) + + # Required return statement. + return "nothing" \ No newline at end of file From 33fc39bf6265eac2713f704efc540c4f04d3a3ac Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 21 Aug 2020 14:38:10 +0100 Subject: [PATCH 17/92] Update subset families on integration --- pype/plugins/global/publish/integrate_new.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index a3c2ffe52b..cc106ad8a2 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -680,6 +680,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): instance.data.get('subsetGroup')}} ) + # Update families on subset. + io.update_many( + {"type": "subset", "_id": io.ObjectId(subset["_id"])}, + {"$set": {"data.families": instance.data.get("families", [])}} + ) + return subset def create_version(self, subset, version_number, data=None): From b37037007e6b7b300e98cb3cdf96809f452ce2eb Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 21 Aug 2020 15:43:52 +0100 Subject: [PATCH 18/92] Fix optional skip reviews on renders. --- pype/plugins/global/publish/submit_publish_job.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 2fe6735e90..bb8473dcf6 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -729,7 +729,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "pixelAspect": data.get("pixelAspect", 1), "resolutionWidth": data.get("resolutionWidth", 1920), "resolutionHeight": data.get("resolutionHeight", 1080), - "multipartExr": data.get("multipartExr", False) + "multipartExr": data.get("multipartExr", False), + "review": data.get("review", True) } if "prerender" in instance.data["families"]: From 104027c17c494969fb0ae1c78e57b1eb910f80a1 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 21 Aug 2020 16:22:41 +0100 Subject: [PATCH 19/92] Integrate family as well --- pype/plugins/global/publish/integrate_new.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index cc106ad8a2..142e72e3ac 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -681,9 +681,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ) # Update families on subset. + families = [instance.data["family"]] + families.extend(instance.data.get("families", [])) io.update_many( {"type": "subset", "_id": io.ObjectId(subset["_id"])}, - {"$set": {"data.families": instance.data.get("families", [])}} + {"$set": {"data.families": families}} ) return subset From f31fb3e03426830a1e1b3d10502230e453fa9e6f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 24 Aug 2020 17:44:16 +0200 Subject: [PATCH 20/92] Removed com32 objects Refactore - changed names to highlight 'stub' approach Small fixes --- .../photoshop_server_stub.py} | 35 +++++++++++++++---- pype/plugins/photoshop/create/create_image.py | 18 +++++----- pype/plugins/photoshop/load/load_image.py | 12 +++---- .../photoshop/publish/collect_current_file.py | 7 ++-- .../photoshop/publish/collect_instances.py | 12 +++---- .../photoshop/publish/extract_image.py | 22 ++++-------- .../photoshop/publish/extract_review.py | 22 ++++-------- .../photoshop/publish/extract_save_scene.py | 7 +--- .../photoshop/publish/increment_workfile.py | 7 ++-- .../publish/validate_instance_asset.py | 10 ++---- .../photoshop/publish/validate_naming.py | 10 ++---- 11 files changed, 70 insertions(+), 92 deletions(-) rename pype/modules/websocket_server/{clients/photoshop_client.py => stubs/photoshop_server_stub.py} (89%) diff --git a/pype/modules/websocket_server/clients/photoshop_client.py b/pype/modules/websocket_server/stubs/photoshop_server_stub.py similarity index 89% rename from pype/modules/websocket_server/clients/photoshop_client.py rename to pype/modules/websocket_server/stubs/photoshop_server_stub.py index 4f0bca99cc..f798a09b92 100644 --- a/pype/modules/websocket_server/clients/photoshop_client.py +++ b/pype/modules/websocket_server/stubs/photoshop_server_stub.py @@ -7,17 +7,28 @@ import json from collections import namedtuple -class PhotoshopClientStub(): +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 """ def __init__(self): self.websocketserver = WebSocketServer.get_instance() self.client = self.websocketserver.get_client() + def open(self, path): + """ + Open file located at 'path' (local). + :param path: file path locally + :return: 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 @@ -107,9 +118,9 @@ class PhotoshopClientStub(): Create new group (eg. LayerSet) :return: """ - ret = self.websocketserver.call(self.client.call - ('Photoshop.create_group', - name=name)) + ret = self.websocketserver.call(self.client.call + ('Photoshop.create_group', + name=name)) # create group on PS is asynchronous, returns only id layer = {"id": ret, "name": name, "group": True} return namedtuple('Layer', layer.keys())(*layer.values()) @@ -168,6 +179,14 @@ class PhotoshopClientStub(): return res + def is_saved(self): + """ + Returns true if no changes in active document + :return: + """ + return self.websocketserver.call(self.client.call + ('Photoshop.is_saved')) + def save(self): """ Saves active document @@ -223,9 +242,11 @@ class PhotoshopClientStub(): Args: path (str): File path to import. """ - self.websocketserver.call(self.client.call - ('Photoshop.import_smart_object', - path=path)) + res = self.websocketserver.call(self.client.call + ('Photoshop.import_smart_object', + path=path)) + + return self._to_records(res).pop() def replace_smart_object(self, layer, path): """ diff --git a/pype/plugins/photoshop/create/create_image.py b/pype/plugins/photoshop/create/create_image.py index 0a019fe2f8..c1a7d92a2c 100644 --- a/pype/plugins/photoshop/create/create_image.py +++ b/pype/plugins/photoshop/create/create_image.py @@ -1,8 +1,6 @@ from avalon import api from avalon.vendor import Qt -from pype.modules.websocket_server.clients.photoshop_client import ( - PhotoshopClientStub -) +from avalon import photoshop class CreateImage(api.Creator): @@ -17,10 +15,10 @@ class CreateImage(api.Creator): layers = [] create_group = False - photoshopClient = PhotoshopClientStub() + stub = photoshop.stub() if (self.options or {}).get("useSelection"): multiple_instances = False - selection = photoshopClient.get_selected_layers() + selection = stub.get_selected_layers() self.log.info("selection {}".format(selection)) if len(selection) > 1: # Ask user whether to create one image or image per selected @@ -49,7 +47,7 @@ class CreateImage(api.Creator): else: layers.append(item) else: - group = photoshopClient.group_selected_layers(self.name) + group = stub.group_selected_layers(self.name) groups.append(group) elif len(selection) == 1: @@ -66,14 +64,14 @@ class CreateImage(api.Creator): create_group = True if create_group: - group = photoshopClient.create_group(self.name) + group = stub.create_group(self.name) groups.append(group) for layer in layers: - photoshopClient.select_layers([layer]) - group = photoshopClient.group_selected_layers(layer.name) + stub.select_layers([layer]) + group = stub.group_selected_layers(layer.name) groups.append(group) for group in groups: self.data.update({"subset": "image" + group.name}) - photoshopClient.imprint(group, self.data) + stub.imprint(group, self.data) diff --git a/pype/plugins/photoshop/load/load_image.py b/pype/plugins/photoshop/load/load_image.py index 1856155b2a..75c02bb327 100644 --- a/pype/plugins/photoshop/load/load_image.py +++ b/pype/plugins/photoshop/load/load_image.py @@ -1,10 +1,6 @@ from avalon import api, photoshop -from pype.modules.websocket_server.clients.photoshop_client import ( - PhotoshopClientStub -) - -photoshopClient = PhotoshopClientStub() +stub = photoshop.stub() class ImageLoader(api.Loader): @@ -18,7 +14,7 @@ class ImageLoader(api.Loader): def load(self, context, name=None, namespace=None, data=None): with photoshop.maintained_selection(): - layer = photoshopClient.import_smart_object(self.fname) + layer = stub.import_smart_object(self.fname) self[:] = [layer] @@ -34,11 +30,11 @@ class ImageLoader(api.Loader): layer = container.pop("layer") with photoshop.maintained_selection(): - photoshopClient.replace_smart_object( + stub.replace_smart_object( layer, api.get_representation_path(representation) ) - photoshopClient.imprint( + stub.imprint( layer, {"representation": str(representation["_id"])} ) diff --git a/pype/plugins/photoshop/publish/collect_current_file.py b/pype/plugins/photoshop/publish/collect_current_file.py index 7877caa137..3cc3e3f636 100644 --- a/pype/plugins/photoshop/publish/collect_current_file.py +++ b/pype/plugins/photoshop/publish/collect_current_file.py @@ -2,9 +2,7 @@ import os import pyblish.api -from pype.modules.websocket_server.clients.photoshop_client import ( - PhotoshopClientStub -) +from avalon import photoshop class CollectCurrentFile(pyblish.api.ContextPlugin): @@ -15,7 +13,6 @@ class CollectCurrentFile(pyblish.api.ContextPlugin): hosts = ["photoshop"] def process(self, context): - photoshop_client = PhotoshopClientStub() context.data["currentFile"] = os.path.normpath( - photoshop_client.get_active_document_full_name() + photoshop.stub().get_active_document_full_name() ).replace("\\", "/") diff --git a/pype/plugins/photoshop/publish/collect_instances.py b/pype/plugins/photoshop/publish/collect_instances.py index 7e433bc92f..81d1c80bf6 100644 --- a/pype/plugins/photoshop/publish/collect_instances.py +++ b/pype/plugins/photoshop/publish/collect_instances.py @@ -2,9 +2,7 @@ import pythoncom import pyblish.api -from pype.modules.websocket_server.clients.photoshop_client import ( - PhotoshopClientStub -) +from avalon import photoshop class CollectInstances(pyblish.api.ContextPlugin): @@ -29,11 +27,11 @@ class CollectInstances(pyblish.api.ContextPlugin): # can be. pythoncom.CoInitialize() - photoshop_client = PhotoshopClientStub() - layers = photoshop_client.get_layers() - layers_meta = photoshop_client.get_layers_metadata() + stub = photoshop.stub() + layers = stub.get_layers() + layers_meta = stub.get_layers_metadata() for layer in layers: - layer_data = photoshop_client.read(layer, layers_meta) + layer_data = stub.read(layer, layers_meta) # Skip layers without metadata. if layer_data is None: diff --git a/pype/plugins/photoshop/publish/extract_image.py b/pype/plugins/photoshop/publish/extract_image.py index e32444c641..38920b5557 100644 --- a/pype/plugins/photoshop/publish/extract_image.py +++ b/pype/plugins/photoshop/publish/extract_image.py @@ -3,10 +3,6 @@ import os import pype.api from avalon import photoshop -from pype.modules.websocket_server.clients.photoshop_client import ( - PhotoshopClientStub -) - class ExtractImage(pype.api.Extractor): """Produce a flattened image file from instance @@ -25,23 +21,21 @@ class ExtractImage(pype.api.Extractor): self.log.info("Outputting image to {}".format(staging_dir)) # Perform extraction - photoshop_client = PhotoshopClientStub() + 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 photoshop_client. + extract_ids = set([ll.id for ll in stub. get_layers_in_layers([instance[0]])]) - for layer in photoshop_client.get_layers(): + for layer in stub.get_layers(): # limit unnecessary calls to client if layer.visible and layer.id not in extract_ids: - photoshop_client.set_visible(layer.id, - False) + stub.set_visible(layer.id, False) if not layer.visible and layer.id in extract_ids: - photoshop_client.set_visible(layer.id, - True) + stub.set_visible(layer.id, True) save_options = [] if "png" in self.formats: @@ -50,16 +44,14 @@ class ExtractImage(pype.api.Extractor): save_options.append('jpg') file_basename = os.path.splitext( - photoshop_client.get_active_document_name() + 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) - photoshop_client.saveAs(full_filename, - extension, - True) + stub.saveAs(full_filename, extension, True) representations = [] for extension, filename in files.items(): diff --git a/pype/plugins/photoshop/publish/extract_review.py b/pype/plugins/photoshop/publish/extract_review.py index 806b59341b..6fb50bba9f 100644 --- a/pype/plugins/photoshop/publish/extract_review.py +++ b/pype/plugins/photoshop/publish/extract_review.py @@ -4,10 +4,6 @@ import pype.api import pype.lib from avalon import photoshop -from pype.modules.websocket_server.clients.photoshop_client import ( - PhotoshopClientStub -) - class ExtractReview(pype.api.Extractor): """Produce a flattened image file from all instances.""" @@ -20,7 +16,7 @@ class ExtractReview(pype.api.Extractor): staging_dir = self.staging_dir(instance) self.log.info("Outputting image to {}".format(staging_dir)) - photoshop_client = PhotoshopClientStub() + stub = photoshop.stub() layers = [] for image_instance in instance.context: @@ -30,26 +26,22 @@ class ExtractReview(pype.api.Extractor): # Perform extraction output_image = "{}.jpg".format( - os.path.splitext(photoshop_client.get_active_document_name())[0] + os.path.splitext(stub.get_active_document_name())[0] ) output_image_path = os.path.join(staging_dir, output_image) with photoshop.maintained_visibility(): # Hide all other layers. - extract_ids = set([ll.id for ll in photoshop_client. + extract_ids = set([ll.id for ll in stub. get_layers_in_layers(layers)]) self.log.info("extract_ids {}".format(extract_ids)) - for layer in photoshop_client.get_layers(): + for layer in stub.get_layers(): # limit unnecessary calls to client if layer.visible and layer.id not in extract_ids: - photoshop_client.set_visible(layer.id, - False) + stub.set_visible(layer.id, False) if not layer.visible and layer.id in extract_ids: - photoshop_client.set_visible(layer.id, - True) + stub.set_visible(layer.id, True) - photoshop_client.saveAs(output_image_path, - 'jpg', - True) + stub.saveAs(output_image_path, 'jpg', True) ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") diff --git a/pype/plugins/photoshop/publish/extract_save_scene.py b/pype/plugins/photoshop/publish/extract_save_scene.py index c56e5418c9..63a4b7b7ea 100644 --- a/pype/plugins/photoshop/publish/extract_save_scene.py +++ b/pype/plugins/photoshop/publish/extract_save_scene.py @@ -1,10 +1,6 @@ import pype.api from avalon import photoshop -from pype.modules.websocket_server.clients.photoshop_client import ( - PhotoshopClientStub -) - class ExtractSaveScene(pype.api.Extractor): """Save scene before extraction.""" @@ -15,5 +11,4 @@ class ExtractSaveScene(pype.api.Extractor): families = ["workfile"] def process(self, instance): - photoshop_client = PhotoshopClientStub() - photoshop_client.save() + photoshop.stub().save() diff --git a/pype/plugins/photoshop/publish/increment_workfile.py b/pype/plugins/photoshop/publish/increment_workfile.py index af8ba0b6ae..eca2583595 100644 --- a/pype/plugins/photoshop/publish/increment_workfile.py +++ b/pype/plugins/photoshop/publish/increment_workfile.py @@ -2,9 +2,7 @@ import pyblish.api from pype.action import get_errored_plugins_from_data from pype.lib import version_up -from pype.modules.websocket_server.clients.photoshop_client import ( - PhotoshopClientStub -) +from avalon import photoshop class IncrementWorkfile(pyblish.api.InstancePlugin): @@ -27,7 +25,6 @@ class IncrementWorkfile(pyblish.api.InstancePlugin): ) scene_path = version_up(instance.context.data["currentFile"]) - photoshop_client = PhotoshopClientStub() - photoshop_client.saveAs(scene_path, 'psd', True) + photoshop.stub().saveAs(scene_path, 'psd', True) self.log.info("Incremented workfile to: {}".format(scene_path)) diff --git a/pype/plugins/photoshop/publish/validate_instance_asset.py b/pype/plugins/photoshop/publish/validate_instance_asset.py index aa8d2661ff..f05d9601dd 100644 --- a/pype/plugins/photoshop/publish/validate_instance_asset.py +++ b/pype/plugins/photoshop/publish/validate_instance_asset.py @@ -4,10 +4,6 @@ import pyblish.api import pype.api from avalon import photoshop -from pype.modules.websocket_server.clients.photoshop_client import ( - PhotoshopClientStub -) - class ValidateInstanceAssetRepair(pyblish.api.Action): """Repair the instance asset.""" @@ -27,12 +23,12 @@ class ValidateInstanceAssetRepair(pyblish.api.Action): # Apply pyblish.logic to get the instances for the plug-in instances = pyblish.api.instances_by_plugin(failed, plugin) - photoshop_client = PhotoshopClientStub() + stub = photoshop.stub() for instance in instances: - data = photoshop_client.read(instance[0]) + data = stub.read(instance[0]) data["asset"] = os.environ["AVALON_ASSET"] - photoshop_client.imprint(instance[0], data) + stub.imprint(instance[0], data) class ValidateInstanceAsset(pyblish.api.InstancePlugin): diff --git a/pype/plugins/photoshop/publish/validate_naming.py b/pype/plugins/photoshop/publish/validate_naming.py index c612270802..2483adcb5e 100644 --- a/pype/plugins/photoshop/publish/validate_naming.py +++ b/pype/plugins/photoshop/publish/validate_naming.py @@ -2,10 +2,6 @@ import pyblish.api import pype.api from avalon import photoshop -from pype.modules.websocket_server.clients.photoshop_client import ( - PhotoshopClientStub -) - class ValidateNamingRepair(pyblish.api.Action): """Repair the instance asset.""" @@ -25,14 +21,14 @@ class ValidateNamingRepair(pyblish.api.Action): # Apply pyblish.logic to get the instances for the plug-in instances = pyblish.api.instances_by_plugin(failed, plugin) - photoshop_client = PhotoshopClientStub() + stub = photoshop.stub() for instance in instances: self.log.info("validate_naming instance {}".format(instance)) name = instance.data["name"].replace(" ", "_") instance[0].Name = name - data = photoshop_client.read(instance[0]) + data = stub.read(instance[0]) data["subset"] = "image" + name - photoshop_client.imprint(instance[0], data) + stub.imprint(instance[0], data) return True From 509776ddc074af2571716308a2eb7fea5ee9d8b3 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 25 Aug 2020 11:16:07 +0100 Subject: [PATCH 21/92] Optional camera creation on image plane loading. --- pype/plugins/maya/load/load_image_plane.py | 38 +++++++++++++--------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/pype/plugins/maya/load/load_image_plane.py b/pype/plugins/maya/load/load_image_plane.py index 08f7c99156..7ec56aab3a 100644 --- a/pype/plugins/maya/load/load_image_plane.py +++ b/pype/plugins/maya/load/load_image_plane.py @@ -29,6 +29,8 @@ class ImagePlaneLoader(api.Loader): # Getting camera from selection. selection = pc.ls(selection=True) + camera = None + if len(selection) > 1: QtWidgets.QMessageBox.critical( None, @@ -39,25 +41,29 @@ class ImagePlaneLoader(api.Loader): return if len(selection) < 1: - QtWidgets.QMessageBox.critical( + result = QtWidgets.QMessageBox.critical( None, "Error!", - "No camera selected.", - QtWidgets.QMessageBox.Ok + "No camera selected. Do you want to create a camera?", + QtWidgets.QMessageBox.Ok, + QtWidgets.QMessageBox.Cancel ) - return - - relatives = pc.listRelatives(selection[0], shapes=True) - if not pc.ls(relatives, type="camera"): - QtWidgets.QMessageBox.critical( - None, - "Error!", - "Selected node is not a camera.", - QtWidgets.QMessageBox.Ok - ) - return - - camera = selection[0] + if result == QtWidgets.QMessageBox.Ok: + camera = pc.createNode("camera") + else: + return + else: + relatives = pc.listRelatives(selection[0], shapes=True) + if pc.ls(relatives, type="camera"): + camera = selection[0] + else: + QtWidgets.QMessageBox.critical( + None, + "Error!", + "Selected node is not a camera.", + QtWidgets.QMessageBox.Ok + ) + return try: camera.displayResolution.set(1) From a00b11d31fe2e1c58dab6b124a1a62a2d377a768 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 26 Aug 2020 14:29:57 +0200 Subject: [PATCH 22/92] Added pulling websocket server port from environment variable WEBSOCKET_URL --- .../websocket_server/websocket_server.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/pype/modules/websocket_server/websocket_server.py b/pype/modules/websocket_server/websocket_server.py index 6b730d4eb3..777bcf1f61 100644 --- a/pype/modules/websocket_server/websocket_server.py +++ b/pype/modules/websocket_server/websocket_server.py @@ -9,6 +9,7 @@ import os import sys import pyclbr import importlib +import urllib log = Logger().get_logger("WebsocketServer") @@ -28,19 +29,16 @@ class WebSocketServer(): self.qaction = None self.failed_icon = None self._is_running = False - default_port = 8099 WebSocketServer._instance = self self.client = None self.handlers = {} - try: - self.presets = config.get_presets()["services"]["websocket_server"] - except Exception: - self.presets = {"default_port": default_port, "exclude_ports": []} - log.debug(( - "There are not set presets for WebsocketServer." - " Using defaults \"{}\"" - ).format(str(self.presets))) + websocket_url = os.getenv("WEBSOCKET_URL") + if websocket_url: + parsed = urllib.parse.urlparse(websocket_url) + port = parsed.port + if not port: + port = 8099 # try default port self.app = web.Application() @@ -52,7 +50,7 @@ class WebSocketServer(): directories_with_routes = ['hosts'] self.add_routes_for_directories(directories_with_routes) - self.websocket_thread = WebsocketServerThread(self, default_port) + self.websocket_thread = WebsocketServerThread(self, port) def add_routes_for_directories(self, directories_with_routes): """ Loops through selected directories to find all modules and From 06359c6dc335779f251dc426af8d6bda5be76422 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 27 Aug 2020 19:22:15 +0200 Subject: [PATCH 23/92] Hound --- pype/modules/websocket_server/hosts/photoshop.py | 2 +- .../modules/websocket_server/stubs/photoshop_server_stub.py | 3 +-- pype/modules/websocket_server/websocket_server.py | 6 +++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pype/modules/websocket_server/hosts/photoshop.py b/pype/modules/websocket_server/hosts/photoshop.py index ae72963b1b..cdfb9413a0 100644 --- a/pype/modules/websocket_server/hosts/photoshop.py +++ b/pype/modules/websocket_server/hosts/photoshop.py @@ -61,4 +61,4 @@ class Photoshop(WebSocketRoute): photoshop.execute_in_main_thread(partial_method) # Required return statement. - return "nothing" \ No newline at end of file + return "nothing" diff --git a/pype/modules/websocket_server/stubs/photoshop_server_stub.py b/pype/modules/websocket_server/stubs/photoshop_server_stub.py index f798a09b92..da69127799 100644 --- a/pype/modules/websocket_server/stubs/photoshop_server_stub.py +++ b/pype/modules/websocket_server/stubs/photoshop_server_stub.py @@ -164,8 +164,7 @@ class PhotoshopServerStub(): :return: full path with name """ res = self.websocketserver.call( - self.client.call - ('Photoshop.get_active_document_full_name')) + self.client.call('Photoshop.get_active_document_full_name')) return res diff --git a/pype/modules/websocket_server/websocket_server.py b/pype/modules/websocket_server/websocket_server.py index 777bcf1f61..4556dd0491 100644 --- a/pype/modules/websocket_server/websocket_server.py +++ b/pype/modules/websocket_server/websocket_server.py @@ -1,4 +1,4 @@ -from pype.api import config, Logger +from pype.api import Logger import threading from aiohttp import web @@ -37,8 +37,8 @@ class WebSocketServer(): if websocket_url: parsed = urllib.parse.urlparse(websocket_url) port = parsed.port - if not port: - port = 8099 # try default port + if not port: + port = 8099 # fallback self.app = web.Application() From cd6c83913fd3af359875c38d17bc1ee93bfc95ae Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 2 Sep 2020 08:56:29 +0100 Subject: [PATCH 24/92] Nuke: skip thumbnail creation on farm submission. The thumbnail is being created when publishing from the baked colourspace movie. --- pype/plugins/global/publish/extract_jpeg.py | 5 +++++ pype/plugins/nuke/publish/extract_thumbnail.py | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/extract_jpeg.py b/pype/plugins/global/publish/extract_jpeg.py index 89a4bbd664..d23ce4360f 100644 --- a/pype/plugins/global/publish/extract_jpeg.py +++ b/pype/plugins/global/publish/extract_jpeg.py @@ -81,6 +81,11 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): jpeg_items.append("-i {}".format(full_input_path)) # output arguments from presets jpeg_items.extend(ffmpeg_args.get("output") or []) + + # If its a movie file, we just want one frame. + if repre["ext"] == "mov": + jpeg_items.append("-vframes 1") + # output file jpeg_items.append(full_output_path) diff --git a/pype/plugins/nuke/publish/extract_thumbnail.py b/pype/plugins/nuke/publish/extract_thumbnail.py index a3ef09bc9f..a53cb4d146 100644 --- a/pype/plugins/nuke/publish/extract_thumbnail.py +++ b/pype/plugins/nuke/publish/extract_thumbnail.py @@ -15,10 +15,12 @@ class ExtractThumbnail(pype.api.Extractor): order = pyblish.api.ExtractorOrder + 0.01 label = "Extract Thumbnail" - families = ["review", "render.farm"] + families = ["review"] hosts = ["nuke"] def process(self, instance): + if "render.farm" in instance.data["families"]: + return with anlib.maintained_selection(): self.log.debug("instance: {}".format(instance)) From f71aad40e04ac934522f25edad1d358bf4a8f5f5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 3 Sep 2020 11:01:09 +0200 Subject: [PATCH 25/92] removed icon attribute --- .../ftrack/ftrack_server/sub_event_status.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/pype/modules/ftrack/ftrack_server/sub_event_status.py b/pype/modules/ftrack/ftrack_server/sub_event_status.py index c2e7b7477f..00a6687de3 100644 --- a/pype/modules/ftrack/ftrack_server/sub_event_status.py +++ b/pype/modules/ftrack/ftrack_server/sub_event_status.py @@ -12,7 +12,7 @@ from pype.modules.ftrack.ftrack_server.lib import ( SocketSession, StatusEventHub, TOPIC_STATUS_SERVER, TOPIC_STATUS_SERVER_RESULT ) -from pype.api import Logger, config +from pype.api import Logger log = Logger().get_logger("Event storer") action_identifier = ( @@ -23,17 +23,7 @@ action_data = { "label": "Pype Admin", "variant": "- Event server Status ({})".format(host_ip), "description": "Get Infromation about event server", - "actionIdentifier": action_identifier, - "icon": "{}/ftrack/action_icons/PypeAdmin.svg".format( - os.environ.get( - "PYPE_STATICS_SERVER", - "http://localhost:{}".format( - config.get_presets().get("services", {}).get( - "rest_api", {} - ).get("default_port", 8021) - ) - ) - ) + "actionIdentifier": action_identifier } From 821a9c2dacae9952830ad7412b717a7885e766eb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 3 Sep 2020 18:07:11 +0200 Subject: [PATCH 26/92] fixed maya icon --- pype/resources/app_icons/maya.png | Bin 41557 -> 122413 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/pype/resources/app_icons/maya.png b/pype/resources/app_icons/maya.png index e84a6a3742f325769d6cf619045d17107d925f3c..95c605f50d6d4459a2c1028922a44affe0d11161 100644 GIT binary patch literal 122413 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 z
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 literal 41557 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 From 1bc174be101002859051445b6013f5415137cdf0 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 8 Sep 2020 09:58:25 +0100 Subject: [PATCH 27/92] Fix comment tag collection and integration. --- .../publish/integrate_hierarchy_ftrack.py | 21 +++++++++++++++++++ .../publish/collect_hierarchy_context.py | 3 +-- .../nukestudio/publish/collect_shots.py | 5 +++-- .../publish/collect_tag_comments.py | 2 +- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py index cc569ce2d1..c4d8f2f705 100644 --- a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py +++ b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py @@ -167,6 +167,27 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): self.session.rollback() six.reraise(tp, value, tb) + # Create notes. + user = self.session.query( + "User where username is \"{}\"".format(self.session.api_user) + ).first() + if user: + for comment in entity_data.get("comments", []): + entity.create_note(comment, user) + else: + self.log.warning( + "Was not able to query current User {}".format( + self.session.api_user + ) + ) + try: + self.session.commit() + except Exception: + tp, value, tb = sys.exc_info() + self.session.rollback() + six.reraise(tp, value, tb) + + # Import children. if 'childs' in entity_data: self.import_to_ftrack( entity_data['childs'], entity) diff --git a/pype/plugins/nukestudio/publish/collect_hierarchy_context.py b/pype/plugins/nukestudio/publish/collect_hierarchy_context.py index a41e987bdb..930efd618e 100644 --- a/pype/plugins/nukestudio/publish/collect_hierarchy_context.py +++ b/pype/plugins/nukestudio/publish/collect_hierarchy_context.py @@ -273,8 +273,6 @@ class CollectHierarchyContext(pyblish.api.ContextPlugin): instance.data["clipOut"] - instance.data["clipIn"]) - - self.log.debug( "__ instance.data[parents]: {}".format( instance.data["parents"] @@ -319,6 +317,7 @@ class CollectHierarchyContext(pyblish.api.ContextPlugin): }) in_info['tasks'] = instance.data['tasks'] + in_info["comments"] = instance.data.get("comments", []) parents = instance.data.get('parents', []) self.log.debug("__ in_info: {}".format(in_info)) diff --git a/pype/plugins/nukestudio/publish/collect_shots.py b/pype/plugins/nukestudio/publish/collect_shots.py index 455e25bf82..7055167143 100644 --- a/pype/plugins/nukestudio/publish/collect_shots.py +++ b/pype/plugins/nukestudio/publish/collect_shots.py @@ -40,11 +40,12 @@ class CollectShots(api.InstancePlugin): data["name"] = data["subset"] + "_" + data["asset"] data["label"] = ( - "{} - {} - tasks:{} - assetbuilds:{}".format( + "{} - {} - tasks: {} - assetbuilds: {} - comments: {}".format( data["asset"], data["subset"], data["tasks"], - [x["name"] for x in data.get("assetbuilds", [])] + [x["name"] for x in data.get("assetbuilds", [])], + len(data.get("comments", [])) ) ) diff --git a/pype/plugins/nukestudio/publish/collect_tag_comments.py b/pype/plugins/nukestudio/publish/collect_tag_comments.py index 1ec98e3d3b..e14e53d439 100644 --- a/pype/plugins/nukestudio/publish/collect_tag_comments.py +++ b/pype/plugins/nukestudio/publish/collect_tag_comments.py @@ -17,7 +17,7 @@ class CollectClipTagComments(api.InstancePlugin): for tag in instance.data["tags"]: if tag["name"].lower() == "comment": instance.data["comments"].append( - tag.metadata().dict()["tag.note"] + tag["metadata"]["tag.note"] ) # Find tags on the source clip. From 6e2dab91b2e6a7c148f28ebdfe6d85c766774807 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 8 Sep 2020 16:06:11 +0100 Subject: [PATCH 28/92] Image plane cache and PNG reprensentations. --- pype/plugins/maya/load/load_image_plane.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/plugins/maya/load/load_image_plane.py b/pype/plugins/maya/load/load_image_plane.py index 17a6866f80..42e7a058ed 100644 --- a/pype/plugins/maya/load/load_image_plane.py +++ b/pype/plugins/maya/load/load_image_plane.py @@ -12,7 +12,7 @@ class ImagePlaneLoader(api.Loader): families = ["plate", "render"] label = "Create imagePlane on selected camera." - representations = ["mov", "exr", "preview"] + representations = ["mov", "exr", "preview", "png"] icon = "image" color = "orange" @@ -81,6 +81,7 @@ class ImagePlaneLoader(api.Loader): image_plane_shape.frameOffset.set(1 - start_frame) image_plane_shape.frameIn.set(start_frame) image_plane_shape.frameOut.set(end_frame) + image_plane_shape.frameCache.set(end_frame) image_plane_shape.useFrameExtension.set(1) movie_representations = ["mov", "preview"] From d6fbbc31e8452e12d17094418f25ac0efefde8c3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 16 Sep 2020 16:03:11 +0200 Subject: [PATCH 29/92] feat(global): add `burnin` suffix only if more burnin profiles active --- pype/plugins/global/publish/extract_burnin.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pype/plugins/global/publish/extract_burnin.py b/pype/plugins/global/publish/extract_burnin.py index 4443cfe223..6e8da1b054 100644 --- a/pype/plugins/global/publish/extract_burnin.py +++ b/pype/plugins/global/publish/extract_burnin.py @@ -195,11 +195,14 @@ class ExtractBurnin(pype.api.Extractor): if "delete" in new_repre["tags"]: new_repre["tags"].remove("delete") - # Update name and outputName to be able have multiple outputs - # Join previous "outputName" with filename suffix - new_name = "_".join([new_repre["outputName"], filename_suffix]) - new_repre["name"] = new_name - new_repre["outputName"] = new_name + if len(repre_burnin_defs.keys()) > 1: + # Update name and outputName to be + # able have multiple outputs in case of more burnin presets + # Join previous "outputName" with filename suffix + new_name = "_".join( + [new_repre["outputName"], filename_suffix]) + new_repre["name"] = new_name + new_repre["outputName"] = new_name # Prepare paths and files for process. self.input_output_paths(new_repre, temp_data, filename_suffix) From 91ac877a1e88bce9acd72218f24156fa6e02d770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 16 Sep 2020 16:05:48 +0200 Subject: [PATCH 30/92] Revert "388 Extract review a representation name with `*_burnin`" --- pype/plugins/global/publish/extract_burnin.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/pype/plugins/global/publish/extract_burnin.py b/pype/plugins/global/publish/extract_burnin.py index 6e8da1b054..4443cfe223 100644 --- a/pype/plugins/global/publish/extract_burnin.py +++ b/pype/plugins/global/publish/extract_burnin.py @@ -195,14 +195,11 @@ class ExtractBurnin(pype.api.Extractor): if "delete" in new_repre["tags"]: new_repre["tags"].remove("delete") - if len(repre_burnin_defs.keys()) > 1: - # Update name and outputName to be - # able have multiple outputs in case of more burnin presets - # Join previous "outputName" with filename suffix - new_name = "_".join( - [new_repre["outputName"], filename_suffix]) - new_repre["name"] = new_name - new_repre["outputName"] = new_name + # Update name and outputName to be able have multiple outputs + # Join previous "outputName" with filename suffix + new_name = "_".join([new_repre["outputName"], filename_suffix]) + new_repre["name"] = new_name + new_repre["outputName"] = new_name # Prepare paths and files for process. self.input_output_paths(new_repre, temp_data, filename_suffix) From 84d5580873ea0b34da3ee65fc339c858dc08d77f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Sep 2020 19:05:12 +0200 Subject: [PATCH 31/92] attribute `is_input_type` renamed to `is_item_type` and added better check --- pype/tools/settings/settings/widgets/item_types.py | 12 ++++++------ pype/tools/settings/settings/widgets/lib.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index e2d59c2e69..ff95ba82c3 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -12,8 +12,8 @@ from avalon.vendor import qtawesome class SettingObject: - # `is_input_type` attribute says if has implemented item type methods - is_input_type = True + # `is_item_type` attribute says if has implemented item type methods + is_item_type = True # each input must have implemented default value for development # when defaults are not filled yet default_input_value = NOT_SET @@ -1935,7 +1935,7 @@ class DictWidget(QtWidgets.QWidget, SettingObject): item_type = child_configuration["type"] klass = TypeToKlass.types.get(item_type) - if not klass.is_input_type: + if not getattr(klass, "is_item_type", False): item = klass(child_configuration, self) self.content_layout.addWidget(item) return item @@ -2226,7 +2226,7 @@ class DictInvisible(QtWidgets.QWidget, SettingObject): item_type = child_configuration["type"] klass = TypeToKlass.types.get(item_type) - if not klass.is_input_type: + if not klass.is_item_type: item = klass(child_configuration, self) self.layout().addWidget(item) return item @@ -3003,7 +3003,7 @@ class DictFormWidget(QtWidgets.QWidget, SettingObject): class LabelWidget(QtWidgets.QWidget): - is_input_type = False + is_item_type = False def __init__(self, configuration, parent=None): super(LabelWidget, self).__init__(parent) @@ -3018,7 +3018,7 @@ class LabelWidget(QtWidgets.QWidget): class SplitterWidget(QtWidgets.QWidget): - is_input_type = False + is_item_type = False _height = 2 def __init__(self, configuration, parent=None): diff --git a/pype/tools/settings/settings/widgets/lib.py b/pype/tools/settings/settings/widgets/lib.py index e225d65417..a2646ad4e6 100644 --- a/pype/tools/settings/settings/widgets/lib.py +++ b/pype/tools/settings/settings/widgets/lib.py @@ -125,7 +125,7 @@ def file_keys_from_schema(schema_data): output = [] item_type = schema_data["type"] klass = TypeToKlass.types[item_type] - if not klass.is_input_type: + if not klass.is_item_type: return output keys = [] @@ -150,7 +150,7 @@ def file_keys_from_schema(schema_data): def validate_all_has_ending_file(schema_data, is_top=True): item_type = schema_data["type"] klass = TypeToKlass.types[item_type] - if not klass.is_input_type: + if not klass.is_item_type: return None if schema_data.get("is_file"): From 20230fbccd10e47abd1fcb130305ddf1b7bafea7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 10:28:17 +0200 Subject: [PATCH 32/92] Revert "attribute `is_input_type` renamed to `is_item_type` and added better check" This reverts commit 84d5580873ea0b34da3ee65fc339c858dc08d77f. --- pype/tools/settings/settings/widgets/item_types.py | 12 ++++++------ pype/tools/settings/settings/widgets/lib.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index ff95ba82c3..e2d59c2e69 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -12,8 +12,8 @@ from avalon.vendor import qtawesome class SettingObject: - # `is_item_type` attribute says if has implemented item type methods - is_item_type = True + # `is_input_type` attribute says if has implemented item type methods + is_input_type = True # each input must have implemented default value for development # when defaults are not filled yet default_input_value = NOT_SET @@ -1935,7 +1935,7 @@ class DictWidget(QtWidgets.QWidget, SettingObject): item_type = child_configuration["type"] klass = TypeToKlass.types.get(item_type) - if not getattr(klass, "is_item_type", False): + if not klass.is_input_type: item = klass(child_configuration, self) self.content_layout.addWidget(item) return item @@ -2226,7 +2226,7 @@ class DictInvisible(QtWidgets.QWidget, SettingObject): item_type = child_configuration["type"] klass = TypeToKlass.types.get(item_type) - if not klass.is_item_type: + if not klass.is_input_type: item = klass(child_configuration, self) self.layout().addWidget(item) return item @@ -3003,7 +3003,7 @@ class DictFormWidget(QtWidgets.QWidget, SettingObject): class LabelWidget(QtWidgets.QWidget): - is_item_type = False + is_input_type = False def __init__(self, configuration, parent=None): super(LabelWidget, self).__init__(parent) @@ -3018,7 +3018,7 @@ class LabelWidget(QtWidgets.QWidget): class SplitterWidget(QtWidgets.QWidget): - is_item_type = False + is_input_type = False _height = 2 def __init__(self, configuration, parent=None): diff --git a/pype/tools/settings/settings/widgets/lib.py b/pype/tools/settings/settings/widgets/lib.py index a2646ad4e6..e225d65417 100644 --- a/pype/tools/settings/settings/widgets/lib.py +++ b/pype/tools/settings/settings/widgets/lib.py @@ -125,7 +125,7 @@ def file_keys_from_schema(schema_data): output = [] item_type = schema_data["type"] klass = TypeToKlass.types[item_type] - if not klass.is_item_type: + if not klass.is_input_type: return output keys = [] @@ -150,7 +150,7 @@ def file_keys_from_schema(schema_data): def validate_all_has_ending_file(schema_data, is_top=True): item_type = schema_data["type"] klass = TypeToKlass.types[item_type] - if not klass.is_item_type: + if not klass.is_input_type: return None if schema_data.get("is_file"): From 828ebde3deea11a28251420d516962be0b0e7bfd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 12:25:47 +0200 Subject: [PATCH 33/92] added attribute to know if item type should be expanded in grid layout --- pype/tools/settings/settings/widgets/item_types.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index ea32d9c79c..ffbb128c3d 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -24,6 +24,8 @@ class SettingObject: # All item types must have implemented Qt signal which is emitted when # it's or it's children value has changed, value_changed = None + # Item will expand to full width in grid layout + expand_in_grid = False def _set_default_attributes(self): """Create and reset attributes required for all item types. @@ -1693,6 +1695,7 @@ class ModifiableDict(QtWidgets.QWidget, InputObject): # Should be used only for dictionary with one datatype as value # TODO this is actually input field (do not care if is group or not) value_changed = QtCore.Signal(object) + expand_in_grid = True def __init__( self, input_data, parent, @@ -1926,6 +1929,7 @@ class ModifiableDict(QtWidgets.QWidget, InputObject): # Dictionaries class DictWidget(QtWidgets.QWidget, SettingObject): value_changed = QtCore.Signal(object) + expand_in_grid = True def __init__( self, input_data, parent, @@ -2256,6 +2260,7 @@ class DictInvisible(QtWidgets.QWidget, SettingObject): # TODO is not overridable by itself value_changed = QtCore.Signal(object) allow_actions = False + expand_in_grid = True def __init__( self, input_data, parent, @@ -2871,6 +2876,7 @@ class FormLabel(QtWidgets.QLabel): class DictFormWidget(QtWidgets.QWidget, SettingObject): value_changed = QtCore.Signal(object) allow_actions = False + expand_in_grid = True def __init__( self, input_data, parent, From 365c6852c0bb0d91ab3248a00d9182ea226ea610 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 12:26:53 +0200 Subject: [PATCH 34/92] using QGridLayout in dict and dict-invisible --- pype/tools/settings/settings/widgets/item_types.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index ffbb128c3d..d745e83ffa 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1969,7 +1969,7 @@ class DictWidget(QtWidgets.QWidget, SettingObject): content_widget = QtWidgets.QWidget(body_widget) content_widget.setObjectName("ContentWidget") content_widget.setProperty("content_state", content_state) - content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout = QtWidgets.QGridLayout(content_widget) content_layout.setContentsMargins(CHILD_OFFSET, 5, 0, bottom_margin) body_widget.set_content_widget(content_widget) @@ -2278,10 +2278,12 @@ class DictInvisible(QtWidgets.QWidget, SettingObject): self.setAttribute(QtCore.Qt.WA_TranslucentBackground) - layout = QtWidgets.QVBoxLayout(self) + layout = QtWidgets.QGridLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(5) + self.content_layout = layout + self.input_fields = [] self.key = input_data["key"] From 5f574c1643bc6db92327ffd8cb9e570ed60adbdd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 12:27:13 +0200 Subject: [PATCH 35/92] items are added to grid layout in right order --- .../settings/settings/widgets/item_types.py | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index d745e83ffa..628b7262e8 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -2001,9 +2001,10 @@ class DictWidget(QtWidgets.QWidget, SettingObject): item_type = child_configuration["type"] klass = TypeToKlass.types.get(item_type) - if not klass.is_input_type: + row = self.content_layout.rowCount() + if not getattr(klass, "is_input_type", False): item = klass(child_configuration, self) - self.content_layout.addWidget(item) + self.content_layout.addWidget(item, row, 0, 1, 2) return item if self.checkbox_key and not self.checkbox_widget: @@ -2011,9 +2012,20 @@ class DictWidget(QtWidgets.QWidget, SettingObject): if key == self.checkbox_key: return self._add_checkbox_child(child_configuration) - item = klass(child_configuration, self) + label_widget = None + if not klass.expand_in_grid: + label = child_configuration.get("label") + if label is not None: + label_widget = QtWidgets.QLabel(label, self) + self.content_layout.addWidget(label_widget, row, 0, 1, 1) + + item = klass(child_configuration, self, label_widget=label_widget) item.value_changed.connect(self._on_value_change) - self.content_layout.addWidget(item) + + if label_widget: + self.content_layout.addWidget(item, row, 1, 1, 1) + else: + self.content_layout.addWidget(item, row, 0, 1, 2) self.input_fields.append(item) return item @@ -2295,16 +2307,27 @@ class DictInvisible(QtWidgets.QWidget, SettingObject): item_type = child_configuration["type"] klass = TypeToKlass.types.get(item_type) - if not klass.is_input_type: + row = self.content_layout.rowCount() + if not getattr(klass, "is_input_type", False): item = klass(child_configuration, self) - self.layout().addWidget(item) + self.content_layout.addWidget(item, row, 0, 1, 2) return item - item = klass(child_configuration, self) - self.layout().addWidget(item) + label_widget = None + if not klass.expand_in_grid: + label = child_configuration.get("label") + if label is not None: + label_widget = QtWidgets.QLabel(label, self) + self.content_layout.addWidget(label_widget, row, 0, 1, 1) + item = klass(child_configuration, self, label_widget=label_widget) item.value_changed.connect(self._on_value_change) + if label_widget: + self.content_layout.addWidget(item, row, 1, 1, 1) + else: + self.content_layout.addWidget(item, row, 0, 1, 2) + self.input_fields.append(item) return item From 3e24555f49b384cc4863db81806e4831dc39741b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 12:27:57 +0200 Subject: [PATCH 36/92] removed dict-form from schemas --- .../projects_schema/1_plugins_gui_schema.json | 83 +++--- .../1_applications_gui_schema.json | 259 +++++++++--------- .../system_schema/1_tools_gui_schema.json | 35 +-- .../system_schema/1_tray_items.json | 91 +++--- 4 files changed, 218 insertions(+), 250 deletions(-) diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/1_plugins_gui_schema.json b/pype/tools/settings/settings/gui_schemas/projects_schema/1_plugins_gui_schema.json index 721b0924e8..b2d7914c84 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/1_plugins_gui_schema.json +++ b/pype/tools/settings/settings/gui_schemas/projects_schema/1_plugins_gui_schema.json @@ -30,34 +30,29 @@ "key": "enabled", "label": "Enabled" }, { - "type": "dict-form", - "children": [ - { - "type": "text", - "key": "deadline_department", - "label": "Deadline apartment" - }, { - "type": "number", - "key": "deadline_priority", - "label": "Deadline priority" - }, { - "type": "text", - "key": "deadline_pool", - "label": "Deadline pool" - }, { - "type": "text", - "key": "deadline_pool_secondary", - "label": "Deadline pool (secondary)" - }, { - "type": "text", - "key": "deadline_group", - "label": "Deadline Group" - }, { - "type": "number", - "key": "deadline_chunk_size", - "label": "Deadline Chunk size" - } - ] + "type": "text", + "key": "deadline_department", + "label": "Deadline apartment" + }, { + "type": "number", + "key": "deadline_priority", + "label": "Deadline priority" + }, { + "type": "text", + "key": "deadline_pool", + "label": "Deadline pool" + }, { + "type": "text", + "key": "deadline_pool_secondary", + "label": "Deadline pool (secondary)" + }, { + "type": "text", + "key": "deadline_group", + "label": "Deadline Group" + }, { + "type": "number", + "key": "deadline_chunk_size", + "label": "Deadline Chunk size" } ] } @@ -531,13 +526,6 @@ "key": "nukestudio", "label": "NukeStudio", "children": [ - { - "type": "raw-json", - "collapsable": true, - "key": "filter", - "label": "Publish GUI Filters", - "is_file": true - }, { "type": "dict", "collapsable": true, @@ -649,21 +637,16 @@ "label": "ffmpeg_args", "children": [ { - "type": "dict-form", - "children": [ - { - "type": "list", - "object_type": "text", - "key": "input", - "label": "input" - }, - { - "type": "list", - "object_type": "text", - "key": "output", - "label": "output" - } - ] + "type": "list", + "object_type": "text", + "key": "input", + "label": "input" + }, + { + "type": "list", + "object_type": "text", + "key": "output", + "label": "output" } ] } diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_applications_gui_schema.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_applications_gui_schema.json index 48f8ecbd7c..3427f98253 100644 --- a/pype/tools/settings/settings/gui_schemas/system_schema/1_applications_gui_schema.json +++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_applications_gui_schema.json @@ -7,138 +7,133 @@ "is_file": true, "children": [ { - "type": "dict-form", - "children": [ - { - "type": "boolean", - "key": "blender_2.80", - "label": "Blender 2.80" - }, { - "type": "boolean", - "key": "blender_2.81", - "label": "Blender 2.81" - }, { - "type": "boolean", - "key": "blender_2.82", - "label": "Blender 2.82" - }, { - "type": "boolean", - "key": "blender_2.83", - "label": "Blender 2.83" - }, { - "type": "boolean", - "key": "celaction_local", - "label": "Celaction Local" - }, { - "type": "boolean", - "key": "celaction_remote", - "label": "Celaction Remote" - }, { - "type": "boolean", - "key": "harmony_17", - "label": "Harmony 17" - }, { - "type": "boolean", - "key": "maya_2017", - "label": "Autodest Maya 2017" - }, { - "type": "boolean", - "key": "maya_2018", - "label": "Autodest Maya 2018" - }, { - "type": "boolean", - "key": "maya_2019", - "label": "Autodest Maya 2019" - }, { - "type": "boolean", - "key": "maya_2020", - "label": "Autodest Maya 2020" - }, { - "key": "nuke_10.0", - "type": "boolean", - "label": "Nuke 10.0" - }, { - "type": "boolean", - "key": "nuke_11.2", - "label": "Nuke 11.2" - }, { - "type": "boolean", - "key": "nuke_11.3", - "label": "Nuke 11.3" - }, { - "type": "boolean", - "key": "nuke_12.0", - "label": "Nuke 12.0" - }, { - "type": "boolean", - "key": "nukex_10.0", - "label": "NukeX 10.0" - }, { - "type": "boolean", - "key": "nukex_11.2", - "label": "NukeX 11.2" - }, { - "type": "boolean", - "key": "nukex_11.3", - "label": "NukeX 11.3" - }, { - "type": "boolean", - "key": "nukex_12.0", - "label": "NukeX 12.0" - }, { - "type": "boolean", - "key": "nukestudio_10.0", - "label": "NukeStudio 10.0" - }, { - "type": "boolean", - "key": "nukestudio_11.2", - "label": "NukeStudio 11.2" - }, { - "type": "boolean", - "key": "nukestudio_11.3", - "label": "NukeStudio 11.3" - }, { - "type": "boolean", - "key": "nukestudio_12.0", - "label": "NukeStudio 12.0" - }, { - "type": "boolean", - "key": "houdini_16", - "label": "Houdini 16" - }, { - "type": "boolean", - "key": "houdini_16.5", - "label": "Houdini 16.5" - }, { - "type": "boolean", - "key": "houdini_17", - "label": "Houdini 17" - }, { - "type": "boolean", - "key": "houdini_18", - "label": "Houdini 18" - }, { - "type": "boolean", - "key": "premiere_2019", - "label": "Premiere 2019" - }, { - "type": "boolean", - "key": "premiere_2020", - "label": "Premiere 2020" - }, { - "type": "boolean", - "key": "resolve_16", - "label": "BM DaVinci Resolve 16" - }, { - "type": "boolean", - "key": "storyboardpro_7", - "label": "Storyboard Pro 7" - }, { - "type": "boolean", - "key": "unreal_4.24", - "label": "Unreal Editor 4.24" - } - ] + "type": "boolean", + "key": "blender_2.80", + "label": "Blender 2.80" + }, { + "type": "boolean", + "key": "blender_2.81", + "label": "Blender 2.81" + }, { + "type": "boolean", + "key": "blender_2.82", + "label": "Blender 2.82" + }, { + "type": "boolean", + "key": "blender_2.83", + "label": "Blender 2.83" + }, { + "type": "boolean", + "key": "celaction_local", + "label": "Celaction Local" + }, { + "type": "boolean", + "key": "celaction_remote", + "label": "Celaction Remote" + }, { + "type": "boolean", + "key": "harmony_17", + "label": "Harmony 17" + }, { + "type": "boolean", + "key": "maya_2017", + "label": "Autodest Maya 2017" + }, { + "type": "boolean", + "key": "maya_2018", + "label": "Autodest Maya 2018" + }, { + "type": "boolean", + "key": "maya_2019", + "label": "Autodest Maya 2019" + }, { + "type": "boolean", + "key": "maya_2020", + "label": "Autodest Maya 2020" + }, { + "key": "nuke_10.0", + "type": "boolean", + "label": "Nuke 10.0" + }, { + "type": "boolean", + "key": "nuke_11.2", + "label": "Nuke 11.2" + }, { + "type": "boolean", + "key": "nuke_11.3", + "label": "Nuke 11.3" + }, { + "type": "boolean", + "key": "nuke_12.0", + "label": "Nuke 12.0" + }, { + "type": "boolean", + "key": "nukex_10.0", + "label": "NukeX 10.0" + }, { + "type": "boolean", + "key": "nukex_11.2", + "label": "NukeX 11.2" + }, { + "type": "boolean", + "key": "nukex_11.3", + "label": "NukeX 11.3" + }, { + "type": "boolean", + "key": "nukex_12.0", + "label": "NukeX 12.0" + }, { + "type": "boolean", + "key": "nukestudio_10.0", + "label": "NukeStudio 10.0" + }, { + "type": "boolean", + "key": "nukestudio_11.2", + "label": "NukeStudio 11.2" + }, { + "type": "boolean", + "key": "nukestudio_11.3", + "label": "NukeStudio 11.3" + }, { + "type": "boolean", + "key": "nukestudio_12.0", + "label": "NukeStudio 12.0" + }, { + "type": "boolean", + "key": "houdini_16", + "label": "Houdini 16" + }, { + "type": "boolean", + "key": "houdini_16.5", + "label": "Houdini 16.5" + }, { + "type": "boolean", + "key": "houdini_17", + "label": "Houdini 17" + }, { + "type": "boolean", + "key": "houdini_18", + "label": "Houdini 18" + }, { + "type": "boolean", + "key": "premiere_2019", + "label": "Premiere 2019" + }, { + "type": "boolean", + "key": "premiere_2020", + "label": "Premiere 2020" + }, { + "type": "boolean", + "key": "resolve_16", + "label": "BM DaVinci Resolve 16" + }, { + "type": "boolean", + "key": "storyboardpro_7", + "label": "Storyboard Pro 7" + }, { + "type": "boolean", + "key": "unreal_4.24", + "label": "Unreal Editor 4.24" } ] } diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_tools_gui_schema.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_tools_gui_schema.json index d9540eeb3e..08b8d13d89 100644 --- a/pype/tools/settings/settings/gui_schemas/system_schema/1_tools_gui_schema.json +++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_tools_gui_schema.json @@ -7,26 +7,21 @@ "is_file": true, "children": [ { - "type": "dict-form", - "children": [ - { - "key": "mtoa_3.0.1", - "type": "boolean", - "label": "Arnold Maya 3.0.1" - }, { - "key": "mtoa_3.1.1", - "type": "boolean", - "label": "Arnold Maya 3.1.1" - }, { - "key": "mtoa_3.2.0", - "type": "boolean", - "label": "Arnold Maya 3.2.0" - }, { - "key": "yeti_2.1.2", - "type": "boolean", - "label": "Yeti 2.1.2" - } - ] + "key": "mtoa_3.0.1", + "type": "boolean", + "label": "Arnold Maya 3.0.1" + }, { + "key": "mtoa_3.1.1", + "type": "boolean", + "label": "Arnold Maya 3.1.1" + }, { + "key": "mtoa_3.2.0", + "type": "boolean", + "label": "Arnold Maya 3.2.0" + }, { + "key": "yeti_2.1.2", + "type": "boolean", + "label": "Yeti 2.1.2" } ] } diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_tray_items.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_tray_items.json index 6da974a415..0d27ccdc4b 100644 --- a/pype/tools/settings/settings/gui_schemas/system_schema/1_tray_items.json +++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_tray_items.json @@ -11,54 +11,49 @@ "type": "dict-invisible", "children": [ { - "type": "dict-form", - "children": [ - { - "type": "boolean", - "key": "User settings", - "label": "User settings" - }, { - "type": "boolean", - "key": "Ftrack", - "label": "Ftrack" - }, { - "type": "boolean", - "key": "Muster", - "label": "Muster" - }, { - "type": "boolean", - "key": "Avalon", - "label": "Avalon" - }, { - "type": "boolean", - "key": "Clockify", - "label": "Clockify" - }, { - "type": "boolean", - "key": "Standalone Publish", - "label": "Standalone Publish" - }, { - "type": "boolean", - "key": "Logging", - "label": "Logging" - }, { - "type": "boolean", - "key": "Idle Manager", - "label": "Idle Manager" - }, { - "type": "boolean", - "key": "Timers Manager", - "label": "Timers Manager" - }, { - "type": "boolean", - "key": "Rest Api", - "label": "Rest Api" - }, { - "type": "boolean", - "key": "Adobe Communicator", - "label": "Adobe Communicator" - } - ] + "type": "boolean", + "key": "User settings", + "label": "User settings" + }, { + "type": "boolean", + "key": "Ftrack", + "label": "Ftrack" + }, { + "type": "boolean", + "key": "Muster", + "label": "Muster" + }, { + "type": "boolean", + "key": "Avalon", + "label": "Avalon" + }, { + "type": "boolean", + "key": "Clockify", + "label": "Clockify" + }, { + "type": "boolean", + "key": "Standalone Publish", + "label": "Standalone Publish" + }, { + "type": "boolean", + "key": "Logging", + "label": "Logging" + }, { + "type": "boolean", + "key": "Idle Manager", + "label": "Idle Manager" + }, { + "type": "boolean", + "key": "Timers Manager", + "label": "Timers Manager" + }, { + "type": "boolean", + "key": "Rest Api", + "label": "Rest Api" + }, { + "type": "boolean", + "key": "Adobe Communicator", + "label": "Adobe Communicator" } ] }, { From 35d7f0b1d28f2c2248a49a2eff5063669ab4b44a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 12:30:37 +0200 Subject: [PATCH 37/92] added Gui schema README --- pype/tools/settings/settings/README.md | 261 +++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 pype/tools/settings/settings/README.md diff --git a/pype/tools/settings/settings/README.md b/pype/tools/settings/settings/README.md new file mode 100644 index 0000000000..db444eb3bd --- /dev/null +++ b/pype/tools/settings/settings/README.md @@ -0,0 +1,261 @@ +# Creating GUI schemas + +## Basic rules +- configurations does not define GUI, but GUI defines configurations! +- output is always json (yaml is not needed for anatomy templates anymore) +- GUI schema has multiple input types, all inputs are represented by a dictionary +- each input may have "input modifiers" (keys in dictionary) that are required or optional + - only required modifier for all input items is key `"type"` which says what type of item it is +- there are special keys across all inputs + - `"is_file"` - this key is for storing pype defaults in `pype` repo + - reasons of existence: developing new schemas does not require to create defaults manually + - key is validated, must be once in hierarchy else it won't be possible to store pype defaults + - `"is_group"` - define that all values under key in hierarchy will be overriden if any value is modified, this information is also stored to overrides + - this keys is not allowed for all inputs as they may have not reason for that + - key is validated, can be only once in hierarchy but is not required +- currently there are `system configurations` and `project configurations` + +## Inner schema +- GUI schemas are huge json files, to be able to split whole configuration into multiple schema there's type `schema` +- system configuration schemas are stored in `~/tools/settings/settings/gui_schemas/system_schema/` and project configurations in `~/tools/settings/settings/gui_schemas/projects_schema/` +- each schema name is filename of json file except extension (without ".json") + +### schema +- can have only key `"children"` which is list of strings, each string should represent another schema (order matters) string represebts name of the schema +- will just paste schemas from other schema file in order of "children" list + +``` +{ + "type": "schema", + "children": [ + "my_schema_name", + "my_other_schema_name" + ] +} +``` + +## Basic Dictionary inputs +- these inputs wraps another inputs into {key: value} relation + +### dict-invisible +- this input gives ability to wrap another inputs but keep them in same widget without visible divider + - this is for example used as first input widget +- has required keys `"key"` and `"children"` + - "children" says what children inputs are underneath + - "key" is key under which will be stored value from it's children +- output is dictionary `{the "key": children values}` +- can't have `"is_group"` key set to True as it breaks visual override showing +``` +{ + "type": "dict-invisible", + "key": "global", + "children": [ + ...ITEMS... + ] +} +``` + +## dict +- this is another dictionary input wrapping more inputs but visually makes them different +- required keys are `"key"` under which will be stored and `"label"` which will be shown in GUI +- this input can be expandable + - that can be set with key `"expandable"` as `True`/`False` (Default: `True`) + - with key `"expanded"` as `True`/`False` can be set that is expanded when GUI is opened (Default: `False`) +- it is possible to add darker background with `"highlight_content"` (Default: `False`) + - darker background has limits of maximum applies after 3-4 nested highlighted items there is not difference in the color +``` +{ + "key": "applications", + "type": "dict", + "label": "Applications", + "expandable": true, + "highlight_content": true, + "is_group": true, + "is_file": true, + "children": [ + ...ITEMS... + ] +} +``` + +## Inputs for setting any kind of value (`Pure` inputs) +- all these input must have defined `"key"` under which will be stored and `"label"` which will be shown next to input + - unless they are used in different types of inputs (later) "as widgets" in that case `"key"` and `"label"` are not required as there is not place where to set them + +### boolean +- simple checkbox, nothing more to set +``` +{ + "type": "boolean", + "key": "my_boolean_key", + "label": "Do you want to use Pype?" +} +``` + +### number +- number input, can be used for both integer and float + - key `"decimal"` defines how many decimal places will be used, 0 is for integer input (Default: `0`) + - key `"minimum"` as minimum allowed number to enter (Default: `-99999`) + - key `"maxium"` as maximum allowed number to enter (Default: `99999`) +``` +{ + "type": "number", + "key": "fps", + "label": "Frame rate (FPS)" + "decimal": 2, + "minimum": 1, + "maximum": 300000 +} +``` + +### text +- simple text input + - key `"multiline"` allows to enter multiple lines of text (Default: `False`) + +``` +{ + "type": "text", + "key": "deadline_pool", + "label": "Deadline pool" +} +``` + +### path-input +- enhanced text input + - does not allow to enter backslash, is auto-converted to forward slash + - may be added another validations, like do not allow end path with slash +- this input is implemented to add additional features to text input +- this is meant to be used in proxy input `path-widget` + - DO NOT USE this input in schema please + +### raw-json +- a little bit enhanced text input for raw json +- has validations of json format + - empty value is invalid value, always must be at least `{}` of `[]` + +``` +{ + "type": "raw-json", + "key": "profiles", + "label": "Extract Review profiles" +} +``` + +## Inputs for setting value using Pure inputs +- these inputs also have required `"key"` and `"label"` +- they use Pure inputs "as widgets" + +### list +- output is list +- items can be added and removed +- items in list must be the same type + - type of items is defined with key `"object_type"` where Pure input name is entered (e.g. `number`) + - because Pure inputs may have modifiers (`number` input has `minimum`, `maximum` and `decimals`) you can set these in key `"input_modifiers"` + +``` +{ + "type": "list", + "object_type": "number", + "key": "exclude_ports", + "label": "Exclude ports", + "input_modifiers": { + "minimum": 1, + "maximum": 65535 + } +} +``` + +### dict-modifiable +- one of dictionary inputs, this is only used as value input +- items in this input can be removed and added same way as in `list` input +- value items in dictionary must be the same type + - type of items is defined with key `"object_type"` where Pure input name is entered (e.g. `number`) + - because Pure inputs may have modifiers (`number` input has `minimum`, `maximum` and `decimals`) you can set these in key `"input_modifiers"` +- this input can be expandable + - that can be set with key `"expandable"` as `True`/`False` (Default: `True`) + - with key `"expanded"` as `True`/`False` can be set that is expanded when GUI is opened (Default: `False`) + +``` +{ + "type": "dict-modifiable", + "object_type": "number", + "input_modifiers": { + "minimum": 0, + "maximum": 300 + }, + "is_group": true, + "key": "templates_mapping", + "label": "Muster - Templates mapping", + "is_file": true +} +``` + +### path-widget +- input for paths, use `path-input` internally +- has 2 input modifiers `"multiplatform"` and `"multipath"` + - `"multiplatform"` - adds `"windows"`, `"linux"` and `"darwin"` path inputs result is dictionary + - `"multipath"` - it is possible to enter multiple paths + - if both are enabled result is dictionary with lists + +``` +{ + "type": "path-widget", + "key": "ffmpeg_path", + "label": "FFmpeg path", + "multiplatform": true, + "multipath": true +} +``` + +## Noninteractive widgets +- have nothing to do with data + +### label +- add label with note or explanations +- it is possible to use html tags inside the label + +``` +{ + "type": "label", + "label": "RED LABEL: Normal label" +} +``` + +### splitter +- visual splitter of items (more divider than splitter) + +``` +{ + "type": "splitter" +} +``` + +## Proxy wrappers +- should wraps multiple inputs only visually +- these does not have `"key"` key and do not allow to have `"is_file"` or `"is_group"` modifiers enabled + +### dict-form +- DEPRECATED + - may be used only in `dict` and `dict-invisible` where is currently used grid layout so form is not needed + - item is kept as still may be used in specific cases +- wraps inputs into form look layout +- should be used only for Pure inputs + +``` +{ + "type": "dict-form", + "children": [ + { + "type": "text", + "key": "deadline_department", + "label": "Deadline apartment" + }, { + "type": "number", + "key": "deadline_priority", + "label": "Deadline priority" + }, { + ... + } + ] +} +``` From b676fc22b6e4991c9c744716d6c354765acfadea Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 13:57:38 +0200 Subject: [PATCH 38/92] order_changed is triggered after new item is added, not before --- .../settings/settings/widgets/item_types.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index ea32d9c79c..9188cd5cb4 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1384,28 +1384,31 @@ class ListWidget(QtWidgets.QWidget, InputObject): item_widget = ListItem( self.object_type, self.input_modifiers, self, self.inputs_widget ) + + previous_field = None + next_field = None + if row is None: if self.input_fields: - self.input_fields[-1].order_changed() + previous_field = self.input_fields[-1] self.inputs_layout.addWidget(item_widget) self.input_fields.append(item_widget) else: - previous_field = None if row > 0: previous_field = self.input_fields[row - 1] - next_field = None max_index = self.count() if row < max_index: next_field = self.input_fields[row] self.inputs_layout.insertWidget(row, item_widget) self.input_fields.insert(row, item_widget) - if previous_field: - previous_field.order_changed() - if next_field: - next_field.order_changed() + if previous_field: + previous_field.order_changed() + + if next_field: + next_field.order_changed() if is_empty: item_widget.set_as_empty() From 1d08c4db454edc4a4524fb571c75efbffa7b8dd6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 17:22:28 +0200 Subject: [PATCH 39/92] content of modifiable dictionary can be also highlighted --- .../settings/settings/widgets/item_types.py | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 4088537a9a..72200f5a95 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1715,21 +1715,22 @@ class ModifiableDict(QtWidgets.QWidget, InputObject): self.key = input_data["key"] + if input_data.get("highlight_content", False): + content_state = "hightlighted" + bottom_margin = 5 + else: + content_state = "" + bottom_margin = 0 + main_layout = QtWidgets.QHBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) - content_widget = QtWidgets.QWidget(self) - content_layout = QtWidgets.QVBoxLayout(content_widget) - content_layout.setContentsMargins(CHILD_OFFSET, 3, 0, 3) - if as_widget: - main_layout.addWidget(content_widget) body_widget = None else: body_widget = ExpandingWidget(input_data["label"], self) main_layout.addWidget(body_widget) - body_widget.set_content_widget(content_widget) self.body_widget = body_widget self.label_widget = body_widget.label_widget @@ -1743,6 +1744,22 @@ class ModifiableDict(QtWidgets.QWidget, InputObject): else: body_widget.hide_toolbox(hide_content=False) + if body_widget is None: + content_parent_widget = self + else: + content_parent_widget = body_widget + + content_widget = QtWidgets.QWidget(content_parent_widget) + content_widget.setObjectName("ContentWidget") + content_widget.setProperty("content_state", content_state) + content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout.setContentsMargins(CHILD_OFFSET, 3, 0, bottom_margin) + + if body_widget is None: + main_layout.addWidget(content_widget) + else: + body_widget.set_content_widget(content_widget) + self.body_widget = body_widget self.content_widget = content_widget self.content_layout = content_layout From ef6c68e89431456ecf4430f9561dae80e64c453a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 17:24:39 +0200 Subject: [PATCH 40/92] align labels in dict and dict-invisible to top right --- pype/tools/settings/settings/widgets/item_types.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 72200f5a95..c8d558e044 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -2037,7 +2037,10 @@ class DictWidget(QtWidgets.QWidget, SettingObject): label = child_configuration.get("label") if label is not None: label_widget = QtWidgets.QLabel(label, self) - self.content_layout.addWidget(label_widget, row, 0, 1, 1) + self.content_layout.addWidget( + label_widget, row, 0, 1, 1, + alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignTop + ) item = klass(child_configuration, self, label_widget=label_widget) item.value_changed.connect(self._on_value_change) @@ -2338,7 +2341,10 @@ class DictInvisible(QtWidgets.QWidget, SettingObject): label = child_configuration.get("label") if label is not None: label_widget = QtWidgets.QLabel(label, self) - self.content_layout.addWidget(label_widget, row, 0, 1, 1) + self.content_layout.addWidget( + label_widget, row, 0, 1, 1, + alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignTop + ) item = klass(child_configuration, self, label_widget=label_widget) item.value_changed.connect(self._on_value_change) From 4934f65432f9e030c88b346adab7571b2b06d3a8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 17:26:00 +0200 Subject: [PATCH 41/92] implemented basic dict-item item type --- pype/tools/settings/settings/style/style.css | 6 + .../settings/settings/widgets/item_types.py | 116 ++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/pype/tools/settings/settings/style/style.css b/pype/tools/settings/settings/style/style.css index 38f69fef50..221f297219 100644 --- a/pype/tools/settings/settings/style/style.css +++ b/pype/tools/settings/settings/style/style.css @@ -152,6 +152,12 @@ QPushButton[btn-type="expand-toggle"] { background: #141a1f; } +#DictItemWidgetBody{ + background: transparent; + border: 2px solid #cccccc; + border-radius: 5px; +} + #SplitterItem { background-color: #1d272f; } diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index c8d558e044..31455ecd98 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1149,6 +1149,121 @@ class RawJsonWidget(QtWidgets.QWidget, InputObject): return self.text_input.json_value() +class DictItemWidget(QtWidgets.QWidget, SettingObject): + default_input_value = True + value_changed = QtCore.Signal(object) + + def __init__( + self, input_data, parent, + as_widget=False, label_widget=None, parent_widget=None + ): + if parent_widget is None: + parent_widget = parent + super(DictItemWidget, self).__init__(parent_widget) + + self.initial_attributes(input_data, parent, as_widget) + + if not self._as_widget: + raise TypeError("{} can be used only as widget.".format( + self.__class__.__name__ + )) + + self.input_fields = [] + + body = QtWidgets.QWidget(self) + body.setObjectName("DictItemWidgetBody") + + content_layout = QtWidgets.QGridLayout(body) + content_layout.setContentsMargins(5, 5, 5, 5) + self.content_layout = content_layout + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + layout.addWidget(body) + + for child_configuration in input_data["children"]: + self.add_children_gui(child_configuration) + + def add_children_gui(self, child_configuration): + item_type = child_configuration["type"] + klass = TypeToKlass.types.get(item_type) + + row = self.content_layout.rowCount() + if not getattr(klass, "is_input_type", False): + item = klass(child_configuration, self) + self.content_layout.addWidget(item, row, 0, 1, 2) + return item + + label_widget = None + if not klass.expand_in_grid: + label = child_configuration.get("label") + if label is not None: + label_widget = QtWidgets.QLabel(label, self) + self.content_layout.addWidget( + label_widget, row, 0, 1, 1, + alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignTop + ) + + item = klass(child_configuration, self, label_widget=label_widget) + item.value_changed.connect(self._on_value_change) + + if label_widget: + self.content_layout.addWidget(item, row, 1, 1, 1) + else: + self.content_layout.addWidget(item, row, 0, 1, 2) + + self.input_fields.append(item) + return item + + def hierarchical_style_update(self): + print("hierarchical_style_update") + + def _on_value_change(self, item=None): + print("_on_value_change") + + def set_value(self, value): + # Ignore value change because if `self.isChecked()` has same + # value as `value` the `_on_value_change` is not triggered + self.checkbox.setChecked(value) + + def update_style(self): + if self._as_widget: + if not self.isEnabled(): + state = self.style_state(False, False, False, False) + else: + state = self.style_state( + False, + self._is_invalid, + False, + self._is_modified + ) + else: + state = self.style_state( + self.has_studio_override, + self.is_invalid, + self.is_overriden, + self.is_modified + ) + if self._state == state: + return + + if self._as_widget: + property_name = "input-state" + else: + property_name = "state" + + self.label_widget.setProperty(property_name, state) + self.label_widget.style().polish(self.label_widget) + self._state = state + + def item_value(self): + output = {} + for input_field in self.input_fields: + output.update(input_field.config_value()) + return output + + class ListItem(QtWidgets.QWidget, SettingObject): _btn_size = 20 value_changed = QtCore.Signal(object) @@ -3159,6 +3274,7 @@ TypeToKlass.types["path-input"] = PathInputWidget TypeToKlass.types["raw-json"] = RawJsonWidget TypeToKlass.types["list"] = ListWidget TypeToKlass.types["dict-modifiable"] = ModifiableDict +TypeToKlass.types["dict-item"] = DictItemWidget TypeToKlass.types["dict"] = DictWidget TypeToKlass.types["dict-invisible"] = DictInvisible TypeToKlass.types["path-widget"] = PathWidget From 5621f029d0a55f64c7aa924710ea17479009f956 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 17:31:25 +0200 Subject: [PATCH 42/92] hode item in ListItem if is set as empty --- .../settings/settings/widgets/item_types.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 31455ecd98..ec75272f07 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1307,9 +1307,6 @@ class ListItem(QtWidgets.QWidget, SettingObject): self.up_btn.setProperty("btn-type", "tool-item") self.down_btn.setProperty("btn-type", "tool-item") - layout.addWidget(self.add_btn, 0) - layout.addWidget(self.remove_btn, 0) - self.add_btn.clicked.connect(self._on_add_clicked) self.remove_btn.clicked.connect(self._on_remove_clicked) self.up_btn.clicked.connect(self._on_up_clicked) @@ -1322,7 +1319,15 @@ class ListItem(QtWidgets.QWidget, SettingObject): as_widget=True, label_widget=None ) + + self.spacer_widget = QtWidgets.QWidget(self) + self.spacer_widget.setVisible(False) + + layout.addWidget(self.add_btn, 0) + layout.addWidget(self.remove_btn, 0) + layout.addWidget(self.value_input, 1) + layout.addWidget(self.spacer_widget, 1) layout.addWidget(self.up_btn, 0) layout.addWidget(self.down_btn, 0) @@ -1330,8 +1335,11 @@ class ListItem(QtWidgets.QWidget, SettingObject): self.value_input.value_changed.connect(self._on_value_change) def set_as_empty(self, is_empty=True): - self.value_input.setEnabled(not is_empty) - self.remove_btn.setEnabled(not is_empty) + self.spacer_widget.setVisible(is_empty) + self.value_input.setVisible(not is_empty) + self.remove_btn.setVisible(not is_empty) + self.up_btn.setVisible(not is_empty) + self.down_btn.setVisible(not is_empty) self.order_changed() self._on_value_change() @@ -1364,7 +1372,7 @@ class ListItem(QtWidgets.QWidget, SettingObject): return len(self._parent.input_fields) def _on_add_clicked(self): - if self.value_input.isEnabled(): + if self.value_input.isVisible(): self._parent.add_row(row=self.row() + 1) else: self.set_as_empty(False) From cd41394f1cf9efec6799a852655f32564888e7aa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 17:40:26 +0200 Subject: [PATCH 43/92] keep visible minus button and added same feature to modifable dict --- .../tools/settings/settings/widgets/item_types.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index ec75272f07..293b2f5503 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1321,6 +1321,7 @@ class ListItem(QtWidgets.QWidget, SettingObject): ) self.spacer_widget = QtWidgets.QWidget(self) + self.spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) self.spacer_widget.setVisible(False) layout.addWidget(self.add_btn, 0) @@ -1337,7 +1338,7 @@ class ListItem(QtWidgets.QWidget, SettingObject): def set_as_empty(self, is_empty=True): self.spacer_widget.setVisible(is_empty) self.value_input.setVisible(not is_empty) - self.remove_btn.setVisible(not is_empty) + self.remove_btn.setEnabled(not is_empty) self.up_btn.setVisible(not is_empty) self.down_btn.setVisible(not is_empty) self.order_changed() @@ -1692,9 +1693,14 @@ class ModifiableDictItem(QtWidgets.QWidget, SettingObject): self.add_btn.setProperty("btn-type", "tool-item") self.remove_btn.setProperty("btn-type", "tool-item") + self.spacer_widget = QtWidgets.QWidget(self) + self.spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + self.spacer_widget.setVisible(False) + layout.addWidget(self.add_btn, 0) layout.addWidget(self.remove_btn, 0) layout.addWidget(self.key_input, 0) + layout.addWidget(self.spacer_widget, 1) layout.addWidget(self.value_input, 1) self.setFocusProxy(self.value_input) @@ -1713,7 +1719,7 @@ class ModifiableDictItem(QtWidgets.QWidget, SettingObject): return self.key_input.text() def _is_enabled(self): - return self.key_input.isEnabled() + return self.key_input.isVisible() def is_key_invalid(self): if not self._is_enabled(): @@ -1759,9 +1765,10 @@ class ModifiableDictItem(QtWidgets.QWidget, SettingObject): self._parent.remove_row(self) def set_as_empty(self, is_empty=True): - self.key_input.setEnabled(not is_empty) - self.value_input.setEnabled(not is_empty) + self.key_input.setVisible(not is_empty) + self.value_input.setVisible(not is_empty) self.remove_btn.setEnabled(not is_empty) + self.spacer_widget.setVisible(is_empty) self._on_value_change() @property From 04977d223e2cb301c4f6bef5b76ba39a191fe374 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 17:45:12 +0200 Subject: [PATCH 44/92] hide up/down btns if there is only one item --- pype/tools/settings/settings/widgets/item_types.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 293b2f5503..f0a26227a1 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1348,10 +1348,15 @@ class ListItem(QtWidgets.QWidget, SettingObject): row = self.row() parent_row_count = self.parent_rows_count() if parent_row_count == 1: - self.up_btn.setEnabled(False) - self.down_btn.setEnabled(False) + self.up_btn.setVisible(False) + self.down_btn.setVisible(False) + return - elif row == 0: + if not self.up_btn.isVisible(): + self.up_btn.setVisible(True) + self.down_btn.setVisible(True) + + if row == 0: self.up_btn.setEnabled(False) self.down_btn.setEnabled(True) From 4fa53fd2e3c996ceba410af4f7af140a8c4f1f56 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 18:55:17 +0200 Subject: [PATCH 45/92] added few attributes to bases --- pype/tools/settings/settings/widgets/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/tools/settings/settings/widgets/base.py b/pype/tools/settings/settings/widgets/base.py index dbcc380daf..423380d54c 100644 --- a/pype/tools/settings/settings/widgets/base.py +++ b/pype/tools/settings/settings/widgets/base.py @@ -34,6 +34,8 @@ class SystemWidget(QtWidgets.QWidget): is_overidable = False has_studio_override = _has_studio_override = False is_overriden = _is_overriden = False + as_widget = _as_widget = False + any_parent_as_widget = _any_parent_as_widget = False is_group = _is_group = False any_parent_is_group = _any_parent_is_group = False @@ -396,6 +398,8 @@ class ProjectListWidget(QtWidgets.QWidget): class ProjectWidget(QtWidgets.QWidget): has_studio_override = _has_studio_override = False is_overriden = _is_overriden = False + as_widget = _as_widget = False + any_parent_as_widget = _any_parent_as_widget = False is_group = _is_group = False any_parent_is_group = _any_parent_is_group = False From c88e48f1af61d6f9c8598ffaa2c67739c8831e6d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 18:55:52 +0200 Subject: [PATCH 46/92] removed not used method --- pype/tools/settings/settings/widgets/item_types.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index f0a26227a1..d09fd3d1ee 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -502,10 +502,6 @@ class SettingObject: "Method `item_value` not implemented!" ) - def studio_value(self): - """Output for saving changes or overrides.""" - return {self.key: self.item_value()} - class InputObject(SettingObject): """Class for inputs with pre-implemented methods. From 3544d819051465d911edeff4fa49d0f054893b92 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 19:23:14 +0200 Subject: [PATCH 47/92] added any_parent_as_widget attribute --- .../settings/settings/widgets/item_types.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index d09fd3d1ee..07f57c994d 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -46,6 +46,7 @@ class SettingObject: self._as_widget = False self._is_group = False + self._any_parent_as_widget = None self._any_parent_is_group = None # Parent input @@ -81,6 +82,12 @@ class SettingObject: # TODO not implemented yet self._is_nullable = input_data.get("is_nullable", False) + any_parent_as_widget = parent.as_widget + if not any_parent_as_widget: + any_parent_as_widget = parent.any_parent_as_widget + + self._any_parent_as_widget = any_parent_as_widget + any_parent_is_group = parent.is_group if not any_parent_is_group: any_parent_is_group = parent.any_parent_is_group @@ -130,6 +137,34 @@ class SettingObject: """ return self._has_studio_override or self._parent.has_studio_override + @property + def as_widget(self): + """Item is used as widget in parent item. + + Returns: + bool + + """ + return self._as_widget + + @property + def any_parent_as_widget(self): + """Any parent of item is used as widget. + + Attribute holding this information is set during creation and + stored to `_any_parent_as_widget`. + + Why is this information useful: If any parent is used as widget then + modifications and override are not important for whole part. + + Returns: + bool + + """ + if self._any_parent_as_widget is None: + return super(SettingObject, self).any_parent_as_widget + return self._any_parent_as_widget + @property def is_group(self): """Item represents key that can be overriden. From ccfec4759f8202be4c49ff8f345d1d3757afd9f1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 19:24:17 +0200 Subject: [PATCH 48/92] added attribute is empty to list item --- pype/tools/settings/settings/widgets/item_types.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 07f57c994d..b013dd24f7 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1306,6 +1306,7 @@ class ListItem(QtWidgets.QWidget, SettingObject): self._parent = config_parent self._any_parent_is_group = True + self._is_empty = False layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) @@ -1367,6 +1368,8 @@ class ListItem(QtWidgets.QWidget, SettingObject): self.value_input.value_changed.connect(self._on_value_change) def set_as_empty(self, is_empty=True): + self._is_empty = is_empty + self.spacer_widget.setVisible(is_empty) self.value_input.setVisible(not is_empty) self.remove_btn.setEnabled(not is_empty) @@ -1409,7 +1412,7 @@ class ListItem(QtWidgets.QWidget, SettingObject): return len(self._parent.input_fields) def _on_add_clicked(self): - if self.value_input.isVisible(): + if self._is_empty: self._parent.add_row(row=self.row() + 1) else: self.set_as_empty(False) @@ -1426,7 +1429,7 @@ class ListItem(QtWidgets.QWidget, SettingObject): self._parent.swap_rows(row, row + 1) def config_value(self): - if self.value_input.isEnabled(): + if not self._is_empty: return self.value_input.item_value() return NOT_SET From e0e7b29f4b2d4494ff6e3ad918d45359ae2de179 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 19:27:18 +0200 Subject: [PATCH 49/92] added _is_empty to ModifiableDictItem too --- .../settings/settings/widgets/item_types.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index b013dd24f7..71d6d2370d 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1706,6 +1706,7 @@ class ModifiableDictItem(QtWidgets.QWidget, SettingObject): self._set_default_attributes() self._parent = config_parent + self._is_empty = False self.is_key_duplicated = False layout = QtWidgets.QHBoxLayout(self) @@ -1757,11 +1758,8 @@ class ModifiableDictItem(QtWidgets.QWidget, SettingObject): def key_value(self): return self.key_input.text() - def _is_enabled(self): - return self.key_input.isVisible() - def is_key_invalid(self): - if not self._is_enabled(): + if self._is_empty: return False if self.key_value() == "": @@ -1795,15 +1793,17 @@ class ModifiableDictItem(QtWidgets.QWidget, SettingObject): return self._parent.is_group def on_add_clicked(self): - if self._is_enabled(): - self._parent.add_row(row=self.row() + 1) - else: + if self._is_empty: self.set_as_empty(False) + else: + self._parent.add_row(row=self.row() + 1) def on_remove_clicked(self): self._parent.remove_row(self) def set_as_empty(self, is_empty=True): + self._is_empty = is_empty + self.key_input.setVisible(not is_empty) self.value_input.setVisible(not is_empty) self.remove_btn.setEnabled(not is_empty) @@ -1830,13 +1830,13 @@ class ModifiableDictItem(QtWidgets.QWidget, SettingObject): @property def is_invalid(self): - if not self._is_enabled(): + if self._is_empty: return False return self.is_key_invalid() or self.value_input.is_invalid def update_style(self): state = "" - if self._is_enabled(): + if not self._is_empty: if self.is_key_invalid(): state = "invalid" elif self.is_key_modified(): @@ -1854,9 +1854,9 @@ class ModifiableDictItem(QtWidgets.QWidget, SettingObject): return {key: value} def config_value(self): - if self._is_enabled(): - return self.item_value() - return {} + if self._is_empty: + return {} + return self.item_value() def mouseReleaseEvent(self, event): return QtWidgets.QWidget.mouseReleaseEvent(self, event) From 180626ec2d0072779a75a9e678cba5bb388f619c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 19:27:44 +0200 Subject: [PATCH 50/92] implemented few methods in dict-item --- .../settings/settings/widgets/item_types.py | 44 ++++--------------- 1 file changed, 9 insertions(+), 35 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 71d6d2370d..875965db32 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1248,45 +1248,19 @@ class DictItemWidget(QtWidgets.QWidget, SettingObject): return item def hierarchical_style_update(self): - print("hierarchical_style_update") + for input_field in self.input_fields: + input_field.hierarchical_style_update() def _on_value_change(self, item=None): - print("_on_value_change") + self.value_changed.emit(self) - def set_value(self, value): - # Ignore value change because if `self.isChecked()` has same - # value as `value` the `_on_value_change` is not triggered - self.checkbox.setChecked(value) + def update_default_values(self, parent_values): + for input_field in self.input_fields: + input_field.update_default_values(parent_values) - def update_style(self): - if self._as_widget: - if not self.isEnabled(): - state = self.style_state(False, False, False, False) - else: - state = self.style_state( - False, - self._is_invalid, - False, - self._is_modified - ) - else: - state = self.style_state( - self.has_studio_override, - self.is_invalid, - self.is_overriden, - self.is_modified - ) - if self._state == state: - return - - if self._as_widget: - property_name = "input-state" - else: - property_name = "state" - - self.label_widget.setProperty(property_name, state) - self.label_widget.style().polish(self.label_widget) - self._state = state + def update_studio_values(self, parent_values): + for input_field in self.input_fields: + input_field.update_studio_values(parent_values) def item_value(self): output = {} From 127426e7864b03459c22df17eea7d6b8fb2d24f2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 19:31:22 +0200 Subject: [PATCH 51/92] added apply_overrides to widget item --- pype/tools/settings/settings/widgets/item_types.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 875965db32..d4faaedcf6 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1262,6 +1262,10 @@ class DictItemWidget(QtWidgets.QWidget, SettingObject): for input_field in self.input_fields: input_field.update_studio_values(parent_values) + def apply_overrides(self, parent_values): + for input_field in self.input_fields: + input_field.apply_overrides(parent_values) + def item_value(self): output = {} for input_field in self.input_fields: From 02ba2a6f16b5304b2a9c1b724adbdf801f77ef98 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 19:32:17 +0200 Subject: [PATCH 52/92] using more attribute as_widget and any_parent_as_widget --- .../settings/settings/widgets/item_types.py | 78 +++++++++++++++---- 1 file changed, 61 insertions(+), 17 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index d4faaedcf6..9275ccbecb 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -198,6 +198,9 @@ class SettingObject: @property def is_modified(self): """Has object any changes that require saving.""" + if self.any_parent_as_widget: + return self._is_modified + if self._is_modified or self.defaults_not_set: return True @@ -626,10 +629,11 @@ class InputObject(SettingObject): if self.ignore_value_changes: return - if self.is_overidable: - self._is_overriden = True - else: - self._has_studio_override = True + if not self.any_parent_as_widget: + if self.is_overidable: + self._is_overriden = True + else: + self._has_studio_override = True if self._is_invalid: self._is_modified = True @@ -645,12 +649,18 @@ class InputObject(SettingObject): self.value_changed.emit(self) def studio_overrides(self): - if not self.has_studio_override: + if ( + not (self.as_widget or self.any_parent_as_widget) + and not self.has_studio_override + ): return NOT_SET, False return self.config_value(), self.is_group def overrides(self): - if not self.is_overriden: + if ( + not (self.as_widget or self.any_parent_as_widget) + and not self.is_overriden + ): return NOT_SET, False return self.config_value(), self.is_group @@ -1213,6 +1223,8 @@ class DictItemWidget(QtWidgets.QWidget, SettingObject): layout.setSpacing(5) layout.addWidget(body) + self.label_widget = label_widget + for child_configuration in input_data["children"]: self.add_children_gui(child_configuration) @@ -1345,6 +1357,14 @@ class ListItem(QtWidgets.QWidget, SettingObject): self.value_input.value_changed.connect(self._on_value_change) + @property + def as_widget(self): + return self._parent.as_widget + + @property + def any_parent_as_widget(self): + return self.as_widget or self._parent.any_parent_as_widget + def set_as_empty(self, is_empty=True): self._is_empty = is_empty @@ -1684,6 +1704,13 @@ class ModifiableDictItem(QtWidgets.QWidget, SettingObject): self._set_default_attributes() self._parent = config_parent + any_parent_as_widget = config_parent.as_widget + if not any_parent_as_widget: + any_parent_as_widget = config_parent.any_parent_as_widget + + self._any_parent_as_widget = any_parent_as_widget + self._any_parent_is_group = True + self._is_empty = False self.is_key_duplicated = False @@ -2303,7 +2330,7 @@ class DictWidget(QtWidgets.QWidget, SettingObject): if self.ignore_value_changes: return - if self.is_group: + if self.is_group and not self.any_parent_as_widget: if self.is_overidable: self._is_overriden = True else: @@ -2406,7 +2433,11 @@ class DictWidget(QtWidgets.QWidget, SettingObject): return output def studio_overrides(self): - if not self.has_studio_override and not self.child_has_studio_override: + if ( + not (self.as_widget or self.any_parent_as_widget) + and not self.has_studio_override + and not self.child_has_studio_override + ): return NOT_SET, False values = {} @@ -2556,7 +2587,7 @@ class DictInvisible(QtWidgets.QWidget, SettingObject): if self.ignore_value_changes: return - if self.is_group: + if self.is_group and not self.any_parent_as_widget: if self.is_overidable: self._is_overriden = True else: @@ -2653,7 +2684,11 @@ class DictInvisible(QtWidgets.QWidget, SettingObject): self._was_overriden = bool(self._is_overriden) def studio_overrides(self): - if not self.has_studio_override and not self.child_has_studio_override: + if ( + not (self.as_widget or self.any_parent_as_widget) + and not self.has_studio_override + and not self.child_has_studio_override + ): return NOT_SET, False values = {} @@ -2916,10 +2951,11 @@ class PathWidget(QtWidgets.QWidget, SettingObject): if self.ignore_value_changes: return - if self.is_overidable: - self._is_overriden = True - else: - self._has_studio_override = True + if not self.any_parent_as_widget: + if self.is_overidable: + self._is_overriden = True + else: + self._has_studio_override = True if self._is_invalid: self._is_modified = True @@ -3046,7 +3082,11 @@ class PathWidget(QtWidgets.QWidget, SettingObject): return output def studio_overrides(self): - if not self.has_studio_override and not self.child_has_studio_override: + if ( + not (self.as_widget or self.any_parent_as_widget) + and not self.has_studio_override + and not self.child_has_studio_override + ): return NOT_SET, False value = self.item_value() @@ -3236,7 +3276,11 @@ class DictFormWidget(QtWidgets.QWidget, SettingObject): return self.item_value() def studio_overrides(self): - if not self.has_studio_override and not self.child_has_studio_override: + if ( + not (self.as_widget or self.any_parent_as_widget) + and not self.has_studio_override + and not self.child_has_studio_override + ): return NOT_SET, False values = {} @@ -3310,7 +3354,7 @@ TypeToKlass.types["dict-item"] = DictItemWidget TypeToKlass.types["dict"] = DictWidget TypeToKlass.types["dict-invisible"] = DictInvisible TypeToKlass.types["path-widget"] = PathWidget -TypeToKlass.types["dict-form"] = DictFormWidget +TypeToKlass.types["form"] = DictFormWidget TypeToKlass.types["label"] = LabelWidget TypeToKlass.types["splitter"] = SplitterWidget From 2517f13266d3ccffed41e1323c31daf6b084b8f9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 19:32:47 +0200 Subject: [PATCH 53/92] ExtractReview plugin converted to use new DictItemWidget --- .../projects_schema/1_plugins_gui_schema.json | 89 ++++++++++++++++++- 1 file changed, 87 insertions(+), 2 deletions(-) diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/1_plugins_gui_schema.json b/pype/tools/settings/settings/gui_schemas/projects_schema/1_plugins_gui_schema.json index b2d7914c84..f70495017e 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/1_plugins_gui_schema.json +++ b/pype/tools/settings/settings/gui_schemas/projects_schema/1_plugins_gui_schema.json @@ -169,9 +169,94 @@ "key": "enabled", "label": "Enabled" }, { - "type": "raw-json", + "type": "list", "key": "profiles", - "label": "Profiles" + "label": "Profiles", + "object_type": "dict-item", + "input_modifiers": { + "children": [ + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, { + "key": "hosts", + "label": "Hosts", + "type": "list", + "object_type": "text" + }, { + "type": "splitter" + }, { + "key": "outputs", + "label": "Output Definitions", + "type": "dict-modifiable", + "highlight_content": true, + "object_type": "dict-item", + "input_modifiers": { + "children": [ + { + "key": "ext", + "label": "Output extension", + "type": "text" + }, { + "key": "tags", + "label": "Tags", + "type": "list", + "object_type": "text" + }, { + "key": "ffmpeg_args", + "label": "FFmpeg arguments", + "type": "dict", + "highlight_content": true, + "children": [ + { + "key": "video_filters", + "label": "Video filters", + "type": "list", + "object_type": "text" + }, { + "type": "splitter" + }, { + "key": "audio_filters", + "label": "Audio filters", + "type": "list", + "object_type": "text" + }, { + "type": "splitter" + }, { + "key": "input", + "label": "Input arguments", + "type": "list", + "object_type": "text" + }, { + "type": "splitter" + }, { + "key": "output", + "label": "Output arguments", + "type": "list", + "object_type": "text" + } + ] + }, { + "key": "filter", + "label": "Additional output filtering", + "type": "dict", + "highlight_content": true, + "children": [ + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + } + ] + } + ] + } + } + ] + } } ] }, { From fae0b851281b5e2fc55085c6f3cff7ee2b262c6b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 19:34:36 +0200 Subject: [PATCH 54/92] fixed list item again --- pype/tools/settings/settings/widgets/item_types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 9275ccbecb..ea93acddb2 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1411,9 +1411,9 @@ class ListItem(QtWidgets.QWidget, SettingObject): def _on_add_clicked(self): if self._is_empty: - self._parent.add_row(row=self.row() + 1) - else: self.set_as_empty(False) + else: + self._parent.add_row(row=self.row() + 1) def _on_remove_clicked(self): self._parent.remove_row(self) From db928a85baf01b488ec5c2117a9f2e9055c53641 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Sep 2020 19:37:28 +0200 Subject: [PATCH 55/92] make sure inputs won't affect it's potential children o remove project overrides --- pype/tools/settings/settings/widgets/item_types.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index ea93acddb2..d1fd219258 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -668,6 +668,8 @@ class InputObject(SettingObject): self.update_style() def remove_overrides(self): + self._is_overriden = False + self._is_modified = False if self.has_studio_override: self.set_value(self.studio_value) else: From bad080ee3ac877ddade28c3441699ed0807b5f19 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Sep 2020 09:53:04 +0200 Subject: [PATCH 56/92] modified defaults --- .../plugins/global/publish.json | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/pype/settings/defaults/project_settings/plugins/global/publish.json b/pype/settings/defaults/project_settings/plugins/global/publish.json index b946ac4b32..0a7f6fbf3d 100644 --- a/pype/settings/defaults/project_settings/plugins/global/publish.json +++ b/pype/settings/defaults/project_settings/plugins/global/publish.json @@ -19,30 +19,30 @@ "hosts": [], "outputs": { "h264": { - "filter": { - "families": [ - "render", - "review", - "ftrack" - ] - }, "ext": "mp4", + "tags": [ + "burnin", + "ftrackreview" + ], "ffmpeg_args": { + "video_filters": [], + "audio_filters": [], "input": [ "-gamma 2.2" ], - "video_filters": [], - "audio_filters": [], "output": [ "-pix_fmt yuv420p", "-crf 18", "-intra" ] }, - "tags": [ - "burnin", - "ftrackreview" - ] + "filter": { + "families": [ + "render", + "review", + "ftrack" + ] + } } } } From dba17756003a1f554f07ce2a90b70da7d2192231 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Sep 2020 11:14:38 +0200 Subject: [PATCH 57/92] ApplicationAction has pype logger --- pype/lib.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index 601c85f521..73b47b8594 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -19,7 +19,7 @@ from abc import ABCMeta, abstractmethod from avalon import io, pipeline import six import avalon.api -from .api import config, Anatomy +from .api import config, Anatomy, Logger log = logging.getLogger(__name__) @@ -1622,7 +1622,7 @@ class ApplicationAction(avalon.api.Action): parsed application `.toml` this can launch the application. """ - + _log = None config = None group = None variant = None @@ -1632,6 +1632,12 @@ class ApplicationAction(avalon.api.Action): "AVALON_TASK" ) + @property + def log(self): + if self._log is None: + self._log = Logger().get_logger(self.__class__.__name__) + return self._log + def is_compatible(self, session): for key in self.required_session_keys: if key not in session: From b326855be0656047ac508dad3a118c36a212dbb9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Sep 2020 11:15:04 +0200 Subject: [PATCH 58/92] Application also change ftrack status and trigger ftrack timer --- pype/lib.py | 161 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 160 insertions(+), 1 deletion(-) diff --git a/pype/lib.py b/pype/lib.py index 73b47b8594..6fa204b379 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1650,6 +1650,165 @@ class ApplicationAction(avalon.api.Action): project_name = session["AVALON_PROJECT"] asset_name = session["AVALON_ASSET"] task_name = session["AVALON_TASK"] - return launch_application( + launch_application( project_name, asset_name, task_name, self.name ) + + self._ftrack_after_launch_procedure( + project_name, asset_name, task_name + ) + + def _ftrack_after_launch_procedure( + self, project_name, asset_name, task_name + ): + # TODO move to launch hook + required_keys = ("FTRACK_SERVER", "FTRACK_API_USER", "FTRACK_API_KEY") + for key in required_keys: + if not os.environ.get(key): + self.log.debug(( + "Missing required environment \"{}\"" + " for Ftrack after launch procedure." + ).format(key)) + return + + try: + import ftrack_api + session = ftrack_api.Session(auto_connect_event_hub=True) + self.log.debug("Ftrack session created") + except Exception: + self.log.warning("Couldn't create Ftrack session") + return + + try: + entity = self._find_ftrack_task_entity( + session, project_name, asset_name, task_name + ) + self._ftrack_status_change(session, entity, project_name) + self._start_timer(session, entity, ftrack_api) + except Exception: + self.log.warning( + "Couldn't finish Ftrack procedure.", exc_info=True + ) + return + + finally: + session.close() + + def _find_ftrack_task_entity( + self, session, project_name, asset_name, task_name + ): + project_entity = session.query( + "Project where full_name is \"{}\"".format(project_name) + ).first() + if not project_entity: + self.log.warning( + "Couldn't find project \"{}\" in Ftrack.".format(project_name) + ) + return + + potential_task_entities = session.query(( + "TypedContext where parent.name is \"{}\" and project_id is \"{}\"" + ).format(asset_name, project_entity["id"])).all() + filtered_entities = [] + for _entity in potential_task_entities: + if ( + _entity.entity_type.lower() == "task" + and _entity["name"] == task_name + ): + filtered_entities.append(_entity) + + if not filtered_entities: + self.log.warning(( + "Couldn't find task \"{}\" under parent \"{}\" in Ftrack." + ).format(task_name, asset_name)) + return + + if len(filtered_entities) > 1: + self.log.warning(( + "Found more than one task \"{}\"" + " under parent \"{}\" in Ftrack." + ).format(task_name, asset_name)) + return + + return filtered_entities[0] + + def _ftrack_status_change(self, session, entity, project_name): + presets = config.get_presets(project_name)["ftrack"]["ftrack_config"] + statuses = presets.get("status_update") + if not statuses: + return + + actual_status = entity["status"]["name"].lower() + already_tested = set() + ent_path = "/".join( + [ent["name"] for ent in entity["link"]] + ) + while True: + next_status_name = None + for key, value in statuses.items(): + if key in already_tested: + continue + if actual_status in value or "_any_" in value: + if key != "_ignore_": + next_status_name = key + already_tested.add(key) + break + already_tested.add(key) + + if next_status_name is None: + break + + try: + query = "Status where name is \"{}\"".format( + next_status_name + ) + status = session.query(query).one() + + entity["status"] = status + session.commit() + self.log.debug("Changing status to \"{}\" <{}>".format( + next_status_name, ent_path + )) + break + + except Exception: + session.rollback() + msg = ( + "Status \"{}\" in presets wasn't found" + " on Ftrack entity type \"{}\"" + ).format(next_status_name, entity.entity_type) + self.log.warning(msg) + + def _start_timer(self, session, entity, _ftrack_api): + self.log.debug("Triggering timer start.") + + user_entity = session.query("User where username is \"{}\"".format( + os.environ["FTRACK_API_USER"] + )).first() + if not user_entity: + self.log.warning( + "Couldn't find user with username \"{}\" in Ftrack".format( + os.environ["FTRACK_API_USER"] + ) + ) + return + + source = { + "user": { + "id": user_entity["id"], + "username": user_entity["username"] + } + } + event_data = { + "actionIdentifier": "start.timer", + "selection": [{"entityId": entity["id"], "entityType": "task"}] + } + session.event_hub.publish( + _ftrack_api.event.base.Event( + topic="ftrack.action.launch", + data=event_data, + source=source + ), + on_error="ignore" + ) + self.log.debug("Timer start triggered successfully.") From cfcd24318fc6cd200011021205c7279223507043 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Sep 2020 11:48:25 +0200 Subject: [PATCH 59/92] avoid bugs when same event is more than once stored in mongo --- pype/modules/ftrack/ftrack_server/lib.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pype/modules/ftrack/ftrack_server/lib.py b/pype/modules/ftrack/ftrack_server/lib.py index acf31ab437..ee6b1216dc 100644 --- a/pype/modules/ftrack/ftrack_server/lib.py +++ b/pype/modules/ftrack/ftrack_server/lib.py @@ -205,10 +205,16 @@ class ProcessEventHub(SocketBaseEventHub): else: try: self._handle(event) + + mongo_id = event["data"].get("_event_mongo_id") + if mongo_id is None: + continue + self.dbcon.update_one( - {"id": event["id"]}, + {"_id": mongo_id}, {"$set": {"pype_data.is_processed": True}} ) + except pymongo.errors.AutoReconnect: self.pypelog.error(( "Mongo server \"{}\" is not responding, exiting." @@ -244,6 +250,7 @@ class ProcessEventHub(SocketBaseEventHub): } try: event = ftrack_api.event.base.Event(**new_event_data) + event["data"]["_event_mongo_id"] = event_data["_id"] except Exception: self.logger.exception(L( 'Failed to convert payload into event: {0}', From 384c2382063b050f0547f0499f5647992d33f9b1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Sep 2020 12:20:27 +0200 Subject: [PATCH 60/92] ListWidget can be now used as widget in other item types --- .../tools/settings/settings/widgets/item_types.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index d1fd219258..68cebf6642 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1479,17 +1479,17 @@ class ListWidget(QtWidgets.QWidget, InputObject): self.object_type = input_data["object_type"] self.input_modifiers = input_data.get("input_modifiers") or {} - self.key = input_data["key"] - self.input_fields = [] layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 5) layout.setSpacing(5) - if not label_widget: - label_widget = QtWidgets.QLabel(input_data["label"], self) - layout.addWidget(label_widget, alignment=QtCore.Qt.AlignTop) + if not self.as_widget: + self.key = input_data["key"] + if not label_widget: + label_widget = QtWidgets.QLabel(input_data["label"], self) + layout.addWidget(label_widget, alignment=QtCore.Qt.AlignTop) self.label_widget = label_widget @@ -1684,8 +1684,9 @@ class ListWidget(QtWidgets.QWidget, InputObject): if self._state == state: return - self.label_widget.setProperty("state", state) - self.label_widget.style().polish(self.label_widget) + if self.label_widget: + self.label_widget.setProperty("state", state) + self.label_widget.style().polish(self.label_widget) def item_value(self): output = [] From 9a5007a9aad24d788aad04bd3cf50cdc4529cf05 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Sep 2020 12:21:06 +0200 Subject: [PATCH 61/92] changed `dict-form` to `form` in examples --- .../settings/settings/gui_schemas/system_schema/1_examples.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json index a884dcb31e..73f72c875c 100644 --- a/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json +++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json @@ -207,7 +207,7 @@ "label": "Inputs with form", "children": [ { - "type": "dict-form", + "type": "form", "children": [ { "type": "text", From 5b867b2def2ea19ca25d3c23f6c67c3343b70d1b Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 18 Sep 2020 12:23:24 +0200 Subject: [PATCH 62/92] change dict-form to form in examples --- .../settings/settings/gui_schemas/system_schema/1_examples.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json index a884dcb31e..73f72c875c 100644 --- a/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json +++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json @@ -207,7 +207,7 @@ "label": "Inputs with form", "children": [ { - "type": "dict-form", + "type": "form", "children": [ { "type": "text", From c5d5492c3375cc22866481beec7fc82faeffaa01 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 18 Sep 2020 12:23:38 +0200 Subject: [PATCH 63/92] start converting ftrack settings --- .../system_schema/1_tray_items.json | 272 ++++++++++-------- 1 file changed, 156 insertions(+), 116 deletions(-) diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_tray_items.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_tray_items.json index 0d27ccdc4b..deb84673f0 100644 --- a/pype/tools/settings/settings/gui_schemas/system_schema/1_tray_items.json +++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_tray_items.json @@ -1,120 +1,160 @@ { - "key": "tray_modules", - "type": "dict", - "label": "Modules", - "collapsable": true, - "is_group": true, - "is_file": true, - "children": [ - { - "key": "item_usage", - "type": "dict-invisible", - "children": [ - { - "type": "boolean", - "key": "User settings", - "label": "User settings" - }, { - "type": "boolean", - "key": "Ftrack", - "label": "Ftrack" - }, { - "type": "boolean", - "key": "Muster", - "label": "Muster" - }, { - "type": "boolean", - "key": "Avalon", - "label": "Avalon" - }, { - "type": "boolean", - "key": "Clockify", - "label": "Clockify" - }, { - "type": "boolean", - "key": "Standalone Publish", - "label": "Standalone Publish" - }, { - "type": "boolean", - "key": "Logging", - "label": "Logging" - }, { - "type": "boolean", - "key": "Idle Manager", - "label": "Idle Manager" - }, { - "type": "boolean", - "key": "Timers Manager", - "label": "Timers Manager" - }, { - "type": "boolean", - "key": "Rest Api", - "label": "Rest Api" - }, { - "type": "boolean", - "key": "Adobe Communicator", - "label": "Adobe Communicator" - } - ] + "key": "tray_modules", + "type": "dict", + "label": "Modules", + "collapsable": true, + "is_file": true, + "children": [{ + "key": "item_usage", + "type": "dict-invisible", + "children": [{ + "type": "boolean", + "key": "User settings", + "label": "User settings" + }, { + "type": "boolean", + "key": "Ftrack", + "label": "Ftrack" + }, { + "type": "boolean", + "key": "Muster", + "label": "Muster" + }, { + "type": "boolean", + "key": "Avalon", + "label": "Avalon" + }, { + "type": "boolean", + "key": "Clockify", + "label": "Clockify" + }, { + "type": "boolean", + "key": "Standalone Publish", + "label": "Standalone Publish" + }, { + "type": "boolean", + "key": "Logging", + "label": "Logging" + }, { + "type": "boolean", + "key": "Idle Manager", + "label": "Idle Manager" + }, { + "type": "boolean", + "key": "Timers Manager", + "label": "Timers Manager" + }, { + "type": "boolean", + "key": "Rest Api", + "label": "Rest Api" + }, { + "type": "boolean", + "key": "Adobe Communicator", + "label": "Adobe Communicator" + }] + }, { + "key": "attributes", + "type": "dict-invisible", + "children": [{ + "type": "dict", + "key": "Ftrack", + "label": "Ftrack", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "text", + "key": "ftrack_server", + "label": "Server" + }, + { + "type": "dict", + "key": "sync_to_avalon", + "label": "Sync to avalon", + "children": [{ + "type": "list", + "key": "statuses_name_change", + "label": "Status name change", + "object_type": "text", + "input_modifiers": { + "multiline": false + } + }] + }, + { + "type": "dict-modifiable", + "key": "status_version_to_task", + "label": "Version to Task status mapping", + "object_type": "text" + } + ] + }, + { + "type": "dict", + "key": "Rest Api", + "label": "Rest Api", + "collapsable": true, + "children": [{ + "type": "number", + "key": "default_port", + "label": "Default Port", + "minimum": 1, + "maximum": 65535 }, { - "key": "attributes", - "type": "dict-invisible", - "children": [ - { - "type": "dict", - "key": "Rest Api", - "label": "Rest Api", - "collapsable": true, - "children": [ - { - "type": "number", - "key": "default_port", - "label": "Default Port", - "minimum": 1, - "maximum": 65535 - }, { - "type": "list", - "object_type": "number", - "key": "exclude_ports", - "label": "Exclude ports", - "input_modifiers": { - "minimum": 1, - "maximum": 65535 - } - } - ] - }, { - "type": "dict", - "key": "Timers Manager", - "label": "Timers Manager", - "collapsable": true, - "children": [ - { - "type": "number", - "decimal": 2, - "key": "full_time", - "label": "Max idle time" - }, { - "type": "number", - "decimal": 2, - "key": "message_time", - "label": "When dialog will show" - } - ] - }, { - "type": "dict", - "key": "Clockify", - "label": "Clockify", - "collapsable": true, - "children": [ - { - "type": "text", - "key": "workspace_name", - "label": "Workspace name" - } - ] - } - ] - } + "type": "list", + "object_type": "number", + "key": "exclude_ports", + "label": "Exclude ports", + "input_modifiers": { + "minimum": 1, + "maximum": 65535 + } + }] + }, { + "type": "dict", + "key": "Timers Manager", + "label": "Timers Manager", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "number", + "decimal": 2, + "key": "full_time", + "label": "Max idle time" + }, { + "type": "number", + "decimal": 2, + "key": "message_time", + "label": "When dialog will show" + } + ] + }, { + "type": "dict", + "key": "Clockify", + "label": "Clockify", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "text", + "key": "workspace_name", + "label": "Workspace name" + } + ] + } ] + }] } From 75fd8c666ee0fc7b867386486a9a9ac13aa13bd1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Sep 2020 14:09:03 +0200 Subject: [PATCH 64/92] added possibility to set placeholder for text input --- pype/tools/settings/settings/widgets/item_types.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 68cebf6642..fea78b713b 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -906,6 +906,7 @@ class TextWidget(QtWidgets.QWidget, InputObject): self.initial_attributes(input_data, parent, as_widget) self.multiline = input_data.get("multiline", False) + placeholder = input_data.get("placeholder") layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) @@ -916,6 +917,9 @@ class TextWidget(QtWidgets.QWidget, InputObject): else: self.text_input = QtWidgets.QLineEdit(self) + if placeholder: + self.text_input.setPlaceholderText(placeholder) + self.setFocusProxy(self.text_input) layout_kwargs = {} From a28e154580c4bb5eef3dcbc38c280a3bb8780c02 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Sep 2020 14:09:09 +0200 Subject: [PATCH 65/92] added info to README --- pype/tools/settings/settings/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/tools/settings/settings/README.md b/pype/tools/settings/settings/README.md index db444eb3bd..da8868199a 100644 --- a/pype/tools/settings/settings/README.md +++ b/pype/tools/settings/settings/README.md @@ -111,6 +111,7 @@ ### text - simple text input - key `"multiline"` allows to enter multiple lines of text (Default: `False`) + - key `"placeholder"` allows to show text inside input when is empty (Default: `None`) ``` { From 98569e4fed3469f7bb19a4b4923dfff1c2ae4add Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Sep 2020 14:14:45 +0200 Subject: [PATCH 66/92] schema item does not have key `children` but `name`, schema type can represent only one schema name --- pype/tools/settings/settings/widgets/lib.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pype/tools/settings/settings/widgets/lib.py b/pype/tools/settings/settings/widgets/lib.py index e225d65417..f54989cfd7 100644 --- a/pype/tools/settings/settings/widgets/lib.py +++ b/pype/tools/settings/settings/widgets/lib.py @@ -69,12 +69,11 @@ def _fill_inner_schemas(schema_data, schema_collection): new_children.append(new_child) continue - for schema_name in child["children"]: - new_child = _fill_inner_schemas( - schema_collection[schema_name], - schema_collection - ) - new_children.append(new_child) + new_child = _fill_inner_schemas( + schema_collection[child["name"]], + schema_collection + ) + new_children.append(new_child) schema_data["children"] = new_children return schema_data From 44dc6805ade48fd2edd94bb1db1479afdc84f0a8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Sep 2020 14:14:56 +0200 Subject: [PATCH 67/92] modified current schemas --- .../projects_schema/0_project_gui_schema.json | 4 +--- .../system_schema/0_system_gui_schema.json | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/0_project_gui_schema.json b/pype/tools/settings/settings/gui_schemas/projects_schema/0_project_gui_schema.json index fa7c6a366d..cf95bf4c45 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/0_project_gui_schema.json +++ b/pype/tools/settings/settings/gui_schemas/projects_schema/0_project_gui_schema.json @@ -22,9 +22,7 @@ "children": [ { "type": "schema", - "children": [ - "1_plugins_gui_schema" - ] + "name": "1_plugins_gui_schema" } ] } diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/0_system_gui_schema.json b/pype/tools/settings/settings/gui_schemas/system_schema/0_system_gui_schema.json index b16545111c..456af7ac0f 100644 --- a/pype/tools/settings/settings/gui_schemas/system_schema/0_system_gui_schema.json +++ b/pype/tools/settings/settings/gui_schemas/system_schema/0_system_gui_schema.json @@ -7,12 +7,16 @@ "key": "global", "children": [{ "type": "schema", - "children": [ - "1_tray_items", - "1_applications_gui_schema", - "1_tools_gui_schema", - "1_intents_gui_schema" - ] + "name": "1_tray_items" + }, { + "type": "schema", + "name": "1_applications_gui_schema" + }, { + "type": "schema", + "name": "1_tools_gui_schema" + }, { + "type": "schema", + "name": "1_intents_gui_schema" }] }, { "type": "dict-invisible", @@ -29,6 +33,9 @@ "label": "Muster - Templates mapping", "is_file": true }] + }, { + "type": "schema", + "name": "1_examples" } ] } From aa472d66de0eb674bad32cc31747a38710eeea09 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Sep 2020 14:15:02 +0200 Subject: [PATCH 68/92] modified README --- pype/tools/settings/settings/README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pype/tools/settings/settings/README.md b/pype/tools/settings/settings/README.md index db444eb3bd..adacd62772 100644 --- a/pype/tools/settings/settings/README.md +++ b/pype/tools/settings/settings/README.md @@ -27,10 +27,7 @@ ``` { "type": "schema", - "children": [ - "my_schema_name", - "my_other_schema_name" - ] + "name": "my_schema_name" } ``` From dd9809e3edd9fc9657f296748d5f2f77a8541267 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 18 Sep 2020 15:39:35 +0200 Subject: [PATCH 69/92] update modules settings, in system --- .../system_settings/global/applications.json | 28 +- .../system_settings/global/modules.json | 57 ++++ .../system_settings/global/tools.json | 4 +- .../system_settings/global/tray_modules.json | 28 -- .../system_schema/0_system_gui_schema.json | 17 +- .../system_schema/1_modules_gui_schema.json | 266 ++++++++++++++++++ .../system_schema/1_tray_items.json | 160 ----------- 7 files changed, 340 insertions(+), 220 deletions(-) create mode 100644 pype/settings/defaults/system_settings/global/modules.json delete mode 100644 pype/settings/defaults/system_settings/global/tray_modules.json create mode 100644 pype/tools/settings/settings/gui_schemas/system_schema/1_modules_gui_schema.json delete mode 100644 pype/tools/settings/settings/gui_schemas/system_schema/1_tray_items.json diff --git a/pype/settings/defaults/system_settings/global/applications.json b/pype/settings/defaults/system_settings/global/applications.json index 3a74a85468..e85e5864d9 100644 --- a/pype/settings/defaults/system_settings/global/applications.json +++ b/pype/settings/defaults/system_settings/global/applications.json @@ -1,32 +1,32 @@ { - "blender_2.80": true, - "blender_2.81": true, - "blender_2.82": true, + "blender_2.80": false, + "blender_2.81": false, + "blender_2.82": false, "blender_2.83": true, "celaction_local": true, "celaction_remote": true, "harmony_17": true, - "maya_2017": true, - "maya_2018": true, + "maya_2017": false, + "maya_2018": false, "maya_2019": true, "maya_2020": true, - "nuke_10.0": true, - "nuke_11.2": true, + "nuke_10.0": false, + "nuke_11.2": false, "nuke_11.3": true, "nuke_12.0": true, - "nukex_10.0": true, - "nukex_11.2": true, + "nukex_10.0": false, + "nukex_11.2": false, "nukex_11.3": true, "nukex_12.0": true, - "nukestudio_10.0": true, - "nukestudio_11.2": true, + "nukestudio_10.0": false, + "nukestudio_11.2": false, "nukestudio_11.3": true, "nukestudio_12.0": true, - "houdini_16": true, + "houdini_16": false, "houdini_16.5": false, - "houdini_17": true, + "houdini_17": false, "houdini_18": true, - "premiere_2019": true, + "premiere_2019": false, "premiere_2020": true, "resolve_16": true, "storyboardpro_7": true, diff --git a/pype/settings/defaults/system_settings/global/modules.json b/pype/settings/defaults/system_settings/global/modules.json new file mode 100644 index 0000000000..6400c2e3f3 --- /dev/null +++ b/pype/settings/defaults/system_settings/global/modules.json @@ -0,0 +1,57 @@ +{ + "Avalon": { + "AVALON_MONGO": "", + "AVALON_DB_DATA": "", + "AVALON_THUMBNAIL_ROOT": "" + }, + "Ftrack": { + "enabled": true, + "ftrack_server": "", + "ftrack_actions_path": [], + "ftrack_events_path": [], + "FTRACK_EVENTS_MONGO_DB": "", + "FTRACK_EVENTS_MONGO_COL": "", + "sync_to_avalon": { + "statuses_name_change": [] + }, + "status_version_to_task": {}, + "status_update": {} + }, + "Rest Api": { + "default_port": 1, + "exclude_ports": [] + }, + "Timers Manager": { + "enabled": true, + "full_time": 0.0, + "message_time": 0.0 + }, + "Clockify": { + "enabled": true, + "workspace_name": "" + }, + "Deadline": { + "enabled": true, + "DEADLINE_REST_URL": "" + }, + "Muster": { + "enabled": true, + "MUSTER_REST_URL": "", + "templates_mapping": {} + }, + "Logging": { + "enabled": true + }, + "Adobe Communicator": { + "enabled": true + }, + "User setting": { + "enabled": true + }, + "Standalone Publish": { + "enabled": true + }, + "Idle Manager": { + "enabled": true + } +} \ No newline at end of file diff --git a/pype/settings/defaults/system_settings/global/tools.json b/pype/settings/defaults/system_settings/global/tools.json index 93895c0e81..1f8c2ad1ea 100644 --- a/pype/settings/defaults/system_settings/global/tools.json +++ b/pype/settings/defaults/system_settings/global/tools.json @@ -1,6 +1,6 @@ { - "mtoa_3.0.1": true, - "mtoa_3.1.1": true, + "mtoa_3.0.1": false, + "mtoa_3.1.1": false, "mtoa_3.2.0": true, "yeti_2.1.2": true } \ No newline at end of file diff --git a/pype/settings/defaults/system_settings/global/tray_modules.json b/pype/settings/defaults/system_settings/global/tray_modules.json deleted file mode 100644 index 0ff5b15552..0000000000 --- a/pype/settings/defaults/system_settings/global/tray_modules.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "item_usage": { - "User settings": false, - "Ftrack": true, - "Muster": false, - "Avalon": true, - "Clockify": false, - "Standalone Publish": true, - "Logging": true, - "Idle Manager": true, - "Timers Manager": true, - "Rest Api": true, - "Adobe Communicator": true - }, - "attributes": { - "Rest Api": { - "default_port": 8021, - "exclude_ports": [] - }, - "Timers Manager": { - "full_time": 15.0, - "message_time": 0.5 - }, - "Clockify": { - "workspace_name": "" - } - } -} \ No newline at end of file diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/0_system_gui_schema.json b/pype/tools/settings/settings/gui_schemas/system_schema/0_system_gui_schema.json index b16545111c..a979ae1fed 100644 --- a/pype/tools/settings/settings/gui_schemas/system_schema/0_system_gui_schema.json +++ b/pype/tools/settings/settings/gui_schemas/system_schema/0_system_gui_schema.json @@ -8,27 +8,12 @@ "children": [{ "type": "schema", "children": [ - "1_tray_items", + "1_modules_gui_schema", "1_applications_gui_schema", "1_tools_gui_schema", "1_intents_gui_schema" ] }] - }, { - "type": "dict-invisible", - "key": "muster", - "children": [{ - "type": "dict-modifiable", - "object_type": "number", - "input_modifiers": { - "minimum": 0, - "maximum": 300 - }, - "is_group": true, - "key": "templates_mapping", - "label": "Muster - Templates mapping", - "is_file": true - }] } ] } diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_modules_gui_schema.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_modules_gui_schema.json new file mode 100644 index 0000000000..feec5735f5 --- /dev/null +++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_modules_gui_schema.json @@ -0,0 +1,266 @@ +{ + "key": "modules", + "type": "dict", + "label": "Modules", + "collapsable": true, + "is_file": true, + "children": [{ + "type": "dict", + "key": "Avalon", + "label": "Avalon", + "collapsable": true, + "children": [ + { + "type": "text", + "key": "AVALON_MONGO", + "label": "Avalon Mongo URL" + }, + { + "type": "text", + "key": "AVALON_DB_DATA", + "label": "Avalon Mongo Data Location" + }, + { + "type": "text", + "key": "AVALON_THUMBNAIL_ROOT", + "label": "Thumbnail Storage Location" + } + ] + },{ + "type": "dict", + "key": "Ftrack", + "label": "Ftrack", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "text", + "key": "ftrack_server", + "label": "Server" + }, + { + "type": "label", + "label": "Additional Ftrack paths" + }, + { + "type": "list", + "key": "ftrack_actions_path", + "label": "Action paths", + "object_type": "text" + }, + { + "type": "list", + "key": "ftrack_events_path", + "label": "Event paths", + "object_type": "text" + }, + { + "type": "label", + "label": "Ftrack event server advanced settings" + }, + { + "type": "text", + "key": "FTRACK_EVENTS_MONGO_DB", + "label": "Event Mongo DB" + }, + { + "type": "text", + "key": "FTRACK_EVENTS_MONGO_COL", + "label": "Events Mongo Collection" + }, + { + "type": "dict", + "key": "sync_to_avalon", + "label": "Sync to avalon", + "children": [{ + "type": "list", + "key": "statuses_name_change", + "label": "Status name change", + "object_type": "text", + "input_modifiers": { + "multiline": false + } + }] + }, + { + "type": "dict-modifiable", + "key": "status_version_to_task", + "label": "Version to Task status mapping", + "object_type": "text" + }, + { + "type": "dict-modifiable", + "key": "status_update", + "label": "Status Updates", + "object_type": "list", + "input_modifiers": { + "object_type": "text" + } + } + ] + }, { + "type": "dict", + "key": "Rest Api", + "label": "Rest Api", + "collapsable": true, + "children": [{ + "type": "number", + "key": "default_port", + "label": "Default Port", + "minimum": 1, + "maximum": 65535 + }, + { + "type": "list", + "object_type": "number", + "key": "exclude_ports", + "label": "Exclude ports", + "input_modifiers": { + "minimum": 1, + "maximum": 65535 + } + } + ] + }, { + "type": "dict", + "key": "Timers Manager", + "label": "Timers Manager", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "number", + "decimal": 2, + "key": "full_time", + "label": "Max idle time" + }, { + "type": "number", + "decimal": 2, + "key": "message_time", + "label": "When dialog will show" + } + ] + }, { + "type": "dict", + "key": "Clockify", + "label": "Clockify", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "text", + "key": "workspace_name", + "label": "Workspace name" + } + ] + }, { + "type": "dict", + "key": "Deadline", + "label": "Deadline", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + },{ + "type": "text", + "key": "DEADLINE_REST_URL", + "label": "Deadline Resl URL" + }] + }, { + "type": "dict", + "key": "Muster", + "label": "Muster", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + },{ + "type": "text", + "key": "MUSTER_REST_URL", + "label": "Muster Resl URL" + },{ + "type": "dict-modifiable", + "object_type": "number", + "input_modifiers": { + "minimum": 0, + "maximum": 300 + }, + "is_group": true, + "key": "templates_mapping", + "label": "Templates mapping", + "is_file": true + }] + }, { + "type": "dict", + "key": "Logging", + "label": "Logging", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }] + }, { + "type": "dict", + "key": "Adobe Communicator", + "label": "Adobe Communicator", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }] + }, { + "type": "dict", + "key": "User setting", + "label": "User setting", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }] + }, { + "type": "dict", + "key": "Standalone Publish", + "label": "Standalone Publish", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }] + }, { + "type": "dict", + "key": "Idle Manager", + "label": "Idle Manager", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }] + } + ] +} diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_tray_items.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_tray_items.json deleted file mode 100644 index deb84673f0..0000000000 --- a/pype/tools/settings/settings/gui_schemas/system_schema/1_tray_items.json +++ /dev/null @@ -1,160 +0,0 @@ -{ - "key": "tray_modules", - "type": "dict", - "label": "Modules", - "collapsable": true, - "is_file": true, - "children": [{ - "key": "item_usage", - "type": "dict-invisible", - "children": [{ - "type": "boolean", - "key": "User settings", - "label": "User settings" - }, { - "type": "boolean", - "key": "Ftrack", - "label": "Ftrack" - }, { - "type": "boolean", - "key": "Muster", - "label": "Muster" - }, { - "type": "boolean", - "key": "Avalon", - "label": "Avalon" - }, { - "type": "boolean", - "key": "Clockify", - "label": "Clockify" - }, { - "type": "boolean", - "key": "Standalone Publish", - "label": "Standalone Publish" - }, { - "type": "boolean", - "key": "Logging", - "label": "Logging" - }, { - "type": "boolean", - "key": "Idle Manager", - "label": "Idle Manager" - }, { - "type": "boolean", - "key": "Timers Manager", - "label": "Timers Manager" - }, { - "type": "boolean", - "key": "Rest Api", - "label": "Rest Api" - }, { - "type": "boolean", - "key": "Adobe Communicator", - "label": "Adobe Communicator" - }] - }, { - "key": "attributes", - "type": "dict-invisible", - "children": [{ - "type": "dict", - "key": "Ftrack", - "label": "Ftrack", - "collapsable": true, - "checkbox_key": "enabled", - "children": [{ - "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "type": "text", - "key": "ftrack_server", - "label": "Server" - }, - { - "type": "dict", - "key": "sync_to_avalon", - "label": "Sync to avalon", - "children": [{ - "type": "list", - "key": "statuses_name_change", - "label": "Status name change", - "object_type": "text", - "input_modifiers": { - "multiline": false - } - }] - }, - { - "type": "dict-modifiable", - "key": "status_version_to_task", - "label": "Version to Task status mapping", - "object_type": "text" - } - ] - }, - { - "type": "dict", - "key": "Rest Api", - "label": "Rest Api", - "collapsable": true, - "children": [{ - "type": "number", - "key": "default_port", - "label": "Default Port", - "minimum": 1, - "maximum": 65535 - }, { - "type": "list", - "object_type": "number", - "key": "exclude_ports", - "label": "Exclude ports", - "input_modifiers": { - "minimum": 1, - "maximum": 65535 - } - }] - }, { - "type": "dict", - "key": "Timers Manager", - "label": "Timers Manager", - "collapsable": true, - "checkbox_key": "enabled", - "children": [{ - "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "type": "number", - "decimal": 2, - "key": "full_time", - "label": "Max idle time" - }, { - "type": "number", - "decimal": 2, - "key": "message_time", - "label": "When dialog will show" - } - ] - }, { - "type": "dict", - "key": "Clockify", - "label": "Clockify", - "collapsable": true, - "checkbox_key": "enabled", - "children": [{ - "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "type": "text", - "key": "workspace_name", - "label": "Workspace name" - } - ] - } - ] - }] -} From 162dc8a4bdfef487b1a804f7adb7022b07276715 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 18 Sep 2020 15:42:51 +0200 Subject: [PATCH 70/92] fill defaults --- .../system_settings/global/modules.json | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/pype/settings/defaults/system_settings/global/modules.json b/pype/settings/defaults/system_settings/global/modules.json index 6400c2e3f3..3fb52fa129 100644 --- a/pype/settings/defaults/system_settings/global/modules.json +++ b/pype/settings/defaults/system_settings/global/modules.json @@ -1,30 +1,40 @@ { "Avalon": { - "AVALON_MONGO": "", - "AVALON_DB_DATA": "", - "AVALON_THUMBNAIL_ROOT": "" + "AVALON_MONGO": "mongodb://localhost:2707", + "AVALON_DB_DATA": "{PYPE_SETUP_PATH}/../mongo_db_data", + "AVALON_THUMBNAIL_ROOT": "{PYPE_SETUP_PATH}/../avalon_thumails" }, "Ftrack": { "enabled": true, - "ftrack_server": "", + "ftrack_server": "https://pype.ftrackapp.com", "ftrack_actions_path": [], "ftrack_events_path": [], - "FTRACK_EVENTS_MONGO_DB": "", - "FTRACK_EVENTS_MONGO_COL": "", + "FTRACK_EVENTS_MONGO_DB": "pype", + "FTRACK_EVENTS_MONGO_COL": "ftrack_events", "sync_to_avalon": { - "statuses_name_change": [] + "statuses_name_change": [ + "ready", + "not ready" + ] }, "status_version_to_task": {}, - "status_update": {} + "status_update": { + "Ready": [ + "Not Ready" + ], + "In Progress": [ + "_any_" + ] + } }, "Rest Api": { - "default_port": 1, + "default_port": 8021, "exclude_ports": [] }, "Timers Manager": { "enabled": true, - "full_time": 0.0, - "message_time": 0.0 + "full_time": 15.0, + "message_time": 0.5 }, "Clockify": { "enabled": true, @@ -32,12 +42,17 @@ }, "Deadline": { "enabled": true, - "DEADLINE_REST_URL": "" + "DEADLINE_REST_URL": "http://localhost:8082" }, "Muster": { - "enabled": true, + "enabled": false, "MUSTER_REST_URL": "", - "templates_mapping": {} + "templates_mapping": { + "arnold": 46, + "redshift": 55, + "renderman": 29, + "vray": 37 + } }, "Logging": { "enabled": true @@ -54,4 +69,4 @@ "Idle Manager": { "enabled": true } -} \ No newline at end of file +} From 7da75db742701e42f2c6a020413e6d1bcfc4ab07 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Fri, 18 Sep 2020 15:43:48 +0200 Subject: [PATCH 71/92] fill muster templates --- .../system_settings/global/modules.json | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/pype/settings/defaults/system_settings/global/modules.json b/pype/settings/defaults/system_settings/global/modules.json index 3fb52fa129..8a2b326d46 100644 --- a/pype/settings/defaults/system_settings/global/modules.json +++ b/pype/settings/defaults/system_settings/global/modules.json @@ -48,10 +48,23 @@ "enabled": false, "MUSTER_REST_URL": "", "templates_mapping": { - "arnold": 46, - "redshift": 55, - "renderman": 29, - "vray": 37 + "3delight": 41, + "arnold": 46, + "arnold_sf": 57, + "gelato": 30, + "harware": 3, + "krakatoa": 51, + "file_layers": 7, + "mentalray": 2, + "mentalray_sf": 6, + "redshift": 55, + "renderman": 29, + "software": 1, + "software_sf": 5, + "turtle": 10, + "vector": 4, + "vray": 37, + "ffmpeg": 48 } }, "Logging": { From 2ee94bd890d0f86052f4d6278f751383d688a82b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Sep 2020 17:33:07 +0200 Subject: [PATCH 72/92] ListItem can be set as strict --- .../settings/settings/widgets/item_types.py | 76 ++++++++++--------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 68cebf6642..8c9fbd8640 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1291,11 +1291,16 @@ class ListItem(QtWidgets.QWidget, SettingObject): _btn_size = 20 value_changed = QtCore.Signal(object) - def __init__(self, object_type, input_modifiers, config_parent, parent): + def __init__( + self, object_type, input_modifiers, config_parent, parent, + is_strict=False + ): super(ListItem, self).__init__(parent) self._set_default_attributes() + self._is_strict = is_strict + self._parent = config_parent self._any_parent_is_group = True self._is_empty = False @@ -1307,34 +1312,38 @@ class ListItem(QtWidgets.QWidget, SettingObject): char_up = qtawesome.charmap("fa.angle-up") char_down = qtawesome.charmap("fa.angle-down") - self.add_btn = QtWidgets.QPushButton("+") - self.remove_btn = QtWidgets.QPushButton("-") - self.up_btn = QtWidgets.QPushButton(char_up) - self.down_btn = QtWidgets.QPushButton(char_down) + if not self._is_strict: + self.add_btn = QtWidgets.QPushButton("+") + self.remove_btn = QtWidgets.QPushButton("-") + self.up_btn = QtWidgets.QPushButton(char_up) + self.down_btn = QtWidgets.QPushButton(char_down) - font_up_down = qtawesome.font("fa", 13) - self.up_btn.setFont(font_up_down) - self.down_btn.setFont(font_up_down) + font_up_down = qtawesome.font("fa", 13) + self.up_btn.setFont(font_up_down) + self.down_btn.setFont(font_up_down) - self.add_btn.setFocusPolicy(QtCore.Qt.ClickFocus) - self.remove_btn.setFocusPolicy(QtCore.Qt.ClickFocus) - self.up_btn.setFocusPolicy(QtCore.Qt.ClickFocus) - self.down_btn.setFocusPolicy(QtCore.Qt.ClickFocus) + self.add_btn.setFocusPolicy(QtCore.Qt.ClickFocus) + self.remove_btn.setFocusPolicy(QtCore.Qt.ClickFocus) + self.up_btn.setFocusPolicy(QtCore.Qt.ClickFocus) + self.down_btn.setFocusPolicy(QtCore.Qt.ClickFocus) - self.add_btn.setFixedSize(self._btn_size, self._btn_size) - self.remove_btn.setFixedSize(self._btn_size, self._btn_size) - self.up_btn.setFixedSize(self._btn_size, self._btn_size) - self.down_btn.setFixedSize(self._btn_size, self._btn_size) + self.add_btn.setFixedSize(self._btn_size, self._btn_size) + self.remove_btn.setFixedSize(self._btn_size, self._btn_size) + self.up_btn.setFixedSize(self._btn_size, self._btn_size) + self.down_btn.setFixedSize(self._btn_size, self._btn_size) - self.add_btn.setProperty("btn-type", "tool-item") - self.remove_btn.setProperty("btn-type", "tool-item") - self.up_btn.setProperty("btn-type", "tool-item") - self.down_btn.setProperty("btn-type", "tool-item") + self.add_btn.setProperty("btn-type", "tool-item") + self.remove_btn.setProperty("btn-type", "tool-item") + self.up_btn.setProperty("btn-type", "tool-item") + self.down_btn.setProperty("btn-type", "tool-item") - self.add_btn.clicked.connect(self._on_add_clicked) - self.remove_btn.clicked.connect(self._on_remove_clicked) - self.up_btn.clicked.connect(self._on_up_clicked) - self.down_btn.clicked.connect(self._on_down_clicked) + self.add_btn.clicked.connect(self._on_add_clicked) + self.remove_btn.clicked.connect(self._on_remove_clicked) + self.up_btn.clicked.connect(self._on_up_clicked) + self.down_btn.clicked.connect(self._on_down_clicked) + + layout.addWidget(self.add_btn, 0) + layout.addWidget(self.remove_btn, 0) ItemKlass = TypeToKlass.types[object_type] self.value_input = ItemKlass( @@ -1344,18 +1353,17 @@ class ListItem(QtWidgets.QWidget, SettingObject): label_widget=None ) - self.spacer_widget = QtWidgets.QWidget(self) - self.spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - self.spacer_widget.setVisible(False) - - layout.addWidget(self.add_btn, 0) - layout.addWidget(self.remove_btn, 0) - layout.addWidget(self.value_input, 1) - layout.addWidget(self.spacer_widget, 1) - layout.addWidget(self.up_btn, 0) - layout.addWidget(self.down_btn, 0) + if not self._is_strict: + self.spacer_widget = QtWidgets.QWidget(self) + self.spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + self.spacer_widget.setVisible(False) + + layout.addWidget(self.spacer_widget, 1) + + layout.addWidget(self.up_btn, 0) + layout.addWidget(self.down_btn, 0) self.value_input.value_changed.connect(self._on_value_change) From 0cccd9e3a7fe7a12866ba627f8d6770813838eca Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Sep 2020 17:44:55 +0200 Subject: [PATCH 73/92] implemented strict list --- .../settings/settings/widgets/item_types.py | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 8c9fbd8640..7afa6fca18 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1705,6 +1705,153 @@ class ListWidget(QtWidgets.QWidget, InputObject): return output +class ListStrictWidget(QtWidgets.QWidget, InputObject): + value_changed = QtCore.Signal(object) + _default_input_value = None + + def __init__( + self, input_data, parent, + as_widget=False, label_widget=None, parent_widget=None + ): + if parent_widget is None: + parent_widget = parent + super(ListStrictWidget, self).__init__(parent_widget) + self.setObjectName("ListStrictWidget") + + horizontal = input_data.get("horizontal", True) + + self.initial_attributes(input_data, parent, as_widget) + + self.input_fields = [] + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 5) + layout.setSpacing(5) + + if not self.as_widget: + self.key = input_data["key"] + if not label_widget: + label_widget = QtWidgets.QLabel(input_data["label"], self) + layout.addWidget(label_widget, alignment=QtCore.Qt.AlignTop) + + self.label_widget = label_widget + + inputs_widget = QtWidgets.QWidget(self) + inputs_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + layout.addWidget(inputs_widget) + + if horizontal: + inputs_layout = QtWidgets.QHBoxLayout(inputs_widget) + else: + inputs_layout = QtWidgets.QVBoxLayout(inputs_widget) + inputs_layout.setContentsMargins(0, 0, 0, 0) + inputs_layout.setSpacing(3) + + self.inputs_widget = inputs_widget + self.inputs_layout = inputs_layout + + for child_configuration in input_data["object_types"]: + object_type = child_configuration["type"] + + proxy_widget = QtWidgets.QWidget(self) + proxy_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + item_widget = ListItem( + object_type, child_configuration, self, proxy_widget, + is_strict=True + ) + + self.input_fields.append(item_widget) + item_widget.value_changed.connect(self._on_value_change) + + proxy_layout = QtWidgets.QHBoxLayout(proxy_widget) + proxy_layout.setContentsMargins(0, 0, 0, 0) + proxy_layout.setSpacing(5) + + label = child_configuration.get("label") + label_widget = None + if label: + label_widget = QtWidgets.QLabel(label, self) + proxy_layout.addWidget(label_widget, 0) + + proxy_layout.addWidget(item_widget, 0) + + if not horizontal: + spacer_widget = QtWidgets.QWidget(proxy_widget) + proxy_layout.addWidget(spacer_widget, 1) + + self.inputs_layout.addWidget(proxy_widget) + + if horizontal: + spacer_widget = QtWidgets.QWidget(proxy_widget) + self.inputs_layout.addWidget(spacer_widget, 1) + + self.updateGeometry() + + @property + def default_input_value(self): + if self._default_input_value is None: + self.set_value(NOT_SET) + self._default_input_value = self.item_value() + return self._default_input_value + + def set_value(self, value): + if self._is_overriden: + method_name = "apply_overrides" + elif not self._has_studio_override: + method_name = "update_default_values" + else: + method_name = "update_studio_values" + + for idx, input_field in enumerate(self.input_fields): + if value is NOT_SET: + _value = value + else: + if idx > len(value) - 1: + _value = NOT_SET + else: + _value = value[idx] + _method = getattr(input_field, method_name) + _method(_value) + + def hierarchical_style_update(self): + for input_field in self.input_fields: + input_field.hierarchical_style_update() + self.update_style() + + def update_style(self): + if self._as_widget: + if not self.isEnabled(): + state = self.style_state(False, False, False, False) + else: + state = self.style_state( + False, + self._is_invalid, + False, + self._is_modified + ) + else: + state = self.style_state( + self.has_studio_override, + self.is_invalid, + self.is_overriden, + self.is_modified + ) + + if self._state == state: + return + + if self.label_widget: + self.label_widget.setProperty("state", state) + self.label_widget.style().polish(self.label_widget) + + def item_value(self): + output = [] + for item in self.input_fields: + output.append(item.config_value()) + return output + + class ModifiableDictItem(QtWidgets.QWidget, SettingObject): _btn_size = 20 value_changed = QtCore.Signal(object) @@ -3360,6 +3507,7 @@ TypeToKlass.types["text"] = TextWidget TypeToKlass.types["path-input"] = PathInputWidget TypeToKlass.types["raw-json"] = RawJsonWidget TypeToKlass.types["list"] = ListWidget +TypeToKlass.types["list-strict"] = ListStrictWidget TypeToKlass.types["dict-modifiable"] = ModifiableDict TypeToKlass.types["dict-item"] = DictItemWidget TypeToKlass.types["dict"] = DictWidget From a13755c341d67a91b9a70ce23440c16ce0d544d9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Sep 2020 17:46:08 +0200 Subject: [PATCH 74/92] make sure defaults_not_set is set to False if defaults are available --- pype/tools/settings/settings/widgets/item_types.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 7afa6fca18..4a9e092a99 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -547,6 +547,7 @@ class InputObject(SettingObject): Class is for item types not creating or using other item types, most of methods has same code in that case. """ + def update_default_values(self, parent_values): self._state = None self._is_modified = False @@ -559,8 +560,8 @@ class InputObject(SettingObject): if value is NOT_SET: if self.develop_mode: - value = self.default_input_value self.defaults_not_set = True + value = self.default_input_value if value is NOT_SET: raise NotImplementedError(( "{} Does not have implemented" @@ -571,6 +572,8 @@ class InputObject(SettingObject): raise ValueError( "Default value is not set. This is implementation BUG." ) + else: + self.defaults_not_set = False self.default_value = value self._has_studio_override = False @@ -3024,6 +3027,8 @@ class PathWidget(QtWidgets.QWidget, SettingObject): raise ValueError( "Default value is not set. This is implementation BUG." ) + else: + self.defaults_not_set = False self.default_value = value self._has_studio_override = False From d3e61cea7dd01d4d950c9786d9184d20a0b59485 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Sep 2020 17:46:24 +0200 Subject: [PATCH 75/92] added strict list and dict-item to readme --- pype/tools/settings/settings/README.md | 79 ++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/pype/tools/settings/settings/README.md b/pype/tools/settings/settings/README.md index db444eb3bd..9d12467cc9 100644 --- a/pype/tools/settings/settings/README.md +++ b/pype/tools/settings/settings/README.md @@ -207,6 +207,85 @@ } ``` +### dict-item +- item represents dictionary with strict keys in and data types of its values +- can be used only as widget (in `list` or `dict-modifiable`) + - only key modifier is `children` which is list of it's keys +- USAGE: e.g. List of dictionaries where each dictionary have same structure. + +``` +{ + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": "dict-item", + "input_modifiers": { + "children": [ + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, { + "key": "hosts", + "label": "Hosts", + "type": "list", + "object_type": "text" + } + ... + ] + } +} +``` + +### list-strict +- input for strict number of items in list +- each child item can be different type with different possible modifiers +- it is possible to display them in horizontal or vertical layout + - key `"horizontal"` as `True`/`False` (Default: `True`) +- each child may have defined `"label"` which is shown next to input + - label does not reflect modifications or overrides (TODO) +- children item are defined under key `"object_types"` which is list of dictionaries + - key `"children"` is not used because is used for hierarchy validations in schema +- USAGE: For colors, transformations, etc. Custom number and different modifiers + give ability to define if color is HUE or RGB, 0-255, 0-1, 0-100 etc. + +``` +{ + "type": "list-strict", + "key": "color", + "label": "Color", + "object_types": [ + { + "label": "Red", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Green", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Blue", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Alpha", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 6 + } + ] +} +``` + + ## Noninteractive widgets - have nothing to do with data From 060aa04f93f6999ddc9146ba762d81eb9ffeb8fa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Sep 2020 17:49:16 +0200 Subject: [PATCH 76/92] added dict-item and list-strict to examples --- .../gui_schemas/system_schema/1_examples.json | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json index 73f72c875c..1284c9948b 100644 --- a/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json +++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json @@ -72,6 +72,57 @@ "minimum": 10, "maximum": 100 } + }, { + "type": "list-strict", + "key": "strict_list", + "label": "StrictList (color)", + "object_types": [ + { + "label": "Red", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Green", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Blue", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Alpha", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 6 + } + ] + }, { + "type": "list", + "key": "dict_item", + "label": "DictItem in List", + "object_type": "dict-item", + "input_modifiers": { + "children": [ + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, { + "key": "hosts", + "label": "Hosts", + "type": "list", + "object_type": "text" + } + ] + } }, { "type": "path-widget", "key": "single_path_input", From 8f967c9d6df25f85bad8ef633890688f51a54aff Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Sep 2020 18:16:21 +0200 Subject: [PATCH 77/92] added more variants to list-strict --- .../gui_schemas/system_schema/1_examples.json | 91 ++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json index 1284c9948b..0578968508 100644 --- a/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json +++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json @@ -74,8 +74,8 @@ } }, { "type": "list-strict", - "key": "strict_list", - "label": "StrictList (color)", + "key": "strict_list_labels_horizontal", + "label": "StrictList-labels-horizontal (color)", "object_types": [ { "label": "Red", @@ -103,6 +103,93 @@ "decimal": 6 } ] + }, { + "type": "list-strict", + "key": "strict_list_labels_vertical", + "label": "StrictList-labels-vertical (color)", + "horizontal": false, + "object_types": [ + { + "label": "Red", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Green", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Blue", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Alpha", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 6 + } + ] + }, { + "type": "list-strict", + "key": "strict_list_nolabels_horizontal", + "label": "StrictList-nolabels-horizontal (color)", + "object_types": [ + { + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 6 + } + ] + }, { + "type": "list-strict", + "key": "strict_list_nolabels_vertical", + "label": "StrictList-nolabels-vertical (color)", + "horizontal": false, + "object_types": [ + { + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 6 + } + ] }, { "type": "list", "key": "dict_item", From 85a55b4f1b649c5d9d6bd3f27360ae2deaca6239 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Sep 2020 18:16:34 +0200 Subject: [PATCH 78/92] list-strict use grid layout now --- .../settings/settings/widgets/item_types.py | 90 ++++++++++++++----- 1 file changed, 67 insertions(+), 23 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 4a9e092a99..01dab9ccc9 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1721,8 +1721,6 @@ class ListStrictWidget(QtWidgets.QWidget, InputObject): super(ListStrictWidget, self).__init__(parent_widget) self.setObjectName("ListStrictWidget") - horizontal = input_data.get("horizontal", True) - self.initial_attributes(input_data, parent, as_widget) self.input_fields = [] @@ -1739,58 +1737,91 @@ class ListStrictWidget(QtWidgets.QWidget, InputObject): self.label_widget = label_widget + self._add_children(layout, input_data) + + def _add_children(self, layout, input_data): inputs_widget = QtWidgets.QWidget(self) inputs_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) layout.addWidget(inputs_widget) + horizontal = input_data.get("horizontal", True) if horizontal: inputs_layout = QtWidgets.QHBoxLayout(inputs_widget) else: - inputs_layout = QtWidgets.QVBoxLayout(inputs_widget) + inputs_layout = QtWidgets.QGridLayout(inputs_widget) + inputs_layout.setContentsMargins(0, 0, 0, 0) inputs_layout.setSpacing(3) self.inputs_widget = inputs_widget self.inputs_layout = inputs_layout + children_item_mapping = [] for child_configuration in input_data["object_types"]: object_type = child_configuration["type"] - proxy_widget = QtWidgets.QWidget(self) - proxy_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - item_widget = ListItem( - object_type, child_configuration, self, proxy_widget, + object_type, child_configuration, self, self.inputs_widget, is_strict=True ) self.input_fields.append(item_widget) item_widget.value_changed.connect(self._on_value_change) - proxy_layout = QtWidgets.QHBoxLayout(proxy_widget) - proxy_layout.setContentsMargins(0, 0, 0, 0) - proxy_layout.setSpacing(5) - label = child_configuration.get("label") label_widget = None if label: label_widget = QtWidgets.QLabel(label, self) - proxy_layout.addWidget(label_widget, 0) - proxy_layout.addWidget(item_widget, 0) - - if not horizontal: - spacer_widget = QtWidgets.QWidget(proxy_widget) - proxy_layout.addWidget(spacer_widget, 1) - - self.inputs_layout.addWidget(proxy_widget) + children_item_mapping.append((label_widget, item_widget)) if horizontal: - spacer_widget = QtWidgets.QWidget(proxy_widget) - self.inputs_layout.addWidget(spacer_widget, 1) + self._add_children_horizontally(children_item_mapping) + else: + self._add_children_vertically(children_item_mapping) self.updateGeometry() + def _add_children_vertically(self, children_item_mapping): + any_has_label = False + for item_mapping in children_item_mapping: + if item_mapping[0]: + any_has_label = True + break + + row = self.inputs_layout.count() + if not any_has_label: + self.inputs_layout.setColumnStretch(1, 1) + for item_mapping in children_item_mapping: + item_widget = item_mapping[1] + self.inputs_layout.addWidget(item_widget, row, 0, 1, 1) + + spacer_widget = QtWidgets.QWidget(self.inputs_widget) + self.inputs_layout.addWidget(spacer_widget, row, 1, 1, 1) + row += 1 + + else: + self.inputs_layout.setColumnStretch(2, 1) + for label_widget, item_widget in children_item_mapping: + self.inputs_layout.addWidget( + label_widget, row, 0, 1, 1, + alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignTop + ) + self.inputs_layout.addWidget(item_widget, row, 1, 1, 1) + + spacer_widget = QtWidgets.QWidget(self.inputs_widget) + self.inputs_layout.addWidget(spacer_widget, row, 2, 1, 1) + row += 1 + + def _add_children_horizontally(self, children_item_mapping): + for label_widget, item_widget in children_item_mapping: + if label_widget: + self.inputs_layout.addWidget(label_widget, 0) + self.inputs_layout.addWidget(item_widget, 0) + + spacer_widget = QtWidgets.QWidget(self.inputs_widget) + self.inputs_layout.addWidget(spacer_widget, 1) + @property def default_input_value(self): if self._default_input_value is None: @@ -1840,7 +1871,7 @@ class ListStrictWidget(QtWidgets.QWidget, InputObject): self.is_overriden, self.is_modified ) - + if self._state == state: return @@ -2630,6 +2661,18 @@ class DictWidget(QtWidgets.QWidget, SettingObject): return {self.key: values}, self.is_group +class GridLabel(QtWidgets.QLabel): + def __init__(self, *args, **kwargs): + super(GridLabel, self).__init__(*args, **kwargs) + self.input_field = None + + def mouseReleaseEvent(self, event): + if self.input_field: + print("here", self.input_field) + self.input_field.mouseReleaseEvent(event) + return super(GridLabel, self).mouseReleaseEvent(event) + + class DictInvisible(QtWidgets.QWidget, SettingObject): # TODO is not overridable by itself value_changed = QtCore.Signal(object) @@ -2679,7 +2722,7 @@ class DictInvisible(QtWidgets.QWidget, SettingObject): if not klass.expand_in_grid: label = child_configuration.get("label") if label is not None: - label_widget = QtWidgets.QLabel(label, self) + label_widget = GridLabel(label, self) self.content_layout.addWidget( label_widget, row, 0, 1, 1, alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignTop @@ -2689,6 +2732,7 @@ class DictInvisible(QtWidgets.QWidget, SettingObject): item.value_changed.connect(self._on_value_change) if label_widget: + label_widget.input_field = item self.content_layout.addWidget(item, row, 1, 1, 1) else: self.content_layout.addWidget(item, row, 0, 1, 2) From dcb0db7937a94e438568308d942cbd13e76d48bc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Sep 2020 18:19:51 +0200 Subject: [PATCH 79/92] reverse testing changes --- .../tools/settings/settings/widgets/item_types.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 01dab9ccc9..3bbf352f8b 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -2661,18 +2661,6 @@ class DictWidget(QtWidgets.QWidget, SettingObject): return {self.key: values}, self.is_group -class GridLabel(QtWidgets.QLabel): - def __init__(self, *args, **kwargs): - super(GridLabel, self).__init__(*args, **kwargs) - self.input_field = None - - def mouseReleaseEvent(self, event): - if self.input_field: - print("here", self.input_field) - self.input_field.mouseReleaseEvent(event) - return super(GridLabel, self).mouseReleaseEvent(event) - - class DictInvisible(QtWidgets.QWidget, SettingObject): # TODO is not overridable by itself value_changed = QtCore.Signal(object) @@ -2722,7 +2710,7 @@ class DictInvisible(QtWidgets.QWidget, SettingObject): if not klass.expand_in_grid: label = child_configuration.get("label") if label is not None: - label_widget = GridLabel(label, self) + label_widget = QtWidget.QLabel(label, self) self.content_layout.addWidget( label_widget, row, 0, 1, 1, alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignTop @@ -2732,7 +2720,6 @@ class DictInvisible(QtWidgets.QWidget, SettingObject): item.value_changed.connect(self._on_value_change) if label_widget: - label_widget.input_field = item self.content_layout.addWidget(item, row, 1, 1, 1) else: self.content_layout.addWidget(item, row, 0, 1, 2) From cc8faba5bffbabec5e6f0d6f7d253ebd4a59aa8d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 18 Sep 2020 18:20:52 +0200 Subject: [PATCH 80/92] typo fix --- pype/tools/settings/settings/widgets/item_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 3bbf352f8b..bf9155f765 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -2710,7 +2710,7 @@ class DictInvisible(QtWidgets.QWidget, SettingObject): if not klass.expand_in_grid: label = child_configuration.get("label") if label is not None: - label_widget = QtWidget.QLabel(label, self) + label_widget = QtWidgets.QLabel(label, self) self.content_layout.addWidget( label_widget, row, 0, 1, 1, alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignTop From bbab7178c7ca1637ccdbcf7291a9209ebcd5954b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Sep 2020 11:53:27 +0200 Subject: [PATCH 81/92] DictWidget can be used as widget --- .../settings/settings/widgets/item_types.py | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 68cebf6642..8a88fbf829 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -2132,18 +2132,26 @@ class DictWidget(QtWidgets.QWidget, SettingObject): self, input_data, parent, as_widget=False, label_widget=None, parent_widget=None ): - if as_widget: - raise TypeError("Can't use \"{}\" as widget item.".format( - self.__class__.__name__ - )) - if parent_widget is None: parent_widget = parent super(DictWidget, self).__init__(parent_widget) - self.setObjectName("DictWidget") self.initial_attributes(input_data, parent, as_widget) + self.input_fields = [] + + self.checkbox_widget = None + self.checkbox_key = input_data.get("checkbox_key") + + self.label_widget = label_widget + + if self.as_widget: + self._ui_as_widget(input_data) + else: + self._ui_as_item(input_data) + + def _ui_as_item(self, input_data): + self.key = input_data["key"] if input_data.get("highlight_content", False): content_state = "hightlighted" bottom_margin = 5 @@ -2151,10 +2159,6 @@ class DictWidget(QtWidgets.QWidget, SettingObject): content_state = "" bottom_margin = 0 - self.input_fields = [] - - self.key = input_data["key"] - main_layout = QtWidgets.QHBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) @@ -2177,9 +2181,6 @@ class DictWidget(QtWidgets.QWidget, SettingObject): self.label_widget = body_widget.label_widget - self.checkbox_widget = None - self.checkbox_key = input_data.get("checkbox_key") - for child_data in input_data.get("children", []): self.add_children_gui(child_data) @@ -2194,6 +2195,22 @@ class DictWidget(QtWidgets.QWidget, SettingObject): else: body_widget.hide_toolbox(hide_content=False) + def _ui_as_widget(self, input_data): + body = QtWidgets.QWidget(self) + body.setObjectName("DictAsWidgetBody") + + content_layout = QtWidgets.QGridLayout(body) + content_layout.setContentsMargins(5, 5, 5, 5) + self.content_layout = content_layout + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + layout.addWidget(body) + + for child_configuration in input_data["children"]: + self.add_children_gui(child_configuration) + def add_children_gui(self, child_configuration): item_type = child_configuration["type"] klass = TypeToKlass.types.get(item_type) From bd5e1ae310168996eda083fc008b190d8f671a64 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Sep 2020 11:55:16 +0200 Subject: [PATCH 82/92] minor tweaks in DictWidget to be able used as widget --- .../settings/settings/widgets/item_types.py | 63 ++++++++++++------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 8a88fbf829..83805d9948 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -2297,8 +2297,12 @@ class DictWidget(QtWidgets.QWidget, SettingObject): item.set_as_overriden() def update_default_values(self, parent_values): + # Make sure this is set to False + self._state = None + self._child_state = None + value = NOT_SET - if self._as_widget: + if self.as_widget: value = parent_values elif parent_values is not NOT_SET: value = parent_values.get(self.key, NOT_SET) @@ -2307,15 +2311,21 @@ class DictWidget(QtWidgets.QWidget, SettingObject): item.update_default_values(value) def update_studio_values(self, parent_values): + # Make sure this is set to False + self._state = None + self._child_state = None value = NOT_SET - if parent_values is not NOT_SET: - value = parent_values.get(self.key, NOT_SET) + if self.as_widget: + value = parent_values + else: + if parent_values is not NOT_SET: + value = parent_values.get(self.key, NOT_SET) - self._has_studio_override = False - if self.is_group and value is not NOT_SET: - self._has_studio_override = True + self._has_studio_override = False + if self.is_group and value is not NOT_SET: + self._has_studio_override = True - self._had_studio_override = bool(self._has_studio_override) + self._had_studio_override = bool(self._has_studio_override) for item in self.input_fields: item.update_studio_values(value) @@ -2325,37 +2335,40 @@ class DictWidget(QtWidgets.QWidget, SettingObject): self._state = None self._child_state = None - metadata = {} - groups = tuple() - override_values = NOT_SET - if parent_values is not NOT_SET: - metadata = parent_values.get(METADATA_KEY) or metadata - groups = metadata.get("groups") or groups - override_values = parent_values.get(self.key, override_values) + if not self.as_widget: + metadata = {} + groups = tuple() + override_values = NOT_SET + if parent_values is not NOT_SET: + metadata = parent_values.get(METADATA_KEY) or metadata + groups = metadata.get("groups") or groups + override_values = parent_values.get(self.key, override_values) - self._is_overriden = self.key in groups + self._is_overriden = self.key in groups for item in self.input_fields: item.apply_overrides(override_values) - if not self._is_overriden: - self._is_overriden = ( - self.is_group - and self.is_overidable - and self.child_overriden - ) - self._was_overriden = bool(self._is_overriden) + if not self.as_widget: + if not self._is_overriden: + self._is_overriden = ( + self.is_group + and self.is_overidable + and self.child_overriden + ) + self._was_overriden = bool(self._is_overriden) def _on_value_change(self, item=None): if self.ignore_value_changes: return - if self.is_group and not self.any_parent_as_widget: + if self.is_group and not (self.as_widget or self.any_parent_as_widget): if self.is_overidable: self._is_overriden = True else: self._has_studio_override = True + # TODO check if this is required self.hierarchical_style_update() self.value_changed.emit(self) @@ -2368,6 +2381,10 @@ class DictWidget(QtWidgets.QWidget, SettingObject): self.update_style() def update_style(self, is_overriden=None): + # TODO add style update when used as widget + if self.as_widget: + return + child_has_studio_override = self.child_has_studio_override child_modified = self.child_modified child_invalid = self.child_invalid From cbbbefc76dd939cbd5e327086825f4eb5d508033 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Sep 2020 11:56:25 +0200 Subject: [PATCH 83/92] "dict-item" is set to DictWidget for backwards compatibility --- pype/tools/settings/settings/widgets/item_types.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 83805d9948..e6fb2d32e3 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -3387,7 +3387,8 @@ TypeToKlass.types["path-input"] = PathInputWidget TypeToKlass.types["raw-json"] = RawJsonWidget TypeToKlass.types["list"] = ListWidget TypeToKlass.types["dict-modifiable"] = ModifiableDict -TypeToKlass.types["dict-item"] = DictItemWidget +# DEPRECATED - remove when removed from schemas +TypeToKlass.types["dict-item"] = DictWidget TypeToKlass.types["dict"] = DictWidget TypeToKlass.types["dict-invisible"] = DictInvisible TypeToKlass.types["path-widget"] = PathWidget From 4bd7a6c4fa2ddabf5a90e0ec39d80c970e069da8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Sep 2020 11:56:45 +0200 Subject: [PATCH 84/92] removed DictItemWidget as is not used --- .../settings/settings/widgets/item_types.py | 95 ------------------- 1 file changed, 95 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index e6fb2d32e3..3cf9cfcc74 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1192,101 +1192,6 @@ class RawJsonWidget(QtWidgets.QWidget, InputObject): return self.text_input.json_value() -class DictItemWidget(QtWidgets.QWidget, SettingObject): - default_input_value = True - value_changed = QtCore.Signal(object) - - def __init__( - self, input_data, parent, - as_widget=False, label_widget=None, parent_widget=None - ): - if parent_widget is None: - parent_widget = parent - super(DictItemWidget, self).__init__(parent_widget) - - self.initial_attributes(input_data, parent, as_widget) - - if not self._as_widget: - raise TypeError("{} can be used only as widget.".format( - self.__class__.__name__ - )) - - self.input_fields = [] - - body = QtWidgets.QWidget(self) - body.setObjectName("DictItemWidgetBody") - - content_layout = QtWidgets.QGridLayout(body) - content_layout.setContentsMargins(5, 5, 5, 5) - self.content_layout = content_layout - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(5) - layout.addWidget(body) - - self.label_widget = label_widget - - for child_configuration in input_data["children"]: - self.add_children_gui(child_configuration) - - def add_children_gui(self, child_configuration): - item_type = child_configuration["type"] - klass = TypeToKlass.types.get(item_type) - - row = self.content_layout.rowCount() - if not getattr(klass, "is_input_type", False): - item = klass(child_configuration, self) - self.content_layout.addWidget(item, row, 0, 1, 2) - return item - - label_widget = None - if not klass.expand_in_grid: - label = child_configuration.get("label") - if label is not None: - label_widget = QtWidgets.QLabel(label, self) - self.content_layout.addWidget( - label_widget, row, 0, 1, 1, - alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignTop - ) - - item = klass(child_configuration, self, label_widget=label_widget) - item.value_changed.connect(self._on_value_change) - - if label_widget: - self.content_layout.addWidget(item, row, 1, 1, 1) - else: - self.content_layout.addWidget(item, row, 0, 1, 2) - - self.input_fields.append(item) - return item - - def hierarchical_style_update(self): - for input_field in self.input_fields: - input_field.hierarchical_style_update() - - def _on_value_change(self, item=None): - self.value_changed.emit(self) - - def update_default_values(self, parent_values): - for input_field in self.input_fields: - input_field.update_default_values(parent_values) - - def update_studio_values(self, parent_values): - for input_field in self.input_fields: - input_field.update_studio_values(parent_values) - - def apply_overrides(self, parent_values): - for input_field in self.input_fields: - input_field.apply_overrides(parent_values) - - def item_value(self): - output = {} - for input_field in self.input_fields: - output.update(input_field.config_value()) - return output - - class ListItem(QtWidgets.QWidget, SettingObject): _btn_size = 20 value_changed = QtCore.Signal(object) From ba8b1b8015b5821996b1b4140d6d2831c1394f03 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Sep 2020 11:57:00 +0200 Subject: [PATCH 85/92] changed object name in styles --- pype/tools/settings/settings/style/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/tools/settings/settings/style/style.css b/pype/tools/settings/settings/style/style.css index 221f297219..3f648abef8 100644 --- a/pype/tools/settings/settings/style/style.css +++ b/pype/tools/settings/settings/style/style.css @@ -152,7 +152,7 @@ QPushButton[btn-type="expand-toggle"] { background: #141a1f; } -#DictItemWidgetBody{ +#DictAsWidgetBody{ background: transparent; border: 2px solid #cccccc; border-radius: 5px; From 14a15c9a8cbedae0641583869b552a8a84af78b1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Sep 2020 11:57:35 +0200 Subject: [PATCH 86/92] current schemas in example does not have dict-item --- .../gui_schemas/projects_schema/1_plugins_gui_schema.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/1_plugins_gui_schema.json b/pype/tools/settings/settings/gui_schemas/projects_schema/1_plugins_gui_schema.json index f70495017e..f357b51dc5 100644 --- a/pype/tools/settings/settings/gui_schemas/projects_schema/1_plugins_gui_schema.json +++ b/pype/tools/settings/settings/gui_schemas/projects_schema/1_plugins_gui_schema.json @@ -172,7 +172,7 @@ "type": "list", "key": "profiles", "label": "Profiles", - "object_type": "dict-item", + "object_type": "dict", "input_modifiers": { "children": [ { @@ -192,7 +192,7 @@ "label": "Output Definitions", "type": "dict-modifiable", "highlight_content": true, - "object_type": "dict-item", + "object_type": "dict", "input_modifiers": { "children": [ { From 83ff6bc85bd8c0016c97fca91e8ac07169ac4a58 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Sep 2020 12:03:57 +0200 Subject: [PATCH 87/92] removed dict-item from readme --- pype/tools/settings/settings/README.md | 31 -------------------------- 1 file changed, 31 deletions(-) diff --git a/pype/tools/settings/settings/README.md b/pype/tools/settings/settings/README.md index 9d12467cc9..8fa0a4616a 100644 --- a/pype/tools/settings/settings/README.md +++ b/pype/tools/settings/settings/README.md @@ -207,37 +207,6 @@ } ``` -### dict-item -- item represents dictionary with strict keys in and data types of its values -- can be used only as widget (in `list` or `dict-modifiable`) - - only key modifier is `children` which is list of it's keys -- USAGE: e.g. List of dictionaries where each dictionary have same structure. - -``` -{ - "type": "list", - "key": "profiles", - "label": "Profiles", - "object_type": "dict-item", - "input_modifiers": { - "children": [ - { - "key": "families", - "label": "Families", - "type": "list", - "object_type": "text" - }, { - "key": "hosts", - "label": "Hosts", - "type": "list", - "object_type": "text" - } - ... - ] - } -} -``` - ### list-strict - input for strict number of items in list - each child item can be different type with different possible modifiers From 7361e86cb9341d9b92e092920711091a71342463 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Sep 2020 12:04:50 +0200 Subject: [PATCH 88/92] added new usage to readme --- pype/tools/settings/settings/README.md | 43 +++++++++++++++++++++----- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/pype/tools/settings/settings/README.md b/pype/tools/settings/settings/README.md index db444eb3bd..969c9dc574 100644 --- a/pype/tools/settings/settings/README.md +++ b/pype/tools/settings/settings/README.md @@ -57,13 +57,18 @@ ## dict - this is another dictionary input wrapping more inputs but visually makes them different -- required keys are `"key"` under which will be stored and `"label"` which will be shown in GUI -- this input can be expandable - - that can be set with key `"expandable"` as `True`/`False` (Default: `True`) - - with key `"expanded"` as `True`/`False` can be set that is expanded when GUI is opened (Default: `False`) -- it is possible to add darker background with `"highlight_content"` (Default: `False`) - - darker background has limits of maximum applies after 3-4 nested highlighted items there is not difference in the color +- item may be used as widget (in `list` or `dict-modifiable`) + - in that case the only key modifier is `children` which is list of it's keys + - USAGE: e.g. List of dictionaries where each dictionary have same structure. +- item options if is not used as widget + - required keys are `"key"` under which will be stored and `"label"` which will be shown in GUI + - this input can be expandable + - that can be set with key `"expandable"` as `True`/`False` (Default: `True`) + - with key `"expanded"` as `True`/`False` can be set that is expanded when GUI is opened (Default: `False`) + - it is possible to add darker background with `"highlight_content"` (Default: `False`) + - darker background has limits of maximum applies after 3-4 nested highlighted items there is not difference in the color ``` +# Example { "key": "applications", "type": "dict", @@ -76,6 +81,30 @@ ...ITEMS... ] } + +# When used as widget +{ + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": "dict-item", + "input_modifiers": { + "children": [ + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, { + "key": "hosts", + "label": "Hosts", + "type": "list", + "object_type": "text" + } + ... + ] + } +} ``` ## Inputs for setting any kind of value (`Pure` inputs) @@ -234,7 +263,7 @@ - should wraps multiple inputs only visually - these does not have `"key"` key and do not allow to have `"is_file"` or `"is_group"` modifiers enabled -### dict-form +### form - DEPRECATED - may be used only in `dict` and `dict-invisible` where is currently used grid layout so form is not needed - item is kept as still may be used in specific cases From ac26fb31eb873ad784202d8575c4f56e7071e913 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Sep 2020 18:01:23 +0200 Subject: [PATCH 89/92] implemented label widget used in grid layout --- .../settings/settings/widgets/widgets.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/pype/tools/settings/settings/widgets/widgets.py b/pype/tools/settings/settings/widgets/widgets.py index 400b9371fd..2a1f5fe804 100644 --- a/pype/tools/settings/settings/widgets/widgets.py +++ b/pype/tools/settings/settings/widgets/widgets.py @@ -226,3 +226,56 @@ class UnsavedChangesDialog(QtWidgets.QDialog): def on_discard_pressed(self): self.done(2) + + +class SpacerWidget(QtWidgets.QWidget): + def __init__(self, parent=None): + super(SpacerWidget, self).__init__(parent) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + +class GridLabelWidget(QtWidgets.QWidget): + def __init__(self, label, parent=None): + super(GridLabelWidget, self).__init__(parent) + + self.input_field = None + + self.properties = {} + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + label_proxy = QtWidgets.QWidget(self) + label_proxy_layout = QtWidgets.QHBoxLayout(label_proxy) + label_proxy_layout.setContentsMargins(0, 0, 0, 0) + label_proxy_layout.setSpacing(0) + + label_widget = QtWidgets.QLabel(label, label_proxy) + spacer_widget_h = SpacerWidget(label_proxy) + label_proxy_layout.addWidget( + spacer_widget_h, 0, alignment=QtCore.Qt.AlignRight + ) + label_proxy_layout.addWidget( + label_widget, 0, alignment=QtCore.Qt.AlignRight + ) + + spacer_widget_v = SpacerWidget(self) + + layout.addWidget(label_proxy, 0) + layout.addWidget(spacer_widget_v, 1) + + self.label_widget = label_widget + + def setProperty(self, name, value): + cur_value = self.properties.get(name) + if cur_value == value: + return + + self.label_widget.setProperty(name, value) + self.label_widget.style().polish(self.label_widget) + + def mouseReleaseEvent(self, event): + if self.input_field: + return self.input_field.show_actions_menu(event) + return super(GridLabelWidget, self).mouseReleaseEvent(event) From 65e9a6a2541f3cacc3f2a98c11c749a9798d50a4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Sep 2020 18:01:54 +0200 Subject: [PATCH 90/92] `show_actions_menu` is separate method now triggered with right click on item --- .../settings/settings/widgets/item_types.py | 124 ++++++++++-------- 1 file changed, 67 insertions(+), 57 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index fea78b713b..9f7d951241 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -285,65 +285,75 @@ class SettingObject: return "-".join(items) or "" + def show_actions_menu(self, event=None): + if event and event.button() != QtCore.Qt.RightButton: + return + + if not self.allow_actions: + if event: + return self.mouseReleaseEvent(event) + return + + menu = QtWidgets.QMenu() + + actions_mapping = {} + if self.child_modified: + action = QtWidgets.QAction("Discard changes") + actions_mapping[action] = self._discard_changes + menu.addAction(action) + + if ( + self.is_overidable + and not self.is_overriden + and not self.any_parent_is_group + ): + action = QtWidgets.QAction("Set project override") + actions_mapping[action] = self._set_as_overriden + menu.addAction(action) + + if ( + not self.is_overidable + and ( + self.has_studio_override + ) + ): + action = QtWidgets.QAction("Reset to pype default") + actions_mapping[action] = self._reset_to_pype_default + menu.addAction(action) + + if ( + not self.is_overidable + and not self.is_overriden + and not self.any_parent_is_group + and not self._had_studio_override + ): + action = QtWidgets.QAction("Set studio default") + actions_mapping[action] = self._set_studio_default + menu.addAction(action) + + if ( + not self.any_parent_overriden() + and (self.is_overriden or self.child_overriden) + ): + # TODO better label + action = QtWidgets.QAction("Remove project override") + actions_mapping[action] = self._remove_overrides + menu.addAction(action) + + if not actions_mapping: + action = QtWidgets.QAction("< No action >") + actions_mapping[action] = None + menu.addAction(action) + + result = menu.exec_(QtGui.QCursor.pos()) + if result: + to_run = actions_mapping[result] + if to_run: + to_run() + def mouseReleaseEvent(self, event): if self.allow_actions and event.button() == QtCore.Qt.RightButton: - menu = QtWidgets.QMenu() - - actions_mapping = {} - if self.child_modified: - action = QtWidgets.QAction("Discard changes") - actions_mapping[action] = self._discard_changes - menu.addAction(action) - - if ( - self.is_overidable - and not self.is_overriden - and not self.any_parent_is_group - ): - action = QtWidgets.QAction("Set project override") - actions_mapping[action] = self._set_as_overriden - menu.addAction(action) - - if ( - not self.is_overidable - and ( - self.has_studio_override - ) - ): - action = QtWidgets.QAction("Reset to pype default") - actions_mapping[action] = self._reset_to_pype_default - menu.addAction(action) - - if ( - not self.is_overidable - and not self.is_overriden - and not self.any_parent_is_group - and not self._had_studio_override - ): - action = QtWidgets.QAction("Set studio default") - actions_mapping[action] = self._set_studio_default - menu.addAction(action) - - if ( - not self.any_parent_overriden() - and (self.is_overriden or self.child_overriden) - ): - # TODO better label - action = QtWidgets.QAction("Remove project override") - actions_mapping[action] = self._remove_overrides - menu.addAction(action) - - if not actions_mapping: - action = QtWidgets.QAction("< No action >") - actions_mapping[action] = None - menu.addAction(action) - - result = menu.exec_(QtGui.QCursor.pos()) - if result: - to_run = actions_mapping[result] - if to_run: - to_run() - return + return self.show_actions_menu() mro = type(self).mro() index = mro.index(self.__class__) From 0d2110a6cce03fafe1682c5189904120c7777013 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Sep 2020 18:03:28 +0200 Subject: [PATCH 91/92] GridLabelWidget is used in dict and dict-invisible --- .../settings/settings/widgets/item_types.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 9f7d951241..213bd00a1b 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -5,7 +5,8 @@ from Qt import QtWidgets, QtCore, QtGui from .widgets import ( ExpandingWidget, NumberSpinBox, - PathInput + PathInput, + GridLabelWidget ) from .lib import NOT_SET, METADATA_KEY, TypeToKlass, CHILD_OFFSET from avalon.vendor import qtawesome @@ -2227,16 +2228,14 @@ class DictWidget(QtWidgets.QWidget, SettingObject): if not klass.expand_in_grid: label = child_configuration.get("label") if label is not None: - label_widget = QtWidgets.QLabel(label, self) - self.content_layout.addWidget( - label_widget, row, 0, 1, 1, - alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignTop - ) + label_widget = GridLabelWidget(label, self) + self.content_layout.addWidget(label_widget, row, 0, 1, 1) item = klass(child_configuration, self, label_widget=label_widget) item.value_changed.connect(self._on_value_change) if label_widget: + label_widget.input_field = item self.content_layout.addWidget(item, row, 1, 1, 1) else: self.content_layout.addWidget(item, row, 0, 1, 2) @@ -2535,16 +2534,14 @@ class DictInvisible(QtWidgets.QWidget, SettingObject): if not klass.expand_in_grid: label = child_configuration.get("label") if label is not None: - label_widget = QtWidgets.QLabel(label, self) - self.content_layout.addWidget( - label_widget, row, 0, 1, 1, - alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignTop - ) + label_widget = GridLabelWidget(label, self) + self.content_layout.addWidget(label_widget, row, 0, 1, 1) item = klass(child_configuration, self, label_widget=label_widget) item.value_changed.connect(self._on_value_change) if label_widget: + label_widget.input_field = item self.content_layout.addWidget(item, row, 1, 1, 1) else: self.content_layout.addWidget(item, row, 0, 1, 2) From b7301a37346cc315739b6f25d94e25219cfc22c1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Sep 2020 10:33:58 +0200 Subject: [PATCH 92/92] items are stretched in settings gui --- pype/tools/settings/settings/widgets/base.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pype/tools/settings/settings/widgets/base.py b/pype/tools/settings/settings/widgets/base.py index 423380d54c..ee4762de14 100644 --- a/pype/tools/settings/settings/widgets/base.py +++ b/pype/tools/settings/settings/widgets/base.py @@ -268,6 +268,10 @@ class SystemWidget(QtWidgets.QWidget): self.input_fields.append(item) self.content_layout.addWidget(item) + # Add spacer to stretch children guis + spacer = QtWidgets.QWidget(self.content_widget) + self.content_layout.addWidget(spacer, 1) + class ProjectListView(QtWidgets.QListView): left_mouse_released_at = QtCore.Signal(QtCore.QModelIndex) @@ -530,6 +534,10 @@ class ProjectWidget(QtWidgets.QWidget): self.input_fields.append(item) self.content_layout.addWidget(item) + # Add spacer to stretch children guis + spacer = QtWidgets.QWidget(self.content_widget) + self.content_layout.addWidget(spacer, 1) + def _on_project_change(self): project_name = self.project_list_widget.project_name() if project_name is None: