From 54e2687afa13b4963b86ecbc4b99786e1a57ced2 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 18 Jul 2023 10:21:21 +0300 Subject: [PATCH 01/70] create review validator --- .../publish/validate_review_colorspace.py | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py new file mode 100644 index 0000000000..18ca39234c --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +import pyblish.api + +from collections import defaultdict +from openpype.pipeline import PublishValidationError + + +class ValidateReviewColorspace(pyblish.api.InstancePlugin): + """Validate Review Colorspace parameters. + + + """ + + order = pyblish.api.ValidatorOrder + 0.1 + families = ["review"] + hosts = ["houdini"] + label = "Validate Review Colorspace" + + def process(self, instance): + invalid = self.get_invalid(instance) + if invalid: + raise PublishValidationError( + ("Colorspace parameter is not valid."), + title=self.label + ) + + @classmethod + def get_invalid(cls, instance): + import hou # noqa + output_node = instance.data.get("output_node") + rop_node = hou.node(instance.data["instance_node"]) + if output_node is None: + cls.log.error( + "SOP Output node in '%s' does not exist. " + "Ensure a valid SOP output path is set." % rop_node.path() + ) + + return [rop_node.path()] + + pattern = rop_node.parm("prim_to_detail_pattern").eval().strip() + if not pattern: + cls.log.debug( + "Alembic ROP has no 'Primitive to Detail' pattern. " + "Validation is ignored.." + ) + return + + build_from_path = rop_node.parm("build_from_path").eval() + if not build_from_path: + cls.log.debug( + "Alembic ROP has 'Build from Path' disabled. " + "Validation is ignored.." + ) + return + + path_attr = rop_node.parm("path_attrib").eval() + if not path_attr: + cls.log.error( + "The Alembic ROP node has no Path Attribute" + "value set, but 'Build Hierarchy from Attribute'" + "is enabled." + ) + return [rop_node.path()] + + # Let's assume each attribute is explicitly named for now and has no + # wildcards for Primitive to Detail. This simplifies the check. + cls.log.debug("Checking Primitive to Detail pattern: %s" % pattern) + cls.log.debug("Checking with path attribute: %s" % path_attr) + + if not hasattr(output_node, "geometry"): + # In the case someone has explicitly set an Object + # node instead of a SOP node in Geometry context + # then for now we ignore - this allows us to also + # export object transforms. + cls.log.warning("No geometry output node found, skipping check..") + return + + # Check if the primitive attribute exists + frame = instance.data.get("frameStart", 0) + geo = output_node.geometryAtFrame(frame) + + # If there are no primitives on the start frame then it might be + # something that is emitted over time. As such we can't actually + # validate whether the attributes exist, because they won't exist + # yet. In that case, just warn the user and allow it. + if len(geo.iterPrims()) == 0: + cls.log.warning( + "No primitives found on current frame. Validation" + " for Primitive to Detail will be skipped." + ) + return + + attrib = geo.findPrimAttrib(path_attr) + if not attrib: + cls.log.info( + "Geometry Primitives are missing " + "path attribute: `%s`" % path_attr + ) + return [output_node.path()] + + # Ensure at least a single string value is present + if not attrib.strings(): + cls.log.info( + "Primitive path attribute has no " + "string values: %s" % path_attr + ) + return [output_node.path()] + + paths = None + for attr in pattern.split(" "): + if not attr.strip(): + # Ignore empty values + continue + + # Check if the primitive attribute exists + attrib = geo.findPrimAttrib(attr) + if not attrib: + # It is allowed to not have the attribute at all + continue + + # The issue can only happen if at least one string attribute is + # present. So we ignore cases with no values whatsoever. + if not attrib.strings(): + continue + + check = defaultdict(set) + values = geo.primStringAttribValues(attr) + if paths is None: + paths = geo.primStringAttribValues(path_attr) + + for path, value in zip(paths, values): + check[path].add(value) + + for path, values in check.items(): + # Whenever a single path has multiple values for the + # Primitive to Detail attribute then we consider it + # inconsistent and invalidate the ROP node's content. + if len(values) > 1: + cls.log.warning( + "Path has multiple values: %s (path: %s)" + % (list(values), path) + ) + return [output_node.path()] From 94ec68ab21182bbb01258a9e73ccb01b10a5a1e8 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 19 Jul 2023 00:13:51 +0300 Subject: [PATCH 02/70] update review validator --- .../publish/validate_review_colorspace.py | 153 ++++++------------ 1 file changed, 49 insertions(+), 104 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 18ca39234c..47c1e886d1 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -1,143 +1,88 @@ # -*- coding: utf-8 -*- import pyblish.api - -from collections import defaultdict from openpype.pipeline import PublishValidationError +from openpype.pipeline.publish import RepairAction +from openpype.hosts.houdini.api.action import SelectROPAction + + +class SetDefaultViewSpaceAction(RepairAction): + label = "Set default view space" + icon = "mdi.monitor" class ValidateReviewColorspace(pyblish.api.InstancePlugin): """Validate Review Colorspace parameters. - + It checks if 'OCIO Colorspace' parameter was set to valid value. """ order = pyblish.api.ValidatorOrder + 0.1 families = ["review"] hosts = ["houdini"] label = "Validate Review Colorspace" + actions = [SetDefaultViewSpaceAction, SelectROPAction] def process(self, instance): invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( - ("Colorspace parameter is not valid."), + ("'OCIO Colorspace' parameter is not valid."), title=self.label ) @classmethod def get_invalid(cls, instance): import hou # noqa - output_node = instance.data.get("output_node") + rop_node = hou.node(instance.data["instance_node"]) - if output_node is None: - cls.log.error( - "SOP Output node in '%s' does not exist. " - "Ensure a valid SOP output path is set." % rop_node.path() - ) - - return [rop_node.path()] - - pattern = rop_node.parm("prim_to_detail_pattern").eval().strip() - if not pattern: - cls.log.debug( - "Alembic ROP has no 'Primitive to Detail' pattern. " - "Validation is ignored.." - ) - return - - build_from_path = rop_node.parm("build_from_path").eval() - if not build_from_path: - cls.log.debug( - "Alembic ROP has 'Build from Path' disabled. " - "Validation is ignored.." - ) - return - - path_attr = rop_node.parm("path_attrib").eval() - if not path_attr: - cls.log.error( - "The Alembic ROP node has no Path Attribute" - "value set, but 'Build Hierarchy from Attribute'" - "is enabled." - ) - return [rop_node.path()] - - # Let's assume each attribute is explicitly named for now and has no - # wildcards for Primitive to Detail. This simplifies the check. - cls.log.debug("Checking Primitive to Detail pattern: %s" % pattern) - cls.log.debug("Checking with path attribute: %s" % path_attr) - - if not hasattr(output_node, "geometry"): - # In the case someone has explicitly set an Object - # node instead of a SOP node in Geometry context - # then for now we ignore - this allows us to also - # export object transforms. - cls.log.warning("No geometry output node found, skipping check..") - return - - # Check if the primitive attribute exists - frame = instance.data.get("frameStart", 0) - geo = output_node.geometryAtFrame(frame) - - # If there are no primitives on the start frame then it might be - # something that is emitted over time. As such we can't actually - # validate whether the attributes exist, because they won't exist - # yet. In that case, just warn the user and allow it. - if len(geo.iterPrims()) == 0: + if hou.Color.ocio_defaultDisplay() == "default": cls.log.warning( - "No primitives found on current frame. Validation" - " for Primitive to Detail will be skipped." + "Default Houdini colorspace is used, " + " skipping check.." ) return - attrib = geo.findPrimAttrib(path_attr) - if not attrib: - cls.log.info( - "Geometry Primitives are missing " - "path attribute: `%s`" % path_attr + if rop_node.evalParm("colorcorrect") != 2: + # any colorspace settings other than default requires + # 'Color Correct' parm to be set to 'OpenColorIO' + rop_node.setParms({"colorcorrect": 2}) + cls.log.debug( + "'Color Correct' parm on '%s' has been set to" + " 'OpenColorIO'", rop_node ) - return [output_node.path()] - # Ensure at least a single string value is present - if not attrib.strings(): - cls.log.info( - "Primitive path attribute has no " - "string values: %s" % path_attr + if rop_node.evalParm("ociocolorspace") not in \ + hou.Color.ocio_spaces(): + + cls.log.error( + "'OCIO Colorspace' value on '%s' is not valid, " + "select a valid option from the dropdown menu.", + rop_node ) - return [output_node.path()] + return rop_node - paths = None - for attr in pattern.split(" "): - if not attr.strip(): - # Ignore empty values - continue + @classmethod + def repair(cls, instance): + """Set Default View Space Action. - # Check if the primitive attribute exists - attrib = geo.findPrimAttrib(attr) - if not attrib: - # It is allowed to not have the attribute at all - continue + It is a helper action more than a repair action, + used to set colorspace on opengl node to the default view. + """ - # The issue can only happen if at least one string attribute is - # present. So we ignore cases with no values whatsoever. - if not attrib.strings(): - continue + import hou + import PyOpenColorIO as OCIO - check = defaultdict(set) - values = geo.primStringAttribValues(attr) - if paths is None: - paths = geo.primStringAttribValues(path_attr) + rop_node = hou.node(instance.data["instance_node"]) - for path, value in zip(paths, values): - check[path].add(value) + config = OCIO.GetCurrentConfig() + display = hou.Color.ocio_defaultDisplay() + view = hou.Color.ocio_defaultView() - for path, values in check.items(): - # Whenever a single path has multiple values for the - # Primitive to Detail attribute then we consider it - # inconsistent and invalidate the ROP node's content. - if len(values) > 1: - cls.log.warning( - "Path has multiple values: %s (path: %s)" - % (list(values), path) - ) - return [output_node.path()] + default_view_space = config.getDisplayColorSpaceName( + display, view) + + rop_node.setParms({"ociocolorspace" : default_view_space}) + cls.log.debug( + "'OCIO Colorspace' parm on '%s' has been set to '%s'", + default_view_space, rop_node + ) From 4d79320cf4f1abdd05dc3ce6fc46d6ccb30b2aca Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 19 Jul 2023 00:50:43 +0300 Subject: [PATCH 03/70] make hound happy --- .../hosts/houdini/plugins/publish/validate_review_colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 47c1e886d1..02284dc641 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -81,7 +81,7 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): default_view_space = config.getDisplayColorSpaceName( display, view) - rop_node.setParms({"ociocolorspace" : default_view_space}) + rop_node.setParms({"ociocolorspace": default_view_space}) cls.log.debug( "'OCIO Colorspace' parm on '%s' has been set to '%s'", default_view_space, rop_node From f4a5858edbed465f11908d18697b5dc0c6414b76 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 19 Jul 2023 19:43:39 +0300 Subject: [PATCH 04/70] update validator --- .../houdini/plugins/publish/validate_review_colorspace.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 02284dc641..8f3799cde4 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -33,9 +33,10 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): import hou # noqa + import os rop_node = hou.node(instance.data["instance_node"]) - if hou.Color.ocio_defaultDisplay() == "default": + if os.getenv("OCIO") is None: cls.log.warning( "Default Houdini colorspace is used, " " skipping check.." @@ -78,8 +79,8 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): display = hou.Color.ocio_defaultDisplay() view = hou.Color.ocio_defaultView() - default_view_space = config.getDisplayColorSpaceName( - display, view) + default_view_space = config.getDisplayViewColorSpaceName( + display, view) # works with PyOpenColorIO 2.2.1 rop_node.setParms({"ociocolorspace": default_view_space}) cls.log.debug( From 00d4afd1178d4ee95ee8d09a4a8c8348f3948606 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 19 Jul 2023 19:44:45 +0300 Subject: [PATCH 05/70] make hound happy --- .../hosts/houdini/plugins/publish/validate_review_colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 8f3799cde4..addfa05bf1 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -80,7 +80,7 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): view = hou.Color.ocio_defaultView() default_view_space = config.getDisplayViewColorSpaceName( - display, view) # works with PyOpenColorIO 2.2.1 + display, view) # works with PyOpenColorIO 2.2.1 rop_node.setParms({"ociocolorspace": default_view_space}) cls.log.debug( From d90d45c56df70e876d02acbf9d4e09aa1b6f5746 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 20 Jul 2023 13:38:09 +0300 Subject: [PATCH 06/70] move get function to colorspace.py --- .../publish/validate_review_colorspace.py | 19 +++++--- openpype/pipeline/colorspace.py | 36 ++++++++++++++++ openpype/scripts/ocio_wrapper.py | 43 +++++++++++++++++++ 3 files changed, 91 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index addfa05bf1..cfc5a5d71d 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -71,19 +71,24 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): """ import hou - import PyOpenColorIO as OCIO + from openpype.pipeline.colorspace import get_display_view_colorspace_name + from openpype.hosts.houdini.api.lib import get_color_management_preferences #noqa rop_node = hou.node(instance.data["instance_node"]) - config = OCIO.GetCurrentConfig() - display = hou.Color.ocio_defaultDisplay() - view = hou.Color.ocio_defaultView() + data = get_color_management_preferences() + config_path = data.get("config") + display = data.get("display") + view = data.get("view") - default_view_space = config.getDisplayViewColorSpaceName( - display, view) # works with PyOpenColorIO 2.2.1 + cls.log.debug("Get default view colorspace name..") + + default_view_space = get_display_view_colorspace_name(config_path, + display, view) rop_node.setParms({"ociocolorspace": default_view_space}) cls.log.debug( - "'OCIO Colorspace' parm on '%s' has been set to '%s'", + "'OCIO Colorspace' parm on '%s' has been set to " + "the default view color space '%s'", default_view_space, rop_node ) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 3f2d4891c1..a1d86b2fec 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -589,3 +589,39 @@ def _get_imageio_settings(project_settings, host_name): imageio_host = project_settings.get(host_name, {}).get("imageio", {}) return imageio_global, imageio_host + +def get_display_view_colorspace_name(config_path, display, view): + + if not compatibility_check(): + # python environment is not compatible with PyOpenColorIO + # needs to be run in subprocess + return get_display_view_colorspace_subprocess(config_path, + display, view) + + from openpype.scripts.ocio_wrapper import _get_display_view_colorspace_name #noqa + + return _get_display_view_colorspace_name(config_path, display, view) + +def get_display_view_colorspace_subprocess(config_path, display, view): + with _make_temp_json_file() as tmp_json_path: + # Prepare subprocess arguments + args = [ + "run", get_ocio_config_script_path(), + "config", "get_display_view_colorspace_name", + "--in_path", config_path, + "--out_path", tmp_json_path, + "--display", display, + "--view", view + + ] + log.info("Executing: {}".format(" ".join(args))) + + process_kwargs = { + "logger": log + } + + run_openpype_process(*args, **process_kwargs) + + # return all colorspaces + return_json_data = open(tmp_json_path).read() + return json.loads(return_json_data) diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index 16558642c6..ca703fd65c 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -173,6 +173,49 @@ def _get_views_data(config_path): return data +def _get_display_view_colorspace_name(config_path, display, view): + config_path = Path(config_path) + + if not config_path.is_file(): + raise IOError("Input path should be `config.ocio` file") + + config = ocio.Config().CreateFromFile(str(config_path)) + colorspace = config.getDisplayViewColorSpaceName(display, view) + + return colorspace + +@config.command( + name="get_display_view_colorspace_name", + help=( + "return default view colorspace name " + "for the given display and view " + "--path input arg is required" + ) +) +@click.option("--in_path", required=True, + help="path where to read ocio config file", + type=click.Path(exists=True)) +@click.option("--out_path", required=True, + help="path where to write output json file", + type=click.Path()) +@click.option("--display", required=True, + help="display", + type=click.STRING) +@click.option("--view", required=True, + help="view", + type=click.STRING) +def get_display_view_colorspace_name(in_path, out_path, + display, view): + + json_path = Path(out_path) + + out_data = _get_display_view_colorspace_name(in_path, + display, view) + + with open(json_path, "w") as f: + json.dump(out_data, f) + + print(f"Viewer data are saved to '{json_path}'") if __name__ == '__main__': main() From 2bb11d956ca2ec629f61d5d14e0b2ef4130a0363 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 20 Jul 2023 13:54:46 +0300 Subject: [PATCH 07/70] add doc strings --- .../publish/validate_review_colorspace.py | 2 +- openpype/pipeline/colorspace.py | 22 ++++++++++++ openpype/scripts/ocio_wrapper.py | 34 +++++++++++++++++-- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index cfc5a5d71d..67e29e0ee2 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -71,7 +71,7 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): """ import hou - from openpype.pipeline.colorspace import get_display_view_colorspace_name + from openpype.pipeline.colorspace import get_display_view_colorspace_name #noqa from openpype.hosts.houdini.api.lib import get_color_management_preferences #noqa rop_node = hou.node(instance.data["instance_node"]) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index a1d86b2fec..22e8175a7e 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -591,6 +591,16 @@ def _get_imageio_settings(project_settings, host_name): return imageio_global, imageio_host def get_display_view_colorspace_name(config_path, display, view): + """get view colorspace name for the given display and view. + + Args: + config_path (str): path string leading to config.ocio + display (str): display name e.g. "ACES" + view (str): view name e.g. "sRGB" + + Returns: + view color space name (str) e.g. "Output - sRGB" + """ if not compatibility_check(): # python environment is not compatible with PyOpenColorIO @@ -603,6 +613,18 @@ def get_display_view_colorspace_name(config_path, display, view): return _get_display_view_colorspace_name(config_path, display, view) def get_display_view_colorspace_subprocess(config_path, display, view): + """get view colorspace name for the given display and view + via subprocess. + + Args: + config_path (str): path string leading to config.ocio + display (str): display name e.g. "ACES" + view (str): view name e.g. "sRGB" + + Returns: + view color space name (str) e.g. "Output - sRGB" + """ + with _make_temp_json_file() as tmp_json_path: # Prepare subprocess arguments args = [ diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index ca703fd65c..f94faabe11 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -174,6 +174,21 @@ def _get_views_data(config_path): return data def _get_display_view_colorspace_name(config_path, display, view): + """get view colorspace name for the given display and view. + + Args: + config_path (str): path string leading to config.ocio + display (str): display name e.g. "ACES" + view (str): view name e.g. "sRGB" + + + Raises: + IOError: Input config does not exist. + + Returns: + view color space name (str) e.g. "Output - sRGB" + """ + config_path = Path(config_path) if not config_path.is_file(): @@ -199,13 +214,28 @@ def _get_display_view_colorspace_name(config_path, display, view): help="path where to write output json file", type=click.Path()) @click.option("--display", required=True, - help="display", + help="display name", type=click.STRING) @click.option("--view", required=True, - help="view", + help="view name", type=click.STRING) def get_display_view_colorspace_name(in_path, out_path, display, view): + """Aggregate view colorspace name to file. + + Python 2 wrapped console command + + Args: + in_path (str): config file path string + out_path (str): temp json file path string + display (str): display name e.g. "ACES" + view (str): view name e.g. "sRGB" + + Example of use: + > pyton.exe ./ocio_wrapper.py config \ + get_display_view_colorspace_name --in_path= \ + --out_path= --display= --view= + """ json_path = Path(out_path) From c8e66fd632323416a63c0c9d1bf248e516f2c2be Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 9 Aug 2023 20:34:05 +0300 Subject: [PATCH 08/70] resolve some conversations --- .../publish/validate_review_colorspace.py | 16 ++++++++-------- openpype/pipeline/colorspace.py | 4 +++- openpype/scripts/ocio_wrapper.py | 4 +++- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 67e29e0ee2..e493349946 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -4,6 +4,9 @@ from openpype.pipeline import PublishValidationError from openpype.pipeline.publish import RepairAction from openpype.hosts.houdini.api.action import SelectROPAction +import os +import hou + class SetDefaultViewSpaceAction(RepairAction): label = "Set default view space" @@ -32,8 +35,6 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - import hou # noqa - import os rop_node = hou.node(instance.data["instance_node"]) if os.getenv("OCIO") is None: @@ -70,16 +71,15 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): used to set colorspace on opengl node to the default view. """ - import hou - from openpype.pipeline.colorspace import get_display_view_colorspace_name #noqa - from openpype.hosts.houdini.api.lib import get_color_management_preferences #noqa + from openpype.pipeline.colorspace import get_display_view_colorspace_name # noqa + from openpype.hosts.houdini.api.lib import get_color_management_preferences # noqa rop_node = hou.node(instance.data["instance_node"]) - data = get_color_management_preferences() + data = get_color_management_preferences() config_path = data.get("config") - display = data.get("display") - view = data.get("view") + display = data.get("display") + view = data.get("view") cls.log.debug("Get default view colorspace name..") diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 22e8175a7e..d84424270c 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -590,6 +590,7 @@ def _get_imageio_settings(project_settings, host_name): return imageio_global, imageio_host + def get_display_view_colorspace_name(config_path, display, view): """get view colorspace name for the given display and view. @@ -608,10 +609,11 @@ def get_display_view_colorspace_name(config_path, display, view): return get_display_view_colorspace_subprocess(config_path, display, view) - from openpype.scripts.ocio_wrapper import _get_display_view_colorspace_name #noqa + from openpype.scripts.ocio_wrapper import _get_display_view_colorspace_name # noqa return _get_display_view_colorspace_name(config_path, display, view) + def get_display_view_colorspace_subprocess(config_path, display, view): """get view colorspace name for the given display and view via subprocess. diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index f94faabe11..556568ce20 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -173,6 +173,7 @@ def _get_views_data(config_path): return data + def _get_display_view_colorspace_name(config_path, display, view): """get view colorspace name for the given display and view. @@ -199,6 +200,7 @@ def _get_display_view_colorspace_name(config_path, display, view): return colorspace + @config.command( name="get_display_view_colorspace_name", help=( @@ -223,7 +225,7 @@ def get_display_view_colorspace_name(in_path, out_path, display, view): """Aggregate view colorspace name to file. - Python 2 wrapped console command + Wrapper command for processes without acces to OpenColorIO Args: in_path (str): config file path string From 877facc5b89317a0fc541d2f06a9d9611c9e1acb Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 9 Aug 2023 21:56:42 +0300 Subject: [PATCH 09/70] set default colorspace on creation if there's OCIO --- .../houdini/plugins/create/create_review.py | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_review.py b/openpype/hosts/houdini/plugins/create/create_review.py index ab06b30c35..797116aaca 100644 --- a/openpype/hosts/houdini/plugins/create/create_review.py +++ b/openpype/hosts/houdini/plugins/create/create_review.py @@ -3,6 +3,9 @@ from openpype.hosts.houdini.api import plugin from openpype.lib import EnumDef, BoolDef, NumberDef +import os +import hou + class CreateReview(plugin.HoudiniCreator): """Review with OpenGL ROP""" @@ -13,7 +16,6 @@ class CreateReview(plugin.HoudiniCreator): icon = "video-camera" def create(self, subset_name, instance_data, pre_create_data): - import hou instance_data.pop("active", None) instance_data.update({"node_type": "opengl"}) @@ -82,6 +84,10 @@ class CreateReview(plugin.HoudiniCreator): instance_node.setParms(parms) + # Set OCIO Colorspace to the default output colorspace + # if there's OCIO + self.set_colorcorrect_to_default_view_space(instance_node) + to_lock = ["id", "family"] self.lock_parameters(instance_node, to_lock) @@ -123,3 +129,46 @@ class CreateReview(plugin.HoudiniCreator): minimum=0.0001, decimals=3) ] + + def set_colorcorrect_to_default_view_space(self, + instance_node): + """Set ociocolorspace to the default output space.""" + + if os.getenv("OCIO") is None: + # No OCIO, skip setting ociocolorspace + return + + # if there's OCIO then set Color Correction parameter + # to OpenColorIO + instance_node.setParms({"colorcorrect": 2}) + + self.log.debug("Get default view colorspace name..") + + default_view_space = self.get_default_view_space() + instance_node.setParms( + {"ociocolorspace": default_view_space} + ) + + self.log.debug( + "'OCIO Colorspace' parm on '{}' has been set to " + "the default view color space '{}'" + .format(instance_node, default_view_space) + ) + + return default_view_space + + def get_default_view_space(self): + """Get default view space for ociocolorspace parm.""" + + from openpype.pipeline.colorspace import get_display_view_colorspace_name # noqa + from openpype.hosts.houdini.api.lib import get_color_management_preferences # noqa + + data = get_color_management_preferences() + config_path = data.get("config") + display = data.get("display") + view = data.get("view") + + default_view_space = get_display_view_colorspace_name(config_path, + display, view) + + return default_view_space From cff92425676acef62fc6b8489517e66286072925 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 9 Aug 2023 21:57:57 +0300 Subject: [PATCH 10/70] use .format instead of %s --- .../publish/validate_review_colorspace.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index e493349946..5390b6b52f 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -49,17 +49,17 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): # 'Color Correct' parm to be set to 'OpenColorIO' rop_node.setParms({"colorcorrect": 2}) cls.log.debug( - "'Color Correct' parm on '%s' has been set to" - " 'OpenColorIO'", rop_node + "'Color Correct' parm on '{}' has been set to" + " 'OpenColorIO'".format(rop_node) ) if rop_node.evalParm("ociocolorspace") not in \ hou.Color.ocio_spaces(): cls.log.error( - "'OCIO Colorspace' value on '%s' is not valid, " - "select a valid option from the dropdown menu.", - rop_node + "'OCIO Colorspace' value on '{}' is not valid, " + "select a valid option from the dropdown menu." + .format(rop_node) ) return rop_node @@ -88,7 +88,8 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): rop_node.setParms({"ociocolorspace": default_view_space}) cls.log.debug( - "'OCIO Colorspace' parm on '%s' has been set to " - "the default view color space '%s'", - default_view_space, rop_node + "'OCIO Colorspace' parm on '{}' has been set to " + "the default view color space '{}'" + .formate(rop_node, default_view_space) + ) From 256ffee407e8718ca6099c5f7185d0ec88a0ace7 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 14 Aug 2023 15:54:14 +0300 Subject: [PATCH 11/70] resolve some conversations --- openpype/pipeline/colorspace.py | 2 +- openpype/scripts/ocio_wrapper.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 392089237b..a0efb5e18c 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -604,7 +604,7 @@ def _get_imageio_settings(project_settings, host_name): def get_display_view_colorspace_name(config_path, display, view): - """get view colorspace name for the given display and view. + """Return colorspace name for the given display and view. Args: config_path (str): path string leading to config.ocio diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index 556568ce20..e491206ebb 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -247,7 +247,7 @@ def get_display_view_colorspace_name(in_path, out_path, with open(json_path, "w") as f: json.dump(out_data, f) - print(f"Viewer data are saved to '{json_path}'") + print(f"Display view colorspace saved to '{json_path}'") if __name__ == '__main__': main() From 499b4623a30a0b4d7d900683d119394f27304540 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Aug 2023 15:41:21 +0200 Subject: [PATCH 12/70] adding abstraction of publishing related functions --- openpype/pipeline/colorspace.py | 153 ++++++++++++++++++++++++++++++-- 1 file changed, 144 insertions(+), 9 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 731132911a..649d355f62 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -13,12 +13,17 @@ from openpype.lib import ( Logger ) from openpype.pipeline import Anatomy +from openpype.lib.transcoding import VIDEO_EXTENSIONS, IMAGE_EXTENSIONS + log = Logger.get_logger(__name__) -class CashedData: +class CachedData: remapping = None + allowed_exts = { + ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS) + } @contextlib.contextmanager @@ -546,15 +551,15 @@ def get_remapped_colorspace_to_native( Union[str, None]: native colorspace name defined in remapping or None """ - CashedData.remapping.setdefault(host_name, {}) - if CashedData.remapping[host_name].get("to_native") is None: + CachedData.remapping.setdefault(host_name, {}) + if CachedData.remapping[host_name].get("to_native") is None: remapping_rules = imageio_host_settings["remapping"]["rules"] - CashedData.remapping[host_name]["to_native"] = { + CachedData.remapping[host_name]["to_native"] = { rule["ocio_name"]: rule["host_native_name"] for rule in remapping_rules } - return CashedData.remapping[host_name]["to_native"].get( + return CachedData.remapping[host_name]["to_native"].get( ocio_colorspace_name) @@ -572,15 +577,15 @@ def get_remapped_colorspace_from_native( Union[str, None]: Ocio colorspace name defined in remapping or None. """ - CashedData.remapping.setdefault(host_name, {}) - if CashedData.remapping[host_name].get("from_native") is None: + CachedData.remapping.setdefault(host_name, {}) + if CachedData.remapping[host_name].get("from_native") is None: remapping_rules = imageio_host_settings["remapping"]["rules"] - CashedData.remapping[host_name]["from_native"] = { + CachedData.remapping[host_name]["from_native"] = { rule["host_native_name"]: rule["ocio_name"] for rule in remapping_rules } - return CashedData.remapping[host_name]["from_native"].get( + return CachedData.remapping[host_name]["from_native"].get( host_native_colorspace_name) @@ -601,3 +606,133 @@ def _get_imageio_settings(project_settings, host_name): imageio_host = project_settings.get(host_name, {}).get("imageio", {}) return imageio_global, imageio_host + + +def get_colorspace_settings_from_publish_context(context_data): + """Returns solved settings for the host context. + + Args: + context_data (publish.Context.data): publishing context data + + Returns: + tuple | bool: config, file rules or None + """ + if "imageioSettings" in context_data: + return context_data["imageioSettings"] + + project_name = context_data["projectName"] + host_name = context_data["hostName"] + anatomy_data = context_data["anatomyData"] + project_settings_ = context_data["project_settings"] + + config_data = get_imageio_config( + project_name, host_name, + project_settings=project_settings_, + anatomy_data=anatomy_data + ) + + # in case host color management is not enabled + if not config_data: + return None + + file_rules = get_imageio_file_rules( + project_name, host_name, + project_settings=project_settings_ + ) + + # caching settings for future instance processing + context_data["imageioSettings"] = (config_data, file_rules) + + return config_data, file_rules + + +def set_colorspace_data_to_representation( + representation, context_data, + colorspace=None, + colorspace_settings=None, + log=None +): + """Sets colorspace data to representation. + + Args: + representation (dict): publishing representation + context_data (publish.Context.data): publishing context data + config_data (dict): host resolved config data + file_rules (dict): host resolved file rules data + colorspace (str, optional): colorspace name. Defaults to None. + colorspace_settings (tuple[dict, dict], optional): + Settings for config_data and file_rules. + Defaults to None. + log (logging.Logger, optional): logger instance. Defaults to None. + + Example: + ``` + { + # for other publish plugins and loaders + "colorspace": "linear", + "config": { + # for future references in case need + "path": "/abs/path/to/config.ocio", + # for other plugins within remote publish cases + "template": "{project[root]}/path/to/config.ocio" + } + } + ``` + + """ + log = log or Logger.get_logger(__name__) + + file_ext = representation["ext"] + + # check if `file_ext` in lower case is in CachedData.allowed_exts + if file_ext.lstrip(".").lower() not in CachedData.allowed_exts: + log.debug( + "Extension '{}' is not in allowed extensions.".format(file_ext) + ) + return + + if colorspace_settings is None: + colorspace_settings = get_colorspace_settings_from_publish_context( + context_data) + + # in case host color management is not enabled + if not colorspace_settings: + log.warning("Host's colorspace management is disabled.") + return + + # unpack colorspace settings + config_data, file_rules = colorspace_settings + + if not config_data: + # warn in case no colorspace path was defined + log.warning("No colorspace management was defined") + return + + log.debug("Config data is: `{}`".format(config_data)) + + project_name = context_data["projectName"] + host_name = context_data["hostName"] + project_settings = context_data["project_settings"] + + # get one filename + filename = representation["files"] + if isinstance(filename, list): + filename = filename[0] + + # get matching colorspace from rules + colorspace = colorspace or get_imageio_colorspace_from_filepath( + filename, host_name, project_name, + config_data=config_data, + file_rules=file_rules, + project_settings=project_settings + ) + + # infuse data to representation + if colorspace: + colorspace_data = { + "colorspace": colorspace, + "config": config_data + } + + # update data key + representation["colorspaceData"] = colorspace_data From 8260eb36bdd3e1d4cb8590f59853099b11dd75a8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Aug 2023 16:39:12 +0200 Subject: [PATCH 13/70] implementing abstarctions from colorspace --- openpype/pipeline/publish/publish_plugins.py | 106 ++----------------- 1 file changed, 10 insertions(+), 96 deletions(-) diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index ba3be6397e..17ede069cb 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -1,6 +1,5 @@ import inspect from abc import ABCMeta -from pprint import pformat import pyblish.api from pyblish.plugin import MetaPlugin, ExplicitMetaPlugin from openpype.lib.transcoding import VIDEO_EXTENSIONS, IMAGE_EXTENSIONS @@ -14,9 +13,8 @@ from .lib import ( ) from openpype.pipeline.colorspace import ( - get_imageio_colorspace_from_filepath, - get_imageio_config, - get_imageio_file_rules + get_colorspace_settings_from_publish_context, + set_colorspace_data_to_representation ) @@ -306,12 +304,8 @@ class ColormanagedPyblishPluginMixin(object): matching colorspace from rules. Finally, it infuses this data into the representation. """ - allowed_ext = set( - ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS) - ) - @staticmethod - def get_colorspace_settings(context): + def get_colorspace_settings(self, context): """Returns solved settings for the host context. Args: @@ -320,33 +314,7 @@ class ColormanagedPyblishPluginMixin(object): Returns: tuple | bool: config, file rules or None """ - if "imageioSettings" in context.data: - return context.data["imageioSettings"] - - project_name = context.data["projectName"] - host_name = context.data["hostName"] - anatomy_data = context.data["anatomyData"] - project_settings_ = context.data["project_settings"] - - config_data = get_imageio_config( - project_name, host_name, - project_settings=project_settings_, - anatomy_data=anatomy_data - ) - - # in case host color management is not enabled - if not config_data: - return None - - file_rules = get_imageio_file_rules( - project_name, host_name, - project_settings=project_settings_ - ) - - # caching settings for future instance processing - context.data["imageioSettings"] = (config_data, file_rules) - - return config_data, file_rules + return get_colorspace_settings_from_publish_context(context.data) def set_representation_colorspace( self, representation, context, @@ -380,64 +348,10 @@ class ColormanagedPyblishPluginMixin(object): ``` """ - ext = representation["ext"] - # check extension - self.log.debug("__ ext: `{}`".format(ext)) - - # check if ext in lower case is in self.allowed_ext - if ext.lstrip(".").lower() not in self.allowed_ext: - self.log.debug( - "Extension '{}' is not in allowed extensions.".format(ext) - ) - return - - if colorspace_settings is None: - colorspace_settings = self.get_colorspace_settings(context) - - # in case host color management is not enabled - if not colorspace_settings: - self.log.warning("Host's colorspace management is disabled.") - return - - # unpack colorspace settings - config_data, file_rules = colorspace_settings - - if not config_data: - # warn in case no colorspace path was defined - self.log.warning("No colorspace management was defined") - return - - self.log.debug("Config data is: `{}`".format(config_data)) - - project_name = context.data["projectName"] - host_name = context.data["hostName"] - project_settings = context.data["project_settings"] - - # get one filename - filename = representation["files"] - if isinstance(filename, list): - filename = filename[0] - - self.log.debug("__ filename: `{}`".format(filename)) - - # get matching colorspace from rules - colorspace = colorspace or get_imageio_colorspace_from_filepath( - filename, host_name, project_name, - config_data=config_data, - file_rules=file_rules, - project_settings=project_settings + # using cached settings if available + set_colorspace_data_to_representation( + representation, context.data, + colorspace, + colorspace_settings, + log=self.log ) - self.log.debug("__ colorspace: `{}`".format(colorspace)) - - # infuse data to representation - if colorspace: - colorspace_data = { - "colorspace": colorspace, - "config": config_data - } - - # update data key - representation["colorspaceData"] = colorspace_data - - self.log.debug("__ colorspace_data: `{}`".format( - pformat(colorspace_data))) From 0f904cb32a0b7283cafd344f0b33fd7b95e6bda7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 28 Aug 2023 17:23:19 +0200 Subject: [PATCH 14/70] adding default sequence frame data --- openpype/plugins/publish/collect_sequence_frame_data.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/plugins/publish/collect_sequence_frame_data.py b/openpype/plugins/publish/collect_sequence_frame_data.py index c200b245e9..241e7b9011 100644 --- a/openpype/plugins/publish/collect_sequence_frame_data.py +++ b/openpype/plugins/publish/collect_sequence_frame_data.py @@ -50,4 +50,7 @@ class CollectSequenceFrameData(pyblish.api.InstancePlugin): return { "frameStart": repres_frames[0], "frameEnd": repres_frames[-1], + "handleStart": 0, + "handleEnd": 0, + "fps": instance.context.data["projectEntity"]["data"]["fps"] } From 9ff5c071b3f7e0cc1bf7f002719565d192030e3a Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 29 Aug 2023 21:05:43 +0300 Subject: [PATCH 15/70] resolve conversations --- openpype/hosts/houdini/api/colorspace.py | 18 +++++++++++++- .../houdini/plugins/create/create_review.py | 24 +++---------------- .../publish/validate_review_colorspace.py | 19 ++++----------- openpype/pipeline/colorspace.py | 6 ++--- 4 files changed, 28 insertions(+), 39 deletions(-) diff --git a/openpype/hosts/houdini/api/colorspace.py b/openpype/hosts/houdini/api/colorspace.py index 7047644225..5c3c605cd1 100644 --- a/openpype/hosts/houdini/api/colorspace.py +++ b/openpype/hosts/houdini/api/colorspace.py @@ -1,7 +1,7 @@ import attr import hou from openpype.hosts.houdini.api.lib import get_color_management_preferences - +from openpype.pipeline.colorspace import get_display_view_colorspace_name @attr.s class LayerMetadata(object): @@ -54,3 +54,19 @@ class ARenderProduct(object): ) ] return colorspace_data + + +def get_default_display_view_colorspace(): + """Get default display view colorspace. + + It's used for 'ociocolorspace' parm in OpneGL Node.""" + + data = get_color_management_preferences() + config_path = data.get("config") + display = data.get("display") + view = data.get("view") + + default_view_space = get_display_view_colorspace_name(config_path, + display, + view) + return default_view_space diff --git a/openpype/hosts/houdini/plugins/create/create_review.py b/openpype/hosts/houdini/plugins/create/create_review.py index 797116aaca..75a92e5e77 100644 --- a/openpype/hosts/houdini/plugins/create/create_review.py +++ b/openpype/hosts/houdini/plugins/create/create_review.py @@ -2,6 +2,7 @@ """Creator plugin for creating openGL reviews.""" from openpype.hosts.houdini.api import plugin from openpype.lib import EnumDef, BoolDef, NumberDef +from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace import os import hou @@ -142,9 +143,8 @@ class CreateReview(plugin.HoudiniCreator): # to OpenColorIO instance_node.setParms({"colorcorrect": 2}) - self.log.debug("Get default view colorspace name..") - - default_view_space = self.get_default_view_space() + # Get default view space for ociocolorspace parm. + default_view_space = get_default_display_view_colorspace() instance_node.setParms( {"ociocolorspace": default_view_space} ) @@ -154,21 +154,3 @@ class CreateReview(plugin.HoudiniCreator): "the default view color space '{}'" .format(instance_node, default_view_space) ) - - return default_view_space - - def get_default_view_space(self): - """Get default view space for ociocolorspace parm.""" - - from openpype.pipeline.colorspace import get_display_view_colorspace_name # noqa - from openpype.hosts.houdini.api.lib import get_color_management_preferences # noqa - - data = get_color_management_preferences() - config_path = data.get("config") - display = data.get("display") - view = data.get("view") - - default_view_space = get_display_view_colorspace_name(config_path, - display, view) - - return default_view_space diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 5390b6b52f..09e4a489d2 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -3,6 +3,7 @@ import pyblish.api from openpype.pipeline import PublishValidationError from openpype.pipeline.publish import RepairAction from openpype.hosts.houdini.api.action import SelectROPAction +from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace import os import hou @@ -38,7 +39,7 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): rop_node = hou.node(instance.data["instance_node"]) if os.getenv("OCIO") is None: - cls.log.warning( + cls.log.debug( "Default Houdini colorspace is used, " " skipping check.." ) @@ -71,25 +72,15 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): used to set colorspace on opengl node to the default view. """ - from openpype.pipeline.colorspace import get_display_view_colorspace_name # noqa - from openpype.hosts.houdini.api.lib import get_color_management_preferences # noqa - rop_node = hou.node(instance.data["instance_node"]) - data = get_color_management_preferences() - config_path = data.get("config") - display = data.get("display") - view = data.get("view") - - cls.log.debug("Get default view colorspace name..") - - default_view_space = get_display_view_colorspace_name(config_path, - display, view) + # Get default view colorspace name + default_view_space = get_default_display_view_colorspace() rop_node.setParms({"ociocolorspace": default_view_space}) cls.log.debug( "'OCIO Colorspace' parm on '{}' has been set to " "the default view color space '{}'" - .formate(rop_node, default_view_space) + .format(rop_node, default_view_space) ) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index a0efb5e18c..37974f4a0b 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -658,6 +658,6 @@ def get_display_view_colorspace_subprocess(config_path, display, view): run_openpype_process(*args, **process_kwargs) - # return all colorspaces - return_json_data = open(tmp_json_path).read() - return json.loads(return_json_data) + # return default view colorspace name + with open(tmp_json_path, "r") as f: + return json.load(f) From c799ae42eab8be32051ee373bdb71176002a20b8 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 29 Aug 2023 21:06:36 +0300 Subject: [PATCH 16/70] add spaces --- openpype/hosts/houdini/api/colorspace.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/api/colorspace.py b/openpype/hosts/houdini/api/colorspace.py index 5c3c605cd1..2662a968e2 100644 --- a/openpype/hosts/houdini/api/colorspace.py +++ b/openpype/hosts/houdini/api/colorspace.py @@ -67,6 +67,6 @@ def get_default_display_view_colorspace(): view = data.get("view") default_view_space = get_display_view_colorspace_name(config_path, - display, - view) + display, + view) return default_view_space From 75673151a6374591067c5e8f356ee3bb93706961 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 29 Aug 2023 21:18:27 +0300 Subject: [PATCH 17/70] resolve hound conversations --- openpype/hosts/houdini/plugins/create/create_review.py | 2 +- .../hosts/houdini/plugins/publish/validate_review_colorspace.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_review.py b/openpype/hosts/houdini/plugins/create/create_review.py index 75a92e5e77..c087c54f6c 100644 --- a/openpype/hosts/houdini/plugins/create/create_review.py +++ b/openpype/hosts/houdini/plugins/create/create_review.py @@ -2,7 +2,7 @@ """Creator plugin for creating openGL reviews.""" from openpype.hosts.houdini.api import plugin from openpype.lib import EnumDef, BoolDef, NumberDef -from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace +from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa import os import hou diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 09e4a489d2..2c7420bf48 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -3,7 +3,7 @@ import pyblish.api from openpype.pipeline import PublishValidationError from openpype.pipeline.publish import RepairAction from openpype.hosts.houdini.api.action import SelectROPAction -from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace +from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa import os import hou From 668bcb2d40d5708fc40c1e1a8465e36c94ffdef8 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 30 Aug 2023 22:28:43 +0300 Subject: [PATCH 18/70] BigRoy's Comments --- openpype/hosts/houdini/api/colorspace.py | 19 +++++++------------ openpype/pipeline/colorspace.py | 2 +- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/houdini/api/colorspace.py b/openpype/hosts/houdini/api/colorspace.py index 2662a968e2..fb0a724eb9 100644 --- a/openpype/hosts/houdini/api/colorspace.py +++ b/openpype/hosts/houdini/api/colorspace.py @@ -57,16 +57,11 @@ class ARenderProduct(object): def get_default_display_view_colorspace(): - """Get default display view colorspace. + """Get default display view colorspace name. """ - It's used for 'ociocolorspace' parm in OpneGL Node.""" - - data = get_color_management_preferences() - config_path = data.get("config") - display = data.get("display") - view = data.get("view") - - default_view_space = get_display_view_colorspace_name(config_path, - display, - view) - return default_view_space + prefs = get_color_management_preferences() + return get_display_view_colorspace_name( + config_path=prefs["config"], + display=prefs["display"], + view=prefs["view"] + ) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 37974f4a0b..3bb258e8f2 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -650,7 +650,7 @@ def get_display_view_colorspace_subprocess(config_path, display, view): "--view", view ] - log.info("Executing: {}".format(" ".join(args))) + log.debug("Executing: {}".format(" ".join(args))) process_kwargs = { "logger": log From 41babcaa85880b957a6409bf29d02da2291f91d2 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 30 Aug 2023 22:43:52 +0300 Subject: [PATCH 19/70] update doc strings --- openpype/hosts/houdini/api/colorspace.py | 4 +++- openpype/pipeline/colorspace.py | 4 ++-- openpype/scripts/ocio_wrapper.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/api/colorspace.py b/openpype/hosts/houdini/api/colorspace.py index fb0a724eb9..b1a4d5dcd5 100644 --- a/openpype/hosts/houdini/api/colorspace.py +++ b/openpype/hosts/houdini/api/colorspace.py @@ -57,7 +57,9 @@ class ARenderProduct(object): def get_default_display_view_colorspace(): - """Get default display view colorspace name. """ + """Returns the colorspace attribute of the default (display, view) pair. + + """ prefs = get_color_management_preferences() return get_display_view_colorspace_name( diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 3bb258e8f2..3dd33d0425 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -604,7 +604,7 @@ def _get_imageio_settings(project_settings, host_name): def get_display_view_colorspace_name(config_path, display, view): - """Return colorspace name for the given display and view. + """Returns the colorspace attribute of the (display, view) pair. Args: config_path (str): path string leading to config.ocio @@ -627,7 +627,7 @@ def get_display_view_colorspace_name(config_path, display, view): def get_display_view_colorspace_subprocess(config_path, display, view): - """get view colorspace name for the given display and view + """Returns the colorspace attribute of the (display, view) pair via subprocess. Args: diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index e491206ebb..cae6e6975b 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -175,7 +175,7 @@ def _get_views_data(config_path): def _get_display_view_colorspace_name(config_path, display, view): - """get view colorspace name for the given display and view. + """Returns the colorspace attribute of the (display, view) pair. Args: config_path (str): path string leading to config.ocio From 6c5039f7075ce59b2911a680d41ffae5a59dca09 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 30 Aug 2023 22:46:07 +0300 Subject: [PATCH 20/70] BigRoy's Comment --- openpype/hosts/houdini/api/colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/colorspace.py b/openpype/hosts/houdini/api/colorspace.py index b1a4d5dcd5..cc40b9df1c 100644 --- a/openpype/hosts/houdini/api/colorspace.py +++ b/openpype/hosts/houdini/api/colorspace.py @@ -59,7 +59,7 @@ class ARenderProduct(object): def get_default_display_view_colorspace(): """Returns the colorspace attribute of the default (display, view) pair. - """ + It's used for 'ociocolorspace' parm in OpenGL Node.""" prefs = get_color_management_preferences() return get_display_view_colorspace_name( From 1a3c8ad77252ffa9217c6af98ba27dcdd56c366a Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 31 Aug 2023 17:34:42 +0300 Subject: [PATCH 21/70] make validateReviewColorspace optional --- .../plugins/publish/validate_review_colorspace.py | 10 ++++++++-- .../defaults/project_settings/houdini.json | 5 +++++ .../schemas/schema_houdini_publish.json | 6 +++++- .../houdini/server/settings/publish_plugins.py | 14 +++++++++++--- server_addon/houdini/server/version.py | 2 +- 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 2c7420bf48..f680f62142 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- import pyblish.api -from openpype.pipeline import PublishValidationError +from openpype.pipeline import ( + PublishValidationError, + OptionalPyblishPluginMixin +) from openpype.pipeline.publish import RepairAction from openpype.hosts.houdini.api.action import SelectROPAction from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa @@ -14,7 +17,8 @@ class SetDefaultViewSpaceAction(RepairAction): icon = "mdi.monitor" -class ValidateReviewColorspace(pyblish.api.InstancePlugin): +class ValidateReviewColorspace(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate Review Colorspace parameters. It checks if 'OCIO Colorspace' parameter was set to valid value. @@ -26,6 +30,8 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): label = "Validate Review Colorspace" actions = [SetDefaultViewSpaceAction, SelectROPAction] + optional = True + def process(self, instance): invalid = self.get_invalid(instance) if invalid: diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 9d047c28bd..93d5c50d5e 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -93,6 +93,11 @@ "$JOB" ] }, + "ValidateReviewColorspace": { + "enabled": true, + "optional": true, + "active": true + }, "ValidateContainers": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json index aa6eaf5164..b57089007e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json @@ -40,6 +40,10 @@ "type": "schema_template", "name": "template_publish_plugin", "template_data": [ + { + "key": "ValidateReviewColorspace", + "label": "Validate Review Colorspace" + }, { "key": "ValidateContainers", "label": "ValidateContainers" @@ -47,4 +51,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/server_addon/houdini/server/settings/publish_plugins.py b/server_addon/houdini/server/settings/publish_plugins.py index 7d35d7e634..4534d8d0d9 100644 --- a/server_addon/houdini/server/settings/publish_plugins.py +++ b/server_addon/houdini/server/settings/publish_plugins.py @@ -120,7 +120,7 @@ class ValidateWorkfilePathsModel(BaseSettingsModel): ) -class ValidateContainersModel(BaseSettingsModel): +class BasicValidateModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") optional: bool = Field(title="Optional") active: bool = Field(title="Active") @@ -130,8 +130,11 @@ class PublishPluginsModel(BaseSettingsModel): ValidateWorkfilePaths: ValidateWorkfilePathsModel = Field( default_factory=ValidateWorkfilePathsModel, title="Validate workfile paths settings.") - ValidateContainers: ValidateContainersModel = Field( - default_factory=ValidateContainersModel, + ValidateReviewColorspace: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Review Colorspace.") + ValidateContainers: BasicValidateModel = Field( + default_factory=BasicValidateModel, title="Validate Latest Containers.") @@ -148,6 +151,11 @@ DEFAULT_HOUDINI_PUBLISH_SETTINGS = { "$JOB" ] }, + "ValidateReviewColorspace": { + "enabled": True, + "optional": True, + "active": True + }, "ValidateContainers": { "enabled": True, "optional": True, diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index 485f44ac21..b3f4756216 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.1.1" +__version__ = "0.1.2" From 1a936c155985e804f0ddc01e14a4573c56bbb8e7 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 31 Aug 2023 22:42:13 +0300 Subject: [PATCH 22/70] BigRoy's comments --- .../houdini/plugins/publish/validate_review_colorspace.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index f680f62142..3f5e5bc354 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -13,7 +13,7 @@ import hou class SetDefaultViewSpaceAction(RepairAction): - label = "Set default view space" + label = "Set default view colorspace" icon = "mdi.monitor" @@ -33,6 +33,10 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, optional = True def process(self, instance): + + if not self.is_active(instance.data): + return + invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( From d4393bad6b466438a839786903152fc36e293cae Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 31 Aug 2023 23:18:52 +0200 Subject: [PATCH 23/70] changing signature since we only need context.data for input. --- openpype/pipeline/colorspace.py | 39 ++++++-------------- openpype/pipeline/publish/publish_plugins.py | 8 +--- 2 files changed, 13 insertions(+), 34 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 649d355f62..b7728936b0 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -617,7 +617,7 @@ def get_colorspace_settings_from_publish_context(context_data): Returns: tuple | bool: config, file rules or None """ - if "imageioSettings" in context_data: + if "imageioSettings" in context_data and context_data["imageioSettings"]: return context_data["imageioSettings"] project_name = context_data["projectName"] @@ -631,14 +631,13 @@ def get_colorspace_settings_from_publish_context(context_data): anatomy_data=anatomy_data ) - # in case host color management is not enabled - if not config_data: - return None - - file_rules = get_imageio_file_rules( - project_name, host_name, - project_settings=project_settings_ - ) + # caching invalid state, so it's not recalculated all the time + file_rules = None + if config_data: + file_rules = get_imageio_file_rules( + project_name, host_name, + project_settings=project_settings_ + ) # caching settings for future instance processing context_data["imageioSettings"] = (config_data, file_rules) @@ -649,7 +648,6 @@ def get_colorspace_settings_from_publish_context(context_data): def set_colorspace_data_to_representation( representation, context_data, colorspace=None, - colorspace_settings=None, log=None ): """Sets colorspace data to representation. @@ -657,12 +655,7 @@ def set_colorspace_data_to_representation( Args: representation (dict): publishing representation context_data (publish.Context.data): publishing context data - config_data (dict): host resolved config data - file_rules (dict): host resolved file rules data colorspace (str, optional): colorspace name. Defaults to None. - colorspace_settings (tuple[dict, dict], optional): - Settings for config_data and file_rules. - Defaults to None. log (logging.Logger, optional): logger instance. Defaults to None. Example: @@ -691,21 +684,13 @@ def set_colorspace_data_to_representation( ) return - if colorspace_settings is None: - colorspace_settings = get_colorspace_settings_from_publish_context( - context_data) + # get colorspace settings + config_data, file_rules = get_colorspace_settings_from_publish_context( + context_data) # in case host color management is not enabled - if not colorspace_settings: - log.warning("Host's colorspace management is disabled.") - return - - # unpack colorspace settings - config_data, file_rules = colorspace_settings - if not config_data: - # warn in case no colorspace path was defined - log.warning("No colorspace management was defined") + log.warning("Host's colorspace management is disabled.") return log.debug("Config data is: `{}`".format(config_data)) diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index 17ede069cb..ae6cbc42d1 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -319,19 +319,13 @@ class ColormanagedPyblishPluginMixin(object): def set_representation_colorspace( self, representation, context, colorspace=None, - colorspace_settings=None ): """Sets colorspace data to representation. Args: representation (dict): publishing representation context (publish.Context): publishing context - config_data (dict): host resolved config data - file_rules (dict): host resolved file rules data colorspace (str, optional): colorspace name. Defaults to None. - colorspace_settings (tuple[dict, dict], optional): - Settings for config_data and file_rules. - Defaults to None. Example: ``` @@ -348,10 +342,10 @@ class ColormanagedPyblishPluginMixin(object): ``` """ + # using cached settings if available set_colorspace_data_to_representation( representation, context.data, colorspace, - colorspace_settings, log=self.log ) From 1f057525dd6dd2f3d03fbb97f68ee8504ce9aae5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 31 Aug 2023 23:28:12 +0200 Subject: [PATCH 24/70] testing: fixing zip file ID --- tests/unit/openpype/pipeline/test_colorspace.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/openpype/pipeline/test_colorspace.py b/tests/unit/openpype/pipeline/test_colorspace.py index c22acee2d4..ac35a28303 100644 --- a/tests/unit/openpype/pipeline/test_colorspace.py +++ b/tests/unit/openpype/pipeline/test_colorspace.py @@ -28,10 +28,9 @@ class TestPipelineColorspace(TestPipeline): cd to OpenPype repo root dir poetry run python ./start.py runtests ../tests/unit/openpype/pipeline """ - TEST_FILES = [ ( - "1Lf-mFxev7xiwZCWfImlRcw7Fj8XgNQMh", + "1csqimz8bbNcNgxtEXklLz6GRv91D3KgA", "test_pipeline_colorspace.zip", "" ) From 869c6277ff1d89599086169926dfb48673c088c0 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 1 Sep 2023 21:03:09 +0300 Subject: [PATCH 25/70] BigRoy's comment --- .../publish/validate_review_colorspace.py | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 3f5e5bc354..e5d4756556 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -37,15 +37,15 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, if not self.is_active(instance.data): return - invalid = self.get_invalid(instance) - if invalid: + invalid_nodes, message = self.get_invalid_with_message(instance) + if invalid_nodes: raise PublishValidationError( - ("'OCIO Colorspace' parameter is not valid."), + message, title=self.label ) @classmethod - def get_invalid(cls, instance): + def get_invalid_with_message(cls, instance): rop_node = hou.node(instance.data["instance_node"]) if os.getenv("OCIO") is None: @@ -53,26 +53,31 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, "Default Houdini colorspace is used, " " skipping check.." ) - return + return None, None if rop_node.evalParm("colorcorrect") != 2: # any colorspace settings other than default requires # 'Color Correct' parm to be set to 'OpenColorIO' - rop_node.setParms({"colorcorrect": 2}) - cls.log.debug( - "'Color Correct' parm on '{}' has been set to" - " 'OpenColorIO'".format(rop_node) + error = ( + "'Color Correction' parm on '{}' ROP must be set to" + " 'OpenColorIO'".format(rop_node.path()) ) + return rop_node , error if rop_node.evalParm("ociocolorspace") not in \ hou.Color.ocio_spaces(): - cls.log.error( - "'OCIO Colorspace' value on '{}' is not valid, " - "select a valid option from the dropdown menu." - .format(rop_node) + error = ( + "Invalid value: Colorspace name doesn't exist.\n" + "Check 'OCIO Colorspace' parameter on '{}' ROP" + .format(rop_node.path()) ) - return rop_node + return rop_node, error + + @classmethod + def get_invalid(cls, instance): + nodes, _ = cls.get_invalid_with_message(instance) + return nodes @classmethod def repair(cls, instance): @@ -84,6 +89,13 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, rop_node = hou.node(instance.data["instance_node"]) + if rop_node.evalParm("colorcorrect") != 2: + rop_node.setParms({"colorcorrect": 2}) + cls.log.debug( + "'Color Correction' parm on '{}' has been set to" + " 'OpenColorIO'".format(rop_node.path()) + ) + # Get default view colorspace name default_view_space = get_default_display_view_colorspace() From 087aca6d8b9377c054e623bc2f12f6b418a69aa0 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 1 Sep 2023 21:04:59 +0300 Subject: [PATCH 26/70] resolve hound --- .../hosts/houdini/plugins/publish/validate_review_colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index e5d4756556..47370678d0 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -62,7 +62,7 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, "'Color Correction' parm on '{}' ROP must be set to" " 'OpenColorIO'".format(rop_node.path()) ) - return rop_node , error + return rop_node, error if rop_node.evalParm("ociocolorspace") not in \ hou.Color.ocio_spaces(): From c2b948d35fea1f0cf2f7146bb0085fc3bd2dfeef Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 4 Sep 2023 11:09:47 +0300 Subject: [PATCH 27/70] BigRoy's comments --- .../publish/validate_review_colorspace.py | 27 ++++--------------- openpype/pipeline/colorspace.py | 7 +---- openpype/scripts/ocio_wrapper.py | 11 ++++---- 3 files changed, 11 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 47370678d0..d457a295c5 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -37,47 +37,30 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, if not self.is_active(instance.data): return - invalid_nodes, message = self.get_invalid_with_message(instance) - if invalid_nodes: - raise PublishValidationError( - message, - title=self.label - ) - - @classmethod - def get_invalid_with_message(cls, instance): - - rop_node = hou.node(instance.data["instance_node"]) if os.getenv("OCIO") is None: - cls.log.debug( + self.log.debug( "Default Houdini colorspace is used, " " skipping check.." ) - return None, None + return + rop_node = hou.node(instance.data["instance_node"]) if rop_node.evalParm("colorcorrect") != 2: # any colorspace settings other than default requires # 'Color Correct' parm to be set to 'OpenColorIO' - error = ( + raise PublishValidationError( "'Color Correction' parm on '{}' ROP must be set to" " 'OpenColorIO'".format(rop_node.path()) ) - return rop_node, error if rop_node.evalParm("ociocolorspace") not in \ hou.Color.ocio_spaces(): - error = ( + raise PublishValidationError( "Invalid value: Colorspace name doesn't exist.\n" "Check 'OCIO Colorspace' parameter on '{}' ROP" .format(rop_node.path()) ) - return rop_node, error - - @classmethod - def get_invalid(cls, instance): - nodes, _ = cls.get_invalid_with_message(instance) - return nodes @classmethod def repair(cls, instance): diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 3dd33d0425..e167e18cfb 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -648,15 +648,10 @@ def get_display_view_colorspace_subprocess(config_path, display, view): "--out_path", tmp_json_path, "--display", display, "--view", view - ] log.debug("Executing: {}".format(" ".join(args))) - process_kwargs = { - "logger": log - } - - run_openpype_process(*args, **process_kwargs) + run_openpype_process(*args, logger=log) # return default view colorspace name with open(tmp_json_path, "r") as f: diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index cae6e6975b..2c11bb7eeb 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -225,7 +225,7 @@ def get_display_view_colorspace_name(in_path, out_path, display, view): """Aggregate view colorspace name to file. - Wrapper command for processes without acces to OpenColorIO + Wrapper command for processes without access to OpenColorIO Args: in_path (str): config file path string @@ -239,15 +239,14 @@ def get_display_view_colorspace_name(in_path, out_path, --out_path= --display= --view= """ - json_path = Path(out_path) - out_data = _get_display_view_colorspace_name(in_path, - display, view) + display, + view) - with open(json_path, "w") as f: + with open(out_path, "w") as f: json.dump(out_data, f) - print(f"Display view colorspace saved to '{json_path}'") + print(f"Display view colorspace saved to '{out_path}'") if __name__ == '__main__': main() From 4ed278c0c885e22fdcf97c9a1b242428fee3ff05 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 4 Sep 2023 12:18:03 +0300 Subject: [PATCH 28/70] BigRoy's comment --- .../houdini/plugins/publish/validate_review_colorspace.py | 3 +-- openpype/scripts/ocio_wrapper.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index d457a295c5..545d7b16b1 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -83,9 +83,8 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, default_view_space = get_default_display_view_colorspace() rop_node.setParms({"ociocolorspace": default_view_space}) - cls.log.debug( + cls.log.info( "'OCIO Colorspace' parm on '{}' has been set to " "the default view color space '{}'" .format(rop_node, default_view_space) - ) diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index 2c11bb7eeb..40553d30f2 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -195,7 +195,7 @@ def _get_display_view_colorspace_name(config_path, display, view): if not config_path.is_file(): raise IOError("Input path should be `config.ocio` file") - config = ocio.Config().CreateFromFile(str(config_path)) + config = ocio.Config.CreateFromFile(str(config_path)) colorspace = config.getDisplayViewColorSpaceName(display, view) return colorspace From df2466b714f86336fad823bd9699aced271e5457 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 4 Sep 2023 12:58:31 +0200 Subject: [PATCH 29/70] default fps for sequence from asset instead of project --- openpype/plugins/publish/collect_sequence_frame_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_sequence_frame_data.py b/openpype/plugins/publish/collect_sequence_frame_data.py index 241e7b9011..6c2bfbf358 100644 --- a/openpype/plugins/publish/collect_sequence_frame_data.py +++ b/openpype/plugins/publish/collect_sequence_frame_data.py @@ -52,5 +52,5 @@ class CollectSequenceFrameData(pyblish.api.InstancePlugin): "frameEnd": repres_frames[-1], "handleStart": 0, "handleEnd": 0, - "fps": instance.context.data["projectEntity"]["data"]["fps"] + "fps": instance.context.data["assetEntity"]["data"]["fps"] } From 378ec74136eba325cba019348cdc69cb90c7d6b6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 4 Sep 2023 12:59:06 +0200 Subject: [PATCH 30/70] validator for frame range should include Plate family --- .../traypublisher/plugins/publish/validate_frame_ranges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py b/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py index b962ea464a..09de2d8db2 100644 --- a/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py +++ b/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py @@ -15,7 +15,7 @@ class ValidateFrameRange(OptionalPyblishPluginMixin, label = "Validate Frame Range" hosts = ["traypublisher"] - families = ["render"] + families = ["render", "plate"] order = ValidateContentsOrder optional = True From ab019e312441af63b447f74d8e6119bce0ebb06e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 4 Sep 2023 13:01:29 +0200 Subject: [PATCH 31/70] name of plugin should be more explicit --- ....py => collect_missing_frame_range_asset_entity.py} | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) rename openpype/hosts/traypublisher/plugins/publish/{collect_frame_range_asset_entity.py => collect_missing_frame_range_asset_entity.py} (83%) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_frame_range_asset_entity.py b/openpype/hosts/traypublisher/plugins/publish/collect_missing_frame_range_asset_entity.py similarity index 83% rename from openpype/hosts/traypublisher/plugins/publish/collect_frame_range_asset_entity.py rename to openpype/hosts/traypublisher/plugins/publish/collect_missing_frame_range_asset_entity.py index c18e10e438..72379ea4e1 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_frame_range_asset_entity.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_missing_frame_range_asset_entity.py @@ -2,16 +2,18 @@ import pyblish.api from openpype.pipeline import OptionalPyblishPluginMixin -class CollectFrameDataFromAssetEntity(pyblish.api.InstancePlugin, - OptionalPyblishPluginMixin): - """Collect Frame Range data From Asset Entity +class CollectMissingFrameDataFromAssetEntity( + pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin +): + """Collect Missing Frame Range data From Asset Entity Frame range data will only be collected if the keys are not yet collected for the instance. """ order = pyblish.api.CollectorOrder + 0.491 - label = "Collect Frame Data From Asset Entity" + label = "Collect Missing Frame Data From Asset Entity" families = ["plate", "pointcache", "vdbcache", "online", "render"] From 90bfd0a79a8ef5fcee474340594949e44f657ab2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 4 Sep 2023 13:26:36 +0200 Subject: [PATCH 32/70] Yeti Cache: Include viewport preview settings --- .../plugins/publish/collect_yeti_cache.py | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_yeti_cache.py b/openpype/hosts/maya/plugins/publish/collect_yeti_cache.py index e6b5ca4260..4dcda29050 100644 --- a/openpype/hosts/maya/plugins/publish/collect_yeti_cache.py +++ b/openpype/hosts/maya/plugins/publish/collect_yeti_cache.py @@ -4,12 +4,23 @@ import pyblish.api from openpype.hosts.maya.api import lib -SETTINGS = {"renderDensity", - "renderWidth", - "renderLength", - "increaseRenderBounds", - "imageSearchPath", - "cbId"} + +SETTINGS = { + # Preview + "displayOutput", + "colorR", "colorG", "colorB", + "viewportDensity", + "viewportWidth", + "viewportLength", + # Render attributes + "renderDensity", + "renderWidth", + "renderLength", + "increaseRenderBounds", + "imageSearchPath", + # Pipeline specific + "cbId" +} class CollectYetiCache(pyblish.api.InstancePlugin): @@ -39,10 +50,6 @@ class CollectYetiCache(pyblish.api.InstancePlugin): # Get yeti nodes and their transforms yeti_shapes = cmds.ls(instance, type="pgYetiMaya") for shape in yeti_shapes: - shape_data = {"transform": None, - "name": shape, - "cbId": lib.get_id(shape), - "attrs": None} # Get specific node attributes attr_data = {} @@ -58,9 +65,12 @@ class CollectYetiCache(pyblish.api.InstancePlugin): parent = cmds.listRelatives(shape, parent=True)[0] transform_data = {"name": parent, "cbId": lib.get_id(parent)} - # Store collected data - shape_data["attrs"] = attr_data - shape_data["transform"] = transform_data + shape_data = { + "transform": transform_data, + "name": shape, + "cbId": lib.get_id(shape), + "attrs": attr_data, + } settings["nodes"].append(shape_data) From 26ef5812b7428f792e81de4a2bfa61867e73f834 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 4 Sep 2023 13:42:01 +0200 Subject: [PATCH 33/70] Skip viewport attributes on 'update' but preserve what artist tweaked after initial load --- .../hosts/maya/plugins/load/load_yeti_cache.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_yeti_cache.py b/openpype/hosts/maya/plugins/load/load_yeti_cache.py index 5cded13d4e..4a11ea9a2c 100644 --- a/openpype/hosts/maya/plugins/load/load_yeti_cache.py +++ b/openpype/hosts/maya/plugins/load/load_yeti_cache.py @@ -15,6 +15,16 @@ from openpype.hosts.maya.api import lib from openpype.hosts.maya.api.pipeline import containerise +# Do not reset these values on update but only apply on first load +# to preserve any potential local overrides +SKIP_UPDATE_ATTRS = { + "displayOutput", + "viewportDensity", + "viewportWidth", + "viewportLength", +} + + def set_attribute(node, attr, value): """Wrapper of set attribute which ignores None values""" if value is None: @@ -205,6 +215,8 @@ class YetiCacheLoader(load.LoaderPlugin): yeti_node = yeti_nodes[0] for attr, value in node_settings["attrs"].items(): + if attr in SKIP_UPDATE_ATTRS: + continue set_attribute(attr, value, yeti_node) cmds.setAttr("{}.representation".format(container_node), @@ -311,7 +323,6 @@ class YetiCacheLoader(load.LoaderPlugin): # Update attributes with defaults attributes = node_settings["attrs"] attributes.update({ - "viewportDensity": 0.1, "verbosity": 2, "fileMode": 1, @@ -321,6 +332,9 @@ class YetiCacheLoader(load.LoaderPlugin): "visibleInRefractions": True }) + if "viewportDensity" not in attributes: + attributes["viewportDensity"] = 0.1 + # Apply attributes to pgYetiMaya node for attr, value in attributes.items(): set_attribute(attr, value, yeti_node) From 6ef67e3ff928fca53f4f10ea3166474d5b59c8c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 4 Sep 2023 17:17:15 +0200 Subject: [PATCH 34/70] Update openpype/pipeline/colorspace.py Co-authored-by: Roy Nieterau --- openpype/pipeline/colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index b7728936b0..ce0835dcc6 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -20,7 +20,7 @@ log = Logger.get_logger(__name__) class CachedData: - remapping = None + remapping = {} allowed_exts = { ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS) } From 2fdaadcdc151918163477f3275b646a7044f43e6 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 4 Sep 2023 19:50:00 +0300 Subject: [PATCH 35/70] Minikiu comment --- .../hosts/houdini/plugins/publish/validate_review_colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 545d7b16b1..61c3a755d0 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -39,7 +39,7 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, if os.getenv("OCIO") is None: self.log.debug( - "Default Houdini colorspace is used, " + "Using Houdini's Default Color Management, " " skipping check.." ) return From caad3e57e67f3f54854c33dd19c6d482fe977f54 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 6 Sep 2023 08:00:16 +0000 Subject: [PATCH 36/70] [Automated] Release --- CHANGELOG.md | 673 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 675 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1948b1a3f..c4f9ff57ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,679 @@ # Changelog +## [3.16.5](https://github.com/ynput/OpenPype/tree/3.16.5) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.16.4...3.16.5) + +### **🆕 New features** + + +
+Attribute Definitions: Multiselection enum def #5547 + +Added `multiselection` option to `EnumDef`. + + +___ + +
+ +### **🚀 Enhancements** + + +
+Farm: adding target collector #5494 + +Enhancing farm publishing workflow. + + +___ + +
+ + +
+Maya: Optimize validate plug-in path attributes #5522 + +- Optimize query (use `cmds.ls` once) +- Add Select Invalid action +- Improve validation report +- Avoid "Unknown object type" errors + + +___ + +
+ + +
+Maya: Remove Validate Instance Attributes plug-in #5525 + +Remove Validate Instance Attributes plug-in. + + +___ + +
+ + +
+Enhancement: Tweak logging for artist facing reports #5537 + +Tweak the logging of publishing for global, deadline, maya and a fusion plugin to have a cleaner artist-facing report. +- Fix context being reported correctly from CollectContext +- Fix ValidateMeshArnoldAttributes: fix when arnold is not loaded, fix applying settings, fix for when ai attributes do not exist + + +___ + +
+ + +
+AYON: Update settings #5544 + +Updated settings in AYON addons and conversion of AYON settings in OpenPype. + + +___ + +
+ + +
+Chore: Removed Ass export script #5560 + +Removed Arnold render script, which was obsolete and unused. + + +___ + +
+ + +
+Nuke: Allow for knob values to be validated against multiple values. #5042 + +Knob values can now be validated against multiple values, so you can allow write nodes to be `exr` and `png`, or `16-bit` and `32-bit`. + + +___ + +
+ + +
+Enhancement: Cosmetics for Higher version of publish already exists validation error #5190 + +Fix double spaces in message.Example output **after** the PR: + + +___ + +
+ + +
+Nuke: publish existing frames on farm #5409 + +This PR proposes adding a fourth option in Nuke render publish called "Use Existing Frames - Farm". This would be useful when the farm is busy or when the artist lacks enough farm licenses. Additionally, some artists prefer rendering on the farm but still want to check frames before publishing.By adding the "Use Existing Frames - Farm" option, artists will have more flexibility and control over their render publishing process. This enhancement will streamline the workflow and improve efficiency for Nuke users. + + +___ + +
+ + +
+Unreal: Create project in temp location and move to final when done #5476 + +Create Unreal project in local temporary folder and when done, move it to final destination. + + +___ + +
+ + +
+TrayPublisher: adding audio product type into default presets #5489 + +Adding Audio product type into default presets so anybody can publish audio to their shots. + + +___ + +
+ + +
+Global: avoiding cleanup of flagged representation #5502 + +Publishing folder can be flagged as persistent at representation level. + + +___ + +
+ + +
+General: missing tag could raise error #5511 + +- avoiding potential situation where missing Tag key could raise error + + +___ + +
+ + +
+Chore: Queued event system #5514 + +Implemented event system with more expected behavior of event system. If an event is triggered during other event callback, it is not processed immediately but waits until all callbacks of previous events are done. The event system also allows to not trigger events directly once `emit_event` is called which gives option to process events in custom loops. + + +___ + +
+ + +
+Publisher: Tweak log message to provide plugin name after "Plugin" #5521 + +Fix logged message for settings automatically applied to plugin attributes + + +___ + +
+ + +
+Houdini: Improve VDB Selection #5523 + +Improves VDB selection if selection is `SopNode`: return the selected sop nodeif selection is `ObjNode`: get the output node with the minimum 'outputidx' or the node with display flag + + +___ + +
+ + +
+Maya: Refactor/tweak Validate Instance In same Context plug-in #5526 + +- Chore/Refactor: Re-use existing select invalid and repair actions +- Enhancement: provide more elaborate PublishValidationError report +- Bugfix: fix "optional" support by using `OptionalPyblishPluginMixin` base class. + + +___ + +
+ + +
+Enhancement: Update houdini main menu #5527 + +This PR adds two updates: +- dynamic main menu +- dynamic asset name and task + + +___ + +
+ + +
+Houdini: Reset FPS when clicking Set Frame Range #5528 + +_Similar to Maya,_ Make `Set Frame Range` resets FPS, issue https://github.com/ynput/OpenPype/issues/5516 + + +___ + +
+ + +
+Enhancement: Deadline plugins optimize, cleanup and fix optional support for validate deadline pools #5531 + +- Fix optional support of validate deadline pools +- Query deadline webservice only once per URL for verification, and once for available deadline pools instead of for every instance +- Use `deadlineUrl` in `instance.data` when validating pools if it is set. +- Code cleanup: Re-use existing `requests_get` implementation + + +___ + +
+ + +
+Chore: PowerShell script for docker build #5535 + +Added PowerShell script to run docker build. + + +___ + +
+ + +
+AYON: Deadline expand userpaths in executables list #5540 + +Expande `~` paths in executables list. + + +___ + +
+ + +
+Chore: Use correct git url #5542 + +Fixed github url in README.md. + + +___ + +
+ + +
+Chore: Create plugin does not expect system settings #5553 + +System settings are not passed to initialization of create plugin initialization (and `apply_settings`). + + +___ + +
+ + +
+Chore: Allow custom Qt scale factor rounding policy #5555 + +Do not force `PassThrough` rounding policy if different policy is defined via env variable. + + +___ + +
+ + +
+Houdini: Fix outdated containers pop-up on opening last workfile on launch #5567 + +Fix Houdini not showing outdated containers pop-up on scene open when launching with last workfile argument + + +___ + +
+ + +
+Houdini: Improve errors e.g. raise PublishValidationError or cosmetics #5568 + +Improve errors e.g. raise PublishValidationError or cosmeticsThis also fixes the Increment Current File plug-in since due to an invalid import it was previously broken + + +___ + +
+ + +
+Fusion: Code updates #5569 + +Update fusion code which contains obsolete code. Removed `switch_ui.py` script from fusion with related script in scripts. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Validate Shape Zero fix repair action + provide informational artist-facing report #5524 + +Refactor to PublishValidationError to allow the RepairAction to work + provide informational report message + + +___ + +
+ + +
+Maya: Fix attribute definitions for `CreateYetiCache` #5574 + +Fix attribute definitions for `CreateYetiCache` + + +___ + +
+ + +
+Max: Optional Renderable Camera Validator for Render Instance #5286 + +Optional validation to check on renderable camera being set up correctly for deadline submission.If not being set up correctly, it wont pass the validation and user can perform repair actions. + + +___ + +
+ + +
+Max: Adding custom modifiers back to the loaded objects #5378 + +The custom parameters OpenpypeData doesn't show in the loaded container when it is being loaded through the loader. + + +___ + +
+ + +
+Houdini: Use default_variant to Houdini Node TAB Creator #5421 + +Use the default variant of the creator plugins on the interactive creator from the TAB node search instead of hard-coding it to `Main`. + + +___ + +
+ + +
+Nuke: adding inherited colorspace from instance #5454 + +Thumbnails are extracted with inherited colorspace collected from rendering write node. + + +___ + +
+ + +
+Add kitsu credentials to deadline publish job #5455 + +This PR hopefully fixes this issue #5440 + + +___ + +
+ + +
+AYON: Fill entities during editorial #5475 + +Fill entities and update template data on instances during extract AYON hierarchy. + + +___ + +
+ + +
+Ftrack: Fix version 0 when integrating to Ftrack - OP-6595 #5477 + +Fix publishing version 0 to Ftrack. + + +___ + +
+ + +
+OCIO: windows unc path support in Nuke and Hiero #5479 + +Hiero and Nuke is not supporting windows unc path formatting in OCIO environment variable. + + +___ + +
+ + +
+Deadline: Added super call to init #5480 + +DL 10.3 requires plugin inheriting from DeadlinePlugin to call super's **init** explicitly. + + +___ + +
+ + +
+Nuke: fixing thumbnail and monitor out root attributes #5483 + +Nuke Root Colorspace settings for Thumbnail and Monitor Out schema was gradually changed between version 12, 13, 14 and we needed to address those changes individually for particular version. + + +___ + +
+ + +
+Nuke: fixing missing `instance_id` error #5484 + +Workfiles with Instances created in old publisher workflow were rising error during converting method since they were missing `instance_id` key introduced in new publisher workflow. + + +___ + +
+ + +
+Nuke: existing frames validator is repairing render target #5486 + +Nuke is now correctly repairing render target after the existing frames validator finds missing frames and repair action is used. + + +___ + +
+ + +
+added UE to extract burnins families #5487 + +This PR fixes missing burnins in reviewables when rendering from UE. +___ + +
+ + +
+Harmony: refresh code for current Deadline #5493 + +- Added support in Deadline Plug-in for new versions of Harmony, in particular version 21 and 22. +- Remove review=False flag on render instance +- Add farm=True flag on render instance +- Fix is_in_tests function call in Harmony Deadline submission plugin +- Force HarmonyOpenPype.py Deadline Python plug-in to py3 +- Fix cosmetics/hound in HarmonyOpenPype.py Deadline Python plug-in + + +___ + +
+ + +
+Publisher: Fix multiselection value #5505 + +Selection of multiple instances in Publisher does not cause that all instances change all publish attributes to the same value. + + +___ + +
+ + +
+Publisher: Avoid warnings on thumbnails if source image also has alpha channel #5510 + +Avoids the following warning from `ExtractThumbnailFromSource`: +``` +// pyblish.ExtractThumbnailFromSource : oiiotool WARNING: -o : Can't save 4 channels to jpeg... saving only R,G,B +``` + + + +___ + +
+ + +
+Update ayon-python-api #5512 + +Update ayon python api and related callbacks. + + +___ + +
+ + +
+Max: Fixing the bug of falling back to use workfile for Arnold or any renderers except Redshift #5520 + +Fix the bug of falling back to use workfile for Arnold + + +___ + +
+ + +
+General: Fix Validate Publish Dir Validator #5534 + +Nonsensical "family" key was used instead of real value (as 'render' etc.) which would result in wrong translation of intermediate family names.Updated docstring. + + +___ + +
+ + +
+have the addons loading respect a custom AYON_ADDONS_DIR #5539 + +When using a custom AYON_ADDONS_DIR environment variable that variable is used in the launcher correctly and downloads and extracts addons to there, however when running Ayon does not respect this environment variable + + +___ + +
+ + +
+Deadline: files on representation cannot be single item list #5545 + +Further logic expects that single item files will be only 'string' not 'list' (eg. repre["files"] = "abc.exr" not repre["files"] = ["abc.exr"].This would cause an issue in ExtractReview later.This could happen if DL rendered single frame file with different frame value. + + +___ + +
+ + +
+Webpublisher: better encode list values for click #5546 + +Targets could be a list, original implementation pushed it as a separate items, it must be added as `--targets webpulish --targets filepublish`.`wepublish_routes` handles triggering from UI, changes in `publish_functions` handle triggering from cmd (for tests, api access). + + +___ + +
+ + +
+Houdini: Introduce imprint function for correct version in hda loader #5548 + +Resolve #5478 + + +___ + +
+ + +
+AYON: Fill entities during editorial (2) #5549 + +Fix changes made in https://github.com/ynput/OpenPype/pull/5475. + + +___ + +
+ + +
+Max: OP Data updates in Loaders #5563 + +Fix the bug on the loaders not being able to load the objects when iterating key and values with the dict.Max prefers list over the list in dict. + + +___ + +
+ + +
+Create Plugins: Better check of overriden '__init__' method #5571 + +Create plugins do not log warning messages about each create plugin because of wrong `__init__` method check. + + +___ + +
+ +### **Merged pull requests** + + +
+Tests: fix unit tests #5533 + +Fixed failing tests.Updated Unreal's validator to match removed general one which had a couple of issues fixed. + + +___ + +
+ + + + ## [3.16.4](https://github.com/ynput/OpenPype/tree/3.16.4) diff --git a/openpype/version.py b/openpype/version.py index 466f9ce033..d5d46bab0c 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.5-nightly.5" +__version__ = "3.16.5" diff --git a/pyproject.toml b/pyproject.toml index a07c547123..68fbf19c91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.16.4" # OpenPype +version = "3.16.5" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 1c667f91f90dbd3f34df971ece4453efc57ff0a4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 6 Sep 2023 08:01:23 +0000 Subject: [PATCH 37/70] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f6d1a25dc2..a35dbf1a17 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.16.5 - 3.16.5-nightly.5 - 3.16.5-nightly.4 - 3.16.5-nightly.3 @@ -134,7 +135,6 @@ body: - 3.14.9-nightly.3 - 3.14.9-nightly.2 - 3.14.9-nightly.1 - - 3.14.8 validations: required: true - type: dropdown From 9f8cd773bbc07e07db0536430735f97ae668fcf7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Sep 2023 10:13:22 +0200 Subject: [PATCH 38/70] use start value instead of current value --- openpype/tools/publisher/widgets/screenshot_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/screenshot_widget.py b/openpype/tools/publisher/widgets/screenshot_widget.py index 4ccf920571..170345f170 100644 --- a/openpype/tools/publisher/widgets/screenshot_widget.py +++ b/openpype/tools/publisher/widgets/screenshot_widget.py @@ -46,7 +46,7 @@ class ScreenMarquee(QtWidgets.QDialog): for screen in QtWidgets.QApplication.screens(): screen.geometryChanged.connect(self._fit_screen_geometry) - self._opacity = fade_anim.currentValue() + self._opacity = fade_anim.startValue() self._click_pos = None self._capture_rect = None From 2315ef84f1325a157f55bf85797d3e9719373392 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Sep 2023 10:13:35 +0200 Subject: [PATCH 39/70] do not start animation on init --- openpype/tools/publisher/widgets/screenshot_widget.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/screenshot_widget.py b/openpype/tools/publisher/widgets/screenshot_widget.py index 170345f170..64cccece6c 100644 --- a/openpype/tools/publisher/widgets/screenshot_widget.py +++ b/openpype/tools/publisher/widgets/screenshot_widget.py @@ -31,7 +31,6 @@ class ScreenMarquee(QtWidgets.QDialog): fade_anim.setEndValue(50) fade_anim.setDuration(200) fade_anim.setEasingCurve(QtCore.QEasingCurve.OutCubic) - fade_anim.start(QtCore.QAbstractAnimation.DeleteWhenStopped) fade_anim.valueChanged.connect(self._on_fade_anim) From 8dd4b70aa3c153ece4cb26fb4ee590742850c990 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Sep 2023 16:52:34 +0200 Subject: [PATCH 40/70] nuke: remove redundant workfile colorspace profiles --- .../defaults/project_settings/nuke.json | 6 +----- .../schemas/schema_nuke_imageio.json | 20 ------------------- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index b736c462ff..7961e77113 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -28,11 +28,7 @@ "colorManagement": "Nuke", "OCIO_config": "nuke-default", "workingSpaceLUT": "linear", - "monitorLut": "sRGB", - "int8Lut": "sRGB", - "int16Lut": "sRGB", - "logLut": "Cineon", - "floatLut": "linear" + "monitorLut": "sRGB" }, "nodes": { "requiredNodes": [ diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json index d4cd332ef8..af826fcf46 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json @@ -106,26 +106,6 @@ "type": "text", "key": "monitorLut", "label": "monitor" - }, - { - "type": "text", - "key": "int8Lut", - "label": "8-bit files" - }, - { - "type": "text", - "key": "int16Lut", - "label": "16-bit files" - }, - { - "type": "text", - "key": "logLut", - "label": "log files" - }, - { - "type": "text", - "key": "floatLut", - "label": "float files" } ] } From 359ead27a2e6f637dda576ae1daf1801bf00f348 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Sep 2023 16:53:41 +0200 Subject: [PATCH 41/70] nuke-addon: remove redundant workfile colorspace names --- server_addon/nuke/server/settings/imageio.py | 32 -------------------- 1 file changed, 32 deletions(-) diff --git a/server_addon/nuke/server/settings/imageio.py b/server_addon/nuke/server/settings/imageio.py index b43017ef8b..3f5ac0b8f9 100644 --- a/server_addon/nuke/server/settings/imageio.py +++ b/server_addon/nuke/server/settings/imageio.py @@ -66,22 +66,6 @@ def ocio_configs_switcher_enum(): class WorkfileColorspaceSettings(BaseSettingsModel): """Nuke workfile colorspace preset. """ - """# TODO: enhance settings with host api: - we need to add mapping to resolve properly keys. - Nuke is excpecting camel case key names, - but for better code consistency we need to - be using snake_case: - - color_management = colorManagement - ocio_config = OCIO_config - working_space_name = workingSpaceLUT - monitor_name = monitorLut - monitor_out_name = monitorOutLut - int_8_name = int8Lut - int_16_name = int16Lut - log_name = logLut - float_name = floatLut - """ colorManagement: Literal["Nuke", "OCIO"] = Field( title="Color Management" @@ -100,18 +84,6 @@ class WorkfileColorspaceSettings(BaseSettingsModel): monitorLut: str = Field( title="Monitor" ) - int8Lut: str = Field( - title="8-bit files" - ) - int16Lut: str = Field( - title="16-bit files" - ) - logLut: str = Field( - title="Log files" - ) - floatLut: str = Field( - title="Float files" - ) class ReadColorspaceRulesItems(BaseSettingsModel): @@ -238,10 +210,6 @@ DEFAULT_IMAGEIO_SETTINGS = { "OCIO_config": "nuke-default", "workingSpaceLUT": "linear", "monitorLut": "sRGB", - "int8Lut": "sRGB", - "int16Lut": "sRGB", - "logLut": "Cineon", - "floatLut": "linear" }, "nodes": { "requiredNodes": [ From 135cb285120495c250a8cbbace25a8581445fb1e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Sep 2023 22:17:21 +0200 Subject: [PATCH 42/70] Add deprecation warning to usage of `fname` on Loader plugins --- openpype/pipeline/load/plugins.py | 13 +++++++++++++ openpype/pipeline/load/utils.py | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/load/plugins.py b/openpype/pipeline/load/plugins.py index f87fb3312d..8acfcfdb6c 100644 --- a/openpype/pipeline/load/plugins.py +++ b/openpype/pipeline/load/plugins.py @@ -234,6 +234,19 @@ class LoaderPlugin(list): """ return cls.options or [] + @property + def fname(self): + """Backwards compatibility with deprecation warning""" + + self.log.warning(( + "DEPRECATION WARNING: Source - Loader plugin {}." + " The 'fname' property on the Loader plugin will be removed in" + " future versions of OpenPype. Planned version to drop the support" + " is 3.16.6 or 3.17.0." + ).format(self.__class__.__name__)) + if hasattr(self, "_fname"): + return self._fname + class SubsetLoaderPlugin(LoaderPlugin): """Load subset into host application diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index 42418be40e..b10d6032b3 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -318,7 +318,8 @@ def load_with_repre_context( # Backwards compatibility: Originally the loader's __init__ required the # representation context to set `fname` attribute to the filename to load - loader.fname = get_representation_path_from_context(repre_context) + # Deprecated - to be removed in OpenPype 3.16.6 or 3.17.0. + loader._fname = get_representation_path_from_context(repre_context) return loader.load(repre_context, name, namespace, options) From f954c877023f902a7ab28d2d86401829883734c0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Sep 2023 22:17:56 +0200 Subject: [PATCH 43/70] Refactor usage of deprecated `self.fname` to new style --- openpype/hosts/blender/plugins/load/load_blend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index 99f291a5a7..fa41f4374b 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -119,7 +119,7 @@ class BlendLoader(plugin.AssetLoader): context: Full parenthood of representation to load options: Additional settings dictionary """ - libpath = self.fname + libpath = self.filepath_from_context(context) asset = context["asset"]["name"] subset = context["subset"]["name"] From ba6dfc5eadd1a4d48f92924a2e1bcf3c7d5524f6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Sep 2023 23:28:39 +0200 Subject: [PATCH 44/70] Remove unused variables + tweak logs --- .../hosts/blender/plugins/load/load_camera_abc.py | 2 +- .../hosts/blender/plugins/load/load_camera_fbx.py | 2 +- .../hosts/blender/plugins/publish/extract_abc.py | 2 -- .../plugins/publish/extract_abc_animation.py | 2 -- .../blender/plugins/publish/extract_camera_abc.py | 5 ----- openpype/hosts/flame/plugins/load/load_clip.py | 1 - .../hosts/flame/plugins/load/load_clip_batch.py | 1 - .../plugins/publish/collect_timeline_instances.py | 1 - .../hosts/harmony/plugins/load/load_template.py | 1 - openpype/hosts/hiero/api/plugin.py | 14 -------------- .../hiero/plugins/publish/collect_clip_effects.py | 2 -- openpype/hosts/houdini/plugins/load/load_bgeo.py | 1 - openpype/hosts/max/plugins/create/create_render.py | 1 - .../hosts/max/plugins/publish/collect_render.py | 1 - .../max/plugins/publish/extract_camera_abc.py | 2 -- .../max/plugins/publish/extract_camera_fbx.py | 3 +-- .../max/plugins/publish/extract_max_scene_raw.py | 3 +-- .../hosts/max/plugins/publish/extract_model.py | 4 +--- .../hosts/max/plugins/publish/extract_model_fbx.py | 5 +---- .../hosts/max/plugins/publish/extract_model_obj.py | 4 +--- .../max/plugins/publish/extract_pointcache.py | 2 -- .../max/plugins/publish/extract_redshift_proxy.py | 3 +-- openpype/hosts/maya/api/plugin.py | 1 - .../maya/plugins/inventory/import_reference.py | 1 - .../hosts/maya/plugins/load/load_multiverse_usd.py | 2 -- openpype/hosts/maya/plugins/load/load_reference.py | 4 ++-- openpype/hosts/maya/plugins/load/load_xgen.py | 2 -- .../plugins/publish/collect_multiverse_look.py | 1 - .../hosts/maya/tools/mayalookassigner/widgets.py | 4 +--- .../hosts/nuke/plugins/load/load_camera_abc.py | 2 -- .../plugins/publish/extract_review_data_lut.py | 1 - .../nuke/plugins/publish/extract_thumbnail.py | 2 -- .../plugins/publish/collect_auto_image.py | 1 - openpype/hosts/resolve/api/plugin.py | 10 ---------- .../plugins/publish/extract_thumbnail.py | 2 -- .../tvpaint/plugins/load/load_reference_image.py | 2 +- .../tvpaint/plugins/publish/extract_sequence.py | 3 --- .../hosts/unreal/plugins/publish/extract_uasset.py | 3 +-- 38 files changed, 13 insertions(+), 90 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_camera_abc.py b/openpype/hosts/blender/plugins/load/load_camera_abc.py index e5afecff66..05d3fb764d 100644 --- a/openpype/hosts/blender/plugins/load/load_camera_abc.py +++ b/openpype/hosts/blender/plugins/load/load_camera_abc.py @@ -100,7 +100,7 @@ class AbcCameraLoader(plugin.AssetLoader): asset_group = bpy.data.objects.new(group_name, object_data=None) avalon_container.objects.link(asset_group) - objects = self._process(libpath, asset_group, group_name) + self._process(libpath, asset_group, group_name) objects = [] nodes = list(asset_group.children) diff --git a/openpype/hosts/blender/plugins/load/load_camera_fbx.py b/openpype/hosts/blender/plugins/load/load_camera_fbx.py index b9d05dda0a..3cca6e7fd3 100644 --- a/openpype/hosts/blender/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/blender/plugins/load/load_camera_fbx.py @@ -103,7 +103,7 @@ class FbxCameraLoader(plugin.AssetLoader): asset_group = bpy.data.objects.new(group_name, object_data=None) avalon_container.objects.link(asset_group) - objects = self._process(libpath, asset_group, group_name) + self._process(libpath, asset_group, group_name) objects = [] nodes = list(asset_group.children) diff --git a/openpype/hosts/blender/plugins/publish/extract_abc.py b/openpype/hosts/blender/plugins/publish/extract_abc.py index f4babc94d3..87159e53f0 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc.py @@ -21,8 +21,6 @@ class ExtractABC(publish.Extractor): filename = f"{instance.name}.abc" filepath = os.path.join(stagingdir, filename) - context = bpy.context - # Perform extraction self.log.info("Performing extraction..") diff --git a/openpype/hosts/blender/plugins/publish/extract_abc_animation.py b/openpype/hosts/blender/plugins/publish/extract_abc_animation.py index e141ccaa44..44b2ba3761 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc_animation.py @@ -20,8 +20,6 @@ class ExtractAnimationABC(publish.Extractor): filename = f"{instance.name}.abc" filepath = os.path.join(stagingdir, filename) - context = bpy.context - # Perform extraction self.log.info("Performing extraction..") diff --git a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py index a21a59b151..036be7bf3c 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py @@ -21,16 +21,11 @@ class ExtractCameraABC(publish.Extractor): filename = f"{instance.name}.abc" filepath = os.path.join(stagingdir, filename) - context = bpy.context - # Perform extraction self.log.info("Performing extraction..") plugin.deselect_all() - selected = [] - active = None - asset_group = None for obj in instance: if obj.get(AVALON_PROPERTY): diff --git a/openpype/hosts/flame/plugins/load/load_clip.py b/openpype/hosts/flame/plugins/load/load_clip.py index 338833b449..ca4eab0f63 100644 --- a/openpype/hosts/flame/plugins/load/load_clip.py +++ b/openpype/hosts/flame/plugins/load/load_clip.py @@ -48,7 +48,6 @@ class LoadClip(opfapi.ClipLoader): self.fpd = fproject.current_workspace.desktop # load clip to timeline and get main variables - namespace = namespace version = context['version'] version_data = version.get("data", {}) version_name = version.get("name", None) diff --git a/openpype/hosts/flame/plugins/load/load_clip_batch.py b/openpype/hosts/flame/plugins/load/load_clip_batch.py index ca43b94ee9..1f3a017d72 100644 --- a/openpype/hosts/flame/plugins/load/load_clip_batch.py +++ b/openpype/hosts/flame/plugins/load/load_clip_batch.py @@ -45,7 +45,6 @@ class LoadClipBatch(opfapi.ClipLoader): self.batch = options.get("batch") or flame.batch # load clip to timeline and get main variables - namespace = namespace version = context['version'] version_data = version.get("data", {}) version_name = version.get("name", None) diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py index 23fdf5e785..e14f960a2b 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py @@ -325,7 +325,6 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): def _create_shot_instance(self, context, clip_name, **data): master_layer = data.get("heroTrack") hierarchy_data = data.get("hierarchyData") - asset = data.get("asset") if not master_layer: return diff --git a/openpype/hosts/harmony/plugins/load/load_template.py b/openpype/hosts/harmony/plugins/load/load_template.py index f3c69a9104..a78a1bf1ec 100644 --- a/openpype/hosts/harmony/plugins/load/load_template.py +++ b/openpype/hosts/harmony/plugins/load/load_template.py @@ -82,7 +82,6 @@ class TemplateLoader(load.LoaderPlugin): node = harmony.find_node_by_name(node_name, "GROUP") self_name = self.__class__.__name__ - update_and_replace = False if is_representation_from_latest(representation): self._set_green(node) else: diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 65a4009756..52f96261b2 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -317,20 +317,6 @@ class Spacer(QtWidgets.QWidget): self.setLayout(layout) -def get_reference_node_parents(ref): - """Return all parent reference nodes of reference node - - Args: - ref (str): reference node. - - Returns: - list: The upstream parent reference nodes. - - """ - parents = [] - return parents - - class SequenceLoader(LoaderPlugin): """A basic SequenceLoader for Resolve diff --git a/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py b/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py index d455ad4a4e..fcb1ab27a0 100644 --- a/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py +++ b/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py @@ -43,7 +43,6 @@ class CollectClipEffects(pyblish.api.InstancePlugin): if review and review_track_index == _track_index: continue for sitem in sub_track_items: - effect = None # make sure this subtrack item is relative of track item if ((track_item not in sitem.linkedItems()) and (len(sitem.linkedItems()) > 0)): @@ -53,7 +52,6 @@ class CollectClipEffects(pyblish.api.InstancePlugin): continue effect = self.add_effect(_track_index, sitem) - if effect: effects.update(effect) diff --git a/openpype/hosts/houdini/plugins/load/load_bgeo.py b/openpype/hosts/houdini/plugins/load/load_bgeo.py index 22680178c0..489bf944ed 100644 --- a/openpype/hosts/houdini/plugins/load/load_bgeo.py +++ b/openpype/hosts/houdini/plugins/load/load_bgeo.py @@ -34,7 +34,6 @@ class BgeoLoader(load.LoaderPlugin): # Create a new geo node container = obj.createNode("geo", node_name=node_name) - is_sequence = bool(context["representation"]["context"].get("frame")) # Remove the file node, it only loads static meshes # Houdini 17 has removed the file node from the geo node diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 235046684e..9cc3c8da8a 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -14,7 +14,6 @@ class CreateRender(plugin.MaxCreator): def create(self, subset_name, instance_data, pre_create_data): from pymxs import runtime as rt - sel_obj = list(rt.selection) file = rt.maxFileName filename, _ = os.path.splitext(file) instance_data["AssetName"] = filename diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 8ee2f43103..2dfa1520a9 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -30,7 +30,6 @@ class CollectRender(pyblish.api.InstancePlugin): asset = get_current_asset_name() files_by_aov = RenderProducts().get_beauty(instance.name) - folder = folder.replace("\\", "/") aovs = RenderProducts().get_aovs(instance.name) files_by_aov.update(aovs) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_abc.py b/openpype/hosts/max/plugins/publish/extract_camera_abc.py index b42732e70d..b1918c53e0 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_abc.py @@ -22,8 +22,6 @@ class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin): start = float(instance.data.get("frameStartHandle", 1)) end = float(instance.data.get("frameEndHandle", 1)) - container = instance.data["instance_node"] - self.log.info("Extracting Camera ...") stagingdir = self.staging_dir(instance) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py index 06ac3da093..537c88eb4d 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py @@ -19,9 +19,8 @@ class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): def process(self, instance): if not self.is_active(instance.data): return - container = instance.data["instance_node"] - self.log.info("Extracting Camera ...") + self.log.debug("Extracting Camera ...") stagingdir = self.staging_dir(instance) filename = "{name}.fbx".format(**instance.data) diff --git a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py index de5db9ab56..a7a889c587 100644 --- a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py +++ b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py @@ -18,10 +18,9 @@ class ExtractMaxSceneRaw(publish.Extractor, OptionalPyblishPluginMixin): def process(self, instance): if not self.is_active(instance.data): return - container = instance.data["instance_node"] # publish the raw scene for camera - self.log.info("Extracting Raw Max Scene ...") + self.log.debug("Extracting Raw Max Scene ...") stagingdir = self.staging_dir(instance) filename = "{name}.max".format(**instance.data) diff --git a/openpype/hosts/max/plugins/publish/extract_model.py b/openpype/hosts/max/plugins/publish/extract_model.py index c7ecf7efc9..38f4848c5e 100644 --- a/openpype/hosts/max/plugins/publish/extract_model.py +++ b/openpype/hosts/max/plugins/publish/extract_model.py @@ -20,9 +20,7 @@ class ExtractModel(publish.Extractor, OptionalPyblishPluginMixin): if not self.is_active(instance.data): return - container = instance.data["instance_node"] - - self.log.info("Extracting Geometry ...") + self.log.debug("Extracting Geometry ...") stagingdir = self.staging_dir(instance) filename = "{name}.abc".format(**instance.data) diff --git a/openpype/hosts/max/plugins/publish/extract_model_fbx.py b/openpype/hosts/max/plugins/publish/extract_model_fbx.py index 56c2cadd94..fd48ed5007 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_model_fbx.py @@ -20,10 +20,7 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): if not self.is_active(instance.data): return - container = instance.data["instance_node"] - - - self.log.info("Extracting Geometry ...") + self.log.debug("Extracting Geometry ...") stagingdir = self.staging_dir(instance) filename = "{name}.fbx".format(**instance.data) diff --git a/openpype/hosts/max/plugins/publish/extract_model_obj.py b/openpype/hosts/max/plugins/publish/extract_model_obj.py index 4fde65cf22..e522b1e7a1 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_obj.py +++ b/openpype/hosts/max/plugins/publish/extract_model_obj.py @@ -20,9 +20,7 @@ class ExtractModelObj(publish.Extractor, OptionalPyblishPluginMixin): if not self.is_active(instance.data): return - container = instance.data["instance_node"] - - self.log.info("Extracting Geometry ...") + self.log.debug("Extracting Geometry ...") stagingdir = self.staging_dir(instance) filename = "{name}.obj".format(**instance.data) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcache.py b/openpype/hosts/max/plugins/publish/extract_pointcache.py index 5a99a8b845..c3de623bc0 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcache.py @@ -54,8 +54,6 @@ class ExtractAlembic(publish.Extractor): start = float(instance.data.get("frameStartHandle", 1)) end = float(instance.data.get("frameEndHandle", 1)) - container = instance.data["instance_node"] - self.log.debug("Extracting pointcache ...") parent_dir = self.staging_dir(instance) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index ab569ecbcb..f67ed30c6b 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -16,11 +16,10 @@ class ExtractRedshiftProxy(publish.Extractor): families = ["redshiftproxy"] def process(self, instance): - container = instance.data["instance_node"] start = int(instance.context.data.get("frameStart")) end = int(instance.context.data.get("frameEnd")) - self.log.info("Extracting Redshift Proxy...") + self.log.debug("Extracting Redshift Proxy...") stagingdir = self.staging_dir(instance) rs_filename = "{name}.rs".format(**instance.data) rs_filepath = os.path.join(stagingdir, rs_filename) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 3f383fafb8..4032618afb 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -683,7 +683,6 @@ class ReferenceLoader(Loader): loaded_containers.append(container) self._organize_containers(nodes, container) c += 1 - namespace = None return loaded_containers diff --git a/openpype/hosts/maya/plugins/inventory/import_reference.py b/openpype/hosts/maya/plugins/inventory/import_reference.py index ecc424209d..3f3b85ba6c 100644 --- a/openpype/hosts/maya/plugins/inventory/import_reference.py +++ b/openpype/hosts/maya/plugins/inventory/import_reference.py @@ -12,7 +12,6 @@ class ImportReference(InventoryAction): color = "#d8d8d8" def process(self, containers): - references = cmds.ls(type="reference") for container in containers: if container["loader"] != "ReferenceLoader": print("Not a reference, skipping") diff --git a/openpype/hosts/maya/plugins/load/load_multiverse_usd.py b/openpype/hosts/maya/plugins/load/load_multiverse_usd.py index d08fcd904e..cad42b55f9 100644 --- a/openpype/hosts/maya/plugins/load/load_multiverse_usd.py +++ b/openpype/hosts/maya/plugins/load/load_multiverse_usd.py @@ -43,8 +43,6 @@ class MultiverseUsdLoader(load.LoaderPlugin): import multiverse # Create the shape - shape = None - transform = None with maintained_selection(): cmds.namespace(addNamespace=namespace) with namespaced(namespace, new=False): diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 91767249e0..61f337f501 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -205,7 +205,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): cmds.setAttr("{}.selectHandleZ".format(group_name), cz) if family == "rig": - self._post_process_rig(name, namespace, context, options) + self._post_process_rig(namespace, context, options) else: if "translate" in options: if not attach_to_root and new_nodes: @@ -229,7 +229,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): members = get_container_members(container) self._lock_camera_transforms(members) - def _post_process_rig(self, name, namespace, context, options): + def _post_process_rig(self, namespace, context, options): nodes = self[:] create_rig_animation_instance( diff --git a/openpype/hosts/maya/plugins/load/load_xgen.py b/openpype/hosts/maya/plugins/load/load_xgen.py index 323f8d7eda..2ad6ad55bc 100644 --- a/openpype/hosts/maya/plugins/load/load_xgen.py +++ b/openpype/hosts/maya/plugins/load/load_xgen.py @@ -53,8 +53,6 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): ) # Reference xgen. Xgen does not like being referenced in under a group. - new_nodes = [] - with maintained_selection(): nodes = cmds.file( maya_filepath, diff --git a/openpype/hosts/maya/plugins/publish/collect_multiverse_look.py b/openpype/hosts/maya/plugins/publish/collect_multiverse_look.py index f05fb76d48..bcb979edfc 100644 --- a/openpype/hosts/maya/plugins/publish/collect_multiverse_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_multiverse_look.py @@ -281,7 +281,6 @@ class CollectMultiverseLookData(pyblish.api.InstancePlugin): long=True) nodes.update(nodes_of_interest) - files = [] sets = {} instance.data["resources"] = [] publishMipMap = instance.data["publishMipMap"] diff --git a/openpype/hosts/maya/tools/mayalookassigner/widgets.py b/openpype/hosts/maya/tools/mayalookassigner/widgets.py index f2df17e68c..82c37e2104 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/widgets.py +++ b/openpype/hosts/maya/tools/mayalookassigner/widgets.py @@ -90,15 +90,13 @@ class AssetOutliner(QtWidgets.QWidget): def get_all_assets(self): """Add all items from the current scene""" - items = [] with preserve_expanded_rows(self.view): with preserve_selection(self.view): self.clear() nodes = commands.get_all_asset_nodes() items = commands.create_items_from_nodes(nodes) self.add_items(items) - - return len(items) > 0 + return len(items) > 0 def get_selected_assets(self): """Add all selected items from the current scene""" diff --git a/openpype/hosts/nuke/plugins/load/load_camera_abc.py b/openpype/hosts/nuke/plugins/load/load_camera_abc.py index fec4ee556e..2939ceebae 100644 --- a/openpype/hosts/nuke/plugins/load/load_camera_abc.py +++ b/openpype/hosts/nuke/plugins/load/load_camera_abc.py @@ -112,8 +112,6 @@ class AlembicCameraLoader(load.LoaderPlugin): version_doc = get_version_by_id(project_name, representation["parent"]) object_name = container['objectName'] - # get corresponding node - camera_node = nuke.toNode(object_name) # get main variables version_data = version_doc.get("data", {}) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py index e4b7b155cd..2a26ed82fb 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py @@ -20,7 +20,6 @@ class ExtractReviewDataLut(publish.Extractor): hosts = ["nuke"] def process(self, instance): - families = instance.data["families"] self.log.info("Creating staging dir...") if "representations" in instance.data: staging_dir = instance.data[ diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index d57d55f85d..b20df4ffe2 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -91,8 +91,6 @@ class ExtractThumbnail(publish.Extractor): if collection: # get path - fname = os.path.basename(collection.format( - "{head}{padding}{tail}")) fhead = collection.format("{head}") thumb_fname = list(collection)[mid_frame] diff --git a/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py b/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py index f1d8419608..77f1a3e91f 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py @@ -16,7 +16,6 @@ class CollectAutoImage(pyblish.api.ContextPlugin): targets = ["automated"] def process(self, context): - family = "image" for instance in context: creator_identifier = instance.data.get("creator_identifier") if creator_identifier and creator_identifier == "auto_image": diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 59c27f29da..e2bd76ffa2 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -413,8 +413,6 @@ class ClipLoader: if self.with_handles: source_in -= handle_start source_out += handle_end - handle_start = 0 - handle_end = 0 # make track item from source in bin as item timeline_item = lib.create_timeline_item( @@ -433,14 +431,6 @@ class ClipLoader: self.data["path"], self.active_bin) _clip_property = media_pool_item.GetClipProperty - # get handles - handle_start = self.data["versionData"].get("handleStart") - handle_end = self.data["versionData"].get("handleEnd") - if handle_start is None: - handle_start = int(self.data["assetData"]["handleStart"]) - if handle_end is None: - handle_end = int(self.data["assetData"]["handleEnd"]) - source_in = int(_clip_property("Start")) source_out = int(_clip_property("End")) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index b99503b3c8..a2afd160fa 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -49,8 +49,6 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): else: first_filename = files - staging_dir = None - # Convert to jpeg if not yet full_input_path = os.path.join( thumbnail_repre["stagingDir"], first_filename diff --git a/openpype/hosts/tvpaint/plugins/load/load_reference_image.py b/openpype/hosts/tvpaint/plugins/load/load_reference_image.py index edc116a8e4..3707ef97aa 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_reference_image.py +++ b/openpype/hosts/tvpaint/plugins/load/load_reference_image.py @@ -171,7 +171,7 @@ class LoadImage(plugin.Loader): george_script = "\n".join(george_script_lines) execute_george_through_file(george_script) - def _remove_container(self, container, members=None): + def _remove_container(self, container): if not container: return representation = container["representation"] diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 8a610cf388..a13a91de46 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -63,7 +63,6 @@ class ExtractSequence(pyblish.api.Extractor): "ignoreLayersTransparency", False ) - family_lowered = instance.data["family"].lower() mark_in = instance.context.data["sceneMarkIn"] mark_out = instance.context.data["sceneMarkOut"] @@ -76,11 +75,9 @@ class ExtractSequence(pyblish.api.Extractor): # Frame start/end may be stored as float frame_start = int(instance.data["frameStart"]) - frame_end = int(instance.data["frameEnd"]) # Handles are not stored per instance but on Context handle_start = instance.context.data["handleStart"] - handle_end = instance.context.data["handleEnd"] scene_bg_color = instance.context.data["sceneBgColor"] diff --git a/openpype/hosts/unreal/plugins/publish/extract_uasset.py b/openpype/hosts/unreal/plugins/publish/extract_uasset.py index 48b62faa97..0dd7ff4a0d 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_uasset.py +++ b/openpype/hosts/unreal/plugins/publish/extract_uasset.py @@ -19,9 +19,8 @@ class ExtractUAsset(publish.Extractor): "umap" if "umap" in instance.data.get("families") else "uasset") ar = unreal.AssetRegistryHelpers.get_asset_registry() - self.log.info("Performing extraction..") + self.log.debug("Performing extraction..") staging_dir = self.staging_dir(instance) - filename = f"{instance.name}.{extension}" members = instance.data.get("members", []) From e79f1ef4b9b48f085f42ac43344404580522d10c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Sep 2023 23:30:17 +0200 Subject: [PATCH 45/70] Replacing value only if value is in `str` is same as just replacing it --- .../hosts/houdini/plugins/publish/collect_vray_rop.py | 9 ++------- openpype/hosts/maya/api/lib_rendersettings.py | 9 ++------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index d4fe37f993..277f922ba4 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -80,14 +80,9 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): def get_beauty_render_product(self, prefix, suffix=""): """Return the beauty output filename if render element enabled """ + # Remove aov suffix from the product: `prefix.aov_suffix` -> `prefix` aov_parm = ".{}".format(suffix) - beauty_product = None - if aov_parm in prefix: - beauty_product = prefix.replace(aov_parm, "") - else: - beauty_product = prefix - - return beauty_product + return prefix.replace(aov_parm, "") def get_render_element_name(self, node, prefix, suffix=""): """Return the output filename using the AOV prefix and suffix diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index f54633c04d..42cf29d0a7 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -177,12 +177,7 @@ class RenderSettings(object): # list all the aovs all_rs_aovs = cmds.ls(type='RedshiftAOV') for rs_aov in redshift_aovs: - rs_layername = rs_aov - if " " in rs_aov: - rs_renderlayer = rs_aov.replace(" ", "") - rs_layername = "rsAov_{}".format(rs_renderlayer) - else: - rs_layername = "rsAov_{}".format(rs_aov) + rs_layername = "rsAov_{}".format(rs_aov.replace(" ", "")) if rs_layername in all_rs_aovs: continue cmds.rsCreateAov(type=rs_aov) @@ -317,7 +312,7 @@ class RenderSettings(object): separators = [cmds.menuItem(i, query=True, label=True) for i in items] # noqa: E501 try: sep_idx = separators.index(aov_separator) - except ValueError as e: + except ValueError: six.reraise( CreatorError, CreatorError( From c1b305a4462b6e58eede2853a5cd0623cf4ef925 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Sep 2023 23:31:33 +0200 Subject: [PATCH 46/70] Fix code (`if not found` should not have been nest into the for loop) + simplify logic --- openpype/hosts/max/api/lib_rendersettings.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index afde5008d5..26e176aa8d 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -37,13 +37,10 @@ class RenderSettings(object): def set_render_camera(self, selection): for sel in selection: # to avoid Attribute Error from pymxs wrapper - found = False if rt.classOf(sel) in rt.Camera.classes: - found = True rt.viewport.setCamera(sel) - break - if not found: - raise RuntimeError("Active Camera not found") + return + raise RuntimeError("Active Camera not found") def render_output(self, container): folder = rt.maxFilePath From 60334621988ee749369d222d11ecd972871a2573 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Sep 2023 23:32:53 +0200 Subject: [PATCH 47/70] Use asset doc and project doc from instance/context + tweak logic for values --- .../publish/validate_resolution_setting.py | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py index 5fcb843b20..5ac41b10a0 100644 --- a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py +++ b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py @@ -6,11 +6,6 @@ from openpype.pipeline import ( from pymxs import runtime as rt from openpype.hosts.max.api.lib import reset_scene_resolution -from openpype.pipeline.context_tools import ( - get_current_project_asset, - get_current_project -) - class ValidateResolutionSetting(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): @@ -43,22 +38,16 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin, "on asset or shot.") def get_db_resolution(self, instance): - data = ["data.resolutionWidth", "data.resolutionHeight"] - project_resolution = get_current_project(fields=data) - project_resolution_data = project_resolution["data"] - asset_resolution = get_current_project_asset(fields=data) - asset_resolution_data = asset_resolution["data"] - # Set project resolution - project_width = int( - project_resolution_data.get("resolutionWidth", 1920)) - project_height = int( - project_resolution_data.get("resolutionHeight", 1080)) - width = int( - asset_resolution_data.get("resolutionWidth", project_width)) - height = int( - asset_resolution_data.get("resolutionHeight", project_height)) + asset_doc = instance.data["assetEntity"] + project_doc = instance.context.data["projectEntity"] + for data in [asset_doc["data"], project_doc["data"]]: + if "resolutionWidth" in data and "resolutionHeight" in data: + width = data["resolutionWidth"] + height = data["resolutionHeight"] + return int(width), int(height) - return width, height + # Defaults if not found in asset document or project document + return 1920, 1080 @classmethod def repair(cls, instance): From 91a4ae5dfd39d64db3ded9e98b750b8d1afe22bc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Sep 2023 23:34:01 +0200 Subject: [PATCH 48/70] Remove (not yet deprecated?) `system_settings` from `apply_settings` --- .../hosts/maya/plugins/publish/extract_import_reference.py | 4 ++-- openpype/hosts/maya/plugins/publish/validate_maya_units.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_import_reference.py b/openpype/hosts/maya/plugins/publish/extract_import_reference.py index 9d2ff1a3eb..1fdee28d0c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_import_reference.py +++ b/openpype/hosts/maya/plugins/publish/extract_import_reference.py @@ -30,8 +30,8 @@ class ExtractImportReference(publish.Extractor, tmp_format = "_tmp" @classmethod - def apply_settings(cls, project_setting, system_settings): - cls.active = project_setting["deadline"]["publish"]["MayaSubmitDeadline"]["import_reference"] # noqa + def apply_settings(cls, project_settings): + cls.active = project_settings["deadline"]["publish"]["MayaSubmitDeadline"]["import_reference"] # noqa def process(self, instance): if not self.is_active(instance.data): diff --git a/openpype/hosts/maya/plugins/publish/validate_maya_units.py b/openpype/hosts/maya/plugins/publish/validate_maya_units.py index 1d5619795f..ae6dc093a9 100644 --- a/openpype/hosts/maya/plugins/publish/validate_maya_units.py +++ b/openpype/hosts/maya/plugins/publish/validate_maya_units.py @@ -37,7 +37,7 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin): ) @classmethod - def apply_settings(cls, project_settings, system_settings): + def apply_settings(cls, project_settings): """Apply project settings to creator""" settings = ( project_settings["maya"]["publish"]["ValidateMayaUnits"] From b601da0dbba469c1a6920622218d74ada0f589f5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 7 Sep 2023 11:40:59 +0200 Subject: [PATCH 49/70] do not fix folder in representation context --- openpype/client/server/conversion_utils.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/client/server/conversion_utils.py b/openpype/client/server/conversion_utils.py index a6c190a0fc..2dca9ad57d 100644 --- a/openpype/client/server/conversion_utils.py +++ b/openpype/client/server/conversion_utils.py @@ -663,8 +663,8 @@ def convert_v4_representation_to_v3(representation): if isinstance(context, six.string_types): context = json.loads(context) - if "folder" in context: - _c_folder = context.pop("folder") + if "asset" not in context and "folder" in context: + _c_folder = context["folder"] context["asset"] = _c_folder["name"] if "product" in context: @@ -959,9 +959,11 @@ def convert_create_representation_to_v4(representation, con): converted_representation["files"] = new_files context = representation["context"] - context["folder"] = { - "name": context.pop("asset", None) - } + if "folder" not in context: + context["folder"] = { + "name": context.get("asset") + } + context["product"] = { "type": context.pop("family", None), "name": context.pop("subset", None), @@ -1285,7 +1287,7 @@ def convert_update_representation_to_v4( if "context" in update_data: context = update_data["context"] - if "asset" in context: + if "folder" not in context and "asset" in context: context["folder"] = {"name": context.pop("asset")} if "family" in context or "subset" in context: From 7cde83933a22437d3ba4a570f3c2a77f0396e810 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 7 Sep 2023 11:50:16 +0200 Subject: [PATCH 50/70] autofix wrong representations --- openpype/client/server/conversion_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/client/server/conversion_utils.py b/openpype/client/server/conversion_utils.py index 2dca9ad57d..f67a1ef9c4 100644 --- a/openpype/client/server/conversion_utils.py +++ b/openpype/client/server/conversion_utils.py @@ -667,6 +667,9 @@ def convert_v4_representation_to_v3(representation): _c_folder = context["folder"] context["asset"] = _c_folder["name"] + elif "asset" in context and "folder" not in context: + context["folder"] = {"name": context["asset"]} + if "product" in context: _c_product = context.pop("product") context["family"] = _c_product["type"] From 2c0a8d41006c5886d0e05e85c302f00b70c8e8f9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Sep 2023 14:47:27 +0200 Subject: [PATCH 51/70] adding default factory to list objects --- .../nuke/server/settings/create_plugins.py | 4 ++++ server_addon/nuke/server/settings/imageio.py | 2 ++ .../nuke/server/settings/publish_plugins.py | 3 +++ .../nuke/server/settings/workfile_builder.py | 20 ++++++++++++++----- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/server_addon/nuke/server/settings/create_plugins.py b/server_addon/nuke/server/settings/create_plugins.py index 0bbae4ee77..b89188a77a 100644 --- a/server_addon/nuke/server/settings/create_plugins.py +++ b/server_addon/nuke/server/settings/create_plugins.py @@ -37,6 +37,7 @@ class PrenodeModel(BaseSettingsModel): - We could not support v3 style of settings. """ knobs: list[KnobModel] = Field( + default_factory=list, title="Knobs", ) @@ -66,6 +67,7 @@ class CreateWriteRenderModel(BaseSettingsModel): (we could not support v3 style of settings) """ prenodes: list[PrenodeModel] = Field( + default_factory=list, title="Preceding nodes", ) @@ -95,6 +97,7 @@ class CreateWritePrerenderModel(BaseSettingsModel): (we could not support v3 style of settings) """ prenodes: list[PrenodeModel] = Field( + default_factory=list, title="Preceding nodes", ) @@ -124,6 +127,7 @@ class CreateWriteImageModel(BaseSettingsModel): (we could not support v3 style of settings) """ prenodes: list[PrenodeModel] = Field( + default_factory=list, title="Preceding nodes", ) diff --git a/server_addon/nuke/server/settings/imageio.py b/server_addon/nuke/server/settings/imageio.py index b43017ef8b..16f9bd309a 100644 --- a/server_addon/nuke/server/settings/imageio.py +++ b/server_addon/nuke/server/settings/imageio.py @@ -14,6 +14,7 @@ class NodesModel(BaseSettingsModel): """ _layout = "expanded" plugins: list[str] = Field( + default_factory=list, title="Used in plugins" ) # TODO: rename `nukeNodeClass` to `nuke_node_class` @@ -25,6 +26,7 @@ class NodesModel(BaseSettingsModel): in nuke integration. We could not support v3 style of settings. """ knobs: list[KnobModel] = Field( + default_factory=list, title="Knobs", ) diff --git a/server_addon/nuke/server/settings/publish_plugins.py b/server_addon/nuke/server/settings/publish_plugins.py index 7e898f8c9a..de21870d5f 100644 --- a/server_addon/nuke/server/settings/publish_plugins.py +++ b/server_addon/nuke/server/settings/publish_plugins.py @@ -46,6 +46,7 @@ class NodeModel(BaseSettingsModel): - We could not support v3 style of settings. """ knobs: list[KnobModel] = Field( + default_factory=list, title="Knobs", ) @@ -105,6 +106,7 @@ class ExtractThumbnailModel(BaseSettingsModel): """ nodes: list[NodeModel] = Field( + default_factory=list, title="Nodes (deprecated)" ) reposition_nodes: list[ThumbnailRepositionNodeModel] = Field( @@ -177,6 +179,7 @@ class ExtractReviewDataMovModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") viewer_lut_raw: bool = Field(title="Viewer lut raw") outputs: list[BakingStreamModel] = Field( + default_factory=list, title="Baking streams" ) diff --git a/server_addon/nuke/server/settings/workfile_builder.py b/server_addon/nuke/server/settings/workfile_builder.py index ee67c7c16a..e9ac0db2a0 100644 --- a/server_addon/nuke/server/settings/workfile_builder.py +++ b/server_addon/nuke/server/settings/workfile_builder.py @@ -48,20 +48,30 @@ class BuilderProfileModel(BaseSettingsModel): title="Task names" ) current_context: list[BuilderProfileItemModel] = Field( - title="Current context") + default_factory=list, + title="Current context" + ) linked_assets: list[BuilderProfileItemModel] = Field( - title="Linked assets/shots") + default_factory=list, + title="Linked assets/shots" + ) class WorkfileBuilderModel(BaseSettingsModel): create_first_version: bool = Field( title="Create first workfile") custom_templates: list[CustomTemplateModel] = Field( - title="Custom templates") + default_factory=list, + title="Custom templates" + ) builder_on_start: bool = Field( - title="Run Builder at first workfile") + default=False, + title="Run Builder at first workfile" + ) profiles: list[BuilderProfileModel] = Field( - title="Builder profiles") + default_factory=list, + title="Builder profiles" + ) DEFAULT_WORKFILE_BUILDER_SETTINGS = { From ee050fe63ba6c498fd736bcf2e1bcb1226e49ed9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Sep 2023 14:47:47 +0200 Subject: [PATCH 52/70] clearing todos and improving docstrings --- server_addon/nuke/server/settings/common.py | 6 ------ .../nuke/server/settings/create_plugins.py | 19 ------------------- server_addon/nuke/server/settings/dirmap.py | 13 ------------- server_addon/nuke/server/settings/imageio.py | 15 ++++----------- .../nuke/server/settings/loader_plugins.py | 8 -------- server_addon/nuke/server/settings/main.py | 4 +--- .../nuke/server/settings/publish_plugins.py | 17 ----------------- .../nuke/server/settings/scriptsmenu.py | 1 - .../settings/templated_workfile_build.py | 1 + .../nuke/server/settings/workfile_builder.py | 2 ++ 10 files changed, 8 insertions(+), 78 deletions(-) diff --git a/server_addon/nuke/server/settings/common.py b/server_addon/nuke/server/settings/common.py index 700f01f3dc..2bc3c9be81 100644 --- a/server_addon/nuke/server/settings/common.py +++ b/server_addon/nuke/server/settings/common.py @@ -89,12 +89,6 @@ knob_types_enum = [ class KnobModel(BaseSettingsModel): - """# TODO: new data structure - - v3 was having type, name, value but - ayon is not able to make it the same. Current model is - defining `type` as `text` and instead of `value` the key is `text`. - So if `type` is `boolean` then key is `boolean` (value). - """ _layout = "expanded" type: str = Field( diff --git a/server_addon/nuke/server/settings/create_plugins.py b/server_addon/nuke/server/settings/create_plugins.py index b89188a77a..80aec51ae0 100644 --- a/server_addon/nuke/server/settings/create_plugins.py +++ b/server_addon/nuke/server/settings/create_plugins.py @@ -16,13 +16,10 @@ def instance_attributes_enum(): class PrenodeModel(BaseSettingsModel): - # TODO: missing in host api - # - good for `dependency` name: str = Field( title="Node name" ) - # TODO: `nodeclass` should be renamed to `nuke_node_class` nodeclass: str = Field( "", title="Node class" @@ -32,10 +29,6 @@ class PrenodeModel(BaseSettingsModel): title="Incoming dependency" ) - """# TODO: Changes in host api: - - Need complete rework of knob types in nuke integration. - - We could not support v3 style of settings. - """ knobs: list[KnobModel] = Field( default_factory=list, title="Knobs", @@ -62,10 +55,6 @@ class CreateWriteRenderModel(BaseSettingsModel): title="Instance attributes" ) - """# TODO: Changes in host api: - - prenodes key was originally dict and now is list - (we could not support v3 style of settings) - """ prenodes: list[PrenodeModel] = Field( default_factory=list, title="Preceding nodes", @@ -92,10 +81,6 @@ class CreateWritePrerenderModel(BaseSettingsModel): title="Instance attributes" ) - """# TODO: Changes in host api: - - prenodes key was originally dict and now is list - (we could not support v3 style of settings) - """ prenodes: list[PrenodeModel] = Field( default_factory=list, title="Preceding nodes", @@ -122,10 +107,6 @@ class CreateWriteImageModel(BaseSettingsModel): title="Instance attributes" ) - """# TODO: Changes in host api: - - prenodes key was originally dict and now is list - (we could not support v3 style of settings) - """ prenodes: list[PrenodeModel] = Field( default_factory=list, title="Preceding nodes", diff --git a/server_addon/nuke/server/settings/dirmap.py b/server_addon/nuke/server/settings/dirmap.py index 2da6d7bf60..7e3c443957 100644 --- a/server_addon/nuke/server/settings/dirmap.py +++ b/server_addon/nuke/server/settings/dirmap.py @@ -25,19 +25,6 @@ class DirmapSettings(BaseSettingsModel): ) -"""# TODO: -nuke is having originally implemented -following data inputs: - -"nuke-dirmap": { - "enabled": false, - "paths": { - "source-path": [], - "destination-path": [] - } -} -""" - DEFAULT_DIRMAP_SETTINGS = { "enabled": False, "paths": { diff --git a/server_addon/nuke/server/settings/imageio.py b/server_addon/nuke/server/settings/imageio.py index 16f9bd309a..44bb72769e 100644 --- a/server_addon/nuke/server/settings/imageio.py +++ b/server_addon/nuke/server/settings/imageio.py @@ -9,22 +9,15 @@ from .common import KnobModel class NodesModel(BaseSettingsModel): - """# TODO: This needs to be somehow labeled in settings panel - or at least it could show gist of configuration - """ _layout = "expanded" plugins: list[str] = Field( default_factory=list, title="Used in plugins" ) - # TODO: rename `nukeNodeClass` to `nuke_node_class` nukeNodeClass: str = Field( title="Nuke Node Class", ) - """ # TODO: Need complete rework of knob types - in nuke integration. We could not support v3 style of settings. - """ knobs: list[KnobModel] = Field( default_factory=list, title="Knobs", @@ -172,7 +165,7 @@ class ImageIOSettings(BaseSettingsModel): _isGroup: bool = True """# TODO: enhance settings with host api: - to restruture settings for simplification. + to restructure settings for simplification. now: nuke/imageio/viewer/viewerProcess future: nuke/imageio/viewer @@ -195,7 +188,7 @@ class ImageIOSettings(BaseSettingsModel): ) """# TODO: enhance settings with host api: - to restruture settings for simplification. + to restructure settings for simplification. now: nuke/imageio/baking/viewerProcess future: nuke/imageio/baking @@ -217,9 +210,9 @@ class ImageIOSettings(BaseSettingsModel): title="Nodes" ) """# TODO: enhance settings with host api: - - old settings are using `regexInputs` key but we + - [ ] old settings are using `regexInputs` key but we need to rename to `regex_inputs` - - no need for `inputs` middle part. It can stay + - [ ] no need for `inputs` middle part. It can stay directly on `regex_inputs` """ regexInputs: RegexInputsModel = Field( diff --git a/server_addon/nuke/server/settings/loader_plugins.py b/server_addon/nuke/server/settings/loader_plugins.py index 6db381bffb..51e2c2149b 100644 --- a/server_addon/nuke/server/settings/loader_plugins.py +++ b/server_addon/nuke/server/settings/loader_plugins.py @@ -6,10 +6,6 @@ class LoadImageModel(BaseSettingsModel): enabled: bool = Field( title="Enabled" ) - """# TODO: v3 api used `_representation` - New api is hiding it so it had to be renamed - to `representations_include` - """ representations_include: list[str] = Field( default_factory=list, title="Include representations" @@ -33,10 +29,6 @@ class LoadClipModel(BaseSettingsModel): enabled: bool = Field( title="Enabled" ) - """# TODO: v3 api used `_representation` - New api is hiding it so it had to be renamed - to `representations_include` - """ representations_include: list[str] = Field( default_factory=list, title="Include representations" diff --git a/server_addon/nuke/server/settings/main.py b/server_addon/nuke/server/settings/main.py index 4687d48ac9..cdaaa3a9e2 100644 --- a/server_addon/nuke/server/settings/main.py +++ b/server_addon/nuke/server/settings/main.py @@ -59,9 +59,7 @@ class NukeSettings(BaseSettingsModel): default_factory=ImageIOSettings, title="Color Management (imageio)", ) - """# TODO: fix host api: - - rename `nuke-dirmap` to `dirmap` was inevitable - """ + dirmap: DirmapSettings = Field( default_factory=DirmapSettings, title="Nuke Directory Mapping", diff --git a/server_addon/nuke/server/settings/publish_plugins.py b/server_addon/nuke/server/settings/publish_plugins.py index de21870d5f..c78685534f 100644 --- a/server_addon/nuke/server/settings/publish_plugins.py +++ b/server_addon/nuke/server/settings/publish_plugins.py @@ -28,11 +28,9 @@ def nuke_product_types_enum(): class NodeModel(BaseSettingsModel): - # TODO: missing in host api name: str = Field( title="Node name" ) - # TODO: `nodeclass` rename to `nuke_node_class` nodeclass: str = Field( "", title="Node class" @@ -41,10 +39,6 @@ class NodeModel(BaseSettingsModel): "", title="Incoming dependency" ) - """# TODO: Changes in host api: - - Need complete rework of knob types in nuke integration. - - We could not support v3 style of settings. - """ knobs: list[KnobModel] = Field( default_factory=list, title="Knobs", @@ -100,10 +94,6 @@ class ExtractThumbnailModel(BaseSettingsModel): use_rendered: bool = Field(title="Use rendered images") bake_viewer_process: bool = Field(title="Bake view process") bake_viewer_input_process: bool = Field(title="Bake viewer input process") - """# TODO: needs to rewrite from v3 to ayon - - `nodes` in v3 was dict but now `prenodes` is list of dict - - also later `nodes` should be `prenodes` - """ nodes: list[NodeModel] = Field( default_factory=list, @@ -216,12 +206,6 @@ class ExctractSlateFrameParamModel(BaseSettingsModel): class ExtractSlateFrameModel(BaseSettingsModel): viewer_lut_raw: bool = Field(title="Viewer lut raw") - """# TODO: v3 api different model: - - not possible to replicate v3 model: - {"name": [bool, str]} - - not it is: - {"name": {"enabled": bool, "template": str}} - """ key_value_mapping: ExctractSlateFrameParamModel = Field( title="Key value mapping", default_factory=ExctractSlateFrameParamModel @@ -290,7 +274,6 @@ class PublishPuginsModel(BaseSettingsModel): title="Extract Slate Frame", default_factory=ExtractSlateFrameModel ) - # TODO: plugin should be renamed - `workfile` not `script` IncrementScriptVersion: IncrementScriptVersionModel = Field( title="Increment Workfile Version", default_factory=IncrementScriptVersionModel, diff --git a/server_addon/nuke/server/settings/scriptsmenu.py b/server_addon/nuke/server/settings/scriptsmenu.py index 9d1c32ebac..0b2d660da5 100644 --- a/server_addon/nuke/server/settings/scriptsmenu.py +++ b/server_addon/nuke/server/settings/scriptsmenu.py @@ -17,7 +17,6 @@ class ScriptsmenuSettings(BaseSettingsModel): """Nuke script menu project settings.""" _isGroup = True - # TODO: in api rename key `name` to `menu_name` name: str = Field(title="Menu Name") definition: list[ScriptsmenuSubmodel] = Field( default_factory=list, diff --git a/server_addon/nuke/server/settings/templated_workfile_build.py b/server_addon/nuke/server/settings/templated_workfile_build.py index e0245c8d06..0899be841e 100644 --- a/server_addon/nuke/server/settings/templated_workfile_build.py +++ b/server_addon/nuke/server/settings/templated_workfile_build.py @@ -28,6 +28,7 @@ class TemplatedWorkfileProfileModel(BaseSettingsModel): class TemplatedWorkfileBuildModel(BaseSettingsModel): + """Settings for templated workfile builder.""" profiles: list[TemplatedWorkfileProfileModel] = Field( default_factory=list ) diff --git a/server_addon/nuke/server/settings/workfile_builder.py b/server_addon/nuke/server/settings/workfile_builder.py index e9ac0db2a0..3ae3b08788 100644 --- a/server_addon/nuke/server/settings/workfile_builder.py +++ b/server_addon/nuke/server/settings/workfile_builder.py @@ -58,6 +58,8 @@ class BuilderProfileModel(BaseSettingsModel): class WorkfileBuilderModel(BaseSettingsModel): + """[deprecated] use Template Workfile Build Settings instead. + """ create_first_version: bool = Field( title="Create first workfile") custom_templates: list[CustomTemplateModel] = Field( From 0836b21116690fa1c7f11dfb0c5e24391a558d6a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 7 Sep 2023 14:23:51 +0200 Subject: [PATCH 53/70] Fix usage of `out_SET` and `controls_SET` since #5310 because they can now be prefixed - Collect the rig sets only once (I've ordered it before Collect History so that the instance still contains less node, as an optimization) - Also fixes a hard error when `out_SET` is not found, instead now only relevant `PublishValidationError` are raised to generate a nice report --- .../maya/plugins/publish/collect_rig_sets.py | 39 +++++++++++++++++ .../plugins/publish/validate_rig_contents.py | 42 +++++++++++++------ .../publish/validate_rig_controllers.py | 36 +++++++++++----- ...idate_rig_controllers_arnold_attributes.py | 6 +-- .../publish/validate_rig_out_set_node_ids.py | 11 +++-- .../publish/validate_rig_output_ids.py | 5 ++- 6 files changed, 108 insertions(+), 31 deletions(-) create mode 100644 openpype/hosts/maya/plugins/publish/collect_rig_sets.py diff --git a/openpype/hosts/maya/plugins/publish/collect_rig_sets.py b/openpype/hosts/maya/plugins/publish/collect_rig_sets.py new file mode 100644 index 0000000000..36a4211af1 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/collect_rig_sets.py @@ -0,0 +1,39 @@ +import pyblish.api +from maya import cmds + + +class CollectRigSets(pyblish.api.InstancePlugin): + """Ensure rig contains pipeline-critical content + + Every rig must contain at least two object sets: + "controls_SET" - Set of all animatable controls + "out_SET" - Set of all cacheable meshes + + """ + + order = pyblish.api.CollectorOrder + 0.05 + label = "Collect Rig Sets" + hosts = ["maya"] + families = ["rig"] + + accepted_output = ["mesh", "transform"] + accepted_controllers = ["transform"] + + def process(self, instance): + + # Find required sets by suffix + searching = {"controls_SET", "out_SET"} + found = {} + for node in cmds.ls(instance, exactType="objectSet"): + for suffix in searching: + if node.endswith(suffix): + found[suffix] = node + searching.remove(suffix) + break + if not searching: + break + + self.log.debug("Found sets: {}".format(found)) + rig_sets = instance.data.setdefault("rig_sets", {}) + for name, objset in found.items(): + rig_sets[name] = objset diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py index 7b5392f8f9..23f031a5db 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py @@ -2,7 +2,9 @@ import pyblish.api from maya import cmds from openpype.pipeline.publish import ( - PublishValidationError, ValidateContentsOrder) + PublishValidationError, + ValidateContentsOrder +) class ValidateRigContents(pyblish.api.InstancePlugin): @@ -24,31 +26,45 @@ class ValidateRigContents(pyblish.api.InstancePlugin): def process(self, instance): - objectsets = ("controls_SET", "out_SET") - missing = [obj for obj in objectsets if obj not in instance] - assert not missing, ("%s is missing %s" % (instance, missing)) + # Find required sets by suffix + required = ["controls_SET", "out_SET"] + missing = [ + key for key in required if key not in instance.data["rig_sets"] + ] + if missing: + raise PublishValidationError( + "%s is missing sets: %s" % (instance, ", ".join(missing)) + ) + + controls_set = instance.data["rig_sets"]["controls_SET"] + out_set = instance.data["rig_sets"]["out_SET"] # Ensure there are at least some transforms or dag nodes # in the rig instance set_members = instance.data['setMembers'] if not cmds.ls(set_members, type="dagNode", long=True): raise PublishValidationError( - ("No dag nodes in the pointcache instance. " - "(Empty instance?)")) + "No dag nodes in the pointcache instance. " + "(Empty instance?)" + ) # Ensure contents in sets and retrieve long path for all objects - output_content = cmds.sets("out_SET", query=True) or [] - assert output_content, "Must have members in rig out_SET" + output_content = cmds.sets(out_set, query=True) or [] + if not output_content: + raise PublishValidationError("Must have members in rig out_SET") output_content = cmds.ls(output_content, long=True) - controls_content = cmds.sets("controls_SET", query=True) or [] - assert controls_content, "Must have members in rig controls_SET" + controls_content = cmds.sets(controls_set, query=True) or [] + if not controls_content: + raise PublishValidationError( + "Must have members in rig controls_SET" + ) controls_content = cmds.ls(controls_content, long=True) # Validate members are inside the hierarchy from root node - root_node = cmds.ls(set_members, assemblies=True) - hierarchy = cmds.listRelatives(root_node, allDescendents=True, - fullPath=True) + root_nodes = cmds.ls(set_members, assemblies=True, long=True) + hierarchy = cmds.listRelatives(root_nodes, allDescendents=True, + fullPath=True) + root_nodes hierarchy = set(hierarchy) invalid_hierarchy = [] diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py b/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py index 7bbf4257ab..a3828f871b 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py @@ -52,22 +52,30 @@ class ValidateRigControllers(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise PublishValidationError('{} failed, see log ' - 'information'.format(self.label)) + raise PublishValidationError( + '{} failed, see log information'.format(self.label) + ) @classmethod def get_invalid(cls, instance): - controllers_sets = [i for i in instance if i == "controls_SET"] - controls = cmds.sets(controllers_sets, query=True) - assert controls, "Must have 'controls_SET' in rig instance" + controls_set = instance.data["rig_sets"].get("controls_SET") + if not controls_set: + cls.log.error( + "Must have 'controls_SET' in rig instance" + ) + return [instance.data["instance_node"]] + + controls = cmds.sets(controls_set, query=True) # Ensure all controls are within the top group lookup = set(instance[:]) - assert all(control in lookup for control in cmds.ls(controls, - long=True)), ( - "All controls must be inside the rig's group." - ) + if not all(control in lookup for control in cmds.ls(controls, + long=True)): + cls.log.error( + "All controls must be inside the rig's group." + ) + return [controls_set] # Validate all controls has_connections = list() @@ -181,9 +189,17 @@ class ValidateRigControllers(pyblish.api.InstancePlugin): @classmethod def repair(cls, instance): + controls_set = instance.data["rig_sets"].get("controls_SET") + if not controls_set: + cls.log.error( + "Unable to repair because no 'controls_SET' found in rig " + "instance: {}".format(instance) + ) + return + # Use a single undo chunk with undo_chunk(): - controls = cmds.sets("controls_SET", query=True) + controls = cmds.sets(controls_set, query=True) for control in controls: # Lock visibility diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py b/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py index 842c1de01b..03f6a5f1ab 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py @@ -56,11 +56,11 @@ class ValidateRigControllersArnoldAttributes(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - controllers_sets = [i for i in instance if i == "controls_SET"] - if not controllers_sets: + controls_set = instance.data["rig_sets"].get("controls_SET") + if not controls_set: return [] - controls = cmds.sets(controllers_sets, query=True) or [] + controls = cmds.sets(controls_set, query=True) or [] if not controls: return [] diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py index 39f0941faa..fbd510c683 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py @@ -38,16 +38,19 @@ class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin): # if a deformer has been created on the shape invalid = self.get_invalid(instance) if invalid: - raise PublishValidationError("Nodes found with mismatching " - "IDs: {0}".format(invalid)) + raise PublishValidationError( + "Nodes found with mismatching IDs: {0}".format(invalid) + ) @classmethod def get_invalid(cls, instance): """Get all nodes which do not match the criteria""" - invalid = [] + out_set = instance.data["rig_sets"].get("out_SET") + if not out_set: + return [] - out_set = next(x for x in instance if x.endswith("out_SET")) + invalid = [] members = cmds.sets(out_set, query=True) shapes = cmds.ls(members, dag=True, diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py index cbc750bace..24fb36eb8b 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py @@ -47,7 +47,10 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): invalid = {} if compute: - out_set = next(x for x in instance if "out_SET" in x) + out_set = instance.data["rig_sets"].get("out_SET") + if not out_set: + instance.data["mismatched_output_ids"] = invalid + return invalid instance_nodes = cmds.sets(out_set, query=True, nodesOnly=True) instance_nodes = cmds.ls(instance_nodes, long=True) From a492addc54691fd23ce462cd06284809a3edeaa0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 7 Sep 2023 15:55:28 +0200 Subject: [PATCH 54/70] Reduce artist-facing logs for component integration for Ftrack + tweak "Comment is not set" log also for Kitsu to debug level --- .../ftrack/plugins/publish/integrate_ftrack_api.py | 12 ++++++------ .../plugins/publish/integrate_ftrack_description.py | 2 +- .../ftrack/plugins/publish/integrate_ftrack_note.py | 6 +++--- .../kitsu/plugins/publish/integrate_kitsu_note.py | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py index 4d474fab10..858c0bb2d6 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py @@ -27,8 +27,8 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): def process(self, instance): component_list = instance.data.get("ftrackComponentsList") if not component_list: - self.log.info( - "Instance don't have components to integrate to Ftrack." + self.log.debug( + "Instance doesn't have components to integrate to Ftrack." " Skipping." ) return @@ -37,7 +37,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): task_entity, parent_entity = self.get_instance_entities( instance, context) if parent_entity is None: - self.log.info(( + self.log.debug(( "Skipping ftrack integration. Instance \"{}\" does not" " have specified ftrack entities." ).format(str(instance))) @@ -323,7 +323,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): "type_id": asset_type_id, "context_id": parent_id } - self.log.info("Created new Asset with data: {}.".format(asset_data)) + self.log.debug("Created new Asset with data: {}.".format(asset_data)) session.create("Asset", asset_data) session.commit() return self._query_asset(session, asset_name, asset_type_id, parent_id) @@ -384,7 +384,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): if comment: new_asset_version_data["comment"] = comment - self.log.info("Created new AssetVersion with data {}".format( + self.log.debug("Created new AssetVersion with data {}".format( new_asset_version_data )) session.create("AssetVersion", new_asset_version_data) @@ -555,7 +555,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): location=location ) data["component"] = component_entity - self.log.info( + self.log.debug( ( "Created new Component with path: {0}, data: {1}," " metadata: {2}, location: {3}" diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_description.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_description.py index 6ed02bc8b6..ceaff8ff54 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_description.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_description.py @@ -40,7 +40,7 @@ class IntegrateFtrackDescription(pyblish.api.InstancePlugin): comment = instance.data["comment"] if not comment: - self.log.info("Comment is not set.") + self.log.debug("Comment is not set.") else: self.log.debug("Comment is set to `{}`".format(comment)) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py index 6e82897d89..10b7932cdf 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py @@ -47,7 +47,7 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): app_label = context.data["appLabel"] comment = instance.data["comment"] if not comment: - self.log.info("Comment is not set.") + self.log.debug("Comment is not set.") else: self.log.debug("Comment is set to `{}`".format(comment)) @@ -127,14 +127,14 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): note_text = StringTemplate.format_template(template, format_data) if not note_text.solved: - self.log.warning(( + self.log.debug(( "Note template require more keys then can be provided." "\nTemplate: {}\nMissing values for keys:{}\nData: {}" ).format(template, note_text.missing_keys, format_data)) continue if not note_text: - self.log.info(( + self.log.debug(( "Note for AssetVersion {} would be empty. Skipping." "\nTemplate: {}\nData: {}" ).format(asset_version["id"], template, format_data)) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 6e5dd056f3..b66e1f01e0 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -121,7 +121,7 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): publish_comment = self.format_publish_comment(instance) if not publish_comment: - self.log.info("Comment is not set.") + self.log.debug("Comment is not set.") else: self.log.debug("Comment is `{}`".format(publish_comment)) From 3a02964af5e4c2c9aac5da4770e59aeef913f98a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 7 Sep 2023 15:58:28 +0200 Subject: [PATCH 55/70] Do not show debug log about ffmpeg probe in artist-facing report --- openpype/lib/transcoding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 2bae28786e..6e323f55c1 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -724,7 +724,7 @@ def get_ffprobe_data(path_to_file, logger=None): """ if not logger: logger = logging.getLogger(__name__) - logger.info( + logger.debug( "Getting information about input \"{}\".".format(path_to_file) ) ffprobe_args = get_ffmpeg_tool_args("ffprobe") From 2919d241da617229efa47c9ae52854633e0100c4 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 7 Sep 2023 19:30:10 +0300 Subject: [PATCH 56/70] move import inside --- .../hosts/houdini/plugins/create/create_review.py | 12 ++++-------- .../plugins/publish/validate_review_colorspace.py | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_review.py b/openpype/hosts/houdini/plugins/create/create_review.py index c087c54f6c..60c34a358b 100644 --- a/openpype/hosts/houdini/plugins/create/create_review.py +++ b/openpype/hosts/houdini/plugins/create/create_review.py @@ -2,7 +2,6 @@ """Creator plugin for creating openGL reviews.""" from openpype.hosts.houdini.api import plugin from openpype.lib import EnumDef, BoolDef, NumberDef -from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa import os import hou @@ -87,7 +86,8 @@ class CreateReview(plugin.HoudiniCreator): # Set OCIO Colorspace to the default output colorspace # if there's OCIO - self.set_colorcorrect_to_default_view_space(instance_node) + if os.getenv("OCIO"): + self.set_colorcorrect_to_default_view_space(instance_node) to_lock = ["id", "family"] @@ -134,13 +134,9 @@ class CreateReview(plugin.HoudiniCreator): def set_colorcorrect_to_default_view_space(self, instance_node): """Set ociocolorspace to the default output space.""" + from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa - if os.getenv("OCIO") is None: - # No OCIO, skip setting ociocolorspace - return - - # if there's OCIO then set Color Correction parameter - # to OpenColorIO + # set Color Correction parameter to OpenColorIO instance_node.setParms({"colorcorrect": 2}) # Get default view space for ociocolorspace parm. diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 61c3a755d0..03ecd1b052 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -6,7 +6,6 @@ from openpype.pipeline import ( ) from openpype.pipeline.publish import RepairAction from openpype.hosts.houdini.api.action import SelectROPAction -from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa import os import hou @@ -69,6 +68,7 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, It is a helper action more than a repair action, used to set colorspace on opengl node to the default view. """ + from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa rop_node = hou.node(instance.data["instance_node"]) From d2f63f0cd489b490111c4fdba4916bfb2c1e2ad0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 7 Sep 2023 23:27:30 +0200 Subject: [PATCH 57/70] Fix look assigner showing no meshes if 'not found' representations are present --- openpype/hosts/maya/tools/mayalookassigner/commands.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/commands.py b/openpype/hosts/maya/tools/mayalookassigner/commands.py index a1290aa68d..5cc4f84931 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/commands.py +++ b/openpype/hosts/maya/tools/mayalookassigner/commands.py @@ -138,8 +138,13 @@ def create_items_from_nodes(nodes): asset_doc = asset_docs_by_id.get(asset_id) # Skip if asset id is not found if not asset_doc: - log.warning("Id not found in the database, skipping '%s'." % _id) - log.warning("Nodes: %s" % id_nodes) + log.warning( + "Id found on {num} nodes for which no asset is found database," + " skipping '{asset_id}'".format( + num=len(nodes), + asset_id=asset_id + ) + ) continue # Collect available look subsets for this asset From 076d16a50d21ceb1edfbc806b20554a971c01b40 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:45:00 +0200 Subject: [PATCH 58/70] Workfiles tool: Refactor workfiles tool (for AYON) (#5550) * ayon workfiles tool initial commit * separated models into smaller files * workfile can be listed and opened * added browse logic * added TODO for helper functions * modified abstract controller * implemented required methods * base of save dialog * added project settings to controller * set context of side panel on init * implemented save as dialog * cleanup expected selection * unify controller variable name * base of published workfiles * working published workfile copy * added more missing features from workfiles tool * Changed size policy of buttons to fill space vertically * added overlay messages * moved objects to abstraction * moved 'window.py' to widgets * small modifications in widgets * get_workfile_info returns object * filled docstrings in abstractions * finishing touches * backwards compatible work with host * close window on successfull open * remove indentation completelly * added style for overlay label * added handling of invalid host in controller * added overlay with message if host is not valid * added missing feature of disabled save * use ayon_workfiles in ayon mode * cleanup * hound fixes * use asset doc for 'change_current_context' * added duplication action * removed unused attributes and methods * refresh workarea view on save as finished * support host integrations without 'HostBase' * fix 'filepath' fill * reset item cache on save * do not handle filepath in prepare workfile * rename '_create_workfile_doc' > '_create_workfile_info_entity' * fill comment before formatting * fix column count by not calling 'clear' * more explicit name of method * use 'setHeaderData' to define header labels * mimic changes from workarea widget in published widget --- openpype/style/style.css | 4 + openpype/tools/ayon_workfiles/__init__.py | 0 openpype/tools/ayon_workfiles/abstract.py | 984 ++++++++++++++++++ openpype/tools/ayon_workfiles/control.py | 642 ++++++++++++ .../tools/ayon_workfiles/models/__init__.py | 10 + .../tools/ayon_workfiles/models/hierarchy.py | 225 ++++ .../tools/ayon_workfiles/models/selection.py | 91 ++ .../tools/ayon_workfiles/models/workfiles.py | 711 +++++++++++++ .../tools/ayon_workfiles/widgets/__init__.py | 6 + .../tools/ayon_workfiles/widgets/constants.py | 7 + .../ayon_workfiles/widgets/files_widget.py | 398 +++++++ .../widgets/files_widget_published.py | 378 +++++++ .../widgets/files_widget_workarea.py | 380 +++++++ .../ayon_workfiles/widgets/folders_widget.py | 324 ++++++ .../ayon_workfiles/widgets/save_as_dialog.py | 351 +++++++ .../ayon_workfiles/widgets/side_panel.py | 163 +++ .../ayon_workfiles/widgets/tasks_widget.py | 420 ++++++++ .../tools/ayon_workfiles/widgets/utils.py | 94 ++ .../tools/ayon_workfiles/widgets/window.py | 400 +++++++ openpype/tools/utils/host_tools.py | 30 +- 20 files changed, 5610 insertions(+), 8 deletions(-) create mode 100644 openpype/tools/ayon_workfiles/__init__.py create mode 100644 openpype/tools/ayon_workfiles/abstract.py create mode 100644 openpype/tools/ayon_workfiles/control.py create mode 100644 openpype/tools/ayon_workfiles/models/__init__.py create mode 100644 openpype/tools/ayon_workfiles/models/hierarchy.py create mode 100644 openpype/tools/ayon_workfiles/models/selection.py create mode 100644 openpype/tools/ayon_workfiles/models/workfiles.py create mode 100644 openpype/tools/ayon_workfiles/widgets/__init__.py create mode 100644 openpype/tools/ayon_workfiles/widgets/constants.py create mode 100644 openpype/tools/ayon_workfiles/widgets/files_widget.py create mode 100644 openpype/tools/ayon_workfiles/widgets/files_widget_published.py create mode 100644 openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py create mode 100644 openpype/tools/ayon_workfiles/widgets/folders_widget.py create mode 100644 openpype/tools/ayon_workfiles/widgets/save_as_dialog.py create mode 100644 openpype/tools/ayon_workfiles/widgets/side_panel.py create mode 100644 openpype/tools/ayon_workfiles/widgets/tasks_widget.py create mode 100644 openpype/tools/ayon_workfiles/widgets/utils.py create mode 100644 openpype/tools/ayon_workfiles/widgets/window.py diff --git a/openpype/style/style.css b/openpype/style/style.css index 5ce55aa658..ca368f84f8 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -1427,6 +1427,10 @@ CreateNextPageOverlay { background: rgba(0, 0, 0, 127); } +#OverlayFrameLabel { + font-size: 15pt; +} + #BreadcrumbsPathInput { padding: 2px; font-size: 9pt; diff --git a/openpype/tools/ayon_workfiles/__init__.py b/openpype/tools/ayon_workfiles/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/tools/ayon_workfiles/abstract.py b/openpype/tools/ayon_workfiles/abstract.py new file mode 100644 index 0000000000..e30a2c2499 --- /dev/null +++ b/openpype/tools/ayon_workfiles/abstract.py @@ -0,0 +1,984 @@ +import os +from abc import ABCMeta, abstractmethod + +import six +from openpype.style import get_default_entity_icon_color + + +class WorkfileInfo: + """Information about workarea file with possible additional from database. + + Args: + folder_id (str): Folder id. + task_id (str): Task id. + filepath (str): Filepath. + filesize (int): File size. + creation_time (int): Creation time (timestamp). + modification_time (int): Modification time (timestamp). + note (str): Note. + """ + + def __init__( + self, + folder_id, + task_id, + filepath, + filesize, + creation_time, + modification_time, + note, + ): + self.folder_id = folder_id + self.task_id = task_id + self.filepath = filepath + self.filesize = filesize + self.creation_time = creation_time + self.modification_time = modification_time + self.note = note + + def to_data(self): + """Converts WorkfileInfo item to data. + + Returns: + dict[str, Any]: Folder item data. + """ + + return { + "folder_id": self.folder_id, + "task_id": self.task_id, + "filepath": self.filepath, + "filesize": self.filesize, + "creation_time": self.creation_time, + "modification_time": self.modification_time, + "note": self.note, + } + + @classmethod + def from_data(cls, data): + """Re-creates WorkfileInfo item from data. + + Args: + data (dict[str, Any]): Workfile info item data. + + Returns: + WorkfileInfo: Workfile info item. + """ + + return cls(**data) + + +class FolderItem: + """Item representing folder entity on a server. + + Folder can be a child of another folder or a project. + + Args: + entity_id (str): Folder id. + parent_id (Union[str, None]): Parent folder id. If 'None' then project + is parent. + name (str): Name of folder. + label (str): Folder label. + icon_name (str): Name of icon from font awesome. + icon_color (str): Hex color string that will be used for icon. + """ + + def __init__( + self, entity_id, parent_id, name, label, icon_name, icon_color + ): + self.entity_id = entity_id + self.parent_id = parent_id + self.name = name + self.icon_name = icon_name or "fa.folder" + self.icon_color = icon_color or get_default_entity_icon_color() + self.label = label or name + + def to_data(self): + """Converts folder item to data. + + Returns: + dict[str, Any]: Folder item data. + """ + + return { + "entity_id": self.entity_id, + "parent_id": self.parent_id, + "name": self.name, + "label": self.label, + "icon_name": self.icon_name, + "icon_color": self.icon_color, + } + + @classmethod + def from_data(cls, data): + """Re-creates folder item from data. + + Args: + data (dict[str, Any]): Folder item data. + + Returns: + FolderItem: Folder item. + """ + + return cls(**data) + + +class TaskItem: + """Task item representing task entity on a server. + + Task is child of a folder. + + Task item has label that is used for display in UI. The label is by + default using task name and type. + + Args: + task_id (str): Task id. + name (str): Name of task. + task_type (str): Type of task. + parent_id (str): Parent folder id. + icon_name (str): Name of icon from font awesome. + icon_color (str): Hex color string that will be used for icon. + """ + + def __init__( + self, task_id, name, task_type, parent_id, icon_name, icon_color + ): + self.task_id = task_id + self.name = name + self.task_type = task_type + self.parent_id = parent_id + self.icon_name = icon_name or "fa.male" + self.icon_color = icon_color or get_default_entity_icon_color() + self._label = None + + @property + def id(self): + """Alias for task_id. + + Returns: + str: Task id. + """ + + return self.task_id + + @property + def label(self): + """Label of task item for UI. + + Returns: + str: Label of task item. + """ + + if self._label is None: + self._label = "{} ({})".format(self.name, self.task_type) + return self._label + + def to_data(self): + """Converts task item to data. + + Returns: + dict[str, Any]: Task item data. + """ + + return { + "task_id": self.task_id, + "name": self.name, + "parent_id": self.parent_id, + "task_type": self.task_type, + "icon_name": self.icon_name, + "icon_color": self.icon_color, + } + + @classmethod + def from_data(cls, data): + """Re-create task item from data. + + Args: + data (dict[str, Any]): Task item data. + + Returns: + TaskItem: Task item. + """ + + return cls(**data) + + +class FileItem: + """File item that represents a file. + + Can be used for both Workarea and Published workfile. Workarea file + will always exist on disk which is not the case for Published workfile. + + Args: + dirpath (str): Directory path of file. + filename (str): Filename. + modified (float): Modified timestamp. + representation_id (Optional[str]): Representation id of published + workfile. + filepath (Optional[str]): Prepared filepath. + exists (Optional[bool]): If file exists on disk. + """ + + def __init__( + self, + dirpath, + filename, + modified, + representation_id=None, + filepath=None, + exists=None + ): + self.filename = filename + self.dirpath = dirpath + self.modified = modified + self.representation_id = representation_id + self._filepath = filepath + self._exists = exists + + @property + def filepath(self): + """Filepath of file. + + Returns: + str: Full path to a file. + """ + + if self._filepath is None: + self._filepath = os.path.join(self.dirpath, self.filename) + return self._filepath + + @property + def exists(self): + """File is available. + + Returns: + bool: If file exists on disk. + """ + + if self._exists is None: + self._exists = os.path.exists(self.filepath) + return self._exists + + def to_data(self): + """Converts file item to data. + + Returns: + dict[str, Any]: File item data. + """ + + return { + "filename": self.filename, + "dirpath": self.dirpath, + "modified": self.modified, + "representation_id": self.representation_id, + "filepath": self.filepath, + "exists": self.exists, + } + + @classmethod + def from_data(cls, data): + """Re-creates file item from data. + + Args: + data (dict[str, Any]): File item data. + + Returns: + FileItem: File item. + """ + + required_keys = { + "filename", + "dirpath", + "modified", + "representation_id" + } + missing_keys = required_keys - set(data.keys()) + if missing_keys: + raise KeyError("Missing keys: {}".format(missing_keys)) + + return cls(**{ + key: data[key] + for key in required_keys + }) + + +class WorkareaFilepathResult: + """Result of workarea file formatting. + + Args: + root (str): Root path of workarea. + filename (str): Filename. + exists (bool): True if file exists. + filepath (str): Filepath. If not provided it will be constructed + from root and filename. + """ + + def __init__(self, root, filename, exists, filepath=None): + if not filepath and root and filename: + filepath = os.path.join(root, filename) + self.root = root + self.filename = filename + self.exists = exists + self.filepath = filepath + + +@six.add_metaclass(ABCMeta) +class AbstractWorkfilesCommon(object): + @abstractmethod + def is_host_valid(self): + """Host is valid for workfiles tool work. + + Returns: + bool: True if host is valid. + """ + + pass + + @abstractmethod + def get_workfile_extensions(self): + """Get possible workfile extensions. + + Defined by host implementation. + + Returns: + Iterable[str]: List of extensions. + """ + + pass + + @abstractmethod + def is_save_enabled(self): + """Is workfile save enabled. + + Returns: + bool: True if save is enabled. + """ + + pass + + @abstractmethod + def set_save_enabled(self, enabled): + """Enable or disabled workfile save. + + Args: + enabled (bool): Enable save workfile when True. + """ + + pass + + +class AbstractWorkfilesBackend(AbstractWorkfilesCommon): + # Current context + @abstractmethod + def get_host_name(self): + """Name of host. + + Returns: + str: Name of host. + """ + pass + + @abstractmethod + def get_current_project_name(self): + """Project name from current context of host. + + Returns: + str: Name of project. + """ + + pass + + @abstractmethod + def get_current_folder_id(self): + """Folder id from current context of host. + + Returns: + Union[str, None]: Folder id or None if host does not have + any context. + """ + + pass + + @abstractmethod + def get_current_task_name(self): + """Task name from current context of host. + + Returns: + Union[str, None]: Task name or None if host does not have + any context. + """ + + pass + + @abstractmethod + def get_current_workfile(self): + """Current workfile from current context of host. + + Returns: + Union[str, None]: Path to workfile or None if host does + not have opened specific file. + """ + + pass + + @property + @abstractmethod + def project_anatomy(self): + """Project anatomy for current project. + + Returns: + Anatomy: Project anatomy. + """ + + pass + + @property + @abstractmethod + def project_settings(self): + """Project settings for current project. + + Returns: + dict[str, Any]: Project settings. + """ + + pass + + @abstractmethod + def get_folder_entity(self, folder_id): + """Get folder entity by id. + + Args: + folder_id (str): Folder id. + + Returns: + dict[str, Any]: Folder entity data. + """ + + pass + + @abstractmethod + def get_task_entity(self, task_id): + """Get task entity by id. + + Args: + task_id (str): Task id. + + Returns: + dict[str, Any]: Task entity data. + """ + + pass + + def emit_event(self, topic, data=None, source=None): + """Emit event. + + Args: + topic (str): Event topic used for callbacks filtering. + data (Optional[dict[str, Any]]): Event data. + source (Optional[str]): Event source. + """ + + pass + + +class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): + """UI controller abstraction that is used for workfiles tool frontend. + + Abstraction to provide data for UI and to handle UI events. + + Provide access to abstract backend data, like folders and tasks. Cares + about handling of selection, keep information about current UI selection + and have ability to tell what selection should UI show. + + Selection is separated into 2 parts, first is what UI elements tell + about selection, and second is what UI should show as selected. + """ + + @abstractmethod + def register_event_callback(self, topic, callback): + """Register event callback. + + Listen for events with given topic. + + Args: + topic (str): Name of topic. + callback (Callable): Callback that will be called when event + is triggered. + """ + + pass + + # Host information + @abstractmethod + def get_workfile_extensions(self): + """Each host can define extensions that can be used for workfile. + + Returns: + List[str]: File extensions that can be used as workfile for + current host. + """ + + pass + + # Selection information + @abstractmethod + def get_selected_folder_id(self): + """Currently selected folder id. + + Returns: + Union[str, None]: Folder id or None if no folder is selected. + """ + + pass + + @abstractmethod + def set_selected_folder(self, folder_id): + """Change selected folder. + + This deselects currently selected task. + + Args: + folder_id (Union[str, None]): Folder id or None if no folder + is selected. + """ + + pass + + @abstractmethod + def get_selected_task_id(self): + """Currently selected task id. + + Returns: + Union[str, None]: Task id or None if no folder is selected. + """ + + pass + + @abstractmethod + def get_selected_task_name(self): + """Currently selected task name. + + Returns: + Union[str, None]: Task name or None if no folder is selected. + """ + + pass + + @abstractmethod + def set_selected_task(self, folder_id, task_id, task_name): + """Change selected task. + + Args: + folder_id (Union[str, None]): Folder id or None if no folder + is selected. + task_id (Union[str, None]): Task id or None if no task + is selected. + task_name (Union[str, None]): Task name or None if no task + is selected. + """ + + pass + + @abstractmethod + def get_selected_workfile_path(self): + """Currently selected workarea workile. + + Returns: + Union[str, None]: Selected workfile path. + """ + + pass + + @abstractmethod + def set_selected_workfile_path(self, path): + """Change selected workfile path. + + Args: + path (Union[str, None]): Selected workfile path. + """ + + pass + + @abstractmethod + def get_selected_representation_id(self): + """Currently selected workfile representation id. + + Returns: + Union[str, None]: Representation id or None if no representation + is selected. + """ + + pass + + @abstractmethod + def set_selected_representation_id(self, representation_id): + """Change selected representation. + + Args: + representation_id (Union[str, None]): Selected workfile + representation id. + """ + + pass + + def get_selected_context(self): + """Obtain selected context. + + Returns: + dict[str, Union[str, None]]: Selected context. + """ + + return { + "folder_id": self.get_selected_folder_id(), + "task_id": self.get_selected_task_id(), + "task_name": self.get_selected_task_name(), + "workfile_path": self.get_selected_workfile_path(), + "representation_id": self.get_selected_representation_id(), + } + + # Expected selection + # - expected selection is used to restore selection after refresh + # or when current context should be used + @abstractmethod + def set_expected_selection( + self, + folder_id, + task_name, + workfile_name=None, + representation_id=None + ): + """Define what should be selected in UI. + + Expected selection provide a way to define/change selection of + sequential UI elements. For example, if folder and task should be + selected a task element should wait until folder element has selected + folder. + + Triggers 'expected_selection.changed' event. + + Args: + folder_id (str): Folder id. + task_name (str): Task name. + workfile_name (Optional[str]): Workfile name. Used for workarea + files UI element. + representation_id (Optional[str]): Representation id. Used for + published filed UI element. + """ + + pass + + @abstractmethod + def get_expected_selection_data(self): + """Data of expected selection. + + TODOs: + Return defined object instead of dict. + + Returns: + dict[str, Any]: Expected selection data. + """ + + pass + + @abstractmethod + def expected_folder_selected(self, folder_id): + """Expected folder was selected in UI. + + Args: + folder_id (str): Folder id which was selected. + """ + + pass + + @abstractmethod + def expected_task_selected(self, folder_id, task_name): + """Expected task was selected in UI. + + Args: + folder_id (str): Folder id under which task is. + task_name (str): Task name which was selected. + """ + + pass + + @abstractmethod + def expected_representation_selected(self, representation_id): + """Expected representation was selected in UI. + + Args: + representation_id (str): Representation id which was selected. + """ + + pass + + @abstractmethod + def expected_workfile_selected(self, workfile_path): + """Expected workfile was selected in UI. + + Args: + workfile_path (str): Workfile path which was selected. + """ + + pass + + @abstractmethod + def go_to_current_context(self): + """Set expected selection to current context.""" + + pass + + # Model functions + @abstractmethod + def get_folder_items(self, sender): + """Folder items to visualize project hierarchy. + + This function may trigger events 'folders.refresh.started' and + 'folders.refresh.finished' which will contain 'sender' value in data. + That may help to avoid re-refresh of folder items in UI elements. + + Args: + sender (str): Who requested folder items. + + Returns: + list[FolderItem]: Minimum possible information needed + for visualisation of folder hierarchy. + """ + + pass + + @abstractmethod + def get_task_items(self, folder_id, sender): + """Task items. + + This function may trigger events 'tasks.refresh.started' and + 'tasks.refresh.finished' which will contain 'sender' value in data. + That may help to avoid re-refresh of task items in UI elements. + + Args: + folder_id (str): Folder ID for which are tasks requested. + sender (str): Who requested folder items. + + Returns: + list[TaskItem]: Minimum possible information needed + for visualisation of tasks. + """ + + pass + + @abstractmethod + def has_unsaved_changes(self): + """Has host unsaved change in currently running session. + + Returns: + bool: Has unsaved changes. + """ + + pass + + @abstractmethod + def get_workarea_dir_by_context(self, folder_id, task_id): + """Get workarea directory by context. + + Args: + folder_id (str): Folder id. + task_id (str): Task id. + + Returns: + str: Workarea directory. + """ + + pass + + @abstractmethod + def get_workarea_file_items(self, folder_id, task_id): + """Get workarea file items. + + Args: + folder_id (str): Folder id. + task_id (str): Task id. + + Returns: + list[FileItem]: List of workarea file items. + """ + + pass + + @abstractmethod + def get_workarea_save_as_data(self, folder_id, task_id): + """Prepare data for Save As operation. + + Todos: + Return defined object instead of dict. + + Args: + folder_id (str): Folder id. + task_id (str): Task id. + + Returns: + dict[str, Any]: Data for Save As operation. + """ + + pass + + @abstractmethod + def fill_workarea_filepath( + self, + folder_id, + task_id, + extension, + use_last_version, + version, + comment, + ): + """Calculate workfile path for passed context. + + Args: + folder_id (str): Folder id. + task_id (str): Task id. + extension (str): File extension. + use_last_version (bool): Use last version. + version (int): Version used if 'use_last_version' if 'False'. + comment (str): User's comment (subversion). + + Returns: + WorkareaFilepathResult: Result of the operation. + """ + + pass + + @abstractmethod + def get_published_file_items(self, folder_id, task_id): + """Get published file items. + + Args: + folder_id (str): Folder id. + task_id (Union[str, None]): Task id. + + Returns: + list[FileItem]: List of published file items. + """ + + pass + + @abstractmethod + def get_workfile_info(self, folder_id, task_id, filepath): + """Workfile info from database. + + Args: + folder_id (str): Folder id. + task_id (str): Task id. + filepath (str): Workfile path. + + Returns: + Union[WorkfileInfo, None]: Workfile info or None if was passed + invalid context. + """ + + pass + + @abstractmethod + def save_workfile_info(self, folder_id, task_id, filepath, note): + """Save workfile info to database. + + At this moment the only information which can be saved about + workfile is 'note'. + + Args: + folder_id (str): Folder id. + task_id (str): Task id. + filepath (str): Workfile path. + note (str): Note. + """ + + pass + + # General commands + @abstractmethod + def refresh(self): + """Refresh everything, models, ui etc. + + Triggers 'controller.refresh.started' event at the beginning and + 'controller.refresh.finished' at the end. + """ + + pass + + # Controller actions + @abstractmethod + def open_workfile(self, filepath): + """Open a workfile. + + Args: + filepath (str): Workfile path. + """ + + pass + + @abstractmethod + def save_current_workfile(self): + """Save state of current workfile.""" + + pass + + @abstractmethod + def save_as_workfile( + self, + folder_id, + task_id, + workdir, + filename, + template_key, + ): + """Save current state of workfile to workarea. + + Args: + folder_id (str): Folder id. + task_id (str): Task id. + workdir (str): Workarea directory. + filename (str): Workarea filename. + template_key (str): Template key used to get the workdir + and filename. + """ + + pass + + @abstractmethod + def copy_workfile_representation( + self, + representation_id, + representation_filepath, + folder_id, + task_id, + workdir, + filename, + template_key, + ): + """Action to copy published workfile representation to workarea. + + Triggers 'copy_representation.started' event on start and + 'copy_representation.finished' event with '{"failed": bool}'. + + Args: + representation_id (str): Representation id. + representation_filepath (str): Path to representation file. + folder_id (str): Folder id. + task_id (str): Task id. + workdir (str): Workarea directory. + filename (str): Workarea filename. + template_key (str): Template key. + """ + + pass + + @abstractmethod + def duplicate_workfile(self, src_filepath, workdir, filename): + """Duplicate workfile. + + Workfiles is not opened when done. + + Args: + src_filepath (str): Source workfile path. + workdir (str): Destination workdir. + filename (str): Destination filename. + """ + + pass diff --git a/openpype/tools/ayon_workfiles/control.py b/openpype/tools/ayon_workfiles/control.py new file mode 100644 index 0000000000..fc8819bff3 --- /dev/null +++ b/openpype/tools/ayon_workfiles/control.py @@ -0,0 +1,642 @@ +import os +import shutil + +import ayon_api + +from openpype.client import get_asset_by_id +from openpype.host import IWorkfileHost +from openpype.lib import Logger, emit_event +from openpype.lib.events import QueuedEventSystem +from openpype.settings import get_project_settings +from openpype.pipeline import Anatomy, registered_host +from openpype.pipeline.context_tools import ( + change_current_context, + get_current_host_name, + get_global_context, +) +from openpype.pipeline.workfile import create_workdir_extra_folders + +from .abstract import ( + AbstractWorkfilesFrontend, + AbstractWorkfilesBackend, +) +from .models import SelectionModel, EntitiesModel, WorkfilesModel + + +class ExpectedSelection: + def __init__(self): + self._folder_id = None + self._task_name = None + self._workfile_name = None + self._representation_id = None + self._folder_selected = True + self._task_selected = True + self._workfile_name_selected = True + self._representation_id_selected = True + + def set_expected_selection( + self, + folder_id, + task_name, + workfile_name=None, + representation_id=None + ): + self._folder_id = folder_id + self._task_name = task_name + self._workfile_name = workfile_name + self._representation_id = representation_id + self._folder_selected = False + self._task_selected = False + self._workfile_name_selected = workfile_name is None + self._representation_id_selected = representation_id is None + + def get_expected_selection_data(self): + return { + "folder_id": self._folder_id, + "task_name": self._task_name, + "workfile_name": self._workfile_name, + "representation_id": self._representation_id, + "folder_selected": self._folder_selected, + "task_selected": self._task_selected, + "workfile_name_selected": self._workfile_name_selected, + "representation_id_selected": self._representation_id_selected, + } + + def is_expected_folder_selected(self, folder_id): + return folder_id == self._folder_id and self._folder_selected + + def is_expected_task_selected(self, folder_id, task_name): + if not self.is_expected_folder_selected(folder_id): + return False + return task_name == self._task_name and self._task_selected + + def expected_folder_selected(self, folder_id): + if folder_id != self._folder_id: + return False + self._folder_selected = True + return True + + def expected_task_selected(self, folder_id, task_name): + if not self.is_expected_folder_selected(folder_id): + return False + + if task_name != self._task_name: + return False + + self._task_selected = True + return True + + def expected_workfile_selected(self, folder_id, task_name, workfile_name): + if not self.is_expected_task_selected(folder_id, task_name): + return False + + if workfile_name != self._workfile_name: + return False + self._workfile_name_selected = True + return True + + def expected_representation_selected( + self, folder_id, task_name, representation_id + ): + if not self.is_expected_task_selected(folder_id, task_name): + return False + if representation_id != self._representation_id: + return False + self._representation_id_selected = True + return True + + +class BaseWorkfileController( + AbstractWorkfilesFrontend, AbstractWorkfilesBackend +): + def __init__(self, host=None): + if host is None: + host = registered_host() + + host_is_valid = False + if host is not None: + missing_methods = ( + IWorkfileHost.get_missing_workfile_methods(host) + ) + host_is_valid = len(missing_methods) == 0 + + self._host = host + self._host_is_valid = host_is_valid + + self._project_anatomy = None + self._project_settings = None + self._event_system = None + self._log = None + + self._current_project_name = None + self._current_folder_name = None + self._current_folder_id = None + self._current_task_name = None + self._save_is_enabled = True + + # Expected selected folder and task + self._expected_selection = self._create_expected_selection_obj() + + self._selection_model = self._create_selection_model() + self._entities_model = self._create_entities_model() + self._workfiles_model = self._create_workfiles_model() + + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger("WorkfilesUI") + return self._log + + def is_host_valid(self): + return self._host_is_valid + + def _create_expected_selection_obj(self): + return ExpectedSelection() + + def _create_selection_model(self): + return SelectionModel(self) + + def _create_entities_model(self): + return EntitiesModel(self) + + def _create_workfiles_model(self): + return WorkfilesModel(self) + + @property + def event_system(self): + """Inner event system for workfiles tool controller. + + Is used for communication with UI. Event system is created on demand. + + Returns: + QueuedEventSystem: Event system which can trigger callbacks + for topics. + """ + + if self._event_system is None: + self._event_system = QueuedEventSystem() + return self._event_system + + # ---------------------------------------------------- + # Implementation of methods required for backend logic + # ---------------------------------------------------- + @property + def project_settings(self): + if self._project_settings is None: + self._project_settings = get_project_settings( + self.get_current_project_name()) + return self._project_settings + + @property + def project_anatomy(self): + if self._project_anatomy is None: + self._project_anatomy = Anatomy(self.get_current_project_name()) + return self._project_anatomy + + def get_folder_entity(self, folder_id): + return self._entities_model.get_folder_entity(folder_id) + + def get_task_entity(self, task_id): + return self._entities_model.get_task_entity(task_id) + + # --------------------------------- + # Implementation of abstract methods + # --------------------------------- + def emit_event(self, topic, data=None, source=None): + """Use implemented event system to trigger event.""" + + if data is None: + data = {} + self.event_system.emit(topic, data, source) + + def register_event_callback(self, topic, callback): + self.event_system.add_callback(topic, callback) + + def is_save_enabled(self): + """Is workfile save enabled. + + Returns: + bool: True if save is enabled. + """ + + return self._save_is_enabled + + def set_save_enabled(self, enabled): + """Enable or disabled workfile save. + + Args: + enabled (bool): Enable save workfile when True. + """ + + if self._save_is_enabled == enabled: + return + + self._save_is_enabled = enabled + self._emit_event( + "workfile_save_enable.changed", + {"enabled": enabled} + ) + + # Host information + def get_workfile_extensions(self): + host = self._host + if isinstance(host, IWorkfileHost): + return host.get_workfile_extensions() + return host.file_extensions() + + def has_unsaved_changes(self): + host = self._host + if isinstance(host, IWorkfileHost): + return host.workfile_has_unsaved_changes() + return host.has_unsaved_changes() + + # Current context + def get_host_name(self): + host = self._host + if isinstance(host, IWorkfileHost): + return host.name + return get_current_host_name() + + def _get_host_current_context(self): + if hasattr(self._host, "get_current_context"): + return self._host.get_current_context() + return get_global_context() + + def get_current_project_name(self): + return self._current_project_name + + def get_current_folder_id(self): + return self._current_folder_id + + def get_current_task_name(self): + return self._current_task_name + + def get_current_workfile(self): + host = self._host + if isinstance(host, IWorkfileHost): + return host.get_current_workfile() + return host.current_file() + + # Selection information + def get_selected_folder_id(self): + return self._selection_model.get_selected_folder_id() + + def set_selected_folder(self, folder_id): + self._selection_model.set_selected_folder(folder_id) + + def get_selected_task_id(self): + return self._selection_model.get_selected_task_id() + + def get_selected_task_name(self): + return self._selection_model.get_selected_task_name() + + def set_selected_task(self, folder_id, task_id, task_name): + return self._selection_model.set_selected_task( + folder_id, task_id, task_name) + + def get_selected_workfile_path(self): + return self._selection_model.get_selected_workfile_path() + + def set_selected_workfile_path(self, path): + self._selection_model.set_selected_workfile_path(path) + + def get_selected_representation_id(self): + return self._selection_model.get_selected_representation_id() + + def set_selected_representation_id(self, representation_id): + self._selection_model.set_selected_representation_id( + representation_id) + + def set_expected_selection( + self, + folder_id, + task_name, + workfile_name=None, + representation_id=None + ): + self._expected_selection.set_expected_selection( + folder_id, task_name, workfile_name, representation_id + ) + self._trigger_expected_selection_changed() + + def expected_folder_selected(self, folder_id): + if self._expected_selection.expected_folder_selected(folder_id): + self._trigger_expected_selection_changed() + + def expected_task_selected(self, folder_id, task_name): + if self._expected_selection.expected_task_selected( + folder_id, task_name + ): + self._trigger_expected_selection_changed() + + def expected_workfile_selected(self, folder_id, task_name, workfile_name): + if self._expected_selection.expected_workfile_selected( + folder_id, task_name, workfile_name + ): + self._trigger_expected_selection_changed() + + def expected_representation_selected( + self, folder_id, task_name, representation_id + ): + if self._expected_selection.expected_representation_selected( + folder_id, task_name, representation_id + ): + self._trigger_expected_selection_changed() + + def get_expected_selection_data(self): + return self._expected_selection.get_expected_selection_data() + + def go_to_current_context(self): + self.set_expected_selection( + self._current_folder_id, self._current_task_name + ) + + # Model functions + def get_folder_items(self, sender): + return self._entities_model.get_folder_items(sender) + + def get_task_items(self, folder_id, sender): + return self._entities_model.get_tasks_items(folder_id, sender) + + def get_workarea_dir_by_context(self, folder_id, task_id): + return self._workfiles_model.get_workarea_dir_by_context( + folder_id, task_id) + + def get_workarea_file_items(self, folder_id, task_id): + return self._workfiles_model.get_workarea_file_items( + folder_id, task_id) + + def get_workarea_save_as_data(self, folder_id, task_id): + return self._workfiles_model.get_workarea_save_as_data( + folder_id, task_id) + + def fill_workarea_filepath( + self, + folder_id, + task_id, + extension, + use_last_version, + version, + comment, + ): + return self._workfiles_model.fill_workarea_filepath( + folder_id, + task_id, + extension, + use_last_version, + version, + comment, + ) + + def get_published_file_items(self, folder_id, task_id): + task_name = None + if task_id: + task = self.get_task_entity(task_id) + task_name = task.get("name") + + return self._workfiles_model.get_published_file_items( + folder_id, task_name) + + def get_workfile_info(self, folder_id, task_id, filepath): + return self._workfiles_model.get_workfile_info( + folder_id, task_id, filepath + ) + + def save_workfile_info(self, folder_id, task_id, filepath, note): + self._workfiles_model.save_workfile_info( + folder_id, task_id, filepath, note + ) + + def refresh(self): + if not self._host_is_valid: + self._emit_event("controller.refresh.started") + self._emit_event("controller.refresh.finished") + return + expected_folder_id = self.get_selected_folder_id() + expected_task_name = self.get_selected_task_name() + + self._emit_event("controller.refresh.started") + + context = self._get_host_current_context() + + project_name = context["project_name"] + folder_name = context["asset_name"] + task_name = context["task_name"] + folder_id = None + if folder_name: + folder = ayon_api.get_folder_by_name(project_name, folder_name) + if folder: + folder_id = folder["id"] + + self._project_settings = None + self._project_anatomy = None + + self._current_project_name = project_name + self._current_folder_name = folder_name + self._current_folder_id = folder_id + self._current_task_name = task_name + + if not expected_folder_id: + expected_folder_id = folder_id + expected_task_name = task_name + + self._expected_selection.set_expected_selection( + expected_folder_id, expected_task_name + ) + + self._entities_model.refresh() + + self._emit_event("controller.refresh.finished") + + # Controller actions + def open_workfile(self, filepath): + self._emit_event("open_workfile.started") + + failed = False + try: + self._host_open_workfile(filepath) + + except Exception: + failed = True + self.log.warning("Open of workfile failed", exc_info=True) + + self._emit_event( + "open_workfile.finished", + {"failed": failed}, + ) + + def save_current_workfile(self): + current_file = self.get_current_workfile() + self._host_save_workfile(current_file) + + def save_as_workfile( + self, + folder_id, + task_id, + workdir, + filename, + template_key, + ): + self._emit_event("save_as.started") + + failed = False + try: + self._save_as_workfile( + folder_id, + task_id, + workdir, + filename, + template_key, + ) + except Exception: + failed = True + self.log.warning("Save as failed", exc_info=True) + + self._emit_event( + "save_as.finished", + {"failed": failed}, + ) + + def copy_workfile_representation( + self, + representation_id, + representation_filepath, + folder_id, + task_id, + workdir, + filename, + template_key, + ): + self._emit_event("copy_representation.started") + + failed = False + try: + self._save_as_workfile( + folder_id, + task_id, + workdir, + filename, + template_key, + ) + except Exception: + failed = True + self.log.warning( + "Copy of workfile representation failed", exc_info=True + ) + + self._emit_event( + "copy_representation.finished", + {"failed": failed}, + ) + + def duplicate_workfile(self, src_filepath, workdir, filename): + self._emit_event("workfile_duplicate.started") + + failed = False + try: + dst_filepath = os.path.join(workdir, filename) + shutil.copy(src_filepath, dst_filepath) + except Exception: + failed = True + self.log.warning("Duplication of workfile failed", exc_info=True) + + self._emit_event( + "workfile_duplicate.finished", + {"failed": failed}, + ) + + # Helper host methods that resolve 'IWorkfileHost' interface + def _host_open_workfile(self, filepath): + host = self._host + if isinstance(host, IWorkfileHost): + host.open_workfile(filepath) + else: + host.open_file(filepath) + + def _host_save_workfile(self, filepath): + host = self._host + if isinstance(host, IWorkfileHost): + host.save_workfile(filepath) + else: + host.save_file(filepath) + + def _emit_event(self, topic, data=None): + self.emit_event(topic, data, "controller") + + # Expected selection + # - expected selection is used to restore selection after refresh + # or when current context should be used + def _trigger_expected_selection_changed(self): + self._emit_event( + "expected_selection_changed", + self._expected_selection.get_expected_selection_data(), + ) + + def _save_as_workfile( + self, + folder_id, + task_id, + workdir, + filename, + template_key, + src_filepath=None, + ): + # Trigger before save event + project_name = self.get_current_project_name() + folder = self.get_folder_entity(folder_id) + task = self.get_task_entity(task_id) + task_name = task["name"] + + # QUESTION should the data be different for 'before' and 'after'? + # NOTE keys should be OpenPype compatible + event_data = { + "project_name": project_name, + "folder_id": folder_id, + "asset_id": folder_id, + "asset_name": folder["name"], + "task_id": task_id, + "task_name": task_name, + "host_name": self.get_host_name(), + "filename": filename, + "workdir_path": workdir, + } + emit_event("workfile.save.before", event_data, source="workfiles.tool") + + # Create workfiles root folder + if not os.path.exists(workdir): + self.log.debug("Initializing work directory: %s", workdir) + os.makedirs(workdir) + + # Change context + if ( + folder_id != self.get_current_folder_id() + or task_name != self.get_current_task_name() + ): + # Use OpenPype asset-like object + asset_doc = get_asset_by_id(project_name, folder["id"]) + change_current_context( + asset_doc, + task["name"], + template_key=template_key + ) + + # Save workfile + dst_filepath = os.path.join(workdir, filename) + if src_filepath: + shutil.copyfile(src_filepath, dst_filepath) + self._host_open_workfile(dst_filepath) + else: + self._host_save_workfile(dst_filepath) + + # Create extra folders + create_workdir_extra_folders( + workdir, + self.get_host_name(), + task["taskType"], + task_name, + project_name + ) + + # Trigger after save events + emit_event("workfile.save.after", event_data, source="workfiles.tool") + self.refresh() diff --git a/openpype/tools/ayon_workfiles/models/__init__.py b/openpype/tools/ayon_workfiles/models/__init__.py new file mode 100644 index 0000000000..d906b9e7bd --- /dev/null +++ b/openpype/tools/ayon_workfiles/models/__init__.py @@ -0,0 +1,10 @@ +from .hierarchy import EntitiesModel +from .selection import SelectionModel +from .workfiles import WorkfilesModel + + +__all__ = ( + "SelectionModel", + "EntitiesModel", + "WorkfilesModel", +) diff --git a/openpype/tools/ayon_workfiles/models/hierarchy.py b/openpype/tools/ayon_workfiles/models/hierarchy.py new file mode 100644 index 0000000000..948c0b8a17 --- /dev/null +++ b/openpype/tools/ayon_workfiles/models/hierarchy.py @@ -0,0 +1,225 @@ +"""Hierarchy model that handles folders and tasks. + +The model can be extracted for common usage. In that case it will be required +to add more handling of project name changes. +""" + +import time +import collections +import contextlib + +import ayon_api + +from openpype.tools.ayon_workfiles.abstract import ( + FolderItem, + TaskItem, +) + + +def _get_task_items_from_tasks(tasks): + """ + + Returns: + TaskItem: Task item. + """ + + output = [] + for task in tasks: + folder_id = task["folderId"] + output.append(TaskItem( + task["id"], + task["name"], + task["type"], + folder_id, + None, + None + )) + return output + + +def _get_folder_item_from_hierarchy_item(item): + return FolderItem( + item["id"], + item["parentId"], + item["name"], + item["label"], + None, + None, + ) + + +class CacheItem: + def __init__(self, lifetime=120): + self._lifetime = lifetime + self._last_update = None + self._data = None + + @property + def is_valid(self): + if self._last_update is None: + return False + + return (time.time() - self._last_update) < self._lifetime + + def set_invalid(self, data=None): + self._last_update = None + self._data = data + + def get_data(self): + return self._data + + def update_data(self, data): + self._data = data + self._last_update = time.time() + + +class EntitiesModel(object): + event_source = "entities.model" + + def __init__(self, controller): + folders_cache = CacheItem() + folders_cache.set_invalid({}) + self._folders_cache = folders_cache + self._tasks_cache = {} + + self._folders_by_id = {} + self._tasks_by_id = {} + + self._folders_refreshing = False + self._tasks_refreshing = set() + self._controller = controller + + def reset(self): + self._folders_cache.set_invalid({}) + self._tasks_cache = {} + + self._folders_by_id = {} + self._tasks_by_id = {} + + def refresh(self): + self._refresh_folders_cache() + + def get_folder_items(self, sender): + if not self._folders_cache.is_valid: + self._refresh_folders_cache(sender) + return self._folders_cache.get_data() + + def get_tasks_items(self, folder_id, sender): + if not folder_id: + return [] + + task_cache = self._tasks_cache.get(folder_id) + if task_cache is None or not task_cache.is_valid: + self._refresh_tasks_cache(folder_id, sender) + task_cache = self._tasks_cache.get(folder_id) + return task_cache.get_data() + + def get_folder_entity(self, folder_id): + if folder_id not in self._folders_by_id: + entity = None + if folder_id: + project_name = self._controller.get_current_project_name() + entity = ayon_api.get_folder_by_id(project_name, folder_id) + self._folders_by_id[folder_id] = entity + return self._folders_by_id[folder_id] + + def get_task_entity(self, task_id): + if task_id not in self._tasks_by_id: + entity = None + if task_id: + project_name = self._controller.get_current_project_name() + entity = ayon_api.get_task_by_id(project_name, task_id) + self._tasks_by_id[task_id] = entity + return self._tasks_by_id[task_id] + + @contextlib.contextmanager + def _folder_refresh_event_manager(self, project_name, sender): + self._folders_refreshing = True + self._controller.emit_event( + "folders.refresh.started", + {"project_name": project_name, "sender": sender}, + self.event_source + ) + try: + yield + + finally: + self._controller.emit_event( + "folders.refresh.finished", + {"project_name": project_name, "sender": sender}, + self.event_source + ) + self._folders_refreshing = False + + @contextlib.contextmanager + def _task_refresh_event_manager( + self, project_name, folder_id, sender + ): + self._tasks_refreshing.add(folder_id) + self._controller.emit_event( + "tasks.refresh.started", + { + "project_name": project_name, + "folder_id": folder_id, + "sender": sender, + }, + self.event_source + ) + try: + yield + + finally: + self._controller.emit_event( + "tasks.refresh.finished", + { + "project_name": project_name, + "folder_id": folder_id, + "sender": sender, + }, + self.event_source + ) + self._tasks_refreshing.discard(folder_id) + + def _refresh_folders_cache(self, sender=None): + if self._folders_refreshing: + return + project_name = self._controller.get_current_project_name() + with self._folder_refresh_event_manager(project_name, sender): + folder_items = self._query_folders(project_name) + self._folders_cache.update_data(folder_items) + + def _query_folders(self, project_name): + hierarchy = ayon_api.get_folders_hierarchy(project_name) + + folder_items = {} + hierachy_queue = collections.deque(hierarchy["hierarchy"]) + while hierachy_queue: + item = hierachy_queue.popleft() + folder_item = _get_folder_item_from_hierarchy_item(item) + folder_items[folder_item.entity_id] = folder_item + hierachy_queue.extend(item["children"] or []) + return folder_items + + def _refresh_tasks_cache(self, folder_id, sender=None): + if folder_id in self._tasks_refreshing: + return + + project_name = self._controller.get_current_project_name() + with self._task_refresh_event_manager( + project_name, folder_id, sender + ): + cache_item = self._tasks_cache.get(folder_id) + if cache_item is None: + cache_item = CacheItem() + self._tasks_cache[folder_id] = cache_item + + task_items = self._query_tasks(project_name, folder_id) + cache_item.update_data(task_items) + + def _query_tasks(self, project_name, folder_id): + tasks = list(ayon_api.get_tasks( + project_name, + folder_ids=[folder_id], + fields={"id", "name", "label", "folderId", "type"} + )) + return _get_task_items_from_tasks(tasks) diff --git a/openpype/tools/ayon_workfiles/models/selection.py b/openpype/tools/ayon_workfiles/models/selection.py new file mode 100644 index 0000000000..ad034794d8 --- /dev/null +++ b/openpype/tools/ayon_workfiles/models/selection.py @@ -0,0 +1,91 @@ +class SelectionModel(object): + """Model handling selection changes. + + Triggering events: + - "selection.folder.changed" + - "selection.task.changed" + - "workarea.selection.changed" + - "selection.representation.changed" + """ + + event_source = "selection.model" + + def __init__(self, controller): + self._controller = controller + + self._folder_id = None + self._task_name = None + self._task_id = None + self._workfile_path = None + self._representation_id = None + + def get_selected_folder_id(self): + return self._folder_id + + def set_selected_folder(self, folder_id): + if folder_id == self._folder_id: + return + + self._folder_id = folder_id + self._controller.emit_event( + "selection.folder.changed", + {"folder_id": folder_id}, + self.event_source + ) + + def get_selected_task_name(self): + return self._task_name + + def get_selected_task_id(self): + return self._task_id + + def set_selected_task(self, folder_id, task_id, task_name): + if folder_id != self._folder_id: + self.set_selected_folder(folder_id) + + if task_id == self._task_id: + return + + self._task_name = task_name + self._task_id = task_id + self._controller.emit_event( + "selection.task.changed", + { + "folder_id": folder_id, + "task_name": task_name, + "task_id": task_id + }, + self.event_source + ) + + def get_selected_workfile_path(self): + return self._workfile_path + + def set_selected_workfile_path(self, path): + if path == self._workfile_path: + return + + self._workfile_path = path + self._controller.emit_event( + "workarea.selection.changed", + { + "path": path, + "folder_id": self._folder_id, + "task_name": self._task_name, + "task_id": self._task_id, + }, + self.event_source + ) + + def get_selected_representation_id(self): + return self._representation_id + + def set_selected_representation_id(self, representation_id): + if representation_id == self._representation_id: + return + self._representation_id = representation_id + self._controller.emit_event( + "selection.representation.changed", + {"representation_id": representation_id}, + self.event_source + ) diff --git a/openpype/tools/ayon_workfiles/models/workfiles.py b/openpype/tools/ayon_workfiles/models/workfiles.py new file mode 100644 index 0000000000..eb82f62de3 --- /dev/null +++ b/openpype/tools/ayon_workfiles/models/workfiles.py @@ -0,0 +1,711 @@ +import os +import re +import copy + +import arrow +import ayon_api +from ayon_api.operations import OperationsSession + +from openpype.client import get_project +from openpype.client.operations import ( + prepare_workfile_info_update_data, +) +from openpype.pipeline.template_data import ( + get_template_data, +) +from openpype.pipeline.workfile import ( + get_workdir_with_workdir_data, + get_workfile_template_key, + get_last_workfile_with_version, +) +from openpype.pipeline.version_start import get_versioning_start +from openpype.tools.ayon_workfiles.abstract import ( + WorkareaFilepathResult, + FileItem, + WorkfileInfo, +) + + +def get_folder_template_data(folder): + if not folder: + return {} + parts = folder["path"].split("/") + parts.pop(-1) + hierarchy = "/".join(parts) + return { + "asset": folder["name"], + "folder": { + "name": folder["name"], + "type": folder["folderType"], + "path": folder["path"], + }, + "hierarchy": hierarchy, + } + + +def get_task_template_data(task): + if not task: + return {} + return { + "task": { + "name": task["name"], + "type": task["taskType"] + } + } + + +class CommentMatcher(object): + """Use anatomy and work file data to parse comments from filenames""" + def __init__(self, extensions, file_template, data): + self.fname_regex = None + + if "{comment}" not in file_template: + # Don't look for comment if template doesn't allow it + return + + # Create a regex group for extensions + any_extension = "(?:{})".format( + "|".join(re.escape(ext.lstrip(".")) for ext in extensions) + ) + + # Use placeholders that will never be in the filename + temp_data = copy.deepcopy(data) + temp_data["comment"] = "<>" + temp_data["version"] = "<>" + temp_data["ext"] = "<>" + + fname_pattern = file_template.format_strict(temp_data) + fname_pattern = re.escape(fname_pattern) + + # Replace comment and version with something we can match with regex + replacements = { + "<>": "(.+)", + "<>": "[0-9]+", + "<>": any_extension, + } + for src, dest in replacements.items(): + fname_pattern = fname_pattern.replace(re.escape(src), dest) + + # Match from beginning to end of string to be safe + fname_pattern = "^{}$".format(fname_pattern) + + self.fname_regex = re.compile(fname_pattern) + + def parse_comment(self, filepath): + """Parse the {comment} part from a filename""" + if not self.fname_regex: + return + + fname = os.path.basename(filepath) + match = self.fname_regex.match(fname) + if match: + return match.group(1) + + +class WorkareaModel: + """Workfiles model looking for workfiles in workare folder. + + Workarea folder is usually task and host specific, defined by + anatomy templates. Is looking for files with extensions defined + by host integration. + """ + + def __init__(self, controller): + self._controller = controller + extensions = None + if controller.is_host_valid(): + extensions = controller.get_workfile_extensions() + self._extensions = extensions + self._base_data = None + self._fill_data_by_folder_id = {} + self._task_data_by_folder_id = {} + self._workdir_by_context = {} + + @property + def project_name(self): + return self._controller.get_current_project_name() + + def reset(self): + self._base_data = None + self._fill_data_by_folder_id = {} + self._task_data_by_folder_id = {} + + def _get_base_data(self): + if self._base_data is None: + base_data = get_template_data(get_project(self.project_name)) + base_data["app"] = self._controller.get_host_name() + self._base_data = base_data + return copy.deepcopy(self._base_data) + + def _get_folder_data(self, folder_id): + fill_data = self._fill_data_by_folder_id.get(folder_id) + if fill_data is None: + folder = self._controller.get_folder_entity(folder_id) + fill_data = get_folder_template_data(folder) + self._fill_data_by_folder_id[folder_id] = fill_data + return copy.deepcopy(fill_data) + + def _get_task_data(self, folder_id, task_id): + task_data = self._task_data_by_folder_id.setdefault(folder_id, {}) + if task_id not in task_data: + task = self._controller.get_task_entity(task_id) + if task: + task_data[task_id] = get_task_template_data(task) + return copy.deepcopy(task_data[task_id]) + + def _prepare_fill_data(self, folder_id, task_id): + if not folder_id or not task_id: + return {} + + base_data = self._get_base_data() + folder_data = self._get_folder_data(folder_id) + task_data = self._get_task_data(folder_id, task_id) + + base_data.update(folder_data) + base_data.update(task_data) + + return base_data + + def get_workarea_dir_by_context(self, folder_id, task_id): + if not folder_id or not task_id: + return None + folder_mapping = self._workdir_by_context.setdefault(folder_id, {}) + workdir = folder_mapping.get(task_id) + if workdir is not None: + return workdir + + workdir_data = self._prepare_fill_data(folder_id, task_id) + + workdir = get_workdir_with_workdir_data( + workdir_data, + self.project_name, + anatomy=self._controller.project_anatomy, + ) + folder_mapping[task_id] = workdir + return workdir + + def get_file_items(self, folder_id, task_id): + items = [] + if not folder_id or not task_id: + return items + + workdir = self.get_workarea_dir_by_context(folder_id, task_id) + if not os.path.exists(workdir): + return items + + for filename in os.listdir(workdir): + filepath = os.path.join(workdir, filename) + if not os.path.isfile(filepath): + continue + + ext = os.path.splitext(filename)[1].lower() + if ext not in self._extensions: + continue + + modified = os.path.getmtime(filepath) + items.append( + FileItem(workdir, filename, modified) + ) + return items + + def _get_template_key(self, fill_data): + task_type = fill_data.get("task", {}).get("type") + # TODO cache + return get_workfile_template_key( + task_type, + self._controller.get_host_name(), + project_name=self.project_name + ) + + def _get_last_workfile_version( + self, workdir, file_template, fill_data, extensions + ): + version = get_last_workfile_with_version( + workdir, str(file_template), fill_data, extensions + )[1] + + if version is None: + task_info = fill_data.get("task", {}) + version = get_versioning_start( + self.project_name, + self._controller.get_host_name(), + task_name=task_info.get("name"), + task_type=task_info.get("type"), + family="workfile", + project_settings=self._controller.project_settings, + ) + else: + version += 1 + return version + + def _get_comments_from_root( + self, + file_template, + extensions, + fill_data, + root, + current_filename, + ): + current_comment = None + comment_hints = set() + filenames = [] + if root and os.path.exists(root): + for filename in os.listdir(root): + path = os.path.join(root, filename) + if not os.path.isfile(path): + continue + + ext = os.path.splitext(filename)[-1].lower() + if ext in extensions: + filenames.append(filename) + + if not filenames: + return comment_hints, current_comment + + matcher = CommentMatcher(extensions, file_template, fill_data) + + for filename in filenames: + comment = matcher.parse_comment(filename) + if comment: + comment_hints.add(comment) + if filename == current_filename: + current_comment = comment + + return list(comment_hints), current_comment + + def _get_workdir(self, anatomy, template_key, fill_data): + template_info = anatomy.templates_obj[template_key] + directory_template = template_info["folder"] + return directory_template.format_strict(fill_data).normalized() + + def get_workarea_save_as_data(self, folder_id, task_id): + folder = None + task = None + if folder_id: + folder = self._controller.get_folder_entity(folder_id) + if task_id: + task = self._controller.get_task_entity(task_id) + + if not folder or not task: + return { + "template_key": None, + "template_has_version": None, + "template_has_comment": None, + "ext": None, + "workdir": None, + "comment": None, + "comment_hints": None, + "last_version": None, + "extensions": None, + } + + anatomy = self._controller.project_anatomy + fill_data = self._prepare_fill_data(folder_id, task_id) + template_key = self._get_template_key(fill_data) + + current_workfile = self._controller.get_current_workfile() + current_filename = None + current_ext = None + if current_workfile: + current_filename = os.path.basename(current_workfile) + current_ext = os.path.splitext(current_filename)[1].lower() + + extensions = self._extensions + if not current_ext and extensions: + current_ext = tuple(extensions)[0] + + workdir = self._get_workdir(anatomy, template_key, fill_data) + + template_info = anatomy.templates_obj[template_key] + file_template = template_info["file"] + + comment_hints, comment = self._get_comments_from_root( + file_template, + extensions, + fill_data, + workdir, + current_filename, + ) + last_version = self._get_last_workfile_version( + workdir, file_template, fill_data, extensions) + str_file_template = str(file_template) + template_has_version = "{version" in str_file_template + template_has_comment = "{comment" in str_file_template + + return { + "template_key": template_key, + "template_has_version": template_has_version, + "template_has_comment": template_has_comment, + "ext": current_ext, + "workdir": workdir, + "comment": comment, + "comment_hints": comment_hints, + "last_version": last_version, + "extensions": extensions, + } + + def fill_workarea_filepath( + self, + folder_id, + task_id, + extension, + use_last_version, + version, + comment, + ): + anatomy = self._controller.project_anatomy + fill_data = self._prepare_fill_data(folder_id, task_id) + template_key = self._get_template_key(fill_data) + + workdir = self._get_workdir(anatomy, template_key, fill_data) + + template_info = anatomy.templates_obj[template_key] + file_template = template_info["file"] + + if use_last_version: + version = self._get_last_workfile_version( + workdir, file_template, fill_data, self._extensions + ) + fill_data["version"] = version + fill_data["ext"] = extension.lstrip(".") + + if comment: + fill_data["comment"] = comment + + filename = file_template.format(fill_data) + if not filename.solved: + filename = None + + exists = False + if filename: + filepath = os.path.join(workdir, filename) + exists = os.path.exists(filepath) + + return WorkareaFilepathResult( + workdir, + filename, + exists + ) + + +class WorkfileEntitiesModel: + """Workfile entities model. + + Args: + control (AbstractWorkfileController): Controller object. + """ + + def __init__(self, controller): + self._controller = controller + self._cache = {} + self._items = {} + + def _get_workfile_info_identifier( + self, folder_id, task_id, rootless_path + ): + return "_".join([folder_id, task_id, rootless_path]) + + def _get_rootless_path(self, filepath): + anatomy = self._controller.project_anatomy + + workdir, filename = os.path.split(filepath) + success, rootless_dir = anatomy.find_root_template_from_path(workdir) + return "/".join([ + os.path.normpath(rootless_dir).replace("\\", "/"), + filename + ]) + + def _prepare_workfile_info_item( + self, folder_id, task_id, workfile_info, filepath + ): + note = "" + if workfile_info: + note = workfile_info["attrib"].get("description") or "" + + filestat = os.stat(filepath) + return WorkfileInfo( + folder_id, + task_id, + filepath, + filesize=filestat.st_size, + creation_time=filestat.st_ctime, + modification_time=filestat.st_mtime, + note=note + ) + + def _get_workfile_info(self, folder_id, task_id, identifier): + workfile_info = self._cache.get(identifier) + if workfile_info is not None: + return workfile_info + + for workfile_info in ayon_api.get_workfiles_info( + self._controller.get_current_project_name(), + task_ids=[task_id], + fields=["id", "path", "attrib"], + ): + workfile_identifier = self._get_workfile_info_identifier( + folder_id, task_id, workfile_info["path"] + ) + self._cache[workfile_identifier] = workfile_info + return self._cache.get(identifier) + + def get_workfile_info( + self, folder_id, task_id, filepath, rootless_path=None + ): + if not folder_id or not task_id or not filepath: + return None + + if rootless_path is None: + rootless_path = self._get_rootless_path(filepath) + + identifier = self._get_workfile_info_identifier( + folder_id, task_id, rootless_path) + item = self._items.get(identifier) + if item is None: + workfile_info = self._get_workfile_info( + folder_id, task_id, identifier + ) + item = self._prepare_workfile_info_item( + folder_id, task_id, workfile_info, filepath + ) + self._items[identifier] = item + return item + + def save_workfile_info(self, folder_id, task_id, filepath, note): + rootless_path = self._get_rootless_path(filepath) + identifier = self._get_workfile_info_identifier( + folder_id, task_id, rootless_path + ) + workfile_info = self._get_workfile_info( + folder_id, task_id, identifier + ) + if not workfile_info: + self._cache[identifier] = self._create_workfile_info_entity( + task_id, rootless_path, note) + self._items.pop(identifier, None) + return + + new_workfile_info = copy.deepcopy(workfile_info) + attrib = new_workfile_info.setdefault("attrib", {}) + attrib["description"] = note + update_data = prepare_workfile_info_update_data( + workfile_info, new_workfile_info + ) + self._cache[identifier] = new_workfile_info + self._items.pop(identifier, None) + if not update_data: + return + + project_name = self._controller.get_current_project_name() + + session = OperationsSession() + session.update_entity( + project_name, "workfile", workfile_info["id"], update_data + ) + session.commit() + + def _create_workfile_info_entity(self, task_id, rootless_path, note): + extension = os.path.splitext(rootless_path)[1] + + project_name = self._controller.get_current_project_name() + + workfile_info = { + "path": rootless_path, + "taskId": task_id, + "attrib": { + "extension": extension, + "description": note + } + } + + session = OperationsSession() + session.create_entity(project_name, "workfile", workfile_info) + session.commit() + return workfile_info + + +class PublishWorkfilesModel: + """Model for handling of published workfiles. + + Todos: + Cache workfiles products and representations for some time. + Note Representations won't change. Only what can change are + versions. + """ + + def __init__(self, controller): + self._controller = controller + self._cached_extensions = None + self._cached_repre_extensions = None + + @property + def _extensions(self): + if self._cached_extensions is None: + exts = self._controller.get_workfile_extensions() or [] + self._cached_extensions = exts + return self._cached_extensions + + @property + def _repre_extensions(self): + if self._cached_repre_extensions is None: + self._cached_repre_extensions = { + ext.lstrip(".") for ext in self._extensions + } + return self._cached_repre_extensions + + def _file_item_from_representation( + self, repre_entity, project_anatomy, task_name=None + ): + if task_name is not None: + task_info = repre_entity["context"].get("task") + if not task_info or task_info["name"] != task_name: + return None + + # Filter by extension + extensions = self._repre_extensions + workfile_path = None + for repre_file in repre_entity["files"]: + ext = ( + os.path.splitext(repre_file["name"])[1] + .lower() + .lstrip(".") + ) + if ext in extensions: + workfile_path = repre_file["path"] + break + + if not workfile_path: + return None + + try: + workfile_path = workfile_path.format( + root=project_anatomy.roots) + except Exception as exc: + print("Failed to format workfile path: {}".format(exc)) + + dirpath, filename = os.path.split(workfile_path) + created_at = arrow.get(repre_entity["createdAt"]) + return FileItem( + dirpath, + filename, + created_at.float_timestamp, + repre_entity["id"] + ) + + def get_file_items(self, folder_id, task_name): + # TODO refactor to use less server API calls + project_name = self._controller.get_current_project_name() + # Get subset docs of asset + product_entities = ayon_api.get_products( + project_name, + folder_ids=[folder_id], + product_types=["workfile"], + fields=["id", "name"] + ) + + output = [] + product_ids = {product["id"] for product in product_entities} + if not product_ids: + return output + + # Get version docs of subsets with their families + version_entities = ayon_api.get_versions( + project_name, + product_ids=product_ids, + fields=["id", "productId"] + ) + version_ids = {version["id"] for version in version_entities} + if not version_ids: + return output + + # Query representations of filtered versions and add filter for + # extension + repre_entities = ayon_api.get_representations( + project_name, + version_ids=version_ids + ) + project_anatomy = self._controller.project_anatomy + + # Filter queried representations by task name if task is set + file_items = [] + for repre_entity in repre_entities: + file_item = self._file_item_from_representation( + repre_entity, project_anatomy, task_name + ) + if file_item is not None: + file_items.append(file_item) + + return file_items + + +class WorkfilesModel: + """Workfiles model.""" + + def __init__(self, controller): + self._controller = controller + + self._entities_model = WorkfileEntitiesModel(controller) + self._workarea_model = WorkareaModel(controller) + self._published_model = PublishWorkfilesModel(controller) + + def get_workfile_info(self, folder_id, task_id, filepath): + return self._entities_model.get_workfile_info( + folder_id, task_id, filepath + ) + + def save_workfile_info(self, folder_id, task_id, filepath, note): + self._entities_model.save_workfile_info( + folder_id, task_id, filepath, note + ) + + def get_workarea_dir_by_context(self, folder_id, task_id): + """Workarea dir for passed context. + + The directory path is based on project anatomy templates. + + Args: + folder_id (str): Folder id. + task_id (str): Task id. + + Returns: + Union[str, None]: Workarea dir path or None for invalid context. + """ + + return self._workarea_model.get_workarea_dir_by_context( + folder_id, task_id) + + def get_workarea_file_items(self, folder_id, task_id): + """Workfile items for passed context from workarea. + + Args: + folder_id (Union[str, None]): Folder id. + task_id (Union[str, None]): Task id. + + Returns: + list[FileItem]: List of file items matching workarea of passed + context. + """ + + return self._workarea_model.get_file_items(folder_id, task_id) + + def get_workarea_save_as_data(self, folder_id, task_id): + return self._workarea_model.get_workarea_save_as_data( + folder_id, task_id) + + def fill_workarea_filepath(self, *args, **kwargs): + return self._workarea_model.fill_workarea_filepath( + *args, **kwargs + ) + + def get_published_file_items(self, folder_id, task_name): + """Published workfiles for passed context. + + Args: + folder_id (str): Folder id. + task_name (str): Task name. + + Returns: + list[FileItem]: List of files for published workfiles. + """ + + return self._published_model.get_file_items(folder_id, task_name) diff --git a/openpype/tools/ayon_workfiles/widgets/__init__.py b/openpype/tools/ayon_workfiles/widgets/__init__.py new file mode 100644 index 0000000000..f0c5ba1c40 --- /dev/null +++ b/openpype/tools/ayon_workfiles/widgets/__init__.py @@ -0,0 +1,6 @@ +from .window import WorkfilesToolWindow + + +__all__ = ( + "WorkfilesToolWindow", +) diff --git a/openpype/tools/ayon_workfiles/widgets/constants.py b/openpype/tools/ayon_workfiles/widgets/constants.py new file mode 100644 index 0000000000..fc74fd9bc4 --- /dev/null +++ b/openpype/tools/ayon_workfiles/widgets/constants.py @@ -0,0 +1,7 @@ +from qtpy import QtCore + + +ITEM_ID_ROLE = QtCore.Qt.UserRole + 1 +PARENT_ID_ROLE = QtCore.Qt.UserRole + 2 +ITEM_NAME_ROLE = QtCore.Qt.UserRole + 3 +TASK_TYPE_ROLE = QtCore.Qt.UserRole + 4 diff --git a/openpype/tools/ayon_workfiles/widgets/files_widget.py b/openpype/tools/ayon_workfiles/widgets/files_widget.py new file mode 100644 index 0000000000..fbf4dbc593 --- /dev/null +++ b/openpype/tools/ayon_workfiles/widgets/files_widget.py @@ -0,0 +1,398 @@ +import os + +import qtpy +from qtpy import QtWidgets, QtCore + +from .save_as_dialog import SaveAsDialog +from .files_widget_workarea import WorkAreaFilesWidget +from .files_widget_published import PublishedFilesWidget + + +class FilesWidget(QtWidgets.QWidget): + """A widget displaying files that allows to save and open files. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + parent (QtWidgets.QWidget): The parent widget. + """ + + def __init__(self, controller, parent): + super(FilesWidget, self).__init__(parent) + + files_widget = QtWidgets.QStackedWidget(self) + workarea_widget = WorkAreaFilesWidget(controller, files_widget) + published_widget = PublishedFilesWidget(controller, files_widget) + files_widget.addWidget(workarea_widget) + files_widget.addWidget(published_widget) + + btns_widget = QtWidgets.QWidget(self) + + workarea_btns_widget = QtWidgets.QWidget(btns_widget) + workarea_btn_open = QtWidgets.QPushButton( + "Open", workarea_btns_widget) + workarea_btn_browse = QtWidgets.QPushButton( + "Browse", workarea_btns_widget) + workarea_btn_save = QtWidgets.QPushButton( + "Save As", workarea_btns_widget) + + workarea_btns_layout = QtWidgets.QHBoxLayout(workarea_btns_widget) + workarea_btns_layout.setContentsMargins(0, 0, 0, 0) + workarea_btns_layout.addWidget(workarea_btn_open, 1) + workarea_btns_layout.addWidget(workarea_btn_browse, 1) + workarea_btns_layout.addWidget(workarea_btn_save, 1) + + published_btns_widget = QtWidgets.QWidget(btns_widget) + published_btn_copy_n_open = QtWidgets.QPushButton( + "Copy && Open", published_btns_widget + ) + published_btn_change_context = QtWidgets.QPushButton( + "Choose different context", published_btns_widget + ) + published_btn_cancel = QtWidgets.QPushButton( + "Cancel", published_btns_widget + ) + + published_btns_layout = QtWidgets.QHBoxLayout(published_btns_widget) + published_btns_layout.setContentsMargins(0, 0, 0, 0) + published_btns_layout.addWidget(published_btn_copy_n_open, 1) + published_btns_layout.addWidget(published_btn_change_context, 1) + published_btns_layout.addWidget(published_btn_cancel, 1) + + btns_layout = QtWidgets.QVBoxLayout(btns_widget) + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addWidget(workarea_btns_widget, 1) + btns_layout.addWidget(published_btns_widget, 1) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(files_widget, 1) + main_layout.addWidget(btns_widget, 0) + + controller.register_event_callback( + "workarea.selection.changed", + self._on_workarea_path_changed + ) + controller.register_event_callback( + "selection.representation.changed", + self._on_published_repre_changed + ) + controller.register_event_callback( + "selection.task.changed", + self._on_task_changed + ) + controller.register_event_callback( + "copy_representation.finished", + self._on_copy_representation_finished, + ) + controller.register_event_callback( + "workfile_save_enable.changed", + self._on_workfile_save_enabled_change, + ) + + workarea_widget.open_current_requested.connect( + self._on_current_open_requests) + workarea_widget.duplicate_requested.connect( + self._on_duplicate_request) + workarea_btn_open.clicked.connect(self._on_workarea_open_clicked) + workarea_btn_browse.clicked.connect(self._on_workarea_browse_clicked) + workarea_btn_save.clicked.connect(self._on_workarea_save_clicked) + + published_widget.save_as_requested.connect(self._on_save_as_request) + published_btn_copy_n_open.clicked.connect( + self._on_published_save_clicked) + published_btn_change_context.clicked.connect( + self._on_published_change_context_clicked) + published_btn_cancel.clicked.connect( + self._on_published_cancel_clicked) + + self._selected_folder_id = None + self._selected_tak_name = None + + self._pre_select_folder_id = None + self._pre_select_task_name = None + + self._select_context_mode = False + self._valid_selected_context = False + self._valid_representation_id = False + self._tmp_text_filter = None + self._is_save_enabled = True + + self._controller = controller + self._files_widget = files_widget + self._workarea_widget = workarea_widget + self._published_widget = published_widget + self._workarea_btns_widget = workarea_btns_widget + self._published_btns_widget = published_btns_widget + + self._workarea_btn_open = workarea_btn_open + self._workarea_btn_browse = workarea_btn_browse + self._workarea_btn_save = workarea_btn_save + + self._published_widget = published_widget + self._published_btn_copy_n_open = published_btn_copy_n_open + self._published_btn_change_context = published_btn_change_context + self._published_btn_cancel = published_btn_cancel + + # Initial setup + workarea_btn_open.setEnabled(False) + published_btn_copy_n_open.setEnabled(False) + published_btn_change_context.setEnabled(False) + published_btn_cancel.setVisible(False) + + def set_published_mode(self, published_mode): + # Make sure context selection is disabled + self._set_select_contex_mode(False) + # Change current widget + self._files_widget.setCurrentWidget(( + self._published_widget + if published_mode + else self._workarea_widget + )) + # Pass the mode to the widgets, so they can start/stop handle events + self._workarea_widget.set_published_mode(published_mode) + self._published_widget.set_published_mode(published_mode) + + # Change available buttons + self._workarea_btns_widget.setVisible(not published_mode) + self._published_btns_widget.setVisible(published_mode) + + def set_text_filter(self, text_filter): + if self._select_context_mode: + self._tmp_text_filter = text_filter + return + self._workarea_widget.set_text_filter(text_filter) + self._published_widget.set_text_filter(text_filter) + + def _exec_save_as_dialog(self): + """Show SaveAs dialog using currently selected context. + + Returns: + Union[dict[str, Any], None]: Result of the dialog. + """ + + dialog = SaveAsDialog(self._controller, self) + dialog.update_context() + dialog.exec_() + return dialog.get_result() + + # ------------------------------------------------------------- + # Workarea workfiles + # ------------------------------------------------------------- + def _open_workfile(self, filepath): + if self._controller.has_unsaved_changes(): + result = self._save_changes_prompt() + if result is None: + return + + if result: + self._controller.save_current_workfile() + self._controller.open_workfile(filepath) + + def _on_workarea_open_clicked(self): + path = self._workarea_widget.get_selected_path() + if path: + self._open_workfile(path) + + def _on_current_open_requests(self): + self._on_workarea_open_clicked() + + def _on_duplicate_request(self): + filepath = self._workarea_widget.get_selected_path() + if filepath is None: + return + + result = self._exec_save_as_dialog() + if result is None: + return + self._controller.duplicate_workfile( + filepath, + result["workdir"], + result["filename"] + ) + + def _on_workarea_browse_clicked(self): + extnsions = self._controller.get_workfile_extensions() + ext_filter = "Work File (*{0})".format( + " *".join(extnsions) + ) + dir_key = "directory" + if qtpy.API in ("pyside", "pyside2", "pyside6"): + dir_key = "dir" + + selected_context = self._controller.get_selected_context() + workfile_root = self._controller.get_workarea_dir_by_context( + selected_context["folder_id"], selected_context["task_id"] + ) + # Find existing directory of workfile root + # - Qt will use 'cwd' instead, if path does not exist, which may lead + # to igniter directory + while workfile_root: + if os.path.exists(workfile_root): + break + workfile_root = os.path.dirname(workfile_root) + + kwargs = { + "caption": "Work Files", + "filter": ext_filter, + dir_key: workfile_root + } + + filepath = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0] + if filepath: + self._open_workfile(filepath) + + def _on_workarea_save_clicked(self): + result = self._exec_save_as_dialog() + if result is None: + return + self._controller.save_as_workfile( + result["folder_id"], + result["task_id"], + result["workdir"], + result["filename"], + result["template_key"], + ) + + def _on_workarea_path_changed(self, event): + valid_path = event["path"] is not None + self._workarea_btn_open.setEnabled(valid_path) + + # ------------------------------------------------------------- + # Published workfiles + # ------------------------------------------------------------- + def _update_published_btns_state(self): + enabled = ( + self._valid_representation_id + and self._valid_selected_context + and self._is_save_enabled + ) + self._published_btn_copy_n_open.setEnabled(enabled) + self._published_btn_change_context.setEnabled(enabled) + + def _update_workarea_btns_state(self): + enabled = self._is_save_enabled + self._workarea_btn_save.setEnabled(enabled) + + def _on_published_repre_changed(self, event): + self._valid_representation_id = event["representation_id"] is not None + self._update_published_btns_state() + + def _on_task_changed(self, event): + self._selected_folder_id = event["folder_id"] + self._selected_tak_name = event["task_name"] + self._valid_selected_context = ( + self._selected_folder_id is not None + and self._selected_tak_name is not None + ) + self._update_published_btns_state() + + def _on_published_save_clicked(self): + result = self._exec_save_as_dialog() + if result is None: + return + + repre_info = self._published_widget.get_selected_repre_info() + self._controller.copy_workfile_representation( + repre_info["representation_id"], + repre_info["filepath"], + result["folder_id"], + result["task_id"], + result["workdir"], + result["filename"], + result["template_key"], + ) + + def _on_save_as_request(self): + self._on_published_save_clicked() + + def _set_select_contex_mode(self, enabled): + if self._select_context_mode is enabled: + return + + if enabled: + self._pre_select_folder_id = self._selected_folder_id + self._pre_select_task_name = self._selected_tak_name + else: + self._pre_select_folder_id = None + self._pre_select_task_name = None + self._select_context_mode = enabled + self._published_btn_cancel.setVisible(enabled) + self._published_btn_change_context.setVisible(not enabled) + self._published_widget.set_select_context_mode(enabled) + + if not enabled and self._tmp_text_filter is not None: + self.set_text_filter(self._tmp_text_filter) + self._tmp_text_filter = None + + def _on_published_change_context_clicked(self): + self._set_select_contex_mode(True) + + def _should_set_pre_select_context(self): + if self._pre_select_folder_id is None: + return False + if self._pre_select_folder_id != self._selected_folder_id: + return True + if self._pre_select_task_name is None: + return False + return self._pre_select_task_name != self._selected_tak_name + + def _on_published_cancel_clicked(self): + folder_id = self._pre_select_folder_id + task_name = self._pre_select_task_name + representation_id = self._published_widget.get_selected_repre_id() + should_change_selection = self._should_set_pre_select_context() + self._set_select_contex_mode(False) + if should_change_selection: + self._controller.set_expected_selection( + folder_id, task_name, representation_id=representation_id + ) + + def _on_copy_representation_finished(self, event): + """Callback for when copy representation is finished. + + Make sure that select context mode is disabled when representation + copy is finished. + + Args: + event (Event): Event object. + """ + + if not event["failed"]: + self._set_select_contex_mode(False) + + def _on_workfile_save_enabled_change(self, event): + enabled = event["enabled"] + self._is_save_enabled = enabled + self._update_published_btns_state() + self._update_workarea_btns_state() + + def _save_changes_prompt(self): + """Ask user if wants to save changes to current file. + + Returns: + Union[bool, None]: True if user wants to save changes, False if + user does not want to save changes, None if user cancels + operation. + """ + messagebox = QtWidgets.QMessageBox(parent=self) + messagebox.setWindowFlags( + messagebox.windowFlags() | QtCore.Qt.FramelessWindowHint + ) + messagebox.setIcon(QtWidgets.QMessageBox.Warning) + messagebox.setWindowTitle("Unsaved Changes!") + messagebox.setText( + "There are unsaved changes to the current file." + "\nDo you want to save the changes?" + ) + messagebox.setStandardButtons( + QtWidgets.QMessageBox.Yes + | QtWidgets.QMessageBox.No + | QtWidgets.QMessageBox.Cancel + ) + + result = messagebox.exec_() + if result == QtWidgets.QMessageBox.Yes: + return True + if result == QtWidgets.QMessageBox.No: + return False + return None diff --git a/openpype/tools/ayon_workfiles/widgets/files_widget_published.py b/openpype/tools/ayon_workfiles/widgets/files_widget_published.py new file mode 100644 index 0000000000..bc59447777 --- /dev/null +++ b/openpype/tools/ayon_workfiles/widgets/files_widget_published.py @@ -0,0 +1,378 @@ +import qtawesome +from qtpy import QtWidgets, QtCore, QtGui + +from openpype.style import ( + get_default_entity_icon_color, + get_disabled_entity_icon_color, +) +from openpype.tools.utils.delegates import PrettyTimeDelegate + +from .utils import TreeView, BaseOverlayFrame + + +REPRE_ID_ROLE = QtCore.Qt.UserRole + 1 +FILEPATH_ROLE = QtCore.Qt.UserRole + 2 +DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 3 + + +class PublishedFilesModel(QtGui.QStandardItemModel): + """A model for displaying files. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + """ + + def __init__(self, controller): + super(PublishedFilesModel, self).__init__() + + self.setColumnCount(2) + + self.setHeaderData(0, QtCore.Qt.Horizontal, "Name") + self.setHeaderData(1, QtCore.Qt.Horizontal, "Date Modified") + + controller.register_event_callback( + "selection.task.changed", + self._on_task_changed + ) + controller.register_event_callback( + "selection.folder.changed", + self._on_folder_changed + ) + + self._file_icon = qtawesome.icon( + "fa.file-o", + color=get_default_entity_icon_color() + ) + self._controller = controller + self._items_by_id = {} + self._missing_context_item = None + self._missing_context_used = False + self._empty_root_item = None + self._empty_item_used = False + + self._published_mode = False + self._context_select_mode = False + + self._last_folder_id = None + self._last_task_id = None + + self._add_empty_item() + + def _clear_items(self): + self._remove_missing_context_item() + self._remove_empty_item() + if self._items_by_id: + root = self.invisibleRootItem() + root.removeRows(0, root.rowCount()) + self._items_by_id = {} + + def set_published_mode(self, published_mode): + if self._published_mode == published_mode: + return + self._published_mode = published_mode + if published_mode: + self._fill_items() + elif self._context_select_mode: + self.set_select_context_mode(False) + + def set_select_context_mode(self, select_mode): + if self._context_select_mode is select_mode: + return + self._context_select_mode = select_mode + if not select_mode and self._published_mode: + self._fill_items() + + def get_index_by_representation_id(self, representation_id): + item = self._items_by_id.get(representation_id) + if item is None: + return QtCore.QModelIndex() + return self.indexFromItem(item) + + def _get_missing_context_item(self): + if self._missing_context_item is None: + message = "Select folder" + item = QtGui.QStandardItem(message) + icon = qtawesome.icon( + "fa.times", + color=get_disabled_entity_icon_color() + ) + item.setData(icon, QtCore.Qt.DecorationRole) + item.setFlags(QtCore.Qt.NoItemFlags) + item.setColumnCount(self.columnCount()) + self._missing_context_item = item + return self._missing_context_item + + def _add_missing_context_item(self): + if self._missing_context_used: + return + self._clear_items() + root_item = self.invisibleRootItem() + root_item.appendRow(self._get_missing_context_item()) + self._missing_context_used = True + + def _remove_missing_context_item(self): + if not self._missing_context_used: + return + root_item = self.invisibleRootItem() + root_item.takeRow(self._missing_context_item.row()) + self._missing_context_used = False + + def _get_empty_root_item(self): + if self._empty_root_item is None: + message = "Didn't find any published workfiles." + item = QtGui.QStandardItem(message) + icon = qtawesome.icon( + "fa.times", + color=get_disabled_entity_icon_color() + ) + item.setData(icon, QtCore.Qt.DecorationRole) + item.setFlags(QtCore.Qt.NoItemFlags) + item.setColumnCount(self.columnCount()) + self._empty_root_item = item + return self._empty_root_item + + def _add_empty_item(self): + if self._empty_item_used: + return + self._clear_items() + root_item = self.invisibleRootItem() + root_item.appendRow(self._get_empty_root_item()) + self._empty_item_used = True + + def _remove_empty_item(self): + if not self._empty_item_used: + return + root_item = self.invisibleRootItem() + root_item.takeRow(self._empty_root_item.row()) + self._empty_item_used = False + + def _on_folder_changed(self, event): + self._last_folder_id = event["folder_id"] + self._last_task_id = None + if self._context_select_mode: + return + + if self._published_mode: + self._fill_items() + + def _on_task_changed(self, event): + self._last_folder_id = event["folder_id"] + self._last_task_id = event["task_id"] + if self._context_select_mode: + return + + if self._published_mode: + self._fill_items() + + def _fill_items(self): + folder_id = self._last_folder_id + task_id = self._last_task_id + if not folder_id: + self._add_missing_context_item() + return + + file_items = self._controller.get_published_file_items( + folder_id, task_id + ) + root_item = self.invisibleRootItem() + if not file_items: + self._add_empty_item() + return + self._remove_empty_item() + self._remove_missing_context_item() + + items_to_remove = set(self._items_by_id.keys()) + new_items = [] + for file_item in file_items: + repre_id = file_item.representation_id + if repre_id in self._items_by_id: + items_to_remove.discard(repre_id) + item = self._items_by_id[repre_id] + else: + item = QtGui.QStandardItem() + new_items.append(item) + item.setColumnCount(self.columnCount()) + item.setData(self._file_icon, QtCore.Qt.DecorationRole) + item.setData(file_item.filename, QtCore.Qt.DisplayRole) + item.setData(repre_id, REPRE_ID_ROLE) + + if file_item.exists: + flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + else: + flags = QtCore.Qt.NoItemFlags + + item.setFlags(flags) + item.setData(file_item.filepath, FILEPATH_ROLE) + item.setData(file_item.modified, DATE_MODIFIED_ROLE) + + self._items_by_id[repre_id] = item + + if new_items: + root_item.appendRows(new_items) + + for repre_id in items_to_remove: + item = self._items_by_id.pop(repre_id) + root_item.removeRow(item.row()) + + if root_item.rowCount() == 0: + self._add_empty_item() + + def flags(self, index): + # Use flags of first column for all columns + if index.column() != 0: + index = self.index(index.row(), 0, index.parent()) + return super(PublishedFilesModel, self).flags(index) + + def data(self, index, role=None): + if role is None: + role = QtCore.Qt.DisplayRole + + # Handle roles for first column + if index.column() == 1: + if role == QtCore.Qt.DecorationRole: + return None + + if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): + role = DATE_MODIFIED_ROLE + index = self.index(index.row(), 0, index.parent()) + + return super(PublishedFilesModel, self).data(index, role) + + +class SelectContextOverlay(BaseOverlayFrame): + """Overlay for files view when user should select context. + + Todos: + The look of this overlay should be improved, it is "not nice" now. + """ + + def __init__(self, parent): + super(SelectContextOverlay, self).__init__(parent) + + label_widget = QtWidgets.QLabel( + "Please choose context on the left
<", + self + ) + label_widget.setAlignment(QtCore.Qt.AlignCenter) + label_widget.setObjectName("OverlayFrameLabel") + + layout = QtWidgets.QHBoxLayout(self) + layout.addWidget(label_widget, 1, QtCore.Qt.AlignCenter) + + label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + +class PublishedFilesWidget(QtWidgets.QWidget): + """Published workfiles widget. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + parent (QtWidgets.QWidget): The parent widget. + """ + + selection_changed = QtCore.Signal() + save_as_requested = QtCore.Signal() + + def __init__(self, controller, parent): + super(PublishedFilesWidget, self).__init__(parent) + + view = TreeView(self) + view.setSortingEnabled(True) + view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + # Smaller indentation + view.setIndentation(0) + + model = PublishedFilesModel(controller) + proxy_model = QtCore.QSortFilterProxyModel() + proxy_model.setSourceModel(model) + proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) + proxy_model.setDynamicSortFilter(True) + + view.setModel(proxy_model) + + time_delegate = PrettyTimeDelegate() + view.setItemDelegateForColumn(1, time_delegate) + + # Default to a wider first filename column it is what we mostly care + # about and the date modified is relatively small anyway. + view.setColumnWidth(0, 330) + + select_overlay = SelectContextOverlay(view) + select_overlay.setVisible(False) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(view, 1) + + selection_model = view.selectionModel() + selection_model.selectionChanged.connect(self._on_selection_change) + view.double_clicked_left.connect(self._on_left_double_click) + + controller.register_event_callback( + "expected_selection_changed", + self._on_expected_selection_change + ) + + self._view = view + self._select_overlay = select_overlay + self._model = model + self._proxy_model = proxy_model + self._time_delegate = time_delegate + self._controller = controller + + def set_published_mode(self, published_mode): + self._model.set_published_mode(published_mode) + + def set_select_context_mode(self, select_mode): + self._model.set_select_context_mode(select_mode) + self._select_overlay.setVisible(select_mode) + + def set_text_filter(self, text_filter): + self._proxy_model.setFilterFixedString(text_filter) + + def get_selected_repre_info(self): + selection_model = self._view.selectionModel() + representation_id = None + filepath = None + for index in selection_model.selectedIndexes(): + representation_id = index.data(REPRE_ID_ROLE) + filepath = index.data(FILEPATH_ROLE) + + return { + "representation_id": representation_id, + "filepath": filepath, + } + + def get_selected_repre_id(self): + return self.get_selected_repre_info()["representation_id"] + + def _on_selection_change(self): + repre_id = self.get_selected_repre_id() + self._controller.set_selected_representation_id(repre_id) + + def _on_left_double_click(self): + self.save_as_requested.emit() + + def _on_expected_selection_change(self, event): + if ( + event["representation_id_selected"] + or not event["folder_selected"] + or (event["task_name"] and not event["task_selected"]) + ): + return + + representation_id = event["representation_id"] + selected_repre_id = self.get_selected_repre_id() + if ( + representation_id is not None + and representation_id != selected_repre_id + ): + index = self._model.get_index_by_representation_id( + representation_id) + if index.isValid(): + proxy_index = self._proxy_model.mapFromSource(index) + self._view.setCurrentIndex(proxy_index) + + self._controller.expected_representation_selected( + event["folder_id"], event["task_name"], representation_id + ) diff --git a/openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py b/openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py new file mode 100644 index 0000000000..e8ccd094d1 --- /dev/null +++ b/openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py @@ -0,0 +1,380 @@ +import qtawesome +from qtpy import QtWidgets, QtCore, QtGui + +from openpype.style import ( + get_default_entity_icon_color, + get_disabled_entity_icon_color, +) +from openpype.tools.utils.delegates import PrettyTimeDelegate + +from .utils import TreeView + +FILENAME_ROLE = QtCore.Qt.UserRole + 1 +FILEPATH_ROLE = QtCore.Qt.UserRole + 2 +DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 3 + + +class WorkAreaFilesModel(QtGui.QStandardItemModel): + """A model for workare workfiles. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + """ + + def __init__(self, controller): + super(WorkAreaFilesModel, self).__init__() + + self.setColumnCount(2) + + self.setHeaderData(0, QtCore.Qt.Horizontal, "Name") + self.setHeaderData(1, QtCore.Qt.Horizontal, "Date Modified") + + controller.register_event_callback( + "selection.task.changed", + self._on_task_changed + ) + controller.register_event_callback( + "workfile_duplicate.finished", + self._on_duplicate_finished + ) + controller.register_event_callback( + "save_as.finished", + self._on_save_as_finished + ) + + self._file_icon = qtawesome.icon( + "fa.file-o", + color=get_default_entity_icon_color() + ) + self._controller = controller + self._items_by_filename = {} + self._missing_context_item = None + self._missing_context_used = False + self._empty_root_item = None + self._empty_item_used = False + self._published_mode = False + self._selected_folder_id = None + self._selected_task_id = None + + self._add_missing_context_item() + + def get_index_by_filename(self, filename): + item = self._items_by_filename.get(filename) + if item is None: + return QtCore.QModelIndex() + return self.indexFromItem(item) + + def _get_missing_context_item(self): + if self._missing_context_item is None: + message = "Select folder and task" + item = QtGui.QStandardItem(message) + icon = qtawesome.icon( + "fa.times", + color=get_disabled_entity_icon_color() + ) + item.setData(icon, QtCore.Qt.DecorationRole) + item.setFlags(QtCore.Qt.NoItemFlags) + item.setColumnCount(self.columnCount()) + self._missing_context_item = item + return self._missing_context_item + + def _clear_items(self): + self._remove_missing_context_item() + self._remove_empty_item() + if self._items_by_filename: + root = self.invisibleRootItem() + root.removeRows(0, root.rowCount()) + self._items_by_filename = {} + + def _add_missing_context_item(self): + if self._missing_context_used: + return + self._clear_items() + root_item = self.invisibleRootItem() + root_item.appendRow(self._get_missing_context_item()) + self._missing_context_used = True + + def _remove_missing_context_item(self): + if not self._missing_context_used: + return + root_item = self.invisibleRootItem() + root_item.takeRow(self._missing_context_item.row()) + self._missing_context_used = False + + def _get_empty_root_item(self): + if self._empty_root_item is None: + message = "Work Area is empty.." + item = QtGui.QStandardItem(message) + icon = qtawesome.icon( + "fa.exclamation-circle", + color=get_disabled_entity_icon_color() + ) + item.setData(icon, QtCore.Qt.DecorationRole) + item.setFlags(QtCore.Qt.NoItemFlags) + item.setColumnCount(self.columnCount()) + self._empty_root_item = item + return self._empty_root_item + + def _add_empty_item(self): + if self._empty_item_used: + return + self._clear_items() + root_item = self.invisibleRootItem() + root_item.appendRow(self._get_empty_root_item()) + self._empty_item_used = True + + def _remove_empty_item(self): + if not self._empty_item_used: + return + root_item = self.invisibleRootItem() + root_item.takeRow(self._empty_root_item.row()) + self._empty_item_used = False + + def _on_task_changed(self, event): + self._selected_folder_id = event["folder_id"] + self._selected_task_id = event["task_id"] + if not self._published_mode: + self._fill_items() + + def _on_duplicate_finished(self, event): + if event["failed"]: + return + + if not self._published_mode: + self._fill_items() + + def _on_save_as_finished(self, event): + if event["failed"]: + return + + if not self._published_mode: + self._fill_items() + + def _fill_items(self): + folder_id = self._selected_folder_id + task_id = self._selected_task_id + if not folder_id or not task_id: + self._add_missing_context_item() + return + + file_items = self._controller.get_workarea_file_items( + folder_id, task_id + ) + root_item = self.invisibleRootItem() + if not file_items: + self._add_empty_item() + return + self._remove_empty_item() + self._remove_missing_context_item() + + items_to_remove = set(self._items_by_filename.keys()) + new_items = [] + for file_item in file_items: + filename = file_item.filename + if filename in self._items_by_filename: + items_to_remove.discard(filename) + item = self._items_by_filename[filename] + else: + item = QtGui.QStandardItem() + new_items.append(item) + item.setColumnCount(self.columnCount()) + item.setFlags( + QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + ) + item.setData(self._file_icon, QtCore.Qt.DecorationRole) + item.setData(file_item.filename, QtCore.Qt.DisplayRole) + item.setData(file_item.filename, FILENAME_ROLE) + + item.setData(file_item.filepath, FILEPATH_ROLE) + item.setData(file_item.modified, DATE_MODIFIED_ROLE) + + self._items_by_filename[file_item.filename] = item + + if new_items: + root_item.appendRows(new_items) + + for filename in items_to_remove: + item = self._items_by_filename.pop(filename) + root_item.removeRow(item.row()) + + if root_item.rowCount() == 0: + self._add_empty_item() + + def flags(self, index): + # Use flags of first column for all columns + if index.column() != 0: + index = self.index(index.row(), 0, index.parent()) + return super(WorkAreaFilesModel, self).flags(index) + + def data(self, index, role=None): + if role is None: + role = QtCore.Qt.DisplayRole + + # Handle roles for first column + if index.column() == 1: + if role == QtCore.Qt.DecorationRole: + return None + + if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): + role = DATE_MODIFIED_ROLE + index = self.index(index.row(), 0, index.parent()) + + return super(WorkAreaFilesModel, self).data(index, role) + + def set_published_mode(self, published_mode): + if self._published_mode == published_mode: + return + self._published_mode = published_mode + if not published_mode: + self._fill_items() + + +class WorkAreaFilesWidget(QtWidgets.QWidget): + """Workarea files widget. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + parent (QtWidgets.QWidget): The parent widget. + """ + + selection_changed = QtCore.Signal() + open_current_requested = QtCore.Signal() + duplicate_requested = QtCore.Signal() + + def __init__(self, controller, parent): + super(WorkAreaFilesWidget, self).__init__(parent) + + view = TreeView(self) + view.setSortingEnabled(True) + view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + # Smaller indentation + view.setIndentation(0) + + model = WorkAreaFilesModel(controller) + proxy_model = QtCore.QSortFilterProxyModel() + proxy_model.setSourceModel(model) + proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) + proxy_model.setDynamicSortFilter(True) + + view.setModel(proxy_model) + + time_delegate = PrettyTimeDelegate() + view.setItemDelegateForColumn(1, time_delegate) + + # Default to a wider first filename column it is what we mostly care + # about and the date modified is relatively small anyway. + view.setColumnWidth(0, 330) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(view, 1) + + selection_model = view.selectionModel() + selection_model.selectionChanged.connect(self._on_selection_change) + view.double_clicked_left.connect(self._on_left_double_click) + view.customContextMenuRequested.connect(self._on_context_menu) + + controller.register_event_callback( + "expected_selection_changed", + self._on_expected_selection_change + ) + + self._view = view + self._model = model + self._proxy_model = proxy_model + self._time_delegate = time_delegate + self._controller = controller + + self._published_mode = False + + def set_published_mode(self, published_mode): + """Set the published mode. + + Widget should ignore most of events when in published mode is enabled. + + Args: + published_mode (bool): The published mode. + """ + + self._model.set_published_mode(published_mode) + self._published_mode = published_mode + + def set_text_filter(self, text_filter): + """Set the text filter. + + Args: + text_filter (str): The text filter. + """ + + self._proxy_model.setFilterFixedString(text_filter) + + def _get_selected_info(self): + selection_model = self._view.selectionModel() + filepath = None + filename = None + for index in selection_model.selectedIndexes(): + filepath = index.data(FILEPATH_ROLE) + filename = index.data(FILENAME_ROLE) + return { + "filepath": filepath, + "filename": filename, + } + + def get_selected_path(self): + """Selected filepath. + + Returns: + Union[str, None]: The selected filepath or None if nothing is + selected. + """ + return self._get_selected_info()["filepath"] + + def _on_selection_change(self): + filepath = self.get_selected_path() + self._controller.set_selected_workfile_path(filepath) + + def _on_left_double_click(self): + self.open_current_requested.emit() + + def _on_context_menu(self, point): + index = self._view.indexAt(point) + if not index.isValid(): + return + + if not index.flags() & QtCore.Qt.ItemIsEnabled: + return + + menu = QtWidgets.QMenu(self) + + # Duplicate + action = QtWidgets.QAction("Duplicate", menu) + tip = "Duplicate selected file." + action.setToolTip(tip) + action.setStatusTip(tip) + action.triggered.connect(self._on_duplicate_pressed) + menu.addAction(action) + + # Show the context action menu + global_point = self._view.mapToGlobal(point) + _ = menu.exec_(global_point) + + def _on_duplicate_pressed(self): + self.duplicate_requested.emit() + + def _on_expected_selection_change(self, event): + if event["workfile_name_selected"]: + return + + workfile_name = event["workfile_name"] + if ( + workfile_name is not None + and workfile_name != self._get_selected_info()["filename"] + ): + index = self._model.get_index_by_filename(workfile_name) + if index.isValid(): + proxy_index = self._proxy_model.mapFromSource(index) + self._view.setCurrentIndex(proxy_index) + + self._controller.expected_workfile_selected( + event["folder_id"], event["task_name"], workfile_name + ) diff --git a/openpype/tools/ayon_workfiles/widgets/folders_widget.py b/openpype/tools/ayon_workfiles/widgets/folders_widget.py new file mode 100644 index 0000000000..b35845f4b6 --- /dev/null +++ b/openpype/tools/ayon_workfiles/widgets/folders_widget.py @@ -0,0 +1,324 @@ +import uuid +import collections + +import qtawesome +from qtpy import QtWidgets, QtGui, QtCore + +from openpype.tools.utils import ( + RecursiveSortFilterProxyModel, + DeselectableTreeView, +) + +from .constants import ITEM_ID_ROLE, ITEM_NAME_ROLE + +SENDER_NAME = "qt_folders_model" + + +class FoldersRefreshThread(QtCore.QThread): + """Thread for refreshing folders. + + Call controller to get folders and emit signal when finished. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + """ + + refresh_finished = QtCore.Signal(str) + + def __init__(self, controller): + super(FoldersRefreshThread, self).__init__() + self._id = uuid.uuid4().hex + self._controller = controller + self._result = None + + @property + def id(self): + """Thread id. + + Returns: + str: Unique id of the thread. + """ + + return self._id + + def run(self): + self._result = self._controller.get_folder_items(SENDER_NAME) + self.refresh_finished.emit(self.id) + + def get_result(self): + return self._result + + +class FoldersModel(QtGui.QStandardItemModel): + """Folders model which cares about refresh of folders. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + """ + + refreshed = QtCore.Signal() + + def __init__(self, controller): + super(FoldersModel, self).__init__() + + self._controller = controller + self._items_by_id = {} + self._parent_id_by_id = {} + + self._refresh_threads = {} + self._current_refresh_thread = None + + self._has_content = False + self._is_refreshing = False + + @property + def is_refreshing(self): + """Model is refreshing. + + Returns: + bool: True if model is refreshing. + """ + return self._is_refreshing + + @property + def has_content(self): + """Has at least one folder. + + Returns: + bool: True if model has at least one folder. + """ + + return self._has_content + + def clear(self): + self._items_by_id = {} + self._parent_id_by_id = {} + self._has_content = False + super(FoldersModel, self).clear() + + def get_index_by_id(self, item_id): + """Get index by folder id. + + Returns: + QtCore.QModelIndex: Index of the folder. Can be invalid if folder + is not available. + """ + item = self._items_by_id.get(item_id) + if item is None: + return QtCore.QModelIndex() + return self.indexFromItem(item) + + def refresh(self): + """Refresh folders items. + + Refresh start thread because it can cause that controller can + start query from database if folders are not cached. + """ + + self._is_refreshing = True + + thread = FoldersRefreshThread(self._controller) + self._current_refresh_thread = thread.id + self._refresh_threads[thread.id] = thread + thread.refresh_finished.connect(self._on_refresh_thread) + thread.start() + + def _on_refresh_thread(self, thread_id): + """Callback when refresh thread is finished. + + Technically can be running multiple refresh threads at the same time, + to avoid using values from wrong thread, we check if thread id is + current refresh thread id. + + Folders are stored by id. + + Args: + thread_id (str): Thread id. + """ + + thread = self._refresh_threads.pop(thread_id) + if thread_id != self._current_refresh_thread: + return + + folder_items_by_id = thread.get_result() + if not folder_items_by_id: + if folder_items_by_id is not None: + self.clear() + self._is_refreshing = False + return + + self._has_content = True + + folder_ids = set(folder_items_by_id) + ids_to_remove = set(self._items_by_id) - folder_ids + + folder_items_by_parent = collections.defaultdict(list) + for folder_item in folder_items_by_id.values(): + folder_items_by_parent[folder_item.parent_id].append(folder_item) + + hierarchy_queue = collections.deque() + hierarchy_queue.append(None) + + while hierarchy_queue: + parent_id = hierarchy_queue.popleft() + folder_items = folder_items_by_parent[parent_id] + if parent_id is None: + parent_item = self.invisibleRootItem() + else: + parent_item = self._items_by_id[parent_id] + + new_items = [] + for folder_item in folder_items: + item_id = folder_item.entity_id + item = self._items_by_id.get(item_id) + if item is None: + is_new = True + item = QtGui.QStandardItem() + item.setEditable(False) + else: + is_new = self._parent_id_by_id[item_id] != parent_id + + icon = qtawesome.icon( + folder_item.icon_name, + color=folder_item.icon_color, + ) + item.setData(item_id, ITEM_ID_ROLE) + item.setData(folder_item.name, ITEM_NAME_ROLE) + item.setData(folder_item.label, QtCore.Qt.DisplayRole) + item.setData(icon, QtCore.Qt.DecorationRole) + if is_new: + new_items.append(item) + self._items_by_id[item_id] = item + self._parent_id_by_id[item_id] = parent_id + + hierarchy_queue.append(item_id) + + if new_items: + parent_item.appendRows(new_items) + + for item_id in ids_to_remove: + item = self._items_by_id[item_id] + parent_id = self._parent_id_by_id[item_id] + if parent_id is None: + parent_item = self.invisibleRootItem() + else: + parent_item = self._items_by_id[parent_id] + parent_item.takeChild(item.row()) + + for item_id in ids_to_remove: + self._items_by_id.pop(item_id) + self._parent_id_by_id.pop(item_id) + + self._is_refreshing = False + self.refreshed.emit() + + +class FoldersWidget(QtWidgets.QWidget): + """Folders widget. + + Widget that handles folders view, model and selection. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + parent (QtWidgets.QWidget): The parent widget. + """ + + def __init__(self, controller, parent): + super(FoldersWidget, self).__init__(parent) + + folders_view = DeselectableTreeView(self) + folders_view.setHeaderHidden(True) + + folders_model = FoldersModel(controller) + folders_proxy_model = RecursiveSortFilterProxyModel() + folders_proxy_model.setSourceModel(folders_model) + + folders_view.setModel(folders_proxy_model) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(folders_view, 1) + + controller.register_event_callback( + "folders.refresh.finished", + self._on_folders_refresh_finished + ) + controller.register_event_callback( + "controller.refresh.finished", + self._on_controller_refresh + ) + controller.register_event_callback( + "expected_selection_changed", + self._on_expected_selection_change + ) + + selection_model = folders_view.selectionModel() + selection_model.selectionChanged.connect(self._on_selection_change) + + folders_model.refreshed.connect(self._on_model_refresh) + + self._controller = controller + self._folders_view = folders_view + self._folders_model = folders_model + self._folders_proxy_model = folders_proxy_model + + self._expected_selection = None + + def set_name_filer(self, name): + self._folders_proxy_model.setFilterFixedString(name) + + def _clear(self): + self._folders_model.clear() + + def _on_folders_refresh_finished(self, event): + if event["sender"] != SENDER_NAME: + self._folders_model.refresh() + + def _on_controller_refresh(self): + self._update_expected_selection() + + def _update_expected_selection(self, expected_data=None): + if expected_data is None: + expected_data = self._controller.get_expected_selection_data() + + # We're done + if expected_data["folder_selected"]: + return + + folder_id = expected_data["folder_id"] + self._expected_selection = folder_id + if not self._folders_model.is_refreshing: + self._set_expected_selection() + + def _set_expected_selection(self): + folder_id = self._expected_selection + self._expected_selection = None + if ( + folder_id is not None + and folder_id != self._get_selected_item_id() + ): + index = self._folders_model.get_index_by_id(folder_id) + if index.isValid(): + proxy_index = self._folders_proxy_model.mapFromSource(index) + self._folders_view.setCurrentIndex(proxy_index) + self._controller.expected_folder_selected(folder_id) + + def _on_model_refresh(self): + if self._expected_selection: + self._set_expected_selection() + self._folders_proxy_model.sort(0) + + def _on_expected_selection_change(self, event): + self._update_expected_selection(event.data) + + def _get_selected_item_id(self): + selection_model = self._folders_view.selectionModel() + for index in selection_model.selectedIndexes(): + item_id = index.data(ITEM_ID_ROLE) + if item_id is not None: + return item_id + return None + + def _on_selection_change(self): + item_id = self._get_selected_item_id() + self._controller.set_selected_folder(item_id) diff --git a/openpype/tools/ayon_workfiles/widgets/save_as_dialog.py b/openpype/tools/ayon_workfiles/widgets/save_as_dialog.py new file mode 100644 index 0000000000..cdce73f030 --- /dev/null +++ b/openpype/tools/ayon_workfiles/widgets/save_as_dialog.py @@ -0,0 +1,351 @@ +from qtpy import QtWidgets, QtCore + +from openpype.tools.utils import PlaceholderLineEdit + + +class SubversionLineEdit(QtWidgets.QWidget): + """QLineEdit with QPushButton for drop down selection of list of strings""" + + text_changed = QtCore.Signal(str) + + def __init__(self, *args, **kwargs): + super(SubversionLineEdit, self).__init__(*args, **kwargs) + + input_field = PlaceholderLineEdit(self) + menu_btn = QtWidgets.QPushButton(self) + menu_btn.setFixedWidth(18) + + menu = QtWidgets.QMenu(self) + menu_btn.setMenu(menu) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(3) + + layout.addWidget(input_field, 1) + layout.addWidget(menu_btn, 0) + + input_field.textChanged.connect(self.text_changed) + + self.setFocusProxy(input_field) + + self._input_field = input_field + self._menu_btn = menu_btn + self._menu = menu + + def set_placeholder(self, placeholder): + self._input_field.setPlaceholderText(placeholder) + + def set_text(self, text): + self._input_field.setText(text) + + def set_values(self, values): + self._update(values) + + def _on_button_clicked(self): + self._menu.exec_() + + def _on_action_clicked(self, action): + self._input_field.setText(action.text()) + + def _update(self, values): + """Create optional predefined subset names + + Args: + default_names(list): all predefined names + + Returns: + None + """ + + menu = self._menu + button = self._menu_btn + + state = any(values) + button.setEnabled(state) + if state is False: + return + + # Include an empty string + values = [""] + sorted(values) + + # Get and destroy the action group + group = button.findChild(QtWidgets.QActionGroup) + if group: + group.deleteLater() + + # Build new action group + group = QtWidgets.QActionGroup(button) + for name in values: + action = group.addAction(name) + menu.addAction(action) + + group.triggered.connect(self._on_action_clicked) + + +class SaveAsDialog(QtWidgets.QDialog): + """Save as dialog to define a unique filename inside workdir. + + The filename is calculated in controller where UI sends values from + dialog inputs. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + """ + + def __init__(self, controller, parent): + super(SaveAsDialog, self).__init__(parent=parent) + self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint) + + self._controller = controller + + self._folder_id = None + self._task_id = None + self._last_version = None + self._template_key = None + self._comment_value = None + self._version_value = None + self._ext_value = None + self._filename = None + self._workdir = None + + self._result = None + + # Btns widget + btns_widget = QtWidgets.QWidget(self) + + btn_ok = QtWidgets.QPushButton("Ok", btns_widget) + btn_cancel = QtWidgets.QPushButton("Cancel", btns_widget) + + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.addWidget(btn_ok) + btns_layout.addWidget(btn_cancel) + + # Inputs widget + inputs_widget = QtWidgets.QWidget(self) + + # Version widget + version_widget = QtWidgets.QWidget(inputs_widget) + + # Version number input + version_input = QtWidgets.QSpinBox(version_widget) + version_input.setMinimum(1) + version_input.setMaximum(9999) + + # Last version checkbox + last_version_check = QtWidgets.QCheckBox( + "Next Available Version", version_widget + ) + last_version_check.setChecked(True) + + version_layout = QtWidgets.QHBoxLayout(version_widget) + version_layout.setContentsMargins(0, 0, 0, 0) + version_layout.addWidget(version_input) + version_layout.addWidget(last_version_check) + + # Preview widget + preview_widget = QtWidgets.QLabel("Preview filename", inputs_widget) + preview_widget.setWordWrap(True) + + # Subversion input + subversion_input = SubversionLineEdit(inputs_widget) + subversion_input.set_placeholder("Will be part of filename.") + + # Extensions combobox + extension_combobox = QtWidgets.QComboBox(inputs_widget) + # Add styled delegate to use stylesheets + extension_delegate = QtWidgets.QStyledItemDelegate() + extension_combobox.setItemDelegate(extension_delegate) + + version_label = QtWidgets.QLabel("Version:", inputs_widget) + subversion_label = QtWidgets.QLabel("Subversion:", inputs_widget) + extension_label = QtWidgets.QLabel("Extension:", inputs_widget) + preview_label = QtWidgets.QLabel("Preview:", inputs_widget) + + # Build inputs + inputs_layout = QtWidgets.QGridLayout(inputs_widget) + inputs_layout.addWidget(version_label, 0, 0) + inputs_layout.addWidget(version_widget, 0, 1) + inputs_layout.addWidget(subversion_label, 1, 0) + inputs_layout.addWidget(subversion_input, 1, 1) + inputs_layout.addWidget(extension_label, 2, 0) + inputs_layout.addWidget(extension_combobox, 2, 1) + inputs_layout.addWidget(preview_label, 3, 0) + inputs_layout.addWidget(preview_widget, 3, 1) + + # Build layout + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(inputs_widget) + main_layout.addWidget(btns_widget) + + # Signal callback registration + version_input.valueChanged.connect(self._on_version_spinbox_change) + last_version_check.stateChanged.connect( + self._on_version_checkbox_change + ) + + subversion_input.text_changed.connect(self._on_comment_change) + extension_combobox.currentIndexChanged.connect( + self._on_extension_change) + + btn_ok.pressed.connect(self._on_ok_pressed) + btn_cancel.pressed.connect(self._on_cancel_pressed) + + # Store objects + self._inputs_layout = inputs_layout + + self._btn_ok = btn_ok + self._btn_cancel = btn_cancel + + self._version_widget = version_widget + + self._version_input = version_input + self._last_version_check = last_version_check + + self._extension_delegate = extension_delegate + self._extension_combobox = extension_combobox + self._subversion_input = subversion_input + self._preview_widget = preview_widget + + self._version_label = version_label + self._subversion_label = subversion_label + self._extension_label = extension_label + self._preview_label = preview_label + + # Post init setup + + # Allow "Enter" key to accept the save. + btn_ok.setDefault(True) + + # Disable version input if last version is checked + version_input.setEnabled(not last_version_check.isChecked()) + + # Force default focus to comment, some hosts didn't automatically + # apply focus to this line edit (e.g. Houdini) + subversion_input.setFocus() + + def get_result(self): + return self._result + + def update_context(self): + # Add version only if template contains version key + # - since the version can be padded with "{version:0>4}" we only search + # for "{version". + selected_context = self._controller.get_selected_context() + folder_id = selected_context["folder_id"] + task_id = selected_context["task_id"] + data = self._controller.get_workarea_save_as_data(folder_id, task_id) + last_version = data["last_version"] + comment = data["comment"] + comment_hints = data["comment_hints"] + + template_has_version = data["template_has_version"] + template_has_comment = data["template_has_comment"] + + self._folder_id = folder_id + self._task_id = task_id + self._workdir = data["workdir"] + self._comment_value = data["comment"] + self._ext_value = data["ext"] + self._template_key = data["template_key"] + self._last_version = data["last_version"] + + self._extension_combobox.clear() + self._extension_combobox.addItems(data["extensions"]) + + self._version_input.setValue(last_version) + + vw_idx = self._inputs_layout.indexOf(self._version_widget) + self._version_label.setVisible(template_has_version) + self._version_widget.setVisible(template_has_version) + if template_has_version: + if vw_idx == -1: + self._inputs_layout.addWidget(self._version_label, 0, 0) + self._inputs_layout.addWidget(self._version_widget, 0, 1) + elif vw_idx != -1: + self._inputs_layout.takeAt(vw_idx) + self._inputs_layout.takeAt( + self._inputs_layout.indexOf(self._version_label) + ) + + cw_idx = self._inputs_layout.indexOf(self._subversion_input) + self._subversion_label.setVisible(template_has_comment) + self._subversion_input.setVisible(template_has_comment) + if template_has_comment: + if cw_idx == -1: + self._inputs_layout.addWidget(self._subversion_label, 1, 0) + self._inputs_layout.addWidget(self._subversion_input, 1, 1) + elif cw_idx != -1: + self._inputs_layout.takeAt(cw_idx) + self._inputs_layout.takeAt( + self._inputs_layout.indexOf(self._subversion_label) + ) + + if template_has_comment: + self._subversion_input.set_text(comment or "") + self._subversion_input.set_values(comment_hints) + self._update_filename() + + def _on_version_spinbox_change(self, value): + if value == self._version_value: + return + self._version_value = value + if not self._last_version_check.isChecked(): + self._update_filename() + + def _on_version_checkbox_change(self): + use_last_version = self._last_version_check.isChecked() + self._version_input.setEnabled(not use_last_version) + if use_last_version: + self._version_input.blockSignals(True) + self._version_input.setValue(self._last_version) + self._version_input.blockSignals(False) + self._update_filename() + + def _on_comment_change(self, text): + if self._comment_value == text: + return + self._comment_value = text + self._update_filename() + + def _on_extension_change(self): + ext = self._extension_combobox.currentText() + if ext == self._ext_value: + return + self._ext_value = ext + self._update_filename() + + def _on_ok_pressed(self): + self._result = { + "filename": self._filename, + "workdir": self._workdir, + "folder_id": self._folder_id, + "task_id": self._task_id, + "template_key": self._template_key, + } + self.close() + + def _on_cancel_pressed(self): + self.close() + + def _update_filename(self): + result = self._controller.fill_workarea_filepath( + self._folder_id, + self._task_id, + self._ext_value, + self._last_version_check.isChecked(), + self._version_value, + self._comment_value, + ) + self._filename = result.filename + self._btn_ok.setEnabled(not result.exists) + + if result.exists: + self._preview_widget.setText(( + "Cannot create \"{}\" because file exists!" + "" + ).format(result.filename)) + else: + self._preview_widget.setText( + "{}".format(result.filename) + ) diff --git a/openpype/tools/ayon_workfiles/widgets/side_panel.py b/openpype/tools/ayon_workfiles/widgets/side_panel.py new file mode 100644 index 0000000000..7f06576a00 --- /dev/null +++ b/openpype/tools/ayon_workfiles/widgets/side_panel.py @@ -0,0 +1,163 @@ +import datetime + +from qtpy import QtWidgets, QtCore + + +def file_size_to_string(file_size): + size = 0 + size_ending_mapping = { + "KB": 1024 ** 1, + "MB": 1024 ** 2, + "GB": 1024 ** 3 + } + ending = "B" + for _ending, _size in size_ending_mapping.items(): + if file_size < _size: + break + size = file_size / _size + ending = _ending + return "{:.2f} {}".format(size, ending) + + +class SidePanelWidget(QtWidgets.QWidget): + """Details about selected workfile. + + Todos: + At this moment only shows created and modified date of file + or its size. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + parent (QtWidgets.QWidget): The parent widget. + """ + + published_workfile_message = ( + "INFO: Opened published workfiles will be stored in" + " temp directory on your machine. Current temp size: {}." + ) + + def __init__(self, controller, parent): + super(SidePanelWidget, self).__init__(parent) + + details_label = QtWidgets.QLabel("Details", self) + details_input = QtWidgets.QPlainTextEdit(self) + details_input.setReadOnly(True) + + artist_note_widget = QtWidgets.QWidget(self) + note_label = QtWidgets.QLabel("Artist note", artist_note_widget) + note_input = QtWidgets.QPlainTextEdit(artist_note_widget) + btn_note_save = QtWidgets.QPushButton("Save note", artist_note_widget) + + artist_note_layout = QtWidgets.QVBoxLayout(artist_note_widget) + artist_note_layout.setContentsMargins(0, 0, 0, 0) + artist_note_layout.addWidget(note_label, 0) + artist_note_layout.addWidget(note_input, 1) + artist_note_layout.addWidget( + btn_note_save, 0, alignment=QtCore.Qt.AlignRight + ) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(details_label, 0) + main_layout.addWidget(details_input, 1) + main_layout.addWidget(artist_note_widget, 1) + + note_input.textChanged.connect(self._on_note_change) + btn_note_save.clicked.connect(self._on_save_click) + + controller.register_event_callback( + "workarea.selection.changed", self._on_selection_change + ) + + self._details_input = details_input + self._artist_note_widget = artist_note_widget + self._note_input = note_input + self._btn_note_save = btn_note_save + + self._folder_id = None + self._task_id = None + self._filepath = None + self._orig_note = "" + self._controller = controller + + self._set_context(None, None, None) + + def set_published_mode(self, published_mode): + """Change published mode. + + Args: + published_mode (bool): Published mode enabled. + """ + + self._artist_note_widget.setVisible(not published_mode) + + def _on_selection_change(self, event): + folder_id = event["folder_id"] + task_id = event["task_id"] + filepath = event["path"] + + self._set_context(folder_id, task_id, filepath) + + def _on_note_change(self): + text = self._note_input.toPlainText() + self._btn_note_save.setEnabled(self._orig_note != text) + + def _on_save_click(self): + note = self._note_input.toPlainText() + self._controller.save_workfile_info( + self._folder_id, + self._task_id, + self._filepath, + note + ) + self._orig_note = note + self._btn_note_save.setEnabled(False) + + def _set_context(self, folder_id, task_id, filepath): + workfile_info = None + # Check if folder, task and file are selected + if bool(folder_id) and bool(task_id) and bool(filepath): + workfile_info = self._controller.get_workfile_info( + folder_id, task_id, filepath + ) + enabled = workfile_info is not None + + self._details_input.setEnabled(enabled) + self._note_input.setEnabled(enabled) + self._btn_note_save.setEnabled(enabled) + + self._folder_id = folder_id + self._task_id = task_id + self._filepath = filepath + + # Disable inputs and remove texts if any required arguments are + # missing + if not enabled: + self._orig_note = "" + self._details_input.setPlainText("") + self._note_input.setPlainText("") + return + + note = workfile_info.note + size_value = file_size_to_string(workfile_info.filesize) + + # Append html string + datetime_format = "%b %d %Y %H:%M:%S" + creation_time = datetime.datetime.fromtimestamp( + workfile_info.creation_time) + modification_time = datetime.datetime.fromtimestamp( + workfile_info.modification_time) + lines = ( + "Size:", + size_value, + "Created:", + creation_time.strftime(datetime_format), + "Modified:", + modification_time.strftime(datetime_format) + ) + self._orig_note = note + self._note_input.setPlainText(note) + + # Set as empty string + self._details_input.setPlainText("") + self._details_input.appendHtml("
".join(lines)) diff --git a/openpype/tools/ayon_workfiles/widgets/tasks_widget.py b/openpype/tools/ayon_workfiles/widgets/tasks_widget.py new file mode 100644 index 0000000000..04f5b286b1 --- /dev/null +++ b/openpype/tools/ayon_workfiles/widgets/tasks_widget.py @@ -0,0 +1,420 @@ +import uuid +import qtawesome +from qtpy import QtWidgets, QtGui, QtCore + +from openpype.style import get_disabled_entity_icon_color +from openpype.tools.utils import DeselectableTreeView + +from .constants import ( + ITEM_NAME_ROLE, + ITEM_ID_ROLE, + PARENT_ID_ROLE, +) + +SENDER_NAME = "qt_tasks_model" + + +class RefreshThread(QtCore.QThread): + """Thread for refreshing tasks. + + Call controller to get tasks and emit signal when finished. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + folder_id (str): Folder id. + """ + + refresh_finished = QtCore.Signal(str) + + def __init__(self, controller, folder_id): + super(RefreshThread, self).__init__() + self._id = uuid.uuid4().hex + self._controller = controller + self._folder_id = folder_id + self._result = None + + @property + def id(self): + return self._id + + def run(self): + self._result = self._controller.get_task_items( + self._folder_id, SENDER_NAME) + self.refresh_finished.emit(self.id) + + def get_result(self): + return self._result + + +class TasksModel(QtGui.QStandardItemModel): + """Tasks model which cares about refresh of tasks by folder id. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + """ + + refreshed = QtCore.Signal() + + def __init__(self, controller): + super(TasksModel, self).__init__() + + self._controller = controller + + self._items_by_name = {} + self._has_content = False + self._is_refreshing = False + + self._invalid_selection_item_used = False + self._invalid_selection_item = None + self._empty_tasks_item_used = False + self._empty_tasks_item = None + + self._last_folder_id = None + + self._refresh_threads = {} + self._current_refresh_thread = None + + # Initial state + self._add_invalid_selection_item() + + def clear(self): + self._items_by_name = {} + self._has_content = False + self._remove_invalid_items() + super(TasksModel, self).clear() + + def refresh(self, folder_id): + """Refresh tasks for folder. + + Args: + folder_id (Union[str, None]): Folder id. + """ + + self._refresh(folder_id) + + def get_index_by_name(self, task_name): + """Find item by name and return its index. + + Returns: + QtCore.QModelIndex: Index of item. Is invalid if task is not + found by name. + """ + + item = self._items_by_name.get(task_name) + if item is None: + return QtCore.QModelIndex() + return self.indexFromItem(item) + + def get_last_folder_id(self): + """Get last refreshed folder id. + + Returns: + Union[str, None]: Folder id. + """ + + return self._last_folder_id + + def _get_invalid_selection_item(self): + if self._invalid_selection_item is None: + item = QtGui.QStandardItem("Select a folder") + item.setFlags(QtCore.Qt.NoItemFlags) + icon = qtawesome.icon( + "fa.times", + color=get_disabled_entity_icon_color() + ) + item.setData(icon, QtCore.Qt.DecorationRole) + self._invalid_selection_item = item + return self._invalid_selection_item + + def _get_empty_task_item(self): + if self._empty_tasks_item is None: + item = QtGui.QStandardItem("No task") + icon = qtawesome.icon( + "fa.exclamation-circle", + color=get_disabled_entity_icon_color() + ) + item.setData(icon, QtCore.Qt.DecorationRole) + item.setFlags(QtCore.Qt.NoItemFlags) + self._empty_tasks_item = item + return self._empty_tasks_item + + def _add_invalid_item(self, item): + self.clear() + root_item = self.invisibleRootItem() + root_item.appendRow(item) + + def _remove_invalid_item(self, item): + root_item = self.invisibleRootItem() + root_item.takeRow(item.row()) + + def _remove_invalid_items(self): + self._remove_invalid_selection_item() + self._remove_empty_task_item() + + def _add_invalid_selection_item(self): + if not self._invalid_selection_item_used: + self._add_invalid_item(self._get_invalid_selection_item()) + self._invalid_selection_item_used = True + + def _remove_invalid_selection_item(self): + if self._invalid_selection_item: + self._remove_invalid_item(self._get_invalid_selection_item()) + self._invalid_selection_item_used = False + + def _add_empty_task_item(self): + if not self._empty_tasks_item_used: + self._add_invalid_item(self._get_empty_task_item()) + self._empty_tasks_item_used = True + + def _remove_empty_task_item(self): + if self._empty_tasks_item_used: + self._remove_invalid_item(self._get_empty_task_item()) + self._empty_tasks_item_used = False + + def _refresh(self, folder_id): + self._is_refreshing = True + self._last_folder_id = folder_id + if not folder_id: + self._add_invalid_selection_item() + self._current_refresh_thread = None + self._is_refreshing = False + self.refreshed.emit() + return + + thread = RefreshThread(self._controller, folder_id) + self._current_refresh_thread = thread.id + self._refresh_threads[thread.id] = thread + thread.refresh_finished.connect(self._on_refresh_thread) + thread.start() + + def _on_refresh_thread(self, thread_id): + """Callback when refresh thread is finished. + + Technically can be running multiple refresh threads at the same time, + to avoid using values from wrong thread, we check if thread id is + current refresh thread id. + + Tasks are stored by name, so if a folder has same task name as + previously selected folder it keeps the selection. + + Args: + thread_id (str): Thread id. + """ + + thread = self._refresh_threads.pop(thread_id) + if thread_id != self._current_refresh_thread: + return + + task_items = thread.get_result() + # Task items are refreshed + if task_items is None: + return + + # No tasks are available on folder + if not task_items: + self._add_empty_task_item() + return + self._remove_invalid_items() + + new_items = [] + new_names = set() + for task_item in task_items: + name = task_item.name + new_names.add(name) + item = self._items_by_name.get(name) + if item is None: + item = QtGui.QStandardItem() + item.setEditable(False) + new_items.append(item) + self._items_by_name[name] = item + + # TODO cache locally + icon = qtawesome.icon( + task_item.icon_name, + color=task_item.icon_color, + ) + item.setData(task_item.label, QtCore.Qt.DisplayRole) + item.setData(name, ITEM_NAME_ROLE) + item.setData(task_item.id, ITEM_ID_ROLE) + item.setData(task_item.parent_id, PARENT_ID_ROLE) + item.setData(icon, QtCore.Qt.DecorationRole) + + root_item = self.invisibleRootItem() + + for name in set(self._items_by_name) - new_names: + item = self._items_by_name.pop(name) + root_item.removeRow(item.row()) + + if new_items: + root_item.appendRows(new_items) + + self._has_content = root_item.rowCount() > 0 + self._is_refreshing = False + self.refreshed.emit() + + @property + def is_refreshing(self): + """Model is refreshing. + + Returns: + bool: Model is refreshing + """ + + return self._is_refreshing + + @property + def has_content(self): + """Model has content. + + Returns: + bools: Have at least one task. + """ + + return self._has_content + + def headerData(self, section, orientation, role): + # Show nice labels in the header + if ( + role == QtCore.Qt.DisplayRole + and orientation == QtCore.Qt.Horizontal + ): + if section == 0: + return "Tasks" + + return super(TasksModel, self).headerData( + section, orientation, role + ) + + +class TasksWidget(QtWidgets.QWidget): + """Tasks widget. + + Widget that handles tasks view, model and selection. + + Args: + controller (AbstractWorkfilesFrontend): Workfiles controller. + """ + + def __init__(self, controller, parent): + super(TasksWidget, self).__init__(parent) + + tasks_view = DeselectableTreeView(self) + tasks_view.setIndentation(0) + + tasks_model = TasksModel(controller) + tasks_proxy_model = QtCore.QSortFilterProxyModel() + tasks_proxy_model.setSourceModel(tasks_model) + + tasks_view.setModel(tasks_proxy_model) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(tasks_view, 1) + + controller.register_event_callback( + "tasks.refresh.finished", + self._on_tasks_refresh_finished + ) + controller.register_event_callback( + "selection.folder.changed", + self._folder_selection_changed + ) + controller.register_event_callback( + "expected_selection_changed", + self._on_expected_selection_change + ) + + selection_model = tasks_view.selectionModel() + selection_model.selectionChanged.connect(self._on_selection_change) + + tasks_model.refreshed.connect(self._on_tasks_model_refresh) + + self._controller = controller + self._tasks_view = tasks_view + self._tasks_model = tasks_model + self._tasks_proxy_model = tasks_proxy_model + + self._selected_folder_id = None + + self._expected_selection_data = None + + def _clear(self): + self._tasks_model.clear() + + def _on_tasks_refresh_finished(self, event): + """Tasks were refreshed in controller. + + Ignore if refresh was triggered by tasks model, or refreshed folder is + not the same as currently selected folder. + + Args: + event (Event): Event object. + """ + + # Refresh only if current folder id is the same + if ( + event["sender"] == SENDER_NAME + or event["folder_id"] != self._selected_folder_id + ): + return + self._tasks_model.refresh(self._selected_folder_id) + + def _folder_selection_changed(self, event): + self._selected_folder_id = event["folder_id"] + self._tasks_model.refresh(self._selected_folder_id) + + def _on_tasks_model_refresh(self): + if not self._set_expected_selection(): + self._on_selection_change() + self._tasks_proxy_model.sort(0) + + def _set_expected_selection(self): + if self._expected_selection_data is None: + return False + folder_id = self._expected_selection_data["folder_id"] + task_name = self._expected_selection_data["task_name"] + self._expected_selection_data = None + model_folder_id = self._tasks_model.get_last_folder_id() + if folder_id != model_folder_id: + return False + if task_name is not None: + index = self._tasks_model.get_index_by_name(task_name) + if index.isValid(): + proxy_index = self._tasks_proxy_model.mapFromSource(index) + self._tasks_view.setCurrentIndex(proxy_index) + self._controller.expected_task_selected(folder_id, task_name) + return True + + def _on_expected_selection_change(self, event): + if event["task_selected"] or not event["folder_selected"]: + return + + model_folder_id = self._tasks_model.get_last_folder_id() + folder_id = event["folder_id"] + self._expected_selection_data = { + "task_name": event["task_name"], + "folder_id": folder_id, + } + + if folder_id != model_folder_id or self._tasks_model.is_refreshing: + return + self._set_expected_selection() + + def _get_selected_item_ids(self): + selection_model = self._tasks_view.selectionModel() + for index in selection_model.selectedIndexes(): + task_id = index.data(ITEM_ID_ROLE) + task_name = index.data(ITEM_NAME_ROLE) + parent_id = index.data(PARENT_ID_ROLE) + if task_name is not None: + return parent_id, task_id, task_name + return self._selected_folder_id, None, None + + def _on_selection_change(self): + # Don't trigger task change during refresh + # - a task was deselected if that happens + # - can cause crash triggered during tasks refreshing + if self._tasks_model.is_refreshing: + return + parent_id, task_id, task_name = self._get_selected_item_ids() + self._controller.set_selected_task(parent_id, task_id, task_name) diff --git a/openpype/tools/ayon_workfiles/widgets/utils.py b/openpype/tools/ayon_workfiles/widgets/utils.py new file mode 100644 index 0000000000..6a61239f8d --- /dev/null +++ b/openpype/tools/ayon_workfiles/widgets/utils.py @@ -0,0 +1,94 @@ +from qtpy import QtWidgets, QtCore +from openpype.tools.flickcharm import FlickCharm + + +class TreeView(QtWidgets.QTreeView): + """Ultimate TreeView with flick charm and double click signals. + + Tree view have deselectable mode, which allows to deselect items by + clicking on item area without any items. + + Todos: + Add to tools utils. + """ + + double_clicked_left = QtCore.Signal() + double_clicked_right = QtCore.Signal() + + def __init__(self, *args, **kwargs): + super(TreeView, self).__init__(*args, **kwargs) + self._deselectable = False + + self._flick_charm_activated = False + self._flick_charm = FlickCharm(parent=self) + self._before_flick_scroll_mode = None + + def is_deselectable(self): + return self._deselectable + + def set_deselectable(self, deselectable): + self._deselectable = deselectable + + deselectable = property(is_deselectable, set_deselectable) + + def mousePressEvent(self, event): + if self._deselectable: + index = self.indexAt(event.pos()) + if not index.isValid(): + # clear the selection + self.clearSelection() + # clear the current index + self.setCurrentIndex(QtCore.QModelIndex()) + super(TreeView, self).mousePressEvent(event) + + def mouseDoubleClickEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + self.double_clicked_left.emit() + + elif event.button() == QtCore.Qt.RightButton: + self.double_clicked_right.emit() + + return super(TreeView, self).mouseDoubleClickEvent(event) + + def activate_flick_charm(self): + if self._flick_charm_activated: + return + self._flick_charm_activated = True + self._before_flick_scroll_mode = self.verticalScrollMode() + self._flick_charm.activateOn(self) + self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) + + def deactivate_flick_charm(self): + if not self._flick_charm_activated: + return + self._flick_charm_activated = False + self._flick_charm.deactivateFrom(self) + if self._before_flick_scroll_mode is not None: + self.setVerticalScrollMode(self._before_flick_scroll_mode) + + +class BaseOverlayFrame(QtWidgets.QFrame): + """Base frame for overlay widgets. + + Has implemented automated resize and event filtering. + """ + + def __init__(self, parent): + super(BaseOverlayFrame, self).__init__(parent) + self.setObjectName("OverlayFrame") + + self._parent = parent + + def setVisible(self, visible): + super(BaseOverlayFrame, self).setVisible(visible) + if visible: + self._parent.installEventFilter(self) + self.resize(self._parent.size()) + else: + self._parent.removeEventFilter(self) + + def eventFilter(self, obj, event): + if event.type() == QtCore.QEvent.Resize: + self.resize(obj.size()) + + return super(BaseOverlayFrame, self).eventFilter(obj, event) diff --git a/openpype/tools/ayon_workfiles/widgets/window.py b/openpype/tools/ayon_workfiles/widgets/window.py new file mode 100644 index 0000000000..ef352c8b18 --- /dev/null +++ b/openpype/tools/ayon_workfiles/widgets/window.py @@ -0,0 +1,400 @@ +from qtpy import QtCore, QtWidgets, QtGui + +from openpype import style, resources +from openpype.tools.utils import ( + PlaceholderLineEdit, + MessageOverlayObject, +) +from openpype.tools.utils.lib import get_qta_icon_by_name_and_color + +from openpype.tools.ayon_workfiles.control import BaseWorkfileController + +from .side_panel import SidePanelWidget +from .folders_widget import FoldersWidget +from .tasks_widget import TasksWidget +from .files_widget import FilesWidget +from .utils import BaseOverlayFrame + + +# TODO move to utils +# from openpype.tools.utils.lib import ( +# get_refresh_icon, get_go_to_current_icon) +def get_refresh_icon(): + return get_qta_icon_by_name_and_color( + "fa.refresh", style.get_default_tools_icon_color() + ) + + +def get_go_to_current_icon(): + return get_qta_icon_by_name_and_color( + "fa.arrow-down", style.get_default_tools_icon_color() + ) + + +class InvalidHostOverlay(BaseOverlayFrame): + def __init__(self, parent): + super(InvalidHostOverlay, self).__init__(parent) + + label_widget = QtWidgets.QLabel( + ( + "Workfiles tool is not supported in this host/DCCs." + "

This may be caused by a bug." + " Please contact your TD for more information." + ), + self + ) + label_widget.setAlignment(QtCore.Qt.AlignCenter) + label_widget.setObjectName("OverlayFrameLabel") + + layout = QtWidgets.QVBoxLayout(self) + layout.addStretch(2) + layout.addWidget(label_widget, 0, QtCore.Qt.AlignCenter) + layout.addStretch(3) + + label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + +class WorkfilesToolWindow(QtWidgets.QWidget): + """WorkFiles Window. + + Main windows of workfiles tool. + + Args: + controller (AbstractWorkfilesFrontend): Frontend controller. + parent (Optional[QtWidgets.QWidget]): Parent widget. + """ + + title = "Work Files" + + def __init__(self, controller=None, parent=None): + super(WorkfilesToolWindow, self).__init__(parent=parent) + + if controller is None: + controller = BaseWorkfileController() + + self.setWindowTitle(self.title) + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) + self.setWindowIcon(icon) + flags = self.windowFlags() | QtCore.Qt.Window + self.setWindowFlags(flags) + + self._default_window_flags = flags + + self._folder_widget = None + self._folder_filter_input = None + + self._files_widget = None + + self._first_show = True + self._controller_refreshed = False + self._context_to_set = None + # Host validation should happen only once + self._host_is_valid = None + + self._controller = controller + + # Create pages widget and set it as central widget + pages_widget = QtWidgets.QStackedWidget(self) + + home_page_widget = QtWidgets.QWidget(pages_widget) + home_body_widget = QtWidgets.QWidget(home_page_widget) + + col_1_widget = self._create_col_1_widget(controller, parent) + tasks_widget = TasksWidget(controller, home_body_widget) + col_3_widget = self._create_col_3_widget(controller, home_body_widget) + side_panel = SidePanelWidget(controller, home_body_widget) + + pages_widget.addWidget(home_page_widget) + + # Build home + home_page_layout = QtWidgets.QVBoxLayout(home_page_widget) + home_page_layout.addWidget(home_body_widget) + + # Build home - body + body_layout = QtWidgets.QVBoxLayout(home_body_widget) + split_widget = QtWidgets.QSplitter(home_body_widget) + split_widget.addWidget(col_1_widget) + split_widget.addWidget(tasks_widget) + split_widget.addWidget(col_3_widget) + split_widget.addWidget(side_panel) + split_widget.setSizes([255, 160, 455, 175]) + + body_layout.addWidget(split_widget) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.addWidget(pages_widget, 1) + + overlay_messages_widget = MessageOverlayObject(self) + overlay_invalid_host = InvalidHostOverlay(self) + overlay_invalid_host.setVisible(False) + + first_show_timer = QtCore.QTimer() + first_show_timer.setSingleShot(True) + first_show_timer.setInterval(50) + + first_show_timer.timeout.connect(self._on_first_show) + + controller.register_event_callback( + "save_as.finished", + self._on_save_as_finished, + ) + controller.register_event_callback( + "copy_representation.finished", + self._on_copy_representation_finished, + ) + controller.register_event_callback( + "workfile_duplicate.finished", + self._on_duplicate_finished + ) + controller.register_event_callback( + "open_workfile.finished", + self._on_open_finished + ) + controller.register_event_callback( + "controller.refresh.started", + self._on_controller_refresh_started, + ) + controller.register_event_callback( + "controller.refresh.finished", + self._on_controller_refresh_finished, + ) + + self._overlay_messages_widget = overlay_messages_widget + self._overlay_invalid_host = overlay_invalid_host + self._home_page_widget = home_page_widget + self._pages_widget = pages_widget + self._home_body_widget = home_body_widget + self._split_widget = split_widget + + self._tasks_widget = tasks_widget + self._side_panel = side_panel + + self._first_show_timer = first_show_timer + + self._post_init() + + def _post_init(self): + self._on_published_checkbox_changed() + + # Force focus on the open button by default, required for Houdini. + self._files_widget.setFocus() + + self.resize(1200, 600) + + def _create_col_1_widget(self, controller, parent): + col_widget = QtWidgets.QWidget(parent) + header_widget = QtWidgets.QWidget(col_widget) + + folder_filter_input = PlaceholderLineEdit(header_widget) + folder_filter_input.setPlaceholderText("Filter folders..") + + go_to_current_btn = QtWidgets.QPushButton(header_widget) + go_to_current_btn.setIcon(get_go_to_current_icon()) + go_to_current_btn_sp = go_to_current_btn.sizePolicy() + go_to_current_btn_sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum) + go_to_current_btn.setSizePolicy(go_to_current_btn_sp) + + refresh_btn = QtWidgets.QPushButton(header_widget) + refresh_btn.setIcon(get_refresh_icon()) + refresh_btn_sp = refresh_btn.sizePolicy() + refresh_btn_sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum) + refresh_btn.setSizePolicy(refresh_btn_sp) + + folder_widget = FoldersWidget(controller, col_widget) + + header_layout = QtWidgets.QHBoxLayout(header_widget) + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.addWidget(folder_filter_input, 1) + header_layout.addWidget(go_to_current_btn, 0) + header_layout.addWidget(refresh_btn, 0) + + col_layout = QtWidgets.QVBoxLayout(col_widget) + col_layout.setContentsMargins(0, 0, 0, 0) + col_layout.addWidget(header_widget, 0) + col_layout.addWidget(folder_widget, 1) + + folder_filter_input.textChanged.connect(self._on_folder_filter_change) + go_to_current_btn.clicked.connect(self._on_go_to_current_clicked) + refresh_btn.clicked.connect(self._on_refresh_clicked) + + self._folder_filter_input = folder_filter_input + self._folder_widget = folder_widget + + return col_widget + + def _create_col_3_widget(self, controller, parent): + col_widget = QtWidgets.QWidget(parent) + + header_widget = QtWidgets.QWidget(col_widget) + + files_filter_input = PlaceholderLineEdit(header_widget) + files_filter_input.setPlaceholderText("Filter files..") + + published_checkbox = QtWidgets.QCheckBox("Published", header_widget) + published_checkbox.setToolTip("Show published workfiles") + + header_layout = QtWidgets.QHBoxLayout(header_widget) + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.addWidget(files_filter_input, 1) + header_layout.addWidget(published_checkbox, 0) + + files_widget = FilesWidget(controller, col_widget) + + col_layout = QtWidgets.QVBoxLayout(col_widget) + col_layout.setContentsMargins(0, 0, 0, 0) + col_layout.addWidget(header_widget, 0) + col_layout.addWidget(files_widget, 1) + + files_filter_input.textChanged.connect( + self._on_file_text_filter_change) + published_checkbox.stateChanged.connect( + self._on_published_checkbox_changed + ) + + self._files_filter_input = files_filter_input + self._published_checkbox = published_checkbox + + self._files_widget = files_widget + + return col_widget + + def set_window_on_top(self, on_top): + """Set window on top of other windows. + + Args: + on_top (bool): Show on top of other windows. + """ + + flags = self._default_window_flags + if on_top: + flags |= QtCore.Qt.WindowStaysOnTopHint + if self.windowFlags() != flags: + self.setWindowFlags(flags) + + def ensure_visible(self, use_context=True, save=True, on_top=False): + """Ensure the window is visible. + + This method expects arguments for compatibility with previous variant + of Workfiles tool. + + Args: + use_context (Optional[bool]): DEPRECATED: This argument is + ignored. + save (Optional[bool]): Allow to save workfiles. + on_top (Optional[bool]): Show on top of other windows. + """ + + save = True if save is None else save + on_top = False if on_top is None else on_top + + is_visible = self.isVisible() + self._controller.set_save_enabled(save) + self.set_window_on_top(on_top) + + self.show() + self.raise_() + self.activateWindow() + if is_visible: + self.refresh() + + def refresh(self): + """Trigger refresh of workfiles tool controller.""" + + self._controller.refresh() + + def showEvent(self, event): + super(WorkfilesToolWindow, self).showEvent(event) + if self._first_show: + self._first_show = False + self._first_show_timer.start() + self.setStyleSheet(style.load_stylesheet()) + + def keyPressEvent(self, event): + """Custom keyPressEvent. + + Override keyPressEvent to do nothing so that Maya's panels won't + take focus when pressing "SHIFT" whilst mouse is over viewport or + outliner. This way users don't accidentally perform Maya commands + whilst trying to name an instance. + """ + + pass + + def _on_first_show(self): + if not self._controller_refreshed: + self.refresh() + + def _on_file_text_filter_change(self, text): + self._files_widget.set_text_filter(text) + + def _on_published_checkbox_changed(self): + """Publish mode changed. + + Tell children widgets about it so they can handle the mode. + """ + + published_mode = self._published_checkbox.isChecked() + self._files_widget.set_published_mode(published_mode) + self._side_panel.set_published_mode(published_mode) + + def _on_folder_filter_change(self, text): + self._folder_widget.set_name_filer(text) + + def _on_go_to_current_clicked(self): + self._controller.go_to_current_context() + + def _on_refresh_clicked(self): + self.refresh() + + def _on_controller_refresh_started(self): + self._controller_refreshed = True + + def _on_controller_refresh_finished(self): + if self._host_is_valid is None: + self._host_is_valid = self._controller.is_host_valid() + self._overlay_invalid_host.setVisible(not self._host_is_valid) + + if not self._host_is_valid: + return + + def _on_save_as_finished(self, event): + if event["failed"]: + self._overlay_messages_widget.add_message( + "Failed to save workfile", + "error", + ) + else: + self._overlay_messages_widget.add_message( + "Workfile saved" + ) + + def _on_copy_representation_finished(self, event): + if event["failed"]: + self._overlay_messages_widget.add_message( + "Failed to copy published workfile", + "error", + ) + else: + self._overlay_messages_widget.add_message( + "Publish workfile saved" + ) + + def _on_duplicate_finished(self, event): + if event["failed"]: + self._overlay_messages_widget.add_message( + "Failed to duplicate workfile", + "error", + ) + else: + self._overlay_messages_widget.add_message( + "Workfile duplicated" + ) + + def _on_open_finished(self, event): + if event["failed"]: + self._overlay_messages_widget.add_message( + "Failed to open workfile", + "error", + ) + else: + self.close() diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index bc4b7867c2..2ebc973a47 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -6,6 +6,8 @@ use singleton approach with global functions (using helper anyway). import os import pyblish.api + +from openpype import AYON_SERVER_ENABLED from openpype.host import IWorkfileHost, ILoadHost from openpype.lib import Logger from openpype.pipeline import ( @@ -46,17 +48,29 @@ class HostToolsHelper: self._log = Logger.get_logger(self.__class__.__name__) return self._log + def _init_ayon_workfiles_tool(self, parent): + from openpype.tools.ayon_workfiles.widgets import WorkfilesToolWindow + + workfiles_window = WorkfilesToolWindow(parent=parent) + self._workfiles_tool = workfiles_window + + def _init_openpype_workfiles_tool(self, parent): + from openpype.tools.workfiles.app import Window + + # Host validation + host = registered_host() + IWorkfileHost.validate_workfile_methods(host) + + workfiles_window = Window(parent=parent) + self._workfiles_tool = workfiles_window + def get_workfiles_tool(self, parent): """Create, cache and return workfiles tool window.""" if self._workfiles_tool is None: - from openpype.tools.workfiles.app import Window - - # Host validation - host = registered_host() - IWorkfileHost.validate_workfile_methods(host) - - workfiles_window = Window(parent=parent) - self._workfiles_tool = workfiles_window + if AYON_SERVER_ENABLED: + self._init_ayon_workfiles_tool(parent) + else: + self._init_openpype_workfiles_tool(parent) return self._workfiles_tool From 5ba400d89173195cadf885df3c5bc0129c84fd61 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 9 Sep 2023 03:24:54 +0000 Subject: [PATCH 59/70] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index d5d46bab0c..b6c56296bc 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.5" +__version__ = "3.16.6-nightly.1" From 5475908051c3166daa57155c18b36f672170e12b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 9 Sep 2023 03:25:32 +0000 Subject: [PATCH 60/70] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a35dbf1a17..7a39103859 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.16.6-nightly.1 - 3.16.5 - 3.16.5-nightly.5 - 3.16.5-nightly.4 @@ -134,7 +135,6 @@ body: - 3.14.9-nightly.4 - 3.14.9-nightly.3 - 3.14.9-nightly.2 - - 3.14.9-nightly.1 validations: required: true - type: dropdown From 81446b1bbb972bcb8ee8fa9c9df06868d7c008aa Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 11 Sep 2023 11:55:49 +0100 Subject: [PATCH 61/70] Remove hardcoded subset name for reviews --- openpype/hosts/blender/plugins/publish/collect_review.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_review.py b/openpype/hosts/blender/plugins/publish/collect_review.py index 6459927015..3bf2e39e24 100644 --- a/openpype/hosts/blender/plugins/publish/collect_review.py +++ b/openpype/hosts/blender/plugins/publish/collect_review.py @@ -39,15 +39,11 @@ class CollectReview(pyblish.api.InstancePlugin): ] if not instance.data.get("remove"): - - task = instance.context.data["task"] - # Store focal length in `burninDataMembers` burninData = instance.data.setdefault("burninDataMembers", {}) burninData["focalLength"] = focal_length instance.data.update({ - "subset": f"{task}Review", "review_camera": camera, "frameStart": instance.context.data["frameStart"], "frameEnd": instance.context.data["frameEnd"], From ae02fe220aefb292f705f715fea4def1b2192782 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 11 Sep 2023 16:28:11 +0200 Subject: [PATCH 62/70] AfterEffects: fix imports of image sequences (#5581) * Fix loading image sequence in AE * Fix logic Files might be list or str * Update openpype/hosts/aftereffects/plugins/load/load_file.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/aftereffects/plugins/load/load_file.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/load/load_file.py b/openpype/hosts/aftereffects/plugins/load/load_file.py index def7c927ab..8d52aac546 100644 --- a/openpype/hosts/aftereffects/plugins/load/load_file.py +++ b/openpype/hosts/aftereffects/plugins/load/load_file.py @@ -31,13 +31,8 @@ class FileLoader(api.AfterEffectsLoader): path = self.filepath_from_context(context) - repr_cont = context["representation"]["context"] - if "#" not in path: - frame = repr_cont.get("frame") - if frame: - padding = len(frame) - path = path.replace(frame, "#" * padding) - import_options['sequence'] = True + if len(context["representation"]["files"]) > 1: + import_options['sequence'] = True if not path: repr_id = context["representation"]["_id"] From 1fdbe05905995641f02b4c4c3b2d7e27be6ecf3a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 11 Sep 2023 17:21:38 +0200 Subject: [PATCH 63/70] Photoshop: fixed blank Flatten image (#5600) * OP-6763 - refresh all visible for Flatten image Previously newly added layers were missing. * OP-6763 - added explicit image collector Creator was adding 'layer' metadata from workfile only during collect_instances, it was missing for newly added layers. This should be cleaner approach * OP-6763 - removed unnecessary method overwrite Creator is not adding layer to instance, separate collector created. * OP-6763 - cleanup of names Was failing when template for subset name for image family contained {layer} * OP-6763 - cleanup, removed adding layer metadata Separate collector created, cleaner. Fixed propagation of mark_for_review * OP-6763 - using members instead of layer data Members should be more reliable. * OP-6763 - updated image from Settings Explicit subset name template was removed some time ago as confusing. * OP-6763 - added explicit local plugin Automated plugin has different logic, local would need to handle if auto_image is disabled by artist * OP-6763 - Hound * OP-6345 - fix - review for image family Image family instance contained flattened content. Now it reuses previously extracted file without need to re-extract. --- .../plugins/create/create_flatten_image.py | 40 +++++++++--- .../photoshop/plugins/create/create_image.py | 17 ++--- .../publish/collect_auto_image_refresh.py | 24 +++++++ .../plugins/publish/collect_image.py | 20 ++++++ .../plugins/publish/extract_image.py | 8 ++- .../plugins/publish/extract_review.py | 59 +++++++++++++++--- website/docs/admin_hosts_photoshop.md | 7 +-- .../assets/admin_hosts_photoshop_settings.png | Bin 14364 -> 16718 bytes 8 files changed, 144 insertions(+), 31 deletions(-) create mode 100644 openpype/hosts/photoshop/plugins/publish/collect_auto_image_refresh.py create mode 100644 openpype/hosts/photoshop/plugins/publish/collect_image.py diff --git a/openpype/hosts/photoshop/plugins/create/create_flatten_image.py b/openpype/hosts/photoshop/plugins/create/create_flatten_image.py index 9d4189a1a3..e4229788bd 100644 --- a/openpype/hosts/photoshop/plugins/create/create_flatten_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_flatten_image.py @@ -4,6 +4,7 @@ from openpype.lib import BoolDef import openpype.hosts.photoshop.api as api from openpype.hosts.photoshop.lib import PSAutoCreator from openpype.pipeline.create import get_subset_name +from openpype.lib import prepare_template_data from openpype.client import get_asset_by_name @@ -37,19 +38,14 @@ class AutoImageCreator(PSAutoCreator): asset_doc = get_asset_by_name(project_name, asset_name) if existing_instance is None: - subset_name = get_subset_name( - self.family, self.default_variant, task_name, asset_doc, + subset_name = self.get_subset_name( + self.default_variant, task_name, asset_doc, project_name, host_name ) - publishable_ids = [layer.id for layer in api.stub().get_layers() - if layer.visible] data = { "asset": asset_name, "task": task_name, - # ids are "virtual" layers, won't get grouped as 'members' do - # same difference in color coded layers in WP - "ids": publishable_ids } if not self.active_on_create: @@ -69,8 +65,8 @@ class AutoImageCreator(PSAutoCreator): existing_instance["asset"] != asset_name or existing_instance["task"] != task_name ): - subset_name = get_subset_name( - self.family, self.default_variant, task_name, asset_doc, + subset_name = self.get_subset_name( + self.default_variant, task_name, asset_doc, project_name, host_name ) @@ -118,3 +114,29 @@ class AutoImageCreator(PSAutoCreator): Artist might disable this instance from publishing or from creating review for it though. """ + + def get_subset_name( + self, + variant, + task_name, + asset_doc, + project_name, + host_name=None, + instance=None + ): + dynamic_data = prepare_template_data({"layer": "{layer}"}) + subset_name = get_subset_name( + self.family, variant, task_name, asset_doc, + project_name, host_name, dynamic_data=dynamic_data + ) + return self._clean_subset_name(subset_name) + + def _clean_subset_name(self, subset_name): + """Clean all variants leftover {layer} from subset name.""" + dynamic_data = prepare_template_data({"layer": "{layer}"}) + for value in dynamic_data.values(): + if value in subset_name: + return (subset_name.replace(value, "") + .replace("__", "_") + .replace("..", ".")) + return subset_name diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index 8d3ac9f459..af20d456e0 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -94,12 +94,17 @@ class ImageCreator(Creator): name = self._clean_highlights(stub, directory) layer_names_in_hierarchy.append(name) - data.update({"subset": subset_name}) - data.update({"members": [str(group.id)]}) - data.update({"layer_name": layer_name}) - data.update({"long_name": "_".join(layer_names_in_hierarchy)}) + data_update = { + "subset": subset_name, + "members": [str(group.id)], + "layer_name": layer_name, + "long_name": "_".join(layer_names_in_hierarchy) + } + data.update(data_update) - creator_attributes = {"mark_for_review": self.mark_for_review} + mark_for_review = (pre_create_data.get("mark_for_review") or + self.mark_for_review) + creator_attributes = {"mark_for_review": mark_for_review} data.update({"creator_attributes": creator_attributes}) if not self.active_on_create: @@ -124,8 +129,6 @@ class ImageCreator(Creator): if creator_id == self.identifier: instance_data = self._handle_legacy(instance_data) - layer = api.stub().get_layer(instance_data["members"][0]) - instance_data["layer"] = layer instance = CreatedInstance.from_existing( instance_data, self ) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_auto_image_refresh.py b/openpype/hosts/photoshop/plugins/publish/collect_auto_image_refresh.py new file mode 100644 index 0000000000..741fb0e9cd --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/collect_auto_image_refresh.py @@ -0,0 +1,24 @@ +import pyblish.api + +from openpype.hosts.photoshop import api as photoshop + + +class CollectAutoImageRefresh(pyblish.api.ContextPlugin): + """Refreshes auto_image instance with currently visible layers.. + """ + + label = "Collect Auto Image Refresh" + order = pyblish.api.CollectorOrder + hosts = ["photoshop"] + order = pyblish.api.CollectorOrder + 0.2 + + def process(self, context): + for instance in context: + creator_identifier = instance.data.get("creator_identifier") + if creator_identifier and creator_identifier == "auto_image": + self.log.debug("Auto image instance found, won't create new") + # refresh existing auto image instance with current visible + publishable_ids = [layer.id for layer in photoshop.stub().get_layers() # noqa + if layer.visible] + instance.data["ids"] = publishable_ids + return diff --git a/openpype/hosts/photoshop/plugins/publish/collect_image.py b/openpype/hosts/photoshop/plugins/publish/collect_image.py new file mode 100644 index 0000000000..64727cef33 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/collect_image.py @@ -0,0 +1,20 @@ +import pyblish.api + +from openpype.hosts.photoshop import api + + +class CollectImage(pyblish.api.InstancePlugin): + """Collect layer metadata into a instance. + + Used later in validation + """ + order = pyblish.api.CollectorOrder + 0.200 + label = 'Collect Image' + + hosts = ["photoshop"] + families = ["image"] + + def process(self, instance): + if instance.data.get("members"): + layer = api.stub().get_layer(instance.data["members"][0]) + instance.data["layer"] = layer diff --git a/openpype/hosts/photoshop/plugins/publish/extract_image.py b/openpype/hosts/photoshop/plugins/publish/extract_image.py index cdb28c742d..680f580cc0 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_image.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_image.py @@ -45,9 +45,11 @@ class ExtractImage(pyblish.api.ContextPlugin): # Perform extraction files = {} ids = set() - layer = instance.data.get("layer") - if layer: - ids.add(layer.id) + # real layers and groups + members = instance.data("members") + if members: + ids.update(set([int(member) for member in members])) + # virtual groups collected by color coding or auto_image add_ids = instance.data.pop("ids", None) if add_ids: ids.update(set(add_ids)) diff --git a/openpype/hosts/photoshop/plugins/publish/extract_review.py b/openpype/hosts/photoshop/plugins/publish/extract_review.py index 4aa7a05bd1..afddbdba31 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_review.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_review.py @@ -1,4 +1,5 @@ import os +import shutil from PIL import Image from openpype.lib import ( @@ -55,6 +56,7 @@ class ExtractReview(publish.Extractor): } if instance.data["family"] != "review": + self.log.debug("Existing extracted file from image family used.") # enable creation of review, without this jpg review would clash # with jpg of the image family output_name = repre_name @@ -62,8 +64,15 @@ class ExtractReview(publish.Extractor): repre_skeleton.update({"name": repre_name, "outputName": output_name}) - if self.make_image_sequence and len(layers) > 1: - self.log.info("Extract layers to image sequence.") + img_file = self.output_seq_filename % 0 + self._prepare_file_for_image_family(img_file, instance, + staging_dir) + repre_skeleton.update({ + "files": img_file, + }) + processed_img_names = [img_file] + elif self.make_image_sequence and len(layers) > 1: + self.log.debug("Extract layers to image sequence.") img_list = self._save_sequence_images(staging_dir, layers) repre_skeleton.update({ @@ -72,17 +81,17 @@ class ExtractReview(publish.Extractor): "fps": fps, "files": img_list, }) - instance.data["representations"].append(repre_skeleton) processed_img_names = img_list else: - self.log.info("Extract layers to flatten image.") - img_list = self._save_flatten_image(staging_dir, layers) + self.log.debug("Extract layers to flatten image.") + img_file = self._save_flatten_image(staging_dir, layers) repre_skeleton.update({ - "files": img_list, + "files": img_file, }) - instance.data["representations"].append(repre_skeleton) - processed_img_names = [img_list] + processed_img_names = [img_file] + + instance.data["representations"].append(repre_skeleton) ffmpeg_args = get_ffmpeg_tool_args("ffmpeg") @@ -111,6 +120,35 @@ class ExtractReview(publish.Extractor): self.log.info(f"Extracted {instance} to {staging_dir}") + def _prepare_file_for_image_family(self, img_file, instance, staging_dir): + """Converts existing file for image family to .jpg + + Image instance could have its own separate review (instance per layer + for example). This uses extracted file instead of extracting again. + Args: + img_file (str): name of output file (with 0000 value for ffmpeg + later) + instance: + staging_dir (str): temporary folder where extracted file is located + """ + repre_file = instance.data["representations"][0] + source_file_path = os.path.join(repre_file["stagingDir"], + repre_file["files"]) + if not os.path.exists(source_file_path): + raise RuntimeError(f"{source_file_path} doesn't exist for " + "review to create from") + _, ext = os.path.splitext(repre_file["files"]) + if ext != ".jpg": + im = Image.open(source_file_path) + # without this it produces messy low quality jpg + rgb_im = Image.new("RGBA", (im.width, im.height), "#ffffff") + rgb_im.alpha_composite(im) + rgb_im.convert("RGB").save(os.path.join(staging_dir, img_file)) + else: + # handles already .jpg + shutil.copy(source_file_path, + os.path.join(staging_dir, img_file)) + def _generate_mov(self, ffmpeg_path, instance, fps, no_of_frames, source_files_pattern, staging_dir): """Generates .mov to upload to Ftrack. @@ -218,6 +256,11 @@ class ExtractReview(publish.Extractor): (list) of PSItem """ layers = [] + # creating review for existing 'image' instance + if instance.data["family"] == "image" and instance.data.get("layer"): + layers.append(instance.data["layer"]) + return layers + for image_instance in instance.context: if image_instance.data["family"] != "image": continue diff --git a/website/docs/admin_hosts_photoshop.md b/website/docs/admin_hosts_photoshop.md index de684f01d2..d79789760e 100644 --- a/website/docs/admin_hosts_photoshop.md +++ b/website/docs/admin_hosts_photoshop.md @@ -33,7 +33,6 @@ Provides list of [variants](artist_concepts.md#variant) that will be shown to an Provides simplified publishing process. It will create single `image` instance for artist automatically. This instance will produce flatten image from all visible layers in a workfile. -- Subset template for flatten image - provide template for subset name for this instance (example `imageBeauty`) - Review - should be separate review created for this instance ### Create Review @@ -111,11 +110,11 @@ Set Byte limit for review file. Applicable if gigantic `image` instances are pro #### Extract jpg Options -Handles tags for produced `.jpg` representation. `Create review` and `Add review to Ftrack` are defaults. +Handles tags for produced `.jpg` representation. `Create review` and `Add review to Ftrack` are defaults. #### Extract mov Options -Handles tags for produced `.mov` representation. `Create review` and `Add review to Ftrack` are defaults. +Handles tags for produced `.mov` representation. `Create review` and `Add review to Ftrack` are defaults. ### Workfile Builder @@ -124,4 +123,4 @@ Allows to open prepared workfile for an artist when no workfile exists. Useful t Could be configured per `Task type`, eg. `composition` task type could use different `.psd` template file than `art` task. Workfile template must be accessible for all artists. -(Currently not handled by [SiteSync](module_site_sync.md)) \ No newline at end of file +(Currently not handled by [SiteSync](module_site_sync.md)) diff --git a/website/docs/assets/admin_hosts_photoshop_settings.png b/website/docs/assets/admin_hosts_photoshop_settings.png index aaa6ecbed7b353733f8424abe3c2df0cb903935a..9478fbedf785ca8ae66132a14cb400c38e2dccc9 100644 GIT binary patch literal 16718 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 literal 14364 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 Date: Mon, 11 Sep 2023 17:23:08 +0200 Subject: [PATCH 64/70] AfterEffects: added validator for missing files in FootageItems (#5590) * OP-6345 - updated logic to return path and comp for FootageItem Used later to check existance of file in published comps * OP-6345 - added validator if footage files exist Comp could contain multiple FootageItems, eg imported file(s). If file is missing render triggered by jsx fails silently. * OP-6345 - updated extension * OP-6345 - small updates after testing * OP-6345 - fix - handle Solid Footage items JSX failed silently on Solid item as it doesn't have any `.file` * OP-6345 - enhance documentation * OP-6345 - remove optionality This plugin shouldn't be optional as when needed and skipped it result in really weird behavior. * OP-6345 - updated documentation Added missing plugins. * OP-6345 - missed functionality for optionality * OP-6345 - removed unneeded import --- openpype/hosts/aftereffects/api/extension.zxp | Bin 102930 -> 103005 bytes .../api/extension/CSXS/manifest.xml | 24 ++++----- .../api/extension/jsx/hostscript.jsx | 16 +++++- openpype/hosts/aftereffects/api/ws_stub.py | 8 ++- .../publish/help/validate_footage_items.xml | 14 +++++ .../plugins/publish/validate_footage_items.py | 49 ++++++++++++++++++ website/docs/admin_hosts_aftereffects.md | 8 +++ 7 files changed, 104 insertions(+), 15 deletions(-) create mode 100644 openpype/hosts/aftereffects/plugins/publish/help/validate_footage_items.xml create mode 100644 openpype/hosts/aftereffects/plugins/publish/validate_footage_items.py diff --git a/openpype/hosts/aftereffects/api/extension.zxp b/openpype/hosts/aftereffects/api/extension.zxp index 358e9740d3c2479b88dff08af47eba3d74be63ce..933dc7dc6cab34b6a1630b25d11a2167fa0aef7a 100644 GIT binary patch delta 10917 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 D9WP{T delta 10814 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 BS&;w$ diff --git a/openpype/hosts/aftereffects/api/extension/CSXS/manifest.xml b/openpype/hosts/aftereffects/api/extension/CSXS/manifest.xml index 0057758320..7329a9e723 100644 --- a/openpype/hosts/aftereffects/api/extension/CSXS/manifest.xml +++ b/openpype/hosts/aftereffects/api/extension/CSXS/manifest.xml @@ -1,5 +1,5 @@ - @@ -10,22 +10,22 @@ - + - + - - + + - + - - + + - + @@ -63,7 +63,7 @@ 550 400 --> - + ./icons/iconNormal.png @@ -71,9 +71,9 @@ ./icons/iconDisabled.png ./icons/iconDarkNormal.png ./icons/iconDarkRollover.png - + - \ No newline at end of file + diff --git a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx index bc443930df..c00844e637 100644 --- a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx +++ b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx @@ -215,6 +215,8 @@ function _getItem(item, comps, folders, footages){ * Refactor */ var item_type = ''; + var path = ''; + var containing_comps = []; if (item instanceof FolderItem){ item_type = 'folder'; if (!folders){ @@ -222,10 +224,18 @@ function _getItem(item, comps, folders, footages){ } } if (item instanceof FootageItem){ - item_type = 'footage'; if (!footages){ return "{}"; } + item_type = 'footage'; + if (item.file){ + path = item.file.fsName; + } + if (item.usedIn){ + for (j = 0; j < item.usedIn.length; ++j){ + containing_comps.push(item.usedIn[j].id); + } + } } if (item instanceof CompItem){ item_type = 'comp'; @@ -236,7 +246,9 @@ function _getItem(item, comps, folders, footages){ var item = {"name": item.name, "id": item.id, - "type": item_type}; + "type": item_type, + "path": path, + "containing_comps": containing_comps}; return JSON.stringify(item); } diff --git a/openpype/hosts/aftereffects/api/ws_stub.py b/openpype/hosts/aftereffects/api/ws_stub.py index f5b96fa63a..18f530e272 100644 --- a/openpype/hosts/aftereffects/api/ws_stub.py +++ b/openpype/hosts/aftereffects/api/ws_stub.py @@ -37,6 +37,9 @@ class AEItem(object): height = attr.ib(default=None) is_placeholder = attr.ib(default=False) uuid = attr.ib(default=False) + path = attr.ib(default=False) # path to FootageItem to validate + # list of composition Footage is in + containing_comps = attr.ib(factory=list) class AfterEffectsServerStub(): @@ -704,7 +707,10 @@ class AfterEffectsServerStub(): d.get("instance_id"), d.get("width"), d.get("height"), - d.get("is_placeholder")) + d.get("is_placeholder"), + d.get("uuid"), + d.get("path"), + d.get("containing_comps"),) ret.append(item) return ret diff --git a/openpype/hosts/aftereffects/plugins/publish/help/validate_footage_items.xml b/openpype/hosts/aftereffects/plugins/publish/help/validate_footage_items.xml new file mode 100644 index 0000000000..01c8966015 --- /dev/null +++ b/openpype/hosts/aftereffects/plugins/publish/help/validate_footage_items.xml @@ -0,0 +1,14 @@ + + + +Footage item missing + +## Footage item missing + + FootageItem `{name}` contains missing `{path}`. Render will not produce any frames and AE will stop react to any integration +### How to repair? + +Remove `{name}` or provide missing file. + + + diff --git a/openpype/hosts/aftereffects/plugins/publish/validate_footage_items.py b/openpype/hosts/aftereffects/plugins/publish/validate_footage_items.py new file mode 100644 index 0000000000..40a08a2c3f --- /dev/null +++ b/openpype/hosts/aftereffects/plugins/publish/validate_footage_items.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +"""Validate presence of footage items in composition +Requires: +""" +import os + +import pyblish.api + +from openpype.pipeline import ( + PublishXmlValidationError +) +from openpype.hosts.aftereffects.api import get_stub + + +class ValidateFootageItems(pyblish.api.InstancePlugin): + """ + Validates if FootageItems contained in composition exist. + + AE fails silently and doesn't render anything if footage item file is + missing. This will result in nonresponsiveness of AE UI as it expects + reaction from user, but it will not provide dialog. + This validator tries to check existence of the files. + It will not protect from missing frame in multiframes though + (as AE api doesn't provide this information and it cannot be told how many + frames should be there easily). Missing frame is replaced by placeholder. + """ + + order = pyblish.api.ValidatorOrder + label = "Validate Footage Items" + families = ["render.farm", "render.local", "render"] + hosts = ["aftereffects"] + optional = True + + def process(self, instance): + """Plugin entry point.""" + + comp_id = instance.data["comp_id"] + for footage_item in get_stub().get_items(comps=False, folders=False, + footages=True): + self.log.info(footage_item) + if comp_id not in footage_item.containing_comps: + continue + + path = footage_item.path + if path and not os.path.exists(path): + msg = f"File {path} not found." + formatting = {"name": footage_item.name, "path": path} + raise PublishXmlValidationError(self, msg, + formatting_data=formatting) diff --git a/website/docs/admin_hosts_aftereffects.md b/website/docs/admin_hosts_aftereffects.md index 974428fe06..72fdb32faf 100644 --- a/website/docs/admin_hosts_aftereffects.md +++ b/website/docs/admin_hosts_aftereffects.md @@ -18,6 +18,10 @@ Location: Settings > Project > AfterEffects ## Publish plugins +### Collect Review + +Enable/disable creation of auto instance of review. + ### Validate Scene Settings #### Skip Resolution Check for Tasks @@ -28,6 +32,10 @@ Set regex pattern(s) to look for in a Task name to skip resolution check against Set regex pattern(s) to look for in a Task name to skip `frameStart`, `frameEnd` check against values from DB. +### ValidateContainers + +By default this validator will look loaded items with lower version than latest. This validator is context wide so it must be disabled in Context button. + ### AfterEffects Submit to Deadline * `Use Published scene` - Set to True (green) when Deadline should take published scene as a source instead of uploaded local one. From 651177dedbfe75c03022c2b1f7930fdda5b93315 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Sep 2023 16:17:22 +0200 Subject: [PATCH 65/70] TVPaint: Fix tool callbacks (#5608) * don't wait for tools to show * use 'deque' instead of 'Queue' --- .../hosts/tvpaint/api/communication_server.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/tvpaint/api/communication_server.py b/openpype/hosts/tvpaint/api/communication_server.py index 6f76c25e0c..d67ef8f798 100644 --- a/openpype/hosts/tvpaint/api/communication_server.py +++ b/openpype/hosts/tvpaint/api/communication_server.py @@ -11,7 +11,7 @@ import filecmp import tempfile import threading import shutil -from queue import Queue + from contextlib import closing from aiohttp import web @@ -319,19 +319,19 @@ class QtTVPaintRpc(BaseTVPaintRpc): async def workfiles_tool(self): log.info("Triggering Workfile tool") item = MainThreadItem(self.tools_helper.show_workfiles) - self._execute_in_main_thread(item) + self._execute_in_main_thread(item, wait=False) return async def loader_tool(self): log.info("Triggering Loader tool") item = MainThreadItem(self.tools_helper.show_loader) - self._execute_in_main_thread(item) + self._execute_in_main_thread(item, wait=False) return async def publish_tool(self): log.info("Triggering Publish tool") item = MainThreadItem(self.tools_helper.show_publisher_tool) - self._execute_in_main_thread(item) + self._execute_in_main_thread(item, wait=False) return async def scene_inventory_tool(self): @@ -350,13 +350,13 @@ class QtTVPaintRpc(BaseTVPaintRpc): async def library_loader_tool(self): log.info("Triggering Library loader tool") item = MainThreadItem(self.tools_helper.show_library_loader) - self._execute_in_main_thread(item) + self._execute_in_main_thread(item, wait=False) return async def experimental_tools(self): log.info("Triggering Library loader tool") item = MainThreadItem(self.tools_helper.show_experimental_tools_dialog) - self._execute_in_main_thread(item) + self._execute_in_main_thread(item, wait=False) return async def _async_execute_in_main_thread(self, item, **kwargs): @@ -867,7 +867,7 @@ class QtCommunicator(BaseCommunicator): def __init__(self, qt_app): super().__init__() - self.callback_queue = Queue() + self.callback_queue = collections.deque() self.qt_app = qt_app def _create_routes(self): @@ -880,14 +880,14 @@ class QtCommunicator(BaseCommunicator): def execute_in_main_thread(self, main_thread_item, wait=True): """Add `MainThreadItem` to callback queue and wait for result.""" - self.callback_queue.put(main_thread_item) + self.callback_queue.append(main_thread_item) if wait: return main_thread_item.wait() return async def async_execute_in_main_thread(self, main_thread_item, wait=True): """Add `MainThreadItem` to callback queue and wait for result.""" - self.callback_queue.put(main_thread_item) + self.callback_queue.append(main_thread_item) if wait: return await main_thread_item.async_wait() @@ -904,9 +904,9 @@ class QtCommunicator(BaseCommunicator): self._exit() return None - if self.callback_queue.empty(): - return None - return self.callback_queue.get() + if self.callback_queue: + return self.callback_queue.popleft() + return None def _on_client_connect(self): super()._on_client_connect() From 2142d596038aa37d382f5dd5c5df947ec437f58b Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 12 Sep 2023 14:37:04 +0000 Subject: [PATCH 66/70] [Automated] Release --- CHANGELOG.md | 237 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 239 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4f9ff57ea..0d7620869b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,243 @@ # Changelog +## [3.16.6](https://github.com/ynput/OpenPype/tree/3.16.6) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.16.5...3.16.6) + +### **🆕 New features** + + +
+Workfiles tool: Refactor workfiles tool (for AYON) #5550 + +Refactored workfiles tool to new tool. Separated backend and frontend logic. Refactored logic is AYON-centric and is used only in AYON mode, so it does not affect OpenPype. + + +___ + +
+ + +
+AfterEffects: added validator for missing files in FootageItems #5590 + +Published composition in AE could contain multiple FootageItems as a layers. If FootageItem contains imported file and it doesn't exist, render triggered by Publish process will silently fail and no output is generated. This could cause failure later in the process with unclear reason. (In `ExtractReview`).This PR adds validation to protect from this. + + +___ + +
+ +### **🚀 Enhancements** + + +
+Maya: Yeti Cache Include viewport preview settings from source #5561 + +When publishing and loading yeti caches persist the display output and preview colors + settings to ensure consistency in the view + + +___ + +
+ + +
+Houdini: validate colorspace in review rop #5322 + +Adding a validator that checks if 'OCIO Colorspace' parameter on review rop was set to a valid value.It is a step towards managing colorspace in review ropvalid values are the ones in the dropdown menuthis validator also provides some helper actions This PR is related to #4836 and #4833 + + +___ + +
+ + +
+Colorspace: adding abstraction of publishing related functions #5497 + +The functionality of Colorspace has been abstracted for greater usability. + + +___ + +
+ + +
+Nuke: removing redundant workfile colorspace attributes #5580 + +Nuke root workfile colorspace data type knobs are long time configured automatically via config roles or the default values are also working well. Therefore there is no need for pipeline managed knobs. + + +___ + +
+ + +
+Ftrack: Less verbose logs for Ftrack integration in artist facing logs #5596 + +- Reduce artist-facing logs for component integration for Ftrack +- Avoid "Comment is not set" log in artist facing report for Kitsu and Ftrack +- Remove info log about `ffprobe` inspecting a file (changed to debug log) +- interesting to see however that it ffprobes the same jpeg twice - but maybe once for thumbnail? + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Fix rig validators for new out_SET and controls_SET names #5595 + +Fix usage of `out_SET` and `controls_SET` since #5310 because they can now be prefixed by the subset name. + + +___ + +
+ + +
+TrayPublisher: set default frame values to sequential data #5530 + +We are inheriting default frame handles and fps data either from project or setting them to 0. This is just for case a production will decide not to injest the sequential representations with asset based metadata. + + +___ + +
+ + +
+Publisher: Screenshot opacity value fix #5576 + +Fix opacity value. + + +___ + +
+ + +
+AfterEffects: fix imports of image sequences #5581 + +#4602 broke imports of image sequences. + + +___ + +
+ + +
+AYON: Fix representation context conversion #5591 + +Do not fix `"folder"` key in representation context until it is needed. + + +___ + +
+ + +
+ayon-nuke: default factory to lists #5594 + +Default factory were missing in settings schemas for complicated objects like lists and it was causing settings to be failing saving. + + +___ + +
+ + +
+Maya: Fix look assigner showing no asset if 'not found' representations are present #5597 + +Fix Maya Look assigner failing to show any content if it finds an invalid container for which it can't find the asset in the current project. (This can happen when e.g. loading something from a library project).There was logic already to avoid this but there was a bug where it used variable `_id` which did not exist and likely had to be `asset_id`.I've fixed that and improved the logged message a bit, e.g.: +``` +// Warning: openpype.hosts.maya.tools.mayalookassigner.commands : Id found on 22 nodes for which no asset is found database, skipping '641d78ec85c3c5b102e836b0' +``` +Example not found representation in Loader:The issue isn't necessarily related to NOT FOUND representations but in essence boils down to finding nodes with asset ids that do not exist in the current project which could very well just be local meshes in your scene.**Note:**I've excluded logging the nodes themselves because that tends to be a very long list of nodes. Only downside to removing that is that it's unclear which nodes are related to that `id`. If there are any ideas on how to still provide a concise informational message about that that'd be great so I could add it. Things I had considered: +- Report the containers, issue here is that it's about asset ids on nodes which don't HAVE to be in containers - it could be local geometry +- Report the namespaces, issue here is that it could be nodes without namespaces (plus potentially not about ALL nodes in a namespace) +- Report the short names of the nodes; it's shorter and readable but still likely a lot of nodes.@tokejepsen @LiborBatek any other ideas? + + +___ + +
+ + +
+Photoshop: fixed blank Flatten image #5600 + +Flatten image is simplified publishing approach where all visible layers are "flatten" and published together. This image could be used as a reference etc.This is implemented by auto creator which wasn't updated after first publish. This would result in missing newly created layers after `auto_image` instance was created. + + +___ + +
+ + +
+Blender: Remove Hardcoded Subset Name for Reviews #5603 + +Fixes hardcoded subset name for Reviews in Blender. + + +___ + +
+ + +
+TVPaint: Fix tool callbacks #5608 + +Do not wait for callback to finish. + + +___ + +
+ +### **🔀 Refactored code** + + +
+Chore: Remove unused variables and cleanup #5588 + +Removing some unused variables. In some cases the unused variables _seemed like they should've been used - maybe?_ so please **double check the code whether it doesn't hint to an already existing bug**.Also tweaked some other small bugs in code + tweaked logging levels. + + +___ + +
+ +### **Merged pull requests** + + +
+Chore: Loader log deprecation warning for 'fname' attribute #5587 + +Since https://github.com/ynput/OpenPype/pull/4602 the `fname` attribute on the `LoaderPlugin` should've been deprecated and set for removal over time. However, no deprecation warning was logged whatsoever and thus one usage appears to have sneaked in (fixed with this PR) and a new one tried to sneak in with a recent PR + + +___ + +
+ + + + ## [3.16.5](https://github.com/ynput/OpenPype/tree/3.16.5) diff --git a/openpype/version.py b/openpype/version.py index b6c56296bc..9d3938dc04 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.6-nightly.1" +__version__ = "3.16.6" diff --git a/pyproject.toml b/pyproject.toml index 68fbf19c91..f859e1aff4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.16.5" # OpenPype +version = "3.16.6" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 9143703a73931b6a4b0c63d0aa939870186c8c43 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 12 Sep 2023 14:38:07 +0000 Subject: [PATCH 67/70] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 7a39103859..eb8053f2b3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.16.6 - 3.16.6-nightly.1 - 3.16.5 - 3.16.5-nightly.5 @@ -134,7 +135,6 @@ body: - 3.14.9-nightly.5 - 3.14.9-nightly.4 - 3.14.9-nightly.3 - - 3.14.9-nightly.2 validations: required: true - type: dropdown From df96f085f2e21d4a3c8a2cb5e3d33001eb40b99d Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 13 Sep 2023 03:24:29 +0000 Subject: [PATCH 68/70] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 9d3938dc04..c4a87e7843 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.6" +__version__ = "3.16.7-nightly.1" From 5de7dc96dda39f51e0664bbd3e572639cf3d3130 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 13 Sep 2023 03:25:10 +0000 Subject: [PATCH 69/70] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index eb8053f2b3..35564c2bf0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.16.7-nightly.1 - 3.16.6 - 3.16.6-nightly.1 - 3.16.5 @@ -134,7 +135,6 @@ body: - 3.14.9 - 3.14.9-nightly.5 - 3.14.9-nightly.4 - - 3.14.9-nightly.3 validations: required: true - type: dropdown From 9369d4d931f8bd9f7e02e480099d64a4bb89f9b9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 13 Sep 2023 09:46:48 +0200 Subject: [PATCH 70/70] plugin does recreate menu items when reopened (#5610) --- .../tvpaint_plugin/plugin_code/library.cpp | 106 +++++++++--------- .../windows_x64/plugin/OpenPypePlugin.dll | Bin 5811200 -> 5811200 bytes .../windows_x86/plugin/OpenPypePlugin.dll | Bin 5571072 -> 5571072 bytes 3 files changed, 56 insertions(+), 50 deletions(-) diff --git a/openpype/hosts/tvpaint/tvpaint_plugin/plugin_code/library.cpp b/openpype/hosts/tvpaint/tvpaint_plugin/plugin_code/library.cpp index 88106bc770..ec45a45123 100644 --- a/openpype/hosts/tvpaint/tvpaint_plugin/plugin_code/library.cpp +++ b/openpype/hosts/tvpaint/tvpaint_plugin/plugin_code/library.cpp @@ -573,56 +573,6 @@ void FAR PASCAL PI_Close( PIFilter* iFilter ) } -/**************************************************************************************/ -// we have something to do ! - -int FAR PASCAL PI_Parameters( PIFilter* iFilter, char* iArg ) -{ - if( !iArg ) - { - - // If the requester is not open, we open it. - if( Data.mReq == 0) - { - // Create empty requester because menu items are defined with - // `define_menu` callback - DWORD req = TVOpenFilterReqEx( - iFilter, - 185, - 20, - NULL, - NULL, - PIRF_STANDARD_REQ | PIRF_COLLAPSABLE_REQ, - FILTERREQ_NO_TBAR - ); - if( req == 0 ) - { - TVWarning( iFilter, TXT_REQUESTER_ERROR ); - return 0; - } - - - Data.mReq = req; - // This is a very simple requester, so we create it's content right here instead - // of waiting for the PICBREQ_OPEN message... - // Not recommended for more complex requesters. (see the other examples) - - // Sets the title of the requester. - TVSetReqTitle( iFilter, Data.mReq, TXT_REQUESTER ); - // Request to listen to ticks - TVGrabTicks(iFilter, req, PITICKS_FLAG_ON); - } - else - { - // If it is already open, we just put it on front of all other requesters. - TVReqToFront( iFilter, Data.mReq ); - } - } - - return 1; -} - - int newMenuItemsProcess(PIFilter* iFilter) { // Menu items defined with `define_menu` should be propagated. @@ -702,6 +652,62 @@ int newMenuItemsProcess(PIFilter* iFilter) { return 1; } + +/**************************************************************************************/ +// we have something to do ! + +int FAR PASCAL PI_Parameters( PIFilter* iFilter, char* iArg ) +{ + if( !iArg ) + { + + // If the requester is not open, we open it. + if( Data.mReq == 0) + { + // Create empty requester because menu items are defined with + // `define_menu` callback + DWORD req = TVOpenFilterReqEx( + iFilter, + 185, + 20, + NULL, + NULL, + PIRF_STANDARD_REQ | PIRF_COLLAPSABLE_REQ, + FILTERREQ_NO_TBAR + ); + if( req == 0 ) + { + TVWarning( iFilter, TXT_REQUESTER_ERROR ); + return 0; + } + + Data.mReq = req; + + // This is a very simple requester, so we create it's content right here instead + // of waiting for the PICBREQ_OPEN message... + // Not recommended for more complex requesters. (see the other examples) + + // Sets the title of the requester. + TVSetReqTitle( iFilter, Data.mReq, TXT_REQUESTER ); + // Request to listen to ticks + TVGrabTicks(iFilter, req, PITICKS_FLAG_ON); + + if ( Data.firstParams == true ) { + Data.firstParams = false; + } else { + newMenuItemsProcess(iFilter); + } + } + else + { + // If it is already open, we just put it on front of all other requesters. + TVReqToFront( iFilter, Data.mReq ); + } + } + + return 1; +} + /**************************************************************************************/ // something happened that needs our attention. // Global variable where current button up data are stored diff --git a/openpype/hosts/tvpaint/tvpaint_plugin/plugin_files/windows_x64/plugin/OpenPypePlugin.dll b/openpype/hosts/tvpaint/tvpaint_plugin/plugin_files/windows_x64/plugin/OpenPypePlugin.dll index 7081778beee266e9b08d20ba7e48710dd7c4f20c..9c6e969e24601f3686477dfe6d7127561f2a3d9a 100644 GIT binary patch delta 102578 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 delta 102519 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 *Se&+ey$67@Jm_y**9-A6__l_Z!+$mA~tGn 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 diff --git a/openpype/hosts/tvpaint/tvpaint_plugin/plugin_files/windows_x86/plugin/OpenPypePlugin.dll b/openpype/hosts/tvpaint/tvpaint_plugin/plugin_files/windows_x86/plugin/OpenPypePlugin.dll index 0f2afec245f7f73ec3a8d0f72b5892653f5d51cd..b573476a21d7fd73ea1e4b4a53794377954fd845 100644 GIT binary patch delta 108350 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ú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∓^R(ytjX5wL>pP1 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