# Studio — UI review: logic, look and information clarity (2026-10) Status (2026-10-18): L1, L2 (rail hint and empty-state copy), L3 and A1 are implemented in the reviewed `orisys-app` worktree; see [Implemented](#implemented-2026-10-18). Everything else below is still a proposal. A touch-screen audit is in [section D](#d-touch-screen-readiness). Maintenance moved to `apps/studio/docs` on 2026-10-18. This move preserves the dated review; current implementation and commit status must be rechecked before starting an item. Source history: workspace commit `5ab0beb`, same filename. This review covers live, replay and calibration UI. An operator meeting it cold cannot tell *what to do next* or *why a control is grey*. The biggest wins are not visual polish: they are (1) making the next action obvious, (2) removing engineering vocabulary from operator-facing text, and (3) fixing one regression of the September customer feedback that is still visible in the default configuration (see L1). Evidence: Six screenshots of the source build (`apps/studio/run.py`, 2026-10-18, dark theme, `zh_CN`, sensor `finger`, no device attached): live view, 绘图设置, 三维显示设置, 手指 2D 显示方向, 传感器与标定, and the 标定 view. Copies are in workspace-root `.scratch/studio-ui-review-claude/shots/` (disposable). Findings were checked against `orisys-app` `main` at `dc5a6f8`. Not covered: the replay view, the light theme, a running session with live data, and the `en_US` locale — each needs its own pass. Severity: **P1** misleads or blocks the operator · **P2** slows or confuses · **P3** polish. --- ## Top priorities | # | finding | sev | effort | |---|---|---|---| | L1 | FN curve still sits on the FX frame and has no 0 label by default | P1 | S | | L2 | Nothing says *why* 运行 is disabled, and the rail offers no way to pick the video it waits for | P1 | S | | L3 | Enabled and disabled buttons look almost the same, so the whole rail reads as "off" | P1 | S | | C1 | 传感器与标定 shows internal state (`degraded`, `shipped`, absolute paths, an English SDK warning) | P1 | S–M | | L4 | Calibration lives in two places with overlapping buttons (dialog and 标定 view) | P2 | M | | A1 | Checked checkboxes are a filled square with no tick | P2 | S | --- ## A. UI logic ### L1 — the "no origin / FN on the FX frame" bug is still the default (P1) The September review items 5 and 19 were closed by making the FN lower bound configurable, but both defaults are `0.0` (`ui/plot_settings_dialog.py:43,48`) and the N-mode floor was then deliberately fixed at 0 (`92e6de6`, 2026-10-07). With the view ending exactly at 0, pyqtgraph does not draw the 0 label and the idle FN trace lies on the border it shares with FX (first screenshot). *Done:* the configured range and ticks are unchanged (the 0 N decision stands); the *view* gets an 8 % margin beyond each bound (`force_plots._display_range`), never crossing zero when the range excludes it, and bound labels may draw past the axis end. ### L2 — disabled 运行 never explains itself (P1) The rail shows `使用摄像机`, which is the *video-mode* group (`toolbar.py:193-200`): the current source is a video file, none is chosen, so 运行 is disabled. The only hint is a tooltip (`run_disabled_video`). The video group contains no "choose video" button; that lives only in 文件 → 打开视频…. The 2D empty state says "请选择视频或摄像机后点击「运行」" but nothing near it does either. *Suggest:* show the source on the rail as one segmented control **摄像机 | 视频文件** followed by the camera id or the file name with a `选择…` button. Put the disabled reason as visible text next to 运行 (e.g. "未选择视频"), and make the 2D empty state contain the two actions as buttons. ### L3 — state is not legible on the rail (P1) `QPushButton#primaryButton:disabled` uses `panel_border` fill and muted text (`theme/qss.py:243`), which on `#0E1320` looks the same as an enabled neutral button. In the screenshot 开始记录, 运行, 停止 and 重置传感器 are all disabled, and it is not obvious. *Suggest:* disabled = no fill, dashed or 40 % opacity border. Only one filled accent button at a time — 运行 when idle, 停止 while running. Move 开始记录 after 运行 (you can only record a running session) and keep it hidden or clearly inert until a session runs. ### L4 — two calibration homes (P2) 导出系数… and 应用到设备 appear both in the 传感器与标定 dialog and at the bottom of the 标定 view; the dialog also has 保存, whose relation to 应用到设备 is unclear. *Suggest:* the dialog chooses *which* calibration to use; the 标定 view *produces* one. Remove export/apply from the dialog, or replace them with a link to the 标定 view. ### L5 — calibration workflow has no order (P2) The 标定 view presents everything at once: channel, weight (default `0.0000` N, a value that can never be valid), duration, 记录, a 0 % progress bar, an empty table, 序列号, five row actions and four file actions. *Suggest:* a three-step layout — ① channel + weight + capture, ② the point table with "fit needs ≥ 2 points (current 0)", ③ serial number + save/export/apply. Disable row actions until a row is selected; separate 全部清空 from the others and confirm it. Rename 记录 to **采集样本** so it is not confused with the session 开始记录 on the rail. ### L6 — inconsistent apply models (P2) 绘图设置 has 确定/取消; 三维显示设置 applies live and has only 关闭; 传感器与标定 has 保存 and no cancel. Pick one rule: live-apply dialogs get 关闭 only and say "更改立即生效"; deferred dialogs get 确定/取消. ### L7 — controls that live in the wrong place (P3) 显示三维视图 sits inside the 3D view's own settings (it belongs in 视图 menu / layout); 复位视角 appears both as ↻ in the pane header and as a button in the dialog; the settings toggle on the rail (设置), the ⚙ on every pane and the 视图 menu are three entry points with no rule for which holds what. ### L8 — disabled fields without a reason (P2) In 绘图设置, FN 上限 / 剪切上限 / 归零阈值(N)are greyed because the sensor is uncalibrated; in 三维显示设置, 色标 and 范围 are greyed. Add one line under each group, e.g. "牛顿量程需要先选择标定". --- ## B. Information clarity ### C1 — internal vocabulary in the sensor dialog (P1) The header block shows `配置档案 orisys.finger.r01 · 描述符配置包 · 来源 shipped`, an absolute path, `SDK 校验: degraded`, and an untranslated SDK message `no calibration selected — calibrated force is unavailable; raw force is available`. *Suggest:* one status line for operators ("可以运行 · 未标定,只能显示原始力"), with the descriptor, root path and SDK check behind a 详情 expander. Map SDK warning codes to locale strings instead of passing text through. ### C2 — terminology drift (P2) One quantity has several names: | concept | names seen | suggest | |---|---|---| | shear force | 切向力 (cards), 剪切 (绘图设置) | 切向力 everywhere | | uncalibrated unit | `raw` (cards, badge), 原始 (settings) | 原始 in `zh_CN` | | calibration for live use | 直播用标定 ("live-stream") | 实时采集用标定 | | 3D pane | 三维网格 (title), 三维表面视图 (empty state) | 三维表面 | | capture a calibration sample | 记录 | 采集样本 | Also word order: "FN 原始上限" vs "原始剪切上限". ### C3 — numbers that are hard to read (P2) Raw axes print `750000`, `-250000`; use `750k` or thousands separators. The x-axis has no unit (s). 全程总览 is a label *below* an empty 0–1.0 overview; hide the overview until there is data, and title it above. ### C4 — the rail summarises the wrong things (P3) `FINGER · sensor.json` shows a file name; an operator needs the sensor and the calibration ("Finger · 未标定"). `使用摄像机` reads as a status ("using the camera") although it is an action to switch to the camera. ### C5 — explanation in a title bar (P3) "手指 2D 显示方向(只影响 2D 画面:3D 视图仍用设计坐标系)" is truncated as a window title. Title "2D 显示方向"; move the note into the dialog body. ### C6 — developer telemetry in an operator dialog (P3) "已显示 1 · 未赶上显示的帧 0 · 合成 0.0 ms · 绘制 1.8 ms" belongs in 帮助 → 诊断 or behind `show_fps`. --- ## C. Aesthetic ### A1 — checkbox and radio states (P2) `QCheckBox::indicator:checked` paints a solid accent square with no tick (`qss.py:481-484`); at 15 px it reads as "disabled" or "indeterminate". Add a tick image, as the spin-box arrows already do (`_arrow_image`). In the 标定 view the selected FN radio renders as a large rounded square ring, unlike the circles beside it. ### A2 — too much chrome, weak primary navigation (P2) Menu bar + brand bar + rail use about 130 px of a 745 px window. The brand bar repeats the product name (ORISYS left, Orisys Studio right), while the actual mode switch 实时 / 回放 / 标定 is the smallest text on screen. *Suggest:* merge brand and tabs into one row, drop the right-hand name, and make the tabs ≥ 13 px with a filled selected state. ### A3 — 3D pane (P2) The header background stops at about 160 px, leaving a two-tone strip; the pane is flush with the window edge while the other panes have margins; an empty strip sits under the view. The idle mesh is a flat grey silhouette with no shading, outline or axis indicator, so it reads as a placeholder blob. Add basic lighting or an edge outline and a small orientation gizmo. ### A4 — chart styling (P3) Subplot left axes and frames are bright white and heavier than the grid, and adjacent subplots draw double borders. Use one muted axis colour, a single separator between subplots, and an accent zero line (now the reference line of the chart). The four icon-only buttons in the chart header (→|, ⛶, ⬚, ⚙) need either labels or a grouped "视图" menu. ### A5 — focus looks like selection (P3) The 2D mode combo and the 时间窗口 field open with an accent border that is the keyboard focus ring; it suggests they are selected or in error. Use a subtler ring, or do not give initial focus to fields. ### A6 — alignment and density (P3) In 三维显示设置 labels wrap ("深度显示阈值") and spin boxes are narrower than combos; in 标定 the "砝码重量 (N)" and "采集时长" labels are not on one edge. Use a two-column form layout with fixed label width. In 绘图设置 split the fields into two columns, **牛顿** and **原始**, so the disabled half is self-evident. --- ## D. Touch-screen readiness Measured from the stylesheet and widgets; target ≈ 40 px for primary touch targets. | # | finding | where | suggestion | |---|---|---|---| | T1 | Rail buttons are ~24 px tall; nav tabs 18 px with 11 px text; unit chip 18 px | `qss.py` `[rail="true"]`, `#navButton`, `#unitBadge` | a **touch density** preference (`compact`/`touch`) that raises these to 36–40 px; `ui_check`'s `top_area_limit = 78` would need a per-density limit — a product decision, not a bug | | T2 | Essential information lives in tooltips (full source path, chart-header icon meaning, disabled reasons) — no hover on touch | `toolbar.py`, `force_plots.py` | visible text or a tap-to-reveal info button; the run reason is now on the rail (done) | | T3 | Chart and 3D zoom are mouse-wheel only; no pinch | `force_plots.py` (`setMouseEnabled`), `scene3d/widget.py` `wheelEvent` | `QPinchGesture`, or on-screen +/− and fit buttons | | T4 | No kinetic scrolling in scroll areas, tables or the sensor-dialog text | no `QScroller` use in `apps/studio` / `shared` | `QScroller.grabGesture(viewport, TouchGesture)` on each scroll area | | T5 | Splitter handles are 6 px | `qss.py` `QSplitter::handle` | ≥ 12 px handles in touch density | | T6 | Spin-box arrows are two half-height targets (~20 px) | `QAbstractSpinBox` | in touch density: side-by-side −/+ buttons, or rely on the numeric keyboard | *Done in this change:* checkbox/radio indicators 18 px with taller rows, spin arrows 22 px wide, scrollbars 12 px, menu rows +4 px, combo-popup rows ≥ 30 px. --- ## Implemented (2026-10-18) | item | change | |---|---| | L1 | `force_plots._display_range`: 8 % view margin, zero-safe; labels may extend past the axis end | | L2 | rail `run_hint` shows the blocking reason in acquire views (replay keeps its empty-state text); live + video reason no longer asks for a file; 2D empty state rewritten | | L3 | disabled buttons: no fill, dashed outline (all four button styles) | | A1 | checkbox tick image; checked radios keep their 20 px outer size (border no longer grows them into rounded squares) | | — | `en_US` `panels.replay_empty` named a button label that does not exist; `ui_check` stub worker updated to the real `start()` keywords | Verification: Linux offscreen (PySide6 6.12) against an untouched HEAD copy with the same stub fix — studio suite fails exactly HEAD's set minus one (the replay-text test now passes), shared identical, `ui_check` 631 checks with the same 2 failures as HEAD (one is Linux font metrics; "replay view is not the live arrangement" is pre-existing and also fails on Windows). Windows run of the targeted tests: App-root `.scratch/studio-ui-quick-fixes/`. --- ## Carried-forward requirements and remaining work The [September plan](../../../../docs/archive/studio/orisys-studio-fix-plan.md) records these customer decisions; they remain requirements, not newly verified implementation claims: - Pause computation and display while keeping the camera connected. - Keep `finger-mems` configuration in Studio, derived from box without calibration; no SDK addition. - Keep camera preferences in `config/`, use app-local discovery and scan at start-up. - Keep force settings per channel and separate device/tracker reset from view reset. - Treat housing as a separate part, hidden by default while debugging. | remaining item | status and acceptance boundary | |---|---| | September C: camera list and auto-pick | Previously not started; reconfirm current code. Persist names/preferences, scan off the GUI thread and avoid scans while a camera is in use. Stubbed tests must cover selection, missing stored IDs and UI responsiveness. | | September display requests | Recheck valid-mask boundary stability, palette/LUT parity, per-axis unit labels and 2D/3D arrow appearance before dispatch; do not infer completion from the old plan. | | Device registration and orientation | September items 8, 14, 15 and 17 still need device evidence. A device session needs the equipment, bounds and stop conditions required by workspace rules. | | Latency | Item 9 needs paired input/configuration and frame-identity evidence for packet-to-paint delay. Synthetic or offscreen checks alone do not establish live-device latency. | Current contracts: [layout](ui-layout.md), [palette](ui-palette.md), [bundles](sensor-bundles.md) and [recording](record-channel.md). Inspect the current [window/controller](../src/orisys_studio/app_window.py) and relevant [UI tests](../tests/test_ui_smoke.py), [pause tests](../tests/test_pause_controls.py) and [UI checker](../tools/ui_check.py) before changing them. These are owners and check entry points, not evidence that this migration exercised the UI or hardware. ## Suggested order 1. ~~L1, L2, L3, A1~~ — done. 2. C1, C2 — one locale sweep plus an SDK-code → message map. 3. L4, L5 — calibration restructure; needs a short agreement on the workflow first. 4. A2–A6, L6–L8, C3–C6 — polish, batchable with `ui_check.py` assertions (rail elision at 1280×720 still applies to any rail change). Historical sources: [end-user comments](../../../../docs/archive/studio/orisys-studio-enduser-comments.md) and [fix plan](../../../../docs/archive/studio/orisys-studio-fix-plan.md).