Skip to content

fix(radio): make disabled Reboot Radio button legible instead of near-invisible (#3334)#3441

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
M7HNF-Ian:fix/3334-reboot-btn-disabled-contrast
Jun 7, 2026
Merged

fix(radio): make disabled Reboot Radio button legible instead of near-invisible (#3334)#3441
ten9876 merged 1 commit into
aethersdr:mainfrom
M7HNF-Ian:fix/3334-reboot-btn-disabled-contrast

Conversation

@M7HNF-Ian

Copy link
Copy Markdown
Contributor

Problem

The "Reboot Radio" button in Radio Setup (added in #3334) appears to vanish if you open Radio Setup while the radio is disconnected/reconnecting — e.g. during the ~40s after clicking Reboot, or any reconnect window.

Root cause

The button is always added, but setEnabled(m_model->isConnected()) disables it when disconnected. The disabled stylesheet used three dim blue-grey tokens — background.1 (#1a2a3a), meter.bar.fill (#405060), background.2 (#304050) — against the dialog background background.0 (#0f0f1a). All near-equal dark tones, so the disabled button blends into the dialog and reads as an empty row rather than a greyed-out control. It reappears once the radio reconnects via the existing connectionStateChanged → setEnabled hook.

Fix

Give the disabled state a legible muted-red appearance consistent with the button's red identity:

QPushButton:disabled { background: #2a1818; color: #8a6055; border-color: #4a2828; }

How found

Instrumented logging confirmed the button is added with enabled=true when connected and enabled=false when the dialog is built mid-reconnect — i.e. a contrast/styling issue, not a missing widget.

Testing

  • Clean build on macOS.
  • Enabled state unchanged (bright orange, verified).
  • Disabled-state contrast raised from near-invisible dim blue-grey to legible muted red.

Follow-up to #3334.

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Hi @M7HNF-Ian — thanks for picking this one up, it's a great little fix and the stylesheet token rewrite is exactly the right call.

Quick note on the red ❌ you're seeing: the only failing check on c25a947 is Qt Accessibility Static Analysis, and looking into it, this is a CI infrastructure issue rather than anything wrong with your change.

What the check actually does

.github/workflows/a11y-check.yml runs tools/check_a11y.py over any src/gui/*.{cpp,h} files touched by the PR. The script itself is non-blocking by design — its module docstring even says so:

Exit 0 always -- accessibility issues are informational, not build-blocking.

And the workflow's summary comment ends with "Findings are warnings only — the build is not blocked."

Why your change is fine

Your diff only touches one disabled-button stylesheet in RadioSetupDialog::buildRadioTab() — no widget construction, no paintEvent, no value-change methods, no QLabel interactivity. None of the four check_a11y.py rules can fire on it. I ran the script locally against the exact file from your PR head and got:

Files scanned      : 1
Files with findings: 0
Total findings     : 0
No accessibility issues found.

Exit code 0.

What's actually breaking

The failing step in the workflow is:

python tools/check_a11y.py $FILES | tee /tmp/a11y_output.txt
COUNT=$(grep -c "^::warning" /tmp/a11y_output.txt || echo "0")
echo "findings=$COUNT" >> $GITHUB_OUTPUT

GitHub Actions runs shell steps with set -eo pipefail. When check_a11y.py produces zero warnings (as it does for your file), grep -c "^::warning" prints 0 and exits with status 1. The || echo "0" salvages the subshell, but the shape of this idiom is fragile and the step has been observed to flake in this configuration.

This is not anything you did — your PR is the right shape and the change is sound. The fix belongs in the workflow file, not in your branch.

What to do

  • No code change needed on your side. The other CI checks (build, check-windows, check-macos, analyze-cpp) are still in progress on this commit — let those settle.
  • Once those finish green, this PR is good to go from a CI standpoint. The a11y check failure can be safely ignored here.
  • A maintainer can either re-run the a11y job (it often passes on retry) or queue a follow-up to make the workflow more robust to the empty-findings case.

Thanks again for sending this in — making the disabled Reboot Radio button legible is a nice quality-of-life fix and the inline comment explaining the reasoning is exactly the right touch. 🎯

— AetherClaude


🤖 aethersdr-agent · cost: $14.1134 · model: claude-opus-4-7

@M7HNF-Ian

Copy link
Copy Markdown
Contributor Author

Heads up: the failing Qt Accessibility Static Analysis check is a pre-existing CI workflow bug, not this PR — the scan itself reports 0 findings ("No accessibility issues found"), then the step errors while capturing the count:

##[error]Unable to process file command 'output' successfully.
##[error]Invalid format '0'

grep -c "^::warning" … || echo "0" writes a two-line 0\n0 to $GITHUB_OUTPUT when there are zero findings. Fixed separately in #3442. The build / macOS / Windows / CodeQL checks are the relevant ones for this PR.

ten9876 pushed a commit that referenced this pull request Jun 6, 2026
…findings (#3442)

The accessibility check job fails on any PR where a changed `src/gui`
file has **zero** findings — the check itself passes, but the step
errors with:

```
##[error]Unable to process file command 'output' successfully.
##[error]Invalid format '0'
```

## Cause
`grep -c "^::warning"` prints `0` to stdout **and** exits non-zero when
there are no matches. So:

```bash
COUNT=$(grep -c "^::warning" /tmp/a11y_output.txt || echo "0")
```

captures **both** grep's own `0` and the `|| echo "0"`, producing the
two-line value `"0\n0"`. Writing `findings=0\n0` to `$GITHUB_OUTPUT` is
malformed → step failure.

## Fix
```diff
-          COUNT=$(grep -c "^::warning" /tmp/a11y_output.txt || echo "0")
+          COUNT=$(grep -c "^::warning" /tmp/a11y_output.txt || true)
```

`grep -c` already prints `0` on no match; `|| true` simply swallows its
non-zero exit under `set -e`, without appending a duplicate. Verified
locally:

| Input | Old `COUNT` | New `COUNT` |
|---|---|---|
| no `::warning` lines | `0\n0` (malformed) | `0` |
| 2 `::warning` lines | `2` | `2` |

Seen on #3441 (a clean GUI change with 0 a11y findings).
…-invisible (aethersdr#3334)

The "Reboot Radio" button in Radio Setup appears to vanish when the dialog
is opened while the radio is disconnected/reconnecting (e.g. the ~40s after
clicking Reboot). The button is always added, but setEnabled(isConnected())
disables it when disconnected, and the disabled stylesheet used three dim
blue-grey tokens (background.1 #1a2a3a / meter.bar.fill #405060 /
background.2 #304050) against the dialog background background.0 #0f0f1a —
all near-equal dark tones, so the disabled button blended into the dialog
and read as an empty row rather than a greyed-out control. It reappears on
reconnect via the existing connectionStateChanged -> setEnabled hook.

Give the disabled state a legible muted-red appearance consistent with the
button's enabled red identity so a disabled control reads as disabled, not
missing.

Follow-up to aethersdr#3334.
@ten9876 ten9876 force-pushed the fix/3334-reboot-btn-disabled-contrast branch from c25a947 to 2853ac6 Compare June 6, 2026 22:59
@ten9876 ten9876 self-assigned this Jun 6, 2026
@ten9876

ten9876 commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

Rebased your branch onto current main (`2853ac62`) to pick up #3442's a11y workflow fix. Your code didn't trigger any a11y findings — the failure was the same workflow-yaml bug that hit #3439: the pre-#3442 step crashed writing the zero-findings result to `$GITHUB_OUTPUT`.

Root cause (workflow bug, not your code)

The old workflow had `COUNT=$(grep -c "^::warning" ... || echo "0")` — when `grep -c` finds zero matches it exits 1 and prints `0`, so the `|| echo "0"` appended a second `0` and `echo "findings=$COUNT" >> $GITHUB_OUTPUT` wrote a malformed multi-line value. The parser rejected it with `Invalid format '0'` and the step failed even though the lint had already succeeded.

#3442 changed it to `|| true`. Timing: your CI ran 2026-06-06 20:07 UTC, #3442 merged 21:44 UTC — the workflow ran from your PR branch's pre-fix copy.

What I did

Same maintainer-fix-up pattern as the other PRs landing today — rebased your single commit onto current main, no content changes to the actual fix (the `#2a1818 / #8a6055 / #4a2828` hex values + the `#3334 follow-up` comment are exactly as you wrote them).

CI will re-run against `2853ac62` and the a11y check will pass cleanly this time.

Real-talk on the fix itself: nice catch. Replacing those theme tokens with concrete contrast-good hex values is the right call — the previous `{{color.background.1}}` / `{{color.meter.bar.fill}}` / `{{color.background.2}}` tokens really did all dim to similar blue-greys, making the disabled button look absent rather than greyed-out. Will give the actual change a closer look once CI greens.

Principle XI.

ten9876 added a commit to ea5wa/AetherSDR that referenced this pull request Jun 6, 2026
Two related fixes to TitleBar's new PanLock accessors so all four platforms
compile again:

1. signals: vs. public: — MOC parses anything inside a `signals:` block as
   a signal declaration, so the inline `bool isPanFollowChecked() const`
   and `void setPanFollowChecked(bool)` were rejected at moc time with
   "Not a signal declaration" (TitleBar.h:56). Both methods are accessors,
   not signals — they belong in `public:`.

2. Out-of-line over inline. Even after the section move, the inline
   bodies referenced `m_panFollowBtn->isChecked()` and `QSignalBlocker`,
   which need the full QPushButton type. The header only forward-declares
   QPushButton, so inline bodies couldn't compile in callers either
   (the moc error masked this until now). Moving the definitions to
   TitleBar.cpp keeps the header light and matches the style of
   neighbours like `isSystemMoveAreaAt`.

Build verified locally on Arch Linux x86 — 632/632 clean (only the
pre-existing unrelated macDaxDriverInstalled warning). Same maintainer
fix-up pattern as aethersdr#3279/aethersdr#3286/aethersdr#3289/aethersdr#3381/aethersdr#3398/aethersdr#3417/aethersdr#3439/aethersdr#3441.

Principle XI.
@ten9876 ten9876 merged commit 25b97f2 into aethersdr:main Jun 7, 2026
6 checks passed
ten9876 added a commit that referenced this pull request Jun 7, 2026
…tions

Follow-up to #3289 closing five gaps surfaced by reviewing our doc against
the WCAG/POUR-grounded coverage at accessibilitychecker.org. Our existing
doc is materially deeper than that article on Qt-specific patterns (live
value announcements, throttling, custom-painted widgets, lint mechanics),
but it was missing five concrete things every Qt-desktop a11y guide
should have. All five land as additions; no existing section is changed.

1. "What we're following" — three-line POUR (Perceivable / Operable /
   Understandable / Robust) grounding right after the Background. Cites
   WCAG 2.1 quick-ref. Future contributors asking "why does this rule
   exist" now get "WCAG 2.1 says so" rather than "because the project
   says so."

2. Form-field instructions — added to the existing "Accessible
   description" bullet. For QSpinBox / QLineEdit / QComboBox, the
   accessible name answers "what is this?" and the description answers
   "what do I type?". Tooltips don't reach the screen reader; the
   description does — that's the right home for input semantics.

3. Keyboard-only test — added to the existing "Tab focus" bullet. Tab to
   the widget from a sibling, activate with Space/Return, no mouse. If
   you can't reach it the focus policy is wrong; if Space/Return doesn't
   fire it the widget needs a keyPressEvent (or it should have been a
   QPushButton — links to the QLabel anti-pattern section).

4. Colour contrast — new section between "Live value updates" and
   "Custom-painted widgets." WCAG 4.5:1 normal text / 3:1 large text and
   non-text components. Cites #3441 (Reboot Radio button) as the
   canonical disabled-state-blends-into-background case so future readers
   see why the section exists. WebAIM Contrast Checker + the
   OS-level "Increase Contrast" mode as the practical verification
   workflow.

5. Manual verification — new section between "Suppressing the lint" and
   "CI enforcement." Names the three platform screen readers (VoiceOver,
   NVDA/Narrator, Orca) with the actual keystrokes to launch them, and
   tells contributors to walk the widget the way a screen-reader user
   would. Lint = static; this = real test. Explicit that the lint
   catches what it can detect statically and nothing more.

Doc grows 166 → 254 lines (+88). Section order flows
motivation → grounding → patterns → escape valves → verification →
mechanism.

Principle XI.
ten9876 added a commit that referenced this pull request Jun 7, 2026
Self-review of #3443 surfaced five issues; all five addressed here.

1. Colour contrast section now matches the theme-tokens-first policy
   established in #3441 / #3446 (instead of contradicting it).

   The earlier text read "pick concrete hex values that hit the WCAG
   ratio against the real surrounding background; reserve the dimmest
   theme tokens for genuinely decorative state where invisibility is the
   intent" — which advocated hex over tokens for disabled state and
   would have read as policy by future contributors. The actual policy
   from the #3441/#3446 thread: theme tokens are first choice; hex is a
   valid intermediate state when no semantic token exists, with a
   follow-up issue to add the proper token (mirroring how #3441 +
   #3446 played out). Adds the three-step rule of thumb (existing token
   for this widget × state? token for similar widget? hex + issue?) and
   cites #3441's merge commit and #3446's open status as the canonical
   example.

2. POUR Operable softened from overclaim to aspirational.

   Was: "Operable — keyboard focus and activation paths for every
   interactive widget; no mouse-only interactions." That's false today —
   CHAIN drag-drop, frequency drag, drag-to-pan, custom panadapter
   handlers are all mouse-only. Replaced with "every interactive widget
   reachable by keyboard, with a non-mouse activation path. (AetherSDR
   still has surfaces that are mouse-only today — [list] — which we're
   working toward; new code shouldn't add to the list.)"

3. Tooltip-vs-description claim corrected.

   Was: "right place to put input semantics, not in a separate tooltip
   the screen reader can't reach." Qt does expose tooltips to AT via
   QAccessible::Help — they reach screen readers as help text, fired on
   explicit request. The argument that descriptions are the better home
   for input semantics still stands, but now states the actual reason
   (descriptions are read automatically as part of widget identity;
   tooltips are help-text-on-request).

4. NVDA "say more" mode reference replaced with neutral language.

   "Say more" isn't a standard NVDA mode name (it may be confused with
   VoiceOver's "speak more"). Now says "in verbose announcement modes"
   without committing to a specific NVDA mode label.

5. WebAIM Contrast Checker workflow fixed.

   The previous text said "take a screenshot of the widget against its
   real background and drop it into the WebAIM Contrast Checker" — but
   that tool takes two hex codes, not an image. Reworded to mention the
   eyedropper step (Digital Color Meter on macOS, PowerToys Color
   Picker on Windows, gpick/gcolor3 on Linux) and added a pointer to
   Colour Contrast Analyser (TPGi) for a one-step
   screenshot-to-ratio workflow with integrated eyedropper.

Doc grows 254 → 286 lines net (+74 modifications inside the existing
+89 from the original commit). Section structure unchanged.

Principle XI.
ten9876 added a commit to ea5wa/AetherSDR that referenced this pull request Jun 7, 2026
Two related fixes to TitleBar's new PanLock accessors so all four platforms
compile again:

1. signals: vs. public: — MOC parses anything inside a `signals:` block as
   a signal declaration, so the inline `bool isPanFollowChecked() const`
   and `void setPanFollowChecked(bool)` were rejected at moc time with
   "Not a signal declaration" (TitleBar.h:56). Both methods are accessors,
   not signals — they belong in `public:`.

2. Out-of-line over inline. Even after the section move, the inline
   bodies referenced `m_panFollowBtn->isChecked()` and `QSignalBlocker`,
   which need the full QPushButton type. The header only forward-declares
   QPushButton, so inline bodies couldn't compile in callers either
   (the moc error masked this until now). Moving the definitions to
   TitleBar.cpp keeps the header light and matches the style of
   neighbours like `isSystemMoveAreaAt`.

Build verified locally on Arch Linux x86 — 632/632 clean (only the
pre-existing unrelated macDaxDriverInstalled warning). Same maintainer
fix-up pattern as aethersdr#3279/aethersdr#3286/aethersdr#3289/aethersdr#3381/aethersdr#3398/aethersdr#3417/aethersdr#3439/aethersdr#3441.

Principle XI.
ten9876 added a commit that referenced this pull request Jun 7, 2026
…tions (#3443)

## Summary

Follow-up to #3289 closing five gaps surfaced by reviewing
`docs/a11y.md` against the WCAG/POUR-grounded coverage at
[accessibilitychecker.org/blog/a11y/](https://www.accessibilitychecker.org/blog/a11y/).

Our existing doc is materially deeper than that article on Qt-specific
patterns (live value announcements, throttling, custom-painted widgets,
lint mechanics) — but it was missing five concrete things every
Qt-desktop a11y guide should have. All five land as pure additions; no
existing section is modified.

## What's added

1. **"What we're following" section** — three-line POUR (Perceivable /
Operable / Understandable / Robust) grounding right after the
Background. Cites WCAG 2.1 quick-ref. Future contributors asking *"why
does this rule exist"* now get *"WCAG 2.1 says so"* rather than
*"because the project says so."*

2. **Form-field instructions guidance** — added to the existing
"Accessible description" bullet. For `QSpinBox` / `QLineEdit` /
`QComboBox`, the accessible name answers *"what is this?"* and the
description answers *"what do I type?"*. Tooltips don't reach the screen
reader; the description does.

3. **Keyboard-only test** — added to the existing "Tab focus" bullet.
Tab to the widget from a sibling, activate with `Space`/`Return`, no
mouse. Links to the existing QLabel anti-pattern section as the
structural escape.

4. **Colour contrast section** — between "Live value updates" and
"Custom-painted widgets." WCAG **4.5:1 normal text / 3:1 large text**
and non-text components. Cites #3441 (Reboot Radio button) as the
canonical disabled-state-blends-into-background case so future readers
see *why* the section exists. WebAIM Contrast Checker + the OS-level
"Increase Contrast" mode as the practical verification workflow.

5. **Manual verification section** — between "Suppressing the lint" and
"CI enforcement." Names the three platform screen readers
([VoiceOver](https://www.apple.com/accessibility/),
[NVDA](https://www.nvaccess.org/) / Narrator,
[Orca](https://help.gnome.org/users/orca/stable/)) with the actual
keystrokes to launch them. Lint = static; this = real test. Explicit
that *"the lint catches what it can detect statically — this is the real
test."*

## Doc shape

| | Before | After |
|---|---|---|
| Lines | 166 | 254 |
| Sections | 7 | 9 |

Section order now flows: motivation → grounding → patterns → escape
valves → verification → mechanism.

## What I *didn't* import from the article

- **Legal framing** ("legally mandated… legal action or fines"). Out of
voice. AetherSDR's a11y motivation is "a blind ham operator filed #3288
and we want AetherSDR usable for him" — that's a stronger frame than
fear of lawsuits.
- **POUR as the organizing structure of the whole doc**. The article is
structured around POUR; we'd lose clarity if we reshuffled our concrete
Qt sections under POUR headings. Mention WCAG/POUR once near the top,
keep our pragmatic structure.
- **Alt text on images / captions on video**. We have ~zero of either in
AetherSDR — no help text videos, the only "images" are panadapter
renders (covered by our custom-painted-widgets section).

## Test plan

- [x] `docs/a11y.md` parses as valid markdown (9 `##` sections, no
orphaned formatting).
- [x] Links checked: WCAG 2.1 quickref, WebAIM contrast checker +
articles/contrast, NVDA, Apple Accessibility, Orca docs — all resolve.
- [x] No code changes — `tools/check_a11y.py` and the workflow are
untouched, so no CI behaviour change.

Refs #3288 #3289
@M7HNF-Ian M7HNF-Ian deleted the fix/3334-reboot-btn-disabled-contrast branch June 7, 2026 14:58
ten9876 pushed a commit that referenced this pull request Jun 19, 2026
… ThemeManager tokens. Principle IX. (#3645)

## Summary

Follow-up to #3439 and #3441. Fills three hardcoded-colour gaps in the
theme token system — the last surfaces in the FreeDV Reporter dialog and
Radio Setup dialog that hadn't been migrated to ThemeManager.

**FreeDvReporterModel — row highlight colours**
- Removes four hardcoded `QColor` constants (`kTxBg` / `kRxBg` /
`kMsgBg` / `kHlFg`) and replaces their three use sites in `data()` with
`ThemeManager::instance().color(token)` calls.
- Adds a `themeChanged` → `dataChanged{BackgroundRole, ForegroundRole}`
connection in the constructor so highlighted rows repaint immediately on
a theme switch instead of waiting for the next 250 ms highlight tick.
- Note: `color.background.tx` (`#3a2a0e`) is a chrome-panel tint used in
35+ surfaces and could not be reused here. The TX-row highlight
(`#fc4500`) is a distinct high-visibility colour matching the freedv-gui
reference palette, so new tokens were required for all four constants.

**RadioSetupDialog — button disabled states**
- Migrates three `QPushButton:disabled` CSS blocks to tokens.
- Reboot Radio button (`buildRadioTab` ~line 601): uses new
`color.button.danger.*disabled` tokens — preserves the intentional
danger-red colouring in the disabled state.
- Upload Firmware button (`buildRadioTab` ~line 838): was using
`{{color.meter.bar.fill}}` and `{{color.background.1}}` as
disabled-state stand-ins (semantically wrong tokens from a prior partial
migration). Now uses correct `color.button.*disabled` tokens.
- Serial tab Open/Close port buttons (`buildSerialTab` ~line 4054):
hardcoded hex replaced with `color.button.*disabled` tokens;
`setStyleSheet` calls replaced with `applyStyleSheet` so the template
engine resolves tokens and the widgets participate in live theme
switching.

**New tokens — 10 total across both theme files**

| Token | Dark | Light |
|---|---|---|
| `color.highlight.tx` | `#fc4500` | `#fc4500` |
| `color.highlight.rx` | `#379baf` | `#379baf` |
| `color.highlight.message` | `#e58be5` | `#e58be5` |
| `color.highlight.fg` | `#000000` | `#000000` |
| `color.button.background.disabled` | `#203040` | `{color.gray.700}` |
| `color.button.foreground.disabled` | `{color.gray.500}` |
`{color.gray.500}` |
| `color.button.border.disabled` | `{color.gray.700}` |
`{color.gray.600}` |
| `color.button.danger.background.disabled` | `#2a1818` | `#f0d8d8` |
| `color.button.danger.foreground.disabled` | `#8a6055` | `#b08080` |
| `color.button.danger.border.disabled` | `#4a2828` | `#d0a8a8` |

Highlight colours are kept identical between dark and light themes —
they are high-saturation reference palette colours from freedv-gui that
provide adequate contrast with the black foreground on both backgrounds.
A future theme author can override them via the token.

Fixes #3446.

## Test plan

- [ ] Build clean on Windows (MinGW), no new warnings
- [ ] FreeDV Reporter: open dialog, observe another station transmitting
— TX row shows orange background with black text
- [ ] FreeDV Reporter: RX highlight (teal) and message highlight
(purple) appear and expire after ~5 s
- [ ] FreeDV Reporter: switch theme while a highlight is active — row
repaints immediately (does not wait for the 250 ms tick)
- [ ] Radio Setup → General tab: disconnect radio, confirm Reboot Radio
button disabled state is dark red-tinted (not generic grey)
- [ ] Radio Setup → General tab: Upload Firmware button renders correct
disabled state when no firmware check has run
- [ ] Radio Setup → Serial tab: open a port config, confirm Open/Close
buttons show correct disabled styling when not applicable
- [ ] Light theme: open FreeDV Reporter and Radio Setup, confirm all
three token groups (highlight, button, danger) render legibly

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

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…findings (aethersdr#3442)

The accessibility check job fails on any PR where a changed `src/gui`
file has **zero** findings — the check itself passes, but the step
errors with:

```
##[error]Unable to process file command 'output' successfully.
##[error]Invalid format '0'
```

## Cause
`grep -c "^::warning"` prints `0` to stdout **and** exits non-zero when
there are no matches. So:

```bash
COUNT=$(grep -c "^::warning" /tmp/a11y_output.txt || echo "0")
```

captures **both** grep's own `0` and the `|| echo "0"`, producing the
two-line value `"0\n0"`. Writing `findings=0\n0` to `$GITHUB_OUTPUT` is
malformed → step failure.

## Fix
```diff
-          COUNT=$(grep -c "^::warning" /tmp/a11y_output.txt || echo "0")
+          COUNT=$(grep -c "^::warning" /tmp/a11y_output.txt || true)
```

`grep -c` already prints `0` on no match; `|| true` simply swallows its
non-zero exit under `set -e`, without appending a duplicate. Verified
locally:

| Input | Old `COUNT` | New `COUNT` |
|---|---|---|
| no `::warning` lines | `0\n0` (malformed) | `0` |
| 2 `::warning` lines | `2` | `2` |

Seen on aethersdr#3441 (a clean GUI change with 0 a11y findings).
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…-invisible (aethersdr#3334) (aethersdr#3441)

## Problem
The "Reboot Radio" button in Radio Setup (added in aethersdr#3334) appears to
vanish if you open Radio Setup while the radio is
disconnected/reconnecting — e.g. during the ~40s after clicking Reboot,
or any reconnect window.

## Root cause
The button is always added, but `setEnabled(m_model->isConnected())`
disables it when disconnected. The disabled stylesheet used three dim
blue-grey tokens — `background.1 (#1a2a3a)`, `meter.bar.fill (#405060)`,
`background.2 (#304050)` — against the dialog background `background.0
(#0f0f1a)`. All near-equal dark tones, so the disabled button blends
into the dialog and reads as an empty row rather than a greyed-out
control. It reappears once the radio reconnects via the existing
`connectionStateChanged → setEnabled` hook.

## Fix
Give the disabled state a legible muted-red appearance consistent with
the button's red identity:
```
QPushButton:disabled { background: #2a1818; color: #8a6055; border-color: #4a2828; }
```

## How found
Instrumented logging confirmed the button is added with `enabled=true`
when connected and `enabled=false` when the dialog is built
mid-reconnect — i.e. a contrast/styling issue, not a missing widget.

## Testing
- Clean build on macOS.
- Enabled state unchanged (bright orange, verified).
- Disabled-state contrast raised from near-invisible dim blue-grey to
legible muted red.

Follow-up to aethersdr#3334.
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…tions (aethersdr#3443)

## Summary

Follow-up to aethersdr#3289 closing five gaps surfaced by reviewing
`docs/a11y.md` against the WCAG/POUR-grounded coverage at
[accessibilitychecker.org/blog/a11y/](https://www.accessibilitychecker.org/blog/a11y/).

Our existing doc is materially deeper than that article on Qt-specific
patterns (live value announcements, throttling, custom-painted widgets,
lint mechanics) — but it was missing five concrete things every
Qt-desktop a11y guide should have. All five land as pure additions; no
existing section is modified.

## What's added

1. **"What we're following" section** — three-line POUR (Perceivable /
Operable / Understandable / Robust) grounding right after the
Background. Cites WCAG 2.1 quick-ref. Future contributors asking *"why
does this rule exist"* now get *"WCAG 2.1 says so"* rather than
*"because the project says so."*

2. **Form-field instructions guidance** — added to the existing
"Accessible description" bullet. For `QSpinBox` / `QLineEdit` /
`QComboBox`, the accessible name answers *"what is this?"* and the
description answers *"what do I type?"*. Tooltips don't reach the screen
reader; the description does.

3. **Keyboard-only test** — added to the existing "Tab focus" bullet.
Tab to the widget from a sibling, activate with `Space`/`Return`, no
mouse. Links to the existing QLabel anti-pattern section as the
structural escape.

4. **Colour contrast section** — between "Live value updates" and
"Custom-painted widgets." WCAG **4.5:1 normal text / 3:1 large text**
and non-text components. Cites aethersdr#3441 (Reboot Radio button) as the
canonical disabled-state-blends-into-background case so future readers
see *why* the section exists. WebAIM Contrast Checker + the OS-level
"Increase Contrast" mode as the practical verification workflow.

5. **Manual verification section** — between "Suppressing the lint" and
"CI enforcement." Names the three platform screen readers
([VoiceOver](https://www.apple.com/accessibility/),
[NVDA](https://www.nvaccess.org/) / Narrator,
[Orca](https://help.gnome.org/users/orca/stable/)) with the actual
keystrokes to launch them. Lint = static; this = real test. Explicit
that *"the lint catches what it can detect statically — this is the real
test."*

## Doc shape

| | Before | After |
|---|---|---|
| Lines | 166 | 254 |
| Sections | 7 | 9 |

Section order now flows: motivation → grounding → patterns → escape
valves → verification → mechanism.

## What I *didn't* import from the article

- **Legal framing** ("legally mandated… legal action or fines"). Out of
voice. AetherSDR's a11y motivation is "a blind ham operator filed aethersdr#3288
and we want AetherSDR usable for him" — that's a stronger frame than
fear of lawsuits.
- **POUR as the organizing structure of the whole doc**. The article is
structured around POUR; we'd lose clarity if we reshuffled our concrete
Qt sections under POUR headings. Mention WCAG/POUR once near the top,
keep our pragmatic structure.
- **Alt text on images / captions on video**. We have ~zero of either in
AetherSDR — no help text videos, the only "images" are panadapter
renders (covered by our custom-painted-widgets section).

## Test plan

- [x] `docs/a11y.md` parses as valid markdown (9 `##` sections, no
orphaned formatting).
- [x] Links checked: WCAG 2.1 quickref, WebAIM contrast checker +
articles/contrast, NVDA, Apple Accessibility, Orca docs — all resolve.
- [x] No code changes — `tools/check_a11y.py` and the workflow are
untouched, so no CI behaviour change.

Refs aethersdr#3288 aethersdr#3289
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.

2 participants