Houdini 20.5 Python scripting

Python state context menu

How to set up your state’s context menu and respond to user interaction with the menu.

On this page

Overview

You can use the hou.ViewerStateMenu object to build a custom context menu that appears when the user presses while your state is active.

Optionally, you can manage the UI states of the custom context menu with the onMenuPreOpen handler.

Building and binding the menu

In the code where you generate the state template, create the context menu using a hou.ViewerStateMenu object, then bind it to the template using hou.ViewerStateTemplate.bindMenu.

from __future__ import print_function


# Class implementing the state behavior
class MyState(object):
    def __init__(self, state_name, scene_viewer):
        self.state_name = state_name
        self.scene_viewer = scene_viewer

    def onMenuAction(self, kwargs):
        print("You chose:", kwargs["menu_item"])


# Function to generate the state template, which describes the state and any
# menus, handles, selectors, etc. bound to it
def createViewerStateTemplate():
    # Create the template object
    template = hou.ViewerStateTemplate(
        "mystate", "My State", hou.sopNodeTypeCategory()
    )
    # Bind the implementation class
    template.bindFactory(MyState)

    # Create and bind context menu
    menu = hou.ViewerStateMenu('bend_menu', 'Bend')
    menu.addActionItem("first", "Action One")
    menu.addActionItem("second", "Action Two")
    menu.addActionItem("third", "Action Three")
    template.bindMenu(menu)

    return template

See the hou.ViewerStateMenu object for how to build the menu.

Responding to menu actions

When the user opens the menu and chooses an item, Houdini will call your state’s onMenuAction method. The dictionary passed to the method has a menu_item key contaning the ID of the selected menu item.

Tip

You may only want to track when action menu items are chosen and ignore the user changing “settings” (toggling checkboxes and changing radio selections). You can read the current value of settings items in other handler methods to use the settings to influence how the state works.

  • For action menu items and toggle items, if the menu_item in the dictionary equals the ID you set on the item, the item was chosen.

    # When you create the menu
    menu = hou.ViewerStateMenu('bend_menu', 'Bend')
    menu.addActionItem("delete", "Delete Current")
    
    # ...
    
    # In the menu handler method
    def onMenuAction(self, kwargs):
        if kwargs["menu_item"] == "delete":
            # Code to implement the "delete" action
            # ...
    
  • Instead of coding a tedious if/else tree for every action in your menu like this:

    def onMenuAction(self, kwargs):
        menu_item = kwargs["menu_item"]
    
        # This gets annoying to write and read if there are a lot of these
        if menu_item == "delete":
            # ...
        elif menu_item == "dup":
            # ...
        elif menu_item == "done":
            # ...
    

    You can use some Python magic to separate action handling into methods based on the menu item ID:

    # When you create the menu
    menu = hou.ViewerStateMenu('bend_menu', 'Bend')
    menu.addActionItem("delete", "Delete")
    menu.addActionItem("dup", "Duplicate")
    menu.addActionItem("done", "Finish")
    
    # ...
    
    # In the state class
    class MyState(object):
        def __init__(self, state_name, scene_viewer):
            self.state_name = state_name
            self.scene_viewer = scene_viewer
    
        def onMenuAction(self, kwargs):
            # Call a method with the same name as the chosen ID
            # (but prefix the method name with an underscore)
            menu_item = kwargs["menu_item"]
            method = getattr(self, "_" + menu_item)
            return method(kwargs)
    
        def _delete(self, kwargs):
            # ...
    
        def _dup(self, kwargs):
            # ...
    
        def _done(self, kwargs):
            # ...
    
  • For toggle items, you can get the new on/off state of the toggle from the dictionary using the menu item’s ID as the key.

    # When you create the menu
    menu = hou.ViewerStateMenu('bend_menu', 'Bend')
    menu.addToggleItem("showpoints", "Show Points")
    
    # ...
    
    # In the menu handler method
    def onMenuAction(self, kwargs):
        if kwargs["menu_item"] == "showpoints":
            # Read the current state of the checkbox
            showpoints = kwargs["showpoints"]
    
  • For radio strip items, if the menu_item in the dictionary equals the ID you set on the radio strip (not an individual radio item), an item from the that strip was chosen.

    You can get the new current item index from the dictionary using the strip ID as the key.

    # When you create the menu
    menu = hou.ViewerStateMenu('bend_menu', 'Bend')
    menu.addRadioStrip("deform_type", "Deformation Type", "bend")
    menu.addRadioStripItem("deform_type", "bend", "Bend")
    menu.addRadioStripItem("deform_type", "squash", "Squash")
    
    # ...
    
    # In the menu handler method
    def onMenuAction(self, kwargs):
        if kwargs["menu_item"] == "deform_type":
            # Get the index of the currently selected item
            current = kwargs["deform_type"]
    

Reading menu settings

Whenever you include checkbox menu items (using hou.ViewerStateMenu.addToggleItem) or radio button items (using hou.ViewerStateMenu.addRadioStrip) in the context menu, Houdini passes the current state of those items in every kwargs dictionary passed to every event handler method.

This makes it easy to check a menu setting to change how you handle a certain event. In the following example, the onMouseEvent code checks menu settings to change how it interprets mouse clicks.

from __future__ import print_function


# Class implementing the state behavior
class MyState(object):
    def __init__(self, state_name, scene_viewer):
        self.state_name = state_name
        self.scene_viewer = scene_viewer

    def onMouseEvent(self, kwargs):
        ui_event = kwargs["ui_event"]
        if ui_event.device().isLeftButton():
            # Check if the "Bend" checkbox item in the menu is on
            if kwargs["bend"]:
                option = kwargs["bend_parm_option"]
                # ...
            else:
                # ...


# Function to generate the state's context menu
def create_menu():
    menu = hou.ViewerStateMenu('bend_menu', 'Bend')

    menu.addToggleItem( 'bend', 'Bend', True )

    menu.addSeparator()

    select_parm_menu = hou.ViewerStateMenu( 'select_parm_menu', 'Select Parm...' )
    select_parm_menu.addRadioStrip( 'bend_parm_option', 'Bend Parm', 0 )  
    select_parm_menu.addRadioStripItem( 'bend_parm_option', 'bend', 'Bend' )
    select_parm_menu.addRadioStripItem( 'bend_parm_option', 'twist', 'Twist' )
    select_parm_menu.addRadioStripItem( 'bend_parm_option', 'lengthscale', 'Length Scale' )
    select_parm_menu.addRadioStripItem( 'bend_parm_option', 'taper', 'Taper' )
    select_parm_menu.addRadioStripItem( 'bend_parm_option', 'squish', 'Squish' )

    menu.addMenu(select_parm_menu)

    return menu


# Function to generate the state template, which describes the state and any
# menus, handles, selectors, etc. bound to it
def createViewerStateTemplate():
    # Create the template object
    template = hou.ViewerStateTemplate(
        "mystate", "My State", hou.sopNodeTypeCategory()
    )
    # Bind the implementation class
    template.bindFactory(MyState)

    # Bind handles, selectors, menus...
    template.bindMenu(create_menu())

    return template

Menu hotkeys

Note

Under the new hotkey system introduced in Houdini 20.5, key assignments are explicitly managed in contexts rather than an attribute of the action.

  • Houdini uses symbols (such as h.pane.gview.world.selectall) to represent hotkey-able actions instead of the key itself (such as shift+p) so users can edit them using the hotkey editor.

    When you build the state menu, you can create your own custom hotkey symbol, and assign it to menu items in the state context menu. This will allow users to assign a hotkey to the menu item.

    You’ll also want to create a custom hotkey context to store key assignments to these actions.

  • The hou.ViewerStateMenu.addActionItem, hou.ViewerStateMenu.addToggleItem, and hou.ViewerStateMenu.addRadioStripItem methods accept Houdini hotkey symbol strings as an optional argument.

  • Hotkey symbols have a dotted prefix that specifies the category to which the symbol belongs. This is not to be confused with the context in which keys are assigned to a hotkey symbol, despite both having the same dotted format. Under the new hotkey system introduced in 20.5, the context and category are separate concepts. The same key can be assigned to different actions in different contexts.

    For a SOP viewer state, the prefix for your hotkeys will be:

    h.pane.gview.state.sop.‹state_name›.

    where ‹state_name› is the internal name for the state you specify when you create the state template.

    The same symbol is also typically used as the primary context for your state’s key assignments.

  • You will add a name for the specific action to the end of the category to make a hotkey symbol. So, for example, if you want to specify a Delete hotkey, you could use the symbol:

    h.pane.gview.state.sop.‹state_name›.delete

import hou


# Create hotkey symbols.  This should be done through a definitons object that
# is later attached to the viewer state template.
definitions = hou.PluginHotkeyDefinitions()

# Need to create the hotkey context with addContext
key_context = "h.pane.gview.state.sop.mystate"
definitions.addContext(
    key_context, "mystate Operation",
    "These keys apply to the mystate operation."
)

# Need to create the hotkey category for our commands with addCommandCategory
key_category = "h.pane.gview.state.sop.mystate"
definitions.addCommandCategory(
    key_category, "mystate Operation",
    "These commands apply to the mystate operation."
)

# Call addCommand with the symbol, a name, and a description
del_key = key_category + ".delete"
definitions.addCommand(del_key, "Delete", "Delete the current change")
dup_key = key_category + ".duplicate"
definitions.addCommand(
    dup_key, "Duplicate", "Copies the current change into a new change"
)
end_key = key_category + ".finish"
definitions.addCommand(end_key, "Finish", "Saves the current change")

# Call addDefaultBinding to assign default keys in your context.
definitions.addDefaultBinding(key_context, del_key, ["Del"])
definitions.addDefaultBinding(key_context, dup_key, ["Ctrl+D"])
definitions.addDefaultBinding(key_context, end_key, [])

# Build the menu
menu = hou.ViewerStateMenu('bend_menu', 'Bend')
# Call add*Item methods with the hotkey symbol as an extra argument 
menu.addActionItem("delete", "Delete", del_key)
menu.addActionItem("dup", "Duplicate", dup_key)
menu.addActionItem("done", "Finish", end_key)

Updating the menu

To improve the workflow of your python state, you might want to update the context menu state dynamically. For instance, enabling/disabling a specific menu or menu item, or setting a toggle item check mark based on some other state in Houdini.

Use the onMenuPreOpen handler for managing the UI states of a context menu. If onMenuPreOpen is implemented, Houdini will invoke it right before the menu is displayed, giving you a chance to perform the updates.

import hou

# State class implementation
class MyState(object):
    def __init__(self, state_name, scene_viewer):
        self.state_name = state_name
        self.scene_viewer = scene_viewer

        # handle accessors
        self.hvector = hou.Handle(self.scene_viewer, 'Vector')
        self.hdist = hou.Handle(self.scene_viewer, 'Distance')
        self.hmodif = hou.Handle(self.scene_viewer, 'Modifier')
        self.hupdir = hou.Handle(self.scene_viewer, 'Up Direction')

    # Update menu and menu item states
    def onMenuPreOpen( self, kwargs ):
        menu_id = kwargs['menu']
        node = kwargs['node']
        menu_states = kwargs['menu_states']
        menu_item_states = kwargs['menu_item_states']

        if menu_id == 'handle_show_menu':
            # Updates the 'handle_show_menu' items with the handles visibility 
            # property.

            menu_item_states['show_distance_handle']['value'] = self.hdist.visible()
            menu_item_states['show_modifier_handle']['value'] = self.hmodif.visible()
            menu_item_states['show_up_direction_handle']['value'] = self.hupdir.visible()
            menu_item_states['show_vector_handle']['value'] = self.hvector.visible()

        elif menu_id == 'select_parm_menu':
            # Enables the 'select_parm_menu' menu

            menu_states['enable'] = True

        elif menu_id == 'bend_menu':
            # Enables 'bend_menu' toggle items if the matching node parameter 
            # is unlocked. Disables it otherwise.

            bend = node.parm('bend')
            squish = node.parm('squish')
            twist = node.parm('twist')
            lengthscale = node.parm('lengthscale')
            taper = node.parm('taper')

            menu_item_states['bend_toggle_item']['enable'] = not bend.isLocked()
            menu_item_states['squish_toggle_item']['enable'] = not squish.isLocked()
            menu_item_states['twist_toggle_item']['enable'] = not twist.isLocked()
            menu_item_states['lengthscale_toggle_item']['enable'] = not lengthscale.isLocked()
            menu_item_states['taper_toggle_item']['enable'] = not taper.isLocked()

        elif menu_id == 'bend_parm_radio':
            # Make the 'bend_parm_radio' menu read-only by disabling all items ...
            menu_states['enable'] = False

            # ... and selecting a radio item with the first unlocked parameter we found
            param = 'bend'
            if not node.parm('bend').isLocked():
                param = 'bend'
            elif not node.parm('squish').isLocked():
                param = 'squish'
            elif not node.parm('twist').isLocked():
                param = 'twist'
            elif not node.parm('lengthscale').isLocked():
                param = 'lengthscale'
            elif not node.parm('taper').isLocked():
                param = 'taper'

            # select the radio item with the unlocked parameter name
            menu_states['value'] = param

    # Menu action callback
    def onMenuAction( self, kwargs ):
        menu_item = kwargs['menu_item']

        if menu_item == 'show_vector_handle':
            self.hvector.show(kwargs['show_vector_handle'])
        elif menu_item == 'show_distance_handle':
            self.hdist.show(kwargs['show_distance_handle'])
        elif menu_item == 'show_up_direction_handle':
            self.hupdir.show(kwargs['show_up_direction_handle'])
        elif menu_item == 'show_modifier_handle':
            self.hmodif.show(kwargs['show_modifier_handle'])

    def onEnter( self, kwargs ):
        # we want to hide the modifier hud slider handle when entering the state
        self.hmodif.show( False );

# Create a custom context menu
def createContextMenu():
    menu = hou.ViewerStateMenu('bend_menu', 'Bend')

    menu.addToggleItem( 'bend_toggle_item', 'Bend', True )
    menu.addToggleItem( 'twist_toggle_item', 'Twist', False )
    menu.addToggleItem( 'lengthscale_toggle_item', 'Length Scale', False )
    menu.addToggleItem( 'taper_toggle_item', 'Taper', False )
    menu.addToggleItem( 'squish_toggle_item', 'Squish', False )

    menu.addSeparator()

    select_parm_menu = hou.ViewerStateMenu( 'select_parm_menu', 'Select Parm to Modulate...' )
    select_parm_menu.addRadioStrip( 'bend_parm_radio', 'Bend Parm', 'bend' )  
    select_parm_menu.addRadioStripItem( 'bend_parm_radio', 'bend', 'Bend' )
    select_parm_menu.addRadioStripItem( 'bend_parm_radio', 'lengthscale', 'Length Scale' )
    select_parm_menu.addRadioStripItem( 'bend_parm_radio', 'squish', 'Squish' )
    select_parm_menu.addRadioStripItem( 'bend_parm_radio', 'taper', 'Taper' )
    select_parm_menu.addRadioStripItem( 'bend_parm_radio', 'twist', 'Twist' )
    menu.addMenu(select_parm_menu)

    menu.addSeparator()

    # handle menu for handles
    handle_show_menu = hou.ViewerStateMenu( 'handle_show_menu', 'Show Handles...' )
    handle_show_menu.addToggleItem( 'show_distance_handle', 'Show Distance', True )
    handle_show_menu.addToggleItem( 'show_modifier_handle', 'Show Modifier', False )
    handle_show_menu.addToggleItem( 'show_up_direction_handle', 'Show Up Direction', True )
    handle_show_menu.addToggleItem( 'show_vector_handle', 'Show Vector', True )
    menu.addMenu(handle_show_menu)    

    menu.addSeparator()

    return menu

# Register the python state
def createViewerStateTemplate():
    # Create the template object
    template = hou.ViewerStateTemplate("mystate", "My State", hou.sopNodeTypeCategory())

    # Bind the implementation class
    template.bindFactory(MyState)

    # Register any hotkey definitions
    #hotkey_definitions = hou.PluginHotkeyDefinition()
    #template.bindHotkeyDefinitions(hotkey_definitions)

    # attach popupmenu to state
    template.bindMenu(createContextMenu())

    # bind the handles
    template.bindHandle( 'vector', 'Vector', cache_previous_parms=True )
    template.bindHandle( 'distance', 'Distance' )
    template.bindHandle( 'hudslider', 'Modifier', cache_previous_parms=True, 
        settings="hudrangelow(0) hudrangehigh(5) hudlocklow(0) hudlockhigh(5)" )
    template.bindHandleStatic( 'vector', 'Up Direction', 
        [
                        ('group', 'input'),
                        ('originx', 'tx'),
                        ('originy', 'ty'),
                        ('originz', 'tz'),
                        ('upx', 'vx'),
                        ('upy', 'vy'),
                        ('upz', 'vz')
                ]
    )

    return template

Python scripting

Getting started

Next steps

Reference

  • hou

    Module containing all the sub-modules, classes, and functions to access Houdini.

Guru level

Python viewer states

Python viewer handles

Plugin types