From b905fa385ea2cee6dd7fc3b5a7baba61845d0739 Mon Sep 17 00:00:00 2001 From: Bonni Date: Wed, 21 Oct 2026 15:46:20 -0300 Subject: [PATCH 1/2] Dialogs: above other programs only while this program is in front (#492) --- src/mp_ui/Mobile.cpp | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/mp_ui/Mobile.cpp b/src/mp_ui/Mobile.cpp index 5ef96f80..1d8f2a96 100644 --- a/src/mp_ui/Mobile.cpp +++ b/src/mp_ui/Mobile.cpp @@ -519,6 +519,31 @@ class SizeKeeper : public juce::ComponentListener { private: juce::String kind_; }; + +// JUCE makes a new dialog always on top whenever another of the program's +// windows is, and the Recorder and Combinations windows are while the +// program is in front: such a dialog then stayed above every other +// program's windows (#492). It follows the program in and out of the front, +// as those windows do. Owned by nobody but itself: it goes with the dialog. +class FrontFollower : public juce::ComponentListener, private juce::Timer { +public: + explicit FrontFollower(juce::Component& dialog) : dialog_(&dialog) { + dialog.addComponentListener(this); + startTimer(250); + } + void componentBeingDeleted(juce::Component& c) override { + stopTimer(); + c.removeComponentListener(this); + delete this; + } + +private: + void timerCallback() override { + const bool front = juce::Process::isForegroundProcess(); + if (dialog_ != nullptr && dialog_->isAlwaysOnTop() != front) dialog_->setAlwaysOnTop(front); + } + juce::Component::SafePointer dialog_; +}; } // namespace juce::DialogWindow* launchDialog(juce::DialogWindow::LaunchOptions& options) { @@ -548,6 +573,7 @@ juce::DialogWindow* launchDialog(juce::DialogWindow::LaunchOptions& options) { } dialog->addComponentListener(new SizeKeeper(kind)); } + if (kLiveOnTop && !kMobile && dialog->isAlwaysOnTop()) new FrontFollower(*dialog); return dialog; } From 79f07c60fb495d35f828ca413728afe81d80f68d Mon Sep 17 00:00:00 2001 From: Bonni Date: Wed, 21 Oct 2026 19:03:57 -0300 Subject: [PATCH 2/2] macOS: the file dialogs hide while another program is in front (#492) An open or save panel hides when the program leaves the front and comes back with it, as the Recorder and Combinations windows do. The application installs this once at startup; a plug-in leaves the host's panels alone. --- apps/MasterpieceApp/Main.cpp | 1 + src/mp_audio/CMakeLists.txt | 3 +++ src/mp_ui/MacWindows.mm | 33 +++++++++++++++++++++++++++++++++ src/mp_ui/Mobile.h | 9 +++++++++ 4 files changed, 46 insertions(+) create mode 100644 src/mp_ui/MacWindows.mm diff --git a/apps/MasterpieceApp/Main.cpp b/apps/MasterpieceApp/Main.cpp index 802178e2..7d216721 100644 --- a/apps/MasterpieceApp/Main.cpp +++ b/apps/MasterpieceApp/Main.cpp @@ -406,6 +406,7 @@ class MasterpieceApp : public juce::JUCEApplication { // language comes first. proc_->applyChosenLanguage(); mp::ui::installTouchLook(); + mp::ui::keepFileDialogsWithProgram(); mp::ui::restoreLinkedDocuments(); win_ = std::make_unique(*proc_, *devices_); win_->setVisible(true); diff --git a/src/mp_audio/CMakeLists.txt b/src/mp_audio/CMakeLists.txt index 71bb2d8b..11269b71 100644 --- a/src/mp_audio/CMakeLists.txt +++ b/src/mp_audio/CMakeLists.txt @@ -78,6 +78,9 @@ endif() # iPhone and iPad: picked documents read in place (#197). if(CMAKE_SYSTEM_NAME STREQUAL "iOS") target_sources(mp_audio PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../mp_ui/IosAccess.mm) +elseif(APPLE) + # macOS: the file dialogs stay with the program (#492). + target_sources(mp_audio PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../mp_ui/MacWindows.mm) endif() target_include_directories(mp_audio PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/..) target_compile_features(mp_audio PUBLIC cxx_std_20) diff --git a/src/mp_ui/MacWindows.mm b/src/mp_ui/MacWindows.mm new file mode 100644 index 00000000..cdf36abf --- /dev/null +++ b/src/mp_ui/MacWindows.mm @@ -0,0 +1,33 @@ +// macOS: the file dialogs stay with the program (#492). +// +// JUCE raises an open or save panel to the modal-panel level, above every +// normal window, and the panel stayed above other programs' windows once the +// player went to one. Each panel hides when the program leaves the front and +// comes back with it, as the Recorder and Combinations windows do. One +// observer, for the application's lifetime: a panel is seen as it takes the +// keyboard. + +#include "Mobile.h" + +#if JUCE_MAC + #import + +namespace mp::ui { + +void keepFileDialogsWithProgram() { + static bool installed = false; + if (installed) return; + installed = true; + [[NSNotificationCenter defaultCenter] addObserverForName:NSWindowDidBecomeKeyNotification + object:nil + queue:nil + usingBlock:^(NSNotification* note) { + id window = note.object; + if ([window isKindOfClass:[NSSavePanel class]]) + [(NSSavePanel*)window setHidesOnDeactivate:YES]; + }]; +} + +} // namespace mp::ui + +#endif diff --git a/src/mp_ui/Mobile.h b/src/mp_ui/Mobile.h index aa558db8..7397d02f 100644 --- a/src/mp_ui/Mobile.h +++ b/src/mp_ui/Mobile.h @@ -69,6 +69,15 @@ constexpr bool kLiveOnTop = true; // screen on a phone or tablet. Every dialog opens through here. juce::DialogWindow* launchDialog(juce::DialogWindow::LaunchOptions& options); +// macOS: an open or save panel hides while another program is in front, and +// comes back with this one (#492; MacWindows.mm). The application calls it +// once at startup; a plug-in leaves the host's panels alone. Nothing elsewhere. +#if JUCE_MAC +void keepFileDialogsWithProgram(); +#else +inline void keepFileDialogsWithProgram() {} +#endif + // Closes the front-most window other than `mainWindow`, as its own close // button would: Android's back button. False when there was none, so the // system's own back behaviour applies. A window whose properties set