Merge branch 'develop' of github.com:pypeclub/OpenPype into feature/OP-1117_Disable-workfile-autoload-in-Launcher

This commit is contained in:
Petr Kalis 2022-01-20 12:33:00 +01:00
commit 90642a191e
75 changed files with 2359 additions and 736 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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("\\", "/")

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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)
return self.node_name_template.format(**name_data)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,2 +1,4 @@
import nuke
# default write mov
nuke.knobDefault('Write.mov.colorspace', 'sRGB')

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 <b>{}</b>."
).format(str(get_openpype_version()))
})
elif is_latest is None:
items.append({
"type": "label",
"value": (
"Can't determine if OpenPype version is outdated"
" <b>{}</b>. 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 <b>{}</b>.".format(
str(get_openpype_version())
)
})
else:
items.append({
"type": "label",
"value": (
"Using <b>outdated</b> OpenPype version <b>{}</b>."
" Expected version is <b>{}</b>."
"<br/>- 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())

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@
"admin_password": "",
"production_version": "",
"staging_version": "",
"version_check_interval": 5,
"environment": {
"__environment_keys__": {
"global": []

View file

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

View file

@ -47,6 +47,19 @@
{
"type": "splitter"
},
{
"type": "label",
"label": "Trigger validation if running OpenPype is using studio defined version each 'n' <b>minutes</b>. Validation happens in OpenPype tray application."
},
{
"type": "number",
"key": "version_check_interval",
"label": "Version check interval",
"minimum": 0
},
{
"type": "splitter"
},
{
"key": "environment",
"label": "Environment",

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View file

@ -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 <b>{}</b>."
" Your production has been updated to version <b>{}</b>."
).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

View file

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

View file

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

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.8.0-nightly.4"
__version__ = "3.8.0-nightly.5"

View file

@ -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 <info@openpype.io>"]
license = "MIT License"

View file

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

View file

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