Repository navigation
feat(gui): Net Scheduler is a CanonWindow workspace, built like AetherRX and AetherTX, and its questions are CanonMessageBoxes. Principle VIII. - #6762
Conversation
|
Following up the #6761 triage. Pushed Fixed: right-click menus with no panel. The triage didn't raise this one, but it's the same defect #6735 and #6753 hit. The canon sheet's transparent Added: close paths. Triage items, in the body now:
Already true at
One triage point I didn't take: " |
There was a problem hiding this comment.
Issue fit
Partially — the implementation matches #6761 almost exactly, but the authorization it claims does not exist yet.
Every mechanical item #6761 asks for is delivered and I verified each against the head checkout: Kind::Workspace + canon/netScheduler + FramelessResizer::install (src/gui/NetSchedulerDialog.cpp:561-575), geometry under the existing key, the canon sheet with the window-local QDoubleSpinBox/QDateTimeEdit/popup rules, the merged editor form with the divider as a spanning row, the editor correctly left as Kind::Dialog so setDefault(true) still means something, all seven QMessageBoxes converted (I counted them: 5 × showNotice + Remove + Import merge), and both palette(...) sheets gone. The triage comment's three extra asks are all honoured too — the :586 QPalette::Disabled brush moved onto ModemChrome::colour(), and the contrast table covers both themes.
The PR also correctly declined the triage suggestion to drop the wasFresh guard. The triage reasoned it is always true; it isn't — the window is non-modal, so re-triggering the menu while it is open leaves m_netSchedulerDialog non-null. Keeping the guard is right.
What's missing is the ruling. See Blockers 1.
Scope
| File / group | What it changes | Claimed? | Verdict |
|---|---|---|---|
src/gui/NetSchedulerDialog.{h,cpp} |
PersistentDialog → CanonWindow::Kind::Workspace; canon sheet; 7 CanonMessageBoxes; editor → canon dialog |
Yes | In scope |
src/gui/MainWindow_Nets.cpp |
Direct create + WA_DeleteOnClose instead of showOrRaisePersistent() |
Yes | In scope — mirrors the SpotHub precedent at MainWindow_Menus.cpp:656-669 line for line |
tests/net_scheduler_dialog_test.cpp, tests/tests.cmake |
New 10-case target; gui_nested_lifetime_test swaps PersistentDialog.cpp/FramelessWindowTitleBar.cpp for CanonMessageBox.cpp/CanonWindow.cpp/ModemChrome.cpp |
Yes | In scope. I checked all 21 sources of gui_nested_lifetime_test: the only remaining PersistentDialog/FramelessWindowTitleBar hits are in comments, so the source swap doesn't break the link |
docs/style/* (3 files) |
Adds Net Scheduler to the canon window / scope / geometry lists | Yes | In scope, but see Blockers 1 — these edits assert the maintainer decision as already made |
docs/user/docs/net-scheduler.md |
Chrome paragraph, Remove-defaults-to-Cancel, and adds the previously-undocumented Tune Now button | Yes (body calls out the Tune Now omission) | In scope |
docs/architecture/aetherd-touchpoints.md |
ThemeManager includers 183 → 184 | Yes | Correct — I counted 184 include "core/ThemeManager.h" in the head vs 183 on main |
docs/gui-nested-loop-lifetime-audit.md |
Line anchor 173 → 212 | Yes | Wrong number — see Nits |
No file is unexplained, no new protocol/settings surface, no removed guard. No CHANGELOG.md entry — correct.
Socket-test disclosure: tests/net_scheduler_dialog_test.cpp greps clean for QTcp|QUdp|QLocalServer|QWebSocket|listen(|bind(|connectToHost|Fake — genuinely socket-free, no peer. Nothing to flag.
Blockers
1. docs/style/dialog-patterns.md:273-277 — this is an unlisted pick, and the PR body states the opposite. The body opens "Part of #6239 and approved RFC #6226." The triage ruling on #6761 (still maintainer-review, still unanswered) says otherwise:
In #6239 the line "Persistent tool dialogs (one PR per dialog)" sits under "Candidates: surfaces whose widgets move from base tokens to
color.canon.*" — a re-skin that keepsPersistentDialogchrome. The CanonWindow candidate list is a separate, closed set…
And the sentence this PR edits says so itself:
Use `CanonWindow` only for windows the
style guide covers: short-lived windows, and the tool windows the maintainer
has moved to it (… Radio Setup, Net Scheduler). … Other tool
and workspace dialogs keep the persistent pattern above.
Adding "Net Scheduler" to that parenthetical asserts a move the maintainer has not made. Merging the PR is the ruling. Sibling #6753 handles this honestly — "Waiting on the maintainer's pick … please don't merge before the pick." #6762 should carry the same banner. GOVERNANCE.md puts the decision with the maintainer; this is one of eight same-shape PRs opened 2026-10-24, and the triage note asks for one ruling to settle all eight.
Not a code defect — a disclosure one. Concretely: change "Part of #6239" to state it is an unlisted pick awaiting the #6761 ruling, as #6753 does.
2. tests/net_scheduler_dialog_test.cpp:98-114 — buttonsFitAtMinimumSize is the font-metric assertion the triage explicitly asked you not to land.
QVERIFY2(button->width() >= button->sizeHint().width(),
qPrintable(QStringLiteral("%1 is %2 px, needs %3")Triage on #6761:
760×420 is fine as a minimum, but it is font-dependent (canon mono metrics + canon button padding). CI renders on DejaVu Sans with themes unloaded, so the PR must not land a
minimumWidth() <= Nstyle assertion — reportminimumSizeHint()before/after at a stated font instead (the 260 px rail lesson, #6131).
Because setMinimumSize(760, 420) is an explicit minimum, it overrides the layout's own, so the check is meaningful rather than tautological — which is exactly why it is font-sensitive: the seven canon buttons' sizeHint() is a function of the rendering font, and 760 is a hand-picked number validated on macOS arm64. On a CI image with different metrics this reds out with nothing regressed. This isn't hypothetical for this repo (#6131's rail-fit tests, and the exact-pixel probes that went the same way). Worse, net_scheduler_dialog_test is not in ci.yml's frozen per-PR ctest -R gate, so the failure lands post-merge in full-suite.yml, attributed to this PR after it is unrevertable in place.
Suggested shape — assert the layout fits, not that a hand-picked number happens to equal a font's metrics:
// The minimum must fit what the layout needs, whatever the font:
// compare the pinned minimum to the layout's own, rather than to a
// number measured on one machine's fonts.
dlg.layout()->activate();
QVERIFY2(dlg.minimumSize().width() >= dlg.bodyWidget()->minimumSizeHint().width(),
qPrintable(QStringLiteral("minimum %1 px, layout needs %2 px")
.arg(dlg.minimumSize().width())
.arg(dlg.bodyWidget()->minimumSizeHint().width())));If you'd rather keep the per-button loop, gate it on a stated font (set one in the test) so a CI font swap can't red it.
Needs maintainer decision
3. src/gui/NetSchedulerDialog.cpp:679 — the disabled-net row is 4.22:1 in the light theme, under docs/a11y.md:143's "4.5 : 1 for normal text against its background."
const QColor muted = ModemChrome::colour("{{color.canon.muted}}", this);Credit where due: the body discloses this and asks for the call. I reproduced the figure independently from resources/themes/default-light.json rather than taking it — canon.muted #506070 on canon.control #c8d2dc is 4.22:1; canon.muted on canon.raised #ffffff is 6.47:1, and dark is 5.39:1 / 6.39:1. So only the non-alternate light row fails, exactly as the table says. The whole contrast table checks out.
The body's alternative (canon.inkSoft, 5.60:1 on control) clears the bar but is the same ink as an enabled row, leaving the missing ✓ and the "—" in Next as the only disabled cue. Both options are defensible; it's your call, and whatever you pick applies to the rest of the wave. Nothing automated gates this — check_a11y.py has no contrast rule.
4. Disclosed visual consequences, flagged because AGENTS.md puts visual design with you, not because they look wrong: the window minimum grows 640×380 → 760×420 and the editor 440 → 560 px; the canon sheet makes the whole table monospace including the Name column; and the import box's button row reverses (main: Skip | Overwrite | Import as new | Cancel → now Cancel | Import as new | Overwrite | Skip existing, default unchanged at Skip existing).
Remove-defaults-to-Cancel is a fix, not a preference — it has authority: #6736 (merged, FirmwareInstallDialog) is the precedent the triage itself named, it moves a destructive action off Return, and CanonMessageBox.h:35-37 requires an explicit default anyway, which is what makes Return's target the same on all three platforms. No preference smuggling found anywhere in the diff.
Nits
All non-blocking.
-
docs/gui-nested-loop-lifetime-audit.md:48— the anchorNetSchedulerDialog.cpp:212matches nothing, at any baseline.editNetEntryis at 234 in the head, 225 at this branch's first commit (701b5a3), 173 onmain, and 170 at the audit's own stated baselinec692c75e. Head line 212 is a barebreak;inside the recurrence-rule switch. The doc's preamble also says "Line numbers refer to the baseline, so they remain reproducible after this patch," so arguably the row should have stayed at 173 untouched. Either way, 212 is wrong:| `NetSchedulerDialog.cpp:234` | Scoped editor, copied capture callback and stable schedule IDs across loops. Double-click opening is deferred until the table event returns. Scheduler is delete-on-close. | -
The body attributes the legacy-geometry clamp to the wrong branch. It says "
CanonWindowreads the old base64 rectangles … A rectangle narrower or shorter than the new 760×420 minimum is widened to it on the first restore (expandedTo(minimumSize()))". That call is only on thex,y,w,hbranch,src/gui/CanonWindow.cpp:169; the base64 branch — the one every existing Net Scheduler user is on — is a barereturn restoreGeometry(QByteArray::fromBase64(saved.toLatin1()))at:181, with noexpandedToand no screen-intersection re-check. AndlegacyGeometryCarriesOverseeds 780×520, already above the 760×420 minimum, so the below-minimum legacy rectangle the body claims to handle is the one case the test doesn't cover. Seeding a 640×380 legacy rect would close it in one line. -
docs/style/dialog-patterns.md:243propagates a stale claim. The row you appended "Net Scheduler" to still reads "(flushed — Escape and ⌘W / Ctrl+W hide it without a close event)", andsrc/gui/CanonWindow.cpp:129says the same. That contradictseveryCloseDestroysTheWindowand the body's "Qt 6.12'sQDialog::done()closes, it does not only hide" — and if it were true,WA_DeleteOnClosewould never fire and the window would never be destroyed. Pre-existing text, not introduced here, but you're editing the line. -
src/gui/NetSchedulerDialog.cpp:679resolves the token inside the row loop, once per disabled row. Hoist it above thefor (int row …);populateTable()runs on everycommit()and on every theme change. -
The import merge box has no test.
setDefaultButton(skipBtn)at:845is the one remaining place where Return's target could silently regress to a destructive action, and the body lists it under "Not run."removeDefaultsToCancelalready has the harness — an import case is ~15 lines, andonNextModalwould drive it the same way. Worth it precisely because the per-PR CI gate never runs this target. -
docs/user/screens.json:2427alt text isn't updated to name the canon header and corner close button, which sibling #6751 did in-PR formqtt-settings-dialog. The body lists both shots as "stale, to recapture", so this is a consistency point only.
What I tried to break
Findings below are reasoned from the head checkout at /tmp/aetherclaude/pr-6762; I'm headless, so nothing here was reproduced at runtime, and I say so where it matters.
- Link breakage from the
tests.cmakesource swap. Grepped all 21 sources ofgui_nested_lifetime_testforPersistentDialogandFramelessWindowTitleBarsymbols after both.cpps were dropped — three hits, all inside//comments. Also checked the new target againstaether_assert_tests_retrofitted(tests/tests.cmake:10379): it linksaethercore+Qt6::Widgetswithout namingaetherdesktop_supportoraether_test_wisdom_isolation, but neither doesgui_nested_lifetime_testand configure is green on all three platforms, soaether_retrofit_tests()supplies them.CMAKE_AUTOMOCis ON globally (CMakeLists.txt:14), so the#include "…moc"resolves. - Lifetime, the recurring minefield here. Walked
onRemove,onImportandonExportpost-exec():if (!self || !boxOwner || box.clickedButton() != removeBtn)short-circuits on the owner before touching the danglingboxreference, the ID relookup survives a list mutated during the loop, andonImport's finalelse returnstill catches both Cancel and the closed-another-way null, preservingmain's QMessageBox behaviour.delete dlg.data()on a nullQPointerineveryCloseDestroysTheWindowis a no-op. Found nothing. - The reopen path.
MainWindow_Nets.cpp:63-84— withWA_DeleteOnClosetheQPointernulls on close, sowasFreshis true again and the connections are remade exactly once per instance; entries come fromm_netScheduler->entries(), whichpersistNetSchedule()keeps current, so no stale snapshot can be committed back. The->show()at:75is reached only after a freshnewor a non-nullQPointer. - The themeChanged refill.
populateTable()+setCurrentCell(row, 1)inside the lambda:setRowCount()with an unchanged count keeps rows,setItemreplaces items, selection is restored anditemSelectionChangedre-runsupdateButtons(). No reentrancy intoonEdit, noentriesChangedemission (that's only incommit()), so a theme switch can't write the schedule file. - Whether the tests bite.
removeDefaultsToCancelfails onmaintwice over (qobject_cast<CanonMessageBox*>is null, and the default reads Yes);disabledRowFollowsTheThemecan't pass against aQPalette-sourced brush;editorSpinTextIsInsetgoes red the momentinsetSpinText()is removed, since the editor then sits ~1 px in. None of them assert the implementation back to itself. The one I don't trust isbuttonsFitAtMinimumSize— Blockers 2. - Verified rather than taken: the 184 ThemeManager includers, the 4.22 / 5.39 / 6.47 contrast ratios recomputed from the theme JSONs (the nested-token indirection, so a flat grep would have missed them), the seven-popup count, and the audit-doc line number at four separate commits.
- CodeGuard's 124 findings went through the security-audit subagent: no findings, all lexical false positives.
NetSchedulerDialog.cpp:343(CS-VLN-CMDI-AMP, CRITICAL) isfor (const QByteArray& id : ids)— a C++ reference declarator overQTimeZone::availableTimeZoneIds(); the file has noQProcess,system(),openUrlor network call at all. ThePanadapterStream.hbackticks are Markdown in//comments, the SSRF-HEX hits are Flex wire constants and ESP32 MMIO register addresses, the test "credentials" are zero-filled buffers handed tocrypto_ed25519_key_pair(), and thectr2_proxy_model_testaddresses are RFC 5737 TEST-NET-1. Credit: security-audit (CodeGuard). One genuine item it surfaced — the R2 credential in curl's argv attools/publish_update_manifest.py:88— is outside this diff and already fixed on main by #6746 (fe9e6b8ff); the branch is just behind. No action for this PR. - Couldn't check: anything at runtime. Specifically unverified by me — whether Qt 6.12's
QDialog::done()really delivers a close event (the body's claim, whicheveryCloseDestroysTheWindowexists to pin, and whichCanonWindow.cpp:129contradicts); the actual rendered contrast; and whether 760 px really fits the seven buttons on CI's fonts, which is the substance of Blockers 2. All eight checks are green on232e36e, but the per-PRctest -Rgate is frozen and does not include this target, so green here means it compiled.
Recommendation
Needs maintainer decision. The engineering is genuinely good — it answers every item in #6761 and all three of the triage's extra asks, the nested-loop lifetime handling survived a hostile read, and the contrast table and mutation list are the kind of evidence Principle VIII is asking for. But #6761 is still open and unruled, and this is a full CanonWindow move where #6239's tool-dialog line authorizes only a token re-skin; the PR's own edit to dialog-patterns.md:273-277 asserts the move as already made. That ruling covers eight PRs at once, not just this one.
Concrete next step: relabel the body as an unlisted pick awaiting #6761, the way #6753 does, and replace the per-button fit assertion in Blockers 2 with a layout-relative one so a CI font swap can't red full-suite post-merge. The nits are cheap and can ride the same push. Thanks for the contrast figures in both themes and for naming the 4.22:1 pair yourself instead of letting it through quietly — that's what made this reviewable.
Review event is COMMENT: every merge here needs a human, and severity is in the labels above, not in a state change.
🤖 aethersdr-agent · cost: $19.7089 · model: claude-opus-5
|
Cisco CodeGuard — static analysis of this PR (124 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: $20.6802 · model: claude-opus-5 |
…rRX and AetherTX, and its questions are CanonMessageBoxes. Principle VIII. Net Scheduler moves from a PersistentDialog to a CanonWindow::Kind::Workspace in its own canon/netScheduler scope, keeping NetSchedulerDialogGeometry so the saved size and position carry over. The Add / Edit Net editor is a canon dialog with Cancel / OK, and all seven QMessageBoxes are CanonMessageBoxes. Remove Net defaults to Cancel; Import keeps Skip existing as its default. The canon controls sheet covers the window, with window-local rules for the fields and popups it does not style. Spin, date and time fields get a text margin, since a stylesheet-framed spin box drops its left padding on Qt 6.12. The minimum grows to 760x420 so the canon buttons and table text fit. New net_scheduler_dialog_test covers the workspace shape and scope, the geometry carry-over, the button fit, the Remove default, the theme refresh, the editor default and the spin text inset; each fails with its part reverted. Closes aethersdr#6761. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ns it and the close paths. Principle VIII. The canon controls sheet clears every descendant to transparent, and Qt parents a field's standard right-click menu (Undo/Cut/Copy/Paste) to the field, so the menus on the editor's name, frequency, spin, date and time fields drew with no panel: the mutant's menu grabs at alpha 0. The window sheet, which the editor and the message boxes share, gives QMenu its canon panel back, as on TX Band Settings. net_scheduler_dialog_test gains editorFieldMenuIsOpaque, which fails without the QMenu rules, and everyCloseDestroysTheWindow: Esc, Ctrl+W / Cmd+W and the corner button each destroy the WA_DeleteOnClose window, so the next open starts from the schedule MainWindow passes in. Co-Authored-By: Claude Opus 5.5 <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>
232e36e to
927d085
Compare
|
Thanks for the review. Pushed 1. Unlisted pick (disclosure). Done. The body now opens with the same banner as #6753: this full 2.
3. Disabled row at 4.22:1 (light). Still the maintainer's call, unchanged. Options as in the body: keep 4. Visible consequences. Still listed for the maintainer: 760×420 / 560 px minimums, the monospace Name column, and the import box's button order (default unchanged at Skip existing). Nits:
Also fixed: the Test runs on the rebased branch: One thing I saw that this PR doesn't touch: run natively on macOS, |
Resolves the conflicts in the three docs/style/* canon-window lists, where main added FFTW Wisdom / canon/nr2Wisdom. Every list keeps main's entries and this branch's. Regenerate docs/architecture/aetherd-touchpoints.md: main and this branch each added a core/ThemeManager.h include, so the merged count is 186. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Summary
Closes #6761. Part of #6239 and approved RFC #6226. Waiting on the maintainer's pick: this full
CanonWindowmove is not on #6239'sCanonWindowlists. #6239's "Persistent tool dialogs (one PR per dialog)" candidate covers only moving a dialog's colours tocolor.canon.*. #6761 lays out both options, and its triage asks for one ruling across the same-shape PRs; please don't merge before the pick. Merging is that pick:aethersdr-style-guide.mdanddialog-patterns.mdlist Net Scheduler among the tool windows the maintainer has moved toCanonWindow, which is true only once this lands.Net Scheduler (
NetSchedulerDialog, Tools → Net Scheduler...) moves from aPersistentDialogto aCanonWindow::Kind::Workspace, built the way AetherRX and AetherTX are. Its Add / Edit editor becomes a canon dialog, and its sevenQMessageBoxes becomeCanonMessageBoxes.FramelessResizer.canon/netScheduler. Its editor and message boxes are its children, so they resolve through it too.NetSchedulerDialogGeometry).CanonWindowreads the old base64 rectangles, so the saved size and position carry over. A rectangle narrower or shorter than the window's minimum opens at the minimum: the base64 branch is a plainrestoreGeometry(), and Qt clamps the restored size tominimumSize()(a 640×380 legacy rectangle opens at 760×420, measured under bothcocoaandoffscreen).ModemChrome::styleSheet(Scale::Dialog, Look::Canon)), applied throughapplyCanonSheet().QDoubleSpinBox/QDateTimeEdit(matching itsQSpinBoxrule), the combo list and the date's calendar popup, the intro line (canon.muted), the next-firing line (canon.cyan) and the editor's hairline divider (canon.line).QMenuits canon panel back, as on TX Band Settings (8caa5a42)."SF Mono", "Menlo", "Consolas", monospaceonQTableWidgetitself, not on a numeric column, so the Name column ("Tuesday County ARES Net") is monospace too. That is most of the table's width growth.palette(...)inline style sheets go. A disabled net's row is drawn incanon.muted, and the table refills onThemeManager::themeChangedso that ink follows a theme change.CanonWindowdialog (netEditorDialog) with the canon header, in place of aQDialogandQDialogButtonBox.CanonMessageBoxdraws its default.QFormLayouts become one, with the divider as a spanning row, so both halves share a label column. "Frequency" was clipped in a two-form render.QAbstractSpinBoxputs its inner editor on the frame's inner edge. That happens under both Fusion and the macOS style, and a standalone Qt probe reproduces it with no AetherSDR code. So the sheet's 12 px left padding never reaches the text, and the numbers sat flush against the border.insetSpinText()gives each editor 13 px of text margin (padding + border). It's the same fix as the MQTT Settings port field in feat(gui): MQTT Settings is a CanonWindow dialog, re-skinned to the canon palette. Principle VIII. #6751, written separately in each PR. Whichever of feat(gui): MQTT Settings is a CanonWindow dialog, re-skinned to the canon palette. Principle VIII. #6751 and feat(gui): Net Scheduler is a CanonWindow workspace, built like AetherRX and AetherTX, and its questions are CanonMessageBoxes. Principle VIII. #6762 lands second should move both onto one shared helper.QMessageBoxes:netRemoveConfirm, so the bridge can tell it from the window's own Remove. Cancel is the default.showNotice()helper: capture with no slice, import open failure, import parse failure, empty import, and export write failure.MainWindow_Nets.cpp):showNetSchedulerDialog()creates the window directly withWA_DeleteOnClose, like SpotHub.showOrRaisePersistent()would apply View → Frameless Window, which a canon window ignores. TheentriesChanged/tuneRequestedwiring is unchanged. ThewasFreshguard stays: triggering the menu while the window is open raises the existing window instead of building a second one.QPointerguards, ID relookup after each loop);NetScheduleStoreand the schedule file.docs/style/dialog-patterns.md,aethersdr-style-guide.mdandtheme-style-guide.mdlist the window and its scope.docs/user/docs/net-scheduler.mdsays how the window moves, resizes and closes, that Remove defaults to Cancel, and now lists Tune Now, which it had left out.docs/architecture/aetherd-touchpoints.mdis regenerated for the newcore/ThemeManager.hinclude (ui-support, 184 → 185).dialog-patterns.md's geometry row and theCanonWindow::hideEvent()comment say that Escape and ⌘W / Ctrl+W close the window throughQDialog::reject()(soWA_DeleteOnCloseapplies) without a close event, instead of "hide it".docs/user/screens.json: the two Net Scheduler recipes target the editor by its objectName,netEditorDialog(it is no longer a plainQDialog, so theQDialog/...targets matched nothing), and both alt texts name the canon header and the corner close button.Intent
After this merges, Net Scheduler, its editor and its questions look and behave like the other canon windows, with the same controls doing the same things. Out of scope: the reminder banner,
NetScheduler, the schedule format, and the other persistent tool dialogs (one PR each, per #6239).Visible changes for the maintainer:
Remove Net defaults to Cancel, so Return keeps the net on every platform (the CTR2 Restore precedent, feat(ctr2): the CTR2 window's questions in the canon style, and a restart that beats its answer is not an error. Principle VIII. #6736).
QMessageBoxwith Yes | No on the Qt 6.12 build found No the default under the native macOS (cocoa) platform and Yes underoffscreen. Windows and Linux were not checked.Size: the window minimum grows from 640×380 to 760×420, and the editor minimum from 440 to 560 px.
Contrast, resolved from the bundled themes (a user theme without a
canonscope falls back to the root colours). Every pair clears 4.5:1 except one:mutedongroundcyanongroundinkSoftoncontrolinkSoftonraisedmutedoncontrolmutedonraisedmutedonraisedinkoncontrolcyanoncontrolinkSoftonnestedinkSoftonraisedonAccentoncyanA disabled net's row on a non-alternate row in the light theme is 4.22:1, under the 4.5:1
docs/a11y.mdasks for. The muted ink is how a disabled net reads as off (with no ✓ and "—" for Next), and WCAG's exemption for inactive controls arguably doesn't cover a data row. Maintainer call: keepcanon.muted, or takecanon.inkSoftand let the ✓ and "—" carry the state alone.Stale screenshots, to recapture:
net-scheduler-window.pngandnet-scheduler-add-net.pngindocs/user/docs/net-scheduler.md. Their recipes and alt texts are updated; the images are not re-shot.Overlap: #6753 and the other canon PRs add windows to the same lists in the three style docs, so whichever lands later needs a small rebase there. This branch is rebased on
mainatbe216ed3(#6735, #6745 and #6768 are in; every list entry is kept).Constitution principle honored
Principle VIII — Evidence Over Assertion: the workspace shape, the canon scope, the geometry carry-over, the button fit, the Remove default, the theme refresh and the editor default each have a check that fails when that part of the change is reverted (see the test plan).
Test plan
cmake --build build --target AetherSDR net_scheduler_dialog_test gui_nested_lifetime_test, macOS arm64, Qt 6.12.0, RelWithDebInfo), with no warnings in the touched files.AETHER_SETTINGS_DIR),AETHER_AUTOMATION_NO_TX=1, and no radio connected (get radio→"connected": false):NetSchedule.json;canon.muted, with no ✓ and "—" for Next;netRemoveConfirm) deleted one (2 → 1).net_scheduler_dialog_test(socket-free; it never touches a radio): 12 cases (15 rows), all passing 12 / 12 runs natively (cocoa) and 12 / 12 underQT_QPA_PLATFORM=offscreen:CanonWindow, a framelessQt::Window, incanon/netScheduler;NetSchedulerDialogGeometryrestores at its size;entriesChangeddoes not fire;netRemoveConfirmremoves the selected net and firesentriesChangedonce;AA_DontUseNativeDialogs);color.canon.muted;CanonWindowtitled "Add Net" with OK the default;WA_DeleteOnClosewindow (Qt 6.12'sQDialog::done()closes the window, though it delivers nocloseEvent(); a standalone Qt 6.12 probe:reject()gives one hide, zero close events, and the window is destroyed). The Ctrl+W row activates the window first, since the shortcut is window-scoped, and skips if a desktop session refuses the activation.theme::setContainer();Kind::Dialog;setGeometryKey();themeChangedconnection;insetSpinText()(the case lists all six flush fields);QMenuselectors so they match nothing (the menu grabs at alpha 0).exec(). Its helper closes the box even when a check there fails, and gives up after 2 s if no box opens, so a failure ends the case rather than hanging it. WithnetRemoveConfirmrenamed, the case fails in about 1 s. The ctest block also setsTIMEOUT 60.gui_nested_lifetime_test: 10 / 10 offscreen (as ctest registers it), includingnetSchedulerEditorReturnsAfterOwnerDeletion(add-net and doubleclick-edit-net). That case closes the delete-on-close window while the canon editor'sexec()is running. Its sources now linkCanonWindow,CanonMessageBoxandModemChromein place ofPersistentDialogandFramelessWindowTitleBar.ci.yml's frozen per-PRctest -Rlist (docs/agents/tests-ci.md); it runs post-merge infull-suite.yml. The local runs above are the evidence.Static checkssteps run locally on the rebased branch pass: engine boundary, touchpoint manifest (regenerated), generated user-docs reference, test registration, frozen per-PR test gate, capability records, command plane, QSettings, theme seed, build options. User docs:npm ci && npm run buildsucceeds, andtest_log_rules.mjspasses.tools/audit_colours.py --compare-src <main> --strict): unique colours 470 (+0), references 1811 (+0),setStyleSheet563 (−2).tools/check_a11y.pyon the touched sources: no findings.Screenshots: the window and the editor from this branch follow in a comment below.
Checklist
docs/COMMIT-SIGNING.md)AppSettingscalls — use nested-JSON-under-one-key(Principle V)
NetSchedulerDialogGeometry.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/net-scheduler.md; the two user-guide screenshots are listed above to recapture.🤖 Generated with Claude Code