blob: 54811c3a0f3ed2d9415e39b9b8f14dcb94b617dc [file] [log] [blame]
// Copyright 2010-2015, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "gui/config_dialog/keymap_editor.h"
#include <QtCore/QFile>
#include <QtGui/QFileDialog>
#include <QtGui/QtGui>
#include <algorithm> // for unique
#include <cctype>
#include <set>
#include <sstream>
#include <string>
#include <vector>
#include "base/config_file_stream.h"
#include "base/logging.h"
#include "base/singleton.h"
#include "base/util.h"
#include "gui/base/table_util.h"
#include "gui/config_dialog/combobox_delegate.h"
#include "gui/config_dialog/keybinding_editor_delegate.h"
#include "session/commands.pb.h"
#include "session/internal/keymap.h"
#include "session/key_parser.h"
// TODO(komatsu): internal files should not be used from external modules.
namespace mozc {
namespace gui {
namespace {
config::Config::SessionKeymap kKeyMaps[] = {
config::Config::ATOK,
config::Config::MSIME,
config::Config::KOTOERI,
};
const char *kKeyMapStatus[] = {
"DirectInput",
"Precomposition",
"Composition",
"Conversion",
"Suggestion",
"Prediction",
};
const char kInsertCharacterCommand[] = "InsertCharacter";
const char kDirectMode[] = "DirectInput";
const char kReportBugCommand[] = "ReportBug";
// Old command name
const char kEditInsertCommand[] = "EditInsert";
#if defined(OS_MACOSX)
const char kIMEOnCommand[] = "IMEOn";
const char kIMEOffCommand[] = "IMEOff";
#endif // OS_MACOSX
enum {
NEW_INDEX = 0,
REMOVE_INDEX = 1,
IMPORT_FROM_FILE_INDEX = 2,
EXPORT_TO_FILE_INDEX = 3,
MENU_SIZE = 4
};
// Keymap validator for deciding that input is configurable
class KeyMapValidator {
public:
KeyMapValidator() {
invisible_commands_.insert(kInsertCharacterCommand);
invisible_commands_.insert(kReportBugCommand);
// Old command name.
invisible_commands_.insert(kEditInsertCommand);
#if defined(OS_MACOSX)
// On Mac, we cannot customize keybindings for IME ON/OFF
// So we do not show them.
// TODO(toshiyuki): remove them after implimenting IME ON/OFF for Mac
invisible_commands_.insert(kIMEOnCommand);
invisible_commands_.insert(kIMEOffCommand);
#endif // OS_MACOSX
invisible_modifiers_.insert(mozc::commands::KeyEvent::KEY_DOWN);
invisible_modifiers_.insert(mozc::commands::KeyEvent::KEY_UP);
invisible_key_events_.insert(mozc::commands::KeyEvent::KANJI);
invisible_key_events_.insert(mozc::commands::KeyEvent::ON);
invisible_key_events_.insert(mozc::commands::KeyEvent::OFF);
invisible_key_events_.insert(mozc::commands::KeyEvent::TEXT_INPUT);
}
bool IsVisibleKey(const string &key) {
mozc::commands::KeyEvent key_event;
const bool parse_success = mozc::KeyParser::ParseKey(key, &key_event);
if (!parse_success) {
VLOG(3) << "key parse failed";
return false;
}
for (size_t i = 0; i < key_event.modifier_keys_size(); ++i) {
if (invisible_modifiers_.find(key_event.modifier_keys(i))
!= invisible_modifiers_.end()) {
VLOG(3) << "invisible modifiers: " << key_event.modifier_keys(i);
return false;
}
}
if (key_event.has_special_key() &&
(invisible_key_events_.find(key_event.special_key())
!= invisible_key_events_.end())) {
VLOG(3) << "invisible special key: " << key_event.special_key();
return false;
}
return true;
}
bool IsVisibleStatus(const string &status) {
// no validation for now.
return true;
}
bool IsVisibleCommand(const string &command) {
if (invisible_commands_.find(command) == invisible_commands_.end()) {
return true;
}
VLOG(3) << "invisible command: " << command;
return false;
}
// Returns true if the key map entry is valid
// invalid keymaps are not exported/imported.
bool IsValidEntry(const vector<string> &fields) {
if (fields.size() < 3) {
return false;
}
#ifdef NO_LOGGING
if (fields[2] == kReportBugCommand) {
return false;
}
#endif
return true;
}
// Returns true if the key map entry is configurable and
// we want to show them.
bool IsVisibleEntry(const vector<string> &fields) {
if (fields.size() < 3) {
return false;
}
const string &key = fields[1];
const string &command = fields[2];
if (!IsVisibleKey(key)) {
return false;
}
if (!IsVisibleCommand(command)) {
return false;
}
return true;
}
private:
set<uint32> invisible_modifiers_;
set<uint32> invisible_key_events_;
set<string> invisible_commands_;
};
class KeyMapTableLoader {
public:
KeyMapTableLoader() {
string line;
vector<string> fields;
set<string> status;
set<string> commands;
KeyMapValidator *validator = mozc::Singleton<KeyMapValidator>::get();
// get all command names
set<string> command_names;
mozc::keymap::KeyMapManager manager;
manager.GetAvailableCommandNameDirect(&command_names);
manager.GetAvailableCommandNamePrecomposition(&command_names);
manager.GetAvailableCommandNameComposition(&command_names);
manager.GetAvailableCommandNameConversion(&command_names);
manager.GetAvailableCommandNameZeroQuerySuggestion(&command_names);
manager.GetAvailableCommandNameSuggestion(&command_names);
manager.GetAvailableCommandNamePrediction(&command_names);
for (set<string>::const_iterator itr = command_names.begin();
itr != command_names.end(); ++itr) {
if (validator->IsVisibleCommand(*itr)) {
commands.insert(*itr);
}
}
for (size_t i = 0; i < arraysize(kKeyMapStatus); ++i) {
status_ << QString::fromUtf8(kKeyMapStatus[i]);
}
for (set<string>::const_iterator it = commands.begin();
it != commands.end(); ++it) {
commands_ << QString::fromUtf8(it->c_str());
}
}
const QStringList &status() { return status_; }
const QStringList &commands() { return commands_; }
private:
QStringList status_;
QStringList commands_;
};
} // namespace
KeyMapEditorDialog::KeyMapEditorDialog(QWidget *parent)
: GenericTableEditorDialog(parent, 3),
status_delegate_(new ComboBoxDelegate),
commands_delegate_(new ComboBoxDelegate),
keybinding_delegate_(new KeyBindingEditorDelegate) {
actions_.reset(new QAction * [MENU_SIZE]);
import_actions_.reset(new QAction * [arraysize(kKeyMaps)]);
actions_[NEW_INDEX] = mutable_edit_menu()->addAction(tr("New entry"));
actions_[REMOVE_INDEX] =
mutable_edit_menu()->addAction(tr("Remove selected entries"));
mutable_edit_menu()->addSeparator();
QMenu *sub_menu =
mutable_edit_menu()->addMenu(tr("Import predefined mapping"));
DCHECK(sub_menu);
// Make sure that the order should be the same as kKeyMapTableFiles
import_actions_[0] = sub_menu->addAction(tr("ATOK"));
import_actions_[1] = sub_menu->addAction(tr("MS-IME"));
import_actions_[2] = sub_menu->addAction(tr("Kotoeri"));
mutable_edit_menu()->addSeparator();
actions_[IMPORT_FROM_FILE_INDEX] =
mutable_edit_menu()->addAction(tr("Import from file..."));
actions_[EXPORT_TO_FILE_INDEX] =
mutable_edit_menu()->addAction(tr("Export to file..."));
// expand the last "Command" column
mutable_table_widget()->setColumnWidth
(0, static_cast<int>(mutable_table_widget()->columnWidth(0) * 1.5));
mutable_table_widget()->setColumnWidth
(1, static_cast<int>(mutable_table_widget()->columnWidth(1) * 1.1));
mutable_table_widget()->horizontalHeader()->setStretchLastSection(true);
KeyMapTableLoader *loader = Singleton<KeyMapTableLoader>::get();
// Generate i18n status list
const QStringList &statuses = loader->status();
QStringList i18n_statuses;
for (size_t i = 0; i < statuses.size(); ++i) {
const QString i18n_status = tr(statuses[i].toStdString().data());
i18n_statuses.append(i18n_status);
normalized_status_map_.insert(
make_pair(i18n_status.toStdString(), statuses[i].toStdString()));
}
status_delegate_->SetItemList(i18n_statuses);
// Generate i18n command list.
const QStringList &commands = loader->commands();
QStringList i18n_commands;
for (size_t i = 0; i < commands.size(); ++i) {
const QString i18n_command = tr(commands[i].toStdString().data());
i18n_commands.append(i18n_command);
normalized_command_map_.insert(
make_pair(i18n_command.toStdString(), commands[i].toStdString()));
}
i18n_commands.sort();
commands_delegate_->SetItemList(i18n_commands);
mutable_table_widget()->setItemDelegateForColumn(0,
status_delegate_.get());
mutable_table_widget()->setItemDelegateForColumn(1,
keybinding_delegate_.get());
mutable_table_widget()->setItemDelegateForColumn(2,
commands_delegate_.get());
setWindowTitle(tr("Mozc keymap editor"));
CHECK(mutable_table_widget());
CHECK_EQ(mutable_table_widget()->columnCount(), 3);
QStringList headers;
headers << tr("Mode") << tr("Key") << tr("Command");
mutable_table_widget()->setHorizontalHeaderLabels(headers);
resize(500, 350);
UpdateMenuStatus();
}
KeyMapEditorDialog::~KeyMapEditorDialog() {}
bool KeyMapEditorDialog::LoadFromStream(istream *is) {
if (is == NULL) {
return false;
}
string line;
if (!getline(*is, line)) { // must have 1st line
return false;
}
vector<string> fields;
int row = 0;
mutable_table_widget()->setRowCount(0);
mutable_table_widget()->verticalHeader()->hide();
invisible_keymap_table_.clear();
direct_mode_commands_.clear();
while (getline(*is, line)) {
if (line.empty() || line[0] == '#') {
continue;
}
Util::ChopReturns(&line);
fields.clear();
Util::SplitStringUsing(line, "\t", &fields);
if (fields.size() < 3) {
VLOG(3) << "field size < 3";
continue;
}
const string &status = fields[0];
const string &key = fields[1];
const string &command = fields[2];
// don't accept invalid keymap entries.
if (!Singleton<KeyMapValidator>::get()->IsValidEntry(fields)) {
VLOG(3) << "invalid entry.";
continue;
}
// don't show invisible (not configurable) keymap entries.
if (!Singleton<KeyMapValidator>::get()->IsVisibleEntry(fields)) {
VLOG(3) << "invalid entry to show. add to invisible_keymap_table_";
invisible_keymap_table_ += status;
invisible_keymap_table_ += '\t';
invisible_keymap_table_ += key;
invisible_keymap_table_ += '\t';
invisible_keymap_table_ += command;
invisible_keymap_table_ += '\n';
continue;
}
if (status == kDirectMode) {
direct_mode_commands_.insert(key);
}
QTableWidgetItem *status_item
= new QTableWidgetItem(tr(status.c_str()));
QTableWidgetItem *key_item
= new QTableWidgetItem(QString::fromUtf8(key.c_str()));
QTableWidgetItem *command_item
= new QTableWidgetItem(tr(command.c_str()));
mutable_table_widget()->insertRow(row);
mutable_table_widget()->setItem(row, 0, status_item);
mutable_table_widget()->setItem(row, 1, key_item);
mutable_table_widget()->setItem(row, 2, command_item);
++row;
}
UpdateMenuStatus();
return true;
}
bool KeyMapEditorDialog::Update() {
if (mutable_table_widget()->rowCount() == 0) {
QMessageBox::warning(this,
windowTitle(),
tr("Current keymap table is empty. "
"You might want to import a pre-defined "
"keymap table first."));
return false;
}
set<string> new_direct_mode_commands;
KeyMapValidator *validator = Singleton<KeyMapValidator>::get();
string *keymap_table = mutable_table();
*keymap_table = "status\tkey\tcommand\n";
for (int i = 0; i < mutable_table_widget()->rowCount(); ++i) {
const string &i18n_status =
TableUtil::SafeGetItemText(mutable_table_widget(), i, 0).toStdString();
const string &key =
TableUtil::SafeGetItemText(mutable_table_widget(), i, 1).toStdString();
const string &i18n_command =
TableUtil::SafeGetItemText(mutable_table_widget(), i, 2).toStdString();
const map<string, string>::const_iterator status_it =
normalized_status_map_.find(i18n_status);
if (status_it == normalized_status_map_.end()) {
LOG(ERROR) << "Unsupported i18n status name: " << i18n_status;
continue;
}
const string &status = status_it->second;
const map<string, string>::const_iterator command_it =
normalized_command_map_.find(i18n_command);
if (command_it == normalized_command_map_.end()) {
LOG(ERROR) << "Unsupported i18n command name:" << i18n_command;
continue;
}
const string &command = command_it->second;
if (!validator->IsVisibleKey(key)) {
QMessageBox::warning(this,
windowTitle(),
(tr("Invalid key:\n%1")
.arg(QString::fromUtf8(key.c_str()))));
return false;
}
const string keymap_line = status + "\t" + key + "\t" + command;
*keymap_table += keymap_line;
*keymap_table += '\n';
if (status == kDirectMode) {
new_direct_mode_commands.insert(key);
}
}
*keymap_table += invisible_keymap_table_;
if (new_direct_mode_commands != direct_mode_commands_) {
#if defined(OS_WIN) || defined(OS_LINUX)
QMessageBox::information(
this,
windowTitle(),
tr("Changes of keymaps for direct input mode will apply only to "
"applications that are launched after making your "
"modifications."));
#endif // OS_WIN || OS_LINUX
direct_mode_commands_ = new_direct_mode_commands;
}
return true;
}
void KeyMapEditorDialog::UpdateMenuStatus() {
const bool status = (mutable_table_widget()->rowCount() > 0);
actions_[REMOVE_INDEX]->setEnabled(status);
actions_[EXPORT_TO_FILE_INDEX]->setEnabled(status);
UpdateOKButton(status);
}
void KeyMapEditorDialog::OnEditMenuAction(QAction *action) {
int import_index = -1;
for (size_t i = 0; i < arraysize(kKeyMaps); ++i) {
if (import_actions_[i] == action) {
import_index = i;
break;
}
}
if (action == actions_[NEW_INDEX]) {
AddNewItem();
} else if (action == actions_[REMOVE_INDEX]) {
DeleteSelectedItems();
} else if (import_index != -1 ||
action == actions_[IMPORT_FROM_FILE_INDEX]) {
if (mutable_table_widget()->rowCount() > 0 &&
QMessageBox::Ok !=
QMessageBox::question(
this,
windowTitle(),
tr("Do you want to overwrite the current keymaps?"),
QMessageBox::Ok | QMessageBox::Cancel,
QMessageBox::Cancel)) {
return;
}
// import_category_index means Import from file
if (action == actions_[IMPORT_FROM_FILE_INDEX]) {
Import();
// otherwise, load from predefined tables
} else if (import_index >= 0 &&
import_index < arraysize(kKeyMaps)) {
const char *keymap_file =
keymap::KeyMapManager::GetKeyMapFileName(kKeyMaps[import_index]);
scoped_ptr<istream> ifs(
ConfigFileStream::LegacyOpen(keymap_file));
CHECK(ifs.get() != NULL); // should never happen
CHECK(LoadFromStream(ifs.get()));
}
} else if (action == actions_[EXPORT_TO_FILE_INDEX]) {
Export();
}
return;
}
// static
bool KeyMapEditorDialog::Show(QWidget *parent,
const string &current_keymap,
string *new_keymap) {
KeyMapEditorDialog window(parent);
window.LoadFromString(current_keymap);
// open modal mode
const bool result = (QDialog::Accepted == window.exec());
new_keymap->clear();
if (result) {
*new_keymap = window.table();
}
return result;
}
} // namespace gui
} // namespace mozc