diff --git a/apps/autocal/config/locales/en_US.json b/apps/autocal/config/locales/en_US.json index 81bc04c..cb4827e 100644 --- a/apps/autocal/config/locales/en_US.json +++ b/apps/autocal/config/locales/en_US.json @@ -225,12 +225,9 @@ "tab_sequence": "Test sequence", "tab_finger": "Finger debug", "tab_sensor": "Calibration capture", - "tab_surface": "Surface map", - "tab_points": "Calibration points", + "tab_records": "Calibration records", "not_built": "Not built yet", - "reserved_sensor": "Sensor connection, live image and force curves will appear here: device and source selection, preview, and a compact version of the Studio curve view.", - "reserved_surface": "Multi-point calibration sensitivity will be drawn as a colour map on the sensor's 3D surface here: each point's sensitivity interpolated and coloured, with a legend and range.", - "reserved_points": "The calibration point table and capture controls will appear here: platform coordinates, reference load, sampling statistics and the resulting sensitivity." + "reserved_sensor": "Sensor connection, live image and force curves will appear here: device and source selection, preview, and a compact version of the Studio curve view." }, "source": { "kind": "Input source", @@ -491,6 +488,8 @@ "flow_missing": "No valid displacement for this frame" }, "overview": { + "open_session": "Open calibration session…", + "empty": "Choose the session folder containing session.json to view collected sites, normal response and sensitivity. You can also open current data from Calibration capture.", "title": "Calibration record overview", "notice": "Inspect acquired sites and normal response; click UV points or table rows. Circles: confirmed windows; squares: candidates. Trial shades differ. The fit uses all eligible loading data for the site and is not refitted by the trial filter.", "coverage": "Acquisition status", diff --git a/apps/autocal/config/locales/zh_CN.json b/apps/autocal/config/locales/zh_CN.json index 702a3d2..88ab201 100644 --- a/apps/autocal/config/locales/zh_CN.json +++ b/apps/autocal/config/locales/zh_CN.json @@ -225,12 +225,9 @@ "tab_sequence": "测试序列", "tab_finger": "手指调试", "tab_sensor": "标定采集", - "tab_surface": "表面分布", - "tab_points": "标定点", + "tab_records": "标定记录", "not_built": "尚未实现", - "reserved_sensor": "传感器连接、实时图像与力曲线将在此显示:设备与信号源选择、预览、以及和 Studio 类似的曲线视图(占用空间更小)。", - "reserved_surface": "多点标定的灵敏度将在此以色图叠加在传感器三维表面上:每个标定点的灵敏度经插值着色,并给出色标与范围。", - "reserved_points": "标定点列表与采集控制将在此显示:平台坐标、参考载荷、采样统计与灵敏度结果。" + "reserved_sensor": "传感器连接、实时图像与力曲线将在此显示:设备与信号源选择、预览、以及和 Studio 类似的曲线视图(占用空间更小)。" }, "source": { "kind": "输入来源", @@ -491,6 +488,8 @@ "flow_missing": "本帧无有效位移信号" }, "overview": { + "open_session": "打开标定会话…", + "empty": "请选择包含 session.json 的会话文件夹,查看已采集测点、法向响应和灵敏度;也可从标定采集页打开本场数据。", "title": "标定记录总览", "notice": "查看实际采集覆盖与法向响应;点击 UV 点或表格联动。圆点为已确认窗口,方点为候选窗口;各 trial 用不同明暗度。拟合线为本测点全部符合条件的加载数据,筛选 trial 不会重新拟合。", "coverage": "采集状态", diff --git a/apps/autocal/docs/calibration-data-format.zh.md b/apps/autocal/docs/calibration-data-format.zh.md index 63cef46..6d2cbc8 100644 --- a/apps/autocal/docs/calibration-data-format.zh.md +++ b/apps/autocal/docs/calibration-data-format.zh.md @@ -108,7 +108,7 @@ finger_F001_<时间>_<标识>/ 主时间是电脑取得该帧时绑定的单调时间,UTC 用于追溯;处理完成时间不能代替采集时间。FT/ECAT 后续接入同源接收时间,按时间关联,不按行号配对;低延迟不等于零延迟。旧视频的 `host_replay` 是本次解码时间,只能调试,不能恢复原实验与参考设备的物理同步。 -点击“查看标定数据…”选会话目录,进入记录总览;点击 UV 点或表格,联动法向响应和 trial 筛选,再“查看信号/复核窗口…”。 +“标定记录”页打开会话目录;采集页“查看标定数据…”也跳转至此。点击 UV 点或表格,联动响应及 trial 筛选,再“查看信号/复核窗口…”。 | 总览 | 规则 | |---|---| diff --git a/apps/autocal/src/orisys_autocal/ui/calibration_panel.py b/apps/autocal/src/orisys_autocal/ui/calibration_panel.py index acdb6f1..f6116ef 100644 --- a/apps/autocal/src/orisys_autocal/ui/calibration_panel.py +++ b/apps/autocal/src/orisys_autocal/ui/calibration_panel.py @@ -36,6 +36,7 @@ class CalibrationPanel(QWidget): """One operator page. No motion commands are issued by this widget.""" loadBundleRequested = Signal() + reviewRequested = Signal(str) def __init__(self, source_panel: QWidget, parent: QWidget | None = None) -> None: super().__init__(parent) @@ -351,19 +352,13 @@ class CalibrationPanel(QWidget): self._sync() def _review(self) -> None: - from .record_overview_dialog import RecordOverviewDialog - session = self.trial.session directory = QFileDialog.getExistingDirectory( self, t("review.open"), str(session.directory) if session is not None else self.output_path.text(), ) if directory: - try: - dialog = RecordOverviewDialog(Path(directory), self) - dialog.exec() - except (OSError, ValueError, KeyError, TypeError) as exc: - self.result_label.setText(t("calibration.error").format(detail=exc)) + self.reviewRequested.emit(directory) def shutdown(self) -> None: self._timer.stop() diff --git a/apps/autocal/src/orisys_autocal/ui/record_overview_dialog.py b/apps/autocal/src/orisys_autocal/ui/record_overview_dialog.py index df39989..9e099f4 100644 --- a/apps/autocal/src/orisys_autocal/ui/record_overview_dialog.py +++ b/apps/autocal/src/orisys_autocal/ui/record_overview_dialog.py @@ -8,7 +8,7 @@ from PySide6.QtCore import Qt from PySide6.QtGui import QColor, QPainter from PySide6.QtWidgets import ( QComboBox, QDialog, QFileDialog, QHBoxLayout, QHeaderView, QLabel, - QPushButton, QTableWidget, QTableWidgetItem, QVBoxLayout, + QPushButton, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget, ) from ..record_overview import load_overview, overview_rows, read_plan, save_overview @@ -16,7 +16,63 @@ from ..session_review import review_session from .session_review_dialog import SessionReviewDialog +class RecordOverviewPage(QWidget): + """Keep the existing record viewer inside the main task workspace.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.overview = None + self.directory = Path.cwd() + self.content_layout = QVBoxLayout(self) + self.open_button = QPushButton(self) + self.open_button.setObjectName("openCalibrationRecords") + self.open_button.clicked.connect(self._open) + self.path_label = QLabel(self) + self.path_label.setWordWrap(True) + self.message = QLabel(self) + self.message.setWordWrap(True) + self.content_layout.addWidget(self.open_button) + self.content_layout.addWidget(self.path_label) + self.content_layout.addWidget(self.message) + self.content_layout.addStretch(1) + self.retranslate() + + def retranslate(self): + self.open_button.setText(t("overview.open_session")) + if self.overview is None: + self.path_label.setText(t("overview.empty")) + + def _open(self): + path = QFileDialog.getExistingDirectory(self, t("overview.open_session"), str(self.directory)) + if path: + self.load_session(Path(path)) + + def load_session(self, directory: Path): + try: + overview = RecordOverviewDialog(directory, self) + except (OSError, ValueError, KeyError, TypeError) as exc: + self.message.setText(t("calibration.error").format(detail=exc)) + return + overview.setWindowFlags(Qt.WindowType.Widget) + if self.overview is not None: + self.content_layout.removeWidget(self.overview) + self.overview.hide() + self.overview.deleteLater() + else: + self.content_layout.takeAt(self.content_layout.count() - 1) + self.overview = overview + self.directory = directory + self.path_label.setText(str(directory)) + self.message.clear() + self.content_layout.addWidget(overview, 1) + overview.show() + + class RecordOverviewDialog(QDialog): + def reject(self): + if self.isWindow(): + super().reject() + def __init__(self, directory: Path, parent=None): super().__init__(parent) self.directory = directory diff --git a/apps/autocal/src/orisys_autocal/ui/window.py b/apps/autocal/src/orisys_autocal/ui/window.py index 2de2855..28a88e3 100644 --- a/apps/autocal/src/orisys_autocal/ui/window.py +++ b/apps/autocal/src/orisys_autocal/ui/window.py @@ -305,6 +305,7 @@ class AutoCalMainWindow(QMainWindow): body, sensor_panel=self.calibration_panel, ) + self.calibration_panel.reviewRequested.connect(self._on_review_records) self.main_splitter = QSplitter(Qt.Orientation.Horizontal, body) self.main_splitter.addWidget(self.left_rail) @@ -325,6 +326,10 @@ class AutoCalMainWindow(QMainWindow): self._wire_signals() + def _on_review_records(self, directory: str) -> None: + self.workspace.page("records").load_session(Path(directory)) + self.workspace.set_current_key("records") + def _wire_signals(self) -> None: self.connection_panel.connectRequested.connect(self._on_connect) self.connection_panel.disconnectRequested.connect(self._on_disconnect) diff --git a/apps/autocal/src/orisys_autocal/ui/workspace.py b/apps/autocal/src/orisys_autocal/ui/workspace.py index 9d03800..20af95e 100644 --- a/apps/autocal/src/orisys_autocal/ui/workspace.py +++ b/apps/autocal/src/orisys_autocal/ui/workspace.py @@ -4,10 +4,7 @@ One tab per task the operator does on the platform, with the motion status strip living *outside* this widget so a tab change cannot hide the state that decides whether it is safe to act. -Pages that are not built yet are present as labelled placeholders rather than -missing: the layout is being reviewed for operator use, and a tab that appears -only later would change the layout under review. Each placeholder names what -will fill it, so nobody mistakes an empty page for a broken one. +Calibration records share one page for coverage, sensitivity and window review. """ from __future__ import annotations @@ -23,25 +20,23 @@ from PySide6.QtWidgets import ( from .axis_panel import AxisPanel from .sequence_panel import SequencePanel +from .record_overview_dialog import RecordOverviewPage #: Tab identity -> locale key for its label. Order is the display order, and it #: follows the work: commission the axes, describe the sequence, watch the -#: sensor, look at the surface, then record calibration points. +#: sensor, capture data, then review calibration records. TABS: tuple[tuple[str, str], ...] = ( ("axes", "workspace.tab_axes"), ("sequence", "workspace.tab_sequence"), ("finger", "workspace.tab_finger"), ("sensor", "workspace.tab_sensor"), - ("surface", "workspace.tab_surface"), - ("points", "workspace.tab_points"), + ("records", "workspace.tab_records"), ) #: Tabs whose content does not exist yet, with the locale key that explains what #: will appear there and when. _PLACEHOLDERS: dict[str, str] = { "sensor": "workspace.reserved_sensor", - "surface": "workspace.reserved_surface", - "points": "workspace.reserved_points", } @@ -93,6 +88,7 @@ class WorkspaceTabs(QTabWidget): self._pages["finger"] = finger_panel if sensor_panel is not None: self._pages["sensor"] = sensor_panel + self._pages["records"] = RecordOverviewPage(self) for key, locale_key in _PLACEHOLDERS.items(): if key in self._pages: continue @@ -134,6 +130,7 @@ class WorkspaceTabs(QTabWidget): page = self._pages[key] if isinstance(page, PlaceholderPage): page.retranslate() + self._pages["records"].retranslate() def _on_current_changed(self, index: int) -> None: keys = self.page_keys() diff --git a/apps/autocal/tests/test_record_overview.py b/apps/autocal/tests/test_record_overview.py index dd4458d..1e4f4ca 100644 --- a/apps/autocal/tests/test_record_overview.py +++ b/apps/autocal/tests/test_record_overview.py @@ -159,15 +159,43 @@ def test_overview_gui_plan_click_trial_filter_and_saved_roundtrip(qt_app, overvi dialog.close() -def test_calibration_review_action_opens_record_overview(make_window, overview_session, monkeypatch): - from orisys_autocal.ui.record_overview_dialog import RecordOverviewDialog +def test_calibration_review_action_opens_records_tab(make_window, overview_session, monkeypatch): from orisys_autocal.ui.calibration_panel import QFileDialog root, _, _ = overview_session - opened = [] monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args: str(root)) - monkeypatch.setattr(RecordOverviewDialog, "exec", lambda dialog: opened.append(dialog)) window = make_window() + assert window.workspace.page_keys() == ["axes", "sequence", "finger", "sensor", "records"] window.calibration_panel.review_button.click() - assert len(opened) == 1 and len(opened[0].rows) == 3 + assert window.workspace.current_key() == "records" + page = window.workspace.page("records") + assert len(page.overview.rows) == 3 + assert not page.overview.isWindow() + window.close() + + +def test_records_tab_browse_error_and_reload(make_window, overview_session, monkeypatch, qt_app, tmp_path): + from PySide6.QtCore import Qt + from PySide6.QtTest import QTest + from orisys_autocal.ui.record_overview_dialog import QFileDialog + + root, _, _ = overview_session + window = make_window() + window.resize(1600, 1000) + window.workspace.set_current_key("records") + page = window.workspace.page("records") + assert page.overview is None and page.path_label.text() + monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args: str(root)) + page.open_button.click() + first = page.overview + page.load_session(root / "not-a-session") + assert page.overview is first and page.message.text() + page.open_button.click() + assert page.overview is not first and not page.message.text() + window.show() + qt_app.processEvents() + assert page.overview.isVisible() + QTest.keyClick(page.overview, Qt.Key.Key_Escape) + assert page.overview.isVisible() + assert window.grab().save(str(tmp_path / "records-tab.png")) window.close()