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 | |
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(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 | |
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 | } |
405 | |