SideFX Houdini customization package
git clone https://git.lucas.co/hou-control.git
hc: drop the deferred pin -- the tracker is what actually fixes the handles
af0605e claimed Houdini relayouts its panes on the next event loop pass, so
that pinning straight after setSplitFraction read stale geometry. Checked
against a live session over the bridge: that is false. setSplitFraction took a
child pane from w=2459 to w=1299 within the same call, and qtScreenGeometry()
reported the new value immediately. Pinning synchronously was always correct.
So the QTimer.singleShot deferral fixed nothing and cost a frame of latency on
every throttle tick of a drag. Removed, along with _pinDeferred and the check
that covered it.
What does fix it is the 250ms tracker, and the live session showed exactly
why. With the tracker stopped, moving a split by any means other than dragging
its own handle left that handle at the old boundary -- still stale a second
later. Restarted, it corrected on the next tick. The handle only ever repinned
when it was itself dragged or the main window resized, and nothing else
reaches it.
Dragging a handle was already fine and still is: simulated over the bridge, a
300px drag moved the handle exactly 300px and applied the matching fraction.
Comments corrected to say this rather than the relayout story.
Co-Authored-By: Claude Opus 5 <[email protected]>
python3.13libs/hc/hcsplithandles.py | 42 +++++++++++++++----------------------
tools/check.py | 9 --------
2 files changed, 17 insertions(+), 34 deletions(-)
diff --git a/python3.13libs/hc/hcsplithandles.py b/python3.13libs/hc/hcsplithandles.py
index cae2198..c473f5c 100644
--- a/python3.13libs/hc/hcsplithandles.py
+++ b/python3.13libs/hc/hcsplithandles.py
@@ -7,10 +7,14 @@ HCSPLITHANDLE_THICKNESS = 20
HCSPLITHANDLE_LENGTH = 80
HCSPLITHANDLE_MIN_FRACTION = 0.05
HCSPLITHANDLE_MAX_FRACTION = 0.95
-# How often handles re-check where their boundary actually is. A split can move
-# for reasons no handle sees -- Houdini's own splitters, Contract/Expand Pane,
-# maximizing a pane -- and without this the handle sits at the old boundary
-# until something resizes the main window.
+# How often handles re-check where their boundary actually is.
+#
+# This is what keeps them correct. Dragging a handle repins it directly, but a
+# split moved any other way -- Houdini's own splitters, Contract/Expand Pane,
+# maximizing a pane, a desktop change -- was seen by nothing, and the handle
+# stayed at the old boundary indefinitely. Verified against a live session: with
+# this timer stopped, a setSplitFraction left the handle stale a second later;
+# restarted, it corrected on the next tick.
HCSPLITHANDLE_TRACK_MS = 250
@@ -81,10 +85,7 @@ class _SplitHandle(QWidget):
self._drag_child = None
self._pending_fraction = None
self._throttle.stop()
- # Deferred for the same reason as in _applyPending: the final
- # setSplitFraction has not been laid out yet at this point, so pinning
- # now would leave every handle at the second-to-last position.
- QTimer.singleShot(0, self.manager._pin_all)
+ self.manager._pin_all()
def isDragging(self):
return self._origin is not None
@@ -94,21 +95,11 @@ class _SplitHandle(QWidget):
return
self._drag_child.setSplitFraction(self._pending_fraction)
self._pending_fraction = None
- # Houdini repositions its panes on the next event loop pass, so the
- # geometry qtScreenGeometry() reports right now is still the pre-change
- # one. Pinning from it puts the handle where the boundary *was* -- the
- # handle then trails the split by one throttle tick for the whole drag
- # and is left behind when the drag ends.
- QTimer.singleShot(0, self._pinDeferred)
-
- def _pinDeferred(self):
- # hide() can tear the handle down between scheduling this and its
- # firing, leaving a wrapper around a deleted widget.
- try:
- if self in self.manager.handles:
- self.manager._pin(self)
- except RuntimeError:
- pass
+ # Houdini relayouts its panes synchronously here -- qtScreenGeometry()
+ # already reports the new split when this returns, so pinning now is
+ # correct. (Measured over the bridge: setSplitFraction took a child
+ # from w=2459 to w=1299 within the same call.)
+ self.manager._pin(self)
class _ResizeFilter(QObject):
@@ -210,8 +201,9 @@ class HCSplitHandles:
def _pin_all(self):
for handle in self.handles:
- # The handle being dragged pins itself from _applyPending; pinning
- # it from here too would fight that with staler geometry.
+ # The handle being dragged pins itself from _applyPending, on the
+ # throttle; a tracker tick landing between those would apply the
+ # same result twice for no reason.
if handle.isDragging():
continue
self._pin(handle)
diff --git a/tools/check.py b/tools/check.py
index e5bcebd..b52a9ce 100644
--- a/tools/check.py
+++ b/tools/check.py
@@ -379,15 +379,6 @@ def check_split_handles():
check("horizontal boundary", horizontal_boundary)
- def deferred_pin_survives_teardown():
- stale = sh._SplitHandle(main, split, horizontal_boundary=False, manager=mgr)
- stale._pinDeferred() # never added to mgr.handles
- mgr.handles.remove(handle)
- handle._pinDeferred() # removed between schedule and fire
- mgr.handles.append(handle)
- return "no exception once the handle is gone"
-
- check("deferred pin after teardown", deferred_pin_survives_teardown)
def check_geometry():