Skip to content

Commit afe34d1

Browse files
committed
fix(window): clamp restored window geometry into the visible screen, center when fully off-screen
1 parent 6691aa9 commit afe34d1

2 files changed

Lines changed: 99 additions & 0 deletions

File tree

tests/test_ui_features.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,57 @@ def test_flash_without_drive_errors_when_none_detected(qapp, tmp_path):
179179
assert "plug one in first" in w._progress._error.text()
180180
finally:
181181
w._shutdown()
182+
183+
184+
def _screen_area():
185+
from PyQt6.QtGui import QGuiApplication
186+
187+
screen = QGuiApplication.primaryScreen()
188+
return screen.availableGeometry() if screen is not None else None
189+
190+
191+
def test_restored_geometry_clamped_inside_screen(qapp, tmp_path):
192+
"""A window whose saved position hangs off the right edge must be
193+
pulled back so nothing is cut off at the border."""
194+
available = _screen_area()
195+
if available is None:
196+
pytest.skip("no screen in this environment")
197+
198+
w = _make_window(qapp, tmp_path)
199+
try:
200+
w.resize(
201+
min(900, available.width() - 20),
202+
min(580, available.height() - 20),
203+
)
204+
w.move(available.right() - w.width() // 2 + 150, available.top() + 40)
205+
w._clamp_to_screen()
206+
frame = w.frameGeometry()
207+
assert frame.left() >= available.left()
208+
assert frame.right() <= available.right()
209+
assert frame.top() >= available.top()
210+
assert frame.bottom() <= available.bottom()
211+
finally:
212+
w._shutdown()
213+
214+
215+
def test_fully_offscreen_geometry_centered_on_screen(qapp, tmp_path):
216+
"""A saved position outside every screen must fall back to a centered
217+
placement instead of opening unreachable."""
218+
available = _screen_area()
219+
if available is None:
220+
pytest.skip("no screen in this environment")
221+
222+
w = _make_window(qapp, tmp_path)
223+
try:
224+
w.resize(
225+
min(900, available.width() - 20),
226+
min(580, available.height() - 20),
227+
)
228+
w.move(available.right() + 500, available.top() + 100)
229+
w._clamp_to_screen()
230+
frame = w.frameGeometry()
231+
assert frame.intersects(available)
232+
assert abs(frame.center().x() - available.center().x()) <= 3
233+
assert abs(frame.center().y() - available.center().y()) <= 3
234+
finally:
235+
w._shutdown()

ui/window.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,6 +1169,7 @@ def __init__(self) -> None:
11691169
self.restoreGeometry(
11701170
QByteArray.fromBase64(geometry.encode("ascii"))
11711171
)
1172+
self._clamp_to_screen()
11721173

11731174
self._poller = DrivePoller(self._detector, 2000)
11741175
self._poller.drives_ready.connect(self._on_drives_ready)
@@ -1216,6 +1217,50 @@ def __init__(self) -> None:
12161217
except Exception:
12171218
pass
12181219

1220+
def _clamp_to_screen(self) -> None:
1221+
"""Keep the window inside the current screen's visible area.
1222+
1223+
The saved window geometry can put the window partially or fully
1224+
off-screen after a monitor change or resolution switch, which
1225+
leaves the right side unreachable. Shrink to fit when possible,
1226+
clamp the position to the edges, and center the window when it
1227+
does not intersect any screen at all.
1228+
"""
1229+
frame = self.frameGeometry()
1230+
screen = QGuiApplication.screenAt(frame.center())
1231+
if screen is None:
1232+
screen = QGuiApplication.primaryScreen()
1233+
if screen is None:
1234+
return
1235+
available = screen.availableGeometry()
1236+
if not frame.intersects(available):
1237+
self.move(
1238+
available.center().x() - self.width() // 2,
1239+
available.center().y() - self.height() // 2,
1240+
)
1241+
return
1242+
if (
1243+
frame.width() > available.width()
1244+
and available.width() >= self.minimumWidth()
1245+
):
1246+
self.resize(available.width(), self.height())
1247+
frame = self.frameGeometry()
1248+
if (
1249+
frame.height() > available.height()
1250+
and available.height() >= self.minimumHeight()
1251+
):
1252+
self.resize(self.width(), available.height())
1253+
frame = self.frameGeometry()
1254+
x = min(
1255+
max(frame.x(), available.left()),
1256+
available.right() - frame.width(),
1257+
)
1258+
y = min(
1259+
max(frame.y(), available.top()),
1260+
available.bottom() - frame.height(),
1261+
)
1262+
self.move(x, y)
1263+
12191264
def _busy(self) -> bool:
12201265
return bool(
12211266
self._writing

0 commit comments

Comments
 (0)