From b6e9806258086f260c604ee1ad5a932d93e85b70 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 23 Aug 2023 22:39:13 +0200 Subject: [PATCH 01/63] Allow duplicating publish instance by defining `instance_node` and `instance_id` from `node.path()` instead of parms. --- openpype/hosts/houdini/api/plugin.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 70c837205e..c3fd313a0b 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -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. From 5ff66afff7b91b1d1583be28c0da99cc088a3300 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 23 Aug 2023 22:40:25 +0200 Subject: [PATCH 02/63] Allow duplicating publish instances in Maya by not storin instance id as an attribute but using the (unique) node's name instead. --- openpype/hosts/maya/api/plugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 00d6602ef9..00a2c899a2 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -134,6 +134,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) # We store creator attributes at the root level and assume they # will not clash in names with `subset`, `task`, etc. and other @@ -185,6 +186,7 @@ class MayaCreatorBase(object): # Explicitly re-parse the node name node_data["instance_node"] = node + node_data["instance_id"] = node return node_data From 6da94d4f27be41a974b8f6348c45060510f74032 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 23 Aug 2023 22:41:25 +0200 Subject: [PATCH 03/63] Allow to duplicate publish instances in Fusion, by not relying on `instance_id` data but have the unique identifier be the node's name. --- openpype/hosts/fusion/plugins/create/create_saver.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 04898d0a45..590a678a3d 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -127,6 +127,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 @@ -192,6 +195,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): From a352a6468022cf46febfeaf19c19ba93d5e0d59c Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 25 Sep 2023 20:16:56 +0300 Subject: [PATCH 04/63] add JOB path houdini setting --- .../defaults/project_settings/houdini.json | 6 ++++ .../schema_project_houdini.json | 4 +++ .../schemas/schema_houdini_general.json | 28 +++++++++++++++++++ .../houdini/server/settings/general.py | 22 +++++++++++++++ server_addon/houdini/server/settings/main.py | 10 ++++++- server_addon/houdini/server/version.py | 2 +- 6 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json create mode 100644 server_addon/houdini/server/settings/general.py diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 5392fc34dd..2b7244ac85 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -1,4 +1,10 @@ { + "general": { + "job_path": { + "enabled": true, + "path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" + } + }, "imageio": { "activate_host_color_management": true, "ocio_config": { 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..c275714ac7 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json @@ -0,0 +1,28 @@ +{ + "type": "dict", + "key": "general", + "label": "General", + "collapsible": true, + "is_group": true, + "children": [ + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "job_path", + "label": "JOB Path", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "text", + "key": "path", + "label": "Path" + } + ] + } + ] +} diff --git a/server_addon/houdini/server/settings/general.py b/server_addon/houdini/server/settings/general.py new file mode 100644 index 0000000000..242093deeb --- /dev/null +++ b/server_addon/houdini/server/settings/general.py @@ -0,0 +1,22 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + + +class JobPathModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + path: str = Field(title="Path") + + +class GeneralSettingsModel(BaseSettingsModel): + JobPath: JobPathModel = Field( + default_factory=JobPathModel, + title="JOB Path" + ) + + +DEFAULT_GENERAL_SETTINGS = { + "JobPath": { + "enabled": True, + "path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" + } +} 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" From fef45ceea24754049ab7acbafe0a5bb655eca497 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 25 Sep 2023 20:31:44 +0300 Subject: [PATCH 05/63] implement get_current_context_template_data function --- openpype/pipeline/context_tools.py | 43 +++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index f567118062..13b14f1296 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,41 @@ 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, str] of the following tokens and their values + - app + - user + - asset + - parent + - hierarchy + - folder[name] + - root[work, ...] + - studio[code, name] + - project[code, name] + - task[type, name, short] + """ + + # 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 template data + template_data = get_template_data( + project_doc, asset_doc, task_name, host_name + ) + + template_data["root"] = anatomy.roots + return template_data From 59a20fe0fb77d32af3165927d3b0e0fd1d71be81 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 25 Sep 2023 21:58:11 +0300 Subject: [PATCH 06/63] implement validate_job_path and register it in houdini callbacks --- openpype/hosts/houdini/api/lib.py | 35 +++++++++++++++++++++++++- openpype/hosts/houdini/api/pipeline.py | 6 +++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index a3f691e1fc..bdc8e0e973 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -9,9 +9,14 @@ 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 +) import hou @@ -747,3 +752,31 @@ def get_camera_from_container(container): assert len(cameras) == 1, "Camera instance must have only one camera" return cameras[0] + + +def validate_job_path(): + """Validate job path to ensure it matches the settings.""" + + project_settings = get_current_project_settings() + + if project_settings["houdini"]["general"]["job_path"]["enabled"]: + + # get and resolve job path template + job_path_template = project_settings["houdini"]["general"]["job_path"]["path"] + job_path = StringTemplate.format_template( + job_path_template, get_current_context_template_data() + ) + job_path = job_path.replace("\\","/") + + if job_path == "": + # Set JOB path to HIP path if JOB path is enabled + # and has empty value. + job_path = os.environ["HIP"] + + current_job = hou.hscript("echo -n `$JOB`")[0] + if current_job != job_path: + hou.hscript("set JOB=" + job_path) + os.environ["JOB"] = job_path + print(" - set $JOB to " + job_path) + else: + print(" - JOB Path is disabled, Skipping Check...") diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index 6aa65deb89..48cc9e2150 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..") + # Validate $JOB value + lib.validate_job_path() + 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..") + # Validate $JOB value + lib.validate_job_path() + # Validate FPS after update_task_from_path to # ensure it is using correct FPS for the asset lib.validate_fps() From fdea715fe0d7aafd7f3000b8aca7780d432aeacb Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 25 Sep 2023 22:23:41 +0300 Subject: [PATCH 07/63] resolve hound --- openpype/hosts/houdini/api/lib.py | 5 +++-- server_addon/houdini/server/settings/general.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index bdc8e0e973..876d39b757 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -762,11 +762,12 @@ def validate_job_path(): if project_settings["houdini"]["general"]["job_path"]["enabled"]: # get and resolve job path template - job_path_template = project_settings["houdini"]["general"]["job_path"]["path"] + job_path_template = \ + project_settings["houdini"]["general"]["job_path"]["path"] job_path = StringTemplate.format_template( job_path_template, get_current_context_template_data() ) - job_path = job_path.replace("\\","/") + job_path = job_path.replace("\\", "/") if job_path == "": # Set JOB path to HIP path if JOB path is enabled diff --git a/server_addon/houdini/server/settings/general.py b/server_addon/houdini/server/settings/general.py index 242093deeb..f5fed1c248 100644 --- a/server_addon/houdini/server/settings/general.py +++ b/server_addon/houdini/server/settings/general.py @@ -17,6 +17,6 @@ class GeneralSettingsModel(BaseSettingsModel): DEFAULT_GENERAL_SETTINGS = { "JobPath": { "enabled": True, - "path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" + "path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" # noqa } } From d1395fe4099bc98dd3bbaf016029fc5d480d0a3c Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 26 Sep 2023 16:22:18 +0300 Subject: [PATCH 08/63] update settings names --- openpype/hosts/houdini/api/lib.py | 6 +++--- .../defaults/project_settings/houdini.json | 4 ++-- .../schemas/schema_houdini_general.json | 8 ++++---- server_addon/houdini/server/settings/general.py | 14 +++++++------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 876d39b757..73a6f452d0 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -758,12 +758,12 @@ def validate_job_path(): """Validate job path to ensure it matches the settings.""" project_settings = get_current_project_settings() + project_settings = project_settings["houdini"]["general"]["update_job_var_context"] - if project_settings["houdini"]["general"]["job_path"]["enabled"]: + if project_settings["enabled"]: # get and resolve job path template - job_path_template = \ - project_settings["houdini"]["general"]["job_path"]["path"] + job_path_template = project_settings["job_path"] job_path = StringTemplate.format_template( job_path_template, get_current_context_template_data() ) diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 2b7244ac85..5057db1f03 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -1,8 +1,8 @@ { "general": { - "job_path": { + "update_job_var_context": { "enabled": true, - "path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" + "job_path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" } }, "imageio": { 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 index c275714ac7..eecc29592a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json @@ -9,8 +9,8 @@ "type": "dict", "collapsible": true, "checkbox_key": "enabled", - "key": "job_path", - "label": "JOB Path", + "key": "update_job_var_context", + "label": "Update $JOB on context change", "children": [ { "type": "boolean", @@ -19,8 +19,8 @@ }, { "type": "text", - "key": "path", - "label": "Path" + "key": "job_path", + "label": "JOB Path" } ] } diff --git a/server_addon/houdini/server/settings/general.py b/server_addon/houdini/server/settings/general.py index f5fed1c248..f47fa9c564 100644 --- a/server_addon/houdini/server/settings/general.py +++ b/server_addon/houdini/server/settings/general.py @@ -2,21 +2,21 @@ from pydantic import Field from ayon_server.settings import BaseSettingsModel -class JobPathModel(BaseSettingsModel): +class UpdateJobVarcontextModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") - path: str = Field(title="Path") + job_path: str = Field(title="JOB Path") class GeneralSettingsModel(BaseSettingsModel): - JobPath: JobPathModel = Field( - default_factory=JobPathModel, - title="JOB Path" + update_job_var_context: UpdateJobVarcontextModel = Field( + default_factory=UpdateJobVarcontextModel, + title="Update $JOB on context change" ) DEFAULT_GENERAL_SETTINGS = { - "JobPath": { + "update_job_var_context": { "enabled": True, - "path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" # noqa + "job_path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" # noqa } } From 30e2ecb8595213542626b51f8514101054d10fef Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 26 Sep 2023 16:26:14 +0300 Subject: [PATCH 09/63] BigRoy's comment --- openpype/hosts/houdini/api/lib.py | 7 +++---- openpype/hosts/houdini/api/pipeline.py | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 73a6f452d0..8624f09289 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -754,11 +754,12 @@ def get_camera_from_container(container): return cameras[0] -def validate_job_path(): +def update_job_var_context(): """Validate job path to ensure it matches the settings.""" project_settings = get_current_project_settings() - project_settings = project_settings["houdini"]["general"]["update_job_var_context"] + project_settings = \ + project_settings["houdini"]["general"]["update_job_var_context"] if project_settings["enabled"]: @@ -779,5 +780,3 @@ def validate_job_path(): hou.hscript("set JOB=" + job_path) os.environ["JOB"] = job_path print(" - set $JOB to " + job_path) - else: - print(" - JOB Path is disabled, Skipping Check...") diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index 48cc9e2150..3efbbb12b3 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -301,7 +301,7 @@ def on_save(): log.info("Running callback on save..") # Validate $JOB value - lib.validate_job_path() + lib.update_job_var_context() nodes = lib.get_id_required_nodes() for node, new_id in lib.generate_ids(nodes): @@ -339,7 +339,7 @@ def on_open(): log.info("Running callback on open..") # Validate $JOB value - lib.validate_job_path() + lib.update_job_var_context() # Validate FPS after update_task_from_path to # ensure it is using correct FPS for the asset From fd8daebed9bbb579c13f193253c00a5d7cf22005 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 26 Sep 2023 20:25:51 +0300 Subject: [PATCH 10/63] update log message --- openpype/hosts/houdini/api/lib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 8624f09289..c8211f45d2 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -779,4 +779,5 @@ def update_job_var_context(): if current_job != job_path: hou.hscript("set JOB=" + job_path) os.environ["JOB"] = job_path - print(" - set $JOB to " + job_path) + print(" - Context changed, update $JOB respectively to " + + job_path) From a349733ecf418f353f940969aea1ab0a6e1aaff8 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 26 Sep 2023 20:38:05 +0300 Subject: [PATCH 11/63] sync $JOB and [JOB] --- openpype/hosts/houdini/api/lib.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index c8211f45d2..ac28163144 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -776,6 +776,12 @@ def update_job_var_context(): job_path = os.environ["HIP"] current_job = hou.hscript("echo -n `$JOB`")[0] + + # sync both environment variables. + # because when opening new file $JOB is overridden with + # the value saved in the HIP file but os.environ["JOB"] is not! + os.environ["JOB"] = current_job + if current_job != job_path: hou.hscript("set JOB=" + job_path) os.environ["JOB"] = job_path From f15dcb30b87daf23b3c0a087237cf50e7b26ddf8 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 27 Sep 2023 12:54:02 +0300 Subject: [PATCH 12/63] create JOB folder if not exists --- openpype/hosts/houdini/api/lib.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index ac28163144..9fe5ac83ce 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -785,5 +785,8 @@ def update_job_var_context(): if current_job != job_path: hou.hscript("set JOB=" + job_path) os.environ["JOB"] = job_path + + os.makedirs(job_path, exist_ok=True) + print(" - Context changed, update $JOB respectively to " + job_path) From edcaa8b62f0be86425da063efe55bc0377fa8c8c Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 27 Sep 2023 16:02:38 +0300 Subject: [PATCH 13/63] update docs --- website/docs/admin_hosts_houdini.md | 19 +++++++++++++++++- .../houdini/update-job-context-change.png | Bin 0 -> 8068 bytes 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 website/docs/assets/houdini/update-job-context-change.png diff --git a/website/docs/admin_hosts_houdini.md b/website/docs/admin_hosts_houdini.md index 64c54db591..1e82dd97dd 100644 --- a/website/docs/admin_hosts_houdini.md +++ b/website/docs/admin_hosts_houdini.md @@ -3,9 +3,26 @@ id: admin_hosts_houdini title: Houdini sidebar_label: Houdini --- +## General Settings +### JOB Path +you can add your studios preffered JOB Path, JOB value will be checked and updated on file save and open. +Disableing this option will effectivly turn off this feature. + +JOB Path can be: +- Arbitrary hardcoded path +- Openpype template path + > This allows dynamic values for assets or shots.
+ > Using template keys is supported but formatting keys capitalization variants is not, + > e.g. {Asset} and {ASSET} won't work +- empty + > In this case, JOB will be synced to HIP + +![update job on context change](assets/houdini/update-job-context-change.png) + + ## Shelves Manager You can add your custom shelf set into Houdini by setting your shelf sets, shelves and tools in **Houdini -> Shelves Manager**. ![Custom menu definition](assets/houdini-admin_shelvesmanager.png) -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-job-context-change.png b/website/docs/assets/houdini/update-job-context-change.png new file mode 100644 index 0000000000000000000000000000000000000000..0faf317a227291024d95d7a60b9f1572ec57995a GIT binary patch literal 8068 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 literal 0 HcmV?d00001 From ef785e75e39d581a86cbcec76f151cd26cde6ebd Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 27 Sep 2023 16:31:47 +0300 Subject: [PATCH 14/63] update os.makedirs --- openpype/hosts/houdini/api/lib.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 9fe5ac83ce..abc6d5d0f5 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 @@ -786,7 +787,14 @@ def update_job_var_context(): hou.hscript("set JOB=" + job_path) os.environ["JOB"] = job_path - os.makedirs(job_path, exist_ok=True) + try: + os.makedirs(job_path) + except OSError as e: + if e.errno != errno.EEXIST: + print( + " - Failed to create JOB dir. Maybe due to " + "insufficient permissions." + ) print(" - Context changed, update $JOB respectively to " + job_path) From a3cb6c445684c5859721a2cdf5961061705d906f Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 27 Sep 2023 16:33:06 +0300 Subject: [PATCH 15/63] resolve hound --- openpype/hosts/houdini/api/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index abc6d5d0f5..1b04fd692a 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -792,8 +792,8 @@ def update_job_var_context(): except OSError as e: if e.errno != errno.EEXIST: print( - " - Failed to create JOB dir. Maybe due to " - "insufficient permissions." + " - Failed to create JOB dir. Maybe due to " + "insufficient permissions." ) print(" - Context changed, update $JOB respectively to " From 05cef1ed91d1982edd6e0cf8143025f0968371d5 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 27 Sep 2023 16:38:52 +0300 Subject: [PATCH 16/63] Minikiu comments --- openpype/hosts/houdini/api/lib.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 1b04fd692a..1ea71fa2a7 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -759,15 +759,15 @@ def update_job_var_context(): """Validate job path to ensure it matches the settings.""" project_settings = get_current_project_settings() - project_settings = \ + job_var_settings = \ project_settings["houdini"]["general"]["update_job_var_context"] - if project_settings["enabled"]: + if job_var_settings["enabled"]: # get and resolve job path template - job_path_template = project_settings["job_path"] job_path = StringTemplate.format_template( - job_path_template, get_current_context_template_data() + job_var_settings["job_path"], + get_current_context_template_data() ) job_path = job_path.replace("\\", "/") From 260650ea43b7850ead2a2820b02bca05eef8d710 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 27 Sep 2023 16:51:50 +0300 Subject: [PATCH 17/63] update docs --- website/docs/admin_hosts_houdini.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/website/docs/admin_hosts_houdini.md b/website/docs/admin_hosts_houdini.md index 1e82dd97dd..9c9536a26e 100644 --- a/website/docs/admin_hosts_houdini.md +++ b/website/docs/admin_hosts_houdini.md @@ -5,16 +5,18 @@ sidebar_label: Houdini --- ## General Settings ### JOB Path -you can add your studios preffered JOB Path, JOB value will be checked and updated on file save and open. -Disableing this option will effectivly turn off this feature. +Specify a studio-wide `JOB` path.
+The Houdini `$JOB` path can be customized through project settings with a (dynamic) path that will be updated on context changes, e.g. when switching to another asset or task. + +Disabling this feature will leave `$JOB` var unmanaged and thus no context update changes will occur. JOB Path can be: -- Arbitrary hardcoded path +- Arbitrary path - Openpype template path > This allows dynamic values for assets or shots.
> Using template keys is supported but formatting keys capitalization variants is not, > e.g. {Asset} and {ASSET} won't work -- empty +- Empty > In this case, JOB will be synced to HIP ![update job on context change](assets/houdini/update-job-context-change.png) From 346544df3cb84a3db153945d768dc8175a36957e Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 27 Sep 2023 17:23:33 +0300 Subject: [PATCH 18/63] update docs 2 --- website/docs/admin_hosts_houdini.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/docs/admin_hosts_houdini.md b/website/docs/admin_hosts_houdini.md index 9c9536a26e..2d345d2d76 100644 --- a/website/docs/admin_hosts_houdini.md +++ b/website/docs/admin_hosts_houdini.md @@ -5,9 +5,11 @@ sidebar_label: Houdini --- ## General Settings ### JOB Path -Specify a studio-wide `JOB` path.
+ The Houdini `$JOB` path can be customized through project settings with a (dynamic) path that will be updated on context changes, e.g. when switching to another asset or task. +> If the folder does not exist on the context change it will be created by this feature so that `$JOB` will always try to point to an existing folder. + Disabling this feature will leave `$JOB` var unmanaged and thus no context update changes will occur. JOB Path can be: From 67964bec3aadcf8035b966c6a77ab586a1f797c3 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 27 Sep 2023 17:47:08 +0300 Subject: [PATCH 19/63] fix format --- website/docs/admin_hosts_houdini.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/admin_hosts_houdini.md b/website/docs/admin_hosts_houdini.md index 2d345d2d76..75b0922dac 100644 --- a/website/docs/admin_hosts_houdini.md +++ b/website/docs/admin_hosts_houdini.md @@ -17,7 +17,7 @@ JOB Path can be: - Openpype template path > This allows dynamic values for assets or shots.
> Using template keys is supported but formatting keys capitalization variants is not, - > e.g. {Asset} and {ASSET} won't work + > e.g. `{Asset}` and `{ASSET}` won't work - Empty > In this case, JOB will be synced to HIP From 61ce75f0c9e23f9d7f36d5bd2d7fc10fe5143514 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 27 Sep 2023 19:12:56 +0300 Subject: [PATCH 20/63] BigRoy's comment --- openpype/hosts/houdini/api/lib.py | 8 +++++--- website/docs/admin_hosts_houdini.md | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 1ea71fa2a7..5302fbea74 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -756,7 +756,10 @@ def get_camera_from_container(container): def update_job_var_context(): - """Validate job path to ensure it matches the settings.""" + """Update $JOB to match current context. + + This will only do something if the setting is enabled in project settings. + """ project_settings = get_current_project_settings() job_var_settings = \ @@ -796,5 +799,4 @@ def update_job_var_context(): "insufficient permissions." ) - print(" - Context changed, update $JOB respectively to " - + job_path) + print(" - Updated $JOB to {}".format(job_path)) diff --git a/website/docs/admin_hosts_houdini.md b/website/docs/admin_hosts_houdini.md index 75b0922dac..ea7991530b 100644 --- a/website/docs/admin_hosts_houdini.md +++ b/website/docs/admin_hosts_houdini.md @@ -8,7 +8,9 @@ sidebar_label: Houdini The Houdini `$JOB` path can be customized through project settings with a (dynamic) path that will be updated on context changes, e.g. when switching to another asset or task. -> If the folder does not exist on the context change it will be created by this feature so that `$JOB` will always try to point to an existing folder. +:::note +If the folder does not exist on the context change it will be created by this feature so that `$JOB` will always try to point to an existing folder. +::: Disabling this feature will leave `$JOB` var unmanaged and thus no context update changes will occur. From 7197134954f4490dcb84032055d17495e4e189a2 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 28 Sep 2023 00:46:47 +0300 Subject: [PATCH 21/63] Allow adding more Houdini vars --- openpype/hosts/houdini/api/lib.py | 74 +++++++++++-------- openpype/hosts/houdini/api/pipeline.py | 8 +- .../defaults/project_settings/houdini.json | 6 +- .../schemas/schema_houdini_general.json | 15 ++-- .../houdini/server/settings/general.py | 29 ++++++-- 5 files changed, 82 insertions(+), 50 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 5302fbea74..f8d17eef07 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -755,48 +755,58 @@ def get_camera_from_container(container): return cameras[0] -def update_job_var_context(): - """Update $JOB to match current context. +def update_houdini_vars_context(): + """Update Houdini vars to match current context. This will only do something if the setting is enabled in project settings. """ project_settings = get_current_project_settings() - job_var_settings = \ - project_settings["houdini"]["general"]["update_job_var_context"] + houdini_vars_settings = \ + project_settings["houdini"]["general"]["update_houdini_var_context"] - if job_var_settings["enabled"]: + if houdini_vars_settings["enabled"]: + houdini_vars = houdini_vars_settings["houdini_vars"] - # get and resolve job path template - job_path = StringTemplate.format_template( - job_var_settings["job_path"], - get_current_context_template_data() - ) - job_path = job_path.replace("\\", "/") + # Remap AYON settings structure to OpenPype settings structure + # It allows me to use the same logic for both AYON and OpenPype + if isinstance(houdini_vars, list): + items = {} + for item in houdini_vars: + items.update({item["var"]: item["path"]}) - if job_path == "": - # Set JOB path to HIP path if JOB path is enabled - # and has empty value. - job_path = os.environ["HIP"] + houdini_vars = items - current_job = hou.hscript("echo -n `$JOB`")[0] + for var, path in houdini_vars.items(): + # get and resolve job path template + path = StringTemplate.format_template( + path, + get_current_context_template_data() + ) + path = path.replace("\\", "/") - # sync both environment variables. - # because when opening new file $JOB is overridden with - # the value saved in the HIP file but os.environ["JOB"] is not! - os.environ["JOB"] = current_job + if var == "JOB" and path == "": + # sync $JOB to $HIP if $JOB is empty + path = os.environ["HIP"] - if current_job != job_path: - hou.hscript("set JOB=" + job_path) - os.environ["JOB"] = job_path + current_path = hou.hscript("echo -n `${}`".format(var))[0] - try: - os.makedirs(job_path) - except OSError as e: - if e.errno != errno.EEXIST: - print( - " - Failed to create JOB dir. Maybe due to " - "insufficient permissions." - ) + # sync both environment variables. + # because houdini doesn't do that by default + # on opening new files + os.environ[var] = current_path - print(" - Updated $JOB to {}".format(job_path)) + if current_path != path: + hou.hscript("set {}={}".format(var, path)) + os.environ[var] = path + + try: + os.makedirs(path) + except OSError as e: + if e.errno != errno.EEXIST: + print( + " - Failed to create {} dir. Maybe due to " + "insufficient permissions.".format(var) + ) + + print(" - Updated ${} to {}".format(var, path)) diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index 3efbbb12b3..f753d518f0 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -300,8 +300,8 @@ def on_save(): log.info("Running callback on save..") - # Validate $JOB value - lib.update_job_var_context() + # update houdini vars + lib.update_houdini_vars_context() nodes = lib.get_id_required_nodes() for node, new_id in lib.generate_ids(nodes): @@ -338,8 +338,8 @@ def on_open(): log.info("Running callback on open..") - # Validate $JOB value - lib.update_job_var_context() + # update houdini vars + lib.update_houdini_vars_context() # Validate FPS after update_task_from_path to # ensure it is using correct FPS for the asset diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 5057db1f03..b2fcb708cf 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -1,8 +1,10 @@ { "general": { - "update_job_var_context": { + "update_houdini_var_context": { "enabled": true, - "job_path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" + "houdini_vars":{ + "JOB": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" + } } }, "imageio": { 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 index eecc29592a..127382f4bc 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json @@ -9,8 +9,8 @@ "type": "dict", "collapsible": true, "checkbox_key": "enabled", - "key": "update_job_var_context", - "label": "Update $JOB on context change", + "key": "update_houdini_var_context", + "label": "Update Houdini Vars on context change", "children": [ { "type": "boolean", @@ -18,9 +18,14 @@ "label": "Enabled" }, { - "type": "text", - "key": "job_path", - "label": "JOB Path" + "type": "dict-modifiable", + "key": "houdini_vars", + "label": "Houdini Vars", + "collapsible": false, + "object_type": { + "type": "path", + "multiplatform": false + } } ] } diff --git a/server_addon/houdini/server/settings/general.py b/server_addon/houdini/server/settings/general.py index f47fa9c564..42a071a688 100644 --- a/server_addon/houdini/server/settings/general.py +++ b/server_addon/houdini/server/settings/general.py @@ -2,21 +2,36 @@ from pydantic import Field from ayon_server.settings import BaseSettingsModel -class UpdateJobVarcontextModel(BaseSettingsModel): +class HoudiniVarModel(BaseSettingsModel): + _layout = "expanded" + var: str = Field("", title="Var") + path: str = Field(default_factory="", title="Path") + + +class UpdateHoudiniVarcontextModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") - job_path: str = Field(title="JOB Path") + # TODO this was dynamic dictionary '{var: path}' + houdini_vars: list[HoudiniVarModel] = Field( + default_factory=list, + title="Houdini Vars" + ) class GeneralSettingsModel(BaseSettingsModel): - update_job_var_context: UpdateJobVarcontextModel = Field( - default_factory=UpdateJobVarcontextModel, - title="Update $JOB on context change" + update_houdini_var_context: UpdateHoudiniVarcontextModel = Field( + default_factory=UpdateHoudiniVarcontextModel, + title="Update Houdini Vars on context change" ) DEFAULT_GENERAL_SETTINGS = { - "update_job_var_context": { + "update_houdini_var_context": { "enabled": True, - "job_path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" # noqa + "houdini_vars": [ + { + "var": "JOB", + "path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" # noqa + } + ] } } From d8715d59d0d9b095cbd7ba84d72e079ba2e12a4d Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 29 Sep 2023 16:57:05 +0300 Subject: [PATCH 22/63] allow values other than paths --- openpype/hosts/houdini/api/lib.py | 62 ++++++++++--------- openpype/pipeline/context_tools.py | 55 ++++++++++++---- .../defaults/project_settings/houdini.json | 10 ++- .../schemas/schema_houdini_general.json | 22 ++++++- .../houdini/server/settings/general.py | 6 +- 5 files changed, 106 insertions(+), 49 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index f8d17eef07..637339f822 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -768,45 +768,49 @@ def update_houdini_vars_context(): if houdini_vars_settings["enabled"]: houdini_vars = houdini_vars_settings["houdini_vars"] - # Remap AYON settings structure to OpenPype settings structure - # It allows me to use the same logic for both AYON and OpenPype - if isinstance(houdini_vars, list): - items = {} - for item in houdini_vars: - items.update({item["var"]: item["path"]}) + # No vars specified - nothing to do + if not houdini_vars: + return - houdini_vars = items + # 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 + item["var"] = item["var"].upper() - for var, path in houdini_vars.items(): # get and resolve job path template - path = StringTemplate.format_template( - path, - get_current_context_template_data() + item_value = StringTemplate.format_template( + item["value"], + template_data ) - path = path.replace("\\", "/") - if var == "JOB" and path == "": + if item["is_path"]: + item_value = item_value.replace("\\", "/") + try: + os.makedirs(item_value) + except OSError as e: + if e.errno != errno.EEXIST: + print( + " - Failed to create ${} dir. Maybe due to " + "insufficient permissions.".format(item["var"]) + ) + + if item["var"] == "JOB" and item_value == "": # sync $JOB to $HIP if $JOB is empty - path = os.environ["HIP"] + item_value = os.environ["HIP"] - current_path = hou.hscript("echo -n `${}`".format(var))[0] + current_value = hou.hscript("echo -n `${}`".format(item["var"]))[0] # sync both environment variables. # because houdini doesn't do that by default # on opening new files - os.environ[var] = current_path + os.environ[item["var"]] = current_value - if current_path != path: - hou.hscript("set {}={}".format(var, path)) - os.environ[var] = path + if current_value != item_value: + hou.hscript("set {}={}".format(item["var"], item_value)) + os.environ[item["var"]] = item_value - try: - os.makedirs(path) - except OSError as e: - if e.errno != errno.EEXIST: - print( - " - Failed to create {} dir. Maybe due to " - "insufficient permissions.".format(var) - ) - - print(" - Updated ${} to {}".format(var, path)) + print(" - Updated ${} to {}".format(item["var"], item_value)) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 13b14f1296..f98132e270 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -667,17 +667,30 @@ def get_current_context_template_data(): """Template data for template fill from current context Returns: - Dict[str, str] of the following tokens and their values - - app - - user - - asset - - parent - - hierarchy - - folder[name] - - root[work, ...] - - studio[code, name] - - project[code, name] - - task[type, name, short] + 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 @@ -692,10 +705,28 @@ def get_current_context_template_data(): task_name = current_context["task_name"] host_name = get_current_host_name() - # get template data + # 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/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index b2fcb708cf..3c43e7ae29 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -2,9 +2,13 @@ "general": { "update_houdini_var_context": { "enabled": true, - "houdini_vars":{ - "JOB": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" - } + "houdini_vars":[ + { + "var": "JOB", + "value": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}", + "is_path": true + } + ] } }, "imageio": { 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 index 127382f4bc..2989d5c5b9 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json @@ -18,13 +18,29 @@ "label": "Enabled" }, { - "type": "dict-modifiable", + "type": "list", "key": "houdini_vars", "label": "Houdini Vars", "collapsible": false, "object_type": { - "type": "path", - "multiplatform": false + "type": "dict", + "children": [ + { + "type": "text", + "key": "var", + "label": "Var" + }, + { + "type": "text", + "key": "value", + "label": "Value" + }, + { + "type": "boolean", + "key": "is_path", + "label": "isPath" + } + ] } } ] diff --git a/server_addon/houdini/server/settings/general.py b/server_addon/houdini/server/settings/general.py index 42a071a688..468c571993 100644 --- a/server_addon/houdini/server/settings/general.py +++ b/server_addon/houdini/server/settings/general.py @@ -5,7 +5,8 @@ from ayon_server.settings import BaseSettingsModel class HoudiniVarModel(BaseSettingsModel): _layout = "expanded" var: str = Field("", title="Var") - path: str = Field(default_factory="", title="Path") + value: str = Field("", title="Value") + is_path: bool = Field(False, title="isPath") class UpdateHoudiniVarcontextModel(BaseSettingsModel): @@ -30,7 +31,8 @@ DEFAULT_GENERAL_SETTINGS = { "houdini_vars": [ { "var": "JOB", - "path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" # noqa + "value": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}", # noqa + "is_path": True } ] } From b93da3bd3ddacfcc3770ee44032f51ec9184b6a4 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 29 Sep 2023 16:59:07 +0300 Subject: [PATCH 23/63] resolve hound --- openpype/pipeline/context_tools.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index f98132e270..13630ae7ca 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -717,14 +717,12 @@ def get_current_context_template_data(): # compute `frameStartHandle` and `frameEndHandle` if "frameStart" in asset_data and "handleStart" in asset_data: - asset_data["frameStartHandle"] = ( - asset_data["frameStart"] - asset_data["handleStart"] - ) + 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"] - ) + asset_data["frameEndHandle"] = \ + asset_data["frameEnd"] + asset_data["handleEnd"] # add assetData template_data["assetData"] = asset_data From a4d55b420b53e475e1ba05c45504b87c65bdbe46 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 29 Sep 2023 17:25:04 +0300 Subject: [PATCH 24/63] update docs and rename `isPath` to `is Dir Path` --- openpype/hosts/houdini/api/lib.py | 2 +- .../defaults/project_settings/houdini.json | 2 +- .../schemas/schema_houdini_general.json | 4 +-- .../houdini/server/settings/general.py | 4 +-- website/docs/admin_hosts_houdini.md | 24 ++++++++---------- .../update-houdini-vars-context-change.png | Bin 0 -> 18456 bytes .../houdini/update-job-context-change.png | Bin 8068 -> 0 bytes 7 files changed, 17 insertions(+), 19 deletions(-) create mode 100644 website/docs/assets/houdini/update-houdini-vars-context-change.png delete mode 100644 website/docs/assets/houdini/update-job-context-change.png diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 637339f822..291817bbe9 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -787,7 +787,7 @@ def update_houdini_vars_context(): template_data ) - if item["is_path"]: + if item["is_dir_path"]: item_value = item_value.replace("\\", "/") try: os.makedirs(item_value) diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 3c43e7ae29..111ed2b24d 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -6,7 +6,7 @@ { "var": "JOB", "value": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}", - "is_path": true + "is_dir_path": true } ] } 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 index 2989d5c5b9..3160e657bf 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json @@ -37,8 +37,8 @@ }, { "type": "boolean", - "key": "is_path", - "label": "isPath" + "key": "is_dir_path", + "label": "is Dir Path" } ] } diff --git a/server_addon/houdini/server/settings/general.py b/server_addon/houdini/server/settings/general.py index 468c571993..7b3b95f978 100644 --- a/server_addon/houdini/server/settings/general.py +++ b/server_addon/houdini/server/settings/general.py @@ -6,7 +6,7 @@ class HoudiniVarModel(BaseSettingsModel): _layout = "expanded" var: str = Field("", title="Var") value: str = Field("", title="Value") - is_path: bool = Field(False, title="isPath") + is_dir_path: bool = Field(False, title="is Dir Path") class UpdateHoudiniVarcontextModel(BaseSettingsModel): @@ -32,7 +32,7 @@ DEFAULT_GENERAL_SETTINGS = { { "var": "JOB", "value": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}", # noqa - "is_path": True + "is_dir_path": True } ] } diff --git a/website/docs/admin_hosts_houdini.md b/website/docs/admin_hosts_houdini.md index ea7991530b..749ca43fe2 100644 --- a/website/docs/admin_hosts_houdini.md +++ b/website/docs/admin_hosts_houdini.md @@ -4,26 +4,24 @@ title: Houdini sidebar_label: Houdini --- ## General Settings -### JOB Path +### 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 -The Houdini `$JOB` path can be customized through project settings with a (dynamic) path that will be updated on context changes, e.g. when switching to another asset or task. :::note -If the folder does not exist on the context change it will be created by this feature so that `$JOB` will always try to point to an existing folder. +If `is Dir Path` 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 this feature will leave `$JOB` var unmanaged and thus no context update changes will occur. +Disabling `Update Houdini vars on context change` feature will leave all Houdini vars unmanaged and thus no context update changes will occur. -JOB Path can be: -- Arbitrary path -- Openpype template path - > This allows dynamic values for assets or shots.
- > Using template keys is supported but formatting keys capitalization variants is not, - > e.g. `{Asset}` and `{ASSET}` won't work -- Empty - > In this case, JOB will be synced to HIP +> If `$JOB` is present in the Houdini var list and has an empty value, OpenPype will set its value to `$HIP` -![update job on context change](assets/houdini/update-job-context-change.png) +![update-houdini-vars-context-change](assets/houdini/update-houdini-vars-context-change.png) 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 0000000000000000000000000000000000000000..77c67a620dd50923bafbba61f4f5e0f9236a3f27 GIT binary patch literal 18456 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 From b45e8c3131f6c136970afccf7c6ef4b3db251e93 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 29 Sep 2023 17:37:29 +0300 Subject: [PATCH 25/63] update photo --- .../update-houdini-vars-context-change.png | Bin 18456 -> 18904 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/website/docs/assets/houdini/update-houdini-vars-context-change.png b/website/docs/assets/houdini/update-houdini-vars-context-change.png index 77c67a620dd50923bafbba61f4f5e0f9236a3f27..76fb7233214c685cfe632d05e937535280c723f0 100644 GIT binary patch literal 18904 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 literal 18456 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 Date: Fri, 29 Sep 2023 17:44:36 +0300 Subject: [PATCH 26/63] avoid using nested if --- openpype/hosts/houdini/api/lib.py | 78 ++++++++++++++++--------------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 291817bbe9..67755c1a72 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -765,52 +765,54 @@ def update_houdini_vars_context(): houdini_vars_settings = \ project_settings["houdini"]["general"]["update_houdini_var_context"] - if houdini_vars_settings["enabled"]: - houdini_vars = houdini_vars_settings["houdini_vars"] + if not houdini_vars_settings["enabled"]: + return - # No vars specified - nothing to do - if not houdini_vars: - return + houdini_vars = houdini_vars_settings["houdini_vars"] - # Get Template data - template_data = get_current_context_template_data() + # No vars specified - nothing to do + if not houdini_vars: + return - # Set Houdini Vars - for item in houdini_vars: + # Get Template data + template_data = get_current_context_template_data() - # For consistency reasons we always force all vars to be uppercase - item["var"] = item["var"].upper() + # Set Houdini Vars + for item in houdini_vars: - # get and resolve job path template - item_value = StringTemplate.format_template( - item["value"], - template_data - ) + # For consistency reasons we always force all vars to be uppercase + item["var"] = item["var"].upper() - if item["is_dir_path"]: - item_value = item_value.replace("\\", "/") - try: - os.makedirs(item_value) - except OSError as e: - if e.errno != errno.EEXIST: - print( - " - Failed to create ${} dir. Maybe due to " - "insufficient permissions.".format(item["var"]) - ) + # get and resolve template in value + item_value = StringTemplate.format_template( + item["value"], + template_data + ) - if item["var"] == "JOB" and item_value == "": - # sync $JOB to $HIP if $JOB is empty - item_value = os.environ["HIP"] + if item["is_dir_path"]: + item_value = item_value.replace("\\", "/") + try: + os.makedirs(item_value) + except OSError as e: + if e.errno != errno.EEXIST: + print( + " - Failed to create ${} dir. Maybe due to " + "insufficient permissions.".format(item["var"]) + ) - current_value = hou.hscript("echo -n `${}`".format(item["var"]))[0] + if item["var"] == "JOB" and item_value == "": + # sync $JOB to $HIP if $JOB is empty + item_value = os.environ["HIP"] - # sync both environment variables. - # because houdini doesn't do that by default - # on opening new files - os.environ[item["var"]] = current_value + current_value = hou.hscript("echo -n `${}`".format(item["var"]))[0] - if current_value != item_value: - hou.hscript("set {}={}".format(item["var"], item_value)) - os.environ[item["var"]] = item_value + # sync both environment variables. + # because houdini doesn't do that by default + # on opening new files + os.environ[item["var"]] = current_value - print(" - Updated ${} to {}".format(item["var"], item_value)) + if current_value != item_value: + hou.hscript("set {}={}".format(item["var"], item_value)) + os.environ[item["var"]] = item_value + + print(" - Updated ${} to {}".format(item["var"], item_value)) From 82b2bd4b4540c435a76e1aa3bcc911296c887c74 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 29 Sep 2023 19:32:08 +0300 Subject: [PATCH 27/63] update labels and add settings tips --- openpype/hosts/houdini/api/lib.py | 2 +- .../defaults/project_settings/houdini.json | 2 +- .../schemas/schema_houdini_general.json | 8 ++++++-- .../houdini/server/settings/general.py | 10 ++++++++-- .../update-houdini-vars-context-change.png | Bin 18904 -> 23727 bytes 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 67755c1a72..ce89ffe606 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -789,7 +789,7 @@ def update_houdini_vars_context(): template_data ) - if item["is_dir_path"]: + if item["is_directory"]: item_value = item_value.replace("\\", "/") try: os.makedirs(item_value) diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 111ed2b24d..4f57ee52c6 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -6,7 +6,7 @@ { "var": "JOB", "value": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}", - "is_dir_path": true + "is_directory": true } ] } 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 index 3160e657bf..c1e2cae8f0 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json @@ -17,6 +17,10 @@ "key": "enabled", "label": "Enabled" }, + { + "type": "label", + "label": "Houdini Vars.
If a value is treated as a directory on update it will be ensured the folder exists" + }, { "type": "list", "key": "houdini_vars", @@ -37,8 +41,8 @@ }, { "type": "boolean", - "key": "is_dir_path", - "label": "is Dir Path" + "key": "is_directory", + "label": "Treat as directory" } ] } diff --git a/server_addon/houdini/server/settings/general.py b/server_addon/houdini/server/settings/general.py index 7b3b95f978..0109eec63d 100644 --- a/server_addon/houdini/server/settings/general.py +++ b/server_addon/houdini/server/settings/general.py @@ -6,10 +6,16 @@ class HoudiniVarModel(BaseSettingsModel): _layout = "expanded" var: str = Field("", title="Var") value: str = Field("", title="Value") - is_dir_path: bool = Field(False, title="is Dir Path") + is_directory: bool = Field(False, title="Treat as directory") class UpdateHoudiniVarcontextModel(BaseSettingsModel): + """Houdini Vars Note. + + 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( @@ -32,7 +38,7 @@ DEFAULT_GENERAL_SETTINGS = { { "var": "JOB", "value": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}", # noqa - "is_dir_path": True + "is_directory": True } ] } diff --git a/website/docs/assets/houdini/update-houdini-vars-context-change.png b/website/docs/assets/houdini/update-houdini-vars-context-change.png index 76fb7233214c685cfe632d05e937535280c723f0..74ac8d86c9fb9d7e0520e9922517039a36df20b6 100644 GIT binary patch literal 23727 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 From 7816c0370f705b796dc4f3cafa379b1d3117f681 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 29 Sep 2023 19:34:02 +0300 Subject: [PATCH 28/63] Big Roy's comment --- .../schemas/projects_schema/schemas/schema_houdini_general.json | 2 +- server_addon/houdini/server/settings/general.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index c1e2cae8f0..de1a0396ec 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json @@ -19,7 +19,7 @@ }, { "type": "label", - "label": "Houdini Vars.
If a value is treated as a directory on update it will be ensured the folder exists" + "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", diff --git a/server_addon/houdini/server/settings/general.py b/server_addon/houdini/server/settings/general.py index 0109eec63d..21cc4c452c 100644 --- a/server_addon/houdini/server/settings/general.py +++ b/server_addon/houdini/server/settings/general.py @@ -10,7 +10,7 @@ class HoudiniVarModel(BaseSettingsModel): class UpdateHoudiniVarcontextModel(BaseSettingsModel): - """Houdini Vars Note. + """Sync vars with context changes. If a value is treated as a directory on update it will be ensured the folder exists. From e6585e8d9dec22461bcd71e974324b8463558c2d Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 29 Sep 2023 19:37:53 +0300 Subject: [PATCH 29/63] update docs --- website/docs/admin_hosts_houdini.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/admin_hosts_houdini.md b/website/docs/admin_hosts_houdini.md index 749ca43fe2..dd0e92f480 100644 --- a/website/docs/admin_hosts_houdini.md +++ b/website/docs/admin_hosts_houdini.md @@ -12,7 +12,7 @@ Using template keys is supported but formatting keys capitalization variants is :::note -If `is Dir Path` toggle is activated, Openpype will consider the given value is a path of a folder. +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. ::: From e75dc71ff7d97410756bc0343774621cd65f6d57 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 29 Sep 2023 19:41:10 +0300 Subject: [PATCH 30/63] update docs --- website/docs/admin_hosts_houdini.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/docs/admin_hosts_houdini.md b/website/docs/admin_hosts_houdini.md index dd0e92f480..18c390e07f 100644 --- a/website/docs/admin_hosts_houdini.md +++ b/website/docs/admin_hosts_houdini.md @@ -21,6 +21,12 @@ Disabling `Update Houdini vars on context change` feature will leave all Houdini > 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` +::: + ![update-houdini-vars-context-change](assets/houdini/update-houdini-vars-context-change.png) From 6d451ccd09fab8bad13a42c21850605133660d03 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 30 Sep 2023 13:15:12 +0200 Subject: [PATCH 31/63] Use settings from `apply_settings` --- openpype/hosts/maya/api/plugin.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 79fcf9bc8b..157ce8368f 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -601,6 +601,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 +620,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 +631,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'], From 28dff4ed3880a008f5a5d3a2cccecb46b16ec4c2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 30 Sep 2023 13:16:02 +0200 Subject: [PATCH 32/63] Use project settings from context data --- .../plugins/publish/validate_unreal_staticmesh_naming.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index 5ba256f9f5..58fa9d02bd 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -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"] From b0a62e3afee78fbad201e1b6c914e8c6241b1d85 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 30 Sep 2023 13:17:21 +0200 Subject: [PATCH 33/63] Remove unused imports --- openpype/hosts/maya/api/pipeline.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 3647ec0b6b..04ff810873 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -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, From fe786236cddf4cb58ae56f03cf02fcfc29955545 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 30 Sep 2023 13:18:25 +0200 Subject: [PATCH 34/63] Move Muster module related submitter to Muster module --- .../muster}/plugins/publish/submit_maya_muster.py | 1 + 1 file changed, 1 insertion(+) rename openpype/{hosts/maya => modules/muster}/plugins/publish/submit_maya_muster.py (99%) diff --git a/openpype/hosts/maya/plugins/publish/submit_maya_muster.py b/openpype/modules/muster/plugins/publish/submit_maya_muster.py similarity index 99% rename from openpype/hosts/maya/plugins/publish/submit_maya_muster.py rename to openpype/modules/muster/plugins/publish/submit_maya_muster.py index c174fa7a33..3c3f901f87 100644 --- a/openpype/hosts/maya/plugins/publish/submit_maya_muster.py +++ b/openpype/modules/muster/plugins/publish/submit_maya_muster.py @@ -25,6 +25,7 @@ def _get_template_id(renderer): :rtype: int """ + # TODO: Use setings from context? templates = get_system_settings()["modules"]["muster"]["templates_mapping"] if not templates: raise RuntimeError(("Muster template mapping missing in " From 31e64d0ef819b0f934e94e7ed47e795991625ac3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 30 Sep 2023 13:25:55 +0200 Subject: [PATCH 35/63] Pass project settings to menu install so it doesn't need to also retrieve it --- openpype/hosts/maya/api/menu.py | 17 +++++++---------- openpype/hosts/maya/api/pipeline.py | 2 +- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 715f54686c..18a4ea0e9a 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -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(): diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 04ff810873..38d7ae08c1 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -106,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) From 61f7a2039b60567416d80005c046aab7a5e28de2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 2 Oct 2023 00:57:22 +0200 Subject: [PATCH 36/63] Update openpype/modules/muster/plugins/publish/submit_maya_muster.py --- openpype/modules/muster/plugins/publish/submit_maya_muster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/muster/plugins/publish/submit_maya_muster.py b/openpype/modules/muster/plugins/publish/submit_maya_muster.py index 3c3f901f87..5c95744876 100644 --- a/openpype/modules/muster/plugins/publish/submit_maya_muster.py +++ b/openpype/modules/muster/plugins/publish/submit_maya_muster.py @@ -25,7 +25,7 @@ def _get_template_id(renderer): :rtype: int """ - # TODO: Use setings from context? + # TODO: Use settings from context? templates = get_system_settings()["modules"]["muster"]["templates_mapping"] if not templates: raise RuntimeError(("Muster template mapping missing in " From 4a2417d2ca741c6b5a8db81b042e0bfff7a4d12a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Oct 2023 03:25:31 +0000 Subject: [PATCH 37/63] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 9fb7bbc66c..e3ca8262e5 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.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 From c5bf50a4541a4c5ddfb1d64bd51b1654abf4cbe5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 4 Oct 2023 17:12:28 +0800 Subject: [PATCH 38/63] minor docstring and code tweaks for ExtractReviewMov --- openpype/hosts/nuke/api/lib.py | 12 +++++------- openpype/hosts/nuke/api/plugin.py | 6 +++--- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 07f394ec00..380d9a42d1 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3425,17 +3425,15 @@ 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 +def prepend_name_before_hashed_frame(original_path, name): + """Function to prepend an extra name before the hashed frame numbers Examples: - >>> get_head_filename_without_hashes("render.####.exr", "baking") + >>> prepend_name_before_hashed_frame("render.####.exr", "baking") render.baking.####.exr - >>> get_head_filename_without_hashes("render.%04d.exr", "tag") + >>> prepend_name_before_hashed_frame("render.%04d.exr", "tag") render.tag.%d.exr - >>> get_head_filename_without_hashes("exr.####.exr", "foo") + >>> prepend_name_before_hashed_frame("exr.####.exr", "foo") exr.foo.%04d.exr Args: diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 81841d17be..2f432ad9b6 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -39,7 +39,7 @@ from .lib import ( get_view_process_node, get_viewer_config_from_string, deprecated, - get_head_filename_without_hashes, + prepend_name_before_hashed_frame, get_filenames_without_hash ) from .pipeline import ( @@ -820,12 +820,12 @@ class ExporterReviewMov(ExporterReview): 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( + filename = prepend_name_before_hashed_frame( 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: + if not self.file.endswith(".{}".format(ext)): filename_no_ext, _ = os.path.splitext(filename) self.file = "{}.{}".format(filename_no_ext, self.ext) From 090f1e041b14bfaa6e903f3f1fe836f84acb4253 Mon Sep 17 00:00:00 2001 From: Claudio Hickstein <122775550+spmhickstein@users.noreply.github.com> Date: Wed, 4 Oct 2023 11:17:22 +0200 Subject: [PATCH 39/63] Deadline: handle all valid paths in RenderExecutable (#5694) * handle all valid paths in RenderExecutable * Update openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --------- Co-authored-by: Petr Kalis Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../modules/deadline/repository/custom/plugins/Ayon/Ayon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py index a29acf9823..2c55e7c951 100644 --- a/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py +++ b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py @@ -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 == "": From aef56b7cd3c89a39bdb0975534d3a78a1c307133 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 4 Oct 2023 18:52:45 +0800 Subject: [PATCH 40/63] remove unnecessary function --- openpype/hosts/nuke/api/lib.py | 26 -------------------------- openpype/hosts/nuke/api/plugin.py | 12 +++++------- 2 files changed, 5 insertions(+), 33 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 380d9a42d1..390545b806 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3425,32 +3425,6 @@ def create_viewer_profile_string(viewer, display=None, path_like=False): return "{} ({})".format(viewer, display) -def prepend_name_before_hashed_frame(original_path, name): - """Function to prepend an extra name before the hashed frame numbers - - Examples: - >>> prepend_name_before_hashed_frame("render.####.exr", "baking") - render.baking.####.exr - >>> prepend_name_before_hashed_frame("render.%04d.exr", "tag") - render.tag.%d.exr - >>> prepend_name_before_hashed_frame("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" diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 2f432ad9b6..2ce41f61c7 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -39,7 +39,6 @@ from .lib import ( get_view_process_node, get_viewer_config_from_string, deprecated, - prepend_name_before_hashed_frame, get_filenames_without_hash ) from .pipeline import ( @@ -818,15 +817,14 @@ class ExporterReviewMov(ExporterReview): 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 = prepend_name_before_hashed_frame( - self.path_in, self.name) - self.file = filename + filename = os.path.basename(self.path_in) + self.file = self.fhead + self.name + ".{}".format( + filename.split(".", 1)[-1] + ) # make sure the filename are in # correct image output format if not self.file.endswith(".{}".format(ext)): - filename_no_ext, _ = os.path.splitext(filename) + filename_no_ext, _ = os.path.splitext(self.file) self.file = "{}.{}".format(filename_no_ext, self.ext) self.path = os.path.join( From 13b46070fe9fef0b6474a50e9cebb7d7b73eac43 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 4 Oct 2023 19:32:41 +0800 Subject: [PATCH 41/63] use re.sub in the function for review frame sequence name --- openpype/hosts/nuke/api/plugin.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 2ce41f61c7..d54967aa15 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -818,9 +818,8 @@ class ExporterReviewMov(ExporterReview): self.file = self.fhead + self.name + ".{}".format(self.ext) if ".{}".format(self.ext) not in VIDEO_EXTENSIONS: filename = os.path.basename(self.path_in) - self.file = self.fhead + self.name + ".{}".format( - filename.split(".", 1)[-1] - ) + self.file = re.sub( + self.fhead, self.fhead + self.name + ".", filename) # make sure the filename are in # correct image output format if not self.file.endswith(".{}".format(ext)): From 253c895363d2ec1fb0dcdc6d40d4539b120cefd6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 4 Oct 2023 21:48:44 +0800 Subject: [PATCH 42/63] clean up the code for implementating variable for self.file when the self.ext is image format --- openpype/hosts/nuke/api/plugin.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index d54967aa15..067814679c 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -817,15 +817,11 @@ class ExporterReviewMov(ExporterReview): self.file = self.fhead + self.name + ".{}".format(self.ext) if ".{}".format(self.ext) not in VIDEO_EXTENSIONS: - filename = os.path.basename(self.path_in) - self.file = re.sub( - self.fhead, self.fhead + self.name + ".", filename) - # make sure the filename are in - # correct image output format - if not self.file.endswith(".{}".format(ext)): - filename_no_ext, _ = os.path.splitext(self.file) - self.file = "{}.{}".format(filename_no_ext, self.ext) - + 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("\\", "/") From 22ac8e7ac6fcece2aa73b066c02008b34bd6afff Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 4 Oct 2023 22:08:33 +0800 Subject: [PATCH 43/63] use fomrat string for self.file --- openpype/hosts/nuke/api/plugin.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 067814679c..0da181908e 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -815,8 +815,10 @@ 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: + if ".{}".format(self.ext) in VIDEO_EXTENSIONS: + self.file = "{}{}.{}".format( + self.fhead, self.name, self.ext) + else: filename_no_ext = os.path.splitext( os.path.basename(self.path_in))[0] after_head = filename_no_ext[len(self.fhead):] From a75e5d8db6aad7c0462af8f1637080f8c619ca0e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 4 Oct 2023 22:09:24 +0800 Subject: [PATCH 44/63] add comments --- openpype/hosts/nuke/api/plugin.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 0da181908e..c39e3c339d 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -819,6 +819,11 @@ class ExporterReviewMov(ExporterReview): 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):] From bf15868fc4cd0a75fe6017ddbdf316beb76af303 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 4 Oct 2023 17:48:16 +0200 Subject: [PATCH 45/63] Chore: Refactor Resolve into new style HostBase, IWorkfileHost, ILoadHost --- openpype/hosts/resolve/api/__init__.py | 11 +-- openpype/hosts/resolve/api/menu.py | 9 +- openpype/hosts/resolve/api/pipeline.py | 95 ++++++++++--------- openpype/hosts/resolve/api/utils.py | 10 +- openpype/hosts/resolve/startup.py | 9 +- .../resolve/utility_scripts/OpenPype__Menu.py | 7 +- 6 files changed, 71 insertions(+), 70 deletions(-) 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..19c7b13371 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,59 @@ 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 = "maya" - This is where you install menus and register families, data - and loaders into resolve. + def __init__(self): + super(ResolveHost, self).__init__() - It is called automatically when installing via `api.install(resolve)`. + def install(self): + """Install resolve-specific functionality of avalon-core. - See the Maya equivalent for inspiration on how to implement this. + This is where you install menus and register families, data + and loaders into resolve. - """ + It is called automatically when installing via `api.install(resolve)`. - log.info("openpype.hosts.resolve installed") + See the Maya equivalent for inspiration on how to implement this. - pyblish.register_host("resolve") - pyblish.register_plugin_path(PUBLISH_PATH) - log.info("Registering DaVinci Resovle plug-ins..") + """ - register_loader_plugin_path(LOAD_PATH) - register_creator_plugin_path(CREATE_PATH) + log.info("openpype.hosts.resolve installed") - # register callback for switching publishable - pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled) + pyblish.register_host("resolve") + pyblish.register_plugin_path(PUBLISH_PATH) + print("Registering DaVinci Resolve plug-ins..") - get_resolve_module() + register_loader_plugin_path(LOAD_PATH) + register_creator_plugin_path(CREATE_PATH) + # register callback for switching publishable + pyblish.register_callback("instanceToggled", + on_pyblish_instance_toggled) -def uninstall(): - """Uninstall all that was installed + get_resolve_module() - 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 open_workfile(self, filepath): + return open_file(filepath) - """ - pyblish.deregister_host("resolve") - pyblish.deregister_plugin_path(PUBLISH_PATH) - log.info("Deregistering DaVinci Resovle plug-ins..") + def save_workfile(self, filepath=None): + return save_file(filepath) - deregister_loader_plugin_path(LOAD_PATH) - deregister_creator_plugin_path(CREATE_PATH) + def work_root(self, session): + return work_root(session) - # register callback for switching publishable - pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled) + def get_current_workfile(self): + return current_file() + + 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 +222,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__": From 05b487a435f8add93e638c2bb4433d7c8dc15054 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 4 Oct 2023 20:30:45 +0200 Subject: [PATCH 46/63] Apply suggestions from code review by @iLLiCiTiT Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/resolve/api/pipeline.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/resolve/api/pipeline.py b/openpype/hosts/resolve/api/pipeline.py index 19c7b13371..05f556fa5b 100644 --- a/openpype/hosts/resolve/api/pipeline.py +++ b/openpype/hosts/resolve/api/pipeline.py @@ -43,10 +43,7 @@ AVALON_CONTAINERS = ":AVALON_CONTAINERS" class ResolveHost(HostBase, IWorkfileHost, ILoadHost): - name = "maya" - - def __init__(self): - super(ResolveHost, self).__init__() + name = "resolve" def install(self): """Install resolve-specific functionality of avalon-core. @@ -62,7 +59,7 @@ class ResolveHost(HostBase, IWorkfileHost, ILoadHost): log.info("openpype.hosts.resolve installed") - pyblish.register_host("resolve") + pyblish.register_host(self.name) pyblish.register_plugin_path(PUBLISH_PATH) print("Registering DaVinci Resolve plug-ins..") From d26df62e1502beed52522efe3a4b5a6bb9679ee8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 5 Oct 2023 13:00:52 +0200 Subject: [PATCH 47/63] do not crash if task is not filled --- openpype/plugins/actions/open_file_explorer.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/plugins/actions/open_file_explorer.py b/openpype/plugins/actions/open_file_explorer.py index e4fbd91143..2eb4ee7f8e 100644 --- a/openpype/plugins/actions/open_file_explorer.py +++ b/openpype/plugins/actions/open_file_explorer.py @@ -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) From 2c68dbcc72a185e69232dc9646dd0c6eebef1f7b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 5 Oct 2023 13:01:02 +0200 Subject: [PATCH 48/63] change an error a little bit --- openpype/plugins/actions/open_file_explorer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/actions/open_file_explorer.py b/openpype/plugins/actions/open_file_explorer.py index 2eb4ee7f8e..1568c41fbd 100644 --- a/openpype/plugins/actions/open_file_explorer.py +++ b/openpype/plugins/actions/open_file_explorer.py @@ -91,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): From 9c543d12ddb6057c120565099fab20b5a06bd4b5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 5 Oct 2023 14:44:13 +0200 Subject: [PATCH 49/63] AYON: Small settings fixes (#5699) * add label to nuke 13-0 variant * make 'ExtractReviewIntermediates' settings backwards compatible * add remaining labels for '13-0' variants --- .../nuke/plugins/publish/extract_review_intermediates.py | 4 +++- openpype/settings/ayon_settings.py | 6 ++++-- server_addon/applications/server/applications.json | 5 +++++ 3 files changed, 12 insertions(+), 3 deletions(-) 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/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/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": [ From d7dcc3862f9e558262b6c1a6a74a24ce24f2a160 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 5 Oct 2023 16:03:28 +0300 Subject: [PATCH 50/63] display changes in menu, add menu button --- openpype/hosts/houdini/api/lib.py | 78 +++++++++++++------ openpype/hosts/houdini/api/pipeline.py | 5 +- .../hosts/houdini/startup/MainMenuCommon.xml | 8 ++ 3 files changed, 65 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index ce89ffe606..eea2df7369 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -18,7 +18,7 @@ from openpype.pipeline.context_tools import ( get_current_context_template_data, get_current_project_asset ) - +from openpype.widgets import popup import hou @@ -166,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: @@ -755,31 +753,29 @@ def get_camera_from_container(container): return cameras[0] -def update_houdini_vars_context(): - """Update Houdini vars to match current context. +def get_context_var_changes(): + """get context var changes.""" - This will only do something if the setting is enabled in project settings. - """ + 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 + return houdini_vars_to_update houdini_vars = houdini_vars_settings["houdini_vars"] # No vars specified - nothing to do if not houdini_vars: - return + 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 item["var"] = item["var"].upper() @@ -789,21 +785,13 @@ def update_houdini_vars_context(): template_data ) - if item["is_directory"]: - item_value = item_value.replace("\\", "/") - try: - os.makedirs(item_value) - except OSError as e: - if e.errno != errno.EEXIST: - print( - " - Failed to create ${} dir. Maybe due to " - "insufficient permissions.".format(item["var"]) - ) - if item["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(item["var"]))[0] # sync both environment variables. @@ -812,7 +800,49 @@ def update_houdini_vars_context(): os.environ[item["var"]] = current_value if current_value != item_value: - hou.hscript("set {}={}".format(item["var"], item_value)) - os.environ[item["var"]] = item_value + houdini_vars_to_update.update({item["var"]: (current_value, item_value, item["is_directory"])}) - print(" - Updated ${} to {}".format(item["var"], item_value)) + 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 + return + + message = "\n".join( + "${}: {} -> {}".format(var, old or "None", new) + for var, (old, new, is_directory) in update_vars.items() + ) + parent = hou.ui.mainQtWindow() + dialog = popup.PopupUpdateKeys(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_state.connect(lambda: update_houdini_vars_context()) + + dialog.show() diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index f753d518f0..f8db45c56b 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -301,7 +301,7 @@ def on_save(): log.info("Running callback on save..") # update houdini vars - lib.update_houdini_vars_context() + lib.update_houdini_vars_context_dialog() nodes = lib.get_id_required_nodes() for node, new_id in lib.generate_ids(nodes): @@ -339,7 +339,7 @@ def on_open(): log.info("Running callback on open..") # update houdini vars - lib.update_houdini_vars_context() + lib.update_houdini_vars_context_dialog() # Validate FPS after update_task_from_path to # ensure it is using correct FPS for the asset @@ -405,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() ]]> + + + + + From 809b6df22178fda6c3b496cd49edc6799f9c3081 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 5 Oct 2023 16:05:47 +0300 Subject: [PATCH 51/63] resolve hound --- openpype/hosts/houdini/api/lib.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index eea2df7369..68ba4589d9 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -800,7 +800,13 @@ def get_context_var_changes(): os.environ[item["var"]] = current_value if current_value != item_value: - houdini_vars_to_update.update({item["var"]: (current_value, item_value, item["is_directory"])}) + houdini_vars_to_update.update( + { + item["var"]: ( + current_value, item_value, item["is_directory"] + ) + } + ) return houdini_vars_to_update From 35194b567f7599b9480b8ba3e048229a0503faa0 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 5 Oct 2023 16:06:35 +0300 Subject: [PATCH 52/63] resolve hound 2 --- openpype/hosts/houdini/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 68ba4589d9..fa94ddfeb4 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -814,7 +814,7 @@ def get_context_var_changes(): def update_houdini_vars_context(): """Update asset context variables""" - for var, (old, new, is_directory) in get_context_var_changes().items(): + for var, (_old, new, is_directory) in get_context_var_changes().items(): if is_directory: try: os.makedirs(new) From 0af1b5846c31602944bb78d396d6e8fdd23b23bd Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 5 Oct 2023 16:07:19 +0300 Subject: [PATCH 53/63] resolve hound 3 --- openpype/hosts/houdini/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index fa94ddfeb4..e4040852b9 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -839,7 +839,7 @@ def update_houdini_vars_context_dialog(): message = "\n".join( "${}: {} -> {}".format(var, old or "None", new) - for var, (old, new, is_directory) in update_vars.items() + for var, (old, new, _is_directory) in update_vars.items() ) parent = hou.ui.mainQtWindow() dialog = popup.PopupUpdateKeys(parent=parent) From 3daa0749d1a40eb0c22214fb69cc5ef76965b65d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 5 Oct 2023 15:08:31 +0200 Subject: [PATCH 54/63] AYON Launcher tool: Fix skip last workfile boolean (#5700) * reverse the boolean to skip last workfile * remove 'start_last_workfile' key to keep logic based on settings * change 'skip_last_workfile' for all variants of DCC * fix context menu on ungrouped items * better sort of action items --- openpype/tools/ayon_launcher/abstract.py | 4 +- openpype/tools/ayon_launcher/control.py | 4 +- .../tools/ayon_launcher/models/actions.py | 10 +++-- .../tools/ayon_launcher/ui/actions_widget.py | 37 +++++++++++++++++-- 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/openpype/tools/ayon_launcher/abstract.py b/openpype/tools/ayon_launcher/abstract.py index 00502fe930..f2ef681c62 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. """ diff --git a/openpype/tools/ayon_launcher/control.py b/openpype/tools/ayon_launcher/control.py index 09e07893c3..a6e528b104 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): 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..0630d1d5b5 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. @@ -51,6 +66,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 +88,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 +121,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 +154,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,10 +162,12 @@ 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): @@ -387,9 +412,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 +433,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 +441,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): From 31d77932ede38fbc5c5eda29df5fcf920210a0e7 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 5 Oct 2023 16:15:26 +0300 Subject: [PATCH 55/63] print message to user if nothing to change --- openpype/hosts/houdini/api/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index e4040852b9..1f71481cc6 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -835,6 +835,7 @@ def update_houdini_vars_context_dialog(): update_vars = get_context_var_changes() if not update_vars: # Nothing to change + print(" - Nothing to change, Houdini Vars are up to date.") return message = "\n".join( From e255c20c440211d3578fc7bcc7b350b6756dd859 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 5 Oct 2023 15:37:45 +0200 Subject: [PATCH 56/63] Remove checks for env var (#5696) Env var will be filled in `env_var` fixture, here it is too early to check --- openpype/pype_commands.py | 6 ------ 1 file changed, 6 deletions(-) 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 From 52c65c9b6cd194f115f64df850e45764bdf3653a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 5 Oct 2023 16:03:56 +0200 Subject: [PATCH 57/63] Fusion: implement toggle to use Deadline plugin FusionCmd (#5678) * OP-6971 - changed DL plugin to FusionCmd Fusion 17 doesn't work in DL 10.3, but FusionCmd does. It might be probably better option as headless variant. * OP-6971 - added dropdown to Project Settings * OP-6971 - updated settings for Ayon * OP-6971 - added default * OP-6971 - bumped up version * Update openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json Co-authored-by: Roy Nieterau --------- Co-authored-by: Roy Nieterau --- .../plugins/publish/submit_fusion_deadline.py | 4 +++- .../defaults/project_settings/deadline.json | 3 ++- .../schema_project_deadline.json | 9 ++++++++ .../server/settings/publish_plugins.py | 21 +++++++++++++++++++ server_addon/deadline/server/version.py | 2 +- 5 files changed, 36 insertions(+), 3 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index 70aa12956d..c91dd4bd69 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -34,6 +34,8 @@ class FusionSubmitDeadline( targets = ["local"] # presets + plugin = None + priority = 50 chunk_size = 1 concurrent_tasks = 1 @@ -173,7 +175,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"]) 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/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/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" From c6b370be9aec3b4f6d262e34f911e6dcad0913fd Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 5 Oct 2023 17:23:53 +0300 Subject: [PATCH 58/63] BigRoy's comments --- openpype/hosts/houdini/api/lib.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 1f71481cc6..3b38a6669f 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -777,7 +777,7 @@ def get_context_var_changes(): # Set Houdini Vars for item in houdini_vars: # For consistency reasons we always force all vars to be uppercase - item["var"] = item["var"].upper() + var = item["var"].upper() # get and resolve template in value item_value = StringTemplate.format_template( @@ -785,27 +785,18 @@ def get_context_var_changes(): template_data ) - if item["var"] == "JOB" and item_value == "": + 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(item["var"]))[0] - - # sync both environment variables. - # because houdini doesn't do that by default - # on opening new files - os.environ[item["var"]] = current_value + current_value = hou.hscript("echo -n `${}`".format(var))[0] if current_value != item_value: - houdini_vars_to_update.update( - { - item["var"]: ( - current_value, item_value, item["is_directory"] - ) - } + houdini_vars_to_update[var] = ( + current_value, item_value, item["is_directory"] ) return houdini_vars_to_update @@ -821,13 +812,13 @@ def update_houdini_vars_context(): except OSError as e: if e.errno != errno.EEXIST: print( - " - Failed to create ${} dir. Maybe due to " + "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)) + print("Updated ${} to {}".format(var, new)) def update_houdini_vars_context_dialog(): @@ -835,7 +826,7 @@ def update_houdini_vars_context_dialog(): update_vars = get_context_var_changes() if not update_vars: # Nothing to change - print(" - Nothing to change, Houdini Vars are up to date.") + print("Nothing to change, Houdini Vars are up to date.") return message = "\n".join( @@ -850,6 +841,6 @@ def update_houdini_vars_context_dialog(): dialog.setButtonText("Fix") # on_show is the Fix button clicked callback - dialog.on_clicked_state.connect(lambda: update_houdini_vars_context()) + dialog.on_clicked_state.connect(update_houdini_vars_context) dialog.show() From 8f0b1827595ef77fa2adcbe2a661c53f99d87513 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 5 Oct 2023 17:26:08 +0300 Subject: [PATCH 59/63] update printed message --- openpype/hosts/houdini/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 3b38a6669f..44752a3369 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -826,7 +826,7 @@ def update_houdini_vars_context_dialog(): update_vars = get_context_var_changes() if not update_vars: # Nothing to change - print("Nothing to change, Houdini Vars are up to date.") + print("Nothing to change, Houdini vars are already up to date.") return message = "\n".join( From 12f41289018c46ab09eb5336a3dcdea93057183d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 5 Oct 2023 16:46:20 +0200 Subject: [PATCH 60/63] Fusion: added missing env vars to Deadline submission (#5659) * OP-6930 - added missing env vars to Fusion Deadline submission Without this injection of environment variables won't start. * OP-6930 - removed unnecessary env var * OP-6930 - removed unnecessary env var --- .../plugins/publish/submit_fusion_deadline.py | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index c91dd4bd69..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 @@ -218,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, From 5b04af7ea138641bb5813ea7894044d03d8285c9 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 5 Oct 2023 19:30:44 +0300 Subject: [PATCH 61/63] remove leading and trailing whitespaces from vars --- openpype/hosts/houdini/api/lib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 44752a3369..75986c71f5 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -777,7 +777,8 @@ def get_context_var_changes(): # Set Houdini Vars for item in houdini_vars: # For consistency reasons we always force all vars to be uppercase - var = item["var"].upper() + # Also remove any leading, and trailing whitespaces. + var = item["var"].strip().upper() # get and resolve template in value item_value = StringTemplate.format_template( From 2ea8d6530fac1818afb98e04d90484f2456614cc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 6 Oct 2023 10:44:39 +0200 Subject: [PATCH 62/63] AYON Launcher tool: Fix refresh btn (#5685) * rename 'refresh' to 'set_context' in 'TasksModel' * implemented 'refresh' for folders and tasks widgets * propagate refresh to all widgets * don't use 'clear' of 'QStandardItemModel' * change lifetime of folders cache to a minute * added 'refresh_actions' method to launcher to skip clear cache of folders * shorten line * sorting is not case sensitive --- openpype/tools/ayon_launcher/abstract.py | 10 +++++ openpype/tools/ayon_launcher/control.py | 12 ++++++ .../tools/ayon_launcher/ui/actions_widget.py | 14 ++----- .../tools/ayon_launcher/ui/hierarchy_page.py | 4 ++ .../tools/ayon_launcher/ui/projects_widget.py | 13 +++++++ openpype/tools/ayon_launcher/ui/window.py | 39 +++++++++++++------ openpype/tools/ayon_utils/models/hierarchy.py | 13 +++++-- .../ayon_utils/widgets/folders_widget.py | 30 ++++++++++---- .../tools/ayon_utils/widgets/tasks_widget.py | 31 ++++++++++----- 9 files changed, 124 insertions(+), 42 deletions(-) diff --git a/openpype/tools/ayon_launcher/abstract.py b/openpype/tools/ayon_launcher/abstract.py index f2ef681c62..95fe2b2c8d 100644 --- a/openpype/tools/ayon_launcher/abstract.py +++ b/openpype/tools/ayon_launcher/abstract.py @@ -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 a6e528b104..36c0536422 100644 --- a/openpype/tools/ayon_launcher/control.py +++ b/openpype/tools/ayon_launcher/control.py @@ -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/ui/actions_widget.py b/openpype/tools/ayon_launcher/ui/actions_widget.py index 0630d1d5b5..2a1a06695d 100644 --- a/openpype/tools/ayon_launcher/ui/actions_widget.py +++ b/openpype/tools/ayon_launcher/ui/actions_widget.py @@ -46,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, @@ -170,13 +166,6 @@ class ActionsQtModel(QtGui.QStandardItemModel): 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 @@ -361,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) 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 ) From 7c5d149f56c7aba57c3325fadd43075ec732580d Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 6 Oct 2023 12:11:51 +0300 Subject: [PATCH 63/63] use different popup --- openpype/hosts/houdini/api/lib.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 75986c71f5..3db18ca69a 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -831,17 +831,19 @@ def update_houdini_vars_context_dialog(): return message = "\n".join( - "${}: {} -> {}".format(var, old or "None", new) + "${}: {} -> {}".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.PopupUpdateKeys(parent=parent) + 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_state.connect(update_houdini_vars_context) + dialog.on_clicked.connect(update_houdini_vars_context) dialog.show()