Skip to content

feat(gui): Ulanzi Dial — Control Mapping is a CanonWindow dialog re-skinned to the canon palette. Principle VIII. - #6749

Open
w5jwp wants to merge 3 commits into
aethersdr:mainfrom
w5jwp:feat/ulanzi-canon-window
Open

w5jwp wants to merge 3 commits into
aethersdr:mainfrom
w5jwp:feat/ulanzi-canon-window

Conversation

@w5jwp

@w5jwp w5jwp commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #6748. Part of #6239 and approved RFC #6226.

The Ulanzi Dial mapping window (UlanziDialMapperDialog) moves from
PersistentDialog to a CanonWindow dialog (Kind::Dialog), as the
AetherRX / AetherTX Settings dialogs and AGC-T Calibration did:

  • Chrome: no title bar, the ambient ground, the canon header and the
    corner close button. It closes with Close, the corner ✕, Esc or Ctrl+W / ⌘W
    and opens centred on the main window. It no longer follows
    View → Frameless Window, so the Settings → Controllers → Ulanzi Dial
    Mapping...
    action creates it directly instead of through
    showOrRaisePersistent().
  • Title: unchanged, "Ulanzi Dial — Control Mapping".
  • Size: fixed 642×722 (was 640×675), the extra height for the header. It stays fixed rather than installing FramelessResizer as canon tool windows otherwise do: the pills sit at fixed points on the dial picture, so a larger window would only add empty ground. dialog-patterns.md now says so, so the next conversion doesn't have to guess.
  • Look: it sits under the canon/ulanziDial theme scope with the
    ModemChrome Canon sheet the AetherRX / AetherTX Settings dialogs use, plus:
    • the pills: cyan on canon.control, a lineHi hairline, aqua on hover and
      focus, the popup list on canon.nested with a cyan selection;
    • Tuning: and Single tap: in ink;
    • the canvas's own transparent-background rule is scoped to the canvas
      (QWidget#UlanziDialCanvas), so it does not override the body sheet on
      the pills it now holds;
    • the status line coloured from a ulanziState property: connected
      accent.success, disconnected or failed accent.danger, the Linux
      permission warning accent.warning, turned off canon.muted. This
      replaces five raw hex stylesheets and the clearWidgetTracking()
      workaround they needed;
    • the canvas no longer paints its own navy fill, so the ground shows
      through; the painted fallback dial (used when the photo is missing) is
      drawn from color.canon.* and repaints on a theme change.
  • Keyboard and screen readers: Return in a pill no longer presses
    Reset to Defaults (setReturnClicksDefault(false), as a page of
    controls rather than a form; main pressed it), and every pill has an
    accessible name ("Top Left key action", "Tuning action", "Single tap
    action", …). The control labels in those names go through tr() (marked
    with QT_TRANSLATE_NOOP in kPillSpecs). The status line ("Ulanzi Dial
    connection status") and the Last event readout ("Last Ulanzi Dial
    event") are named too.
  • Popup: the Linux "pkexec was not found" notice is a CanonMessageBox
    with its wording unchanged, as the CTR2 popups became in feat(ctr2): the CTR2 window's questions in the canon style, and a restart that beats its answer is not an error. Principle VIII. #6736.
  • Layout: the three top pills, the Tuning row and the dial-press pill used
    to be children of the dialog, placed in dialog coordinates that had the
    canvas offset (10, 37) baked in, and needed a re-layout after show. Every
    pill is now a canvas child placed in canvas coordinates, so the header
    cannot shift them, the re-layout is gone, and the canon sheet on the body
    reaches them. Positions match the previous window.

Docs:

  • docs/user/docs/ulanzi-dial.md says how to close the window.
  • docs/style/* list it among the canon windows and the canon/ulanziDial
    scope. That includes the tool-window list in dialog-patterns.md, with
    the fixed-size note above.
  • Generated reference: tools/docs/gen_reference.py reads a
    QT_TRANSLATE_NOOP("context", "text") or QT_TR_NOOP("text") table entry
    as its text, so the Ulanzi button table in the generated reference keeps
    reading "Top Left", … with the labels marked for translation. Pinned by
    tools/test_gen_reference_literal.py (gen_reference_literal_test).
  • Stale screenshot, to recapture after merge: ulanzi-mapper.png shows
    the old window (the capture tool's passes are Linux-only).

Intent

After this merges the Ulanzi Dial mapper looks and closes like the other
canon windows. Out of scope: mappings, defaults, the backend and dispatch,
and the dial photo not loading from a macOS app bundle (it is not in a
.qrc; the dialog only looks next to the binary — on main too).

Screenshots

Before (main, the current docs shot):

Before

After (this branch, macOS offscreen; dial photo placed in the build bundle
for the shot):

Image

An open pill list, on canon.nested with the cyan selection:

Image

Constitution principle honored

Principle VIII (Evidence Over Assertion) covers the checks below. The visual
direction is RFC #6226's.

Test plan

  • Local build passes (cmake --build build --target AetherSDR, macOS
    arm64, Qt 6.12)
  • canon_window_test, theme_manager_test, theme_seed_test and
    ulanzi_mapping_migration_test pass locally (the last scans this
    file's kPillSpecs for TX defaults).
  • Static checks pass locally: engine boundary, touchpoint manifest,
    gen_reference.py --check (on the rebased head, after the generator
    fix), test registration, CI test gate, qsettings, theme seed,
    network timeouts, command plane.
  • New gen_reference_literal_test (11 cases): QT_TRANSLATE_NOOP with
    spacing, a newline and a comma in the context; QT_TR_NOOP; the
    existing wrappers; and non-literals that must stay None.
    Mutation-checked: without the QT_TRANSLATE_NOOP branch three cases fail
    and --check reports the macro call in the button column; without
    QT_TR_NOOP its case fails.
  • Driven through the automation bridge with isolated settings and
    AETHER_AUTOMATION_NO_TX=1, offscreen and on screen: opened from the
    menu and grabbed (above); a pill's popup opened with showPopup on
    "Tuning action" and grabbed on the canon ground.
  • New ulanzi_dial_mapper_dialog_test (19 checks, socket-free, no backend or device):
    • the mapper is a fixed-size CanonWindow with no FramelessResizer;
    • Return in a pill leaves every mapping as it was;
    • the status line reads Connected (without the " Keyboard" suffix), Disconnected, and off when the dial is turned off in Radio Setup;
    • every pill, the status line and the Last event readout are named;
    • Close, Esc, Ctrl+W / ⌘W and the corner button each destroy the WA_DeleteOnClose window.
    • Mutation-checked: setReturnClicksDefault(true) fails the Return check, and removing the turned-off short-circuit in onConnectionChanged fails the off check.
    • CI does not run it before merge: it is not in ci.yml's frozen per-PR ctest -R steps (docs/agents/tests-ci.md), so the green checks prove it compiles; it runs post-merge in full-suite.yml.
  • tools/check_a11y.py: 0 findings.
  • node tools/docs/test_log_rules.mjs (767 checks, 49 rules) and the
    docs site build (npm ci && npm run build in docs/user) pass locally.
  • Not exercised: a real Ulanzi Dial, the Linux Grant access flow and its
    CanonMessageBox, Windows. The Linux permission state draws in
    color.accent.warning, which measures 2.85:1 on the canon ground in
    Default Light, under 4.5:1. That's the shared warning token, used by many
    widgets, not something this PR introduces.

Checklist

  • Commits are signed
  • No new flat-key AppSettings calls. As a centred dialog it no longer
    saves its geometry, so main.cpp retires UlanziDialMapperGeom at
    startup beside AboutDialogGeometry. feat(gui): MQTT Settings is a CanonWindow dialog, re-skinned to the canon palette. Principle VIII. #6751 edits the same retired-keys
    line for MQTT Settings, so whichever lands second takes a one-line rebase.
  • Code is clean-room
  • All meter UI uses MeterSmoother (n/a)
  • Documentation updated: docs/user/docs/ulanzi-dial.md, docs/style/*
  • Security-sensitive changes: n/a

🤖 Generated with Claude Code

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue fit

Yes, on every acceptance bullet of #6748 — and I checked them rather than taking the body's word. CanonWindow::Kind::Dialog with theme::setContainer(this, "canon/ulanziDial") and makeCanonHeader(); the CanonMessageBox swap with the wording byte-identical; docs/style/* and the user page updated. The acceptance line I expected to fail is the one that holds cleanest: grep '#[0-9a-fA-F]\{6\}' src/gui/UlanziDialMapperDialog.cpp returns nothing — not one hex literal left, including the painted fallback dial.

The one requirement that isn't in #6748 at all is the best part of the PR: setReturnClicksDefault(false). On main Return in a pill pressed Reset to Defaults, i.e. a keyboard user tabbing through the pills could silently rewrite every mapping. That is a real bug fix riding in a re-skin, and it is the thing here with no test (nit 2).

Scope

Everything in the diff is explained by the issue. No unrelated files, no new public surface, no build or CI churn, and the - lines delete only what the + lines replace.

File Changes Claimed? Verdict
src/gui/UlanziDialMapperDialog.{h,cpp} PersistentDialog → CanonWindow, canon sheet, ulanziState property, pills re-parented to the canvas, CanonMessageBox Yes In scope
src/gui/MainWindow_Menus.cpp showOrRaisePersistent() → direct new + WA_DeleteOnClose Yes In scope; required (a CanonWindow satisfies neither setFramelessMode() nor trackPersistentDialog())
docs/style/*.md (3 files) add the window to the canon lists and canon/ulanziDial to the scopes Yes In scope — but see Needs maintainer decision
docs/user/docs/ulanzi-dial.md +2 lines, the close paths Yes In scope

Deleted behaviour I read as carefully as the additions: pillComboStyle(), setStatusStyle() and the ThemeManager::clearWidgetTracking() call all go, and the comment they carried named a real symptom ("a later theme change would repaint a connected or permission line in the disabled colour"). The property-selector replacement is immune to it by construction — the dynamic property survives applyStyleSheet()'s re-polish, so there is nothing left to clear. grep -rn "UlanziDialMapperGeom\|setStatusStyle" src/ tests/ docs/ returns nothing; no stale reader was left behind.

Needs maintainer decision

1. The pick itself — docs/style/aethersdr-style-guide.md:118, docs/style/dialog-patterns.md:237. This is not a code problem; it is the one thing I cannot settle, and the diff asserts it rather than citing it.

The sentence the PR edits says canon covers "the candidates listed on #6239, once the maintainer picks them, and the tool windows the maintainer has moved to it" — and the PR adds itself to the second clause, which is the maintainer's. Reading #6239 directly:

  • Picked: tool windows on CanonWindow ends at AGC-T Calibration (#6372). Ulanzi Dial — Control Mapping is not on it.
  • Candidates: short-lived windows is a closed five (What's New, System Info, Help pages, SWR sweep licence, FramelessMessageBox) and prefaces itself with "Tool and workspace dialogs keep the persistent pattern."
  • The only line that could cover this window, "Persistent tool dialogs (one PR per dialog)", sits under "surfaces whose widgets move from base tokens to color.canon.*" — a re-skin that keeps PersistentDialog, not a chrome move.
  • docs/style/dialog-patterns.md:273-277 closes it: "Use CanonWindow only for windows the style guide covers … Other tool and workspace dialogs keep the persistent pattern above."

#6748 is the contributor's own acceptance spec, labelled maintainer-review, with no maintainer comment on it. On the twin #6745 (Icom RC-28, same author, 15 minutes earlier, identical position) @w5jwp said it himself: "Finding 1 (the pick …) is the maintainer's call." Same answer applies here, and one ruling should cover the batch. @ten9876 — the token re-skin is unambiguously wanted and this PR delivers it; the question is only whether a controller-mapping tool dialog leaves PersistentDialog chrome. If the answer is "re-skin yes, chrome no", most of this diff survives and only the base class and the menu call site change back.

2. FramelessResizer is not installed — src/gui/UlanziDialMapperDialog.cpp:217. docs/style/dialog-patterns.md:275-276: "A tool window on it also installs FramelessResizer, since CanonWindow itself only moves."

    setFixedSize(kDialogSize);

The PR keeps the fixed size instead, which is a coherent choice for a pixel-placed canvas — you cannot usefully resize a dialog whose contents are absolute-positioned — but the PR body doesn't say so, and #6745 was asked for the resizer and took it. Either install it (and the pills need a real layout first) or state in the body that a fixed-size canon dialog opts out, so the next conversion doesn't have to guess.

Blockers

None. No correctness, lifetime, or safety defect found.

Nits (all non-blocking)

  • No test, where the twin shipped one. #6745 added rc28_mapping_dialog_test (21 checks, null encoder, socket-free) with mutation proofs. The seam exists here too — the constructor tolerates manager == nullptr (UlanziDialMapperDialog.cpp:305, the Ulanzi Dial manager unavailable branch), so a widget-level QTest can assert, deterministically and socket-free: CanonWindow base; Return in a pill does not click Reset to Defaults (the regression fix, which nothing currently pins); the five ulanziState values and the enabled()==false short-circuit in onConnectionChanged (:737-741); the pills' accessible names. Per the test-layer boundary this is GUI state, not wire or policy, so it isn't merge-gating — but the one behavioural fix in the PR deserves the same treatment its sibling got.
  • src/gui/UlanziDialMapperDialog.cpp:400 — half-translated accessible name:
                                         : tr("%1 key action").arg(p.defaultLabel));
    p.defaultLabel is QString::fromLatin1(kPillSpecs[i].label) (:391), so a localised build announces a translated frame around an untranslated "Top Left". tr() the label at the point of use, or keep an English-only name deliberately and say so.
  • src/gui/UlanziDialMapperDialog.cpp:238-258 — the status line and the Last event readout get object names but no accessible name. #6745 named its status line RC-28 connection status. Both are live regions a screen-reader user has no other route to; docs/a11y.md doesn't require it for a non-interactive QLabel, hence a nit, and check_a11y.py would not have caught it either way.
  • Light-theme amber, src/gui/UlanziDialMapperDialog.cpp:198 — QLabel#UlanziStatus[ulanziState='warning'] resolves to color.accent.warning, which in Default Light measures 2.85 : 1 against color.canon.ground (#ffb84d… light #d08010 on #f5f5f8), under docs/a11y.md:143's 4.5 : 1 for this 12 px/600 text. Computed by flattening both bundled theme JSONs and resolving the {alias} chains, not eyeballed. This is not the PR's defect — it used the shared token exactly as docs/a11y.md:148 asks, and dozens of widgets use the same one; it wants its own issue alongside #6239's carried contrast question. Worth flagging here only because the amber state is the single path the PR explicitly could not exercise (Linux, blocked dial). Every other pair I measured improves on main: pill text 5.93 → 10.47 dark, disconnected 6.03, connected 10.72, popup 11.61, selection 12.71.
  • Geometry. UlanziDialMapperGeom is now orphaned in every existing settings store — nothing reads or writes it. Disclosed in the body, and #6748 asked for centred placement, so this is the sanctioned behaviour; but #6745 went the other way (setGeometryKey(), legacy base64 read once and rewritten as x,y,w,h), and CanonWindow::restoreGeometryFromSettings() would have carried it under the same key. Two conversions, two patterns, same day — worth one ruling, not two.
  • docs/style/dialog-patterns.md:275 — the PR adds the window to two of the three canon-window lists in docs/style/ but not this one. Arguably correct while the pick is open; flagging so it isn't missed if the pick lands.
  • docs/user/screens.json shot 58 (ulanzi-mapper) still describes the old chrome. Disclosed as "recapture after merge"; its wait_visible/grab target is UlanziDialMapperDialog, which the new setObjectName() at :216 keeps valid, so the capture recipe itself doesn't break.

Security

CodeGuard's 90 findings were audited against the head checkout. All refuted; nothing confirmed. The file list is main's tip, not this PR's diff — PanadapterStream.{cpp,h}, RadioModel.cpp and tools/publish_update_manifest*.py are the work of the commits before this branch point and contain no #6749 line. By rule family:

  • CS-VLN-CMDI-PIPE/-AMP → C++ || and &&, a const QString&, and a Qt accelerator (addMenu("&Window")). No shell.
  • CS-VLN-CMDI-BACKTICK (9 sites) → every one inside a // comment quoting a Flex verb, e.g. // `pan close all`.
  • CS-VLN-SSRF-HEX (24 sites) → protocol sentinels and constants: 0xFFFFFFFF, kNeutralPanStreamIdBase = 0xE1000000u, 0x00001C2D // OUI (FlexRadio).
  • CG-PATH-001 at UlanziDialMapperDialog.cpp:51 → QCoreApplication::applicationDirPath() plus a literal; no external input in scope.
  • The Linux pkexec path (:772-852), checked because it is the only privileged code near the diff, is clean and unchanged in substance: proc->start(pkexec, {"/bin/sh", "-c", kScript, "aethersdr", kRule}) is an argv list, kScript/kRule are static const QString literals with no arg() or concatenation, and the rule text reaches the shell only as a quoted "$1" into printf '%s'. The PR changes only the "pkexec was not found" box.

Credit: security-audit (CodeGuard).

What I tried to break

  • Dialog lifetime, the usual way this pattern dies. WA_DeleteOnClose set by the caller plus a raw member would dangle on reopen. MainWindow.h:1772 is QPointer<UlanziDialMapperDialog>, and the other toucher, MainWindow_Controllers.cpp:415, null-checks before refreshStatus(). QDialog::done() honours WA_DeleteOnClose, so Close (accept()), Esc and ⌘W/Ctrl+W all destroy it and null the QPointer — the same conclusion @w5jwp corrected himself to on #6745, and it matches docs/style/dialog-patterns.md:262-264's "WA_DeleteOnClose + a QPointer". Backend signals are all connect(m_manager, …, this, …), so destruction disconnects them; nothing fires into a dead dialog.
  • The pill re-parenting arithmetic, the one place "positions match the previous window" can silently break. Every moved coordinate is the old one minus exactly (10, 37): (120,100)→(110,63), (260,100)→(250,63), (400,100)→(390,63), (265,557)→(255,520), (185,525)→(175,488), (265,525)→(255,488), (185,557)→(175,520). Uniform, so the layout is internally consistent — and because the pills and the dial image now share the canvas's coordinate system, header height can no longer shift one relative to the other, which is the actual win. Reconstructing the old canvas origin from main's layout (640×675, a ~30 px frameless title bar, AlignCenter in the leftover) gives ≈(10, 39), so any residual is a pixel or two; the screenshot is the real evidence and I cannot measure rendering headless.
  • The fixed-size height budget. 722 − 2·kInset − 24 of margins − 590 canvas − 8 spacing leaves 98 px for the canon header and the button row. makeCanonHeader() is a 17 px/700 label + 8 spacing + a 1 px rule ≈ 32 px, and the button row ≈ 30 px, so ~36 px of slack — the fixed size will not clip the Close button. Width is exact rather than slack: 642 − 2·kInset − 20 = 620, the canvas's fixed width to the pixel.
  • Every theme token in the new sheet and the new paintEvent. Flattened both bundled theme JSONs and resolved the {alias} chains: all 15 of canon.{cyan,aqua,control,nested,raised,ink,inkSoft,muted,line,lineHi,onAccent} and accent.{success,danger,warning} exist as root tokens in both themes. A typo'd token would have left a literal {{…}} in the QSS and silently dropped the whole rule block; none does. canon/ulanziDial has no entry in either theme, but neither does any of the thirteen existing canon/* containers — resolution falls back through canon to root, so that's the established pattern, not an omission.
  • Whether the canvas's own sheet eats the body's. QWidget#UlanziDialCanvas { background: transparent; } carries an id selector that can never match a QComboBox, so the body's QComboBox#UlanziPill rules still reach the pills it now parents — the stated reason for the scoping holds, and the open-popup screenshot confirms the cascade reaches QAbstractItemView too.
  • The status-state ordering trap I expected to find. refreshStatus() calls showDisabledStatus() and then queues reportState(), so a reply arriving later could have repainted a disabled dial as a red "Disconnected". onConnectionChanged (:737-741) short-circuits to showDisabledStatus() when !connected && !UlanziDialMappings::enabled(). Covered.
  • Checked and found nothing: no inventory test or generator enumerates canon windows or canon/* scopes (grep -rn "agcCalibration" tests/ tools/ is empty), so the three doc lists are the only registry and no gate was skipped.
  • Could not check, stated plainly: nothing here was run. There is no build, no test run and no way to drive the GUI from this seat, so every finding above is reasoned from the code at 9ae805bc6, not reproduced. Rendering, the Linux Grant-access flow, light-theme re-skin (which #6745 exercised and this PR's test plan does not mention) and Windows are unverified by me and by the PR.

Socket tests: none added, modified or removed. No QTcpServer/QUdpSocket/QLocalServer/bind/listen/connectToHost and no Fake* peer anywhere in the diff.

Recommendation

Needs maintainer decision — on scope, not on code. The implementation is the cleanest of these conversions I've read: zero hex literals left, the clearWidgetTracking() hack retired by construction rather than worked around, the pill re-parenting fixing the exact coordinate hazard the triage comment predicted, contrast improving on main in nine of ten measured pairs, and a genuine keyboard bug fixed on the way past. I found no blocker.

What it cannot supply for itself is the pick: #6239 doesn't list this window, dialog-patterns.md:276 currently says tool dialogs keep PersistentDialog, and the PR edits that documentation to say otherwise. Concrete next step: @ten9876 rules once on #6239 for the batch (#6745 and #6749 are in the identical position), and in the same breath on the FramelessResizer/fixed-size question and on which geometry pattern a fixed-size canon dialog follows. If the pick lands, this merges with nits — of which the test mirroring #6745's is the one I'd ask for.

Thanks @w5jwp — the token work and the a11y additions here are solid, and deferring the pick on #6745 yourself was the right call.


🤖 aethersdr-agent · cost: $14.4460 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Cisco CodeGuard — static analysis of this PR (91 finding(s))

  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/backends/flex/PanadapterStream.cpp /tmp/aetherclaude/pr-6749/src/core/backends/flex/PanadapterStream.cpp:242
  • [CRITICAL] CS-VLN-CMDI-PIPE — Vulnerability: CS-VLN-CMDI-PIPE in src/core/backends/flex/PanadapterStream.cpp /tmp/aetherclaude/pr-6749/src/core/backends/flex/PanadapterStream.cpp:907
  • [CRITICAL] CS-VLN-CMDI-PIPE — Vulnerability: CS-VLN-CMDI-PIPE in src/core/backends/flex/PanadapterStream.cpp /tmp/aetherclaude/pr-6749/src/core/backends/flex/PanadapterStream.cpp:1574
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6749/src/core/backends/flex/PanadapterStream.h:280
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6749/src/core/backends/flex/PanadapterStream.h:300
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6749/src/core/backends/flex/PanadapterStream.h:145
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6749/src/core/backends/flex/PanadapterStream.h:153
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6749/src/core/backends/flex/PanadapterStream.h:155
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6749/src/core/backends/flex/PanadapterStream.h:164
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6749/src/core/backends/flex/PanadapterStream.h:202
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6749/src/core/backends/flex/PanadapterStream.h:376
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/MainWindow_Menus.cpp /tmp/aetherclaude/pr-6749/src/gui/MainWindow_Menus.cpp:1750
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/UlanziDialMapperDialog.cpp /tmp/aetherclaude/pr-6749/src/gui/UlanziDialMapperDialog.cpp:51
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:77
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:78
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:1679
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:3017
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:5533
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:5534
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:6841
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:6847
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:6883
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:7122
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:9840
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:10233
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:10743
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:11339
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:11340
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:11654
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:11662
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:11827
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:12014
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:12021
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:12099
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:13230
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:13296
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:10748
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:83
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:449
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6749/src/models/RadioModel.cpp:706

Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them.


🤖 aethersdr-agent · cost: $15.5737 · model: claude-opus-5

@w5jwp

w5jwp commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Addressed in 76d5b307 and the body:

  • FramelessResizer: kept the fixed size, now stated in the body and in dialog-patterns.md. The pills sit at fixed points on the dial picture, so a larger window would only add empty ground. The test asserts the opt-out: fixed size, no resizer.
  • Geometry: the mapper opens centred, as Ulanzi Dial — Control Mapping: move to CanonWindow (style guide, RFC #6226) #6748 asked, and main.cpp now retires UlanziDialMapperGeom at startup beside AboutDialogGeometry, so no store keeps an orphan. (feat(gui): MQTT Settings is a CanonWindow dialog, re-skinned to the canon palette. Principle VIII. #6751 edits the same line; whichever lands second takes a one-line rebase.)
  • Test: added ulanzi_dial_mapper_dialog_test (19 checks, no backend or device):
    • fixed-size CanonWindow with no resizer;
    • Return in a pill leaves every mapping alone (fails with setReturnClicksDefault(true));
    • status Connected without the " Keyboard" suffix, Disconnected, and off when turned off in Radio Setup (fails without the short-circuit in onConnectionChanged);
    • every control named;
    • Close, Esc, Ctrl+W / ⌘W and the corner button each destroy the WA_DeleteOnClose window.
  • Accessible names: the pill labels are marked with QT_TRANSLATE_NOOP and read through tr(). The status line and the Last event readout are named.
  • dialog-patterns.md:275: the mapper is now in that list too.
  • Light-theme amber (2.85:1): disclosed in the body as the shared color.accent.warning token's property, not this PR's. It's worth its own issue.

@aethersdr-agent

Copy link
Copy Markdown
Contributor

CI: both reds are the same checker, and the fix is in the doc generator, not in your docs

Thanks for this — the conversion itself is fine, and the two red checks on 76d5b307 are one small tooling gap, not a problem with the dialog.

What's red

Job Failed step
Static checks (run 40138480894) User docs generated reference
Build docs site (run 40138480879) Generated reference pages are current

Both run python3 tools/docs/gen_reference.py --check (static-checks.yml, docs.yml). One fix clears both. Everything else is green: build, check-macos, check-windows and the sanitizer configure all passed, so the code compiles everywhere.

The cause

I reproduced it on your head commit (git fetch origin pull/6749/head, then gen_reference.py --check):

error: generated reference pages are stale; run `python3 tools/docs/gen_reference.py` and commit the result
--- a/docs/user/docs/generated/ulanzi-dial-actions.md
+++ b/docs/user/docs/generated/ulanzi-dial-actions.md
-| Top Left | `top_left` | Unassigned |   |
-| Top Middle | `top_middle` | \[RIT/XIT\] RIT Toggle | `shortcut:rit_toggle` |
+| QT\_TRANSLATE\_NOOP("AetherSDR::UlanziDialMapperDialog", "Top Left") | `top_left` | Unassigned |   |
+| QT\_TRANSLATE\_NOOP("AetherSDR::UlanziDialMapperDialog", "Top Middle") | `top_middle` | \[RIT/XIT\] RIT Toggle | `shortcut:rit_toggle` |

ulanzi-dial-actions.md is generated by scraping kPillSpecs out of src/gui/UlanziDialMapperDialog.cpp. The generator's literal() helper (tools/docs/gen_reference.py:285) unwraps one of STRING_WRAPPERS — QStringLiteral, QLatin1String, QString, tr, QObject::tr — and otherwise falls back to the raw source text. QT_TRANSLATE_NOOP isn't in that list, so wrapping the pill labels turned the Button column into the macro call itself.

Please don't do what the error message says. Running gen_reference.py and committing would make CI green by baking QT_TRANSLATE_NOOP("AetherSDR::UlanziDialMapperDialog", "Top Left") into the user manual's button table. The field splitting is already paren-aware, so only that one column is wrong — the ID, default action and Stored as columns are all still correct.

Why your local run passed. It did, at 9ae805bc6. The QT_TRANSLATE_NOOP marking arrived in the review-response commit 76d5b307 (git log -S QT_TRANSLATE_NOOP confirms), and the check wasn't re-run after it. Sequencing, not method.

Suggested fix — teach the parser the macro. QT_TRANSLATE_NOOP takes the context first and the text second, so it needs its own branch rather than a STRING_WRAPPERS entry (unwrapping it that way leaves a stray , and literal() returns None):

STRING_WRAPPERS = ("QStringLiteral", "QLatin1String", "QString", "tr", "QObject::tr",
                   "QT_TR_NOOP")
# QT_TRANSLATE_NOOP(context, "text"): the text is the second argument.
QT_TRANSLATE_NOOP_RE = r'QT_TRANSLATE_NOOP\s*\(\s*"(?:[^"\\]|\\.)*"\s*,(.*)\)'

and in literal():

    if m:
        e = m.group(1).strip()
    else:
        m = re.fullmatch(QT_TRANSLATE_NOOP_RE, e, re.S)
        if m:
            e = m.group(1).strip()

I tried exactly that on your branch and gen_reference.py --check goes to generated reference pages are up to date (8 pages) with no diff — the table reads Top Left again and the macro stays in kPillSpecs. Adding QT_TR_NOOP costs nothing and saves the next conversion the same trip, since tr is already handled.

Alternative, your call: drop the macro and keep tr(kPillSpecs[i].label). That works at runtime, but at file scope in an anonymous namespace lupdate has no class context to attach the strings to, which is presumably why you reached for QT_TRANSLATE_NOOP in the first place. The parser fix keeps the marking you wanted and makes the tool honest about translation markup, so I'd go that way.

tools/docs/gen_reference.py falls under the * rule in .github/CODEOWNERS, so it stays at @aethersdr/reviewers — no new review tier. There's no self-test fixture for the generator, but a QT_TRANSLATE_NOOP row somewhere exercised would fit the Principle VIII habit if you want one.

CodeGuard, for completeness: CG-PATH-001 at UlanziDialMapperDialog.cpp:51 is a false positive — those three candidates are fixed applicationDirPath()-relative paths and a :/ resource path, with no caller-supplied input. The rest of that scan is in files this PR doesn't touch. None of it relates to the CI failure.

Nice piece of work, and the Return-key fix in particular is a real improvement. ~6 lines in the generator and both checks go green.

🤖 Generated with Claude Code


🤖 aethersdr-agent · cost: $7.2933 · model: claude-opus-5

w5jwp and others added 3 commits October 24, 2026 22:08
…kinned to the canon palette. Principle VIII.

The mapper moves from PersistentDialog to a CanonWindow dialog
(Kind::Dialog), as the AetherRX / AetherTX Settings dialogs and AGC-T
Calibration did: canon header, ambient ground, corner close button, Esc
and Ctrl+W / Cmd+W to close, centred on the main window. It no longer
follows View -> Frameless Window, so the menu action creates it directly
instead of through showOrRaisePersistent(), and as a centred dialog it no
longer saves its geometry.

Look: the canon/ulanziDial theme scope, the ModemChrome Canon sheet the
Settings dialogs use, and canon pills (cyan on control, aqua on hover and
focus). The status line colours from a ulanziState property instead of
hard-coded hex, which also retires the ThemeManager tracking workaround.
The painted fallback dial uses color.canon.* and repaints on a theme
change. The Linux "pkexec was not found" notice is a CanonMessageBox with
its wording unchanged.

Every pill is now a canvas child placed in canvas coordinates (the old
dialog-coordinate pills were offset by the canvas position), so the
post-show re-layout is gone. Positions match the previous window.

The canvas's own transparent-background rule is scoped to the canvas, so
it no longer overrides the canon sheet on the pills and their popup lists.
Return in a pill no longer presses Reset to Defaults
(setReturnClicksDefault(false)), and every pill has an accessible name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…its retired geometry key goes at startup, and a test pins Return and the status states. Principle VIII.

- The status line and the Last event readout get accessible names; a pill's
  name translates its control label too (the labels are marked with
  QT_TRANSLATE_NOOP and read through tr()).
- UlanziDialMapperGeom joins the keys main.cpp retires at startup: the canon
  mapper opens centred and nothing reads it.
- dialog-patterns.md lists the mapper among the canon tool windows and says
  why it keeps a fixed size instead of installing FramelessResizer: its pills
  sit at fixed points on the dial picture.

ulanzi_dial_mapper_dialog_test (19 checks, no backend or device): the mapper
is a fixed-size CanonWindow with no resizer; Return in a pill leaves every
mapping alone (fails with setReturnClicksDefault(true)); the status line
reads Connected (without the Keyboard suffix), Disconnected, and off when the
dial is turned off in Radio Setup (fails without that short-circuit); every
control is named; and Close, Esc, Ctrl+W / Cmd+W and the corner button each
destroy the WA_DeleteOnClose window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…NOOP table entries as their text, so the Ulanzi button table names its buttons. Principle VIII.

literal() in tools/docs/gen_reference.py unwrapped QStringLiteral/tr/...
but not the translation no-op macros, so the kPillSpecs labels marked with
QT_TRANSLATE_NOOP read as raw source and gen_reference.py --check failed in
Static checks and Build docs site. QT_TRANSLATE_NOOP takes the context
first, so it has its own pattern; QT_TR_NOOP joins STRING_WRAPPERS.
tools/test_gen_reference_literal.py pins both, registered in
tests/tests.cmake as gen_reference_literal_test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@w5jwp
w5jwp force-pushed the feat/ulanzi-canon-window branch from 76d5b30 to 0b84f76 Compare October 25, 2026 03:33
@w5jwp

w5jwp commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, that was it. Fixed in 0b84f763 as you suggested, and the branch is rebased onto main.

  • The two red checks (Static checks and Build docs site, both gen_reference.py --check): confirmed from both CI logs. The diff was only the Button column, which held the raw QT_TRANSLATE_NOOP(...) call. I didn't regenerate. literal() in tools/docs/gen_reference.py now reads QT_TRANSLATE_NOOP("context", text) with its own pattern, since the text is the second argument, and QT_TR_NOOP joins STRING_WRAPPERS. The macro stays in kPillSpecs, and --check reports generated reference pages are up to date (8 pages) with no change to ulanzi-dial-actions.md.
  • Test: new tools/test_gen_reference_literal.py, registered in tests/tests.cmake as gen_reference_literal_test (11 cases). It covers QT_TRANSLATE_NOOP with extra spacing, a newline, and a comma inside the context string; QT_TR_NOOP; the existing wrappers; and three non-literals that must still return None (a bare identifier, and QT_TRANSLATE_NOOP with an identifier as either argument).
    • Mutation-checked: without the QT_TRANSLATE_NOOP branch, 3 of 11 fail and --check reports the same stale diff as CI. Without QT_TR_NOOP, 1 of 11 fails.
    • 5/5 runs native and 5/5 with QT_QPA_PLATFORM=offscreen, together with ulanzi_dial_mapper_dialog_test.
  • "Why your local run passed": right. I ran the check at 9ae805bc and not again after the review-response commit. The body now says where it was run.
  • Rebase: main had added the multiFLEX Dashboard and TX Band Settings to the same canon-window lists in docs/style/*, and update_stager_test to AETHER_SETTINGS_CONSUMERS. All entries are kept on both sides. The fixed-size note in dialog-patterns.md is unchanged.
  • Re-run on the rebased head: gen_reference.py --check, check_test_registration.py, check_ci_test_gate.py, gen_touchpoint_manifest.py --check, check_theme_seed.py, check_a11y.py (0 findings), node tools/docs/test_log_rules.mjs (767 checks, 49 rules), and the docs site build (npm ci && npm run build). Before, the body listed the last two as not run. AetherSDR builds and links on macOS arm64.
  • CodeGuard CG-PATH-001: agreed it's a false positive, as you say. Fixed paths, no caller input.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ulanzi Dial — Control Mapping: move to CanonWindow (style guide, RFC #6226)

1 participant