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 +[![Actions Status](https://github.com/huderlem/porymap/workflows/Build%20Porymap/badge.svg)](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();