Fix macOS panadapter popout refresh#2280
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes macOS panadapter popouts rendering stale/static spectrum after cross-window reparenting, and addresses layout/state issues when floating/docking pans.
Changes:
- Add macOS-specific reparent refresh logic for
SpectrumWidgetplus a float/dock signal hook for dimension re-push. - Rebuild the docked pan splitter after float/dock to avoid blank placeholder space.
- Prevent late-arriving pans from re-triggering the initial “restore floating state” debounce.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/gui/PanadapterStack.h |
Adds float/dock signals and a helper to rebuild the docked splitter. |
src/gui/PanadapterStack.cpp |
Implements macOS reparent refresh behavior; rebuilds the docked splitter on float/dock; forces settings save for floating IDs. |
src/gui/MainWindow.cpp |
Tracks whether the initial layout restore timer has fired; on macOS re-pushes pan dimensions when a pan is floated/docked. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| #ifdef Q_OS_MAC | ||
| const bool wasVisible = sw->isVisible(); | ||
| sw->hide(); | ||
| sw->resetGpuResources(); | ||
| if (QWindow* windowHandle = sw->windowHandle()) { | ||
| windowHandle->destroy(); | ||
| } | ||
| sw->setAttribute(Qt::WA_NativeWindow); | ||
| if (wasVisible) { | ||
| sw->show(); | ||
| } | ||
| QTimer::singleShot(50, sw, [sw]() { sw->update(); }); |
There was a problem hiding this comment.
Addressed in 7cc1854: the native QWindow::destroy()/WA_NativeWindow refresh is now guarded behind Q_OS_MAC && AETHER_GPU_SPECTRUM, so software-spectrum builds keep the existing resetGpuResources()/update() behavior and we only destroy the native handle for the Metal/QRhi path that exhibited the stale-surface bug.
| for (PanadapterApplet* applet : docked) { | ||
| applet->setParent(nullptr); | ||
| } | ||
|
|
||
| layout()->removeWidget(m_splitter); | ||
| m_splitter->hide(); | ||
| m_splitter->deleteLater(); | ||
|
|
||
| m_splitter = new QSplitter(Qt::Vertical, this); | ||
| m_splitter->setHandleWidth(3); | ||
| m_splitter->setChildrenCollapsible(false); | ||
| layout()->addWidget(m_splitter); | ||
|
|
||
| for (PanadapterApplet* applet : docked) { | ||
| m_splitter->addWidget(applet); | ||
| applet->show(); | ||
| } | ||
|
|
There was a problem hiding this comment.
Addressed in 7cc1854: rebuildDockedSplitter() no longer calls setParent(nullptr). It creates the replacement splitter first, moves docked applets directly into that splitter via addWidget(), then removes/deletes the old splitter, avoiding the transient top-level widget state on macOS.
| m_splitter = new QSplitter(Qt::Vertical, this); | ||
| m_splitter->setHandleWidth(3); | ||
| m_splitter->setChildrenCollapsible(false); | ||
| layout()->addWidget(m_splitter); |
There was a problem hiding this comment.
Addressed in 7cc1854: the docked splitter rebuild now uses the saved PanadapterLayout when the remaining docked pan count matches that layout. If it no longer matches because panes were floated, it falls back to the same count-based defaults used during initial layout restore, instead of always forcing a vertical splitter.
|
Hi @rfoust — thanks for the contribution! 🙌 Quick note on the red check: the only failing job is Cleanup artifacts → Delete artifacts (job log). That's part of our post-build housekeeping workflow that deletes per-PR artifacts via the GitHub API — it's CI infrastructure, not anything in your code. It typically fails when the GH API throws a transient 5xx, when an artifact has already been GC'd, or when token scope is briefly off. Nothing for you to fix on your branch. The actual code-validating jobs on this commit ( Unrelated to CI: Copilot left a couple of suggestions on Thanks again for working through this. 🙏 |
9d1bee4 to
7cc1854
Compare
There was a problem hiding this comment.
Thanks for tracking down a particularly thorny native-window/QRhi interaction, @rfoust — and for already absorbing the Copilot feedback (AETHER_GPU_SPECTRUM guard, no setParent(nullptr) transient, layout-aware rebuild). This looks good overall; just a couple of small cleanup observations, none blocking.
Redundant size handling in dockPanadapter() (PanadapterStack.cpp:564-583)
After this change, the existing splitter-sizing block is now dead/dueling code:
m_splitter->addWidget(applet); // adds to OLD splitter
applet->show();
rebuildDockedSplitter(); // reparents applet again into NEW splitter, schedules equalizeSizes()
const int count = m_splitter->count(); // operates on NEW splitter (top-level slots, not pan count)
if (count > 1) {
int total = ...;
int each = total / count;
...
m_splitter->setSizes(sizes); // immediately overwritten by queued equalizeSizes()
}
m_splitter->updateGeometry();Two small things worth tidying:
m_splitter->addWidget(applet)happens against the soon-to-be-deleted old splitter;rebuildDockedSplitter()then reparents it again. You can drop the firstaddWidget(rebuildDockedSplitter already collects the docked applet viam_pansonce it's been removed fromm_floatingWindows).- The manual
setSizes(...)block runs synchronously, butrebuildDockedSplitter()queuesequalizeSizes()viaQTimer::singleShot(0, ...)which immediately overrides it. Safe to delete.
Saved PanadapterLayout may not match what's on screen after float
rebuildDockedSplitter() falls back to a count-based default (2v/2h1/2x2/vertical) when the saved layout doesn't match docked.size(), which is the right call. But AppSettings::PanadapterLayout is left at the previous value — so if the user had 4v saved, floats one, then re-docks, you re-enter rebuildDockedSplitter() with 4 docked applets and 4v matches, but 4v has no branch in the if/else chain and falls through to addVertical() (which is the right behavior, just by accident). Worth either updating PanadapterLayout on float/dock or being explicit about 4v/3v mapping to addVertical(). Not urgent.
Minor: setProperty("fired", ...) (MainWindow.cpp:2079, 2134, 2899)
Using QObject dynamic property as a one-shot latch works, but a plain bool m_layoutRestoreTimerFired{false}; member next to the existing m_layoutRestoreTimer is a hair clearer and avoids the QVariant round-trip. Style-only, take it or leave it.
Things that look right
refreshAfterReparent()macOS path is appropriately scoped behind bothQ_OS_MACandAETHER_GPU_SPECTRUM, so software-spectrum builds keep the lighterresetGpuResources()path.- The
panFloated/panDockedsignal + 200 msdisplay pan set xpixels=...repush is a reasonable workaround for the radio not knowing the new surface dimensions until after the reparent settles. AppSettings::instance().save()aftersaveFloatingState()matches the convention used elsewhere for state that needs to survive a hard exit.- Layout-aware
rebuildDockedSplitter()correctly handles the2x2/2h1/12h/etc. nested-splitter cases, and reparents directly viaaddWidget()without a transient top-level state.
Nothing here is blocking from my read.
The v0.9.5.1 entry was originally cut for just the TCI TX hotfix (#2285), but six additional fixes had already landed in main between the v0.9.5 tag and v0.9.5.1: - #2275 NR2 wisdom generation off the audio thread - #2278 SmartLink disconnect teardown - #2279 Reset RX slice tabs on disconnect (#2254) - #2280 macOS panadapter pop-out refresh + multi-pan dock layout - #2282 SmartLink reconnect after WAN drop - #2284 Qt log handler serialization fixes macOS tune-time crash CHANGELOG.md gets a per-fix entry with root cause + behavior change. WhatsNewData.cpp regenerated via scripts/gen_whatsnew.py so the in-app What's New dialog reflects the full v0.9.5.1 contents. Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Match the v0.9.5 `(#NNNN, contributor)` heading style and call out @rfoust and @jensenpat in the summary for the bulk of this release. Authors per PR: - @rfoust: #2278, #2279, #2280, #2282 (SmartLink + macOS popout) - @jensenpat: #2275, #2284 (NR2 wisdom + Qt log serialization) WhatsNewData.cpp regenerated. Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Summary:
Bug fixed:
On macOS, popping out a panadapter could create a separate window with only a still/static panadapter image. Closing/docking that popout and later adding another panadapter could also recreate an unwanted blank floating window because saved floating state was restored after the initial layout phase. With multiple panadapters, detaching panes could leave an empty splitter slot in the main window.
Testing: