From 574aa4e70a335a4705c4e3845b6f07435fe1a37c Mon Sep 17 00:00:00 2001 From: Le Tan Date: Sat, 29 Sep 2018 18:37:26 +0800 Subject: [PATCH] KeyboardLayout: support specifying keyboard layout mappings Captain mode now supports different layout mappings. --- src/dialog/vkeyboardlayoutmappingdialog.cpp | 497 ++++++++++++++++++++ src/dialog/vkeyboardlayoutmappingdialog.h | 73 +++ src/dialog/vsettingsdialog.cpp | 60 +++ src/dialog/vsettingsdialog.h | 6 + src/resources/icons/add.svg | 7 + src/resources/icons/delete.svg | 10 + src/resources/vnote.ini | 11 +- src/src.pro | 8 +- src/utils/vkeyboardlayoutmanager.cpp | 298 ++++++++++++ src/utils/vkeyboardlayoutmanager.h | 79 ++++ src/utils/vutils.cpp | 4 + src/utils/vvim.cpp | 6 +- src/vcaptain.cpp | 6 +- src/vconfigmanager.cpp | 14 +- src/vconfigmanager.h | 47 +- src/vnote.qrc | 2 + 16 files changed, 1096 insertions(+), 32 deletions(-) create mode 100644 src/dialog/vkeyboardlayoutmappingdialog.cpp create mode 100644 src/dialog/vkeyboardlayoutmappingdialog.h create mode 100644 src/resources/icons/add.svg create mode 100644 src/resources/icons/delete.svg create mode 100644 src/utils/vkeyboardlayoutmanager.cpp create mode 100644 src/utils/vkeyboardlayoutmanager.h diff --git a/src/dialog/vkeyboardlayoutmappingdialog.cpp b/src/dialog/vkeyboardlayoutmappingdialog.cpp new file mode 100644 index 00000000..e5a02463 --- /dev/null +++ b/src/dialog/vkeyboardlayoutmappingdialog.cpp @@ -0,0 +1,497 @@ +#include "vkeyboardlayoutmappingdialog.h" + +#include + +#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(&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 %2.") + .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 %2.") + .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 %2.") + .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 keysNeededToMap() +{ + QList 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 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 &p_mappings) +{ + QList 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 retrieveMappingFromTree(QTreeWidget *p_tree) +{ + QHash 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 %2.") + .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 mappings = retrieveMappingFromTree(m_contentTree); + if (!VKeyboardLayoutManager::updateLayout(mapping, mappings)) { + VUtils::showMessage(QMessageBox::Warning, + tr("Warning"), + tr("Fail to update mapping %2.") + .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(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")); +} diff --git a/src/dialog/vkeyboardlayoutmappingdialog.h b/src/dialog/vkeyboardlayoutmappingdialog.h new file mode 100644 index 00000000..fa219b08 --- /dev/null +++ b/src/dialog/vkeyboardlayoutmappingdialog.h @@ -0,0 +1,73 @@ +#ifndef VKEYBOARDLAYOUTMAPPINGDIALOG_H +#define VKEYBOARDLAYOUTMAPPINGDIALOG_H + +#include + + +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 diff --git a/src/dialog/vsettingsdialog.cpp b/src/dialog/vsettingsdialog.cpp index f5550143..03dc889e 100644 --- a/src/dialog/vsettingsdialog.cpp +++ b/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) { diff --git a/src/dialog/vsettingsdialog.h b/src/dialog/vsettingsdialog.h index 89cad234..7bb9eaa9 100644 --- a/src/dialog/vsettingsdialog.h +++ b/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 c_availableLangs; }; diff --git a/src/resources/icons/add.svg b/src/resources/icons/add.svg new file mode 100644 index 00000000..b9e8f00d --- /dev/null +++ b/src/resources/icons/add.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/src/resources/icons/delete.svg b/src/resources/icons/delete.svg new file mode 100644 index 00000000..382a85ce --- /dev/null +++ b/src/resources/icons/delete.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/src/resources/vnote.ini b/src/resources/vnote.ini index f4836129..23b85ef5 100644 --- a/src/resources/vnote.ini +++ b/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 diff --git a/src/src.pro b/src/src.pro index ffa15725..bdffbe10 100644 --- a/src/src.pro +++ b/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 \ diff --git a/src/utils/vkeyboardlayoutmanager.cpp b/src/utils/vkeyboardlayoutmanager.cpp new file mode 100644 index 00000000..d507b8ec --- /dev/null +++ b/src/utils/vkeyboardlayoutmanager.cpp @@ -0,0 +1,298 @@ +#include "vkeyboardlayoutmanager.h" + +#include +#include +#include +#include + +#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 layoutSettings(const VConfigManager *p_config, + bool p_create = false) +{ + QSharedPointer 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 &p_settings, + const QString &p_name) +{ + p_settings->beginGroup(p_name); + p_settings->remove(""); + p_settings->endGroup(); +} + +static QHash readLayoutMappingInternal(const QSharedPointer &p_settings, + const QString &p_name) +{ + QHash 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 &p_settings, + const QString &p_name, + const QHash &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 &p_settings) +{ + QString fullKey("global/layout_mappings"); + return p_settings->value(fullKey).toStringList(); +} + +static void writeAvailableLayoutMappings(const QSharedPointer &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 VKeyboardLayoutManager::readLayoutMapping(const QString &p_name) +{ + QHash 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 &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; +} diff --git a/src/utils/vkeyboardlayoutmanager.h b/src/utils/vkeyboardlayoutmanager.h new file mode 100644 index 00000000..162060ca --- /dev/null +++ b/src/utils/vkeyboardlayoutmanager.h @@ -0,0 +1,79 @@ +#ifndef VKEYBOARDLAYOUTMANAGER_H +#define VKEYBOARDLAYOUTMANAGER_H + +#include +#include +#include + +class VConfigManager; + +class VKeyboardLayoutManager +{ +public: + struct Layout + { + void clear() + { + m_name.clear(); + m_mapping.clear(); + } + + void setMapping(const QHash &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 m_mapping; + }; + + static void update(); + + static const VKeyboardLayoutManager::Layout ¤tLayout(); + + 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 &p_mapping); + + static QHash 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 diff --git a/src/utils/vutils.cpp b/src/utils/vutils.cpp index 8fcfe717..946b8038 100644 --- a/src/utils/vutils.cpp +++ b/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; } diff --git a/src/utils/vvim.cpp b/src/utils/vvim.cpp index 42730bd9..a2aa7741 100644 --- a/src/utils/vvim.cpp +++ b/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; } diff --git a/src/vcaptain.cpp b/src/vcaptain.cpp index 1e56270b..9b8f3694 100644 --- a/src/vcaptain.cpp +++ b/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(); diff --git a/src/vconfigmanager.cpp b/src/vconfigmanager.cpp index de77130c..ff464063 100644 --- a/src/vconfigmanager.cpp +++ b/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); diff --git a/src/vconfigmanager.h b/src/vconfigmanager.h index c580bbf0..542090bd 100644 --- a/src/vconfigmanager.h +++ b/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 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 getKeyboardLayoutMappingKeys() const; + private: // Look up a config from user and default settings. QVariant getConfigFromSettings(const QString §ion, 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 VConfigManager::getKeyboardLayoutMappingKeys() const +{ + QStringList keyStrs = getConfigFromSettings("global", + "keyboard_layout_mapping_keys").toStringList(); + + QList keys; + for (auto & str : keyStrs) { + bool ok; + int tmp = str.toInt(&ok); + if (ok) { + keys.append(tmp); + } + } + + return keys; +} #endif // VCONFIGMANAGER_H diff --git a/src/vnote.qrc b/src/vnote.qrc index 8532a8c3..d9c39e66 100644 --- a/src/vnote.qrc +++ b/src/vnote.qrc @@ -274,5 +274,7 @@ resources/export/export_template.html resources/export/outline.css resources/export/outline.js + resources/icons/add.svg + resources/icons/delete.svg