mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
[Automated] Merged develop into main
This commit is contained in:
commit
958352eb49
49 changed files with 715 additions and 223 deletions
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -35,6 +35,7 @@ body:
|
|||
label: Version
|
||||
description: What version are you running? Look to OpenPype Tray
|
||||
options:
|
||||
- 3.17.2-nightly.2
|
||||
- 3.17.2-nightly.1
|
||||
- 3.17.1
|
||||
- 3.17.1-nightly.3
|
||||
|
|
@ -134,7 +135,6 @@ body:
|
|||
- 3.14.10-nightly.9
|
||||
- 3.14.10-nightly.8
|
||||
- 3.14.10-nightly.7
|
||||
- 3.14.10-nightly.6
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
|||
|
|
@ -123,6 +123,9 @@ class CreateSaver(NewCreator):
|
|||
def _imprint(self, tool, data):
|
||||
# Save all data in a "openpype.{key}" = value data
|
||||
|
||||
# Instance id is the tool's name so we don't need to imprint as data
|
||||
data.pop("instance_id", None)
|
||||
|
||||
active = data.pop("active", None)
|
||||
if active is not None:
|
||||
# Use active value to set the passthrough state
|
||||
|
|
@ -188,6 +191,10 @@ class CreateSaver(NewCreator):
|
|||
passthrough = attrs["TOOLB_PassThrough"]
|
||||
data["active"] = not passthrough
|
||||
|
||||
# Override publisher's UUID generation because tool names are
|
||||
# already unique in Fusion in a comp
|
||||
data["instance_id"] = tool.Name
|
||||
|
||||
return data
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
import os
|
||||
import errno
|
||||
import re
|
||||
import uuid
|
||||
import logging
|
||||
|
|
@ -9,10 +10,15 @@ import json
|
|||
|
||||
import six
|
||||
|
||||
from openpype.lib import StringTemplate
|
||||
from openpype.client import get_asset_by_name
|
||||
from openpype.settings import get_current_project_settings
|
||||
from openpype.pipeline import get_current_project_name, get_current_asset_name
|
||||
from openpype.pipeline.context_tools import get_current_project_asset
|
||||
|
||||
from openpype.pipeline.context_tools import (
|
||||
get_current_context_template_data,
|
||||
get_current_project_asset
|
||||
)
|
||||
from openpype.widgets import popup
|
||||
import hou
|
||||
|
||||
|
||||
|
|
@ -160,8 +166,6 @@ def validate_fps():
|
|||
|
||||
if current_fps != fps:
|
||||
|
||||
from openpype.widgets import popup
|
||||
|
||||
# Find main window
|
||||
parent = hou.ui.mainQtWindow()
|
||||
if parent is None:
|
||||
|
|
@ -747,3 +751,99 @@ def get_camera_from_container(container):
|
|||
|
||||
assert len(cameras) == 1, "Camera instance must have only one camera"
|
||||
return cameras[0]
|
||||
|
||||
|
||||
def get_context_var_changes():
|
||||
"""get context var changes."""
|
||||
|
||||
houdini_vars_to_update = {}
|
||||
|
||||
project_settings = get_current_project_settings()
|
||||
houdini_vars_settings = \
|
||||
project_settings["houdini"]["general"]["update_houdini_var_context"]
|
||||
|
||||
if not houdini_vars_settings["enabled"]:
|
||||
return houdini_vars_to_update
|
||||
|
||||
houdini_vars = houdini_vars_settings["houdini_vars"]
|
||||
|
||||
# No vars specified - nothing to do
|
||||
if not houdini_vars:
|
||||
return houdini_vars_to_update
|
||||
|
||||
# Get Template data
|
||||
template_data = get_current_context_template_data()
|
||||
|
||||
# Set Houdini Vars
|
||||
for item in houdini_vars:
|
||||
# For consistency reasons we always force all vars to be uppercase
|
||||
# Also remove any leading, and trailing whitespaces.
|
||||
var = item["var"].strip().upper()
|
||||
|
||||
# get and resolve template in value
|
||||
item_value = StringTemplate.format_template(
|
||||
item["value"],
|
||||
template_data
|
||||
)
|
||||
|
||||
if var == "JOB" and item_value == "":
|
||||
# sync $JOB to $HIP if $JOB is empty
|
||||
item_value = os.environ["HIP"]
|
||||
|
||||
if item["is_directory"]:
|
||||
item_value = item_value.replace("\\", "/")
|
||||
|
||||
current_value = hou.hscript("echo -n `${}`".format(var))[0]
|
||||
|
||||
if current_value != item_value:
|
||||
houdini_vars_to_update[var] = (
|
||||
current_value, item_value, item["is_directory"]
|
||||
)
|
||||
|
||||
return houdini_vars_to_update
|
||||
|
||||
|
||||
def update_houdini_vars_context():
|
||||
"""Update asset context variables"""
|
||||
|
||||
for var, (_old, new, is_directory) in get_context_var_changes().items():
|
||||
if is_directory:
|
||||
try:
|
||||
os.makedirs(new)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
print(
|
||||
"Failed to create ${} dir. Maybe due to "
|
||||
"insufficient permissions.".format(var)
|
||||
)
|
||||
|
||||
hou.hscript("set {}={}".format(var, new))
|
||||
os.environ[var] = new
|
||||
print("Updated ${} to {}".format(var, new))
|
||||
|
||||
|
||||
def update_houdini_vars_context_dialog():
|
||||
"""Show pop-up to update asset context variables"""
|
||||
update_vars = get_context_var_changes()
|
||||
if not update_vars:
|
||||
# Nothing to change
|
||||
print("Nothing to change, Houdini vars are already up to date.")
|
||||
return
|
||||
|
||||
message = "\n".join(
|
||||
"${}: {} -> {}".format(var, old or "None", new or "None")
|
||||
for var, (old, new, _is_directory) in update_vars.items()
|
||||
)
|
||||
|
||||
# TODO: Use better UI!
|
||||
parent = hou.ui.mainQtWindow()
|
||||
dialog = popup.Popup(parent=parent)
|
||||
dialog.setModal(True)
|
||||
dialog.setWindowTitle("Houdini scene has outdated asset variables")
|
||||
dialog.setMessage(message)
|
||||
dialog.setButtonText("Fix")
|
||||
|
||||
# on_show is the Fix button clicked callback
|
||||
dialog.on_clicked.connect(update_houdini_vars_context)
|
||||
|
||||
dialog.show()
|
||||
|
|
|
|||
|
|
@ -300,6 +300,9 @@ def on_save():
|
|||
|
||||
log.info("Running callback on save..")
|
||||
|
||||
# update houdini vars
|
||||
lib.update_houdini_vars_context_dialog()
|
||||
|
||||
nodes = lib.get_id_required_nodes()
|
||||
for node, new_id in lib.generate_ids(nodes):
|
||||
lib.set_id(node, new_id, overwrite=False)
|
||||
|
|
@ -335,6 +338,9 @@ def on_open():
|
|||
|
||||
log.info("Running callback on open..")
|
||||
|
||||
# update houdini vars
|
||||
lib.update_houdini_vars_context_dialog()
|
||||
|
||||
# Validate FPS after update_task_from_path to
|
||||
# ensure it is using correct FPS for the asset
|
||||
lib.validate_fps()
|
||||
|
|
@ -399,6 +405,7 @@ def _set_context_settings():
|
|||
"""
|
||||
|
||||
lib.reset_framerange()
|
||||
lib.update_houdini_vars_context()
|
||||
|
||||
|
||||
def on_pyblish_instance_toggled(instance, new_value, old_value):
|
||||
|
|
|
|||
|
|
@ -187,13 +187,14 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase):
|
|||
self.customize_node_look(instance_node)
|
||||
|
||||
instance_data["instance_node"] = instance_node.path()
|
||||
instance_data["instance_id"] = instance_node.path()
|
||||
instance = CreatedInstance(
|
||||
self.family,
|
||||
subset_name,
|
||||
instance_data,
|
||||
self)
|
||||
self._add_instance_to_context(instance)
|
||||
imprint(instance_node, instance.data_to_store())
|
||||
self.imprint(instance_node, instance.data_to_store())
|
||||
return instance
|
||||
|
||||
except hou.Error as er:
|
||||
|
|
@ -222,25 +223,41 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase):
|
|||
self.cache_subsets(self.collection_shared_data)
|
||||
for instance in self.collection_shared_data[
|
||||
"houdini_cached_subsets"].get(self.identifier, []):
|
||||
|
||||
node_data = read(instance)
|
||||
|
||||
# Node paths are always the full node path since that is unique
|
||||
# Because it's the node's path it's not written into attributes
|
||||
# but explicitly collected
|
||||
node_path = instance.path()
|
||||
node_data["instance_id"] = node_path
|
||||
node_data["instance_node"] = node_path
|
||||
|
||||
created_instance = CreatedInstance.from_existing(
|
||||
read(instance), self
|
||||
node_data, self
|
||||
)
|
||||
self._add_instance_to_context(created_instance)
|
||||
|
||||
def update_instances(self, update_list):
|
||||
for created_inst, changes in update_list:
|
||||
instance_node = hou.node(created_inst.get("instance_node"))
|
||||
|
||||
new_values = {
|
||||
key: changes[key].new_value
|
||||
for key in changes.changed_keys
|
||||
}
|
||||
imprint(
|
||||
self.imprint(
|
||||
instance_node,
|
||||
new_values,
|
||||
update=True
|
||||
)
|
||||
|
||||
def imprint(self, node, values, update=False):
|
||||
# Never store instance node and instance id since that data comes
|
||||
# from the node's path
|
||||
values.pop("instance_node", None)
|
||||
values.pop("instance_id", None)
|
||||
imprint(node, values, update=update)
|
||||
|
||||
def remove_instances(self, instances):
|
||||
"""Remove specified instance from the scene.
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,14 @@ openpype.hosts.houdini.api.lib.reset_framerange()
|
|||
]]></scriptCode>
|
||||
</scriptItem>
|
||||
|
||||
<scriptItem id="update_context_vars">
|
||||
<label>Update Houdini Vars</label>
|
||||
<scriptCode><![CDATA[
|
||||
import openpype.hosts.houdini.api.lib
|
||||
openpype.hosts.houdini.api.lib.update_houdini_vars_context_dialog()
|
||||
]]></scriptCode>
|
||||
</scriptItem>
|
||||
|
||||
<separatorItem/>
|
||||
<scriptItem id="experimental_tools">
|
||||
<label>Experimental tools...</label>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import os
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
from qtpy import QtWidgets, QtGui
|
||||
|
||||
import maya.utils
|
||||
import maya.cmds as cmds
|
||||
|
||||
from openpype.settings import get_project_settings
|
||||
from openpype.pipeline import (
|
||||
get_current_project_name,
|
||||
get_current_asset_name,
|
||||
get_current_task_name
|
||||
)
|
||||
|
|
@ -46,12 +45,12 @@ def get_context_label():
|
|||
)
|
||||
|
||||
|
||||
def install():
|
||||
def install(project_settings):
|
||||
if cmds.about(batch=True):
|
||||
log.info("Skipping openpype.menu initialization in batch mode..")
|
||||
return
|
||||
|
||||
def deferred():
|
||||
def add_menu():
|
||||
pyblish_icon = host_tools.get_pyblish_icon()
|
||||
parent_widget = get_main_window()
|
||||
cmds.menu(
|
||||
|
|
@ -191,7 +190,7 @@ def install():
|
|||
|
||||
cmds.setParent(MENU_NAME, menu=True)
|
||||
|
||||
def add_scripts_menu():
|
||||
def add_scripts_menu(project_settings):
|
||||
try:
|
||||
import scriptsmenu.launchformaya as launchformaya
|
||||
except ImportError:
|
||||
|
|
@ -201,9 +200,6 @@ def install():
|
|||
)
|
||||
return
|
||||
|
||||
# load configuration of custom menu
|
||||
project_name = get_current_project_name()
|
||||
project_settings = get_project_settings(project_name)
|
||||
config = project_settings["maya"]["scriptsmenu"]["definition"]
|
||||
_menu = project_settings["maya"]["scriptsmenu"]["name"]
|
||||
|
||||
|
|
@ -225,8 +221,9 @@ def install():
|
|||
# so that it only gets called after Maya UI has initialized too.
|
||||
# This is crucial with Maya 2020+ which initializes without UI
|
||||
# first as a QCoreApplication
|
||||
maya.utils.executeDeferred(deferred)
|
||||
cmds.evalDeferred(add_scripts_menu, lowestPriority=True)
|
||||
maya.utils.executeDeferred(add_menu)
|
||||
cmds.evalDeferred(partial(add_scripts_menu, project_settings),
|
||||
lowestPriority=True)
|
||||
|
||||
|
||||
def uninstall():
|
||||
|
|
|
|||
|
|
@ -28,8 +28,6 @@ from openpype.lib import (
|
|||
from openpype.pipeline import (
|
||||
legacy_io,
|
||||
get_current_project_name,
|
||||
get_current_asset_name,
|
||||
get_current_task_name,
|
||||
register_loader_plugin_path,
|
||||
register_inventory_action_path,
|
||||
register_creator_plugin_path,
|
||||
|
|
@ -108,7 +106,7 @@ class MayaHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
|
|||
_set_project()
|
||||
self._register_callbacks()
|
||||
|
||||
menu.install()
|
||||
menu.install(project_settings)
|
||||
|
||||
register_event_callback("save", on_save)
|
||||
register_event_callback("open", on_open)
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ class MayaCreatorBase(object):
|
|||
# We never store the instance_node as value on the node since
|
||||
# it's the node name itself
|
||||
data.pop("instance_node", None)
|
||||
data.pop("instance_id", None)
|
||||
|
||||
# Don't store `families` since it's up to the creator itself
|
||||
# to define the initial publish families - not a stored attribute of
|
||||
|
|
@ -227,6 +228,7 @@ class MayaCreatorBase(object):
|
|||
|
||||
# Explicitly re-parse the node name
|
||||
node_data["instance_node"] = node
|
||||
node_data["instance_id"] = node
|
||||
|
||||
# If the creator plug-in specifies
|
||||
families = self.get_publish_families()
|
||||
|
|
@ -601,6 +603,13 @@ class RenderlayerCreator(NewCreator, MayaCreatorBase):
|
|||
class Loader(LoaderPlugin):
|
||||
hosts = ["maya"]
|
||||
|
||||
load_settings = {} # defined in settings
|
||||
|
||||
@classmethod
|
||||
def apply_settings(cls, project_settings, system_settings):
|
||||
super(Loader, cls).apply_settings(project_settings, system_settings)
|
||||
cls.load_settings = project_settings['maya']['load']
|
||||
|
||||
def get_custom_namespace_and_group(self, context, options, loader_key):
|
||||
"""Queries Settings to get custom template for namespace and group.
|
||||
|
||||
|
|
@ -613,12 +622,9 @@ class Loader(LoaderPlugin):
|
|||
loader_key (str): key to get separate configuration from Settings
|
||||
('reference_loader'|'import_loader')
|
||||
"""
|
||||
options["attach_to_root"] = True
|
||||
|
||||
asset = context['asset']
|
||||
subset = context['subset']
|
||||
settings = get_project_settings(context['project']['name'])
|
||||
custom_naming = settings['maya']['load'][loader_key]
|
||||
options["attach_to_root"] = True
|
||||
custom_naming = self.load_settings[loader_key]
|
||||
|
||||
if not custom_naming['namespace']:
|
||||
raise LoadError("No namespace specified in "
|
||||
|
|
@ -627,6 +633,8 @@ class Loader(LoaderPlugin):
|
|||
self.log.debug("No custom group_name, no group will be created.")
|
||||
options["attach_to_root"] = False
|
||||
|
||||
asset = context['asset']
|
||||
subset = context['subset']
|
||||
formatting_data = {
|
||||
"asset_name": asset['name'],
|
||||
"asset_type": asset['type'],
|
||||
|
|
|
|||
|
|
@ -69,11 +69,8 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin,
|
|||
|
||||
invalid = []
|
||||
|
||||
project_settings = get_project_settings(
|
||||
legacy_io.Session["AVALON_PROJECT"]
|
||||
)
|
||||
collision_prefixes = (
|
||||
project_settings
|
||||
instance.context.data["project_settings"]
|
||||
["maya"]
|
||||
["create"]
|
||||
["CreateUnrealStaticMesh"]
|
||||
|
|
|
|||
|
|
@ -3425,34 +3425,6 @@ def create_viewer_profile_string(viewer, display=None, path_like=False):
|
|||
return "{} ({})".format(viewer, display)
|
||||
|
||||
|
||||
def get_head_filename_without_hashes(original_path, name):
|
||||
"""Function to get the renamed head filename without frame hashes
|
||||
To avoid the system being confused on finding the filename with
|
||||
frame hashes if the head of the filename has the hashed symbol
|
||||
|
||||
Examples:
|
||||
>>> get_head_filename_without_hashes("render.####.exr", "baking")
|
||||
render.baking.####.exr
|
||||
>>> get_head_filename_without_hashes("render.%04d.exr", "tag")
|
||||
render.tag.%d.exr
|
||||
>>> get_head_filename_without_hashes("exr.####.exr", "foo")
|
||||
exr.foo.%04d.exr
|
||||
|
||||
Args:
|
||||
original_path (str): the filename with frame hashes
|
||||
name (str): the name of the tags
|
||||
|
||||
Returns:
|
||||
str: the renamed filename with the tag
|
||||
"""
|
||||
filename = os.path.basename(original_path)
|
||||
|
||||
def insert_name(matchobj):
|
||||
return "{}.{}".format(name, matchobj.group(0))
|
||||
|
||||
return re.sub(r"(%\d*d)|#+", insert_name, filename)
|
||||
|
||||
|
||||
def get_filenames_without_hash(filename, frame_start, frame_end):
|
||||
"""Get filenames without frame hash
|
||||
i.e. "renderCompositingMain.baking.0001.exr"
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ from .lib import (
|
|||
get_view_process_node,
|
||||
get_viewer_config_from_string,
|
||||
deprecated,
|
||||
get_head_filename_without_hashes,
|
||||
get_filenames_without_hash
|
||||
)
|
||||
from .pipeline import (
|
||||
|
|
@ -816,19 +815,20 @@ class ExporterReviewMov(ExporterReview):
|
|||
|
||||
self.log.info("File info was set...")
|
||||
|
||||
self.file = self.fhead + self.name + ".{}".format(self.ext)
|
||||
if ".{}".format(self.ext) not in VIDEO_EXTENSIONS:
|
||||
# filename would be with frame hashes if
|
||||
# the file extension is not in video format
|
||||
filename = get_head_filename_without_hashes(
|
||||
self.path_in, self.name)
|
||||
self.file = filename
|
||||
# make sure the filename are in
|
||||
# correct image output format
|
||||
if ".{}".format(self.ext) not in self.file:
|
||||
filename_no_ext, _ = os.path.splitext(filename)
|
||||
self.file = "{}.{}".format(filename_no_ext, self.ext)
|
||||
|
||||
if ".{}".format(self.ext) in VIDEO_EXTENSIONS:
|
||||
self.file = "{}{}.{}".format(
|
||||
self.fhead, self.name, self.ext)
|
||||
else:
|
||||
# Output is image (or image sequence)
|
||||
# When the file is an image it's possible it
|
||||
# has extra information after the `fhead` that
|
||||
# we want to preserve, e.g. like frame numbers
|
||||
# or frames hashes like `####`
|
||||
filename_no_ext = os.path.splitext(
|
||||
os.path.basename(self.path_in))[0]
|
||||
after_head = filename_no_ext[len(self.fhead):]
|
||||
self.file = "{}{}.{}.{}".format(
|
||||
self.fhead, self.name, after_head, self.ext)
|
||||
self.path = os.path.join(
|
||||
self.staging_dir, self.file).replace("\\", "/")
|
||||
|
||||
|
|
|
|||
|
|
@ -33,11 +33,13 @@ class ExtractReviewIntermediates(publish.Extractor):
|
|||
"""
|
||||
nuke_publish = project_settings["nuke"]["publish"]
|
||||
deprecated_setting = nuke_publish["ExtractReviewDataMov"]
|
||||
current_setting = nuke_publish["ExtractReviewIntermediates"]
|
||||
current_setting = nuke_publish.get("ExtractReviewIntermediates")
|
||||
if deprecated_setting["enabled"]:
|
||||
# Use deprecated settings if they are still enabled
|
||||
cls.viewer_lut_raw = deprecated_setting["viewer_lut_raw"]
|
||||
cls.outputs = deprecated_setting["outputs"]
|
||||
elif current_setting is None:
|
||||
pass
|
||||
elif current_setting["enabled"]:
|
||||
cls.viewer_lut_raw = current_setting["viewer_lut_raw"]
|
||||
cls.outputs = current_setting["outputs"]
|
||||
|
|
|
|||
|
|
@ -6,13 +6,10 @@ from .utils import (
|
|||
)
|
||||
|
||||
from .pipeline import (
|
||||
install,
|
||||
uninstall,
|
||||
ResolveHost,
|
||||
ls,
|
||||
containerise,
|
||||
update_container,
|
||||
publish,
|
||||
launch_workfiles_app,
|
||||
maintained_selection,
|
||||
remove_instance,
|
||||
list_instances
|
||||
|
|
@ -76,14 +73,10 @@ __all__ = [
|
|||
"bmdvf",
|
||||
|
||||
# pipeline
|
||||
"install",
|
||||
"uninstall",
|
||||
"ResolveHost",
|
||||
"ls",
|
||||
"containerise",
|
||||
"update_container",
|
||||
"reload_pipeline",
|
||||
"publish",
|
||||
"launch_workfiles_app",
|
||||
"maintained_selection",
|
||||
"remove_instance",
|
||||
"list_instances",
|
||||
|
|
|
|||
|
|
@ -5,11 +5,6 @@ from qtpy import QtWidgets, QtCore
|
|||
|
||||
from openpype.tools.utils import host_tools
|
||||
|
||||
from .pipeline import (
|
||||
publish,
|
||||
launch_workfiles_app
|
||||
)
|
||||
|
||||
|
||||
def load_stylesheet():
|
||||
path = os.path.join(os.path.dirname(__file__), "menu_style.qss")
|
||||
|
|
@ -113,7 +108,7 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
|
||||
def on_workfile_clicked(self):
|
||||
print("Clicked Workfile")
|
||||
launch_workfiles_app()
|
||||
host_tools.show_workfiles()
|
||||
|
||||
def on_create_clicked(self):
|
||||
print("Clicked Create")
|
||||
|
|
@ -121,7 +116,7 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
|
||||
def on_publish_clicked(self):
|
||||
print("Clicked Publish")
|
||||
publish(None)
|
||||
host_tools.show_publish(parent=None)
|
||||
|
||||
def on_load_clicked(self):
|
||||
print("Clicked Load")
|
||||
|
|
|
|||
|
|
@ -12,14 +12,24 @@ from openpype.pipeline import (
|
|||
schema,
|
||||
register_loader_plugin_path,
|
||||
register_creator_plugin_path,
|
||||
deregister_loader_plugin_path,
|
||||
deregister_creator_plugin_path,
|
||||
AVALON_CONTAINER_ID,
|
||||
)
|
||||
from openpype.tools.utils import host_tools
|
||||
from openpype.host import (
|
||||
HostBase,
|
||||
IWorkfileHost,
|
||||
ILoadHost
|
||||
)
|
||||
|
||||
from . import lib
|
||||
from .utils import get_resolve_module
|
||||
from .workio import (
|
||||
open_file,
|
||||
save_file,
|
||||
file_extensions,
|
||||
has_unsaved_changes,
|
||||
work_root,
|
||||
current_file
|
||||
)
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
|
||||
|
|
@ -32,53 +42,56 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
|
|||
AVALON_CONTAINERS = ":AVALON_CONTAINERS"
|
||||
|
||||
|
||||
def install():
|
||||
"""Install resolve-specific functionality of avalon-core.
|
||||
class ResolveHost(HostBase, IWorkfileHost, ILoadHost):
|
||||
name = "resolve"
|
||||
|
||||
This is where you install menus and register families, data
|
||||
and loaders into resolve.
|
||||
def install(self):
|
||||
"""Install resolve-specific functionality of avalon-core.
|
||||
|
||||
It is called automatically when installing via `api.install(resolve)`.
|
||||
This is where you install menus and register families, data
|
||||
and loaders into resolve.
|
||||
|
||||
See the Maya equivalent for inspiration on how to implement this.
|
||||
It is called automatically when installing via `api.install(resolve)`.
|
||||
|
||||
"""
|
||||
See the Maya equivalent for inspiration on how to implement this.
|
||||
|
||||
log.info("openpype.hosts.resolve installed")
|
||||
"""
|
||||
|
||||
pyblish.register_host("resolve")
|
||||
pyblish.register_plugin_path(PUBLISH_PATH)
|
||||
log.info("Registering DaVinci Resovle plug-ins..")
|
||||
log.info("openpype.hosts.resolve installed")
|
||||
|
||||
register_loader_plugin_path(LOAD_PATH)
|
||||
register_creator_plugin_path(CREATE_PATH)
|
||||
pyblish.register_host(self.name)
|
||||
pyblish.register_plugin_path(PUBLISH_PATH)
|
||||
print("Registering DaVinci Resolve plug-ins..")
|
||||
|
||||
# register callback for switching publishable
|
||||
pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled)
|
||||
register_loader_plugin_path(LOAD_PATH)
|
||||
register_creator_plugin_path(CREATE_PATH)
|
||||
|
||||
get_resolve_module()
|
||||
# register callback for switching publishable
|
||||
pyblish.register_callback("instanceToggled",
|
||||
on_pyblish_instance_toggled)
|
||||
|
||||
get_resolve_module()
|
||||
|
||||
def uninstall():
|
||||
"""Uninstall all that was installed
|
||||
def open_workfile(self, filepath):
|
||||
return open_file(filepath)
|
||||
|
||||
This is where you undo everything that was done in `install()`.
|
||||
That means, removing menus, deregistering families and data
|
||||
and everything. It should be as though `install()` was never run,
|
||||
because odds are calling this function means the user is interested
|
||||
in re-installing shortly afterwards. If, for example, he has been
|
||||
modifying the menu or registered families.
|
||||
def save_workfile(self, filepath=None):
|
||||
return save_file(filepath)
|
||||
|
||||
"""
|
||||
pyblish.deregister_host("resolve")
|
||||
pyblish.deregister_plugin_path(PUBLISH_PATH)
|
||||
log.info("Deregistering DaVinci Resovle plug-ins..")
|
||||
def work_root(self, session):
|
||||
return work_root(session)
|
||||
|
||||
deregister_loader_plugin_path(LOAD_PATH)
|
||||
deregister_creator_plugin_path(CREATE_PATH)
|
||||
def get_current_workfile(self):
|
||||
return current_file()
|
||||
|
||||
# register callback for switching publishable
|
||||
pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled)
|
||||
def workfile_has_unsaved_changes(self):
|
||||
return has_unsaved_changes()
|
||||
|
||||
def get_workfile_extensions(self):
|
||||
return file_extensions()
|
||||
|
||||
def get_containers(self):
|
||||
return ls()
|
||||
|
||||
|
||||
def containerise(timeline_item,
|
||||
|
|
@ -206,15 +219,6 @@ def update_container(timeline_item, data=None):
|
|||
return bool(lib.set_timeline_item_pype_tag(timeline_item, container))
|
||||
|
||||
|
||||
def launch_workfiles_app(*args):
|
||||
host_tools.show_workfiles()
|
||||
|
||||
|
||||
def publish(parent):
|
||||
"""Shorthand to publish from within host"""
|
||||
return host_tools.show_publish()
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def maintained_selection():
|
||||
"""Maintain selection during context
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ def get_resolve_module():
|
|||
# dont run if already loaded
|
||||
if api.bmdvr:
|
||||
log.info(("resolve module is assigned to "
|
||||
f"`pype.hosts.resolve.api.bmdvr`: {api.bmdvr}"))
|
||||
f"`openpype.hosts.resolve.api.bmdvr`: {api.bmdvr}"))
|
||||
return api.bmdvr
|
||||
try:
|
||||
"""
|
||||
|
|
@ -41,6 +41,10 @@ def get_resolve_module():
|
|||
)
|
||||
elif sys.platform.startswith("linux"):
|
||||
expected_path = "/opt/resolve/libs/Fusion/Modules"
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Unsupported platform: {}".format(sys.platform)
|
||||
)
|
||||
|
||||
# check if the default path has it...
|
||||
print(("Unable to find module DaVinciResolveScript from "
|
||||
|
|
@ -74,6 +78,6 @@ def get_resolve_module():
|
|||
api.bmdvr = bmdvr
|
||||
api.bmdvf = bmdvf
|
||||
log.info(("Assigning resolve module to "
|
||||
f"`pype.hosts.resolve.api.bmdvr`: {api.bmdvr}"))
|
||||
f"`openpype.hosts.resolve.api.bmdvr`: {api.bmdvr}"))
|
||||
log.info(("Assigning resolve module to "
|
||||
f"`pype.hosts.resolve.api.bmdvf`: {api.bmdvf}"))
|
||||
f"`openpype.hosts.resolve.api.bmdvf`: {api.bmdvf}"))
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ def ensure_installed_host():
|
|||
if host:
|
||||
return host
|
||||
|
||||
install_host(openpype.hosts.resolve.api)
|
||||
host = openpype.hosts.resolve.api.ResolveHost()
|
||||
install_host(host)
|
||||
return registered_host()
|
||||
|
||||
|
||||
|
|
@ -37,10 +38,10 @@ def launch_menu():
|
|||
openpype.hosts.resolve.api.launch_pype_menu()
|
||||
|
||||
|
||||
def open_file(path):
|
||||
def open_workfile(path):
|
||||
# Avoid the need to "install" the host
|
||||
host = ensure_installed_host()
|
||||
host.open_file(path)
|
||||
host.open_workfile(path)
|
||||
|
||||
|
||||
def main():
|
||||
|
|
@ -49,7 +50,7 @@ def main():
|
|||
|
||||
if workfile_path and os.path.exists(workfile_path):
|
||||
log.info(f"Opening last workfile: {workfile_path}")
|
||||
open_file(workfile_path)
|
||||
open_workfile(workfile_path)
|
||||
else:
|
||||
log.info("No last workfile set to open. Skipping..")
|
||||
|
||||
|
|
|
|||
|
|
@ -8,12 +8,13 @@ log = Logger.get_logger(__name__)
|
|||
|
||||
|
||||
def main(env):
|
||||
import openpype.hosts.resolve.api as bmdvr
|
||||
from openpype.hosts.resolve.api import ResolveHost, launch_pype_menu
|
||||
|
||||
# activate resolve from openpype
|
||||
install_host(bmdvr)
|
||||
host = ResolveHost()
|
||||
install_host(host)
|
||||
|
||||
bmdvr.launch_pype_menu()
|
||||
launch_pype_menu()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import requests
|
|||
|
||||
import pyblish.api
|
||||
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.pipeline.publish import (
|
||||
OpenPypePyblishPluginMixin
|
||||
|
|
@ -34,6 +35,8 @@ class FusionSubmitDeadline(
|
|||
targets = ["local"]
|
||||
|
||||
# presets
|
||||
plugin = None
|
||||
|
||||
priority = 50
|
||||
chunk_size = 1
|
||||
concurrent_tasks = 1
|
||||
|
|
@ -173,7 +176,7 @@ class FusionSubmitDeadline(
|
|||
"SecondaryPool": instance.data.get("secondaryPool"),
|
||||
"Group": self.group,
|
||||
|
||||
"Plugin": "Fusion",
|
||||
"Plugin": self.plugin,
|
||||
"Frames": "{start}-{end}".format(
|
||||
start=int(instance.data["frameStartHandle"]),
|
||||
end=int(instance.data["frameEndHandle"])
|
||||
|
|
@ -216,16 +219,29 @@ class FusionSubmitDeadline(
|
|||
|
||||
# Include critical variables with submission
|
||||
keys = [
|
||||
# TODO: This won't work if the slaves don't have access to
|
||||
# these paths, such as if slaves are running Linux and the
|
||||
# submitter is on Windows.
|
||||
"PYTHONPATH",
|
||||
"OFX_PLUGIN_PATH",
|
||||
"FUSION9_MasterPrefs"
|
||||
"FTRACK_API_KEY",
|
||||
"FTRACK_API_USER",
|
||||
"FTRACK_SERVER",
|
||||
"AVALON_PROJECT",
|
||||
"AVALON_ASSET",
|
||||
"AVALON_TASK",
|
||||
"AVALON_APP_NAME",
|
||||
"OPENPYPE_DEV",
|
||||
"OPENPYPE_LOG_NO_COLORS",
|
||||
"IS_TEST"
|
||||
]
|
||||
environment = dict({key: os.environ[key] for key in keys
|
||||
if key in os.environ}, **legacy_io.Session)
|
||||
|
||||
# to recognize render jobs
|
||||
if AYON_SERVER_ENABLED:
|
||||
environment["AYON_BUNDLE_NAME"] = os.environ["AYON_BUNDLE_NAME"]
|
||||
render_job_label = "AYON_RENDER_JOB"
|
||||
else:
|
||||
render_job_label = "OPENPYPE_RENDER_JOB"
|
||||
|
||||
environment[render_job_label] = "1"
|
||||
|
||||
payload["JobInfo"].update({
|
||||
"EnvironmentKeyValue%d" % index: "{key}={value}".format(
|
||||
key=key,
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ class AyonDeadlinePlugin(DeadlinePlugin):
|
|||
for path in exe_list.split(";"):
|
||||
if path.startswith("~"):
|
||||
path = os.path.expanduser(path)
|
||||
expanded_paths.append(path)
|
||||
expanded_paths.append(path)
|
||||
exe = FileUtils.SearchFileList(";".join(expanded_paths))
|
||||
|
||||
if exe == "":
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ def _get_template_id(renderer):
|
|||
:rtype: int
|
||||
"""
|
||||
|
||||
# TODO: Use settings from context?
|
||||
templates = get_system_settings()["modules"]["muster"]["templates_mapping"]
|
||||
if not templates:
|
||||
raise RuntimeError(("Muster template mapping missing in "
|
||||
|
|
@ -25,7 +25,10 @@ from openpype.tests.lib import is_in_tests
|
|||
|
||||
from .publish.lib import filter_pyblish_plugins
|
||||
from .anatomy import Anatomy
|
||||
from .template_data import get_template_data_with_names
|
||||
from .template_data import (
|
||||
get_template_data_with_names,
|
||||
get_template_data
|
||||
)
|
||||
from .workfile import (
|
||||
get_workfile_template_key,
|
||||
get_custom_workfile_template_by_string_context,
|
||||
|
|
@ -658,3 +661,70 @@ def get_process_id():
|
|||
if _process_id is None:
|
||||
_process_id = str(uuid.uuid4())
|
||||
return _process_id
|
||||
|
||||
|
||||
def get_current_context_template_data():
|
||||
"""Template data for template fill from current context
|
||||
|
||||
Returns:
|
||||
Dict[str, Any] of the following tokens and their values
|
||||
Supported Tokens:
|
||||
- Regular Tokens
|
||||
- app
|
||||
- user
|
||||
- asset
|
||||
- parent
|
||||
- hierarchy
|
||||
- folder[name]
|
||||
- root[work, ...]
|
||||
- studio[code, name]
|
||||
- project[code, name]
|
||||
- task[type, name, short]
|
||||
|
||||
- Context Specific Tokens
|
||||
- assetData[frameStart]
|
||||
- assetData[frameEnd]
|
||||
- assetData[handleStart]
|
||||
- assetData[handleEnd]
|
||||
- assetData[frameStartHandle]
|
||||
- assetData[frameEndHandle]
|
||||
- assetData[resolutionHeight]
|
||||
- assetData[resolutionWidth]
|
||||
|
||||
"""
|
||||
|
||||
# pre-prepare get_template_data args
|
||||
current_context = get_current_context()
|
||||
project_name = current_context["project_name"]
|
||||
asset_name = current_context["asset_name"]
|
||||
anatomy = Anatomy(project_name)
|
||||
|
||||
# prepare get_template_data args
|
||||
project_doc = get_project(project_name)
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
task_name = current_context["task_name"]
|
||||
host_name = get_current_host_name()
|
||||
|
||||
# get regular template data
|
||||
template_data = get_template_data(
|
||||
project_doc, asset_doc, task_name, host_name
|
||||
)
|
||||
|
||||
template_data["root"] = anatomy.roots
|
||||
|
||||
# get context specific vars
|
||||
asset_data = asset_doc["data"].copy()
|
||||
|
||||
# compute `frameStartHandle` and `frameEndHandle`
|
||||
if "frameStart" in asset_data and "handleStart" in asset_data:
|
||||
asset_data["frameStartHandle"] = \
|
||||
asset_data["frameStart"] - asset_data["handleStart"]
|
||||
|
||||
if "frameEnd" in asset_data and "handleEnd" in asset_data:
|
||||
asset_data["frameEndHandle"] = \
|
||||
asset_data["frameEnd"] + asset_data["handleEnd"]
|
||||
|
||||
# add assetData
|
||||
template_data["assetData"] = asset_data
|
||||
|
||||
return template_data
|
||||
|
|
|
|||
|
|
@ -83,10 +83,6 @@ class OpenTaskPath(LauncherAction):
|
|||
if os.path.exists(valid_workdir):
|
||||
return valid_workdir
|
||||
|
||||
# If task was selected, try to find asset path only to asset
|
||||
if not task_name:
|
||||
raise AssertionError("Folder does not exist.")
|
||||
|
||||
data.pop("task", None)
|
||||
workdir = anatomy.templates_obj["work"]["folder"].format(data)
|
||||
valid_workdir = self._find_first_filled_path(workdir)
|
||||
|
|
@ -95,7 +91,7 @@ class OpenTaskPath(LauncherAction):
|
|||
valid_workdir = os.path.normpath(valid_workdir)
|
||||
if os.path.exists(valid_workdir):
|
||||
return valid_workdir
|
||||
raise AssertionError("Folder does not exist.")
|
||||
raise AssertionError("Folder does not exist yet.")
|
||||
|
||||
@staticmethod
|
||||
def open_in_explorer(path):
|
||||
|
|
|
|||
|
|
@ -271,12 +271,6 @@ class PypeCommands:
|
|||
|
||||
if mongo_url:
|
||||
args.extend(["--mongo_url", mongo_url])
|
||||
else:
|
||||
msg = (
|
||||
"Either provide uri to MongoDB through environment variable"
|
||||
" OPENPYPE_MONGO or the command flag --mongo_url"
|
||||
)
|
||||
assert not os.environ.get("OPENPYPE_MONGO"), msg
|
||||
|
||||
print("run_tests args: {}".format(args))
|
||||
import pytest
|
||||
|
|
|
|||
|
|
@ -748,15 +748,17 @@ def _convert_nuke_project_settings(ayon_settings, output):
|
|||
)
|
||||
|
||||
new_review_data_outputs = {}
|
||||
outputs_settings = None
|
||||
outputs_settings = []
|
||||
# Check deprecated ExtractReviewDataMov
|
||||
# settings for backwards compatibility
|
||||
deprecrated_review_settings = ayon_publish["ExtractReviewDataMov"]
|
||||
current_review_settings = (
|
||||
ayon_publish["ExtractReviewIntermediates"]
|
||||
ayon_publish.get("ExtractReviewIntermediates")
|
||||
)
|
||||
if deprecrated_review_settings["enabled"]:
|
||||
outputs_settings = deprecrated_review_settings["outputs"]
|
||||
elif current_review_settings is None:
|
||||
pass
|
||||
elif current_review_settings["enabled"]:
|
||||
outputs_settings = current_review_settings["outputs"]
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,8 @@
|
|||
"priority": 50,
|
||||
"chunk_size": 10,
|
||||
"concurrent_tasks": 1,
|
||||
"group": ""
|
||||
"group": "",
|
||||
"plugin": "Fusion"
|
||||
},
|
||||
"NukeSubmitDeadline": {
|
||||
"enabled": true,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,16 @@
|
|||
{
|
||||
"general": {
|
||||
"update_houdini_var_context": {
|
||||
"enabled": true,
|
||||
"houdini_vars":[
|
||||
{
|
||||
"var": "JOB",
|
||||
"value": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}",
|
||||
"is_directory": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"imageio": {
|
||||
"activate_host_color_management": true,
|
||||
"ocio_config": {
|
||||
|
|
|
|||
|
|
@ -289,6 +289,15 @@
|
|||
"type": "text",
|
||||
"key": "group",
|
||||
"label": "Group Name"
|
||||
},
|
||||
{
|
||||
"type": "enum",
|
||||
"key": "plugin",
|
||||
"label": "Deadline Plugin",
|
||||
"enum_items": [
|
||||
{"Fusion": "Fusion"},
|
||||
{"FusionCmd": "FusionCmd"}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"label": "Houdini",
|
||||
"is_file": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_houdini_general"
|
||||
},
|
||||
{
|
||||
"key": "imageio",
|
||||
"type": "dict",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"type": "dict",
|
||||
"key": "general",
|
||||
"label": "General",
|
||||
"collapsible": true,
|
||||
"is_group": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"checkbox_key": "enabled",
|
||||
"key": "update_houdini_var_context",
|
||||
"label": "Update Houdini Vars on context change",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "Sync vars with context changes.<br>If a value is treated as a directory on update it will be ensured the folder exists"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "houdini_vars",
|
||||
"label": "Houdini Vars",
|
||||
"collapsible": false,
|
||||
"object_type": {
|
||||
"type": "dict",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"key": "var",
|
||||
"label": "Var"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "value",
|
||||
"label": "Value"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "is_directory",
|
||||
"label": "Treat as directory"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -272,7 +272,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
|
||||
@abstractmethod
|
||||
def set_application_force_not_open_workfile(
|
||||
self, project_name, folder_id, task_id, action_id, enabled
|
||||
self, project_name, folder_id, task_id, action_ids, enabled
|
||||
):
|
||||
"""This is application action related to force not open last workfile.
|
||||
|
||||
|
|
@ -280,7 +280,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
project_name (Union[str, None]): Project name.
|
||||
folder_id (Union[str, None]): Folder id.
|
||||
task_id (Union[str, None]): Task id.
|
||||
action_id (str): Action identifier.
|
||||
action_id (Iterable[str]): Action identifiers.
|
||||
enabled (bool): New value of force not open workfile.
|
||||
"""
|
||||
|
||||
|
|
@ -295,3 +295,13 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def refresh_actions(self):
|
||||
"""Refresh actions and all related data.
|
||||
|
||||
Triggers 'controller.refresh.actions.started' event at the beginning
|
||||
and 'controller.refresh.actions.finished' at the end.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -121,10 +121,10 @@ class BaseLauncherController(
|
|||
project_name, folder_id, task_id)
|
||||
|
||||
def set_application_force_not_open_workfile(
|
||||
self, project_name, folder_id, task_id, action_id, enabled
|
||||
self, project_name, folder_id, task_id, action_ids, enabled
|
||||
):
|
||||
self._actions_model.set_application_force_not_open_workfile(
|
||||
project_name, folder_id, task_id, action_id, enabled
|
||||
project_name, folder_id, task_id, action_ids, enabled
|
||||
)
|
||||
|
||||
def trigger_action(self, project_name, folder_id, task_id, identifier):
|
||||
|
|
@ -145,5 +145,17 @@ class BaseLauncherController(
|
|||
|
||||
self._emit_event("controller.refresh.finished")
|
||||
|
||||
def refresh_actions(self):
|
||||
self._emit_event("controller.refresh.actions.started")
|
||||
|
||||
# Refresh project settings (used for actions discovery)
|
||||
self._project_settings = {}
|
||||
# Refresh projects - they define applications
|
||||
self._projects_model.reset()
|
||||
# Refresh actions
|
||||
self._actions_model.refresh()
|
||||
|
||||
self._emit_event("controller.refresh.actions.finished")
|
||||
|
||||
def _emit_event(self, topic, data=None):
|
||||
self.emit_event(topic, data, "controller")
|
||||
|
|
|
|||
|
|
@ -326,13 +326,14 @@ class ActionsModel:
|
|||
return output
|
||||
|
||||
def set_application_force_not_open_workfile(
|
||||
self, project_name, folder_id, task_id, action_id, enabled
|
||||
self, project_name, folder_id, task_id, action_ids, enabled
|
||||
):
|
||||
no_workfile_reg_data = self._get_no_last_workfile_reg_data()
|
||||
project_data = no_workfile_reg_data.setdefault(project_name, {})
|
||||
folder_data = project_data.setdefault(folder_id, {})
|
||||
task_data = folder_data.setdefault(task_id, {})
|
||||
task_data[action_id] = enabled
|
||||
for action_id in action_ids:
|
||||
task_data[action_id] = enabled
|
||||
self._launcher_tool_reg.set_item(
|
||||
self._not_open_workfile_reg_key, no_workfile_reg_data
|
||||
)
|
||||
|
|
@ -359,7 +360,10 @@ class ActionsModel:
|
|||
project_name, folder_id, task_id
|
||||
)
|
||||
force_not_open_workfile = per_action.get(identifier, False)
|
||||
action.data["start_last_workfile"] = force_not_open_workfile
|
||||
if force_not_open_workfile:
|
||||
action.data["start_last_workfile"] = False
|
||||
else:
|
||||
action.data.pop("start_last_workfile", None)
|
||||
action.process(session)
|
||||
except Exception as exc:
|
||||
self.log.warning("Action trigger failed.", exc_info=True)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,21 @@ ANIMATION_STATE_ROLE = QtCore.Qt.UserRole + 6
|
|||
FORCE_NOT_OPEN_WORKFILE_ROLE = QtCore.Qt.UserRole + 7
|
||||
|
||||
|
||||
def _variant_label_sort_getter(action_item):
|
||||
"""Get variant label value for sorting.
|
||||
|
||||
Make sure the output value is a string.
|
||||
|
||||
Args:
|
||||
action_item (ActionItem): Action item.
|
||||
|
||||
Returns:
|
||||
str: Variant label or empty string.
|
||||
"""
|
||||
|
||||
return action_item.variant_label or ""
|
||||
|
||||
|
||||
class ActionsQtModel(QtGui.QStandardItemModel):
|
||||
"""Qt model for actions.
|
||||
|
||||
|
|
@ -31,10 +46,6 @@ class ActionsQtModel(QtGui.QStandardItemModel):
|
|||
def __init__(self, controller):
|
||||
super(ActionsQtModel, self).__init__()
|
||||
|
||||
controller.register_event_callback(
|
||||
"controller.refresh.finished",
|
||||
self._on_controller_refresh_finished,
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"selection.project.changed",
|
||||
self._on_selection_project_changed,
|
||||
|
|
@ -51,6 +62,7 @@ class ActionsQtModel(QtGui.QStandardItemModel):
|
|||
self._controller = controller
|
||||
|
||||
self._items_by_id = {}
|
||||
self._action_items_by_id = {}
|
||||
self._groups_by_id = {}
|
||||
|
||||
self._selected_project_name = None
|
||||
|
|
@ -72,8 +84,12 @@ class ActionsQtModel(QtGui.QStandardItemModel):
|
|||
def get_item_by_id(self, action_id):
|
||||
return self._items_by_id.get(action_id)
|
||||
|
||||
def get_action_item_by_id(self, action_id):
|
||||
return self._action_items_by_id.get(action_id)
|
||||
|
||||
def _clear_items(self):
|
||||
self._items_by_id = {}
|
||||
self._action_items_by_id = {}
|
||||
self._groups_by_id = {}
|
||||
root = self.invisibleRootItem()
|
||||
root.removeRows(0, root.rowCount())
|
||||
|
|
@ -101,12 +117,14 @@ class ActionsQtModel(QtGui.QStandardItemModel):
|
|||
|
||||
groups_by_id = {}
|
||||
for action_items in items_by_label.values():
|
||||
action_items.sort(key=_variant_label_sort_getter, reverse=True)
|
||||
first_item = next(iter(action_items))
|
||||
all_action_items_info.append((first_item, len(action_items) > 1))
|
||||
groups_by_id[first_item.identifier] = action_items
|
||||
|
||||
new_items = []
|
||||
items_by_id = {}
|
||||
action_items_by_id = {}
|
||||
for action_item_info in all_action_items_info:
|
||||
action_item, is_group = action_item_info
|
||||
icon = get_qt_icon(action_item.icon)
|
||||
|
|
@ -132,6 +150,7 @@ class ActionsQtModel(QtGui.QStandardItemModel):
|
|||
action_item.force_not_open_workfile,
|
||||
FORCE_NOT_OPEN_WORKFILE_ROLE)
|
||||
items_by_id[action_item.identifier] = item
|
||||
action_items_by_id[action_item.identifier] = action_item
|
||||
|
||||
if new_items:
|
||||
root_item.appendRows(new_items)
|
||||
|
|
@ -139,19 +158,14 @@ class ActionsQtModel(QtGui.QStandardItemModel):
|
|||
to_remove = set(self._items_by_id.keys()) - set(items_by_id.keys())
|
||||
for identifier in to_remove:
|
||||
item = self._items_by_id.pop(identifier)
|
||||
self._action_items_by_id.pop(identifier)
|
||||
root_item.removeRow(item.row())
|
||||
|
||||
self._groups_by_id = groups_by_id
|
||||
self._items_by_id = items_by_id
|
||||
self._action_items_by_id = action_items_by_id
|
||||
self.refreshed.emit()
|
||||
|
||||
def _on_controller_refresh_finished(self):
|
||||
context = self._controller.get_selected_context()
|
||||
self._selected_project_name = context["project_name"]
|
||||
self._selected_folder_id = context["folder_id"]
|
||||
self._selected_task_id = context["task_id"]
|
||||
self.refresh()
|
||||
|
||||
def _on_selection_project_changed(self, event):
|
||||
self._selected_project_name = event["project_name"]
|
||||
self._selected_folder_id = None
|
||||
|
|
@ -336,6 +350,9 @@ class ActionsWidget(QtWidgets.QWidget):
|
|||
|
||||
self._set_row_height(1)
|
||||
|
||||
def refresh(self):
|
||||
self._model.refresh()
|
||||
|
||||
def _set_row_height(self, rows):
|
||||
self.setMinimumHeight(rows * 75)
|
||||
|
||||
|
|
@ -387,9 +404,15 @@ class ActionsWidget(QtWidgets.QWidget):
|
|||
checkbox.setChecked(True)
|
||||
|
||||
action_id = index.data(ACTION_ID_ROLE)
|
||||
is_group = index.data(ACTION_IS_GROUP_ROLE)
|
||||
if is_group:
|
||||
action_items = self._model.get_group_items(action_id)
|
||||
else:
|
||||
action_items = [self._model.get_action_item_by_id(action_id)]
|
||||
action_ids = {action_item.identifier for action_item in action_items}
|
||||
checkbox.stateChanged.connect(
|
||||
lambda: self._on_checkbox_changed(
|
||||
action_id, checkbox.isChecked()
|
||||
action_ids, checkbox.isChecked()
|
||||
)
|
||||
)
|
||||
action = QtWidgets.QWidgetAction(menu)
|
||||
|
|
@ -402,7 +425,7 @@ class ActionsWidget(QtWidgets.QWidget):
|
|||
menu.exec_(global_point)
|
||||
self._context_menu = None
|
||||
|
||||
def _on_checkbox_changed(self, action_id, is_checked):
|
||||
def _on_checkbox_changed(self, action_ids, is_checked):
|
||||
if self._context_menu is not None:
|
||||
self._context_menu.close()
|
||||
|
||||
|
|
@ -410,7 +433,7 @@ class ActionsWidget(QtWidgets.QWidget):
|
|||
folder_id = self._model.get_selected_folder_id()
|
||||
task_id = self._model.get_selected_task_id()
|
||||
self._controller.set_application_force_not_open_workfile(
|
||||
project_name, folder_id, task_id, action_id, is_checked)
|
||||
project_name, folder_id, task_id, action_ids, is_checked)
|
||||
self._model.refresh()
|
||||
|
||||
def _on_clicked(self, index):
|
||||
|
|
|
|||
|
|
@ -92,6 +92,10 @@ class HierarchyPage(QtWidgets.QWidget):
|
|||
if visible and project_name:
|
||||
self._projects_combobox.set_selection(project_name)
|
||||
|
||||
def refresh(self):
|
||||
self._folders_widget.refresh()
|
||||
self._tasks_widget.refresh()
|
||||
|
||||
def _on_back_clicked(self):
|
||||
self._controller.set_selected_project(None)
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,9 @@ class ProjectIconView(QtWidgets.QListView):
|
|||
|
||||
class ProjectsWidget(QtWidgets.QWidget):
|
||||
"""Projects Page"""
|
||||
|
||||
refreshed = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent=None):
|
||||
super(ProjectsWidget, self).__init__(parent=parent)
|
||||
|
||||
|
|
@ -104,6 +107,7 @@ class ProjectsWidget(QtWidgets.QWidget):
|
|||
main_layout.addWidget(projects_view, 1)
|
||||
|
||||
projects_view.clicked.connect(self._on_view_clicked)
|
||||
projects_model.refreshed.connect(self.refreshed)
|
||||
projects_filter_text.textChanged.connect(
|
||||
self._on_project_filter_change)
|
||||
refresh_btn.clicked.connect(self._on_refresh_clicked)
|
||||
|
|
@ -119,6 +123,15 @@ class ProjectsWidget(QtWidgets.QWidget):
|
|||
self._projects_model = projects_model
|
||||
self._projects_proxy_model = projects_proxy_model
|
||||
|
||||
def has_content(self):
|
||||
"""Model has at least one project.
|
||||
|
||||
Returns:
|
||||
bool: True if there is any content in the model.
|
||||
"""
|
||||
|
||||
return self._projects_model.has_content()
|
||||
|
||||
def _on_view_clicked(self, index):
|
||||
if index.isValid():
|
||||
project_name = index.data(QtCore.Qt.DisplayRole)
|
||||
|
|
|
|||
|
|
@ -99,8 +99,8 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
message_timer.setInterval(self.message_interval)
|
||||
message_timer.setSingleShot(True)
|
||||
|
||||
refresh_timer = QtCore.QTimer()
|
||||
refresh_timer.setInterval(self.refresh_interval)
|
||||
actions_refresh_timer = QtCore.QTimer()
|
||||
actions_refresh_timer.setInterval(self.refresh_interval)
|
||||
|
||||
page_slide_anim = QtCore.QVariantAnimation(self)
|
||||
page_slide_anim.setDuration(self.page_side_anim_interval)
|
||||
|
|
@ -108,8 +108,10 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
page_slide_anim.setEndValue(1.0)
|
||||
page_slide_anim.setEasingCurve(QtCore.QEasingCurve.OutQuad)
|
||||
|
||||
projects_page.refreshed.connect(self._on_projects_refresh)
|
||||
message_timer.timeout.connect(self._on_message_timeout)
|
||||
refresh_timer.timeout.connect(self._on_refresh_timeout)
|
||||
actions_refresh_timer.timeout.connect(
|
||||
self._on_actions_refresh_timeout)
|
||||
page_slide_anim.valueChanged.connect(
|
||||
self._on_page_slide_value_changed)
|
||||
page_slide_anim.finished.connect(self._on_page_slide_finished)
|
||||
|
|
@ -132,6 +134,7 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
self._is_on_projects_page = True
|
||||
self._window_is_active = False
|
||||
self._refresh_on_activate = False
|
||||
self._selected_project_name = None
|
||||
|
||||
self._pages_widget = pages_widget
|
||||
self._pages_layout = pages_layout
|
||||
|
|
@ -143,7 +146,7 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
# self._action_history = action_history
|
||||
|
||||
self._message_timer = message_timer
|
||||
self._refresh_timer = refresh_timer
|
||||
self._actions_refresh_timer = actions_refresh_timer
|
||||
self._page_slide_anim = page_slide_anim
|
||||
|
||||
hierarchy_page.setVisible(not self._is_on_projects_page)
|
||||
|
|
@ -152,14 +155,14 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
def showEvent(self, event):
|
||||
super(LauncherWindow, self).showEvent(event)
|
||||
self._window_is_active = True
|
||||
if not self._refresh_timer.isActive():
|
||||
self._refresh_timer.start()
|
||||
if not self._actions_refresh_timer.isActive():
|
||||
self._actions_refresh_timer.start()
|
||||
self._controller.refresh()
|
||||
|
||||
def closeEvent(self, event):
|
||||
super(LauncherWindow, self).closeEvent(event)
|
||||
self._window_is_active = False
|
||||
self._refresh_timer.stop()
|
||||
self._actions_refresh_timer.stop()
|
||||
|
||||
def changeEvent(self, event):
|
||||
if event.type() in (
|
||||
|
|
@ -170,15 +173,15 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
self._window_is_active = is_active
|
||||
if is_active and self._refresh_on_activate:
|
||||
self._refresh_on_activate = False
|
||||
self._on_refresh_timeout()
|
||||
self._refresh_timer.start()
|
||||
self._on_actions_refresh_timeout()
|
||||
self._actions_refresh_timer.start()
|
||||
|
||||
super(LauncherWindow, self).changeEvent(event)
|
||||
|
||||
def _on_refresh_timeout(self):
|
||||
def _on_actions_refresh_timeout(self):
|
||||
# Stop timer if widget is not visible
|
||||
if self._window_is_active:
|
||||
self._controller.refresh()
|
||||
self._controller.refresh_actions()
|
||||
else:
|
||||
self._refresh_on_activate = True
|
||||
|
||||
|
|
@ -191,12 +194,26 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
|
||||
def _on_project_selection_change(self, event):
|
||||
project_name = event["project_name"]
|
||||
self._selected_project_name = project_name
|
||||
if not project_name:
|
||||
self._go_to_projects_page()
|
||||
|
||||
elif self._is_on_projects_page:
|
||||
self._go_to_hierarchy_page(project_name)
|
||||
|
||||
def _on_projects_refresh(self):
|
||||
# There is nothing to do, we're on projects page
|
||||
if self._is_on_projects_page:
|
||||
return
|
||||
|
||||
# No projects were found -> go back to projects page
|
||||
if not self._projects_page.has_content():
|
||||
self._go_to_projects_page()
|
||||
return
|
||||
|
||||
self._hierarchy_page.refresh()
|
||||
self._actions_widget.refresh()
|
||||
|
||||
def _on_action_trigger_started(self, event):
|
||||
self._echo("Running action: {}".format(event["full_label"]))
|
||||
|
||||
|
|
|
|||
|
|
@ -199,13 +199,18 @@ class HierarchyModel(object):
|
|||
Hierarchy items are folders and tasks. Folders can have as parent another
|
||||
folder or project. Tasks can have as parent only folder.
|
||||
"""
|
||||
lifetime = 60 # A minute
|
||||
|
||||
def __init__(self, controller):
|
||||
self._folders_items = NestedCacheItem(levels=1, default_factory=dict)
|
||||
self._folders_by_id = NestedCacheItem(levels=2, default_factory=dict)
|
||||
self._folders_items = NestedCacheItem(
|
||||
levels=1, default_factory=dict, lifetime=self.lifetime)
|
||||
self._folders_by_id = NestedCacheItem(
|
||||
levels=2, default_factory=dict, lifetime=self.lifetime)
|
||||
|
||||
self._task_items = NestedCacheItem(levels=2, default_factory=dict)
|
||||
self._tasks_by_id = NestedCacheItem(levels=2, default_factory=dict)
|
||||
self._task_items = NestedCacheItem(
|
||||
levels=2, default_factory=dict, lifetime=self.lifetime)
|
||||
self._tasks_by_id = NestedCacheItem(
|
||||
levels=2, default_factory=dict, lifetime=self.lifetime)
|
||||
|
||||
self._folders_refreshing = set()
|
||||
self._tasks_refreshing = set()
|
||||
|
|
|
|||
|
|
@ -56,11 +56,21 @@ class FoldersModel(QtGui.QStandardItemModel):
|
|||
|
||||
return self._has_content
|
||||
|
||||
def clear(self):
|
||||
def refresh(self):
|
||||
"""Refresh folders for last selected project.
|
||||
|
||||
Force to update folders model from controller. This may or may not
|
||||
trigger query from server, that's based on controller's cache.
|
||||
"""
|
||||
|
||||
self.set_project_name(self._last_project_name)
|
||||
|
||||
def _clear_items(self):
|
||||
self._items_by_id = {}
|
||||
self._parent_id_by_id = {}
|
||||
self._has_content = False
|
||||
super(FoldersModel, self).clear()
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.removeRows(0, root_item.rowCount())
|
||||
|
||||
def get_index_by_id(self, item_id):
|
||||
"""Get index by folder id.
|
||||
|
|
@ -90,7 +100,7 @@ class FoldersModel(QtGui.QStandardItemModel):
|
|||
self._is_refreshing = True
|
||||
|
||||
if self._last_project_name != project_name:
|
||||
self.clear()
|
||||
self._clear_items()
|
||||
self._last_project_name = project_name
|
||||
|
||||
thread = self._refresh_threads.get(project_name)
|
||||
|
|
@ -135,7 +145,7 @@ class FoldersModel(QtGui.QStandardItemModel):
|
|||
def _fill_items(self, folder_items_by_id):
|
||||
if not folder_items_by_id:
|
||||
if folder_items_by_id is not None:
|
||||
self.clear()
|
||||
self._clear_items()
|
||||
self._is_refreshing = False
|
||||
self.refreshed.emit()
|
||||
return
|
||||
|
|
@ -247,6 +257,7 @@ class FoldersWidget(QtWidgets.QWidget):
|
|||
folders_model = FoldersModel(controller)
|
||||
folders_proxy_model = RecursiveSortFilterProxyModel()
|
||||
folders_proxy_model.setSourceModel(folders_model)
|
||||
folders_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
|
||||
folders_view.setModel(folders_proxy_model)
|
||||
|
||||
|
|
@ -293,6 +304,14 @@ class FoldersWidget(QtWidgets.QWidget):
|
|||
|
||||
self._folders_proxy_model.setFilterFixedString(name)
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh folders model.
|
||||
|
||||
Force to update folders model from controller.
|
||||
"""
|
||||
|
||||
self._folders_model.refresh()
|
||||
|
||||
def _on_project_selection_change(self, event):
|
||||
project_name = event["project_name"]
|
||||
self._set_project_name(project_name)
|
||||
|
|
@ -300,9 +319,6 @@ class FoldersWidget(QtWidgets.QWidget):
|
|||
def _set_project_name(self, project_name):
|
||||
self._folders_model.set_project_name(project_name)
|
||||
|
||||
def _clear(self):
|
||||
self._folders_model.clear()
|
||||
|
||||
def _on_folders_refresh_finished(self, event):
|
||||
if event["sender"] != SENDER_NAME:
|
||||
self._set_project_name(event["project_name"])
|
||||
|
|
|
|||
|
|
@ -44,14 +44,20 @@ class TasksModel(QtGui.QStandardItemModel):
|
|||
# Initial state
|
||||
self._add_invalid_selection_item()
|
||||
|
||||
def clear(self):
|
||||
def _clear_items(self):
|
||||
self._items_by_name = {}
|
||||
self._has_content = False
|
||||
self._remove_invalid_items()
|
||||
super(TasksModel, self).clear()
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.removeRows(0, root_item.rowCount())
|
||||
|
||||
def refresh(self, project_name, folder_id):
|
||||
"""Refresh tasks for folder.
|
||||
def refresh(self):
|
||||
"""Refresh tasks for last project and folder."""
|
||||
|
||||
self._refresh(self._last_project_name, self._last_folder_id)
|
||||
|
||||
def set_context(self, project_name, folder_id):
|
||||
"""Set context for which should be tasks showed.
|
||||
|
||||
Args:
|
||||
project_name (Union[str]): Name of project.
|
||||
|
|
@ -121,7 +127,7 @@ class TasksModel(QtGui.QStandardItemModel):
|
|||
return self._empty_tasks_item
|
||||
|
||||
def _add_invalid_item(self, item):
|
||||
self.clear()
|
||||
self._clear_items()
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.appendRow(item)
|
||||
|
||||
|
|
@ -299,6 +305,7 @@ class TasksWidget(QtWidgets.QWidget):
|
|||
tasks_model = TasksModel(controller)
|
||||
tasks_proxy_model = QtCore.QSortFilterProxyModel()
|
||||
tasks_proxy_model.setSourceModel(tasks_model)
|
||||
tasks_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
|
||||
tasks_view.setModel(tasks_proxy_model)
|
||||
|
||||
|
|
@ -334,8 +341,14 @@ class TasksWidget(QtWidgets.QWidget):
|
|||
self._handle_expected_selection = handle_expected_selection
|
||||
self._expected_selection_data = None
|
||||
|
||||
def _clear(self):
|
||||
self._tasks_model.clear()
|
||||
def refresh(self):
|
||||
"""Refresh folders for last selected project.
|
||||
|
||||
Force to update folders model from controller. This may or may not
|
||||
trigger query from server, that's based on controller's cache.
|
||||
"""
|
||||
|
||||
self._tasks_model.refresh()
|
||||
|
||||
def _on_tasks_refresh_finished(self, event):
|
||||
"""Tasks were refreshed in controller.
|
||||
|
|
@ -353,13 +366,13 @@ class TasksWidget(QtWidgets.QWidget):
|
|||
or event["folder_id"] != self._selected_folder_id
|
||||
):
|
||||
return
|
||||
self._tasks_model.refresh(
|
||||
self._tasks_model.set_context(
|
||||
event["project_name"], self._selected_folder_id
|
||||
)
|
||||
|
||||
def _folder_selection_changed(self, event):
|
||||
self._selected_folder_id = event["folder_id"]
|
||||
self._tasks_model.refresh(
|
||||
self._tasks_model.set_context(
|
||||
event["project_name"], self._selected_folder_id
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -237,6 +237,7 @@
|
|||
},
|
||||
{
|
||||
"name": "13-0",
|
||||
"label": "13.0",
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
|
|
@ -319,6 +320,7 @@
|
|||
},
|
||||
{
|
||||
"name": "13-0",
|
||||
"label": "13.0",
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
|
|
@ -405,6 +407,7 @@
|
|||
},
|
||||
{
|
||||
"name": "13-0",
|
||||
"label": "13.0",
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
|
|
@ -491,6 +494,7 @@
|
|||
},
|
||||
{
|
||||
"name": "13-0",
|
||||
"label": "13.0",
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
|
|
@ -577,6 +581,7 @@
|
|||
},
|
||||
{
|
||||
"name": "13-0",
|
||||
"label": "13.0",
|
||||
"use_python_2": false,
|
||||
"executables": {
|
||||
"windows": [
|
||||
|
|
|
|||
|
|
@ -124,6 +124,24 @@ class LimitGroupsSubmodel(BaseSettingsModel):
|
|||
)
|
||||
|
||||
|
||||
def fusion_deadline_plugin_enum():
|
||||
"""Return a list of value/label dicts for the enumerator.
|
||||
|
||||
Returning a list of dicts is used to allow for a custom label to be
|
||||
displayed in the UI.
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"value": "Fusion",
|
||||
"label": "Fusion"
|
||||
},
|
||||
{
|
||||
"value": "FusionCmd",
|
||||
"label": "FusionCmd"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
class FusionSubmitDeadlineModel(BaseSettingsModel):
|
||||
enabled: bool = Field(True, title="Enabled")
|
||||
optional: bool = Field(False, title="Optional")
|
||||
|
|
@ -132,6 +150,9 @@ class FusionSubmitDeadlineModel(BaseSettingsModel):
|
|||
chunk_size: int = Field(10, title="Frame per Task")
|
||||
concurrent_tasks: int = Field(1, title="Number of concurrent tasks")
|
||||
group: str = Field("", title="Group Name")
|
||||
plugin: str = Field("Fusion",
|
||||
enum_resolver=fusion_deadline_plugin_enum,
|
||||
title="Deadline Plugin")
|
||||
|
||||
|
||||
class NukeSubmitDeadlineModel(BaseSettingsModel):
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
__version__ = "0.1.1"
|
||||
__version__ = "0.1.2"
|
||||
|
|
|
|||
45
server_addon/houdini/server/settings/general.py
Normal file
45
server_addon/houdini/server/settings/general.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
from pydantic import Field
|
||||
from ayon_server.settings import BaseSettingsModel
|
||||
|
||||
|
||||
class HoudiniVarModel(BaseSettingsModel):
|
||||
_layout = "expanded"
|
||||
var: str = Field("", title="Var")
|
||||
value: str = Field("", title="Value")
|
||||
is_directory: bool = Field(False, title="Treat as directory")
|
||||
|
||||
|
||||
class UpdateHoudiniVarcontextModel(BaseSettingsModel):
|
||||
"""Sync vars with context changes.
|
||||
|
||||
If a value is treated as a directory on update
|
||||
it will be ensured the folder exists.
|
||||
"""
|
||||
|
||||
enabled: bool = Field(title="Enabled")
|
||||
# TODO this was dynamic dictionary '{var: path}'
|
||||
houdini_vars: list[HoudiniVarModel] = Field(
|
||||
default_factory=list,
|
||||
title="Houdini Vars"
|
||||
)
|
||||
|
||||
|
||||
class GeneralSettingsModel(BaseSettingsModel):
|
||||
update_houdini_var_context: UpdateHoudiniVarcontextModel = Field(
|
||||
default_factory=UpdateHoudiniVarcontextModel,
|
||||
title="Update Houdini Vars on context change"
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_GENERAL_SETTINGS = {
|
||||
"update_houdini_var_context": {
|
||||
"enabled": True,
|
||||
"houdini_vars": [
|
||||
{
|
||||
"var": "JOB",
|
||||
"value": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}", # noqa
|
||||
"is_directory": True
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,10 @@ from ayon_server.settings import (
|
|||
MultiplatformPathModel,
|
||||
MultiplatformPathListModel,
|
||||
)
|
||||
|
||||
from .general import (
|
||||
GeneralSettingsModel,
|
||||
DEFAULT_GENERAL_SETTINGS
|
||||
)
|
||||
from .imageio import HoudiniImageIOModel
|
||||
from .publish_plugins import (
|
||||
PublishPluginsModel,
|
||||
|
|
@ -52,6 +55,10 @@ class ShelvesModel(BaseSettingsModel):
|
|||
|
||||
|
||||
class HoudiniSettings(BaseSettingsModel):
|
||||
general: GeneralSettingsModel = Field(
|
||||
default_factory=GeneralSettingsModel,
|
||||
title="General"
|
||||
)
|
||||
imageio: HoudiniImageIOModel = Field(
|
||||
default_factory=HoudiniImageIOModel,
|
||||
title="Color Management (ImageIO)"
|
||||
|
|
@ -73,6 +80,7 @@ class HoudiniSettings(BaseSettingsModel):
|
|||
|
||||
|
||||
DEFAULT_VALUES = {
|
||||
"general": DEFAULT_GENERAL_SETTINGS,
|
||||
"shelves": [],
|
||||
"create": DEFAULT_HOUDINI_CREATE_SETTINGS,
|
||||
"publish": DEFAULT_HOUDINI_PUBLISH_SETTINGS
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
__version__ = "0.1.3"
|
||||
__version__ = "0.1.4"
|
||||
|
|
|
|||
|
|
@ -3,9 +3,36 @@ id: admin_hosts_houdini
|
|||
title: Houdini
|
||||
sidebar_label: Houdini
|
||||
---
|
||||
## General Settings
|
||||
### Houdini Vars
|
||||
|
||||
Allows admins to have a list of vars (e.g. JOB) with (dynamic) values that will be updated on context changes, e.g. when switching to another asset or task.
|
||||
|
||||
Using template keys is supported but formatting keys capitalization variants is not, e.g. `{Asset}` and `{ASSET}` won't work
|
||||
|
||||
|
||||
:::note
|
||||
If `Treat as directory` toggle is activated, Openpype will consider the given value is a path of a folder.
|
||||
|
||||
If the folder does not exist on the context change it will be created by this feature so that the path will always try to point to an existing folder.
|
||||
:::
|
||||
|
||||
Disabling `Update Houdini vars on context change` feature will leave all Houdini vars unmanaged and thus no context update changes will occur.
|
||||
|
||||
> If `$JOB` is present in the Houdini var list and has an empty value, OpenPype will set its value to `$HIP`
|
||||
|
||||
|
||||
:::note
|
||||
For consistency reasons we always force all vars to be uppercase.
|
||||
e.g. `myvar` will be `MYVAR`
|
||||
:::
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## Shelves Manager
|
||||
You can add your custom shelf set into Houdini by setting your shelf sets, shelves and tools in **Houdini -> Shelves Manager**.
|
||||

|
||||
|
||||
The Shelf Set Path is used to load a .shelf file to generate your shelf set. If the path is specified, you don't have to set the shelves and tools.
|
||||
The Shelf Set Path is used to load a .shelf file to generate your shelf set. If the path is specified, you don't have to set the shelves and tools.
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Loading…
Add table
Add a link
Reference in a new issue