Merge branch 'develop' into enhancement/OP-8105_Validate-Context

This commit is contained in:
Kayla Man 2024-02-23 18:33:23 +08:00
commit 672685bf58
37 changed files with 855 additions and 599 deletions

View file

@ -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({

View file

@ -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()

View file

@ -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)

View file

@ -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)

View file

@ -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):

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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):

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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))

View file

@ -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

View file

@ -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"):

View file

@ -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)

View file

@ -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.
</description>
</error>

View file

@ -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 = []

View file

@ -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"
)

View file

@ -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:

View file

@ -1,6 +1,6 @@
from .module import JobQueueModule
from .addon import JobQueueAddon
__all__ = (
"JobQueueModule",
"JobQueueAddon",
)

View file

@ -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)

View file

@ -133,221 +133,6 @@ 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
@ -364,9 +149,6 @@ 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():

View file

@ -61,6 +61,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,

View file

@ -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:

View file

@ -1,5 +1,6 @@
from .tray import main
__all__ = (
"main",
)

View file

@ -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."
"<br/><br/>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()
)

View file

@ -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()

View file

@ -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
)

View file

@ -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",

View file

@ -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",

View file

@ -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,
@ -67,7 +78,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"
@ -110,6 +128,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,