1 /*
2  * SPDX-FileCopyrightText: 2013~2020 CSSlayer <wengxt@gmail.com>
3  *
4  * This library is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU Lesser General Public License as
6  * published by the Free Software Foundation; either version 2 of the
7  * License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12  * Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with this library; see the file COPYING. If not,
16  * see <http://www.gnu.org/licenses/>.
17  */
18 
19 /* this is forked from kdelibs/kdeui/kkeysequencewidget.cpp */
20 
21 /*
22     Original Copyright header
23     SPDX-FileCopyrightText: 1998 Mark Donohoe <donohoe@kde.org>
24     SPDX-FileCopyrightText: 2001 Ellis Whitehead <ellis@kde.org>
25     SPDX-FileCopyrightText: 2007 Andreas Hartmetz <ahartmetz@gmail.com>
26 
27     This library is free software; you can redistribute it and/or
28     modify it under the terms of the GNU Library General Public
29     License as published by the Free Software Foundation; either
30     version 2 of the License, or (at your option) any later version.
31 
32     This library is distributed in the hope that it will be useful,
33     but WITHOUT ANY WARRANTY; without even the implied warranty of
34     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
35     Library General Public License for more details.
36 
37     You should have received a copy of the GNU Library General Public License
38     along with this library; see the file COPYING.LIB.  If not, write to
39     the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
40     Boston, MA 02110-1301, USA.
41 */
42 
43 #include "fcitxqtkeysequencewidget.h"
44 #include "fcitxqtkeysequencewidget_p.h"
45 
46 #include "qtkeytrans.h"
47 #include <QApplication>
48 #include <QHBoxLayout>
49 #include <QHash>
50 #include <QKeyEvent>
51 #include <QLoggingCategory>
52 #include <QMessageBox>
53 #include <QTimer>
54 #include <QToolButton>
55 #include <fcitx-utils/i18n.h>
56 #include <fcitx-utils/key.h>
57 
58 Q_LOGGING_CATEGORY(fcitx5qtKeysequenceWidget, "fcitx5.qt.keysequencewidget")
59 
60 namespace fcitx {
61 
62 namespace {
63 
isX11LikePlatform()64 bool isX11LikePlatform() {
65     return QGuiApplication::platformName() == "xcb" ||
66            QGuiApplication::platformName().startsWith("wayland");
67 }
68 
keyQtToFcitx(int keyQt,const QString & text,FcitxQtModifierSide side,Key & outkey)69 bool keyQtToFcitx(int keyQt, const QString &text, FcitxQtModifierSide side,
70                   Key &outkey) {
71     int key = keyQt & (~Qt::KeyboardModifierMask);
72     int state = keyQt & Qt::KeyboardModifierMask;
73     int sym;
74     unsigned int states;
75     if (!keyQtToSym(key, Qt::KeyboardModifiers(state), text, sym, states)) {
76         return false;
77     }
78     if (side == MS_Right) {
79         switch (sym) {
80         case FcitxKey_Control_L:
81             sym = FcitxKey_Control_R;
82             break;
83         case FcitxKey_Alt_L:
84             sym = FcitxKey_Alt_R;
85             break;
86         case FcitxKey_Shift_L:
87             sym = FcitxKey_Shift_R;
88             break;
89         case FcitxKey_Super_L:
90             sym = FcitxKey_Super_R;
91             break;
92         }
93     }
94 
95     outkey = Key(static_cast<KeySym>(sym), KeyStates(states));
96     return true;
97 }
98 
99 } // namespace
100 
101 class FcitxQtKeySequenceWidgetPrivate {
102 public:
103     FcitxQtKeySequenceWidgetPrivate(FcitxQtKeySequenceWidget *q);
104 
105     void init();
106 
107     static bool isOkWhenModifierless(int keyQt);
108 
109     void updateShortcutDisplay();
110     void startRecording();
111 
controlModifierlessTimout()112     void controlModifierlessTimout() {
113         if (keySequence_.size() != 0 && !modifierKeys_) {
114             // No modifier key pressed currently. Start the timout
115             modifierlessTimeout_.start(600);
116         } else {
117             // A modifier is pressed. Stop the timeout
118             modifierlessTimeout_.stop();
119         }
120     }
121 
cancelRecording()122     void cancelRecording() {
123         keySequence_ = oldKeySequence_;
124         doneRecording();
125     }
126 
127     // private slot
128     void doneRecording();
129 
130     // members
131     FcitxQtKeySequenceWidget *const q;
132     QHBoxLayout *layout_;
133     FcitxQtKeySequenceButton *keyButton_;
134     QToolButton *clearButton_;
135     QAction *keyCodeModeAction_;
136 
137     QList<Key> keySequence_;
138     QList<Key> oldKeySequence_;
139     QTimer modifierlessTimeout_;
140     bool allowModifierless_;
141     KeyStates modifierKeys_;
142     unsigned int qtModifierKeys_ = 0;
143     bool isRecording_;
144     bool multiKeyShortcutsAllowed_;
145     bool allowModifierOnly_;
146 };
147 
FcitxQtKeySequenceWidgetPrivate(FcitxQtKeySequenceWidget * q)148 FcitxQtKeySequenceWidgetPrivate::FcitxQtKeySequenceWidgetPrivate(
149     FcitxQtKeySequenceWidget *q)
150     : q(q), layout_(nullptr), keyButton_(nullptr), clearButton_(nullptr),
151       keyCodeModeAction_(nullptr), allowModifierless_(false), modifierKeys_(0),
152       isRecording_(false), multiKeyShortcutsAllowed_(false),
153       allowModifierOnly_(false) {}
154 
FcitxQtKeySequenceWidget(QWidget * parent)155 FcitxQtKeySequenceWidget::FcitxQtKeySequenceWidget(QWidget *parent)
156     : QWidget(parent), d(new FcitxQtKeySequenceWidgetPrivate(this)) {
157     d->init();
158     setFocusProxy(d->keyButton_);
159     connect(d->keyButton_, &QPushButton::clicked, this,
160             &FcitxQtKeySequenceWidget::captureKeySequence);
161     connect(d->clearButton_, &QPushButton::clicked, this,
162             &FcitxQtKeySequenceWidget::clearKeySequence);
163     connect(&d->modifierlessTimeout_, &QTimer::timeout, this,
164             [this]() { d->doneRecording(); });
165     d->updateShortcutDisplay();
166 }
167 
init()168 void FcitxQtKeySequenceWidgetPrivate::init() {
169     layout_ = new QHBoxLayout(q);
170     layout_->setMargin(0);
171 
172     keyButton_ = new FcitxQtKeySequenceButton(this, q);
173     keyButton_->setFocusPolicy(Qt::StrongFocus);
174     keyButton_->setIcon(QIcon::fromTheme("configure"));
175     layout_->addWidget(keyButton_);
176 
177     clearButton_ = new QToolButton(q);
178     layout_->addWidget(clearButton_);
179 
180     keyCodeModeAction_ = new QAction(_("Key code mode"));
181     keyCodeModeAction_->setCheckable(true);
182     keyCodeModeAction_->setEnabled(isX11LikePlatform());
183     q->setContextMenuPolicy(Qt::ActionsContextMenu);
184     q->addAction(keyCodeModeAction_);
185 
186     if (qApp->isLeftToRight())
187         clearButton_->setIcon(QIcon::fromTheme("edit-clear-locationbar-rtl"));
188     else
189         clearButton_->setIcon(QIcon::fromTheme("edit-clear-locationbar-ltr"));
190 
191     q->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
192 }
193 
~FcitxQtKeySequenceWidget()194 FcitxQtKeySequenceWidget::~FcitxQtKeySequenceWidget() { delete d; }
195 
multiKeyShortcutsAllowed() const196 bool FcitxQtKeySequenceWidget::multiKeyShortcutsAllowed() const {
197     return d->multiKeyShortcutsAllowed_;
198 }
199 
setMultiKeyShortcutsAllowed(bool allowed)200 void FcitxQtKeySequenceWidget::setMultiKeyShortcutsAllowed(bool allowed) {
201     d->multiKeyShortcutsAllowed_ = allowed;
202 }
203 
setModifierlessAllowed(bool allow)204 void FcitxQtKeySequenceWidget::setModifierlessAllowed(bool allow) {
205     d->allowModifierless_ = allow;
206 }
207 
isModifierlessAllowed()208 bool FcitxQtKeySequenceWidget::isModifierlessAllowed() {
209     return d->allowModifierless_;
210 }
211 
isModifierOnlyAllowed()212 bool FcitxQtKeySequenceWidget::isModifierOnlyAllowed() {
213     return d->allowModifierOnly_;
214 }
215 
setModifierOnlyAllowed(bool allow)216 void FcitxQtKeySequenceWidget::setModifierOnlyAllowed(bool allow) {
217     d->allowModifierOnly_ = allow;
218 }
219 
setClearButtonShown(bool show)220 void FcitxQtKeySequenceWidget::setClearButtonShown(bool show) {
221     d->clearButton_->setVisible(show);
222 }
223 
224 // slot
captureKeySequence()225 void FcitxQtKeySequenceWidget::captureKeySequence() { d->startRecording(); }
226 
keySequence() const227 const QList<Key> &FcitxQtKeySequenceWidget::keySequence() const {
228     return d->keySequence_;
229 }
230 
231 // slot
setKeySequence(const QList<Key> & seq)232 void FcitxQtKeySequenceWidget::setKeySequence(const QList<Key> &seq) {
233     // oldKeySequence holds the key sequence before recording started, if
234     // setKeySequence()
235     // is called while not recording then set oldKeySequence to the existing
236     // sequence so
237     // that the keySequenceChanged() signal is emitted if the new and previous
238     // key
239     // sequences are different
240     if (!d->isRecording_) {
241         d->oldKeySequence_ = d->keySequence_;
242     }
243 
244     d->keySequence_ = QList<Key>();
245     for (auto key : seq) {
246         if (key.isValid()) {
247             d->keySequence_ << key;
248         }
249     }
250     d->doneRecording();
251 }
252 
253 // slot
clearKeySequence()254 void FcitxQtKeySequenceWidget::clearKeySequence() {
255     setKeySequence(QList<Key>());
256 }
257 
startRecording()258 void FcitxQtKeySequenceWidgetPrivate::startRecording() {
259     modifierKeys_ = 0;
260     oldKeySequence_ = keySequence_;
261     keySequence_ = QList<Key>();
262     isRecording_ = true;
263     keyButton_->grabKeyboard();
264 
265     if (!QWidget::keyboardGrabber()) {
266         qWarning() << "Failed to grab the keyboard! Most likely qt's nograb "
267                       "option is active";
268     }
269 
270     keyButton_->setDown(true);
271     updateShortcutDisplay();
272 }
273 
doneRecording()274 void FcitxQtKeySequenceWidgetPrivate::doneRecording() {
275     modifierlessTimeout_.stop();
276     isRecording_ = false;
277     keyButton_->releaseKeyboard();
278     keyButton_->setDown(false);
279 
280     if (keySequence_ == oldKeySequence_ && !allowModifierOnly_) {
281         // The sequence hasn't changed
282         updateShortcutDisplay();
283         return;
284     }
285 
286     Q_EMIT q->keySequenceChanged(keySequence_);
287 
288     updateShortcutDisplay();
289 }
290 
updateShortcutDisplay()291 void FcitxQtKeySequenceWidgetPrivate::updateShortcutDisplay() {
292     QString s = QString::fromUtf8(
293         Key::keyListToString(keySequence_, KeyStringFormat::Localized).c_str());
294     s.replace('&', QLatin1String("&&"));
295 
296     if (isRecording_) {
297         if (modifierKeys_) {
298             if (!s.isEmpty())
299                 s.append(",");
300             if (modifierKeys_ & KeyState::Super)
301                 s += "Super+";
302             if (modifierKeys_ & KeyState::Ctrl)
303                 s += "Control+";
304             if (modifierKeys_ & KeyState::Alt)
305                 s += "Alt+";
306             if (modifierKeys_ & KeyState::Shift)
307                 s += "Shift+";
308             if (modifierKeys_ & KeyState::Hyper)
309                 s += "Hyper+";
310 
311         } else if (keySequence_.size() == 0) {
312             s = "...";
313         }
314         // make it clear that input is still going on
315         s.append(" ...");
316     }
317 
318     if (s.isEmpty()) {
319         s = _("Empty");
320     }
321 
322     s.prepend(' ');
323     s.append(' ');
324     keyButton_->setText(s);
325 }
326 
~FcitxQtKeySequenceButton()327 FcitxQtKeySequenceButton::~FcitxQtKeySequenceButton() {}
328 
329 // prevent Qt from special casing Tab and Backtab
event(QEvent * e)330 bool FcitxQtKeySequenceButton::event(QEvent *e) {
331     if (d->isRecording_ && e->type() == QEvent::KeyPress) {
332         keyPressEvent(static_cast<QKeyEvent *>(e));
333         return true;
334     }
335 
336     // The shortcut 'alt+c' ( or any other dialog local action shortcut )
337     // ended the recording and triggered the action associated with the
338     // action. In case of 'alt+c' ending the dialog.  It seems that those
339     // ShortcutOverride events get sent even if grabKeyboard() is active.
340     if (d->isRecording_ && e->type() == QEvent::ShortcutOverride) {
341         e->accept();
342         return true;
343     }
344 
345     return QPushButton::event(e);
346 }
347 
keyPressEvent(QKeyEvent * e)348 void FcitxQtKeySequenceButton::keyPressEvent(QKeyEvent *e) {
349     int keyQt = e->key();
350     if (keyQt == -1) {
351         // Qt sometimes returns garbage keycodes, I observed -1, if it doesn't
352         // know a key. We cannot do anything useful with those (several keys
353         // have -1, indistinguishable) and QKeySequence.toString() will also
354         // yield a garbage string.
355         QMessageBox::warning(
356             this, _("The key you just pressed is not supported by Qt."),
357             _("Unsupported Key"));
358         return d->cancelRecording();
359     }
360 
361     // Same as Key::normalize();
362     unsigned int newQtModifiers =
363         e->modifiers() & (Qt::META | Qt::ALT | Qt::CTRL | Qt::SHIFT);
364     KeyStates newModifiers;
365     if (isX11LikePlatform()) {
366         newModifiers = KeyStates(e->nativeModifiers()) &
367                        KeyStates{KeyState::Ctrl_Alt_Shift, KeyState::Hyper,
368                                  KeyState::Super};
369         newModifiers |=
370             Key::keySymToStates(static_cast<KeySym>(e->nativeVirtualKey()));
371     } else {
372         if (newQtModifiers & Qt::META) {
373             newModifiers |= KeyState::Super;
374         }
375         if (newQtModifiers & Qt::ALT) {
376             newModifiers |= KeyState::Alt;
377         }
378         if (newQtModifiers & Qt::CTRL) {
379             newModifiers |= KeyState::Ctrl;
380         }
381         if (newQtModifiers & Qt::SHIFT) {
382             newModifiers |= KeyState::Shift;
383         }
384     }
385 
386     // don't have the return or space key appear as first key of the sequence
387     // when they
388     // were pressed to start editing - catch and them and imitate their effect
389     if (!d->isRecording_ &&
390         ((keyQt == Qt::Key_Return || keyQt == Qt::Key_Space))) {
391         d->startRecording();
392         d->modifierKeys_ = newModifiers;
393         d->qtModifierKeys_ = newQtModifiers;
394         d->updateShortcutDisplay();
395         return;
396     }
397 
398     // We get events even if recording isn't active.
399     if (!d->isRecording_)
400         return QPushButton::keyPressEvent(e);
401 
402     e->accept();
403     d->modifierKeys_ = newModifiers;
404     d->qtModifierKeys_ = newQtModifiers;
405 
406     switch (keyQt) {
407     case Qt::Key_AltGr: // or else we get unicode salad
408         return;
409     case Qt::Key_Shift:
410     case Qt::Key_Control:
411     case Qt::Key_Alt:
412     case Qt::Key_Super_L:
413     case Qt::Key_Super_R:
414     case Qt::Key_Hyper_L:
415     case Qt::Key_Hyper_R:
416     case Qt::Key_Meta:
417     case Qt::Key_Menu: // unused (yes, but why?)
418         d->controlModifierlessTimout();
419         d->updateShortcutDisplay();
420         break;
421     default:
422         // We now have a valid key press.
423         if (keyQt) {
424             if ((keyQt == Qt::Key_Backtab) &&
425                 d->modifierKeys_.test(KeyState::Shift)) {
426                 keyQt = Qt::Key_Tab | d->qtModifierKeys_;
427             } else {
428                 keyQt |= d->qtModifierKeys_;
429             }
430 
431             Key key;
432             if (d->keyCodeModeAction_->isChecked()) {
433                 key = Key::fromKeyCode(e->nativeScanCode(), key.states());
434             } else {
435                 if (isX11LikePlatform()) {
436                     key = Key(static_cast<KeySym>(e->nativeVirtualKey()),
437                               KeyStates(e->nativeModifiers()))
438                               .normalize();
439                 } else {
440                     if (!keyQtToFcitx(keyQt, e->text(), MS_Unknown, key)) {
441                         qCDebug(fcitx5qtKeysequenceWidget)
442                             << "FcitxQtKeySequenceButton::keyPressEvent() "
443                                "Failed to "
444                                "convert Qt key to fcitx: "
445                             << e;
446                     }
447                 }
448             }
449 
450             if (d->keySequence_.size() == 0 && !d->allowModifierless_ &&
451                 key.states() == 0) {
452                 return;
453             }
454 
455             if (key.isValid()) {
456                 d->keySequence_ << key;
457             }
458 
459             if ((!d->multiKeyShortcutsAllowed_) ||
460                 (d->keySequence_.size() >= 4)) {
461                 d->doneRecording();
462                 return;
463             }
464             d->controlModifierlessTimout();
465             d->updateShortcutDisplay();
466         }
467     }
468 }
469 
keyReleaseEvent(QKeyEvent * e)470 void FcitxQtKeySequenceButton::keyReleaseEvent(QKeyEvent *e) {
471     if (e->key() == -1) {
472         // ignore garbage, see keyPressEvent()
473         return;
474     }
475 
476     if (!d->isRecording_)
477         return QPushButton::keyReleaseEvent(e);
478 
479     e->accept();
480 
481     if (!d->multiKeyShortcutsAllowed_ && d->allowModifierOnly_ &&
482         (e->key() == Qt::Key_Shift || e->key() == Qt::Key_Control ||
483          e->key() == Qt::Key_Meta || e->key() == Qt::Key_Alt)) {
484         auto side = MS_Unknown;
485 
486         if (isX11LikePlatform()) {
487 
488             if (e->nativeVirtualKey() == FcitxKey_Control_L ||
489                 e->nativeVirtualKey() == FcitxKey_Alt_L ||
490                 e->nativeVirtualKey() == FcitxKey_Shift_L ||
491                 e->nativeVirtualKey() == FcitxKey_Super_L) {
492                 side = MS_Left;
493             }
494             if (e->nativeVirtualKey() == FcitxKey_Control_R ||
495                 e->nativeVirtualKey() == FcitxKey_Alt_R ||
496                 e->nativeVirtualKey() == FcitxKey_Shift_R ||
497                 e->nativeVirtualKey() == FcitxKey_Super_R) {
498                 side = MS_Right;
499             }
500         }
501         int keyQt = e->key() | d->qtModifierKeys_;
502         Key key;
503         if (keyQtToFcitx(keyQt, e->text(), side, key)) {
504             if (d->keyCodeModeAction_->isChecked()) {
505                 key = Key::fromKeyCode(e->nativeScanCode(), key.states());
506             }
507             d->keySequence_ = QList<Key>({key});
508         }
509         d->doneRecording();
510         return;
511     }
512 
513     unsigned int newQtModifiers =
514         e->modifiers() & (Qt::META | Qt::ALT | Qt::CTRL | Qt::SHIFT);
515     KeyStates newModifiers;
516     if (isX11LikePlatform()) {
517         newModifiers = KeyStates(e->nativeModifiers()) &
518                        KeyStates{KeyState::Ctrl_Alt_Shift, KeyState::Hyper,
519                                  KeyState::Super};
520         newModifiers &=
521             ~Key::keySymToStates(static_cast<KeySym>(e->nativeVirtualKey()));
522     } else {
523         if (newQtModifiers & Qt::META) {
524             newModifiers |= KeyState::Super;
525         }
526         if (newQtModifiers & Qt::ALT) {
527             newModifiers |= KeyState::Alt;
528         }
529         if (newQtModifiers & Qt::CTRL) {
530             newModifiers |= KeyState::Ctrl;
531         }
532         if (newQtModifiers & Qt::SHIFT) {
533             newModifiers |= KeyState::Shift;
534         }
535     }
536 
537     // if a modifier that belongs to the shortcut was released...
538     if ((newModifiers & d->modifierKeys_) < d->modifierKeys_) {
539         d->modifierKeys_ = newModifiers;
540         d->controlModifierlessTimout();
541         d->updateShortcutDisplay();
542     }
543 }
544 
545 // static
isOkWhenModifierless(int keyQt)546 bool FcitxQtKeySequenceWidgetPrivate::isOkWhenModifierless(int keyQt) {
547     // this whole function is a hack, but especially the first line of code
548     if (QKeySequence(keyQt).toString().length() == 1)
549         return false;
550 
551     switch (keyQt) {
552     case Qt::Key_Return:
553     case Qt::Key_Space:
554     case Qt::Key_Tab:
555     case Qt::Key_Backtab: // does this ever happen?
556     case Qt::Key_Backspace:
557     case Qt::Key_Delete:
558         return false;
559     default:
560         return true;
561     }
562 }
563 } // namespace fcitx
564 
565 #include "moc_fcitxqtkeysequencewidget.cpp"
566