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();
+}