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/keymap_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 #include <QtCore/QFile>
37 #include <QtGui/QtGui>
38 #include <QtWidgets/QFileDialog>
39 #include <QtWidgets/QMenu>
40 #include <QtWidgets/QMessageBox>
41
42 #include <algorithm> // for unique
43 #include <cctype>
44 #include <memory>
45 #include <set>
46 #include <sstream>
47 #include <string>
48 #include <vector>
49
50 #include "base/config_file_stream.h"
51 #include "base/logging.h"
52 #include "base/singleton.h"
53 #include "base/util.h"
54 #include "composer/key_parser.h"
55 #include "gui/base/table_util.h"
56 #include "gui/config_dialog/combobox_delegate.h"
57 #include "gui/config_dialog/keybinding_editor_delegate.h"
58 #include "protocol/commands.pb.h"
59 #include "session/internal/keymap.h"
60 // TODO(komatsu): internal files should not be used from external modules.
61
62 namespace mozc {
63 namespace gui {
64 namespace {
65
66 config::Config::SessionKeymap kKeyMaps[] = {
67 config::Config::ATOK,
68 config::Config::MSIME,
69 config::Config::KOTOERI,
70 };
71
72 const char *kKeyMapStatus[] = {
73 "DirectInput",
74 "Precomposition",
75 "Composition",
76 "Conversion",
77 "Suggestion",
78 "Prediction",
79 };
80
81 const char kInsertCharacterCommand[] = "InsertCharacter";
82 const char kDirectMode[] = "DirectInput";
83 const char kReportBugCommand[] = "ReportBug";
84 // Old command name
85 const char kEditInsertCommand[] = "EditInsert";
86
87 #if defined(OS_MACOSX)
88 const char kIMEOnCommand[] = "IMEOn";
89 const char kIMEOffCommand[] = "IMEOff";
90 #endif // OS_MACOSX
91
92 enum {
93 NEW_INDEX = 0,
94 REMOVE_INDEX = 1,
95 IMPORT_FROM_FILE_INDEX = 2,
96 EXPORT_TO_FILE_INDEX = 3,
97 MENU_SIZE = 4
98 };
99
100 // Keymap validator for deciding that input is configurable
101 class KeyMapValidator {
102 public:
KeyMapValidator()103 KeyMapValidator() {
104 invisible_commands_.insert(kInsertCharacterCommand);
105 invisible_commands_.insert(kReportBugCommand);
106 // Old command name.
107 invisible_commands_.insert(kEditInsertCommand);
108 #if defined(OS_MACOSX)
109 // On Mac, we cannot customize keybindings for IME ON/OFF
110 // So we do not show them.
111 // TODO(toshiyuki): remove them after implimenting IME ON/OFF for Mac
112 invisible_commands_.insert(kIMEOnCommand);
113 invisible_commands_.insert(kIMEOffCommand);
114 #endif // OS_MACOSX
115
116 invisible_modifiers_.insert(mozc::commands::KeyEvent::KEY_DOWN);
117 invisible_modifiers_.insert(mozc::commands::KeyEvent::KEY_UP);
118
119 invisible_key_events_.insert(mozc::commands::KeyEvent::KANJI);
120 invisible_key_events_.insert(mozc::commands::KeyEvent::ON);
121 invisible_key_events_.insert(mozc::commands::KeyEvent::OFF);
122 invisible_key_events_.insert(mozc::commands::KeyEvent::TEXT_INPUT);
123 }
124
IsVisibleKey(const string & key)125 bool IsVisibleKey(const string &key) {
126 mozc::commands::KeyEvent key_event;
127 const bool parse_success = mozc::KeyParser::ParseKey(key, &key_event);
128 if (!parse_success) {
129 VLOG(3) << "key parse failed";
130 return false;
131 }
132 for (size_t i = 0; i < key_event.modifier_keys_size(); ++i) {
133 if (invisible_modifiers_.find(key_event.modifier_keys(i))
134 != invisible_modifiers_.end()) {
135 VLOG(3) << "invisible modifiers: " << key_event.modifier_keys(i);
136 return false;
137 }
138 }
139 if (key_event.has_special_key() &&
140 (invisible_key_events_.find(key_event.special_key())
141 != invisible_key_events_.end())) {
142 VLOG(3) << "invisible special key: " << key_event.special_key();
143 return false;
144 }
145 return true;
146 }
147
IsVisibleStatus(const string & status)148 bool IsVisibleStatus(const string &status) {
149 // no validation for now.
150 return true;
151 }
152
IsVisibleCommand(const string & command)153 bool IsVisibleCommand(const string &command) {
154 if (invisible_commands_.find(command) == invisible_commands_.end()) {
155 return true;
156 }
157 VLOG(3) << "invisible command: " << command;
158 return false;
159 }
160
161 // Returns true if the key map entry is valid
162 // invalid keymaps are not exported/imported.
IsValidEntry(const std::vector<string> & fields)163 bool IsValidEntry(const std::vector<string> &fields) {
164 if (fields.size() < 3) {
165 return false;
166 }
167
168 #ifdef NO_LOGGING
169 if (fields[2] == kReportBugCommand) {
170 return false;
171 }
172 #endif
173 return true;
174 }
175
176 // Returns true if the key map entry is configurable and
177 // we want to show them.
IsVisibleEntry(const std::vector<string> & fields)178 bool IsVisibleEntry(const std::vector<string> &fields) {
179 if (fields.size() < 3) {
180 return false;
181 }
182 const string &key = fields[1];
183 const string &command = fields[2];
184 if (!IsVisibleKey(key)) {
185 return false;
186 }
187 if (!IsVisibleCommand(command)) {
188 return false;
189 }
190
191 return true;
192 }
193
194 private:
195 std::set<uint32> invisible_modifiers_;
196 std::set<uint32> invisible_key_events_;
197 std::set<string> invisible_commands_;
198 };
199
200 class KeyMapTableLoader {
201 public:
KeyMapTableLoader()202 KeyMapTableLoader() {
203 string line;
204 std::vector<string> fields;
205 std::set<string> status;
206 std::set<string> commands;
207 KeyMapValidator *validator = mozc::Singleton<KeyMapValidator>::get();
208
209 // get all command names
210 std::set<string> command_names;
211 mozc::keymap::KeyMapManager manager;
212 manager.GetAvailableCommandNameDirect(&command_names);
213 manager.GetAvailableCommandNamePrecomposition(&command_names);
214 manager.GetAvailableCommandNameComposition(&command_names);
215 manager.GetAvailableCommandNameConversion(&command_names);
216 manager.GetAvailableCommandNameZeroQuerySuggestion(&command_names);
217 manager.GetAvailableCommandNameSuggestion(&command_names);
218 manager.GetAvailableCommandNamePrediction(&command_names);
219 for (std::set<string>::const_iterator itr = command_names.begin();
220 itr != command_names.end(); ++itr) {
221 if (validator->IsVisibleCommand(*itr)) {
222 commands.insert(*itr);
223 }
224 }
225
226 for (size_t i = 0; i < arraysize(kKeyMapStatus); ++i) {
227 status_ << QString::fromUtf8(kKeyMapStatus[i]);
228 }
229
230 for (std::set<string>::const_iterator it = commands.begin();
231 it != commands.end(); ++it) {
232 commands_ << QString::fromUtf8(it->c_str());
233 }
234 }
235
status()236 const QStringList &status() { return status_; }
commands()237 const QStringList &commands() { return commands_; }
238
239 private:
240 QStringList status_;
241 QStringList commands_;
242 };
243 } // namespace
244
KeyMapEditorDialog(QWidget * parent)245 KeyMapEditorDialog::KeyMapEditorDialog(QWidget *parent)
246 : GenericTableEditorDialog(parent, 3),
247 status_delegate_(new ComboBoxDelegate),
248 commands_delegate_(new ComboBoxDelegate),
249 keybinding_delegate_(new KeyBindingEditorDelegate) {
250 actions_.reset(new QAction * [MENU_SIZE]);
251 import_actions_.reset(new QAction * [arraysize(kKeyMaps)]);
252
253 actions_[NEW_INDEX] = mutable_edit_menu()->addAction(tr("New entry"));
254 actions_[REMOVE_INDEX] =
255 mutable_edit_menu()->addAction(tr("Remove selected entries"));
256 mutable_edit_menu()->addSeparator();
257
258 QMenu *sub_menu =
259 mutable_edit_menu()->addMenu(tr("Import predefined mapping"));
260 DCHECK(sub_menu);
261
262 // Make sure that the order should be the same as kKeyMapTableFiles
263 import_actions_[0] = sub_menu->addAction(tr("ATOK"));
264 import_actions_[1] = sub_menu->addAction(tr("MS-IME"));
265 import_actions_[2] = sub_menu->addAction(tr("Kotoeri"));
266
267 mutable_edit_menu()->addSeparator();
268 actions_[IMPORT_FROM_FILE_INDEX] =
269 mutable_edit_menu()->addAction(tr("Import from file..."));
270 actions_[EXPORT_TO_FILE_INDEX] =
271 mutable_edit_menu()->addAction(tr("Export to file..."));
272
273 // expand the last "Command" column
274 mutable_table_widget()->setColumnWidth
275 (0, static_cast<int>(mutable_table_widget()->columnWidth(0) * 1.5));
276 mutable_table_widget()->setColumnWidth
277 (1, static_cast<int>(mutable_table_widget()->columnWidth(1) * 1.1));
278 mutable_table_widget()->horizontalHeader()->setStretchLastSection(true);
279
280 KeyMapTableLoader *loader = Singleton<KeyMapTableLoader>::get();
281
282 // Generate i18n status list
283 const QStringList &statuses = loader->status();
284 QStringList i18n_statuses;
285 for (size_t i = 0; i < statuses.size(); ++i) {
286 const QString i18n_status = tr(statuses[i].toStdString().data());
287 i18n_statuses.append(i18n_status);
288 normalized_status_map_.insert(
289 std::make_pair(i18n_status.toStdString(), statuses[i].toStdString()));
290 }
291 status_delegate_->SetItemList(i18n_statuses);
292
293 // Generate i18n command list.
294 const QStringList &commands = loader->commands();
295 QStringList i18n_commands;
296 for (size_t i = 0; i < commands.size(); ++i) {
297 const QString i18n_command = tr(commands[i].toStdString().data());
298 i18n_commands.append(i18n_command);
299 normalized_command_map_.insert(
300 std::make_pair(i18n_command.toStdString(), commands[i].toStdString()));
301 }
302 i18n_commands.sort();
303 commands_delegate_->SetItemList(i18n_commands);
304
305 mutable_table_widget()->setItemDelegateForColumn(0,
306 status_delegate_.get());
307 mutable_table_widget()->setItemDelegateForColumn(1,
308 keybinding_delegate_.get());
309 mutable_table_widget()->setItemDelegateForColumn(2,
310 commands_delegate_.get());
311
312 setWindowTitle(tr("Mozc keymap editor"));
313 CHECK(mutable_table_widget());
314 CHECK_EQ(mutable_table_widget()->columnCount(), 3);
315 QStringList headers;
316 headers << tr("Mode") << tr("Key") << tr("Command");
317 mutable_table_widget()->setHorizontalHeaderLabels(headers);
318
319 resize(500, 350);
320
321 UpdateMenuStatus();
322 }
323
~KeyMapEditorDialog()324 KeyMapEditorDialog::~KeyMapEditorDialog() {}
325
LoadFromStream(std::istream * is)326 bool KeyMapEditorDialog::LoadFromStream(std::istream *is) {
327 if (is == NULL) {
328 return false;
329 }
330
331 string line;
332 if (!getline(*is, line)) { // must have 1st line
333 return false;
334 }
335
336 std::vector<string> fields;
337 int row = 0;
338 mutable_table_widget()->setRowCount(0);
339 mutable_table_widget()->verticalHeader()->hide();
340
341 invisible_keymap_table_.clear();
342 direct_mode_commands_.clear();
343 while (getline(*is, line)) {
344 if (line.empty() || line[0] == '#') {
345 continue;
346 }
347 Util::ChopReturns(&line);
348
349 fields.clear();
350 Util::SplitStringUsing(line, "\t", &fields);
351 if (fields.size() < 3) {
352 VLOG(3) << "field size < 3";
353 continue;
354 }
355
356 const string &status = fields[0];
357 const string &key = fields[1];
358 const string &command = fields[2];
359
360 // don't accept invalid keymap entries.
361 if (!Singleton<KeyMapValidator>::get()->IsValidEntry(fields)) {
362 VLOG(3) << "invalid entry.";
363 continue;
364 }
365
366 // don't show invisible (not configurable) keymap entries.
367 if (!Singleton<KeyMapValidator>::get()->IsVisibleEntry(fields)) {
368 VLOG(3) << "invalid entry to show. add to invisible_keymap_table_";
369 invisible_keymap_table_ += status;
370 invisible_keymap_table_ += '\t';
371 invisible_keymap_table_ += key;
372 invisible_keymap_table_ += '\t';
373 invisible_keymap_table_ += command;
374 invisible_keymap_table_ += '\n';
375 continue;
376 }
377
378 if (status == kDirectMode) {
379 direct_mode_commands_.insert(key);
380 }
381
382 QTableWidgetItem *status_item
383 = new QTableWidgetItem(tr(status.c_str()));
384 QTableWidgetItem *key_item
385 = new QTableWidgetItem(QString::fromUtf8(key.c_str()));
386 QTableWidgetItem *command_item
387 = new QTableWidgetItem(tr(command.c_str()));
388
389 mutable_table_widget()->insertRow(row);
390 mutable_table_widget()->setItem(row, 0, status_item);
391 mutable_table_widget()->setItem(row, 1, key_item);
392 mutable_table_widget()->setItem(row, 2, command_item);
393 ++row;
394 }
395
396 UpdateMenuStatus();
397
398 return true;
399 }
400
Update()401 bool KeyMapEditorDialog::Update() {
402 if (mutable_table_widget()->rowCount() == 0) {
403 QMessageBox::warning(this,
404 windowTitle(),
405 tr("Current keymap table is empty. "
406 "You might want to import a pre-defined "
407 "keymap table first."));
408 return false;
409 }
410
411 std::set<string> new_direct_mode_commands;
412
413 KeyMapValidator *validator = Singleton<KeyMapValidator>::get();
414 string *keymap_table = mutable_table();
415
416 *keymap_table = "status\tkey\tcommand\n";
417
418 for (int i = 0; i < mutable_table_widget()->rowCount(); ++i) {
419 const string &i18n_status =
420 TableUtil::SafeGetItemText(mutable_table_widget(), i, 0).toStdString();
421 const string &key =
422 TableUtil::SafeGetItemText(mutable_table_widget(), i, 1).toStdString();
423 const string &i18n_command =
424 TableUtil::SafeGetItemText(mutable_table_widget(), i, 2).toStdString();
425
426 const std::map<string, string>::const_iterator status_it =
427 normalized_status_map_.find(i18n_status);
428 if (status_it == normalized_status_map_.end()) {
429 LOG(ERROR) << "Unsupported i18n status name: " << i18n_status;
430 continue;
431 }
432 const string &status = status_it->second;
433
434 const std::map<string, string>::const_iterator command_it =
435 normalized_command_map_.find(i18n_command);
436 if (command_it == normalized_command_map_.end()) {
437 LOG(ERROR) << "Unsupported i18n command name:" << i18n_command;
438 continue;
439 }
440 const string &command = command_it->second;
441
442 if (!validator->IsVisibleKey(key)) {
443 QMessageBox::warning(this,
444 windowTitle(),
445 (tr("Invalid key:\n%1")
446 .arg(QString::fromUtf8(key.c_str()))));
447 return false;
448 }
449 const string keymap_line = status + "\t" + key + "\t" + command;
450 *keymap_table += keymap_line;
451 *keymap_table += '\n';
452
453 if (status == kDirectMode) {
454 new_direct_mode_commands.insert(key);
455 }
456 }
457 *keymap_table += invisible_keymap_table_;
458
459 if (new_direct_mode_commands != direct_mode_commands_) {
460 #if defined(OS_WIN) || defined(OS_LINUX)
461 QMessageBox::information(
462 this,
463 windowTitle(),
464 tr("Changes of keymaps for direct input mode will apply only to "
465 "applications that are launched after making your "
466 "modifications."));
467 #endif // OS_WIN || OS_LINUX
468 direct_mode_commands_ = new_direct_mode_commands;
469 }
470
471 return true;
472 }
473
UpdateMenuStatus()474 void KeyMapEditorDialog::UpdateMenuStatus() {
475 const bool status = (mutable_table_widget()->rowCount() > 0);
476 actions_[REMOVE_INDEX]->setEnabled(status);
477 actions_[EXPORT_TO_FILE_INDEX]->setEnabled(status);
478 UpdateOKButton(status);
479 }
480
OnEditMenuAction(QAction * action)481 void KeyMapEditorDialog::OnEditMenuAction(QAction *action) {
482 int import_index = -1;
483 for (size_t i = 0; i < arraysize(kKeyMaps); ++i) {
484 if (import_actions_[i] == action) {
485 import_index = i;
486 break;
487 }
488 }
489
490 if (action == actions_[NEW_INDEX]) {
491 AddNewItem();
492 } else if (action == actions_[REMOVE_INDEX]) {
493 DeleteSelectedItems();
494 } else if (import_index != -1 ||
495 action == actions_[IMPORT_FROM_FILE_INDEX]) {
496 if (mutable_table_widget()->rowCount() > 0 &&
497 QMessageBox::Ok !=
498 QMessageBox::question(
499 this,
500 windowTitle(),
501 tr("Do you want to overwrite the current keymaps?"),
502 QMessageBox::Ok | QMessageBox::Cancel,
503 QMessageBox::Cancel)) {
504 return;
505 }
506
507 // import_category_index means Import from file
508 if (action == actions_[IMPORT_FROM_FILE_INDEX]) {
509 Import();
510 // otherwise, load from predefined tables
511 } else if (import_index >= 0 &&
512 import_index < arraysize(kKeyMaps)) {
513 const char *keymap_file =
514 keymap::KeyMapManager::GetKeyMapFileName(kKeyMaps[import_index]);
515 std::unique_ptr<std::istream> ifs(
516 ConfigFileStream::LegacyOpen(keymap_file));
517 CHECK(ifs.get() != NULL); // should never happen
518 CHECK(LoadFromStream(ifs.get()));
519 }
520 } else if (action == actions_[EXPORT_TO_FILE_INDEX]) {
521 Export();
522 }
523
524 return;
525 }
526
527 // static
Show(QWidget * parent,const string & current_keymap,string * new_keymap)528 bool KeyMapEditorDialog::Show(QWidget *parent,
529 const string ¤t_keymap,
530 string *new_keymap) {
531 KeyMapEditorDialog window(parent);
532 window.LoadFromString(current_keymap);
533
534 // open modal mode
535 const bool result = (QDialog::Accepted == window.exec());
536 new_keymap->clear();
537 if (result) {
538 *new_keymap = window.table();
539 }
540 return result;
541 }
542 } // namespace gui
543 } // namespace mozc
544