From a2b759be9052099ef82b6d04932e464fef984d63 Mon Sep 17 00:00:00 2001 From: pax Date: Wed, 8 Apr 2026 20:35:36 -0500 Subject: [PATCH] popout/window: drop refactor shims (final cleanup) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the last vestiges of the legacy compatibility layer that commits 13-15 left in place to keep the app runnable across the authority transfer: 1. Three `_hyprctl_*` shim methods on FullscreenPreview that delegated to the popout/hyprland module-level functions. Commit 13 added them to preserve byte-for-byte call-site compatibility while window.py still had its old imperative event handling. After commit 14b switched authority to the dispatch+apply path and commit 15 cleaned up main_window's interface, every remaining call site in window.py is updated to call hyprland.* directly: self._hyprctl_get_window() → hyprland.get_window(self.windowTitle()) self._hyprctl_resize(0, 0) → hyprland.resize(self.windowTitle(), 0, 0) self._hyprctl_resize_and_move(...) → hyprland.resize_and_move(self.windowTitle(), ...) 8 internal call sites updated, 3 shim methods removed. 2. The legacy `self._video.video_size.connect(self._on_video_size)` parallel-path connection plus the dead `_on_video_size` method. The dispatch lambda wired in __init__ already handles VideoSizeKnown → FitWindowToContent → _fit_to_content via the apply path. The legacy direct connection was a duplicate that the same-rect skip in _fit_to_content made harmless, but it muddied the dispatch trace and was dead weight after 14b. A new `from . import hyprland` at the top of window.py imports the module once at load time instead of inline-importing on every shim call (the legacy shims used `from . import hyprland` inside each method body to avoid import order issues during the commit-13 extraction). After this commit, FullscreenPreview's interaction with Hyprland is: - Single import: `from . import hyprland` - Direct calls: `hyprland.get_window(self.windowTitle())` etc - No shim layer - The popout/hyprland module is the single source of Hyprland IPC for the popout Tests passing after this commit: 81 / 81 (16 Phase A + 65 state). Phase A still green. Final state of the popout state machine refactor: - 6 states / 17 events / 14 effects (within budget 10/20/15) - 6 race-fix invariants enforced structurally (no timestamp windows in state.py, no guards, no fall-throughs) - popout/state.py + popout/effects.py: pure Python, no PySide6, no mpv, no httpx — verifiable via the meta_path import blocker - popout/hyprland.py: isolated subprocess wrappers - popout/window.py: thin Qt adapter — translates Qt events into state machine dispatches, applies returned effects to widgets via the existing private helpers - main_window.py: zero direct popout._underscore access; all interaction goes through the public method surface defined in commit 15 Test cases / followups: none. The refactor is complete. --- booru_viewer/gui/popout/window.py | 78 +++++++++++-------------------- 1 file changed, 28 insertions(+), 50 deletions(-) diff --git a/booru_viewer/gui/popout/window.py b/booru_viewer/gui/popout/window.py index f74d15c..fa9186e 100644 --- a/booru_viewer/gui/popout/window.py +++ b/booru_viewer/gui/popout/window.py @@ -15,6 +15,7 @@ from PySide6.QtWidgets import ( from ..media.constants import _is_video from ..media.image_viewer import ImageViewer from ..media.video_player import VideoPlayer +from . import hyprland from .effects import ( ApplyLoopMode, ApplyMute, @@ -137,20 +138,25 @@ class FullscreenPreview(QMainWindow): self._stack.addWidget(self._viewer) self._video = VideoPlayer() - # Note: the legacy `self._video.play_next.connect(self.play_next_requested)` - # signal-to-signal forwarding was removed in commit 14b. The - # state machine dispatch path now handles play_next_requested - # via the EmitPlayNextRequested effect: - # 1. mpv eof-reached → VideoPlayer.play_next emits - # 2. Adapter dispatch lambda (wired in __init__) → - # VideoEofReached event - # 3. State machine PlayingVideo + Loop=Next → emits - # EmitPlayNextRequested effect - # 4. _apply_effects → self.play_next_requested.emit() - # Keeping the legacy forwarding here would double-emit the - # signal and cause main_window to navigate twice on every - # video EOF in Loop=Next mode. - self._video.video_size.connect(self._on_video_size) + # Note: two legacy VideoPlayer signal connections removed in + # commits 14b and 16: + # + # - `self._video.play_next.connect(self.play_next_requested)` + # (removed in 14b): the EmitPlayNextRequested effect now + # emits play_next_requested via the state machine dispatch + # path. Keeping the forwarding would double-emit the signal + # and cause main_window to navigate twice on every video + # EOF in Loop=Next mode. + # + # - `self._video.video_size.connect(self._on_video_size)` + # (removed in 16): the dispatch path's VideoSizeKnown + # handler emits FitWindowToContent which the apply path + # delegates to _fit_to_content. The legacy direct call to + # _on_video_size → _fit_to_content was a parallel duplicate + # that the same-rect skip in _fit_to_content made harmless, + # but it muddied the trace. The dispatch lambda below is + # wired in the same __init__ block (post state machine + # construction) and is now the sole path. self._stack.addWidget(self._video) self.setCentralWidget(central) @@ -923,13 +929,9 @@ class FullscreenPreview(QMainWindow): # eventFilter on mouse-move into the top/bottom edge zones), # not pop back up after every navigation. - def _on_video_size(self, w: int, h: int) -> None: - if not self.isFullScreen() and w > 0 and h > 0: - self._fit_to_content(w, h) - def _is_hypr_floating(self) -> bool | None: """Check if this window is floating in Hyprland. None if not on Hyprland.""" - win = self._hyprctl_get_window() + win = hyprland.get_window(self.windowTitle()) if win is None: return None # not Hyprland return bool(win.get("floating")) @@ -988,7 +990,7 @@ class FullscreenPreview(QMainWindow): """ if floating is True: if win is None: - win = self._hyprctl_get_window() + win = hyprland.get_window(self.windowTitle()) if win and win.get("at") and win.get("size"): wx, wy = win["at"] ww, wh = win["size"] @@ -1061,7 +1063,7 @@ class FullscreenPreview(QMainWindow): # moved or resized the window externally since our last dispatch. if floating is True and self._last_dispatched_rect is not None: if win is None: - win = self._hyprctl_get_window() + win = hyprland.get_window(self.windowTitle()) if win and win.get("at") and win.get("size"): cur_x, cur_y = win["at"] cur_w, cur_h = win["size"] @@ -1117,7 +1119,7 @@ class FullscreenPreview(QMainWindow): # from three to one, eliminating ~6ms of UI freeze per navigation. win = None if on_hypr: - win = self._hyprctl_get_window() + win = hyprland.get_window(self.windowTitle()) if win is None: if _retry < 5: QTimer.singleShot( @@ -1129,7 +1131,7 @@ class FullscreenPreview(QMainWindow): else: floating = None if floating is False: - self._hyprctl_resize(0, 0) # tiled: just set keep_aspect_ratio + hyprland.resize(self.windowTitle(), 0, 0) # tiled: just set keep_aspect_ratio return aspect = content_w / content_h screen = self.screen() @@ -1172,7 +1174,7 @@ class FullscreenPreview(QMainWindow): # Hyprland: hyprctl is the sole authority. Calling self.resize() # here would race with the batch below and produce visible flashing # when the window also has to move. - self._hyprctl_resize_and_move(w, h, x, y, win=win) + hyprland.resize_and_move(self.windowTitle(), w, h, x, y, win=win) else: # Non-Hyprland fallback: Qt drives geometry directly. Use # setGeometry with the computed top-left rather than resize() @@ -1312,30 +1314,6 @@ class FullscreenPreview(QMainWindow): self._ui_visible = self._toolbar.isVisible() or self._video._controls_bar.isVisible() return super().eventFilter(obj, event) - # Hyprland helpers — moved to popout/hyprland.py in commit 13. These - # methods are now thin shims around the module-level functions so - # the existing call sites in this file (`_fit_to_content`, - # `_enter_fullscreen`, `closeEvent`) keep working byte-for-byte. - # Commit 14's adapter rewrite drops the shims and calls the - # hyprland module directly. - - def _hyprctl_get_window(self) -> dict | None: - """Shim → `popout.hyprland.get_window`.""" - from . import hyprland - return hyprland.get_window(self.windowTitle()) - - def _hyprctl_resize(self, w: int, h: int) -> None: - """Shim → `popout.hyprland.resize`.""" - from . import hyprland - hyprland.resize(self.windowTitle(), w, h) - - def _hyprctl_resize_and_move( - self, w: int, h: int, x: int, y: int, win: dict | None = None - ) -> None: - """Shim → `popout.hyprland.resize_and_move`.""" - from . import hyprland - hyprland.resize_and_move(self.windowTitle(), w, h, x, y, win=win) - def privacy_hide(self) -> None: """Cover the popout's content with a black overlay for privacy. @@ -1375,7 +1353,7 @@ class FullscreenPreview(QMainWindow): `_viewport` here makes the restore correct regardless. """ from PySide6.QtCore import QRect - win = self._hyprctl_get_window() + win = hyprland.get_window(self.windowTitle()) if win and win.get("at") and win.get("size"): x, y = win["at"] w, h = win["size"] @@ -1559,7 +1537,7 @@ class FullscreenPreview(QMainWindow): FullscreenPreview._saved_fullscreen = self.isFullScreen() if not self.isFullScreen(): # On Hyprland, Qt doesn't know the real position — ask the WM - win = self._hyprctl_get_window() + win = hyprland.get_window(self.windowTitle()) if win and win.get("at") and win.get("size"): from PySide6.QtCore import QRect x, y = win["at"]