diff --git a/engine.py b/engine.py index 25ffa21..ac40783 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): @@ -281,18 +280,17 @@ 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 # ahold of the current engine. self._menu = tk_houdini.AppCommandsMenu(self, commands) + + menu = tk_houdini.MenuBuilder(self._menu_name, self.logger) if not os.path.exists(menu_file): # just create the xml for the menus - self._menu.create_menu(menu_file) + menu.create(menu_file) if commands and enable_sg_shelf: @@ -351,7 +349,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,53 +384,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): """ Engine shutdown. @@ -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. @@ -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' @@ -936,31 +848,16 @@ 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 # 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. - # - # 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)): + + if sgtk.util.is_macos(): dialog.setWindowFlags(dialog.windowFlags() | QtCore.Qt.Tool) else: # no parent found, so style should be ok. this is probably, @@ -986,14 +883,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. @@ -1002,39 +897,31 @@ 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() - 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 - # engine level qss only. # - # If we're in 16+, we also need to apply the engine-level qss. - 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 = 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) + 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 + # engine level qss only. + 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 +932,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 +970,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 +1010,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 diff --git a/python/tk_houdini/__init__.py b/python/tk_houdini/__init__.py index fa32fa9..388ff47 100644 --- a/python/tk_houdini/__init__.py +++ b/python/tk_houdini/__init__.py @@ -12,6 +12,7 @@ AppCommandsMenu, AppCommandsShelf, AppCommandsPanelHandler, + MenuBuilder, ensure_file_change_timer_running, get_registered_commands, get_registered_panels, diff --git a/python/tk_houdini/ui_generation.py b/python/tk_houdini/ui_generation.py index d47baad..0bd196b 100644 --- a/python/tk_houdini/ui_generation.py +++ b/python/tk_houdini/ui_generation.py @@ -9,7 +9,6 @@ # not expressly granted therein are reserved by Shotgun Software Inc. import os -import re import sys import xml.etree.ElementTree as ET @@ -119,20 +118,6 @@ def __init__(self, engine, commands): # _get_context_commands method. self._context_menu_item_id = None - def create_menu(self, xml_path): - """Create the PTR Menu""" - - import hou - - # 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. @@ -210,176 +195,6 @@ def _get_commands_by_app(self): return self._commands_by_app - def _build_shotgun_menu_item(self): - """Constructs a top-level "Flow Production Tracking" menu. - - Same logic for both the static and dynamic menu. - - :returns: tuple containing the root element and the shotgun menu item - - """ - - root = ET.Element("mainMenu") - menubar = ET.SubElement(root, "menuBar") - shotgun_menu = self._menuNode( - menubar, - self._engine._menu_name, - "tk.shotgun", - ) - - insert_before = ET.SubElement(shotgun_menu, "insertBefore") - insert_before.text = "help_menu" - - # make sure the Help menu still comes last - modify_item = ET.SubElement(menubar, "modifyItem") - modify_item.set("id", "help_menu") - ET.SubElement(modify_item, "insertAfter") - - return (root, shotgun_menu) - - def _create_dynamic_menu(self, xml_path): - """Construct the dynamic Shotgun menu for toolkit in Houdini 15+. - - :param xml_path: The path to the xml file to store the menu definitions - - """ - # documentation on the dynamic 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() - - # add the context menu - context_menu = self._menuNode( - shotgun_menu, "Current Context", "tk.context.menu" - ) - ET.SubElement(shotgun_menu, "separatorItem") - - context_dynamic_menu = ET.SubElement(context_menu, "scriptMenuStripDynamic") - context_dynamic_menu.set("id", "tk.context_dynamic_menu") - - # here we build an element that stores a python script for returning - # the ids and names of context menu items. the code is executed each - # time the menu is built. - context_dynamic_menu_contents = ET.SubElement( - context_dynamic_menu, "contentsScriptCode" - ) - context_dynamic_menu_contents.text = ( - "CDATA_START" - + _g_dynamic_menu_names % ("_get_context_commands",) - + "CDATA_END" - ) - - # this element defines a python script that has access to the id of the - # menu selected by the user (as defined in the previous element). this - # script uses the id to determine the command and callback execute. - context_dynamic_menu_script = ET.SubElement(context_dynamic_menu, "scriptCode") - context_dynamic_menu_script.text = ( - "CDATA_START" + _g_dynamic_menu_script + "CDATA_END" - ) - - main_dynamic_menu = ET.SubElement(shotgun_menu, "scriptMenuStripDynamic") - main_dynamic_menu.set("id", "tk.main_dynamic_menu") - - # similar to the dynamic context menu. this time we format the python - # script to call the method to return the app specific commands. - main_dynamic_menu_contents = ET.SubElement( - main_dynamic_menu, "contentsScriptCode" - ) - main_dynamic_menu_contents.text = ( - "CDATA_START" - + _g_dynamic_menu_names % ("_get_commands_by_app",) - + "CDATA_END" - ) - - # same script as the context menu for mapping ids to callbacks for - # execution - main_dynamic_menu_script = ET.SubElement(main_dynamic_menu, "scriptCode") - main_dynamic_menu_script.text = ( - "CDATA_START" + _g_dynamic_menu_script + "CDATA_END" - ) - - # format the xml and write it to disk - xml = _format_xml(ET.tostring(root).decode("utf-8")) - _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.""" - - menu = ET.SubElement(parent, "subMenu") - menu.set("id", id) - node = ET.SubElement(menu, "label") - 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.""" @@ -716,6 +531,130 @@ def get_type(self): return self.properties.get("type", "default") +class MenuBuilder(object): + """ + A dedicated class for building Houdini menu XML files. + This class is engine-agnostic and handles the pure menu construction logic. + """ + + def __init__(self, menu_name, logger): + """ + Initialize the menu builder. + + :param menu_name: The name to display for the menu + :param logger: Optional logger for debug messages. If None, no logging is performed. + """ + self._menu_name = menu_name + self._logger = logger + + def create(self, xml_path): + """ + Create a dynamic menu XML file. + + :param xml_path: The path to the xml file to store the menu definitions + """ + + # documentation on the dynamic menu xml tags can be found here: + # https://www.sidefx.com/docs/houdini21.0/basics/config_menus + + self._logger.debug("Constructing dynamic PTR menu.") + + # build the Shotgun menu + (root, shotgun_menu) = self._build_shotgun_menu_item() + + # add the context menu + context_menu = self._menuNode( + shotgun_menu, "Current Context", "tk.context.menu" + ) + ET.SubElement(shotgun_menu, "separatorItem") + + context_dynamic_menu = ET.SubElement(context_menu, "scriptMenuStripDynamic") + context_dynamic_menu.set("id", "tk.context_dynamic_menu") + + # here we build an element that stores a python script for returning + # the ids and names of context menu items. the code is executed each + # time the menu is built. + context_dynamic_menu_contents = ET.SubElement( + context_dynamic_menu, "contentsScriptCode" + ) + context_dynamic_menu_contents.text = ( + "CDATA_START" + + _g_dynamic_menu_names % ("_get_context_commands",) + + "CDATA_END" + ) + + # this element defines a python script that has access to the id of the + # menu selected by the user (as defined in the previous element). this + # script uses the id to determine the command and callback execute. + context_dynamic_menu_script = ET.SubElement(context_dynamic_menu, "scriptCode") + context_dynamic_menu_script.text = ( + "CDATA_START" + _g_dynamic_menu_script + "CDATA_END" + ) + + main_dynamic_menu = ET.SubElement(shotgun_menu, "scriptMenuStripDynamic") + main_dynamic_menu.set("id", "tk.main_dynamic_menu") + + # similar to the dynamic context menu. this time we format the python + # script to call the method to return the app specific commands. + main_dynamic_menu_contents = ET.SubElement( + main_dynamic_menu, "contentsScriptCode" + ) + main_dynamic_menu_contents.text = ( + "CDATA_START" + + _g_dynamic_menu_names % ("_get_commands_by_app",) + + "CDATA_END" + ) + + # same script as the context menu for mapping ids to callbacks for + # execution + main_dynamic_menu_script = ET.SubElement(main_dynamic_menu, "scriptCode") + main_dynamic_menu_script.text = ( + "CDATA_START" + _g_dynamic_menu_script + "CDATA_END" + ) + + # format the xml and write it to disk + xml = _format_xml(ET.tostring(root).decode("utf-8")) + _write_xml(xml, xml_path) + self._logger.debug("Dynamic menu written to: %s" % (xml_path,)) + + def _build_shotgun_menu_item(self): + """Constructs a top-level "Flow Production Tracking" menu. + + Same logic for both the static and dynamic menu. + + :returns: tuple containing the root element and the shotgun menu item + + """ + + root = ET.Element("mainMenu") + menubar = ET.SubElement(root, "menuBar") + shotgun_menu = self._menuNode( + menubar, + self._menu_name, + "tk.shotgun", + ) + + insert_before = ET.SubElement(shotgun_menu, "insertBefore") + insert_before.text = "help_menu" + + # make sure the Help menu still comes last + modify_item = ET.SubElement(menubar, "modifyItem") + modify_item.set("id", "help_menu") + ET.SubElement(modify_item, "insertAfter") + + return (root, shotgun_menu) + + def _menuNode(self, parent, label, id): + """Constructs a submenu for the supplied parent.""" + + menu = ET.SubElement(parent, "subMenu") + menu.set("id", id) + node = ET.SubElement(menu, "label") + node.text = label + return menu + + + def get_registered_commands(engine): """Returns a list of AppCommands for the engine's registered commands. @@ -860,15 +799,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 +810,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( diff --git a/startup.py b/startup.py index 850b2af..31ee239 100644 --- a/startup.py +++ b/startup.py @@ -131,6 +131,26 @@ def prepare_launch(self, exec_path, args, file_to_open=None): self.logger.debug("Launch environment: %s" % (required_env,)) + # TODO - only for Houdini version 21.0+ + enable_sg_menu = self.get_setting("enable_sg_menu", True) + if enable_sg_menu: + menu_name = "Flow Production Tracking" + if self.get_setting("use_short_menu_name", False): + menu_name = "FPTR" + + import tk_houdini + + xml_tmp_dir = required_env[bootstrap.g_temp_env] + menu_file = os.path.join(xml_tmp_dir, "MainMenuCommon.xml").replace( + os.path.sep, "/" + ) + + menu = tk_houdini.MenuBuilder(menu_name, self.logger) + self.logger.debug( + "Constructing dynamic PTR menu - before starting Houdini - special 21.0 behaviour" + ) + menu.create(menu_file) + return LaunchInformation(exec_path, args, required_env) def scan_software(self):