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,