git.lucas.co / hou-control
SideFX Houdini customization package
git clone https://git.lucas.co/hou-control.git

commitf17e7055fc809eba7ec203d6b5b464a021d8bc27
parent3762aa582b
authorLucas Galante <[email protected]>
date2026-09-17 14:06
session: Next Pane / Previous Pane warp the pointer between panes

Houdini has no keyboard focus for panes -- a pane-scoped hotkey goes to
whichever pane is under the pointer -- so cycling panes means moving the
pointer. The commands walk the visible panes in reading order (stowed
panes report a -1x-1 rectangle and are skipped; a maximized pane is the
only stop while it is up) and warp the cursor to the centre of the next
or previous one. Bound to alt+n / alt+shift+n, which takes alt+n away
from New File (ctrl+n still does it).

Co-Authored-By: Claude Fable 5.1 <[email protected]>

 MainMenuCommon.xml             | 10 +++++++++
 hc_hotkeys.json                |  2 ++
 python3.13libs/hc/hcsession.py | 46 ++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 58 insertions(+)

diff --git a/MainMenuCommon.xml b/MainMenuCommon.xml
index a81f444..9ed2bf0 100755
--- a/MainMenuCommon.xml
+++ b/MainMenuCommon.xml
@@ -72,6 +72,16 @@
         <scriptCode><![CDATA[from hc import HCSession; HCSession().toggleAllTabs()]]></scriptCode>
       </scriptItem>
 
+      <scriptItem id="h.hc_session_nextpane">
+        <label>Next Pane</label>
+        <scriptCode><![CDATA[from hc import HCSession; HCSession().focusNextPane()]]></scriptCode>
+      </scriptItem>
+
+      <scriptItem id="h.hc_session_prevpane">
+        <label>Previous Pane</label>
+        <scriptCode><![CDATA[from hc import HCSession; HCSession().focusPrevPane()]]></scriptCode>
+      </scriptItem>
+
       <scriptItem id="h.hc_session_restarthoudini">
         <label>Restart Houdini</label>
         <scriptCode><![CDATA[from hc import HCSession; HCSession().restartHoudini()]]></scriptCode>
diff --git a/hc_hotkeys.json b/hc_hotkeys.json
index cced0a6..37f8ce3 100644
--- a/hc_hotkeys.json
+++ b/hc_hotkeys.json
@@ -6,6 +6,8 @@
         "h.hc_session_opensettings": "alt+,",
         "h.hc_session_togglecurrenttabs": "alt+t",
         "h.hc_session_toggletabs": "alt+shift+t",
+        "h.hc_session_nextpane": "alt+n",
+        "h.hc_session_prevpane": "alt+shift+n",
         "h.hc_session_togglemainmenu": "alt+m",
         "h.hc_session_togglestatusbar": "alt+b",
         "h.pane.gview.state.obj.keycam.trans_up": "shift+k",
diff --git a/python3.13libs/hc/hcsession.py b/python3.13libs/hc/hcsession.py
index 5d74a8f..55b0029 100644
--- a/python3.13libs/hc/hcsession.py
+++ b/python3.13libs/hc/hcsession.py
@@ -1069,3 +1069,49 @@ class HCSession:
         visible = self.isAnyPaneShowingTabs()
         for pane in self.allPanes():
             pane.showTabs(not visible)
+
+    def cyclablePanes(self):
+        """Every pane a warp could land in, in reading order.
+
+        Houdini has no keyboard focus for panes: a pane-scoped hotkey goes
+        to whichever pane is under the pointer, so "focusing" a pane means
+        putting the pointer there. A stowed or collapsed pane reports a
+        -1 by -1 rectangle and is left out; so is every other pane while
+        one is maximized, since their rectangles are stale then.
+        """
+        panes = []
+        for pane in self.allPanes():
+            rect = pane.qtScreenGeometry()
+            if rect.width() <= 0 or rect.height() <= 0:
+                continue
+            panes.append((rect.top(), rect.left(), pane))
+        maximized = [entry for entry in panes if entry[2].isMaximized()]
+        if maximized:
+            panes = maximized
+        panes.sort(key=lambda entry: entry[:2])
+        return [entry[2] for entry in panes]
+
+    def _cyclePane(self, step):
+        panes = self.cyclablePanes()
+        if not panes:
+            return None
+        current = self.currentPane()
+        ids = [pane.hou_pane.id() for pane in panes]
+        if current is not None and current.hou_pane.id() in ids:
+            index = (ids.index(current.hou_pane.id()) + step) % len(panes)
+        else:
+            index = 0 if step > 0 else len(panes) - 1
+        target = panes[index]
+        from PySide6.QtGui import QCursor
+        QCursor.setPos(target.qtScreenGeometry().center())
+        return target
+
+    @command("Next Pane")
+    def focusNextPane(self):
+        """Warp the pointer to the centre of the next pane in reading order."""
+        self._cyclePane(1)
+
+    @command("Previous Pane")
+    def focusPrevPane(self):
+        """Warp the pointer to the centre of the previous pane in reading order."""
+        self._cyclePane(-1)