F11 round-trip: preserve image zoom/pan + popout window position
Two related preservation bugs around the popout's F11 fullscreen
toggle, both surfaced during the post-refactor verification sweep.
1. ImageViewer zoom/pan loss on resize
ImageViewer.resizeEvent unconditionally called _fit_to_view() on every
resize event. F11 enter resizes the widget to the full screen, F11
exit resizes it back to the windowed size — both fired _fit_to_view,
clobbering any explicit user zoom and offset. Same problem for manual
window drags and splitter moves.
Fix: in resizeEvent, compute the previous-size fit-to-view zoom from
event.oldSize() and compare to current _zoom. Only re-fit if the user
was at fit-to-view at the previous size (within a 0.001 epsilon —
tighter than any wheel/key zoom step). Otherwise leave _zoom and
_offset alone.
The first-resize case (no valid oldSize, e.g. initial layout) still
defaults to fit, matching the original behavior for fresh widgets.
2. Popout window position lost on F11 round-trip
FullscreenPreview._enter_fullscreen captured _windowed_geometry but
the F11-exit restore goes through `_viewport` (the persistent center +
long_side that drives _fit_to_content). The drift detection in
_derive_viewport_for_fit only updates _viewport when
_last_dispatched_rect is set AND a fit is being computed — neither
path catches the "user dragged the popout with Super+drag and then
immediately pressed F11" sequence:
- Hyprland Super+drag does NOT fire Qt's moveEvent (xdg-toplevel
doesn't expose absolute screen position to clients on Wayland),
so Qt-side drift detection is dead on Hyprland.
- The Hyprland-side drift detection in _derive_viewport_for_fit
only fires inside a fit, and no fit is triggered between a drag
and F11.
- Result: _viewport still holds whatever it had before the drag —
typically the saved-from-last-session geometry seeded by the
first-fit one-shot at popout open.
When F11 exits, the deferred _fit_to_content reads the stale viewport
and restores the popout to the *previously seeded* position instead of
where the user actually had it.
Fix: in _enter_fullscreen, after capturing _windowed_geometry, also
write the current windowed state into self._viewport directly. The
viewport then holds the actual pre-fullscreen position regardless of
how it got there (drag, drag+nav, drag+F11, etc.), and F11 exit's
restore reads it correctly.
Bundled into one commit because both fixes are "F11 round-trip should
preserve where the user was" — the image fix preserves content state
(zoom/pan), the popout fix preserves window state (position). Same
theme, related root cause class. Bisecting one without the other
would be misleading.
Verified manually:
- image: scroll-zoom + drag pan + F11 + F11 → zoom and pan preserved
- image: untouched zoom + F11 + F11 → still fits to view
- image: scroll-zoom + manual window resize → zoom preserved
- popout: Super+drag to a new position + F11 + F11 → lands at the
dragged position, not at the saved-from-last-session position
- popout: same sequence on a video post → same result (videos don't
have zoom/pan, but the window-position fix applies to all media)
This commit is contained in:
parent
c4061b0d20
commit
b571c9a486
@ -149,6 +149,24 @@ class ImageViewer(QWidget):
|
|||||||
event.ignore()
|
event.ignore()
|
||||||
|
|
||||||
def resizeEvent(self, event) -> None:
|
def resizeEvent(self, event) -> None:
|
||||||
if self._pixmap:
|
if not self._pixmap:
|
||||||
|
return
|
||||||
|
pw, ph = self._pixmap.width(), self._pixmap.height()
|
||||||
|
if pw == 0 or ph == 0:
|
||||||
|
return
|
||||||
|
# Only re-fit if the user was at fit-to-view at the *previous*
|
||||||
|
# size. If they had explicitly zoomed/panned, leave _zoom and
|
||||||
|
# _offset alone — clobbering them on every resize (F11 toggle,
|
||||||
|
# manual window drag, splitter move) loses their state. Use
|
||||||
|
# event.oldSize() to compute the prior fit-to-view zoom and
|
||||||
|
# compare to current _zoom; the 0.001 epsilon absorbs float
|
||||||
|
# drift but is tighter than any wheel/key zoom step (±20%).
|
||||||
|
old = event.oldSize()
|
||||||
|
if old.isValid() and old.width() > 0 and old.height() > 0:
|
||||||
|
old_fit = min(old.width() / pw, old.height() / ph)
|
||||||
|
if abs(self._zoom - old_fit) < 0.001:
|
||||||
|
self._fit_to_view()
|
||||||
|
else:
|
||||||
|
# First resize (no valid old size) — default to fit.
|
||||||
self._fit_to_view()
|
self._fit_to_view()
|
||||||
self.update()
|
self.update()
|
||||||
|
|||||||
@ -905,15 +905,39 @@ class FullscreenPreview(QMainWindow):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def _enter_fullscreen(self) -> None:
|
def _enter_fullscreen(self) -> None:
|
||||||
"""Enter fullscreen — capture windowed geometry first so F11 back can restore it."""
|
"""Enter fullscreen — capture windowed geometry first so F11 back can restore it.
|
||||||
|
|
||||||
|
Also capture the current windowed state into the persistent
|
||||||
|
`_viewport` so the F11-exit restore lands at the user's actual
|
||||||
|
pre-F11 position, not at a stale viewport from before they last
|
||||||
|
dragged the window. The drift detection in `_derive_viewport_for_fit`
|
||||||
|
only fires when `_last_dispatched_rect` is set AND a fit is being
|
||||||
|
computed — neither path catches the "user dragged the popout
|
||||||
|
with Super+drag and then immediately pressed F11" sequence,
|
||||||
|
because Hyprland Super+drag doesn't fire Qt's moveEvent and no
|
||||||
|
nav has happened to trigger a fit. Capturing fresh into
|
||||||
|
`_viewport` here makes the restore correct regardless.
|
||||||
|
"""
|
||||||
from PySide6.QtCore import QRect
|
from PySide6.QtCore import QRect
|
||||||
win = self._hyprctl_get_window()
|
win = self._hyprctl_get_window()
|
||||||
if win and win.get("at") and win.get("size"):
|
if win and win.get("at") and win.get("size"):
|
||||||
x, y = win["at"]
|
x, y = win["at"]
|
||||||
w, h = win["size"]
|
w, h = win["size"]
|
||||||
self._windowed_geometry = QRect(x, y, w, h)
|
self._windowed_geometry = QRect(x, y, w, h)
|
||||||
|
self._viewport = Viewport(
|
||||||
|
center_x=x + w / 2,
|
||||||
|
center_y=y + h / 2,
|
||||||
|
long_side=float(max(w, h)),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self._windowed_geometry = self.frameGeometry()
|
self._windowed_geometry = self.frameGeometry()
|
||||||
|
rect = self._windowed_geometry
|
||||||
|
if rect.width() > 0 and rect.height() > 0:
|
||||||
|
self._viewport = Viewport(
|
||||||
|
center_x=rect.x() + rect.width() / 2,
|
||||||
|
center_y=rect.y() + rect.height() / 2,
|
||||||
|
long_side=float(max(rect.width(), rect.height())),
|
||||||
|
)
|
||||||
self.showFullScreen()
|
self.showFullScreen()
|
||||||
|
|
||||||
def _exit_fullscreen(self) -> None:
|
def _exit_fullscreen(self) -> None:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user