gui/main_window: replace popout internal access with public methods
Drops every direct popout._underscore access from main_window in favor
of nine new public methods on FullscreenPreview. The legacy private
fields (_video, _viewer, _stack, _bookmark_btn, etc.) stay in place —
this is a clean public wrapper layer, not a re-architecture. Going
through public methods makes the popout's interface explicit and
prevents future code from reaching into popout internals.
New public methods on FullscreenPreview:
is_video_active() -> bool
Replaces popout._stack.currentIndex() == 1 checks. Used to gate
video-only operations.
set_toolbar_visibility(*, bookmark, save, bl_tag, bl_post)
Replaces 4-line popout._bookmark_btn.setVisible(...) etc block.
Per-tab toolbar gating.
sync_video_state(*, volume, mute, autoplay, loop_state)
Replaces 4-line popout._video.volume = ... etc block. Called by
main_window's _open_fullscreen_preview to push embedded preview
state into the popout.
get_video_state() -> dict
Returns volume / mute / autoplay / loop_state / position_ms in
one read. Replaces 5 separate popout._video.* attribute reads
in main_window's _on_fullscreen_closed reverse sync.
seek_video_to(ms)
Wraps VideoPlayer.seek_to_ms (which uses 'absolute+exact' since
the 609066c drag-back fix). Used by the seek-after-load pattern.
connect_media_ready_once(callback)
One-shot callback wiring with auto-disconnect. Replaces the
manual lambda + try/except disconnect dance in main_window.
pause_media()
Wraps VideoPlayer.pause(). Replaces 3 sites of direct
popout._video.pause() calls in privacy-screen / external-open
paths.
force_mpv_pause()
Direct mpv.pause = True without button text update. Replaces
the legacy popout._video._mpv.pause = True deep attribute access
in main_window's _on_post_activated. Used to prevent the OLD
video from reaching natural EOF during the new post's async
download.
stop_media()
Stops the video and clears the image viewer. Replaces 2 sites
of the popout._viewer.clear() + popout._video.stop() sequence
in blacklist-removal flow.
main_window.py call sites updated:
Line 1122-1130 (_on_post_activated):
popout._video._mpv.pause = True → popout.force_mpv_pause()
Line 1339-1342 (_update_fullscreen):
4 popout._*.setVisible(...) → popout.set_toolbar_visibility(...)
Line 1798, 1811, 2731:
popout._video.pause() → popout.pause_media()
Line 2151-2166 (_open_fullscreen_preview sync block):
sv = popout._video; sv.volume = ...; ...
+ manual seek-when-ready closure
→ popout.sync_video_state(...) + popout.connect_media_ready_once(...)
Line 2196-2207 (_on_fullscreen_closed reverse sync):
sv = popout._video; pv.volume = sv.volume; ...; popout._stack.currentIndex...
→ popout.get_video_state() returning a dict
Line 2393-2394, 2421-2423 (blacklist removal):
popout._viewer.clear() + popout._video.stop()
→ popout.stop_media()
After this commit, main_window has ZERO direct popout._underscore
accesses. The popout's public method surface is the only way for
main_window to interact with the popout's internals.
The popout's public method surface is now:
Lifecycle:
- set_media (existing — keeps the kind, info, width, height contract)
- update_state (existing — bookmarked/saved button labels)
- close (Qt builtin — triggers closeEvent)
Wiring:
- set_post_tags
- set_bookmark_folders_callback
- set_folders_callback
Privacy:
- privacy_hide / privacy_show (existing)
New in commit 15:
- is_video_active
- set_toolbar_visibility
- sync_video_state
- get_video_state
- seek_video_to
- connect_media_ready_once
- pause_media
- force_mpv_pause
- stop_media
Outbound signals (unchanged from refactor start):
- navigate / play_next_requested / closed
- bookmark_requested / bookmark_to_folder
- save_to_folder / unsave_requested
- blacklist_tag_requested / blacklist_post_requested
- privacy_requested
Tests passing after this commit: 81 / 81 (16 Phase A + 65 state).
Phase A still green.
Verification:
- Imports clean
- Pure-Python state machine + tests unchanged
- main_window's popout interaction goes through public methods only
Test cases for commit 16 (final shim cleanup):
- Drop the hyprland re-export shim methods from popout/window.py
- Have callers use popout.hyprland directly
This commit is contained in:
parent
69d25b325e
commit
ec238f3aa4
@ -1120,9 +1120,7 @@ class BooruApp(QMainWindow):
|
|||||||
# or already stopped — pause is a no-op there.
|
# or already stopped — pause is a no-op there.
|
||||||
try:
|
try:
|
||||||
if self._fullscreen_window:
|
if self._fullscreen_window:
|
||||||
fmpv = self._fullscreen_window._video._mpv
|
self._fullscreen_window.force_mpv_pause()
|
||||||
if fmpv is not None:
|
|
||||||
fmpv.pause = True
|
|
||||||
pmpv = self._preview._video_player._mpv
|
pmpv = self._preview._video_player._mpv
|
||||||
if pmpv is not None:
|
if pmpv is not None:
|
||||||
pmpv.pause = True
|
pmpv.pause = True
|
||||||
@ -1336,10 +1334,12 @@ class BooruApp(QMainWindow):
|
|||||||
# visible — it acts as Unsave for the library file currently
|
# visible — it acts as Unsave for the library file currently
|
||||||
# being viewed, matching the embedded preview's library mode.
|
# being viewed, matching the embedded preview's library mode.
|
||||||
show_full = self._stack.currentIndex() != 2
|
show_full = self._stack.currentIndex() != 2
|
||||||
self._fullscreen_window._bookmark_btn.setVisible(show_full)
|
self._fullscreen_window.set_toolbar_visibility(
|
||||||
self._fullscreen_window._save_btn.setVisible(True)
|
bookmark=show_full,
|
||||||
self._fullscreen_window._bl_tag_btn.setVisible(show_full)
|
save=True,
|
||||||
self._fullscreen_window._bl_post_btn.setVisible(show_full)
|
bl_tag=show_full,
|
||||||
|
bl_post=show_full,
|
||||||
|
)
|
||||||
self._update_fullscreen_state()
|
self._update_fullscreen_state()
|
||||||
|
|
||||||
def _update_fullscreen_state(self) -> None:
|
def _update_fullscreen_state(self) -> None:
|
||||||
@ -1795,7 +1795,7 @@ class BooruApp(QMainWindow):
|
|||||||
if path is not None:
|
if path is not None:
|
||||||
self._preview._video_player.pause()
|
self._preview._video_player.pause()
|
||||||
if self._fullscreen_window and self._fullscreen_window.isVisible():
|
if self._fullscreen_window and self._fullscreen_window.isVisible():
|
||||||
self._fullscreen_window._video.pause()
|
self._fullscreen_window.pause_media()
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(str(path)))
|
QDesktopServices.openUrl(QUrl.fromLocalFile(str(path)))
|
||||||
else:
|
else:
|
||||||
self._status.showMessage("Bookmark not cached — open it first to download")
|
self._status.showMessage("Bookmark not cached — open it first to download")
|
||||||
@ -1808,7 +1808,7 @@ class BooruApp(QMainWindow):
|
|||||||
if current and Path(current).exists():
|
if current and Path(current).exists():
|
||||||
self._preview._video_player.pause()
|
self._preview._video_player.pause()
|
||||||
if self._fullscreen_window and self._fullscreen_window.isVisible():
|
if self._fullscreen_window and self._fullscreen_window.isVisible():
|
||||||
self._fullscreen_window._video.pause()
|
self._fullscreen_window.pause_media()
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(current))
|
QDesktopServices.openUrl(QUrl.fromLocalFile(current))
|
||||||
return
|
return
|
||||||
# Browse: original path. Removed the "open random cache file"
|
# Browse: original path. Removed the "open random cache file"
|
||||||
@ -2148,22 +2148,21 @@ class BooruApp(QMainWindow):
|
|||||||
post = self._preview._current_post
|
post = self._preview._current_post
|
||||||
if post:
|
if post:
|
||||||
self._fullscreen_window.set_post_tags(post.tag_categories, post.tag_list)
|
self._fullscreen_window.set_post_tags(post.tag_categories, post.tag_list)
|
||||||
# Sync video player state from preview to popout
|
# Sync video player state from preview to popout via the
|
||||||
|
# popout's public sync_video_state method (replaces direct
|
||||||
|
# popout._video.* attribute writes).
|
||||||
pv = self._preview._video_player
|
pv = self._preview._video_player
|
||||||
sv = self._fullscreen_window._video
|
self._fullscreen_window.sync_video_state(
|
||||||
sv.volume = pv.volume
|
volume=pv.volume,
|
||||||
sv.is_muted = pv.is_muted
|
mute=pv.is_muted,
|
||||||
sv.autoplay = pv.autoplay
|
autoplay=pv.autoplay,
|
||||||
sv.loop_state = pv.loop_state
|
loop_state=pv.loop_state,
|
||||||
|
)
|
||||||
# Connect seek-after-load BEFORE set_media so we don't miss media_ready
|
# Connect seek-after-load BEFORE set_media so we don't miss media_ready
|
||||||
if video_pos > 0:
|
if video_pos > 0:
|
||||||
def _seek_when_ready():
|
self._fullscreen_window.connect_media_ready_once(
|
||||||
sv.seek_to_ms(video_pos)
|
lambda: self._fullscreen_window.seek_video_to(video_pos)
|
||||||
try:
|
)
|
||||||
sv.media_ready.disconnect(_seek_when_ready)
|
|
||||||
except RuntimeError:
|
|
||||||
pass
|
|
||||||
sv.media_ready.connect(_seek_when_ready)
|
|
||||||
# Pre-fit dimensions for the popout video pre-fit optimization
|
# Pre-fit dimensions for the popout video pre-fit optimization
|
||||||
# — `post` is the same `self._preview._current_post` referenced
|
# — `post` is the same `self._preview._current_post` referenced
|
||||||
# at line 2164 (set above), so reuse it without an extra read.
|
# at line 2164 (set above), so reuse it without an extra read.
|
||||||
@ -2193,18 +2192,20 @@ class BooruApp(QMainWindow):
|
|||||||
# Clear the popout-active flag now that the right splitter is back
|
# Clear the popout-active flag now that the right splitter is back
|
||||||
# in its real shape — future splitterMoved events should persist.
|
# in its real shape — future splitterMoved events should persist.
|
||||||
self._popout_active = False
|
self._popout_active = False
|
||||||
# Sync video player state from popout back to preview
|
# Sync video player state from popout back to preview via
|
||||||
if self._fullscreen_window:
|
# the popout's public get_video_state method (replaces direct
|
||||||
sv = self._fullscreen_window._video
|
# popout._video.* attribute reads + popout._stack.currentIndex
|
||||||
pv = self._preview._video_player
|
# check). The dict carries volume / mute / autoplay / loop_state
|
||||||
pv.volume = sv.volume
|
# / position_ms in one read.
|
||||||
pv.is_muted = sv.is_muted
|
|
||||||
pv.autoplay = sv.autoplay
|
|
||||||
pv.loop_state = sv.loop_state
|
|
||||||
# Grab video position before cleanup
|
|
||||||
video_pos = 0
|
video_pos = 0
|
||||||
if self._fullscreen_window and self._fullscreen_window._stack.currentIndex() == 1:
|
if self._fullscreen_window:
|
||||||
video_pos = self._fullscreen_window._video.get_position_ms()
|
vstate = self._fullscreen_window.get_video_state()
|
||||||
|
pv = self._preview._video_player
|
||||||
|
pv.volume = vstate["volume"]
|
||||||
|
pv.is_muted = vstate["mute"]
|
||||||
|
pv.autoplay = vstate["autoplay"]
|
||||||
|
pv.loop_state = vstate["loop_state"]
|
||||||
|
video_pos = vstate["position_ms"]
|
||||||
# Restore preview with current media
|
# Restore preview with current media
|
||||||
path = self._preview._current_path
|
path = self._preview._current_path
|
||||||
info = self._preview._info_label.text()
|
info = self._preview._info_label.text()
|
||||||
@ -2390,8 +2391,7 @@ class BooruApp(QMainWindow):
|
|||||||
if cp == self._preview._current_path:
|
if cp == self._preview._current_path:
|
||||||
self._preview.clear()
|
self._preview.clear()
|
||||||
if self._fullscreen_window and self._fullscreen_window.isVisible():
|
if self._fullscreen_window and self._fullscreen_window.isVisible():
|
||||||
self._fullscreen_window._viewer.clear()
|
self._fullscreen_window.stop_media()
|
||||||
self._fullscreen_window._video.stop()
|
|
||||||
self._status.showMessage(f"Blacklisted: {tag}")
|
self._status.showMessage(f"Blacklisted: {tag}")
|
||||||
self._remove_blacklisted_from_grid(tag=tag)
|
self._remove_blacklisted_from_grid(tag=tag)
|
||||||
elif action == bl_post_action:
|
elif action == bl_post_action:
|
||||||
@ -2419,8 +2419,7 @@ class BooruApp(QMainWindow):
|
|||||||
if cp == self._preview._current_path:
|
if cp == self._preview._current_path:
|
||||||
self._preview.clear()
|
self._preview.clear()
|
||||||
if self._fullscreen_window and self._fullscreen_window.isVisible():
|
if self._fullscreen_window and self._fullscreen_window.isVisible():
|
||||||
self._fullscreen_window._viewer.clear()
|
self._fullscreen_window.stop_media()
|
||||||
self._fullscreen_window._video.stop()
|
|
||||||
break
|
break
|
||||||
|
|
||||||
# Remove from posts list (reverse order to keep indices valid)
|
# Remove from posts list (reverse order to keep indices valid)
|
||||||
@ -2728,7 +2727,7 @@ class BooruApp(QMainWindow):
|
|||||||
# Pause any playing video before opening externally
|
# Pause any playing video before opening externally
|
||||||
self._preview._video_player.pause()
|
self._preview._video_player.pause()
|
||||||
if self._fullscreen_window and self._fullscreen_window.isVisible():
|
if self._fullscreen_window and self._fullscreen_window.isVisible():
|
||||||
self._fullscreen_window._video.pause()
|
self._fullscreen_window.pause_media()
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(str(path)))
|
QDesktopServices.openUrl(QUrl.fromLocalFile(str(path)))
|
||||||
else:
|
else:
|
||||||
self._status.showMessage("Image not cached yet — double-click to download first")
|
self._status.showMessage("Image not cached yet — double-click to download first")
|
||||||
|
|||||||
@ -615,6 +615,164 @@ class FullscreenPreview(QMainWindow):
|
|||||||
self._is_saved = saved
|
self._is_saved = saved
|
||||||
self._save_btn.setText("Unsave" if saved else "Save")
|
self._save_btn.setText("Unsave" if saved else "Save")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Public method interface (commit 15)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# The methods below replace direct underscore access from
|
||||||
|
# main_window.py. They wrap the existing private fields so
|
||||||
|
# main_window doesn't have to know about VideoPlayer / ImageViewer
|
||||||
|
# / QStackedWidget internals. The legacy private fields stay in
|
||||||
|
# place — these are clean public wrappers, not a re-architecture.
|
||||||
|
|
||||||
|
def is_video_active(self) -> bool:
|
||||||
|
"""True if the popout is currently showing a video (vs image).
|
||||||
|
|
||||||
|
Replaces direct `popout._stack.currentIndex() == 1` checks
|
||||||
|
from main_window. Used to gate per-tab video-only operations
|
||||||
|
(volume scroll, seek, pause).
|
||||||
|
"""
|
||||||
|
return self._stack.currentIndex() == 1
|
||||||
|
|
||||||
|
def set_toolbar_visibility(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
bookmark: bool,
|
||||||
|
save: bool,
|
||||||
|
bl_tag: bool,
|
||||||
|
bl_post: bool,
|
||||||
|
) -> None:
|
||||||
|
"""Per-tab toolbar gating.
|
||||||
|
|
||||||
|
Replaces direct `popout._bookmark_btn.setVisible(...)` etc
|
||||||
|
from main_window's `_update_fullscreen` method. Library tab
|
||||||
|
hides Bookmark / BL Tag / BL Post (no site/post id to act
|
||||||
|
on) but keeps Save (acts as Unsave for the file currently
|
||||||
|
being viewed).
|
||||||
|
"""
|
||||||
|
self._bookmark_btn.setVisible(bookmark)
|
||||||
|
self._save_btn.setVisible(save)
|
||||||
|
self._bl_tag_btn.setVisible(bl_tag)
|
||||||
|
self._bl_post_btn.setVisible(bl_post)
|
||||||
|
|
||||||
|
def sync_video_state(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
volume: int,
|
||||||
|
mute: bool,
|
||||||
|
autoplay: bool,
|
||||||
|
loop_state: int,
|
||||||
|
) -> None:
|
||||||
|
"""Push state from the embedded preview into the popout's
|
||||||
|
video player.
|
||||||
|
|
||||||
|
Called by main_window's `_open_fullscreen_preview` after the
|
||||||
|
popout is constructed. Replaces direct `popout._video.volume
|
||||||
|
= ...` etc writes. Uses VideoPlayer's existing setters which
|
||||||
|
handle the lazy-mpv pending-state pattern (mute survives
|
||||||
|
first-load via _pending_mute, volume survives via the slider
|
||||||
|
widget acting as persistent storage).
|
||||||
|
"""
|
||||||
|
self._video.volume = volume
|
||||||
|
self._video.is_muted = mute
|
||||||
|
self._video.autoplay = autoplay
|
||||||
|
self._video.loop_state = loop_state
|
||||||
|
|
||||||
|
def get_video_state(self) -> dict:
|
||||||
|
"""Read video player state for the reverse sync at popout close.
|
||||||
|
|
||||||
|
Returns a dict with `volume`, `mute`, `autoplay`, `loop_state`,
|
||||||
|
and `position_ms` (current playback position in milliseconds,
|
||||||
|
0 if the popout isn't currently on the video stack). Called
|
||||||
|
by main_window's `_on_fullscreen_closed` to push the state
|
||||||
|
back into the embedded preview's video player.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"volume": self._video.volume,
|
||||||
|
"mute": self._video.is_muted,
|
||||||
|
"autoplay": self._video.autoplay,
|
||||||
|
"loop_state": self._video.loop_state,
|
||||||
|
"position_ms": (
|
||||||
|
self._video.get_position_ms()
|
||||||
|
if self.is_video_active()
|
||||||
|
else 0
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
def seek_video_to(self, ms: int) -> None:
|
||||||
|
"""Seek the video to a specific position in milliseconds.
|
||||||
|
|
||||||
|
Used by main_window's seek-after-load pattern when restoring
|
||||||
|
video position across popout open/close cycles. Wraps
|
||||||
|
`VideoPlayer.seek_to_ms` (which uses `'absolute+exact'` for
|
||||||
|
frame-accurate landing — same as the slider's `_seek` after
|
||||||
|
the 609066c drag-back fix).
|
||||||
|
"""
|
||||||
|
self._video.seek_to_ms(ms)
|
||||||
|
|
||||||
|
def connect_media_ready_once(self, callback) -> None:
|
||||||
|
"""Wire a one-shot callback to the video player's media_ready
|
||||||
|
signal. The callback fires once when the next loaded video
|
||||||
|
becomes ready, then disconnects itself.
|
||||||
|
|
||||||
|
Replaces main_window's manual lambda + try/except disconnect
|
||||||
|
dance for the seek-when-ready pattern (open popout → wait for
|
||||||
|
the new mpv instance to load → restore the embedded preview's
|
||||||
|
playback position).
|
||||||
|
"""
|
||||||
|
def _wrapper():
|
||||||
|
try:
|
||||||
|
callback()
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
self._video.media_ready.disconnect(_wrapper)
|
||||||
|
except (TypeError, RuntimeError):
|
||||||
|
pass
|
||||||
|
self._video.media_ready.connect(_wrapper)
|
||||||
|
|
||||||
|
def pause_media(self) -> None:
|
||||||
|
"""Pause the active video player. No-op if no video is loaded.
|
||||||
|
|
||||||
|
Replaces direct `popout._video.pause()` calls from main_window
|
||||||
|
in privacy-screen / blacklist / video-end paths. Goes through
|
||||||
|
VideoPlayer.pause() which handles the play-button text update
|
||||||
|
and respects the lazy-mpv state.
|
||||||
|
"""
|
||||||
|
self._video.pause()
|
||||||
|
|
||||||
|
def force_mpv_pause(self) -> None:
|
||||||
|
"""Set mpv.pause = True directly without going through Qt
|
||||||
|
property setters or button text updates.
|
||||||
|
|
||||||
|
Used by main_window's `_on_post_activated` to prevent the OLD
|
||||||
|
video from reaching natural EOF during the new post's async
|
||||||
|
download (which would auto-advance past the post the user
|
||||||
|
clicked). Different from `pause_media` because this writes
|
||||||
|
the mpv property directly — no eof-reached side effect, no
|
||||||
|
button text flicker mid-load.
|
||||||
|
|
||||||
|
Replaces the legacy `popout._video._mpv.pause = True` deep
|
||||||
|
attribute access from main_window line ~1125. The
|
||||||
|
`_mpv is None` guard handles the pre-first-load case where
|
||||||
|
`_ensure_mpv` hasn't run yet.
|
||||||
|
"""
|
||||||
|
if self._video._mpv is not None:
|
||||||
|
try:
|
||||||
|
self._video._mpv.pause = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stop_media(self) -> None:
|
||||||
|
"""Stop the video and clear the image viewer.
|
||||||
|
|
||||||
|
Used by main_window's blacklist-removal flow when the post
|
||||||
|
being viewed gets blacklisted. Replaces the
|
||||||
|
`popout._viewer.clear() + popout._video.stop()` sequence from
|
||||||
|
main_window with a single call.
|
||||||
|
"""
|
||||||
|
self._video.stop()
|
||||||
|
self._viewer.clear()
|
||||||
|
|
||||||
def set_bookmark_folders_callback(self, callback) -> None:
|
def set_bookmark_folders_callback(self, callback) -> None:
|
||||||
"""Wire the bookmark folder list source. Called once from app.py
|
"""Wire the bookmark folder list source. Called once from app.py
|
||||||
right after the popout is constructed; matches the embedded
|
right after the popout is constructed; matches the embedded
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user