From 553e31075df6043a29dc79abec74fa334c1d7b48 Mon Sep 17 00:00:00 2001 From: pax Date: Wed, 8 Apr 2026 16:30:37 -0500 Subject: [PATCH] Privacy screen: resume video on un-hide, popout uses in-place overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related improvements to the Ctrl+P privacy screen flow. 1. Resume video on un-hide Pre-fix: Ctrl+P paused any playing video in the embedded preview and the popout, but the second Ctrl+P only hid the privacy overlay — the videos stayed paused. The user had to manually click Play to resume. Fix: in _toggle_privacy's privacy-off branch, mirror the privacy-on pause logic with resume() calls on the embedded preview's video player and the popout's video. Unconditional resume — if the user manually paused before Ctrl+P, the auto-resume on un-hide is a tiny annoyance, but the common case (privacy hides → user comes back → video should be playing again) wins. 2. Popout privacy uses an in-place overlay instead of hide() Pre-fix attempt: privacy-on called self._fullscreen_window.hide() and privacy-off called .show(). On Wayland (Hyprland) the hide→show round trip drops the window's position because the compositor unmaps the window on hide and remaps it at the default tile position on show. A first attempt at restoring the position via a deferred hyprctl_resize_and_move dispatch in privacy_show didn't take — by the time the dispatch landed, the window had already been re-tiled and the move was gated by `if not win.get("floating"): return`. Cleaner fix: don't hide the popout window at all. FullscreenPreview gains its own _privacy_overlay (a black QWidget child of central, parallel to the existing toolbar / controls bar children) that privacy_hide raises over the media stack. The popout window stays mapped, position is preserved automatically because nothing moves, and the overlay covers the content visually. privacy_hide / privacy_show methods live in FullscreenPreview, not in main_window — popout-internal state belongs to the popout module. _toggle_privacy in main_window just calls them. This also makes adding more popout-side privacy state later (e.g. fullscreen save) a one-method change inside the popout class. Also added a _popout_was_visible flag in BooruApp._toggle_privacy so privacy-off only restores the popout if it was actually visible at privacy-on time. Without the gate, privacy-off would inappropriately re-show a popout the user had closed before triggering privacy. Verified manually: - popout open + drag to non-default pos + Ctrl+P + Ctrl+P → popout still at the dragged position, content visible again - popout open + video playing + Ctrl+P + Ctrl+P → video resumes - popout closed + Ctrl+P + Ctrl+P → popout stays closed - embedded preview video + Ctrl+P + Ctrl+P → resumes - Ctrl+P with no video on screen → no errors --- booru_viewer/gui/main_window.py | 30 ++++++++++++++++++------ booru_viewer/gui/popout/window.py | 38 +++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/booru_viewer/gui/main_window.py b/booru_viewer/gui/main_window.py index 4ed8104..5465566 100644 --- a/booru_viewer/gui/main_window.py +++ b/booru_viewer/gui/main_window.py @@ -2929,6 +2929,11 @@ class BooruApp(QMainWindow): self._privacy_overlay = QWidget(self) self._privacy_overlay.setStyleSheet("background: black;") self._privacy_overlay.hide() + # Tracks whether the popout was visible at privacy-on time + # so privacy-off only restores it if it was actually up + # before. Without the gate, privacy-off would re-show a + # popout that the user closed before triggering privacy. + self._popout_was_visible = False self._privacy_on = not self._privacy_on if self._privacy_on: @@ -2939,15 +2944,26 @@ class BooruApp(QMainWindow): # Pause preview video if self._preview._stack.currentIndex() == 1: self._preview._video_player.pause() - # Hide and pause popout - if self._fullscreen_window and self._fullscreen_window.isVisible(): - if self._fullscreen_window._stack.currentIndex() == 1: - self._fullscreen_window._video.pause() - self._fullscreen_window.hide() + # Delegate popout hide-and-pause to FullscreenPreview so it + # can capture its own geometry for restore. + self._popout_was_visible = bool( + self._fullscreen_window and self._fullscreen_window.isVisible() + ) + if self._popout_was_visible: + self._fullscreen_window.privacy_hide() else: self._privacy_overlay.hide() - if self._fullscreen_window: - self._fullscreen_window.show() + # Resume embedded preview video — unconditional resume, the + # common case (privacy hides → user comes back → video should + # be playing again) wins over the manually-paused edge case. + if self._preview._stack.currentIndex() == 1: + self._preview._video_player.resume() + # Restore the popout via its own privacy_show method, which + # also re-dispatches the captured geometry to Hyprland (Qt + # show() alone doesn't preserve position on Wayland) and + # resumes its video. + if self._popout_was_visible and self._fullscreen_window: + self._fullscreen_window.privacy_show() def resizeEvent(self, event) -> None: super().resizeEvent(event) diff --git a/booru_viewer/gui/popout/window.py b/booru_viewer/gui/popout/window.py index 25c3638..e01ea9e 100644 --- a/booru_viewer/gui/popout/window.py +++ b/booru_viewer/gui/popout/window.py @@ -172,6 +172,17 @@ class FullscreenPreview(QMainWindow): self._video._controls_bar.raise_() self._toolbar.raise_() + # Privacy overlay — black QWidget child of central, raised over + # the media stack on privacy_hide. Lives inside the popout + # itself instead of forcing main_window to hide() the popout + # window — Wayland's hide→show round-trip drops position because + # the compositor unmaps and remaps, and Hyprland may re-tile the + # remap depending on window rules. Keeping the popout mapped + # with an in-place overlay sidesteps both issues. + self._privacy_overlay = QWidget(central) + self._privacy_overlay.setStyleSheet("background: black;") + self._privacy_overlay.hide() + # Auto-hide timer for overlay UI self._ui_visible = True self._hide_timer = QTimer(self) @@ -904,6 +915,30 @@ class FullscreenPreview(QMainWindow): except FileNotFoundError: pass + def privacy_hide(self) -> None: + """Cover the popout's content with a black overlay for privacy. + + The popout window itself is NOT hidden — Wayland's hide→show + round-trip drops position because the compositor unmaps and + remaps the window, and Hyprland may re-tile the remapped window + depending on its rules. Instead we raise an in-place black + QWidget overlay over the central widget. The window stays + mapped, position is preserved automatically, video is paused. + """ + if self._stack.currentIndex() == 1: + self._video.pause() + central = self.centralWidget() + if central is not None: + self._privacy_overlay.setGeometry(0, 0, central.width(), central.height()) + self._privacy_overlay.raise_() + self._privacy_overlay.show() + + def privacy_show(self) -> None: + """Lift the black overlay and resume video. Counterpart to privacy_hide.""" + self._privacy_overlay.hide() + if self._stack.currentIndex() == 1: + self._video.resume() + def _enter_fullscreen(self) -> None: """Enter fullscreen — capture windowed geometry first so F11 back can restore it. @@ -996,6 +1031,9 @@ class FullscreenPreview(QMainWindow): self._toolbar.setGeometry(0, 0, w, tb_h) ctrl_h = self._video._controls_bar.sizeHint().height() self._video._controls_bar.setGeometry(0, h - ctrl_h, w, ctrl_h) + # Privacy overlay covers the entire central widget when active. + if self._privacy_overlay.isVisible(): + self._privacy_overlay.setGeometry(0, 0, w, h) # Capture corner-resize into the persistent viewport so the # long_side the user chose survives subsequent navigations. #