Clean up cancel operation handling on OS X

The logic for handling cancel operations was spread out through
the code base and sometimes hard-coded to only include the Escape
key shortcut, missing the Command+. shortcut.

We now intercept both attempts at cancel operations from the system
through cancelOperation, which we forward as normal key events.

A new QKeySequence::StandardKey has been added for the Cancel sequence,
which maps to Escape on all platforms, and Command+. in addition for
OS X. The hard-coded logic in QWidget and subclasses for dealing
with closing the dialogs has been replaced with this key sequence,
which allows clients to override the behavior. Note that the widget
code is not wrapped in checks for QT_NO_SHORTCUT, as we don't care
about keeping widgets building and working under that define.

The logic in QCocoaWindow to bypass windowShouldClose when delivering
IM events has been removed as we now handle that specific case by
also forwarding Escape as a cancel operation.

Task-number: QTBUG-47557
Task-number: QTBUG-45771
Task-number: QTBUG-44076
Change-Id: Ibe0b3a4819f8659d246a2142dd7d9cd3a826ef78
Reviewed-by: Tim Blechmann <tim@klingt.org>
Reviewed-by: Morten Johan Sørvig <morten.sorvig@theqtcompany.com>
Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@theqtcompany.com>
This commit is contained in:
Tor Arne Vestbø 2015-09-04 17:56:30 +02:00
parent b2e664df61
commit 126c2cb8fb
23 changed files with 106 additions and 101 deletions

View File

@ -291,6 +291,7 @@ void Q_GUI_EXPORT qt_set_sequence_auto_mnemonic(bool b) { qt_sequence_no_mnemoni
\row \li InsertParagraphSeparator \li Enter \li Enter \li Enter \li Enter
\row \li InsertLineSeparator \li Shift+Enter \li Meta+Enter, Meta+O \li Shift+Enter \li Shift+Enter
\row \li Backspace \li (none) \li Meta+H \li (none) \li (none)
\row \li Cancel \li Escape \li Escape, Ctrl+. \li Escape \li Escape
\endtable
Note that, since the key sequences used for the standard shortcuts differ
@ -752,6 +753,7 @@ static const struct {
\value ZoomIn Zoom in.
\value ZoomOut Zoom out.
\value FullScreen Toggle the window state to/from full screen.
\value Cancel Cancel the current operation.
*/
/*!

View File

@ -137,7 +137,8 @@ public:
FullScreen,
Deselect,
DeleteCompleteLine,
Backspace
Backspace,
Cancel
};
Q_ENUM(StandardKey)

View File

@ -323,7 +323,9 @@ const QKeyBinding QPlatformThemePrivate::keyBindings[] = {
{QKeySequence::FullScreen, 1, Qt::Key_F11, KB_Win | KB_KDE},
{QKeySequence::Deselect, 0, Qt::CTRL | Qt::SHIFT | Qt::Key_A, KB_X11},
{QKeySequence::DeleteCompleteLine, 0, Qt::CTRL | Qt::Key_U, KB_X11},
{QKeySequence::Backspace, 0, Qt::META | Qt::Key_H, KB_Mac}
{QKeySequence::Backspace, 0, Qt::META | Qt::Key_H, KB_Mac},
{QKeySequence::Cancel, 0, Qt::Key_Escape, KB_All},
{QKeySequence::Cancel, 0, Qt::CTRL | Qt::Key_Period, KB_Mac}
};
const uint QPlatformThemePrivate::numberOfKeyBindings = sizeof(QPlatformThemePrivate::keyBindings)/(sizeof(QKeyBinding));

View File

@ -270,7 +270,6 @@ public: // for QNSView
QPointer<QWindow> m_enterLeaveTargetWindow;
bool m_windowUnderMouse;
bool m_ignoreWindowShouldClose;
bool m_inConstructor;
bool m_inSetVisible;
bool m_inSetGeometry;

View File

@ -345,7 +345,6 @@ QCocoaWindow::QCocoaWindow(QWindow *tlw)
, m_synchedWindowState(Qt::WindowActive)
, m_windowModality(Qt::NonModal)
, m_windowUnderMouse(false)
, m_ignoreWindowShouldClose(false)
, m_inConstructor(true)
, m_inSetVisible(false)
, m_inSetGeometry(false)
@ -1219,9 +1218,10 @@ void QCocoaWindow::windowDidEndLiveResize()
bool QCocoaWindow::windowShouldClose()
{
// might have been set from qnsview.mm
if (m_ignoreWindowShouldClose)
return false;
// This callback should technically only determine if the window
// should (be allowed to) close, but since our QPA API to determine
// that also involves actually closing the window we do both at the
// same time, instead of doing the latter in windowWillClose.
bool accepted = false;
QWindowSystemInterface::handleCloseEvent(window(), &accepted);
QWindowSystemInterface::flushWindowSystemEvents();

View File

@ -76,6 +76,7 @@ Q_FORWARD_DECLARE_OBJC_CLASS(QT_MANGLE_NAMESPACE(QNSViewMouseMoveHelper));
bool m_resendKeyEvent;
bool m_scrolling;
bool m_exposedOnMoveToWindow;
NSEvent *m_currentlyInterpretedKeyEvent;
}
- (id)init;
@ -131,7 +132,6 @@ Q_FORWARD_DECLARE_OBJC_CLASS(QT_MANGLE_NAMESPACE(QNSViewMouseMoveHelper));
- (void)handleKeyEvent:(NSEvent *)theEvent eventType:(int)eventType;
- (void)keyDown:(NSEvent *)theEvent;
- (void)keyUp:(NSEvent *)theEvent;
- (BOOL)performKeyEquivalent:(NSEvent *)theEvent;
- (void)registerDragTypes;
- (NSDragOperation)handleDrag:(id <NSDraggingInfo>)sender;

View File

@ -152,6 +152,7 @@ static NSString *_q_NSWindowDidChangeOcclusionStateNotification = nil;
m_mouseMoveHelper = [[QT_MANGLE_NAMESPACE(QNSViewMouseMoveHelper) alloc] initWithView:self];
m_resendKeyEvent = false;
m_scrolling = false;
m_currentlyInterpretedKeyEvent = 0;
if (!touchDevice) {
touchDevice = new QTouchDevice;
@ -1449,21 +1450,17 @@ static QTabletEvent::TabletDevice wacomTabletDevice(NSEvent *theEvent)
QObject *fo = QGuiApplication::focusObject();
if (m_sendKeyEvent && fo) {
// if escape is pressed we don't want interpretKeyEvents to close a dialog. This will be done via QWindowSystemInterface
if (keyCode == Qt::Key_Escape)
m_platformWindow->m_ignoreWindowShouldClose = true;
QInputMethodQueryEvent queryEvent(Qt::ImEnabled | Qt::ImHints);
if (QCoreApplication::sendEvent(fo, &queryEvent)) {
bool imEnabled = queryEvent.value(Qt::ImEnabled).toBool();
Qt::InputMethodHints hints = static_cast<Qt::InputMethodHints>(queryEvent.value(Qt::ImHints).toUInt());
if (imEnabled && !(hints & Qt::ImhDigitsOnly || hints & Qt::ImhFormattedNumbersOnly || hints & Qt::ImhHiddenText)) {
// pass the key event to the input method. note that m_sendKeyEvent may be set to false during this call
m_currentlyInterpretedKeyEvent = nsevent;
[self interpretKeyEvents:[NSArray arrayWithObject:nsevent]];
m_currentlyInterpretedKeyEvent = 0;
}
}
m_platformWindow->m_ignoreWindowShouldClose = false;;
}
if (m_resendKeyEvent)
m_sendKeyEvent = true;
@ -1491,21 +1488,23 @@ static QTabletEvent::TabletDevice wacomTabletDevice(NSEvent *theEvent)
[self handleKeyEvent:nsevent eventType:int(QEvent::KeyRelease)];
}
- (BOOL)performKeyEquivalent:(NSEvent *)nsevent
- (void)cancelOperation:(id)sender
{
NSString *chars = [nsevent charactersIgnoringModifiers];
Q_UNUSED(sender);
if ([nsevent type] == NSKeyDown && [chars length] > 0) {
QChar ch = [chars characterAtIndex:0];
Qt::Key qtKey = qt_mac_cocoaKey2QtKey(ch);
// check for Command + Key_Period
if ([nsevent modifierFlags] & NSCommandKeyMask
&& qtKey == Qt::Key_Period) {
[self handleKeyEvent:nsevent eventType:int(QEvent::KeyPress)];
return YES;
}
}
return [super performKeyEquivalent:nsevent];
NSEvent *currentEvent = [NSApp currentEvent];
if (!currentEvent || currentEvent.type != NSKeyDown)
return;
// Handling the key event may recurse back here through interpretKeyEvents
// (when IM is enabled), so we need to guard against that.
if (currentEvent == m_currentlyInterpretedKeyEvent)
return;
// Send Command+Key_Period and Escape as normal keypresses so that
// the key sequence is delivered through Qt. That way clients can
// intercept the shortcut and override its effect.
[self handleKeyEvent:currentEvent eventType:int(QEvent::KeyPress)];
}
- (void)flagsChanged:(NSEvent *)nsevent

View File

@ -2286,16 +2286,12 @@ bool QColorDialogPrivate::handleColorPickingMouseButtonRelease(QMouseEvent *e)
bool QColorDialogPrivate::handleColorPickingKeyPress(QKeyEvent *e)
{
Q_Q(QColorDialog);
switch (e->key()) {
case Qt::Key_Escape:
if (e->matches(QKeySequence::Cancel)) {
releaseColorPicking();
q->setCurrentColor(beforeScreenColorPicking);
break;
case Qt::Key_Return:
case Qt::Key_Enter:
} else if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) {
q->setCurrentColor(grabScreenColor(QCursor::pos()));
releaseColorPicking();
break;
}
e->accept();
return true;

View File

@ -651,11 +651,9 @@ void QDialog::keyPressEvent(QKeyEvent *e)
// Calls reject() if Escape is pressed. Simulates a button
// click for the default button if Enter is pressed. Move focus
// for the arrow keys. Ignore the rest.
#ifdef Q_OS_MAC
if(e->modifiers() == Qt::ControlModifier && e->key() == Qt::Key_Period) {
if (e->matches(QKeySequence::Cancel)) {
reject();
} else
#endif
if (!e->modifiers() || (e->modifiers() & Qt::KeypadModifier && e->key() == Qt::Key_Enter)) {
switch (e->key()) {
case Qt::Key_Enter:
@ -671,9 +669,6 @@ void QDialog::keyPressEvent(QKeyEvent *e)
}
}
break;
case Qt::Key_Escape:
reject();
break;
default:
e->ignore();
return;

View File

@ -3778,6 +3778,12 @@ void QFileDialogPrivate::_q_nativeEnterDirectory(const QUrl &directory)
bool QFileDialogPrivate::itemViewKeyboardEvent(QKeyEvent *event) {
Q_Q(QFileDialog);
if (event->matches(QKeySequence::Cancel)) {
q->hide();
return true;
}
switch (event->key()) {
case Qt::Key_Backspace:
_q_navigateToParent();
@ -3793,9 +3799,6 @@ bool QFileDialogPrivate::itemViewKeyboardEvent(QKeyEvent *event) {
return true;
}
break;
case Qt::Key_Escape:
q->hide();
return true;
default:
break;
}
@ -3982,7 +3985,7 @@ void QFileDialogLineEdit::keyPressEvent(QKeyEvent *e)
int key = e->key();
QLineEdit::keyPressEvent(e);
if (key != Qt::Key_Escape && key != Qt::Key_Back)
if (!e->matches(QKeySequence::Cancel) && key != Qt::Key_Back)
e->accept();
}

View File

@ -1450,11 +1450,8 @@ void QMessageBox::changeEvent(QEvent *ev)
void QMessageBox::keyPressEvent(QKeyEvent *e)
{
Q_D(QMessageBox);
if (e->key() == Qt::Key_Escape
#ifdef Q_OS_MAC
|| (e->modifiers() == Qt::ControlModifier && e->key() == Qt::Key_Period)
#endif
) {
if (e->matches(QKeySequence::Cancel)) {
if (d->detectedEscapeButton) {
#ifdef Q_OS_MAC
d->detectedEscapeButton->animateClick();

View File

@ -411,7 +411,8 @@ void QProgressDialog::setCancelButton(QPushButton *cancelButton)
if (cancelButton) {
connect(d->cancel, SIGNAL(clicked()), this, SIGNAL(canceled()));
#ifndef QT_NO_SHORTCUT
d->escapeShortcut = new QShortcut(Qt::Key_Escape, this, SIGNAL(canceled()));
// FIXME: This only registers the primary key sequence of the cancel action
d->escapeShortcut = new QShortcut(QKeySequence::Cancel, this, SIGNAL(canceled()));
#endif
} else {
#ifndef QT_NO_SHORTCUT

View File

@ -455,6 +455,12 @@ bool QAbstractItemDelegatePrivate::editorEventFilter(QObject *object, QEvent *ev
if (editorHandlesKeyEvent(editor, keyEvent))
return false;
if (keyEvent->matches(QKeySequence::Cancel)) {
// don't commit data
emit q->closeEditor(editor, QAbstractItemDelegate::RevertModelCache);
return true;
}
switch (keyEvent->key()) {
case Qt::Key_Tab:
if (tryFixup(editor)) {
@ -479,10 +485,6 @@ bool QAbstractItemDelegatePrivate::editorEventFilter(QObject *object, QEvent *ev
QMetaObject::invokeMethod(q, "_q_commitDataAndCloseEditor",
Qt::QueuedConnection, Q_ARG(QWidget*, editor));
return false;
case Qt::Key_Escape:
// don't commit data
emit q->closeEditor(editor, QAbstractItemDelegate::RevertModelCache);
return true;
default:
return false;
}
@ -509,7 +511,7 @@ bool QAbstractItemDelegatePrivate::editorEventFilter(QObject *object, QEvent *ev
emit q->closeEditor(editor, QAbstractItemDelegate::NoHint);
}
} else if (event->type() == QEvent::ShortcutOverride) {
if (static_cast<QKeyEvent*>(event)->key() == Qt::Key_Escape) {
if (static_cast<QKeyEvent*>(event)->matches(QKeySequence::Cancel)) {
event->accept();
return true;
}

View File

@ -459,7 +459,7 @@ bool QWhatsThisPrivate::eventFilter(QObject *o, QEvent *e)
{
QKeyEvent* kev = (QKeyEvent*)e;
if (kev->key() == Qt::Key_Escape) {
if (kev->matches(QKeySequence::Cancel)) {
QWhatsThis::leaveWhatsThisMode();
return true;
} else if (customWhatsThis) {

View File

@ -9349,7 +9349,8 @@ void QWidget::tabletEvent(QTabletEvent *event)
call the base class implementation if you do not act upon the key.
The default implementation closes popup widgets if the user
presses Esc. Otherwise the event is ignored, so that the widget's
presses the key sequence for QKeySequence::Cancel (typically the
Escape key). Otherwise the event is ignored, so that the widget's
parent can interpret it.
Note that QKeyEvent starts with isAccepted() == true, so you do not
@ -9362,7 +9363,7 @@ void QWidget::tabletEvent(QTabletEvent *event)
void QWidget::keyPressEvent(QKeyEvent *event)
{
if ((windowType() == Qt::Popup) && event->key() == Qt::Key_Escape) {
if ((windowType() == Qt::Popup) && event->matches(QKeySequence::Cancel)) {
event->accept();
close();
} else {

View File

@ -1337,6 +1337,11 @@ bool QCompleter::eventFilter(QObject *o, QEvent *e)
}
// default implementation for keys not handled by the widget when popup is open
if (ke->matches(QKeySequence::Cancel)) {
d->popup->hide();
return true;
}
switch (key) {
#ifdef QT_KEYPAD_NAVIGATION
case Qt::Key_Select:
@ -1357,7 +1362,6 @@ bool QCompleter::eventFilter(QObject *o, QEvent *e)
break;
case Qt::Key_Backtab:
case Qt::Key_Escape:
d->popup->hide();
break;

View File

@ -1221,16 +1221,14 @@ void QAbstractButton::keyPressEvent(QKeyEvent *e)
}
break;
}
case Qt::Key_Escape:
if (d->down) {
default:
if (e->matches(QKeySequence::Cancel) && d->down) {
setDown(false);
repaint(); //flush paint event before invoking potentially expensive operation
QApplication::flush();
d->emitReleased();
break;
return;
}
// fall through
default:
e->ignore();
}
}

View File

@ -776,7 +776,7 @@ bool QCalendarTextNavigator::eventFilter(QObject *o, QEvent *e)
applyDate();
emit editingFinished();
removeDateLabel();
} else if (ke->key() == Qt::Key_Escape) {
} else if (ke->matches(QKeySequence::Cancel)) {
removeDateLabel();
} else if (e->type() == QEvent::KeyPress) {
createDateLabel();
@ -3078,8 +3078,7 @@ void QCalendarWidget::resizeEvent(QResizeEvent * event)
void QCalendarWidget::keyPressEvent(QKeyEvent * event)
{
Q_D(QCalendarWidget);
if(d->yearEdit->isVisible()&& event->key() == Qt::Key_Escape)
{
if (d->yearEdit->isVisible()&& event->matches(QKeySequence::Cancel)) {
d->yearEdit->setValue(yearShown());
d->_q_yearEditingFinished();
return;

View File

@ -654,8 +654,9 @@ void QComboBoxPrivateContainer::changeEvent(QEvent *e)
bool QComboBoxPrivateContainer::eventFilter(QObject *o, QEvent *e)
{
switch (e->type()) {
case QEvent::ShortcutOverride:
switch (static_cast<QKeyEvent*>(e)->key()) {
case QEvent::ShortcutOverride: {
QKeyEvent *keyEvent = static_cast<QKeyEvent*>(e);
switch (keyEvent->key()) {
case Qt::Key_Enter:
case Qt::Key_Return:
#ifdef QT_KEYPAD_NAVIGATION
@ -667,17 +668,21 @@ bool QComboBoxPrivateContainer::eventFilter(QObject *o, QEvent *e)
}
return true;
case Qt::Key_Down:
if (!(static_cast<QKeyEvent*>(e)->modifiers() & Qt::AltModifier))
if (!(keyEvent->modifiers() & Qt::AltModifier))
break;
// fall through
case Qt::Key_F4:
case Qt::Key_Escape:
combo->hidePopup();
return true;
default:
if (keyEvent->matches(QKeySequence::Cancel)) {
combo->hidePopup();
return true;
}
break;
}
break;
}
case QEvent::MouseMove:
if (isVisible()) {
QMouseEvent *m = static_cast<QMouseEvent *>(e);

View File

@ -2655,7 +2655,7 @@ bool QCalendarPopup::event(QEvent *event)
{
if (event->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
if (keyEvent->key()== Qt::Key_Escape)
if (keyEvent->matches(QKeySequence::Cancel))
dateChanged = false;
}
return QWidget::event(event);

View File

@ -197,7 +197,7 @@ bool QAlphaWidget::eventFilter(QObject *o, QEvent *e)
break;
case QEvent::KeyPress: {
QKeyEvent *ke = (QKeyEvent*)e;
if (ke->key() == Qt::Key_Escape) {
if (ke->matches(QKeySequence::Cancel)) {
showWidget = false;
} else {
duration = 0;

View File

@ -2679,7 +2679,7 @@ QMenu::event(QEvent *e)
if (kev->key() == Qt::Key_Up || kev->key() == Qt::Key_Down
|| kev->key() == Qt::Key_Left || kev->key() == Qt::Key_Right
|| kev->key() == Qt::Key_Enter || kev->key() == Qt::Key_Return
|| kev->key() == Qt::Key_Escape) {
|| kev->matches(QKeySequence::Cancel)) {
e->accept();
return true;
}
@ -2965,27 +2965,6 @@ void QMenu::keyPressEvent(QKeyEvent *e)
}
break;
case Qt::Key_Escape:
#ifdef QT_KEYPAD_NAVIGATION
case Qt::Key_Back:
#endif
key_consumed = true;
if (d->tornoff) {
close();
return;
}
{
QPointer<QWidget> caused = d->causedPopup.widget;
d->hideMenu(this); // hide after getting causedPopup
#ifndef QT_NO_MENUBAR
if (QMenuBar *mb = qobject_cast<QMenuBar*>(caused)) {
mb->d_func()->setCurrentAction(d->menuAction);
mb->d_func()->setKeyboardMode(true);
}
#endif
}
break;
case Qt::Key_Space:
if (!style()->styleHint(QStyle::SH_Menu_SpaceActivatesItem, 0, this))
break;
@ -3022,6 +3001,28 @@ void QMenu::keyPressEvent(QKeyEvent *e)
key_consumed = false;
}
if (!key_consumed && (e->matches(QKeySequence::Cancel)
#ifdef QT_KEYPAD_NAVIGATION
|| e->key() == Qt::Key_Back
#endif
)) {
key_consumed = true;
if (d->tornoff) {
close();
return;
}
{
QPointer<QWidget> caused = d->causedPopup.widget;
d->hideMenu(this); // hide after getting causedPopup
#ifndef QT_NO_MENUBAR
if (QMenuBar *mb = qobject_cast<QMenuBar*>(caused)) {
mb->d_func()->setCurrentAction(d->menuAction);
mb->d_func()->setKeyboardMode(true);
}
#endif
}
}
if (!key_consumed) { // send to menu bar
if ((!e->modifiers() || e->modifiers() == Qt::AltModifier || e->modifiers() == Qt::ShiftModifier) &&
e->text().length()==1) {

View File

@ -1122,14 +1122,14 @@ void QMenuBar::keyPressEvent(QKeyEvent *e)
}
break; }
case Qt::Key_Escape:
default:
key_consumed = false;
}
if (!key_consumed && e->matches(QKeySequence::Cancel)) {
d->setCurrentAction(0);
d->setKeyboardMode(false);
key_consumed = true;
break;
default:
key_consumed = false;
}
if(!key_consumed &&
@ -1432,7 +1432,7 @@ bool QMenuBar::event(QEvent *e)
case QEvent::ShortcutOverride: {
QKeyEvent *kev = static_cast<QKeyEvent*>(e);
//we only filter out escape if there is a current action
if (kev->key() == Qt::Key_Escape && d->currentAction) {
if (kev->matches(QKeySequence::Cancel) && d->currentAction) {
e->accept();
return true;
}