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>
|
After Width: | Height: | Size: 139 B |
|
After Width: | Height: | Size: 141 B |
|
After Width: | Height: | Size: 148 B |
|
After Width: | Height: | Size: 137 B |
|
After Width: | Height: | Size: 145 B |
|
After Width: | Height: | Size: 155 B |
|
After Width: | Height: | Size: 151 B |
|
After Width: | Height: | Size: 143 B |
@@ -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()
|
||||
|
||||
@@ -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"))
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||