Skip to content

feat(memory): sanitize NULs, add field dropdowns, and smoother editing#3657

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
jensenpat:feat/memory-dialog-usability
Jun 19, 2026
Merged

feat(memory): sanitize NULs, add field dropdowns, and smoother editing#3657
ten9876 merged 1 commit into
aethersdr:mainfrom
jensenpat:feat/memory-dialog-usability

Conversation

@jensenpat

@jensenpat jensenpat commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Memory Channels dialog: tougher data, friendlier editing ⚡

This is a focused glow-up of the Memory Channels dialog — it fixes a real data-corruption bug, makes the constrained fields impossible to fat-finger, and turns the spreadsheet from "fight it" into "fly through it."


🐛 The bug that started it: NUL bytes in Mode

Memory entries were turning up with embedded NUL (\0) characters in the Mode field — invisible in the UI, but enough to confuse the radio and any other SmartSDR-compatible software that later reads the channel. Nothing in the pipeline was scrubbing control characters, on the way in or out.

Fixed at every boundary. New MemoryFields::sanitizeText() strips all C0 control bytes (0x000x1F, including those NULs) plus the reserved 0x7f DEL byte, and it's wired into all four crossing points:

Boundary Where
Radio status → app RadioModel::handleMemoryStatus (decode 0x7f→space, then sanitize)
App → radio command encodeMemoryText + mode/offset/tone wire conversion
CSV import MemoryCsvCompat::parseRecord (+ logs unrecognized modes)
CSV export recordToFields — so corruption can never leak to other tools

Garbage can no longer enter the model, reach the radio, or escape into a .csv.


🎯 One-click dropdowns for the constrained fields

No more typing CTCSS_TX by hand and hoping. A new MemoryFieldDelegate gives the constrained columns proper combo-box editors that pop open on a single click:

  • Mode — strict list: USB, LSB, CW, AM, SAM, DSB, FM, NFM, DFM, DIGU, DIGL, RTTY, FDV, DSTR, AME
  • Offset Dir — strict: SIMPLEX, UP, DOWN
  • Tone Mode — strict: OFF, CTCSS_TX
  • Tone Value — type-or-pick: all 50 standard EIA CTCSS tones, free entry still allowed
  • Step & Group — type-or-pick (Group seeded live from existing groups + global/TX profiles)

Every value list is sourced from the authoritative FlexLib definitions (Slice.cs / Memory.cs), and display↔wire conversion is centralized in one place so the table, the radio commands, and CSV always agree (modes upper-case on the wire, offset/tone lower-case).


⌨️ Editing that gets out of your way

  • Retired the modal "Edit" toggle. Cells are always editable — click a selected cell, hit F2, or just start typing.
  • Tab / Shift+Tab walk you across the fields like a real spreadsheet.
  • Double-click stays reserved for Tune, so editing and tuning never collide.

📏 macOS column sizing fixes

Two papercuts squashed:

  1. resizeColumnsToContents() runs a couple pixels tight on macOS, eliding short values — CW showed as just C after selection.
  2. Long values like DIGU and CTCSS_TX got chopped because the floor widths were measured in the dialog's font while the table renders in the (larger) themed font.

Now every column gets breathing room, and each dropdown column is floored to its widest possible value, measured in the table's own font + slack for cell margins and the combo arrow — so the value always shows in full. Also shortened the most crowded header (FM TX Offset DirOffset Dir).


✅ Tests

New memory_field_values_test covering sanitization (NUL / TAB / CR / LF / DEL), mode validation, and offset/tone display↔wire round-trips. Passes alongside the existing memory_recall_policy_test.


🔍 How to verify

  1. Open Memory Channels.
  2. Edit Mode, Offset Dir, Tone Mode — confirm the dropdowns open in one click and the picked value (incl. DIGU / CTCSS_TX) shows in full.
  3. Tab across fields; confirm double-click still tunes.
  4. Import a CSV containing a Mode value with an embedded NUL → confirm it's scrubbed clean and the channel round-trips to the radio and back out to export with no control bytes.

💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat

Overhaul the Memory Channels dialog for usability and data integrity.

Data integrity
- Add MemoryFields::sanitizeText() to strip C0 control bytes (incl. the
  NUL characters seen corrupting Mode) and the reserved 0x7f DEL byte.
- Apply it at all four boundaries: radio status in (handleMemoryStatus),
  command out (encodeMemoryText + mode/offset/tone wire conversion),
  CSV import (parseRecord), and CSV export (recordToFields). Corrupt
  values can no longer reach the UI, the radio, or other SmartSDR tools.

One-click dropdowns (MemoryFieldDelegate)
- Mode / Offset Dir / Tone Mode are strict combos sourced from the
  FlexLib value sets (Slice.cs / Memory.cs); Tone Value, Step and Group
  are type-or-pick combos. The list pops open on edit for a one-click
  pick, and display<->wire conversion is centralized so the table,
  radio I/O and CSV always agree (modes upper-case, offset/tone lower).

Smoother editing & navigation
- Retire the modal Edit-mode toggle: cells are always editable (click a
  selected cell, F2, or type). Tab/Shift+Tab move between fields.
  Double-click stays reserved for Tune.

Column sizing (macOS)
- resizeColumnsToContents ran a few px tight, eliding short values like
  "CW" to one character, and crowded the headers. Pad every column and
  floor each dropdown column to its widest value measured in the table's
  own (themed) font, so DIGU / CTCSS_TX / SIMPLEX always fit. Shorten the
  longest header ("FM TX Offset Dir" -> "Offset Dir").

Tests
- New memory_field_values_test covers sanitization (NUL/TAB/CR/LF/DEL),
  mode validation, and offset/tone display<->wire round-trips.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jensenpat jensenpat marked this pull request as ready for review June 19, 2026 04:14
@jensenpat jensenpat requested review from a team as code owners June 19, 2026 04:14

@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.

Nice, well-structured PR — thanks @jensenpat. I read the full diff (the inline preview truncates) plus the resulting MemoryDialog.cpp/RadioModel.cpp/MemoryCsvCompat.cpp on the branch. All six CI checks are green (build, macOS, Windows, CodeQL, Qt a11y), includes for QTimer/<functional> are present, and the removed onEdit/setInlineEditMode/m_inlineEditMode/m_editBtn plumbing is cleanly excised on both sides.

What I like:

  • Sanitization at every boundary is the right design — centralizing it in MemoryFields::sanitizeText() and wiring it into radio-in (handleMemoryStatus), radio-out (encodeMemoryText + *ToWire), CSV-in (parseRecord) and CSV-out (recordToFields) means corruption genuinely can't enter or escape. The decodeText ordering (0x7f→space then sanitize) is correct.
  • Single source of truth for the value lists / display↔wire conversion, with focused unit coverage in memory_field_values_test. Bonus that you registered the previously-unregistered memory_recall_policy_test with add_test while you were in there.
  • Delegate ownership/RAII is clean (delegates parented to the dialog, validators to the combo), and the setEditorData "preserve an out-of-list value" path is a thoughtful touch for legacy modes.

A couple of minor, non-blocking observations:

  1. toneModeToDisplay collapses unknown tone modes to OFF. FlexLib's FMToneMode reserves ctcss_txrx (your header notes it's "not exposed"). If a memory ever arrives from the radio with that value, populateTable will render it as OFF — a faithful-display gap rather than a crash. If you're confident the radio never emits it for memories, this is fine to leave; otherwise consider preserving the raw token the way the Mode delegate's setEditorData does. Worth a one-line confirmation.

  2. offsetDirToWire/toneModeToWire return "" for unrecognized input. Not reachable today (cols 6/8 use strict, non-editable combos), but if a preserved out-of-list value were committed unchanged it'd send repeater=/tone_mode= with an empty value. Low severity given the strict delegates — just flagging the latent edge for future callers.

  3. Trivial: the if (combo) guard inside the QTimer::singleShot lambda is always true (combo is captured by value and non-null); the combo context object already cancels the timer if the editor is destroyed. Harmless, ignore at will.

None of these block — the core fix and UX work look solid and well-tested. 👍


🤖 aethersdr-agent · cost: $6.8243 · model: claude-opus-4-8

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving — a genuine data-corruption fix plus well-architected ergonomics.

Verified:

  • sanitizeText() correctly drops C0 controls (< 0x20, incl. NUL) + 0x7f DEL, and is wired at all four boundaries. The radio-in path orders it right: decode 0x7f→space first, then sanitize, so legit spaces survive while stray control bytes are stripped. App→radio, CSV import, and CSV export all sanitize too — corruption can't enter the model, reach the radio, or leak to a .csv.
  • Display↔wire conversion is centralized + authoritative (FlexLib Slice.cs/Memory.cs), normalizing case from one place so table/commands/CSV can't drift.
  • isKnownMode() logs an unrecognized CSV mode and passes it through to the radio rather than rejecting — radio stays authoritative (Principle I).
  • New hardware-independent memory_field_values_test; CI green across all six. macOS column-sizing fixes solve a platform-specific problem.

The editing-model change (modal Edit → always-editable + F2/Tab) is a UX refinement the RFC gate technically names — landing it as a maintainer-driven dialog refinement. Nice work @jensenpat.

@ten9876 ten9876 merged commit 903ccfb into aethersdr:main Jun 19, 2026
6 checks passed
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