1 // Copyright 2010-2018, Google Inc.
2 // All rights reserved.
3 //
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions are
6 // met:
7 //
8 // * Redistributions of source code must retain the above copyright
9 // notice, this list of conditions and the following disclaimer.
10 // * Redistributions in binary form must reproduce the above
11 // copyright notice, this list of conditions and the following disclaimer
12 // in the documentation and/or other materials provided with the
13 // distribution.
14 // * Neither the name of Google Inc. nor the names of its
15 // contributors may be used to endorse or promote products derived from
16 // this software without specific prior written permission.
17 //
18 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30 #include "gui/config_dialog/keybinding_editor.h"
31
32 #if defined(OS_ANDROID) || defined(OS_NACL)
33 #error "This platform is not supported."
34 #endif // OS_ANDROID || OS_NACL
35
36 #ifdef OS_WIN
37 #include <windows.h>
38 #include <imm.h>
39 #include <ime.h>
40 #elif OS_LINUX
41 #define XK_MISCELLANY
42 #include <X11/keysymdef.h>
43 #endif
44
45 #include <QtCore/QString>
46 #include <QtWidgets/QMessageBox>
47 #include <QtWidgets/QMenu>
48 #include <QtWidgets/QPushButton>
49 #include <QtWidgets/QTableWidget>
50
51 #include "base/logging.h"
52 #include "base/util.h"
53
54 namespace mozc {
55 namespace gui {
56
57 namespace {
58 struct QtKeyEntry {
59 Qt::Key qt_key;
60 const char *mozc_key_name;
61 };
62
63 // TODO(taku): check it these mappings are correct.
64 const QtKeyEntry kQtKeyModifierNonRequiredTable[] = {
65 { Qt::Key_Escape, "Escape" },
66 { Qt::Key_Tab, "Tab" },
67 { Qt::Key_Backtab, "Tab" }, // Qt handles Tab + Shift as a special key
68 { Qt::Key_Backspace, "Backspace" },
69 { Qt::Key_Return, "Enter" },
70 { Qt::Key_Enter, "Enter" },
71 { Qt::Key_Insert, "Insert" },
72 { Qt::Key_Delete, "Delete" },
73 { Qt::Key_Home, "Home" },
74 { Qt::Key_End, "End" },
75 { Qt::Key_Left, "Left" },
76 { Qt::Key_Up, "Up" },
77 { Qt::Key_Right, "Right" },
78 { Qt::Key_Down, "Down" },
79 { Qt::Key_PageUp, "PageUp" },
80 { Qt::Key_PageDown, "PageDown" },
81 { Qt::Key_Space, "Space" },
82 { Qt::Key_F1, "F1" },
83 { Qt::Key_F2, "F2" },
84 { Qt::Key_F3, "F3" },
85 { Qt::Key_F4, "F4" },
86 { Qt::Key_F5, "F5" },
87 { Qt::Key_F6, "F6" },
88 { Qt::Key_F7, "F7" },
89 { Qt::Key_F8, "F8" },
90 { Qt::Key_F9, "F9" },
91 { Qt::Key_F10, "F10" },
92 { Qt::Key_F11, "F11" },
93 { Qt::Key_F12, "F12" },
94 { Qt::Key_F13, "F13" },
95 { Qt::Key_F14, "F14" },
96 { Qt::Key_F15, "F15" },
97 { Qt::Key_F16, "F16" },
98 { Qt::Key_F17, "F17" },
99 { Qt::Key_F18, "F18" },
100 { Qt::Key_F19, "F19" },
101 { Qt::Key_F20, "F20" },
102 { Qt::Key_F21, "F21" },
103 { Qt::Key_F22, "F22" },
104 { Qt::Key_F23, "F23" },
105 { Qt::Key_F24, "F24" }
106 };
107
108 #ifdef OS_WIN
109 struct WinVirtualKeyEntry {
110 DWORD virtual_key;
111 const char *mozc_key_name;
112 };
113
114 const WinVirtualKeyEntry kWinVirtualKeyModifierNonRequiredTable[] = {
115 // { VK_DBE_HIRAGANA, "Kana" }, // Kana
116 // "Hiragana" and "Kana" are the same key on Mozc
117 { VK_DBE_HIRAGANA, "Hiragana" }, // Hiragana
118 { VK_DBE_KATAKANA, "Katakana" }, // Ktakana
119 { VK_DBE_ALPHANUMERIC, "Eisu" }, // Eisu
120 // TODO(taku): better to support Romaji key
121 // { VK_DBE_ROMAN, "Romaji" }, // Romaji
122 // { VK_DBE_NOROMAN, "Romaji" }, // Romaji
123 { VK_NONCONVERT, "Muhenkan" }, // Muhenkan
124 { VK_CONVERT, "Henkan" }, // Henkan
125 // JP109's Hankaku/Zenkaku key has two V_KEY for toggling IME-On and Off.
126 // Althogh these are visible keys on 109JP, Mozc doesn't support them.
127 { VK_DBE_SBCSCHAR, "Hankaku/Zenkaku" }, // Zenkaku/hankaku
128 { VK_DBE_DBCSCHAR, "Hankaku/Zenkaku" }, // Zenkaku/hankaku
129 // { VK_KANJI, "Kanji" }, // Do not support Kanji
130 };
131 #elif OS_LINUX
132 struct LinuxVirtualKeyEntry {
133 uint16 virtual_key;
134 const char *mozc_key_name;
135 };
136
137 const LinuxVirtualKeyEntry kLinuxVirtualKeyModifierNonRequiredTable[] = {
138 { XK_Muhenkan, "Muhenkan" },
139 { XK_Henkan, "Henkan" },
140 { XK_Hiragana, "Hiragana" },
141 { XK_Katakana, "Katakana" },
142 // We need special hack for Hiragana_Katakana key. For the detail, please see
143 // KeyBindingFilter::AddKey implementation.
144 { XK_Hiragana_Katakana, "Hiragana" },
145 { XK_Eisu_toggle, "Eisu" },
146 { XK_Zenkaku_Hankaku, "Hankaku/Zenkaku" },
147 };
148 #endif
149
150 // On Windows Hiragana/Eisu keys only emits KEY_DOWN event.
151 // for these keys we don't hanlde auto-key repeat.
IsDownOnlyKey(const QKeyEvent & key_event)152 bool IsDownOnlyKey(const QKeyEvent &key_event) {
153 #ifdef OS_WIN
154 const DWORD virtual_key = key_event.nativeVirtualKey();
155 return (virtual_key == VK_DBE_ALPHANUMERIC ||
156 virtual_key == VK_DBE_HIRAGANA ||
157 virtual_key == VK_DBE_KATAKANA);
158 #else
159 return false;
160 #endif // OS_WIN
161 }
162
IsAlphabet(const char key)163 bool IsAlphabet(const char key) {
164 return (key >= 'a' && key <= 'z');
165 }
166 } // namespace
167
168 class KeyBindingFilter : public QObject {
169 public:
170 KeyBindingFilter(QLineEdit *line_edit, QPushButton *ok_button);
171 virtual ~KeyBindingFilter();
172
173 enum KeyState {
174 DENY_KEY,
175 ACCEPT_KEY,
176 SUBMIT_KEY
177 };
178
179 protected:
180 bool eventFilter(QObject *obj, QEvent *event);
181
182 private:
183 void Reset();
184
185 // add new "qt_key" to the filter.
186 // return true if the current key_bindings the KeyBindingFilter holds
187 // is valid. Composed key_bindings are stored to "result"
188 KeyState AddKey(const QKeyEvent &key_event, QString *result);
189
190 // encode the current key binding
191 KeyState Encode(QString *result) const;
192
193 bool committed_;
194 bool ctrl_pressed_;
195 bool alt_pressed_;
196 bool shift_pressed_;
197 QString modifier_required_key_;
198 QString modifier_non_required_key_;
199 QString unknown_key_;
200 QLineEdit *line_edit_;
201 QPushButton *ok_button_;
202 };
203
KeyBindingFilter(QLineEdit * line_edit,QPushButton * ok_button)204 KeyBindingFilter::KeyBindingFilter(QLineEdit *line_edit,
205 QPushButton *ok_button)
206 : committed_(false),
207 ctrl_pressed_(false),
208 alt_pressed_(false),
209 shift_pressed_(false),
210 line_edit_(line_edit),
211 ok_button_(ok_button) {
212 Reset();
213 }
214
~KeyBindingFilter()215 KeyBindingFilter::~KeyBindingFilter() {}
216
Reset()217 void KeyBindingFilter::Reset() {
218 ctrl_pressed_ = false;
219 alt_pressed_ = false;
220 shift_pressed_ = false;
221 modifier_required_key_.clear();
222 modifier_non_required_key_.clear();
223 unknown_key_.clear();
224 committed_ = true;
225 ok_button_->setEnabled(false);
226 }
227
Encode(QString * result) const228 KeyBindingFilter::KeyState KeyBindingFilter::Encode(QString *result) const {
229 CHECK(result);
230
231 // We don't accept any modifier keys for Hiragana, Eisu, Hankaku/Zenkaku keys.
232 // On Windows, KEY_UP event is not raised for Hiragana/Eisu keys
233 // until alternative keys (e.g., Eisu for Hiragana and Hiragana for Eisu)
234 // are pressed. If Hiragana/Eisu key is pressed, we assume that
235 // the key is already released at the same time.
236 // Hankaku/Zenkaku key is preserved key and modifier keys are ignored.
237 if (modifier_non_required_key_ == "Hiragana" ||
238 modifier_non_required_key_ == "Katakana" ||
239 modifier_non_required_key_ == "Eisu" ||
240 modifier_non_required_key_ == "Hankaku/Zenkaku") {
241 *result = modifier_non_required_key_;
242 return KeyBindingFilter::SUBMIT_KEY;
243 }
244
245 QStringList results;
246
247 if (ctrl_pressed_) {
248 results << "Ctrl";
249 }
250
251 if (shift_pressed_) {
252 results << "Shift";
253 }
254
255 if (alt_pressed_) {
256 #ifdef OS_MACOSX
257 results << "Option";
258 #else
259 // Do not support and show keybindings with alt for Windows
260 // results << "Alt";
261 #endif
262 }
263
264 const bool has_modifier = !results.isEmpty();
265
266 if (!modifier_non_required_key_.isEmpty()) {
267 results << modifier_non_required_key_;
268 }
269
270 if (!modifier_required_key_.isEmpty()) {
271 results << modifier_required_key_;
272 }
273
274 // in release binary, unknown_key_ is hidden
275 #ifndef NO_LOGGING
276 if (!unknown_key_.isEmpty()) {
277 results << unknown_key_;
278 }
279 #endif
280
281 KeyBindingFilter::KeyState result_state = KeyBindingFilter::ACCEPT_KEY;
282
283 if (!unknown_key_.isEmpty()) {
284 result_state = KeyBindingFilter::DENY_KEY;
285 }
286
287 const char key = modifier_required_key_.isEmpty() ?
288 0 : modifier_required_key_[0].toLatin1();
289
290 // Alt or Ctrl or these combinations
291 if ((alt_pressed_ || ctrl_pressed_) &&
292 modifier_non_required_key_.isEmpty() &&
293 modifier_required_key_.isEmpty()) {
294 result_state = KeyBindingFilter::DENY_KEY;
295 }
296
297 // TODO(taku) Shift + 3 ("#" on US-keyboard) is also valid
298 // keys, but we disable it for now, since we have no way
299 // to get the original key "3" from "#" only with Qt layer.
300 // need to see platform dependent scan code here.
301
302 // Don't support Shift only
303 // Shift in composition is set to EDIT_INSERT by default.
304 // Now we do not make the keybindings for EDIT_INSERT configurable.
305 // For avoiding complexity, we do not support Shift here.
306 if (shift_pressed_ && !ctrl_pressed_ && !alt_pressed_ &&
307 modifier_required_key_.isEmpty() &&
308 modifier_non_required_key_.isEmpty()) {
309 result_state = KeyBindingFilter::DENY_KEY;
310 }
311
312 // Don't support Shift + 'a' only
313 if (shift_pressed_ && !ctrl_pressed_ && !alt_pressed_ &&
314 !modifier_required_key_.isEmpty() && IsAlphabet(key)) {
315 result_state = KeyBindingFilter::DENY_KEY;
316 }
317
318 // Don't support Shift + Ctrl + '@'
319 if (shift_pressed_ && !modifier_required_key_.isEmpty() &&
320 !IsAlphabet(key)) {
321 result_state = KeyBindingFilter::DENY_KEY;
322 }
323
324 // no modifer for modifier_required_key
325 if (!has_modifier && !modifier_required_key_.isEmpty()) {
326 result_state = KeyBindingFilter::DENY_KEY;
327 }
328
329 // modifier_required_key and modifier_non_required_key
330 // cannot co-exist
331 if (!modifier_required_key_.isEmpty() &&
332 !modifier_non_required_key_.isEmpty()) {
333 result_state = KeyBindingFilter::DENY_KEY;
334 }
335
336 // no valid key
337 if (results.empty()) {
338 result_state = KeyBindingFilter::DENY_KEY;
339 }
340
341 *result = results.join(" ");
342
343 return result_state;
344 }
345
AddKey(const QKeyEvent & key_event,QString * result)346 KeyBindingFilter::KeyState KeyBindingFilter::AddKey(
347 const QKeyEvent &key_event, QString *result) {
348 CHECK(result);
349 result->clear();
350
351 const int qt_key = key_event.key();
352
353 // modifier keys
354 switch (qt_key) {
355 #ifdef OS_MACOSX
356 case Qt::Key_Meta:
357 ctrl_pressed_ = true;
358 return Encode(result);
359 case Qt::Key_Alt: // Option key
360 // case Qt::Key_Control: Command key
361 alt_pressed_ = true;
362 return Encode(result);
363 #else
364 case Qt::Key_Control:
365 ctrl_pressed_ = true;
366 return Encode(result);
367 // case Qt::Key_Meta: // Windows key
368 case Qt::Key_Alt:
369 alt_pressed_ = true;
370 return Encode(result);
371 #endif
372 case Qt::Key_Shift:
373 shift_pressed_ = true;
374 return Encode(result);
375 default:
376 break;
377 }
378
379 // non-printable command, which doesn't require modifier keys
380 for (size_t i = 0; i < arraysize(kQtKeyModifierNonRequiredTable); ++i) {
381 if (kQtKeyModifierNonRequiredTable[i].qt_key == qt_key) {
382 modifier_non_required_key_ =
383 kQtKeyModifierNonRequiredTable[i].mozc_key_name;
384 return Encode(result);
385 }
386 }
387
388 #ifdef OS_WIN
389 // Handle JP109's Muhenkan/Henkan/katakana-hiragana and Zenkaku/Hankaku
390 const DWORD virtual_key = key_event.nativeVirtualKey();
391 for (size_t i = 0; i < arraysize(kWinVirtualKeyModifierNonRequiredTable);
392 ++i) {
393 if (kWinVirtualKeyModifierNonRequiredTable[i].virtual_key ==
394 virtual_key) {
395 modifier_non_required_key_ =
396 kWinVirtualKeyModifierNonRequiredTable[i].mozc_key_name;
397 return Encode(result);
398 }
399 }
400 #elif OS_LINUX
401 const uint16 virtual_key = key_event.nativeVirtualKey();
402
403 // The XKB defines three types of logical key code: "xkb::Hiragana",
404 // "xkb::Katakana" and "xkb::Hiragana_Katakana".
405 // On most of Linux distributions, any key event against physical
406 // "ひらがな/カタカナ" key is likely to be mapped into
407 // "xkb::Hiragana_Katakana" regardless of the state of shift modifier. This
408 // means that you are likely to receive "Shift + xkb::Hiragana_Katakana"
409 // rather than "xkb::Katakana" when you physically press Shift +
410 // "ひらがな/カタカナ".
411 // On the other hand, Mozc protocol expects that Shift + "ひらがな/カタカナ"
412 // key event is always interpret as "{special_key: KeyEvent::KATAKANA}"
413 // without shift modifier. This is why we have the following special treatment
414 // against "shift + XK_Hiragana_Katakana". See b/6087341 for the background
415 // information.
416 // We use |key_event.modifiers()| instead of |shift_pressed_| because
417 // |shift_pressed_| is no longer valid in the following scenario.
418 // 1. Press "Shift"
419 // 2. Press "Hiragana/Katakana" (shift_pressed_ == true)
420 // 3. Press "Hiragana/Katakana" (shift_pressed_ == false)
421 const bool with_shift = (key_event.modifiers() & Qt::ShiftModifier) != 0;
422 if (with_shift && (virtual_key == XK_Hiragana_Katakana)) {
423 modifier_non_required_key_ = "Katakana";
424 return Encode(result);
425 }
426
427 // Handle JP109's Muhenkan/Henkan/katakana-hiragana and Zenkaku/Hankaku
428 for (size_t i = 0; i < arraysize(kLinuxVirtualKeyModifierNonRequiredTable);
429 ++i) {
430 if (kLinuxVirtualKeyModifierNonRequiredTable[i].virtual_key ==
431 virtual_key) {
432 modifier_non_required_key_ =
433 kLinuxVirtualKeyModifierNonRequiredTable[i].mozc_key_name;
434 return Encode(result);
435 }
436 }
437 #endif
438
439 if (qt_key == Qt::Key_yen) {
440 // Japanese Yen mark, treat it as backslash for compatibility
441 modifier_non_required_key_ = "\\";
442 return Encode(result);
443 }
444
445 // printable command, which requires modifier keys
446 if ((qt_key >= 0x21 && qt_key <= 0x60) ||
447 (qt_key >= 0x7B && qt_key <= 0x7E)) {
448 if (qt_key >= 0x41 && qt_key <= 0x5A) {
449 modifier_required_key_ = static_cast<char>(qt_key - 'A' + 'a');
450 } else {
451 modifier_required_key_ = static_cast<char>(qt_key);
452 }
453 return Encode(result);
454 }
455
456 unknown_key_.sprintf("<UNK:0x%x 0x%x 0x%x>",
457 key_event.key(),
458 key_event.nativeScanCode(),
459 key_event.nativeVirtualKey());
460
461 return Encode(result);
462 }
463
eventFilter(QObject * obj,QEvent * event)464 bool KeyBindingFilter::eventFilter(QObject *obj, QEvent *event) {
465 if ((event->type() == QEvent::KeyPress ||
466 event->type() == QEvent::KeyRelease) &&
467 !IsDownOnlyKey(*static_cast<QKeyEvent *>(event)) &&
468 static_cast<QKeyEvent *>(event)->isAutoRepeat()) {
469 // ignores auto key repeat. just eat the event
470 return true;
471 }
472
473 // TODO(taku): the following sequence doesn't work as once
474 // user relase any of keys, the statues goes to "submitted"
475 // 1. Press Ctrl + a
476 // 2. Release a, but keep pressing Ctrl
477 // 3. Press b (the result should be "Ctrl + b").
478
479 if (event->type() == QEvent::KeyPress) {
480 // when the state is committed, reset the internal key binding
481 if (committed_) {
482 Reset();
483 line_edit_->clear();
484 }
485 committed_ = false;
486 const QKeyEvent *key_event = static_cast<QKeyEvent *>(event);
487 QString result;
488 const KeyBindingFilter::KeyState state = AddKey(*key_event, &result);
489 ok_button_->setEnabled(state != KeyBindingFilter::DENY_KEY);
490 line_edit_->setText(result);
491 line_edit_->setCursorPosition(0);
492 line_edit_->setAttribute(Qt::WA_InputMethodEnabled, false);
493 if (state == KeyBindingFilter::SUBMIT_KEY) {
494 committed_ = true;
495 }
496 return true;
497 } else if (event->type() == QEvent::KeyRelease) {
498 // when any of key is released, change the state "committed";
499 line_edit_->setCursorPosition(0);
500 committed_ = true;
501 return true;
502 }
503
504 return QObject::eventFilter(obj, event);
505 }
506
KeyBindingEditor(QWidget * parent,QWidget * trigger_parent)507 KeyBindingEditor::KeyBindingEditor(QWidget *parent, QWidget *trigger_parent)
508 : QDialog(parent), trigger_parent_(trigger_parent) {
509 setupUi(this);
510 #if defined(OS_LINUX)
511 // Workaround for the issue https://github.com/google/mozc/issues/9
512 // Seems that even after clicking the button for the keybinding dialog,
513 // the edit is not raised. This might be a bug of setFocusProxy.
514 setWindowFlags(Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint |
515 Qt::Tool | Qt::WindowStaysOnTopHint);
516 #else
517 setWindowFlags(Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint |
518 Qt::Tool);
519 #endif
520
521 QPushButton *ok_button =
522 KeyBindingEditorbuttonBox->button(QDialogButtonBox::Ok);
523 CHECK(ok_button != NULL);
524
525 filter_.reset(new KeyBindingFilter(KeyBindingLineEdit, ok_button));
526 KeyBindingLineEdit->installEventFilter(filter_.get());
527
528 // no right click
529 KeyBindingLineEdit->setContextMenuPolicy(Qt::NoContextMenu);
530 KeyBindingLineEdit->setMaxLength(32);
531 KeyBindingLineEdit->setAttribute(Qt::WA_InputMethodEnabled, false);
532
533 #ifdef OS_WIN
534 ::ImmAssociateContext(reinterpret_cast<HWND>(KeyBindingLineEdit->winId()), 0);
535 #endif
536
537 QObject::connect(KeyBindingEditorbuttonBox,
538 SIGNAL(clicked(QAbstractButton *)),
539 this,
540 SLOT(Clicked(QAbstractButton *)));
541
542 setFocusProxy(KeyBindingLineEdit);
543 }
544
~KeyBindingEditor()545 KeyBindingEditor::~KeyBindingEditor() {}
546
Clicked(QAbstractButton * button)547 void KeyBindingEditor::Clicked(QAbstractButton *button) {
548 switch (KeyBindingEditorbuttonBox->buttonRole(button)) {
549 case QDialogButtonBox::AcceptRole:
550 QDialog::accept();
551 break;
552 default:
553 QDialog::reject();
554 break;
555 }
556 }
557
GetBinding() const558 const QString KeyBindingEditor::GetBinding() const {
559 return KeyBindingLineEdit->text();
560 }
561
SetBinding(const QString & binding)562 void KeyBindingEditor::SetBinding(const QString &binding) {
563 KeyBindingLineEdit->setText(binding);
564 KeyBindingLineEdit->setCursorPosition(0);
565 }
566 } // namespace gui
567 } // namespace mozc
568