From 7ee3b5ac850545b02cbe45d0d8d1ebda8c67f374 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 6 Nov 2024 12:01:04 -0500 Subject: [PATCH 1/5] Add class to allow VREDEngine to emit Qt signals. --- python/tk_vred/__init__.py | 1 + python/tk_vred/notifier.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 python/tk_vred/notifier.py diff --git a/python/tk_vred/__init__.py b/python/tk_vred/__init__.py index 163e5b2..bc6c6ec 100644 --- a/python/tk_vred/__init__.py +++ b/python/tk_vred/__init__.py @@ -11,4 +11,5 @@ from .menu_generation import VREDMenuGenerator from .dock_widget import DockWidget from .vred_py import VREDPy +from .notifier import VREDNotifier from .progress_widget import VREDFileIOProgressWidget diff --git a/python/tk_vred/notifier.py b/python/tk_vred/notifier.py new file mode 100644 index 0000000..f69ae3d --- /dev/null +++ b/python/tk_vred/notifier.py @@ -0,0 +1,19 @@ +# Copyright (c) 2024 Autodesk Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the ShotGrid Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the ShotGrid Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Autodesk Inc. + +from sgtk.platform.qt import QtCore + + +class VREDNotifier(QtCore.QObject): + """Class to define Qt signals for VRED events.""" + + # Signal emitted when files have finished importing in VRED. The signal + # carries the result of the import operation (0 for failure, 1 for success) + file_import_finished = QtCore.Signal() From 72a29e5b905ef2e9a93dafab032da4ebd3bd80b2 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 6 Nov 2024 12:01:52 -0500 Subject: [PATCH 2/5] Improve progress widget finish and cleanup --- python/tk_vred/progress_widget.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/python/tk_vred/progress_widget.py b/python/tk_vred/progress_widget.py index a2d0aa3..5e3ffdc 100644 --- a/python/tk_vred/progress_widget.py +++ b/python/tk_vred/progress_widget.py @@ -93,7 +93,7 @@ def __init__( self.__close_button.setToolTip( "Close the dialog. The operation will continue in the background." ) - self.__close_button.clicked.connect(self.accept) + self.__close_button.clicked.connect(self.hide) button_layout = QtGui.QHBoxLayout() button_layout.setContentsMargins(0, 0, 0, 0) @@ -134,8 +134,7 @@ def __init__( def __del__(self): """Destructor.""" - for signal, slot in self.__signal_slots: - signal.disconnect(slot) + self.__cleanup() # -------------------------------------------------------------------------- # Properties @@ -183,21 +182,18 @@ def abort(self): """Abort the operation in progress.""" self.__abort_callback(self.job_id) - self.job_completed(force=True) + self.job_completed(False, force=True) - def job_completed(self, force: bool = False): + def job_completed(self, success: bool, force: bool = False): """Called when a file I/O job has completed.""" self.__completed_jobs += 1 if not force and self.__completed_jobs < self.__total_jobs: return - # If all jobs are complete, destroy the widget. - while self.__signal_slots: - signal, slot = self.__signal_slots.pop() - signal.disconnect(slot) - self.accept() - self.deleteLater() + result = 1 if success else 0 + self.done(result) + self.__cleanup() def is_job_valid(self, job_id: int): """ @@ -211,6 +207,9 @@ def is_job_valid(self, job_id: int): :return: True if the job ID is valid, False otherwise. """ + if job_id is None or self.job_id is None: + return False + if self.job_id == job_id: return True @@ -297,7 +296,7 @@ def on_finish(self, job_id: int, file: str = None, state=None): self.on_failed(job_id, file, "Job finished with an unknown state.") return - self.job_completed() + self.job_completed(True) def on_failed(self, job_id: int, file: str = None, description: str = None): """ @@ -319,7 +318,7 @@ def on_failed(self, job_id: int, file: str = None, description: str = None): f"An error occurred while processing {file}.\n\n{description}", ) - self.job_completed() + self.job_completed(False, force=True) # -------------------------------------------------------------------------- # Private methods @@ -357,3 +356,9 @@ def __setup_default_signals(self): self.vredpy.vrFileIOService.fileLoadingFailed, self.on_failed, ) + + def __cleanup(self): + """Clean up the widget.""" + + for signal, slot in self.__signal_slots: + signal.disconnect(slot) From 157c96cfb4583569dc914b8fd2fe7860c1a15257 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 6 Nov 2024 12:03:45 -0500 Subject: [PATCH 3/5] Update engine to create a notifier object. Update import file method to allow specifying root node and only show progress widget if necessary. --- engine.py | 80 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/engine.py b/engine.py index efd5b2c..3806afb 100644 --- a/engine.py +++ b/engine.py @@ -7,7 +7,7 @@ # agreement to the Shotgun Pipeline Toolkit Source Code License. All rights # not expressly granted therein are reserved by Autodesk Inc. -from typing import List +from typing import List, Optional import logging import os import re @@ -35,6 +35,7 @@ def __init__(self, tk, context, engine_instance_name, env): self._tabbed_dock_widgets = {} self._vredpy = None self.__vred_execpath = os.getenv("TK_VRED_EXECPATH", None) + self.__notifier = None # Set a flag to indicate that the engine has been initialized self.__initialized = False @@ -83,6 +84,11 @@ def vredpy(self): self._vredpy = self._tk_vred.VREDPy() return self._vredpy + @property + def notifier(self): + """Get the Qt notifier object for the engine.""" + return self.__notifier + @property def executable_path(self): """Get the path to the currently running VRED executable.""" @@ -141,6 +147,9 @@ def pre_app_init(self): # import python/tk_vred module self._tk_vred = self.import_module("tk_vred") + # Initialize the notifier + self.__notifier = self._tk_vred.VREDNotifier() + # check for version compatibility self.vred_version = os.getenv("TK_VRED_VERSION", None) vred_major_version = self.vred_version[:4] @@ -908,11 +917,14 @@ def save_current_file(self, file_path, set_render_path=True): if set_render_path: self.set_render_path(file_path) - def import_files(self, paths: List[str]): + def import_files(self, paths: List[str], root_node: Optional[object] = None): """ Import files from the given paths into the VRED scene. :param paths: List of file paths to import. + :param root_node: (Optional) the root VRED scenegraph node (vrdNode) to + place the nodes created on import. Defaults to the root node of the + VRED scenegraph. """ from sgtk.platform.qt import QtGui @@ -920,47 +932,39 @@ def import_files(self, paths: List[str]): if not paths: return - # Import .vpb files separate from other files. On importing .vpb files - # VRED displays a progress bar, for non .vpbs it will not, so we will - # have to show our own in this case. - vpb_file_paths = [] - other_file_paths = [] - for path in paths: - if path.endswith(".vpb"): - vpb_file_paths.append(path) - else: - other_file_paths.append(path) + # Use the given root node, else default to the scene root node + root_node = root_node or self.vredpy.vrScenegraph.getRootNode() - root_node = self.vredpy.vrScenegraph.getRootNode() - - # Import .vpb files first. VRED will show a loading bar for these files - if vpb_file_paths: - self.vredpy.vrFileIOService.importFiles(vpb_file_paths, root_node) + # Check if there are non VRED files, if there are, we will need to show + # the custom progress widget since VRED only shows a progress widget for + # VRED files. + show_progress_widget = any( + not path.endswith(".vpb") and not path.endswith(".osb") for path in paths + ) - # Import any non .vpb files, show our custom progress bar for these files - if other_file_paths: - progress_widget = None - if self.has_ui: - parent = ( - QtGui.QApplication.activeWindow() - or self.__engine._get_dialog_parent() - ) - progress_widget = self._tk_vred.VREDFileIOProgressWidget( - self.vredpy, - len(other_file_paths), - abort_callback=self.vredpy.vrFileIOService.abortImport, - parent=parent, - ) + progress_widget = None + if self.has_ui: + parent = ( + QtGui.QApplication.activeWindow() or self.__engine._get_dialog_parent() + ) + progress_widget = self._tk_vred.VREDFileIOProgressWidget( + self.vredpy, + len(paths), + abort_callback=self.vredpy.vrFileIOService.abortImport, + parent=parent, + ) + progress_widget.set_connection( + progress_widget.finished, self.notifier.file_import_finished + ) + if show_progress_widget: progress_widget.show() - # Start the import operation in VRED async - import_job_id = self.vredpy.vrFileIOService.importFiles( - other_file_paths, root_node - ) + # Start the import operation in VRED async + import_job_id = self.vredpy.vrFileIOService.importFiles(paths, root_node) - # Update the progress widget to track the import job - if progress_widget: - progress_widget.job_id = import_job_id + # Update the progress widget to track the import job + if progress_widget: + progress_widget.job_id = import_job_id def set_render_path(self, file_path=None): """ From 02aeda7e206d62e64a7147e81c588ac526a680f9 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 6 Nov 2024 12:04:14 -0500 Subject: [PATCH 4/5] Add Loader hook action to import a file has an Environment. --- hooks/tk-multi-loader2/basic/scene_actions.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/hooks/tk-multi-loader2/basic/scene_actions.py b/hooks/tk-multi-loader2/basic/scene_actions.py index d518e1e..cca08f7 100644 --- a/hooks/tk-multi-loader2/basic/scene_actions.py +++ b/hooks/tk-multi-loader2/basic/scene_actions.py @@ -105,6 +105,16 @@ def generate_actions(self, sg_publish_data, actions, ui_area): } ) + if "import_as_env" in actions: + action_instances.append( + { + "name": "import_as_env", + "params": None, + "caption": "Import as Environment", + "description": "This will create an enviornment node and add it to the VRED scenegraph under Environments.", + } + ) + if "import_with_options" in actions: if ( self.parent.engine._version_check( @@ -165,6 +175,9 @@ def execute_action(self, name, params, sg_publish_data): elif name == "import": self.import_file(path) + elif name == "import_as_env": + self.import_as_env([path]) + elif name == "import_with_options": self.open_import_dialog(path) @@ -203,6 +216,10 @@ def execute_multiple_actions(self, actions): "paths": [], "func": self.import_files, }, + "import_as_env": { + "paths": [], + "func": self.import_envs, + }, "import_with_options": { "paths": [], "func": self.open_import_batch_dialog, @@ -291,6 +308,63 @@ def create_smart_reference(self, path): finally: QtGui.QApplication.restoreOverrideCursor() + def import_envs(self, paths): + """ + Import the list of files into VRED as environments. + + :param paths: The file paths to import + :type paths: List[str] + """ + + def __on_envs_imported(env_group_node): + """ + Callback triggered after the environments have been imported. + + Move all nodes in the given group node, to the Environments node in + the VRED scenegraph. Delete the temporary group node. + + :param env_group_node: The temporary group node that contains the + imported environments. + """ + if not env_group_node: + return + # Ensure we have a vrdNode object + if isinstance(env_group_node, self.vredpy.vrNodePtr.vrNodePtr): + env_group_node = self.vredpy.vrNodeService.getNodeFromId( + env_group_node.getID() + ) + env_root_node = self.vredpy.vrNodeService.findNode("Environments") + if not env_root_node: + self.logger.error("Could not find Environments node in the scenegraph.") + return + for env_node in env_group_node.children: + env_root_node.children.append(env_node) + self.vredpy.vrNodeService.removeNodes([env_group_node]) + + # Create a temporary group node to place the imported environments. The + # environments are imported async, so this is how we can find them after + # the import is finished. + root_node = self.vredpy.vrScenegraph.getRootNode() + unique_name = self.vredpy.vrNodeService.getUniqueName( + "FPTR_Imported_Envs", root_node + ) + temp_group_node = self.vredpy.vrScenegraph.createNode( + "Group", unique_name, root_node + ) + + # Import the environments from the files + self.parent.engine.import_files(paths, root_node=temp_group_node) + if not self.parent.engine.notifier: + self.logger.error( + "Missing VRED notifier. Environments will not be moved to the Environments node." + ) + return + # Set up signal/slot to move the imported environments to the Environments node + # after the import is finished + self.parent.engine.notifier.file_import_finished.connect( + lambda: __on_envs_imported(temp_group_node) + ) + def import_files(self, paths): """ Import the list of files into VRED. From 444cf105eba53fac93fb4240c7acd2a457e0a8df Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 6 Nov 2024 12:29:12 -0500 Subject: [PATCH 5/5] Fix signal/slot handling --- hooks/tk-multi-loader2/basic/scene_actions.py | 24 ++++++++++++++++--- python/tk_vred/progress_widget.py | 3 ++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/hooks/tk-multi-loader2/basic/scene_actions.py b/hooks/tk-multi-loader2/basic/scene_actions.py index cca08f7..7a561db 100644 --- a/hooks/tk-multi-loader2/basic/scene_actions.py +++ b/hooks/tk-multi-loader2/basic/scene_actions.py @@ -360,9 +360,10 @@ def __on_envs_imported(env_group_node): ) return # Set up signal/slot to move the imported environments to the Environments node - # after the import is finished - self.parent.engine.notifier.file_import_finished.connect( - lambda: __on_envs_imported(temp_group_node) + # after the import is finished. This signal is disconnected after it is triggered. + self.__connect_once( + self.parent.engine.notifier.file_import_finished, + lambda: __on_envs_imported(temp_group_node), ) def import_files(self, paths): @@ -404,3 +405,20 @@ def open_import_dialog(self, path): """ self.open_import_batch_dialog([path]) + + # -------------------------------------------------------------------------- + # Private methods + + def __connect_once(self, signal, slot): + """ + Connect the signal to the slot, but only trigger once. + + :param signal: The signal to connect. + :param slot: The slot to connect. + """ + + def wrapper(*args, **kwargs): + slot(*args, **kwargs) + signal.disconnect(wrapper) + + signal.connect(wrapper) diff --git a/python/tk_vred/progress_widget.py b/python/tk_vred/progress_widget.py index 5e3ffdc..57b8631 100644 --- a/python/tk_vred/progress_widget.py +++ b/python/tk_vred/progress_widget.py @@ -360,5 +360,6 @@ def __setup_default_signals(self): def __cleanup(self): """Clean up the widget.""" - for signal, slot in self.__signal_slots: + while self.__signal_slots: + signal, slot = self.__signal_slots.pop() signal.disconnect(slot)