Restore popout windowed position on F11 exit (defer fit, disable Hyprland anim, dedupe video-params)
This commit is contained in:
parent
a6bb08e6c1
commit
8ef40dc0fe
@ -181,6 +181,12 @@ class FullscreenPreview(QMainWindow):
|
|||||||
# position the user has dragged the window to.
|
# position the user has dragged the window to.
|
||||||
self._first_fit_pending = True
|
self._first_fit_pending = True
|
||||||
self._pending_position_restore: tuple[int, int] | None = None
|
self._pending_position_restore: tuple[int, int] | None = None
|
||||||
|
self._pending_size: tuple[int, int] | None = None
|
||||||
|
# Last known windowed geometry — captured on entering fullscreen so
|
||||||
|
# F11 → windowed can land back on the same spot. Seeded from saved
|
||||||
|
# geometry when the popout opens windowed, so even an immediate
|
||||||
|
# F11 → fullscreen → F11 has a sensible target.
|
||||||
|
self._windowed_geometry = None
|
||||||
# Restore saved state or start fullscreen
|
# Restore saved state or start fullscreen
|
||||||
if FullscreenPreview._saved_geometry and not FullscreenPreview._saved_fullscreen:
|
if FullscreenPreview._saved_geometry and not FullscreenPreview._saved_fullscreen:
|
||||||
self.setGeometry(FullscreenPreview._saved_geometry)
|
self.setGeometry(FullscreenPreview._saved_geometry)
|
||||||
@ -188,6 +194,11 @@ class FullscreenPreview(QMainWindow):
|
|||||||
FullscreenPreview._saved_geometry.x(),
|
FullscreenPreview._saved_geometry.x(),
|
||||||
FullscreenPreview._saved_geometry.y(),
|
FullscreenPreview._saved_geometry.y(),
|
||||||
)
|
)
|
||||||
|
self._pending_size = (
|
||||||
|
FullscreenPreview._saved_geometry.width(),
|
||||||
|
FullscreenPreview._saved_geometry.height(),
|
||||||
|
)
|
||||||
|
self._windowed_geometry = FullscreenPreview._saved_geometry
|
||||||
self.show()
|
self.show()
|
||||||
else:
|
else:
|
||||||
self.showFullScreen()
|
self.showFullScreen()
|
||||||
@ -269,12 +280,19 @@ class FullscreenPreview(QMainWindow):
|
|||||||
screen = self.screen()
|
screen = self.screen()
|
||||||
max_h = int(screen.availableGeometry().height() * 0.90) if screen else 9999
|
max_h = int(screen.availableGeometry().height() * 0.90) if screen else 9999
|
||||||
max_w = screen.availableGeometry().width() if screen else 9999
|
max_w = screen.availableGeometry().width() if screen else 9999
|
||||||
w = min(self.width(), max_w)
|
# Starting width: prefer the pending one-shot size when set (saves us
|
||||||
|
# from depending on self.width() during transitional Qt states like
|
||||||
|
# right after showNormal(), where Qt may briefly report fullscreen
|
||||||
|
# dimensions before Hyprland confirms the windowed geometry).
|
||||||
|
if self._first_fit_pending and self._pending_size:
|
||||||
|
start_w = self._pending_size[0]
|
||||||
|
else:
|
||||||
|
start_w = self.width()
|
||||||
|
w = min(start_w, max_w)
|
||||||
h = int(w / aspect)
|
h = int(w / aspect)
|
||||||
if h > max_h:
|
if h > max_h:
|
||||||
h = max_h
|
h = max_h
|
||||||
w = int(h * aspect)
|
w = int(h * aspect)
|
||||||
self.resize(w, h)
|
|
||||||
# Decide target top-left:
|
# Decide target top-left:
|
||||||
# first fit after open with a saved position → restore it (one-shot)
|
# first fit after open with a saved position → restore it (one-shot)
|
||||||
# any subsequent fit → center-pin from current Hyprland position
|
# any subsequent fit → center-pin from current Hyprland position
|
||||||
@ -287,12 +305,20 @@ class FullscreenPreview(QMainWindow):
|
|||||||
cx = win["at"][0] + win["size"][0] // 2
|
cx = win["at"][0] + win["size"][0] // 2
|
||||||
cy = win["at"][1] + win["size"][1] // 2
|
cy = win["at"][1] + win["size"][1] // 2
|
||||||
target = (cx - w // 2, cy - h // 2)
|
target = (cx - w // 2, cy - h // 2)
|
||||||
if target is not None:
|
if floating is True:
|
||||||
self._hyprctl_resize_and_move(w, h, target[0], target[1])
|
# 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.
|
||||||
|
if target is not None:
|
||||||
|
self._hyprctl_resize_and_move(w, h, target[0], target[1])
|
||||||
|
else:
|
||||||
|
self._hyprctl_resize(w, h)
|
||||||
else:
|
else:
|
||||||
self._hyprctl_resize(w, h)
|
# Non-Hyprland fallback: Qt drives geometry directly.
|
||||||
|
self.resize(w, h)
|
||||||
self._first_fit_pending = False
|
self._first_fit_pending = False
|
||||||
self._pending_position_restore = None
|
self._pending_position_restore = None
|
||||||
|
self._pending_size = None
|
||||||
|
|
||||||
def _show_overlay(self) -> None:
|
def _show_overlay(self) -> None:
|
||||||
"""Show toolbar and video controls, restart auto-hide timer."""
|
"""Show toolbar and video controls, restart auto-hide timer."""
|
||||||
@ -350,7 +376,7 @@ class FullscreenPreview(QMainWindow):
|
|||||||
if self.isFullScreen():
|
if self.isFullScreen():
|
||||||
self._exit_fullscreen()
|
self._exit_fullscreen()
|
||||||
else:
|
else:
|
||||||
self.showFullScreen()
|
self._enter_fullscreen()
|
||||||
return True
|
return True
|
||||||
elif key == Qt.Key.Key_Space and self._stack.currentIndex() == 1:
|
elif key == Qt.Key.Key_Space and self._stack.currentIndex() == 1:
|
||||||
self._video._toggle_play()
|
self._video._toggle_play()
|
||||||
@ -423,9 +449,12 @@ class FullscreenPreview(QMainWindow):
|
|||||||
return
|
return
|
||||||
if not win.get("floating"):
|
if not win.get("floating"):
|
||||||
# Tiled — don't resize (fights the layout), just set aspect lock
|
# Tiled — don't resize (fights the layout), just set aspect lock
|
||||||
|
# and disable animations to prevent flashing on later transitions.
|
||||||
try:
|
try:
|
||||||
subprocess.Popen(
|
subprocess.Popen(
|
||||||
["hyprctl", "dispatch", "setprop", f"address:{addr} keep_aspect_ratio 1"],
|
["hyprctl", "--batch",
|
||||||
|
f"dispatch setprop address:{addr} no_anim 1"
|
||||||
|
f" ; dispatch setprop address:{addr} keep_aspect_ratio 1"],
|
||||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
@ -434,7 +463,8 @@ class FullscreenPreview(QMainWindow):
|
|||||||
try:
|
try:
|
||||||
subprocess.Popen(
|
subprocess.Popen(
|
||||||
["hyprctl", "--batch",
|
["hyprctl", "--batch",
|
||||||
f"dispatch setprop address:{addr} keep_aspect_ratio 0"
|
f"dispatch setprop address:{addr} no_anim 1"
|
||||||
|
f" ; dispatch setprop address:{addr} keep_aspect_ratio 0"
|
||||||
f" ; dispatch resizewindowpixel exact {w} {h},address:{addr}"
|
f" ; dispatch resizewindowpixel exact {w} {h},address:{addr}"
|
||||||
f" ; dispatch setprop address:{addr} keep_aspect_ratio 1"],
|
f" ; dispatch setprop address:{addr} keep_aspect_ratio 1"],
|
||||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||||
@ -456,7 +486,8 @@ class FullscreenPreview(QMainWindow):
|
|||||||
try:
|
try:
|
||||||
subprocess.Popen(
|
subprocess.Popen(
|
||||||
["hyprctl", "--batch",
|
["hyprctl", "--batch",
|
||||||
f"dispatch setprop address:{addr} keep_aspect_ratio 0"
|
f"dispatch setprop address:{addr} no_anim 1"
|
||||||
|
f" ; dispatch setprop address:{addr} keep_aspect_ratio 0"
|
||||||
f" ; dispatch resizewindowpixel exact {w} {h},address:{addr}"
|
f" ; dispatch resizewindowpixel exact {w} {h},address:{addr}"
|
||||||
f" ; dispatch movewindowpixel exact {x} {y},address:{addr}"
|
f" ; dispatch movewindowpixel exact {x} {y},address:{addr}"
|
||||||
f" ; dispatch setprop address:{addr} keep_aspect_ratio 1"],
|
f" ; dispatch setprop address:{addr} keep_aspect_ratio 1"],
|
||||||
@ -465,8 +496,20 @@ class FullscreenPreview(QMainWindow):
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _enter_fullscreen(self) -> None:
|
||||||
|
"""Enter fullscreen — capture windowed geometry first so F11 back can restore it."""
|
||||||
|
from PySide6.QtCore import QRect
|
||||||
|
win = self._hyprctl_get_window()
|
||||||
|
if win and win.get("at") and win.get("size"):
|
||||||
|
x, y = win["at"]
|
||||||
|
w, h = win["size"]
|
||||||
|
self._windowed_geometry = QRect(x, y, w, h)
|
||||||
|
else:
|
||||||
|
self._windowed_geometry = self.frameGeometry()
|
||||||
|
self.showFullScreen()
|
||||||
|
|
||||||
def _exit_fullscreen(self) -> None:
|
def _exit_fullscreen(self) -> None:
|
||||||
"""Leave fullscreen — sizes to content aspect ratio using current width."""
|
"""Leave fullscreen — restore the pre-fullscreen position via the same handshake as open."""
|
||||||
content_w, content_h = 0, 0
|
content_w, content_h = 0, 0
|
||||||
if self._stack.currentIndex() == 1:
|
if self._stack.currentIndex() == 1:
|
||||||
mpv = self._video._mpv
|
mpv = self._video._mpv
|
||||||
@ -482,9 +525,28 @@ class FullscreenPreview(QMainWindow):
|
|||||||
if pix and not pix.isNull():
|
if pix and not pix.isNull():
|
||||||
content_w, content_h = pix.width(), pix.height()
|
content_w, content_h = pix.width(), pix.height()
|
||||||
FullscreenPreview._saved_fullscreen = False
|
FullscreenPreview._saved_fullscreen = False
|
||||||
|
# Re-arm the one-shot handshake. Note: no setGeometry here — Qt's
|
||||||
|
# setGeometry on a fullscreen window races with showNormal() and the
|
||||||
|
# subsequent hyprctl batch, leaving the window stuck at the
|
||||||
|
# default child-window placement (top-left). Instead, _pending_size
|
||||||
|
# seeds the fit math directly and the deferred fit below dispatches
|
||||||
|
# the resize+move via hyprctl after Qt's state transition has settled.
|
||||||
|
if self._windowed_geometry is not None:
|
||||||
|
self._first_fit_pending = True
|
||||||
|
self._pending_position_restore = (
|
||||||
|
self._windowed_geometry.x(),
|
||||||
|
self._windowed_geometry.y(),
|
||||||
|
)
|
||||||
|
self._pending_size = (
|
||||||
|
self._windowed_geometry.width(),
|
||||||
|
self._windowed_geometry.height(),
|
||||||
|
)
|
||||||
self.showNormal()
|
self.showNormal()
|
||||||
if content_w > 0 and content_h > 0:
|
if content_w > 0 and content_h > 0:
|
||||||
self._fit_to_content(content_w, content_h)
|
# Defer to next event-loop tick so Qt's showNormal() is processed
|
||||||
|
# by Hyprland before our hyprctl batch fires. Without this defer
|
||||||
|
# the two race and the window lands at top-left.
|
||||||
|
QTimer.singleShot(0, lambda: self._fit_to_content(content_w, content_h))
|
||||||
|
|
||||||
def resizeEvent(self, event) -> None:
|
def resizeEvent(self, event) -> None:
|
||||||
super().resizeEvent(event)
|
super().resizeEvent(event)
|
||||||
@ -880,6 +942,10 @@ class VideoPlayer(QWidget):
|
|||||||
self._pending_duration: float | None = None
|
self._pending_duration: float | None = None
|
||||||
self._media_ready_fired = False
|
self._media_ready_fired = False
|
||||||
self._current_file: str | None = None
|
self._current_file: str | None = None
|
||||||
|
# Last reported source video size — used to dedupe video-params
|
||||||
|
# observer firings so widget-driven re-emissions don't trigger
|
||||||
|
# repeated _fit_to_content calls (which would loop forever).
|
||||||
|
self._last_video_size: tuple[int, int] | None = None
|
||||||
|
|
||||||
def _ensure_mpv(self) -> mpvlib.MPV:
|
def _ensure_mpv(self) -> mpvlib.MPV:
|
||||||
"""Set up mpv callbacks on first use. MPV instance is pre-created."""
|
"""Set up mpv callbacks on first use. MPV instance is pre-created."""
|
||||||
@ -954,6 +1020,7 @@ class VideoPlayer(QWidget):
|
|||||||
self._media_ready_fired = False
|
self._media_ready_fired = False
|
||||||
self._pending_duration = None
|
self._pending_duration = None
|
||||||
self._eof_pending = False
|
self._eof_pending = False
|
||||||
|
self._last_video_size = None # reset dedupe so new file fires a fit
|
||||||
self._apply_loop_to_mpv()
|
self._apply_loop_to_mpv()
|
||||||
m.loadfile(path)
|
m.loadfile(path)
|
||||||
if self._autoplay:
|
if self._autoplay:
|
||||||
@ -1028,7 +1095,13 @@ class VideoPlayer(QWidget):
|
|||||||
def _on_video_params(self, _name: str, value) -> None:
|
def _on_video_params(self, _name: str, value) -> None:
|
||||||
"""Called from mpv thread when video dimensions become known."""
|
"""Called from mpv thread when video dimensions become known."""
|
||||||
if isinstance(value, dict) and value.get('w') and value.get('h'):
|
if isinstance(value, dict) and value.get('w') and value.get('h'):
|
||||||
self._pending_video_size = (value['w'], value['h'])
|
new_size = (value['w'], value['h'])
|
||||||
|
# mpv re-fires video-params on output-area changes too. Dedupe
|
||||||
|
# against the source dimensions we last reported so resizing the
|
||||||
|
# popout doesn't kick off a fit→resize→fit feedback loop.
|
||||||
|
if new_size != self._last_video_size:
|
||||||
|
self._last_video_size = new_size
|
||||||
|
self._pending_video_size = new_size
|
||||||
|
|
||||||
def _on_eof_reached(self, _name: str, value) -> None:
|
def _on_eof_reached(self, _name: str, value) -> None:
|
||||||
"""Called from mpv thread when eof-reached changes."""
|
"""Called from mpv thread when eof-reached changes."""
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user