macOS: Detect menu bar roles for untranslated menu titles as well

If an application added an "About" menu action, and then loaded
the qtbase translations, we would try to match the incoming menu
title ("About") against QCoreApplication::translate("QCocoaMenuItem",
"About"), which since qtbase provides translation in the QCocoaMenuItem
context would fail, and we would not detect the menu item role
successfully.

For this to work, the application developer would need to add their
own translations for every menu item, and these translations would
have to match the QCocoaMenuItem translations in qtbase.

By also comparing the menu titles against the untranslated heuristics
we now account for the situation that the app has not translated
its menu items.

If the app does add translations of its menu items these still need
to match the QCocoaMenuItem context translations.

Change-Id: Ic2f019cd42b7e080187f9738840f84b0cec239df
Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
This commit is contained in:
Tor Arne Vestbø 2023-04-27 15:24:43 +02:00
parent c33427240f
commit 842dbca617
2 changed files with 71 additions and 36 deletions

View File

@ -180,38 +180,71 @@ void QCocoaMenuItem::setNativeContents(WId item)
m_itemView.needsDisplay = YES;
}
static QPlatformMenuItem::MenuRole detectMenuRole(const QString &caption)
static QPlatformMenuItem::MenuRole detectMenuRole(const QString &captionWithPossibleMnemonic)
{
QString captionNoAmpersand(caption);
captionNoAmpersand.remove(u'&');
const QString aboutString = QCoreApplication::translate("QCocoaMenuItem", "About");
if (captionNoAmpersand.startsWith(aboutString, Qt::CaseInsensitive)
|| captionNoAmpersand.endsWith(aboutString, Qt::CaseInsensitive)) {
QString itemCaption(captionWithPossibleMnemonic);
itemCaption.remove(u'&');
static const std::tuple<QPlatformMenuItem::MenuRole, std::vector<std::tuple<Qt::MatchFlags, const char *>>> roleMap[] = {
{ QPlatformMenuItem::AboutRole, {
{ Qt::MatchStartsWith | Qt::MatchEndsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "About") }
}},
{ QPlatformMenuItem::PreferencesRole, {
{ Qt::MatchStartsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Config") },
{ Qt::MatchStartsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Preference") },
{ Qt::MatchStartsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Options") },
{ Qt::MatchStartsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Setting") },
{ Qt::MatchStartsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Setup") },
}},
{ QPlatformMenuItem::QuitRole, {
{ Qt::MatchStartsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Quit") },
{ Qt::MatchStartsWith, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Exit") },
}},
{ QPlatformMenuItem::CutRole, {
{ Qt::MatchExactly, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Cut") }
}},
{ QPlatformMenuItem::CopyRole, {
{ Qt::MatchExactly, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Copy") }
}},
{ QPlatformMenuItem::PasteRole, {
{ Qt::MatchExactly, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Paste") }
}},
{ QPlatformMenuItem::SelectAllRole, {
{ Qt::MatchExactly, QT_TRANSLATE_NOOP("QCocoaMenuItem", "Select All") }
}},
};
auto match = [](const QString &caption, const QString &itemCaption, Qt::MatchFlags matchFlags) {
if (matchFlags.testFlag(Qt::MatchExactly))
return !itemCaption.compare(caption, Qt::CaseInsensitive);
if (matchFlags.testFlag(Qt::MatchStartsWith) && itemCaption.startsWith(caption, Qt::CaseInsensitive))
return true;
if (matchFlags.testFlag(Qt::MatchEndsWith) && itemCaption.endsWith(caption, Qt::CaseInsensitive))
return true;
return false;
};
QPlatformMenuItem::MenuRole detectedRole = [&]{
for (const auto &[role, captions] : roleMap) {
for (const auto &[matchFlags, caption] : captions) {
// Check for untranslated match
if (match(caption, itemCaption, matchFlags))
return role;
// Then translated with the current Qt translation
if (match(QCoreApplication::translate("QCocoaMenuItem", caption), itemCaption, matchFlags))
return role;
}
}
return QPlatformMenuItem::NoRole;
}();
if (detectedRole == QPlatformMenuItem::AboutRole) {
static const QRegularExpression qtRegExp("qt$"_L1, QRegularExpression::CaseInsensitiveOption);
if (captionNoAmpersand.contains(qtRegExp))
return QPlatformMenuItem::AboutQtRole;
return QPlatformMenuItem::AboutRole;
if (itemCaption.contains(qtRegExp))
detectedRole = QPlatformMenuItem::AboutQtRole;
}
if (captionNoAmpersand.startsWith(QCoreApplication::translate("QCocoaMenuItem", "Config"), Qt::CaseInsensitive)
|| captionNoAmpersand.startsWith(QCoreApplication::translate("QCocoaMenuItem", "Preference"), Qt::CaseInsensitive)
|| captionNoAmpersand.startsWith(QCoreApplication::translate("QCocoaMenuItem", "Options"), Qt::CaseInsensitive)
|| captionNoAmpersand.startsWith(QCoreApplication::translate("QCocoaMenuItem", "Setting"), Qt::CaseInsensitive)
|| captionNoAmpersand.startsWith(QCoreApplication::translate("QCocoaMenuItem", "Setup"), Qt::CaseInsensitive)) {
return QPlatformMenuItem::PreferencesRole;
}
if (captionNoAmpersand.startsWith(QCoreApplication::translate("QCocoaMenuItem", "Quit"), Qt::CaseInsensitive)
|| captionNoAmpersand.startsWith(QCoreApplication::translate("QCocoaMenuItem", "Exit"), Qt::CaseInsensitive)) {
return QPlatformMenuItem::QuitRole;
}
if (!captionNoAmpersand.compare(QCoreApplication::translate("QCocoaMenuItem", "Cut"), Qt::CaseInsensitive))
return QPlatformMenuItem::CutRole;
if (!captionNoAmpersand.compare(QCoreApplication::translate("QCocoaMenuItem", "Copy"), Qt::CaseInsensitive))
return QPlatformMenuItem::CopyRole;
if (!captionNoAmpersand.compare(QCoreApplication::translate("QCocoaMenuItem", "Paste"), Qt::CaseInsensitive))
return QPlatformMenuItem::PasteRole;
if (!captionNoAmpersand.compare(QCoreApplication::translate("QCocoaMenuItem", "Select All"), Qt::CaseInsensitive))
return QPlatformMenuItem::SelectAllRole;
return QPlatformMenuItem::NoRole;
return detectedRole;
}
NSMenuItem *QCocoaMenuItem::sync()

View File

@ -596,11 +596,13 @@ void QMenuBar::initStyleOption(QStyleOptionMenuItem *option, const QAction *acti
Qt for \macos also provides a menu bar merging feature to make
QMenuBar conform more closely to accepted \macos menu bar layout.
The merging functionality is based on string matching the title of
a QMenu entry. These strings are translated (using QObject::tr())
in the "QMenuBar" context. If an entry is moved its slots will still
fire as if it was in the original place. The table below outlines
the strings looked for and where the entry is placed if matched:
If an entry is moved its slots will still fire as if it was in the
original place.
The merging functionality is based on the QAction::menuRole() of
the menu entries. If an item has QAction::TextHeuristicRole,
the role is determined by string matching the title using the
following heuristics:
\table
\header \li String matches \li Placement \li Notes
@ -618,8 +620,8 @@ void QMenuBar::initStyleOption(QStyleOptionMenuItem *option, const QAction *acti
created to call QCoreApplication::quit()
\endtable
You can override this behavior by using the QAction::menuRole()
property.
You can override this behavior by setting the QAction::menuRole()
property to QAction::NoRole.
If you want all windows in a Mac application to share one menu
bar, you must create a menu bar that does not have a parent.