From 4ca55d59d7ad70a1ec2ea947b0c323bf4597303d Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 16 Apr 2025 16:47:29 -0700 Subject: [PATCH 01/10] WIP --- engine.py | 197 ++++++++++++------------------------------------------ 1 file changed, 44 insertions(+), 153 deletions(-) diff --git a/engine.py b/engine.py index 25ffa21..52751ce 100644 --- a/engine.py +++ b/engine.py @@ -281,10 +281,7 @@ def _load_otls(): # setup houdini menus menu_file = self._safe_path_join(xml_tmp_dir, "MainMenuCommon") - - # as of houdini 12.5 add .xml - if self._houdini_version > (12, 5, 0): - menu_file = menu_file + ".xml" + menu_file = menu_file + ".xml" # keep the reference to the menu handler for convenience so # that we can access it from the menu scripts when they get @@ -351,7 +348,7 @@ def _poll_for_ui_available_then_setup_shelves(): else: _setup_shelf() - if commands and self._panels_supported(): + if commands: # Get the list of registered commands to build panels for. The # commands returned are AppCommand objects defined in @@ -386,52 +383,10 @@ def _poll_for_ui_available_then_setup_shelves(): # consistent, intended look and feel of the toolkit widgets. # Surprisingly, calling this does not seem to have any affect on # houdini itself, despite the global nature of the method. - # - # NOTE: Except for 16+. It's no longer safe and causes lots of styling - # problems in Houdini's UI globally. - if self._houdini_version < (16, 0, 0): - self.logger.debug("Houdini < 16 detected: applying dark look and feel.") - self._initialize_dark_look_and_feel() # Run a series of app instance commands at startup. self._run_app_instance_commands() - # In Houdini 18, we see substantial stability problems related to Qt in - # builds older than 18.0.348, which is the point when SideFx moved to a - # newer version of Qt and PySide2. We've reproduced problems on OSX and - # Linux, and we have reports of crashes on Windows, as well. All of these - # issues are no longer a problem in 348+, so we'll warn users on builds - # of H18 older than that. - if self._houdini_version[0] == 18 and self._houdini_version[-1] < 348: - # We need to wait until Houdini idles before showing the message. - # If we show it right now, it will pop up behind Houdini's splash - # screen, and since the dialog is modal you end up in a situation - # where Houdini does not continue to launch, and you can't see or - # dismiss the dialog to unblock it. - def run_when_idle(): - hou.ui.displayMessage( - text="Houdini 18 versions older than 18.0.348 are unstable when using " - "Flow Production Tracking. Be aware that Houdini crashes may " - "occur if attempting to use Toolkit apps from your current Houdini " - "session. PTR recommends updating Houdini to 18.0.348 or newer.", - title="Flow Production Tracking", - severity=hou.severityType.Warning, - ) - - # Have the function unregister itself. It does this by looping over - # all the registered callbacks and finding itself by looking for a - # special attribute that is added below (just before registering it - # as an event loop callback). - for callback in hou.ui.eventLoopCallbacks(): - if hasattr(callback, "tk_houdini_stability_msg"): - hou.ui.removeEventLoopCallback(callback) - - # Add the special attribute that the function will look use to find - # and unregister itself when executed. - run_when_idle.tk_houdini_stability_msg = True - - # Add the function as an event loop callback. - hou.ui.addEventLoopCallback(run_when_idle) def destroy_engine(self): """ @@ -561,48 +516,36 @@ def show_panel(self, panel_id, title, bundle, widget_class, *args, **kwargs): pane_tab.setIsCurrentTab() return - # panel support differs between 14/15. - if self._panels_supported(): - - # if it can't be located, try to create a new tab and set the - # interface. - panel_interface = None - try: - for interface in hou.pypanel.interfacesInFile(self._panels_file): - if interface.name() == panel_id: - panel_interface = interface - break - except hou.OperationFailed: - # likely due to panels file not being a valid file, missing, etc. - # hopefully not the case, but try to continue gracefully. - self.logger.warning( - "Unable to find interface for panel '%s' in file: %s" - % (panel_id, self._panels_file) - ) + # if it can't be located, try to create a new tab and set the + # interface. + panel_interface = None + try: + for interface in hou.pypanel.interfacesInFile(self._panels_file): + if interface.name() == panel_id: + panel_interface = interface + break + except hou.OperationFailed: + # likely due to panels file not being a valid file, missing, etc. + # hopefully not the case, but try to continue gracefully. + self.logger.warning( + "Unable to find interface for panel '%s' in file: %s" + % (panel_id, self._panels_file) + ) - if panel_interface: - # the options to create a named panel on the far right of the - # UI doesn't seem to be present in python. so hscript it is! - # Here's the docs for the hscript command: - # https://www.sidefx.com/docs/houdini14.0/commands/pane - hou.hscript("pane -S -m pythonpanel -o -n %s" % panel_id) - panel = hou.ui.curDesktop().findPaneTab(panel_id) - - # different calls to set the python panel interface in Houdini - # 14/15 - if self._houdini_version[0] >= 15: - panel.setActiveInterface(panel_interface) - else: - # if SESI puts in a fix for setInterface, then panels - # will work for houini 14. will just need to update - # _panels_supported() to add the proper version. and - # remove this comment. - panel.setInterface(panel_interface) - - # turn off the python panel toolbar to make the tk panels look - # more integrated. should be all good so just return - panel.showToolbar(False) - return + if panel_interface: + # the options to create a named panel on the far right of the + # UI doesn't seem to be present in python. so hscript it is! + # Here's the docs for the hscript command: + # https://www.sidefx.com/docs/houdini14.0/commands/pane + hou.hscript("pane -S -m pythonpanel -o -n %s" % panel_id) + panel = hou.ui.curDesktop().findPaneTab(panel_id) + + panel.setActiveInterface(panel_interface) + + # turn off the python panel toolbar to make the tk panels look + # more integrated. should be all good so just return + panel.showToolbar(False) + return # if we're here, then showing as a panel was unsuccesful or not # supported. Just show it as a dialog. @@ -640,7 +583,7 @@ def _safe_path_join(self, *args): return os.path.join(*args).replace(os.path.sep, "/") @staticmethod - def _is_version_less_or_equal(check_version, base_version): + def _is_version_less_or_equal(check_version, base_version): # TODO replace that with modern Python version cmp functions """ Checks if the folder version is less than or equal to the current Houdini session version. :param check_version: @@ -749,37 +692,6 @@ def _get_otl_paths(self, otl_path): return otl_paths - def _panels_supported(self): - """ - Returns True if panels are supported for current Houdini version. - """ - - ver = hou.applicationVersion() - - # first version where saving python panel in desktop was fixed - if sgtk.util.is_macos(): - # We have some serious painting problems with Python panes in - # H16 that are specific to OS X. We have word out to SESI, and - # are waiting to hear back from them as to how we might be able - # to proceed. Until that is sorted out, though, we're going to - # have to disable panel support on OS X for H16. Our panel apps - # appear to function just fine in dialog mode. - # - # Update: H17 resolves some of the issues, but we still have problems - # with item delegates not rendering consistently in the Shotgun Panel's - # entity views. - if ver >= (16, 0, 0): - return False - - if ver >= (15, 0, 272): - return True - - return False - - # NOTE: there is an outstanding bug at SESI to backport a fix to make - # setInterface work properly in houdini 14. If that goes through, we'll - # be able to make embedded panels work in houdini 14 too. - def _run_app_instance_commands(self): """ Runs the series of app instance commands listed in the 'run_at_startup' @@ -943,14 +855,6 @@ def _create_dialog(self, title, bundle, widget, parent): # the style to the dark look and feel in preparation for the # re-application below. See the comment about initializing the dark # look and feel above. - # - # We can only do this in Houdini 15.x or older. With the switch to - # Qt5/PySide2 in H16, enough has changed in Houdini's styling that - # we break its styling in a few places if we zero out the main window's - # stylesheet. We're now compensating for the problems that arise in - # the engine's style.qss. - if h_ver < (16, 0, 0): - dialog.parent().setStyleSheet("") # This will ensure our dialogs don't fall behind Houdini's main # window when they lose focus. @@ -958,9 +862,8 @@ def _create_dialog(self, title, bundle, widget, parent): # NOTE: Setting the window flags in H18 on OSX causes a crash. Once # that bug is resolved we can re-enable this. The result is that # on H18 without the window flags set per the below, our dialogs - # will fall behind Houdini if they lose focus. This is only an issue - # for versions of H18 older than 18.0.348. - if sgtk.util.is_macos() and (h_ver[0] == 18 and h_ver >= (18, 0, 348)): + # will fall behind Houdini if they lose focus. + if sgtk.util.is_macos(): dialog.setWindowFlags(dialog.windowFlags() | QtCore.Qt.Tool) else: # no parent found, so style should be ok. this is probably, @@ -1021,20 +924,14 @@ def _create_dialog(self, title, bundle, widget, parent): widget.setStyleSheet(widget.styleSheet() + qss_data) widget.update() else: - # manually re-apply any bundled stylesheet to the dialog if we are older - # than H16. In 16 we inherited styling problems and need to rely on the + # manually re-apply any bundled stylesheet to the dialog + # We inherited styling problems and need to rely on the # engine level qss only. - # - # If we're in 16+, we also need to apply the engine-level qss. - if h_major_ver >= 16: - self._apply_external_styleshet(self, dialog) - qss = dialog.styleSheet() - qss = qss.replace("{{ENGINE_ROOT_PATH}}", engine_root_path) - dialog.setStyleSheet(qss) - dialog.update() - - if hou.applicationVersion()[0] < 16: - self._apply_external_styleshet(bundle, dialog) + self._apply_external_styleshet(self, dialog) + qss = dialog.styleSheet() + qss = qss.replace("{{ENGINE_ROOT_PATH}}", engine_root_path) + dialog.setStyleSheet(qss) + dialog.update() # raise and activate the dialog: dialog.raise_() @@ -1045,11 +942,7 @@ def _create_dialog(self, title, bundle, widget, parent): # Anything beyond 16.5.481 bundles a PySide2 version that gives us # a usable hwnd directly. We also check to make sure this is Qt5, # since SideFX still offers Qt4/PySide builds of modern Houdinis. - if hou.applicationVersion() >= ( - 16, - 5, - 481, - ) and QtCore.__version__.startswith("5."): + if QtCore.__version__.startswith("5."): hwnd = dialog.winId() else: ctypes.pythonapi.PyCObject_AsVoidPtr.restype = ctypes.c_void_p @@ -1087,8 +980,7 @@ def show_modal(self, title, bundle, widget_class, *args, **kwargs): # our dialog, those styling changes we've applied either as part # of the app's style.qss, or tk-houdini's, everything sticks the # way it should. - if hou.applicationVersion() >= (16, 0, 0): - dialog.parent().setStyleSheet(dialog.parent().styleSheet()) + dialog.parent().setStyleSheet(dialog.parent().styleSheet()) # finally launch it, modal state status = dialog.exec_() @@ -1128,8 +1020,7 @@ def show_dialog(self, title, bundle, widget_class, *args, **kwargs): # our dialog, those styling changes we've applied either as part # of the app's style.qss, or tk-houdini's, everything sticks the # way it should. - if hou.applicationVersion() >= (16, 0, 0): - dialog.parent().setStyleSheet(dialog.parent().styleSheet()) + dialog.parent().setStyleSheet(dialog.parent().styleSheet()) # lastly, return the instantiated widget return widget From f2b5abaecfda1567db5a17184d3c7edfd80ddddf Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 10 Sep 2025 10:35:13 -0700 Subject: [PATCH 02/10] Remove some more --- engine.py | 57 +++++++++---------- python/tk_houdini/ui_generation.py | 91 +++++------------------------- 2 files changed, 41 insertions(+), 107 deletions(-) diff --git a/engine.py b/engine.py index 52751ce..b16dfe0 100644 --- a/engine.py +++ b/engine.py @@ -210,14 +210,13 @@ def pre_app_init(self): if not self._ui_enabled: return - if self._houdini_version[0] >= 15: - # In houdini 15+, we can use the dynamic menus and shelf api to - # properly handle cases where a file is loaded outside of a PTR - # context. Make sure the timer that looks for current file changes - # is running. - tk_houdini = self.import_module("tk_houdini") - if self.get_setting("automatic_context_switch", True): - tk_houdini.ensure_file_change_timer_running() + # We can use the dynamic menus and shelf api to + # properly handle cases where a file is loaded outside of a PTR + # context. Make sure the timer that looks for current file changes + # is running. + tk_houdini = self.import_module("tk_houdini") + if self.get_setting("automatic_context_switch", True): + tk_houdini.ensure_file_change_timer_running() self._menu_name = "Flow Production Tracking" if self.get_setting("use_short_menu_name", False): @@ -848,8 +847,6 @@ def _create_dialog(self, title, bundle, widget, parent): self, title, bundle, widget, parent ) - h_ver = hou.applicationVersion() - if dialog.parent(): # parenting crushes the dialog's style. This seems to work to reset # the style to the dark look and feel in preparation for the @@ -889,14 +886,12 @@ def _create_dialog(self, title, bundle, widget, parent): # and combine the two into a single, unified stylesheet for the dialog # and widget. engine_root_path = self._get_engine_root_path() - h_major_ver = hou.applicationVersion()[0] if bundle.name in ["tk-multi-shotgunpanel", "tk-multi-publish2"]: if bundle.name == "tk-multi-shotgunpanel": self._apply_external_styleshet(bundle, dialog) - # Styling in H16+ is very different than in earlier versions of - # Houdini. The result is that we have to be more careful about + # Styling Houdini, we have to be more careful about # behavior concerning stylesheets, because we might bleed into # Houdini itself if we change qss on parent objects or make use # of QStyles on the QApplication. @@ -905,24 +900,24 @@ def _create_dialog(self, title, bundle, widget, parent): # already assigned to the widget. This means that the engine # styling is helping patch holes in any app- or framework-level # qss that might have already been applied. - if h_major_ver >= 16: - # We don't apply the engine's style.qss to the dialog for the panel, - # but we do for the publisher. This will make sure that the tank - # dialog's header and info slide-out widget is properly styled. The - # panel app doesn't show that stuff, so we don't need to worry about - # it. - if bundle.name == "tk-multi-publish2": - self._apply_external_styleshet(self, dialog) - - qss_file = self._get_engine_qss_file() - with open(qss_file, "rt") as f: - qss_data = f.read() - qss_data = self._resolve_sg_stylesheet_tokens(qss_data) - qss_data = qss_data.replace( - "{{ENGINE_ROOT_PATH}}", engine_root_path - ) - widget.setStyleSheet(widget.styleSheet() + qss_data) - widget.update() + # + # We don't apply the engine's style.qss to the dialog for the panel, + # but we do for the publisher. This will make sure that the tank + # dialog's header and info slide-out widget is properly styled. The + # panel app doesn't show that stuff, so we don't need to worry about + # it. + if bundle.name == "tk-multi-publish2": + self._apply_external_styleshet(self, dialog) + + qss_file = self._get_engine_qss_file() + with open(qss_file, "rt") as f: + qss_data = f.read() + qss_data = self._resolve_sg_stylesheet_tokens(qss_data) + qss_data = qss_data.replace( + "{{ENGINE_ROOT_PATH}}", engine_root_path + ) + widget.setStyleSheet(widget.styleSheet() + qss_data) + widget.update() else: # manually re-apply any bundled stylesheet to the dialog # We inherited styling problems and need to rely on the diff --git a/python/tk_houdini/ui_generation.py b/python/tk_houdini/ui_generation.py index d47baad..b9cd96b 100644 --- a/python/tk_houdini/ui_generation.py +++ b/python/tk_houdini/ui_generation.py @@ -13,6 +13,9 @@ import sys import xml.etree.ElementTree as ET +# TODO - ask copilot if any methods in this file are unused/orphan + + # Make sure we always give Houdini forward-slash-delimited paths. There is # a crash bug in early releases of H17 on Windows when it's given backslash # paths to read. @@ -122,16 +125,9 @@ def __init__(self, engine, commands): def create_menu(self, xml_path): """Create the PTR Menu""" - import hou + self._engine.logger.debug("Constructing dynamic PTR menu.") + self._create_dynamic_menu(xml_path) - # houdini 15+ allows for dynamic menu creation, so do that if possible. - # otherwise, fallback to the static menu - if hou.applicationVersion()[0] >= 15: - self._engine.logger.debug("Constructing dynamic PTR menu.") - self._create_dynamic_menu(xml_path) - else: - self._engine.logger.debug("Constructing static PTR menu.") - self._create_static_menu(xml_path) def _get_context_commands(self): """This method returns a modified list of context commands. @@ -304,56 +300,6 @@ def _create_dynamic_menu(self, xml_path): _write_xml(xml, xml_path) self._engine.logger.debug("Dynamic menu written to: %s" % (xml_path,)) - def _create_static_menu(self, xml_path): - """Construct the static Shotgun menu for older versions of Houdini. - - :param xml_path: The path to the xml file to store the menu definitions - - """ - - # documentation on the static menu xml tags can be found here: - # http://www.sidefx.com/docs/houdini15.0/basics/config_menus - - # build the Shotgun menu - (root, shotgun_menu) = self._build_shotgun_menu_item() - - # create the menu object - ctx_name = self._get_context_name() - ctx_menu = self._menuNode(shotgun_menu, ctx_name, "tk.context") - ET.SubElement(ctx_menu, "separatorItem") - - (context_cmds, cmds_by_app, favourite_cmds) = self._group_commands() - - # favourites - ET.SubElement(shotgun_menu, "separatorItem") - for cmd in favourite_cmds: - self._itemNode(shotgun_menu, cmd.name, cmd.get_id()) - - # everything else - ET.SubElement(shotgun_menu, "separatorItem") - - # add the context menu items - for cmd in context_cmds: - self._itemNode(ctx_menu, cmd.name, cmd.get_id()) - - # build the main app-centric menu - for app_name in sorted(cmds_by_app.keys()): - cmds = cmds_by_app[app_name] - if len(cmds) > 1: - menu = self._menuNode( - shotgun_menu, app_name, "tk.%s" % app_name.lower() - ) - for cmd in cmds: - self._itemNode(menu, cmd.name, cmd.get_id()) - else: - if not cmds[0].favourite: - self._itemNode(shotgun_menu, cmds[0].name, cmds[0].get_id()) - - # format the xml and write it to disk - xml = _format_xml(ET.tostring(root, encoding="UTF-8")) - _write_xml(xml, xml_path) - self._engine.logger.debug("Static menu written to: %s" % (xml_path,)) - def _menuNode(self, parent, label, id): """Constructs a submenu for the supplied parent.""" @@ -860,15 +806,9 @@ def apply_stylesheet(self): self._changing_stylesheet = True try: - # This is only safe in pre-H16. If we do this in 16 it destroys - # some styling in Houdini itself. - if self.parent() and hou.applicationVersion() < (16, 0, 0): - self.parent().setStyleSheet("") - engine._apply_external_styleshet(bundle, self) - # Styling in H16+ is very different than in earlier versions of - # Houdini. The result is that we have to be more careful about + # Styling Houdini, we have to be more careful about # behavior concerning stylesheets, because we might bleed into # Houdini itself if we change qss on parent objects or make use # of QStyles on the QApplication. @@ -877,16 +817,15 @@ def apply_stylesheet(self): # already assigned to the widget. This means that the engine # styling is helping patch holes in any app- or framework-level # qss that might have already been applied. - if hou.applicationVersion() >= (16, 0, 0): - qss_file = engine._get_engine_qss_file() - with open(qss_file, "rt") as f: - qss_data = f.read() - qss_data = engine._resolve_sg_stylesheet_tokens(qss_data) - qss_data = qss_data.replace( - "{{ENGINE_ROOT_PATH}}", engine._get_engine_root_path() - ) - self.setStyleSheet(self.styleSheet() + qss_data) - self.update() + qss_file = engine._get_engine_qss_file() + with open(qss_file, "rt") as f: + qss_data = f.read() + qss_data = engine._resolve_sg_stylesheet_tokens(qss_data) + qss_data = qss_data.replace( + "{{ENGINE_ROOT_PATH}}", engine._get_engine_root_path() + ) + self.setStyleSheet(self.styleSheet() + qss_data) + self.update() except Exception as e: engine.logger.warning( From 64ddf622ecdac877dcfd453bbd02005a24598aa8 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 10 Sep 2025 10:59:08 -0700 Subject: [PATCH 03/10] Cleanup --- engine.py | 8 ++------ python/tk_houdini/ui_generation.py | 4 ---- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/engine.py b/engine.py index b16dfe0..fd23586 100644 --- a/engine.py +++ b/engine.py @@ -582,7 +582,7 @@ def _safe_path_join(self, *args): return os.path.join(*args).replace(os.path.sep, "/") @staticmethod - def _is_version_less_or_equal(check_version, base_version): # TODO replace that with modern Python version cmp functions + def _is_version_less_or_equal(check_version, base_version): """ Checks if the folder version is less than or equal to the current Houdini session version. :param check_version: @@ -855,11 +855,7 @@ def _create_dialog(self, title, bundle, widget, parent): # This will ensure our dialogs don't fall behind Houdini's main # window when they lose focus. - # - # NOTE: Setting the window flags in H18 on OSX causes a crash. Once - # that bug is resolved we can re-enable this. The result is that - # on H18 without the window flags set per the below, our dialogs - # will fall behind Houdini if they lose focus. + if sgtk.util.is_macos(): dialog.setWindowFlags(dialog.windowFlags() | QtCore.Qt.Tool) else: diff --git a/python/tk_houdini/ui_generation.py b/python/tk_houdini/ui_generation.py index b9cd96b..f404d52 100644 --- a/python/tk_houdini/ui_generation.py +++ b/python/tk_houdini/ui_generation.py @@ -9,13 +9,9 @@ # not expressly granted therein are reserved by Shotgun Software Inc. import os -import re import sys import xml.etree.ElementTree as ET -# TODO - ask copilot if any methods in this file are unused/orphan - - # Make sure we always give Houdini forward-slash-delimited paths. There is # a crash bug in early releases of H17 on Windows when it's given backslash # paths to read. From 0e584a8bbb5a19c3f454e9f2d46caf93f8ac0ac7 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 10 Sep 2025 13:58:34 -0700 Subject: [PATCH 04/10] Black --- engine.py | 5 +---- python/tk_houdini/ui_generation.py | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/engine.py b/engine.py index fd23586..d69d0f3 100644 --- a/engine.py +++ b/engine.py @@ -386,7 +386,6 @@ def _poll_for_ui_available_then_setup_shelves(): # Run a series of app instance commands at startup. self._run_app_instance_commands() - def destroy_engine(self): """ Engine shutdown. @@ -909,9 +908,7 @@ def _create_dialog(self, title, bundle, widget, parent): with open(qss_file, "rt") as f: qss_data = f.read() qss_data = self._resolve_sg_stylesheet_tokens(qss_data) - qss_data = qss_data.replace( - "{{ENGINE_ROOT_PATH}}", engine_root_path - ) + qss_data = qss_data.replace("{{ENGINE_ROOT_PATH}}", engine_root_path) widget.setStyleSheet(widget.styleSheet() + qss_data) widget.update() else: diff --git a/python/tk_houdini/ui_generation.py b/python/tk_houdini/ui_generation.py index f404d52..554a0e6 100644 --- a/python/tk_houdini/ui_generation.py +++ b/python/tk_houdini/ui_generation.py @@ -124,7 +124,6 @@ def create_menu(self, xml_path): self._engine.logger.debug("Constructing dynamic PTR menu.") self._create_dynamic_menu(xml_path) - def _get_context_commands(self): """This method returns a modified list of context commands. From a36c222fa7fd04dc3bcc0e84a2c25e988b7b86be Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Tue, 16 Sep 2025 13:15:54 -0700 Subject: [PATCH 05/10] fixup! Remove some more --- python/tk_houdini/ui_generation.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/python/tk_houdini/ui_generation.py b/python/tk_houdini/ui_generation.py index 554a0e6..3a3e9c8 100644 --- a/python/tk_houdini/ui_generation.py +++ b/python/tk_houdini/ui_generation.py @@ -304,22 +304,6 @@ def _menuNode(self, parent, label, id): node.text = label return menu - def _itemNode(self, parent, label, id): - """Constructs a static menu item for the supplied parent. - - Adds the script path and args which houdini uses as the callback. - - """ - - item = ET.SubElement(parent, "scriptItem") - node = ET.SubElement(item, "label") - node.text = label - node = ET.SubElement(item, "scriptPath") - node.text = '"%s"' % (g_menu_item_script,) - node = ET.SubElement(item, "scriptArgs") - node.text = id - return item - class AppCommandsPanelHandler(AppCommandsUI): """Creates panels and installs them into the session.""" From fd599ec1b5582ba5149104146550eca6a1ef4aff Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 18 Sep 2025 15:34:59 -0700 Subject: [PATCH 06/10] TODO --- python/tk_houdini/menu_action.py | 31 -------------------------- python/tk_houdini/python_qt_houdini.py | 4 ++++ 2 files changed, 4 insertions(+), 31 deletions(-) delete mode 100644 python/tk_houdini/menu_action.py diff --git a/python/tk_houdini/menu_action.py b/python/tk_houdini/menu_action.py deleted file mode 100644 index 3982293..0000000 --- a/python/tk_houdini/menu_action.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) 2013 Shotgun Software Inc. -# -# CONFIDENTIAL AND PROPRIETARY -# -# This work is provided "AS IS" and subject to the Shotgun 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 Shotgun Pipeline Toolkit Source Code License. All rights -# not expressly granted therein are reserved by Shotgun Software Inc. - -import sys - -import hou - -import tank.platform.engine - - -def error(msg): - if hou.isUIAvailable(): - hou.ui.displayMessage(msg) - else: - print(msg) - - -cmd_id = sys.argv[1] -engine = tank.platform.engine.current_engine() - -if engine is None or not hasattr(engine, "launch_command"): - error("Flow Production Tracking: Houdini engine is not loaded") -else: - engine.launch_command(cmd_id) diff --git a/python/tk_houdini/python_qt_houdini.py b/python/tk_houdini/python_qt_houdini.py index 6c862c2..6fee18a 100644 --- a/python/tk_houdini/python_qt_houdini.py +++ b/python/tk_houdini/python_qt_houdini.py @@ -45,3 +45,7 @@ def exec_(application): This function returns right away. """ IntegratedEventLoop(application).exec_() + + + +### Does this code ever runs? From 551490956df61bf2f61709527904a3545165881e Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 18 Sep 2025 15:35:36 -0700 Subject: [PATCH 07/10] fixup! TODO --- python/tk_houdini/ui_generation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/tk_houdini/ui_generation.py b/python/tk_houdini/ui_generation.py index 3a3e9c8..4f07598 100644 --- a/python/tk_houdini/ui_generation.py +++ b/python/tk_houdini/ui_generation.py @@ -964,7 +964,7 @@ def _write_xml(xml, xml_path): # The code that executes when a shelf button is clicked. This is pulled from # menu_action.py. Maybe there's a good way to share this rather than -# duplicating the logic? +# duplicating the logic? TODO!!! _g_launch_script = """ import hou import tank.platform.engine From 29c841a890cd3e443ec9848edd7cc3522f6d05c4 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 18 Sep 2025 15:52:39 -0700 Subject: [PATCH 08/10] Remove unused code since 2017 (#5b14396) --- python/tk_houdini/__init__.py | 6 --- python/tk_houdini/python_qt_houdini.py | 51 -------------------------- 2 files changed, 57 deletions(-) delete mode 100644 python/tk_houdini/python_qt_houdini.py diff --git a/python/tk_houdini/__init__.py b/python/tk_houdini/__init__.py index fa32fa9..02b83f1 100644 --- a/python/tk_houdini/__init__.py +++ b/python/tk_houdini/__init__.py @@ -17,9 +17,3 @@ get_registered_panels, get_wrapped_panel_widget, ) - -try: - # hou might not be available during bootstrap - from . import python_qt_houdini -except: - pass diff --git a/python/tk_houdini/python_qt_houdini.py b/python/tk_houdini/python_qt_houdini.py deleted file mode 100644 index 6fee18a..0000000 --- a/python/tk_houdini/python_qt_houdini.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) 2013 Shotgun Software Inc. -# -# CONFIDENTIAL AND PROPRIETARY -# -# This work is provided "AS IS" and subject to the Shotgun 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 Shotgun Pipeline Toolkit Source Code License. All rights -# not expressly granted therein are reserved by Shotgun Software Inc. - -import hou - -from tank.platform.qt import QtCore - - -class IntegratedEventLoop(object): - """This class behaves like QEventLoop except it allows Python's Qt to run inside - Houdini's event loop on the main thread. You probably just want to - call exec_() below instead of using this class directly. - """ - - def __init__(self, application): - # We need the application to send posted events. We hold a reference - # to any dialogs to ensure that they don't get garbage collected - # (and thus close in the process). The reference count for this object - # will go to zero when it removes itself from Houdini's event loop. - self.application = application - self.event_loop = QtCore.QEventLoop() - - def exec_(self): - hou.ui.addEventLoopCallback(self.processEvents) - - def processEvents(self): - self.event_loop.processEvents() - self.application.sendPostedEvents(None, 0) - - -def exec_(application): - """You cannot call QApplication.exec_, or Houdini will freeze while Python's Qt - waits for and processes events. Instead, call this function to allow - Houdini's and Python's Qt's event loops to coexist. Pass in any dialogs as - extra arguments, if you want to ensure that something holds a reference - to them while the event loop runs. - - This function returns right away. - """ - IntegratedEventLoop(application).exec_() - - - -### Does this code ever runs? From 02fa7203b3442828abeeb6e6d5533eeb6814c1c2 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 18 Sep 2025 15:55:34 -0700 Subject: [PATCH 09/10] fixup! fixup! TODO --- python/tk_houdini/ui_generation.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/python/tk_houdini/ui_generation.py b/python/tk_houdini/ui_generation.py index 4f07598..ff55b39 100644 --- a/python/tk_houdini/ui_generation.py +++ b/python/tk_houdini/ui_generation.py @@ -12,16 +12,6 @@ import sys import xml.etree.ElementTree as ET -# Make sure we always give Houdini forward-slash-delimited paths. There is -# a crash bug in early releases of H17 on Windows when it's given backslash -# paths to read. -g_menu_item_script = os.path.join(os.path.dirname(__file__), "menu_action.py").replace( - os.path.sep, "/" -) - -# #3716 Fixes UNC problems with menus. Prefix '\' are otherwise concatenated to a single character, therefore using '/' instead. -g_menu_item_script = g_menu_item_script.replace("\\", "/") - # global used to indicate that the file change time has been initialized and # started g_file_change_timer = None From 318c1785da78e27a0fff01a4e05f68620b6bf675 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 2 Oct 2025 11:54:20 -0700 Subject: [PATCH 10/10] Fix missing FPTR menu in Houdini 21.0+ by refreshing startup cache --- engine.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/engine.py b/engine.py index d69d0f3..2f79b94 100644 --- a/engine.py +++ b/engine.py @@ -367,6 +367,14 @@ def _poll_for_ui_available_then_setup_shelves(): ) panels.create_panels(self._panels_file) + if self._houdini_version >= (21, 0, 479): + # Houdini 21.0+ introduced changes to how startup paths are cached, which can + # prevent custom menus (like the FPTR menu) from appearing unless the cache is + # refreshed. The call below ensures that Houdini recognizes and loads our custom + # menu definitions from the temporary directory, as documented in SideFx ticket + # 169562 (SG-40163). + hou.refreshStartupPathCacheDirectory(xml_tmp_dir) + # tell QT to interpret C strings as utf-8 utf8 = QtCore.QTextCodec.codecForName("utf-8") QtCore.QTextCodec.setCodecForCStrings(utf8)