拆分 C2:显示偏好移入 DisplayPrefsController(app_window 2347→2340 行 / 净减 110 行代码)
- What: 新增 apps/studio/src/orisys_studio/display_prefs.py:把「同一个测量值可以怎么 显示」搬进 DisplayPrefsController——单位(newton / raw,含工具栏、曲线图、重算引擎、 数值卡四处同步)、量程与时间窗(含默认值、应用到面板、供对话框读取)、箭头显示 (按传感器决定旋转是否可用、图例倍率、旋转 90°)、2D 视图模式切换后的重绘、以及 rail 上的帧率读数开关。控制器自己拥有 show_fps;其余偏好仍写在 preferences 里。 `DisplayHost` Protocol 写明它向窗口借的五件事:_persist_prefs / _refresh_status_rail / _latest_fps(算滤波设计用的采样率)/ _drop_render_marker + _repaint_live_frame (2D 视图模式变了要重建那一帧)。 _on_plot_settings *不搬*:它要开 QDialog、还要把滤波系数交给采集 worker,那是会话与 窗口的事,不是显示偏好;它现在只向控制器取对话框要显示的值、把结果交回去应用。 - What: 窗口侧保留过渡 shim(_set_display_unit / _on_units_changed / _on_chart_unit_toggle / _on_image_view_mode_changed / _on_show_fps_toggled / _apply_plot_prefs_to_panel / _device_sample_rate / _apply_arrow_rotation_for_sensor / _apply_arrow_legend / _draw_style / _on_rotate_arrows,以及 _show_fps 可读写属性),供 tests 与 tools/ui_check.py 继续驱动窗口(其中 window._on_plot_settings 仍可被测试整体替换)。 窗口内部连线(toolbar.units_changed、arrow_panel.view_mode_changed、 force_plots.unit_toggle_requested、act_show_fps.toggled、act_rotate_arrows.triggered) 改为直连控制器;_sync_menu_checks 里的箭头部分也走控制器。 - What: 新增 apps/studio/tests/test_display_prefs.py:11 个用例——单位四处同步、 工具栏改单位时不回写工具栏(防回声)、图表单位芯片翻转、对话框值取默认值、 应用量程写偏好与面板、采样率优先用实测值再回落存储值与默认值、不可旋转的传感器 不提供旋转且不写偏好、可旋转的重启后记住角度、图例倍率随 draw style 变化、 切换 2D 模式只重绘一次、帧率开关同时更新偏好与 rail。 - Why: 「数字怎么显示」是 app_window 的第四个独立变更驱动:改一个显示偏好不该碰会话、 回放或布局。搬完之后这条规则的唯一所有者在 display_prefs 里,且可以脱离窗口单测。 - Verification: pytest shared/tests apps/studio/tests(offscreen)632 passed / 0 failed / 15 skipped(原 621 + 新增 11);apps/studio/tools/ui_check.py 587 项检查 0 失败; .repro/locale_check.py 0 缺失。行为等价(搬运 + 一层转发)。 - Not in this change: 标定交互(_on_calibration_apply / 身份与不匹配检查 / 导出与事件) 仍未拆分;预览(_start_preview/_stop_preview/_on_preview_tick)留在窗口; _depth_range/_depth_scale_text 与 _show_image_frame 同属 2D 渲染,留在窗口; docs 的行数/方法数指标待全部切片落定后统一刷新。
This commit is contained in:
@@ -46,6 +46,7 @@ from orisys_app_shared.record.sample import to_record_sample
|
||||
from orisys_studio.recording import RecordingCoordinator, record_config_from_prefs
|
||||
from orisys_studio.replay import ReplayEngine
|
||||
from orisys_studio.replay_controller import ReplayController
|
||||
from orisys_studio.display_prefs import DisplayPrefsController
|
||||
from orisys_studio.view_layout import ViewLayoutController
|
||||
from orisys_studio import session_metadata
|
||||
from orisys_studio.session_controller import SessionController
|
||||
@@ -154,9 +155,9 @@ class StudioMainWindow(QMainWindow):
|
||||
self.prefs: dict[str, Any] = dict(prefs or {})
|
||||
self._theme = str(self.prefs.get("theme") or "dark")
|
||||
self._language = str(self.prefs.get("language") or "zh_CN")
|
||||
# The view preset, the calibration panel and the 3D pane's switch are the layout
|
||||
# controller's state; the window reads and writes them through the shims below.
|
||||
self._show_fps = bool(self.prefs.get("show_fps", False))
|
||||
# The view preset, the calibration panel, the 3D pane's switch and the fps
|
||||
# readout are the controllers' state; the window reads and writes them through
|
||||
# the shims below.
|
||||
# Whether a recompute pass is running is the replay controller's state; the
|
||||
# window reads and writes it through the ``_recompute_running`` shim below.
|
||||
self._preview = None
|
||||
@@ -266,6 +267,27 @@ class StudioMainWindow(QMainWindow):
|
||||
parent=self,
|
||||
)
|
||||
self.replay_ctl.wire()
|
||||
# How the numbers are shown (units, chart range, arrows, the fps readout) is a
|
||||
# preference of its own: the dialog that changes the chart settings stays here,
|
||||
# the values and what they do to the panels live there.
|
||||
self.display_ctl = DisplayPrefsController(
|
||||
host=self,
|
||||
toolbar=self.toolbar,
|
||||
charts=self.force_plots,
|
||||
arrow_panel=self.arrow_panel,
|
||||
engine=self.replay,
|
||||
sensor_picker=self.sensor_picker,
|
||||
plot_defaults={
|
||||
"window_sec": DEFAULT_PLOT_WINDOW_SEC,
|
||||
"fn_max": DEFAULT_PLOT_FN_MAX,
|
||||
"shear_max": DEFAULT_PLOT_SHEAR_MAX,
|
||||
"fn_raw_max": DEFAULT_PLOT_FN_RAW_MAX,
|
||||
"shear_raw_max": DEFAULT_PLOT_SHEAR_RAW_MAX,
|
||||
},
|
||||
default_sample_rate_hz=float(FORCE_SAMPLE_RATE_HZ),
|
||||
arrow_rotate_types=ARROW_ROTATE_TYPES,
|
||||
parent=self,
|
||||
)
|
||||
# Which panels are on screen, in what arrangement, in which view: also a
|
||||
# controller, so the window owns the widgets rather than the furniture's state.
|
||||
# Its initial values come from the preferences, so nothing is duplicated here.
|
||||
@@ -716,16 +738,20 @@ class StudioMainWindow(QMainWindow):
|
||||
self.toolbar.run_toggled.connect(self._on_run_toggled)
|
||||
self.toolbar.record_toggled.connect(self._on_record_toggled)
|
||||
self.toolbar.reset_clicked.connect(self._on_reset)
|
||||
self.toolbar.units_changed.connect(self._on_units_changed)
|
||||
self.toolbar.units_changed.connect(self.display_ctl.on_units_changed)
|
||||
self.toolbar.settings_toggled.connect(self.layout_ctl.on_settings_toggled)
|
||||
|
||||
def _wire_sensor_picker(self) -> None:
|
||||
self.sensor_picker.sensor_type_changed.connect(self._on_sensor_type_changed)
|
||||
self.sensor_picker.config_changed.connect(self._on_config_changed)
|
||||
self.arrow_panel.view_mode_changed.connect(self._on_image_view_mode_changed)
|
||||
self.arrow_panel.view_mode_changed.connect(
|
||||
self.display_ctl.on_image_view_mode_changed
|
||||
)
|
||||
self.arrow_panel.view_mode_changed.connect(lambda _mode: self.replay_ctl.render_frame())
|
||||
# The unit chip on the chart header switches the same unit everywhere.
|
||||
self.force_plots.unit_toggle_requested.connect(self._on_chart_unit_toggle)
|
||||
self.force_plots.unit_toggle_requested.connect(
|
||||
self.display_ctl.on_chart_unit_toggle
|
||||
)
|
||||
# The gear on the chart header opens the same dialog as 视图 ▸ 曲线图设置.
|
||||
self.force_plots.plot_settings_requested.connect(self._on_plot_settings)
|
||||
|
||||
@@ -875,7 +901,7 @@ class StudioMainWindow(QMainWindow):
|
||||
self.act_show_fps = QAction(self)
|
||||
self.act_show_fps.setCheckable(True)
|
||||
self.act_show_fps.setChecked(self._show_fps)
|
||||
self.act_show_fps.toggled.connect(self._on_show_fps_toggled)
|
||||
self.act_show_fps.toggled.connect(self.display_ctl.on_show_fps_toggled)
|
||||
self.view_menu.addAction(self.act_show_fps)
|
||||
|
||||
self.act_settings = QAction(self)
|
||||
@@ -888,7 +914,7 @@ class StudioMainWindow(QMainWindow):
|
||||
self.view_menu.addAction(self.act_plot_settings)
|
||||
|
||||
self.act_rotate_arrows = QAction(self)
|
||||
self.act_rotate_arrows.triggered.connect(self._on_rotate_arrows)
|
||||
self.act_rotate_arrows.triggered.connect(self.display_ctl.on_rotate_arrows)
|
||||
self.view_menu.addAction(self.act_rotate_arrows)
|
||||
|
||||
# The replay reads the recorded CSV (the values the session actually
|
||||
@@ -970,6 +996,9 @@ class StudioMainWindow(QMainWindow):
|
||||
finger_3d=self.act_finger_3d,
|
||||
settings=self.act_settings,
|
||||
)
|
||||
# The arrow-rotation entry follows the sensor, which is a display preference
|
||||
# (which draw style the legend describes) rather than a layout one.
|
||||
self.display_ctl.bind_actions(rotate_arrows=self.act_rotate_arrows)
|
||||
self._sync_menu_checks()
|
||||
|
||||
def _sync_menu_checks(self) -> None:
|
||||
@@ -1367,77 +1396,96 @@ class StudioMainWindow(QMainWindow):
|
||||
self.sensor_dialog.raise_()
|
||||
self.sensor_dialog.activateWindow()
|
||||
|
||||
# ------------------------------------------------- display (forwards)
|
||||
# How the numbers are shown lives in ``DisplayPrefsController``; these forward for
|
||||
# the call sites still in this file and for the tests / ``tools/ui_check``, which
|
||||
# drive the window. ``_on_plot_settings`` stays here: it opens a dialog and hands the
|
||||
# filters to the acquisition workers, which is not a display preference.
|
||||
def _set_display_unit(self, unit: str, *, sync_toolbar: bool = True) -> None:
|
||||
self.display_ctl.set_display_unit(unit, sync_toolbar=sync_toolbar)
|
||||
|
||||
def _on_units_changed(self, units: str) -> None:
|
||||
self._set_display_unit(
|
||||
"N" if str(units) == "newton" else "raw", sync_toolbar=False
|
||||
)
|
||||
self.display_ctl.on_units_changed(units)
|
||||
|
||||
def _on_chart_unit_toggle(self) -> None:
|
||||
"""The chart's own unit chip: newton <-> raw."""
|
||||
self._set_display_unit("raw" if self.force_plots.unit() == "N" else "N")
|
||||
|
||||
def _set_display_unit(self, unit: str, *, sync_toolbar: bool = True) -> None:
|
||||
"""One display unit for the toolbar, the charts and the value cards.
|
||||
|
||||
The toolbar combo can already *be* on the requested unit (for instance
|
||||
after a replay's unit switch), in which case its signal never fires — so
|
||||
the charts are told directly instead of relying on that signal.
|
||||
"""
|
||||
if sync_toolbar:
|
||||
self.toolbar.set_units("newton" if unit == "N" else "raw")
|
||||
if self.replay.is_loaded():
|
||||
# One unit for the charts, the rail and the replay controller.
|
||||
self.replay.set_unit(unit)
|
||||
self.prefs["units"] = self.toolbar.units()
|
||||
self.force_plots.set_fixed_newton(unit == "N")
|
||||
# Rebuild the visible curves from the attached source (the session store
|
||||
# or the recording being replayed), which keeps both triplets, instead of
|
||||
# appending new-unit values onto old ones.
|
||||
self.force_plots.set_units(unit)
|
||||
self._persist_prefs()
|
||||
self.display_ctl.on_chart_unit_toggle()
|
||||
|
||||
def _on_image_view_mode_changed(self, mode: str) -> None:
|
||||
self.prefs["image_view_mode"] = str(mode)
|
||||
self._last_rendered_view_mode = None
|
||||
self._persist_prefs()
|
||||
if self._running:
|
||||
if self._active_worker is not None:
|
||||
self._render_run_image(self._active_worker.latest())
|
||||
self.display_ctl.on_image_view_mode_changed(mode)
|
||||
|
||||
def _on_show_fps_toggled(self, visible: bool) -> None:
|
||||
self.display_ctl.on_show_fps_toggled(visible)
|
||||
|
||||
def _apply_plot_prefs_to_panel(self) -> None:
|
||||
self.display_ctl.apply_plot_prefs_to_panel()
|
||||
|
||||
def _device_sample_rate(self) -> float:
|
||||
return self.display_ctl.device_sample_rate()
|
||||
|
||||
def _apply_arrow_rotation_for_sensor(self) -> None:
|
||||
self.display_ctl.apply_arrow_rotation_for_sensor()
|
||||
|
||||
def _apply_arrow_legend(self) -> None:
|
||||
self.display_ctl.apply_arrow_legend()
|
||||
|
||||
def _draw_style(self) -> str:
|
||||
return self.display_ctl.draw_style()
|
||||
|
||||
def _on_rotate_arrows(self) -> None:
|
||||
self.display_ctl.on_rotate_arrows()
|
||||
|
||||
def _repaint_live_frame(self) -> None:
|
||||
"""Repaint the 2D panel from the newest packet (the view mode changed).
|
||||
|
||||
The panel is showing a frame drawn in the *previous* mode, so it has to be
|
||||
rebuilt; only a running session has a newer packet to build it from.
|
||||
"""
|
||||
if not self._running or self._active_worker is None:
|
||||
return
|
||||
self._render_run_image(self._active_worker.latest())
|
||||
|
||||
@property
|
||||
def _show_fps(self) -> bool:
|
||||
return self.display_ctl.show_fps
|
||||
|
||||
@_show_fps.setter
|
||||
def _show_fps(self, visible: bool) -> None:
|
||||
self.display_ctl.show_fps = visible
|
||||
|
||||
|
||||
def _on_plot_settings(self) -> None:
|
||||
settings = self.display_ctl.plot_settings()
|
||||
dialog = PlotSettingsDialog(
|
||||
self,
|
||||
window_sec=float(self.prefs.get("plot_window_sec") or DEFAULT_PLOT_WINDOW_SEC),
|
||||
fn_max=float(self.prefs.get("plot_fn_max") or DEFAULT_PLOT_FN_MAX),
|
||||
shear_max=float(self.prefs.get("plot_shear_max") or DEFAULT_PLOT_SHEAR_MAX),
|
||||
fn_raw_max=float(
|
||||
self.prefs.get("plot_fn_raw_max") or DEFAULT_PLOT_FN_RAW_MAX
|
||||
),
|
||||
shear_raw_max=float(
|
||||
self.prefs.get("plot_shear_raw_max") or DEFAULT_PLOT_SHEAR_RAW_MAX
|
||||
),
|
||||
units=self.toolbar.units(),
|
||||
window_sec=settings["window_sec"],
|
||||
fn_max=settings["fn_max"],
|
||||
shear_max=settings["shear_max"],
|
||||
fn_raw_max=settings["fn_raw_max"],
|
||||
shear_raw_max=settings["shear_raw_max"],
|
||||
units=settings["units"],
|
||||
filters=self._filter_settings(),
|
||||
# Asked *now*: the answer is a property of this moment, and the
|
||||
# dialog is the place that has to say "next session, not this one".
|
||||
filters_locked=bool(self._running or self.recorder.is_recording),
|
||||
sample_rate_hz=self._device_sample_rate(),
|
||||
sample_rate_hz=self.display_ctl.device_sample_rate(),
|
||||
)
|
||||
if dialog.exec_() != PlotSettingsDialog.Accepted:
|
||||
return
|
||||
window_sec, fn_max, shear_max, fn_raw_max, shear_raw_max = dialog.values()
|
||||
self.prefs["plot_window_sec"] = window_sec
|
||||
self.prefs["plot_fn_max"] = fn_max
|
||||
self.prefs["plot_shear_max"] = shear_max
|
||||
self.prefs["plot_fn_raw_max"] = fn_raw_max
|
||||
self.prefs["plot_shear_raw_max"] = shear_raw_max
|
||||
self.display_ctl.apply_plot_settings(
|
||||
{
|
||||
"window_sec": window_sec,
|
||||
"fn_max": fn_max,
|
||||
"shear_max": shear_max,
|
||||
"fn_raw_max": fn_raw_max,
|
||||
"shear_raw_max": shear_raw_max,
|
||||
}
|
||||
)
|
||||
filters = dialog.filter_values()
|
||||
# The same keys ``_filter_settings()`` reads back, so what the dialog
|
||||
# shows next time is what the session will actually be given.
|
||||
for key, value in filters.as_dict().items():
|
||||
self.prefs[f"plot_{key}"] = value
|
||||
self.force_plots.set_window_sec(window_sec)
|
||||
self.force_plots.set_axis_limits(fn_max, shear_max, fn_raw_max, shear_raw_max)
|
||||
self._last_plot_packet_key = None
|
||||
self._persist_prefs()
|
||||
if not self._running and not self.recorder.is_recording:
|
||||
@@ -1451,26 +1499,6 @@ class StudioMainWindow(QMainWindow):
|
||||
t("status.filters_saved", describe=filters.describe())
|
||||
)
|
||||
|
||||
def _device_sample_rate(self) -> float:
|
||||
"""The frame rate the low-pass coefficients would be designed for."""
|
||||
return session_metadata.device_sample_rate(
|
||||
self._latest_fps,
|
||||
self.prefs.get("measured_fps"),
|
||||
default=float(FORCE_SAMPLE_RATE_HZ),
|
||||
)
|
||||
|
||||
def _apply_plot_prefs_to_panel(self) -> None:
|
||||
self.force_plots.set_window_sec(
|
||||
float(self.prefs.get("plot_window_sec") or DEFAULT_PLOT_WINDOW_SEC)
|
||||
)
|
||||
self.force_plots.set_axis_limits(
|
||||
float(self.prefs.get("plot_fn_max") or DEFAULT_PLOT_FN_MAX),
|
||||
float(self.prefs.get("plot_shear_max") or DEFAULT_PLOT_SHEAR_MAX),
|
||||
float(self.prefs.get("plot_fn_raw_max") or DEFAULT_PLOT_FN_RAW_MAX),
|
||||
float(self.prefs.get("plot_shear_raw_max") or DEFAULT_PLOT_SHEAR_RAW_MAX),
|
||||
)
|
||||
self.force_plots.set_fixed_newton(self.toolbar.units() == "newton")
|
||||
|
||||
def _on_calibration_exported(self, path: str) -> None:
|
||||
"""Note the coefficients that were written. Nothing is applied here.
|
||||
|
||||
@@ -1655,13 +1683,6 @@ class StudioMainWindow(QMainWindow):
|
||||
return
|
||||
view.submit_packet(packet)
|
||||
|
||||
def _on_show_fps_toggled(self, visible: bool) -> None:
|
||||
self._show_fps = bool(visible)
|
||||
self.prefs["show_fps"] = self._show_fps
|
||||
# FPS has one home now: the status rail.
|
||||
self._refresh_status_rail()
|
||||
self._persist_prefs()
|
||||
|
||||
def _on_sensor_type_changed(self, sensor_type: str) -> None:
|
||||
self.prefs["sensor_type"] = str(sensor_type)
|
||||
self.prefs["config_name"] = self.sensor_picker.config_name()
|
||||
@@ -1680,34 +1701,6 @@ class StudioMainWindow(QMainWindow):
|
||||
self._refresh_status_rail()
|
||||
self._persist_prefs()
|
||||
|
||||
def _apply_arrow_rotation_for_sensor(self) -> None:
|
||||
enabled = self.sensor_picker.sensor_type() in ARROW_ROTATE_TYPES
|
||||
self.arrow_panel.set_rotation_enabled(enabled)
|
||||
self.act_rotate_arrows.setEnabled(enabled)
|
||||
self._apply_arrow_legend()
|
||||
|
||||
def _draw_style(self) -> str:
|
||||
"""Which arrow draw style the configured sensor uses."""
|
||||
return "finger" if self.sensor_picker.sensor_type() == "finger" else "box"
|
||||
|
||||
def _apply_arrow_legend(self) -> None:
|
||||
"""The arrow legend describes the *configured* sensor, from the start.
|
||||
|
||||
The magnification is a property of the draw style, not of the first
|
||||
frame that happens to arrive, so a finger sensor must not advertise the
|
||||
box magnification while it is still idle.
|
||||
"""
|
||||
from orisys_app_shared.acquisition.arrows import arrow_display_scale
|
||||
|
||||
self.arrow_panel.set_arrow_scale(arrow_display_scale(self._draw_style()))
|
||||
|
||||
def _on_rotate_arrows(self) -> None:
|
||||
if self.sensor_picker.sensor_type() not in ARROW_ROTATE_TYPES:
|
||||
return
|
||||
deg = self.arrow_panel.rotate_90_ccw()
|
||||
self.prefs["arrow_rotation_deg"] = deg
|
||||
self._persist_prefs()
|
||||
|
||||
# ----------------------------------------------------------- shortcuts
|
||||
def _sync_shortcut_actions(self) -> None:
|
||||
if getattr(self, "act_run", None) is None:
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
"""How the numbers are *shown*: units, chart ranges, arrows, and the readouts.
|
||||
|
||||
The panels render; this controller decides what the operator is looking at when the same
|
||||
measurement can be presented two ways — newtons or raw counts, a rolling window or a
|
||||
fixed range, arrows drawn for a box or for a finger, the frame rate on the rail or not.
|
||||
That is a change driver of its own: a preference changes without touching the acquisition
|
||||
session, the replay policy or the panel arrangement.
|
||||
|
||||
It owns the dialogue-free half of the plot settings (the values, their defaults and what
|
||||
they do to the panel) and leaves the *dialog* to the window: opening a QDialog and handing
|
||||
the filters to the acquisition workers is window work, and the session's filters are not a
|
||||
display preference.
|
||||
|
||||
What it borrows from the window is named by :class:`DisplayHost` — persisting the choice,
|
||||
the rail, the filter settings the dialog shows, the latest measured rate, and a live
|
||||
repaint when the 2D view mode changes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Optional, Protocol
|
||||
|
||||
from PyQt5.QtCore import QObject
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DisplayHost(Protocol):
|
||||
"""What the display controller needs from the window.
|
||||
|
||||
The names are the window's own (its transitional shims included), because the
|
||||
window is the only implementation today.
|
||||
"""
|
||||
|
||||
prefs: dict
|
||||
|
||||
@property
|
||||
def _latest_fps(self) -> Optional[float]: ...
|
||||
|
||||
def _persist_prefs(self) -> None: ...
|
||||
|
||||
def _refresh_status_rail(self) -> None: ...
|
||||
|
||||
def _drop_render_marker(self) -> None: ...
|
||||
|
||||
def _repaint_live_frame(self) -> None: ...
|
||||
|
||||
|
||||
class DisplayPrefsController(QObject):
|
||||
"""Units, chart range and window, arrow display, and the fps readout."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
host: DisplayHost,
|
||||
toolbar: Any,
|
||||
charts: Any,
|
||||
arrow_panel: Any,
|
||||
engine: Any,
|
||||
sensor_picker: Any,
|
||||
plot_defaults: dict,
|
||||
default_sample_rate_hz: float,
|
||||
arrow_rotate_types: frozenset,
|
||||
rotate_arrows_action: Any = None,
|
||||
parent: Optional[QObject] = None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self._host = host
|
||||
self._toolbar = toolbar
|
||||
self._charts = charts
|
||||
self._arrow_panel = arrow_panel
|
||||
self._engine = engine
|
||||
self._picker = sensor_picker
|
||||
self._rotate_action = rotate_arrows_action
|
||||
self._plot_defaults = dict(plot_defaults)
|
||||
self._default_sample_rate_hz = float(default_sample_rate_hz)
|
||||
self._arrow_rotate_types = frozenset(arrow_rotate_types)
|
||||
#: Whether the rail shows the frame rate: a display preference, so it lives with
|
||||
#: the other ones rather than with the session that produces the number.
|
||||
self._show_fps = bool(host.prefs.get("show_fps", False))
|
||||
|
||||
def bind_actions(self, *, rotate_arrows: Any) -> None:
|
||||
"""Take the rotate entry once the menus exist (it follows the sensor)."""
|
||||
self._rotate_action = rotate_arrows
|
||||
|
||||
# ------------------------------------------------------------------- state
|
||||
@property
|
||||
def show_fps(self) -> bool:
|
||||
return self._show_fps
|
||||
|
||||
@show_fps.setter
|
||||
def show_fps(self, visible: bool) -> None:
|
||||
self._show_fps = bool(visible)
|
||||
|
||||
# ------------------------------------------------------------------- units
|
||||
def set_display_unit(self, unit: str, *, sync_toolbar: bool = True) -> None:
|
||||
"""One display unit for the toolbar, the charts and the value cards.
|
||||
|
||||
The toolbar combo can already *be* on the requested unit (for instance after a
|
||||
replay's unit switch), in which case its signal never fires — so the charts are
|
||||
told directly instead of relying on that signal.
|
||||
"""
|
||||
if sync_toolbar:
|
||||
self._toolbar.set_units("newton" if unit == "N" else "raw")
|
||||
if self._engine.is_loaded():
|
||||
# One unit for the charts, the rail and the replay controller.
|
||||
self._engine.set_unit(unit)
|
||||
self._host.prefs["units"] = self._toolbar.units()
|
||||
self._charts.set_fixed_newton(unit == "N")
|
||||
# Rebuild the visible curves from the attached source (the session store or the
|
||||
# recording being replayed), which keeps both triplets, instead of appending
|
||||
# new-unit values onto old ones.
|
||||
self._charts.set_units(unit)
|
||||
self._host._persist_prefs()
|
||||
|
||||
def on_units_changed(self, units: str) -> None:
|
||||
self.set_display_unit(
|
||||
"N" if str(units) == "newton" else "raw", sync_toolbar=False
|
||||
)
|
||||
|
||||
def on_chart_unit_toggle(self) -> None:
|
||||
"""The chart's own unit chip: newton <-> raw."""
|
||||
self.set_display_unit("raw" if self._charts.unit() == "N" else "N")
|
||||
|
||||
# ------------------------------------------------------------- 2D view mode
|
||||
def on_image_view_mode_changed(self, mode: str) -> None:
|
||||
self._host.prefs["image_view_mode"] = str(mode)
|
||||
# The next packet must repaint the panel: the frame that is on screen was drawn
|
||||
# in the previous mode.
|
||||
self._host._drop_render_marker()
|
||||
self._host._persist_prefs()
|
||||
self._host._repaint_live_frame()
|
||||
|
||||
# --------------------------------------------------------------------- plot
|
||||
def plot_settings(self) -> dict:
|
||||
"""The chart range and window as the settings dialog should show them."""
|
||||
prefs = self._host.prefs
|
||||
return {
|
||||
"window_sec": float(
|
||||
prefs.get("plot_window_sec") or self._plot_defaults["window_sec"]
|
||||
),
|
||||
"fn_max": float(prefs.get("plot_fn_max") or self._plot_defaults["fn_max"]),
|
||||
"shear_max": float(
|
||||
prefs.get("plot_shear_max") or self._plot_defaults["shear_max"]
|
||||
),
|
||||
"fn_raw_max": float(
|
||||
prefs.get("plot_fn_raw_max") or self._plot_defaults["fn_raw_max"]
|
||||
),
|
||||
"shear_raw_max": float(
|
||||
prefs.get("plot_shear_raw_max") or self._plot_defaults["shear_raw_max"]
|
||||
),
|
||||
"units": self._toolbar.units(),
|
||||
}
|
||||
|
||||
def apply_plot_settings(self, values: dict) -> None:
|
||||
"""Remember the chart range the operator chose, and show it at once."""
|
||||
for key in ("window_sec", "fn_max", "shear_max", "fn_raw_max", "shear_raw_max"):
|
||||
self._host.prefs[f"plot_{key}"] = values[key]
|
||||
self._charts.set_window_sec(float(values["window_sec"]))
|
||||
self._charts.set_axis_limits(
|
||||
float(values["fn_max"]),
|
||||
float(values["shear_max"]),
|
||||
float(values["fn_raw_max"]),
|
||||
float(values["shear_raw_max"]),
|
||||
)
|
||||
self._host._persist_prefs()
|
||||
|
||||
def apply_plot_prefs_to_panel(self) -> None:
|
||||
"""Show the stored chart range without asking anything (startup path)."""
|
||||
values = self.plot_settings()
|
||||
self._charts.set_window_sec(float(values["window_sec"]))
|
||||
self._charts.set_axis_limits(
|
||||
float(values["fn_max"]),
|
||||
float(values["shear_max"]),
|
||||
float(values["fn_raw_max"]),
|
||||
float(values["shear_raw_max"]),
|
||||
)
|
||||
self._charts.set_fixed_newton(self._toolbar.units() == "newton")
|
||||
|
||||
def device_sample_rate(self) -> float:
|
||||
"""The frame rate the low-pass coefficients would be designed for."""
|
||||
from orisys_studio import session_metadata
|
||||
|
||||
return session_metadata.device_sample_rate(
|
||||
self._host._latest_fps,
|
||||
self._host.prefs.get("measured_fps"),
|
||||
default=self._default_sample_rate_hz,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------- arrows
|
||||
def draw_style(self) -> str:
|
||||
"""Which arrow draw style the configured sensor uses."""
|
||||
return "finger" if self._picker.sensor_type() == "finger" else "box"
|
||||
|
||||
def apply_arrow_rotation_for_sensor(self) -> None:
|
||||
enabled = self._picker.sensor_type() in self._arrow_rotate_types
|
||||
self._arrow_panel.set_rotation_enabled(enabled)
|
||||
if self._rotate_action is not None:
|
||||
self._rotate_action.setEnabled(enabled)
|
||||
self.apply_arrow_legend()
|
||||
|
||||
def apply_arrow_legend(self) -> None:
|
||||
"""The arrow legend describes the *configured* sensor, from the start.
|
||||
|
||||
The magnification is a property of the draw style, not of the first frame that
|
||||
happens to arrive, so a finger sensor must not advertise the box magnification
|
||||
while it is still idle.
|
||||
"""
|
||||
from orisys_app_shared.acquisition.arrows import arrow_display_scale
|
||||
|
||||
self._arrow_panel.set_arrow_scale(arrow_display_scale(self.draw_style()))
|
||||
|
||||
def on_rotate_arrows(self) -> None:
|
||||
if self._picker.sensor_type() not in self._arrow_rotate_types:
|
||||
return
|
||||
deg = self._arrow_panel.rotate_90_ccw()
|
||||
self._host.prefs["arrow_rotation_deg"] = deg
|
||||
self._host._persist_prefs()
|
||||
|
||||
# ---------------------------------------------------------------------- fps
|
||||
def on_show_fps_toggled(self, visible: bool) -> None:
|
||||
self._show_fps = bool(visible)
|
||||
self._host.prefs["show_fps"] = self._show_fps
|
||||
# FPS has one home now: the status rail.
|
||||
self._host._refresh_status_rail()
|
||||
self._host._persist_prefs()
|
||||
Reference in New Issue
Block a user