From 1b9bb2a98b0d33c6df82239d2fe28dfb02301095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Deverly?= <39291844+gplsteph@users.noreply.github.com> Date: Tue, 7 Dec 2021 18:03:58 +0100 Subject: [PATCH 1/3] 6648 basic config (#7) Allowed the publish hooks to be used with a basic config and only a publish template. Added some UI to choose the root of publishes if the publish template is a string template. Allowed settings to be saved and restored. --- .../tk-multi-publish2/basic/publish_asset.py | 187 +++++++++++++++++- .../tk-multi-publish2/basic/publish_movie.py | 134 ++++++++++++- info.yml | 1 + 3 files changed, 304 insertions(+), 18 deletions(-) diff --git a/hooks/tk-multi-publish2/basic/publish_asset.py b/hooks/tk-multi-publish2/basic/publish_asset.py index ba7df2d..6085010 100644 --- a/hooks/tk-multi-publish2/basic/publish_asset.py +++ b/hooks/tk-multi-publish2/basic/publish_asset.py @@ -4,9 +4,20 @@ import sgtk import os +import sys import unreal import datetime + +# Local storage path field for known Oses. +_OS_LOCAL_STORAGE_PATH_FIELD = { + "darwin": "mac_path", + "win32": "windows_path", + "linux": "linux_path", + "linux2": "linux_path", +}[sys.platform] + + HookBaseClass = sgtk.get_hook_baseclass() @@ -70,7 +81,12 @@ def settings(self): "description": "Template path for published work files. Should" "correspond to a template defined in " "templates.yml.", - } + }, + "Publish Folder": { + "type": "string", + "default": None, + "description": "Optional folder to use as a root for publishes" + }, } # update the base settings @@ -89,6 +105,151 @@ def item_filters(self): """ return ["unreal.asset.StaticMesh"] + def create_settings_widget(self, parent): + """ + Creates a Qt widget, for the supplied parent widget (a container widget + on the right side of the publish UI). + + :param parent: The parent to use for the widget being created. + :returns: A :class:`QtGui.QFrame` that displays editable widgets for + modifying the plugin's settings. + """ + # defer Qt-related imports + from sgtk.platform.qt import QtGui, QtCore + + # Create a QFrame with all our widgets + settings_frame = QtGui.QFrame(parent) + # Create our widgets, we add them as properties on the QFrame so we can + # retrieve them easily. Qt uses camelCase so our xxxx_xxxx names can't + # clash with existing Qt properties. + + # Show this plugin description + settings_frame.description_label = QtGui.QLabel(self.description) + settings_frame.description_label.setWordWrap(True) + settings_frame.description_label.setOpenExternalLinks(True) + settings_frame.description_label.setTextFormat(QtCore.Qt.RichText) + + # Unreal setttings + settings_frame.unreal_publish_folder_label = QtGui.QLabel("Publish folder:") + storage_roots = self.parent.shotgun.find( + "LocalStorage", + [], + ["code", _OS_LOCAL_STORAGE_PATH_FIELD] + ) + settings_frame.storage_roots_widget = QtGui.QComboBox() + settings_frame.storage_roots_widget.addItem("Current Unreal Project") + for storage_root in storage_roots: + if storage_root[_OS_LOCAL_STORAGE_PATH_FIELD]: + settings_frame.storage_roots_widget.addItem( + "%s (%s)" % ( + storage_root["code"], + storage_root[_OS_LOCAL_STORAGE_PATH_FIELD] + ), + userData=storage_root, + ) + # Create the layout to use within the QFrame + settings_layout = QtGui.QVBoxLayout() + settings_layout.addWidget(settings_frame.description_label) + settings_layout.addWidget(settings_frame.unreal_publish_folder_label) + settings_layout.addWidget(settings_frame.storage_roots_widget) + + settings_layout.addStretch() + settings_frame.setLayout(settings_layout) + return settings_frame + + def get_ui_settings(self, widget): + """ + Method called by the publisher to retrieve setting values from the UI. + + :returns: A dictionary with setting values. + """ + # defer Qt-related imports + from sgtk.platform.qt import QtCore + + self.logger.info("Getting settings from UI") + + # Please note that we don't have to return all settings here, just the + # settings which are editable in the UI. + storage_index = widget.storage_roots_widget.currentIndex() + publish_folder = None + if storage_index > 0: # Something selected and not the first entry + storage = widget.storage_roots_widget.itemData(storage_index, role=QtCore.Qt.UserRole) + publish_folder = storage[_OS_LOCAL_STORAGE_PATH_FIELD] + + settings = { + "Publish Folder": publish_folder, + } + return settings + + def set_ui_settings(self, widget, settings): + """ + Method called by the publisher to populate the UI with the setting values. + + :param widget: A QFrame we created in `create_settings_widget`. + :param settings: A list of dictionaries. + :raises NotImplementedError: if editing multiple items. + """ + # defer Qt-related imports + from sgtk.platform.qt import QtCore + + self.logger.info("Setting UI settings") + if len(settings) > 1: + # We do not allow editing multiple items + raise NotImplementedError + cur_settings = settings[0] + # Note: the template is validated in the accept method, no need to check it here. + publish_template_setting = cur_settings.get("Publish Template") + publisher = self.parent + publish_template = publisher.get_template_by_name(publish_template_setting) + if isinstance(publish_template, sgtk.TemplatePath): + widget.unreal_publish_folder_label.setEnabled(False) + widget.storage_roots_widget.setEnabled(False) + folder_index = 0 + publish_folder = cur_settings["Publish Folder"] + if publish_folder: + for i in range(widget.storage_roots_widget.count()): + data = widget.storage_roots_widget.itemData(i, role=QtCore.Qt.UserRole) + if data and data[_OS_LOCAL_STORAGE_PATH_FIELD] == publish_folder: + folder_index = i + break + self.logger.debug("Index for %s is %s" % (publish_folder, folder_index)) + widget.storage_roots_widget.setCurrentIndex(folder_index) + + def load_saved_ui_settings(self, settings): + """ + Load saved settings and update the given settings dictionary with them. + + :param settings: A dictionary where keys are settings names and + values Settings instances. + """ + # Retrieve SG utils framework settings module and instantiate a manager + fw = self.load_framework("tk-framework-shotgunutils_v5.x.x") + module = fw.import_module("settings") + settings_manager = module.UserSettings(self.parent) + + # Retrieve saved settings + settings["Publish Folder"].value = settings_manager.retrieve( + "publish2.publish_folder", + settings["Publish Folder"].value, + settings_manager.SCOPE_PROJECT + ) + self.logger.debug("Loaded settings %s" % settings["Publish Folder"]) + + def save_ui_settings(self, settings): + """ + Save UI settings. + + :param settings: A dictionary of Settings instances. + """ + # Retrieve SG utils framework settings module and instantiate a manager + fw = self.load_framework("tk-framework-shotgunutils_v5.x.x") + module = fw.import_module("settings") + settings_manager = module.UserSettings(self.parent) + + # Save settings + publish_folder = settings["Publish Folder"].value + settings_manager.store("publish2.publish_folder", publish_folder, settings_manager.SCOPE_PROJECT) + def accept(self, settings, item): """ Method called by the publisher to determine if an item is of any @@ -132,6 +293,7 @@ def accept(self, settings, item): # for use in subsequent methods item.properties["publish_template"] = publish_template + self.load_saved_ui_settings(settings) return { "accepted": accepted, "checked": True @@ -172,10 +334,7 @@ def validate(self, settings, item): self.logger.debug("Asset path or name not configured.") return False - publish_template = item.properties.get("publish_template") - if not publish_template: - self.logger.debug("No publish template configured.") - return False + publish_template = item.properties["publish_template"] # Add the Unreal asset name to the fields fields = {"name": asset_name} @@ -194,10 +353,22 @@ def validate(self, settings, item): # which should be project root + publish template publish_path = publish_template.apply_fields(fields) publish_path = os.path.normpath(publish_path) + if not os.path.isabs(publish_path): + # If the path is not absolute, prepend the publish folder setting. + publish_folder = settings["Publish Folder"].value + if not publish_folder: + publish_folder = unreal.Paths.project_saved_dir() + publish_path = os.path.abspath( + os.path.join( + publish_folder, + publish_path + ) + ) + item.properties["publish_path"] = publish_path item.properties["path"] = publish_path - # Remove the filename from the work path - destination_path = os.path.split(publish_path)[0] + # Remove the filename from the publish path + destination_path = os.path.dirname(publish_path) # Stash the destination path in properties item.properties["destination_path"] = destination_path @@ -207,7 +378,7 @@ def validate(self, settings, item): # run the base class validation # return super(UnrealAssetPublishPlugin, self).validate(settings, item) - + self.save_ui_settings(settings) return True def publish(self, settings, item): diff --git a/hooks/tk-multi-publish2/basic/publish_movie.py b/hooks/tk-multi-publish2/basic/publish_movie.py index 8d14517..b207178 100644 --- a/hooks/tk-multi-publish2/basic/publish_movie.py +++ b/hooks/tk-multi-publish2/basic/publish_movie.py @@ -14,6 +14,13 @@ import sys import tempfile +# Local storage path field for known Oses. +_OS_LOCAL_STORAGE_PATH_FIELD = { + "darwin": "mac_path", + "win32": "windows_path", + "linux": "linux_path", + "linux2": "linux_path", +}[sys.platform] HookBaseClass = sgtk.get_hook_baseclass() @@ -89,6 +96,11 @@ def settings(self): "default": None, "description": "Optional Unreal Path to saved presets " "for rendering with the Movie Render Queue" + }, + "Publish Folder": { + "type": "string", + "default": None, + "description": "Optional folder to use as a root for publishes" } } @@ -139,11 +151,31 @@ def create_settings_widget(self, parent): presets_folder = unreal.MovieRenderPipelineProjectSettings().preset_save_dir for preset in unreal.EditorAssetLibrary.list_assets(presets_folder.path): settings_frame.unreal_render_presets_widget.addItem(preset.split(".")[0]) + + settings_frame.unreal_publish_folder_label = QtGui.QLabel("Publish folder:") + storage_roots = self.parent.shotgun.find( + "LocalStorage", + [], + ["code", _OS_LOCAL_STORAGE_PATH_FIELD] + ) + settings_frame.storage_roots_widget = QtGui.QComboBox() + settings_frame.storage_roots_widget.addItem("Current Unreal Project") + for storage_root in storage_roots: + if storage_root[_OS_LOCAL_STORAGE_PATH_FIELD]: + settings_frame.storage_roots_widget.addItem( + "%s (%s)" % ( + storage_root["code"], + storage_root[_OS_LOCAL_STORAGE_PATH_FIELD] + ), + userData=storage_root, + ) # Create the layout to use within the QFrame settings_layout = QtGui.QVBoxLayout() settings_layout.addWidget(settings_frame.description_label) settings_layout.addWidget(settings_frame.unreal_render_presets_label) settings_layout.addWidget(settings_frame.unreal_render_presets_widget) + settings_layout.addWidget(settings_frame.unreal_publish_folder_label) + settings_layout.addWidget(settings_frame.storage_roots_widget) settings_layout.addStretch() settings_frame.setLayout(settings_layout) @@ -155,6 +187,9 @@ def get_ui_settings(self, widget): :returns: A dictionary with setting values. """ + # defer Qt-related imports + from sgtk.platform.qt import QtCore + self.logger.info("Getting settings from UI") # Please note that we don't have to return all settings here, just the @@ -162,8 +197,15 @@ def get_ui_settings(self, widget): render_presets_path = None if widget.unreal_render_presets_widget.currentIndex() > 0: # First entry is "No Presets" render_presets_path = six.ensure_str(widget.unreal_render_presets_widget.currentText()) + storage_index = widget.storage_roots_widget.currentIndex() + publish_folder = None + if storage_index > 0: # Something selected and not the first entry + storage = widget.storage_roots_widget.itemData(storage_index, role=QtCore.Qt.UserRole) + publish_folder = storage[_OS_LOCAL_STORAGE_PATH_FIELD] + settings = { "Movie Render Queue Presets Path": render_presets_path, + "Publish Folder": publish_folder, } return settings @@ -175,6 +217,9 @@ def set_ui_settings(self, widget, settings): :param settings: A list of dictionaries. :raises NotImplementedError: if editing multiple items. """ + # defer Qt-related imports + from sgtk.platform.qt import QtCore + self.logger.info("Setting UI settings") if len(settings) > 1: # We do not allow editing multiple items @@ -186,6 +231,66 @@ def set_ui_settings(self, widget, settings): preset_index = widget.unreal_render_presets_widget.findText(render_presets_path) self.logger.info("Index for %s is %s" % (render_presets_path, preset_index)) widget.unreal_render_presets_widget.setCurrentIndex(preset_index) + # Note: the template is validated in the accept method, no need to check it here. + publish_template_setting = cur_settings.get("Publish Template") + publisher = self.parent + publish_template = publisher.get_template_by_name(publish_template_setting) + if isinstance(publish_template, sgtk.TemplatePath): + widget.unreal_publish_folder_label.setEnabled(False) + widget.storage_roots_widget.setEnabled(False) + folder_index = 0 + publish_folder = cur_settings["Publish Folder"] + if publish_folder: + for i in range(widget.storage_roots_widget.count()): + data = widget.storage_roots_widget.itemData(i, role=QtCore.Qt.UserRole) + if data and data[_OS_LOCAL_STORAGE_PATH_FIELD] == publish_folder: + folder_index = i + break + self.logger.debug("Index for %s is %s" % (publish_folder, folder_index)) + widget.storage_roots_widget.setCurrentIndex(folder_index) + + def load_saved_ui_settings(self, settings): + """ + Load saved settings and update the given settings dictionary with them. + + :param settings: A dictionary where keys are settings names and + values Settings instances. + """ + # Retrieve SG utils framework settings module and instantiate a manager + fw = self.load_framework("tk-framework-shotgunutils_v5.x.x") + module = fw.import_module("settings") + settings_manager = module.UserSettings(self.parent) + + # Retrieve saved settings + settings["Movie Render Queue Presets Path"].value = settings_manager.retrieve( + "publish2.movie_render_queue_presets_path", + settings["Movie Render Queue Presets Path"].value, + settings_manager.SCOPE_PROJECT, + ) + settings["Publish Folder"].value = settings_manager.retrieve( + "publish2.publish_folder", + settings["Publish Folder"].value, + settings_manager.SCOPE_PROJECT + ) + self.logger.debug("Loaded settings %s" % settings["Publish Folder"]) + self.logger.debug("Loaded settings %s" % settings["Movie Render Queue Presets Path"]) + + def save_ui_settings(self, settings): + """ + Save UI settings. + + :param settings: A dictionary of Settings instances. + """ + # Retrieve SG utils framework settings module and instantiate a manager + fw = self.load_framework("tk-framework-shotgunutils_v5.x.x") + module = fw.import_module("settings") + settings_manager = module.UserSettings(self.parent) + + # Save settings + render_presets_path = settings["Movie Render Queue Presets Path"].value + settings_manager.store("publish2.movie_render_queue_presets_path", render_presets_path, settings_manager.SCOPE_PROJECT) + publish_folder = settings["Publish Folder"].value + settings_manager.store("publish2.publish_folder", publish_folder, settings_manager.SCOPE_PROJECT) def accept(self, settings, item): """ @@ -222,14 +327,14 @@ def accept(self, settings, item): if not publish_template: self.logger.debug( "A publish template could not be determined for the " - "sequence item. Not accepting the item." + "item. Not accepting the item." ) accepted = False # we've validated the work and publish templates. add them to the item properties # for use in subsequent methods item.properties["publish_template"] = publish_template - + self.load_saved_ui_settings(settings) return { "accepted": accepted, "checked": True @@ -271,7 +376,7 @@ def validate(self, settings, item): return False # Get the configured publish template - publish_template = item.properties.get("publish_template") + publish_template = item.properties["publish_template"] # Get the context from the Publisher UI context = item.context @@ -359,11 +464,23 @@ def validate(self, settings, item): self.logger.error(error_msg) raise ValueError(error_msg) - item.properties["path"] = publish_template.apply_fields(fields) - item.properties["publish_path"] = item.properties["path"] + publish_path = publish_template.apply_fields(fields) + if not os.path.isabs(publish_path): + # If the path is not absolute, prepend the publish folder setting. + publish_folder = settings["Publish Folder"].value + if not publish_folder: + publish_folder = unreal.Paths.project_saved_dir() + publish_path = os.path.abspath( + os.path.join( + publish_folder, + publish_path + ) + ) + item.properties["path"] = publish_path + item.properties["publish_path"] = publish_path item.properties["publish_type"] = "Unreal Render" item.properties["version_number"] = version_number - + self.save_ui_settings(settings) return True def _check_render_settings(self, render_config): @@ -404,8 +521,7 @@ def publish(self, settings, item): # let the base class register the publish - publish_path = item.properties.get("path") - publish_path = os.path.normpath(publish_path) + publish_path = os.path.normpath(item.properties["publish_path"]) # Split the destination path into folder and filename destination_folder, movie_name = os.path.split(publish_path) @@ -503,8 +619,6 @@ def finalize(self, settings, item): # do the base class finalization super(UnrealMoviePublishPlugin, self).finalize(settings, item) - pass - def _get_version_entity(self, item): """ Returns the best entity to link the version to. diff --git a/info.yml b/info.yml index 577bbd1..cd74090 100644 --- a/info.yml +++ b/info.yml @@ -35,4 +35,5 @@ requires_core_version: "v0.18.8" frameworks: - {"name": "tk-framework-unrealqt", "version": "v1.x.x"} + - {"name": "tk-framework-shotgunutils", "version": "v5.x.x"} From 14eab3f0137efac08f5545a2d15549ccaab283c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Deverly?= <39291844+gplsteph@users.noreply.github.com> Date: Tue, 11 Jan 2022 09:47:03 +0100 Subject: [PATCH 2/3] 7504 event loop (#8) - Added a workaround for processing Qt events on other platforms than Windows, which does not need it. - Fixed legacy Sequencer render command line to make it cross platform: spaces are perfectly well handled by subprocess.call without any need to protect the strings, if using a list for the call. - Fixed UE5.0EA discovery for other platforms than Windows. --- engine.py | 22 +++- .../tk-multi-publish2/basic/publish_movie.py | 106 ++++++++++-------- startup.py | 12 +- 3 files changed, 85 insertions(+), 55 deletions(-) diff --git a/engine.py b/engine.py index 36c6910..ca8ed09 100644 --- a/engine.py +++ b/engine.py @@ -106,19 +106,36 @@ def init_engine(self): def init_qt_app(self): self.logger.debug("%s: Initializing QtApp for Unreal", self) - from sgtk.platform.qt5 import QtWidgets if not QtWidgets.QApplication.instance(): self._qt_app = QtWidgets.QApplication(sys.argv) self._qt_app.setQuitOnLastWindowClosed(False) - unreal.log("Created QApplication instance: {0}".format(self._qt_app)) else: self._qt_app = QtWidgets.QApplication.instance() + # On other platforms than Windows, we need to process the Qt events otherwise + # UIs are "frozen". We use a slate tick callback to do that on a regular basis. + # It is not clear why this is not needed on Windows, possibly because a + # dedicated Windows event dispatcher is used instead of a regular + # QAbstractEventDispatcher + if sys.platform != "win32": + unreal.register_slate_post_tick_callback(self._process_qt_events_cb) # Make the QApplication use the dark theme. Must be called after the QApplication is instantiated self._initialize_dark_look_and_feel() + @staticmethod + def _process_qt_events_cb(delta_time): + """ + An Unreal tick callback to process QT events. + + :param float delta_time: delta time since the last run. + """ + from sgtk.platform.qt5 import QtWidgets + qapp = QtWidgets.QApplication.instance() + if qapp: + qapp.processEvents() + def post_app_init(self): """ Called when all apps have initialized @@ -256,7 +273,6 @@ def _create_dialog(self, title, bundle, widget, parent): "icon_256.png")) dialog.setWindowIcon(QtGui.QIcon(unreal_icon)) - return dialog def _define_qt_base(self): diff --git a/hooks/tk-multi-publish2/basic/publish_movie.py b/hooks/tk-multi-publish2/basic/publish_movie.py index b207178..597f229 100644 --- a/hooks/tk-multi-publish2/basic/publish_movie.py +++ b/hooks/tk-multi-publish2/basic/publish_movie.py @@ -541,11 +541,23 @@ def publish(self, settings, item): self.logger.info("Rendering %s with the Movie Render Queue with %s presets." % (publish_path, presets.get_name())) else: self.logger.info("Rendering %s with the Movie Render Queue." % publish_path) - self._unreal_render_sequence_with_movie_queue(publish_path, unreal_map_path, unreal_asset_path, presets) + res, _ = self._unreal_render_sequence_with_movie_queue( + publish_path, + unreal_map_path, + unreal_asset_path, + presets + ) else: self.logger.info("Rendering %s with the Level Sequencer." % publish_path) - self._unreal_render_sequence_with_sequencer(publish_path, unreal_map_path, unreal_asset_path) - + res, _ = self._unreal_render_sequence_with_sequencer( + publish_path, + unreal_map_path, + unreal_asset_path + ) + if not res: + raise RuntimeError( + "Unable to render %s" % publish_path + ) # Increment the version number self._unreal_asset_set_version(unreal_asset_path, item.properties["version_number"]) @@ -696,50 +708,47 @@ def _unreal_render_sequence_with_sequencer(self, output_path, unreal_map_path, s return False, None # Render the sequence to a movie file using the following command-line arguments - cmdline_args = [] - - # Note that any command-line arguments (usually paths) that could contain spaces must be enclosed between quotes - unreal_exec_path = '"{}"'.format(sys.executable) - - # Get the Unreal project to load - unreal_project_filename = "{}.uproject".format(unreal.SystemLibrary.get_game_name()) - unreal_project_path = os.path.join(unreal.SystemLibrary.get_project_directory(), unreal_project_filename) - unreal_project_path = '"{}"'.format(unreal_project_path) - - # Important to keep the order for these arguments - cmdline_args.append(unreal_exec_path) # Unreal executable path - cmdline_args.append(unreal_project_path) # Unreal project - cmdline_args.append(unreal_map_path) # Level to load for rendering the sequence - - # Command-line arguments for Sequencer Render to Movie - # See: https://docs.unrealengine.com/en-us/Engine/Sequencer/Workflow/RenderingCmdLine - sequence_path = "-LevelSequence={}".format(sequence_path) - cmdline_args.append(sequence_path) # The sequence to render - - output_path = '-MovieFolder="{}"'.format(output_folder) - cmdline_args.append(output_path) # output folder, must match the work template - - movie_name_arg = "-MovieName={}".format(movie_name) - cmdline_args.append(movie_name_arg) # output filename - - cmdline_args.append("-game") - cmdline_args.append("-MovieSceneCaptureType=/Script/MovieSceneCapture.AutomatedLevelSequenceCapture") - cmdline_args.append("-ResX=1280") - cmdline_args.append("-ResY=720") - cmdline_args.append("-ForceRes") - cmdline_args.append("-Windowed") - cmdline_args.append("-MovieCinematicMode=yes") - cmdline_args.append("-MovieFormat=Video") - cmdline_args.append("-MovieFrameRate=24") - cmdline_args.append("-MovieQuality=75") - cmdline_args.append("-NoTextureStreaming") - cmdline_args.append("-NoLoadingScreen") - cmdline_args.append("-NoScreenMessages") - - unreal.log("Sequencer command-line arguments: {}".format(cmdline_args)) - - # Send the arguments as a single string because some arguments could contain spaces and we don't want those to be quoted - subprocess.call(" ".join(cmdline_args)) + cmdline_args = [ + sys.executable, # Unreal executable path + "%s" % os.path.join( + unreal.SystemLibrary.get_project_directory(), + "%s.uproject" % unreal.SystemLibrary.get_game_name(), + ), # Unreal project + unreal_map_path, # Level to load for rendering the sequence + # Command-line arguments for Sequencer Render to Movie + # See: https://docs.unrealengine.com/en-us/Engine/Sequencer/Workflow/RenderingCmdLine + # + "-LevelSequence=%s" % sequence_path, # The sequence to render + "-MovieFolder=%s" % output_folder, # Output folder, must match the work template + "-MovieName=%s" % movie_name, # Output filename + "-game", + "-MovieSceneCaptureType=/Script/MovieSceneCapture.AutomatedLevelSequenceCapture", + "-ResX=1280", + "-ResY=720", + "-ForceRes", + "-Windowed", + "-MovieCinematicMode=yes", + "-MovieFormat=Video", + "-MovieFrameRate=24", + "-MovieQuality=75", + "-NoTextureStreaming", + "-NoLoadingScreen", + "-NoScreenMessages", + ] + + unreal.log( + "Sequencer command-line arguments: {}".format( + " ".join(cmdline_args) + ) + ) + + # Make a shallow copy of the current environment and clear some variables + run_env = copy.copy(os.environ) + # Prevent SG TK to try to bootstrap in the new process + if "UE_SHOTGUN_BOOTSTRAP" in run_env: + del run_env["UE_SHOTGUN_BOOTSTRAP"] + + subprocess.call(cmdline_args, env=run_env) return os.path.isfile(output_path), output_path @@ -773,6 +782,9 @@ def _unreal_render_sequence_with_movie_queue(self, output_path, unreal_map_path, output_setting.output_resolution = unreal.IntPoint(1280, 720) output_setting.file_name_format = movie_name output_setting.override_existing_output = True # Overwrite existing files + # If needed we could enforce a frame rate, like for the Sequencer code. + # output_setting.output_frame_rate = unreal.FrameRate(24) + # output_setting.use_custom_frame_rate = True # Remove problematic settings for setting, reason in self._check_render_settings(config): self.logger.warning("Disabling %s: %s." % (setting.get_name(), reason)) diff --git a/startup.py b/startup.py index 1728b95..466c43f 100644 --- a/startup.py +++ b/startup.py @@ -21,21 +21,23 @@ class EngineLauncher(SoftwareLauncher): # matching against supplied versions and products. Similar to the glob # strings, these allow us to alter the regex matching for any of the # variable components of the path in one place - COMPONENT_REGEX_LOOKUP = {"version": r"\d+\.\d+", "major": r"\d+"} + # We match 4.27, 5.0EA + COMPONENT_REGEX_LOOKUP = {"version": r"\d+\.\d+\w*", "major": r"\d+"} # This dictionary defines a list of executable template strings for each # of the supported operating systems. The templates are used for both # globbing and regex matches by replacing the named format placeholders # with an appropriate glob or regex string. - # Note: Windows is handled differently by checking registries and Linux is - # not yet supported. + # Note: Software entities need to be manually added by users in SG for Linux + # since there is no standard installation path. EXECUTABLE_TEMPLATES = { "darwin": [ - "/Users/Shared/Epic Games/UE_{version}/Engine/Binaries/Mac/UE{major}Editor.app" + "/Users/Shared/Epic Games/UE_{version}/Engine/Binaries/Mac/UE{major}Editor.app", + "/Users/Shared/Epic Games/UE_{version}/Engine/Binaries/Mac/UnrealEditor.app" ], "win32": [ "C:/Program Files/Epic Games/UE_{version}/Engine/Binaries/Win64/UE{major}Editor.exe", - "C:/Program Files/Epic Games/UE_{version}EA/Engine/Binaries/Win64/UnrealEditor.exe" + "C:/Program Files/Epic Games/UE_{version}/Engine/Binaries/Win64/UnrealEditor.exe" ], } From 3086cbb801f34e32ba0129edd02b21d9fe1dfecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Deverly?= <39291844+gplsteph@users.noreply.github.com> Date: Wed, 12 Jan 2022 09:42:13 +0100 Subject: [PATCH 3/3] 7675 linux polish (#9) Disabled things which make Unreal crash or which are not fully working. --- engine.py | 10 ++++++---- .../tk-multi-publish2/basic/publish_movie.py | 13 +++++++++++-- startup.py | 19 +++++++++++++------ 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/engine.py b/engine.py index ca8ed09..99a6ed8 100644 --- a/engine.py +++ b/engine.py @@ -268,10 +268,12 @@ def _create_dialog(self, title, bundle, widget, parent): from sgtk.platform.qt import QtGui - unreal_icon = os.path.realpath(os.path.join( - os.path.dirname(__file__), - "icon_256.png")) - + unreal_icon = os.path.realpath( + os.path.join( + os.path.dirname(__file__), + "icon_256.png" + ) + ) dialog.setWindowIcon(QtGui.QIcon(unreal_icon)) return dialog diff --git a/hooks/tk-multi-publish2/basic/publish_movie.py b/hooks/tk-multi-publish2/basic/publish_movie.py index 597f229..25ef3ea 100644 --- a/hooks/tk-multi-publish2/basic/publish_movie.py +++ b/hooks/tk-multi-publish2/basic/publish_movie.py @@ -319,8 +319,17 @@ def accept(self, settings, item): """ accepted = True - publisher = self.parent + checked = True + if sys.platform != "win32": + self.logger.warning( + "Movie publishing is not supported on other platforms than Windows..." + ) + return { + "accepted": False, + } + + publisher = self.parent # ensure the publish template is defined publish_template_setting = settings.get("Publish Template") publish_template = publisher.get_template_by_name(publish_template_setting.value) @@ -337,7 +346,7 @@ def accept(self, settings, item): self.load_saved_ui_settings(settings) return { "accepted": accepted, - "checked": True + "checked": checked } def validate(self, settings, item): diff --git a/startup.py b/startup.py index 466c43f..01e4ee4 100644 --- a/startup.py +++ b/startup.py @@ -61,7 +61,20 @@ def prepare_launch(self, exec_path, args, file_to_open=None): :returns: A :class:`LaunchInformation` instance. """ + # TODO: Get the startup project from settings somewhere, for now hardcoded + # Otherwise, leave it empty and the project selection window will appear + # (the command-line arguments are forwarded to the new instance of Unreal Editor) + unreal_project = "" + args = args + unreal_project required_env = {} + # Creating a QApplication in Unreal on Mac crashes it, so disable the + # SG TK integration for now, just run the executable with its args. This + # allows to launch Unreal from tk-desktop or submit turntable renders + # from Maya to Unreal, without the integration crashing Unreal. + if sys.platform == "darwin": + self.logger.warning("SG TK Unreal integration is not available on Mac.") + return LaunchInformation(exec_path, args, required_env) + # Usually DCCs have an environment variable for plugins that need to be loaded. # Here we're adding ourselves to that list. We add ourselves to the existing one # in fact so we play nice with the current environment. @@ -76,12 +89,6 @@ def prepare_launch(self, exec_path, args, file_to_open=None): # Signals which engine instance from the environment is going to be used. required_env["SHOTGUN_ENGINE"] = self.engine_name - # TODO: Get the startup project from settings somewhere, for now hardcoded - # Otherwise, leave it empty and the project selection window will appear - # (the command-line arguments are forwarded to the new instance of Unreal Editor) - unreal_project = "" - args = args + unreal_project - # Set the bootstrap location in the environment variable that will be used by the Unreal Shotgun startup script bootstrap_script = os.path.join(self.disk_location, "plugins", "basic", "bootstrap.py") required_env["UE_SHOTGUN_BOOTSTRAP"] = bootstrap_script