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