Skip to content

feat(aethercontrol): double-click to latch / unlatch (replaces click+ESC asymmetry)#3103

Merged
ten9876 merged 1 commit into
mainfrom
auto/aethercontrol-doubleclick-latch
May 25, 2026
Merged

feat(aethercontrol): double-click to latch / unlatch (replaces click+ESC asymmetry)#3103
ten9876 merged 1 commit into
mainfrom
auto/aethercontrol-doubleclick-latch

Conversation

@ten9876
Copy link
Copy Markdown
Collaborator

@ten9876 ten9876 commented May 25, 2026

Summary

Changes AetherControl's virtual FlexControl knob from a click-to-latch / Escape-to-unlatch UX to a double-click for both directions, with Escape kept as a secondary safety release.

Why

Operators report losing track of the release path. First-time users miss the small "Press ESC to release" hint at the bottom of the dialog and feel stuck with the cursor captured.

What changed

Action Before After
Latch Single left-click on wheel Double-click on wheel
Unlatch ESC key Double-click on wheel (primary) or ESC (secondary safety)
Single click on wheel Latched No-op (event accepted to suppress accidental window drag)

Updated user-visible strings:

  • Resting hint: "Double-click the knob to capture circular tuning."
  • Armed hint: "Mouse locked to FlexControl. Double-click the knob to release (or press ESC)."
  • Accessible description mirrors the new gesture.

ESC stays as a backup for the edge case where the cursor ended up offscreen and can't get back over the knob to double-click.

Attribution

AetherControl original implementation by @rfoust (#2888); FlexControl wheel UX overhaul also by @rfoust (#3029). This change preserves the existing wheel-feel work and only touches the capture/release gesture.

Test plan

  • Linux build clean
  • CI: build / check-paths / check-windows / CodeQL
  • Manual: open AetherControl (Settings → FlexControl), single-click on wheel does nothing, double-click captures, double-click again releases, ESC still releases as backup

73, Jeremy KK7GWY & Claude (AI dev partner)

🤖 Generated with Claude Code

…ESC asymmetry)

AetherControl's virtual FlexControl knob captured the mouse on
single-click and released it on Escape.  Operators reported losing
track of the release path (especially first-time users who didn't see
the small "Press ESC to release" hint) and feeling stuck.

Bind both directions to the same gesture:

  Latch    →  double-click on the wheel
  Unlatch  →  double-click on the wheel (or ESC as secondary safety)
  Single-click on the wheel  →  no-op (event accepted so the parent
                                dialog doesn't mis-interpret it as a
                                window-drag start)

ESC stays as a secondary release path for the edge case where the
operator can't get the cursor back over the knob to double-click
(e.g. wheel scrolled the radio's VFO far enough that the cursor
ended up offscreen).

Updated user-visible strings:
  - Resting hint:  "Double-click the knob to capture circular tuning."
  - Armed   hint:  "Mouse locked to FlexControl. Double-click the knob
                    to release (or press ESC)."
  - Accessible description mirrors the new gesture.

AetherControl original implementation @rfoust (#2888); FlexControl
wheel UX overhaul @rfoust (#3029).  This change preserves the
existing wheel-feel work and only touches the capture/release
gesture.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ten9876 ten9876 requested a review from a team as a code owner May 25, 2026 03:15
Copy link
Copy Markdown
Contributor

@aethersdr-agent aethersdr-agent Bot left a comment

Choose a reason for hiding this comment

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

Thanks @ten9876 — agree the click-to-latch / Esc-to-unlatch asymmetry was a real discoverability cliff, and double-click as a single binding is a nice fix. Implementation looks clean: the press-event no-op is correctly still accept()-ing so it doesn't fall through to a window drag, mouseDoubleClickEvent handles both directions, and the explanatory comments capture the why well.

One inconsistency worth fixing in this PR

The in-knob "armed" badge painted at src/gui/FlexControlDialog.cpp:621 still reads:

const QString hint = QStringLiteral("Press Esc\nto release");

That overlay is the most prominent capture-state cue (centered on the wheel, accented border) and now contradicts the updated bottom-of-dialog hint and accessible description, both of which name double-click as the primary release path. A first-time captured user will see "Press Esc to release" front-and-centre and never discover that double-click also works — which is the exact UX gap this PR is trying to close. Suggest something like "Double-click\nor press Esc" (or two lines, whichever still fits the badge rect at the existing point size).

Minor observations, not blockers

  • mousePressEvent only checks isEnabled() and not m_captured — fine, since while captured the press just accepts and the double-click event drives the release, but worth a mental note in case anyone adds press-side behavior later.
  • Test plan still has the CI / manual boxes unchecked; once Linux build + manual smoke confirm, this is ready.

Nice quality-of-life improvement. Just the badge text update and this is good to go from my end.


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

@ten9876 ten9876 merged commit 4d414d9 into main May 25, 2026
5 checks passed
@ten9876 ten9876 deleted the auto/aethercontrol-doubleclick-latch branch May 25, 2026 04:28
ten9876 added a commit that referenced this pull request May 25, 2026
…+ Save As) (#3130)

## Summary

First user-facing surface of the theming subsystem. Operators can now
edit any colour token in the active theme and ship a custom theme to
disk, all without touching a JSON file.

**View → Theme Editor…** opens a modeless dialog with:

- "Editing: \<active theme\>" header
- Filter bar — type-to-match against dotted token names (e.g.
\`accent\`, \`slice\`, \`meter\`)
- Sorted list of every colour token with a 16×16 swatch + hex value per
row
- Click row → \`QColorDialog\` opens seeded with the current value
- Accept → \`ThemeManager::setColor()\` → emits \`themeChanged\` → every
widget registered through \`applyStyleSheet\` repaints on the next
event-loop turn. **No restart**.
- "Save As…" prompts for a theme name and writes the current tokens to
\`~/.config/AetherSDR/themes/\<name\>.json\` via
\`saveCurrentThemeAs()\`. The new theme is registered + made active
immediately.

## ThemeManager additions

| Method | Purpose |
|---|---|
| \`allTokenKeys()\` | Sorted enumeration of every loaded token — used
by the editor to populate its list. |
| \`setColor(token, color)\` | In-memory mutation + themeChanged. Hex
round-trip so \`cssFragment()\` / \`resolve()\` pick up the new value
the same way they pick up loaded tokens. No-op on a same-value write. |
| \`setSizing(token, int)\` | Integer-token equivalent for panel padding
/ border widths. Not yet exposed in the editor UI; will land in PR 2
alongside font/sizing surface. |
| \`saveCurrentThemeAs(name)\` | Rebuilds nested JSON from the flat
dotted-key map (so on-disk shape matches bundled themes), writes to user
themes dir, registers + activates. Handles QString/int/double/bool
scalars + full \`ThemeGradient\` round-trip so the waterfall colormap
and slice.dim tokens persist correctly. |

## Explicitly NOT in this PR (deferred to Phase 5 PR 2-4)

1. **Inspector mode** (click-on-widget to find tokens that paint it).
\`ThemeManager::tokensForWidget()\` already exposes the reverse-map
populated by \`applyStyleSheet\`; wiring is a few hundred lines once we
settle on the latch/unlatch UX. Probably matches the AetherControl
double-click convention (#3103).
2. **Gradient editing** for
\`waterfall.colormap.{default,grayscale,fire,…}\` and
\`slice.dim.{a..h}\`. The list filters them out via the
\`brush().gradient()\` probe so they don't render as
misleading-first-stop swatches.
3. **Font / sizing editing** — same Phase 5 PR 2 surface as the
inspector mode.
4. **Import** (file picker for arbitrary JSON), drag-and-drop, theme
deletion, theme renaming.

## Build verified clean

- [x] AetherSDR builds clean
- [x] All 320 targets including every test target

## Phase status

| Phase | Item | Status |
|---|---|---|
| 1 | ThemeManager scaffolding + JSON loader | ✅ |
| 2 | Gradient tokens + applyStyleSheet reverse-map + mass migration | ✅
|
| 3 | Paint-code migration + canonical-table closeout | ✅ |
| 4 | Default Light theme | ⏳ (#3129, in review) |
| **5** | **Editor UI (PR 1: live colour editing + Save As)** | **this
PR** |

## Test plan

- [ ] CI: build / check-paths / check-windows / CodeQL
- [ ] Visual smoke:
  - [ ] View → Theme Editor… opens the modeless dialog
  - [ ] Title shows "Editing: Default Dark" (or whatever's active)
  - [ ] Filter bar — type \`accent\` → only accent.* rows visible
- [ ] Click an accent row → QColorDialog opens, pick a wild hue, accept
→ that colour appears immediately wherever \`color.accent\` paints (most
buttons, slider handles, badges)
  - [ ] Save As… → name it "Test", confirm:
    - File written to \`~/.config/AetherSDR/themes/Test.json\`
    - Title bar updates to "Editing: Test"
    - View → Theme submenu (from #3129) now lists "Test"
- [ ] Switch back to Default Dark via the View menu → original colours
restored, custom edits gone (file on disk preserved)
- [ ] Reopen Test from the menu → custom edits restored from the saved
file

73, Jeremy KK7GWY & Claude (AI dev partner)

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

---------

Co-authored-by: Claude Opus 4.7 (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.

1 participant