| 697 | // ── Float / Dock ────────────────────────────────────────────────────────── |
| 698 | |
| 699 | void PanadapterStack::floatPanadapter(const QString& panId) |
| 700 | { |
| 701 | PanadapterApplet* applet = m_pans.value(panId, nullptr); |
| 702 | if (!applet || m_floatingWindows.contains(panId)) return; |
| 703 | |
| 704 | // Hide the SpectrumWidget and release GPU resources *before* reparenting. |
| 705 | // On macOS, QRhiWidget with WA_NativeWindow has a native NSView; orphaning |
| 706 | // the widget to nullptr creates a transient top-level NSWindow whose |
| 707 | // destruction can corrupt the main window's NSResponder chain (#1344). |
| 708 | SpectrumWidget* sw = applet->spectrumWidget(); |
| 709 | sw->hide(); |
| 710 | sw->prepareForTopLevelChange(); |
| 711 | sw->resetGpuResources(); |
| 712 | |
| 713 | // Reparent directly from splitter into the floating window — never through |
| 714 | // nullptr. Using adoptApplet() calls addWidget() internally which sets |
| 715 | // the parent to the floating window in one step, avoiding the transient |
| 716 | // top-level NSWindow that corrupts the NSResponder chain (#1668). |
| 717 | // Parenting the floating window to MainWindow keeps it on top of the |
| 718 | // main window without becoming WindowStaysOnTopHint (which would |
| 719 | // float above other apps too). Qt::Window inside PanFloatingWindow |
| 720 | // keeps it as a top-level window — the parent only affects z-order |
| 721 | // and lifetime. |
| 722 | auto* fw = new PanFloatingWindow(window()); |
| 723 | fw->adoptApplet(applet); |
| 724 | applet->spectrumWidget()->setFloating(true); |
| 725 | |
| 726 | m_floatingWindows[panId] = fw; |
| 727 | rebuildDockedSplitter(); |
| 728 | fw->restoreWindowGeometry(); |
| 729 | fw->show(); |
| 730 | fw->raise(); |
| 731 | saveFloatingState(); |
| 732 | emit panFloated(panId); |
| 733 | |
| 734 | connect(fw, &PanFloatingWindow::dockRequested, |
| 735 | this, &PanadapterStack::dockPanadapter); |
| 736 | |
| 737 | // Defer sw->show() until after refreshAfterReparent() so Metal binds to |
| 738 | // the new NSView before the first render. Showing before the reset can |
| 739 | // trigger render() on a stale/transitional Metal surface. |
| 740 | QTimer::singleShot(0, this, [this, sw]() { |
| 741 | refreshAfterReparent(sw); |
| 742 | sw->show(); |
| 743 | equalizeSizes(); |
| 744 | }); |
| 745 | } |
| 746 | |
| 747 | void PanadapterStack::dockPanadapter(const QString& panId) |
| 748 | { |
no test coverage detected