modernize: Prefer default member init over ctor init
[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 <QKeyEvent>
31 #include <QHBoxLayout>
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
51
52 bool KeySequenceButton::event(QEvent *e)
53 {
54     if (d->isRecording() && e->type() == QEvent::KeyPress) {
55         keyPressEvent(static_cast<QKeyEvent *>(e));
56         return true;
57     }
58
59     // The shortcut 'alt+c' ( or any other dialog local action shortcut )
60     // ended the recording and triggered the action associated with the
61     // action. In case of 'alt+c' ending the dialog.  It seems that those
62     // ShortcutOverride events get sent even if grabKeyboard() is active.
63     if (d->isRecording() && e->type() == QEvent::ShortcutOverride) {
64         e->accept();
65         return true;
66     }
67
68     return QPushButton::event(e);
69 }
70
71
72 void KeySequenceButton::keyPressEvent(QKeyEvent *e)
73 {
74     int keyQt = e->key();
75     if (keyQt == -1) {
76         // Qt sometimes returns garbage keycodes, I observed -1, if it doesn't know a key.
77         // We cannot do anything useful with those (several keys have -1, indistinguishable)
78         // and QKeySequence.toString() will also yield a garbage string.
79         QMessageBox::information(this,
80             tr("The key you just pressed is not supported by Qt."),
81             tr("Unsupported Key"));
82         return d->cancelRecording();
83     }
84
85     uint newModifiers = e->modifiers() & (Qt::SHIFT | Qt::CTRL | Qt::ALT | Qt::META);
86
87     //don't have the return or space key appear as first key of the sequence when they
88     //were pressed to start editing - catch and them and imitate their effect
89     if (!d->isRecording() && ((keyQt == Qt::Key_Return || keyQt == Qt::Key_Space))) {
90         d->startRecording();
91         d->_modifierKeys = newModifiers;
92         d->updateShortcutDisplay();
93         return;
94     }
95
96     // We get events even if recording isn't active.
97     if (!d->isRecording())
98         return QPushButton::keyPressEvent(e);
99
100     e->accept();
101     d->_modifierKeys = newModifiers;
102
103     switch (keyQt) {
104     case Qt::Key_AltGr: //or else we get unicode salad
105         return;
106     case Qt::Key_Shift:
107     case Qt::Key_Control:
108     case Qt::Key_Alt:
109     case Qt::Key_Meta:
110     case Qt::Key_Menu: //unused (yes, but why?)
111         d->updateShortcutDisplay();
112         break;
113
114     default:
115         if (!(d->_modifierKeys & ~Qt::SHIFT)) {
116             // It's the first key and no modifier pressed. Check if this is
117             // allowed
118             if (!d->isOkWhenModifierless(keyQt))
119                 return;
120         }
121
122         // We now have a valid key press.
123         if (keyQt) {
124             if ((keyQt == Qt::Key_Backtab) && (d->_modifierKeys & Qt::SHIFT)) {
125                 keyQt = Qt::Key_Tab | d->_modifierKeys;
126             }
127             else if (d->isShiftAsModifierAllowed(keyQt)) {
128                 keyQt |= d->_modifierKeys;
129             }
130             else
131                 keyQt |= (d->_modifierKeys & ~Qt::SHIFT);
132
133             d->_keySequence = QKeySequence(keyQt);
134             d->doneRecording();
135         }
136     }
137 }
138
139
140 void KeySequenceButton::keyReleaseEvent(QKeyEvent *e)
141 {
142     if (e->key() == -1) {
143         // ignore garbage, see keyPressEvent()
144         return;
145     }
146
147     if (!d->isRecording())
148         return QPushButton::keyReleaseEvent(e);
149
150     e->accept();
151
152     uint newModifiers = e->modifiers() & (Qt::SHIFT | Qt::CTRL | Qt::ALT | Qt::META);
153
154     // if a modifier that belongs to the shortcut was released...
155     if ((newModifiers & d->_modifierKeys) < d->_modifierKeys) {
156         d->_modifierKeys = newModifiers;
157         d->updateShortcutDisplay();
158     }
159 }
160
161
162 /******************************************************************************/
163
164 KeySequenceWidget::KeySequenceWidget(QWidget *parent)
165     : QWidget(parent)
166 {
167     QHBoxLayout *layout = new QHBoxLayout(this);
168     layout->setMargin(0);
169
170     _keyButton = new KeySequenceButton(this, this);
171     _keyButton->setFocusPolicy(Qt::StrongFocus);
172     _keyButton->setIcon(icon::get("configure"));
173     _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."));
174     layout->addWidget(_keyButton);
175
176     _clearButton = new QToolButton(this);
177     layout->addWidget(_clearButton);
178
179     if (qApp->isLeftToRight())
180         _clearButton->setIcon(icon::get("edit-clear-locationbar-rtl"));
181     else
182         _clearButton->setIcon(icon::get("edit-clear-locationbar-ltr"));
183
184     setLayout(layout);
185
186     connect(_keyButton, SIGNAL(clicked()), SLOT(startRecording()));
187     connect(_keyButton, SIGNAL(clicked()), SIGNAL(clicked()));
188     connect(_clearButton, SIGNAL(clicked()), SLOT(clear()));
189     connect(_clearButton, SIGNAL(clicked()), SIGNAL(clicked()));
190 }
191
192
193 void KeySequenceWidget::setModel(ShortcutsModel *model)
194 {
195     Q_ASSERT(!_shortcutsModel);
196     _shortcutsModel = model;
197 }
198
199
200 bool KeySequenceWidget::isOkWhenModifierless(int keyQt) const
201 {
202     //this whole function is a hack, but especially the first line of code
203     if (QKeySequence(keyQt).toString().length() == 1)
204         return false;
205
206     switch (keyQt) {
207     case Qt::Key_Return:
208     case Qt::Key_Space:
209     case Qt::Key_Tab:
210     case Qt::Key_Backtab: //does this ever happen?
211     case Qt::Key_Backspace:
212     case Qt::Key_Delete:
213         return false;
214     default:
215         return true;
216     }
217 }
218
219
220 bool KeySequenceWidget::isShiftAsModifierAllowed(int keyQt) const
221 {
222     // Shift only works as a modifier with certain keys. It's not possible
223     // to enter the SHIFT+5 key sequence for me because this is handled as
224     // '%' by qt on my keyboard.
225     // The working keys are all hardcoded here :-(
226     if (keyQt >= Qt::Key_F1 && keyQt <= Qt::Key_F35)
227         return true;
228
229     if (QChar(keyQt).isLetter())
230         return true;
231
232     switch (keyQt) {
233     case Qt::Key_Return:
234     case Qt::Key_Space:
235     case Qt::Key_Backspace:
236     case Qt::Key_Escape:
237     case Qt::Key_Print:
238     case Qt::Key_ScrollLock:
239     case Qt::Key_Pause:
240     case Qt::Key_PageUp:
241     case Qt::Key_PageDown:
242     case Qt::Key_Insert:
243     case Qt::Key_Delete:
244     case Qt::Key_Home:
245     case Qt::Key_End:
246     case Qt::Key_Up:
247     case Qt::Key_Down:
248     case Qt::Key_Left:
249     case Qt::Key_Right:
250         return true;
251
252     default:
253         return false;
254     }
255 }
256
257
258 void KeySequenceWidget::updateShortcutDisplay()
259 {
260     QString s = _keySequence.toString(QKeySequence::NativeText);
261     s.replace('&', QLatin1String("&&"));
262
263     if (_isRecording) {
264         if (_modifierKeys) {
265 #ifdef Q_OS_MAC
266             if (_modifierKeys & Qt::META) s += QChar(kControlUnicode);
267             if (_modifierKeys & Qt::ALT) s += QChar(kOptionUnicode);
268             if (_modifierKeys & Qt::SHIFT) s += QChar(kShiftUnicode);
269             if (_modifierKeys & Qt::CTRL) s += QChar(kCommandUnicode);
270 #else
271             if (_modifierKeys & Qt::META) s += tr("Meta", "Meta key") + '+';
272             if (_modifierKeys & Qt::CTRL) s += tr("Ctrl", "Ctrl key") + '+';
273             if (_modifierKeys & Qt::ALT) s += tr("Alt", "Alt key") + '+';
274             if (_modifierKeys & Qt::SHIFT) s += tr("Shift", "Shift key") + '+';
275 #endif
276         }
277         else {
278             s = tr("Input", "What the user inputs now will be taken as the new shortcut");
279         }
280         // make it clear that input is still going on
281         s.append(" ...");
282     }
283
284     if (s.isEmpty()) {
285         s = tr("None", "No shortcut defined");
286     }
287
288     s.prepend(' ');
289     s.append(' ');
290     _keyButton->setText(s);
291 }
292
293
294 void KeySequenceWidget::startRecording()
295 {
296     _modifierKeys = 0;
297     _oldKeySequence = _keySequence;
298     _keySequence = QKeySequence();
299     _conflictingIndex = QModelIndex();
300     _isRecording = true;
301     _keyButton->grabKeyboard();
302
303     if (!QWidget::keyboardGrabber()) {
304         qWarning() << "Failed to grab the keyboard! Most likely qt's nograb option is active";
305     }
306
307     _keyButton->setDown(true);
308     updateShortcutDisplay();
309 }
310
311
312 void KeySequenceWidget::doneRecording()
313 {
314     bool wasRecording = _isRecording;
315     _isRecording = false;
316     _keyButton->releaseKeyboard();
317     _keyButton->setDown(false);
318
319     if (!wasRecording || _keySequence == _oldKeySequence) {
320         // The sequence hasn't changed
321         updateShortcutDisplay();
322         return;
323     }
324
325     if (!isKeySequenceAvailable(_keySequence)) {
326         _keySequence = _oldKeySequence;
327     }
328     else if (wasRecording) {
329         emit keySequenceChanged(_keySequence, _conflictingIndex);
330     }
331     updateShortcutDisplay();
332 }
333
334
335 void KeySequenceWidget::cancelRecording()
336 {
337     _keySequence = _oldKeySequence;
338     doneRecording();
339 }
340
341
342 void KeySequenceWidget::setKeySequence(const QKeySequence &seq)
343 {
344     // oldKeySequence holds the key sequence before recording started, if setKeySequence()
345     // is called while not recording then set oldKeySequence to the existing sequence so
346     // that the keySequenceChanged() signal is emitted if the new and previous key
347     // sequences are different
348     if (!isRecording())
349         _oldKeySequence = _keySequence;
350
351     _keySequence = seq;
352     _clearButton->setVisible(!_keySequence.isEmpty());
353     doneRecording();
354 }
355
356
357 void KeySequenceWidget::clear()
358 {
359     setKeySequence(QKeySequence());
360     // setKeySequence() won't emit a signal when we're not recording
361     emit keySequenceChanged(QKeySequence());
362 }
363
364
365 bool KeySequenceWidget::isKeySequenceAvailable(const QKeySequence &seq)
366 {
367     if (seq.isEmpty())
368         return true;
369
370     // We need to access the root model, not the filtered one
371     for (int cat = 0; cat < _shortcutsModel->rowCount(); cat++) {
372         QModelIndex catIdx = _shortcutsModel->index(cat, 0);
373         for (int r = 0; r < _shortcutsModel->rowCount(catIdx); r++) {
374             QModelIndex actIdx = _shortcutsModel->index(r, 0, catIdx);
375             Q_ASSERT(actIdx.isValid());
376             if (actIdx.data(ShortcutsModel::ActiveShortcutRole).value<QKeySequence>() != seq)
377                 continue;
378
379             if (!actIdx.data(ShortcutsModel::IsConfigurableRole).toBool()) {
380                 QMessageBox::warning(this, tr("Shortcut Conflict"),
381                     tr("The \"%1\" shortcut is already in use, and cannot be configured.\nPlease choose another one.").arg(seq.toString(QKeySequence::NativeText)),
382                     QMessageBox::Ok);
383                 return false;
384             }
385
386             QMessageBox box(QMessageBox::Warning, tr("Shortcut Conflict"),
387                 (tr("The \"%1\" shortcut is ambiguous with the shortcut for the following action:")
388                  + "<br><ul><li>%2</li></ul><br>"
389                  + tr("Do you want to reassign this shortcut to the selected action?")
390                 ).arg(seq.toString(QKeySequence::NativeText), actIdx.data().toString()),
391                 QMessageBox::Cancel, this);
392             box.addButton(tr("Reassign"), QMessageBox::AcceptRole);
393             if (box.exec() == QMessageBox::Cancel)
394                 return false;
395
396             _conflictingIndex = actIdx;
397             return true;
398         }
399     }
400     return true;
401 }