From 79ed81c24a2e7dcb30e0724ffb8b996ad1c5f174 Mon Sep 17 00:00:00 2001 From: reuk Date: Wed, 15 Feb 2023 22:05:10 +0000 Subject: [PATCH] ScopedMessageBox: Add new helper type to bound alert window lifetimes The biggest new feature in this commit is the addition of NativeMessageBox::scopedAsync and AlertWindow::scopedAsync, both of which return an instance of ScopedMessageBox that will hide the message box in its destructor. The code for displaying modal dialogs on Windows has also been updated. Now, the dialog itself is run from a new thread with its own message loop. This means that when the dialog is dismissed, the background thread can be joined safely. In plugins, this means that there's no danger of the plugin view being destroyed from within the message box runloop, for example. --- modules/juce_gui_basics/juce_gui_basics.cpp | 8 + modules/juce_gui_basics/juce_gui_basics.h | 1 + .../native/juce_android_NativeMessageBox.cpp | 115 ++++++ .../native/juce_android_Windowing.cpp | 149 +------- .../native/juce_ios_NativeMessageBox.mm | 110 ++++++ .../native/juce_ios_Windowing.mm | 191 ---------- .../native/juce_linux_NativeMessageBox.cpp | 34 ++ .../native/juce_linux_Windowing.cpp | 126 ------- .../native/juce_mac_NativeMessageBox.mm | 134 +++++++ .../native/juce_mac_Windowing.mm | 213 ----------- .../native/juce_win32_NativeMessageBox.cpp | 346 ++++++++++++++++++ .../native/juce_win32_Windowing.cpp | 343 ----------------- .../windows/juce_AlertWindow.cpp | 255 ++++++------- .../windows/juce_AlertWindow.h | 33 ++ .../windows/juce_MessageBoxOptions.cpp | 93 +++++ .../windows/juce_MessageBoxOptions.h | 44 +++ .../windows/juce_NativeMessageBox.cpp | 127 +++++++ .../windows/juce_NativeMessageBox.h | 20 + .../windows/juce_ScopedMessageBox.cpp | 181 +++++++++ .../windows/juce_ScopedMessageBox.h | 69 ++++ 20 files changed, 1438 insertions(+), 1154 deletions(-) create mode 100644 modules/juce_gui_basics/native/juce_android_NativeMessageBox.cpp create mode 100644 modules/juce_gui_basics/native/juce_ios_NativeMessageBox.mm create mode 100644 modules/juce_gui_basics/native/juce_linux_NativeMessageBox.cpp create mode 100644 modules/juce_gui_basics/native/juce_mac_NativeMessageBox.mm create mode 100644 modules/juce_gui_basics/native/juce_win32_NativeMessageBox.cpp create mode 100644 modules/juce_gui_basics/windows/juce_MessageBoxOptions.cpp create mode 100644 modules/juce_gui_basics/windows/juce_NativeMessageBox.cpp create mode 100644 modules/juce_gui_basics/windows/juce_ScopedMessageBox.cpp create mode 100644 modules/juce_gui_basics/windows/juce_ScopedMessageBox.h diff --git a/modules/juce_gui_basics/juce_gui_basics.cpp b/modules/juce_gui_basics/juce_gui_basics.cpp index daa711279f..cf089966df 100644 --- a/modules/juce_gui_basics/juce_gui_basics.cpp +++ b/modules/juce_gui_basics/juce_gui_basics.cpp @@ -254,7 +254,10 @@ namespace juce #include "widgets/juce_Toolbar.cpp" #include "widgets/juce_ToolbarItemPalette.cpp" #include "widgets/juce_TreeView.cpp" +#include "windows/juce_MessageBoxOptions.cpp" +#include "windows/juce_ScopedMessageBox.cpp" #include "windows/juce_AlertWindow.cpp" +#include "windows/juce_NativeMessageBox.cpp" #include "windows/juce_CallOutBox.cpp" #include "windows/juce_ComponentPeer.cpp" #include "windows/juce_DialogWindow.cpp" @@ -293,6 +296,7 @@ namespace juce #include "native/juce_ios_UIViewComponentPeer.mm" #include "native/accessibility/juce_ios_Accessibility.mm" #include "native/juce_ios_Windowing.mm" + #include "native/juce_ios_NativeMessageBox.mm" #include "native/juce_ios_FileChooser.mm" #if JUCE_CONTENT_SHARING @@ -304,6 +308,7 @@ namespace juce #include "native/juce_mac_PerScreenDisplayLinks.h" #include "native/juce_mac_NSViewComponentPeer.mm" #include "native/juce_mac_Windowing.mm" + #include "native/juce_mac_NativeMessageBox.mm" #include "native/juce_mac_MainMenu.mm" #include "native/juce_mac_FileChooser.mm" #endif @@ -319,6 +324,7 @@ namespace juce #include "native/accessibility/juce_win32_AccessibilityElement.cpp" #include "native/accessibility/juce_win32_Accessibility.cpp" #include "native/juce_win32_Windowing.cpp" + #include "native/juce_win32_NativeMessageBox.cpp" #include "native/juce_win32_DragAndDrop.cpp" #include "native/juce_win32_FileChooser.cpp" @@ -330,6 +336,7 @@ namespace juce #include "native/x11/juce_linux_ScopedWindowAssociation.h" #include "native/juce_linux_Windowing.cpp" + #include "native/juce_linux_NativeMessageBox.cpp" #include "native/x11/juce_linux_XWindowSystem.cpp" JUCE_END_IGNORE_WARNINGS_GCC_LIKE @@ -362,6 +369,7 @@ static jobject makeAndroidPoint (Point p) #include "juce_core/files/juce_common_MimeTypes.h" #include "native/accessibility/juce_android_Accessibility.cpp" #include "native/juce_android_Windowing.cpp" + #include "native/juce_android_NativeMessageBox.cpp" #include "native/juce_android_FileChooser.cpp" #if JUCE_CONTENT_SHARING diff --git a/modules/juce_gui_basics/juce_gui_basics.h b/modules/juce_gui_basics/juce_gui_basics.h index 6d061e59e1..6f76a2c50b 100644 --- a/modules/juce_gui_basics/juce_gui_basics.h +++ b/modules/juce_gui_basics/juce_gui_basics.h @@ -269,6 +269,7 @@ namespace juce #include "widgets/juce_TreeView.h" #include "windows/juce_TopLevelWindow.h" #include "windows/juce_MessageBoxOptions.h" +#include "windows/juce_ScopedMessageBox.h" #include "windows/juce_AlertWindow.h" #include "windows/juce_CallOutBox.h" #include "windows/juce_ComponentPeer.h" diff --git a/modules/juce_gui_basics/native/juce_android_NativeMessageBox.cpp b/modules/juce_gui_basics/native/juce_android_NativeMessageBox.cpp new file mode 100644 index 0000000000..fc428aa344 --- /dev/null +++ b/modules/juce_gui_basics/native/juce_android_NativeMessageBox.cpp @@ -0,0 +1,115 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +std::unique_ptr ScopedMessageBoxInterface::create (const MessageBoxOptions& options) +{ + class AndroidMessageBox : public ScopedMessageBoxInterface + { + public: + explicit AndroidMessageBox (const MessageBoxOptions& o) : opts (o) {} + + void runAsync (std::function recipient) override + { + const auto makeDialogListener = [&recipient] (int result) + { + return new DialogListener ([recipient, result] { recipient (result); }); + }; + + auto* env = getEnv(); + + LocalRef builder (env->NewObject (AndroidAlertDialogBuilder, AndroidAlertDialogBuilder.construct, getMainActivity().get())); + + const auto setText = [&] (auto method, const String& text) + { + builder = LocalRef (env->CallObjectMethod (builder, method, javaString (text).get())); + }; + + setText (AndroidAlertDialogBuilder.setTitle, opts.getTitle()); + setText (AndroidAlertDialogBuilder.setMessage, opts.getMessage()); + builder = LocalRef (env->CallObjectMethod (builder, AndroidAlertDialogBuilder.setCancelable, true)); + + builder = LocalRef (env->CallObjectMethod (builder, AndroidAlertDialogBuilder.setOnCancelListener, + CreateJavaInterface (makeDialogListener (0), + "android/content/DialogInterface$OnCancelListener").get())); + + const auto addButton = [&] (auto method, int index) + { + builder = LocalRef (env->CallObjectMethod (builder, + method, + javaString (opts.getButtonText (index)).get(), + CreateJavaInterface (makeDialogListener (index), + "android/content/DialogInterface$OnClickListener").get())); + }; + + addButton (AndroidAlertDialogBuilder.setPositiveButton, 0); + + if (opts.getButtonText (1).isNotEmpty()) + addButton (AndroidAlertDialogBuilder.setNegativeButton, 1); + + if (opts.getButtonText (2).isNotEmpty()) + addButton (AndroidAlertDialogBuilder.setNeutralButton, 2); + + dialog = GlobalRef (LocalRef (env->CallObjectMethod (builder, AndroidAlertDialogBuilder.create))); + + LocalRef window (env->CallObjectMethod (dialog, AndroidDialog.getWindow)); + + if (Desktop::getInstance().getKioskModeComponent() != nullptr) + { + env->CallVoidMethod (window, AndroidWindow.setFlags, FLAG_NOT_FOCUSABLE, FLAG_NOT_FOCUSABLE); + LocalRef decorView (env->CallObjectMethod (window, AndroidWindow.getDecorView)); + env->CallVoidMethod (decorView, AndroidView.setSystemUiVisibility, fullScreenFlags); + } + + env->CallVoidMethod (dialog, AndroidDialog.show); + + if (Desktop::getInstance().getKioskModeComponent() != nullptr) + env->CallVoidMethod (window, AndroidWindow.clearFlags, FLAG_NOT_FOCUSABLE); + } + + int runSync() override + { + // Not implemented on this platform. + jassertfalse; + return 0; + } + + void close() override + { + if (dialog != nullptr) + getEnv()->CallVoidMethod (dialog, AndroidDialogInterface.dismiss); + } + + private: + const MessageBoxOptions opts; + GlobalRef dialog; + }; + + return std::make_unique (options); +} + +} // namespace juce diff --git a/modules/juce_gui_basics/native/juce_android_Windowing.cpp b/modules/juce_gui_basics/native/juce_android_Windowing.cpp index 9c866429bc..fbb2c74af6 100644 --- a/modules/juce_gui_basics/native/juce_android_Windowing.cpp +++ b/modules/juce_gui_basics/native/juce_android_Windowing.cpp @@ -2349,22 +2349,8 @@ DECLARE_JNI_CLASS (AndroidDialogOnClickListener, "android/content/DialogInterfac class DialogListener : public juce::AndroidInterfaceImplementer { public: - DialogListener (std::shared_ptr callbackToUse, int resultToUse) - : callback (std::move (callbackToUse)), result (resultToUse) - {} + explicit DialogListener (std::function cb) : callback (std::move (cb)) {} - void onResult (jobject dialog) - { - auto* env = getEnv(); - env->CallVoidMethod (dialog, AndroidDialogInterface.dismiss); - - if (callback != nullptr) - callback->modalStateFinished (result); - - callback = nullptr; - } - -private: jobject invoke (jobject proxy, jobject method, jobjectArray args) override { auto* env = getEnv(); @@ -2372,7 +2358,11 @@ private: if (methodName == "onCancel" || methodName == "onClick") { - onResult (env->GetObjectArrayElement (args, 0)); + auto* dialog = env->GetObjectArrayElement (args, 0); + env->CallVoidMethod (dialog, AndroidDialogInterface.dismiss); + + NullCheckedInvocation::invoke (callback); + return nullptr; } @@ -2380,133 +2370,10 @@ private: return AndroidInterfaceImplementer::invoke (proxy, method, args); } - std::shared_ptr callback; - int result; +private: + std::function callback; }; -//============================================================================== -static void createAndroidDialog (const MessageBoxOptions& opts, - ModalComponentManager::Callback* callbackIn, - AlertWindowMappings::MapFn mapFn) -{ - auto* env = getEnv(); - - LocalRef builder (env->NewObject (AndroidAlertDialogBuilder, AndroidAlertDialogBuilder.construct, getMainActivity().get())); - - builder = LocalRef (env->CallObjectMethod (builder.get(), AndroidAlertDialogBuilder.setTitle, javaString (opts.getTitle()).get())); - builder = LocalRef (env->CallObjectMethod (builder.get(), AndroidAlertDialogBuilder.setMessage, javaString (opts.getMessage()).get())); - builder = LocalRef (env->CallObjectMethod (builder.get(), AndroidAlertDialogBuilder.setCancelable, true)); - - std::shared_ptr sharedCallback (AlertWindowMappings::getWrappedCallback (callbackIn, mapFn)); - - builder = LocalRef (env->CallObjectMethod (builder.get(), AndroidAlertDialogBuilder.setOnCancelListener, - CreateJavaInterface (new DialogListener (sharedCallback, 0), - "android/content/DialogInterface$OnCancelListener").get())); - - builder = LocalRef (env->CallObjectMethod (builder.get(), AndroidAlertDialogBuilder.setPositiveButton, - javaString (opts.getButtonText (0)).get(), - CreateJavaInterface (new DialogListener (sharedCallback, 0), - "android/content/DialogInterface$OnClickListener").get())); - - if (opts.getButtonText (1).isNotEmpty()) - builder = LocalRef (env->CallObjectMethod (builder.get(), AndroidAlertDialogBuilder.setNegativeButton, - javaString (opts.getButtonText (1)).get(), - CreateJavaInterface (new DialogListener (sharedCallback, 1), - "android/content/DialogInterface$OnClickListener").get())); - - if (opts.getButtonText (2).isNotEmpty()) - builder = LocalRef (env->CallObjectMethod (builder.get(), AndroidAlertDialogBuilder.setNeutralButton, - javaString (opts.getButtonText (2)).get(), - CreateJavaInterface (new DialogListener (sharedCallback, 2), - "android/content/DialogInterface$OnClickListener").get())); - - LocalRef dialog (env->CallObjectMethod (builder.get(), AndroidAlertDialogBuilder.create)); - - LocalRef window (env->CallObjectMethod (dialog.get(), AndroidDialog.getWindow)); - - if (Desktop::getInstance().getKioskModeComponent() != nullptr) - { - env->CallVoidMethod (window.get(), AndroidWindow.setFlags, FLAG_NOT_FOCUSABLE, FLAG_NOT_FOCUSABLE); - LocalRef decorView (env->CallObjectMethod (window.get(), AndroidWindow.getDecorView)); - env->CallVoidMethod (decorView.get(), AndroidView.setSystemUiVisibility, fullScreenFlags); - } - - env->CallVoidMethod (dialog.get(), AndroidDialog.show); - - if (Desktop::getInstance().getKioskModeComponent() != nullptr) - env->CallVoidMethod (window.get(), AndroidWindow.clearFlags, FLAG_NOT_FOCUSABLE); -} - -void JUCE_CALLTYPE NativeMessageBox::showMessageBoxAsync (MessageBoxIconType /*iconType*/, - const String& title, const String& message, - Component* /*associatedComponent*/, - ModalComponentManager::Callback* callback) -{ - createAndroidDialog (MessageBoxOptions() - .withTitle (title) - .withMessage (message) - .withButton (TRANS("OK")), - callback, AlertWindowMappings::messageBox); -} - -bool JUCE_CALLTYPE NativeMessageBox::showOkCancelBox (MessageBoxIconType /*iconType*/, - const String& title, const String& message, - Component* /*associatedComponent*/, - ModalComponentManager::Callback* callback) -{ - createAndroidDialog (MessageBoxOptions() - .withTitle (title) - .withMessage (message) - .withButton (TRANS("OK")) - .withButton (TRANS("Cancel")), - callback, AlertWindowMappings::okCancel); - - return false; -} - -int JUCE_CALLTYPE NativeMessageBox::showYesNoCancelBox (MessageBoxIconType /*iconType*/, - const String& title, const String& message, - Component* /*associatedComponent*/, - ModalComponentManager::Callback* callback) -{ - createAndroidDialog (MessageBoxOptions() - .withTitle (title) - .withMessage (message) - .withButton (TRANS("Yes")) - .withButton (TRANS("No")) - .withButton (TRANS("Cancel")), - callback, AlertWindowMappings::yesNoCancel); - - return 0; -} - -int JUCE_CALLTYPE NativeMessageBox::showYesNoBox (MessageBoxIconType /*iconType*/, - const String& title, const String& message, - Component* /*associatedComponent*/, - ModalComponentManager::Callback* callback) -{ - createAndroidDialog (MessageBoxOptions() - .withTitle (title) - .withMessage (message) - .withButton (TRANS("Yes")) - .withButton (TRANS("No")), - callback, AlertWindowMappings::okCancel); - - return 0; -} - -void JUCE_CALLTYPE NativeMessageBox::showAsync (const MessageBoxOptions& options, - ModalComponentManager::Callback* callback) -{ - createAndroidDialog (options, callback, AlertWindowMappings::noMapping); -} - -void JUCE_CALLTYPE NativeMessageBox::showAsync (const MessageBoxOptions& options, - std::function callback) -{ - showAsync (options, ModalCallbackFunction::create (callback)); -} - //============================================================================== static bool androidScreenSaverEnabled = true; diff --git a/modules/juce_gui_basics/native/juce_ios_NativeMessageBox.mm b/modules/juce_gui_basics/native/juce_ios_NativeMessageBox.mm new file mode 100644 index 0000000000..17a1a8bfe8 --- /dev/null +++ b/modules/juce_gui_basics/native/juce_ios_NativeMessageBox.mm @@ -0,0 +1,110 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +std::unique_ptr ScopedMessageBoxInterface::create (const MessageBoxOptions& options) +{ + class MessageBox : public ScopedMessageBoxInterface + { + public: + explicit MessageBox (const MessageBoxOptions& opts) : options (opts) {} + + void runAsync (std::function recipient) override + { + if (iOSGlobals::currentlyFocusedPeer == nullptr) + { + // Since iOS8, alert windows need to be associated with a window, so you need to + // have at least one window on screen when you use this + jassertfalse; + return; + } + + alert.reset ([[UIAlertController alertControllerWithTitle: juceStringToNS (options.getTitle()) + message: juceStringToNS (options.getMessage()) + preferredStyle: UIAlertControllerStyleAlert] retain]); + + for (auto i = 0; i < options.getNumButtons(); ++i) + { + const auto text = options.getButtonText (i); + + if (text.isEmpty()) + continue; + + auto* action = [UIAlertAction actionWithTitle: juceStringToNS (text) + style: UIAlertActionStyleDefault + handler: ^(UIAlertAction*) + { + MessageManager::callAsync ([recipient, i] { NullCheckedInvocation::invoke (recipient, i); }); + }]; + + [alert.get() addAction: action]; + + if (i == 0) + [alert.get() setPreferredAction: action]; + } + + [iOSGlobals::currentlyFocusedPeer->controller presentViewController: alert.get() + animated: YES + completion: nil]; + } + + int runSync() override + { + int result = -1; + + JUCE_AUTORELEASEPOOL + { + runAsync ([&result] (int r) { result = r; }); + + while (result < 0) + { + JUCE_AUTORELEASEPOOL + { + [[NSRunLoop mainRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 0.01]]; + } + } + } + + return result; + } + + void close() override + { + if (auto* alertViewController = alert.get()) + [alertViewController dismissViewControllerAnimated: YES completion: nil]; + } + + private: + const MessageBoxOptions options; + NSUniquePtr alert; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MessageBox) + }; + + return std::make_unique (options); +} + +} // namespace juce diff --git a/modules/juce_gui_basics/native/juce_ios_Windowing.mm b/modules/juce_gui_basics/native/juce_ios_Windowing.mm index b8287238ca..b21a0ed928 100644 --- a/modules/juce_gui_basics/native/juce_ios_Windowing.mm +++ b/modules/juce_gui_basics/native/juce_ios_Windowing.mm @@ -470,197 +470,6 @@ void LookAndFeel::playAlertSound() // TODO } -//============================================================================== -class iOSMessageBox -{ -public: - iOSMessageBox (const MessageBoxOptions& opts, - std::unique_ptr&& cb, - bool deleteOnCompletion) - : callback (std::move (cb)), - shouldDeleteThis (deleteOnCompletion) - { - if (iOSGlobals::currentlyFocusedPeer != nullptr) - { - UIAlertController* alert = [UIAlertController alertControllerWithTitle: juceStringToNS (opts.getTitle()) - message: juceStringToNS (opts.getMessage()) - preferredStyle: UIAlertControllerStyleAlert]; - - addButton (alert, opts.getButtonText (0)); - addButton (alert, opts.getButtonText (1)); - addButton (alert, opts.getButtonText (2)); - - [iOSGlobals::currentlyFocusedPeer->controller presentViewController: alert - animated: YES - completion: nil]; - } - else - { - // Since iOS8, alert windows need to be associated with a window, so you need to - // have at least one window on screen when you use this - jassertfalse; - } - } - - int getResult() - { - jassert (callback == nullptr); - - JUCE_AUTORELEASEPOOL - { - while (result < 0) - [[NSRunLoop mainRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 0.01]]; - } - - return result; - } - - void buttonClicked (int buttonIndex) noexcept - { - result = buttonIndex; - - if (callback != nullptr) - callback->modalStateFinished (result); - - if (shouldDeleteThis) - delete this; - } - -private: - void addButton (UIAlertController* alert, const String& text) - { - if (! text.isEmpty()) - { - const auto index = [[alert actions] count]; - - [alert addAction: [UIAlertAction actionWithTitle: juceStringToNS (text) - style: UIAlertActionStyleDefault - handler: ^(UIAlertAction*) { this->buttonClicked ((int) index); }]]; - } - } - - int result = -1; - std::unique_ptr callback; - const bool shouldDeleteThis; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (iOSMessageBox) -}; - -//============================================================================== -static int showDialog (const MessageBoxOptions& options, - ModalComponentManager::Callback* callbackIn, - AlertWindowMappings::MapFn mapFn) -{ - #if JUCE_MODAL_LOOPS_PERMITTED - if (callbackIn == nullptr) - { - JUCE_AUTORELEASEPOOL - { - jassert (mapFn != nullptr); - - iOSMessageBox messageBox (options, nullptr, false); - return mapFn (messageBox.getResult()); - } - } - #endif - - const auto showBox = [options, callbackIn, mapFn] - { - new iOSMessageBox (options, - AlertWindowMappings::getWrappedCallback (callbackIn, mapFn), - true); - }; - - if (MessageManager::getInstance()->isThisTheMessageThread()) - showBox(); - else - MessageManager::callAsync (showBox); - - return 0; -} - -#if JUCE_MODAL_LOOPS_PERMITTED -void JUCE_CALLTYPE NativeMessageBox::showMessageBox (MessageBoxIconType /*iconType*/, - const String& title, const String& message, - Component* /*associatedComponent*/) -{ - showDialog (MessageBoxOptions() - .withTitle (title) - .withMessage (message) - .withButton (TRANS("OK")), - nullptr, AlertWindowMappings::messageBox); -} - -int JUCE_CALLTYPE NativeMessageBox::show (const MessageBoxOptions& options) -{ - return showDialog (options, nullptr, AlertWindowMappings::noMapping); -} -#endif - -void JUCE_CALLTYPE NativeMessageBox::showMessageBoxAsync (MessageBoxIconType /*iconType*/, - const String& title, const String& message, - Component* /*associatedComponent*/, - ModalComponentManager::Callback* callback) -{ - showDialog (MessageBoxOptions() - .withTitle (title) - .withMessage (message) - .withButton (TRANS("OK")), - callback, AlertWindowMappings::messageBox); -} - -bool JUCE_CALLTYPE NativeMessageBox::showOkCancelBox (MessageBoxIconType /*iconType*/, - const String& title, const String& message, - Component* /*associatedComponent*/, - ModalComponentManager::Callback* callback) -{ - return showDialog (MessageBoxOptions() - .withTitle (title) - .withMessage (message) - .withButton (TRANS("OK")) - .withButton (TRANS("Cancel")), - callback, AlertWindowMappings::okCancel) != 0; -} - -int JUCE_CALLTYPE NativeMessageBox::showYesNoCancelBox (MessageBoxIconType /*iconType*/, - const String& title, const String& message, - Component* /*associatedComponent*/, - ModalComponentManager::Callback* callback) -{ - return showDialog (MessageBoxOptions() - .withTitle (title) - .withMessage (message) - .withButton (TRANS("Yes")) - .withButton (TRANS("No")) - .withButton (TRANS("Cancel")), - callback, AlertWindowMappings::yesNoCancel); -} - -int JUCE_CALLTYPE NativeMessageBox::showYesNoBox (MessageBoxIconType /*iconType*/, - const String& title, const String& message, - Component* /*associatedComponent*/, - ModalComponentManager::Callback* callback) -{ - return showDialog (MessageBoxOptions() - .withTitle (title) - .withMessage (message) - .withButton (TRANS("Yes")) - .withButton (TRANS("No")), - callback, AlertWindowMappings::okCancel); -} - -void JUCE_CALLTYPE NativeMessageBox::showAsync (const MessageBoxOptions& options, - ModalComponentManager::Callback* callback) -{ - showDialog (options, callback, AlertWindowMappings::noMapping); -} - -void JUCE_CALLTYPE NativeMessageBox::showAsync (const MessageBoxOptions& options, - std::function callback) -{ - showAsync (options, ModalCallbackFunction::create (callback)); -} - //============================================================================== bool DragAndDropContainer::performExternalDragDropOfFiles (const StringArray&, bool, Component*, std::function) { diff --git a/modules/juce_gui_basics/native/juce_linux_NativeMessageBox.cpp b/modules/juce_gui_basics/native/juce_linux_NativeMessageBox.cpp new file mode 100644 index 0000000000..17ff64db2e --- /dev/null +++ b/modules/juce_gui_basics/native/juce_linux_NativeMessageBox.cpp @@ -0,0 +1,34 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +std::unique_ptr ScopedMessageBoxInterface::create (const MessageBoxOptions& options) +{ + return createAlertWindowImpl (options); +} + +} // namespace juce diff --git a/modules/juce_gui_basics/native/juce_linux_Windowing.cpp b/modules/juce_gui_basics/native/juce_linux_Windowing.cpp index f1e71c0027..e7bc85984d 100644 --- a/modules/juce_gui_basics/native/juce_linux_Windowing.cpp +++ b/modules/juce_gui_basics/native/juce_linux_Windowing.cpp @@ -851,132 +851,6 @@ void LookAndFeel::playAlertSound() std::cout << "\a" << std::flush; } -//============================================================================== -static int showDialog (const MessageBoxOptions& options, - ModalComponentManager::Callback* callback, - Async async) -{ - const auto dummyCallback = [] (int) {}; - - switch (options.getNumButtons()) - { - case 2: - { - if (async == Async::yes && callback == nullptr) - callback = ModalCallbackFunction::create (dummyCallback); - - return AlertWindow::showOkCancelBox (options.getIconType(), - options.getTitle(), - options.getMessage(), - options.getButtonText (0), - options.getButtonText (1), - options.getAssociatedComponent(), - callback) ? 1 : 0; - } - - case 3: - { - if (async == Async::yes && callback == nullptr) - callback = ModalCallbackFunction::create (dummyCallback); - - return AlertWindow::showYesNoCancelBox (options.getIconType(), - options.getTitle(), - options.getMessage(), - options.getButtonText (0), - options.getButtonText (1), - options.getButtonText (2), - options.getAssociatedComponent(), - callback); - } - - case 1: - default: - break; - } - - #if JUCE_MODAL_LOOPS_PERMITTED - if (async == Async::no) - { - AlertWindow::showMessageBox (options.getIconType(), - options.getTitle(), - options.getMessage(), - options.getButtonText (0), - options.getAssociatedComponent()); - } - else - #endif - { - AlertWindow::showMessageBoxAsync (options.getIconType(), - options.getTitle(), - options.getMessage(), - options.getButtonText (0), - options.getAssociatedComponent(), - callback); - } - - return 0; -} - -#if JUCE_MODAL_LOOPS_PERMITTED -void JUCE_CALLTYPE NativeMessageBox::showMessageBox (MessageBoxIconType iconType, - const String& title, const String& message, - Component* /*associatedComponent*/) -{ - AlertWindow::showMessageBox (iconType, title, message); -} - -int JUCE_CALLTYPE NativeMessageBox::show (const MessageBoxOptions& options) -{ - return showDialog (options, nullptr, Async::no); -} -#endif - -void JUCE_CALLTYPE NativeMessageBox::showMessageBoxAsync (MessageBoxIconType iconType, - const String& title, const String& message, - Component* associatedComponent, - ModalComponentManager::Callback* callback) -{ - AlertWindow::showMessageBoxAsync (iconType, title, message, {}, associatedComponent, callback); -} - -bool JUCE_CALLTYPE NativeMessageBox::showOkCancelBox (MessageBoxIconType iconType, - const String& title, const String& message, - Component* associatedComponent, - ModalComponentManager::Callback* callback) -{ - return AlertWindow::showOkCancelBox (iconType, title, message, {}, {}, associatedComponent, callback); -} - -int JUCE_CALLTYPE NativeMessageBox::showYesNoCancelBox (MessageBoxIconType iconType, - const String& title, const String& message, - Component* associatedComponent, - ModalComponentManager::Callback* callback) -{ - return AlertWindow::showYesNoCancelBox (iconType, title, message, {}, {}, {}, - associatedComponent, callback); -} - -int JUCE_CALLTYPE NativeMessageBox::showYesNoBox (MessageBoxIconType iconType, - const String& title, const String& message, - Component* associatedComponent, - ModalComponentManager::Callback* callback) -{ - return AlertWindow::showOkCancelBox (iconType, title, message, TRANS("Yes"), TRANS("No"), - associatedComponent, callback); -} - -void JUCE_CALLTYPE NativeMessageBox::showAsync (const MessageBoxOptions& options, - ModalComponentManager::Callback* callback) -{ - showDialog (options, callback, Async::yes); -} - -void JUCE_CALLTYPE NativeMessageBox::showAsync (const MessageBoxOptions& options, - std::function callback) -{ - showAsync (options, ModalCallbackFunction::create (callback)); -} - //============================================================================== Image juce_createIconForFile (const File&) { diff --git a/modules/juce_gui_basics/native/juce_mac_NativeMessageBox.mm b/modules/juce_gui_basics/native/juce_mac_NativeMessageBox.mm new file mode 100644 index 0000000000..3d406a0373 --- /dev/null +++ b/modules/juce_gui_basics/native/juce_mac_NativeMessageBox.mm @@ -0,0 +1,134 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +std::unique_ptr ScopedMessageBoxInterface::create (const MessageBoxOptions& options) +{ + class OSXMessageBox : public ScopedMessageBoxInterface + { + public: + explicit OSXMessageBox (const MessageBoxOptions& opts) + : options (opts) {} + + void runAsync (std::function recipient) override + { + makeAlert(); + + const auto onDone = [recipient] (NSModalResponse result) + { + recipient (convertResult (result)); + }; + + if (auto* comp = options.getAssociatedComponent()) + { + if (auto* peer = comp->getPeer()) + { + if (auto* view = static_cast (peer->getNativeHandle())) + { + if (auto* window = [view window]) + { + if (@available (macOS 10.9, *)) + { + [alertWindow.get() beginSheetModalForWindow: window completionHandler: ^(NSModalResponse result) + { + onDone (result); + }]; + + return; + } + } + } + } + } + + const auto result = [alertWindow.get() runModal]; + onDone (result); + } + + int runSync() override + { + makeAlert(); + return convertResult ([alertWindow.get() runModal]); + } + + void close() override + { + if (auto* alert = alertWindow.get()) + [[alert window] close]; + } + + private: + static int convertResult (NSModalResponse response) + { + switch (response) + { + case NSAlertFirstButtonReturn: return 0; + case NSAlertSecondButtonReturn: return 1; + case NSAlertThirdButtonReturn: return 2; + default: break; + } + + jassertfalse; + return 0; + } + + static void addButton (NSAlert* alert, const String& button) + { + if (! button.isEmpty()) + [alert addButtonWithTitle: juceStringToNS (button)]; + } + + void makeAlert() + { + NSAlert* alert = [[NSAlert alloc] init]; + + [alert setMessageText: juceStringToNS (options.getTitle())]; + [alert setInformativeText: juceStringToNS (options.getMessage())]; + + [alert setAlertStyle: options.getIconType() == MessageBoxIconType::WarningIcon ? NSAlertStyleCritical + : NSAlertStyleInformational]; + + const auto button1Text = options.getButtonText (0); + + addButton (alert, button1Text.isEmpty() ? "OK" : button1Text); + addButton (alert, options.getButtonText (1)); + addButton (alert, options.getButtonText (2)); + + alertWindow.reset (alert); + } + + NSUniquePtr alertWindow; + MessageBoxOptions options; + std::unique_ptr callback; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OSXMessageBox) + }; + + return std::make_unique (options); +} + +} // namespace juce diff --git a/modules/juce_gui_basics/native/juce_mac_Windowing.mm b/modules/juce_gui_basics/native/juce_mac_Windowing.mm index f04b323614..ba7e7e03d8 100644 --- a/modules/juce_gui_basics/native/juce_mac_Windowing.mm +++ b/modules/juce_gui_basics/native/juce_mac_Windowing.mm @@ -31,219 +31,6 @@ void LookAndFeel::playAlertSound() NSBeep(); } -//============================================================================== -class OSXMessageBox : private AsyncUpdater -{ -public: - OSXMessageBox (const MessageBoxOptions& opts, - std::unique_ptr&& c) - : options (opts), callback (std::move (c)) - { - } - - int getResult() const - { - return convertResult ([getAlert() runModal]); - } - - using AsyncUpdater::triggerAsyncUpdate; - -private: - static int convertResult (NSModalResponse response) - { - switch (response) - { - case NSAlertFirstButtonReturn: return 0; - case NSAlertSecondButtonReturn: return 1; - case NSAlertThirdButtonReturn: return 2; - default: break; - } - - jassertfalse; - return 0; - } - - void handleAsyncUpdate() override - { - if (auto* comp = options.getAssociatedComponent()) - { - if (auto* peer = comp->getPeer()) - { - if (auto* view = static_cast (peer->getNativeHandle())) - { - if (auto* window = [view window]) - { - if (@available (macOS 10.9, *)) - { - [getAlert() beginSheetModalForWindow: window completionHandler: ^(NSModalResponse result) - { - handleModalFinished (result); - }]; - - return; - } - } - } - } - } - - handleModalFinished ([getAlert() runModal]); - } - - void handleModalFinished (NSModalResponse result) - { - if (callback != nullptr) - callback->modalStateFinished (convertResult (result)); - - delete this; - } - - static void addButton (NSAlert* alert, const String& button) - { - if (! button.isEmpty()) - [alert addButtonWithTitle: juceStringToNS (button)]; - } - - NSAlert* getAlert() const - { - NSAlert* alert = [[[NSAlert alloc] init] autorelease]; - - [alert setMessageText: juceStringToNS (options.getTitle())]; - [alert setInformativeText: juceStringToNS (options.getMessage())]; - - [alert setAlertStyle: options.getIconType() == MessageBoxIconType::WarningIcon ? NSAlertStyleCritical - : NSAlertStyleInformational]; - - const auto button1Text = options.getButtonText (0); - - addButton (alert, button1Text.isEmpty() ? "OK" : button1Text); - addButton (alert, options.getButtonText (1)); - addButton (alert, options.getButtonText (2)); - - return alert; - } - - MessageBoxOptions options; - std::unique_ptr callback; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OSXMessageBox) -}; - -static int showDialog (const MessageBoxOptions& options, - ModalComponentManager::Callback* callbackIn, - AlertWindowMappings::MapFn mapFn) -{ - #if JUCE_MODAL_LOOPS_PERMITTED - if (callbackIn == nullptr) - { - jassert (mapFn != nullptr); - - OSXMessageBox messageBox (options, nullptr); - return mapFn (messageBox.getResult()); - } - #endif - - auto messageBox = std::make_unique (options, - AlertWindowMappings::getWrappedCallback (callbackIn, mapFn)); - - messageBox->triggerAsyncUpdate(); - messageBox.release(); - - return 0; -} - -#if JUCE_MODAL_LOOPS_PERMITTED -void JUCE_CALLTYPE NativeMessageBox::showMessageBox (MessageBoxIconType iconType, - const String& title, const String& message, - Component* associatedComponent) -{ - showDialog (MessageBoxOptions() - .withIconType (iconType) - .withTitle (title) - .withMessage (message) - .withButton (TRANS("OK")) - .withAssociatedComponent (associatedComponent), - nullptr, AlertWindowMappings::messageBox); -} - -int JUCE_CALLTYPE NativeMessageBox::show (const MessageBoxOptions& options) -{ - return showDialog (options, nullptr, AlertWindowMappings::noMapping); -} -#endif - -void JUCE_CALLTYPE NativeMessageBox::showMessageBoxAsync (MessageBoxIconType iconType, - const String& title, const String& message, - Component* associatedComponent, - ModalComponentManager::Callback* callback) -{ - showDialog (MessageBoxOptions() - .withIconType (iconType) - .withTitle (title) - .withMessage (message) - .withButton (TRANS("OK")) - .withAssociatedComponent (associatedComponent), - callback, AlertWindowMappings::messageBox); -} - -bool JUCE_CALLTYPE NativeMessageBox::showOkCancelBox (MessageBoxIconType iconType, - const String& title, const String& message, - Component* associatedComponent, - ModalComponentManager::Callback* callback) -{ - return showDialog (MessageBoxOptions() - .withIconType (iconType) - .withTitle (title) - .withMessage (message) - .withButton (TRANS("OK")) - .withButton (TRANS("Cancel")) - .withAssociatedComponent (associatedComponent), - callback, AlertWindowMappings::okCancel) != 0; -} - -int JUCE_CALLTYPE NativeMessageBox::showYesNoCancelBox (MessageBoxIconType iconType, - const String& title, const String& message, - Component* associatedComponent, - ModalComponentManager::Callback* callback) -{ - return showDialog (MessageBoxOptions() - .withIconType (iconType) - .withTitle (title) - .withMessage (message) - .withButton (TRANS("Yes")) - .withButton (TRANS("No")) - .withButton (TRANS("Cancel")) - .withAssociatedComponent (associatedComponent), - callback, AlertWindowMappings::yesNoCancel); -} - -int JUCE_CALLTYPE NativeMessageBox::showYesNoBox (MessageBoxIconType iconType, - const String& title, const String& message, - Component* associatedComponent, - ModalComponentManager::Callback* callback) -{ - return showDialog (MessageBoxOptions() - .withIconType (iconType) - .withTitle (title) - .withMessage (message) - .withButton (TRANS("Yes")) - .withButton (TRANS("No")) - .withAssociatedComponent (associatedComponent), - callback, AlertWindowMappings::okCancel); -} - -void JUCE_CALLTYPE NativeMessageBox::showAsync (const MessageBoxOptions& options, - ModalComponentManager::Callback* callback) -{ - showDialog (options, callback, AlertWindowMappings::noMapping); -} - -void JUCE_CALLTYPE NativeMessageBox::showAsync (const MessageBoxOptions& options, - std::function callback) -{ - showAsync (options, ModalCallbackFunction::create (callback)); -} - //============================================================================== static NSRect getDragRect (NSView* view, NSEvent* event) { diff --git a/modules/juce_gui_basics/native/juce_win32_NativeMessageBox.cpp b/modules/juce_gui_basics/native/juce_win32_NativeMessageBox.cpp new file mode 100644 index 0000000000..a5f48069a6 --- /dev/null +++ b/modules/juce_gui_basics/native/juce_win32_NativeMessageBox.cpp @@ -0,0 +1,346 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +#if JUCE_MSVC + // required to enable the newer dialog box on vista and above + #pragma comment(linker, \ + "\"/MANIFESTDEPENDENCY:type='Win32' " \ + "name='Microsoft.Windows.Common-Controls' " \ + "version='6.0.0.0' " \ + "processorArchitecture='*' " \ + "publicKeyToken='6595b64144ccf1df' " \ + "language='*'\"" \ + ) +#endif + +std::unique_ptr ScopedMessageBoxInterface::create (const MessageBoxOptions& options) +{ + class WindowsMessageBoxBase : public ScopedMessageBoxInterface + { + public: + explicit WindowsMessageBoxBase (Component* comp) + : associatedComponent (comp) {} + + void runAsync (std::function recipient) override + { + future = std::async (std::launch::async, [showMessageBox = getShowMessageBox(), recipient] + { + const auto initComResult = CoInitializeEx (nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + + if (initComResult != S_OK) + return; + + const ScopeGuard scope { [] { CoUninitialize(); } }; + + const auto messageResult = showMessageBox != nullptr ? showMessageBox() : 0; + NullCheckedInvocation::invoke (recipient, messageResult); + }); + } + + int runSync() override + { + if (auto showMessageBox = getShowMessageBox()) + return showMessageBox(); + + return 0; + } + + void close() override + { + if (auto* toClose = windowHandle.exchange (nullptr)) + EndDialog (toClose, 0); + } + + void setDialogWindowHandle (HWND dialogHandle) + { + windowHandle = dialogHandle; + } + + private: + std::function getShowMessageBox() + { + const auto parent = associatedComponent != nullptr ? (HWND) associatedComponent->getWindowHandle() : nullptr; + return getShowMessageBoxForParent (parent); + } + + /* Returns a function that should display a message box and return the result. + + getShowMessageBoxForParent() will be called on the message thread. + + The returned function will be called on a separate thread, in order to avoid blocking the + message thread. + + 'this' is guaranteed to be alive when the returned function is called. + */ + virtual std::function getShowMessageBoxForParent (HWND parent) = 0; + + Component::SafePointer associatedComponent; + std::atomic windowHandle { nullptr }; + std::future future; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WindowsMessageBoxBase) + }; + + class PreVistaMessageBox : public WindowsMessageBoxBase + { + public: + PreVistaMessageBox (const MessageBoxOptions& opts, UINT extraFlags) + : WindowsMessageBoxBase (opts.getAssociatedComponent()), + flags (extraFlags | getMessageBoxFlags (opts.getIconType())), + title (opts.getTitle()), message (opts.getMessage()) {} + + private: + std::function getShowMessageBoxForParent (const HWND parent) override + { + JUCE_ASSERT_MESSAGE_THREAD + + static std::map map; + static std::mutex mapMutex; + + return [this, parent] + { + const auto threadId = GetCurrentThreadId(); + + { + const std::scoped_lock scope { mapMutex }; + map.emplace (threadId, this); + } + + const ScopeGuard eraseFromMap { [threadId] + { + const std::scoped_lock scope { mapMutex }; + map.erase (threadId); + } }; + + const auto hookCallback = [] (int nCode, const WPARAM wParam, const LPARAM lParam) + { + auto* params = reinterpret_cast (lParam); + + if (nCode >= 0 + && params != nullptr + && (params->message == WM_INITDIALOG || params->message == WM_DESTROY)) + { + const auto callbackThreadId = GetCurrentThreadId(); + + const std::scoped_lock scope { mapMutex }; + + if (const auto iter = map.find (callbackThreadId); iter != map.cend()) + iter->second->setDialogWindowHandle (params->message == WM_INITDIALOG ? params->hwnd : nullptr); + } + + return CallNextHookEx ({}, nCode, wParam, lParam); + }; + + const auto hook = SetWindowsHookEx (WH_CALLWNDPROC, + hookCallback, + (HINSTANCE) juce::Process::getCurrentModuleInstanceHandle(), + threadId); + const ScopeGuard removeHook { [hook] { UnhookWindowsHookEx (hook); } }; + + const auto result = MessageBox (parent, message.toWideCharPointer(), title.toWideCharPointer(), flags); + + if (result == IDYES || result == IDOK) return 0; + if (result == IDNO && ((flags & 1) != 0)) return 1; + + return 2; + }; + } + + static UINT getMessageBoxFlags (MessageBoxIconType iconType) noexcept + { + // this window can get lost behind JUCE windows which are set to be alwaysOnTop + // so if there are any set it to be topmost + const auto topmostFlag = juce_areThereAnyAlwaysOnTopWindows() ? MB_TOPMOST : 0; + + const auto iconFlags = [&]() -> decltype (topmostFlag) + { + switch (iconType) + { + case MessageBoxIconType::QuestionIcon: return MB_ICONQUESTION; + case MessageBoxIconType::WarningIcon: return MB_ICONWARNING; + case MessageBoxIconType::InfoIcon: return MB_ICONINFORMATION; + case MessageBoxIconType::NoIcon: break; + } + + return 0; + }(); + + return static_cast (MB_TASKMODAL | MB_SETFOREGROUND | topmostFlag | iconFlags); + } + + const UINT flags; + const String title, message; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PreVistaMessageBox) + }; + + class WindowsTaskDialog : public WindowsMessageBoxBase + { + static auto getTaskDialogFunc() + { + using TaskDialogIndirectFunc = HRESULT (WINAPI*) (const TASKDIALOGCONFIG*, INT*, INT*, BOOL*); + + static const auto result = [&]() -> TaskDialogIndirectFunc + { + if (SystemStats::getOperatingSystemType() < SystemStats::WinVista) + return nullptr; + + const auto comctl = "Comctl32.dll"; + LoadLibraryA (comctl); + const auto comctlModule = GetModuleHandleA (comctl); + + if (comctlModule != nullptr) + return (TaskDialogIndirectFunc) GetProcAddress (comctlModule, "TaskDialogIndirect"); + + return nullptr; + }(); + + return result; + } + + public: + explicit WindowsTaskDialog (const MessageBoxOptions& opts) + : WindowsMessageBoxBase (opts.getAssociatedComponent()), + iconType (opts.getIconType()), + title (opts.getTitle()), message (opts.getMessage()), + buttons { opts.getButtonText (0), opts.getButtonText (1), opts.getButtonText (2) } {} + + static bool isAvailable() + { + return getTaskDialogFunc() != nullptr; + } + + private: + std::function getShowMessageBoxForParent (const HWND parent) override + { + JUCE_ASSERT_MESSAGE_THREAD + + return [this, parent] + { + TASKDIALOGCONFIG config{}; + + config.cbSize = sizeof (config); + config.hwndParent = parent; + config.pszWindowTitle = title.toWideCharPointer(); + config.pszContent = message.toWideCharPointer(); + config.hInstance = (HINSTANCE) Process::getCurrentModuleInstanceHandle(); + config.lpCallbackData = reinterpret_cast (this); + config.pfCallback = [] (HWND hwnd, UINT msg, WPARAM, LPARAM, LONG_PTR lpRefData) + { + if (auto* t = reinterpret_cast (lpRefData)) + { + switch (msg) + { + case TDN_CREATED: + case TDN_DIALOG_CONSTRUCTED: + t->setDialogWindowHandle (hwnd); + break; + + case TDN_DESTROYED: + t->setDialogWindowHandle (nullptr); + break; + } + } + + return S_OK; + }; + + if (iconType == MessageBoxIconType::QuestionIcon) + { + if (auto* questionIcon = LoadIcon (nullptr, IDI_QUESTION)) + { + config.hMainIcon = questionIcon; + config.dwFlags |= TDF_USE_HICON_MAIN; + } + } + else + { + config.pszMainIcon = [&]() -> LPWSTR + { + switch (iconType) + { + case MessageBoxIconType::WarningIcon: return TD_WARNING_ICON; + case MessageBoxIconType::InfoIcon: return TD_INFORMATION_ICON; + + case MessageBoxIconType::QuestionIcon: JUCE_FALLTHROUGH + case MessageBoxIconType::NoIcon: + break; + } + + return nullptr; + }(); + } + + std::vector buttonLabels; + + for (const auto& buttonText : buttons) + if (buttonText.isNotEmpty()) + buttonLabels.push_back ({ (int) buttonLabels.size(), buttonText.toWideCharPointer() }); + + config.pButtons = buttonLabels.data(); + config.cButtons = (UINT) buttonLabels.size(); + + int buttonIndex = 0; + + if (auto* func = getTaskDialogFunc()) + func (&config, &buttonIndex, nullptr, nullptr); + else + jassertfalse; + + return buttonIndex; + }; + } + + const MessageBoxIconType iconType; + const String title, message; + const std::array buttons; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WindowsTaskDialog) + }; + + if (WindowsTaskDialog::isAvailable()) + return std::make_unique (options); + + const auto extraFlags = [&options] + { + const auto numButtons = options.getNumButtons(); + + if (numButtons == 3) + return MB_YESNOCANCEL; + + if (numButtons == 2) + return options.getButtonText (0) == "OK" ? MB_OKCANCEL + : MB_YESNO; + + return MB_OK; + }(); + + return std::make_unique (options, (UINT) extraFlags); +} + +} // namespace juce diff --git a/modules/juce_gui_basics/native/juce_win32_Windowing.cpp b/modules/juce_gui_basics/native/juce_win32_Windowing.cpp index 6756cfd0d2..d5b3f07c1d 100644 --- a/modules/juce_gui_basics/native/juce_win32_Windowing.cpp +++ b/modules/juce_gui_basics/native/juce_win32_Windowing.cpp @@ -4804,349 +4804,6 @@ bool juce_areThereAnyAlwaysOnTopWindows() return anyAlwaysOnTopFound; } -//============================================================================== -#if JUCE_MSVC - // required to enable the newer dialog box on vista and above - #pragma comment(linker, \ - "\"/MANIFESTDEPENDENCY:type='Win32' " \ - "name='Microsoft.Windows.Common-Controls' " \ - "version='6.0.0.0' " \ - "processorArchitecture='*' " \ - "publicKeyToken='6595b64144ccf1df' " \ - "language='*'\"" \ - ) -#endif - -class WindowsMessageBoxBase : private AsyncUpdater -{ -public: - WindowsMessageBoxBase (Component* comp, - std::unique_ptr&& cb) - : associatedComponent (comp), - callback (std::move (cb)) - { - } - - virtual int getResult() = 0; - - HWND getParentHWND() const - { - if (associatedComponent != nullptr) - return (HWND) associatedComponent->getWindowHandle(); - - return nullptr; - } - - using AsyncUpdater::triggerAsyncUpdate; - -private: - void handleAsyncUpdate() override - { - const auto result = getResult(); - - if (callback != nullptr) - callback->modalStateFinished (result); - - delete this; - } - - Component::SafePointer associatedComponent; - std::unique_ptr callback; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WindowsMessageBoxBase) -}; - -class PreVistaMessageBox : public WindowsMessageBoxBase -{ -public: - PreVistaMessageBox (const MessageBoxOptions& opts, - UINT extraFlags, - std::unique_ptr&& cb) - : WindowsMessageBoxBase (opts.getAssociatedComponent(), std::move (cb)), - flags (extraFlags | getMessageBoxFlags (opts.getIconType())), - title (opts.getTitle()), message (opts.getMessage()) - { - } - - int getResult() override - { - const auto result = MessageBox (getParentHWND(), message.toWideCharPointer(), title.toWideCharPointer(), flags); - - if (result == IDYES || result == IDOK) return 0; - if (result == IDNO && ((flags & 1) != 0)) return 1; - - return 2; - } - -private: - static UINT getMessageBoxFlags (MessageBoxIconType iconType) noexcept - { - // this window can get lost behind JUCE windows which are set to be alwaysOnTop - // so if there are any set it to be topmost - const auto topmostFlag = juce_areThereAnyAlwaysOnTopWindows() ? MB_TOPMOST : 0; - - const auto iconFlags = [&]() -> decltype (topmostFlag) - { - switch (iconType) - { - case MessageBoxIconType::QuestionIcon: return MB_ICONQUESTION; - case MessageBoxIconType::WarningIcon: return MB_ICONWARNING; - case MessageBoxIconType::InfoIcon: return MB_ICONINFORMATION; - case MessageBoxIconType::NoIcon: break; - } - - return 0; - }(); - - return static_cast (MB_TASKMODAL | MB_SETFOREGROUND | topmostFlag | iconFlags); - } - - const UINT flags; - const String title, message; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PreVistaMessageBox) -}; - -using TaskDialogIndirectFunc = HRESULT (WINAPI*) (const TASKDIALOGCONFIG*, INT*, INT*, BOOL*); -static TaskDialogIndirectFunc taskDialogIndirect = nullptr; - -class WindowsTaskDialog : public WindowsMessageBoxBase -{ -public: - WindowsTaskDialog (const MessageBoxOptions& opts, - std::unique_ptr&& cb) - : WindowsMessageBoxBase (opts.getAssociatedComponent(), std::move (cb)), - iconType (opts.getIconType()), - title (opts.getTitle()), message (opts.getMessage()), - button1 (opts.getButtonText (0)), button2 (opts.getButtonText (1)), button3 (opts.getButtonText (2)) - { - } - - int getResult() override - { - TASKDIALOGCONFIG config{}; - - config.cbSize = sizeof (config); - config.hwndParent = getParentHWND(); - config.pszWindowTitle = title.toWideCharPointer(); - config.pszContent = message.toWideCharPointer(); - config.hInstance = (HINSTANCE) Process::getCurrentModuleInstanceHandle(); - - if (iconType == MessageBoxIconType::QuestionIcon) - { - if (auto* questionIcon = LoadIcon (nullptr, IDI_QUESTION)) - { - config.hMainIcon = questionIcon; - config.dwFlags |= TDF_USE_HICON_MAIN; - } - } - else - { - auto icon = [this]() -> LPWSTR - { - switch (iconType) - { - case MessageBoxIconType::WarningIcon: return TD_WARNING_ICON; - case MessageBoxIconType::InfoIcon: return TD_INFORMATION_ICON; - - case MessageBoxIconType::QuestionIcon: JUCE_FALLTHROUGH - case MessageBoxIconType::NoIcon: - break; - } - - return nullptr; - }(); - - if (icon != nullptr) - config.pszMainIcon = icon; - } - - std::vector buttons; - - for (const auto* buttonText : { &button1, &button2, &button3 }) - if (buttonText->isNotEmpty()) - buttons.push_back ({ (int) buttons.size(), buttonText->toWideCharPointer() }); - - config.pButtons = buttons.data(); - config.cButtons = (UINT) buttons.size(); - - int buttonIndex = 0; - taskDialogIndirect (&config, &buttonIndex, nullptr, nullptr); - - return buttonIndex; - } - - static bool loadTaskDialog() - { - static bool hasChecked = false; - - if (! hasChecked) - { - hasChecked = true; - - const auto comctl = "Comctl32.dll"; - LoadLibraryA (comctl); - const auto comctlModule = GetModuleHandleA (comctl); - - if (comctlModule != nullptr) - taskDialogIndirect = (TaskDialogIndirectFunc) GetProcAddress (comctlModule, "TaskDialogIndirect"); - } - - return taskDialogIndirect != nullptr; - } - -private: - MessageBoxIconType iconType; - String title, message, button1, button2, button3; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WindowsTaskDialog) -}; - -static std::unique_ptr createMessageBox (const MessageBoxOptions& options, - std::unique_ptr callback) -{ - const auto useTaskDialog = - #if JUCE_MODAL_LOOPS_PERMITTED - callback != nullptr && - #endif - SystemStats::getOperatingSystemType() >= SystemStats::WinVista - && WindowsTaskDialog::loadTaskDialog(); - - if (useTaskDialog) - return std::make_unique (options, std::move (callback)); - - const auto extraFlags = [&options] - { - const auto numButtons = options.getNumButtons(); - - if (numButtons == 3) - return MB_YESNOCANCEL; - - if (numButtons == 2) - return options.getButtonText (0) == "OK" ? MB_OKCANCEL - : MB_YESNO; - - return MB_OK; - }(); - - return std::make_unique (options, (UINT) extraFlags, std::move (callback)); -} - -static int showDialog (const MessageBoxOptions& options, - ModalComponentManager::Callback* callbackIn, - AlertWindowMappings::MapFn mapFn) -{ - #if JUCE_MODAL_LOOPS_PERMITTED - if (callbackIn == nullptr) - { - jassert (mapFn != nullptr); - - auto messageBox = createMessageBox (options, nullptr); - return mapFn (messageBox->getResult()); - } - #endif - - auto messageBox = createMessageBox (options, - AlertWindowMappings::getWrappedCallback (callbackIn, mapFn)); - - messageBox->triggerAsyncUpdate(); - messageBox.release(); - - return 0; -} - -#if JUCE_MODAL_LOOPS_PERMITTED -void JUCE_CALLTYPE NativeMessageBox::showMessageBox (MessageBoxIconType iconType, - const String& title, const String& message, - Component* associatedComponent) -{ - showDialog (MessageBoxOptions() - .withIconType (iconType) - .withTitle (title) - .withMessage (message) - .withButton (TRANS("OK")) - .withAssociatedComponent (associatedComponent), - nullptr, AlertWindowMappings::messageBox); -} - -int JUCE_CALLTYPE NativeMessageBox::show (const MessageBoxOptions& options) -{ - return showDialog (options, nullptr, AlertWindowMappings::noMapping); -} -#endif - -void JUCE_CALLTYPE NativeMessageBox::showMessageBoxAsync (MessageBoxIconType iconType, - const String& title, const String& message, - Component* associatedComponent, - ModalComponentManager::Callback* callback) -{ - showDialog (MessageBoxOptions() - .withIconType (iconType) - .withTitle (title) - .withMessage (message) - .withButton (TRANS("OK")) - .withAssociatedComponent (associatedComponent), - callback, AlertWindowMappings::messageBox); -} - -bool JUCE_CALLTYPE NativeMessageBox::showOkCancelBox (MessageBoxIconType iconType, - const String& title, const String& message, - Component* associatedComponent, - ModalComponentManager::Callback* callback) -{ - return showDialog (MessageBoxOptions() - .withIconType (iconType) - .withTitle (title) - .withMessage (message) - .withButton (TRANS("OK")) - .withButton (TRANS("Cancel")) - .withAssociatedComponent (associatedComponent), - callback, AlertWindowMappings::okCancel) != 0; -} - -int JUCE_CALLTYPE NativeMessageBox::showYesNoCancelBox (MessageBoxIconType iconType, - const String& title, const String& message, - Component* associatedComponent, - ModalComponentManager::Callback* callback) -{ - return showDialog (MessageBoxOptions() - .withIconType (iconType) - .withTitle (title) - .withMessage (message) - .withButton (TRANS("Yes")) - .withButton (TRANS("No")) - .withButton (TRANS("Cancel")) - .withAssociatedComponent (associatedComponent), - callback, AlertWindowMappings::yesNoCancel); -} - -int JUCE_CALLTYPE NativeMessageBox::showYesNoBox (MessageBoxIconType iconType, - const String& title, const String& message, - Component* associatedComponent, - ModalComponentManager::Callback* callback) -{ - return showDialog (MessageBoxOptions() - .withIconType (iconType) - .withTitle (title) - .withMessage (message) - .withButton (TRANS("Yes")) - .withButton (TRANS("No")) - .withAssociatedComponent (associatedComponent), - callback, AlertWindowMappings::okCancel); -} - -void JUCE_CALLTYPE NativeMessageBox::showAsync (const MessageBoxOptions& options, - ModalComponentManager::Callback* callback) -{ - showDialog (options, callback, AlertWindowMappings::noMapping); -} - -void JUCE_CALLTYPE NativeMessageBox::showAsync (const MessageBoxOptions& options, - std::function callback) -{ - showAsync (options, ModalCallbackFunction::create (callback)); -} - //============================================================================== bool MouseInputSource::SourceList::addSource() { diff --git a/modules/juce_gui_basics/windows/juce_AlertWindow.cpp b/modules/juce_gui_basics/windows/juce_AlertWindow.cpp index 14de42fa2f..3816af7751 100644 --- a/modules/juce_gui_basics/windows/juce_AlertWindow.cpp +++ b/modules/juce_gui_basics/windows/juce_AlertWindow.cpp @@ -35,6 +35,83 @@ static juce_wchar getDefaultPasswordChar() noexcept #endif } +static std::unique_ptr createAlertWindowImpl (const MessageBoxOptions& opts) +{ + class AlertWindowImpl : public ScopedMessageBoxInterface + { + public: + explicit AlertWindowImpl (const MessageBoxOptions& opts) : options (opts) {} + + void runAsync (std::function recipient) override + { + if (auto* comp = setUpAlert()) + comp->enterModalState (true, ModalCallbackFunction::create (std::move (recipient)), true); + else + NullCheckedInvocation::invoke (recipient, 0); + } + + int runSync() override + { + #if JUCE_MODAL_LOOPS_PERMITTED + if (auto comp = rawToUniquePtr (setUpAlert())) + return comp->runModalLoop(); + #endif + + jassertfalse; + return 0; + } + + void close() override + { + if (alert != nullptr) + if (alert->isCurrentlyModal()) + alert->exitModalState(); + + alert = nullptr; + } + + private: + Component* setUpAlert() + { + auto* component = options.getAssociatedComponent(); + + auto& lf = component != nullptr ? component->getLookAndFeel() + : LookAndFeel::getDefaultLookAndFeel(); + + alert = lf.createAlertWindow (options.getTitle(), + options.getMessage(), + options.getButtonText (0), + options.getButtonText (1), + options.getButtonText (2), + options.getIconType(), + options.getNumButtons(), + component); + + if (alert == nullptr) + { + // You have to return an alert box! + jassertfalse; + return nullptr; + } + + alert->setAlwaysOnTop (juce_areThereAnyAlwaysOnTopWindows()); + return alert; + } + + const MessageBoxOptions options; + Component::SafePointer alert; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AlertWindowImpl) + }; + + return std::make_unique (opts); +} + +static int showAlertWindowUnmanaged (const MessageBoxOptions& opts, ModalComponentManager::Callback* cb) +{ + return ScopedMessageBox::Pimpl::showUnmanaged (createAlertWindowImpl (opts), cb); +} + //============================================================================== AlertWindow::AlertWindow (const String& title, const String& message, @@ -564,97 +641,7 @@ int AlertWindow::getDesktopWindowStyleFlags() const return getLookAndFeel().getAlertBoxWindowFlags(); } -enum class Async { no, yes }; - //============================================================================== -class AlertWindowInfo -{ -public: - AlertWindowInfo (const MessageBoxOptions& opts, - std::unique_ptr&& cb, - Async showAsync) - : options (opts), - callback (std::move (cb)), - async (showAsync) - { - } - - int invoke() const - { - MessageManager::getInstance()->callFunctionOnMessageThread (showCallback, (void*) this); - return returnValue; - } - -private: - static void* showCallback (void* userData) - { - static_cast (userData)->show(); - return nullptr; - } - - void show() - { - auto* component = options.getAssociatedComponent(); - - auto& lf = (component != nullptr ? component->getLookAndFeel() - : LookAndFeel::getDefaultLookAndFeel()); - - std::unique_ptr alertBox (lf.createAlertWindow (options.getTitle(), options.getMessage(), - options.getButtonText (0), options.getButtonText (1), options.getButtonText (2), - options.getIconType(), options.getNumButtons(), component)); - - jassert (alertBox != nullptr); // you have to return one of these! - - alertBox->setAlwaysOnTop (juce_areThereAnyAlwaysOnTopWindows()); - - #if JUCE_MODAL_LOOPS_PERMITTED - if (async == Async::no) - returnValue = alertBox->runModalLoop(); - else - #endif - { - ignoreUnused (async); - - alertBox->enterModalState (true, callback.release(), true); - alertBox.release(); - } - } - - MessageBoxOptions options; - std::unique_ptr callback; - const Async async; - int returnValue = 0; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AlertWindowInfo) -}; - -namespace AlertWindowMappings -{ - using MapFn = int (*) (int); - - static inline int noMapping (int buttonIndex) { return buttonIndex; } - static inline int messageBox (int) { return 0; } - static inline int okCancel (int buttonIndex) { return buttonIndex == 0 ? 1 : 0; } - static inline int yesNoCancel (int buttonIndex) { return buttonIndex == 2 ? 0 : buttonIndex + 1; } - - static std::unique_ptr getWrappedCallback (ModalComponentManager::Callback* callbackIn, - MapFn mapFn) - { - jassert (mapFn != nullptr); - - if (callbackIn == nullptr) - return nullptr; - - auto wrappedCallback = [innerCallback = rawToUniquePtr (callbackIn), mapFn] (int buttonIndex) - { - innerCallback->modalStateFinished (mapFn (buttonIndex)); - }; - - return rawToUniquePtr (ModalCallbackFunction::create (std::move (wrappedCallback))); - } - -} - #if JUCE_MODAL_LOOPS_PERMITTED void AlertWindow::showMessageBox (MessageBoxIconType iconType, const String& title, @@ -675,8 +662,7 @@ int AlertWindow::show (const MessageBoxOptions& options) if (LookAndFeel::getDefaultLookAndFeel().isUsingNativeAlertWindows()) return NativeMessageBox::show (options); - AlertWindowInfo info (options, nullptr, Async::no); - return info.invoke(); + return showAlertWindowUnmanaged (options, nullptr); } bool AlertWindow::showNativeDialogBox (const String& title, @@ -694,14 +680,9 @@ bool AlertWindow::showNativeDialogBox (const String& title, void AlertWindow::showAsync (const MessageBoxOptions& options, ModalComponentManager::Callback* callback) { if (LookAndFeel::getDefaultLookAndFeel().isUsingNativeAlertWindows()) - { NativeMessageBox::showAsync (options, callback); - } else - { - AlertWindowInfo info (options, rawToUniquePtr (callback), Async::yes); - info.invoke(); - } + showAlertWindowUnmanaged (options, callback); } void AlertWindow::showAsync (const MessageBoxOptions& options, std::function callback) @@ -716,37 +697,31 @@ void AlertWindow::showMessageBoxAsync (MessageBoxIconType iconType, Component* associatedComponent, ModalComponentManager::Callback* callback) { - showAsync (MessageBoxOptions() - .withIconType (iconType) - .withTitle (title) - .withMessage (message) - .withButton (buttonText.isEmpty() ? TRANS("OK") : buttonText) - .withAssociatedComponent (associatedComponent), - callback); + auto options = MessageBoxOptions::makeOptionsOk (iconType, + title, + message, + buttonText, + associatedComponent); + showAsync (options, callback); } static int showMaybeAsync (const MessageBoxOptions& options, - ModalComponentManager::Callback* callbackIn, - AlertWindowMappings::MapFn mapFn) + ModalComponentManager::Callback* callbackIn) { - const auto showAsync = (callbackIn != nullptr ? Async::yes - : Async::no); - - auto callback = AlertWindowMappings::getWrappedCallback (callbackIn, mapFn); - if (LookAndFeel::getDefaultLookAndFeel().isUsingNativeAlertWindows()) { #if JUCE_MODAL_LOOPS_PERMITTED - if (showAsync == Async::no) - return mapFn (NativeMessageBox::show (options)); + if (callbackIn == nullptr) + return NativeMessageBox::show (options); #endif - NativeMessageBox::showAsync (options, callback.release()); +JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") + NativeMessageBox::showAsync (options, callbackIn); +JUCE_END_IGNORE_WARNINGS_GCC_LIKE return false; } - AlertWindowInfo info (options, std::move (callback), showAsync); - return info.invoke(); + return showAlertWindowUnmanaged (options, callbackIn); } bool AlertWindow::showOkCancelBox (MessageBoxIconType iconType, @@ -757,17 +732,13 @@ bool AlertWindow::showOkCancelBox (MessageBoxIconType iconType, Component* associatedComponent, ModalComponentManager::Callback* callback) { - return showMaybeAsync (MessageBoxOptions() - .withIconType (iconType) - .withTitle (title) - .withMessage (message) - .withButton (button1Text.isEmpty() ? TRANS("OK") : button1Text) - .withButton (button2Text.isEmpty() ? TRANS("Cancel") : button2Text) - .withAssociatedComponent (associatedComponent), - callback, - LookAndFeel::getDefaultLookAndFeel().isUsingNativeAlertWindows() - ? AlertWindowMappings::okCancel - : AlertWindowMappings::noMapping) == 1; + auto options = MessageBoxOptions::makeOptionsOkCancel (iconType, + title, + message, + button1Text, + button2Text, + associatedComponent); + return showMaybeAsync (options, callback) == 1; } int AlertWindow::showYesNoCancelBox (MessageBoxIconType iconType, @@ -779,18 +750,22 @@ int AlertWindow::showYesNoCancelBox (MessageBoxIconType iconType, Component* associatedComponent, ModalComponentManager::Callback* callback) { - return showMaybeAsync (MessageBoxOptions() - .withIconType (iconType) - .withTitle (title) - .withMessage (message) - .withButton (button1Text.isEmpty() ? TRANS("Yes") : button1Text) - .withButton (button2Text.isEmpty() ? TRANS("No") : button2Text) - .withButton (button3Text.isEmpty() ? TRANS("Cancel") : button3Text) - .withAssociatedComponent (associatedComponent), - callback, - LookAndFeel::getDefaultLookAndFeel().isUsingNativeAlertWindows() - ? AlertWindowMappings::yesNoCancel - : AlertWindowMappings::noMapping); + auto options = MessageBoxOptions::makeOptionsYesNoCancel (iconType, + title, + message, + button1Text, + button2Text, + button3Text, + associatedComponent); + return showMaybeAsync (options, callback); +} + +ScopedMessageBox AlertWindow::showScopedAsync (const MessageBoxOptions& options, std::function callback) +{ + if (LookAndFeel::getDefaultLookAndFeel().isUsingNativeAlertWindows()) + return NativeMessageBox::showScopedAsync (options, std::move (callback)); + + return ScopedMessageBox::Pimpl::show (createAlertWindowImpl (options), std::move (callback)); } //============================================================================== diff --git a/modules/juce_gui_basics/windows/juce_AlertWindow.h b/modules/juce_gui_basics/windows/juce_AlertWindow.h index 6d26795b92..bcdffe83bc 100644 --- a/modules/juce_gui_basics/windows/juce_AlertWindow.h +++ b/modules/juce_gui_basics/windows/juce_AlertWindow.h @@ -413,6 +413,39 @@ public: ModalComponentManager::Callback* callback); #endif + /** Shows an alert window using the specified options. + + The box will be displayed and placed into a modal state, but this method will return + immediately, and the callback will be invoked later when the user dismisses the box. + + This function is always asynchronous, even if the callback is null. + + The result codes returned by the alert window are as follows. + - One button: + - button[0] returns 0 + - Two buttons: + - button[0] returns 1 + - button[1] returns 0 + - Three buttons: + - button[0] returns 1 + - button[1] returns 2 + - button[2] returns 0 + + @param options the options to use when creating the dialog. + @param callback if this is non-null, the callback will receive a call to its + modalStateFinished() when the box is dismissed with the index of the + button that was clicked as its argument. + The callback object will be owned and deleted by the system, so make sure + that it works safely and doesn't keep any references to objects that might + be deleted before it gets called. + @returns a ScopedMessageBox instance. The message box will remain visible for no + longer than the ScopedMessageBox remains alive. + + @see MessageBoxOptions + */ + [[nodiscard]] static ScopedMessageBox showScopedAsync (const MessageBoxOptions& options, + std::function callback); + //============================================================================== #if JUCE_MODAL_LOOPS_PERMITTED && ! defined (DOXYGEN) /** Shows an operating-system native dialog box. diff --git a/modules/juce_gui_basics/windows/juce_MessageBoxOptions.cpp b/modules/juce_gui_basics/windows/juce_MessageBoxOptions.cpp new file mode 100644 index 0000000000..4703d90498 --- /dev/null +++ b/modules/juce_gui_basics/windows/juce_MessageBoxOptions.cpp @@ -0,0 +1,93 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +MessageBoxOptions MessageBoxOptions::makeOptionsOk (MessageBoxIconType iconType, + const String& title, + const String& message, + const String& buttonText, + Component* associatedComponent) +{ + return MessageBoxOptions() + .withIconType (iconType) + .withTitle (title) + .withMessage (message) + .withButton (buttonText.isEmpty() ? TRANS ("OK") : buttonText) + .withAssociatedComponent (associatedComponent); +} + +MessageBoxOptions MessageBoxOptions::makeOptionsOkCancel (MessageBoxIconType iconType, + const String& title, + const String& message, + const String& button1Text, + const String& button2Text, + Component* associatedComponent) +{ + return MessageBoxOptions() + .withIconType (iconType) + .withTitle (title) + .withMessage (message) + .withButton (button1Text.isEmpty() ? TRANS ("OK") : button1Text) + .withButton (button2Text.isEmpty() ? TRANS ("Cancel") : button2Text) + .withAssociatedComponent (associatedComponent); +} + +MessageBoxOptions MessageBoxOptions::makeOptionsYesNo (MessageBoxIconType iconType, + const String& title, + const String& message, + const String& button1Text, + const String& button2Text, + Component* associatedComponent) +{ + return MessageBoxOptions() + .withIconType (iconType) + .withTitle (title) + .withMessage (message) + .withButton (button1Text.isEmpty() ? TRANS ("Yes") : button1Text) + .withButton (button2Text.isEmpty() ? TRANS ("No") : button2Text) + .withAssociatedComponent (associatedComponent); +} + +MessageBoxOptions MessageBoxOptions::makeOptionsYesNoCancel (MessageBoxIconType iconType, + const String& title, + const String& message, + const String& button1Text, + const String& button2Text, + const String& button3Text, + Component* associatedComponent) +{ + return MessageBoxOptions() + .withIconType (iconType) + .withTitle (title) + .withMessage (message) + .withButton (button1Text.isEmpty() ? TRANS ("Yes") : button1Text) + .withButton (button2Text.isEmpty() ? TRANS ("No") : button2Text) + .withButton (button3Text.isEmpty() ? TRANS ("Cancel") : button3Text) + .withAssociatedComponent (associatedComponent); +} + +} // namespace juce diff --git a/modules/juce_gui_basics/windows/juce_MessageBoxOptions.h b/modules/juce_gui_basics/windows/juce_MessageBoxOptions.h index b8d70abb96..35b9f461fe 100644 --- a/modules/juce_gui_basics/windows/juce_MessageBoxOptions.h +++ b/modules/juce_gui_basics/windows/juce_MessageBoxOptions.h @@ -124,6 +124,50 @@ public: */ Component* getAssociatedComponent() const noexcept { return associatedComponent; } + /** Creates options suitable for a message box with a single button. + + If no button text is supplied, "OK" will be used. + */ + static MessageBoxOptions makeOptionsOk (MessageBoxIconType iconType, + const String& title, + const String& message, + const String& buttonText = String(), + Component* associatedComponent = nullptr); + + /** Creates options suitable for a message box with two buttons. + + If no button text is supplied, "OK" and "Cancel" will be used. + */ + static MessageBoxOptions makeOptionsOkCancel (MessageBoxIconType iconType, + const String& title, + const String& message, + const String& button1Text = String(), + const String& button2Text = String(), + Component* associatedComponent = nullptr); + + /** Creates options suitable for a message box with two buttons. + + If no button text is supplied, "Yes" and "No" will be used. + */ + static MessageBoxOptions makeOptionsYesNo (MessageBoxIconType iconType, + const String& title, + const String& message, + const String& button1Text = String(), + const String& button2Text = String(), + Component* associatedComponent = nullptr); + + /** Creates options suitable for a message box with three buttons. + * + If no button text is supplied, "Yes", "No", and "Cancel" will be used. + */ + static MessageBoxOptions makeOptionsYesNoCancel (MessageBoxIconType iconType, + const String& title, + const String& message, + const String& button1Text = String(), + const String& button2Text = String(), + const String& button3Text = String(), + Component* associatedComponent = nullptr); + private: //============================================================================== template diff --git a/modules/juce_gui_basics/windows/juce_NativeMessageBox.cpp b/modules/juce_gui_basics/windows/juce_NativeMessageBox.cpp new file mode 100644 index 0000000000..6292d3a49b --- /dev/null +++ b/modules/juce_gui_basics/windows/juce_NativeMessageBox.cpp @@ -0,0 +1,127 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +template +static int showNativeBoxUnmanaged (const MessageBoxOptions& opts, ModalComponentManager::Callback* cb) +{ + return ScopedMessageBox::Pimpl::showUnmanaged (ScopedMessageBoxInterface::create (opts), cb); +} + +#if JUCE_MODAL_LOOPS_PERMITTED +void JUCE_CALLTYPE NativeMessageBox::showMessageBox (MessageBoxIconType iconType, + const String& title, const String& message, + Component* associatedComponent) +{ + showNativeBoxUnmanaged (MessageBoxOptions().withIconType (iconType) + .withTitle (title) + .withMessage (message) + .withButton (TRANS("OK")) + .withAssociatedComponent (associatedComponent), + nullptr); +} + +int JUCE_CALLTYPE NativeMessageBox::show (const MessageBoxOptions& options) +{ + return showNativeBoxUnmanaged<> (options, nullptr); +} +#endif + +void JUCE_CALLTYPE NativeMessageBox::showMessageBoxAsync (MessageBoxIconType iconType, + const String& title, const String& message, + Component* associatedComponent, + ModalComponentManager::Callback* callback) +{ + showNativeBoxUnmanaged (MessageBoxOptions().withIconType (iconType) + .withTitle (title) + .withMessage (message) + .withButton (TRANS("OK")) + .withAssociatedComponent (associatedComponent), + callback); +} + +bool JUCE_CALLTYPE NativeMessageBox::showOkCancelBox (MessageBoxIconType iconType, + const String& title, const String& message, + Component* associatedComponent, + ModalComponentManager::Callback* callback) +{ + return showNativeBoxUnmanaged (MessageBoxOptions().withIconType (iconType) + .withTitle (title) + .withMessage (message) + .withButton (TRANS("OK")) + .withButton (TRANS("Cancel")) + .withAssociatedComponent (associatedComponent), + callback) != 0; +} + +int JUCE_CALLTYPE NativeMessageBox::showYesNoCancelBox (MessageBoxIconType iconType, + const String& title, const String& message, + Component* associatedComponent, + ModalComponentManager::Callback* callback) +{ + return showNativeBoxUnmanaged (MessageBoxOptions().withIconType (iconType) + .withTitle (title) + .withMessage (message) + .withButton (TRANS("Yes")) + .withButton (TRANS("No")) + .withButton (TRANS("Cancel")) + .withAssociatedComponent (associatedComponent), + callback); +} + +int JUCE_CALLTYPE NativeMessageBox::showYesNoBox (MessageBoxIconType iconType, + const String& title, const String& message, + Component* associatedComponent, + ModalComponentManager::Callback* callback) +{ + return showNativeBoxUnmanaged (MessageBoxOptions().withIconType (iconType) + .withTitle (title) + .withMessage (message) + .withButton (TRANS("Yes")) + .withButton (TRANS("No")) + .withAssociatedComponent (associatedComponent), + callback); +} + +void JUCE_CALLTYPE NativeMessageBox::showAsync (const MessageBoxOptions& options, + ModalComponentManager::Callback* callback) +{ + showNativeBoxUnmanaged<> (options, callback); +} + +void JUCE_CALLTYPE NativeMessageBox::showAsync (const MessageBoxOptions& options, + std::function callback) +{ + showAsync (options, ModalCallbackFunction::create (callback)); +} + +ScopedMessageBox NativeMessageBox::showScopedAsync (const MessageBoxOptions& options, std::function callback) +{ + return ScopedMessageBox::Pimpl::show (ScopedMessageBoxInterface::create (options), std::move (callback)); +} + +} // namespace juce diff --git a/modules/juce_gui_basics/windows/juce_NativeMessageBox.h b/modules/juce_gui_basics/windows/juce_NativeMessageBox.h index 033d1a8bf3..f1236dafa5 100644 --- a/modules/juce_gui_basics/windows/juce_NativeMessageBox.h +++ b/modules/juce_gui_basics/windows/juce_NativeMessageBox.h @@ -257,6 +257,26 @@ public: ModalComponentManager::Callback* callback); #endif + /** Shows a dialog box using the specified options. + + The box will be displayed and placed into a modal state, but this method will return + immediately, and the callback will be invoked later when the user dismisses the box. + + @param options the options to use when creating the dialog. + @param callback if this is non-null, the callback will receive a call to its + modalStateFinished() when the box is dismissed with the index of the + button that was clicked as its argument. + The callback object will be owned and deleted by the system, so make sure + that it works safely and doesn't keep any references to objects that might + be deleted before it gets called. + @returns a ScopedMessageBox instance. The message box will remain visible for no + longer than the ScopedMessageBox remains alive. + + @see MessageBoxOptions + */ + [[nodiscard]] static ScopedMessageBox showScopedAsync (const MessageBoxOptions& options, + std::function callback); + private: NativeMessageBox() = delete; JUCE_DECLARE_NON_COPYABLE (NativeMessageBox) diff --git a/modules/juce_gui_basics/windows/juce_ScopedMessageBox.cpp b/modules/juce_gui_basics/windows/juce_ScopedMessageBox.cpp new file mode 100644 index 0000000000..6bddfe4837 --- /dev/null +++ b/modules/juce_gui_basics/windows/juce_ScopedMessageBox.cpp @@ -0,0 +1,181 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +/* + Instances of this type can show and dismiss a message box. + + This is an interface rather than a concrete type so that platforms can pick an implementation at + runtime if necessary. +*/ +struct ScopedMessageBoxInterface +{ + virtual ~ScopedMessageBoxInterface() = default; + + /* Shows the message box. + + When the message box exits normally, it should send the result to the passed-in function. + The passed-in function is safe to call from any thread at any time. + */ + virtual void runAsync (std::function) = 0; + + /* Shows the message box and blocks. */ + virtual int runSync() = 0; + + /* Forcefully closes the message box. + + This will be called when the message box handle has fallen out of scope. + If the message box has already been closed by the user, this shouldn't do anything. + */ + virtual void close() = 0; + + /* Implemented differently for each platform. */ + static std::unique_ptr create (const MessageBoxOptions& options); +}; + +//============================================================================== +class ScopedMessageBox::Pimpl : private AsyncUpdater +{ +public: + static ScopedMessageBox show (std::unique_ptr&& native, std::function callback) + { + return ScopedMessageBox (runAsync (std::move (native), rawToUniquePtr (ModalCallbackFunction::create (std::move (callback))))); + } + + static int showUnmanaged (std::unique_ptr&& native, + ModalComponentManager::Callback* cb) + { + #if JUCE_MODAL_LOOPS_PERMITTED + if (cb == nullptr) + return runSync (std::move (native)); + #endif + + runAsync (std::move (native), rawToUniquePtr (cb)); + + return 0; + } + + ~Pimpl() override + { + cancelPendingUpdate(); + } + + void close() + { + cancelPendingUpdate(); + nativeImplementation->close(); + self.reset(); + } + +private: + static std::shared_ptr runAsync (std::unique_ptr&& p, + std::unique_ptr&& c) + { + std::shared_ptr result (new Pimpl (std::move (p), std::move (c))); + result->self = result; + result->triggerAsyncUpdate(); + return result; + } + + static int runSync (std::unique_ptr&& p) + { + auto local = std::move (p); + return local != nullptr ? local->runSync() : 0; + } + + explicit Pimpl (std::unique_ptr&& p) + : Pimpl (std::move (p), nullptr) {} + + Pimpl (std::unique_ptr&& p, + std::unique_ptr&& c) + : callback (std::move (c)), nativeImplementation (std::move (p)) {} + + void handleAsyncUpdate() override + { + nativeImplementation->runAsync ([weakRecipient = std::weak_ptr (self)] (int result) + { + const auto notifyRecipient = [result, weakRecipient] + { + if (const auto locked = weakRecipient.lock()) + { + if (auto* cb = locked->callback.get()) + cb->modalStateFinished (result); + + locked->self.reset(); + } + }; + + if (MessageManager::getInstance()->isThisTheMessageThread()) + notifyRecipient(); + else + MessageManager::callAsync (notifyRecipient); + }); + } + + std::unique_ptr callback; + std::unique_ptr nativeImplementation; + + /* The 'old' native message box API doesn't have a concept of message box owners. + Instead, message boxes have to clean up after themselves, once they're done displaying. + To allow this mode of usage, the implementation keeps an owning reference to itself, + which is cleared once the message box is closed or asked to quit. To display a native + message box without a scoped lifetime, just create a Pimpl instance without using + the ScopedMessageBox wrapper, and the Pimpl will destroy itself after it is dismissed. + */ + std::shared_ptr self; +}; + +//============================================================================== +ScopedMessageBox::ScopedMessageBox() = default; + +ScopedMessageBox::ScopedMessageBox (std::shared_ptr p) + : pimpl (std::move (p)) {} + +ScopedMessageBox::~ScopedMessageBox() noexcept +{ + close(); +} + +ScopedMessageBox::ScopedMessageBox (ScopedMessageBox&& other) noexcept + : pimpl (std::exchange (other.pimpl, nullptr)) {} + +ScopedMessageBox& ScopedMessageBox::operator= (ScopedMessageBox&& other) noexcept +{ + ScopedMessageBox temp (std::move (other)); + std::swap (temp.pimpl, pimpl); + return *this; +} + +void ScopedMessageBox::close() +{ + if (pimpl != nullptr) + pimpl->close(); + + pimpl.reset(); +} + +} // namespace juce diff --git a/modules/juce_gui_basics/windows/juce_ScopedMessageBox.h b/modules/juce_gui_basics/windows/juce_ScopedMessageBox.h new file mode 100644 index 0000000000..841643312a --- /dev/null +++ b/modules/juce_gui_basics/windows/juce_ScopedMessageBox.h @@ -0,0 +1,69 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2022 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 7 End-User License + Agreement and JUCE Privacy Policy. + + End User License Agreement: www.juce.com/juce-7-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +/** + Objects of this type can be used to programmatically close message boxes. + + @see NativeMessageBox::showScopedAsync(), AlertWindow::showScopedAsync() +*/ +class ScopedMessageBox +{ +public: + /** Constructor */ + ScopedMessageBox(); + + /** Destructor */ + ~ScopedMessageBox() noexcept; + + /** Move constructor */ + ScopedMessageBox (ScopedMessageBox&&) noexcept; + + /** Move assignment operator */ + ScopedMessageBox& operator= (ScopedMessageBox&&) noexcept; + + /** Closes the message box, if it is currently showing. + + This is also called automatically during ~ScopedMessageBox. This is useful if you want + to display a message corresponding to a particular view, and hide the message automatically + when the view is hidden. This situation commonly arises when displaying messages in plugin + editors. + */ + void close(); + + /** @internal */ + class Pimpl; + +private: + explicit ScopedMessageBox (std::shared_ptr); + + std::shared_ptr pimpl; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ScopedMessageBox) +}; + +} // namespace juce