From 369164615fe60916aa29a32377d0fc4fc002dd4b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 12 Apr 2023 10:35:29 +0200 Subject: [PATCH 1/3] Initial commit for adding Houdini TAB search support for OpenPype Creators --- .../hosts/houdini/api/creator_node_shelves.py | 178 ++++++++++++++++++ openpype/hosts/houdini/api/pipeline.py | 6 +- 2 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/houdini/api/creator_node_shelves.py diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py new file mode 100644 index 0000000000..90bfb4c497 --- /dev/null +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -0,0 +1,178 @@ +"""Library to register OpenPype Creators for Houdini TAB node search menu. + +This can be used to install custom houdini tools for the TAB search +menu which will trigger a publish instance to be created interactively. + +The Creators are automatically registered on launch of Houdini through the +Houdini integration's `host.install()` method. + +""" +import contextlib +import tempfile +import logging +import os + +from openpype.pipeline import registered_host +from openpype.pipeline.create import CreateContext +from openpype.resources import get_openpype_icon_filepath + +import hou + +log = logging.getLogger(__name__) + +CREATE_SCRIPT = """ +from openpype.hosts.houdini.api.creator_node_shelves import create_interactive +create_interactive("{identifier}") +""" + + +def create_interactive(creator_identifier): + """Create a Creator using its identifier interactively. + + This is used by the generated shelf tools as callback when a user selects + the creator from the node tab search menu. + + Args: + creator_identifier (str): The creator identifier of the Creator plugin + to create. + + Return: + list: The created instances. + + """ + + # TODO Use Qt instead + result, variant = hou.ui.readInput('Set variant', initial_contents='Main') + if result == -1: + raise RuntimeError("User interrupted") + variant = variant.strip() + if not variant: + raise RuntimeError("Empty variant value entered.") + + host = registered_host() + context = CreateContext(host) + + before = context.instances_by_id.copy() + + # Create the instance + context.create( + creator_identifier=creator_identifier, + variant=variant, + pre_create_data={"use_selection": True} + ) + + # For convenience we set the new node as current since that's much more + # familiar to the artist when creating a node interactively + # TODO Allow to disable auto-select in studio settings or user preferences + after = context.instances_by_id + new = set(after) - set(before) + if new: + # Select the new instance + for instance_id in new: + instance = after[instance_id] + node = hou.node(instance.get("instance_node")) + node.setCurrent(True) + + return list(new) + + +@contextlib.contextmanager +def shelves_change_block(): + """Write shelf changes at the end of the context.""" + hou.shelves.beginChangeBlock() + try: + yield + finally: + hou.shelves.endChangeBlock() + + +def install(): + """Install the Creator plug-ins to show in Houdini's TAB node search menu. + + This function can is re-entrant and can be called again to reinstall and + update the node definitions. For example during development it can be + useful to call it manually: + >>> from openpype.hosts.houdini.api.creator_node_shelves import install + >>> install() + + Returns: + list: List of `hou.Tool` instances + + """ + + host = registered_host() + + # Store the filepath on the host + # TODO: Define a less hacky static shelf path for current houdini session + filepath_attr = "_creator_node_shelf_filepath" + filepath = getattr(host, filepath_attr, None) + if filepath is None: + f = tempfile.NamedTemporaryFile(prefix="houdini_creator_nodes_", + suffix=".shelf", + delete=False) + f.close() + filepath = f.name + setattr(host, filepath_attr, filepath) + elif os.path.exists(filepath): + # Remove any existing shelf file so that we can completey regenerate + # and update the tools file if creator identifiers change + os.remove(filepath) + + icon = get_openpype_icon_filepath() + + # Create context only to get creator plugins, so we don't reset and only + # populate what we need to retrieve the list of creator plugins + create_context = CreateContext(host, reset=False) + create_context.reset_current_context() + create_context._reset_creator_plugins() + + log.debug("Writing OpenPype Creator nodes to shelf: {}".format(filepath)) + tools = [] + with shelves_change_block(): + for identifier, creator in create_context.manual_creators.items(): + + # TODO: Allow the creator plug-in itself to override the categories + # for where they are shown, by e.g. defining + # `Creator.get_network_categories()` + + key = "openpype_create.{}".format(identifier) + log.debug(f"Registering {key}") + script = CREATE_SCRIPT.format(identifier=identifier) + data = { + "script": script, + "language": hou.scriptLanguage.Python, + "icon": icon, + "help": "Create OpenPype publish instance for {}".format( + creator.label + ), + "help_url": None, + "network_categories": [ + hou.ropNodeTypeCategory(), + hou.sopNodeTypeCategory() + ], + "viewer_categories": [], + "cop_viewer_categories": [], + "network_op_type": None, + "viewer_op_type": None, + "locations": ["OpenPype"] + } + + label = "Create {}".format(creator.label) + tool = hou.shelves.tool(key) + if tool: + tool.setData(**data) + tool.setLabel(label) + else: + tool = hou.shelves.newTool( + file_path=filepath, + name=key, + label=label, + **data + ) + + tools.append(tool) + + # Ensure the shelf is reloaded + hou.shelves.loadFile(filepath) + + return tools diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index 45e2f8f87f..61274e6028 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -18,7 +18,7 @@ from openpype.pipeline import ( ) from openpype.pipeline.load import any_outdated_containers from openpype.hosts.houdini import HOUDINI_HOST_DIR -from openpype.hosts.houdini.api import lib, shelves +from openpype.hosts.houdini.api import lib, shelves, creator_node_shelves from openpype.lib import ( register_event_callback, @@ -83,6 +83,10 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): _set_context_settings() shelves.generate_shelves() + if not IS_HEADLESS: + import hdefereval # noqa, hdefereval is only available in ui mode + hdefereval.executeDeferred(creator_node_shelves.install) + def has_unsaved_changes(self): return hou.hipFile.hasUnsavedChanges() From 12830254baab23cdb4ae6bf65871ac9a74cfcd5b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 12 Apr 2023 10:53:31 +0200 Subject: [PATCH 2/3] Fix typo --- openpype/hosts/houdini/api/creator_node_shelves.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py index 90bfb4c497..7e6e019b63 100644 --- a/openpype/hosts/houdini/api/creator_node_shelves.py +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -89,7 +89,7 @@ def shelves_change_block(): def install(): """Install the Creator plug-ins to show in Houdini's TAB node search menu. - This function can is re-entrant and can be called again to reinstall and + This function is re-entrant and can be called again to reinstall and update the node definitions. For example during development it can be useful to call it manually: >>> from openpype.hosts.houdini.api.creator_node_shelves import install From ede0333fbc3cc4fcddc310a3d3ed00e8949ec6b5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 12 Apr 2023 11:57:31 +0200 Subject: [PATCH 3/3] Fix `hou.ui.readInput` not returning cancel correctly --- openpype/hosts/houdini/api/creator_node_shelves.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py index 7e6e019b63..3638e14296 100644 --- a/openpype/hosts/houdini/api/creator_node_shelves.py +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -42,9 +42,16 @@ def create_interactive(creator_identifier): """ # TODO Use Qt instead - result, variant = hou.ui.readInput('Set variant', initial_contents='Main') - if result == -1: - raise RuntimeError("User interrupted") + result, variant = hou.ui.readInput('Define variant name', + buttons=("Ok", "Cancel"), + initial_contents='Main', + title="Define variant", + help="Set the variant for the " + "publish instance", + close_choice=1) + if result == 1: + # User interrupted + return variant = variant.strip() if not variant: raise RuntimeError("Empty variant value entered.")