/*************************************************************************** * Copyright (C) 2005-2015 by the Quassel Project * * devel@quassel-irc.org * * * * This class has been inspired by KDE's KKeySequenceWidget and uses * * some code snippets of its implementation, part of kdelibs. * * The original file is * * Copyright (C) 1998 Mark Donohoe * * Copyright (C) 2001 Ellis Whitehead * * Copyright (C) 2007 Andreas Hartmetz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include #include #include #include #include #include #include // This defines the unicode symbols for special keys (kCommandUnicode and friends) #ifdef Q_OS_MAC # include #endif #include "action.h" #include "actioncollection.h" #include "keysequencewidget.h" KeySequenceButton::KeySequenceButton(KeySequenceWidget *d_, QWidget *parent) : QPushButton(parent), d(d_) { } bool KeySequenceButton::event(QEvent *e) { if (d->isRecording() && e->type() == QEvent::KeyPress) { keyPressEvent(static_cast(e)); return true; } // The shortcut 'alt+c' ( or any other dialog local action shortcut ) // ended the recording and triggered the action associated with the // action. In case of 'alt+c' ending the dialog. It seems that those // ShortcutOverride events get sent even if grabKeyboard() is active. if (d->isRecording() && e->type() == QEvent::ShortcutOverride) { e->accept(); return true; } return QPushButton::event(e); } void KeySequenceButton::keyPressEvent(QKeyEvent *e) { int keyQt = e->key(); if (keyQt == -1) { // Qt sometimes returns garbage keycodes, I observed -1, if it doesn't know a key. // We cannot do anything useful with those (several keys have -1, indistinguishable) // and QKeySequence.toString() will also yield a garbage string. QMessageBox::information(this, tr("The key you just pressed is not supported by Qt."), tr("Unsupported Key")); return d->cancelRecording(); } uint newModifiers = e->modifiers() & (Qt::SHIFT | Qt::CTRL | Qt::ALT | Qt::META); //don't have the return or space key appear as first key of the sequence when they //were pressed to start editing - catch and them and imitate their effect if (!d->isRecording() && ((keyQt == Qt::Key_Return || keyQt == Qt::Key_Space))) { d->startRecording(); d->_modifierKeys = newModifiers; d->updateShortcutDisplay(); return; } // We get events even if recording isn't active. if (!d->isRecording()) return QPushButton::keyPressEvent(e); e->accept(); d->_modifierKeys = newModifiers; switch (keyQt) { case Qt::Key_AltGr: //or else we get unicode salad return; case Qt::Key_Shift: case Qt::Key_Control: case Qt::Key_Alt: case Qt::Key_Meta: case Qt::Key_Menu: //unused (yes, but why?) d->updateShortcutDisplay(); break; default: if (!(d->_modifierKeys & ~Qt::SHIFT)) { // It's the first key and no modifier pressed. Check if this is // allowed if (!d->isOkWhenModifierless(keyQt)) return; } // We now have a valid key press. if (keyQt) { if ((keyQt == Qt::Key_Backtab) && (d->_modifierKeys & Qt::SHIFT)) { keyQt = Qt::Key_Tab | d->_modifierKeys; } else if (d->isShiftAsModifierAllowed(keyQt)) { keyQt |= d->_modifierKeys; } else keyQt |= (d->_modifierKeys & ~Qt::SHIFT); d->_keySequence = QKeySequence(keyQt); d->doneRecording(); } } } void KeySequenceButton::keyReleaseEvent(QKeyEvent *e) { if (e->key() == -1) { // ignore garbage, see keyPressEvent() return; } if (!d->isRecording()) return QPushButton::keyReleaseEvent(e); e->accept(); uint newModifiers = e->modifiers() & (Qt::SHIFT | Qt::CTRL | Qt::ALT | Qt::META); // if a modifier that belongs to the shortcut was released... if ((newModifiers & d->_modifierKeys) < d->_modifierKeys) { d->_modifierKeys = newModifiers; d->updateShortcutDisplay(); } } /******************************************************************************/ KeySequenceWidget::KeySequenceWidget(QWidget *parent) : QWidget(parent), _shortcutsModel(0), _isRecording(false), _modifierKeys(0) { QHBoxLayout *layout = new QHBoxLayout(this); layout->setMargin(0); _keyButton = new KeySequenceButton(this, this); _keyButton->setFocusPolicy(Qt::StrongFocus); _keyButton->setIcon(QIcon::fromTheme("configure")); _keyButton->setToolTip(tr("Click on the button, then enter the shortcut like you would in the program.\nExample for Ctrl+a: hold the Ctrl key and press a.")); layout->addWidget(_keyButton); _clearButton = new QToolButton(this); layout->addWidget(_clearButton); if (qApp->isLeftToRight()) _clearButton->setIcon(QIcon::fromTheme("edit-clear-locationbar-rtl", QIcon::fromTheme("edit-clear"))); else _clearButton->setIcon(QIcon::fromTheme("edit-clear-locationbar-ltr", QIcon::fromTheme("edit-clear"))); setLayout(layout); connect(_keyButton, SIGNAL(clicked()), SLOT(startRecording())); connect(_keyButton, SIGNAL(clicked()), SIGNAL(clicked())); connect(_clearButton, SIGNAL(clicked()), SLOT(clear())); connect(_clearButton, SIGNAL(clicked()), SIGNAL(clicked())); } void KeySequenceWidget::setModel(ShortcutsModel *model) { Q_ASSERT(!_shortcutsModel); _shortcutsModel = model; } bool KeySequenceWidget::isOkWhenModifierless(int keyQt) const { //this whole function is a hack, but especially the first line of code // if (QKeySequence(keyQt).toString().length() == 1) // return false; switch (keyQt) { case Qt::Key_Return: // case Qt::Key_Space: case Qt::Key_Tab: case Qt::Key_Backtab: //does this ever happen? case Qt::Key_Backspace: case Qt::Key_Delete: return false; default: return true; } } bool KeySequenceWidget::isShiftAsModifierAllowed(int keyQt) const { // Shift only works as a modifier with certain keys. It's not possible // to enter the SHIFT+5 key sequence for me because this is handled as // '%' by qt on my keyboard. // The working keys are all hardcoded here :-( if (keyQt >= Qt::Key_F1 && keyQt <= Qt::Key_F35) return true; if (QChar(keyQt).isLetter()) return true; switch (keyQt) { case Qt::Key_Return: case Qt::Key_Space: case Qt::Key_Backspace: case Qt::Key_Escape: case Qt::Key_Print: case Qt::Key_ScrollLock: case Qt::Key_Pause: case Qt::Key_PageUp: case Qt::Key_PageDown: case Qt::Key_Insert: case Qt::Key_Delete: case Qt::Key_Home: case Qt::Key_End: case Qt::Key_Up: case Qt::Key_Down: case Qt::Key_Left: case Qt::Key_Right: return true; default: return false; } } void KeySequenceWidget::updateShortcutDisplay() { QString s = _keySequence.toString(QKeySequence::NativeText); s.replace('&', QLatin1String("&&")); if (_isRecording) { if (_modifierKeys) { #ifdef Q_OS_MAC if (_modifierKeys & Qt::META) s += QChar(kControlUnicode); if (_modifierKeys & Qt::ALT) s += QChar(kOptionUnicode); if (_modifierKeys & Qt::SHIFT) s += QChar(kShiftUnicode); if (_modifierKeys & Qt::CTRL) s += QChar(kCommandUnicode); #else if (_modifierKeys & Qt::META) s += tr("Meta", "Meta key") + '+'; if (_modifierKeys & Qt::CTRL) s += tr("Ctrl", "Ctrl key") + '+'; if (_modifierKeys & Qt::ALT) s += tr("Alt", "Alt key") + '+'; if (_modifierKeys & Qt::SHIFT) s += tr("Shift", "Shift key") + '+'; #endif } else { s = tr("Input", "What the user inputs now will be taken as the new shortcut"); } // make it clear that input is still going on s.append(" ..."); } if (s.isEmpty()) { s = tr("None", "No shortcut defined"); } s.prepend(' '); s.append(' '); _keyButton->setText(s); } void KeySequenceWidget::startRecording() { _modifierKeys = 0; _oldKeySequence = _keySequence; _keySequence = QKeySequence(); _conflictingIndex = QModelIndex(); _isRecording = true; _keyButton->grabKeyboard(); if (!QWidget::keyboardGrabber()) { qWarning() << "Failed to grab the keyboard! Most likely qt's nograb option is active"; } _keyButton->setDown(true); updateShortcutDisplay(); } void KeySequenceWidget::doneRecording() { bool wasRecording = _isRecording; _isRecording = false; _keyButton->releaseKeyboard(); _keyButton->setDown(false); if (!wasRecording || _keySequence == _oldKeySequence) { // The sequence hasn't changed updateShortcutDisplay(); return; } if (!isKeySequenceAvailable(_keySequence)) { _keySequence = _oldKeySequence; } else if (wasRecording) { emit keySequenceChanged(_keySequence, _conflictingIndex); } updateShortcutDisplay(); } void KeySequenceWidget::cancelRecording() { _keySequence = _oldKeySequence; doneRecording(); } void KeySequenceWidget::setKeySequence(const QKeySequence &seq) { // oldKeySequence holds the key sequence before recording started, if setKeySequence() // is called while not recording then set oldKeySequence to the existing sequence so // that the keySequenceChanged() signal is emitted if the new and previous key // sequences are different if (!isRecording()) _oldKeySequence = _keySequence; _keySequence = seq; _clearButton->setVisible(!_keySequence.isEmpty()); doneRecording(); } void KeySequenceWidget::clear() { setKeySequence(QKeySequence()); // setKeySequence() won't emit a signal when we're not recording emit keySequenceChanged(QKeySequence()); } bool KeySequenceWidget::isKeySequenceAvailable(const QKeySequence &seq) { if (seq.isEmpty()) return true; // We need to access the root model, not the filtered one for (int cat = 0; cat < _shortcutsModel->rowCount(); cat++) { QModelIndex catIdx = _shortcutsModel->index(cat, 0); for (int r = 0; r < _shortcutsModel->rowCount(catIdx); r++) { QModelIndex actIdx = _shortcutsModel->index(r, 0, catIdx); Q_ASSERT(actIdx.isValid()); if (actIdx.data(ShortcutsModel::ActiveShortcutRole).value() != seq) continue; if (!actIdx.data(ShortcutsModel::IsConfigurableRole).toBool()) { QMessageBox::warning(this, tr("Shortcut Conflict"), tr("The \"%1\" shortcut is already in use, and cannot be configured.\nPlease choose another one.").arg(seq.toString(QKeySequence::NativeText)), QMessageBox::Ok); return false; } QMessageBox box(QMessageBox::Warning, tr("Shortcut Conflict"), (tr("The \"%1\" shortcut is ambiguous with the shortcut for the following action:") + "
  • %2

" + tr("Do you want to reassign this shortcut to the selected action?") ).arg(seq.toString(QKeySequence::NativeText), actIdx.data().toString()), QMessageBox::Cancel, this); box.addButton(tr("Reassign"), QMessageBox::AcceptRole); if (box.exec() == QMessageBox::Cancel) return false; _conflictingIndex = actIdx; return true; } } return true; }