git.lucas.co / hou-control
git clone https://git.lucas.co/hou-control.git

commit803c61e07d2617b131c32c70b1cb41580c4a52cd
parent5e178760b5
authorLucas Galante <lsgalante12@gmail.com>
date2026-04-16 22:15
keycam: use hybrid hotkey system with robust manual interception

 viewer_states/keycam.py | 112 ++++++++++++++++++++++++++++++++++++------------
 1 file changed, 85 insertions(+), 27 deletions(-)

diff --git a/viewer_states/keycam.py b/viewer_states/keycam.py
index 0ed2f4a..90440ae 100644
--- a/viewer_states/keycam.py
+++ b/viewer_states/keycam.py
@@ -67,50 +67,78 @@ class State(object):
 
     def onKeyEvent(self, kwargs):
         self.cam.fitAspectRatio()
+        ui_event = kwargs['ui_event']
+        device = ui_event.device()
+        key = device.keyString()
+        is_shift = device.isShiftKey()
 
+        # Manual mapping as fallback/primary if symbols fail to resolve
         key_map = {
-            'o': self.cam.toggleProjection,
             'h': self.cam.rotateLeft,
-            'j': self.cam.rotateDown,
-            'k': self.cam.rotateUp,
             'l': self.cam.rotateRight,
-            # 'v': self.scene_viewer.nextView,
-            'Shift+h': self.cam.translateLeft,
-            'Shift+j': self.cam.translateDown,
-            'Shift+k': self.cam.translateUp,
-            'Shift+l': self.cam.translateRight,
-            # 'Ctrl+l': self.scene_viewer.nextLayout,
-            'f': self.cam.frame
-            # 'c': self.cam.center
+            'k': self.cam.rotateUp,
+            'j': self.cam.rotateDown,
+            'f': self.cam.frame,
+            'o': self.cam.toggleProjection,
+        }
+        
+        shift_key_map = {
+            'H': self.cam.translateLeft,
+            'L': self.cam.translateRight,
+            'K': self.cam.translateUp,
+            'J': self.cam.translateDown,
         }
-        key_map_persp = {
+
+        # 1. Handle Navigation
+        if is_shift:
+            # Shift+K might come through as 'K' or 'Shift+K'
+            # We strip 'Shift+' and use uppercase
+            lookup = key.replace('Shift+', '').upper()
+            if lookup in shift_key_map:
+                shift_key_map[lookup]()
+                self.guides.update()
+                return True
+        else:
+            lookup = key.lower()
+            if lookup in key_map:
+                key_map[lookup]()
+                self.guides.update()
+                return True
+
+        # 2. Zoom mappings
+        zoom_map_persp = {
             '-': lambda: self.cam.zoom('out'),
             '=': lambda: self.cam.zoom('in'),
             'Shift+-': lambda: self.cam.zoomOrtho('out'),
             'Shift+=': lambda: self.cam.zoomOrtho('in')
         }
-        key_map_ortho = {
+        zoom_map_ortho = {
             '-': lambda: self.cam.zoomOrtho('out'),
             '=': lambda: self.cam.zoomOrtho('in'),
             'Shift+-': lambda: self.cam.zoom('out'),
             'Shift+=': lambda: self.cam.zoom('in')
         }
 
-        if self.cam.projection == 'perspective':
-            key_map = key_map | key_map_persp
-        else:
-            key_map = key_map | key_map_ortho
-
-        key = kwargs['ui_event'].device().keyString()
-        if key in key_map:
-            key_map[key]()
-            print(self.cam.p)
-            print(self.cam.t)
-            print(self.cam.r)
+        zoom_map = zoom_map_persp if self.cam.projection == 'perspective' else zoom_map_ortho
+        
+        if key in zoom_map:
+            zoom_map[key]()
             self.guides.update()
             return True
-        else:
-            return False
+        
+        if key.lower() in zoom_map:
+            zoom_map[key.lower()]()
+            self.guides.update()
+            return True
+        
+        # 3. Explicitly consume 'j' to prevent "Modify Channels"
+        if key.lower() == 'j':
+            return True
+
+        return False
+
+    def onKeyboardEvent(self, kwargs):
+        return self.onKeyEvent(kwargs)
 
     def onMenuAction(self, kwargs):
         menu_item = kwargs['menu_item']
@@ -272,6 +300,33 @@ def createViewerStateTemplate():
     template.bindFactory(State)
     template.bindIcon('DESKTOP_application_sierra')
 
+    # Hotkey Definitions
+    # Using the official system to define context-sensitive hotkeys
+    hotkeys = hou.PluginHotkeyDefinitions()
+    ctx = 'h.pane.gview.state.obj.keycam'
+    hotkeys.addContext(ctx, 'HC Keycam', 'Hotkeys for the HC Keycam viewer state')
+    
+    # Define Commands
+    commands = {
+        'rotate_left':   ('Rotate Left',   ['h']),
+        'rotate_right':  ('Rotate Right',  ['l']),
+        'rotate_up':     ('Rotate Up',     ['k']),
+        'rotate_down':   ('Rotate Down',   ['j']),
+        'trans_left':    ('Translate Left', ['Shift+h']),
+        'trans_right':   ('Translate Right',['Shift+l']),
+        'trans_up':      ('Translate Up',   ['Shift+k']),
+        'trans_down':    ('Translate Down', ['Shift+j']),
+        'frame':         ('Frame',         ['f']),
+        'toggle_proj':   ('Toggle Projection', ['o']),
+    }
+    
+    for cmd_id, (label, defaults) in commands.items():
+        symbol = f"{ctx}.{cmd_id}"
+        hotkeys.addCommand(symbol, label, "")
+        hotkeys.addDefaultBinding(ctx, symbol, defaults)
+    
+    template.bindHotkeyDefinitions(hotkeys)
+
     # Parameters
 
     template.bindParameter(
@@ -430,7 +485,7 @@ def createViewerStateTemplate():
     """ Context Menu """
 
     menu = hou.ViewerStateMenu('keycam_menu', 'Keycam Menu')
-    menu.addActionItem('frame', 'Frame')
+    menu.addActionItem('frame', 'Frame', hotkey='h.pane.gview.state.obj.keycam.frame')
     menu.addActionItem('reset', 'Reset')
 
     set_view_menu = hou.ViewerStateMenu('set_view_menu', 'Set View')
@@ -453,6 +508,9 @@ def createViewerStateTemplate():
     menu.addMenu(guide_menu)
 
     menu.addSeparator()
+    # Add toggle projection to menu so we can bind the hotkey
+    menu.addToggleItem('toggle_proj', 'Toggle Projection', 0, hotkey='h.pane.gview.state.obj.keycam.toggle_proj')
+    
     template.bindMenu(menu)
 
     # Ok