Skip to content

Fix macOS panadapter popout refresh#2280

Merged
ten9876 merged 1 commit intoten9876:mainfrom
rfoust:codex/fix-macos-panadapter-popout
May 2, 2026
Merged

Fix macOS panadapter popout refresh#2280
ten9876 merged 1 commit intoten9876:mainfrom
rfoust:codex/fix-macos-panadapter-popout

Conversation

@rfoust
Copy link
Copy Markdown
Contributor

@rfoust rfoust commented May 2, 2026

Summary:

  • Fix macOS panadapter popouts showing a static/stale spectrum image after detaching from the main window.
  • Recreate the native QRhi/Metal surface after cross-window reparenting on macOS, then request fresh pan dimensions from the radio after float/dock.
  • Prevent stale floating state from being restored again after later user-added pans, and rebuild the docked splitter so floating/docking multiple pans does not leave blank placeholder space in the main window.

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:

  • Configured and built on macOS with cmake -B /private/tmp/aethersdr-panadapter-popout-build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo.
  • Built with cmake --build /private/tmp/aethersdr-panadapter-popout-build -j8.
  • Manual app testing on macOS confirmed the detached panadapter updates live after the native surface reset change.
  • Manual follow-up exposed and fixed the blank main-window placeholder after detaching multiple panadapters.

Copilot AI review requested due to automatic review settings May 2, 2026 11:30
@rfoust rfoust requested review from jensenpat and ten9876 as code owners May 2, 2026 11:30
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 SpectrumWidget plus 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.

Comment thread src/gui/PanadapterStack.cpp Outdated
Comment on lines +21 to +32
#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(); });
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread src/gui/PanadapterStack.cpp Outdated
Comment on lines +362 to +379
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();
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread src/gui/PanadapterStack.cpp Outdated
Comment on lines +370 to +373
m_splitter = new QSplitter(Qt::Vertical, this);
m_splitter->setHandleWidth(3);
m_splitter->setChildrenCollapsible(false);
layout()->addWidget(m_splitter);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@aethersdr-agent
Copy link
Copy Markdown
Contributor

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 (build, analyze (cpp)) were still in_progress at the time of this comment, so the overall PR status will update on its own once those finish. If they go green, this can be merged regardless of the cleanup job — a maintainer can re-run that one with one click, or just dismiss it.

Unrelated to CI: Copilot left a couple of suggestions on src/gui/PanadapterStack.cpp worth a look when you have a moment (guarding WA_NativeWindow/QWindow::destroy() behind AETHER_GPU_SPECTRUM since the stale-surface issue is Metal-specific, and avoiding the transient setParent(nullptr) in rebuildDockedSplitter() so docked applets don't briefly become top-level windows on macOS). Those are quality-of-implementation notes, not blockers — happy to discuss if helpful.

Thanks again for working through this. 🙏

@rfoust rfoust force-pushed the codex/fix-macos-panadapter-popout branch from 9d1bee4 to 7cc1854 Compare May 2, 2026 12:38
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 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 first addWidget (rebuildDockedSplitter already collects the docked applet via m_pans once it's been removed from m_floatingWindows).
  • The manual setSizes(...) block runs synchronously, but rebuildDockedSplitter() queues equalizeSizes() via QTimer::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 both Q_OS_MAC and AETHER_GPU_SPECTRUM, so software-spectrum builds keep the lighter resetGpuResources() path.
  • The panFloated/panDocked signal + 200 ms display 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() after saveFloatingState() matches the convention used elsewhere for state that needs to survive a hard exit.
  • Layout-aware rebuildDockedSplitter() correctly handles the 2x2/2h1/12h/etc. nested-splitter cases, and reparents directly via addWidget() without a transient top-level state.

Nothing here is blocking from my read.

@ten9876 ten9876 merged commit 48d8ba9 into ten9876:main May 2, 2026
5 checks passed
ten9876 added a commit that referenced this pull request May 2, 2026
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]>
ten9876 added a commit that referenced this pull request May 2, 2026
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]>
@aethersdr-agent aethersdr-agent Bot mentioned this pull request May 4, 2026
2 tasks
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.

3 participants