Add missing #include for Mac
[quassel.git] / src / qtui / settingspages / keysequencewidget.cpp
1 /***************************************************************************
2  *   Copyright (C) 2010 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  *   59 Temple Place - Suite 330, Boston, MA  02111-1307, 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_WS_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   if(d->isRecording() && e->type() == QEvent::KeyPress) {
54     keyPressEvent(static_cast<QKeyEvent *>(e));
55     return true;
56   }
57
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) {
63     e->accept();
64     return true;
65   }
66
67   return QPushButton::event(e);
68 }
69
70 void KeySequenceButton::keyPressEvent(QKeyEvent *e) {
71   int keyQt = e->key();
72   if(keyQt == -1) {
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();
80   }
81
82   uint newModifiers = e->modifiers() & (Qt::SHIFT | Qt::CTRL | Qt::ALT | Qt::META);
83
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))) {
87     d->startRecording();
88     d->_modifierKeys = newModifiers;
89     d->updateShortcutDisplay();
90     return;
91   }
92
93   // We get events even if recording isn't active.
94   if(!d->isRecording())
95     return QPushButton::keyPressEvent(e);
96
97   e->accept();
98   d->_modifierKeys = newModifiers;
99
100   switch(keyQt) {
101   case Qt::Key_AltGr: //or else we get unicode salad
102     return;
103   case Qt::Key_Shift:
104   case Qt::Key_Control:
105   case Qt::Key_Alt:
106   case Qt::Key_Meta:
107   case Qt::Key_Menu: //unused (yes, but why?)
108     d->updateShortcutDisplay();
109     break;
110
111   default:
112     if(!(d->_modifierKeys & ~Qt::SHIFT)) {
113       // It's the first key and no modifier pressed. Check if this is
114       // allowed
115       if(!d->isOkWhenModifierless(keyQt))
116         return;
117     }
118
119     // We now have a valid key press.
120     if(keyQt) {
121       if((keyQt == Qt::Key_Backtab) && (d->_modifierKeys & Qt::SHIFT)) {
122         keyQt = Qt::Key_Tab | d->_modifierKeys;
123       }
124       else if(d->isShiftAsModifierAllowed(keyQt)) {
125         keyQt |= d->_modifierKeys;
126       } else
127         keyQt |= (d->_modifierKeys & ~Qt::SHIFT);
128
129       d->_keySequence = QKeySequence(keyQt);
130       d->doneRecording();
131     }
132   }
133 }
134
135 void KeySequenceButton::keyReleaseEvent(QKeyEvent *e) {
136   if(e->key() == -1) {
137     // ignore garbage, see keyPressEvent()
138     return;
139   }
140
141   if(!d->isRecording())
142     return QPushButton::keyReleaseEvent(e);
143
144   e->accept();
145
146   uint newModifiers = e->modifiers() & (Qt::SHIFT | Qt::CTRL | Qt::ALT | Qt::META);
147
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();
152   }
153 }
154
155 /******************************************************************************/
156
157 KeySequenceWidget::KeySequenceWidget(QWidget *parent)
158   : QWidget(parent),
159   _shortcutsModel(0),
160   _isRecording(false),
161   _modifierKeys(0)
162 {
163   QHBoxLayout *layout = new QHBoxLayout(this);
164   layout->setMargin(0);
165
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);
171
172   _clearButton = new QToolButton(this);
173   layout->addWidget(_clearButton);
174
175   if(qApp->isLeftToRight())
176     _clearButton->setIcon(SmallIcon("edit-clear-locationbar-rtl"));
177   else
178     _clearButton->setIcon(SmallIcon("edit-clear-locationbar-ltr"));
179
180   setLayout(layout);
181
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()));
186 }
187
188 void KeySequenceWidget::setModel(ShortcutsModel *model) {
189   Q_ASSERT(!_shortcutsModel);
190   _shortcutsModel = model;
191 }
192
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)
196     return false;
197
198   switch(keyQt) {
199   case Qt::Key_Return:
200   case Qt::Key_Space:
201   case Qt::Key_Tab:
202   case Qt::Key_Backtab: //does this ever happen?
203   case Qt::Key_Backspace:
204   case Qt::Key_Delete:
205     return false;
206   default:
207     return true;
208   }
209 }
210
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)
217     return true;
218
219   if(QChar(keyQt).isLetter())
220     return true;
221
222   switch(keyQt) {
223   case Qt::Key_Return:
224   case Qt::Key_Space:
225   case Qt::Key_Backspace:
226   case Qt::Key_Escape:
227   case Qt::Key_Print:
228   case Qt::Key_ScrollLock:
229   case Qt::Key_Pause:
230   case Qt::Key_PageUp:
231   case Qt::Key_PageDown:
232   case Qt::Key_Insert:
233   case Qt::Key_Delete:
234   case Qt::Key_Home:
235   case Qt::Key_End:
236   case Qt::Key_Up:
237   case Qt::Key_Down:
238   case Qt::Key_Left:
239   case Qt::Key_Right:
240     return true;
241
242   default:
243     return false;
244   }
245 }
246
247 void KeySequenceWidget::updateShortcutDisplay() {
248   QString s = _keySequence.toString(QKeySequence::NativeText);
249   s.replace('&', QLatin1String("&&"));
250
251   if(_isRecording) {
252     if(_modifierKeys) {
253 #ifdef Q_WS_MAC
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);
258 #else
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") + '+';
263 #endif
264     } else {
265       s = tr("Input", "What the user inputs now will be taken as the new shortcut");
266     }
267     // make it clear that input is still going on
268     s.append(" ...");
269   }
270
271   if(s.isEmpty()) {
272     s = tr("None", "No shortcut defined");
273   }
274
275   s.prepend(' ');
276   s.append(' ');
277   _keyButton->setText(s);
278 }
279
280 void KeySequenceWidget::startRecording() {
281   _modifierKeys = 0;
282   _oldKeySequence = _keySequence;
283   _keySequence = QKeySequence();
284   _conflictingIndex = QModelIndex();
285   _isRecording = true;
286   _keyButton->grabKeyboard();
287
288   if(!QWidget::keyboardGrabber()) {
289     qWarning() << "Failed to grab the keyboard! Most likely qt's nograb option is active";
290   }
291
292   _keyButton->setDown(true);
293   updateShortcutDisplay();
294 }
295
296
297 void KeySequenceWidget::doneRecording() {
298   bool wasRecording = _isRecording;
299   _isRecording = false;
300   _keyButton->releaseKeyboard();
301   _keyButton->setDown(false);
302
303   if(!wasRecording || _keySequence == _oldKeySequence) {
304     // The sequence hasn't changed
305     updateShortcutDisplay();
306     return;
307   }
308
309   if(!isKeySequenceAvailable(_keySequence)) {
310     _keySequence = _oldKeySequence;
311   } else if(wasRecording) {
312     emit keySequenceChanged(_keySequence, _conflictingIndex);
313   }
314   updateShortcutDisplay();
315 }
316
317 void KeySequenceWidget::cancelRecording() {
318   _keySequence = _oldKeySequence;
319   doneRecording();
320 }
321
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
327   if(!isRecording())
328     _oldKeySequence = _keySequence;
329
330   _keySequence = seq;
331   _clearButton->setVisible(!_keySequence.isEmpty());
332   doneRecording();
333 }
334
335 void KeySequenceWidget::clear() {
336   setKeySequence(QKeySequence());
337   // setKeySequence() won't emit a signal when we're not recording
338   emit keySequenceChanged(QKeySequence());
339 }
340
341 bool KeySequenceWidget::isKeySequenceAvailable(const QKeySequence &seq) {
342   if(seq.isEmpty())
343     return true;
344
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)
352         continue;
353
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)),
357                              QMessageBox::Ok);
358         return false;
359       }
360
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)
369         return false;
370
371       _conflictingIndex = actIdx;
372       return true;
373     }
374   }
375   return true;
376 }