1/***************************************************************************
2 * Copyright (C) 2005-2014 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 "iconloader.h"
43#include "keysequencewidget.h"
44
45KeySequenceButton::KeySequenceButton(KeySequenceWidget *d_, QWidget *parent)
46 : QPushButton(parent),
47 d(d_)
48{
49}
50
51
52bool 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
72void 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
140void 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
164KeySequenceWidget::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(SmallIcon("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(SmallIcon("edit-clear-locationbar-rtl"));
184 else
185 _clearButton->setIcon(SmallIcon("edit-clear-locationbar-ltr"));
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
196void KeySequenceWidget::setModel(ShortcutsModel *model)
197{
198 Q_ASSERT(!_shortcutsModel);
199 _shortcutsModel = model;
200}
201
202
203bool 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
223bool 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
261void 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
297void 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
315void 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
338void KeySequenceWidget::cancelRecording()
339{
340 _keySequence = _oldKeySequence;
341 doneRecording();
342}
343
344
345void 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
360void 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
368bool 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}
405