diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index e3ca8262e5..78bea3d838 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -35,6 +35,7 @@ body:
label: Version
description: What version are you running? Look to OpenPype Tray
options:
+ - 3.17.2-nightly.3
- 3.17.2-nightly.2
- 3.17.2-nightly.1
- 3.17.1
@@ -134,7 +135,6 @@ body:
- 3.14.10
- 3.14.10-nightly.9
- 3.14.10-nightly.8
- - 3.14.10-nightly.7
validations:
required: true
- type: dropdown
diff --git a/openpype/hosts/blender/hooks/pre_pyside_install.py b/openpype/hosts/blender/hooks/pre_pyside_install.py
index 777e383215..2aa3a5e49a 100644
--- a/openpype/hosts/blender/hooks/pre_pyside_install.py
+++ b/openpype/hosts/blender/hooks/pre_pyside_install.py
@@ -31,7 +31,7 @@ class InstallPySideToBlender(PreLaunchHook):
def inner_execute(self):
# Get blender's python directory
- version_regex = re.compile(r"^[2-3]\.[0-9]+$")
+ version_regex = re.compile(r"^[2-4]\.[0-9]+$")
platform = system().lower()
executable = self.launch_context.executable.executable_path
diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py
index a3f691e1fc..3db18ca69a 100644
--- a/openpype/hosts/houdini/api/lib.py
+++ b/openpype/hosts/houdini/api/lib.py
@@ -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()
diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py
index 6aa65deb89..f8db45c56b 100644
--- a/openpype/hosts/houdini/api/pipeline.py
+++ b/openpype/hosts/houdini/api/pipeline.py
@@ -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):
diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml
index 5818a117eb..b2e32a70f9 100644
--- a/openpype/hosts/houdini/startup/MainMenuCommon.xml
+++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml
@@ -86,6 +86,14 @@ openpype.hosts.houdini.api.lib.reset_framerange()
]]>
+
+
+
+
+
diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py b/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py
index da060e3157..9730e3b61f 100644
--- a/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py
+++ b/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py
@@ -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"]
diff --git a/openpype/hosts/resolve/api/__init__.py b/openpype/hosts/resolve/api/__init__.py
index 2b4546f8d6..dba275e6c4 100644
--- a/openpype/hosts/resolve/api/__init__.py
+++ b/openpype/hosts/resolve/api/__init__.py
@@ -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",
diff --git a/openpype/hosts/resolve/api/menu.py b/openpype/hosts/resolve/api/menu.py
index b3717e01ea..34a63eb89f 100644
--- a/openpype/hosts/resolve/api/menu.py
+++ b/openpype/hosts/resolve/api/menu.py
@@ -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")
diff --git a/openpype/hosts/resolve/api/pipeline.py b/openpype/hosts/resolve/api/pipeline.py
index 899cb825bb..05f556fa5b 100644
--- a/openpype/hosts/resolve/api/pipeline.py
+++ b/openpype/hosts/resolve/api/pipeline.py
@@ -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
diff --git a/openpype/hosts/resolve/api/utils.py b/openpype/hosts/resolve/api/utils.py
index 871b3af38d..851851a3b3 100644
--- a/openpype/hosts/resolve/api/utils.py
+++ b/openpype/hosts/resolve/api/utils.py
@@ -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}"))
diff --git a/openpype/hosts/resolve/startup.py b/openpype/hosts/resolve/startup.py
index e807a48f5a..5ac3c99524 100644
--- a/openpype/hosts/resolve/startup.py
+++ b/openpype/hosts/resolve/startup.py
@@ -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..")
diff --git a/openpype/hosts/resolve/utility_scripts/OpenPype__Menu.py b/openpype/hosts/resolve/utility_scripts/OpenPype__Menu.py
index 1087a7b7a0..4f14927074 100644
--- a/openpype/hosts/resolve/utility_scripts/OpenPype__Menu.py
+++ b/openpype/hosts/resolve/utility_scripts/OpenPype__Menu.py
@@ -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__":
diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py
index 70aa12956d..0b97582d2a 100644
--- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py
+++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py
@@ -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,
diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py
index f567118062..13630ae7ca 100644
--- a/openpype/pipeline/context_tools.py
+++ b/openpype/pipeline/context_tools.py
@@ -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
diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py
index 7adebbbc97..071ecfffd2 100644
--- a/openpype/pype_commands.py
+++ b/openpype/pype_commands.py
@@ -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
diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py
index 68693bb953..d54d71e851 100644
--- a/openpype/settings/ayon_settings.py
+++ b/openpype/settings/ayon_settings.py
@@ -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"]
diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json
index 9e88f3b6f2..2c5e0dc65d 100644
--- a/openpype/settings/defaults/project_settings/deadline.json
+++ b/openpype/settings/defaults/project_settings/deadline.json
@@ -52,7 +52,8 @@
"priority": 50,
"chunk_size": 10,
"concurrent_tasks": 1,
- "group": ""
+ "group": "",
+ "plugin": "Fusion"
},
"NukeSubmitDeadline": {
"enabled": true,
diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json
index 5392fc34dd..4f57ee52c6 100644
--- a/openpype/settings/defaults/project_settings/houdini.json
+++ b/openpype/settings/defaults/project_settings/houdini.json
@@ -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": {
diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json
index 596bc30f91..64db852c89 100644
--- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json
+++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json
@@ -289,6 +289,15 @@
"type": "text",
"key": "group",
"label": "Group Name"
+ },
+ {
+ "type": "enum",
+ "key": "plugin",
+ "label": "Deadline Plugin",
+ "enum_items": [
+ {"Fusion": "Fusion"},
+ {"FusionCmd": "FusionCmd"}
+ ]
}
]
},
diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json
index 7f782e3647..d4d0565ec9 100644
--- a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json
+++ b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json
@@ -5,6 +5,10 @@
"label": "Houdini",
"is_file": true,
"children": [
+ {
+ "type": "schema",
+ "name": "schema_houdini_general"
+ },
{
"key": "imageio",
"type": "dict",
diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json
new file mode 100644
index 0000000000..de1a0396ec
--- /dev/null
+++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json
@@ -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.
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"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/openpype/tools/ayon_launcher/abstract.py b/openpype/tools/ayon_launcher/abstract.py
index 00502fe930..95fe2b2c8d 100644
--- a/openpype/tools/ayon_launcher/abstract.py
+++ b/openpype/tools/ayon_launcher/abstract.py
@@ -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
diff --git a/openpype/tools/ayon_launcher/control.py b/openpype/tools/ayon_launcher/control.py
index 09e07893c3..36c0536422 100644
--- a/openpype/tools/ayon_launcher/control.py
+++ b/openpype/tools/ayon_launcher/control.py
@@ -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")
diff --git a/openpype/tools/ayon_launcher/models/actions.py b/openpype/tools/ayon_launcher/models/actions.py
index 24fea44db2..93ec115734 100644
--- a/openpype/tools/ayon_launcher/models/actions.py
+++ b/openpype/tools/ayon_launcher/models/actions.py
@@ -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)
diff --git a/openpype/tools/ayon_launcher/ui/actions_widget.py b/openpype/tools/ayon_launcher/ui/actions_widget.py
index d04f8f8d24..2a1a06695d 100644
--- a/openpype/tools/ayon_launcher/ui/actions_widget.py
+++ b/openpype/tools/ayon_launcher/ui/actions_widget.py
@@ -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):
diff --git a/openpype/tools/ayon_launcher/ui/hierarchy_page.py b/openpype/tools/ayon_launcher/ui/hierarchy_page.py
index 5047cdc692..8c546b38ac 100644
--- a/openpype/tools/ayon_launcher/ui/hierarchy_page.py
+++ b/openpype/tools/ayon_launcher/ui/hierarchy_page.py
@@ -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)
diff --git a/openpype/tools/ayon_launcher/ui/projects_widget.py b/openpype/tools/ayon_launcher/ui/projects_widget.py
index baa399d0ed..7dbaec5147 100644
--- a/openpype/tools/ayon_launcher/ui/projects_widget.py
+++ b/openpype/tools/ayon_launcher/ui/projects_widget.py
@@ -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)
diff --git a/openpype/tools/ayon_launcher/ui/window.py b/openpype/tools/ayon_launcher/ui/window.py
index 139da42a2e..ffc74a2fdc 100644
--- a/openpype/tools/ayon_launcher/ui/window.py
+++ b/openpype/tools/ayon_launcher/ui/window.py
@@ -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"]))
diff --git a/openpype/tools/ayon_utils/models/hierarchy.py b/openpype/tools/ayon_utils/models/hierarchy.py
index 8e01c557c5..93f4c48d98 100644
--- a/openpype/tools/ayon_utils/models/hierarchy.py
+++ b/openpype/tools/ayon_utils/models/hierarchy.py
@@ -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()
diff --git a/openpype/tools/ayon_utils/widgets/folders_widget.py b/openpype/tools/ayon_utils/widgets/folders_widget.py
index 3fab64f657..4f44881081 100644
--- a/openpype/tools/ayon_utils/widgets/folders_widget.py
+++ b/openpype/tools/ayon_utils/widgets/folders_widget.py
@@ -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"])
diff --git a/openpype/tools/ayon_utils/widgets/tasks_widget.py b/openpype/tools/ayon_utils/widgets/tasks_widget.py
index 66ebd0b777..0af506863a 100644
--- a/openpype/tools/ayon_utils/widgets/tasks_widget.py
+++ b/openpype/tools/ayon_utils/widgets/tasks_widget.py
@@ -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
)
diff --git a/openpype/version.py b/openpype/version.py
index 399c1404b1..01c000e54d 100644
--- a/openpype/version.py
+++ b/openpype/version.py
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
-__version__ = "3.17.2-nightly.2"
+__version__ = "3.17.2-nightly.3"
diff --git a/server_addon/applications/server/applications.json b/server_addon/applications/server/applications.json
index 8e5b28623e..e40b8d41f6 100644
--- a/server_addon/applications/server/applications.json
+++ b/server_addon/applications/server/applications.json
@@ -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": [
diff --git a/server_addon/deadline/server/settings/publish_plugins.py b/server_addon/deadline/server/settings/publish_plugins.py
index 32a5d0e353..8d48695a9c 100644
--- a/server_addon/deadline/server/settings/publish_plugins.py
+++ b/server_addon/deadline/server/settings/publish_plugins.py
@@ -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):
diff --git a/server_addon/deadline/server/version.py b/server_addon/deadline/server/version.py
index 485f44ac21..b3f4756216 100644
--- a/server_addon/deadline/server/version.py
+++ b/server_addon/deadline/server/version.py
@@ -1 +1 @@
-__version__ = "0.1.1"
+__version__ = "0.1.2"
diff --git a/server_addon/houdini/server/settings/general.py b/server_addon/houdini/server/settings/general.py
new file mode 100644
index 0000000000..21cc4c452c
--- /dev/null
+++ b/server_addon/houdini/server/settings/general.py
@@ -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
+ }
+ ]
+ }
+}
diff --git a/server_addon/houdini/server/settings/main.py b/server_addon/houdini/server/settings/main.py
index fdb6838f5c..0c2e160c87 100644
--- a/server_addon/houdini/server/settings/main.py
+++ b/server_addon/houdini/server/settings/main.py
@@ -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
diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py
index ae7362549b..bbab0242f6 100644
--- a/server_addon/houdini/server/version.py
+++ b/server_addon/houdini/server/version.py
@@ -1 +1 @@
-__version__ = "0.1.3"
+__version__ = "0.1.4"
diff --git a/website/docs/admin_hosts_houdini.md b/website/docs/admin_hosts_houdini.md
index 64c54db591..18c390e07f 100644
--- a/website/docs/admin_hosts_houdini.md
+++ b/website/docs/admin_hosts_houdini.md
@@ -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.
\ No newline at end of file
+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.
diff --git a/website/docs/assets/houdini/update-houdini-vars-context-change.png b/website/docs/assets/houdini/update-houdini-vars-context-change.png
new file mode 100644
index 0000000000..74ac8d86c9
Binary files /dev/null and b/website/docs/assets/houdini/update-houdini-vars-context-change.png differ