diff --git a/engine.py b/engine.py index 97dc28de..74785828 100644 --- a/engine.py +++ b/engine.py @@ -191,6 +191,11 @@ def data_validator(self): """Get the AliasDataValidator object to help validate the Alias data.""" return self.__data_validator + @property + def executable_path(self): + """Get the path to the currently running Alias executable.""" + return self.alias_execpath + # ------------------------------------------------------------------------------------------------------- # Override base Engine class methods # ------------------------------------------------------------------------------------------------------- @@ -241,7 +246,7 @@ def post_context_change(self, old_context, new_context): self.logger.debug("%s: Post context change...", self) # Rebuild the menu only if we change of context and if we're running Alias in interactive mode - if self.has_ui: + if self.has_ui and self.__menu_generator: self.__menu_generator.build() def destroy_engine(self): @@ -358,14 +363,25 @@ def restart_process(self): with Alias (e.g. OpenAlias mode). """ - if self.__menu_generator: - self.__menu_generator.clean_menu() + self.logger.info("Restarting the Alias Engine...") - if self.__sio: - self.__sio.emit_threadsafe("restart") - else: + if not self.__sio: raise NotImplementedError() + if self.__menu_generator: + status = self.__menu_generator.remove_menu() + if status == self.alias_py.AlStatusCode.Success.value: + self.logger.debug("Removed ShotGrid menu from Alias successfully.") + elif status == self.alias_py.AlStatusCode.Failure.value: + self.logger.error("Failed to remove ShotGrid menu from Alias") + else: + self.logger.warning( + f"Alias Python API menu.remove() returned non-success status code {status}" + ) + self.__menu_generator = None + + self.__sio.emit_threadsafe("restart") + def shutdown(self): """ Shutdown the application running the engine. @@ -375,6 +391,8 @@ def shutdown(self): on the engine itself). """ + self.logger.info("Shutting down the Alias Engine...") + from sgtk.platform.qt import QtGui qt_app = QtGui.QApplication.instance() or self.__qt_app @@ -474,8 +492,16 @@ def post_qt_init(self): # Check if there is a file set to open on startup path = os.environ.get("SGTK_FILE_TO_OPEN", None) if path: - self.open_file(path) - # clear the env var after loading so that it doesn't get reopened on an engine restart. + if self.__sio: + self.open_file(path) + else: + # Add a timer to delay opening the file for 5 seconds. This is a work around for + # Alias when running SG in the same process (<2024), which is not ready to open + # a file on engine startup. This is not a bullet proof solution, but it should + # work in most cases, and there is not a better alternative to support older + # versions of Alias. + QtCore.QTimer.singleShot(1000 * 5, lambda: self.open_file(path)) + # Clear the env var after loading so that it doesn't get reopened on an engine restart. del os.environ["SGTK_FILE_TO_OPEN"] def save_context_for_stage(self, context=None): @@ -509,9 +535,11 @@ def save_file(self): """ status = self.alias_py.save_file() - if status != self.alias_py.AlStatusCode.Success.value: - self.logger.error( - "Alias Python API Error: save_file returned non-success status code {}".format( + if status == self.alias_py.AlStatusCode.Failure.value: + self.logger.error("Alias Python API save_file failed") + elif status != self.alias_py.AlStatusCode.Success.value: + self.logger.warning( + "Alias Python API save_file returned non-success status code {}".format( status ) ) @@ -529,9 +557,11 @@ def save_file_as(self, path): """ status = self.alias_py.save_file_as(path) - if status != self.alias_py.AlStatusCode.Success.value: - self.logger.error( - "Alias Python API Error: save_file_as('{}') returned non-success status code {}".format( + if status == self.alias_py.AlStatusCode.Failure.value: + self.logger.error("Alias Python API save_file_as failed") + elif status != self.alias_py.AlStatusCode.Success.value: + self.logger.warning( + "Alias Python API save_file_as('{}') returned non-success status code {}".format( path, status ) ) @@ -549,9 +579,11 @@ def open_file(self, path): """ status = self.alias_py.open_file(path) - if status != self.alias_py.AlStatusCode.Success.value: - self.logger.error( - "Alias Python API Error: open_file('{}') returned non-success status code {}".format( + if status == self.alias_py.AlStatusCode.Failure.value: + self.logger.error("Alias Python API open_file failed") + elif status != self.alias_py.AlStatusCode.Success.value: + self.logger.warning( + "Alias Python API open_file('{}') returned non-success status code {}".format( path, status ) ) diff --git a/hooks/menu_customization.py b/hooks/menu_customization.py new file mode 100644 index 00000000..e3e09866 --- /dev/null +++ b/hooks/menu_customization.py @@ -0,0 +1,56 @@ +# Copyright (c) 2023 Autodesk +# +# 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. + +import sgtk + +HookBaseClass = sgtk.get_hook_baseclass() + + +class MenuCustomization(HookBaseClass): + """Hook to allow customizing the ShotGrid menu in Alias.""" + + def sorted_menu_commands(self, commands): + """ + Return the given commands as a list in the order they should be displayed in the menu. + + The menu will display the commands in the order they are returned by this method. the + default implementation will sort the commands alphabetically by command's app name, + then the command name itself. + + The engine commands should be retrieved from the engine `commands` property. Apply + any custom ordering to these commands. + + The commands are returned as a list of tuples, where the first item corresponds to the + command dict entry key, and the second item is the command dict entry value. + + :param commands: The commands to add to the menu. + :type commands: dict + + :return: The list of commands in order they should be displayed in. + :rtype: List[tuple[str, dict]] + """ + + # To display menu commands in the order that they are defined in the config settings, + # uncomment the line below. Note that this requirest Python >= 3.7 because it relies + # on the dictionary preserve their order of insertion. + # return list(commands.items()) + + # Sort by the command app name (if not a context menu command), then the command name. + return sorted( + commands.items(), + key=lambda command: ( + command[1]["properties"]["app"].display_name + if command[1].get("properties", {}).get("app") + and command[1].get("properties", {}).get("type", "default") + != "context_menu" + else "Other Items", + command[0], + ), + ) diff --git a/hooks/tk-multi-publish2/basic/collector.py b/hooks/tk-multi-publish2/basic/collector.py index 678ec29e..43bcdbb1 100644 --- a/hooks/tk-multi-publish2/basic/collector.py +++ b/hooks/tk-multi-publish2/basic/collector.py @@ -9,9 +9,11 @@ # not expressly granted therein are reserved by Shotgun Software Inc. import os +import tempfile import sgtk -import alias_api +from sgtk.platform.qt import QtGui + HookBaseClass = sgtk.get_hook_baseclass() @@ -46,6 +48,11 @@ def settings(self): return collector_settings + @property + def alias_py(self): + """Get the Alias api module.""" + return self.parent.engine.alias_py + def process_current_session(self, settings, parent_item): """ Analyzes the current scene open in a DCC and parents a subtree of items @@ -62,7 +69,7 @@ def process_current_session(self, settings, parent_item): parent_item.properties["bg_processing"] = bg_processing.value # get the path to the current file - path = alias_api.get_current_path() + path = self.alias_py.get_current_path() # determine the display name for the item if path: @@ -80,6 +87,9 @@ def process_current_session(self, settings, parent_item): icon_path = os.path.join(self.disk_location, os.pardir, "icons", "alias.png") session_item.set_icon_from_path(icon_path) + # set the default thumbnail to the current Alias viewport + session_item.thumbnail = self._get_thumbnail_pixmap() + # add a new item for Alias translations to separate them from the main session item translation_item = session_item.create_item( "alias.session.translation", "Alias Translations", "All Alias Translations" @@ -111,3 +121,34 @@ def process_current_session(self, settings, parent_item): vred_item.set_icon_from_path(icon_path) self.logger.info("Collected current Alias file") + + def _get_thumbnail_pixmap(self): + """ + Generate a thumbnail from the current Alias viewport. + + :return: A thumbnail of the current Alias viewport. + :rtype: QtGui.QPixmap + """ + + pixmap = None + thumbnail_path = None + + try: + thumbnail_path = tempfile.NamedTemporaryFile( + suffix=".jpg", prefix="sgtk_thumb", delete=False + ).name + status = self.alias_py.store_current_window(thumbnail_path) + if not self.alias_py.py_utils.is_success(status): + self.logger.warning( + f"Alias API store_current_window returned non-success status code '{status}'" + ) + pixmap = QtGui.QPixmap(thumbnail_path) + except Exception as e: + self.logger.error(f"Failed to set default thumbnail: {e}") + finally: + try: + os.remove(thumbnail_path) + except: + pass + + return pixmap diff --git a/hooks/tk-multi-publish2/basic/publish_translation.py b/hooks/tk-multi-publish2/basic/publish_translation.py index cc062544..80442c92 100644 --- a/hooks/tk-multi-publish2/basic/publish_translation.py +++ b/hooks/tk-multi-publish2/basic/publish_translation.py @@ -269,12 +269,21 @@ def validate(self, settings, item): # if we don't have translator settings, we can't publish if not translator.translation_type: - self.logger.warning("Couldn't find the translation type.") + self.logger.warning( + f"Couldn't find the translation type {translator.translator_type}." + ) return False if not translator.translator_path: - self.logger.warning("Couldn't find translator path.") + self.logger.warning(f"Couldn't determine which translator to use.") + return False + + if not os.path.exists(translator.translator_path): + self.logger.warning( + f"Translator path does not exist {translator.translator_path}." + ) return False + self.logger.info(f"Translator in use: {translator.translator_path}.") # store the licensing information in the item properties so that the translation could be run in # background mode diff --git a/hooks/tk-multi-publish2/basic/upload_version.py b/hooks/tk-multi-publish2/basic/upload_version.py index ab589c1f..51ecc178 100644 --- a/hooks/tk-multi-publish2/basic/upload_version.py +++ b/hooks/tk-multi-publish2/basic/upload_version.py @@ -9,22 +9,15 @@ # not expressly granted therein are reserved by Shotgun Software Inc. import os import shutil +import tempfile + import sgtk HookBaseClass = sgtk.get_hook_baseclass() class UploadVersionPlugin(HookBaseClass): - """ - Plugin for sending quicktimes and images to ShotGrid for review. - """ - - # Translation workers are responsible for performing the LMV translation. - # 'local': a local translator will be used, determined based on file type and current engine - # 'framework': the tk-framework-lmv translator will be used (default) - TRANSLATION_WORKER_LOCAL = "local" - TRANSLATION_WORKER_FRAMEWORK = "framework" - TRANSLATION_WORKERS = [TRANSLATION_WORKER_LOCAL, TRANSLATION_WORKER_FRAMEWORK] + """Plugin for uploading Versions to ShotGrid for review.""" # Version Type string constants VERSION_TYPE_2D = "2D Version" @@ -46,15 +39,14 @@ class UploadVersionPlugin(HookBaseClass): VERSION_TYPE_3D: """ Create a Version in ShotGrid for Review.

A 3D Version (LMV translation of your file/scene's geometry) will be created in ShotGrid. - This Version can then be reviewed via ShotGrid's many review apps. + This Version can then be reviewed via ShotGrid's many review apps.

+ References in your file will not be included in the 3D version. """, } @property def icon(self): - """ - Path to an png icon on disk - """ + """Path to an png icon on disk.""" return os.path.join(self.disk_location, os.pardir, "icons", "review.png") @@ -95,11 +87,6 @@ def settings(self): "default": False, "description": "Upload content to ShotGrid?", }, - "Translation Worker": { - "type": "str", - "default": self.TRANSLATION_WORKER_FRAMEWORK, - "description": "Specify the worker to use to perform LMV translation.", - }, } # update the base settings @@ -158,6 +145,11 @@ def validate(self, settings, item): :returns: True if item is valid, False otherwise. """ + path = item.get_property("path") + if not path: + self.logger.error("No path found for item") + return False + # Validate fails if the Version Type is not supported version_type = settings.get("Version Type").value if version_type not in self.VERSION_TYPE_OPTIONS: @@ -182,20 +174,21 @@ def validate(self, settings, item): "Please contact Autodesk support to have 3D Review enabled on your ShotGrid site or use the 2D Version publish option instead." ) - framework_lmv = self.load_framework("tk-framework-lmv_v0.x.x") - if not framework_lmv: - self.logger.error("Could not run LMV translation: missing ATF framework") - return False + framework_lmv = self.load_framework("tk-framework-lmv_v1.x.x") + if not framework_lmv: + self.logger.error("Missing required framework tk-framework-lmv v1.x.x") + return False - translation_worker = settings.get("Translation Worker").value - if translation_worker not in self.TRANSLATION_WORKERS: - self.logger.error( - "Unknown Translation Worker '{worker}'. Translation worker must be one of {workers}".format( - worker=translation_worker, - workers=", ".join(self.TRANSLATION_WORKERS), - ) + translator = framework_lmv.import_module("translator") + lmv_translator = translator.LMVTranslator( + path, self.parent.sgtk, item.context ) - return False + lmv_translator_path = lmv_translator.get_translator_path() + if not lmv_translator_path: + self.logger.error( + "Missing translator for Alias. Alias must be installed locally to run LMV translation." + ) + return False return True @@ -208,7 +201,7 @@ def publish(self, settings, item): :param item: Item to process """ - # get the publish "mode" stored inside of the root item properties + # Get the publish "mode" stored inside of the root item properties bg_processing = item.parent.properties.get("bg_processing", False) in_bg_process = item.parent.properties.get("in_bg_process", False) @@ -217,95 +210,66 @@ def publish(self, settings, item): publisher = self.parent path = item.properties["path"] - # be sure to strip the extension from the publish name + # Be sure to strip the extension from the publish name path_components = publisher.util.get_file_path_components(path) filename = path_components["filename"] - (publish_name, extension) = os.path.splitext(filename) + (publish_name, _) = os.path.splitext(filename) item.properties["publish_name"] = publish_name - # create the Version in ShotGrid + # Create the Version in ShotGrid super(UploadVersionPlugin, self).publish(settings, item) - # Get the version type to create - version_type = settings.get("Version Type").value - - # generate the Version content: LMV file (for 3D) or simple 2D thumbnail - if version_type == self.VERSION_TYPE_3D: - use_framework_translator = ( - settings.get("Translation Worker").value - == self.TRANSLATION_WORKER_FRAMEWORK + # Generate media content and upload to ShotGrid + version_type = item.properties["sg_version_data"]["type"] + version_id = item.properties["sg_version_data"]["id"] + thumbnail_path = item.get_thumbnail_as_path() + media_package_path = None + media_version_type = settings.get("Version Type").value + if media_version_type == self.VERSION_TYPE_3D: + # Pass the thumbnail retrieved to override the LMV thumbnail, and ignore the + # LMV thumbnail output + media_package_path, _, _ = self._translate_file_to_lmv( + item, thumbnail_path=thumbnail_path ) - self.logger.debug("Creating LMV files from source file") - # translate the file to lmv and upload the corresponding package to the Version - ( - package_path, - thumbnail_path, - output_directory, - ) = self._translate_file_to_lmv(item, use_framework_translator) - self.logger.info("Uploading LMV files to ShotGrid") + self.logger.info("Translated file to LMV") + + if media_package_path: + # For 3D media, a media package path will be generated. Set the translation + # type on the Version in order to view 3D media in ShotGrid Web. self.parent.shotgun.update( - entity_type="Version", - entity_id=item.properties["sg_version_data"]["id"], + entity_type=version_type, + entity_id=version_id, data={"sg_translation_type": "LMV"}, ) + self.logger.info("Set Version translation type to LMV") + + uploaded_movie_path = media_package_path or thumbnail_path + if uploaded_movie_path: + # Uplod to the `sg_uploaded_movie` field on the Version so that the Version + # thumbnail shows the "play" button on hover from ShotGrid Web self.parent.shotgun.upload( - entity_type="Version", - entity_id=item.properties["sg_version_data"]["id"], - path=package_path, + entity_type=version_type, + entity_id=version_id, + path=uploaded_movie_path, field_name="sg_uploaded_movie", ) - # if the Version thumbnail is empty, update it with the newly created thumbnail - if not item.get_thumbnail_as_path() and thumbnail_path: - self.parent.shotgun.upload_thumbnail( - entity_type="Version", - entity_id=item.properties["sg_version_data"]["id"], - path=thumbnail_path, - ) - # delete the temporary folder on disk - self.logger.debug("Deleting temporary folder") - shutil.rmtree(output_directory) - - elif version_type == self.VERSION_TYPE_2D: - thumbnail_path = item.get_thumbnail_as_path() - self.logger.debug("Using thumbnail image as Version media") - if thumbnail_path: - self.parent.shotgun.upload( - entity_type="Version", - entity_id=item.properties["sg_version_data"]["id"], - path=thumbnail_path, - field_name="sg_uploaded_movie", - ) - else: - use_framework_translator = ( - settings.get("Translation Worker").value - == self.TRANSLATION_WORKER_FRAMEWORK - ) - self.logger.debug("Converting file to LMV to extract thumbnails") - output_directory, thumbnail_path = self._get_thumbnail_from_lmv( - item, use_framework_translator - ) - if thumbnail_path: - self.logger.info("Uploading LMV thumbnail file to ShotGrid") - self.parent.shotgun.upload( - entity_type="Version", - entity_id=item.properties["sg_version_data"]["id"], - path=thumbnail_path, - field_name="sg_uploaded_movie", - ) - self.parent.shotgun.upload_thumbnail( - entity_type="Version", - entity_id=item.properties["sg_version_data"]["id"], - path=thumbnail_path, - ) - self.logger.debug("Deleting temporary folder") - shutil.rmtree(output_directory) - else: - raise NotImplementedError( - "Failed to generate thumbnail for Version Type '{}'".format( - version_type - ) + self.logger.info( + f"Uploaded Version media from path {uploaded_movie_path}" + ) + + if thumbnail_path: + self.parent.shotgun.upload_thumbnail( + entity_type=version_type, + entity_id=version_id, + path=thumbnail_path, + ) + self.logger.info( + f"Uploaded Version thumbnail from path {thumbnail_path}" ) + # Remove the temporary directory or files created to generate media content + self._cleanup_temp_files(media_package_path) + def finalize(self, settings, item): """ Execute the finalization pass. This pass executes once all the publish @@ -419,7 +383,6 @@ def get_ui_settings(self, widget, items=None): version_type_combobox = widget.property("version_type_combobox") if version_type_combobox: version_type_index = version_type_combobox.currentIndex() - # if version_type_index >= 0 and version_type_index < len(self.VERSION_TYPE_OPTIONS): if 0 <= version_type_index < len(self.VERSION_TYPE_OPTIONS): self.VERSION_TYPE_OPTIONS[version_type_index] ui_settings["Version Type"] = self.VERSION_TYPE_OPTIONS[ @@ -574,63 +537,73 @@ def _on_version_type_changed(self, version_type, description_label): ############################################################################ # Protected functions - def _translate_file_to_lmv(self, item, use_framework_translator): + def _cleanup_temp_files(self, path, remove_from_root=True): """ - Translate the current Alias file as an LMV package in order to upload it to ShotGrid as a 3D Version + Remove any temporary directories or files from the given path. - :param item: Item to process - :param use_framework_translator: True will force the translator shipped with tk-framework-lmv to be used - :returns: - - The path to the LMV zip file - - The path to the LMV thumbnail - - The path to the temporary folder where the LMV files have been processed + If `remove_from_root` is True, the top most level directory of the given path is + used to remove all sub directories and files. + + :param path: The file path to remove temporary files and/or directories from. + :type path: str + :param remove_from_root: True will remove directories and files from the top most level + directory within the root temporary directory, else False will remove the single + file or directory (and its children). Default is True. + :type remove_from_root: bool """ - framework_lmv = self.load_framework("tk-framework-lmv_v0.x.x") - translator = framework_lmv.import_module("translator") + if path is None or not os.path.exists(path): + return # Cannot clean up a path that does not exist - # translate the file to lmv - lmv_translator = translator.LMVTranslator(item.properties.path) - self.logger.info("Converting file to LMV") - lmv_translator.translate(use_framework_translator=use_framework_translator) + tempdir = tempfile.gettempdir() + if os.path.commonpath([path, tempdir]) != tempdir: + return # Not a temporary directory or file - # package it up - self.logger.info("Packaging LMV files") - package_path, thumbnail_path = lmv_translator.package( - svf_file_name=str(item.properties["sg_version_data"]["id"]), - thumbnail_path=item.get_thumbnail_as_path(), - ) + if remove_from_root: + # Get the top most level of the path that is inside the root temp dir + relative_path = os.path.relpath(path, tempdir) + path = os.path.normpath( + os.path.join(tempdir, relative_path.split(os.path.sep)[0]) + ) - return package_path, thumbnail_path, lmv_translator.output_directory + if os.path.isdir(path): + shutil.rmtree(path) + elif os.path.isfile(path): + os.remove(path) - def _get_thumbnail_from_lmv(self, item, use_framework_translator): + def _translate_file_to_lmv(self, item, thumbnail_path=None): """ - Extract the thumbnail from the source file, using the LMV conversion + Translate the current Alias file as an LMV package in order to upload it to ShotGrid as a 3D Version :param item: Item to process - :param use_framework_translator: True will force the translator shipped with tk-framework-lmv to be used + :type item: PublishItem + :param thumbnail_path: Optionally pass a thumbnail file path to override the LMV + thumbnail (this thumbnail will be included in the LMV packaged zip file). + :type thumbnail_path: str + :returns: - - The path to the temporary folder where the LMV files have been processed + - The path to the LMV zip file - The path to the LMV thumbnail + - The path to the temporary folder where the LMV files have been processed """ - framework_lmv = self.load_framework("tk-framework-lmv_v0.x.x") - translator = framework_lmv.import_module("translator") - - # translate the file to lmv - lmv_translator = translator.LMVTranslator(item.properties.path) - self.logger.info("Converting file to LMV") - lmv_translator.translate(use_framework_translator=use_framework_translator) + path = item.get_property("path") + thumbnail_path = thumbnail_path or item.get_thumbnail_as_path() - self.logger.info("Extracting thumbnails from LMV") - thumbnail_path = lmv_translator.extract_thumbnail() - if not thumbnail_path: - self.logger.warning( - "Couldn't retrieve thumbnail data from LMV. Version won't have any associated media" - ) - return lmv_translator.output_directory + # Translate the file to LMV + framework_lmv = self.load_framework("tk-framework-lmv_v1.x.x") + translator = framework_lmv.import_module("translator") + lmv_translator = translator.LMVTranslator(path, self.parent.sgtk, item.context) + lmv_translator.translate() + + # Package up the LMV files into a zip file + file_name = str(item.properties["sg_version_data"]["id"]) + package_path, lmv_thumbnail_path = lmv_translator.package( + svf_file_name=file_name, + thumbnail_path=thumbnail_path, + ) - return lmv_translator.output_directory, thumbnail_path + return package_path, lmv_thumbnail_path, lmv_translator.output_directory def _is_3d_viewer_enabled(self): """ diff --git a/hooks/tk-multi-shotgunpanel/basic/scene_actions.py b/hooks/tk-multi-shotgunpanel/basic/scene_actions.py index 4d22d612..26f5d64c 100644 --- a/hooks/tk-multi-shotgunpanel/basic/scene_actions.py +++ b/hooks/tk-multi-shotgunpanel/basic/scene_actions.py @@ -15,6 +15,7 @@ import os import sgtk import alias_api +import tempfile HookBaseClass = sgtk.get_hook_baseclass() @@ -117,6 +118,16 @@ def generate_actions(self, sg_data, actions, ui_area): } ) + if "import_note_attachments" in actions: + action_instances.append( + { + "name": "import_note_attachments", + "params": None, + "caption": "Import Note attachment(s) as canvas image(s)", + "description": "This will create a new canvas for each image attached to the note.", + } + ) + if "import_subdiv" in actions: action_instances.append( { @@ -161,6 +172,9 @@ def execute_action(self, name, params, sg_data): path = self.get_publish_path(sg_data) self._create_texture_node(path) + if name == "import_note_attachments": + self._import_note_attachments_as_canvas(sg_data) + elif name == "import_subdiv": path = self.get_publish_path(sg_data) self._import_subdivision(path) @@ -270,6 +284,31 @@ def _create_texture_node(self, path): raise Exception("File not found on disk - '%s'" % path) alias_api.create_texture_node(path, True) + def _import_note_attachments_as_canvas(self, sg_data): + """ + Import the Note attachments as canvas images. + + This will create a new canvas for each image attached to the note. + + :param sg_data: The ShotGrid entity dict for the note. + :type sg_data: dict + """ + + if not sg_data or not sg_data.get("id"): + return + + sg_note = self.parent.shotgun.find_one( + "Note", [["id", "is", sg_data["id"]]], ["attachments"] + ) + if not sg_note: + return + + with tempfile.TemporaryDirectory() as temp_dir: + for attachment in sg_note["attachments"]: + temp_path = os.path.join(temp_dir, attachment["name"]) + self.parent.shotgun.download_attachment(attachment, temp_path) + alias_api.create_texture_node(temp_path) + def _import_subdivision(self, path): """ Import a file as subdivision in the current Alias session. diff --git a/info.yml b/info.yml index 81910a78..fb1c86a6 100644 --- a/info.yml +++ b/info.yml @@ -12,6 +12,11 @@ # expected fields in the configuration file for this engine configuration: + hook_menu_customization: + type: hook + description: "A hook that customizes the ShotGrid menu in Alias." + default_value: "{self}/menu_customization.py" + debug_logging: type: bool description: Controls whether debug messages should be emitted to the logger @@ -78,5 +83,6 @@ requires_shotgun_version: requires_core_version: "v0.19.18" frameworks: - - {"name": "tk-framework-aliastranslations", "version": "v0.2.3"} - - {"name": "tk-framework-alias", "version": "v1.x.x", "minimum_version": "v1.0.1"} + - {"name": "tk-framework-aliastranslations", "version": "v0.x.x", "minimum_version": "v0.2.3"} + - {"name": "tk-framework-alias", "version": "v1.x.x", "minimum_version": "v1.2.0"} + - {"name": "tk-framework-lmv", "version": "v1.x.x"} diff --git a/python/tk_alias/menu_generation.py b/python/tk_alias/menu_generation.py index 44127415..1d794e26 100644 --- a/python/tk_alias/menu_generation.py +++ b/python/tk_alias/menu_generation.py @@ -8,6 +8,7 @@ # agreement to the Shotgun Pipeline Toolkit Source Code License. All rights # not expressly granted therein are reserved by Shotgun Software Inc. +from collections import OrderedDict import os from tank_vendor import six @@ -17,6 +18,8 @@ class AliasMenuGenerator(object): """Menu handling for Alias.""" + MENU_CUSTOMIZATION_HOOK = "hook_menu_customization" + def __init__(self, engine): """ Initializes a new menu generator. @@ -27,6 +30,11 @@ def __init__(self, engine): self.__engine = engine + menu_customization_path = engine.get_setting(self.MENU_CUSTOMIZATION_HOOK) + self.__menu_customization_hook_instance = engine.create_hook_instance( + menu_customization_path + ) + if self._version_check(engine.alias_version, "2024.0") >= 0: self.__menu_name = "ShotGrid" elif self._version_check(engine.alias_version, "2022.2") >= 0: @@ -79,17 +87,19 @@ def build(self): parent=plugin_menu, ) - # Now enumerate all items and create menu objects for them. + # Call the hook to get the engine commands in order. + menu_commands = self.__menu_customization_hook_instance.sorted_menu_commands( + self.engine.commands + ) + + # Convert command dictionaries to list of AppCommand objects menu_items = [] - for (cmd_name, cmd_details) in self.engine.commands.items(): + for (cmd_name, cmd_details) in menu_commands: menu_items.append(AppCommand(cmd_name, cmd_details)) - # Sort list of commands in name order - menu_items.sort(key=lambda x: x.name) - - # Add favourites + # Add favourites in the order that they are defined in the config settings. add_separator = True - for fav in self.engine.get_setting("menu_favourites"): + for fav in self.engine.get_setting("menu_favourites", []): app_instance_name = fav["app_instance"] menu_name = fav["name"] @@ -103,9 +113,9 @@ def build(self): # Only add a separator for the first menu item add_separator = False - # Go through all of the menu items. - # Separate them out into various sections. - commands_by_app = {} + # Add the rest of the menu commands. Use an OrderedDict to ensure the ordering of menu + # command is preservered (for Python < 3.7) + commands_by_app = OrderedDict() add_separator = True for cmd in menu_items: @@ -127,6 +137,15 @@ def build(self): # add all the apps to the main menu self._add_apps_to_menu(commands_by_app) + def remove_menu(self): + """Remove the ShotGrid menu from Alias. The menu will be destroyed.""" + + if not self.alias_menu: + return self.engine.alias_py.AlStatusCode.InvalidObject + status = self.alias_menu.remove() + self.__alias_menu = None + return status + def clean_menu(self): """Clean the ShotGrid menu in Alias by removing all its entries.""" @@ -164,7 +183,7 @@ def _add_apps_to_menu(self, commands_by_app): """ add_separator = True - for app_name in sorted(commands_by_app.keys()): + for app_name in commands_by_app: if len(commands_by_app[app_name]) > 1: # more than one menu entry fort his app # make a sub menu and put all items in the sub menu diff --git a/startup.py b/startup.py index 6cd23da3..f1ec2d88 100644 --- a/startup.py +++ b/startup.py @@ -260,8 +260,16 @@ def _get_release_version(self, exec_path, code_name): os.path.dirname(alias_bindir), "resources", "AboutBox.txt" ) - with open(about_box_file, "r") as f: - about_box_file_first_line = f.readline().split("\r")[0].strip() + try: + # First try to read the file with utf-8 encoding. Any errors will be replaced with + # the Unicode replacement. + with open(about_box_file, "r", encoding="utf-8", errors="replace") as f: + about_box_file_first_line = f.readline().split("\r")[0].strip() + except UnicodeDecodeError: + # Fallback to trying to read the file with the latin-1 encoding. This encoding is + # more lenient. + with open(about_box_file, "r", encoding="latin-1") as f: + about_box_file_first_line = f.readline().split("\r")[0].strip() release_prefix = "Alias " + code_name releases = about_box_file_first_line.strip().split(",") @@ -401,8 +409,10 @@ def __ensure_plugin_ready(self, framework_location, alias_version, alias_exec_pa # Get the pipeline config id engine = sgtk.platform.current_engine() pipeline_config_id = engine.sgtk.pipeline_configuration.get_shotgun_id() - entity_type = self.context.project["type"] - entity_id = self.context.project["id"] + # Get the entity from the current context + entity_dict = self.context.task or self.context.entity or self.context.project + entity_type = entity_dict["type"] + entity_id = entity_dict["id"] # Ensure the basic.alias plugin is installed and up to date return startup_utils.ensure_plugin_ready(