From 1be9a4112a7baff6ae91f324f048b5af849bb32a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 9 Jan 2022 20:42:40 +0100 Subject: [PATCH 01/39] Improve FusionPreLaunch hook error readability + make it a pop-up from the launcher. - I've removed the usage of ` in the string as they would convert into special characters in the pop-up. So those are changed to '. --- .../hosts/fusion/hooks/pre_fusion_setup.py | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/fusion/hooks/pre_fusion_setup.py b/openpype/hosts/fusion/hooks/pre_fusion_setup.py index a0c16a6700..9da7237505 100644 --- a/openpype/hosts/fusion/hooks/pre_fusion_setup.py +++ b/openpype/hosts/fusion/hooks/pre_fusion_setup.py @@ -1,6 +1,6 @@ import os import importlib -from openpype.lib import PreLaunchHook +from openpype.lib import PreLaunchHook, ApplicationLaunchFailed from openpype.hosts.fusion.api import utils @@ -14,24 +14,26 @@ class FusionPrelaunch(PreLaunchHook): def execute(self): # making sure pyton 3.6 is installed at provided path py36_dir = os.path.normpath(self.launch_context.env.get("PYTHON36", "")) - assert os.path.isdir(py36_dir), ( - "Python 3.6 is not installed at the provided folder path. Either " - "make sure the `environments\resolve.json` is having correctly " - "set `PYTHON36` or make sure Python 3.6 is installed " - f"in given path. \nPYTHON36E: `{py36_dir}`" + if not os.path.isdir(py36_dir): + raise ApplicationLaunchFailed( + "Python 3.6 is not installed at the provided path.\n" + "Either make sure the 'environments/fusion.json' has " + "'PYTHON36' set corectly or make sure Python 3.6 is installed " + f"in the given path.\n\nPYTHON36: {py36_dir}" ) - self.log.info(f"Path to Fusion Python folder: `{py36_dir}`...") + self.log.info(f"Path to Fusion Python folder: '{py36_dir}'...") self.launch_context.env["PYTHON36"] = py36_dir # setting utility scripts dir for scripts syncing us_dir = os.path.normpath( self.launch_context.env.get("FUSION_UTILITY_SCRIPTS_DIR", "") ) - assert os.path.isdir(us_dir), ( - "Fusion utility script dir does not exists. Either make sure " - "the `environments\fusion.json` is having correctly set " - "`FUSION_UTILITY_SCRIPTS_DIR` or reinstall DaVinci Resolve. \n" - f"FUSION_UTILITY_SCRIPTS_DIR: `{us_dir}`" + if not os.path.isdir(us_dir): + raise ApplicationLaunchFailed( + "Fusion utility script dir does not exist. Either make sure " + "the 'environments/fusion.json' has 'FUSION_UTILITY_SCRIPTS_DIR' " + "set correctly or reinstall DaVinci Resolve.\n\n" + f"FUSION_UTILITY_SCRIPTS_DIR: '{us_dir}'" ) try: From ff8643a128e57bb72ad42c8e31ad9925026c2e81 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 9 Jan 2022 20:48:39 +0100 Subject: [PATCH 02/39] Fix indentations --- .../hosts/fusion/hooks/pre_fusion_setup.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/fusion/hooks/pre_fusion_setup.py b/openpype/hosts/fusion/hooks/pre_fusion_setup.py index 9da7237505..906c1e7b8a 100644 --- a/openpype/hosts/fusion/hooks/pre_fusion_setup.py +++ b/openpype/hosts/fusion/hooks/pre_fusion_setup.py @@ -16,11 +16,11 @@ class FusionPrelaunch(PreLaunchHook): py36_dir = os.path.normpath(self.launch_context.env.get("PYTHON36", "")) if not os.path.isdir(py36_dir): raise ApplicationLaunchFailed( - "Python 3.6 is not installed at the provided path.\n" - "Either make sure the 'environments/fusion.json' has " - "'PYTHON36' set corectly or make sure Python 3.6 is installed " - f"in the given path.\n\nPYTHON36: {py36_dir}" - ) + "Python 3.6 is not installed at the provided path.\n" + "Either make sure the 'environments/fusion.json' has " + "'PYTHON36' set corectly or make sure Python 3.6 is installed " + f"in the given path.\n\nPYTHON36: {py36_dir}" + ) self.log.info(f"Path to Fusion Python folder: '{py36_dir}'...") self.launch_context.env["PYTHON36"] = py36_dir @@ -30,11 +30,12 @@ class FusionPrelaunch(PreLaunchHook): ) if not os.path.isdir(us_dir): raise ApplicationLaunchFailed( - "Fusion utility script dir does not exist. Either make sure " - "the 'environments/fusion.json' has 'FUSION_UTILITY_SCRIPTS_DIR' " - "set correctly or reinstall DaVinci Resolve.\n\n" - f"FUSION_UTILITY_SCRIPTS_DIR: '{us_dir}'" - ) + "Fusion utility script dir does not exist. Either make sure " + "the 'environments/fusion.json' has " + "'FUSION_UTILITY_SCRIPTS_DIR' set correctly or reinstall " + "DaVinci Resolve.\n\n" + f"FUSION_UTILITY_SCRIPTS_DIR: '{us_dir}'" + ) try: __import__("avalon.fusion") From 425dbad2ac33cdcb960aa1ed539f2caf9532543e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 9 Jan 2022 20:49:24 +0100 Subject: [PATCH 03/39] Refactor mention of Resolve to Fusion. --- openpype/hosts/fusion/hooks/pre_fusion_setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/hooks/pre_fusion_setup.py b/openpype/hosts/fusion/hooks/pre_fusion_setup.py index 906c1e7b8a..8c4973cf43 100644 --- a/openpype/hosts/fusion/hooks/pre_fusion_setup.py +++ b/openpype/hosts/fusion/hooks/pre_fusion_setup.py @@ -33,8 +33,7 @@ class FusionPrelaunch(PreLaunchHook): "Fusion utility script dir does not exist. Either make sure " "the 'environments/fusion.json' has " "'FUSION_UTILITY_SCRIPTS_DIR' set correctly or reinstall " - "DaVinci Resolve.\n\n" - f"FUSION_UTILITY_SCRIPTS_DIR: '{us_dir}'" + f"Fusion.\n\nFUSION_UTILITY_SCRIPTS_DIR: '{us_dir}'" ) try: From e4368e69b1088ea3345932b9109a20a5c0d83de7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Jan 2022 12:25:50 +0100 Subject: [PATCH 04/39] moved nuke implementation from avalon --- openpype/hosts/nuke/api/__init__.py | 164 ++--- openpype/hosts/nuke/api/actions.py | 5 +- openpype/hosts/nuke/api/command.py | 135 ++++ openpype/hosts/nuke/api/lib.py | 616 ++++++++++++++++-- openpype/hosts/nuke/api/menu.py | 166 ----- openpype/hosts/nuke/api/pipeline.py | 421 ++++++++++++ openpype/hosts/nuke/api/plugin.py | 67 +- openpype/hosts/nuke/api/utils.py | 5 +- openpype/hosts/nuke/api/workio.py | 55 ++ .../nuke/plugins/create/create_backdrop.py | 15 +- .../nuke/plugins/create/create_camera.py | 12 +- .../hosts/nuke/plugins/create/create_gizmo.py | 26 +- .../hosts/nuke/plugins/create/create_model.py | 12 +- .../hosts/nuke/plugins/create/create_read.py | 15 +- .../plugins/create/create_write_prerender.py | 11 +- .../plugins/create/create_write_render.py | 11 +- .../nuke/plugins/create/create_write_still.py | 11 +- .../plugins/inventory/repair_old_loaders.py | 9 +- .../plugins/inventory/select_containers.py | 4 +- .../hosts/nuke/plugins/load/load_backdrop.py | 40 +- .../nuke/plugins/load/load_camera_abc.py | 18 +- openpype/hosts/nuke/plugins/load/load_clip.py | 13 +- .../hosts/nuke/plugins/load/load_effects.py | 17 +- .../nuke/plugins/load/load_effects_ip.py | 17 +- .../hosts/nuke/plugins/load/load_gizmo.py | 23 +- .../hosts/nuke/plugins/load/load_gizmo_ip.py | 31 +- .../hosts/nuke/plugins/load/load_image.py | 17 +- .../hosts/nuke/plugins/load/load_model.py | 15 +- .../nuke/plugins/load/load_script_precomp.py | 17 +- .../nuke/plugins/publish/extract_backdrop.py | 25 +- .../nuke/plugins/publish/extract_camera.py | 10 +- .../nuke/plugins/publish/extract_gizmo.py | 20 +- .../nuke/plugins/publish/extract_model.py | 13 +- .../plugins/publish/extract_ouput_node.py | 2 +- .../publish/extract_review_data_lut.py | 6 +- .../publish/extract_review_data_mov.py | 6 +- .../plugins/publish/extract_slate_frame.py | 4 +- .../nuke/plugins/publish/extract_thumbnail.py | 4 +- .../plugins/publish/precollect_instances.py | 9 +- .../plugins/publish/precollect_workfile.py | 15 +- .../nuke/plugins/publish/validate_backdrop.py | 6 +- .../nuke/plugins/publish/validate_gizmo.py | 6 +- .../publish/validate_instance_in_context.py | 13 +- .../plugins/publish/validate_write_legacy.py | 5 +- .../plugins/publish/validate_write_nodes.py | 15 +- openpype/hosts/nuke/startup/init.py | 2 + openpype/hosts/nuke/startup/menu.py | 15 +- 47 files changed, 1581 insertions(+), 563 deletions(-) create mode 100644 openpype/hosts/nuke/api/command.py delete mode 100644 openpype/hosts/nuke/api/menu.py create mode 100644 openpype/hosts/nuke/api/pipeline.py create mode 100644 openpype/hosts/nuke/api/workio.py diff --git a/openpype/hosts/nuke/api/__init__.py b/openpype/hosts/nuke/api/__init__.py index 1567189ed1..d3b7f74d6d 100644 --- a/openpype/hosts/nuke/api/__init__.py +++ b/openpype/hosts/nuke/api/__init__.py @@ -1,130 +1,52 @@ -import os -import nuke +from .workio import ( + file_extensions, + has_unsaved_changes, + save_file, + open_file, + current_file, + work_root, +) -import avalon.api -import pyblish.api -import openpype -from . import lib, menu +from .command import ( + reset_frame_range, + get_handles, + reset_resolution, + viewer_update_and_undo_stop +) -log = openpype.api.Logger().get_logger(__name__) +from .plugin import OpenPypeCreator +from .pipeline import ( + install, + uninstall, -AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") -HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.nuke.__file__)) -PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") -PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") -LOAD_PATH = os.path.join(PLUGINS_DIR, "load") -CREATE_PATH = os.path.join(PLUGINS_DIR, "create") -INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") + ls, + + containerise, + parse_container, + update_container, +) -# registering pyblish gui regarding settings in presets -if os.getenv("PYBLISH_GUI", None): - pyblish.api.register_gui(os.getenv("PYBLISH_GUI", None)) +__all__ = ( + "file_extensions", + "has_unsaved_changes", + "save_file", + "open_file", + "current_file", + "work_root", + "reset_frame_range", + "get_handles", + "reset_resolution", + "viewer_update_and_undo_stop", -def reload_config(): - """Attempt to reload pipeline at run-time. + "OpenPypeCreator", + "install", + "uninstall", - CAUTION: This is primarily for development and debugging purposes. + "ls", - """ - - import importlib - - for module in ( - "{}.api".format(AVALON_CONFIG), - "{}.hosts.nuke.api.actions".format(AVALON_CONFIG), - "{}.hosts.nuke.api.menu".format(AVALON_CONFIG), - "{}.hosts.nuke.api.plugin".format(AVALON_CONFIG), - "{}.hosts.nuke.api.lib".format(AVALON_CONFIG), - ): - log.info("Reloading module: {}...".format(module)) - - module = importlib.import_module(module) - - try: - importlib.reload(module) - except AttributeError as e: - from importlib import reload - log.warning("Cannot reload module: {}".format(e)) - reload(module) - - -def install(): - ''' Installing all requarements for Nuke host - ''' - - # remove all registred callbacks form avalon.nuke - from avalon import pipeline - pipeline._registered_event_handlers.clear() - - log.info("Registering Nuke plug-ins..") - pyblish.api.register_plugin_path(PUBLISH_PATH) - avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) - avalon.api.register_plugin_path(avalon.api.InventoryAction, INVENTORY_PATH) - - # Register Avalon event for workfiles loading. - avalon.api.on("workio.open_file", lib.check_inventory_versions) - avalon.api.on("taskChanged", menu.change_context_label) - - pyblish.api.register_callback( - "instanceToggled", on_pyblish_instance_toggled) - workfile_settings = lib.WorkfileSettings() - # Disable all families except for the ones we explicitly want to see - family_states = [ - "write", - "review", - "nukenodes", - "model", - "gizmo" - ] - - avalon.api.data["familiesStateDefault"] = False - avalon.api.data["familiesStateToggled"] = family_states - - # Set context settings. - nuke.addOnCreate(workfile_settings.set_context_settings, nodeClass="Root") - nuke.addOnCreate(workfile_settings.set_favorites, nodeClass="Root") - nuke.addOnCreate(lib.process_workfile_builder, nodeClass="Root") - nuke.addOnCreate(lib.launch_workfiles_app, nodeClass="Root") - menu.install() - - -def uninstall(): - '''Uninstalling host's integration - ''' - log.info("Deregistering Nuke plug-ins..") - pyblish.api.deregister_plugin_path(PUBLISH_PATH) - avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH) - - pyblish.api.deregister_callback( - "instanceToggled", on_pyblish_instance_toggled) - - reload_config() - menu.uninstall() - - -def on_pyblish_instance_toggled(instance, old_value, new_value): - """Toggle node passthrough states on instance toggles.""" - - log.info("instance toggle: {}, old_value: {}, new_value:{} ".format( - instance, old_value, new_value)) - - from avalon.nuke import ( - viewer_update_and_undo_stop, - add_publish_knob - ) - - # Whether instances should be passthrough based on new value - - with viewer_update_and_undo_stop(): - n = instance[0] - try: - n["publish"].value() - except ValueError: - n = add_publish_knob(n) - log.info(" `Publish` knob was added to write node..") - - n["publish"].setValue(new_value) + "containerise", + "parse_container", + "update_container", +) diff --git a/openpype/hosts/nuke/api/actions.py b/openpype/hosts/nuke/api/actions.py index fd18c787c4..c4a6f0fb84 100644 --- a/openpype/hosts/nuke/api/actions.py +++ b/openpype/hosts/nuke/api/actions.py @@ -1,12 +1,11 @@ import pyblish.api -from avalon.nuke.lib import ( +from openpype.api import get_errored_instances_from_context +from .lib import ( reset_selection, select_nodes ) -from openpype.api import get_errored_instances_from_context - class SelectInvalidAction(pyblish.api.Action): """Select invalid nodes in Nuke when plug-in failed. diff --git a/openpype/hosts/nuke/api/command.py b/openpype/hosts/nuke/api/command.py new file mode 100644 index 0000000000..212d4757c6 --- /dev/null +++ b/openpype/hosts/nuke/api/command.py @@ -0,0 +1,135 @@ +import logging +import contextlib +import nuke + +from avalon import api, io + + +log = logging.getLogger(__name__) + + +def reset_frame_range(): + """ Set frame range to current asset + Also it will set a Viewer range with + displayed handles + """ + + fps = float(api.Session.get("AVALON_FPS", 25)) + + nuke.root()["fps"].setValue(fps) + name = api.Session["AVALON_ASSET"] + asset = io.find_one({"name": name, "type": "asset"}) + asset_data = asset["data"] + + handles = get_handles(asset) + + frame_start = int(asset_data.get( + "frameStart", + asset_data.get("edit_in"))) + + frame_end = int(asset_data.get( + "frameEnd", + asset_data.get("edit_out"))) + + if not all([frame_start, frame_end]): + missing = ", ".join(["frame_start", "frame_end"]) + msg = "'{}' are not set for asset '{}'!".format(missing, name) + log.warning(msg) + nuke.message(msg) + return + + frame_start -= handles + frame_end += handles + + nuke.root()["first_frame"].setValue(frame_start) + nuke.root()["last_frame"].setValue(frame_end) + + # setting active viewers + vv = nuke.activeViewer().node() + vv["frame_range_lock"].setValue(True) + vv["frame_range"].setValue("{0}-{1}".format( + int(asset_data["frameStart"]), + int(asset_data["frameEnd"])) + ) + + +def get_handles(asset): + """ Gets handles data + + Arguments: + asset (dict): avalon asset entity + + Returns: + handles (int) + """ + data = asset["data"] + if "handles" in data and data["handles"] is not None: + return int(data["handles"]) + + parent_asset = None + if "visualParent" in data: + vp = data["visualParent"] + if vp is not None: + parent_asset = io.find_one({"_id": io.ObjectId(vp)}) + + if parent_asset is None: + parent_asset = io.find_one({"_id": io.ObjectId(asset["parent"])}) + + if parent_asset is not None: + return get_handles(parent_asset) + else: + return 0 + + +def reset_resolution(): + """Set resolution to project resolution.""" + project = io.find_one({"type": "project"}) + p_data = project["data"] + + width = p_data.get("resolution_width", + p_data.get("resolutionWidth")) + height = p_data.get("resolution_height", + p_data.get("resolutionHeight")) + + if not all([width, height]): + missing = ", ".join(["width", "height"]) + msg = "No resolution information `{0}` found for '{1}'.".format( + missing, + project["name"]) + log.warning(msg) + nuke.message(msg) + return + + current_width = nuke.root()["format"].value().width() + current_height = nuke.root()["format"].value().height() + + if width != current_width or height != current_height: + + fmt = None + for f in nuke.formats(): + if f.width() == width and f.height() == height: + fmt = f.name() + + if not fmt: + nuke.addFormat( + "{0} {1} {2}".format(int(width), int(height), project["name"]) + ) + fmt = project["name"] + + nuke.root()["format"].setValue(fmt) + + +@contextlib.contextmanager +def viewer_update_and_undo_stop(): + """Lock viewer from updating and stop recording undo steps""" + try: + # stop active viewer to update any change + viewer = nuke.activeViewer() + if viewer: + viewer.stop() + else: + log.warning("No available active Viewer") + nuke.Undo.disable() + yield + finally: + nuke.Undo.enable() diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index e36a5aa5ba..0508de9f1d 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3,15 +3,15 @@ import re import sys import six import platform +import contextlib from collections import OrderedDict +import clique + +import nuke from avalon import api, io, lib -import avalon.nuke -from avalon.nuke import lib as anlib -from avalon.nuke import ( - save_file, open_file -) + from openpype.api import ( Logger, Anatomy, @@ -28,21 +28,476 @@ from openpype.lib.path_tools import HostDirmap from openpype.settings import get_project_settings from openpype.modules import ModulesManager -import nuke +from .workio import ( + save_file, + open_file +) -from .utils import set_context_favorites +log = Logger.get_logger(__name__) -log = Logger().get_logger(__name__) +_NODE_TAB_NAME = "{}".format(os.getenv("AVALON_LABEL") or "Avalon") +AVALON_LABEL = os.getenv("AVALON_LABEL") or "Avalon" +AVALON_TAB = "{}".format(AVALON_LABEL) +AVALON_DATA_GROUP = "{}DataGroup".format(AVALON_LABEL.capitalize()) +EXCLUDED_KNOB_TYPE_ON_READ = ( + 20, # Tab Knob + 26, # Text Knob (But for backward compatibility, still be read + # if value is not an empty string.) +) -opnl = sys.modules[__name__] -opnl._project = None -opnl.project_name = os.getenv("AVALON_PROJECT") -opnl.workfiles_launched = False -opnl._node_tab_name = "{}".format(os.getenv("AVALON_LABEL") or "Avalon") + +class Context: + main_window = None + context_label = None + project_name = os.getenv("AVALON_PROJECT") + workfiles_launched = False + # Seems unused + _project_doc = None + + +class Knobby(object): + """For creating knob which it's type isn't mapped in `create_knobs` + + Args: + type (string): Nuke knob type name + value: Value to be set with `Knob.setValue`, put `None` if not required + flags (list, optional): Knob flags to be set with `Knob.setFlag` + *args: Args other than knob name for initializing knob class + + """ + + def __init__(self, type, value, flags=None, *args): + self.type = type + self.value = value + self.flags = flags or [] + self.args = args + + def create(self, name, nice=None): + knob_cls = getattr(nuke, self.type) + knob = knob_cls(name, nice, *self.args) + if self.value is not None: + knob.setValue(self.value) + for flag in self.flags: + knob.setFlag(flag) + return knob + + +def create_knobs(data, tab=None): + """Create knobs by data + + Depending on the type of each dict value and creates the correct Knob. + + Mapped types: + bool: nuke.Boolean_Knob + int: nuke.Int_Knob + float: nuke.Double_Knob + list: nuke.Enumeration_Knob + six.string_types: nuke.String_Knob + + dict: If it's a nested dict (all values are dict), will turn into + A tabs group. Or just a knobs group. + + Args: + data (dict): collection of attributes and their value + tab (string, optional): Knobs' tab name + + Returns: + list: A list of `nuke.Knob` objects + + """ + def nice_naming(key): + """Convert camelCase name into UI Display Name""" + words = re.findall('[A-Z][^A-Z]*', key[0].upper() + key[1:]) + return " ".join(words) + + # Turn key-value pairs into knobs + knobs = list() + + if tab: + knobs.append(nuke.Tab_Knob(tab)) + + for key, value in data.items(): + # Knob name + if isinstance(key, tuple): + name, nice = key + else: + name, nice = key, nice_naming(key) + + # Create knob by value type + if isinstance(value, Knobby): + knobby = value + knob = knobby.create(name, nice) + + elif isinstance(value, float): + knob = nuke.Double_Knob(name, nice) + knob.setValue(value) + + elif isinstance(value, bool): + knob = nuke.Boolean_Knob(name, nice) + knob.setValue(value) + knob.setFlag(nuke.STARTLINE) + + elif isinstance(value, int): + knob = nuke.Int_Knob(name, nice) + knob.setValue(value) + + elif isinstance(value, six.string_types): + knob = nuke.String_Knob(name, nice) + knob.setValue(value) + + elif isinstance(value, list): + knob = nuke.Enumeration_Knob(name, nice, value) + + elif isinstance(value, dict): + if all(isinstance(v, dict) for v in value.values()): + # Create a group of tabs + begain = nuke.BeginTabGroup_Knob() + end = nuke.EndTabGroup_Knob() + begain.setName(name) + end.setName(name + "_End") + knobs.append(begain) + for k, v in value.items(): + knobs += create_knobs(v, tab=k) + knobs.append(end) + else: + # Create a group of knobs + knobs.append(nuke.Tab_Knob( + name, nice, nuke.TABBEGINCLOSEDGROUP)) + knobs += create_knobs(value) + knobs.append( + nuke.Tab_Knob(name + "_End", nice, nuke.TABENDGROUP)) + continue + + else: + raise TypeError("Unsupported type: %r" % type(value)) + + knobs.append(knob) + + return knobs + + +def imprint(node, data, tab=None): + """Store attributes with value on node + + Parse user data into Node knobs. + Use `collections.OrderedDict` to ensure knob order. + + Args: + node(nuke.Node): node object from Nuke + data(dict): collection of attributes and their value + + Returns: + None + + Examples: + ``` + import nuke + from avalon.nuke import lib + + node = nuke.createNode("NoOp") + data = { + # Regular type of attributes + "myList": ["x", "y", "z"], + "myBool": True, + "myFloat": 0.1, + "myInt": 5, + + # Creating non-default imprint type of knob + "MyFilePath": lib.Knobby("File_Knob", "/file/path"), + "divider": lib.Knobby("Text_Knob", ""), + + # Manual nice knob naming + ("my_knob", "Nice Knob Name"): "some text", + + # dict type will be created as knob group + "KnobGroup": { + "knob1": 5, + "knob2": "hello", + "knob3": ["a", "b"], + }, + + # Nested dict will be created as tab group + "TabGroup": { + "tab1": {"count": 5}, + "tab2": {"isGood": True}, + "tab3": {"direction": ["Left", "Right"]}, + }, + } + lib.imprint(node, data, tab="Demo") + + ``` + + """ + for knob in create_knobs(data, tab): + node.addKnob(knob) + + +def add_publish_knob(node): + """Add Publish knob to node + + Arguments: + node (nuke.Node): nuke node to be processed + + Returns: + node (nuke.Node): processed nuke node + + """ + if "publish" not in node.knobs(): + body = OrderedDict() + body[("divd", "Publishing")] = Knobby("Text_Knob", '') + body["publish"] = True + imprint(node, body) + return node + + +def set_avalon_knob_data(node, data=None, prefix="avalon:"): + """ Sets data into nodes's avalon knob + + Arguments: + node (nuke.Node): Nuke node to imprint with data, + data (dict, optional): Data to be imprinted into AvalonTab + prefix (str, optional): filtering prefix + + Returns: + node (nuke.Node) + + Examples: + data = { + 'asset': 'sq020sh0280', + 'family': 'render', + 'subset': 'subsetMain' + } + """ + data = data or dict() + create = OrderedDict() + + tab_name = AVALON_TAB + editable = ["asset", "subset", "name", "namespace"] + + existed_knobs = node.knobs() + + for key, value in data.items(): + knob_name = prefix + key + gui_name = key + + if knob_name in existed_knobs: + # Set value + try: + node[knob_name].setValue(value) + except TypeError: + node[knob_name].setValue(str(value)) + else: + # New knob + name = (knob_name, gui_name) # Hide prefix on GUI + if key in editable: + create[name] = value + else: + create[name] = Knobby("String_Knob", + str(value), + flags=[nuke.READ_ONLY]) + if tab_name in existed_knobs: + tab_name = None + else: + tab = OrderedDict() + warn = Knobby("Text_Knob", "Warning! Do not change following data!") + divd = Knobby("Text_Knob", "") + head = [ + (("warn", ""), warn), + (("divd", ""), divd), + ] + tab[AVALON_DATA_GROUP] = OrderedDict(head + list(create.items())) + create = tab + + imprint(node, create, tab=tab_name) + return node + + +def get_avalon_knob_data(node, prefix="avalon:"): + """ Gets a data from nodes's avalon knob + + Arguments: + node (obj): Nuke node to search for data, + prefix (str, optional): filtering prefix + + Returns: + data (dict) + """ + + # check if lists + if not isinstance(prefix, list): + prefix = list([prefix]) + + data = dict() + + # loop prefix + for p in prefix: + # check if the node is avalon tracked + if AVALON_TAB not in node.knobs(): + continue + try: + # check if data available on the node + test = node[AVALON_DATA_GROUP].value() + log.debug("Only testing if data avalable: `{}`".format(test)) + except NameError as e: + # if it doesn't then create it + log.debug("Creating avalon knob: `{}`".format(e)) + node = set_avalon_knob_data(node) + return get_avalon_knob_data(node) + + # get data from filtered knobs + data.update({k.replace(p, ''): node[k].value() + for k in node.knobs().keys() + if p in k}) + + return data + + +def fix_data_for_node_create(data): + """Fixing data to be used for nuke knobs + """ + for k, v in data.items(): + if isinstance(v, six.text_type): + data[k] = str(v) + if str(v).startswith("0x"): + data[k] = int(v, 16) + return data + + +def add_write_node(name, **kwarg): + """Adding nuke write node + + Arguments: + name (str): nuke node name + kwarg (attrs): data for nuke knobs + + Returns: + node (obj): nuke write node + """ + frame_range = kwarg.get("frame_range", None) + + w = nuke.createNode( + "Write", + "name {}".format(name)) + + w["file"].setValue(kwarg["file"]) + + for k, v in kwarg.items(): + if "frame_range" in k: + continue + log.info([k, v]) + try: + w[k].setValue(v) + except KeyError as e: + log.debug(e) + continue + + if frame_range: + w["use_limit"].setValue(True) + w["first"].setValue(frame_range[0]) + w["last"].setValue(frame_range[1]) + + return w + + +def read(node): + """Return user-defined knobs from given `node` + + Args: + node (nuke.Node): Nuke node object + + Returns: + list: A list of nuke.Knob object + + """ + def compat_prefixed(knob_name): + if knob_name.startswith("avalon:"): + return knob_name[len("avalon:"):] + elif knob_name.startswith("ak:"): + return knob_name[len("ak:"):] + else: + return knob_name + + data = dict() + + pattern = ("(?<=addUserKnob {)" + "([0-9]*) (\\S*)" # Matching knob type and knob name + "(?=[ |}])") + tcl_script = node.writeKnobs(nuke.WRITE_USER_KNOB_DEFS) + result = re.search(pattern, tcl_script) + + if result: + first_user_knob = result.group(2) + # Collect user knobs from the end of the knob list + for knob in reversed(node.allKnobs()): + knob_name = knob.name() + if not knob_name: + # Ignore unnamed knob + continue + + knob_type = nuke.knob(knob.fullyQualifiedName(), type=True) + value = knob.value() + + if ( + knob_type not in EXCLUDED_KNOB_TYPE_ON_READ or + # For compating read-only string data that imprinted + # by `nuke.Text_Knob`. + (knob_type == 26 and value) + ): + key = compat_prefixed(knob_name) + data[key] = value + + if knob_name == first_user_knob: + break + + return data + + +def get_node_path(path, padding=4): + """Get filename for the Nuke write with padded number as '#' + + Arguments: + path (str): The path to render to. + + Returns: + tuple: head, padding, tail (extension) + + Examples: + >>> get_frame_path("test.exr") + ('test', 4, '.exr') + + >>> get_frame_path("filename.#####.tif") + ('filename.', 5, '.tif') + + >>> get_frame_path("foobar##.tif") + ('foobar', 2, '.tif') + + >>> get_frame_path("foobar_%08d.tif") + ('foobar_', 8, '.tif') + """ + filename, ext = os.path.splitext(path) + + # Find a final number group + if '%' in filename: + match = re.match('.*?(%[0-9]+d)$', filename) + if match: + padding = int(match.group(1).replace('%', '').replace('d', '')) + # remove number from end since fusion + # will swap it with the frame number + filename = filename.replace(match.group(1), '') + elif '#' in filename: + match = re.match('.*?(#+)$', filename) + + if match: + padding = len(match.group(1)) + # remove number from end since fusion + # will swap it with the frame number + filename = filename.replace(match.group(1), '') + + return filename, padding, ext def get_nuke_imageio_settings(): - return get_anatomy_settings(opnl.project_name)["imageio"]["nuke"] + return get_anatomy_settings(Context.project_name)["imageio"]["nuke"] def get_created_node_imageio_setting(**kwarg): @@ -103,14 +558,15 @@ def check_inventory_versions(): and check if the node is having actual version. If not then it will color it to red. """ + from .pipeline import parse_container + # get all Loader nodes by avalon attribute metadata for each in nuke.allNodes(): - container = avalon.nuke.parse_container(each) + container = parse_container(each) if container: node = nuke.toNode(container["objectName"]) - avalon_knob_data = avalon.nuke.read( - node) + avalon_knob_data = read(node) # get representation from io representation = io.find_one({ @@ -163,11 +619,10 @@ def writes_version_sync(): for each in nuke.allNodes(filter="Write"): # check if the node is avalon tracked - if opnl._node_tab_name not in each.knobs(): + if _NODE_TAB_NAME not in each.knobs(): continue - avalon_knob_data = avalon.nuke.read( - each) + avalon_knob_data = read(each) try: if avalon_knob_data['families'] not in ["render"]: @@ -209,14 +664,14 @@ def check_subsetname_exists(nodes, subset_name): bool: True of False """ return next((True for n in nodes - if subset_name in avalon.nuke.read(n).get("subset", "")), + if subset_name in read(n).get("subset", "")), False) def get_render_path(node): ''' Generate Render path from presets regarding avalon knob data ''' - data = {'avalon': avalon.nuke.read(node)} + data = {'avalon': read(node)} data_preset = { "nodeclass": data['avalon']['family'], "families": [data['avalon']['families']], @@ -385,7 +840,7 @@ def create_write_node(name, data, input=None, prenodes=None, for knob in imageio_writes["knobs"]: _data.update({knob["name"]: knob["value"]}) - _data = anlib.fix_data_for_node_create(_data) + _data = fix_data_for_node_create(_data) log.debug("_data: `{}`".format(_data)) @@ -466,7 +921,7 @@ def create_write_node(name, data, input=None, prenodes=None, prev_node = now_node # creating write node - write_node = now_node = anlib.add_write_node( + write_node = now_node = add_write_node( "inside_{}".format(name), **_data ) @@ -484,8 +939,8 @@ def create_write_node(name, data, input=None, prenodes=None, now_node.setInput(0, prev_node) # imprinting group node - anlib.set_avalon_knob_data(GN, data["avalon"]) - anlib.add_publish_knob(GN) + set_avalon_knob_data(GN, data["avalon"]) + add_publish_knob(GN) add_rendering_knobs(GN, farm) if review: @@ -537,7 +992,7 @@ def create_write_node(name, data, input=None, prenodes=None, add_deadline_tab(GN) # open the our Tab as default - GN[opnl._node_tab_name].setFlag(0) + GN[_NODE_TAB_NAME].setFlag(0) # set tile color tile_color = _data.get("tile_color", "0xff0000ff") @@ -663,7 +1118,7 @@ class WorkfileSettings(object): root_node=None, nodes=None, **kwargs): - opnl._project = kwargs.get( + Context._project_doc = kwargs.get( "project") or io.find_one({"type": "project"}) self._asset = kwargs.get("asset_name") or api.Session["AVALON_ASSET"] self._asset_entity = get_asset(self._asset) @@ -804,8 +1259,6 @@ class WorkfileSettings(object): ''' Adds correct colorspace to write node dict ''' - from avalon.nuke import read - for node in nuke.allNodes(filter="Group"): # get data from avalon knob @@ -1005,7 +1458,7 @@ class WorkfileSettings(object): node['frame_range_lock'].setValue(True) # adding handle_start/end to root avalon knob - if not anlib.set_avalon_knob_data(self._root_node, { + if not set_avalon_knob_data(self._root_node, { "handleStart": int(handle_start), "handleEnd": int(handle_end) }): @@ -1089,6 +1542,8 @@ class WorkfileSettings(object): self.set_colorspace() def set_favorites(self): + from .utils import set_context_favorites + work_dir = os.getenv("AVALON_WORKDIR") asset = os.getenv("AVALON_ASSET") favorite_items = OrderedDict() @@ -1096,9 +1551,9 @@ class WorkfileSettings(object): # project # get project's root and split to parts projects_root = os.path.normpath(work_dir.split( - opnl.project_name)[0]) + Context.project_name)[0]) # add project name - project_dir = os.path.join(projects_root, opnl.project_name) + "/" + project_dir = os.path.join(projects_root, Context.project_name) + "/" # add to favorites favorite_items.update({"Project dir": project_dir.replace("\\", "/")}) @@ -1145,8 +1600,7 @@ def get_write_node_template_attr(node): ''' # get avalon data from node data = dict() - data['avalon'] = avalon.nuke.read( - node) + data['avalon'] = read(node) data_preset = { "nodeclass": data['avalon']['family'], "families": [data['avalon']['families']], @@ -1167,7 +1621,7 @@ def get_write_node_template_attr(node): if k not in ["_id", "_previous"]} # fix badly encoded data - return anlib.fix_data_for_node_create(correct_data) + return fix_data_for_node_create(correct_data) def get_dependent_nodes(nodes): @@ -1274,13 +1728,53 @@ def find_free_space_to_paste_nodes( return xpos, ypos +@contextlib.contextmanager +def maintained_selection(): + """Maintain selection during context + + Example: + >>> with maintained_selection(): + ... node['selected'].setValue(True) + >>> print(node['selected'].value()) + False + """ + previous_selection = nuke.selectedNodes() + try: + yield + finally: + # unselect all selection in case there is some + current_seletion = nuke.selectedNodes() + [n['selected'].setValue(False) for n in current_seletion] + # and select all previously selected nodes + if previous_selection: + [n['selected'].setValue(True) for n in previous_selection] + + +def reset_selection(): + """Deselect all selected nodes""" + for node in nuke.selectedNodes(): + node["selected"].setValue(False) + + +def select_nodes(nodes): + """Selects all inputed nodes + + Arguments: + nodes (list): nuke nodes to be selected + """ + assert isinstance(nodes, (list, tuple)), "nodes has to be list or tuple" + + for node in nodes: + node["selected"].setValue(True) + + def launch_workfiles_app(): '''Function letting start workfiles after start of host ''' from openpype.lib import ( env_value_to_bool ) - from avalon.nuke.pipeline import get_main_window + from .pipeline import get_main_window # get all imortant settings open_at_start = env_value_to_bool( @@ -1291,8 +1785,8 @@ def launch_workfiles_app(): if not open_at_start: return - if not opnl.workfiles_launched: - opnl.workfiles_launched = True + if not Context.workfiles_launched: + Context.workfiles_launched = True main_window = get_main_window() host_tools.show_workfiles(parent=main_window) @@ -1378,7 +1872,7 @@ def recreate_instance(origin_node, avalon_data=None): knobs_wl = ["render", "publish", "review", "ypos", "use_limit", "first", "last"] # get data from avalon knobs - data = anlib.get_avalon_knob_data( + data = get_avalon_knob_data( origin_node) # add input data to avalon data @@ -1494,3 +1988,45 @@ def dirmap_file_name_filter(file_name): if os.path.exists(dirmap_processor.file_name): return dirmap_processor.file_name return file_name + + +# ------------------------------------ +# This function seems to be deprecated +# ------------------------------------ +def ls_img_sequence(path): + """Listing all available coherent image sequence from path + + Arguments: + path (str): A nuke's node object + + Returns: + data (dict): with nuke formated path and frameranges + """ + file = os.path.basename(path) + dirpath = os.path.dirname(path) + base, ext = os.path.splitext(file) + name, padding = os.path.splitext(base) + + # populate list of files + files = [ + f for f in os.listdir(dirpath) + if name in f + if ext in f + ] + + # create collection from list of files + collections, reminder = clique.assemble(files) + + if len(collections) > 0: + head = collections[0].format("{head}") + padding = collections[0].format("{padding}") % 1 + padding = "#" * len(padding) + tail = collections[0].format("{tail}") + file = head + padding + tail + + return { + "path": os.path.join(dirpath, file).replace("\\", "/"), + "frames": collections[0].format("[{ranges}]") + } + + return False diff --git a/openpype/hosts/nuke/api/menu.py b/openpype/hosts/nuke/api/menu.py deleted file mode 100644 index 86293edb99..0000000000 --- a/openpype/hosts/nuke/api/menu.py +++ /dev/null @@ -1,166 +0,0 @@ -import os -import nuke -from avalon.nuke.pipeline import get_main_window - -from .lib import WorkfileSettings -from openpype.api import Logger, BuildWorkfile, get_current_project_settings -from openpype.tools.utils import host_tools - - -log = Logger().get_logger(__name__) - -menu_label = os.environ["AVALON_LABEL"] -context_label = None - - -def change_context_label(*args): - global context_label - menubar = nuke.menu("Nuke") - menu = menubar.findItem(menu_label) - - label = "{0}, {1}".format( - os.environ["AVALON_ASSET"], os.environ["AVALON_TASK"] - ) - - rm_item = [ - (i, item) for i, item in enumerate(menu.items()) - if context_label in item.name() - ][0] - - menu.removeItem(rm_item[1].name()) - - context_action = menu.addCommand( - label, - index=(rm_item[0]) - ) - context_action.setEnabled(False) - - log.info("Task label changed from `{}` to `{}`".format( - context_label, label)) - - context_label = label - - - -def install(): - from openpype.hosts.nuke.api import reload_config - - global context_label - - # uninstall original avalon menu - uninstall() - - main_window = get_main_window() - menubar = nuke.menu("Nuke") - menu = menubar.addMenu(menu_label) - - label = "{0}, {1}".format( - os.environ["AVALON_ASSET"], os.environ["AVALON_TASK"] - ) - context_label = label - context_action = menu.addCommand(label) - context_action.setEnabled(False) - - menu.addSeparator() - menu.addCommand( - "Work Files...", - lambda: host_tools.show_workfiles(parent=main_window) - ) - - menu.addSeparator() - menu.addCommand( - "Create...", - lambda: host_tools.show_creator(parent=main_window) - ) - menu.addCommand( - "Load...", - lambda: host_tools.show_loader( - parent=main_window, - use_context=True - ) - ) - menu.addCommand( - "Publish...", - lambda: host_tools.show_publish(parent=main_window) - ) - menu.addCommand( - "Manage...", - lambda: host_tools.show_scene_inventory(parent=main_window) - ) - - menu.addSeparator() - menu.addCommand( - "Set Resolution", - lambda: WorkfileSettings().reset_resolution() - ) - menu.addCommand( - "Set Frame Range", - lambda: WorkfileSettings().reset_frame_range_handles() - ) - menu.addCommand( - "Set Colorspace", - lambda: WorkfileSettings().set_colorspace() - ) - menu.addCommand( - "Apply All Settings", - lambda: WorkfileSettings().set_context_settings() - ) - - menu.addSeparator() - menu.addCommand( - "Build Workfile", - lambda: BuildWorkfile().process() - ) - - menu.addSeparator() - menu.addCommand( - "Experimental tools...", - lambda: host_tools.show_experimental_tools_dialog(parent=main_window) - ) - - # add reload pipeline only in debug mode - if bool(os.getenv("NUKE_DEBUG")): - menu.addSeparator() - menu.addCommand("Reload Pipeline", reload_config) - - # adding shortcuts - add_shortcuts_from_presets() - - -def uninstall(): - - menubar = nuke.menu("Nuke") - menu = menubar.findItem(menu_label) - - for item in menu.items(): - log.info("Removing menu item: {}".format(item.name())) - menu.removeItem(item.name()) - - -def add_shortcuts_from_presets(): - menubar = nuke.menu("Nuke") - nuke_presets = get_current_project_settings()["nuke"]["general"] - - if nuke_presets.get("menu"): - menu_label_mapping = { - "manage": "Manage...", - "create": "Create...", - "load": "Load...", - "build_workfile": "Build Workfile", - "publish": "Publish..." - } - - for command_name, shortcut_str in nuke_presets.get("menu").items(): - log.info("menu_name `{}` | menu_label `{}`".format( - command_name, menu_label - )) - log.info("Adding Shortcut `{}` to `{}`".format( - shortcut_str, command_name - )) - try: - menu = menubar.findItem(menu_label) - item_label = menu_label_mapping[command_name] - menuitem = menu.findItem(item_label) - menuitem.setShortcut(shortcut_str) - except AttributeError as e: - log.error(e) diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py new file mode 100644 index 0000000000..c47187666b --- /dev/null +++ b/openpype/hosts/nuke/api/pipeline.py @@ -0,0 +1,421 @@ +import os +import importlib +from collections import OrderedDict + +import nuke + +import pyblish.api +import avalon.api +from avalon import pipeline + +import openpype +from openpype.api import ( + Logger, + BuildWorkfile, + get_current_project_settings +) +from openpype.tools.utils import host_tools + +from .command import viewer_update_and_undo_stop +from .lib import ( + add_publish_knob, + WorkfileSettings, + process_workfile_builder, + launch_workfiles_app, + check_inventory_versions, + set_avalon_knob_data, + read, + Context +) + +log = Logger.get_logger(__name__) + +AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype") +HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.nuke.__file__)) +PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") +PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") +LOAD_PATH = os.path.join(PLUGINS_DIR, "load") +CREATE_PATH = os.path.join(PLUGINS_DIR, "create") +INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") + +MENU_LABEL = os.environ["AVALON_LABEL"] + + +# registering pyblish gui regarding settings in presets +if os.getenv("PYBLISH_GUI", None): + pyblish.api.register_gui(os.getenv("PYBLISH_GUI", None)) + + +def get_main_window(): + """Acquire Nuke's main window""" + if Context.main_window is None: + from Qt import QtWidgets + + top_widgets = QtWidgets.QApplication.topLevelWidgets() + name = "Foundry::UI::DockMainWindow" + for widget in top_widgets: + if ( + widget.inherits("QMainWindow") + and widget.metaObject().className() == name + ): + Context.main_window = widget + break + return Context.main_window + + +def reload_config(): + """Attempt to reload pipeline at run-time. + + CAUTION: This is primarily for development and debugging purposes. + + """ + + for module in ( + "{}.api".format(AVALON_CONFIG), + "{}.hosts.nuke.api.actions".format(AVALON_CONFIG), + "{}.hosts.nuke.api.menu".format(AVALON_CONFIG), + "{}.hosts.nuke.api.plugin".format(AVALON_CONFIG), + "{}.hosts.nuke.api.lib".format(AVALON_CONFIG), + ): + log.info("Reloading module: {}...".format(module)) + + module = importlib.import_module(module) + + try: + importlib.reload(module) + except AttributeError as e: + from importlib import reload + log.warning("Cannot reload module: {}".format(e)) + reload(module) + + +def install(): + ''' Installing all requarements for Nuke host + ''' + + pyblish.api.register_host("nuke") + + log.info("Registering Nuke plug-ins..") + pyblish.api.register_plugin_path(PUBLISH_PATH) + avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) + avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.register_plugin_path(avalon.api.InventoryAction, INVENTORY_PATH) + + # Register Avalon event for workfiles loading. + avalon.api.on("workio.open_file", check_inventory_versions) + avalon.api.on("taskChanged", change_context_label) + + pyblish.api.register_callback( + "instanceToggled", on_pyblish_instance_toggled) + workfile_settings = WorkfileSettings() + # Disable all families except for the ones we explicitly want to see + family_states = [ + "write", + "review", + "nukenodes", + "model", + "gizmo" + ] + + avalon.api.data["familiesStateDefault"] = False + avalon.api.data["familiesStateToggled"] = family_states + + # Set context settings. + nuke.addOnCreate(workfile_settings.set_context_settings, nodeClass="Root") + nuke.addOnCreate(workfile_settings.set_favorites, nodeClass="Root") + nuke.addOnCreate(process_workfile_builder, nodeClass="Root") + nuke.addOnCreate(launch_workfiles_app, nodeClass="Root") + _install_menu() + + +def uninstall(): + '''Uninstalling host's integration + ''' + log.info("Deregistering Nuke plug-ins..") + pyblish.deregister_host("nuke") + pyblish.api.deregister_plugin_path(PUBLISH_PATH) + avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH) + avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH) + + pyblish.api.deregister_callback( + "instanceToggled", on_pyblish_instance_toggled) + + reload_config() + _uninstall_menu() + + +def _install_menu(): + # uninstall original avalon menu + main_window = get_main_window() + menubar = nuke.menu("Nuke") + menu = menubar.addMenu(MENU_LABEL) + + label = "{0}, {1}".format( + os.environ["AVALON_ASSET"], os.environ["AVALON_TASK"] + ) + Context.context_label = label + context_action = menu.addCommand(label) + context_action.setEnabled(False) + + menu.addSeparator() + menu.addCommand( + "Work Files...", + lambda: host_tools.show_workfiles(parent=main_window) + ) + + menu.addSeparator() + menu.addCommand( + "Create...", + lambda: host_tools.show_creator(parent=main_window) + ) + menu.addCommand( + "Load...", + lambda: host_tools.show_loader( + parent=main_window, + use_context=True + ) + ) + menu.addCommand( + "Publish...", + lambda: host_tools.show_publish(parent=main_window) + ) + menu.addCommand( + "Manage...", + lambda: host_tools.show_scene_inventory(parent=main_window) + ) + + menu.addSeparator() + menu.addCommand( + "Set Resolution", + lambda: WorkfileSettings().reset_resolution() + ) + menu.addCommand( + "Set Frame Range", + lambda: WorkfileSettings().reset_frame_range_handles() + ) + menu.addCommand( + "Set Colorspace", + lambda: WorkfileSettings().set_colorspace() + ) + menu.addCommand( + "Apply All Settings", + lambda: WorkfileSettings().set_context_settings() + ) + + menu.addSeparator() + menu.addCommand( + "Build Workfile", + lambda: BuildWorkfile().process() + ) + + menu.addSeparator() + menu.addCommand( + "Experimental tools...", + lambda: host_tools.show_experimental_tools_dialog(parent=main_window) + ) + + # add reload pipeline only in debug mode + if bool(os.getenv("NUKE_DEBUG")): + menu.addSeparator() + menu.addCommand("Reload Pipeline", reload_config) + + # adding shortcuts + add_shortcuts_from_presets() + + +def _uninstall_menu(): + menubar = nuke.menu("Nuke") + menu = menubar.findItem(MENU_LABEL) + + for item in menu.items(): + log.info("Removing menu item: {}".format(item.name())) + menu.removeItem(item.name()) + + +def change_context_label(*args): + menubar = nuke.menu("Nuke") + menu = menubar.findItem(MENU_LABEL) + + label = "{0}, {1}".format( + os.environ["AVALON_ASSET"], os.environ["AVALON_TASK"] + ) + + rm_item = [ + (i, item) for i, item in enumerate(menu.items()) + if Context.context_label in item.name() + ][0] + + menu.removeItem(rm_item[1].name()) + + context_action = menu.addCommand( + label, + index=(rm_item[0]) + ) + context_action.setEnabled(False) + + log.info("Task label changed from `{}` to `{}`".format( + Context.context_label, label)) + + +def add_shortcuts_from_presets(): + menubar = nuke.menu("Nuke") + nuke_presets = get_current_project_settings()["nuke"]["general"] + + if nuke_presets.get("menu"): + menu_label_mapping = { + "manage": "Manage...", + "create": "Create...", + "load": "Load...", + "build_workfile": "Build Workfile", + "publish": "Publish..." + } + + for command_name, shortcut_str in nuke_presets.get("menu").items(): + log.info("menu_name `{}` | menu_label `{}`".format( + command_name, MENU_LABEL + )) + log.info("Adding Shortcut `{}` to `{}`".format( + shortcut_str, command_name + )) + try: + menu = menubar.findItem(MENU_LABEL) + item_label = menu_label_mapping[command_name] + menuitem = menu.findItem(item_label) + menuitem.setShortcut(shortcut_str) + except AttributeError as e: + log.error(e) + + +def on_pyblish_instance_toggled(instance, old_value, new_value): + """Toggle node passthrough states on instance toggles.""" + + log.info("instance toggle: {}, old_value: {}, new_value:{} ".format( + instance, old_value, new_value)) + + # Whether instances should be passthrough based on new value + + with viewer_update_and_undo_stop(): + n = instance[0] + try: + n["publish"].value() + except ValueError: + n = add_publish_knob(n) + log.info(" `Publish` knob was added to write node..") + + n["publish"].setValue(new_value) + + +def containerise(node, + name, + namespace, + context, + loader=None, + data=None): + """Bundle `node` into an assembly and imprint it with metadata + + Containerisation enables a tracking of version, author and origin + for loaded assets. + + Arguments: + node (nuke.Node): Nuke's node object to imprint as container + name (str): Name of resulting assembly + namespace (str): Namespace under which to host container + context (dict): Asset information + loader (str, optional): Name of node used to produce this container. + + Returns: + node (nuke.Node): containerised nuke's node object + + """ + data = OrderedDict( + [ + ("schema", "openpype:container-2.0"), + ("id", pipeline.AVALON_CONTAINER_ID), + ("name", name), + ("namespace", namespace), + ("loader", str(loader)), + ("representation", context["representation"]["_id"]), + ], + + **data or dict() + ) + + set_avalon_knob_data(node, data) + + return node + + +def parse_container(node): + """Returns containerised data of a node + + Reads the imprinted data from `containerise`. + + Arguments: + node (nuke.Node): Nuke's node object to read imprinted data + + Returns: + dict: The container schema data for this container node. + + """ + data = read(node) + + # (TODO) Remove key validation when `ls` has re-implemented. + # + # If not all required data return the empty container + required = ["schema", "id", "name", + "namespace", "loader", "representation"] + if not all(key in data for key in required): + return + + # Store the node's name + data["objectName"] = node["name"].value() + + return data + + +def update_container(node, keys=None): + """Returns node with updateted containder data + + Arguments: + node (nuke.Node): The node in Nuke to imprint as container, + keys (dict, optional): data which should be updated + + Returns: + node (nuke.Node): nuke node with updated container data + + Raises: + TypeError on given an invalid container node + + """ + keys = keys or dict() + + container = parse_container(node) + if not container: + raise TypeError("Not a valid container node.") + + container.update(keys) + node = set_avalon_knob_data(node, container) + + return node + + +def ls(): + """List available containers. + + This function is used by the Container Manager in Nuke. You'll + need to implement a for-loop that then *yields* one Container at + a time. + + See the `container.json` schema for details on how it should look, + and the Maya equivalent, which is in `avalon.maya.pipeline` + """ + all_nodes = nuke.allNodes(recurseGroups=False) + + # TODO: add readgeo, readcamera, readimage + nodes = [n for n in all_nodes] + + for n in nodes: + log.debug("name: `{}`".format(n.name())) + container = parse_container(n) + if container: + yield container diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 82299dd354..66b42f7bb1 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -2,23 +2,30 @@ import os import random import string -import avalon.nuke -from avalon.nuke import lib as anlib -from avalon import api +import nuke + +import avalon.api from openpype.api import ( get_current_project_settings, PypeCreatorMixin ) -from .lib import check_subsetname_exists -import nuke +from .lib import ( + Knobby, + check_subsetname_exists, + reset_selection, + maintained_selection, + set_avalon_knob_data, + add_publish_knob +) -class PypeCreator(PypeCreatorMixin, avalon.nuke.pipeline.Creator): - """Pype Nuke Creator class wrapper - """ +class OpenPypeCreator(PypeCreatorMixin, avalon.api.Creator): + """Pype Nuke Creator class wrapper""" + node_color = "0xdfea5dff" + def __init__(self, *args, **kwargs): - super(PypeCreator, self).__init__(*args, **kwargs) + super(OpenPypeCreator, self).__init__(*args, **kwargs) self.presets = get_current_project_settings()["nuke"]["create"].get( self.__class__.__name__, {} ) @@ -31,6 +38,38 @@ class PypeCreator(PypeCreatorMixin, avalon.nuke.pipeline.Creator): raise NameError("`{0}: {1}".format(__name__, msg)) return + def process(self): + from nukescripts import autoBackdrop + + instance = None + + if (self.options or {}).get("useSelection"): + + nodes = nuke.selectedNodes() + if not nodes: + nuke.message("Please select nodes that you " + "wish to add to a container") + return + + elif len(nodes) == 1: + # only one node is selected + instance = nodes[0] + + if not instance: + # Not using selection or multiple nodes selected + bckd_node = autoBackdrop() + bckd_node["tile_color"].setValue(int(self.node_color, 16)) + bckd_node["note_font_size"].setValue(24) + bckd_node["label"].setValue("[{}]".format(self.name)) + + instance = bckd_node + + # add avalon knobs + set_avalon_knob_data(instance, self.data) + add_publish_knob(instance) + + return instance + def get_review_presets_config(): settings = get_current_project_settings() @@ -48,7 +87,7 @@ def get_review_presets_config(): return [str(name) for name, _prop in outputs.items()] -class NukeLoader(api.Loader): +class NukeLoader(avalon.api.Loader): container_id_knob = "containerId" container_id = None @@ -74,7 +113,7 @@ class NukeLoader(api.Loader): node[self.container_id_knob].setValue(source_id) else: HIDEN_FLAG = 0x00040000 - _knob = anlib.Knobby( + _knob = Knobby( "String_Knob", self.container_id, flags=[ @@ -183,7 +222,7 @@ class ExporterReview(object): Returns: nuke.Node: copy node of Input Process node """ - anlib.reset_selection() + reset_selection() ipn_orig = None for v in nuke.allNodes(filter="Viewer"): ip = v["input_process"].getValue() @@ -196,7 +235,7 @@ class ExporterReview(object): # copy selected to clipboard nuke.nodeCopy("%clipboard%") # reset selection - anlib.reset_selection() + reset_selection() # paste node and selection is on it only nuke.nodePaste("%clipboard%") # assign to variable @@ -396,7 +435,7 @@ class ExporterReviewMov(ExporterReview): def save_file(self): import shutil - with anlib.maintained_selection(): + with maintained_selection(): self.log.info("Saving nodes as file... ") # create nk path path = os.path.splitext(self.path)[0] + ".nk" diff --git a/openpype/hosts/nuke/api/utils.py b/openpype/hosts/nuke/api/utils.py index e43c11a380..f8f248357b 100644 --- a/openpype/hosts/nuke/api/utils.py +++ b/openpype/hosts/nuke/api/utils.py @@ -1,7 +1,8 @@ import os import nuke -from avalon.nuke import lib as anlib + from openpype.api import resources +from .lib import maintained_selection def set_context_favorites(favorites=None): @@ -55,7 +56,7 @@ def bake_gizmos_recursively(in_group=nuke.Root()): is_group (nuke.Node)[optonal]: group node or all nodes """ # preserve selection after all is done - with anlib.maintained_selection(): + with maintained_selection(): # jump to the group with in_group: for node in nuke.allNodes(): diff --git a/openpype/hosts/nuke/api/workio.py b/openpype/hosts/nuke/api/workio.py new file mode 100644 index 0000000000..dbc24fdc9b --- /dev/null +++ b/openpype/hosts/nuke/api/workio.py @@ -0,0 +1,55 @@ +"""Host API required Work Files tool""" +import os +import nuke +import avalon.api + + +def file_extensions(): + return avalon.api.HOST_WORKFILE_EXTENSIONS["nuke"] + + +def has_unsaved_changes(): + return nuke.root().modified() + + +def save_file(filepath): + path = filepath.replace("\\", "/") + nuke.scriptSaveAs(path) + nuke.Root()["name"].setValue(path) + nuke.Root()["project_directory"].setValue(os.path.dirname(path)) + nuke.Root().setModified(False) + + +def open_file(filepath): + filepath = filepath.replace("\\", "/") + + # To remain in the same window, we have to clear the script and read + # in the contents of the workfile. + nuke.scriptClear() + nuke.scriptReadFile(filepath) + nuke.Root()["name"].setValue(filepath) + nuke.Root()["project_directory"].setValue(os.path.dirname(filepath)) + nuke.Root().setModified(False) + return True + + +def current_file(): + current_file = nuke.root().name() + + # Unsaved current file + if current_file == 'Root': + return None + + return os.path.normpath(current_file).replace("\\", "/") + + +def work_root(session): + + work_dir = session["AVALON_WORKDIR"] + scene_dir = session.get("AVALON_SCENEDIR") + if scene_dir: + path = os.path.join(work_dir, scene_dir) + else: + path = work_dir + + return os.path.normpath(path).replace("\\", "/") diff --git a/openpype/hosts/nuke/plugins/create/create_backdrop.py b/openpype/hosts/nuke/plugins/create/create_backdrop.py index cda2629587..0c11b3f274 100644 --- a/openpype/hosts/nuke/plugins/create/create_backdrop.py +++ b/openpype/hosts/nuke/plugins/create/create_backdrop.py @@ -1,9 +1,12 @@ -from avalon.nuke import lib as anlib -from openpype.hosts.nuke.api import plugin import nuke +from openpype.hosts.nuke.api import plugin +from openpype.hosts.nuke.api.lib import ( + select_nodes, + set_avalon_knob_data +) -class CreateBackdrop(plugin.PypeCreator): +class CreateBackdrop(plugin.OpenPypeCreator): """Add Publishable Backdrop""" name = "nukenodes" @@ -25,14 +28,14 @@ class CreateBackdrop(plugin.PypeCreator): nodes = self.nodes if len(nodes) >= 1: - anlib.select_nodes(nodes) + select_nodes(nodes) bckd_node = autoBackdrop() bckd_node["name"].setValue("{}_BDN".format(self.name)) bckd_node["tile_color"].setValue(int(self.node_color, 16)) bckd_node["note_font_size"].setValue(24) bckd_node["label"].setValue("[{}]".format(self.name)) # add avalon knobs - instance = anlib.set_avalon_knob_data(bckd_node, self.data) + instance = set_avalon_knob_data(bckd_node, self.data) return instance else: @@ -48,6 +51,6 @@ class CreateBackdrop(plugin.PypeCreator): bckd_node["note_font_size"].setValue(24) bckd_node["label"].setValue("[{}]".format(self.name)) # add avalon knobs - instance = anlib.set_avalon_knob_data(bckd_node, self.data) + instance = set_avalon_knob_data(bckd_node, self.data) return instance diff --git a/openpype/hosts/nuke/plugins/create/create_camera.py b/openpype/hosts/nuke/plugins/create/create_camera.py index 359086d48f..3b13c80dc4 100644 --- a/openpype/hosts/nuke/plugins/create/create_camera.py +++ b/openpype/hosts/nuke/plugins/create/create_camera.py @@ -1,9 +1,11 @@ -from avalon.nuke import lib as anlib -from openpype.hosts.nuke.api import plugin import nuke +from openpype.hosts.nuke.api import plugin +from openpype.hosts.nuke.api.lib import ( + set_avalon_knob_data +) -class CreateCamera(plugin.PypeCreator): +class CreateCamera(plugin.OpenPypeCreator): """Add Publishable Backdrop""" name = "camera" @@ -36,7 +38,7 @@ class CreateCamera(plugin.PypeCreator): # change node color n["tile_color"].setValue(int(self.node_color, 16)) # add avalon knobs - anlib.set_avalon_knob_data(n, data) + set_avalon_knob_data(n, data) return True else: msg = str("Please select nodes you " @@ -49,5 +51,5 @@ class CreateCamera(plugin.PypeCreator): camera_node = nuke.createNode("Camera2") camera_node["tile_color"].setValue(int(self.node_color, 16)) # add avalon knobs - instance = anlib.set_avalon_knob_data(camera_node, self.data) + instance = set_avalon_knob_data(camera_node, self.data) return instance diff --git a/openpype/hosts/nuke/plugins/create/create_gizmo.py b/openpype/hosts/nuke/plugins/create/create_gizmo.py index c59713cff1..de73623a1e 100644 --- a/openpype/hosts/nuke/plugins/create/create_gizmo.py +++ b/openpype/hosts/nuke/plugins/create/create_gizmo.py @@ -1,9 +1,14 @@ -from avalon.nuke import lib as anlib -from openpype.hosts.nuke.api import plugin import nuke +from openpype.hosts.nuke.api import plugin +from openpype.hosts.nuke.api.lib import ( + maintained_selection, + select_nodes, + set_avalon_knob_data +) -class CreateGizmo(plugin.PypeCreator): + +class CreateGizmo(plugin.OpenPypeCreator): """Add Publishable "gizmo" group The name is symbolically gizmo as presumably @@ -28,13 +33,13 @@ class CreateGizmo(plugin.PypeCreator): nodes = self.nodes self.log.info(len(nodes)) if len(nodes) == 1: - anlib.select_nodes(nodes) + select_nodes(nodes) node = nodes[-1] # check if Group node if node.Class() in "Group": node["name"].setValue("{}_GZM".format(self.name)) node["tile_color"].setValue(int(self.node_color, 16)) - return anlib.set_avalon_knob_data(node, self.data) + return set_avalon_knob_data(node, self.data) else: msg = ("Please select a group node " "you wish to publish as the gizmo") @@ -42,7 +47,7 @@ class CreateGizmo(plugin.PypeCreator): nuke.message(msg) if len(nodes) >= 2: - anlib.select_nodes(nodes) + select_nodes(nodes) nuke.makeGroup() gizmo_node = nuke.selectedNode() gizmo_node["name"].setValue("{}_GZM".format(self.name)) @@ -57,16 +62,15 @@ class CreateGizmo(plugin.PypeCreator): "- create User knobs on the group") # add avalon knobs - return anlib.set_avalon_knob_data(gizmo_node, self.data) + return set_avalon_knob_data(gizmo_node, self.data) else: - msg = ("Please select nodes you " - "wish to add to the gizmo") + msg = "Please select nodes you wish to add to the gizmo" self.log.error(msg) nuke.message(msg) return else: - with anlib.maintained_selection(): + with maintained_selection(): gizmo_node = nuke.createNode("Group") gizmo_node["name"].setValue("{}_GZM".format(self.name)) gizmo_node["tile_color"].setValue(int(self.node_color, 16)) @@ -80,4 +84,4 @@ class CreateGizmo(plugin.PypeCreator): "- create User knobs on the group") # add avalon knobs - return anlib.set_avalon_knob_data(gizmo_node, self.data) + return set_avalon_knob_data(gizmo_node, self.data) diff --git a/openpype/hosts/nuke/plugins/create/create_model.py b/openpype/hosts/nuke/plugins/create/create_model.py index 4e30860e05..15a4e3ab8a 100644 --- a/openpype/hosts/nuke/plugins/create/create_model.py +++ b/openpype/hosts/nuke/plugins/create/create_model.py @@ -1,9 +1,11 @@ -from avalon.nuke import lib as anlib -from openpype.hosts.nuke.api import plugin import nuke +from openpype.hosts.nuke.api import plugin +from openpype.hosts.nuke.api.lib import ( + set_avalon_knob_data +) -class CreateModel(plugin.PypeCreator): +class CreateModel(plugin.OpenPypeCreator): """Add Publishable Model Geometry""" name = "model" @@ -68,7 +70,7 @@ class CreateModel(plugin.PypeCreator): # change node color n["tile_color"].setValue(int(self.node_color, 16)) # add avalon knobs - anlib.set_avalon_knob_data(n, data) + set_avalon_knob_data(n, data) return True else: msg = str("Please select nodes you " @@ -81,5 +83,5 @@ class CreateModel(plugin.PypeCreator): model_node = nuke.createNode("WriteGeo") model_node["tile_color"].setValue(int(self.node_color, 16)) # add avalon knobs - instance = anlib.set_avalon_knob_data(model_node, self.data) + instance = set_avalon_knob_data(model_node, self.data) return instance diff --git a/openpype/hosts/nuke/plugins/create/create_read.py b/openpype/hosts/nuke/plugins/create/create_read.py index bf5de23346..bdc67add42 100644 --- a/openpype/hosts/nuke/plugins/create/create_read.py +++ b/openpype/hosts/nuke/plugins/create/create_read.py @@ -1,13 +1,16 @@ from collections import OrderedDict -import avalon.api -import avalon.nuke -from openpype import api as pype -from openpype.hosts.nuke.api import plugin import nuke +import avalon.api +from openpype import api as pype +from openpype.hosts.nuke.api import plugin +from openpype.hosts.nuke.api.lib import ( + set_avalon_knob_data +) -class CrateRead(plugin.PypeCreator): + +class CrateRead(plugin.OpenPypeCreator): # change this to template preset name = "ReadCopy" label = "Create Read Copy" @@ -45,7 +48,7 @@ class CrateRead(plugin.PypeCreator): continue avalon_data = self.data avalon_data['subset'] = "{}".format(self.name) - avalon.nuke.lib.set_avalon_knob_data(node, avalon_data) + set_avalon_knob_data(node, avalon_data) node['tile_color'].setValue(16744935) count_reads += 1 diff --git a/openpype/hosts/nuke/plugins/create/create_write_prerender.py b/openpype/hosts/nuke/plugins/create/create_write_prerender.py index 1b925014ad..3285e5f92d 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_prerender.py +++ b/openpype/hosts/nuke/plugins/create/create_write_prerender.py @@ -1,11 +1,12 @@ from collections import OrderedDict -from openpype.hosts.nuke.api import ( - plugin, - lib) + import nuke +from openpype.hosts.nuke.api import plugin +from openpype.hosts.nuke.api.lib import create_write_node -class CreateWritePrerender(plugin.PypeCreator): + +class CreateWritePrerender(plugin.OpenPypeCreator): # change this to template preset name = "WritePrerender" label = "Create Write Prerender" @@ -98,7 +99,7 @@ class CreateWritePrerender(plugin.PypeCreator): self.log.info("write_data: {}".format(write_data)) - write_node = lib.create_write_node( + write_node = create_write_node( self.data["subset"], write_data, input=selected_node, diff --git a/openpype/hosts/nuke/plugins/create/create_write_render.py b/openpype/hosts/nuke/plugins/create/create_write_render.py index 5f13fddf4e..a9c4b5341e 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_render.py +++ b/openpype/hosts/nuke/plugins/create/create_write_render.py @@ -1,11 +1,12 @@ from collections import OrderedDict -from openpype.hosts.nuke.api import ( - plugin, - lib) + import nuke +from openpype.hosts.nuke.api import plugin +from openpype.hosts.nuke.api.lib import create_write_node -class CreateWriteRender(plugin.PypeCreator): + +class CreateWriteRender(plugin.OpenPypeCreator): # change this to template preset name = "WriteRender" label = "Create Write Render" @@ -119,7 +120,7 @@ class CreateWriteRender(plugin.PypeCreator): } ] - write_node = lib.create_write_node( + write_node = create_write_node( self.data["subset"], write_data, input=selected_node, diff --git a/openpype/hosts/nuke/plugins/create/create_write_still.py b/openpype/hosts/nuke/plugins/create/create_write_still.py index eebb5613c3..0037b64ce3 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_still.py +++ b/openpype/hosts/nuke/plugins/create/create_write_still.py @@ -1,11 +1,12 @@ from collections import OrderedDict -from openpype.hosts.nuke.api import ( - plugin, - lib) + import nuke +from openpype.hosts.nuke.api import plugin +from openpype.hosts.nuke.api.lib import create_write_node -class CreateWriteStill(plugin.PypeCreator): + +class CreateWriteStill(plugin.OpenPypeCreator): # change this to template preset name = "WriteStillFrame" label = "Create Write Still Image" @@ -108,7 +109,7 @@ class CreateWriteStill(plugin.PypeCreator): } ] - write_node = lib.create_write_node( + write_node = create_write_node( self.name, write_data, input=selected_node, diff --git a/openpype/hosts/nuke/plugins/inventory/repair_old_loaders.py b/openpype/hosts/nuke/plugins/inventory/repair_old_loaders.py index e7ae51fa86..49405fd213 100644 --- a/openpype/hosts/nuke/plugins/inventory/repair_old_loaders.py +++ b/openpype/hosts/nuke/plugins/inventory/repair_old_loaders.py @@ -1,7 +1,6 @@ from avalon import api, style -from avalon.nuke import lib as anlib -from openpype.api import ( - Logger) +from openpype.api import Logger +from openpype.hosts.nuke.api.lib import set_avalon_knob_data class RepairOldLoaders(api.InventoryAction): @@ -10,7 +9,7 @@ class RepairOldLoaders(api.InventoryAction): icon = "gears" color = style.colors.alert - log = Logger().get_logger(__name__) + log = Logger.get_logger(__name__) def process(self, containers): import nuke @@ -34,4 +33,4 @@ class RepairOldLoaders(api.InventoryAction): }) node["name"].setValue(new_name) # get data from avalon knob - anlib.set_avalon_knob_data(node, cdata) + set_avalon_knob_data(node, cdata) diff --git a/openpype/hosts/nuke/plugins/inventory/select_containers.py b/openpype/hosts/nuke/plugins/inventory/select_containers.py index bd00983172..3f174b3562 100644 --- a/openpype/hosts/nuke/plugins/inventory/select_containers.py +++ b/openpype/hosts/nuke/plugins/inventory/select_containers.py @@ -1,4 +1,5 @@ from avalon import api +from openpype.hosts.nuke.api.commands import viewer_update_and_undo_stop class SelectContainers(api.InventoryAction): @@ -9,11 +10,10 @@ class SelectContainers(api.InventoryAction): def process(self, containers): import nuke - import avalon.nuke nodes = [nuke.toNode(i["objectName"]) for i in containers] - with avalon.nuke.viewer_update_and_undo_stop(): + with viewer_update_and_undo_stop(): # clear previous_selection [n['selected'].setValue(False) for n in nodes] # Select tool diff --git a/openpype/hosts/nuke/plugins/load/load_backdrop.py b/openpype/hosts/nuke/plugins/load/load_backdrop.py index 9148260e9e..a2bd458948 100644 --- a/openpype/hosts/nuke/plugins/load/load_backdrop.py +++ b/openpype/hosts/nuke/plugins/load/load_backdrop.py @@ -1,9 +1,18 @@ from avalon import api, style, io import nuke import nukescripts -from openpype.hosts.nuke.api import lib as pnlib -from avalon.nuke import lib as anlib -from avalon.nuke import containerise, update_container + +from openpype.hosts.nuke.api.lib import ( + find_free_space_to_paste_nodes, + maintained_selection, + reset_selection, + select_nodes, + get_avalon_knob_data, + set_avalon_knob_data +) +from openpype.hosts.nuke.api.commands import viewer_update_and_undo_stop +from openpype.hosts.nuke.api import containerise, update_container + class LoadBackdropNodes(api.Loader): """Loading Published Backdrop nodes (workfile, nukenodes)""" @@ -66,12 +75,12 @@ class LoadBackdropNodes(api.Loader): # Get mouse position n = nuke.createNode("NoOp") xcursor, ycursor = (n.xpos(), n.ypos()) - anlib.reset_selection() + reset_selection() nuke.delete(n) bdn_frame = 50 - with anlib.maintained_selection(): + with maintained_selection(): # add group from nk nuke.nodePaste(file) @@ -81,11 +90,13 @@ class LoadBackdropNodes(api.Loader): nodes = nuke.selectedNodes() # get pointer position in DAG - xpointer, ypointer = pnlib.find_free_space_to_paste_nodes(nodes, direction="right", offset=200+bdn_frame) + xpointer, ypointer = find_free_space_to_paste_nodes( + nodes, direction="right", offset=200 + bdn_frame + ) # reset position to all nodes and replace inputs and output for n in nodes: - anlib.reset_selection() + reset_selection() xpos = (n.xpos() - xcursor) + xpointer ypos = (n.ypos() - ycursor) + ypointer n.setXYpos(xpos, ypos) @@ -108,7 +119,7 @@ class LoadBackdropNodes(api.Loader): d.setInput(index, dot) # remove Input node - anlib.reset_selection() + reset_selection() nuke.delete(n) continue @@ -127,15 +138,15 @@ class LoadBackdropNodes(api.Loader): dot.setInput(0, dep) # remove Input node - anlib.reset_selection() + reset_selection() nuke.delete(n) continue else: new_nodes.append(n) # reselect nodes with new Dot instead of Inputs and Output - anlib.reset_selection() - anlib.select_nodes(new_nodes) + reset_selection() + select_nodes(new_nodes) # place on backdrop bdn = nukescripts.autoBackdrop() @@ -208,16 +219,16 @@ class LoadBackdropNodes(api.Loader): # just in case we are in group lets jump out of it nuke.endGroup() - with anlib.maintained_selection(): + with maintained_selection(): xpos = GN.xpos() ypos = GN.ypos() - avalon_data = anlib.get_avalon_knob_data(GN) + avalon_data = get_avalon_knob_data(GN) nuke.delete(GN) # add group from nk nuke.nodePaste(file) GN = nuke.selectedNode() - anlib.set_avalon_knob_data(GN, avalon_data) + set_avalon_knob_data(GN, avalon_data) GN.setXYpos(xpos, ypos) GN["name"].setValue(object_name) @@ -243,7 +254,6 @@ class LoadBackdropNodes(api.Loader): self.update(container, representation) def remove(self, container): - from avalon.nuke import viewer_update_and_undo_stop node = nuke.toNode(container['objectName']) with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_camera_abc.py b/openpype/hosts/nuke/plugins/load/load_camera_abc.py index 377d60e84b..b9d4bb358f 100644 --- a/openpype/hosts/nuke/plugins/load/load_camera_abc.py +++ b/openpype/hosts/nuke/plugins/load/load_camera_abc.py @@ -1,8 +1,15 @@ -from avalon import api, io -from avalon.nuke import lib as anlib -from avalon.nuke import containerise, update_container import nuke +from avalon import api, io +from openpype.hosts.nuke.api import ( + containerise, + update_container, + viewer_update_and_undo_stop +) +from openpype.hosts.nuke.api.lib import ( + maintained_selection +) + class AlembicCameraLoader(api.Loader): """ @@ -43,7 +50,7 @@ class AlembicCameraLoader(api.Loader): # getting file path file = self.fname.replace("\\", "/") - with anlib.maintained_selection(): + with maintained_selection(): camera_node = nuke.createNode( "Camera2", "name {} file {} read_from_file True".format( @@ -122,7 +129,7 @@ class AlembicCameraLoader(api.Loader): # getting file path file = api.get_representation_path(representation).replace("\\", "/") - with anlib.maintained_selection(): + with maintained_selection(): camera_node = nuke.toNode(object_name) camera_node['selected'].setValue(True) @@ -181,7 +188,6 @@ class AlembicCameraLoader(api.Loader): self.update(container, representation) def remove(self, container): - from avalon.nuke import viewer_update_and_undo_stop node = nuke.toNode(container['objectName']) with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index 9ce72c0519..712cdf213f 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -3,13 +3,13 @@ from avalon.vendor import qargparse from avalon import api, io from openpype.hosts.nuke.api.lib import ( - get_imageio_input_colorspace + get_imageio_input_colorspace, + maintained_selection ) -from avalon.nuke import ( +from openpype.hosts.nuke.api import ( containerise, update_container, - viewer_update_and_undo_stop, - maintained_selection + viewer_update_and_undo_stop ) from openpype.hosts.nuke.api import plugin @@ -280,9 +280,6 @@ class LoadClip(plugin.NukeLoader): self.set_as_member(read_node) def remove(self, container): - - from avalon.nuke import viewer_update_and_undo_stop - read_node = nuke.toNode(container['objectName']) assert read_node.Class() == "Read", "Must be Read" @@ -378,4 +375,4 @@ class LoadClip(plugin.NukeLoader): "class_name": self.__class__.__name__ } - return self.node_name_template.format(**name_data) \ No newline at end of file + return self.node_name_template.format(**name_data) diff --git a/openpype/hosts/nuke/plugins/load/load_effects.py b/openpype/hosts/nuke/plugins/load/load_effects.py index 8ba1b6b7c1..8b8867feba 100644 --- a/openpype/hosts/nuke/plugins/load/load_effects.py +++ b/openpype/hosts/nuke/plugins/load/load_effects.py @@ -1,7 +1,12 @@ -from avalon import api, style, io -import nuke import json from collections import OrderedDict +import nuke +from avalon import api, style, io +from openpype.hosts.nuke.api import ( + containerise, + update_container, + viewer_update_and_undo_stop +) class LoadEffects(api.Loader): @@ -30,9 +35,6 @@ class LoadEffects(api.Loader): Returns: nuke node: containerised nuke node object """ - # import dependencies - from avalon.nuke import containerise - # get main variables version = context['version'] version_data = version.get("data", {}) @@ -138,10 +140,6 @@ class LoadEffects(api.Loader): inputs: """ - - from avalon.nuke import ( - update_container - ) # get main variables # Get version from io version = io.find_one({ @@ -338,7 +336,6 @@ class LoadEffects(api.Loader): self.update(container, representation) def remove(self, container): - from avalon.nuke import viewer_update_and_undo_stop node = nuke.toNode(container['objectName']) with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_effects_ip.py b/openpype/hosts/nuke/plugins/load/load_effects_ip.py index d0cab26842..7948cbba9a 100644 --- a/openpype/hosts/nuke/plugins/load/load_effects_ip.py +++ b/openpype/hosts/nuke/plugins/load/load_effects_ip.py @@ -1,8 +1,15 @@ -from avalon import api, style, io -import nuke import json from collections import OrderedDict + +import nuke + +from avalon import api, style, io from openpype.hosts.nuke.api import lib +from openpype.hosts.nuke.api import ( + containerise, + update_container, + viewer_update_and_undo_stop +) class LoadEffectsInputProcess(api.Loader): @@ -30,8 +37,6 @@ class LoadEffectsInputProcess(api.Loader): Returns: nuke node: containerised nuke node object """ - # import dependencies - from avalon.nuke import containerise # get main variables version = context['version'] @@ -142,9 +147,6 @@ class LoadEffectsInputProcess(api.Loader): """ - from avalon.nuke import ( - update_container - ) # get main variables # Get version from io version = io.find_one({ @@ -355,7 +357,6 @@ class LoadEffectsInputProcess(api.Loader): self.update(container, representation) def remove(self, container): - from avalon.nuke import viewer_update_and_undo_stop node = nuke.toNode(container['objectName']) with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_gizmo.py b/openpype/hosts/nuke/plugins/load/load_gizmo.py index c6228b95f6..f549623b88 100644 --- a/openpype/hosts/nuke/plugins/load/load_gizmo.py +++ b/openpype/hosts/nuke/plugins/load/load_gizmo.py @@ -1,7 +1,15 @@ -from avalon import api, style, io import nuke -from avalon.nuke import lib as anlib -from avalon.nuke import containerise, update_container +from avalon import api, style, io +from openpype.hosts.nuke.api.lib import ( + maintained_selection, + get_avalon_knob_data, + set_avalon_knob_data +) +from openpype.hosts.nuke.api import ( + containerise, + update_container, + viewer_update_and_undo_stop +) class LoadGizmo(api.Loader): @@ -61,7 +69,7 @@ class LoadGizmo(api.Loader): # just in case we are in group lets jump out of it nuke.endGroup() - with anlib.maintained_selection(): + with maintained_selection(): # add group from nk nuke.nodePaste(file) @@ -122,16 +130,16 @@ class LoadGizmo(api.Loader): # just in case we are in group lets jump out of it nuke.endGroup() - with anlib.maintained_selection(): + with maintained_selection(): xpos = GN.xpos() ypos = GN.ypos() - avalon_data = anlib.get_avalon_knob_data(GN) + avalon_data = get_avalon_knob_data(GN) nuke.delete(GN) # add group from nk nuke.nodePaste(file) GN = nuke.selectedNode() - anlib.set_avalon_knob_data(GN, avalon_data) + set_avalon_knob_data(GN, avalon_data) GN.setXYpos(xpos, ypos) GN["name"].setValue(object_name) @@ -157,7 +165,6 @@ class LoadGizmo(api.Loader): self.update(container, representation) def remove(self, container): - from avalon.nuke import viewer_update_and_undo_stop node = nuke.toNode(container['objectName']) with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py b/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py index 5ca101d6cb..4f17446673 100644 --- a/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py +++ b/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py @@ -1,8 +1,16 @@ from avalon import api, style, io import nuke -from openpype.hosts.nuke.api import lib as pnlib -from avalon.nuke import lib as anlib -from avalon.nuke import containerise, update_container +from openpype.hosts.nuke.api.lib import ( + maintained_selection, + create_backdrop, + get_avalon_knob_data, + set_avalon_knob_data +) +from openpype.hosts.nuke.api import ( + containerise, + update_container, + viewer_update_and_undo_stop +) class LoadGizmoInputProcess(api.Loader): @@ -62,7 +70,7 @@ class LoadGizmoInputProcess(api.Loader): # just in case we are in group lets jump out of it nuke.endGroup() - with anlib.maintained_selection(): + with maintained_selection(): # add group from nk nuke.nodePaste(file) @@ -128,16 +136,16 @@ class LoadGizmoInputProcess(api.Loader): # just in case we are in group lets jump out of it nuke.endGroup() - with anlib.maintained_selection(): + with maintained_selection(): xpos = GN.xpos() ypos = GN.ypos() - avalon_data = anlib.get_avalon_knob_data(GN) + avalon_data = get_avalon_knob_data(GN) nuke.delete(GN) # add group from nk nuke.nodePaste(file) GN = nuke.selectedNode() - anlib.set_avalon_knob_data(GN, avalon_data) + set_avalon_knob_data(GN, avalon_data) GN.setXYpos(xpos, ypos) GN["name"].setValue(object_name) @@ -197,8 +205,12 @@ class LoadGizmoInputProcess(api.Loader): viewer["input_process_node"].setValue(group_node_name) # put backdrop under - pnlib.create_backdrop(label="Input Process", layer=2, - nodes=[viewer, group_node], color="0x7c7faaff") + create_backdrop( + label="Input Process", + layer=2, + nodes=[viewer, group_node], + color="0x7c7faaff" + ) return True @@ -234,7 +246,6 @@ class LoadGizmoInputProcess(api.Loader): self.update(container, representation) def remove(self, container): - from avalon.nuke import viewer_update_and_undo_stop node = nuke.toNode(container['objectName']) with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_image.py b/openpype/hosts/nuke/plugins/load/load_image.py index 02a5b55c18..427167ca98 100644 --- a/openpype/hosts/nuke/plugins/load/load_image.py +++ b/openpype/hosts/nuke/plugins/load/load_image.py @@ -7,6 +7,11 @@ from avalon import api, io from openpype.hosts.nuke.api.lib import ( get_imageio_input_colorspace ) +from openpype.hosts.nuke.api import ( + containerise, + update_container, + viewer_update_and_undo_stop +) class LoadImage(api.Loader): @@ -46,10 +51,6 @@ class LoadImage(api.Loader): return cls.representations + cls._representations def load(self, context, name, namespace, options): - from avalon.nuke import ( - containerise, - viewer_update_and_undo_stop - ) self.log.info("__ options: `{}`".format(options)) frame_number = options.get("frame_number", 1) @@ -154,11 +155,6 @@ class LoadImage(api.Loader): inputs: """ - - from avalon.nuke import ( - update_container - ) - node = nuke.toNode(container["objectName"]) frame_number = node["first"].value() @@ -234,9 +230,6 @@ class LoadImage(api.Loader): self.log.info("udated to version: {}".format(version.get("name"))) def remove(self, container): - - from avalon.nuke import viewer_update_and_undo_stop - node = nuke.toNode(container['objectName']) assert node.Class() == "Read", "Must be Read" diff --git a/openpype/hosts/nuke/plugins/load/load_model.py b/openpype/hosts/nuke/plugins/load/load_model.py index 15fa4fa35c..8c8dc7f37d 100644 --- a/openpype/hosts/nuke/plugins/load/load_model.py +++ b/openpype/hosts/nuke/plugins/load/load_model.py @@ -1,7 +1,11 @@ -from avalon import api, io -from avalon.nuke import lib as anlib -from avalon.nuke import containerise, update_container import nuke +from avalon import api, io +from openpype.hosts.nuke.api.lib import maintained_selection +from openpype.hosts.nuke.api import ( + containerise, + update_container, + viewer_update_and_undo_stop +) class AlembicModelLoader(api.Loader): @@ -43,7 +47,7 @@ class AlembicModelLoader(api.Loader): # getting file path file = self.fname.replace("\\", "/") - with anlib.maintained_selection(): + with maintained_selection(): model_node = nuke.createNode( "ReadGeo2", "name {} file {} ".format( @@ -122,7 +126,7 @@ class AlembicModelLoader(api.Loader): # getting file path file = api.get_representation_path(representation).replace("\\", "/") - with anlib.maintained_selection(): + with maintained_selection(): model_node = nuke.toNode(object_name) model_node['selected'].setValue(True) @@ -181,7 +185,6 @@ class AlembicModelLoader(api.Loader): self.update(container, representation) def remove(self, container): - from avalon.nuke import viewer_update_and_undo_stop node = nuke.toNode(container['objectName']) with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/load/load_script_precomp.py b/openpype/hosts/nuke/plugins/load/load_script_precomp.py index 7444dd6e96..8489283e8c 100644 --- a/openpype/hosts/nuke/plugins/load/load_script_precomp.py +++ b/openpype/hosts/nuke/plugins/load/load_script_precomp.py @@ -1,6 +1,11 @@ -from avalon import api, style, io -from avalon.nuke import get_avalon_knob_data import nuke +from avalon import api, style, io +from openpype.hosts.nuke.api.lib import get_avalon_knob_data +from openpype.hosts.nuke.api import ( + containerise, + update_container, + viewer_update_and_undo_stop +) class LinkAsGroup(api.Loader): @@ -15,8 +20,6 @@ class LinkAsGroup(api.Loader): color = style.colors.alert def load(self, context, name, namespace, data): - - from avalon.nuke import containerise # for k, v in context.items(): # log.info("key: `{}`, value: {}\n".format(k, v)) version = context['version'] @@ -103,11 +106,6 @@ class LinkAsGroup(api.Loader): inputs: """ - - from avalon.nuke import ( - update_container - ) - node = nuke.toNode(container['objectName']) root = api.get_representation_path(representation).replace("\\", "/") @@ -155,7 +153,6 @@ class LinkAsGroup(api.Loader): self.log.info("udated to version: {}".format(version.get("name"))) def remove(self, container): - from avalon.nuke import viewer_update_and_undo_stop node = nuke.toNode(container['objectName']) with viewer_update_and_undo_stop(): nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/publish/extract_backdrop.py b/openpype/hosts/nuke/plugins/publish/extract_backdrop.py index 0747c15ea7..0a2df0898e 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_backdrop.py +++ b/openpype/hosts/nuke/plugins/publish/extract_backdrop.py @@ -1,9 +1,16 @@ -import pyblish.api -from avalon.nuke import lib as anlib -from openpype.hosts.nuke.api import lib as pnlib -import nuke import os + +import nuke + +import pyblish.api + import openpype +from openpype.hosts.nuke.api.lib import ( + maintained_selection, + reset_selection, + select_nodes +) + class ExtractBackdropNode(openpype.api.Extractor): """Extracting content of backdrop nodes @@ -27,7 +34,7 @@ class ExtractBackdropNode(openpype.api.Extractor): path = os.path.join(stagingdir, filename) # maintain selection - with anlib.maintained_selection(): + with maintained_selection(): # all connections outside of backdrop connections_in = instance.data["nodeConnectionsIn"] connections_out = instance.data["nodeConnectionsOut"] @@ -44,7 +51,7 @@ class ExtractBackdropNode(openpype.api.Extractor): nodes.append(inpn) tmp_nodes.append(inpn) - anlib.reset_selection() + reset_selection() # connect output node for n, output in connections_out.items(): @@ -58,11 +65,11 @@ class ExtractBackdropNode(openpype.api.Extractor): opn.autoplace() nodes.append(opn) tmp_nodes.append(opn) - anlib.reset_selection() + reset_selection() # select nodes to copy - anlib.reset_selection() - anlib.select_nodes(nodes) + reset_selection() + select_nodes(nodes) # create tmp nk file # save file to the path nuke.nodeCopy(path) diff --git a/openpype/hosts/nuke/plugins/publish/extract_camera.py b/openpype/hosts/nuke/plugins/publish/extract_camera.py index bc50dac108..942cdc537d 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_camera.py +++ b/openpype/hosts/nuke/plugins/publish/extract_camera.py @@ -1,10 +1,12 @@ -import nuke import os import math +from pprint import pformat + +import nuke + import pyblish.api import openpype.api -from avalon.nuke import lib as anlib -from pprint import pformat +from openpype.hosts.nuke.api.lib import maintained_selection class ExtractCamera(openpype.api.Extractor): @@ -52,7 +54,7 @@ class ExtractCamera(openpype.api.Extractor): filename = subset + ".{}".format(extension) file_path = os.path.join(staging_dir, filename).replace("\\", "/") - with anlib.maintained_selection(): + with maintained_selection(): # bake camera with axeses onto word coordinate XYZ rm_n = bakeCameraWithAxeses( nuke.toNode(instance.data["name"]), output_range) diff --git a/openpype/hosts/nuke/plugins/publish/extract_gizmo.py b/openpype/hosts/nuke/plugins/publish/extract_gizmo.py index 78bf9c998d..2d5bfdeb5e 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_gizmo.py +++ b/openpype/hosts/nuke/plugins/publish/extract_gizmo.py @@ -1,9 +1,15 @@ -import pyblish.api -from avalon.nuke import lib as anlib -from openpype.hosts.nuke.api import utils as pnutils -import nuke import os +import nuke + +import pyblish.api + import openpype +from openpype.hosts.nuke.api import utils as pnutils +from openpype.hosts.nuke.api.lib import ( + maintained_selection, + reset_selection, + select_nodes +) class ExtractGizmo(openpype.api.Extractor): @@ -26,17 +32,17 @@ class ExtractGizmo(openpype.api.Extractor): path = os.path.join(stagingdir, filename) # maintain selection - with anlib.maintained_selection(): + with maintained_selection(): orig_grpn_name = orig_grpn.name() tmp_grpn_name = orig_grpn_name + "_tmp" # select original group node - anlib.select_nodes([orig_grpn]) + select_nodes([orig_grpn]) # copy to clipboard nuke.nodeCopy("%clipboard%") # reset selection to none - anlib.reset_selection() + reset_selection() # paste clipboard nuke.nodePaste("%clipboard%") diff --git a/openpype/hosts/nuke/plugins/publish/extract_model.py b/openpype/hosts/nuke/plugins/publish/extract_model.py index 43214bf3e9..0375263338 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_model.py +++ b/openpype/hosts/nuke/plugins/publish/extract_model.py @@ -1,9 +1,12 @@ -import nuke import os +from pprint import pformat +import nuke import pyblish.api import openpype.api -from avalon.nuke import lib as anlib -from pprint import pformat +from openpype.hosts.nuke.api.lib import ( + maintained_selection, + select_nodes +) class ExtractModel(openpype.api.Extractor): @@ -49,9 +52,9 @@ class ExtractModel(openpype.api.Extractor): filename = subset + ".{}".format(extension) file_path = os.path.join(staging_dir, filename).replace("\\", "/") - with anlib.maintained_selection(): + with maintained_selection(): # select model node - anlib.select_nodes([model_node]) + select_nodes([model_node]) # create write geo node wg_n = nuke.createNode("WriteGeo") diff --git a/openpype/hosts/nuke/plugins/publish/extract_ouput_node.py b/openpype/hosts/nuke/plugins/publish/extract_ouput_node.py index c3a6a3b167..e38927c3a7 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_ouput_node.py +++ b/openpype/hosts/nuke/plugins/publish/extract_ouput_node.py @@ -1,6 +1,6 @@ import nuke import pyblish.api -from avalon.nuke import maintained_selection +from openpype.hosts.nuke.api.lib import maintained_selection class CreateOutputNode(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py index 8ba746a3c4..4cf2fd7d9f 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py @@ -1,8 +1,8 @@ import os import pyblish.api -from avalon.nuke import lib as anlib -from openpype.hosts.nuke.api import plugin import openpype +from openpype.hosts.nuke.api import plugin +from openpype.hosts.nuke.api.lib import maintained_selection class ExtractReviewDataLut(openpype.api.Extractor): @@ -37,7 +37,7 @@ class ExtractReviewDataLut(openpype.api.Extractor): "StagingDir `{0}`...".format(instance.data["stagingDir"])) # generate data - with anlib.maintained_selection(): + with maintained_selection(): exporter = plugin.ExporterReviewLut( self, instance ) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py index 32962b57a6..13d23ffb9c 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py @@ -1,8 +1,8 @@ import os import pyblish.api -from avalon.nuke import lib as anlib -from openpype.hosts.nuke.api import plugin import openpype +from openpype.hosts.nuke.api import plugin +from openpype.hosts.nuke.api.lib import maintained_selection class ExtractReviewDataMov(openpype.api.Extractor): @@ -41,7 +41,7 @@ class ExtractReviewDataMov(openpype.api.Extractor): self.log.info(self.outputs) # generate data - with anlib.maintained_selection(): + with maintained_selection(): generated_repres = [] for o_name, o_data in self.outputs.items(): f_families = o_data["filter"]["families"] diff --git a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py index 0f68680742..50e5f995f4 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py +++ b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py @@ -1,8 +1,8 @@ import os import nuke -from avalon.nuke import lib as anlib import pyblish.api import openpype +from openpype.hosts.nuke.api.lib import maintained_selection class ExtractSlateFrame(openpype.api.Extractor): @@ -25,7 +25,7 @@ class ExtractSlateFrame(openpype.api.Extractor): else: self.viewer_lut_raw = False - with anlib.maintained_selection(): + with maintained_selection(): self.log.debug("instance: {}".format(instance)) self.log.debug("instance.data[families]: {}".format( instance.data["families"])) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 0c9af66435..ef6d486ca2 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -1,9 +1,9 @@ import sys import os import nuke -from avalon.nuke import lib as anlib import pyblish.api import openpype +from openpype.hosts.nuke.api.lib import maintained_selection if sys.version_info[0] >= 3: @@ -30,7 +30,7 @@ class ExtractThumbnail(openpype.api.Extractor): if "render.farm" in instance.data["families"]: return - with anlib.maintained_selection(): + with maintained_selection(): self.log.debug("instance: {}".format(instance)) self.log.debug("instance.data[families]: {}".format( instance.data["families"])) diff --git a/openpype/hosts/nuke/plugins/publish/precollect_instances.py b/openpype/hosts/nuke/plugins/publish/precollect_instances.py index 5c30df9a62..97ddef0a59 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_instances.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_instances.py @@ -1,7 +1,10 @@ import nuke import pyblish.api from avalon import io, api -from avalon.nuke import lib as anlib +from openpype.hosts.nuke.api.lib import ( + add_publish_knob, + get_avalon_knob_data +) @pyblish.api.log @@ -39,7 +42,7 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin): self.log.warning(E) # get data from avalon knob - avalon_knob_data = anlib.get_avalon_knob_data( + avalon_knob_data = get_avalon_knob_data( node, ["avalon:", "ak:"]) self.log.debug("avalon_knob_data: {}".format(avalon_knob_data)) @@ -115,7 +118,7 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin): # get publish knob value if "publish" not in node.knobs(): - anlib.add_publish_knob(node) + add_publish_knob(node) # sync workfile version _families_test = [family] + families diff --git a/openpype/hosts/nuke/plugins/publish/precollect_workfile.py b/openpype/hosts/nuke/plugins/publish/precollect_workfile.py index 0e27273ceb..a2d1c80628 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_workfile.py @@ -1,8 +1,13 @@ -import nuke -import pyblish.api import os + +import nuke + +import pyblish.api import openpype.api as pype -from avalon.nuke import lib as anlib +from openpype.hosts.nuke.api.lib import ( + add_publish_knob, + get_avalon_knob_data +) class CollectWorkfile(pyblish.api.ContextPlugin): @@ -17,9 +22,9 @@ class CollectWorkfile(pyblish.api.ContextPlugin): current_file = os.path.normpath(nuke.root().name()) - knob_data = anlib.get_avalon_knob_data(root) + knob_data = get_avalon_knob_data(root) - anlib.add_publish_knob(root) + add_publish_knob(root) family = "workfile" task = os.getenv("AVALON_TASK", None) diff --git a/openpype/hosts/nuke/plugins/publish/validate_backdrop.py b/openpype/hosts/nuke/plugins/publish/validate_backdrop.py index f280ad4af1..7694c3d2ba 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_backdrop.py +++ b/openpype/hosts/nuke/plugins/publish/validate_backdrop.py @@ -1,6 +1,6 @@ -import pyblish -from avalon.nuke import lib as anlib import nuke +import pyblish +from openpype.hosts.nuke.api.lib import maintained_selection class SelectCenterInNodeGraph(pyblish.api.Action): @@ -28,7 +28,7 @@ class SelectCenterInNodeGraph(pyblish.api.Action): all_yC = list() # maintain selection - with anlib.maintained_selection(): + with maintained_selection(): # collect all failed nodes xpos and ypos for instance in instances: bdn = instance[0] diff --git a/openpype/hosts/nuke/plugins/publish/validate_gizmo.py b/openpype/hosts/nuke/plugins/publish/validate_gizmo.py index 9c94ea88ef..d0d930f50c 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_gizmo.py +++ b/openpype/hosts/nuke/plugins/publish/validate_gizmo.py @@ -1,6 +1,6 @@ -import pyblish -from avalon.nuke import lib as anlib import nuke +import pyblish +from openpype.hosts.nuke.api.lib import maintained_selection class OpenFailedGroupNode(pyblish.api.Action): @@ -25,7 +25,7 @@ class OpenFailedGroupNode(pyblish.api.Action): instances = pyblish.api.instances_by_plugin(failed, plugin) # maintain selection - with anlib.maintained_selection(): + with maintained_selection(): # collect all failed nodes xpos and ypos for instance in instances: grpn = instance[0] diff --git a/openpype/hosts/nuke/plugins/publish/validate_instance_in_context.py b/openpype/hosts/nuke/plugins/publish/validate_instance_in_context.py index ddf46a0873..842f74b6f6 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_instance_in_context.py +++ b/openpype/hosts/nuke/plugins/publish/validate_instance_in_context.py @@ -6,8 +6,11 @@ import nuke import pyblish.api import openpype.api -import avalon.nuke.lib -import openpype.hosts.nuke.api as nuke_api +from openpype.hosts.nuke.api.lib import ( + recreate_instance, + reset_selection, + select_nodes +) class SelectInvalidInstances(pyblish.api.Action): @@ -47,12 +50,12 @@ class SelectInvalidInstances(pyblish.api.Action): self.deselect() def select(self, instances): - avalon.nuke.lib.select_nodes( + select_nodes( [nuke.toNode(str(x)) for x in instances] ) def deselect(self): - avalon.nuke.lib.reset_selection() + reset_selection() class RepairSelectInvalidInstances(pyblish.api.Action): @@ -82,7 +85,7 @@ class RepairSelectInvalidInstances(pyblish.api.Action): context_asset = context.data["assetEntity"]["name"] for instance in instances: origin_node = instance[0] - nuke_api.lib.recreate_instance( + recreate_instance( origin_node, avalon_data={"asset": context_asset} ) diff --git a/openpype/hosts/nuke/plugins/publish/validate_write_legacy.py b/openpype/hosts/nuke/plugins/publish/validate_write_legacy.py index ba34ec8338..a73bed8edd 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_write_legacy.py +++ b/openpype/hosts/nuke/plugins/publish/validate_write_legacy.py @@ -1,13 +1,12 @@ -import toml import os +import toml import nuke from avalon import api -import re import pyblish.api import openpype.api -from avalon.nuke import get_avalon_knob_data +from openpype.hosts.nuke.api.lib import get_avalon_knob_data class ValidateWriteLegacy(pyblish.api.InstancePlugin): diff --git a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py index 732f321b85..c0d5c8f402 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py +++ b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py @@ -1,8 +1,11 @@ import os import pyblish.api import openpype.utils -import openpype.hosts.nuke.lib as nukelib -import avalon.nuke +from openpype.hosts.nuke.api.lib import ( + get_write_node_template_attr, + get_node_path +) + @pyblish.api.log class RepairNukeWriteNodeAction(pyblish.api.Action): @@ -15,7 +18,7 @@ class RepairNukeWriteNodeAction(pyblish.api.Action): for instance in instances: node = instance[1] - correct_data = nukelib.get_write_node_template_attr(node) + correct_data = get_write_node_template_attr(node) for k, v in correct_data.items(): node[k].setValue(v) self.log.info("Node attributes were fixed") @@ -34,14 +37,14 @@ class ValidateNukeWriteNode(pyblish.api.InstancePlugin): def process(self, instance): node = instance[1] - correct_data = nukelib.get_write_node_template_attr(node) + correct_data = get_write_node_template_attr(node) check = [] for k, v in correct_data.items(): if k is 'file': padding = len(v.split('#')) - ref_path = avalon.nuke.lib.get_node_path(v, padding) - n_path = avalon.nuke.lib.get_node_path(node[k].value(), padding) + ref_path = get_node_path(v, padding) + n_path = get_node_path(node[k].value(), padding) isnt = False for i, p in enumerate(ref_path): if str(n_path[i]) not in str(p): diff --git a/openpype/hosts/nuke/startup/init.py b/openpype/hosts/nuke/startup/init.py index 0ea5d1ad7d..d7560814bf 100644 --- a/openpype/hosts/nuke/startup/init.py +++ b/openpype/hosts/nuke/startup/init.py @@ -1,2 +1,4 @@ +import nuke + # default write mov nuke.knobDefault('Write.mov.colorspace', 'sRGB') diff --git a/openpype/hosts/nuke/startup/menu.py b/openpype/hosts/nuke/startup/menu.py index b7ed35b3b4..2cac6d09e7 100644 --- a/openpype/hosts/nuke/startup/menu.py +++ b/openpype/hosts/nuke/startup/menu.py @@ -1,14 +1,19 @@ +import nuke +import avalon.api + +from openpype.api import Logger +from openpype.hosts.nuke import api from openpype.hosts.nuke.api.lib import ( on_script_load, check_inventory_versions, - WorkfileSettings + WorkfileSettings, + dirmap_file_name_filter ) -import nuke -from openpype.api import Logger -from openpype.hosts.nuke.api.lib import dirmap_file_name_filter +log = Logger.get_logger(__name__) -log = Logger().get_logger(__name__) + +avalon.api.install(api) # fix ffmpeg settings on script nuke.addOnScriptLoad(on_script_load) From 26d8304fd9704f04bd9ac076d193dc1646e4a38b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Jan 2022 12:27:09 +0100 Subject: [PATCH 05/39] removed avalon nuke path from add implementation environments --- openpype/hosts/nuke/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openpype/hosts/nuke/__init__.py b/openpype/hosts/nuke/__init__.py index 366f704dd8..60b37ce1dd 100644 --- a/openpype/hosts/nuke/__init__.py +++ b/openpype/hosts/nuke/__init__.py @@ -6,10 +6,7 @@ def add_implementation_envs(env, _app): # Add requirements to NUKE_PATH pype_root = os.environ["OPENPYPE_REPOS_ROOT"] new_nuke_paths = [ - os.path.join(pype_root, "openpype", "hosts", "nuke", "startup"), - os.path.join( - pype_root, "repos", "avalon-core", "setup", "nuke", "nuke_path" - ) + os.path.join(pype_root, "openpype", "hosts", "nuke", "startup") ] old_nuke_path = env.get("NUKE_PATH") or "" for path in old_nuke_path.split(os.pathsep): From 9980aa90fa196eb07e57ea7155b7ce98469d81e9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 12 Jan 2022 12:32:21 +0100 Subject: [PATCH 06/39] fix default value of function argument --- openpype/hosts/nuke/api/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/utils.py b/openpype/hosts/nuke/api/utils.py index f8f248357b..205b23efe6 100644 --- a/openpype/hosts/nuke/api/utils.py +++ b/openpype/hosts/nuke/api/utils.py @@ -49,12 +49,14 @@ def gizmo_is_nuke_default(gizmo): return gizmo.filename().startswith(plug_dir) -def bake_gizmos_recursively(in_group=nuke.Root()): +def bake_gizmos_recursively(in_group=None): """Converting a gizmo to group Argumets: is_group (nuke.Node)[optonal]: group node or all nodes """ + if in_group is None: + in_group = nuke.Root() # preserve selection after all is done with maintained_selection(): # jump to the group From 29445314346e644ea02e1df8f4479ce8b587a32d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 Jan 2022 13:02:51 +0100 Subject: [PATCH 07/39] implemented callback warpper for execution in main thread --- openpype/tools/tray/pype_tray.py | 24 ++++++----- openpype/tools/utils/__init__.py | 5 ++- openpype/tools/utils/lib.py | 70 +++++++++++++++++++++++++++++++- 3 files changed, 87 insertions(+), 12 deletions(-) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index df0238c848..e7ac390c30 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -22,6 +22,7 @@ from openpype.settings import ( ProjectSettings, DefaultsNotDefined ) +from openpype.tools.utils import WrappedCallbackItem from .pype_info_widget import PypeInfoWidget @@ -61,21 +62,24 @@ class TrayManager: if callback: self.execute_in_main_thread(callback) - def execute_in_main_thread(self, callback): - self._main_thread_callbacks.append(callback) + 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 _main_thread_execution(self): if self._execution_in_progress: return self._execution_in_progress = True - while self._main_thread_callbacks: - try: - callback = self._main_thread_callbacks.popleft() - callback() - except: - self.log.warning( - "Failed to execute {} in main thread".format(callback), - exc_info=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 diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index 4dd6bdd05f..65025ac358 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -6,6 +6,7 @@ from .widgets import ( ) from .error_dialog import ErrorMessageBox +from .lib import WrappedCallbackItem __all__ = ( @@ -14,5 +15,7 @@ __all__ = ( "ClickableFrame", "ExpandBtn", - "ErrorMessageBox" + "ErrorMessageBox", + + "WrappedCallbackItem", ) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 6742df8557..5f3456ae3e 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -9,7 +9,10 @@ import avalon.api from avalon import style from avalon.vendor import qtawesome -from openpype.api import get_project_settings +from openpype.api import ( + get_project_settings, + Logger +) from openpype.lib import filter_profiles @@ -598,3 +601,68 @@ def is_remove_site_loader(loader): def is_add_site_loader(loader): return hasattr(loader, "add_site_to_representation") + + +class WrappedCallbackItem: + """Structure to store information about callback and args/kwargs for it. + + Item can be used to execute callback in main thread which may be needed + for execution of Qt objects. + + Item store callback (callable variable), arguments and keyword arguments + for the callback. Item hold information about it's process. + """ + not_set = object() + _log = None + + def __init__(self, callback, *args, **kwargs): + self._done = False + self._exception = self.not_set + self._result = self.not_set + self._callback = callback + self._args = args + self._kwargs = kwargs + + def __call__(self): + self.execute() + + @property + def log(self): + cls = self.__class__ + if cls._log is None: + cls._log = Logger.get_logger(cls.__name__) + return cls._log + + @property + def done(self): + return self._done + + @property + def exception(self): + return self._exception + + @property + def result(self): + return self._result + + def execute(self): + """Execute callback and store it's result. + + Method must be called from main thread. Item is marked as `done` + when callback execution finished. Store output of callback of exception + information when callback raise one. + """ + if self.done: + self.log.warning("- item is already processed") + return + + self.log.debug("Running callback: {}".format(str(self._callback))) + try: + result = self._callback(*self._args, **self._kwargs) + self._result = result + + except Exception as exc: + self._exception = exc + + finally: + self._done = True From d7fb171f101bf36495a106fbca296515f692b080 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 Jan 2022 13:08:35 +0100 Subject: [PATCH 08/39] added check if current running openpype has expected version --- openpype/lib/__init__.py | 6 ++- openpype/lib/pype_info.py | 48 +++++++++++++++++---- openpype/tools/tray/pype_tray.py | 74 ++++++++++++++++++++++++++++++-- 3 files changed, 115 insertions(+), 13 deletions(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 12e47a8961..65019f3fab 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -170,7 +170,9 @@ from .editorial import ( from .pype_info import ( get_openpype_version, - get_build_version + get_build_version, + is_running_from_build, + is_current_version_studio_latest ) terminal = Terminal @@ -304,4 +306,6 @@ __all__ = [ "get_openpype_version", "get_build_version", + "is_running_from_build", + "is_current_version_studio_latest", ] diff --git a/openpype/lib/pype_info.py b/openpype/lib/pype_info.py index 15856bfb19..ea804c8a18 100644 --- a/openpype/lib/pype_info.py +++ b/openpype/lib/pype_info.py @@ -10,6 +10,12 @@ from openpype.settings.lib import get_local_settings from .execute import get_openpype_execute_args from .local_settings import get_local_site_id from .python_module_tools import import_filepath +from .openpype_version import ( + op_version_control_available, + openpype_path_is_accessible, + get_expected_studio_version, + get_OpenPypeVersion +) def get_openpype_version(): @@ -17,15 +23,6 @@ def get_openpype_version(): return openpype.version.__version__ -def get_pype_version(): - """Backwards compatibility. Remove when 100% not used.""" - print(( - "Using deprecated function 'openpype.lib.pype_info.get_pype_version'" - " replace with 'openpype.lib.pype_info.get_openpype_version'." - )) - return get_openpype_version() - - def get_build_version(): """OpenPype version of build.""" # Return OpenPype version if is running from code @@ -138,3 +135,36 @@ def extract_pype_info_to_file(dirpath): with open(filepath, "w") as file_stream: json.dump(data, file_stream, indent=4) return filepath + + +def is_current_version_studio_latest(): + """Is currently running OpenPype version which is defined by studio. + + It is not recommended to ask in each process as there may be situations + when older OpenPype should be used. For example on farm. But it does make + sense in processes that can run for a long time. + + Returns: + None: Can't determine. e.g. when running from code or the build is + too old. + bool: True when is using studio + """ + output = None + # Skip if is not running from build + if not is_running_from_build(): + return output + + # Skip if build does not support version control + if not op_version_control_available(): + return output + + # Skip if path to folder with zip files is not accessible + if not openpype_path_is_accessible(): + return output + + # Check if current version is expected version + OpenPypeVersion = get_OpenPypeVersion() + current_version = OpenPypeVersion(get_openpype_version()) + expected_version = get_expected_studio_version(is_running_staging()) + + return current_version == expected_version diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index e7ac390c30..5af82b2c64 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -14,7 +14,11 @@ from openpype.api import ( resources, get_system_settings ) -from openpype.lib import get_openpype_execute_args +from openpype.lib import ( + get_openpype_execute_args, + is_current_version_studio_latest, + is_running_from_build +) from openpype.modules import TrayModulesManager from openpype import style from openpype.settings import ( @@ -27,11 +31,43 @@ from openpype.tools.utils import WrappedCallbackItem from .pype_info_widget import PypeInfoWidget +class VersionDialog(QtWidgets.QDialog): + def __init__(self, parent=None): + super(VersionDialog, self).__init__(parent) + + label_widget = QtWidgets.QLabel( + "Your version does not match to studio version", self + ) + + ignore_btn = QtWidgets.QPushButton("Ignore", self) + restart_btn = QtWidgets.QPushButton("Restart and Install", self) + + 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(label_widget, 0) + layout.addStretch(1) + layout.addLayout(btns_layout, 0) + + ignore_btn.clicked.connect(self._on_ignore) + restart_btn.clicked.connect(self._on_reset) + + def _on_ignore(self): + self.reject() + + def _on_reset(self): + self.accept() + + class TrayManager: """Cares about context of application. Load submenus, actions, separators and modules into tray's context. """ + _version_check_interval = 5 * 60 * 1000 def __init__(self, tray_widget, main_window): self.tray_widget = tray_widget @@ -46,6 +82,9 @@ class TrayManager: self.errors = [] + self._version_check_timer = None + self._version_dialog = None + self.main_thread_timer = None self._main_thread_callbacks = collections.deque() self._execution_in_progress = None @@ -62,6 +101,24 @@ class TrayManager: if callback: self.execute_in_main_thread(callback) + def _on_version_check_timer(self): + # Check if is running from build and stop future validations if yes + if not is_running_from_build(): + self._version_check_timer.stop() + return + + self.validate_openpype_version() + + def validate_openpype_version(self): + if is_current_version_studio_latest(): + return + + if self._version_dialog is None: + self._version_dialog = VersionDialog() + result = self._version_dialog.exec_() + if result: + self.restart() + def execute_in_main_thread(self, callback, *args, **kwargs): if isinstance(callback, WrappedCallbackItem): item = callback @@ -123,6 +180,12 @@ class TrayManager: self.main_thread_timer = main_thread_timer + version_check_timer = QtCore.QTimer() + version_check_timer.setInterval(self._version_check_interval) + version_check_timer.timeout.connect(self._on_version_check_timer) + version_check_timer.start() + self._version_check_timer = version_check_timer + # For storing missing settings dialog self._settings_validation_dialog = None @@ -207,7 +270,7 @@ class TrayManager: self.tray_widget.menu.addAction(version_action) self.tray_widget.menu.addSeparator() - def restart(self): + def restart(self, reset_version=True): """Restart Tray tool. First creates new process with same argument and close current tray. @@ -221,7 +284,9 @@ class TrayManager: additional_args.pop(0) args.extend(additional_args) - kwargs = {} + kwargs = { + "env": dict(os.environ.items()) + } if platform.system().lower() == "windows": flags = ( subprocess.CREATE_NEW_PROCESS_GROUP @@ -229,6 +294,9 @@ class TrayManager: ) kwargs["creationflags"] = flags + if reset_version: + kwargs["env"].pop("OPENPYPE_VERSION", None) + subprocess.Popen(args, **kwargs) self.exit() From 7d283f55558f203cd98265ec327be6e2caa5fd53 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 Jan 2022 17:08:37 +0100 Subject: [PATCH 09/39] moved code from pype_info to openpype_version and fixed few bugs --- openpype/lib/__init__.py | 2 +- openpype/lib/openpype_version.py | 117 ++++++++++++++++++++++++++++++- openpype/lib/pype_info.py | 89 +---------------------- openpype/resources/__init__.py | 2 +- 4 files changed, 118 insertions(+), 92 deletions(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 65019f3fab..c556f2adc1 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -168,7 +168,7 @@ from .editorial import ( make_sequence_collection ) -from .pype_info import ( +from .openpype_version import ( get_openpype_version, get_build_version, is_running_from_build, diff --git a/openpype/lib/openpype_version.py b/openpype/lib/openpype_version.py index e3a4e1fa3e..839222018c 100644 --- a/openpype/lib/openpype_version.py +++ b/openpype/lib/openpype_version.py @@ -9,9 +9,69 @@ OpenPype version located in build but versions available in remote versions repository or locally available. """ +import os import sys +import openpype.version +from .python_module_tools import import_filepath + + +# ---------------------------------------- +# Functions independent on OpenPypeVersion +# ---------------------------------------- +def get_openpype_version(): + """Version of pype that is currently used.""" + return openpype.version.__version__ + + +def get_build_version(): + """OpenPype version of build.""" + # Return OpenPype version if is running from code + if not is_running_from_build(): + return get_openpype_version() + + # Import `version.py` from build directory + version_filepath = os.path.join( + os.environ["OPENPYPE_ROOT"], + "openpype", + "version.py" + ) + if not os.path.exists(version_filepath): + return None + + module = import_filepath(version_filepath, "openpype_build_version") + return getattr(module, "__version__", None) + + +def is_running_from_build(): + """Determine if current process is running from build or code. + + Returns: + bool: True if running from build. + """ + executable_path = os.environ["OPENPYPE_EXECUTABLE"] + executable_filename = os.path.basename(executable_path) + if "python" in executable_filename.lower(): + return False + return True + + +def is_running_staging(): + """Currently used OpenPype is staging version. + + Returns: + bool: True if openpype version containt 'staging'. + """ + if "staging" in get_openpype_version(): + return True + return False + + +# ---------------------------------------- +# Functions dependent on OpenPypeVersion +# - Make sense to call only in OpenPype process +# ---------------------------------------- def get_OpenPypeVersion(): """Access to OpenPypeVersion class stored in sys modules.""" return sys.modules.get("OpenPypeVersion") @@ -71,15 +131,66 @@ def get_remote_versions(*args, **kwargs): return None -def get_latest_version(*args, **kwargs): +def get_latest_version(staging=None, local=None, remote=None): """Get latest version from repository path.""" + if staging is None: + staging = is_running_staging() if op_version_control_available(): - return get_OpenPypeVersion().get_latest_version(*args, **kwargs) + return get_OpenPypeVersion().get_latest_version( + staging=staging, + local=local, + remote=remote + ) return None -def get_expected_studio_version(staging=False): +def get_expected_studio_version(staging=None): """Expected production or staging version in studio.""" + if staging is None: + staging = is_running_staging() if op_version_control_available(): return get_OpenPypeVersion().get_expected_studio_version(staging) return None + + +def is_current_version_studio_latest(): + """Is currently running OpenPype version which is defined by studio. + + It is not recommended to ask in each process as there may be situations + when older OpenPype should be used. For example on farm. But it does make + sense in processes that can run for a long time. + + Returns: + None: Can't determine. e.g. when running from code or the build is + too old. + bool: True when is using studio + """ + output = None + # Skip if is not running from build + if not is_running_from_build(): + return output + + # Skip if build does not support version control + if not op_version_control_available(): + return output + + # Skip if path to folder with zip files is not accessible + if not openpype_path_is_accessible(): + return output + + # Get OpenPypeVersion class + OpenPypeVersion = get_OpenPypeVersion() + # Convert current version to OpenPypeVersion object + current_version = OpenPypeVersion(version=get_openpype_version()) + + staging = is_running_staging() + # Get expected version (from settings) + expected_version = get_expected_studio_version(staging) + if expected_version is None: + # Look for latest if expected version is not set in settings + expected_version = get_latest_version( + staging=staging, + remote=True + ) + # Check if current version is expected version + return current_version == expected_version diff --git a/openpype/lib/pype_info.py b/openpype/lib/pype_info.py index ea804c8a18..848a505187 100644 --- a/openpype/lib/pype_info.py +++ b/openpype/lib/pype_info.py @@ -5,67 +5,15 @@ import platform import getpass import socket -import openpype.version from openpype.settings.lib import get_local_settings from .execute import get_openpype_execute_args from .local_settings import get_local_site_id -from .python_module_tools import import_filepath from .openpype_version import ( - op_version_control_available, - openpype_path_is_accessible, - get_expected_studio_version, - get_OpenPypeVersion + is_running_from_build, + get_openpype_version ) -def get_openpype_version(): - """Version of pype that is currently used.""" - return openpype.version.__version__ - - -def get_build_version(): - """OpenPype version of build.""" - # Return OpenPype version if is running from code - if not is_running_from_build(): - return get_openpype_version() - - # Import `version.py` from build directory - version_filepath = os.path.join( - os.environ["OPENPYPE_ROOT"], - "openpype", - "version.py" - ) - if not os.path.exists(version_filepath): - return None - - module = import_filepath(version_filepath, "openpype_build_version") - return getattr(module, "__version__", None) - - -def is_running_from_build(): - """Determine if current process is running from build or code. - - Returns: - bool: True if running from build. - """ - executable_path = os.environ["OPENPYPE_EXECUTABLE"] - executable_filename = os.path.basename(executable_path) - if "python" in executable_filename.lower(): - return False - return True - - -def is_running_staging(): - """Currently used OpenPype is staging version. - - Returns: - bool: True if openpype version containt 'staging'. - """ - if "staging" in get_openpype_version(): - return True - return False - - def get_pype_info(): """Information about currently used Pype process.""" executable_args = get_openpype_execute_args() @@ -135,36 +83,3 @@ def extract_pype_info_to_file(dirpath): with open(filepath, "w") as file_stream: json.dump(data, file_stream, indent=4) return filepath - - -def is_current_version_studio_latest(): - """Is currently running OpenPype version which is defined by studio. - - It is not recommended to ask in each process as there may be situations - when older OpenPype should be used. For example on farm. But it does make - sense in processes that can run for a long time. - - Returns: - None: Can't determine. e.g. when running from code or the build is - too old. - bool: True when is using studio - """ - output = None - # Skip if is not running from build - if not is_running_from_build(): - return output - - # Skip if build does not support version control - if not op_version_control_available(): - return output - - # Skip if path to folder with zip files is not accessible - if not openpype_path_is_accessible(): - return output - - # Check if current version is expected version - OpenPypeVersion = get_OpenPypeVersion() - current_version = OpenPypeVersion(get_openpype_version()) - expected_version = get_expected_studio_version(is_running_staging()) - - return current_version == expected_version diff --git a/openpype/resources/__init__.py b/openpype/resources/__init__.py index f463933525..34a833d080 100644 --- a/openpype/resources/__init__.py +++ b/openpype/resources/__init__.py @@ -1,5 +1,5 @@ import os -from openpype.lib.pype_info import is_running_staging +from openpype.lib.openpype_version import is_running_staging RESOURCES_DIR = os.path.dirname(os.path.abspath(__file__)) From 12156b6d90f723d6a96016fb51c3e876415dca8c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 Jan 2022 17:57:27 +0100 Subject: [PATCH 10/39] tray will show info that is outdated and user should restart --- openpype/lib/__init__.py | 2 + openpype/lib/openpype_version.py | 37 ++++++------ openpype/style/data.json | 6 +- openpype/style/style.css | 8 +-- .../project_manager/project_manager/style.py | 2 +- .../project_manager/widgets.py | 2 +- openpype/tools/tray/pype_tray.py | 58 ++++++++++++++++--- 7 files changed, 81 insertions(+), 34 deletions(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index c556f2adc1..a2a16bcc00 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -171,6 +171,7 @@ from .editorial import ( from .openpype_version import ( get_openpype_version, get_build_version, + get_expected_version, is_running_from_build, is_current_version_studio_latest ) @@ -306,6 +307,7 @@ __all__ = [ "get_openpype_version", "get_build_version", + "get_expected_version", "is_running_from_build", "is_current_version_studio_latest", ] diff --git a/openpype/lib/openpype_version.py b/openpype/lib/openpype_version.py index 839222018c..201bf646e9 100644 --- a/openpype/lib/openpype_version.py +++ b/openpype/lib/openpype_version.py @@ -153,6 +153,17 @@ def get_expected_studio_version(staging=None): return None +def get_expected_version(staging=None): + expected_version = get_expected_studio_version(staging) + if expected_version is None: + # Look for latest if expected version is not set in settings + expected_version = get_latest_version( + staging=staging, + remote=True + ) + return expected_version + + def is_current_version_studio_latest(): """Is currently running OpenPype version which is defined by studio. @@ -166,16 +177,13 @@ def is_current_version_studio_latest(): bool: True when is using studio """ output = None - # Skip if is not running from build - if not is_running_from_build(): - return output - - # Skip if build does not support version control - if not op_version_control_available(): - return output - - # Skip if path to folder with zip files is not accessible - if not openpype_path_is_accessible(): + # Skip if is not running from build or build does not support version + # control or path to folder with zip files is not accessible + if ( + not is_running_from_build() + or not op_version_control_available() + or not openpype_path_is_accessible() + ): return output # Get OpenPypeVersion class @@ -183,14 +191,7 @@ def is_current_version_studio_latest(): # Convert current version to OpenPypeVersion object current_version = OpenPypeVersion(version=get_openpype_version()) - staging = is_running_staging() # Get expected version (from settings) - expected_version = get_expected_studio_version(staging) - if expected_version is None: - # Look for latest if expected version is not set in settings - expected_version = get_latest_version( - staging=staging, - remote=True - ) + expected_version = get_expected_version() # Check if current version is expected version return current_version == expected_version diff --git a/openpype/style/data.json b/openpype/style/data.json index b3dffd7c71..6e1b6e822b 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -51,8 +51,10 @@ "border-hover": "rgba(168, 175, 189, .3)", "border-focus": "rgb(92, 173, 214)", - "delete-btn-bg": "rgb(201, 54, 54)", - "delete-btn-bg-disabled": "rgba(201, 54, 54, 64)", + "warning-btn-bg": "rgb(201, 54, 54)", + + "warning-btn-bg": "rgb(201, 54, 54)", + "warning-btn-bg-disabled": "rgba(201, 54, 54, 64)", "tab-widget": { "bg": "#21252B", diff --git a/openpype/style/style.css b/openpype/style/style.css index 7f7f30e2bc..65e8d0cb40 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -734,11 +734,11 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: {color:bg-view-hover}; } -#DeleteButton { - background: {color:delete-btn-bg}; +#WarningButton { + background: {color:warning-btn-bg}; } -#DeleteButton:disabled { - background: {color:delete-btn-bg-disabled}; +#WarningButton:disabled { + background: {color:warning-btn-bg-disabled}; } /* Launcher specific stylesheets */ diff --git a/openpype/tools/project_manager/project_manager/style.py b/openpype/tools/project_manager/project_manager/style.py index 9fa7a5520b..980c637bca 100644 --- a/openpype/tools/project_manager/project_manager/style.py +++ b/openpype/tools/project_manager/project_manager/style.py @@ -95,7 +95,7 @@ class ResourceCache: def get_warning_pixmap(cls): src_image = get_warning_image() colors = get_objected_colors() - color_value = colors["delete-btn-bg"] + color_value = colors["warning-btn-bg"] return paint_image_with_color( src_image, diff --git a/openpype/tools/project_manager/project_manager/widgets.py b/openpype/tools/project_manager/project_manager/widgets.py index 4b5aca35ef..e58dcc7d0c 100644 --- a/openpype/tools/project_manager/project_manager/widgets.py +++ b/openpype/tools/project_manager/project_manager/widgets.py @@ -369,7 +369,7 @@ class ConfirmProjectDeletion(QtWidgets.QDialog): cancel_btn = QtWidgets.QPushButton("Cancel", self) cancel_btn.setToolTip("Cancel deletion of the project") confirm_btn = QtWidgets.QPushButton("Permanently Delete Project", self) - confirm_btn.setObjectName("DeleteButton") + confirm_btn.setObjectName("WarningButton") confirm_btn.setEnabled(False) confirm_btn.setToolTip("Confirm deletion") diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 5af82b2c64..c32cf17e18 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -17,7 +17,9 @@ from openpype.api import ( from openpype.lib import ( get_openpype_execute_args, is_current_version_studio_latest, - is_running_from_build + is_running_from_build, + get_expected_version, + get_openpype_version ) from openpype.modules import TrayModulesManager from openpype import style @@ -32,15 +34,30 @@ from .pype_info_widget import PypeInfoWidget class VersionDialog(QtWidgets.QDialog): + restart_requested = QtCore.Signal() + + _min_width = 400 + _min_height = 130 + def __init__(self, parent=None): super(VersionDialog, self).__init__(parent) - - label_widget = QtWidgets.QLabel( - "Your version does not match to studio version", self + self.setWindowTitle("Wrong OpenPype version") + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) + self.setWindowIcon(icon) + self.setWindowFlags( + self.windowFlags() + | QtCore.Qt.WindowStaysOnTopHint ) + self.setMinimumWidth(self._min_width) + self.setMinimumHeight(self._min_height) + + label_widget = QtWidgets.QLabel(self) + label_widget.setWordWrap(True) + ignore_btn = QtWidgets.QPushButton("Ignore", self) - restart_btn = QtWidgets.QPushButton("Restart and Install", self) + ignore_btn.setObjectName("WarningButton") + restart_btn = QtWidgets.QPushButton("Restart and Change", self) btns_layout = QtWidgets.QHBoxLayout() btns_layout.addStretch(1) @@ -55,10 +72,22 @@ class VersionDialog(QtWidgets.QDialog): ignore_btn.clicked.connect(self._on_ignore) restart_btn.clicked.connect(self._on_reset) + self._label_widget = label_widget + + self.setStyleSheet(style.load_stylesheet()) + + def update_versions(self, current_version, expected_version): + message = ( + "Your OpenPype version {} does" + " not match to studio version {}" + ).format(str(current_version), str(expected_version)) + self._label_widget.setText(message) + def _on_ignore(self): self.reject() def _on_reset(self): + self.restart_requested.emit() self.accept() @@ -115,9 +144,22 @@ class TrayManager: if self._version_dialog is None: self._version_dialog = VersionDialog() - result = self._version_dialog.exec_() - if result: - self.restart() + self._version_dialog.restart_requested.connect( + self._restart_and_install + ) + + if self._version_dialog.isVisible(): + return + + expected_version = get_expected_version() + current_version = get_openpype_version() + self._version_dialog.update_versions( + current_version, expected_version + ) + self._version_dialog.exec_() + + def _restart_and_install(self): + self.restart() def execute_in_main_thread(self, callback, *args, **kwargs): if isinstance(callback, WrappedCallbackItem): From 644711c9d61f34e83b5f821d833c597b032a37b5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 Jan 2022 18:16:23 +0100 Subject: [PATCH 11/39] status action gives information about openpype version --- .../ftrack/scripts/sub_event_status.py | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/openpype/modules/default_modules/ftrack/scripts/sub_event_status.py b/openpype/modules/default_modules/ftrack/scripts/sub_event_status.py index 004f61338c..3163642e3f 100644 --- a/openpype/modules/default_modules/ftrack/scripts/sub_event_status.py +++ b/openpype/modules/default_modules/ftrack/scripts/sub_event_status.py @@ -16,8 +16,14 @@ from openpype_modules.ftrack.ftrack_server.lib import ( TOPIC_STATUS_SERVER_RESULT ) from openpype.api import Logger +from openpype.lib import ( + is_current_version_studio_latest, + is_running_from_build, + get_expected_version, + get_openpype_version +) -log = Logger().get_logger("Event storer") +log = Logger.get_logger("Event storer") action_identifier = ( "event.server.status" + os.environ["FTRACK_EVENT_SUB_ID"] ) @@ -203,8 +209,57 @@ class StatusFactory: }) return items + def openpype_version_items(self): + items = [] + is_latest = is_current_version_studio_latest() + items.append({ + "type": "label", + "value": "# OpenPype version" + }) + if not is_running_from_build(): + items.append({ + "type": "label", + "value": ( + "OpenPype event server is running from code {}." + ).format(str(get_openpype_version())) + }) + + elif is_latest is None: + items.append({ + "type": "label", + "value": ( + "Can't determine if OpenPype version is outdated" + " {}. OpenPype build version should be updated." + ).format(str(get_openpype_version())) + }) + elif is_latest: + items.append({ + "type": "label", + "value": "OpenPype version is up to date {}.".format( + str(get_openpype_version()) + ) + }) + else: + items.append({ + "type": "label", + "value": ( + "Using outdated OpenPype version {}." + " Expected version is {}." + "
- Please restart event server for automatic" + " updates or update manually." + ).format( + str(get_openpype_version()), + str(get_expected_version()) + ) + }) + + items.append({"type": "label", "value": "---"}) + + return items + def items(self): items = [] + items.extend(self.openpype_version_items()) items.append(self.note_item) items.extend(self.bool_items()) From 4ee86a6ce27f0a56b88926719c61bab308e5c144 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 Jan 2022 18:26:13 +0100 Subject: [PATCH 12/39] show tray message when update dialog is ignored --- openpype/tools/tray/pype_tray.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index c32cf17e18..17251b404f 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -35,6 +35,7 @@ from .pype_info_widget import PypeInfoWidget class VersionDialog(QtWidgets.QDialog): restart_requested = QtCore.Signal() + ignore_requested = QtCore.Signal() _min_width = 400 _min_height = 130 @@ -73,9 +74,19 @@ class VersionDialog(QtWidgets.QDialog): restart_btn.clicked.connect(self._on_reset) self._label_widget = label_widget + self._restart_accepted = False self.setStyleSheet(style.load_stylesheet()) + def showEvent(self, event): + super().showEvent(event) + self._restart_accepted = False + + def closeEvent(self, event): + super().closeEvent(event) + if not self._restart_accepted: + self.ignore_requested.emit() + def update_versions(self, current_version, expected_version): message = ( "Your OpenPype version {} does" @@ -87,6 +98,7 @@ class VersionDialog(QtWidgets.QDialog): self.reject() def _on_reset(self): + self._restart_accepted = True self.restart_requested.emit() self.accept() @@ -147,6 +159,9 @@ class TrayManager: self._version_dialog.restart_requested.connect( self._restart_and_install ) + self._version_dialog.ignore_requested.connect( + self._outdated_version_ignored + ) if self._version_dialog.isVisible(): return @@ -161,6 +176,15 @@ class TrayManager: def _restart_and_install(self): self.restart() + def _outdated_version_ignored(self): + self.show_tray_message( + "Outdated OpenPype version", + ( + "Please update your OpenPype as soon as possible." + " All you have to do is to restart tray." + ) + ) + def execute_in_main_thread(self, callback, *args, **kwargs): if isinstance(callback, WrappedCallbackItem): item = callback From 687181e3825373894111f8a6267ad9fd9fe99917 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 Jan 2022 10:58:36 +0100 Subject: [PATCH 13/39] interval of validation can be modified --- .../defaults/system_settings/general.json | 1 + .../schemas/system_schema/schema_general.json | 13 +++++++++++++ openpype/tools/tray/pype_tray.py | 17 ++++++++++++----- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/openpype/settings/defaults/system_settings/general.json b/openpype/settings/defaults/system_settings/general.json index a07152eaf8..7c78de9a5c 100644 --- a/openpype/settings/defaults/system_settings/general.json +++ b/openpype/settings/defaults/system_settings/general.json @@ -4,6 +4,7 @@ "admin_password": "", "production_version": "", "staging_version": "", + "version_check_interval": 5, "environment": { "__environment_keys__": { "global": [] diff --git a/openpype/settings/entities/schemas/system_schema/schema_general.json b/openpype/settings/entities/schemas/system_schema/schema_general.json index b4c83fc85f..3af3f5ce35 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_general.json +++ b/openpype/settings/entities/schemas/system_schema/schema_general.json @@ -47,6 +47,19 @@ { "type": "splitter" }, + { + "type": "label", + "label": "Trigger validation if running OpenPype is using studio defined version each 'n' minutes. Validation happens in OpenPype tray application." + }, + { + "type": "number", + "key": "version_check_interval", + "label": "Version check interval", + "minimum": 0 + }, + { + "type": "splitter" + }, { "key": "environment", "label": "Environment", diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 17251b404f..de1a8577b0 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -108,8 +108,6 @@ class TrayManager: Load submenus, actions, separators and modules into tray's context. """ - _version_check_interval = 5 * 60 * 1000 - def __init__(self, tray_widget, main_window): self.tray_widget = tray_widget self.main_window = main_window @@ -117,7 +115,15 @@ class TrayManager: self.log = Logger.get_logger(self.__class__.__name__) - self.module_settings = get_system_settings()["modules"] + system_settings = get_system_settings() + self.module_settings = system_settings["modules"] + + version_check_interval = system_settings["general"].get( + "version_check_interval" + ) + if version_check_interval is None: + version_check_interval = 5 + self._version_check_interval = version_check_interval * 60 * 1000 self.modules_manager = TrayModulesManager() @@ -247,9 +253,10 @@ class TrayManager: self.main_thread_timer = main_thread_timer version_check_timer = QtCore.QTimer() - version_check_timer.setInterval(self._version_check_interval) version_check_timer.timeout.connect(self._on_version_check_timer) - version_check_timer.start() + if self._version_check_interval > 0: + version_check_timer.setInterval(self._version_check_interval) + version_check_timer.start() self._version_check_timer = version_check_timer # For storing missing settings dialog From 0ebd7881c144a16e98a8923c4f5e2f8ea22a355e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 Jan 2022 11:40:38 +0100 Subject: [PATCH 14/39] fixed reseting from staging --- openpype/tools/tray/pype_tray.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index de1a8577b0..7f78140211 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -18,6 +18,7 @@ from openpype.lib import ( get_openpype_execute_args, is_current_version_studio_latest, is_running_from_build, + is_running_staging, get_expected_version, get_openpype_version ) @@ -349,17 +350,25 @@ class TrayManager: First creates new process with same argument and close current tray. """ args = get_openpype_execute_args() + kwargs = { + "env": dict(os.environ.items()) + } + # Create a copy of sys.argv additional_args = list(sys.argv) # Check last argument from `get_openpype_execute_args` # - when running from code it is the same as first from sys.argv if args[-1] == additional_args[0]: additional_args.pop(0) - args.extend(additional_args) - kwargs = { - "env": dict(os.environ.items()) - } + # Pop OPENPYPE_VERSION + if reset_version: + # Add staging flag if was running from staging + if is_running_staging(): + args.append("--use-staging") + kwargs["env"].pop("OPENPYPE_VERSION", None) + + args.extend(additional_args) if platform.system().lower() == "windows": flags = ( subprocess.CREATE_NEW_PROCESS_GROUP @@ -367,9 +376,6 @@ class TrayManager: ) kwargs["creationflags"] = flags - if reset_version: - kwargs["env"].pop("OPENPYPE_VERSION", None) - subprocess.Popen(args, **kwargs) self.exit() From 1150de03b307105f39d99a6f96ec8cab5a0ccb2b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 13 Jan 2022 11:02:21 +0100 Subject: [PATCH 15/39] format output arguments with anatomy data --- openpype/plugins/publish/extract_review.py | 25 +++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index b6c2e49385..be29c7bf9c 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -187,6 +187,7 @@ class ExtractReview(pyblish.api.InstancePlugin): outputs_per_repres = self._get_outputs_per_representations( instance, profile_outputs ) + fill_data = copy.deepcopy(instance.data["anatomyData"]) for repre, outputs in outputs_per_repres: # Check if input should be preconverted before processing # Store original staging dir (it's value may change) @@ -293,7 +294,7 @@ class ExtractReview(pyblish.api.InstancePlugin): try: # temporary until oiiotool is supported cross platform ffmpeg_args = self._ffmpeg_arguments( - output_def, instance, new_repre, temp_data + output_def, instance, new_repre, temp_data, fill_data ) except ZeroDivisionError: if 'exr' in temp_data["origin_repre"]["ext"]: @@ -446,7 +447,9 @@ class ExtractReview(pyblish.api.InstancePlugin): "handles_are_set": handles_are_set } - def _ffmpeg_arguments(self, output_def, instance, new_repre, temp_data): + def _ffmpeg_arguments( + self, output_def, instance, new_repre, temp_data, fill_data + ): """Prepares ffmpeg arguments for expected extraction. Prepares input and output arguments based on output definition and @@ -472,9 +475,6 @@ class ExtractReview(pyblish.api.InstancePlugin): ffmpeg_input_args = [ value for value in _ffmpeg_input_args if value.strip() ] - ffmpeg_output_args = [ - value for value in _ffmpeg_output_args if value.strip() - ] ffmpeg_video_filters = [ value for value in _ffmpeg_video_filters if value.strip() ] @@ -482,6 +482,21 @@ class ExtractReview(pyblish.api.InstancePlugin): value for value in _ffmpeg_audio_filters if value.strip() ] + ffmpeg_output_args = [] + for value in _ffmpeg_output_args: + value = value.strip() + if not value: + continue + try: + value = value.format(**fill_data) + except Exception: + self.log.warning( + "Failed to format ffmpeg argument: {}".format(value), + exc_info=True + ) + pass + ffmpeg_output_args.append(value) + # Prepare input and output filepaths self.input_output_paths(new_repre, output_def, temp_data) From eed31e543372d3616433aa08d8250993fbc7d0e2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 14 Jan 2022 15:56:36 +0100 Subject: [PATCH 16/39] burnins fix bit rate for dnxhd mxf passing metadata to burnins --- openpype/scripts/otio_burnin.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index 15a62ef38e..63a8b064db 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -157,6 +157,16 @@ def _dnxhd_codec_args(stream_data, source_ffmpeg_cmd): if pix_fmt: output.extend(["-pix_fmt", pix_fmt]) + # Use arguments from source if are available source arguments + if source_ffmpeg_cmd: + copy_args = ( + "-b:v", "-vb", + ) + args = source_ffmpeg_cmd.split(" ") + for idx, arg in enumerate(args): + if arg in copy_args: + output.extend([arg, args[idx + 1]]) + output.extend(["-g", "1"]) return output @@ -715,6 +725,15 @@ def burnins_from_data( ffmpeg_args.extend( get_codec_args(burnin.ffprobe_data, source_ffmpeg_cmd) ) + # Use arguments from source if are available source arguments + if source_ffmpeg_cmd: + copy_args = ( + "-metadata", + ) + args = source_ffmpeg_cmd.split(" ") + for idx, arg in enumerate(args): + if arg in copy_args: + ffmpeg_args.extend([arg, args[idx + 1]]) # Use group one (same as `-intra` argument, which is deprecated) ffmpeg_args_str = " ".join(ffmpeg_args) From 17578c54471ad931bc48afc82b5aa8ccd4a29908 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 Jan 2022 16:20:05 +0100 Subject: [PATCH 17/39] fix import if 'is_running_staging' --- openpype/lib/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index a2a16bcc00..62d204186d 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -173,6 +173,7 @@ from .openpype_version import ( get_build_version, get_expected_version, is_running_from_build, + is_running_staging, is_current_version_studio_latest ) @@ -309,5 +310,6 @@ __all__ = [ "get_build_version", "get_expected_version", "is_running_from_build", + "is_running_staging", "is_current_version_studio_latest", ] From ce5c70e28d99d1528ab12e75be91c1a219b38aae Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 Jan 2022 17:47:11 +0100 Subject: [PATCH 18/39] change back project manager styles --- openpype/style/data.json | 5 ++--- openpype/style/style.css | 13 +++++++++---- .../tools/project_manager/project_manager/style.py | 2 +- .../project_manager/project_manager/widgets.py | 2 +- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/openpype/style/data.json b/openpype/style/data.json index 6e1b6e822b..c8adc0674a 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -51,10 +51,9 @@ "border-hover": "rgba(168, 175, 189, .3)", "border-focus": "rgb(92, 173, 214)", - "warning-btn-bg": "rgb(201, 54, 54)", - "warning-btn-bg": "rgb(201, 54, 54)", - "warning-btn-bg-disabled": "rgba(201, 54, 54, 64)", + "delete-btn-bg": "rgb(201, 54, 54)", + "delete-btn-bg-disabled": "rgba(201, 54, 54, 64)", "tab-widget": { "bg": "#21252B", diff --git a/openpype/style/style.css b/openpype/style/style.css index 65e8d0cb40..d9b0ff7421 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -734,11 +734,11 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: {color:bg-view-hover}; } -#WarningButton { - background: {color:warning-btn-bg}; +#DeleteButton { + background: {color:delete-btn-bg}; } -#WarningButton:disabled { - background: {color:warning-btn-bg-disabled}; +#DeleteButton:disabled { + background: {color:delete-btn-bg-disabled}; } /* Launcher specific stylesheets */ @@ -1228,6 +1228,11 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: #21252B; } +/* Tray */ +#TrayRestartButton { + background: {color:restart-btn-bg}; +} + /* Globally used names */ #Separator { background: {color:bg-menu-separator}; diff --git a/openpype/tools/project_manager/project_manager/style.py b/openpype/tools/project_manager/project_manager/style.py index 980c637bca..9fa7a5520b 100644 --- a/openpype/tools/project_manager/project_manager/style.py +++ b/openpype/tools/project_manager/project_manager/style.py @@ -95,7 +95,7 @@ class ResourceCache: def get_warning_pixmap(cls): src_image = get_warning_image() colors = get_objected_colors() - color_value = colors["warning-btn-bg"] + color_value = colors["delete-btn-bg"] return paint_image_with_color( src_image, diff --git a/openpype/tools/project_manager/project_manager/widgets.py b/openpype/tools/project_manager/project_manager/widgets.py index e58dcc7d0c..4b5aca35ef 100644 --- a/openpype/tools/project_manager/project_manager/widgets.py +++ b/openpype/tools/project_manager/project_manager/widgets.py @@ -369,7 +369,7 @@ class ConfirmProjectDeletion(QtWidgets.QDialog): cancel_btn = QtWidgets.QPushButton("Cancel", self) cancel_btn.setToolTip("Cancel deletion of the project") confirm_btn = QtWidgets.QPushButton("Permanently Delete Project", self) - confirm_btn.setObjectName("WarningButton") + confirm_btn.setObjectName("DeleteButton") confirm_btn.setEnabled(False) confirm_btn.setToolTip("Confirm deletion") From a18bdbc418e004bb8d3c0606296d1649872fa74b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 14 Jan 2022 17:53:56 +0100 Subject: [PATCH 19/39] changed dialog and added restart and update action to tray --- openpype/style/data.json | 1 + openpype/tools/tray/images/gifts.png | Bin 0 -> 8605 bytes openpype/tools/tray/pype_tray.py | 118 +++++++++++++++++++++++---- openpype/tools/utils/__init__.py | 6 +- 4 files changed, 108 insertions(+), 17 deletions(-) create mode 100644 openpype/tools/tray/images/gifts.png diff --git a/openpype/style/data.json b/openpype/style/data.json index c8adc0674a..1db0c732cf 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -51,6 +51,7 @@ "border-hover": "rgba(168, 175, 189, .3)", "border-focus": "rgb(92, 173, 214)", + "restart-btn-bg": "#458056", "delete-btn-bg": "rgb(201, 54, 54)", "delete-btn-bg-disabled": "rgba(201, 54, 54, 64)", diff --git a/openpype/tools/tray/images/gifts.png b/openpype/tools/tray/images/gifts.png new file mode 100644 index 0000000000000000000000000000000000000000..57fb3f286312878c641f27362653ede1b7746810 GIT binary patch literal 8605 zcmcI{2UL??x^6-ZC=dh{>Ai{|O#x{EK~W$y>7alhMX8YvK|>V~q)C zqbJrZd;!&r3J%rJXwiSe&A(ybsPh2Kb@xS=A`SJUhG4GSalGvGZ?6(7C=h)o&F|UYA7g9rsKvq4y2D>fB~rvfT62J(tgS%Q|ATR0YIzHC7>Ov(E(s zq!bQT)zq{TuGxf|>i97g5wIa5w`x~1T2YXEdi`w8@tGCd*BEFw(n)XzdoX_e3mB5~ zId^>kBR0}Avm?sS1?=GU-DNK-b!Ih3 zMquzbBIDtpTJ`SR*VtmzDPRF-45gNpC*8vX0~1>Y=_9Bo7DH+QAH%@orWCHo4#haL ztC@v`8`K=?<_wKQvtzxami-4CM{gUs|@w$R7rEM zQA46edJJvTjkRzsvw9T7xPcCOzu6a;A?+g;PYUpU)kP{>)JH}RSs4K7^BwbU$cb%4 znXat)%Wwu);UqwM7WSdt@P`3@@!RDsByrPOdRuq#Qp*DU&YMpjsiMWoJKhx*J0;MU zQiD}9JoSs>Vy17GcVGldw(TVdAPl3ZB)~ zPGx!C5ZerKO7a{nFA_r&qg5UNax^F$4i(vZ6CGM3-8cTqrM$A;o&fQopduyaap@p@ zFr{#<>bIaaaU;vhS&m|Kti5)`jz-N#m`Bm-J2VNPzMQKNTp6;Zu!@4 zuU87*(+{U6WA{8?xs|6rlv>5@xk@=y0A? zq68K}Hu|S}N<;U`ahM>bWwBYal){DP!VxW%Qt0DsF>WbVu`v+`b&I zNG_1UrY0I?xpT96YWMYQiFa+84Yf$-lf>)nSY$=*+@KN<%Eh1sw(C?LQGQb$kea&< zE-96NIa&=0(DOf|mfGw^-VfAsa-nzhZbAACwo9FTe0DWl{NAqJ*9-y)A;>dWQB+$m zX2r%VJ77e4RxAxk0}skEX^sl452_FBeh0nFls4Sbk*jmx#?&S(8q4?43w4@OI2SD@ zz^h}XOXry)E`^{7gF0Td`Ov?tp+?g#S~tT`+R*-1YH*53?hwO_>iz;uqDxQX7iT^b z#~O87Ys2>5M&l&YN02f-?W-l)NJCR}&@~1>6zS-95V1CT9HoNoDQgPo@+=A@06wI_}0X zO669`8I8mpNWyMu5bhCQ_6nAjZ#0FpDV4<4g}^b4GruUa#K;z>LBbaEB)r^vA3-sK zRD!v}cLw3b_{v~gVT|=i+E7hp2!eX1tO};;K8AYvEYHW@n)2?Bm-47PvNfN}4XAOF zWnfHqrC*fH;1ld;h|nOcUzp#U9&+8N@QI;_fJ)V2WwMG#UFjXERCoZH4MFyfhyfcz z^Ny#3HVq$$Oq_09m>Z_1NqsCLxH^B**Y0ga_*)d2CiNiyT`ecp6I0Ph{X=k*Tls}j zMsa*q11Nh~I)oo_)i5e=HKmP2g*RngO^1y$JpID=ijFON%SrM@Xz?M{Q1+a{5y}g> z-CbR3?ZAo}>W-{^_4SU+#rb{H_OB2kcET`fMUsj#>JH^*b%zkDTOSL}B_tqY^%SNL zY`zB*kH1w5hwhJ?x$s4N6gDHRz~@n)x&W@wmVY66I@E6D^hRNLL#Nbp?u9W2_-VsgQAcgSVTu2OYhBLVtXaT z$qSx3Wik)Tg`;Dywm}RtY!nVUdP3Tr7Rk7Sk}BTQC)^JSi`j@bnFm>8UhGrU%Ap|Y?8}P0RMpJpr5^f zwXti+<~x8(`xr~e4-kW>fe2L-J?)6+NV8)GZZaveLY;L~sHna2TKH2`ZbC(L%zD8d zqk$AyMKS1!ZWbm)dPv{DE`&7nIBCW@L~6*;|N0ebTI;PbmaI%jE6o*Jy*=(9k^TX; zDo`jRsw$4!v8_#d()DVyMgXH%;LVjs#?VPu@y5WL1{rZgB+U;6J&M1`@1g<^4*1l zHs1-GO;&@Yh7QNfG`{)&5XztwXY#I@TrDol^%>IDTQvhv@d)h%`|f~xWf>eVb_ z1y1#ZmNja%>xLD=$($o>GdN&n>qAYRnlxhCF1p1Fs(jg3`Soh*2aY`rIK&^Iwe;39 zJ!x4vgJwL&-t7%*-oZNg(9im`yLlS8Ez^ppPYmSc*J9P{3Tl9|;3Fjt*G#DbSz^%E zqHL>IDYSL@RXE0$&~7F#gg&pQ;L*g(z3Z^+6a$C@NwGXQMrkDYcs4E-YpIryu*#h>FUB2?{< zU?XOz@7sv$cGXfyI051=G^dX7S4~T;sy3tPu#!o(a!5fveizNC?jDHCzd;N-QVWQv zsGk|nAL?Kz@AAu!$@5_EC<$L57NMdsEhZg`BOt0jE>iQtYL+ai$J>>Tf3H0mxA_=V z^5G3jDogP`E$ci-iQy?{ZUBkROQK@uSQ497{WvpGifuz*qiL1Py}9RBAla;#0b^8o zWh-{o@9S4SWcX1nw^gfUsB6WZP>l1l{8#Pq4znS$S)Vm|3{Gx2+Y*(ymrJ$eoYgMB z1L1)-%iIUP(In9yH<8AcwaGd^%M5ItwTnvGtpc5=ZTByqiv(>pS;zTK4{3^W_%7@W z%ap8^AO!ZI3<3v*4EEu*j`>ThUsKbcu357V3mN7 zUD+IobF%Jc56|eS>)Z21jskTF@jE8SOMLRd*Lvxu)rk2+epS^c98IGDf6>W6f`?UEf^EHctEo;rB#i7>>kA-mbUad&^-T~U< z1haRIS&xYve6my5Z=^Kb9j$m{%+ZYq4bIODvJjo=*DR0^Wz9D>cf5Yp*0RuQNB{Gg zs%DvK@UXXv(o(qa{m92TH^L8_%948XKupsw(=g_DX5=*Cv(cs`2YqX0$$b^nHJ=_5 z)-iV;O{)Umpb1$Pr{C;`HmmH2-{vN`Ix6q*eg87HnU?i%?t069Z<&YZFG=at9b>0DOL0T@2$dnuuKKtP;@O96YhcjA^X7bm) z_ycaMN(LA^F=$=ZU1H~Z@im;Rg7(CV-BGBONm8E7QBiMDfi_wDkK2mJi|vT+mnlsu zC&>wJZpW88LFC}czQ(pq~ za_}!lKELxD-+u`y7F%T0pOIM3w?BRBFKQpuvOkhrQ=#PFUp21Yll5V6#ZARS^Kv6 z=2r_6_VwH-`m=%=J3Itts~TZ@Hk>17`R()>6)1FKKD603e;=DAu%}YDRFoF~<%{IC z1+ZUSplqpAef-nH5T9HO^X5nkO)IBWlJtAUIPPN>w<`kDY z=D3)$(<#8(>r4XX+9P^ewX|X2QYYF+oj7|q2{FO5;;7IjGtj(Z?aSp)sX&WF^WW>a zTcyIiw^==~M^Cp*Q(0R(v10wERTKU+&zu8&S6C%!mbAz3uv6I*Kg2$dkhZJIM4bf@ z5YaroT%-8fahGA1T=Bi~*TKQ$&H-PGy8B-|rL}fRIjS6%vCEl(uvZb^pkOC!*EXgf zSegc8*xE{`%v9xEW8ssfrKoKN=fg|l(Y^~Wx>NY%LA^3;Zf)J_Pi*%6@8)_P3E0(9 zc2t*Jd`&Je%5(|qaSMyqJ&zzjY#Lv24yr!XdFh{cV;aLFbyDqnsl?J4Lqvr%L=IAD zRMkYcR>>&>A?{LvlWd}lgpKZGz_HK))H&I^fd!wNj=WS8#{8f;oww9Dn+5! zj{B>Z9#zR9&9_l}CD z;LzP%^~cE9RH?NQ=yM;;m<3nwXK$_GP?9m4`}h^2yeXvHiaaG@%fd`Qb}?<&rBkhF zR#j?v<@2KMV;3yn2~~JAy<9LncU>N!QQtT}N!&50tnf{HM}VNO!u3Q{R=_ICb^}3ULD!u+Uk56I(_dBayw#%l zQ7*ms@v)pc)^$Js!6im)tYSw72`^?DO+#iZ>*c2M(W_71xe~Br{kU6 z=q_BW$~FwrPB(u=)4{cqwzw!X>no5wT06&{a+`>^0Bp2)s+}N`D*2Q82T+T zIo(_c^V(v1BhCbakaPUqhBl$;s+3Iy0N7pVqLCi_#kBPLmt+SoI7*ba%z@ zE9AO{V9I1~mKJ)`85X~|M_Du3+0_Fp=NS1?qYW0P(UslaMIY}cF3x-EEwfT#o~VfK zybkKxb`&<1QW8ipo+5p9D<~s`Tj2Bij0_bflN|!4UvpUBlq#?9t^J3mV!i#tQyM_- zgiEwcHC}On@5$}ch&^f)uhIG7%w9{4&T%Y$(S)}z2!(!l?Rr>ICPF2*ik1;NXj@~B zsE?8SQX5;0O(_py9vptqKOjI^;+whdbdnXhXc!g55+&w#Vz2^Ot&+{_*(tsiRvSVe zi_OSeQEe5aTRL1a@$33xA~Lk%J&qM@#ki3Xf7D6Ee;^t%Fls!DRb}+OG%eA)PNAJX zmE4_^ijz7Y-X%e00iqw){3K`eR)HQIre9vCyOT@j$_L9e~C z(3b4~VcY4uk(J8d&#N|MA1m!`sbS3KSATx(7RFv}h&Oim9;y+3Vinpv66AVlqmps)Zq~qQMT){{S!tWNf1Ud==!Mg;EJVw|FaJPttOR208+QzOaNa) zUYXK?rHf9m!u_BMig+`B-1pfCH(FMrbJa439MSB{eEg5aP7Y@3aqxUCc*s)S?)xxv zdCm0q{kYYXAoQy=KqE=#po~exmeRs|mXqi5d?G(cc0{SP=)K_R`n+aIVo zJJCmtyP=&0U;{7-H^}`|RLP6s!+(##+303JwL(;adFt?=1c-M0P8vdzhvHAbzmWRN z`Z>G>)~_&$$jc@q)Ho>e7a^zqTc$S42LX_(Jdj-pNbtWF{-2IB0DSTY zF!6)`fZ^#C@<_5DEQrnb)c>Ii{>;JuTVY)!3t!Yqe|h;%wDmuC!Jl*WFI)D{VE^3V z|GHiORb5?l`TwuN`rYCGm@oeh?BAN^{~9g-4l9`T@2zt&E4;Xw;O z7@v#LxZC_aXW>JHm%DDqj(6%4U+7Kv?(2O_y1iseX5rG&B*KTk(X+wEcvFSKF(Vqf zRK=0J@P6{|c)qnf^}I;LbLD+LM^a1uJP;AUYesjQ?{)7LZS2ccpp%jWCC&2_J3r&8 zer$UbYwy?jHtitki-DQ27U?7p13qWM2HwR}JE(K8l^{U*!1XyJhO{#2>M&*i5CnAX9ge-kq^PXky3J T%VmTC_`iqu)s>1NPv86(+fBse literal 0 HcmV?d00001 diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 7f78140211..0d3e7ae04c 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -29,11 +29,51 @@ from openpype.settings import ( ProjectSettings, DefaultsNotDefined ) -from openpype.tools.utils import WrappedCallbackItem +from openpype.tools.utils import ( + WrappedCallbackItem, + paint_image_with_color +) from .pype_info_widget import PypeInfoWidget +# 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) + + class VersionDialog(QtWidgets.QDialog): restart_requested = QtCore.Signal() ignore_requested = QtCore.Signal() @@ -43,7 +83,7 @@ class VersionDialog(QtWidgets.QDialog): def __init__(self, parent=None): super(VersionDialog, self).__init__(parent) - self.setWindowTitle("Wrong OpenPype version") + self.setWindowTitle("OpenPype update is needed") icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) self.setWindowIcon(icon) self.setWindowFlags( @@ -54,12 +94,23 @@ class VersionDialog(QtWidgets.QDialog): self.setMinimumWidth(self._min_width) self.setMinimumHeight(self._min_height) - label_widget = QtWidgets.QLabel(self) + top_widget = QtWidgets.QWidget(self) + + gift_pixmap = self._get_gift_pixmap() + gift_icon_label = PixmapLabel(gift_pixmap, top_widget) + + label_widget = QtWidgets.QLabel(top_widget) label_widget.setWordWrap(True) - ignore_btn = QtWidgets.QPushButton("Ignore", self) - ignore_btn.setObjectName("WarningButton") - restart_btn = QtWidgets.QPushButton("Restart and Change", self) + top_layout = QtWidgets.QHBoxLayout(top_widget) + # top_layout.setContentsMargins(0, 0, 0, 0) + top_layout.setSpacing(10) + top_layout.addWidget(gift_icon_label, 0, QtCore.Qt.AlignCenter) + top_layout.addWidget(label_widget, 1) + + ignore_btn = QtWidgets.QPushButton("Later", self) + restart_btn = QtWidgets.QPushButton("Restart && Update", self) + restart_btn.setObjectName("TrayRestartButton") btns_layout = QtWidgets.QHBoxLayout() btns_layout.addStretch(1) @@ -67,7 +118,7 @@ class VersionDialog(QtWidgets.QDialog): btns_layout.addWidget(restart_btn, 0) layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(label_widget, 0) + layout.addWidget(top_widget, 0) layout.addStretch(1) layout.addLayout(btns_layout, 0) @@ -79,6 +130,21 @@ class VersionDialog(QtWidgets.QDialog): self.setStyleSheet(style.load_stylesheet()) + 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) + colors = style.get_objected_colors() + color_value = colors["font"] + + return paint_image_with_color( + src_image, + color_value.get_qcolor() + ) + def showEvent(self, event): super().showEvent(event) self._restart_accepted = False @@ -90,8 +156,8 @@ class VersionDialog(QtWidgets.QDialog): def update_versions(self, current_version, expected_version): message = ( - "Your OpenPype version {} does" - " not match to studio version {}" + "Running OpenPype version is {}." + " Your production has been updated to version {}." ).format(str(current_version), str(expected_version)) self._label_widget.setText(message) @@ -113,6 +179,7 @@ class TrayManager: self.tray_widget = tray_widget self.main_window = main_window self.pype_info_widget = None + self._restart_action = None self.log = Logger.get_logger(self.__class__.__name__) @@ -158,7 +225,14 @@ class TrayManager: self.validate_openpype_version() def validate_openpype_version(self): - if is_current_version_studio_latest(): + using_requested = is_current_version_studio_latest() + self._restart_action.setVisible(not using_requested) + if using_requested: + if ( + self._version_dialog is not None + and self._version_dialog.isVisible() + ): + self._version_dialog.close() return if self._version_dialog is None: @@ -170,25 +244,24 @@ class TrayManager: self._outdated_version_ignored ) - if self._version_dialog.isVisible(): - return - expected_version = get_expected_version() current_version = get_openpype_version() self._version_dialog.update_versions( current_version, expected_version ) - self._version_dialog.exec_() + self._version_dialog.show() + self._version_dialog.raise_() + self._version_dialog.activateWindow() def _restart_and_install(self): self.restart() def _outdated_version_ignored(self): self.show_tray_message( - "Outdated OpenPype version", + "OpenPype version is outdated", ( "Please update your OpenPype as soon as possible." - " All you have to do is to restart tray." + " To update, restart OpenPype Tray application." ) ) @@ -341,9 +414,22 @@ class TrayManager: version_action = QtWidgets.QAction(version_string, self.tray_widget) version_action.triggered.connect(self._on_version_action) + + restart_action = QtWidgets.QAction( + "Restart && Update", self.tray_widget + ) + restart_action.triggered.connect(self._on_restart_action) + restart_action.setVisible(False) + self.tray_widget.menu.addAction(version_action) + self.tray_widget.menu.addAction(restart_action) self.tray_widget.menu.addSeparator() + self._restart_action = restart_action + + def _on_restart_action(self): + self.restart() + def restart(self, reset_version=True): """Restart Tray tool. diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index 65025ac358..eb0cb1eef5 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -6,7 +6,10 @@ from .widgets import ( ) from .error_dialog import ErrorMessageBox -from .lib import WrappedCallbackItem +from .lib import ( + WrappedCallbackItem, + paint_image_with_color +) __all__ = ( @@ -18,4 +21,5 @@ __all__ = ( "ErrorMessageBox", "WrappedCallbackItem", + "paint_image_with_color", ) From ab97a3266a9bfdb563ae74692656f8c8b86e4f4a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Jan 2022 07:05:47 +0000 Subject: [PATCH 20/39] build(deps): bump shelljs from 0.8.4 to 0.8.5 in /website Bumps [shelljs](https://github.com/shelljs/shelljs) from 0.8.4 to 0.8.5. - [Release notes](https://github.com/shelljs/shelljs/releases) - [Changelog](https://github.com/shelljs/shelljs/blob/master/CHANGELOG.md) - [Commits](https://github.com/shelljs/shelljs/compare/v0.8.4...v0.8.5) --- updated-dependencies: - dependency-name: shelljs dependency-type: indirect ... Signed-off-by: dependabot[bot] --- website/yarn.lock | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/website/yarn.lock b/website/yarn.lock index 89da2289de..e34f951572 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -2250,9 +2250,9 @@ bail@^1.0.0: integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ== balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== base16@^1.0.0: version "1.0.0" @@ -4136,9 +4136,9 @@ glob-to-regexp@^0.4.1: integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== glob@^7.0.0, glob@^7.0.3, glob@^7.1.3: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -4825,6 +4825,13 @@ is-core-module@^2.2.0: dependencies: has "^1.0.3" +is-core-module@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" + integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== + dependencies: + has "^1.0.3" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -6167,7 +6174,7 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.6: +path-parse@^1.0.6, path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== @@ -7208,7 +7215,16 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@^1.1.6, resolve@^1.14.2, resolve@^1.3.2: +resolve@^1.1.6: + version "1.21.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.21.0.tgz#b51adc97f3472e6a5cf4444d34bc9d6b9037591f" + integrity sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA== + dependencies: + is-core-module "^2.8.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +resolve@^1.14.2, resolve@^1.3.2: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== @@ -7533,9 +7549,9 @@ shell-quote@1.7.2: integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== shelljs@^0.8.4: - version "0.8.4" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" - integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== + version "0.8.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" + integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== dependencies: glob "^7.0.0" interpret "^1.0.0" @@ -7896,6 +7912,11 @@ supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + svg-parser@^2.0.2: version "2.0.4" resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" From 900650a45cb13024402b37be76154ac8c309bd4e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 17 Jan 2022 13:41:49 +0100 Subject: [PATCH 21/39] Renamed to proper name --- website/docs/module_slack.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/module_slack.md b/website/docs/module_slack.md index 02676d68a8..6c621105e5 100644 --- a/website/docs/module_slack.md +++ b/website/docs/module_slack.md @@ -76,7 +76,7 @@ Burnin version (usually .mp4) is preferred if present. Please be sure that this configuration is viable for your use case. In case of uploading large reviews to Slack, all publishes will be slowed down and you might hit a file limit on Slack pretty soon (it is 5GB for Free version of Slack, any file cannot be bigger than 1GB). -You might try to add `{review_link}` to message content. This link might help users to find review easier on their machines. +You might try to add `{review_filepath}` to message content. This link might help users to find review easier on their machines. (It won't show a playable preview though!) #### Message From 7e25f228a943eb502dfcd04c6a05fcf49a9d241a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 17 Jan 2022 16:50:14 +0100 Subject: [PATCH 22/39] global: extract review output name to anatomy fill data allow to use it in ffmpeg args for metadata --- openpype/plugins/publish/extract_review.py | 23 ++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index be29c7bf9c..0a34dbaf6a 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -292,6 +292,21 @@ class ExtractReview(pyblish.api.InstancePlugin): temp_data["frame_start"], temp_data["frame_end"]) + # create or update outputName + output_name = new_repre.get("outputName", "") + output_ext = new_repre["ext"] + if output_name: + output_name += "_" + output_name += output_def["filename_suffix"] + if temp_data["without_handles"]: + output_name += "_noHandles" + + # add outputName to anatomy format fill_data + fill_data.update({ + "output": output_name, + "ext": output_ext + }) + try: # temporary until oiiotool is supported cross platform ffmpeg_args = self._ffmpeg_arguments( output_def, instance, new_repre, temp_data, fill_data @@ -317,14 +332,6 @@ class ExtractReview(pyblish.api.InstancePlugin): for f in files_to_clean: os.unlink(f) - output_name = new_repre.get("outputName", "") - output_ext = new_repre["ext"] - if output_name: - output_name += "_" - output_name += output_def["filename_suffix"] - if temp_data["without_handles"]: - output_name += "_noHandles" - new_repre.update({ "name": "{}_{}".format(output_name, output_ext), "outputName": output_name, From e512965a3ef33f06dfaabe6e99b881a6a555f46a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Jan 2022 18:34:49 +0100 Subject: [PATCH 23/39] fix usage of not existing method 'get_local_live_version' --- igniter/bootstrap_repos.py | 4 ++-- igniter/install_thread.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 637f821366..207253cd2d 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -912,7 +912,6 @@ class BootstrapRepos: processed_path = file self._print(f"- processing {processed_path}") - checksums.append( ( sha256sum(file.as_posix()), @@ -1544,7 +1543,8 @@ class BootstrapRepos: Args: zip_item (Path): Zip file to test. - detected_version (OpenPypeVersion): Pype version detected from name. + detected_version (OpenPypeVersion): Pype version detected from + name. Returns: True if it is valid OpenPype version, False otherwise. diff --git a/igniter/install_thread.py b/igniter/install_thread.py index 383012b88b..8e31f8cb8f 100644 --- a/igniter/install_thread.py +++ b/igniter/install_thread.py @@ -60,7 +60,7 @@ class InstallThread(QThread): # find local version of OpenPype bs = BootstrapRepos( progress_callback=self.set_progress, message=self.message) - local_version = bs.get_local_live_version() + local_version = OpenPypeVersion.get_installed_version_str() # if user did entered nothing, we install OpenPype from local version. # zip content of `repos`, copy it to user data dir and append From a2475ff051a49e27a42e664951052bed7661a449 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 17 Jan 2022 18:42:57 +0100 Subject: [PATCH 24/39] PathInput will strip passed string --- openpype/settings/entities/input_entities.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index ff32df9262..7512d7bfcc 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -469,6 +469,17 @@ class PathInput(InputEntity): # GUI attributes self.placeholder_text = self.schema_data.get("placeholder") + def set(self, value): + # Strip value + super(PathInput, self).set(value.strip()) + + def set_override_state(self, state, ignore_missing_defaults): + super(PathInput, self).set_override_state( + state, ignore_missing_defaults + ) + # Strip current value + self._current_value = self._current_value.strip() + class RawJsonEntity(InputEntity): schema_types = ["raw-json"] From 49de654339647d8be26a6e6bf5b849a4aeeca9d9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 18 Jan 2022 11:05:22 +0100 Subject: [PATCH 25/39] Nuke: multiple baking profile fix --- .../nuke/plugins/publish/extract_review_data_mov.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py index 261fca6583..32962b57a6 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py @@ -42,6 +42,7 @@ class ExtractReviewDataMov(openpype.api.Extractor): # generate data with anlib.maintained_selection(): + generated_repres = [] for o_name, o_data in self.outputs.items(): f_families = o_data["filter"]["families"] f_task_types = o_data["filter"]["task_types"] @@ -112,11 +113,13 @@ class ExtractReviewDataMov(openpype.api.Extractor): }) else: data = exporter.generate_mov(**o_data) + generated_repres.extend(data["representations"]) - self.log.info(data["representations"]) + self.log.info(generated_repres) - # assign to representations - instance.data["representations"] += data["representations"] + if generated_repres: + # assign to representations + instance.data["representations"] += generated_repres self.log.debug( "_ representations: {}".format( From 7e7d12423a65087b321d9fdfa486b3500cbd8d2c Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 19 Jan 2022 03:37:43 +0000 Subject: [PATCH 26/39] [Automated] Bump version --- CHANGELOG.md | 13 ++++++------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7cd3cb7d2..d1b390da5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ # Changelog -## [3.8.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.8.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.7.0...HEAD) ### πŸ“– Documentation +- Renamed to proper name [\#2546](https://github.com/pypeclub/OpenPype/pull/2546) - Slack: Add review to notification message [\#2498](https://github.com/pypeclub/OpenPype/pull/2498) **πŸ†• New features** @@ -14,6 +15,8 @@ **πŸš€ Enhancements** +- Settings: PathInput strip passed string [\#2550](https://github.com/pypeclub/OpenPype/pull/2550) +- General: Validate if current process OpenPype version is requested version [\#2529](https://github.com/pypeclub/OpenPype/pull/2529) - General: Be able to use anatomy data in ffmpeg output arguments [\#2525](https://github.com/pypeclub/OpenPype/pull/2525) - Expose toggle publish plug-in settings for Maya Look Shading Engine Naming [\#2521](https://github.com/pypeclub/OpenPype/pull/2521) - Photoshop: Move implementation to OpenPype [\#2510](https://github.com/pypeclub/OpenPype/pull/2510) @@ -48,6 +51,8 @@ **Merged pull requests:** +- General: Fix install thread in igniter [\#2549](https://github.com/pypeclub/OpenPype/pull/2549) +- AfterEffects: Move implementation to OpenPype [\#2543](https://github.com/pypeclub/OpenPype/pull/2543) - Fix create zip tool - path argument [\#2522](https://github.com/pypeclub/OpenPype/pull/2522) - General: Modules import function output fix [\#2492](https://github.com/pypeclub/OpenPype/pull/2492) - AE: fix hiding of alert window below Publish [\#2491](https://github.com/pypeclub/OpenPype/pull/2491) @@ -57,10 +62,6 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.7.0-nightly.14...3.7.0) -**Deprecated:** - -- General: Default modules hierarchy n2 [\#2368](https://github.com/pypeclub/OpenPype/pull/2368) - **πŸš€ Enhancements** - General: Workdir extra folders [\#2462](https://github.com/pypeclub/OpenPype/pull/2462) @@ -79,7 +80,6 @@ - Enhancement: Settings: Use project settings values from another project [\#2382](https://github.com/pypeclub/OpenPype/pull/2382) - Blender 3: Support auto install for new blender version [\#2377](https://github.com/pypeclub/OpenPype/pull/2377) - Maya add render image path to settings [\#2375](https://github.com/pypeclub/OpenPype/pull/2375) -- Hiero: python3 compatibility [\#2365](https://github.com/pypeclub/OpenPype/pull/2365) **πŸ› Bug fixes** @@ -97,7 +97,6 @@ - hiero: fix workio and flatten [\#2378](https://github.com/pypeclub/OpenPype/pull/2378) - Nuke: fixing menu re-drawing during context change [\#2374](https://github.com/pypeclub/OpenPype/pull/2374) - Webpublisher: Fix assignment of families of TVpaint instances [\#2373](https://github.com/pypeclub/OpenPype/pull/2373) -- Nuke: fixing node name based on switched asset name [\#2369](https://github.com/pypeclub/OpenPype/pull/2369) **Merged pull requests:** diff --git a/openpype/version.py b/openpype/version.py index 520048bca7..121bb01e8f 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.8.0-nightly.4" +__version__ = "3.8.0-nightly.5" diff --git a/pyproject.toml b/pyproject.toml index 598d2b4798..04d48401ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.8.0-nightly.4" # OpenPype +version = "3.8.0-nightly.5" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From c2642974d984583c41df9fe9f5c04541cd208e5c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 Jan 2022 10:12:28 +0100 Subject: [PATCH 27/39] do not validate version if build does not support it --- openpype/lib/__init__.py | 2 ++ openpype/tools/tray/pype_tray.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 62d204186d..1c8f7a57af 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -169,6 +169,7 @@ from .editorial import ( ) from .openpype_version import ( + op_version_control_available, get_openpype_version, get_build_version, get_expected_version, @@ -306,6 +307,7 @@ __all__ = [ "create_workdir_extra_folders", "get_project_basic_paths", + "op_version_control_available", "get_openpype_version", "get_build_version", "get_expected_version", diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 0d3e7ae04c..c9b8aaa842 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -16,6 +16,7 @@ from openpype.api import ( ) from openpype.lib import ( get_openpype_execute_args, + op_version_control_available, is_current_version_studio_latest, is_running_from_build, is_running_staging, @@ -218,7 +219,7 @@ class TrayManager: def _on_version_check_timer(self): # Check if is running from build and stop future validations if yes - if not is_running_from_build(): + if not is_running_from_build() or not op_version_control_available(): self._version_check_timer.stop() return From 40ba77207ff6d510355630299648f645c8558872 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 Jan 2022 10:47:17 +0100 Subject: [PATCH 28/39] use applications manager to get djv path --- .../event_handlers_user/action_djvview.py | 106 +++++++++++++----- 1 file changed, 81 insertions(+), 25 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_djvview.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_djvview.py index c603a2d200..334519b4bb 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_djvview.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_djvview.py @@ -1,6 +1,8 @@ import os +import time import subprocess from operator import itemgetter +from openpype.lib import ApplicationManager from openpype_modules.ftrack.lib import BaseAction, statics_icon @@ -23,15 +25,25 @@ class DJVViewAction(BaseAction): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.djv_path = self.find_djv_path() + self.application_manager = ApplicationManager() + self._last_check = time.time() + self._check_interval = 10 - def preregister(self): - if self.djv_path is None: - return ( - 'DJV View is not installed' - ' or paths in presets are not set correctly' - ) - return True + def _get_djv_apps(self): + app_group = self.application_manager.app_groups["djvview"] + + output = [] + for app in app_group: + executable = app.find_executable() + if executable is not None: + output.append(app) + return output + + def get_djv_apps(self): + cur_time = time.time() + if (cur_time - self._last_check) > self._check_interval: + self.application_manager.refresh() + return self._get_djv_apps() def discover(self, session, entities, event): """Return available actions based on *event*. """ @@ -40,15 +52,13 @@ class DJVViewAction(BaseAction): return False entityType = selection[0].get("entityType", None) - if entityType in ["assetversion", "task"]: + if entityType not in ["assetversion", "task"]: + return False + + if self.get_djv_apps(): return True return False - def find_djv_path(self): - for path in (os.environ.get("DJV_PATH") or "").split(os.pathsep): - if os.path.exists(path): - return path - def interface(self, session, entities, event): if event['data'].get('values', {}): return @@ -88,7 +98,37 @@ class DJVViewAction(BaseAction): 'message': 'There are no Asset Versions to open.' } - items = [] + # TODO sort them (somehow?) + enum_items = [] + first_value = None + for app in self.get_djv_apps(): + if first_value is None: + first_value = app.full_name + enum_items.append({ + "value": app.full_name, + "label": app.full_label + }) + + if not enum_items: + return { + "success": False, + "message": "Couldn't find DJV executable." + } + + items = [ + { + "type": "enumerator", + "label": "DJV version:", + "name": "djv_app_name", + "data": enum_items, + "value": first_value + }, + { + "type": "label", + "value": "---" + } + ] + version_items = [] base_label = "v{0} - {1} - {2}" default_component = None last_available = None @@ -115,11 +155,11 @@ class DJVViewAction(BaseAction): last_available = file_path if component['name'] == default_component: select_value = file_path - items.append( + version_items.append( {'label': label, 'value': file_path} ) - if len(items) == 0: + if len(version_items) == 0: return { 'success': False, 'message': ( @@ -132,7 +172,7 @@ class DJVViewAction(BaseAction): 'type': 'enumerator', 'name': 'path', 'data': sorted( - items, + version_items, key=itemgetter('label'), reverse=True ) @@ -142,21 +182,37 @@ class DJVViewAction(BaseAction): else: item['value'] = last_available - return {'items': [item]} + items.append(item) + + return {'items': items} def launch(self, session, entities, event): """Callback method for DJVView action.""" # Launching application - if "values" not in event["data"]: + event_data = event["data"] + if "values" not in event_data: return - filpath = event['data']['values']['path'] + + djv_app_name = event_data["djv_app_name"] + app = self.applicaion_manager.applications.get(djv_app_name) + executable = None + if app is not None: + executable = app.find_executable() + + if not executable: + return { + "success": False, + "message": "Couldn't find DJV executable." + } + + filpath = os.path.normpath(event_data["values"]["path"]) cmd = [ # DJV path - os.path.normpath(self.djv_path), + executable, # PATH TO COMPONENT - os.path.normpath(filpath) + filpath ] try: @@ -164,8 +220,8 @@ class DJVViewAction(BaseAction): subprocess.Popen(cmd) except FileNotFoundError: return { - 'success': False, - 'message': 'File "{}" was not found.'.format( + "success": False, + "message": "File \"{}\" was not found.".format( os.path.basename(filpath) ) } From 34fa94fba3b8b9799a43df6c64d3782c159aedec Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 19 Jan 2022 14:06:57 +0100 Subject: [PATCH 29/39] global: improving source resolution of gap img --- .../plugins/publish/extract_otio_review.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/openpype/plugins/publish/extract_otio_review.py b/openpype/plugins/publish/extract_otio_review.py index ed2ba017d5..675e5e0ee0 100644 --- a/openpype/plugins/publish/extract_otio_review.py +++ b/openpype/plugins/publish/extract_otio_review.py @@ -85,6 +85,28 @@ class ExtractOTIOReview(openpype.api.Extractor): for index, r_otio_cl in enumerate(otio_review_clips): # QUESTION: what if transition on clip? + # check if resolution is the same + width = self.to_width + height = self.to_height + otio_media = r_otio_cl.media_reference + media_metadata = otio_media.metadata + + # get from media reference metadata source + if media_metadata.get("openpype.source.width"): + width = int(media_metadata.get("openpype.source.width")) + if media_metadata.get("openpype.source.height"): + height = int(media_metadata.get("openpype.source.height")) + + # compare and reset + if width != self.to_width: + self.to_width = width + if height != self.to_height: + self.to_height = height + + self.log.debug("> self.to_width x self.to_height: {} x {}".format( + self.to_width, self.to_height + )) + # get frame range values src_range = r_otio_cl.source_range start = src_range.start_time.value From 303d6e2815d3f5dc384383176d31477979d8e69f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 Jan 2022 16:28:34 +0100 Subject: [PATCH 30/39] change message when avalon entities are not available --- .../ftrack/event_handlers_user/action_delete_asset.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py index d3cc0ad971..be53e2f234 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py @@ -163,8 +163,11 @@ class DeleteAssetSubset(BaseAction): if not selected_av_entities: return { - "success": False, - "message": "Didn't found entities in avalon" + "success": True, + "message": ( + "Didn't found entities in avalon." + " You can use Ftrack's Delete button fot this selection." + ) } # Remove cached action older than 2 minutes From d68b08bb6e31b97fb090fe763dd678d97a9774fb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 Jan 2022 16:29:04 +0100 Subject: [PATCH 31/39] few formatting changes --- .../action_delete_asset.py | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py index be53e2f234..900516e790 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py @@ -172,18 +172,18 @@ class DeleteAssetSubset(BaseAction): # Remove cached action older than 2 minutes old_action_ids = [] - for id, data in self.action_data_by_id.items(): + for action_id, data in self.action_data_by_id.items(): created_at = data.get("created_at") if not created_at: - old_action_ids.append(id) + old_action_ids.append(action_id) continue cur_time = datetime.now() existing_in_sec = (created_at - cur_time).total_seconds() if existing_in_sec > 60 * 2: - old_action_ids.append(id) + old_action_ids.append(action_id) - for id in old_action_ids: - self.action_data_by_id.pop(id, None) + for action_id in old_action_ids: + self.action_data_by_id.pop(action_id, None) # Store data for action id action_id = str(uuid.uuid1()) @@ -442,7 +442,11 @@ class DeleteAssetSubset(BaseAction): subsets_to_delete = to_delete.get("subsets") or [] # Convert asset ids to ObjectId obj - assets_to_delete = [ObjectId(id) for id in assets_to_delete if id] + assets_to_delete = [ + ObjectId(asset_id) + for asset_id in assets_to_delete + if asset_id + ] subset_ids_by_parent = spec_data["subset_ids_by_parent"] subset_ids_by_name = spec_data["subset_ids_by_name"] @@ -471,9 +475,8 @@ class DeleteAssetSubset(BaseAction): if not ftrack_id: ftrack_id = asset["data"].get("ftrackId") - if not ftrack_id: - continue - ftrack_ids_to_delete.append(ftrack_id) + if ftrack_id: + ftrack_ids_to_delete.append(ftrack_id) children_queue = collections.deque() for mongo_id in assets_to_delete: @@ -572,12 +575,12 @@ class DeleteAssetSubset(BaseAction): exc_info=True ) - if not_deleted_entities_id: - joined_not_deleted = ", ".join([ + if not_deleted_entities_id and asset_names_to_delete: + joined_not_deleted = ",".join([ "\"{}\"".format(ftrack_id) for ftrack_id in not_deleted_entities_id ]) - joined_asset_names = ", ".join([ + joined_asset_names = ",".join([ "\"{}\"".format(name) for name in asset_names_to_delete ]) From e02f03c3432a9a5640ec0be7f0a021d57f40e7c5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 Jan 2022 16:29:19 +0100 Subject: [PATCH 32/39] find all children under selection and add them to delete queue --- .../action_delete_asset.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py index 900516e790..586f04004d 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py @@ -619,6 +619,25 @@ class DeleteAssetSubset(BaseAction): joined_ids_to_delete ) ).all() + # Find all children entities and add them to list + # - Delete tasks first then their parents and continue + parent_ids_to_delete = [ + entity["id"] + for entity in to_delete_entities + ] + while parent_ids_to_delete: + joined_parent_ids_to_delete = ",".join([ + "\"{}\"".format(ftrack_id) + for ftrack_id in parent_ids_to_delete + ]) + _to_delete = session.query(( + "select id, link from TypedContext where parent_id in ({})" + ).format(joined_parent_ids_to_delete)).all() + parent_ids_to_delete = [] + for entity in _to_delete: + parent_ids_to_delete.append(entity["id"]) + to_delete_entities.append(entity) + entities_by_link_len = collections.defaultdict(list) for entity in to_delete_entities: entities_by_link_len[len(entity["link"])].append(entity) From 4c1eda4558f9ae76b7833ab49da6b536872d210f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 Jan 2022 16:42:22 +0100 Subject: [PATCH 33/39] fix typo --- .../ftrack/event_handlers_user/action_delete_asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py index 586f04004d..676dd80e93 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py @@ -166,7 +166,7 @@ class DeleteAssetSubset(BaseAction): "success": True, "message": ( "Didn't found entities in avalon." - " You can use Ftrack's Delete button fot this selection." + " You can use Ftrack's Delete button for the selection." ) } From 8eded893aafa1f1cb62bc5e371ae01c39d58d6a6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 Jan 2022 16:45:52 +0100 Subject: [PATCH 34/39] fixed mising 'maintained_selection' --- openpype/hosts/nuke/api/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/nuke/api/__init__.py b/openpype/hosts/nuke/api/__init__.py index d3b7f74d6d..f7ebcb41da 100644 --- a/openpype/hosts/nuke/api/__init__.py +++ b/openpype/hosts/nuke/api/__init__.py @@ -25,6 +25,9 @@ from .pipeline import ( parse_container, update_container, ) +from .lib import ( + maintained_selection +) __all__ = ( @@ -49,4 +52,6 @@ __all__ = ( "containerise", "parse_container", "update_container", + + "maintained_selection", ) From f6441176e16dcba607ad0c3af2162a8ccfe45b8f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 Jan 2022 17:11:57 +0100 Subject: [PATCH 35/39] set env variables to skip validation of 3rd party libs --- .github/workflows/test_build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml index 6e1e38d0b2..dd52e83b61 100644 --- a/.github/workflows/test_build.yml +++ b/.github/workflows/test_build.yml @@ -37,6 +37,7 @@ jobs: - name: πŸ”¨ Build shell: pwsh run: | + $env:SKIP_THIRD_PARTY_VALIDATION = "1" ./tools/build.ps1 Ubuntu-latest: @@ -61,6 +62,7 @@ jobs: - name: πŸ”¨ Build run: | + echo "1" >> $SKIP_THIRD_PARTY_VALIDATION ./tools/build.sh # MacOS-latest: From c4de9bc9ace6ed22e75913be55faecc7f34045e0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 Jan 2022 17:26:48 +0100 Subject: [PATCH 36/39] try use 'env' in step configuration --- .github/workflows/test_build.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml index dd52e83b61..6dea05559b 100644 --- a/.github/workflows/test_build.yml +++ b/.github/workflows/test_build.yml @@ -36,8 +36,9 @@ jobs: - name: πŸ”¨ Build shell: pwsh + env: + SKIP_THIRD_PARTY_VALIDATION: "1" run: | - $env:SKIP_THIRD_PARTY_VALIDATION = "1" ./tools/build.ps1 Ubuntu-latest: @@ -61,6 +62,8 @@ jobs: ./tools/create_env.sh - name: πŸ”¨ Build + env: + SKIP_THIRD_PARTY_VALIDATION: "1" run: | echo "1" >> $SKIP_THIRD_PARTY_VALIDATION ./tools/build.sh From 750944ca21fd04711eced4b6e28b54ffd6f717b0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 Jan 2022 17:44:22 +0100 Subject: [PATCH 37/39] try to get rid of spaces --- .github/workflows/test_build.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml index 6dea05559b..9e098db69b 100644 --- a/.github/workflows/test_build.yml +++ b/.github/workflows/test_build.yml @@ -37,7 +37,7 @@ jobs: - name: πŸ”¨ Build shell: pwsh env: - SKIP_THIRD_PARTY_VALIDATION: "1" + SKIP_THIRD_PARTY_VALIDATION: 1 run: | ./tools/build.ps1 @@ -63,9 +63,8 @@ jobs: - name: πŸ”¨ Build env: - SKIP_THIRD_PARTY_VALIDATION: "1" + SKIP_THIRD_PARTY_VALIDATION: 1 run: | - echo "1" >> $SKIP_THIRD_PARTY_VALIDATION ./tools/build.sh # MacOS-latest: From e63b14d781419f5f944f7c9c806401ec4c4e9563 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 Jan 2022 18:09:17 +0100 Subject: [PATCH 38/39] use export command --- .github/workflows/test_build.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml index 9e098db69b..3b5e6dc970 100644 --- a/.github/workflows/test_build.yml +++ b/.github/workflows/test_build.yml @@ -36,9 +36,8 @@ jobs: - name: πŸ”¨ Build shell: pwsh - env: - SKIP_THIRD_PARTY_VALIDATION: 1 run: | + $env:SKIP_THIRD_PARTY_VALIDATION="1" ./tools/build.ps1 Ubuntu-latest: @@ -62,9 +61,8 @@ jobs: ./tools/create_env.sh - name: πŸ”¨ Build - env: - SKIP_THIRD_PARTY_VALIDATION: 1 run: | + export SKIP_THIRD_PARTY_VALIDATION=1 ./tools/build.sh # MacOS-latest: From 7affac0d1adfe96a9c06b918e7e3c39bf43fd6f3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 19 Jan 2022 19:43:46 +0100 Subject: [PATCH 39/39] fix setting of env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: OndΕ™ej Samohel <33513211+antirotor@users.noreply.github.com> --- .github/workflows/test_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml index 3b5e6dc970..ac7279117a 100644 --- a/.github/workflows/test_build.yml +++ b/.github/workflows/test_build.yml @@ -62,7 +62,7 @@ jobs: - name: πŸ”¨ Build run: | - export SKIP_THIRD_PARTY_VALIDATION=1 + export SKIP_THIRD_PARTY_VALIDATION="1" ./tools/build.sh # MacOS-latest: