diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index f1061bdb..f953c243 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -2639,6 +2639,8 @@ + + @@ -2905,6 +2907,11 @@ Export Map Stitch Image... + + + Edit Shortcuts... + + diff --git a/forms/shortcutseditor.ui b/forms/shortcutseditor.ui new file mode 100644 index 00000000..31b7014a --- /dev/null +++ b/forms/shortcutseditor.ui @@ -0,0 +1,48 @@ + + + ShortcutsEditor + + + + 0 + 0 + 540 + 640 + + + + Shortcuts Editor + + + + + + true + + + + + 0 + 0 + 516 + 580 + + + + + + + + + QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults + + + false + + + + + + + + diff --git a/include/config.h b/include/config.h index 008eeeb7..4c4543d4 100644 --- a/include/config.h +++ b/include/config.h @@ -5,6 +5,8 @@ #include #include #include +#include +#include enum MapSortOrder { Group = 0, @@ -192,4 +194,37 @@ private: extern ProjectConfig projectConfig; +class QAction; + +class ShortcutsConfig : public KeyValueConfigBase +{ +public: + ShortcutsConfig() : + shortcuts(QMultiMap()), + defaultShortcuts(QMultiMap()) + { + reset(); + } + virtual void reset() override { shortcuts.clear(); } + void setDefaultShortcuts(const QList &actions); + QList getDefaultShortcuts(QAction *action) const; + void setUserShortcuts(const QList &actions); + QList getUserShortcuts(QAction *action) const; + +protected: + virtual QString getConfigFilepath() override; + virtual void parseConfigKeyValue(QString key, QString value) override; + virtual QMap getKeyValueMap() override; + virtual void onNewConfigFileCreated() override {}; + virtual void setUnreadKeys() override {}; + +private: + QMultiMap shortcuts; + QMultiMap defaultShortcuts; + + QString getKey(QObject *object) const; +}; + +extern ShortcutsConfig shortcutsConfig; + #endif // CONFIG_H diff --git a/include/mainwindow.h b/include/mainwindow.h index a9180402..f406f87f 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -21,6 +21,7 @@ #include "filterchildrenproxymodel.h" #include "newmappopup.h" #include "newtilesetdialog.h" +#include "shortcutseditor.h" namespace Ui { class MainWindow; @@ -147,6 +148,7 @@ private slots: void on_actionUse_Encounter_Json_triggered(bool checked); void on_actionMonitor_Project_Files_triggered(bool checked); void on_actionUse_Poryscript_triggered(bool checked); + void on_actionEdit_Shortcuts_triggered(); void on_mainTabBar_tabBarClicked(int index); @@ -231,6 +233,7 @@ private: Ui::MainWindow *ui; TilesetEditor *tilesetEditor = nullptr; RegionMapEditor *regionMapEditor = nullptr; + ShortcutsEditor *shortcutsEditor = nullptr; MapImageExporter *mapImageExporter = nullptr; FilterChildrenProxyModel *mapListProxyModel; NewMapPopup *newmapprompt = nullptr; @@ -294,6 +297,7 @@ private: void initEditor(); void initMiscHeapObjects(); void initMapSortOrder(); + void initUserShortcuts(); void setProjectSpecificUIVisibility(); void loadUserSettings(); void applyMapListFilter(QString filterText); diff --git a/include/ui/shortcutseditor.h b/include/ui/shortcutseditor.h new file mode 100644 index 00000000..b5aadc92 --- /dev/null +++ b/include/ui/shortcutseditor.h @@ -0,0 +1,80 @@ +#ifndef SHORTCUTSEDITOR_H +#define SHORTCUTSEDITOR_H + +#include +#include + +class QAbstractButton; +class QAction; +class QKeySequenceEdit; +class ActionShortcutEdit; + + +namespace Ui { +class ShortcutsEditor; +} + +class ShortcutsEditor : public QDialog +{ + Q_OBJECT + +public: + explicit ShortcutsEditor(QWidget *parent = nullptr); + ~ShortcutsEditor(); + +private: + Ui::ShortcutsEditor *ui; + QMap actions; + QWidget *ase_container; + + void populateShortcuts(); + void saveShortcuts(); + void resetShortcuts(); + void promptUser(ActionShortcutEdit *current, ActionShortcutEdit *sender); + +private slots: + void checkForDuplicates(); + void dialogButtonClicked(QAbstractButton *button); +}; + + +// A collection of QKeySequenceEdit's in a QHBoxLayout with a cooresponding QAction +class ActionShortcutEdit : public QWidget +{ + Q_OBJECT + +public: + explicit ActionShortcutEdit(QWidget *parent = nullptr, QAction *action = nullptr, int count = 1); + + bool eventFilter(QObject *watched, QEvent *event) override; + + int count() const { return kse_children.count(); } + void setCount(int count); + QList shortcuts() const; + void setShortcuts(const QList &keySequences); + void applyShortcuts(); + bool removeOne(const QKeySequence &keySequence); + bool contains(const QKeySequence &keySequence); + bool contains(QKeySequenceEdit *keySequenceEdit); + QKeySequence last() const { return shortcuts().last(); } + + QAction *action; + +public slots: + void clear(); + +signals: + void editingFinished(); + +private: + QVector kse_children; + QList ks_list; + + void updateShortcuts() { setShortcuts(shortcuts()); } + void focusLast(); + +private slots: + void onEditingFinished(); +}; + +#endif // SHORTCUTSEDITOR_H diff --git a/porymap.pro b/porymap.pro index 0da4a14b..f970606c 100644 --- a/porymap.pro +++ b/porymap.pro @@ -71,6 +71,7 @@ SOURCES += src/core/block.cpp \ src/ui/newtilesetdialog.cpp \ src/ui/flowlayout.cpp \ src/ui/mapruler.cpp \ + src/ui/shortcutseditor.cpp \ src/config.cpp \ src/editor.cpp \ src/main.cpp \ @@ -140,6 +141,7 @@ HEADERS += include/core/block.h \ include/ui/overlay.h \ include/ui/flowlayout.h \ include/ui/mapruler.h \ + include/ui/shortcutseditor.h \ include/config.h \ include/editor.h \ include/mainwindow.h \ @@ -156,7 +158,8 @@ FORMS += forms/mainwindow.ui \ forms/newmappopup.ui \ forms/aboutporymap.ui \ forms/newtilesetdialog.ui \ - forms/mapimageexporter.ui + forms/mapimageexporter.ui \ + forms/shortcutseditor.ui RESOURCES += \ resources/images.qrc \ diff --git a/src/config.cpp b/src/config.cpp index 15df7d43..2c009b36 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -11,6 +11,7 @@ #include #include #include +#include KeyValueConfigBase::~KeyValueConfigBase() { @@ -414,7 +415,7 @@ ProjectConfig projectConfig; QString ProjectConfig::getConfigFilepath() { // porymap config file is in the same directory as porymap itself. - return QDir(this->projectDir).filePath("porymap.project.cfg");; + return QDir(this->projectDir).filePath("porymap.project.cfg"); } void ProjectConfig::parseConfigKeyValue(QString key, QString value) { @@ -703,3 +704,84 @@ void ProjectConfig::setCustomScripts(QList scripts) { QList ProjectConfig::getCustomScripts() { return this->customScripts; } + +ShortcutsConfig shortcutsConfig; + +QString ShortcutsConfig::getConfigFilepath() { + QString settingsPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + QDir dir(settingsPath); + if (!dir.exists()) + dir.mkpath(settingsPath); + + QString configPath = dir.absoluteFilePath("porymap.shortcuts.cfg"); + + return configPath; +} + +void ShortcutsConfig::parseConfigKeyValue(QString key, QString value) { + QStringList keySeqs = value.split(' '); + if (keySeqs.isEmpty()) + shortcuts.insert(key, value); + else for (auto keySeq : keySeqs) + shortcuts.insert(key, keySeq); +} + +QMap ShortcutsConfig::getKeyValueMap() { + QMap map; + for (auto key : shortcuts.uniqueKeys()) { + auto keySeqs = shortcuts.values(key); + QStringList values; + for (auto keySeq : keySeqs) + values.append(keySeq.toString()); + QString value = values.join(' '); + map.insert(key, value); + } + return map; +} + +// Call this before applying user shortcuts to be able to restore default shortcuts. +void ShortcutsConfig::setDefaultShortcuts(const QList &actions) { + defaultShortcuts.clear(); + for (auto *action : actions) { + const QString key = getKey(action); + bool addToUserShortcuts = !shortcuts.contains(key); + for (auto shortcut : action->shortcuts()) { + defaultShortcuts.insert(key, shortcut); + if (addToUserShortcuts) + shortcuts.insert(key, shortcut); + } + } + save(); +} + +QList ShortcutsConfig::getDefaultShortcuts(QAction *action) const { + return defaultShortcuts.values(getKey(action)); +} + +void ShortcutsConfig::setUserShortcuts(const QList &actions) { + shortcuts.clear(); + for (auto *action : actions) { + const QString key = getKey(action); + if (action->shortcuts().isEmpty()) + shortcuts.insert(key, QKeySequence()); + else for (auto shortcut : action->shortcuts()) + shortcuts.insert(key, shortcut); + } + save(); +} + +QList ShortcutsConfig::getUserShortcuts(QAction *action) const { + return shortcuts.values(getKey(action)); +} + +QString ShortcutsConfig::getKey(QObject *object) const { + QString key = object->objectName(); + QRegularExpression re("[A-Z]"); + int i = key.indexOf(re); + while (i != -1) { + if (key.at(i - 1) != '_') + key.insert(i++, '_'); + i = key.indexOf(re, i + 1); + } + return key.toLower(); +} diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index f239d4d6..eb4a2de9 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -92,6 +92,7 @@ void MainWindow::initWindow() { this->initEditor(); this->initMiscHeapObjects(); this->initMapSortOrder(); + this->initUserShortcuts(); this->restoreWindowState(); setWindowDisabled(true); @@ -106,6 +107,13 @@ void MainWindow::initExtraShortcuts() { ui->actionZoom_In->setShortcuts({QKeySequence("Ctrl++"), QKeySequence("Ctrl+=")}); } +void MainWindow::initUserShortcuts() { + shortcutsConfig.load(); + shortcutsConfig.setDefaultShortcuts(findChildren()); + for (auto *action : findChildren()) + action->setShortcuts(shortcutsConfig.getUserShortcuts(action)); +} + void MainWindow::initCustomUI() { // Set up the tab bar ui->mainTabBar->addTab("Map"); @@ -158,9 +166,11 @@ void MainWindow::initEditor() { this->loadUserSettings(); undoAction = editor->editGroup.createUndoAction(this, tr("&Undo")); + undoAction->setObjectName("action_Undo"); undoAction->setShortcut(QKeySequence("Ctrl+Z")); redoAction = editor->editGroup.createRedoAction(this, tr("&Redo")); + redoAction->setObjectName("action_Redo"); redoAction->setShortcuts({QKeySequence("Ctrl+Y"), QKeySequence("Ctrl+Shift+Z")}); ui->menuEdit->addAction(undoAction); @@ -171,7 +181,8 @@ void MainWindow::initEditor() { undoView->setAttribute(Qt::WA_QuitOnClose, false); // Show the EditHistory dialog with Ctrl+E - QAction *showHistory = new QAction("Show Edit History..."); + QAction *showHistory = new QAction("Show Edit History...", this); + showHistory->setObjectName("action_ShowEditHistory"); showHistory->setShortcut(QKeySequence("Ctrl+E")); connect(showHistory, &QAction::triggered, [undoView](){ undoView->show(); }); @@ -1403,6 +1414,19 @@ void MainWindow::on_actionUse_Poryscript_triggered(bool checked) projectConfig.setUsePoryScript(checked); } +void MainWindow::on_actionEdit_Shortcuts_triggered() +{ + if (!shortcutsEditor) + shortcutsEditor = new ShortcutsEditor(this); + + if (shortcutsEditor->isHidden()) + shortcutsEditor->show(); + else if (shortcutsEditor->isMinimized()) + shortcutsEditor->showNormal(); + else + shortcutsEditor->activateWindow(); +} + void MainWindow::on_actionPencil_triggered() { on_toolButton_Paint_clicked(); diff --git a/src/ui/shortcutseditor.cpp b/src/ui/shortcutseditor.cpp new file mode 100644 index 00000000..97a3ebf4 --- /dev/null +++ b/src/ui/shortcutseditor.cpp @@ -0,0 +1,234 @@ +#include "shortcutseditor.h" +#include "ui_shortcutseditor.h" +#include "config.h" +#include "log.h" + +#include +#include +#include +#include +#include +#include + + +ShortcutsEditor::ShortcutsEditor(QWidget *parent) : + QDialog(parent), + ui(new Ui::ShortcutsEditor), + actions(QMap()) +{ + ui->setupUi(this); + ase_container = ui->scrollAreaWidgetContents_Shortcuts; + connect(ui->buttonBox, &QDialogButtonBox::clicked, + this, &ShortcutsEditor::dialogButtonClicked); + populateShortcuts(); +} + +ShortcutsEditor::~ShortcutsEditor() +{ + delete ui; +} + +void ShortcutsEditor::populateShortcuts() { + if (!parent()) + return; + + for (auto action : parent()->findChildren()) + if (!action->text().isEmpty() && !action->objectName().isEmpty()) + actions.insert(action->text().remove('&'), action); + + auto *formLayout = new QFormLayout(ase_container); + for (auto *action : actions) { + auto userShortcuts = shortcutsConfig.getUserShortcuts(action); + auto *ase = new ActionShortcutEdit(ase_container, action, 2); + connect(ase, &ActionShortcutEdit::editingFinished, + this, &ShortcutsEditor::checkForDuplicates); + ase->setShortcuts(userShortcuts); + formLayout->addRow(action->text(), ase); + } +} + +void ShortcutsEditor::saveShortcuts() { + auto ase_children = ase_container->findChildren(QString(), Qt::FindDirectChildrenOnly); + for (auto *ase : ase_children) + ase->applyShortcuts(); + shortcutsConfig.setUserShortcuts(actions.values()); +} + +void ShortcutsEditor::resetShortcuts() { + auto ase_children = ase_container->findChildren(QString(), Qt::FindDirectChildrenOnly); + for (auto *ase : ase_children) + ase->setShortcuts(shortcutsConfig.getDefaultShortcuts(ase->action)); +} + +void ShortcutsEditor::promptUser(ActionShortcutEdit *current, ActionShortcutEdit *sender) { + auto result = QMessageBox::question( + this, + "porymap", + QString("Shortcut \"%1\" is already used by \"%2\", would you like to replace it?") + .arg(sender->last().toString()).arg(current->action->text()), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (result == QMessageBox::Yes) + current->removeOne(sender->last()); + else if (result == QMessageBox::No) + sender->removeOne(sender->last()); +} + +void ShortcutsEditor::checkForDuplicates() { + auto *sender_ase = qobject_cast(sender()); + if (!sender_ase) + return; + + for (auto *child_kse : findChildren()) { + if (child_kse->keySequence().isEmpty() || child_kse->parent() == sender()) + continue; + + if (sender_ase->contains(child_kse->keySequence())) { + auto *current_ase = qobject_cast(child_kse->parent()); + if (!current_ase) + continue; + + promptUser(current_ase, sender_ase); + activateWindow(); + return; + } + } +} + +void ShortcutsEditor::dialogButtonClicked(QAbstractButton *button) { + auto buttonRole = ui->buttonBox->buttonRole(button); + if (buttonRole == QDialogButtonBox::AcceptRole) { + saveShortcuts(); + hide(); + } else if (buttonRole == QDialogButtonBox::ApplyRole) { + saveShortcuts(); + } else if (buttonRole == QDialogButtonBox::RejectRole) { + hide(); + } else if (buttonRole == QDialogButtonBox::ResetRole) { + resetShortcuts(); + } +} + + +ActionShortcutEdit::ActionShortcutEdit(QWidget *parent, QAction *action, int count) : + QWidget(parent), + action(action), + kse_children(QVector()), + ks_list(QList()) +{ + setLayout(new QHBoxLayout(this)); + layout()->setContentsMargins(0, 0, 0, 0); + setCount(count); +} + +bool ActionShortcutEdit::eventFilter(QObject *watched, QEvent *event) { + auto *watched_kse = qobject_cast(watched); + if (!watched_kse) + return false; + + if (event->type() == QEvent::KeyPress) { + auto *keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Escape) { + watched_kse->clearFocus(); + return true; + } else if (keyEvent->key() == Qt::Key_Backspace) { + removeOne(watched_kse->keySequence()); + return true; + } else { + watched_kse->clear(); + } + } + return false; +} + +void ActionShortcutEdit::setCount(int count) { + if (count < 1) + count = 1; + + while (kse_children.count() > count) { + layout()->removeWidget(kse_children.last()); + delete kse_children.last(); + kse_children.removeLast(); + } + + while (kse_children.count() < count) { + auto *kse = new QKeySequenceEdit(this); + connect(kse, &QKeySequenceEdit::editingFinished, + this, &ActionShortcutEdit::onEditingFinished); + kse->installEventFilter(this); + layout()->addWidget(kse); + kse_children.append(kse); + } +} + +QList ActionShortcutEdit::shortcuts() const { + QList current_ks_list; + for (auto *kse : kse_children) + if (!kse->keySequence().isEmpty()) + current_ks_list.append(kse->keySequence()); + return current_ks_list; +} + +void ActionShortcutEdit::setShortcuts(const QList &keySequences) { + clear(); + ks_list = keySequences; + int minCount = qMin(kse_children.count(), ks_list.count()); + for (int i = 0; i < minCount; ++i) + kse_children[i]->setKeySequence(ks_list[i]); +} + +void ActionShortcutEdit::applyShortcuts() { + action->setShortcuts(shortcuts()); +} + +bool ActionShortcutEdit::removeOne(const QKeySequence &keySequence) { + for (auto *kse : kse_children) { + if (kse->keySequence() == keySequence) { + ks_list.removeOne(keySequence); + kse->clear(); + updateShortcuts(); + return true; + } + } + return false; +} + +bool ActionShortcutEdit::contains(const QKeySequence &keySequence) { + for (auto ks : shortcuts()) + if (ks == keySequence) + return true; + return false; +} + +bool ActionShortcutEdit::contains(QKeySequenceEdit *keySequenceEdit) { + for (auto *kse : kse_children) + if (kse == keySequenceEdit) + return true; + return false; +} + +void ActionShortcutEdit::clear() { + for (auto *kse : kse_children) + kse->clear(); +} + +void ActionShortcutEdit::focusLast() { + for (int i = count() - 1; i >= 0; --i) { + if (!kse_children[i]->keySequence().isEmpty()) { + kse_children[i]->setFocus(); + return; + } + } +} + +void ActionShortcutEdit::onEditingFinished() { + auto *kse = qobject_cast(sender()); + if (!kse) + return; + + if (ks_list.contains(kse->keySequence())) + removeOne(kse->keySequence()); + updateShortcuts(); + focusLast(); + emit editingFinished(); +}