Quassel IRC  Pre-Release
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros
keysequencewidget.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  * Copyright (C) 2005-2015 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 
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 
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;
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?)
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 
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;
158  }
159 }
160 
161 
162 /******************************************************************************/
163 
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 
197 {
198  Q_ASSERT(!_shortcutsModel);
199  _shortcutsModel = model;
200 }
201 
202 
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 
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 
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 
298 {
299  _modifierKeys = 0;
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);
312 }
313 
314 
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
325  return;
326  }
327 
330  }
331  else if (wasRecording) {
333  }
335 }
336 
337 
339 {
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())
353 
354  _keySequence = seq;
355  _clearButton->setVisible(!_keySequence.isEmpty());
356  doneRecording();
357 }
358 
359 
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 }