modernize: Reformat ALL the source... again!
[quassel.git] / src / qtui / settingspages / keysequencewidget.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2018 by the Quassel Project                        *
3  *   devel@quassel-irc.org                                                 *
4  *                                                                         *
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>         *
11  *                                                                         *
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.                                   *
16  *                                                                         *
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.                          *
21  *                                                                         *
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  ***************************************************************************/
27
28 #include <QApplication>
29 #include <QDebug>
30 #include <QHBoxLayout>
31 #include <QKeyEvent>
32 #include <QMessageBox>
33 #include <QToolButton>
34
35 // This defines the unicode symbols for special keys (kCommandUnicode and friends)
36 #ifdef Q_OS_MAC
37 #    include <Carbon/Carbon.h>
38 #endif
39
40 #include "action.h"
41 #include "actioncollection.h"
42 #include "icon.h"
43 #include "keysequencewidget.h"
44
45 KeySequenceButton::KeySequenceButton(KeySequenceWidget* d_, QWidget* parent)
46     : QPushButton(parent)
47     , d(d_)
48 {}
49
50 bool KeySequenceButton::event(QEvent* e)
51 {
52     if (d->isRecording() && e->type() == QEvent::KeyPress) {
53         keyPressEvent(static_cast<QKeyEvent*>(e));
54         return true;
55     }
56
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) {
62         e->accept();
63         return true;
64     }
65
66     return QPushButton::event(e);
67 }
68
69 void KeySequenceButton::keyPressEvent(QKeyEvent* e)
70 {
71     int keyQt = e->key();
72     if (keyQt == -1) {
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();
78     }
79
80     uint newModifiers = e->modifiers() & (Qt::SHIFT | Qt::CTRL | Qt::ALT | Qt::META);
81
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))) {
85         d->startRecording();
86         d->_modifierKeys = newModifiers;
87         d->updateShortcutDisplay();
88         return;
89     }
90
91     // We get events even if recording isn't active.
92     if (!d->isRecording())
93         return QPushButton::keyPressEvent(e);
94
95     e->accept();
96     d->_modifierKeys = newModifiers;
97
98     switch (keyQt) {
99     case Qt::Key_AltGr:  // or else we get unicode salad
100         return;
101     case Qt::Key_Shift:
102     case Qt::Key_Control:
103     case Qt::Key_Alt:
104     case Qt::Key_Meta:
105     case Qt::Key_Menu:  // unused (yes, but why?)
106         d->updateShortcutDisplay();
107         break;
108
109     default:
110         if (!(d->_modifierKeys & ~Qt::SHIFT)) {
111             // It's the first key and no modifier pressed. Check if this is
112             // allowed
113             if (!d->isOkWhenModifierless(keyQt))
114                 return;
115         }
116
117         // We now have a valid key press.
118         if (keyQt) {
119             if ((keyQt == Qt::Key_Backtab) && (d->_modifierKeys & Qt::SHIFT)) {
120                 keyQt = Qt::Key_Tab | d->_modifierKeys;
121             }
122             else if (d->isShiftAsModifierAllowed(keyQt)) {
123                 keyQt |= d->_modifierKeys;
124             }
125             else
126                 keyQt |= (d->_modifierKeys & ~Qt::SHIFT);
127
128             d->_keySequence = QKeySequence(keyQt);
129             d->doneRecording();
130         }
131     }
132 }
133
134 void KeySequenceButton::keyReleaseEvent(QKeyEvent* e)
135 {
136     if (e->key() == -1) {
137         // ignore garbage, see keyPressEvent()
138         return;
139     }
140
141     if (!d->isRecording())
142         return QPushButton::keyReleaseEvent(e);
143
144     e->accept();
145
146     uint newModifiers = e->modifiers() & (Qt::SHIFT | Qt::CTRL | Qt::ALT | Qt::META);
147
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();
152     }
153 }
154
155 /******************************************************************************/
156
157 KeySequenceWidget::KeySequenceWidget(QWidget* parent)
158     : QWidget(parent)
159 {
160     auto* layout = new QHBoxLayout(this);
161     layout->setMargin(0);
162
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);
169
170     _clearButton = new QToolButton(this);
171     layout->addWidget(_clearButton);
172
173     if (qApp->isLeftToRight())
174         _clearButton->setIcon(icon::get("edit-clear-locationbar-rtl"));
175     else
176         _clearButton->setIcon(icon::get("edit-clear-locationbar-ltr"));
177
178     setLayout(layout);
179
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);
184 }
185
186 void KeySequenceWidget::setModel(ShortcutsModel* model)
187 {
188     Q_ASSERT(!_shortcutsModel);
189     _shortcutsModel = model;
190 }
191
192 bool KeySequenceWidget::isOkWhenModifierless(int keyQt) const
193 {
194     // this whole function is a hack, but especially the first line of code
195     if (QKeySequence(keyQt).toString().length() == 1)
196         return false;
197
198     switch (keyQt) {
199     case Qt::Key_Return:
200     case Qt::Key_Space:
201     case Qt::Key_Tab:
202     case Qt::Key_Backtab:  // does this ever happen?
203     case Qt::Key_Backspace:
204     case Qt::Key_Delete:
205         return false;
206     default:
207         return true;
208     }
209 }
210
211 bool KeySequenceWidget::isShiftAsModifierAllowed(int keyQt) const
212 {
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)
218         return true;
219
220     if (QChar(keyQt).isLetter())
221         return true;
222
223     switch (keyQt) {
224     case Qt::Key_Return:
225     case Qt::Key_Space:
226     case Qt::Key_Backspace:
227     case Qt::Key_Escape:
228     case Qt::Key_Print:
229     case Qt::Key_ScrollLock:
230     case Qt::Key_Pause:
231     case Qt::Key_PageUp:
232     case Qt::Key_PageDown:
233     case Qt::Key_Insert:
234     case Qt::Key_Delete:
235     case Qt::Key_Home:
236     case Qt::Key_End:
237     case Qt::Key_Up:
238     case Qt::Key_Down:
239     case Qt::Key_Left:
240     case Qt::Key_Right:
241         return true;
242
243     default:
244         return false;
245     }
246 }
247
248 void KeySequenceWidget::updateShortcutDisplay()
249 {
250     QString s = _keySequence.toString(QKeySequence::NativeText);
251     s.replace('&', QLatin1String("&&"));
252
253     if (_isRecording) {
254         if (_modifierKeys) {
255 #ifdef Q_OS_MAC
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);
264 #else
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") + '+';
273 #endif
274         }
275         else {
276             s = tr("Input", "What the user inputs now will be taken as the new shortcut");
277         }
278         // make it clear that input is still going on
279         s.append(" ...");
280     }
281
282     if (s.isEmpty()) {
283         s = tr("None", "No shortcut defined");
284     }
285
286     s.prepend(' ');
287     s.append(' ');
288     _keyButton->setText(s);
289 }
290
291 void KeySequenceWidget::startRecording()
292 {
293     _modifierKeys = 0;
294     _oldKeySequence = _keySequence;
295     _keySequence = QKeySequence();
296     _conflictingIndex = QModelIndex();
297     _isRecording = true;
298     _keyButton->grabKeyboard();
299
300     if (!QWidget::keyboardGrabber()) {
301         qWarning() << "Failed to grab the keyboard! Most likely qt's nograb option is active";
302     }
303
304     _keyButton->setDown(true);
305     updateShortcutDisplay();
306 }
307
308 void KeySequenceWidget::doneRecording()
309 {
310     bool wasRecording = _isRecording;
311     _isRecording = false;
312     _keyButton->releaseKeyboard();
313     _keyButton->setDown(false);
314
315     if (!wasRecording || _keySequence == _oldKeySequence) {
316         // The sequence hasn't changed
317         updateShortcutDisplay();
318         return;
319     }
320
321     if (!isKeySequenceAvailable(_keySequence)) {
322         _keySequence = _oldKeySequence;
323     }
324     else if (wasRecording) {
325         emit keySequenceChanged(_keySequence, _conflictingIndex);
326     }
327     updateShortcutDisplay();
328 }
329
330 void KeySequenceWidget::cancelRecording()
331 {
332     _keySequence = _oldKeySequence;
333     doneRecording();
334 }
335
336 void KeySequenceWidget::setKeySequence(const QKeySequence& seq)
337 {
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
342     if (!isRecording())
343         _oldKeySequence = _keySequence;
344
345     _keySequence = seq;
346     _clearButton->setVisible(!_keySequence.isEmpty());
347     doneRecording();
348 }
349
350 void KeySequenceWidget::clear()
351 {
352     setKeySequence(QKeySequence());
353     // setKeySequence() won't emit a signal when we're not recording
354     emit keySequenceChanged(QKeySequence());
355 }
356
357 bool KeySequenceWidget::isKeySequenceAvailable(const QKeySequence& seq)
358 {
359     if (seq.isEmpty())
360         return true;
361
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)
369                 continue;
370
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)),
376                                      QMessageBox::Ok);
377                 return false;
378             }
379
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()),
385                             QMessageBox::Cancel,
386                             this);
387             box.addButton(tr("Reassign"), QMessageBox::AcceptRole);
388             if (box.exec() == QMessageBox::Cancel)
389                 return false;
390
391             _conflictingIndex = actIdx;
392             return true;
393         }
394     }
395     return true;
396 }