diff options
-rw-r--r-- | src/plugins/platforms/cocoa/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/plugins/platforms/cocoa/qcocoahelpers.h | 1 | ||||
-rw-r--r-- | src/plugins/platforms/cocoa/qcocoahelpers.mm | 1 | ||||
-rw-r--r-- | src/plugins/platforms/cocoa/qcocoamessagedialog.h | 37 | ||||
-rw-r--r-- | src/plugins/platforms/cocoa/qcocoamessagedialog.mm | 304 | ||||
-rw-r--r-- | src/plugins/platforms/cocoa/qcocoatheme.mm | 17 | ||||
-rw-r--r-- | src/plugins/platforms/cocoa/qnsview.h | 2 | ||||
-rw-r--r-- | src/plugins/platforms/cocoa/qnswindow.h | 3 | ||||
-rw-r--r-- | tests/auto/widgets/dialogs/qmessagebox/tst_qmessagebox.cpp | 35 |
9 files changed, 395 insertions, 6 deletions
diff --git a/src/plugins/platforms/cocoa/CMakeLists.txt b/src/plugins/platforms/cocoa/CMakeLists.txt index c5f71316da..cc4722cb3e 100644 --- a/src/plugins/platforms/cocoa/CMakeLists.txt +++ b/src/plugins/platforms/cocoa/CMakeLists.txt @@ -48,6 +48,7 @@ qt_internal_add_plugin(QCocoaIntegrationPlugin qcocoacolordialoghelper.h qcocoacolordialoghelper.mm qcocoafiledialoghelper.h qcocoafiledialoghelper.mm qcocoafontdialoghelper.h qcocoafontdialoghelper.mm + qcocoamessagedialog.h qcocoamessagedialog.mm DEFINES QT_NO_FOREACH LIBRARIES diff --git a/src/plugins/platforms/cocoa/qcocoahelpers.h b/src/plugins/platforms/cocoa/qcocoahelpers.h index 1964dcf59d..369f752dc9 100644 --- a/src/plugins/platforms/cocoa/qcocoahelpers.h +++ b/src/plugins/platforms/cocoa/qcocoahelpers.h @@ -41,6 +41,7 @@ Q_DECLARE_LOGGING_CATEGORY(lcQpaScreen) Q_DECLARE_LOGGING_CATEGORY(lcQpaApplication) Q_DECLARE_LOGGING_CATEGORY(lcQpaClipboard) Q_DECLARE_LOGGING_CATEGORY(lcInputDevices) +Q_DECLARE_LOGGING_CATEGORY(lcQpaDialogs) class QPixmap; class QString; diff --git a/src/plugins/platforms/cocoa/qcocoahelpers.mm b/src/plugins/platforms/cocoa/qcocoahelpers.mm index 0a6a2a7d04..0810324784 100644 --- a/src/plugins/platforms/cocoa/qcocoahelpers.mm +++ b/src/plugins/platforms/cocoa/qcocoahelpers.mm @@ -28,6 +28,7 @@ Q_LOGGING_CATEGORY(lcQpaScreen, "qt.qpa.screen", QtCriticalMsg); Q_LOGGING_CATEGORY(lcQpaApplication, "qt.qpa.application"); Q_LOGGING_CATEGORY(lcQpaClipboard, "qt.qpa.clipboard") Q_LOGGING_CATEGORY(lcInputDevices, "qt.qpa.input.devices") +Q_LOGGING_CATEGORY(lcQpaDialogs, "qt.qpa.dialogs") // // Conversion Functions diff --git a/src/plugins/platforms/cocoa/qcocoamessagedialog.h b/src/plugins/platforms/cocoa/qcocoamessagedialog.h new file mode 100644 index 0000000000..564dd915c5 --- /dev/null +++ b/src/plugins/platforms/cocoa/qcocoamessagedialog.h @@ -0,0 +1,37 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QCOCOAMESSAGEDIALOG_H +#define QCOCOAMESSAGEDIALOG_H + +#include <qpa/qplatformdialoghelper.h> + +Q_FORWARD_DECLARE_OBJC_CLASS(NSAlert); +typedef long NSInteger; +typedef NSInteger NSModalResponse; + +QT_BEGIN_NAMESPACE + +class QEventLoop; + +class QCocoaMessageDialog : public QPlatformMessageDialogHelper +{ +public: + QCocoaMessageDialog() = default; + ~QCocoaMessageDialog(); + + void exec() override; + bool show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent) override; + void hide() override; + +private: + Qt::WindowModality modality() const; + NSAlert *m_alert = nullptr; + QEventLoop *m_eventLoop = nullptr; + void processResponse(NSModalResponse response); +}; + +QT_END_NAMESPACE + +#endif // QCOCOAMESSAGEDIALOG_H + diff --git a/src/plugins/platforms/cocoa/qcocoamessagedialog.mm b/src/plugins/platforms/cocoa/qcocoamessagedialog.mm new file mode 100644 index 0000000000..c21fbc2ce4 --- /dev/null +++ b/src/plugins/platforms/cocoa/qcocoamessagedialog.mm @@ -0,0 +1,304 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qcocoamessagedialog.h" + +#include "qcocoawindow.h" +#include "qcocoahelpers.h" + +#include <QtCore/qmetaobject.h> +#include <QtCore/qscopedvaluerollback.h> +#include <QtCore/qtimer.h> + +#include <QtGui/qtextdocument.h> +#include <QtGui/private/qguiapplication_p.h> +#include <QtGui/qpa/qplatformtheme.h> + +#include <AppKit/NSAlert.h> +#include <AppKit/NSButton.h> + +QT_USE_NAMESPACE + +using namespace Qt::StringLiterals; + +QT_BEGIN_NAMESPACE + +QCocoaMessageDialog::~QCocoaMessageDialog() +{ + hide(); + [m_alert release]; +} + +static QString toPlainText(const QString &text) +{ + // FIXME: QMessageDialog supports Qt::TextFormat, which + // nowadays includes Qt::MarkdownText, but we don't have + // the machinery to deal with that yet. We should as a + // start plumb the dialog's text format to the platform + // via the dialog options. + + if (!Qt::mightBeRichText(text)) + return text; + + QTextDocument textDocument; + textDocument.setHtml(text); + return textDocument.toPlainText(); +} + +/* + Called from QDialogPrivate::setNativeDialogVisible() when the message box + is ready to be shown. + + At this point the options() will reflect the specific dialog shown. + + Returns true if the helper could successfully show the dialog, or + false if the cross platform fallback dialog should be used instead. +*/ +bool QCocoaMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent) +{ + Q_UNUSED(windowFlags); + + qCDebug(lcQpaDialogs) << "Asked to show" << windowModality << "dialog with parent" << parent; + + if (m_alert.window.visible) { + qCDebug(lcQpaDialogs) << "Dialog already visible, ignoring request to show"; + return true; // But we don't want to show the fallback dialog instead + } + + // We can only do application and window modal dialogs + if (windowModality == Qt::NonModal) + return false; + + // And only window modal if we have a parent + if (windowModality == Qt::WindowModal && (!parent || !parent->handle())) { + qCWarning(lcQpaDialogs, "Cannot run window modal dialog without parent window"); + return false; + } + + // And without options we don't know what to show + if (!options()) + return false; + + Q_ASSERT(!m_alert); + m_alert = [NSAlert new]; + m_alert.window.title = options()->windowTitle().toNSString(); + + QString text = toPlainText(options()->text()); + QString details = toPlainText(options()->detailedText()); + if (!details.isEmpty()) + text += u"\n\n"_s + details; + m_alert.messageText = text.toNSString(); + m_alert.informativeText = toPlainText(options()->informativeText()).toNSString(); + + switch (options()->icon()) { + case QMessageDialogOptions::NoIcon: + case QMessageDialogOptions::Information: + case QMessageDialogOptions::Question: + [m_alert setAlertStyle:NSAlertStyleInformational]; + break; + case QMessageDialogOptions::Warning: + [m_alert setAlertStyle:NSAlertStyleWarning]; + break; + case QMessageDialogOptions::Critical: + [m_alert setAlertStyle:NSAlertStyleCritical]; + break; + } + + // FIXME: Propagate iconPixmap through dialog options + + bool defaultButtonAdded = false; + bool cancelButtonAdded = false; + + const auto addButton = [&](auto title, auto tag, auto role) { + title = QPlatformTheme::removeMnemonics(title); + NSButton *button = [m_alert addButtonWithTitle:title.toNSString()]; + + // Calling addButtonWithTitle places buttons starting at the right side/top of the alert + // and going toward the left/bottom. By default, the first button has a key equivalent of + // Return, any button with a title of "Cancel" has a key equivalent of Escape, and any button + // with the title "Don't Save" has a key equivalent of Command-D (but only if it's not the first + // button). Unfortunately QMessageBox does not currently plumb setDefaultButton/setEscapeButton + // through the dialog options, so we can't forward this information directly. The closest we + // can get right now is to use the role to set the button's key equivalent. + + if (role == AcceptRole && !defaultButtonAdded) { + button.keyEquivalent = @"\r"; + defaultButtonAdded = true; + } else if (role == RejectRole && !cancelButtonAdded) { + button.keyEquivalent = @"\e"; + cancelButtonAdded = true; + } + + if (@available(macOS 11, *)) + button.hasDestructiveAction = role == DestructiveRole; + + // The NSModalResponse of showing an NSAlert normally depends on the order of the + // button that was clicked, starting from the right with NSAlertFirstButtonReturn (1000), + // NSAlertSecondButtonReturn (1001), NSAlertThirdButtonReturn (1002), and after that + // NSAlertThirdButtonReturn + n. The response can also be customized per button via its + // tag, which, following the above logic, can include any positive value from 1000 and up. + // In addition the system reserves the values from -1000 and down for its own modal responses, + // such as NSModalResponseStop, NSModalResponseAbort, and NSModalResponseContinue. + // Luckily for us, the QPlatformDialogHelper::StandardButton enum values all fall within + // the positive range, so we can use the standard button value as the tag directly. + // The same applies to the custom button IDs, as these are generated in sequence after + // the QPlatformDialogHelper::LastButton. + Q_ASSERT(tag >= NSAlertFirstButtonReturn); + button.tag = tag; + }; + + const auto *platformTheme = QGuiApplicationPrivate::platformTheme(); + if (auto standardButtons = options()->standardButtons()) { + for (int standardButton = FirstButton; standardButton < LastButton; standardButton <<= 1) { + if (standardButtons & standardButton) { + auto title = platformTheme->standardButtonText(standardButton); + addButton(title, standardButton, buttonRole(StandardButton(standardButton))); + } + } + } + + const auto customButtons = options()->customButtons(); + for (auto customButton : customButtons) + addButton(customButton.label, customButton.id, customButton.role); + + + // QMessageDialog's logic for adding a fallback OK button if no other buttons + // are added depends on QMessageBox::showEvent(), which is too late when + // native dialogs are in use. To ensure there's always an OK button with a tag + // we recognize we add it explicitly here as a fallback. + if (!m_alert.buttons.count) { + addButton(platformTheme->standardButtonText(StandardButton::Ok), + StandardButton::Ok, ButtonRole::AcceptRole); + } + + qCDebug(lcQpaDialogs) << "Showing" << m_alert; + + if (windowModality == Qt::WindowModal) { + auto *cocoaWindow = static_cast<QCocoaWindow*>(parent->handle()); + [m_alert beginSheetModalForWindow:cocoaWindow->nativeWindow() + completionHandler:^(NSModalResponse response) { + processResponse(response); + } + ]; + } else { + // The dialog is application modal, so we need to call runModal, + // but we can't call it here as the nativeDialogInUse state of QDialog + // depends on the result of show(), and we can't rely on doing it + // in exec(), as we can't guarantee that the user will call exec() + // after showing the dialog. As a workaround, we call it from exec(), + // but also make sure that if the user returns to the main runloop + // we'll run the modal dialog from there. + QTimer::singleShot(0, this, [this]{ + if (m_alert && NSApp.modalWindow != m_alert.window) { + qCDebug(lcQpaDialogs) << "Running deferred modal" << m_alert; + processResponse([m_alert runModal]); + } + }); + } + + return true; +} + +void QCocoaMessageDialog::exec() +{ + Q_ASSERT(m_alert); + + if (modality() == Qt::WindowModal) { + qCDebug(lcQpaDialogs) << "Running local event loop for window modal" << m_alert; + QEventLoop eventLoop; + QScopedValueRollback updateGuard(m_eventLoop, &eventLoop); + m_eventLoop->exec(QEventLoop::DialogExec); + } else { + qCDebug(lcQpaDialogs) << "Running modal" << m_alert; + processResponse([m_alert runModal]); + } +} + +// Custom modal response code to record that the dialog was hidden by us +static const NSInteger kModalResponseDialogHidden = NSAlertThirdButtonReturn + 1; + +void QCocoaMessageDialog::processResponse(NSModalResponse response) +{ + qCDebug(lcQpaDialogs) << "Processing response" << response << "for" << m_alert; + + if (response >= NSAlertFirstButtonReturn) { + // Safe range for user-defined modal responses + if (response == kModalResponseDialogHidden) { + // Dialog was explicitly hidden by us, so nothing to report + qCDebug(lcQpaDialogs) << "Dialog was hidden; ignoring response"; + } else { + // Dialog buttons + if (response <= StandardButton::LastButton) { + Q_ASSERT(response >= StandardButton::FirstButton); + auto standardButton = StandardButton(response); + emit clicked(standardButton, buttonRole(standardButton)); + } else { + auto *customButton = options()->customButton(response); + Q_ASSERT(customButton); + emit clicked(StandardButton(customButton->id), customButton->role); + } + } + } else { + // We have to consider NSModalResponses beyond the ones specific to + // the alert buttons as the alert may be canceled programmatically. + + switch (response) { + case NSModalResponseContinue: + // Modal session is continuing (returned by runModalSession: only) + Q_UNREACHABLE(); + case NSModalResponseOK: + emit accept(); + break; + case NSModalResponseCancel: + case NSModalResponseStop: // Modal session was broken with stopModal + case NSModalResponseAbort: // Modal session was broken with abortModal + emit reject(); + break; + default: + qCWarning(lcQpaDialogs) << "Unrecognized modal response" << response; + } + } + + if (m_eventLoop) + m_eventLoop->exit(response); + + // We can't re-use the same dialog for the next show() anyways, + // since the options may have changed, so get rid of it now. + [m_alert release]; + m_alert = nil; +} + +void QCocoaMessageDialog::hide() +{ + if (!m_alert) + return; + + if (m_alert.window.visible) { + qCDebug(lcQpaDialogs) << "Hiding" << modality() << m_alert; + + // Note: Just hiding or closing the NSAlert's NWindow here is not sufficient, + // as the dialog is running a modal event loop as well, which we need to end. + + if (modality() == Qt::WindowModal) { + // Will call processResponse() synchronously + [m_alert.window.sheetParent endSheet:m_alert.window returnCode:kModalResponseDialogHidden]; + } else { + if (NSApp.modalWindow == m_alert.window) { + // Will call processResponse() asynchronously + [NSApp stopModalWithCode:kModalResponseDialogHidden]; + } else { + qCWarning(lcQpaDialogs, "Dialog is not top level modal window. Cannot hide."); + } + } + } else { + qCDebug(lcQpaDialogs) << "No need to hide already hidden" << m_alert; + } +} + +Qt::WindowModality QCocoaMessageDialog::modality() const +{ + Q_ASSERT(m_alert && m_alert.window); + return m_alert.window.sheetParent ? Qt::WindowModal : Qt::ApplicationModal; +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qcocoatheme.mm b/src/plugins/platforms/cocoa/qcocoatheme.mm index 1d3fc2c1b1..69823b409b 100644 --- a/src/plugins/platforms/cocoa/qcocoatheme.mm +++ b/src/plugins/platforms/cocoa/qcocoatheme.mm @@ -31,6 +31,7 @@ #include "qcocoacolordialoghelper.h" #include "qcocoafiledialoghelper.h" #include "qcocoafontdialoghelper.h" +#include "qcocoamessagedialog.h" #include <CoreServices/CoreServices.h> @@ -251,13 +252,15 @@ void QCocoaTheme::handleSystemThemeChange() bool QCocoaTheme::usePlatformNativeDialog(DialogType dialogType) const { - if (dialogType == QPlatformTheme::FileDialog) - return true; - if (dialogType == QPlatformTheme::ColorDialog) - return true; - if (dialogType == QPlatformTheme::FontDialog) + switch (dialogType) { + case QPlatformTheme::FileDialog: + case QPlatformTheme::ColorDialog: + case QPlatformTheme::FontDialog: + case QPlatformTheme::MessageDialog: return true; - return false; + default: + return false; + } } QPlatformDialogHelper *QCocoaTheme::createPlatformDialogHelper(DialogType dialogType) const @@ -269,6 +272,8 @@ QPlatformDialogHelper *QCocoaTheme::createPlatformDialogHelper(DialogType dialog return new QCocoaColorDialogHelper(); case QPlatformTheme::FontDialog: return new QCocoaFontDialogHelper(); + case QPlatformTheme::MessageDialog: + return new QCocoaMessageDialog; default: return nullptr; } diff --git a/src/plugins/platforms/cocoa/qnsview.h b/src/plugins/platforms/cocoa/qnsview.h index 1858c2660d..e41f5a7296 100644 --- a/src/plugins/platforms/cocoa/qnsview.h +++ b/src/plugins/platforms/cocoa/qnsview.h @@ -4,6 +4,8 @@ #ifndef QNSVIEW_H #define QNSVIEW_H +#include <AppKit/NSView.h> + #include <QtCore/private/qcore_mac_p.h> QT_BEGIN_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qnswindow.h b/src/plugins/platforms/cocoa/qnswindow.h index ae43f28e8e..f69e809133 100644 --- a/src/plugins/platforms/cocoa/qnswindow.h +++ b/src/plugins/platforms/cocoa/qnswindow.h @@ -8,6 +8,9 @@ #include <QPointer> #include <QtCore/private/qcore_mac_p.h> +#include <AppKit/NSWindow.h> +#include <AppKit/NSPanel.h> + QT_FORWARD_DECLARE_CLASS(QCocoaWindow) #if defined(__OBJC__) diff --git a/tests/auto/widgets/dialogs/qmessagebox/tst_qmessagebox.cpp b/tests/auto/widgets/dialogs/qmessagebox/tst_qmessagebox.cpp index 495a935255..808cd41b28 100644 --- a/tests/auto/widgets/dialogs/qmessagebox/tst_qmessagebox.cpp +++ b/tests/auto/widgets/dialogs/qmessagebox/tst_qmessagebox.cpp @@ -23,6 +23,9 @@ class tst_QMessageBox : public QObject Q_OBJECT private slots: + void initTestCase_data(); + void init(); + void sanityTest(); void defaultButton(); void escapeButton(); @@ -129,6 +132,22 @@ void ExecCloseHelper::timerEvent(QTimerEvent *te) } } +void tst_QMessageBox::initTestCase_data() +{ + QTest::addColumn<bool>("useNativeDialog"); + QTest::newRow("widget") << false; + if (const QPlatformTheme *theme = QGuiApplicationPrivate::platformTheme()) { + if (theme->usePlatformNativeDialog(QPlatformTheme::MessageDialog)) + QTest::newRow("native") << true; + } +} + +void tst_QMessageBox::init() +{ + QFETCH_GLOBAL(bool, useNativeDialog); + qApp->setAttribute(Qt::AA_DontUseNativeDialogs, !useNativeDialog); +} + void tst_QMessageBox::cleanup() { QTRY_VERIFY(QApplication::topLevelWidgets().isEmpty()); // OS X requires TRY @@ -484,6 +503,10 @@ void tst_QMessageBox::instanceSourceCompat() void tst_QMessageBox::detailsText() { + QFETCH_GLOBAL(bool, useNativeDialog); + if (useNativeDialog) + QSKIP("Native dialogs do not propagate expose events"); + QMessageBox box; QString text("This is the details text."); box.setDetailedText(text); @@ -497,6 +520,10 @@ void tst_QMessageBox::detailsText() void tst_QMessageBox::detailsButtonText() { + QFETCH_GLOBAL(bool, useNativeDialog); + if (useNativeDialog) + QSKIP("Native dialogs do not propagate expose events"); + QMessageBox box; box.setDetailedText("bla"); box.open(); @@ -518,6 +545,10 @@ void tst_QMessageBox::detailsButtonText() void tst_QMessageBox::expandDetailsWithoutMoving() // QTBUG-32473 { + QFETCH_GLOBAL(bool, useNativeDialog); + if (useNativeDialog) + QSKIP("Native dialogs do not propagate expose events"); + tst_ResizingMessageBox box; box.setDetailedText("bla"); box.show(); @@ -617,6 +648,10 @@ Q_DECLARE_METATYPE(RoleSet); void tst_QMessageBox::acceptedRejectedSignals() { + QFETCH_GLOBAL(bool, useNativeDialog); + if (useNativeDialog) + QSKIP("Native dialogs do not propagate expose events"); + QMessageBox messageBox(QMessageBox::Information, "Test window", "Test text"); QFETCH(ButtonsCreator, buttonsCreator); |