Skip to content

DVK panel on the AetherSDR style guide, in the title bar's vocabulary #6249

Closed

Description

@ten9876

Summary

Move the Digital Voice Keyer panel (src/gui/DvkPanel.cpp) onto the AetherSDR style guide (RFC #6226). It should use the same vocabulary as the unified title bar (#6198). This covers the controls, structure and states as well as colour. Requested by the maintainer as a theming pass on the whole panel.

Today

  • Hard-coded colours. The panel has 11 of them (#1a2a3a, #00b4d8, #c8d8e8, #6a8090, #505060, #0f1520, #203040, #1a2a4a, #cc3333, #33aa33, #3388cc). It also uses pre-canon tokens.
  • Boxes inside boxes. Each row's QFrame { … } rule also matches its QLabels, because QLabel is a QFrame subclass. So every name and length label is drawn as a bordered box inside the row box.
  • Red and green checked states. REC turns red and PLAY turns green when checked. The canon reserves red for transmit indicators, and the one control that keys the radio (PLAY) should read like MOX.
  • Buttons that can't act stay enabled. STOP is enabled while idle, and PLAY and PREV are enabled on an empty slot.

Direction

  • Surfaces: a canon.raised panel. Slot rows become canon.nested cards with a canon.line hairline and an 8 px radius. The selected row uses the title bar's active-tab pair (titlebar.tab.active.*).
  • Header: modelled on a title-bar radio tab. The title in ink, with a status line underneath led by a status dot.
  • Controls: F-keys and transport buttons use the canon secondary-control recipe with 4 px corners, at 22 / 24 px heights. Hover is aqua and the focus ring is aqua.
  • States:
    • PLAY and the slot on the air use the MOX family (tx.mox.*, background.tx).
    • Recording and preview use the canon cyan active treatment.
    • Refusals show in accent.danger, always with the word "failed".
  • Menu: the slot menu uses the title bar's rounded menu.

PLAY being amber and REC/PREV cyan is provisional; the maintainer will adjust after review.

Activity

  1. self-assigned this
    on Oct 7, 2026
  2. added
    claude-activeAetherClaude is actively working on this issue
    GUIUser interface
    maintainer-reviewRequires maintainer review before any action is taken
    on Oct 7, 2026
  3. aethersdr-agent commented on Oct 7, 2026

    @aethersdr-agent
    Contributor

    Triage: maintainer-review + GUI

    Checked every claim against src/gui/DvkPanel.cpp (598 lines, one file), the style guide, and the theme JSON. The issue is accurate; below are three corrections, then the things the fix has to handle that the Direction section doesn't name.

    Tokens first: every token the Direction names resolves in both default-dark.json and default-light.json — canon.raised/nested/line/control/ink/inkSoft/muted/cyan/aqua/onAccent, titlebar.tab.active.background, titlebar.tab.active.border, titlebar.tab.hover, tx.mox.border, tx.mox.text, background.tx, accent.danger. No need to re-check. One gap: there is no titlebar.tab.active.text — the active-tab pair is fill + border only, so the selected row's text has to be canon.ink (that is what RadioTabBar.cpp:325-326 does too).

    Corrections to "Today"

    1. 14 literals, not 11. The list is missing #253545 (the hover fill, L22 and L34) and #000 / #fff (text-on-accent, L23/L35/L141/L151/L157). All 14 are counted by tools/audit_colours.py, which static-checks.yml:425 runs as --compare-src <merge-base> --strict, so the ratchet is a strict set/count comparison against the PR base — this PR lands net-negative, which is the easy part.
    2. Boxes-in-boxes is real and worse than described. QFrame { … } is a type selector, so it matches QFrame and its subclasses descending from the widget it is set on — QLabel is a QFrame subclass, so both nameLabel and durLabel pick up the row's background and 1px solid border. The per-label QLabel { color: … } rules (L79/L83) set no border, so they do not cancel it. Fix is one exact-class or object-name selector (.QFrame { … } / QFrame#dvkSlotRow { … }), not border overrides on every label.
    3. "Buttons that can't act stay enabled" — confirmed: there is no setEnabled call anywhere in the file. Note the knock-on: the blockSignals/setChecked(false)/blockSignals dance at L190 and L198 exists only because PLAY/PREV are clickable on an empty slot, and it refuses silently — no word, no message. Real enablement deletes both branches.

    What the Direction doesn't cover (the substance of the work)

    Row theming is already dead for live-reload. L61 registers the row template through ThemeManager::applyStyleSheet (tracked, re-resolved on themeChanged — ThemeManager.cpp:354), but selectSlot() at L280-282 immediately overwrites all twelve with raw setStyleSheet literals, and the ctor calls selectSlot(1) at L267. Same for the F-key active/idle swap at L310-313. So today a theme switch leaves every row and F-key stale. A straight token substitution into those raw strings would reproduce the bug. Proposed shape: one tracked template per widget class carrying [slotState="selected"] / [slotState="onAir"] / [fkeyState="active"] property selectors, and change state with setProperty + style()->unpolish/polish — house idiom, e.g. RadioTabBar.cpp:593, RxApplet.cpp:2031, TitleBar.cpp:132-133. That also removes twelve full-subtree restyles per click.

    State precedence. The selected row and the on-air row can be the same row. On-air (background.tx / tx.mox.*) must win over titlebar.tab.active.*; a property-selector design needs that ordering decided rather than emergent. Also, per docs/a11y.md and the canon's "state is never colour alone", the selected row and the on-air row each need a non-colour marker — the status line's word covers the transport state, but row selection is colour-only today and would stay colour-only under the Direction as written.

    The status line has five writers. onStatusChanged (L367-369), onElapsedTick (L401-411), the commandFailed lambda (L216), and — the one that will break a dot-plus-text design — DvkWavTransfer::statusChanged wired directly to QLabel::setText at L470-471, plus finished at L473-475. A status line with a dot and an accent.danger state needs a single setStatus(text, State) seam and that direct connect replaced. Keep the #3377 ordering documented at L206-218: re-drive from status first, set the failure text/colour after, or it gets clobbered.

    Enablement must fail open on Unknown. DvkModel::Status is {Unknown, Disabled, Idle, Recording, Preview, Playback} and Unknown is the initial value (DvkModel.h:66) — a radio that has not sent a dvk status yet sits there, and onStatusChanged's default: currently prints "Status: Idle" for it. If the matrix only admits Idle, the panel is dead on arrival on firmware that never sends one. Treat Unknown as operable, matching the existing precedent in DvkAvailabilityGate.h ("fails OPEN while the entitlement is unknown", #4210). Disabled should disable all four transport buttons, which the Direction's bullet doesn't say.

    Suggested matrix (maintainer to confirm):

    Control Enabled when
    REC slot selected, status ∈ {Unknown, Idle, Recording}
    STOP status ∈ {Recording, Playback, Preview}
    PLAY / PREV selected slot durationMs > 0, status ∈ {Unknown, Idle} or its own state
    F-key n slot n durationMs > 0, or slot n is the active playback

    Recompute from selectSlot, onStatusChanged and onRecordingChanged (per-slot recordingChanged fires for every id on bulk load, DvkModel.cpp:163, so recordingsLoaded is not separately required).

    No disabled recipe exists in the canon. docs/style/aethersdr-style-guide.md's secondary-control row gives fill / border / text / hover only. Suggest canon.muted text over canon.control with a canon.line border — flagging it as a maintainer call rather than inventing it.

    The rounded menu is not just a stylesheet. TitleBar.cpp:136-152 (roundMenuTree) sets WA_TranslucentBackground + NoDropShadowWindowHint before the native window exists, and syncMenuCheckColumn pads the check column; the stylesheet at L1153-1159 is only half of it. Both live in TitleBar.cpp's anonymous namespace. Copying them into showContextMenu (L478-535) is the wrong move — extract a shared roundedMenu(QMenu*) helper (e.g. src/gui/RoundedMenu.{h,cpp}) and have TitleBar use it too. The DVK menu is a fresh stack QMenu per invocation, so the before-first-show requirement is satisfied for free. While there: that menu's QMenu::item:disabled rule (L531) is a #505060 literal.

    Geometry / radius deviations, for the record. The guide's card recipe is 12 px radius; the issue asks for 8 px. "The canon does not change control geometry" and the standard control height is 22 px; the issue asks for 22 and 24 px. Both are the maintainer's call, just noting they are departures from the written guide, not applications of it.

    Proposed change set (one PR)

    • src/gui/DvkPanel.cpp — the four hard-coded style constants (L19-35) and the eight inline stylesheets move to ThemeManager::applyStyleSheet templates on canon tokens; the raw setStyleSheet calls in selectSlot/onStatusChanged/onRecordingChanged become property + repolish; progress-bar chunk colours become background.tx (playback) and canon.cyan (record/preview); single setStatus() seam; enablement wired in.
    • New src/gui/DvkTransportState.h — the enablement matrix as a pure function, modelled on DvkAvailabilityGate.h, with a tests/dvk_transport_state_test.cpp. There is no DvkPanel GUI test today (the three dvk_* tests cover the gate and WAV transfer), and this matrix is the only part that can silently regress.
    • New shared rounded-menu helper, with TitleBar.cpp repointed at it.

    Not blockers, but noted

    • default-light.json keeps tx.mox.text at #f0c890 — the dark value — against a background.tx of #ffeacc. The MOX family is unreadable in light mode. Pre-existing and app-wide (the MOX button has it today); theme_manager_test does not pair-check these. Out of scope here, worth its own issue.
    • DVK playback keys the radio but does not pass through beginTxActivity/TxCoordinator — a pre-existing TX-gate gap, unrelated to theming. Separate issue.

    Nothing to ask the reporter; the Direction is specific enough to implement. Holding for aetherclaude-eligible, plus rulings on: the 8 px / 24 px departures, the disabled recipe, selected-vs-on-air precedence, and whether extracting the rounded-menu helper from TitleBar is in scope for this PR.


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


    📍 Citation health check (Foundry Constitution Principle I): 0 resolved, 9 unresolved against /Users/aetherclaude/workspace/AetherSDR at origin/main.

    Unresolved citations (9)
    Citation Reason
    DvkModel.cpp:163 file not found in workspace
    DvkModel.h:66 file not found in workspace
    RadioTabBar.cpp:325-326 file not found in workspace
    RadioTabBar.cpp:593 file not found in workspace
    RxApplet.cpp:2031 file not found in workspace
    ThemeManager.cpp:354 file not found in workspace
    TitleBar.cpp:132-133 file not found in workspace
    TitleBar.cpp:136-152 file not found in workspace
    static-checks.yml:425 file not found in workspace
  4. added a commit that references this issue on Oct 7, 2026
    a9f1208
  5. added 2 commits that reference this issue on Oct 7, 2026
    11dbe6b
    5f86d2f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

GUIUser interfaceenhancementImprovement to existing featuremaintainer-reviewRequires maintainer review before any action is taken

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions