diff options
Diffstat (limited to 'src/plugins/platforms/cocoa/qcocoamessagedialog.mm')
-rw-r--r-- | src/plugins/platforms/cocoa/qcocoamessagedialog.mm | 132 |
1 files changed, 102 insertions, 30 deletions
diff --git a/src/plugins/platforms/cocoa/qcocoamessagedialog.mm b/src/plugins/platforms/cocoa/qcocoamessagedialog.mm index e058450ebd..f786fc65c4 100644 --- a/src/plugins/platforms/cocoa/qcocoamessagedialog.mm +++ b/src/plugins/platforms/cocoa/qcocoamessagedialog.mm @@ -91,15 +91,25 @@ bool QCocoaMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality w 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(); - QString text = toPlainText(options()->text()); - QString details = toPlainText(options()->detailedText()); - if (!details.isEmpty()) - text += u"\n\n"_s + details; + const QString text = toPlainText(options()->text()); m_alert.messageText = text.toNSString(); m_alert.informativeText = toPlainText(options()->informativeText()).toNSString(); @@ -127,8 +137,8 @@ bool QCocoaMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality w break; } - bool defaultButtonAdded = false; - bool cancelButtonAdded = false; + auto defaultButton = options()->defaultButton(); + auto escapeButton = options()->escapeButton(); const auto addButton = [&](auto title, auto tag, auto role) { title = QPlatformTheme::removeMnemonics(title); @@ -138,20 +148,29 @@ bool QCocoaMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality w // 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. + // 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 (role == AcceptRole && !defaultButtonAdded) { + if (!defaultButton && role == AcceptRole) + defaultButton = tag; + + if (tag == defaultButton) button.keyEquivalent = @"\r"; - defaultButtonAdded = true; - } else if (role == RejectRole && !cancelButtonAdded) { + 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"; - cancelButtonAdded = true; - } + else if ([button.keyEquivalent isEqualToString:@"\e"]) + button.keyEquivalent = @""; - if (@available(macOS 11, *)) - button.hasDestructiveAction = role == DestructiveRole; + 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), @@ -168,30 +187,69 @@ bool QCocoaMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality w button.tag = tag; }; + // Resolve all dialog buttons from the options, both standard and custom + + struct Button { QString title; int identifier; ButtonRole role; }; + std::vector<Button> buttons; + const auto *platformTheme = QGuiApplicationPrivate::platformTheme(); if (auto standardButtons = options()->standardButtons()) { - for (int standardButton = FirstButton; standardButton < LastButton; standardButton <<= 1) { + for (int standardButton = FirstButton; standardButton <= LastButton; standardButton <<= 1) { if (standardButtons & standardButton) { auto title = platformTheme->standardButtonText(standardButton); - addButton(title, standardButton, buttonRole(StandardButton(standardButton))); + buttons.push_back({ + title, standardButton, buttonRole(StandardButton(standardButton)) + }); } } } - const auto customButtons = options()->customButtons(); for (auto customButton : customButtons) - addButton(customButton.label, customButton.id, customButton.role); + buttons.push_back({customButton.label, customButton.id, customButton.role}); + + // Sort them according to the QPlatformDialogHelper::ButtonLayout for macOS + + // The ButtonLayout adds one additional role, AlternateRole, which is used + // for any AcceptRole beyond the first one, and should be ordered before the + // AcceptRole. Set this up by fixing the roles up front. + bool seenAccept = false; + for (auto &button : buttons) { + if (button.role == AcceptRole) { + if (!seenAccept) + seenAccept = true; + else + button.role = AlternateRole; + } + } + + std::vector<Button> orderedButtons; + const int *layoutEntry = buttonLayout(Qt::Horizontal, ButtonLayout::MacLayout); + while (*layoutEntry != QPlatformDialogHelper::EOL) { + const auto role = ButtonRole(*layoutEntry & ~ButtonRole::Reverse); + const bool reverse = *layoutEntry & ButtonRole::Reverse; + + auto addButton = [&](const Button &button) { + if (button.role == role) + orderedButtons.push_back(button); + }; + if (reverse) + std::for_each(std::crbegin(buttons), std::crend(buttons), addButton); + else + std::for_each(std::cbegin(buttons), std::cend(buttons), addButton); - // 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); + ++layoutEntry; } + // Add them to the alert in reverse order, since buttons are added right to left + for (auto button = orderedButtons.crbegin(); button != orderedButtons.crend(); ++button) + addButton(button->title, button->identifier, button->role); + + // If we didn't find a an explicit or implicit default button above + // we restore the AppKit behavior of making the first button default. + if (!defaultButton) + m_alert.buttons.firstObject.keyEquivalent = @"\r"; + if (auto checkBoxLabel = options()->checkBoxLabel(); !checkBoxLabel.isNull()) { checkBoxLabel = QPlatformTheme::removeMnemonics(checkBoxLabel); m_alert.suppressionButton.title = checkBoxLabel.toNSString(); @@ -219,10 +277,10 @@ bool QCocoaMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality w // 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) { + if (m_alert && !m_alert.window.visible) { qCDebug(lcQpaDialogs) << "Running deferred modal" << m_alert; QCocoaEventDispatcher::clearCurrentThreadCocoaEventDispatcherInterruptFlag(); - processResponse([m_alert runModal]); + processResponse(runModal()); } }); } @@ -230,6 +288,20 @@ bool QCocoaMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality w return true; } +// We shouldn't get NSModalResponseContinue as a response from NSAlert::runModal, +// and processResponse must not be called with that value (if we are there, it's +// too late to do anything about it. +// However, as QTBUG-114546 shows, there are scenarios where we might get that +// response anyway. We interpret it to keep the modal loop running, and we only +// return if we got something else to pass to processResponse. +NSModalResponse QCocoaMessageDialog::runModal() const +{ + NSModalResponse response = NSModalResponseContinue; + while (response == NSModalResponseContinue) + response = [m_alert runModal]; + return response; +} + void QCocoaMessageDialog::exec() { Q_ASSERT(m_alert); @@ -242,7 +314,7 @@ void QCocoaMessageDialog::exec() } else { qCDebug(lcQpaDialogs) << "Running modal" << m_alert; QCocoaEventDispatcher::clearCurrentThreadCocoaEventDispatcherInterruptFlag(); - processResponse([m_alert runModal]); + processResponse(runModal()); } } |