// 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 "qcocoaeventdispatcher.h" #include #include #include #include #include #include #include #include #include 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(); } static NSControlStateValue controlStateFor(Qt::CheckState state) { switch (state) { case Qt::Checked: return NSControlStateValueOn; case Qt::Unchecked: return NSControlStateValueOff; case Qt::PartiallyChecked: return NSControlStateValueMixed; } Q_UNREACHABLE(); } /* 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; // NSAlert doesn't have a section for detailed text if (!options()->detailedText().isEmpty()) { qCWarning(lcQpaDialogs, "Message box contains detailed text"); return false; } if (Qt::mightBeRichText(options()->text()) || Qt::mightBeRichText(options()->informativeText())) { // Let's fallback to non-native message box, // we only have plain NSString/text in NSAlert. qCDebug(lcQpaDialogs, "Message box contains text in rich text format"); return false; } Q_ASSERT(!m_alert); m_alert = [NSAlert new]; m_alert.window.title = options()->windowTitle().toNSString(); const QString text = toPlainText(options()->text()); m_alert.messageText = text.toNSString(); m_alert.informativeText = toPlainText(options()->informativeText()).toNSString(); switch (options()->standardIcon()) { case QMessageDialogOptions::NoIcon: { // We only reflect the pixmap icon if the standard icon is unset, // as setting a standard icon will also set a corresponding pixmap // icon, which we don't want since it conflicts with the platform. // If the user has set an explicit pixmap icon however, the standard // icon will be NoIcon, so we're good. QPixmap iconPixmap = options()->iconPixmap(); if (!iconPixmap.isNull()) m_alert.icon = [NSImage imageFromQImage:iconPixmap.toImage()]; break; } 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; } auto defaultButton = options()->defaultButton(); auto escapeButton = options()->escapeButton(); 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). If an explicit default or escape button has been set, we respect these, // and otherwise we fall back to role-based default and escape buttons. qCDebug(lcQpaDialogs).verbosity(0) << "Adding button" << title << "with" << role; if (!defaultButton && role == AcceptRole) defaultButton = tag; if (tag == defaultButton) button.keyEquivalent = @"\r"; else if ([button.keyEquivalent isEqualToString:@"\r"]) button.keyEquivalent = @""; if (!escapeButton && role == RejectRole) escapeButton = tag; // Don't override default button with escape button, to match AppKit default if (tag == escapeButton && ![button.keyEquivalent isEqualToString:@"\r"]) button.keyEquivalent = @"\e"; else if ([button.keyEquivalent isEqualToString:@"\e"]) button.keyEquivalent = @""; 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; }; // Resolve all dialog buttons from the options, both standard and custom struct Button { QString title; int identifier; ButtonRole role; }; std::vector