feat(ui): adapt to the screen and its scaling, not to fixed pixels
Two things differ between machines and only one of them is width: a 4K panel
has more pixels, while a 125%/150% display has the same logical pixels holding
LESS, because every label and margin is taller. Breakpoints written as raw
pixels only hold on the machine they were tuned on.
* ui_scale() derives a factor from font height (1.0 at the 15px line the
layouts were measured against) and every narrow-guard threshold is
multiplied by it, so panes fold when the content is cramped rather than
when a number is crossed.
* The window takes a share of the available screen (80% × 85%) with the old
1180×760 as the floor, instead of opening at that size on any monitor.
* Moving the window to another screen re-pins the assistant and re-decides
the fold, since the new screen's work area and scaling may differ.
Found by tools/check_multi_screen.py, which walks 5 window sizes × 3 font
scales:
* At 150%, Schedule was clipped on 1280 and 1366 screens and the window's
own minimum grew to 1459px — wider than a 1280 laptop, so the app could
not fit at all. The cause was not the lanes: the one-line lane-count
summary in the header reported a sizeHint wide enough to set the minimum
width of the entire window. It now yields first (its text stays in the
tooltip); the window minimum drops 1459 → 752 and holds there at every
scale.
Also: these checkers exited 0xC0000409 from a Qt teardown crash AFTER printing
their verdict. check_probes_bite decides whether a probe caught its mutation by
reading exit codes, so a crash would have counted as "caught" — the round could
have passed while proving nothing. They now flush and os._exit with the real
verdict, and round 5 still catches all six mutations.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -966,7 +966,7 @@ class Co4ETab(QWidget):
|
||||
super().showEvent(e)
|
||||
self._narrow_guard.attach()
|
||||
|
||||
def _apply_narrow_layout(self, narrow: bool) -> None:
|
||||
def _apply_narrow_layout(self, narrow: bool) -> None: # noqa: D401
|
||||
"""Fold the step-config panel on a narrow window, restore it when there
|
||||
is room again.
|
||||
|
||||
|
||||
+13
-4
@@ -16,8 +16,8 @@ from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QAbstractItemView, QComboBox, QDialog, QDialogButtonBox, QHBoxLayout,
|
||||
QLabel, QLineEdit, QListWidget, QListWidgetItem, QMenu, QMessageBox,
|
||||
QPlainTextEdit, QPushButton, QScrollArea, QStackedWidget, QTabBar,
|
||||
QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget,
|
||||
QPlainTextEdit, QPushButton, QScrollArea, QSizePolicy, QStackedWidget,
|
||||
QTabBar, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
from ..core import tasks as taskrepo
|
||||
@@ -89,6 +89,13 @@ class ScheduleTaskTab(QWidget):
|
||||
self._title.setStyleSheet("font-weight:700; font-size:15px;")
|
||||
self.counts_lbl = QLabel("")
|
||||
self.counts_lbl.setObjectName("hint")
|
||||
# A one-line summary of every lane's count. Left to size itself it
|
||||
# reported a sizeHint wide enough to set the MINIMUM width of the whole
|
||||
# screen — 1285px at 150% scaling, which then became the window's
|
||||
# minimum and stopped the app fitting a 1280px laptop. It is a summary,
|
||||
# and the same numbers are on each lane header, so it gives way first.
|
||||
self.counts_lbl.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Preferred)
|
||||
self.counts_lbl.setMinimumWidth(0)
|
||||
self.add_btn = QPushButton()
|
||||
self.add_btn.setIcon(icon("plus"))
|
||||
self.add_btn.setObjectName("primary")
|
||||
@@ -241,8 +248,10 @@ class ScheduleTaskTab(QWidget):
|
||||
empty = QListWidgetItem(tr("schedtask.no_tasks"))
|
||||
empty.setFlags(Qt.NoItemFlags)
|
||||
col.addItem(empty)
|
||||
self.counts_lbl.setText(" ".join(
|
||||
f"{tr(f'schedtask.status.{s}')}: {counts[s]}" for s in STATUSES if counts[s]))
|
||||
summary = " ".join(
|
||||
f"{tr(f'schedtask.status.{s}')}: {counts[s]}" for s in STATUSES if counts[s])
|
||||
self.counts_lbl.setText(summary)
|
||||
self.counts_lbl.setToolTip(summary) # full text stays reachable if clipped
|
||||
self.calendar.set_tasks(all_tasks)
|
||||
|
||||
# ---- actions --------------------------------------------------------
|
||||
|
||||
+23
-1
@@ -159,6 +159,21 @@ def guard_wheel(root: QWidget) -> None:
|
||||
w.installEventFilter(_wheel_guard)
|
||||
|
||||
|
||||
def ui_scale(widget: QWidget) -> float:
|
||||
"""How much bigger this machine draws things than the design baseline.
|
||||
|
||||
Breakpoints written as raw pixels only hold on the screen they were tuned
|
||||
on. At 125%/150% display scaling Qt still reports logical pixels, but every
|
||||
label, button and margin is taller — so the same layout needs MORE logical
|
||||
width before it stops being cramped. Font height is the honest proxy for
|
||||
that: it moves with the display scale and with a user's font-size choice,
|
||||
both of which change how much fits.
|
||||
|
||||
1.0 at the 15px line height the layouts were measured against.
|
||||
"""
|
||||
return max(0.75, min(2.5, widget.fontMetrics().height() / 15.0))
|
||||
|
||||
|
||||
class _NarrowGuard(QObject):
|
||||
"""Calls back when the WINDOW crosses a width threshold.
|
||||
|
||||
@@ -184,6 +199,11 @@ class _NarrowGuard(QObject):
|
||||
if win is not None and win is not self._owner and win is not self._window:
|
||||
win.installEventFilter(self)
|
||||
self._window = win
|
||||
# Dragging the window to a monitor with different scaling changes
|
||||
# how much fits without changing its width, so re-decide then too.
|
||||
handle = win.windowHandle()
|
||||
if handle is not None:
|
||||
handle.screenChanged.connect(lambda *_a: self.check())
|
||||
self.check()
|
||||
|
||||
def eventFilter(self, obj, ev): # noqa: N802 - Qt override
|
||||
@@ -194,7 +214,9 @@ class _NarrowGuard(QObject):
|
||||
def check(self) -> None:
|
||||
win = self._owner.window()
|
||||
width = win.width() if win is not None else self._owner.width()
|
||||
narrow = width < self._threshold
|
||||
# The threshold is written for the baseline scale and grows with the
|
||||
# machine's — see ui_scale().
|
||||
narrow = width < self._threshold * ui_scale(self._owner)
|
||||
if narrow == self._auto:
|
||||
return
|
||||
self._auto = narrow
|
||||
|
||||
Reference in New Issue
Block a user