diff --git a/app/overlay/overlay_widget.py b/app/overlay/overlay_widget.py index e456b99..c5bdd25 100644 --- a/app/overlay/overlay_widget.py +++ b/app/overlay/overlay_widget.py @@ -31,11 +31,12 @@ class OverlayWidget(QWidget): def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) - # Make widget transparent to mouse and visual background + # Child widget — NO window flags (FramelessWindowHint would detach it + # from the parent and create an invisible top-level window). + # WA_TranslucentBackground on a child only works when the parent is + # also translucent, so we paint the background ourselves in paintEvent. self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True) self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground, True) - self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True) - self.setWindowFlags(Qt.WindowType.FramelessWindowHint) self._snapshot: TelemetrySnapshot | None = None self._visible_overlay: bool = True diff --git a/app/ui/main_window.py b/app/ui/main_window.py index 64666f2..7168954 100644 --- a/app/ui/main_window.py +++ b/app/ui/main_window.py @@ -6,7 +6,7 @@ import logging from PySide6.QtCore import Qt, QTimer from PySide6.QtMultimediaWidgets import QVideoWidget -from PySide6.QtWidgets import QLabel, QMainWindow, QSizePolicy, QStatusBar +from PySide6.QtWidgets import QLabel, QMainWindow, QSizePolicy, QStatusBar, QVBoxLayout, QWidget from app.camera.camera_enumerator import CameraEnumerator, CameraInfo from app.camera.camera_service import CameraService @@ -41,21 +41,34 @@ class MainWindow(QMainWindow): self._dispatcher = FrameDispatcher(self) self._telemetry = TelemetryCollector(parent=self) - # --- Video widget (central widget) --- - self._video_widget = QVideoWidget(self) + # --- Central container --- + # We need an extra QWidget layer between QMainWindow and QVideoWidget so + # that OverlayWidget can be a sibling of QVideoWidget (not its child). + # QVideoWidget renders via a native D3D/GL surface that occludes any + # QWidget children painted on top of it. + self._container = QWidget(self) + self._container.setStyleSheet("background: black;") + container_layout = QVBoxLayout(self._container) + container_layout.setContentsMargins(0, 0, 0, 0) + container_layout.setSpacing(0) + + # --- Video widget --- + self._video_widget = QVideoWidget(self._container) self._video_widget.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding ) self._video_widget.setAspectRatioMode(Qt.AspectRatioMode.KeepAspectRatio) - self.setCentralWidget(self._video_widget) + container_layout.addWidget(self._video_widget) + self.setCentralWidget(self._container) # Connect camera session to video widget — this is the zero-copy render path self._camera_service.capture_session().setVideoOutput(self._video_widget) # --- Overlay --- - self._overlay = OverlayWidget(parent=self._video_widget) + # Sibling of QVideoWidget inside _container; positioned manually so it + # floats above the video without being occluded by the native GL surface. + self._overlay = OverlayWidget(parent=self._container) self._overlay.raise_() - self._overlay.resize(self._video_widget.size()) # --- Menu bar --- self._menu = AppMenuBar(self) @@ -72,6 +85,8 @@ class MainWindow(QMainWindow): # --- Enumerate cameras and start --- QTimer.singleShot(0, self._initialise_cameras) + # Reposition overlay after the event loop starts (layout is finalised) + QTimer.singleShot(0, self._reposition_overlay) # ------------------------------------------------------------------ # Initialisation @@ -161,9 +176,16 @@ class MainWindow(QMainWindow): def resizeEvent(self, event) -> None: # noqa: N802 super().resizeEvent(event) - # Keep overlay covering the video widget - if hasattr(self, "_overlay") and hasattr(self, "_video_widget"): - self._overlay.resize(self._video_widget.size()) + self._reposition_overlay() + + def _reposition_overlay(self) -> None: + """Keep the overlay covering the video widget exactly.""" + if not (hasattr(self, "_overlay") and hasattr(self, "_video_widget")): + return + # _overlay and _video_widget share the same parent (_container), + # so video_widget.geometry() is already in the right coordinate space. + self._overlay.setGeometry(self._video_widget.geometry()) + self._overlay.raise_() def closeEvent(self, event) -> None: # noqa: N802 self._camera_service.stop()