feat: collapsible half-height panel banners with a universal gap
CI / lint (push) Skipped
CI / test (3.11) (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
CI / test (3.12) (pull_request) Successful in 48s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Successful in 52s
CI / lint (pull_request) Failing after 1m47s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Successful in 2m27s
CI / test (3.13) (pull_request) Successful in 2m56s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Successful in 3m55s
CI / test-with-coverage (pull_request) Successful in 4m23s
CI / coverage-analysis (pull_request) Successful in 4s
CI / test (3.11) (pull_request) Successful in 7m2s
CI / lint (push) Skipped
CI / test (3.11) (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
CI / test (3.12) (pull_request) Successful in 48s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Successful in 52s
CI / lint (pull_request) Failing after 1m47s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Successful in 2m27s
CI / test (3.13) (pull_request) Successful in 2m56s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Successful in 3m55s
CI / test-with-coverage (pull_request) Successful in 4m23s
CI / coverage-analysis (pull_request) Successful in 4s
CI / test (3.11) (pull_request) Successful in 7m2s
TitleLabel gains an optional collapsible mode: whole-banner click target, bare +/- glyph (no pill background), per-title collapsed state persisted in QSettings, and an expand() helper. Banners are halved to 25px with plain text + QSS font (H3 margins would clip vertically). - convert Zoom, Light, Omega, Smargon, Sample camera, Monochromator, ABR meas. pos., Beam mark, Beam center, Beam size, Dataset path and Loop centering to collapsible banners - new collapsible Exp. Config. section wrapping the scan tabs - move Manual sample from a bottom dock into the left column between Dataset path and Exp. Config.; Ctrl+M now expands it instead of raising the dock - Beamline state banner restyled to match (25px, plain font, bare glyph, whole banner clickable) - universal 11px banner gap via tighten_column() shared by both panel columns and the left column stack; ABR banner spans all grid columns and file path / manual sample drop side margins so widths align - abort button sits under the tabs instead of pinned to the bottom - unit tests for the collapse behavior Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+12
-27
@@ -50,7 +50,6 @@ from aare.gui.panels.local_contact_panel import LocalContactDialog
|
||||
# panels
|
||||
from aare.gui.panels.log_panel import LogDock
|
||||
from aare.gui.panels.loop_centering_panel import LoopCenteringPanel
|
||||
from aare.gui.panels.manual_sample_panel import ManualSamplePanel
|
||||
from aare.gui.panels.portrait_mode import PortraitModePanel
|
||||
from aare.gui.panels.prediction_metrics_panel import PredictionMetricsPanel
|
||||
from aare.gui.panels.reference_tools_panel import ReferenceToolsPanel
|
||||
@@ -86,6 +85,7 @@ from aare.gui.widgets.baton_request_dialog import BatonPendingDialog, BatonReque
|
||||
from aare.gui.widgets.busy_overlay import build_busy_overlay_style
|
||||
from aare.gui.widgets.camera_image import SampleCameraImageLabel
|
||||
from aare.gui.widgets.message_box import precondition_check
|
||||
from aare.gui.widgets.title_label import tighten_column
|
||||
from aare.gui.widgets.no_wheel_scroll_area import NoWheelScrollArea
|
||||
from aare.gui.widgets.status_bar import StatusBar
|
||||
from aare.gui.widgets.video_image import VideoGraphicsView
|
||||
@@ -235,7 +235,6 @@ class MainWindow(QMainWindow):
|
||||
self.left_column = QWidget(parent=top_widget)
|
||||
self.left_column_layout = QVBoxLayout(self.left_column)
|
||||
self.left_column_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.left_column_layout.setSpacing(8)
|
||||
|
||||
self.data_collection = DataCollectionSettings(
|
||||
s=geom, parent=self.left_column, raster_mgr=self.raster, diffraction=diffraction
|
||||
@@ -255,6 +254,8 @@ class MainWindow(QMainWindow):
|
||||
else:
|
||||
self.beamline_state_panel.hide()
|
||||
self.left_column_layout.addStretch()
|
||||
# Same universal banner gap as inside the panel columns.
|
||||
tighten_column(self.left_column_layout)
|
||||
|
||||
top_widget_layout.addWidget(self.collection_controls_scroll)
|
||||
self.collection_controls_scroll.setWidget(self.left_column)
|
||||
@@ -356,6 +357,9 @@ class MainWindow(QMainWindow):
|
||||
)
|
||||
top_widget_layout.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
|
||||
)
|
||||
@@ -400,13 +404,10 @@ class MainWindow(QMainWindow):
|
||||
self.job_list_dock.setAllowedAreas(Qt.DockWidgetArea.BottomDockWidgetArea)
|
||||
self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.job_list_dock)
|
||||
|
||||
self.manual_sample_panel = ManualSamplePanel()
|
||||
|
||||
self.manual_sample_dock = QDockWidget("Manual sample", self)
|
||||
self.manual_sample_dock.setObjectName("manual_sample_dock")
|
||||
self.manual_sample_dock.setWidget(self.manual_sample_panel)
|
||||
self.manual_sample_dock.setAllowedAreas(Qt.DockWidgetArea.BottomDockWidgetArea)
|
||||
self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.manual_sample_dock)
|
||||
# Manual sample lives in the left column (DataCollectionSettings)
|
||||
# between Dataset path and Exp. Config., collapsible like its
|
||||
# neighbors — it is no longer a bottom dock.
|
||||
self.manual_sample_panel = self.data_collection.manual_sample_panel
|
||||
|
||||
self.automation_progress_panel = AutomationProgressWidget()
|
||||
self.automation_progress_dock = QDockWidget("Automation progress", self)
|
||||
@@ -446,9 +447,6 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self.tabifyDockWidget(self.automation_progress_dock, self.log_dock)
|
||||
|
||||
self.tabifyDockWidget(self.manual_sample_dock, self.automation_progress_dock)
|
||||
self.tabifyDockWidget(self.automation_progress_dock, self.log_dock)
|
||||
|
||||
self.job_list_panel.samples_in_queue_changed.connect(
|
||||
self.automation_progress_panel.set_samples_in_queue
|
||||
)
|
||||
@@ -888,10 +886,10 @@ class MainWindow(QMainWindow):
|
||||
register_tutorials(self, self.tutorial_manager)
|
||||
|
||||
def _setup_global_shortcuts(self) -> None:
|
||||
self._shortcut_manual_sample = QAction("Raise Manual Sample Dock", self)
|
||||
self._shortcut_manual_sample = QAction("Expand Manual Sample", self)
|
||||
self._shortcut_manual_sample.setShortcut(QKeySequence("Ctrl+M"))
|
||||
self._shortcut_manual_sample.triggered.connect(
|
||||
lambda: (self.manual_sample_dock.setVisible(True), self.manual_sample_dock.raise_())
|
||||
lambda: self.manual_sample_panel.title.expand()
|
||||
)
|
||||
self.addAction(self._shortcut_manual_sample)
|
||||
|
||||
@@ -1114,7 +1112,6 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self.tell_samples_dock.setVisible(False)
|
||||
self.job_list_dock.setVisible(False)
|
||||
self.manual_sample_dock.setVisible(False)
|
||||
self.automation_progress_dock.setVisible(False)
|
||||
self.face_panel_dock.setVisible(False)
|
||||
self.fluor_panel_dock.setVisible(False)
|
||||
@@ -1192,7 +1189,6 @@ class MainWindow(QMainWindow):
|
||||
for dock_attr in (
|
||||
"tell_samples_dock",
|
||||
"job_list_dock",
|
||||
"manual_sample_dock",
|
||||
"automation_progress_dock",
|
||||
"face_panel_dock",
|
||||
"fluor_panel_dock",
|
||||
@@ -1260,7 +1256,6 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self.tell_samples_dock.setVisible(True)
|
||||
self.job_list_dock.setVisible(True)
|
||||
self.manual_sample_dock.setVisible(True)
|
||||
self.automation_progress_dock.setVisible(False)
|
||||
self.face_panel_dock.setVisible(False)
|
||||
self.fluor_panel_dock.setVisible(False)
|
||||
@@ -1462,15 +1457,6 @@ class MainWindow(QMainWindow):
|
||||
self.job_list_dock.visibilityChanged.connect(show_job_list_action.setChecked)
|
||||
view_menu.addAction(show_job_list_action)
|
||||
|
||||
show_manual_sample_action = QAction("Show manual sample", self)
|
||||
show_manual_sample_action.setCheckable(True)
|
||||
show_manual_sample_action.setChecked(True)
|
||||
show_manual_sample_action.triggered.connect(
|
||||
lambda checked: self.manual_sample_dock.setVisible(checked)
|
||||
)
|
||||
self.manual_sample_dock.visibilityChanged.connect(show_manual_sample_action.setChecked)
|
||||
view_menu.addAction(show_manual_sample_action)
|
||||
|
||||
show_face_panel_action = QAction("Show face detection", self)
|
||||
show_face_panel_action.setCheckable(True)
|
||||
show_face_panel_action.setChecked(False)
|
||||
@@ -1611,7 +1597,6 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self.tell_samples_dock.setVisible(True)
|
||||
self.job_list_dock.setVisible(True)
|
||||
self.manual_sample_dock.setVisible(True)
|
||||
|
||||
if self._beamline_state_panel_enabled:
|
||||
self.beamline_state_panel.setVisible(True)
|
||||
|
||||
@@ -95,7 +95,9 @@ class AbrTweakWidget(QWidget):
|
||||
grid_layout.setColumnStretch(1, 0)
|
||||
grid_layout.setColumnStretch(2, 1)
|
||||
|
||||
grid_layout.addWidget(TitleLabel("ABR meas. pos.", self), 0, 0, 1, 3)
|
||||
# Span all 4 grid columns (the ABR buttons row uses 4), otherwise the
|
||||
# banner renders narrower than the neighboring panels.
|
||||
grid_layout.addWidget(TitleLabel("ABR meas. pos.", self, collapsible=True), 0, 0, 1, 4)
|
||||
|
||||
self._abr_buttons = AbrTweakButtons(DEFAULT_ABR_STEP_UM / 1000, parent=self)
|
||||
grid_layout.addWidget(self._abr_buttons, 1, 0, 1, 4)
|
||||
|
||||
@@ -14,7 +14,7 @@ class BeamCenterWidget(QWidget):
|
||||
|
||||
grid_layout = QGridLayout(self)
|
||||
|
||||
grid_layout.addWidget(TitleLabel("Beam center (detector)", self), 0, 0, 1, 5)
|
||||
grid_layout.addWidget(TitleLabel("Beam center (detector)", self, collapsible=True), 0, 0, 1, 5)
|
||||
|
||||
self.x = NumberLineEdit(-4000, 4000, 0, parent=self)
|
||||
self.x.newValue.connect(self.beam_center_edited)
|
||||
|
||||
@@ -13,7 +13,7 @@ class BeamMarkWidget(QWidget):
|
||||
|
||||
grid_layout = QGridLayout(self)
|
||||
|
||||
grid_layout.addWidget(TitleLabel("Beam mark (image)", self), 0, 0, 1, 5)
|
||||
grid_layout.addWidget(TitleLabel("Beam mark (image)", self, collapsible=True), 0, 0, 1, 5)
|
||||
|
||||
self.x = QLabel("0")
|
||||
self.y = QLabel("0")
|
||||
|
||||
@@ -14,7 +14,7 @@ class BeamSizeWidget(QWidget):
|
||||
|
||||
grid_layout = QGridLayout(self)
|
||||
|
||||
grid_layout.addWidget(TitleLabel("Beam size", self), 0, 0, 1, 5)
|
||||
grid_layout.addWidget(TitleLabel("Beam size", self, collapsible=True), 0, 0, 1, 5)
|
||||
|
||||
self.x = NumberLineEdit(1, 400.0, 10, parent=self)
|
||||
self.x.newValue.connect(self.beam_size_edited)
|
||||
|
||||
@@ -10,6 +10,7 @@ from aare.gui.panels.omega_panel import OmegaPanel
|
||||
from aare.gui.panels.samcam_panel import SamcamPanel
|
||||
from aare.gui.panels.smargon_panel import SmargonPanel
|
||||
from aare.gui.panels.zoom_panel import ZoomPanel
|
||||
from aare.gui.widgets.title_label import tighten_column
|
||||
|
||||
|
||||
class BeamlineControls(QFrame):
|
||||
@@ -51,4 +52,5 @@ class BeamlineControls(QFrame):
|
||||
self.v_layout.addWidget(self.beam_size)
|
||||
|
||||
self.v_layout.addStretch()
|
||||
tighten_column(self.v_layout)
|
||||
self.setLayout(self.v_layout)
|
||||
|
||||
@@ -6,6 +6,8 @@ from PySide6.QtCore import QPoint, QRect, Qt, Signal, Slot
|
||||
from PySide6.QtGui import QColor, QPainter, QPen
|
||||
from PySide6.QtWidgets import QFrame, QLabel, QPushButton
|
||||
|
||||
from aare.gui.widgets.title_label import PANEL_VMARGIN
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StationSpec:
|
||||
@@ -57,8 +59,11 @@ class BeamlineStatePanel(QFrame):
|
||||
|
||||
set_width = 400
|
||||
map_height = 542
|
||||
title_height = 50
|
||||
collapsed_height = 50
|
||||
# 25 matches the halved TitleLabel banners used by every other panel;
|
||||
# PANEL_VMARGIN mimics the layout margin other panels get from
|
||||
# tighten_column, so the inter-banner gap stays universal.
|
||||
title_height = 25
|
||||
collapsed_height = title_height + 2 * PANEL_VMARGIN
|
||||
station_radius = 8
|
||||
|
||||
def __init__(self, parent=None):
|
||||
@@ -209,16 +214,22 @@ class BeamlineStatePanel(QFrame):
|
||||
|
||||
self.title = QLabel(self)
|
||||
self.title.setObjectName("beamlineStateTitle")
|
||||
self.title.setText("<H3>Beamline state</H3>")
|
||||
# Plain text + QSS font: <H3> margins would clip in the 25px banner.
|
||||
self.title.setText("Beamline state")
|
||||
self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.title.setFixedHeight(self.title_height)
|
||||
self.title.setGeometry(0, 0, self.set_width, self.title_height)
|
||||
self.title.setGeometry(0, PANEL_VMARGIN, self.set_width, self.title_height)
|
||||
# Whole banner toggles, like TitleLabel; the +/− glyph is the indicator.
|
||||
self.title.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.title.mousePressEvent = lambda _event: self.toggle_collapsed()
|
||||
|
||||
self.toggle_button = QPushButton("−", self)
|
||||
self.toggle_button.setObjectName("beamlineStateToggleButton")
|
||||
self.toggle_button.setToolTip("Minimise beamline state panel")
|
||||
self.toggle_button.setFixedSize(28, 28)
|
||||
self.toggle_button.move(self.set_width - 36, 11)
|
||||
self.toggle_button.setFixedSize(21, 21)
|
||||
self.toggle_button.move(
|
||||
self.set_width - 29, PANEL_VMARGIN + (self.title_height - 21) // 2
|
||||
)
|
||||
self.toggle_button.clicked.connect(self.toggle_collapsed)
|
||||
|
||||
self.current_label = QLabel("Current: —", self)
|
||||
|
||||
@@ -2,9 +2,11 @@ from aarecommon.math.diffraction_geometry import DiffractionGeometry
|
||||
from aarecommon.math.sample_geometry import SampleGeometryModel
|
||||
from aarecommon.models.models import DAQStatusModel
|
||||
from PySide6.QtCore import Signal, Slot
|
||||
from PySide6.QtWidgets import QFrame, QPushButton, QTabWidget, QVBoxLayout
|
||||
from PySide6.QtWidgets import QFrame, QPushButton, QTabWidget, QVBoxLayout, QWidget
|
||||
|
||||
from aare.gui.panels.file_path_panel import FilePathPanel
|
||||
from aare.gui.panels.manual_sample_panel import ManualSamplePanel
|
||||
from aare.gui.widgets.title_label import TitleLabel, tighten_column
|
||||
from aare.gui.panels.fluorescence_data_collection import FluorescenceDataCollectionPanel
|
||||
from aare.gui.panels.raster_data_collection import RasterDataCollectionPanel
|
||||
from aare.gui.panels.rotation_data_collection import RotationDataCollectionPanel
|
||||
@@ -33,6 +35,11 @@ class DataCollectionSettings(QFrame):
|
||||
self.file_path_panel = FilePathPanel(self)
|
||||
v_layout.addWidget(self.file_path_panel)
|
||||
|
||||
# Between Dataset path and Exp. Config., collapsible like both;
|
||||
# main_window aliases this instead of the former bottom dock.
|
||||
self.manual_sample_panel = ManualSamplePanel(self)
|
||||
v_layout.addWidget(self.manual_sample_panel)
|
||||
|
||||
self._tab_widget = QTabWidget()
|
||||
|
||||
self.raster = RasterDataCollectionPanel(
|
||||
@@ -49,13 +56,23 @@ class DataCollectionSettings(QFrame):
|
||||
self.fluo = FluorescenceDataCollectionPanel(parent=self)
|
||||
self._tab_widget.addTab(self.fluo, "XRF")
|
||||
|
||||
v_layout.addWidget(self._tab_widget)
|
||||
v_layout.addStretch()
|
||||
# Own container: TitleLabel collapse hides its siblings, so without it
|
||||
# "Exp. Config." would also swallow the dataset path and abort button.
|
||||
exp_config = QWidget(self)
|
||||
exp_config_layout = QVBoxLayout(exp_config)
|
||||
exp_config_layout.setContentsMargins(0, 0, 0, 0)
|
||||
exp_config_layout.addWidget(TitleLabel("Exp. Config.", exp_config, collapsible=True))
|
||||
exp_config_layout.addWidget(self._tab_widget)
|
||||
v_layout.addWidget(exp_config)
|
||||
|
||||
abort_button = QPushButton("Abort measurement", parent=self)
|
||||
abort_button.setStyleSheet("color: rgb(164, 0, 0);")
|
||||
abort_button.clicked.connect(self.cancel_button_clicked)
|
||||
v_layout.addWidget(abort_button)
|
||||
# Stretch after the button: abort sits snug under the tabs instead of
|
||||
# being pinned to the bottom of the fixed-height column.
|
||||
v_layout.addStretch()
|
||||
tighten_column(v_layout)
|
||||
|
||||
raster_mgr.update_filename(self.file_path_panel.filename)
|
||||
self.screening.update_filename(self.file_path_panel.filename)
|
||||
|
||||
@@ -23,6 +23,10 @@ class FilePathPanel(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
grid_layout = QGridLayout(self)
|
||||
# No horizontal inset: aligns the banner and fields edge-to-edge with
|
||||
# the zero-margin Exp. Config. section below.
|
||||
_m = grid_layout.contentsMargins()
|
||||
grid_layout.setContentsMargins(0, _m.top(), 0, _m.bottom())
|
||||
self._sample_name = "sample"
|
||||
self._sample_id = -1
|
||||
self._dewar_pos = "None"
|
||||
@@ -36,7 +40,7 @@ class FilePathPanel(QWidget):
|
||||
|
||||
self._formatted_date = datetime.now().strftime("%Y%m%d")
|
||||
|
||||
grid_layout.addWidget(TitleLabel("Dataset path", self), 0, 0, 1, 2)
|
||||
grid_layout.addWidget(TitleLabel("Dataset path", self, collapsible=True), 0, 0, 1, 2)
|
||||
|
||||
grid_layout.addWidget(QLabel("Directory", parent=self), 1, 0)
|
||||
self.directory_edit = QLineEdit("{date}/{puck}/{pos}", parent=self)
|
||||
|
||||
@@ -13,7 +13,7 @@ class IlluminationPanel(QWidget):
|
||||
super().__init__(parent)
|
||||
grid_layout = QGridLayout(self)
|
||||
|
||||
grid_layout.addWidget(TitleLabel("Light", self), 0, 0, 1, 2)
|
||||
grid_layout.addWidget(TitleLabel("Light", self, collapsible=True), 0, 0, 1, 2)
|
||||
|
||||
front_label = QLabel("Front light", parent=self)
|
||||
front_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
@@ -8,7 +8,7 @@ class LoopCenteringPanel(QWidget):
|
||||
super().__init__(parent)
|
||||
grid_layout = QGridLayout(self)
|
||||
|
||||
grid_layout.addWidget(TitleLabel("Loop centering", self), 0, 0, 1, 2)
|
||||
grid_layout.addWidget(TitleLabel("Loop centering", self, collapsible=True), 0, 0, 1, 2)
|
||||
grid_layout.setColumnStretch(0, 1)
|
||||
grid_layout.setColumnStretch(1, 1)
|
||||
|
||||
|
||||
@@ -18,8 +18,14 @@ class ManualSamplePanel(QWidget):
|
||||
self._pgroup = "p16371"
|
||||
|
||||
grid_layout = QGridLayout(self)
|
||||
# No horizontal inset: aligns the banner edge-to-edge with the Dataset
|
||||
# path / Exp. Config. sections around it in the left column.
|
||||
_m = grid_layout.contentsMargins()
|
||||
grid_layout.setContentsMargins(0, _m.top(), 0, _m.bottom())
|
||||
|
||||
grid_layout.addWidget(TitleLabel("Manual sample", self), 0, 0, 1, 3)
|
||||
# Kept as attribute: the Ctrl+M shortcut expands the panel via title.
|
||||
self.title = TitleLabel("Manual sample", self, collapsible=True)
|
||||
grid_layout.addWidget(self.title, 0, 0, 1, 3)
|
||||
|
||||
grid_layout.addWidget(QLabel("Sample"), 1, 0)
|
||||
self._text_name = QLineEdit(self._sample_name)
|
||||
|
||||
@@ -13,7 +13,7 @@ class MonochromatorPanel(QWidget):
|
||||
super().__init__(parent)
|
||||
|
||||
grid_layout = QGridLayout(self)
|
||||
grid_layout.addWidget(TitleLabel("Monochromator", self), 0, 0, 1, 2)
|
||||
grid_layout.addWidget(TitleLabel("Monochromator", self, collapsible=True), 0, 0, 1, 2)
|
||||
|
||||
self.mono_pitch_scan_button = QPushButton("Mono Pitch Scan", parent=self)
|
||||
self.mono_pitch_scan_button.clicked.connect(self.mono_pitch_scan.emit)
|
||||
|
||||
@@ -29,7 +29,7 @@ class OmegaPanel(QWidget):
|
||||
|
||||
grid_layout = QGridLayout(self)
|
||||
|
||||
grid_layout.addWidget(TitleLabel("Omega", self), 0, 0, 1, 2)
|
||||
grid_layout.addWidget(TitleLabel("Omega", self, collapsible=True), 0, 0, 1, 2)
|
||||
grid_layout.setColumnStretch(0, 1)
|
||||
grid_layout.setColumnStretch(1, 1)
|
||||
omega_settings = [
|
||||
|
||||
@@ -35,7 +35,7 @@ class SamcamPanel(QWidget):
|
||||
# Create layout
|
||||
layout = QVBoxLayout()
|
||||
|
||||
layout.addWidget(TitleLabel("Sample camera", self))
|
||||
layout.addWidget(TitleLabel("Sample camera", self, collapsible=True))
|
||||
|
||||
# Exposure control
|
||||
exposure_layout = QHBoxLayout()
|
||||
|
||||
@@ -58,7 +58,7 @@ class SmargonPanel(QWidget):
|
||||
|
||||
grid_layout = QGridLayout(self)
|
||||
|
||||
grid_layout.addWidget(TitleLabel("Smargon", self), 0, 0, 1, 6)
|
||||
grid_layout.addWidget(TitleLabel("Smargon", self, collapsible=True), 0, 0, 1, 6)
|
||||
|
||||
grid_layout.addWidget(QLabel("Chi", parent=self), 1, 0)
|
||||
self.chi_enter = NumberLineEdit(-0.2, 40, decimals=1, parent=self)
|
||||
|
||||
@@ -23,7 +23,7 @@ class ZoomPanel(QWidget):
|
||||
{"name": "7.5x", "value": 800},
|
||||
{"name": "12.5x", "value": 1000},
|
||||
]
|
||||
grid_layout.addWidget(TitleLabel("Zoom", self), 0, 0, 1, 2)
|
||||
grid_layout.addWidget(TitleLabel("Zoom", self, collapsible=True), 0, 0, 1, 2)
|
||||
i = 2
|
||||
|
||||
self._buttons = []
|
||||
|
||||
+14
-18
@@ -186,19 +186,17 @@ def _original_stylesheet() -> str:
|
||||
QLabel#beamlineStateTitle {
|
||||
background-color: #4B0082;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
QPushButton#beamlineStateToggleButton {
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background-color: rgba(255, 255, 255, 0.20);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
QPushButton#beamlineStateToggleButton:hover {
|
||||
background-color: rgba(255, 255, 255, 0.32);
|
||||
/* Bare glyph to match the TitleLabel toggles: no pill background. */
|
||||
QPushButton#beamlineStateToggleButton {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
QLabel#beamlineStateCurrentLabel {
|
||||
@@ -430,19 +428,17 @@ def _portrait_stylesheet() -> str:
|
||||
QLabel#beamlineStateTitle {
|
||||
background-color: #132131;
|
||||
color: #F5F7FA;
|
||||
}
|
||||
|
||||
QPushButton#beamlineStateToggleButton {
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background-color: rgba(255, 255, 255, 0.10);
|
||||
color: #F5F7FA;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
QPushButton#beamlineStateToggleButton:hover {
|
||||
background-color: rgba(255, 255, 255, 0.18);
|
||||
/* Bare glyph to match the TitleLabel toggles: no pill background. */
|
||||
QPushButton#beamlineStateToggleButton {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #F5F7FA;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
QLabel#beamlineStateCurrentLabel {
|
||||
|
||||
@@ -1,12 +1,109 @@
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QLabel
|
||||
from PySide6.QtCore import QSettings, Qt, QTimer
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QLayout, QPushButton
|
||||
|
||||
# Universal vertical rhythm between stacked panels: each panel contributes
|
||||
# PANEL_VMARGIN top and bottom, the column adds PANEL_VSPACING between them,
|
||||
# so every banner-to-banner gap is 4 + 3 + 4 = 11px in every column.
|
||||
PANEL_VSPACING = 3
|
||||
PANEL_VMARGIN = 4
|
||||
|
||||
|
||||
def tighten_column(layout: QLayout) -> None:
|
||||
"""Apply the universal panel gap to a column layout and its child panels.
|
||||
|
||||
Qt's defaults (9px margins + 6px spacing) and ad-hoc per-panel margins
|
||||
made the gaps uneven between the left and right columns.
|
||||
"""
|
||||
layout.setSpacing(PANEL_VSPACING)
|
||||
for i in range(layout.count()):
|
||||
w = layout.itemAt(i).widget()
|
||||
if w is not None and w.layout() is not None:
|
||||
m = w.layout().contentsMargins()
|
||||
w.layout().setContentsMargins(m.left(), PANEL_VMARGIN, m.right(), PANEL_VMARGIN)
|
||||
|
||||
|
||||
class TitleLabel(QLabel):
|
||||
def __init__(self, text: str, parent=None):
|
||||
def __init__(self, text: str, parent=None, collapsible: bool = False):
|
||||
super().__init__(parent)
|
||||
self.setText(f"<H3>{text}</H3>")
|
||||
self.setStyleSheet("background-color: #4B0082; color: #ffffff;")
|
||||
# Plain text + QSS font instead of <H3>: rich-text heading margins
|
||||
# would clip vertically in the halved banner height.
|
||||
self.setText(text)
|
||||
# Scoped selector: an unscoped widget stylesheet propagates to child
|
||||
# widgets and would paint the toggle button solid purple, overriding
|
||||
# the app QSS.
|
||||
self.setStyleSheet(
|
||||
"TitleLabel { background-color: #4B0082; color: #ffffff;"
|
||||
" font-size: 16px; font-weight: 700; }"
|
||||
)
|
||||
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.setFixedHeight(50)
|
||||
# Half the original 50px: the full-height banner wasted vertical space.
|
||||
self.setFixedHeight(25)
|
||||
|
||||
self._collapsible = collapsible
|
||||
if not collapsible:
|
||||
return
|
||||
|
||||
self._collapsed = False
|
||||
# ponytail: settings key is the title text — unique across panels;
|
||||
# renaming a title just resets that panel to expanded once.
|
||||
self._settings_key = f"panel_collapsed/{text}"
|
||||
|
||||
self.toggle_button = QPushButton("−", self)
|
||||
# Bare glyph, no pill: the shared beamlineStateToggleButton QSS paints
|
||||
# a translucent white background, which is unwanted on these banners.
|
||||
self.toggle_button.setStyleSheet(
|
||||
"QPushButton { background: transparent; border: none;"
|
||||
" color: #ffffff; font-size: 14px; font-weight: 700; }"
|
||||
)
|
||||
self.toggle_button.setToolTip("Minimise panel")
|
||||
self.toggle_button.setFixedSize(21, 21)
|
||||
self.toggle_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.toggle_button.clicked.connect(self.toggle_collapsed)
|
||||
|
||||
button_layout = QHBoxLayout(self)
|
||||
button_layout.setContentsMargins(0, 0, 8, 0)
|
||||
button_layout.addStretch()
|
||||
button_layout.addWidget(self.toggle_button)
|
||||
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
|
||||
settings = QSettings("PSI", "AareGUI")
|
||||
if settings.value(self._settings_key, False, type=bool):
|
||||
self._collapsed = True
|
||||
# Deferred: the panel adds its other widgets after constructing
|
||||
# the TitleLabel, so siblings don't exist yet.
|
||||
QTimer.singleShot(0, self._apply_collapsed)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if self._collapsible:
|
||||
self.toggle_collapsed()
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def expand(self) -> None:
|
||||
if self._collapsible and self._collapsed:
|
||||
self.toggle_collapsed()
|
||||
|
||||
def toggle_collapsed(self) -> None:
|
||||
self._collapsed = not self._collapsed
|
||||
self._apply_collapsed()
|
||||
QSettings("PSI", "AareGUI").setValue(self._settings_key, self._collapsed)
|
||||
|
||||
def _apply_collapsed(self) -> None:
|
||||
parent = self.parentWidget()
|
||||
if parent is None or parent.layout() is None:
|
||||
return
|
||||
self._set_visible(parent.layout(), not self._collapsed)
|
||||
self.toggle_button.setText("+" if self._collapsed else "−")
|
||||
self.toggle_button.setToolTip("Restore panel" if self._collapsed else "Minimise panel")
|
||||
|
||||
def _set_visible(self, layout: QLayout, visible: bool) -> None:
|
||||
# Recursive: panels like SamcamPanel nest sub-layouts via addLayout.
|
||||
for i in range(layout.count()):
|
||||
item = layout.itemAt(i)
|
||||
widget = item.widget()
|
||||
if widget is not None:
|
||||
if widget is not self:
|
||||
widget.setVisible(visible)
|
||||
elif item.layout() is not None:
|
||||
self._set_visible(item.layout(), visible)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
from PySide6.QtCore import QSettings
|
||||
from PySide6.QtWidgets import QGridLayout, QHBoxLayout, QPushButton, QWidget
|
||||
|
||||
from aare.gui.widgets.title_label import TitleLabel
|
||||
|
||||
# Unique title so the test never clashes with real panel settings.
|
||||
TITLE = "TitleLabelTestPanel"
|
||||
KEY = f"panel_collapsed/{TITLE}"
|
||||
|
||||
|
||||
def _remove_key():
|
||||
QSettings("PSI", "AareGUI").remove(KEY)
|
||||
|
||||
|
||||
def _build_panel(qtbot):
|
||||
panel = QWidget()
|
||||
qtbot.addWidget(panel)
|
||||
grid = QGridLayout(panel)
|
||||
title = TitleLabel(TITLE, panel, collapsible=True)
|
||||
grid.addWidget(title, 0, 0, 1, 2)
|
||||
direct_child = QPushButton("direct", panel)
|
||||
grid.addWidget(direct_child, 1, 0)
|
||||
nested = QHBoxLayout()
|
||||
nested_child = QPushButton("nested", panel)
|
||||
nested.addWidget(nested_child)
|
||||
grid.addLayout(nested, 1, 1)
|
||||
return panel, title, direct_child, nested_child
|
||||
|
||||
|
||||
def test_toggle_hides_children_and_persists(qtbot):
|
||||
_remove_key()
|
||||
try:
|
||||
panel, title, direct_child, nested_child = _build_panel(qtbot)
|
||||
expanded_height = panel.sizeHint().height()
|
||||
|
||||
title.toggle_collapsed()
|
||||
assert direct_child.isHidden()
|
||||
assert nested_child.isHidden()
|
||||
assert not title.isHidden()
|
||||
assert panel.sizeHint().height() < expanded_height
|
||||
assert title.toggle_button.text() == "+"
|
||||
assert QSettings("PSI", "AareGUI").value(KEY, False, type=bool) is True
|
||||
|
||||
title.toggle_collapsed()
|
||||
assert not direct_child.isHidden()
|
||||
assert not nested_child.isHidden()
|
||||
assert title.toggle_button.text() == "−"
|
||||
assert QSettings("PSI", "AareGUI").value(KEY, False, type=bool) is False
|
||||
finally:
|
||||
_remove_key()
|
||||
|
||||
|
||||
def test_collapsed_state_restored_on_construction(qtbot):
|
||||
QSettings("PSI", "AareGUI").setValue(KEY, True)
|
||||
try:
|
||||
panel, title, direct_child, nested_child = _build_panel(qtbot)
|
||||
# Restore is deferred with a 0 ms timer (siblings don't exist yet at
|
||||
# TitleLabel construction), so let the event loop run once.
|
||||
qtbot.waitUntil(lambda: direct_child.isHidden(), timeout=1000)
|
||||
assert nested_child.isHidden()
|
||||
assert title.toggle_button.text() == "+"
|
||||
finally:
|
||||
_remove_key()
|
||||
|
||||
|
||||
def test_not_collapsible_by_default(qtbot):
|
||||
panel = QWidget()
|
||||
qtbot.addWidget(panel)
|
||||
grid = QGridLayout(panel)
|
||||
title = TitleLabel("Plain", panel)
|
||||
grid.addWidget(title, 0, 0)
|
||||
assert not hasattr(title, "toggle_button")
|
||||
Reference in New Issue
Block a user