diff --git a/client/ayon_core/hosts/hiero/plugins/load/load_clip.py b/client/ayon_core/hosts/hiero/plugins/load/load_clip.py index d77a28872f..b8c51e7536 100644 --- a/client/ayon_core/hosts/hiero/plugins/load/load_clip.py +++ b/client/ayon_core/hosts/hiero/plugins/load/load_clip.py @@ -54,25 +54,36 @@ class LoadClip(phiero.SequenceLoader): plugin_name = cls.__name__ - plugin_settings = None # Look for plugin settings in host specific settings - if plugin_name in plugin_type_settings: - plugin_settings = plugin_type_settings[plugin_name] - + plugin_settings = plugin_type_settings.get(plugin_name) if not plugin_settings: return print(">>> We have preset for {}".format(plugin_name)) for option, value in plugin_settings.items(): + if option == "representations": + continue + + if option == "product_types": + # TODO remove the key conversion when loaders can filter by + # product types + # convert 'product_types' to 'families' + option = "families" + + elif option == "clip_name_template": + # TODO remove the formatting replacement + value = ( + value + .replace("{folder[name]}", "{asset}") + .replace("{product[name]}", "{subset}") + ) + if option == "enabled" and value is False: print(" - is disabled by preset") - elif option == "representations": - continue else: print(" - setting `{}`: `{}`".format(option, value)) setattr(cls, option, value) - def load(self, context, name, namespace, options): # add clip name template to options options.update({ diff --git a/client/ayon_core/hosts/max/api/action.py b/client/ayon_core/hosts/max/api/action.py new file mode 100644 index 0000000000..bed72bc493 --- /dev/null +++ b/client/ayon_core/hosts/max/api/action.py @@ -0,0 +1,42 @@ +from pymxs import runtime as rt + +import pyblish.api + +from ayon_core.pipeline.publish import get_errored_instances_from_context + + +class SelectInvalidAction(pyblish.api.Action): + """Select invalid objects in Blender when a publish plug-in failed.""" + label = "Select Invalid" + on = "failed" + icon = "search" + + def process(self, context, plugin): + errored_instances = get_errored_instances_from_context(context, + plugin=plugin) + + # Get the invalid nodes for the plug-ins + self.log.info("Finding invalid nodes...") + invalid = list() + for instance in errored_instances: + invalid_nodes = plugin.get_invalid(instance) + if invalid_nodes: + if isinstance(invalid_nodes, (list, tuple)): + invalid.extend(invalid_nodes) + else: + self.log.warning( + "Failed plug-in doesn't have any selectable objects." + ) + + if not invalid: + self.log.info("No invalid nodes found.") + return + invalid_names = [obj.name for obj in invalid if not isinstance(obj, tuple)] + if not invalid_names: + invalid_names = [obj.name for obj, _ in invalid] + invalid = [obj for obj, _ in invalid] + self.log.info( + "Selecting invalid objects: %s", ", ".join(invalid_names) + ) + + rt.Select(invalid) diff --git a/client/ayon_core/hosts/max/api/pipeline.py b/client/ayon_core/hosts/max/api/pipeline.py index 46c5aeec11..1486f7218d 100644 --- a/client/ayon_core/hosts/max/api/pipeline.py +++ b/client/ayon_core/hosts/max/api/pipeline.py @@ -245,3 +245,27 @@ def get_previous_loaded_object(container: str): if str(obj) in sel_list: node_list.append(obj) return node_list + + +def remove_container_data(container_node: str): + """Function to remove container data after updating, switching or deleting it. + + Args: + container_node (str): container node + """ + if container_node.modifiers[0].name == "OP Data": + all_set_members_names = [ + member.node for member + in container_node.modifiers[0].openPypeData.all_handles] + # clean up the children of alembic dummy objects + for current_set_member in all_set_members_names: + shape_list = [members for members in current_set_member.Children + if rt.ClassOf(members) == rt.AlembicObject + or rt.isValidNode(members)] + if shape_list: # noqa + rt.Delete(shape_list) + rt.Delete(current_set_member) + rt.deleteModifier(container_node, container_node.modifiers[0]) + + rt.Delete(container_node) + rt.redrawViews() diff --git a/client/ayon_core/hosts/max/plugins/load/load_camera_fbx.py b/client/ayon_core/hosts/max/plugins/load/load_camera_fbx.py index 34b120c179..8387d7a837 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_camera_fbx.py +++ b/client/ayon_core/hosts/max/plugins/load/load_camera_fbx.py @@ -1,6 +1,6 @@ import os -from ayon_core.hosts.max.api import lib, maintained_selection +from ayon_core.hosts.max.api import lib from ayon_core.hosts.max.api.lib import ( unique_namespace, get_namespace, @@ -9,7 +9,8 @@ from ayon_core.hosts.max.api.lib import ( from ayon_core.hosts.max.api.pipeline import ( containerise, get_previous_loaded_object, - update_custom_attribute_data + update_custom_attribute_data, + remove_container_data ) from ayon_core.pipeline import get_representation_path, load @@ -96,4 +97,4 @@ class FbxLoader(load.LoaderPlugin): from pymxs import runtime as rt node = rt.GetNodeByName(container["instance_node"]) - rt.Delete(node) + remove_container_data(node) diff --git a/client/ayon_core/hosts/max/plugins/load/load_max_scene.py b/client/ayon_core/hosts/max/plugins/load/load_max_scene.py index 7267d7a59e..ead77cd2f2 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_max_scene.py +++ b/client/ayon_core/hosts/max/plugins/load/load_max_scene.py @@ -8,7 +8,8 @@ from ayon_core.hosts.max.api.lib import ( ) from ayon_core.hosts.max.api.pipeline import ( containerise, get_previous_loaded_object, - update_custom_attribute_data + update_custom_attribute_data, + remove_container_data ) from ayon_core.pipeline import get_representation_path, load @@ -93,6 +94,5 @@ class MaxSceneLoader(load.LoaderPlugin): def remove(self, container): from pymxs import runtime as rt - node = rt.GetNodeByName(container["instance_node"]) - rt.Delete(node) + remove_container_data(node) diff --git a/client/ayon_core/hosts/max/plugins/load/load_model.py b/client/ayon_core/hosts/max/plugins/load/load_model.py index 796e1b80ad..cf35e107c2 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_model.py +++ b/client/ayon_core/hosts/max/plugins/load/load_model.py @@ -2,7 +2,8 @@ import os from ayon_core.pipeline import load, get_representation_path from ayon_core.hosts.max.api.pipeline import ( containerise, - get_previous_loaded_object + get_previous_loaded_object, + remove_container_data ) from ayon_core.hosts.max.api import lib from ayon_core.hosts.max.api.lib import ( @@ -97,9 +98,9 @@ class ModelAbcLoader(load.LoaderPlugin): def remove(self, container): from pymxs import runtime as rt - node = rt.GetNodeByName(container["instance_node"]) - rt.Delete(node) + remove_container_data(node) + @staticmethod def get_container_children(parent, type_name): diff --git a/client/ayon_core/hosts/max/plugins/load/load_model_fbx.py b/client/ayon_core/hosts/max/plugins/load/load_model_fbx.py index 827cf63b39..c0bacca33a 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_model_fbx.py +++ b/client/ayon_core/hosts/max/plugins/load/load_model_fbx.py @@ -2,7 +2,8 @@ import os from ayon_core.pipeline import load, get_representation_path from ayon_core.hosts.max.api.pipeline import ( containerise, get_previous_loaded_object, - update_custom_attribute_data + update_custom_attribute_data, + remove_container_data ) from ayon_core.hosts.max.api import lib from ayon_core.hosts.max.api.lib import ( @@ -92,6 +93,5 @@ class FbxModelLoader(load.LoaderPlugin): def remove(self, container): from pymxs import runtime as rt - node = rt.GetNodeByName(container["instance_node"]) - rt.Delete(node) + remove_container_data(node) diff --git a/client/ayon_core/hosts/max/plugins/load/load_model_obj.py b/client/ayon_core/hosts/max/plugins/load/load_model_obj.py index 22d3d4b58a..1023b67f0c 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_model_obj.py +++ b/client/ayon_core/hosts/max/plugins/load/load_model_obj.py @@ -11,7 +11,8 @@ from ayon_core.hosts.max.api.lib import maintained_selection from ayon_core.hosts.max.api.pipeline import ( containerise, get_previous_loaded_object, - update_custom_attribute_data + update_custom_attribute_data, + remove_container_data ) from ayon_core.pipeline import get_representation_path, load @@ -84,6 +85,5 @@ class ObjLoader(load.LoaderPlugin): def remove(self, container): from pymxs import runtime as rt - node = rt.GetNodeByName(container["instance_node"]) - rt.Delete(node) + remove_container_data(node) diff --git a/client/ayon_core/hosts/max/plugins/load/load_model_usd.py b/client/ayon_core/hosts/max/plugins/load/load_model_usd.py index 8d42219217..0ec6e5e8e7 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_model_usd.py +++ b/client/ayon_core/hosts/max/plugins/load/load_model_usd.py @@ -13,7 +13,8 @@ from ayon_core.hosts.max.api.lib import maintained_selection from ayon_core.hosts.max.api.pipeline import ( containerise, get_previous_loaded_object, - update_custom_attribute_data + update_custom_attribute_data, + remove_container_data ) from ayon_core.pipeline import get_representation_path, load @@ -113,5 +114,6 @@ class ModelUSDLoader(load.LoaderPlugin): self.update(container, representation) def remove(self, container): + from pymxs import runtime as rt node = rt.GetNodeByName(container["instance_node"]) - rt.Delete(node) + remove_container_data(node) diff --git a/client/ayon_core/hosts/max/plugins/load/load_pointcache.py b/client/ayon_core/hosts/max/plugins/load/load_pointcache.py index a92fa66757..e9cde4c654 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_pointcache.py +++ b/client/ayon_core/hosts/max/plugins/load/load_pointcache.py @@ -10,7 +10,8 @@ from ayon_core.hosts.max.api import lib, maintained_selection from ayon_core.hosts.max.api.lib import unique_namespace from ayon_core.hosts.max.api.pipeline import ( containerise, - get_previous_loaded_object + get_previous_loaded_object, + remove_container_data ) @@ -103,9 +104,9 @@ class AbcLoader(load.LoaderPlugin): def remove(self, container): from pymxs import runtime as rt - node = rt.GetNodeByName(container["instance_node"]) - rt.Delete(node) + remove_container_data(node) + @staticmethod def get_container_children(parent, type_name): diff --git a/client/ayon_core/hosts/max/plugins/load/load_pointcache_ornatrix.py b/client/ayon_core/hosts/max/plugins/load/load_pointcache_ornatrix.py index 27b2e271d2..338cbfafb9 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_pointcache_ornatrix.py +++ b/client/ayon_core/hosts/max/plugins/load/load_pointcache_ornatrix.py @@ -4,7 +4,8 @@ from ayon_core.pipeline.load import LoadError from ayon_core.hosts.max.api.pipeline import ( containerise, get_previous_loaded_object, - update_custom_attribute_data + update_custom_attribute_data, + remove_container_data ) from ayon_core.hosts.max.api.lib import ( @@ -104,5 +105,6 @@ class OxAbcLoader(load.LoaderPlugin): self.update(container, representation) def remove(self, container): + from pymxs import runtime as rt node = rt.GetNodeByName(container["instance_node"]) - rt.Delete(node) + remove_container_data(node) diff --git a/client/ayon_core/hosts/max/plugins/load/load_pointcloud.py b/client/ayon_core/hosts/max/plugins/load/load_pointcloud.py index 45e3da5621..7f4fba50b3 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_pointcloud.py +++ b/client/ayon_core/hosts/max/plugins/load/load_pointcloud.py @@ -8,7 +8,8 @@ from ayon_core.hosts.max.api.lib import ( from ayon_core.hosts.max.api.pipeline import ( containerise, get_previous_loaded_object, - update_custom_attribute_data + update_custom_attribute_data, + remove_container_data ) from ayon_core.pipeline import get_representation_path, load @@ -63,6 +64,5 @@ class PointCloudLoader(load.LoaderPlugin): def remove(self, container): """remove the container""" from pymxs import runtime as rt - node = rt.GetNodeByName(container["instance_node"]) - rt.Delete(node) + remove_container_data(node) diff --git a/client/ayon_core/hosts/max/plugins/load/load_redshift_proxy.py b/client/ayon_core/hosts/max/plugins/load/load_redshift_proxy.py index 3f73210c24..5f2f5ec1ad 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_redshift_proxy.py +++ b/client/ayon_core/hosts/max/plugins/load/load_redshift_proxy.py @@ -9,7 +9,8 @@ from ayon_core.pipeline.load import LoadError from ayon_core.hosts.max.api.pipeline import ( containerise, update_custom_attribute_data, - get_previous_loaded_object + get_previous_loaded_object, + remove_container_data ) from ayon_core.hosts.max.api import lib from ayon_core.hosts.max.api.lib import ( @@ -72,6 +73,5 @@ class RedshiftProxyLoader(load.LoaderPlugin): def remove(self, container): from pymxs import runtime as rt - - node = rt.getNodeByName(container["instance_node"]) - rt.delete(node) + node = rt.GetNodeByName(container["instance_node"]) + remove_container_data(node) diff --git a/client/ayon_core/hosts/max/plugins/load/load_tycache.py b/client/ayon_core/hosts/max/plugins/load/load_tycache.py index 48fb5c447a..7ae1aea72c 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_tycache.py +++ b/client/ayon_core/hosts/max/plugins/load/load_tycache.py @@ -7,7 +7,8 @@ from ayon_core.hosts.max.api.lib import ( from ayon_core.hosts.max.api.pipeline import ( containerise, get_previous_loaded_object, - update_custom_attribute_data + update_custom_attribute_data, + remove_container_data ) from ayon_core.pipeline import get_representation_path, load @@ -59,6 +60,5 @@ class TyCacheLoader(load.LoaderPlugin): def remove(self, container): """remove the container""" from pymxs import runtime as rt - node = rt.GetNodeByName(container["instance_node"]) - rt.Delete(node) + remove_container_data(node) diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_camera_attributes.py b/client/ayon_core/hosts/max/plugins/publish/validate_camera_attributes.py new file mode 100644 index 0000000000..9398cba2b7 --- /dev/null +++ b/client/ayon_core/hosts/max/plugins/publish/validate_camera_attributes.py @@ -0,0 +1,88 @@ +import pyblish.api +from pymxs import runtime as rt + +from ayon_core.pipeline.publish import ( + RepairAction, + OptionalPyblishPluginMixin, + PublishValidationError +) +from ayon_core.hosts.max.api.action import SelectInvalidAction + + +class ValidateCameraAttributes(OptionalPyblishPluginMixin, + pyblish.api.InstancePlugin): + """Validates Camera has no invalid attribute properties + or values.(For 3dsMax Cameras only) + + """ + + order = pyblish.api.ValidatorOrder + families = ['camera'] + hosts = ['max'] + label = 'Validate Camera Attributes' + actions = [SelectInvalidAction, RepairAction] + optional = True + + DEFAULTS = ["fov", "nearrange", "farrange", + "nearclip", "farclip"] + CAM_TYPE = ["Freecamera", "Targetcamera", + "Physical"] + + @classmethod + def get_invalid(cls, instance): + invalid = [] + if rt.units.DisplayType != rt.Name("Generic"): + cls.log.warning( + "Generic Type is not used as a scene unit\n\n" + "sure you tweak the settings with your own values\n\n" + "before validation.") + cameras = instance.data["members"] + project_settings = instance.context.data["project_settings"].get("max") + cam_attr_settings = ( + project_settings["publish"]["ValidateCameraAttributes"] + ) + for camera in cameras: + if str(rt.ClassOf(camera)) not in cls.CAM_TYPE: + cls.log.debug( + "Skipping camera created from external plugin..") + continue + for attr in cls.DEFAULTS: + default_value = cam_attr_settings.get(attr) + if default_value == float(0): + cls.log.debug( + f"the value of {attr} in setting set to" + " zero. Skipping the check.") + continue + if round(rt.getProperty(camera, attr), 1) != default_value: + cls.log.error( + f"Invalid attribute value for {camera.name}:{attr} " + f"(should be: {default_value}))") + invalid.append(camera) + + return invalid + + def process(self, instance): + if not self.is_active(instance.data): + self.log.debug("Skipping Validate Camera Attributes.") + return + invalid = self.get_invalid(instance) + + if invalid: + raise PublishValidationError( + "Invalid camera attributes found. See log.") + + @classmethod + def repair(cls, instance): + invalid_cameras = cls.get_invalid(instance) + project_settings = instance.context.data["project_settings"].get("max") + cam_attr_settings = ( + project_settings["publish"]["ValidateCameraAttributes"] + ) + for camera in invalid_cameras: + for attr in cls.DEFAULTS: + expected_value = cam_attr_settings.get(attr) + if expected_value == float(0): + cls.log.debug( + f"the value of {attr} in setting set to zero.") + continue + rt.setProperty(camera, attr, expected_value) diff --git a/client/ayon_core/hosts/nuke/api/lib.py b/client/ayon_core/hosts/nuke/api/lib.py index a2ec8fdb98..c11a9d9be0 100644 --- a/client/ayon_core/hosts/nuke/api/lib.py +++ b/client/ayon_core/hosts/nuke/api/lib.py @@ -720,17 +720,17 @@ def get_created_node_imageio_setting_legacy(nodeclass, creator, subset): "`{}`: Missing mandatory kwargs `host`, `cls`".format(__file__)) imageio_nodes = get_nuke_imageio_settings()["nodes"] - required_nodes = imageio_nodes["requiredNodes"] + required_nodes = imageio_nodes["required_nodes"] # HACK: for backward compatibility this needs to be optional - override_nodes = imageio_nodes.get("overrideNodes", []) + override_nodes = imageio_nodes.get("override_nodes", []) imageio_node = None for node in required_nodes: log.info(node) if ( - nodeclass in node["nukeNodeClass"] - and creator in node["plugins"] + nodeclass in node["nuke_node_class"] + and creator in node["plugins"] ): imageio_node = node break @@ -741,10 +741,10 @@ def get_created_node_imageio_setting_legacy(nodeclass, creator, subset): override_imageio_node = None for onode in override_nodes: log.info(onode) - if nodeclass not in node["nukeNodeClass"]: + if nodeclass not in onode["nuke_node_class"]: continue - if creator not in node["plugins"]: + if creator not in onode["plugins"]: continue if ( @@ -766,26 +766,33 @@ def get_created_node_imageio_setting_legacy(nodeclass, creator, subset): knob_names = [k["name"] for k in imageio_node["knobs"]] for oknob in override_imageio_node["knobs"]: + oknob_name = oknob["name"] + oknob_type = oknob["type"] + oknob_value = oknob[oknob_type] for knob in imageio_node["knobs"]: - # override matching knob name - if oknob["name"] == knob["name"]: - log.debug( - "_ overriding knob: `{}` > `{}`".format( - knob, oknob - )) - if not oknob["value"]: - # remove original knob if no value found in oknob - imageio_node["knobs"].remove(knob) - else: - # override knob value with oknob's - knob["value"] = oknob["value"] - + knob_name = knob["name"] # add missing knobs into imageio_node - if oknob["name"] not in knob_names: + if oknob_name not in knob_names: log.debug( "_ adding knob: `{}`".format(oknob)) imageio_node["knobs"].append(oknob) - knob_names.append(oknob["name"]) + knob_names.append(oknob_name) + continue + + # override matching knob name + if oknob_name != knob_name: + continue + + knob_type = knob["type"] + log.debug( + "_ overriding knob: `{}` > `{}`".format(knob, oknob) + ) + if not oknob_value: + # remove original knob if no value found in oknob + imageio_node["knobs"].remove(knob) + else: + # override knob value with oknob's + knob[knob_type] = oknob_value log.info("ImageIO node: {}".format(imageio_node)) return imageio_node @@ -795,14 +802,14 @@ def get_imageio_node_setting(node_class, plugin_name, subset): ''' Get preset data for dataflow (fileType, compression, bitDepth) ''' imageio_nodes = get_nuke_imageio_settings()["nodes"] - required_nodes = imageio_nodes["requiredNodes"] + required_nodes = imageio_nodes["required_nodes"] imageio_node = None for node in required_nodes: log.info(node) if ( - node_class in node["nukeNodeClass"] - and plugin_name in node["plugins"] + node_class in node["nuke_node_class"] + and plugin_name in node["plugins"] ): imageio_node = node break @@ -830,14 +837,14 @@ def get_imageio_node_override_setting( ''' Get imageio node overrides from settings ''' imageio_nodes = get_nuke_imageio_settings()["nodes"] - override_nodes = imageio_nodes["overrideNodes"] + override_nodes = imageio_nodes["override_nodes"] # find matching override node override_imageio_node = None for onode in override_nodes: log.debug("__ onode: {}".format(onode)) log.debug("__ subset: {}".format(subset)) - if node_class not in onode["nukeNodeClass"]: + if node_class not in onode["nuke_node_class"]: continue if plugin_name not in onode["plugins"]: @@ -862,26 +869,31 @@ def get_imageio_node_override_setting( knob_names = [k["name"] for k in knobs_settings] for oknob in override_imageio_node["knobs"]: + oknob_name = oknob["name"] + oknob_type = oknob["type"] + oknob_value = oknob[oknob_type] for knob in knobs_settings: - # override matching knob name - if oknob["name"] == knob["name"]: - log.debug( - "_ overriding knob: `{}` > `{}`".format( - knob, oknob - )) - if not oknob["value"]: - # remove original knob if no value found in oknob - knobs_settings.remove(knob) - else: - # override knob value with oknob's - knob["value"] = oknob["value"] - # add missing knobs into imageio_node - if oknob["name"] not in knob_names: - log.debug( - "_ adding knob: `{}`".format(oknob)) + if oknob_name not in knob_names: + log.debug("_ adding knob: `{}`".format(oknob)) knobs_settings.append(oknob) - knob_names.append(oknob["name"]) + knob_names.append(oknob_name) + continue + + if oknob_name != knob["name"]: + continue + + knob_type = knob["type"] + # override matching knob name + log.debug( + "_ overriding knob: `{}` > `{}`".format(knob, oknob) + ) + if not oknob_value: + # remove original knob if no value found in oknob + knobs_settings.remove(knob) + else: + # override knob value with oknob's + knob[knob_type] = oknob_value return knobs_settings @@ -890,7 +902,7 @@ def get_imageio_input_colorspace(filename): ''' Get input file colorspace based on regex in settings. ''' imageio_regex_inputs = ( - get_nuke_imageio_settings()["regexInputs"]["inputs"]) + get_nuke_imageio_settings()["regex_inputs"]["inputs"]) preset_clrsp = None for regexInput in imageio_regex_inputs: @@ -1177,8 +1189,9 @@ def create_prenodes( ): last_node = None for_dependency = {} - for name, node in nodes_setting.items(): + for node in nodes_setting: # get attributes + name = node["name"] nodeclass = node["nodeclass"] knobs = node["knobs"] @@ -1240,8 +1253,8 @@ def create_write_node( name (str): name of node data (dict): creator write instance data input (node)[optional]: selected node to connect to - prenodes (dict)[optional]: - nodes to be created before write with dependency + prenodes (Optional[list[dict]]): nodes to be created before write + with dependency review (bool)[optional]: adding review knob farm (bool)[optional]: rendering workflow target kwargs (dict)[optional]: additional key arguments for formatting @@ -1270,7 +1283,7 @@ def create_write_node( Return: node (obj): group node with avalon data as Knobs ''' - prenodes = prenodes or {} + prenodes = prenodes or [] # filtering variables plugin_name = data["creator"] @@ -1285,7 +1298,8 @@ def create_write_node( for knob in imageio_writes["knobs"]: if knob["name"] == "file_type": - ext = knob["value"] + knot_type = knob["type"] + ext = knob[knot_type] data.update({ "imageio_writes": imageio_writes, @@ -1400,12 +1414,17 @@ def create_write_node( # set tile color tile_color = next( iter( - k["value"] for k in imageio_writes["knobs"] + k[k["type"]] for k in imageio_writes["knobs"] if "tile_color" in k["name"] ), [255, 0, 0, 255] ) + new_tile_color = [] + for c in tile_color: + if isinstance(c, float): + c = int(c * 255) + new_tile_color.append(c) GN["tile_color"].setValue( - color_gui_to_int(tile_color)) + color_gui_to_int(new_tile_color)) return GN @@ -1701,42 +1720,32 @@ def set_node_knobs_from_settings(node, knob_settings, **kwargs): """ for knob in knob_settings: log.debug("__ knob: {}".format(pformat(knob))) - knob_type = knob["type"] knob_name = knob["name"] - if knob_name not in node.knobs(): continue + knob_type = knob["type"] + knob_value = knob[knob_type] if knob_type == "expression": - knob_expression = knob["expression"] - node[knob_name].setExpression( - knob_expression - ) + node[knob_name].setExpression(knob_value) continue # first deal with formattable knob settings if knob_type == "formatable": - template = knob["template"] - to_type = knob["to_type"] + template = knob_value["template"] + to_type = knob_value["to_type"] try: - _knob_value = template.format( - **kwargs - ) + knob_value = template.format(**kwargs) except KeyError as msg: raise KeyError( "Not able to format expression: {}".format(msg)) # convert value to correct type if to_type == "2d_vector": - knob_value = _knob_value.split(";").split(",") - else: - knob_value = _knob_value + knob_value = knob_value.split(";").split(",") knob_type = to_type - else: - knob_value = knob["value"] - if not knob_value: continue @@ -1747,29 +1756,46 @@ def set_node_knobs_from_settings(node, knob_settings, **kwargs): def convert_knob_value_to_correct_type(knob_type, knob_value): - # first convert string types to string - # just to ditch unicode - if isinstance(knob_value, six.text_type): - knob_value = str(knob_value) + # Convert 'text' to string to avoid unicode + if knob_type == "text": + return str(knob_value) - # set correctly knob types - if knob_type == "bool": - knob_value = bool(knob_value) - elif knob_type == "decimal_number": - knob_value = float(knob_value) - elif knob_type == "number": - knob_value = int(knob_value) - elif knob_type == "text": - knob_value = knob_value - elif knob_type == "color_gui": - knob_value = color_gui_to_int(knob_value) - elif knob_type in ["2d_vector", "3d_vector", "color", "box"]: - knob_value = [float(val_) for val_ in knob_value] + if knob_type == "boolean": + return bool(knob_value) + + if knob_type == "decimal_number": + return float(knob_value) + + if knob_type == "number": + return int(knob_value) + + if knob_type == "color_gui": + new_color = [] + for value in knob_value: + if isinstance(value, float): + value = int(value * 255) + new_color.append(value) + return color_gui_to_int(new_color) + + if knob_type == "box": + return [ + knob_value["x"], knob_value["y"], + knob_value["r"], knob_value["t"] + ] + + if knob_type == "vector_2d": + return [knob_value["x"], knob_value["y"]] + + if knob_type == "vector_3d": + return [knob_value["x"], knob_value["y"], knob_value["z"]] return knob_value def color_gui_to_int(color_gui): + # Append alpha channel if not present + if len(color_gui) == 3: + color_gui = list(color_gui) + [255] hex_value = ( "0x{0:0>2x}{1:0>2x}{2:0>2x}{3:0>2x}").format(*color_gui) return int(hex_value, 16) @@ -2016,41 +2042,21 @@ class WorkfileSettings(object): host_name="nuke" ) - workfile_settings = imageio_host["workfile"] viewer_process_settings = imageio_host["viewer"]["viewerProcess"] + workfile_settings = imageio_host["workfile"] + color_management = workfile_settings["color_management"] + native_ocio_config = workfile_settings["native_ocio_config"] if not config_data: - # TODO: backward compatibility for old projects - remove later - # perhaps old project overrides is having it set to older version - # with use of `customOCIOConfigPath` - resolved_path = None - if workfile_settings.get("customOCIOConfigPath"): - unresolved_path = workfile_settings["customOCIOConfigPath"] - ocio_paths = unresolved_path[platform.system().lower()] + # no ocio config found and no custom path used + if self._root_node["colorManagement"].value() \ + not in color_management: + self._root_node["colorManagement"].setValue(color_management) - for ocio_p in ocio_paths: - resolved_path = str(ocio_p).format(**os.environ) - if not os.path.exists(resolved_path): - continue - - if resolved_path: - # set values to root - self._root_node["colorManagement"].setValue("OCIO") - self._root_node["OCIO_config"].setValue("custom") - self._root_node["customOCIOConfigPath"].setValue( - resolved_path) - else: - # no ocio config found and no custom path used - if self._root_node["colorManagement"].value() \ - not in str(workfile_settings["colorManagement"]): - self._root_node["colorManagement"].setValue( - str(workfile_settings["colorManagement"])) - - # second set ocio version - if self._root_node["OCIO_config"].value() \ - not in str(workfile_settings["OCIO_config"]): - self._root_node["OCIO_config"].setValue( - str(workfile_settings["OCIO_config"])) + # second set ocio version + if self._root_node["OCIO_config"].value() \ + not in native_ocio_config: + self._root_node["OCIO_config"].setValue(native_ocio_config) else: # OCIO config path is defined from prelaunch hook @@ -2063,22 +2069,17 @@ class WorkfileSettings(object): residual_path )) - # we dont need the key anymore - workfile_settings.pop("customOCIOConfigPath", None) - workfile_settings.pop("colorManagement", None) - workfile_settings.pop("OCIO_config", None) - # get monitor lut from settings respecting Nuke version differences - monitor_lut = workfile_settings.pop("monitorLut", None) + monitor_lut = workfile_settings["thumbnail_space"] monitor_lut_data = self._get_monitor_settings( - viewer_process_settings, monitor_lut) - - # set monitor related knobs luts (MonitorOut, Thumbnails) - for knob, value_ in monitor_lut_data.items(): - workfile_settings[knob] = value_ + viewer_process_settings, monitor_lut + ) + monitor_lut_data["workingSpaceLUT"] = ( + workfile_settings["working_space"] + ) # then set the rest - for knob, value_ in workfile_settings.items(): + for knob, value_ in monitor_lut_data.items(): # skip unfilled ocio config path # it will be dict in value if isinstance(value_, dict): @@ -2360,25 +2361,8 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. if not write_node: return - try: - # write all knobs to node - for knob in nuke_imageio_writes["knobs"]: - value = knob["value"] - if isinstance(value, six.text_type): - value = str(value) - if str(value).startswith("0x"): - value = int(value, 16) - - log.debug("knob: {}| value: {}".format( - knob["name"], value - )) - write_node[knob["name"]].setValue(value) - except TypeError: - log.warning( - "Legacy workflow didn't work, switching to current") - - set_node_knobs_from_settings( - write_node, nuke_imageio_writes["knobs"]) + set_node_knobs_from_settings( + write_node, nuke_imageio_writes["knobs"]) def set_reads_colorspace(self, read_clrs_inputs): """ Setting colorspace to Read nodes @@ -2456,7 +2440,7 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. log.error(_error) log.info("Setting colorspace to read nodes...") - read_clrs_inputs = nuke_colorspace["regexInputs"].get("inputs", []) + read_clrs_inputs = nuke_colorspace["regex_inputs"].get("inputs", []) if read_clrs_inputs: self.set_reads_colorspace(read_clrs_inputs) diff --git a/client/ayon_core/hosts/nuke/api/plugin.py b/client/ayon_core/hosts/nuke/api/plugin.py index 59042acee1..bf338a3010 100644 --- a/client/ayon_core/hosts/nuke/api/plugin.py +++ b/client/ayon_core/hosts/nuke/api/plugin.py @@ -396,17 +396,25 @@ class NukeWriteCreator(NukeCreator): # plugin settings plugin_settings = self.get_creator_settings(project_settings) - + temp_rendering_path_template = ( + plugin_settings.get("temp_rendering_path_template") + or self.temp_rendering_path_template + ) + # TODO remove template key replacements + temp_rendering_path_template = ( + temp_rendering_path_template + .replace("{product[name]}", "{subset}") + .replace("{product[type]}", "{family}") + .replace("{task[name]}", "{task}") + .replace("{folder[name]}", "{asset}") + ) # individual attributes self.instance_attributes = plugin_settings.get( "instance_attributes") or self.instance_attributes self.prenodes = plugin_settings["prenodes"] self.default_variants = plugin_settings.get( "default_variants") or self.default_variants - self.temp_rendering_path_template = ( - plugin_settings.get("temp_rendering_path_template") - or self.temp_rendering_path_template - ) + self.temp_rendering_path_template = temp_rendering_path_template class OpenPypeCreator(LegacyCreator): @@ -1061,7 +1069,7 @@ class AbstractWriteRender(OpenPypeCreator): icon = "sign-out" defaults = ["Main", "Mask"] knobs = [] - prenodes = {} + prenodes = [] def __init__(self, *args, **kwargs): super(AbstractWriteRender, self).__init__(*args, **kwargs) @@ -1167,7 +1175,7 @@ class AbstractWriteRender(OpenPypeCreator): bool: True if legacy """ imageio_nodes = get_nuke_imageio_settings()["nodes"] - node = imageio_nodes["requiredNodes"][0] + node = imageio_nodes["required_nodes"][0] if "type" not in node["knobs"][0]: # if type is not yet in project anatomy return True diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_clip.py b/client/ayon_core/hosts/nuke/plugins/load/load_clip.py index 8bce2eac6e..31b523fbc8 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_clip.py +++ b/client/ayon_core/hosts/nuke/plugins/load/load_clip.py @@ -53,7 +53,7 @@ class LoadClip(plugin.NukeLoader): color = "white" # Loaded from settings - _representations = [] + representations_include = [] script_start = int(nuke.root()["first_frame"].value()) @@ -82,7 +82,7 @@ class LoadClip(plugin.NukeLoader): @classmethod def get_representations(cls): - return cls._representations or cls.representations + return cls.representations_include or cls.representations def load(self, context, name, namespace, options): """Load asset via database @@ -457,7 +457,7 @@ class LoadClip(plugin.NukeLoader): colorspace = repre_data.get("colorspace") colorspace = colorspace or version_data.get("colorspace") - # colorspace from `project_settings/nuke/imageio/regexInputs` + # colorspace from `project_settings/nuke/imageio/regex_inputs` iio_colorspace = get_imageio_input_colorspace(path) # Set colorspace defined in version data diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_image.py b/client/ayon_core/hosts/nuke/plugins/load/load_image.py index b9f47bddc9..e9435ec10a 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_image.py +++ b/client/ayon_core/hosts/nuke/plugins/load/load_image.py @@ -47,7 +47,7 @@ class LoadImage(load.LoaderPlugin): color = "white" # Loaded from settings - _representations = [] + representations_include = [] node_name_template = "{class_name}_{ext}" @@ -64,7 +64,7 @@ class LoadImage(load.LoaderPlugin): @classmethod def get_representations(cls): - return cls._representations or cls.representations + return cls.representations_include or cls.representations def load(self, context, name, namespace, options): self.log.info("__ options: `{}`".format(options)) diff --git a/client/ayon_core/hosts/nuke/plugins/publish/collect_nuke_instance_data.py b/client/ayon_core/hosts/nuke/plugins/publish/collect_nuke_instance_data.py index 449a1cc935..75380bf409 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/collect_nuke_instance_data.py +++ b/client/ayon_core/hosts/nuke/plugins/publish/collect_nuke_instance_data.py @@ -12,7 +12,7 @@ class CollectInstanceData(pyblish.api.InstancePlugin): hosts = ["nuke", "nukeassist"] # presets - sync_workfile_version_on_families = [] + sync_workfile_version_on_product_types = [] def process(self, instance): family = instance.data["family"] @@ -25,7 +25,7 @@ class CollectInstanceData(pyblish.api.InstancePlugin): pixel_aspect = format_.pixelAspect() # sync workfile version - if family in self.sync_workfile_version_on_families: + if family in self.sync_workfile_version_on_product_types: self.log.debug( "Syncing version with workfile for '{}'".format( family diff --git a/client/ayon_core/hosts/nuke/plugins/publish/extract_review_intermediates.py b/client/ayon_core/hosts/nuke/plugins/publish/extract_review_intermediates.py index a00c1c593f..1f4410d347 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/extract_review_intermediates.py +++ b/client/ayon_core/hosts/nuke/plugins/publish/extract_review_intermediates.py @@ -50,6 +50,7 @@ class ExtractReviewIntermediates(publish.Extractor): cls.outputs = current_setting["outputs"] def process(self, instance): + # TODO 'families' should not be included for filtering of outputs families = set(instance.data["families"]) # add main family to make sure all families are compared @@ -75,29 +76,33 @@ class ExtractReviewIntermediates(publish.Extractor): # generate data with maintained_selection(): generated_repres = [] - for o_name, o_data in self.outputs.items(): + for o_data in self.outputs: + o_name = o_data["name"] self.log.debug( "o_name: {}, o_data: {}".format(o_name, pformat(o_data))) - f_families = o_data["filter"]["families"] + f_product_types = o_data["filter"]["product_types"] f_task_types = o_data["filter"]["task_types"] - f_subsets = o_data["filter"]["subsets"] + product_names = o_data["filter"]["product_names"] self.log.debug( - "f_families `{}` > families: {}".format( - f_families, families)) + "f_product_types `{}` > families: {}".format( + f_product_types, families)) self.log.debug( "f_task_types `{}` > task_type: {}".format( f_task_types, task_type)) self.log.debug( - "f_subsets `{}` > subset: {}".format( - f_subsets, subset)) + "product_names `{}` > subset: {}".format( + product_names, subset)) # test if family found in context # using intersection to make sure all defined # families are present in combination - if f_families and not families.intersection(f_families): + if ( + f_product_types + and not families.intersection(f_product_types) + ): continue # test task types from filter @@ -105,8 +110,9 @@ class ExtractReviewIntermediates(publish.Extractor): continue # test subsets from filter - if f_subsets and not any( - re.search(s, subset) for s in f_subsets): + if product_names and not any( + re.search(p, subset) for p in product_names + ): continue self.log.debug( @@ -117,7 +123,7 @@ class ExtractReviewIntermediates(publish.Extractor): # check if settings have more then one preset # so we dont need to add outputName to representation # in case there is only one preset - multiple_presets = len(self.outputs.keys()) > 1 + multiple_presets = len(self.outputs) > 1 # adding bake presets to instance data for other plugins if not instance.data.get("bakePresets"): diff --git a/client/ayon_core/hosts/nuke/plugins/publish/extract_slate_frame.py b/client/ayon_core/hosts/nuke/plugins/publish/extract_slate_frame.py index 0c4823b1aa..6918e4f50f 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/extract_slate_frame.py +++ b/client/ayon_core/hosts/nuke/plugins/publish/extract_slate_frame.py @@ -29,9 +29,15 @@ class ExtractSlateFrame(publish.Extractor): # Settings values key_value_mapping = { - "f_submission_note": [True, "{comment}"], - "f_submitting_for": [True, "{intent[value]}"], - "f_vfx_scope_of_work": [False, ""] + "f_submission_note": { + "enabled": True, "template": "{comment}" + }, + "f_submitting_for": { + "enabled": True, "template": "{intent[value]}" + }, + "f_vfx_scope_of_work": { + "enabled": False, "template": "" + } } def process(self, instance): @@ -316,11 +322,11 @@ class ExtractSlateFrame(publish.Extractor): }) for key, _values in self.key_value_mapping.items(): - enabled, template = _values - if not enabled: + if not _values["enabled"]: self.log.debug("Key \"{}\" is disabled".format(key)) continue + template = _values["template"] try: value = template.format(**fill_data) diff --git a/client/ayon_core/hosts/nuke/plugins/publish/help/validate_write_nodes.xml b/client/ayon_core/hosts/nuke/plugins/publish/help/validate_write_nodes.xml index 1717622a45..96aa6e4494 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/help/validate_write_nodes.xml +++ b/client/ayon_core/hosts/nuke/plugins/publish/help/validate_write_nodes.xml @@ -25,7 +25,7 @@ ### How to repair? Contact your supervisor or fix it in project settings at - 'project_settings/nuke/imageio/nodes/requiredNodes' at knobs. + 'project_settings/nuke/imageio/nodes/required_nodes' at knobs. Each '__legacy__' type has to be defined accordingly to its type. diff --git a/client/ayon_core/hosts/nuke/plugins/publish/validate_knobs.py b/client/ayon_core/hosts/nuke/plugins/publish/validate_knobs.py index 84efebab53..bede67dabe 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/validate_knobs.py +++ b/client/ayon_core/hosts/nuke/plugins/publish/validate_knobs.py @@ -30,6 +30,8 @@ class ValidateKnobs(pyblish.api.ContextPlugin): actions = [RepairContextAction] optional = True + knobs = "{}" + def process(self, context): invalid = self.get_invalid(context, compute=True) if invalid: @@ -61,6 +63,8 @@ class ValidateKnobs(pyblish.api.ContextPlugin): invalid_knobs = [] for instance in context: + # Load fresh knobs data for each instance + settings_knobs = json.loads(cls.knobs) # Filter families. families = [instance.data["family"]] @@ -74,12 +78,12 @@ class ValidateKnobs(pyblish.api.ContextPlugin): family = family.split(".")[0] # avoid families not in settings - if family not in cls.knobs: + if family not in settings_knobs: continue # get presets of knobs - for preset in cls.knobs[family]: - knobs[preset] = cls.knobs[family][preset] + for preset in settings_knobs[family]: + knobs[preset] = settings_knobs[family][preset] # Get invalid knobs. nodes = [] diff --git a/client/ayon_core/hosts/nuke/plugins/publish/validate_write_nodes.py b/client/ayon_core/hosts/nuke/plugins/publish/validate_write_nodes.py index 7679022487..0244c1d504 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/validate_write_nodes.py +++ b/client/ayon_core/hosts/nuke/plugins/publish/validate_write_nodes.py @@ -86,7 +86,10 @@ class ValidateNukeWriteNode( # Collect key values of same type in a list. values_by_name = defaultdict(list) for knob_data in correct_data["knobs"]: - values_by_name[knob_data["name"]].append(knob_data["value"]) + knob_type = knob_data["type"] + knob_value = knob_data[knob_type] + + values_by_name[knob_data["name"]].append(knob_value) for knob_data in correct_data["knobs"]: knob_type = knob_data["type"] @@ -97,7 +100,7 @@ class ValidateNukeWriteNode( raise PublishXmlValidationError( self, ( "Please update data in settings 'project_settings" - "/nuke/imageio/nodes/requiredNodes'" + "/nuke/imageio/nodes/required_nodes'" ), key="legacy" ) diff --git a/client/ayon_core/hosts/nuke/startup/custom_write_node.py b/client/ayon_core/hosts/nuke/startup/custom_write_node.py index 89dfde297c..5eb58a3679 100644 --- a/client/ayon_core/hosts/nuke/startup/custom_write_node.py +++ b/client/ayon_core/hosts/nuke/startup/custom_write_node.py @@ -127,8 +127,8 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): knobs_nodes = [] settings = [ node_settings for node_settings - in get_nuke_imageio_settings()["nodes"]["overrideNodes"] - if node_settings["nukeNodeClass"] == "Write" + in get_nuke_imageio_settings()["nodes"]["override_nodes"] + if node_settings["nuke_node_class"] == "Write" and node_settings["subsets"] ] if not settings: diff --git a/client/ayon_core/modules/job_queue/__init__.py b/client/ayon_core/modules/job_queue/__init__.py index 6f2cec1b97..0a4c62abfb 100644 --- a/client/ayon_core/modules/job_queue/__init__.py +++ b/client/ayon_core/modules/job_queue/__init__.py @@ -1,6 +1,6 @@ -from .module import JobQueueModule +from .addon import JobQueueAddon __all__ = ( - "JobQueueModule", + "JobQueueAddon", ) diff --git a/client/ayon_core/modules/job_queue/module.py b/client/ayon_core/modules/job_queue/addon.py similarity index 89% rename from client/ayon_core/modules/job_queue/module.py rename to client/ayon_core/modules/job_queue/addon.py index f2b022069b..b28e915ac0 100644 --- a/client/ayon_core/modules/job_queue/module.py +++ b/client/ayon_core/modules/job_queue/addon.py @@ -41,29 +41,24 @@ import json import copy import platform -from ayon_core.addon import click_wrap -from ayon_core.modules import OpenPypeModule +from ayon_core.addon import AYONAddon, click_wrap from ayon_core.settings import get_system_settings -class JobQueueModule(OpenPypeModule): +class JobQueueAddon(AYONAddon): name = "job_queue" - def initialize(self, modules_settings): - module_settings = modules_settings.get(self.name) or {} - server_url = module_settings.get("server_url") or "" + def initialize(self, studio_settings): + addon_settings = studio_settings.get(self.name) or {} + server_url = addon_settings.get("server_url") or "" self._server_url = self.url_conversion(server_url) jobs_root_mapping = self._roots_mapping_conversion( - module_settings.get("jobs_root") + addon_settings.get("jobs_root") ) self._jobs_root_mapping = jobs_root_mapping - # Is always enabled - # - the module does nothing until is used - self.enabled = True - @classmethod def _root_conversion(cls, root_path): """Make sure root path does not end with slash.""" @@ -127,8 +122,8 @@ class JobQueueModule(OpenPypeModule): @classmethod def get_jobs_root_from_settings(cls): - module_settings = get_system_settings()["modules"] - jobs_root_mapping = module_settings.get(cls.name, {}).get("jobs_root") + studio_settings = get_system_settings() + jobs_root_mapping = studio_settings.get(cls.name, {}).get("jobs_root") converted_mapping = cls._roots_mapping_conversion(jobs_root_mapping) return converted_mapping[platform.system().lower()] @@ -157,9 +152,9 @@ class JobQueueModule(OpenPypeModule): @classmethod def get_server_url_from_settings(cls): - module_settings = get_system_settings()["modules"] + studio_settings = get_system_settings() return cls.url_conversion( - module_settings + studio_settings .get(cls.name, {}) .get("server_url") ) @@ -214,7 +209,7 @@ class JobQueueModule(OpenPypeModule): @click_wrap.group( - JobQueueModule.name, + JobQueueAddon.name, help="Application job server. Can be used as render farm." ) def cli_main(): @@ -228,7 +223,7 @@ def cli_main(): @click_wrap.option("--port", help="Server port") @click_wrap.option("--host", help="Server host (ip address)") def cli_start_server(port, host): - JobQueueModule.start_server(port, host) + JobQueueAddon.start_server(port, host) @cli_main.command( @@ -241,4 +236,4 @@ def cli_start_server(port, host): "--server_url", help="Server url which handle workers and jobs.") def cli_start_worker(app_name, server_url): - JobQueueModule.start_worker(app_name, server_url) + JobQueueAddon.start_worker(app_name, server_url) diff --git a/client/ayon_core/modules/royalrender/lib.py b/client/ayon_core/modules/royalrender/lib.py index 60c0427d99..d552e7fb19 100644 --- a/client/ayon_core/modules/royalrender/lib.py +++ b/client/ayon_core/modules/royalrender/lib.py @@ -108,7 +108,7 @@ class BaseCreateRoyalRenderJob(pyblish.api.InstancePlugin, context = instance.context - self._rr_root = instance.data.get("rrPathName") + self._rr_root = instance.data.get("rr_root") if not self._rr_root: raise KnownPublishError( ("Missing RoyalRender root. " diff --git a/client/ayon_core/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py b/client/ayon_core/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py index d860df4684..7fad573a8b 100644 --- a/client/ayon_core/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py +++ b/client/ayon_core/modules/royalrender/plugins/publish/collect_rr_path_from_instance.py @@ -1,41 +1,52 @@ # -*- coding: utf-8 -*- +""" +Requires: + instance.context.data["project_settings"] +Provides: + instance.data["rr_root"] (str) - root folder of RoyalRender server +""" +import os.path + import pyblish.api +from ayon_core.modules.royalrender.rr_job import get_rr_platform class CollectRRPathFromInstance(pyblish.api.InstancePlugin): - """Collect RR Path from instance.""" + """Collect RR Path from instance. + + All RoyalRender server roots are set in `Studio Settings`, each project + uses only key pointing to that part to limit typos inside of Project + settings. + Eventually could be possible to add dropdown with these keys to the + Creators to allow artists to select which RR server they would like to use. + """ order = pyblish.api.CollectorOrder label = "Collect Royal Render path name from the Instance" families = ["render", "prerender", "renderlayer"] def process(self, instance): - instance.data["rrPathName"] = self._collect_rr_path_name(instance) + instance.data["rr_root"] = self._collect_root(instance) self.log.info( - "Using '{}' for submission.".format(instance.data["rrPathName"])) + "Using '{}' for submission.".format(instance.data["rr_root"])) - @staticmethod - def _collect_rr_path_name(instance): + def _collect_root(self, instance): # type: (pyblish.api.Instance) -> str - """Get Royal Render pat name from render instance.""" - - # TODO there are no "rrPaths" on instance, if Publisher should expose - # this (eg. artist could select specific server) it must be added - # to publisher - instance_rr_paths = instance.data.get("rrPaths") - if instance_rr_paths is None: - return "default" - + """Get Royal Render pat name from render instance. + If artist should be able to select specific RR server it must be added + to creator. It is not there yet. + """ rr_settings = instance.context.data["project_settings"]["royalrender"] rr_paths = rr_settings["rr_paths"] - selected_paths = rr_settings["selected_rr_paths"] + selected_keys = rr_settings["selected_rr_paths"] - rr_servers = { - path_key - for path_key in selected_paths - if path_key in rr_paths + platform = get_rr_platform() + key_to_path = { + item["name"]: item["value"][platform] + for item in rr_paths } - for instance_rr_path in instance_rr_paths: - if instance_rr_path in rr_servers: - return instance_rr_path - return "default" + + for selected_key in selected_keys: + rr_root = key_to_path[selected_key] + if os.path.exists(rr_root): + return rr_root diff --git a/client/ayon_core/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py b/client/ayon_core/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py index dcec2ac810..54de943428 100644 --- a/client/ayon_core/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py +++ b/client/ayon_core/modules/royalrender/plugins/publish/submit_jobs_to_royalrender.py @@ -37,8 +37,8 @@ class SubmitJobsToRoyalRender(pyblish.api.ContextPlugin): isinstance(job, RRJob) for job in instance.data.get("rrJobs")): jobs += instance.data.get("rrJobs") - if instance.data.get("rrPathName"): - instance_rr_path = instance.data["rrPathName"] + if instance.data.get("rr_root"): + instance_rr_path = instance.data["rr_root"] if jobs: self._rr_root = instance_rr_path diff --git a/client/ayon_core/settings/ayon_settings.py b/client/ayon_core/settings/ayon_settings.py index 503dcc926d..32bb814c5b 100644 --- a/client/ayon_core/settings/ayon_settings.py +++ b/client/ayon_core/settings/ayon_settings.py @@ -133,242 +133,9 @@ def convert_system_settings(ayon_settings, default_settings, addon_versions): # --------- Project settings --------- -def _convert_nuke_knobs(knobs): - new_knobs = [] - for knob in knobs: - knob_type = knob["type"] - - if knob_type == "boolean": - knob_type = "bool" - - if knob_type != "bool": - value = knob[knob_type] - elif knob_type in knob: - value = knob[knob_type] - else: - value = knob["boolean"] - - new_knob = { - "type": knob_type, - "name": knob["name"], - } - new_knobs.append(new_knob) - - if knob_type == "formatable": - new_knob["template"] = value["template"] - new_knob["to_type"] = value["to_type"] - continue - - value_key = "value" - if knob_type == "expression": - value_key = "expression" - - elif knob_type == "color_gui": - value = _convert_color(value) - - elif knob_type == "vector_2d": - value = [value["x"], value["y"]] - - elif knob_type == "vector_3d": - value = [value["x"], value["y"], value["z"]] - - elif knob_type == "box": - value = [value["x"], value["y"], value["r"], value["t"]] - - new_knob[value_key] = value - return new_knobs - - -def _convert_nuke_project_settings(ayon_settings, output): - if "nuke" not in ayon_settings: - return - - ayon_nuke = ayon_settings["nuke"] - - # --- Load --- - ayon_load = ayon_nuke["load"] - ayon_load["LoadClip"]["_representations"] = ( - ayon_load["LoadClip"].pop("representations_include") - ) - ayon_load["LoadImage"]["_representations"] = ( - ayon_load["LoadImage"].pop("representations_include") - ) - - # --- Create --- - ayon_create = ayon_nuke["create"] - for creator_name in ( - "CreateWritePrerender", - "CreateWriteImage", - "CreateWriteRender", - ): - create_plugin_settings = ayon_create[creator_name] - create_plugin_settings["temp_rendering_path_template"] = ( - create_plugin_settings["temp_rendering_path_template"] - .replace("{product[name]}", "{subset}") - .replace("{product[type]}", "{family}") - .replace("{task[name]}", "{task}") - .replace("{folder[name]}", "{asset}") - ) - new_prenodes = {} - for prenode in create_plugin_settings["prenodes"]: - name = prenode.pop("name") - prenode["knobs"] = _convert_nuke_knobs(prenode["knobs"]) - new_prenodes[name] = prenode - - create_plugin_settings["prenodes"] = new_prenodes - - # --- Publish --- - ayon_publish = ayon_nuke["publish"] - slate_mapping = ayon_publish["ExtractSlateFrame"]["key_value_mapping"] - for key in tuple(slate_mapping.keys()): - value = slate_mapping[key] - slate_mapping[key] = [value["enabled"], value["template"]] - - ayon_publish["ValidateKnobs"]["knobs"] = json.loads( - ayon_publish["ValidateKnobs"]["knobs"] - ) - - new_review_data_outputs = {} - outputs_settings = [] - # Check deprecated ExtractReviewDataMov - # settings for backwards compatibility - deprecrated_review_settings = ayon_publish["ExtractReviewDataMov"] - current_review_settings = ( - ayon_publish.get("ExtractReviewIntermediates") - ) - if deprecrated_review_settings["enabled"]: - outputs_settings = deprecrated_review_settings["outputs"] - elif current_review_settings is None: - pass - elif current_review_settings["enabled"]: - outputs_settings = current_review_settings["outputs"] - - for item in outputs_settings: - item_filter = item["filter"] - if "product_names" in item_filter: - item_filter["subsets"] = item_filter.pop("product_names") - item_filter["families"] = item_filter.pop("product_types") - - reformat_nodes_config = item.get("reformat_nodes_config") or {} - reposition_nodes = reformat_nodes_config.get( - "reposition_nodes") or [] - - for reposition_node in reposition_nodes: - if "knobs" not in reposition_node: - continue - reposition_node["knobs"] = _convert_nuke_knobs( - reposition_node["knobs"] - ) - - name = item.pop("name") - new_review_data_outputs[name] = item - - if deprecrated_review_settings["enabled"]: - deprecrated_review_settings["outputs"] = new_review_data_outputs - elif current_review_settings["enabled"]: - current_review_settings["outputs"] = new_review_data_outputs - - collect_instance_data = ayon_publish["CollectInstanceData"] - if "sync_workfile_version_on_product_types" in collect_instance_data: - collect_instance_data["sync_workfile_version_on_families"] = ( - collect_instance_data.pop( - "sync_workfile_version_on_product_types")) - - # --- ImageIO --- - # NOTE 'monitorOutLut' is maybe not yet in v3 (ut should be) - ayon_imageio = ayon_nuke["imageio"] - - # workfile - imageio_workfile = ayon_imageio["workfile"] - workfile_keys_mapping = ( - ("color_management", "colorManagement"), - ("native_ocio_config", "OCIO_config"), - ("working_space", "workingSpaceLUT"), - ("thumbnail_space", "monitorLut"), - ) - for src, dst in workfile_keys_mapping: - if ( - src in imageio_workfile - and dst not in imageio_workfile - ): - imageio_workfile[dst] = imageio_workfile.pop(src) - - # regex inputs - if "regex_inputs" in ayon_imageio: - ayon_imageio["regexInputs"] = ayon_imageio.pop("regex_inputs") - - # nodes - ayon_imageio_nodes = ayon_imageio["nodes"] - if "required_nodes" in ayon_imageio_nodes: - ayon_imageio_nodes["requiredNodes"] = ( - ayon_imageio_nodes.pop("required_nodes")) - if "override_nodes" in ayon_imageio_nodes: - ayon_imageio_nodes["overrideNodes"] = ( - ayon_imageio_nodes.pop("override_nodes")) - - for item in ayon_imageio_nodes["requiredNodes"]: - if "nuke_node_class" in item: - item["nukeNodeClass"] = item.pop("nuke_node_class") - item["knobs"] = _convert_nuke_knobs(item["knobs"]) - - for item in ayon_imageio_nodes["overrideNodes"]: - if "nuke_node_class" in item: - item["nukeNodeClass"] = item.pop("nuke_node_class") - item["knobs"] = _convert_nuke_knobs(item["knobs"]) - - output["nuke"] = ayon_nuke - - -def _convert_hiero_project_settings(ayon_settings, output): - if "hiero" not in ayon_settings: - return - - ayon_hiero = ayon_settings["hiero"] - - new_gui_filters = {} - for item in ayon_hiero.pop("filters", []): - subvalue = {} - key = item["name"] - for subitem in item["value"]: - subvalue[subitem["name"]] = subitem["value"] - new_gui_filters[key] = subvalue - ayon_hiero["filters"] = new_gui_filters - - ayon_load_clip = ayon_hiero["load"]["LoadClip"] - if "product_types" in ayon_load_clip: - ayon_load_clip["families"] = ayon_load_clip.pop("product_types") - - ayon_load_clip = ayon_hiero["load"]["LoadClip"] - ayon_load_clip["clip_name_template"] = ( - ayon_load_clip["clip_name_template"] - .replace("{folder[name]}", "{asset}") - .replace("{product[name]}", "{subset}") - ) - - output["hiero"] = ayon_hiero - - -def _convert_royalrender_project_settings(ayon_settings, output): - if "royalrender" not in ayon_settings: - return - ayon_royalrender = ayon_settings["royalrender"] - rr_paths = ayon_royalrender.get("selected_rr_paths", []) - - output["royalrender"] = { - "publish": ayon_royalrender["publish"], - "rr_paths": rr_paths, - } - - def convert_project_settings(ayon_settings, default_settings): default_settings = copy.deepcopy(default_settings) output = {} - - _convert_nuke_project_settings(ayon_settings, output) - _convert_hiero_project_settings(ayon_settings, output) - - _convert_royalrender_project_settings(ayon_settings, output) - for key, value in ayon_settings.items(): if key not in output: output[key] = value diff --git a/client/ayon_core/settings/defaults/project_settings/max.json b/client/ayon_core/settings/defaults/project_settings/max.json index d1610610dc..a0a4fcf83d 100644 --- a/client/ayon_core/settings/defaults/project_settings/max.json +++ b/client/ayon_core/settings/defaults/project_settings/max.json @@ -56,6 +56,16 @@ "enabled": false, "attributes": {} }, + "ValidateCameraAttributes": { + "enabled": true, + "optional": true, + "active": false, + "fov": 45.0, + "nearrange": 0.0, + "farrange": 1000.0, + "nearclip": 1.0, + "farclip": 1000.0 + }, "ValidateLoadedPlugin": { "enabled": false, "optional": true, diff --git a/client/ayon_core/tools/pyblish_pype/control.py b/client/ayon_core/tools/pyblish_pype/control.py index 0e25fa9e27..c5034e2736 100644 --- a/client/ayon_core/tools/pyblish_pype/control.py +++ b/client/ayon_core/tools/pyblish_pype/control.py @@ -202,19 +202,39 @@ class Controller(QtCore.QObject): def current_state(self): return self._current_state + @staticmethod + def _convert_filter_presets(filter_presets): + """Convert AYON settings presets to dictionary. + + Returns: + dict[str, dict[str, Any]]: Filter presets converted to dictionary. + """ + if not isinstance(filter_presets, list): + return filter_presets + + return { + filter_preset["name"]: { + item["name"]: item["value"] + for item in filter_preset["value"] + } + for filter_preset in filter_presets + } + def presets_by_hosts(self): # Get global filters as base presets = get_current_project_settings() if not presets: return {} - result = presets.get("core", {}).get("filters", {}) + result = {} hosts = pyblish.api.registered_hosts() for host in hosts: host_presets = presets.get(host, {}).get("filters") if not host_presets: continue + host_presets = self._convert_filter_presets(host_presets) + for key, value in host_presets.items(): if value is None: if key in result: diff --git a/client/ayon_core/tools/tray/__init__.py b/client/ayon_core/tools/tray/__init__.py index f5e558e0bb..49130e660a 100644 --- a/client/ayon_core/tools/tray/__init__.py +++ b/client/ayon_core/tools/tray/__init__.py @@ -1,5 +1,6 @@ from .tray import main + __all__ = ( "main", ) diff --git a/client/ayon_core/tools/tray/dialogs.py b/client/ayon_core/tools/tray/dialogs.py new file mode 100644 index 0000000000..67348284a1 --- /dev/null +++ b/client/ayon_core/tools/tray/dialogs.py @@ -0,0 +1,155 @@ +import os + +from qtpy import QtWidgets, QtCore, QtGui + +from ayon_core import resources, style +from ayon_core.tools.utils import paint_image_with_color + + +class PixmapLabel(QtWidgets.QLabel): + """Label resizing image to height of font.""" + def __init__(self, pixmap, parent): + super(PixmapLabel, self).__init__(parent) + self._empty_pixmap = QtGui.QPixmap(0, 0) + self._source_pixmap = pixmap + + def set_source_pixmap(self, pixmap): + """Change source image.""" + self._source_pixmap = pixmap + self._set_resized_pix() + + def _get_pix_size(self): + size = self.fontMetrics().height() * 3 + return size, size + + def _set_resized_pix(self): + if self._source_pixmap is None: + self.setPixmap(self._empty_pixmap) + return + width, height = self._get_pix_size() + self.setPixmap( + self._source_pixmap.scaled( + width, + height, + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation + ) + ) + + def resizeEvent(self, event): + self._set_resized_pix() + super(PixmapLabel, self).resizeEvent(event) + + +class UpdateDialog(QtWidgets.QDialog): + restart_requested = QtCore.Signal() + ignore_requested = QtCore.Signal() + + _min_width = 400 + _min_height = 130 + + def __init__(self, parent=None): + super(UpdateDialog, self).__init__(parent) + + icon = QtGui.QIcon(resources.get_ayon_icon_filepath()) + self.setWindowIcon(icon) + self.setWindowTitle("AYON update") + self.setWindowFlags( + self.windowFlags() + | QtCore.Qt.WindowStaysOnTopHint + ) + + self.setMinimumWidth(self._min_width) + self.setMinimumHeight(self._min_height) + + top_widget = QtWidgets.QWidget(self) + + gift_pixmap = self._get_gift_pixmap() + gift_icon_label = PixmapLabel(gift_pixmap, top_widget) + + label_widget = QtWidgets.QLabel( + ( + "Your AYON needs to update." + "

Please restart AYON launcher and all running" + " applications as soon as possible." + ), + top_widget + ) + label_widget.setWordWrap(True) + + top_layout = QtWidgets.QHBoxLayout(top_widget) + top_layout.setSpacing(10) + top_layout.addWidget(gift_icon_label, 0, QtCore.Qt.AlignCenter) + top_layout.addWidget(label_widget, 1) + + ignore_btn = QtWidgets.QPushButton("Ignore", self) + restart_btn = QtWidgets.QPushButton("Restart && Change", self) + restart_btn.setObjectName("TrayRestartButton") + + btns_layout = QtWidgets.QHBoxLayout() + btns_layout.addStretch(1) + btns_layout.addWidget(ignore_btn, 0) + btns_layout.addWidget(restart_btn, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(top_widget, 0) + layout.addStretch(1) + layout.addLayout(btns_layout, 0) + + ignore_btn.clicked.connect(self._on_ignore) + restart_btn.clicked.connect(self._on_reset) + + self._label_widget = label_widget + self._gift_icon_label = gift_icon_label + self._ignore_btn = ignore_btn + self._restart_btn = restart_btn + + self._restart_accepted = False + self._current_is_higher = False + + self._close_silently = False + + self.setStyleSheet(style.load_stylesheet()) + + def close_silently(self): + self._close_silently = True + self.close() + + def showEvent(self, event): + super(UpdateDialog, self).showEvent(event) + self._close_silently = False + self._restart_accepted = False + + def closeEvent(self, event): + super(UpdateDialog, self).closeEvent(event) + if self._restart_accepted or self._current_is_higher: + return + + if self._close_silently: + return + + # Trigger ignore requested only if restart was not clicked and current + # version is lower + self.ignore_requested.emit() + + def _on_ignore(self): + self.reject() + + def _on_reset(self): + self._restart_accepted = True + self.restart_requested.emit() + self.accept() + + def _get_gift_pixmap(self): + image_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "images", + "gifts.png" + ) + src_image = QtGui.QImage(image_path) + color_value = style.get_objected_colors("font") + + return paint_image_with_color( + src_image, + color_value.get_qcolor() + ) diff --git a/client/ayon_core/tools/tray/tray.py b/client/ayon_core/tools/tray/tray.py index 3a70d68466..d09f40b7fc 100644 --- a/client/ayon_core/tools/tray/tray.py +++ b/client/ayon_core/tools/tray/tray.py @@ -1,10 +1,11 @@ -import collections import os import sys +import collections import atexit import platform +import ayon_api from qtpy import QtCore, QtGui, QtWidgets from ayon_core import resources, style @@ -12,57 +13,25 @@ from ayon_core.lib import ( Logger, get_ayon_launcher_args, run_detached_process, + is_dev_mode_enabled, + is_staging_enabled, + is_running_from_build, ) -from ayon_core.lib import is_running_from_build +from ayon_core.settings import get_ayon_settings from ayon_core.addon import ( ITrayAction, ITrayService, TrayAddonsManager, ) -from ayon_core.settings import get_system_settings from ayon_core.tools.utils import ( WrappedCallbackItem, get_ayon_qt_app, ) from .info_widget import InfoWidget - - -# TODO PixmapLabel should be moved to 'utils' in other future PR so should be -# imported from there -class PixmapLabel(QtWidgets.QLabel): - """Label resizing image to height of font.""" - def __init__(self, pixmap, parent): - super(PixmapLabel, self).__init__(parent) - self._empty_pixmap = QtGui.QPixmap(0, 0) - self._source_pixmap = pixmap - - def set_source_pixmap(self, pixmap): - """Change source image.""" - self._source_pixmap = pixmap - self._set_resized_pix() - - def _get_pix_size(self): - size = self.fontMetrics().height() * 3 - return size, size - - def _set_resized_pix(self): - if self._source_pixmap is None: - self.setPixmap(self._empty_pixmap) - return - width, height = self._get_pix_size() - self.setPixmap( - self._source_pixmap.scaled( - width, - height, - QtCore.Qt.KeepAspectRatio, - QtCore.Qt.SmoothTransformation - ) - ) - - def resizeEvent(self, event): - self._set_resized_pix() - super(PixmapLabel, self).resizeEvent(event) +from .dialogs import ( + UpdateDialog, +) class TrayManager: @@ -78,22 +47,26 @@ class TrayManager: self.log = Logger.get_logger(self.__class__.__name__) - system_settings = get_system_settings() + studio_settings = get_ayon_settings() - version_check_interval = system_settings["general"].get( - "version_check_interval" + update_check_interval = studio_settings["core"].get( + "update_check_interval" ) - if version_check_interval is None: - version_check_interval = 5 - self._version_check_interval = version_check_interval * 60 * 1000 + if update_check_interval is None: + update_check_interval = 5 + self._update_check_interval = update_check_interval * 60 * 1000 self._addons_manager = TrayAddonsManager() self.errors = [] - self.main_thread_timer = None + self._update_check_timer = None + self._outdated_dialog = None + + self._main_thread_timer = None self._main_thread_callbacks = collections.deque() self._execution_in_progress = None + self._closing = False @property def doubleclick_callback(self): @@ -107,29 +80,25 @@ class TrayManager: if callback: self.execute_in_main_thread(callback) - def _restart_and_install(self): - self.restart(use_expected_version=True) + def show_tray_message(self, title, message, icon=None, msecs=None): + """Show tray message. - def execute_in_main_thread(self, callback, *args, **kwargs): - if isinstance(callback, WrappedCallbackItem): - item = callback - else: - item = WrappedCallbackItem(callback, *args, **kwargs) + Args: + title (str): Title of message. + message (str): Content of message. + icon (QSystemTrayIcon.MessageIcon): Message's icon. Default is + Information icon, may differ by Qt version. + msecs (int): Duration of message visibility in milliseconds. + Default is 10000 msecs, may differ by Qt version. + """ + args = [title, message] + kwargs = {} + if icon: + kwargs["icon"] = icon + if msecs: + kwargs["msecs"] = msecs - self._main_thread_callbacks.append(item) - - return item - - def _main_thread_execution(self): - if self._execution_in_progress: - return - self._execution_in_progress = True - for _ in range(len(self._main_thread_callbacks)): - if self._main_thread_callbacks: - item = self._main_thread_callbacks.popleft() - item.execute() - - self._execution_in_progress = False + self.tray_widget.showMessage(*args, **kwargs) def initialize_addons(self): """Add addons to tray.""" @@ -140,7 +109,9 @@ class TrayManager: self.tray_widget.menu.addMenu(admin_submenu) # Add services if they are - services_submenu = ITrayService.services_submenu(self.tray_widget.menu) + services_submenu = ITrayService.services_submenu( + self.tray_widget.menu + ) self.tray_widget.menu.addMenu(services_submenu) # Add separator @@ -165,39 +136,172 @@ class TrayManager: main_thread_timer.timeout.connect(self._main_thread_execution) main_thread_timer.start() - self.main_thread_timer = main_thread_timer + self._main_thread_timer = main_thread_timer + + update_check_timer = QtCore.QTimer() + if self._update_check_interval > 0: + update_check_timer.timeout.connect(self._on_update_check_timer) + update_check_timer.setInterval(self._update_check_interval) + update_check_timer.start() + self._update_check_timer = update_check_timer self.execute_in_main_thread(self._startup_validations) + def restart(self): + """Restart Tray tool. + + First creates new process with same argument and close current tray. + """ + + self._closing = True + + args = get_ayon_launcher_args() + + # Create a copy of sys.argv + additional_args = list(sys.argv) + # Remove first argument from 'sys.argv' + # - when running from code the first argument is 'start.py' + # - when running from build the first argument is executable + additional_args.pop(0) + additional_args = [ + arg + for arg in additional_args + if arg not in {"--use-staging", "--use-dev"} + ] + + if is_dev_mode_enabled(): + additional_args.append("--use-dev") + elif is_staging_enabled(): + additional_args.append("--use-staging") + + args.extend(additional_args) + + envs = dict(os.environ.items()) + for key in { + "AYON_BUNDLE_NAME", + }: + envs.pop(key, None) + + run_detached_process(args, env=envs) + self.exit() + + def exit(self): + self._closing = True + self.tray_widget.exit() + + def on_exit(self): + self._addons_manager.on_exit() + + def execute_in_main_thread(self, callback, *args, **kwargs): + if isinstance(callback, WrappedCallbackItem): + item = callback + else: + item = WrappedCallbackItem(callback, *args, **kwargs) + + self._main_thread_callbacks.append(item) + + return item + + def _on_update_check_timer(self): + try: + bundles = ayon_api.get_bundles() + user = ayon_api.get_user() + # This is a workaround for bug in ayon-python-api + if user.get("code") == 401: + raise Exception("Unauthorized") + except Exception: + self._revalidate_ayon_auth() + if self._closing: + return + + try: + bundles = ayon_api.get_bundles() + except Exception: + return + + if is_dev_mode_enabled(): + return + + bundle_type = ( + "stagingBundle" + if is_staging_enabled() + else "productionBundle" + ) + + expected_bundle = bundles.get(bundle_type) + current_bundle = os.environ.get("AYON_BUNDLE_NAME") + is_expected = expected_bundle == current_bundle + if is_expected or expected_bundle is None: + self._restart_action.setVisible(False) + if ( + self._outdated_dialog is not None + and self._outdated_dialog.isVisible() + ): + self._outdated_dialog.close_silently() + return + + self._restart_action.setVisible(True) + + if self._outdated_dialog is None: + self._outdated_dialog = UpdateDialog() + self._outdated_dialog.restart_requested.connect( + self._restart_and_install + ) + self._outdated_dialog.ignore_requested.connect( + self._outdated_bundle_ignored + ) + + self._outdated_dialog.show() + self._outdated_dialog.raise_() + self._outdated_dialog.activateWindow() + + def _revalidate_ayon_auth(self): + result = self._show_ayon_login(restart_on_token_change=False) + if self._closing: + return False + + if not result.new_token: + self.exit() + return False + return True + + def _restart_and_install(self): + self.restart() + + def _outdated_bundle_ignored(self): + self.show_tray_message( + "AYON update ignored", + ( + "Please restart AYON launcher as soon as possible" + " to propagate updates." + ) + ) + + def _main_thread_execution(self): + if self._execution_in_progress: + return + self._execution_in_progress = True + for _ in range(len(self._main_thread_callbacks)): + if self._main_thread_callbacks: + item = self._main_thread_callbacks.popleft() + try: + item.execute() + except BaseException: + self.log.erorr( + "Main thread execution failed", exc_info=True + ) + + self._execution_in_progress = False + def _startup_validations(self): """Run possible startup validations.""" - pass - - def show_tray_message(self, title, message, icon=None, msecs=None): - """Show tray message. - - Args: - title (str): Title of message. - message (str): Content of message. - icon (QSystemTrayIcon.MessageIcon): Message's icon. Default is - Information icon, may differ by Qt version. - msecs (int): Duration of message visibility in milliseconds. - Default is 10000 msecs, may differ by Qt version. - """ - args = [title, message] - kwargs = {} - if icon: - kwargs["icon"] = icon - if msecs: - kwargs["msecs"] = msecs - - self.tray_widget.showMessage(*args, **kwargs) + # Trigger bundle validation on start + self._update_check_timer.timeout.emit() def _add_version_item(self): login_action = QtWidgets.QAction("Login", self.tray_widget) login_action.triggered.connect(self._on_ayon_login) self.tray_widget.menu.addAction(login_action) - version_string = os.getenv("AYON_VERSION", "AYON Info") version_action = QtWidgets.QAction(version_string, self.tray_widget) @@ -216,16 +320,24 @@ class TrayManager: self._restart_action = restart_action def _on_ayon_login(self): - self.execute_in_main_thread(self._show_ayon_login) + self.execute_in_main_thread( + self._show_ayon_login, + restart_on_token_change=True + ) - def _show_ayon_login(self): + def _show_ayon_login(self, restart_on_token_change): from ayon_common.connection.credentials import change_user_ui result = change_user_ui() if result.shutdown: self.exit() + return result - elif result.restart or result.token_changed: + restart = result.restart + if restart_on_token_change and result.token_changed: + restart = True + + if restart: # Remove environment variables from current connection # - keep develop, staging, headless values for key in { @@ -235,23 +347,13 @@ class TrayManager: }: os.environ.pop(key, None) self.restart() + return result def _on_restart_action(self): - self.restart(use_expected_version=True) + self.restart() - def restart(self, use_expected_version=False, reset_version=False): - """Restart Tray tool. - - First creates new process with same argument and close current tray. - - Args: - use_expected_version(bool): OpenPype version is set to expected - version. - reset_version(bool): OpenPype version is cleaned up so igniters - logic will decide which version will be used. - """ + def _restart_ayon(self): args = get_ayon_launcher_args() - envs = dict(os.environ.items()) # Create a copy of sys.argv additional_args = list(sys.argv) @@ -259,35 +361,28 @@ class TrayManager: # - when running from code the first argument is 'start.py' # - when running from build the first argument is executable additional_args.pop(0) + additional_args = [ + arg + for arg in additional_args + if arg not in {"--use-staging", "--use-dev"} + ] - cleanup_additional_args = False - if use_expected_version: - cleanup_additional_args = True - reset_version = True - - # Pop OPENPYPE_VERSION - if reset_version: - cleanup_additional_args = True - envs.pop("OPENPYPE_VERSION", None) - - if cleanup_additional_args: - _additional_args = [] - for arg in additional_args: - if arg == "--use-staging" or arg.startswith("--use-version"): - continue - _additional_args.append(arg) - additional_args = _additional_args + if is_dev_mode_enabled(): + additional_args.append("--use-dev") + elif is_staging_enabled(): + additional_args.append("--use-staging") args.extend(additional_args) + + envs = dict(os.environ.items()) + for key in { + "AYON_BUNDLE_NAME", + }: + envs.pop(key, None) + run_detached_process(args, env=envs) self.exit() - def exit(self): - self.tray_widget.exit() - - def on_exit(self): - self._addons_manager.on_exit() - def _on_version_action(self): if self._info_widget is None: self._info_widget = InfoWidget() diff --git a/create_package.py b/create_package.py index 94b31a03f2..48952c43c5 100644 --- a/create_package.py +++ b/create_package.py @@ -279,7 +279,8 @@ def create_server_package( def main( output_dir: Optional[str]=None, skip_zip: bool=False, - keep_sources: bool=False + keep_sources: bool=False, + clear_output_dir: bool=False ): log = logging.getLogger("create_package") log.info("Start creating package") @@ -292,7 +293,8 @@ def main( new_created_version_dir = os.path.join( output_dir, ADDON_NAME, ADDON_VERSION ) - if os.path.isdir(new_created_version_dir): + + if os.path.isdir(new_created_version_dir) and clear_output_dir: log.info(f"Purging {new_created_version_dir}") shutil.rmtree(output_dir) @@ -339,6 +341,15 @@ if __name__ == "__main__": "Keep folder structure when server package is created." ) ) + parser.add_argument( + "-c", "--clear-output-dir", + dest="clear_output_dir", + action="store_true", + help=( + "Clear output directory before package creation." + ) + ) + parser.add_argument( "-o", "--output", dest="output_dir", @@ -350,4 +361,9 @@ if __name__ == "__main__": ) args = parser.parse_args(sys.argv[1:]) - main(args.output_dir, args.skip_zip, args.keep_sources) + main( + args.output_dir, + args.skip_zip, + args.keep_sources, + args.clear_output_dir + ) diff --git a/server/settings/main.py b/server/settings/main.py index 1bdfcefe19..28a69e182d 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -121,6 +121,11 @@ class CoreSettings(BaseSettingsModel): widget="textarea", scope=["studio"], ) + update_check_interval: int = SettingsField( + 5, + title="Update check interval (minutes)", + ge=0 + ) disk_mapping: DiskMappingModel = SettingsField( default_factory=DiskMappingModel, title="Disk mapping", diff --git a/server_addon/deadline/server/settings/publish_plugins.py b/server_addon/deadline/server/settings/publish_plugins.py index 3f5ac3108d..10ec8ac95f 100644 --- a/server_addon/deadline/server/settings/publish_plugins.py +++ b/server_addon/deadline/server/settings/publish_plugins.py @@ -287,6 +287,13 @@ class ProcessSubmittedJobOnFarmModel(BaseSettingsModel): default_factory=list, title="Skip integration of representation with ext" ) + families_transfer: list[str] = SettingsField( + default_factory=list, + title=( + "List of family names to transfer\n" + "to generated instances (AOVs for example)." + ) + ) aov_filter: list[AOVFilterSubmodel] = SettingsField( default_factory=list, title="Reviewable products filter", @@ -470,6 +477,7 @@ DEFAULT_DEADLINE_PLUGINS_SETTINGS = { "deadline_priority": 50, "publishing_script": "", "skip_integration_repre_list": [], + "families_transfer": ["render3d", "render2d", "ftrack", "slate"], "aov_filter": [ { "name": "maya", diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py index e8a48ec3d1..5e28c1b467 100644 --- a/server_addon/max/server/settings/publishers.py +++ b/server_addon/max/server/settings/publishers.py @@ -27,6 +27,17 @@ class ValidateAttributesModel(BaseSettingsModel): return value +class ValidateCameraAttributesModel(BaseSettingsModel): + enabled: bool = SettingsField(title="Enabled") + optional: bool = SettingsField(title="Optional") + active: bool = SettingsField(title="Active") + fov: float = SettingsField(0.0, title="Focal Length") + nearrange: float = SettingsField(0.0, title="Near Range") + farrange: float = SettingsField(0.0, title="Far Range") + nearclip: float = SettingsField(0.0, title="Near Clip") + farclip: float = SettingsField(0.0, title="Far Clip") + + class FamilyMappingItemModel(BaseSettingsModel): families: list[str] = SettingsField( default_factory=list, @@ -63,7 +74,14 @@ class PublishersModel(BaseSettingsModel): default_factory=ValidateAttributesModel, title="Validate Attributes" ) - + ValidateCameraAttributes: ValidateCameraAttributesModel = SettingsField( + default_factory=ValidateCameraAttributesModel, + title="Validate Camera Attributes", + description=( + "If the value of the camera attributes set to 0, " + "the system automatically skips checking it" + ) + ) ValidateLoadedPlugin: ValidateLoadedPluginModel = SettingsField( default_factory=ValidateLoadedPluginModel, title="Validate Loaded Plugin" @@ -101,6 +119,16 @@ DEFAULT_PUBLISH_SETTINGS = { "enabled": False, "attributes": "{}" }, + "ValidateCameraAttributes": { + "enabled": True, + "optional": True, + "active": False, + "fov": 45.0, + "nearrange": 0.0, + "farrange": 1000.0, + "nearclip": 1.0, + "farclip": 1000.0 + }, "ValidateLoadedPlugin": { "enabled": False, "optional": True,