Skip to content

feat(gui): Keyboard Shortcuts is a CanonWindow dialog, and its questions are CanonMessageBoxes. Principle VIII. - #6759

Open
w5jwp wants to merge 8 commits into
aethersdr:mainfrom
w5jwp:feat/shortcuts-canonwindow
Open

w5jwp wants to merge 8 commits into
aethersdr:mainfrom
w5jwp:feat/shortcuts-canonwindow

Conversation

@w5jwp

@w5jwp w5jwp commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

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

The shortcut editor (ShortcutDialog, Settings → Configure Shortcuts...) moves from a hand-styled QDialog to a CanonWindow dialog, following the AetherRX / AetherTX Settings dialogs. Its six QMessageBoxes become CanonMessageBoxes.

It also fixes a data-loss bug on main: with a bound key selected, Return in the Filter field silently cleared that key's binding. QDialog turned Return into a click on the first auto-default button, which is Clear.

  • Window:
    • CanonWindow::Kind::Dialog, still modal over the main window: canon header and ground, corner close button, Esc / ⌘W close, and edge resizing through FramelessResizer. The menu still opens it with exec() and rebuilds the shortcuts when it closes (MainWindow_Menus.cpp is unchanged).
    • Its own theme scope, canon/shortcuts (was dialog/shortcut, which no theme file uses).
    • The canon ModemChrome dialog sheet replaces the hard-coded hex sheet. On top of it:
      • the table sits on a nested card;
      • the combos' lists and right-click menus get their panel back from the sheet's transparent ground. The lists sit on canon.nested and the menu on canon.raised, the pairing six of the seven canon windows on main use (3415c22c). The menu carries the same canon block as TX Band Settings and the other canon windows: item padding, color.control.unavailable for disabled items, and a canon.line separator (583cda8c). Without the disabled rule, an unavailable Undo / Cut / Copy drew in canon.inkSoft, like an available item;
      • the combos draw the canon down-arrow (comboArrowRule()), as the other canon windows' combos do (the old sheet hid it);
      • the legend, key and category labels select on an object name or role property, not a sheet each.
    • setReturnClicksDefault(false): Return in a field presses no button, which is the fix above. A focused button still takes Return.
    • The minimum stays 950×700, which fits a 768 px screen's usable height. The layout's own minimum is 946 px wide offscreen, so the width has 4 px of headroom (maintainer call whether that is enough). The opening size grows from 1100×800 to 1100×840 for the canon header.
  • Keyboard map (KeyboardMapWidget):
    • Tokens resolve through the widget (ThemeManager::color(this, …)), so the map takes the canon scope's backgrounds and accent. It is drawn on a rounded card; category colours are unchanged.
    • It is as tall as its keys (heightForWidth, from the keys' exact extent: 24.76 × 6.67 key units with gaps, where the old estimate was 23.5 × 6.5). On main it took four sevenths of the spare height while its key size came from the width, leaving an empty band below the keys.
    • Past 1100 px the keys stop growing and centre, so a wide or maximised window gives its spare height to the table.
    • A press between or around the keys goes to the window, so the map card drags the window like any other empty area.
    • Unbound keys label in color.text.primary instead of a fixed white, so they read under Default Light; bound keys keep white on their dark category fills.
    • The rows below it get a little more spacing, and the table takes the rest of the height.
  • Questions and notices:
    • Reset All: Cancel / Reset All (gold, Role::Destructive). Cancel is the default, so Return no longer resets every binding; before, Yes was the default.
    • Key Conflict: Cancel / Reassign, with Reassign the default, as Yes was. Cancel puts the Action field back on the key's own action (on main it kept showing the refused one).
    • Import failure, import result, export failure, export result: one OK. A canon box has no icon, so an import with skipped or displaced actions now says "…, with warnings." in its text.
    • All are ScopedChildWidgets, as in the CTR2 window, so the editor closing mid-question tears them down.
    • The file pickers stay the platform's own.
  • CanonMessageBox::setDetailedText(): import reported its parse errors and its skipped and displaced actions in QMessageBox's detailed text, which can run to hundreds of lines. The canon box gains a read-only QPlainTextEdit (canonMessageDetails) on a nested card, at most 180 px tall, with canon scrollbars, hidden when empty. The text can be scrolled and copied, is plain text, and doesn't take the focus from the default button.
  • Docs:
    • docs/style/aethersdr-style-guide.md, dialog-patterns.md and theme-style-guide.md list the window and its scope. dialog-patterns.md describes setDetailedText().
    • docs/user/docs/keyboard-shortcuts.md names the Reassign / Reset All buttons, says Return and Esc cancel Reset All, and says how the editor moves, resizes and closes.
    • The guide's keyboard-shortcuts screenshot is retaken from the demo simulator at the old image's density (1833 px wide). Its alt text says the map fills the upper part of the window. The manifest entry takes the guide's US spelling, so embed_screenshots.py --check passes for the page (14 other pages are out of step on main and are left alone).

Intent

After this merges, the shortcut editor and its questions look and behave like the other canon windows and follow the theme. Every control does what it did before, except Return in a field, which no longer clears a binding. Out of scope: the editor's controls and workflow, the shortcut registry, and the category colours.

Visible changes for the maintainer:

  • Reset All opens on Cancel; Return no longer confirms it.
  • Return in the Filter field presses nothing (on main it cleared the selected key's binding).
  • Import's details are always shown, in a scrolling panel, instead of behind "Show Details…".
  • The canon sheet's look: the action table is in the sheet's monospace font, and buttons and fields are larger (14 px text, 10 × 18 px button padding), as in AetherRX's pages.
  • The keyboard map is as tall as its keys (no empty band) and stops growing past 1100 px.

Constitution principle honored

Principle VIII — Evidence Over Assertion: each behaviour this PR claims has a check that fails when its change is reverted (see the test plan).

Test plan

  • Local build passes (cmake --build build)
    • Built AetherSDR, shortcut_dialog_test and canon_message_box_test on macOS arm64.
  • Behavior verified on a real radio if applicable
    • n/a: the editor has no radio dependency.
    • Drove this branch against the demo simulator (DEMO-0001, isolated settings, offscreen, TX off): opened the editor, grabbed it at 950×740, 1100×840 and 2000×840. Nothing clips at the minimum; at 2000 wide the keys centre and the table keeps its rows.
    • Opened in a normal build on macOS and reviewed by the author.
    • Light theme: grabbed under Default Light from the demo instance. Unbound key labels were nearly invisible (fixed white on a light fill, also on main) and are fixed here.
  • Existing tests pass (CI)
    • Locally, both tests pass: shortcut_dialog_test 20/20 offscreen and 5/5 on the native cocoa platform (Retina display).
    • New shortcut_dialog_test:
      • it is a frameless CanonWindow in the canon/shortcuts scope, with the canon header and the keyboard map;
      • Reset All asks, opens on Cancel, and Escape leaves the bindings unchanged; Reset All resets them;
      • Key Conflict opens on Reassign; Cancel keeps both bindings and puts the Action field back on the key's own action, Reassign moves the key;
      • Return in the Filter field, with a bound key selected, leaves the binding alone;
      • the map is no taller than its keys, and past the cap the table's height doesn't change with width;
      • the minimum height is at most 700, and the layout, with the window's hairline inset, fits within the minimum in both width and height (946 × 595 against 950 × 700 offscreen; 930 wide on cocoa);
      • a press between keys reaches the window; a press on a key selects it;
      • the Filter field's right-click menu is opaque, and the unavailable Undo row's glyph core lies on the line from the panel to color.control.unavailable, at least half way along, so lighter hinting can't blend it out of range;
      • a combo's drop-down list is on color.canon.nested;
      • a modal that is not a CanonMessageBox is closed, so a regression fails instead of hanging.
    • canon_message_box_test gains the details panel: hidden when empty; shown and read-only; 200 lines scroll inside a panel no taller than 180 px; markup kept as text; the default button keeps the focus; hidden again when cleared.
    • Mutation-checked, each failing the named check:
      • Reset All defaulting to Reset All: "opens on Cancel";
      • setReturnClicksDefault(true): the Return-in-Filter check (PTT's binding cleared);
      • removing the width cap: "table height unchanged past the cap" and its 250 px floor;
      • accepting every map press again: "press between keys reaches the window";
      • the 740 px minimum: "minimum height at most 700";
      • truncating the details text: "200 lines scroll";
      • removing the QMenu::item:disabled rule: the Undo glyph core reads #c4d4e8 (canon.inkSoft), 3.53 along the line and 19.6 off it, instead of #3a4a5a;
      • dropping updateSelectedKeyInfo() from the Key Conflict Cancel path: the combo still reads Mute;
      • a 945 px minimum width: "the layout fits within the minimum" (width);
      • the drop-down list back on canon.raised: its ground reads #0a121e instead of #0e1a2a.
    • CI does not run these checks before merge. Neither test is in ci.yml's per-PR ctest -R steps (the per-PR gate is frozen, docs/agents/tests-ci.md); they run post-merge in full-suite.yml. The local runs above are the evidence.
    • Static checks steps run locally pass: engine boundary, generated user-docs reference, test registration, frozen per-PR test gate, QSettings ratchet, theme seed.
    • Colour-audit ratchet (tools/audit_colours.py --compare-src <origin/main> --strict): unique colours 470 (+0), colour references 1786 (−25), setStyleSheet calls 564 (−1), on the branch rebased onto be216ed3. ShortcutDialog.cpp has no hex colour literals left.
    • tools/check_a11y.py on ShortcutDialog.cpp, CanonMessageBox.cpp and KeyboardMapWidget.cpp: no findings.
  • Reproduction steps documented if user-reported bug
    • The Return bug was found in review, not reported. Steps: open Configure Shortcuts, select a bound key on the map (Esc, PTT in the test), click in Filter, press Return; the binding is gone. Reproduced by restoring main's Return handling (setReturnClicksDefault(true), which is plain QDialog::keyPressEvent) on this branch, not on a main build.

Also checked:

  • Docs site: cd docs/user && npm ci && npm run build passes locally (links and anchors).
  • Screenshot capture: capture_screenshots.py was not used for the retake. It runs this untagged shot only against a real radio. The grab reproduced its settings by hand instead: demo instance, isolated settings, offscreen, TX off, QT_SCALE_FACTOR 1.6667, oxipng, and an OCR check for addresses and serials.

Checklist

  • Commits are signed (docs/COMMIT-SIGNING.md)
  • No new flat-key AppSettings calls — use nested-JSON-under-one-key (Principle V)
  • Code is clean-room — not decompiled, disassembled, or reverse-engineered from a proprietary binary (Principle IV)
  • All meter UI uses MeterSmoother (AGENTS.md convention) — n/a, no meters
  • Documentation updated if user-visible behavior changed — docs/style/ and docs/user/docs/keyboard-shortcuts.md. No CHANGELOG.md entry.
  • Security-sensitive changes reference a GHSA if applicable — n/a

🤖 Generated with Claude Code

@w5jwp
w5jwp requested a review from a team as a code owner October 24, 2026 21:18
@w5jwp

w5jwp commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

After:

Image Image Image

@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. #6758 lists nine asks and every one is in the diff: CanonWindow::Kind::Dialog with FramelessResizer (src/gui/ShortcutDialog.cpp:139-153), the canon/shortcuts scope, the ModemChrome canon sheet in place of the hex sheet, the map resolving tokens through its widget and sitting on a rounded card, all six QMessageBoxes gone (grep QMessageBox src/gui/ShortcutDialog.* is empty), Reset All opening on Cancel with a gold accept, CanonMessageBox::setDetailedText(), the map sized to its keys with a cap, and the Return-in-Filter data-loss fix. The platform file pickers are untouched, and the declared non-goals (registry, workflow, category colours) are respected.

Two claims I could check and which hold exactly:

  • The key extent is not an estimate. kKeysW = 22·1.08 + 1 = 24.76 and kKeysH = 5.25·1.08 + 1 = 6.67 (src/gui/KeyboardMapWidget.cpp:43-45) are the true maxima of buildLayout(): the numpad -/+/ENT column at x = 22, w = 1 is the rightmost edge, and both y5 row keys (h = 1) and ENT (y = 4.25, h = 2) bottom out at 6.67. No key clips, and the old 23.5 × 6.5 × 1.08 divisor was over-reserving, which is the empty band the issue describes.
  • The screenshot was really retaken at the old density. keyboard-shortcuts.png goes 1833×1333 → 1833×1400 — exactly 1100×840 × 1.6667, against main's 1100×800 × 1.6667. Same width, new height, 232 KB → 222 KB.

Coverage is at the right layer: shortcut_dialog_test and the canon_message_box_test addition are socket-free CTests driving widgets with injected events, which is where AGENTS.md's test-layer boundary puts GUI state. Both are registered in tests/tests.cmake:2368-2392 and shortcut_dialog_test joins AETHER_SETTINGS_CONSUMERS as the CMake contract requires (it compiles AppSettings through $<TARGET_OBJECTS:aether_test_settings> and isolates with TestSettingsProfile.h). The PR is straight about CI: neither test is in ci.yml's frozen per-PR ctest -R gate, so the green checks on 3ff4b31f (build, check-macos, check-windows, Static checks, Build docs site — all success) prove compilation, not the new assertions.

No socket tests. No QTcpServer/QUdpSocket/QLocalServer/bind/listen/connectToHost, no peer process, no Fake* device in either added or modified test; nothing registered gains network behaviour. Nothing to surface under #5254.

Scope

File(s) Change Claimed? Verdict
src/gui/ShortcutDialog.{h,cpp} QDialog → CanonWindow, canon sheet, six boxes → CanonMessageBox, setReturnClicksDefault(false) Yes In scope
src/gui/CanonMessageBox.{h,cpp} setDetailedText() + kDetailsHeight, details panel and its scrollbar rules Yes (issue asks for it by name) In scope — new shared-widget API, one caller; modest and inside the canon the maintainer already owns
src/gui/KeyboardMapWidget.{h,cpp} heightForWidth + cap, widget-scoped tokens, rounded card, light-theme key labels, press-between-keys ignored Yes In scope. The press-passthrough is the one item not in #6758's text — it comes from CanonWindow's documented contract ("the window moves by dragging any empty part of it", CanonWindow.h:16-17), and the PR body discloses it
tests/shortcut_dialog_test.cpp, tests/canon_message_box_test.cpp, tests/tests.cmake New target + details-panel checks Yes In scope
docs/style/{aethersdr-style-guide,dialog-patterns,theme-style-guide}.md Add the window and canon/shortcuts to the three lists Yes In scope. Overlaps #6757/#6753/#6751/#6749/#6745, which all append to the same lines — whichever lands second rebases there
docs/user/docs/keyboard-shortcuts.md, docs/user/screens.json, .../keyboard-shortcuts.png Button names, chrome, retaken shot, alt text Yes In scope

Everything in the diff is explained by #6758 or by the canon contract. No unrelated file, no build/CI churn, no vendored tree, no CHANGELOG.md entry (correct — it must not have one). The - lines delete only the hand-rolled hex sheet, the six QMessageBox constructions and the per-widget applyStyleSheet calls; no guard, early return, confirmation or symptom-naming comment is removed. The one added confirmation gate (Reset All now opens on Cancel) is a safety improvement the issue asks for.

Preference check: every change to existing UI points at an authority — RFC #6226 / #6239 for the canon look, #6758 (labelled maintainer-review) for the map sizing and the Cancel default, and CanonWindow.h's own contract for the drag passthrough. Nothing here reads as smuggled taste. The two numbers that are the author's own pick rather than the issue's — kMaxSizingWidth = 1100 and the 1100×840 opening size — are both disclosed in the body as visible changes, so they are a maintainer visual call, not a violation.

Security audit (CodeGuard, required): ran, no findings. Every flagged class is a false positive and nine of the eleven listed files are not in this diff at all (the known "file list can be main's tip" case): CS-VLN-CMDI-AMP fires on C++ reference syntax (const QString&, &ShortcutDialog::importShortcuts); CS-VLN-CMDI-PIPE only on PanadapterStream.cpp:907/1574, which this PR does not touch, where | is a Qt flag OR and not a shell (the same idiom this PR adds at KeyboardMapWidget.cpp:318, Qt::AlignLeft | Qt::AlignTop); CS-VLN-CMDI-BACKTICK on backticks inside comments; CS-VLN-SSRF-HEX on colour literals (QColor(0xc0, 0xd0, 0xe0)); CS-VLN-SSRF-LOCALHOST/CS-PII-IP/CS-PII-EMAIL on RFC 5737 example addresses and you@example.com in screens.json alt text; CG-NET-001 on the pre-existing Flex VITA ephemeral-port binds from merged 2a6ee37ec. The one path worth real scrutiny — untrusted CSV error text reaching the new details panel — is clean: QPlainTextEdit::setPlainText has no rich-text mode, the siblings are forced Qt::PlainText including the header title, and ShortcutManager caps the input at 1 MiB / 4096 rows / 512-byte fields before any of it is joined.

Blockers

None.

Nits

All non-blocking.

  1. src/gui/ShortcutDialog.cpp:484-489 — on Cancel the Action combo keeps showing the action you just rejected. The early return skips the updateSelectedKeyInfo() two lines below, so the binding is untouched (correct) but the combo still reads the new action, which is the opposite of what happened. It is pre-existing (main's if (r != QMessageBox::Yes) return; behaves identically) and #6758 puts the workflow out of scope, so this is only worth taking because this is the PR that gives Cancel its name and adds the test that asserts "Cancel keeps it where it was" — and the test checks the manager, not the combo. One line:
                          QStringLiteral("Reassign"), CanonMessageBox::Role::Accept)) {
                 updateSelectedKeyInfo();   // the combo still reads the rejected action
                 return;
             }
  2. tests/shortcut_dialog_test.cpp:239-241 — the minimum-size check covers height but not width. dlg.bodyWidget()->minimumSizeHint().height() <= dlg.minimumHeight() is there; the width twin is not, and width is the dimension the sibling canon migrations tripped on. #6757 had to raise its minimum from 780 to the layout's own because "an explicit minimum overrides the layout's own … so Track and Band would otherwise clip", and #6753 grew 420×260 → 500×320 for the same reason. Here the minimum stays main's 950 while the canon sheet grows the type and the button padding. I make the slack about 20 px (950 − 2·kInset − 28 px margins = 920 body px against the map's 900 px minimumSizeHint), so it very likely does fit — but that is my arithmetic plus your macOS grab at 950×740, and the clipping symptom is silent and font-dependent. One more line in the same block pins it:
     EXPECT_TRUE(dlg.bodyWidget()->minimumSizeHint().width() <= dlg.minimumWidth());
  3. src/gui/CanonMessageBox.cpp:100-105 — setDetailedText() doesn't feed the accessible description, though setInformativeText() three lines above does. The panel carries setAccessibleName("Details"), so it is reachable, but a screen reader hearing the box open gets text + informative text and has to navigate to find the skipped rows. Deliberate is defensible (hundreds of lines is not something to read aloud on open) — worth a one-line comment saying so, since the asymmetry with its neighbour reads as an oversight.
  4. src/gui/KeyboardMapWidget.cpp:42 vs :223 — the key gap is now two constants. kKeyGap = 0.08f (line 42) drives kKeysW/kKeysH, while keyRect() keeps its own local constexpr float gap = 0.08f. They must agree or keys clip against the extent the new heightForWidth reserves. keyRect() can use kKeyGap.
  5. tests/shortcut_dialog_test.cpp:266-337 — the two pixel probes are font- and DPR-dependent. The disabled-Undo check takes the row pixel furthest from the panel colour and allows distance <= 12, which is more robust than an exact match but still assumes the glyph has a near-opaque core; a platform with lighter hinting can blend past 12. And list.pixelColor(QPoint(list.width() - 4, int(rowH * 1.5 * dpr))) scales y by the device pixel ratio but not x — harmless at the offscreen DPR of 1, wrong on a HiDPI runner. Since these land in full-suite.yml rather than the per-PR gate, a flake there costs more than it does here.
  6. src/gui/ShortcutDialog.cpp:547-554 — the keyPressEvent override is now an empty if and a comment. The PR touches it only to swap QDialog:: for CanonWindow::; since the base call is all that is left, the override and its if (ev->key() == Qt::Key_Escape && …) { /* Could use this … in the future */ } can both go.
  7. docs/user/screens.json:1958-1959 — the manifest now mixes spellings. Moving this entry to "colored" to match the guide page is the minimal way to satisfy embed_screenshots.py --check, and the body discloses it, but 101 of the user-doc pages use "colour" to 17 that don't, and screens.json is left with one "coloured" (line 3444) against two "colored". Fixing keyboard-shortcuts.md to British instead would have satisfied the same check and kept both files consistent. Purely a maintainer-taste call.

What I tried to break

Findings below are reasoned from the head checkout at /tmp/aetherclaude/pr-6759; I have no build, no test run and no way to drive the GUI, so nothing here was reproduced at runtime.

  • "The map card drags the window" vs. the drag dying after the first pixel. mousePressEvent now ignores a press off a key so it propagates (QApplication::notify's parent walk) up to CanonWindow::mousePressEvent — but mouseMoveEvent still calls ev->accept() unconditionally with mouse tracking on, which looked like it would swallow every subsequent move and freeze the drag. It doesn't: FramelessMoveHelper::start() either hands off to startSystemMove() (compositor-driven, no further Qt events needed) or calls handle->grabMouse() on the CanonWindow itself (FramelessMoveHelper.h:42), which re-routes the moves away from the map. The test's QCoreApplication::sendEvent(map, &miss) does exercise the real propagation path, not a shortcut.
  • canon/shortcuts silently falling back to the root palette. ThemeManager::lookupRaw does not walk to the nearest existing ancestor — an unknown path drops straight to m_rootScope (ThemeManager.cpp:293-295), which would have made the whole re-skin a no-op. It is safe because theme::setContainer → registerDeclaredContainer → scopeOrCreate materialises canon/shortcuts as a child of canon (ThemeManager.cpp:252-278, 1905-1913), and m_declaredContainers is re-seeded on every theme load (:519), so the chain survives a theme switch. The theme JSON needs no new entry; canon already carries the base-token overrides and the empty child inherits them.
  • Return no longer clicking anything. CanonWindow::keyPressEvent guards on the same "plain Enter" test QDialog uses and ignore()s before the default-button dispatch (CanonWindow.cpp:108-123), and QLineEdit leaves Return unaccepted, so the filter's Return does reach it. Clear really is the first autoDefault button (clearBtn and resetBtn are still the only bottom-area buttons without setAutoDefault(false)), which is why the main bug cleared the binding — so the author's mutation result is consistent. Numpad Enter is covered by the same clause.
  • The width cap leaving a new empty band, or clipping keys. Past 1100 px heightForWidth pins the height, paintEvent takes min(unitFromW, unitFromH) so the unit stops growing, and m_originX centres — but m_originY stays at kMapPadding, so any over-allocated height would pool at the bottom. It can't: the map's vertical policy is Preferred at stretch 0 while the table is stretch 1, so the layout gives the map exactly its heightForWidth and the surplus to the table. Under-allocation degrades gracefully (unit shrinks, keys centre).
  • Teardown and reentrancy through the six boxes. Every box is a ScopedChildWidget, whose destructor deletes through a QPointer so a parent that dies inside exec() isn't double-freed, and confirm() short-circuits on operator bool before touching clickedButton(), so the dangling go is never dereferenced. m_clicked is reset in showEvent, so a reused box can't return a stale answer, and closing by ✕/Esc/⌘W leaves it null → confirm() false. What is not guarded is the resetAllBtn lambda continuing to use this after confirm() returns, but that is the pre-existing modal-exec() pattern used across the codebase and only reachable at app teardown.
  • Scrollbar completeness. ModemChrome ships only QScrollBar:vertical (ModemChrome.cpp:338-351); both new sheets add the horizontal twin, so the NoWrap details panel and the action table are styled in both axes rather than falling back to the OS bar in one.
  • Untrusted text into the new panel — covered by the security audit above; QPlainTextEdit + the three ShortcutManager caps close it.
  • Couldn't check: the author's own mutation runs, the local test passes, the light-theme and 950/1100/2000-px grabs, the npm run build and audit_colours numbers, and whether kMaxSizingWidth = 1100 and the canon type sizes look right. Those are visual or build-time claims; the three green check-* jobs plus Static checks cover compilation and the static gates, and nits 2 and 5 are where I think the remaining risk sits.

Recommendation

Approve with nits — with the caveat that this is a canon-look change, so the maintainer still owns the visual calls the body flags (the 1100 px sizing cap, the larger canon type in the action table, and Reset All opening on Cancel).

The migration is complete and honest: every issue requirement is in the diff, the two measurable claims I checked (the key-extent constants and the screenshot density) are exact rather than approximate, the Return data-loss fix is real and the mutation story for it is self-consistent, and the test additions fail for the right reasons rather than asserting the implementation back to itself. No blockers, and the nits are all one-liners. Next step: take nit 1 and nit 2 (both single lines, and nit 2 closes the gap that bit #6757 and #6753 in this same series), then it is the maintainer's visual sign-off. Thanks for the unusually legible body — the "visible changes for the maintainer" list and the explicit "CI does not run these checks" note made this much faster to review.


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

@aethersdr-agent

Copy link
Copy Markdown
Contributor

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

  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in docs/user/screens.json /tmp/aetherclaude/pr-6759/docs/user/screens.json:2543
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in docs/user/screens.json /tmp/aetherclaude/pr-6759/docs/user/screens.json:4183
  • [MEDIUM] CS-PII-EMAIL — PII detected: email in docs/user/screens.json /tmp/aetherclaude/pr-6759/docs/user/screens.json:3841
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6759/docs/user/screens.json:1124
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6759/docs/user/screens.json:2543
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6759/docs/user/screens.json:2589
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6759/docs/user/screens.json:2623
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6759/docs/user/screens.json:2720
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6759/docs/user/screens.json:2760
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6759/docs/user/screens.json:4183
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/EspFlasher.cpp /tmp/aetherclaude/pr-6759/src/core/EspFlasher.cpp:32
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/EspFlasher.cpp /tmp/aetherclaude/pr-6759/src/core/EspFlasher.cpp:34
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/EspFlasher.cpp /tmp/aetherclaude/pr-6759/src/core/EspFlasher.cpp:35
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/EspFlasher.cpp /tmp/aetherclaude/pr-6759/src/core/EspFlasher.cpp:36
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/EspFlasher.cpp /tmp/aetherclaude/pr-6759/src/core/EspFlasher.cpp:37
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/backends/flex/PanadapterStream.cpp /tmp/aetherclaude/pr-6759/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-6759/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-6759/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-6759/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-6759/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-6759/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-6759/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-6759/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-6759/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-6759/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-6759/src/core/backends/flex/PanadapterStream.h:376
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/KeyboardMapWidget.cpp /tmp/aetherclaude/pr-6759/src/gui/KeyboardMapWidget.cpp:23
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/ShortcutDialog.cpp /tmp/aetherclaude/pr-6759/src/gui/ShortcutDialog.cpp:171
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/ShortcutDialog.cpp /tmp/aetherclaude/pr-6759/src/gui/ShortcutDialog.cpp:242
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/ShortcutDialog.cpp /tmp/aetherclaude/pr-6759/src/gui/ShortcutDialog.cpp:244
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6759/src/models/RadioModel.cpp:77
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6759/src/models/RadioModel.cpp:78
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6759/src/models/RadioModel.cpp:1679
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6759/src/models/RadioModel.cpp:3017
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6759/src/models/RadioModel.cpp:5533
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6759/src/models/RadioModel.cpp:5534
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6759/src/models/RadioModel.cpp:6841
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6759/src/models/RadioModel.cpp:6847
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6759/src/models/RadioModel.cpp:6883
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/models/RadioModel.cpp /tmp/aetherclaude/pr-6759/src/models/RadioModel.cpp:7122

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: $20.3569 · model: claude-opus-5

@w5jwp

w5jwp commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I took nits 1–6 in 6c637a7a. Nit 7 doesn't hold up, and nit 5's x coordinate was already right. Details below.

The branch is rebased onto main (cfe8abb8, after #6768 and #6735). The only conflicts were the three docs/style/* canon-window lists, and each now keeps every entry: … the multiFLEX Dashboard, Radio Setup, TX Band Settings, Connect to Radio, AGC-T Calibration, Keyboard Shortcuts, plus canon/multiFlex, canon/txBand and canon/shortcuts in the theme guide.

1. Cancel on Key Conflict leaves the combo on the refused action. Fixed. The Cancel path now calls updateSelectedKeyInfo() before it returns, so the Action field goes back to the key's own action. You were right that the test checked the manager and not the combo. It now does what a user's pick does (setCurrentIndex on Mute, then activated), refuses, and checks combo->currentData() == "ptt". Mutation: without the call the check fails (shortcut_dialog_test.cpp:196, the combo reads Mute).

2. The minimum-size check didn't cover width. Added the width check. Both checks now also add the window's hairline kInset on each side, because the body sits inside it and comparing the body alone was 2 px loose. Measured:

platform body minimum + inset window minimum
offscreen (what CI uses) 946 × 595 950 × 700
cocoa, Retina display 930 × 595 950 × 700

So the width headroom is 4 px offscreen, not the ~20 px you estimated. It fits, but only just, and it depends on the font. Mutation: a 945 px minimum fails the width check (:255). Whether 950 should get more headroom is a sizing call I've left to @ten9876. It isn't in this commit.

3. setDetailedText() doesn't feed the accessible description. It's deliberate, and the code now says so in a two-line comment: the details stay out of the description, which is read aloud when the box opens, and the panel has its own accessible name ("Details").

4. Two key-gap constants. keyRect() uses kKeyGap now, and the local gap is gone.

5. The pixel probes depend on font and DPR.

  • Undo glyph core: reworked. An antialiased pixel is a blend of the ink and the panel, so it lies on the line between them however light the hinting is. The check is now that the core lies on the line from canon.raised to color.control.unavailable (≤ 8 channel units off it) and at least half way along. The old check was "within 12 of the ink". Measured: 1.00 along and 0.0 off on both offscreen and cocoa. Mutation: without the QMenu::item:disabled rule the core is #c4d4e8, 3.53 along and 19.6 off, and both checks fail.
  • Drop-down probe x: this one doesn't hold up. list is the QImage from grab(), so list.width() is already in device pixels, and width() - 4 is 4 device pixels from the right edge at any DPR. Only rowH (logical, from sizeHintForRow) needed scaling, and it's scaled. To check, I ran the test on the native cocoa platform on a Retina display, 5/5 passes, with the ground reading #0e1a2a = canon.nested.

6. The empty keyPressEvent override. Removed from the .h and the .cpp, along with the QKeyEvent include it needed. CanonWindow::keyPressEvent is the one that runs, and the Return-in-Filter and Escape checks still pass.

7. "colored" vs "colour" in screens.json. This doesn't hold up. The user guide's own rule is US spelling: docs/user/docs/docs-style-guide.md:100 says "Spelling and voice: US spelling (color, behavior, center)". And the count goes the other way: on this branch, 26 pages under docs/user/docs/ use color/colored/colors as words, and only 1 (cw-decoder.md:68, "coloured") uses the British form. Changing the guide page to British would break the written rule, so the manifest takes the guide's spelling. screens.json:3444's "coloured" and cw-decoder.md belong to other pages and are left alone.

Runs on 6c637a7a: shortcut_dialog_test 20/20 (10 with QT_QPA_PLATFORM unset, which the test turns into offscreen, and 10 with it set explicitly), plus 5/5 on cocoa. canon_message_box_test passes on both. AetherSDR builds and links. check_a11y.py on the three files: no findings. check_test_registration.py --strict, check_ci_test_gate.py, check_theme_seed.py and gen_touchpoint_manifest.py --check all pass. Colour ratchet against origin/main: unique 476 (+0), references 1813 (−25), setStyleSheet 588 (−1).

Still for @ten9876, as the body lists: the 1100 px sizing cap, the canon type in the action table, Reset All opening on Cancel, and now whether the 950 px minimum should keep only 4 px of width headroom.

w5jwp and others added 7 commits October 24, 2026 21:59
…ons are CanonMessageBoxes. Principle VIII.

Configure Shortcuts moves from a hand-styled QDialog to a CanonWindow dialog,
following the AetherRX / AetherTX Settings dialogs: canon header and ground,
its own theme scope (canon/shortcuts), the canon ModemChrome dialog sheet in
place of the hard-coded hex sheet, and FramelessResizer so it still resizes
from every edge. It stays modal over the main window, and the menu still
rebuilds the shortcuts when it closes. Return in the filter field no longer
presses Clear or Reset to Default (setReturnClicksDefault(false)).

The keyboard map resolves its tokens through its own widget, so it takes the
canon scope's backgrounds and accent, and sits on a rounded card. Category
colours are unchanged.

All six QMessageBoxes become CanonMessageBoxes:
- Reset All asks with Cancel and a gold Reset All; Cancel is the default, so
  Return no longer resets every binding.
- Key Conflict asks with Cancel and Reassign (the default, as Yes was).
- Import's failure and result notices and Export's notices have one OK button.

CanonMessageBox gains setDetailedText(): a read-only list, at most 180 px tall,
to scroll and copy below the texts, in place of QMessageBox's detailed text.
Import uses it for the parse errors, skipped actions and displaced bindings.
The file pickers stay the platform's own.

New shortcut_dialog_test covers the window's shape and scope, and that Reset
All and Key Conflict change nothing unless their accepting button is clicked.
canon_message_box_test gains the details panel. The style docs and the user
guide list the window; the guide's screenshot is now stale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and the controls below it get room to breathe. Principle VIII.

The map took four sevenths of the spare height against the table, while
its key size was set by the width, so an empty band sat below the keys. It
now reports heightForWidth from the keys' exact extent (24.76 x 6.67 key
units with gaps, not the old 23.5 x 6.5 estimate) and is laid out without
stretch; the keys fill its card and centre if the height ever limits them.

The rows below it are spaced a little more (10 px, 6 px more after the
legend, and between each row's label pairs), and the table takes the rest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…8 px screen, and draws its combo arrows. Principle VIII.

Review follow-ups on this branch:

- The keyboard map's height grew with the window's width with no cap, so a
  wide or maximised editor squeezed the action table to two rows. Past
  1100 px the keys stop growing and centre; the table keeps its height.
- The minimum height returns to 700 px. The canon layout needs less than
  that, and 740 did not fit a 768 px screen's usable height.
- A press between or around the keys is ignored, so it reaches the window
  and drags it, as the user guide says.
- The Action and Category combos draw the canon down-arrow
  (comboArrowRule()), as the other canon windows' combos do.
- CanonMessageBox's details panel styles its horizontal scrollbar.
- An import with skipped or displaced actions says "with warnings" in its
  text, since a canon box has no warning icon.

Tests: shortcut_dialog_test pins Return in the Filter field leaving the
selected key's binding alone (on main it pressed Clear), the map's height,
the table keeping its height past the cap, the 700 px minimum, and a press
between keys reaching the window; answerNextBox closes a modal that is not
a CanonMessageBox so the test fails instead of hanging. The details check
in canon_message_box_test now asserts the 200 lines scroll. Each new check
fails when its fix is reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the user guide shows the canon editor. Principle VIII.

Unbound keys drew their labels in a fixed white over the theme's
background fill, so under Default Light ESC, the F keys and every unbound
letter were nearly invisible. They now use color.text.primary through the
map's scope; bound keys keep white on their dark category fills.

The user guide's keyboard-shortcuts screenshot is retaken from the demo
simulator (DEMO-0001, isolated settings, offscreen, TX off) at the old
image's density (QT_SCALE_FACTOR 1.6667, 1833 px wide), as
capture_screenshots.py would; OCR finds nothing to redact. Its alt text
says the map fills the upper part of the window rather than the top half,
and the manifest entry takes the guide's US spelling, so
embed_screenshots.py --check passes for the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he canon menu rules the other canon windows carry. Principle VIII.

The sheet gave QMenu its canon panel and a cyan selected item, but no rule
for a disabled item, so an unavailable Undo, Cut or Copy drew in
canon.inkSoft, the same ink as an available one, and read as clickable. The
menu now carries the full canon block the other canon windows use: item
padding, color.control.unavailable for disabled items, and a canon.line
separator.

shortcut_dialog_test opens the Filter field's standard menu on an untouched
field and checks the panel is opaque and that the Undo row's glyph core is
color.control.unavailable. Without the disabled rule it is #c4d4e8
(inkSoft) and the check fails. It also checks the two qWaitForWindowExposed
results it was ignoring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the other canon windows' lists do. Principle VIII.

Six of the seven canon windows on main draw a combo's drop-down list on
canon.nested (SpotHub, Network Diagnostics, Firmware Install, AetherControl,
AX.25 HF Packet Decode, Radio Setup) and their right-click menus on
canon.raised. This window had its lists on raised; they now follow the
convention, leaving its menu on raised.

shortcut_dialog_test opens the category filter's list and checks an empty
part of an unselected row is color.canon.nested; with raised it reads
#0a121e and the check fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… own action, and the minimum width is pinned against the layout. Principle VIII.

Cancelling Key Conflict returned before updateSelectedKeyInfo(), so the
binding was kept but the Action combo still showed the action just refused.
It now goes back to the key's own action. shortcut_dialog_test picks the
conflicting action in the combo before it is refused and checks the combo
reads PTT again; without the call it reads Mute and the check fails.

The minimum-size check covers width as well as height, and both now add
the window's hairline inset to the body's minimum: 946 x 595 against
950 x 700 offscreen, 930 wide on cocoa. A 945 px minimum fails the width
check.

The Undo-row probe checks that the glyph core lies on the line from the
panel to color.control.unavailable, at least half way along, rather than
within 12 of the ink, so lighter hinting cannot blend it out of range.
Without the QMenu::item:disabled rule the core is #c4d4e8, 3.53 along and
19.6 off the line, and both checks fail.

keyRect() uses kKeyGap, the gap the map's extent is computed from, instead
of its own copy. The empty keyPressEvent override goes; CanonWindow's is
the one that runs. setDetailedText() says why the details stay out of the
accessible description.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@w5jwp
w5jwp force-pushed the feat/shortcuts-canonwindow branch from 6c637a7 to 96adcb1 Compare October 25, 2026 03:03
@w5jwp

w5jwp commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Rebased again onto main at be216ed3, because #6745 landed while this was being pushed and edited the same three docs/style/* canon-window lists. Each list now keeps both Icom RC-28 Button Mapping / canon/rc28Mapping and Keyboard Shortcuts / canon/shortcuts. There is no code change since 6c637a7a; the commits named in my reply above are now:

  • 6c637a7a → 96adcb1d
  • 023a0bc2 → 3415c22c
  • d88ae957 → 583cda8c

On the new base, AetherSDR, shortcut_dialog_test and canon_message_box_test build. shortcut_dialog_test passes 3/3 offscreen and 1/1 on cocoa, and canon_message_box_test passes. The colour ratchet against be216ed3 is unique +0, references −25, setStyleSheet −1. The body's SHAs and audit numbers are updated.

Resolves the conflicts in the three docs/style/* canon-window lists, where
main added FFTW Wisdom / canon/nr2Wisdom. Every list keeps main's entries
and this branch's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

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.

Keyboard Shortcuts (Configure Shortcuts...) moves to CanonWindow, and its questions to CanonMessageBox

1 participant