1 /***************************************************************************
2 * Copyright (C) 2005-2019 by the Quassel Project *
3 * devel@quassel-irc.org *
5 * This class has been inspired by KDE's KKeySequenceWidget and uses *
6 * some code snippets of its implementation, part of kdelibs. *
7 * The original file is *
8 * Copyright (C) 1998 Mark Donohoe <donohoe@kde.org> *
9 * Copyright (C) 2001 Ellis Whitehead <ellis@kde.org> *
10 * Copyright (C) 2007 Andreas Hartmetz <ahartmetz@gmail.com> *
12 * This program is free software; you can redistribute it and/or modify *
13 * it under the terms of the GNU General Public License as published by *
14 * the Free Software Foundation; either version 2 of the License, or *
15 * (at your option) any later version. *
17 * This program is distributed in the hope that it will be useful, *
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
20 * GNU General Public License for more details. *
22 * You should have received a copy of the GNU General Public License *
23 * along with this program; if not, write to the *
24 * Free Software Foundation, Inc., *
25 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
26 ***************************************************************************/
28 #include <QApplication>
30 #include <QHBoxLayout>
32 #include <QMessageBox>
33 #include <QToolButton>
35 // This defines the unicode symbols for special keys (kCommandUnicode and friends)
37 # include <Carbon/Carbon.h>
41 #include "actioncollection.h"
43 #include "keysequencewidget.h"
45 KeySequenceButton::KeySequenceButton(KeySequenceWidget* d_, QWidget* parent)
50 bool KeySequenceButton::event(QEvent* e)
52 if (d->isRecording() && e->type() == QEvent::KeyPress) {
53 keyPressEvent(static_cast<QKeyEvent*>(e));
57 // The shortcut 'alt+c' ( or any other dialog local action shortcut )
58 // ended the recording and triggered the action associated with the
59 // action. In case of 'alt+c' ending the dialog. It seems that those
60 // ShortcutOverride events get sent even if grabKeyboard() is active.
61 if (d->isRecording() && e->type() == QEvent::ShortcutOverride) {
66 return QPushButton::event(e);
69 void KeySequenceButton::keyPressEvent(QKeyEvent* e)
73 // Qt sometimes returns garbage keycodes, I observed -1, if it doesn't know a key.
74 // We cannot do anything useful with those (several keys have -1, indistinguishable)
75 // and QKeySequence.toString() will also yield a garbage string.
76 QMessageBox::information(this, tr("The key you just pressed is not supported by Qt."), tr("Unsupported Key"));
77 return d->cancelRecording();
80 uint newModifiers = e->modifiers() & (Qt::SHIFT | Qt::CTRL | Qt::ALT | Qt::META);
82 // don't have the return or space key appear as first key of the sequence when they
83 // were pressed to start editing - catch and them and imitate their effect
84 if (!d->isRecording() && ((keyQt == Qt::Key_Return || keyQt == Qt::Key_Space))) {
86 d->_modifierKeys = newModifiers;
87 d->updateShortcutDisplay();
91 // We get events even if recording isn't active.
92 if (!d->isRecording())
93 return QPushButton::keyPressEvent(e);
96 d->_modifierKeys = newModifiers;
99 case Qt::Key_AltGr: // or else we get unicode salad
102 case Qt::Key_Control:
105 case Qt::Key_Menu: // unused (yes, but why?)
106 d->updateShortcutDisplay();
110 if (!(d->_modifierKeys & ~Qt::SHIFT)) {
111 // It's the first key and no modifier pressed. Check if this is
113 if (!d->isOkWhenModifierless(keyQt))
117 // We now have a valid key press.
119 if ((keyQt == Qt::Key_Backtab) && (d->_modifierKeys & Qt::SHIFT)) {
120 keyQt = Qt::Key_Tab | d->_modifierKeys;
122 else if (d->isShiftAsModifierAllowed(keyQt)) {
123 keyQt |= d->_modifierKeys;
126 keyQt |= (d->_modifierKeys & ~Qt::SHIFT);
128 d->_keySequence = QKeySequence(keyQt);
134 void KeySequenceButton::keyReleaseEvent(QKeyEvent* e)
136 if (e->key() == -1) {
137 // ignore garbage, see keyPressEvent()
141 if (!d->isRecording())
142 return QPushButton::keyReleaseEvent(e);
146 uint newModifiers = e->modifiers() & (Qt::SHIFT | Qt::CTRL | Qt::ALT | Qt::META);
148 // if a modifier that belongs to the shortcut was released...
149 if ((newModifiers & d->_modifierKeys) < d->_modifierKeys) {
150 d->_modifierKeys = newModifiers;
151 d->updateShortcutDisplay();
155 /******************************************************************************/
157 KeySequenceWidget::KeySequenceWidget(QWidget* parent)
160 auto* layout = new QHBoxLayout(this);
161 layout->setMargin(0);
163 _keyButton = new KeySequenceButton(this, this);
164 _keyButton->setFocusPolicy(Qt::StrongFocus);
165 _keyButton->setIcon(icon::get("configure"));
166 _keyButton->setToolTip(tr(
167 "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."));
168 layout->addWidget(_keyButton);
170 _clearButton = new QToolButton(this);
171 layout->addWidget(_clearButton);
173 if (qApp->isLeftToRight())
174 _clearButton->setIcon(icon::get("edit-clear-locationbar-rtl"));
176 _clearButton->setIcon(icon::get("edit-clear-locationbar-ltr"));
180 connect(_keyButton, &QAbstractButton::clicked, this, &KeySequenceWidget::startRecording);
181 connect(_keyButton, &QAbstractButton::clicked, this, &KeySequenceWidget::clicked);
182 connect(_clearButton, &QAbstractButton::clicked, this, &KeySequenceWidget::clear);
183 connect(_clearButton, &QAbstractButton::clicked, this, &KeySequenceWidget::clicked);
186 void KeySequenceWidget::setModel(ShortcutsModel* model)
188 Q_ASSERT(!_shortcutsModel);
189 _shortcutsModel = model;
192 bool KeySequenceWidget::isOkWhenModifierless(int keyQt) const
194 // this whole function is a hack, but especially the first line of code
195 if (QKeySequence(keyQt).toString().length() == 1)
202 case Qt::Key_Backtab: // does this ever happen?
203 case Qt::Key_Backspace:
211 bool KeySequenceWidget::isShiftAsModifierAllowed(int keyQt) const
213 // Shift only works as a modifier with certain keys. It's not possible
214 // to enter the SHIFT+5 key sequence for me because this is handled as
215 // '%' by qt on my keyboard.
216 // The working keys are all hardcoded here :-(
217 if (keyQt >= Qt::Key_F1 && keyQt <= Qt::Key_F35)
220 if (QChar(keyQt).isLetter())
226 case Qt::Key_Backspace:
229 case Qt::Key_ScrollLock:
232 case Qt::Key_PageDown:
248 void KeySequenceWidget::updateShortcutDisplay()
250 QString s = _keySequence.toString(QKeySequence::NativeText);
251 s.replace('&', QLatin1String("&&"));
256 if (_modifierKeys & Qt::META)
257 s += QChar(kControlUnicode);
258 if (_modifierKeys & Qt::ALT)
259 s += QChar(kOptionUnicode);
260 if (_modifierKeys & Qt::SHIFT)
261 s += QChar(kShiftUnicode);
262 if (_modifierKeys & Qt::CTRL)
263 s += QChar(kCommandUnicode);
265 if (_modifierKeys & Qt::META)
266 s += tr("Meta", "Meta key") + '+';
267 if (_modifierKeys & Qt::CTRL)
268 s += tr("Ctrl", "Ctrl key") + '+';
269 if (_modifierKeys & Qt::ALT)
270 s += tr("Alt", "Alt key") + '+';
271 if (_modifierKeys & Qt::SHIFT)
272 s += tr("Shift", "Shift key") + '+';
276 s = tr("Input", "What the user inputs now will be taken as the new shortcut");
278 // make it clear that input is still going on
283 s = tr("None", "No shortcut defined");
288 _keyButton->setText(s);
291 void KeySequenceWidget::startRecording()
294 _oldKeySequence = _keySequence;
295 _keySequence = QKeySequence();
296 _conflictingIndex = QModelIndex();
298 _keyButton->grabKeyboard();
300 if (!QWidget::keyboardGrabber()) {
301 qWarning() << "Failed to grab the keyboard! Most likely qt's nograb option is active";
304 _keyButton->setDown(true);
305 updateShortcutDisplay();
308 void KeySequenceWidget::doneRecording()
310 bool wasRecording = _isRecording;
311 _isRecording = false;
312 _keyButton->releaseKeyboard();
313 _keyButton->setDown(false);
315 if (!wasRecording || _keySequence == _oldKeySequence) {
316 // The sequence hasn't changed
317 updateShortcutDisplay();
321 if (!isKeySequenceAvailable(_keySequence)) {
322 _keySequence = _oldKeySequence;
324 else if (wasRecording) {
325 emit keySequenceChanged(_keySequence, _conflictingIndex);
327 updateShortcutDisplay();
330 void KeySequenceWidget::cancelRecording()
332 _keySequence = _oldKeySequence;
336 void KeySequenceWidget::setKeySequence(const QKeySequence& seq)
338 // oldKeySequence holds the key sequence before recording started, if setKeySequence()
339 // is called while not recording then set oldKeySequence to the existing sequence so
340 // that the keySequenceChanged() signal is emitted if the new and previous key
341 // sequences are different
343 _oldKeySequence = _keySequence;
346 _clearButton->setVisible(!_keySequence.isEmpty());
350 void KeySequenceWidget::clear()
352 setKeySequence(QKeySequence());
353 // setKeySequence() won't emit a signal when we're not recording
354 emit keySequenceChanged(QKeySequence());
357 bool KeySequenceWidget::isKeySequenceAvailable(const QKeySequence& seq)
362 // We need to access the root model, not the filtered one
363 for (int cat = 0; cat < _shortcutsModel->rowCount(); cat++) {
364 QModelIndex catIdx = _shortcutsModel->index(cat, 0);
365 for (int r = 0; r < _shortcutsModel->rowCount(catIdx); r++) {
366 QModelIndex actIdx = _shortcutsModel->index(r, 0, catIdx);
367 Q_ASSERT(actIdx.isValid());
368 if (actIdx.data(ShortcutsModel::ActiveShortcutRole).value<QKeySequence>() != seq)
371 if (!actIdx.data(ShortcutsModel::IsConfigurableRole).toBool()) {
372 QMessageBox::warning(this,
373 tr("Shortcut Conflict"),
374 tr("The \"%1\" shortcut is already in use, and cannot be configured.\nPlease choose another one.")
375 .arg(seq.toString(QKeySequence::NativeText)),
380 QMessageBox box(QMessageBox::Warning,
381 tr("Shortcut Conflict"),
382 (tr("The \"%1\" shortcut is ambiguous with the shortcut for the following action:")
383 + "<br><ul><li>%2</li></ul><br>" + tr("Do you want to reassign this shortcut to the selected action?"))
384 .arg(seq.toString(QKeySequence::NativeText), actIdx.data().toString()),
387 box.addButton(tr("Reassign"), QMessageBox::AcceptRole);
388 if (box.exec() == QMessageBox::Cancel)
391 _conflictingIndex = actIdx;