Repository navigation
feat(gui): TX Band Settings is a CanonWindow workspace that follows the radio's TX profile, band values and answers. Principle II. - #6735
Conversation
…the canon palette. Principle VIII. TX Band Settings moves from a PersistentDialog to a CanonWindow::Kind::Workspace, following AetherRX, AetherTX and Radio Setup: canon header and ground, its own theme scope (canon/txBand), FramelessResizer, and geometry under the key it used as a PersistentDialog (TxBandDialogGeometry), so saved size and position carry over. The menu action creates it directly, like AetherControl, because showOrRaisePersistent() would apply View → Frameless Window. Every hard-coded colour in the table becomes a color.canon.* token through one body sheet with role properties, and the check boxes use the canon indicators. The grid's widgets, object names and accessible names are unchanged. Header labels are polished by the body sheet before they are measured, so the bold 10 px text is never clipped. tune_output_dialog_test gains the workspace shape and the geometry key, including a legacy base64 rectangle; the style docs and Radio Setup's user page list the window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Review — #6735 (head e3fcc6ec)
Edited after the triage pass on #6734: corrected the #6239 citation and the clip-check name, and added the second commit.
Issue fit. #6734 asks the maintainer to pick a scope for TX Band Settings under #6239: colours only, or the full CanonWindow workspace move this PR implements. The ask is: canon header and ground, canon/txBand scope, geometry kept under TxBandDialogGeometry, every colour a color.canon.* token, canon check boxes, same table and same requests. Intent (from the body): TX Band Settings looks and behaves like the other canon tool windows, with the same controls doing the same things. The diff does what the issue describes. Whether it should land is the maintainer's pick on #6734; see below.
Scope
| File | Change | Claimed | Verdict |
|---|---|---|---|
src/gui/TxBandDialog.h / .cpp |
CanonWindow::Kind::Workspace; one token sheet; canon indicators; header-label polish before measuring |
Yes | In scope |
src/gui/MainWindow_Menus.cpp:856-866 |
Direct lazy-construct instead of showOrRaisePersistent() |
Yes | In scope |
tests/tune_output_dialog_test.cpp, tests/tests.cmake |
7 checks; drops PersistentDialog / FramelessWindowTitleBar from the target |
Yes | In scope |
docs/style/*.md (3), docs/user/docs/radio-setup.md |
Window listed; user-facing move/resize/close paragraph | Yes | In scope |
Everything in the diff is explained by the issue. There is one commit, authored today. The - side is only the old hard-coded styles, the per-widget style wrappers, the PersistentDialog base and its open helper. No connect, object name, accessible name, guard or request path was removed. There is no new settings key (the geometry key is the existing one) and no protocol surface. CHANGELOG.md is untouched. The body's checklist claims hold.
Blockers
None in the code.
Needs maintainer decision
- The #6239 pick (@ten9876). #6239: "Items under 'Candidates' are not approved scope until the maintainer picks them." Its "Persistent tool dialogs (one PR per dialog)" line is a token-only candidate (under "surfaces whose widgets move from base tokens to
color.canon.*"), so a fullCanonWindowmove for this window is unlisted, a one-off pick like Radio Setup or AetherControl. #6734 lays out both options, and the PR body asks not to merge before the pick.
Nits (non-blocking)
- Not run per-PR.
tune_output_dialog_testandradio_setup_operations_testare not inci.yml's per-PRcteststeps (the gate is frozen), so CI only compiles them here; they run post-merge infull-suite.yml. - Old builds can't read the new geometry format. After this lands,
CanonWindowrewritesTxBandDialogGeometryasx,y,w,hon hide, so a downgrade to an older build would open the dialog at its default position. Same trade every earlier canon migration made (AetherControl, Radio Setup); noting it for completeness.
Verified vs. read
- Size. Minimum 780×450 on
mainand 816×450 here, measured with the same fixture and font. - Before/after. I built the PR's test file against
main(85882c10, scratch worktree,tune_output_dialog_test). Three checks fail there, as they should: "TX Band Settings is a CanonWindow", "it is a frameless workspace window…" and "hiding the window saves its size under the same key". "Geometry saved by the old dialog restores under its key" passes onmaintoo, which is correct, since it pins carry-over. At the head,tune_output_dialog_testandradio_setup_operations_testpass. - Mutations (run while the change was being built, not re-run for this review):
- dropping
setGeometryKey()fails the restore and save checks; Kind::Dialogfails the workspace check;- measuring header labels before polish fails the clip check (label 83 px < hint 85 px). The second commit (
d0b6ad2e) renames it "the window at its minimum size cannot clip the header text" and sizes the window tominimumSize()instead of a 700×450 that Qt clamps; the same mutation still fails it.
- dropping
- Colour-audit ratchet.
tools/audit_colours.py --compare-src <main src> --strict: unique colours 487 (+0), references 1868 (−11),setStyleSheet591 (−8).tools/check_a11y.py src/gui/TxBandDialog.cpp: 0 findings. - Rendering (checked while the change was being built).
- Demo-mode instance (offscreen, isolated settings,
DEMO-0001, TX off): the window opens from Settings → TX Band Settings... (invoke … trigger) with the canon chrome. The demo reports no TX bands, so there are no rows there. - Rows checked from the test fixture (40 m / 20 m), grabbed under Default Dark and Default Light while switching themes with the window open: the token sheet and canon indicators rebuild on the switch.
- Demo-mode instance (offscreen, isolated settings,
- Read, not run.
- Escape / Ctrl+W go through
QDialog::reject()on bothmainand the head. NeitherPersistentDialognorCanonWindowoverrides Escape, so the hide/close behaviour (and the documented "close and reopen after reconnecting") is unchanged. - The corner ✕ calls
close(), soWA_DeleteOnClosestill gives a fresh session on reopen. - Return in a field still commits through
editingFinished. With no buttons in the window, the workspace's skip-the-default-button rule has nothing to press either way. - Every entry point (Radio Setup → Transmit, Inhibit during TUNE ▸ Configure RCA suppression…) triggers
m_txBandAction, so all of them go through the new construct path.
- Escape / Ctrl+W go through
- Not run. The
code-reviewautomated pass; I did the equivalent pass by hand. CI'sbuild,check-macosandcheck-windowswere still pending at review time; the runs were created aftermain's tip, so the merge base is current.
… its minimum. Principle VIII. The check resized to a fixed 700x450, below the canon window's 816 px minimum, so Qt clamped it and the "restoring a small window" wording no longer matched what ran. It now resizes to minimumSize(), the worst case a restored rectangle can produce, and says so. Measuring the header labels before the sheet polishes them still fails it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Issue fit
Yes, for the option the issue labels option 2. #6734 lays out two scopes — colours only (the #6239 candidate line) or a full CanonWindow move — and this PR implements the second one completely: Kind::Workspace, canon header and ground, corner close button, Esc / ⌘W, FramelessResizer, canon/txBand, geometry carried over under the existing key, every hard-coded colour replaced by a color.canon.* token, canon indicators. Nothing in #6734's description of option 2 is left undone. The issue itself says the pick is the maintainer's, and the PR body repeats it — see the Recommendation.
Scope
| File / group | What it changes | Claimed by the body? | Verdict |
|---|---|---|---|
src/gui/TxBandDialog.{h,cpp} |
PersistentDialog → CanonWindow::Kind::Workspace; one body sheet on a txBandCell role property replaces 10 per-widget sheets; ensurePolished() before measuring headers |
Yes | In scope |
src/gui/MainWindow_Menus.cpp:856-866 |
showOrRaisePersistent() → direct construction |
Yes | In scope, and necessary: showOrRaisePersistent calls dlg->setFramelessMode(...) (MainWindow.h:2359), which CanonWindow does not have — the old call would not compile against the new base class |
tests/tune_output_dialog_test.cpp |
canonWorkspace() (7 checks); clip check resized to minimumSize() |
Yes | In scope |
tests/tests.cmake:4212-4221 |
Drops PersistentDialog.cpp and FramelessWindowTitleBar.cpp from the target |
Implied by the migration | In scope — no remaining TU in that target references either |
docs/style/{aethersdr-style-guide,dialog-patterns,theme-style-guide}.md |
Adds TX Band Settings to the canon lists and canon/txBand to the scope list |
Yes | In scope — but see Nit 1 |
docs/user/docs/radio-setup.md:97 |
One paragraph on chrome, resize and close | Yes | In scope |
No CHANGELOG.md entry — correct. Everything in the diff is explained by #6734 and #6239; nothing to unbundle.
Blockers
None.
Nits (non-blocking)
-
docs/style/aethersdr-style-guide.md:117pre-writes the ruling the PR is waiting for. The sentence the diff edits reads "the tool windows the maintainer has moved to it (Updates, Waveforms, …, Radio Setup, TX Band Settings, …)". The PR body says the opposite — "this fullCanonWindowmove is not on #6239'sCanonWindowlists … please don't merge before the pick" — and my own notes agree that #6239's tool-dialog line is token-only. These are consistent only if merging is the pick; that's a reasonable way to land it, but the doc line should not be read as citing a ruling that already exists. No code change needed — just flagging what the maintainer is ratifying by merging. -
tests/tune_output_dialog_test.cpp:199-238does not run in PR CI.ci.yml:5states "the onlyctest -Rsteps are the ones listed in" that comment, andtune_output_dialog_testappears in no-Rstep in any workflow (its onlytests.cmakelist membership outside its own block isAETHER_SETTINGS_CONSUMERS, which is the settings-store contract, not a gate). All 11 checks ond0b6ad2eare green, but that green covers compilation on three platforms plus Static checks — not the 7 new canon-workspace checks. Your local run and your stated mutation checks are the only evidence those pass; worth saying so explicitly in the test plan so the maintainer doesn't read the badge as covering them. -
Reopen after Esc / ⌘W reuses the old instance; reopen after ✕ rebuilds it —
MainWindow_Menus.cpp:858.CanonWindowwires ✕ toQDialog::close()(CanonWindow.cpp:74) but ⌘W toQDialog::reject()(CanonWindow.cpp:79), and Escape goes throughQDialog's own reject path;reject()hides without a close event, soWA_DeleteOnClosedoes not fire and theQPointerstays live. The window title'sCurrent TX Profile: %1, the table rows, and theRadioControlSessioncaptured atTxBandDialog.cpp:145are therefore refreshed on one close path and stale on the other. This is pre-existing, not introduced here —showOrRaisePersistentalready setWA_DeleteOnClose(MainWindow.h:2358), so the asymmetry is unchanged by this PR, and the canon header shows the samewindowTitle()the frameless title bar showed before. Mentioning it only because the new header makes the stale profile string more prominent; a follow-up issue rather than anything for this PR.
What I tried to break
- Dropped behaviour from
showOrRaisePersistent. The new handler omitstrackPersistentDialog(dlg). I chasedm_persistentDialogsto its only consumer (MainWindow.cpp:10254-10265) — it exists solely to propagatesetFramelessMode(). Nothing else (shutdown, disconnect teardown) reads the list, andtrackPersistentDialogtakesPersistentDialog*, whichTxBandDialogno longer is. The drop is complete and correct, and matchesdialog-patterns.md's "not tracked bytrackPersistentDialog()". - Sibling call sites. Grepped for every
TxBandDialogconstruction in the head checkout: one, atMainWindow_Menus.cpp:859. The two other entry points (MainWindow_Menus.cpp:992,MainWindow.cpp:3592) bothm_txBandAction->trigger(), so the body's "same action" claim holds and no surface is left on the old path. TheisConnected()guard and them_txBandAction->setEnabled(cmdPlane)gate atMainWindow.cpp:8296are untouched. - Lifetime, now that the window can be deleted.
FramelessResizer::installinstalls an application-wide event filter (FramelessResizer.cpp:46-51) and its own comment reasons about instances outliving close. It is parented to the window (QObject(window)), Qt removes the filter on destruction, and~FramelessResizerclearsm_manualResizeActiveand callsleaveEdgeZone()so a destroyed-mid-drag resizer cannot strand an override cursor. No static registry, no accumulation across reopens. Clean.ControlAvailabilityRegistryis likewise parented to the dialog (TxBandDialog.cpp:141) andpruneDead()/QPointerentries cover widget death, so nothing dangles. - Stale-session writes.
RadioModel::requestTxBandcompares the captured session againstcontrolSession()and returnsUnsupported(RadioModel_Setup.cpp:196), so an instance that survived a reconnect cannot write to the new session. Pre-existing protection, unchanged by this PR. - Theme resolution.
canon/txBandhas no scope entry indefault-dark.jsonordefault-light.json— neither doescanon/radioSetup,canon/aetherRxor any sibling; resolution falls through to the registeredcanonscope, and the sheet's{{color.canon.*}}tokens are root tokens present in both theme files. Consistent with every other canon window, not a gap. - Stylesheet specificity, since the comment at
TxBandDialog.cpp:25-29stakes a claim on it.QWidget#canonBody QWidget { background: transparent; }vs the cell rules:[txBandCell="value"]adds an attribute and outranks it;QLineEditties on specificity (one id + one type) and wins by appearing later;canonIndicatorRules()is appended last so its::indicatorrules win ties, and it degrades to Qt's own indicators by returning{}if the cache write fails. The reasoning in the comment is sound. - Migration shape.
restoreGeometryFromSettings(CanonWindow.cpp:153-181) reads the legacy base64 form only when the value does not split into four comma-separated ints, and the next save rewrites it asx,y,w,h— a one-shot claim-and-replace, not a perpetual legacy fallback, which is what AGENTS.md asks for.TxBandDialogGeometrystays a flat key, which is allowed: it is app-global window geometry, not per-radio state. - The test's own teeth.
check(dialog.windowType() == Qt::Window && ...FramelessWindowHint)does fail underKind::Dialog, sinceCanonWindow.cpp:49picksQt::Dialogthere — the stated mutation holds. The restore check writes the settings value beforeshow(), soshowEvent's one-shot restore sees it, andcheck(restored.height() != chosen.height())fails safe rather than passing vacuously.saved.endsWith(",w,h")cannot pass on an empty value. Comparing height rather than width is consistent with the code: the offscreen screen is 800 px and the table's minimum is now 816, so a restored width is clamped. - CMake contract. The target is already in
AETHER_SETTINGS_CONSUMERS(tests.cmake:9751), which it now needs since the test callsAppSettings::instance().load(), and it isolates throughTestSettingsProfile.h. - Socket tests: none added, modified or removed. No
QTcpServer/QUdpSocket/bind/listen/connectToHost/fake-peer anywhere in the changed test, andcheck(f.commands.isEmpty(), ...)asserts the new path sends nothing. - Security audit (required by the harness). Ran the
security-auditsubagent over the listed files. Every one of the 114 CodeGuard findings is a false positive, and all but two sit in files this PR does not touch — the harness list is main's tip, not the diff. The two that do intersect:CS-VLN-CMDI-AMPatMainWindow_Menus.cpp:851,859is C++ address-of (&QAction::triggered,&m_radioModel) with noQProcessin the file, andCS-VLN-SSRF-HEXattune_output_dialog_test.cpp:143is the Flex status code0x5000002d, not an IP.TxBandDialog.cpp's only untrusted input isQLineEdit::text().toInt(), guarded by the validity flag andqBound(0, parsed, 100)(lines 164-168, 178-181). Separately, the audit surfaced a pre-existing item in untouched code —FirmwareStager.cpp:554skips MD5 verification of a downloaded installer entirely when the vendor hash fetch fails, rather than failing closed. Out of scope here; worth its own issue.
I could not verify any of this at runtime — no build, no test run, no GUI. Every finding and every clean result above is reasoned from the code in the head checkout, plus CI status. Specifically unverifiable: the contrast table, the radio_setup_operations_test positional-index claim, the 2 px "RCA off/TUNE" clip the polish ordering fixes, the 780→816 px measurement, and the visual result in either theme. Those rest on your local runs and the screenshot.
Recommendation
Needs maintainer decision — and the decision is the scope, not the code. The implementation is careful: it keeps every object name, accessible name and wire effect, routes the sheet through ThemeManager so a theme change reaches it, carries the saved geometry forward through a one-shot migration, and the one dropped helper call (trackPersistentDialog) is provably complete. I could not find a correctness defect. But #6734 is explicit that the full CanonWindow move is not on #6239's approved lists, and merging this PR is the pick — if the maintainer prefers option 1 (colours to color.canon.*, window chrome unchanged), this PR is the wrong shape and the three style-doc hunks would need to come back out.
Concrete next step for the maintainer: rule on #6734's option 1 vs option 2. If option 2, the only things I'd ask of the author first are the two nits — note in the test plan that tune_output_dialog_test is not in a PR -R gate, so the 7 new checks are local-only evidence. Nice work on the mutation checks and on measuring the minimum-size change before anyone asked.
🤖 aethersdr-agent · cost: $16.9066 · model: claude-opus-5
|
Cisco CodeGuard — static analysis of this PR (122 finding(s))
Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them. 🤖 aethersdr-agent · cost: $17.0758 · model: claude-opus-5 |
|
Coordination note from #6736, which is landing now. #6736 adds TX Band Settings raises no message boxes today, so nothing in this PR needs to change for it. Two notes:
|
…rinciple VIII. The canon body sheet clears every descendant to transparent so the canon ground shows through, and Qt parents a field's standard right-click menu (Undo/Cut/Copy/Paste/Select All) to the field, so the rule reached it: the menu on the RF and Tune power fields drew no background at all. The sheet now gives QMenu its canon panel back (raised, lineHi border, cyan selection, unavailable for disabled items, line separators). tune_output_dialog_test gains a check that the menu is parented inside the canon body (control) and draws an opaque background; it fails without the QMenu rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ten9876
left a comment
There was a problem hiding this comment.
Review of #6735 at head 078b162a
Maintainer pick on #6734: option 2, the full CanonWindow workspace move this PR implements. @ten9876 has ruled on the scope, so the "Needs maintainer decision" item in the earlier reviews is settled. The style-doc lines that list TX Band Settings among the windows the maintainer has moved are now accurate.
Issue fit. #6734's option 2 is fully implemented:
Kind::Workspace, with the canon header and ground, the corner ✕, Esc and ⌘W to close, andFramelessResizer;- the
canon/txBandtheme scope; - geometry under the existing
TxBandDialogGeometrykey; - every colour is now a
color.canon.*token; - the check boxes use the canon indicators;
- the table, the object names, the accessible names and the requests sent to the radio are unchanged.
The third commit (078b162a, the right-click menu fix) came after both earlier reviews, so this review looked hardest at that commit.
Scope
| File | Change | Claimed | Verdict |
|---|---|---|---|
src/gui/TxBandDialog.{h,cpp} |
Moves to CanonWindow::Kind::Workspace; one token sheet keyed on txBandCell; canon indicators; header polish before measuring; QMenu panel rules |
Yes | In scope |
src/gui/MainWindow_Menus.cpp:856-866 |
Constructs the window directly instead of calling showOrRaisePersistent(); the isConnected() guard is kept |
Yes | In scope, and required by the base-class change |
tests/tune_output_dialog_test.cpp, tests/tests.cmake |
New canonWorkspace() and rightClickMenuIsOpaque(); the clip check now resizes to minimumSize(); PersistentDialog.cpp and FramelessWindowTitleBar.cpp dropped from the target |
Yes | In scope |
docs/style/*.md (3), docs/user/docs/radio-setup.md |
List the window and its scope; a paragraph on moving, resizing and closing it | Yes | In scope |
Everything in the diff is explained by #6734. All three commits were authored today. The removed lines are only the old styles, their per-widget wrappers, the PersistentDialog base and its open helper. No connect, guard, object name or request path was removed. There is no new settings key, no protocol surface and no CHANGELOG.md entry.
Blockers
None.
Nits (non-blocking)
- On Linux, the header-clip check passes with the fix reverted.
TxBandDialog.cpp:147-153andtests/tune_output_dialog_test.cpp:113-121. I restored the original order (unparented grid,setMinimumSize(sizeHint())beforeaddWidget) on Linux (Qt 6.12, offscreen, Adwaita Sans), and every check still passed. The "RCA off/TUNE" label measures 101 px before polish and 80 px after. The default font there is wider than the sheet's bold 10 px, so measuring too early over-sizes the label instead of clipping it. The mutation evidence in the body (83 px < 85 px) is macOS-only, so the Full Suite on Linux won't catch a regression of the polish order. The fix itself is still correct: it is what makes the minimum follow the sheet's font on every platform. Consider a check that doesn't depend on which font is wider. I triedlabel->minimumSize() == label->sizeHint()and it doesn't work, becauseQLabelcaches its size hint across the polish (both read 101 under the mutation). - "Constructing the table sends no commands" doesn't guard the reorder that #6734's triage named.
TxBandDialog.cpp:217-222. I movedchk->setChecked(cb.val)after thetoggledconnect, and bothtune_output_dialog_testandradio_setup_operations_teststill passed. The fixture's bands report every bandset flag false, sosetChecked(false)on an unchecked box emits nothing. Addingtx1_enabled=1toFixture::current()'sinterlock bandstatus still didn't make the reorder fail, so the fixture needs a field thattxBandSettings()actually picks up. The gap predates this PR, but the body cites this check as the guard, so please don't count it as one. - The header shows a stale profile name (
TxBandDialog.cpp:233,makeCanonHeader(windowTitle())). The automated pass found this. The title capturesactiveProfile()once, at construction. Switch the TX profile while the window is open and the 17 px heading still names the old one. The old title bar had the same problem, but the heading makes it prominent. On the demo radio it reads "TX Band Settings (Current TX Profile: )". Worth a follow-up issue, not this PR. - Screen-reader names. Only the RCA off/TUNE box has an accessible name. The RF and Tune power fields and the eight bandset boxes have none (
TxBandDialog.cpp:179-222). This predates the PR andcheck_a11y.pydoesn't flag it, but these controls are in the constructor this PR rewrites, and #4896 is open. A follow-up, not a blocker. - The CI green is from an older
main. The PR's runs were created at 19:01 UTC, andmainhas moved 8 commits since (tip3b4931f08, 22:26 UTC).git merge-treeagainst currentmainis clean, and none of those commits touches a file in this diff except one unrelated hunk inMainWindow_Menus.cpp(showUpdatesWindow). I have no reason to think the result changes; noting it becausestrict: falsewon't rerun it.
Automated pass: findings I checked and dropped:
- The blanket
QWidget#canonBody QWidget { background: transparent }will need a special case for every new popup type. That is the house pattern:ConnectionPanel.cpp'skConnectionStyledoes the same and addsQMenuback. - The centre-pixel test can land on a glyph. Deleting the
QMenurules does fail it here. - Restored height clamped by the 600 px offscreen screen. The chosen height is 560 today, so not yet.
- Downgrades can't read the new geometry format. Already disclosed in the earlier review.
- The open block duplicates other canon windows. That is the same pattern the other canon windows use.
- Taller rows. Speculative, and the author's FLEX-8600 screenshot shows the full table.
Verified vs. read
- Built at the head on Linux (Qt 6.12, shared core):
AetherSDR,tune_output_dialog_testandradio_setup_operations_test. Both tests pass, and all 10 new checks pass. - Mutations, with the source restored between runs:
- deleting the
QMenurules fails "a field's right-click menu has an opaque background"; Kind::Dialogfails "it is a frameless workspace window";- the pre-polish order and the
setCheckedreorder pass, as described in nits 1 and 2.
- deleting the
- Static gates, base vs. head: identical. Engine boundary is at 101 findings, the manifest is up to date, there are 70 capability booleans, and 125 command-plane entries. The colour audit matches the body: unique colours +0, references −11,
setStyleSheet−8.check_a11y.pyreports no findings. - Bridge, on the demo radio (offscreen, isolated settings,
DEMO-0001, TX off):invoke "TX Band Settings..." triggeropens a visibleAetherSDR::TxBandDialog. It is 761×450 with only the header row, because the demo reports no TX bands.grabshows the canon header, ground, ✕ and a cyan header row on raised cells. - Read, not run: the Esc / ⌘W / ✕ close paths and the drop of
trackPersistentDialog. The earlier review covered both; I re-read them and agree. - No socket tests were added, modified or removed.
…rofile, session or band rebuilds the table, refused changes show the radio's value. Principle II. The open table belongs to one TX profile, one radio session and one set of bands (by id and name). When any of them changes it is rebuilt; the heading and window title name the profile, focus returns to the same control, and text being typed is dropped rather than written. Esc and Ctrl+W hide the window, and showing it again builds a fresh table, as closing with the corner button and reopening does. A value the radio reports for a band reaches the open table without being written back. Only a value the radio changed since it was last shown is written, so a click or entry the radio has not yet echoed stays as the operator set it, and a field with uncommitted typing keeps it. A hidden window does not refresh or rebuild. A TX band write now carries a completion, as a transverter write does: FlexBackend passes it to the setup command sink, and RadioModel fences it to the caller's session and context and refuses it at once when the write is not dispatched. A check box or power field the radio refuses, or that is never sent, shows the radio's value again. The RF and tune power fields and the eight bandset check boxes get accessible names that give the column and the band. The eight columns, their headers and their names come from one table that building and refreshing both read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…both the multiFLEX Dashboard and TX Band Settings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ten9876
left a comment
There was a problem hiding this comment.
Approved at 6a364bbb2
Maintainer rulings (@ten9876, through /pr-land):
- #6734: option 2, the full
CanonWindowmove. - The open table is rebuilt when the TX profile changes.
- Reopening the window after Esc or ⌘W builds a fresh table.
- A TX band write gets a reply path, so a change the radio refuses shows the radio's value again.
What the landing commit 0a662a804 did with each finding
From my review on 078b162a, the earlier reviews, and two automated passes over the landing commit:
- On Linux, the header-clip check passed with the fix reverted.
tests/tune_output_dialog_test.cppnow also checks that a header's minimum width is its text in the font the sheet gives it. Measuring before polish fails that check on Linux (101 px against 80 px), and the existing clip check catches the macOS direction. - The check "constructing the table sends no commands" couldn't see a bandset write. That fixture has no setup sink.
radio_setup_operations_testnow builds the table with HWALC confirmed on, asserts the box is shown checked, and asserts nothing was written. Connecting a box before setting its value fails it. - The header showed a stale profile (ruling: rebuild on profile change). A new TX profile, a new radio session, or a band added or renamed rebuilds the table. The heading and window title name the profile, focus returns to the same control, and text being typed is dropped rather than written. Values the radio reports reach the open table, and only values the radio changed since they were last shown are written. So a click or entry the radio hasn't echoed stays as the operator set it, and a field with uncommitted typing keeps it.
- Esc reused the old window while ✕ rebuilt it (ruling: rebuild on every open).
TxBandDialog::showEventrebuilds after a non-spontaneous hide, so this covers every way of opening it. The menu handler just shows and raises the window. - A refused write left the widget wrong (ruling: add a reply path).
IRadioBackend::requestTxBandandFlexBackend::requestTxBandtake aRadioSetupCompletion, asrequestTransverteralready does.FlexBackendpasses it to the setup sink.RadioModel::requestTxBandfences it withfencedSetupCompletionand answersRefusedat once when the write isn't dispatched.- In the dialog, a refused or failed reply puts the radio's value back in the check box or power field.
- Engine-boundary findings for the touched files are identical to
main; the command-plane count stays 125.
- Screen-reader names. The RF and tune power fields and the eight bandset boxes are named by column and band, for example "RCA TX1, band 40". The eight columns, their headers and their names come from one table that both building and refreshing read.
- Docs. The
radio-setup.mdparagraph sits in the TX Band Settings section and describes what the table does. The troubleshooting note no longer tells users to reopen TX Band Settings after reconnecting. - Merge. #6768 changed the same canon window lists. The merge
6a364bbb2keeps both entries; the style docs differ frommainonly by TX Band Settings andcanon/txBand.
Refuted from the automated passes:
- Filter the refresh by status object name. That would put Flex status object names in a
gui/file, which is wire text above the seam (#5554). The refresh compares a few integers and booleans per band, and a hidden window doesn't run it. - Store a
RadioControlSessionmember. That needs acore/backendsinclude in agui/header, a new touchpoint. A captured closure avoids it. - A hidden window still runs per-box suppression syncs. Those existed before this PR, and they're unchanged.
- Blocking signals before deleting the table. With real focus, deleting a focused field doesn't emit
editingFinished. The test that types into a focused field and then switches profile proves nothing is written, so the guard was dead code and was removed.
Evidence
Built on Linux x86_64 (Qt 6.12, shared core): AetherSDR, tune_output_dialog_test, radio_setup_operations_test and radio_setup_control_routing_test. All pass on the merged head:
100% tests passed out of 3
Each guard in the landing code was broken on purpose and its test went red, 18 mutants in all:
- measuring before polish;
- connecting a box before setting it;
- no rebuild on a profile, session, band, band name or reshow;
- no refresh;
- no typing guard, or no signal blocker, on refresh;
- writing every value instead of only changed ones;
- no focus restore;
- swapped columns;
FlexBackenddropping the completion, orRadioModelnot forwarding it or not refusing an undispatched write;- the dialog ignoring a refusal, or reverting an accepted change.
Static checks were run locally on the merged tree, and all pass:
- engine boundary (101 findings, the same as
main), touchpoint manifest, seam probe table; - user-docs reference, test registration, WDSP links;
- capability records (70), command plane (125), per-PR test gate;
- network timeouts, QSettings, theme seed.
The colour audit against the base gives unique +0, references −11 and setStyleSheet −8. check_a11y.py reports no findings.
Demo radio (offscreen, isolated settings, DEMO-0001, TX off):
- Settings → TX Band Settings... opens one window, and triggering it again raises the same one.
- ✕ deletes it, and reopening builds a new one.
- The headers render as before.
Not verified here: live FLEX behaviour, which this pass did not touch. On real firmware, a radio that refuses a bandset with an error reply now drives the revert; the test backend's Failed reply stands in for that.
… RC-28 entries. Principle VIII. aethersdr#6735 (TX Band Settings) added an entry to the same canon-window, geometry and theme-scope lists in docs/style/aethersdr-style-guide.md, dialog-patterns.md and theme-style-guide.md; the resolution keeps both. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…the import default and a small legacy rectangle. Principle VIII. The window's 760x420 minimum was an explicit floor measured on macOS fonts. An explicit minimum overrides the layout's own, so on a font with wider metrics the seven buttons would clip at the minimum, and the per-button fit test would go red with nothing regressed. The minimum is now 760x420 or the layout's minimum size hint, whichever is larger, recomputed on a theme change. The fit test replaces its fixed-number check with the layout-relative one the review suggested, and runs it under the application font and a 160% letter-spaced font (layout needs 929 px natively, 949 px offscreen; the 760 floor alone fails that row). New cases: - a 640x380 legacy base64 rectangle opens at the 760x420 minimum (Qt's restoreGeometry() clamps to it; the base64 branch has no expandedTo of its own); - Return in the Import box takes Skip existing: the existing net keeps its name and only the new one is added (Qt's own file picker via AA_DontUseNativeDialogs); - the Ctrl+W / Cmd+W row activates the window first, since the close shortcut is window-scoped; a desktop session that refuses the activation skips the row instead of failing it. Also: the disabled-row ink is resolved once per fill, not per row; the lifetime audit row goes back to its baseline line (173); the CanonWindow hideEvent comment and dialog-patterns row say that reject() closes the window without a closeEvent(); and the two Net Scheduler screenshot recipes target the editor by its objectName (netEditorDialog), with alt text naming the canon header and corner close button. Rebased onto main (aethersdr#6735, aethersdr#6745, aethersdr#6768 add windows to the same style-doc lists; every entry is kept). The touchpoint manifest counts this branch's ThemeManager include on top of main's: 184 -> 185. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Closes #6734. Part of #6239 and approved RFC #6226. On #6734 the maintainer picked option 2: the full
CanonWindowmove, not colours only.TX Band Settings (
TxBandDialog) moves from aPersistentDialogto aCanonWindow::Kind::Workspace, following AetherRX, AetherTX and Radio Setup. It is a workspace rather than a dialog because it opens from Radio Setup, which is itself a workspace window, and a dialog parented to the main window would open underneath Radio Setup.FramelessResizer.canon/txBand.TxBandDialogGeometry).CanonWindowreads the old base64 rectangles and rewrites them asx,y,w,hon the next save, so the saved size and position carry over.color.canon.*token, through one body sheet that selects on atxBandCellrole property (header, band, value, note). All cells share one background token.canonIndicatorRules()).raisedwith alineHiborder,cyanselection,color.control.unavailablefor disabled items, andlineseparators.IRadioBackend::requestTxBand,FlexBackend,RadioModel::requestTxBand): a TX band write now carries a completion, as a transverter write already does.FlexBackendpasses it to the setup command sink.RadioModelfences it to the caller's session and context, and refuses it at once when the write is not dispatched. Both new parameters default, so other callers are unchanged. OnlyFlexBackendimplements the method; the engine-boundary findings for the touched files and the command-plane count are the same as onmain.MainWindow_Menus.cpp): the menu action creates the window directly, as AetherControl does.showOrRaisePersistent()would apply View → Frameless Window, which a canon window ignores. TheisConnected()guard stays, and an existing window is shown and raised. The other two entry points (Radio Setup → Transmit, and Inhibit during TUNE ▸ Configure RCA suppression…) trigger the same action.docs/style/dialog-patterns.md,aethersdr-style-guide.mdandtheme-style-guide.mdlist the window and its scope.docs/user/docs/radio-setup.mdsays how the window moves, resizes and closes, and that the table follows the radio. The troubleshooting note no longer tells users to reopen TX Band Settings after reconnecting.Intent
After this merges, TX Band Settings looks and behaves like the other canon tool windows, follows the theme, and shows the active TX profile's values as the radio reports them. The same controls send the same commands. Out of scope: the table's columns and layout, and the other persistent tool dialogs (one PR each, per #6239).
Visible size change: the minimum grows from 780×450 to 816×450, measured with the same two-band test fixture at the same font on
main(85882c10) and on this branch. The extra 36 px come from the canon cell padding, not from the check boxes.Contrast, from the theme files against solid cell backgrounds (the window ground has a gradient, so the note row is approximate). Every pair clears the 4.5:1 that
docs/a11y.mdasks for:Constitution principle honored
Principle II (Radio-Authoritative State): the open table shows what the radio reports for the active TX profile, and showing it never writes it back. Principle VIII (Evidence Over Assertion): each behaviour below has a check that fails when it is reverted.
Test plan
cmake --build build)AetherSDR,tune_output_dialog_testandradio_setup_operations_teston macOS arm64 (Qt 6.12.0) and Linux x86_64 (Qt 6.12.0, shared core).DEMO-0001, TX off): opening from Settings → TX Band Settings..., closing with ✕ and reopening leaves exactly one visible window.tune_output_dialog_testandradio_setup_operations_testpass locally. Neither is inci.yml's per-PRctest -Rsteps (the per-PR gate is frozen,docs/agents/tests-ci.md), so the per-PR jobs only compile them; they run after merge infull-suite.yml.CanonWindow; it is a framelessQt::Window; legacy geometry restores and hiding saves under the same key; a field's right-click menu is opaque; a header's minimum width is its text in the canon sheet's font; the heading names the profile; a reported band value and a reported keying output reach the open table; a field being typed in keeps the operator's text; a new TX profile renames the heading and window and rebuilds the table; a band the radio adds gets its own row; a band whose name arrives later is rebuilt under its name; reopening a hidden window builds a fresh table; a click that can't be sent shows the radio's value again. Inradio_setup_operations_test: constructing the table writes nothing and shows a confirmed-on value; an unrelated report and the radio's echo keep a click, and a later report moves the box without writing it back; a check box or power field the radio refuses after it was sent shows the radio's value again, and an accepted one stays; a new session rebuilds the table, and its fields write; text being typed when a new profile rebuilds the table is not written, and focus returns to the same field. None of this sends a command.setGeometryKey();Kind::Dialog;QMenurules;FlexBackenddropping the completion,RadioModelnot forwarding it, orRadioModelnot refusing an undispatched write;setStyleSheet591 (−8).tools/check_a11y.py src/gui/TxBandDialog.cppreports nothing.Checklist
docs/COMMIT-SIGNING.md)AppSettingscalls — use nested-JSON-under-one-key(Principle V)
TxBandDialogGeometry.reverse-engineered from a proprietary binary (Principle IV)
MeterSmoother(AGENTS.md convention)docs/and theaffected READMEs. Not
CHANGELOG.md, which is a release-prep file aPR must not add to (AGENTS.md); describe it in the Summary above instead
docs/user/docs/radio-setup.md.🤖 Generated with Claude Code