Untitled
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