WIP: resizable camera region, menu panel collapse, zoom ladder

- center splitter: left controls | camera tabs | beamline column;
  drag resizes the camera width, View menu check actions collapse
  the fixed-width side panels (no on-screen buttons)
- font zoom restricted to a 100/125/150 percent ladder; the
  fixed-width side boxes scale with it so zoomed text does not clip
- camera tab widget min width clamped to 1/3 of its natural minimum
- resize bars: short centered grip PNGs shared by QMainWindow
  separators and QSplitter handles, single style source in styles.py
- window min width 1244 -> ~376: automation row and sample-list
  chip row stop propagating their natural minimums (Ignored-width
  hosts), portrait page fixed width removed (portrait mode pins the
  window width itself)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 13:28:16 +02:00
committed by duan_j
co-authored by Claude Fable 5
parent 30151b4441
commit fd0edffdc9
12 changed files with 173 additions and 88 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

+85 -23
View File
@@ -55,6 +55,7 @@ from PySide6.QtWidgets import (
QScrollArea,
QSizePolicy,
QSlider,
QSplitter,
QStackedWidget,
QTabWidget,
QToolBar,
@@ -101,7 +102,7 @@ from aare.gui.styles import (
APP_BACKGROUND,
DARK_TEXT,
DOCK_CONTENT_LEFT_PAD,
FONT_SCALE_STEP,
FONT_SCALE_LADDER,
SEPARATOR_HINT_DELAY_MS,
THEME_BLUEBIRD,
THEME_FADE_MS,
@@ -460,20 +461,32 @@ class MainWindow(QMainWindow):
self.left_column_layout.addWidget(self.left_column_tabs)
self.left_column_layout.addStretch()
top_widget_layout.addWidget(self.collection_controls_scroll)
# Splitter instead of a plain HBox: the side columns kept their fixed
# widths while the camera region could only grow vertically; dragging
# the handles now resizes the camera width too. Collapsing to zero is
# off — the View-menu toggles are the collapse feature.
# No local handle styling: the app stylesheet's QSplitter::handle rule
# already paints the same line as the QMainWindow::separator between
# docks, so both resize bars share the one definition in styles.py.
self.center_splitter = QSplitter(Qt.Orientation.Horizontal, top_widget)
self.center_splitter.setChildrenCollapsible(False)
top_widget_layout.addWidget(self.center_splitter)
self.center_splitter.addWidget(self.collection_controls_scroll)
self.collection_controls_scroll.setWidget(self.left_column)
# AsNeeded (was AlwaysOff): the splitter can now make the viewport
# narrower than the fixed-width column, and clipped controls must
# stay reachable.
self.collection_controls_scroll.setHorizontalScrollBarPolicy(
Qt.ScrollBarPolicy.ScrollBarAlwaysOff
Qt.ScrollBarPolicy.ScrollBarAsNeeded
)
self.collection_controls_scroll.setWidgetResizable(True)
# No frame: its border drew a line above the tab bar (Dewar tabs have
# none). Freeze the inner column width: widgetResizable makes it track
# the viewport, so the scrollbar appearing used to re-flow every
# banner. Fixed width + a permanent 10px scrollbar gutter means the
# scrollbar pops into spare space and nothing moves.
self.collection_controls_scroll.setFrameShape(QFrame.Shape.NoFrame)
self.left_column.setFixedWidth(self.data_collection.set_width)
self.collection_controls_scroll.setFixedWidth(self.data_collection.set_width + 10)
# banner. Fixed inner width + a 10px scrollbar gutter means the
# scrollbar pops into spare space and nothing moves; the widths are
# zoom-dependent and live in _apply_zoom_widths.
self.video_tab = QTabWidget(parent=top_widget)
@@ -514,6 +527,14 @@ class MainWindow(QMainWindow):
self.video_tab.addTab(self.beamline_view_panel, "Beamline view")
self.video_tab.addTab(self.beamline_combined_panel, "Beamline combined view")
# The camera region's natural minimum (~284px) came from the widest
# page's controls row (title + refresh button) and the tab labels —
# not from the video views, which rescale freely like they do in
# height. Clamp to 1/3 so the splitter can shrink all four camera
# views that far; past the natural width the tab bar scrolls and the
# controls rows clip.
self.video_tab.setMinimumWidth(self.video_tab.minimumSizeHint().width() // 3)
# if cfg_get("gui.cameras.secondary_beamline_camera_url", None):
# self.secondary_beamline_view = VideoGraphicsView()
# self.secondary_beamline_view_panel = AxisVideoPanel("Secondary view", self.secondary_beamline_view,
@@ -548,28 +569,32 @@ class MainWindow(QMainWindow):
portrait_page_layout = QHBoxLayout(self.portrait_mode_page)
portrait_page_layout.setContentsMargins(0, 0, 0, 0)
portrait_page_layout.setSpacing(0)
self.portrait_mode_page.setFixedWidth(self.portrait_mode_panel.PORTRAIT_WIDTH + 24)
# No fixed page width: an explicit minimum beats the content_stack's
# Ignored-when-hidden policy and pinned the whole window >=444px in
# NORMAL mode. enter_portrait_mode pins the window width itself.
portrait_page_layout.addWidget(
self.portrait_mode_panel, alignment=Qt.AlignmentFlag.AlignHCenter
)
top_widget_layout.addWidget(self.video_tab)
self.center_splitter.addWidget(self.video_tab)
self._start_axis_camera_threads()
self.beamline_controls_scroll = NoWheelScrollArea(top_widget)
self.beamline = BeamlineControls(self.beamline_controls_scroll)
top_widget_layout.addWidget(self.beamline_controls_scroll)
self.center_splitter.addWidget(self.beamline_controls_scroll)
self.beamline_controls_scroll.setWidget(self.beamline)
# Resizable so the column shrinks when panels collapse; without it the
# scrollbar keeps dead range below the collapsed panels.
self.beamline_controls_scroll.setWidgetResizable(True)
self.beamline_controls_scroll.setHorizontalScrollBarPolicy(
Qt.ScrollBarPolicy.ScrollBarAlwaysOff
Qt.ScrollBarPolicy.ScrollBarAsNeeded
)
# Same gutter math as the left column: 10px scrollbar + 2px frame, so
# the fixed-width controls are never clipped when the scrollbar shows.
self.beamline_controls_scroll.setFixedWidth(self.beamline.set_width + 12)
# Only the camera region absorbs a window resize; the side columns
# keep whatever width the user dragged.
self.center_splitter.setStretchFactor(1, 1)
# Gutter math per column (10px scrollbar + frame) lives here too.
self._apply_zoom_widths()
self.tell_samples = TellSamplePanel(samples=SampleShortInfoList(s=[]))
self.ref_tools_panel = ReferenceToolsPanel(samples=SampleShortInfoList(s=[]))
@@ -624,7 +649,14 @@ class MainWindow(QMainWindow):
# and the buttons balloon; five buttons plus two long checkboxes need
# them at their natural size.
automation_row.addStretch(1)
dewar_layout.addLayout(automation_row)
# Host widget with Ignored horizontal policy: the row's ~1000px
# natural minimum used to propagate dock -> window and pin the whole
# window wider than small screens (unshrinkable). Now the row clips
# from the right instead.
automation_row_host = QWidget(dewar_tab)
automation_row_host.setLayout(automation_row)
automation_row_host.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Fixed)
dewar_layout.addWidget(automation_row_host)
# "Remove selected" now unqueues the dewar-table selection — the
# queue's own table is no longer displayed.
@@ -2059,15 +2091,32 @@ class MainWindow(QMainWindow):
settings.setValue("appearance/font_scale", font_scale())
def _change_font_zoom(self, direction: int) -> None:
"""Ctrl+plus / Ctrl+minus / Ctrl+0 accessibility zoom: step the FONT_*
ladder scale and rebuild the app stylesheet (same repolish path as a
theme switch, so the whole UI rescales in one pass)."""
# round: repeated 0.1 float steps otherwise drift (1.2000000000000002)
set_font_scale(
1.0 if direction == 0 else round(font_scale() + direction * FONT_SCALE_STEP, 2)
)
"""Ctrl+plus / Ctrl+minus / Ctrl+0 accessibility zoom: walk the
100/125/150% ladder and rebuild the app stylesheet (same repolish
path as a theme switch, so the whole UI rescales in one pass)."""
# index() is safe: set_font_scale snaps every value onto the ladder.
i = FONT_SCALE_LADDER.index(font_scale())
i = 0 if direction == 0 else max(0, min(len(FONT_SCALE_LADDER) - 1, i + direction))
set_font_scale(FONT_SCALE_LADDER[i])
self._save_theme_settings()
self._apply_theme()
self._apply_zoom_widths()
def _apply_zoom_widths(self) -> None:
# The side columns are fixed-width designs; when the ladder scales the
# text, the boxes must widen with it or the zoomed text clips. Runs
# once at construction and again on every zoom change.
s = font_scale()
left = round(self.data_collection.set_width * s)
self.data_collection.setFixedWidth(left)
self.left_column.setFixedWidth(left)
self.collection_controls_scroll.setMaximumWidth(left + 10)
right = round(self.beamline.set_width * s)
self.beamline.setFixedWidth(right)
self.beamline_controls_scroll.setMaximumWidth(right + 12)
# Re-seat the splitter: open both columns to their (new) full width,
# the camera region takes the rest.
self.center_splitter.setSizes([left + 10, 10_000, right + 12])
@Slot()
def use_legacy_theme(self) -> None:
@@ -2157,6 +2206,19 @@ class MainWindow(QMainWindow):
reset_text_action.triggered.connect(lambda: self._change_font_zoom(0))
view_menu.addAction(reset_text_action)
view_menu.addSeparator()
# Side-panel collapse lives here, not as on-screen buttons.
self._show_left_panel_action = QAction("Show Left Panel", self)
self._show_left_panel_action.setCheckable(True)
self._show_left_panel_action.setChecked(True)
self._show_left_panel_action.toggled.connect(self.collection_controls_scroll.setVisible)
view_menu.addAction(self._show_left_panel_action)
self._show_right_panel_action = QAction("Show Right Panel", self)
self._show_right_panel_action.setCheckable(True)
self._show_right_panel_action.setChecked(True)
self._show_right_panel_action.toggled.connect(self.beamline_controls_scroll.setVisible)
view_menu.addAction(self._show_right_panel_action)
view_menu.addSeparator()
view_menu.addAction(self._portrait_mode_action)
view_menu.addAction(self._enter_automation_view_action)
view_menu.addSeparator()
+10 -1
View File
@@ -15,7 +15,9 @@ from PySide6.QtWidgets import (
QHeaderView,
QMenu,
QPushButton,
QSizePolicy,
QTableView,
QWidget,
)
from aare.gui.constants import LOGGER_NAME
@@ -196,7 +198,14 @@ class TellSamplePanel(QFrame):
self.status_chips.addButton(chip)
chip_row.addWidget(chip)
chip_row.addStretch()
grid_layout.addLayout(chip_row, 1, 0, 1, 4)
# Ignored-width host (same trick as the automation row in
# main_window): the five chips' ~380px natural minimum otherwise
# propagates dock -> window and blocks shrinking the window on small
# screens. The row clips from the right instead.
chip_host = QWidget(self)
chip_host.setLayout(chip_row)
chip_host.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Fixed)
grid_layout.addWidget(chip_host, 1, 0, 1, 4)
self.status_chips.buttonClicked.connect(
lambda chip: self.table_model.set_status_filter(chip.property("status_key"))
)
+49 -58
View File
@@ -82,6 +82,20 @@ DARK_SLIDER_GRIP = (_GRAPHICS_DIR / "slider_grip_dark.png").as_posix()
# Check marks (PRIMARY blue / dusk gold at generation time):
CHECK_MARK = (_GRAPHICS_DIR / "check_mark_light.png").as_posix()
DARK_CHECK_MARK = (_GRAPHICS_DIR / "check_mark_dark.png").as_posix()
# Resize-bar grips: a short (66px) centered bar instead of a full-length
# line — the full line read as a wall between panels. QSS can't paint a
# fixed-length centered segment, so PNGs like the arrows above; used by BOTH
# QMainWindow::separator and QSplitter::handle, so this is the one place.
# Colors baked in (SEPARATOR_IDLE / SEPARATOR_HINT / DARK_SEPARATOR_IDLE /
# DARK_ACCENT) — regenerate if those knobs change.
GRIP_IDLE_V = (_GRAPHICS_DIR / "resize_grip_v_idle_light.png").as_posix()
GRIP_IDLE_H = (_GRAPHICS_DIR / "resize_grip_h_idle_light.png").as_posix()
GRIP_HINT_V = (_GRAPHICS_DIR / "resize_grip_v_hint_light.png").as_posix()
GRIP_HINT_H = (_GRAPHICS_DIR / "resize_grip_h_hint_light.png").as_posix()
DARK_GRIP_IDLE_V = (_GRAPHICS_DIR / "resize_grip_v_idle_dark.png").as_posix()
DARK_GRIP_IDLE_H = (_GRAPHICS_DIR / "resize_grip_h_idle_dark.png").as_posix()
DARK_GRIP_HINT_V = (_GRAPHICS_DIR / "resize_grip_v_hint_dark.png").as_posix()
DARK_GRIP_HINT_H = (_GRAPHICS_DIR / "resize_grip_h_hint_dark.png").as_posix()
# Borders (all can be "transparent" to hide the line):
BORDER = "transparent" # main dividers, e.g. the beamline state bar top line
@@ -124,32 +138,10 @@ SEPARATOR_HINT = "#3f4a5f"
SEPARATOR_IDLE = "#47536a"
SEPARATOR_HINT_DELAY_MS = 66 # int, used in code, not QSS
# The gutter keeps this full width for the mouse; only a 2px line + 1px
# shadow is painted inside it (a solid 5px bar read too heavy). 5px is a
# The gutter keeps this full width for the mouse; only the short GRIP_*
# bar is painted inside it (a full-length line read as a wall). 5px is a
# first guess — adjust here if the grab target feels off.
SEPARATOR_REGION = "5px"
SEPARATOR_SHADOW = "rgba(31, 41, 59, 25%)"
def _separator_gradient(line: str, shadow: str, axis: str) -> str:
"""Paint of a resize gutter: transparent 1px, shadow 1px, line 2px,
shadow 1px — hard gradient stops at 1/5 steps of SEPARATOR_REGION.
axis "x" runs the gradient left->right (an upright line), "y" top->down
(a lying line)."""
x2, y2 = ("1", "0") if axis == "x" else ("0", "1")
return (
f"qlineargradient(x1:0, y1:0, x2:{x2}, y2:{y2},"
f" stop:0 transparent, stop:0.19 transparent,"
f" stop:0.2 {shadow}, stop:0.39 {shadow},"
f" stop:0.4 {line}, stop:0.79 {line},"
f" stop:0.8 {shadow}, stop:1 {shadow})"
)
SEP_IDLE_X = _separator_gradient(SEPARATOR_IDLE, SEPARATOR_SHADOW, "x")
SEP_IDLE_Y = _separator_gradient(SEPARATOR_IDLE, SEPARATOR_SHADOW, "y")
SEP_HINT_X = _separator_gradient(SEPARATOR_HINT, SEPARATOR_SHADOW, "x")
SEP_HINT_Y = _separator_gradient(SEPARATOR_HINT, SEPARATOR_SHADOW, "y")
# Theme-switch screenshot cross-fade duration (int ms, used in code).
THEME_FADE_MS = 250
@@ -257,11 +249,6 @@ DARK_ACCENT = "#e0913f" # gold
# DARK_ACCENT @70%: 10% measured 1.16:1 on DARK_BG (invisible); 70% blends to
# >=3.74:1 — same WCAG 1.4.11 floor as the light-theme separators.
DARK_SEPARATOR_IDLE = "rgba(224, 145, 63, 70%)"
DARK_SEPARATOR_SHADOW = "rgba(0, 0, 0, 45%)"
DARK_SEP_IDLE_X = _separator_gradient(DARK_SEPARATOR_IDLE, DARK_SEPARATOR_SHADOW, "x")
DARK_SEP_IDLE_Y = _separator_gradient(DARK_SEPARATOR_IDLE, DARK_SEPARATOR_SHADOW, "y")
DARK_SEP_HINT_X = _separator_gradient(DARK_ACCENT, DARK_SEPARATOR_SHADOW, "x")
DARK_SEP_HINT_Y = _separator_gradient(DARK_ACCENT, DARK_SEPARATOR_SHADOW, "y")
DARK_ACCENT_HOVER = "#eaa253" # accent2 — brighter gold
DARK_ACCENT_FILL = "#89b4fa" # action blue
DARK_ACCENT_FILL_HOVER = "#9ec2fb" # +10% white, derived (site has no step)
@@ -607,9 +594,10 @@ FONT_FINE = "11px" # fine print, queue titles
# Whole-app text scale applied to the FONT_* ladder when the QSS is built, so
# one re-apply of the stylesheet rescales every rule. Floats, so _palette()'s
# str filter never picks them up as colors.
FONT_SCALE_MIN = 0.8
FONT_SCALE_MAX = 1.6
FONT_SCALE_STEP = 0.1
# Discrete ladder: 100% / 125% / 150% only. Free ±10% steps produced
# in-between sizes where the fixed-width side boxes clipped their text;
# three vetted stops keep fonts and box widths in lockstep.
FONT_SCALE_LADDER = (1.0, 1.25, 1.5)
_font_scale = 1.0
@@ -618,10 +606,10 @@ def font_scale() -> float:
def set_font_scale(scale: float) -> None:
# Clamped: below 0.8 the fine-print sizes fall under 9px (unreadable),
# above 1.6 the fixed-height banner rows start clipping their text.
# Snap to the ladder so old saved settings (e.g. 1.2 from the former
# free-step zoom) land on the nearest vetted stop.
global _font_scale
_font_scale = max(FONT_SCALE_MIN, min(FONT_SCALE_MAX, scale))
_font_scale = min(FONT_SCALE_LADDER, key=lambda s: abs(s - scale))
# -- Hover tooltips (the QToolTip popup; styled borderless) -----------------
@@ -1169,20 +1157,21 @@ def _sunrise_stylesheet(overrides: dict[str, str] | None = None) -> str:
}
/* Resize gutters: the mouse keeps the full $separator_region, but only
a 2px line + 1px shadow paints (gradients in _separator_gradient).
An upright line needs the x-gradient: that is a :vertical main-window
separator but a :horizontal splitter handle (handle orientation
follows the splitter, not the bar). */
the short centered GRIP_* bar paints (image: never stretches, it
centers). An upright grip is the _v file: that is a :vertical
main-window separator but a :horizontal splitter handle (handle
orientation follows the splitter, not the bar). */
QMainWindow::separator {
width: $separator_region;
height: $separator_region;
background: transparent;
}
QSplitter::handle { background: transparent; }
QMainWindow::separator:vertical, QSplitter::handle:horizontal {
background: $sep_idle_x;
image: url($grip_idle_v);
}
QMainWindow::separator:horizontal, QSplitter::handle:vertical {
background: $sep_idle_y;
image: url($grip_idle_h);
}
QSplitter::handle:horizontal { width: $separator_region; }
QSplitter::handle:vertical { height: $separator_region; }
@@ -1191,19 +1180,20 @@ def _sunrise_stylesheet(overrides: dict[str, str] | None = None) -> str:
MainWindow.event() after a 1s hover rest or on press; :hover limits
the fill to the exact separator being dragged. */
QMainWindow[separatorHint="true"]::separator:vertical:hover {
background: $sep_hint_x;
image: url($grip_hint_v);
}
QMainWindow[separatorHint="true"]::separator:horizontal:hover {
background: $sep_hint_y;
image: url($grip_hint_h);
}
/* Splitter handles (prediction metrics) are plain child widgets the
property gate above doesn't reach — immediate hover/press hint. */
/* Splitter handles (camera region, prediction metrics) are plain child
widgets the property gate above doesn't reach — immediate hover/press
hint. */
QSplitter::handle:horizontal:hover, QSplitter::handle:horizontal:pressed {
background: $sep_hint_x;
image: url($grip_hint_v);
}
QSplitter::handle:vertical:hover, QSplitter::handle:vertical:pressed {
background: $sep_hint_y;
image: url($grip_hint_h);
}
QFrame#beamlineControls,
@@ -1846,33 +1836,34 @@ def _sunset_stylesheet() -> str:
background: transparent;
}
/* Idle + hover resize lines, dark flavor — see the light-theme note. */
/* Idle + hover resize grips, dark flavor — see the light-theme note. */
QMainWindow::separator {
width: $separator_region;
height: $separator_region;
background: transparent;
}
QSplitter::handle { background: transparent; }
QMainWindow::separator:vertical, QSplitter::handle:horizontal {
background: $dark_sep_idle_x;
image: url($dark_grip_idle_v);
}
QMainWindow::separator:horizontal, QSplitter::handle:vertical {
background: $dark_sep_idle_y;
image: url($dark_grip_idle_h);
}
QSplitter::handle:horizontal { width: $separator_region; }
QSplitter::handle:vertical { height: $separator_region; }
QMainWindow[separatorHint="true"]::separator:vertical:hover {
background: $dark_sep_hint_x;
image: url($dark_grip_hint_v);
}
QMainWindow[separatorHint="true"]::separator:horizontal:hover {
background: $dark_sep_hint_y;
image: url($dark_grip_hint_h);
}
QSplitter::handle:horizontal:hover, QSplitter::handle:horizontal:pressed {
background: $dark_sep_hint_x;
image: url($dark_grip_hint_v);
}
QSplitter::handle:vertical:hover, QSplitter::handle:vertical:pressed {
background: $dark_sep_hint_y;
image: url($dark_grip_hint_h);
}
/* Plain scroll containers stay frameless. */
@@ -2047,12 +2038,12 @@ if __name__ == "__main__":
assert APP_BACKGROUND not in build_app_stylesheet(THEME_BLUEBIRD)
# Font zoom: the FONT_* ladder must follow the scale, and the scale must
# clamp to its documented bounds.
set_font_scale(1.3)
assert "font-size: 21px" in build_app_stylesheet(THEME_SUNRISE) # title 16->21
set_font_scale(1.3) # snaps to the 1.25 ladder stop
assert "font-size: 20px" in build_app_stylesheet(THEME_SUNRISE) # title 16->20
set_font_scale(99.0)
assert font_scale() == FONT_SCALE_MAX
assert font_scale() == FONT_SCALE_LADDER[-1]
set_font_scale(0.0)
assert font_scale() == FONT_SCALE_MIN
assert font_scale() == FONT_SCALE_LADDER[0]
set_font_scale(1.0)
assert "font-size: 16px" in build_app_stylesheet(THEME_SUNRISE)
# This line was added by Claude. But I would do the same. So all gude.
+29 -6
View File
@@ -72,6 +72,21 @@ def test_main_window_init(qtbot, mock_ui_state, daq_status_factory):
win.data_collection._emit_change_energy()
assert sent and abs(sent[0] - 12400.0) < 1e-6
# Side panels collapse from the View menu (no on-screen buttons), and
# the camera region sits in a splitter so its width is drag-resizable.
assert win.center_splitter.widget(1) is win.video_tab
# The clamp lets the splitter shrink the camera region well below the
# natural minimum the tab labels + controls rows would demand.
assert 0 < win.video_tab.minimumWidth() < win.video_tab.minimumSizeHint().width()
win._show_left_panel_action.trigger()
assert win.collection_controls_scroll.isHidden()
win._show_left_panel_action.trigger()
assert not win.collection_controls_scroll.isHidden()
win._show_right_panel_action.trigger()
assert win.beamline_controls_scroll.isHidden()
win._show_right_panel_action.trigger()
assert not win.beamline_controls_scroll.isHidden()
# Motion watch: only the robot station switches to the combined
# beamline view. Moving no longer does (users kept losing the sample
# camera on short gonio moves), and busy alone never does — Sample
@@ -525,20 +540,28 @@ def test_font_zoom_steps_clamps_and_resets(qtbot, mock_ui_state):
try:
base_pt = win._default_app_font.pointSizeF()
win._change_font_zoom(1)
assert styles.font_scale() == pytest.approx(1.1)
assert styles.font_scale() == pytest.approx(1.25)
# Part 2 of the zoom: the app default font scales with the ladder.
app = QApplication.instance()
assert isinstance(app, QApplication) # narrow from QCoreApplication|None
assert app.font().pointSizeF() == pytest.approx(base_pt * 1.1)
for _ in range(20):
assert app.font().pointSizeF() == pytest.approx(base_pt * 1.25)
# Part 3: the fixed-width side boxes follow the ladder, else the
# zoomed text clips inside them.
assert win.data_collection.minimumWidth() == round(win.data_collection.set_width * 1.25)
assert win.collection_controls_scroll.maximumWidth() == (
round(win.data_collection.set_width * 1.25) + 10
)
assert win.beamline.minimumWidth() == round(win.beamline.set_width * 1.25)
for _ in range(5):
win._change_font_zoom(1)
assert styles.font_scale() == styles.FONT_SCALE_MAX
assert styles.font_scale() == styles.FONT_SCALE_LADDER[-1]
win._change_font_zoom(0)
assert styles.font_scale() == 1.0
assert app.font().pointSizeF() == pytest.approx(base_pt)
for _ in range(20):
assert win.data_collection.minimumWidth() == win.data_collection.set_width
for _ in range(5):
win._change_font_zoom(-1)
assert styles.font_scale() == styles.FONT_SCALE_MIN
assert styles.font_scale() == styles.FONT_SCALE_LADDER[0] # floor is 100%
finally:
styles.set_font_scale(1.0)
if saved is None: