diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 00000000..40c9e1ba
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,40 @@
+name: Build Porymap
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+jobs:
+ build:
+ strategy:
+ matrix:
+ os: [ubuntu-latest, macos-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
+ - uses: actions/checkout@v2
+
+ - name: Cache Qt
+ id: cache-qt
+ uses: actions/cache@v1
+ with:
+ path: ../Qt
+ key: ${{ runner.os }}-QtCache
+
+ - name: Install Qt
+ uses: jurplel/install-qt-action@v2
+ with:
+ version: '5.14.2'
+ modules: 'qtwidgets qtqml'
+ cached: ${{ steps.cache-qt.outputs.cache-hit }}
+
+ - name: Configure
+ run: qmake porymap.pro
+
+ - name: Compile
+ run: make
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa367a92..a41a90b2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d
- The window sizes and positions of the tileset editor, palette editor, and region map editor are now stored in `porymap.cfg`.
- Add ruler tool for measuring metatile distance in events tab (Right-click to turn on/off, left-click to lock in place).
- Add delete button to wild pokemon encounters tab.
+- Add shortcut customization via `Options -> Edit Shortcuts`.
- Add custom text editor commands in `Options -> Edit Preferences`, a tool-button next to the `Script` combo-box, and `Tools -> Open Project in Text Editor`. The tool-button will open the containing file to the cooresponding script.
### Changed
diff --git a/README.md b/README.md
index f6f7a8b8..f1bd58d1 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# porymap
+[](https://github.com/huderlem/porymap/actions)
+
A map editor for the generation 3 decompilation projects using Qt.
Currently supports [pokeruby][pokeruby], [pokeemerald][pokeemerald], and [pokefirered][pokefirered].
diff --git a/docsrc/manual/images/shortcuts/edit-shortcuts.gif b/docsrc/manual/images/shortcuts/edit-shortcuts.gif
new file mode 100644
index 00000000..274abe8b
Binary files /dev/null and b/docsrc/manual/images/shortcuts/edit-shortcuts.gif differ
diff --git a/docsrc/manual/shortcuts.rst b/docsrc/manual/shortcuts.rst
index 2cea79a2..05a5cfd1 100644
--- a/docsrc/manual/shortcuts.rst
+++ b/docsrc/manual/shortcuts.rst
@@ -2,8 +2,16 @@
Shortcuts
*********
-Porymap has many shortcuts and it can sometimes be hard to keep track of them all.
-Here is a comprehensive list of the shortcuts provided all in one place for your convenience.
+Porymap has many keyboard shortcuts set by default, and even more that can be customized yourself. You can view and customize your shortcuts by going to *Options -> Edit Shortcuts*. Shortcut actions are grouped together by the window that they are used in (Main Window, Tileset Editor...). You can set up to 2 shortcuts per action, but you cannot have duplicate shortcuts set within the same window. If you enter a shortcut that is already in use, Porymap will prompt you cancel or overwrite the old shortcut. You can also restore your shortcuts to the defaults.
+
+.. figure:: images/shortcuts/edit-shortcuts.gif
+ :alt: Edit Shortcuts
+
+ Edit Shortcuts
+
+Your shortcuts are stored at ``%Appdata%\pret\porymap\porymap.shortcuts.cfg`` on Windows and ``~/Library/Application\ Support/pret/porymap/porymap.shortcuts.cfg`` on macOS).
+
+For reference, here is a comprehensive list of the default shortcuts set in Porymap.
Main Window
-----------
diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui
index 4a844c89..941a3179 100644
--- a/forms/mainwindow.ui
+++ b/forms/mainwindow.ui
@@ -2658,6 +2658,7 @@
+
@@ -2932,6 +2933,11 @@
Open Project in Text Editor
+
+
+ Edit Shortcuts...
+
+
diff --git a/forms/shortcutseditor.ui b/forms/shortcutseditor.ui
new file mode 100644
index 00000000..fab28d98
--- /dev/null
+++ b/forms/shortcutseditor.ui
@@ -0,0 +1,89 @@
+
+
+ ShortcutsEditor
+
+
+
+ 0
+ 0
+ 800
+ 700
+
+
+
+ Shortcuts Editor
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ QFrame::Plain
+
+
+ 0
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 794
+ 642
+
+
+
+
+
+ -
+
+
+ QFrame::Box
+
+
+ QFrame::Raised
+
+
+
-
+
+
+ QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/include/config.h b/include/config.h
index 37c5fb25..bd0a1296 100644
--- a/include/config.h
+++ b/include/config.h
@@ -5,6 +5,8 @@
#include
#include
#include
+#include
+#include
enum MapSortOrder {
Group = 0,
@@ -200,4 +202,53 @@ private:
extern ProjectConfig projectConfig;
+class QAction;
+class Shortcut;
+
+class ShortcutsConfig : public KeyValueConfigBase
+{
+public:
+ ShortcutsConfig() :
+ user_shortcuts({ }),
+ default_shortcuts({ })
+ { }
+
+ virtual void reset() override { user_shortcuts.clear(); }
+
+ // Call this before applying user shortcuts so that the user can restore defaults.
+ void setDefaultShortcuts(const QObjectList &objects);
+ QList defaultShortcuts(const QObject *object) const;
+
+ void setUserShortcuts(const QObjectList &objects);
+ void setUserShortcuts(const QMultiMap &objects_keySequences);
+ QList userShortcuts(const QObject *object) 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 user_shortcuts;
+ QMultiMap default_shortcuts;
+
+ enum StoreType {
+ User,
+ Default
+ };
+
+ QString cfgKey(const QObject *object) const;
+ QList currentShortcuts(const QObject *object) const;
+
+ void storeShortcutsFromList(StoreType storeType, const QObjectList &objects);
+ void storeShortcuts(
+ StoreType storeType,
+ const QString &cfgKey,
+ const QList &keySequences);
+};
+
+extern ShortcutsConfig shortcutsConfig;
+
#endif // CONFIG_H
diff --git a/include/mainwindow.h b/include/mainwindow.h
index 653bd2bc..f5cab1e8 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"
#include "preferenceeditor.h"
namespace Ui {
@@ -128,6 +129,7 @@ private slots:
void onNewMapCreated();
void onMapCacheCleared();
void onMapRulerStatusChanged(const QString &);
+ void applyUserShortcuts();
void on_action_NewMap_triggered();
void on_actionNew_Tileset_triggered();
@@ -147,6 +149,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);
@@ -234,6 +237,7 @@ private:
QLabel *label_MapRulerStatus;
TilesetEditor *tilesetEditor = nullptr;
RegionMapEditor *regionMapEditor = nullptr;
+ ShortcutsEditor *shortcutsEditor = nullptr;
MapImageExporter *mapImageExporter = nullptr;
FilterChildrenProxyModel *mapListProxyModel;
NewMapPopup *newmapprompt = nullptr;
@@ -294,11 +298,12 @@ private:
void initWindow();
void initCustomUI();
- void initExtraShortcuts();
void initExtraSignals();
void initEditor();
void initMiscHeapObjects();
void initMapSortOrder();
+ void initShortcuts();
+ void initExtraShortcuts();
void setProjectSpecificUIVisibility();
void loadUserSettings();
void applyMapListFilter(QString filterText);
@@ -310,9 +315,16 @@ private:
void closeSupplementaryWindows();
void setWindowDisabled(bool);
+ void initTilesetEditor();
+ bool initRegionMapEditor();
+ void initShortcutsEditor();
+ void connectSubEditorsToShortcutsEditor();
+
bool isProjectOpen();
void showExportMapImageWindow(bool stitchMode);
void redrawMetatileSelection();
+
+ QObjectList shortcutableObjects() const;
};
enum MapListUserRoles {
diff --git a/include/ui/multikeyedit.h b/include/ui/multikeyedit.h
new file mode 100644
index 00000000..27231330
--- /dev/null
+++ b/include/ui/multikeyedit.h
@@ -0,0 +1,52 @@
+#ifndef MULTIKEYEDIT_H
+#define MULTIKEYEDIT_H
+
+#include
+#include
+
+class QLineEdit;
+
+
+// A collection of QKeySequenceEdit's laid out horizontally.
+class MultiKeyEdit : public QWidget
+{
+ Q_OBJECT
+
+public:
+ MultiKeyEdit(QWidget *parent = nullptr, int fieldCount = 2);
+
+ bool eventFilter(QObject *watched, QEvent *event) override;
+
+ int fieldCount() const;
+ void setFieldCount(int count);
+ QList keySequences() const;
+ bool removeOne(const QKeySequence &keySequence);
+ bool contains(const QKeySequence &keySequence) const;
+ void setContextMenuPolicy(Qt::ContextMenuPolicy policy);
+ bool isClearButtonEnabled() const;
+ void setClearButtonEnabled(bool enable);
+
+public slots:
+ void clear();
+ void setKeySequences(const QList &keySequences);
+ void addKeySequence(const QKeySequence &keySequence);
+
+signals:
+ void keySequenceChanged(const QKeySequence &keySequence);
+ void editingFinished();
+ void customContextMenuRequested(const QPoint &pos);
+
+private:
+ QVector keySequenceEdit_vec;
+ QList keySequence_list; // Used to track changes
+
+ void addNewKeySequenceEdit();
+ void alignKeySequencesLeft();
+ void setFocusToLastNonEmptyKeySequenceEdit();
+
+private slots:
+ void onEditingFinished();
+ void showDefaultContextMenu(QLineEdit *lineEdit, const QPoint &pos);
+};
+
+#endif // MULTIKEYEDIT_H
diff --git a/include/ui/regionmapeditor.h b/include/ui/regionmapeditor.h
index 484103f3..cba14251 100644
--- a/include/ui/regionmapeditor.h
+++ b/include/ui/regionmapeditor.h
@@ -47,6 +47,11 @@ public:
void resize(int width, int height);
+ QObjectList shortcutableObjects() const;
+
+public slots:
+ void applyUserShortcuts();
+
private:
Ui::RegionMapEditor *ui;
Project *project;
@@ -81,6 +86,7 @@ private:
RegionMapPixmapItem *region_map_item = nullptr;
CityMapPixmapItem *city_map_item = nullptr;
+ void initShortcuts();
void displayRegionMap();
void displayRegionMapImage();
void displayRegionMapLayout();
diff --git a/include/ui/shortcut.h b/include/ui/shortcut.h
new file mode 100644
index 00000000..58980a3b
--- /dev/null
+++ b/include/ui/shortcut.h
@@ -0,0 +1,71 @@
+#ifndef SHORTCUT_H
+#define SHORTCUT_H
+
+#include
+#include
+#include
+
+
+// Alternative to QShortcut that adds support for multiple key sequences.
+// Use this to allow the shortcut to be editable in ShortcutsEditor.
+class Shortcut : public QObject
+{
+ Q_OBJECT
+ Q_DECLARE_PRIVATE(QShortcut)
+ Q_PROPERTY(QKeySequence key READ key WRITE setKey)
+ Q_PROPERTY(QString whatsThis READ whatsThis WRITE setWhatsThis)
+ Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled)
+ Q_PROPERTY(bool autoRepeat READ autoRepeat WRITE setAutoRepeat)
+ Q_PROPERTY(Qt::ShortcutContext context READ context WRITE setContext)
+
+public:
+ explicit Shortcut(QWidget *parent);
+ Shortcut(const QKeySequence &key, QWidget *parent,
+ const char *member = nullptr, const char *ambiguousMember = nullptr,
+ Qt::ShortcutContext shortcutContext = Qt::WindowShortcut);
+ Shortcut(const QList &keys, QWidget *parent,
+ const char *member = nullptr, const char *ambiguousMember = nullptr,
+ Qt::ShortcutContext shortcutContext = Qt::WindowShortcut);
+ ~Shortcut();
+
+ void addKey(const QKeySequence &key);
+ void setKey(const QKeySequence &key);
+ QKeySequence key() const;
+
+ void addKeys(const QList &keys);
+ void setKeys(const QList &keys);
+ QList keys() const;
+
+ void setEnabled(bool enable);
+ bool isEnabled() const;
+
+ void setContext(Qt::ShortcutContext context);
+ Qt::ShortcutContext context() const;
+
+ void setWhatsThis(const QString &text);
+ QString whatsThis() const;
+
+ void setAutoRepeat(bool on);
+ bool autoRepeat() const;
+
+ int id() const;
+ QList ids() const;
+
+ inline QWidget *parentWidget() const
+ { return static_cast(QObject::parent()); }
+
+signals:
+ void activated();
+ void activatedAmbiguously();
+
+protected:
+ bool event(QEvent *e) override;
+
+private:
+ const char *sc_member;
+ const char *sc_ambiguousmember;
+ Qt::ShortcutContext sc_context;
+ QVector sc_vec;
+};
+
+#endif // SHORTCUT_H
diff --git a/include/ui/shortcutseditor.h b/include/ui/shortcutseditor.h
new file mode 100644
index 00000000..31b0279e
--- /dev/null
+++ b/include/ui/shortcutseditor.h
@@ -0,0 +1,60 @@
+#ifndef SHORTCUTSEDITOR_H
+#define SHORTCUTSEDITOR_H
+
+#include "shortcut.h"
+
+#include
+#include
+#include
+#include
+#include
+
+class QFormLayout;
+class MultiKeyEdit;
+class QAbstractButton;
+
+
+namespace Ui {
+class ShortcutsEditor;
+}
+
+class ShortcutsEditor : public QMainWindow
+{
+ Q_OBJECT
+
+public:
+ explicit ShortcutsEditor(QWidget *parent = nullptr);
+ explicit ShortcutsEditor(const QObjectList &shortcutableObjects, QWidget *parent = nullptr);
+ ~ShortcutsEditor();
+
+ void setShortcutableObjects(const QObjectList &shortcutableObjects);
+
+signals:
+ void shortcutsSaved();
+
+private:
+ Ui::ShortcutsEditor *ui;
+ QWidget *main_container;
+ QMultiMap labels_objects;
+ QHash contexts_layouts;
+ QHash multiKeyEdits_objects;
+
+ void parseObjectList(const QObjectList &objectList);
+ QString getLabel(const QObject *object) const;
+ bool stringPropertyIsNotEmpty(const QObject *object, const char *name) const;
+ void populateMainContainer();
+ QString getShortcutContext(const QObject *object) const;
+ void addNewContextGroup(const QString &shortcutContext);
+ void addNewMultiKeyEdit(const QObject *object, const QString &shortcutContext);
+ QList siblings(MultiKeyEdit *multiKeyEdit) const;
+ void promptUserOnDuplicateFound(MultiKeyEdit *current, MultiKeyEdit *sender);
+ void removeKeySequence(const QKeySequence &keySequence, MultiKeyEdit *multiKeyEdit);
+ void saveShortcuts();
+ void resetShortcuts();
+
+private slots:
+ void checkForDuplicates(const QKeySequence &keySequence);
+ void dialogButtonClicked(QAbstractButton *button);
+};
+
+#endif // SHORTCUTSEDITOR_H
diff --git a/include/ui/tileseteditor.h b/include/ui/tileseteditor.h
index 283f9cde..1af92f35 100644
--- a/include/ui/tileseteditor.h
+++ b/include/ui/tileseteditor.h
@@ -42,6 +42,11 @@ public:
void updateTilesets(QString primaryTilsetLabel, QString secondaryTilesetLabel);
bool selectMetatile(uint16_t metatileId);
+ QObjectList shortcutableObjects() const;
+
+public slots:
+ void applyUserShortcuts();
+
private slots:
void onHoveredMetatileChanged(uint16_t);
void onHoveredMetatileCleared();
@@ -102,6 +107,8 @@ private:
void initTileSelector();
void initSelectedTileItem();
void initMetatileLayersItem();
+ void initShortcuts();
+ void initExtraShortcuts();
void restoreWindowState();
void initMetatileHistory();
void setTilesets(QString primaryTilesetLabel, QString secondaryTilesetLabel);
@@ -112,6 +119,7 @@ private:
void refresh();
void saveMetatileLabel();
void closeEvent(QCloseEvent*);
+
Ui::TilesetEditor *ui;
History metatileHistory;
TilesetEditorMetatileSelector *metatileSelector = nullptr;
diff --git a/porymap.pro b/porymap.pro
index 806f3b5b..56331b25 100644
--- a/porymap.pro
+++ b/porymap.pro
@@ -71,6 +71,9 @@ SOURCES += src/core/block.cpp \
src/ui/newtilesetdialog.cpp \
src/ui/flowlayout.cpp \
src/ui/mapruler.cpp \
+ src/ui/shortcut.cpp \
+ src/ui/shortcutseditor.cpp \
+ src/ui/multikeyedit.cpp \
src/ui/preferenceeditor.cpp \
src/config.cpp \
src/editor.cpp \
@@ -141,6 +144,9 @@ HEADERS += include/core/block.h \
include/ui/overlay.h \
include/ui/flowlayout.h \
include/ui/mapruler.h \
+ include/ui/shortcut.h \
+ include/ui/shortcutseditor.h \
+ include/ui/multikeyedit.h \
include/ui/preferenceeditor.h \
include/config.h \
include/editor.h \
@@ -159,6 +165,7 @@ FORMS += forms/mainwindow.ui \
forms/aboutporymap.ui \
forms/newtilesetdialog.ui \
forms/mapimageexporter.ui \
+ forms/shortcutseditor.ui
forms/preferenceeditor.ui
RESOURCES += \
diff --git a/src/config.cpp b/src/config.cpp
index 831e5507..5ef3723f 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -1,5 +1,6 @@
#include "config.h"
#include "log.h"
+#include "shortcut.h"
#include
#include
#include
@@ -11,6 +12,8 @@
#include
#include
#include
+#include
+#include
KeyValueConfigBase::~KeyValueConfigBase() {
@@ -36,7 +39,7 @@ void KeyValueConfigBase::load() {
QTextStream in(&file);
in.setCodec("UTF-8");
QList configLines;
- QRegularExpression re("^(?.+)=(?.*)$");
+ QRegularExpression re("^(?[^=]+)=(?.*)$");
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
int commentIndex = line.indexOf("#");
@@ -438,7 +441,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) {
@@ -727,3 +730,128 @@ 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 keySequences = value.split(' ');
+ for (auto keySequence : keySequences)
+ user_shortcuts.insert(key, keySequence);
+}
+
+QMap ShortcutsConfig::getKeyValueMap() {
+ QMap map;
+ for (auto cfg_key : user_shortcuts.uniqueKeys()) {
+ auto keySequences = user_shortcuts.values(cfg_key);
+ QStringList keySequenceStrings;
+ for (auto keySequence : keySequences)
+ keySequenceStrings.append(keySequence.toString());
+ map.insert(cfg_key, keySequenceStrings.join(' '));
+ }
+ return map;
+}
+
+void ShortcutsConfig::setDefaultShortcuts(const QObjectList &objects) {
+ storeShortcutsFromList(StoreType::Default, objects);
+ save();
+}
+
+QList ShortcutsConfig::defaultShortcuts(const QObject *object) const {
+ return default_shortcuts.values(cfgKey(object));
+}
+
+void ShortcutsConfig::setUserShortcuts(const QObjectList &objects) {
+ storeShortcutsFromList(StoreType::User, objects);
+ save();
+}
+
+void ShortcutsConfig::setUserShortcuts(const QMultiMap &objects_keySequences) {
+ for (auto *object : objects_keySequences.uniqueKeys())
+ if (!object->objectName().isEmpty() && !object->objectName().startsWith("_q_"))
+ storeShortcuts(StoreType::User, cfgKey(object), objects_keySequences.values(object));
+ save();
+}
+
+QList ShortcutsConfig::userShortcuts(const QObject *object) const {
+ return user_shortcuts.values(cfgKey(object));
+}
+
+void ShortcutsConfig::storeShortcutsFromList(StoreType storeType, const QObjectList &objects) {
+ for (const auto *object : objects)
+ if (!object->objectName().isEmpty() && !object->objectName().startsWith("_q_"))
+ storeShortcuts(storeType, cfgKey(object), currentShortcuts(object));
+}
+
+void ShortcutsConfig::storeShortcuts(
+ StoreType storeType,
+ const QString &cfgKey,
+ const QList &keySequences)
+{
+ bool storeUser = (storeType == User) || !user_shortcuts.contains(cfgKey);
+
+ if (storeType == Default)
+ default_shortcuts.remove(cfgKey);
+ if (storeUser)
+ user_shortcuts.remove(cfgKey);
+
+ if (keySequences.isEmpty()) {
+ if (storeType == Default)
+ default_shortcuts.insert(cfgKey, QKeySequence());
+ if (storeUser)
+ user_shortcuts.insert(cfgKey, QKeySequence());
+ } else {
+ for (auto keySequence : keySequences) {
+ if (storeType == Default)
+ default_shortcuts.insert(cfgKey, keySequence);
+ if (storeUser)
+ user_shortcuts.insert(cfgKey, keySequence);
+ }
+ }
+}
+
+/* Creates a config key from the object's name prepended with the parent
+ * window's object name, and converts camelCase to snake_case. */
+QString ShortcutsConfig::cfgKey(const QObject *object) const {
+ auto cfg_key = QString();
+ auto *parentWidget = static_cast(object->parent());
+ if (parentWidget)
+ cfg_key = parentWidget->window()->objectName() + '_';
+ cfg_key += object->objectName();
+
+ QRegularExpression re("[A-Z]");
+ int i = cfg_key.indexOf(re, 1);
+ while (i != -1) {
+ if (cfg_key.at(i - 1) != '_')
+ cfg_key.insert(i++, '_');
+ i = cfg_key.indexOf(re, i + 1);
+ }
+ return cfg_key.toLower();
+}
+
+QList ShortcutsConfig::currentShortcuts(const QObject *object) const {
+ if (object->inherits("QAction")) {
+ const auto *action = qobject_cast(object);
+ return action->shortcuts();
+ } else if (object->inherits("Shortcut")) {
+ const auto *shortcut = qobject_cast(object);
+ return shortcut->keys();
+ } else if (object->inherits("QShortcut")) {
+ const auto *qshortcut = qobject_cast(object);
+ return { qshortcut->key() };
+ } else if (object->property("shortcut").isValid()) {
+ return { object->property("shortcut").value() };
+ } else {
+ return { };
+ }
+}
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 5573a223..7882b4f3 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -14,11 +14,11 @@
#include "draggablepixmapitem.h"
#include "editcommands.h"
#include "flowlayout.h"
+#include "shortcut.h"
#include
#include
#include
-#include
#include
#include
#include
@@ -91,22 +91,84 @@ void MainWindow::initWindow() {
porymapConfig.load();
this->initCustomUI();
this->initExtraSignals();
- this->initExtraShortcuts();
this->initEditor();
this->initMiscHeapObjects();
this->initMapSortOrder();
+ this->initShortcuts();
this->restoreWindowState();
setWindowDisabled(true);
}
+void MainWindow::initShortcuts() {
+ initExtraShortcuts();
+
+ shortcutsConfig.load();
+ shortcutsConfig.setDefaultShortcuts(shortcutableObjects());
+ applyUserShortcuts();
+}
+
void MainWindow::initExtraShortcuts() {
- new QShortcut(QKeySequence("Ctrl+0"), this, SLOT(resetMapViewScale()));
- new QShortcut(QKeySequence("Ctrl+G"), ui->checkBox_ToggleGrid, SLOT(toggle()));
- new QShortcut(QKeySequence("Ctrl+D"), this, SLOT(duplicate()));
- new QShortcut(QKeySequence::Delete, this, SLOT(on_toolButton_deleteObject_clicked()));
- new QShortcut(QKeySequence("Backspace"), this, SLOT(on_toolButton_deleteObject_clicked()));
- ui->actionZoom_In->setShortcuts({QKeySequence("Ctrl++"), QKeySequence("Ctrl+=")});
+ ui->actionZoom_In->setShortcuts({ui->actionZoom_In->shortcut(), QKeySequence("Ctrl+=")});
+
+ auto *shortcutReset_Zoom = new Shortcut(QKeySequence("Ctrl+0"), this, SLOT(resetMapViewScale()));
+ shortcutReset_Zoom->setObjectName("shortcutZoom_Reset");
+ shortcutReset_Zoom->setWhatsThis("Zoom Reset");
+
+ auto *shortcutToggle_Grid = new Shortcut(QKeySequence("Ctrl+G"), ui->checkBox_ToggleGrid, SLOT(toggle()));
+ shortcutToggle_Grid->setObjectName("shortcutToggle_Grid");
+ shortcutToggle_Grid->setWhatsThis("Toggle Grid");
+
+ auto *shortcutDuplicate_Events = new Shortcut(QKeySequence("Ctrl+D"), this, SLOT(duplicate()));
+ shortcutDuplicate_Events->setObjectName("shortcutDuplicate_Events");
+ shortcutDuplicate_Events->setWhatsThis("Duplicate Selected Event(s)");
+
+ auto *shortcutDelete_Object = new Shortcut(
+ {QKeySequence("Del"), QKeySequence("Backspace")}, this, SLOT(on_toolButton_deleteObject_clicked()));
+ shortcutDelete_Object->setObjectName("shortcutDelete_Object");
+ shortcutDelete_Object->setWhatsThis("Delete Selected Event(s)");
+
+ auto *shortcutToggle_Border = new Shortcut(QKeySequence(), ui->checkBox_ToggleBorder, SLOT(toggle()));
+ shortcutToggle_Border->setObjectName("shortcutToggle_Border");
+ shortcutToggle_Border->setWhatsThis("Toggle Border");
+
+ auto *shortcutToggle_Smart_Paths = new Shortcut(QKeySequence(), ui->checkBox_smartPaths, SLOT(toggle()));
+ shortcutToggle_Smart_Paths->setObjectName("shortcutToggle_Smart_Paths");
+ shortcutToggle_Smart_Paths->setWhatsThis("Toggle Smart Paths");
+
+ auto *shortcutExpand_All = new Shortcut(QKeySequence(), this, SLOT(on_toolButton_ExpandAll_clicked()));
+ shortcutExpand_All->setObjectName("shortcutExpand_All");
+ shortcutExpand_All->setWhatsThis("Map List: Expand all folders");
+
+ auto *shortcutCollapse_All = new Shortcut(QKeySequence(), this, SLOT(on_toolButton_CollapseAll_clicked()));
+ shortcutCollapse_All->setObjectName("shortcutCollapse_All");
+ shortcutCollapse_All->setWhatsThis("Map List: Collapse all folders");
+
+ auto *shortcutNew_Event = new Shortcut(QKeySequence(), this, SLOT(on_toolButton_Open_Scripts_clicked()));
+ shortcutNew_Event->setObjectName("shortcut_Open_Scripts");
+ shortcutNew_Event->setWhatsThis("Open Map Scripts");
+}
+
+QObjectList MainWindow::shortcutableObjects() const {
+ QObjectList shortcutable_objects;
+
+ for (auto *action : findChildren())
+ if (!action->objectName().isEmpty())
+ shortcutable_objects.append(qobject_cast(action));
+ for (auto *shortcut : findChildren())
+ if (!shortcut->objectName().isEmpty())
+ shortcutable_objects.append(qobject_cast(shortcut));
+
+ return shortcutable_objects;
+}
+
+void MainWindow::applyUserShortcuts() {
+ for (auto *action : findChildren())
+ if (!action->objectName().isEmpty())
+ action->setShortcuts(shortcutsConfig.userShortcuts(action));
+ for (auto *shortcut : findChildren())
+ if (!shortcut->objectName().isEmpty())
+ shortcut->setKeys(shortcutsConfig.userShortcuts(shortcut));
}
void MainWindow::initCustomUI() {
@@ -175,9 +237,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);
@@ -188,7 +252,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(); });
@@ -232,7 +297,7 @@ void MainWindow::initMapSortOrder() {
mapSortOrderActionGroup->addAction(ui->actionSort_by_Area);
mapSortOrderActionGroup->addAction(ui->actionSort_by_Layout);
- connect(ui->toolButton_MapSortOrder, &QToolButton::triggered, this, &MainWindow::mapSortOrder_changed);
+ connect(mapSortOrderActionGroup, &QActionGroup::triggered, this, &MainWindow::mapSortOrder_changed);
QAction* sortOrder = ui->toolButton_MapSortOrder->menu()->actions()[mapSortOrder];
ui->toolButton_MapSortOrder->setIcon(sortOrder->icon());
@@ -1410,6 +1475,48 @@ void MainWindow::on_actionUse_Poryscript_triggered(bool checked)
projectConfig.setUsePoryScript(checked);
}
+void MainWindow::on_actionEdit_Shortcuts_triggered()
+{
+ if (!shortcutsEditor)
+ initShortcutsEditor();
+
+ if (shortcutsEditor->isHidden())
+ shortcutsEditor->show();
+ else if (shortcutsEditor->isMinimized())
+ shortcutsEditor->showNormal();
+ else
+ shortcutsEditor->activateWindow();
+}
+
+void MainWindow::initShortcutsEditor() {
+ shortcutsEditor = new ShortcutsEditor(this);
+ connect(shortcutsEditor, &ShortcutsEditor::shortcutsSaved,
+ this, &MainWindow::applyUserShortcuts);
+ connect(shortcutsEditor, &QObject::destroyed, [=](QObject *) { shortcutsEditor = nullptr; });
+
+ connectSubEditorsToShortcutsEditor();
+
+ shortcutsEditor->setShortcutableObjects(shortcutableObjects());
+}
+
+void MainWindow::connectSubEditorsToShortcutsEditor() {
+ /* Initialize sub-editors so that their children are added to MainWindow's object tree and will
+ * be returned by shortcutableObjects() to be passed to ShortcutsEditor. */
+ if (!tilesetEditor)
+ initTilesetEditor();
+ connect(shortcutsEditor, &ShortcutsEditor::shortcutsSaved,
+ tilesetEditor, &TilesetEditor::applyUserShortcuts);
+
+ // TODO: Remove this check when the region map editor supports pokefirered.
+ if (projectConfig.getBaseGameVersion() != BaseGameVersion::pokefirered) {
+ if (!regionMapEditor)
+ initRegionMapEditor();
+ if (regionMapEditor)
+ connect(shortcutsEditor, &ShortcutsEditor::shortcutsSaved,
+ regionMapEditor, &RegionMapEditor::applyUserShortcuts);
+ }
+}
+
void MainWindow::on_actionPencil_triggered()
{
on_toolButton_Paint_clicked();
@@ -2537,9 +2644,7 @@ void MainWindow::on_checkBox_ToggleBorder_stateChanged(int selected)
void MainWindow::on_actionTileset_Editor_triggered()
{
if (!this->tilesetEditor) {
- this->tilesetEditor = new TilesetEditor(this->editor->project, this->editor->map, this);
- connect(this->tilesetEditor, SIGNAL(tilesetsSaved(QString, QString)), this, SLOT(onTilesetsSaved(QString, QString)));
- connect(this->tilesetEditor, &QObject::destroyed, [=](QObject *) { this->tilesetEditor = nullptr; });
+ initTilesetEditor();
}
if (!this->tilesetEditor->isVisible()) {
@@ -2552,6 +2657,12 @@ void MainWindow::on_actionTileset_Editor_triggered()
this->tilesetEditor->selectMetatile(this->editor->metatile_selector_item->getSelectedMetatiles()->at(0));
}
+void MainWindow::initTilesetEditor() {
+ this->tilesetEditor = new TilesetEditor(this->editor->project, this->editor->map, this);
+ connect(this->tilesetEditor, SIGNAL(tilesetsSaved(QString, QString)), this, SLOT(onTilesetsSaved(QString, QString)));
+ connect(this->tilesetEditor, &QObject::destroyed, [=](QObject *) { this->tilesetEditor = nullptr; });
+}
+
void MainWindow::on_toolButton_ExpandAll_clicked()
{
if (ui->mapList) {
@@ -2668,20 +2779,9 @@ void MainWindow::on_horizontalSlider_MetatileZoom_valueChanged(int value) {
void MainWindow::on_actionRegion_Map_Editor_triggered() {
if (!this->regionMapEditor) {
- this->regionMapEditor = new RegionMapEditor(this, this->editor->project);
- bool success = this->regionMapEditor->loadRegionMapData()
- && this->regionMapEditor->loadCityMaps();
- if (!success) {
- delete this->regionMapEditor;
- this->regionMapEditor = nullptr;
- QMessageBox msgBox(this);
- QString errorMsg = QString("There was an error opening the region map data. Please see %1 for full error details.\n\n%3")
- .arg(getLogPath())
- .arg(getMostRecentError());
- msgBox.critical(nullptr, "Error Opening Region Map Editor", errorMsg);
+ if (!initRegionMapEditor()) {
return;
}
- connect(this->regionMapEditor, &QObject::destroyed, [=](QObject *) { this->regionMapEditor = nullptr; });
}
if (!this->regionMapEditor->isVisible()) {
@@ -2693,6 +2793,26 @@ void MainWindow::on_actionRegion_Map_Editor_triggered() {
}
}
+bool MainWindow::initRegionMapEditor() {
+ this->regionMapEditor = new RegionMapEditor(this, this->editor->project);
+ bool success = this->regionMapEditor->loadRegionMapData()
+ && this->regionMapEditor->loadCityMaps();
+ if (!success) {
+ delete this->regionMapEditor;
+ this->regionMapEditor = nullptr;
+ QMessageBox msgBox(this);
+ QString errorMsg = QString("There was an error opening the region map data. Please see %1 for full error details.\n\n%3")
+ .arg(getLogPath())
+ .arg(getMostRecentError());
+ msgBox.critical(nullptr, "Error Opening Region Map Editor", errorMsg);
+
+ return false;
+ }
+ connect(this->regionMapEditor, &QObject::destroyed, [=](QObject *) { this->regionMapEditor = nullptr; });
+
+ return true;
+}
+
void MainWindow::closeSupplementaryWindows() {
if (this->tilesetEditor)
delete this->tilesetEditor;
@@ -2702,6 +2822,8 @@ void MainWindow::closeSupplementaryWindows() {
delete this->mapImageExporter;
if (this->newmapprompt)
delete this->newmapprompt;
+ if (this->shortcutsEditor)
+ delete this->shortcutsEditor;
}
void MainWindow::closeEvent(QCloseEvent *event) {
@@ -2728,6 +2850,7 @@ void MainWindow::closeEvent(QCloseEvent *event) {
);
porymapConfig.save();
projectConfig.save();
+ shortcutsConfig.save();
QMainWindow::closeEvent(event);
}
diff --git a/src/ui/multikeyedit.cpp b/src/ui/multikeyedit.cpp
new file mode 100644
index 00000000..09943861
--- /dev/null
+++ b/src/ui/multikeyedit.cpp
@@ -0,0 +1,185 @@
+#include "multikeyedit.h"
+
+#include
+#include
+#include
+#include
+#include
+
+
+MultiKeyEdit::MultiKeyEdit(QWidget *parent, int fieldCount) :
+ QWidget(parent),
+ keySequenceEdit_vec(QVector()),
+ keySequence_list(QList())
+{
+ setLayout(new QHBoxLayout(this));
+ layout()->setContentsMargins(0, 0, 0, 0);
+ setFieldCount(fieldCount);
+}
+
+bool MultiKeyEdit::eventFilter(QObject *watched, QEvent *event) {
+ if (event->type() == QEvent::KeyPress) {
+ auto *watched_kse = qobject_cast(watched);
+ if (!watched_kse)
+ return false;
+
+ auto *keyEvent = static_cast(event);
+ if (keyEvent->key() == Qt::Key_Escape) {
+ watched_kse->clearFocus();
+ return true;
+ } else {
+ watched_kse->clear();
+ }
+ }
+
+ if (event->type() == QEvent::ContextMenu) {
+ auto *watched_lineEdit = qobject_cast(watched);
+ if (!watched_lineEdit)
+ return false;
+
+ auto *contextMenuEvent = static_cast(event);
+ if (contextMenuPolicy() == Qt::DefaultContextMenu) {
+ showDefaultContextMenu(watched_lineEdit, contextMenuEvent->pos());
+ return true;
+ }
+ }
+
+ return false;
+}
+
+int MultiKeyEdit::fieldCount() const {
+ return keySequenceEdit_vec.count();
+}
+
+void MultiKeyEdit::setFieldCount(int count) {
+ if (count < 1)
+ count = 1;
+
+ while (keySequenceEdit_vec.count() < count)
+ addNewKeySequenceEdit();
+
+ while (keySequenceEdit_vec.count() > count)
+ delete keySequenceEdit_vec.takeLast();
+
+ alignKeySequencesLeft();
+}
+
+QList MultiKeyEdit::keySequences() const {
+ QList current_keySequences;
+ for (auto *kse : keySequenceEdit_vec)
+ if (!kse->keySequence().isEmpty())
+ current_keySequences.append(kse->keySequence());
+ return current_keySequences;
+}
+
+bool MultiKeyEdit::removeOne(const QKeySequence &keySequence) {
+ for (auto *keySequenceEdit : keySequenceEdit_vec) {
+ if (keySequenceEdit->keySequence() == keySequence) {
+ keySequence_list.removeOne(keySequence);
+ keySequenceEdit->clear();
+ alignKeySequencesLeft();
+ return true;
+ }
+ }
+ return false;
+}
+
+bool MultiKeyEdit::contains(const QKeySequence &keySequence) const {
+ for (auto current_keySequence : keySequences())
+ if (current_keySequence == keySequence)
+ return true;
+ return false;
+}
+
+void MultiKeyEdit::setContextMenuPolicy(Qt::ContextMenuPolicy policy) {
+ QWidget::setContextMenuPolicy(policy);
+ auto lineEdit_children = findChildren();
+ for (auto *lineEdit : lineEdit_children)
+ lineEdit->setContextMenuPolicy(policy);
+}
+
+bool MultiKeyEdit::isClearButtonEnabled() const {
+ return findChild()->isClearButtonEnabled();
+}
+
+void MultiKeyEdit::setClearButtonEnabled(bool enable) {
+ for (auto *lineEdit : findChildren())
+ lineEdit->setClearButtonEnabled(enable);
+}
+
+void MultiKeyEdit::clear() {
+ for (auto *keySequenceEdit : keySequenceEdit_vec)
+ keySequenceEdit->clear();
+ keySequence_list.clear();
+}
+
+void MultiKeyEdit::setKeySequences(const QList &keySequences) {
+ clear();
+ keySequence_list = keySequences;
+ int minCount = qMin(keySequenceEdit_vec.count(), keySequence_list.count());
+ for (int i = 0; i < minCount; ++i)
+ keySequenceEdit_vec[i]->setKeySequence(keySequence_list[i]);
+}
+
+void MultiKeyEdit::addKeySequence(const QKeySequence &keySequence) {
+ keySequenceEdit_vec.last()->setKeySequence(keySequence);
+ alignKeySequencesLeft();
+}
+
+void MultiKeyEdit::addNewKeySequenceEdit() {
+ auto *keySequenceEdit = new QKeySequenceEdit(this);
+ keySequenceEdit->installEventFilter(this);
+ connect(keySequenceEdit, &QKeySequenceEdit::editingFinished,
+ this, &MultiKeyEdit::onEditingFinished);
+ connect(keySequenceEdit, &QKeySequenceEdit::keySequenceChanged,
+ this, &MultiKeyEdit::keySequenceChanged);
+
+ auto *lineEdit = keySequenceEdit->findChild();
+ lineEdit->setClearButtonEnabled(true);
+ lineEdit->installEventFilter(this);
+ connect(lineEdit, &QLineEdit::customContextMenuRequested,
+ this, &MultiKeyEdit::customContextMenuRequested);
+
+ layout()->addWidget(keySequenceEdit);
+ keySequenceEdit_vec.append(keySequenceEdit);
+}
+
+// Shift all key sequences left if there are any empty QKeySequenceEdit's.
+void MultiKeyEdit::alignKeySequencesLeft() {
+ blockSignals(true);
+ setKeySequences(keySequences());
+ blockSignals(false);
+}
+
+void MultiKeyEdit::setFocusToLastNonEmptyKeySequenceEdit() {
+ for (auto it = keySequenceEdit_vec.rbegin(); it != keySequenceEdit_vec.rend(); ++it) {
+ if (!(*it)->keySequence().isEmpty()) {
+ (*it)->setFocus();
+ return;
+ }
+ }
+}
+
+void MultiKeyEdit::onEditingFinished() {
+ auto *keySequenceEdit = qobject_cast(sender());
+ if (keySequenceEdit && keySequence_list.contains(keySequenceEdit->keySequence()))
+ removeOne(keySequenceEdit->keySequence());
+ alignKeySequencesLeft();
+ setFocusToLastNonEmptyKeySequenceEdit();
+
+ emit editingFinished();
+}
+
+/* QKeySequenceEdit doesn't send or receive context menu events, but it owns QLineEdit that does.
+ * This QLineEdit hijacks those events and so we need to filter/connect to it directly, rather than
+ * the QKeySequenceEdit. I wouldn't be surprised if Qt fixed this in the future, in which case any
+ * context menu related code in this class might need to change. */
+void MultiKeyEdit::showDefaultContextMenu(QLineEdit *lineEdit, const QPoint &pos) {
+ QMenu menu(this);
+ QAction clearAction("Clear Shortcut", &menu);
+ connect(&clearAction, &QAction::triggered, lineEdit, [this, &lineEdit]() {
+ removeOne(lineEdit->text());
+ });
+ menu.addAction(&clearAction);
+ menu.exec(lineEdit->mapToGlobal(pos));
+}
diff --git a/src/ui/newmappopup.cpp b/src/ui/newmappopup.cpp
index 697881a4..19059623 100644
--- a/src/ui/newmappopup.cpp
+++ b/src/ui/newmappopup.cpp
@@ -185,7 +185,9 @@ void NewMapPopup::on_pushButton_NewMap_Accept_clicked() {
MapLayout *layout;
// If map name is not unique, use default value. Also use only valid characters.
+ // After stripping invalid characters, strip any leading digits.
QString newMapName = this->ui->lineEdit_NewMap_Name->text().remove(QRegularExpression("[^a-zA-Z0-9_]+"));
+ newMapName.remove(QRegularExpression("^[0-9]*"));
if (project->mapNames->contains(newMapName) || newMapName.isEmpty()) {
newMapName = project->getNewMapName();
}
diff --git a/src/ui/regionmapeditor.cpp b/src/ui/regionmapeditor.cpp
index ce3440dd..a116aa5a 100644
--- a/src/ui/regionmapeditor.cpp
+++ b/src/ui/regionmapeditor.cpp
@@ -1,6 +1,7 @@
#include "regionmapeditor.h"
#include "ui_regionmapeditor.h"
#include "imageexport.h"
+#include "shortcut.h"
#include "config.h"
#include "log.h"
@@ -24,6 +25,7 @@ RegionMapEditor::RegionMapEditor(QWidget *parent, Project *project_) :
this->project = project_;
this->region_map = new RegionMap;
this->ui->action_RegionMap_Resize->setVisible(false);
+ this->initShortcuts();
this->restoreWindowState();
}
@@ -92,6 +94,39 @@ bool RegionMapEditor::loadCityMaps() {
return true;
}
+void RegionMapEditor::initShortcuts() {
+ auto *shortcut_RM_Options_delete = new Shortcut(
+ {QKeySequence("Del"), QKeySequence("Backspace")}, this, SLOT(on_pushButton_RM_Options_delete_clicked()));
+ shortcut_RM_Options_delete->setObjectName("shortcut_RM_Options_delete");
+ shortcut_RM_Options_delete->setWhatsThis("Map Layout: Delete Square");
+
+ shortcutsConfig.load();
+ shortcutsConfig.setDefaultShortcuts(shortcutableObjects());
+ applyUserShortcuts();
+}
+
+QObjectList RegionMapEditor::shortcutableObjects() const {
+ QObjectList shortcutable_objects;
+
+ for (auto *action : findChildren())
+ if (!action->objectName().isEmpty())
+ shortcutable_objects.append(qobject_cast(action));
+ for (auto *shortcut : findChildren())
+ if (!shortcut->objectName().isEmpty())
+ shortcutable_objects.append(qobject_cast(shortcut));
+
+ return shortcutable_objects;
+}
+
+void RegionMapEditor::applyUserShortcuts() {
+ for (auto *action : findChildren())
+ if (!action->objectName().isEmpty())
+ action->setShortcuts(shortcutsConfig.userShortcuts(action));
+ for (auto *shortcut : findChildren())
+ if (!shortcut->objectName().isEmpty())
+ shortcut->setKeys(shortcutsConfig.userShortcuts(shortcut));
+}
+
void RegionMapEditor::displayRegionMap() {
displayRegionMapTileSelector();
displayCityMapTileSelector();
diff --git a/src/ui/shortcut.cpp b/src/ui/shortcut.cpp
new file mode 100644
index 00000000..e94b4095
--- /dev/null
+++ b/src/ui/shortcut.cpp
@@ -0,0 +1,153 @@
+#include "shortcut.h"
+
+#include
+#include
+
+
+Shortcut::Shortcut(QWidget *parent) :
+ QObject(parent),
+ sc_member(nullptr),
+ sc_ambiguousmember(nullptr),
+ sc_context(Qt::WindowShortcut),
+ sc_vec(QVector({new QShortcut(parent)}))
+{ }
+
+Shortcut::Shortcut(const QKeySequence &key, QWidget *parent,
+ const char *member, const char *ambiguousMember,
+ Qt::ShortcutContext shortcutContext) :
+ QObject(parent),
+ sc_member(member),
+ sc_ambiguousmember(ambiguousMember),
+ sc_context(shortcutContext),
+ sc_vec(QVector())
+{
+ setKey(key);
+}
+
+Shortcut::Shortcut(const QList &keys, QWidget *parent,
+ const char *member, const char *ambiguousMember,
+ Qt::ShortcutContext shortcutContext) :
+ QObject(parent),
+ sc_member(member),
+ sc_ambiguousmember(ambiguousMember),
+ sc_context(shortcutContext),
+ sc_vec(QVector())
+{
+ setKeys(keys);
+}
+
+Shortcut::~Shortcut()
+{
+ for (auto *sc : sc_vec)
+ delete sc;
+}
+
+void Shortcut::addKey(const QKeySequence &key) {
+ sc_vec.append(new QShortcut(key, parentWidget(), sc_member, sc_ambiguousmember, sc_context));
+}
+
+void Shortcut::setKey(const QKeySequence &key) {
+ if (sc_vec.isEmpty()) {
+ addKey(key);
+ } else {
+ while (sc_vec.count() != 1)
+ delete sc_vec.takeLast();
+ sc_vec.first()->setKey(key);
+ }
+}
+
+QKeySequence Shortcut::key() const {
+ return sc_vec.first()->key();
+}
+
+void Shortcut::addKeys(const QList &keys) {
+ for (auto key : keys)
+ addKey(key);
+}
+
+void Shortcut::setKeys(const QList &keys) {
+ if (keys.isEmpty())
+ return;
+
+ while (sc_vec.count() < keys.count())
+ addKey(QKeySequence());
+
+ while (sc_vec.count() > keys.count())
+ delete sc_vec.takeLast();
+
+ for (int i = 0; i < keys.count(); ++i)
+ sc_vec[i]->setKey(keys[i]);
+}
+
+QList Shortcut::keys() const {
+ QList ks_list = QList();
+ for (auto *sc : sc_vec)
+ ks_list.append(sc->key());
+ return ks_list;
+}
+
+void Shortcut::setEnabled(bool enable) {
+ for (auto *sc : sc_vec)
+ sc->setEnabled(enable);
+}
+
+bool Shortcut::isEnabled() const {
+ return sc_vec.first()->isEnabled();
+}
+
+void Shortcut::setContext(Qt::ShortcutContext context) {
+ sc_context = context;
+ for (auto *sc : sc_vec)
+ sc->setContext(context);
+}
+
+Qt::ShortcutContext Shortcut::context() const {
+ return sc_context;
+}
+
+void Shortcut::setWhatsThis(const QString &text) {
+ for (auto *sc : sc_vec)
+ sc->setWhatsThis(text);
+}
+
+QString Shortcut::whatsThis() const {
+ return sc_vec.first()->whatsThis();
+}
+
+void Shortcut::setAutoRepeat(bool on) {
+ for (auto *sc : sc_vec)
+ sc->setAutoRepeat(on);
+}
+
+bool Shortcut::autoRepeat() const {
+ return sc_vec.first()->autoRepeat();
+}
+
+int Shortcut::id() const {
+ return sc_vec.first()->id();
+}
+
+QList Shortcut::ids() const {
+ QList id_list;
+ for (auto *sc : sc_vec)
+ id_list.append(sc->id());
+ return id_list;
+}
+
+bool Shortcut::event(QEvent *e) {
+ if (isEnabled() && e->type() == QEvent::Shortcut) {
+ auto se = static_cast(e);
+ if (ids().contains(se->shortcutId()) && keys().contains(se->key())) {
+ if (QWhatsThis::inWhatsThisMode()) {
+ QWhatsThis::showText(QCursor::pos(), whatsThis());
+ } else {
+ if (se->isAmbiguous())
+ emit activatedAmbiguously();
+ else
+ emit activated();
+ }
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/src/ui/shortcutseditor.cpp b/src/ui/shortcutseditor.cpp
new file mode 100644
index 00000000..0f9e77ac
--- /dev/null
+++ b/src/ui/shortcutseditor.cpp
@@ -0,0 +1,188 @@
+#include "shortcutseditor.h"
+#include "ui_shortcutseditor.h"
+#include "config.h"
+#include "multikeyedit.h"
+#include "log.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+
+ShortcutsEditor::ShortcutsEditor(QWidget *parent) :
+ QMainWindow(parent),
+ ui(new Ui::ShortcutsEditor),
+ main_container(nullptr)
+{
+ ui->setupUi(this);
+ setAttribute(Qt::WA_DeleteOnClose);
+ main_container = ui->scrollAreaWidgetContents_Shortcuts;
+ auto *main_layout = new QVBoxLayout(main_container);
+ main_layout->setSpacing(12);
+ connect(ui->buttonBox, &QDialogButtonBox::clicked,
+ this, &ShortcutsEditor::dialogButtonClicked);
+}
+
+ShortcutsEditor::ShortcutsEditor(const QObjectList &shortcutableObjects, QWidget *parent) :
+ ShortcutsEditor(parent)
+{
+ setShortcutableObjects(shortcutableObjects);
+}
+
+ShortcutsEditor::~ShortcutsEditor()
+{
+ delete ui;
+}
+
+void ShortcutsEditor::setShortcutableObjects(const QObjectList &shortcutableObjects) {
+ parseObjectList(shortcutableObjects);
+ populateMainContainer();
+}
+
+void ShortcutsEditor::saveShortcuts() {
+ QMultiMap objects_keySequences;
+ for (auto it = multiKeyEdits_objects.cbegin(); it != multiKeyEdits_objects.cend(); ++it) {
+ if (it.key()->keySequences().isEmpty())
+ objects_keySequences.insert(it.value(), QKeySequence());
+ for (auto keySequence : it.key()->keySequences())
+ objects_keySequences.insert(it.value(), keySequence);
+ }
+
+ shortcutsConfig.setUserShortcuts(objects_keySequences);
+ emit shortcutsSaved();
+}
+
+// Restores default shortcuts but doesn't save until Apply or OK is clicked.
+void ShortcutsEditor::resetShortcuts() {
+ for (auto it = multiKeyEdits_objects.begin(); it != multiKeyEdits_objects.end(); ++it) {
+ it.key()->blockSignals(true);
+ const auto defaults = shortcutsConfig.defaultShortcuts(it.value());
+ it.key()->setKeySequences(defaults);
+ it.key()->blockSignals(false);
+ }
+}
+
+void ShortcutsEditor::parseObjectList(const QObjectList &objectList) {
+ for (auto *object : objectList) {
+ const auto label = getLabel(object);
+ if (!label.isEmpty() && !object->objectName().isEmpty() && !object->objectName().startsWith("_q_"))
+ labels_objects.insert(label, object);
+ }
+}
+
+QString ShortcutsEditor::getLabel(const QObject *object) const {
+ if (stringPropertyIsNotEmpty(object, "text"))
+ return object->property("text").toString().remove('&');
+ else if (stringPropertyIsNotEmpty(object, "whatsThis"))
+ return object->property("whatsThis").toString().remove('&');
+ else
+ return QString();
+}
+
+bool ShortcutsEditor::stringPropertyIsNotEmpty(const QObject *object, const char *name) const {
+ return object->property(name).isValid() && !object->property(name).toString().isEmpty();
+}
+
+void ShortcutsEditor::populateMainContainer() {
+ for (auto object : labels_objects) {
+ const auto shortcutContext = getShortcutContext(object);
+ if (!contexts_layouts.contains(shortcutContext))
+ addNewContextGroup(shortcutContext);
+
+ addNewMultiKeyEdit(object, shortcutContext);
+ }
+}
+
+// The context for which the object's shortcut is active (Displayed in group box title).
+// Uses the parent window's objectName and adds spaces between words.
+QString ShortcutsEditor::getShortcutContext(const QObject *object) const {
+ auto objectParentWidget = static_cast(object->parent());
+ auto context = objectParentWidget->window()->objectName();
+ QRegularExpression re("[A-Z]");
+ int i = context.indexOf(re, 1);
+ while (i != -1) {
+ if (context.at(i - 1) != ' ')
+ context.insert(i++, ' ');
+ i = context.indexOf(re, i + 1);
+ }
+ return context;
+}
+
+// Seperate shortcuts into context groups for duplicate checking.
+void ShortcutsEditor::addNewContextGroup(const QString &shortcutContext) {
+ auto *groupBox = new QGroupBox(shortcutContext, main_container);
+ main_container->layout()->addWidget(groupBox);
+ auto *formLayout = new QFormLayout(groupBox);
+ contexts_layouts.insert(shortcutContext, formLayout);
+}
+
+void ShortcutsEditor::addNewMultiKeyEdit(const QObject *object, const QString &shortcutContext) {
+ auto *container = contexts_layouts.value(shortcutContext)->parentWidget();
+ auto *multiKeyEdit = new MultiKeyEdit(container);
+ multiKeyEdit->setKeySequences(shortcutsConfig.userShortcuts(object));
+ connect(multiKeyEdit, &MultiKeyEdit::keySequenceChanged,
+ this, &ShortcutsEditor::checkForDuplicates);
+ contexts_layouts.value(shortcutContext)->addRow(labels_objects.key(object), multiKeyEdit);
+ multiKeyEdits_objects.insert(multiKeyEdit, object);
+}
+
+void ShortcutsEditor::checkForDuplicates(const QKeySequence &keySequence) {
+ if (keySequence.isEmpty())
+ return;
+
+ auto *sender_multiKeyEdit = qobject_cast(sender());
+ if (!sender_multiKeyEdit)
+ return;
+
+ for (auto *sibling_multiKeyEdit : siblings(sender_multiKeyEdit))
+ if (sibling_multiKeyEdit->contains(keySequence))
+ promptUserOnDuplicateFound(sender_multiKeyEdit, sibling_multiKeyEdit);
+}
+
+QList ShortcutsEditor::siblings(MultiKeyEdit *multiKeyEdit) const {
+ auto list = multiKeyEdit->parent()->findChildren(QString(), Qt::FindDirectChildrenOnly);
+ list.removeOne(multiKeyEdit);
+ return list;
+}
+
+void ShortcutsEditor::promptUserOnDuplicateFound(MultiKeyEdit *sender, MultiKeyEdit *sibling) {
+ const auto duplicateKeySequence = sender->keySequences().last();
+ const auto siblingLabel = getLabel(multiKeyEdits_objects.value(sibling));
+ const auto message = QString(
+ "Shortcut '%1' is already used by '%2', would you like to replace it?")
+ .arg(duplicateKeySequence.toString()).arg(siblingLabel);
+
+ const auto result = QMessageBox::question(
+ this, "porymap", message, QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
+
+ if (result == QMessageBox::Yes)
+ removeKeySequence(duplicateKeySequence, sibling);
+ else
+ removeKeySequence(duplicateKeySequence, sender);
+
+ activateWindow();
+}
+
+void ShortcutsEditor::removeKeySequence(const QKeySequence &keySequence, MultiKeyEdit *multiKeyEdit) {
+ multiKeyEdit->blockSignals(true);
+ multiKeyEdit->removeOne(keySequence);
+ multiKeyEdit->blockSignals(false);
+}
+
+void ShortcutsEditor::dialogButtonClicked(QAbstractButton *button) {
+ auto buttonRole = ui->buttonBox->buttonRole(button);
+ if (buttonRole == QDialogButtonBox::AcceptRole) {
+ saveShortcuts();
+ close();
+ } else if (buttonRole == QDialogButtonBox::ApplyRole) {
+ saveShortcuts();
+ } else if (buttonRole == QDialogButtonBox::RejectRole) {
+ close();
+ } else if (buttonRole == QDialogButtonBox::ResetRole) {
+ resetShortcuts();
+ }
+}
diff --git a/src/ui/tileseteditor.cpp b/src/ui/tileseteditor.cpp
index 950b8d46..a63b6680 100644
--- a/src/ui/tileseteditor.cpp
+++ b/src/ui/tileseteditor.cpp
@@ -6,6 +6,7 @@
#include "paletteutil.h"
#include "imageexport.h"
#include "config.h"
+#include "shortcut.h"
#include
#include
#include
@@ -86,7 +87,6 @@ void TilesetEditor::setTilesets(QString primaryTilesetLabel, QString secondaryTi
void TilesetEditor::initUi() {
ui->setupUi(this);
- new QShortcut(QKeySequence("Ctrl+Shift+Z"), this, SLOT(on_actionRedo_triggered()));
this->tileXFlip = ui->checkBox_xFlip->isChecked();
this->tileYFlip = ui->checkBox_yFlip->isChecked();
this->paletteId = ui->spinBox_paletteSelector->value();
@@ -102,6 +102,7 @@ void TilesetEditor::initUi() {
this->initMetatileLayersItem();
this->initTileSelector();
this->initSelectedTileItem();
+ this->initShortcuts();
this->metatileSelector->select(0);
this->restoreWindowState();
}
@@ -209,6 +210,48 @@ void TilesetEditor::initSelectedTileItem() {
this->ui->graphicsView_selectedTile->setFixedSize(this->selectedTilePixmapItem->pixmap().width() + 2, this->selectedTilePixmapItem->pixmap().height() + 2);
}
+void TilesetEditor::initShortcuts() {
+ initExtraShortcuts();
+
+ shortcutsConfig.load();
+ shortcutsConfig.setDefaultShortcuts(shortcutableObjects());
+ applyUserShortcuts();
+}
+
+void TilesetEditor::initExtraShortcuts() {
+ ui->actionRedo->setShortcuts({ui->actionRedo->shortcut(), QKeySequence("Ctrl+Shift+Z")});
+
+ auto *shortcut_xFlip = new Shortcut(QKeySequence(), ui->checkBox_xFlip, SLOT(toggle()));
+ shortcut_xFlip->setObjectName("shortcut_xFlip");
+ shortcut_xFlip->setWhatsThis("X Flip");
+
+ auto *shortcut_yFlip = new Shortcut(QKeySequence(), ui->checkBox_yFlip, SLOT(toggle()));
+ shortcut_yFlip->setObjectName("shortcut_yFlip");
+ shortcut_yFlip->setWhatsThis("Y Flip");
+}
+
+QObjectList TilesetEditor::shortcutableObjects() const {
+ QObjectList shortcutable_objects;
+
+ for (auto *action : findChildren())
+ if (!action->objectName().isEmpty())
+ shortcutable_objects.append(qobject_cast(action));
+ for (auto *shortcut : findChildren())
+ if (!shortcut->objectName().isEmpty())
+ shortcutable_objects.append(qobject_cast(shortcut));
+
+ return shortcutable_objects;
+}
+
+void TilesetEditor::applyUserShortcuts() {
+ for (auto *action : findChildren())
+ if (!action->objectName().isEmpty())
+ action->setShortcuts(shortcutsConfig.userShortcuts(action));
+ for (auto *shortcut : findChildren())
+ if (!shortcut->objectName().isEmpty())
+ shortcut->setKeys(shortcutsConfig.userShortcuts(shortcut));
+}
+
void TilesetEditor::restoreWindowState() {
logInfo("Restoring tileset editor geometry from previous session.");
QMap geometry = porymapConfig.getTilesetEditorGeometry();