Skip to content

fix(a11y): status-bar indicators work from the keyboard and read as buttons. Principle VI. - #6267

Merged
ten9876 merged 3 commits into
mainfrom
fix/statusbar-indicators-keyboard
Oct 8, 2026
Merged

ten9876 merged 3 commits into
mainfrom
fix/statusbar-indicators-keyboard

Conversation

@ten9876

@ten9876 ten9876 commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Closes #6257. Related to #4896.

The ten click-handled status-bar indicators (band stack, +PAN, TNF, CWX, ASR, DVK, FDX, TUN, AMP and TX) were QLabels, or containers of labels, that only a mouse could use. That's the interactive-QLabel anti-pattern in docs/a11y.md.

A new StatusIndicator helper (src/gui/StatusIndicator.{h,cpp}) is attached to each indicator:

  • Tab reaches it. Focus is Qt::TabFocus, so a mouse click still doesn't take focus. Disabled indicators stay out of the Tab order, as Qt does.
  • Return or Enter runs its action, once per press; auto-repeat from a held key is ignored.
  • Space is left to PTT. MainWindow's app-wide filter binds Space to PTT (Hold), and it runs before any widget's filter. So with a radio connected, Space keys PTT on a focused indicator exactly as it does on every other button. The helper never claims Space.
  • Screen readers see a Button with a Press action. TNF, FDX, CWX, ASR, DVK and the band stack are checkable buttons, and they report on/off state wherever the UI restyles them, so the state isn't carried by colour alone. TUN and AMP lead their accessible description with their state word (OPERATE, BYPASS, STANDBY).
  • It still works if a screen reader is already running. Naming an indicator then makes Qt cache a plain-text interface first. The helper retires it (ObjectDestroyed), and the next query builds the Button (ObjectCreated).
  • Focus mark: a 2 px color.canon.cyan underline while it has keyboard focus. It's a child bar, not a stylesheet rule, because the indicators restyle themselves with whole-sheet setStyleSheet() calls.

One action path. MainWindow::activateStatusIndicator() holds the ten actions, moved verbatim from the event filter's press branches, so mouse and keyboard run the same code. TX still answers only the left mouse button. The ASR indicator is compared only under AETHER_ASR_ENABLED, which check-windows builds without.

Accessible names were added to band stack, TNF, CWX, ASR and FDX. The other five already had them.

Maintainer rulings:

  • all ten indicators
  • keep them as QLabels with a helper, rather than converting them to buttons
  • a cyan underline as the focus mark
  • Return/Enter, with Space left to PTT
  • report on/off state
  • one shared mouse/keyboard dispatcher
  • disabled indicators stay out of the Tab order
  • accept the new ThemeManager.h include

Constitution principle honored

Principle VI. A focused status indicator never turns Space into anything but the PTT behaviour every other button already has. A test pins it behind the real PTT decision.

Test plan

  • status_indicator_test (33 cases, offscreen):
    • Tab focus policy
    • Return/Enter activate; Space, other keys, auto-repeat and a disabled indicator don't
    • Button role, name, and a Press action that activates
    • a container of labels activates the same way, and its inner labels stay plain text
    • checkable state, with one StateChanged per real change
    • when accessibility is already active: interface replacement, with ObjectDestroyed before ObjectCreated
    • underline placement, visibility and focus behaviour
    • behind an app-level filter running the real pttHoldKeyStep() with a radio connected: Space on a focused indicator is PTT and doesn't activate it; Return activates it and keys nothing
  • Checked by inverting each fix:
    • Space put back into the helper: 5 checks fail
    • checked-state reporting removed: 2 fail
    • state-change event removed: 3 fail
    • interface replacement removed: 5 fail
    • auto-repeat guard removed: the held-key check fails
  • ASR off (as check-windows builds): the MainWindow sources compile. Restoring the old comparison reproduces "'m_asrIndicator' was not declared in this scope".
  • Demo session (DEMO-0001, AETHER_AUTOMATION_NO_TX=1, isolated settings): transmitting=false, the indicators carry their names, and a click on the band stack opens its panel.
  • Full ctest: 673/673. The colour ratchet is flat, and the gates match the merge base apart from the two new files.
  • Touchpoint: core/ThemeManager.h gains one includer, StatusIndicator.cpp, for the underline's colour token (maintainer-accepted).
  • Not verified:
    • A real screen reader (Orca, NVDA, JAWS). In particular, whether each one re-queries after ObjectDestroyed/ObjectCreated.
    • check_a11y. It can't see this change, because the indicators are declared in the header and handled in another file, so its silence isn't evidence.

Checklist

  • Signed commits
  • No new settings keys or dependencies
  • Colours through ThemeManager tokens (color.canon.cyan)
  • No CHANGELOG entry

🤖 Generated with Claude Code

…uttons. Principle VIII.

Closes #6257.

The ten click-handled status-bar indicators (band stack, +PAN, TNF, CWX,
ASR, DVK, FDX, TUN, AMP and TX) were QLabels and label containers that
only a mouse could use, the interactive-QLabel anti-pattern in
docs/a11y.md. A screen-reader user could not open the DVK panel, cancel
transmit or switch full duplex from the status bar.

- New StatusIndicator helper (src/gui/StatusIndicator.{h,cpp}), attached
  to each indicator:
  - Tab reaches it (Qt::TabFocus, so a mouse click still takes no focus).
  - Return, Enter or Space runs its action.
  - An accessibility factory reports it as a Button with a Press action
    that runs the same action.
  - A 2 px color.canon.cyan underline shows while it has keyboard focus.
    It is a child bar rather than a stylesheet rule, because the
    indicators restyle themselves with whole-sheet setStyleSheet() calls.
  - The indicators keep their existing look and layout.
- MainWindow::activateStatusIndicator() holds the ten actions, moved
  verbatim from the eventFilter's press branches. A mouse press and the
  keyboard now run one path. TX still answers only the left button to a
  mouse; the keyboard path is the left-click equivalent.
- Band stack, TNF, CWX, ASR and FDX gain accessible names ("Band stack
  panel", "Tracking notch filters", "CW keyer", "Speech-to-text (Copy
  Assist)", "Full duplex"); the other five already had them.

Maintainer rulings: all ten indicators; keep QLabels and add focus and
keys rather than converting them to QPushButtons; a cyan underline as the
focus mark.

Tests: status_indicator_test (16 cases, offscreen):
- Tab focus policy
- Return/Enter/Space activate; other keys and a disabled indicator do not
- Button role, accessible name and Press action, and the action activates
- a label container activates the same way, and its inner labels stay
  plain text
- the underline shows on keyboard focus along the bottom edge, hides on
  focus out, and never takes focus or clicks

In the running app (offscreen, isolated profile, no radio), the widget
tree shows all ten indicators with their names, and a click on the band
stack toggles its panel open, closed and open through the shared action
path. The full ctest suite passes (673/673); check_a11y reports nothing;
the gates pass; the colour ratchet is flat. The touchpoint manifest
records one more ThemeManager.h includer (StatusIndicator.cpp, for the
underline token).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876
ten9876 requested review from a team as code owners October 7, 2026 23:55
@ten9876 ten9876 self-assigned this Oct 7, 2026
@aethersdr-agent

Copy link
Copy Markdown
Contributor

check-windows Build: m_asrIndicator referenced outside its #ifdef

Good news first — this is a one-line config guard, not a problem with the design. build (Linux) and the four static/analysis jobs are green, and the break is confined to a single comparison.

Note on method: get_ci_run_log returned only step names for this run, no compiler output, so I read the diff against the Windows job's configure flags rather than quoting a log line. The chain below is from the tree and ci.yml, not from an error message I saw.

The defect

The new predicate tests the ASR indicator unconditionally — src/gui/MainWindow_Shortcuts.cpp:869:

return obj && (obj == m_asrIndicator || obj == m_cwxIndicator || obj == m_tnfIndicator

But that member only exists in an ASR build — src/gui/MainWindow.h:1738-1740:

#ifdef AETHER_ASR_ENABLED
    QLabel* m_asrIndicator{nullptr};  // status-bar ASR (Copy Assist) toggle
#endif

And check-windows is the one job that configures ASR off — .github/workflows/ci.yml ~line 698:

# ASR OFF for this PR sanity check: its job is Windows-only APP
# regressions (WASAPI, Q_OS_WIN), not ASR. ...
"-DENABLE_ASR=OFF",

So on that job AETHER_ASR_ENABLED is undefined, the member is not declared, and the reference is an undeclared-identifier error (MSVC C2065). Linux has ASR on, so the symbol is there and the same line compiles — which is why only the Windows lane is red. This is config scope, not an MSVC quirk: any platform configured -DENABLE_ASR=OFF would fail identically.

Worth saying that the rest of the ASR handling in this PR is already correct: activateStatusIndicator()'s ASR branch is inside the #ifdef (MainWindow_Shortcuts.cpp:877-884), and so is keyboardOperable(m_asrIndicator) in MainWindow.cpp. I checked the other nine indicators (m_cwxIndicator, m_tnfIndicator, m_fdxIndicator, m_bandStackIndicator, m_tgxlContainer, m_pgxlContainer, m_txIndicator, m_addPanLabel, m_dvkIndicator) — all declared unconditionally in MainWindow.h. isStatusIndicator() is the only leaking site.

Suggested fix

bool MainWindow::isStatusIndicator(const QObject* obj) const
{
    if (!obj) return false;
#ifdef AETHER_ASR_ENABLED
    if (obj == m_asrIndicator) return true;
#endif
    return obj == m_cwxIndicator || obj == m_tnfIndicator || obj == m_fdxIndicator
        || obj == m_bandStackIndicator || obj == m_tgxlContainer || obj == m_pgxlContainer
        || obj == m_txIndicator || obj == m_addPanLabel || obj == m_dvkIndicator;
}

That keeps the shared-action design exactly as written and mirrors the guard you already used in activateStatusIndicator().

Reproducing it locally

Your local build has ASR on, so it cannot show this — which is why a full green local ctest (673/673) was consistent with a red Windows lane. An ASR-off configure reproduces it on macOS or Linux, no Windows host needed:

cmake -B build-noasr -DENABLE_ASR=OFF
cmake --build build-noasr --target AetherSDR

Worth keeping that arm around whenever a change touches status-bar members, since AETHER_ASR_ENABLED is the only #ifdef in that member block.

Everything else

  • build (Linux), Static checks, Sanitizer option configures, and both CodeQL Analyze jobs: green.
  • check-macos was still in_progress when I pulled the check runs, so I can't report it either way yet. It builds with ASR on, so this particular line won't be its problem.
  • No reviewer or Copilot comments on the PR to cross-reference.
  • status_indicator_test isn't built by check-windows — that job deliberately links the app plus a handful of MSVC-specific tests rather than the all target — so the failure is in the AetherSDR target itself, not your new test.

Nice piece of work on the underlying change, by the way: routing the mouse press and the keyboard through one activateStatusIndicator() is the right shape, and the child-bar focus mark neatly sidesteps the whole-sheet setStyleSheet() problem that would have eaten a stylesheet rule. Thanks for taking on the interactive-QLabel cleanup — ten indicators is real work.

Reference: docs/a11y.md · src/gui/MainWindow_Shortcuts.cpp · .github/workflows/ci.yml

🤖 Generated with Claude Code


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

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Antares Detector — candidate vulnerable file(s)

  • src/gui/SmartMtrConfig.cpp
  • src/gui/SmartMtrWidget.cpp

The indicatorPosition implementation in src/gui/SmartMtrConfig.cpp calls meterConfig(in.kind).valueToPosition(...) and clamps the result, but the valueToPosition implementation is not shown here and may perform unsafe array indexing/scaling on untrusted input. This can lead to out-of-bounds access or unsafe scaling if valueToPosition uses direct indexing or assumes valid min/max ranges.

Localized by Cisco Foundation AI Antares-1B running locally in the AetherClaude sandbox, seeded by the Cartographer security map. Advisory only — please verify before acting.


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

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Issue fit: partially fixes #6257; recommendation: Request changes. The ten indicators receive focus, key handling and a shared activation path, matching the issue and the maintainer rulings disclosed in the body. However, ASR-disabled builds fail, and starting with accessibility active leaves the original accessibility interfaces cached, so the screen-reader activation requirement is still broken.

Scope audit

File/group Change Claimed? Verdict
src/gui/MainWindow.cpp Attach helpers to ten indicators; add missing names Yes In scope
src/gui/MainWindow.h, MainWindow_Shortcuts.cpp Share mouse/keyboard actions and recognize indicators Yes In scope; blocker 1
src/gui/StatusIndicator.{h,cpp} Focus, key handling, Button/Press interface, token-based underline Yes In scope; blocker 2
CMakeLists.txt Compile the helper into the GUI Yes Necessary wiring
tests/status_indicator_test.cpp, tests/tests.cmake Register socket-free helper coverage Yes In scope; misses active-at-construction accessibility
docs/architecture/aetherd-touchpoints.md ThemeManager includers 167 → 168 Explicitly disclosed In scope

Everything in the diff is explained by the issue. One signed commit, with no unrelated commit history, new setting/dependency/default, or CHANGELOG entry. GitHub verifies the signature. The maintainer's explicit ten-indicator/helper/underline rulings address the UX/design choices.

Blockers

  1. [P1] ASR-disabled configurations do not compile — src/gui/MainWindow_Shortcuts.cpp:869.

    isStatusIndicator() compares against m_asrIndicator unconditionally, but MainWindow.h:1738 declares that member only under AETHER_ASR_ENABLED. The Windows job configures ENABLE_ASR=OFF. I independently obtained its compiler output, rather than relying on the existing bot's inference:

    MainWindow_Shortcuts.cpp(869): error C2065: 'm_asrIndicator': undeclared identifier
    

    Failing required Windows job. This affects every platform building with ASR off. Keep the conditional member comparison under the same guard:

    bool MainWindow::isStatusIndicator(const QObject* obj) const
    {
        if (!obj) {
            return false;
        }
    #ifdef AETHER_ASR_ENABLED
        if (obj == m_asrIndicator) {
            return true;
        }
    #endif
        return obj == m_cwxIndicator || obj == m_tnfIndicator
            || obj == m_fdxIndicator || obj == m_bandStackIndicator || obj == m_tgxlContainer
            || obj == m_pgxlContainer || obj == m_txIndicator || obj == m_addPanLabel
            || obj == m_dvkIndicator;
    }
  2. [P1] Active accessibility caches the original interface before attachment — src/gui/StatusIndicator.cpp:69; construction order at src/gui/MainWindow.cpp:5805, coverage at tests/status_indicator_test.cpp:97.

    The app sets accessible names before keyboardOperable() attaches the helper. With accessibility already active, Qt resolves and caches the original interface during these change notifications. Installing a factory and adding the helper property does not replace a cached interface. Subsequent queries return the original StaticText interface for a label, without a Press action; containers also retain their original role.

    Reproduced on the pinned Qt 6.12, using the PR's actual helper, without a custom accessibility handler:

    active=1
    name-before-attach role=41 expected=43
    cached label: before_role=41 after_role=41 expected_button=43 same_interface=1 press=0
    

    41 is StaticText and 43 is Button. The unmodified test passes 16/16. Moving its existing QAccessible::setActive(true) before widget construction produces 12/16, failing Button role, Press action availability/activation, and container Button role. No native screen-reader speech was measured: this is a deterministic reproduction at the Qt interface consumed by assistive technology.

    Qt's implementation explicitly resolves interfaces during active accessibility updates and consults the cache before factories: Qt accessibility implementation. Invalidate the previous interface after attaching the helper; include an active-before-construction regression. This replacement for the return at line 69 restored 16/16 in the adversarial variant:

        StatusIndicator* helper = new StatusIndicator(widget);
        if (QAccessibleInterface* iface = QAccessible::queryAccessibleInterface(widget)) {
            QAccessible::deleteAccessibleInterface(QAccessible::uniqueId(iface));
        }
        return helper;

    Also verify platform accessibility notifications when replacing an interface that an AT client has already observed.

Non-blocking nit

  • src/gui/StatusIndicator.cpp:117: repeated KeyPress events activate repeatedly. One Space press followed by one isAutoRepeat() event emitted two activations. Consider consuming auto-repeat without activating for these toggle/cycle controls, and cover it with a socket-free test. Holding Space can otherwise repeatedly toggle a panel or cycle TUN/AMP state.

Verification and limits

The new test owns no sockets and contains no synthetic device peer. The original 16 checks passed; removing key activation made five checks fail (11/16), demonstrating meaningful keyboard coverage. Disabled activation, unrelated keys, inner-label roles and underline geometry/focus behavior survived the existing tests. Active-before-construction and cached-interface probes exposed blocker 2; the cache-invalidation variant restored all 16 checks.

Engine boundary remains 102 known findings/zero blockers, command plane remains 368 commands across 32 files, capability records/test registration/frozen CI gate/QSettings checks pass, and a11y lint reports zero findings on all five touched GUI files. Colour counts match base exactly: 582 unique colours, 2487 references, 893 stylesheet calls. No automated code-review skill was available; the existing bot's ASR diagnosis was independently verified. Its unrelated SmartMtr speculation supplies no evidence about this diff and is not adopted.

I independently configured and built AetherSDR and the registered status_indicator_test from the clean PR head e2c62cd98 in /tmp/pr6267-build; the registered test passed (1/1). A real two-indicator widget chain also passed Tab navigation. The earlier adversarial binaries compiled the actual head helper/test against unchanged base support libraries; the subsequent full head build independently passed the original test.

Two isolated offscreen head instances used fresh settings, explicit sockets and AETHER_AUTOMATION_NO_TX=1. Bridge ping reported sha=e2c62cd98, dirty=false; connect local serial DEMO-0001, connect wait, and get radio confirmed the demo, one pan/one slice and transmitting=false. An owned LD_PRELOAD Qt-entrypoint probe in the first instance queried all ten Button/Press interfaces and sent Space/accessible Press into the actual app:

REVIEW keyboard_space panel_before=0 panel_after=1
REVIEW keyboard_space panel_before=1 panel_after=0
REVIEW keyboard_space panel_before=0 panel_after=1
REVIEW accessible_press panel_before=1 panel_after=0

The second instance ran without instrumentation. Bridge clickAt on Band stack panel plus dumpTree returned panel visibility [true], [false], [true], confirming the mouse path still works. Generic invoke click does not support QLabel; coordinate clicks do. Both owned instances (pr6267-head-review, pr6267-mouse-review) were closed through their explicit sockets and exited successfully; the existing operator instance was untouched. I did not build/run a base app; the base's NoFocus/StaticText behavior was inspected, and deleting key activation produced the expected failing behavioral tests.

CI tested a pull_request merge created 2026-10-07 23:55:11 UTC after main's current tip (23:35:09 UTC), so it is not stale against current main. Linux/macOS/static checks pass; required Windows check fails. The author log records 673 selected tests with one skipped; I did not independently rerun the full suite or a native screen reader.

Recommendation: Request changes. Fix both blockers, add the active-at-construction regression and rerun the required Windows job. This review is submitted as COMMENT because the authenticated account (ten9876) is also the PR author, which GitHub does not permit to request changes on its own PR.

…n reader already cached, and ignore key auto-repeat. Principle VIII.

Addresses the review on #6267 (#6257).

- isStatusIndicator() compares m_asrIndicator only under
  AETHER_ASR_ENABLED, where the member is declared. check-windows builds
  with ENABLE_ASR=OFF, and the unguarded comparison failed there with
  C2065. With ASR off, MainWindow.cpp and MainWindow_Shortcuts.cpp now
  compile; restoring the old line reproduces "'m_asrIndicator' was not
  declared in this scope".
- With accessibility already active, setAccessibleName() makes Qt create
  and cache the widget's plain interface before attach() runs, and Qt
  consults that cache before any factory. attach() now drops a cached
  non-Button interface, so the next query builds the Button with its
  Press action. It also sends ObjectCreated, so an assistive client that
  already saw the old object looks again.
- Return/Enter/Space auto-repeat is consumed without acting. These
  indicators toggle panels and cycle TUN/AMP, so holding a key no longer
  repeats the action.

status_indicator_test 24/24. New cases: widgets built and named after
accessibility is active (label and container) become Buttons with a
working Press action, keep their name, and announce ObjectCreated once
each; a held Space is consumed without a second activation. Without the
cache replacement 5 checks fail; without the auto-repeat guard the
held-key check fails. Full ctest passes (673/673); check_a11y and the
gates pass.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876

ten9876 commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

All addressed in 55990b5bc.

Review blockers

1. ASR-off builds don't compile. Fixed with your guard, at MainWindow_Shortcuts.cpp:869: m_asrIndicator is compared only under AETHER_ASR_ENABLED. Proof from a local -DENABLE_ASR=OFF tree (the check-windows configuration):

  • MainWindow.cpp.o and MainWindow_Shortcuts.cpp.o now compile.
  • Restoring the old line reproduces GCC's form of the MSVC C2065: 'm_asrIndicator' was not declared in this scope.

2. A cached interface survives attach(). Fixed in StatusIndicator::attach(). When accessibility is active and the widget already has a cached interface that isn't a Button, the helper:

  • deletes it, so the next query builds the Button with its Press action;
  • sends ObjectCreated, so an assistive client that already saw the old object looks again. That covers your point about platform notifications.

status_indicator_test now builds a label and a container after accessibility is active, names them before attaching (as MainWindow does), and checks that:

  • each comes back as a Button and keeps its name;
  • Press is offered and activates it;
  • each sends ObjectCreated once.

With the replacement removed, 5 checks fail (19/24).

Nit

Auto-repeat. Return/Enter/Space auto-repeat is consumed without acting, so holding a key no longer re-toggles a panel or cycles TUN/AMP. A held-Space event is consumed with no second activation. With the guard removed, that check fails.

Bot comments

  • check-windows: this is the same root cause as blocker 1, fixed as described there.
  • SmartMtr "candidate vulnerable files": not adopted. This PR doesn't touch SmartMtrConfig.cpp or SmartMtrWidget.cpp, and the comment gives no evidence connecting them to this diff.

Verification

  • status_indicator_test: 24/24.
  • Full ctest: 673/673.
  • check_a11y: nothing on the touched files.
  • Engine boundary, capability records, command plane, test registration, CI gate and QSettings checks pass, and the touchpoint manifest is current.
  • The PR description now describes all three changes.

@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

Partially. #6257 asks for Qt::TabFocus, activation with Return or Space, and a Button role on the click-only status-bar indicators. Tab focus, Return/Enter, the Button role and the accessible Press action all land, and the StatusIndicator helper is a genuinely good shape — one action path shared by mouse and keyboard, a child focus bar that sidesteps the whole-sheet setStyleSheet() problem, and the DVK panel finally has a non-mouse entry point, which was the worst part of the issue. Space does not work in the app, for a reason the helper can't see from where it sits (Blocker 1), and the scope expansion past the five styled indicators rests on rulings I can't find (Blocker 2).

Scope

File / group What it changes Claimed by title or body? Verdict
src/gui/StatusIndicator.{h,cpp} New helper: focus policy, key activation, Button role + Press, focus underline Yes In scope
src/gui/MainWindow.cpp (buildUI) keyboardOperable() lambda on ten indicators; five new accessible names Yes In scope
src/gui/MainWindow_Shortcuts.cpp Press branches extracted to isStatusIndicator / activateStatusIndicator Yes In scope
src/gui/MainWindow.h Two slot declarations Yes In scope
CMakeLists.txt GUI_SOURCES += StatusIndicator.cpp Implied In scope — required, the file would otherwise be a dead addition
tests/status_indicator_test.cpp, tests/tests.cmake New registered test, matches the dvk_panel_theme_test block above it verbatim Yes In scope
docs/architecture/aetherd-touchpoints.md core/ThemeManager.h includers 167 → 168 Yes, disclosed explicitly In scope

No CHANGELOG entry, no new settings keys, no new public/protocol surface, no deleted guards. The scope expansion from five indicators to ten is a scope question but it's disclosed in the body, so it's Blocker 2 rather than an undisclosed row here.

Blockers

1. src/gui/StatusIndicator.cpp:124-133 — Space never reaches the helper when a radio is connected; it keys the transmitter instead.

        if (key == Qt::Key_Return || key == Qt::Key_Enter || key == Qt::Key_Space) {

MainWindow is installed as an application-level event filter (src/gui/MainWindow.cpp:1547, qApp->installEventFilter(this)). Qt runs application filters before the receiving object's own filters (QCoreApplication::notify → sendThroughApplicationEventFilters → sendThroughObjectEventFilters), so MainWindow::eventFilter sees every KeyPress before StatusIndicator::eventFilter does. That filter calls handlePttHoldShortcut on every key press (src/gui/MainWindow_Shortcuts.cpp:735), and ptt_hold is bound to Space by default (src/gui/MainWindow_Shortcuts.cpp:1229, QKeySequence(Qt::Key_Space), keysTx=true).

With a focused indicator, pttHoldKeyStep(KeyPress, holdActive=false, shortcutsEnabled=true, textEntryCaptured=false, connected=true) returns KeyTx — a focused QLabel is not text entry — and the branch returns true. src/gui/PttHoldKeyStep.h says so in its own comment: "Consume the bound key so it cannot also activate a button."

So in the primary operating state (connected, shortcuts on), Tab to FDX, CWX, "Cancel transmit" or the AMP stack and press Space → MOX keys for as long as the key is held, and the indicator does not activate. Disconnected, or with shortcuts off, pttHoldKeyStep returns PassThrough and Space works — which is exactly the state the body's one instrumented run was in ("offscreen, isolated profile, no radio"). The body lists keyboard activation in the running app under "Not verified", so this is the gap that check would have closed.

tests/status_indicator_test.cpp:95 (report("Return, Enter and Space each activate it", dvkActivations == 3)) can't see it either: the test installs no application-level filter, so the assertion is true of the helper in isolation and false of the helper in the app. Space is the key assistive technology presses on something advertised as a Button, so a screen-reader user tabbing to the DVK indicator and pressing Space keys the radio rather than opening the panel.

This is also precisely what #6257's triage comment asked to be ruled on — "Return vs Space — needs a decision".

Smallest honest fix: drop Space from the helper and say so in the body. Return/Enter plus the accessible Press action still satisfy the issue, and #6257's ask for Space becomes a maintainer call about whether a focused indicator should steal Space from PTT:

        if (key == Qt::Key_Return || key == Qt::Key_Enter) {

Please don't route the helper through ShortcutManager to resolve this — that couples a generic widget helper to MainWindow. If the maintainer does want Space, it belongs in the app-level filter ahead of handlePttHoldShortcut, and that is a TX-behaviour change deserving its own ruling. Either way there is a deterministic, policy-compliant pin available: tests/window_shortcut_test.cpp:272 already drives ptt_hold through a filter, so "focused helper + connected + shortcuts on → Space does not activate, Return does" is testable at that seam without a fake radio.

2. src/gui/MainWindow.cpp:5743-6207 — the three "maintainer rulings" the scope rests on aren't in either thread.

The body states: "Maintainer rulings: all ten indicators; keep them as QLabels and add focus and keys, rather than converting them to buttons; a cyan underline as the focus mark." I read every comment on #6257 and on this PR. #6257 has one comment, from aethersdr-agent[bot], and it recommends the opposite on scope:

I'd hold this issue to the five indicators that genuinely share the greyIndLg / setIndicatorStyle path. … the TGXL/PGXL containers cycle an amplifier into OPERATE, so giving them a focusable Space target deserves its own look. m_txIndicator is … a different widget with different semantics; separate issue.

#6257 still carries the maintainer-review label. I'm not calling the claim false — the ruling may have arrived somewhere I can't see — but it's unverifiable from the record, and it reverses a documented recommendation. Two of the five added indicators are the ones that comment carved out: m_tgxlContainer (:6134) and m_pgxlContainer (:6172). Tab + a keystroke now cycles a tuner through BYPASS/STANDBY and puts an amplifier into OPERATE, which is the "own look" that was asked for. Routing this to Needs maintainer decision on scope: either link the ruling, or split TUN/AMP (and arguably TX) into a follow-up and land the eight uncontested ones.

Worth noting the keyboard reach for TUN/AMP intersects Blocker 1: with Space taken by PTT, those two cycle on Return/Enter only today.

Nits

  • tests/status_indicator_test.cpp:110-113 — report("a disabled indicator does not activate", ...) calls dvkHelper->activate() directly rather than sending a key to the disabled widget, so it pins the isEnabled() guard inside activate() but not the end-to-end disabled path. Cheap to strengthen: press(dvk, Qt::Key_Return) while disabled, alongside the direct call.
  • src/gui/MainWindow.cpp:10489 — m_dvkIndicator->setEnabled(dvkAvailable). Qt skips disabled widgets in the tab chain, so the accessibleDescription carrying the live unavailability reason (#6247, :10508) is unreachable to a keyboard-only screen-reader user, even though docs/a11y.md:194-213 makes that description the channel the reason must travel on. Tree-browsing AT still finds it, so this is not a regression this PR introduces — but "dimmed with a reason" and "focusable" are in tension and the PR is the first thing to make it visible. Out of scope to fix here; worth a follow-up note.
  • The body cites check_a11y reporting nothing on the touched files as evidence. It can't report anything here: as #6257's own triage established, Check 1 builds its label set per-file from local declarations and looks within ±6 lines of the eventFilter token, and these indicators are header-declared and handled in a different translation unit. A clean run is vacuous for this change, not confirmation.
  • src/gui/StatusIndicator.h:12-15 — the class comment promises "Return/Enter/Space activate it". Needs the same correction as the body if Blocker 1 lands as suggested.

What I tried to break

  • The refactor dropping a guard. Walked all ten extracted branches against their - lines: every if (!x->isEnabled()) return true; became return inside activateStatusIndicator with the filter still returning true, so consumption is unchanged; the TX left-button-only gate survived at the filter (MainWindow_Shortcuts.cpp:856-859) and is behaviourally identical to the old inner check. FDX's command-then-apply-on-reply shape moved verbatim. No guard lost.
  • The ASR-off lane. isStatusIndicator and activateStatusIndicator both guard m_asrIndicator under AETHER_ASR_ENABLED, as does keyboardOperable(m_asrIndicator). The member set in isStatusIndicator matches the ten old press branches exactly — nothing added, nothing dropped. check-windows (the only -DENABLE_ASR=OFF lane) is green on 55990b5, as are build, check-macos, Static checks, Sanitizer configures and both CodeQL jobs.
  • Stylesheet cascade eating the focus bar. The indicators repolish with whole-sheet setStyleSheet(), and Qt sheets cascade to children — but every sheet in that path is QLabel {-selected (MainWindow.cpp:5657-5660, 10396-10398) and the focus bar is a bare QWidget, so none of them match it. The TUN/AMP containers set no sheet at all. The child-bar approach holds.
  • Lifetime and dangling state. StatusIndicator is QObject(widget)-parented, so the aetherStatusIndicator property can never outlive its target; the MainWindow connect takes this as context and the sender dies with the indicator, so the lambda can't fire into a dead widget; doAction null-checks of(widget()). attach() is idempotent via the early of() return, and installFactory runs once behind a function-local static.
  • Inner labels of a container turning into buttons. The factory keys on the helper property, which only the container carries, so TUN/OPERATE stay StaticText — the test pins this at :131-133 and the mechanism agrees.
  • Theme token and test registration. color.canon.cyan resolves in both themes (#005f85 light, #5de3ff dark) — I walked the JSON rather than grepping, since those tokens are nested. The tests.cmake block mirrors the dvk_panel_theme_test registration above it (same link set, THEME_TEST_RESOURCES, offscreen, TIMEOUT 60), and StatusIndicator.cpp is referenced from both GUI_SOURCES and the test target, so neither is a dead addition.
  • Socket tests: none added, modified or removed by this PR.
  • Could not check: no build and no GUI here, so the focus bar's painting and placement inside the TUN/AMP containers (its child is created before their QVBoxLayout and is never laid out), real AT behaviour, and whether NVDA/Orca/JAWS actually re-query on the ObjectCreated the interface replacement sends. The body already flags the last of those honestly. Every finding above is reasoned from the code in the head checkout, not reproduced at runtime — including Blocker 1, which rests on Qt's documented filter ordering plus three source sites rather than an observed keystroke.

Recommendation

Request changes, on Blocker 1 alone — the advertised key does the wrong thing in the state that matters, and what it does instead is key the transmitter. The fix is one line plus a body correction, and the mechanism is a seam the helper genuinely couldn't see from inside itself; a pin at window_shortcut_test.cpp would keep it from coming back. Blocker 2 is for the maintainer, not for you: link the ruling and it evaporates, or split TUN/AMP out and the remaining eight are ready.

Everything else here is solid work. One action path for mouse and keyboard is the right shape, the cached-interface replacement in attach() is a subtlety most implementations miss, and the invert-each-fix test discipline is exactly what Principle VIII is asking for. Ten indicators is real work — thank you for taking the interactive-QLabel cleanup on rather than doing just the DVK one.


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

…tire a replaced interface. Principle VI.

Landing pass on #6267 (#6257).

Space (maintainer: Return/Enter only)
- MainWindow's app-level filter runs before any widget filter and binds
  Space to PTT (Hold), so with a radio connected and shortcuts on, Space
  on a focused indicator keyed MOX instead of activating it, even on
  "Cancel transmit". The helper now takes Return/Enter only and leaves
  Space untouched, so Space means PTT everywhere, as for every other
  button. The class comment and description say so.
- status_indicator_test reproduces MainWindow's PTT decision as an
  app-level filter (the real pttHoldKeyStep(), connected, shortcuts on).
  Behind it, Space on a focused indicator is PTT and does not activate it;
  Return still activates it and keys nothing.

On/off state (maintainer: fold in)
- TNF, FDX, CWX, ASR, DVK and the band stack are checkable buttons:
  StatusIndicator::setCheckable/setChecked report QAccessible checkable and
  checked, and emit one StateChanged per real change. MainWindow sets the
  state wherever it restyles them: TNF global enable, full duplex, an open
  CWX/DVK/ASR panel, and the band stack's visibility. Colour is no longer
  the only carrier of the state.
- TUN and AMP lead their accessible description with the current state
  word ("OPERATE. Press to cycle between OPERATE, BYPASS, and STANDBY").

Smaller
- Replacing a cached interface first sends ObjectDestroyed for the old
  object, then ObjectCreated for the Button, so a client holding the old
  id lets it go.
- The accessibility factory acts only at an object's most-derived class
  level, so every other object and level skips the property lookup.
- The disabled-indicator test sends a real Return key as well as calling
  activate().

Maintainer rulings in this pass: keep all ten indicators (recorded on
#6257); keep the shared mouse/keyboard dispatcher; leave disabled
indicators out of the Tab chain, as Qt does; accept core/ThemeManager.h's
one new includer (StatusIndicator.cpp, for the underline token).

status_indicator_test 33/33. Each guard was checked by inverting it:
- Space back in the helper: 5 fail
- checked-state reporting removed: 2 fail
- StateChanged removed: 3 fail
Full ctest 673/673. ASR-off compiles. Demo bridge session (DEMO-0001,
NO_TX): transmitting=false, the indicator names are present, and a band
stack click opens its panel. Gates match the merge base apart from the two
new files; the colour ratchet is flat.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876 ten9876 changed the title fix(a11y): status-bar indicators work from the keyboard and read as buttons. Principle VIII. fix(a11y): status-bar indicators work from the keyboard and read as buttons. Principle VI. Oct 8, 2026
@ten9876

ten9876 commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

All addressed in be90925ef.

aethersdr-agent review

Blocker 1: Space keys TX. Confirmed and fixed. MainWindow's app-level filter runs first and turns Space into PTT (Hold) when connected with shortcuts on, so a focused indicator never saw it. Per the maintainer's ruling, the helper now activates on Return/Enter only and leaves Space alone, so Space means PTT everywhere, as for every other button.

  • status_indicator_test now runs the real pttHoldKeyStep() as an app-level filter (connected, shortcuts on). Behind it, Space on a focused indicator is PTT and does not activate it, while Return activates it and keys nothing.
  • With Space put back into the helper, 5 checks fail.
  • The class comment and the PR description are corrected.

Blocker 2: rulings not on record. The rulings were given to the agent in session. They are now recorded on #6257, and the maintainer confirmed keeping all ten, TUN/AMP/TX included. Those three are keyboard-operable on Return/Enter and the Press action.

Nits:

  • Disabled-path test: it now sends a real Return to the disabled indicator, as well as calling activate().
  • Disabled indicators unreachable by Tab: maintainer ruling, left as Qt does. The reason stays reachable by screen-reader object navigation.
  • check_a11y claim: removed from the description. You're right that it can't see this change.
  • Class comment: corrected for Space.

Automated code-review pass

  • Space keys TX: same as Blocker 1 above.
  • On/off state not exposed: maintainer ruled to fold it in. TNF, FDX, CWX, ASR, DVK and the band stack are now checkable and report checked state at every restyle site, with one StateChanged per real change. TUN and AMP lead their description with their state word. Removing the state reporting fails 2 checks; removing the event fails 3.
  • No ObjectDestroyed on interface replacement: fixed. The old object is retired with ObjectDestroyed, then ObjectCreated announces the Button. The test checks the order.
  • Factory runs a property lookup at every class level: fixed. It now acts only at the object's most-derived class.
  • Two registries for mouse and keyboard: maintainer ruling, keep the single shared dispatcher, so mouse-button semantics are unchanged.
  • Dead isEnabled() guards: not changed. They were moved verbatim and are harmless, and I can't demonstrate they're unreachable.

Verification

  • status_indicator_test 33/33; full ctest 673/673.
  • ASR-off compiles.
  • Demo bridge session (DEMO-0001, NO_TX): transmitting=false, names present, and a band-stack click opens its panel.
  • Gates match the merge base apart from the two new files, and the colour ratchet is flat.

@ten9876
ten9876 merged commit f0af38e into main Oct 8, 2026
7 checks passed
@ten9876
ten9876 deleted the fix/statusbar-indicators-keyboard branch October 8, 2026 07:42
ten9876 added a commit that referenced this pull request Oct 8, 2026
…merged together. Principle VIII.

Each PR regenerated the manifest without the other. Together, ThemeManager.h
has one more GUI includer (168 to 169), which left Static checks red on main.
The tag is unchanged (ui-support); only the count moves.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request Oct 8, 2026
…ouchpoint manifest is regenerated after #6267 and #6273. Principle VIII. (#6275)

Fixes main after #6273. On `8603a1a1`, **Build CI Docker Image** and
**System Libraries Canary** fail on the libsecret check that #6273
added.

## Cause
qtkeychain loads libsecret at runtime: `LibSecretKeyring` loads
`secret-1` through `QLibrary`. Ubuntu links with `--as-needed` by
default, so the built library has no `NEEDED libsecret-1` entry, even
though its libsecret backend is compiled in. The check looked for that
entry (`readelf -d … | grep libsecret-1`), so it rejected a correct
build. The same check is in `setup-qtkeychain.sh`, which is also used by
Ubuntu builds that follow the README and by the AppImage job.

## Fix
Both checks now look for the backend itself: the libsecret function
names qtkeychain resolves (`secret_password_lookup` …), which are in the
library only when it was built with `LIBSECRET_SUPPORT=ON`.

## Evidence
- `setup-qtkeychain.sh` built with `LDFLAGS=-Wl,--as-needed`,
reproducing Ubuntu's linking:
  - `NEEDED libsecret: 0`, so the old check would fail;
  - the script prints `Verified: qtkeychain has its libsecret backend`.
- A `LIBSECRET_SUPPORT=OFF` build (qtkeychain 0.16.0) has no
`secret_password_lookup`, so the new check fails it as it should.
- The script header no longer claims linuxdeploy bundles libsecret. It
is loaded from the user's system, which every Secret Service desktop
provides.

## Also: stale touchpoint manifest
#6267 and #6273 each regenerated
`docs/architecture/aetherd-touchpoints.md` without the other's change.
Together, `core/ThemeManager.h` has one more GUI includer (168 to 169),
which leaves **Static checks** red on main. The manifest is regenerated
here (`gen_touchpoint_manifest.py --check`: up to date). The tag is
unchanged (ui-support); only the count moves.

The Dockerfile change takes effect when the CI image is next built:
**Build CI Docker Image** runs on push to main and proves it there.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request Oct 12, 2026
… Principle VIII.

An audit of every page against the 106 PRs in v26.10.2, each fact checked
against the source at the release commit:

- Menu paths: AetherMap, AetherModem, Callsign Lookup and FreeDV Reporter
  are on View, and Waveforms is on Tools.
- New coverage: Windows Firewall rules and the Network Diagnostics page
  (#6289), live CW contacts (#6268), NB2 on the HL2 and ANAN-G2 (#5966),
  ANAN-G2 AGC-T and per-radio memory (#6282, #6129), HL2 state restore
  and its level changes (#6259, #6254, #5975), DVK import (#6247, #6262,
  #6251), Copy Assist from unprocessed audio (#6216), keyboard-operable
  status-bar indicators (#6267), the KPA1500 (#5905), CTR2 extensions
  and the AetherKnob (#6338), live PPM (#6082), and Icom COMP and the
  squelch scale (#6174, #6184).
- Known issues whose GitHub issues are closed are removed.
- The style guide now asks for US spelling, and every page follows it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request Oct 12, 2026
… and re-take the Stable snapshot. Principle VIII. (#6396)

## Summary

This brings the user guide (docs.aethersdr.com) up to date with
everything in v26.10.2 and re-takes the Stable snapshot so the default
site shows it. Docs only, apart from the TCI page prose in
`tools/docs/gen_reference.py`; no application source, test or CI
changes.

Every page was audited against the 106 PRs in the release. Every fact
that was added or kept was checked against the source at `f753dbfc4`,
the v26.10.2 release commit, not taken from the PR bodies alone.

## What changed

- **Menu paths.**
- AetherModem, Callsign Lookup, AetherMap and FreeDV Reporter are on
**Tools**. They are created on View and then moved to Tools
(`MainWindow_Menus.cpp:1519-1520`, `:1569-1575`); the style guide's
example says **Tools → AetherMap...**.
  - **Show live CW contacts** is on **View**.
  - Waveforms is on **Tools**, not File.
  - The VUDU applet button was called TXDSP.
  - Labels now match the source's `...` vs `…`.
- **Gaps from this cycle.**
- **Windows and Network Diagnostics:** Windows Firewall rules and the
Network Diagnostics Windows Firewall page (#6289); the `aether.network`
and `aether.nvafx` log categories (#6289, #6308).
  - **Remote access:** Tailscale shim revocation semantics (#6325).
  - **Sleep, App Nap and power throttling (#6195):** per-OS notes.
- **Hermes-Lite 2:** state restore, RX audio at unity, CW at TUNE level,
multi-interface discovery and a discovery troubleshooting entry (#6259,
#6254, #5975, #6009, #6293).
- **ANAN-G2:** AGC-T range, a "What is remembered" section, and supply
voltage and link status (#6282, #6129, #6137, #6211).
  - **NB2** on the HL2 and ANAN-G2 (#5966).
- **DVK:** WAV import requirements, the slot menu without Delete, status
text and licensing (#6247, #6262, #6251).
  - **Copy Assist** from unprocessed audio (#6216).
  - **Live CW contacts** replace the decoder's contact card (#6268).
  - **Status-bar indicators** work from the keyboard (#6267).
- **Volume:** controllers, hotkeys and TCI `volume` follow the active
output (#6129, #6260).
- **Peripherals:** the KPA1500 (#5905), CTR2 link extensions and the
AetherKnob (#6338), TMate 2 udev (#6250), MIDI RADE Modem and FDV modes
(#6031, #6038).
  - **RTL-SDR:** live PPM (#6082).
  - **Icom:** COMP and the MK2 squelch scale (#6174, #6184).
- **Known issues.** Entries whose GitHub issues are closed are removed
(each checked with `gh issue view`). As decided, this includes three
closed today without a demonstrated fix: #5107, #4824 and #5665.
- **US spelling.** `docs-style-guide.md` asked for British spelling; it
now asks for US, by the maintainer's decision, and every page is
converted. The generated TCI pages are converted at their source, the
prose strings in `tools/docs/gen_reference.py`. Code spans, links and
URLs were left alone. UI labels were checked against the source strings,
and none of them are British. Two in-page anchors and two Log Analyzer
rule anchors (`#bnr-is-grayed-out-…`) follow the renamed headings.
- **Stable snapshot.** `snapshot_stable.py 26.10.2` was re-run on the
release tree with these corrections, before `main` was merged in, so
Stable carries no post-release docs. `docs/` also has `main`'s
post-release pages, in US spelling. Stable alone keeps the IC-9700
waterfall-rate entry (#5996): #6402 fixed it after the 26.10.2 tag, so
26.10.2 still has it. The PDF manual already attached to the v26.10.2
release keeps the tag's docs.

## Validation

- `npm ci && npm run build` in `docs/user`: success. This is the
broken-link and broken-anchor check.
- `node tools/docs/test_log_rules.mjs`: 707 checks passed for 45 rules.
- `python3 tools/docs/gen_reference.py --check`: up to date (8 pages).
- `git diff --check`: clean.
- Menu paths on a build of this branch (offscreen, isolated settings, no
radio), `menu list`: `Tools → AetherModem...`, `Tools → Callsign
Lookup...`, `Tools → AetherMap...`, `Tools → FreeDV Reporter...`, `Tools
→ Waveforms...`, `View → Show live CW contacts`.

## Screenshots now out of date (not recaptured)

These need a recapture pass on the radio:
- **Old panadapter title strip (#6386):** `panadapter`, `main-window`,
`first-session-*`, `multi-slice-two-slices`, `split-pair`,
`tnf-markers`, `offscreen-vfo-indicator`, `pan-3d-stacked-trace`,
`default-light-theme`.
- **Pre-canon windows (#6334, #6339, #6341, #6372):** `aetherrx*`,
`aethertx*`, `aethermodem-aprs`, `bnr-panel`, `connect-dialog`,
`connect-by-ip-card`, `connect-icom-rows`, `connect-smartlink-card`,
`rx-agc-calibration`.
- **Other:**
  - `title-bar` (lowercase tab states, #6281);
  - `menu-help` (#6294);
  - `display-panel` (#6371);
  - `support-diagnostics-dialog`;
  - `waveforms-dialog` and `tailnet-remote-access` (show shim 0.4.0);
  - `about-window` (v26.10.1);
  - `radio-setup-qrz`.

## Found during the audit, not fixed here

- **App string bug:** `TailnetShimDialog.cpp:628` tells the user to
"Connect remotely with Connect → Manual"; the real path is **File →
Connect to Radio... → Connect by IP**.
- **ANAN AGC-T tooltips:** the RX applet and VFO tooltips show the
slider position as dB, which is wrong on ANAN. #6282 lists this as a
follow-up.
- **MIDI Master Volume on non-Flex radios:** it is refused while PC
Audio is off, whereas hotkeys, FlexControl and HID drive the line out.
The docs describe the code as it is.
- **RTL mode names:** `rtl-sdr.md` writes "FM-N" and "CW-R", but the
source strings are `FMN` and `CWR`. Left as is until someone confirms
what the mode menu displays.
- **IC-705 COMP readback:** inferred from the MK2 (#6174) and not
verified on an IC-705.
- **Not yet documented:** the #6230 Radio Health row "Forward responses
skipped", and the Store/MSIX package.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

Status-bar keyer indicators are click-only QLabels (no keyboard access)

1 participant