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(