diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml
index 6e1e38d0b2..ac7279117a 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: |
+ export SKIP_THIRD_PARTY_VALIDATION="1"
./tools/build.sh
# MacOS-latest:
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/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
diff --git a/openpype/hosts/fusion/hooks/pre_fusion_setup.py b/openpype/hosts/fusion/hooks/pre_fusion_setup.py
index a0c16a6700..8c4973cf43 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,25 +14,27 @@ 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}`"
- )
- self.log.info(f"Path to Fusion Python folder: `{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.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 "
+ f"Fusion.\n\nFUSION_UTILITY_SCRIPTS_DIR: '{us_dir}'"
+ )
try:
__import__("avalon.fusion")
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):
diff --git a/openpype/hosts/nuke/api/__init__.py b/openpype/hosts/nuke/api/__init__.py
index 1567189ed1..f7ebcb41da 100644
--- a/openpype/hosts/nuke/api/__init__.py
+++ b/openpype/hosts/nuke/api/__init__.py
@@ -1,130 +1,57 @@
-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,
+)
+from .lib import (
+ maintained_selection
+)
-# 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",
- """
+ "containerise",
+ "parse_container",
+ "update_container",
- 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)
+ "maintained_selection",
+)
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..205b23efe6 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):
@@ -48,14 +49,16 @@ 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 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)
diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py
index 12e47a8961..1c8f7a57af 100644
--- a/openpype/lib/__init__.py
+++ b/openpype/lib/__init__.py
@@ -168,9 +168,14 @@ from .editorial import (
make_sequence_collection
)
-from .pype_info import (
+from .openpype_version import (
+ op_version_control_available,
get_openpype_version,
- get_build_version
+ get_build_version,
+ get_expected_version,
+ is_running_from_build,
+ is_running_staging,
+ is_current_version_studio_latest
)
terminal = Terminal
@@ -302,6 +307,11 @@ __all__ = [
"create_workdir_extra_folders",
"get_project_basic_paths",
+ "op_version_control_available",
"get_openpype_version",
"get_build_version",
+ "get_expected_version",
+ "is_running_from_build",
+ "is_running_staging",
+ "is_current_version_studio_latest",
]
diff --git a/openpype/lib/openpype_version.py b/openpype/lib/openpype_version.py
index e3a4e1fa3e..201bf646e9 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,67 @@ 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 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.
+
+ 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 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
+ OpenPypeVersion = get_OpenPypeVersion()
+ # Convert current version to OpenPypeVersion object
+ current_version = OpenPypeVersion(version=get_openpype_version())
+
+ # Get expected version (from settings)
+ expected_version = get_expected_version()
+ # 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 15856bfb19..848a505187 100644
--- a/openpype/lib/pype_info.py
+++ b/openpype/lib/pype_info.py
@@ -5,68 +5,13 @@ 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
-
-
-def get_openpype_version():
- """Version of pype that is currently used."""
- 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
- 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
+from .openpype_version import (
+ is_running_from_build,
+ get_openpype_version
+)
def get_pype_info():
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..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
@@ -163,24 +163,27 @@ 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 for the selection."
+ )
}
# 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())
@@ -439,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"]
@@ -468,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:
@@ -569,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
])
@@ -613,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)
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)
)
}
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())
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
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,
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__))
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/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"]
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/style/data.json b/openpype/style/data.json
index b3dffd7c71..1db0c732cf 100644
--- a/openpype/style/data.json
+++ b/openpype/style/data.json
@@ -51,6 +51,8 @@
"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/style/style.css b/openpype/style/style.css
index 7f7f30e2bc..d9b0ff7421 100644
--- a/openpype/style/style.css
+++ b/openpype/style/style.css
@@ -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/tray/images/gifts.png b/openpype/tools/tray/images/gifts.png
new file mode 100644
index 0000000000..57fb3f2863
Binary files /dev/null and b/openpype/tools/tray/images/gifts.png differ
diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py
index df0238c848..c9b8aaa842 100644
--- a/openpype/tools/tray/pype_tray.py
+++ b/openpype/tools/tray/pype_tray.py
@@ -14,7 +14,15 @@ from openpype.api import (
resources,
get_system_settings
)
-from openpype.lib import get_openpype_execute_args
+from openpype.lib import (
+ get_openpype_execute_args,
+ op_version_control_available,
+ is_current_version_studio_latest,
+ is_running_from_build,
+ is_running_staging,
+ get_expected_version,
+ get_openpype_version
+)
from openpype.modules import TrayModulesManager
from openpype import style
from openpype.settings import (
@@ -22,29 +30,177 @@ from openpype.settings import (
ProjectSettings,
DefaultsNotDefined
)
+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()
+
+ _min_width = 400
+ _min_height = 130
+
+ def __init__(self, parent=None):
+ super(VersionDialog, self).__init__(parent)
+ self.setWindowTitle("OpenPype update is needed")
+ 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)
+
+ 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)
+
+ 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)
+ btns_layout.addWidget(ignore_btn, 0)
+ btns_layout.addWidget(restart_btn, 0)
+
+ layout = QtWidgets.QVBoxLayout(self)
+ layout.addWidget(top_widget, 0)
+ layout.addStretch(1)
+ layout.addLayout(btns_layout, 0)
+
+ ignore_btn.clicked.connect(self._on_ignore)
+ restart_btn.clicked.connect(self._on_reset)
+
+ self._label_widget = label_widget
+ self._restart_accepted = False
+
+ 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
+
+ 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 = (
+ "Running OpenPype version is {}."
+ " Your production has been updated to 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_accepted = True
+ self.restart_requested.emit()
+ self.accept()
+
+
class TrayManager:
"""Cares about context of application.
Load submenus, actions, separators and modules into tray's context.
"""
-
def __init__(self, tray_widget, main_window):
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__)
- 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()
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
@@ -61,21 +217,73 @@ class TrayManager:
if callback:
self.execute_in_main_thread(callback)
- def execute_in_main_thread(self, callback):
- self._main_thread_callbacks.append(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() or not op_version_control_available():
+ self._version_check_timer.stop()
+ return
+
+ self.validate_openpype_version()
+
+ def validate_openpype_version(self):
+ 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:
+ self._version_dialog = VersionDialog()
+ self._version_dialog.restart_requested.connect(
+ self._restart_and_install
+ )
+ self._version_dialog.ignore_requested.connect(
+ self._outdated_version_ignored
+ )
+
+ expected_version = get_expected_version()
+ current_version = get_openpype_version()
+ self._version_dialog.update_versions(
+ current_version, expected_version
+ )
+ 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(
+ "OpenPype version is outdated",
+ (
+ "Please update your OpenPype as soon as possible."
+ " To update, restart OpenPype Tray application."
+ )
+ )
+
+ 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
@@ -119,6 +327,13 @@ class TrayManager:
self.main_thread_timer = main_thread_timer
+ version_check_timer = QtCore.QTimer()
+ version_check_timer.timeout.connect(self._on_version_check_timer)
+ 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
self._settings_validation_dialog = None
@@ -200,24 +415,47 @@ 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()
- def restart(self):
+ self._restart_action = restart_action
+
+ def _on_restart_action(self):
+ self.restart()
+
+ def restart(self, reset_version=True):
"""Restart Tray tool.
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 = {}
+ # 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
diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py
index 4dd6bdd05f..eb0cb1eef5 100644
--- a/openpype/tools/utils/__init__.py
+++ b/openpype/tools/utils/__init__.py
@@ -6,6 +6,10 @@ from .widgets import (
)
from .error_dialog import ErrorMessageBox
+from .lib import (
+ WrappedCallbackItem,
+ paint_image_with_color
+)
__all__ = (
@@ -14,5 +18,8 @@ __all__ = (
"ClickableFrame",
"ExpandBtn",
- "ErrorMessageBox"
+ "ErrorMessageBox",
+
+ "WrappedCallbackItem",
+ "paint_image_with_color",
)
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
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"
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
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"