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