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 &current_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