收紧绘图设置为两页,并把 FN 牛顿下限固定为 0。
手指 2D 方向改到 ArrowPanel 设置对话框;归零阈值不变。已用 display_prefs、finger_2d_orientation 与绘图对话框相关 smoke 测试验证。
This commit is contained in:
@@ -52,10 +52,12 @@
|
||||
},
|
||||
"dialog": {
|
||||
"plot_settings_title": "Plot settings",
|
||||
"plot_tab_curves": "Curves",
|
||||
"plot_tab_filters": "Filters",
|
||||
"plot_window": "Time window",
|
||||
"plot_window_tip": "Force plot time window (seconds)",
|
||||
"plot_fn_min": "FN min",
|
||||
"plot_fn_min_tip": "Normal-force axis minimum (Newton mode). It may be negative: leaving room below zero keeps the idle curve off the FX frame and makes small presses visible",
|
||||
"plot_fn_min_tip": "Normal-force axis minimum (Newton mode); fixed at 0",
|
||||
"plot_fn_max": "FN max",
|
||||
"plot_fn_max_tip": "Normal force axis max (Newton mode)",
|
||||
"plot_shear_max": "Shear max",
|
||||
@@ -204,6 +206,7 @@
|
||||
"view_depth": "Depth map",
|
||||
"view_recorded": "Recorded image",
|
||||
"view_mode_tip": "Image display mode while running",
|
||||
"arrow_settings_tip": "Finger 2D presentation (orientation; display only)",
|
||||
"replay_pipeline": "Recorded video (raw camera image)",
|
||||
"replay_pipeline_tip": "Replay shows the recorded raw camera image with no live arrow overlay; to get arrows and depth from analysis, use View → Analyze video",
|
||||
"replay_raw_note": "No analysis result yet. Opening a recording runs analysis, or choose Analyze video… above.",
|
||||
|
||||
@@ -52,10 +52,12 @@
|
||||
},
|
||||
"dialog": {
|
||||
"plot_settings_title": "绘图设置",
|
||||
"plot_tab_curves": "曲线",
|
||||
"plot_tab_filters": "滤波",
|
||||
"plot_window": "时间窗口",
|
||||
"plot_window_tip": "力曲线时间窗口(秒)",
|
||||
"plot_fn_min": "FN 下限",
|
||||
"plot_fn_min_tip": "法向力纵轴下限(牛顿模式)。可以是负数:留出零点以下的空间,静止曲线不会压在 FX 框线上,小压力也更清楚",
|
||||
"plot_fn_min_tip": "法向力纵轴下限(牛顿模式);固定为 0",
|
||||
"plot_fn_max": "FN 上限",
|
||||
"plot_fn_max_tip": "法向力纵轴上限(牛顿模式)",
|
||||
"plot_shear_max": "剪切上限",
|
||||
@@ -204,6 +206,7 @@
|
||||
"view_depth": "深度图",
|
||||
"view_recorded": "原始图像",
|
||||
"view_mode_tip": "运行时的图像显示模式",
|
||||
"arrow_settings_tip": "手指 2D 显示方向(只影响画面)",
|
||||
"replay_pipeline": "录制视频(原始相机图像)",
|
||||
"replay_pipeline_tip": "回放显示的是录制下来的原始相机图像,不含实时箭头叠加;要看分析得到的箭头与深度请用「视图 → 分析视频」",
|
||||
"replay_raw_note": "尚无分析结果。打开录像会自动分析,或点顶部「分析视频…」。",
|
||||
|
||||
@@ -65,6 +65,7 @@ from orisys_studio.ui.brand_bar import BrandBar
|
||||
from orisys_studio.ui.cal_panel import CalPanel
|
||||
from orisys_studio.ui.force_plots import CHANNEL_LABELS, ForcePlotsPanel
|
||||
from orisys_studio.ui.mesh3d_panel import Mesh3dPanel
|
||||
from orisys_studio.ui.finger_orientation_dialog import FingerOrientationDialog
|
||||
from orisys_studio.ui.plot_settings_dialog import (
|
||||
DEFAULT_PLOT_FN_MAX,
|
||||
DEFAULT_PLOT_FN_RAW_MAX,
|
||||
@@ -926,6 +927,8 @@ class StudioMainWindow(QMainWindow):
|
||||
self.display_ctl.on_image_view_mode_changed
|
||||
)
|
||||
self.arrow_panel.view_mode_changed.connect(lambda _mode: self.replay_ctl.render_frame())
|
||||
# Finger 2D presentation lives on the 2D panel (not in plot settings).
|
||||
self.arrow_panel.settings_requested.connect(self._on_finger_orientation_settings)
|
||||
# The unit chip on the chart header switches the same unit everywhere.
|
||||
self.force_plots.unit_toggle_requested.connect(
|
||||
self.display_ctl.on_chart_unit_toggle
|
||||
@@ -1766,13 +1769,21 @@ class StudioMainWindow(QMainWindow):
|
||||
self.display_ctl.show_fps = visible
|
||||
|
||||
|
||||
def _on_finger_orientation_settings(self) -> None:
|
||||
"""2D panel gear: how the finger canvas is presented (display only)."""
|
||||
dialog = FingerOrientationDialog(
|
||||
self, finger_orientation=self.finger_orientation()
|
||||
)
|
||||
if dialog.exec() != FingerOrientationDialog.Accepted:
|
||||
return
|
||||
self.set_finger_orientation(dialog.orientation())
|
||||
|
||||
def _on_plot_settings(self) -> None:
|
||||
settings = self.display_ctl.plot_settings()
|
||||
dialog = PlotSettingsDialog(
|
||||
self,
|
||||
window_sec=settings["window_sec"],
|
||||
# FN's lower bound may be negative: that is the "show me below zero" the
|
||||
# operator asked for, and it is what puts the 0 tick back on the axis.
|
||||
# Newton FN floor is fixed at zero; the dialog still accepts the key.
|
||||
fn_min=settings["fn_min"],
|
||||
fn_max=settings["fn_max"],
|
||||
shear_max=settings["shear_max"],
|
||||
@@ -1790,17 +1801,12 @@ class StudioMainWindow(QMainWindow):
|
||||
# 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.display_ctl.device_sample_rate(),
|
||||
finger_orientation=self._finger_orientation,
|
||||
)
|
||||
if dialog.exec() != PlotSettingsDialog.Accepted:
|
||||
return
|
||||
# One call for the whole picture: the dialog owns the schema, the controller
|
||||
# writes what it was given and re-reads the rest.
|
||||
self.display_ctl.apply_plot_settings(dialog.settings())
|
||||
# The 2D presentation is applied here rather than in the controller: it changes
|
||||
# the picture the *window* draws (``_show_image_frame``), and it must take
|
||||
# effect without a restart.
|
||||
self.set_finger_orientation(dialog.orientation())
|
||||
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.
|
||||
|
||||
@@ -42,8 +42,8 @@ from orisys_studio.ui.plot_settings_dialog import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
#: The chart range, per channel and per unit. ``fn_min`` is the one that may be
|
||||
#: negative (FN is asymmetric); FX/FY take one absolute magnitude each.
|
||||
#: The chart range, per channel and per unit. Newton ``fn_min`` is fixed at zero;
|
||||
#: FX/FY take one absolute magnitude each. Raw FN still has its own lower bound.
|
||||
PLOT_RANGE_DEFAULTS = {
|
||||
"window_sec": DEFAULT_PLOT_WINDOW_SEC,
|
||||
"fn_min": DEFAULT_PLOT_FN_MIN,
|
||||
@@ -162,7 +162,6 @@ class DisplayPrefsController(QObject):
|
||||
# --------------------------------------------------------------------- plot
|
||||
def plot_settings(self) -> dict:
|
||||
"""The chart range, deadband and window as the settings dialog shows them."""
|
||||
prefs = self._host.prefs
|
||||
settings = {
|
||||
key: self._number(
|
||||
f"plot_{key}", self._plot_defaults.get(key, fallback)
|
||||
@@ -171,6 +170,8 @@ class DisplayPrefsController(QObject):
|
||||
**PLOT_RANGE_DEFAULTS, **PLOT_DEADBAND_DEFAULTS, **PLOT_PRECISION_DEFAULTS
|
||||
}.items()
|
||||
}
|
||||
# Newton FN floor is fixed; older prefs may still hold a negative value.
|
||||
settings["fn_min"] = float(DEFAULT_PLOT_FN_MIN)
|
||||
settings["units"] = self._toolbar.units()
|
||||
return settings
|
||||
|
||||
@@ -197,6 +198,8 @@ class DisplayPrefsController(QObject):
|
||||
for key in (*PLOT_RANGE_DEFAULTS, *PLOT_DEADBAND_DEFAULTS, *PLOT_PRECISION_DEFAULTS):
|
||||
if values.get(key) is not None:
|
||||
self._host.prefs[f"plot_{key}"] = values[key]
|
||||
# Always rewrite Newton FN floor so a stored negative cannot linger.
|
||||
self._host.prefs["plot_fn_min"] = float(DEFAULT_PLOT_FN_MIN)
|
||||
settings = self.plot_settings()
|
||||
self._charts.set_window_sec(float(settings["window_sec"]))
|
||||
self._apply_ranges(settings)
|
||||
|
||||
@@ -17,17 +17,19 @@ from orisys_app_shared.acquisition.image_backend import (
|
||||
ImagePlan,
|
||||
)
|
||||
from orisys_app_shared.i18n import t
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtCore import QSize, Qt, Signal
|
||||
from PySide6.QtGui import QImage, QPainter, QPixmap, QTransform
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from orisys_studio.ui import icons
|
||||
from orisys_studio.ui.fit_text import ShrinkableLabel
|
||||
|
||||
VIEW_MODES = (ARROW_PURE, ARROW_STITCHED, DEPTH)
|
||||
@@ -149,6 +151,8 @@ class ColorBarLabel(QLabel):
|
||||
|
||||
class ArrowPanel(QFrame):
|
||||
view_mode_changed = Signal(str)
|
||||
#: Finger 2D presentation (orientation). The window opens the small dialog.
|
||||
settings_requested = Signal()
|
||||
|
||||
def __init__(self, parent=None, *, theme: str = "dark") -> None:
|
||||
super().__init__(parent)
|
||||
@@ -178,9 +182,15 @@ class ArrowPanel(QFrame):
|
||||
self.view_combo = QComboBox()
|
||||
self.view_combo.setMinimumWidth(140)
|
||||
self.view_combo.currentIndexChanged.connect(self._on_view_index_changed)
|
||||
self.btn_settings = QPushButton()
|
||||
self.btn_settings.setProperty("compact", "true")
|
||||
self.btn_settings.setProperty("iconOnly", "true")
|
||||
self.btn_settings.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.btn_settings.clicked.connect(self.settings_requested.emit)
|
||||
header.addWidget(self.title)
|
||||
header.addStretch(1)
|
||||
header.addWidget(self.view_combo)
|
||||
header.addWidget(self.btn_settings)
|
||||
layout.addLayout(header)
|
||||
|
||||
self.image_label = AspectImageLabel()
|
||||
@@ -197,6 +207,7 @@ class ArrowPanel(QFrame):
|
||||
|
||||
self._placeholder = ""
|
||||
self._populate_view_combo()
|
||||
self._apply_icons()
|
||||
self.retranslate()
|
||||
self._refresh_footer_visibility()
|
||||
self._sync_aspect()
|
||||
@@ -382,6 +393,12 @@ class ArrowPanel(QFrame):
|
||||
def set_theme(self, theme: str) -> None:
|
||||
self._theme = str(theme)
|
||||
self.colorbar.set_theme(self._theme)
|
||||
self._apply_icons()
|
||||
|
||||
def _apply_icons(self) -> None:
|
||||
size = QSize(icons.ICON_SIZE, icons.ICON_SIZE)
|
||||
self.btn_settings.setIcon(icons.icon("settings", self._theme))
|
||||
self.btn_settings.setIconSize(size)
|
||||
|
||||
def set_depth_scale(
|
||||
self, text: str | None, *, tip_key: str = "panels.depth_scale_tip"
|
||||
@@ -458,6 +475,7 @@ class ArrowPanel(QFrame):
|
||||
if i < self.view_combo.count():
|
||||
self.view_combo.setItemText(i, t(_VIEW_LABELS.get(mode, mode)))
|
||||
self.view_combo.setToolTip(t("panels.view_mode_tip"))
|
||||
self.btn_settings.setToolTip(t("panels.arrow_settings_tip"))
|
||||
self.image_label.setToolTip(t("panels.arrow_empty_tip"))
|
||||
self._refresh_pipeline_label()
|
||||
self._refresh_scale_label()
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Finger 2D canvas presentation: how the unrolled domain is shown.
|
||||
|
||||
Display only — the record and stored arrays keep the domain's own orientation.
|
||||
The 3D view is untouched (its frame is the design export's).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from orisys_app_shared.acquisition.display_orientation import (
|
||||
DEFAULT_FINGER_ORIENTATION,
|
||||
ORIENTATIONS,
|
||||
normalise_orientation,
|
||||
)
|
||||
from orisys_app_shared.i18n import t
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QFormLayout,
|
||||
QLabel,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
|
||||
class FingerOrientationDialog(QDialog):
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
*,
|
||||
finger_orientation: str = DEFAULT_FINGER_ORIENTATION,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self.setModal(True)
|
||||
self.resize(420, 200)
|
||||
|
||||
root = QVBoxLayout(self)
|
||||
form = QFormLayout()
|
||||
|
||||
self.orientation_label = QLabel()
|
||||
self.orientation_combo = QComboBox()
|
||||
for name in ORIENTATIONS:
|
||||
self.orientation_combo.addItem(t(f"dialog.orientation_{name}"), name)
|
||||
index = self.orientation_combo.findData(
|
||||
normalise_orientation(finger_orientation)
|
||||
)
|
||||
self.orientation_combo.setCurrentIndex(max(index, 0))
|
||||
self.orientation_note = QLabel()
|
||||
self.orientation_note.setObjectName("hintText")
|
||||
self.orientation_note.setWordWrap(True)
|
||||
|
||||
form.addRow(self.orientation_label, self.orientation_combo)
|
||||
form.addRow(self.orientation_note)
|
||||
root.addLayout(form)
|
||||
|
||||
self.button_box = QDialogButtonBox(
|
||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
)
|
||||
self.button_box.accepted.connect(self.accept)
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
root.addWidget(self.button_box)
|
||||
|
||||
self.retranslate()
|
||||
|
||||
def orientation(self) -> str:
|
||||
return normalise_orientation(self.orientation_combo.currentData())
|
||||
|
||||
def retranslate(self) -> None:
|
||||
self.setWindowTitle(t("dialog.plot_orientation_group"))
|
||||
self.orientation_label.setText(t("dialog.plot_orientation"))
|
||||
self.orientation_combo.setToolTip(t("dialog.plot_orientation_tip"))
|
||||
self.orientation_note.setText(t("dialog.plot_orientation_note"))
|
||||
for index in range(self.orientation_combo.count()):
|
||||
name = str(self.orientation_combo.itemData(index) or "")
|
||||
self.orientation_combo.setItemText(index, t(f"dialog.orientation_{name}"))
|
||||
ok_btn = self.button_box.button(QDialogButtonBox.StandardButton.Ok)
|
||||
cancel_btn = self.button_box.button(QDialogButtonBox.StandardButton.Cancel)
|
||||
if ok_btn is not None:
|
||||
ok_btn.setText(t("dialog.ok"))
|
||||
if cancel_btn is not None:
|
||||
cancel_btn.setText(t("dialog.cancel"))
|
||||
@@ -6,16 +6,14 @@ a resting channel at zero), and what the acquisition is allowed to filter.
|
||||
The filter half is a *request*: it is handed to the SDK through
|
||||
``FilterSettings.apply_to`` at the start of the next session, and what the device
|
||||
reports as done is what the record will say (see ``force/filters.py``).
|
||||
|
||||
The FN Newton lower bound is fixed at zero (see :data:`DEFAULT_PLOT_FN_MIN`); it
|
||||
is not offered as a control. Finger 2D presentation lives on the 2D panel.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from orisys_app_shared.acquisition.constants import FORCE_SAMPLE_RATE_HZ
|
||||
from orisys_app_shared.acquisition.display_orientation import (
|
||||
DEFAULT_FINGER_ORIENTATION,
|
||||
ORIENTATIONS,
|
||||
normalise_orientation,
|
||||
)
|
||||
from orisys_app_shared.force.filters import (
|
||||
FilterSettings,
|
||||
available_presets,
|
||||
@@ -33,13 +31,15 @@ from PySide6.QtWidgets import (
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QSpinBox,
|
||||
QTabWidget,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
DEFAULT_PLOT_WINDOW_SEC = 4.0
|
||||
#: Lower bounds. FN is allowed below zero: a resting normal force has no reason
|
||||
#: to sit exactly on the frame, and with the lower bound at 0 the idle curve is
|
||||
#: drawn along the subplot border that abuts the FX frame (see force_plots).
|
||||
#: FN Newton Y lower bound is always zero. A negative floor used to keep the
|
||||
#: idle curve off the FX frame; that control was removed so the axis always
|
||||
#: starts at rest = 0.
|
||||
DEFAULT_PLOT_FN_MIN = 0.0
|
||||
DEFAULT_PLOT_FN_MAX = 20.0
|
||||
DEFAULT_PLOT_SHEAR_MAX = 20.0
|
||||
@@ -75,20 +75,23 @@ class PlotSettingsDialog(QDialog):
|
||||
filters: FilterSettings | None = None,
|
||||
filters_locked: bool = False,
|
||||
sample_rate_hz: float = FORCE_SAMPLE_RATE_HZ,
|
||||
finger_orientation: str = DEFAULT_FINGER_ORIENTATION,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self.setModal(True)
|
||||
# The layout's own minimum (618 px with the 2D presentation group) keeps the
|
||||
# buttons on screen; this is the height it opens at.
|
||||
self.resize(460, 640)
|
||||
self.resize(460, 420)
|
||||
self._sample_rate_hz = float(sample_rate_hz)
|
||||
# Callers may still pass a remembered lower bound; Newton FN floor is fixed.
|
||||
_ = fn_min
|
||||
#: True while the widgets are being filled in from a preset, so the
|
||||
#: "edited -> custom" handler does not fire on our own writes.
|
||||
self._syncing_filters = False
|
||||
|
||||
root = QVBoxLayout(self)
|
||||
form = QFormLayout()
|
||||
self.tabs = QTabWidget(self)
|
||||
root.addWidget(self.tabs, 1)
|
||||
|
||||
curves = QWidget(self.tabs)
|
||||
form = QFormLayout(curves)
|
||||
|
||||
self.window_label = QLabel()
|
||||
self.window_spin = QDoubleSpinBox()
|
||||
@@ -98,15 +101,6 @@ class PlotSettingsDialog(QDialog):
|
||||
self.window_spin.setSuffix(" s")
|
||||
self.window_spin.setValue(float(window_sec))
|
||||
|
||||
# FN is asymmetric: a lower bound that may be negative, and an upper one.
|
||||
self.fn_min_label = QLabel()
|
||||
self.fn_min_spin = QDoubleSpinBox()
|
||||
self.fn_min_spin.setRange(-1000.0, 1000.0)
|
||||
self.fn_min_spin.setSingleStep(1.0)
|
||||
self.fn_min_spin.setDecimals(1)
|
||||
self.fn_min_spin.setSuffix(" N")
|
||||
self.fn_min_spin.setValue(float(fn_min))
|
||||
|
||||
self.fn_max_label = QLabel()
|
||||
self.fn_max_spin = QDoubleSpinBox()
|
||||
self.fn_max_spin.setRange(1.0, 100.0)
|
||||
@@ -173,24 +167,7 @@ class PlotSettingsDialog(QDialog):
|
||||
precision_row.addWidget(QLabel("raw"))
|
||||
precision_row.addWidget(self.raw_decimals_spin)
|
||||
|
||||
# How the finger's 2D canvas is turned for display. A *presentation* choice, so
|
||||
# it lives with the other display ones (this dialog), not with the acquisition
|
||||
# filters below it, and it is applied at display time only — see
|
||||
# ``acquisition.display_orientation`` for why the shipped default is a half turn.
|
||||
self.orientation_label = QLabel()
|
||||
self.orientation_combo = QComboBox()
|
||||
for name in ORIENTATIONS:
|
||||
self.orientation_combo.addItem(t(f"dialog.orientation_{name}"), name)
|
||||
index = self.orientation_combo.findData(
|
||||
normalise_orientation(finger_orientation)
|
||||
)
|
||||
self.orientation_combo.setCurrentIndex(max(index, 0))
|
||||
self.orientation_note = QLabel()
|
||||
self.orientation_note.setObjectName("hintText")
|
||||
self.orientation_note.setWordWrap(True)
|
||||
|
||||
form.addRow(self.window_label, self.window_spin)
|
||||
form.addRow(self.fn_min_label, self.fn_min_spin)
|
||||
form.addRow(self.fn_max_label, self.fn_max_spin)
|
||||
form.addRow(self.shear_max_label, self.shear_max_spin)
|
||||
form.addRow(self.fn_raw_min_label, self.fn_raw_min_spin)
|
||||
@@ -200,13 +177,15 @@ class PlotSettingsDialog(QDialog):
|
||||
form.addRow(self.deadband_raw_label, self.deadband_raw_spin)
|
||||
self.precision_label = QLabel()
|
||||
form.addRow(self.precision_label, precision_row)
|
||||
root.addLayout(form)
|
||||
self.tabs.addTab(curves, "")
|
||||
|
||||
self.orientation_group = QGroupBox()
|
||||
orientation_form = QFormLayout(self.orientation_group)
|
||||
orientation_form.addRow(self.orientation_label, self.orientation_combo)
|
||||
orientation_form.addRow(self.orientation_note)
|
||||
root.addWidget(self.orientation_group)
|
||||
filters_page = QWidget(self.tabs)
|
||||
filters_layout = QVBoxLayout(filters_page)
|
||||
filters_layout.setContentsMargins(0, 8, 0, 0)
|
||||
self._build_filter_section(
|
||||
filters_layout, filters or FilterSettings(), filters_locked
|
||||
)
|
||||
self.tabs.addTab(filters_page, "")
|
||||
|
||||
newton = str(units or "newton").lower().startswith("n")
|
||||
raw_widgets = (
|
||||
@@ -219,21 +198,26 @@ class PlotSettingsDialog(QDialog):
|
||||
self.shear_raw_max_label,
|
||||
self.deadband_raw_label,
|
||||
)
|
||||
for widget in (self.fn_min_spin, self.fn_max_spin, self.shear_max_spin,
|
||||
self.deadband_n_spin, self.fn_min_label, self.fn_max_label,
|
||||
self.shear_max_label, self.deadband_n_label):
|
||||
for widget in (
|
||||
self.fn_max_spin,
|
||||
self.shear_max_spin,
|
||||
self.deadband_n_spin,
|
||||
self.fn_max_label,
|
||||
self.shear_max_label,
|
||||
self.deadband_n_label,
|
||||
):
|
||||
widget.setEnabled(newton)
|
||||
for widget in raw_widgets:
|
||||
widget.setEnabled(not newton)
|
||||
|
||||
self._build_filter_section(root, filters or FilterSettings(), filters_locked)
|
||||
|
||||
buttons_row = QHBoxLayout()
|
||||
self.btn_reset = QPushButton()
|
||||
self.btn_reset.clicked.connect(self._reset_defaults)
|
||||
buttons_row.addWidget(self.btn_reset)
|
||||
buttons_row.addStretch(1)
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||
self.button_box = QDialogButtonBox(
|
||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
)
|
||||
self.button_box.accepted.connect(self.accept)
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
buttons_row.addWidget(self.button_box)
|
||||
@@ -310,8 +294,13 @@ class PlotSettingsDialog(QDialog):
|
||||
group_form.addRow(self.lowpass_order_label, self.lowpass_order_spin)
|
||||
group_form.addRow(self.rate_auto_check, self.rate_spin)
|
||||
|
||||
for widget in (self.flow_ema_spin, self.median_spin, self.lowpass_spin,
|
||||
self.rate_spin, self.rate_auto_check):
|
||||
for widget in (
|
||||
self.flow_ema_spin,
|
||||
self.median_spin,
|
||||
self.lowpass_spin,
|
||||
self.rate_spin,
|
||||
self.rate_auto_check,
|
||||
):
|
||||
widget.setToolTip("") # filled in by retranslate()
|
||||
self.flow_ema_spin.valueChanged.connect(self._on_filter_value_edited)
|
||||
self.median_spin.valueChanged.connect(self._on_filter_value_edited)
|
||||
@@ -337,6 +326,7 @@ class PlotSettingsDialog(QDialog):
|
||||
root.addWidget(self.filters_group)
|
||||
root.addWidget(self.filter_note)
|
||||
root.addWidget(self.filter_warning)
|
||||
root.addStretch(1)
|
||||
|
||||
self._filters_locked = bool(locked)
|
||||
self.filters_group.setEnabled(not self._filters_locked)
|
||||
@@ -443,7 +433,7 @@ class PlotSettingsDialog(QDialog):
|
||||
"""Every axis limit, keyed the way the preference names it."""
|
||||
return {
|
||||
"window_sec": float(self.window_spin.value()),
|
||||
"fn_min": float(self.fn_min_spin.value()),
|
||||
"fn_min": DEFAULT_PLOT_FN_MIN,
|
||||
"fn_max": float(self.fn_max_spin.value()),
|
||||
"shear_max": float(self.shear_max_spin.value()),
|
||||
"fn_raw_min": float(self.fn_raw_min_spin.value()),
|
||||
@@ -461,27 +451,19 @@ class PlotSettingsDialog(QDialog):
|
||||
def settings(self) -> dict[str, float]:
|
||||
"""Everything the plot half of the dialog decides, in one mapping."""
|
||||
return {
|
||||
**self.ranges(), **self.deadbands(),
|
||||
**self.ranges(),
|
||||
**self.deadbands(),
|
||||
"newton_decimals": int(self.newton_decimals_spin.value()),
|
||||
"raw_decimals": int(self.raw_decimals_spin.value()),
|
||||
}
|
||||
|
||||
def orientation(self) -> str:
|
||||
"""The 2D presentation the dialog is showing (see ``display_orientation``)."""
|
||||
return normalise_orientation(self.orientation_combo.currentData())
|
||||
|
||||
def _reset_defaults(self) -> None:
|
||||
self.window_spin.setValue(DEFAULT_PLOT_WINDOW_SEC)
|
||||
self.fn_min_spin.setValue(DEFAULT_PLOT_FN_MIN)
|
||||
self.fn_max_spin.setValue(DEFAULT_PLOT_FN_MAX)
|
||||
self.shear_max_spin.setValue(DEFAULT_PLOT_SHEAR_MAX)
|
||||
self.fn_raw_min_spin.setValue(DEFAULT_PLOT_FN_RAW_MIN)
|
||||
self.fn_raw_max_spin.setValue(DEFAULT_PLOT_FN_RAW_MAX)
|
||||
self.shear_raw_max_spin.setValue(DEFAULT_PLOT_SHEAR_RAW_MAX)
|
||||
# "Reset" means the shipped picture as well: the finger's derived default.
|
||||
self.orientation_combo.setCurrentIndex(
|
||||
max(self.orientation_combo.findData(DEFAULT_FINGER_ORIENTATION), 0)
|
||||
)
|
||||
self.deadband_n_spin.setValue(DEFAULT_PLOT_DEADBAND_N)
|
||||
self.deadband_raw_spin.setValue(DEFAULT_PLOT_DEADBAND_RAW)
|
||||
self.newton_decimals_spin.setValue(DEFAULT_PLOT_NEWTON_DECIMALS)
|
||||
@@ -498,10 +480,10 @@ class PlotSettingsDialog(QDialog):
|
||||
|
||||
def retranslate(self) -> None:
|
||||
self.setWindowTitle(t("dialog.plot_settings_title"))
|
||||
self.tabs.setTabText(0, t("dialog.plot_tab_curves"))
|
||||
self.tabs.setTabText(1, t("dialog.plot_tab_filters"))
|
||||
self.window_label.setText(t("dialog.plot_window"))
|
||||
self.window_spin.setToolTip(t("dialog.plot_window_tip"))
|
||||
self.fn_min_label.setText(t("dialog.plot_fn_min"))
|
||||
self.fn_min_spin.setToolTip(t("dialog.plot_fn_min_tip"))
|
||||
self.fn_max_label.setText(t("dialog.plot_fn_max"))
|
||||
self.fn_max_spin.setToolTip(t("dialog.plot_fn_max_tip"))
|
||||
self.shear_max_label.setText(t("dialog.plot_shear_max"))
|
||||
@@ -518,17 +500,12 @@ class PlotSettingsDialog(QDialog):
|
||||
self.deadband_raw_spin.setToolTip(t("dialog.plot_deadband_raw_tip"))
|
||||
self.precision_label.setText(t("dialog.plot_value_decimals"))
|
||||
self.btn_reset.setText(t("dialog.reset_defaults"))
|
||||
self.orientation_group.setTitle(t("dialog.plot_orientation_group"))
|
||||
self.orientation_label.setText(t("dialog.plot_orientation"))
|
||||
self.orientation_combo.setToolTip(t("dialog.plot_orientation_tip"))
|
||||
self.orientation_note.setText(t("dialog.plot_orientation_note"))
|
||||
for index in range(self.orientation_combo.count()):
|
||||
name = str(self.orientation_combo.itemData(index) or "")
|
||||
self.orientation_combo.setItemText(index, t(f"dialog.orientation_{name}"))
|
||||
self.filters_group.setTitle(t("dialog.plot_filters"))
|
||||
self.filter_preset_label.setText(t("dialog.filter_preset"))
|
||||
self.preset_combo.setToolTip(t("dialog.filter_preset_tip"))
|
||||
self.preset_combo.setItemText(self.preset_combo.count() - 1, t("dialog.filter_preset_custom"))
|
||||
self.preset_combo.setItemText(
|
||||
self.preset_combo.count() - 1, t("dialog.filter_preset_custom")
|
||||
)
|
||||
self.flow_ema_label.setText(t("dialog.filter_flow_ema"))
|
||||
self.flow_ema_spin.setToolTip(t("dialog.filter_flow_ema_tip"))
|
||||
self.median_label.setText(t("dialog.filter_median"))
|
||||
|
||||
@@ -257,12 +257,12 @@ def test_applying_plot_settings_writes_the_preferences_and_the_panel():
|
||||
}
|
||||
)
|
||||
assert parts["host"].prefs["plot_window_sec"] == 20.0
|
||||
assert parts["host"].prefs["plot_fn_min"] == -2.0
|
||||
assert parts["host"].prefs["plot_fn_min"] == 0.0
|
||||
assert parts["host"].prefs["plot_shear_raw_max"] == 3e5
|
||||
assert parts["host"].prefs["plot_deadband_n"] == 0.25
|
||||
assert parts["charts"].window_sec == [20.0]
|
||||
assert parts["charts"].limits[-1] == {
|
||||
"fn_min": -2.0,
|
||||
"fn_min": 0.0,
|
||||
"fn_max": 8.0,
|
||||
"shear_max": 9.0,
|
||||
"fn_raw_min": -1000.0,
|
||||
@@ -281,8 +281,9 @@ def test_a_partial_plot_settings_mapping_leaves_the_rest_as_stored():
|
||||
{"window_sec": 6.0, "fn_max": 12.0, "shear_max": 3.0,
|
||||
"fn_raw_max": 4e5, "shear_raw_max": 5e5}
|
||||
)
|
||||
assert parts["host"].prefs["plot_fn_min"] == -5.0
|
||||
assert parts["charts"].limits[-1]["fn_min"] == -5.0
|
||||
# Newton FN floor is always rewritten to 0; other stored values stay.
|
||||
assert parts["host"].prefs["plot_fn_min"] == 0.0
|
||||
assert parts["charts"].limits[-1]["fn_min"] == 0.0
|
||||
assert parts["charts"].deadband_floors[-1]["raw"] == 900.0
|
||||
|
||||
|
||||
|
||||
@@ -524,7 +524,7 @@ def test_the_orientation_preference_survives_a_restart(make_window, qt_app) -> N
|
||||
|
||||
|
||||
def test_the_dialog_carries_the_orientation_to_the_window(make_window, monkeypatch) -> None:
|
||||
"""The control is in the dialog the app already opens, and reaches the prefs."""
|
||||
"""The 2D panel settings dialog carries orientation into prefs."""
|
||||
from orisys_app_shared.prefs import load_preferences
|
||||
from orisys_studio import app_window as app_window_module
|
||||
|
||||
@@ -539,22 +539,12 @@ def test_the_dialog_carries_the_orientation_to_the_window(make_window, monkeypat
|
||||
def exec(self) -> int:
|
||||
return 1
|
||||
|
||||
def settings(self) -> dict:
|
||||
return {}
|
||||
|
||||
def filter_values(self):
|
||||
from orisys_app_shared.force.filters import FilterSettings
|
||||
|
||||
return FilterSettings()
|
||||
|
||||
def orientation(self) -> str:
|
||||
return ORIENTATION_MIRROR_V
|
||||
|
||||
monkeypatch.setattr(app_window_module, "PlotSettingsDialog", _Dialog)
|
||||
monkeypatch.setattr(app_window_module, "FingerOrientationDialog", _Dialog)
|
||||
window = make_window(sensor_type="finger")
|
||||
# No device: the dialog path would otherwise create the acquisition workers.
|
||||
window._ensure_workers = lambda: None
|
||||
window._on_plot_settings()
|
||||
window._on_finger_orientation_settings()
|
||||
|
||||
assert seen.get("finger_orientation") == ORIENTATION_ROTATE_180, (
|
||||
"the dialog was not told what the window is showing"
|
||||
|
||||
@@ -3044,22 +3044,24 @@ def test_the_dialog_writes_the_keys_the_session_reads_back(qt_app):
|
||||
|
||||
|
||||
def test_the_plot_dialog_carries_the_ranges_and_the_zero_threshold(qt_app):
|
||||
"""FN has a lower bound (which may be negative); both units a deadband."""
|
||||
"""Newton FN floor is fixed at 0; both units keep a deadband."""
|
||||
from orisys_studio.ui.plot_settings_dialog import PlotSettingsDialog
|
||||
|
||||
dialog = PlotSettingsDialog(
|
||||
units="newton", fn_min=-2.0, fn_max=6.0, deadband_n=0.25, deadband_raw=2000.0
|
||||
)
|
||||
assert dialog.settings()["fn_min"] == -2.0
|
||||
assert dialog.settings()["fn_min"] == 0.0
|
||||
assert dialog.settings()["fn_max"] == 6.0
|
||||
assert dialog.deadbands() == {"deadband_n": 0.25, "deadband_raw": 2000.0}
|
||||
assert dialog.tabs.count() == 2
|
||||
# Only the half that belongs to the unit being shown can be edited.
|
||||
assert dialog.fn_min_spin.isEnabled() and not dialog.fn_raw_min_spin.isEnabled()
|
||||
assert dialog.fn_max_spin.isEnabled() and not dialog.fn_raw_min_spin.isEnabled()
|
||||
assert dialog.deadband_n_spin.isEnabled()
|
||||
assert not dialog.deadband_raw_spin.isEnabled()
|
||||
assert not hasattr(dialog, "fn_min_spin")
|
||||
|
||||
raw = PlotSettingsDialog(units="raw")
|
||||
assert raw.fn_raw_min_spin.isEnabled() and not raw.fn_min_spin.isEnabled()
|
||||
assert raw.fn_raw_min_spin.isEnabled() and not raw.fn_max_spin.isEnabled()
|
||||
assert raw.deadband_raw_spin.isEnabled() and not raw.deadband_n_spin.isEnabled()
|
||||
raw.fn_raw_min_spin.setValue(-50000.0)
|
||||
raw.deadband_raw_spin.setValue(5000.0)
|
||||
@@ -3094,17 +3096,17 @@ def test_the_plot_dialog_values_reach_the_chart(make_window):
|
||||
window.display_ctl.apply_plot_settings(dialog.settings())
|
||||
|
||||
plots = window.force_plots
|
||||
assert (plots._fn_min, plots._fn_max, plots._shear_max) == (-2.0, 6.0, 3.0)
|
||||
assert (plots._fn_min, plots._fn_max, plots._shear_max) == (0.0, 6.0, 3.0)
|
||||
assert (plots._fn_raw_min, plots._fn_raw_max, plots._shear_raw_max) == (
|
||||
-1000.0, 5e5, 2e5,
|
||||
)
|
||||
assert plots.presentation.min_deadband_for("N") == 0.25
|
||||
assert plots.presentation.min_deadband_for("raw") == 3000.0
|
||||
assert window.prefs["plot_fn_min"] == -2.0
|
||||
assert window.prefs["plot_fn_min"] == 0.0
|
||||
assert window.prefs["plot_deadband_raw"] == 3000.0
|
||||
assert plots.history.window_sec == 6.0
|
||||
# And the dialog would open on the same numbers next time.
|
||||
assert window.display_ctl.plot_settings()["fn_min"] == -2.0
|
||||
assert window.display_ctl.plot_settings()["fn_min"] == 0.0
|
||||
window.close()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user