Browse Source

KeyboardLayout: support specifying keyboard layout mappings

Captain mode now supports different layout mappings.
Le Tan 7 years ago
parent
commit
574aa4e70a

+ 497 - 0
src/dialog/vkeyboardlayoutmappingdialog.cpp

@@ -0,0 +1,497 @@
+#include "vkeyboardlayoutmappingdialog.h"
+
+#include <QtWidgets>
+
+#include "vlineedit.h"
+#include "utils/vkeyboardlayoutmanager.h"
+#include "utils/vutils.h"
+#include "utils/viconutils.h"
+#include "vconfigmanager.h"
+
+extern VConfigManager *g_config;
+
+VKeyboardLayoutMappingDialog::VKeyboardLayoutMappingDialog(QWidget *p_parent)
+    : QDialog(p_parent),
+      m_mappingModified(false),
+      m_listenIndex(-1)
+{
+    setupUI();
+
+    loadAvailableMappings();
+}
+
+void VKeyboardLayoutMappingDialog::setupUI()
+{
+    QString info = tr("Manage keybaord layout mappings to used in shortcuts.");
+    info += "\n";
+    info += tr("Double click an item to set mapping key.");
+    QLabel *infoLabel = new QLabel(info, this);
+
+    // Selector.
+    m_selectorCombo = VUtils::getComboBox(this);
+    connect(m_selectorCombo, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
+            this, [this](int p_idx) {
+                loadMappingInfo(m_selectorCombo->itemData(p_idx).toString());
+            });
+
+    // Add.
+    m_addBtn = new QPushButton(VIconUtils::buttonIcon(":/resources/icons/add.svg"), "", this);
+    m_addBtn->setToolTip(tr("New Mapping"));
+    m_addBtn->setProperty("FlatBtn", true);
+    connect(m_addBtn, &QPushButton::clicked,
+            this, &VKeyboardLayoutMappingDialog::newMapping);
+
+    // Delete.
+    m_deleteBtn = new QPushButton(VIconUtils::buttonDangerIcon(":/resources/icons/delete.svg"),
+                                  "",
+                                  this);
+    m_deleteBtn->setToolTip(tr("Delete Mapping"));
+    m_deleteBtn->setProperty("FlatBtn", true);
+    connect(m_deleteBtn, &QPushButton::clicked,
+            this, &VKeyboardLayoutMappingDialog::deleteCurrentMapping);
+
+    QHBoxLayout *selectLayout = new QHBoxLayout();
+    selectLayout->addWidget(new QLabel(tr("Keyboard layout mapping:"), this));
+    selectLayout->addWidget(m_selectorCombo);
+    selectLayout->addWidget(m_addBtn);
+    selectLayout->addWidget(m_deleteBtn);
+    selectLayout->addStretch();
+
+    // Name.
+    m_nameEdit = new VLineEdit(this);
+    connect(m_nameEdit, &QLineEdit::textEdited,
+            this, [this](const QString &p_text) {
+                Q_UNUSED(p_text);
+                setModified(true);
+            });
+
+    QHBoxLayout *editLayout = new QHBoxLayout();
+    editLayout->addWidget(new QLabel(tr("Name:"), this));
+    editLayout->addWidget(m_nameEdit);
+    editLayout->addStretch();
+
+    // Tree.
+    m_contentTree = new QTreeWidget(this);
+    m_contentTree->setProperty("ItemBorder", true);
+    m_contentTree->setRootIsDecorated(false);
+    m_contentTree->setColumnCount(2);
+    m_contentTree->setSelectionBehavior(QAbstractItemView::SelectRows);
+    QStringList headers;
+    headers << tr("Key") << tr("New Key");
+    m_contentTree->setHeaderLabels(headers);
+
+    m_contentTree->installEventFilter(this);
+
+    connect(m_contentTree, &QTreeWidget::itemDoubleClicked,
+            this, [this](QTreeWidgetItem *p_item, int p_column) {
+                Q_UNUSED(p_column);
+                int idx = m_contentTree->indexOfTopLevelItem(p_item);
+                if (m_listenIndex == -1) {
+                    // Listen key for this item.
+                    setListeningKey(idx);
+                } else if (idx == m_listenIndex) {
+                    // Cancel listening key for this item.
+                    cancelListeningKey();
+                } else {
+                    // Recover previous item.
+                    cancelListeningKey();
+                    setListeningKey(idx);
+                }
+            });
+
+    connect(m_contentTree, &QTreeWidget::itemClicked,
+            this, [this](QTreeWidgetItem *p_item, int p_column) {
+                Q_UNUSED(p_column);
+                int idx = m_contentTree->indexOfTopLevelItem(p_item);
+                if (idx != m_listenIndex) {
+                    cancelListeningKey();
+                }
+            });
+
+    QVBoxLayout *infoLayout = new QVBoxLayout();
+    infoLayout->addLayout(editLayout);
+    infoLayout->addWidget(m_contentTree);
+
+    QGroupBox *box = new QGroupBox(tr("Mapping Information"));
+    box->setLayout(infoLayout);
+
+    // Ok is the default button.
+    QDialogButtonBox *btnBox = new QDialogButtonBox(QDialogButtonBox::Ok
+                                                    | QDialogButtonBox::Apply
+                                                    | QDialogButtonBox::Cancel);
+    connect(btnBox, &QDialogButtonBox::accepted,
+            this, [this]() {
+                if (applyChanges()) {
+                    QDialog::accept();
+                }
+            });
+    connect(btnBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+
+    QPushButton *okBtn = btnBox->button(QDialogButtonBox::Ok);
+    okBtn->setProperty("SpecialBtn", true);
+
+    m_applyBtn = btnBox->button(QDialogButtonBox::Apply);
+    connect(m_applyBtn, &QPushButton::clicked,
+            this, &VKeyboardLayoutMappingDialog::applyChanges);
+
+    QVBoxLayout *mainLayout = new QVBoxLayout();
+    mainLayout->addWidget(infoLabel);
+    mainLayout->addLayout(selectLayout);
+    mainLayout->addWidget(box);
+    mainLayout->addWidget(btnBox);
+
+    setLayout(mainLayout);
+
+    setWindowTitle(tr("Keyboard Layout Mappings"));
+}
+
+void VKeyboardLayoutMappingDialog::newMapping()
+{
+    QString name = getNewMappingName();
+    if (!VKeyboardLayoutManager::addLayout(name)) {
+        VUtils::showMessage(QMessageBox::Warning,
+                            tr("Warning"),
+                            tr("Fail to add mapping <span style=\"%1\">%2</span>.")
+                              .arg(g_config->c_dataTextStyle)
+                              .arg(name),
+                            tr("Please check the configuration file and try again."),
+                            QMessageBox::Ok,
+                            QMessageBox::Ok,
+                            this);
+        return;
+    }
+
+    loadAvailableMappings();
+
+    setCurrentMapping(name);
+}
+
+QString VKeyboardLayoutMappingDialog::getNewMappingName() const
+{
+    QString name;
+    QString baseName("layout_mapping");
+    int seq = 1;
+    do {
+        name = QString("%1_%2").arg(baseName).arg(QString::number(seq++), 3, '0');
+    } while (m_selectorCombo->findData(name) != -1);
+
+    return name;
+}
+
+void VKeyboardLayoutMappingDialog::deleteCurrentMapping()
+{
+    QString mapping = currentMapping();
+    if (mapping.isEmpty()) {
+        return;
+    }
+
+    int ret = VUtils::showMessage(QMessageBox::Warning,
+                                  tr("Warning"),
+                                  tr("Are you sure to delete mapping <span style=\"%1\">%2</span>.")
+                                    .arg(g_config->c_dataTextStyle)
+                                    .arg(mapping),
+                                  "",
+                                  QMessageBox::Ok | QMessageBox::Cancel,
+                                  QMessageBox::Ok,
+                                  this);
+    if (ret != QMessageBox::Ok) {
+        return;
+    }
+
+    if (!VKeyboardLayoutManager::removeLayout(mapping)) {
+        VUtils::showMessage(QMessageBox::Warning,
+                            tr("Warning"),
+                            tr("Fail to delete mapping <span style=\"%1\">%2</span>.")
+                              .arg(g_config->c_dataTextStyle)
+                              .arg(mapping),
+                            tr("Please check the configuration file and try again."),
+                            QMessageBox::Ok,
+                            QMessageBox::Ok,
+                            this);
+    }
+
+    loadAvailableMappings();
+}
+
+void VKeyboardLayoutMappingDialog::loadAvailableMappings()
+{
+    m_selectorCombo->setCurrentIndex(-1);
+    m_selectorCombo->clear();
+
+    QStringList layouts = VKeyboardLayoutManager::availableLayouts();
+    for (auto const & layout : layouts) {
+        m_selectorCombo->addItem(layout, layout);
+    }
+
+    if (m_selectorCombo->count() > 0) {
+        m_selectorCombo->setCurrentIndex(0);
+    }
+}
+
+static QList<int> keysNeededToMap()
+{
+    QList<int> keys;
+
+    for (int i = Qt::Key_0; i <= Qt::Key_9; ++i) {
+        keys.append(i);
+    }
+
+    for (int i = Qt::Key_A; i <= Qt::Key_Z; ++i) {
+        keys.append(i);
+    }
+
+    QList<int> addi = g_config->getKeyboardLayoutMappingKeys();
+    for (auto tmp : addi) {
+        if (!keys.contains(tmp)) {
+            keys.append(tmp);
+        }
+    }
+
+    return keys;
+}
+
+static void recoverTreeItem(QTreeWidgetItem *p_item)
+{
+    int key = p_item->data(0, Qt::UserRole).toInt();
+    QString text0 = QString("%1 (%2)").arg(VUtils::keyToChar(key, false))
+                                      .arg(key);
+    p_item->setText(0, text0);
+
+    int newKey = p_item->data(1, Qt::UserRole).toInt();
+    QString text1;
+    if (newKey > 0) {
+        text1 = QString("%1 (%2)").arg(VUtils::keyToChar(newKey, false))
+                                  .arg(newKey);
+    }
+
+    p_item->setText(1, text1);
+}
+
+// @p_newKey, 0 if there is no mapping.
+static void fillTreeItem(QTreeWidgetItem *p_item, int p_key, int p_newKey)
+{
+    p_item->setData(0, Qt::UserRole, p_key);
+    p_item->setData(1, Qt::UserRole, p_newKey);
+    recoverTreeItem(p_item);
+}
+
+static void setTreeItemMapping(QTreeWidgetItem *p_item, int p_newKey)
+{
+    p_item->setData(1, Qt::UserRole, p_newKey);
+}
+
+static void fillMappingTree(QTreeWidget *p_tree, const QHash<int, int> &p_mappings)
+{
+    QList<int> keys = keysNeededToMap();
+
+    for (auto key : keys) {
+        int val = 0;
+        auto it = p_mappings.find(key);
+        if (it != p_mappings.end()) {
+            val = it.value();
+        }
+
+        QTreeWidgetItem *item = new QTreeWidgetItem(p_tree);
+        fillTreeItem(item, key, val);
+    }
+}
+
+static QHash<int, int> retrieveMappingFromTree(QTreeWidget *p_tree)
+{
+    QHash<int, int> mappings;
+    int cnt = p_tree->topLevelItemCount();
+    for (int i = 0; i < cnt; ++i) {
+        QTreeWidgetItem *item = p_tree->topLevelItem(i);
+        int key = item->data(0, Qt::UserRole).toInt();
+        int newKey = item->data(1, Qt::UserRole).toInt();
+        if (newKey > 0) {
+            mappings.insert(key, newKey);
+        }
+    }
+
+    return mappings;
+}
+
+void VKeyboardLayoutMappingDialog::loadMappingInfo(const QString &p_layout)
+{
+    setModified(false);
+
+    if (p_layout.isEmpty()) {
+        m_nameEdit->clear();
+        m_contentTree->clear();
+        m_nameEdit->setEnabled(false);
+        m_contentTree->setEnabled(false);
+        return;
+    }
+
+    m_nameEdit->setText(p_layout);
+    m_nameEdit->setEnabled(true);
+
+    m_contentTree->clear();
+    if (!p_layout.isEmpty()) {
+        auto mappings = VKeyboardLayoutManager::readLayoutMapping(p_layout);
+        fillMappingTree(m_contentTree, mappings);
+    }
+    m_contentTree->setEnabled(true);
+}
+
+void VKeyboardLayoutMappingDialog::updateButtons()
+{
+    QString mapping = currentMapping();
+
+    m_deleteBtn->setEnabled(!mapping.isEmpty());
+    m_applyBtn->setEnabled(m_mappingModified);
+}
+
+QString VKeyboardLayoutMappingDialog::currentMapping() const
+{
+    return m_selectorCombo->currentData().toString();
+}
+
+void VKeyboardLayoutMappingDialog::setCurrentMapping(const QString &p_layout)
+{
+    return m_selectorCombo->setCurrentIndex(m_selectorCombo->findData(p_layout));
+}
+
+bool VKeyboardLayoutMappingDialog::applyChanges()
+{
+    if (!m_mappingModified) {
+        return true;
+    }
+
+    QString mapping = currentMapping();
+    if (mapping.isEmpty()) {
+        setModified(false);
+        return true;
+    }
+
+    // Check the name.
+    QString newName = m_nameEdit->text();
+    if (newName.isEmpty() || newName.toLower() == "global") {
+        // Set back the original name.
+        m_nameEdit->setText(mapping);
+        m_nameEdit->selectAll();
+        m_nameEdit->setFocus();
+        return false;
+    } else if (newName != mapping) {
+        // Rename the mapping.
+        if (!VKeyboardLayoutManager::renameLayout(mapping, newName)) {
+            VUtils::showMessage(QMessageBox::Warning,
+                                tr("Warning"),
+                                tr("Fail to rename mapping <span style=\"%1\">%2</span>.")
+                                  .arg(g_config->c_dataTextStyle)
+                                  .arg(mapping),
+                                tr("Please check the configuration file and try again."),
+                                QMessageBox::Ok,
+                                QMessageBox::Ok,
+                                this);
+            m_nameEdit->setText(mapping);
+            m_nameEdit->selectAll();
+            m_nameEdit->setFocus();
+            return false;
+        }
+
+        // Update the combobox.
+        int idx = m_selectorCombo->currentIndex();
+        m_selectorCombo->setItemText(idx, newName);
+        m_selectorCombo->setItemData(idx, newName);
+
+        mapping = newName;
+    }
+
+    // Check the mappings.
+    QHash<int, int> mappings = retrieveMappingFromTree(m_contentTree);
+    if (!VKeyboardLayoutManager::updateLayout(mapping, mappings)) {
+        VUtils::showMessage(QMessageBox::Warning,
+                            tr("Warning"),
+                            tr("Fail to update mapping <span style=\"%1\">%2</span>.")
+                              .arg(g_config->c_dataTextStyle)
+                              .arg(mapping),
+                            tr("Please check the configuration file and try again."),
+                            QMessageBox::Ok,
+                            QMessageBox::Ok,
+                            this);
+        return false;
+    }
+
+    setModified(false);
+    return true;
+}
+
+bool VKeyboardLayoutMappingDialog::eventFilter(QObject *p_obj, QEvent *p_event)
+{
+    if (p_obj == m_contentTree) {
+        switch (p_event->type()) {
+        case QEvent::FocusOut:
+            cancelListeningKey();
+            break;
+
+        case QEvent::KeyPress:
+            if (listenKey(static_cast<QKeyEvent *>(p_event))) {
+                return true;
+            }
+
+            break;
+
+        default:
+            break;
+        }
+    }
+
+    return QDialog::eventFilter(p_obj, p_event);
+}
+
+bool VKeyboardLayoutMappingDialog::listenKey(QKeyEvent *p_event)
+{
+    if (m_listenIndex == -1) {
+        return false;
+    }
+
+    int key = p_event->key();
+
+    if (VUtils::isMetaKey(key)) {
+        return false;
+    }
+
+    if (key == Qt::Key_Escape) {
+        cancelListeningKey();
+        return true;
+    }
+
+    // Set the mapping.
+    QTreeWidgetItem *item = m_contentTree->topLevelItem(m_listenIndex);
+    setTreeItemMapping(item, key);
+    setModified(true);
+
+    // Try next item automatically.
+    int nextIdx = m_listenIndex + 1;
+    cancelListeningKey();
+
+    if (nextIdx < m_contentTree->topLevelItemCount()) {
+        QTreeWidgetItem *item = m_contentTree->topLevelItem(nextIdx);
+        m_contentTree->clearSelection();
+        m_contentTree->setCurrentItem(item);
+
+        setListeningKey(nextIdx);
+    }
+
+    return true;
+}
+
+void VKeyboardLayoutMappingDialog::cancelListeningKey()
+{
+    if (m_listenIndex > -1) {
+        // Recover that item.
+        recoverTreeItem(m_contentTree->topLevelItem(m_listenIndex));
+
+        m_listenIndex = -1;
+    }
+}
+
+void VKeyboardLayoutMappingDialog::setListeningKey(int p_idx)
+{
+    Q_ASSERT(m_listenIndex == -1 && p_idx > -1);
+    m_listenIndex = p_idx;
+    QTreeWidgetItem *item = m_contentTree->topLevelItem(m_listenIndex);
+    item->setText(1, tr("Press key to set mapping"));
+}

+ 73 - 0
src/dialog/vkeyboardlayoutmappingdialog.h

@@ -0,0 +1,73 @@
+#ifndef VKEYBOARDLAYOUTMAPPINGDIALOG_H
+#define VKEYBOARDLAYOUTMAPPINGDIALOG_H
+
+#include <QDialog>
+
+
+class QDialogButtonBox;
+class QString;
+class QTreeWidget;
+class VLineEdit;
+class QPushButton;
+class QComboBox;
+
+class VKeyboardLayoutMappingDialog : public QDialog
+{
+    Q_OBJECT
+public:
+    explicit VKeyboardLayoutMappingDialog(QWidget *p_parent = nullptr);
+
+protected:
+    bool eventFilter(QObject *p_obj, QEvent *p_event) Q_DECL_OVERRIDE;
+
+private slots:
+    void newMapping();
+
+    void deleteCurrentMapping();
+
+    // Return true if changes are saved.
+    bool applyChanges();
+
+private:
+    void setupUI();
+
+    void loadAvailableMappings();
+
+    void loadMappingInfo(const QString &p_layout);
+
+    void updateButtons();
+
+    QString currentMapping() const;
+
+    void setCurrentMapping(const QString &p_layout);
+
+    QString getNewMappingName() const;
+
+    bool listenKey(QKeyEvent *p_event);
+
+    void cancelListeningKey();
+
+    void setListeningKey(int p_idx);
+
+    void setModified(bool p_modified);
+
+    QComboBox *m_selectorCombo;
+    QPushButton *m_addBtn;
+    QPushButton *m_deleteBtn;
+    VLineEdit *m_nameEdit;
+    QTreeWidget *m_contentTree;
+    QPushButton *m_applyBtn;
+
+    bool m_mappingModified;
+
+    // Index of the item in the tree which is listening key.
+    // -1 for not listening.
+    int m_listenIndex;
+};
+
+inline void VKeyboardLayoutMappingDialog::setModified(bool p_modified)
+{
+    m_mappingModified = p_modified;
+    updateButtons();
+}
+#endif // VKEYBOARDLAYOUTMAPPINGDIALOG_H

+ 60 - 0
src/dialog/vsettingsdialog.cpp

@@ -9,6 +9,8 @@
 #include "vlineedit.h"
 #include "vplantumlhelper.h"
 #include "vgraphvizhelper.h"
+#include "utils/vkeyboardlayoutmanager.h"
+#include "dialog/vkeyboardlayoutmappingdialog.h"
 
 extern VConfigManager *g_config;
 
@@ -324,11 +326,29 @@ VGeneralTab::VGeneralTab(QWidget *p_parent)
     qaLayout->addWidget(m_quickAccessEdit);
     qaLayout->addWidget(browseBtn);
 
+    // Keyboard layout mappings.
+    m_keyboardLayoutCombo = VUtils::getComboBox(this);
+    m_keyboardLayoutCombo->setToolTip(tr("Choose the keyboard layout mapping to use in shortcuts"));
+
+    QPushButton *editLayoutBtn = new QPushButton(tr("Edit"), this);
+    connect(editLayoutBtn, &QPushButton::clicked,
+            this, [this]() {
+                VKeyboardLayoutMappingDialog dialog(this);
+                dialog.exec();
+                loadKeyboardLayoutMapping();
+            });
+
+    QHBoxLayout *klLayout = new QHBoxLayout();
+    klLayout->addWidget(m_keyboardLayoutCombo);
+    klLayout->addWidget(editLayoutBtn);
+    klLayout->addStretch();
+
     QFormLayout *optionLayout = new QFormLayout();
     optionLayout->addRow(tr("Language:"), m_langCombo);
     optionLayout->addRow(m_systemTray);
     optionLayout->addRow(tr("Startup pages:"), startupLayout);
     optionLayout->addRow(tr("Quick access:"), qaLayout);
+    optionLayout->addRow(tr("Keyboard layout mapping:"), klLayout);
 
     QVBoxLayout *mainLayout = new QVBoxLayout();
     mainLayout->addLayout(optionLayout);
@@ -409,6 +429,10 @@ bool VGeneralTab::loadConfiguration()
         return false;
     }
 
+    if (!loadKeyboardLayoutMapping()) {
+        return false;
+    }
+
     return true;
 }
 
@@ -430,6 +454,10 @@ bool VGeneralTab::saveConfiguration()
         return false;
     }
 
+    if (!saveKeyboardLayoutMapping()) {
+        return false;
+    }
+
     return true;
 }
 
@@ -528,6 +556,38 @@ bool VGeneralTab::saveQuickAccess()
     return true;
 }
 
+bool VGeneralTab::loadKeyboardLayoutMapping()
+{
+    m_keyboardLayoutCombo->clear();
+
+    m_keyboardLayoutCombo->addItem(tr("None"), "");
+
+    QStringList layouts = VKeyboardLayoutManager::availableLayouts();
+    for (auto const & layout : layouts) {
+        m_keyboardLayoutCombo->addItem(layout, layout);
+    }
+
+    int idx = 0;
+    const auto &cur = VKeyboardLayoutManager::currentLayout();
+    if (!cur.m_name.isEmpty()) {
+        idx = m_keyboardLayoutCombo->findData(cur.m_name);
+        if (idx == -1) {
+            idx = 0;
+            VKeyboardLayoutManager::setCurrentLayout("");
+        }
+    }
+
+    m_keyboardLayoutCombo->setCurrentIndex(idx);
+    return true;
+}
+
+bool VGeneralTab::saveKeyboardLayoutMapping()
+{
+    g_config->setKeyboardLayout(m_keyboardLayoutCombo->currentData().toString());
+    VKeyboardLayoutManager::update();
+    return true;
+}
+
 VLookTab::VLookTab(QWidget *p_parent)
     : QWidget(p_parent)
 {

+ 6 - 0
src/dialog/vsettingsdialog.h

@@ -40,6 +40,9 @@ private:
     bool loadQuickAccess();
     bool saveQuickAccess();
 
+    bool loadKeyboardLayoutMapping();
+    bool saveKeyboardLayoutMapping();
+
     // Language
     QComboBox *m_langCombo;
 
@@ -58,6 +61,9 @@ private:
     // Quick access note path.
     VLineEdit *m_quickAccessEdit;
 
+    // Keyboard layout mappings.
+    QComboBox *m_keyboardLayoutCombo;
+
     static const QVector<QString> c_availableLangs;
 };
 

+ 7 - 0
src/resources/icons/add.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<polygon style="fill:#000000" points="448,224 288,224 288,64 224,64 224,224 64,224 64,288 224,288 224,448 288,448 288,288 448,288 "/>
+</svg>

+ 10 - 0
src/resources/icons/delete.svg

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
+<path style="fill:#000000" d="M341,128V99c0-19.1-14.5-35-34.5-35H205.4C185.5,64,171,79.9,171,99v29H80v32h9.2c0,0,5.4,0.6,8.2,3.4c2.8,2.8,3.9,9,3.9,9
+	l19,241.7c1.5,29.4,1.5,33.9,36,33.9h199.4c34.5,0,34.5-4.4,36-33.8l19-241.6c0,0,1.1-6.3,3.9-9.1c2.8-2.8,8.2-3.4,8.2-3.4h9.2v-32
+	h-91V128z M192,99c0-9.6,7.8-15,17.7-15h91.7c9.9,0,18.6,5.5,18.6,15v29H192V99z M183.5,384l-10.3-192h20.3L204,384H183.5z
+	 M267.1,384h-22V192h22V384z M328.7,384h-20.4l10.5-192h20.3L328.7,384z"/>
+</svg>

+ 7 - 4
src/resources/vnote.ini

@@ -256,10 +256,6 @@ max_num_of_tag_labels=3
 ; 2 - web to editor
 smart_live_preview=3
 
-; Support multiple keyboard layout
-; Not valid on macOS
-multiple_keyboard_layout=true
-
 ; Whether insert new note in front
 insert_new_note_in_front=false
 
@@ -269,6 +265,13 @@ highlight_matches_in_page=true
 ; Incremental search in page
 find_incremental_search=true
 
+; Additional Qt::Key_XXX which will be mapped in different layouts
+; List of integer values.
+keyboard_layout_mapping_keys=
+
+; Chosen keyboard layout mapping from keyboard_layouts.ini
+keyboard_layout=
+
 [editor]
 ; Auto indent as previous line
 auto_indent=true

+ 6 - 2
src/src.pro

@@ -146,7 +146,9 @@ SOURCES += main.cpp\
     pegmarkdownhighlighter.cpp \
     pegparser.cpp \
     peghighlighterresult.cpp \
-    vtexteditcompleter.cpp
+    vtexteditcompleter.cpp \
+    utils/vkeyboardlayoutmanager.cpp \
+    dialog/vkeyboardlayoutmappingdialog.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
@@ -285,7 +287,9 @@ HEADERS  += vmainwindow.h \
     pegparser.h \
     peghighlighterresult.h \
     vtexteditcompleter.h \
-    vtextdocumentlayoutdata.h
+    vtextdocumentlayoutdata.h \
+    utils/vkeyboardlayoutmanager.h \
+    dialog/vkeyboardlayoutmappingdialog.h
 
 RESOURCES += \
     vnote.qrc \

+ 298 - 0
src/utils/vkeyboardlayoutmanager.cpp

@@ -0,0 +1,298 @@
+#include "vkeyboardlayoutmanager.h"
+
+#include <QSharedPointer>
+#include <QSettings>
+#include <QFileInfo>
+#include <QDebug>
+
+#include "vconfigmanager.h"
+
+extern VConfigManager *g_config;
+
+VKeyboardLayoutManager *VKeyboardLayoutManager::s_inst = NULL;
+
+VKeyboardLayoutManager *VKeyboardLayoutManager::inst()
+{
+    if (!s_inst) {
+        s_inst = new VKeyboardLayoutManager();
+        s_inst->update(g_config);
+    }
+
+    return s_inst;
+}
+
+static QSharedPointer<QSettings> layoutSettings(const VConfigManager *p_config,
+                                                bool p_create = false)
+{
+    QSharedPointer<QSettings> settings;
+    QString file = p_config->getKeyboardLayoutConfigFilePath();
+    if (file.isEmpty()) {
+        return settings;
+    }
+
+    if (!QFileInfo::exists(file) && !p_create) {
+        return settings;
+    }
+
+    settings.reset(new QSettings(file, QSettings::IniFormat));
+    return settings;
+}
+
+static void clearLayoutMapping(const QSharedPointer<QSettings> &p_settings,
+                               const QString &p_name)
+{
+    p_settings->beginGroup(p_name);
+    p_settings->remove("");
+    p_settings->endGroup();
+}
+
+static QHash<int, int> readLayoutMappingInternal(const QSharedPointer<QSettings> &p_settings,
+                                                 const QString &p_name)
+{
+    QHash<int, int> mappings;
+
+    p_settings->beginGroup(p_name);
+    QStringList keys = p_settings->childKeys();
+    for (auto const & key : keys) {
+        if (key.isEmpty()) {
+            continue;
+        }
+
+        bool ok;
+        int keyNum = key.toInt(&ok);
+        if (!ok) {
+            qWarning() << "readLayoutMappingInternal() skip bad key" << key << "in layout" << p_name;
+            continue;
+        }
+
+        int valNum = p_settings->value(key).toInt();
+        mappings.insert(keyNum, valNum);
+    }
+
+    p_settings->endGroup();
+
+    return mappings;
+}
+
+static bool writeLayoutMapping(const QSharedPointer<QSettings> &p_settings,
+                               const QString &p_name,
+                               const QHash<int, int> &p_mappings)
+{
+    clearLayoutMapping(p_settings, p_name);
+
+    p_settings->beginGroup(p_name);
+    for (auto it = p_mappings.begin(); it != p_mappings.end(); ++it) {
+        p_settings->setValue(QString::number(it.key()), it.value());
+    }
+    p_settings->endGroup();
+
+    return true;
+}
+
+void VKeyboardLayoutManager::update(VConfigManager *p_config)
+{
+    m_layout.clear();
+
+    m_layout.m_name = p_config->getKeyboardLayout();
+    if (m_layout.m_name.isEmpty()) {
+        // No mapping.
+        return;
+    }
+
+    qDebug() << "using keyboard layout mapping" << m_layout.m_name;
+
+    auto settings = layoutSettings(p_config);
+    if (settings.isNull()) {
+        return;
+    }
+
+    m_layout.setMapping(readLayoutMappingInternal(settings, m_layout.m_name));
+}
+
+void VKeyboardLayoutManager::update()
+{
+    inst()->update(g_config);
+}
+
+const VKeyboardLayoutManager::Layout &VKeyboardLayoutManager::currentLayout()
+{
+    return inst()->m_layout;
+}
+
+static QStringList readAvailableLayoutMappings(const QSharedPointer<QSettings> &p_settings)
+{
+    QString fullKey("global/layout_mappings");
+    return p_settings->value(fullKey).toStringList();
+}
+
+static void writeAvailableLayoutMappings(const QSharedPointer<QSettings> &p_settings,
+                                         const QStringList &p_layouts)
+{
+    QString fullKey("global/layout_mappings");
+    return p_settings->setValue(fullKey, p_layouts);
+}
+
+QStringList VKeyboardLayoutManager::availableLayouts()
+{
+    QStringList layouts;
+    auto settings = layoutSettings(g_config);
+    if (settings.isNull()) {
+        return layouts;
+    }
+
+    layouts = readAvailableLayoutMappings(settings);
+    return layouts;
+}
+
+void VKeyboardLayoutManager::setCurrentLayout(const QString &p_name)
+{
+    auto mgr = inst();
+    if (mgr->m_layout.m_name == p_name) {
+        return;
+    }
+
+    g_config->setKeyboardLayout(p_name);
+    mgr->update(g_config);
+}
+
+static bool isValidLayoutName(const QString &p_name)
+{
+    return !p_name.isEmpty() && p_name.toLower() != "global";
+}
+
+bool VKeyboardLayoutManager::addLayout(const QString &p_name)
+{
+    Q_ASSERT(isValidLayoutName(p_name));
+
+    auto settings = layoutSettings(g_config, true);
+    if (settings.isNull()) {
+        qWarning() << "fail to open keyboard layout QSettings";
+        return false;
+    }
+
+    QStringList layouts = readAvailableLayoutMappings(settings);
+    if (layouts.contains(p_name)) {
+        qWarning() << "Keyboard layout mapping" << p_name << "already exists";
+        return false;
+    }
+
+    layouts.append(p_name);
+    writeAvailableLayoutMappings(settings, layouts);
+
+    clearLayoutMapping(settings, p_name);
+    return true;
+}
+
+bool VKeyboardLayoutManager::removeLayout(const QString &p_name)
+{
+    Q_ASSERT(isValidLayoutName(p_name));
+
+    auto settings = layoutSettings(g_config, true);
+    if (settings.isNull()) {
+        qWarning() << "fail to open keyboard layout QSettings";
+        return false;
+    }
+
+    QStringList layouts = readAvailableLayoutMappings(settings);
+    int idx = layouts.indexOf(p_name);
+    if (idx == -1) {
+        return true;
+    }
+
+    layouts.removeAt(idx);
+    writeAvailableLayoutMappings(settings, layouts);
+
+    clearLayoutMapping(settings, p_name);
+    return true;
+}
+
+bool VKeyboardLayoutManager::renameLayout(const QString &p_name, const QString &p_newName)
+{
+    Q_ASSERT(isValidLayoutName(p_name));
+    Q_ASSERT(isValidLayoutName(p_newName));
+
+    auto settings = layoutSettings(g_config, true);
+    if (settings.isNull()) {
+        qWarning() << "fail to open keyboard layout QSettings";
+        return false;
+    }
+
+    QStringList layouts = readAvailableLayoutMappings(settings);
+    int idx = layouts.indexOf(p_name);
+    if (idx == -1) {
+        qWarning() << "fail to find keyboard layout mapping" << p_name << "to rename";
+        return false;
+    }
+
+    if (layouts.indexOf(p_newName) != -1) {
+        qWarning() << "keyboard layout mapping" << p_newName << "already exists";
+        return false;
+    }
+
+    auto content = readLayoutMappingInternal(settings, p_name);
+    // Copy the group.
+    if (!writeLayoutMapping(settings, p_newName, content)) {
+        qWarning() << "fail to write new layout mapping" << p_newName;
+        return false;
+    }
+
+    clearLayoutMapping(settings, p_name);
+
+    layouts.replace(idx, p_newName);
+    writeAvailableLayoutMappings(settings, layouts);
+
+    // Check current layout.
+    if (g_config->getKeyboardLayout() == p_name) {
+        Q_ASSERT(inst()->m_layout.m_name == p_name);
+        g_config->setKeyboardLayout(p_newName);
+        inst()->m_layout.m_name = p_newName;
+    }
+
+    return true;
+}
+
+QHash<int, int> VKeyboardLayoutManager::readLayoutMapping(const QString &p_name)
+{
+    QHash<int, int> mappings;
+    if (p_name.isEmpty()) {
+        return mappings;
+    }
+
+    auto settings = layoutSettings(g_config);
+    if (settings.isNull()) {
+        return mappings;
+    }
+
+    return readLayoutMappingInternal(settings, p_name);
+}
+
+bool VKeyboardLayoutManager::updateLayout(const QString &p_name,
+                                          const QHash<int, int> &p_mapping)
+{
+    Q_ASSERT(isValidLayoutName(p_name));
+
+    auto settings = layoutSettings(g_config, true);
+    if (settings.isNull()) {
+        qWarning() << "fail to open keyboard layout QSettings";
+        return false;
+    }
+
+    QStringList layouts = readAvailableLayoutMappings(settings);
+    int idx = layouts.indexOf(p_name);
+    if (idx == -1) {
+        qWarning() << "fail to find keyboard layout mapping" << p_name << "to update";
+        return false;
+    }
+
+    if (!writeLayoutMapping(settings, p_name, p_mapping)) {
+        qWarning() << "fail to write layout mapping" << p_name;
+        return false;
+    }
+
+    // Check current layout.
+    if (inst()->m_layout.m_name == p_name) {
+        inst()->m_layout.setMapping(p_mapping);
+    }
+
+    return true;
+}

+ 79 - 0
src/utils/vkeyboardlayoutmanager.h

@@ -0,0 +1,79 @@
+#ifndef VKEYBOARDLAYOUTMANAGER_H
+#define VKEYBOARDLAYOUTMANAGER_H
+
+#include <QString>
+#include <QStringList>
+#include <QHash>
+
+class VConfigManager;
+
+class VKeyboardLayoutManager
+{
+public:
+    struct Layout
+    {
+        void clear()
+        {
+            m_name.clear();
+            m_mapping.clear();
+        }
+
+        void setMapping(const QHash<int, int> &p_mapping)
+        {
+            m_mapping.clear();
+
+            for (auto it = p_mapping.begin(); it != p_mapping.end(); ++it) {
+                m_mapping.insert(it.value(), it.key());
+            }
+        }
+
+        QString m_name;
+        // Reversed mapping.
+        QHash<int, int> m_mapping;
+    };
+
+    static void update();
+
+    static const VKeyboardLayoutManager::Layout &currentLayout();
+
+    static void setCurrentLayout(const QString &p_name);
+
+    static QStringList availableLayouts();
+
+    static bool addLayout(const QString &p_name);
+
+    static bool removeLayout(const QString &p_name);
+
+    static bool renameLayout(const QString &p_name, const QString &p_newName);
+
+    static bool updateLayout(const QString &p_name, const QHash<int, int> &p_mapping);
+
+    static QHash<int, int> readLayoutMapping(const QString &p_name);
+
+    static int mapKey(int p_key);
+
+private:
+    VKeyboardLayoutManager() {}
+
+    static VKeyboardLayoutManager *inst();
+
+    void update(VConfigManager *p_config);
+
+    Layout m_layout;
+
+    static VKeyboardLayoutManager *s_inst;
+};
+
+inline int VKeyboardLayoutManager::mapKey(int p_key)
+{
+    const Layout &layout = inst()->m_layout;
+    if (!layout.m_name.isEmpty()) {
+        auto it = layout.m_mapping.find(p_key);
+        if (it != layout.m_mapping.end()) {
+            return it.value();
+        }
+    }
+
+    return p_key;
+}
+#endif // VKEYBOARDLAYOUTMANAGER_H

+ 4 - 0
src/utils/vutils.cpp

@@ -1376,6 +1376,10 @@ bool VUtils::isMetaKey(int p_key)
     return p_key == Qt::Key_Control
            || p_key == Qt::Key_Shift
            || p_key == Qt::Key_Meta
+#if defined(Q_OS_LINUX)
+           // For mapping Caps as Ctrl in KDE.
+           || p_key == Qt::Key_CapsLock
+#endif
            || p_key == Qt::Key_Alt;
 }
 

+ 1 - 5
src/utils/vvim.cpp

@@ -571,11 +571,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
 
         if (m_registerPending) {
             // Ctrl and Shift may be sent out first.
-            if (key == Qt::Key_Control
-                || key == Qt::Key_Shift
-                || key == Qt::Key_Meta
-                // For mapping Caps as Ctrl in KDE.
-                || key == Qt::Key_CapsLock) {
+            if (VUtils::isMetaKey(key)) {
                 goto accept;
             }
 

+ 2 - 4
src/vcaptain.cpp

@@ -8,6 +8,7 @@
 #include "vfilelist.h"
 #include "vnavigationmode.h"
 #include "vconfigmanager.h"
+#include "utils/vkeyboardlayoutmanager.h"
 
 extern VConfigManager *g_config;
 
@@ -95,10 +96,7 @@ void VCaptain::keyPressEvent(QKeyEvent *p_event)
         return;
     }
 
-    if (g_config->getMultipleKeyboardLayout()) {
-        // Use virtual key here for different layout.
-        key = p_event->nativeVirtualKey();
-    }
+    key = VKeyboardLayoutManager::mapKey(key);
 
     if (handleKeyPress(key, modifiers)) {
         p_event->accept();

+ 7 - 7
src/vconfigmanager.cpp

@@ -29,6 +29,8 @@ const QString VConfigManager::c_sessionConfigFile = QString("session.ini");
 
 const QString VConfigManager::c_snippetConfigFile = QString("snippet.json");
 
+const QString VConfigManager::c_keyboardLayoutConfigFile = QString("keyboard_layouts.ini");
+
 const QString VConfigManager::c_styleConfigFolder = QString("styles");
 
 const QString VConfigManager::c_themeConfigFolder = QString("themes");
@@ -314,13 +316,6 @@ void VConfigManager::initialize()
     m_smartLivePreview = getConfigFromSettings("global",
                                                "smart_live_preview").toInt();
 
-#if defined(Q_OS_MACOS) || defined(Q_OS_MAC)
-    m_multipleKeyboardLayout = false;
-#else
-    m_multipleKeyboardLayout = getConfigFromSettings("global",
-                                                     "multiple_keyboard_layout").toBool();
-#endif
-
     m_insertNewNoteInFront = getConfigFromSettings("global",
                                                    "insert_new_note_in_front").toBool();
 
@@ -862,6 +857,11 @@ const QString &VConfigManager::getSnippetConfigFilePath() const
     return path;
 }
 
+const QString VConfigManager::getKeyboardLayoutConfigFilePath() const
+{
+    return QDir(getConfigFolder()).filePath(c_keyboardLayoutConfigFile);
+}
+
 QString VConfigManager::getThemeFile() const
 {
     auto it = m_themes.find(m_theme);

+ 37 - 10
src/vconfigmanager.h

@@ -449,6 +449,8 @@ public:
 
     const QString &getSnippetConfigFilePath() const;
 
+    const QString getKeyboardLayoutConfigFilePath() const;
+
     // Read all available templates files in c_templateConfigFolder.
     QVector<QString> getNoteTemplates(DocType p_type = DocType::Unknown) const;
 
@@ -565,11 +567,14 @@ public:
     int getSmartLivePreview() const;
     void setSmartLivePreview(int p_preview);
 
-    bool getMultipleKeyboardLayout() const;
-
     bool getInsertNewNoteInFront() const;
     void setInsertNewNoteInFront(bool p_enabled);
 
+    QString getKeyboardLayout() const;
+    void setKeyboardLayout(const QString &p_name);
+
+    QList<int> getKeyboardLayoutMappingKeys() const;
+
 private:
     // Look up a config from user and default settings.
     QVariant getConfigFromSettings(const QString &section, const QString &key) const;
@@ -1019,9 +1024,6 @@ private:
     // Smart live preview.
     int m_smartLivePreview;
 
-    // Support multiple keyboard layout.
-    bool m_multipleKeyboardLayout;
-
     // Whether insert new note in front.
     bool m_insertNewNoteInFront;
 
@@ -1040,6 +1042,9 @@ private:
     // The name of the config file for snippets folder.
     static const QString c_snippetConfigFile;
 
+    // The name of the config file for keyboard layouts.
+    static const QString c_keyboardLayoutConfigFile;
+
     // QSettings for the user configuration
     QSettings *userSettings;
 
@@ -2613,11 +2618,6 @@ inline void VConfigManager::setSmartLivePreview(int p_preview)
     setConfigToSettings("global", "smart_live_preview", m_smartLivePreview);
 }
 
-inline bool VConfigManager::getMultipleKeyboardLayout() const
-{
-    return m_multipleKeyboardLayout;
-}
-
 inline bool VConfigManager::getInsertNewNoteInFront() const
 {
     return m_insertNewNoteInFront;
@@ -2657,4 +2657,31 @@ inline void VConfigManager::setHighlightMatchesInPage(bool p_enabled)
     m_highlightMatchesInPage = p_enabled;
     setConfigToSettings("global", "highlight_matches_in_page", m_highlightMatchesInPage);
 }
+
+inline QString VConfigManager::getKeyboardLayout() const
+{
+    return getConfigFromSettings("global", "keyboard_layout").toString();
+}
+
+inline void VConfigManager::setKeyboardLayout(const QString &p_name)
+{
+    setConfigToSettings("global", "keyboard_layout", p_name);
+}
+
+inline QList<int> VConfigManager::getKeyboardLayoutMappingKeys() const
+{
+    QStringList keyStrs = getConfigFromSettings("global",
+                                                "keyboard_layout_mapping_keys").toStringList();
+
+    QList<int> keys;
+    for (auto & str : keyStrs) {
+        bool ok;
+        int tmp = str.toInt(&ok);
+        if (ok) {
+            keys.append(tmp);
+        }
+    }
+
+    return keys;
+}
 #endif // VCONFIGMANAGER_H

+ 2 - 0
src/vnote.qrc

@@ -274,5 +274,7 @@
         <file>resources/export/export_template.html</file>
         <file>resources/export/outline.css</file>
         <file>resources/export/outline.js</file>
+        <file>resources/icons/add.svg</file>
+        <file>resources/icons/delete.svg</file>
     </qresource>
 </RCC>