Dewar "#" column sat 1px above the other rows at some zoom steps: the frozen-column overlay sizes its own header from "#" alone while the main header also sees the symbol columns, whose fallback-font glyphs have a taller line box. The overlay header is now pinned to the main header height on resize and (deferred, receiver-bound) on font/style change. Zoom also left columns at their startup widths (headers truncated) and clipped descenders in every button/combo/entry box: the 16px height pin in both sheets was raw px while the body font grew to 18/21px. Columns re-autosize on FontChange and the pin scales with the font ladder. Camera "Currently mounted" HUD: black ink on a white halo in the light themes, white on black in Sunset, instead of white-on-black everywhere. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
187 lines
6.4 KiB
Python
187 lines
6.4 KiB
Python
"""The combined sample dock: chip row filters the table by status, chips
|
|
double as drop targets for queue/flag relabeling, and a pop-out panel shares
|
|
the docked panel's model so both stay in sync without wiring."""
|
|
|
|
import pytest
|
|
from aarecommon.models.models import DewarAddress, SampleShortInfo, SampleShortInfoList
|
|
|
|
from aare.gui.panels.tell_sample_panel import TellSamplePanel
|
|
|
|
|
|
@pytest.fixture
|
|
def samples():
|
|
return SampleShortInfoList(
|
|
s=[
|
|
SampleShortInfo(
|
|
db_id=i,
|
|
puck_name=f"P{i}",
|
|
dewar_name="D1",
|
|
sample_name=f"S{i}",
|
|
run_number=i,
|
|
user="U1",
|
|
pin=i,
|
|
location=DewarAddress(segment="A", pos=i),
|
|
)
|
|
for i in (1, 2, 3)
|
|
]
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def panel(qtbot, samples):
|
|
panel = TellSamplePanel(samples=samples)
|
|
qtbot.addWidget(panel)
|
|
panel.table_model.set_show_all_pgroups(True)
|
|
return panel
|
|
|
|
|
|
def _chip(panel, key):
|
|
return next(c for c in panel.status_chips.buttons() if c.property("status_key") == key)
|
|
|
|
|
|
def test_chip_click_drives_the_status_filter(panel):
|
|
panel.table_model.set_queued_ids({2})
|
|
_chip(panel, "queued").click()
|
|
assert panel.table_model.status_filter == "queued"
|
|
assert panel.table_model.rowCount() == 1
|
|
_chip(panel, None).click()
|
|
assert panel.table_model.status_filter is None
|
|
assert panel.table_model.rowCount() == 3
|
|
|
|
|
|
def test_set_status_chip_syncs_without_filtering(panel):
|
|
panel.set_status_chip("flagged")
|
|
assert _chip(panel, "flagged").isChecked()
|
|
# Sync only checks the chip; it must not fire the filter.
|
|
assert panel.table_model.status_filter is None
|
|
|
|
|
|
def test_queued_chip_drop_relays_to_the_queue(panel, qtbot, samples):
|
|
chip = _chip(panel, "queued")
|
|
with qtbot.waitSignal(panel.add_to_queue) as blocker:
|
|
chip.samples_dropped.emit(samples)
|
|
assert [s.db_id for s in blocker.args[0].s] == [1, 2, 3]
|
|
|
|
|
|
def test_flagged_chip_drop_flags_in_the_model(panel, samples):
|
|
panel._flag_dropped_samples(SampleShortInfoList(s=samples.s[:2]))
|
|
assert panel.table_model.flagged_ids == {1, 2}
|
|
|
|
|
|
def test_popout_panel_shares_the_model(panel, qtbot):
|
|
popout = TellSamplePanel(model=panel.table_model)
|
|
qtbot.addWidget(popout)
|
|
assert popout.table_model is panel.table_model
|
|
panel.table_model.set_flagged(3, True)
|
|
assert 3 in popout.table_model.flagged_ids
|
|
|
|
|
|
def test_new_sample_list_updates_rows(panel, samples):
|
|
extra = samples.s + [
|
|
SampleShortInfo(
|
|
db_id=9,
|
|
puck_name="P9",
|
|
dewar_name="D2",
|
|
sample_name="S9",
|
|
run_number=9,
|
|
user="U2",
|
|
pin=9,
|
|
location=DewarAddress(segment="B", pos=1),
|
|
)
|
|
]
|
|
panel.new_sample_list(SampleShortInfoList(s=extra))
|
|
assert panel.table_model.rowCount() == 4
|
|
|
|
|
|
def test_queue_drop_chip_accepts_sample_payloads(panel, qtbot, samples):
|
|
from PySide6.QtCore import QMimeData, QPointF, Qt
|
|
from PySide6.QtGui import QDropEvent
|
|
|
|
from aare.gui.panels.tell_sample_panel import QueueDropChip
|
|
|
|
chip = next(c for c in panel.status_chips.buttons() if isinstance(c, QueueDropChip))
|
|
|
|
# The event only borrows the QMimeData (C++ pointer), so the mime must
|
|
# outlive the dropEvent call — hence created in the test's scope.
|
|
def drop(mime):
|
|
return QDropEvent(
|
|
QPointF(1, 1),
|
|
Qt.DropAction.CopyAction,
|
|
mime,
|
|
Qt.MouseButton.NoButton,
|
|
Qt.KeyboardModifier.NoModifier,
|
|
)
|
|
|
|
good = QMimeData()
|
|
good.setText(samples.model_dump_json())
|
|
with qtbot.waitSignal(chip.samples_dropped):
|
|
chip.dropEvent(drop(good))
|
|
|
|
# A non-sample payload is ignored, not crashed on.
|
|
bad = QMimeData()
|
|
bad.setText("not json")
|
|
with qtbot.assertNotEmitted(chip.samples_dropped):
|
|
chip.dropEvent(drop(bad))
|
|
|
|
|
|
def test_status_header_click_selects_all(panel):
|
|
header = panel.table_view.frozen.horizontalHeader()
|
|
header.sectionClicked.emit(0)
|
|
assert len(panel.table_view.selectionModel().selectedRows()) == 3
|
|
# Other sections keep their normal sort-click behavior.
|
|
panel.table_view.clearSelection()
|
|
header.sectionClicked.emit(1)
|
|
assert len(panel.table_view.selectionModel().selectedRows()) == 0
|
|
|
|
|
|
def test_selected_samples_follow_the_click(panel):
|
|
view = panel.table_view
|
|
view.selectRow(0)
|
|
row_ids = [panel.table_model.get_id(r).db_id for r in range(3)]
|
|
# Click inside the selection: the selection is acted on.
|
|
assert [s.db_id for s in panel._selected_samples(0)] == [row_ids[0]]
|
|
# Click outside the selection: only the clicked row is acted on.
|
|
assert [s.db_id for s in panel._selected_samples(2)] == [row_ids[2]]
|
|
|
|
|
|
def test_font_zoom_keeps_frozen_column_aligned_and_reautosizes(panel, qtbot):
|
|
"""Ctrl+plus zoom: the frozen "#" overlay header must stay exactly as
|
|
tall as the main header (else its rows sit 1px higher), and the columns
|
|
sized once at startup must re-autosize for the wider glyphs."""
|
|
from PySide6.QtGui import QFont
|
|
from PySide6.QtWidgets import QApplication
|
|
|
|
from aare.gui import styles
|
|
|
|
app = QApplication.instance()
|
|
assert isinstance(app, QApplication) # font() lives on QApplication, not QCoreApplication
|
|
base = QFont(app.font())
|
|
# Startup look first: under a styled ancestor (MainWindow) fonts reach
|
|
# the view through the QSS path; without any sheet Qt would not
|
|
# propagate the app font to a child that carries WA_StyleSheet.
|
|
panel.setStyleSheet(styles.build_app_stylesheet(styles.THEME_SUNRISE))
|
|
panel.resize(600, 300)
|
|
panel.show()
|
|
qtbot.waitExposed(panel)
|
|
view = panel.table_view
|
|
width_before = view.columnWidth(1)
|
|
|
|
def row0_top(v):
|
|
return v.viewport().mapTo(panel, v.viewport().rect().topLeft()).y() + v.rowViewportPosition(
|
|
0
|
|
)
|
|
|
|
try:
|
|
# Same three steps as MainWindow._apply_theme on a zoom change.
|
|
styles.set_font_scale(1.25)
|
|
big = QFont(base)
|
|
big.setPointSizeF(base.pointSizeF() * 1.25)
|
|
app.setFont(big)
|
|
panel.setStyleSheet(styles.build_app_stylesheet(styles.THEME_SUNRISE))
|
|
qtbot.waitUntil(lambda: view.columnWidth(1) > width_before)
|
|
assert view.frozen.horizontalHeader().height() == view.horizontalHeader().height()
|
|
assert row0_top(view.frozen) == row0_top(view)
|
|
finally:
|
|
styles.set_font_scale(1.0)
|
|
app.setFont(base)
|