OSX: add several menuitem roles to support menu shortcuts in dialogs

Now menu items and key shortcuts for Cut, Copy, Paste and Select All
work in the standard ways in dialogs such as the file dialog, provided
that the corresponding QActions have been created and added to the menu.
This depends on new roles to identify each menu item which is so
broadly applicable that it should work even when a native widget has
focus; but the role will be auto-detected, just as we were already
doing for application menu items such as Quit, About and Preferences.
When the QFileDialog is opened, it will call
redirectKnownMenuItemsToFirstResponder() which will make only those
"special" menu items have the standard actions and nil targets.  When
the dialog is dismissed, those NSMenuItems must be reverted by calling
resetKnownMenuItemsToQt(), because to invoke a QAction, the NSMenuItem's
action should be itemFired and the target should be the
QCocoaMenuDelegate.

Task-number: QTBUG-17291
Change-Id: I501375ca6fa13fac75d4b4fdcede993ec2329cc7
Reviewed-by: Morten Johan Sørvig <morten.sorvig@digia.com>
This commit is contained in:
Shawn Rutledge 2014-03-31 14:17:29 +02:00 committed by The Qt Project
parent a7f98a7ac0
commit b5eb850e0d
8 changed files with 111 additions and 3 deletions

View File

@ -66,7 +66,11 @@ Q_OBJECT
public:
// copied from, and must stay in sync with, QAction menu roles.
enum MenuRole { NoRole = 0, TextHeuristicRole, ApplicationSpecificRole, AboutQtRole,
AboutRole, PreferencesRole, QuitRole };
AboutRole, PreferencesRole, QuitRole,
// However these roles are private, perhaps temporarily.
// They could be added as public QAction roles if necessary.
CutRole, CopyRole, PasteRole, SelectAllRole,
RoleCount };
virtual void setTag(quintptr tag) = 0;
virtual quintptr tag()const = 0;

View File

@ -90,6 +90,14 @@ QPlatformMenuItem::MenuRole detectMenuRole(const QString &caption)
|| caption.startsWith(QCoreApplication::translate("QCocoaMenuItem", "Exit"), Qt::CaseInsensitive)) {
return QPlatformMenuItem::QuitRole;
}
if (!caption.compare(QCoreApplication::translate("QCocoaMenuItem", "Cut"), Qt::CaseInsensitive))
return QPlatformMenuItem::CutRole;
if (!caption.compare(QCoreApplication::translate("QCocoaMenuItem", "Copy"), Qt::CaseInsensitive))
return QPlatformMenuItem::CopyRole;
if (!caption.compare(QCoreApplication::translate("QCocoaMenuItem", "Paste"), Qt::CaseInsensitive))
return QPlatformMenuItem::PasteRole;
if (!caption.compare(QCoreApplication::translate("QCocoaMenuItem", "Select All"), Qt::CaseInsensitive))
return QPlatformMenuItem::SelectAllRole;
return QPlatformMenuItem::NoRole;
}

View File

@ -52,6 +52,7 @@
#include <private/qguiapplication_p.h>
#include "qt_mac_p.h"
#include "qcocoahelpers.h"
#include "qcocoamenubar.h"
#include <qregexp.h>
#include <qbuffer.h>
#include <qdebug.h>
@ -225,6 +226,7 @@ static QString strippedText(QString s)
|| [self panel:nil shouldShowFilename:filepath];
[self updateProperties];
QCocoaMenuBar::redirectKnownMenuItemsToFirstResponder();
[mOpenPanel setAllowedFileTypes:nil];
[mSavePanel setNameFieldStringValue:selectable ? QT_PREPEND_NAMESPACE(QCFString::toNSString)(info.fileName()) : @""];
@ -250,7 +252,9 @@ static QString strippedText(QString s)
// cleanup of modal sessions. Do this before showing the native dialog, otherwise it will
// close down during the cleanup.
qApp->processEvents(QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers);
QCocoaMenuBar::redirectKnownMenuItemsToFirstResponder();
mReturnCode = [mSavePanel runModal];
QCocoaMenuBar::resetKnownMenuItemsToQt();
QAbstractEventDispatcher::instance()->interrupt();
return (mReturnCode == NSOKButton);
@ -269,6 +273,7 @@ static QString strippedText(QString s)
|| [self panel:nil shouldShowFilename:filepath];
[self updateProperties];
QCocoaMenuBar::redirectKnownMenuItemsToFirstResponder();
[mSavePanel setDirectoryURL: [NSURL fileURLWithPath:mCurrentDir]];
[mSavePanel setNameFieldStringValue:selectable ? QCFString::toNSString(info.fileName()) : @""];
@ -583,6 +588,7 @@ void QCocoaFileDialogHelper::QNSOpenSavePanelDelegate_selectionChanged(const QSt
void QCocoaFileDialogHelper::QNSOpenSavePanelDelegate_panelClosed(bool accepted)
{
QCocoaMenuBar::resetKnownMenuItemsToQt();
if (accepted) {
emit accept();
} else {

View File

@ -67,9 +67,13 @@ public:
inline NSMenu *nsMenu() const
{ return m_nativeMenu; }
static void redirectKnownMenuItemsToFirstResponder();
static void resetKnownMenuItemsToQt();
static void updateMenuBarImmediately();
QList<QCocoaMenuItem*> merged() const;
NSMenuItem *itemForRole(QPlatformMenuItem::MenuRole r);
private:
static QCocoaWindow *findWindowForMenubar();
static QCocoaMenuBar *findGlobalMenubar();

View File

@ -205,6 +205,59 @@ QCocoaMenuBar *QCocoaMenuBar::findGlobalMenubar()
return NULL;
}
void QCocoaMenuBar::redirectKnownMenuItemsToFirstResponder()
{
// QTBUG-17291: http://forums.macrumors.com/showthread.php?t=1249452
// When a dialog is opened, shortcuts for actions inside the dialog (cut, paste, ...)
// continue to go through the same menu items which claimed those shortcuts.
// They are not keystrokes which we can intercept in any other way; the OS intercepts them.
// The menu items had to be created by the application. That's why we need roles
// to identify those "special" menu items which can be useful even when non-Qt
// native widgets are in focus. When the native widget is focused it will be the
// first responder, so the menu item needs to have its target be the first responder;
// this is done by setting it to nil.
// This function will find all menu items on all menus which have
// "special" roles, set the target and also set the standard actions which
// apply to those roles. But afterwards it is necessary to call
// resetKnownMenuItemsToQt() to put back the target and action so that
// those menu items will go back to invoking their associated QActions.
foreach (QCocoaMenuBar *mb, static_menubars)
foreach (QCocoaMenu *m, mb->m_menus)
foreach (QCocoaMenuItem *i, m->items()) {
bool known = true;
switch (i->effectiveRole()) {
case QPlatformMenuItem::CutRole:
[i->nsItem() setAction:@selector(cut:)];
break;
case QPlatformMenuItem::CopyRole:
[i->nsItem() setAction:@selector(copy:)];
break;
case QPlatformMenuItem::PasteRole:
[i->nsItem() setAction:@selector(paste:)];
break;
case QPlatformMenuItem::SelectAllRole:
[i->nsItem() setAction:@selector(selectAll:)];
break;
// We may discover later that there are other roles/actions which
// are meaningful to standard native widgets; they can be added.
default:
known = false;
break;
}
if (known)
[i->nsItem() setTarget:nil];
}
}
void QCocoaMenuBar::resetKnownMenuItemsToQt()
{
// Undo the effect of redirectKnownMenuItemsToFirstResponder():
// set the menu items' actions to itemFired and their targets to
// the QCocoaMenuDelegate.
updateMenuBarImmediately();
}
void QCocoaMenuBar::updateMenuBarImmediately()
{
QCocoaAutoReleasePool pool;
@ -321,3 +374,13 @@ QPlatformMenu *QCocoaMenuBar::menuForTag(quintptr tag) const
return 0;
}
NSMenuItem *QCocoaMenuBar::itemForRole(QPlatformMenuItem::MenuRole r)
{
foreach (QCocoaMenu *m, m_menus)
foreach (QCocoaMenuItem *i, m->items())
if (i->effectiveRole() == r)
return i->nsItem();
return Q_NULLPTR;
}

View File

@ -98,6 +98,8 @@ public:
inline bool isSeparator() const { return m_isSeparator; }
QCocoaMenu *menu() const { return m_menu; }
MenuRole effectiveRole() const;
private:
QString mergeText();
QKeySequence mergeAccel();
@ -112,6 +114,7 @@ private:
bool m_isSeparator;
QFont m_font;
MenuRole m_role;
MenuRole m_detectedRole;
QKeySequence m_shortcut;
bool m_checked;
bool m_merged;

View File

@ -227,7 +227,8 @@ NSMenuItem *QCocoaMenuItem::sync()
if (depth == 3 || !menubar)
break; // Menu item too deep in the hierarchy, or not connected to any menubar
switch (detectMenuRole(m_text)) {
m_detectedRole = detectMenuRole(m_text);
switch (m_detectedRole) {
case QPlatformMenuItem::AboutRole:
if (m_text.indexOf(QRegExp(QString::fromLatin1("qt$"), Qt::CaseInsensitive)) == -1)
mergeItem = [loader aboutMenuItem];
@ -241,6 +242,8 @@ NSMenuItem *QCocoaMenuItem::sync()
mergeItem = [loader quitMenuItem];
break;
default:
if (m_detectedRole >= CutRole && m_detectedRole < RoleCount && menubar)
mergeItem = menubar->itemForRole(m_detectedRole);
if (!m_text.isEmpty())
m_textSynced = true;
break;
@ -249,7 +252,7 @@ NSMenuItem *QCocoaMenuItem::sync()
}
default:
qWarning() << Q_FUNC_INFO << "unsupported role" << (int) m_role;
qWarning() << Q_FUNC_INFO << "menu item" << m_text << "has unsupported role" << (int)m_role;
}
if (mergeItem) {
@ -374,3 +377,11 @@ void QCocoaMenuItem::syncModalState(bool modal)
else
[m_native setEnabled:m_enabled];
}
QPlatformMenuItem::MenuRole QCocoaMenuItem::effectiveRole() const
{
if (m_role > TextHeuristicRole)
return m_role;
else
return m_detectedRole;
}

View File

@ -70,6 +70,15 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
QAction *quitAction = fileMenu->addAction(tr("Quit"));
quitAction->setShortcut(QKeySequence(QKeySequence::Quit));
connect(quitAction, SIGNAL(triggered()), qApp, SLOT(quit()));
QMenu *editMenu = menuBar()->addMenu(tr("&Edit"));
QAction *action = editMenu->addAction(tr("Cut"));
action->setShortcut(QKeySequence(QKeySequence::Quit));
action = editMenu->addAction(tr("Copy"));
action->setShortcut(QKeySequence(QKeySequence::Copy));
action = editMenu->addAction(tr("Paste"));
action->setShortcut(QKeySequence(QKeySequence::Paste));
action = editMenu->addAction(tr("Select All"));
action->setShortcut(QKeySequence(QKeySequence::SelectAll));
QTabWidget *tabWidget = new QTabWidget;
tabWidget->addTab(new FileDialogPanel, tr("QFileDialog"));
tabWidget->addTab(new ColorDialogPanel, tr("QColorDialog"));