Untitled

 avatar
unknown
python
a year ago
16 kB
5
Indexable
"""
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
Rule Based Context Sensitive Hotkey System
////////////////////////////////////////////////////////////////////////////////////

=== Introduction ===

This is an implementation of a rule based hotkey system that can be 
customized per context in Houdini.

The default hotkeys focus on the most commonly performed operations,
rather than only the most useful operations. For example Point Deform SOP
is very useful but it's not common to create dozens of it in rapid succession,
compared to Attribute Wrangle SOP, or Blast SOP.


The current assignment also mostly takes advantage of mnemonics and key 
layout/proximity for similar operations. For example G for Group Create SOP, H for 
Group Delete SOP, J for Group Promote SOP.


This system has support for creating new nodes in many ways, but it also 
supports executing arbitrary functions implemented by the user.

By default all new nodes created are automatically connected to the  selected
nodes based on their order.

If the new node has only 1 input connection, it will be connected to the first selected node.

If it has more than 1 input connections, then the selected nodes will be connected 
based on their horizontal order, starting from left to right of the screen.



=== Hotkey Syntax ===

1. Creating a Node:

   op:nodetypename

   - Purpose: Create a node instance of a specific type.

   - Details: No need to specify context. Specify the full namespace and version 
     if used in the node type name. To create the latest version, omit the version.

   To load an existing preset on creation:
   op:nodetypename('Optional Custom Preset Name')

   To press buttons:
   op:nodetypename[buttonName1, buttonName2]


2. Executing a Custom Python Function:

   fn:functionname()

   - Purpose: Calls a custom Python function within the context of the 
     executeActionString function found in utility_hotkey_system.py.

   - Access to variables:
   uievent
   action
   editor = uievent.editor
   mousestate = uievent.mousestate
   modifierstate = uievent.modifierstate
   etc


3. Popup Menu:

   mn:[('Optional Custom Label', 'op:volumesamplefile', 'S'),
   ('op:volumesamplevfile', 'V'),
   ('op:volumebound(Name of Custom Preset to Load)', 'Ctrl+B'),
   ('op:volumeresample', 'R', 'icon:SOP_volumeresample'),
   ('op:volumegradientfile', 'G')]

   - Purpose: Displays a popup menu with predefined operators or custom actions.

   - Details: The first item is an optional label. The third item defines the hotkey.



=== Separators ===

Each op:, fn:, or mn: item must be separated by ~.



=== Action Modifiers ===

Actions can be customized based on the active modifiers during hotkey invocation. 

Input Modifiers:
- C: Ctrl
- A: Alt
- S: Shift
- P: Space
- L: LMB
- R: RMB
- M: MMB

Selection Modifiers:
- SE: Node Selection > 0
- NS: Node Selection == 0

Modifiers should be listed between <>. For example: op:bend<C S> implies Ctrl+Shift.
Modifier order doesn't matter.



=== Order of Precedence ===

The system determines which action to execute based on the following order:

1. Actions with both input modifiers and node selection modifier.
2. Actions with only input modifiers.
3. Actions with only node selection modifier.
4. Actions without any modifiers.

This structure ensures the most appropriate action is always selected.
"""





import hou, nodegraph, os, sys
from collections import defaultdict
from canvaseventtypes import *
from utility_ui import getSessionVariable, setSessionVariable
import utility_generic
import utility_hotkey_system
import traceback
from PySide2 import QtCore, QtWidgets, QtGui
import nodegraphutils as utils
import nodegraphbase as base
import nodegraphstates as states
import importlib

try:
    from utility_overlay_network_editor import setOverlayNetworkEditorVisible
except:
    pass



this = sys.modules[__name__]
currentdir = os.path.dirname(os.path.realpath(__file__))

def __reload_pythonlibs(showstatus=True):
    if showstatus:
        print ("Reloading hotkey system...")

    importlib.reload(this)
    importlib.reload(utility_hotkey_system)

fs_watcher = QtCore.QFileSystemWatcher()
fs_watcher.addPath(os.path.join(currentdir, "nodegraphhooks.py"))
fs_watcher.addPath(os.path.join(currentdir, "utility_hotkey_system.py"))


fs_watcher.fileChanged.connect(__reload_pythonlibs)



class CustomViewPanHandler(base.EventHandler):
    def __init__(self, start_uievent):
        base.EventHandler.__init__(self, start_uievent)
        self.startbounds = start_uievent.editor.visibleBounds()
        self.olddefaultcursor = start_uievent.editor.defaultCursor()
        self.start_uievent.editor.setDefaultCursor(utils.theCursorPan)

    def handleEvent(self, uievent, pending_actions):
        if uievent.eventtype == 'mousedrag':# and delay >= hou.session.MMBSelectNearestNodeTimeLimit * 0.3:
            dist = uievent.mousestartpos - uievent.mousepos
            dist = uievent.editor.sizeFromScreen(dist)
            bounds = hou.BoundingRect(self.startbounds)
            bounds.translate(dist)
            uievent.editor.setVisibleBounds(bounds)
            return self

        # Select Nearest Node using MMB
        elif uievent.eventtype == 'mouseup':
            self.start_uievent.editor.setDefaultCursor(self.olddefaultcursor)
            if hou.session.UseMMBToSelectNearestNode and self.start_uievent.mousestate.mmb:
                delay = uievent.time - hou.session.mouseDownEventTime
                if delay < hou.session.MMBSelectNearestNodeTimeLimit:
                    # Restore network editor visible bounds back because it's not a panning event
                    uievent.editor.setVisibleBounds(hou.session.networkEditorVisibleBounds)
                    utility_generic.selectNearestNode(uievent)
                    return None

            return None

        # Keep handling events until the mouse button is released.
        return self



class CustomBackgroundMouseHandler(base.EventHandler):
    def handleEvent(self, uievent, pending_actions):
        if uievent.eventtype == 'mousedrag':
            handler = None
            if self.start_uievent.mousestate.lmb:
                handler = states.BoxPickHandler(self.start_uievent, True)
            elif base.isPanEvent(self.start_uievent):
                if hou.session.UseMMBToSelectNearestNode:
                    handler = base.CustomViewPanHandler(self.start_uievent)
                else:
                    handler = base.ViewPanHandler(self.start_uievent)
            elif base.isScaleEvent(self.start_uievent):
                handler = base.ViewScaleHandler(self.start_uievent)
            if handler:
                return handler.handleEvent(uievent, pending_actions)

        elif uievent.eventtype == 'mouseup':
            # Select Nearest Node using MMB
            if hou.session.UseMMBToSelectNearestNode and self.start_uievent.mousestate.mmb:
                delay = uievent.time - hou.session.mouseDownEventTime
                if delay < hou.session.MMBSelectNearestNodeTimeLimit:
                    utility_generic.selectNearestNode(uievent)
                    return None

            if self.start_uievent.mousestate.lmb:
                with hou.undos.group('Clear selection', uievent.editor):
                    uievent.editor.clearAllSelected()

            elif self.start_uievent.mousestate.rmb:
                uievent.editor.openTabMenu(key = utils.getDefaultTabMenuKey(uievent.editor))

            return None

        # deselect TOP workitems
        elif uievent.eventtype == 'mousedown':
            if self.start_uievent.mousestate.lmb:
                pwd = uievent.editor.pwd()
                if isinstance(pwd, hou.OpNode):
                    pwd.deselectWorkItem()

        # Select Nearest Node using LMB
        elif uievent.eventtype == 'doubleclick':
            if hou.session.UseLMBToSelectNearestNode and self.start_uievent.mousestate.lmb:
                utility_generic.selectNearestNode(uievent)
                return None

        # Keep handling events until the mouse is dragged, or the mouse button
        # is released.
        return self



class BoxPickHandler(base.EventHandler):
    def __init__(self, start_uievent, set_cursor = False):
        base.EventHandler.__init__(self, start_uievent)
        # Remember the node-space position of where the box starts.
        self.start_pos = start_uievent.mousestartpos
        self.start_pos = start_uievent.editor.posFromScreen(self.start_pos)
        self.set_cursor = set_cursor
        if set_cursor:
            self.oldcursormap = start_uievent.editor.cursorMap()
            self.olddefaultcursor = start_uievent.editor.defaultCursor()
            start_uievent.editor.setCursorMap({})
            self.setSelectCursor(start_uievent)

    def getItemsInBox(self, items):
        items = list(item[0] for item in items)
        # If we have any non-wires in the box, ignore the wires.
        has_non_wire = any((not isinstance(item, hou.NodeConnection)
                            for item in items))
        if has_non_wire:
            items = list(item for item in items
                         if not isinstance(item, hou.NodeConnection))
            # Select box picked nodes in visual order.
            if utils.isNetworkHorizontal(items[0].parent()):
                items.sort(key = lambda item : -item.position().y())
            else:
                items.sort(key = lambda item : item.position().x())

        return items

    def setSelectCursor(self, uievent):
        if self.set_cursor:
            if isinstance(uievent, MouseEvent) or \
               isinstance(uievent, KeyboardEvent):
                if uievent.modifierstate.ctrl and uievent.modifierstate.shift:
                    cursor = utils.theCursorSelectToggle
                elif uievent.modifierstate.ctrl:
                    cursor = utils.theCursorSelectRemove
                elif uievent.modifierstate.shift:
                    cursor = utils.theCursorSelectAdd
                else:
                    cursor = utils.theCursorSelect
                uievent.editor.setDefaultCursor(cursor)

    def handleBoxPickComplete(self, items, uievent):
        view.modifySelection(uievent, None, items)

    def handleEvent(self, uievent, pending_actions):
        # Set the current selection cursor based on our modifier key states.
        self.setSelectCursor(uievent)

        # Check if the user wants to enter the scroll state.
        if isScrollStateEvent(uievent):
            return ScrollStateHandler(uievent, self)

        if uievent.eventtype == 'mousedrag':
            autoscroll.startAutoScroll(self, uievent, pending_actions)
            # Convert the node space position to a screen-space position for
            # the starting point of the box (which may be outside the visible
            # area of the view).
            pos1 = uievent.editor.posToScreen(self.start_pos)
            pos2 = uievent.editor.screenBounds().closestPoint(uievent.mousepos)
            rect = hou.BoundingRect(pos1, pos2)
            pickbox = hou.NetworkShapeBox(rect,
                            hou.ui.colorFromName('GraphPickFill'), 0.3,
                            True, True)
            pickboxborder = hou.NetworkShapeBox(rect,
                            hou.ui.colorFromName('GraphPickFill'), 0.8,
                            False, True)
            self.editor_updates.setOverlayShapes([pickbox, pickboxborder])
            items = uievent.editor.networkItemsInBox(pos1,pos2,for_select=True)
            items = self.getItemsInBox(items)
            uievent.editor.setPreSelectedItems(items)
            return self

        elif uievent.eventtype == 'mouseup':
            pos1 = uievent.editor.posToScreen(self.start_pos)
            pos2 = uievent.editor.screenBounds().closestPoint(uievent.mousepos)
            items = uievent.editor.networkItemsInBox(pos1,pos2,for_select=True)
            items = self.getItemsInBox(items)
            uievent.editor.setPreSelectedItems(())
            self.handleBoxPickComplete(items, uievent)
            if self.set_cursor:
                uievent.editor.setCursorMap(self.oldcursormap)
                uievent.editor.setDefaultCursor(self.olddefaultcursor)
            return None

        # Keep handling events until the mouse button is released.
        return self



def createEventHandler(uievent, pending_actions):
    if not isinstance(uievent.editor, hou.NetworkEditor):
        return None, False


    if uievent.eventtype == 'mousedown' and uievent.selected.item is None and not uievent.selected.name.startswith('overview'):
        hou.session.networkEditorVisibleBounds = uievent.editor.visibleBounds()
        hou.session.mouseDownEventTime = uievent.time
        return CustomBackgroundMouseHandler(uievent), True

    # if uievent.eventtype == 'mousemove':
    #     currentnode = uievent.editor.currentNode()
    #     a = uievent.located.item
    #     if a:
    #         noderect = uievent.editor.itemRect(a)
    #         #print (uievent.located.item)
    #         s1 = hou.NetworkShapeNodeShape(noderect, 'rect')

    #         uievent.editor.setShapes([s1])


    if getSessionVariable("useRMBToSelectDisplayNodes") and uievent.eventtype == 'mousedown' and uievent.mousestate.rmb:
        node = uievent.selected.item
        
        if node and not isinstance(node, hou.NodeConnection) and not isinstance(node, hou.NetworkDot) and not isinstance(node, hou.OpSubnetIndirectInput):
            category = node.type().category().name()
            if category in hou.nodeTypeCategories().keys() and category != "Vop":
                if uievent.modifierstate.ctrl:
                    if uievent.modifierstate.shift:
                        utility_generic.selectableTemplateNearestNodeInEditor(nearestNode=node)
                    if uievent.modifierstate.alt:
                        utility_generic.bypassNearestNodeInEditor(nearestNode=node)
                    else:
                        utility_generic.templateNearestNodeInEditor(nearestNode=node)
                elif uievent.modifierstate.shift:
                    utility_generic.displayNearestNodeInEditor(nearestNode=node)
                elif uievent.modifierstate.alt:
                    utility_generic.showNodeMenuNearestNodeInEditor()
                else:
                    utility_generic.selectDisplayNearestNodeInEditor(nearestNode=node)


                return None, True



    if isinstance(uievent, KeyboardEvent):
        key = utility_generic.getUnshiftedKey(uievent.key, uievent.modifierstate)
        
        if hou.session.useVolatileSpaceToToggleNetworkEditor:
            hou.session.spaceKeyIsDown = uievent.editor.isVolatileHotkeyDown('h.pane.wsheet.view_mode')
        #print("space is down:", uievent.editor.isVolatileHotkeyDown('h.pane.wsheet.view_mode'))

        if uievent.eventtype == 'keyhit':
            return utility_hotkey_system.invokeActionFromKey(uievent)

    return None, False
Editor is loading...
Leave a Comment