Make Breeze the default icon theme
[quassel.git] / src / qtui / settingspages / keysequencewidget.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2016 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 <QIcon>
33 #include <QMessageBox>
34 #include <QToolButton>
35
36 // This defines the unicode symbols for special keys (kCommandUnicode and friends)
37 #ifdef Q_OS_MAC
38 #  include <Carbon/Carbon.h>
39 #endif
40
41 #include "action.h"
42 #include "actioncollection.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     _shortcutsModel(0),
167     _isRecording(false),
168     _modifierKeys(0)
169 {
170     QHBoxLayout *layout = new QHBoxLayout(this);
171     layout->setMargin(0);
172
173     _keyButton = new KeySequenceButton(this, this);
174     _keyButton->setFocusPolicy(Qt::StrongFocus);
175     _keyButton->setIcon(QIcon::fromTheme("configure"));
176     _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."));
177     layout->addWidget(_keyButton);
178
179     _clearButton = new QToolButton(this);
180     layout->addWidget(_clearButton);
181
182     if (qApp->isLeftToRight())
183         _clearButton->setIcon(QIcon::fromTheme("edit-clear-locationbar-rtl", QIcon::fromTheme("edit-clear")));
184     else
185         _clearButton->setIcon(QIcon::fromTheme("edit-clear-locationbar-ltr", QIcon::fromTheme("edit-clear")));
186
187     setLayout(layout);
188
189     connect(_keyButton, SIGNAL(clicked()), SLOT(startRecording()));
190     connect(_keyButton, SIGNAL(clicked()), SIGNAL(clicked()));
191     connect(_clearButton, SIGNAL(clicked()), SLOT(clear()));
192     connect(_clearButton, SIGNAL(clicked()), SIGNAL(clicked()));
193 }
194
195
196 void KeySequenceWidget::setModel(ShortcutsModel *model)
197 {
198     Q_ASSERT(!_shortcutsModel);
199     _shortcutsModel = model;
200 }
201
202
203 bool KeySequenceWidget::isOkWhenModifierless(int keyQt) const
204 {
205     //this whole function is a hack, but especially the first line of code
206     if (QKeySequence(keyQt).toString().length() == 1)
207         return false;
208
209     switch (keyQt) {
210     case Qt::Key_Return:
211     case Qt::Key_Space:
212     case Qt::Key_Tab:
213     case Qt::Key_Backtab: //does this ever happen?
214     case Qt::Key_Backspace:
215     case Qt::Key_Delete:
216         return false;
217     default:
218         return true;
219     }
220 }
221
222
223 bool KeySequenceWidget::isShiftAsModifierAllowed(int keyQt) const
224 {
225     // Shift only works as a modifier with certain keys. It's not possible
226     // to enter the SHIFT+5 key sequence for me because this is handled as
227     // '%' by qt on my keyboard.
228     // The working keys are all hardcoded here :-(
229     if (keyQt >= Qt::Key_F1 && keyQt <= Qt::Key_F35)
230         return true;
231
232     if (QChar(keyQt).isLetter())
233         return true;
234
235     switch (keyQt) {
236     case Qt::Key_Return:
237     case Qt::Key_Space:
238     case Qt::Key_Backspace:
239     case Qt::Key_Escape:
240     case Qt::Key_Print:
241     case Qt::Key_ScrollLock:
242     case Qt::Key_Pause:
243     case Qt::Key_PageUp:
244     case Qt::Key_PageDown:
245     case Qt::Key_Insert:
246     case Qt::Key_Delete:
247     case Qt::Key_Home:
248     case Qt::Key_End:
249     case Qt::Key_Up:
250     case Qt::Key_Down:
251     case Qt::Key_Left:
252     case Qt::Key_Right:
253         return true;
254
255     default:
256         return false;
257     }
258 }
259
260
261 void KeySequenceWidget::updateShortcutDisplay()
262 {
263     QString s = _keySequence.toString(QKeySequence::NativeText);
264     s.replace('&', QLatin1String("&&"));
265
266     if (_isRecording) {
267         if (_modifierKeys) {
268 #ifdef Q_OS_MAC
269             if (_modifierKeys & Qt::META) s += QChar(kControlUnicode);
270             if (_modifierKeys & Qt::ALT) s += QChar(kOptionUnicode);
271             if (_modifierKeys & Qt::SHIFT) s += QChar(kShiftUnicode);
272             if (_modifierKeys & Qt::CTRL) s += QChar(kCommandUnicode);
273 #else
274             if (_modifierKeys & Qt::META) s += tr("Meta", "Meta key") + '+';
275             if (_modifierKeys & Qt::CTRL) s += tr("Ctrl", "Ctrl key") + '+';
276             if (_modifierKeys & Qt::ALT) s += tr("Alt", "Alt key") + '+';
277             if (_modifierKeys & Qt::SHIFT) s += tr("Shift", "Shift key") + '+';
278 #endif
279         }
280         else {
281             s = tr("Input", "What the user inputs now will be taken as the new shortcut");
282         }
283         // make it clear that input is still going on
284         s.append(" ...");
285     }
286
287     if (s.isEmpty()) {
288         s = tr("None", "No shortcut defined");
289     }
290
291     s.prepend(' ');
292     s.append(' ');
293     _keyButton->setText(s);
294 }
295
296
297 void KeySequenceWidget::startRecording()
298 {
299     _modifierKeys = 0;
300     _oldKeySequence = _keySequence;
301     _keySequence = QKeySequence();
302     _conflictingIndex = QModelIndex();
303     _isRecording = true;
304     _keyButton->grabKeyboard();
305
306     if (!QWidget::keyboardGrabber()) {
307         qWarning() << "Failed to grab the keyboard! Most likely qt's nograb option is active";
308     }
309
310     _keyButton->setDown(true);
311     updateShortcutDisplay();
312 }
313
314
315 void KeySequenceWidget::doneRecording()
316 {
317     bool wasRecording = _isRecording;
318     _isRecording = false;
319     _keyButton->releaseKeyboard();
320     _keyButton->setDown(false);
321
322     if (!wasRecording || _keySequence == _oldKeySequence) {
323         // The sequence hasn't changed
324         updateShortcutDisplay();
325         return;
326     }
327
328     if (!isKeySequenceAvailable(_keySequence)) {
329         _keySequence = _oldKeySequence;
330     }
331     else if (wasRecording) {
332         emit keySequenceChanged(_keySequence, _conflictingIndex);
333     }
334     updateShortcutDisplay();
335 }
336
337
338 void KeySequenceWidget::cancelRecording()
339 {
340     _keySequence = _oldKeySequence;
341     doneRecording();
342 }
343
344
345 void KeySequenceWidget::setKeySequence(const QKeySequence &seq)
346 {
347     // oldKeySequence holds the key sequence before recording started, if setKeySequence()
348     // is called while not recording then set oldKeySequence to the existing sequence so
349     // that the keySequenceChanged() signal is emitted if the new and previous key
350     // sequences are different
351     if (!isRecording())
352         _oldKeySequence = _keySequence;
353
354     _keySequence = seq;
355     _clearButton->setVisible(!_keySequence.isEmpty());
356     doneRecording();
357 }
358
359
360 void KeySequenceWidget::clear()
361 {
362     setKeySequence(QKeySequence());
363     // setKeySequence() won't emit a signal when we're not recording
364     emit keySequenceChanged(QKeySequence());
365 }
366
367
368 bool KeySequenceWidget::isKeySequenceAvailable(const QKeySequence &seq)
369 {
370     if (seq.isEmpty())
371         return true;
372
373     // We need to access the root model, not the filtered one
374     for (int cat = 0; cat < _shortcutsModel->rowCount(); cat++) {
375         QModelIndex catIdx = _shortcutsModel->index(cat, 0);
376         for (int r = 0; r < _shortcutsModel->rowCount(catIdx); r++) {
377             QModelIndex actIdx = _shortcutsModel->index(r, 0, catIdx);
378             Q_ASSERT(actIdx.isValid());
379             if (actIdx.data(ShortcutsModel::ActiveShortcutRole).value<QKeySequence>() != seq)
380                 continue;
381
382             if (!actIdx.data(ShortcutsModel::IsConfigurableRole).toBool()) {
383                 QMessageBox::warning(this, tr("Shortcut Conflict"),
384                     tr("The \"%1\" shortcut is already in use, and cannot be configured.\nPlease choose another one.").arg(seq.toString(QKeySequence::NativeText)),
385                     QMessageBox::Ok);
386                 return false;
387             }
388
389             QMessageBox box(QMessageBox::Warning, tr("Shortcut Conflict"),
390                 (tr("The \"%1\" shortcut is ambiguous with the shortcut for the following action:")
391                  + "<br><ul><li>%2</li></ul><br>"
392                  + tr("Do you want to reassign this shortcut to the selected action?")
393                 ).arg(seq.toString(QKeySequence::NativeText), actIdx.data().toString()),
394                 QMessageBox::Cancel, this);
395             box.addButton(tr("Reassign"), QMessageBox::AcceptRole);
396             if (box.exec() == QMessageBox::Cancel)
397                 return false;
398
399             _conflictingIndex = actIdx;
400             return true;
401         }
402     }
403     return true;
404 }