From 851b573a81798a85b61ac6e1bfb20b83e91e8d02 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 18 Aug 2022 21:21:56 +0800 Subject: [PATCH 001/275] add write_color_sets in create_rig and enable options of swtiching on/off for write_color_sets in create_model/rig --- .../hosts/maya/plugins/create/create_model.py | 4 +- .../hosts/maya/plugins/create/create_rig.py | 7 ++- .../defaults/project_settings/maya.json | 4 +- .../schemas/schema_maya_create.json | 60 ++++++++++++++++--- 4 files changed, 61 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_model.py b/openpype/hosts/maya/plugins/create/create_model.py index 37faad23a0..041d3a77e2 100644 --- a/openpype/hosts/maya/plugins/create/create_model.py +++ b/openpype/hosts/maya/plugins/create/create_model.py @@ -9,12 +9,12 @@ class CreateModel(plugin.Creator): family = "model" icon = "cube" defaults = ["Main", "Proxy", "_MD", "_HD", "_LD"] - + write_color_sets = False def __init__(self, *args, **kwargs): super(CreateModel, self).__init__(*args, **kwargs) # Vertex colors with the geometry - self.data["writeColorSets"] = False + self.data["writeColorSets"] = self.write_color_sets self.data["writeFaceSets"] = False # Include attributes by attribute name or prefix diff --git a/openpype/hosts/maya/plugins/create/create_rig.py b/openpype/hosts/maya/plugins/create/create_rig.py index 8032e5fbbd..37fadbe3e1 100644 --- a/openpype/hosts/maya/plugins/create/create_rig.py +++ b/openpype/hosts/maya/plugins/create/create_rig.py @@ -13,13 +13,16 @@ class CreateRig(plugin.Creator): label = "Rig" family = "rig" icon = "wheelchair" + write_color_sets = False + def __init__(self, *args, **kwargs): + super(CreateRig, self).__init__(*args, **kwargs) + self.data["writeColorSets"] = self.write_color_sets def process(self): with lib.undo_chunk(): instance = super(CreateRig, self).process() - self.log.info("Creating Rig instance set up ...") controls = cmds.sets(name="controls_SET", empty=True) pointcache = cmds.sets(name="out_SET", empty=True) - cmds.sets([controls, pointcache], forceElement=instance) + cmds.sets([controls, pointcache], forceElement=instance) \ No newline at end of file diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index ac0f161cf2..4e950aa8b5 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -33,7 +33,7 @@ }, "RenderSettings": { "apply_render_settings": true, - "default_render_image_folder": "", + "default_render_image_folder": "renders", "aov_separator": "underscore", "reset_current_frame": false, "arnold_renderer": { @@ -163,6 +163,7 @@ }, "CreateModel": { "enabled": true, + "write_color_sets": false, "defaults": [ "Main", "Proxy", @@ -183,6 +184,7 @@ }, "CreateRig": { "enabled": true, + "write_color_sets": false, "defaults": [ "Main", "Sim", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json index 431add28df..b9ef6cb80c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json @@ -135,6 +135,56 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "CreateModel", + "label": "Create Model", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "write_color_sets", + "label": "Write Color Sets" + }, + { + "type": "list", + "key": "defaults", + "label": "Default Subsets", + "object_type": "text" + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "CreateRig", + "label": "Create Rig", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "write_color_sets", + "label": "Write Color Sets" + }, + { + "type": "list", + "key": "defaults", + "label": "Default Subsets", + "object_type": "text" + } + ] + }, { "type": "dict", "collapsible": true, @@ -160,7 +210,7 @@ } ] }, - + { "type": "schema_template", "name": "template_create_plugin", @@ -197,10 +247,6 @@ "key": "CreateMayaScene", "label": "Create Maya Scene" }, - { - "key": "CreateModel", - "label": "Create Model" - }, { "key": "CreateRenderSetup", "label": "Create Render Setup" @@ -209,10 +255,6 @@ "key": "CreateReview", "label": "Create Review" }, - { - "key": "CreateRig", - "label": "Create Rig" - }, { "key": "CreateSetDress", "label": "Create Set Dress" From d90fa8b8fba57992894272827576b9bfd354fd4b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 18 Aug 2022 21:32:56 +0800 Subject: [PATCH 002/275] add write_color_sets in create_rig and enable options of swtiching on/off for write_color_sets in create_model/rig --- openpype/hosts/maya/plugins/create/create_rig.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_rig.py b/openpype/hosts/maya/plugins/create/create_rig.py index 37fadbe3e1..9484605076 100644 --- a/openpype/hosts/maya/plugins/create/create_rig.py +++ b/openpype/hosts/maya/plugins/create/create_rig.py @@ -14,6 +14,7 @@ class CreateRig(plugin.Creator): family = "rig" icon = "wheelchair" write_color_sets = False + def __init__(self, *args, **kwargs): super(CreateRig, self).__init__(*args, **kwargs) self.data["writeColorSets"] = self.write_color_sets @@ -25,4 +26,4 @@ class CreateRig(plugin.Creator): self.log.info("Creating Rig instance set up ...") controls = cmds.sets(name="controls_SET", empty=True) pointcache = cmds.sets(name="out_SET", empty=True) - cmds.sets([controls, pointcache], forceElement=instance) \ No newline at end of file + cmds.sets([controls, pointcache], forceElement=instance) From 745386decba851f07fcc72b8e9cac7d758ca4ef9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 18 Aug 2022 22:12:05 +0800 Subject: [PATCH 003/275] add write_color_sets in create_rig and enable options of swtiching on/off for write_color_sets in create_model/rig --- openpype/hosts/maya/plugins/create/create_rig.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/plugins/create/create_rig.py b/openpype/hosts/maya/plugins/create/create_rig.py index 9484605076..8eb1fab5e0 100644 --- a/openpype/hosts/maya/plugins/create/create_rig.py +++ b/openpype/hosts/maya/plugins/create/create_rig.py @@ -18,6 +18,7 @@ class CreateRig(plugin.Creator): def __init__(self, *args, **kwargs): super(CreateRig, self).__init__(*args, **kwargs) self.data["writeColorSets"] = self.write_color_sets + self.data["writeFaceSets"] = False def process(self): From 31cc50534439315117e7bd68626a5fe807df3f2a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 19 Aug 2022 20:23:32 +0800 Subject: [PATCH 004/275] add write_color_sets in create_rig and enable options of swtiching on/off for write_color_sets in create_model/rig --- .../maya/plugins/create/create_animation.py | 3 ++- .../hosts/maya/plugins/create/create_model.py | 3 ++- .../maya/plugins/create/create_pointcache.py | 4 +++- .../hosts/maya/plugins/create/create_rig.py | 3 ++- .../defaults/project_settings/maya.json | 4 ++++ .../schemas/schema_maya_create.json | 20 +++++++++++++++++++ 6 files changed, 33 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_animation.py b/openpype/hosts/maya/plugins/create/create_animation.py index e47d4e5b5a..5ef5f61ab1 100644 --- a/openpype/hosts/maya/plugins/create/create_animation.py +++ b/openpype/hosts/maya/plugins/create/create_animation.py @@ -12,6 +12,7 @@ class CreateAnimation(plugin.Creator): family = "animation" icon = "male" write_color_sets = False + write_face_sets = False def __init__(self, *args, **kwargs): super(CreateAnimation, self).__init__(*args, **kwargs) @@ -24,7 +25,7 @@ class CreateAnimation(plugin.Creator): # Write vertex colors with the geometry. self.data["writeColorSets"] = self.write_color_sets - self.data["writeFaceSets"] = False + self.data["writeFaceSets"] = self.write_face_sets # Include only renderable visible shapes. # Skips locators and empty transforms diff --git a/openpype/hosts/maya/plugins/create/create_model.py b/openpype/hosts/maya/plugins/create/create_model.py index 041d3a77e2..520e962f74 100644 --- a/openpype/hosts/maya/plugins/create/create_model.py +++ b/openpype/hosts/maya/plugins/create/create_model.py @@ -10,12 +10,13 @@ class CreateModel(plugin.Creator): icon = "cube" defaults = ["Main", "Proxy", "_MD", "_HD", "_LD"] write_color_sets = False + write_face_sets = False def __init__(self, *args, **kwargs): super(CreateModel, self).__init__(*args, **kwargs) # Vertex colors with the geometry self.data["writeColorSets"] = self.write_color_sets - self.data["writeFaceSets"] = False + self.data["writeFaceSets"] = self.write_face_sets # Include attributes by attribute name or prefix self.data["attr"] = "" diff --git a/openpype/hosts/maya/plugins/create/create_pointcache.py b/openpype/hosts/maya/plugins/create/create_pointcache.py index 5516445de8..ab8fe12079 100644 --- a/openpype/hosts/maya/plugins/create/create_pointcache.py +++ b/openpype/hosts/maya/plugins/create/create_pointcache.py @@ -12,6 +12,7 @@ class CreatePointCache(plugin.Creator): family = "pointcache" icon = "gears" write_color_sets = False + write_face_sets = False def __init__(self, *args, **kwargs): super(CreatePointCache, self).__init__(*args, **kwargs) @@ -21,7 +22,8 @@ class CreatePointCache(plugin.Creator): # Vertex colors with the geometry. self.data["writeColorSets"] = self.write_color_sets - self.data["writeFaceSets"] = False # Vertex colors with the geometry. + # Vertex colors with the geometry. + self.data["writeFaceSets"] = self.write_face_sets self.data["renderableOnly"] = False # Only renderable visible shapes self.data["visibleOnly"] = False # only nodes that are visible self.data["includeParentHierarchy"] = False # Include parent groups diff --git a/openpype/hosts/maya/plugins/create/create_rig.py b/openpype/hosts/maya/plugins/create/create_rig.py index 8eb1fab5e0..3b0ee1e22a 100644 --- a/openpype/hosts/maya/plugins/create/create_rig.py +++ b/openpype/hosts/maya/plugins/create/create_rig.py @@ -14,11 +14,12 @@ class CreateRig(plugin.Creator): family = "rig" icon = "wheelchair" write_color_sets = False + write_face_sets = False def __init__(self, *args, **kwargs): super(CreateRig, self).__init__(*args, **kwargs) self.data["writeColorSets"] = self.write_color_sets - self.data["writeFaceSets"] = False + self.data["writeFaceSets"] = self.write_face_sets def process(self): diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 4e950aa8b5..b4164c63f0 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -102,6 +102,7 @@ "CreateAnimation": { "enabled": true, "write_color_sets": false, + "write_face_sets": false, "defaults": [ "Main" ] @@ -109,6 +110,7 @@ "CreatePointCache": { "enabled": true, "write_color_sets": false, + "write_face_sets": false, "defaults": [ "Main" ] @@ -164,6 +166,7 @@ "CreateModel": { "enabled": true, "write_color_sets": false, + "write_face_sets": false, "defaults": [ "Main", "Proxy", @@ -185,6 +188,7 @@ "CreateRig": { "enabled": true, "write_color_sets": false, + "write_face_sets": false, "defaults": [ "Main", "Sim", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json index b9ef6cb80c..7e12897336 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json @@ -127,6 +127,11 @@ "key": "write_color_sets", "label": "Write Color Sets" }, + { + "type": "boolean", + "key": "write_face_sets", + "label": "Write Face Sets" + }, { "type": "list", "key": "defaults", @@ -152,6 +157,11 @@ "key": "write_color_sets", "label": "Write Color Sets" }, + { + "type": "boolean", + "key": "write_face_sets", + "label": "Write Face Sets" + }, { "type": "list", "key": "defaults", @@ -177,6 +187,11 @@ "key": "write_color_sets", "label": "Write Color Sets" }, + { + "type": "boolean", + "key": "write_face_sets", + "label": "Write Face Sets" + }, { "type": "list", "key": "defaults", @@ -202,6 +217,11 @@ "key": "write_color_sets", "label": "Write Color Sets" }, + { + "type": "boolean", + "key": "write_face_sets", + "label": "Write Face Sets" + }, { "type": "list", "key": "defaults", From c69736a597cf5b20d6090a5c20f0d05494679852 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 10:52:07 +0200 Subject: [PATCH 005/275] tvpaint is installed as object ingeriting HostBase --- openpype/hosts/tvpaint/api/__init__.py | 12 +- openpype/hosts/tvpaint/api/launch_script.py | 7 +- openpype/hosts/tvpaint/api/pipeline.py | 214 ++++++++++++-------- 3 files changed, 131 insertions(+), 102 deletions(-) diff --git a/openpype/hosts/tvpaint/api/__init__.py b/openpype/hosts/tvpaint/api/__init__.py index 43d411d8f9..b07658c583 100644 --- a/openpype/hosts/tvpaint/api/__init__.py +++ b/openpype/hosts/tvpaint/api/__init__.py @@ -5,11 +5,7 @@ from . import workio from . import pipeline from . import plugin from .pipeline import ( - install, - maintained_selection, - remove_instance, - list_instances, - ls + TVPaintHost, ) from .workio import ( @@ -31,11 +27,7 @@ __all__ = ( "pipeline", "plugin", - "install", - "maintained_selection", - "remove_instance", - "list_instances", - "ls", + "TVPaintHost", # Workfiles API "open_file", diff --git a/openpype/hosts/tvpaint/api/launch_script.py b/openpype/hosts/tvpaint/api/launch_script.py index 0b25027fc6..c474a10529 100644 --- a/openpype/hosts/tvpaint/api/launch_script.py +++ b/openpype/hosts/tvpaint/api/launch_script.py @@ -10,10 +10,10 @@ from Qt import QtWidgets, QtCore, QtGui from openpype import style from openpype.pipeline import install_host -from openpype.hosts.tvpaint.api.communication_server import ( - CommunicationWrapper +from openpype.hosts.tvpaint.api import ( + TVPaintHost, + CommunicationWrapper, ) -from openpype.hosts.tvpaint import api as tvpaint_host log = logging.getLogger(__name__) @@ -30,6 +30,7 @@ def main(launch_args): # - QApplicaiton is also main thread/event loop of the server qt_app = QtWidgets.QApplication([]) + tvpaint_host = TVPaintHost() # Execute pipeline installation install_host(tvpaint_host) diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index 427c927264..6c90de2aa9 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -1,6 +1,5 @@ import os import json -import contextlib import tempfile import logging @@ -9,7 +8,8 @@ import requests import pyblish.api from openpype.client import get_project, get_asset_by_name -from openpype.hosts import tvpaint +from openpype.host import HostBase, IWorkfileHost, ILoadHost +from openpype.hosts.tvpaint import TVPAINT_ROOT_DIR from openpype.api import get_current_project_settings from openpype.lib import register_event_callback from openpype.pipeline import ( @@ -26,11 +26,6 @@ from .lib import ( log = logging.getLogger(__name__) -HOST_DIR = os.path.dirname(os.path.abspath(tvpaint.__file__)) -PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") -PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") -LOAD_PATH = os.path.join(PLUGINS_DIR, "load") -CREATE_PATH = os.path.join(PLUGINS_DIR, "create") METADATA_SECTION = "avalon" SECTION_NAME_CONTEXT = "context" @@ -63,30 +58,132 @@ instances=2 """ -def install(): - """Install TVPaint-specific functionality.""" +class TVPaintHost(HostBase, IWorkfileHost, ILoadHost): + name = "tvpaint" - log.info("OpenPype - Installing TVPaint integration") - legacy_io.install() + def install(self): + """Install TVPaint-specific functionality.""" - # Create workdir folder if does not exist yet - workdir = legacy_io.Session["AVALON_WORKDIR"] - if not os.path.exists(workdir): - os.makedirs(workdir) + log.info("OpenPype - Installing TVPaint integration") + legacy_io.install() - pyblish.api.register_host("tvpaint") - pyblish.api.register_plugin_path(PUBLISH_PATH) - register_loader_plugin_path(LOAD_PATH) - register_creator_plugin_path(CREATE_PATH) + # Create workdir folder if does not exist yet + workdir = legacy_io.Session["AVALON_WORKDIR"] + if not os.path.exists(workdir): + os.makedirs(workdir) - registered_callbacks = ( - pyblish.api.registered_callbacks().get("instanceToggled") or [] - ) - if on_instance_toggle not in registered_callbacks: - pyblish.api.register_callback("instanceToggled", on_instance_toggle) + plugins_dir = os.path.join(TVPAINT_ROOT_DIR, "plugins") + publish_dir = os.path.join(plugins_dir, "publish") + load_dir = os.path.join(plugins_dir, "load") + create_dir = os.path.join(plugins_dir, "create") - register_event_callback("application.launched", initial_launch) - register_event_callback("application.exit", application_exit) + pyblish.api.register_host("tvpaint") + pyblish.api.register_plugin_path(publish_dir) + register_loader_plugin_path(load_dir) + register_creator_plugin_path(create_dir) + + registered_callbacks = ( + pyblish.api.registered_callbacks().get("instanceToggled") or [] + ) + if self.on_instance_toggle not in registered_callbacks: + pyblish.api.register_callback( + "instanceToggled", self.on_instance_toggle + ) + + register_event_callback("application.launched", self.initial_launch) + register_event_callback("application.exit", self.application_exit) + + def open_workfile(self, filepath): + george_script = "tv_LoadProject '\"'\"{}\"'\"'".format( + filepath.replace("\\", "/") + ) + return execute_george_through_file(george_script) + + def save_workfile(self, filepath=None): + if not filepath: + filepath = self.get_current_workfile() + context = { + "project": legacy_io.Session["AVALON_PROJECT"], + "asset": legacy_io.Session["AVALON_ASSET"], + "task": legacy_io.Session["AVALON_TASK"] + } + save_current_workfile_context(context) + + # Execute george script to save workfile. + george_script = "tv_SaveProject {}".format(filepath.replace("\\", "/")) + return execute_george(george_script) + + def work_root(self, session): + return session["AVALON_WORKDIR"] + + def get_current_workfile(self): + return execute_george("tv_GetProjectName") + + def workfile_has_unsaved_changes(self): + return None + + def get_workfile_extensions(self): + return [".tvpp"] + + def get_containers(self): + return get_containers() + + def initial_launch(self): + # Setup project settings if its the template that's launched. + # TODO also check for template creation when it's possible to define + # templates + last_workfile = os.environ.get("AVALON_LAST_WORKFILE") + if not last_workfile or os.path.exists(last_workfile): + return + + log.info("Setting up project...") + set_context_settings() + + def application_exit(self): + """Logic related to TimerManager. + + Todo: + This should be handled out of TVPaint integration logic. + """ + + data = get_current_project_settings() + stop_timer = data["tvpaint"]["stop_timer_on_application_exit"] + + if not stop_timer: + return + + # Stop application timer. + webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL") + rest_api_url = "{}/timers_manager/stop_timer".format(webserver_url) + requests.post(rest_api_url) + + def on_instance_toggle(self, instance, old_value, new_value): + """Update instance data in workfile on publish toggle.""" + # Review may not have real instance in wokrfile metadata + if not instance.data.get("uuid"): + return + + instance_id = instance.data["uuid"] + found_idx = None + current_instances = list_instances() + for idx, workfile_instance in enumerate(current_instances): + if workfile_instance["uuid"] == instance_id: + found_idx = idx + break + + if found_idx is None: + return + + if "active" in current_instances[found_idx]: + current_instances[found_idx]["active"] = new_value + self.write_instances(current_instances) + + def list_instances(self): + """List all created instances from current workfile.""" + return list_instances() + + def write_instances(self, data): + return write_instances(data) def containerise( @@ -116,7 +213,7 @@ def containerise( "representation": str(context["representation"]["_id"]) } if current_containers is None: - current_containers = ls() + current_containers = get_containers() # Add container to containers list current_containers.append(container_data) @@ -127,15 +224,6 @@ def containerise( return container_data -@contextlib.contextmanager -def maintained_selection(): - # TODO implement logic - try: - yield - finally: - pass - - def split_metadata_string(text, chunk_length=None): """Split string by length. @@ -359,12 +447,7 @@ def write_instances(data): return write_workfile_metadata(SECTION_NAME_INSTANCES, data) -# Backwards compatibility -def _write_instances(*args, **kwargs): - return write_instances(*args, **kwargs) - - -def ls(): +def get_containers(): output = get_workfile_metadata(SECTION_NAME_CONTAINERS) if output: for item in output: @@ -376,53 +459,6 @@ def ls(): return output -def on_instance_toggle(instance, old_value, new_value): - """Update instance data in workfile on publish toggle.""" - # Review may not have real instance in wokrfile metadata - if not instance.data.get("uuid"): - return - - instance_id = instance.data["uuid"] - found_idx = None - current_instances = list_instances() - for idx, workfile_instance in enumerate(current_instances): - if workfile_instance["uuid"] == instance_id: - found_idx = idx - break - - if found_idx is None: - return - - if "active" in current_instances[found_idx]: - current_instances[found_idx]["active"] = new_value - write_instances(current_instances) - - -def initial_launch(): - # Setup project settings if its the template that's launched. - # TODO also check for template creation when it's possible to define - # templates - last_workfile = os.environ.get("AVALON_LAST_WORKFILE") - if not last_workfile or os.path.exists(last_workfile): - return - - log.info("Setting up project...") - set_context_settings() - - -def application_exit(): - data = get_current_project_settings() - stop_timer = data["tvpaint"]["stop_timer_on_application_exit"] - - if not stop_timer: - return - - # Stop application timer. - webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL") - rest_api_url = "{}/timers_manager/stop_timer".format(webserver_url) - requests.post(rest_api_url) - - def set_context_settings(asset_doc=None): """Set workfile settings by asset document data. From 0b473de76bbe3548a2d43cba7c083a415b5d7f78 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 10:53:27 +0200 Subject: [PATCH 006/275] changed imports in plugin logic --- openpype/hosts/tvpaint/api/plugin.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/tvpaint/api/plugin.py b/openpype/hosts/tvpaint/api/plugin.py index 15ad8905e0..da456e7067 100644 --- a/openpype/hosts/tvpaint/api/plugin.py +++ b/openpype/hosts/tvpaint/api/plugin.py @@ -4,11 +4,11 @@ import uuid from openpype.pipeline import ( LegacyCreator, LoaderPlugin, + registered_host, ) -from openpype.hosts.tvpaint.api import ( - pipeline, - lib -) + +from .lib import get_layers_data +from .pipeline import get_current_workfile_context class Creator(LegacyCreator): @@ -22,7 +22,7 @@ class Creator(LegacyCreator): dynamic_data = super(Creator, cls).get_dynamic_data(*args, **kwargs) # Change asset and name by current workfile context - workfile_context = pipeline.get_current_workfile_context() + workfile_context = get_current_workfile_context() asset_name = workfile_context.get("asset") task_name = workfile_context.get("task") if "asset" not in dynamic_data and asset_name: @@ -67,10 +67,12 @@ class Creator(LegacyCreator): self.log.debug( "Storing instance data to workfile. {}".format(str(data)) ) - return pipeline.write_instances(data) + host = registered_host() + return host.write_instances(data) def process(self): - data = pipeline.list_instances() + host = registered_host() + data = host.list_instances() data.append(self.data) self.write_instances(data) @@ -108,7 +110,7 @@ class Loader(LoaderPlugin): counter_regex = re.compile(r"_(\d{3})$") higher_counter = 0 - for layer in lib.get_layers_data(): + for layer in get_layers_data(): layer_name = layer["name"] if not layer_name.startswith(layer_name_base): continue From bcad1ab7fd120938593d59d35fc39cae2677cbc8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 10:53:43 +0200 Subject: [PATCH 007/275] changed import of CommunicationsWrapper --- openpype/hosts/tvpaint/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/api/lib.py b/openpype/hosts/tvpaint/api/lib.py index a341f48859..5e64773b8e 100644 --- a/openpype/hosts/tvpaint/api/lib.py +++ b/openpype/hosts/tvpaint/api/lib.py @@ -2,7 +2,7 @@ import os import logging import tempfile -from . import CommunicationWrapper +from .communication_server import CommunicationWrapper log = logging.getLogger(__name__) From 88b900bda06e7b13bee7e62a2185cb4eefab3c65 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 10:54:58 +0200 Subject: [PATCH 008/275] use explicit imports --- .../plugins/create/create_render_layer.py | 22 +++++---- .../plugins/create/create_render_pass.py | 10 ++-- .../hosts/tvpaint/plugins/load/load_image.py | 5 +- .../plugins/load/load_reference_image.py | 41 +++++++++++----- .../hosts/tvpaint/plugins/load/load_sound.py | 7 ++- .../tvpaint/plugins/load/load_workfile.py | 21 +++++--- .../plugins/publish/collect_workfile_data.py | 49 ++++++++++++------- .../plugins/publish/extract_sequence.py | 20 +++++--- .../publish/increment_workfile_version.py | 7 +-- .../plugins/publish/validate_asset_name.py | 9 ++-- .../tvpaint/plugins/publish/validate_marks.py | 6 +-- .../plugins/publish/validate_start_frame.py | 6 +-- .../publish/validate_workfile_metadata.py | 6 +-- 13 files changed, 132 insertions(+), 77 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py index 3b5bd47189..a085830e96 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py @@ -1,11 +1,15 @@ -from openpype.pipeline import CreatorError from openpype.lib import prepare_template_data +from openpype.pipeline import CreatorError from openpype.hosts.tvpaint.api import ( plugin, - pipeline, - lib, CommunicationWrapper ) +from openpype.hosts.tvpaint.api.lib import ( + get_layers_data, + get_groups_data, + execute_george_through_file, +) +from openpype.hosts.tvpaint.api.pipeline import list_instances class CreateRenderlayer(plugin.Creator): @@ -63,7 +67,7 @@ class CreateRenderlayer(plugin.Creator): # Validate that communication is initialized if CommunicationWrapper.communicator: # Get currently selected layers - layers_data = lib.get_layers_data() + layers_data = get_layers_data() selected_layers = [ layer @@ -81,8 +85,8 @@ class CreateRenderlayer(plugin.Creator): def process(self): self.log.debug("Query data from workfile.") - instances = pipeline.list_instances() - layers_data = lib.get_layers_data() + instances = list_instances() + layers_data = get_layers_data() self.log.debug("Checking for selection groups.") # Collect group ids from selection @@ -109,7 +113,7 @@ class CreateRenderlayer(plugin.Creator): self.log.debug(f"Selected group id is \"{group_id}\".") self.data["group_id"] = group_id - group_data = lib.get_groups_data() + group_data = get_groups_data() group_name = None for group in group_data: if group["group_id"] == group_id: @@ -176,7 +180,7 @@ class CreateRenderlayer(plugin.Creator): return self.log.debug("Querying groups data from workfile.") - groups_data = lib.get_groups_data() + groups_data = get_groups_data() self.log.debug("Changing name of the group.") selected_group = None @@ -195,7 +199,7 @@ class CreateRenderlayer(plugin.Creator): b=selected_group["blue"], name=new_group_name ) - lib.execute_george_through_file(rename_script) + execute_george_through_file(rename_script) self.log.info( f"Name of group with index {group_id}" diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py index 26fa8ac51a..a44cb29f20 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py @@ -2,10 +2,10 @@ from openpype.pipeline import CreatorError from openpype.lib import prepare_template_data from openpype.hosts.tvpaint.api import ( plugin, - pipeline, - lib, CommunicationWrapper ) +from openpype.hosts.tvpaint.api.lib import get_layers_data +from openpype.hosts.tvpaint.api.pipeline import list_instances class CreateRenderPass(plugin.Creator): @@ -54,7 +54,7 @@ class CreateRenderPass(plugin.Creator): # Validate that communication is initialized if CommunicationWrapper.communicator: # Get currently selected layers - layers_data = lib.layers_data() + layers_data = get_layers_data() selected_layers = [ layer @@ -72,8 +72,8 @@ class CreateRenderPass(plugin.Creator): def process(self): self.log.debug("Query data from workfile.") - instances = pipeline.list_instances() - layers_data = lib.layers_data() + instances = list_instances() + layers_data = get_layers_data() self.log.debug("Checking selection.") # Get all selected layers and their group ids diff --git a/openpype/hosts/tvpaint/plugins/load/load_image.py b/openpype/hosts/tvpaint/plugins/load/load_image.py index f861d0119e..151db94135 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_image.py +++ b/openpype/hosts/tvpaint/plugins/load/load_image.py @@ -1,5 +1,6 @@ import qargparse -from openpype.hosts.tvpaint.api import lib, plugin +from openpype.hosts.tvpaint.api import plugin +from openpype.hosts.tvpaint.api.lib import execute_george_through_file class ImportImage(plugin.Loader): @@ -79,4 +80,4 @@ class ImportImage(plugin.Loader): layer_name, load_options_str ) - return lib.execute_george_through_file(george_script) + return execute_george_through_file(george_script) diff --git a/openpype/hosts/tvpaint/plugins/load/load_reference_image.py b/openpype/hosts/tvpaint/plugins/load/load_reference_image.py index af1a4a9b6b..393236fba6 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_reference_image.py +++ b/openpype/hosts/tvpaint/plugins/load/load_reference_image.py @@ -1,7 +1,21 @@ import collections + import qargparse -from openpype.pipeline import get_representation_context -from openpype.hosts.tvpaint.api import lib, pipeline, plugin + +from openpype.pipeline import ( + get_representation_context, + register_host, +) +from openpype.hosts.tvpaint.api import plugin +from openpype.hosts.tvpaint.api.lib import ( + get_layers_data, + execute_george_through_file, +) +from openpype.hosts.tvpaint.api.pipeline import ( + write_workfile_metadata, + SECTION_NAME_CONTAINERS, + containerise, +) class LoadImage(plugin.Loader): @@ -79,10 +93,10 @@ class LoadImage(plugin.Loader): load_options_str ) - lib.execute_george_through_file(george_script) + execute_george_through_file(george_script) loaded_layer = None - layers = lib.layers_data() + layers = get_layers_data() for layer in layers: if layer["name"] == layer_name: loaded_layer = layer @@ -95,7 +109,7 @@ class LoadImage(plugin.Loader): layer_names = [loaded_layer["name"]] namespace = namespace or layer_name - return pipeline.containerise( + return containerise( name=name, namespace=namespace, members=layer_names, @@ -109,7 +123,7 @@ class LoadImage(plugin.Loader): return if layers is None: - layers = lib.layers_data() + layers = get_layers_data() available_ids = set(layer["layer_id"] for layer in layers) @@ -152,14 +166,15 @@ class LoadImage(plugin.Loader): line = "tv_layerkill {}".format(layer_id) george_script_lines.append(line) george_script = "\n".join(george_script_lines) - lib.execute_george_through_file(george_script) + execute_george_through_file(george_script) def _remove_container(self, container, members=None): if not container: return representation = container["representation"] members = self.get_members_from_container(container) - current_containers = pipeline.ls() + host = register_host() + current_containers = host.get_containers() pop_idx = None for idx, cur_con in enumerate(current_containers): cur_members = self.get_members_from_container(cur_con) @@ -179,8 +194,8 @@ class LoadImage(plugin.Loader): return current_containers.pop(pop_idx) - pipeline.write_workfile_metadata( - pipeline.SECTION_NAME_CONTAINERS, current_containers + write_workfile_metadata( + SECTION_NAME_CONTAINERS, current_containers ) def remove(self, container): @@ -214,7 +229,7 @@ class LoadImage(plugin.Loader): break old_layers = [] - layers = lib.layers_data() + layers = get_layers_data() previous_layer_ids = set(layer["layer_id"] for layer in layers) if old_layers_are_ids: for layer in layers: @@ -263,7 +278,7 @@ class LoadImage(plugin.Loader): new_container = self.load(context, name, namespace, {}) new_layer_names = self.get_members_from_container(new_container) - layers = lib.layers_data() + layers = get_layers_data() new_layers = [] for layer in layers: @@ -304,4 +319,4 @@ class LoadImage(plugin.Loader): # Execute george scripts if there are any if george_script_lines: george_script = "\n".join(george_script_lines) - lib.execute_george_through_file(george_script) + execute_george_through_file(george_script) diff --git a/openpype/hosts/tvpaint/plugins/load/load_sound.py b/openpype/hosts/tvpaint/plugins/load/load_sound.py index 3f42370f5c..f312db262a 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_sound.py +++ b/openpype/hosts/tvpaint/plugins/load/load_sound.py @@ -1,6 +1,9 @@ import os import tempfile -from openpype.hosts.tvpaint.api import lib, plugin +from openpype.hosts.tvpaint.api import plugin +from openpype.hosts.tvpaint.api.lib import ( + execute_george_through_file, +) class ImportSound(plugin.Loader): @@ -64,7 +67,7 @@ class ImportSound(plugin.Loader): ) self.log.info("*** George script:\n{}\n***".format(george_script)) # Execute geoge script - lib.execute_george_through_file(george_script) + execute_george_through_file(george_script) # Read output file lines = [] diff --git a/openpype/hosts/tvpaint/plugins/load/load_workfile.py b/openpype/hosts/tvpaint/plugins/load/load_workfile.py index a99b300730..fc7588f56e 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_workfile.py +++ b/openpype/hosts/tvpaint/plugins/load/load_workfile.py @@ -11,7 +11,13 @@ from openpype.pipeline.workfile import ( get_last_workfile_with_version, ) from openpype.pipeline.template_data import get_template_data_with_names -from openpype.hosts.tvpaint.api import lib, pipeline, plugin +from openpype.hosts.tvpaint.api import plugin +from openpype.hosts.tvpaint.api.lib import ( + execute_george_through_file, +) +from openpype.hosts.tvpaint.api.pipeline import ( + get_current_workfile_context, +) class LoadWorkfile(plugin.Loader): @@ -26,9 +32,9 @@ class LoadWorkfile(plugin.Loader): # Load context of current workfile as first thing # - which context and extension has host = registered_host() - current_file = host.current_file() + current_file = host.get_current_workfile() - context = pipeline.get_current_workfile_context() + context = get_current_workfile_context() filepath = self.fname.replace("\\", "/") @@ -40,7 +46,7 @@ class LoadWorkfile(plugin.Loader): george_script = "tv_LoadProject '\"'\"{}\"'\"'".format( filepath ) - lib.execute_george_through_file(george_script) + execute_george_through_file(george_script) # Save workfile. host_name = "tvpaint" @@ -69,12 +75,13 @@ class LoadWorkfile(plugin.Loader): file_template = anatomy.templates[template_key]["file"] # Define saving file extension + extensions = host.get_workfile_extensions() if current_file: # Match the extension of current file _, extension = os.path.splitext(current_file) else: # Fall back to the first extension supported for this host. - extension = host.file_extensions()[0] + extension = extensions[0] data["ext"] = extension @@ -83,7 +90,7 @@ class LoadWorkfile(plugin.Loader): folder_template, data ) version = get_last_workfile_with_version( - work_root, file_template, data, host.file_extensions() + work_root, file_template, data, extensions )[1] if version is None: @@ -97,4 +104,4 @@ class LoadWorkfile(plugin.Loader): file_template, data ) path = os.path.join(work_root, filename) - host.save_file(path) + host.save_workfile(path) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py index c59ef82f85..8fe71a4a46 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py @@ -5,7 +5,22 @@ import tempfile import pyblish.api from openpype.pipeline import legacy_io -from openpype.hosts.tvpaint.api import pipeline, lib +from openpype.hosts.tvpaint.api.lib import ( + execute_george, + execute_george_through_file, + get_layers_data, + get_groups_data, +) +from openpype.hosts.tvpaint.api.pipeline import ( + SECTION_NAME_CONTEXT, + SECTION_NAME_INSTANCES, + SECTION_NAME_CONTAINERS, + + get_workfile_metadata_string, + write_workfile_metadata, + get_current_workfile_context, + list_instances, +) class ResetTVPaintWorkfileMetadata(pyblish.api.Action): @@ -15,12 +30,12 @@ class ResetTVPaintWorkfileMetadata(pyblish.api.Action): def process(self, context, plugin): metadata_keys = { - pipeline.SECTION_NAME_CONTEXT: {}, - pipeline.SECTION_NAME_INSTANCES: [], - pipeline.SECTION_NAME_CONTAINERS: [] + SECTION_NAME_CONTEXT: {}, + SECTION_NAME_INSTANCES: [], + SECTION_NAME_CONTAINERS: [] } for metadata_key, default in metadata_keys.items(): - json_string = pipeline.get_workfile_metadata_string(metadata_key) + json_string = get_workfile_metadata_string(metadata_key) if not json_string: continue @@ -35,7 +50,7 @@ class ResetTVPaintWorkfileMetadata(pyblish.api.Action): ).format(metadata_key, default, json_string), exc_info=True ) - pipeline.write_workfile_metadata(metadata_key, default) + write_workfile_metadata(metadata_key, default) class CollectWorkfileData(pyblish.api.ContextPlugin): @@ -45,8 +60,8 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): actions = [ResetTVPaintWorkfileMetadata] def process(self, context): - current_project_id = lib.execute_george("tv_projectcurrentid") - lib.execute_george("tv_projectselect {}".format(current_project_id)) + current_project_id = execute_george("tv_projectcurrentid") + execute_george("tv_projectselect {}".format(current_project_id)) # Collect and store current context to have reference current_context = { @@ -60,7 +75,7 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): # Collect context from workfile metadata self.log.info("Collecting workfile context") - workfile_context = pipeline.get_current_workfile_context() + workfile_context = get_current_workfile_context() # Store workfile context to pyblish context context.data["workfile_context"] = workfile_context if workfile_context: @@ -96,7 +111,7 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): # Collect instances self.log.info("Collecting instance data from workfile") - instance_data = pipeline.list_instances() + instance_data = list_instances() context.data["workfileInstances"] = instance_data self.log.debug( "Instance data:\"{}".format(json.dumps(instance_data, indent=4)) @@ -104,7 +119,7 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): # Collect information about layers self.log.info("Collecting layers data from workfile") - layers_data = lib.layers_data() + layers_data = get_layers_data() layers_by_name = {} for layer in layers_data: layer_name = layer["name"] @@ -120,14 +135,14 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): # Collect information about groups self.log.info("Collecting groups data from workfile") - group_data = lib.groups_data() + group_data = get_groups_data() context.data["groupsData"] = group_data self.log.debug( "Group data:\"{}".format(json.dumps(group_data, indent=4)) ) self.log.info("Collecting scene data from workfile") - workfile_info_parts = lib.execute_george("tv_projectinfo").split(" ") + workfile_info_parts = execute_george("tv_projectinfo").split(" ") # Project frame start - not used workfile_info_parts.pop(-1) @@ -139,10 +154,10 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): workfile_path = " ".join(workfile_info_parts).replace("\"", "") # Marks return as "{frame - 1} {state} ", example "0 set". - result = lib.execute_george("tv_markin") + result = execute_george("tv_markin") mark_in_frame, mark_in_state, _ = result.split(" ") - result = lib.execute_george("tv_markout") + result = execute_george("tv_markout") mark_out_frame, mark_out_state, _ = result.split(" ") scene_data = { @@ -156,7 +171,7 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): "sceneMarkInState": mark_in_state == "set", "sceneMarkOut": int(mark_out_frame), "sceneMarkOutState": mark_out_state == "set", - "sceneStartFrame": int(lib.execute_george("tv_startframe")), + "sceneStartFrame": int(execute_george("tv_startframe")), "sceneBgColor": self._get_bg_color() } self.log.debug( @@ -188,7 +203,7 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): ] george_script = "\n".join(george_script_lines) - lib.execute_george_through_file(george_script) + execute_george_through_file(george_script) with open(output_filepath, "r") as stream: data = stream.read() diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 77712347bd..1ebaf1da64 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -5,7 +5,13 @@ import tempfile from PIL import Image import pyblish.api -from openpype.hosts.tvpaint.api import lib + +from openpype.hosts.tvpaint.api.lib import ( + execute_george, + execute_george_through_file, + get_layers_pre_post_behavior, + get_layers_exposure_frames, +) from openpype.hosts.tvpaint.lib import ( calculate_layers_extraction_data, get_frame_filename_template, @@ -61,7 +67,7 @@ class ExtractSequence(pyblish.api.Extractor): # different way when Start Frame is not `0` # NOTE It will be set back after rendering scene_start_frame = instance.context.data["sceneStartFrame"] - lib.execute_george("tv_startframe 0") + execute_george("tv_startframe 0") # Frame start/end may be stored as float frame_start = int(instance.data["frameStart"]) @@ -113,7 +119,7 @@ class ExtractSequence(pyblish.api.Extractor): output_filepaths_by_frame_idx, thumbnail_fullpath = result # Change scene frame Start back to previous value - lib.execute_george("tv_startframe {}".format(scene_start_frame)) + execute_george("tv_startframe {}".format(scene_start_frame)) # Sequence of one frame if not output_filepaths_by_frame_idx: @@ -241,7 +247,7 @@ class ExtractSequence(pyblish.api.Extractor): george_script_lines.append(" ".join(orig_color_command)) - lib.execute_george_through_file("\n".join(george_script_lines)) + execute_george_through_file("\n".join(george_script_lines)) first_frame_filepath = None output_filepaths_by_frame_idx = {} @@ -304,8 +310,8 @@ class ExtractSequence(pyblish.api.Extractor): return [], None self.log.debug("Collecting pre/post behavior of individual layers.") - behavior_by_layer_id = lib.get_layers_pre_post_behavior(layer_ids) - exposure_frames_by_layer_id = lib.get_layers_exposure_frames( + behavior_by_layer_id = get_layers_pre_post_behavior(layer_ids) + exposure_frames_by_layer_id = get_layers_exposure_frames( layer_ids, layers ) extraction_data_by_layer_id = calculate_layers_extraction_data( @@ -410,7 +416,7 @@ class ExtractSequence(pyblish.api.Extractor): ",".join(frames_to_render), layer_id, layer["name"] )) # Let TVPaint render layer's image - lib.execute_george_through_file("\n".join(george_script_lines)) + execute_george_through_file("\n".join(george_script_lines)) # Fill frames between `frame_start_index` and `frame_end_index` self.log.debug("Filling frames not rendered frames.") diff --git a/openpype/hosts/tvpaint/plugins/publish/increment_workfile_version.py b/openpype/hosts/tvpaint/plugins/publish/increment_workfile_version.py index 24d6558168..a85caf2557 100644 --- a/openpype/hosts/tvpaint/plugins/publish/increment_workfile_version.py +++ b/openpype/hosts/tvpaint/plugins/publish/increment_workfile_version.py @@ -1,7 +1,7 @@ import pyblish.api -from openpype.api import version_up -from openpype.hosts.tvpaint.api import workio +from openpype.lib import version_up +from openpype.pipeline import registered_host class IncrementWorkfileVersion(pyblish.api.ContextPlugin): @@ -17,6 +17,7 @@ class IncrementWorkfileVersion(pyblish.api.ContextPlugin): assert all(result["success"] for result in context.data["results"]), ( "Publishing not successful so version is not increased.") + host = registered_host() path = context.data["currentFile"] - workio.save_file(version_up(path)) + host.save_workfile(version_up(path)) self.log.info('Incrementing workfile version') diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py b/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py index 70816f9f18..7e35726030 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py @@ -1,6 +1,9 @@ import pyblish.api from openpype.pipeline import PublishXmlValidationError -from openpype.hosts.tvpaint.api import pipeline +from openpype.hosts.tvpaint.api.pipeline import ( + list_instances, + write_instances, +) class FixAssetNames(pyblish.api.Action): @@ -15,7 +18,7 @@ class FixAssetNames(pyblish.api.Action): def process(self, context, plugin): context_asset_name = context.data["asset"] - old_instance_items = pipeline.list_instances() + old_instance_items = list_instances() new_instance_items = [] for instance_item in old_instance_items: instance_asset_name = instance_item.get("asset") @@ -25,7 +28,7 @@ class FixAssetNames(pyblish.api.Action): ): instance_item["asset"] = context_asset_name new_instance_items.append(instance_item) - pipeline._write_instances(new_instance_items) + write_instances(new_instance_items) class ValidateAssetNames(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_marks.py b/openpype/hosts/tvpaint/plugins/publish/validate_marks.py index d1f299e006..12d50e17ff 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_marks.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_marks.py @@ -2,7 +2,7 @@ import json import pyblish.api from openpype.pipeline import PublishXmlValidationError -from openpype.hosts.tvpaint.api import lib +from openpype.hosts.tvpaint.api.lib import execute_george class ValidateMarksRepair(pyblish.api.Action): @@ -15,10 +15,10 @@ class ValidateMarksRepair(pyblish.api.Action): def process(self, context, plugin): expected_data = ValidateMarks.get_expected_data(context) - lib.execute_george( + execute_george( "tv_markin {} set".format(expected_data["markIn"]) ) - lib.execute_george( + execute_george( "tv_markout {} set".format(expected_data["markOut"]) ) diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py b/openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py index ddc738c6ed..066e54c670 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py @@ -1,6 +1,6 @@ import pyblish.api from openpype.pipeline import PublishXmlValidationError -from openpype.hosts.tvpaint.api import lib +from openpype.hosts.tvpaint.api.lib import execute_george class RepairStartFrame(pyblish.api.Action): @@ -11,7 +11,7 @@ class RepairStartFrame(pyblish.api.Action): on = "failed" def process(self, context, plugin): - lib.execute_george("tv_startframe 0") + execute_george("tv_startframe 0") class ValidateStartFrame(pyblish.api.ContextPlugin): @@ -24,7 +24,7 @@ class ValidateStartFrame(pyblish.api.ContextPlugin): optional = True def process(self, context): - start_frame = lib.execute_george("tv_startframe") + start_frame = execute_george("tv_startframe") if start_frame == 0: return diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_workfile_metadata.py b/openpype/hosts/tvpaint/plugins/publish/validate_workfile_metadata.py index eac345f395..d66ae50c60 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_workfile_metadata.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_workfile_metadata.py @@ -1,6 +1,5 @@ import pyblish.api -from openpype.pipeline import PublishXmlValidationError -from openpype.hosts.tvpaint.api import save_file +from openpype.pipeline import PublishXmlValidationError, registered_host class ValidateWorkfileMetadataRepair(pyblish.api.Action): @@ -13,8 +12,9 @@ class ValidateWorkfileMetadataRepair(pyblish.api.Action): def process(self, context, _plugin): """Save current workfile which should trigger storing of metadata.""" current_file = context.data["currentFile"] + host = registered_host() # Save file should trigger - save_file(current_file) + host.save_workfile(current_file) class ValidateWorkfileMetadata(pyblish.api.ContextPlugin): From 326508888727c4626bf60e7469c7efab5ce6483a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 10:55:13 +0200 Subject: [PATCH 009/275] removed unused workio --- openpype/hosts/tvpaint/api/__init__.py | 17 -------- openpype/hosts/tvpaint/api/workio.py | 58 -------------------------- 2 files changed, 75 deletions(-) delete mode 100644 openpype/hosts/tvpaint/api/workio.py diff --git a/openpype/hosts/tvpaint/api/__init__.py b/openpype/hosts/tvpaint/api/__init__.py index b07658c583..5d42a8cc02 100644 --- a/openpype/hosts/tvpaint/api/__init__.py +++ b/openpype/hosts/tvpaint/api/__init__.py @@ -8,15 +8,6 @@ from .pipeline import ( TVPaintHost, ) -from .workio import ( - open_file, - save_file, - current_file, - has_unsaved_changes, - file_extensions, - work_root, -) - __all__ = ( "CommunicationWrapper", @@ -28,12 +19,4 @@ __all__ = ( "plugin", "TVPaintHost", - - # Workfiles API - "open_file", - "save_file", - "current_file", - "has_unsaved_changes", - "file_extensions", - "work_root" ) diff --git a/openpype/hosts/tvpaint/api/workio.py b/openpype/hosts/tvpaint/api/workio.py deleted file mode 100644 index 1a5ad00ca8..0000000000 --- a/openpype/hosts/tvpaint/api/workio.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Host API required for Work Files. -# TODO @iLLiCiT implement functions: - has_unsaved_changes -""" - -from openpype.pipeline import ( - HOST_WORKFILE_EXTENSIONS, - legacy_io, -) -from .lib import ( - execute_george, - execute_george_through_file -) -from .pipeline import save_current_workfile_context - - -def open_file(filepath): - """Open the scene file in Blender.""" - george_script = "tv_LoadProject '\"'\"{}\"'\"'".format( - filepath.replace("\\", "/") - ) - return execute_george_through_file(george_script) - - -def save_file(filepath): - """Save the open scene file.""" - # Store context to workfile before save - context = { - "project": legacy_io.Session["AVALON_PROJECT"], - "asset": legacy_io.Session["AVALON_ASSET"], - "task": legacy_io.Session["AVALON_TASK"] - } - save_current_workfile_context(context) - - # Execute george script to save workfile. - george_script = "tv_SaveProject {}".format(filepath.replace("\\", "/")) - return execute_george(george_script) - - -def current_file(): - """Return the path of the open scene file.""" - george_script = "tv_GetProjectName" - return execute_george(george_script) - - -def has_unsaved_changes(): - """Does the open scene file have unsaved changes?""" - return False - - -def file_extensions(): - """Return the supported file extensions for Blender scene files.""" - return HOST_WORKFILE_EXTENSIONS["tvpaint"] - - -def work_root(session): - """Return the default root to browse for work files.""" - return session["AVALON_WORKDIR"] From dbd983f8c8c35c7d747c247e9593d451b42f6d5d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 23 Aug 2022 10:55:23 +0200 Subject: [PATCH 010/275] cleanup imports in api init file --- openpype/hosts/tvpaint/api/__init__.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/openpype/hosts/tvpaint/api/__init__.py b/openpype/hosts/tvpaint/api/__init__.py index 5d42a8cc02..7b53aad9a4 100644 --- a/openpype/hosts/tvpaint/api/__init__.py +++ b/openpype/hosts/tvpaint/api/__init__.py @@ -1,9 +1,4 @@ from .communication_server import CommunicationWrapper -from . import lib -from . import launch_script -from . import workio -from . import pipeline -from . import plugin from .pipeline import ( TVPaintHost, ) @@ -12,11 +7,5 @@ from .pipeline import ( __all__ = ( "CommunicationWrapper", - "lib", - "launch_script", - "workio", - "pipeline", - "plugin", - "TVPaintHost", ) From c238a9cbc15d516b1d2b662eb6b67aec5d341afa Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 24 Aug 2022 10:51:12 +0100 Subject: [PATCH 011/275] Fix maya extractor for instance_name --- openpype/hosts/maya/plugins/publish/extract_layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_layout.py b/openpype/hosts/maya/plugins/publish/extract_layout.py index 991217684a..92ca6c883f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_layout.py +++ b/openpype/hosts/maya/plugins/publish/extract_layout.py @@ -56,7 +56,7 @@ class ExtractLayout(openpype.api.Extractor): json_element = { "family": family, - "instance_name": cmds.getAttr(f"{container}.name"), + "instance_name": cmds.getAttr(f"{container}.namespace"), "representation": str(representation_id), "version": str(version_id) } From e3de88e4fe54f0e483d16841730883a6762a7f85 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 24 Aug 2022 10:52:22 +0100 Subject: [PATCH 012/275] Implemented loader for layouts for existing scenes --- .../plugins/load/load_layout_existing.py | 403 ++++++++++++++++++ 1 file changed, 403 insertions(+) create mode 100644 openpype/hosts/unreal/plugins/load/load_layout_existing.py diff --git a/openpype/hosts/unreal/plugins/load/load_layout_existing.py b/openpype/hosts/unreal/plugins/load/load_layout_existing.py new file mode 100644 index 0000000000..297e8d1a4c --- /dev/null +++ b/openpype/hosts/unreal/plugins/load/load_layout_existing.py @@ -0,0 +1,403 @@ +import json +from pathlib import Path + +import unreal +from unreal import EditorLevelLibrary + +from bson.objectid import ObjectId + +from openpype import pipeline +from openpype.pipeline import ( + discover_loader_plugins, + loaders_from_representation, + load_container, + AVALON_CONTAINER_ID, + legacy_io, +) +from openpype.hosts.unreal.api import plugin +from openpype.hosts.unreal.api import pipeline as upipeline + + +class ExistingLayoutLoader(plugin.Loader): + """ + Load Layout for an existing scene, and match the existing assets. + """ + + families = ["layout"] + representations = ["json"] + + label = "Load Layout on Existing Scene" + icon = "code-fork" + color = "orange" + ASSET_ROOT = "/Game/OpenPype" + + @staticmethod + def _create_container( + asset_name, asset_dir, asset, representation, parent, family + ): + container_name = f"{asset_name}_CON" + + container = None + if not unreal.EditorAssetLibrary.does_asset_exist( + f"{asset_dir}/{container_name}" + ): + container = upipeline.create_container(container_name, asset_dir) + else: + ar = unreal.AssetRegistryHelpers.get_asset_registry() + obj = ar.get_asset_by_object_path( + f"{asset_dir}/{container_name}.{container_name}") + container = obj.get_asset() + + data = { + "schema": "openpype:container-2.0", + "id": AVALON_CONTAINER_ID, + "asset": asset, + "namespace": asset_dir, + "container_name": container_name, + "asset_name": asset_name, + # "loader": str(self.__class__.__name__), + "representation": representation, + "parent": parent, + "family": family + } + + upipeline.imprint( + "{}/{}".format(asset_dir, container_name), data) + + return container.get_path_name() + + @staticmethod + def _get_current_level(): + ue_version = unreal.SystemLibrary.get_engine_version().split('.') + ue_major = ue_version[0] + + if ue_major == '4': + return EditorLevelLibrary.get_editor_world() + elif ue_major == '5': + return unreal.LevelEditorSubsystem().get_current_level() + + raise NotImplementedError( + f"Unreal version {ue_major} not supported") + + @staticmethod + def _transform_from_basis(transform, basis, conversion): + """Transform a transform from a basis to a new basis.""" + # Get the basis matrix + basis_matrix = unreal.Matrix( + basis[0], + basis[1], + basis[2], + basis[3] + ) + transform_matrix = unreal.Matrix( + transform[0], + transform[1], + transform[2], + transform[3] + ) + + new_transform = ( + basis_matrix.get_inverse() * transform_matrix * basis_matrix) + + return conversion.inverse() * new_transform.transform() + + def _get_transform(self, ext, import_data, lasset): + conversion = unreal.Matrix.IDENTITY.transform() + + # Check for the conversion settings. We cannot access + # the alembic conversion settings, so we assume that + # the maya ones have been applied. + if ext == '.fbx': + loc = import_data.import_translation + rot = import_data.import_rotation.to_vector() + scale = import_data.import_scale + conversion = unreal.Transform( + location=[loc.x, loc.y, loc.z], + rotation=[rot.x, rot.y, rot.z], + scale=[scale, scale, scale] + ) + elif ext == '.abc': + # This is the standard conversion settings for + # alembic files from Maya. + conversion = unreal.Transform( + location=[0.0, 0.0, 0.0], + rotation=[0.0, 0.0, 0.0], + scale=[1.0, -1.0, 1.0] + ) + + transform = self._transform_from_basis( + lasset.get('transform_matrix'), + lasset.get('basis'), + conversion + ) + return transform + + @staticmethod + def _get_fbx_loader(loaders, family): + name = "" + if family == 'rig': + name = "SkeletalMeshFBXLoader" + elif family == 'model': + name = "StaticMeshFBXLoader" + elif family == 'camera': + name = "CameraLoader" + + if name == "": + return None + + for loader in loaders: + if loader.__name__ == name: + return loader + + return None + + @staticmethod + def _get_abc_loader(loaders, family): + name = "" + if family == 'rig': + name = "SkeletalMeshAlembicLoader" + elif family == 'model': + name = "StaticMeshAlembicLoader" + + if name == "": + return None + + for loader in loaders: + if loader.__name__ == name: + return loader + + return None + + def _load_asset(self, representation, version, instance_name, family): + valid_formats = ['fbx', 'abc'] + + repr_data = legacy_io.find_one({ + "type": "representation", + "parent": ObjectId(version), + "name": {"$in": valid_formats} + }) + repr_format = repr_data.get('name') + + all_loaders = discover_loader_plugins() + loaders = loaders_from_representation( + all_loaders, representation) + + loader = None + + if repr_format == 'fbx': + loader = self._get_fbx_loader(loaders, family) + elif repr_format == 'abc': + loader = self._get_abc_loader(loaders, family) + + if not loader: + raise AssertionError(f"No valid loader found for {representation}") + + assets = load_container( + loader, + representation, + namespace=instance_name + ) + + return assets + + def load(self, context, name, namespace, options): + print("Loading Layout and Match Assets") + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + asset = context.get('asset').get('name') + container_name = f"{asset}_{name}_CON" + + actors = EditorLevelLibrary.get_all_level_actors() + + with open(self.fname, "r") as fp: + data = json.load(fp) + + layout_data = [] + + # Get all the representations in the JSON from the database. + for element in data: + if element.get('representation'): + layout_data.append(( + pipeline.legacy_io.find_one({ + "_id": ObjectId(element.get('representation')) + }), + element + )) + + containers = [] + actors_matched = [] + + for (repr_data, lasset) in layout_data: + if not repr_data: + raise AssertionError("Representation not found") + if not (repr_data.get('data') or repr_data.get('data').get('path')): + raise AssertionError("Representation does not have path") + if not repr_data.get('context'): + raise AssertionError("Representation does not have context") + + # For every actor in the scene, check if it has a representation in + # those we got from the JSON. If so, create a container for it. + # Otherwise, remove it from the scene. + found = False + + for actor in actors: + if not actor.get_class().get_name() == 'StaticMeshActor': + continue + if actor in actors_matched: + continue + + # Get the original path of the file from which the asset has + # been imported. + actor.set_actor_label(lasset.get('instance_name')) + smc = actor.get_editor_property('static_mesh_component') + mesh = smc.get_editor_property('static_mesh') + import_data = mesh.get_editor_property('asset_import_data') + filename = import_data.get_first_filename() + path = Path(filename) + + if not path.name in repr_data.get('data').get('path'): + continue + + asset_name = path.with_suffix('').name + mesh_path = Path(mesh.get_path_name()).parent.as_posix() + + # Create the container for the asset. + asset = repr_data.get('context').get('asset') + subset = repr_data.get('context').get('subset') + container = self._create_container( + f"{asset}_{subset}", mesh_path, asset, + repr_data.get('_id'), repr_data.get('parent'), + repr_data.get('context').get('family') + ) + containers.append(container) + + # Set the transform for the actor. + transform = self._get_transform( + path.suffix, import_data, lasset) + actor.set_actor_transform(transform, False, True) + + actors_matched.append(actor) + found = True + break + + # If an actor has not been found for this representation, + # we check if it has been loaded already by checking all the + # loaded containers. If so, we add it to the scene. Otherwise, + # we load it. + if found: + continue + + all_containers = upipeline.ls() + + loaded = False + + for container in all_containers: + repr = container.get('representation') + + if not repr == str(repr_data.get('_id')): + continue + + asset_dir = container.get('namespace') + + filter = unreal.ARFilter( + class_names=["StaticMesh"], + package_paths=[asset_dir], + recursive_paths=False) + assets = ar.get_assets(filter) + + for asset in assets: + obj = asset.get_asset() + actor = EditorLevelLibrary.spawn_actor_from_object( + obj, unreal.Vector(0.0, 0.0, 0.0) + ) + + actor.set_actor_label(lasset.get('instance_name')) + smc = actor.get_editor_property( + 'static_mesh_component') + mesh = smc.get_editor_property('static_mesh') + import_data = mesh.get_editor_property( + 'asset_import_data') + filename = import_data.get_first_filename() + path = Path(filename) + + transform = self._get_transform( + path.suffix, import_data, lasset) + + actor.set_actor_transform(transform, False, True) + + loaded = True + break + + # If the asset has not been loaded yet, we load it. + if loaded: + continue + + assets = self._load_asset( + lasset.get('representation'), + lasset.get('version'), + lasset.get('instance_name'), + lasset.get('family') + ) + + for asset in assets: + obj = ar.get_asset_by_object_path(asset).get_asset() + if not obj.get_class().get_name() == 'StaticMesh': + continue + actor = EditorLevelLibrary.spawn_actor_from_object( + obj, unreal.Vector(0.0, 0.0, 0.0) + ) + + actor.set_actor_label(lasset.get('instance_name')) + smc = actor.get_editor_property('static_mesh_component') + mesh = smc.get_editor_property('static_mesh') + import_data = mesh.get_editor_property('asset_import_data') + filename = import_data.get_first_filename() + path = Path(filename) + + transform = self._transform_from_basis( + lasset.get('transform_matrix'), + lasset.get('basis'), + unreal.Matrix.IDENTITY.transform() + ) + + actor.set_actor_transform(transform, False, True) + + break + + # Check if an actor was not matched to a representation. + # If so, remove it from the scene. + for actor in actors: + if not actor.get_class().get_name() == 'StaticMeshActor': + continue + if actor not in actors_matched: + EditorLevelLibrary.destroy_actor(actor) + + curr_level = self._get_current_level() + + if not curr_level: + return + + curr_level_path = Path( + curr_level.get_outer().get_path_name()).parent.as_posix() + + if not unreal.EditorAssetLibrary.does_asset_exist( + f"{curr_level_path}/{container_name}" + ): + upipeline.create_container( + container=container_name, path=curr_level_path) + + data = { + "schema": "openpype:container-2.0", + "id": AVALON_CONTAINER_ID, + "asset": asset, + "namespace": curr_level_path, + "container_name": container_name, + "asset_name": asset_name, + "loader": str(self.__class__.__name__), + "representation": context["representation"]["_id"], + "parent": context["representation"]["parent"], + "family": context["representation"]["context"]["family"], + "loaded_assets": containers + } + upipeline.imprint(f"{curr_level_path}/{container_name}", data) From b3cd5e1ea060a533d983d7cfb4b231f240430226 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 24 Aug 2022 11:46:18 +0100 Subject: [PATCH 013/275] Hound fixes --- openpype/hosts/unreal/plugins/load/load_layout_existing.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout_existing.py b/openpype/hosts/unreal/plugins/load/load_layout_existing.py index 297e8d1a4c..c20af950d9 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout_existing.py +++ b/openpype/hosts/unreal/plugins/load/load_layout_existing.py @@ -231,7 +231,8 @@ class ExistingLayoutLoader(plugin.Loader): for (repr_data, lasset) in layout_data: if not repr_data: raise AssertionError("Representation not found") - if not (repr_data.get('data') or repr_data.get('data').get('path')): + if not (repr_data.get('data') or + repr_data.get('data').get('path')): raise AssertionError("Representation does not have path") if not repr_data.get('context'): raise AssertionError("Representation does not have context") @@ -256,7 +257,7 @@ class ExistingLayoutLoader(plugin.Loader): filename = import_data.get_first_filename() path = Path(filename) - if not path.name in repr_data.get('data').get('path'): + if path.name not in repr_data.get('data').get('path'): continue asset_name = path.with_suffix('').name From 2f0f9508d4d36a157d4a4a55ed63c7408ae3c7f8 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 26 Aug 2022 12:05:18 +0100 Subject: [PATCH 014/275] Implemented update --- .../plugins/load/load_alembic_staticmesh.py | 26 +++-- .../plugins/load/load_layout_existing.py | 103 ++++++++++-------- 2 files changed, 75 insertions(+), 54 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py b/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py index 50e498dbb0..a5b9cbd1fc 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_staticmesh.py @@ -20,15 +20,11 @@ class StaticMeshAlembicLoader(plugin.Loader): icon = "cube" color = "orange" - def get_task(self, filename, asset_dir, asset_name, replace): + @staticmethod + def get_task(filename, asset_dir, asset_name, replace, default_conversion): task = unreal.AssetImportTask() options = unreal.AbcImportSettings() sm_settings = unreal.AbcStaticMeshSettings() - conversion_settings = unreal.AbcConversionSettings( - preset=unreal.AbcConversionPreset.CUSTOM, - flip_u=False, flip_v=False, - rotation=[0.0, 0.0, 0.0], - scale=[1.0, 1.0, 1.0]) task.set_editor_property('filename', filename) task.set_editor_property('destination_path', asset_dir) @@ -44,13 +40,20 @@ class StaticMeshAlembicLoader(plugin.Loader): sm_settings.set_editor_property('merge_meshes', True) + if not default_conversion: + conversion_settings = unreal.AbcConversionSettings( + preset=unreal.AbcConversionPreset.CUSTOM, + flip_u=False, flip_v=False, + rotation=[0.0, 0.0, 0.0], + scale=[1.0, 1.0, 1.0]) + options.conversion_settings = conversion_settings + options.static_mesh_settings = sm_settings - options.conversion_settings = conversion_settings task.options = options return task - def load(self, context, name, namespace, data): + def load(self, context, name, namespace, options): """Load and containerise representation into Content Browser. This is two step process. First, import FBX to temporary path and @@ -82,6 +85,10 @@ class StaticMeshAlembicLoader(plugin.Loader): asset_name = "{}".format(name) version = context.get('version').get('name') + default_conversion = False + if options.get("default_conversion"): + default_conversion = options.get("default_conversion") + tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( f"{root}/{asset}/{name}_v{version:03d}", suffix="") @@ -91,7 +98,8 @@ class StaticMeshAlembicLoader(plugin.Loader): if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): unreal.EditorAssetLibrary.make_directory(asset_dir) - task = self.get_task(self.fname, asset_dir, asset_name, False) + task = self.get_task( + self.fname, asset_dir, asset_name, False, default_conversion) unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 diff --git a/openpype/hosts/unreal/plugins/load/load_layout_existing.py b/openpype/hosts/unreal/plugins/load/load_layout_existing.py index c20af950d9..8cd1950f7e 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout_existing.py +++ b/openpype/hosts/unreal/plugins/load/load_layout_existing.py @@ -11,6 +11,7 @@ from openpype.pipeline import ( discover_loader_plugins, loaders_from_representation, load_container, + get_representation_path, AVALON_CONTAINER_ID, legacy_io, ) @@ -132,6 +133,23 @@ class ExistingLayoutLoader(plugin.Loader): ) return transform + def _spawn_actor(self, obj, lasset): + actor = EditorLevelLibrary.spawn_actor_from_object( + obj, unreal.Vector(0.0, 0.0, 0.0) + ) + + actor.set_actor_label(lasset.get('instance_name')) + smc = actor.get_editor_property('static_mesh_component') + mesh = smc.get_editor_property('static_mesh') + import_data = mesh.get_editor_property('asset_import_data') + filename = import_data.get_first_filename() + path = Path(filename) + + transform = self._get_transform( + path.suffix, import_data, lasset) + + actor.set_actor_transform(transform, False, True) + @staticmethod def _get_fbx_loader(loaders, family): name = "" @@ -192,25 +210,29 @@ class ExistingLayoutLoader(plugin.Loader): if not loader: raise AssertionError(f"No valid loader found for {representation}") + # This option is necessary to avoid importing the assets with a + # different conversion compared to the other assets. For ABC files, + # it is in fact impossible to access the conversion settings. So, + # we must assume that the Maya conversion settings have been applied. + options = { + "default_conversion": True + } + assets = load_container( loader, representation, - namespace=instance_name + namespace=instance_name, + options=options ) return assets - def load(self, context, name, namespace, options): - print("Loading Layout and Match Assets") - + def _process(self, lib_path): ar = unreal.AssetRegistryHelpers.get_asset_registry() - asset = context.get('asset').get('name') - container_name = f"{asset}_{name}_CON" - actors = EditorLevelLibrary.get_all_level_actors() - with open(self.fname, "r") as fp: + with open(lib_path, "r") as fp: data = json.load(fp) layout_data = [] @@ -260,7 +282,6 @@ class ExistingLayoutLoader(plugin.Loader): if path.name not in repr_data.get('data').get('path'): continue - asset_name = path.with_suffix('').name mesh_path = Path(mesh.get_path_name()).parent.as_posix() # Create the container for the asset. @@ -309,23 +330,7 @@ class ExistingLayoutLoader(plugin.Loader): for asset in assets: obj = asset.get_asset() - actor = EditorLevelLibrary.spawn_actor_from_object( - obj, unreal.Vector(0.0, 0.0, 0.0) - ) - - actor.set_actor_label(lasset.get('instance_name')) - smc = actor.get_editor_property( - 'static_mesh_component') - mesh = smc.get_editor_property('static_mesh') - import_data = mesh.get_editor_property( - 'asset_import_data') - filename = import_data.get_first_filename() - path = Path(filename) - - transform = self._get_transform( - path.suffix, import_data, lasset) - - actor.set_actor_transform(transform, False, True) + self._spawn_actor(obj, lasset) loaded = True break @@ -345,24 +350,7 @@ class ExistingLayoutLoader(plugin.Loader): obj = ar.get_asset_by_object_path(asset).get_asset() if not obj.get_class().get_name() == 'StaticMesh': continue - actor = EditorLevelLibrary.spawn_actor_from_object( - obj, unreal.Vector(0.0, 0.0, 0.0) - ) - - actor.set_actor_label(lasset.get('instance_name')) - smc = actor.get_editor_property('static_mesh_component') - mesh = smc.get_editor_property('static_mesh') - import_data = mesh.get_editor_property('asset_import_data') - filename = import_data.get_first_filename() - path = Path(filename) - - transform = self._transform_from_basis( - lasset.get('transform_matrix'), - lasset.get('basis'), - unreal.Matrix.IDENTITY.transform() - ) - - actor.set_actor_transform(transform, False, True) + self._spawn_actor(obj, lasset) break @@ -374,10 +362,21 @@ class ExistingLayoutLoader(plugin.Loader): if actor not in actors_matched: EditorLevelLibrary.destroy_actor(actor) + return containers + + def load(self, context, name, namespace, options): + print("Loading Layout and Match Assets") + + asset = context.get('asset').get('name') + asset_name = f"{asset}_{name}" if asset else name + container_name = f"{asset}_{name}_CON" + curr_level = self._get_current_level() if not curr_level: - return + raise AssertionError("Current level not saved") + + containers = self._process(self.fname) curr_level_path = Path( curr_level.get_outer().get_path_name()).parent.as_posix() @@ -402,3 +401,17 @@ class ExistingLayoutLoader(plugin.Loader): "loaded_assets": containers } upipeline.imprint(f"{curr_level_path}/{container_name}", data) + + def update(self, container, representation): + asset_dir = container.get('namespace') + + source_path = get_representation_path(representation) + containers = self._process(source_path) + + data = { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]), + "loaded_assets": containers + } + upipeline.imprint( + "{}/{}".format(asset_dir, container.get('container_name')), data) From 41e673c3ee45c85a7c71e5b0a0bb465ae95a9b83 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 9 Sep 2022 00:20:47 +0200 Subject: [PATCH 015/275] Cleanup comment --- openpype/hosts/maya/api/lib.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 58e160cb2f..5e449b324e 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2633,8 +2633,6 @@ def load_capture_preset(data=None): scene = capture.parse_active_scene() options['sound'] = scene['sound'] - # options['display_options'] = temp_options - return options From ed11baf0fdaa8c35764bdee8b04215e58aca1e23 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 9 Sep 2022 00:29:16 +0200 Subject: [PATCH 016/275] Include camera options --- openpype/hosts/maya/api/lib.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 5e449b324e..c8369bac13 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2629,6 +2629,13 @@ def load_capture_preset(data=None): options['viewport_options'] = temp_options options['viewport2_options'] = temp_options2 + # CAMERA OPTIONS + id = 'Camera Options' + camera_options = {} + for key, value in preset[id].items(): + camera_options[key] = value + options['camera_options'] = camera_options + # use active sound track scene = capture.parse_active_scene() options['sound'] = scene['sound'] From 5558c1eb46c8e2045c26c19ae4afcf8a47520eba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Albert?= Date: Fri, 9 Sep 2022 12:07:56 +0200 Subject: [PATCH 017/275] Kitsu : add launcher action --- .../kitsu/actions/launcher_show_in_kitsu.py | 131 ++++++++++++++++++ openpype/modules/kitsu/kitsu_module.py | 5 +- 2 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 openpype/modules/kitsu/actions/launcher_show_in_kitsu.py diff --git a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py new file mode 100644 index 0000000000..0ac9c6e9b7 --- /dev/null +++ b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py @@ -0,0 +1,131 @@ +import webbrowser + +from openpype.pipeline import LauncherAction +from openpype.modules import ModulesManager +from openpype.client import get_project, get_asset_by_name + + +class ShowInKitsu(LauncherAction): + name = "showinkitsu" + label = "Show in Kitsu" + icon = "external-link-square" + color = "#e0e1e1" + order = 10 + + @staticmethod + def get_kitsu_module(): + return ModulesManager().modules_by_name.get("kitsu") + + def is_compatible(self, session): + if not session.get("AVALON_PROJECT"): + return False + + return True + + def process(self, session, **kwargs): + + # Context inputs + project_name = session["AVALON_PROJECT"] + asset_name = session.get("AVALON_ASSET", None) + task_name = session.get("AVALON_TASK", None) + + project = get_project(project_name=project_name, + fields=["data.zou_id"]) + if not project: + raise RuntimeError(f"Project {project_name} not found.") + + project_zou_id = project["data"].get("zou_id") + if not project_zou_id: + raise RuntimeError(f"Project {project_name} has no " + f"connected ftrack id.") + + asset_zou_data = None + task_zou_id = None + asset_zou_name = None + asset_zou_id = None + asset_zou_type = 'Assets' + zou_sub_type = ['AssetType','Sequence'] + if asset_name: + asset_zou_name = asset_name + asset_fields = ["data.zou.id", "data.zou.type"] + if task_name: + asset_fields.append(f"data.tasks.{task_name}.zou.id") + + asset = get_asset_by_name(project_name, + asset_name=asset_name, + fields=asset_fields) + + asset_zou_data = asset["data"].get("zou") + + if asset_zou_data: + asset_zou_type = asset_zou_data["type"] + if not asset_zou_type in zou_sub_type: + asset_zou_id = asset_zou_data["id"] + else: + asset_zou_type = asset_name + + + if task_name: + task_data = asset["data"]["tasks"][task_name] + task_zou_data = task_data.get("zou", {}) + if not task_zou_data: + self.log.debug(f"No zou task data for task: {task_name}") + task_zou_id = task_zou_data["id"] + + # Define URL + url = self.get_url(project_id=project_zou_id, + asset_name=asset_zou_name, + asset_id=asset_zou_id, + asset_type=asset_zou_type, + task_id=task_zou_id) + + # Open URL in webbrowser + self.log.info(f"Opening URL: {url}") + webbrowser.open(url, + # Try in new tab + new=2) + + def get_url(self, + project_id, + asset_name=None, + asset_id=None, + asset_type=None, + task_id=None): + + shots_url = ['Shots','Sequence','Shot'] + sub_type = ['AssetType','Sequence'] + kitsu_module = self.get_kitsu_module() + + # Get kitsu url with /api stripped + kitsu_url = kitsu_module.server_url + if kitsu_url.endswith("/api"): + kitsu_url = kitsu_url[:-len("/api")] + + sub_url = f"/productions/{project_id}" + asset_type_url = "Assets" + + # Add redirection url for shots_url list + if asset_type in shots_url: + asset_type_url = 'Shots' + + if task_id: + # Go to task page + # /productions/{project-id}/{asset_type}/tasks/{task_id} + sub_url += f"/{asset_type_url}/tasks/{task_id}" + + elif asset_id: + # Go to asset or shot page + # /productions/{project-id}/assets/{entity_id} + # /productions/{project-id}/shots/{entity_id} + sub_url += f"/{asset_type_url}/{asset_id}" + + else: + # Go to project page + # Project page must end with a view + # /productions/{project-id}/assets/ + # Add search method if is a sub_type + sub_url += f"/{asset_type_url}" + if asset_type in sub_type: + sub_url += f'?search={asset_name}' + + return f"{kitsu_url}{sub_url}" diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index d19d14dda7..23c032715b 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -89,7 +89,10 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): """Implementation of abstract method for `IPluginPaths`.""" current_dir = os.path.dirname(os.path.abspath(__file__)) - return {"publish": [os.path.join(current_dir, "plugins", "publish")]} + return { + "publish": [os.path.join(current_dir, "plugins", "publish")], + "actions": [os.path.join(current_dir, "actions")] + } def cli(self, click_group): click_group.add_command(cli_main) From d0d80b0b90648b1633a11c5980c0c66e3a3cff7f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 13 Sep 2022 14:57:05 +0200 Subject: [PATCH 018/275] Fix typo `camera_option` -> `camera_options` - Also use `setdefault` to ensure its added into the preset when key wasn't there originally --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 871adda0c3..6010319f40 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -79,8 +79,10 @@ class ExtractPlayblast(openpype.api.Extractor): preset['height'] = asset_height preset['start_frame'] = start preset['end_frame'] = end - camera_option = preset.get("camera_option", {}) - camera_option["depthOfField"] = cmds.getAttr( + + # Enforce persisting camera depth of field + camera_options = preset.setdefault("camera_options", {}) + camera_options["depthOfField"] = cmds.getAttr( "{0}.depthOfField".format(camera)) stagingdir = self.staging_dir(instance) From 4b7ecac2bbc5fc0f33839c3b0efa0cf2ff304e4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Albert?= Date: Wed, 14 Sep 2022 12:31:13 +0200 Subject: [PATCH 019/275] Fix Format Document --- .../modules/kitsu/actions/launcher_show_in_kitsu.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py index 0ac9c6e9b7..bca57ce4c6 100644 --- a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py +++ b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py @@ -44,7 +44,7 @@ class ShowInKitsu(LauncherAction): asset_zou_name = None asset_zou_id = None asset_zou_type = 'Assets' - zou_sub_type = ['AssetType','Sequence'] + zou_sub_type = ['AssetType', 'Sequence'] if asset_name: asset_zou_name = asset_name asset_fields = ["data.zou.id", "data.zou.type"] @@ -59,11 +59,10 @@ class ShowInKitsu(LauncherAction): if asset_zou_data: asset_zou_type = asset_zou_data["type"] - if not asset_zou_type in zou_sub_type: + if asset_zou_type not in zou_sub_type: asset_zou_id = asset_zou_data["id"] else: asset_zou_type = asset_name - if task_name: task_data = asset["data"]["tasks"][task_name] @@ -71,7 +70,7 @@ class ShowInKitsu(LauncherAction): if not task_zou_data: self.log.debug(f"No zou task data for task: {task_name}") task_zou_id = task_zou_data["id"] - + # Define URL url = self.get_url(project_id=project_zou_id, asset_name=asset_zou_name, @@ -92,8 +91,8 @@ class ShowInKitsu(LauncherAction): asset_type=None, task_id=None): - shots_url = ['Shots','Sequence','Shot'] - sub_type = ['AssetType','Sequence'] + shots_url = ['Shots', 'Sequence', 'Shot'] + sub_type = ['AssetType', 'Sequence'] kitsu_module = self.get_kitsu_module() # Get kitsu url with /api stripped @@ -103,7 +102,7 @@ class ShowInKitsu(LauncherAction): sub_url = f"/productions/{project_id}" asset_type_url = "Assets" - + # Add redirection url for shots_url list if asset_type in shots_url: asset_type_url = 'Shots' From 2fb40a9db7c3fde1c1842958c597c07693bb17e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Albert?= <89812691+sebasti1a@users.noreply.github.com> Date: Wed, 14 Sep 2022 14:49:44 +0200 Subject: [PATCH 020/275] Update openpype/modules/kitsu/actions/launcher_show_in_kitsu.py Co-authored-by: Roy Nieterau --- openpype/modules/kitsu/actions/launcher_show_in_kitsu.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py index bca57ce4c6..0ee95e773d 100644 --- a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py +++ b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py @@ -101,11 +101,7 @@ class ShowInKitsu(LauncherAction): kitsu_url = kitsu_url[:-len("/api")] sub_url = f"/productions/{project_id}" - asset_type_url = "Assets" - - # Add redirection url for shots_url list - if asset_type in shots_url: - asset_type_url = 'Shots' + asset_type_url = "Shots" if asset_type in shots_url else "Assets" if task_id: # Go to task page From 9b34573361ef292fcb68e692efc74268ac165ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Albert?= <89812691+sebasti1a@users.noreply.github.com> Date: Wed, 14 Sep 2022 14:50:29 +0200 Subject: [PATCH 021/275] Update openpype/modules/kitsu/actions/launcher_show_in_kitsu.py Co-authored-by: Roy Nieterau --- openpype/modules/kitsu/actions/launcher_show_in_kitsu.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py index 0ee95e773d..68da3e3a0e 100644 --- a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py +++ b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py @@ -39,7 +39,6 @@ class ShowInKitsu(LauncherAction): raise RuntimeError(f"Project {project_name} has no " f"connected ftrack id.") - asset_zou_data = None task_zou_id = None asset_zou_name = None asset_zou_id = None From 40e3dbb38fe91cdc07814707b56f2550138c7eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Albert?= <89812691+sebasti1a@users.noreply.github.com> Date: Wed, 14 Sep 2022 14:50:42 +0200 Subject: [PATCH 022/275] Update openpype/modules/kitsu/actions/launcher_show_in_kitsu.py Co-authored-by: Roy Nieterau --- openpype/modules/kitsu/actions/launcher_show_in_kitsu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py index 68da3e3a0e..575d1eecd0 100644 --- a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py +++ b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py @@ -39,10 +39,10 @@ class ShowInKitsu(LauncherAction): raise RuntimeError(f"Project {project_name} has no " f"connected ftrack id.") - task_zou_id = None asset_zou_name = None asset_zou_id = None asset_zou_type = 'Assets' + task_zou_id = None zou_sub_type = ['AssetType', 'Sequence'] if asset_name: asset_zou_name = asset_name From ab03df702a0f14c8d0dd94ed3c917c78fde9cb4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Albert?= <89812691+sebasti1a@users.noreply.github.com> Date: Wed, 14 Sep 2022 14:50:50 +0200 Subject: [PATCH 023/275] Update openpype/modules/kitsu/actions/launcher_show_in_kitsu.py Co-authored-by: Roy Nieterau --- openpype/modules/kitsu/actions/launcher_show_in_kitsu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py index 575d1eecd0..ab523876ed 100644 --- a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py +++ b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py @@ -90,8 +90,8 @@ class ShowInKitsu(LauncherAction): asset_type=None, task_id=None): - shots_url = ['Shots', 'Sequence', 'Shot'] - sub_type = ['AssetType', 'Sequence'] + shots_url = {'Shots', 'Sequence', 'Shot'} + sub_type = {'AssetType', 'Sequence'} kitsu_module = self.get_kitsu_module() # Get kitsu url with /api stripped From 3fff6647389d28b47fe4048a5a048d3fb420da91 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 15 Sep 2022 16:13:01 +0200 Subject: [PATCH 024/275] Refactor `load_capture_preset` --- openpype/hosts/maya/api/lib.py | 204 +++++++++++---------------------- 1 file changed, 64 insertions(+), 140 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 97035ad3f2..3561c3f53f 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2462,28 +2462,16 @@ def load_capture_preset(data=None): import capture preset = data - options = dict() - # CODEC - id = 'Codec' - for key in preset[id]: - options[str(key)] = preset[id][key] - - # GENERIC - id = 'Generic' - for key in preset[id]: - options[str(key)] = preset[id][key] - - # RESOLUTION - id = 'Resolution' - options['height'] = preset[id]['height'] - options['width'] = preset[id]['width'] + # Straight key-value match from settings to capture arguments + for settings_key in ["Codec", "Generic", "Resolution"]: + for key, value in preset[settings_key].items(): + options[key] = value # DISPLAY OPTIONS - id = 'Display Options' disp_options = {} - for key in preset[id]: + for key in preset['Display Options']: if key.startswith('background'): disp_options[key] = preset['Display Options'][key] if len(disp_options[key]) == 4: @@ -2497,142 +2485,78 @@ def load_capture_preset(data=None): options['display_options'] = disp_options # VIEWPORT OPTIONS - temp_options = {} - id = 'Renderer' - for key in preset[id]: - temp_options[str(key)] = preset[id][key] + viewport_options = {} + viewport2_options = {} - temp_options2 = {} - id = 'Viewport Options' - for key in preset[id]: + for key, value in preset['Renderer'].items(): + viewport_options[key] = value + + # Viewport Options has a mixture of Viewport2 Options and Viewport Options + # to pass along to capture. So we'll need to differentiate between the two + VIEWPORT2_OPTIONS = { + "textureMaxResolution", + "renderDepthOfField", + "ssaoEnable", + "ssaoSamples", + "ssaoAmount", + "ssaoRadius", + "ssaoFilterRadius", + "hwFogStart", + "hwFogEnd", + "hwFogAlpha", + "hwFogFalloff", + "hwFogColorR", + "hwFogColorG", + "hwFogColorB", + "hwFogDensity", + "motionBlurEnable", + "motionBlurSampleCount", + "motionBlurShutterOpenFraction", + "lineAAEnable" + } + for key, value in preset['Viewport Options'].items(): + + # There are some keys we want to ignore + if key in {"override_viewport_options", "high_quality"}: + continue + + # First handle special cases where we do value conversion to + # separate option values if key == 'textureMaxResolution': - if preset[id][key] > 0: - temp_options2['textureMaxResolution'] = preset[id][key] - temp_options2['enableTextureMaxRes'] = True - temp_options2['textureMaxResMode'] = 1 + viewport2_options['textureMaxResolution'] = value + if value > 0: + viewport2_options['enableTextureMaxRes'] = True + viewport2_options['textureMaxResMode'] = 1 else: - temp_options2['textureMaxResolution'] = preset[id][key] - temp_options2['enableTextureMaxRes'] = False - temp_options2['textureMaxResMode'] = 0 + viewport2_options['enableTextureMaxRes'] = False + viewport2_options['textureMaxResMode'] = 0 - if key == 'multiSample': - if preset[id][key] > 0: - temp_options2['multiSampleEnable'] = True - temp_options2['multiSampleCount'] = preset[id][key] - else: - temp_options2['multiSampleEnable'] = False - temp_options2['multiSampleCount'] = preset[id][key] + elif key == 'multiSample': + viewport2_options['multiSampleEnable'] = value > 0 + viewport2_options['multiSampleCount'] = value - if key == 'renderDepthOfField': - temp_options2['renderDepthOfField'] = preset[id][key] + elif key == 'alphaCut': + viewport2_options['transparencyAlgorithm'] = 5 + viewport2_options['transparencyQuality'] = 1 - if key == 'ssaoEnable': - if preset[id][key] is True: - temp_options2['ssaoEnable'] = True - else: - temp_options2['ssaoEnable'] = False + elif key == 'hwFogFalloff': + # Settings enum value string to integer + viewport2_options['hwFogFalloff'] = int(value) - if key == 'ssaoSamples': - temp_options2['ssaoSamples'] = preset[id][key] - - if key == 'ssaoAmount': - temp_options2['ssaoAmount'] = preset[id][key] - - if key == 'ssaoRadius': - temp_options2['ssaoRadius'] = preset[id][key] - - if key == 'hwFogDensity': - temp_options2['hwFogDensity'] = preset[id][key] - - if key == 'ssaoFilterRadius': - temp_options2['ssaoFilterRadius'] = preset[id][key] - - if key == 'alphaCut': - temp_options2['transparencyAlgorithm'] = 5 - temp_options2['transparencyQuality'] = 1 - - if key == 'headsUpDisplay': - temp_options['headsUpDisplay'] = True - - if key == 'fogging': - temp_options['fogging'] = preset[id][key] or False - - if key == 'hwFogStart': - temp_options2['hwFogStart'] = preset[id][key] - - if key == 'hwFogEnd': - temp_options2['hwFogEnd'] = preset[id][key] - - if key == 'hwFogAlpha': - temp_options2['hwFogAlpha'] = preset[id][key] - - if key == 'hwFogFalloff': - temp_options2['hwFogFalloff'] = int(preset[id][key]) - - if key == 'hwFogColorR': - temp_options2['hwFogColorR'] = preset[id][key] - - if key == 'hwFogColorG': - temp_options2['hwFogColorG'] = preset[id][key] - - if key == 'hwFogColorB': - temp_options2['hwFogColorB'] = preset[id][key] - - if key == 'motionBlurEnable': - if preset[id][key] is True: - temp_options2['motionBlurEnable'] = True - else: - temp_options2['motionBlurEnable'] = False - - if key == 'motionBlurSampleCount': - temp_options2['motionBlurSampleCount'] = preset[id][key] - - if key == 'motionBlurShutterOpenFraction': - temp_options2['motionBlurShutterOpenFraction'] = preset[id][key] - - if key == 'lineAAEnable': - if preset[id][key] is True: - temp_options2['lineAAEnable'] = True - else: - temp_options2['lineAAEnable'] = False + # Then handle Viewport 2.0 Options + elif key in VIEWPORT2_OPTIONS: + viewport2_options[key] = value + # Then assume remainder is Viewport Options else: - temp_options[str(key)] = preset[id][key] + viewport_options[key] = value - for key in ['override_viewport_options', - 'high_quality', - 'alphaCut', - 'gpuCacheDisplayFilter', - 'multiSample', - 'ssaoEnable', - 'ssaoSamples', - 'ssaoAmount', - 'ssaoFilterRadius', - 'ssaoRadius', - 'hwFogStart', - 'hwFogEnd', - 'hwFogAlpha', - 'hwFogFalloff', - 'hwFogColorR', - 'hwFogColorG', - 'hwFogColorB', - 'hwFogDensity', - 'textureMaxResolution', - 'motionBlurEnable', - 'motionBlurSampleCount', - 'motionBlurShutterOpenFraction', - 'lineAAEnable', - 'renderDepthOfField' - ]: - temp_options.pop(key, None) - - options['viewport_options'] = temp_options - options['viewport2_options'] = temp_options2 + options['viewport_options'] = viewport_options + options['viewport2_options'] = viewport2_options # CAMERA OPTIONS - id = 'Camera Options' camera_options = {} - for key, value in preset[id].items(): + for key, value in preset['Camera Options'].items(): camera_options[key] = value options['camera_options'] = camera_options From 2560fc0081292b7862367101e895a7d3526045da Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 15 Sep 2022 16:29:16 +0200 Subject: [PATCH 025/275] Simplify logic where we're taking values directly --- openpype/hosts/maya/api/lib.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 3561c3f53f..4d6f599d2b 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2460,14 +2460,20 @@ def bake_to_world_space(nodes, def load_capture_preset(data=None): import capture - preset = data + options = dict() + viewport_options = dict() + viewport2_options = dict() + camera_options = dict() # Straight key-value match from settings to capture arguments - for settings_key in ["Codec", "Generic", "Resolution"]: - for key, value in preset[settings_key].items(): - options[key] = value + options.update(preset["Codec"]) + options.update(preset["Generic"]) + options.update(preset["Resolution"]) + + camera_options.update(preset['Camera Options']) + viewport_options.update(preset["Renderer"]) # DISPLAY OPTIONS disp_options = {} @@ -2484,13 +2490,6 @@ def load_capture_preset(data=None): options['display_options'] = disp_options - # VIEWPORT OPTIONS - viewport_options = {} - viewport2_options = {} - - for key, value in preset['Renderer'].items(): - viewport_options[key] = value - # Viewport Options has a mixture of Viewport2 Options and Viewport Options # to pass along to capture. So we'll need to differentiate between the two VIEWPORT2_OPTIONS = { @@ -2553,11 +2552,6 @@ def load_capture_preset(data=None): options['viewport_options'] = viewport_options options['viewport2_options'] = viewport2_options - - # CAMERA OPTIONS - camera_options = {} - for key, value in preset['Camera Options'].items(): - camera_options[key] = value options['camera_options'] = camera_options # use active sound track From f99b31d13a7bafbfe3763978db9a10a1024f8d85 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 15 Sep 2022 16:30:32 +0200 Subject: [PATCH 026/275] Don't remap input argument `preset` isn't necessarily more explicit than `data` and actually adds to confusion because it makes it feel like it's an actual capture preset but instead it converts OpenPype preset data into capture preset --- openpype/hosts/maya/api/lib.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 4d6f599d2b..7220f53e66 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2460,7 +2460,6 @@ def bake_to_world_space(nodes, def load_capture_preset(data=None): import capture - preset = data options = dict() viewport_options = dict() @@ -2468,18 +2467,18 @@ def load_capture_preset(data=None): camera_options = dict() # Straight key-value match from settings to capture arguments - options.update(preset["Codec"]) - options.update(preset["Generic"]) - options.update(preset["Resolution"]) + options.update(data["Codec"]) + options.update(data["Generic"]) + options.update(data["Resolution"]) - camera_options.update(preset['Camera Options']) - viewport_options.update(preset["Renderer"]) + camera_options.update(data['Camera Options']) + viewport_options.update(data["Renderer"]) # DISPLAY OPTIONS disp_options = {} - for key in preset['Display Options']: + for key in data['Display Options']: if key.startswith('background'): - disp_options[key] = preset['Display Options'][key] + disp_options[key] = data['Display Options'][key] if len(disp_options[key]) == 4: disp_options[key][0] = (float(disp_options[key][0])/255) disp_options[key][1] = (float(disp_options[key][1])/255) @@ -2513,7 +2512,7 @@ def load_capture_preset(data=None): "motionBlurShutterOpenFraction", "lineAAEnable" } - for key, value in preset['Viewport Options'].items(): + for key, value in data['Viewport Options'].items(): # There are some keys we want to ignore if key in {"override_viewport_options", "high_quality"}: From 1132d8d4f8ffa4b224308b5099424e9984d0822c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 15 Sep 2022 16:34:11 +0200 Subject: [PATCH 027/275] Add docstring --- openpype/hosts/maya/api/lib.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 7220f53e66..dec698062a 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2459,6 +2459,19 @@ def bake_to_world_space(nodes, def load_capture_preset(data=None): + """Convert OpenPype Extract Playblast settings to `capture` arguments + + Input data is the settings from: + `project_settings/maya/publish/ExtractPlayblast/capture_preset` + + Args: + data (dict): Capture preset settings from OpenPype settings + + Returns: + dict: `capture.capture` compatible keyword arguments + + """ + import capture options = dict() From 598ec6e2dcb5664407945d081ae15adad6b4d8c6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 15 Sep 2022 16:57:42 +0200 Subject: [PATCH 028/275] Improve readability of color conversion logic --- openpype/hosts/maya/api/lib.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index dec698062a..5b436a018d 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2489,14 +2489,17 @@ def load_capture_preset(data=None): # DISPLAY OPTIONS disp_options = {} - for key in data['Display Options']: + for key, value in data['Display Options'].items(): if key.startswith('background'): - disp_options[key] = data['Display Options'][key] - if len(disp_options[key]) == 4: - disp_options[key][0] = (float(disp_options[key][0])/255) - disp_options[key][1] = (float(disp_options[key][1])/255) - disp_options[key][2] = (float(disp_options[key][2])/255) - disp_options[key].pop() + # Convert background, backgroundTop, backgroundBottom colors + if len(value) == 4: + # Ignore alpha + convert RGB to float + value = [ + float(value[0]) / 255, + float(value[1]) / 255, + float(value[2]) / 255 + ] + disp_options[key] = value else: disp_options['displayGradient'] = True From ccab10b0d3fecc4e32711e5f1a783de433b54ce8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 16 Sep 2022 14:43:13 +0200 Subject: [PATCH 029/275] Do not enforce maya/ folder --- .../publish/validate_rendersettings.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py index 08ecc0d149..4a67cb73e4 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py +++ b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py @@ -16,10 +16,10 @@ from openpype.hosts.maya.api import lib class ValidateRenderSettings(pyblish.api.InstancePlugin): """Validates the global render settings - * File Name Prefix must start with: `maya/` + * File Name Prefix must start with: `` all other token are customizable but sane values for Arnold are: - `maya///_` + `//_` token is supported also, useful for multiple renderable cameras per render layer. @@ -58,12 +58,12 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): } ImagePrefixTokens = { - 'mentalray': 'maya///{aov_separator}', # noqa: E501 - 'arnold': 'maya///{aov_separator}', # noqa: E501 - 'redshift': 'maya///', - 'vray': 'maya///', + 'mentalray': '//{aov_separator}', # noqa: E501 + 'arnold': '//{aov_separator}', # noqa: E501 + 'redshift': '//', + 'vray': '//', 'renderman': '{aov_separator}..', - 'mayahardware2': 'maya///', + 'mayahardware2': '//', } _aov_chars = { @@ -74,7 +74,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): redshift_AOV_prefix = "/{aov_separator}" # noqa: E501 - renderman_dir_prefix = "maya//" + renderman_dir_prefix = "/" R_AOV_TOKEN = re.compile( r'%a||', re.IGNORECASE) @@ -84,8 +84,8 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): R_SCENE_TOKEN = re.compile(r'%s|', re.IGNORECASE) DEFAULT_PADDING = 4 - VRAY_PREFIX = "maya///" - DEFAULT_PREFIX = "maya///_" + VRAY_PREFIX = "//" + DEFAULT_PREFIX = "//_" def process(self, instance): @@ -116,7 +116,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): prefix = prefix.replace( "{aov_separator}", instance.data.get("aovSeparator", "_")) - required_prefix = "maya/" + required_prefix = "" default_prefix = cls.ImagePrefixTokens[renderer] if not anim_override: From 59cfca7508f493bb7afec85ded74bd8abd44e2e0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 16 Sep 2022 14:44:19 +0200 Subject: [PATCH 030/275] Do not enforce maya/ folder --- openpype/hosts/maya/api/lib_rendersettings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 7cd2193086..21dd7f00c8 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -28,7 +28,7 @@ class RenderSettings(object): _image_prefixes = { 'vray': get_current_project_settings()["maya"]["RenderSettings"]["vray_renderer"]["image_prefix"], # noqa 'arnold': get_current_project_settings()["maya"]["RenderSettings"]["arnold_renderer"]["image_prefix"], # noqa - 'renderman': 'maya///{aov_separator}', + 'renderman': '//{aov_separator}', 'redshift': get_current_project_settings()["maya"]["RenderSettings"]["redshift_renderer"]["image_prefix"] # noqa } From 47d7b3044bff4827356ddde4f4c257ac87a9287c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 16 Sep 2022 14:46:45 +0200 Subject: [PATCH 031/275] Remove maya/ from file prefixes in setting defaults --- openpype/settings/defaults/project_settings/maya.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 8643297f02..716e45a6e2 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -34,12 +34,12 @@ }, "RenderSettings": { "apply_render_settings": true, - "default_render_image_folder": "renders", + "default_render_image_folder": "renders/maya", "enable_all_lights": false, "aov_separator": "underscore", "reset_current_frame": false, "arnold_renderer": { - "image_prefix": "maya///_", + "image_prefix": "//_", "image_format": "exr", "multilayer_exr": true, "tiled": true, @@ -47,14 +47,14 @@ "additional_options": [] }, "vray_renderer": { - "image_prefix": "maya///", + "image_prefix": "//", "engine": "1", "image_format": "png", "aov_list": [], "additional_options": [] }, "redshift_renderer": { - "image_prefix": "maya///", + "image_prefix": "//", "primary_gi_engine": "0", "secondary_gi_engine": "0", "image_format": "iff", From ea5ae50982cc867f24c253e8c5dcb1df1612d30f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 16 Sep 2022 14:47:28 +0200 Subject: [PATCH 032/275] Add maya/ folder by default into "images" file rule in workspace --- openpype/settings/defaults/project_settings/maya.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 716e45a6e2..a62d356162 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -1,5 +1,5 @@ { - "mel_workspace": "workspace -fr \"shaders\" \"renderData/shaders\";\nworkspace -fr \"images\" \"renders\";\nworkspace -fr \"particles\" \"particles\";\nworkspace -fr \"mayaAscii\" \"\";\nworkspace -fr \"mayaBinary\" \"\";\nworkspace -fr \"scene\" \"\";\nworkspace -fr \"alembicCache\" \"cache/alembic\";\nworkspace -fr \"renderData\" \"renderData\";\nworkspace -fr \"sourceImages\" \"sourceimages\";\nworkspace -fr \"fileCache\" \"cache/nCache\";\n", + "mel_workspace": "workspace -fr \"shaders\" \"renderData/shaders\";\nworkspace -fr \"images\" \"renders/maya\";\nworkspace -fr \"particles\" \"particles\";\nworkspace -fr \"mayaAscii\" \"\";\nworkspace -fr \"mayaBinary\" \"\";\nworkspace -fr \"scene\" \"\";\nworkspace -fr \"alembicCache\" \"cache/alembic\";\nworkspace -fr \"renderData\" \"renderData\";\nworkspace -fr \"sourceImages\" \"sourceimages\";\nworkspace -fr \"fileCache\" \"cache/nCache\";\n", "ext_mapping": { "model": "ma", "mayaAscii": "ma", From b4b62ce7ba793dcd812ea6cc40adcdb79b687aac Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 16 Sep 2022 14:48:56 +0200 Subject: [PATCH 033/275] Fix remainder of hardcoded maya/ folders in file prefixes --- openpype/hosts/maya/api/lib_renderproducts.py | 2 +- openpype/hosts/maya/plugins/publish/submit_maya_muster.py | 2 +- .../modules/deadline/plugins/publish/submit_maya_deadline.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index 1e883ea43f..1ab771cfe6 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -80,7 +80,7 @@ IMAGE_PREFIXES = { "mayahardware2": "defaultRenderGlobals.imageFilePrefix" } -RENDERMAN_IMAGE_DIR = "maya//" +RENDERMAN_IMAGE_DIR = "/" def has_tokens(string, tokens): diff --git a/openpype/hosts/maya/plugins/publish/submit_maya_muster.py b/openpype/hosts/maya/plugins/publish/submit_maya_muster.py index c4250a20bd..01008b7756 100644 --- a/openpype/hosts/maya/plugins/publish/submit_maya_muster.py +++ b/openpype/hosts/maya/plugins/publish/submit_maya_muster.py @@ -118,7 +118,7 @@ def preview_fname(folder, scene, layer, padding, ext): """ # Following hardcoded "/_/" - output = "maya/{scene}/{layer}/{layer}.{number}.{ext}".format( + output = "{scene}/{layer}/{layer}.{number}.{ext}".format( scene=scene, layer=layer, number="#" * padding, diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 44f2b5b2b4..3e3e5c5b16 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -729,10 +729,10 @@ def _format_tiles( Example:: Image prefix is: - `maya///_` + `//_` Result for tile 0 for 4x4 will be: - `maya///_tile_1x1_4x4__` + `//_tile_1x1_4x4__` Calculating coordinates is tricky as in Job they are defined as top, left, bottom, right with zero being in top-left corner. But Assembler From 8742acaef94ab0a2695e4637d66fccd783742bea Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 23:56:10 +0200 Subject: [PATCH 034/275] Match logic of global thumbnail extractor to avoid overwriting source files --- .../maya/plugins/publish/extract_thumbnail.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 854301ea48..712159c2be 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -1,5 +1,6 @@ import os import glob +import tempfile import capture @@ -81,9 +82,17 @@ class ExtractThumbnail(publish.Extractor): elif asset_width and asset_height: preset['width'] = asset_width preset['height'] = asset_height - stagingDir = self.staging_dir(instance) + + # Create temp directory for thumbnail + # - this is to avoid "override" of source file + dst_staging = tempfile.mkdtemp(prefix="pyblish_tmp_") + self.log.debug( + "Create temp directory {} for thumbnail".format(dst_staging) + ) + # Store new staging to cleanup paths + instance.context.data["cleanupFullPaths"].append(dst_staging) filename = "{0}".format(instance.name) - path = os.path.join(stagingDir, filename) + path = os.path.join(dst_staging, filename) self.log.info("Outputting images to %s" % path) @@ -137,7 +146,7 @@ class ExtractThumbnail(publish.Extractor): 'name': 'thumbnail', 'ext': 'jpg', 'files': thumbnail, - "stagingDir": stagingDir, + "stagingDir": dst_staging, "thumbnail": True } instance.data["representations"].append(representation) From e97b6ce01f511b1cf240cb8640b871de3d79dc4e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 23 Sep 2022 16:18:08 +0100 Subject: [PATCH 035/275] Fixed path resolving not finding the workfile in certain conditions In case the workfile only contains the project name, the workfile is not found because while the regex matches, the match doesn't have any group, and so it throws an exception. --- openpype/pipeline/workfile/path_resolving.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/pipeline/workfile/path_resolving.py b/openpype/pipeline/workfile/path_resolving.py index ed1d1d793e..4e4d3ca1c0 100644 --- a/openpype/pipeline/workfile/path_resolving.py +++ b/openpype/pipeline/workfile/path_resolving.py @@ -265,6 +265,10 @@ def get_last_workfile_with_version( if not match: continue + if not match.groups(): + output_filenames.append(filename) + continue + file_version = int(match.group(1)) if version is None or file_version > version: output_filenames[:] = [] From 548a37e4e1baf7f8cd9c4af61d0172caeec0f3ba Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 23 Sep 2022 16:18:52 +0100 Subject: [PATCH 036/275] Added setting to remove unmatched assets --- openpype/settings/defaults/project_settings/unreal.json | 1 + .../schemas/projects_schema/schema_project_unreal.json | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/openpype/settings/defaults/project_settings/unreal.json b/openpype/settings/defaults/project_settings/unreal.json index c5f5cdf719..391e2415a5 100644 --- a/openpype/settings/defaults/project_settings/unreal.json +++ b/openpype/settings/defaults/project_settings/unreal.json @@ -1,5 +1,6 @@ { "level_sequences_for_layouts": false, + "delete_unmatched_assets": false, "project_setup": { "dev_mode": true } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json index d26b5c1ccf..09e5791ac4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json @@ -10,6 +10,11 @@ "key": "level_sequences_for_layouts", "label": "Generate level sequences when loading layouts" }, + { + "type": "boolean", + "key": "delete_unmatched_assets", + "label": "Delete assets that are not matched" + }, { "type": "dict", "collapsible": true, From b42fb6aedb10934f5836d89e1734e32669671350 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 23 Sep 2022 16:24:21 +0100 Subject: [PATCH 037/275] Fixed problem with transformations from FBX files --- .../plugins/load/load_layout_existing.py | 52 ++++++++----------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout_existing.py b/openpype/hosts/unreal/plugins/load/load_layout_existing.py index 8cd1950f7e..9ab27d0cef 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout_existing.py +++ b/openpype/hosts/unreal/plugins/load/load_layout_existing.py @@ -80,30 +80,22 @@ class ExistingLayoutLoader(plugin.Loader): raise NotImplementedError( f"Unreal version {ue_major} not supported") - @staticmethod - def _transform_from_basis(transform, basis, conversion): - """Transform a transform from a basis to a new basis.""" - # Get the basis matrix - basis_matrix = unreal.Matrix( - basis[0], - basis[1], - basis[2], - basis[3] - ) - transform_matrix = unreal.Matrix( - transform[0], - transform[1], - transform[2], - transform[3] - ) - - new_transform = ( - basis_matrix.get_inverse() * transform_matrix * basis_matrix) - - return conversion.inverse() * new_transform.transform() - def _get_transform(self, ext, import_data, lasset): conversion = unreal.Matrix.IDENTITY.transform() + fbx_tuning = unreal.Matrix.IDENTITY.transform() + + basis = unreal.Matrix( + lasset.get('basis')[0], + lasset.get('basis')[1], + lasset.get('basis')[2], + lasset.get('basis')[3] + ).transform() + transform = unreal.Matrix( + lasset.get('transform_matrix')[0], + lasset.get('transform_matrix')[1], + lasset.get('transform_matrix')[2], + lasset.get('transform_matrix')[3] + ).transform() # Check for the conversion settings. We cannot access # the alembic conversion settings, so we assume that @@ -111,11 +103,15 @@ class ExistingLayoutLoader(plugin.Loader): if ext == '.fbx': loc = import_data.import_translation rot = import_data.import_rotation.to_vector() - scale = import_data.import_scale + scale = import_data.import_uniform_scale conversion = unreal.Transform( location=[loc.x, loc.y, loc.z], rotation=[rot.x, rot.y, rot.z], - scale=[scale, scale, scale] + scale=[-scale, scale, scale] + ) + fbx_tuning = unreal.Transform( + rotation=[180.0, 0.0, 90.0], + scale=[1.0, 1.0, 1.0] ) elif ext == '.abc': # This is the standard conversion settings for @@ -126,12 +122,8 @@ class ExistingLayoutLoader(plugin.Loader): scale=[1.0, -1.0, 1.0] ) - transform = self._transform_from_basis( - lasset.get('transform_matrix'), - lasset.get('basis'), - conversion - ) - return transform + new_transform = (basis.inverse() * transform * basis) + return fbx_tuning * conversion.inverse() * new_transform def _spawn_actor(self, obj, lasset): actor = EditorLevelLibrary.spawn_actor_from_object( From a3eb15387108938f8536ed4b336e66c893e595aa Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 23 Sep 2022 16:34:05 +0100 Subject: [PATCH 038/275] Checks settings to determine if deleting or not unmatched assets --- .../plugins/load/load_layout_existing.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout_existing.py b/openpype/hosts/unreal/plugins/load/load_layout_existing.py index 9ab27d0cef..3ce99f8ef6 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout_existing.py +++ b/openpype/hosts/unreal/plugins/load/load_layout_existing.py @@ -15,6 +15,7 @@ from openpype.pipeline import ( AVALON_CONTAINER_ID, legacy_io, ) +from openpype.api import get_current_project_settings from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as upipeline @@ -147,7 +148,7 @@ class ExistingLayoutLoader(plugin.Loader): name = "" if family == 'rig': name = "SkeletalMeshFBXLoader" - elif family == 'model': + elif family == 'model' or family == 'staticMesh': name = "StaticMeshFBXLoader" elif family == 'camera': name = "CameraLoader" @@ -200,7 +201,8 @@ class ExistingLayoutLoader(plugin.Loader): loader = self._get_abc_loader(loaders, family) if not loader: - raise AssertionError(f"No valid loader found for {representation}") + self.log.error(f"No valid loader found for {representation}") + return [] # This option is necessary to avoid importing the assets with a # different conversion compared to the other assets. For ABC files, @@ -220,6 +222,9 @@ class ExistingLayoutLoader(plugin.Loader): return assets def _process(self, lib_path): + data = get_current_project_settings() + delete_unmatched = data["unreal"]["delete_unmatched_assets"] + ar = unreal.AssetRegistryHelpers.get_asset_registry() actors = EditorLevelLibrary.get_all_level_actors() @@ -264,16 +269,18 @@ class ExistingLayoutLoader(plugin.Loader): # Get the original path of the file from which the asset has # been imported. - actor.set_actor_label(lasset.get('instance_name')) smc = actor.get_editor_property('static_mesh_component') mesh = smc.get_editor_property('static_mesh') import_data = mesh.get_editor_property('asset_import_data') filename = import_data.get_first_filename() path = Path(filename) - if path.name not in repr_data.get('data').get('path'): + if (not path.name or + path.name not in repr_data.get('data').get('path')): continue + actor.set_actor_label(lasset.get('instance_name')) + mesh_path = Path(mesh.get_path_name()).parent.as_posix() # Create the container for the asset. @@ -352,7 +359,9 @@ class ExistingLayoutLoader(plugin.Loader): if not actor.get_class().get_name() == 'StaticMeshActor': continue if actor not in actors_matched: - EditorLevelLibrary.destroy_actor(actor) + self.log.warning(f"Actor {actor.get_name()} not matched.") + if delete_unmatched: + EditorLevelLibrary.destroy_actor(actor) return containers From 38387fc8997e579c1d4c8281f3810abc48059ce8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 23 Sep 2022 18:46:12 +0200 Subject: [PATCH 039/275] removed 'abstract_collect_render' --- openpype/lib/abstract_collect_render.py | 33 ------------------------- 1 file changed, 33 deletions(-) delete mode 100644 openpype/lib/abstract_collect_render.py diff --git a/openpype/lib/abstract_collect_render.py b/openpype/lib/abstract_collect_render.py deleted file mode 100644 index e4ff87aa0f..0000000000 --- a/openpype/lib/abstract_collect_render.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -"""Content was moved to 'openpype.pipeline.publish.abstract_collect_render'. - -Please change your imports as soon as possible. - -File will be probably removed in OpenPype 3.14.* -""" - -import warnings -from openpype.pipeline.publish import AbstractCollectRender, RenderInstance - - -class CollectRenderDeprecated(DeprecationWarning): - pass - - -warnings.simplefilter("always", CollectRenderDeprecated) -warnings.warn( - ( - "Content of 'abstract_collect_render' was moved." - "\nUsing deprecated source of 'abstract_collect_render'. Content was" - " move to 'openpype.pipeline.publish.abstract_collect_render'." - " Please change your imports as soon as possible." - ), - category=CollectRenderDeprecated, - stacklevel=4 -) - - -__all__ = ( - "AbstractCollectRender", - "RenderInstance" -) From 97f368c3a5377e0972ba56bba99b90dc092be2bf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 23 Sep 2022 18:46:23 +0200 Subject: [PATCH 040/275] removed 'abstract_expected_files' --- openpype/lib/abstract_expected_files.py | 32 ------------------------- 1 file changed, 32 deletions(-) delete mode 100644 openpype/lib/abstract_expected_files.py diff --git a/openpype/lib/abstract_expected_files.py b/openpype/lib/abstract_expected_files.py deleted file mode 100644 index f24d844fe5..0000000000 --- a/openpype/lib/abstract_expected_files.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- -"""Content was moved to 'openpype.pipeline.publish.abstract_expected_files'. - -Please change your imports as soon as possible. - -File will be probably removed in OpenPype 3.14.* -""" - -import warnings -from openpype.pipeline.publish import ExpectedFiles - - -class ExpectedFilesDeprecated(DeprecationWarning): - pass - - -warnings.simplefilter("always", ExpectedFilesDeprecated) -warnings.warn( - ( - "Content of 'abstract_expected_files' was moved." - "\nUsing deprecated source of 'abstract_expected_files'. Content was" - " move to 'openpype.pipeline.publish.abstract_expected_files'." - " Please change your imports as soon as possible." - ), - category=ExpectedFilesDeprecated, - stacklevel=4 -) - - -__all__ = ( - "ExpectedFiles", -) From e21334463398df7eb2e199dc61c4f5eef85325d0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 23 Sep 2022 18:46:33 +0200 Subject: [PATCH 041/275] removed 'abstract_metaplugins' --- openpype/lib/abstract_metaplugins.py | 35 ---------------------------- 1 file changed, 35 deletions(-) delete mode 100644 openpype/lib/abstract_metaplugins.py diff --git a/openpype/lib/abstract_metaplugins.py b/openpype/lib/abstract_metaplugins.py deleted file mode 100644 index 346b5d86b3..0000000000 --- a/openpype/lib/abstract_metaplugins.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Content was moved to 'openpype.pipeline.publish.publish_plugins'. - -Please change your imports as soon as possible. - -File will be probably removed in OpenPype 3.14.* -""" - -import warnings -from openpype.pipeline.publish import ( - AbstractMetaInstancePlugin, - AbstractMetaContextPlugin -) - - -class MetaPluginsDeprecated(DeprecationWarning): - pass - - -warnings.simplefilter("always", MetaPluginsDeprecated) -warnings.warn( - ( - "Content of 'abstract_metaplugins' was moved." - "\nUsing deprecated source of 'abstract_metaplugins'. Content was" - " moved to 'openpype.pipeline.publish.publish_plugins'." - " Please change your imports as soon as possible." - ), - category=MetaPluginsDeprecated, - stacklevel=4 -) - - -__all__ = ( - "AbstractMetaInstancePlugin", - "AbstractMetaContextPlugin", -) From 5f32cdc7a53e7ec3aa5dcf4df1fe0ddfc1d8ab43 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 23 Sep 2022 18:49:34 +0200 Subject: [PATCH 042/275] removed config --- openpype/api.py | 2 -- openpype/lib/config.py | 41 ----------------------------------------- 2 files changed, 43 deletions(-) delete mode 100644 openpype/lib/config.py diff --git a/openpype/api.py b/openpype/api.py index 0466eb7f78..b60cd21d2b 100644 --- a/openpype/api.py +++ b/openpype/api.py @@ -11,7 +11,6 @@ from .lib import ( PypeLogger, Logger, Anatomy, - config, execute, run_subprocess, version_up, @@ -72,7 +71,6 @@ __all__ = [ "PypeLogger", "Logger", "Anatomy", - "config", "execute", "get_default_components", "ApplicationManager", diff --git a/openpype/lib/config.py b/openpype/lib/config.py deleted file mode 100644 index 26822649e4..0000000000 --- a/openpype/lib/config.py +++ /dev/null @@ -1,41 +0,0 @@ -import warnings -import functools - - -class ConfigDeprecatedWarning(DeprecationWarning): - pass - - -def deprecated(func): - """Mark functions as deprecated. - - It will result in a warning being emitted when the function is used. - """ - - @functools.wraps(func) - def new_func(*args, **kwargs): - warnings.simplefilter("always", ConfigDeprecatedWarning) - warnings.warn( - ( - "Deprecated import of function '{}'." - " Class was moved to 'openpype.lib.dateutils.{}'." - " Please change your imports." - ).format(func.__name__), - category=ConfigDeprecatedWarning - ) - return func(*args, **kwargs) - return new_func - - -@deprecated -def get_datetime_data(datetime_obj=None): - from .dateutils import get_datetime_data - - return get_datetime_data(datetime_obj) - - -@deprecated -def get_formatted_current_time(): - from .dateutils import get_formatted_current_time - - return get_formatted_current_time() From c06ce65885f819475aa36b625f0f0d05f3de0b21 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 23 Sep 2022 18:54:23 +0200 Subject: [PATCH 043/275] removed 'editorial' --- openpype/lib/__init__.py | 23 --------- openpype/lib/editorial.py | 102 -------------------------------------- 2 files changed, 125 deletions(-) delete mode 100644 openpype/lib/editorial.py diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 17aafc3e8b..a64b7c2911 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -203,19 +203,6 @@ from .path_tools import ( get_project_basic_paths, ) -from .editorial import ( - is_overlapping_otio_ranges, - otio_range_to_frame_range, - otio_range_with_handles, - get_media_range_with_retimes, - convert_to_padded_path, - trim_media_range, - range_from_frames, - frames_to_secons, - frames_to_timecode, - make_sequence_collection -) - from .openpype_version import ( op_version_control_available, get_openpype_version, @@ -383,16 +370,6 @@ __all__ = [ "validate_mongo_connection", "OpenPypeMongoConnection", - "is_overlapping_otio_ranges", - "otio_range_with_handles", - "convert_to_padded_path", - "otio_range_to_frame_range", - "get_media_range_with_retimes", - "trim_media_range", - "range_from_frames", - "frames_to_secons", - "frames_to_timecode", - "make_sequence_collection", "create_project_folders", "create_workdir_extra_folders", "get_project_basic_paths", diff --git a/openpype/lib/editorial.py b/openpype/lib/editorial.py deleted file mode 100644 index 49220b4f15..0000000000 --- a/openpype/lib/editorial.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Code related to editorial utility functions was moved -to 'openpype.pipeline.editorial' please change your imports as soon as -possible. File will be probably removed in OpenPype 3.14.* -""" - -import warnings -import functools - - -class EditorialDeprecatedWarning(DeprecationWarning): - pass - - -def editorial_deprecated(func): - """Mark functions as deprecated. - - It will result in a warning being emitted when the function is used. - """ - - @functools.wraps(func) - def new_func(*args, **kwargs): - warnings.simplefilter("always", EditorialDeprecatedWarning) - warnings.warn( - ( - "Call to deprecated function '{}'." - " Function was moved to 'openpype.pipeline.editorial'." - ).format(func.__name__), - category=EditorialDeprecatedWarning, - stacklevel=2 - ) - return func(*args, **kwargs) - return new_func - - -@editorial_deprecated -def otio_range_to_frame_range(*args, **kwargs): - from openpype.pipeline.editorial import otio_range_to_frame_range - - return otio_range_to_frame_range(*args, **kwargs) - - -@editorial_deprecated -def otio_range_with_handles(*args, **kwargs): - from openpype.pipeline.editorial import otio_range_with_handles - - return otio_range_with_handles(*args, **kwargs) - - -@editorial_deprecated -def is_overlapping_otio_ranges(*args, **kwargs): - from openpype.pipeline.editorial import is_overlapping_otio_ranges - - return is_overlapping_otio_ranges(*args, **kwargs) - - -@editorial_deprecated -def convert_to_padded_path(*args, **kwargs): - from openpype.pipeline.editorial import convert_to_padded_path - - return convert_to_padded_path(*args, **kwargs) - - -@editorial_deprecated -def trim_media_range(*args, **kwargs): - from openpype.pipeline.editorial import trim_media_range - - return trim_media_range(*args, **kwargs) - - -@editorial_deprecated -def range_from_frames(*args, **kwargs): - from openpype.pipeline.editorial import range_from_frames - - return range_from_frames(*args, **kwargs) - - -@editorial_deprecated -def frames_to_secons(*args, **kwargs): - from openpype.pipeline.editorial import frames_to_seconds - - return frames_to_seconds(*args, **kwargs) - - -@editorial_deprecated -def frames_to_timecode(*args, **kwargs): - from openpype.pipeline.editorial import frames_to_timecode - - return frames_to_timecode(*args, **kwargs) - - -@editorial_deprecated -def make_sequence_collection(*args, **kwargs): - from openpype.pipeline.editorial import make_sequence_collection - - return make_sequence_collection(*args, **kwargs) - - -@editorial_deprecated -def get_media_range_with_retimes(*args, **kwargs): - from openpype.pipeline.editorial import get_media_range_with_retimes - - return get_media_range_with_retimes(*args, **kwargs) From affd54bd1ae6603fb0e59fffc3c39c2b08c85e41 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 28 Sep 2022 18:45:47 +0800 Subject: [PATCH 044/275] write color sets --- openpype/hosts/maya/plugins/create/create_rig.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_rig.py b/openpype/hosts/maya/plugins/create/create_rig.py index 3b0ee1e22a..8032e5fbbd 100644 --- a/openpype/hosts/maya/plugins/create/create_rig.py +++ b/openpype/hosts/maya/plugins/create/create_rig.py @@ -13,18 +13,12 @@ class CreateRig(plugin.Creator): label = "Rig" family = "rig" icon = "wheelchair" - write_color_sets = False - write_face_sets = False - - def __init__(self, *args, **kwargs): - super(CreateRig, self).__init__(*args, **kwargs) - self.data["writeColorSets"] = self.write_color_sets - self.data["writeFaceSets"] = self.write_face_sets def process(self): with lib.undo_chunk(): instance = super(CreateRig, self).process() + self.log.info("Creating Rig instance set up ...") controls = cmds.sets(name="controls_SET", empty=True) pointcache = cmds.sets(name="out_SET", empty=True) From 073a38726e3450409a9fd2bc7e6d789583c379e3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 15:00:49 +0200 Subject: [PATCH 045/275] footer widget is not part of subset widget --- openpype/tools/publisher/window.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 2a0e6e940a..3b504655d9 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -142,6 +142,9 @@ class PublisherWindow(QtWidgets.QDialog): subset_content_layout.addWidget(subset_attributes_wrap, 7) # Footer + footer_widget = QtWidgets.QWidget(self) + footer_bottom_widget = QtWidgets.QWidget(footer_widget) + comment_input = PlaceholderLineEdit(subset_frame) comment_input.setObjectName("PublishCommentInput") comment_input.setPlaceholderText( @@ -153,13 +156,17 @@ class PublisherWindow(QtWidgets.QDialog): validate_btn = ValidateBtn(subset_frame) publish_btn = PublishBtn(subset_frame) - footer_layout = QtWidgets.QHBoxLayout() - footer_layout.setContentsMargins(0, 0, 0, 0) - footer_layout.addWidget(comment_input, 1) - footer_layout.addWidget(reset_btn, 0) - footer_layout.addWidget(stop_btn, 0) - footer_layout.addWidget(validate_btn, 0) - footer_layout.addWidget(publish_btn, 0) + footer_bottom_layout = QtWidgets.QHBoxLayout(footer_bottom_widget) + footer_bottom_layout.setContentsMargins(0, 0, 0, 0) + footer_bottom_layout.addStretch(1) + footer_bottom_layout.addWidget(reset_btn, 0) + footer_bottom_layout.addWidget(stop_btn, 0) + footer_bottom_layout.addWidget(validate_btn, 0) + footer_bottom_layout.addWidget(publish_btn, 0) + + footer_layout = QtWidgets.QVBoxLayout(footer_widget) + footer_layout.addWidget(comment_input, 0) + footer_layout.addWidget(footer_bottom_widget, 0) # Subset frame layout subset_layout = QtWidgets.QVBoxLayout(subset_frame) @@ -167,10 +174,9 @@ class PublisherWindow(QtWidgets.QDialog): marings.setLeft(marings.left() * 2) marings.setRight(marings.right() * 2) marings.setTop(marings.top() * 2) - marings.setBottom(marings.bottom() * 2) + marings.setBottom(0) subset_layout.setContentsMargins(marings) subset_layout.addWidget(subset_content_widget, 1) - subset_layout.addLayout(footer_layout, 0) # Create publish frame publish_frame = PublishFrame(controller, content_stacked_widget) @@ -192,6 +198,7 @@ class PublisherWindow(QtWidgets.QDialog): main_layout.addWidget(header_widget, 0) main_layout.addWidget(line_widget, 0) main_layout.addWidget(content_stacked_widget, 1) + main_layout.addWidget(footer_widget, 0) creator_window = CreateDialog(controller, parent=self) From 46ea4561f31cc96537e3820c858385baa7e1b30b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 15:49:31 +0200 Subject: [PATCH 046/275] implemented tabs widget --- openpype/style/data.json | 1 + openpype/style/style.css | 23 +++++ openpype/tools/publisher/widgets/__init__.py | 6 ++ .../tools/publisher/widgets/tabs_widget.py | 83 +++++++++++++++++++ 4 files changed, 113 insertions(+) create mode 100644 openpype/tools/publisher/widgets/tabs_widget.py diff --git a/openpype/style/data.json b/openpype/style/data.json index adda49de23..b75aa98508 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -91,6 +91,7 @@ "error": "#AA5050", "success": "#458056", "warning": "#ffc671", + "tab-bg": "#16191d", "list-view-group": { "bg": "#434a56", "bg-hover": "rgba(168, 175, 189, 0.3)", diff --git a/openpype/style/style.css b/openpype/style/style.css index 72d12a9230..ab23dd621f 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -856,6 +856,29 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { } /* New Create/Publish UI */ +PublisherTabsWidget { + background: {color:publisher:tab-bg}; +} + +PublisherTabBtn { + border-radius: 0px; + background: {color:bg-inputs}; + font-size: 9pt; + font-weight: regular; + padding: 0.5em 1em 0.5em 1em; +} + +PublisherTabBtn:hover { + background: {color:bg-buttons}; +} + +PublisherTabBtn[active="1"] { + background: {color:bg}; +} +PublisherTabBtn[active="1"]:hover { + background: {color:bg}; +} + #CreatorDetailedDescription { padding-left: 5px; padding-right: 5px; diff --git a/openpype/tools/publisher/widgets/__init__.py b/openpype/tools/publisher/widgets/__init__.py index 55afc349ff..a09e1353ec 100644 --- a/openpype/tools/publisher/widgets/__init__.py +++ b/openpype/tools/publisher/widgets/__init__.py @@ -33,6 +33,10 @@ from .list_view_widgets import ( InstanceListView ) +from .tabs_widget import ( + PublisherTabsWidget +) + __all__ = ( "get_icon_path", @@ -57,4 +61,6 @@ __all__ = ( "InstanceCardView", "InstanceListView", + + "PublisherTabsWidget", ) diff --git a/openpype/tools/publisher/widgets/tabs_widget.py b/openpype/tools/publisher/widgets/tabs_widget.py new file mode 100644 index 0000000000..0e92a6fd8d --- /dev/null +++ b/openpype/tools/publisher/widgets/tabs_widget.py @@ -0,0 +1,83 @@ +from Qt import QtWidgets, QtCore +from openpype.tools.utils import set_style_property + + +class PublisherTabBtn(QtWidgets.QPushButton): + tab_clicked = QtCore.Signal(str) + + def __init__(self, identifier, label, parent): + super(PublisherTabBtn, self).__init__(label, parent) + self._identifier = identifier + self._active = False + + self.clicked.connect(self._on_click) + + def _on_click(self): + self.tab_clicked.emit(self.identifier) + + @property + def identifier(self): + return self._identifier + + def activate(self): + if self._active: + return + self._active = True + set_style_property(self, "active", "1") + + def deactivate(self): + if not self._active: + return + self._active = False + set_style_property(self, "active", "") + + +class PublisherTabsWidget(QtWidgets.QFrame): + tab_changed = QtCore.Signal(str, str) + + def __init__(self, parent=None): + super(PublisherTabsWidget, self).__init__(parent) + + btns_widget = QtWidgets.QWidget(self) + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.setSpacing(0) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(btns_widget, 0) + layout.addStretch(1) + + self._btns_layout = btns_layout + + self._current_button = None + self._buttons_by_identifier = {} + + def add_tab(self, label, identifier): + button = PublisherTabBtn(identifier, label, self) + button.tab_clicked.connect(self._on_tab_click) + self._btns_layout.addWidget(button, 0) + self._buttons_by_identifier[identifier] = button + + if self._current_button is None: + self.set_current_tab(identifier) + + def set_current_tab(self, identifier): + if identifier == self._current_button: + return + + new_btn = self._buttons_by_identifier.get(identifier) + if new_btn is None: + return + + old_identifier = self._current_button + old_btn = self._buttons_by_identifier.get(old_identifier) + self._current_button = identifier + + if old_btn is not None: + old_btn.deactivate() + new_btn.activate() + self.tab_changed.emit(old_identifier, identifier) + + def _on_tab_click(self, identifier): + self.set_current_tab(identifier) From ae62357d9873fec68e55fbfc545c8b9a61409667 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 16:00:22 +0200 Subject: [PATCH 047/275] added tab to main window --- openpype/tools/publisher/window.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 3b504655d9..d02fe704ee 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -15,6 +15,8 @@ from .widgets import ( SubsetAttributesWidget, InstanceCardView, InstanceListView, + PublisherTabsWidget, + CreateDialog, StopBtn, @@ -78,9 +80,11 @@ class PublisherWindow(QtWidgets.QDialog): header_layout.addWidget(icon_label, 0) header_layout.addWidget(context_label, 1) - line_widget = QtWidgets.QWidget(self) - line_widget.setObjectName("Separator") - line_widget.setMinimumHeight(2) + tabs_widget = PublisherTabsWidget(self) + tabs_widget.add_tab("Create", "create") + tabs_widget.add_tab("Publish", "publish") + tabs_widget.add_tab("Report", "report") + tabs_widget.add_tab("Details", "details") # Content content_stacked_widget = QtWidgets.QWidget(self) @@ -196,12 +200,14 @@ class PublisherWindow(QtWidgets.QDialog): main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) main_layout.addWidget(header_widget, 0) - main_layout.addWidget(line_widget, 0) + main_layout.addWidget(tabs_widget, 0) main_layout.addWidget(content_stacked_widget, 1) main_layout.addWidget(footer_widget, 0) creator_window = CreateDialog(controller, parent=self) + tabs_widget.tab_changed.connect(self._on_tab_change) + create_btn.clicked.connect(self._on_create_clicked) delete_btn.clicked.connect(self._on_delete_clicked) change_view_btn.clicked.connect(self._on_change_view_clicked) @@ -318,6 +324,9 @@ class PublisherWindow(QtWidgets.QDialog): self._on_subset_change() + def _on_tab_change(self, prev_tab, new_tab): + print(prev_tab, new_tab) + def _on_create_clicked(self): self.creator_window.show() From 7a0ce610b1472332719ec19e3ca98badbf8ab8c6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 16:41:12 +0200 Subject: [PATCH 048/275] moved overview widget from window --- openpype/tools/publisher/widgets/__init__.py | 43 +-- .../publisher/widgets/overview_widget.py | 246 +++++++++++++ openpype/tools/publisher/window.py | 340 ++++-------------- openpype/tools/traypublisher/window.py | 2 +- 4 files changed, 328 insertions(+), 303 deletions(-) create mode 100644 openpype/tools/publisher/widgets/overview_widget.py diff --git a/openpype/tools/publisher/widgets/__init__.py b/openpype/tools/publisher/widgets/__init__.py index a09e1353ec..869f7adf9b 100644 --- a/openpype/tools/publisher/widgets/__init__.py +++ b/openpype/tools/publisher/widgets/__init__.py @@ -3,64 +3,31 @@ from .icons import ( get_pixmap, get_icon ) -from .border_label_widget import ( - BorderedLabelWidget -) from .widgets import ( - SubsetAttributesWidget, - StopBtn, ResetBtn, ValidateBtn, PublishBtn, - - CreateInstanceBtn, - RemoveInstanceBtn, - ChangeViewBtn ) -from .publish_widget import ( - PublishFrame -) -from .create_dialog import ( - CreateDialog -) - -from .card_view_widgets import ( - InstanceCardView -) - -from .list_view_widgets import ( - InstanceListView -) - -from .tabs_widget import ( - PublisherTabsWidget -) - +from .publish_widget import PublishFrame +from .create_dialog import CreateDialog +from .tabs_widget import PublisherTabsWidget +from .overview_widget import CreateOverviewWidget __all__ = ( "get_icon_path", "get_pixmap", "get_icon", - "SubsetAttributesWidget", - "BorderedLabelWidget", - "StopBtn", "ResetBtn", "ValidateBtn", "PublishBtn", - "CreateInstanceBtn", - "RemoveInstanceBtn", - "ChangeViewBtn", - "PublishFrame", "CreateDialog", - "InstanceCardView", - "InstanceListView", - "PublisherTabsWidget", + "CreateOverviewWidget", ) diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py new file mode 100644 index 0000000000..abdd98ff7c --- /dev/null +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -0,0 +1,246 @@ +from Qt import QtWidgets, QtCore + +from .border_label_widget import BorderedLabelWidget + +from .card_view_widgets import InstanceCardView +from .list_view_widgets import InstanceListView +from .widgets import ( + SubsetAttributesWidget, + CreateInstanceBtn, + RemoveInstanceBtn, + ChangeViewBtn +) + + +class CreateOverviewWidget(QtWidgets.QFrame): + active_changed = QtCore.Signal() + instance_context_changed = QtCore.Signal() + create_requested = QtCore.Signal() + + def __init__(self, controller, parent): + super(CreateOverviewWidget, self).__init__(parent) + + self._controller = controller + self._refreshing_instances = False + + subset_views_widget = BorderedLabelWidget( + "Subsets to publish", self + ) + + subset_view_cards = InstanceCardView(controller, subset_views_widget) + subset_list_view = InstanceListView(controller, subset_views_widget) + + subset_views_layout = QtWidgets.QStackedLayout() + subset_views_layout.addWidget(subset_view_cards) + subset_views_layout.addWidget(subset_list_view) + + # Buttons at the bottom of subset view + create_btn = CreateInstanceBtn(self) + delete_btn = RemoveInstanceBtn(self) + change_view_btn = ChangeViewBtn(self) + + # Subset details widget + subset_attributes_wrap = BorderedLabelWidget( + "Publish options", self + ) + subset_attributes_widget = SubsetAttributesWidget( + controller, subset_attributes_wrap + ) + subset_attributes_wrap.set_center_widget(subset_attributes_widget) + + # Layout of buttons at the bottom of subset view + subset_view_btns_layout = QtWidgets.QHBoxLayout() + subset_view_btns_layout.setContentsMargins(0, 5, 0, 0) + subset_view_btns_layout.addWidget(create_btn) + subset_view_btns_layout.addSpacing(5) + subset_view_btns_layout.addWidget(delete_btn) + subset_view_btns_layout.addStretch(1) + subset_view_btns_layout.addWidget(change_view_btn) + + # Layout of view and buttons + # - widget 'subset_view_widget' is necessary + # - only layout won't be resized automatically to minimum size hint + # on child resize request! + subset_view_widget = QtWidgets.QWidget(subset_views_widget) + subset_view_layout = QtWidgets.QVBoxLayout(subset_view_widget) + subset_view_layout.setContentsMargins(0, 0, 0, 0) + subset_view_layout.addLayout(subset_views_layout, 1) + subset_view_layout.addLayout(subset_view_btns_layout, 0) + + subset_views_widget.set_center_widget(subset_view_widget) + + # Whole subset layout with attributes and details + subset_content_widget = QtWidgets.QWidget(self) + subset_content_layout = QtWidgets.QHBoxLayout(subset_content_widget) + subset_content_layout.setContentsMargins(0, 0, 0, 0) + subset_content_layout.addWidget(subset_views_widget, 3) + subset_content_layout.addWidget(subset_attributes_wrap, 7) + + # Subset frame layout + main_layout = QtWidgets.QVBoxLayout(self) + marings = main_layout.contentsMargins() + marings.setLeft(marings.left() * 2) + marings.setRight(marings.right() * 2) + marings.setTop(marings.top() * 2) + marings.setBottom(0) + main_layout.setContentsMargins(marings) + main_layout.addWidget(subset_content_widget, 1) + + create_btn.clicked.connect(self._on_create_clicked) + delete_btn.clicked.connect(self._on_delete_clicked) + change_view_btn.clicked.connect(self._on_change_view_clicked) + + # Selection changed + subset_list_view.selection_changed.connect( + self._on_subset_change + ) + subset_view_cards.selection_changed.connect( + self._on_subset_change + ) + # Active instances changed + subset_list_view.active_changed.connect( + self._on_active_changed + ) + subset_view_cards.active_changed.connect( + self._on_active_changed + ) + # Instance context has changed + subset_attributes_widget.instance_context_changed.connect( + self._on_instance_context_change + ) + + controller.add_publish_reset_callback(self._on_publish_reset) + controller.add_instances_refresh_callback(self._on_instances_refresh) + + self.subset_content_widget = subset_content_widget + + self.subset_view_cards = subset_view_cards + self.subset_list_view = subset_list_view + self.subset_views_layout = subset_views_layout + + self.delete_btn = delete_btn + + self.subset_attributes_widget = subset_attributes_widget + + def _on_create_clicked(self): + """Pass signal to parent widget which should care about changing state. + + We don't change anything here until the parent will care about it. + """ + + self.create_requested.emit() + + def _on_delete_clicked(self): + instances, _ = self.get_selected_items() + + # Ask user if he really wants to remove instances + dialog = QtWidgets.QMessageBox(self) + dialog.setIcon(QtWidgets.QMessageBox.Question) + dialog.setWindowTitle("Are you sure?") + if len(instances) > 1: + msg = ( + "Do you really want to remove {} instances?" + ).format(len(instances)) + else: + msg = ( + "Do you really want to remove the instance?" + ) + dialog.setText(msg) + dialog.setStandardButtons( + QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel + ) + dialog.setDefaultButton(QtWidgets.QMessageBox.Ok) + dialog.setEscapeButton(QtWidgets.QMessageBox.Cancel) + dialog.exec_() + # Skip if OK was not clicked + if dialog.result() == QtWidgets.QMessageBox.Ok: + self._controller.remove_instances(instances) + + def _on_change_view_clicked(self): + self._change_view_type() + + def _on_subset_change(self, *_args): + # Ignore changes if in middle of refreshing + if self._refreshing_instances: + return + + instances, context_selected = self.get_selected_items() + + # Disable delete button if nothing is selected + self.delete_btn.setEnabled(len(instances) > 0) + + self.subset_attributes_widget.set_current_instances( + instances, context_selected + ) + + def _on_active_changed(self): + if self._refreshing_instances: + return + self.active_changed.emit() + + def _on_instance_context_change(self): + current_idx = self.subset_views_layout.currentIndex() + for idx in range(self.subset_views_layout.count()): + if idx == current_idx: + continue + widget = self.subset_views_layout.widget(idx) + if widget.refreshed: + widget.set_refreshed(False) + + current_widget = self.subset_views_layout.widget(current_idx) + current_widget.refresh_instance_states() + + self.instance_context_changed.emit() + + def get_selected_items(self): + view = self.subset_views_layout.currentWidget() + return view.get_selected_items() + + def _change_view_type(self): + idx = self.subset_views_layout.currentIndex() + new_idx = (idx + 1) % self.subset_views_layout.count() + self.subset_views_layout.setCurrentIndex(new_idx) + + new_view = self.subset_views_layout.currentWidget() + if not new_view.refreshed: + new_view.refresh() + new_view.set_refreshed(True) + else: + new_view.refresh_instance_states() + + self._on_subset_change() + + def _refresh_instances(self): + if self._refreshing_instances: + return + + self._refreshing_instances = True + + for idx in range(self.subset_views_layout.count()): + widget = self.subset_views_layout.widget(idx) + widget.set_refreshed(False) + + view = self.subset_views_layout.currentWidget() + view.refresh() + view.set_refreshed(True) + + self._refreshing_instances = False + + # Force to change instance and refresh details + self._on_subset_change() + + def _on_publish_reset(self): + """Context in controller has been refreshed.""" + + self.subset_content_widget.setEnabled(self._controller.host_is_valid) + + def _on_instances_refresh(self): + """Controller refreshed instances.""" + + self._refresh_instances() + + # Give a change to process Resize Request + QtWidgets.QApplication.processEvents() + # Trigger update geometry of + widget = self.subset_views_layout.currentWidget() + widget.updateGeometry() diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index d02fe704ee..8df9f9bbf5 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -10,11 +10,9 @@ from openpype.tools.utils import ( ) from .control import PublisherController from .widgets import ( - BorderedLabelWidget, + CreateOverviewWidget, PublishFrame, - SubsetAttributesWidget, - InstanceCardView, - InstanceListView, + PublisherTabsWidget, CreateDialog, @@ -23,10 +21,6 @@ from .widgets import ( ResetBtn, ValidateBtn, PublishBtn, - - CreateInstanceBtn, - RemoveInstanceBtn, - ChangeViewBtn ) @@ -62,7 +56,6 @@ class PublisherWindow(QtWidgets.QDialog): self._reset_on_show = reset_on_show self._first_show = True - self._refreshing_instances = False controller = PublisherController() @@ -78,8 +71,10 @@ class PublisherWindow(QtWidgets.QDialog): header_layout.setContentsMargins(15, 15, 15, 15) header_layout.setSpacing(15) header_layout.addWidget(icon_label, 0) - header_layout.addWidget(context_label, 1) + header_layout.addWidget(context_label, 0) + header_layout.addStretch(1) + # Tabs widget under header tabs_widget = PublisherTabsWidget(self) tabs_widget.add_tab("Create", "create") tabs_widget.add_tab("Publish", "publish") @@ -89,76 +84,24 @@ class PublisherWindow(QtWidgets.QDialog): # Content content_stacked_widget = QtWidgets.QWidget(self) - # Subset widget - subset_frame = QtWidgets.QFrame(content_stacked_widget) - - subset_views_widget = BorderedLabelWidget( - "Subsets to publish", subset_frame + create_overview_widget = CreateOverviewWidget( + controller, content_stacked_widget ) - subset_view_cards = InstanceCardView(controller, subset_views_widget) - subset_list_view = InstanceListView(controller, subset_views_widget) - - subset_views_layout = QtWidgets.QStackedLayout() - subset_views_layout.addWidget(subset_view_cards) - subset_views_layout.addWidget(subset_list_view) - - # Buttons at the bottom of subset view - create_btn = CreateInstanceBtn(subset_frame) - delete_btn = RemoveInstanceBtn(subset_frame) - change_view_btn = ChangeViewBtn(subset_frame) - - # Subset details widget - subset_attributes_wrap = BorderedLabelWidget( - "Publish options", subset_frame - ) - subset_attributes_widget = SubsetAttributesWidget( - controller, subset_attributes_wrap - ) - subset_attributes_wrap.set_center_widget(subset_attributes_widget) - - # Layout of buttons at the bottom of subset view - subset_view_btns_layout = QtWidgets.QHBoxLayout() - subset_view_btns_layout.setContentsMargins(0, 5, 0, 0) - subset_view_btns_layout.addWidget(create_btn) - subset_view_btns_layout.addSpacing(5) - subset_view_btns_layout.addWidget(delete_btn) - subset_view_btns_layout.addStretch(1) - subset_view_btns_layout.addWidget(change_view_btn) - - # Layout of view and buttons - # - widget 'subset_view_widget' is necessary - # - only layout won't be resized automatically to minimum size hint - # on child resize request! - subset_view_widget = QtWidgets.QWidget(subset_views_widget) - subset_view_layout = QtWidgets.QVBoxLayout(subset_view_widget) - subset_view_layout.setContentsMargins(0, 0, 0, 0) - subset_view_layout.addLayout(subset_views_layout, 1) - subset_view_layout.addLayout(subset_view_btns_layout, 0) - - subset_views_widget.set_center_widget(subset_view_widget) - - # Whole subset layout with attributes and details - subset_content_widget = QtWidgets.QWidget(subset_frame) - subset_content_layout = QtWidgets.QHBoxLayout(subset_content_widget) - subset_content_layout.setContentsMargins(0, 0, 0, 0) - subset_content_layout.addWidget(subset_views_widget, 3) - subset_content_layout.addWidget(subset_attributes_wrap, 7) - # Footer footer_widget = QtWidgets.QWidget(self) footer_bottom_widget = QtWidgets.QWidget(footer_widget) - comment_input = PlaceholderLineEdit(subset_frame) + comment_input = PlaceholderLineEdit(footer_widget) comment_input.setObjectName("PublishCommentInput") comment_input.setPlaceholderText( "Attach a comment to your publish" ) - reset_btn = ResetBtn(subset_frame) - stop_btn = StopBtn(subset_frame) - validate_btn = ValidateBtn(subset_frame) - publish_btn = PublishBtn(subset_frame) + reset_btn = ResetBtn(footer_widget) + stop_btn = StopBtn(footer_widget) + validate_btn = ValidateBtn(footer_widget) + publish_btn = PublishBtn(footer_widget) footer_bottom_layout = QtWidgets.QHBoxLayout(footer_bottom_widget) footer_bottom_layout.setContentsMargins(0, 0, 0, 0) @@ -172,16 +115,6 @@ class PublisherWindow(QtWidgets.QDialog): footer_layout.addWidget(comment_input, 0) footer_layout.addWidget(footer_bottom_widget, 0) - # Subset frame layout - subset_layout = QtWidgets.QVBoxLayout(subset_frame) - marings = subset_layout.contentsMargins() - marings.setLeft(marings.left() * 2) - marings.setRight(marings.right() * 2) - marings.setTop(marings.top() * 2) - marings.setBottom(0) - subset_layout.setContentsMargins(marings) - subset_layout.addWidget(subset_content_widget, 1) - # Create publish frame publish_frame = PublishFrame(controller, content_stacked_widget) @@ -192,7 +125,7 @@ class PublisherWindow(QtWidgets.QDialog): content_stacked_layout.setStackingMode( QtWidgets.QStackedLayout.StackAll ) - content_stacked_layout.addWidget(subset_frame) + content_stacked_layout.addWidget(create_overview_widget) content_stacked_layout.addWidget(publish_frame) # Add main frame to this window @@ -207,37 +140,22 @@ class PublisherWindow(QtWidgets.QDialog): creator_window = CreateDialog(controller, parent=self) tabs_widget.tab_changed.connect(self._on_tab_change) - - create_btn.clicked.connect(self._on_create_clicked) - delete_btn.clicked.connect(self._on_delete_clicked) - change_view_btn.clicked.connect(self._on_change_view_clicked) + create_overview_widget.active_changed.connect( + self._on_context_or_active_change + ) + create_overview_widget.instance_context_changed.connect( + self._on_context_or_active_change + ) + create_overview_widget.create_requested.connect( + self._on_create_request + ) reset_btn.clicked.connect(self._on_reset_clicked) stop_btn.clicked.connect(self._on_stop_clicked) validate_btn.clicked.connect(self._on_validate_clicked) publish_btn.clicked.connect(self._on_publish_clicked) - # Selection changed - subset_list_view.selection_changed.connect( - self._on_subset_change - ) - subset_view_cards.selection_changed.connect( - self._on_subset_change - ) - # Active instances changed - subset_list_view.active_changed.connect( - self._on_active_changed - ) - subset_view_cards.active_changed.connect( - self._on_active_changed - ) - # Instance context has changed - subset_attributes_widget.instance_context_changed.connect( - self._on_instance_context_change - ) - controller.add_instances_refresh_callback(self._on_instances_refresh) - controller.add_publish_reset_callback(self._on_publish_reset) controller.add_publish_started_callback(self._on_publish_start) controller.add_publish_validated_callback(self._on_publish_validated) @@ -246,22 +164,15 @@ class PublisherWindow(QtWidgets.QDialog): # Store header for TrayPublisher self._header_layout = header_layout + self._tabs_widget = tabs_widget + self._content_stacked_widget = content_stacked_widget self.content_stacked_layout = content_stacked_layout + self._create_overview_widget = create_overview_widget self.publish_frame = publish_frame - self.subset_frame = subset_frame - self.subset_content_widget = subset_content_widget self.context_label = context_label - self.subset_view_cards = subset_view_cards - self.subset_list_view = subset_list_view - self.subset_views_layout = subset_views_layout - - self.delete_btn = delete_btn - - self.subset_attributes_widget = subset_attributes_widget - self.comment_input = comment_input self.stop_btn = stop_btn @@ -269,10 +180,14 @@ class PublisherWindow(QtWidgets.QDialog): self.validate_btn = validate_btn self.publish_btn = publish_btn - self.controller = controller + self._controller = controller self.creator_window = creator_window + @property + def controller(self): + return self._controller + def showEvent(self, event): super(PublisherWindow, self).showEvent(event) if self._first_show: @@ -283,88 +198,33 @@ class PublisherWindow(QtWidgets.QDialog): self.reset() def closeEvent(self, event): - self.controller.save_changes() + self._controller.save_changes() super(PublisherWindow, self).closeEvent(event) def reset(self): - self.controller.reset() + self._controller.reset() def set_context_label(self, label): self.context_label.setText(label) - def get_selected_items(self): - view = self.subset_views_layout.currentWidget() - return view.get_selected_items() - - def _on_instance_context_change(self): - current_idx = self.subset_views_layout.currentIndex() - for idx in range(self.subset_views_layout.count()): - if idx == current_idx: - continue - widget = self.subset_views_layout.widget(idx) - if widget.refreshed: - widget.set_refreshed(False) - - current_widget = self.subset_views_layout.widget(current_idx) - current_widget.refresh_instance_states() - - self._validate_create_instances() - - def _change_view_type(self): - idx = self.subset_views_layout.currentIndex() - new_idx = (idx + 1) % self.subset_views_layout.count() - self.subset_views_layout.setCurrentIndex(new_idx) - - new_view = self.subset_views_layout.currentWidget() - if not new_view.refreshed: - new_view.refresh() - new_view.set_refreshed(True) - else: - new_view.refresh_instance_states() - - self._on_subset_change() - def _on_tab_change(self, prev_tab, new_tab): print(prev_tab, new_tab) - def _on_create_clicked(self): - self.creator_window.show() + def _on_context_or_active_change(self): + self._validate_create_instances() - def _on_delete_clicked(self): - instances, _ = self.get_selected_items() + def _on_create_request(self): + self._go_to_create_tab() - # Ask user if he really wants to remove instances - dialog = QtWidgets.QMessageBox(self) - dialog.setIcon(QtWidgets.QMessageBox.Question) - dialog.setWindowTitle("Are you sure?") - if len(instances) > 1: - msg = ( - "Do you really want to remove {} instances?" - ).format(len(instances)) - else: - msg = ( - "Do you really want to remove the instance?" - ) - dialog.setText(msg) - dialog.setStandardButtons( - QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel - ) - dialog.setDefaultButton(QtWidgets.QMessageBox.Ok) - dialog.setEscapeButton(QtWidgets.QMessageBox.Cancel) - dialog.exec_() - # Skip if OK was not clicked - if dialog.result() == QtWidgets.QMessageBox.Ok: - self.controller.remove_instances(instances) - - def _on_change_view_clicked(self): - self._change_view_type() + def _go_to_create_tab(self): + self._tabs_widget.set_current_tab("create") def _set_publish_visibility(self, visible): if visible: widget = self.publish_frame publish_frame_visible = True else: - widget = self.subset_frame + widget = self._create_overview_widget publish_frame_visible = False self.content_stacked_layout.setCurrentWidget(widget) self._set_publish_frame_visible(publish_frame_visible) @@ -381,79 +241,27 @@ class PublisherWindow(QtWidgets.QDialog): self.creator_window.close() def _on_reset_clicked(self): - self.controller.reset() + self._controller.reset() def _on_stop_clicked(self): - self.controller.stop_publish() + self._controller.stop_publish() def _set_publish_comment(self): - if self.controller.publish_comment_is_set: + if self._controller.publish_comment_is_set: return comment = self.comment_input.text() - self.controller.set_comment(comment) + self._controller.set_comment(comment) def _on_validate_clicked(self): self._set_publish_comment() self._set_publish_visibility(True) - self.controller.validate() + self._controller.validate() def _on_publish_clicked(self): self._set_publish_comment() self._set_publish_visibility(True) - self.controller.publish() - - def _refresh_instances(self): - if self._refreshing_instances: - return - - self._refreshing_instances = True - - for idx in range(self.subset_views_layout.count()): - widget = self.subset_views_layout.widget(idx) - widget.set_refreshed(False) - - view = self.subset_views_layout.currentWidget() - view.refresh() - view.set_refreshed(True) - - self._refreshing_instances = False - - # Force to change instance and refresh details - self._on_subset_change() - - def _on_instances_refresh(self): - self._refresh_instances() - - self._validate_create_instances() - - context_title = self.controller.get_context_title() - self.set_context_label(context_title) - - # Give a change to process Resize Request - QtWidgets.QApplication.processEvents() - # Trigger update geometry of - widget = self.subset_views_layout.currentWidget() - widget.updateGeometry() - - def _on_subset_change(self, *_args): - # Ignore changes if in middle of refreshing - if self._refreshing_instances: - return - - instances, context_selected = self.get_selected_items() - - # Disable delete button if nothing is selected - self.delete_btn.setEnabled(len(instances) > 0) - - self.subset_attributes_widget.set_current_instances( - instances, context_selected - ) - - def _on_active_changed(self): - if self._refreshing_instances: - return - self._validate_create_instances() + self._controller.publish() def _set_footer_enabled(self, enabled): self.comment_input.setEnabled(enabled) @@ -467,30 +275,9 @@ class PublisherWindow(QtWidgets.QDialog): self.validate_btn.setEnabled(enabled) self.publish_btn.setEnabled(enabled) - def _validate_create_instances(self): - if not self.controller.host_is_valid: - self._set_footer_enabled(True) - return - - all_valid = None - for instance in self.controller.instances: - if not instance["active"]: - continue - - if not instance.has_valid_context: - all_valid = False - break - - if all_valid is None: - all_valid = True - - self._set_footer_enabled(bool(all_valid)) - def _on_publish_reset(self): self._set_publish_visibility(False) - self.subset_content_widget.setEnabled(self.controller.host_is_valid) - self._set_footer_enabled(False) def _on_publish_start(self): @@ -505,19 +292,44 @@ class PublisherWindow(QtWidgets.QDialog): def _on_publish_stop(self): self.reset_btn.setEnabled(True) self.stop_btn.setEnabled(False) - validate_enabled = not self.controller.publish_has_crashed - publish_enabled = not self.controller.publish_has_crashed + validate_enabled = not self._controller.publish_has_crashed + publish_enabled = not self._controller.publish_has_crashed if validate_enabled: - validate_enabled = not self.controller.publish_has_validated + validate_enabled = not self._controller.publish_has_validated if publish_enabled: if ( - self.controller.publish_has_validated - and self.controller.publish_has_validation_errors + self._controller.publish_has_validated + and self._controller.publish_has_validation_errors ): publish_enabled = False else: - publish_enabled = not self.controller.publish_has_finished + publish_enabled = not self._controller.publish_has_finished self.validate_btn.setEnabled(validate_enabled) self.publish_btn.setEnabled(publish_enabled) + + def _validate_create_instances(self): + if not self._controller.host_is_valid: + self._set_footer_enabled(True) + return + + all_valid = None + for instance in self._controller.instances: + if not instance["active"]: + continue + + if not instance.has_valid_context: + all_valid = False + break + + if all_valid is None: + all_valid = True + + self._set_footer_enabled(bool(all_valid)) + + def _on_instances_refresh(self): + self._validate_create_instances() + + context_title = self.controller.get_context_title() + self.set_context_label(context_title) diff --git a/openpype/tools/traypublisher/window.py b/openpype/tools/traypublisher/window.py index 930c27ca9c..128c0fef11 100644 --- a/openpype/tools/traypublisher/window.py +++ b/openpype/tools/traypublisher/window.py @@ -244,7 +244,7 @@ class TrayPublishWindow(PublisherWindow): self.reset() if not self.controller.instances: - self._on_create_clicked() + self._go_to_create_tab() def _on_tray_publish_save(self): self.controller.save_changes() From 05372493a3fcb21903ddc12a310ec258b6c9e20c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 17:45:35 +0200 Subject: [PATCH 049/275] added create widget to overview widget --- .../tools/publisher/widgets/assets_widget.py | 10 +- .../tools/publisher/widgets/create_widget.py | 986 ++++++++++++++++++ .../publisher/widgets/overview_widget.py | 74 +- .../tools/publisher/widgets/tasks_widget.py | 6 +- 4 files changed, 1045 insertions(+), 31 deletions(-) create mode 100644 openpype/tools/publisher/widgets/create_widget.py diff --git a/openpype/tools/publisher/widgets/assets_widget.py b/openpype/tools/publisher/widgets/assets_widget.py index 46fdcc6526..7a77c9e898 100644 --- a/openpype/tools/publisher/widgets/assets_widget.py +++ b/openpype/tools/publisher/widgets/assets_widget.py @@ -13,13 +13,13 @@ from openpype.tools.utils.assets_widget import ( ) -class CreateDialogAssetsWidget(SingleSelectAssetsWidget): +class CreateWidgetAssetsWidget(SingleSelectAssetsWidget): current_context_required = QtCore.Signal() header_height_changed = QtCore.Signal(int) def __init__(self, controller, parent): self._controller = controller - super(CreateDialogAssetsWidget, self).__init__(None, parent) + super(CreateWidgetAssetsWidget, self).__init__(None, parent) self.set_refresh_btn_visibility(False) self.set_current_asset_btn_visibility(False) @@ -42,11 +42,11 @@ class CreateDialogAssetsWidget(SingleSelectAssetsWidget): self.header_height_changed.emit(height) def resizeEvent(self, event): - super(CreateDialogAssetsWidget, self).resizeEvent(event) + super(CreateWidgetAssetsWidget, self).resizeEvent(event) self._check_header_height() def showEvent(self, event): - super(CreateDialogAssetsWidget, self).showEvent(event) + super(CreateWidgetAssetsWidget, self).showEvent(event) self._check_header_height() def _on_current_asset_click(self): @@ -63,7 +63,7 @@ class CreateDialogAssetsWidget(SingleSelectAssetsWidget): self.select_asset(self._last_selection) def _select_indexes(self, *args, **kwargs): - super(CreateDialogAssetsWidget, self)._select_indexes(*args, **kwargs) + super(CreateWidgetAssetsWidget, self)._select_indexes(*args, **kwargs) if self._enabled: return self._last_selection = self.get_selected_asset_id() diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py new file mode 100644 index 0000000000..a0b3db0409 --- /dev/null +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -0,0 +1,986 @@ +import sys +import re +import traceback +import copy + +import qtawesome +try: + import commonmark +except Exception: + commonmark = None +from Qt import QtWidgets, QtCore, QtGui + +from openpype.client import get_asset_by_name, get_subsets +from openpype.pipeline.create import ( + CreatorError, + SUBSET_NAME_ALLOWED_SYMBOLS, + TaskNotSetError, +) +from openpype.tools.utils import ( + ErrorMessageBox, + MessageOverlayObject, + ClickableFrame, +) + +from .widgets import IconValuePixmapLabel +from .assets_widget import CreateWidgetAssetsWidget +from .tasks_widget import CreateWidgetTasksWidget +from .precreate_widget import PreCreateWidget +from ..constants import ( + VARIANT_TOOLTIP, + CREATOR_IDENTIFIER_ROLE, + FAMILY_ROLE +) + +SEPARATORS = ("---separator---", "---") + + +class VariantInputsWidget(QtWidgets.QWidget): + resized = QtCore.Signal() + + def resizeEvent(self, event): + super(VariantInputsWidget, self).resizeEvent(event) + self.resized.emit() + + +class CreateErrorMessageBox(ErrorMessageBox): + def __init__( + self, + creator_label, + subset_name, + asset_name, + exc_msg, + formatted_traceback, + parent + ): + self._creator_label = creator_label + self._subset_name = subset_name + self._asset_name = asset_name + self._exc_msg = exc_msg + self._formatted_traceback = formatted_traceback + super(CreateErrorMessageBox, self).__init__("Creation failed", parent) + + def _create_top_widget(self, parent_widget): + label_widget = QtWidgets.QLabel(parent_widget) + label_widget.setText( + "Failed to create" + ) + return label_widget + + def _get_report_data(self): + report_message = ( + "{creator}: Failed to create Subset: \"{subset}\"" + " in Asset: \"{asset}\"" + "\n\nError: {message}" + ).format( + creator=self._creator_label, + subset=self._subset_name, + asset=self._asset_name, + message=self._exc_msg, + ) + if self._formatted_traceback: + report_message += "\n\n{}".format(self._formatted_traceback) + return [report_message] + + def _create_content(self, content_layout): + item_name_template = ( + "Creator: {}
" + "Subset: {}
" + "Asset: {}
" + ) + exc_msg_template = "{}" + + line = self._create_line() + content_layout.addWidget(line) + + item_name_widget = QtWidgets.QLabel(self) + item_name_widget.setText( + item_name_template.format( + self._creator_label, self._subset_name, self._asset_name + ) + ) + content_layout.addWidget(item_name_widget) + + message_label_widget = QtWidgets.QLabel(self) + message_label_widget.setText( + exc_msg_template.format(self.convert_text_for_html(self._exc_msg)) + ) + content_layout.addWidget(message_label_widget) + + if self._formatted_traceback: + line_widget = self._create_line() + tb_widget = self._create_traceback_widget( + self._formatted_traceback + ) + content_layout.addWidget(line_widget) + content_layout.addWidget(tb_widget) + + +# TODO add creator identifier/label to details +class CreatorShortDescWidget(QtWidgets.QWidget): + height_changed = QtCore.Signal(int) + + def __init__(self, parent=None): + super(CreatorShortDescWidget, self).__init__(parent=parent) + + # --- Short description widget --- + icon_widget = IconValuePixmapLabel(None, self) + icon_widget.setObjectName("FamilyIconLabel") + + # --- Short description inputs --- + short_desc_input_widget = QtWidgets.QWidget(self) + + family_label = QtWidgets.QLabel(short_desc_input_widget) + family_label.setAlignment( + QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft + ) + + description_label = QtWidgets.QLabel(short_desc_input_widget) + description_label.setAlignment( + QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft + ) + + short_desc_input_layout = QtWidgets.QVBoxLayout( + short_desc_input_widget + ) + short_desc_input_layout.setSpacing(0) + short_desc_input_layout.addWidget(family_label) + short_desc_input_layout.addWidget(description_label) + # -------------------------------- + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(icon_widget, 0) + layout.addWidget(short_desc_input_widget, 1) + # -------------------------------- + + self._icon_widget = icon_widget + self._family_label = family_label + self._description_label = description_label + + self._last_height = None + + def _check_height_change(self): + height = self.height() + if height != self._last_height: + self._last_height = height + self.height_changed.emit(height) + + def showEvent(self, event): + super(CreatorShortDescWidget, self).showEvent(event) + self._check_height_change() + + def resizeEvent(self, event): + super(CreatorShortDescWidget, self).resizeEvent(event) + self._check_height_change() + + def set_plugin(self, plugin=None): + if not plugin: + self._icon_widget.set_icon_def(None) + self._family_label.setText("") + self._description_label.setText("") + return + + plugin_icon = plugin.get_icon() + description = plugin.get_description() or "" + + self._icon_widget.set_icon_def(plugin_icon) + self._family_label.setText("{}".format(plugin.family)) + self._family_label.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + self._description_label.setText(description) + + +class HelpButton(ClickableFrame): + resized = QtCore.Signal(int) + question_mark_icon_name = "fa.question" + help_icon_name = "fa.question-circle" + hide_icon_name = "fa.angle-left" + + def __init__(self, *args, **kwargs): + super(HelpButton, self).__init__(*args, **kwargs) + self.setObjectName("CreateDialogHelpButton") + + question_mark_label = QtWidgets.QLabel(self) + help_widget = QtWidgets.QWidget(self) + + help_question = QtWidgets.QLabel(help_widget) + help_label = QtWidgets.QLabel("Help", help_widget) + hide_icon = QtWidgets.QLabel(help_widget) + + help_layout = QtWidgets.QHBoxLayout(help_widget) + help_layout.setContentsMargins(0, 0, 5, 0) + help_layout.addWidget(help_question, 0) + help_layout.addWidget(help_label, 0) + help_layout.addStretch(1) + help_layout.addWidget(hide_icon, 0) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + layout.addWidget(question_mark_label, 0) + layout.addWidget(help_widget, 1) + + help_widget.setVisible(False) + + self._question_mark_label = question_mark_label + self._help_widget = help_widget + self._help_question = help_question + self._hide_icon = hide_icon + + self._expanded = None + self.set_expanded() + + def set_expanded(self, expanded=None): + if self._expanded is expanded: + if expanded is not None: + return + expanded = False + self._expanded = expanded + self._help_widget.setVisible(expanded) + self._update_content() + + def _update_content(self): + width = self.get_icon_width() + if self._expanded: + question_mark_pix = QtGui.QPixmap(width, width) + question_mark_pix.fill(QtCore.Qt.transparent) + + else: + question_mark_icon = qtawesome.icon( + self.question_mark_icon_name, color=QtCore.Qt.white + ) + question_mark_pix = question_mark_icon.pixmap(width, width) + + hide_icon = qtawesome.icon( + self.hide_icon_name, color=QtCore.Qt.white + ) + help_question_icon = qtawesome.icon( + self.help_icon_name, color=QtCore.Qt.white + ) + self._question_mark_label.setPixmap(question_mark_pix) + self._question_mark_label.setMaximumWidth(width) + self._hide_icon.setPixmap(hide_icon.pixmap(width, width)) + self._help_question.setPixmap(help_question_icon.pixmap(width, width)) + + def get_icon_width(self): + metrics = self.fontMetrics() + return metrics.height() + + def set_pos_and_size(self, pos_x, pos_y, width, height): + update_icon = self.height() != height + self.move(pos_x, pos_y) + self.resize(width, height) + + if update_icon: + self._update_content() + self.updateGeometry() + + def showEvent(self, event): + super(HelpButton, self).showEvent(event) + self.resized.emit(self.height()) + + def resizeEvent(self, event): + super(HelpButton, self).resizeEvent(event) + self.resized.emit(self.height()) + + +class CreateWidget(QtWidgets.QWidget): + def __init__(self, controller, parent=None): + super(CreateWidget, self).__init__(parent) + + self.setWindowTitle("Create new instance") + + self.controller = controller + + self._asset_name = self.dbcon.Session.get("AVALON_ASSET") + self._task_name = self.dbcon.Session.get("AVALON_TASK") + + self._asset_doc = None + self._subset_names = None + self._selected_creator = None + + self._prereq_available = False + + self._message_dialog = None + + name_pattern = "^[{}]*$".format(SUBSET_NAME_ALLOWED_SYMBOLS) + self._name_pattern = name_pattern + self._compiled_name_pattern = re.compile(name_pattern) + + overlay_object = MessageOverlayObject(self) + + context_widget = QtWidgets.QWidget(self) + + assets_widget = CreateWidgetAssetsWidget(controller, context_widget) + tasks_widget = CreateWidgetTasksWidget(controller, context_widget) + + context_layout = QtWidgets.QVBoxLayout(context_widget) + context_layout.setContentsMargins(0, 0, 0, 0) + context_layout.setSpacing(0) + context_layout.addWidget(assets_widget, 2) + context_layout.addWidget(tasks_widget, 1) + + # --- Creators view --- + creators_header_widget = QtWidgets.QWidget(self) + header_label_widget = QtWidgets.QLabel( + "Choose family:", creators_header_widget + ) + creators_header_layout = QtWidgets.QHBoxLayout(creators_header_widget) + creators_header_layout.setContentsMargins(0, 0, 0, 0) + creators_header_layout.addWidget(header_label_widget, 1) + + creators_view = QtWidgets.QListView(self) + creators_model = QtGui.QStandardItemModel() + creators_sort_model = QtCore.QSortFilterProxyModel() + creators_sort_model.setSourceModel(creators_model) + creators_view.setModel(creators_sort_model) + + variant_widget = VariantInputsWidget(self) + + variant_input = QtWidgets.QLineEdit(variant_widget) + variant_input.setObjectName("VariantInput") + variant_input.setToolTip(VARIANT_TOOLTIP) + + variant_hints_btn = QtWidgets.QToolButton(variant_widget) + variant_hints_btn.setArrowType(QtCore.Qt.DownArrow) + variant_hints_btn.setIconSize(QtCore.QSize(12, 12)) + + variant_hints_menu = QtWidgets.QMenu(variant_widget) + variant_hints_group = QtWidgets.QActionGroup(variant_hints_menu) + + variant_layout = QtWidgets.QHBoxLayout(variant_widget) + variant_layout.setContentsMargins(0, 0, 0, 0) + variant_layout.setSpacing(0) + variant_layout.addWidget(variant_input, 1) + variant_layout.addWidget(variant_hints_btn, 0, QtCore.Qt.AlignVCenter) + + subset_name_input = QtWidgets.QLineEdit(self) + subset_name_input.setEnabled(False) + + form_layout = QtWidgets.QFormLayout() + form_layout.addRow("Variant:", variant_widget) + form_layout.addRow("Subset:", subset_name_input) + + mid_widget = QtWidgets.QWidget(self) + mid_layout = QtWidgets.QVBoxLayout(mid_widget) + mid_layout.setContentsMargins(0, 0, 0, 0) + mid_layout.addWidget(creators_header_widget, 0) + mid_layout.addWidget(creators_view, 1) + mid_layout.addLayout(form_layout, 0) + # ------------ + + # --- Creator short info and attr defs --- + creator_attrs_widget = QtWidgets.QWidget(self) + + creator_short_desc_widget = CreatorShortDescWidget( + creator_attrs_widget + ) + + attr_separator_widget = QtWidgets.QWidget(self) + attr_separator_widget.setObjectName("Separator") + attr_separator_widget.setMinimumHeight(1) + attr_separator_widget.setMaximumHeight(1) + + # Precreate attributes widget + pre_create_widget = PreCreateWidget(creator_attrs_widget) + + # Create button + create_btn_wrapper = QtWidgets.QWidget(creator_attrs_widget) + create_btn = QtWidgets.QPushButton("Create", create_btn_wrapper) + create_btn.setEnabled(False) + + create_btn_wrap_layout = QtWidgets.QHBoxLayout(create_btn_wrapper) + create_btn_wrap_layout.setContentsMargins(0, 0, 0, 0) + create_btn_wrap_layout.addStretch(1) + create_btn_wrap_layout.addWidget(create_btn, 0) + + creator_attrs_layout = QtWidgets.QVBoxLayout(creator_attrs_widget) + creator_attrs_layout.setContentsMargins(0, 0, 0, 0) + creator_attrs_layout.addWidget(creator_short_desc_widget, 0) + creator_attrs_layout.addWidget(attr_separator_widget, 0) + creator_attrs_layout.addWidget(pre_create_widget, 1) + creator_attrs_layout.addWidget(create_btn_wrapper, 0) + # ------------------------------------- + + # --- Detailed information about creator --- + # Detailed description of creator + detail_description_widget = QtWidgets.QWidget(self) + + detail_placoholder_widget = QtWidgets.QWidget( + detail_description_widget + ) + detail_placoholder_widget.setAttribute( + QtCore.Qt.WA_TranslucentBackground + ) + + detail_description_input = QtWidgets.QTextEdit( + detail_description_widget + ) + detail_description_input.setObjectName("CreatorDetailedDescription") + detail_description_input.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction + ) + + detail_description_layout = QtWidgets.QVBoxLayout( + detail_description_widget + ) + detail_description_layout.setContentsMargins(0, 0, 0, 0) + detail_description_layout.setSpacing(0) + detail_description_layout.addWidget(detail_placoholder_widget, 0) + detail_description_layout.addWidget(detail_description_input, 1) + + detail_description_widget.setVisible(False) + + # ------------------------------------------- + splitter_widget = QtWidgets.QSplitter(self) + splitter_widget.addWidget(context_widget) + splitter_widget.addWidget(mid_widget) + splitter_widget.addWidget(creator_attrs_widget) + splitter_widget.addWidget(detail_description_widget) + splitter_widget.setStretchFactor(0, 1) + splitter_widget.setStretchFactor(1, 1) + splitter_widget.setStretchFactor(2, 1) + splitter_widget.setStretchFactor(3, 1) + + layout = QtWidgets.QHBoxLayout(self) + layout.addWidget(splitter_widget, 1) + + prereq_timer = QtCore.QTimer() + prereq_timer.setInterval(50) + prereq_timer.setSingleShot(True) + + prereq_timer.timeout.connect(self._invalidate_prereq) + + assets_widget.header_height_changed.connect( + self._on_asset_filter_height_change + ) + + create_btn.clicked.connect(self._on_create) + variant_widget.resized.connect(self._on_variant_widget_resize) + variant_input.returnPressed.connect(self._on_create) + variant_input.textChanged.connect(self._on_variant_change) + creators_view.selectionModel().currentChanged.connect( + self._on_creator_item_change + ) + variant_hints_btn.clicked.connect(self._on_variant_btn_click) + variant_hints_menu.triggered.connect(self._on_variant_action) + assets_widget.selection_changed.connect(self._on_asset_change) + assets_widget.current_context_required.connect( + self._on_current_session_context_request + ) + tasks_widget.task_changed.connect(self._on_task_change) + creator_short_desc_widget.height_changed.connect( + self._on_description_height_change + ) + + controller.add_plugins_refresh_callback(self._on_plugins_refresh) + + self._overlay_object = overlay_object + + self._splitter_widget = splitter_widget + + self._context_widget = context_widget + self._assets_widget = assets_widget + self._tasks_widget = tasks_widget + + self.subset_name_input = subset_name_input + + self.variant_input = variant_input + self.variant_hints_btn = variant_hints_btn + self.variant_hints_menu = variant_hints_menu + self.variant_hints_group = variant_hints_group + + self._creators_header_widget = creators_header_widget + self._creators_model = creators_model + self._creators_sort_model = creators_sort_model + self._creators_view = creators_view + self._create_btn = create_btn + + self._creator_short_desc_widget = creator_short_desc_widget + self._pre_create_widget = pre_create_widget + self._attr_separator_widget = attr_separator_widget + + self._detail_placoholder_widget = detail_placoholder_widget + self._detail_description_widget = detail_description_widget + self._detail_description_input = detail_description_input + + self._prereq_timer = prereq_timer + self._first_show = True + + def _emit_message(self, message): + self._overlay_object.add_message(message) + + def _context_change_is_enabled(self): + return self._context_widget.isEnabled() + + def _get_asset_name(self): + asset_name = None + if self._context_change_is_enabled(): + asset_name = self._assets_widget.get_selected_asset_name() + + if asset_name is None: + asset_name = self._asset_name + return asset_name + + def _get_task_name(self): + task_name = None + if self._context_change_is_enabled(): + # Don't use selection of task if asset is not set + asset_name = self._assets_widget.get_selected_asset_name() + if asset_name: + task_name = self._tasks_widget.get_selected_task_name() + + if not task_name: + task_name = self._task_name + return task_name + + @property + def dbcon(self): + return self.controller.dbcon + + def _set_context_enabled(self, enabled): + self._assets_widget.set_enabled(enabled) + self._tasks_widget.set_enabled(enabled) + check_prereq = self._context_widget.isEnabled() != enabled + self._context_widget.setEnabled(enabled) + if check_prereq: + self._invalidate_prereq() + + def refresh(self): + # Get context before refresh to keep selection of asset and + # task widgets + asset_name = self._get_asset_name() + task_name = self._get_task_name() + + self._prereq_available = False + + # Disable context widget so refresh of asset will use context asset + # name + self._set_context_enabled(False) + + self._assets_widget.refresh() + + # Refresh data before update of creators + self._refresh_asset() + # Then refresh creators which may trigger callbacks using refreshed + # data + self._refresh_creators() + + self._assets_widget.set_current_asset_name(self._asset_name) + self._assets_widget.select_asset_by_name(asset_name) + self._tasks_widget.set_asset_name(asset_name) + self._tasks_widget.select_task_name(task_name) + + self._invalidate_prereq_deffered() + + def _invalidate_prereq_deffered(self): + self._prereq_timer.start() + + def _on_asset_filter_height_change(self, height): + self._creators_header_widget.setMinimumHeight(height) + self._creators_header_widget.setMaximumHeight(height) + + def _invalidate_prereq(self): + prereq_available = True + creator_btn_tooltips = [] + + available_creators = self._creators_model.rowCount() > 0 + if available_creators != self._creators_view.isEnabled(): + self._creators_view.setEnabled(available_creators) + + if not available_creators: + prereq_available = False + creator_btn_tooltips.append("Creator is not selected") + + if self._context_change_is_enabled() and self._asset_doc is None: + # QUESTION how to handle invalid asset? + prereq_available = False + creator_btn_tooltips.append("Context is not selected") + + if prereq_available != self._prereq_available: + self._prereq_available = prereq_available + + self._create_btn.setEnabled(prereq_available) + + self.variant_input.setEnabled(prereq_available) + self.variant_hints_btn.setEnabled(prereq_available) + + tooltip = "" + if creator_btn_tooltips: + tooltip = "\n".join(creator_btn_tooltips) + self._create_btn.setToolTip(tooltip) + + self._on_variant_change() + + def _refresh_asset(self): + asset_name = self._get_asset_name() + + # Skip if asset did not change + if self._asset_doc and self._asset_doc["name"] == asset_name: + return + + # Make sure `_asset_doc` and `_subset_names` variables are reset + self._asset_doc = None + self._subset_names = None + if asset_name is None: + return + + project_name = self.dbcon.active_project() + asset_doc = get_asset_by_name(project_name, asset_name) + self._asset_doc = asset_doc + + if asset_doc: + asset_id = asset_doc["_id"] + subset_docs = get_subsets( + project_name, asset_ids=[asset_id], fields=["name"] + ) + self._subset_names = { + subset_doc["name"] + for subset_doc in subset_docs + } + + if not asset_doc: + self.subset_name_input.setText("< Asset is not set >") + + def _refresh_creators(self): + # Refresh creators and add their families to list + existing_items = {} + old_creators = set() + for row in range(self._creators_model.rowCount()): + item = self._creators_model.item(row, 0) + identifier = item.data(CREATOR_IDENTIFIER_ROLE) + existing_items[identifier] = item + old_creators.add(identifier) + + # Add new families + new_creators = set() + for identifier, creator in self.controller.manual_creators.items(): + # TODO add details about creator + new_creators.add(identifier) + if identifier in existing_items: + item = existing_items[identifier] + else: + item = QtGui.QStandardItem() + item.setFlags( + QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + ) + self._creators_model.appendRow(item) + + label = creator.label or identifier + item.setData(label, QtCore.Qt.DisplayRole) + item.setData(identifier, CREATOR_IDENTIFIER_ROLE) + item.setData(creator.family, FAMILY_ROLE) + + # Remove families that are no more available + for identifier in (old_creators - new_creators): + item = existing_items[identifier] + self._creators_model.takeRow(item.row()) + + if self._creators_model.rowCount() < 1: + return + + self._creators_sort_model.sort(0) + # Make sure there is a selection + indexes = self._creators_view.selectedIndexes() + if not indexes: + index = self._creators_sort_model.index(0, 0) + self._creators_view.setCurrentIndex(index) + else: + index = indexes[0] + + identifier = index.data(CREATOR_IDENTIFIER_ROLE) + + self._set_creator_by_identifier(identifier) + + def _on_plugins_refresh(self): + # Trigger refresh only if is visible + self.refresh() + + def _on_asset_change(self): + self._refresh_asset() + + asset_name = self._assets_widget.get_selected_asset_name() + self._tasks_widget.set_asset_name(asset_name) + if self._context_change_is_enabled(): + self._invalidate_prereq_deffered() + + def _on_task_change(self): + if self._context_change_is_enabled(): + self._invalidate_prereq_deffered() + + def _on_current_session_context_request(self): + self._assets_widget.set_current_session_asset() + if self._task_name: + self._tasks_widget.select_task_name(self._task_name) + + def _on_description_height_change(self): + # Use separator's 'y' position as height + height = self._attr_separator_widget.y() + self._detail_placoholder_widget.setMinimumHeight(height) + self._detail_placoholder_widget.setMaximumHeight(height) + + def _on_creator_item_change(self, new_index, _old_index): + identifier = None + if new_index.isValid(): + identifier = new_index.data(CREATOR_IDENTIFIER_ROLE) + self._set_creator_by_identifier(identifier) + + def _set_creator_detailed_text(self, creator): + if not creator: + self._detail_description_input.setPlainText("") + return + detailed_description = creator.get_detail_description() or "" + if commonmark: + html = commonmark.commonmark(detailed_description) + self._detail_description_input.setHtml(html) + else: + self._detail_description_input.setMarkdown(detailed_description) + + def _set_creator_by_identifier(self, identifier): + creator = self.controller.manual_creators.get(identifier) + self._set_creator(creator) + + def _set_creator(self, creator): + self._creator_short_desc_widget.set_plugin(creator) + self._set_creator_detailed_text(creator) + self._pre_create_widget.set_plugin(creator) + + self._selected_creator = creator + + if not creator: + self._set_context_enabled(False) + return + + if ( + creator.create_allow_context_change + != self._context_change_is_enabled() + ): + self._set_context_enabled(creator.create_allow_context_change) + self._refresh_asset() + + default_variants = creator.get_default_variants() + if not default_variants: + default_variants = ["Main"] + + default_variant = creator.get_default_variant() + if not default_variant: + default_variant = default_variants[0] + + for action in tuple(self.variant_hints_menu.actions()): + self.variant_hints_menu.removeAction(action) + action.deleteLater() + + for variant in default_variants: + if variant in SEPARATORS: + self.variant_hints_menu.addSeparator() + elif variant: + self.variant_hints_menu.addAction(variant) + + variant_text = default_variant or "Main" + # Make sure subset name is updated to new plugin + if variant_text == self.variant_input.text(): + self._on_variant_change() + else: + self.variant_input.setText(variant_text) + + def _on_variant_widget_resize(self): + self.variant_hints_btn.setFixedHeight(self.variant_input.height()) + + def _on_variant_btn_click(self): + pos = self.variant_hints_btn.rect().bottomLeft() + point = self.variant_hints_btn.mapToGlobal(pos) + self.variant_hints_menu.popup(point) + + def _on_variant_action(self, action): + value = action.text() + if self.variant_input.text() != value: + self.variant_input.setText(value) + + def _on_variant_change(self, variant_value=None): + if not self._prereq_available: + return + + # This should probably never happen? + if not self._selected_creator: + if self.subset_name_input.text(): + self.subset_name_input.setText("") + return + + if variant_value is None: + variant_value = self.variant_input.text() + + if not self._compiled_name_pattern.match(variant_value): + self._create_btn.setEnabled(False) + self._set_variant_state_property("invalid") + self.subset_name_input.setText("< Invalid variant >") + return + + if not self._context_change_is_enabled(): + self._create_btn.setEnabled(True) + self._set_variant_state_property("") + self.subset_name_input.setText("< Valid variant >") + return + + project_name = self.controller.project_name + task_name = self._get_task_name() + + asset_doc = copy.deepcopy(self._asset_doc) + # Calculate subset name with Creator plugin + try: + subset_name = self._selected_creator.get_subset_name( + variant_value, task_name, asset_doc, project_name + ) + except TaskNotSetError: + self._create_btn.setEnabled(False) + self._set_variant_state_property("invalid") + self.subset_name_input.setText("< Missing task >") + return + + self.subset_name_input.setText(subset_name) + + self._create_btn.setEnabled(True) + self._validate_subset_name(subset_name, variant_value) + + def _validate_subset_name(self, subset_name, variant_value): + # Get all subsets of the current asset + if self._subset_names: + existing_subset_names = set(self._subset_names) + else: + existing_subset_names = set() + existing_subset_names_low = set( + _name.lower() + for _name in existing_subset_names + ) + + # Replace + compare_regex = re.compile(re.sub( + variant_value, "(.+)", subset_name, flags=re.IGNORECASE + )) + variant_hints = set() + if variant_value: + for _name in existing_subset_names: + _result = compare_regex.search(_name) + if _result: + variant_hints |= set(_result.groups()) + + # Remove previous hints from menu + for action in tuple(self.variant_hints_group.actions()): + self.variant_hints_group.removeAction(action) + self.variant_hints_menu.removeAction(action) + action.deleteLater() + + # Add separator if there are hints and menu already has actions + if variant_hints and self.variant_hints_menu.actions(): + self.variant_hints_menu.addSeparator() + + # Add hints to actions + for variant_hint in variant_hints: + action = self.variant_hints_menu.addAction(variant_hint) + self.variant_hints_group.addAction(action) + + # Indicate subset existence + if not variant_value: + property_value = "empty" + + elif subset_name.lower() in existing_subset_names_low: + # validate existence of subset name with lowered text + # - "renderMain" vs. "rendermain" mean same path item for + # windows + property_value = "exists" + else: + property_value = "new" + + self._set_variant_state_property(property_value) + + variant_is_valid = variant_value.strip() != "" + if variant_is_valid != self._create_btn.isEnabled(): + self._create_btn.setEnabled(variant_is_valid) + + def _set_variant_state_property(self, state): + current_value = self.variant_input.property("state") + if current_value != state: + self.variant_input.setProperty("state", state) + self.variant_input.style().polish(self.variant_input) + + def _on_first_show(self): + width = self.width() + part = int(width / 7) + self._splitter_widget.setSizes( + [part * 2, part * 2, width - (part * 4)] + ) + + def showEvent(self, event): + super(CreateWidget, self).showEvent(event) + if self._first_show: + self._first_show = False + self._on_first_show() + + def _on_create(self): + indexes = self._creators_view.selectedIndexes() + if not indexes or len(indexes) > 1: + return + + if not self._create_btn.isEnabled(): + return + + index = indexes[0] + creator_label = index.data(QtCore.Qt.DisplayRole) + creator_identifier = index.data(CREATOR_IDENTIFIER_ROLE) + family = index.data(FAMILY_ROLE) + variant = self.variant_input.text() + # Care about subset name only if context change is enabled + subset_name = None + asset_name = None + task_name = None + if self._context_change_is_enabled(): + subset_name = self.subset_name_input.text() + asset_name = self._get_asset_name() + task_name = self._get_task_name() + + pre_create_data = self._pre_create_widget.current_value() + # Where to define these data? + # - what data show be stored? + instance_data = { + "asset": asset_name, + "task": task_name, + "variant": variant, + "family": family + } + + error_msg = None + formatted_traceback = None + try: + self.controller.create( + creator_identifier, + subset_name, + instance_data, + pre_create_data + ) + + except CreatorError as exc: + error_msg = str(exc) + + # Use bare except because some hosts raise their exceptions that + # do not inherit from python's `BaseException` + except: + exc_type, exc_value, exc_traceback = sys.exc_info() + formatted_traceback = "".join(traceback.format_exception( + exc_type, exc_value, exc_traceback + )) + error_msg = str(exc_value) + + if error_msg is None: + self._set_creator(self._selected_creator) + self._emit_message("Creation finished...") + else: + box = CreateErrorMessageBox( + creator_label, + subset_name, + asset_name, + error_msg, + formatted_traceback, + parent=self + ) + box.show() + # Store dialog so is not garbage collected before is shown + self._message_dialog = box diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index abdd98ff7c..ddc976d458 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -10,6 +10,7 @@ from .widgets import ( RemoveInstanceBtn, ChangeViewBtn ) +from .create_widget import CreateWidget class CreateOverviewWidget(QtWidgets.QFrame): @@ -20,9 +21,13 @@ class CreateOverviewWidget(QtWidgets.QFrame): def __init__(self, controller, parent): super(CreateOverviewWidget, self).__init__(parent) - self._controller = controller self._refreshing_instances = False + self._controller = controller + create_widget = CreateWidget(controller, self) + + # --- Created Subsets/Instances --- + # Common widget for creation and overview subset_views_widget = BorderedLabelWidget( "Subsets to publish", self ) @@ -39,6 +44,7 @@ class CreateOverviewWidget(QtWidgets.QFrame): delete_btn = RemoveInstanceBtn(self) change_view_btn = ChangeViewBtn(self) + # --- Overview --- # Subset details widget subset_attributes_wrap = BorderedLabelWidget( "Publish options", self @@ -73,6 +79,7 @@ class CreateOverviewWidget(QtWidgets.QFrame): subset_content_widget = QtWidgets.QWidget(self) subset_content_layout = QtWidgets.QHBoxLayout(subset_content_widget) subset_content_layout.setContentsMargins(0, 0, 0, 0) + subset_content_layout.addWidget(create_widget, 7) subset_content_layout.addWidget(subset_views_widget, 3) subset_content_layout.addWidget(subset_attributes_wrap, 7) @@ -86,6 +93,7 @@ class CreateOverviewWidget(QtWidgets.QFrame): main_layout.setContentsMargins(marings) main_layout.addWidget(subset_content_widget, 1) + # --- Calbacks for instances/subsets view --- create_btn.clicked.connect(self._on_create_clicked) delete_btn.clicked.connect(self._on_delete_clicked) change_view_btn.clicked.connect(self._on_change_view_clicked) @@ -109,18 +117,38 @@ class CreateOverviewWidget(QtWidgets.QFrame): self._on_instance_context_change ) + # --- Controller callbacks --- controller.add_publish_reset_callback(self._on_publish_reset) controller.add_instances_refresh_callback(self._on_instances_refresh) - self.subset_content_widget = subset_content_widget + self._subset_content_widget = subset_content_widget - self.subset_view_cards = subset_view_cards - self.subset_list_view = subset_list_view - self.subset_views_layout = subset_views_layout + self._subset_view_cards = subset_view_cards + self._subset_list_view = subset_list_view + self._subset_views_layout = subset_views_layout - self.delete_btn = delete_btn + self._delete_btn = delete_btn - self.subset_attributes_widget = subset_attributes_widget + self._subset_attributes_widget = subset_attributes_widget + self._create_widget = create_widget + self._subset_attributes_wrap = subset_attributes_wrap + + # Start in create mode + self._current_state = "create" + subset_attributes_wrap.setVisible(False) + + def set_state(self, old_state, new_state): + if new_state == self._current_state: + return + + self._current_state = new_state + + self._create_widget.setVisible( + self._current_state == "create" + ) + self._subset_attributes_wrap.setVisible( + self._current_state == "publish" + ) def _on_create_clicked(self): """Pass signal to parent widget which should care about changing state. @@ -167,9 +195,9 @@ class CreateOverviewWidget(QtWidgets.QFrame): instances, context_selected = self.get_selected_items() # Disable delete button if nothing is selected - self.delete_btn.setEnabled(len(instances) > 0) + self._delete_btn.setEnabled(len(instances) > 0) - self.subset_attributes_widget.set_current_instances( + self._subset_attributes_widget.set_current_instances( instances, context_selected ) @@ -179,29 +207,29 @@ class CreateOverviewWidget(QtWidgets.QFrame): self.active_changed.emit() def _on_instance_context_change(self): - current_idx = self.subset_views_layout.currentIndex() - for idx in range(self.subset_views_layout.count()): + current_idx = self._subset_views_layout.currentIndex() + for idx in range(self._subset_views_layout.count()): if idx == current_idx: continue - widget = self.subset_views_layout.widget(idx) + widget = self._subset_views_layout.widget(idx) if widget.refreshed: widget.set_refreshed(False) - current_widget = self.subset_views_layout.widget(current_idx) + current_widget = self._subset_views_layout.widget(current_idx) current_widget.refresh_instance_states() self.instance_context_changed.emit() def get_selected_items(self): - view = self.subset_views_layout.currentWidget() + view = self._subset_views_layout.currentWidget() return view.get_selected_items() def _change_view_type(self): - idx = self.subset_views_layout.currentIndex() - new_idx = (idx + 1) % self.subset_views_layout.count() - self.subset_views_layout.setCurrentIndex(new_idx) + idx = self._subset_views_layout.currentIndex() + new_idx = (idx + 1) % self._subset_views_layout.count() + self._subset_views_layout.setCurrentIndex(new_idx) - new_view = self.subset_views_layout.currentWidget() + new_view = self._subset_views_layout.currentWidget() if not new_view.refreshed: new_view.refresh() new_view.set_refreshed(True) @@ -216,11 +244,11 @@ class CreateOverviewWidget(QtWidgets.QFrame): self._refreshing_instances = True - for idx in range(self.subset_views_layout.count()): - widget = self.subset_views_layout.widget(idx) + for idx in range(self._subset_views_layout.count()): + widget = self._subset_views_layout.widget(idx) widget.set_refreshed(False) - view = self.subset_views_layout.currentWidget() + view = self._subset_views_layout.currentWidget() view.refresh() view.set_refreshed(True) @@ -232,7 +260,7 @@ class CreateOverviewWidget(QtWidgets.QFrame): def _on_publish_reset(self): """Context in controller has been refreshed.""" - self.subset_content_widget.setEnabled(self._controller.host_is_valid) + self._subset_content_widget.setEnabled(self._controller.host_is_valid) def _on_instances_refresh(self): """Controller refreshed instances.""" @@ -242,5 +270,5 @@ class CreateOverviewWidget(QtWidgets.QFrame): # Give a change to process Resize Request QtWidgets.QApplication.processEvents() # Trigger update geometry of - widget = self.subset_views_layout.currentWidget() + widget = self._subset_views_layout.currentWidget() widget.updateGeometry() diff --git a/openpype/tools/publisher/widgets/tasks_widget.py b/openpype/tools/publisher/widgets/tasks_widget.py index aa239f6334..f31fffb9ea 100644 --- a/openpype/tools/publisher/widgets/tasks_widget.py +++ b/openpype/tools/publisher/widgets/tasks_widget.py @@ -141,10 +141,10 @@ class TasksModel(QtGui.QStandardItemModel): return super(TasksModel, self).headerData(section, orientation, role) -class CreateDialogTasksWidget(TasksWidget): +class CreateWidgetTasksWidget(TasksWidget): def __init__(self, controller, parent): self._controller = controller - super(CreateDialogTasksWidget, self).__init__(None, parent) + super(CreateWidgetTasksWidget, self).__init__(None, parent) self._enabled = None @@ -164,7 +164,7 @@ class CreateDialogTasksWidget(TasksWidget): self.task_changed.emit() def select_task_name(self, task_name): - super(CreateDialogTasksWidget, self).select_task_name(task_name) + super(CreateWidgetTasksWidget, self).select_task_name(task_name) if not self._enabled: current = self.get_selected_task_name() if current: From b6312fe3692850dc1a821e9c79d83425078d5ba7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 17:45:43 +0200 Subject: [PATCH 050/275] changed style of disable button --- openpype/style/style.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/style/style.css b/openpype/style/style.css index ab23dd621f..1d112fa575 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -868,6 +868,10 @@ PublisherTabBtn { padding: 0.5em 1em 0.5em 1em; } +PublisherTabBtn:disabled { + background: {color:bg-inputs}; +} + PublisherTabBtn:hover { background: {color:bg-buttons}; } From 22dab5ddefb2908d3eefcbd8a46cbb77a204ed4c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 17:46:20 +0200 Subject: [PATCH 051/275] removed created dialog --- openpype/tools/publisher/widgets/__init__.py | 3 - .../tools/publisher/widgets/create_dialog.py | 1222 ----------------- .../tools/publisher/widgets/tabs_widget.py | 9 + openpype/tools/publisher/window.py | 36 +- 4 files changed, 22 insertions(+), 1248 deletions(-) delete mode 100644 openpype/tools/publisher/widgets/create_dialog.py diff --git a/openpype/tools/publisher/widgets/__init__.py b/openpype/tools/publisher/widgets/__init__.py index 869f7adf9b..1d0ed0633b 100644 --- a/openpype/tools/publisher/widgets/__init__.py +++ b/openpype/tools/publisher/widgets/__init__.py @@ -10,7 +10,6 @@ from .widgets import ( PublishBtn, ) from .publish_widget import PublishFrame -from .create_dialog import CreateDialog from .tabs_widget import PublisherTabsWidget from .overview_widget import CreateOverviewWidget @@ -26,8 +25,6 @@ __all__ = ( "PublishFrame", - "CreateDialog", - "PublisherTabsWidget", "CreateOverviewWidget", ) diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py deleted file mode 100644 index 173df7d5c8..0000000000 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ /dev/null @@ -1,1222 +0,0 @@ -import sys -import re -import traceback -import copy - -import qtawesome -try: - import commonmark -except Exception: - commonmark = None -from Qt import QtWidgets, QtCore, QtGui - -from openpype.client import get_asset_by_name, get_subsets -from openpype.pipeline.create import ( - CreatorError, - SUBSET_NAME_ALLOWED_SYMBOLS, - TaskNotSetError, -) -from openpype.tools.utils import ( - ErrorMessageBox, - MessageOverlayObject, - ClickableFrame, -) - -from .widgets import IconValuePixmapLabel -from .assets_widget import CreateDialogAssetsWidget -from .tasks_widget import CreateDialogTasksWidget -from .precreate_widget import PreCreateWidget -from ..constants import ( - VARIANT_TOOLTIP, - CREATOR_IDENTIFIER_ROLE, - FAMILY_ROLE -) - -SEPARATORS = ("---separator---", "---") - - -class VariantInputsWidget(QtWidgets.QWidget): - resized = QtCore.Signal() - - def resizeEvent(self, event): - super(VariantInputsWidget, self).resizeEvent(event) - self.resized.emit() - - -class CreateErrorMessageBox(ErrorMessageBox): - def __init__( - self, - creator_label, - subset_name, - asset_name, - exc_msg, - formatted_traceback, - parent - ): - self._creator_label = creator_label - self._subset_name = subset_name - self._asset_name = asset_name - self._exc_msg = exc_msg - self._formatted_traceback = formatted_traceback - super(CreateErrorMessageBox, self).__init__("Creation failed", parent) - - def _create_top_widget(self, parent_widget): - label_widget = QtWidgets.QLabel(parent_widget) - label_widget.setText( - "Failed to create" - ) - return label_widget - - def _get_report_data(self): - report_message = ( - "{creator}: Failed to create Subset: \"{subset}\"" - " in Asset: \"{asset}\"" - "\n\nError: {message}" - ).format( - creator=self._creator_label, - subset=self._subset_name, - asset=self._asset_name, - message=self._exc_msg, - ) - if self._formatted_traceback: - report_message += "\n\n{}".format(self._formatted_traceback) - return [report_message] - - def _create_content(self, content_layout): - item_name_template = ( - "Creator: {}
" - "Subset: {}
" - "Asset: {}
" - ) - exc_msg_template = "{}" - - line = self._create_line() - content_layout.addWidget(line) - - item_name_widget = QtWidgets.QLabel(self) - item_name_widget.setText( - item_name_template.format( - self._creator_label, self._subset_name, self._asset_name - ) - ) - content_layout.addWidget(item_name_widget) - - message_label_widget = QtWidgets.QLabel(self) - message_label_widget.setText( - exc_msg_template.format(self.convert_text_for_html(self._exc_msg)) - ) - content_layout.addWidget(message_label_widget) - - if self._formatted_traceback: - line_widget = self._create_line() - tb_widget = self._create_traceback_widget( - self._formatted_traceback - ) - content_layout.addWidget(line_widget) - content_layout.addWidget(tb_widget) - - -# TODO add creator identifier/label to details -class CreatorShortDescWidget(QtWidgets.QWidget): - height_changed = QtCore.Signal(int) - - def __init__(self, parent=None): - super(CreatorShortDescWidget, self).__init__(parent=parent) - - # --- Short description widget --- - icon_widget = IconValuePixmapLabel(None, self) - icon_widget.setObjectName("FamilyIconLabel") - - # --- Short description inputs --- - short_desc_input_widget = QtWidgets.QWidget(self) - - family_label = QtWidgets.QLabel(short_desc_input_widget) - family_label.setAlignment( - QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft - ) - - description_label = QtWidgets.QLabel(short_desc_input_widget) - description_label.setAlignment( - QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft - ) - - short_desc_input_layout = QtWidgets.QVBoxLayout( - short_desc_input_widget - ) - short_desc_input_layout.setSpacing(0) - short_desc_input_layout.addWidget(family_label) - short_desc_input_layout.addWidget(description_label) - # -------------------------------- - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(icon_widget, 0) - layout.addWidget(short_desc_input_widget, 1) - # -------------------------------- - - self._icon_widget = icon_widget - self._family_label = family_label - self._description_label = description_label - - self._last_height = None - - def _check_height_change(self): - height = self.height() - if height != self._last_height: - self._last_height = height - self.height_changed.emit(height) - - def showEvent(self, event): - super(CreatorShortDescWidget, self).showEvent(event) - self._check_height_change() - - def resizeEvent(self, event): - super(CreatorShortDescWidget, self).resizeEvent(event) - self._check_height_change() - - def set_plugin(self, plugin=None): - if not plugin: - self._icon_widget.set_icon_def(None) - self._family_label.setText("") - self._description_label.setText("") - return - - plugin_icon = plugin.get_icon() - description = plugin.get_description() or "" - - self._icon_widget.set_icon_def(plugin_icon) - self._family_label.setText("{}".format(plugin.family)) - self._family_label.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) - self._description_label.setText(description) - - -class HelpButton(ClickableFrame): - resized = QtCore.Signal(int) - question_mark_icon_name = "fa.question" - help_icon_name = "fa.question-circle" - hide_icon_name = "fa.angle-left" - - def __init__(self, *args, **kwargs): - super(HelpButton, self).__init__(*args, **kwargs) - self.setObjectName("CreateDialogHelpButton") - - question_mark_label = QtWidgets.QLabel(self) - help_widget = QtWidgets.QWidget(self) - - help_question = QtWidgets.QLabel(help_widget) - help_label = QtWidgets.QLabel("Help", help_widget) - hide_icon = QtWidgets.QLabel(help_widget) - - help_layout = QtWidgets.QHBoxLayout(help_widget) - help_layout.setContentsMargins(0, 0, 5, 0) - help_layout.addWidget(help_question, 0) - help_layout.addWidget(help_label, 0) - help_layout.addStretch(1) - help_layout.addWidget(hide_icon, 0) - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - layout.addWidget(question_mark_label, 0) - layout.addWidget(help_widget, 1) - - help_widget.setVisible(False) - - self._question_mark_label = question_mark_label - self._help_widget = help_widget - self._help_question = help_question - self._hide_icon = hide_icon - - self._expanded = None - self.set_expanded() - - def set_expanded(self, expanded=None): - if self._expanded is expanded: - if expanded is not None: - return - expanded = False - self._expanded = expanded - self._help_widget.setVisible(expanded) - self._update_content() - - def _update_content(self): - width = self.get_icon_width() - if self._expanded: - question_mark_pix = QtGui.QPixmap(width, width) - question_mark_pix.fill(QtCore.Qt.transparent) - - else: - question_mark_icon = qtawesome.icon( - self.question_mark_icon_name, color=QtCore.Qt.white - ) - question_mark_pix = question_mark_icon.pixmap(width, width) - - hide_icon = qtawesome.icon( - self.hide_icon_name, color=QtCore.Qt.white - ) - help_question_icon = qtawesome.icon( - self.help_icon_name, color=QtCore.Qt.white - ) - self._question_mark_label.setPixmap(question_mark_pix) - self._question_mark_label.setMaximumWidth(width) - self._hide_icon.setPixmap(hide_icon.pixmap(width, width)) - self._help_question.setPixmap(help_question_icon.pixmap(width, width)) - - def get_icon_width(self): - metrics = self.fontMetrics() - return metrics.height() - - def set_pos_and_size(self, pos_x, pos_y, width, height): - update_icon = self.height() != height - self.move(pos_x, pos_y) - self.resize(width, height) - - if update_icon: - self._update_content() - self.updateGeometry() - - def showEvent(self, event): - super(HelpButton, self).showEvent(event) - self.resized.emit(self.height()) - - def resizeEvent(self, event): - super(HelpButton, self).resizeEvent(event) - self.resized.emit(self.height()) - - -class CreateDialog(QtWidgets.QDialog): - default_size = (1000, 560) - - def __init__( - self, controller, asset_name=None, task_name=None, parent=None - ): - super(CreateDialog, self).__init__(parent) - - self.setWindowTitle("Create new instance") - - self.controller = controller - - if asset_name is None: - asset_name = self.dbcon.Session.get("AVALON_ASSET") - - if task_name is None: - task_name = self.dbcon.Session.get("AVALON_TASK") - - self._asset_name = asset_name - self._task_name = task_name - - self._last_pos = None - self._asset_doc = None - self._subset_names = None - self._selected_creator = None - - self._prereq_available = False - - self._message_dialog = None - - name_pattern = "^[{}]*$".format(SUBSET_NAME_ALLOWED_SYMBOLS) - self._name_pattern = name_pattern - self._compiled_name_pattern = re.compile(name_pattern) - - overlay_object = MessageOverlayObject(self) - - context_widget = QtWidgets.QWidget(self) - - assets_widget = CreateDialogAssetsWidget(controller, context_widget) - tasks_widget = CreateDialogTasksWidget(controller, context_widget) - - context_layout = QtWidgets.QVBoxLayout(context_widget) - context_layout.setContentsMargins(0, 0, 0, 0) - context_layout.setSpacing(0) - context_layout.addWidget(assets_widget, 2) - context_layout.addWidget(tasks_widget, 1) - - # --- Creators view --- - creators_header_widget = QtWidgets.QWidget(self) - header_label_widget = QtWidgets.QLabel( - "Choose family:", creators_header_widget - ) - creators_header_layout = QtWidgets.QHBoxLayout(creators_header_widget) - creators_header_layout.setContentsMargins(0, 0, 0, 0) - creators_header_layout.addWidget(header_label_widget, 1) - - creators_view = QtWidgets.QListView(self) - creators_model = QtGui.QStandardItemModel() - creators_sort_model = QtCore.QSortFilterProxyModel() - creators_sort_model.setSourceModel(creators_model) - creators_view.setModel(creators_sort_model) - - variant_widget = VariantInputsWidget(self) - - variant_input = QtWidgets.QLineEdit(variant_widget) - variant_input.setObjectName("VariantInput") - variant_input.setToolTip(VARIANT_TOOLTIP) - - variant_hints_btn = QtWidgets.QToolButton(variant_widget) - variant_hints_btn.setArrowType(QtCore.Qt.DownArrow) - variant_hints_btn.setIconSize(QtCore.QSize(12, 12)) - - variant_hints_menu = QtWidgets.QMenu(variant_widget) - variant_hints_group = QtWidgets.QActionGroup(variant_hints_menu) - - variant_layout = QtWidgets.QHBoxLayout(variant_widget) - variant_layout.setContentsMargins(0, 0, 0, 0) - variant_layout.setSpacing(0) - variant_layout.addWidget(variant_input, 1) - variant_layout.addWidget(variant_hints_btn, 0, QtCore.Qt.AlignVCenter) - - subset_name_input = QtWidgets.QLineEdit(self) - subset_name_input.setEnabled(False) - - form_layout = QtWidgets.QFormLayout() - form_layout.addRow("Variant:", variant_widget) - form_layout.addRow("Subset:", subset_name_input) - - mid_widget = QtWidgets.QWidget(self) - mid_layout = QtWidgets.QVBoxLayout(mid_widget) - mid_layout.setContentsMargins(0, 0, 0, 0) - mid_layout.addWidget(creators_header_widget, 0) - mid_layout.addWidget(creators_view, 1) - mid_layout.addLayout(form_layout, 0) - # ------------ - - # --- Creator short info and attr defs --- - creator_attrs_widget = QtWidgets.QWidget(self) - - creator_short_desc_widget = CreatorShortDescWidget( - creator_attrs_widget - ) - - attr_separator_widget = QtWidgets.QWidget(self) - attr_separator_widget.setObjectName("Separator") - attr_separator_widget.setMinimumHeight(1) - attr_separator_widget.setMaximumHeight(1) - - # Precreate attributes widget - pre_create_widget = PreCreateWidget(creator_attrs_widget) - - # Create button - create_btn_wrapper = QtWidgets.QWidget(creator_attrs_widget) - create_btn = QtWidgets.QPushButton("Create", create_btn_wrapper) - create_btn.setEnabled(False) - - create_btn_wrap_layout = QtWidgets.QHBoxLayout(create_btn_wrapper) - create_btn_wrap_layout.setContentsMargins(0, 0, 0, 0) - create_btn_wrap_layout.addStretch(1) - create_btn_wrap_layout.addWidget(create_btn, 0) - - creator_attrs_layout = QtWidgets.QVBoxLayout(creator_attrs_widget) - creator_attrs_layout.setContentsMargins(0, 0, 0, 0) - creator_attrs_layout.addWidget(creator_short_desc_widget, 0) - creator_attrs_layout.addWidget(attr_separator_widget, 0) - creator_attrs_layout.addWidget(pre_create_widget, 1) - creator_attrs_layout.addWidget(create_btn_wrapper, 0) - # ------------------------------------- - - # --- Detailed information about creator --- - # Detailed description of creator - detail_description_widget = QtWidgets.QWidget(self) - - detail_placoholder_widget = QtWidgets.QWidget( - detail_description_widget - ) - detail_placoholder_widget.setAttribute( - QtCore.Qt.WA_TranslucentBackground - ) - - detail_description_input = QtWidgets.QTextEdit( - detail_description_widget - ) - detail_description_input.setObjectName("CreatorDetailedDescription") - detail_description_input.setTextInteractionFlags( - QtCore.Qt.TextBrowserInteraction - ) - - detail_description_layout = QtWidgets.QVBoxLayout( - detail_description_widget - ) - detail_description_layout.setContentsMargins(0, 0, 0, 0) - detail_description_layout.setSpacing(0) - detail_description_layout.addWidget(detail_placoholder_widget, 0) - detail_description_layout.addWidget(detail_description_input, 1) - - detail_description_widget.setVisible(False) - - # ------------------------------------------- - splitter_widget = QtWidgets.QSplitter(self) - splitter_widget.addWidget(context_widget) - splitter_widget.addWidget(mid_widget) - splitter_widget.addWidget(creator_attrs_widget) - splitter_widget.addWidget(detail_description_widget) - splitter_widget.setStretchFactor(0, 1) - splitter_widget.setStretchFactor(1, 1) - splitter_widget.setStretchFactor(2, 1) - splitter_widget.setStretchFactor(3, 1) - - layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(splitter_widget, 1) - - # Floating help button - # - Create this button as last to be fully visible - help_btn = HelpButton(self) - - prereq_timer = QtCore.QTimer() - prereq_timer.setInterval(50) - prereq_timer.setSingleShot(True) - - desc_width_anim_timer = QtCore.QTimer() - desc_width_anim_timer.setInterval(10) - - prereq_timer.timeout.connect(self._invalidate_prereq) - - desc_width_anim_timer.timeout.connect(self._on_desc_animation) - - help_btn.clicked.connect(self._on_help_btn) - help_btn.resized.connect(self._on_help_btn_resize) - - assets_widget.header_height_changed.connect( - self._on_asset_filter_height_change - ) - - create_btn.clicked.connect(self._on_create) - variant_widget.resized.connect(self._on_variant_widget_resize) - variant_input.returnPressed.connect(self._on_create) - variant_input.textChanged.connect(self._on_variant_change) - creators_view.selectionModel().currentChanged.connect( - self._on_creator_item_change - ) - variant_hints_btn.clicked.connect(self._on_variant_btn_click) - variant_hints_menu.triggered.connect(self._on_variant_action) - assets_widget.selection_changed.connect(self._on_asset_change) - assets_widget.current_context_required.connect( - self._on_current_session_context_request - ) - tasks_widget.task_changed.connect(self._on_task_change) - creator_short_desc_widget.height_changed.connect( - self._on_description_height_change - ) - splitter_widget.splitterMoved.connect(self._on_splitter_move) - - controller.add_plugins_refresh_callback(self._on_plugins_refresh) - - self._overlay_object = overlay_object - - self._splitter_widget = splitter_widget - - self._context_widget = context_widget - self._assets_widget = assets_widget - self._tasks_widget = tasks_widget - - self.subset_name_input = subset_name_input - - self.variant_input = variant_input - self.variant_hints_btn = variant_hints_btn - self.variant_hints_menu = variant_hints_menu - self.variant_hints_group = variant_hints_group - - self._creators_header_widget = creators_header_widget - self._creators_model = creators_model - self._creators_sort_model = creators_sort_model - self._creators_view = creators_view - self._create_btn = create_btn - - self._creator_short_desc_widget = creator_short_desc_widget - self._pre_create_widget = pre_create_widget - self._attr_separator_widget = attr_separator_widget - - self._detail_placoholder_widget = detail_placoholder_widget - self._detail_description_widget = detail_description_widget - self._detail_description_input = detail_description_input - self._help_btn = help_btn - - self._prereq_timer = prereq_timer - self._first_show = True - - # Description animation - self._description_size_policy = detail_description_widget.sizePolicy() - self._desc_width_anim_timer = desc_width_anim_timer - self._desc_widget_step = 0 - self._last_description_width = None - self._last_full_width = 0 - self._expected_description_width = 0 - self._last_desc_max_width = None - self._other_widgets_widths = [] - - def _emit_message(self, message): - self._overlay_object.add_message(message) - - def _context_change_is_enabled(self): - return self._context_widget.isEnabled() - - def _get_asset_name(self): - asset_name = None - if self._context_change_is_enabled(): - asset_name = self._assets_widget.get_selected_asset_name() - - if asset_name is None: - asset_name = self._asset_name - return asset_name - - def _get_task_name(self): - task_name = None - if self._context_change_is_enabled(): - # Don't use selection of task if asset is not set - asset_name = self._assets_widget.get_selected_asset_name() - if asset_name: - task_name = self._tasks_widget.get_selected_task_name() - - if not task_name: - task_name = self._task_name - return task_name - - @property - def dbcon(self): - return self.controller.dbcon - - def _set_context_enabled(self, enabled): - self._assets_widget.set_enabled(enabled) - self._tasks_widget.set_enabled(enabled) - check_prereq = self._context_widget.isEnabled() != enabled - self._context_widget.setEnabled(enabled) - if check_prereq: - self._invalidate_prereq() - - def refresh(self): - # Get context before refresh to keep selection of asset and - # task widgets - asset_name = self._get_asset_name() - task_name = self._get_task_name() - - self._prereq_available = False - - # Disable context widget so refresh of asset will use context asset - # name - self._set_context_enabled(False) - - self._assets_widget.refresh() - - # Refresh data before update of creators - self._refresh_asset() - # Then refresh creators which may trigger callbacks using refreshed - # data - self._refresh_creators() - - self._assets_widget.set_current_asset_name(self._asset_name) - self._assets_widget.select_asset_by_name(asset_name) - self._tasks_widget.set_asset_name(asset_name) - self._tasks_widget.select_task_name(task_name) - - self._invalidate_prereq_deffered() - - def _invalidate_prereq_deffered(self): - self._prereq_timer.start() - - def _on_asset_filter_height_change(self, height): - self._creators_header_widget.setMinimumHeight(height) - self._creators_header_widget.setMaximumHeight(height) - - def _invalidate_prereq(self): - prereq_available = True - creator_btn_tooltips = [] - - available_creators = self._creators_model.rowCount() > 0 - if available_creators != self._creators_view.isEnabled(): - self._creators_view.setEnabled(available_creators) - - if not available_creators: - prereq_available = False - creator_btn_tooltips.append("Creator is not selected") - - if self._context_change_is_enabled() and self._asset_doc is None: - # QUESTION how to handle invalid asset? - prereq_available = False - creator_btn_tooltips.append("Context is not selected") - - if prereq_available != self._prereq_available: - self._prereq_available = prereq_available - - self._create_btn.setEnabled(prereq_available) - - self.variant_input.setEnabled(prereq_available) - self.variant_hints_btn.setEnabled(prereq_available) - - tooltip = "" - if creator_btn_tooltips: - tooltip = "\n".join(creator_btn_tooltips) - self._create_btn.setToolTip(tooltip) - - self._on_variant_change() - - def _refresh_asset(self): - asset_name = self._get_asset_name() - - # Skip if asset did not change - if self._asset_doc and self._asset_doc["name"] == asset_name: - return - - # Make sure `_asset_doc` and `_subset_names` variables are reset - self._asset_doc = None - self._subset_names = None - if asset_name is None: - return - - project_name = self.dbcon.active_project() - asset_doc = get_asset_by_name(project_name, asset_name) - self._asset_doc = asset_doc - - if asset_doc: - asset_id = asset_doc["_id"] - subset_docs = get_subsets( - project_name, asset_ids=[asset_id], fields=["name"] - ) - self._subset_names = { - subset_doc["name"] - for subset_doc in subset_docs - } - - if not asset_doc: - self.subset_name_input.setText("< Asset is not set >") - - def _refresh_creators(self): - # Refresh creators and add their families to list - existing_items = {} - old_creators = set() - for row in range(self._creators_model.rowCount()): - item = self._creators_model.item(row, 0) - identifier = item.data(CREATOR_IDENTIFIER_ROLE) - existing_items[identifier] = item - old_creators.add(identifier) - - # Add new families - new_creators = set() - for identifier, creator in self.controller.manual_creators.items(): - # TODO add details about creator - new_creators.add(identifier) - if identifier in existing_items: - item = existing_items[identifier] - else: - item = QtGui.QStandardItem() - item.setFlags( - QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable - ) - self._creators_model.appendRow(item) - - label = creator.label or identifier - item.setData(label, QtCore.Qt.DisplayRole) - item.setData(identifier, CREATOR_IDENTIFIER_ROLE) - item.setData(creator.family, FAMILY_ROLE) - - # Remove families that are no more available - for identifier in (old_creators - new_creators): - item = existing_items[identifier] - self._creators_model.takeRow(item.row()) - - if self._creators_model.rowCount() < 1: - return - - self._creators_sort_model.sort(0) - # Make sure there is a selection - indexes = self._creators_view.selectedIndexes() - if not indexes: - index = self._creators_sort_model.index(0, 0) - self._creators_view.setCurrentIndex(index) - else: - index = indexes[0] - - identifier = index.data(CREATOR_IDENTIFIER_ROLE) - - self._set_creator_by_identifier(identifier) - - def _on_plugins_refresh(self): - # Trigger refresh only if is visible - if self.isVisible(): - self.refresh() - - def _on_asset_change(self): - self._refresh_asset() - - asset_name = self._assets_widget.get_selected_asset_name() - self._tasks_widget.set_asset_name(asset_name) - if self._context_change_is_enabled(): - self._invalidate_prereq_deffered() - - def _on_task_change(self): - if self._context_change_is_enabled(): - self._invalidate_prereq_deffered() - - def _on_current_session_context_request(self): - self._assets_widget.set_current_session_asset() - if self._task_name: - self._tasks_widget.select_task_name(self._task_name) - - def _on_description_height_change(self): - # Use separator's 'y' position as height - height = self._attr_separator_widget.y() - self._detail_placoholder_widget.setMinimumHeight(height) - self._detail_placoholder_widget.setMaximumHeight(height) - - def _on_creator_item_change(self, new_index, _old_index): - identifier = None - if new_index.isValid(): - identifier = new_index.data(CREATOR_IDENTIFIER_ROLE) - self._set_creator_by_identifier(identifier) - - def _update_help_btn(self): - short_desc_rect = self._creator_short_desc_widget.rect() - - # point = short_desc_rect.topRight() - point = short_desc_rect.center() - mapped_point = self._creator_short_desc_widget.mapTo(self, point) - # pos_y = mapped_point.y() - center_pos_y = mapped_point.y() - icon_width = self._help_btn.get_icon_width() - - _height = int(icon_width * 2.5) - height = min(_height, short_desc_rect.height()) - pos_y = center_pos_y - int(height / 2) - - pos_x = self.width() - icon_width - if self._detail_placoholder_widget.isVisible(): - pos_x -= ( - self._detail_placoholder_widget.width() - + self._splitter_widget.handle(3).width() - ) - - width = self.width() - pos_x - - self._help_btn.set_pos_and_size( - max(0, pos_x), max(0, pos_y), - width, height - ) - - def _on_help_btn_resize(self, height): - if self._creator_short_desc_widget.height() != height: - self._update_help_btn() - - def _on_splitter_move(self, *args): - self._update_help_btn() - - def _on_help_btn(self): - if self._desc_width_anim_timer.isActive(): - return - - final_size = self.size() - cur_sizes = self._splitter_widget.sizes() - - if self._desc_widget_step == 0: - now_visible = self._detail_description_widget.isVisible() - else: - now_visible = self._desc_widget_step > 0 - - sizes = [] - for idx, value in enumerate(cur_sizes): - if idx < 3: - sizes.append(value) - - self._last_full_width = final_size.width() - self._other_widgets_widths = list(sizes) - - if now_visible: - cur_desc_width = self._detail_description_widget.width() - if cur_desc_width < 1: - cur_desc_width = 2 - step_size = int(cur_desc_width / 5) - if step_size < 1: - step_size = 1 - - step_size *= -1 - expected_width = 0 - desc_width = cur_desc_width - 1 - width = final_size.width() - 1 - min_max = desc_width - self._last_description_width = cur_desc_width - - else: - self._detail_description_widget.setVisible(True) - handle = self._splitter_widget.handle(3) - desc_width = handle.sizeHint().width() - if self._last_description_width: - expected_width = self._last_description_width - else: - hint = self._detail_description_widget.sizeHint() - expected_width = hint.width() - - width = final_size.width() + desc_width - step_size = int(expected_width / 5) - if step_size < 1: - step_size = 1 - min_max = 0 - - if self._last_desc_max_width is None: - self._last_desc_max_width = ( - self._detail_description_widget.maximumWidth() - ) - self._detail_description_widget.setMinimumWidth(min_max) - self._detail_description_widget.setMaximumWidth(min_max) - self._expected_description_width = expected_width - self._desc_widget_step = step_size - - self._desc_width_anim_timer.start() - - sizes.append(desc_width) - - final_size.setWidth(width) - - self._splitter_widget.setSizes(sizes) - self.resize(final_size) - - self._help_btn.set_expanded(not now_visible) - - def _on_desc_animation(self): - current_width = self._detail_description_widget.width() - - desc_width = None - last_step = False - growing = self._desc_widget_step > 0 - - # Growing - if growing: - if current_width < self._expected_description_width: - desc_width = current_width + self._desc_widget_step - if desc_width >= self._expected_description_width: - desc_width = self._expected_description_width - last_step = True - - # Decreasing - elif self._desc_widget_step < 0: - if current_width > self._expected_description_width: - desc_width = current_width + self._desc_widget_step - if desc_width <= self._expected_description_width: - desc_width = self._expected_description_width - last_step = True - - if desc_width is None: - self._desc_widget_step = 0 - self._desc_width_anim_timer.stop() - return - - if last_step and not growing: - self._detail_description_widget.setVisible(False) - QtWidgets.QApplication.processEvents() - - width = self._last_full_width - handle_width = self._splitter_widget.handle(3).width() - if growing: - width += (handle_width + desc_width) - else: - width -= self._last_description_width - if last_step: - width -= handle_width - else: - width += desc_width - - if not last_step or growing: - self._detail_description_widget.setMaximumWidth(desc_width) - self._detail_description_widget.setMinimumWidth(desc_width) - - window_size = self.size() - window_size.setWidth(width) - self.resize(window_size) - if not last_step: - return - - self._desc_widget_step = 0 - self._desc_width_anim_timer.stop() - - if not growing: - return - - self._detail_description_widget.setMinimumWidth(0) - self._detail_description_widget.setMaximumWidth( - self._last_desc_max_width - ) - self._detail_description_widget.setSizePolicy( - self._description_size_policy - ) - - sizes = list(self._other_widgets_widths) - sizes.append(desc_width) - self._splitter_widget.setSizes(sizes) - - def _set_creator_detailed_text(self, creator): - if not creator: - self._detail_description_input.setPlainText("") - return - detailed_description = creator.get_detail_description() or "" - if commonmark: - html = commonmark.commonmark(detailed_description) - self._detail_description_input.setHtml(html) - else: - self._detail_description_input.setMarkdown(detailed_description) - - def _set_creator_by_identifier(self, identifier): - creator = self.controller.manual_creators.get(identifier) - self._set_creator(creator) - - def _set_creator(self, creator): - self._creator_short_desc_widget.set_plugin(creator) - self._set_creator_detailed_text(creator) - self._pre_create_widget.set_plugin(creator) - - self._selected_creator = creator - - if not creator: - self._set_context_enabled(False) - return - - if ( - creator.create_allow_context_change - != self._context_change_is_enabled() - ): - self._set_context_enabled(creator.create_allow_context_change) - self._refresh_asset() - - default_variants = creator.get_default_variants() - if not default_variants: - default_variants = ["Main"] - - default_variant = creator.get_default_variant() - if not default_variant: - default_variant = default_variants[0] - - for action in tuple(self.variant_hints_menu.actions()): - self.variant_hints_menu.removeAction(action) - action.deleteLater() - - for variant in default_variants: - if variant in SEPARATORS: - self.variant_hints_menu.addSeparator() - elif variant: - self.variant_hints_menu.addAction(variant) - - variant_text = default_variant or "Main" - # Make sure subset name is updated to new plugin - if variant_text == self.variant_input.text(): - self._on_variant_change() - else: - self.variant_input.setText(variant_text) - - def _on_variant_widget_resize(self): - self.variant_hints_btn.setFixedHeight(self.variant_input.height()) - - def _on_variant_btn_click(self): - pos = self.variant_hints_btn.rect().bottomLeft() - point = self.variant_hints_btn.mapToGlobal(pos) - self.variant_hints_menu.popup(point) - - def _on_variant_action(self, action): - value = action.text() - if self.variant_input.text() != value: - self.variant_input.setText(value) - - def _on_variant_change(self, variant_value=None): - if not self._prereq_available: - return - - # This should probably never happen? - if not self._selected_creator: - if self.subset_name_input.text(): - self.subset_name_input.setText("") - return - - if variant_value is None: - variant_value = self.variant_input.text() - - if not self._compiled_name_pattern.match(variant_value): - self._create_btn.setEnabled(False) - self._set_variant_state_property("invalid") - self.subset_name_input.setText("< Invalid variant >") - return - - if not self._context_change_is_enabled(): - self._create_btn.setEnabled(True) - self._set_variant_state_property("") - self.subset_name_input.setText("< Valid variant >") - return - - project_name = self.controller.project_name - task_name = self._get_task_name() - - asset_doc = copy.deepcopy(self._asset_doc) - # Calculate subset name with Creator plugin - try: - subset_name = self._selected_creator.get_subset_name( - variant_value, task_name, asset_doc, project_name - ) - except TaskNotSetError: - self._create_btn.setEnabled(False) - self._set_variant_state_property("invalid") - self.subset_name_input.setText("< Missing task >") - return - - self.subset_name_input.setText(subset_name) - - self._create_btn.setEnabled(True) - self._validate_subset_name(subset_name, variant_value) - - def _validate_subset_name(self, subset_name, variant_value): - # Get all subsets of the current asset - if self._subset_names: - existing_subset_names = set(self._subset_names) - else: - existing_subset_names = set() - existing_subset_names_low = set( - _name.lower() - for _name in existing_subset_names - ) - - # Replace - compare_regex = re.compile(re.sub( - variant_value, "(.+)", subset_name, flags=re.IGNORECASE - )) - variant_hints = set() - if variant_value: - for _name in existing_subset_names: - _result = compare_regex.search(_name) - if _result: - variant_hints |= set(_result.groups()) - - # Remove previous hints from menu - for action in tuple(self.variant_hints_group.actions()): - self.variant_hints_group.removeAction(action) - self.variant_hints_menu.removeAction(action) - action.deleteLater() - - # Add separator if there are hints and menu already has actions - if variant_hints and self.variant_hints_menu.actions(): - self.variant_hints_menu.addSeparator() - - # Add hints to actions - for variant_hint in variant_hints: - action = self.variant_hints_menu.addAction(variant_hint) - self.variant_hints_group.addAction(action) - - # Indicate subset existence - if not variant_value: - property_value = "empty" - - elif subset_name.lower() in existing_subset_names_low: - # validate existence of subset name with lowered text - # - "renderMain" vs. "rendermain" mean same path item for - # windows - property_value = "exists" - else: - property_value = "new" - - self._set_variant_state_property(property_value) - - variant_is_valid = variant_value.strip() != "" - if variant_is_valid != self._create_btn.isEnabled(): - self._create_btn.setEnabled(variant_is_valid) - - def _set_variant_state_property(self, state): - current_value = self.variant_input.property("state") - if current_value != state: - self.variant_input.setProperty("state", state) - self.variant_input.style().polish(self.variant_input) - - def _on_first_show(self): - center = self.rect().center() - - width, height = self.default_size - self.resize(width, height) - part = int(width / 7) - self._splitter_widget.setSizes( - [part * 2, part * 2, width - (part * 4)] - ) - - new_pos = self.mapToGlobal(center) - new_pos.setX(new_pos.x() - int(self.width() / 2)) - new_pos.setY(new_pos.y() - int(self.height() / 2)) - self.move(new_pos) - - def moveEvent(self, event): - super(CreateDialog, self).moveEvent(event) - self._last_pos = self.pos() - - def showEvent(self, event): - super(CreateDialog, self).showEvent(event) - if self._first_show: - self._first_show = False - self._on_first_show() - - if self._last_pos is not None: - self.move(self._last_pos) - - self._update_help_btn() - - self.refresh() - - def resizeEvent(self, event): - super(CreateDialog, self).resizeEvent(event) - self._update_help_btn() - - def _on_create(self): - indexes = self._creators_view.selectedIndexes() - if not indexes or len(indexes) > 1: - return - - if not self._create_btn.isEnabled(): - return - - index = indexes[0] - creator_label = index.data(QtCore.Qt.DisplayRole) - creator_identifier = index.data(CREATOR_IDENTIFIER_ROLE) - family = index.data(FAMILY_ROLE) - variant = self.variant_input.text() - # Care about subset name only if context change is enabled - subset_name = None - asset_name = None - task_name = None - if self._context_change_is_enabled(): - subset_name = self.subset_name_input.text() - asset_name = self._get_asset_name() - task_name = self._get_task_name() - - pre_create_data = self._pre_create_widget.current_value() - # Where to define these data? - # - what data show be stored? - instance_data = { - "asset": asset_name, - "task": task_name, - "variant": variant, - "family": family - } - - error_msg = None - formatted_traceback = None - try: - self.controller.create( - creator_identifier, - subset_name, - instance_data, - pre_create_data - ) - - except CreatorError as exc: - error_msg = str(exc) - - # Use bare except because some hosts raise their exceptions that - # do not inherit from python's `BaseException` - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - formatted_traceback = "".join(traceback.format_exception( - exc_type, exc_value, exc_traceback - )) - error_msg = str(exc_value) - - if error_msg is None: - self._set_creator(self._selected_creator) - self._emit_message("Creation finished...") - else: - box = CreateErrorMessageBox( - creator_label, - subset_name, - asset_name, - error_msg, - formatted_traceback, - parent=self - ) - box.show() - # Store dialog so is not garbage collected before is shown - self._message_dialog = box diff --git a/openpype/tools/publisher/widgets/tabs_widget.py b/openpype/tools/publisher/widgets/tabs_widget.py index 0e92a6fd8d..bf3ef9eec0 100644 --- a/openpype/tools/publisher/widgets/tabs_widget.py +++ b/openpype/tools/publisher/widgets/tabs_widget.py @@ -53,6 +53,11 @@ class PublisherTabsWidget(QtWidgets.QFrame): self._current_button = None self._buttons_by_identifier = {} + def is_current_tab(self, identifier): + if isinstance(identifier, PublisherTabBtn): + identifier = identifier.identifier + return self._current_button == identifier + def add_tab(self, label, identifier): button = PublisherTabBtn(identifier, label, self) button.tab_clicked.connect(self._on_tab_click) @@ -61,8 +66,12 @@ class PublisherTabsWidget(QtWidgets.QFrame): if self._current_button is None: self.set_current_tab(identifier) + return button def set_current_tab(self, identifier): + if isinstance(identifier, PublisherTabBtn): + identifier = identifier.identifier + if identifier == self._current_button: return diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 8df9f9bbf5..5fd558a1b5 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -15,8 +15,6 @@ from .widgets import ( PublisherTabsWidget, - CreateDialog, - StopBtn, ResetBtn, ValidateBtn, @@ -76,7 +74,7 @@ class PublisherWindow(QtWidgets.QDialog): # Tabs widget under header tabs_widget = PublisherTabsWidget(self) - tabs_widget.add_tab("Create", "create") + create_tab = tabs_widget.add_tab("Create", "create") tabs_widget.add_tab("Publish", "publish") tabs_widget.add_tab("Report", "report") tabs_widget.add_tab("Details", "details") @@ -137,8 +135,6 @@ class PublisherWindow(QtWidgets.QDialog): main_layout.addWidget(content_stacked_widget, 1) main_layout.addWidget(footer_widget, 0) - creator_window = CreateDialog(controller, parent=self) - tabs_widget.tab_changed.connect(self._on_tab_change) create_overview_widget.active_changed.connect( self._on_context_or_active_change @@ -165,6 +161,7 @@ class PublisherWindow(QtWidgets.QDialog): self._header_layout = header_layout self._tabs_widget = tabs_widget + self._create_tab = create_tab self._content_stacked_widget = content_stacked_widget self.content_stacked_layout = content_stacked_layout @@ -182,8 +179,6 @@ class PublisherWindow(QtWidgets.QDialog): self._controller = controller - self.creator_window = creator_window - @property def controller(self): return self._controller @@ -208,7 +203,10 @@ class PublisherWindow(QtWidgets.QDialog): self.context_label.setText(label) def _on_tab_change(self, prev_tab, new_tab): - print(prev_tab, new_tab) + if new_tab in ("create", "publish"): + self._create_overview_widget.set_state(prev_tab, new_tab) + + # TODO handle rest of conditions def _on_context_or_active_change(self): self._validate_create_instances() @@ -222,23 +220,9 @@ class PublisherWindow(QtWidgets.QDialog): def _set_publish_visibility(self, visible): if visible: widget = self.publish_frame - publish_frame_visible = True else: widget = self._create_overview_widget - publish_frame_visible = False self.content_stacked_layout.setCurrentWidget(widget) - self._set_publish_frame_visible(publish_frame_visible) - - def _set_publish_frame_visible(self, publish_frame_visible): - """Publish frame visibility has changed. - - Also used in TrayPublisher to be able handle start/end of publish - widget overlay. - """ - - # Hide creator dialog if visible - if publish_frame_visible and self.creator_window.isVisible(): - self.creator_window.close() def _on_reset_clicked(self): self._controller.reset() @@ -264,7 +248,6 @@ class PublisherWindow(QtWidgets.QDialog): self._controller.publish() def _set_footer_enabled(self, enabled): - self.comment_input.setEnabled(enabled) self.reset_btn.setEnabled(True) if enabled: self.stop_btn.setEnabled(False) @@ -276,6 +259,8 @@ class PublisherWindow(QtWidgets.QDialog): self.publish_btn.setEnabled(enabled) def _on_publish_reset(self): + self._create_tab.setEnabled(True) + self.comment_input.setVisible(True) self._set_publish_visibility(False) self._set_footer_enabled(False) @@ -286,6 +271,11 @@ class PublisherWindow(QtWidgets.QDialog): self.validate_btn.setEnabled(False) self.publish_btn.setEnabled(False) + self.comment_input.setVisible(False) + self._create_tab.setEnabled(False) + if self._tabs_widget.is_current_tab(self._create_tab): + self._tabs_widget.set_current_tab("publish") + def _on_publish_validated(self): self.validate_btn.setEnabled(False) From 88ace97c941def6b9189cc4bc9e8bd6792d173bf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 17:57:16 +0200 Subject: [PATCH 052/275] fixed project selection in tray publisher --- openpype/tools/traypublisher/window.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/openpype/tools/traypublisher/window.py b/openpype/tools/traypublisher/window.py index 128c0fef11..97edb9ab06 100644 --- a/openpype/tools/traypublisher/window.py +++ b/openpype/tools/traypublisher/window.py @@ -137,11 +137,14 @@ class StandaloneOverlayWidget(QtWidgets.QFrame): src_index = self._projects_model.find_project(project_name) if src_index is not None: index = self._projects_proxy.mapFromSource(src_index) - if index: - mode = ( - QtCore.QItemSelectionModel.Select - | QtCore.QItemSelectionModel.Rows) - self._projects_view.selectionModel().select(index, mode) + + if index is not None: + selection_model = self._projects_view.selectionModel() + selection_model.select( + index, + QtCore.QItemSelectionModel.SelectCurrent + ) + self._projects_view.setCurrentIndex(index) self._cancel_btn.setVisible(self._project_name is not None) super(StandaloneOverlayWidget, self).showEvent(event) @@ -239,15 +242,15 @@ class TrayPublishWindow(PublisherWindow): def _on_project_select(self, project_name): # TODO register project specific plugin paths - self.controller.save_changes() - self.controller.reset_project_data_cache() + self._controller.save_changes() + self._controller.reset_project_data_cache() self.reset() - if not self.controller.instances: + if not self._controller.instances: self._go_to_create_tab() def _on_tray_publish_save(self): - self.controller.save_changes() + self._controller.save_changes() print("NOT YET IMPLEMENTED") From 5857c01442dc81daa71e71f78725a682c450f27e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 18:20:08 +0200 Subject: [PATCH 053/275] changed PublishReportViewerWidget to frame --- openpype/tools/publisher/publish_report_viewer/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/publish_report_viewer/widgets.py b/openpype/tools/publisher/publish_report_viewer/widgets.py index 61eb814a56..dc82448495 100644 --- a/openpype/tools/publisher/publish_report_viewer/widgets.py +++ b/openpype/tools/publisher/publish_report_viewer/widgets.py @@ -331,7 +331,7 @@ class DetailsPopup(QtWidgets.QDialog): self.closed.emit() -class PublishReportViewerWidget(QtWidgets.QWidget): +class PublishReportViewerWidget(QtWidgets.QFrame): def __init__(self, parent=None): super(PublishReportViewerWidget, self).__init__(parent) From 55e4aa3c76d397e59c909021e4eb642f6e73821a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 18:20:33 +0200 Subject: [PATCH 054/275] added details to window --- openpype/tools/publisher/widgets/__init__.py | 1 + .../tools/publisher/widgets/create_widget.py | 1 + .../publisher/widgets/overview_widget.py | 9 +- openpype/tools/publisher/window.py | 128 +++++++++++------- 4 files changed, 86 insertions(+), 53 deletions(-) diff --git a/openpype/tools/publisher/widgets/__init__.py b/openpype/tools/publisher/widgets/__init__.py index 1d0ed0633b..f8e3c4b19b 100644 --- a/openpype/tools/publisher/widgets/__init__.py +++ b/openpype/tools/publisher/widgets/__init__.py @@ -13,6 +13,7 @@ from .publish_widget import PublishFrame from .tabs_widget import PublisherTabsWidget from .overview_widget import CreateOverviewWidget + __all__ = ( "get_icon_path", "get_pixmap", diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index a0b3db0409..733dbf18ca 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -443,6 +443,7 @@ class CreateWidget(QtWidgets.QWidget): splitter_widget.setStretchFactor(3, 1) layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(splitter_widget, 1) prereq_timer = QtCore.QTimer() diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index ddc976d458..7afe02116f 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -85,12 +85,7 @@ class CreateOverviewWidget(QtWidgets.QFrame): # Subset frame layout main_layout = QtWidgets.QVBoxLayout(self) - marings = main_layout.contentsMargins() - marings.setLeft(marings.left() * 2) - marings.setRight(marings.right() * 2) - marings.setTop(marings.top() * 2) - marings.setBottom(0) - main_layout.setContentsMargins(marings) + main_layout.setContentsMargins(0, 0, 0, 0) main_layout.addWidget(subset_content_widget, 1) # --- Calbacks for instances/subsets view --- @@ -137,7 +132,7 @@ class CreateOverviewWidget(QtWidgets.QFrame): self._current_state = "create" subset_attributes_wrap.setVisible(False) - def set_state(self, old_state, new_state): + def set_state(self, new_state, animate): if new_state == self._current_state: return diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 5fd558a1b5..4aa02ff2d5 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -8,6 +8,7 @@ from openpype.tools.utils import ( PlaceholderLineEdit, PixmapLabel ) +from .publish_report_viewer import PublishReportViewerWidget from .control import PublisherController from .widgets import ( CreateOverviewWidget, @@ -79,13 +80,6 @@ class PublisherWindow(QtWidgets.QDialog): tabs_widget.add_tab("Report", "report") tabs_widget.add_tab("Details", "details") - # Content - content_stacked_widget = QtWidgets.QWidget(self) - - create_overview_widget = CreateOverviewWidget( - controller, content_stacked_widget - ) - # Footer footer_widget = QtWidgets.QWidget(self) footer_bottom_widget = QtWidgets.QWidget(footer_widget) @@ -113,17 +107,45 @@ class PublisherWindow(QtWidgets.QDialog): footer_layout.addWidget(comment_input, 0) footer_layout.addWidget(footer_bottom_widget, 0) + # Content + # - wrap stacked widget under one more widget to be able propagate + # margins (QStackedLayout can't have margins) + content_widget = QtWidgets.QWidget(self) + + content_stacked_widget = QtWidgets.QWidget(content_widget) + + content_layout = QtWidgets.QVBoxLayout(content_widget) + marings = content_layout.contentsMargins() + marings.setLeft(marings.left() * 2) + marings.setRight(marings.right() * 2) + marings.setTop(marings.top() * 2) + marings.setBottom(0) + content_layout.setContentsMargins(marings) + content_layout.addWidget(content_stacked_widget, 1) + + # Overview - create and attributes part + overview_widget = CreateOverviewWidget( + controller, content_stacked_widget + ) + + # Details - Publish details + publish_details_widget = PublishReportViewerWidget( + content_stacked_widget + ) + # Create publish frame publish_frame = PublishFrame(controller, content_stacked_widget) content_stacked_layout = QtWidgets.QStackedLayout( content_stacked_widget ) + content_stacked_layout.setContentsMargins(0, 0, 0, 0) content_stacked_layout.setStackingMode( QtWidgets.QStackedLayout.StackAll ) - content_stacked_layout.addWidget(create_overview_widget) + content_stacked_layout.addWidget(overview_widget) + content_stacked_layout.addWidget(publish_details_widget) content_stacked_layout.addWidget(publish_frame) # Add main frame to this window @@ -132,17 +154,17 @@ class PublisherWindow(QtWidgets.QDialog): main_layout.setSpacing(0) main_layout.addWidget(header_widget, 0) main_layout.addWidget(tabs_widget, 0) - main_layout.addWidget(content_stacked_widget, 1) + main_layout.addWidget(content_widget, 1) main_layout.addWidget(footer_widget, 0) tabs_widget.tab_changed.connect(self._on_tab_change) - create_overview_widget.active_changed.connect( + overview_widget.active_changed.connect( self._on_context_or_active_change ) - create_overview_widget.instance_context_changed.connect( + overview_widget.instance_context_changed.connect( self._on_context_or_active_change ) - create_overview_widget.create_requested.connect( + overview_widget.create_requested.connect( self._on_create_request ) @@ -164,18 +186,20 @@ class PublisherWindow(QtWidgets.QDialog): self._create_tab = create_tab self._content_stacked_widget = content_stacked_widget - self.content_stacked_layout = content_stacked_layout - self._create_overview_widget = create_overview_widget - self.publish_frame = publish_frame + self._content_stacked_layout = content_stacked_layout - self.context_label = context_label + self._overview_widget = overview_widget + self._publish_details_widget = publish_details_widget + self._publish_frame = publish_frame - self.comment_input = comment_input + self._context_label = context_label - self.stop_btn = stop_btn - self.reset_btn = reset_btn - self.validate_btn = validate_btn - self.publish_btn = publish_btn + self._comment_input = comment_input + + self._stop_btn = stop_btn + self._reset_btn = reset_btn + self._validate_btn = validate_btn + self._publish_btn = publish_btn self._controller = controller @@ -200,11 +224,23 @@ class PublisherWindow(QtWidgets.QDialog): self._controller.reset() def set_context_label(self, label): - self.context_label.setText(label) + self._context_label.setText(label) - def _on_tab_change(self, prev_tab, new_tab): + def _on_tab_change(self, old_tab, new_tab): if new_tab in ("create", "publish"): - self._create_overview_widget.set_state(prev_tab, new_tab) + animate = True + if old_tab not in ("create", "publish"): + animate = False + self._content_stacked_layout.setCurrentWidget( + self._overview_widget + ) + self._overview_widget.set_state(new_tab, animate) + + elif new_tab == "details": + self._content_stacked_layout.setCurrentWidget( + self._publish_details_widget + ) + # TODO handle rest of conditions @@ -219,10 +255,10 @@ class PublisherWindow(QtWidgets.QDialog): def _set_publish_visibility(self, visible): if visible: - widget = self.publish_frame + widget = self._publish_frame else: - widget = self._create_overview_widget - self.content_stacked_layout.setCurrentWidget(widget) + widget = self._overview_widget + self._content_stacked_layout.setCurrentWidget(widget) def _on_reset_clicked(self): self._controller.reset() @@ -234,7 +270,7 @@ class PublisherWindow(QtWidgets.QDialog): if self._controller.publish_comment_is_set: return - comment = self.comment_input.text() + comment = self._comment_input.text() self._controller.set_comment(comment) def _on_validate_clicked(self): @@ -248,40 +284,40 @@ class PublisherWindow(QtWidgets.QDialog): self._controller.publish() def _set_footer_enabled(self, enabled): - self.reset_btn.setEnabled(True) + self._reset_btn.setEnabled(True) if enabled: - self.stop_btn.setEnabled(False) - self.validate_btn.setEnabled(True) - self.publish_btn.setEnabled(True) + self._stop_btn.setEnabled(False) + self._validate_btn.setEnabled(True) + self._publish_btn.setEnabled(True) else: - self.stop_btn.setEnabled(enabled) - self.validate_btn.setEnabled(enabled) - self.publish_btn.setEnabled(enabled) + self._stop_btn.setEnabled(enabled) + self._validate_btn.setEnabled(enabled) + self._publish_btn.setEnabled(enabled) def _on_publish_reset(self): self._create_tab.setEnabled(True) - self.comment_input.setVisible(True) + self._comment_input.setVisible(True) self._set_publish_visibility(False) self._set_footer_enabled(False) def _on_publish_start(self): - self.reset_btn.setEnabled(False) - self.stop_btn.setEnabled(True) - self.validate_btn.setEnabled(False) - self.publish_btn.setEnabled(False) + self._reset_btn.setEnabled(False) + self._stop_btn.setEnabled(True) + self._validate_btn.setEnabled(False) + self._publish_btn.setEnabled(False) - self.comment_input.setVisible(False) + self._comment_input.setVisible(False) self._create_tab.setEnabled(False) if self._tabs_widget.is_current_tab(self._create_tab): self._tabs_widget.set_current_tab("publish") def _on_publish_validated(self): - self.validate_btn.setEnabled(False) + self._validate_btn.setEnabled(False) def _on_publish_stop(self): - self.reset_btn.setEnabled(True) - self.stop_btn.setEnabled(False) + self._reset_btn.setEnabled(True) + self._stop_btn.setEnabled(False) validate_enabled = not self._controller.publish_has_crashed publish_enabled = not self._controller.publish_has_crashed if validate_enabled: @@ -296,8 +332,8 @@ class PublisherWindow(QtWidgets.QDialog): else: publish_enabled = not self._controller.publish_has_finished - self.validate_btn.setEnabled(validate_enabled) - self.publish_btn.setEnabled(publish_enabled) + self._validate_btn.setEnabled(validate_enabled) + self._publish_btn.setEnabled(publish_enabled) def _validate_create_instances(self): if not self._controller.host_is_valid: From bd9a987f7ba9de82a75f3d6b8e3d0cfbcd1b872c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 18:33:22 +0200 Subject: [PATCH 055/275] update details in certain situations --- openpype/tools/publisher/widgets/tabs_widget.py | 15 +++++++++------ openpype/tools/publisher/window.py | 16 ++++++++++++++-- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/openpype/tools/publisher/widgets/tabs_widget.py b/openpype/tools/publisher/widgets/tabs_widget.py index bf3ef9eec0..84638a002c 100644 --- a/openpype/tools/publisher/widgets/tabs_widget.py +++ b/openpype/tools/publisher/widgets/tabs_widget.py @@ -50,13 +50,13 @@ class PublisherTabsWidget(QtWidgets.QFrame): self._btns_layout = btns_layout - self._current_button = None + self._current_identifier = None self._buttons_by_identifier = {} def is_current_tab(self, identifier): if isinstance(identifier, PublisherTabBtn): identifier = identifier.identifier - return self._current_button == identifier + return self._current_identifier == identifier def add_tab(self, label, identifier): button = PublisherTabBtn(identifier, label, self) @@ -64,7 +64,7 @@ class PublisherTabsWidget(QtWidgets.QFrame): self._btns_layout.addWidget(button, 0) self._buttons_by_identifier[identifier] = button - if self._current_button is None: + if self._current_identifier is None: self.set_current_tab(identifier) return button @@ -72,21 +72,24 @@ class PublisherTabsWidget(QtWidgets.QFrame): if isinstance(identifier, PublisherTabBtn): identifier = identifier.identifier - if identifier == self._current_button: + if identifier == self._current_identifier: return new_btn = self._buttons_by_identifier.get(identifier) if new_btn is None: return - old_identifier = self._current_button + old_identifier = self._current_identifier old_btn = self._buttons_by_identifier.get(old_identifier) - self._current_button = identifier + self._current_identifier = identifier if old_btn is not None: old_btn.deactivate() new_btn.activate() self.tab_changed.emit(old_identifier, identifier) + def current_tab(self): + return self._current_identifier + def _on_tab_click(self, identifier): self.set_current_tab(identifier) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 4aa02ff2d5..95c639a56c 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -226,7 +226,17 @@ class PublisherWindow(QtWidgets.QDialog): def set_context_label(self, label): self._context_label.setText(label) + def _update_publish_details_widget(self, force=False): + if not force and self._tabs_widget.current_tab() != "details": + return + + report_data = self.controller.get_publish_report() + self._publish_details_widget.set_report_data(report_data) + def _on_tab_change(self, old_tab, new_tab): + if old_tab == "details": + self._publish_details_widget.close_details_popup() + if new_tab in ("create", "publish"): animate = True if old_tab not in ("create", "publish"): @@ -240,7 +250,7 @@ class PublisherWindow(QtWidgets.QDialog): self._content_stacked_layout.setCurrentWidget( self._publish_details_widget ) - + self._update_publish_details_widget() # TODO handle rest of conditions @@ -298,8 +308,8 @@ class PublisherWindow(QtWidgets.QDialog): self._create_tab.setEnabled(True) self._comment_input.setVisible(True) self._set_publish_visibility(False) - self._set_footer_enabled(False) + self._update_publish_details_widget() def _on_publish_start(self): self._reset_btn.setEnabled(False) @@ -334,6 +344,7 @@ class PublisherWindow(QtWidgets.QDialog): self._validate_btn.setEnabled(validate_enabled) self._publish_btn.setEnabled(publish_enabled) + self._update_publish_details_widget() def _validate_create_instances(self): if not self._controller.host_is_valid: @@ -359,3 +370,4 @@ class PublisherWindow(QtWidgets.QDialog): context_title = self.controller.get_context_title() self.set_context_label(context_title) + self._update_publish_details_widget() From acb167c0d4b48afce082f6d76dad2ecf264b0096 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 18:45:48 +0200 Subject: [PATCH 056/275] added report page to window --- openpype/tools/publisher/widgets/__init__.py | 6 ++++-- .../publisher/widgets/overview_widget.py | 4 ++-- .../publisher/widgets/validations_widget.py | 5 ++--- openpype/tools/publisher/window.py | 19 ++++++++++++++++--- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/openpype/tools/publisher/widgets/__init__.py b/openpype/tools/publisher/widgets/__init__.py index f8e3c4b19b..81bb77ce89 100644 --- a/openpype/tools/publisher/widgets/__init__.py +++ b/openpype/tools/publisher/widgets/__init__.py @@ -11,7 +11,8 @@ from .widgets import ( ) from .publish_widget import PublishFrame from .tabs_widget import PublisherTabsWidget -from .overview_widget import CreateOverviewWidget +from .overview_widget import OverviewWidget +from .validations_widget import ValidationsWidget __all__ = ( @@ -27,5 +28,6 @@ __all__ = ( "PublishFrame", "PublisherTabsWidget", - "CreateOverviewWidget", + "OverviewWidget", + "ValidationsWidget", ) diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index 7afe02116f..90527234a7 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -13,13 +13,13 @@ from .widgets import ( from .create_widget import CreateWidget -class CreateOverviewWidget(QtWidgets.QFrame): +class OverviewWidget(QtWidgets.QFrame): active_changed = QtCore.Signal() instance_context_changed = QtCore.Signal() create_requested = QtCore.Signal() def __init__(self, controller, parent): - super(CreateOverviewWidget, self).__init__(parent) + super(OverviewWidget, self).__init__(parent) self._refreshing_instances = False self._controller = controller diff --git a/openpype/tools/publisher/widgets/validations_widget.py b/openpype/tools/publisher/widgets/validations_widget.py index e7ab4ecf5a..b70cd81878 100644 --- a/openpype/tools/publisher/widgets/validations_widget.py +++ b/openpype/tools/publisher/widgets/validations_widget.py @@ -400,7 +400,7 @@ class VerticallScrollArea(QtWidgets.QScrollArea): return super(VerticallScrollArea, self).eventFilter(obj, event) -class ValidationsWidget(QtWidgets.QWidget): +class ValidationsWidget(QtWidgets.QFrame): """Widgets showing validation error. This widget is shown if validation error/s happened during validation part. @@ -418,11 +418,10 @@ class ValidationsWidget(QtWidgets.QWidget): β”‚ Publish buttons β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ """ + def __init__(self, controller, parent): super(ValidationsWidget, self).__init__(parent) - self.setAttribute(QtCore.Qt.WA_TranslucentBackground) - errors_scroll = VerticallScrollArea(self) errors_scroll.setWidgetResizable(True) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 95c639a56c..568735f4ae 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -11,7 +11,8 @@ from openpype.tools.utils import ( from .publish_report_viewer import PublishReportViewerWidget from .control import PublisherController from .widgets import ( - CreateOverviewWidget, + OverviewWidget, + ValidationsWidget, PublishFrame, PublisherTabsWidget, @@ -124,10 +125,12 @@ class PublisherWindow(QtWidgets.QDialog): content_layout.addWidget(content_stacked_widget, 1) # Overview - create and attributes part - overview_widget = CreateOverviewWidget( + overview_widget = OverviewWidget( controller, content_stacked_widget ) + report_widget = ValidationsWidget(controller, parent) + # Details - Publish details publish_details_widget = PublishReportViewerWidget( content_stacked_widget @@ -145,6 +148,7 @@ class PublisherWindow(QtWidgets.QDialog): QtWidgets.QStackedLayout.StackAll ) content_stacked_layout.addWidget(overview_widget) + content_stacked_layout.addWidget(report_widget) content_stacked_layout.addWidget(publish_details_widget) content_stacked_layout.addWidget(publish_frame) @@ -189,6 +193,7 @@ class PublisherWindow(QtWidgets.QDialog): self._content_stacked_layout = content_stacked_layout self._overview_widget = overview_widget + self._report_widget = report_widget self._publish_details_widget = publish_details_widget self._publish_frame = publish_frame @@ -252,7 +257,10 @@ class PublisherWindow(QtWidgets.QDialog): ) self._update_publish_details_widget() - # TODO handle rest of conditions + elif new_tab == "report": + self._content_stacked_layout.setCurrentWidget( + self._report_widget + ) def _on_context_or_active_change(self): self._validate_create_instances() @@ -319,6 +327,8 @@ class PublisherWindow(QtWidgets.QDialog): self._comment_input.setVisible(False) self._create_tab.setEnabled(False) + + self._report_widget.clear() if self._tabs_widget.is_current_tab(self._create_tab): self._tabs_widget.set_current_tab("publish") @@ -346,6 +356,9 @@ class PublisherWindow(QtWidgets.QDialog): self._publish_btn.setEnabled(publish_enabled) self._update_publish_details_widget() + validation_errors = self._controller.get_validation_errors() + self._report_widget.set_errors(validation_errors) + def _validate_create_instances(self): if not self._controller.host_is_valid: self._set_footer_enabled(True) From 3f4d59b54667b9c0fbda928173793ea31e599e9c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 28 Sep 2022 18:45:58 +0200 Subject: [PATCH 057/275] removed report and validation from publish frame --- .../tools/publisher/widgets/publish_widget.py | 41 ++----------------- 1 file changed, 3 insertions(+), 38 deletions(-) diff --git a/openpype/tools/publisher/widgets/publish_widget.py b/openpype/tools/publisher/widgets/publish_widget.py index b32b5381d1..ea23f9c42c 100644 --- a/openpype/tools/publisher/widgets/publish_widget.py +++ b/openpype/tools/publisher/widgets/publish_widget.py @@ -6,8 +6,6 @@ from Qt import QtWidgets, QtCore, QtGui from openpype.pipeline import KnownPublishError -from .validations_widget import ValidationsWidget -from ..publish_report_viewer import PublishReportViewerWidget from .widgets import ( StopBtn, ResetBtn, @@ -101,9 +99,6 @@ class PublishFrame(QtWidgets.QFrame): self.setObjectName("PublishFrame") - # Widget showing validation errors. Their details and action callbacks. - validation_errors_widget = ValidationsWidget(controller, self) - # Bottom part of widget where process and callback buttons are showed # - QFrame used to be able set background using stylesheets easily # and not override all children widgets style @@ -203,17 +198,15 @@ class PublishFrame(QtWidgets.QFrame): publish_widget = QtWidgets.QWidget(self) publish_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) publish_layout = QtWidgets.QVBoxLayout(publish_widget) - publish_layout.addWidget(validation_errors_widget, 1) + publish_layout.addStretch(1) publish_layout.addWidget(info_frame, 0) details_widget = QtWidgets.QWidget(self) - report_view = PublishReportViewerWidget(details_widget) close_report_btn = QtWidgets.QPushButton(details_widget) close_report_icon = self._get_report_close_icon() close_report_btn.setIcon(close_report_icon) details_layout = QtWidgets.QVBoxLayout(details_widget) - details_layout.addWidget(report_view) details_layout.addWidget(close_report_btn) main_layout = QtWidgets.QStackedLayout(self) @@ -224,8 +217,6 @@ class PublishFrame(QtWidgets.QFrame): main_layout.setCurrentWidget(publish_widget) - show_details_btn.clicked.connect(self._on_show_details) - copy_report_btn.clicked.connect(self._on_copy_report) export_report_btn.clicked.connect(self._on_export_report) @@ -234,8 +225,6 @@ class PublishFrame(QtWidgets.QFrame): validate_btn.clicked.connect(self._on_validate_clicked) publish_btn.clicked.connect(self._on_publish_clicked) - close_report_btn.clicked.connect(self._on_close_report_clicked) - controller.add_publish_reset_callback(self._on_publish_reset) controller.add_publish_started_callback(self._on_publish_start) controller.add_publish_validated_callback(self._on_publish_validated) @@ -249,8 +238,6 @@ class PublishFrame(QtWidgets.QFrame): self._info_frame = info_frame self._publish_widget = publish_widget - self._validation_errors_widget = validation_errors_widget - self._main_layout = main_layout self._main_label = main_label @@ -269,7 +256,6 @@ class PublishFrame(QtWidgets.QFrame): self._publish_btn = publish_btn self._details_widget = details_widget - self._report_view = report_view def _get_report_close_icon(self): size = 100 @@ -314,8 +300,6 @@ class PublishFrame(QtWidgets.QFrame): self._progress_widget.setMaximum(self.controller.publish_max_progress) def _on_publish_start(self): - self._validation_errors_widget.clear() - self._set_success_property(-1) self._change_bg_property() self._set_progress_visibility(True) @@ -388,7 +372,7 @@ class PublishFrame(QtWidgets.QFrame): elif validation_errors: self._set_progress_visibility(False) self._change_bg_property(1) - self._set_validation_errors(validation_errors) + self._set_validation_errors() elif self.controller.publish_has_finished: self._set_finished() @@ -421,14 +405,12 @@ class PublishFrame(QtWidgets.QFrame): self._message_label_bottom.setText("") self._set_success_property(0) - def _set_validation_errors(self, validation_errors): + def _set_validation_errors(self): self._main_label.setText("Your publish didn't pass studio validations") self._message_label_top.setText("") self._message_label_bottom.setText("Check results above please") self._set_success_property(2) - self._validation_errors_widget.set_errors(validation_errors) - def _set_finished(self): self._main_label.setText("Finished") self._message_label_top.setText("") @@ -489,23 +471,6 @@ class PublishFrame(QtWidgets.QFrame): with open(full_path, "w") as file_stream: json.dump(logs, file_stream) - def _on_show_details(self): - self._change_bg_property(2) - self._main_layout.setCurrentWidget(self._details_widget) - report_data = self.controller.get_publish_report() - self._report_view.set_report_data(report_data) - - def _on_close_report_clicked(self): - self._report_view.close_details_popup() - if self.controller.get_publish_crash_error(): - self._change_bg_property() - - elif self.controller.get_validation_errors(): - self._change_bg_property(1) - else: - self._change_bg_property(2) - self._main_layout.setCurrentWidget(self._publish_widget) - def _on_reset_clicked(self): self.controller.reset() From 0c86e157f98544a5f57b80b0b59450f51ed5a49a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 29 Sep 2022 12:37:55 +0200 Subject: [PATCH 058/275] disable attributes after publish start --- openpype/tools/publisher/widgets/overview_widget.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index 90527234a7..99b9d88007 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -38,6 +38,7 @@ class OverviewWidget(QtWidgets.QFrame): subset_views_layout = QtWidgets.QStackedLayout() subset_views_layout.addWidget(subset_view_cards) subset_views_layout.addWidget(subset_list_view) + subset_views_layout.setCurrentWidget(subset_view_cards) # Buttons at the bottom of subset view create_btn = CreateInstanceBtn(self) @@ -113,6 +114,7 @@ class OverviewWidget(QtWidgets.QFrame): ) # --- Controller callbacks --- + controller.add_publish_started_callback(self._on_publish_start) controller.add_publish_reset_callback(self._on_publish_reset) controller.add_instances_refresh_callback(self._on_instances_refresh) @@ -252,9 +254,15 @@ class OverviewWidget(QtWidgets.QFrame): # Force to change instance and refresh details self._on_subset_change() + def _on_publish_start(self): + """Publish started.""" + + self._subset_attributes_wrap.setEnabled(False) + def _on_publish_reset(self): """Context in controller has been refreshed.""" + self._subset_attributes_wrap.setEnabled(True) self._subset_content_widget.setEnabled(self._controller.host_is_valid) def _on_instances_refresh(self): From a2a686cfb86156b4bb12ed8256e572867a3e3a53 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 29 Sep 2022 12:38:50 +0200 Subject: [PATCH 059/275] added base of validation labels showed in different stages of publishing --- .../publisher/widgets/validations_widget.py | 136 ++++++++++++++++-- 1 file changed, 124 insertions(+), 12 deletions(-) diff --git a/openpype/tools/publisher/widgets/validations_widget.py b/openpype/tools/publisher/widgets/validations_widget.py index b70cd81878..b21cb3bb1b 100644 --- a/openpype/tools/publisher/widgets/validations_widget.py +++ b/openpype/tools/publisher/widgets/validations_widget.py @@ -414,15 +414,76 @@ class ValidationsWidget(QtWidgets.QFrame): β”‚ β”‚ Error detail β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ - β”œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€ - β”‚ Publish buttons β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β””β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”˜ """ def __init__(self, controller, parent): super(ValidationsWidget, self).__init__(parent) - errors_scroll = VerticallScrollArea(self) + # Before publishing + before_publish_widget = QtWidgets.QWidget(self) + before_publish_label = QtWidgets.QLabel( + "Nothing to report until you run publish", + before_publish_widget + ) + before_publish_label.setAlignment(QtCore.Qt.AlignCenter) + before_publish_layout = QtWidgets.QHBoxLayout(before_publish_widget) + before_publish_layout.setContentsMargins(0, 0, 0, 0) + before_publish_layout.addWidget( + before_publish_label, 1, QtCore.Qt.AlignCenter + ) + + # After success publishing + publish_started_widget = QtWidgets.QWidget(self) + publish_started_label = QtWidgets.QLabel( + "Publishing run smoothly", + publish_started_widget + ) + publish_started_label.setAlignment(QtCore.Qt.AlignCenter) + publish_started_layout = QtWidgets.QHBoxLayout( + publish_started_widget + ) + publish_started_layout.setContentsMargins(0, 0, 0, 0) + publish_started_layout.addWidget( + publish_started_label, 1, QtCore.Qt.AlignCenter + ) + + # After success publishing + publish_stop_ok_widget = QtWidgets.QWidget(self) + publish_stop_ok_label = QtWidgets.QLabel( + "Publishing finished successfully", + publish_stop_ok_widget + ) + publish_stop_ok_label.setAlignment(QtCore.Qt.AlignCenter) + publish_stop_ok_layout = QtWidgets.QHBoxLayout( + publish_stop_ok_widget + ) + publish_stop_ok_layout.setContentsMargins(0, 0, 0, 0) + publish_stop_ok_layout.addWidget( + publish_stop_ok_label, 1, QtCore.Qt.AlignCenter + ) + + # After failed publishing (not with validation error) + publish_stop_fail_widget = QtWidgets.QWidget(self) + publish_stop_fail_label = QtWidgets.QLabel( + "This is not your fault", + publish_stop_fail_widget + ) + publish_stop_fail_label.setAlignment(QtCore.Qt.AlignCenter) + publish_stop_fail_layout = QtWidgets.QHBoxLayout( + publish_stop_fail_widget + ) + publish_stop_fail_layout.setContentsMargins(0, 0, 0, 0) + publish_stop_fail_layout.addWidget( + publish_stop_fail_label, 1, QtCore.Qt.AlignCenter + ) + + # Validation errors + validations_widget = QtWidgets.QWidget(self) + + content_widget = QtWidgets.QWidget(validations_widget) + + errors_scroll = VerticallScrollArea(content_widget) errors_scroll.setWidgetResizable(True) errors_widget = QtWidgets.QWidget(errors_scroll) @@ -432,35 +493,58 @@ class ValidationsWidget(QtWidgets.QFrame): errors_scroll.setWidget(errors_widget) - error_details_frame = QtWidgets.QFrame(self) + error_details_frame = QtWidgets.QFrame(content_widget) error_details_input = QtWidgets.QTextEdit(error_details_frame) error_details_input.setObjectName("InfoText") error_details_input.setTextInteractionFlags( QtCore.Qt.TextBrowserInteraction ) - actions_widget = ValidateActionsWidget(controller, self) + actions_widget = ValidateActionsWidget(controller, content_widget) actions_widget.setMinimumWidth(140) error_details_layout = QtWidgets.QHBoxLayout(error_details_frame) error_details_layout.addWidget(error_details_input, 1) error_details_layout.addWidget(actions_widget, 0) - content_layout = QtWidgets.QHBoxLayout() + content_layout = QtWidgets.QHBoxLayout(content_widget) content_layout.setSpacing(0) content_layout.setContentsMargins(0, 0, 0, 0) content_layout.addWidget(errors_scroll, 0) content_layout.addWidget(error_details_frame, 1) - top_label = QtWidgets.QLabel("Publish validation report", self) + top_label = QtWidgets.QLabel( + "Publish validation report", content_widget + ) top_label.setObjectName("PublishInfoMainLabel") top_label.setAlignment(QtCore.Qt.AlignCenter) - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(top_label) - layout.addLayout(content_layout) + validation_layout = QtWidgets.QVBoxLayout(validations_widget) + validation_layout.setContentsMargins(0, 0, 0, 0) + validation_layout.addWidget(top_label, 0) + validation_layout.addWidget(content_widget, 1) + + main_layout = QtWidgets.QStackedLayout(self) + main_layout.addWidget(before_publish_widget) + main_layout.addWidget(publish_started_widget) + main_layout.addWidget(publish_stop_ok_widget) + main_layout.addWidget(publish_stop_fail_widget) + main_layout.addWidget(validations_widget) + + main_layout.setCurrentWidget(before_publish_widget) + + controller.add_publish_started_callback(self._on_publish_start) + controller.add_publish_reset_callback(self._on_publish_reset) + controller.add_publish_stopped_callback(self._on_publish_stop) + + self._main_layout = main_layout + + self._before_publish_widget = before_publish_widget + self._publish_started_widget = publish_started_widget + self._publish_stop_ok_widget = publish_stop_ok_widget + self._publish_stop_fail_widget = publish_stop_fail_widget + self._validations_widget = validations_widget self._top_label = top_label self._errors_widget = errors_widget @@ -473,6 +557,8 @@ class ValidationsWidget(QtWidgets.QFrame): self._error_info = {} self._previous_select = None + self._controller = controller + def clear(self): """Delete all dynamic widgets and hide all wrappers.""" self._title_widgets = {} @@ -536,6 +622,32 @@ class ValidationsWidget(QtWidgets.QFrame): self.updateGeometry() + def _set_current_widget(self, widget): + self._main_layout.setCurrentWidget(widget) + + def _on_publish_start(self): + self._set_current_widget(self._publish_started_widget) + + def _on_publish_reset(self): + self._set_current_widget(self._before_publish_widget) + + def _on_publish_stop(self): + if self._controller.publish_has_crashed: + self._set_current_widget(self._publish_stop_fail_widget) + return + + if self._controller.publish_has_validation_errors: + validation_errors = self._controller.get_validation_errors() + self._set_current_widget(self._validations_widget) + self.set_errors(validation_errors) + return + + if self._contoller.publish_has_finished: + self._set_current_widget(self._publish_stop_ok_widget) + return + + self._set_current_widget(self._publish_started_widget) + def _on_select(self, index): if self._previous_select: if self._previous_select.index == index: From e725246c31acb948c0075aff4c33a4cac7ebe921 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 29 Sep 2022 13:49:39 +0200 Subject: [PATCH 060/275] publish frame is showed in a different way --- openpype/style/style.css | 10 - .../tools/publisher/widgets/publish_widget.py | 188 ++++++------------ openpype/tools/publisher/widgets/widgets.py | 43 ++-- openpype/tools/publisher/window.py | 141 ++++++++++--- 4 files changed, 194 insertions(+), 188 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 1d112fa575..9dbc6b2adc 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -971,16 +971,6 @@ VariantInputsWidget QToolButton { color: {color:publisher:error}; } -#PublishFrame { - background: rgba(0, 0, 0, 127); -} -#PublishFrame[state="1"] { - background: rgb(22, 25, 29); -} -#PublishFrame[state="2"] { - background: {color:bg}; -} - #PublishInfoFrame { background: {color:bg}; border: 2px solid black; diff --git a/openpype/tools/publisher/widgets/publish_widget.py b/openpype/tools/publisher/widgets/publish_widget.py index ea23f9c42c..8a8c25e1b6 100644 --- a/openpype/tools/publisher/widgets/publish_widget.py +++ b/openpype/tools/publisher/widgets/publish_widget.py @@ -2,7 +2,7 @@ import os import json import time -from Qt import QtWidgets, QtCore, QtGui +from Qt import QtWidgets, QtCore from openpype.pipeline import KnownPublishError @@ -11,9 +11,7 @@ from .widgets import ( ResetBtn, ValidateBtn, PublishBtn, - CopyPublishReportBtn, - SavePublishReportBtn, - ShowPublishReportBtn + PublishReportBtn, ) @@ -66,7 +64,7 @@ class ActionsButton(QtWidgets.QToolButton): self._set_action(action) -class PublishFrame(QtWidgets.QFrame): +class PublishFrame(QtWidgets.QWidget): """Frame showed during publishing. Shows all information related to publishing. Contains validation error @@ -77,65 +75,49 @@ class PublishFrame(QtWidgets.QFrame): only when publishing process is stopped and must be manually triggered to change into that layer. - +------------------------------------------------------------------------+ - | | - | | - | | - | < Validation error widget > | - | | - | | - | | - | | +------------------------------------------------------------------------+ | < Main label > | | < Label top > | | (#### 10% ) | | | - | Report: