1 /***************************************************************************
2 * Copyright (C) 2010 by the Quassel Project *
3 * devel@quassel-irc.org *
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> *
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. *
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. *
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 * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
26 ***************************************************************************/
28 #include <QApplication>
31 #include <QHBoxLayout>
32 #include <QMessageBox>
33 #include <QToolButton>
35 // This defines the unicode symbols for special keys (kCommandUnicode and friends)
37 # include <Carbon/Carbon.h>
41 #include "actioncollection.h"
42 #include "iconloader.h"
43 #include "keysequencewidget.h"
45 KeySequenceButton::KeySequenceButton(KeySequenceWidget *d_, QWidget *parent)
46 : QPushButton(parent),
52 bool KeySequenceButton::event(QEvent *e) {
53 if(d->isRecording() && e->type() == QEvent::KeyPress) {
54 keyPressEvent(static_cast<QKeyEvent *>(e));
58 // The shortcut 'alt+c' ( or any other dialog local action shortcut )
59 // ended the recording and triggered the action associated with the
60 // action. In case of 'alt+c' ending the dialog. It seems that those
61 // ShortcutOverride events get sent even if grabKeyboard() is active.
62 if(d->isRecording() && e->type() == QEvent::ShortcutOverride) {
67 return QPushButton::event(e);
70 void KeySequenceButton::keyPressEvent(QKeyEvent *e) {
73 // Qt sometimes returns garbage keycodes, I observed -1, if it doesn't know a key.
74 // We cannot do anything useful with those (several keys have -1, indistinguishable)
75 // and QKeySequence.toString() will also yield a garbage string.
76 QMessageBox::information(this,
77 tr("The key you just pressed is not supported by Qt."),
78 tr("Unsupported Key"));
79 return d->cancelRecording();
82 uint newModifiers = e->modifiers() & (Qt::SHIFT | Qt::CTRL | Qt::ALT | Qt::META);
84 //don't have the return or space key appear as first key of the sequence when they
85 //were pressed to start editing - catch and them and imitate their effect
86 if(!d->isRecording() && ((keyQt == Qt::Key_Return || keyQt == Qt::Key_Space))) {
88 d->_modifierKeys = newModifiers;
89 d->updateShortcutDisplay();
93 // We get events even if recording isn't active.
95 return QPushButton::keyPressEvent(e);
98 d->_modifierKeys = newModifiers;
101 case Qt::Key_AltGr: //or else we get unicode salad
104 case Qt::Key_Control:
107 case Qt::Key_Menu: //unused (yes, but why?)
108 d->updateShortcutDisplay();
112 if(!(d->_modifierKeys & ~Qt::SHIFT)) {
113 // It's the first key and no modifier pressed. Check if this is
115 if(!d->isOkWhenModifierless(keyQt))
119 // We now have a valid key press.
121 if((keyQt == Qt::Key_Backtab) && (d->_modifierKeys & Qt::SHIFT)) {
122 keyQt = Qt::Key_Tab | d->_modifierKeys;
124 else if(d->isShiftAsModifierAllowed(keyQt)) {
125 keyQt |= d->_modifierKeys;
127 keyQt |= (d->_modifierKeys & ~Qt::SHIFT);
129 d->_keySequence = QKeySequence(keyQt);
135 void KeySequenceButton::keyReleaseEvent(QKeyEvent *e) {
137 // ignore garbage, see keyPressEvent()
141 if(!d->isRecording())
142 return QPushButton::keyReleaseEvent(e);
146 uint newModifiers = e->modifiers() & (Qt::SHIFT | Qt::CTRL | Qt::ALT | Qt::META);
148 // if a modifier that belongs to the shortcut was released...
149 if((newModifiers & d->_modifierKeys) < d->_modifierKeys) {
150 d->_modifierKeys = newModifiers;
151 d->updateShortcutDisplay();
155 /******************************************************************************/
157 KeySequenceWidget::KeySequenceWidget(QWidget *parent)
163 QHBoxLayout *layout = new QHBoxLayout(this);
164 layout->setMargin(0);
166 _keyButton = new KeySequenceButton(this, this);
167 _keyButton->setFocusPolicy(Qt::StrongFocus);
168 _keyButton->setIcon(SmallIcon("configure"));
169 _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."));
170 layout->addWidget(_keyButton);
172 _clearButton = new QToolButton(this);
173 layout->addWidget(_clearButton);
175 if(qApp->isLeftToRight())
176 _clearButton->setIcon(SmallIcon("edit-clear-locationbar-rtl"));
178 _clearButton->setIcon(SmallIcon("edit-clear-locationbar-ltr"));
182 connect(_keyButton, SIGNAL(clicked()), SLOT(startRecording()));
183 connect(_keyButton, SIGNAL(clicked()), SIGNAL(clicked()));
184 connect(_clearButton, SIGNAL(clicked()), SLOT(clear()));
185 connect(_clearButton, SIGNAL(clicked()), SIGNAL(clicked()));
188 void KeySequenceWidget::setModel(ShortcutsModel *model) {
189 Q_ASSERT(!_shortcutsModel);
190 _shortcutsModel = model;
193 bool KeySequenceWidget::isOkWhenModifierless(int keyQt) const {
194 //this whole function is a hack, but especially the first line of code
195 if(QKeySequence(keyQt).toString().length() == 1)
202 case Qt::Key_Backtab: //does this ever happen?
203 case Qt::Key_Backspace:
211 bool KeySequenceWidget::isShiftAsModifierAllowed(int keyQt) const {
212 // Shift only works as a modifier with certain keys. It's not possible
213 // to enter the SHIFT+5 key sequence for me because this is handled as
214 // '%' by qt on my keyboard.
215 // The working keys are all hardcoded here :-(
216 if(keyQt >= Qt::Key_F1 && keyQt <= Qt::Key_F35)
219 if(QChar(keyQt).isLetter())
225 case Qt::Key_Backspace:
228 case Qt::Key_ScrollLock:
231 case Qt::Key_PageDown:
247 void KeySequenceWidget::updateShortcutDisplay() {
248 QString s = _keySequence.toString(QKeySequence::NativeText);
249 s.replace('&', QLatin1String("&&"));
254 if(_modifierKeys & Qt::META) s += QChar(kControlUnicode);
255 if(_modifierKeys & Qt::ALT) s += QChar(kOptionUnicode);
256 if(_modifierKeys & Qt::SHIFT) s += QChar(kShiftUnicode);
257 if(_modifierKeys & Qt::CTRL) s += QChar(kCommandUnicode);
259 if(_modifierKeys & Qt::META) s += tr("Meta", "Meta key") + '+';
260 if(_modifierKeys & Qt::CTRL) s += tr("Ctrl", "Ctrl key") + '+';
261 if(_modifierKeys & Qt::ALT) s += tr("Alt", "Alt key") + '+';
262 if(_modifierKeys & Qt::SHIFT) s += tr("Shift", "Shift key") + '+';
265 s = tr("Input", "What the user inputs now will be taken as the new shortcut");
267 // make it clear that input is still going on
272 s = tr("None", "No shortcut defined");
277 _keyButton->setText(s);
280 void KeySequenceWidget::startRecording() {
282 _oldKeySequence = _keySequence;
283 _keySequence = QKeySequence();
284 _conflictingIndex = QModelIndex();
286 _keyButton->grabKeyboard();
288 if(!QWidget::keyboardGrabber()) {
289 qWarning() << "Failed to grab the keyboard! Most likely qt's nograb option is active";
292 _keyButton->setDown(true);
293 updateShortcutDisplay();
297 void KeySequenceWidget::doneRecording() {
298 bool wasRecording = _isRecording;
299 _isRecording = false;
300 _keyButton->releaseKeyboard();
301 _keyButton->setDown(false);
303 if(!wasRecording || _keySequence == _oldKeySequence) {
304 // The sequence hasn't changed
305 updateShortcutDisplay();
309 if(!isKeySequenceAvailable(_keySequence)) {
310 _keySequence = _oldKeySequence;
311 } else if(wasRecording) {
312 emit keySequenceChanged(_keySequence, _conflictingIndex);
314 updateShortcutDisplay();
317 void KeySequenceWidget::cancelRecording() {
318 _keySequence = _oldKeySequence;
322 void KeySequenceWidget::setKeySequence(const QKeySequence &seq) {
323 // oldKeySequence holds the key sequence before recording started, if setKeySequence()
324 // is called while not recording then set oldKeySequence to the existing sequence so
325 // that the keySequenceChanged() signal is emitted if the new and previous key
326 // sequences are different
328 _oldKeySequence = _keySequence;
331 _clearButton->setVisible(!_keySequence.isEmpty());
335 void KeySequenceWidget::clear() {
336 setKeySequence(QKeySequence());
337 // setKeySequence() won't emit a signal when we're not recording
338 emit keySequenceChanged(QKeySequence());
341 bool KeySequenceWidget::isKeySequenceAvailable(const QKeySequence &seq) {
345 // We need to access the root model, not the filtered one
346 for(int cat = 0; cat < _shortcutsModel->rowCount(); cat++) {
347 QModelIndex catIdx = _shortcutsModel->index(cat, 0);
348 for(int r = 0; r < _shortcutsModel->rowCount(catIdx); r++) {
349 QModelIndex actIdx = _shortcutsModel->index(r, 0, catIdx);
350 Q_ASSERT(actIdx.isValid());
351 if(actIdx.data(ShortcutsModel::ActiveShortcutRole).value<QKeySequence>() != seq)
354 if(!actIdx.data(ShortcutsModel::IsConfigurableRole).toBool()) {
355 QMessageBox::warning(this, tr("Shortcut Conflict"),
356 tr("The \"%1\" shortcut is already in use, and cannot be configured.\nPlease choose another one.").arg(seq.toString(QKeySequence::NativeText)),
361 QMessageBox box(QMessageBox::Warning, tr("Shortcut Conflict"),
362 (tr("The \"%1\" shortcut is ambiguous with the shortcut for the following action:")
363 + "<br><ul><li>%2</li></ul><br>"
364 + tr("Do you want to reassign this shortcut to the selected action?")
365 ).arg(seq.toString(QKeySequence::NativeText), actIdx.data().toString()),
366 QMessageBox::Cancel, this);
367 box.addButton(tr("Reassign"), QMessageBox::AcceptRole);
368 if(box.exec() == QMessageBox::Cancel)
371 _conflictingIndex = actIdx;