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

commitbfb73d1a558b5e77b0a0231d7fa44535d9e376e3
parentffd6c75955
authorLucas Galante <[email protected]>
date2026-09-16 12:21
hc: Pin toggle on every path-based tab

The HC Panel's Pin command lived on HCTab with a tabs= filter naming only
HCPathTab and HCParameterTab, so network editors and scene viewers -- which
are hou.PathBasedPaneTabs and pinnable -- never showed it, and tabs with no
path (Python shell, help browser) wrapped isPin() methods hou does not give
them. It now lives on HCPathTab with no filter: every path tab inherits it
and nothing else does. check.py asserts exactly that instead of asserting
Pin stays out of network editors.

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

 python3.13libs/hc/hcpathtab.py | 18 ++++++++++++++++++
 python3.13libs/hc/hctab.py     | 10 ----------
 tools/check.py                 | 12 +++++++++---
 3 files changed, 27 insertions(+), 13 deletions(-)

diff --git a/python3.13libs/hc/hcpathtab.py b/python3.13libs/hc/hcpathtab.py
index 196574d..345f278 100644
--- a/python3.13libs/hc/hcpathtab.py
+++ b/python3.13libs/hc/hcpathtab.py
@@ -1,6 +1,7 @@
 import hou
 from .hcgeo import HCGeo
 from .hcgeometryutils import merged_visible_geo
+from .hccommands import command
 from .hctab import HCTab
 from .hcvisibility import childCategory, collect_visible_nodes
 
@@ -21,6 +22,23 @@ class HCPathTab(HCTab):
     def pwd(self):
         return self.hou_tab.pwd()
 
+    """ Pin """
+
+    # Pinning is a hou.PathBasedPaneTab feature, so it belongs here and not
+    # on HCTab: a Python shell or a help browser has no isPin(). Every path
+    # tab -- Parm, DetailsView, NetworkEditor, SceneViewer -- inherits the
+    # command, which is why it carries no tabs= filter.
+
+    def isPin(self):
+        return self.hou_tab.isPin()
+
+    def setPin(self, value):
+        self.hou_tab.setPin(value)
+
+    @command("Pin", state="isPin")
+    def togglePin(self):
+        self.setPin(not self.isPin())
+
     def path(self):
         return self.pwd().path()
 
diff --git a/python3.13libs/hc/hctab.py b/python3.13libs/hc/hctab.py
index b85bc60..50198e7 100644
--- a/python3.13libs/hc/hctab.py
+++ b/python3.13libs/hc/hctab.py
@@ -135,16 +135,10 @@ class HCTab():
     def hasNetworkControls(self):
         return self.hou_tab.hasNetworkControls()
 
-    def isPin(self):
-        return self.hou_tab.isPin()
-
     def isShowingNetworkControls(self):
         value = self.hou_tab.isShowingNetworkControls()
         return value
 
-    def setPin(self, value):
-        self.hou_tab.setPin(value)
-
     def showNetworkControls(self, value):
         self.hou_tab.showNetworkControls(value)
 
@@ -170,7 +164,3 @@ class HCTab():
     def showChrome(self, visible):
         if self.hasNetworkControls():
             self.showNetworkControls(visible)
-
-    @command("Pin", tabs=("HCPathTab", "HCParameterTab"), state="isPin")
-    def togglePin(self):
-        self.setPin(not self.isPin())
diff --git a/tools/check.py b/tools/check.py
index 481c91f..19e363d 100644
--- a/tools/check.py
+++ b/tools/check.py
@@ -226,11 +226,17 @@ def check_commands():
         check(f"{label} panel binds", binds)
 
     def scoping_holds():
-        """Path/Pin live on HCTab but only apply to path-like tabs."""
+        """Path lives on HCTab but only applies to path-like tabs; Pin lives
+        on HCPathTab so every pinnable tab inherits it and nothing else does."""
         network = maps.commands(session, pane, blank(HCNetworkEditor))
         parm = maps.commands(session, pane, blank(HCParameterTab))
-        assert "Pin" in parm, "path tabs lost Pin"
-        assert "Pin" not in network, "Pin leaked into network editors"
+        other = maps.commands(session, pane, blank(HCTab))
+        for cls in (HCPathTab, HCParameterTab, HCNetworkEditor, HCSceneViewer):
+            bound = maps.commands(session, pane, blank(cls))
+            assert "Pin" in bound, f"{cls.__name__} lost Pin"
+            assert bound["Pin"].kind == "toggle", f"{cls.__name__}: Pin is not a toggle"
+        assert "Pin" not in other, "Pin leaked into tabs without a path"
+        assert "Path" in parm and "Path" not in other, "Path scoping broke"
         assert "Replace Node" in network, "network editors lost Replace Node"
         assert "Replace Node" not in parm, "Replace Node leaked into parameter tabs"
         return "tab-scoped commands stay scoped"