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. #