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

commit50aab36035326d779b8cdfa2f50f8b94d51ac04f
parent389170976f
authorLucas Galante <[email protected]>
date2026-09-15 12:25
statusbar: give the circle its own X window so clicks reach it

Houdini draws its UI into one native GL window that owns mouse input
across its area; the circle, a plain child widget painted above it,
received no mouse or hover event at all. Same fix and same measurement
as the split handles: WA_NativeWindow plus an early winId(), with a
circular window mask so the native surface's corners stay hidden and
the ellipse painted edge to edge.

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

 python3.13libs/hc/hcstatusbar.py | 18 +++++++++++++++---
 tools/check.py                   | 15 +++++++++++++++
 2 files changed, 30 insertions(+), 3 deletions(-)

diff --git a/python3.13libs/hc/hcstatusbar.py b/python3.13libs/hc/hcstatusbar.py
index 8a1fd33..7d0c335 100644
--- a/python3.13libs/hc/hcstatusbar.py
+++ b/python3.13libs/hc/hcstatusbar.py
@@ -21,7 +21,7 @@ import tempfile
 import hou
 import lxml.etree as ET
 from PySide6.QtCore import QEvent, QObject, QPoint, Qt, QTimer
-from PySide6.QtGui import QColor, QFont, QPainter, QPen
+from PySide6.QtGui import QColor, QFont, QPainter, QRegion
 from PySide6.QtWidgets import QFrame, QHBoxLayout, QWidget
 
 
@@ -123,6 +123,16 @@ class StatusCircle(QWidget):
         self.setObjectName(self.OBJECT_NAME)
         self.setFixedSize(HCSTATUSBAR_SIZE, HCSTATUSBAR_SIZE)
         self.setCursor(Qt.PointingHandCursor)
+        # Its own X window, or it can be seen but never clicked: Houdini
+        # draws its UI into one native GL window that owns mouse input across
+        # its area, and a plain child widget stacked above only paints there.
+        # Same fix as hcsplithandles._SplitHandle, measured the same way --
+        # without it the circle received no mouse or hover event at all.
+        self.setAttribute(Qt.WA_NativeWindow, True)
+        self.winId()
+        # A native child has its own surface, so the corners outside the
+        # circle would be its uninitialised black; mask them away.
+        self.setMask(QRegion(self.rect(), QRegion.Ellipse))
         self._state = None
         self._minutes = 0
         self._timer = QTimer(self)
@@ -155,8 +165,10 @@ class StatusCircle(QWidget):
     def paintEvent(self, event):
         painter = QPainter(self)
         painter.setRenderHint(QPainter.Antialiasing)
-        rect = self.rect().adjusted(2, 2, -2, -2)
-        painter.setPen(QPen(QColor(0, 0, 0, 90), 1))
+        # Fill to the edge: the window mask is the circle's outline, and any
+        # pixel inside it left unpainted shows the native surface's black.
+        rect = self.rect()
+        painter.setPen(Qt.NoPen)
         painter.setBrush(self.color())
         painter.drawEllipse(rect)
         if self._state == "autosave":
diff --git a/tools/check.py b/tools/check.py
index b118804..b2491ea 100644
--- a/tools/check.py
+++ b/tools/check.py
@@ -634,6 +634,21 @@ def check_status_circle():
 
     check("menu XML filtered to runnable items", menu_documents_are_runnable)
 
+    def circle_is_a_native_window():
+        """Same lesson as the split handles: over Houdini's GL window a plain
+        child widget is painted but never clicked."""
+        from PySide6.QtCore import Qt
+        circle = hcstatusbar.StatusCircle()
+        try:
+            assert circle.testAttribute(Qt.WA_NativeWindow), "status circle is not a native window"
+            assert circle.internalWinId(), "native window was never created"
+            assert not circle.mask().isEmpty(), "no window mask; native corners would show black"
+        finally:
+            circle.deleteLater()
+        return "native, winId set, masked"
+
+    check("circle owns an X window", circle_is_a_native_window)
+
 
 def check_geometry():
     """The visible-geometry walk, after HCNode was removed from under it."""