From 236ad9b73c220008623089df506c2c51b962e568 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 11 Jun 2024 12:26:34 -0400 Subject: [PATCH 01/76] Fix Add Region Map button --- src/ui/regionmapeditor.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui/regionmapeditor.cpp b/src/ui/regionmapeditor.cpp index 220f9cca..92da19c2 100644 --- a/src/ui/regionmapeditor.cpp +++ b/src/ui/regionmapeditor.cpp @@ -298,7 +298,7 @@ bool RegionMapEditor::buildConfigDialog() { form.addRow(addMapButton); // allow user to add region maps - connect(addMapButton, &QPushButton::clicked, [this, regionMapList] { + connect(addMapButton, &QPushButton::clicked, [this, regionMapList, &updateJsonFromList] { poryjson::Json resultJson = configRegionMapDialog(); poryjson::Json::object resultObj = resultJson.object_items(); @@ -310,6 +310,7 @@ bool RegionMapEditor::buildConfigDialog() { newItem->setText(resultObj["alias"].string_value()); newItem->setData(Qt::UserRole, resultStr); regionMapList->addItem(newItem); + updateJsonFromList(); }); QPushButton *delMapButton = new QPushButton("Delete Selected Region Map"); From 0954fe26ff603fbf26a0c01526bcf03343353a14 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Mon, 17 Jun 2024 11:26:45 -0400 Subject: [PATCH 02/76] Fix confusing error logging during region map setup --- include/ui/regionmapeditor.h | 5 +++++ src/mainwindow.cpp | 7 +++---- src/ui/regionmapeditor.cpp | 38 +++++++++++++++--------------------- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/include/ui/regionmapeditor.h b/include/ui/regionmapeditor.h index 3cbc2515..ef72b5ee 100644 --- a/include/ui/regionmapeditor.h +++ b/include/ui/regionmapeditor.h @@ -27,6 +27,7 @@ public: ~RegionMapEditor(); bool load(bool silent = false); + bool setupErrored() const { return setupError; } void onRegionMapTileSelectorSelectedTileChanged(unsigned id); void onRegionMapTileSelectorHoveredTileChanged(unsigned id); @@ -53,9 +54,13 @@ private: RegionMap *region_map = nullptr; tsl::ordered_map region_maps; + QString configFilepath; + QString mapSectionFilepath; + poryjson::Json rmConfigJson; bool configSaved = false; + bool setupError = false; QUndoGroup history; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 6f3b87d7..ae498fb4 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2960,15 +2960,14 @@ bool MainWindow::initRegionMapEditor(bool silent) { this->regionMapEditor = new RegionMapEditor(this, this->editor->project); bool success = this->regionMapEditor->load(silent); if (!success) { - delete this->regionMapEditor; - this->regionMapEditor = nullptr; - if (!silent) { + if (!silent && this->regionMapEditor->setupErrored()) { 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") + QString errorMsg = QString("There was an error opening the region map data. Please see %1 for full error details.\n\n%2") .arg(getLogPath()) .arg(getMostRecentError()); msgBox.critical(nullptr, "Error Opening Region Map Editor", errorMsg); } + delete this->regionMapEditor; return false; } diff --git a/src/ui/regionmapeditor.cpp b/src/ui/regionmapeditor.cpp index 92da19c2..16ea4a5a 100644 --- a/src/ui/regionmapeditor.cpp +++ b/src/ui/regionmapeditor.cpp @@ -28,6 +28,8 @@ RegionMapEditor::RegionMapEditor(QWidget *parent, Project *project) : { this->ui->setupUi(this); this->project = project; + this->configFilepath = QString("%1/%2").arg(this->project->root).arg(projectConfig.getFilePath(ProjectFilePath::json_region_porymap_cfg)); + this->mapSectionFilepath = QString("%1/%2").arg(this->project->root).arg(projectConfig.getFilePath(ProjectFilePath::json_region_map_entries)); this->initShortcuts(); this->restoreWindowState(); } @@ -110,12 +112,10 @@ void RegionMapEditor::applyUserShortcuts() { bool RegionMapEditor::loadRegionMapEntries() { this->region_map_entries.clear(); - QString regionMapSectionFilepath = QString("%1/%2").arg(this->project->root).arg(projectConfig.getFilePath(ProjectFilePath::json_region_map_entries)); - ParseUtil parser; QJsonDocument sectionsDoc; - if (!parser.tryParseJsonFile(§ionsDoc, regionMapSectionFilepath)) { - logError(QString("Failed to read map data from %1").arg(regionMapSectionFilepath)); + if (!parser.tryParseJsonFile(§ionsDoc, this->mapSectionFilepath)) { + logError(QString("Failed to read map data from %1").arg(this->mapSectionFilepath)); return false; } @@ -140,11 +140,9 @@ bool RegionMapEditor::loadRegionMapEntries() { } bool RegionMapEditor::saveRegionMapEntries() { - QString regionMapSectionFilepath = QString("%1/%2").arg(this->project->root).arg(projectConfig.getFilePath(ProjectFilePath::json_region_map_entries)); - - QFile sectionsFile(regionMapSectionFilepath); + QFile sectionsFile(this->mapSectionFilepath); if (!sectionsFile.open(QIODevice::WriteOnly)) { - logError(QString("Error: Could not open %1 for writing").arg(regionMapSectionFilepath)); + logError(QString("Could not open %1 for writing").arg(this->mapSectionFilepath)); return false; } @@ -477,6 +475,7 @@ bool RegionMapEditor::setup() { if (!newMap->loadMapData(o)) { delete newMap; // TODO: consider continue, just reporting error loading single map? + this->setupError = true; return false; } @@ -499,26 +498,21 @@ bool RegionMapEditor::setup() { if (!region_maps.empty()) { setRegionMap(region_maps.begin()->second); } + this->setupError = false; return true; } bool RegionMapEditor::load(bool silent) { // check for config json file - QString jsonConfigFilepath = this->project->root + "/" + projectConfig.getFilePath(ProjectFilePath::json_region_porymap_cfg); - bool badConfig = true; - - if (QFile::exists(jsonConfigFilepath)) { - logInfo("Region map configuration file found."); + if (QFile::exists(this->configFilepath)) { ParseUtil parser; OrderedJson::object obj; - if (parser.tryParseOrderedJsonFile(&obj, jsonConfigFilepath)) { + if (parser.tryParseOrderedJsonFile(&obj, this->configFilepath)) { this->rmConfigJson = OrderedJson(obj); this->configSaved = true; } badConfig = !verifyConfig(this->rmConfigJson); - } else { - logWarn("Region Map config file not found."); } if (badConfig) { @@ -534,14 +528,15 @@ bool RegionMapEditor::load(bool silent) { if (warning.exec() == QMessageBox::Ok) { // there is a separate window that allows to load multiple region maps, if (!buildConfigDialog()) { - logError("Region map loading interrupted [user]"); + // User canceled config set up return false; } } else { - // do not open editor - logError("Region map loading interrupted [user]"); + // User declined config set up return false; } + } else { + logInfo("Successfully loaded region map configuration file."); } return setup(); @@ -583,10 +578,9 @@ void RegionMapEditor::saveConfig() { mapsObject["region_maps"] = mapArray; OrderedJson newConfigJson(mapsObject); - QString filepath = QString("%1/%2").arg(this->project->root).arg(projectConfig.getFilePath(ProjectFilePath::json_region_porymap_cfg)); - QFile file(filepath); + QFile file(this->configFilepath); if (!file.open(QIODevice::WriteOnly)) { - logError(QString("Error: Could not open %1 for writing").arg(filepath)); + logError(QString("Could not open %1 for writing").arg(this->configFilepath)); return; } OrderedJsonDoc jsonDoc(&newConfigJson); From 4a79114b98ec7d07c1a4f286f48baec73581afc7 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Mon, 17 Jun 2024 11:38:01 -0400 Subject: [PATCH 03/76] Fix crash if region map tileset is missing --- src/core/regionmap.cpp | 10 ++++++++-- src/ui/regionmapeditor.cpp | 10 ---------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/core/regionmap.cpp b/src/core/regionmap.cpp index 68da1fa7..9c66860b 100644 --- a/src/core/regionmap.cpp +++ b/src/core/regionmap.cpp @@ -79,14 +79,20 @@ bool RegionMap::loadTilemap(poryjson::Json tilemapJson) { this->palette_path = tilemapObject["palette"].string_value(); } + QFileInfo tilesetFileInfo(fullPath(this->tileset_path)); + if (!tilesetFileInfo.exists() || !tilesetFileInfo.isFile()) { + logError(QString("Failed to open region map tileset file '%1'.").arg(tileset_path)); + return false; + } + QFile tilemapFile(fullPath(this->tilemap_path)); if (!tilemapFile.open(QIODevice::ReadOnly)) { - logError(QString("Failed to open region map tilemap file %1.").arg(tilemap_path)); + logError(QString("Failed to open region map tilemap file '%1'.").arg(tilemap_path)); return false; } if (tilemapFile.size() < tilemapBytes()) { - logError(QString("The region map tilemap at %1 is too small.").arg(tilemap_path)); + logError(QString("The region map tilemap at '%1' is too small.").arg(tilemap_path)); return false; } diff --git a/src/ui/regionmapeditor.cpp b/src/ui/regionmapeditor.cpp index 16ea4a5a..ee491a3e 100644 --- a/src/ui/regionmapeditor.cpp +++ b/src/ui/regionmapeditor.cpp @@ -402,16 +402,6 @@ bool RegionMapEditor::verifyConfig(poryjson::Json cfg) { logError("Region map config json has no map list."); return false; } - - OrderedJson::array arr = obj["region_maps"].array_items(); - - for (auto ref : arr) { - RegionMap tempMap(this->project); - if (!tempMap.loadMapData(ref)) { - return false; - } - } - return true; } From 1c2be70ff0215112718196031ba70787a0831302 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Mon, 17 Jun 2024 12:11:55 -0400 Subject: [PATCH 04/76] Allow users to fix faulty region map settings --- include/mainwindow.h | 1 + include/ui/regionmapeditor.h | 2 ++ src/core/regionmap.cpp | 2 +- src/mainwindow.cpp | 35 ++++++++++++++++++++++++++++------- src/ui/regionmapeditor.cpp | 10 ++++++++-- 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/include/mainwindow.h b/include/mainwindow.h index f3aa9375..7fda1b09 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -388,6 +388,7 @@ private: void initTilesetEditor(); bool initRegionMapEditor(bool silent = false); + bool askToFixRegionMapEditor(); void initShortcutsEditor(); void initCustomScriptsEditor(); void connectSubEditorsToShortcutsEditor(); diff --git a/include/ui/regionmapeditor.h b/include/ui/regionmapeditor.h index ef72b5ee..3d889f88 100644 --- a/include/ui/regionmapeditor.h +++ b/include/ui/regionmapeditor.h @@ -42,6 +42,8 @@ public: void resizeTilemap(int width, int height); + bool reconfigure(); + QObjectList shortcutableObjects() const; public slots: diff --git a/src/core/regionmap.cpp b/src/core/regionmap.cpp index 9c66860b..45fc1141 100644 --- a/src/core/regionmap.cpp +++ b/src/core/regionmap.cpp @@ -303,7 +303,7 @@ bool RegionMap::loadLayout(poryjson::Json layoutJson) { } setLayout("main", layout); } else { - logError("Region map layout is not readable."); + logError(QString("Failed to read region map layout from '%1'.").arg(this->layout_path)); return false; } } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index ae498fb4..0b92c1c9 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2958,14 +2958,12 @@ void MainWindow::on_pushButton_CreatePrefab_clicked() { bool MainWindow::initRegionMapEditor(bool silent) { this->regionMapEditor = new RegionMapEditor(this, this->editor->project); - bool success = this->regionMapEditor->load(silent); - if (!success) { + if (!this->regionMapEditor->load(silent)) { + // The region map editor either failed to load, + // or the user declined configuring their settings. if (!silent && this->regionMapEditor->setupErrored()) { - QMessageBox msgBox(this); - QString errorMsg = QString("There was an error opening the region map data. Please see %1 for full error details.\n\n%2") - .arg(getLogPath()) - .arg(getMostRecentError()); - msgBox.critical(nullptr, "Error Opening Region Map Editor", errorMsg); + if (this->askToFixRegionMapEditor()) + return true; } delete this->regionMapEditor; return false; @@ -2974,6 +2972,29 @@ bool MainWindow::initRegionMapEditor(bool silent) { return true; } +bool MainWindow::askToFixRegionMapEditor() { + QMessageBox msgBox; + msgBox.setIcon(QMessageBox::Critical); + msgBox.setText(QString("There was an error opening the region map data. Please see %1 for full error details.").arg(getLogPath())); + msgBox.setDetailedText(getMostRecentError()); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setDefaultButton(QMessageBox::Ok); + auto reconfigButton = msgBox.addButton("Reconfigure", QMessageBox::ActionRole); + msgBox.exec(); + if (msgBox.clickedButton() == reconfigButton) { + if (this->regionMapEditor->reconfigure()) { + // User fixed error + return true; + } + if (this->regionMapEditor->setupErrored()) { + // User's new settings still fail, show error and ask again + return this->askToFixRegionMapEditor(); + } + } + // User accepted error + return false; +} + void MainWindow::closeSupplementaryWindows() { delete this->tilesetEditor; delete this->regionMapEditor; diff --git a/src/ui/regionmapeditor.cpp b/src/ui/regionmapeditor.cpp index ee491a3e..0f75a730 100644 --- a/src/ui/regionmapeditor.cpp +++ b/src/ui/regionmapeditor.cpp @@ -599,6 +599,11 @@ void RegionMapEditor::on_actionSave_All_triggered() { } void RegionMapEditor::on_action_Configure_triggered() { + reconfigure(); +} + +bool RegionMapEditor::reconfigure() { + this->setupError = false; if (this->modified()) { QMessageBox warning; warning.setIcon(QMessageBox::Warning); @@ -609,15 +614,16 @@ void RegionMapEditor::on_action_Configure_triggered() { if (warning.exec() == QMessageBox::Ok) { if (buildConfigDialog()) { - reload(); + return reload(); } } } else { if (buildConfigDialog()) { - reload(); + return reload(); } } + return false; } void RegionMapEditor::displayRegionMap() { From 79955715dd36051a31cdaf414465cc516b2ef446 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Mon, 17 Jun 2024 14:32:02 -0400 Subject: [PATCH 05/76] Fix crash if region map tileset file is too small --- src/core/regionmap.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/core/regionmap.cpp b/src/core/regionmap.cpp index 45fc1141..7dbd4c5f 100644 --- a/src/core/regionmap.cpp +++ b/src/core/regionmap.cpp @@ -79,12 +79,17 @@ bool RegionMap::loadTilemap(poryjson::Json tilemapJson) { this->palette_path = tilemapObject["palette"].string_value(); } - QFileInfo tilesetFileInfo(fullPath(this->tileset_path)); - if (!tilesetFileInfo.exists() || !tilesetFileInfo.isFile()) { + QImage tilesetFile(fullPath(this->tileset_path)); + if (tilesetFile.isNull()) { logError(QString("Failed to open region map tileset file '%1'.").arg(tileset_path)); return false; } + if (tilesetFile.width() < 8 || tilesetFile.height() < 8) { + logError(QString("Region map tileset file '%1' must be at least 8x8.").arg(tileset_path)); + return false; + } + QFile tilemapFile(fullPath(this->tilemap_path)); if (!tilemapFile.open(QIODevice::ReadOnly)) { logError(QString("Failed to open region map tilemap file '%1'.").arg(tilemap_path)); From 06b6651e46cd5e52cd327d7b1e1bb8cf910d5edf Mon Sep 17 00:00:00 2001 From: GriffinR Date: Mon, 17 Jun 2024 14:32:19 -0400 Subject: [PATCH 06/76] Update changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6d13ce3..9c9977a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,10 @@ and this project somewhat adheres to [Semantic Versioning](https://semver.org/sp The **"Breaking Changes"** listed below are changes that have been made in the decompilation projects (e.g. pokeemerald), which porymap requires in order to work properly. It also includes changes to the scripting API that may change the behavior of existing porymap scripts. If porymap is used with a project or API script that is not up-to-date with the breaking changes, then porymap will likely break or behave improperly. ## [Unreleased] -Nothing, yet. +### Fixed +- Fix `Add Region Map...` not updating the region map settings file. +- Fix some crashes on invalid region map tilesets. +- Improve error reporting for invalid region map editor settings. ## [5.4.1] - 2024-03-21 ### Fixed From 3af14307d3fc785e8e27bf2a1632d2cdc7d2cbab Mon Sep 17 00:00:00 2001 From: GriffinR Date: Mon, 17 Jun 2024 14:47:19 -0400 Subject: [PATCH 07/76] Bump macOS Qt version --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 55edb7b6..9c997ad1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -58,7 +58,7 @@ jobs: - name: Install Qt uses: jurplel/install-qt-action@v3 with: - version: '6.2.*' + version: '6.5.*' cached: ${{ steps.cache-qt.outputs.cache-hit }} - name: Configure From b28d4085eca017ee22afb4fa8b720de6c7d8d8c4 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Wed, 26 Jun 2024 14:51:41 -0400 Subject: [PATCH 08/76] Disable update promoter on Windows --- src/mainwindow.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 6f3b87d7..487c3e5c 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -43,7 +43,9 @@ // We only publish release binaries for Windows and macOS. // This is relevant for the update promoter, which alerts users of a new release. -#if defined(Q_OS_WIN) || defined(Q_OS_MACOS) +// TODO: Currently the update promoter is disabled on our Windows releases because +// the pre-compiled Qt build doesn't link OpenSSL. Re-enable below once this is fixed. +#if /*defined(Q_OS_WIN) || */defined(Q_OS_MACOS) #define RELEASE_PLATFORM #endif From db35ec2c7927b67590e673736ad18ea9d6e02fb6 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Wed, 26 Jun 2024 16:03:39 -0400 Subject: [PATCH 09/76] Initial connections tab UI redesign --- forms/connectionslistitem.ui | 92 ++++++ forms/mainwindow.ui | 499 +++++++++++-------------------- include/editor.h | 5 +- include/ui/connectionslistitem.h | 26 ++ porymap.pro | 3 + resources/icons/map_go.ico | Bin 0 -> 1993 bytes resources/images.qrc | 1 + src/editor.cpp | 97 +++--- src/mainwindow.cpp | 8 +- src/ui/connectionslistitem.cpp | 37 +++ 10 files changed, 383 insertions(+), 385 deletions(-) create mode 100644 forms/connectionslistitem.ui create mode 100644 include/ui/connectionslistitem.h create mode 100755 resources/icons/map_go.ico create mode 100644 src/ui/connectionslistitem.cpp diff --git a/forms/connectionslistitem.ui b/forms/connectionslistitem.ui new file mode 100644 index 00000000..f35c509e --- /dev/null +++ b/forms/connectionslistitem.ui @@ -0,0 +1,92 @@ + + + ConnectionsListItem + + + + 0 + 0 + 176 + 153 + + + + + 0 + 0 + + + + + + + + 0 + 0 + + + + Map + + + + + + + + 0 + 0 + + + + Offset + + + + + + + + 0 + 0 + + + + Direction + + + + + + + ... + + + + :/icons/delete.ico:/icons/delete.ico + + + + + + + + + + + + + + + + + NoScrollComboBox + QComboBox +
noscrollcombobox.h
+
+
+ + + + +
diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index a28f1db9..02f1a690 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -260,7 +260,7 @@ - 0 + 2 false @@ -769,8 +769,8 @@ 0 0 - 423 - 79 + 256 + 74 @@ -888,8 +888,8 @@ 0 0 - 423 - 79 + 256 + 74 @@ -990,10 +990,10 @@ - 8 + 0 0 - 411 - 446 + 98 + 74 @@ -1193,8 +1193,8 @@ 0 0 - 427 - 594 + 98 + 512 @@ -1413,8 +1413,8 @@ 0 0 - 382 - 699 + 98 + 444 @@ -1715,7 +1715,7 @@ 0 0 100 - 16 + 30 @@ -1809,7 +1809,7 @@ 0 0 100 - 16 + 30 @@ -1903,7 +1903,7 @@ 0 0 100 - 16 + 30 @@ -2003,7 +2003,7 @@ 0 0 100 - 16 + 30 @@ -2097,7 +2097,7 @@ 0 0 100 - 16 + 30 @@ -2541,7 +2541,7 @@ 0 - + 0 @@ -2554,24 +2554,9 @@ QFrame::Raised - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - + + + 0 @@ -2590,10 +2575,7 @@ QFrame::Raised - - - 4 - + 4 @@ -2606,229 +2588,7 @@ 4 - - - - - 0 - 0 - - - - <html><head/><body><p>Add a new connection.</p></body></html> - - - - - - - :/icons/add.ico - - - - - - - - <html><head/><body><p>Remove the currently-selected connection.</p></body></html> - - - - - - - :/icons/delete.ico - - - - - - - - Number of Connections: - - - - - - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Expanding - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - <html><head/><body><p>If enabled, connections will automatically be updated on the connected map.</p></body></html> - - - Mirror to Connecting Maps - - - true - - - - - - - - - - - 0 - 0 - - - - false - - - false - - - Qt::ScrollBarAsNeeded - - - Qt::ScrollBarAsNeeded - - - QAbstractScrollArea::AdjustIgnored - - - QGraphicsView::NoDrag - - - QGraphicsView::AnchorUnderMouse - - - QGraphicsView::AnchorUnderMouse - - - - - - - - 0 - 0 - - - - - 0 - 32 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - 6 - - - 4 - - - 4 - - - 4 - - - 4 - - - - - Map - - - - - - - <html><head/><body><p>The destination map name of the connection.</p></body></html> - - - true - - - - - - - Offset - - - - - - - <html><head/><body><p>The number of metatiles to offset the connection.</p></body></html> - - - -999 - - - 999 - - - - - - - <html><head/><body><p>The direction of the connection.</p></body></html> - - - - up - - - - - right - - - - - down - - - - - left - - - - - + Qt::Horizontal @@ -2841,47 +2601,34 @@ - - - - - - - - 0 - 0 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - 4 - - - 4 - - - 4 - - - 4 - - - 4 - - - + + + + If enabled, connections will automatically be updated on the connected map. + + + Mirror to Connecting Maps + + + true + + + + + + + Emerge Map + + + + + Dive Map - + <html><head/><body><p>Destination map name when using <span style=" font-weight:600;">Dive</span>. If empty, no such connection will exist.</p></body></html> @@ -2891,14 +2638,7 @@ - - - - Emerge Map - - - - + <html><head/><body><p>Destination map name when emerging using <span style=" font-weight:600;">Dive</span>. If empty, no such connection will exist.</p></body></html> @@ -2908,22 +2648,147 @@ - - - - Qt::Horizontal + + + + Open the selected Dive Map - - - 40 - 20 - + + ... - + + + :/icons/map_go.ico:/icons/map_go.ico + + + + + + + Open the selected Emerge Map + + + ... + + + + :/icons/map_go.ico:/icons/map_go.ico + + + + + + Qt::Horizontal + + + + + 0 + 0 + + + + false + + + false + + + Qt::ScrollBarAsNeeded + + + Qt::ScrollBarAsNeeded + + + QAbstractScrollArea::AdjustIgnored + + + QGraphicsView::NoDrag + + + QGraphicsView::AnchorUnderMouse + + + QGraphicsView::AnchorUnderMouse + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + Add Connection + + + + :/icons/add.ico:/icons/add.ico + + + + + + + QFrame::NoFrame + + + true + + + + + 0 + 0 + 365 + 658 + + + + + 8 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + diff --git a/include/editor.h b/include/editor.h index f684b491..e9588cae 100644 --- a/include/editor.h +++ b/include/editor.h @@ -171,11 +171,10 @@ private: void setConnectionItemsVisible(bool); void setBorderItemsVisible(bool, qreal = 1); void setConnectionEditControlValues(MapConnection*); - void setConnectionEditControlsEnabled(bool); void setConnectionsEditable(bool); void createConnectionItem(MapConnection* connection); - void populateConnectionMapPickers(); - void setDiveEmergeControls(); + void populateConnectionsList(); + void addConnectionToList(const MapConnection * connection); void updateDiveEmergeMap(QString mapName, QString direction); void onConnectionOffsetChanged(int newOffset); void removeMirroredConnection(MapConnection*); diff --git a/include/ui/connectionslistitem.h b/include/ui/connectionslistitem.h new file mode 100644 index 00000000..0c4ee923 --- /dev/null +++ b/include/ui/connectionslistitem.h @@ -0,0 +1,26 @@ +#ifndef CONNECTIONSLISTITEM_H +#define CONNECTIONSLISTITEM_H + +#include "mapconnection.h" + +#include + +namespace Ui { +class ConnectionsListItem; +} + +class ConnectionsListItem : public QFrame +{ + Q_OBJECT + +public: + explicit ConnectionsListItem(QWidget *parent, const QStringList &mapNames); + ~ConnectionsListItem(); + + void populate(const MapConnection * connection); + +public: + Ui::ConnectionsListItem *ui; +}; + +#endif // CONNECTIONSLISTITEM_H diff --git a/porymap.pro b/porymap.pro index dfc3306e..5c898302 100644 --- a/porymap.pro +++ b/porymap.pro @@ -46,6 +46,7 @@ SOURCES += src/core/block.cpp \ src/scriptapi/apiutility.cpp \ src/scriptapi/scripting.cpp \ src/ui/aboutporymap.cpp \ + src/ui/connectionslistitem.cpp \ src/ui/customscriptseditor.cpp \ src/ui/customscriptslistitem.cpp \ src/ui/draggablepixmapitem.cpp \ @@ -139,6 +140,7 @@ HEADERS += include/core/block.h \ include/lib/orderedmap.h \ include/lib/orderedjson.h \ include/ui/aboutporymap.h \ + include/ui/connectionslistitem.h \ include/ui/customscriptseditor.h \ include/ui/customscriptslistitem.h \ include/ui/draggablepixmapitem.h \ @@ -205,6 +207,7 @@ HEADERS += include/core/block.h \ include/ui/updatepromoter.h FORMS += forms/mainwindow.ui \ + forms/connectionslistitem.ui \ forms/prefabcreationdialog.ui \ forms/prefabframe.ui \ forms/tileseteditor.ui \ diff --git a/resources/icons/map_go.ico b/resources/icons/map_go.ico new file mode 100755 index 0000000000000000000000000000000000000000..8ca93206b6b0c8e9fbf149cc42c0667ff67fa908 GIT binary patch literal 1993 zcmV;)2R8VLP)R~7#5y)(1B_S&1o zc5KIq$%9luq7Mk7{AdaSS_Cnu{*eMd8Wa_wmMTOljYLr!tEE&?RkcVUM2ke4q<|Vw zRgt<;LI_C#wRI9`X$nekn#7N-z3WZusPQ=Zu9dYZRpUXLMQD}frNyN9|q-o=Gd}5 zu>bTW2)h=r9Y_zs)V_wNOK?$SY&=-PgG1*Puf?>}T^YIZnpyRIz~x29x)y}CqMQ>I z2Snv%$BCn7(82dN0DJ!hj9&m++W=1j_eFlrr+r*Xrtm=R6NsTeCp3f*7#nyWiRG)I zwO=+ZWOdEg8bvN|ZWs$4(-rTk5vUGZ+E5C}F9Vu-9Zba(EM-Fr!X+6sQKU0b+!7f^ zue$^hMnTfKBC(As6Rq&<)7E@HJe(ABGDFUE08kA3TR#MHtrkyWL2@eTAca%h*#zIl zL-ogC^W>hi0&9YsOWsR*6HH!DUo%m%)ZEmxFbzG6PGO}f&;C-M#lx99FgkD!jVo3|R5?&Ko@iS| z4)}10ppw~ZL!d->2*PltQ(@h&a|pK75=z@R?C-&tKSE&XH=U}N6ZmPOax4fBy;BEgAttCT0`5y*e9MQ@qE1DzZ?dVPG}2@R2Dy4 zdJ?S`ozQ$n3(StSxl{`wp&^QKRPqf!W>D0)W#qBE&7C%S`Vspe8TZ-2L9!K+@29a*9YvHmyf3qYk}InuoGKB5Rc+UHARu8Y9pH%_M_?o)(D$1J z8vm=|w%c$R@=TwPAc;xuqQ=HW*f10=(K~O!2 z>z>x~vobPI7R=TP;Tu_G^Hc}--3O8Hd4#yLkC;ec;@wUti|8^^5ywKzj^bkU1Q1yq z%($}%K{ey})@|FdqJ9{^I(!eKxnzDFC=*bZ+acvbWbTB2_C7X_M-aLBdE|$4NVNVF z7ysG?w<(2J&%T7?@3Cg+FcUQ^W`(@ruaLFqSicE>e&0Yj?nkorfBz;WJUBhS|EU+KDrV9>9sx3wY@V&ttT>KMd&1_RK&ibpzh} z;1Brf`g@?9y=Z)#!PtRKMm4P2U0egZp}H85=zA9Fi6R=l+yxcOk{`9iGr}y^k~K-L|>KYI5=Oz*X3G+_IKEIO6ii?4iY3(Lo@PW2>SIrw9AURfdXl;Z#F zf@}99zvfX^-V8jKV&eG}6xKuEW7*}ksqyh-5cjN?EOT#;$tJ2=vnUNXlldJ!`-xAQ z`wl)4vu4}3`@V|A8)9;LZrDLE z0)2zPks~;I>?v&Hl53R}P=4s|oqKP;8_(?Chq<;e#tSt5oo|=P(ANk2sb?cjJk|XW zSBM*OfdjQlPb9hy)VajjU*MOSX)+YGI$~|*0k#KyO;Y#%4|w&> zFQLEm1V0eEjVxX&*G|AF#wyOmxH^mhRMF&i~ z%AOj7*WrtFvH8&X`|B8|@y^i3SZMPK&RmaSqPj9X=m4i_9NgL3b3F%D05#!tcD8Xd b{x84)t}!rl^*H;f00000NkvXXu0mjfXHKz& literal 0 HcmV?d00001 diff --git a/resources/images.qrc b/resources/images.qrc index 86d56cb2..bdac1a47 100644 --- a/resources/images.qrc +++ b/resources/images.qrc @@ -65,5 +65,6 @@ images/Entities_16x16.png images/pokemon_icon_placeholder.png icons/clipboard.ico + icons/map_go.ico diff --git a/src/editor.cpp b/src/editor.cpp index 014fa489..39e2718d 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -2,7 +2,7 @@ #include "draggablepixmapitem.h" #include "imageproviders.h" #include "log.h" -#include "mapconnection.h" +#include "connectionslistitem.h" #include "currentselectedmetatilespixmapitem.h" #include "mapsceneeventfilter.h" #include "metatile.h" @@ -178,11 +178,7 @@ void Editor::setEditingConnections() { map_item->paintingMode = MapPixmapItem::PaintMode::Disabled; map_item->draw(); map_item->setVisible(true); - populateConnectionMapPickers(); - ui->label_NumConnections->setText(QString::number(map->connections.length())); - setDiveEmergeControls(); - bool controlsEnabled = selected_connection_item != nullptr; - setConnectionEditControlsEnabled(controlsEnabled); + populateConnectionsList(); if (selected_connection_item) { onConnectionOffsetChanged(selected_connection_item->connection->offset); setConnectionMap(selected_connection_item->connection->map_name); @@ -739,11 +735,18 @@ void Editor::updateEncounterFields(EncounterFields newFields) { project->wildMonFields = newFields; } -void Editor::setDiveEmergeControls() { - ui->comboBox_DiveMap->blockSignals(true); - ui->comboBox_EmergeMap->blockSignals(true); +void Editor::populateConnectionsList() { + const QSignalBlocker blocker1(ui->comboBox_DiveMap); + const QSignalBlocker blocker2(ui->comboBox_EmergeMap); + + ui->comboBox_DiveMap->clear(); + ui->comboBox_DiveMap->addItems(project->mapNames); ui->comboBox_DiveMap->setCurrentText(""); + + ui->comboBox_EmergeMap->clear(); + ui->comboBox_EmergeMap->addItems(project->mapNames); ui->comboBox_EmergeMap->setCurrentText(""); + for (MapConnection* connection : map->connections) { if (connection->direction == "dive") { ui->comboBox_DiveMap->setCurrentText(connection->map_name); @@ -751,25 +754,20 @@ void Editor::setDiveEmergeControls() { ui->comboBox_EmergeMap->setCurrentText(connection->map_name); } } - ui->comboBox_DiveMap->blockSignals(false); - ui->comboBox_EmergeMap->blockSignals(false); + + // Clear any existing connections in list + for (auto w : ui->scrollAreaContents_ConnectionsList->findChildren()) + w->deleteLater(); + + for (auto item :connection_items) + addConnectionToList(item->connection); } -void Editor::populateConnectionMapPickers() { - ui->comboBox_ConnectedMap->blockSignals(true); - ui->comboBox_DiveMap->blockSignals(true); - ui->comboBox_EmergeMap->blockSignals(true); - - ui->comboBox_ConnectedMap->clear(); - ui->comboBox_ConnectedMap->addItems(project->mapNames); - ui->comboBox_DiveMap->clear(); - ui->comboBox_DiveMap->addItems(project->mapNames); - ui->comboBox_EmergeMap->clear(); - ui->comboBox_EmergeMap->addItems(project->mapNames); - - ui->comboBox_ConnectedMap->blockSignals(false); - ui->comboBox_DiveMap->blockSignals(true); - ui->comboBox_EmergeMap->blockSignals(true); +// TODO: Vertical scrolling +void Editor::addConnectionToList(const MapConnection * connection) { + ConnectionsListItem *listItem = new ConnectionsListItem(ui->scrollAreaContents_ConnectionsList, project->mapNames); + listItem->populate(connection); + ui->layout_ConnectionsList->insertWidget(ui->layout_ConnectionsList->count() - 1, listItem); // Insert above the vertical spacer } void Editor::setConnectionItemsVisible(bool visible) { @@ -848,38 +846,15 @@ void Editor::onConnectionMoved(MapConnection* connection) { } void Editor::onConnectionOffsetChanged(int newOffset) { - ui->spinBox_ConnectionOffset->blockSignals(true); + // Connections TODO: Change offset spin box for selected connection + /*ui->spinBox_ConnectionOffset->blockSignals(true); ui->spinBox_ConnectionOffset->setValue(newOffset); ui->spinBox_ConnectionOffset->blockSignals(false); - + */ } void Editor::setConnectionEditControlValues(MapConnection* connection) { - QString mapName = connection ? connection->map_name : ""; - QString direction = connection ? connection->direction : ""; - int offset = connection ? connection->offset : 0; - - ui->comboBox_ConnectedMap->blockSignals(true); - ui->comboBox_ConnectionDirection->blockSignals(true); - ui->spinBox_ConnectionOffset->blockSignals(true); - - ui->comboBox_ConnectedMap->setCurrentText(mapName); - ui->comboBox_ConnectionDirection->setCurrentText(direction); - ui->spinBox_ConnectionOffset->setValue(offset); - - ui->comboBox_ConnectedMap->blockSignals(false); - ui->comboBox_ConnectionDirection->blockSignals(false); - ui->spinBox_ConnectionOffset->blockSignals(false); -} - -void Editor::setConnectionEditControlsEnabled(bool enabled) { - ui->comboBox_ConnectionDirection->setEnabled(enabled); - ui->comboBox_ConnectedMap->setEnabled(enabled); - ui->spinBox_ConnectionOffset->setEnabled(enabled); - - if (!enabled) { - setConnectionEditControlValues(nullptr); - } + // Connections TODO: Highlight selected connection } void Editor::setConnectionsEditable(bool editable) { @@ -896,10 +871,11 @@ void Editor::onConnectionItemSelected(ConnectionPixmapItem* connectionItem) { selected_connection_item = connectionItem; for (ConnectionPixmapItem* item : connection_items) item->updateHighlight(item == selected_connection_item); - setConnectionEditControlsEnabled(true); setConnectionEditControlValues(selected_connection_item->connection); + /* // Connections TODO: ui->spinBox_ConnectionOffset->setMaximum(selected_connection_item->getMaxOffset()); ui->spinBox_ConnectionOffset->setMinimum(selected_connection_item->getMinOffset()); + */ onConnectionOffsetChanged(selected_connection_item->connection->offset); } @@ -918,9 +894,11 @@ void Editor::onConnectionItemDoubleClicked(ConnectionPixmapItem* connectionItem) } void Editor::onConnectionDirectionChanged(QString newDirection) { + /* // Connections TODO: ui->comboBox_ConnectionDirection->blockSignals(true); ui->comboBox_ConnectionDirection->setCurrentText(newDirection); ui->comboBox_ConnectionDirection->blockSignals(false); + */ } void Editor::onBorderMetatilesChanged() { @@ -1746,13 +1724,12 @@ void Editor::setConnectionMap(QString mapName) { } QString originalMapName = selected_connection_item->connection->map_name; - setConnectionEditControlsEnabled(true); selected_connection_item->connection->map_name = mapName; setCurrentConnectionDirection(selected_connection_item->connection->direction); // New map may have a different minimum offset than the last one. The maximum will be the same. int min = selected_connection_item->getMinOffset(); - ui->spinBox_ConnectionOffset->setMinimum(min); + //ui->spinBox_ConnectionOffset->setMinimum(min); // Connections TODO: onConnectionOffsetChanged(qMax(min, selected_connection_item->connection->offset)); updateMirroredConnectionMap(selected_connection_item->connection, originalMapName); @@ -1786,8 +1763,8 @@ void Editor::addNewConnection() { newConnection->map_name = defaultMapName; map->connections.append(newConnection); createConnectionItem(newConnection); + addConnectionToList(newConnection); onConnectionItemSelected(connection_items.last()); - ui->label_NumConnections->setText(QString::number(map->connections.length())); updateMirroredConnection(newConnection, newConnection->direction, newConnection->map_name); } @@ -1867,9 +1844,7 @@ void Editor::removeCurrentConnection() { } selected_connection_item = nullptr; - setConnectionEditControlsEnabled(false); - ui->spinBox_ConnectionOffset->setValue(0); - ui->label_NumConnections->setText(QString::number(map->connections.length())); + //ui->spinBox_ConnectionOffset->setValue(0); // Connections TODO: if (connection_items.length() > 0) { onConnectionItemSelected(connection_items.last()); @@ -1918,8 +1893,6 @@ void Editor::updateDiveEmergeMap(QString mapName, QString direction) { updateMirroredConnectionMap(connection, originalMapName); } } - - ui->label_NumConnections->setText(QString::number(map->connections.length())); } void Editor::updatePrimaryTileset(QString tilesetLabel, bool forceLoad) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 98b602ee..814e60fa 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2546,18 +2546,19 @@ void MainWindow::showExportMapImageWindow(ImageExporterMode mode) { openSubWindow(this->mapImageExporter); } +// TODO: Move responsibility to list item void MainWindow::on_comboBox_ConnectionDirection_currentTextChanged(const QString &direction) { editor->updateCurrentConnectionDirection(direction); markMapEdited(); } - +// TODO: Move responsibility to list item void MainWindow::on_spinBox_ConnectionOffset_valueChanged(int offset) { editor->updateConnectionOffset(offset); markMapEdited(); } - +// TODO: Move responsibility to list item void MainWindow::on_comboBox_ConnectedMap_currentTextChanged(const QString &mapName) { if (mapName.isEmpty() || editor->project->mapNames.contains(mapName)) { @@ -2568,10 +2569,11 @@ void MainWindow::on_comboBox_ConnectedMap_currentTextChanged(const QString &mapN void MainWindow::on_pushButton_AddConnection_clicked() { + // TODO: Bring up a prompt for information. Mark the current map *AND* the connected map as edited editor->addNewConnection(); markMapEdited(); } - +// TODO: Move responsibility to list item void MainWindow::on_pushButton_RemoveConnection_clicked() { editor->removeCurrentConnection(); diff --git a/src/ui/connectionslistitem.cpp b/src/ui/connectionslistitem.cpp new file mode 100644 index 00000000..47589a12 --- /dev/null +++ b/src/ui/connectionslistitem.cpp @@ -0,0 +1,37 @@ +#include "connectionslistitem.h" +#include "ui_connectionslistitem.h" + +static const QStringList directions = {"up", "down", "left", "right"}; + +ConnectionsListItem::ConnectionsListItem(QWidget *parent, const QStringList &mapNames) : + QFrame(parent), + ui(new Ui::ConnectionsListItem) +{ + ui->setupUi(this); + + const QSignalBlocker blocker1(ui->comboBox_Direction); + const QSignalBlocker blocker2(ui->comboBox_Map); + const QSignalBlocker blocker3(ui->spinBox_Offset); + + ui->comboBox_Direction->setEditable(false); + ui->comboBox_Direction->setMinimumContentsLength(0); + ui->comboBox_Direction->addItems(directions); + + ui->comboBox_Map->setMinimumContentsLength(6); + ui->comboBox_Map->addItems(mapNames); +} + +void ConnectionsListItem::populate(const MapConnection * connection) { + const QSignalBlocker blocker1(ui->comboBox_Direction); + const QSignalBlocker blocker2(ui->comboBox_Map); + const QSignalBlocker blocker3(ui->spinBox_Offset); + + ui->comboBox_Direction->setTextItem(connection->direction); + ui->comboBox_Map->setTextItem(connection->map_name); + ui->spinBox_Offset->setValue(connection->offset); +} + +ConnectionsListItem::~ConnectionsListItem() +{ + delete ui; +} From 391285aee89e211fb05426701f8833822a79e74e Mon Sep 17 00:00:00 2001 From: GriffinR Date: Fri, 28 Jun 2024 22:01:13 -0400 Subject: [PATCH 10/76] Add buttons to open Dive/Emerge maps --- forms/mainwindow.ui | 94 ++++++++++++++++++++++---------------------- include/mainwindow.h | 3 ++ include/project.h | 1 + src/editor.cpp | 2 +- src/mainwindow.cpp | 71 ++++++++++++++++----------------- src/project.cpp | 4 ++ 6 files changed, 92 insertions(+), 83 deletions(-) diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index 02f1a690..75ceeb2e 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -992,7 +992,7 @@ 0 0 - 98 + 91 74 @@ -1193,7 +1193,7 @@ 0 0 - 98 + 91 512 @@ -1413,8 +1413,8 @@ 0 0 - 98 - 444 + 93 + 460 @@ -1715,7 +1715,7 @@ 0 0 100 - 30 + 16 @@ -1809,7 +1809,7 @@ 0 0 100 - 30 + 16 @@ -1903,7 +1903,7 @@ 0 0 100 - 30 + 16 @@ -2003,7 +2003,7 @@ 0 0 100 - 30 + 16 @@ -2097,7 +2097,7 @@ 0 0 100 - 30 + 16 @@ -2588,7 +2588,7 @@ 4 - + Qt::Horizontal @@ -2601,7 +2601,7 @@ - + If enabled, connections will automatically be updated on the connected map. @@ -2614,41 +2614,7 @@ - - - - Emerge Map - - - - - - - Dive Map - - - - - - - <html><head/><body><p>Destination map name when using <span style=" font-weight:600;">Dive</span>. If empty, no such connection will exist.</p></body></html> - - - true - - - - - - - <html><head/><body><p>Destination map name when emerging using <span style=" font-weight:600;">Dive</span>. If empty, no such connection will exist.</p></body></html> - - - true - - - - + Open the selected Dive Map @@ -2662,7 +2628,7 @@ - + Open the selected Emerge Map @@ -2676,6 +2642,40 @@ + + + + Emerge Map + + + + + + + Dive Map + + + + + + + <html><head/><body><p>Destination map name when using <span style=" font-weight:600;">Dive</span>. If empty, no such connection will exist.</p></body></html> + + + true + + + + + + + <html><head/><body><p>Destination map name when emerging using <span style=" font-weight:600;">Dive</span>. If empty, no such connection will exist.</p></body></html> + + + true + + + diff --git a/include/mainwindow.h b/include/mainwindow.h index 7fda1b09..6bcef815 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -248,6 +248,8 @@ private slots: void on_comboBox_ConnectedMap_currentTextChanged(const QString &mapName); void on_pushButton_AddConnection_clicked(); void on_pushButton_RemoveConnection_clicked(); + void on_button_OpenDiveMap_clicked(); + void on_button_OpenEmergeMap_clicked(); void on_comboBox_DiveMap_currentTextChanged(const QString &mapName); void on_comboBox_EmergeMap_currentTextChanged(const QString &mapName); void on_comboBox_PrimaryTileset_currentTextChanged(const QString &arg1); @@ -342,6 +344,7 @@ private: bool tilesetNeedsRedraw = false; + bool userSetMap(QString, bool scrollTreeView = false); bool setMap(QString, bool scrollTreeView = false); void redrawMapScene(); void refreshMapScene(); diff --git a/include/project.h b/include/project.h index 643f74da..49c0aaa8 100644 --- a/include/project.h +++ b/include/project.h @@ -119,6 +119,7 @@ public: bool readMapGroups(); Map* addNewMapToGroup(QString, int, Map*, bool, bool); QString getNewMapName(); + bool isExistingMapName(const QString&); QString getProjectTitle(); QString readMapLayoutId(QString map_name); diff --git a/src/editor.cpp b/src/editor.cpp index 39e2718d..ee314c19 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -763,7 +763,7 @@ void Editor::populateConnectionsList() { addConnectionToList(item->connection); } -// TODO: Vertical scrolling +// TODO: Don't allow splitter resizing to shrink list items to the point where horizontal scroll is needed void Editor::addConnectionToList(const MapConnection * connection) { ConnectionsListItem *listItem = new ConnectionsListItem(ui->scrollAreaContents_ConnectionsList, project->mapNames); listItem->populate(connection); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 814e60fa..20a48990 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -718,6 +718,21 @@ void MainWindow::on_action_Reload_Project_triggered() { openProject(editor->project->root); } +// setMap, but with a visible error message in case of failure. +// Use when the user is specifically requesting a map to open. +bool MainWindow::userSetMap(QString map_name, bool scrollTreeView) { + if (!setMap(map_name, scrollTreeView)) { + QMessageBox msgBox(this); + QString errorMsg = QString("There was an error opening map %1. Please see %2 for full error details.\n\n%3") + .arg(map_name) + .arg(getLogPath()) + .arg(getMostRecentError()); + msgBox.critical(nullptr, "Error Opening Map", errorMsg); + return false; + } + return true; +} + bool MainWindow::setMap(QString map_name, bool scrollTreeView) { logInfo(QString("Setting map to '%1'").arg(map_name)); if (map_name.isEmpty()) { @@ -810,15 +825,8 @@ void MainWindow::openWarpMap(QString map_name, int event_id, Event::Group event_ } // Open the destination map. - if (!setMap(map_name, true)) { - QMessageBox msgBox(this); - QString errorMsg = QString("There was an error opening map %1. Please see %2 for full error details.\n\n%3") - .arg(map_name) - .arg(getLogPath()) - .arg(getMostRecentError()); - msgBox.critical(nullptr, "Error Opening Map", errorMsg); + if (!userSetMap(map_name, true)) return; - } // Select the target event. int index = event_id - Event::getIndexOffset(event_group); @@ -1468,17 +1476,8 @@ void MainWindow::currentMetatilesSelectionChanged() { void MainWindow::on_mapList_activated(const QModelIndex &index) { QVariant data = index.data(Qt::UserRole); - if (index.data(MapListUserRoles::TypeRole) == "map_name" && !data.isNull()) { - QString mapName = data.toString(); - if (!setMap(mapName)) { - QMessageBox msgBox(this); - QString errorMsg = QString("There was an error opening map %1. Please see %2 for full error details.\n\n%3") - .arg(mapName) - .arg(getLogPath()) - .arg(getMostRecentError()); - msgBox.critical(nullptr, "Error Opening Map", errorMsg); - } - } + if (index.data(MapListUserRoles::TypeRole) == "map_name" && !data.isNull()) + userSetMap(data.toString()); } void MainWindow::drawMapListIcons(QAbstractItemModel *model) { @@ -2421,16 +2420,8 @@ void MainWindow::clickToolButtonFromEditMode(QString editMode) { } void MainWindow::onLoadMapRequested(QString mapName, QString fromMapName) { - if (!setMap(mapName, true)) { - QMessageBox msgBox(this); - QString errorMsg = QString("There was an error opening map %1. Please see %2 for full error details.\n\n%3") - .arg(mapName) - .arg(getLogPath()) - .arg(getMostRecentError()); - msgBox.critical(nullptr, "Error Opening Map", errorMsg); - return; - } - editor->setSelectedConnectionFromMap(fromMapName); + if (userSetMap(mapName, true)) + editor->setSelectedConnectionFromMap(fromMapName); } void MainWindow::onMapChanged(Map *) { @@ -2592,17 +2583,27 @@ void MainWindow::on_pushButton_ConfigureEncountersJSON_clicked() { editor->configureEncounterJSON(this); } -void MainWindow::on_comboBox_DiveMap_currentTextChanged(const QString &mapName) -{ - if (mapName.isEmpty() || editor->project->mapNames.contains(mapName)) { +void MainWindow::on_button_OpenDiveMap_clicked() { + const QString mapName = ui->comboBox_DiveMap->currentText(); + if (editor->project->isExistingMapName(mapName)) + userSetMap(mapName, true); +} + +void MainWindow::on_button_OpenEmergeMap_clicked() { + const QString mapName = ui->comboBox_EmergeMap->currentText(); + if (editor->project->isExistingMapName(mapName)) + userSetMap(mapName, true); +} + +void MainWindow::on_comboBox_DiveMap_currentTextChanged(const QString &mapName) { + if (editor->project->isExistingMapName(mapName)) { editor->updateDiveMap(mapName); markMapEdited(); } } -void MainWindow::on_comboBox_EmergeMap_currentTextChanged(const QString &mapName) -{ - if (mapName.isEmpty() || editor->project->mapNames.contains(mapName)) { +void MainWindow::on_comboBox_EmergeMap_currentTextChanged(const QString &mapName) { + if (editor->project->isExistingMapName(mapName)) { editor->updateEmergeMap(mapName); markMapEdited(); } diff --git a/src/project.cpp b/src/project.cpp index c6550c6a..c1c8f5c7 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -1800,6 +1800,10 @@ QString Project::getNewMapName() { return newMapName; } +bool Project::isExistingMapName(const QString &mapName) { + return !mapName.isEmpty() && mapName != DYNAMIC_MAP_NAME && this->mapNames.contains(mapName); +} + Project::DataQualifiers Project::getDataQualifiers(QString text, QString label) { Project::DataQualifiers qualifiers; From 2fa3a9b3982d7ba89caf48f66566c90f208e2bfe Mon Sep 17 00:00:00 2001 From: GriffinR Date: Mon, 1 Jul 2024 13:58:58 -0400 Subject: [PATCH 11/76] Connection reorganization --- include/editor.h | 5 +- include/mainwindow.h | 4 - include/ui/connectionslistitem.h | 6 ++ src/editor.cpp | 167 +++++++++++++------------------ src/mainwindow.cpp | 28 +----- src/ui/connectionslistitem.cpp | 41 ++++++++ 6 files changed, 119 insertions(+), 132 deletions(-) diff --git a/include/editor.h b/include/editor.h index e9588cae..37f60b6a 100644 --- a/include/editor.h +++ b/include/editor.h @@ -74,11 +74,8 @@ public: void setEditingObjects(); void setEditingConnections(); void setMapEditingButtonsEnabled(bool enabled); - void setCurrentConnectionDirection(QString curDirection); - void updateCurrentConnectionDirection(QString curDirection); void setConnectionsVisibility(bool visible); void updateConnectionOffset(int offset); - void setConnectionMap(QString mapName); void addNewConnection(); void removeCurrentConnection(); void addNewWildMonGroup(QWidget *window); @@ -170,8 +167,8 @@ private: void setConnectionItemsVisible(bool); void setBorderItemsVisible(bool, qreal = 1); - void setConnectionEditControlValues(MapConnection*); void setConnectionsEditable(bool); + void redrawConnection(ConnectionPixmapItem* connectionItem); void createConnectionItem(MapConnection* connection); void populateConnectionsList(); void addConnectionToList(const MapConnection * connection); diff --git a/include/mainwindow.h b/include/mainwindow.h index 6bcef815..51bfecc3 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -243,11 +243,7 @@ private slots: void on_actionExport_Map_Timelapse_Image_triggered(); void on_actionImport_Map_from_Advance_Map_1_92_triggered(); - void on_comboBox_ConnectionDirection_currentTextChanged(const QString &arg1); - void on_spinBox_ConnectionOffset_valueChanged(int offset); - void on_comboBox_ConnectedMap_currentTextChanged(const QString &mapName); void on_pushButton_AddConnection_clicked(); - void on_pushButton_RemoveConnection_clicked(); void on_button_OpenDiveMap_clicked(); void on_button_OpenEmergeMap_clicked(); void on_comboBox_DiveMap_currentTextChanged(const QString &mapName); diff --git a/include/ui/connectionslistitem.h b/include/ui/connectionslistitem.h index 0c4ee923..a9dab7ca 100644 --- a/include/ui/connectionslistitem.h +++ b/include/ui/connectionslistitem.h @@ -21,6 +21,12 @@ public: public: Ui::ConnectionsListItem *ui; + +private slots: + void on_comboBox_Direction_currentTextChanged(const QString &direction); + void on_comboBox_Map_currentTextChanged(const QString &mapName); + void on_spinBox_Offset_valueChanged(int offset); + void on_button_Delete_clicked(); }; #endif // CONNECTIONSLISTITEM_H diff --git a/src/editor.cpp b/src/editor.cpp index ee314c19..ec3e7f0b 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -180,9 +180,8 @@ void Editor::setEditingConnections() { map_item->setVisible(true); populateConnectionsList(); if (selected_connection_item) { - onConnectionOffsetChanged(selected_connection_item->connection->offset); - setConnectionMap(selected_connection_item->connection->map_name); - setCurrentConnectionDirection(selected_connection_item->connection->direction); + // TODO: Do we need additional handling here again + redrawConnection(selected_connection_item); } maskNonVisibleConnectionTiles(); } @@ -784,79 +783,101 @@ void Editor::setBorderItemsVisible(bool visible, qreal opacity) { } } -void Editor::setCurrentConnectionDirection(QString curDirection) { - if (!selected_connection_item) +void Editor::redrawConnection(ConnectionPixmapItem* connectionItem) { + if (!connectionItem || !connectionItem->connection) return; - Map *connected_map = project->getMap(selected_connection_item->connection->map_name); - if (!connected_map) { + + const QString mapName = connectionItem->connection->map_name; + if (mapName.isEmpty()) + return; + + // TODO: What happens if a connection is saved with an empty name + if (mapName == DYNAMIC_MAP_NAME || !project->mapNames.contains(mapName)) { + logError(QString("Invalid map name '%1' specified for connection.").arg(mapName)); return; } - selected_connection_item->connection->direction = curDirection; + Map *connectedMap = project->getMap(mapName); + if (!connectedMap) + return; - QPixmap pixmap = connected_map->renderConnection(*selected_connection_item->connection, map->layout); - int offset = selected_connection_item->connection->offset; - selected_connection_item->initialOffset = offset; + QPixmap pixmap = connectedMap->renderConnection(*connectionItem->connection, map->layout); + + int offset = connectionItem->connection->offset; + connectionItem->initialOffset = offset; int x = 0, y = 0; - if (selected_connection_item->connection->direction == "up") { + if (connectionItem->connection->direction == "up") { x = offset * 16; y = -pixmap.height(); - } else if (selected_connection_item->connection->direction == "down") { + } else if (connectionItem->connection->direction == "down") { x = offset * 16; y = map->getHeight() * 16; - } else if (selected_connection_item->connection->direction == "left") { + } else if (connectionItem->connection->direction == "left") { x = -pixmap.width(); y = offset * 16; - } else if (selected_connection_item->connection->direction == "right") { + } else if (connectionItem->connection->direction == "right") { x = map->getWidth() * 16; y = offset * 16; } - selected_connection_item->basePixmap = pixmap; - QPainter painter(&pixmap); - painter.setPen(QColor(255, 0, 255)); - painter.drawRect(0, 0, pixmap.width() - 1, pixmap.height() - 1); - painter.end(); - selected_connection_item->setPixmap(pixmap); - selected_connection_item->initialX = x; - selected_connection_item->initialY = y; - selected_connection_item->blockSignals(true); - selected_connection_item->setX(x); - selected_connection_item->setY(y); - selected_connection_item->setZValue(-1); - selected_connection_item->blockSignals(false); + connectionItem->basePixmap = pixmap; - setConnectionEditControlValues(selected_connection_item->connection); + // TODO: Make sure offset limiting is correct (and updated) + // New map may have a different minimum offset than the last one. The maximum will be the same. + /*int min = selected_connection_item->getMinOffset(); + //ui->spinBox_ConnectionOffset->setMinimum(min); // Connections TODO: + onConnectionOffsetChanged(qMax(min, selected_connection_item->connection->offset));*/ + + if (connectionItem == selected_connection_item) { + QPainter painter(&pixmap); + painter.setPen(QColor(255, 0, 255)); + painter.drawRect(0, 0, pixmap.width() - 1, pixmap.height() - 1); + painter.end(); + } + connectionItem->setPixmap(pixmap); + connectionItem->initialX = x; + connectionItem->initialY = y; + connectionItem->blockSignals(true); + connectionItem->setX(x); + connectionItem->setY(y); + connectionItem->setZValue(-1); + connectionItem->blockSignals(false); + + // TODO: + //updateMirroredConnectionMap(selected_connection_item->connection, originalMapName); + + maskNonVisibleConnectionTiles(); } -void Editor::updateCurrentConnectionDirection(QString curDirection) { +// TODO: Generalize +void Editor::updateConnectionOffset(int offset) { if (!selected_connection_item) return; - QString originalDirection = selected_connection_item->connection->direction; - setCurrentConnectionDirection(curDirection); - updateMirroredConnectionDirection(selected_connection_item->connection, originalDirection); + selected_connection_item->blockSignals(true); + offset = qMin(offset, selected_connection_item->getMaxOffset()); + offset = qMax(offset, selected_connection_item->getMinOffset()); + selected_connection_item->connection->offset = offset; + if (selected_connection_item->connection->direction == "up" || selected_connection_item->connection->direction == "down") { + selected_connection_item->setX(selected_connection_item->initialX + (offset - selected_connection_item->initialOffset) * 16); + } else if (selected_connection_item->connection->direction == "left" || selected_connection_item->connection->direction == "right") { + selected_connection_item->setY(selected_connection_item->initialY + (offset - selected_connection_item->initialOffset) * 16); + } + selected_connection_item->blockSignals(false); + updateMirroredConnectionOffset(selected_connection_item->connection); maskNonVisibleConnectionTiles(); } void Editor::onConnectionMoved(MapConnection* connection) { - updateMirroredConnectionOffset(connection); - onConnectionOffsetChanged(connection->offset); + // TODO: + //updateMirroredConnectionOffset(connection); + + // TODO: Sync change to correct offset spin box + + // TODO: This is likely the source of the visual masking bug while dragging (this happens after the move) maskNonVisibleConnectionTiles(); } -void Editor::onConnectionOffsetChanged(int newOffset) { - // Connections TODO: Change offset spin box for selected connection - /*ui->spinBox_ConnectionOffset->blockSignals(true); - ui->spinBox_ConnectionOffset->setValue(newOffset); - ui->spinBox_ConnectionOffset->blockSignals(false); - */ -} - -void Editor::setConnectionEditControlValues(MapConnection* connection) { - // Connections TODO: Highlight selected connection -} - void Editor::setConnectionsEditable(bool editable) { for (ConnectionPixmapItem* item : connection_items) { item->setEditable(editable); @@ -871,12 +892,8 @@ void Editor::onConnectionItemSelected(ConnectionPixmapItem* connectionItem) { selected_connection_item = connectionItem; for (ConnectionPixmapItem* item : connection_items) item->updateHighlight(item == selected_connection_item); - setConnectionEditControlValues(selected_connection_item->connection); - /* // Connections TODO: - ui->spinBox_ConnectionOffset->setMaximum(selected_connection_item->getMaxOffset()); - ui->spinBox_ConnectionOffset->setMinimum(selected_connection_item->getMinOffset()); - */ - onConnectionOffsetChanged(selected_connection_item->connection->offset); + + // TODO: Handle the highlight done in redrawConnection? } void Editor::setSelectedConnectionFromMap(QString mapName) { @@ -1692,50 +1709,6 @@ void Editor::displayMapGrid() { connect(ui->checkBox_ToggleGrid, &QCheckBox::toggled, this, &Editor::onToggleGridClicked); } -void Editor::updateConnectionOffset(int offset) { - if (!selected_connection_item) - return; - - selected_connection_item->blockSignals(true); - offset = qMin(offset, selected_connection_item->getMaxOffset()); - offset = qMax(offset, selected_connection_item->getMinOffset()); - selected_connection_item->connection->offset = offset; - if (selected_connection_item->connection->direction == "up" || selected_connection_item->connection->direction == "down") { - selected_connection_item->setX(selected_connection_item->initialX + (offset - selected_connection_item->initialOffset) * 16); - } else if (selected_connection_item->connection->direction == "left" || selected_connection_item->connection->direction == "right") { - selected_connection_item->setY(selected_connection_item->initialY + (offset - selected_connection_item->initialOffset) * 16); - } - selected_connection_item->blockSignals(false); - updateMirroredConnectionOffset(selected_connection_item->connection); - maskNonVisibleConnectionTiles(); -} - -void Editor::setConnectionMap(QString mapName) { - if (!mapName.isEmpty() && !project->mapNames.contains(mapName)) { - logError(QString("Invalid map name '%1' specified for connection.").arg(mapName)); - return; - } - if (!selected_connection_item) - return; - - if (mapName.isEmpty() || mapName == DYNAMIC_MAP_NAME) { - removeCurrentConnection(); - return; - } - - QString originalMapName = selected_connection_item->connection->map_name; - selected_connection_item->connection->map_name = mapName; - setCurrentConnectionDirection(selected_connection_item->connection->direction); - - // New map may have a different minimum offset than the last one. The maximum will be the same. - int min = selected_connection_item->getMinOffset(); - //ui->spinBox_ConnectionOffset->setMinimum(min); // Connections TODO: - onConnectionOffsetChanged(qMax(min, selected_connection_item->connection->offset)); - - updateMirroredConnectionMap(selected_connection_item->connection, originalMapName); - maskNonVisibleConnectionTiles(); -} - void Editor::addNewConnection() { // Find direction with least number of connections. QMap directionCounts = QMap({{"up", 0}, {"right", 0}, {"down", 0}, {"left", 0}}); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 20a48990..c5142799 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2537,39 +2537,12 @@ void MainWindow::showExportMapImageWindow(ImageExporterMode mode) { openSubWindow(this->mapImageExporter); } -// TODO: Move responsibility to list item -void MainWindow::on_comboBox_ConnectionDirection_currentTextChanged(const QString &direction) -{ - editor->updateCurrentConnectionDirection(direction); - markMapEdited(); -} -// TODO: Move responsibility to list item -void MainWindow::on_spinBox_ConnectionOffset_valueChanged(int offset) -{ - editor->updateConnectionOffset(offset); - markMapEdited(); -} -// TODO: Move responsibility to list item -void MainWindow::on_comboBox_ConnectedMap_currentTextChanged(const QString &mapName) -{ - if (mapName.isEmpty() || editor->project->mapNames.contains(mapName)) { - editor->setConnectionMap(mapName); - markMapEdited(); - } -} - void MainWindow::on_pushButton_AddConnection_clicked() { // TODO: Bring up a prompt for information. Mark the current map *AND* the connected map as edited editor->addNewConnection(); markMapEdited(); } -// TODO: Move responsibility to list item -void MainWindow::on_pushButton_RemoveConnection_clicked() -{ - editor->removeCurrentConnection(); - markMapEdited(); -} void MainWindow::on_pushButton_NewWildMonGroup_clicked() { editor->addNewWildMonGroup(this); @@ -2595,6 +2568,7 @@ void MainWindow::on_button_OpenEmergeMap_clicked() { userSetMap(mapName, true); } +// TODO: Mirror change to/from other maps void MainWindow::on_comboBox_DiveMap_currentTextChanged(const QString &mapName) { if (editor->project->isExistingMapName(mapName)) { editor->updateDiveMap(mapName); diff --git a/src/ui/connectionslistitem.cpp b/src/ui/connectionslistitem.cpp index 47589a12..810b9a56 100644 --- a/src/ui/connectionslistitem.cpp +++ b/src/ui/connectionslistitem.cpp @@ -19,6 +19,14 @@ ConnectionsListItem::ConnectionsListItem(QWidget *parent, const QStringList &map ui->comboBox_Map->setMinimumContentsLength(6); ui->comboBox_Map->addItems(mapNames); + + /* TODO: Spin box limits + ui->spinBox_ConnectionOffset->setMaximum(selected_connection_item->getMaxOffset()); + ui->spinBox_ConnectionOffset->setMinimum(selected_connection_item->getMinOffset()); + */ + + // TODO: + //connect(ui->button_Delete, &QAbstractButton::clicked, [this](bool) { this->d;}); } void ConnectionsListItem::populate(const MapConnection * connection) { @@ -35,3 +43,36 @@ ConnectionsListItem::~ConnectionsListItem() { delete ui; } + +// TODO +void ConnectionsListItem::on_comboBox_Direction_currentTextChanged(const QString &direction) +{ + /*editor->updateCurrentConnectionDirection(direction); + markMapEdited();*/ +} + +// TODO +void ConnectionsListItem::on_comboBox_Map_currentTextChanged(const QString &mapName) +{ + /*if (mapName.isEmpty() || editor->project->mapNames.contains(mapName)) { + editor->setConnectionMap(mapName); + markMapEdited(); + }*/ +} + +// TODO +void ConnectionsListItem::on_spinBox_Offset_valueChanged(int offset) +{ + /*editor->updateConnectionOffset(offset); + markMapEdited();*/ +} + +// TODO +void ConnectionsListItem::on_button_Delete_clicked() +{ + /* + editor->removeCurrentConnection(); + markMapEdited(); + */ +} + From 1f78fb9c4f6066380187bb8aad7b46ad3c4522e5 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Mon, 1 Jul 2024 14:10:55 -0400 Subject: [PATCH 12/76] Remove strict offset limitations --- include/ui/connectionpixmapitem.h | 2 -- src/editor.cpp | 8 -------- src/ui/connectionpixmapitem.cpp | 18 ------------------ src/ui/connectionslistitem.cpp | 6 ++---- 4 files changed, 2 insertions(+), 32 deletions(-) diff --git a/include/ui/connectionpixmapitem.h b/include/ui/connectionpixmapitem.h index c3d98cff..fd1042fd 100644 --- a/include/ui/connectionpixmapitem.h +++ b/include/ui/connectionpixmapitem.h @@ -27,8 +27,6 @@ public: int baseMapWidth; int baseMapHeight; void render(qreal opacity = 1); - int getMinOffset(); - int getMaxOffset(); void setEditable(bool editable); bool getEditable(); void updateHighlight(bool selected); diff --git a/src/editor.cpp b/src/editor.cpp index ec3e7f0b..bd217f81 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -822,12 +822,6 @@ void Editor::redrawConnection(ConnectionPixmapItem* connectionItem) { connectionItem->basePixmap = pixmap; - // TODO: Make sure offset limiting is correct (and updated) - // New map may have a different minimum offset than the last one. The maximum will be the same. - /*int min = selected_connection_item->getMinOffset(); - //ui->spinBox_ConnectionOffset->setMinimum(min); // Connections TODO: - onConnectionOffsetChanged(qMax(min, selected_connection_item->connection->offset));*/ - if (connectionItem == selected_connection_item) { QPainter painter(&pixmap); painter.setPen(QColor(255, 0, 255)); @@ -855,8 +849,6 @@ void Editor::updateConnectionOffset(int offset) { return; selected_connection_item->blockSignals(true); - offset = qMin(offset, selected_connection_item->getMaxOffset()); - offset = qMax(offset, selected_connection_item->getMinOffset()); selected_connection_item->connection->offset = offset; if (selected_connection_item->connection->direction == "up" || selected_connection_item->connection->direction == "down") { selected_connection_item->setX(selected_connection_item->initialX + (offset - selected_connection_item->initialOffset) * 16); diff --git a/src/ui/connectionpixmapitem.cpp b/src/ui/connectionpixmapitem.cpp index 256c556a..65907490 100644 --- a/src/ui/connectionpixmapitem.cpp +++ b/src/ui/connectionpixmapitem.cpp @@ -13,20 +13,6 @@ void ConnectionPixmapItem::render(qreal opacity) { this->setPixmap(newPixmap); } -int ConnectionPixmapItem::getMinOffset() { - if (this->connection->direction == "up" || this->connection->direction == "down") - return -(this->pixmap().width() / 16) - 6; - else - return -(this->pixmap().height() / 16) - 6; -} - -int ConnectionPixmapItem::getMaxOffset() { - if (this->connection->direction == "up" || this->connection->direction == "down") - return this->baseMapWidth + 6; - else - return this->baseMapHeight + 6; -} - QVariant ConnectionPixmapItem::itemChange(GraphicsItemChange change, const QVariant &value) { if (change == ItemPositionChange) { @@ -37,8 +23,6 @@ QVariant ConnectionPixmapItem::itemChange(GraphicsItemChange change, const QVari if (this->connection->direction == "up" || this->connection->direction == "down") { x = round(newPos.x() / 16) * 16; newOffset += (x - initialX) / 16; - newOffset = qMin(newOffset, this->getMaxOffset()); - newOffset = qMax(newOffset, this->getMinOffset()); x = newOffset * 16; } else { @@ -48,8 +32,6 @@ QVariant ConnectionPixmapItem::itemChange(GraphicsItemChange change, const QVari if (this->connection->direction == "right" || this->connection->direction == "left") { y = round(newPos.y() / 16) * 16; newOffset += (y - this->initialY) / 16; - newOffset = qMin(newOffset, this->getMaxOffset()); - newOffset = qMax(newOffset, this->getMinOffset()); y = newOffset * 16; } else { diff --git a/src/ui/connectionslistitem.cpp b/src/ui/connectionslistitem.cpp index 810b9a56..4980bff3 100644 --- a/src/ui/connectionslistitem.cpp +++ b/src/ui/connectionslistitem.cpp @@ -20,10 +20,8 @@ ConnectionsListItem::ConnectionsListItem(QWidget *parent, const QStringList &map ui->comboBox_Map->setMinimumContentsLength(6); ui->comboBox_Map->addItems(mapNames); - /* TODO: Spin box limits - ui->spinBox_ConnectionOffset->setMaximum(selected_connection_item->getMaxOffset()); - ui->spinBox_ConnectionOffset->setMinimum(selected_connection_item->getMinOffset()); - */ + ui->spinBox_Offset->setMinimum(INT_MIN); + ui->spinBox_Offset->setMaximum(INT_MAX); // TODO: //connect(ui->button_Delete, &QAbstractButton::clicked, [this](bool) { this->d;}); From 367198f98f1d2aa10f5c55e44b69cf4d470abdc8 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 2 Jul 2024 14:05:54 -0400 Subject: [PATCH 13/76] Sync changes between connection list and map --- forms/connectionslistitem.ui | 2 +- include/editor.h | 5 +- include/ui/connectionpixmapitem.h | 8 +- include/ui/connectionslistitem.h | 15 +++- src/editor.cpp | 142 +++++++++++++++--------------- src/ui/connectionslistitem.cpp | 55 +++++------- 6 files changed, 118 insertions(+), 109 deletions(-) diff --git a/forms/connectionslistitem.ui b/forms/connectionslistitem.ui index f35c509e..a55c8d90 100644 --- a/forms/connectionslistitem.ui +++ b/forms/connectionslistitem.ui @@ -57,7 +57,7 @@ - + ... diff --git a/include/editor.h b/include/editor.h index 37f60b6a..5b3ed90e 100644 --- a/include/editor.h +++ b/include/editor.h @@ -77,7 +77,7 @@ public: void setConnectionsVisibility(bool visible); void updateConnectionOffset(int offset); void addNewConnection(); - void removeCurrentConnection(); + void removeConnection(ConnectionPixmapItem* connectionItem); void addNewWildMonGroup(QWidget *window); void deleteWildMonGroup(); void updateDiveMap(QString mapName); @@ -168,10 +168,11 @@ private: void setConnectionItemsVisible(bool); void setBorderItemsVisible(bool, qreal = 1); void setConnectionsEditable(bool); + QPoint calculateConnectionPosition(const MapConnection *connection, const QPixmap &pixmap); void redrawConnection(ConnectionPixmapItem* connectionItem); void createConnectionItem(MapConnection* connection); void populateConnectionsList(); - void addConnectionToList(const MapConnection * connection); + void addConnectionToList(ConnectionPixmapItem* connection); void updateDiveEmergeMap(QString mapName, QString direction); void onConnectionOffsetChanged(int newOffset); void removeMirroredConnection(MapConnection*); diff --git a/include/ui/connectionpixmapitem.h b/include/ui/connectionpixmapitem.h index fd1042fd..c9b79226 100644 --- a/include/ui/connectionpixmapitem.h +++ b/include/ui/connectionpixmapitem.h @@ -8,9 +8,11 @@ class ConnectionPixmapItem : public QObject, public QGraphicsPixmapItem { Q_OBJECT public: - ConnectionPixmapItem(QPixmap pixmap, MapConnection* connection, int x, int y, int baseMapWidth, int baseMapHeight): QGraphicsPixmapItem(pixmap) { + ConnectionPixmapItem(QPixmap pixmap, MapConnection* connection, int x, int y, int baseMapWidth, int baseMapHeight) + : QGraphicsPixmapItem(pixmap), + connection(connection) + { this->basePixmap = pixmap; - this->connection = connection; setFlag(ItemIsMovable); setFlag(ItemSendsGeometryChanges); this->initialX = x; @@ -20,7 +22,7 @@ public: this->baseMapHeight = baseMapHeight; } QPixmap basePixmap; - MapConnection* connection; + MapConnection* const connection; int initialX; int initialY; int initialOffset; diff --git a/include/ui/connectionslistitem.h b/include/ui/connectionslistitem.h index a9dab7ca..ffa9ddca 100644 --- a/include/ui/connectionslistitem.h +++ b/include/ui/connectionslistitem.h @@ -9,18 +9,27 @@ namespace Ui { class ConnectionsListItem; } +// We show the data for each map connection in the panel on the right side of the Connections tab. +// An instance of this class is used for each item in that list. +// It communicates with the ConnectionPixmapItem on the map through a shared MapConnection pointer, +// and the two classes should signal one another when they change this data. class ConnectionsListItem : public QFrame { Q_OBJECT public: - explicit ConnectionsListItem(QWidget *parent, const QStringList &mapNames); + explicit ConnectionsListItem(QWidget *parent, MapConnection * connection, const QStringList &mapNames); ~ConnectionsListItem(); - void populate(const MapConnection * connection); + void updateUI(); -public: +private: Ui::ConnectionsListItem *ui; + MapConnection * const connection; + +signals: + void edited(); + void deleted(); private slots: void on_comboBox_Direction_currentTextChanged(const QString &direction); diff --git a/src/editor.cpp b/src/editor.cpp index bd217f81..39de3a8a 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -759,14 +759,49 @@ void Editor::populateConnectionsList() { w->deleteLater(); for (auto item :connection_items) - addConnectionToList(item->connection); + addConnectionToList(item); } // TODO: Don't allow splitter resizing to shrink list items to the point where horizontal scroll is needed -void Editor::addConnectionToList(const MapConnection * connection) { - ConnectionsListItem *listItem = new ConnectionsListItem(ui->scrollAreaContents_ConnectionsList, project->mapNames); - listItem->populate(connection); +void Editor::addConnectionToList(ConnectionPixmapItem * connectionItem) { + ConnectionsListItem *listItem = new ConnectionsListItem(ui->scrollAreaContents_ConnectionsList, connectionItem->connection, project->mapNames); ui->layout_ConnectionsList->insertWidget(ui->layout_ConnectionsList->count() - 1, listItem); // Insert above the vertical spacer + + connect(connectionItem, &ConnectionPixmapItem::connectionMoved, listItem, &ConnectionsListItem::updateUI); + + // TODO: This is probably slower than necessary (we don't need a full redraw if we're just moving it) + // TODO: Handle mirroring + connect(listItem, &ConnectionsListItem::edited, [this, connectionItem] { + redrawConnection(connectionItem); + emit editedMapData(); + }); + + connect(listItem, &ConnectionsListItem::deleted, [this, connectionItem] { + removeConnection(connectionItem); + }); +} + +// TODO: Connect to Delete key for selected connection +void Editor::removeConnection(ConnectionPixmapItem* connectionItem) { + if (!connectionItem) + return; + + map->connections.removeOne(connectionItem->connection); + connection_items.removeOne(connectionItem); + //removeMirroredConnection(connectionItem->connection); // TODO + + if (connectionItem->scene()) + connectionItem->scene()->removeItem(connectionItem); + + if (connectionItem == selected_connection_item) { + selected_connection_item = nullptr; + if (!connection_items.isEmpty()) + onConnectionItemSelected(connection_items.first()); + } + + delete connectionItem->connection; + delete connectionItem; + emit editedMapData(); } void Editor::setConnectionItemsVisible(bool visible) { @@ -783,6 +818,25 @@ void Editor::setBorderItemsVisible(bool visible, qreal opacity) { } } +QPoint Editor::calculateConnectionPosition(const MapConnection *connection, const QPixmap &pixmap) { + int x = 0, y = 0; + const int mWidth = 16, mHeight = 16; + if (connection->direction == "up") { + x = connection->offset * mWidth; + y = -pixmap.height(); + } else if (connection->direction == "down") { + x = connection->offset * mWidth; + y = map->getHeight() * mHeight; + } else if (connection->direction == "left") { + x = -pixmap.width(); + y = connection->offset * mHeight; + } else if (connection->direction == "right") { + x = map->getWidth() * mWidth; + y = connection->offset * mHeight; + } + return QPoint(x, y); +} + void Editor::redrawConnection(ConnectionPixmapItem* connectionItem) { if (!connectionItem || !connectionItem->connection) return; @@ -802,25 +856,9 @@ void Editor::redrawConnection(ConnectionPixmapItem* connectionItem) { return; QPixmap pixmap = connectedMap->renderConnection(*connectionItem->connection, map->layout); - - int offset = connectionItem->connection->offset; - connectionItem->initialOffset = offset; - int x = 0, y = 0; - if (connectionItem->connection->direction == "up") { - x = offset * 16; - y = -pixmap.height(); - } else if (connectionItem->connection->direction == "down") { - x = offset * 16; - y = map->getHeight() * 16; - } else if (connectionItem->connection->direction == "left") { - x = -pixmap.width(); - y = offset * 16; - } else if (connectionItem->connection->direction == "right") { - x = map->getWidth() * 16; - y = offset * 16; - } - + connectionItem->initialOffset = connectionItem->connection->offset; connectionItem->basePixmap = pixmap; + QPoint pos = calculateConnectionPosition(connectionItem->connection, pixmap); if (connectionItem == selected_connection_item) { QPainter painter(&pixmap); @@ -829,11 +867,11 @@ void Editor::redrawConnection(ConnectionPixmapItem* connectionItem) { painter.end(); } connectionItem->setPixmap(pixmap); - connectionItem->initialX = x; - connectionItem->initialY = y; + connectionItem->initialX = pos.x(); + connectionItem->initialY = pos.y(); connectionItem->blockSignals(true); - connectionItem->setX(x); - connectionItem->setY(y); + connectionItem->setX(pos.x()); + connectionItem->setY(pos.y()); connectionItem->setZValue(-1); connectionItem->blockSignals(false); @@ -843,7 +881,7 @@ void Editor::redrawConnection(ConnectionPixmapItem* connectionItem) { maskNonVisibleConnectionTiles(); } -// TODO: Generalize +// TODO: Generalize, maybe use in addConnectionToList connection instead of full render void Editor::updateConnectionOffset(int offset) { if (!selected_connection_item) return; @@ -863,11 +901,10 @@ void Editor::updateConnectionOffset(int offset) { void Editor::onConnectionMoved(MapConnection* connection) { // TODO: //updateMirroredConnectionOffset(connection); - - // TODO: Sync change to correct offset spin box // TODO: This is likely the source of the visual masking bug while dragging (this happens after the move) maskNonVisibleConnectionTiles(); + emit editedMapData(); } void Editor::setConnectionsEditable(bool editable) { @@ -1559,30 +1596,18 @@ void Editor::createConnectionItem(MapConnection* connection) { } QPixmap pixmap = connected_map->renderConnection(*connection, map->layout); - int offset = connection->offset; - int x = 0, y = 0; - if (connection->direction == "up") { - x = offset * 16; - y = -pixmap.height(); - } else if (connection->direction == "down") { - x = offset * 16; - y = map->getHeight() * 16; - } else if (connection->direction == "left") { - x = -pixmap.width(); - y = offset * 16; - } else if (connection->direction == "right") { - x = map->getWidth() * 16; - y = offset * 16; - } + QPoint pos = calculateConnectionPosition(connection, pixmap); - ConnectionPixmapItem *item = new ConnectionPixmapItem(pixmap, connection, x, y, map->getWidth(), map->getHeight()); - item->setX(x); - item->setY(y); + ConnectionPixmapItem *item = new ConnectionPixmapItem(pixmap, connection, pos.x(), pos.y(), map->getWidth(), map->getHeight()); + item->setX(pos.x()); + item->setY(pos.y()); item->setZValue(-1); scene->addItem(item); + connect(item, &ConnectionPixmapItem::connectionMoved, this, &Editor::onConnectionMoved); connect(item, &ConnectionPixmapItem::connectionItemSelected, this, &Editor::onConnectionItemSelected); connect(item, &ConnectionPixmapItem::connectionItemDoubleClicked, this, &Editor::onConnectionItemDoubleClicked); + connection_items.append(item); } @@ -1728,7 +1753,7 @@ void Editor::addNewConnection() { newConnection->map_name = defaultMapName; map->connections.append(newConnection); createConnectionItem(newConnection); - addConnectionToList(newConnection); + addConnectionToList(connection_items.last()); onConnectionItemSelected(connection_items.last()); updateMirroredConnection(newConnection, newConnection->direction, newConnection->map_name); @@ -1795,27 +1820,6 @@ void Editor::updateMirroredConnection(MapConnection* connection, QString origina mirrorConnection->offset = -connection->offset; } -void Editor::removeCurrentConnection() { - if (!selected_connection_item) - return; - - map->connections.removeOne(selected_connection_item->connection); - connection_items.removeOne(selected_connection_item); - removeMirroredConnection(selected_connection_item->connection); - - if (selected_connection_item && selected_connection_item->scene()) { - selected_connection_item->scene()->removeItem(selected_connection_item); - delete selected_connection_item; - } - - selected_connection_item = nullptr; - //ui->spinBox_ConnectionOffset->setValue(0); // Connections TODO: - - if (connection_items.length() > 0) { - onConnectionItemSelected(connection_items.last()); - } -} - void Editor::updateDiveMap(QString mapName) { updateDiveEmergeMap(mapName, "dive"); } diff --git a/src/ui/connectionslistitem.cpp b/src/ui/connectionslistitem.cpp index 4980bff3..246e83f4 100644 --- a/src/ui/connectionslistitem.cpp +++ b/src/ui/connectionslistitem.cpp @@ -3,9 +3,10 @@ static const QStringList directions = {"up", "down", "left", "right"}; -ConnectionsListItem::ConnectionsListItem(QWidget *parent, const QStringList &mapNames) : +ConnectionsListItem::ConnectionsListItem(QWidget *parent, MapConnection * connection, const QStringList &mapNames) : QFrame(parent), - ui(new Ui::ConnectionsListItem) + ui(new Ui::ConnectionsListItem), + connection(connection) { ui->setupUi(this); @@ -23,18 +24,7 @@ ConnectionsListItem::ConnectionsListItem(QWidget *parent, const QStringList &map ui->spinBox_Offset->setMinimum(INT_MIN); ui->spinBox_Offset->setMaximum(INT_MAX); - // TODO: - //connect(ui->button_Delete, &QAbstractButton::clicked, [this](bool) { this->d;}); -} - -void ConnectionsListItem::populate(const MapConnection * connection) { - const QSignalBlocker blocker1(ui->comboBox_Direction); - const QSignalBlocker blocker2(ui->comboBox_Map); - const QSignalBlocker blocker3(ui->spinBox_Offset); - - ui->comboBox_Direction->setTextItem(connection->direction); - ui->comboBox_Map->setTextItem(connection->map_name); - ui->spinBox_Offset->setValue(connection->offset); + this->updateUI(); } ConnectionsListItem::~ConnectionsListItem() @@ -42,35 +32,38 @@ ConnectionsListItem::~ConnectionsListItem() delete ui; } -// TODO +void ConnectionsListItem::updateUI() { + const QSignalBlocker blocker1(ui->comboBox_Direction); + const QSignalBlocker blocker2(ui->comboBox_Map); + const QSignalBlocker blocker3(ui->spinBox_Offset); + + ui->comboBox_Direction->setTextItem(this->connection->direction); + ui->comboBox_Map->setTextItem(this->connection->map_name); + ui->spinBox_Offset->setValue(this->connection->offset); +} + void ConnectionsListItem::on_comboBox_Direction_currentTextChanged(const QString &direction) { - /*editor->updateCurrentConnectionDirection(direction); - markMapEdited();*/ + this->connection->direction = direction; + emit this->edited(); } -// TODO void ConnectionsListItem::on_comboBox_Map_currentTextChanged(const QString &mapName) { - /*if (mapName.isEmpty() || editor->project->mapNames.contains(mapName)) { - editor->setConnectionMap(mapName); - markMapEdited(); - }*/ + if (ui->comboBox_Map->findText(mapName) >= 0) { + this->connection->map_name = mapName; + emit this->edited(); + } } -// TODO void ConnectionsListItem::on_spinBox_Offset_valueChanged(int offset) { - /*editor->updateConnectionOffset(offset); - markMapEdited();*/ + this->connection->offset = offset; + emit this->edited(); } -// TODO void ConnectionsListItem::on_button_Delete_clicked() { - /* - editor->removeCurrentConnection(); - markMapEdited(); - */ + this->deleteLater(); + emit this->deleted(); } - From 6b0034e52384cd3d1053d6fb5afeed5c41c01a5f Mon Sep 17 00:00:00 2001 From: GriffinR Date: Wed, 3 Jul 2024 11:34:27 -0400 Subject: [PATCH 14/76] Fix border visibility in API --- include/editor.h | 8 +--- include/mainwindow.h | 2 +- src/editor.cpp | 88 ++++++++++++++---------------------- src/mainwindow.cpp | 9 ++-- src/scriptapi/apiutility.cpp | 2 +- 5 files changed, 41 insertions(+), 68 deletions(-) diff --git a/include/editor.h b/include/editor.h index 5b3ed90e..4906a4e1 100644 --- a/include/editor.h +++ b/include/editor.h @@ -165,9 +165,7 @@ private: const QImage collisionPlaceholder = QImage(":/images/collisions_unknown.png"); QPixmap collisionSheetPixmap; - void setConnectionItemsVisible(bool); - void setBorderItemsVisible(bool, qreal = 1); - void setConnectionsEditable(bool); + void updateBorderVisibility(); QPoint calculateConnectionPosition(const MapConnection *connection, const QPixmap &pixmap); void redrawConnection(ConnectionPixmapItem* connectionItem); void createConnectionItem(MapConnection* connection); @@ -197,8 +195,6 @@ private slots: void mouseEvent_collision(QGraphicsSceneMouseEvent *event, CollisionPixmapItem *item); void onConnectionMoved(MapConnection*); void onConnectionItemSelected(ConnectionPixmapItem* connectionItem); - void onConnectionItemDoubleClicked(ConnectionPixmapItem* connectionItem); - void onConnectionDirectionChanged(QString newDirection); void onHoveredMovementPermissionChanged(uint16_t, uint16_t); void onHoveredMovementPermissionCleared(); void onHoveredMetatileSelectionChanged(uint16_t); @@ -213,7 +209,7 @@ private slots: signals: void objectsChanged(); - void loadMapRequested(QString, QString); + void connectionItemDoubleClicked(QString, QString); void wildMonDataChanged(); void warpEventDoubleClicked(QString, int, Event::Group); void currentMetatilesSelectionChanged(); diff --git a/include/mainwindow.h b/include/mainwindow.h index 51bfecc3..72067d5c 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -177,7 +177,7 @@ private slots: void copy(); void paste(); - void onLoadMapRequested(QString, QString); + void onConnectionItemDoubleClicked(QString, QString); void onMapChanged(Map *map); void onMapNeedsRedrawing(); void onTilesetsSaved(QString, QString); diff --git a/src/editor.cpp b/src/editor.cpp index 39de3a8a..d5a0dbd2 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -99,9 +99,7 @@ void Editor::setEditingMap() { if (events_group) { events_group->setVisible(false); } - setBorderItemsVisible(ui->checkBox_ToggleBorder->isChecked()); - setConnectionItemsVisible(ui->checkBox_ToggleBorder->isChecked()); - setConnectionsEditable(false); + updateBorderVisibility(); this->cursorMapTileRect->stopSingleTileMode(); this->cursorMapTileRect->setActive(true); @@ -123,9 +121,7 @@ void Editor::setEditingCollision() { if (events_group) { events_group->setVisible(false); } - setBorderItemsVisible(ui->checkBox_ToggleBorder->isChecked()); - setConnectionItemsVisible(ui->checkBox_ToggleBorder->isChecked()); - setConnectionsEditable(false); + updateBorderVisibility(); this->cursorMapTileRect->setSingleTileMode(); this->cursorMapTileRect->setActive(true); @@ -146,9 +142,7 @@ void Editor::setEditingObjects() { if (collision_item) { collision_item->setVisible(false); } - setBorderItemsVisible(ui->checkBox_ToggleBorder->isChecked()); - setConnectionItemsVisible(ui->checkBox_ToggleBorder->isChecked()); - setConnectionsEditable(false); + updateBorderVisibility(); this->cursorMapTileRect->setSingleTileMode(); this->cursorMapTileRect->setActive(false); updateWarpEventWarnings(); @@ -179,10 +173,6 @@ void Editor::setEditingConnections() { map_item->draw(); map_item->setVisible(true); populateConnectionsList(); - if (selected_connection_item) { - // TODO: Do we need additional handling here again - redrawConnection(selected_connection_item); - } maskNonVisibleConnectionTiles(); } if (collision_item) { @@ -191,9 +181,7 @@ void Editor::setEditingConnections() { if (events_group) { events_group->setVisible(false); } - setBorderItemsVisible(true, 0.4); - setConnectionItemsVisible(true); - setConnectionsEditable(true); + updateBorderVisibility(); this->cursorMapTileRect->setSingleTileMode(); this->cursorMapTileRect->setActive(false); } @@ -804,20 +792,6 @@ void Editor::removeConnection(ConnectionPixmapItem* connectionItem) { emit editedMapData(); } -void Editor::setConnectionItemsVisible(bool visible) { - for (ConnectionPixmapItem* item : connection_items) { - item->setVisible(visible); - item->setEnabled(visible); - } -} - -void Editor::setBorderItemsVisible(bool visible, qreal opacity) { - for (QGraphicsPixmapItem* item : borderItems) { - item->setVisible(visible); - item->setOpacity(opacity); - } -} - QPoint Editor::calculateConnectionPosition(const MapConnection *connection, const QPixmap &pixmap) { int x = 0, y = 0; const int mWidth = 16, mHeight = 16; @@ -883,7 +857,7 @@ void Editor::redrawConnection(ConnectionPixmapItem* connectionItem) { // TODO: Generalize, maybe use in addConnectionToList connection instead of full render void Editor::updateConnectionOffset(int offset) { - if (!selected_connection_item) + /*if (!selected_connection_item) return; selected_connection_item->blockSignals(true); @@ -895,7 +869,7 @@ void Editor::updateConnectionOffset(int offset) { } selected_connection_item->blockSignals(false); updateMirroredConnectionOffset(selected_connection_item->connection); - maskNonVisibleConnectionTiles(); + maskNonVisibleConnectionTiles();*/ } void Editor::onConnectionMoved(MapConnection* connection) { @@ -907,13 +881,6 @@ void Editor::onConnectionMoved(MapConnection* connection) { emit editedMapData(); } -void Editor::setConnectionsEditable(bool editable) { - for (ConnectionPixmapItem* item : connection_items) { - item->setEditable(editable); - item->updateHighlight(item == selected_connection_item); - } -} - void Editor::onConnectionItemSelected(ConnectionPixmapItem* connectionItem) { if (!connectionItem) return; @@ -925,6 +892,7 @@ void Editor::onConnectionItemSelected(ConnectionPixmapItem* connectionItem) { // TODO: Handle the highlight done in redrawConnection? } +// TODO: Inaccurate if there are multiple connections from the same map void Editor::setSelectedConnectionFromMap(QString mapName) { // Search for the first connection that connects to the given map map. for (ConnectionPixmapItem* item : connection_items) { @@ -935,21 +903,9 @@ void Editor::setSelectedConnectionFromMap(QString mapName) { } } -void Editor::onConnectionItemDoubleClicked(ConnectionPixmapItem* connectionItem) { - emit loadMapRequested(connectionItem->connection->map_name, map->name); -} - -void Editor::onConnectionDirectionChanged(QString newDirection) { - /* // Connections TODO: - ui->comboBox_ConnectionDirection->blockSignals(true); - ui->comboBox_ConnectionDirection->setCurrentText(newDirection); - ui->comboBox_ConnectionDirection->blockSignals(false); - */ -} - void Editor::onBorderMetatilesChanged() { displayMapBorder(); - setBorderItemsVisible(ui->checkBox_ToggleBorder->isChecked()); + updateBorderVisibility(); // TODO: Why do we need to call this here } void Editor::onHoveredMovementPermissionChanged(uint16_t collision, uint16_t elevation) { @@ -1606,7 +1562,9 @@ void Editor::createConnectionItem(MapConnection* connection) { connect(item, &ConnectionPixmapItem::connectionMoved, this, &Editor::onConnectionMoved); connect(item, &ConnectionPixmapItem::connectionItemSelected, this, &Editor::onConnectionItemSelected); - connect(item, &ConnectionPixmapItem::connectionItemDoubleClicked, this, &Editor::onConnectionItemDoubleClicked); + connect(item, &ConnectionPixmapItem::connectionItemDoubleClicked, [this, item] { + emit this->connectionItemDoubleClicked(item->connection->map_name, map->name); + }); connection_items.append(item); } @@ -1886,13 +1844,33 @@ void Editor::updateSecondaryTileset(QString tilesetLabel, bool forceLoad) void Editor::toggleBorderVisibility(bool visible, bool enableScriptCallback) { - this->setBorderItemsVisible(visible); - this->setConnectionItemsVisible(visible); porymapConfig.setShowBorder(visible); + updateBorderVisibility(); if (enableScriptCallback) Scripting::cb_BorderVisibilityToggled(visible); } +void Editor::updateBorderVisibility() { + // On the connections tab, the border is always visible, and the connections can be edited. + bool editingConnections = (ui->mainTabBar->currentIndex() == 3); + bool visible = (editingConnections || ui->checkBox_ToggleBorder->isChecked()); + + // Update border + const qreal borderOpacity = editingConnections ? 0.4 : 1; + for (QGraphicsPixmapItem* item : borderItems) { + item->setVisible(visible); + item->setOpacity(borderOpacity); + } + + // Update map connections + for (ConnectionPixmapItem* item : connection_items) { + item->setVisible(visible); + item->setEditable(editingConnections); + item->setEnabled(visible); + item->updateHighlight(item == selected_connection_item); + } +} + void Editor::updateCustomMapHeaderValues(QTableWidget *table) { map->customHeaders = CustomAttributesTable::getAttributes(table); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index c5142799..b90f9523 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -298,7 +298,7 @@ void MainWindow::checkForUpdates(bool) {} void MainWindow::initEditor() { this->editor = new Editor(ui); connect(this->editor, &Editor::objectsChanged, this, &MainWindow::updateObjects); - connect(this->editor, &Editor::loadMapRequested, this, &MainWindow::onLoadMapRequested); + connect(this->editor, &Editor::connectionItemDoubleClicked, this, &MainWindow::onConnectionItemDoubleClicked); connect(this->editor, &Editor::warpEventDoubleClicked, this, &MainWindow::openWarpMap); connect(this->editor, &Editor::currentMetatilesSelectionChanged, this, &MainWindow::currentMetatilesSelectionChanged); connect(this->editor, &Editor::wildMonDataChanged, this, &MainWindow::onWildMonDataChanged); @@ -2419,8 +2419,8 @@ void MainWindow::clickToolButtonFromEditMode(QString editMode) { } } -void MainWindow::onLoadMapRequested(QString mapName, QString fromMapName) { - if (userSetMap(mapName, true)) +void MainWindow::onConnectionItemDoubleClicked(QString mapName, QString fromMapName) { + if (mapName != fromMapName && userSetMap(mapName, true)) editor->setSelectedConnectionFromMap(fromMapName); } @@ -2706,8 +2706,7 @@ void MainWindow::on_checkBox_smartPaths_stateChanged(int selected) void MainWindow::on_checkBox_ToggleBorder_stateChanged(int selected) { - bool visible = selected != 0; - editor->toggleBorderVisibility(visible); + editor->toggleBorderVisibility(selected != 0); } void MainWindow::on_actionTileset_Editor_triggered() diff --git a/src/scriptapi/apiutility.cpp b/src/scriptapi/apiutility.cpp index 38ff3e39..1e44b1b7 100644 --- a/src/scriptapi/apiutility.cpp +++ b/src/scriptapi/apiutility.cpp @@ -168,7 +168,7 @@ bool ScriptUtility::getGridVisibility() { } void ScriptUtility::setBorderVisibility(bool visible) { - window->editor->toggleBorderVisibility(visible, false); + window->ui->checkBox_ToggleBorder->setChecked(visible); } bool ScriptUtility::getBorderVisibility() { From 0aa7ddf27d082047a80123376424aaddbeeb7ff8 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Wed, 3 Jul 2024 13:41:00 -0400 Subject: [PATCH 15/76] Add tab enums --- include/mainwindow.h | 19 +++++++++ src/editor.cpp | 2 +- src/mainwindow.cpp | 79 ++++++++++++++++++++---------------- src/scriptapi/apiutility.cpp | 5 ++- 4 files changed, 69 insertions(+), 36 deletions(-) diff --git a/include/mainwindow.h b/include/mainwindow.h index 72067d5c..121fe1cd 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -411,4 +411,23 @@ enum MapListUserRoles { TypeRole2, // Used for various extra data needed. }; +// These are namespaced in a struct to avoid colliding with e.g. class Map. +struct MainTab { + enum { + Map, + Events, + Header, + Connections, + WildPokemon, + }; +}; + +struct MapViewTab { + enum { + Metatiles, + Collision, + Prefabs, + }; +}; + #endif // MAINWINDOW_H diff --git a/src/editor.cpp b/src/editor.cpp index d5a0dbd2..09922372 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -1852,7 +1852,7 @@ void Editor::toggleBorderVisibility(bool visible, bool enableScriptCallback) void Editor::updateBorderVisibility() { // On the connections tab, the border is always visible, and the connections can be edited. - bool editingConnections = (ui->mainTabBar->currentIndex() == 3); + bool editingConnections = (ui->mainTabBar->currentIndex() == MainTab::Connections); bool visible = (editingConnections || ui->checkBox_ToggleBorder->isChecked()); // Update border diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index b90f9523..cfea8e85 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -208,16 +208,23 @@ void MainWindow::applyUserShortcuts() { shortcut->setKeys(shortcutsConfig.userShortcuts(shortcut)); } +static const QMap mainTabNames = { + {MainTab::Map, "Map"}, + {MainTab::Events, "Events"}, + {MainTab::Header, "Header"}, + {MainTab::Connections, "Connections"}, + {MainTab::WildPokemon, "Wild Pokemon"}, +}; + void MainWindow::initCustomUI() { // Set up the tab bar while (ui->mainTabBar->count()) ui->mainTabBar->removeTab(0); - ui->mainTabBar->addTab("Map"); - ui->mainTabBar->setTabIcon(0, QIcon(QStringLiteral(":/icons/map.ico"))); - ui->mainTabBar->addTab("Events"); - ui->mainTabBar->addTab("Header"); - ui->mainTabBar->addTab("Connections"); - ui->mainTabBar->addTab("Wild Pokemon"); - ui->mainTabBar->setTabIcon(4, QIcon(QStringLiteral(":/icons/tall_grass.ico"))); + + for (int i = 0; i < mainTabNames.count(); i++) + ui->mainTabBar->addTab(mainTabNames.value(i)); + + ui->mainTabBar->setTabIcon(MainTab::Map, QIcon(QStringLiteral(":/icons/map.ico"))); + ui->mainTabBar->setTabIcon(MainTab::WildPokemon, QIcon(QStringLiteral(":/icons/tall_grass.ico"))); } void MainWindow::initExtraSignals() { @@ -416,8 +423,7 @@ void MainWindow::markMapEdited() { void MainWindow::setProjectSpecificUI() { // Wild Encounters tab - // TODO: This index should come from an enum - ui->mainTabBar->setTabEnabled(4, editor->project->wildEncountersLoaded); + ui->mainTabBar->setTabEnabled(MainTab::WildPokemon, editor->project->wildEncountersLoaded); bool hasFlags = projectConfig.getMapAllowFlagsEnabled(); ui->checkBox_AllowRunning->setVisible(hasFlags); @@ -1569,7 +1575,7 @@ void MainWindow::copy() { { default: break; - case 0: + case MainTab::Map: { // copy the map image QPixmap pixmap = editor->map ? editor->map->render(true) : QPixmap(); @@ -1577,7 +1583,7 @@ void MainWindow::copy() { logInfo("Copied current map image to clipboard"); break; } - case 1: + case MainTab::Events: { if (!editor || !editor->project) break; @@ -1617,7 +1623,7 @@ void MainWindow::copy() { } } } - else if (this->ui->mainTabBar->currentIndex() == 4) { + else if (this->ui->mainTabBar->currentIndex() == MainTab::WildPokemon) { QWidget *w = this->ui->stackedWidget_WildMons->currentWidget(); if (w) { MonTabWidget *mtw = static_cast(w); @@ -1648,7 +1654,7 @@ void MainWindow::paste() { QClipboard *clipboard = QGuiApplication::clipboard(); QString clipboardText(clipboard->text()); - if (ui->mainTabBar->currentIndex() == 4) { + if (ui->mainTabBar->currentIndex() == MainTab::WildPokemon) { QWidget *w = this->ui->stackedWidget_WildMons->currentWidget(); if (w) { w->setFocus(); @@ -1676,7 +1682,7 @@ void MainWindow::paste() { { default: break; - case 0: + case MainTab::Map: { // can only paste currently selected metatiles on this tab if (pasteObject["object"].toString() != "metatile_selection") { @@ -1785,11 +1791,11 @@ void MainWindow::on_mapViewTab_tabBarClicked(int index) if (index != oldIndex) Scripting::cb_MapViewTabChanged(oldIndex, index); - if (index == 0) { + if (index == MapViewTab::Metatiles) { editor->setEditingMap(); - } else if (index == 1) { + } else if (index == MapViewTab::Collision) { editor->setEditingCollision(); - } else if (index == 2) { + } else if (index == MapViewTab::Prefabs) { editor->setEditingMap(); if (projectConfig.getPrefabFilepath().isEmpty() && !projectConfig.getPrefabImportPrompted()) { // User hasn't set up prefabs and hasn't been prompted before. @@ -1813,25 +1819,31 @@ void MainWindow::on_mainTabBar_tabBarClicked(int index) if (index != oldIndex) Scripting::cb_MainTabChanged(oldIndex, index); - int tabIndexToStackIndex[5] = {0, 0, 1, 2, 3}; - ui->mainStackedWidget->setCurrentIndex(tabIndexToStackIndex[index]); + static const QMap tabIndexToStackIndex = { + {MainTab::Map, 0}, + {MainTab::Events, 0}, + {MainTab::Header, 1}, + {MainTab::Connections, 2}, + {MainTab::WildPokemon, 3}, + }; + ui->mainStackedWidget->setCurrentIndex(tabIndexToStackIndex.value(index)); - if (index == 0) { + if (index == MainTab::Map) { ui->stackedWidget_MapEvents->setCurrentIndex(0); on_mapViewTab_tabBarClicked(ui->mapViewTab->currentIndex()); clickToolButtonFromEditMode(editor->map_edit_mode); - } else if (index == 1) { + } else if (index == MainTab::Events) { ui->stackedWidget_MapEvents->setCurrentIndex(1); editor->setEditingObjects(); clickToolButtonFromEditMode(editor->obj_edit_mode); - } else if (index == 3) { + } else if (index == MainTab::Connections) { editor->setEditingConnections(); } - if (index != 4) { + if (index != MainTab::WildPokemon) { if (editor->project && editor->project->wildEncountersLoaded) editor->saveEncounterTabData(); } - if (index != 1) { + if (index != MainTab::Events) { editor->map_ruler->setEnabled(false); } } @@ -2218,7 +2230,7 @@ void MainWindow::on_horizontalSlider_CollisionTransparency_valueChanged(int valu } void MainWindow::on_toolButton_deleteObject_clicked() { - if (ui->mainTabBar->currentIndex() != 1) { + if (ui->mainTabBar->currentIndex() != MainTab::Events) { // do not delete an event when not on event tab return; } @@ -2268,15 +2280,14 @@ void MainWindow::on_toolButton_deleteObject_clicked() { void MainWindow::on_toolButton_Paint_clicked() { - if (ui->mainTabBar->currentIndex() == 0) + if (ui->mainTabBar->currentIndex() == MainTab::Map) editor->map_edit_mode = "paint"; else editor->obj_edit_mode = "paint"; editor->settings->mapCursor = QCursor(QPixmap(":/icons/pencil_cursor.ico"), 10, 10); - // do not stop single tile mode when editing collision - if (ui->mapViewTab->currentIndex() != 1) + if (ui->mapViewTab->currentIndex() != MapViewTab::Collision) editor->cursorMapTileRect->stopSingleTileMode(); ui->graphicsView_Map->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); @@ -2290,7 +2301,7 @@ void MainWindow::on_toolButton_Paint_clicked() void MainWindow::on_toolButton_Select_clicked() { - if (ui->mainTabBar->currentIndex() == 0) + if (ui->mainTabBar->currentIndex() == MainTab::Map) editor->map_edit_mode = "select"; else editor->obj_edit_mode = "select"; @@ -2309,7 +2320,7 @@ void MainWindow::on_toolButton_Select_clicked() void MainWindow::on_toolButton_Fill_clicked() { - if (ui->mainTabBar->currentIndex() == 0) + if (ui->mainTabBar->currentIndex() == MainTab::Map) editor->map_edit_mode = "fill"; else editor->obj_edit_mode = "fill"; @@ -2328,7 +2339,7 @@ void MainWindow::on_toolButton_Fill_clicked() void MainWindow::on_toolButton_Dropper_clicked() { - if (ui->mainTabBar->currentIndex() == 0) + if (ui->mainTabBar->currentIndex() == MainTab::Map) editor->map_edit_mode = "pick"; else editor->obj_edit_mode = "pick"; @@ -2347,7 +2358,7 @@ void MainWindow::on_toolButton_Dropper_clicked() void MainWindow::on_toolButton_Move_clicked() { - if (ui->mainTabBar->currentIndex() == 0) + if (ui->mainTabBar->currentIndex() == MainTab::Map) editor->map_edit_mode = "move"; else editor->obj_edit_mode = "move"; @@ -2366,7 +2377,7 @@ void MainWindow::on_toolButton_Move_clicked() void MainWindow::on_toolButton_Shift_clicked() { - if (ui->mainTabBar->currentIndex() == 0) + if (ui->mainTabBar->currentIndex() == MainTab::Map) editor->map_edit_mode = "shift"; else editor->obj_edit_mode = "shift"; @@ -2385,7 +2396,7 @@ void MainWindow::on_toolButton_Shift_clicked() void MainWindow::checkToolButtons() { QString edit_mode; - if (ui->mainTabBar->currentIndex() == 0) { + if (ui->mainTabBar->currentIndex() == MainTab::Map) { edit_mode = editor->map_edit_mode; } else { edit_mode = editor->obj_edit_mode; diff --git a/src/scriptapi/apiutility.cpp b/src/scriptapi/apiutility.cpp index 1e44b1b7..61b6e978 100644 --- a/src/scriptapi/apiutility.cpp +++ b/src/scriptapi/apiutility.cpp @@ -154,7 +154,10 @@ int ScriptUtility::getMapViewTab() { } void ScriptUtility::setMapViewTab(int index) { - if (this->getMainTab() != 0 || !window->ui->mapViewTab || index < 0 || index >= window->ui->mapViewTab->count()) + if (this->getMainTab() != MainTab::Map || !window->ui->mapViewTab || index < 0 || index >= window->ui->mapViewTab->count()) + return; + // Can't select tab if it's disabled + if (!window->ui->mapViewTab->isTabEnabled(index)) return; window->on_mapViewTab_tabBarClicked(index); } From 7c6b33da1b33cf21934b3a5f2f4735ab8ee12ed8 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Wed, 3 Jul 2024 16:01:52 -0400 Subject: [PATCH 16/76] Add selection highlight to connections list --- forms/connectionslistitem.ui | 7 +++++-- forms/mainwindow.ui | 24 +++++++++++----------- include/project.h | 1 - include/ui/connectionpixmapitem.h | 4 ++++ include/ui/connectionslistitem.h | 7 +++++++ src/editor.cpp | 33 +++++++++++++++++++------------ src/mainwindow.cpp | 9 +++++---- src/project.cpp | 4 ---- src/ui/connectionpixmapitem.cpp | 27 +++++++++++++++++++------ src/ui/connectionslistitem.cpp | 18 +++++++++++++++++ 10 files changed, 92 insertions(+), 42 deletions(-) diff --git a/forms/connectionslistitem.ui b/forms/connectionslistitem.ui index a55c8d90..80e6beb5 100644 --- a/forms/connectionslistitem.ui +++ b/forms/connectionslistitem.ui @@ -6,8 +6,8 @@ 0 0 - 176 - 153 + 178 + 157 @@ -16,6 +16,9 @@ 0 + + QFrame::StyledPanel + diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index 75ceeb2e..c8bd5044 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -260,7 +260,7 @@ - 2 + 0 false @@ -769,8 +769,8 @@ 0 0 - 256 - 74 + 423 + 79 @@ -888,8 +888,8 @@ 0 0 - 256 - 74 + 423 + 79 @@ -990,10 +990,10 @@ - 0 + 8 0 - 91 - 74 + 411 + 446 @@ -1193,8 +1193,8 @@ 0 0 - 91 - 512 + 427 + 594 @@ -1413,8 +1413,8 @@ 0 0 - 93 - 460 + 382 + 699 diff --git a/include/project.h b/include/project.h index 49c0aaa8..643f74da 100644 --- a/include/project.h +++ b/include/project.h @@ -119,7 +119,6 @@ public: bool readMapGroups(); Map* addNewMapToGroup(QString, int, Map*, bool, bool); QString getNewMapName(); - bool isExistingMapName(const QString&); QString getProjectTitle(); QString readMapLayoutId(QString map_name); diff --git a/include/ui/connectionpixmapitem.h b/include/ui/connectionpixmapitem.h index c9b79226..41995003 100644 --- a/include/ui/connectionpixmapitem.h +++ b/include/ui/connectionpixmapitem.h @@ -33,6 +33,9 @@ public: bool getEditable(); void updateHighlight(bool selected); +private: + bool highlighted = false; + protected: QVariant itemChange(GraphicsItemChange change, const QVariant &value); void mousePressEvent(QGraphicsSceneMouseEvent*); @@ -42,6 +45,7 @@ signals: void connectionItemSelected(ConnectionPixmapItem* connectionItem); void connectionItemDoubleClicked(ConnectionPixmapItem* connectionItem); void connectionMoved(MapConnection*); + void highlightChanged(bool highlighted); }; #endif // CONNECTIONPIXMAPITEM_H diff --git a/include/ui/connectionslistitem.h b/include/ui/connectionslistitem.h index ffa9ddca..75f7ba5f 100644 --- a/include/ui/connectionslistitem.h +++ b/include/ui/connectionslistitem.h @@ -4,6 +4,7 @@ #include "mapconnection.h" #include +#include namespace Ui { class ConnectionsListItem; @@ -22,14 +23,20 @@ public: ~ConnectionsListItem(); void updateUI(); + void setSelected(bool selected); private: Ui::ConnectionsListItem *ui; MapConnection * const connection; + bool isSelected = false; + +protected: + void mousePressEvent(QMouseEvent *); signals: void edited(); void deleted(); + void selected(); private slots: void on_comboBox_Direction_currentTextChanged(const QString &direction); diff --git a/src/editor.cpp b/src/editor.cpp index 09922372..10249363 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -755,15 +755,29 @@ void Editor::addConnectionToList(ConnectionPixmapItem * connectionItem) { ConnectionsListItem *listItem = new ConnectionsListItem(ui->scrollAreaContents_ConnectionsList, connectionItem->connection, project->mapNames); ui->layout_ConnectionsList->insertWidget(ui->layout_ConnectionsList->count() - 1, listItem); // Insert above the vertical spacer + // Connect the pixmap item to the list item connect(connectionItem, &ConnectionPixmapItem::connectionMoved, listItem, &ConnectionsListItem::updateUI); + connect(connectionItem, &ConnectionPixmapItem::highlightChanged, listItem, &ConnectionsListItem::setSelected); + if (connectionItem == selected_connection_item) + listItem->setSelected(true); - // TODO: This is probably slower than necessary (we don't need a full redraw if we're just moving it) - // TODO: Handle mirroring + // Connect the list item to the pixmap item + connect(listItem, &ConnectionsListItem::selected, [this, connectionItem] { + if (connectionItem == selected_connection_item) { + // Already selected, no change + return; + } + // Deselect old connection and select new connection + if (selected_connection_item) selected_connection_item->updateHighlight(false); + selected_connection_item = connectionItem; + selected_connection_item->updateHighlight(true); + }); connect(listItem, &ConnectionsListItem::edited, [this, connectionItem] { + // TODO: This is probably slower than necessary (we don't need a full redraw if we're just moving it) + // TODO: Handle mirroring redrawConnection(connectionItem); emit editedMapData(); }); - connect(listItem, &ConnectionsListItem::deleted, [this, connectionItem] { removeConnection(connectionItem); }); @@ -834,16 +848,11 @@ void Editor::redrawConnection(ConnectionPixmapItem* connectionItem) { connectionItem->basePixmap = pixmap; QPoint pos = calculateConnectionPosition(connectionItem->connection, pixmap); - if (connectionItem == selected_connection_item) { - QPainter painter(&pixmap); - painter.setPen(QColor(255, 0, 255)); - painter.drawRect(0, 0, pixmap.width() - 1, pixmap.height() - 1); - painter.end(); - } + connectionItem->blockSignals(true); connectionItem->setPixmap(pixmap); + connectionItem->updateHighlight(connectionItem == selected_connection_item); connectionItem->initialX = pos.x(); connectionItem->initialY = pos.y(); - connectionItem->blockSignals(true); connectionItem->setX(pos.x()); connectionItem->setY(pos.y()); connectionItem->setZValue(-1); @@ -888,8 +897,6 @@ void Editor::onConnectionItemSelected(ConnectionPixmapItem* connectionItem) { selected_connection_item = connectionItem; for (ConnectionPixmapItem* item : connection_items) item->updateHighlight(item == selected_connection_item); - - // TODO: Handle the highlight done in redrawConnection? } // TODO: Inaccurate if there are multiple connections from the same map @@ -1851,7 +1858,7 @@ void Editor::toggleBorderVisibility(bool visible, bool enableScriptCallback) } void Editor::updateBorderVisibility() { - // On the connections tab, the border is always visible, and the connections can be edited. + // On the connections tab the border is always visible, and the connections can be edited. bool editingConnections = (ui->mainTabBar->currentIndex() == MainTab::Connections); bool visible = (editingConnections || ui->checkBox_ToggleBorder->isChecked()); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index cfea8e85..77df41fa 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2569,26 +2569,27 @@ void MainWindow::on_pushButton_ConfigureEncountersJSON_clicked() { void MainWindow::on_button_OpenDiveMap_clicked() { const QString mapName = ui->comboBox_DiveMap->currentText(); - if (editor->project->isExistingMapName(mapName)) + if (mapName != DYNAMIC_MAP_NAME && editor->project->mapNames.contains(mapName)) userSetMap(mapName, true); } void MainWindow::on_button_OpenEmergeMap_clicked() { const QString mapName = ui->comboBox_EmergeMap->currentText(); - if (editor->project->isExistingMapName(mapName)) + if (mapName != DYNAMIC_MAP_NAME && editor->project->mapNames.contains(mapName)) userSetMap(mapName, true); } // TODO: Mirror change to/from other maps void MainWindow::on_comboBox_DiveMap_currentTextChanged(const QString &mapName) { - if (editor->project->isExistingMapName(mapName)) { + // Include empty names as an update (user is deleting the connection) + if (mapName.isEmpty() || editor->project->mapNames.contains(mapName)) { editor->updateDiveMap(mapName); markMapEdited(); } } void MainWindow::on_comboBox_EmergeMap_currentTextChanged(const QString &mapName) { - if (editor->project->isExistingMapName(mapName)) { + if (mapName.isEmpty() || editor->project->mapNames.contains(mapName)) { editor->updateEmergeMap(mapName); markMapEdited(); } diff --git a/src/project.cpp b/src/project.cpp index c1c8f5c7..c6550c6a 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -1800,10 +1800,6 @@ QString Project::getNewMapName() { return newMapName; } -bool Project::isExistingMapName(const QString &mapName) { - return !mapName.isEmpty() && mapName != DYNAMIC_MAP_NAME && this->mapNames.contains(mapName); -} - Project::DataQualifiers Project::getDataQualifiers(QString text, QString label) { Project::DataQualifiers qualifiers; diff --git a/src/ui/connectionpixmapitem.cpp b/src/ui/connectionpixmapitem.cpp index 65907490..301fcfb6 100644 --- a/src/ui/connectionpixmapitem.cpp +++ b/src/ui/connectionpixmapitem.cpp @@ -56,13 +56,23 @@ bool ConnectionPixmapItem::getEditable() { return (this->flags() & ItemIsMovable) != 0; } +// TODO: Consider whether it still makes sense to highlight the "current" connection (given you can edit them at any point now) void ConnectionPixmapItem::updateHighlight(bool selected) { - bool editable = this->getEditable(); - int zValue = (selected || !editable) ? -1 : -2; - qreal opacity = (selected || !editable) ? 1 : 0.75; - this->setZValue(zValue); - this->render(opacity); - if (editable && selected) { + const int normalZ = -1; + const qreal normalOpacity = 1.0; + + // When editing is inactive the current selection is ignored, all connections should appear normal. + if (!this->getEditable()) { + this->setZValue(normalZ); + this->render(normalOpacity); + return; + } + + this->setZValue(selected ? normalZ : -2); + this->render(selected ? normalOpacity : 0.75); + + if (selected) { + // Draw highlight QPixmap pixmap = this->pixmap(); QPainter painter(&pixmap); painter.setPen(QColor(255, 0, 255)); @@ -70,6 +80,11 @@ void ConnectionPixmapItem::updateHighlight(bool selected) { painter.end(); this->setPixmap(pixmap); } + + // Let the list UI know if the selection highlight changes so it can update accordingly + if (this->highlighted != selected) + emit highlightChanged(selected); + this->highlighted = selected; } void ConnectionPixmapItem::mousePressEvent(QGraphicsSceneMouseEvent *) { diff --git a/src/ui/connectionslistitem.cpp b/src/ui/connectionslistitem.cpp index 246e83f4..096407b6 100644 --- a/src/ui/connectionslistitem.cpp +++ b/src/ui/connectionslistitem.cpp @@ -42,9 +42,25 @@ void ConnectionsListItem::updateUI() { ui->spinBox_Offset->setValue(this->connection->offset); } +// TODO: Frame shifts slightly when style changes +void ConnectionsListItem::setSelected(bool selected) { + if (selected == this->isSelected) + return; + this->isSelected = selected; + + this->setStyleSheet(selected ? ".ConnectionsListItem { border: 1px solid rgb(255, 0, 255); }" : ""); + if (selected) + emit this->selected(); +} + +void ConnectionsListItem::mousePressEvent(QMouseEvent *) { + this->setSelected(true); +} + void ConnectionsListItem::on_comboBox_Direction_currentTextChanged(const QString &direction) { this->connection->direction = direction; + this->setSelected(true); emit this->edited(); } @@ -52,6 +68,7 @@ void ConnectionsListItem::on_comboBox_Map_currentTextChanged(const QString &mapN { if (ui->comboBox_Map->findText(mapName) >= 0) { this->connection->map_name = mapName; + this->setSelected(true); emit this->edited(); } } @@ -59,6 +76,7 @@ void ConnectionsListItem::on_comboBox_Map_currentTextChanged(const QString &mapN void ConnectionsListItem::on_spinBox_Offset_valueChanged(int offset) { this->connection->offset = offset; + this->setSelected(true); emit this->edited(); } From f1cfc3c78ec43eae70f06892005e7b908588b32b Mon Sep 17 00:00:00 2001 From: GriffinR Date: Mon, 8 Jul 2024 11:44:23 -0400 Subject: [PATCH 17/76] Connections tab UI adjustments --- forms/connectionslistitem.ui | 26 ++++++++++-- forms/mainwindow.ui | 71 ++++++++++++++++++-------------- include/editor.h | 1 + include/mainwindow.h | 1 + include/ui/connectionslistitem.h | 4 +- src/editor.cpp | 20 +++++++-- src/mainwindow.cpp | 17 +++++--- src/ui/connectionslistitem.cpp | 7 +++- 8 files changed, 100 insertions(+), 47 deletions(-) diff --git a/forms/connectionslistitem.ui b/forms/connectionslistitem.ui index 80e6beb5..434c5a29 100644 --- a/forms/connectionslistitem.ui +++ b/forms/connectionslistitem.ui @@ -61,6 +61,9 @@ + + Remove this connection + ... @@ -71,13 +74,25 @@ - + + + Where the connected map should be positioned relative to the current map + + - + + + The name of the map to connect to the current map + + - + + + The number of spaces to move the connected map perpendicular to its connected direction + + @@ -87,6 +102,11 @@ QComboBox
noscrollcombobox.h
+ + NoScrollSpinBox + QSpinBox +
noscrollspinbox.h
+
diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index c8bd5044..cdb4dcb8 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -2588,6 +2588,16 @@ 4 + + + + <html><head/><body><p>Destination map name when using <span style=" font-weight:600;">Dive</span>. If empty, no such connection will exist.</p></body></html> + + + true + + + @@ -2601,6 +2611,20 @@ + + + + Open the selected Emerge Map + + + ... + + + + :/icons/map_go.ico:/icons/map_go.ico + + + @@ -2628,27 +2652,6 @@ - - - - Open the selected Emerge Map - - - ... - - - - :/icons/map_go.ico:/icons/map_go.ico - - - - - - - Emerge Map - - - @@ -2656,16 +2659,6 @@ - - - - <html><head/><body><p>Destination map name when using <span style=" font-weight:600;">Dive</span>. If empty, no such connection will exist.</p></body></html> - - - true - - - @@ -2676,6 +2669,13 @@ + + + + Emerge Map + + +
@@ -2717,6 +2717,12 @@ + + + 230 + 0 + + QFrame::StyledPanel @@ -2740,6 +2746,9 @@ QFrame::NoFrame + + Qt::ScrollBarAlwaysOff + true diff --git a/include/editor.h b/include/editor.h index 4906a4e1..f9bbe051 100644 --- a/include/editor.h +++ b/include/editor.h @@ -78,6 +78,7 @@ public: void updateConnectionOffset(int offset); void addNewConnection(); void removeConnection(ConnectionPixmapItem* connectionItem); + void removeSelectedConnection(); void addNewWildMonGroup(QWidget *window); void deleteWildMonGroup(); void updateDiveMap(QString mapName); diff --git a/include/mainwindow.h b/include/mainwindow.h index 121fe1cd..16eee651 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -217,6 +217,7 @@ private slots: void on_actionMove_triggered(); void on_actionMap_Shift_triggered(); + void onDeleteKeyPressed(); void on_toolButton_deleteObject_clicked(); void addNewEvent(Event::Type type); diff --git a/include/ui/connectionslistitem.h b/include/ui/connectionslistitem.h index 75f7ba5f..ce8a0163 100644 --- a/include/ui/connectionslistitem.h +++ b/include/ui/connectionslistitem.h @@ -32,11 +32,13 @@ private: protected: void mousePressEvent(QMouseEvent *); + void mouseDoubleClickEvent(QMouseEvent *); signals: void edited(); - void deleted(); + void deleteRequested(); void selected(); + void doubleClicked(); private slots: void on_comboBox_Direction_currentTextChanged(const QString &direction); diff --git a/src/editor.cpp b/src/editor.cpp index 10249363..98dca046 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -729,10 +729,12 @@ void Editor::populateConnectionsList() { ui->comboBox_DiveMap->clear(); ui->comboBox_DiveMap->addItems(project->mapNames); ui->comboBox_DiveMap->setCurrentText(""); + ui->comboBox_DiveMap->lineEdit()->setClearButtonEnabled(true); ui->comboBox_EmergeMap->clear(); ui->comboBox_EmergeMap->addItems(project->mapNames); ui->comboBox_EmergeMap->setCurrentText(""); + ui->comboBox_EmergeMap->lineEdit()->setClearButtonEnabled(true); for (MapConnection* connection : map->connections) { if (connection->direction == "dive") { @@ -750,7 +752,6 @@ void Editor::populateConnectionsList() { addConnectionToList(item); } -// TODO: Don't allow splitter resizing to shrink list items to the point where horizontal scroll is needed void Editor::addConnectionToList(ConnectionPixmapItem * connectionItem) { ConnectionsListItem *listItem = new ConnectionsListItem(ui->scrollAreaContents_ConnectionsList, connectionItem->connection, project->mapNames); ui->layout_ConnectionsList->insertWidget(ui->layout_ConnectionsList->count() - 1, listItem); // Insert above the vertical spacer @@ -758,11 +759,12 @@ void Editor::addConnectionToList(ConnectionPixmapItem * connectionItem) { // Connect the pixmap item to the list item connect(connectionItem, &ConnectionPixmapItem::connectionMoved, listItem, &ConnectionsListItem::updateUI); connect(connectionItem, &ConnectionPixmapItem::highlightChanged, listItem, &ConnectionsListItem::setSelected); + connect(connectionItem, &ConnectionPixmapItem::destroyed, listItem, &ConnectionsListItem::deleteLater); if (connectionItem == selected_connection_item) listItem->setSelected(true); - // Connect the list item to the pixmap item connect(listItem, &ConnectionsListItem::selected, [this, connectionItem] { + // When the list item is selected, select the pixmap too if (connectionItem == selected_connection_item) { // Already selected, no change return; @@ -773,17 +775,23 @@ void Editor::addConnectionToList(ConnectionPixmapItem * connectionItem) { selected_connection_item->updateHighlight(true); }); connect(listItem, &ConnectionsListItem::edited, [this, connectionItem] { + // When the list item is edited update the pixmap // TODO: This is probably slower than necessary (we don't need a full redraw if we're just moving it) // TODO: Handle mirroring redrawConnection(connectionItem); emit editedMapData(); }); - connect(listItem, &ConnectionsListItem::deleted, [this, connectionItem] { + connect(listItem, &ConnectionsListItem::deleteRequested, [this, connectionItem] { + // 'Remove' button has been clicked. Delete the pixmap. + // The list item will be deleted via the earlier connection to the pixmap's 'destroyed' signal. removeConnection(connectionItem); }); + connect(listItem, &ConnectionsListItem::doubleClicked, [this, connectionItem] { + // Double clicking the list item opens the connected map + emit connectionItemDoubleClicked(connectionItem->connection->map_name, map->name); + }); } -// TODO: Connect to Delete key for selected connection void Editor::removeConnection(ConnectionPixmapItem* connectionItem) { if (!connectionItem) return; @@ -806,6 +814,10 @@ void Editor::removeConnection(ConnectionPixmapItem* connectionItem) { emit editedMapData(); } +void Editor::removeSelectedConnection() { + removeConnection(selected_connection_item); +} + QPoint Editor::calculateConnectionPosition(const MapConnection *connection, const QPixmap &pixmap) { int x = 0, y = 0; const int mWidth = 16, mHeight = 16; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 77df41fa..7de1c54b 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -150,9 +150,9 @@ void MainWindow::initExtraShortcuts() { shortcutDuplicate_Events->setWhatsThis("Duplicate Selected Event(s)"); auto *shortcutDelete_Object = new Shortcut( - {QKeySequence("Del"), QKeySequence("Backspace")}, this, SLOT(on_toolButton_deleteObject_clicked())); + {QKeySequence("Del"), QKeySequence("Backspace")}, this, SLOT(onDeleteKeyPressed())); shortcutDelete_Object->setObjectName("shortcutDelete_Object"); - shortcutDelete_Object->setWhatsThis("Delete Selected Event(s)"); + shortcutDelete_Object->setWhatsThis("Delete Selected Item(s)"); auto *shortcutToggle_Border = new Shortcut(QKeySequence(), ui->checkBox_ToggleBorder, SLOT(toggle())); shortcutToggle_Border->setObjectName("shortcutToggle_Border"); @@ -2229,11 +2229,16 @@ void MainWindow::on_horizontalSlider_CollisionTransparency_valueChanged(int valu this->editor->collision_item->draw(true); } -void MainWindow::on_toolButton_deleteObject_clicked() { - if (ui->mainTabBar->currentIndex() != MainTab::Events) { - // do not delete an event when not on event tab - return; +void MainWindow::onDeleteKeyPressed() { + auto tab = ui->mainTabBar->currentIndex(); + if (tab == MainTab::Events) { + on_toolButton_deleteObject_clicked(); + } else if (tab == MainTab::Connections) { + if (editor) editor->removeSelectedConnection(); } +} + +void MainWindow::on_toolButton_deleteObject_clicked() { if (editor && editor->selected_events) { if (editor->selected_events->length()) { DraggablePixmapItem *nextSelectedEvent = nullptr; diff --git a/src/ui/connectionslistitem.cpp b/src/ui/connectionslistitem.cpp index 096407b6..b9f456a3 100644 --- a/src/ui/connectionslistitem.cpp +++ b/src/ui/connectionslistitem.cpp @@ -57,6 +57,10 @@ void ConnectionsListItem::mousePressEvent(QMouseEvent *) { this->setSelected(true); } +void ConnectionsListItem::mouseDoubleClickEvent(QMouseEvent *) { + emit doubleClicked(); +} + void ConnectionsListItem::on_comboBox_Direction_currentTextChanged(const QString &direction) { this->connection->direction = direction; @@ -82,6 +86,5 @@ void ConnectionsListItem::on_spinBox_Offset_valueChanged(int offset) void ConnectionsListItem::on_button_Delete_clicked() { - this->deleteLater(); - emit this->deleted(); + emit this->deleteRequested(); } From 1e09d08c9cdeaebad47914afc32477f946fe2a38 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Mon, 8 Jul 2024 16:01:30 -0400 Subject: [PATCH 18/76] Reimplement connection mirroring --- include/core/map.h | 1 - include/core/mapconnection.h | 15 +- include/editor.h | 24 +- include/mainwindow.h | 2 +- include/ui/connectionpixmapitem.h | 2 +- include/ui/connectionslistitem.h | 13 +- src/core/map.cpp | 4 +- src/core/mapconnection.cpp | 43 +++ src/editor.cpp | 490 ++++++++++++++++++------------ src/mainwindow.cpp | 34 +-- src/ui/connectionpixmapitem.cpp | 4 +- src/ui/connectionslistitem.cpp | 24 +- 12 files changed, 405 insertions(+), 251 deletions(-) create mode 100644 src/core/mapconnection.cpp diff --git a/include/core/map.h b/include/core/map.h index c214fbb7..a6f6fa1c 100644 --- a/include/core/map.h +++ b/include/core/map.h @@ -128,7 +128,6 @@ private: void setNewBorderDimensionsBlockdata(int newWidth, int newHeight); signals: - void mapChanged(Map *map); void modified(); void mapDimensionsChanged(const QSize &size); void mapNeedsRedrawing(); diff --git a/include/core/mapconnection.h b/include/core/mapconnection.h index 11aa0e59..474be5b1 100644 --- a/include/core/mapconnection.h +++ b/include/core/mapconnection.h @@ -5,6 +5,8 @@ #include #include +class Map; + class MapConnection { public: QString direction; @@ -12,12 +14,15 @@ public: QString map_name; }; -inline bool operator==(const MapConnection &c1, const MapConnection &c2) { - return c1.map_name == c2.map_name; -} +struct MapConnectionMirror { + MapConnection * connection = nullptr; + Map * map = nullptr; +}; -inline uint qHash(const MapConnection &key) { - return qHash(key.map_name); +inline bool operator==(const MapConnection &c1, const MapConnection &c2) { + return c1.direction == c2.direction && + c1.offset == c2.offset && + c1.map_name == c2.map_name; } #endif // MAPCONNECTION_H diff --git a/include/editor.h b/include/editor.h index f9bbe051..f0b54b69 100644 --- a/include/editor.h +++ b/include/editor.h @@ -75,9 +75,10 @@ public: void setEditingConnections(); void setMapEditingButtonsEnabled(bool enabled); void setConnectionsVisibility(bool visible); - void updateConnectionOffset(int offset); void addNewConnection(); - void removeConnection(ConnectionPixmapItem* connectionItem); + void addConnection(Map* map, MapConnection* connection); + void removeConnection(Map* map, MapConnection* connection); + void removeConnectionItem(ConnectionPixmapItem* connectionItem, bool removeMirror = true); void removeSelectedConnection(); void addNewWildMonGroup(QWidget *window); void deleteWildMonGroup(); @@ -168,17 +169,16 @@ private: void updateBorderVisibility(); QPoint calculateConnectionPosition(const MapConnection *connection, const QPixmap &pixmap); - void redrawConnection(ConnectionPixmapItem* connectionItem); + void updateConnectionItem(ConnectionPixmapItem* connectionItem); + void updateConnectionItemPos(ConnectionPixmapItem* connectionItem); void createConnectionItem(MapConnection* connection); void populateConnectionsList(); void addConnectionToList(ConnectionPixmapItem* connection); void updateDiveEmergeMap(QString mapName, QString direction); - void onConnectionOffsetChanged(int newOffset); - void removeMirroredConnection(MapConnection*); - void updateMirroredConnectionOffset(MapConnection*); - void updateMirroredConnectionDirection(MapConnection*, QString); - void updateMirroredConnectionMap(MapConnection*, QString); - void updateMirroredConnection(MapConnection*, QString, QString, bool isDelete = false); + bool shouldMirrorConnection(const MapConnection &source); + MapConnectionMirror getMirroredConnection(const MapConnection&); + void addMirroredConnection(const MapConnection&); + void removeMirroredConnection(const MapConnection&); void updateEncounterFields(EncounterFields newFields); QString getMovementPermissionText(uint16_t collision, uint16_t elevation); QString getMetatileDisplayMessage(uint16_t metatileId); @@ -194,7 +194,9 @@ private slots: void setStraightPathCursorMode(QGraphicsSceneMouseEvent *event); void mouseEvent_map(QGraphicsSceneMouseEvent *event, MapPixmapItem *item); void mouseEvent_collision(QGraphicsSceneMouseEvent *event, CollisionPixmapItem *item); - void onConnectionMoved(MapConnection*); + void setConnectionOffset(MapConnection *connection, int offset); + void setConnectionMap(MapConnection *connection, const QString &mapName); + void setConnectionDirection(MapConnection *connection, const QString &direction); void onConnectionItemSelected(ConnectionPixmapItem* connectionItem); void onHoveredMovementPermissionChanged(uint16_t, uint16_t); void onHoveredMovementPermissionCleared(); @@ -215,7 +217,7 @@ signals: void warpEventDoubleClicked(QString, int, Event::Group); void currentMetatilesSelectionChanged(); void mapRulerStatusChanged(const QString &); - void editedMapData(); + void editedMapData(Map*); void tilesetUpdated(QString); }; diff --git a/include/mainwindow.h b/include/mainwindow.h index 16eee651..14a5e9cd 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -178,7 +178,6 @@ private slots: void paste(); void onConnectionItemDoubleClicked(QString, QString); - void onMapChanged(Map *map); void onMapNeedsRedrawing(); void onTilesetsSaved(QString, QString); void onWildMonDataChanged(); @@ -366,6 +365,7 @@ private: void clickToolButtonFromEditMode(QString editMode); void markMapEdited(); + void markMapEdited(Map*); void showWindowTitle(); void initWindow(); diff --git a/include/ui/connectionpixmapitem.h b/include/ui/connectionpixmapitem.h index 41995003..f0e00ae1 100644 --- a/include/ui/connectionpixmapitem.h +++ b/include/ui/connectionpixmapitem.h @@ -44,7 +44,7 @@ protected: signals: void connectionItemSelected(ConnectionPixmapItem* connectionItem); void connectionItemDoubleClicked(ConnectionPixmapItem* connectionItem); - void connectionMoved(MapConnection*); + void connectionMoved(MapConnection *, int newOffset); void highlightChanged(bool highlighted); }; diff --git a/include/ui/connectionslistitem.h b/include/ui/connectionslistitem.h index ce8a0163..a2ce2c16 100644 --- a/include/ui/connectionslistitem.h +++ b/include/ui/connectionslistitem.h @@ -19,15 +19,16 @@ class ConnectionsListItem : public QFrame Q_OBJECT public: - explicit ConnectionsListItem(QWidget *parent, MapConnection * connection, const QStringList &mapNames); + explicit ConnectionsListItem(QWidget *parent, MapConnection *connection, const QStringList &mapNames); ~ConnectionsListItem(); void updateUI(); void setSelected(bool selected); + MapConnection * connection; + private: Ui::ConnectionsListItem *ui; - MapConnection * const connection; bool isSelected = false; protected: @@ -35,10 +36,12 @@ protected: void mouseDoubleClickEvent(QMouseEvent *); signals: - void edited(); - void deleteRequested(); + void editedOffset(MapConnection *connection, int newOffset); + void editedDirection(MapConnection *connection, const QString &direction); + void editedMapName(MapConnection *connection, const QString &mapName); + void removed(); void selected(); - void doubleClicked(); + void doubleClicked(const QString &mapName); private slots: void on_comboBox_Direction_currentTextChanged(const QString &direction); diff --git a/src/core/map.cpp b/src/core/map.cpp index a0ecf404..463b9341 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -304,8 +304,8 @@ void Map::setDimensions(int newWidth, int newHeight, bool setNewBlockdata, bool Scripting::cb_MapResized(oldWidth, oldHeight, newWidth, newHeight); } - emit mapChanged(this); emit mapDimensionsChanged(QSize(getWidth(), getHeight())); + modify(); } void Map::setBorderDimensions(int newWidth, int newHeight, bool setNewBlockdata, bool enableScriptCallback) { @@ -322,7 +322,7 @@ void Map::setBorderDimensions(int newWidth, int newHeight, bool setNewBlockdata, Scripting::cb_BorderResized(oldWidth, oldHeight, newWidth, newHeight); } - emit mapChanged(this); + modify(); } void Map::openScript(QString label) { diff --git a/src/core/mapconnection.cpp b/src/core/mapconnection.cpp new file mode 100644 index 00000000..ebf48592 --- /dev/null +++ b/src/core/mapconnection.cpp @@ -0,0 +1,43 @@ +#include "mapconnection.h" +#include "map.h" + +void MapConnection::setDirection(const QString & direction) { + if (direction == m_direction) + return; + auto before = m_direction; + m_direction = direction; + emit directionChanged(before, m_direction); +} + +void MapConnection::setOffset(int offset) { + if (offset == m_offset) + return; + auto before = m_offset; + m_offset = offset; + emit offsetChanged(before, m_offset); +} + +void MapConnection::setMapName(const QString &mapName) { + if (mapName == m_mapName) + return; + auto before = m_mapName; + m_mapName = mapName; + emit mapNameChanged(before, m_mapName); +} +/* +static QString MapConnection::oppositeDirection(const QString &direction) { + static const QMap oppositeDirections = { + {"up", "down"}, {"down", "up"}, + {"right", "left"}, {"left", "right"}, + {"dive", "emerge"}, {"emerge", "dive"} + }; + return oppositeDirections.value(direction); +} + +static MapConnection* MapConnection::getMirror(const MapConnection*, const Map*) { + +} + +static MapConnection* MapConnection::newMirror(const MapConnection*) { + +}*/ diff --git a/src/editor.cpp b/src/editor.cpp index 98dca046..b88ac4cf 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -726,14 +726,14 @@ void Editor::populateConnectionsList() { const QSignalBlocker blocker1(ui->comboBox_DiveMap); const QSignalBlocker blocker2(ui->comboBox_EmergeMap); + // TODO: We should probably just be doing this once, and updating the items when new maps are created. ui->comboBox_DiveMap->clear(); - ui->comboBox_DiveMap->addItems(project->mapNames); - ui->comboBox_DiveMap->setCurrentText(""); - ui->comboBox_DiveMap->lineEdit()->setClearButtonEnabled(true); - ui->comboBox_EmergeMap->clear(); + ui->comboBox_DiveMap->addItems(project->mapNames); ui->comboBox_EmergeMap->addItems(project->mapNames); + ui->comboBox_DiveMap->setCurrentText(""); ui->comboBox_EmergeMap->setCurrentText(""); + ui->comboBox_DiveMap->lineEdit()->setClearButtonEnabled(true); ui->comboBox_EmergeMap->lineEdit()->setClearButtonEnabled(true); for (MapConnection* connection : map->connections) { @@ -745,24 +745,22 @@ void Editor::populateConnectionsList() { } // Clear any existing connections in list - for (auto w : ui->scrollAreaContents_ConnectionsList->findChildren()) - w->deleteLater(); + for (auto listItem : ui->scrollAreaContents_ConnectionsList->findChildren()) + listItem->deleteLater(); for (auto item :connection_items) addConnectionToList(item); } +// TODO: When connecting a map to itself the new mirror is shaded incorrectly +// TODO: Set default focus to the selected connection, right now it defaults to the dive combo box + void Editor::addConnectionToList(ConnectionPixmapItem * connectionItem) { ConnectionsListItem *listItem = new ConnectionsListItem(ui->scrollAreaContents_ConnectionsList, connectionItem->connection, project->mapNames); ui->layout_ConnectionsList->insertWidget(ui->layout_ConnectionsList->count() - 1, listItem); // Insert above the vertical spacer - // Connect the pixmap item to the list item - connect(connectionItem, &ConnectionPixmapItem::connectionMoved, listItem, &ConnectionsListItem::updateUI); + // Sync the selection highlight between the list UI and the graphical map connection connect(connectionItem, &ConnectionPixmapItem::highlightChanged, listItem, &ConnectionsListItem::setSelected); - connect(connectionItem, &ConnectionPixmapItem::destroyed, listItem, &ConnectionsListItem::deleteLater); - if (connectionItem == selected_connection_item) - listItem->setSelected(true); - connect(listItem, &ConnectionsListItem::selected, [this, connectionItem] { // When the list item is selected, select the pixmap too if (connectionItem == selected_connection_item) { @@ -774,32 +772,113 @@ void Editor::addConnectionToList(ConnectionPixmapItem * connectionItem) { selected_connection_item = connectionItem; selected_connection_item->updateHighlight(true); }); - connect(listItem, &ConnectionsListItem::edited, [this, connectionItem] { - // When the list item is edited update the pixmap - // TODO: This is probably slower than necessary (we don't need a full redraw if we're just moving it) - // TODO: Handle mirroring - redrawConnection(connectionItem); - emit editedMapData(); + if (connectionItem == selected_connection_item) + listItem->setSelected(true); + + // Sync edits to 'offset' between the list UI and the graphical map connection + connect(connectionItem, &ConnectionPixmapItem::connectionMoved, [this, listItem](MapConnection* connection, int offset) { + setConnectionOffset(connection, offset); + listItem->updateUI(); }); - connect(listItem, &ConnectionsListItem::deleteRequested, [this, connectionItem] { + connect (listItem, &ConnectionsListItem::editedOffset, [this, connectionItem](MapConnection* connection, int offset) { + setConnectionOffset(connection, offset); + updateConnectionItemPos(connectionItem); + }); + + // Sync edits to 'direction' or 'map' between the list UI and the graphical map connection. + // These are 1-way because the direction and map cannot be edited graphically, only via the list UI. + connect(listItem, &ConnectionsListItem::editedDirection, [this, connectionItem](MapConnection* connection, QString direction) { + setConnectionDirection(connection, direction); + updateConnectionItem(connectionItem); // TODO: Simplify? + }); + connect(listItem, &ConnectionsListItem::editedMapName, [this, connectionItem](MapConnection* connection, QString mapName) { + setConnectionMap(connection, mapName); + updateConnectionItem(connectionItem); + }); + + // Sync deleting the map connection + connect(connectionItem, &ConnectionPixmapItem::destroyed, listItem, &ConnectionsListItem::deleteLater); + connect(listItem, &ConnectionsListItem::removed, [this, connectionItem] { // 'Remove' button has been clicked. Delete the pixmap. - // The list item will be deleted via the earlier connection to the pixmap's 'destroyed' signal. - removeConnection(connectionItem); + // The list item will be deleted via the above connection to the pixmap's 'destroyed' signal. + removeConnectionItem(connectionItem); }); - connect(listItem, &ConnectionsListItem::doubleClicked, [this, connectionItem] { - // Double clicking the list item opens the connected map - emit connectionItemDoubleClicked(connectionItem->connection->map_name, map->name); + + // Double clicking the list item opens the connected map + connect(listItem, &ConnectionsListItem::doubleClicked, [this](QString connectedMapName) { + emit connectionItemDoubleClicked(connectedMapName, map->name); }); } -void Editor::removeConnection(ConnectionPixmapItem* connectionItem) { + +void Editor::addNewConnection() { + // Find direction with least number of connections. + QMap directionCounts = QMap({{"up", 0}, {"right", 0}, {"down", 0}, {"left", 0}}); + for (MapConnection* connection : map->connections) { + directionCounts[connection->direction]++; + } + QString minDirection = "up"; + int minCount = INT_MAX; + for (QString direction : directionCounts.keys()) { + if (directionCounts[direction] < minCount) { + minDirection = direction; + minCount = directionCounts[direction]; + } + } + + // Prefer not to connect the map to itself (we have to if it's the only map). + // TODO: Is this more or less sensible than a connection with no map name + QString defaultMapName = project->mapNames.first(); + if (defaultMapName == map->name && project->mapNames.length() > 1) { + defaultMapName = project->mapNames.at(1); + } + + MapConnection* newConnection = new MapConnection; + newConnection->direction = minDirection; + newConnection->offset = 0; + newConnection->map_name = defaultMapName; + addConnection(map, newConnection); + onConnectionItemSelected(connection_items.last()); + addMirroredConnection(*newConnection); +} + +void Editor::addConnection(Map* map, MapConnection * connection) { + map->connections.append(connection); + if (map == this->map) { + // Adding a connection to the current map, we need to display it visually. + // Note that for the Dive/Emerge combo boxes this is normally redundant + // as the user can only create these by having already set the text, + // *except* in the case where they're connecting a map to itself. + if (connection->direction == "dive") { + const QSignalBlocker blocker(ui->comboBox_DiveMap); + ui->comboBox_DiveMap->setCurrentText(connection->map_name); + } else if (connection->direction == "emerge") { + const QSignalBlocker blocker(ui->comboBox_EmergeMap); + ui->comboBox_EmergeMap->setCurrentText(connection->map_name); + } else { + createConnectionItem(connection); + addConnectionToList(connection_items.last()); + } + } + emit editedMapData(map); +} + +void Editor::removeConnectionItem(ConnectionPixmapItem* connectionItem, bool removeMirror) { if (!connectionItem) return; - map->connections.removeOne(connectionItem->connection); - connection_items.removeOne(connectionItem); - //removeMirroredConnection(connectionItem->connection); // TODO + if (connectionItem->connection) { + if (removeMirror) { + // If a map is connected to itself and we delete the connection then this function + // will be called twice, once for the target of the delete and once for its mirror. + // We only want to try deleting the mirror once, on the first call (because the mirror + // of the mirror is the original target again, and we already deleted it). + removeMirroredConnection(*connectionItem->connection); + } + removeConnection(map, connectionItem->connection); + } + connection_items.removeOne(connectionItem); if (connectionItem->scene()) connectionItem->scene()->removeItem(connectionItem); @@ -808,14 +887,136 @@ void Editor::removeConnection(ConnectionPixmapItem* connectionItem) { if (!connection_items.isEmpty()) onConnectionItemSelected(connection_items.first()); } - - delete connectionItem->connection; delete connectionItem; - emit editedMapData(); +} + +void Editor::removeConnection(Map* map, MapConnection* connection) { + if (!map) + return; + map->connections.removeOne(connection); + delete connection; + emit editedMapData(map); } void Editor::removeSelectedConnection() { - removeConnection(selected_connection_item); + removeConnectionItem(selected_connection_item); +} + +static const QMap oppositeDirections = { + {"up", "down"}, {"down", "up"}, + {"right", "left"}, {"left", "right"}, + {"dive", "emerge"}, {"emerge", "dive"} +}; + +bool Editor::shouldMirrorConnection(const MapConnection &source) { + // The current map's connections are not mirrored if the user has disabled this setting, + // unless it's a connection to itself (which is mirrored by definition). + // TODO: Confirm in-game representation of the above fact is accurate. + return ui->checkBox_MirrorConnections->isChecked() || (map && map->name == source.map_name); +} + +MapConnectionMirror Editor::getMirroredConnection(const MapConnection &source) { + MapConnectionMirror mirror; + if (!map || !shouldMirrorConnection(source)) + return mirror; + + // Note: It's possible (and ok) for mirror.map == this->map + mirror.map = project->getMap(source.map_name); + if (!mirror.map) + return mirror; + + MapConnection target; + target.direction = oppositeDirections.value(source.direction); + target.map_name = map->name; + target.offset = -source.offset; + + // Find the matching connection in the connected map. + for (auto connection : mirror.map->connections) { + if (*connection == target) { + mirror.connection = connection; + break; + } + } + return mirror; +} + +void Editor::addMirroredConnection(const MapConnection &source) { + MapConnectionMirror mirror = getMirroredConnection(source); + if (!mirror.map) + return; + + mirror.connection = new MapConnection; + mirror.connection->direction = oppositeDirections.value(source.direction); + mirror.connection->map_name = map->name; + mirror.connection->offset = -source.offset; + + addConnection(mirror.map, mirror.connection); +} + +void Editor::removeMirroredConnection(const MapConnection &source) { + MapConnectionMirror mirror = getMirroredConnection(source); + if (!mirror.map || !mirror.connection) + return; + + if (map == mirror.map) { + // The connection to delete is displayed on the currently-opened map, we need to delete it visually as well. + if (source.direction == "dive") { + const QSignalBlocker blocker(ui->comboBox_DiveMap); + ui->comboBox_DiveMap->setCurrentText(""); + } else if (source.direction == "emerge") { + const QSignalBlocker blocker(ui->comboBox_EmergeMap); + ui->comboBox_EmergeMap->setCurrentText(""); + } else { + // Find and delete the matching connection graphics + for (auto item :connection_items) { + if (item->connection == mirror.connection) { + removeConnectionItem(item, false); + return; + } + } + } + } + removeConnection(mirror.map, mirror.connection); +} + +void Editor::updateDiveMap(QString mapName) { + updateDiveEmergeMap(mapName, "dive"); +} + +void Editor::updateEmergeMap(QString mapName) { + updateDiveEmergeMap(mapName, "emerge"); +} + +void Editor::updateDiveEmergeMap(QString mapName, QString direction) { + if (!mapName.isEmpty() && !project->mapNamesToMapConstants.contains(mapName)) { + logError(QString("Invalid %1 connection map name: '%2'").arg(direction).arg(mapName)); + return; + } + + // TODO: How does the game handle having multiple Dive/Emerge maps. Should we support this? + MapConnection* connection = nullptr; + for (MapConnection* conn : map->connections) { + if (conn->direction == direction) { + connection = conn; + break; + } + } + + // We (lazily) always update the connection by deleting the old one and creating a new one. + // Unlike the displayed connections which can be updated rapidly by click+drag, this isn't + // really an issue for Dive/Emerge maps. + if (connection) { + removeMirroredConnection(*connection); + removeConnection(map, connection); + } + if (!mapName.isEmpty() && mapName != DYNAMIC_MAP_NAME) { + connection = new MapConnection; + connection->direction = direction; + connection->offset = 0; + connection->map_name = mapName; + addConnection(map, connection); + addMirroredConnection(*connection); + } } QPoint Editor::calculateConnectionPosition(const MapConnection *connection, const QPixmap &pixmap) { @@ -837,7 +1038,7 @@ QPoint Editor::calculateConnectionPosition(const MapConnection *connection, cons return QPoint(x, y); } -void Editor::redrawConnection(ConnectionPixmapItem* connectionItem) { +void Editor::updateConnectionItem(ConnectionPixmapItem* connectionItem) { if (!connectionItem || !connectionItem->connection) return; @@ -870,36 +1071,80 @@ void Editor::redrawConnection(ConnectionPixmapItem* connectionItem) { connectionItem->setZValue(-1); connectionItem->blockSignals(false); - // TODO: - //updateMirroredConnectionMap(selected_connection_item->connection, originalMapName); - maskNonVisibleConnectionTiles(); } -// TODO: Generalize, maybe use in addConnectionToList connection instead of full render -void Editor::updateConnectionOffset(int offset) { - /*if (!selected_connection_item) +void Editor::updateConnectionItemPos(ConnectionPixmapItem* connectionItem) { + if (!connectionItem || !connectionItem->connection) return; - selected_connection_item->blockSignals(true); - selected_connection_item->connection->offset = offset; - if (selected_connection_item->connection->direction == "up" || selected_connection_item->connection->direction == "down") { - selected_connection_item->setX(selected_connection_item->initialX + (offset - selected_connection_item->initialOffset) * 16); - } else if (selected_connection_item->connection->direction == "left" || selected_connection_item->connection->direction == "right") { - selected_connection_item->setY(selected_connection_item->initialY + (offset - selected_connection_item->initialOffset) * 16); + MapConnection *connection = connectionItem->connection; + connectionItem->blockSignals(true); + if (connection->direction == "up" || connection->direction == "down") { + connectionItem->setX(connectionItem->initialX + (connection->offset - connectionItem->initialOffset) * 16); + } else if (connection->direction == "left" || connection->direction == "right") { + connectionItem->setY(connectionItem->initialY + (connection->offset - connectionItem->initialOffset) * 16); } - selected_connection_item->blockSignals(false); - updateMirroredConnectionOffset(selected_connection_item->connection); - maskNonVisibleConnectionTiles();*/ + connectionItem->blockSignals(false); + maskNonVisibleConnectionTiles(); } -void Editor::onConnectionMoved(MapConnection* connection) { - // TODO: - //updateMirroredConnectionOffset(connection); +void Editor::setConnectionOffset(MapConnection *connection, int offset) { + if (!connection || !map || connection->offset == offset) + return; + + MapConnectionMirror mirror = getMirroredConnection(*connection); + if (mirror.connection && mirror.map) { + mirror.connection->offset = -offset; + if (mirror.map != map) { + emit editedMapData(mirror.map); + } else { + // The mirror is displayed on the current map, update its graphics + for (auto item :connection_items) { + if (item->connection == mirror.connection) { + updateConnectionItemPos(item); + break; + } + } + // TODO: We should be signaling to the list item from the pixmap item, rather than searching for it. + for (auto listItem : ui->scrollAreaContents_ConnectionsList->findChildren()) { + if (listItem->connection == mirror.connection){ + listItem->updateUI(); + break; + } + } + } + } + // TODO: If there's no mirror but there should be we're not creating one + + connection->offset = offset; + emit editedMapData(map); // TODO: This is likely the source of the visual masking bug while dragging (this happens after the move) maskNonVisibleConnectionTiles(); - emit editedMapData(); +} + +void Editor::setConnectionMap(MapConnection *connection, const QString &mapName) { + if (!connection || !map || connection->map_name == mapName) + return; + + removeMirroredConnection(*connection); + connection->map_name = mapName; + addMirroredConnection(*connection); + + emit editedMapData(map); +} + +void Editor::setConnectionDirection(MapConnection *connection, const QString &direction) { + if (!connection || !map || connection->direction == direction) + return; + + // TODO: Lazy + removeMirroredConnection(*connection); + connection->direction = direction; + addMirroredConnection(*connection); + + emit editedMapData(map); } void Editor::onConnectionItemSelected(ConnectionPixmapItem* connectionItem) { @@ -1579,7 +1824,6 @@ void Editor::createConnectionItem(MapConnection* connection) { item->setZValue(-1); scene->addItem(item); - connect(item, &ConnectionPixmapItem::connectionMoved, this, &Editor::onConnectionMoved); connect(item, &ConnectionPixmapItem::connectionItemSelected, this, &Editor::onConnectionItemSelected); connect(item, &ConnectionPixmapItem::connectionItemDoubleClicked, [this, item] { emit this->connectionItemDoubleClicked(item->connection->map_name, map->name); @@ -1703,144 +1947,6 @@ void Editor::displayMapGrid() { connect(ui->checkBox_ToggleGrid, &QCheckBox::toggled, this, &Editor::onToggleGridClicked); } -void Editor::addNewConnection() { - // Find direction with least number of connections. - QMap directionCounts = QMap({{"up", 0}, {"right", 0}, {"down", 0}, {"left", 0}}); - for (MapConnection* connection : map->connections) { - directionCounts[connection->direction]++; - } - QString minDirection = "up"; - int minCount = INT_MAX; - for (QString direction : directionCounts.keys()) { - if (directionCounts[direction] < minCount) { - minDirection = direction; - minCount = directionCounts[direction]; - } - } - - // Don't connect the map to itself. - QString defaultMapName = project->mapNames.first(); - if (defaultMapName == map->name) { - defaultMapName = project->mapNames.value(1); - } - - MapConnection* newConnection = new MapConnection; - newConnection->direction = minDirection; - newConnection->offset = 0; - newConnection->map_name = defaultMapName; - map->connections.append(newConnection); - createConnectionItem(newConnection); - addConnectionToList(connection_items.last()); - onConnectionItemSelected(connection_items.last()); - - updateMirroredConnection(newConnection, newConnection->direction, newConnection->map_name); -} - -void Editor::updateMirroredConnectionOffset(MapConnection* connection) { - updateMirroredConnection(connection, connection->direction, connection->map_name); -} -void Editor::updateMirroredConnectionDirection(MapConnection* connection, QString originalDirection) { - updateMirroredConnection(connection, originalDirection, connection->map_name); -} -void Editor::updateMirroredConnectionMap(MapConnection* connection, QString originalMapName) { - updateMirroredConnection(connection, connection->direction, originalMapName); -} -void Editor::removeMirroredConnection(MapConnection* connection) { - updateMirroredConnection(connection, connection->direction, connection->map_name, true); -} -void Editor::updateMirroredConnection(MapConnection* connection, QString originalDirection, QString originalMapName, bool isDelete) { - if (!ui->checkBox_MirrorConnections->isChecked()) - return; - Map* otherMap = project->getMap(originalMapName); - if (!otherMap) - return; - - static QMap oppositeDirections = QMap({ - {"up", "down"}, {"right", "left"}, - {"down", "up"}, {"left", "right"}, - {"dive", "emerge"},{"emerge", "dive"}}); - QString oppositeDirection = oppositeDirections.value(originalDirection); - - // Find the matching connection in the connected map. - MapConnection* mirrorConnection = nullptr; - for (MapConnection* conn : otherMap->connections) { - if (conn->direction == oppositeDirection && conn->map_name == map->name) { - mirrorConnection = conn; - } - } - - if (isDelete) { - if (mirrorConnection) { - otherMap->connections.removeOne(mirrorConnection); - delete mirrorConnection; - } - return; - } - - if (connection->direction != originalDirection || connection->map_name != originalMapName) { - if (mirrorConnection) { - otherMap->connections.removeOne(mirrorConnection); - delete mirrorConnection; - mirrorConnection = nullptr; - otherMap = project->getMap(connection->map_name); - } - } - - // Create a new mirrored connection, if a matching one doesn't already exist. - if (!mirrorConnection) { - mirrorConnection = new MapConnection; - mirrorConnection->direction = oppositeDirections.value(connection->direction); - mirrorConnection->map_name = map->name; - otherMap->connections.append(mirrorConnection); - } - - mirrorConnection->offset = -connection->offset; -} - -void Editor::updateDiveMap(QString mapName) { - updateDiveEmergeMap(mapName, "dive"); -} - -void Editor::updateEmergeMap(QString mapName) { - updateDiveEmergeMap(mapName, "emerge"); -} - -void Editor::updateDiveEmergeMap(QString mapName, QString direction) { - if (!mapName.isEmpty() && !project->mapNamesToMapConstants.contains(mapName)) { - logError(QString("Invalid %1 connection map name: '%2'").arg(direction).arg(mapName)); - return; - } - - MapConnection* connection = nullptr; - for (MapConnection* conn : map->connections) { - if (conn->direction == direction) { - connection = conn; - break; - } - } - - if (mapName.isEmpty() || mapName == DYNAMIC_MAP_NAME) { - // Remove dive/emerge connection - if (connection) { - map->connections.removeOne(connection); - removeMirroredConnection(connection); - } - } else { - if (!connection) { - connection = new MapConnection; - connection->direction = direction; - connection->offset = 0; - connection->map_name = mapName; - map->connections.append(connection); - updateMirroredConnection(connection, connection->direction, connection->map_name); - } else { - QString originalMapName = connection->map_name; - connection->map_name = mapName; - updateMirroredConnectionMap(connection, originalMapName); - } - } -} - void Editor::updatePrimaryTileset(QString tilesetLabel, bool forceLoad) { if (map->layout->tileset_primary_label != tilesetLabel || forceLoad) @@ -1893,7 +1999,7 @@ void Editor::updateBorderVisibility() { void Editor::updateCustomMapHeaderValues(QTableWidget *table) { map->customHeaders = CustomAttributesTable::getAttributes(table); - emit editedMapData(); + emit editedMapData(map); } Tileset* Editor::getCurrentMapPrimaryTileset() diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 7de1c54b..38af29d2 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -310,7 +310,7 @@ void MainWindow::initEditor() { connect(this->editor, &Editor::currentMetatilesSelectionChanged, this, &MainWindow::currentMetatilesSelectionChanged); connect(this->editor, &Editor::wildMonDataChanged, this, &MainWindow::onWildMonDataChanged); connect(this->editor, &Editor::mapRulerStatusChanged, this, &MainWindow::onMapRulerStatusChanged); - connect(this->editor, &Editor::editedMapData, this, &MainWindow::markMapEdited); + connect(this->editor, &Editor::editedMapData, [this](Map* map) { this->markMapEdited(map); }); connect(this->editor, &Editor::tilesetUpdated, this, &Scripting::cb_TilesetUpdated); connect(ui->toolButton_Open_Scripts, &QToolButton::pressed, this->editor, &Editor::openMapScripts); connect(ui->actionOpen_Project_in_Text_Editor, &QAction::triggered, this->editor, &Editor::openProjectInTextEditor); @@ -413,10 +413,19 @@ void MainWindow::showWindowTitle() { } void MainWindow::markMapEdited() { - if (editor && editor->map) { - editor->map->hasUnsavedDataChanges = true; + if (editor) markMapEdited(editor->map); +} + +void MainWindow::markMapEdited(Map* map) { + if (!map) + return; + map->hasUnsavedDataChanges = true; + + // TODO: Only update the necessary list icon + updateMapList(); + + if (editor && editor->map == map) showWindowTitle(); - } } // Update the UI using information we've read from the user's project files. @@ -769,7 +778,6 @@ bool MainWindow::setMap(QString map_name, bool scrollTreeView) { showWindowTitle(); - connect(editor->map, &Map::mapChanged, this, &MainWindow::onMapChanged); connect(editor->map, &Map::mapNeedsRedrawing, this, &MainWindow::onMapNeedsRedrawing); connect(editor->map, &Map::modified, [this](){ this->markMapEdited(); }); @@ -2440,10 +2448,6 @@ void MainWindow::onConnectionItemDoubleClicked(QString mapName, QString fromMapN editor->setSelectedConnectionFromMap(fromMapName); } -void MainWindow::onMapChanged(Map *) { - updateMapList(); -} - void MainWindow::onMapNeedsRedrawing() { redrawMapScene(); } @@ -2555,9 +2559,8 @@ void MainWindow::showExportMapImageWindow(ImageExporterMode mode) { void MainWindow::on_pushButton_AddConnection_clicked() { - // TODO: Bring up a prompt for information. Mark the current map *AND* the connected map as edited + // TODO: Bring up a prompt for information? editor->addNewConnection(); - markMapEdited(); } void MainWindow::on_pushButton_NewWildMonGroup_clicked() { @@ -2584,20 +2587,15 @@ void MainWindow::on_button_OpenEmergeMap_clicked() { userSetMap(mapName, true); } -// TODO: Mirror change to/from other maps void MainWindow::on_comboBox_DiveMap_currentTextChanged(const QString &mapName) { // Include empty names as an update (user is deleting the connection) - if (mapName.isEmpty() || editor->project->mapNames.contains(mapName)) { + if (mapName.isEmpty() || editor->project->mapNames.contains(mapName)) editor->updateDiveMap(mapName); - markMapEdited(); - } } void MainWindow::on_comboBox_EmergeMap_currentTextChanged(const QString &mapName) { - if (mapName.isEmpty() || editor->project->mapNames.contains(mapName)) { + if (mapName.isEmpty() || editor->project->mapNames.contains(mapName)) editor->updateEmergeMap(mapName); - markMapEdited(); - } } void MainWindow::on_comboBox_PrimaryTileset_currentTextChanged(const QString &tilesetLabel) diff --git a/src/ui/connectionpixmapitem.cpp b/src/ui/connectionpixmapitem.cpp index 301fcfb6..060f084f 100644 --- a/src/ui/connectionpixmapitem.cpp +++ b/src/ui/connectionpixmapitem.cpp @@ -38,8 +38,8 @@ QVariant ConnectionPixmapItem::itemChange(GraphicsItemChange change, const QVari y = this->initialY; } - this->connection->offset = newOffset; - emit connectionMoved(this->connection); + if (this->connection->offset != newOffset) + emit connectionMoved(this->connection, newOffset); return QPointF(x, y); } else { diff --git a/src/ui/connectionslistitem.cpp b/src/ui/connectionslistitem.cpp index b9f456a3..f40ae57f 100644 --- a/src/ui/connectionslistitem.cpp +++ b/src/ui/connectionslistitem.cpp @@ -5,8 +5,7 @@ static const QStringList directions = {"up", "down", "left", "right"}; ConnectionsListItem::ConnectionsListItem(QWidget *parent, MapConnection * connection, const QStringList &mapNames) : QFrame(parent), - ui(new Ui::ConnectionsListItem), - connection(connection) + ui(new Ui::ConnectionsListItem) { ui->setupUi(this); @@ -24,6 +23,7 @@ ConnectionsListItem::ConnectionsListItem(QWidget *parent, MapConnection * connec ui->spinBox_Offset->setMinimum(INT_MIN); ui->spinBox_Offset->setMaximum(INT_MAX); + this->connection = connection; this->updateUI(); } @@ -58,33 +58,31 @@ void ConnectionsListItem::mousePressEvent(QMouseEvent *) { } void ConnectionsListItem::mouseDoubleClickEvent(QMouseEvent *) { - emit doubleClicked(); + emit doubleClicked(this->connection->map_name); } void ConnectionsListItem::on_comboBox_Direction_currentTextChanged(const QString &direction) { - this->connection->direction = direction; this->setSelected(true); - emit this->edited(); + if (this->connection->direction != direction) + emit this->editedDirection(this->connection, direction); } void ConnectionsListItem::on_comboBox_Map_currentTextChanged(const QString &mapName) { - if (ui->comboBox_Map->findText(mapName) >= 0) { - this->connection->map_name = mapName; - this->setSelected(true); - emit this->edited(); - } + this->setSelected(true); + if (ui->comboBox_Map->findText(mapName) >= 0 && this->connection->map_name != mapName) + emit this->editedMapName(this->connection, mapName); } void ConnectionsListItem::on_spinBox_Offset_valueChanged(int offset) { - this->connection->offset = offset; this->setSelected(true); - emit this->edited(); + if (this->connection->offset != offset) + emit editedOffset(this->connection, offset); } void ConnectionsListItem::on_button_Delete_clicked() { - emit this->deleteRequested(); + emit this->removed(); } From b60e54c07c3c34eea6f911882720c05869f3e8e2 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Thu, 8 Feb 2024 16:00:55 -0500 Subject: [PATCH 19/76] Close subwindows gracefully, prompt save on quit --- include/editor.h | 2 +- include/mainwindow.h | 7 +- src/editor.cpp | 9 ++- src/mainwindow.cpp | 150 +++++++++++++++++++++--------------- src/ui/mapimageexporter.cpp | 1 + src/ui/newmappopup.cpp | 1 + src/ui/regionmapeditor.cpp | 1 + src/ui/tileseteditor.cpp | 1 + 8 files changed, 100 insertions(+), 72 deletions(-) diff --git a/include/editor.h b/include/editor.h index f684b491..837b267c 100644 --- a/include/editor.h +++ b/include/editor.h @@ -43,7 +43,7 @@ public: public: Ui::MainWindow* ui; QObject *parent = nullptr; - Project *project = nullptr; + QPointer project = nullptr; Map *map = nullptr; Settings *settings; void saveProject(); diff --git a/include/mainwindow.h b/include/mainwindow.h index 6f969eba..5dc6b124 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -295,7 +295,7 @@ private slots: public: Ui::MainWindow *ui; - Editor *editor = nullptr; + QPointer editor = nullptr; private: QLabel *label_MapRulerStatus = nullptr; @@ -331,7 +331,6 @@ private: bool isProgrammaticEventTabChange; bool projectHasUnsavedChanges; - bool projectOpenFailure = false; bool newMapDefaultsSet = false; MapSortOrder mapSortOrder; @@ -348,7 +347,9 @@ private: void openSubWindow(QWidget * window); QString getExistingDirectory(QString); bool openProject(const QString &dir, bool initial = false); + bool closeProject(); void showProjectOpenFailure(); + void saveGlobalConfigs(); bool setInitialMap(); void setRecentMap(QString map_name); QStandardItem* createMapItem(QString mapName, int groupNum, int inGroupNum); @@ -379,7 +380,7 @@ private: void setTheme(QString); void updateTilesetEditor(); Event::Group getEventGroupFromTabWidget(QWidget *tab); - void closeSupplementaryWindows(); + bool closeSupplementaryWindows(); void setWindowDisabled(bool); void initTilesetEditor(); diff --git a/src/editor.cpp b/src/editor.cpp index 014fa489..a2e4f3f9 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -79,10 +79,11 @@ void Editor::saveUiFields() { } void Editor::closeProject() { - if (this->project) { - delete this->project; - this->project = nullptr; - } + if (!this->project) + return; + + Scripting::cb_ProjectClosed(this->project->root); + delete this->project; } void Editor::setEditingMap() { diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index f7e0dee1..beddb7ec 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -71,6 +71,7 @@ MainWindow::MainWindow(QWidget *parent) : MainWindow::~MainWindow() { delete label_MapRulerStatus; + delete editor; delete ui; } @@ -477,8 +478,12 @@ void MainWindow::setTheme(QString theme) { } bool MainWindow::openProject(const QString &dir, bool initial) { + if (!this->closeProject()) { + logInfo("Aborted project open."); + return false; + } + if (dir.isNull() || dir.length() <= 0) { - projectOpenFailure = true; if (!initial) setWindowDisabled(true); return false; } @@ -486,7 +491,6 @@ bool MainWindow::openProject(const QString &dir, bool initial) { const QString projectString = QString("%1project '%2'").arg(initial ? "recent " : "").arg(QDir::toNativeSeparators(dir)); if (!QDir(dir).exists()) { - projectOpenFailure = true; const QString errorMsg = QString("Failed to open %1: No such directory").arg(projectString); this->statusBar()->showMessage(errorMsg); if (initial) { @@ -503,43 +507,30 @@ bool MainWindow::openProject(const QString &dir, bool initial) { this->statusBar()->showMessage(openMessage); logInfo(openMessage); + // TODO: Don't save these yet userConfig.setProjectDir(dir); userConfig.load(); projectConfig.setProjectDir(dir); projectConfig.load(); - this->closeSupplementaryWindows(); this->newMapDefaultsSet = false; - if (isProjectOpen()) - Scripting::cb_ProjectClosed(editor->project->root); Scripting::init(this); - bool already_open = isProjectOpen() && (editor->project->root == dir); - if (!already_open) { - editor->closeProject(); - editor->project = new Project(this); - QObject::connect(editor->project, &Project::reloadProject, this, &MainWindow::on_action_Reload_Project_triggered); - QObject::connect(editor->project, &Project::mapCacheCleared, this, &MainWindow::onMapCacheCleared); - QObject::connect(editor->project, &Project::uncheckMonitorFilesAction, [this]() { - porymapConfig.setMonitorFiles(false); - if (this->preferenceEditor) - this->preferenceEditor->updateFields(); - }); - editor->project->set_root(dir); - } else { - editor->project->fileWatcher.removePaths(editor->project->fileWatcher.files()); - editor->project->clearMapCache(); - editor->project->clearTilesetCache(); - } + this->editor->project = new Project(this); + QObject::connect(this->editor->project, &Project::reloadProject, this, &MainWindow::on_action_Reload_Project_triggered); + QObject::connect(this->editor->project, &Project::mapCacheCleared, this, &MainWindow::onMapCacheCleared); + QObject::connect(this->editor->project, &Project::uncheckMonitorFilesAction, [this]() { + porymapConfig.setMonitorFiles(false); + if (this->preferenceEditor) + this->preferenceEditor->updateFields(); + }); + this->editor->project->set_root(dir); - this->projectOpenFailure = !(loadDataStructures() - && populateMapList() - && setInitialMap()); - - if (this->projectOpenFailure) { + if (!(loadDataStructures() && populateMapList() && setInitialMap())) { this->statusBar()->showMessage(QString("Failed to open %1").arg(projectString)); showProjectOpenFailure(); + delete this->editor->project; return false; } @@ -568,7 +559,7 @@ void MainWindow::showProjectOpenFailure() { } bool MainWindow::isProjectOpen() { - return !projectOpenFailure && editor && editor->project; + return editor && editor->project; } bool MainWindow::setInitialMap() { @@ -1228,12 +1219,10 @@ void MainWindow::openNewMapPopupWindow() { } if (!this->newMapPrompt) { this->newMapPrompt = new NewMapPopup(this, this->editor->project); + connect(this->newMapPrompt, &NewMapPopup::applied, this, &MainWindow::onNewMapCreated); } openSubWindow(this->newMapPrompt); - - connect(this->newMapPrompt, &NewMapPopup::applied, this, &MainWindow::onNewMapCreated); - this->newMapPrompt->setAttribute(Qt::WA_DeleteOnClose); } void MainWindow::on_action_NewMap_triggered() { @@ -1751,11 +1740,6 @@ void MainWindow::on_mapViewTab_tabBarClicked(int index) editor->setCursorRectVisible(false); } -void MainWindow::on_action_Exit_triggered() -{ - QApplication::quit(); -} - void MainWindow::on_mainTabBar_tabBarClicked(int index) { int oldIndex = ui->mainTabBar->currentIndex(); @@ -2486,11 +2470,8 @@ void MainWindow::importMapFromAdvanceMap1_92() void MainWindow::showExportMapImageWindow(ImageExporterMode mode) { if (!editor->project) return; - if (this->mapImageExporter) - delete this->mapImageExporter; - - this->mapImageExporter = new MapImageExporter(this, this->editor, mode); - this->mapImageExporter->setAttribute(Qt::WA_DeleteOnClose); + if (!this->mapImageExporter) + this->mapImageExporter = new MapImageExporter(this, this->editor, mode); openSubWindow(this->mapImageExporter); } @@ -2923,36 +2904,59 @@ bool MainWindow::initRegionMapEditor(bool silent) { return true; } -void MainWindow::closeSupplementaryWindows() { - delete this->tilesetEditor; - delete this->regionMapEditor; - delete this->mapImageExporter; - delete this->newMapPrompt; - delete this->shortcutsEditor; - delete this->customScriptsEditor; +// Attempt to close any open sub-windows of the main window, giving each a chance to abort the process. +// Each of these are expected to be a QPointer to a widget with WA_DeleteOnClose set, so manually deleting +// and nullifying the pointer members is not necessary here. +// TODO: Testing +bool MainWindow::closeSupplementaryWindows() { + if (this->tilesetEditor && !this->tilesetEditor->close()) + return false; + if (this->regionMapEditor && !this->regionMapEditor->close()) + return false; + if (this->mapImageExporter && !this->mapImageExporter->close()) + return false; + if (this->newMapPrompt && !this->newMapPrompt->close()) + return false; + if (this->shortcutsEditor && !this->shortcutsEditor->close()) + return false; + if (this->preferenceEditor && !this->preferenceEditor->close()) + return false; + if (this->customScriptsEditor && !this->customScriptsEditor->close()) + return false; if (this->projectSettingsEditor) this->projectSettingsEditor->closeQuietly(); + + return true; } -void MainWindow::closeEvent(QCloseEvent *event) { - if (isProjectOpen()) { - if (projectHasUnsavedChanges || (editor->map && editor->map->hasUnsavedChanges())) { - QMessageBox::StandardButton result = QMessageBox::question( - this, "porymap", "The project has been modified, save changes?", - QMessageBox::No | QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Yes); +bool MainWindow::closeProject() { + if (!closeSupplementaryWindows()) + return false; - if (result == QMessageBox::Yes) { - editor->saveProject(); - } else if (result == QMessageBox::No) { - logWarn("Closing porymap with unsaved changes."); - } else if (result == QMessageBox::Cancel) { - event->ignore(); - return; - } + if (!isProjectOpen()) + return true; + + if (projectHasUnsavedChanges || (editor->map && editor->map->hasUnsavedChanges())) { + QMessageBox::StandardButton result = QMessageBox::question( + this, "porymap", "The project has been modified, save changes?", + QMessageBox::No | QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Yes); + + if (result == QMessageBox::Yes) { + editor->saveProject(); + } else if (result == QMessageBox::No) { + logWarn("Closing project with unsaved changes."); + } else if (result == QMessageBox::Cancel) { + return false; } - projectConfig.save(); - userConfig.save(); } + projectConfig.save(); + userConfig.save(); + editor->closeProject(); + + return true; +} + +void MainWindow::saveGlobalConfigs() { porymapConfig.setMainGeometry( this->saveGeometry(), this->saveState(), @@ -2962,6 +2966,24 @@ void MainWindow::closeEvent(QCloseEvent *event) { ); porymapConfig.save(); shortcutsConfig.save(); +} + +void MainWindow::on_action_Exit_triggered() { + if (!closeProject()) + return; + + saveGlobalConfigs(); + + QApplication::quit(); +} + +void MainWindow::closeEvent(QCloseEvent *event) { + if (!closeProject()) { + event->ignore(); + return; + } + + saveGlobalConfigs(); QMainWindow::closeEvent(event); } diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index 099f2288..e36d9606 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -27,6 +27,7 @@ MapImageExporter::MapImageExporter(QWidget *parent_, Editor *editor_, ImageExpor QDialog(parent_), ui(new Ui::MapImageExporter) { + this->setAttribute(Qt::WA_DeleteOnClose); ui->setupUi(this); this->map = editor_->map; this->editor = editor_; diff --git a/src/ui/newmappopup.cpp b/src/ui/newmappopup.cpp index 2c4141bc..fdc3f79c 100644 --- a/src/ui/newmappopup.cpp +++ b/src/ui/newmappopup.cpp @@ -14,6 +14,7 @@ NewMapPopup::NewMapPopup(QWidget *parent, Project *project) : QMainWindow(parent), ui(new Ui::NewMapPopup) { + this->setAttribute(Qt::WA_DeleteOnClose); ui->setupUi(this); this->project = project; this->existingLayout = false; diff --git a/src/ui/regionmapeditor.cpp b/src/ui/regionmapeditor.cpp index 220f9cca..318bc517 100644 --- a/src/ui/regionmapeditor.cpp +++ b/src/ui/regionmapeditor.cpp @@ -26,6 +26,7 @@ RegionMapEditor::RegionMapEditor(QWidget *parent, Project *project) : QMainWindow(parent), ui(new Ui::RegionMapEditor) { + this->setAttribute(Qt::WA_DeleteOnClose); this->ui->setupUi(this); this->project = project; this->initShortcuts(); diff --git a/src/ui/tileseteditor.cpp b/src/ui/tileseteditor.cpp index 4813790a..bd233974 100644 --- a/src/ui/tileseteditor.cpp +++ b/src/ui/tileseteditor.cpp @@ -20,6 +20,7 @@ TilesetEditor::TilesetEditor(Project *project, Map *map, QWidget *parent) : map(map), hasUnsavedChanges(false) { + this->setAttribute(Qt::WA_DeleteOnClose); this->setTilesets(this->map->layout->tileset_primary_label, this->map->layout->tileset_secondary_label); this->initUi(); } From db598a43db682ca84015f9e89f4f39456dd4dc09 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Mon, 15 Jul 2024 16:14:38 -0400 Subject: [PATCH 20/76] Write config files on close / save, not on modification --- include/config.h | 277 ++++-------- include/mainwindow.h | 1 - include/project.h | 1 + src/config.cpp | 750 +++---------------------------- src/core/block.cpp | 6 +- src/core/events.cpp | 26 +- src/core/heallocation.cpp | 2 +- src/core/map.cpp | 4 +- src/core/metatile.cpp | 10 +- src/core/metatileparser.cpp | 2 +- src/core/network.cpp | 16 +- src/core/tileset.cpp | 10 +- src/editor.cpp | 22 +- src/mainwindow.cpp | 111 +++-- src/project.cpp | 128 +++--- src/scriptapi/apimap.cpp | 6 +- src/ui/citymappixmapitem.cpp | 4 +- src/ui/customscriptseditor.cpp | 4 +- src/ui/eventframes.cpp | 16 +- src/ui/imageproviders.cpp | 3 +- src/ui/newmappopup.cpp | 12 +- src/ui/newtilesetdialog.cpp | 4 +- src/ui/paletteeditor.cpp | 4 +- src/ui/prefab.cpp | 21 +- src/ui/preferenceeditor.cpp | 28 +- src/ui/projectsettingseditor.cpp | 166 ++++--- src/ui/regionmapeditor.cpp | 4 +- src/ui/shortcutseditor.cpp | 1 + src/ui/tileseteditor.cpp | 30 +- src/ui/updatepromoter.cpp | 9 +- 30 files changed, 458 insertions(+), 1220 deletions(-) diff --git a/include/config.h b/include/config.h index 54398cca..89a27d24 100644 --- a/include/config.h +++ b/include/config.h @@ -33,20 +33,17 @@ class KeyValueConfigBase public: void save(); void load(); - void setSaveDisabled(bool disabled); virtual ~KeyValueConfigBase(); virtual void reset() = 0; protected: virtual QString getConfigFilepath() = 0; virtual void parseConfigKeyValue(QString key, QString value) = 0; virtual QMap getKeyValueMap() = 0; - virtual void onNewConfigFileCreated() = 0; + virtual void init() = 0; virtual void setUnreadKeys() = 0; bool getConfigBool(QString key, QString value); int getConfigInteger(QString key, QString value, int min = INT_MIN, int max = INT_MAX, int defaultValue = 0); uint32_t getConfigUint32(QString key, QString value, uint32_t min = 0, uint32_t max = UINT_MAX, uint32_t defaultValue = 0); -private: - bool saveDisabled = false; }; class PorymapConfig: public KeyValueConfigBase @@ -86,101 +83,24 @@ public: } void addRecentProject(QString project); void setRecentProjects(QStringList projects); - void setReopenOnLaunch(bool enabled); - void setMapSortOrder(MapSortOrder order); - void setPrettyCursors(bool enabled); + QString getRecentProject(); + QStringList getRecentProjects(); void setMainGeometry(QByteArray, QByteArray, QByteArray, QByteArray, QByteArray); void setTilesetEditorGeometry(QByteArray, QByteArray, QByteArray); void setPaletteEditorGeometry(QByteArray, QByteArray); void setRegionMapEditorGeometry(QByteArray, QByteArray); void setProjectSettingsEditorGeometry(QByteArray, QByteArray); void setCustomScriptsEditorGeometry(QByteArray, QByteArray); - void setCollisionOpacity(int opacity); - void setCollisionZoom(int zoom); - void setMetatilesZoom(int zoom); - void setTilesetEditorMetatilesZoom(int zoom); - void setTilesetEditorTilesZoom(int zoom); - void setShowPlayerView(bool enabled); - void setShowCursorTile(bool enabled); - void setShowBorder(bool enabled); - void setShowGrid(bool enabled); - void setShowTilesetEditorMetatileGrid(bool enabled); - void setShowTilesetEditorLayerGrid(bool enabled); - void setMonitorFiles(bool monitor); - void setTilesetCheckerboardFill(bool checkerboard); - void setTheme(QString theme); - void setTextEditorOpenFolder(const QString &command); - void setTextEditorGotoLine(const QString &command); - void setPaletteEditorBitDepth(int bitDepth); - void setProjectSettingsTab(int tab); - void setWarpBehaviorWarningDisabled(bool disabled); - void setCheckForUpdates(bool enabled); - void setLastUpdateCheckTime(QDateTime time); - void setLastUpdateCheckVersion(QVersionNumber version); - void setRateLimitTimes(QMap map); - QString getRecentProject(); - QStringList getRecentProjects(); - bool getReopenOnLaunch(); - MapSortOrder getMapSortOrder(); - bool getPrettyCursors(); QMap getMainGeometry(); QMap getTilesetEditorGeometry(); QMap getPaletteEditorGeometry(); QMap getRegionMapEditorGeometry(); QMap getProjectSettingsEditorGeometry(); QMap getCustomScriptsEditorGeometry(); - int getCollisionOpacity(); - int getCollisionZoom(); - int getMetatilesZoom(); - int getTilesetEditorMetatilesZoom(); - int getTilesetEditorTilesZoom(); - bool getShowPlayerView(); - bool getShowCursorTile(); - bool getShowBorder(); - bool getShowGrid(); - bool getShowTilesetEditorMetatileGrid(); - bool getShowTilesetEditorLayerGrid(); - bool getMonitorFiles(); - bool getTilesetCheckerboardFill(); - QString getTheme(); - QString getTextEditorOpenFolder(); - QString getTextEditorGotoLine(); - int getPaletteEditorBitDepth(); - int getProjectSettingsTab(); - bool getWarpBehaviorWarningDisabled(); - bool getCheckForUpdates(); - QDateTime getLastUpdateCheckTime(); - QVersionNumber getLastUpdateCheckVersion(); - QMap getRateLimitTimes(); -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: - QStringList recentProjects; + bool reopenOnLaunch; - QString stringFromByteArray(QByteArray); - QByteArray bytesFromString(QString); MapSortOrder mapSortOrder; bool prettyCursors; - QByteArray mainWindowGeometry; - QByteArray mainWindowState; - QByteArray mapSplitterState; - QByteArray mainSplitterState; - QByteArray metatilesSplitterState; - QByteArray tilesetEditorGeometry; - QByteArray tilesetEditorState; - QByteArray tilesetEditorSplitterState; - QByteArray paletteEditorGeometry; - QByteArray paletteEditorState; - QByteArray regionMapEditorGeometry; - QByteArray regionMapEditorState; - QByteArray projectSettingsEditorGeometry; - QByteArray projectSettingsEditorState; - QByteArray customScriptsEditorGeometry; - QByteArray customScriptsEditorState; int collisionOpacity; int collisionZoom; int metatilesZoom; @@ -204,6 +124,35 @@ private: QDateTime lastUpdateCheckTime; QVersionNumber lastUpdateCheckVersion; QMap rateLimitTimes; + +protected: + virtual QString getConfigFilepath() override; + virtual void parseConfigKeyValue(QString key, QString value) override; + virtual QMap getKeyValueMap() override; + virtual void init() override {}; + virtual void setUnreadKeys() override {}; + +private: + QString stringFromByteArray(QByteArray); + QByteArray bytesFromString(QString); + + QStringList recentProjects; + QByteArray mainWindowGeometry; + QByteArray mainWindowState; + QByteArray mapSplitterState; + QByteArray mainSplitterState; + QByteArray metatilesSplitterState; + QByteArray tilesetEditorGeometry; + QByteArray tilesetEditorState; + QByteArray tilesetEditorSplitterState; + QByteArray paletteEditorGeometry; + QByteArray paletteEditorState; + QByteArray regionMapEditorGeometry; + QByteArray regionMapEditorState; + QByteArray projectSettingsEditorGeometry; + QByteArray projectSettingsEditorState; + QByteArray customScriptsEditorGeometry; + QByteArray customScriptsEditorState; }; extern PorymapConfig porymapConfig; @@ -330,7 +279,7 @@ public: this->baseGameVersion = BaseGameVersion::pokeemerald; // Reset non-version-specific settings this->usePoryScript = false; - this->enableTripleLayerMetatiles = false; + this->tripleLayerMetatilesEnabled = false; this->defaultMetatileId = 1; this->defaultElevation = 3; this->defaultCollision = 0; @@ -354,52 +303,11 @@ public: static const QMap> defaultIdentifiers; static const QMap> defaultPaths; static const QStringList versionStrings; + static BaseGameVersion stringToBaseGameVersion(QString string, bool * ok = nullptr); + void reset(BaseGameVersion baseGameVersion); - void setBaseGameVersion(BaseGameVersion baseGameVersion); - BaseGameVersion getBaseGameVersion(); - QString getBaseGameVersionString(); - QString getBaseGameVersionString(BaseGameVersion version); - BaseGameVersion stringToBaseGameVersion(QString string, bool * ok = nullptr); - void setUsePoryScript(bool usePoryScript); - bool getUsePoryScript(); - void setProjectDir(QString projectDir); - QString getProjectDir(); - void setUseCustomBorderSize(bool enable); - bool getUseCustomBorderSize(); - void setEventWeatherTriggerEnabled(bool enable); - bool getEventWeatherTriggerEnabled(); - void setEventSecretBaseEnabled(bool enable); - bool getEventSecretBaseEnabled(); - void setHiddenItemQuantityEnabled(bool enable); - bool getHiddenItemQuantityEnabled(); - void setHiddenItemRequiresItemfinderEnabled(bool enable); - bool getHiddenItemRequiresItemfinderEnabled(); - void setHealLocationRespawnDataEnabled(bool enable); - bool getHealLocationRespawnDataEnabled(); - void setEventCloneObjectEnabled(bool enable); - bool getEventCloneObjectEnabled(); - void setFloorNumberEnabled(bool enable); - bool getFloorNumberEnabled(); - void setCreateMapTextFileEnabled(bool enable); - bool getCreateMapTextFileEnabled(); - void setTripleLayerMetatilesEnabled(bool enable); - bool getTripleLayerMetatilesEnabled(); - int getNumLayersInMetatile(); - int getNumTilesInMetatile(); - void setDefaultMetatileId(uint16_t metatileId); - uint16_t getDefaultMetatileId(); - void setDefaultElevation(uint16_t elevation); - uint16_t getDefaultElevation(); - void setDefaultCollision(uint16_t collision); - uint16_t getDefaultCollision(); - void setNewMapBorderMetatileIds(QList metatileIds); - QList getNewMapBorderMetatileIds(); - QString getDefaultPrimaryTileset(); - QString getDefaultSecondaryTileset(); - void setDefaultPrimaryTileset(QString tilesetName); - void setDefaultSecondaryTileset(QString tilesetName); - void setFilePath(const QString &pathId, const QString &path); void setFilePath(ProjectFilePath pathId, const QString &path); + void setFilePath(const QString &pathId, const QString &path); QString getCustomFilePath(ProjectFilePath pathId); QString getCustomFilePath(const QString &pathId); QString getFilePath(ProjectFilePath pathId); @@ -408,75 +316,35 @@ public: QString getCustomIdentifier(ProjectIdentifier id); QString getCustomIdentifier(const QString &id); QString getIdentifier(ProjectIdentifier id); - void setPrefabFilepath(QString filepath); - QString getPrefabFilepath(); - void setPrefabImportPrompted(bool prompted); - bool getPrefabImportPrompted(); - void setTilesetsHaveCallback(bool has); - bool getTilesetsHaveCallback(); - void setTilesetsHaveIsCompressed(bool has); - bool getTilesetsHaveIsCompressed(); - int getMetatileAttributesSize(); - void setMetatileAttributesSize(int size); - uint32_t getMetatileBehaviorMask(); - uint32_t getMetatileTerrainTypeMask(); - uint32_t getMetatileEncounterTypeMask(); - uint32_t getMetatileLayerTypeMask(); - void setMetatileBehaviorMask(uint32_t mask); - void setMetatileTerrainTypeMask(uint32_t mask); - void setMetatileEncounterTypeMask(uint32_t mask); - void setMetatileLayerTypeMask(uint32_t mask); - uint16_t getBlockMetatileIdMask(); - uint16_t getBlockCollisionMask(); - uint16_t getBlockElevationMask(); - void setBlockMetatileIdMask(uint16_t mask); - void setBlockCollisionMask(uint16_t mask); - void setBlockElevationMask(uint16_t mask); - bool getMapAllowFlagsEnabled(); - void setMapAllowFlagsEnabled(bool enabled); + QString getBaseGameVersionString(BaseGameVersion version); + QString getBaseGameVersionString(); + int getNumLayersInMetatile(); + int getNumTilesInMetatile(); void setEventIconPath(Event::Group group, const QString &path); QString getEventIconPath(Event::Group group); void setPokemonIconPath(const QString &species, const QString &path); QString getPokemonIconPath(const QString &species); QHash getPokemonIconPaths(); - void setCollisionSheetPath(const QString &path); - QString getCollisionSheetPath(); - void setCollisionSheetWidth(int width); - int getCollisionSheetWidth(); - void setCollisionSheetHeight(int height); - int getCollisionSheetHeight(); - void setWarpBehaviors(const QSet &behaviors); - QSet getWarpBehaviors(); -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: BaseGameVersion baseGameVersion; QString projectDir; - QMap identifiers; - QMap filePaths; bool usePoryScript; bool useCustomBorderSize; - bool enableEventWeatherTrigger; - bool enableEventSecretBase; - bool enableHiddenItemQuantity; - bool enableHiddenItemRequiresItemfinder; - bool enableHealLocationRespawnData; - bool enableEventCloneObject; - bool enableFloorNumber; - bool createMapTextFile; - bool enableTripleLayerMetatiles; + bool eventWeatherTriggerEnabled; + bool eventSecretBaseEnabled; + bool hiddenItemQuantityEnabled; + bool hiddenItemRequiresItemfinderEnabled; + bool healLocationRespawnDataEnabled; + bool eventCloneObjectEnabled; + bool floorNumberEnabled; + bool createMapTextFileEnabled; + bool tripleLayerMetatilesEnabled; uint16_t defaultMetatileId; uint16_t defaultElevation; uint16_t defaultCollision; QList newMapBorderMetatileIds; QString defaultPrimaryTileset; QString defaultSecondaryTileset; - QStringList readKeys; QString prefabFilepath; bool prefabImportPrompted; bool tilesetsHaveCallback; @@ -489,13 +357,25 @@ private: uint16_t blockMetatileIdMask; uint16_t blockCollisionMask; uint16_t blockElevationMask; - bool enableMapAllowFlags; - QMap eventIconPaths; - QHash pokemonIconPaths; + bool mapAllowFlagsEnabled; QString collisionSheetPath; int collisionSheetWidth; int collisionSheetHeight; QSet warpBehaviors; + +protected: + virtual QString getConfigFilepath() override; + virtual void parseConfigKeyValue(QString key, QString value) override; + virtual QMap getKeyValueMap() override; + virtual void init() override; + virtual void setUnreadKeys() override; + +private: + QStringList readKeys; + QMap identifiers; + QMap filePaths; + QMap eventIconPaths; + QHash pokemonIconPaths; }; extern ProjectConfig projectConfig; @@ -512,32 +392,29 @@ public: this->customScripts.clear(); this->readKeys.clear(); } - void setRecentMap(const QString &map); - QString getRecentMap(); - void setEncounterJsonActive(bool active); - bool getEncounterJsonActive(); - void setProjectDir(QString projectDir); - QString getProjectDir(); + void parseCustomScripts(QString input); + QString outputCustomScripts(); void setCustomScripts(QStringList scripts, QList enabled); QStringList getCustomScriptPaths(); QList getCustomScriptsEnabled(); - void parseCustomScripts(QString input); - QString outputCustomScripts(); + + QString projectDir; + QString recentMap; + bool useEncounterJson; + protected: virtual QString getConfigFilepath() override; virtual void parseConfigKeyValue(QString key, QString value) override; virtual QMap getKeyValueMap() override; - virtual void onNewConfigFileCreated() override; + virtual void init() override; virtual void setUnreadKeys() override; #ifdef CONFIG_BACKWARDS_COMPATABILITY friend class ProjectConfig; -#endif +#endif + private: - QString projectDir; - QString recentMap; - bool useEncounterJson; - QMap customScripts; QStringList readKeys; + QMap customScripts; }; extern UserConfig userConfig; @@ -567,7 +444,7 @@ protected: virtual QString getConfigFilepath() override; virtual void parseConfigKeyValue(QString key, QString value) override; virtual QMap getKeyValueMap() override; - virtual void onNewConfigFileCreated() override { }; + virtual void init() override { }; virtual void setUnreadKeys() override { }; private: diff --git a/include/mainwindow.h b/include/mainwindow.h index 512bb52c..bb4d4b91 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -355,7 +355,6 @@ private: void showProjectOpenFailure(); void saveGlobalConfigs(); bool setInitialMap(); - void setRecentMap(QString map_name); QStandardItem* createMapItem(QString mapName, int groupNum, int inGroupNum); void refreshRecentProjectsMenu(); diff --git a/include/project.h b/include/project.h index 643f74da..90172f39 100644 --- a/include/project.h +++ b/include/project.h @@ -153,6 +153,7 @@ public: void saveAllMaps(); void saveMap(Map*); void saveAllDataStructures(); + void saveConfig(); void saveMapLayouts(); void saveMapGroups(); void saveWildMonData(); diff --git a/src/config.cpp b/src/config.cpp index 177aeafa..1ee00c18 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -198,16 +198,9 @@ KeyValueConfigBase::~KeyValueConfigBase() { void KeyValueConfigBase::load() { reset(); QFile file(this->getConfigFilepath()); - if (!file.exists()) { - if (!file.open(QIODevice::WriteOnly)) { - logError(QString("Could not create config file '%1'").arg(this->getConfigFilepath())); - } else { - file.close(); - this->onNewConfigFileCreated(); - } - } - - if (!file.open(QIODevice::ReadOnly)) { + if (!file.exists()) + this->init(); + else if (!file.open(QIODevice::ReadOnly)) { logError(QString("Could not open config file '%1': ").arg(this->getConfigFilepath()) + file.errorString()); } @@ -234,15 +227,11 @@ void KeyValueConfigBase::load() { this->parseConfigKeyValue(match.captured("key").trimmed().toLower(), match.captured("value").trimmed()); } this->setUnreadKeys(); - this->save(); file.close(); } void KeyValueConfigBase::save() { - if (this->saveDisabled) - return; - QString text = ""; QMap map = this->getKeyValueMap(); for (QMap::iterator it = map.begin(); it != map.end(); it++) { @@ -287,11 +276,6 @@ uint32_t KeyValueConfigBase::getConfigUint32(QString key, QString value, uint32_ return qMin(max, qMax(min, result)); } -// For temporarily disabling saving during frequent config changes. -void KeyValueConfigBase::setSaveDisabled(bool disabled) { - this->saveDisabled = disabled; -} - const QMap mapSortOrderMap = { {MapSortOrder::Group, "group"}, {MapSortOrder::Layout, "layout"}, @@ -504,183 +488,10 @@ QByteArray PorymapConfig::bytesFromString(QString in) { void PorymapConfig::addRecentProject(QString project) { this->recentProjects.removeOne(project); this->recentProjects.prepend(project); - this->save(); } void PorymapConfig::setRecentProjects(QStringList projects) { this->recentProjects = projects; - this->save(); -} - -void PorymapConfig::setReopenOnLaunch(bool enabled) { - this->reopenOnLaunch = enabled; - this->save(); -} - -void PorymapConfig::setMapSortOrder(MapSortOrder order) { - this->mapSortOrder = order; - this->save(); -} - -void PorymapConfig::setPrettyCursors(bool enabled) { - this->prettyCursors = enabled; - this->save(); -} - -void PorymapConfig::setMonitorFiles(bool monitor) { - this->monitorFiles = monitor; - this->save(); -} - -void PorymapConfig::setTilesetCheckerboardFill(bool checkerboard) { - this->tilesetCheckerboardFill = checkerboard; - this->save(); -} - -void PorymapConfig::setMainGeometry(QByteArray mainWindowGeometry_, QByteArray mainWindowState_, - QByteArray mapSplitterState_, QByteArray mainSplitterState_, QByteArray metatilesSplitterState_) { - this->mainWindowGeometry = mainWindowGeometry_; - this->mainWindowState = mainWindowState_; - this->mapSplitterState = mapSplitterState_; - this->mainSplitterState = mainSplitterState_; - this->metatilesSplitterState = metatilesSplitterState_; - this->save(); -} - -void PorymapConfig::setTilesetEditorGeometry(QByteArray tilesetEditorGeometry_, QByteArray tilesetEditorState_, - QByteArray tilesetEditorSplitterState_) { - this->tilesetEditorGeometry = tilesetEditorGeometry_; - this->tilesetEditorState = tilesetEditorState_; - this->tilesetEditorSplitterState = tilesetEditorSplitterState_; - this->save(); -} - -void PorymapConfig::setPaletteEditorGeometry(QByteArray paletteEditorGeometry_, QByteArray paletteEditorState_) { - this->paletteEditorGeometry = paletteEditorGeometry_; - this->paletteEditorState = paletteEditorState_; - this->save(); -} - -void PorymapConfig::setRegionMapEditorGeometry(QByteArray regionMapEditorGeometry_, QByteArray regionMapEditorState_) { - this->regionMapEditorGeometry = regionMapEditorGeometry_; - this->regionMapEditorState = regionMapEditorState_; - this->save(); -} - -void PorymapConfig::setProjectSettingsEditorGeometry(QByteArray projectSettingsEditorGeometry_, QByteArray projectSettingsEditorState_) { - this->projectSettingsEditorGeometry = projectSettingsEditorGeometry_; - this->projectSettingsEditorState = projectSettingsEditorState_; - this->save(); -} - -void PorymapConfig::setCustomScriptsEditorGeometry(QByteArray customScriptsEditorGeometry_, QByteArray customScriptsEditorState_) { - this->customScriptsEditorGeometry = customScriptsEditorGeometry_; - this->customScriptsEditorState = customScriptsEditorState_; - this->save(); -} - -void PorymapConfig::setCollisionOpacity(int opacity) { - this->collisionOpacity = opacity; - // don't auto-save here because this can be called very frequently. -} - -void PorymapConfig::setCollisionZoom(int zoom) { - this->collisionZoom = zoom; - // don't auto-save here because this can be called very frequently. -} - -void PorymapConfig::setMetatilesZoom(int zoom) { - this->metatilesZoom = zoom; - // don't auto-save here because this can be called very frequently. -} - -void PorymapConfig::setTilesetEditorMetatilesZoom(int zoom) { - this->tilesetEditorMetatilesZoom = zoom; - // don't auto-save here because this can be called very frequently. -} - -void PorymapConfig::setTilesetEditorTilesZoom(int zoom) { - this->tilesetEditorTilesZoom = zoom; - // don't auto-save here because this can be called very frequently. -} - -void PorymapConfig::setShowPlayerView(bool enabled) { - this->showPlayerView = enabled; - this->save(); -} - -void PorymapConfig::setShowCursorTile(bool enabled) { - this->showCursorTile = enabled; - this->save(); -} - -void PorymapConfig::setShowBorder(bool enabled) { - this->showBorder = enabled; - this->save(); -} - -void PorymapConfig::setShowGrid(bool enabled) { - this->showGrid = enabled; - this->save(); -} - -void PorymapConfig::setShowTilesetEditorMetatileGrid(bool enabled) { - this->showTilesetEditorMetatileGrid = enabled; - this->save(); -} - -void PorymapConfig::setShowTilesetEditorLayerGrid(bool enabled) { - this->showTilesetEditorLayerGrid = enabled; - this->save(); -} - -void PorymapConfig::setTheme(QString theme) { - this->theme = theme; -} - -void PorymapConfig::setTextEditorOpenFolder(const QString &command) { - this->textEditorOpenFolder = command; - this->save(); -} - -void PorymapConfig::setTextEditorGotoLine(const QString &command) { - this->textEditorGotoLine = command; - this->save(); -} - -void PorymapConfig::setPaletteEditorBitDepth(int bitDepth) { - this->paletteEditorBitDepth = bitDepth; - this->save(); -} - -void PorymapConfig::setProjectSettingsTab(int tab) { - this->projectSettingsTab = tab; - this->save(); -} - -void PorymapConfig::setWarpBehaviorWarningDisabled(bool disabled) { - this->warpBehaviorWarningDisabled = disabled; - this->save(); -} - -void PorymapConfig::setCheckForUpdates(bool enabled) { - this->checkForUpdates = enabled; - this->save(); -} - -void PorymapConfig::setLastUpdateCheckTime(QDateTime time) { - this->lastUpdateCheckTime = time; - this->save(); -} - -void PorymapConfig::setLastUpdateCheckVersion(QVersionNumber version) { - this->lastUpdateCheckVersion = version; - this->save(); -} - -void PorymapConfig::setRateLimitTimes(QMap map) { - this->rateLimitTimes = map; - this->save(); } QString PorymapConfig::getRecentProject() { @@ -691,16 +502,40 @@ QStringList PorymapConfig::getRecentProjects() { return this->recentProjects; } -bool PorymapConfig::getReopenOnLaunch() { - return this->reopenOnLaunch; +void PorymapConfig::setMainGeometry(QByteArray mainWindowGeometry_, QByteArray mainWindowState_, + QByteArray mapSplitterState_, QByteArray mainSplitterState_, QByteArray metatilesSplitterState_) { + this->mainWindowGeometry = mainWindowGeometry_; + this->mainWindowState = mainWindowState_; + this->mapSplitterState = mapSplitterState_; + this->mainSplitterState = mainSplitterState_; + this->metatilesSplitterState = metatilesSplitterState_; } -MapSortOrder PorymapConfig::getMapSortOrder() { - return this->mapSortOrder; +void PorymapConfig::setTilesetEditorGeometry(QByteArray tilesetEditorGeometry_, QByteArray tilesetEditorState_, + QByteArray tilesetEditorSplitterState_) { + this->tilesetEditorGeometry = tilesetEditorGeometry_; + this->tilesetEditorState = tilesetEditorState_; + this->tilesetEditorSplitterState = tilesetEditorSplitterState_; } -bool PorymapConfig::getPrettyCursors() { - return this->prettyCursors; +void PorymapConfig::setPaletteEditorGeometry(QByteArray paletteEditorGeometry_, QByteArray paletteEditorState_) { + this->paletteEditorGeometry = paletteEditorGeometry_; + this->paletteEditorState = paletteEditorState_; +} + +void PorymapConfig::setRegionMapEditorGeometry(QByteArray regionMapEditorGeometry_, QByteArray regionMapEditorState_) { + this->regionMapEditorGeometry = regionMapEditorGeometry_; + this->regionMapEditorState = regionMapEditorState_; +} + +void PorymapConfig::setProjectSettingsEditorGeometry(QByteArray projectSettingsEditorGeometry_, QByteArray projectSettingsEditorState_) { + this->projectSettingsEditorGeometry = projectSettingsEditorGeometry_; + this->projectSettingsEditorState = projectSettingsEditorState_; +} + +void PorymapConfig::setCustomScriptsEditorGeometry(QByteArray customScriptsEditorGeometry_, QByteArray customScriptsEditorState_) { + this->customScriptsEditorGeometry = customScriptsEditorGeometry_; + this->customScriptsEditorState = customScriptsEditorState_; } QMap PorymapConfig::getMainGeometry() { @@ -761,98 +596,6 @@ QMap PorymapConfig::getCustomScriptsEditorGeometry() { return geometry; } -int PorymapConfig::getCollisionOpacity() { - return this->collisionOpacity; -} - -int PorymapConfig::getCollisionZoom() { - return this->collisionZoom; -} - -int PorymapConfig::getMetatilesZoom() { - return this->metatilesZoom; -} - -int PorymapConfig::getTilesetEditorMetatilesZoom() { - return this->tilesetEditorMetatilesZoom; -} - -int PorymapConfig::getTilesetEditorTilesZoom() { - return this->tilesetEditorTilesZoom; -} - -bool PorymapConfig::getShowPlayerView() { - return this->showPlayerView; -} - -bool PorymapConfig::getShowCursorTile() { - return this->showCursorTile; -} - -bool PorymapConfig::getShowBorder() { - return this->showBorder; -} - -bool PorymapConfig::getShowGrid() { - return this->showGrid; -} - -bool PorymapConfig::getShowTilesetEditorMetatileGrid() { - return this->showTilesetEditorMetatileGrid; -} - -bool PorymapConfig::getShowTilesetEditorLayerGrid() { - return this->showTilesetEditorLayerGrid; -} - -bool PorymapConfig::getMonitorFiles() { - return this->monitorFiles; -} - -bool PorymapConfig::getTilesetCheckerboardFill() { - return this->tilesetCheckerboardFill; -} - -QString PorymapConfig::getTheme() { - return this->theme; -} - -QString PorymapConfig::getTextEditorOpenFolder() { - return this->textEditorOpenFolder; -} - -QString PorymapConfig::getTextEditorGotoLine() { - return this->textEditorGotoLine; -} - -int PorymapConfig::getPaletteEditorBitDepth() { - return this->paletteEditorBitDepth; -} - -int PorymapConfig::getProjectSettingsTab() { - return this->projectSettingsTab; -} - -bool PorymapConfig::getWarpBehaviorWarningDisabled() { - return this->warpBehaviorWarningDisabled; -} - -bool PorymapConfig::getCheckForUpdates() { - return this->checkForUpdates; -} - -QDateTime PorymapConfig::getLastUpdateCheckTime() { - return this->lastUpdateCheckTime; -} - -QVersionNumber PorymapConfig::getLastUpdateCheckVersion() { - return this->lastUpdateCheckVersion; -} - -QMap PorymapConfig::getRateLimitTimes() { - return this->rateLimitTimes; -} - const QStringList ProjectConfig::versionStrings = { "pokeruby", "pokefirered", @@ -899,24 +642,24 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { } else if (key == "use_custom_border_size") { this->useCustomBorderSize = getConfigBool(key, value); } else if (key == "enable_event_weather_trigger") { - this->enableEventWeatherTrigger = getConfigBool(key, value); + this->eventWeatherTriggerEnabled = getConfigBool(key, value); } else if (key == "enable_event_secret_base") { - this->enableEventSecretBase = getConfigBool(key, value); + this->eventSecretBaseEnabled = getConfigBool(key, value); } else if (key == "enable_hidden_item_quantity") { - this->enableHiddenItemQuantity = getConfigBool(key, value); + this->hiddenItemQuantityEnabled = getConfigBool(key, value); } else if (key == "enable_hidden_item_requires_itemfinder") { - this->enableHiddenItemRequiresItemfinder = getConfigBool(key, value); + this->hiddenItemRequiresItemfinderEnabled = getConfigBool(key, value); } else if (key == "enable_heal_location_respawn_data") { - this->enableHealLocationRespawnData = getConfigBool(key, value); + this->healLocationRespawnDataEnabled = getConfigBool(key, value); } else if (key == "enable_event_clone_object" || key == "enable_object_event_in_connection") { - this->enableEventCloneObject = getConfigBool(key, value); + this->eventCloneObjectEnabled = getConfigBool(key, value); key = "enable_event_clone_object"; // Backwards compatibiliy, replace old name above } else if (key == "enable_floor_number") { - this->enableFloorNumber = getConfigBool(key, value); + this->floorNumberEnabled = getConfigBool(key, value); } else if (key == "create_map_text_file") { - this->createMapTextFile = getConfigBool(key, value); + this->createMapTextFileEnabled = getConfigBool(key, value); } else if (key == "enable_triple_layer_metatiles") { - this->enableTripleLayerMetatiles = getConfigBool(key, value); + this->tripleLayerMetatilesEnabled = getConfigBool(key, value); } else if (key == "default_metatile") { this->defaultMetatileId = getConfigUint32(key, value, 0, Block::maxValue); } else if (key == "default_elevation") { @@ -956,10 +699,10 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { } else if (key == "block_elevation_mask") { this->blockElevationMask = getConfigUint32(key, value, 0, Block::maxValue); } else if (key == "enable_map_allow_flags") { - this->enableMapAllowFlags = getConfigBool(key, value); + this->mapAllowFlagsEnabled = getConfigBool(key, value); #ifdef CONFIG_BACKWARDS_COMPATABILITY } else if (key == "recent_map") { - userConfig.setRecentMap(value); + userConfig.recentMap = value; } else if (key == "use_encounter_json") { userConfig.useEncounterJson = getConfigBool(key, value); } else if (key == "custom_scripts") { @@ -1028,14 +771,14 @@ void ProjectConfig::setUnreadKeys() { // Set game-version specific defaults for any config field that wasn't read bool isPokefirered = this->baseGameVersion == BaseGameVersion::pokefirered; if (!readKeys.contains("use_custom_border_size")) this->useCustomBorderSize = isPokefirered; - if (!readKeys.contains("enable_event_weather_trigger")) this->enableEventWeatherTrigger = !isPokefirered; - if (!readKeys.contains("enable_event_secret_base")) this->enableEventSecretBase = !isPokefirered; - if (!readKeys.contains("enable_hidden_item_quantity")) this->enableHiddenItemQuantity = isPokefirered; - if (!readKeys.contains("enable_hidden_item_requires_itemfinder")) this->enableHiddenItemRequiresItemfinder = isPokefirered; - if (!readKeys.contains("enable_heal_location_respawn_data")) this->enableHealLocationRespawnData = isPokefirered; - if (!readKeys.contains("enable_event_clone_object")) this->enableEventCloneObject = isPokefirered; - if (!readKeys.contains("enable_floor_number")) this->enableFloorNumber = isPokefirered; - if (!readKeys.contains("create_map_text_file")) this->createMapTextFile = (this->baseGameVersion != BaseGameVersion::pokeemerald); + if (!readKeys.contains("enable_event_weather_trigger")) this->eventWeatherTriggerEnabled = !isPokefirered; + if (!readKeys.contains("enable_event_secret_base")) this->eventSecretBaseEnabled = !isPokefirered; + if (!readKeys.contains("enable_hidden_item_quantity")) this->hiddenItemQuantityEnabled = isPokefirered; + if (!readKeys.contains("enable_hidden_item_requires_itemfinder")) this->hiddenItemRequiresItemfinderEnabled = isPokefirered; + if (!readKeys.contains("enable_heal_location_respawn_data")) this->healLocationRespawnDataEnabled = isPokefirered; + if (!readKeys.contains("enable_event_clone_object")) this->eventCloneObjectEnabled = isPokefirered; + if (!readKeys.contains("enable_floor_number")) this->floorNumberEnabled = isPokefirered; + if (!readKeys.contains("create_map_text_file")) this->createMapTextFileEnabled = (this->baseGameVersion != BaseGameVersion::pokeemerald); if (!readKeys.contains("new_map_border_metatiles")) this->newMapBorderMetatileIds = isPokefirered ? DEFAULT_BORDER_FRLG : DEFAULT_BORDER_RSE; if (!readKeys.contains("default_secondary_tileset")) this->defaultSecondaryTileset = isPokefirered ? "gTileset_PalletTown" : "gTileset_Petalburg"; if (!readKeys.contains("metatile_attributes_size")) this->metatileAttributesSize = Metatile::getDefaultAttributesSize(this->baseGameVersion); @@ -1043,7 +786,7 @@ void ProjectConfig::setUnreadKeys() { if (!readKeys.contains("metatile_terrain_type_mask")) this->metatileTerrainTypeMask = Metatile::getDefaultAttributesMask(this->baseGameVersion, Metatile::Attr::TerrainType); if (!readKeys.contains("metatile_encounter_type_mask")) this->metatileEncounterTypeMask = Metatile::getDefaultAttributesMask(this->baseGameVersion, Metatile::Attr::EncounterType); if (!readKeys.contains("metatile_layer_type_mask")) this->metatileLayerTypeMask = Metatile::getDefaultAttributesMask(this->baseGameVersion, Metatile::Attr::LayerType); - if (!readKeys.contains("enable_map_allow_flags")) this->enableMapAllowFlags = (this->baseGameVersion != BaseGameVersion::pokeruby); + if (!readKeys.contains("enable_map_allow_flags")) this->mapAllowFlagsEnabled = (this->baseGameVersion != BaseGameVersion::pokeruby); if (!readKeys.contains("warp_behaviors")) this->warpBehaviors = isPokefirered ? defaultWarpBehaviors_FRLG : defaultWarpBehaviors_RSE; } @@ -1052,15 +795,15 @@ QMap ProjectConfig::getKeyValueMap() { map.insert("base_game_version", baseGameVersionMap.value(this->baseGameVersion)); map.insert("use_poryscript", QString::number(this->usePoryScript)); map.insert("use_custom_border_size", QString::number(this->useCustomBorderSize)); - map.insert("enable_event_weather_trigger", QString::number(this->enableEventWeatherTrigger)); - map.insert("enable_event_secret_base", QString::number(this->enableEventSecretBase)); - map.insert("enable_hidden_item_quantity", QString::number(this->enableHiddenItemQuantity)); - map.insert("enable_hidden_item_requires_itemfinder", QString::number(this->enableHiddenItemRequiresItemfinder)); - map.insert("enable_heal_location_respawn_data", QString::number(this->enableHealLocationRespawnData)); - map.insert("enable_event_clone_object", QString::number(this->enableEventCloneObject)); - map.insert("enable_floor_number", QString::number(this->enableFloorNumber)); - map.insert("create_map_text_file", QString::number(this->createMapTextFile)); - map.insert("enable_triple_layer_metatiles", QString::number(this->enableTripleLayerMetatiles)); + map.insert("enable_event_weather_trigger", QString::number(this->eventWeatherTriggerEnabled)); + map.insert("enable_event_secret_base", QString::number(this->eventSecretBaseEnabled)); + map.insert("enable_hidden_item_quantity", QString::number(this->hiddenItemQuantityEnabled)); + map.insert("enable_hidden_item_requires_itemfinder", QString::number(this->hiddenItemRequiresItemfinderEnabled)); + map.insert("enable_heal_location_respawn_data", QString::number(this->healLocationRespawnDataEnabled)); + map.insert("enable_event_clone_object", QString::number(this->eventCloneObjectEnabled)); + map.insert("enable_floor_number", QString::number(this->floorNumberEnabled)); + map.insert("create_map_text_file", QString::number(this->createMapTextFileEnabled)); + map.insert("enable_triple_layer_metatiles", QString::number(this->tripleLayerMetatilesEnabled)); map.insert("default_metatile", Metatile::getMetatileIdString(this->defaultMetatileId)); map.insert("default_elevation", QString::number(this->defaultElevation)); map.insert("default_collision", QString::number(this->defaultCollision)); @@ -1082,7 +825,7 @@ QMap ProjectConfig::getKeyValueMap() { map.insert("block_metatile_id_mask", "0x" + QString::number(this->blockMetatileIdMask, 16).toUpper()); map.insert("block_collision_mask", "0x" + QString::number(this->blockCollisionMask, 16).toUpper()); map.insert("block_elevation_mask", "0x" + QString::number(this->blockElevationMask, 16).toUpper()); - map.insert("enable_map_allow_flags", QString::number(this->enableMapAllowFlags)); + map.insert("enable_map_allow_flags", QString::number(this->mapAllowFlagsEnabled)); map.insert("event_icon_path_object", this->eventIconPaths[Event::Group::Object]); map.insert("event_icon_path_warp", this->eventIconPaths[Event::Group::Warp]); map.insert("event_icon_path_coord", this->eventIconPaths[Event::Group::Coord]); @@ -1106,9 +849,10 @@ QMap ProjectConfig::getKeyValueMap() { return map; } -void ProjectConfig::onNewConfigFileCreated() { +void ProjectConfig::init() { QString dirName = QDir(this->projectDir).dirName().toLower(); if (baseGameVersionReverseMap.contains(dirName)) { + // TODO: Improve detection (ex: emerald or pokeemerald-2 aren't currently auto-detected) this->baseGameVersion = baseGameVersionReverseMap.value(dirName); logInfo(QString("Auto-detected base_game_version as '%1'").arg(dirName)); } else { @@ -1124,6 +868,8 @@ void ProjectConfig::onNewConfigFileCreated() { baseGameVersionComboBox->addItem("pokeemerald", BaseGameVersion::pokeemerald); form.addRow(new QLabel("Game Version"), baseGameVersionComboBox); + // TODO: Advanced button to open the project settings window (with some settings disabled) + QDialogButtonBox buttonBox(QDialogButtonBox::Ok, Qt::Horizontal, &dialog); QObject::connect(&buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); form.addRow(&buttonBox); @@ -1135,14 +881,6 @@ void ProjectConfig::onNewConfigFileCreated() { this->setUnreadKeys(); // Initialize version-specific options } -void ProjectConfig::setProjectDir(QString projectDir) { - this->projectDir = projectDir; -} - -QString ProjectConfig::getProjectDir() { - return this->projectDir; -} - void ProjectConfig::setFilePath(ProjectFilePath pathId, const QString &path) { if (!defaultPaths.contains(pathId)) return; if (path.isEmpty()) { @@ -1208,15 +946,6 @@ QString ProjectConfig::getIdentifier(ProjectIdentifier id) { return defaultIdentifiers.contains(id) ? defaultIdentifiers[id].second : QString(); } -void ProjectConfig::setBaseGameVersion(BaseGameVersion baseGameVersion) { - this->baseGameVersion = baseGameVersion; - this->save(); -} - -BaseGameVersion ProjectConfig::getBaseGameVersion() { - return this->baseGameVersion; -} - QString ProjectConfig::getBaseGameVersionString(BaseGameVersion version) { if (!baseGameVersionMap.contains(version)) { version = BaseGameVersion::pokeemerald; @@ -1228,287 +957,16 @@ QString ProjectConfig::getBaseGameVersionString() { return this->getBaseGameVersionString(this->baseGameVersion); } -void ProjectConfig::setUsePoryScript(bool usePoryScript) { - this->usePoryScript = usePoryScript; - this->save(); -} - -bool ProjectConfig::getUsePoryScript() { - return this->usePoryScript; -} - -void ProjectConfig::setUseCustomBorderSize(bool enable) { - this->useCustomBorderSize = enable; - this->save(); -} - -bool ProjectConfig::getUseCustomBorderSize() { - return this->useCustomBorderSize; -} - -void ProjectConfig::setEventWeatherTriggerEnabled(bool enable) { - this->enableEventWeatherTrigger = enable; - this->save(); -} - -bool ProjectConfig::getEventWeatherTriggerEnabled() { - return this->enableEventWeatherTrigger; -} - -void ProjectConfig::setEventSecretBaseEnabled(bool enable) { - this->enableEventSecretBase = enable; - this->save(); -} - -bool ProjectConfig::getEventSecretBaseEnabled() { - return this->enableEventSecretBase; -} - -void ProjectConfig::setHiddenItemQuantityEnabled(bool enable) { - this->enableHiddenItemQuantity = enable; - this->save(); -} - -bool ProjectConfig::getHiddenItemQuantityEnabled() { - return this->enableHiddenItemQuantity; -} - -void ProjectConfig::setHiddenItemRequiresItemfinderEnabled(bool enable) { - this->enableHiddenItemRequiresItemfinder = enable; - this->save(); -} - -bool ProjectConfig::getHiddenItemRequiresItemfinderEnabled() { - return this->enableHiddenItemRequiresItemfinder; -} - -void ProjectConfig::setHealLocationRespawnDataEnabled(bool enable) { - this->enableHealLocationRespawnData = enable; - this->save(); -} - -bool ProjectConfig::getHealLocationRespawnDataEnabled() { - return this->enableHealLocationRespawnData; -} - -void ProjectConfig::setEventCloneObjectEnabled(bool enable) { - this->enableEventCloneObject = enable; - this->save(); -} - -bool ProjectConfig::getEventCloneObjectEnabled() { - return this->enableEventCloneObject; -} - -void ProjectConfig::setFloorNumberEnabled(bool enable) { - this->enableFloorNumber = enable; - this->save(); -} - -bool ProjectConfig::getFloorNumberEnabled() { - return this->enableFloorNumber; -} - -void ProjectConfig::setCreateMapTextFileEnabled(bool enable) { - this->createMapTextFile = enable; - this->save(); -} - -bool ProjectConfig::getCreateMapTextFileEnabled() { - return this->createMapTextFile; -} - -void ProjectConfig::setTripleLayerMetatilesEnabled(bool enable) { - this->enableTripleLayerMetatiles = enable; - this->save(); -} - -bool ProjectConfig::getTripleLayerMetatilesEnabled() { - return this->enableTripleLayerMetatiles; -} - int ProjectConfig::getNumLayersInMetatile() { - return this->enableTripleLayerMetatiles ? 3 : 2; + return this->tripleLayerMetatilesEnabled ? 3 : 2; } int ProjectConfig::getNumTilesInMetatile() { - return this->enableTripleLayerMetatiles ? 12 : 8; -} - -void ProjectConfig::setDefaultMetatileId(uint16_t metatileId) { - this->defaultMetatileId = metatileId; - this->save(); -} - -uint16_t ProjectConfig::getDefaultMetatileId() { - return this->defaultMetatileId; -} - -void ProjectConfig::setDefaultElevation(uint16_t elevation) { - this->defaultElevation = elevation; - this->save(); -} - -uint16_t ProjectConfig::getDefaultElevation() { - return this->defaultElevation; -} - -void ProjectConfig::setDefaultCollision(uint16_t collision) { - this->defaultCollision = collision; - this->save(); -} - -uint16_t ProjectConfig::getDefaultCollision() { - return this->defaultCollision; -} - -void ProjectConfig::setNewMapBorderMetatileIds(QList metatileIds) { - this->newMapBorderMetatileIds = metatileIds; - this->save(); -} - -QList ProjectConfig::getNewMapBorderMetatileIds() { - return this->newMapBorderMetatileIds; -} - -QString ProjectConfig::getDefaultPrimaryTileset() { - return this->defaultPrimaryTileset; -} - -QString ProjectConfig::getDefaultSecondaryTileset() { - return this->defaultSecondaryTileset; -} - -void ProjectConfig::setDefaultPrimaryTileset(QString tilesetName) { - this->defaultPrimaryTileset = tilesetName; - this->save(); -} - -void ProjectConfig::setDefaultSecondaryTileset(QString tilesetName) { - this->defaultSecondaryTileset = tilesetName; - this->save(); -} - -void ProjectConfig::setPrefabFilepath(QString filepath) { - this->prefabFilepath = filepath; - this->save(); -} - -QString ProjectConfig::getPrefabFilepath() { - return this->prefabFilepath; -} - -void ProjectConfig::setPrefabImportPrompted(bool prompted) { - this->prefabImportPrompted = prompted; - this->save(); -} - -bool ProjectConfig::getPrefabImportPrompted() { - return this->prefabImportPrompted; -} - -void ProjectConfig::setTilesetsHaveCallback(bool has) { - this->tilesetsHaveCallback = has; - this->save(); -} - -bool ProjectConfig::getTilesetsHaveCallback() { - return this->tilesetsHaveCallback; -} - -void ProjectConfig::setTilesetsHaveIsCompressed(bool has) { - this->tilesetsHaveIsCompressed = has; - this->save(); -} - -bool ProjectConfig::getTilesetsHaveIsCompressed() { - return this->tilesetsHaveIsCompressed; -} - -int ProjectConfig::getMetatileAttributesSize() { - return this->metatileAttributesSize; -} - -void ProjectConfig::setMetatileAttributesSize(int size) { - this->metatileAttributesSize = size; - this->save(); -} - -uint32_t ProjectConfig::getMetatileBehaviorMask() { - return this->metatileBehaviorMask; -} - -uint32_t ProjectConfig::getMetatileTerrainTypeMask() { - return this->metatileTerrainTypeMask; -} - -uint32_t ProjectConfig::getMetatileEncounterTypeMask() { - return this->metatileEncounterTypeMask; -} - -uint32_t ProjectConfig::getMetatileLayerTypeMask() { - return this->metatileLayerTypeMask; -} - -void ProjectConfig::setMetatileBehaviorMask(uint32_t mask) { - this->metatileBehaviorMask = mask; - this->save(); -} - -void ProjectConfig::setMetatileTerrainTypeMask(uint32_t mask) { - this->metatileTerrainTypeMask = mask; - this->save(); -} - -void ProjectConfig::setMetatileEncounterTypeMask(uint32_t mask) { - this->metatileEncounterTypeMask = mask; - this->save(); -} - -void ProjectConfig::setMetatileLayerTypeMask(uint32_t mask) { - this->metatileLayerTypeMask = mask; - this->save(); -} - -uint16_t ProjectConfig::getBlockMetatileIdMask() { - return this->blockMetatileIdMask; -} - -uint16_t ProjectConfig::getBlockCollisionMask() { - return this->blockCollisionMask; -} - -uint16_t ProjectConfig::getBlockElevationMask() { - return this->blockElevationMask; -} - -void ProjectConfig::setBlockMetatileIdMask(uint16_t mask) { - this->blockMetatileIdMask = mask; - this->save(); -} - -void ProjectConfig::setBlockCollisionMask(uint16_t mask) { - this->blockCollisionMask = mask; - this->save(); -} - -void ProjectConfig::setBlockElevationMask(uint16_t mask) { - this->blockElevationMask = mask; - this->save(); -} - -bool ProjectConfig::getMapAllowFlagsEnabled() { - return this->enableMapAllowFlags; -} - -void ProjectConfig::setMapAllowFlagsEnabled(bool enabled) { - this->enableMapAllowFlags = enabled; - this->save(); + return this->tripleLayerMetatilesEnabled ? 12 : 8; } void ProjectConfig::setEventIconPath(Event::Group group, const QString &path) { this->eventIconPaths[group] = path; - this->save(); } QString ProjectConfig::getEventIconPath(Event::Group group) { @@ -1517,7 +975,6 @@ QString ProjectConfig::getEventIconPath(Event::Group group) { void ProjectConfig::setPokemonIconPath(const QString &species, const QString &path) { this->pokemonIconPaths[species] = path; - this->save(); } QString ProjectConfig::getPokemonIconPath(const QString &species) { @@ -1528,43 +985,6 @@ QHash ProjectConfig::getPokemonIconPaths() { return this->pokemonIconPaths; } -void ProjectConfig::setCollisionSheetPath(const QString &path) { - this->collisionSheetPath = path; - this->save(); -} - -QString ProjectConfig::getCollisionSheetPath() { - return this->collisionSheetPath; -} - -void ProjectConfig::setCollisionSheetWidth(int width) { - this->collisionSheetWidth = width; - this->save(); -} - -int ProjectConfig::getCollisionSheetWidth() { - return this->collisionSheetWidth; -} - -void ProjectConfig::setCollisionSheetHeight(int height) { - this->collisionSheetHeight = height; - this->save(); -} - -int ProjectConfig::getCollisionSheetHeight() { - return this->collisionSheetHeight; -} - -void ProjectConfig::setWarpBehaviors(const QSet &behaviors) { - this->warpBehaviors = behaviors; - this->save(); -} - -QSet ProjectConfig::getWarpBehaviors() { - return this->warpBehaviors; -} - - UserConfig userConfig; QString UserConfig::getConfigFilepath() { @@ -1596,38 +1016,12 @@ QMap UserConfig::getKeyValueMap() { return map; } -void UserConfig::onNewConfigFileCreated() { +void UserConfig::init() { QString dirName = QDir(this->projectDir).dirName().toLower(); this->useEncounterJson = true; this->customScripts.clear(); } -void UserConfig::setProjectDir(QString projectDir) { - this->projectDir = projectDir; -} - -QString UserConfig::getProjectDir() { - return this->projectDir; -} - -void UserConfig::setRecentMap(const QString &map) { - this->recentMap = map; - this->save(); -} - -QString UserConfig::getRecentMap() { - return this->recentMap; -} - -void UserConfig::setEncounterJsonActive(bool active) { - this->useEncounterJson = active; - this->save(); -} - -bool UserConfig::getEncounterJsonActive() { - return this->useEncounterJson; -} - // Read input from the config to get the script paths and whether each is enabled or disbled. // The format is a comma-separated list of paths. Each path can be followed (before the comma) // by a :0 or :1 to indicate whether it should be disabled or enabled, respectively. If neither @@ -1664,7 +1058,6 @@ void UserConfig::setCustomScripts(QStringList scripts, QList enabled) { size_t size = qMin(scripts.length(), enabled.length()); for (size_t i = 0; i < size; i++) this->customScripts.insert(scripts.at(i), enabled.at(i)); - this->save(); } QStringList UserConfig::getCustomScriptPaths() { @@ -1708,7 +1101,6 @@ QMap ShortcutsConfig::getKeyValueMap() { void ShortcutsConfig::setDefaultShortcuts(const QObjectList &objects) { storeShortcutsFromList(StoreType::Default, objects); - save(); } QList ShortcutsConfig::defaultShortcuts(const QObject *object) const { @@ -1717,14 +1109,12 @@ QList ShortcutsConfig::defaultShortcuts(const QObject *object) con 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 { diff --git a/src/core/block.cpp b/src/core/block.cpp index 9eab9d13..fc4fe774 100644 --- a/src/core/block.cpp +++ b/src/core/block.cpp @@ -47,9 +47,9 @@ uint16_t Block::rawValue() const { } void Block::setLayout() { - bitsMetatileId.setMask(projectConfig.getBlockMetatileIdMask()); - bitsCollision.setMask(projectConfig.getBlockCollisionMask()); - bitsElevation.setMask(projectConfig.getBlockElevationMask()); + bitsMetatileId.setMask(projectConfig.blockMetatileIdMask); + bitsCollision.setMask(projectConfig.blockCollisionMask); + bitsElevation.setMask(projectConfig.blockElevationMask); } bool Block::operator ==(Block other) const { diff --git a/src/core/events.cpp b/src/core/events.cpp index 40067e22..b0ea3e39 100644 --- a/src/core/events.cpp +++ b/src/core/events.cpp @@ -35,7 +35,7 @@ int Event::getEventIndex() { void Event::setDefaultValues(Project *) { this->setX(0); this->setY(0); - this->setElevation(projectConfig.getDefaultElevation()); + this->setElevation(projectConfig.defaultElevation); } void Event::readCustomValues(QJsonObject values) { @@ -195,7 +195,7 @@ EventFrame *ObjectEvent::createEventFrame() { OrderedJson::object ObjectEvent::buildEventJson(Project *) { OrderedJson::object objectJson; - if (projectConfig.getEventCloneObjectEnabled()) { + if (projectConfig.eventCloneObjectEnabled) { objectJson["type"] = "object"; } objectJson["graphics_id"] = this->getGfx(); @@ -259,7 +259,7 @@ const QSet expectedObjectFields = { QSet ObjectEvent::getExpectedFields() { QSet expectedFields = QSet(); expectedFields = expectedObjectFields; - if (projectConfig.getEventCloneObjectEnabled()) { + if (projectConfig.eventCloneObjectEnabled) { expectedFields.insert("type"); } expectedFields << "x" << "y"; @@ -788,10 +788,10 @@ OrderedJson::object HiddenItemEvent::buildEventJson(Project *) { hiddenItemJson["elevation"] = this->getElevation(); hiddenItemJson["item"] = this->getItem(); hiddenItemJson["flag"] = this->getFlag(); - if (projectConfig.getHiddenItemQuantityEnabled()) { + if (projectConfig.hiddenItemQuantityEnabled) { hiddenItemJson["quantity"] = this->getQuantity(); } - if (projectConfig.getHiddenItemRequiresItemfinderEnabled()) { + if (projectConfig.hiddenItemRequiresItemfinderEnabled) { hiddenItemJson["underfoot"] = this->getUnderfoot(); } @@ -806,10 +806,10 @@ bool HiddenItemEvent::loadFromJson(QJsonObject json, Project *) { this->setElevation(ParseUtil::jsonToInt(json["elevation"])); this->setItem(ParseUtil::jsonToQString(json["item"])); this->setFlag(ParseUtil::jsonToQString(json["flag"])); - if (projectConfig.getHiddenItemQuantityEnabled()) { + if (projectConfig.hiddenItemQuantityEnabled) { this->setQuantity(ParseUtil::jsonToInt(json["quantity"])); } - if (projectConfig.getHiddenItemRequiresItemfinderEnabled()) { + if (projectConfig.hiddenItemRequiresItemfinderEnabled) { this->setUnderfoot(ParseUtil::jsonToBool(json["underfoot"])); } @@ -821,10 +821,10 @@ bool HiddenItemEvent::loadFromJson(QJsonObject json, Project *) { void HiddenItemEvent::setDefaultValues(Project *project) { this->setItem(project->itemNames.value(0, "0")); this->setFlag(project->flagNames.value(0, "0")); - if (projectConfig.getHiddenItemQuantityEnabled()) { + if (projectConfig.hiddenItemQuantityEnabled) { this->setQuantity(1); } - if (projectConfig.getHiddenItemRequiresItemfinderEnabled()) { + if (projectConfig.hiddenItemRequiresItemfinderEnabled) { this->setUnderfoot(false); } } @@ -839,10 +839,10 @@ const QSet expectedHiddenItemFields = { QSet HiddenItemEvent::getExpectedFields() { QSet expectedFields = QSet(); expectedFields = expectedHiddenItemFields; - if (projectConfig.getHiddenItemQuantityEnabled()) { + if (projectConfig.hiddenItemQuantityEnabled) { expectedFields << "quantity"; } - if (projectConfig.getHiddenItemRequiresItemfinderEnabled()) { + if (projectConfig.hiddenItemRequiresItemfinderEnabled) { expectedFields << "underfoot"; } expectedFields << "x" << "y"; @@ -930,10 +930,10 @@ OrderedJson::object HealLocationEvent::buildEventJson(Project *) { } void HealLocationEvent::setDefaultValues(Project *) { - this->setElevation(projectConfig.getDefaultElevation()); + this->setElevation(projectConfig.defaultElevation); if (!this->getMap()) return; - bool respawnEnabled = projectConfig.getHealLocationRespawnDataEnabled(); + bool respawnEnabled = projectConfig.healLocationRespawnDataEnabled; const QString mapConstant = Map::mapConstantFromName(this->getMap()->name, false); const QString prefix = projectConfig.getIdentifier(respawnEnabled ? ProjectIdentifier::define_spawn_prefix : ProjectIdentifier::define_heal_locations_prefix); diff --git a/src/core/heallocation.cpp b/src/core/heallocation.cpp index 42d444af..7fb424da 100644 --- a/src/core/heallocation.cpp +++ b/src/core/heallocation.cpp @@ -24,7 +24,7 @@ HealLocation HealLocation::fromEvent(Event *fromEvent) { healLocation.index = event->getIndex(); healLocation.x = event->getX(); healLocation.y = event->getY(); - if (projectConfig.getHealLocationRespawnDataEnabled()) { + if (projectConfig.healLocationRespawnDataEnabled) { healLocation.respawnNPC = event->getRespawnNPC(); healLocation.respawnMap = Map::mapConstantFromName(event->getRespawnMap(), false); } diff --git a/src/core/map.cpp b/src/core/map.cpp index a0ecf404..7e584db8 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -499,9 +499,9 @@ QStringList Map::getScriptLabels(Event::Group group) { } QString Map::getScriptsFilePath() const { - const bool usePoryscript = projectConfig.getUsePoryScript(); + const bool usePoryscript = projectConfig.usePoryScript; auto path = QDir::cleanPath(QString("%1/%2/%3/scripts") - .arg(projectConfig.getProjectDir()) + .arg(projectConfig.projectDir) .arg(projectConfig.getFilePath(ProjectFilePath::data_map_folders)) .arg(this->name)); auto extension = Project::getScriptFileExtension(usePoryscript); diff --git a/src/core/metatile.cpp b/src/core/metatile.cpp index 1a3dc11b..5f79129c 100644 --- a/src/core/metatile.cpp +++ b/src/core/metatile.cpp @@ -100,7 +100,7 @@ uint32_t Metatile::getMaxAttributesMask() { {2, 0xFFFF}, {4, 0xFFFFFFFF}, }; - return maxMasks.value(projectConfig.getMetatileAttributesSize(), 0); + return maxMasks.value(projectConfig.metatileAttributesSize, 0); } void Metatile::setLayout(Project * project) { @@ -109,10 +109,10 @@ void Metatile::setLayout(Project * project) { for (uint16_t i = Block::getMaxMetatileId(); i > 0; i /= 16) numMetatileIdChars++; - uint32_t behaviorMask = projectConfig.getMetatileBehaviorMask(); - uint32_t terrainTypeMask = projectConfig.getMetatileTerrainTypeMask(); - uint32_t encounterTypeMask = projectConfig.getMetatileEncounterTypeMask(); - uint32_t layerTypeMask = projectConfig.getMetatileLayerTypeMask(); + uint32_t behaviorMask = projectConfig.metatileBehaviorMask; + uint32_t terrainTypeMask = projectConfig.metatileTerrainTypeMask; + uint32_t encounterTypeMask = projectConfig.metatileEncounterTypeMask; + uint32_t layerTypeMask = projectConfig.metatileLayerTypeMask; // Calculate mask of bits not used by standard behaviors so we can preserve this data. uint32_t unusedMask = ~(behaviorMask | terrainTypeMask | encounterTypeMask | layerTypeMask); diff --git a/src/core/metatileparser.cpp b/src/core/metatileparser.cpp index 5b399328..104b757a 100644 --- a/src/core/metatileparser.cpp +++ b/src/core/metatileparser.cpp @@ -80,7 +80,7 @@ QList MetatileParser::parse(QString filepath, bool *error, bool prima // AdvanceMap .bvd files only contain 8 tiles of data per metatile. // If the user has triple-layer metatiles enabled we need to fill the remaining 4 tiles ourselves. - if (projectConfig.getTripleLayerMetatilesEnabled()) { + if (projectConfig.tripleLayerMetatilesEnabled) { Tile tile = Tile(); for (int j = 0; j < 4; j++) tiles.append(tile); diff --git a/src/core/network.cpp b/src/core/network.cpp index 94594d68..4f2a1c19 100644 --- a/src/core/network.cpp +++ b/src/core/network.cpp @@ -9,15 +9,10 @@ static const int DefaultWaitTime = 120; NetworkAccessManager::NetworkAccessManager(QObject * parent) : QNetworkAccessManager(parent) { - // We store rate limit end times in the user's config so that Porymap will still respect them after a restart. - // To avoid reading/writing to a local file during network operations, we only read/write the file when the - // manager is created/destroyed respectively. - this->rateLimitTimes = porymapConfig.getRateLimitTimes(); this->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy); }; NetworkAccessManager::~NetworkAccessManager() { - porymapConfig.setRateLimitTimes(this->rateLimitTimes); qDeleteAll(this->cache); } @@ -47,8 +42,9 @@ NetworkReplyData * NetworkAccessManager::get(const QUrl &url) { data->m_url = url; // If we are rate-limited, don't send a new request. - if (this->rateLimitTimes.contains(url)) { - auto time = this->rateLimitTimes.value(url); + // We store rate limit end times in the user's config so that Porymap will still respect them after a restart. + if (porymapConfig.rateLimitTimes.contains(url)) { + auto time = porymapConfig.rateLimitTimes.value(url); if (!time.isNull() && time > QDateTime::currentDateTime()) { data->m_retryAfter = time; data->m_error = QString("Rate limit reached. Please try again after %1.").arg(data->m_retryAfter.toString()); @@ -56,7 +52,7 @@ NetworkReplyData * NetworkAccessManager::get(const QUrl &url) { return data; } // Rate limiting expired - this->rateLimitTimes.remove(url); + porymapConfig.rateLimitTimes.remove(url); } QNetworkReply * reply = QNetworkAccessManager::get(this->getRequest(url)); @@ -117,7 +113,7 @@ void NetworkAccessManager::processReply(QNetworkReply * reply, NetworkReplyData data->m_error = "Service busy or unavailable. "; } data->m_error.append(QString("Please try again after %1.").arg(data->m_retryAfter.toString())); - this->rateLimitTimes.insert(url, data->m_retryAfter); + porymapConfig.rateLimitTimes.insert(url, data->m_retryAfter); return; } @@ -129,7 +125,7 @@ void NetworkAccessManager::processReply(QNetworkReply * reply, NetworkReplyData data->m_retryAfter = ok ? QDateTime::fromSecsSinceEpoch(limitReset).toLocalTime() : QDateTime::currentDateTime().addSecs(DefaultWaitTime);; data->m_error = QString("Too many requests. Please try again after %1.").arg(data->m_retryAfter.toString()); - this->rateLimitTimes.insert(url, data->m_retryAfter); + porymapConfig.rateLimitTimes.insert(url, data->m_retryAfter); return; } diff --git a/src/core/tileset.cpp b/src/core/tileset.cpp index 85fab154..7e5729ee 100644 --- a/src/core/tileset.cpp +++ b/src/core/tileset.cpp @@ -238,7 +238,7 @@ bool Tileset::appendToHeaders(QString root, QString friendlyName, bool usingAsm) dataString.append(QString("\t.4byte gTilesetTiles_%1\n").arg(friendlyName)); dataString.append(QString("\t.4byte gTilesetPalettes_%1\n").arg(friendlyName)); dataString.append(QString("\t.4byte gMetatiles_%1\n").arg(friendlyName)); - if (projectConfig.getBaseGameVersion() == BaseGameVersion::pokefirered) { + if (projectConfig.baseGameVersion == BaseGameVersion::pokefirered) { dataString.append("\t.4byte NULL @ animation callback\n"); dataString.append(QString("\t.4byte gMetatileAttributes_%1\n").arg(friendlyName)); } else { @@ -248,13 +248,13 @@ bool Tileset::appendToHeaders(QString root, QString friendlyName, bool usingAsm) } else { // Append to C file dataString.append(QString("const struct Tileset %1 =\n{\n").arg(this->name)); - if (projectConfig.getTilesetsHaveIsCompressed()) dataString.append(" .isCompressed = TRUE,\n"); + if (projectConfig.tilesetsHaveIsCompressed) dataString.append(" .isCompressed = TRUE,\n"); dataString.append(QString(" .isSecondary = %1,\n").arg(isSecondaryStr)); dataString.append(QString(" .tiles = gTilesetTiles_%1,\n").arg(friendlyName)); dataString.append(QString(" .palettes = gTilesetPalettes_%1,\n").arg(friendlyName)); dataString.append(QString(" .metatiles = gMetatiles_%1,\n").arg(friendlyName)); dataString.append(QString(" .metatileAttributes = gMetatileAttributes_%1,\n").arg(friendlyName)); - if (projectConfig.getTilesetsHaveCallback()) dataString.append(" .callback = NULL,\n"); + if (projectConfig.tilesetsHaveCallback) dataString.append(" .callback = NULL,\n"); dataString.append("};\n"); } file.write(dataString.toUtf8()); @@ -325,7 +325,7 @@ bool Tileset::appendToMetatiles(QString root, QString friendlyName, bool usingAs } else { // Append to C file dataString.append(QString("const u16 gMetatiles_%1[] = INCBIN_U16(\"%2\");\n").arg(friendlyName, metatilesPath)); - QString numBits = QString::number(projectConfig.getMetatileAttributesSize() * 8); + QString numBits = QString::number(projectConfig.metatileAttributesSize * 8); dataString.append(QString("const u%1 gMetatileAttributes_%2[] = INCBIN_U%1(\"%3\");\n").arg(numBits, friendlyName, metatileAttrsPath)); } file.write(dataString.toUtf8()); @@ -358,7 +358,7 @@ QHash Tileset::getHeaderMemberMap(bool usingAsm) int paddingOffset = usingAsm ? 1 : 0; // The position of metatileAttributes changes between games - bool isPokefirered = (projectConfig.getBaseGameVersion() == BaseGameVersion::pokefirered); + bool isPokefirered = (projectConfig.baseGameVersion == BaseGameVersion::pokefirered); int metatileAttrPosition = (isPokefirered ? 6 : 5) + paddingOffset; auto map = QHash(); diff --git a/src/editor.cpp b/src/editor.cpp index a2e4f3f9..9557ee13 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -1501,7 +1501,7 @@ void Editor::displayMovementPermissionSelector() { connect(movement_permissions_selector_item, &SelectablePixmapItem::selectionChanged, [this](int x, int y, int, int) { this->setCollisionTabSpinBoxes(x, y); }); - movement_permissions_selector_item->select(projectConfig.getDefaultCollision(), projectConfig.getDefaultElevation()); + movement_permissions_selector_item->select(projectConfig.defaultCollision, projectConfig.defaultElevation); } scene_collision_metatiles->addItem(movement_permissions_selector_item); @@ -1684,7 +1684,7 @@ int Editor::getBorderDrawDistance(int dimension) { } void Editor::onToggleGridClicked(bool checked) { - porymapConfig.setShowGrid(checked); + porymapConfig.showGrid = checked; if (ui->graphicsView_Map->scene()) ui->graphicsView_Map->scene()->update(); } @@ -1947,7 +1947,7 @@ void Editor::toggleBorderVisibility(bool visible, bool enableScriptCallback) { this->setBorderItemsVisible(visible); this->setConnectionItemsVisible(visible); - porymapConfig.setShowBorder(visible); + porymapConfig.showBorder = visible; if (enableScriptCallback) Scripting::cb_BorderVisibilityToggled(visible); } @@ -1993,7 +1993,7 @@ void Editor::redrawObject(DraggablePixmapItem *item) { // Warp events display a warning if they're not positioned on a metatile with a warp behavior. void Editor::updateWarpEventWarning(Event *event) { - if (porymapConfig.getWarpBehaviorWarningDisabled()) + if (porymapConfig.warpBehaviorWarningDisabled) return; if (!project || !map || !event || event->getEventType() != Event::Type::Warp) return; @@ -2004,7 +2004,7 @@ void Editor::updateWarpEventWarning(Event *event) { metatile = Tileset::getMetatile(block.metatileId(), map->layout->tileset_primary, map->layout->tileset_secondary); } // metatile may be null if the warp is in the map border. Display the warning in this case - bool validWarpBehavior = metatile && projectConfig.getWarpBehaviors().contains(metatile->behavior()); + bool validWarpBehavior = metatile && projectConfig.warpBehaviors.contains(metatile->behavior()); warpEvent->setWarningEnabled(!validWarpBehavior); } @@ -2014,7 +2014,7 @@ void Editor::updateWarpEventWarning(Event *event) { // events when the Events tab is opened. This does not cover the case where metatiles are painted while // still on the Events tab, such as by Undo/Redo or the scripting API. void Editor::updateWarpEventWarnings() { - if (porymapConfig.getWarpBehaviorWarningDisabled()) + if (porymapConfig.warpBehaviorWarningDisabled) return; if (selected_events) { for (auto selection : *selected_events) @@ -2189,7 +2189,7 @@ void Editor::openScript(const QString &scriptLabel) const { } void Editor::openInTextEditor(const QString &path, int lineNum) { - QString command = porymapConfig.getTextEditorGotoLine(); + QString command = porymapConfig.textEditorGotoLine; if (command.isEmpty()) { // Open map scripts in the system's default editor. QDesktopServices::openUrl(QUrl::fromLocalFile(path)); @@ -2206,7 +2206,7 @@ void Editor::openInTextEditor(const QString &path, int lineNum) { } void Editor::openProjectInTextEditor() const { - QString command = porymapConfig.getTextEditorOpenFolder(); + QString command = porymapConfig.textEditorOpenFolder; if (command.contains("%D")) command.replace("%D", '\"' + project->root + '\"'); else @@ -2279,7 +2279,7 @@ void Editor::setCollisionTabSpinBoxes(uint16_t collision, uint16_t elevation) { // Custom collision graphics may be provided by the user. void Editor::setCollisionGraphics() { - QString filepath = projectConfig.getCollisionSheetPath(); + QString filepath = projectConfig.collisionSheetPath; QImage imgSheet; if (filepath.isEmpty()) { @@ -2299,8 +2299,8 @@ void Editor::setCollisionGraphics() { // Users are not required to provide an image that gives an icon for every elevation/collision combination. // Instead they tell us how many are provided in their image by specifying the number of columns and rows. - const int imgColumns = projectConfig.getCollisionSheetWidth(); - const int imgRows = projectConfig.getCollisionSheetHeight(); + const int imgColumns = projectConfig.collisionSheetWidth; + const int imgRows = projectConfig.collisionSheetHeight; // Create a pixmap for the selector on the Collision tab. If a project was previously opened we'll also need to refresh the selector. this->collisionSheetPixmap = QPixmap::fromImage(imgSheet).scaled(MovementPermissionsSelector::CellWidth * imgColumns, diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 981dc04e..3b48da1e 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -70,14 +70,14 @@ MainWindow::MainWindow(QWidget *parent) : logInfo(QString("Launching Porymap v%1").arg(QCoreApplication::applicationVersion())); this->initWindow(); - if (porymapConfig.getReopenOnLaunch() && this->openProject(porymapConfig.getRecentProject(), true)) + if (porymapConfig.reopenOnLaunch && this->openProject(porymapConfig.getRecentProject(), true)) on_toolButton_Paint_clicked(); // there is a bug affecting macOS users, where the trackpad deilveres a bad touch-release gesture // the warning is a bit annoying, so it is disabled here QLoggingCategory::setFilterRules(QStringLiteral("qt.pointer.dispatch=false")); - if (porymapConfig.getCheckForUpdates()) + if (porymapConfig.checkForUpdates) this->checkForUpdates(false); } @@ -285,12 +285,12 @@ void MainWindow::checkForUpdates(bool requestedByUser) { openSubWindow(this->updatePromoter); } else { // This is an automatic update check. Only run if we haven't done one in the last 5 minutes - QDateTime lastCheck = porymapConfig.getLastUpdateCheckTime(); + QDateTime lastCheck = porymapConfig.lastUpdateCheckTime; if (lastCheck.addSecs(5*60) >= QDateTime::currentDateTime()) return; } this->updatePromoter->checkForUpdates(); - porymapConfig.setLastUpdateCheckTime(QDateTime::currentDateTime()); + porymapConfig.lastUpdateCheckTime = QDateTime::currentDateTime(); } #else void MainWindow::checkForUpdates(bool) {} @@ -420,7 +420,7 @@ void MainWindow::setProjectSpecificUI() // TODO: This index should come from an enum ui->mainTabBar->setTabEnabled(4, editor->project->wildEncountersLoaded); - bool hasFlags = projectConfig.getMapAllowFlagsEnabled(); + bool hasFlags = projectConfig.mapAllowFlagsEnabled; ui->checkBox_AllowRunning->setVisible(hasFlags); ui->checkBox_AllowBiking->setVisible(hasFlags); ui->checkBox_AllowEscaping->setVisible(hasFlags); @@ -428,11 +428,11 @@ void MainWindow::setProjectSpecificUI() ui->label_AllowBiking->setVisible(hasFlags); ui->label_AllowEscaping->setVisible(hasFlags); - ui->newEventToolButton->newWeatherTriggerAction->setVisible(projectConfig.getEventWeatherTriggerEnabled()); - ui->newEventToolButton->newSecretBaseAction->setVisible(projectConfig.getEventSecretBaseEnabled()); - ui->newEventToolButton->newCloneObjectAction->setVisible(projectConfig.getEventCloneObjectEnabled()); + ui->newEventToolButton->newWeatherTriggerAction->setVisible(projectConfig.eventWeatherTriggerEnabled); + ui->newEventToolButton->newSecretBaseAction->setVisible(projectConfig.eventSecretBaseEnabled); + ui->newEventToolButton->newCloneObjectAction->setVisible(projectConfig.eventCloneObjectEnabled); - bool floorNumEnabled = projectConfig.getFloorNumberEnabled(); + bool floorNumEnabled = projectConfig.floorNumberEnabled; ui->spinBox_FloorNumber->setVisible(floorNumEnabled); ui->label_FloorNumber->setVisible(floorNumEnabled); @@ -457,8 +457,7 @@ void MainWindow::mapSortOrder_changed(QAction *action) if (i != mapSortOrder) { ui->toolButton_MapSortOrder->setIcon(action->icon()); - mapSortOrder = static_cast(i); - porymapConfig.setMapSortOrder(mapSortOrder); + porymapConfig.mapSortOrder = static_cast(i); if (isProjectOpen()) { sortMapList(); @@ -485,26 +484,26 @@ void MainWindow::applyMapListFilter(QString filterText) } void MainWindow::loadUserSettings() { - ui->actionBetter_Cursors->setChecked(porymapConfig.getPrettyCursors()); - this->editor->settings->betterCursors = porymapConfig.getPrettyCursors(); - ui->actionPlayer_View_Rectangle->setChecked(porymapConfig.getShowPlayerView()); - this->editor->settings->playerViewRectEnabled = porymapConfig.getShowPlayerView(); - ui->actionCursor_Tile_Outline->setChecked(porymapConfig.getShowCursorTile()); - this->editor->settings->cursorTileRectEnabled = porymapConfig.getShowCursorTile(); - ui->checkBox_ToggleBorder->setChecked(porymapConfig.getShowBorder()); - ui->checkBox_ToggleGrid->setChecked(porymapConfig.getShowGrid()); - mapSortOrder = porymapConfig.getMapSortOrder(); + ui->actionBetter_Cursors->setChecked(porymapConfig.prettyCursors); + this->editor->settings->betterCursors = porymapConfig.prettyCursors; + ui->actionPlayer_View_Rectangle->setChecked(porymapConfig.showPlayerView); + this->editor->settings->playerViewRectEnabled = porymapConfig.showPlayerView; + ui->actionCursor_Tile_Outline->setChecked(porymapConfig.showCursorTile); + this->editor->settings->cursorTileRectEnabled = porymapConfig.showCursorTile; + ui->checkBox_ToggleBorder->setChecked(porymapConfig.showBorder); + ui->checkBox_ToggleGrid->setChecked(porymapConfig.showGrid); + mapSortOrder = porymapConfig.mapSortOrder; ui->horizontalSlider_CollisionTransparency->blockSignals(true); - this->editor->collisionOpacity = static_cast(porymapConfig.getCollisionOpacity()) / 100; - ui->horizontalSlider_CollisionTransparency->setValue(porymapConfig.getCollisionOpacity()); + this->editor->collisionOpacity = static_cast(porymapConfig.collisionOpacity) / 100; + ui->horizontalSlider_CollisionTransparency->setValue(porymapConfig.collisionOpacity); ui->horizontalSlider_CollisionTransparency->blockSignals(false); ui->horizontalSlider_MetatileZoom->blockSignals(true); - ui->horizontalSlider_MetatileZoom->setValue(porymapConfig.getMetatilesZoom()); + ui->horizontalSlider_MetatileZoom->setValue(porymapConfig.metatilesZoom); ui->horizontalSlider_MetatileZoom->blockSignals(false); ui->horizontalSlider_CollisionZoom->blockSignals(true); - ui->horizontalSlider_CollisionZoom->setValue(porymapConfig.getCollisionZoom()); + ui->horizontalSlider_CollisionZoom->setValue(porymapConfig.collisionZoom); ui->horizontalSlider_CollisionZoom->blockSignals(false); - setTheme(porymapConfig.getTheme()); + setTheme(porymapConfig.theme); } void MainWindow::restoreWindowState() { @@ -558,32 +557,38 @@ bool MainWindow::openProject(const QString &dir, bool initial) { this->statusBar()->showMessage(openMessage); logInfo(openMessage); - // TODO: Don't save these yet - userConfig.setProjectDir(dir); + userConfig.projectDir = dir; userConfig.load(); - projectConfig.setProjectDir(dir); + projectConfig.projectDir = dir; projectConfig.load(); this->newMapDefaultsSet = false; Scripting::init(this); + // Create the project this->editor->project = new Project(this); QObject::connect(this->editor->project, &Project::reloadProject, this, &MainWindow::on_action_Reload_Project_triggered); QObject::connect(this->editor->project, &Project::mapCacheCleared, this, &MainWindow::onMapCacheCleared); QObject::connect(this->editor->project, &Project::uncheckMonitorFilesAction, [this]() { - porymapConfig.setMonitorFiles(false); + porymapConfig.monitorFiles = false; if (this->preferenceEditor) this->preferenceEditor->updateFields(); }); this->editor->project->set_root(dir); + // Load the project if (!(loadDataStructures() && populateMapList() && setInitialMap())) { this->statusBar()->showMessage(QString("Failed to open %1").arg(projectString)); showProjectOpenFailure(); delete this->editor->project; + // TODO: Allow changing project settings at this point return false; } + + // Only create the config files once the project has opened successfully in case the user selected an invalid directory + userConfig.save(); + projectConfig.save(); showWindowTitle(); this->statusBar()->showMessage(QString("Opened %1").arg(projectString)); @@ -619,7 +624,7 @@ bool MainWindow::setInitialMap() { names = editor->project->mapNames; // Try to set most recently-opened map, if it's still in the list. - QString recentMap = userConfig.getRecentMap(); + QString recentMap = userConfig.recentMap; if (!recentMap.isEmpty() && names.contains(recentMap) && setMap(recentMap, true)) return true; @@ -687,11 +692,7 @@ QString MainWindow::getExistingDirectory(QString dir) { void MainWindow::on_action_Open_Project_triggered() { - QString recent = "."; - if (!userConfig.getRecentMap().isEmpty()) { - recent = userConfig.getRecentMap(); - } - QString dir = getExistingDirectory(recent); + QString dir = getExistingDirectory(!userConfig.recentMap.isEmpty() ? userConfig.recentMap : "."); if (!dir.isEmpty()) openProject(dir); } @@ -743,7 +744,7 @@ bool MainWindow::setMap(QString map_name, bool scrollTreeView) { connect(editor->map, &Map::mapNeedsRedrawing, this, &MainWindow::onMapNeedsRedrawing); connect(editor->map, &Map::modified, [this](){ this->markMapEdited(); }); - setRecentMap(map_name); + userConfig.recentMap = map_name; updateMapList(); Scripting::cb_MapOpened(map_name); @@ -829,10 +830,6 @@ void MainWindow::openWarpMap(QString map_name, int event_id, Event::Group event_ } } -void MainWindow::setRecentMap(QString mapName) { - userConfig.setRecentMap(mapName); -} - void MainWindow::displayMapProperties() { // Block signals to the comboboxes while they are being modified const QSignalBlocker blocker1(ui->comboBox_Song); @@ -1407,7 +1404,7 @@ void MainWindow::updateTilesetEditor() { } double MainWindow::getMetatilesZoomScale() { - return pow(3.0, static_cast(porymapConfig.getMetatilesZoom() - 30) / 30.0); + return pow(3.0, static_cast(porymapConfig.metatilesZoom - 30) / 30.0); } void MainWindow::redrawMetatileSelection() { @@ -1541,8 +1538,8 @@ void MainWindow::copy() { collisions.clear(); for (int i = 0; i < metatiles.length(); i++) { OrderedJson::object collision; - collision["collision"] = projectConfig.getDefaultCollision(); - collision["elevation"] = projectConfig.getDefaultElevation(); + collision["collision"] = projectConfig.defaultCollision; + collision["elevation"] = projectConfig.defaultElevation; collisions.append(collision); } } @@ -1781,10 +1778,10 @@ void MainWindow::on_mapViewTab_tabBarClicked(int index) editor->setEditingCollision(); } else if (index == 2) { editor->setEditingMap(); - if (projectConfig.getPrefabFilepath().isEmpty() && !projectConfig.getPrefabImportPrompted()) { + if (projectConfig.prefabFilepath.isEmpty() && !projectConfig.prefabImportPrompted) { // User hasn't set up prefabs and hasn't been prompted before. // Ask if they'd like to import the default prefabs file. - if (prefab.tryImportDefaultPrefabs(this, projectConfig.getBaseGameVersion())) + if (prefab.tryImportDefaultPrefabs(this, projectConfig.baseGameVersion)) prefab.updatePrefabUi(this->editor->map); } } @@ -1830,14 +1827,14 @@ void MainWindow::on_actionZoom_Out_triggered() { } void MainWindow::on_actionBetter_Cursors_triggered() { - porymapConfig.setPrettyCursors(ui->actionBetter_Cursors->isChecked()); + porymapConfig.prettyCursors = ui->actionBetter_Cursors->isChecked(); this->editor->settings->betterCursors = ui->actionBetter_Cursors->isChecked(); } void MainWindow::on_actionPlayer_View_Rectangle_triggered() { bool enabled = ui->actionPlayer_View_Rectangle->isChecked(); - porymapConfig.setShowPlayerView(enabled); + porymapConfig.showPlayerView = enabled; this->editor->settings->playerViewRectEnabled = enabled; if ((this->editor->map_item && this->editor->map_item->has_mouse) || (this->editor->collision_item && this->editor->collision_item->has_mouse)) { @@ -1849,7 +1846,7 @@ void MainWindow::on_actionPlayer_View_Rectangle_triggered() void MainWindow::on_actionCursor_Tile_Outline_triggered() { bool enabled = ui->actionCursor_Tile_Outline->isChecked(); - porymapConfig.setShowCursorTile(enabled); + porymapConfig.showCursorTile = enabled; this->editor->settings->cursorTileRectEnabled = enabled; if ((this->editor->map_item && this->editor->map_item->has_mouse) || (this->editor->collision_item && this->editor->collision_item->has_mouse)) { @@ -2198,7 +2195,7 @@ void MainWindow::eventTabChanged(int index) { void MainWindow::on_horizontalSlider_CollisionTransparency_valueChanged(int value) { this->editor->collisionOpacity = static_cast(value) / 100; - porymapConfig.setCollisionOpacity(value); + porymapConfig.collisionOpacity = value; this->editor->collision_item->draw(true); } @@ -2635,7 +2632,7 @@ void MainWindow::on_pushButton_ChangeDimensions_clicked() heightSpinBox->setValue(editor->map->getHeight()); bwidthSpinBox->setValue(editor->map->getBorderWidth()); bheightSpinBox->setValue(editor->map->getBorderHeight()); - if (projectConfig.getUseCustomBorderSize()) { + if (projectConfig.useCustomBorderSize) { form.addRow(new QLabel("Map Width"), widthSpinBox); form.addRow(new QLabel("Map Height"), heightSpinBox); form.addRow(new QLabel("Border Width"), bwidthSpinBox); @@ -2777,7 +2774,7 @@ void MainWindow::on_actionPreferences_triggered() { } void MainWindow::togglePreferenceSpecificUi() { - if (porymapConfig.getTextEditorOpenFolder().isEmpty()) + if (porymapConfig.textEditorOpenFolder.isEmpty()) ui->actionOpen_Project_in_Text_Editor->setEnabled(false); else ui->actionOpen_Project_in_Text_Editor->setEnabled(true); @@ -2797,7 +2794,7 @@ void MainWindow::openProjectSettingsEditor(int tab) { } void MainWindow::on_actionProject_Settings_triggered() { - this->openProjectSettingsEditor(porymapConfig.getProjectSettingsTab()); + this->openProjectSettingsEditor(porymapConfig.projectSettingsTab); } void MainWindow::onWarpBehaviorWarningClicked() { @@ -2843,7 +2840,7 @@ void MainWindow::reloadScriptEngine() { Scripting::init(this); Scripting::populateGlobalObject(this); // Lying to the scripts here, simulating a project reload - Scripting::cb_ProjectOpened(projectConfig.getProjectDir()); + Scripting::cb_ProjectOpened(projectConfig.projectDir); if (editor && editor->map) Scripting::cb_MapOpened(editor->map->name); } @@ -2870,7 +2867,7 @@ void MainWindow::on_tableWidget_CustomHeaderFields_cellChanged(int, int) } void MainWindow::on_horizontalSlider_MetatileZoom_valueChanged(int value) { - porymapConfig.setMetatilesZoom(value); + porymapConfig.metatilesZoom = value; double scale = pow(3.0, static_cast(value - 30) / 30.0); QTransform transform; @@ -2895,7 +2892,7 @@ void MainWindow::on_horizontalSlider_MetatileZoom_valueChanged(int value) { } void MainWindow::on_horizontalSlider_CollisionZoom_valueChanged(int value) { - porymapConfig.setCollisionZoom(value); + porymapConfig.collisionZoom = value; double scale = pow(3.0, static_cast(value - 30) / 30.0); QTransform transform; @@ -2981,7 +2978,6 @@ bool MainWindow::askToFixRegionMapEditor() { // Attempt to close any open sub-windows of the main window, giving each a chance to abort the process. // Each of these are expected to be a QPointer to a widget with WA_DeleteOnClose set, so manually deleting // and nullifying the pointer members is not necessary here. -// TODO: Testing bool MainWindow::closeSupplementaryWindows() { if (this->tilesetEditor && !this->tilesetEditor->close()) return false; @@ -3022,9 +3018,6 @@ bool MainWindow::closeProject() { return false; } } - projectConfig.save(); - userConfig.save(); - editor->closeProject(); return true; diff --git a/src/project.cpp b/src/project.cpp index c6550c6a..d50c305e 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -49,7 +49,7 @@ Project::~Project() void Project::initSignals() { // detect changes to specific filepaths being monitored QObject::connect(&fileWatcher, &QFileSystemWatcher::fileChanged, [this](QString changed){ - if (!porymapConfig.getMonitorFiles()) return; + if (!porymapConfig.monitorFiles) return; if (modifiedFileTimestamps.contains(changed)) { if (QDateTime::currentMSecsSinceEpoch() < modifiedFileTimestamps[changed]) { return; @@ -77,7 +77,7 @@ void Project::initSignals() { emit reloadProject(); } else if (choice == QMessageBox::No) { if (showAgainCheck.isChecked()) { - porymapConfig.setMonitorFiles(false); + porymapConfig.monitorFiles = false; emit uncheckMonitorFilesAction(); } } @@ -162,13 +162,13 @@ const QSet defaultTopLevelMapFields = { QSet Project::getTopLevelMapFields() { QSet topLevelMapFields = defaultTopLevelMapFields; - if (projectConfig.getMapAllowFlagsEnabled()) { + if (projectConfig.mapAllowFlagsEnabled) { topLevelMapFields.insert("allow_cycling"); topLevelMapFields.insert("allow_escaping"); topLevelMapFields.insert("allow_running"); } - if (projectConfig.getFloorNumberEnabled()) { + if (projectConfig.floorNumberEnabled) { topLevelMapFields.insert("floor_number"); } return topLevelMapFields; @@ -197,12 +197,12 @@ bool Project::loadMapData(Map* map) { map->show_location = ParseUtil::jsonToBool(mapObj["show_map_name"]); map->battle_scene = ParseUtil::jsonToQString(mapObj["battle_scene"]); - if (projectConfig.getMapAllowFlagsEnabled()) { + if (projectConfig.mapAllowFlagsEnabled) { map->allowBiking = ParseUtil::jsonToBool(mapObj["allow_cycling"]); map->allowEscaping = ParseUtil::jsonToBool(mapObj["allow_escaping"]); map->allowRunning = ParseUtil::jsonToBool(mapObj["allow_running"]); } - if (projectConfig.getFloorNumberEnabled()) { + if (projectConfig.floorNumberEnabled) { map->floorNumber = ParseUtil::jsonToInt(mapObj["floor_number"]); } map->sharedEventsMap = ParseUtil::jsonToQString(mapObj["shared_events_map"]); @@ -211,11 +211,10 @@ bool Project::loadMapData(Map* map) { // Events map->events[Event::Group::Object].clear(); QJsonArray objectEventsArr = mapObj["object_events"].toArray(); - bool hasCloneObjects = projectConfig.getEventCloneObjectEnabled(); for (int i = 0; i < objectEventsArr.size(); i++) { QJsonObject event = objectEventsArr[i].toObject(); // If clone objects are not enabled then no type field is present - QString type = hasCloneObjects ? ParseUtil::jsonToQString(event["type"]) : "object"; + QString type = projectConfig.eventCloneObjectEnabled ? ParseUtil::jsonToQString(event["type"]) : "object"; if (type.isEmpty() || type == "object") { ObjectEvent *object = new ObjectEvent(); object->loadFromJson(event, this); @@ -297,11 +296,11 @@ bool Project::loadMapData(Map* map) { heal->setMap(map); heal->setX(loc.x); heal->setY(loc.y); - heal->setElevation(projectConfig.getDefaultElevation()); + heal->setElevation(projectConfig.defaultElevation); heal->setLocationName(loc.mapName); heal->setIdName(loc.idName); heal->setIndex(loc.index); - if (projectConfig.getHealLocationRespawnDataEnabled()) { + if (projectConfig.healLocationRespawnDataEnabled) { heal->setRespawnMap(mapConstantsToMapNames.value(QString(mapPrefix + loc.respawnMap))); heal->setRespawnNPC(loc.respawnNPC); } @@ -438,7 +437,6 @@ bool Project::readMapLayouts() { "border_filepath", "blockdata_filepath", }; - bool useCustomBorderSize = projectConfig.getUseCustomBorderSize(); for (int i = 0; i < layouts.size(); i++) { QJsonObject layoutObj = layouts[i].toObject(); if (layoutObj.isEmpty()) @@ -479,7 +477,7 @@ bool Project::readMapLayouts() { return false; } layout->height = lheight; - if (useCustomBorderSize) { + if (projectConfig.useCustomBorderSize) { int bwidth = ParseUtil::jsonToInt(layoutObj["border_width"]); if (bwidth <= 0) { // 0 is an expected border width/height that should be handled, GF used it for the RS layouts in FRLG logWarn(QString("Invalid 'border_width' value '%1' for %2 in %3. Must be greater than 0. Using default (%4) instead.") @@ -545,7 +543,6 @@ void Project::saveMapLayouts() { OrderedJson::object layoutsObj; layoutsObj["layouts_table_label"] = layoutsLabel; - bool useCustomBorderSize = projectConfig.getUseCustomBorderSize(); OrderedJson::array layoutsArr; for (QString layoutId : mapLayoutsTableMaster) { MapLayout *layout = mapLayouts.value(layoutId); @@ -554,7 +551,7 @@ void Project::saveMapLayouts() { layoutObj["name"] = layout->name; layoutObj["width"] = layout->width; layoutObj["height"] = layout->height; - if (useCustomBorderSize) { + if (projectConfig.useCustomBorderSize) { layoutObj["border_width"] = layout->border_width; layoutObj["border_height"] = layout->border_height; } @@ -768,7 +765,7 @@ void Project::saveHealLocationsData(Map *map) { } // Create the definition text for each data table - bool respawnEnabled = projectConfig.getHealLocationRespawnDataEnabled(); + bool respawnEnabled = projectConfig.healLocationRespawnDataEnabled; const QString qualifiers = QString(healLocationDataQualifiers.isStatic ? "static " : "") + QString(healLocationDataQualifiers.isConst ? "const " : ""); @@ -951,10 +948,9 @@ void Project::saveTilesetMetatileAttributes(Tileset *tileset) { QFile attrs_file(tileset->metatile_attrs_path); if (attrs_file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { QByteArray data; - int attrSize = projectConfig.getMetatileAttributesSize(); for (Metatile *metatile : tileset->metatiles) { uint32_t attributes = metatile->getAttributes(); - for (int i = 0; i < attrSize; i++) + for (int i = 0; i < projectConfig.metatileAttributesSize; i++) data.append(static_cast(attributes >> (8 * i))); } attrs_file.write(data); @@ -1090,7 +1086,7 @@ void Project::setNewMapBlockdata(Map *map) { map->layout->blockdata.clear(); int width = map->getWidth(); int height = map->getHeight(); - Block block(projectConfig.getDefaultMetatileId(), projectConfig.getDefaultCollision(), projectConfig.getDefaultElevation()); + Block block(projectConfig.defaultMetatileId, projectConfig.defaultCollision, projectConfig.defaultElevation); for (int i = 0; i < width * height; i++) { map->layout->blockdata.append(block); } @@ -1119,8 +1115,7 @@ void Project::setNewMapBorder(Map *map) { int width = map->getBorderWidth(); int height = map->getBorderHeight(); - const QList configMetatileIds = projectConfig.getNewMapBorderMetatileIds(); - if (configMetatileIds.length() != width * height) { + if (projectConfig.newMapBorderMetatileIds.length() != width * height) { // Border size doesn't match the number of default border metatiles. // Fill the border with empty metatiles. for (int i = 0; i < width * height; i++) { @@ -1129,7 +1124,7 @@ void Project::setNewMapBorder(Map *map) { } else { // Fill the border with the default metatiles from the config. for (int i = 0; i < width * height; i++) { - map->layout->border.append(configMetatileIds.at(i)); + map->layout->border.append(projectConfig.newMapBorderMetatileIds.at(i)); } } @@ -1172,18 +1167,17 @@ void Project::saveMap(Map *map) { } // Create file data/maps//scripts.inc - QString text = this->getScriptDefaultString(projectConfig.getUsePoryScript(), map->name); - saveTextFile(mapDataDir + "/scripts" + this->getScriptFileExtension(projectConfig.getUsePoryScript()), text); + QString text = this->getScriptDefaultString(projectConfig.usePoryScript, map->name); + saveTextFile(mapDataDir + "/scripts" + this->getScriptFileExtension(projectConfig.usePoryScript), text); - bool usesTextFile = projectConfig.getCreateMapTextFileEnabled(); - if (usesTextFile) { + if (projectConfig.createMapTextFileEnabled) { // Create file data/maps//text.inc - saveTextFile(mapDataDir + "/text" + this->getScriptFileExtension(projectConfig.getUsePoryScript()), "\n"); + saveTextFile(mapDataDir + "/text" + this->getScriptFileExtension(projectConfig.usePoryScript), "\n"); } // Simply append to data/event_scripts.s. text = QString("\n\t.include \"%1%2/scripts.inc\"\n").arg(basePath, map->name); - if (usesTextFile) { + if (projectConfig.createMapTextFileEnabled) { text += QString("\t.include \"%1%2/text.inc\"\n").arg(basePath, map->name); } appendTextFile(root + "/" + projectConfig.getFilePath(ProjectFilePath::data_event_scripts), text); @@ -1214,13 +1208,13 @@ void Project::saveMap(Map *map) { mapObj["requires_flash"] = map->requiresFlash; mapObj["weather"] = map->weather; mapObj["map_type"] = map->type; - if (projectConfig.getMapAllowFlagsEnabled()) { + if (projectConfig.mapAllowFlagsEnabled) { mapObj["allow_cycling"] = map->allowBiking; mapObj["allow_escaping"] = map->allowEscaping; mapObj["allow_running"] = map->allowRunning; } mapObj["show_map_name"] = map->show_location; - if (projectConfig.getFloorNumberEnabled()) { + if (projectConfig.floorNumberEnabled) { mapObj["floor_number"] = map->floorNumber; } mapObj["battle_scene"] = map->battle_scene; @@ -1328,6 +1322,12 @@ void Project::saveAllDataStructures() { saveMapGroups(); saveMapConstantsHeader(); saveWildMonData(); + saveConfig(); +} + +void Project::saveConfig() { + projectConfig.save(); + userConfig.save(); } void Project::loadTilesetAssets(Tileset* tileset) { @@ -1466,7 +1466,7 @@ void Project::loadTilesetMetatiles(Tileset* tileset) { if (attrs_file.open(QIODevice::ReadOnly)) { QByteArray data = attrs_file.readAll(); int num_metatiles = tileset->metatiles.count(); - int attrSize = projectConfig.getMetatileAttributesSize(); + int attrSize = projectConfig.metatileAttributesSize; int num_metatileAttrs = data.length() / attrSize; if (num_metatiles != num_metatileAttrs) { logWarn(QString("Metatile count %1 does not match metatile attribute count %2 in %3").arg(num_metatiles).arg(num_metatileAttrs).arg(tileset->name)); @@ -1603,7 +1603,7 @@ bool Project::readWildMonData() { wildMonData.clear(); encounterGroupLabels.clear(); this->wildEncountersLoaded = false; - if (!userConfig.getEncounterJsonActive()) { + if (!userConfig.useEncounterJson) { return true; } @@ -1813,7 +1813,7 @@ Project::DataQualifiers Project::getDataQualifiers(QString text, QString label) } QString Project::getDefaultPrimaryTilesetLabel() { - QString defaultLabel = projectConfig.getDefaultPrimaryTileset(); + QString defaultLabel = projectConfig.defaultPrimaryTileset; if (!this->primaryTilesetLabels.contains(defaultLabel)) { QString firstLabel = this->primaryTilesetLabels.first(); logWarn(QString("Unable to find default primary tileset '%1', using '%2' instead.").arg(defaultLabel).arg(firstLabel)); @@ -1823,7 +1823,7 @@ QString Project::getDefaultPrimaryTilesetLabel() { } QString Project::getDefaultSecondaryTilesetLabel() { - QString defaultLabel = projectConfig.getDefaultSecondaryTileset(); + QString defaultLabel = projectConfig.defaultSecondaryTileset; if (!this->secondaryTilesetLabels.contains(defaultLabel)) { QString firstLabel = this->secondaryTilesetLabels.first(); logWarn(QString("Unable to find default secondary tileset '%1', using '%2' instead.").arg(defaultLabel).arg(firstLabel)); @@ -1975,9 +1975,6 @@ bool Project::readFieldmapMasks() { const QStringList defineNames = defines.keys(); this->disabledSettingsNames = QSet(defineNames.constBegin(), defineNames.constEnd()); - // Avoid repeatedly writing the config file - projectConfig.setSaveDisabled(true); - // Read Block masks auto readBlockMask = [defines](const QString name, uint16_t *value) { auto it = defines.find(name); @@ -1992,21 +1989,22 @@ bool Project::readFieldmapMasks() { } return true; }; + uint16_t blockMask; if (readBlockMask(metatileIdMaskName, &blockMask)) - projectConfig.setBlockMetatileIdMask(blockMask); + projectConfig.blockMetatileIdMask = blockMask; if (readBlockMask(collisionMaskName, &blockMask)) - projectConfig.setBlockCollisionMask(blockMask); + projectConfig.blockCollisionMask = blockMask; if (readBlockMask(elevationMaskName, &blockMask)) - projectConfig.setBlockElevationMask(blockMask); + projectConfig.blockElevationMask = blockMask; // Read RSE metatile attribute masks auto it = defines.find(behaviorMaskName); if (it != defines.end()) - projectConfig.setMetatileBehaviorMask(static_cast(it.value())); + projectConfig.metatileBehaviorMask = static_cast(it.value()); it = defines.find(layerTypeMaskName); if (it != defines.end()) - projectConfig.setMetatileLayerTypeMask(static_cast(it.value())); + projectConfig.metatileLayerTypeMask = static_cast(it.value()); // pokefirered keeps its attribute masks in a separate table, parse this too. const QString attrTableName = projectConfig.getIdentifier(ProjectIdentifier::symbol_attribute_table); @@ -2023,13 +2021,13 @@ bool Project::readFieldmapMasks() { // Read terrain type mask uint32_t mask = attrTable.value(terrainTypeTableName).toUInt(&ok, 0); if (ok) { - projectConfig.setMetatileTerrainTypeMask(mask); + projectConfig.metatileTerrainTypeMask = mask; this->disabledSettingsNames.insert(terrainTypeTableName); } // Read encounter type mask mask = attrTable.value(encounterTypeTableName).toUInt(&ok, 0); if (ok) { - projectConfig.setMetatileEncounterTypeMask(mask); + projectConfig.metatileEncounterTypeMask = mask; this->disabledSettingsNames.insert(encounterTypeTableName); } // If we haven't already parsed behavior and layer type then try those too @@ -2037,7 +2035,7 @@ bool Project::readFieldmapMasks() { // Read behavior mask mask = attrTable.value(behaviorTableName).toUInt(&ok, 0); if (ok) { - projectConfig.setMetatileBehaviorMask(mask); + projectConfig.metatileBehaviorMask = mask; this->disabledSettingsNames.insert(behaviorTableName); } } @@ -2045,12 +2043,11 @@ bool Project::readFieldmapMasks() { // Read layer type mask mask = attrTable.value(layerTypeTableName).toUInt(&ok, 0); if (ok) { - projectConfig.setMetatileLayerTypeMask(mask); + projectConfig.metatileLayerTypeMask = mask; this->disabledSettingsNames.insert(layerTypeTableName); } } } - projectConfig.setSaveDisabled(false); return true; } @@ -2102,7 +2099,7 @@ bool Project::readHealLocations() { static const QRegularExpression re_comments("//.*?(\r\n?|\n)|/\\*.*?\\*/", QRegularExpression::DotMatchesEverythingOption); text.replace(re_comments, ""); - bool respawnEnabled = projectConfig.getHealLocationRespawnDataEnabled(); + bool respawnEnabled = projectConfig.healLocationRespawnDataEnabled; // Search for the name of the main Heal Locations table const QRegularExpression tableNameExpr(QString("%1\\s+(?[A-Za-z0-9_]+)\\[").arg(projectConfig.getIdentifier(ProjectIdentifier::symbol_heal_locations_type))); @@ -2260,7 +2257,7 @@ bool Project::readWeatherNames() { } bool Project::readCoordEventWeatherNames() { - if (!projectConfig.getEventWeatherTriggerEnabled()) + if (!projectConfig.eventWeatherTriggerEnabled) return true; const QStringList prefixes = {projectConfig.getIdentifier(ProjectIdentifier::regex_coord_event_weather)}; @@ -2273,7 +2270,7 @@ bool Project::readCoordEventWeatherNames() { } bool Project::readSecretBaseIds() { - if (!projectConfig.getEventSecretBaseEnabled()) + if (!projectConfig.eventSecretBaseEnabled) return true; const QStringList prefixes = {projectConfig.getIdentifier(ProjectIdentifier::regex_secret_bases)}; @@ -2316,7 +2313,7 @@ bool Project::readMetatileBehaviors() { if (defines.isEmpty()) { // Not having any metatile behavior names is ok (their values will be displayed instead). // If the user's metatiles can have nonzero values then warn them, as they likely want names. - if (projectConfig.getMetatileBehaviorMask()) + if (projectConfig.metatileBehaviorMask) logWarn(QString("Failed to read metatile behaviors from %1.").arg(filename)); return true; } @@ -2357,7 +2354,7 @@ bool Project::readObjEventGfxConstants() { bool Project::readMiscellaneousConstants() { miscConstants.clear(); - if (userConfig.getEncounterJsonActive()) { + if (userConfig.useEncounterJson) { const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_pokemon); const QString minLevelName = projectConfig.getIdentifier(ProjectIdentifier::define_min_level); const QString maxLevelName = projectConfig.getIdentifier(ProjectIdentifier::define_max_level); @@ -2436,9 +2433,8 @@ QStringList Project::getEventScriptsFilePaths() const { QStringList filePaths(QDir::cleanPath(root + "/" + projectConfig.getFilePath(ProjectFilePath::data_event_scripts))); const QString scriptsDir = QDir::cleanPath(root + "/" + projectConfig.getFilePath(ProjectFilePath::data_scripts_folders)); const QString mapsDir = QDir::cleanPath(root + "/" + projectConfig.getFilePath(ProjectFilePath::data_map_folders)); - const bool usePoryscript = projectConfig.getUsePoryScript(); - if (usePoryscript) { + if (projectConfig.usePoryScript) { QDirIterator it_pory_shared(scriptsDir, {"*.pory"}, QDir::Files); while (it_pory_shared.hasNext()) filePaths << it_pory_shared.next(); @@ -2739,7 +2735,7 @@ QString Project::getExistingFilepath(QString filepath) { if (filepath.isEmpty() || QFile::exists(filepath)) return filepath; - filepath = QDir::cleanPath(projectConfig.getProjectDir() + QDir::separator() + filepath); + filepath = QDir::cleanPath(projectConfig.projectDir + QDir::separator() + filepath); if (QFile::exists(filepath)) return filepath; @@ -2752,25 +2748,19 @@ QString Project::getExistingFilepath(QString filepath) { // can be limited by fieldmap defines) // Once we've read data from the project files we can adjust these accordingly. void Project::applyParsedLimits() { - // Avoid repeatedly writing the config file - projectConfig.setSaveDisabled(true); - uint32_t maxMask = Metatile::getMaxAttributesMask(); - projectConfig.setMetatileBehaviorMask(projectConfig.getMetatileBehaviorMask() & maxMask); - projectConfig.setMetatileTerrainTypeMask(projectConfig.getMetatileTerrainTypeMask() & maxMask); - projectConfig.setMetatileEncounterTypeMask(projectConfig.getMetatileEncounterTypeMask() & maxMask); - projectConfig.setMetatileLayerTypeMask(projectConfig.getMetatileLayerTypeMask() & maxMask); + projectConfig.metatileBehaviorMask &= maxMask; + projectConfig.metatileTerrainTypeMask &= maxMask; + projectConfig.metatileEncounterTypeMask &= maxMask; + projectConfig.metatileLayerTypeMask &= maxMask; Block::setLayout(); Metatile::setLayout(this); Project::num_metatiles_primary = qMin(Project::num_metatiles_primary, Block::getMaxMetatileId() + 1); - projectConfig.setDefaultMetatileId(qMin(projectConfig.getDefaultMetatileId(), Block::getMaxMetatileId())); - projectConfig.setDefaultElevation(qMin(projectConfig.getDefaultElevation(), Block::getMaxElevation())); - projectConfig.setDefaultCollision(qMin(projectConfig.getDefaultCollision(), Block::getMaxCollision())); - projectConfig.setCollisionSheetHeight(qMin(projectConfig.getCollisionSheetHeight(), Block::getMaxElevation() + 1)); - projectConfig.setCollisionSheetWidth(qMin(projectConfig.getCollisionSheetWidth(), Block::getMaxCollision() + 1)); - - projectConfig.setSaveDisabled(false); - projectConfig.save(); + projectConfig.defaultMetatileId = qMin(projectConfig.defaultMetatileId, Block::getMaxMetatileId()); + projectConfig.defaultElevation = qMin(projectConfig.defaultElevation, Block::getMaxElevation()); + projectConfig.defaultCollision = qMin(projectConfig.defaultCollision, Block::getMaxCollision()); + projectConfig.collisionSheetHeight = qMin(projectConfig.collisionSheetHeight, Block::getMaxElevation() + 1); + projectConfig.collisionSheetWidth = qMin(projectConfig.collisionSheetWidth, Block::getMaxCollision() + 1); } diff --git a/src/scriptapi/apimap.cpp b/src/scriptapi/apimap.cpp index 3741092e..2911fe74 100644 --- a/src/scriptapi/apimap.cpp +++ b/src/scriptapi/apimap.cpp @@ -295,7 +295,7 @@ int MainWindow::getBorderHeight() { } void MainWindow::setBorderDimensions(int width, int height) { - if (!this->editor || !this->editor->map || !projectConfig.getUseCustomBorderSize()) + if (!this->editor || !this->editor->map || !projectConfig.useCustomBorderSize) return; if (width < 1 || height < 1 || width > MAX_BORDER_WIDTH || height > MAX_BORDER_HEIGHT) return; @@ -305,7 +305,7 @@ void MainWindow::setBorderDimensions(int width, int height) { } void MainWindow::setBorderWidth(int width) { - if (!this->editor || !this->editor->map || !projectConfig.getUseCustomBorderSize()) + if (!this->editor || !this->editor->map || !projectConfig.useCustomBorderSize) return; if (width < 1 || width > MAX_BORDER_WIDTH) return; @@ -315,7 +315,7 @@ void MainWindow::setBorderWidth(int width) { } void MainWindow::setBorderHeight(int height) { - if (!this->editor || !this->editor->map || !projectConfig.getUseCustomBorderSize()) + if (!this->editor || !this->editor->map || !projectConfig.useCustomBorderSize) return; if (height < 1 || height > MAX_BORDER_HEIGHT) return; diff --git a/src/ui/citymappixmapitem.cpp b/src/ui/citymappixmapitem.cpp index eb9a2b32..c0a96717 100644 --- a/src/ui/citymappixmapitem.cpp +++ b/src/ui/citymappixmapitem.cpp @@ -15,7 +15,7 @@ void CityMapPixmapItem::init() { if (!binFile.open(QIODevice::ReadOnly)) return; data = binFile.readAll(); - if (projectConfig.getBaseGameVersion() == BaseGameVersion::pokeruby) { + if (projectConfig.baseGameVersion == BaseGameVersion::pokeruby) { for (int i = 0; i < data.size(); i++) data[i] = data[i] ^ 0x80; } @@ -46,7 +46,7 @@ void CityMapPixmapItem::save() { logError(QString("Cannot save city map tilemap to %1.").arg(file)); return; } - if (projectConfig.getBaseGameVersion() == BaseGameVersion::pokeruby) { + if (projectConfig.baseGameVersion == BaseGameVersion::pokeruby) { for (int i = 0; i < data.size(); i++) data[i] = data[i] ^ 0x80; } diff --git a/src/ui/customscriptseditor.cpp b/src/ui/customscriptseditor.cpp index 31e2ad7a..13650053 100644 --- a/src/ui/customscriptseditor.cpp +++ b/src/ui/customscriptseditor.cpp @@ -11,7 +11,7 @@ CustomScriptsEditor::CustomScriptsEditor(QWidget *parent) : QMainWindow(parent), ui(new Ui::CustomScriptsEditor), - baseDir(userConfig.getProjectDir() + QDir::separator()) + baseDir(userConfig.projectDir + QDir::separator()) { ui->setupUi(this); setAttribute(Qt::WA_DeleteOnClose); @@ -23,7 +23,7 @@ CustomScriptsEditor::CustomScriptsEditor(QWidget *parent) : for (int i = 0; i < paths.length(); i++) this->displayScript(paths.at(i), enabled.at(i)); - this->fileDialogDir = userConfig.getProjectDir(); + this->fileDialogDir = userConfig.projectDir; connect(ui->button_CreateNewScript, &QAbstractButton::clicked, this, &CustomScriptsEditor::createNewScript); connect(ui->button_LoadScript, &QAbstractButton::clicked, this, &CustomScriptsEditor::loadScript); diff --git a/src/ui/eventframes.cpp b/src/ui/eventframes.cpp index 7537588f..90d5b776 100644 --- a/src/ui/eventframes.cpp +++ b/src/ui/eventframes.cpp @@ -360,7 +360,7 @@ void ObjectFrame::initialize() { // script this->combo_script->setCurrentText(this->object->getScript()); - if (porymapConfig.getTextEditorGotoLine().isEmpty()) + if (porymapConfig.textEditorGotoLine.isEmpty()) this->button_script->hide(); // flag @@ -858,16 +858,16 @@ void HiddenItemFrame::initialize() { this->combo_flag->setTextItem(this->hiddenItem->getFlag()); // quantity - if (projectConfig.getHiddenItemQuantityEnabled()) { + if (projectConfig.hiddenItemQuantityEnabled) { this->spinner_quantity->setValue(this->hiddenItem->getQuantity()); } - this->hideable_quantity->setVisible(projectConfig.getHiddenItemQuantityEnabled()); + this->hideable_quantity->setVisible(projectConfig.hiddenItemQuantityEnabled); // underfoot - if (projectConfig.getHiddenItemRequiresItemfinderEnabled()) { + if (projectConfig.hiddenItemRequiresItemfinderEnabled) { this->check_itemfinder->setChecked(this->hiddenItem->getUnderfoot()); } - this->hideable_itemfinder->setVisible(projectConfig.getHiddenItemRequiresItemfinderEnabled()); + this->hideable_itemfinder->setVisible(projectConfig.hiddenItemRequiresItemfinderEnabled); } void HiddenItemFrame::populate(Project *project) { @@ -971,7 +971,7 @@ void HealLocationFrame::connectSignals(MainWindow *window) { EventFrame::connectSignals(window); - if (projectConfig.getHealLocationRespawnDataEnabled()) { + if (projectConfig.healLocationRespawnDataEnabled) { this->combo_respawn_map->disconnect(); connect(this->combo_respawn_map, &QComboBox::currentTextChanged, [this](const QString &text) { this->healLocation->setRespawnMap(text); @@ -992,7 +992,7 @@ void HealLocationFrame::initialize() { const QSignalBlocker blocker(this); EventFrame::initialize(); - bool respawnEnabled = projectConfig.getHealLocationRespawnDataEnabled(); + bool respawnEnabled = projectConfig.healLocationRespawnDataEnabled; if (respawnEnabled) { this->combo_respawn_map->setTextItem(this->healLocation->getRespawnMap()); this->spinner_respawn_npc->setValue(this->healLocation->getRespawnNPC()); @@ -1008,6 +1008,6 @@ void HealLocationFrame::populate(Project *project) { const QSignalBlocker blocker(this); EventFrame::populate(project); - if (projectConfig.getHealLocationRespawnDataEnabled()) + if (projectConfig.healLocationRespawnDataEnabled) this->combo_respawn_map->addItems(project->mapNames); } diff --git a/src/ui/imageproviders.cpp b/src/ui/imageproviders.cpp index f52bf590..86f0c07a 100644 --- a/src/ui/imageproviders.cpp +++ b/src/ui/imageproviders.cpp @@ -48,7 +48,6 @@ QImage getMetatileImage( QList> palettes = Tileset::getBlockPalettes(primaryTileset, secondaryTileset, useTruePalettes); QPainter metatile_painter(&metatile_image); - bool isTripleLayerMetatile = projectConfig.getTripleLayerMetatilesEnabled(); const int numLayers = 3; // When rendering, metatiles always have 3 layers uint32_t layerType = metatile->layerType(); for (int layer = 0; layer < numLayers; layer++) @@ -60,7 +59,7 @@ QImage getMetatileImage( // Get the tile to render next Tile tile; int tileOffset = (y * 2) + x; - if (isTripleLayerMetatile) { + if (projectConfig.tripleLayerMetatilesEnabled) { tile = metatile->tiles.value(tileOffset + (l * 4)); } else { // "Vanilla" metatiles only have 8 tiles, but render 12. diff --git a/src/ui/newmappopup.cpp b/src/ui/newmappopup.cpp index fdc3f79c..be6499e6 100644 --- a/src/ui/newmappopup.cpp +++ b/src/ui/newmappopup.cpp @@ -49,7 +49,7 @@ void NewMapPopup::init() { ui->spinBox_NewMap_Floor_Number->setMaximum(127); // Hide config specific ui elements - bool hasFlags = projectConfig.getMapAllowFlagsEnabled(); + bool hasFlags = projectConfig.mapAllowFlagsEnabled; ui->checkBox_NewMap_Allow_Running->setVisible(hasFlags); ui->checkBox_NewMap_Allow_Biking->setVisible(hasFlags); ui->checkBox_NewMap_Allow_Escape_Rope->setVisible(hasFlags); @@ -57,13 +57,13 @@ void NewMapPopup::init() { ui->label_NewMap_Allow_Biking->setVisible(hasFlags); ui->label_NewMap_Allow_Escape_Rope->setVisible(hasFlags); - bool hasCustomBorders = projectConfig.getUseCustomBorderSize(); + bool hasCustomBorders = projectConfig.useCustomBorderSize; ui->spinBox_NewMap_BorderWidth->setVisible(hasCustomBorders); ui->spinBox_NewMap_BorderHeight->setVisible(hasCustomBorders); ui->label_NewMap_BorderWidth->setVisible(hasCustomBorders); ui->label_NewMap_BorderHeight->setVisible(hasCustomBorders); - bool hasFloorNumber = projectConfig.getFloorNumberEnabled(); + bool hasFloorNumber = projectConfig.floorNumberEnabled; ui->spinBox_NewMap_Floor_Number->setVisible(hasFloorNumber); ui->label_NewMap_Floor_Number->setVisible(hasFloorNumber); @@ -272,7 +272,7 @@ void NewMapPopup::on_pushButton_NewMap_Accept_clicked() { layout->name = QString("%1_Layout").arg(newMap->name); layout->width = this->ui->spinBox_NewMap_Width->value(); layout->height = this->ui->spinBox_NewMap_Height->value(); - if (projectConfig.getUseCustomBorderSize()) { + if (projectConfig.useCustomBorderSize) { layout->border_width = this->ui->spinBox_NewMap_BorderWidth->value(); layout->border_height = this->ui->spinBox_NewMap_BorderHeight->value(); } else { @@ -296,12 +296,12 @@ void NewMapPopup::on_pushButton_NewMap_Accept_clicked() { newMap->needsHealLocation = true; } - if (projectConfig.getMapAllowFlagsEnabled()) { + if (projectConfig.mapAllowFlagsEnabled) { newMap->allowRunning = this->ui->checkBox_NewMap_Allow_Running->isChecked(); newMap->allowBiking = this->ui->checkBox_NewMap_Allow_Biking->isChecked(); newMap->allowEscaping = this->ui->checkBox_NewMap_Allow_Escape_Rope->isChecked(); } - if (projectConfig.getFloorNumberEnabled()) { + if (projectConfig.floorNumberEnabled) { newMap->floorNumber = this->ui->spinBox_NewMap_Floor_Number->value(); } diff --git a/src/ui/newtilesetdialog.cpp b/src/ui/newtilesetdialog.cpp index e38c499e..fa951879 100644 --- a/src/ui/newtilesetdialog.cpp +++ b/src/ui/newtilesetdialog.cpp @@ -15,7 +15,7 @@ NewTilesetDialog::NewTilesetDialog(Project* project, QWidget *parent) : QRegularExpressionValidator *validator = new QRegularExpressionValidator(expression); this->ui->nameLineEdit->setValidator(validator); - bool checkerboard = porymapConfig.getTilesetCheckerboardFill(); + bool checkerboard = porymapConfig.tilesetCheckerboardFill; this->ui->fillCheckBox->setChecked(checkerboard); this->checkerboardFill = checkerboard; @@ -46,5 +46,5 @@ void NewTilesetDialog::NameOrSecondaryChanged() { void NewTilesetDialog::FillChanged() { this->checkerboardFill = this->ui->fillCheckBox->isChecked(); - porymapConfig.setTilesetCheckerboardFill(this->checkerboardFill); + porymapConfig.tilesetCheckerboardFill = this->checkerboardFill; } diff --git a/src/ui/paletteeditor.cpp b/src/ui/paletteeditor.cpp index 7422d7db..8dcd2a7a 100644 --- a/src/ui/paletteeditor.cpp +++ b/src/ui/paletteeditor.cpp @@ -87,7 +87,7 @@ PaletteEditor::PaletteEditor(Project *project, Tileset *primaryTileset, Tileset connect(this->pickButtons[i], &QToolButton::clicked, [this, i](){ this->pickColor(i); }); } - int bitDepth = porymapConfig.getPaletteEditorBitDepth(); + int bitDepth = porymapConfig.paletteEditorBitDepth; if (bitDepth == 15) { this->ui->bit_depth_15->setChecked(true); } else { @@ -233,7 +233,7 @@ void PaletteEditor::setBitDepth(int bits) { break; } this->bitDepth = bits; - porymapConfig.setPaletteEditorBitDepth(bits); + porymapConfig.paletteEditorBitDepth = bits; refreshColorUis(); setSignalsEnabled(true); } diff --git a/src/ui/prefab.cpp b/src/ui/prefab.cpp index c5ad7f9b..3559b9a8 100644 --- a/src/ui/prefab.cpp +++ b/src/ui/prefab.cpp @@ -23,7 +23,7 @@ const QString defaultFilepath = "prefabs.json"; void Prefab::loadPrefabs() { this->items.clear(); - QString filepath = projectConfig.getPrefabFilepath(); + QString filepath = projectConfig.prefabFilepath; if (filepath.isEmpty()) return; ParseUtil parser; @@ -86,15 +86,14 @@ void Prefab::loadPrefabs() { } void Prefab::savePrefabs() { - QString filepath = projectConfig.getPrefabFilepath(); - if (filepath.isEmpty()) { - filepath = defaultFilepath; - projectConfig.setPrefabFilepath(filepath); - } + if (projectConfig.prefabFilepath.isEmpty()) + projectConfig.prefabFilepath = defaultFilepath; + + QString filepath = projectConfig.prefabFilepath; QFileInfo info(filepath); if (info.isRelative()) { - filepath = QDir::cleanPath(projectConfig.getProjectDir() + QDir::separator() + filepath); + filepath = QDir::cleanPath(projectConfig.projectDir + QDir::separator() + filepath); } QFile prefabsFile(filepath); if (!prefabsFile.open(QIODevice::WriteOnly)) { @@ -287,7 +286,7 @@ bool Prefab::tryImportDefaultPrefabs(QWidget * parent, BaseGameVersion version, if (fileInfo.suffix().isEmpty()) filepath += ".json"; if (fileInfo.isRelative()) { - absFilepath = QDir::cleanPath(projectConfig.getProjectDir() + QDir::separator() + filepath); + absFilepath = QDir::cleanPath(projectConfig.projectDir + QDir::separator() + filepath); } else { absFilepath = filepath; } @@ -313,10 +312,10 @@ bool Prefab::tryImportDefaultPrefabs(QWidget * parent, BaseGameVersion version, bool acceptedImport = (prompt == QMessageBox::Yes); if (acceptedImport) { // Sets up the default prefabs.json filepath. - projectConfig.setPrefabFilepath(filepath); + projectConfig.prefabFilepath = filepath; QFile prefabsFile(absFilepath); if (!prefabsFile.open(QIODevice::WriteOnly)) { - projectConfig.setPrefabFilepath(QString()); + projectConfig.prefabFilepath = QString(); logError(QString("Error: Could not open %1 for writing").arg(absFilepath)); QMessageBox messageBox(parent); @@ -346,7 +345,7 @@ bool Prefab::tryImportDefaultPrefabs(QWidget * parent, BaseGameVersion version, this->loadPrefabs(); } - projectConfig.setPrefabImportPrompted(true); + projectConfig.prefabImportPrompted = true; return acceptedImport; } diff --git a/src/ui/preferenceeditor.cpp b/src/ui/preferenceeditor.cpp index eacc3f3d..a51b614d 100644 --- a/src/ui/preferenceeditor.cpp +++ b/src/ui/preferenceeditor.cpp @@ -44,28 +44,26 @@ void PreferenceEditor::initFields() { } void PreferenceEditor::updateFields() { - themeSelector->setCurrentText(porymapConfig.getTheme()); - ui->lineEdit_TextEditorOpenFolder->setText(porymapConfig.getTextEditorOpenFolder()); - ui->lineEdit_TextEditorGotoLine->setText(porymapConfig.getTextEditorGotoLine()); - ui->checkBox_MonitorProjectFiles->setChecked(porymapConfig.getMonitorFiles()); - ui->checkBox_OpenRecentProject->setChecked(porymapConfig.getReopenOnLaunch()); - ui->checkBox_CheckForUpdates->setChecked(porymapConfig.getCheckForUpdates()); + themeSelector->setCurrentText(porymapConfig.theme); + ui->lineEdit_TextEditorOpenFolder->setText(porymapConfig.textEditorOpenFolder); + ui->lineEdit_TextEditorGotoLine->setText(porymapConfig.textEditorGotoLine); + ui->checkBox_MonitorProjectFiles->setChecked(porymapConfig.monitorFiles); + ui->checkBox_OpenRecentProject->setChecked(porymapConfig.reopenOnLaunch); + ui->checkBox_CheckForUpdates->setChecked(porymapConfig.checkForUpdates); } void PreferenceEditor::saveFields() { - if (themeSelector->currentText() != porymapConfig.getTheme()) { + if (themeSelector->currentText() != porymapConfig.theme) { const auto theme = themeSelector->currentText(); - porymapConfig.setTheme(theme); + porymapConfig.theme = theme; emit themeChanged(theme); } - porymapConfig.setSaveDisabled(true); - porymapConfig.setTextEditorOpenFolder(ui->lineEdit_TextEditorOpenFolder->text()); - porymapConfig.setTextEditorGotoLine(ui->lineEdit_TextEditorGotoLine->text()); - porymapConfig.setMonitorFiles(ui->checkBox_MonitorProjectFiles->isChecked()); - porymapConfig.setReopenOnLaunch(ui->checkBox_OpenRecentProject->isChecked()); - porymapConfig.setCheckForUpdates(ui->checkBox_CheckForUpdates->isChecked()); - porymapConfig.setSaveDisabled(false); + porymapConfig.textEditorOpenFolder = ui->lineEdit_TextEditorOpenFolder->text(); + porymapConfig.textEditorGotoLine = ui->lineEdit_TextEditorGotoLine->text(); + porymapConfig.monitorFiles = ui->checkBox_MonitorProjectFiles->isChecked(); + porymapConfig.reopenOnLaunch = ui->checkBox_OpenRecentProject->isChecked(); + porymapConfig.checkForUpdates = ui->checkBox_CheckForUpdates->isChecked(); porymapConfig.save(); emit preferencesSaved(); diff --git a/src/ui/projectsettingseditor.cpp b/src/ui/projectsettingseditor.cpp index 950dd87b..8a360eb2 100644 --- a/src/ui/projectsettingseditor.cpp +++ b/src/ui/projectsettingseditor.cpp @@ -18,7 +18,7 @@ ProjectSettingsEditor::ProjectSettingsEditor(QWidget *parent, Project *project) QMainWindow(parent), ui(new Ui::ProjectSettingsEditor), project(project), - baseDir(userConfig.getProjectDir() + QDir::separator()) + baseDir(projectConfig.projectDir + QDir::separator()) { ui->setupUi(this); setAttribute(Qt::WA_DeleteOnClose); @@ -109,7 +109,7 @@ void ProjectSettingsEditor::initUi() { static const QRegularExpression expression_HexList(QString("^(%1,)*%1$").arg(regex_Hex)); // Comma-separated list of hex values QRegularExpressionValidator *validator_HexList = new QRegularExpressionValidator(expression_HexList); ui->lineEdit_BorderMetatiles->setValidator(validator_HexList); - this->setBorderMetatilesUi(projectConfig.getUseCustomBorderSize()); + this->setBorderMetatilesUi(projectConfig.useCustomBorderSize); // Validate that the text added to the warp behavior list could be a valid define // (we don't care whether it actually is a metatile behavior define) @@ -184,12 +184,12 @@ void ProjectSettingsEditor::disableParsedSetting(QWidget * widget, const QString // Remember the current settings tab for future sessions void ProjectSettingsEditor::on_mainTabs_tabBarClicked(int index) { - porymapConfig.setProjectSettingsTab(index); + porymapConfig.projectSettingsTab = index; } void ProjectSettingsEditor::setTab(int index) { ui->mainTabs->setCurrentIndex(index); - porymapConfig.setProjectSettingsTab(index); + porymapConfig.projectSettingsTab = index; } void ProjectSettingsEditor::setBorderMetatilesUi(bool customSize) { @@ -424,10 +424,10 @@ void ProjectSettingsEditor::refresh() { this->refreshing = true; // Block signals // Set combo box texts - ui->comboBox_DefaultPrimaryTileset->setTextItem(projectConfig.getDefaultPrimaryTileset()); - ui->comboBox_DefaultSecondaryTileset->setTextItem(projectConfig.getDefaultSecondaryTileset()); + ui->comboBox_DefaultPrimaryTileset->setTextItem(projectConfig.defaultPrimaryTileset); + ui->comboBox_DefaultSecondaryTileset->setTextItem(projectConfig.defaultSecondaryTileset); ui->comboBox_BaseGameVersion->setTextItem(projectConfig.getBaseGameVersionString()); - ui->comboBox_AttributesSize->setTextItem(QString::number(projectConfig.getMetatileAttributesSize())); + ui->comboBox_AttributesSize->setTextItem(QString::number(projectConfig.metatileAttributesSize)); this->updateAttributeLimits(ui->comboBox_AttributesSize->currentText()); this->prevIconSpecies = QString(); @@ -435,45 +435,44 @@ void ProjectSettingsEditor::refresh() { this->updatePokemonIconPath(ui->comboBox_IconSpecies->currentText()); // Set check box states - ui->checkBox_UsePoryscript->setChecked(projectConfig.getUsePoryScript()); - ui->checkBox_ShowWildEncounterTables->setChecked(userConfig.getEncounterJsonActive()); - ui->checkBox_CreateTextFile->setChecked(projectConfig.getCreateMapTextFileEnabled()); - ui->checkBox_EnableTripleLayerMetatiles->setChecked(projectConfig.getTripleLayerMetatilesEnabled()); - ui->checkBox_EnableRequiresItemfinder->setChecked(projectConfig.getHiddenItemRequiresItemfinderEnabled()); - ui->checkBox_EnableQuantity->setChecked(projectConfig.getHiddenItemQuantityEnabled()); - ui->checkBox_EnableCloneObjects->setChecked(projectConfig.getEventCloneObjectEnabled()); - ui->checkBox_EnableWeatherTriggers->setChecked(projectConfig.getEventWeatherTriggerEnabled()); - ui->checkBox_EnableSecretBases->setChecked(projectConfig.getEventSecretBaseEnabled()); - ui->checkBox_EnableRespawn->setChecked(projectConfig.getHealLocationRespawnDataEnabled()); - ui->checkBox_EnableAllowFlags->setChecked(projectConfig.getMapAllowFlagsEnabled()); - ui->checkBox_EnableFloorNumber->setChecked(projectConfig.getFloorNumberEnabled()); - ui->checkBox_EnableCustomBorderSize->setChecked(projectConfig.getUseCustomBorderSize()); - ui->checkBox_OutputCallback->setChecked(projectConfig.getTilesetsHaveCallback()); - ui->checkBox_OutputIsCompressed->setChecked(projectConfig.getTilesetsHaveIsCompressed()); - ui->checkBox_DisableWarning->setChecked(porymapConfig.getWarpBehaviorWarningDisabled()); + ui->checkBox_UsePoryscript->setChecked(projectConfig.usePoryScript); + ui->checkBox_ShowWildEncounterTables->setChecked(userConfig.useEncounterJson); + ui->checkBox_CreateTextFile->setChecked(projectConfig.createMapTextFileEnabled); + ui->checkBox_EnableTripleLayerMetatiles->setChecked(projectConfig.tripleLayerMetatilesEnabled); + ui->checkBox_EnableRequiresItemfinder->setChecked(projectConfig.hiddenItemRequiresItemfinderEnabled); + ui->checkBox_EnableQuantity->setChecked(projectConfig.hiddenItemQuantityEnabled); + ui->checkBox_EnableCloneObjects->setChecked(projectConfig.eventCloneObjectEnabled); + ui->checkBox_EnableWeatherTriggers->setChecked(projectConfig.eventWeatherTriggerEnabled); + ui->checkBox_EnableSecretBases->setChecked(projectConfig.eventSecretBaseEnabled); + ui->checkBox_EnableRespawn->setChecked(projectConfig.healLocationRespawnDataEnabled); + ui->checkBox_EnableAllowFlags->setChecked(projectConfig.mapAllowFlagsEnabled); + ui->checkBox_EnableFloorNumber->setChecked(projectConfig.floorNumberEnabled); + ui->checkBox_EnableCustomBorderSize->setChecked(projectConfig.useCustomBorderSize); + ui->checkBox_OutputCallback->setChecked(projectConfig.tilesetsHaveCallback); + ui->checkBox_OutputIsCompressed->setChecked(projectConfig.tilesetsHaveIsCompressed); + ui->checkBox_DisableWarning->setChecked(porymapConfig.warpBehaviorWarningDisabled); // Set spin box values - ui->spinBox_Elevation->setValue(projectConfig.getDefaultElevation()); - ui->spinBox_Collision->setValue(projectConfig.getDefaultCollision()); - ui->spinBox_FillMetatile->setValue(projectConfig.getDefaultMetatileId()); - ui->spinBox_MaxElevation->setValue(projectConfig.getCollisionSheetHeight() - 1); - ui->spinBox_MaxCollision->setValue(projectConfig.getCollisionSheetWidth() - 1); - ui->spinBox_BehaviorMask->setValue(projectConfig.getMetatileBehaviorMask() & ui->spinBox_BehaviorMask->maximum()); - ui->spinBox_EncounterTypeMask->setValue(projectConfig.getMetatileEncounterTypeMask() & ui->spinBox_EncounterTypeMask->maximum()); - ui->spinBox_LayerTypeMask->setValue(projectConfig.getMetatileLayerTypeMask() & ui->spinBox_LayerTypeMask->maximum()); - ui->spinBox_TerrainTypeMask->setValue(projectConfig.getMetatileTerrainTypeMask() & ui->spinBox_TerrainTypeMask->maximum()); - ui->spinBox_MetatileIdMask->setValue(projectConfig.getBlockMetatileIdMask() & ui->spinBox_MetatileIdMask->maximum()); - ui->spinBox_CollisionMask->setValue(projectConfig.getBlockCollisionMask() & ui->spinBox_CollisionMask->maximum()); - ui->spinBox_ElevationMask->setValue(projectConfig.getBlockElevationMask() & ui->spinBox_ElevationMask->maximum()); + ui->spinBox_Elevation->setValue(projectConfig.defaultElevation); + ui->spinBox_Collision->setValue(projectConfig.defaultCollision); + ui->spinBox_FillMetatile->setValue(projectConfig.defaultMetatileId); + ui->spinBox_MaxElevation->setValue(projectConfig.collisionSheetHeight - 1); + ui->spinBox_MaxCollision->setValue(projectConfig.collisionSheetWidth - 1); + ui->spinBox_BehaviorMask->setValue(projectConfig.metatileBehaviorMask & ui->spinBox_BehaviorMask->maximum()); + ui->spinBox_EncounterTypeMask->setValue(projectConfig.metatileEncounterTypeMask & ui->spinBox_EncounterTypeMask->maximum()); + ui->spinBox_LayerTypeMask->setValue(projectConfig.metatileLayerTypeMask & ui->spinBox_LayerTypeMask->maximum()); + ui->spinBox_TerrainTypeMask->setValue(projectConfig.metatileTerrainTypeMask & ui->spinBox_TerrainTypeMask->maximum()); + ui->spinBox_MetatileIdMask->setValue(projectConfig.blockMetatileIdMask & ui->spinBox_MetatileIdMask->maximum()); + ui->spinBox_CollisionMask->setValue(projectConfig.blockCollisionMask & ui->spinBox_CollisionMask->maximum()); + ui->spinBox_ElevationMask->setValue(projectConfig.blockElevationMask & ui->spinBox_ElevationMask->maximum()); // Set (and sync) border metatile IDs - auto metatileIds = projectConfig.getNewMapBorderMetatileIds(); - this->setBorderMetatileIds(false, metatileIds); - this->setBorderMetatileIds(true, metatileIds); + this->setBorderMetatileIds(false, projectConfig.newMapBorderMetatileIds); + this->setBorderMetatileIds(true, projectConfig.newMapBorderMetatileIds); // Set line edit texts - ui->lineEdit_PrefabsPath->setText(projectConfig.getPrefabFilepath()); - ui->lineEdit_CollisionGraphics->setText(projectConfig.getCollisionSheetPath()); + ui->lineEdit_PrefabsPath->setText(projectConfig.prefabFilepath); + ui->lineEdit_CollisionGraphics->setText(projectConfig.collisionSheetPath); ui->lineEdit_ObjectsIcon->setText(projectConfig.getEventIconPath(Event::Group::Object)); ui->lineEdit_WarpsIcon->setText(projectConfig.getEventIconPath(Event::Group::Warp)); ui->lineEdit_TriggersIcon->setText(projectConfig.getEventIconPath(Event::Group::Coord)); @@ -485,9 +484,8 @@ void ProjectSettingsEditor::refresh() { lineEdit->setText(projectConfig.getCustomIdentifier(lineEdit->objectName())); // Set warp behaviors - auto behaviorValues = projectConfig.getWarpBehaviors(); QStringList behaviorNames; - for (auto value : behaviorValues) { + for (auto value : projectConfig.warpBehaviors) { if (project->metatileBehaviorMapInverse.contains(value)) behaviorNames.append(project->metatileBehaviorMapInverse.value(value)); } @@ -500,50 +498,47 @@ void ProjectSettingsEditor::save() { if (!this->hasUnsavedChanges) return; - // Prevent a call to save() for each of the config settings - projectConfig.setSaveDisabled(true); - // Save combo box settings - projectConfig.setDefaultPrimaryTileset(ui->comboBox_DefaultPrimaryTileset->currentText()); - projectConfig.setDefaultSecondaryTileset(ui->comboBox_DefaultSecondaryTileset->currentText()); - projectConfig.setBaseGameVersion(projectConfig.stringToBaseGameVersion(ui->comboBox_BaseGameVersion->currentText())); - projectConfig.setMetatileAttributesSize(ui->comboBox_AttributesSize->currentText().toInt()); + projectConfig.defaultPrimaryTileset = ui->comboBox_DefaultPrimaryTileset->currentText(); + projectConfig.defaultSecondaryTileset = ui->comboBox_DefaultSecondaryTileset->currentText(); + projectConfig.baseGameVersion = projectConfig.stringToBaseGameVersion(ui->comboBox_BaseGameVersion->currentText()); + projectConfig.metatileAttributesSize = ui->comboBox_AttributesSize->currentText().toInt(); // Save check box settings - projectConfig.setUsePoryScript(ui->checkBox_UsePoryscript->isChecked()); - userConfig.setEncounterJsonActive(ui->checkBox_ShowWildEncounterTables->isChecked()); - projectConfig.setCreateMapTextFileEnabled(ui->checkBox_CreateTextFile->isChecked()); - projectConfig.setTripleLayerMetatilesEnabled(ui->checkBox_EnableTripleLayerMetatiles->isChecked()); - projectConfig.setHiddenItemRequiresItemfinderEnabled(ui->checkBox_EnableRequiresItemfinder->isChecked()); - projectConfig.setHiddenItemQuantityEnabled(ui->checkBox_EnableQuantity->isChecked()); - projectConfig.setEventCloneObjectEnabled(ui->checkBox_EnableCloneObjects->isChecked()); - projectConfig.setEventWeatherTriggerEnabled(ui->checkBox_EnableWeatherTriggers->isChecked()); - projectConfig.setEventSecretBaseEnabled(ui->checkBox_EnableSecretBases->isChecked()); - projectConfig.setHealLocationRespawnDataEnabled(ui->checkBox_EnableRespawn->isChecked()); - projectConfig.setMapAllowFlagsEnabled(ui->checkBox_EnableAllowFlags->isChecked()); - projectConfig.setFloorNumberEnabled(ui->checkBox_EnableFloorNumber->isChecked()); - projectConfig.setUseCustomBorderSize(ui->checkBox_EnableCustomBorderSize->isChecked()); - projectConfig.setTilesetsHaveCallback(ui->checkBox_OutputCallback->isChecked()); - projectConfig.setTilesetsHaveIsCompressed(ui->checkBox_OutputIsCompressed->isChecked()); - porymapConfig.setWarpBehaviorWarningDisabled(ui->checkBox_DisableWarning->isChecked()); + projectConfig.usePoryScript = ui->checkBox_UsePoryscript->isChecked(); + userConfig.useEncounterJson = ui->checkBox_ShowWildEncounterTables->isChecked(); + projectConfig.createMapTextFileEnabled = ui->checkBox_CreateTextFile->isChecked(); + projectConfig.tripleLayerMetatilesEnabled = ui->checkBox_EnableTripleLayerMetatiles->isChecked(); + projectConfig.hiddenItemRequiresItemfinderEnabled = ui->checkBox_EnableRequiresItemfinder->isChecked(); + projectConfig.hiddenItemQuantityEnabled = ui->checkBox_EnableQuantity->isChecked(); + projectConfig.eventCloneObjectEnabled = ui->checkBox_EnableCloneObjects->isChecked(); + projectConfig.eventWeatherTriggerEnabled = ui->checkBox_EnableWeatherTriggers->isChecked(); + projectConfig.eventSecretBaseEnabled = ui->checkBox_EnableSecretBases->isChecked(); + projectConfig.healLocationRespawnDataEnabled = ui->checkBox_EnableRespawn->isChecked(); + projectConfig.mapAllowFlagsEnabled = ui->checkBox_EnableAllowFlags->isChecked(); + projectConfig.floorNumberEnabled = ui->checkBox_EnableFloorNumber->isChecked(); + projectConfig.useCustomBorderSize = ui->checkBox_EnableCustomBorderSize->isChecked(); + projectConfig.tilesetsHaveCallback = ui->checkBox_OutputCallback->isChecked(); + projectConfig.tilesetsHaveIsCompressed = ui->checkBox_OutputIsCompressed->isChecked(); + porymapConfig.warpBehaviorWarningDisabled = ui->checkBox_DisableWarning->isChecked(); // Save spin box settings - projectConfig.setDefaultElevation(ui->spinBox_Elevation->value()); - projectConfig.setDefaultCollision(ui->spinBox_Collision->value()); - projectConfig.setDefaultMetatileId(ui->spinBox_FillMetatile->value()); - projectConfig.setCollisionSheetHeight(ui->spinBox_MaxElevation->value() + 1); - projectConfig.setCollisionSheetWidth(ui->spinBox_MaxCollision->value() + 1); - projectConfig.setMetatileBehaviorMask(ui->spinBox_BehaviorMask->value()); - projectConfig.setMetatileTerrainTypeMask(ui->spinBox_TerrainTypeMask->value()); - projectConfig.setMetatileEncounterTypeMask(ui->spinBox_EncounterTypeMask->value()); - projectConfig.setMetatileLayerTypeMask(ui->spinBox_LayerTypeMask->value()); - projectConfig.setBlockMetatileIdMask(ui->spinBox_MetatileIdMask->value()); - projectConfig.setBlockCollisionMask(ui->spinBox_CollisionMask->value()); - projectConfig.setBlockElevationMask(ui->spinBox_ElevationMask->value()); + projectConfig.defaultElevation = ui->spinBox_Elevation->value(); + projectConfig.defaultCollision = ui->spinBox_Collision->value(); + projectConfig.defaultMetatileId = ui->spinBox_FillMetatile->value(); + projectConfig.collisionSheetHeight = ui->spinBox_MaxElevation->value() + 1; + projectConfig.collisionSheetWidth = ui->spinBox_MaxCollision->value() + 1; + projectConfig.metatileBehaviorMask = ui->spinBox_BehaviorMask->value(); + projectConfig.metatileTerrainTypeMask = ui->spinBox_TerrainTypeMask->value(); + projectConfig.metatileEncounterTypeMask = ui->spinBox_EncounterTypeMask->value(); + projectConfig.metatileLayerTypeMask = ui->spinBox_LayerTypeMask->value(); + projectConfig.blockMetatileIdMask = ui->spinBox_MetatileIdMask->value(); + projectConfig.blockCollisionMask = ui->spinBox_CollisionMask->value(); + projectConfig.blockElevationMask = ui->spinBox_ElevationMask->value(); // Save line edit settings - projectConfig.setPrefabFilepath(ui->lineEdit_PrefabsPath->text()); - projectConfig.setCollisionSheetPath(ui->lineEdit_CollisionGraphics->text()); + projectConfig.prefabFilepath = ui->lineEdit_PrefabsPath->text(); + projectConfig.collisionSheetPath = ui->lineEdit_CollisionGraphics->text(); projectConfig.setEventIconPath(Event::Group::Object, ui->lineEdit_ObjectsIcon->text()); projectConfig.setEventIconPath(Event::Group::Warp, ui->lineEdit_WarpsIcon->text()); projectConfig.setEventIconPath(Event::Group::Coord, ui->lineEdit_TriggersIcon->text()); @@ -555,14 +550,13 @@ void ProjectSettingsEditor::save() { projectConfig.setIdentifier(lineEdit->objectName(), lineEdit->text()); // Save warp behaviors + projectConfig.warpBehaviors.clear(); QStringList behaviorNames = this->getWarpBehaviorsList(); - QSet behaviorValues; for (auto name : behaviorNames) - behaviorValues.insert(project->metatileBehaviorMap.value(name)); - projectConfig.setWarpBehaviors(behaviorValues); + projectConfig.warpBehaviors.insert(project->metatileBehaviorMap.value(name)); // Save border metatile IDs - projectConfig.setNewMapBorderMetatileIds(this->getBorderMetatileIds(ui->checkBox_EnableCustomBorderSize->isChecked())); + projectConfig.newMapBorderMetatileIds = this->getBorderMetatileIds(ui->checkBox_EnableCustomBorderSize->isChecked()); // Save pokemon icon paths const QString species = ui->comboBox_IconSpecies->currentText(); @@ -571,8 +565,10 @@ void ProjectSettingsEditor::save() { for (auto i = this->editedPokemonIconPaths.cbegin(), end = this->editedPokemonIconPaths.cend(); i != end; i++) projectConfig.setPokemonIconPath(i.key(), i.value()); - projectConfig.setSaveDisabled(false); projectConfig.save(); + userConfig.save(); + porymapConfig.save(); + this->hasUnsavedChanges = false; // Technically, a reload is not required for several of the config settings. @@ -611,7 +607,7 @@ void ProjectSettingsEditor::importDefaultPrefabsClicked(bool) { // If the prompt is accepted the prefabs file will be created and its filepath will be saved in the config. BaseGameVersion version = projectConfig.stringToBaseGameVersion(ui->comboBox_BaseGameVersion->currentText()); if (prefab.tryImportDefaultPrefabs(this, version, ui->lineEdit_PrefabsPath->text())) { - ui->lineEdit_PrefabsPath->setText(projectConfig.getPrefabFilepath()); // Refresh with new filepath + ui->lineEdit_PrefabsPath->setText(projectConfig.prefabFilepath); // Refresh with new filepath this->hasUnsavedChanges = true; } } diff --git a/src/ui/regionmapeditor.cpp b/src/ui/regionmapeditor.cpp index 13650c83..5cd0c9cb 100644 --- a/src/ui/regionmapeditor.cpp +++ b/src/ui/regionmapeditor.cpp @@ -202,7 +202,7 @@ void buildFireredDefaults(poryjson::Json &json) { poryjson::Json RegionMapEditor::buildDefaultJson() { poryjson::Json defaultJson; - switch (projectConfig.getBaseGameVersion()) { + switch (projectConfig.baseGameVersion) { case BaseGameVersion::pokeemerald: buildEmeraldDefaults(defaultJson); break; @@ -345,7 +345,7 @@ bool RegionMapEditor::buildConfigDialog() { // for sake of convenience, option to just use defaults for each basegame version QPushButton *config_useProjectDefault; - switch (projectConfig.getBaseGameVersion()) { + switch (projectConfig.baseGameVersion) { case BaseGameVersion::pokefirered: config_useProjectDefault = new QPushButton("\nUse pokefirered defaults\n"); break; diff --git a/src/ui/shortcutseditor.cpp b/src/ui/shortcutseditor.cpp index 83ac1147..30f9d62a 100644 --- a/src/ui/shortcutseditor.cpp +++ b/src/ui/shortcutseditor.cpp @@ -53,6 +53,7 @@ void ShortcutsEditor::saveShortcuts() { } shortcutsConfig.setUserShortcuts(objects_keySequences); + shortcutsConfig.save(); emit shortcutsSaved(); } diff --git a/src/ui/tileseteditor.cpp b/src/ui/tileseteditor.cpp index bd233974..9ef50cc6 100644 --- a/src/ui/tileseteditor.cpp +++ b/src/ui/tileseteditor.cpp @@ -114,7 +114,7 @@ void TilesetEditor::initUi() { void TilesetEditor::setAttributesUi() { // Behavior - if (projectConfig.getMetatileBehaviorMask()) { + if (projectConfig.metatileBehaviorMask) { for (int num : project->metatileBehaviorMapInverse.keys()) { this->ui->comboBox_metatileBehaviors->addItem(project->metatileBehaviorMapInverse[num], num); } @@ -125,7 +125,7 @@ void TilesetEditor::setAttributesUi() { } // Terrain Type - if (projectConfig.getMetatileTerrainTypeMask()) { + if (projectConfig.metatileTerrainTypeMask) { this->ui->comboBox_terrainType->addItem("Normal", TERRAIN_NONE); this->ui->comboBox_terrainType->addItem("Grass", TERRAIN_GRASS); this->ui->comboBox_terrainType->addItem("Water", TERRAIN_WATER); @@ -138,7 +138,7 @@ void TilesetEditor::setAttributesUi() { } // Encounter Type - if (projectConfig.getMetatileEncounterTypeMask()) { + if (projectConfig.metatileEncounterTypeMask) { this->ui->comboBox_encounterType->addItem("None", ENCOUNTER_NONE); this->ui->comboBox_encounterType->addItem("Land", ENCOUNTER_LAND); this->ui->comboBox_encounterType->addItem("Water", ENCOUNTER_WATER); @@ -150,13 +150,13 @@ void TilesetEditor::setAttributesUi() { } // Layer Type - if (!projectConfig.getTripleLayerMetatilesEnabled()) { + if (!projectConfig.tripleLayerMetatilesEnabled) { this->ui->comboBox_layerType->addItem("Normal - Middle/Top", METATILE_LAYER_MIDDLE_TOP); this->ui->comboBox_layerType->addItem("Covered - Bottom/Middle", METATILE_LAYER_BOTTOM_MIDDLE); this->ui->comboBox_layerType->addItem("Split - Bottom/Top", METATILE_LAYER_BOTTOM_TOP); this->ui->comboBox_layerType->setEditable(false); this->ui->comboBox_layerType->setMinimumContentsLength(0); - if (!projectConfig.getMetatileLayerTypeMask()) { + if (!projectConfig.metatileLayerTypeMask) { // User doesn't have triple layer metatiles, but has no layer type attribute. // Porymap is still using the layer type value to render metatiles, and with // no mask set every metatile will be "Middle/Top", so just display the combo @@ -188,7 +188,7 @@ void TilesetEditor::initMetatileSelector() connect(this->metatileSelector, &TilesetEditorMetatileSelector::selectedMetatileChanged, this, &TilesetEditor::onSelectedMetatileChanged); - bool showGrid = porymapConfig.getShowTilesetEditorMetatileGrid(); + bool showGrid = porymapConfig.showTilesetEditorMetatileGrid; this->ui->actionMetatile_Grid->setChecked(showGrid); this->metatileSelector->showGrid = showGrid; @@ -198,7 +198,7 @@ void TilesetEditor::initMetatileSelector() this->ui->graphicsView_Metatiles->setScene(this->metatilesScene); this->ui->graphicsView_Metatiles->setResizeAnchor(QGraphicsView::AnchorViewCenter); - this->ui->horizontalSlider_MetatilesZoom->setValue(porymapConfig.getTilesetEditorMetatilesZoom()); + this->ui->horizontalSlider_MetatilesZoom->setValue(porymapConfig.tilesetEditorMetatilesZoom); } void TilesetEditor::initMetatileLayersItem() { @@ -213,7 +213,7 @@ void TilesetEditor::initMetatileLayersItem() { connect(this->metatileLayersItem, &MetatileLayersItem::hoveredTileCleared, this, &TilesetEditor::onHoveredTileCleared); - bool showGrid = porymapConfig.getShowTilesetEditorLayerGrid(); + bool showGrid = porymapConfig.showTilesetEditorLayerGrid; this->ui->actionLayer_Grid->setChecked(showGrid); this->metatileLayersItem->showGrid = showGrid; @@ -239,7 +239,7 @@ void TilesetEditor::initTileSelector() this->ui->graphicsView_Tiles->setScene(this->tilesScene); this->ui->graphicsView_Tiles->setResizeAnchor(QGraphicsView::AnchorViewCenter); - this->ui->horizontalSlider_TilesZoom->setValue(porymapConfig.getTilesetEditorTilesZoom()); + this->ui->horizontalSlider_TilesZoom->setValue(porymapConfig.tilesetEditorTilesZoom); } void TilesetEditor::initSelectedTileItem() { @@ -1070,13 +1070,13 @@ void TilesetEditor::on_actionShow_UnusedTiles_toggled(bool checked) { void TilesetEditor::on_actionMetatile_Grid_triggered(bool checked) { this->metatileSelector->showGrid = checked; this->metatileSelector->draw(); - porymapConfig.setShowTilesetEditorMetatileGrid(checked); + porymapConfig.showTilesetEditorMetatileGrid = checked; } void TilesetEditor::on_actionLayer_Grid_triggered(bool checked) { this->metatileLayersItem->showGrid = checked; this->metatileLayersItem->draw(); - porymapConfig.setShowTilesetEditorLayerGrid(checked); + porymapConfig.showTilesetEditorLayerGrid = checked; } void TilesetEditor::countMetatileUsage() { @@ -1188,7 +1188,7 @@ void TilesetEditor::on_copyButton_metatileLabel_clicked() { } void TilesetEditor::on_horizontalSlider_MetatilesZoom_valueChanged(int value) { - porymapConfig.setTilesetEditorMetatilesZoom(value); + porymapConfig.tilesetEditorMetatilesZoom = value; this->redrawMetatileSelector(); } @@ -1196,7 +1196,7 @@ void TilesetEditor::redrawMetatileSelector() { QSize size(this->metatileSelector->pixmap().width(), this->metatileSelector->pixmap().height()); this->ui->graphicsView_Metatiles->setSceneRect(0, 0, size.width(), size.height()); - double scale = pow(3.0, static_cast(porymapConfig.getTilesetEditorMetatilesZoom() - 30) / 30.0); + double scale = pow(3.0, static_cast(porymapConfig.tilesetEditorMetatilesZoom - 30) / 30.0); QTransform transform; transform.scale(scale, scale); size *= scale; @@ -1213,7 +1213,7 @@ void TilesetEditor::redrawMetatileSelector() { } void TilesetEditor::on_horizontalSlider_TilesZoom_valueChanged(int value) { - porymapConfig.setTilesetEditorTilesZoom(value); + porymapConfig.tilesetEditorTilesZoom = value; this->redrawTileSelector(); } @@ -1221,7 +1221,7 @@ void TilesetEditor::redrawTileSelector() { QSize size(this->tileSelector->pixmap().width(), this->tileSelector->pixmap().height()); this->ui->graphicsView_Tiles->setSceneRect(0, 0, size.width(), size.height()); - double scale = pow(3.0, static_cast(porymapConfig.getTilesetEditorTilesZoom() - 30) / 30.0); + double scale = pow(3.0, static_cast(porymapConfig.tilesetEditorTilesZoom - 30) / 30.0); QTransform transform; transform.scale(scale, scale); size *= scale; diff --git a/src/ui/updatepromoter.cpp b/src/ui/updatepromoter.cpp index 8ca2b6cd..7b9d39d6 100644 --- a/src/ui/updatepromoter.cpp +++ b/src/ui/updatepromoter.cpp @@ -20,8 +20,7 @@ UpdatePromoter::UpdatePromoter(QWidget *parent, NetworkAccessManager *manager) this->updatePreferences(); ui->checkBox_StopAlerts->setVisible(false); connect(ui->checkBox_StopAlerts, &QCheckBox::stateChanged, [this](int state) { - bool enable = (state != Qt::Checked); - porymapConfig.setCheckForUpdates(enable); + porymapConfig.checkForUpdates = (state != Qt::Checked); emit this->changedPreferences(); }); @@ -141,11 +140,11 @@ void UpdatePromoter::processWebpage(const QJsonDocument &data, const QUrl &nextU // Alert the user about the new version if the dialog wasn't already open. // Show the window, but also show the option to turn off automatic alerts in the future. // We only show this alert once for a given release. - if (!this->isVisible() && this->newVersion > porymapConfig.getLastUpdateCheckVersion()) { + if (!this->isVisible() && this->newVersion > porymapConfig.lastUpdateCheckVersion) { ui->checkBox_StopAlerts->setVisible(true); this->show(); } - porymapConfig.setLastUpdateCheckVersion(this->newVersion); + porymapConfig.lastUpdateCheckVersion = this->newVersion; } else { ui->label_Status->setText("Your version of Porymap is up to date!"); ui->label_Warning->setVisible(false); @@ -173,7 +172,7 @@ void UpdatePromoter::error(const QString &err, const QDateTime retryAfter) { void UpdatePromoter::updatePreferences() { const QSignalBlocker blocker(ui->checkBox_StopAlerts); - ui->checkBox_StopAlerts->setChecked(!porymapConfig.getCheckForUpdates()); + ui->checkBox_StopAlerts->setChecked(!porymapConfig.checkForUpdates); } void UpdatePromoter::dialogButtonClicked(QAbstractButton *button) { From 29ed696d9ed2bad01b68d07d2957fb44ed9fa463 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Mon, 15 Jul 2024 21:10:54 -0400 Subject: [PATCH 21/76] Auto-detect more version names --- include/config.h | 3 ++- src/config.cpp | 50 ++++++++++++++++++++++++-------------- src/ui/prefab.cpp | 3 +++ src/ui/regionmapeditor.cpp | 8 +++--- 4 files changed, 41 insertions(+), 23 deletions(-) diff --git a/include/config.h b/include/config.h index 89a27d24..7e87e986 100644 --- a/include/config.h +++ b/include/config.h @@ -158,6 +158,7 @@ private: extern PorymapConfig porymapConfig; enum BaseGameVersion { + none, pokeruby, pokefirered, pokeemerald, @@ -303,7 +304,7 @@ public: static const QMap> defaultIdentifiers; static const QMap> defaultPaths; static const QStringList versionStrings; - static BaseGameVersion stringToBaseGameVersion(QString string, bool * ok = nullptr); + static BaseGameVersion stringToBaseGameVersion(const QString &string); void reset(BaseGameVersion baseGameVersion); void setFilePath(ProjectFilePath pathId, const QString &path); diff --git a/src/config.cpp b/src/config.cpp index 1ee00c18..98681264 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -608,20 +608,32 @@ const QMap baseGameVersionMap = { {BaseGameVersion::pokeemerald, ProjectConfig::versionStrings[2]}, }; -const QMap baseGameVersionReverseMap = { - {ProjectConfig::versionStrings[0], BaseGameVersion::pokeruby}, - {ProjectConfig::versionStrings[1], BaseGameVersion::pokefirered}, - {ProjectConfig::versionStrings[2], BaseGameVersion::pokeemerald}, +const QMap versionDetectNames = { + {BaseGameVersion::pokeruby, {"ruby", "sapphire"}}, + {BaseGameVersion::pokefirered, {"firered", "leafgreen"}}, + {BaseGameVersion::pokeemerald, {"emerald"}}, }; -BaseGameVersion ProjectConfig::stringToBaseGameVersion(QString string, bool * ok) { - if (baseGameVersionReverseMap.contains(string)) { - if (ok) *ok = true; - return baseGameVersionReverseMap.value(string); - } else { - if (ok) *ok = false; - return BaseGameVersion::pokeemerald; +// If a string exclusively contains one version name we assume its identity, +// otherwise we leave it unknown and we'll need the user to tell us the version. +BaseGameVersion ProjectConfig::stringToBaseGameVersion(const QString &string) { + BaseGameVersion version = BaseGameVersion::none; + for (auto i = versionDetectNames.cbegin(), end = versionDetectNames.cend(); i != end; i++) { + // Compare the given string to all the possible names for this game version + const QStringList names = i.value(); + for (auto name : names) { + if (string.contains(name)) { + if (version != BaseGameVersion::none) { + // The given string matches multiple versions, so we can't be sure which it is. + return BaseGameVersion::none; + } + version = i.key(); + break; + } + } } + // We finished checking the names for each version; the name either matched 1 version or none. + return version; } ProjectConfig projectConfig; @@ -633,10 +645,11 @@ QString ProjectConfig::getConfigFilepath() { void ProjectConfig::parseConfigKeyValue(QString key, QString value) { if (key == "base_game_version") { - bool ok; - this->baseGameVersion = this->stringToBaseGameVersion(value.toLower(), &ok); - if (!ok) + this->baseGameVersion = this->stringToBaseGameVersion(value.toLower()); + if (this->baseGameVersion == BaseGameVersion::none) { logWarn(QString("Invalid config value for base_game_version: '%1'. Must be 'pokeruby', 'pokefirered' or 'pokeemerald'.").arg(value)); + this->baseGameVersion = BaseGameVersion::pokeemerald; + } } else if (key == "use_poryscript") { this->usePoryScript = getConfigBool(key, value); } else if (key == "use_custom_border_size") { @@ -851,10 +864,11 @@ QMap ProjectConfig::getKeyValueMap() { void ProjectConfig::init() { QString dirName = QDir(this->projectDir).dirName().toLower(); - if (baseGameVersionReverseMap.contains(dirName)) { - // TODO: Improve detection (ex: emerald or pokeemerald-2 aren't currently auto-detected) - this->baseGameVersion = baseGameVersionReverseMap.value(dirName); - logInfo(QString("Auto-detected base_game_version as '%1'").arg(dirName)); + + BaseGameVersion version = stringToBaseGameVersion(dirName); + if (version != BaseGameVersion::none) { + this->baseGameVersion = version; + logInfo(QString("Auto-detected base_game_version as '%1'").arg(getBaseGameVersionString(version))); } else { QDialog dialog(nullptr, Qt::WindowTitleHint); dialog.setWindowTitle("Project Configuration"); diff --git a/src/ui/prefab.cpp b/src/ui/prefab.cpp index 3559b9a8..594e811a 100644 --- a/src/ui/prefab.cpp +++ b/src/ui/prefab.cpp @@ -338,6 +338,9 @@ bool Prefab::tryImportDefaultPrefabs(QWidget * parent, BaseGameVersion version, case BaseGameVersion::pokeemerald: content = parser.readTextFile(":/text/prefabs_default_emerald.json"); break; + default: + content = QString(); + break; } prefabsFile.write(content.toUtf8()); diff --git a/src/ui/regionmapeditor.cpp b/src/ui/regionmapeditor.cpp index 5cd0c9cb..0a5451a7 100644 --- a/src/ui/regionmapeditor.cpp +++ b/src/ui/regionmapeditor.cpp @@ -206,14 +206,14 @@ poryjson::Json RegionMapEditor::buildDefaultJson() { case BaseGameVersion::pokeemerald: buildEmeraldDefaults(defaultJson); break; - case BaseGameVersion::pokeruby: buildRubyDefaults(defaultJson); break; - case BaseGameVersion::pokefirered: buildFireredDefaults(defaultJson); break; + default: + break; } return defaultJson; @@ -315,7 +315,7 @@ bool RegionMapEditor::buildConfigDialog() { QPushButton *delMapButton = new QPushButton("Delete Selected Region Map"); form.addRow(delMapButton); - connect(delMapButton, &QPushButton::clicked, [this, regionMapList, &updateJsonFromList] { + connect(delMapButton, &QPushButton::clicked, [regionMapList, &updateJsonFromList] { QListWidgetItem *item = regionMapList->currentItem(); if (item) { regionMapList->removeItemWidget(item); @@ -344,7 +344,7 @@ bool RegionMapEditor::buildConfigDialog() { // for sake of convenience, option to just use defaults for each basegame version - QPushButton *config_useProjectDefault; + QPushButton *config_useProjectDefault = nullptr; switch (projectConfig.baseGameVersion) { case BaseGameVersion::pokefirered: config_useProjectDefault = new QPushButton("\nUse pokefirered defaults\n"); From 9efe67a72f8952ba4fd6dd8dcff50f06565d2c9f Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 16 Jul 2024 14:19:47 -0400 Subject: [PATCH 22/76] Add sanity check to project opening --- include/mainwindow.h | 7 +- include/project.h | 3 + src/config.cpp | 2 + src/mainwindow.cpp | 165 +++++++++++++++++++++---------------------- src/project.cpp | 54 ++++++++++++++ 5 files changed, 143 insertions(+), 88 deletions(-) diff --git a/include/mainwindow.h b/include/mainwindow.h index bb4d4b91..e65605e0 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -344,9 +344,9 @@ private: bool setMap(QString, bool scrollTreeView = false); void redrawMapScene(); void refreshMapScene(); - bool loadDataStructures(); - bool loadProjectCombos(); - bool populateMapList(); + bool checkProjectSanity(); + bool loadProjectData(); + bool setProjectUI(); void sortMapList(); void openSubWindow(QWidget * window); QString getExistingDirectory(QString); @@ -376,7 +376,6 @@ private: void initMapSortOrder(); void initShortcuts(); void initExtraShortcuts(); - void setProjectSpecificUI(); void loadUserSettings(); void applyMapListFilter(QString filterText); void restoreWindowState(); diff --git a/include/project.h b/include/project.h index 90172f39..c9a1a0c0 100644 --- a/include/project.h +++ b/include/project.h @@ -97,6 +97,9 @@ public: DataQualifiers healLocationDataQualifiers; QString healLocationsTableName; + bool sanityCheck(); + bool load(); + QMap mapCache; Map* loadMap(QString); Map* getMap(QString); diff --git a/src/config.cpp b/src/config.cpp index 98681264..c294a0ee 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -890,6 +890,8 @@ void ProjectConfig::init() { if (dialog.exec() == QDialog::Accepted) { this->baseGameVersion = static_cast(baseGameVersionComboBox->currentData().toInt()); + } else { + // TODO: If user closes window Porymap assumes pokeemerald; it should instead abort project opening } } this->setUnreadKeys(); // Initialize version-specific options diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 3b48da1e..88c12103 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -413,35 +413,6 @@ void MainWindow::markMapEdited() { } } -// Update the UI using information we've read from the user's project files. -void MainWindow::setProjectSpecificUI() -{ - // Wild Encounters tab - // TODO: This index should come from an enum - ui->mainTabBar->setTabEnabled(4, editor->project->wildEncountersLoaded); - - bool hasFlags = projectConfig.mapAllowFlagsEnabled; - ui->checkBox_AllowRunning->setVisible(hasFlags); - ui->checkBox_AllowBiking->setVisible(hasFlags); - ui->checkBox_AllowEscaping->setVisible(hasFlags); - ui->label_AllowRunning->setVisible(hasFlags); - ui->label_AllowBiking->setVisible(hasFlags); - ui->label_AllowEscaping->setVisible(hasFlags); - - ui->newEventToolButton->newWeatherTriggerAction->setVisible(projectConfig.eventWeatherTriggerEnabled); - ui->newEventToolButton->newSecretBaseAction->setVisible(projectConfig.eventSecretBaseEnabled); - ui->newEventToolButton->newCloneObjectAction->setVisible(projectConfig.eventCloneObjectEnabled); - - bool floorNumEnabled = projectConfig.floorNumberEnabled; - ui->spinBox_FloorNumber->setVisible(floorNumEnabled); - ui->label_FloorNumber->setVisible(floorNumEnabled); - - Event::setIcons(); - editor->setCollisionGraphics(); - ui->spinBox_SelectedElevation->setMaximum(Block::getMaxElevation()); - ui->spinBox_SelectedCollision->setMaximum(Block::getMaxCollision()); -} - void MainWindow::mapSortOrder_changed(QAction *action) { QList items = ui->toolButton_MapSortOrder->menu()->actions(); @@ -528,13 +499,13 @@ void MainWindow::setTheme(QString theme) { } bool MainWindow::openProject(const QString &dir, bool initial) { - if (!this->closeProject()) { - logInfo("Aborted project open."); - return false; - } - if (dir.isNull() || dir.length() <= 0) { - if (!initial) setWindowDisabled(true); + // If this happened on startup it's because the user has no recent projects, which is fine. + // This shouldn't happen otherwise, but if it does then display an error. + if (!initial) { + logError("Failed to open project: Directory name cannot be empty"); + showProjectOpenFailure(); + } return false; } @@ -553,6 +524,13 @@ bool MainWindow::openProject(const QString &dir, bool initial) { return false; } + // The above checks can fail and the user will be allowed to continue with their currently-opened project (if there is one). + // We close the current project below, after which either the new project will open successfully or the window will be disabled. + if (!this->closeProject()) { + logInfo("Aborted project open."); + return false; + } + const QString openMessage = QString("Opening %1").arg(projectString); this->statusBar()->showMessage(openMessage); logInfo(openMessage); @@ -577,8 +555,14 @@ bool MainWindow::openProject(const QString &dir, bool initial) { }); this->editor->project->set_root(dir); + // Make sure project looks reasonable before attempting to load it + if (!checkProjectSanity()) { + delete this->editor->project; + return false; + } + // Load the project - if (!(loadDataStructures() && populateMapList() && setInitialMap())) { + if (!(loadProjectData() && setProjectUI() && setInitialMap())) { this->statusBar()->showMessage(QString("Failed to open %1").arg(projectString)); showProjectOpenFailure(); delete this->editor->project; @@ -606,12 +590,39 @@ bool MainWindow::openProject(const QString &dir, bool initial) { return true; } +bool MainWindow::loadProjectData() { + bool success = editor->project->load(); + Scripting::populateGlobalObject(this); + return success; +} + +bool MainWindow::checkProjectSanity() { + if (editor->project->sanityCheck()) + return true; + + QMessageBox msgBox; + msgBox.setIcon(QMessageBox::Critical); + msgBox.setText(QString("The selected directory appears to be invalid.")); + msgBox.setInformativeText(QString("The directory '%1' is missing key files.\n\n" + "Make sure you selected the correct project directory " + "(the one used to make your .gba file, e.g. 'pokeemerald').").arg(editor->project->root)); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setDefaultButton(QMessageBox::Ok); + auto tryAnyway = msgBox.addButton("Try Anyway", QMessageBox::ActionRole); + msgBox.exec(); + if (msgBox.clickedButton() == tryAnyway) { + // The user has chosen to try to load this project anyway. + // This will almost certainly fail, but they'll get a more specific error message. + return true; + } + return false; +} + void MainWindow::showProjectOpenFailure() { QString errorMsg = QString("There was an error opening the project. Please see %1 for full error details.").arg(getLogPath()); QMessageBox error(QMessageBox::Critical, "porymap", errorMsg, QMessageBox::Ok, this); error.setDetailedText(getMostRecentError()); error.exec(); - setWindowDisabled(true); } bool MainWindow::isProjectOpen() { @@ -975,45 +986,8 @@ void MainWindow::on_spinBox_FloorNumber_valueChanged(int offset) } } -bool MainWindow::loadDataStructures() { - Project *project = editor->project; - bool success = project->readMapLayouts() - && project->readRegionMapSections() - && project->readItemNames() - && project->readFlagNames() - && project->readVarNames() - && project->readMovementTypes() - && project->readInitialFacingDirections() - && project->readMapTypes() - && project->readMapBattleScenes() - && project->readWeatherNames() - && project->readCoordEventWeatherNames() - && project->readSecretBaseIds() - && project->readBgEventFacingDirections() - && project->readTrainerTypes() - && project->readMetatileBehaviors() - && project->readFieldmapProperties() - && project->readFieldmapMasks() - && project->readTilesetLabels() - && project->readTilesetMetatileLabels() - && project->readHealLocations() - && project->readMiscellaneousConstants() - && project->readSpeciesIconPaths() - && project->readWildMonData() - && project->readEventScriptLabels() - && project->readObjEventGfxConstants() - && project->readEventGraphics() - && project->readSongNames(); - - project->applyParsedLimits(); - setProjectSpecificUI(); - Scripting::populateGlobalObject(this); - - return success && loadProjectCombos(); -} - -bool MainWindow::loadProjectCombos() { - // set up project ui comboboxes +// Update the UI using information we've read from the user's project files. +bool MainWindow::setProjectUI() { Project *project = editor->project; // Block signals to the comboboxes while they are being modified @@ -1025,6 +999,7 @@ bool MainWindow::loadProjectCombos() { const QSignalBlocker blocker6(ui->comboBox_BattleScene); const QSignalBlocker blocker7(ui->comboBox_Type); + // Set up project comboboxes ui->comboBox_Song->clear(); ui->comboBox_Song->addItems(project->songNames); ui->comboBox_Location->clear(); @@ -1040,15 +1015,36 @@ bool MainWindow::loadProjectCombos() { ui->comboBox_Type->clear(); ui->comboBox_Type->addItems(project->mapTypes); - return true; -} + sortMapList(); -bool MainWindow::populateMapList() { - bool success = editor->project->readMapGroups(); - if (success) { - sortMapList(); - } - return success; + // Show/hide parts of the UI that are dependent on the user's project settings + + // Wild Encounters tab + // TODO: This index should come from an enum + ui->mainTabBar->setTabEnabled(4, editor->project->wildEncountersLoaded); + + bool hasFlags = projectConfig.mapAllowFlagsEnabled; + ui->checkBox_AllowRunning->setVisible(hasFlags); + ui->checkBox_AllowBiking->setVisible(hasFlags); + ui->checkBox_AllowEscaping->setVisible(hasFlags); + ui->label_AllowRunning->setVisible(hasFlags); + ui->label_AllowBiking->setVisible(hasFlags); + ui->label_AllowEscaping->setVisible(hasFlags); + + ui->newEventToolButton->newWeatherTriggerAction->setVisible(projectConfig.eventWeatherTriggerEnabled); + ui->newEventToolButton->newSecretBaseAction->setVisible(projectConfig.eventSecretBaseEnabled); + ui->newEventToolButton->newCloneObjectAction->setVisible(projectConfig.eventCloneObjectEnabled); + + bool floorNumEnabled = projectConfig.floorNumberEnabled; + ui->spinBox_FloorNumber->setVisible(floorNumEnabled); + ui->label_FloorNumber->setVisible(floorNumEnabled); + + Event::setIcons(); + editor->setCollisionGraphics(); + ui->spinBox_SelectedElevation->setMaximum(Block::getMaxElevation()); + ui->spinBox_SelectedCollision->setMaximum(Block::getMaxCollision()); + + return true; } void MainWindow::sortMapList() { @@ -3019,6 +3015,7 @@ bool MainWindow::closeProject() { } } editor->closeProject(); + setWindowDisabled(true); return true; } diff --git a/src/project.cpp b/src/project.cpp index d50c305e..7048812a 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -91,6 +91,60 @@ void Project::set_root(QString dir) { this->parser.set_root(dir); } +// Before attempting the initial project load we should check for a few notable files. +// If all are missing then we can warn the user, they may have accidentally selected the wrong folder. +bool Project::sanityCheck() { + // The goal with the file selection is to pick files that are important enough that any reasonable project would have + // at least 1 in the expected location, but unique enough that they're unlikely to overlap with a completely unrelated + // directory (e.g. checking for 'data/maps/' is a bad choice because it's too generic, pokeyellow would pass for instance) + static const QSet pathsToCheck = { + ProjectFilePath::json_map_groups, + ProjectFilePath::json_layouts, + ProjectFilePath::tilesets_headers, + ProjectFilePath::global_fieldmap, + }; + for (auto pathId : pathsToCheck) { + const QString path = QString("%1/%2").arg(this->root).arg(projectConfig.getFilePath(pathId)); + QFileInfo fileInfo(path); + if (fileInfo.exists() && fileInfo.isFile()) + return true; + } + return false; +} + +bool Project::load() { + bool success = readMapLayouts() + && readRegionMapSections() + && readItemNames() + && readFlagNames() + && readVarNames() + && readMovementTypes() + && readInitialFacingDirections() + && readMapTypes() + && readMapBattleScenes() + && readWeatherNames() + && readCoordEventWeatherNames() + && readSecretBaseIds() + && readBgEventFacingDirections() + && readTrainerTypes() + && readMetatileBehaviors() + && readFieldmapProperties() + && readFieldmapMasks() + && readTilesetLabels() + && readTilesetMetatileLabels() + && readHealLocations() + && readMiscellaneousConstants() + && readSpeciesIconPaths() + && readWildMonData() + && readEventScriptLabels() + && readObjEventGfxConstants() + && readEventGraphics() + && readSongNames() + && readMapGroups(); + applyParsedLimits(); + return success; +} + QString Project::getProjectTitle() { if (!root.isNull()) { return root.section('/', -1); From 4c25813bd33069c39f3222b9cfaf9d2b7834cb09 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 16 Jul 2024 16:26:59 -0400 Subject: [PATCH 23/76] Add project close option --- forms/mainwindow.ui | 9 +++ include/config.h | 2 + include/editor.h | 33 ++++++---- include/mainwindow.h | 4 +- include/scripting.h | 1 + src/config.cpp | 11 ++-- src/editor.cpp | 116 +++++++++++++++++++++++++++++------- src/mainwindow.cpp | 70 +++++++++++++++++++--- src/scriptapi/scripting.cpp | 19 +++--- 9 files changed, 214 insertions(+), 51 deletions(-) diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index a28f1db9..8e586198 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -3061,6 +3061,7 @@ + @@ -3415,6 +3416,14 @@ QAction::ApplicationSpecificRole + + + Close Project + + + Ctrl+W + + diff --git a/include/config.h b/include/config.h index 7e87e986..3f77aa90 100644 --- a/include/config.h +++ b/include/config.h @@ -54,6 +54,7 @@ public: } virtual void reset() override { this->recentProjects.clear(); + this->projectManuallyClosed = false; this->reopenOnLaunch = true; this->mapSortOrder = MapSortOrder::Group; this->prettyCursors = true; @@ -99,6 +100,7 @@ public: QMap getCustomScriptsEditorGeometry(); bool reopenOnLaunch; + bool projectManuallyClosed; MapSortOrder mapSortOrder; bool prettyCursors; int collisionOpacity; diff --git a/include/editor.h b/include/editor.h index 837b267c..8e30ae70 100644 --- a/include/editor.h +++ b/include/editor.h @@ -107,13 +107,13 @@ public: void updateWarpEventWarnings(); bool eventLimitReached(Map *, Event::Type); - QGraphicsScene *scene = nullptr; + QPointer scene = nullptr; QGraphicsPixmapItem *current_view = nullptr; - MapPixmapItem *map_item = nullptr; + QPointer map_item = nullptr; ConnectionPixmapItem* selected_connection_item = nullptr; QList connection_items; QGraphicsPathItem *connection_mask = nullptr; - CollisionPixmapItem *collision_item = nullptr; + QPointer collision_item = nullptr; QGraphicsItemGroup *events_group = nullptr; QList borderItems; QList gridLines; @@ -121,16 +121,15 @@ public: CursorTileRect *cursorMapTileRect = nullptr; MapRuler *map_ruler = nullptr; - QGraphicsScene *scene_metatiles = nullptr; - QGraphicsScene *scene_current_metatile_selection = nullptr; - QGraphicsScene *scene_selected_border_metatiles = nullptr; - QGraphicsScene *scene_collision_metatiles = nullptr; - QGraphicsScene *scene_elevation_metatiles = nullptr; - MetatileSelector *metatile_selector_item = nullptr; + QPointer scene_metatiles = nullptr; + QPointer scene_current_metatile_selection = nullptr; + QPointer scene_selected_border_metatiles = nullptr; + QPointer scene_collision_metatiles = nullptr; + QPointer metatile_selector_item = nullptr; - BorderMetatilesPixmapItem *selected_border_metatiles_item = nullptr; + QPointer selected_border_metatiles_item = nullptr; CurrentSelectedMetatilesPixmapItem *current_metatile_selection_item = nullptr; - MovementPermissionsSelector *movement_permissions_selector_item = nullptr; + QPointer movement_permissions_selector_item = nullptr; QList *selected_events = nullptr; @@ -168,6 +167,18 @@ private: const QImage collisionPlaceholder = QImage(":/images/collisions_unknown.png"); QPixmap collisionSheetPixmap; + void clearMap(); + void clearMetatileSelector(); + void clearMovementPermissionSelector(); + void clearMapMetatiles(); + void clearMapMovementPermissions(); + void clearBorderMetatiles(); + void clearCurrentMetatilesSelection(); + void clearMapEvents(); + //void clearMapConnections(); + void clearMapBorder(); + void clearMapGrid(); + void clearWildMonTables(); void setConnectionItemsVisible(bool); void setBorderItemsVisible(bool, qreal = 1); void setConnectionEditControlValues(MapConnection*); diff --git a/include/mainwindow.h b/include/mainwindow.h index e65605e0..41bf8a1e 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -167,6 +167,7 @@ public slots: private slots: void on_action_Open_Project_triggered(); void on_action_Reload_Project_triggered(); + void on_action_Close_Project_triggered(); void on_mapList_activated(const QModelIndex &index); void on_action_Save_Project_triggered(); void openWarpMap(QString map_name, int event_id, Event::Group event_group); @@ -347,10 +348,11 @@ private: bool checkProjectSanity(); bool loadProjectData(); bool setProjectUI(); + void clearProjectUI(); void sortMapList(); void openSubWindow(QWidget * window); QString getExistingDirectory(QString); - bool openProject(const QString &dir, bool initial = false); + bool openProject(QString dir, bool initial = false); bool closeProject(); void showProjectOpenFailure(); void saveGlobalConfigs(); diff --git a/include/scripting.h b/include/scripting.h index fb96e7ae..e2bfb1ff 100644 --- a/include/scripting.h +++ b/include/scripting.h @@ -30,6 +30,7 @@ class Scripting { public: Scripting(MainWindow *mainWindow); + static void stop(); static void init(MainWindow *mainWindow); static void populateGlobalObject(MainWindow *mainWindow); static QJSEngine *getEngine(); diff --git a/src/config.cpp b/src/config.cpp index c294a0ee..16853e8f 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -198,9 +198,9 @@ KeyValueConfigBase::~KeyValueConfigBase() { void KeyValueConfigBase::load() { reset(); QFile file(this->getConfigFilepath()); - if (!file.exists()) + if (!file.exists()) { this->init(); - else if (!file.open(QIODevice::ReadOnly)) { + } else if (!file.open(QIODevice::ReadOnly)) { logError(QString("Could not open config file '%1': ").arg(this->getConfigFilepath()) + file.errorString()); } @@ -306,6 +306,8 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) { if (key == "recent_project") { this->recentProjects = value.split(",", Qt::SkipEmptyParts); this->recentProjects.removeDuplicates(); + } else if (key == "project_manually_closed") { + this->projectManuallyClosed = getConfigBool(key, value); } else if (key == "reopen_on_launch") { this->reopenOnLaunch = getConfigBool(key, value); } else if (key == "pretty_cursors") { @@ -417,6 +419,7 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) { QMap PorymapConfig::getKeyValueMap() { QMap map; map.insert("recent_project", this->recentProjects.join(",")); + map.insert("project_manually_closed", this->projectManuallyClosed ? "1" : "0"); map.insert("reopen_on_launch", this->reopenOnLaunch ? "1" : "0"); map.insert("pretty_cursors", this->prettyCursors ? "1" : "0"); map.insert("map_sort_order", mapSortOrderMap.value(this->mapSortOrder)); @@ -882,7 +885,7 @@ void ProjectConfig::init() { baseGameVersionComboBox->addItem("pokeemerald", BaseGameVersion::pokeemerald); form.addRow(new QLabel("Game Version"), baseGameVersionComboBox); - // TODO: Advanced button to open the project settings window (with some settings disabled) + // TODO: Add an 'Advanced' button to open the project settings window (with some settings disabled) QDialogButtonBox buttonBox(QDialogButtonBox::Ok, Qt::Horizontal, &dialog); QObject::connect(&buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); @@ -891,7 +894,7 @@ void ProjectConfig::init() { if (dialog.exec() == QDialog::Accepted) { this->baseGameVersion = static_cast(baseGameVersionComboBox->currentData().toInt()); } else { - // TODO: If user closes window Porymap assumes pokeemerald; it should instead abort project opening + logWarn(QString("No base_game_version selected, using default '%1'").arg(getBaseGameVersionString(this->baseGameVersion))); } } this->setUnreadKeys(); // Initialize version-specific options diff --git a/src/editor.cpp b/src/editor.cpp index 9557ee13..d3116af4 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -81,8 +81,9 @@ void Editor::saveUiFields() { void Editor::closeProject() { if (!this->project) return; - Scripting::cb_ProjectClosed(this->project->root); + Scripting::stop(); + clearMap(); delete this->project; } @@ -204,9 +205,8 @@ void Editor::setEditingConnections() { this->cursorMapTileRect->setActive(false); } -void Editor::displayWildMonTables() { +void Editor::clearWildMonTables() { QStackedWidget *stack = ui->stackedWidget_WildMons; - QComboBox *labelCombo = ui->comboBox_EncounterGroupLabel; // delete widgets from previous map data if they exist while (stack->count()) { @@ -215,18 +215,24 @@ void Editor::displayWildMonTables() { delete oldWidget; } - labelCombo->clear(); + ui->comboBox_EncounterGroupLabel->clear(); +} + +void Editor::displayWildMonTables() { + clearWildMonTables(); // Don't try to read encounter data if it doesn't exist on disk for this map. if (!project->wildMonData.contains(map->constantName)) { return; } + QComboBox *labelCombo = ui->comboBox_EncounterGroupLabel; for (auto groupPair : project->wildMonData[map->constantName]) labelCombo->addItem(groupPair.first); labelCombo->setCurrentText(labelCombo->itemText(0)); + QStackedWidget *stack = ui->stackedWidget_WildMons; int labelIndex = 0; for (auto labelPair : project->wildMonData[map->constantName]) { @@ -1337,6 +1343,35 @@ void Editor::mouseEvent_collision(QGraphicsSceneMouseEvent *event, CollisionPixm } } +// On project close we want to leave the editor view empty. +// Otherwise a map is normally only cleared when a new one is being displayed. +void Editor::clearMap() { + clearMetatileSelector(); + clearMovementPermissionSelector(); + clearMapMetatiles(); + clearMapMovementPermissions(); + clearBorderMetatiles(); + clearCurrentMetatilesSelection(); + clearMapEvents(); + //clearMapConnections(); + clearMapBorder(); + clearMapGrid(); + clearWildMonTables(); + + // TODO: Handle connections after redesign PR. + selected_connection_item = nullptr; + connection_items.clear(); + connection_mask = nullptr; + + current_view = nullptr; + map = nullptr; + + // These are normally preserved between map displays, we only delete them now. + delete scene; + delete metatile_selector_item; + delete movement_permissions_selector_item; +} + bool Editor::displayMap() { if (!scene) { scene = new QGraphicsScene; @@ -1346,12 +1381,6 @@ bool Editor::displayMap() { scene->installEventFilter(this->map_ruler); } - if (map_item && scene) { - scene->removeItem(map_item); - delete map_item; - scene->removeItem(this->map_ruler); - } - displayMetatileSelector(); displayMovementPermissionSelector(); displayMapMetatiles(); @@ -1379,11 +1408,16 @@ bool Editor::displayMap() { return true; } -void Editor::displayMetatileSelector() { +void Editor::clearMetatileSelector() { if (metatile_selector_item && metatile_selector_item->scene()) { metatile_selector_item->scene()->removeItem(metatile_selector_item); delete scene_metatiles; } +} + +void Editor::displayMetatileSelector() { + clearMetatileSelector(); + scene_metatiles = new QGraphicsScene; if (!metatile_selector_item) { metatile_selector_item = new MetatileSelector(8, map); @@ -1408,7 +1442,17 @@ void Editor::displayMetatileSelector() { scene_metatiles->addItem(metatile_selector_item); } +void Editor::clearMapMetatiles() { + if (map_item && scene) { + scene->removeItem(map_item); + delete map_item; + scene->removeItem(this->map_ruler); + } +} + void Editor::displayMapMetatiles() { + clearMapMetatiles(); + map_item = new MapPixmapItem(map, this->metatile_selector_item, this->settings); connect(map_item, &MapPixmapItem::mouseEvent, this, &Editor::mouseEvent_map); connect(map_item, &MapPixmapItem::startPaint, this, &Editor::onMapStartPaint); @@ -1429,11 +1473,16 @@ void Editor::displayMapMetatiles() { ); } -void Editor::displayMapMovementPermissions() { +void Editor::clearMapMovementPermissions() { if (collision_item && scene) { scene->removeItem(collision_item); delete collision_item; } +} + +void Editor::displayMapMovementPermissions() { + clearMapMovementPermissions(); + collision_item = new CollisionPixmapItem(map, ui->spinBox_SelectedCollision, ui->spinBox_SelectedElevation, this->metatile_selector_item, this->settings, &this->collisionOpacity); connect(collision_item, &CollisionPixmapItem::mouseEvent, this, &Editor::mouseEvent_collision); @@ -1446,11 +1495,16 @@ void Editor::displayMapMovementPermissions() { scene->addItem(collision_item); } -void Editor::displayBorderMetatiles() { +void Editor::clearBorderMetatiles() { if (selected_border_metatiles_item && selected_border_metatiles_item->scene()) { selected_border_metatiles_item->scene()->removeItem(selected_border_metatiles_item); delete selected_border_metatiles_item; + delete scene_selected_border_metatiles; } +} + +void Editor::displayBorderMetatiles() { + clearBorderMetatiles(); scene_selected_border_metatiles = new QGraphicsScene; selected_border_metatiles_item = new BorderMetatilesPixmapItem(map, this->metatile_selector_item); @@ -1465,11 +1519,17 @@ void Editor::displayBorderMetatiles() { this, &Editor::onBorderMetatilesChanged); } -void Editor::displayCurrentMetatilesSelection() { +void Editor::clearCurrentMetatilesSelection() { if (current_metatile_selection_item && current_metatile_selection_item->scene()) { current_metatile_selection_item->scene()->removeItem(current_metatile_selection_item); delete current_metatile_selection_item; + current_metatile_selection_item = nullptr; + delete scene_current_metatile_selection; } +} + +void Editor::displayCurrentMetatilesSelection() { + clearCurrentMetatilesSelection(); scene_current_metatile_selection = new QGraphicsScene; current_metatile_selection_item = new CurrentSelectedMetatilesPixmapItem(map, this->metatile_selector_item); @@ -1485,11 +1545,15 @@ void Editor::redrawCurrentMetatilesSelection() { } } -void Editor::displayMovementPermissionSelector() { +void Editor::clearMovementPermissionSelector() { if (movement_permissions_selector_item && movement_permissions_selector_item->scene()) { movement_permissions_selector_item->scene()->removeItem(movement_permissions_selector_item); delete scene_collision_metatiles; } +} + +void Editor::displayMovementPermissionSelector() { + clearMovementPermissionSelector(); scene_collision_metatiles = new QGraphicsScene; if (!movement_permissions_selector_item) { @@ -1507,7 +1571,7 @@ void Editor::displayMovementPermissionSelector() { scene_collision_metatiles->addItem(movement_permissions_selector_item); } -void Editor::displayMapEvents() { +void Editor::clearMapEvents() { if (events_group) { for (QGraphicsItem *child : events_group->childItems()) { events_group->removeFromGroup(child); @@ -1519,9 +1583,13 @@ void Editor::displayMapEvents() { } delete events_group; + events_group = nullptr; } - selected_events->clear(); +} + +void Editor::displayMapEvents() { + clearMapEvents(); events_group = new QGraphicsItemGroup; scene->addItem(events_group); @@ -1626,7 +1694,7 @@ void Editor::maskNonVisibleConnectionTiles() { connection_mask = scene->addPath(mask, pen, brush); } -void Editor::displayMapBorder() { +void Editor::clearMapBorder() { for (QGraphicsPixmapItem* item : borderItems) { if (item->scene()) { item->scene()->removeItem(item); @@ -1634,6 +1702,10 @@ void Editor::displayMapBorder() { delete item; } borderItems.clear(); +} + +void Editor::displayMapBorder() { + clearMapBorder(); int borderWidth = map->getBorderWidth(); int borderHeight = map->getBorderHeight(); @@ -1646,7 +1718,7 @@ void Editor::displayMapBorder() { item->setX(x * 16); item->setY(y * 16); item->setZValue(-3); - scene->addItem(item); + scene->addItem(item); // TODO: If the scene is taking ownership here is a double-free possible? borderItems.append(item); } } @@ -1689,11 +1761,15 @@ void Editor::onToggleGridClicked(bool checked) { ui->graphicsView_Map->scene()->update(); } -void Editor::displayMapGrid() { +void Editor::clearMapGrid() { for (QGraphicsLineItem* item : gridLines) { if (item) delete item; } gridLines.clear(); +} + +void Editor::displayMapGrid() { + clearMapGrid(); ui->checkBox_ToggleGrid->disconnect(); int pixelWidth = map->getWidth() * 16; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 88c12103..3f90e133 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -70,7 +70,7 @@ MainWindow::MainWindow(QWidget *parent) : logInfo(QString("Launching Porymap v%1").arg(QCoreApplication::applicationVersion())); this->initWindow(); - if (porymapConfig.reopenOnLaunch && this->openProject(porymapConfig.getRecentProject(), true)) + if (porymapConfig.reopenOnLaunch && !porymapConfig.projectManuallyClosed && this->openProject(porymapConfig.getRecentProject(), true)) on_toolButton_Paint_clicked(); // there is a bug affecting macOS users, where the trackpad deilveres a bad touch-release gesture @@ -498,7 +498,7 @@ void MainWindow::setTheme(QString theme) { } } -bool MainWindow::openProject(const QString &dir, bool initial) { +bool MainWindow::openProject(QString dir, bool initial) { if (dir.isNull() || dir.length() <= 0) { // If this happened on startup it's because the user has no recent projects, which is fine. // This shouldn't happen otherwise, but if it does then display an error. @@ -526,7 +526,7 @@ bool MainWindow::openProject(const QString &dir, bool initial) { // The above checks can fail and the user will be allowed to continue with their currently-opened project (if there is one). // We close the current project below, after which either the new project will open successfully or the window will be disabled. - if (!this->closeProject()) { + if (!closeProject()) { logInfo("Aborted project open."); return false; } @@ -577,6 +577,7 @@ bool MainWindow::openProject(const QString &dir, bool initial) { showWindowTitle(); this->statusBar()->showMessage(QString("Opened %1").arg(projectString)); + porymapConfig.projectManuallyClosed = false; porymapConfig.addRecentProject(dir); refreshRecentProjectsMenu(); @@ -600,12 +601,14 @@ bool MainWindow::checkProjectSanity() { if (editor->project->sanityCheck()) return true; + logWarn(QString("The directory '%1' failed the project sanity check.").arg(editor->project->root)); + QMessageBox msgBox; msgBox.setIcon(QMessageBox::Critical); msgBox.setText(QString("The selected directory appears to be invalid.")); msgBox.setInformativeText(QString("The directory '%1' is missing key files.\n\n" "Make sure you selected the correct project directory " - "(the one used to make your .gba file, e.g. 'pokeemerald').").arg(editor->project->root)); + "(the one used to make your .gba file, e.g. 'pokeemerald').").arg(editor->project->root)); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setDefaultButton(QMessageBox::Ok); auto tryAnyway = msgBox.addButton("Try Anyway", QMessageBox::ActionRole); @@ -721,6 +724,11 @@ void MainWindow::on_action_Reload_Project_triggered() { openProject(editor->project->root); } +void MainWindow::on_action_Close_Project_triggered() { + closeProject(); + porymapConfig.projectManuallyClosed = true; +} + bool MainWindow::setMap(QString map_name, bool scrollTreeView) { logInfo(QString("Setting map to '%1'").arg(map_name)); if (map_name.isEmpty()) { @@ -1047,6 +1055,29 @@ bool MainWindow::setProjectUI() { return true; } +void MainWindow::clearProjectUI() { + // Block signals to the comboboxes while they are being modified + const QSignalBlocker blocker1(ui->comboBox_Song); + const QSignalBlocker blocker2(ui->comboBox_Location); + const QSignalBlocker blocker3(ui->comboBox_PrimaryTileset); + const QSignalBlocker blocker4(ui->comboBox_SecondaryTileset); + const QSignalBlocker blocker5(ui->comboBox_Weather); + const QSignalBlocker blocker6(ui->comboBox_BattleScene); + const QSignalBlocker blocker7(ui->comboBox_Type); + + ui->comboBox_Song->clear(); + ui->comboBox_Location->clear(); + ui->comboBox_PrimaryTileset->clear(); + ui->comboBox_SecondaryTileset->clear(); + ui->comboBox_Weather->clear(); + ui->comboBox_BattleScene->clear(); + ui->comboBox_Type->clear(); + + // Clear map list + mapListModel->clear(); + mapGroupItemsList->clear(); +} + void MainWindow::sortMapList() { Project *project = editor->project; @@ -2514,8 +2545,12 @@ void MainWindow::importMapFromAdvanceMap1_92() void MainWindow::showExportMapImageWindow(ImageExporterMode mode) { if (!editor->project) return; - if (!this->mapImageExporter) - this->mapImageExporter = new MapImageExporter(this, this->editor, mode); + // If the user is requesting this window again we assume it's for a new + // window (the map/mode may have changed), so delete the old window. + if (this->mapImageExporter) + delete this->mapImageExporter; + + this->mapImageExporter = new MapImageExporter(this, this->editor, mode); openSubWindow(this->mapImageExporter); } @@ -2972,24 +3007,41 @@ bool MainWindow::askToFixRegionMapEditor() { } // Attempt to close any open sub-windows of the main window, giving each a chance to abort the process. -// Each of these are expected to be a QPointer to a widget with WA_DeleteOnClose set, so manually deleting -// and nullifying the pointer members is not necessary here. +// Each of these windows is a widget with WA_DeleteOnClose set, so manually deleting them isn't necessary. +// Because they're tracked with QPointers nullifying them shouldn't be necessary either, but it seems the +// delete is happening too late and some of the pointers haven't been cleared by the time we need them to, +// so we nullify them all here anyway. bool MainWindow::closeSupplementaryWindows() { if (this->tilesetEditor && !this->tilesetEditor->close()) return false; + this->tilesetEditor = nullptr; + if (this->regionMapEditor && !this->regionMapEditor->close()) return false; + this->regionMapEditor = nullptr; + if (this->mapImageExporter && !this->mapImageExporter->close()) return false; + this->mapImageExporter = nullptr; + if (this->newMapPrompt && !this->newMapPrompt->close()) return false; + this->newMapPrompt = nullptr; + if (this->shortcutsEditor && !this->shortcutsEditor->close()) return false; + this->shortcutsEditor = nullptr; + if (this->preferenceEditor && !this->preferenceEditor->close()) return false; + this->preferenceEditor = nullptr; + if (this->customScriptsEditor && !this->customScriptsEditor->close()) return false; + this->customScriptsEditor = nullptr; + if (this->projectSettingsEditor) this->projectSettingsEditor->closeQuietly(); + this->projectSettingsEditor = nullptr; return true; } @@ -3014,8 +3066,10 @@ bool MainWindow::closeProject() { return false; } } + clearProjectUI(); editor->closeProject(); setWindowDisabled(true); + setWindowTitle(QCoreApplication::applicationName()); return true; } diff --git a/src/scriptapi/scripting.cpp b/src/scriptapi/scripting.cpp index 112585c7..82944be9 100644 --- a/src/scriptapi/scripting.cpp +++ b/src/scriptapi/scripting.cpp @@ -21,14 +21,19 @@ QMap callbackFunctions = { Scripting *instance = nullptr; +void Scripting::stop() { + if (!instance) return; + instance->engine->setInterrupted(true); + instance->scriptUtility->clearActions(); + qDeleteAll(instance->imageCache); + delete instance; + instance = nullptr; +} + void Scripting::init(MainWindow *mainWindow) { - mainWindow->ui->graphicsView_Map->clearOverlayMap(); - if (instance) { - instance->engine->setInterrupted(true); - instance->scriptUtility->clearActions(); - qDeleteAll(instance->imageCache); - delete instance; - } + if (mainWindow->ui->graphicsView_Map) + mainWindow->ui->graphicsView_Map->clearOverlayMap(); + Scripting::stop(); instance = new Scripting(mainWindow); } From 0012ea83e6c94cb68e330e24b196f968eefe6cfa Mon Sep 17 00:00:00 2001 From: GriffinR Date: Wed, 17 Jul 2024 12:31:07 -0400 Subject: [PATCH 24/76] Update changelog --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c9977a1..67e9a0b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,21 @@ and this project somewhat adheres to [Semantic Versioning](https://semver.org/sp The **"Breaking Changes"** listed below are changes that have been made in the decompilation projects (e.g. pokeemerald), which porymap requires in order to work properly. It also includes changes to the scripting API that may change the behavior of existing porymap scripts. If porymap is used with a project or API script that is not up-to-date with the breaking changes, then porymap will likely break or behave improperly. ## [Unreleased] +### Added +- Add a `Close Project` option +- An alert will be displayed when attempting to open a seemingly invalid project. + +### Changed +- The base game version is now auto-detected if the project name contains only one of "emerald", "firered/leafgreen", or "ruby/sapphire". +- It's now possible to cancel quitting if there are unsaved changes in sub-windows. + ### Fixed - Fix `Add Region Map...` not updating the region map settings file. - Fix some crashes on invalid region map tilesets. - Improve error reporting for invalid region map editor settings. +- Fix config files being written before the project is opened successfully. +- Fix the map and other project info still displaying if a new project fails to open. +- Fix unsaved changes being ignored when quitting (such as with Cmd+Q on macOS). ## [5.4.1] - 2024-03-21 ### Fixed From 96b5fb161711824386f2f14055121f02eac90350 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Fri, 12 Jul 2024 14:05:37 -0400 Subject: [PATCH 25/76] Performance & bug clean up --- forms/mainwindow.ui | 20 ++- include/core/map.h | 2 +- include/core/mapconnection.h | 6 + include/editor.h | 7 +- include/ui/connectionpixmapitem.h | 10 +- include/ui/noscrollcombobox.h | 1 + porymap.pro | 1 + src/core/map.cpp | 23 +-- src/core/mapconnection.cpp | 50 +++--- src/editor.cpp | 250 +++++++++++++----------------- src/mainwindow.cpp | 19 ++- src/project.cpp | 3 + src/ui/connectionpixmapitem.cpp | 68 ++++---- src/ui/connectionslistitem.cpp | 4 +- src/ui/noscrollcombobox.cpp | 6 + 15 files changed, 214 insertions(+), 256 deletions(-) diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index cdb4dcb8..c2f0647c 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -1715,7 +1715,7 @@ 0 0 100 - 16 + 30 @@ -1809,7 +1809,7 @@ 0 0 100 - 16 + 30 @@ -1903,7 +1903,7 @@ 0 0 100 - 16 + 30 @@ -2003,7 +2003,7 @@ 0 0 100 - 16 + 30 @@ -2097,7 +2097,7 @@ 0 0 100 - 16 + 30 @@ -2596,6 +2596,9 @@ true + + true + @@ -2667,6 +2670,9 @@ true + + true + @@ -2757,8 +2763,8 @@ 0 0 - 365 - 658 + 100 + 30 diff --git a/include/core/map.h b/include/core/map.h index a6f6fa1c..83f9a9d1 100644 --- a/include/core/map.h +++ b/include/core/map.h @@ -98,7 +98,7 @@ public: QStringList getScriptLabels(Event::Group group = Event::Group::None); void removeEvent(Event *); void addEvent(Event *); - QPixmap renderConnection(MapConnection, MapLayout *); + QPixmap renderConnection(const MapConnection&, MapLayout *); QPixmap renderBorder(bool ignoreCache = false); void setDimensions(int newWidth, int newHeight, bool setNewBlockdata = true, bool enableScriptCallback = false); void setBorderDimensions(int newWidth, int newHeight, bool setNewBlockdata = true, bool enableScriptCallback = false); diff --git a/include/core/mapconnection.h b/include/core/mapconnection.h index 474be5b1..a7077813 100644 --- a/include/core/mapconnection.h +++ b/include/core/mapconnection.h @@ -12,6 +12,12 @@ public: QString direction; int offset; QString map_name; + + static const QStringList cardinalDirections; + static bool isCardinal(const QString &direction); + static bool isHorizontal(const QString &direction); + static bool isVertical(const QString &direction); + static MapConnection mirror(const MapConnection &source, const QString &mapName); }; struct MapConnectionMirror { diff --git a/include/editor.h b/include/editor.h index f0b54b69..7c878023 100644 --- a/include/editor.h +++ b/include/editor.h @@ -168,14 +168,13 @@ private: QPixmap collisionSheetPixmap; void updateBorderVisibility(); - QPoint calculateConnectionPosition(const MapConnection *connection, const QPixmap &pixmap); + QPoint calculateConnectionPosition(const MapConnection &connection, const QPixmap &pixmap); + QPixmap getConnectionPixmap(const MapConnection &connection); void updateConnectionItem(ConnectionPixmapItem* connectionItem); void updateConnectionItemPos(ConnectionPixmapItem* connectionItem); void createConnectionItem(MapConnection* connection); - void populateConnectionsList(); void addConnectionToList(ConnectionPixmapItem* connection); void updateDiveEmergeMap(QString mapName, QString direction); - bool shouldMirrorConnection(const MapConnection &source); MapConnectionMirror getMirroredConnection(const MapConnection&); void addMirroredConnection(const MapConnection&); void removeMirroredConnection(const MapConnection&); @@ -197,7 +196,7 @@ private slots: void setConnectionOffset(MapConnection *connection, int offset); void setConnectionMap(MapConnection *connection, const QString &mapName); void setConnectionDirection(MapConnection *connection, const QString &direction); - void onConnectionItemSelected(ConnectionPixmapItem* connectionItem); + void setSelectedConnection(ConnectionPixmapItem* connectionItem); void onHoveredMovementPermissionChanged(uint16_t, uint16_t); void onHoveredMovementPermissionCleared(); void onHoveredMetatileSelectionChanged(uint16_t); diff --git a/include/ui/connectionpixmapitem.h b/include/ui/connectionpixmapitem.h index f0e00ae1..4f18b649 100644 --- a/include/ui/connectionpixmapitem.h +++ b/include/ui/connectionpixmapitem.h @@ -28,13 +28,14 @@ public: int initialOffset; int baseMapWidth; int baseMapHeight; - void render(qreal opacity = 1); + void setEditable(bool editable); bool getEditable(); - void updateHighlight(bool selected); + void setSelected(bool selected); + void render(); private: - bool highlighted = false; + bool selected = false; protected: QVariant itemChange(GraphicsItemChange change, const QVariant &value); @@ -42,10 +43,9 @@ protected: void mouseDoubleClickEvent(QGraphicsSceneMouseEvent*); signals: - void connectionItemSelected(ConnectionPixmapItem* connectionItem); void connectionItemDoubleClicked(ConnectionPixmapItem* connectionItem); void connectionMoved(MapConnection *, int newOffset); - void highlightChanged(bool highlighted); + void selectionChanged(bool selected); }; #endif // CONNECTIONPIXMAPITEM_H diff --git a/include/ui/noscrollcombobox.h b/include/ui/noscrollcombobox.h index 59c680a3..43db9deb 100644 --- a/include/ui/noscrollcombobox.h +++ b/include/ui/noscrollcombobox.h @@ -13,6 +13,7 @@ public: void setTextItem(const QString &text); void setNumberItem(int value); void setHexItem(uint32_t value); + void setClearButtonEnabled(bool enabled); private: void setItem(int index, const QString &text); diff --git a/porymap.pro b/porymap.pro index 5c898302..8afc99a5 100644 --- a/porymap.pro +++ b/porymap.pro @@ -24,6 +24,7 @@ SOURCES += src/core/block.cpp \ src/core/heallocation.cpp \ src/core/imageexport.cpp \ src/core/map.cpp \ + src/core/mapconnection.cpp \ src/core/maplayout.cpp \ src/core/mapparser.cpp \ src/core/metatile.cpp \ diff --git a/src/core/map.cpp b/src/core/map.cpp index 463b9341..fad4683a 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -217,34 +217,21 @@ QPixmap Map::renderBorder(bool ignoreCache) { return layout->border_pixmap; } -QPixmap Map::renderConnection(MapConnection connection, MapLayout * fromLayout) { - int x, y, w, h; +QPixmap Map::renderConnection(const MapConnection &connection, MapLayout * fromLayout) { + if (!MapConnection::isCardinal(connection.direction)) + return QPixmap(); + + int x = 0, y = 0, w = getWidth(), h = getHeight(); if (connection.direction == "up") { - x = 0; y = getHeight() - BORDER_DISTANCE; - w = getWidth(); h = BORDER_DISTANCE; } else if (connection.direction == "down") { - x = 0; - y = 0; - w = getWidth(); h = BORDER_DISTANCE; } else if (connection.direction == "left") { x = getWidth() - BORDER_DISTANCE; - y = 0; w = BORDER_DISTANCE; - h = getHeight(); } else if (connection.direction == "right") { - x = 0; - y = 0; w = BORDER_DISTANCE; - h = getHeight(); - } else { - // this should not happen - x = 0; - y = 0; - w = getWidth(); - h = getHeight(); } render(true, fromLayout, QRect(x, y, w, h)); diff --git a/src/core/mapconnection.cpp b/src/core/mapconnection.cpp index ebf48592..82a0b71e 100644 --- a/src/core/mapconnection.cpp +++ b/src/core/mapconnection.cpp @@ -1,43 +1,35 @@ #include "mapconnection.h" #include "map.h" -void MapConnection::setDirection(const QString & direction) { - if (direction == m_direction) - return; - auto before = m_direction; - m_direction = direction; - emit directionChanged(before, m_direction); -} +const QStringList MapConnection::cardinalDirections = { + "up", "down", "left", "right" +}; -void MapConnection::setOffset(int offset) { - if (offset == m_offset) - return; - auto before = m_offset; - m_offset = offset; - emit offsetChanged(before, m_offset); -} - -void MapConnection::setMapName(const QString &mapName) { - if (mapName == m_mapName) - return; - auto before = m_mapName; - m_mapName = mapName; - emit mapNameChanged(before, m_mapName); -} -/* -static QString MapConnection::oppositeDirection(const QString &direction) { +MapConnection MapConnection::mirror(const MapConnection &source, const QString &mapName) { static const QMap oppositeDirections = { {"up", "down"}, {"down", "up"}, {"right", "left"}, {"left", "right"}, {"dive", "emerge"}, {"emerge", "dive"} }; - return oppositeDirections.value(direction); + + // TODO: Allowing editing unknown directions is a can of worms. + // Specifically a self-connection with an empty direction and an offset of 0 can be identified as its own mirror + MapConnection mirror; + mirror.direction = oppositeDirections.value(source.direction/*, source.direction*/); + mirror.map_name = mapName; + mirror.offset = -source.offset; + + return mirror; } -static MapConnection* MapConnection::getMirror(const MapConnection*, const Map*) { - +bool MapConnection::isHorizontal(const QString &direction) { + return direction == "left" || direction == "right"; } -static MapConnection* MapConnection::newMirror(const MapConnection*) { +bool MapConnection::isVertical(const QString &direction) { + return direction == "up" || direction == "down"; +} -}*/ +bool MapConnection::isCardinal(const QString &direction) { + return cardinalDirections.contains(direction); +} diff --git a/src/editor.cpp b/src/editor.cpp index b88ac4cf..0f23f331 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -89,7 +89,6 @@ void Editor::setEditingMap() { current_view = map_item; if (map_item) { map_item->paintingMode = MapPixmapItem::PaintMode::Metatiles; - displayMapConnections(); map_item->draw(); map_item->setVisible(true); } @@ -109,7 +108,6 @@ void Editor::setEditingMap() { void Editor::setEditingCollision() { current_view = collision_item; if (collision_item) { - displayMapConnections(); collision_item->draw(); collision_item->setVisible(true); } @@ -135,7 +133,6 @@ void Editor::setEditingObjects() { } if (map_item) { map_item->paintingMode = MapPixmapItem::PaintMode::EventObjects; - displayMapConnections(); map_item->draw(); map_item->setVisible(true); } @@ -172,8 +169,6 @@ void Editor::setEditingConnections() { map_item->paintingMode = MapPixmapItem::PaintMode::Disabled; map_item->draw(); map_item->setVisible(true); - populateConnectionsList(); - maskNonVisibleConnectionTiles(); } if (collision_item) { collision_item->setVisible(false); @@ -722,58 +717,15 @@ void Editor::updateEncounterFields(EncounterFields newFields) { project->wildMonFields = newFields; } -void Editor::populateConnectionsList() { - const QSignalBlocker blocker1(ui->comboBox_DiveMap); - const QSignalBlocker blocker2(ui->comboBox_EmergeMap); - - // TODO: We should probably just be doing this once, and updating the items when new maps are created. - ui->comboBox_DiveMap->clear(); - ui->comboBox_EmergeMap->clear(); - ui->comboBox_DiveMap->addItems(project->mapNames); - ui->comboBox_EmergeMap->addItems(project->mapNames); - ui->comboBox_DiveMap->setCurrentText(""); - ui->comboBox_EmergeMap->setCurrentText(""); - ui->comboBox_DiveMap->lineEdit()->setClearButtonEnabled(true); - ui->comboBox_EmergeMap->lineEdit()->setClearButtonEnabled(true); - - for (MapConnection* connection : map->connections) { - if (connection->direction == "dive") { - ui->comboBox_DiveMap->setCurrentText(connection->map_name); - } else if (connection->direction == "emerge") { - ui->comboBox_EmergeMap->setCurrentText(connection->map_name); - } - } - - // Clear any existing connections in list - for (auto listItem : ui->scrollAreaContents_ConnectionsList->findChildren()) - listItem->deleteLater(); - - for (auto item :connection_items) - addConnectionToList(item); -} - -// TODO: When connecting a map to itself the new mirror is shaded incorrectly -// TODO: Set default focus to the selected connection, right now it defaults to the dive combo box - void Editor::addConnectionToList(ConnectionPixmapItem * connectionItem) { ConnectionsListItem *listItem = new ConnectionsListItem(ui->scrollAreaContents_ConnectionsList, connectionItem->connection, project->mapNames); ui->layout_ConnectionsList->insertWidget(ui->layout_ConnectionsList->count() - 1, listItem); // Insert above the vertical spacer // Sync the selection highlight between the list UI and the graphical map connection - connect(connectionItem, &ConnectionPixmapItem::highlightChanged, listItem, &ConnectionsListItem::setSelected); + connect(connectionItem, &ConnectionPixmapItem::selectionChanged, listItem, &ConnectionsListItem::setSelected); connect(listItem, &ConnectionsListItem::selected, [this, connectionItem] { - // When the list item is selected, select the pixmap too - if (connectionItem == selected_connection_item) { - // Already selected, no change - return; - } - // Deselect old connection and select new connection - if (selected_connection_item) selected_connection_item->updateHighlight(false); - selected_connection_item = connectionItem; - selected_connection_item->updateHighlight(true); + setSelectedConnection(connectionItem); }); - if (connectionItem == selected_connection_item) - listItem->setSelected(true); // Sync edits to 'offset' between the list UI and the graphical map connection connect(connectionItem, &ConnectionPixmapItem::connectionMoved, [this, listItem](MapConnection* connection, int offset) { @@ -787,9 +739,10 @@ void Editor::addConnectionToList(ConnectionPixmapItem * connectionItem) { // Sync edits to 'direction' or 'map' between the list UI and the graphical map connection. // These are 1-way because the direction and map cannot be edited graphically, only via the list UI. - connect(listItem, &ConnectionsListItem::editedDirection, [this, connectionItem](MapConnection* connection, QString direction) { + connect(listItem, &ConnectionsListItem::editedDirection, [this, connectionItem, listItem](MapConnection* connection, QString direction) { setConnectionDirection(connection, direction); - updateConnectionItem(connectionItem); // TODO: Simplify? + updateConnectionItem(connectionItem); // TODO: Simplify + listItem->updateUI(); // Changing direction may have changed our offset too }); connect(listItem, &ConnectionsListItem::editedMapName, [this, connectionItem](MapConnection* connection, QString mapName) { setConnectionMap(connection, mapName); @@ -813,13 +766,13 @@ void Editor::addConnectionToList(ConnectionPixmapItem * connectionItem) { void Editor::addNewConnection() { // Find direction with least number of connections. - QMap directionCounts = QMap({{"up", 0}, {"right", 0}, {"down", 0}, {"left", 0}}); + QMap directionCounts; for (MapConnection* connection : map->connections) { directionCounts[connection->direction]++; } QString minDirection = "up"; int minCount = INT_MAX; - for (QString direction : directionCounts.keys()) { + for (QString direction : MapConnection::cardinalDirections) { if (directionCounts[direction] < minCount) { minDirection = direction; minCount = directionCounts[direction]; @@ -827,7 +780,6 @@ void Editor::addNewConnection() { } // Prefer not to connect the map to itself (we have to if it's the only map). - // TODO: Is this more or less sensible than a connection with no map name QString defaultMapName = project->mapNames.first(); if (defaultMapName == map->name && project->mapNames.length() > 1) { defaultMapName = project->mapNames.at(1); @@ -838,11 +790,14 @@ void Editor::addNewConnection() { newConnection->offset = 0; newConnection->map_name = defaultMapName; addConnection(map, newConnection); - onConnectionItemSelected(connection_items.last()); + setSelectedConnection(connection_items.last()); addMirroredConnection(*newConnection); } void Editor::addConnection(Map* map, MapConnection * connection) { + if (!map || !connection) + return; + map->connections.append(connection); if (map == this->map) { // Adding a connection to the current map, we need to display it visually. @@ -857,7 +812,6 @@ void Editor::addConnection(Map* map, MapConnection * connection) { ui->comboBox_EmergeMap->setCurrentText(connection->map_name); } else { createConnectionItem(connection); - addConnectionToList(connection_items.last()); } } emit editedMapData(map); @@ -885,7 +839,7 @@ void Editor::removeConnectionItem(ConnectionPixmapItem* connectionItem, bool rem if (connectionItem == selected_connection_item) { selected_connection_item = nullptr; if (!connection_items.isEmpty()) - onConnectionItemSelected(connection_items.first()); + setSelectedConnection(connection_items.first()); } delete connectionItem; } @@ -902,22 +856,9 @@ void Editor::removeSelectedConnection() { removeConnectionItem(selected_connection_item); } -static const QMap oppositeDirections = { - {"up", "down"}, {"down", "up"}, - {"right", "left"}, {"left", "right"}, - {"dive", "emerge"}, {"emerge", "dive"} -}; - -bool Editor::shouldMirrorConnection(const MapConnection &source) { - // The current map's connections are not mirrored if the user has disabled this setting, - // unless it's a connection to itself (which is mirrored by definition). - // TODO: Confirm in-game representation of the above fact is accurate. - return ui->checkBox_MirrorConnections->isChecked() || (map && map->name == source.map_name); -} - MapConnectionMirror Editor::getMirroredConnection(const MapConnection &source) { MapConnectionMirror mirror; - if (!map || !shouldMirrorConnection(source)) + if (!map || !ui->checkBox_MirrorConnections->isChecked()) return mirror; // Note: It's possible (and ok) for mirror.map == this->map @@ -925,12 +866,10 @@ MapConnectionMirror Editor::getMirroredConnection(const MapConnection &source) { if (!mirror.map) return mirror; - MapConnection target; - target.direction = oppositeDirections.value(source.direction); - target.map_name = map->name; - target.offset = -source.offset; - // Find the matching connection in the connected map. + // Note: There is no strict source -> mirror pairing, i.e. we are not guaranteed + // to always get the same MapConnection if there are multiple identical copies. + MapConnection target = MapConnection::mirror(source, map->name); for (auto connection : mirror.map->connections) { if (*connection == target) { mirror.connection = connection; @@ -946,10 +885,7 @@ void Editor::addMirroredConnection(const MapConnection &source) { return; mirror.connection = new MapConnection; - mirror.connection->direction = oppositeDirections.value(source.direction); - mirror.connection->map_name = map->name; - mirror.connection->offset = -source.offset; - + *mirror.connection = MapConnection::mirror(source, map->name); addConnection(mirror.map, mirror.connection); } @@ -993,7 +929,7 @@ void Editor::updateDiveEmergeMap(QString mapName, QString direction) { return; } - // TODO: How does the game handle having multiple Dive/Emerge maps. Should we support this? + // Only the first Dive/Emerge map (if present) is considered, as in-game. MapConnection* connection = nullptr; for (MapConnection* conn : map->connections) { if (conn->direction == direction) { @@ -1002,14 +938,18 @@ void Editor::updateDiveEmergeMap(QString mapName, QString direction) { } } - // We (lazily) always update the connection by deleting the old one and creating a new one. - // Unlike the displayed connections which can be updated rapidly by click+drag, this isn't - // really an issue for Dive/Emerge maps. + // Dive/Emerge maps aren't represented visually (aside from their combo boxes), + // so we have less to do here than a normal connection. if (connection) { - removeMirroredConnection(*connection); - removeConnection(map, connection); - } - if (!mapName.isEmpty() && mapName != DYNAMIC_MAP_NAME) { + // Update existing connection + if (mapName.isEmpty()) { + removeMirroredConnection(*connection); + removeConnection(map, connection); + } else { + setConnectionMap(connection, mapName); + } + } else if (!mapName.isEmpty()) { + // Create new connection connection = new MapConnection; connection->direction = direction; connection->offset = 0; @@ -1019,25 +959,36 @@ void Editor::updateDiveEmergeMap(QString mapName, QString direction) { } } -QPoint Editor::calculateConnectionPosition(const MapConnection *connection, const QPixmap &pixmap) { +QPoint Editor::calculateConnectionPosition(const MapConnection &connection, const QPixmap &pixmap) { int x = 0, y = 0; const int mWidth = 16, mHeight = 16; - if (connection->direction == "up") { - x = connection->offset * mWidth; + if (connection.direction == "up") { + x = connection.offset * mWidth; y = -pixmap.height(); - } else if (connection->direction == "down") { - x = connection->offset * mWidth; + } else if (connection.direction == "down") { + x = connection.offset * mWidth; y = map->getHeight() * mHeight; - } else if (connection->direction == "left") { + } else if (connection.direction == "left") { x = -pixmap.width(); - y = connection->offset * mHeight; - } else if (connection->direction == "right") { + y = connection.offset * mHeight; + } else if (connection.direction == "right") { x = map->getWidth() * mWidth; - y = connection->offset * mHeight; + y = connection.offset * mHeight; } return QPoint(x, y); } +QPixmap Editor::getConnectionPixmap(const MapConnection &connection) { + Map *connectedMap = project->getMap(connection.map_name); + + // connectedMap will be null for MAP_DYNAMIC and any map that fails to load. + // The connection will be editable from the list, but no image will be displayed on the map. + if (!connectedMap) + return QPixmap(); + + return connectedMap->renderConnection(connection, map->layout); +} + void Editor::updateConnectionItem(ConnectionPixmapItem* connectionItem) { if (!connectionItem || !connectionItem->connection) return; @@ -1046,29 +997,22 @@ void Editor::updateConnectionItem(ConnectionPixmapItem* connectionItem) { if (mapName.isEmpty()) return; - // TODO: What happens if a connection is saved with an empty name - if (mapName == DYNAMIC_MAP_NAME || !project->mapNames.contains(mapName)) { + if (!project->mapNames.contains(mapName)) { logError(QString("Invalid map name '%1' specified for connection.").arg(mapName)); return; } - Map *connectedMap = project->getMap(mapName); - if (!connectedMap) - return; - - QPixmap pixmap = connectedMap->renderConnection(*connectionItem->connection, map->layout); connectionItem->initialOffset = connectionItem->connection->offset; - connectionItem->basePixmap = pixmap; - QPoint pos = calculateConnectionPosition(connectionItem->connection, pixmap); + connectionItem->basePixmap = getConnectionPixmap(*connectionItem->connection); + QPoint pos = calculateConnectionPosition(*connectionItem->connection, connectionItem->basePixmap); connectionItem->blockSignals(true); - connectionItem->setPixmap(pixmap); - connectionItem->updateHighlight(connectionItem == selected_connection_item); + connectionItem->setPixmap(connectionItem->basePixmap); connectionItem->initialX = pos.x(); connectionItem->initialY = pos.y(); connectionItem->setX(pos.x()); connectionItem->setY(pos.y()); - connectionItem->setZValue(-1); + connectionItem->render(); connectionItem->blockSignals(false); maskNonVisibleConnectionTiles(); @@ -1080,9 +1024,9 @@ void Editor::updateConnectionItemPos(ConnectionPixmapItem* connectionItem) { MapConnection *connection = connectionItem->connection; connectionItem->blockSignals(true); - if (connection->direction == "up" || connection->direction == "down") { + if (MapConnection::isVertical(connection->direction)) { connectionItem->setX(connectionItem->initialX + (connection->offset - connectionItem->initialOffset) * 16); - } else if (connection->direction == "left" || connection->direction == "right") { + } else if (MapConnection::isHorizontal(connection->direction)) { connectionItem->setY(connectionItem->initialY + (connection->offset - connectionItem->initialOffset) * 16); } connectionItem->blockSignals(false); @@ -1115,7 +1059,6 @@ void Editor::setConnectionOffset(MapConnection *connection, int offset) { } } } - // TODO: If there's no mirror but there should be we're not creating one connection->offset = offset; emit editedMapData(map); @@ -1141,19 +1084,26 @@ void Editor::setConnectionDirection(MapConnection *connection, const QString &di // TODO: Lazy removeMirroredConnection(*connection); + + if (MapConnection::isHorizontal(connection->direction) != MapConnection::isHorizontal(direction) + || MapConnection::isVertical(connection->direction) != MapConnection::isVertical(direction)) { + // If the direction has changed between vertical/horizontal then the old offset may not make sense, so we reset it + setConnectionOffset(connection, 0); + } connection->direction = direction; + addMirroredConnection(*connection); emit editedMapData(map); } -void Editor::onConnectionItemSelected(ConnectionPixmapItem* connectionItem) { - if (!connectionItem) +void Editor::setSelectedConnection(ConnectionPixmapItem* connectionItem) { + if (!connectionItem || connectionItem == selected_connection_item) return; + if (selected_connection_item) selected_connection_item->setSelected(false); selected_connection_item = connectionItem; - for (ConnectionPixmapItem* item : connection_items) - item->updateHighlight(item == selected_connection_item); + selected_connection_item->setSelected(true); } // TODO: Inaccurate if there are multiple connections from the same map @@ -1161,7 +1111,7 @@ void Editor::setSelectedConnectionFromMap(QString mapName) { // Search for the first connection that connects to the given map map. for (ConnectionPixmapItem* item : connection_items) { if (item->connection->map_name == mapName) { - onConnectionItemSelected(item); + setSelectedConnection(item); break; } } @@ -1606,6 +1556,7 @@ bool Editor::displayMap() { displayMapBorder(); displayMapGrid(); displayWildMonTables(); + maskNonVisibleConnectionTiles(); this->map_ruler->setZValue(1000); scene->addItem(this->map_ruler); @@ -1795,41 +1746,56 @@ void Editor::displayMapConnections() { selected_connection_item = nullptr; connection_items.clear(); + const QSignalBlocker blocker1(ui->comboBox_DiveMap); + const QSignalBlocker blocker2(ui->comboBox_EmergeMap); + ui->comboBox_DiveMap->clear(); + ui->comboBox_EmergeMap->clear(); + ui->comboBox_DiveMap->addItems(project->mapNames); + ui->comboBox_EmergeMap->addItems(project->mapNames); + ui->comboBox_DiveMap->setCurrentText(""); + ui->comboBox_EmergeMap->setCurrentText(""); + + // Note: We only support editing 1 Dive and Emerge connection per map. + // In a vanilla game only the first Dive/Emerge connection is considered, so allowing + // users to have multiple is likely to lead to confusion. In case users have changed + // this we won't delete extra diving connections, but we'll only display the first one. for (MapConnection *connection : map->connections) { - if (connection->direction == "dive" || connection->direction == "emerge") { - continue; + if (connection->direction == "dive") { + if (ui->comboBox_DiveMap->currentText().isEmpty()) + ui->comboBox_DiveMap->setCurrentText(connection->map_name); + } else if (connection->direction == "emerge") { + if (ui->comboBox_EmergeMap->currentText().isEmpty()) + ui->comboBox_EmergeMap->setCurrentText(connection->map_name); + } else { + // We allow any unknown direction names here. They'll be editable from the list menu. + createConnectionItem(connection); } - createConnectionItem(connection); } - if (!connection_items.empty()) { - onConnectionItemSelected(connection_items.first()); - } - - maskNonVisibleConnectionTiles(); + if (!connection_items.empty()) + setSelectedConnection(connection_items.first()); } void Editor::createConnectionItem(MapConnection* connection) { - Map *connected_map = project->getMap(connection->map_name); - if (!connected_map) { + if (!connection) return; - } - - QPixmap pixmap = connected_map->renderConnection(*connection, map->layout); - QPoint pos = calculateConnectionPosition(connection, pixmap); - + QPixmap pixmap = getConnectionPixmap(*connection); + QPoint pos = calculateConnectionPosition(*connection, pixmap); ConnectionPixmapItem *item = new ConnectionPixmapItem(pixmap, connection, pos.x(), pos.y(), map->getWidth(), map->getHeight()); item->setX(pos.x()); item->setY(pos.y()); - item->setZValue(-1); + item->render(); scene->addItem(item); - connect(item, &ConnectionPixmapItem::connectionItemSelected, this, &Editor::onConnectionItemSelected); + connect(item, &ConnectionPixmapItem::selectionChanged, [this, item](bool selected) { + if (selected) setSelectedConnection(item); + }); connect(item, &ConnectionPixmapItem::connectionItemDoubleClicked, [this, item] { emit this->connectionItemDoubleClicked(item->connection->map_name, map->name); }); connection_items.append(item); + addConnectionToList(item); } // Hides connected map tiles that cannot be seen from the current map (beyond BORDER_DISTANCE). @@ -1891,14 +1857,12 @@ void Editor::updateMapBorder() { } void Editor::updateMapConnections() { - for (int i = 0; i < connection_items.size(); i++) { - Map *connected_map = project->getMap(connection_items[i]->connection->map_name); - if (!connected_map) + for (auto item : connection_items) { + if (!item->connection) continue; - - QPixmap pixmap = connected_map->renderConnection(*(connection_items[i]->connection), map->layout); - connection_items[i]->basePixmap = pixmap; - connection_items[i]->setPixmap(pixmap); + QPixmap pixmap = getConnectionPixmap(*item->connection); + item->basePixmap = pixmap; + item->setPixmap(pixmap); } maskNonVisibleConnectionTiles(); @@ -1992,7 +1956,7 @@ void Editor::updateBorderVisibility() { item->setVisible(visible); item->setEditable(editingConnections); item->setEnabled(visible); - item->updateHighlight(item == selected_connection_item); + item->render(); } } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 38af29d2..19377a49 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -736,6 +736,13 @@ void MainWindow::on_action_Reload_Project_triggered() { // setMap, but with a visible error message in case of failure. // Use when the user is specifically requesting a map to open. bool MainWindow::userSetMap(QString map_name, bool scrollTreeView) { + if (map_name == DYNAMIC_MAP_NAME) { + QMessageBox msgBox(this); + QString errorMsg = QString("The map '%1' can't be opened, it's a placeholder to indicate the specified map will be set programmatically.").arg(map_name); + msgBox.critical(nullptr, "Error Opening Map", errorMsg); + return false; + } + if (!setMap(map_name, scrollTreeView)) { QMessageBox msgBox(this); QString errorMsg = QString("There was an error opening map %1. Please see %2 for full error details.\n\n%3") @@ -750,7 +757,7 @@ bool MainWindow::userSetMap(QString map_name, bool scrollTreeView) { bool MainWindow::setMap(QString map_name, bool scrollTreeView) { logInfo(QString("Setting map to '%1'").arg(map_name)); - if (map_name.isEmpty()) { + if (map_name.isEmpty() || map_name == DYNAMIC_MAP_NAME) { return false; } @@ -828,10 +835,6 @@ void MainWindow::refreshMapScene() } void MainWindow::openWarpMap(QString map_name, int event_id, Event::Group event_group) { - // Can't warp to dynamic maps - if (map_name == DYNAMIC_MAP_NAME) - return; - // Ensure valid destination map name. if (!editor->project->mapNames.contains(map_name)) { logError(QString("Invalid map name '%1'").arg(map_name)); @@ -1846,6 +1849,8 @@ void MainWindow::on_mainTabBar_tabBarClicked(int index) clickToolButtonFromEditMode(editor->obj_edit_mode); } else if (index == MainTab::Connections) { editor->setEditingConnections(); + // Stop the Dive/Emerge combo boxes from getting the initial focus + ui->graphicsView_Map->setFocus(); } if (index != MainTab::WildPokemon) { if (editor->project && editor->project->wildEncountersLoaded) @@ -2577,13 +2582,13 @@ void MainWindow::on_pushButton_ConfigureEncountersJSON_clicked() { void MainWindow::on_button_OpenDiveMap_clicked() { const QString mapName = ui->comboBox_DiveMap->currentText(); - if (mapName != DYNAMIC_MAP_NAME && editor->project->mapNames.contains(mapName)) + if (editor->project->mapNames.contains(mapName)) userSetMap(mapName, true); } void MainWindow::on_button_OpenEmergeMap_clicked() { const QString mapName = ui->comboBox_EmergeMap->currentText(); - if (mapName != DYNAMIC_MAP_NAME && editor->project->mapNames.contains(mapName)) + if (editor->project->mapNames.contains(mapName)) userSetMap(mapName, true); } diff --git a/src/project.cpp b/src/project.cpp index c6550c6a..06382168 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -117,6 +117,9 @@ void Project::clearTilesetCache() { } Map* Project::loadMap(QString map_name) { + if (map_name == DYNAMIC_MAP_NAME) + return nullptr; + Map *map; if (mapCache.contains(map_name)) { map = mapCache.value(map_name); diff --git a/src/ui/connectionpixmapitem.cpp b/src/ui/connectionpixmapitem.cpp index 060f084f..1b7743f9 100644 --- a/src/ui/connectionpixmapitem.cpp +++ b/src/ui/connectionpixmapitem.cpp @@ -2,15 +2,28 @@ #include -void ConnectionPixmapItem::render(qreal opacity) { - QPixmap newPixmap = this->basePixmap.copy(0, 0, this->basePixmap.width(), this->basePixmap.height()); - if (opacity < 1) { - QPainter painter(&newPixmap); - int alpha = static_cast(255 * (1 - opacity)); - painter.fillRect(0, 0, newPixmap.width(), newPixmap.height(), QColor(0, 0, 0, alpha)); - painter.end(); +void ConnectionPixmapItem::render() { + QPixmap pixmap = this->basePixmap.copy(0, 0, this->basePixmap.width(), this->basePixmap.height()); + this->setZValue(-1); + + // When editing is inactive the current selection is ignored, all connections should appear normal. + if (this->getEditable()) { + if (this->selected) { + // Draw highlight + QPainter painter(&pixmap); + painter.setPen(QColor(255, 0, 255)); + painter.drawRect(0, 0, pixmap.width() - 1, pixmap.height() - 1); + painter.end(); + } else { + // Darken the image + this->setZValue(-2); + QPainter painter(&pixmap); + int alpha = static_cast(255 * 0.25); + painter.fillRect(0, 0, pixmap.width(), pixmap.height(), QColor(0, 0, 0, alpha)); + painter.end(); + } } - this->setPixmap(newPixmap); + this->setPixmap(pixmap); } QVariant ConnectionPixmapItem::itemChange(GraphicsItemChange change, const QVariant &value) @@ -20,7 +33,7 @@ QVariant ConnectionPixmapItem::itemChange(GraphicsItemChange change, const QVari qreal x, y; int newOffset = this->initialOffset; - if (this->connection->direction == "up" || this->connection->direction == "down") { + if (MapConnection::isVertical(this->connection->direction)) { x = round(newPos.x() / 16) * 16; newOffset += (x - initialX) / 16; x = newOffset * 16; @@ -29,7 +42,7 @@ QVariant ConnectionPixmapItem::itemChange(GraphicsItemChange change, const QVari x = this->initialX; } - if (this->connection->direction == "right" || this->connection->direction == "left") { + if (MapConnection::isHorizontal(this->connection->direction)) { y = round(newPos.y() / 16) * 16; newOffset += (y - this->initialY) / 16; y = newOffset * 16; @@ -56,41 +69,18 @@ bool ConnectionPixmapItem::getEditable() { return (this->flags() & ItemIsMovable) != 0; } -// TODO: Consider whether it still makes sense to highlight the "current" connection (given you can edit them at any point now) -void ConnectionPixmapItem::updateHighlight(bool selected) { - const int normalZ = -1; - const qreal normalOpacity = 1.0; - - // When editing is inactive the current selection is ignored, all connections should appear normal. - if (!this->getEditable()) { - this->setZValue(normalZ); - this->render(normalOpacity); +void ConnectionPixmapItem::setSelected(bool selected) { + if (this->selected == selected) return; - } - - this->setZValue(selected ? normalZ : -2); - this->render(selected ? normalOpacity : 0.75); - - if (selected) { - // Draw highlight - QPixmap pixmap = this->pixmap(); - QPainter painter(&pixmap); - painter.setPen(QColor(255, 0, 255)); - painter.drawRect(0, 0, pixmap.width() - 1, pixmap.height() - 1); - painter.end(); - this->setPixmap(pixmap); - } - - // Let the list UI know if the selection highlight changes so it can update accordingly - if (this->highlighted != selected) - emit highlightChanged(selected); - this->highlighted = selected; + this->selected = selected; + this->render(); + emit selectionChanged(selected); } void ConnectionPixmapItem::mousePressEvent(QGraphicsSceneMouseEvent *) { if (!this->getEditable()) return; - emit connectionItemSelected(this); + this->setSelected(true); } void ConnectionPixmapItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *) { diff --git a/src/ui/connectionslistitem.cpp b/src/ui/connectionslistitem.cpp index f40ae57f..cf6e3020 100644 --- a/src/ui/connectionslistitem.cpp +++ b/src/ui/connectionslistitem.cpp @@ -1,8 +1,6 @@ #include "connectionslistitem.h" #include "ui_connectionslistitem.h" -static const QStringList directions = {"up", "down", "left", "right"}; - ConnectionsListItem::ConnectionsListItem(QWidget *parent, MapConnection * connection, const QStringList &mapNames) : QFrame(parent), ui(new Ui::ConnectionsListItem) @@ -15,7 +13,7 @@ ConnectionsListItem::ConnectionsListItem(QWidget *parent, MapConnection * connec ui->comboBox_Direction->setEditable(false); ui->comboBox_Direction->setMinimumContentsLength(0); - ui->comboBox_Direction->addItems(directions); + ui->comboBox_Direction->addItems(MapConnection::cardinalDirections); ui->comboBox_Map->setMinimumContentsLength(6); ui->comboBox_Map->addItems(mapNames); diff --git a/src/ui/noscrollcombobox.cpp b/src/ui/noscrollcombobox.cpp index ee778df5..fcb662c7 100644 --- a/src/ui/noscrollcombobox.cpp +++ b/src/ui/noscrollcombobox.cpp @@ -1,6 +1,7 @@ #include "noscrollcombobox.h" #include +#include NoScrollComboBox::NoScrollComboBox(QWidget *parent) : QComboBox(parent) @@ -61,3 +62,8 @@ void NoScrollComboBox::setHexItem(uint32_t value) { this->setItem(this->findData(value), "0x" + QString::number(value, 16).toUpper()); } + +void NoScrollComboBox::setClearButtonEnabled(bool enabled) { + if (this->lineEdit()) + this->lineEdit()->setClearButtonEnabled(enabled); +} From 9ca5f6bc15745e286539012225b4bd96a7362edd Mon Sep 17 00:00:00 2001 From: GriffinR Date: Thu, 18 Jul 2024 13:30:56 -0400 Subject: [PATCH 26/76] More map connection bug fixes --- include/editor.h | 6 +-- include/ui/connectionpixmapitem.h | 8 ++-- src/core/mapconnection.cpp | 2 - src/editor.cpp | 61 ++++++++++++++----------------- 4 files changed, 34 insertions(+), 43 deletions(-) diff --git a/include/editor.h b/include/editor.h index 7c878023..f1ddb9bf 100644 --- a/include/editor.h +++ b/include/editor.h @@ -175,9 +175,9 @@ private: void createConnectionItem(MapConnection* connection); void addConnectionToList(ConnectionPixmapItem* connection); void updateDiveEmergeMap(QString mapName, QString direction); - MapConnectionMirror getMirroredConnection(const MapConnection&); - void addMirroredConnection(const MapConnection&); - void removeMirroredConnection(const MapConnection&); + MapConnectionMirror getMirroredConnection(MapConnection*); + void addMirroredConnection(MapConnection*); + void removeMirroredConnection(MapConnection*); void updateEncounterFields(EncounterFields newFields); QString getMovementPermissionText(uint16_t collision, uint16_t elevation); QString getMetatileDisplayMessage(uint16_t metatileId); diff --git a/include/ui/connectionpixmapitem.h b/include/ui/connectionpixmapitem.h index 4f18b649..a856eb45 100644 --- a/include/ui/connectionpixmapitem.h +++ b/include/ui/connectionpixmapitem.h @@ -8,7 +8,7 @@ class ConnectionPixmapItem : public QObject, public QGraphicsPixmapItem { Q_OBJECT public: - ConnectionPixmapItem(QPixmap pixmap, MapConnection* connection, int x, int y, int baseMapWidth, int baseMapHeight) + ConnectionPixmapItem(QPixmap pixmap, MapConnection* connection, int x, int y) : QGraphicsPixmapItem(pixmap), connection(connection) { @@ -18,16 +18,14 @@ public: this->initialX = x; this->initialY = y; this->initialOffset = connection->offset; - this->baseMapWidth = baseMapWidth; - this->baseMapHeight = baseMapHeight; + this->setX(x); + this->setY(y); } QPixmap basePixmap; MapConnection* const connection; int initialX; int initialY; int initialOffset; - int baseMapWidth; - int baseMapHeight; void setEditable(bool editable); bool getEditable(); diff --git a/src/core/mapconnection.cpp b/src/core/mapconnection.cpp index 82a0b71e..b249ef56 100644 --- a/src/core/mapconnection.cpp +++ b/src/core/mapconnection.cpp @@ -12,8 +12,6 @@ MapConnection MapConnection::mirror(const MapConnection &source, const QString & {"dive", "emerge"}, {"emerge", "dive"} }; - // TODO: Allowing editing unknown directions is a can of worms. - // Specifically a self-connection with an empty direction and an offset of 0 can be identified as its own mirror MapConnection mirror; mirror.direction = oppositeDirections.value(source.direction/*, source.direction*/); mirror.map_name = mapName; diff --git a/src/editor.cpp b/src/editor.cpp index 0f23f331..67d5690a 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -791,7 +791,7 @@ void Editor::addNewConnection() { newConnection->map_name = defaultMapName; addConnection(map, newConnection); setSelectedConnection(connection_items.last()); - addMirroredConnection(*newConnection); + addMirroredConnection(newConnection); } void Editor::addConnection(Map* map, MapConnection * connection) { @@ -821,16 +821,14 @@ void Editor::removeConnectionItem(ConnectionPixmapItem* connectionItem, bool rem if (!connectionItem) return; - if (connectionItem->connection) { - if (removeMirror) { - // If a map is connected to itself and we delete the connection then this function - // will be called twice, once for the target of the delete and once for its mirror. - // We only want to try deleting the mirror once, on the first call (because the mirror - // of the mirror is the original target again, and we already deleted it). - removeMirroredConnection(*connectionItem->connection); - } - removeConnection(map, connectionItem->connection); + if (removeMirror) { + // If a map is connected to itself and we delete the connection then this function + // will be called twice, once for the target of the delete and once for its mirror. + // We only want to try deleting the mirror once, on the first call (because the mirror + // of the mirror is the original target again, and we already deleted it). + removeMirroredConnection(connectionItem->connection); } + removeConnection(map, connectionItem->connection); connection_items.removeOne(connectionItem); if (connectionItem->scene()) @@ -856,22 +854,22 @@ void Editor::removeSelectedConnection() { removeConnectionItem(selected_connection_item); } -MapConnectionMirror Editor::getMirroredConnection(const MapConnection &source) { +MapConnectionMirror Editor::getMirroredConnection(MapConnection* source) { MapConnectionMirror mirror; - if (!map || !ui->checkBox_MirrorConnections->isChecked()) + if (!map || !source || !ui->checkBox_MirrorConnections->isChecked()) return mirror; // Note: It's possible (and ok) for mirror.map == this->map - mirror.map = project->getMap(source.map_name); + mirror.map = project->getMap(source->map_name); if (!mirror.map) return mirror; // Find the matching connection in the connected map. // Note: There is no strict source -> mirror pairing, i.e. we are not guaranteed // to always get the same MapConnection if there are multiple identical copies. - MapConnection target = MapConnection::mirror(source, map->name); + MapConnection target = MapConnection::mirror(*source, map->name); for (auto connection : mirror.map->connections) { - if (*connection == target) { + if (*connection == target && connection != source) { mirror.connection = connection; break; } @@ -879,27 +877,27 @@ MapConnectionMirror Editor::getMirroredConnection(const MapConnection &source) { return mirror; } -void Editor::addMirroredConnection(const MapConnection &source) { +void Editor::addMirroredConnection(MapConnection* source) { MapConnectionMirror mirror = getMirroredConnection(source); if (!mirror.map) return; mirror.connection = new MapConnection; - *mirror.connection = MapConnection::mirror(source, map->name); + *mirror.connection = MapConnection::mirror(*source, map->name); addConnection(mirror.map, mirror.connection); } -void Editor::removeMirroredConnection(const MapConnection &source) { +void Editor::removeMirroredConnection(MapConnection* source) { MapConnectionMirror mirror = getMirroredConnection(source); if (!mirror.map || !mirror.connection) return; if (map == mirror.map) { // The connection to delete is displayed on the currently-opened map, we need to delete it visually as well. - if (source.direction == "dive") { + if (source->direction == "dive") { const QSignalBlocker blocker(ui->comboBox_DiveMap); ui->comboBox_DiveMap->setCurrentText(""); - } else if (source.direction == "emerge") { + } else if (source->direction == "emerge") { const QSignalBlocker blocker(ui->comboBox_EmergeMap); ui->comboBox_EmergeMap->setCurrentText(""); } else { @@ -943,7 +941,7 @@ void Editor::updateDiveEmergeMap(QString mapName, QString direction) { if (connection) { // Update existing connection if (mapName.isEmpty()) { - removeMirroredConnection(*connection); + removeMirroredConnection(connection); removeConnection(map, connection); } else { setConnectionMap(connection, mapName); @@ -955,7 +953,7 @@ void Editor::updateDiveEmergeMap(QString mapName, QString direction) { connection->offset = 0; connection->map_name = mapName; addConnection(map, connection); - addMirroredConnection(*connection); + addMirroredConnection(connection); } } @@ -1037,7 +1035,7 @@ void Editor::setConnectionOffset(MapConnection *connection, int offset) { if (!connection || !map || connection->offset == offset) return; - MapConnectionMirror mirror = getMirroredConnection(*connection); + MapConnectionMirror mirror = getMirroredConnection(connection); if (mirror.connection && mirror.map) { mirror.connection->offset = -offset; if (mirror.map != map) { @@ -1071,9 +1069,9 @@ void Editor::setConnectionMap(MapConnection *connection, const QString &mapName) if (!connection || !map || connection->map_name == mapName) return; - removeMirroredConnection(*connection); + removeMirroredConnection(connection); connection->map_name = mapName; - addMirroredConnection(*connection); + addMirroredConnection(connection); emit editedMapData(map); } @@ -1083,7 +1081,7 @@ void Editor::setConnectionDirection(MapConnection *connection, const QString &di return; // TODO: Lazy - removeMirroredConnection(*connection); + removeMirroredConnection(connection); if (MapConnection::isHorizontal(connection->direction) != MapConnection::isHorizontal(direction) || MapConnection::isVertical(connection->direction) != MapConnection::isVertical(direction)) { @@ -1092,7 +1090,7 @@ void Editor::setConnectionDirection(MapConnection *connection, const QString &di } connection->direction = direction; - addMirroredConnection(*connection); + addMirroredConnection(connection); emit editedMapData(map); } @@ -1781,9 +1779,7 @@ void Editor::createConnectionItem(MapConnection* connection) { return; QPixmap pixmap = getConnectionPixmap(*connection); QPoint pos = calculateConnectionPosition(*connection, pixmap); - ConnectionPixmapItem *item = new ConnectionPixmapItem(pixmap, connection, pos.x(), pos.y(), map->getWidth(), map->getHeight()); - item->setX(pos.x()); - item->setY(pos.y()); + ConnectionPixmapItem *item = new ConnectionPixmapItem(pixmap, connection, pos.x(), pos.y()); item->render(); scene->addItem(item); @@ -1860,9 +1856,8 @@ void Editor::updateMapConnections() { for (auto item : connection_items) { if (!item->connection) continue; - QPixmap pixmap = getConnectionPixmap(*item->connection); - item->basePixmap = pixmap; - item->setPixmap(pixmap); + item->basePixmap = getConnectionPixmap(*item->connection); + item->render(); } maskNonVisibleConnectionTiles(); From 80497805a157973184af3347e752b4f7c7b231ea Mon Sep 17 00:00:00 2001 From: GriffinR Date: Wed, 24 Jul 2024 12:00:34 -0400 Subject: [PATCH 27/76] Save project config on close --- src/editor.cpp | 1 + src/mainwindow.cpp | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/editor.cpp b/src/editor.cpp index d3116af4..54194a96 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -81,6 +81,7 @@ void Editor::saveUiFields() { void Editor::closeProject() { if (!this->project) return; + this->project->saveConfig(); Scripting::cb_ProjectClosed(this->project->root); Scripting::stop(); clearMap(); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 3f90e133..721a3fa3 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -571,8 +571,7 @@ bool MainWindow::openProject(QString dir, bool initial) { } // Only create the config files once the project has opened successfully in case the user selected an invalid directory - userConfig.save(); - projectConfig.save(); + this->editor->project->saveConfig(); showWindowTitle(); this->statusBar()->showMessage(QString("Opened %1").arg(projectString)); From c8434c85b31d55182d121a94da7269f1c2f5700f Mon Sep 17 00:00:00 2001 From: GriffinR Date: Wed, 24 Jul 2024 13:52:46 -0400 Subject: [PATCH 28/76] Clean up project close for connections tab --- CHANGELOG.md | 1 + include/editor.h | 3 ++- src/editor.cpp | 27 ++++++++++++++------------- src/mainwindow.cpp | 20 ++++++++++++++++++++ src/project.cpp | 18 +++++++++++------- 5 files changed, 48 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67e9a0b3..2a687aad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - Fix config files being written before the project is opened successfully. - Fix the map and other project info still displaying if a new project fails to open. - Fix unsaved changes being ignored when quitting (such as with Cmd+Q on macOS). +- New maps are now always inserted in map dropdowns at the correct position, rather than at the bottom of the list until the project is reloaded. ## [5.4.1] - 2024-03-21 ### Fixed diff --git a/include/editor.h b/include/editor.h index 6d1f88d4..2c566abc 100644 --- a/include/editor.h +++ b/include/editor.h @@ -174,7 +174,8 @@ private: void clearBorderMetatiles(); void clearCurrentMetatilesSelection(); void clearMapEvents(); - //void clearMapConnections(); + void clearMapConnections(); + void clearConnectionMask(); void clearMapBorder(); void clearMapGrid(); void clearWildMonTables(); diff --git a/src/editor.cpp b/src/editor.cpp index de28ebe0..f427676b 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -1546,16 +1546,13 @@ void Editor::clearMap() { clearBorderMetatiles(); clearCurrentMetatilesSelection(); clearMapEvents(); - //clearMapConnections(); + clearMapConnections(); clearMapBorder(); clearMapGrid(); clearWildMonTables(); + clearConnectionMask(); - // TODO: Handle connections after redesign PR. - selected_connection_item = nullptr; - connection_items.clear(); - connection_mask = nullptr; - + // Clear pointers to objects deleted elsewhere current_view = nullptr; map = nullptr; @@ -1804,7 +1801,7 @@ DraggablePixmapItem *Editor::addMapEvent(Event *event) { return object; } -void Editor::displayMapConnections() { +void Editor::clearMapConnections() { for (ConnectionPixmapItem* item : connection_items) { if (item->scene()) { item->scene()->removeItem(item); @@ -1813,13 +1810,13 @@ void Editor::displayMapConnections() { } selected_connection_item = nullptr; connection_items.clear(); +} + +void Editor::displayMapConnections() { + clearMapConnections(); const QSignalBlocker blocker1(ui->comboBox_DiveMap); const QSignalBlocker blocker2(ui->comboBox_EmergeMap); - ui->comboBox_DiveMap->clear(); - ui->comboBox_EmergeMap->clear(); - ui->comboBox_DiveMap->addItems(project->mapNames); - ui->comboBox_EmergeMap->addItems(project->mapNames); ui->comboBox_DiveMap->setCurrentText(""); ui->comboBox_EmergeMap->setCurrentText(""); @@ -1864,8 +1861,7 @@ void Editor::createConnectionItem(MapConnection* connection) { addConnectionToList(item); } -// Hides connected map tiles that cannot be seen from the current map (beyond BORDER_DISTANCE). -void Editor::maskNonVisibleConnectionTiles() { +void Editor::clearConnectionMask() { if (connection_mask) { if (connection_mask->scene()) { connection_mask->scene()->removeItem(connection_mask); @@ -1873,6 +1869,11 @@ void Editor::maskNonVisibleConnectionTiles() { delete connection_mask; connection_mask = nullptr; } +} + +// Hides connected map tiles that cannot be seen from the current map (beyond BORDER_DISTANCE). +void Editor::maskNonVisibleConnectionTiles() { + clearConnectionMask(); QPainterPath mask; mask.addRect(scene->itemsBoundingRect().toRect()); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index f08ce4b3..fc7c72f3 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1031,6 +1031,8 @@ bool MainWindow::setProjectUI() { const QSignalBlocker blocker5(ui->comboBox_Weather); const QSignalBlocker blocker6(ui->comboBox_BattleScene); const QSignalBlocker blocker7(ui->comboBox_Type); + const QSignalBlocker blocker8(ui->comboBox_DiveMap); + const QSignalBlocker blocker9(ui->comboBox_EmergeMap); // Set up project comboboxes ui->comboBox_Song->clear(); @@ -1047,6 +1049,10 @@ bool MainWindow::setProjectUI() { ui->comboBox_BattleScene->addItems(project->mapBattleScenes); ui->comboBox_Type->clear(); ui->comboBox_Type->addItems(project->mapTypes); + ui->comboBox_DiveMap->clear(); + ui->comboBox_DiveMap->addItems(project->mapNames); + ui->comboBox_EmergeMap->clear(); + ui->comboBox_EmergeMap->addItems(project->mapNames); sortMapList(); @@ -1088,6 +1094,8 @@ void MainWindow::clearProjectUI() { const QSignalBlocker blocker5(ui->comboBox_Weather); const QSignalBlocker blocker6(ui->comboBox_BattleScene); const QSignalBlocker blocker7(ui->comboBox_Type); + const QSignalBlocker blocker8(ui->comboBox_DiveMap); + const QSignalBlocker blocker9(ui->comboBox_EmergeMap); ui->comboBox_Song->clear(); ui->comboBox_Location->clear(); @@ -1096,6 +1104,8 @@ void MainWindow::clearProjectUI() { ui->comboBox_Weather->clear(); ui->comboBox_BattleScene->clear(); ui->comboBox_Type->clear(); + ui->comboBox_DiveMap->clear(); + ui->comboBox_EmergeMap->clear(); // Clear map list mapListModel->clear(); @@ -1301,6 +1311,16 @@ void MainWindow::onNewMapCreated() { sortMapList(); setMap(newMapName, true); + // Refresh any combo box that displays map names and persists between maps + // (others combo boxes like for warp destinations are repopulated when the map changes). + int index = this->editor->project->mapNames.indexOf(newMapName); + if (index >= 0) { + const QSignalBlocker blocker1(ui->comboBox_DiveMap); + const QSignalBlocker blocker2(ui->comboBox_EmergeMap); + ui->comboBox_DiveMap->insertItem(index, newMapName); + ui->comboBox_EmergeMap->insertItem(index, newMapName); + } + if (newMap->needsHealLocation) { addNewEvent(Event::Type::HealLocation); editor->project->saveHealLocations(newMap); diff --git a/src/project.cpp b/src/project.cpp index 6764839b..42487269 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -1819,18 +1819,22 @@ bool Project::readMapGroups() { } Map* Project::addNewMapToGroup(QString mapName, int groupNum, Map *newMap, bool existingLayout, bool importedMap) { - mapNames.append(mapName); - mapGroups.insert(mapName, groupNum); - groupedMapNames[groupNum].append(mapName); + int mapNamePos = 0; + for (int i = 0; i <= groupNum; i++) + mapNamePos += this->groupedMapNames.value(i).length(); + + this->mapNames.insert(mapNamePos, mapName); + this->mapGroups.insert(mapName, groupNum); + this->groupedMapNames[groupNum].append(mapName); newMap->isPersistedToFile = false; newMap->setName(mapName); - mapConstantsToMapNames.insert(newMap->constantName, newMap->name); - mapNamesToMapConstants.insert(newMap->name, newMap->constantName); + this->mapConstantsToMapNames.insert(newMap->constantName, newMap->name); + this->mapNamesToMapConstants.insert(newMap->name, newMap->constantName); if (!existingLayout) { - mapLayouts.insert(newMap->layoutId, newMap->layout); - mapLayoutsTable.append(newMap->layoutId); + this->mapLayouts.insert(newMap->layoutId, newMap->layout); + this->mapLayoutsTable.append(newMap->layoutId); if (!importedMap) { setNewMapBlockdata(newMap); } From 13252f98f37015009beaf95cec1f9184b5041bd3 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Wed, 24 Jul 2024 15:46:20 -0400 Subject: [PATCH 29/76] Add dive/emerge map overlay --- forms/mainwindow.ui | 221 ++++++++++++++++++++------- include/config.h | 8 + include/editor.h | 14 +- include/mainwindow.h | 4 + include/ui/connectionpixmapitem.h | 6 +- src/config.cpp | 12 ++ src/editor.cpp | 242 +++++++++++++++++++----------- src/mainwindow.cpp | 50 +++++- 8 files changed, 406 insertions(+), 151 deletions(-) diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index 9e1fabe4..2428b619 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -1715,7 +1715,7 @@ 0 0 100 - 30 + 16 @@ -1809,7 +1809,7 @@ 0 0 100 - 30 + 16 @@ -1903,7 +1903,7 @@ 0 0 100 - 30 + 16 @@ -2003,7 +2003,7 @@ 0 0 100 - 30 + 16 @@ -2097,7 +2097,7 @@ 0 0 100 - 30 + 16 @@ -2601,34 +2601,7 @@ - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Open the selected Emerge Map - - - ... - - - - :/icons/map_go.ico:/icons/map_go.ico - - - - + If enabled, connections will automatically be updated on the connected map. @@ -2641,20 +2614,6 @@ - - - - Open the selected Dive Map - - - ... - - - - :/icons/map_go.ico:/icons/map_go.ico - - - @@ -2662,6 +2621,13 @@ + + + + Emerge Map + + + @@ -2675,10 +2641,161 @@ - - + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Open the selected Dive Map + - Emerge Map + ... + + + + :/icons/map_go.ico:/icons/map_go.ico + + + + + + + Show Emerge/Dive Maps + + + true + + + true + + + true + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 1 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 10 + + + 90 + + + 30 + + + Qt::Horizontal + + + + + + + 10 + + + 90 + + + 30 + + + Qt::Horizontal + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 10 + + + 90 + + + 30 + + + Qt::Horizontal + + + + + + + + + + + + + + Open the selected Emerge Map + + + ... + + + + :/icons/map_go.ico:/icons/map_go.ico @@ -2763,8 +2880,8 @@ 0 0 - 100 - 30 + 365 + 651 diff --git a/include/config.h b/include/config.h index 3f77aa90..a039e4eb 100644 --- a/include/config.h +++ b/include/config.h @@ -58,6 +58,10 @@ public: this->reopenOnLaunch = true; this->mapSortOrder = MapSortOrder::Group; this->prettyCursors = true; + this->showDiveEmergeMaps = false; + this->diveEmergeMapOpacity = 30; + this->diveMapOpacity = 15; + this->emergeMapOpacity = 15; this->collisionOpacity = 50; this->collisionZoom = 30; this->metatilesZoom = 30; @@ -103,6 +107,10 @@ public: bool projectManuallyClosed; MapSortOrder mapSortOrder; bool prettyCursors; + bool showDiveEmergeMaps; + int diveEmergeMapOpacity; + int diveMapOpacity; + int emergeMapOpacity; int collisionOpacity; int collisionZoom; int metatilesZoom; diff --git a/include/editor.h b/include/editor.h index 2c566abc..d4dd0e85 100644 --- a/include/editor.h +++ b/include/editor.h @@ -75,10 +75,11 @@ public: void setEditingConnections(); void setMapEditingButtonsEnabled(bool enabled); void setConnectionsVisibility(bool visible); + void updateDiveEmergeVisibility(); void addNewConnection(); - void addConnection(Map* map, MapConnection* connection); - void removeConnection(Map* map, MapConnection* connection); - void removeConnectionItem(ConnectionPixmapItem* connectionItem, bool removeMirror = true); + void addConnection(Map* map, MapConnection* connection, bool addMirror = true); + void removeConnection(Map* map, MapConnection* connection, bool removeMirror = true); + void removeConnectionItem(ConnectionPixmapItem* connectionItem); void removeSelectedConnection(); void addNewWildMonGroup(QWidget *window); void deleteWildMonGroup(); @@ -111,6 +112,8 @@ public: QPointer map_item = nullptr; ConnectionPixmapItem* selected_connection_item = nullptr; QList connection_items; + QGraphicsPixmapItem *dive_map_overlay = nullptr; + QGraphicsPixmapItem *emerge_map_overlay = nullptr; QGraphicsPathItem *connection_mask = nullptr; QPointer collision_item = nullptr; QGraphicsItemGroup *events_group = nullptr; @@ -175,6 +178,8 @@ private: void clearCurrentMetatilesSelection(); void clearMapEvents(); void clearMapConnections(); + void clearDiveMap(); + void clearEmergeMap(); void clearConnectionMask(); void clearMapBorder(); void clearMapGrid(); @@ -186,7 +191,8 @@ private: void updateConnectionItemPos(ConnectionPixmapItem* connectionItem); void createConnectionItem(MapConnection* connection); void addConnectionToList(ConnectionPixmapItem* connection); - void updateDiveEmergeMap(QString mapName, QString direction); + void createDiveEmergeConnection(MapConnection* connection); + void setDiveEmergeMapName(QString mapName, QString direction); MapConnectionMirror getMirroredConnection(MapConnection*); void addMirroredConnection(MapConnection*); void removeMirroredConnection(MapConnection*); diff --git a/include/mainwindow.h b/include/mainwindow.h index f859ded9..20d3965f 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -269,6 +269,10 @@ private slots: void eventTabChanged(int index); + void on_groupBox_DiveMapOpacity_toggled(bool on); + void on_slider_DiveEmergeMapOpacity_valueChanged(int value); + void on_slider_DiveMapOpacity_valueChanged(int value); + void on_slider_EmergeMapOpacity_valueChanged(int value); void on_horizontalSlider_CollisionTransparency_valueChanged(int value); void on_toolButton_ExpandAll_clicked(); void on_toolButton_CollapseAll_clicked(); diff --git a/include/ui/connectionpixmapitem.h b/include/ui/connectionpixmapitem.h index a856eb45..e5577d2f 100644 --- a/include/ui/connectionpixmapitem.h +++ b/include/ui/connectionpixmapitem.h @@ -9,10 +9,10 @@ class ConnectionPixmapItem : public QObject, public QGraphicsPixmapItem { Q_OBJECT public: ConnectionPixmapItem(QPixmap pixmap, MapConnection* connection, int x, int y) - : QGraphicsPixmapItem(pixmap), - connection(connection) + : QGraphicsPixmapItem(pixmap) { this->basePixmap = pixmap; + this->connection = connection; setFlag(ItemIsMovable); setFlag(ItemSendsGeometryChanges); this->initialX = x; @@ -22,7 +22,7 @@ public: this->setY(y); } QPixmap basePixmap; - MapConnection* const connection; + MapConnection* connection; int initialX; int initialY; int initialOffset; diff --git a/src/config.cpp b/src/config.cpp index 16853e8f..eb87adf2 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -330,6 +330,14 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) { this->mainSplitterState = bytesFromString(value); } else if (key == "metatiles_splitter_state") { this->metatilesSplitterState = bytesFromString(value); + } else if (key == "show_dive_emerge_maps") { + this->showDiveEmergeMaps = getConfigBool(key, value); + } else if (key == "dive_emerge_map_opacity") { + this->diveEmergeMapOpacity = getConfigInteger(key, value, 10, 90, 30); + } else if (key == "dive_map_opacity") { + this->diveMapOpacity = getConfigInteger(key, value, 10, 90, 15); + } else if (key == "emerge_map_opacity") { + this->emergeMapOpacity = getConfigInteger(key, value, 10, 90, 15); } else if (key == "collision_opacity") { this->collisionOpacity = getConfigInteger(key, value, 0, 100, 50); } else if (key == "tileset_editor_geometry") { @@ -439,6 +447,10 @@ QMap PorymapConfig::getKeyValueMap() { map.insert("project_settings_editor_state", stringFromByteArray(this->projectSettingsEditorState)); map.insert("custom_scripts_editor_geometry", stringFromByteArray(this->customScriptsEditorGeometry)); map.insert("custom_scripts_editor_state", stringFromByteArray(this->customScriptsEditorState)); + map.insert("show_dive_emerge_maps", this->showDiveEmergeMaps ? "1" : "0"); + map.insert("dive_emerge_map_opacity", QString::number(this->diveEmergeMapOpacity)); + map.insert("dive_map_opacity", QString::number(this->diveMapOpacity)); + map.insert("emerge_map_opacity", QString::number(this->emergeMapOpacity)); map.insert("collision_opacity", QString::number(this->collisionOpacity)); map.insert("collision_zoom", QString::number(this->collisionZoom)); map.insert("metatiles_zoom", QString::number(this->metatilesZoom)); diff --git a/src/editor.cpp b/src/editor.cpp index f427676b..ba079f50 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -725,6 +725,26 @@ void Editor::updateEncounterFields(EncounterFields newFields) { project->wildMonFields = newFields; } +void Editor::createConnectionItem(MapConnection* connection) { + if (!connection) + return; + QPixmap pixmap = getConnectionPixmap(*connection); + QPoint pos = calculateConnectionPosition(*connection, pixmap); + ConnectionPixmapItem *item = new ConnectionPixmapItem(pixmap, connection, pos.x(), pos.y()); + item->render(); + scene->addItem(item); + + connect(item, &ConnectionPixmapItem::selectionChanged, [this, item](bool selected) { + if (selected) setSelectedConnection(item); + }); + connect(item, &ConnectionPixmapItem::connectionItemDoubleClicked, [this, item] { + emit this->connectionItemDoubleClicked(item->connection->map_name, map->name); + }); + + connection_items.append(item); + addConnectionToList(item); +} + void Editor::addConnectionToList(ConnectionPixmapItem * connectionItem) { ConnectionsListItem *listItem = new ConnectionsListItem(ui->scrollAreaContents_ConnectionsList, connectionItem->connection, project->mapNames); ui->layout_ConnectionsList->insertWidget(ui->layout_ConnectionsList->count() - 1, listItem); // Insert above the vertical spacer @@ -799,25 +819,20 @@ void Editor::addNewConnection() { newConnection->map_name = defaultMapName; addConnection(map, newConnection); setSelectedConnection(connection_items.last()); - addMirroredConnection(newConnection); } -void Editor::addConnection(Map* map, MapConnection * connection) { +void Editor::addConnection(Map* map, MapConnection * connection, bool addMirror) { if (!map || !connection) return; + if (addMirror) + addMirroredConnection(connection); + map->connections.append(connection); if (map == this->map) { // Adding a connection to the current map, we need to display it visually. - // Note that for the Dive/Emerge combo boxes this is normally redundant - // as the user can only create these by having already set the text, - // *except* in the case where they're connecting a map to itself. - if (connection->direction == "dive") { - const QSignalBlocker blocker(ui->comboBox_DiveMap); - ui->comboBox_DiveMap->setCurrentText(connection->map_name); - } else if (connection->direction == "emerge") { - const QSignalBlocker blocker(ui->comboBox_EmergeMap); - ui->comboBox_EmergeMap->setCurrentText(connection->map_name); + if (connection->direction == "dive" || connection->direction == "emerge") { + createDiveEmergeConnection(connection); } else { createConnectionItem(connection); } @@ -825,19 +840,10 @@ void Editor::addConnection(Map* map, MapConnection * connection) { emit editedMapData(map); } -void Editor::removeConnectionItem(ConnectionPixmapItem* connectionItem, bool removeMirror) { +void Editor::removeConnectionItem(ConnectionPixmapItem* connectionItem) { if (!connectionItem) return; - if (removeMirror) { - // If a map is connected to itself and we delete the connection then this function - // will be called twice, once for the target of the delete and once for its mirror. - // We only want to try deleting the mirror once, on the first call (because the mirror - // of the mirror is the original target again, and we already deleted it). - removeMirroredConnection(connectionItem->connection); - } - removeConnection(map, connectionItem->connection); - connection_items.removeOne(connectionItem); if (connectionItem->scene()) connectionItem->scene()->removeItem(connectionItem); @@ -847,21 +853,48 @@ void Editor::removeConnectionItem(ConnectionPixmapItem* connectionItem, bool rem if (!connection_items.isEmpty()) setSelectedConnection(connection_items.first()); } - delete connectionItem; -} -void Editor::removeConnection(Map* map, MapConnection* connection) { - if (!map) - return; - map->connections.removeOne(connection); - delete connection; - emit editedMapData(map); + removeConnection(map, connectionItem->connection); + + delete connectionItem; } void Editor::removeSelectedConnection() { removeConnectionItem(selected_connection_item); } +void Editor::removeConnection(Map* map, MapConnection* connection, bool removeMirror) { + if (!map || !connection) + return; + + if (removeMirror) + removeMirroredConnection(connection); + + if (map == this->map) { + // The connection to delete is displayed on the currently-opened map, we need to delete it visually as well. + if (connection->direction == "dive") { + clearDiveMap(); + } else if (connection->direction == "emerge") { + clearEmergeMap(); + } else { + // Find and delete the matching connection graphics. + // If this was called via removeConnectionItem we rely on + // the item having already been removed from this list. + for (auto item :connection_items) { + if (item->connection == connection) { + item->connection = nullptr; + removeConnectionItem(item); + break; + } + } + } + } + + map->connections.removeOne(connection); + delete connection; + emit editedMapData(map); +} + MapConnectionMirror Editor::getMirroredConnection(MapConnection* source) { MapConnectionMirror mirror; if (!map || !source || !ui->checkBox_MirrorConnections->isChecked()) @@ -890,46 +923,58 @@ void Editor::addMirroredConnection(MapConnection* source) { if (!mirror.map) return; - mirror.connection = new MapConnection; + mirror.connection = new MapConnection; // TODO: Are we leaking memory here if mirror.connection != nullptr *mirror.connection = MapConnection::mirror(*source, map->name); - addConnection(mirror.map, mirror.connection); + addConnection(mirror.map, mirror.connection, false); } void Editor::removeMirroredConnection(MapConnection* source) { MapConnectionMirror mirror = getMirroredConnection(source); - if (!mirror.map || !mirror.connection) - return; + removeConnection(mirror.map, mirror.connection, false); +} - if (map == mirror.map) { - // The connection to delete is displayed on the currently-opened map, we need to delete it visually as well. - if (source->direction == "dive") { - const QSignalBlocker blocker(ui->comboBox_DiveMap); - ui->comboBox_DiveMap->setCurrentText(""); - } else if (source->direction == "emerge") { - const QSignalBlocker blocker(ui->comboBox_EmergeMap); - ui->comboBox_EmergeMap->setCurrentText(""); - } else { - // Find and delete the matching connection graphics - for (auto item :connection_items) { - if (item->connection == mirror.connection) { - removeConnectionItem(item, false); - return; - } - } - } +void Editor::createDiveEmergeConnection(MapConnection* connection) { + if (!connection) + return; + + QGraphicsPixmapItem *item; + Map *connectedMap = project->getMap(connection->map_name); + if (!connectedMap || connectedMap == this->map) { + // There's no point in rendering a map on top of itself. + // We create an empty image anyway to allow for changes later. + item = new QGraphicsPixmapItem(QPixmap()); + } else { + item = new QGraphicsPixmapItem(connectedMap->render()); + } + // TODO: Set pos. In-game, how are diving coordinates converted if the maps are different sizes? + scene->addItem(item); + + if (connection->direction == "dive") { + clearDiveMap(); + dive_map_overlay = item; + const QSignalBlocker blocker(ui->comboBox_DiveMap); + ui->comboBox_DiveMap->setCurrentText(connection->map_name); + } else if (connection->direction == "emerge") { + clearEmergeMap(); + emerge_map_overlay = item; + const QSignalBlocker blocker(ui->comboBox_EmergeMap); + ui->comboBox_EmergeMap->setCurrentText(connection->map_name); + } else { + // Shouldn't happen + scene->removeItem(item); + delete item; } - removeConnection(mirror.map, mirror.connection); } void Editor::updateDiveMap(QString mapName) { - updateDiveEmergeMap(mapName, "dive"); + setDiveEmergeMapName(mapName, "dive"); } void Editor::updateEmergeMap(QString mapName) { - updateDiveEmergeMap(mapName, "emerge"); + setDiveEmergeMapName(mapName, "emerge"); } -void Editor::updateDiveEmergeMap(QString mapName, QString direction) { +void Editor::setDiveEmergeMapName(QString mapName, QString direction) { if (!mapName.isEmpty() && !project->mapNamesToMapConstants.contains(mapName)) { logError(QString("Invalid %1 connection map name: '%2'").arg(direction).arg(mapName)); return; @@ -944,12 +989,12 @@ void Editor::updateDiveEmergeMap(QString mapName, QString direction) { } } - // Dive/Emerge maps aren't represented visually (aside from their combo boxes), - // so we have less to do here than a normal connection. if (connection) { + if (connection->map_name == mapName) + return; + // Update existing connection if (mapName.isEmpty()) { - removeMirroredConnection(connection); removeConnection(map, connection); } else { setConnectionMap(connection, mapName); @@ -961,7 +1006,24 @@ void Editor::updateDiveEmergeMap(QString mapName, QString direction) { connection->offset = 0; connection->map_name = mapName; addConnection(map, connection); - addMirroredConnection(connection); + } + updateDiveEmergeVisibility(); +} + +void Editor::updateDiveEmergeVisibility() { + if (dive_map_overlay && emerge_map_overlay) { + // Both connections in use, use separate sliders + ui->stackedWidget_DiveMapOpacity->setCurrentIndex(0); + dive_map_overlay->setOpacity(!porymapConfig.showDiveEmergeMaps ? 0 : static_cast(porymapConfig.diveMapOpacity) / 100); + emerge_map_overlay->setOpacity(!porymapConfig.showDiveEmergeMaps ? 0 : static_cast(porymapConfig.emergeMapOpacity) / 100); + } else { + // One connection in use (or none), use single slider + ui->stackedWidget_DiveMapOpacity->setCurrentIndex(1); + qreal opacity = !porymapConfig.showDiveEmergeMaps ? 0 : static_cast(porymapConfig.diveEmergeMapOpacity) / 100; + if (dive_map_overlay) + dive_map_overlay->setOpacity(opacity); + else if (emerge_map_overlay) + emerge_map_overlay->setOpacity(opacity); } } @@ -1040,7 +1102,7 @@ void Editor::updateConnectionItemPos(ConnectionPixmapItem* connectionItem) { } void Editor::setConnectionOffset(MapConnection *connection, int offset) { - if (!connection || !map || connection->offset == offset) + if (!connection || !map || connection->offset == offset || !MapConnection::isCardinal(connection->direction)) return; MapConnectionMirror mirror = getMirroredConnection(connection); @@ -1808,29 +1870,55 @@ void Editor::clearMapConnections() { } delete item; } - selected_connection_item = nullptr; connection_items.clear(); + + clearDiveMap(); + clearEmergeMap(); + + selected_connection_item = nullptr; +} + +void Editor::clearDiveMap() { + const QSignalBlocker blocker(ui->comboBox_DiveMap); + ui->comboBox_DiveMap->setCurrentText(""); + + if (dive_map_overlay) { + if (dive_map_overlay->scene()){ + dive_map_overlay->scene()->removeItem(dive_map_overlay); + } + delete dive_map_overlay; + dive_map_overlay = nullptr; + } +} + +void Editor::clearEmergeMap() { + const QSignalBlocker blocker(ui->comboBox_EmergeMap); + ui->comboBox_EmergeMap->setCurrentText(""); + + if (emerge_map_overlay) { + if (emerge_map_overlay->scene()){ + emerge_map_overlay->scene()->removeItem(emerge_map_overlay); + } + delete emerge_map_overlay; + emerge_map_overlay = nullptr; + } } void Editor::displayMapConnections() { clearMapConnections(); - const QSignalBlocker blocker1(ui->comboBox_DiveMap); - const QSignalBlocker blocker2(ui->comboBox_EmergeMap); - ui->comboBox_DiveMap->setCurrentText(""); - ui->comboBox_EmergeMap->setCurrentText(""); - // Note: We only support editing 1 Dive and Emerge connection per map. // In a vanilla game only the first Dive/Emerge connection is considered, so allowing // users to have multiple is likely to lead to confusion. In case users have changed // this we won't delete extra diving connections, but we'll only display the first one. + // TODO: Move text check to inside createDiveEmergeConnection? for (MapConnection *connection : map->connections) { if (connection->direction == "dive") { if (ui->comboBox_DiveMap->currentText().isEmpty()) - ui->comboBox_DiveMap->setCurrentText(connection->map_name); + createDiveEmergeConnection(connection); } else if (connection->direction == "emerge") { if (ui->comboBox_EmergeMap->currentText().isEmpty()) - ui->comboBox_EmergeMap->setCurrentText(connection->map_name); + createDiveEmergeConnection(connection); } else { // We allow any unknown direction names here. They'll be editable from the list menu. createConnectionItem(connection); @@ -1839,26 +1927,8 @@ void Editor::displayMapConnections() { if (!connection_items.empty()) setSelectedConnection(connection_items.first()); -} -void Editor::createConnectionItem(MapConnection* connection) { - if (!connection) - return; - QPixmap pixmap = getConnectionPixmap(*connection); - QPoint pos = calculateConnectionPosition(*connection, pixmap); - ConnectionPixmapItem *item = new ConnectionPixmapItem(pixmap, connection, pos.x(), pos.y()); - item->render(); - scene->addItem(item); - - connect(item, &ConnectionPixmapItem::selectionChanged, [this, item](bool selected) { - if (selected) setSelectedConnection(item); - }); - connect(item, &ConnectionPixmapItem::connectionItemDoubleClicked, [this, item] { - emit this->connectionItemDoubleClicked(item->connection->map_name, map->name); - }); - - connection_items.append(item); - addConnectionToList(item); + updateDiveEmergeVisibility(); } void Editor::clearConnectionMask() { diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index fc7c72f3..07a21fbc 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -471,6 +471,13 @@ void MainWindow::applyMapListFilter(QString filterText) } void MainWindow::loadUserSettings() { + const QSignalBlocker blocker1(ui->horizontalSlider_CollisionTransparency); + const QSignalBlocker blocker2(ui->slider_DiveEmergeMapOpacity); + const QSignalBlocker blocker3(ui->slider_DiveMapOpacity); + const QSignalBlocker blocker4(ui->slider_EmergeMapOpacity); + const QSignalBlocker blocker5(ui->horizontalSlider_MetatileZoom); + const QSignalBlocker blocker6(ui->horizontalSlider_CollisionZoom); + ui->actionBetter_Cursors->setChecked(porymapConfig.prettyCursors); this->editor->settings->betterCursors = porymapConfig.prettyCursors; ui->actionPlayer_View_Rectangle->setChecked(porymapConfig.showPlayerView); @@ -479,17 +486,17 @@ void MainWindow::loadUserSettings() { this->editor->settings->cursorTileRectEnabled = porymapConfig.showCursorTile; ui->checkBox_ToggleBorder->setChecked(porymapConfig.showBorder); ui->checkBox_ToggleGrid->setChecked(porymapConfig.showGrid); + ui->groupBox_DiveMapOpacity->setChecked(porymapConfig.showDiveEmergeMaps); mapSortOrder = porymapConfig.mapSortOrder; - ui->horizontalSlider_CollisionTransparency->blockSignals(true); + this->editor->collisionOpacity = static_cast(porymapConfig.collisionOpacity) / 100; ui->horizontalSlider_CollisionTransparency->setValue(porymapConfig.collisionOpacity); - ui->horizontalSlider_CollisionTransparency->blockSignals(false); - ui->horizontalSlider_MetatileZoom->blockSignals(true); + ui->slider_DiveEmergeMapOpacity->setValue(porymapConfig.diveEmergeMapOpacity); + ui->slider_DiveMapOpacity->setValue(porymapConfig.diveMapOpacity); + ui->slider_EmergeMapOpacity->setValue(porymapConfig.emergeMapOpacity); ui->horizontalSlider_MetatileZoom->setValue(porymapConfig.metatilesZoom); - ui->horizontalSlider_MetatileZoom->blockSignals(false); - ui->horizontalSlider_CollisionZoom->blockSignals(true); ui->horizontalSlider_CollisionZoom->setValue(porymapConfig.collisionZoom); - ui->horizontalSlider_CollisionZoom->blockSignals(false); + setTheme(porymapConfig.theme); } @@ -2263,6 +2270,37 @@ void MainWindow::eventTabChanged(int index) { isProgrammaticEventTabChange = false; } +void MainWindow::on_groupBox_DiveMapOpacity_toggled(bool on) { + // Qt doesn't change the style of disabled sliders, so we do it ourselves + QString stylesheet = on ? "" : "QSlider::groove:horizontal {border: 1px solid #999999; border-radius: 3px; height: 2px; background: #B1B1B1;}" + "QSlider::handle:horizontal {border: 1px solid #444444; border-radius: 3px; width: 10px; height: 9px; margin: -5px -1px; background: #5C5C5C; }"; + ui->slider_DiveEmergeMapOpacity->setStyleSheet(stylesheet); + ui->slider_DiveMapOpacity->setStyleSheet(stylesheet); + ui->slider_EmergeMapOpacity->setStyleSheet(stylesheet); + + porymapConfig.showDiveEmergeMaps = on; + this->editor->updateDiveEmergeVisibility(); +} + +// Normally a map only has either a Dive map connection or an Emerge map connection, +// in which case we only show a single opacity slider to modify the one in use. +// If a user has both connections we show two separate opacity sliders so they can +// modify them independently. +void MainWindow::on_slider_DiveEmergeMapOpacity_valueChanged(int value) { + porymapConfig.diveEmergeMapOpacity = value; + this->editor->updateDiveEmergeVisibility(); +} + +void MainWindow::on_slider_DiveMapOpacity_valueChanged(int value) { + porymapConfig.diveMapOpacity = value; + this->editor->updateDiveEmergeVisibility(); +} + +void MainWindow::on_slider_EmergeMapOpacity_valueChanged(int value) { + porymapConfig.emergeMapOpacity = value; + this->editor->updateDiveEmergeVisibility(); +} + void MainWindow::on_horizontalSlider_CollisionTransparency_valueChanged(int value) { this->editor->collisionOpacity = static_cast(value) / 100; porymapConfig.collisionOpacity = value; From a07517be83a5a576fcf71315b58fc07bb241f3aa Mon Sep 17 00:00:00 2001 From: GriffinR Date: Thu, 25 Jul 2024 15:15:02 -0400 Subject: [PATCH 30/76] Preserve selection when deleting connections --- src/editor.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/editor.cpp b/src/editor.cpp index ba079f50..944119c8 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -844,16 +844,21 @@ void Editor::removeConnectionItem(ConnectionPixmapItem* connectionItem) { if (!connectionItem) return; - connection_items.removeOne(connectionItem); + int index = connection_items.indexOf(connectionItem); + if (index >= 0) { + connection_items.removeAt(index); + if (connectionItem == selected_connection_item) { + // This was the selected connection, select the next one up in the list. + selected_connection_item = nullptr; + if (index != 0) index--; + if (connection_items.length() > index) + setSelectedConnection(connection_items.at(index)); + } + } + if (connectionItem->scene()) connectionItem->scene()->removeItem(connectionItem); - if (connectionItem == selected_connection_item) { - selected_connection_item = nullptr; - if (!connection_items.isEmpty()) - setSelectedConnection(connection_items.first()); - } - removeConnection(map, connectionItem->connection); delete connectionItem; From 2bb01a99882b9b0b2ed3e9a09527e199175a35cf Mon Sep 17 00:00:00 2001 From: GriffinR Date: Thu, 25 Jul 2024 16:32:26 -0400 Subject: [PATCH 31/76] Add dive map toggle under View --- forms/mainwindow.ui | 32 +++++++++++++++++++------------- include/mainwindow.h | 2 ++ src/editor.cpp | 4 ---- src/mainwindow.cpp | 28 ++++++++++++++++++++-------- 4 files changed, 41 insertions(+), 25 deletions(-) diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index 2428b619..ff12136e 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -1715,7 +1715,7 @@ 0 0 100 - 16 + 30 @@ -1809,7 +1809,7 @@ 0 0 100 - 16 + 30 @@ -1903,7 +1903,7 @@ 0 0 100 - 16 + 30 @@ -2003,7 +2003,7 @@ 0 0 100 - 16 + 30 @@ -2097,7 +2097,7 @@ 0 0 100 - 16 + 30 @@ -2596,9 +2596,6 @@ true - - true - @@ -2636,9 +2633,6 @@ true - - true - @@ -2880,8 +2874,8 @@ 0 0 - 365 - 651 + 100 + 30 @@ -3082,6 +3076,7 @@ + @@ -3421,6 +3416,17 @@ Ctrl+W + + + true + + + true + + + Dive/Emerge Map + + diff --git a/include/mainwindow.h b/include/mainwindow.h index 20d3965f..eb6f4be1 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -269,6 +269,7 @@ private slots: void eventTabChanged(int index); + void on_actionDive_Emerge_Map_triggered(); void on_groupBox_DiveMapOpacity_toggled(bool on); void on_slider_DiveEmergeMapOpacity_valueChanged(int value); void on_slider_DiveMapOpacity_valueChanged(int value); @@ -409,6 +410,7 @@ private: int insertTilesetLabel(QStringList * list, QString label); void checkForUpdates(bool requestedByUser); + void setDiveEmergeMapVisible(bool visible); }; enum MapListUserRoles { diff --git a/src/editor.cpp b/src/editor.cpp index 944119c8..b3eefda7 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -951,7 +951,6 @@ void Editor::createDiveEmergeConnection(MapConnection* connection) { } else { item = new QGraphicsPixmapItem(connectedMap->render()); } - // TODO: Set pos. In-game, how are diving coordinates converted if the maps are different sizes? scene->addItem(item); if (connection->direction == "dive") { @@ -995,9 +994,6 @@ void Editor::setDiveEmergeMapName(QString mapName, QString direction) { } if (connection) { - if (connection->map_name == mapName) - return; - // Update existing connection if (mapName.isEmpty()) { removeConnection(map, connection); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 07a21fbc..519fee16 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -486,7 +486,6 @@ void MainWindow::loadUserSettings() { this->editor->settings->cursorTileRectEnabled = porymapConfig.showCursorTile; ui->checkBox_ToggleBorder->setChecked(porymapConfig.showBorder); ui->checkBox_ToggleGrid->setChecked(porymapConfig.showGrid); - ui->groupBox_DiveMapOpacity->setChecked(porymapConfig.showDiveEmergeMaps); mapSortOrder = porymapConfig.mapSortOrder; this->editor->collisionOpacity = static_cast(porymapConfig.collisionOpacity) / 100; @@ -498,6 +497,7 @@ void MainWindow::loadUserSettings() { ui->horizontalSlider_CollisionZoom->setValue(porymapConfig.collisionZoom); setTheme(porymapConfig.theme); + setDiveEmergeMapVisible(porymapConfig.showDiveEmergeMaps); } void MainWindow::restoreWindowState() { @@ -910,12 +910,8 @@ void MainWindow::displayMapProperties() { ui->frame_3->setEnabled(true); Map *map = editor->map; - ui->comboBox_PrimaryTileset->blockSignals(true); - ui->comboBox_SecondaryTileset->blockSignals(true); ui->comboBox_PrimaryTileset->setCurrentText(map->layout->tileset_primary_label); ui->comboBox_SecondaryTileset->setCurrentText(map->layout->tileset_secondary_label); - ui->comboBox_PrimaryTileset->blockSignals(false); - ui->comboBox_SecondaryTileset->blockSignals(false); ui->comboBox_Song->setCurrentText(map->song); ui->comboBox_Location->setCurrentText(map->location); @@ -1058,8 +1054,10 @@ bool MainWindow::setProjectUI() { ui->comboBox_Type->addItems(project->mapTypes); ui->comboBox_DiveMap->clear(); ui->comboBox_DiveMap->addItems(project->mapNames); + ui->comboBox_DiveMap->setClearButtonEnabled(true); ui->comboBox_EmergeMap->clear(); ui->comboBox_EmergeMap->addItems(project->mapNames); + ui->comboBox_EmergeMap->setClearButtonEnabled(true); sortMapList(); @@ -2270,15 +2268,29 @@ void MainWindow::eventTabChanged(int index) { isProgrammaticEventTabChange = false; } +void MainWindow::on_actionDive_Emerge_Map_triggered() { + setDiveEmergeMapVisible(ui->actionDive_Emerge_Map->isChecked()); +} + void MainWindow::on_groupBox_DiveMapOpacity_toggled(bool on) { + setDiveEmergeMapVisible(on); +} + +void MainWindow::setDiveEmergeMapVisible(bool visible) { // Qt doesn't change the style of disabled sliders, so we do it ourselves - QString stylesheet = on ? "" : "QSlider::groove:horizontal {border: 1px solid #999999; border-radius: 3px; height: 2px; background: #B1B1B1;}" - "QSlider::handle:horizontal {border: 1px solid #444444; border-radius: 3px; width: 10px; height: 9px; margin: -5px -1px; background: #5C5C5C; }"; + QString stylesheet = visible ? "" : "QSlider::groove:horizontal {border: 1px solid #999999; border-radius: 3px; height: 2px; background: #B1B1B1;}" + "QSlider::handle:horizontal {border: 1px solid #444444; border-radius: 3px; width: 10px; height: 9px; margin: -5px -1px; background: #5C5C5C; }"; ui->slider_DiveEmergeMapOpacity->setStyleSheet(stylesheet); ui->slider_DiveMapOpacity->setStyleSheet(stylesheet); ui->slider_EmergeMapOpacity->setStyleSheet(stylesheet); - porymapConfig.showDiveEmergeMaps = on; + // Sync UI toggle elements + const QSignalBlocker blocker1(ui->groupBox_DiveMapOpacity); + const QSignalBlocker blocker2(ui->actionDive_Emerge_Map); + ui->groupBox_DiveMapOpacity->setChecked(visible); + ui->actionDive_Emerge_Map->setChecked(visible); + + porymapConfig.showDiveEmergeMaps = visible; this->editor->updateDiveEmergeVisibility(); } From 7eb3c17f4a029dd6a1f1f86778f5abe2524acdaf Mon Sep 17 00:00:00 2001 From: GriffinR Date: Sat, 3 Aug 2024 20:52:10 -0400 Subject: [PATCH 32/76] Some connection TODO cleanup --- include/editor.h | 1 - src/core/mapconnection.cpp | 2 +- src/editor.cpp | 55 +++++++++++++++++++++----------------- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/include/editor.h b/include/editor.h index d4dd0e85..ed8be27c 100644 --- a/include/editor.h +++ b/include/editor.h @@ -190,7 +190,6 @@ private: void updateConnectionItem(ConnectionPixmapItem* connectionItem); void updateConnectionItemPos(ConnectionPixmapItem* connectionItem); void createConnectionItem(MapConnection* connection); - void addConnectionToList(ConnectionPixmapItem* connection); void createDiveEmergeConnection(MapConnection* connection); void setDiveEmergeMapName(QString mapName, QString direction); MapConnectionMirror getMirroredConnection(MapConnection*); diff --git a/src/core/mapconnection.cpp b/src/core/mapconnection.cpp index b249ef56..668a01a1 100644 --- a/src/core/mapconnection.cpp +++ b/src/core/mapconnection.cpp @@ -13,7 +13,7 @@ MapConnection MapConnection::mirror(const MapConnection &source, const QString & }; MapConnection mirror; - mirror.direction = oppositeDirections.value(source.direction/*, source.direction*/); + mirror.direction = oppositeDirections.value(source.direction, source.direction); mirror.map_name = mapName; mirror.offset = -source.offset; diff --git a/src/editor.cpp b/src/editor.cpp index b3eefda7..13ddd826 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -728,27 +728,25 @@ void Editor::updateEncounterFields(EncounterFields newFields) { void Editor::createConnectionItem(MapConnection* connection) { if (!connection) return; + + // Create connection image QPixmap pixmap = getConnectionPixmap(*connection); QPoint pos = calculateConnectionPosition(*connection, pixmap); - ConnectionPixmapItem *item = new ConnectionPixmapItem(pixmap, connection, pos.x(), pos.y()); - item->render(); - scene->addItem(item); + ConnectionPixmapItem *connectionItem = new ConnectionPixmapItem(pixmap, connection, pos.x(), pos.y()); + connectionItem->render(); + scene->addItem(connectionItem); - connect(item, &ConnectionPixmapItem::selectionChanged, [this, item](bool selected) { - if (selected) setSelectedConnection(item); - }); - connect(item, &ConnectionPixmapItem::connectionItemDoubleClicked, [this, item] { - emit this->connectionItemDoubleClicked(item->connection->map_name, map->name); - }); - - connection_items.append(item); - addConnectionToList(item); -} - -void Editor::addConnectionToList(ConnectionPixmapItem * connectionItem) { + // Create item for the list panel ConnectionsListItem *listItem = new ConnectionsListItem(ui->scrollAreaContents_ConnectionsList, connectionItem->connection, project->mapNames); ui->layout_ConnectionsList->insertWidget(ui->layout_ConnectionsList->count() - 1, listItem); // Insert above the vertical spacer + connect(connectionItem, &ConnectionPixmapItem::selectionChanged, [this, connectionItem](bool selected) { + if (selected) setSelectedConnection(connectionItem); + }); + connect(connectionItem, &ConnectionPixmapItem::connectionItemDoubleClicked, [this, connectionItem] { + emit this->connectionItemDoubleClicked(connectionItem->connection->map_name, map->name); + }); + // Sync the selection highlight between the list UI and the graphical map connection connect(connectionItem, &ConnectionPixmapItem::selectionChanged, listItem, &ConnectionsListItem::setSelected); connect(listItem, &ConnectionsListItem::selected, [this, connectionItem] { @@ -789,6 +787,8 @@ void Editor::addConnectionToList(ConnectionPixmapItem * connectionItem) { connect(listItem, &ConnectionsListItem::doubleClicked, [this](QString connectedMapName) { emit connectionItemDoubleClicked(connectedMapName, map->name); }); + + connection_items.append(connectionItem); } @@ -924,13 +924,17 @@ MapConnectionMirror Editor::getMirroredConnection(MapConnection* source) { } void Editor::addMirroredConnection(MapConnection* source) { - MapConnectionMirror mirror = getMirroredConnection(source); - if (!mirror.map) + if (!map || !source || !ui->checkBox_MirrorConnections->isChecked()) return; - mirror.connection = new MapConnection; // TODO: Are we leaking memory here if mirror.connection != nullptr - *mirror.connection = MapConnection::mirror(*source, map->name); - addConnection(mirror.map, mirror.connection, false); + // Note: It's possible (and ok) for connectedMap == this->map + Map* connectedMap = project->getMap(source->map_name); + if (!connectedMap) + return; + + MapConnection *mirror = new MapConnection; + *mirror = MapConnection::mirror(*source, map->name); + addConnection(connectedMap, mirror, false); } void Editor::removeMirroredConnection(MapConnection* source) { @@ -938,19 +942,22 @@ void Editor::removeMirroredConnection(MapConnection* source) { removeConnection(mirror.map, mirror.connection, false); } +// TODO: Self-connecting a Dive/Emerge map connection will not actually replace any existing Dive/Emerge connection, if there is one. void Editor::createDiveEmergeConnection(MapConnection* connection) { if (!connection) return; - QGraphicsPixmapItem *item; + // Create image of Dive/Emerge map + QPixmap pixmap; Map *connectedMap = project->getMap(connection->map_name); if (!connectedMap || connectedMap == this->map) { // There's no point in rendering a map on top of itself. // We create an empty image anyway to allow for changes later. - item = new QGraphicsPixmapItem(QPixmap()); + pixmap = QPixmap(); } else { - item = new QGraphicsPixmapItem(connectedMap->render()); + pixmap = connectedMap->render(); } + QGraphicsPixmapItem *item = new QGraphicsPixmapItem(pixmap); scene->addItem(item); if (connection->direction == "dive") { @@ -1986,7 +1993,7 @@ void Editor::displayMapBorder() { item->setX(x * 16); item->setY(y * 16); item->setZValue(-3); - scene->addItem(item); // TODO: If the scene is taking ownership here is a double-free possible? + scene->addItem(item); borderItems.append(item); } } From 4e04e57c05d70f67cc66bb81c0b0db02ef2ec42f Mon Sep 17 00:00:00 2001 From: GriffinR Date: Sun, 4 Aug 2024 16:08:16 -0400 Subject: [PATCH 33/76] MapConnection to QObject --- include/core/map.h | 2 +- include/core/mapconnection.h | 49 +++++--- include/editor.h | 10 +- include/ui/connectionpixmapitem.h | 2 +- src/core/map.cpp | 12 +- src/core/mapconnection.cpp | 81 +++++++++--- src/editor.cpp | 196 ++++++++++++++---------------- src/project.cpp | 20 +-- src/ui/connectionpixmapitem.cpp | 6 +- src/ui/connectionslistitem.cpp | 26 ++-- src/ui/mapimageexporter.cpp | 20 +-- 11 files changed, 238 insertions(+), 186 deletions(-) diff --git a/include/core/map.h b/include/core/map.h index 83f9a9d1..eabe0a8a 100644 --- a/include/core/map.h +++ b/include/core/map.h @@ -98,7 +98,7 @@ public: QStringList getScriptLabels(Event::Group group = Event::Group::None); void removeEvent(Event *); void addEvent(Event *); - QPixmap renderConnection(const MapConnection&, MapLayout *); + QPixmap renderConnection(const QString &, MapLayout *); QPixmap renderBorder(bool ignoreCache = false); void setDimensions(int newWidth, int newHeight, bool setNewBlockdata = true, bool enableScriptCallback = false); void setBorderDimensions(int newWidth, int newHeight, bool setNewBlockdata = true, bool enableScriptCallback = false); diff --git a/include/core/mapconnection.h b/include/core/mapconnection.h index a7077813..1da62efa 100644 --- a/include/core/mapconnection.h +++ b/include/core/mapconnection.h @@ -3,32 +3,45 @@ #define MAPCONNECTION_H #include -#include +#include -class Map; - -class MapConnection { +class MapConnection : public QObject +{ + Q_OBJECT public: - QString direction; - int offset; - QString map_name; + MapConnection(const QString &direction, const QString &hostMapName, const QString &targetMapName, int offset = 0); + + QString direction() const { return m_direction; } + void setDirection(const QString &direction); + + QString hostMapName() const { return m_hostMapName; } + void setHostMapName(const QString &hostMapName); + + QString targetMapName() const { return m_targetMapName; } + void setTargetMapName(const QString &targetMapName); + + int offset() const { return m_offset; } + void setOffset(int offset); + + MapConnection * createMirror(); + bool isMirror(const MapConnection*); static const QStringList cardinalDirections; static bool isCardinal(const QString &direction); static bool isHorizontal(const QString &direction); static bool isVertical(const QString &direction); - static MapConnection mirror(const MapConnection &source, const QString &mapName); -}; -struct MapConnectionMirror { - MapConnection * connection = nullptr; - Map * map = nullptr; -}; +private: + QString m_direction; + QString m_hostMapName; + QString m_targetMapName; + int m_offset; -inline bool operator==(const MapConnection &c1, const MapConnection &c2) { - return c1.direction == c2.direction && - c1.offset == c2.offset && - c1.map_name == c2.map_name; -} +signals: + void directionChanged(const QString &before, const QString &after); + void targetMapNameChanged(const QString &before, const QString &after); + void hostMapNameChanged(const QString &before, const QString &after); + void offsetChanged(int before, int after); +}; #endif // MAPCONNECTION_H diff --git a/include/editor.h b/include/editor.h index ed8be27c..982e8f77 100644 --- a/include/editor.h +++ b/include/editor.h @@ -77,8 +77,8 @@ public: void setConnectionsVisibility(bool visible); void updateDiveEmergeVisibility(); void addNewConnection(); - void addConnection(Map* map, MapConnection* connection, bool addMirror = true); - void removeConnection(Map* map, MapConnection* connection, bool removeMirror = true); + void addConnection(MapConnection* connection, bool addMirror = true); + void removeConnection(MapConnection* connection, bool removeMirror = true); void removeConnectionItem(ConnectionPixmapItem* connectionItem); void removeSelectedConnection(); void addNewWildMonGroup(QWidget *window); @@ -185,14 +185,14 @@ private: void clearMapGrid(); void clearWildMonTables(); void updateBorderVisibility(); - QPoint calculateConnectionPosition(const MapConnection &connection, const QPixmap &pixmap); - QPixmap getConnectionPixmap(const MapConnection &connection); + QPoint calculateConnectionPosition(MapConnection *connection, const QPixmap &pixmap); + QPixmap getConnectionPixmap(MapConnection *connection); void updateConnectionItem(ConnectionPixmapItem* connectionItem); void updateConnectionItemPos(ConnectionPixmapItem* connectionItem); void createConnectionItem(MapConnection* connection); void createDiveEmergeConnection(MapConnection* connection); void setDiveEmergeMapName(QString mapName, QString direction); - MapConnectionMirror getMirroredConnection(MapConnection*); + MapConnection* getMirroredConnection(MapConnection*); void addMirroredConnection(MapConnection*); void removeMirroredConnection(MapConnection*); void updateEncounterFields(EncounterFields newFields); diff --git a/include/ui/connectionpixmapitem.h b/include/ui/connectionpixmapitem.h index e5577d2f..cd1d6622 100644 --- a/include/ui/connectionpixmapitem.h +++ b/include/ui/connectionpixmapitem.h @@ -17,7 +17,7 @@ public: setFlag(ItemSendsGeometryChanges); this->initialX = x; this->initialY = y; - this->initialOffset = connection->offset; + this->initialOffset = connection->offset(); this->setX(x); this->setY(y); } diff --git a/src/core/map.cpp b/src/core/map.cpp index 40ee9de5..494ff273 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -217,20 +217,20 @@ QPixmap Map::renderBorder(bool ignoreCache) { return layout->border_pixmap; } -QPixmap Map::renderConnection(const MapConnection &connection, MapLayout * fromLayout) { - if (!MapConnection::isCardinal(connection.direction)) +QPixmap Map::renderConnection(const QString &direction, MapLayout * fromLayout) { + if (!MapConnection::isCardinal(direction)) return QPixmap(); int x = 0, y = 0, w = getWidth(), h = getHeight(); - if (connection.direction == "up") { + if (direction == "up") { y = getHeight() - BORDER_DISTANCE; h = BORDER_DISTANCE; - } else if (connection.direction == "down") { + } else if (direction == "down") { h = BORDER_DISTANCE; - } else if (connection.direction == "left") { + } else if (direction == "left") { x = getWidth() - BORDER_DISTANCE; w = BORDER_DISTANCE; - } else if (connection.direction == "right") { + } else if (direction == "right") { w = BORDER_DISTANCE; } diff --git a/src/core/mapconnection.cpp b/src/core/mapconnection.cpp index 668a01a1..4e4434ba 100644 --- a/src/core/mapconnection.cpp +++ b/src/core/mapconnection.cpp @@ -1,25 +1,74 @@ +#include #include "mapconnection.h" -#include "map.h" + +const QMap oppositeDirections = { + {"up", "down"}, {"down", "up"}, + {"right", "left"}, {"left", "right"}, + {"dive", "emerge"}, {"emerge", "dive"} +}; + +MapConnection::MapConnection(const QString &direction, const QString &hostMapName, const QString &targetMapName, int offset) { + m_direction = direction; + m_hostMapName = hostMapName; + m_targetMapName = targetMapName; + m_offset = offset; +} + +void MapConnection::setDirection(const QString &direction) { + if (direction == m_direction) + return; + + auto before = m_direction; + m_direction = direction; + emit directionChanged(before, m_direction); +} + +void MapConnection::setHostMapName(const QString &hostMapName) { + if (hostMapName == m_hostMapName) + return; + + auto before = m_hostMapName; + m_hostMapName = hostMapName; + emit hostMapNameChanged(before, m_hostMapName); + +} + +void MapConnection::setTargetMapName(const QString &targetMapName) { + if (targetMapName == m_targetMapName) + return; + + auto before = m_targetMapName; + m_targetMapName = targetMapName; + emit targetMapNameChanged(before, m_targetMapName); +} + +void MapConnection::setOffset(int offset) { + if (offset == m_offset) + return; + + auto before = m_offset; + m_offset = offset; + emit offsetChanged(before, m_offset); +} + +MapConnection * MapConnection::createMirror() { + return new MapConnection(oppositeDirections.value(m_direction, m_direction), m_targetMapName, m_hostMapName, -m_offset); +} + +bool MapConnection::isMirror(const MapConnection* other) { + if (!other) + return false; + + return m_hostMapName == other->m_targetMapName + && m_targetMapName == other->m_hostMapName + && m_offset == -other->m_offset + && m_direction == oppositeDirections.value(other->m_direction, other->m_direction); +} const QStringList MapConnection::cardinalDirections = { "up", "down", "left", "right" }; -MapConnection MapConnection::mirror(const MapConnection &source, const QString &mapName) { - static const QMap oppositeDirections = { - {"up", "down"}, {"down", "up"}, - {"right", "left"}, {"left", "right"}, - {"dive", "emerge"}, {"emerge", "dive"} - }; - - MapConnection mirror; - mirror.direction = oppositeDirections.value(source.direction, source.direction); - mirror.map_name = mapName; - mirror.offset = -source.offset; - - return mirror; -} - bool MapConnection::isHorizontal(const QString &direction) { return direction == "left" || direction == "right"; } diff --git a/src/editor.cpp b/src/editor.cpp index 13ddd826..817e02eb 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -730,8 +730,8 @@ void Editor::createConnectionItem(MapConnection* connection) { return; // Create connection image - QPixmap pixmap = getConnectionPixmap(*connection); - QPoint pos = calculateConnectionPosition(*connection, pixmap); + QPixmap pixmap = getConnectionPixmap(connection); + QPoint pos = calculateConnectionPosition(connection, pixmap); ConnectionPixmapItem *connectionItem = new ConnectionPixmapItem(pixmap, connection, pos.x(), pos.y()); connectionItem->render(); scene->addItem(connectionItem); @@ -744,7 +744,7 @@ void Editor::createConnectionItem(MapConnection* connection) { if (selected) setSelectedConnection(connectionItem); }); connect(connectionItem, &ConnectionPixmapItem::connectionItemDoubleClicked, [this, connectionItem] { - emit this->connectionItemDoubleClicked(connectionItem->connection->map_name, map->name); + emit this->connectionItemDoubleClicked(connectionItem->connection->targetMapName(), map->name); }); // Sync the selection highlight between the list UI and the graphical map connection @@ -796,7 +796,7 @@ void Editor::addNewConnection() { // Find direction with least number of connections. QMap directionCounts; for (MapConnection* connection : map->connections) { - directionCounts[connection->direction]++; + directionCounts[connection->direction()]++; } QString minDirection = "up"; int minCount = INT_MAX; @@ -813,25 +813,25 @@ void Editor::addNewConnection() { defaultMapName = project->mapNames.at(1); } - MapConnection* newConnection = new MapConnection; - newConnection->direction = minDirection; - newConnection->offset = 0; - newConnection->map_name = defaultMapName; - addConnection(map, newConnection); + addConnection(new MapConnection(minDirection, map->name, defaultMapName)); setSelectedConnection(connection_items.last()); } -void Editor::addConnection(Map* map, MapConnection * connection, bool addMirror) { - if (!map || !connection) +void Editor::addConnection(MapConnection * connection, bool addMirror) { + if (!connection) return; if (addMirror) addMirroredConnection(connection); + Map *map = project->getMap(connection->hostMapName()); + if (!map) + return; + map->connections.append(connection); if (map == this->map) { // Adding a connection to the current map, we need to display it visually. - if (connection->direction == "dive" || connection->direction == "emerge") { + if (connection->direction() == "dive" || connection->direction() == "emerge") { createDiveEmergeConnection(connection); } else { createConnectionItem(connection); @@ -859,7 +859,7 @@ void Editor::removeConnectionItem(ConnectionPixmapItem* connectionItem) { if (connectionItem->scene()) connectionItem->scene()->removeItem(connectionItem); - removeConnection(map, connectionItem->connection); + removeConnection(connectionItem->connection); delete connectionItem; } @@ -868,18 +868,22 @@ void Editor::removeSelectedConnection() { removeConnectionItem(selected_connection_item); } -void Editor::removeConnection(Map* map, MapConnection* connection, bool removeMirror) { - if (!map || !connection) +void Editor::removeConnection(MapConnection* connection, bool removeMirror) { + if (!connection) return; if (removeMirror) removeMirroredConnection(connection); + Map* map = project->getMap(connection->hostMapName()); + if (!map) + return; + if (map == this->map) { // The connection to delete is displayed on the currently-opened map, we need to delete it visually as well. - if (connection->direction == "dive") { + if (connection->direction() == "dive") { clearDiveMap(); - } else if (connection->direction == "emerge") { + } else if (connection->direction() == "emerge") { clearEmergeMap(); } else { // Find and delete the matching connection graphics. @@ -900,46 +904,34 @@ void Editor::removeConnection(Map* map, MapConnection* connection, bool removeMi emit editedMapData(map); } -MapConnectionMirror Editor::getMirroredConnection(MapConnection* source) { - MapConnectionMirror mirror; - if (!map || !source || !ui->checkBox_MirrorConnections->isChecked()) - return mirror; +MapConnection* Editor::getMirroredConnection(MapConnection* source) { + if (!source || !ui->checkBox_MirrorConnections->isChecked()) + return nullptr; - // Note: It's possible (and ok) for mirror.map == this->map - mirror.map = project->getMap(source->map_name); - if (!mirror.map) - return mirror; + // Note: It's possible (and ok) for connectedMap == this->map + Map *connectedMap = project->getMap(source->targetMapName()); + if (!connectedMap) + return nullptr; // Find the matching connection in the connected map. // Note: There is no strict source -> mirror pairing, i.e. we are not guaranteed // to always get the same MapConnection if there are multiple identical copies. - MapConnection target = MapConnection::mirror(*source, map->name); - for (auto connection : mirror.map->connections) { - if (*connection == target && connection != source) { - mirror.connection = connection; - break; - } + for (auto connection : connectedMap->connections) { + if (connection != source && connection->isMirror(source)) + return connection; } - return mirror; + + return nullptr; } void Editor::addMirroredConnection(MapConnection* source) { - if (!map || !source || !ui->checkBox_MirrorConnections->isChecked()) + if (!source || !ui->checkBox_MirrorConnections->isChecked()) return; - - // Note: It's possible (and ok) for connectedMap == this->map - Map* connectedMap = project->getMap(source->map_name); - if (!connectedMap) - return; - - MapConnection *mirror = new MapConnection; - *mirror = MapConnection::mirror(*source, map->name); - addConnection(connectedMap, mirror, false); + addConnection(source->createMirror(), false); } void Editor::removeMirroredConnection(MapConnection* source) { - MapConnectionMirror mirror = getMirroredConnection(source); - removeConnection(mirror.map, mirror.connection, false); + removeConnection(getMirroredConnection(source), false); } // TODO: Self-connecting a Dive/Emerge map connection will not actually replace any existing Dive/Emerge connection, if there is one. @@ -949,7 +941,7 @@ void Editor::createDiveEmergeConnection(MapConnection* connection) { // Create image of Dive/Emerge map QPixmap pixmap; - Map *connectedMap = project->getMap(connection->map_name); + Map *connectedMap = project->getMap(connection->targetMapName()); if (!connectedMap || connectedMap == this->map) { // There's no point in rendering a map on top of itself. // We create an empty image anyway to allow for changes later. @@ -960,16 +952,16 @@ void Editor::createDiveEmergeConnection(MapConnection* connection) { QGraphicsPixmapItem *item = new QGraphicsPixmapItem(pixmap); scene->addItem(item); - if (connection->direction == "dive") { + if (connection->direction() == "dive") { clearDiveMap(); dive_map_overlay = item; const QSignalBlocker blocker(ui->comboBox_DiveMap); - ui->comboBox_DiveMap->setCurrentText(connection->map_name); - } else if (connection->direction == "emerge") { + ui->comboBox_DiveMap->setCurrentText(connection->targetMapName()); + } else if (connection->direction() == "emerge") { clearEmergeMap(); emerge_map_overlay = item; const QSignalBlocker blocker(ui->comboBox_EmergeMap); - ui->comboBox_EmergeMap->setCurrentText(connection->map_name); + ui->comboBox_EmergeMap->setCurrentText(connection->targetMapName()); } else { // Shouldn't happen scene->removeItem(item); @@ -994,7 +986,7 @@ void Editor::setDiveEmergeMapName(QString mapName, QString direction) { // Only the first Dive/Emerge map (if present) is considered, as in-game. MapConnection* connection = nullptr; for (MapConnection* conn : map->connections) { - if (conn->direction == direction) { + if (conn->direction() == direction) { connection = conn; break; } @@ -1003,17 +995,14 @@ void Editor::setDiveEmergeMapName(QString mapName, QString direction) { if (connection) { // Update existing connection if (mapName.isEmpty()) { - removeConnection(map, connection); + removeConnection(connection); } else { setConnectionMap(connection, mapName); } } else if (!mapName.isEmpty()) { // Create new connection - connection = new MapConnection; - connection->direction = direction; - connection->offset = 0; - connection->map_name = mapName; - addConnection(map, connection); + connection = new MapConnection(direction, map->name, mapName); + addConnection(connection); } updateDiveEmergeVisibility(); } @@ -1035,41 +1024,43 @@ void Editor::updateDiveEmergeVisibility() { } } -QPoint Editor::calculateConnectionPosition(const MapConnection &connection, const QPixmap &pixmap) { +QPoint Editor::calculateConnectionPosition(MapConnection *connection, const QPixmap &pixmap) { + const QString direction = connection->direction(); + int offset = connection->offset(); int x = 0, y = 0; const int mWidth = 16, mHeight = 16; - if (connection.direction == "up") { - x = connection.offset * mWidth; + if (direction == "up") { + x = offset * mWidth; y = -pixmap.height(); - } else if (connection.direction == "down") { - x = connection.offset * mWidth; + } else if (direction == "down") { + x = offset * mWidth; y = map->getHeight() * mHeight; - } else if (connection.direction == "left") { + } else if (direction == "left") { x = -pixmap.width(); - y = connection.offset * mHeight; - } else if (connection.direction == "right") { + y = offset * mHeight; + } else if (direction == "right") { x = map->getWidth() * mWidth; - y = connection.offset * mHeight; + y = offset * mHeight; } return QPoint(x, y); } -QPixmap Editor::getConnectionPixmap(const MapConnection &connection) { - Map *connectedMap = project->getMap(connection.map_name); +QPixmap Editor::getConnectionPixmap(MapConnection *connection) { + Map *connectedMap = project->getMap(connection->targetMapName()); // connectedMap will be null for MAP_DYNAMIC and any map that fails to load. // The connection will be editable from the list, but no image will be displayed on the map. if (!connectedMap) return QPixmap(); - return connectedMap->renderConnection(connection, map->layout); + return connectedMap->renderConnection(connection->direction(), this->map->layout); } void Editor::updateConnectionItem(ConnectionPixmapItem* connectionItem) { if (!connectionItem || !connectionItem->connection) return; - const QString mapName = connectionItem->connection->map_name; + const QString mapName = connectionItem->connection->targetMapName(); if (mapName.isEmpty()) return; @@ -1078,18 +1069,17 @@ void Editor::updateConnectionItem(ConnectionPixmapItem* connectionItem) { return; } - connectionItem->initialOffset = connectionItem->connection->offset; - connectionItem->basePixmap = getConnectionPixmap(*connectionItem->connection); - QPoint pos = calculateConnectionPosition(*connectionItem->connection, connectionItem->basePixmap); + connectionItem->initialOffset = connectionItem->connection->offset(); + connectionItem->basePixmap = getConnectionPixmap(connectionItem->connection); + QPoint pos = calculateConnectionPosition(connectionItem->connection, connectionItem->basePixmap); - connectionItem->blockSignals(true); + const QSignalBlocker blocker(connectionItem); connectionItem->setPixmap(connectionItem->basePixmap); connectionItem->initialX = pos.x(); connectionItem->initialY = pos.y(); connectionItem->setX(pos.x()); connectionItem->setY(pos.y()); connectionItem->render(); - connectionItem->blockSignals(false); maskNonVisibleConnectionTiles(); } @@ -1098,37 +1088,39 @@ void Editor::updateConnectionItemPos(ConnectionPixmapItem* connectionItem) { if (!connectionItem || !connectionItem->connection) return; + const QSignalBlocker blocker(connectionItem); MapConnection *connection = connectionItem->connection; - connectionItem->blockSignals(true); - if (MapConnection::isVertical(connection->direction)) { - connectionItem->setX(connectionItem->initialX + (connection->offset - connectionItem->initialOffset) * 16); - } else if (MapConnection::isHorizontal(connection->direction)) { - connectionItem->setY(connectionItem->initialY + (connection->offset - connectionItem->initialOffset) * 16); + if (MapConnection::isVertical(connection->direction())) { + connectionItem->setX(connectionItem->initialX + (connection->offset() - connectionItem->initialOffset) * 16); + } else if (MapConnection::isHorizontal(connection->direction())) { + connectionItem->setY(connectionItem->initialY + (connection->offset() - connectionItem->initialOffset) * 16); } - connectionItem->blockSignals(false); maskNonVisibleConnectionTiles(); } void Editor::setConnectionOffset(MapConnection *connection, int offset) { - if (!connection || !map || connection->offset == offset || !MapConnection::isCardinal(connection->direction)) + if (!connection || !this->map || connection->offset() == offset || !MapConnection::isCardinal(connection->direction())) return; - MapConnectionMirror mirror = getMirroredConnection(connection); - if (mirror.connection && mirror.map) { - mirror.connection->offset = -offset; - if (mirror.map != map) { - emit editedMapData(mirror.map); + MapConnection *mirror = getMirroredConnection(connection); + if (mirror) { + mirror->setOffset(-offset); + + Map *connectedMap = project->getMap(mirror->hostMapName()); + if (connectedMap != this->map) { + emit editedMapData(connectedMap); } else { + // TODO: Remove, this will be handled by connecting to the MapConnection + // The mirror is displayed on the current map, update its graphics for (auto item :connection_items) { - if (item->connection == mirror.connection) { + if (item->connection == mirror) { updateConnectionItemPos(item); break; } } - // TODO: We should be signaling to the list item from the pixmap item, rather than searching for it. for (auto listItem : ui->scrollAreaContents_ConnectionsList->findChildren()) { - if (listItem->connection == mirror.connection){ + if (listItem->connection == mirror){ listItem->updateUI(); break; } @@ -1136,37 +1128,37 @@ void Editor::setConnectionOffset(MapConnection *connection, int offset) { } } - connection->offset = offset; - emit editedMapData(map); + connection->setOffset(offset); + emit editedMapData(this->map); // TODO: This is likely the source of the visual masking bug while dragging (this happens after the move) maskNonVisibleConnectionTiles(); } void Editor::setConnectionMap(MapConnection *connection, const QString &mapName) { - if (!connection || !map || connection->map_name == mapName) + if (!connection || !map || connection->targetMapName() == mapName) return; removeMirroredConnection(connection); - connection->map_name = mapName; + connection->setTargetMapName(mapName); addMirroredConnection(connection); emit editedMapData(map); } void Editor::setConnectionDirection(MapConnection *connection, const QString &direction) { - if (!connection || !map || connection->direction == direction) + if (!connection || !map || connection->direction() == direction) return; // TODO: Lazy removeMirroredConnection(connection); - if (MapConnection::isHorizontal(connection->direction) != MapConnection::isHorizontal(direction) - || MapConnection::isVertical(connection->direction) != MapConnection::isVertical(direction)) { + if (MapConnection::isHorizontal(connection->direction()) != MapConnection::isHorizontal(direction) + || MapConnection::isVertical(connection->direction()) != MapConnection::isVertical(direction)) { // If the direction has changed between vertical/horizontal then the old offset may not make sense, so we reset it setConnectionOffset(connection, 0); } - connection->direction = direction; + connection->setDirection(direction); addMirroredConnection(connection); @@ -1184,9 +1176,9 @@ void Editor::setSelectedConnection(ConnectionPixmapItem* connectionItem) { // TODO: Inaccurate if there are multiple connections from the same map void Editor::setSelectedConnectionFromMap(QString mapName) { - // Search for the first connection that connects to the given map map. + // Search for the first connection that connects to the given map. for (ConnectionPixmapItem* item : connection_items) { - if (item->connection->map_name == mapName) { + if (item->connection->targetMapName() == mapName) { setSelectedConnection(item); break; } @@ -1921,10 +1913,10 @@ void Editor::displayMapConnections() { // this we won't delete extra diving connections, but we'll only display the first one. // TODO: Move text check to inside createDiveEmergeConnection? for (MapConnection *connection : map->connections) { - if (connection->direction == "dive") { + if (connection->direction() == "dive") { if (ui->comboBox_DiveMap->currentText().isEmpty()) createDiveEmergeConnection(connection); - } else if (connection->direction == "emerge") { + } else if (connection->direction() == "emerge") { if (ui->comboBox_EmergeMap->currentText().isEmpty()) createDiveEmergeConnection(connection); } else { @@ -2009,7 +2001,7 @@ void Editor::updateMapConnections() { for (auto item : connection_items) { if (!item->connection) continue; - item->basePixmap = getConnectionPixmap(*item->connection); + item->basePixmap = getConnectionPixmap(item->connection); item->render(); } diff --git a/src/project.cpp b/src/project.cpp index 42487269..60d8ef87 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -370,12 +370,12 @@ bool Project::loadMapData(Map* map) { if (!connectionsArr.isEmpty()) { for (int i = 0; i < connectionsArr.size(); i++) { QJsonObject connectionObj = connectionsArr[i].toObject(); - MapConnection *connection = new MapConnection; - connection->direction = ParseUtil::jsonToQString(connectionObj["direction"]); - connection->offset = ParseUtil::jsonToInt(connectionObj["offset"]); - QString mapConstant = ParseUtil::jsonToQString(connectionObj["map"]); + const QString direction = ParseUtil::jsonToQString(connectionObj["direction"]); + int offset = ParseUtil::jsonToInt(connectionObj["offset"]); + const QString mapConstant = ParseUtil::jsonToQString(connectionObj["map"]); if (mapConstantsToMapNames.contains(mapConstant)) { - connection->map_name = mapConstantsToMapNames.value(mapConstant); + // Successully read map connection + MapConnection *connection = new MapConnection(direction, map->name, mapConstantsToMapNames.value(mapConstant), offset); map->connections.append(connection); } else { logError(QString("Failed to find connected map for map constant '%1'").arg(mapConstant)); @@ -1280,14 +1280,14 @@ void Project::saveMap(Map *map) { if (map->connections.length() > 0) { OrderedJson::array connectionsArr; for (MapConnection* connection : map->connections) { - if (mapNamesToMapConstants.contains(connection->map_name)) { + if (mapNamesToMapConstants.contains(connection->targetMapName())) { OrderedJson::object connectionObj; - connectionObj["map"] = this->mapNamesToMapConstants.value(connection->map_name); - connectionObj["offset"] = connection->offset; - connectionObj["direction"] = connection->direction; + connectionObj["map"] = this->mapNamesToMapConstants.value(connection->targetMapName()); + connectionObj["offset"] = connection->offset(); + connectionObj["direction"] = connection->direction(); connectionsArr.append(connectionObj); } else { - logError(QString("Failed to write map connection. '%1' is not a valid map name").arg(connection->map_name)); + logError(QString("Failed to write map connection. '%1' is not a valid map name").arg(connection->targetMapName())); } } mapObj["connections"] = connectionsArr; diff --git a/src/ui/connectionpixmapitem.cpp b/src/ui/connectionpixmapitem.cpp index 1b7743f9..14a46358 100644 --- a/src/ui/connectionpixmapitem.cpp +++ b/src/ui/connectionpixmapitem.cpp @@ -33,7 +33,7 @@ QVariant ConnectionPixmapItem::itemChange(GraphicsItemChange change, const QVari qreal x, y; int newOffset = this->initialOffset; - if (MapConnection::isVertical(this->connection->direction)) { + if (MapConnection::isVertical(this->connection->direction())) { x = round(newPos.x() / 16) * 16; newOffset += (x - initialX) / 16; x = newOffset * 16; @@ -42,7 +42,7 @@ QVariant ConnectionPixmapItem::itemChange(GraphicsItemChange change, const QVari x = this->initialX; } - if (MapConnection::isHorizontal(this->connection->direction)) { + if (MapConnection::isHorizontal(this->connection->direction())) { y = round(newPos.y() / 16) * 16; newOffset += (y - this->initialY) / 16; y = newOffset * 16; @@ -51,7 +51,7 @@ QVariant ConnectionPixmapItem::itemChange(GraphicsItemChange change, const QVari y = this->initialY; } - if (this->connection->offset != newOffset) + if (this->connection->offset() != newOffset) emit connectionMoved(this->connection, newOffset); return QPointF(x, y); } diff --git a/src/ui/connectionslistitem.cpp b/src/ui/connectionslistitem.cpp index cf6e3020..b735559a 100644 --- a/src/ui/connectionslistitem.cpp +++ b/src/ui/connectionslistitem.cpp @@ -35,9 +35,9 @@ void ConnectionsListItem::updateUI() { const QSignalBlocker blocker2(ui->comboBox_Map); const QSignalBlocker blocker3(ui->spinBox_Offset); - ui->comboBox_Direction->setTextItem(this->connection->direction); - ui->comboBox_Map->setTextItem(this->connection->map_name); - ui->spinBox_Offset->setValue(this->connection->offset); + ui->comboBox_Direction->setTextItem(this->connection->direction()); + ui->comboBox_Map->setTextItem(this->connection->targetMapName()); + ui->spinBox_Offset->setValue(this->connection->offset()); } // TODO: Frame shifts slightly when style changes @@ -56,31 +56,27 @@ void ConnectionsListItem::mousePressEvent(QMouseEvent *) { } void ConnectionsListItem::mouseDoubleClickEvent(QMouseEvent *) { - emit doubleClicked(this->connection->map_name); + emit doubleClicked(this->connection->targetMapName()); } -void ConnectionsListItem::on_comboBox_Direction_currentTextChanged(const QString &direction) -{ +void ConnectionsListItem::on_comboBox_Direction_currentTextChanged(const QString &direction) { this->setSelected(true); - if (this->connection->direction != direction) + if (this->connection->direction() != direction) emit this->editedDirection(this->connection, direction); } -void ConnectionsListItem::on_comboBox_Map_currentTextChanged(const QString &mapName) -{ +void ConnectionsListItem::on_comboBox_Map_currentTextChanged(const QString &mapName) { this->setSelected(true); - if (ui->comboBox_Map->findText(mapName) >= 0 && this->connection->map_name != mapName) + if (ui->comboBox_Map->findText(mapName) >= 0 && this->connection->targetMapName() != mapName) emit this->editedMapName(this->connection, mapName); } -void ConnectionsListItem::on_spinBox_Offset_valueChanged(int offset) -{ +void ConnectionsListItem::on_spinBox_Offset_valueChanged(int offset) { this->setSelected(true); - if (this->connection->offset != offset) + if (this->connection->offset() != offset) emit editedOffset(this->connection, offset); } -void ConnectionsListItem::on_button_Delete_clicked() -{ +void ConnectionsListItem::on_button_Delete_clicked() { emit this->removed(); } diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index e36d9606..f26a05a3 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -239,24 +239,26 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool inclu stitchedMaps.append(cur); for (MapConnection *connection : cur.map->connections) { - if (connection->direction == "dive" || connection->direction == "emerge") - continue; + const QString direction = connection->direction(); int x = cur.x; int y = cur.y; - int offset = connection->offset; - Map *connectionMap = this->editor->project->loadMap(connection->map_name); - if (connection->direction == "up") { + int offset = connection->offset(); + Map *connectionMap = this->editor->project->loadMap(connection->targetMapName()); + if (direction == "up") { x += offset; y -= connectionMap->getHeight(); - } else if (connection->direction == "down") { + } else if (direction == "down") { x += offset; y += cur.map->getHeight(); - } else if (connection->direction == "left") { + } else if (direction == "left") { x -= connectionMap->getWidth(); y += offset; - } else if (connection->direction == "right") { + } else if (direction == "right") { x += cur.map->getWidth(); y += offset; + } else { + // Ignore Dive/Emerge connections and unrecognized directions + continue; } unvisited.append(StitchedMap{x, y, connectionMap}); } @@ -397,7 +399,7 @@ QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { // if showing connections, draw on outside of image QPainter connectionPainter(&pixmap); for (auto connectionItem : editor->connection_items) { - QString direction = connectionItem->connection->direction; + const QString direction = connectionItem->connection->direction(); if ((showUpConnections && direction == "up") || (showDownConnections && direction == "down") || (showLeftConnections && direction == "left") From b5c7f9f86b4d1deebe3d6191d5b67bd18970a612 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Sun, 4 Aug 2024 17:39:56 -0400 Subject: [PATCH 34/76] Save mirroring in config --- include/config.h | 2 ++ include/mainwindow.h | 1 + src/config.cpp | 3 +++ src/editor.cpp | 4 ++-- src/mainwindow.cpp | 6 ++++++ 5 files changed, 14 insertions(+), 2 deletions(-) diff --git a/include/config.h b/include/config.h index a039e4eb..7486919f 100644 --- a/include/config.h +++ b/include/config.h @@ -58,6 +58,7 @@ public: this->reopenOnLaunch = true; this->mapSortOrder = MapSortOrder::Group; this->prettyCursors = true; + this->mirrorConnectingMaps = true; this->showDiveEmergeMaps = false; this->diveEmergeMapOpacity = 30; this->diveMapOpacity = 15; @@ -107,6 +108,7 @@ public: bool projectManuallyClosed; MapSortOrder mapSortOrder; bool prettyCursors; + bool mirrorConnectingMaps; bool showDiveEmergeMaps; int diveEmergeMapOpacity; int diveMapOpacity; diff --git a/include/mainwindow.h b/include/mainwindow.h index eb6f4be1..e37c2c56 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -269,6 +269,7 @@ private slots: void eventTabChanged(int index); + void on_checkBox_MirrorConnections_stateChanged(int selected); void on_actionDive_Emerge_Map_triggered(); void on_groupBox_DiveMapOpacity_toggled(bool on); void on_slider_DiveEmergeMapOpacity_valueChanged(int value); diff --git a/src/config.cpp b/src/config.cpp index eb87adf2..818dd714 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -330,6 +330,8 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) { this->mainSplitterState = bytesFromString(value); } else if (key == "metatiles_splitter_state") { this->metatilesSplitterState = bytesFromString(value); + } else if (key == "mirror_connecting_maps") { + this->mirrorConnectingMaps = getConfigBool(key, value); } else if (key == "show_dive_emerge_maps") { this->showDiveEmergeMaps = getConfigBool(key, value); } else if (key == "dive_emerge_map_opacity") { @@ -447,6 +449,7 @@ QMap PorymapConfig::getKeyValueMap() { map.insert("project_settings_editor_state", stringFromByteArray(this->projectSettingsEditorState)); map.insert("custom_scripts_editor_geometry", stringFromByteArray(this->customScriptsEditorGeometry)); map.insert("custom_scripts_editor_state", stringFromByteArray(this->customScriptsEditorState)); + map.insert("mirror_connecting_maps", this->mirrorConnectingMaps ? "1" : "0"); map.insert("show_dive_emerge_maps", this->showDiveEmergeMaps ? "1" : "0"); map.insert("dive_emerge_map_opacity", QString::number(this->diveEmergeMapOpacity)); map.insert("dive_map_opacity", QString::number(this->diveMapOpacity)); diff --git a/src/editor.cpp b/src/editor.cpp index 817e02eb..8fc3b842 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -905,7 +905,7 @@ void Editor::removeConnection(MapConnection* connection, bool removeMirror) { } MapConnection* Editor::getMirroredConnection(MapConnection* source) { - if (!source || !ui->checkBox_MirrorConnections->isChecked()) + if (!source || !porymapConfig.mirrorConnectingMaps) return nullptr; // Note: It's possible (and ok) for connectedMap == this->map @@ -925,7 +925,7 @@ MapConnection* Editor::getMirroredConnection(MapConnection* source) { } void Editor::addMirroredConnection(MapConnection* source) { - if (!source || !ui->checkBox_MirrorConnections->isChecked()) + if (!source || !porymapConfig.mirrorConnectingMaps) return; addConnection(source->createMirror(), false); } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 519fee16..588547f2 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -486,6 +486,7 @@ void MainWindow::loadUserSettings() { this->editor->settings->cursorTileRectEnabled = porymapConfig.showCursorTile; ui->checkBox_ToggleBorder->setChecked(porymapConfig.showBorder); ui->checkBox_ToggleGrid->setChecked(porymapConfig.showGrid); + ui->checkBox_MirrorConnections->setChecked(porymapConfig.mirrorConnectingMaps); mapSortOrder = porymapConfig.mapSortOrder; this->editor->collisionOpacity = static_cast(porymapConfig.collisionOpacity) / 100; @@ -2807,6 +2808,11 @@ void MainWindow::on_checkBox_ToggleBorder_stateChanged(int selected) editor->toggleBorderVisibility(selected != 0); } +void MainWindow::on_checkBox_MirrorConnections_stateChanged(int selected) +{ + porymapConfig.mirrorConnectingMaps = (selected == Qt::Checked); +} + void MainWindow::on_actionTileset_Editor_triggered() { if (!this->tilesetEditor) { From 4af1c4d463b846efe9bfe6e72269adf15b90bba6 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Sun, 4 Aug 2024 19:11:29 -0400 Subject: [PATCH 35/76] Take advantage of MapConnection as QObject --- include/core/map.h | 3 + include/core/mapconnection.h | 18 +- include/editor.h | 24 +- include/mainwindow.h | 3 +- include/project.h | 1 + include/ui/connectionpixmapitem.h | 9 +- include/ui/connectionslistitem.h | 10 +- src/core/map.cpp | 19 ++ src/core/mapconnection.cpp | 181 +++++++++--- src/editor.cpp | 442 +++++++++++------------------- src/mainwindow.cpp | 22 +- src/project.cpp | 5 +- src/ui/connectionpixmapitem.cpp | 5 +- src/ui/connectionslistitem.cpp | 14 +- 14 files changed, 374 insertions(+), 382 deletions(-) diff --git a/include/core/map.h b/include/core/map.h index eabe0a8a..1dfc881e 100644 --- a/include/core/map.h +++ b/include/core/map.h @@ -98,6 +98,8 @@ public: QStringList getScriptLabels(Event::Group group = Event::Group::None); void removeEvent(Event *); void addEvent(Event *); + bool removeConnection(MapConnection *); + void addConnection(MapConnection *); QPixmap renderConnection(const QString &, MapLayout *); QPixmap renderBorder(bool ignoreCache = false); void setDimensions(int newWidth, int newHeight, bool setNewBlockdata = true, bool enableScriptCallback = false); @@ -132,6 +134,7 @@ signals: void mapDimensionsChanged(const QSize &size); void mapNeedsRedrawing(); void openScriptRequested(QString label); + void connectionAdded(MapConnection*); }; #endif // MAP_H diff --git a/include/core/mapconnection.h b/include/core/mapconnection.h index 1da62efa..d934b388 100644 --- a/include/core/mapconnection.h +++ b/include/core/mapconnection.h @@ -4,6 +4,9 @@ #include #include +#include + +class Project; class MapConnection : public QObject { @@ -23,19 +26,32 @@ public: int offset() const { return m_offset; } void setOffset(int offset); - MapConnection * createMirror(); bool isMirror(const MapConnection*); + MapConnection* findMirror(); + MapConnection* createMirror(); + QPixmap getPixmap(); + + static QPointer project; + static const QMap oppositeDirections; static const QStringList cardinalDirections; static bool isCardinal(const QString &direction); static bool isHorizontal(const QString &direction); static bool isVertical(const QString &direction); + static QString oppositeDirection(const QString &direction) { return oppositeDirections.value(direction, direction); } private: QString m_direction; QString m_hostMapName; QString m_targetMapName; int m_offset; + bool m_ignoreMirror; + + void mirrorDirection(const QString &direction); + void mirrorHostMapName(const QString &hostMapName); + void mirrorTargetMapName(const QString &targetMapName); + void mirrorOffset(int offset); + void markMapEdited(); signals: void directionChanged(const QString &before, const QString &after); diff --git a/include/editor.h b/include/editor.h index 982e8f77..e1a24de6 100644 --- a/include/editor.h +++ b/include/editor.h @@ -46,6 +46,7 @@ public: QPointer project = nullptr; Map *map = nullptr; Settings *settings; + void setProject(Project * project); void saveProject(); void save(); void closeProject(); @@ -78,8 +79,8 @@ public: void updateDiveEmergeVisibility(); void addNewConnection(); void addConnection(MapConnection* connection, bool addMirror = true); - void removeConnection(MapConnection* connection, bool removeMirror = true); - void removeConnectionItem(ConnectionPixmapItem* connectionItem); + void removeConnection(MapConnection* connection, bool addMirror = true); + void removeConnectionPixmap(ConnectionPixmapItem* connectionItem); void removeSelectedConnection(); void addNewWildMonGroup(QWidget *window); void deleteWildMonGroup(); @@ -186,15 +187,11 @@ private: void clearWildMonTables(); void updateBorderVisibility(); QPoint calculateConnectionPosition(MapConnection *connection, const QPixmap &pixmap); - QPixmap getConnectionPixmap(MapConnection *connection); - void updateConnectionItem(ConnectionPixmapItem* connectionItem); - void updateConnectionItemPos(ConnectionPixmapItem* connectionItem); - void createConnectionItem(MapConnection* connection); - void createDiveEmergeConnection(MapConnection* connection); + void updateConnectionPixmap(ConnectionPixmapItem* connectionItem); + void updateConnectionPixmapPos(ConnectionPixmapItem* connectionItem); + void displayConnection(MapConnection* connection); + void displayDiveEmergeConnection(MapConnection* connection); void setDiveEmergeMapName(QString mapName, QString direction); - MapConnection* getMirroredConnection(MapConnection*); - void addMirroredConnection(MapConnection*); - void removeMirroredConnection(MapConnection*); void updateEncounterFields(EncounterFields newFields); QString getMovementPermissionText(uint16_t collision, uint16_t elevation); QString getMetatileDisplayMessage(uint16_t metatileId); @@ -210,9 +207,6 @@ private slots: void setStraightPathCursorMode(QGraphicsSceneMouseEvent *event); void mouseEvent_map(QGraphicsSceneMouseEvent *event, MapPixmapItem *item); void mouseEvent_collision(QGraphicsSceneMouseEvent *event, CollisionPixmapItem *item); - void setConnectionOffset(MapConnection *connection, int offset); - void setConnectionMap(MapConnection *connection, const QString &mapName); - void setConnectionDirection(MapConnection *connection, const QString &direction); void setSelectedConnection(ConnectionPixmapItem* connectionItem); void onHoveredMovementPermissionChanged(uint16_t, uint16_t); void onHoveredMovementPermissionCleared(); @@ -225,15 +219,15 @@ private slots: void onSelectedMetatilesChanged(); void onWheelZoom(int); void onToggleGridClicked(bool); + void onMapConnectionDoubleClicked(MapConnection*); signals: void objectsChanged(); - void connectionItemDoubleClicked(QString, QString); + void openConnectedMap(QString, QString); void wildMonDataChanged(); void warpEventDoubleClicked(QString, int, Event::Group); void currentMetatilesSelectionChanged(); void mapRulerStatusChanged(const QString &); - void editedMapData(Map*); void tilesetUpdated(QString); }; diff --git a/include/mainwindow.h b/include/mainwindow.h index e37c2c56..619ee544 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -178,13 +178,14 @@ private slots: void copy(); void paste(); - void onConnectionItemDoubleClicked(QString, QString); + void onOpenConnectedMap(QString, QString); void onMapNeedsRedrawing(); void onTilesetsSaved(QString, QString); void onWildMonDataChanged(); void openNewMapPopupWindow(); void onNewMapCreated(); void onMapCacheCleared(); + void onMapLoaded(Map *map); void importMapFromAdvanceMap1_92(); void onMapRulerStatusChanged(const QString &); void applyUserShortcuts(); diff --git a/include/project.h b/include/project.h index c9a1a0c0..8c08e508 100644 --- a/include/project.h +++ b/include/project.h @@ -257,6 +257,7 @@ signals: void reloadProject(); void uncheckMonitorFilesAction(); void mapCacheCleared(); + void mapLoaded(Map *map); }; #endif // PROJECT_H diff --git a/include/ui/connectionpixmapitem.h b/include/ui/connectionpixmapitem.h index cd1d6622..969fa6c1 100644 --- a/include/ui/connectionpixmapitem.h +++ b/include/ui/connectionpixmapitem.h @@ -4,6 +4,7 @@ #include "mapconnection.h" #include #include +#include class ConnectionPixmapItem : public QObject, public QGraphicsPixmapItem { Q_OBJECT @@ -18,11 +19,10 @@ public: this->initialX = x; this->initialY = y; this->initialOffset = connection->offset(); - this->setX(x); - this->setY(y); + this->setPos(x, y); } QPixmap basePixmap; - MapConnection* connection; + QPointer connection; int initialX; int initialY; int initialOffset; @@ -41,8 +41,7 @@ protected: void mouseDoubleClickEvent(QGraphicsSceneMouseEvent*); signals: - void connectionItemDoubleClicked(ConnectionPixmapItem* connectionItem); - void connectionMoved(MapConnection *, int newOffset); + void connectionItemDoubleClicked(MapConnection*); void selectionChanged(bool selected); }; diff --git a/include/ui/connectionslistitem.h b/include/ui/connectionslistitem.h index a2ce2c16..9cac5689 100644 --- a/include/ui/connectionslistitem.h +++ b/include/ui/connectionslistitem.h @@ -5,6 +5,7 @@ #include #include +#include namespace Ui { class ConnectionsListItem; @@ -25,7 +26,7 @@ public: void updateUI(); void setSelected(bool selected); - MapConnection * connection; + QPointer connection; private: Ui::ConnectionsListItem *ui; @@ -36,12 +37,9 @@ protected: void mouseDoubleClickEvent(QMouseEvent *); signals: - void editedOffset(MapConnection *connection, int newOffset); - void editedDirection(MapConnection *connection, const QString &direction); - void editedMapName(MapConnection *connection, const QString &mapName); - void removed(); + void removed(MapConnection*); void selected(); - void doubleClicked(const QString &mapName); + void doubleClicked(MapConnection*); private slots: void on_comboBox_Direction_currentTextChanged(const QString &direction); diff --git a/src/core/map.cpp b/src/core/map.cpp index 494ff273..75191a2d 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -218,6 +218,9 @@ QPixmap Map::renderBorder(bool ignoreCache) { } QPixmap Map::renderConnection(const QString &direction, MapLayout * fromLayout) { + if (direction == "dive" || direction == "emerge") + return render(); + if (!MapConnection::isCardinal(direction)) return QPixmap(); @@ -510,6 +513,22 @@ void Map::addEvent(Event *event) { if (!ownedEvents.contains(event)) ownedEvents.append(event); } +bool Map::removeConnection(MapConnection *connection) { + if (connections.removeOne(connection)) { + modify(); + return true; + } + return false; +} + +void Map::addConnection(MapConnection *connection) { + if (!connection || connections.contains(connection)) + return; + connections.append(connection); + modify(); + emit connectionAdded(connection); +} + void Map::modify() { emit modified(); } diff --git a/src/core/mapconnection.cpp b/src/core/mapconnection.cpp index 4e4434ba..5139d810 100644 --- a/src/core/mapconnection.cpp +++ b/src/core/mapconnection.cpp @@ -1,7 +1,9 @@ -#include #include "mapconnection.h" +#include "project.h" -const QMap oppositeDirections = { +QPointer MapConnection::project = nullptr; + +const QMap MapConnection::oppositeDirections = { {"up", "down"}, {"down", "up"}, {"right", "left"}, {"left", "right"}, {"dive", "emerge"}, {"emerge", "dive"} @@ -12,47 +14,7 @@ MapConnection::MapConnection(const QString &direction, const QString &hostMapNam m_hostMapName = hostMapName; m_targetMapName = targetMapName; m_offset = offset; -} - -void MapConnection::setDirection(const QString &direction) { - if (direction == m_direction) - return; - - auto before = m_direction; - m_direction = direction; - emit directionChanged(before, m_direction); -} - -void MapConnection::setHostMapName(const QString &hostMapName) { - if (hostMapName == m_hostMapName) - return; - - auto before = m_hostMapName; - m_hostMapName = hostMapName; - emit hostMapNameChanged(before, m_hostMapName); - -} - -void MapConnection::setTargetMapName(const QString &targetMapName) { - if (targetMapName == m_targetMapName) - return; - - auto before = m_targetMapName; - m_targetMapName = targetMapName; - emit targetMapNameChanged(before, m_targetMapName); -} - -void MapConnection::setOffset(int offset) { - if (offset == m_offset) - return; - - auto before = m_offset; - m_offset = offset; - emit offsetChanged(before, m_offset); -} - -MapConnection * MapConnection::createMirror() { - return new MapConnection(oppositeDirections.value(m_direction, m_direction), m_targetMapName, m_hostMapName, -m_offset); + m_ignoreMirror = false; } bool MapConnection::isMirror(const MapConnection* other) { @@ -62,7 +24,138 @@ bool MapConnection::isMirror(const MapConnection* other) { return m_hostMapName == other->m_targetMapName && m_targetMapName == other->m_hostMapName && m_offset == -other->m_offset - && m_direction == oppositeDirections.value(other->m_direction, other->m_direction); + && m_direction == oppositeDirection(other->m_direction); +} + +MapConnection* MapConnection::findMirror() { + if (!porymapConfig.mirrorConnectingMaps || !project || m_ignoreMirror) + return nullptr; + + Map *connectedMap = project->getMap(m_targetMapName); + if (!connectedMap) + return nullptr; + + // Find the matching connection in the connected map. + // Note: There is no strict source -> mirror pairing, i.e. we are not guaranteed + // to always get the same MapConnection if there are multiple identical copies. + for (auto connection : connectedMap->connections) { + if (this != connection && this->isMirror(connection)) + return connection; + } + return nullptr; +} + +MapConnection * MapConnection::createMirror() { + if (!porymapConfig.mirrorConnectingMaps) + return nullptr; + return new MapConnection(oppositeDirection(m_direction), m_targetMapName, m_hostMapName, -m_offset); +} + +void MapConnection::markMapEdited() { + if (project) { + Map * map = project->getMap(m_hostMapName); + if (map) map->modify(); + } +} + +QPixmap MapConnection::getPixmap() { + if (!project) + return QPixmap(); + + Map *hostMap = project->getMap(m_hostMapName); + Map *targetMap = project->getMap(m_targetMapName); + if (!hostMap || !targetMap) + return QPixmap(); + + return targetMap->renderConnection(m_direction, hostMap->layout); +} + +void MapConnection::setDirection(const QString &direction) { + if (direction == m_direction) + return; + + mirrorDirection(direction); + auto before = m_direction; + m_direction = direction; + emit directionChanged(before, m_direction); + markMapEdited(); +} + +void MapConnection::mirrorDirection(const QString &direction) { + MapConnection * mirror = findMirror(); + if (!mirror) + return; + mirror->m_ignoreMirror = true; + mirror->setDirection(oppositeDirection(direction)); + mirror->m_ignoreMirror = false; +} + +void MapConnection::setHostMapName(const QString &hostMapName) { + if (hostMapName == m_hostMapName) + return; + + mirrorHostMapName(hostMapName); + auto before = m_hostMapName; + m_hostMapName = hostMapName; + + if (project) { + // TODO: This is probably unexpected design, esp because creating a MapConnection doesn't insert to host map + // Reassign connection to new host map + Map * oldMap = project->getMap(before); + Map * newMap = project->getMap(m_hostMapName); + if (oldMap) oldMap->removeConnection(this); + if (newMap) newMap->addConnection(this); + } + emit hostMapNameChanged(before, m_hostMapName); +} + +void MapConnection::mirrorHostMapName(const QString &hostMapName) { + MapConnection * mirror = findMirror(); + if (!mirror) + return; + mirror->m_ignoreMirror = true; + mirror->setTargetMapName(hostMapName); + mirror->m_ignoreMirror = false; +} + +void MapConnection::setTargetMapName(const QString &targetMapName) { + if (targetMapName == m_targetMapName) + return; + + mirrorTargetMapName(targetMapName); + auto before = m_targetMapName; + m_targetMapName = targetMapName; + emit targetMapNameChanged(before, m_targetMapName); + markMapEdited(); +} + +void MapConnection::mirrorTargetMapName(const QString &targetMapName) { + MapConnection * mirror = findMirror(); + if (!mirror) + return; + mirror->m_ignoreMirror = true; + mirror->setHostMapName(targetMapName); + mirror->m_ignoreMirror = false; +} + +void MapConnection::setOffset(int offset) { + if (offset == m_offset) + return; + + mirrorOffset(offset); + auto before = m_offset; + m_offset = offset; + emit offsetChanged(before, m_offset); + markMapEdited(); +} + +void MapConnection::mirrorOffset(int offset) { + MapConnection * mirror = findMirror(); + if (!mirror) + return; + mirror->m_ignoreMirror = true; + mirror->setOffset(-offset); + mirror->m_ignoreMirror = false; } const QStringList MapConnection::cardinalDirections = { diff --git a/src/editor.cpp b/src/editor.cpp index 8fc3b842..c9d1f196 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -78,6 +78,14 @@ void Editor::saveUiFields() { saveEncounterTabData(); } +void Editor::setProject(Project * project) { + if (this->project) { + closeProject(); + } + this->project = project; + MapConnection::project = project; +} + void Editor::closeProject() { if (!this->project) return; @@ -725,70 +733,83 @@ void Editor::updateEncounterFields(EncounterFields newFields) { project->wildMonFields = newFields; } -void Editor::createConnectionItem(MapConnection* connection) { +void Editor::displayConnection(MapConnection* connection) { if (!connection) return; + // It's possible for a map connection to be displayed repeatedly if it's being + // updated by mirroring and the target map is changing to/from the current map. + connection->disconnect(); + + if (connection->direction() == "dive" || connection->direction() == "emerge") { + displayDiveEmergeConnection(connection); + return; + } + // Create connection image - QPixmap pixmap = getConnectionPixmap(connection); + QPixmap pixmap = connection->getPixmap(); QPoint pos = calculateConnectionPosition(connection, pixmap); - ConnectionPixmapItem *connectionItem = new ConnectionPixmapItem(pixmap, connection, pos.x(), pos.y()); - connectionItem->render(); - scene->addItem(connectionItem); + ConnectionPixmapItem *pixmapItem = new ConnectionPixmapItem(pixmap, connection, pos.x(), pos.y()); + pixmapItem->render(); + scene->addItem(pixmapItem); // Create item for the list panel - ConnectionsListItem *listItem = new ConnectionsListItem(ui->scrollAreaContents_ConnectionsList, connectionItem->connection, project->mapNames); + ConnectionsListItem *listItem = new ConnectionsListItem(ui->scrollAreaContents_ConnectionsList, pixmapItem->connection, project->mapNames); ui->layout_ConnectionsList->insertWidget(ui->layout_ConnectionsList->count() - 1, listItem); // Insert above the vertical spacer - connect(connectionItem, &ConnectionPixmapItem::selectionChanged, [this, connectionItem](bool selected) { - if (selected) setSelectedConnection(connectionItem); + // Double clicking the list item or pixmap opens the connected map + connect(listItem, &ConnectionsListItem::doubleClicked, this, &Editor::onMapConnectionDoubleClicked); + connect(pixmapItem, &ConnectionPixmapItem::connectionItemDoubleClicked, this, &Editor::onMapConnectionDoubleClicked); + + // Sync the selection highlight between the list UI and the pixmap + connect(pixmapItem, &ConnectionPixmapItem::selectionChanged, [this, listItem, pixmapItem](bool selected) { + listItem->setSelected(selected); + if (selected) setSelectedConnection(pixmapItem); }); - connect(connectionItem, &ConnectionPixmapItem::connectionItemDoubleClicked, [this, connectionItem] { - emit this->connectionItemDoubleClicked(connectionItem->connection->targetMapName(), map->name); + connect(listItem, &ConnectionsListItem::selected, [this, pixmapItem] { + setSelectedConnection(pixmapItem); }); - // Sync the selection highlight between the list UI and the graphical map connection - connect(connectionItem, &ConnectionPixmapItem::selectionChanged, listItem, &ConnectionsListItem::setSelected); - connect(listItem, &ConnectionsListItem::selected, [this, connectionItem] { - setSelectedConnection(connectionItem); - }); - - // Sync edits to 'offset' between the list UI and the graphical map connection - connect(connectionItem, &ConnectionPixmapItem::connectionMoved, [this, listItem](MapConnection* connection, int offset) { - setConnectionOffset(connection, offset); + // Sync edits to 'offset' between the list UI and the pixmap + connect(connection, &MapConnection::offsetChanged, [this, listItem, pixmapItem](int, int) { listItem->updateUI(); - }); - connect (listItem, &ConnectionsListItem::editedOffset, [this, connectionItem](MapConnection* connection, int offset) { - setConnectionOffset(connection, offset); - updateConnectionItemPos(connectionItem); + updateConnectionPixmapPos(pixmapItem); }); - // Sync edits to 'direction' or 'map' between the list UI and the graphical map connection. - // These are 1-way because the direction and map cannot be edited graphically, only via the list UI. - connect(listItem, &ConnectionsListItem::editedDirection, [this, connectionItem, listItem](MapConnection* connection, QString direction) { - setConnectionDirection(connection, direction); - updateConnectionItem(connectionItem); // TODO: Simplify - listItem->updateUI(); // Changing direction may have changed our offset too - }); - connect(listItem, &ConnectionsListItem::editedMapName, [this, connectionItem](MapConnection* connection, QString mapName) { - setConnectionMap(connection, mapName); - updateConnectionItem(connectionItem); + // Sync edits to 'direction' between the list UI and the pixmap + connect(connection, &MapConnection::directionChanged, [this, listItem, pixmapItem](QString before, QString after) { + if (MapConnection::isHorizontal(before) != MapConnection::isHorizontal(after) + || MapConnection::isVertical(before) != MapConnection::isVertical(after)) { + // If the direction has changed between vertical/horizontal then the old offset may not make sense, so we reset it + pixmapItem->connection->setOffset(0); + } + listItem->updateUI(); + updateConnectionPixmap(pixmapItem); }); - // Sync deleting the map connection - connect(connectionItem, &ConnectionPixmapItem::destroyed, listItem, &ConnectionsListItem::deleteLater); - connect(listItem, &ConnectionsListItem::removed, [this, connectionItem] { - // 'Remove' button has been clicked. Delete the pixmap. - // The list item will be deleted via the above connection to the pixmap's 'destroyed' signal. - removeConnectionItem(connectionItem); + // Sync edits to 'map' between the list UI and the pixmap + connect(connection, &MapConnection::targetMapNameChanged, [this, listItem, pixmapItem](QString, QString) { + listItem->updateUI(); + updateConnectionPixmap(pixmapItem); }); - // Double clicking the list item opens the connected map - connect(listItem, &ConnectionsListItem::doubleClicked, [this](QString connectedMapName) { - emit connectionItemDoubleClicked(connectedMapName, map->name); + connect(connection, &MapConnection::hostMapNameChanged, [this, pixmapItem](QString before, QString) { + // A connection was removed from the current map by reassignment. We're deleting the UI elements, + // but not the associated connection (it still exists, but now it belongs to a different map). + // At the moment this is only possible when updating the mirror for a map connected to itself, + // users have no other way (or need) to relocate a map connection like this. + if (before == this->map->name) + removeConnectionPixmap(pixmapItem); }); - connection_items.append(connectionItem); + // User manually deleting connection via the remove button + connect(listItem, &ConnectionsListItem::removed, [this](MapConnection* connection) { removeConnection(connection); }); + + // Sync removing the UI elements. They may be removed when deleting connections or when displaying a new map. + connect(connection, &MapConnection::destroyed, [this, pixmapItem] { removeConnectionPixmap(pixmapItem); }); + connect(pixmapItem, &ConnectionPixmapItem::destroyed, listItem, &ConnectionsListItem::deleteLater); + + connection_items.append(pixmapItem); } @@ -821,33 +842,38 @@ void Editor::addConnection(MapConnection * connection, bool addMirror) { if (!connection) return; - if (addMirror) - addMirroredConnection(connection); + if (addMirror) addConnection(connection->createMirror(), false); Map *map = project->getMap(connection->hostMapName()); - if (!map) - return; - - map->connections.append(connection); - if (map == this->map) { - // Adding a connection to the current map, we need to display it visually. - if (connection->direction() == "dive" || connection->direction() == "emerge") { - createDiveEmergeConnection(connection); - } else { - createConnectionItem(connection); - } - } - emit editedMapData(map); + if (map) map->addConnection(connection); + // TODO: Edit history } -void Editor::removeConnectionItem(ConnectionPixmapItem* connectionItem) { - if (!connectionItem) +void Editor::removeConnection(MapConnection* connection, bool removeMirror) { + if (!connection) return; - int index = connection_items.indexOf(connectionItem); + if (removeMirror) removeConnection(connection->findMirror(), false); + + Map* map = project->getMap(connection->hostMapName()); + if (map) map->removeConnection(connection); + delete connection; + // TODO: Edit history +} + +void Editor::removeSelectedConnection() { + if (selected_connection_item) + removeConnection(selected_connection_item->connection); +} + +void Editor::removeConnectionPixmap(ConnectionPixmapItem* pixmapItem) { + if (!pixmapItem) + return; + + int index = connection_items.indexOf(pixmapItem); if (index >= 0) { connection_items.removeAt(index); - if (connectionItem == selected_connection_item) { + if (pixmapItem == selected_connection_item) { // This was the selected connection, select the next one up in the list. selected_connection_item = nullptr; if (index != 0) index--; @@ -856,117 +882,53 @@ void Editor::removeConnectionItem(ConnectionPixmapItem* connectionItem) { } } - if (connectionItem->scene()) - connectionItem->scene()->removeItem(connectionItem); + if (pixmapItem->scene()) + pixmapItem->scene()->removeItem(pixmapItem); - removeConnection(connectionItem->connection); - - delete connectionItem; -} - -void Editor::removeSelectedConnection() { - removeConnectionItem(selected_connection_item); -} - -void Editor::removeConnection(MapConnection* connection, bool removeMirror) { - if (!connection) - return; - - if (removeMirror) - removeMirroredConnection(connection); - - Map* map = project->getMap(connection->hostMapName()); - if (!map) - return; - - if (map == this->map) { - // The connection to delete is displayed on the currently-opened map, we need to delete it visually as well. - if (connection->direction() == "dive") { - clearDiveMap(); - } else if (connection->direction() == "emerge") { - clearEmergeMap(); - } else { - // Find and delete the matching connection graphics. - // If this was called via removeConnectionItem we rely on - // the item having already been removed from this list. - for (auto item :connection_items) { - if (item->connection == connection) { - item->connection = nullptr; - removeConnectionItem(item); - break; - } - } - } - } - - map->connections.removeOne(connection); - delete connection; - emit editedMapData(map); -} - -MapConnection* Editor::getMirroredConnection(MapConnection* source) { - if (!source || !porymapConfig.mirrorConnectingMaps) - return nullptr; - - // Note: It's possible (and ok) for connectedMap == this->map - Map *connectedMap = project->getMap(source->targetMapName()); - if (!connectedMap) - return nullptr; - - // Find the matching connection in the connected map. - // Note: There is no strict source -> mirror pairing, i.e. we are not guaranteed - // to always get the same MapConnection if there are multiple identical copies. - for (auto connection : connectedMap->connections) { - if (connection != source && connection->isMirror(source)) - return connection; - } - - return nullptr; -} - -void Editor::addMirroredConnection(MapConnection* source) { - if (!source || !porymapConfig.mirrorConnectingMaps) - return; - addConnection(source->createMirror(), false); -} - -void Editor::removeMirroredConnection(MapConnection* source) { - removeConnection(getMirroredConnection(source), false); + delete pixmapItem; } // TODO: Self-connecting a Dive/Emerge map connection will not actually replace any existing Dive/Emerge connection, if there is one. -void Editor::createDiveEmergeConnection(MapConnection* connection) { +void Editor::displayDiveEmergeConnection(MapConnection* connection) { if (!connection) return; - - // Create image of Dive/Emerge map - QPixmap pixmap; - Map *connectedMap = project->getMap(connection->targetMapName()); - if (!connectedMap || connectedMap == this->map) { - // There's no point in rendering a map on top of itself. - // We create an empty image anyway to allow for changes later. - pixmap = QPixmap(); + + // Note: We only support editing 1 Dive and Emerge connection per map. + // In a vanilla game only the first Dive/Emerge connection is considered, so allowing + // users to have multiple is likely to lead to confusion. In case users have changed + // this we won't delete extra diving connections, but we'll only display the first one. + if (connection->direction() == "dive") { + if (dive_map_overlay) return; + } else if (connection->direction() == "emerge") { + if (emerge_map_overlay) return; } else { - pixmap = connectedMap->render(); + // Invalid direction + return; } + + // Create image of Dive/Emerge map (unless we'd be rendering the current map on top of itself) + QPixmap pixmap = (connection->targetMapName() == this->map->name) ? QPixmap() : connection->getPixmap(); QGraphicsPixmapItem *item = new QGraphicsPixmapItem(pixmap); scene->addItem(item); if (connection->direction() == "dive") { - clearDiveMap(); dive_map_overlay = item; const QSignalBlocker blocker(ui->comboBox_DiveMap); ui->comboBox_DiveMap->setCurrentText(connection->targetMapName()); - } else if (connection->direction() == "emerge") { - clearEmergeMap(); + connect(connection, &MapConnection::destroyed, this, &Editor::clearDiveMap); + connect(connection, &MapConnection::hostMapNameChanged, this, &Editor::clearDiveMap); + } else { emerge_map_overlay = item; const QSignalBlocker blocker(ui->comboBox_EmergeMap); ui->comboBox_EmergeMap->setCurrentText(connection->targetMapName()); - } else { - // Shouldn't happen - scene->removeItem(item); - delete item; + connect(connection, &MapConnection::destroyed, this, &Editor::clearEmergeMap); + connect(connection, &MapConnection::hostMapNameChanged, this, &Editor::clearEmergeMap); } + + // Update pixmap if the connected map is swapped + connect(connection, &MapConnection::targetMapNameChanged, [this, item, connection] { + item->setPixmap((connection->targetMapName() == this->map->name) ? QPixmap() : connection->getPixmap()); + }); } void Editor::updateDiveMap(QString mapName) { @@ -978,11 +940,6 @@ void Editor::updateEmergeMap(QString mapName) { } void Editor::setDiveEmergeMapName(QString mapName, QString direction) { - if (!mapName.isEmpty() && !project->mapNamesToMapConstants.contains(mapName)) { - logError(QString("Invalid %1 connection map name: '%2'").arg(direction).arg(mapName)); - return; - } - // Only the first Dive/Emerge map (if present) is considered, as in-game. MapConnection* connection = nullptr; for (MapConnection* conn : map->connections) { @@ -996,13 +953,13 @@ void Editor::setDiveEmergeMapName(QString mapName, QString direction) { // Update existing connection if (mapName.isEmpty()) { removeConnection(connection); + // TODO: Queue up another Dive/Emerge map if there is one } else { - setConnectionMap(connection, mapName); + connection->setTargetMapName(mapName); } } else if (!mapName.isEmpty()) { // Create new connection - connection = new MapConnection(direction, map->name, mapName); - addConnection(connection); + addConnection(new MapConnection(direction, map->name, mapName)); } updateDiveEmergeVisibility(); } @@ -1024,6 +981,7 @@ void Editor::updateDiveEmergeVisibility() { } } +// TODO: Fold into PixmapItem QPoint Editor::calculateConnectionPosition(MapConnection *connection, const QPixmap &pixmap) { const QString direction = connection->direction(); int offset = connection->offset(); @@ -1045,132 +1003,46 @@ QPoint Editor::calculateConnectionPosition(MapConnection *connection, const QPix return QPoint(x, y); } -QPixmap Editor::getConnectionPixmap(MapConnection *connection) { - Map *connectedMap = project->getMap(connection->targetMapName()); - - // connectedMap will be null for MAP_DYNAMIC and any map that fails to load. - // The connection will be editable from the list, but no image will be displayed on the map. - if (!connectedMap) - return QPixmap(); - - return connectedMap->renderConnection(connection->direction(), this->map->layout); -} - -void Editor::updateConnectionItem(ConnectionPixmapItem* connectionItem) { - if (!connectionItem || !connectionItem->connection) +void Editor::updateConnectionPixmap(ConnectionPixmapItem* pixmapItem) { + if (!pixmapItem || !pixmapItem->connection) return; - const QString mapName = connectionItem->connection->targetMapName(); - if (mapName.isEmpty()) - return; + pixmapItem->initialOffset = pixmapItem->connection->offset(); + pixmapItem->basePixmap = pixmapItem->connection->getPixmap(); + QPoint pos = calculateConnectionPosition(pixmapItem->connection, pixmapItem->basePixmap); - if (!project->mapNames.contains(mapName)) { - logError(QString("Invalid map name '%1' specified for connection.").arg(mapName)); - return; - } - - connectionItem->initialOffset = connectionItem->connection->offset(); - connectionItem->basePixmap = getConnectionPixmap(connectionItem->connection); - QPoint pos = calculateConnectionPosition(connectionItem->connection, connectionItem->basePixmap); - - const QSignalBlocker blocker(connectionItem); - connectionItem->setPixmap(connectionItem->basePixmap); - connectionItem->initialX = pos.x(); - connectionItem->initialY = pos.y(); - connectionItem->setX(pos.x()); - connectionItem->setY(pos.y()); - connectionItem->render(); + const QSignalBlocker blocker(pixmapItem); + pixmapItem->setPixmap(pixmapItem->basePixmap); + pixmapItem->initialX = pos.x(); + pixmapItem->initialY = pos.y(); + pixmapItem->setPos(pos); + pixmapItem->render(); maskNonVisibleConnectionTiles(); } -void Editor::updateConnectionItemPos(ConnectionPixmapItem* connectionItem) { - if (!connectionItem || !connectionItem->connection) +void Editor::updateConnectionPixmapPos(ConnectionPixmapItem* pixmapItem) { + if (!pixmapItem || !pixmapItem->connection) return; - const QSignalBlocker blocker(connectionItem); - MapConnection *connection = connectionItem->connection; + const QSignalBlocker blocker(pixmapItem); + MapConnection *connection = pixmapItem->connection; if (MapConnection::isVertical(connection->direction())) { - connectionItem->setX(connectionItem->initialX + (connection->offset() - connectionItem->initialOffset) * 16); + qreal x = pixmapItem->initialX + (connection->offset() - pixmapItem->initialOffset) * 16; + if (x != pixmapItem->x()) pixmapItem->setX(x); } else if (MapConnection::isHorizontal(connection->direction())) { - connectionItem->setY(connectionItem->initialY + (connection->offset() - connectionItem->initialOffset) * 16); + qreal y = pixmapItem->initialY + (connection->offset() - pixmapItem->initialOffset) * 16; + if (y != pixmapItem->y()) pixmapItem->setY(y); } maskNonVisibleConnectionTiles(); } -void Editor::setConnectionOffset(MapConnection *connection, int offset) { - if (!connection || !this->map || connection->offset() == offset || !MapConnection::isCardinal(connection->direction())) - return; - - MapConnection *mirror = getMirroredConnection(connection); - if (mirror) { - mirror->setOffset(-offset); - - Map *connectedMap = project->getMap(mirror->hostMapName()); - if (connectedMap != this->map) { - emit editedMapData(connectedMap); - } else { - // TODO: Remove, this will be handled by connecting to the MapConnection - - // The mirror is displayed on the current map, update its graphics - for (auto item :connection_items) { - if (item->connection == mirror) { - updateConnectionItemPos(item); - break; - } - } - for (auto listItem : ui->scrollAreaContents_ConnectionsList->findChildren()) { - if (listItem->connection == mirror){ - listItem->updateUI(); - break; - } - } - } - } - - connection->setOffset(offset); - emit editedMapData(this->map); - - // TODO: This is likely the source of the visual masking bug while dragging (this happens after the move) - maskNonVisibleConnectionTiles(); -} - -void Editor::setConnectionMap(MapConnection *connection, const QString &mapName) { - if (!connection || !map || connection->targetMapName() == mapName) - return; - - removeMirroredConnection(connection); - connection->setTargetMapName(mapName); - addMirroredConnection(connection); - - emit editedMapData(map); -} - -void Editor::setConnectionDirection(MapConnection *connection, const QString &direction) { - if (!connection || !map || connection->direction() == direction) - return; - - // TODO: Lazy - removeMirroredConnection(connection); - - if (MapConnection::isHorizontal(connection->direction()) != MapConnection::isHorizontal(direction) - || MapConnection::isVertical(connection->direction()) != MapConnection::isVertical(direction)) { - // If the direction has changed between vertical/horizontal then the old offset may not make sense, so we reset it - setConnectionOffset(connection, 0); - } - connection->setDirection(direction); - - addMirroredConnection(connection); - - emit editedMapData(map); -} - -void Editor::setSelectedConnection(ConnectionPixmapItem* connectionItem) { - if (!connectionItem || connectionItem == selected_connection_item) +void Editor::setSelectedConnection(ConnectionPixmapItem* pixmapItem) { + if (!pixmapItem || pixmapItem == selected_connection_item) return; if (selected_connection_item) selected_connection_item->setSelected(false); - selected_connection_item = connectionItem; + selected_connection_item = pixmapItem; selected_connection_item->setSelected(true); } @@ -1185,6 +1057,10 @@ void Editor::setSelectedConnectionFromMap(QString mapName) { } } +void Editor::onMapConnectionDoubleClicked(MapConnection* connection) { + emit openConnectedMap(connection->targetMapName(), connection->hostMapName()); +} + void Editor::onBorderMetatilesChanged() { displayMapBorder(); updateBorderVisibility(); // TODO: Why do we need to call this here @@ -1368,6 +1244,8 @@ bool Editor::setMap(QString map_name) { // multiple times if set again in the future if (map) { map->disconnect(this); + for (auto connection : map->connections) + connection->disconnect(); } if (project) { @@ -1387,6 +1265,7 @@ bool Editor::setMap(QString map_name) { map_ruler->setMapDimensions(QSize(map->getWidth(), map->getHeight())); connect(map, &Map::mapDimensionsChanged, map_ruler, &MapRuler::setMapDimensions); connect(map, &Map::openScriptRequested, this, &Editor::openScript); + connect(map, &Map::connectionAdded, this, &Editor::displayConnection); updateSelectedEvents(); } @@ -1907,23 +1786,8 @@ void Editor::clearEmergeMap() { void Editor::displayMapConnections() { clearMapConnections(); - // Note: We only support editing 1 Dive and Emerge connection per map. - // In a vanilla game only the first Dive/Emerge connection is considered, so allowing - // users to have multiple is likely to lead to confusion. In case users have changed - // this we won't delete extra diving connections, but we'll only display the first one. - // TODO: Move text check to inside createDiveEmergeConnection? - for (MapConnection *connection : map->connections) { - if (connection->direction() == "dive") { - if (ui->comboBox_DiveMap->currentText().isEmpty()) - createDiveEmergeConnection(connection); - } else if (connection->direction() == "emerge") { - if (ui->comboBox_EmergeMap->currentText().isEmpty()) - createDiveEmergeConnection(connection); - } else { - // We allow any unknown direction names here. They'll be editable from the list menu. - createConnectionItem(connection); - } - } + for (MapConnection *connection : map->connections) + displayConnection(connection); if (!connection_items.empty()) setSelectedConnection(connection_items.first()); @@ -2001,7 +1865,7 @@ void Editor::updateMapConnections() { for (auto item : connection_items) { if (!item->connection) continue; - item->basePixmap = getConnectionPixmap(item->connection); + item->basePixmap = item->connection->getPixmap(); item->render(); } @@ -2107,7 +1971,7 @@ void Editor::updateBorderVisibility() { void Editor::updateCustomMapHeaderValues(QTableWidget *table) { map->customHeaders = CustomAttributesTable::getAttributes(table); - emit editedMapData(map); + map->modify(); } Tileset* Editor::getCurrentMapPrimaryTileset() diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 588547f2..e4728465 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -306,12 +306,11 @@ void MainWindow::checkForUpdates(bool) {} void MainWindow::initEditor() { this->editor = new Editor(ui); connect(this->editor, &Editor::objectsChanged, this, &MainWindow::updateObjects); - connect(this->editor, &Editor::connectionItemDoubleClicked, this, &MainWindow::onConnectionItemDoubleClicked); + connect(this->editor, &Editor::openConnectedMap, this, &MainWindow::onOpenConnectedMap); connect(this->editor, &Editor::warpEventDoubleClicked, this, &MainWindow::openWarpMap); connect(this->editor, &Editor::currentMetatilesSelectionChanged, this, &MainWindow::currentMetatilesSelectionChanged); connect(this->editor, &Editor::wildMonDataChanged, this, &MainWindow::onWildMonDataChanged); connect(this->editor, &Editor::mapRulerStatusChanged, this, &MainWindow::onMapRulerStatusChanged); - connect(this->editor, &Editor::editedMapData, [this](Map* map) { this->markMapEdited(map); }); connect(this->editor, &Editor::tilesetUpdated, this, &Scripting::cb_TilesetUpdated); connect(ui->toolButton_Open_Scripts, &QToolButton::pressed, this->editor, &Editor::openMapScripts); connect(ui->actionOpen_Project_in_Text_Editor, &QAction::triggered, this->editor, &Editor::openProjectInTextEditor); @@ -569,15 +568,17 @@ bool MainWindow::openProject(QString dir, bool initial) { Scripting::init(this); // Create the project - this->editor->project = new Project(this); - QObject::connect(this->editor->project, &Project::reloadProject, this, &MainWindow::on_action_Reload_Project_triggered); - QObject::connect(this->editor->project, &Project::mapCacheCleared, this, &MainWindow::onMapCacheCleared); - QObject::connect(this->editor->project, &Project::uncheckMonitorFilesAction, [this]() { + auto project = new Project(this); + project->set_root(dir); + QObject::connect(project, &Project::reloadProject, this, &MainWindow::on_action_Reload_Project_triggered); + QObject::connect(project, &Project::mapCacheCleared, this, &MainWindow::onMapCacheCleared); + QObject::connect(project, &Project::mapLoaded, this, &MainWindow::onMapLoaded); + QObject::connect(project, &Project::uncheckMonitorFilesAction, [this]() { porymapConfig.monitorFiles = false; if (this->preferenceEditor) this->preferenceEditor->updateFields(); }); - this->editor->project->set_root(dir); + this->editor->setProject(project); // Make sure project looks reasonable before attempting to load it if (!checkProjectSanity()) { @@ -805,7 +806,6 @@ bool MainWindow::setMap(QString map_name, bool scrollTreeView) { showWindowTitle(); connect(editor->map, &Map::mapNeedsRedrawing, this, &MainWindow::onMapNeedsRedrawing); - connect(editor->map, &Map::modified, [this](){ this->markMapEdited(); }); userConfig.recentMap = map_name; updateMapList(); @@ -2526,7 +2526,7 @@ void MainWindow::clickToolButtonFromEditMode(QString editMode) { } } -void MainWindow::onConnectionItemDoubleClicked(QString mapName, QString fromMapName) { +void MainWindow::onOpenConnectedMap(QString mapName, QString fromMapName) { if (mapName != fromMapName && userSetMap(mapName, true)) editor->setSelectedConnectionFromMap(fromMapName); } @@ -2539,6 +2539,10 @@ void MainWindow::onMapCacheCleared() { editor->map = nullptr; } +void MainWindow::onMapLoaded(Map *map) { + connect(map, &Map::modified, [this, map] { this->markMapEdited(map); }); +} + void MainWindow::onTilesetsSaved(QString primaryTilesetLabel, QString secondaryTilesetLabel) { // If saved tilesets are currently in-use, update them and redraw // Otherwise overwrite the cache for the saved tileset diff --git a/src/project.cpp b/src/project.cpp index 60d8ef87..d60c8a09 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -186,10 +186,13 @@ Map* Project::loadMap(QString map_name) { map->setName(map_name); } - if (!(loadMapData(map) && loadMapLayout(map))) + if (!(loadMapData(map) && loadMapLayout(map))){ + delete map; return nullptr; + } mapCache.insert(map_name, map); + emit mapLoaded(map); return map; } diff --git a/src/ui/connectionpixmapitem.cpp b/src/ui/connectionpixmapitem.cpp index 14a46358..c98d3ba7 100644 --- a/src/ui/connectionpixmapitem.cpp +++ b/src/ui/connectionpixmapitem.cpp @@ -51,8 +51,7 @@ QVariant ConnectionPixmapItem::itemChange(GraphicsItemChange change, const QVari y = this->initialY; } - if (this->connection->offset() != newOffset) - emit connectionMoved(this->connection, newOffset); + this->connection->setOffset(newOffset); return QPointF(x, y); } else { @@ -84,5 +83,5 @@ void ConnectionPixmapItem::mousePressEvent(QGraphicsSceneMouseEvent *) { } void ConnectionPixmapItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *) { - emit connectionItemDoubleClicked(this); + emit connectionItemDoubleClicked(this->connection); } diff --git a/src/ui/connectionslistitem.cpp b/src/ui/connectionslistitem.cpp index b735559a..fd2532bf 100644 --- a/src/ui/connectionslistitem.cpp +++ b/src/ui/connectionslistitem.cpp @@ -56,27 +56,25 @@ void ConnectionsListItem::mousePressEvent(QMouseEvent *) { } void ConnectionsListItem::mouseDoubleClickEvent(QMouseEvent *) { - emit doubleClicked(this->connection->targetMapName()); + emit doubleClicked(this->connection); } void ConnectionsListItem::on_comboBox_Direction_currentTextChanged(const QString &direction) { this->setSelected(true); - if (this->connection->direction() != direction) - emit this->editedDirection(this->connection, direction); + this->connection->setDirection(direction); } void ConnectionsListItem::on_comboBox_Map_currentTextChanged(const QString &mapName) { this->setSelected(true); - if (ui->comboBox_Map->findText(mapName) >= 0 && this->connection->targetMapName() != mapName) - emit this->editedMapName(this->connection, mapName); + if (ui->comboBox_Map->findText(mapName) >= 0) + this->connection->setTargetMapName(mapName); } void ConnectionsListItem::on_spinBox_Offset_valueChanged(int offset) { this->setSelected(true); - if (this->connection->offset() != offset) - emit editedOffset(this->connection, offset); + this->connection->setOffset(offset); } void ConnectionsListItem::on_button_Delete_clicked() { - emit this->removed(); + emit this->removed(this->connection); } From edb4a6799462d2b923d1f276896cef5f181a7fb7 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Thu, 8 Aug 2024 17:26:42 -0400 Subject: [PATCH 36/76] Simplify map list icon updating --- include/mainwindow.h | 3 +- src/editor.cpp | 1 - src/mainwindow.cpp | 75 +++++++++++++++++++++++++------------------- 3 files changed, 44 insertions(+), 35 deletions(-) diff --git a/include/mainwindow.h b/include/mainwindow.h index 619ee544..ebc0ea16 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -340,7 +340,6 @@ private: QMap lastSelectedEvent; bool isProgrammaticEventTabChange; - bool projectHasUnsavedChanges; bool newMapDefaultsSet = false; MapSortOrder mapSortOrder; @@ -366,7 +365,7 @@ private: QStandardItem* createMapItem(QString mapName, int groupNum, int inGroupNum); void refreshRecentProjectsMenu(); - void drawMapListIcons(QAbstractItemModel *model); + void updateMapListIcon(const QString &mapName); void updateMapList(); void displayMapProperties(); diff --git a/src/editor.cpp b/src/editor.cpp index c9d1f196..64ba76d3 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -981,7 +981,6 @@ void Editor::updateDiveEmergeVisibility() { } } -// TODO: Fold into PixmapItem QPoint Editor::calculateConnectionPosition(MapConnection *connection, const QPixmap &pixmap) { const QString direction = connection->direction(); int offset = connection->offset(); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index e4728465..5f6e3573 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -421,9 +421,7 @@ void MainWindow::markMapEdited(Map* map) { return; map->hasUnsavedDataChanges = true; - // TODO: Only update the necessary list icon - updateMapList(); - + updateMapListIcon(map->name); if (editor && editor->map == map) showWindowTitle(); } @@ -807,8 +805,10 @@ bool MainWindow::setMap(QString map_name, bool scrollTreeView) { connect(editor->map, &Map::mapNeedsRedrawing, this, &MainWindow::onMapNeedsRedrawing); + // Swap the "currently-open" icon from the old map to the new map + updateMapListIcon(userConfig.recentMap); userConfig.recentMap = map_name; - updateMapList(); + updateMapListIcon(userConfig.recentMap); Scripting::cb_MapOpened(map_name); prefab.updatePrefabUi(editor->map); @@ -1535,46 +1535,54 @@ void MainWindow::on_mapList_activated(const QModelIndex &index) userSetMap(data.toString()); } -void MainWindow::drawMapListIcons(QAbstractItemModel *model) { - projectHasUnsavedChanges = false; +void MainWindow::updateMapListIcon(const QString &mapName) { + if (!editor->project || !editor->project->mapCache.contains(mapName)) + return; + + QStandardItem *item = mapListModel->itemFromIndex(mapListIndexes.value(mapName)); + if (!item) + return; + + if (editor->map && editor->map->name == mapName) { + item->setIcon(*mapOpenedIcon); + } else if (editor->project->mapCache.value(mapName)->hasUnsavedChanges()) { + item->setIcon(*mapEditedIcon); + } else { + item->setIcon(*mapIcon); + } +} + +void MainWindow::updateMapList() { QList list; list.append(QModelIndex()); while (list.length()) { QModelIndex parent = list.takeFirst(); - for (int i = 0; i < model->rowCount(parent); i++) { - QModelIndex index = model->index(i, 0, parent); - if (model->hasChildren(index)) { + for (int i = 0; i < mapListModel->rowCount(parent); i++) { + QModelIndex index = mapListModel->index(i, 0, parent); + if (mapListModel->hasChildren(index)) { list.append(index); } QVariant data = index.data(Qt::UserRole); if (!data.isNull()) { - QString map_name = data.toString(); - if (editor->project && editor->project->mapCache.contains(map_name)) { - QStandardItem *map = mapListModel->itemFromIndex(mapListIndexes.value(map_name)); - map->setIcon(*mapIcon); - if (editor->project->mapCache.value(map_name)->hasUnsavedChanges()) { - map->setIcon(*mapEditedIcon); - projectHasUnsavedChanges = true; - } - if (editor->map->name == map_name) { - map->setIcon(*mapOpenedIcon); - } - } + updateMapListIcon(data.toString()); } } } } -void MainWindow::updateMapList() { - drawMapListIcons(mapListModel); -} - void MainWindow::on_action_Save_Project_triggered() { editor->saveProject(); updateMapList(); showWindowTitle(); } +void MainWindow::on_action_Save_triggered() { + editor->save(); + if (editor->map) + updateMapListIcon(editor->map->name); + showWindowTitle(); +} + void MainWindow::duplicate() { editor->duplicateSelectedEvents(); } @@ -1827,12 +1835,6 @@ void MainWindow::paste() { } } -void MainWindow::on_action_Save_triggered() { - editor->save(); - updateMapList(); - showWindowTitle(); -} - void MainWindow::on_mapViewTab_tabBarClicked(int index) { int oldIndex = ui->mapViewTab->currentIndex(); @@ -3128,7 +3130,16 @@ bool MainWindow::closeProject() { if (!isProjectOpen()) return true; - if (projectHasUnsavedChanges || (editor->map && editor->map->hasUnsavedChanges())) { + // Check loaded maps for unsaved changes + bool unsavedChanges = false; + for (auto map : editor->project->mapCache.values()) { + if (map && map->hasUnsavedChanges()) { + unsavedChanges = true; + break; + } + } + + if (unsavedChanges) { QMessageBox::StandardButton result = QMessageBox::question( this, "porymap", "The project has been modified, save changes?", QMessageBox::No | QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Yes); From 6fbc6d8d8604cdc6d2adf23627eeb29b5c72da0d Mon Sep 17 00:00:00 2001 From: GriffinR Date: Thu, 8 Aug 2024 21:24:15 -0400 Subject: [PATCH 37/76] Prompt for new map connections --- forms/newmapconnectiondialog.ui | 95 +++++++++++++++++++++++++++++ include/editor.h | 2 +- include/ui/newmapconnectiondialog.h | 29 +++++++++ porymap.pro | 3 + src/editor.cpp | 31 ++-------- src/mainwindow.cpp | 8 ++- src/ui/newmapconnectiondialog.cpp | 62 +++++++++++++++++++ 7 files changed, 201 insertions(+), 29 deletions(-) create mode 100644 forms/newmapconnectiondialog.ui create mode 100644 include/ui/newmapconnectiondialog.h create mode 100644 src/ui/newmapconnectiondialog.cpp diff --git a/forms/newmapconnectiondialog.ui b/forms/newmapconnectiondialog.ui new file mode 100644 index 00000000..da109604 --- /dev/null +++ b/forms/newmapconnectiondialog.ui @@ -0,0 +1,95 @@ + + + NewMapConnectionDialog + + + + 0 + 0 + 234 + 120 + + + + Add New Map Connection + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Map + + + + + + + + + + Direction + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + NoScrollComboBox + QComboBox +
noscrollcombobox.h
+
+
+ + + + buttonBox + accepted() + NewMapConnectionDialog + accept() + + + buttonBox + rejected() + NewMapConnectionDialog + reject() + + +
diff --git a/include/editor.h b/include/editor.h index e1a24de6..db07fd87 100644 --- a/include/editor.h +++ b/include/editor.h @@ -77,7 +77,6 @@ public: void setMapEditingButtonsEnabled(bool enabled); void setConnectionsVisibility(bool visible); void updateDiveEmergeVisibility(); - void addNewConnection(); void addConnection(MapConnection* connection, bool addMirror = true); void removeConnection(MapConnection* connection, bool addMirror = true); void removeConnectionPixmap(ConnectionPixmapItem* connectionItem); @@ -86,6 +85,7 @@ public: void deleteWildMonGroup(); void updateDiveMap(QString mapName); void updateEmergeMap(QString mapName); + void selectLastConnection(); void setSelectedConnectionFromMap(QString mapName); void updatePrimaryTileset(QString tilesetLabel, bool forceLoad = false); void updateSecondaryTileset(QString tilesetLabel, bool forceLoad = false); diff --git a/include/ui/newmapconnectiondialog.h b/include/ui/newmapconnectiondialog.h new file mode 100644 index 00000000..8d053515 --- /dev/null +++ b/include/ui/newmapconnectiondialog.h @@ -0,0 +1,29 @@ +#ifndef NEWMAPCONNECTIONDIALOG_H +#define NEWMAPCONNECTIONDIALOG_H + +#include +#include "map.h" +#include "mapconnection.h" + +namespace Ui { +class NewMapConnectionDialog; +} + +class NewMapConnectionDialog : public QDialog +{ + Q_OBJECT + +public: + explicit NewMapConnectionDialog(QWidget *parent, Map* map, const QStringList &mapNames); + ~NewMapConnectionDialog(); + + MapConnection *result; + + virtual void accept() override; + +private: + Ui::NewMapConnectionDialog *ui; + Map *map; +}; + +#endif // NEWMAPCONNECTIONDIALOG_H diff --git a/porymap.pro b/porymap.pro index 8afc99a5..8f785541 100644 --- a/porymap.pro +++ b/porymap.pro @@ -55,6 +55,7 @@ SOURCES += src/core/block.cpp \ src/ui/collisionpixmapitem.cpp \ src/ui/connectionpixmapitem.cpp \ src/ui/currentselectedmetatilespixmapitem.cpp \ + src/ui/newmapconnectiondialog.cpp \ src/ui/overlay.cpp \ src/ui/prefab.cpp \ src/ui/projectsettingseditor.cpp \ @@ -149,6 +150,7 @@ HEADERS += include/core/block.h \ include/ui/collisionpixmapitem.h \ include/ui/connectionpixmapitem.h \ include/ui/currentselectedmetatilespixmapitem.h \ + include/ui/newmapconnectiondialog.h \ include/ui/prefabframe.h \ include/ui/projectsettingseditor.h \ include/ui/regionmaplayoutpixmapitem.h \ @@ -209,6 +211,7 @@ HEADERS += include/core/block.h \ FORMS += forms/mainwindow.ui \ forms/connectionslistitem.ui \ + forms/newmapconnectiondialog.ui \ forms/prefabcreationdialog.ui \ forms/prefabframe.ui \ forms/tileseteditor.ui \ diff --git a/src/editor.cpp b/src/editor.cpp index 64ba76d3..db93600c 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -752,6 +752,7 @@ void Editor::displayConnection(MapConnection* connection) { ConnectionPixmapItem *pixmapItem = new ConnectionPixmapItem(pixmap, connection, pos.x(), pos.y()); pixmapItem->render(); scene->addItem(pixmapItem); + maskNonVisibleConnectionTiles(); // Create item for the list panel ConnectionsListItem *listItem = new ConnectionsListItem(ui->scrollAreaContents_ConnectionsList, pixmapItem->connection, project->mapNames); @@ -812,32 +813,6 @@ void Editor::displayConnection(MapConnection* connection) { connection_items.append(pixmapItem); } - -void Editor::addNewConnection() { - // Find direction with least number of connections. - QMap directionCounts; - for (MapConnection* connection : map->connections) { - directionCounts[connection->direction()]++; - } - QString minDirection = "up"; - int minCount = INT_MAX; - for (QString direction : MapConnection::cardinalDirections) { - if (directionCounts[direction] < minCount) { - minDirection = direction; - minCount = directionCounts[direction]; - } - } - - // Prefer not to connect the map to itself (we have to if it's the only map). - QString defaultMapName = project->mapNames.first(); - if (defaultMapName == map->name && project->mapNames.length() > 1) { - defaultMapName = project->mapNames.at(1); - } - - addConnection(new MapConnection(minDirection, map->name, defaultMapName)); - setSelectedConnection(connection_items.last()); -} - void Editor::addConnection(MapConnection * connection, bool addMirror) { if (!connection) return; @@ -1056,6 +1031,10 @@ void Editor::setSelectedConnectionFromMap(QString mapName) { } } +void Editor::selectLastConnection() { + setSelectedConnection(connection_items.last()); +} + void Editor::onMapConnectionDoubleClicked(MapConnection* connection) { emit openConnectedMap(connection->targetMapName(), connection->hostMapName()); } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 5f6e3573..81e19609 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -19,6 +19,7 @@ #include "prefab.h" #include "montabwidget.h" #include "imageexport.h" +#include "newmapconnectiondialog.h" #include #include @@ -2649,8 +2650,11 @@ void MainWindow::showExportMapImageWindow(ImageExporterMode mode) { void MainWindow::on_pushButton_AddConnection_clicked() { - // TODO: Bring up a prompt for information? - editor->addNewConnection(); + auto dialog = new NewMapConnectionDialog(this, this->editor->map, this->editor->project->mapNames); + if(dialog->exec() == QDialog::Accepted) { + this->editor->addConnection(dialog->result); + this->editor->selectLastConnection(); + } } void MainWindow::on_pushButton_NewWildMonGroup_clicked() { diff --git a/src/ui/newmapconnectiondialog.cpp b/src/ui/newmapconnectiondialog.cpp new file mode 100644 index 00000000..3a7aa481 --- /dev/null +++ b/src/ui/newmapconnectiondialog.cpp @@ -0,0 +1,62 @@ +#include "newmapconnectiondialog.h" +#include "ui_newmapconnectiondialog.h" + +NewMapConnectionDialog::NewMapConnectionDialog(QWidget *parent, Map* map, const QStringList &mapNames) : + QDialog(parent), + ui(new Ui::NewMapConnectionDialog) +{ + ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose); + + this->map = map; + this->result = nullptr; + + ui->comboBox_Direction->setEditable(false); + ui->comboBox_Direction->setMinimumContentsLength(0); + ui->comboBox_Direction->addItems(MapConnection::cardinalDirections); + + ui->comboBox_Map->setMinimumContentsLength(6); + ui->comboBox_Map->addItems(mapNames); + + // Choose default direction + QMap directionCounts; + for (auto connection : map->connections) { + directionCounts[connection->direction()]++; + } + QString defaultDirection; + int minCount = INT_MAX; + for (auto direction : MapConnection::cardinalDirections) { + if (directionCounts[direction] < minCount) { + defaultDirection = direction; + minCount = directionCounts[direction]; + } + } + ui->comboBox_Direction->setTextItem(defaultDirection); + + // Choose default map + QString defaultMapName; + if (mapNames.isEmpty()) { + defaultMapName = QString(); + } else if (mapNames.first() == map->name && mapNames.length() > 1) { + // Prefer not to connect the map to itself + defaultMapName = mapNames.at(1); + } else { + defaultMapName = mapNames.first(); + } + ui->comboBox_Map->setTextItem(defaultMapName); +} + +NewMapConnectionDialog::~NewMapConnectionDialog() +{ + delete ui; +} + +void NewMapConnectionDialog::accept() { + QString direction = ui->comboBox_Direction->currentText(); + QString hostMapName = this->map ? this->map->name : QString(); + QString targetMapName = ui->comboBox_Map->currentText(); + + this->result = new MapConnection(direction, this->map->name, targetMapName); + + QDialog::accept(); +} From bb0071e8ca1bac82d029d26fb109f9f9f62137ed Mon Sep 17 00:00:00 2001 From: GriffinR Date: Sun, 11 Aug 2024 03:11:46 -0400 Subject: [PATCH 38/76] Fix script engine memory leak --- include/scriptutility.h | 4 +++- src/scriptapi/apiutility.cpp | 28 +++++++++++++++++++--------- src/scriptapi/scripting.cpp | 14 +++++++++++--- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/include/scriptutility.h b/include/scriptutility.h index 639e71f6..0d6fca75 100644 --- a/include/scriptutility.h +++ b/include/scriptutility.h @@ -10,7 +10,8 @@ class ScriptUtility : public QObject public: ScriptUtility(MainWindow *mainWindow); - void clearActions(); + ~ScriptUtility(); + QString getActionFunctionName(int actionIndex); Q_INVOKABLE bool registerAction(QString functionName, QString actionName, QString shortcut = ""); Q_INVOKABLE bool registerToggleAction(QString functionName, QString actionName, QString shortcut = "", bool checked = false); @@ -59,6 +60,7 @@ private: MainWindow *window; QList registeredActions; + QSet activeTimers; QHash actionMap; }; diff --git a/src/scriptapi/apiutility.cpp b/src/scriptapi/apiutility.cpp index 38ff3e39..d2b8ebf3 100644 --- a/src/scriptapi/apiutility.cpp +++ b/src/scriptapi/apiutility.cpp @@ -7,6 +7,18 @@ ScriptUtility::ScriptUtility(MainWindow *mainWindow) { this->window = mainWindow; } +ScriptUtility::~ScriptUtility() { + if (window && window->ui && window->ui->menuTools) { + for (auto action : this->registeredActions) { + window->ui->menuTools->removeAction(action); + } + } + for (auto timer : this->activeTimers) { + timer->stop(); + delete timer; + } +} + bool ScriptUtility::registerAction(QString functionName, QString actionName, QString shortcut) { if (!window || !window->ui || !window->ui->menuTools) return false; @@ -44,12 +56,6 @@ bool ScriptUtility::registerToggleAction(QString functionName, QString actionNam return true; } -void ScriptUtility::clearActions() { - for (auto action : this->registeredActions) { - window->ui->menuTools->removeAction(action); - } -} - QString ScriptUtility::getActionFunctionName(int actionIndex) { return this->actionMap.value(actionIndex); } @@ -58,11 +64,15 @@ void ScriptUtility::setTimeout(QJSValue callback, int milliseconds) { if (!callback.isCallable() || milliseconds < 0) return; - QTimer *timer = new QTimer(0); + QTimer *timer = new QTimer(); connect(timer, &QTimer::timeout, [=](){ - this->callTimeoutFunction(callback); + if (this->activeTimers.remove(timer)) { + this->callTimeoutFunction(callback); + timer->deleteLater(); + } }); - connect(timer, &QTimer::timeout, timer, &QTimer::deleteLater); + + this->activeTimers.insert(timer); timer->setSingleShot(true); timer->start(milliseconds); } diff --git a/src/scriptapi/scripting.cpp b/src/scriptapi/scripting.cpp index 82944be9..95a4325f 100644 --- a/src/scriptapi/scripting.cpp +++ b/src/scriptapi/scripting.cpp @@ -1,8 +1,10 @@ +#include + #include "scripting.h" #include "log.h" #include "config.h" -QMap callbackFunctions = { +const QMap callbackFunctions = { {OnProjectOpened, "onProjectOpened"}, {OnProjectClosed, "onProjectClosed"}, {OnBlockChanged, "onBlockChanged"}, @@ -24,8 +26,9 @@ Scripting *instance = nullptr; void Scripting::stop() { if (!instance) return; instance->engine->setInterrupted(true); - instance->scriptUtility->clearActions(); qDeleteAll(instance->imageCache); + delete instance->engine; + delete instance->scriptUtility; delete instance; instance = nullptr; } @@ -39,7 +42,7 @@ void Scripting::init(MainWindow *mainWindow) { Scripting::Scripting(MainWindow *mainWindow) { this->mainWindow = mainWindow; - this->engine = new QJSEngine(mainWindow); + this->engine = new QJSEngine(); this->engine->installExtensions(QJSEngine::ConsoleExtension); const QStringList paths = userConfig.getCustomScriptPaths(); const QList enabled = userConfig.getCustomScriptsEnabled(); @@ -79,6 +82,11 @@ void Scripting::populateGlobalObject(MainWindow *mainWindow) { instance->engine->globalObject().setProperty("overlay", instance->engine->newQObject(mainWindow->ui->graphicsView_Map)); instance->engine->globalObject().setProperty("utility", instance->engine->newQObject(instance->scriptUtility)); + // Note: QJSEngine also has these functions, but not in Qt 5.15. + QQmlEngine::setObjectOwnership(mainWindow, QQmlEngine::CppOwnership); + QQmlEngine::setObjectOwnership(mainWindow->ui->graphicsView_Map, QQmlEngine::CppOwnership); + QQmlEngine::setObjectOwnership(instance->scriptUtility, QQmlEngine::CppOwnership); + QJSValue constants = instance->engine->newObject(); // Get version numbers From 168616771459e16a899b986d37d9a15565d987db Mon Sep 17 00:00:00 2001 From: GriffinR Date: Fri, 9 Aug 2024 01:29:28 -0400 Subject: [PATCH 39/76] Fix more minor map connection issues --- forms/connectionslistitem.ui | 3 + include/core/map.h | 9 +- include/core/mapconnection.h | 36 ++-- include/editor.h | 30 ++-- include/mainwindow.h | 8 +- include/ui/divingmappixmapitem.h | 35 ++++ include/ui/newmapconnectiondialog.h | 1 - porymap.pro | 1 + src/core/map.cpp | 59 +++++-- src/core/mapconnection.cpp | 166 ++++++++--------- src/editor.cpp | 264 +++++++++++++++------------- src/mainwindow.cpp | 55 +++--- src/project.cpp | 15 +- src/ui/connectionslistitem.cpp | 4 +- src/ui/mapimageexporter.cpp | 2 +- src/ui/newmapconnectiondialog.cpp | 10 +- 16 files changed, 382 insertions(+), 316 deletions(-) create mode 100644 include/ui/divingmappixmapitem.h diff --git a/forms/connectionslistitem.ui b/forms/connectionslistitem.ui index 434c5a29..c39c86d0 100644 --- a/forms/connectionslistitem.ui +++ b/forms/connectionslistitem.ui @@ -16,6 +16,9 @@ 0 + + .ConnectionsListItem { border-width: 1px; } + QFrame::StyledPanel diff --git a/include/core/map.h b/include/core/map.h index 1dfc881e..9f039a6f 100644 --- a/include/core/map.h +++ b/include/core/map.h @@ -69,7 +69,6 @@ public: QMap> events; QList ownedEvents; // for memory management - QList connections; QList metatileLayerOrder; QList metatileLayerOpacity; @@ -98,8 +97,11 @@ public: QStringList getScriptLabels(Event::Group group = Event::Group::None); void removeEvent(Event *); void addEvent(Event *); - bool removeConnection(MapConnection *); + void deleteConnections(); + QList getConnections() const; + void removeConnection(MapConnection *); void addConnection(MapConnection *); + void loadConnection(MapConnection *); QPixmap renderConnection(const QString &, MapLayout *); QPixmap renderBorder(bool ignoreCache = false); void setDimensions(int newWidth, int newHeight, bool setNewBlockdata = true, bool enableScriptCallback = false); @@ -129,12 +131,15 @@ private: void setNewDimensionsBlockdata(int newWidth, int newHeight); void setNewBorderDimensionsBlockdata(int newWidth, int newHeight); + QList connections; + signals: void modified(); void mapDimensionsChanged(const QSize &size); void mapNeedsRedrawing(); void openScriptRequested(QString label); void connectionAdded(MapConnection*); + void connectionRemoved(MapConnection*); }; #endif // MAP_H diff --git a/include/core/mapconnection.h b/include/core/mapconnection.h index d934b388..956574dc 100644 --- a/include/core/mapconnection.h +++ b/include/core/mapconnection.h @@ -7,24 +7,27 @@ #include class Project; +class Map; class MapConnection : public QObject { Q_OBJECT public: - MapConnection(const QString &direction, const QString &hostMapName, const QString &targetMapName, int offset = 0); + MapConnection(const QString &targetMapName, const QString &direction, int offset = 0); + + Map* parentMap() const { return m_parentMap; } + QString parentMapName() const; + void setParentMap(Map* map, bool mirror = true); + + Map* targetMap() const; + QString targetMapName() const { return m_targetMapName; } + void setTargetMapName(const QString &targetMapName, bool mirror = true); QString direction() const { return m_direction; } - void setDirection(const QString &direction); - - QString hostMapName() const { return m_hostMapName; } - void setHostMapName(const QString &hostMapName); - - QString targetMapName() const { return m_targetMapName; } - void setTargetMapName(const QString &targetMapName); + void setDirection(const QString &direction, bool mirror = true); int offset() const { return m_offset; } - void setOffset(int offset); + void setOffset(int offset, bool mirror = true); bool isMirror(const MapConnection*); MapConnection* findMirror(); @@ -38,25 +41,22 @@ public: static bool isCardinal(const QString &direction); static bool isHorizontal(const QString &direction); static bool isVertical(const QString &direction); + static bool isDiving(const QString &direction); static QString oppositeDirection(const QString &direction) { return oppositeDirections.value(direction, direction); } private: - QString m_direction; - QString m_hostMapName; + Map* m_parentMap; QString m_targetMapName; + QString m_direction; int m_offset; - bool m_ignoreMirror; - void mirrorDirection(const QString &direction); - void mirrorHostMapName(const QString &hostMapName); - void mirrorTargetMapName(const QString &targetMapName); - void mirrorOffset(int offset); void markMapEdited(); + Map* getMap(const QString& mapName) const; signals: - void directionChanged(const QString &before, const QString &after); + void parentMapChanged(Map* before, Map* after); void targetMapNameChanged(const QString &before, const QString &after); - void hostMapNameChanged(const QString &before, const QString &after); + void directionChanged(const QString &before, const QString &after); void offsetChanged(int before, int after); }; diff --git a/include/editor.h b/include/editor.h index db07fd87..86d5bfdb 100644 --- a/include/editor.h +++ b/include/editor.h @@ -18,6 +18,7 @@ #include "ui_mainwindow.h" #include "bordermetatilespixmapitem.h" #include "connectionpixmapitem.h" +#include "divingmappixmapitem.h" #include "currentselectedmetatilespixmapitem.h" #include "collisionpixmapitem.h" #include "mappixmapitem.h" @@ -76,17 +77,15 @@ public: void setEditingConnections(); void setMapEditingButtonsEnabled(bool enabled); void setConnectionsVisibility(bool visible); - void updateDiveEmergeVisibility(); - void addConnection(MapConnection* connection, bool addMirror = true); - void removeConnection(MapConnection* connection, bool addMirror = true); - void removeConnectionPixmap(ConnectionPixmapItem* connectionItem); + void updateDivingMapsVisibility(); + void addConnection(MapConnection* connection); + void removeConnection(MapConnection* connection); void removeSelectedConnection(); void addNewWildMonGroup(QWidget *window); void deleteWildMonGroup(); void updateDiveMap(QString mapName); void updateEmergeMap(QString mapName); - void selectLastConnection(); - void setSelectedConnectionFromMap(QString mapName); + void setSelectedConnection(MapConnection *connection); void updatePrimaryTileset(QString tilesetLabel, bool forceLoad = false); void updateSecondaryTileset(QString tilesetLabel, bool forceLoad = false); void toggleBorderVisibility(bool visible, bool enableScriptCallback = true); @@ -111,10 +110,9 @@ public: QPointer scene = nullptr; QGraphicsPixmapItem *current_view = nullptr; QPointer map_item = nullptr; - ConnectionPixmapItem* selected_connection_item = nullptr; - QList connection_items; - QGraphicsPixmapItem *dive_map_overlay = nullptr; - QGraphicsPixmapItem *emerge_map_overlay = nullptr; + QPointer selected_connection_item = nullptr; + QList> connection_items; + QMap> diving_map_items; QGraphicsPathItem *connection_mask = nullptr; QPointer collision_item = nullptr; QGraphicsItemGroup *events_group = nullptr; @@ -179,19 +177,19 @@ private: void clearCurrentMetatilesSelection(); void clearMapEvents(); void clearMapConnections(); - void clearDiveMap(); - void clearEmergeMap(); void clearConnectionMask(); void clearMapBorder(); void clearMapGrid(); void clearWildMonTables(); void updateBorderVisibility(); QPoint calculateConnectionPosition(MapConnection *connection, const QPixmap &pixmap); + void removeConnectionPixmap(MapConnection* connection); void updateConnectionPixmap(ConnectionPixmapItem* connectionItem); void updateConnectionPixmapPos(ConnectionPixmapItem* connectionItem); void displayConnection(MapConnection* connection); - void displayDiveEmergeConnection(MapConnection* connection); - void setDiveEmergeMapName(QString mapName, QString direction); + void displayDivingConnection(MapConnection* connection); + void setDivingMapName(QString mapName, QString direction); + void removeDivingMapPixmap(MapConnection* connection); void updateEncounterFields(EncounterFields newFields); QString getMovementPermissionText(uint16_t collision, uint16_t elevation); QString getMetatileDisplayMessage(uint16_t metatileId); @@ -207,7 +205,7 @@ private slots: void setStraightPathCursorMode(QGraphicsSceneMouseEvent *event); void mouseEvent_map(QGraphicsSceneMouseEvent *event, MapPixmapItem *item); void mouseEvent_collision(QGraphicsSceneMouseEvent *event, CollisionPixmapItem *item); - void setSelectedConnection(ConnectionPixmapItem* connectionItem); + void setSelectedConnectionItem(ConnectionPixmapItem* connectionItem); void onHoveredMovementPermissionChanged(uint16_t, uint16_t); void onHoveredMovementPermissionCleared(); void onHoveredMetatileSelectionChanged(uint16_t); @@ -223,7 +221,7 @@ private slots: signals: void objectsChanged(); - void openConnectedMap(QString, QString); + void openConnectedMap(MapConnection*); void wildMonDataChanged(); void warpEventDoubleClicked(QString, int, Event::Group); void currentMetatilesSelectionChanged(); diff --git a/include/mainwindow.h b/include/mainwindow.h index ebc0ea16..644cafd4 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -178,7 +178,7 @@ private slots: void copy(); void paste(); - void onOpenConnectedMap(QString, QString); + void onOpenConnectedMap(MapConnection*); void onMapNeedsRedrawing(); void onTilesetsSaved(QString, QString); void onWildMonDataChanged(); @@ -321,9 +321,7 @@ private: QStandardItemModel *mapListModel; QList *mapGroupItemsList; QMap mapListIndexes; - QIcon* mapIcon; - QIcon* mapEditedIcon; - QIcon* mapOpenedIcon; + QIcon mapIcon; QAction *undoAction = nullptr; QAction *redoAction = nullptr; @@ -411,7 +409,7 @@ private: int insertTilesetLabel(QStringList * list, QString label); void checkForUpdates(bool requestedByUser); - void setDiveEmergeMapVisible(bool visible); + void setDivingMapsVisible(bool visible); }; enum MapListUserRoles { diff --git a/include/ui/divingmappixmapitem.h b/include/ui/divingmappixmapitem.h new file mode 100644 index 00000000..96b47e47 --- /dev/null +++ b/include/ui/divingmappixmapitem.h @@ -0,0 +1,35 @@ +#ifndef DIVINGMAPPIXMAPITEM_H +#define DIVINGMAPPIXMAPITEM_H + +#include "mapconnection.h" + +#include +#include + +class DivingMapPixmapItem : public QObject, public QGraphicsPixmapItem { + Q_OBJECT +public: + DivingMapPixmapItem(MapConnection* connection) + : QGraphicsPixmapItem(getBasePixmap(connection)) + { + m_connection = connection; + setZValue(2); + + // Update pixmap if the connected map is swapped. + connect(m_connection, &MapConnection::targetMapNameChanged, this, &DivingMapPixmapItem::updatePixmap); + } + MapConnection* connection() const { return m_connection; } + +private: + QPointer m_connection; + + static QPixmap getBasePixmap(MapConnection* connection) { + // If the map is connected to itself then rendering is pointless. + if (!connection || connection->targetMapName() == connection->parentMapName()) + return QPixmap(); + return connection->getPixmap(); + } + void updatePixmap() { setPixmap(getBasePixmap(m_connection)); } +}; + +#endif // DIVINGMAPPIXMAPITEM_H diff --git a/include/ui/newmapconnectiondialog.h b/include/ui/newmapconnectiondialog.h index 8d053515..3b37c454 100644 --- a/include/ui/newmapconnectiondialog.h +++ b/include/ui/newmapconnectiondialog.h @@ -23,7 +23,6 @@ public: private: Ui::NewMapConnectionDialog *ui; - Map *map; }; #endif // NEWMAPCONNECTIONDIALOG_H diff --git a/porymap.pro b/porymap.pro index 8f785541..e5cad6dd 100644 --- a/porymap.pro +++ b/porymap.pro @@ -145,6 +145,7 @@ HEADERS += include/core/block.h \ include/ui/connectionslistitem.h \ include/ui/customscriptseditor.h \ include/ui/customscriptslistitem.h \ + include/ui/divingmappixmapitem.h \ include/ui/draggablepixmapitem.h \ include/ui/bordermetatilespixmapitem.h \ include/ui/collisionpixmapitem.h \ diff --git a/src/core/map.cpp b/src/core/map.cpp index 75191a2d..b0abe125 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -17,11 +17,9 @@ Map::Map(QObject *parent) : QObject(parent) } Map::~Map() { - // delete all associated events - while (!ownedEvents.isEmpty()) { - Event *last = ownedEvents.takeLast(); - if (last) delete last; - } + qDeleteAll(ownedEvents); + ownedEvents.clear(); + deleteConnections(); } void Map::setName(QString mapName) { @@ -219,7 +217,7 @@ QPixmap Map::renderBorder(bool ignoreCache) { QPixmap Map::renderConnection(const QString &direction, MapLayout * fromLayout) { if (direction == "dive" || direction == "emerge") - return render(); + return render(true); if (!MapConnection::isCardinal(direction)) return QPixmap(); @@ -513,22 +511,57 @@ void Map::addEvent(Event *event) { if (!ownedEvents.contains(event)) ownedEvents.append(event); } -bool Map::removeConnection(MapConnection *connection) { - if (connections.removeOne(connection)) { - modify(); - return true; - } - return false; +void Map::deleteConnections() { + qDeleteAll(connections); + connections.clear(); +} + +QList Map::getConnections() const { + return connections; } void Map::addConnection(MapConnection *connection) { if (!connection || connections.contains(connection)) return; - connections.append(connection); + + // Adding new Dive/Emerge maps replaces an existing one. + if (MapConnection::isDiving(connection->direction())) { + for (auto connectionToReplace : connections) { + if (connectionToReplace->direction() != connection->direction()) + continue; + if (porymapConfig.mirrorConnectingMaps) { + auto mirror = connectionToReplace->findMirror(); + if (mirror && mirror->parentMap()) + mirror->parentMap()->removeConnection(mirror); + delete mirror; + } + removeConnection(connectionToReplace); + delete connectionToReplace; + break; + } + } + + loadConnection(connection); modify(); emit connectionAdded(connection); } +void Map::loadConnection(MapConnection *connection) { + if (!connection) + return; + connections.append(connection); + connection->setParentMap(this, false); +} + +// Caller takes ownership of connection +void Map::removeConnection(MapConnection *connection) { + if (!connections.removeOne(connection)) + return; + connection->setParentMap(nullptr, false); + modify(); + emit connectionRemoved(connection); +} + void Map::modify() { emit modified(); } diff --git a/src/core/mapconnection.cpp b/src/core/mapconnection.cpp index 5139d810..5fe2e730 100644 --- a/src/core/mapconnection.cpp +++ b/src/core/mapconnection.cpp @@ -9,36 +9,32 @@ const QMap MapConnection::oppositeDirections = { {"dive", "emerge"}, {"emerge", "dive"} }; -MapConnection::MapConnection(const QString &direction, const QString &hostMapName, const QString &targetMapName, int offset) { - m_direction = direction; - m_hostMapName = hostMapName; +MapConnection::MapConnection(const QString &targetMapName, const QString &direction, int offset) { + m_parentMap = nullptr; m_targetMapName = targetMapName; + m_direction = direction; m_offset = offset; - m_ignoreMirror = false; } bool MapConnection::isMirror(const MapConnection* other) { - if (!other) + if (!other || !other->m_parentMap) return false; - return m_hostMapName == other->m_targetMapName - && m_targetMapName == other->m_hostMapName + return parentMapName() == other->m_targetMapName + && m_targetMapName == other->parentMapName() && m_offset == -other->m_offset && m_direction == oppositeDirection(other->m_direction); } MapConnection* MapConnection::findMirror() { - if (!porymapConfig.mirrorConnectingMaps || !project || m_ignoreMirror) - return nullptr; - - Map *connectedMap = project->getMap(m_targetMapName); - if (!connectedMap) + auto map = targetMap(); + if (!map) return nullptr; // Find the matching connection in the connected map. // Note: There is no strict source -> mirror pairing, i.e. we are not guaranteed // to always get the same MapConnection if there are multiple identical copies. - for (auto connection : connectedMap->connections) { + for (auto connection : map->getConnections()) { if (this != connection && this->isMirror(connection)) return connection; } @@ -46,122 +42,114 @@ MapConnection* MapConnection::findMirror() { } MapConnection * MapConnection::createMirror() { - if (!porymapConfig.mirrorConnectingMaps) - return nullptr; - return new MapConnection(oppositeDirection(m_direction), m_targetMapName, m_hostMapName, -m_offset); + auto mirror = new MapConnection(parentMapName(), oppositeDirection(m_direction), -m_offset); + mirror->setParentMap(targetMap(), false); + return mirror; } void MapConnection::markMapEdited() { - if (project) { - Map * map = project->getMap(m_hostMapName); - if (map) map->modify(); - } + if (m_parentMap) + m_parentMap->modify(); +} + +Map* MapConnection::getMap(const QString& mapName) const { + return project ? project->getMap(mapName) : nullptr; +} + +Map* MapConnection::targetMap() const { + return getMap(m_targetMapName); } QPixmap MapConnection::getPixmap() { - if (!project) + auto map = targetMap(); + if (!map) return QPixmap(); - Map *hostMap = project->getMap(m_hostMapName); - Map *targetMap = project->getMap(m_targetMapName); - if (!hostMap || !targetMap) - return QPixmap(); - - return targetMap->renderConnection(m_direction, hostMap->layout); + return map->renderConnection(m_direction, m_parentMap ? m_parentMap->layout : nullptr); } -void MapConnection::setDirection(const QString &direction) { - if (direction == m_direction) +void MapConnection::setParentMap(Map* map, bool mirror) { + if (map == m_parentMap) return; - mirrorDirection(direction); - auto before = m_direction; - m_direction = direction; - emit directionChanged(before, m_direction); - markMapEdited(); -} - -void MapConnection::mirrorDirection(const QString &direction) { - MapConnection * mirror = findMirror(); - if (!mirror) - return; - mirror->m_ignoreMirror = true; - mirror->setDirection(oppositeDirection(direction)); - mirror->m_ignoreMirror = false; -} - -void MapConnection::setHostMapName(const QString &hostMapName) { - if (hostMapName == m_hostMapName) - return; - - mirrorHostMapName(hostMapName); - auto before = m_hostMapName; - m_hostMapName = hostMapName; - - if (project) { - // TODO: This is probably unexpected design, esp because creating a MapConnection doesn't insert to host map - // Reassign connection to new host map - Map * oldMap = project->getMap(before); - Map * newMap = project->getMap(m_hostMapName); - if (oldMap) oldMap->removeConnection(this); - if (newMap) newMap->addConnection(this); + if (mirror && porymapConfig.mirrorConnectingMaps) { + auto connection = findMirror(); + if (connection) + connection->setTargetMapName(map ? map->name : QString(), false); } - emit hostMapNameChanged(before, m_hostMapName); + + if (m_parentMap) + m_parentMap->removeConnection(this); + + auto before = m_parentMap; + m_parentMap = map; + + if (m_parentMap) + m_parentMap->addConnection(this); + + emit parentMapChanged(before, m_parentMap); } -void MapConnection::mirrorHostMapName(const QString &hostMapName) { - MapConnection * mirror = findMirror(); - if (!mirror) - return; - mirror->m_ignoreMirror = true; - mirror->setTargetMapName(hostMapName); - mirror->m_ignoreMirror = false; +QString MapConnection::parentMapName() const { + return m_parentMap ? m_parentMap->name : QString(); } -void MapConnection::setTargetMapName(const QString &targetMapName) { +void MapConnection::setTargetMapName(const QString &targetMapName, bool mirror) { if (targetMapName == m_targetMapName) return; - mirrorTargetMapName(targetMapName); + if (mirror && porymapConfig.mirrorConnectingMaps) { + auto connection = findMirror(); + if (connection) + connection->setParentMap(getMap(targetMapName), false); + } + auto before = m_targetMapName; m_targetMapName = targetMapName; emit targetMapNameChanged(before, m_targetMapName); markMapEdited(); } -void MapConnection::mirrorTargetMapName(const QString &targetMapName) { - MapConnection * mirror = findMirror(); - if (!mirror) +void MapConnection::setDirection(const QString &direction, bool mirror) { + if (direction == m_direction) return; - mirror->m_ignoreMirror = true; - mirror->setHostMapName(targetMapName); - mirror->m_ignoreMirror = false; + + if (mirror && porymapConfig.mirrorConnectingMaps) { + auto connection = findMirror(); + if (connection) + connection->setDirection(oppositeDirection(direction), false); + } + + auto before = m_direction; + m_direction = direction; + emit directionChanged(before, m_direction); + markMapEdited(); } -void MapConnection::setOffset(int offset) { +void MapConnection::setOffset(int offset, bool mirror) { if (offset == m_offset) return; - mirrorOffset(offset); + if (mirror && porymapConfig.mirrorConnectingMaps) { + auto connection = findMirror(); + if (connection) + connection->setOffset(-offset, false); + } + auto before = m_offset; m_offset = offset; emit offsetChanged(before, m_offset); markMapEdited(); } -void MapConnection::mirrorOffset(int offset) { - MapConnection * mirror = findMirror(); - if (!mirror) - return; - mirror->m_ignoreMirror = true; - mirror->setOffset(-offset); - mirror->m_ignoreMirror = false; -} - const QStringList MapConnection::cardinalDirections = { "up", "down", "left", "right" }; +bool MapConnection::isCardinal(const QString &direction) { + return cardinalDirections.contains(direction); +} + bool MapConnection::isHorizontal(const QString &direction) { return direction == "left" || direction == "right"; } @@ -170,6 +158,6 @@ bool MapConnection::isVertical(const QString &direction) { return direction == "up" || direction == "down"; } -bool MapConnection::isCardinal(const QString &direction) { - return cardinalDirections.contains(direction); +bool MapConnection::isDiving(const QString &direction) { + return direction == "dive" || direction == "emerge"; } diff --git a/src/editor.cpp b/src/editor.cpp index db93600c..981fbd57 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -741,8 +741,8 @@ void Editor::displayConnection(MapConnection* connection) { // updated by mirroring and the target map is changing to/from the current map. connection->disconnect(); - if (connection->direction() == "dive" || connection->direction() == "emerge") { - displayDiveEmergeConnection(connection); + if (MapConnection::isDiving(connection->direction())) { + displayDivingConnection(connection); return; } @@ -765,10 +765,10 @@ void Editor::displayConnection(MapConnection* connection) { // Sync the selection highlight between the list UI and the pixmap connect(pixmapItem, &ConnectionPixmapItem::selectionChanged, [this, listItem, pixmapItem](bool selected) { listItem->setSelected(selected); - if (selected) setSelectedConnection(pixmapItem); + if (selected) setSelectedConnectionItem(pixmapItem); }); connect(listItem, &ConnectionsListItem::selected, [this, pixmapItem] { - setSelectedConnection(pixmapItem); + setSelectedConnectionItem(pixmapItem); }); // Sync edits to 'offset' between the list UI and the pixmap @@ -790,49 +790,47 @@ void Editor::displayConnection(MapConnection* connection) { // Sync edits to 'map' between the list UI and the pixmap connect(connection, &MapConnection::targetMapNameChanged, [this, listItem, pixmapItem](QString, QString) { + // The old offset may not make sense, so we reset it + pixmapItem->connection->setOffset(0); listItem->updateUI(); updateConnectionPixmap(pixmapItem); }); - connect(connection, &MapConnection::hostMapNameChanged, [this, pixmapItem](QString before, QString) { - // A connection was removed from the current map by reassignment. We're deleting the UI elements, - // but not the associated connection (it still exists, but now it belongs to a different map). - // At the moment this is only possible when updating the mirror for a map connected to itself, - // users have no other way (or need) to relocate a map connection like this. - if (before == this->map->name) - removeConnectionPixmap(pixmapItem); - }); - // User manually deleting connection via the remove button connect(listItem, &ConnectionsListItem::removed, [this](MapConnection* connection) { removeConnection(connection); }); - // Sync removing the UI elements. They may be removed when deleting connections or when displaying a new map. - connect(connection, &MapConnection::destroyed, [this, pixmapItem] { removeConnectionPixmap(pixmapItem); }); + // When the pixmap is deleted, remove its associated list item connect(pixmapItem, &ConnectionPixmapItem::destroyed, listItem, &ConnectionsListItem::deleteLater); connection_items.append(pixmapItem); } -void Editor::addConnection(MapConnection * connection, bool addMirror) { +void Editor::addConnection(MapConnection * connection) { if (!connection) return; - if (addMirror) addConnection(connection->createMirror(), false); + this->map->addConnection(connection); + if (porymapConfig.mirrorConnectingMaps) + connection->createMirror(); - Map *map = project->getMap(connection->hostMapName()); - if (map) map->addConnection(connection); // TODO: Edit history } -void Editor::removeConnection(MapConnection* connection, bool removeMirror) { +void Editor::removeConnection(MapConnection* connection) { if (!connection) return; - if (removeMirror) removeConnection(connection->findMirror(), false); + if (porymapConfig.mirrorConnectingMaps) { + auto mirror = connection->findMirror(); + if (mirror && mirror->parentMap()) + mirror->parentMap()->removeConnection(mirror); + delete mirror; + } - Map* map = project->getMap(connection->hostMapName()); - if (map) map->removeConnection(connection); + if (connection->parentMap()) + connection->parentMap()->removeConnection(connection); delete connection; + // TODO: Edit history } @@ -841,20 +839,30 @@ void Editor::removeSelectedConnection() { removeConnection(selected_connection_item->connection); } -void Editor::removeConnectionPixmap(ConnectionPixmapItem* pixmapItem) { - if (!pixmapItem) +void Editor::removeConnectionPixmap(MapConnection* connection) { + if (!connection) return; - int index = connection_items.indexOf(pixmapItem); - if (index >= 0) { - connection_items.removeAt(index); - if (pixmapItem == selected_connection_item) { - // This was the selected connection, select the next one up in the list. - selected_connection_item = nullptr; - if (index != 0) index--; - if (connection_items.length() > index) - setSelectedConnection(connection_items.at(index)); - } + if (MapConnection::isDiving(connection->direction())) { + removeDivingMapPixmap(connection); + return; + } + + int i; + for (i = 0; i < connection_items.length(); i++) { + if (connection_items.at(i)->connection == connection) + break; + } + if (i == connection_items.length()) + return; // Connection is not displayed, nothing to do. + + auto pixmapItem = connection_items.takeAt(i); + if (pixmapItem == selected_connection_item) { + // This was the selected connection, select the next one up in the list. + selected_connection_item = nullptr; + if (i != 0) i--; + if (connection_items.length() > i) + setSelectedConnectionItem(connection_items.at(i)); } if (pixmapItem->scene()) @@ -863,63 +871,82 @@ void Editor::removeConnectionPixmap(ConnectionPixmapItem* pixmapItem) { delete pixmapItem; } -// TODO: Self-connecting a Dive/Emerge map connection will not actually replace any existing Dive/Emerge connection, if there is one. -void Editor::displayDiveEmergeConnection(MapConnection* connection) { +void Editor::displayDivingConnection(MapConnection* connection) { if (!connection) return; + const QString direction = connection->direction(); + if (!MapConnection::isDiving(direction)) + return; + // Note: We only support editing 1 Dive and Emerge connection per map. // In a vanilla game only the first Dive/Emerge connection is considered, so allowing // users to have multiple is likely to lead to confusion. In case users have changed // this we won't delete extra diving connections, but we'll only display the first one. - if (connection->direction() == "dive") { - if (dive_map_overlay) return; - } else if (connection->direction() == "emerge") { - if (emerge_map_overlay) return; - } else { - // Invalid direction + if (diving_map_items.value(direction)) return; - } - // Create image of Dive/Emerge map (unless we'd be rendering the current map on top of itself) - QPixmap pixmap = (connection->targetMapName() == this->map->name) ? QPixmap() : connection->getPixmap(); - QGraphicsPixmapItem *item = new QGraphicsPixmapItem(pixmap); + // Create map image + auto item = new DivingMapPixmapItem(connection); scene->addItem(item); + diving_map_items.insert(direction, item); + maskNonVisibleConnectionTiles(); - if (connection->direction() == "dive") { - dive_map_overlay = item; - const QSignalBlocker blocker(ui->comboBox_DiveMap); - ui->comboBox_DiveMap->setCurrentText(connection->targetMapName()); - connect(connection, &MapConnection::destroyed, this, &Editor::clearDiveMap); - connect(connection, &MapConnection::hostMapNameChanged, this, &Editor::clearDiveMap); - } else { - emerge_map_overlay = item; - const QSignalBlocker blocker(ui->comboBox_EmergeMap); - ui->comboBox_EmergeMap->setCurrentText(connection->targetMapName()); - connect(connection, &MapConnection::destroyed, this, &Editor::clearEmergeMap); - connect(connection, &MapConnection::hostMapNameChanged, this, &Editor::clearEmergeMap); + // Display map name in combo box + auto comboBox = (direction == "dive") ? ui->comboBox_DiveMap : ui->comboBox_EmergeMap; + const QSignalBlocker blocker(comboBox); + comboBox->setCurrentText(connection->targetMapName()); +} + +void Editor::removeDivingMapPixmap(MapConnection *connection) { + if (!connection) + return; + + const QString direction = connection->direction(); + if (!diving_map_items.contains(direction)) + return; + + // If the diving map being displayed is different than the one being removed we don't need to do anything. + if (diving_map_items.value(direction)->connection() != connection) + return; + + // Delete map image + auto pixmapItem = diving_map_items.take(direction); + if (pixmapItem->scene()) + pixmapItem->scene()->removeItem(pixmapItem); + delete pixmapItem; + + // Clear map name from combo box + auto comboBox = (direction == "dive") ? ui->comboBox_DiveMap : ui->comboBox_EmergeMap; + const QSignalBlocker blocker(comboBox); + comboBox->setCurrentText(""); + + // Reveal any previously-hidden connection (because we only ever display one diving map of each type). + // Note: When this occurs as a result of the user clicking the 'X' clear button it seems the QComboBox + // doesn't expect the line edit to be immediately repopulated, and the 'X' doesn't reappear. + // As a workaround we wait before displaying the new text. The wait time is essentially arbitrary. + for (auto i : map->getConnections()) { + if (i->direction() == direction) { + QTimer::singleShot(10, Qt::CoarseTimer, [this, i]() { displayDivingConnection(i); }); + break; + } } - - // Update pixmap if the connected map is swapped - connect(connection, &MapConnection::targetMapNameChanged, [this, item, connection] { - item->setPixmap((connection->targetMapName() == this->map->name) ? QPixmap() : connection->getPixmap()); - }); } void Editor::updateDiveMap(QString mapName) { - setDiveEmergeMapName(mapName, "dive"); + setDivingMapName(mapName, "dive"); } void Editor::updateEmergeMap(QString mapName) { - setDiveEmergeMapName(mapName, "emerge"); + setDivingMapName(mapName, "emerge"); } -void Editor::setDiveEmergeMapName(QString mapName, QString direction) { +void Editor::setDivingMapName(QString mapName, QString direction) { // Only the first Dive/Emerge map (if present) is considered, as in-game. MapConnection* connection = nullptr; - for (MapConnection* conn : map->connections) { - if (conn->direction() == direction) { - connection = conn; + for (auto i : map->getConnections()) { + if (i->direction() == direction) { + connection = i; break; } } @@ -928,31 +955,32 @@ void Editor::setDiveEmergeMapName(QString mapName, QString direction) { // Update existing connection if (mapName.isEmpty()) { removeConnection(connection); - // TODO: Queue up another Dive/Emerge map if there is one } else { connection->setTargetMapName(mapName); } } else if (!mapName.isEmpty()) { // Create new connection - addConnection(new MapConnection(direction, map->name, mapName)); + addConnection(new MapConnection(mapName, direction)); } - updateDiveEmergeVisibility(); + updateDivingMapsVisibility(); + maskNonVisibleConnectionTiles(); } -void Editor::updateDiveEmergeVisibility() { - if (dive_map_overlay && emerge_map_overlay) { +void Editor::updateDivingMapsVisibility() { + auto dive = diving_map_items.value("dive"); + auto emerge = diving_map_items.value("emerge"); + + if (dive && emerge) { // Both connections in use, use separate sliders ui->stackedWidget_DiveMapOpacity->setCurrentIndex(0); - dive_map_overlay->setOpacity(!porymapConfig.showDiveEmergeMaps ? 0 : static_cast(porymapConfig.diveMapOpacity) / 100); - emerge_map_overlay->setOpacity(!porymapConfig.showDiveEmergeMaps ? 0 : static_cast(porymapConfig.emergeMapOpacity) / 100); + dive->setOpacity(!porymapConfig.showDiveEmergeMaps ? 0 : static_cast(porymapConfig.diveMapOpacity) / 100); + emerge->setOpacity(!porymapConfig.showDiveEmergeMaps ? 0 : static_cast(porymapConfig.emergeMapOpacity) / 100); } else { // One connection in use (or none), use single slider ui->stackedWidget_DiveMapOpacity->setCurrentIndex(1); qreal opacity = !porymapConfig.showDiveEmergeMaps ? 0 : static_cast(porymapConfig.diveEmergeMapOpacity) / 100; - if (dive_map_overlay) - dive_map_overlay->setOpacity(opacity); - else if (emerge_map_overlay) - emerge_map_overlay->setOpacity(opacity); + if (dive) dive->setOpacity(opacity); + else if (emerge) emerge->setOpacity(opacity); } } @@ -1011,7 +1039,7 @@ void Editor::updateConnectionPixmapPos(ConnectionPixmapItem* pixmapItem) { maskNonVisibleConnectionTiles(); } -void Editor::setSelectedConnection(ConnectionPixmapItem* pixmapItem) { +void Editor::setSelectedConnectionItem(ConnectionPixmapItem* pixmapItem) { if (!pixmapItem || pixmapItem == selected_connection_item) return; @@ -1020,23 +1048,21 @@ void Editor::setSelectedConnection(ConnectionPixmapItem* pixmapItem) { selected_connection_item->setSelected(true); } -// TODO: Inaccurate if there are multiple connections from the same map -void Editor::setSelectedConnectionFromMap(QString mapName) { - // Search for the first connection that connects to the given map. - for (ConnectionPixmapItem* item : connection_items) { - if (item->connection->targetMapName() == mapName) { - setSelectedConnection(item); +void Editor::setSelectedConnection(MapConnection *connection) { + if (!connection) + return; + + for (auto item : connection_items) { + if (item->connection == connection) { + setSelectedConnectionItem(item); break; } } } -void Editor::selectLastConnection() { - setSelectedConnection(connection_items.last()); -} - void Editor::onMapConnectionDoubleClicked(MapConnection* connection) { - emit openConnectedMap(connection->targetMapName(), connection->hostMapName()); + if (connection) + emit openConnectedMap(connection); } void Editor::onBorderMetatilesChanged() { @@ -1222,7 +1248,7 @@ bool Editor::setMap(QString map_name) { // multiple times if set again in the future if (map) { map->disconnect(this); - for (auto connection : map->connections) + for (auto connection : map->getConnections()) connection->disconnect(); } @@ -1244,6 +1270,7 @@ bool Editor::setMap(QString map_name) { connect(map, &Map::mapDimensionsChanged, map_ruler, &MapRuler::setMapDimensions); connect(map, &Map::openScriptRequested, this, &Editor::openScript); connect(map, &Map::connectionAdded, this, &Editor::displayConnection); + connect(map, &Map::connectionRemoved, this, &Editor::removeConnectionPixmap); updateSelectedEvents(); } @@ -1721,56 +1748,38 @@ DraggablePixmapItem *Editor::addMapEvent(Event *event) { } void Editor::clearMapConnections() { - for (ConnectionPixmapItem* item : connection_items) { - if (item->scene()) { + for (auto item : connection_items) { + if (item->scene()) item->scene()->removeItem(item); - } delete item; } connection_items.clear(); - clearDiveMap(); - clearEmergeMap(); - - selected_connection_item = nullptr; -} - -void Editor::clearDiveMap() { - const QSignalBlocker blocker(ui->comboBox_DiveMap); + const QSignalBlocker blocker1(ui->comboBox_DiveMap); + const QSignalBlocker blocker2(ui->comboBox_EmergeMap); ui->comboBox_DiveMap->setCurrentText(""); - - if (dive_map_overlay) { - if (dive_map_overlay->scene()){ - dive_map_overlay->scene()->removeItem(dive_map_overlay); - } - delete dive_map_overlay; - dive_map_overlay = nullptr; - } -} - -void Editor::clearEmergeMap() { - const QSignalBlocker blocker(ui->comboBox_EmergeMap); ui->comboBox_EmergeMap->setCurrentText(""); - if (emerge_map_overlay) { - if (emerge_map_overlay->scene()){ - emerge_map_overlay->scene()->removeItem(emerge_map_overlay); - } - delete emerge_map_overlay; - emerge_map_overlay = nullptr; + for (auto item : diving_map_items.values()) { + if (item->scene()) + item->scene()->removeItem(item); + delete item; } + diving_map_items.clear(); + + selected_connection_item = nullptr; } void Editor::displayMapConnections() { clearMapConnections(); - for (MapConnection *connection : map->connections) + for (MapConnection *connection : map->getConnections()) displayConnection(connection); - if (!connection_items.empty()) - setSelectedConnection(connection_items.first()); + if (!connection_items.isEmpty()) + setSelectedConnectionItem(connection_items.first()); - updateDiveEmergeVisibility(); + updateDivingMapsVisibility(); } void Editor::clearConnectionMask() { @@ -1801,6 +1810,7 @@ void Editor::maskNonVisibleConnectionTiles() { QBrush brush(ui->graphicsView_Map->palette().color(QPalette::Active, QPalette::Base)); connection_mask = scene->addPath(mask, pen, brush); + connection_mask->setZValue(3); // Above diving maps } void Editor::clearMapBorder() { diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 81e19609..48dc6564 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -363,9 +363,7 @@ void MainWindow::initEditor() { } void MainWindow::initMiscHeapObjects() { - mapIcon = new QIcon(QStringLiteral(":/icons/map.ico")); - mapEditedIcon = new QIcon(QStringLiteral(":/icons/map_edited.ico")); - mapOpenedIcon = new QIcon(QStringLiteral(":/icons/map_opened.ico")); + mapIcon = QIcon(QStringLiteral(":/icons/map.ico")); mapListModel = new QStandardItemModel; mapGroupItemsList = new QList; @@ -496,7 +494,7 @@ void MainWindow::loadUserSettings() { ui->horizontalSlider_CollisionZoom->setValue(porymapConfig.collisionZoom); setTheme(porymapConfig.theme); - setDiveEmergeMapVisible(porymapConfig.showDiveEmergeMaps); + setDivingMapsVisible(porymapConfig.showDiveEmergeMaps); } void MainWindow::restoreWindowState() { @@ -755,6 +753,9 @@ void MainWindow::on_action_Close_Project_triggered() { // setMap, but with a visible error message in case of failure. // Use when the user is specifically requesting a map to open. bool MainWindow::userSetMap(QString map_name, bool scrollTreeView) { + if (editor->map && editor->map->name == map_name) + return true; // Already set + if (map_name == DYNAMIC_MAP_NAME) { QMessageBox msgBox(this); QString errorMsg = QString("The map '%1' can't be opened, it's a placeholder to indicate the specified map will be set programmatically.").arg(map_name); @@ -807,7 +808,8 @@ bool MainWindow::setMap(QString map_name, bool scrollTreeView) { connect(editor->map, &Map::mapNeedsRedrawing, this, &MainWindow::onMapNeedsRedrawing); // Swap the "currently-open" icon from the old map to the new map - updateMapListIcon(userConfig.recentMap); + if (!userConfig.recentMap.isEmpty() && userConfig.recentMap != map_name) + updateMapListIcon(userConfig.recentMap); userConfig.recentMap = map_name; updateMapListIcon(userConfig.recentMap); @@ -1230,7 +1232,7 @@ void MainWindow::sortMapList() { QStandardItem* MainWindow::createMapItem(QString mapName, int groupNum, int inGroupNum) { QStandardItem *map = new QStandardItem; map->setText(QString("[%1.%2] ").arg(groupNum).arg(inGroupNum, 2, 10, QLatin1Char('0')) + mapName); - map->setIcon(*mapIcon); + map->setIcon(mapIcon); map->setEditable(false); map->setData(mapName, Qt::UserRole); map->setData("map_name", MapListUserRoles::TypeRole); @@ -1319,7 +1321,7 @@ void MainWindow::onNewMapCreated() { setMap(newMapName, true); // Refresh any combo box that displays map names and persists between maps - // (others combo boxes like for warp destinations are repopulated when the map changes). + // (other combo boxes like for warp destinations are repopulated when the map changes). int index = this->editor->project->mapNames.indexOf(newMapName); if (index >= 0) { const QSignalBlocker blocker1(ui->comboBox_DiveMap); @@ -1544,12 +1546,15 @@ void MainWindow::updateMapListIcon(const QString &mapName) { if (!item) return; + static const QIcon mapEditedIcon = QIcon(QStringLiteral(":/icons/map_edited.ico")); + static const QIcon mapOpenedIcon = QIcon(QStringLiteral(":/icons/map_opened.ico")); + if (editor->map && editor->map->name == mapName) { - item->setIcon(*mapOpenedIcon); + item->setIcon(mapOpenedIcon); } else if (editor->project->mapCache.value(mapName)->hasUnsavedChanges()) { - item->setIcon(*mapEditedIcon); + item->setIcon(mapEditedIcon); } else { - item->setIcon(*mapIcon); + item->setIcon(mapIcon); } } @@ -2273,14 +2278,14 @@ void MainWindow::eventTabChanged(int index) { } void MainWindow::on_actionDive_Emerge_Map_triggered() { - setDiveEmergeMapVisible(ui->actionDive_Emerge_Map->isChecked()); + setDivingMapsVisible(ui->actionDive_Emerge_Map->isChecked()); } void MainWindow::on_groupBox_DiveMapOpacity_toggled(bool on) { - setDiveEmergeMapVisible(on); + setDivingMapsVisible(on); } -void MainWindow::setDiveEmergeMapVisible(bool visible) { +void MainWindow::setDivingMapsVisible(bool visible) { // Qt doesn't change the style of disabled sliders, so we do it ourselves QString stylesheet = visible ? "" : "QSlider::groove:horizontal {border: 1px solid #999999; border-radius: 3px; height: 2px; background: #B1B1B1;}" "QSlider::handle:horizontal {border: 1px solid #444444; border-radius: 3px; width: 10px; height: 9px; margin: -5px -1px; background: #5C5C5C; }"; @@ -2295,7 +2300,7 @@ void MainWindow::setDiveEmergeMapVisible(bool visible) { ui->actionDive_Emerge_Map->setChecked(visible); porymapConfig.showDiveEmergeMaps = visible; - this->editor->updateDiveEmergeVisibility(); + this->editor->updateDivingMapsVisibility(); } // Normally a map only has either a Dive map connection or an Emerge map connection, @@ -2304,17 +2309,17 @@ void MainWindow::setDiveEmergeMapVisible(bool visible) { // modify them independently. void MainWindow::on_slider_DiveEmergeMapOpacity_valueChanged(int value) { porymapConfig.diveEmergeMapOpacity = value; - this->editor->updateDiveEmergeVisibility(); + this->editor->updateDivingMapsVisibility(); } void MainWindow::on_slider_DiveMapOpacity_valueChanged(int value) { porymapConfig.diveMapOpacity = value; - this->editor->updateDiveEmergeVisibility(); + this->editor->updateDivingMapsVisibility(); } void MainWindow::on_slider_EmergeMapOpacity_valueChanged(int value) { porymapConfig.emergeMapOpacity = value; - this->editor->updateDiveEmergeVisibility(); + this->editor->updateDivingMapsVisibility(); } void MainWindow::on_horizontalSlider_CollisionTransparency_valueChanged(int value) { @@ -2529,9 +2534,9 @@ void MainWindow::clickToolButtonFromEditMode(QString editMode) { } } -void MainWindow::onOpenConnectedMap(QString mapName, QString fromMapName) { - if (mapName != fromMapName && userSetMap(mapName, true)) - editor->setSelectedConnectionFromMap(fromMapName); +void MainWindow::onOpenConnectedMap(MapConnection *connection) { + if (userSetMap(connection->targetMapName(), true)) + editor->setSelectedConnection(connection->findMirror()); } void MainWindow::onMapNeedsRedrawing() { @@ -2648,12 +2653,14 @@ void MainWindow::showExportMapImageWindow(ImageExporterMode mode) { openSubWindow(this->mapImageExporter); } -void MainWindow::on_pushButton_AddConnection_clicked() -{ +void MainWindow::on_pushButton_AddConnection_clicked() { + if (!this->editor || !this->editor->map) + return; + auto dialog = new NewMapConnectionDialog(this, this->editor->map, this->editor->project->mapNames); - if(dialog->exec() == QDialog::Accepted) { + if (dialog->exec() == QDialog::Accepted) { this->editor->addConnection(dialog->result); - this->editor->selectLastConnection(); + this->editor->setSelectedConnection(dialog->result); } } diff --git a/src/project.cpp b/src/project.cpp index d60c8a09..10ace396 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -196,10 +196,6 @@ Map* Project::loadMap(QString map_name) { return map; } -void Project::setNewMapConnections(Map *map) { - map->connections.clear(); -} - const QSet defaultTopLevelMapFields = { "id", "name", @@ -368,7 +364,7 @@ bool Project::loadMapData(Map* map) { } } - map->connections.clear(); + map->deleteConnections(); QJsonArray connectionsArr = mapObj["connections"].toArray(); if (!connectionsArr.isEmpty()) { for (int i = 0; i < connectionsArr.size(); i++) { @@ -378,8 +374,7 @@ bool Project::loadMapData(Map* map) { const QString mapConstant = ParseUtil::jsonToQString(connectionObj["map"]); if (mapConstantsToMapNames.contains(mapConstant)) { // Successully read map connection - MapConnection *connection = new MapConnection(direction, map->name, mapConstantsToMapNames.value(mapConstant), offset); - map->connections.append(connection); + map->loadConnection(new MapConnection(mapConstantsToMapNames.value(mapConstant), direction, offset)); } else { logError(QString("Failed to find connected map for map constant '%1'").arg(mapConstant)); } @@ -1280,9 +1275,10 @@ void Project::saveMap(Map *map) { mapObj["battle_scene"] = map->battle_scene; // Connections - if (map->connections.length() > 0) { + auto connections = map->getConnections(); + if (connections.length() > 0) { OrderedJson::array connectionsArr; - for (MapConnection* connection : map->connections) { + for (auto connection : connections) { if (mapNamesToMapConstants.contains(connection->targetMapName())) { OrderedJson::object connectionObj; connectionObj["map"] = this->mapNamesToMapConstants.value(connection->targetMapName()); @@ -1848,7 +1844,6 @@ Map* Project::addNewMapToGroup(QString mapName, int groupNum, Map *newMap, bool loadLayoutTilesets(newMap->layout); setNewMapEvents(newMap); - setNewMapConnections(newMap); return newMap; } diff --git a/src/ui/connectionslistitem.cpp b/src/ui/connectionslistitem.cpp index fd2532bf..2501d2d4 100644 --- a/src/ui/connectionslistitem.cpp +++ b/src/ui/connectionslistitem.cpp @@ -40,13 +40,13 @@ void ConnectionsListItem::updateUI() { ui->spinBox_Offset->setValue(this->connection->offset()); } -// TODO: Frame shifts slightly when style changes void ConnectionsListItem::setSelected(bool selected) { if (selected == this->isSelected) return; this->isSelected = selected; - this->setStyleSheet(selected ? ".ConnectionsListItem { border: 1px solid rgb(255, 0, 255); }" : ""); + this->setStyleSheet(selected ? ".ConnectionsListItem { border: 1px solid rgb(255, 0, 255); }" + : ".ConnectionsListItem { border-width: 1px; }"); if (selected) emit this->selected(); } diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index f26a05a3..03cbc2b8 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -238,7 +238,7 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool inclu visited.insert(cur.map->name); stitchedMaps.append(cur); - for (MapConnection *connection : cur.map->connections) { + for (MapConnection *connection : cur.map->getConnections()) { const QString direction = connection->direction(); int x = cur.x; int y = cur.y; diff --git a/src/ui/newmapconnectiondialog.cpp b/src/ui/newmapconnectiondialog.cpp index 3a7aa481..aa7c05cf 100644 --- a/src/ui/newmapconnectiondialog.cpp +++ b/src/ui/newmapconnectiondialog.cpp @@ -8,7 +8,6 @@ NewMapConnectionDialog::NewMapConnectionDialog(QWidget *parent, Map* map, const ui->setupUi(this); setAttribute(Qt::WA_DeleteOnClose); - this->map = map; this->result = nullptr; ui->comboBox_Direction->setEditable(false); @@ -20,7 +19,7 @@ NewMapConnectionDialog::NewMapConnectionDialog(QWidget *parent, Map* map, const // Choose default direction QMap directionCounts; - for (auto connection : map->connections) { + for (auto connection : map->getConnections()) { directionCounts[connection->direction()]++; } QString defaultDirection; @@ -52,11 +51,6 @@ NewMapConnectionDialog::~NewMapConnectionDialog() } void NewMapConnectionDialog::accept() { - QString direction = ui->comboBox_Direction->currentText(); - QString hostMapName = this->map ? this->map->name : QString(); - QString targetMapName = ui->comboBox_Map->currentText(); - - this->result = new MapConnection(direction, this->map->name, targetMapName); - + this->result = new MapConnection(ui->comboBox_Map->currentText(), ui->comboBox_Direction->currentText()); QDialog::accept(); } From 724b35be951d6306ed7e2e7bc1e4cc7a00b4f332 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 13 Aug 2024 15:12:02 -0400 Subject: [PATCH 40/76] Define destructor for Scripting --- include/scripting.h | 3 ++- src/scriptapi/scripting.cpp | 12 +++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/include/scripting.h b/include/scripting.h index e2bfb1ff..c08bbd74 100644 --- a/include/scripting.h +++ b/include/scripting.h @@ -30,8 +30,9 @@ class Scripting { public: Scripting(MainWindow *mainWindow); - static void stop(); + ~Scripting(); static void init(MainWindow *mainWindow); + static void stop(); static void populateGlobalObject(MainWindow *mainWindow); static QJSEngine *getEngine(); static void invokeAction(int actionIndex); diff --git a/src/scriptapi/scripting.cpp b/src/scriptapi/scripting.cpp index 95a4325f..71fbabca 100644 --- a/src/scriptapi/scripting.cpp +++ b/src/scriptapi/scripting.cpp @@ -24,11 +24,6 @@ const QMap callbackFunctions = { Scripting *instance = nullptr; void Scripting::stop() { - if (!instance) return; - instance->engine->setInterrupted(true); - qDeleteAll(instance->imageCache); - delete instance->engine; - delete instance->scriptUtility; delete instance; instance = nullptr; } @@ -54,6 +49,13 @@ Scripting::Scripting(MainWindow *mainWindow) { this->scriptUtility = new ScriptUtility(mainWindow); } +Scripting::~Scripting() { + this->engine->setInterrupted(true); + qDeleteAll(this->imageCache); + delete this->engine; + delete this->scriptUtility; +} + void Scripting::loadModules(QStringList moduleFiles) { for (QString filepath : moduleFiles) { QString validPath = Project::getExistingFilepath(filepath); From 256f6eed54aa9d97aaecb98368bd01b57916bb55 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 13 Aug 2024 20:36:37 -0400 Subject: [PATCH 41/76] Preserve NoScrollComboBox focus policy on macOS --- include/ui/noscrollcombobox.h | 2 ++ src/ui/noscrollcombobox.cpp | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/include/ui/noscrollcombobox.h b/include/ui/noscrollcombobox.h index 59c680a3..65b6ab5b 100644 --- a/include/ui/noscrollcombobox.h +++ b/include/ui/noscrollcombobox.h @@ -13,6 +13,8 @@ public: void setTextItem(const QString &text); void setNumberItem(int value); void setHexItem(uint32_t value); + void setEditable(bool editable); + void setLineEdit(QLineEdit *edit); private: void setItem(int index, const QString &text); diff --git a/src/ui/noscrollcombobox.cpp b/src/ui/noscrollcombobox.cpp index ee778df5..a634af7f 100644 --- a/src/ui/noscrollcombobox.cpp +++ b/src/ui/noscrollcombobox.cpp @@ -22,6 +22,18 @@ NoScrollComboBox::NoScrollComboBox(QWidget *parent) this->setValidator(validator); } +// On macOS QComboBox::setEditable and QComboBox::setLineEdit will override our changes to the focus policy, so we enforce it here. +void NoScrollComboBox::setEditable(bool editable) { + auto policy = focusPolicy(); + QComboBox::setEditable(editable); + setFocusPolicy(policy); +} +void NoScrollComboBox::setLineEdit(QLineEdit *edit) { + auto policy = focusPolicy(); + QComboBox::setLineEdit(edit); + setFocusPolicy(policy); +} + void NoScrollComboBox::wheelEvent(QWheelEvent *event) { // Only allow scrolling to modify contents when it explicitly has focus. From 63b77a1fdc66fb58ab22db4eb325261e5f893985 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 13 Aug 2024 21:43:23 -0400 Subject: [PATCH 42/76] Ignore scrolling for connection map combo boxes --- include/ui/noscrollcombobox.h | 3 +++ src/mainwindow.cpp | 2 ++ src/ui/connectionslistitem.cpp | 1 + src/ui/noscrollcombobox.cpp | 10 ++++++++-- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/include/ui/noscrollcombobox.h b/include/ui/noscrollcombobox.h index 7b6f7522..32966b3a 100644 --- a/include/ui/noscrollcombobox.h +++ b/include/ui/noscrollcombobox.h @@ -16,9 +16,12 @@ public: void setClearButtonEnabled(bool enabled); void setEditable(bool editable); void setLineEdit(QLineEdit *edit); + void setFocusedScrollingEnabled(bool enabled); private: void setItem(int index, const QString &text); + + bool focusedScrollingEnabled = true; }; #endif // NOSCROLLCOMBOBOX_H diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 48dc6564..c6b8c3c0 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1059,9 +1059,11 @@ bool MainWindow::setProjectUI() { ui->comboBox_DiveMap->clear(); ui->comboBox_DiveMap->addItems(project->mapNames); ui->comboBox_DiveMap->setClearButtonEnabled(true); + ui->comboBox_DiveMap->setFocusedScrollingEnabled(false); ui->comboBox_EmergeMap->clear(); ui->comboBox_EmergeMap->addItems(project->mapNames); ui->comboBox_EmergeMap->setClearButtonEnabled(true); + ui->comboBox_EmergeMap->setFocusedScrollingEnabled(false); sortMapList(); diff --git a/src/ui/connectionslistitem.cpp b/src/ui/connectionslistitem.cpp index 2501d2d4..049bb17d 100644 --- a/src/ui/connectionslistitem.cpp +++ b/src/ui/connectionslistitem.cpp @@ -17,6 +17,7 @@ ConnectionsListItem::ConnectionsListItem(QWidget *parent, MapConnection * connec ui->comboBox_Map->setMinimumContentsLength(6); ui->comboBox_Map->addItems(mapNames); + ui->comboBox_Map->setFocusedScrollingEnabled(false); // Scrolling could cause rapid changes to many different maps ui->spinBox_Offset->setMinimum(INT_MIN); ui->spinBox_Offset->setMaximum(INT_MAX); diff --git a/src/ui/noscrollcombobox.cpp b/src/ui/noscrollcombobox.cpp index 5288ad97..3db2a956 100644 --- a/src/ui/noscrollcombobox.cpp +++ b/src/ui/noscrollcombobox.cpp @@ -37,11 +37,17 @@ void NoScrollComboBox::setLineEdit(QLineEdit *edit) { void NoScrollComboBox::wheelEvent(QWheelEvent *event) { - // Only allow scrolling to modify contents when it explicitly has focus. - if (hasFocus()) + // By default NoScrollComboBoxes will allow scrolling to modify its contents only when it explicitly has focus. + // If focusedScrollingEnabled is false it won't allow scrolling even with focus. + if (this->focusedScrollingEnabled && hasFocus()) QComboBox::wheelEvent(event); } +void NoScrollComboBox::setFocusedScrollingEnabled(bool enabled) +{ + this->focusedScrollingEnabled = enabled; +} + void NoScrollComboBox::setItem(int index, const QString &text) { if (index >= 0) { From 0b800c12011c0430fb01f1073a4540244bb8ff38 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 13 Aug 2024 22:18:40 -0400 Subject: [PATCH 43/76] Skip unnecessary dive map rendering, fix rendering small connections --- include/editor.h | 1 + include/ui/divingmappixmapitem.h | 1 - src/core/map.cpp | 30 ++++++++++++++++++------------ src/editor.cpp | 23 ++++++++++++++++------- src/mainwindow.cpp | 6 ++++++ 5 files changed, 41 insertions(+), 20 deletions(-) diff --git a/include/editor.h b/include/editor.h index 86d5bfdb..46432347 100644 --- a/include/editor.h +++ b/include/editor.h @@ -78,6 +78,7 @@ public: void setMapEditingButtonsEnabled(bool enabled); void setConnectionsVisibility(bool visible); void updateDivingMapsVisibility(); + void displayDivingConnections(); void addConnection(MapConnection* connection); void removeConnection(MapConnection* connection); void removeSelectedConnection(); diff --git a/include/ui/divingmappixmapitem.h b/include/ui/divingmappixmapitem.h index 96b47e47..7596aeaf 100644 --- a/include/ui/divingmappixmapitem.h +++ b/include/ui/divingmappixmapitem.h @@ -13,7 +13,6 @@ public: : QGraphicsPixmapItem(getBasePixmap(connection)) { m_connection = connection; - setZValue(2); // Update pixmap if the connected map is swapped. connect(m_connection, &MapConnection::targetMapNameChanged, this, &DivingMapPixmapItem::updatePixmap); diff --git a/src/core/map.cpp b/src/core/map.cpp index b0abe125..ed011ec5 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -216,23 +216,29 @@ QPixmap Map::renderBorder(bool ignoreCache) { } QPixmap Map::renderConnection(const QString &direction, MapLayout * fromLayout) { - if (direction == "dive" || direction == "emerge") - return render(true); - - if (!MapConnection::isCardinal(direction)) - return QPixmap(); - + // Cardinal connections are rendered within the bounds of the border draw distance, + // and we need to render the nearest segment of the map. + // Dive/Emerge maps are rendered normally, but within the bounds of their parent map. int x = 0, y = 0, w = getWidth(), h = getHeight(); if (direction == "up") { - y = getHeight() - BORDER_DISTANCE; - h = BORDER_DISTANCE; + h = qMin(h, BORDER_DISTANCE); + y = getHeight() - h; } else if (direction == "down") { - h = BORDER_DISTANCE; + h = qMin(h, BORDER_DISTANCE); } else if (direction == "left") { - x = getWidth() - BORDER_DISTANCE; - w = BORDER_DISTANCE; + w = qMin(w, BORDER_DISTANCE); + x = getWidth() - w; } else if (direction == "right") { - w = BORDER_DISTANCE; + w = qMin(w, BORDER_DISTANCE); + } else if (MapConnection::isDiving(direction)) { + if (fromLayout) { + w = qMin(w, fromLayout->getWidth()); + h = qMin(h, fromLayout->getHeight()); + fromLayout = nullptr; // This would be used for palettes later, but we want our own palettes, not the parent's. + } + } else { + // Unknown direction + return QPixmap(); } render(true, fromLayout, QRect(x, y, w, h)); diff --git a/src/editor.cpp b/src/editor.cpp index 981fbd57..e2093665 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -737,10 +737,6 @@ void Editor::displayConnection(MapConnection* connection) { if (!connection) return; - // It's possible for a map connection to be displayed repeatedly if it's being - // updated by mirroring and the target map is changing to/from the current map. - connection->disconnect(); - if (MapConnection::isDiving(connection->direction())) { displayDivingConnection(connection); return; @@ -758,6 +754,10 @@ void Editor::displayConnection(MapConnection* connection) { ConnectionsListItem *listItem = new ConnectionsListItem(ui->scrollAreaContents_ConnectionsList, pixmapItem->connection, project->mapNames); ui->layout_ConnectionsList->insertWidget(ui->layout_ConnectionsList->count() - 1, listItem); // Insert above the vertical spacer + // It's possible for a map connection to be displayed repeatedly if it's being + // updated by mirroring and the target map is changing to/from the current map. + connection->disconnect(); + // Double clicking the list item or pixmap opens the connected map connect(listItem, &ConnectionsListItem::doubleClicked, this, &Editor::onMapConnectionDoubleClicked); connect(pixmapItem, &ConnectionPixmapItem::connectionItemDoubleClicked, this, &Editor::onMapConnectionDoubleClicked); @@ -871,6 +871,14 @@ void Editor::removeConnectionPixmap(MapConnection* connection) { delete pixmapItem; } +void Editor::displayDivingConnections() { + if (!this->map) + return; + + for (auto connection : this->map->getConnections()) + displayDivingConnection(connection); +} + void Editor::displayDivingConnection(MapConnection* connection) { if (!connection) return; @@ -879,6 +887,10 @@ void Editor::displayDivingConnection(MapConnection* connection) { if (!MapConnection::isDiving(direction)) return; + // Save some rendering time if it won't be displayed + if (!porymapConfig.showDiveEmergeMaps) + return; + // Note: We only support editing 1 Dive and Emerge connection per map. // In a vanilla game only the first Dive/Emerge connection is considered, so allowing // users to have multiple is likely to lead to confusion. In case users have changed @@ -890,7 +902,6 @@ void Editor::displayDivingConnection(MapConnection* connection) { auto item = new DivingMapPixmapItem(connection); scene->addItem(item); diving_map_items.insert(direction, item); - maskNonVisibleConnectionTiles(); // Display map name in combo box auto comboBox = (direction == "dive") ? ui->comboBox_DiveMap : ui->comboBox_EmergeMap; @@ -963,7 +974,6 @@ void Editor::setDivingMapName(QString mapName, QString direction) { addConnection(new MapConnection(mapName, direction)); } updateDivingMapsVisibility(); - maskNonVisibleConnectionTiles(); } void Editor::updateDivingMapsVisibility() { @@ -1810,7 +1820,6 @@ void Editor::maskNonVisibleConnectionTiles() { QBrush brush(ui->graphicsView_Map->palette().color(QPalette::Active, QPalette::Base)); connection_mask = scene->addPath(mask, pen, brush); - connection_mask->setZValue(3); // Above diving maps } void Editor::clearMapBorder() { diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index c6b8c3c0..0f1b83df 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2302,6 +2302,12 @@ void MainWindow::setDivingMapsVisible(bool visible) { ui->actionDive_Emerge_Map->setChecked(visible); porymapConfig.showDiveEmergeMaps = visible; + + if (visible) { + // We skip rendering diving maps if this setting is not enabled, + // so when we enable it we need to make sure they've rendered. + this->editor->displayDivingConnections(); + } this->editor->updateDivingMapsVisibility(); } From 403bade4077e7b2fad52412a67210974c8a24524 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Wed, 14 Aug 2024 22:46:37 -0400 Subject: [PATCH 44/76] Fix some minor window issues --- CHANGELOG.md | 2 ++ include/mainwindow.h | 2 ++ src/mainwindow.cpp | 9 ++++----- src/ui/aboutporymap.cpp | 1 + 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67e9a0b3..51520783 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - Fix config files being written before the project is opened successfully. - Fix the map and other project info still displaying if a new project fails to open. - Fix unsaved changes being ignored when quitting (such as with Cmd+Q on macOS). +- Fix `About porymap` opening a new window each time it's activated. +- Fix the `Edit History` window not raising to the front when reactivated. ## [5.4.1] - 2024-03-21 ### Fixed diff --git a/include/mainwindow.h b/include/mainwindow.h index 41bf8a1e..93f0d265 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -28,6 +28,7 @@ #include "projectsettingseditor.h" #include "customscriptseditor.h" #include "updatepromoter.h" +#include "aboutporymap.h" @@ -312,6 +313,7 @@ private: QPointer customScriptsEditor = nullptr; QPointer updatePromoter = nullptr; QPointer networkAccessManager = nullptr; + QPointer aboutWindow = nullptr; FilterChildrenProxyModel *mapListProxyModel; QStandardItemModel *mapListModel; QList *mapGroupItemsList; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 721a3fa3..37f45f20 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,6 +1,5 @@ #include "mainwindow.h" #include "ui_mainwindow.h" -#include "aboutporymap.h" #include "project.h" #include "log.h" #include "editor.h" @@ -330,7 +329,7 @@ void MainWindow::initEditor() { QAction *showHistory = new QAction("Show Edit History...", this); showHistory->setObjectName("action_ShowEditHistory"); showHistory->setShortcut(QKeySequence("Ctrl+E")); - connect(showHistory, &QAction::triggered, [undoView](){ undoView->show(); }); + connect(showHistory, &QAction::triggered, [this, undoView](){ openSubWindow(undoView); }); ui->menuEdit->addAction(showHistory); @@ -2774,9 +2773,9 @@ void MainWindow::on_toolButton_CollapseAll_clicked() void MainWindow::on_actionAbout_Porymap_triggered() { - AboutPorymap *window = new AboutPorymap(this); - window->setAttribute(Qt::WA_DeleteOnClose); - window->show(); + if (!this->aboutWindow) + this->aboutWindow = new AboutPorymap(this); + openSubWindow(this->aboutWindow); } void MainWindow::on_actionOpen_Log_File_triggered() { diff --git a/src/ui/aboutporymap.cpp b/src/ui/aboutporymap.cpp index 38c28144..22c5fabc 100644 --- a/src/ui/aboutporymap.cpp +++ b/src/ui/aboutporymap.cpp @@ -7,6 +7,7 @@ AboutPorymap::AboutPorymap(QWidget *parent) : ui(new Ui::AboutPorymap) { ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose); this->ui->label_Version->setText(QString("Version %1 - %2").arg(QCoreApplication::applicationVersion()).arg(QStringLiteral(__DATE__))); this->ui->textBrowser->setSource(QUrl("qrc:/CHANGELOG.md")); From cbd75dc20b072b67290e535b3d32b8d33b328c80 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Wed, 14 Aug 2024 22:22:54 -0400 Subject: [PATCH 45/76] Map connection edit history --- include/core/editcommands.h | 116 ++++++++++++++++ include/core/map.h | 11 +- include/core/mapconnection.h | 2 +- include/editor.h | 7 +- include/ui/connectionpixmapitem.h | 43 +++--- include/ui/connectionslistitem.h | 7 +- include/ui/divingmappixmapitem.h | 24 ++-- include/ui/newmapconnectiondialog.h | 5 +- porymap.pro | 1 + src/core/editcommands.cpp | 202 ++++++++++++++++++++++++++++ src/core/map.cpp | 106 ++++++++++----- src/core/mapconnection.cpp | 24 ++-- src/editor.cpp | 191 +++++++++++--------------- src/mainwindow.cpp | 10 +- src/ui/connectionpixmapitem.cpp | 82 ++++++++--- src/ui/connectionslistitem.cpp | 33 ++++- src/ui/divingmappixmapitem.cpp | 46 +++++++ src/ui/mapimageexporter.cpp | 11 +- src/ui/newmapconnectiondialog.cpp | 10 +- 19 files changed, 691 insertions(+), 240 deletions(-) create mode 100644 src/ui/divingmappixmapitem.cpp diff --git a/include/core/editcommands.h b/include/core/editcommands.h index ea66b722..40f0cd62 100644 --- a/include/core/editcommands.h +++ b/include/core/editcommands.h @@ -3,9 +3,11 @@ #define EDITCOMMANDS_H #include "blockdata.h" +#include "mapconnection.h" #include #include +#include class MapPixmapItem; class Map; @@ -31,6 +33,11 @@ enum CommandId { ID_EventDelete, ID_EventDuplicate, ID_EventPaste, + ID_MapConnectionMove, + ID_MapConnectionChangeDirection, + ID_MapConnectionChangeMap, + ID_MapConnectionAdd, + ID_MapConnectionRemove, }; #define IDMask_EventType_Object (1 << 8) @@ -379,4 +386,113 @@ private: int newBorderHeight; }; + + +/// Implements a command to commit Map Connectien move actions. +/// Actions are merged into one until the mouse is released when editing by click-and-drag, +/// or when the offset spin box loses focus when editing with the list UI. +class MapConnectionMove : public QUndoCommand { +public: + MapConnectionMove(MapConnection *connection, int newOffset, unsigned actionId, + QUndoCommand *parent = nullptr); + + void undo() override; + void redo() override; + + bool mergeWith(const QUndoCommand *command) override; + int id() const override { return CommandId::ID_MapConnectionMove; } + +private: + MapConnection *connection; + int newOffset; + int oldOffset; + bool mirrored; + unsigned actionId; +}; + + + +/// Implements a command to commit changes to a Map Connectien's 'direction' field. +class MapConnectionChangeDirection : public QUndoCommand { +public: + MapConnectionChangeDirection(MapConnection *connection, QString newDirection, + QUndoCommand *parent = nullptr); + + void undo() override; + void redo() override; + + int id() const override { return CommandId::ID_MapConnectionChangeDirection; } + +private: + QPointer connection; + QString newDirection; + QString oldDirection; + int oldOffset; + int newOffset; + bool mirrored; +}; + + + +/// Implements a command to commit changes to a Map Connectien's 'map' field. +class MapConnectionChangeMap : public QUndoCommand { +public: + MapConnectionChangeMap(MapConnection *connection, QString newMapName, + QUndoCommand *parent = nullptr); + + void undo() override; + void redo() override; + + int id() const override { return CommandId::ID_MapConnectionChangeMap; } + +private: + QPointer connection; + QString newMapName; + QString oldMapName; + int oldOffset; + int newOffset; + bool mirrored; +}; + + + +/// Implements a command to commit adding a Map Connection to a map. +class MapConnectionAdd : public QUndoCommand { +public: + MapConnectionAdd(Map *map, MapConnection *connection, + QUndoCommand *parent = nullptr); + + void undo() override; + void redo() override; + + int id() const override { return CommandId::ID_MapConnectionAdd; } + +private: + Map *map = nullptr; + Map *mirrorMap = nullptr; + QPointer connection = nullptr; + QPointer mirror = nullptr; +}; + + + +/// Implements a command to commit removing a Map Connection from a map. +class MapConnectionRemove : public QUndoCommand { +public: + MapConnectionRemove(Map *map, MapConnection *connection, + QUndoCommand *parent = nullptr); + + void undo() override; + void redo() override; + + int id() const override { return CommandId::ID_MapConnectionRemove; } + +private: + Map *map = nullptr; + Map *mirrorMap = nullptr; + QPointer connection = nullptr; + QPointer mirror = nullptr; +}; + + #endif // EDITCOMMANDS_H diff --git a/include/core/map.h b/include/core/map.h index 9f039a6f..27020236 100644 --- a/include/core/map.h +++ b/include/core/map.h @@ -99,10 +99,12 @@ public: void addEvent(Event *); void deleteConnections(); QList getConnections() const; - void removeConnection(MapConnection *); - void addConnection(MapConnection *); + bool takeConnection(MapConnection *); + bool removeConnection(MapConnection *); + bool addConnection(MapConnection *); void loadConnection(MapConnection *); - QPixmap renderConnection(const QString &, MapLayout *); + QRect getConnectionRect(const QString &direction, MapLayout *fromLayout = nullptr); + QPixmap renderConnection(const QString &direction, MapLayout *fromLayout = nullptr); QPixmap renderBorder(bool ignoreCache = false); void setDimensions(int newWidth, int newHeight, bool setNewBlockdata = true, bool enableScriptCallback = false); void setBorderDimensions(int newWidth, int newHeight, bool setNewBlockdata = true, bool enableScriptCallback = false); @@ -126,12 +128,15 @@ public: QUndoStack editHistory; void modify(); void clean(); + void pruneEditHistory(); private: void setNewDimensionsBlockdata(int newWidth, int newHeight); void setNewBorderDimensionsBlockdata(int newWidth, int newHeight); + // MapConnections in 'ownedConnections' but not 'connections' persist in the edit history. QList connections; + QSet ownedConnections; signals: void modified(); diff --git a/include/core/mapconnection.h b/include/core/mapconnection.h index 956574dc..6ffa85b7 100644 --- a/include/core/mapconnection.h +++ b/include/core/mapconnection.h @@ -29,7 +29,6 @@ public: int offset() const { return m_offset; } void setOffset(int offset, bool mirror = true); - bool isMirror(const MapConnection*); MapConnection* findMirror(); MapConnection* createMirror(); @@ -43,6 +42,7 @@ public: static bool isVertical(const QString &direction); static bool isDiving(const QString &direction); static QString oppositeDirection(const QString &direction) { return oppositeDirections.value(direction, direction); } + static bool areMirrored(const MapConnection*, const MapConnection*); private: Map* m_parentMap; diff --git a/include/editor.h b/include/editor.h index 46432347..3b2c9ae6 100644 --- a/include/editor.h +++ b/include/editor.h @@ -79,6 +79,7 @@ public: void setConnectionsVisibility(bool visible); void updateDivingMapsVisibility(); void displayDivingConnections(); + void renderDivingConnections(); void addConnection(MapConnection* connection); void removeConnection(MapConnection* connection); void removeSelectedConnection(); @@ -111,7 +112,6 @@ public: QPointer scene = nullptr; QGraphicsPixmapItem *current_view = nullptr; QPointer map_item = nullptr; - QPointer selected_connection_item = nullptr; QList> connection_items; QMap> diving_map_items; QGraphicsPathItem *connection_mask = nullptr; @@ -134,6 +134,8 @@ public: QPointer movement_permissions_selector_item = nullptr; QList *selected_events = nullptr; + QPointer selected_connection_item = nullptr; + QPointer connection_to_select = nullptr; QString map_edit_mode = "paint"; QString obj_edit_mode = "select"; @@ -183,10 +185,9 @@ private: void clearMapGrid(); void clearWildMonTables(); void updateBorderVisibility(); - QPoint calculateConnectionPosition(MapConnection *connection, const QPixmap &pixmap); + QPoint getConnectionOrigin(MapConnection *connection); void removeConnectionPixmap(MapConnection* connection); void updateConnectionPixmap(ConnectionPixmapItem* connectionItem); - void updateConnectionPixmapPos(ConnectionPixmapItem* connectionItem); void displayConnection(MapConnection* connection); void displayDivingConnection(MapConnection* connection); void setDivingMapName(QString mapName, QString direction); diff --git a/include/ui/connectionpixmapitem.h b/include/ui/connectionpixmapitem.h index 969fa6c1..62eda6fe 100644 --- a/include/ui/connectionpixmapitem.h +++ b/include/ui/connectionpixmapitem.h @@ -9,36 +9,37 @@ class ConnectionPixmapItem : public QObject, public QGraphicsPixmapItem { Q_OBJECT public: - ConnectionPixmapItem(QPixmap pixmap, MapConnection* connection, int x, int y) - : QGraphicsPixmapItem(pixmap) - { - this->basePixmap = pixmap; - this->connection = connection; - setFlag(ItemIsMovable); - setFlag(ItemSendsGeometryChanges); - this->initialX = x; - this->initialY = y; - this->initialOffset = connection->offset(); - this->setPos(x, y); - } - QPixmap basePixmap; - QPointer connection; - int initialX; - int initialY; - int initialOffset; + ConnectionPixmapItem(MapConnection* connection, int originX, int originY); + ConnectionPixmapItem(MapConnection* connection, QPoint origin); + + const QPointer connection; + + void setOrigin(int x, int y); + void setOrigin(QPoint pos); void setEditable(bool editable); bool getEditable(); + void setSelected(bool selected); - void render(); + + void updatePos(); + void render(bool ignoreCache = false); private: + QPixmap basePixmap; + qreal originX; + qreal originY; bool selected = false; + unsigned actionId = 0; + + static const int mWidth = 16; + static const int mHeight = 16; protected: - QVariant itemChange(GraphicsItemChange change, const QVariant &value); - void mousePressEvent(QGraphicsSceneMouseEvent*); - void mouseDoubleClickEvent(QGraphicsSceneMouseEvent*); + QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; + void mousePressEvent(QGraphicsSceneMouseEvent*) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent*) override; + void mouseDoubleClickEvent(QGraphicsSceneMouseEvent*) override; signals: void connectionItemDoubleClicked(MapConnection*); diff --git a/include/ui/connectionslistitem.h b/include/ui/connectionslistitem.h index 9cac5689..87051c37 100644 --- a/include/ui/connectionslistitem.h +++ b/include/ui/connectionslistitem.h @@ -2,6 +2,7 @@ #define CONNECTIONSLISTITEM_H #include "mapconnection.h" +#include "map.h" #include #include @@ -30,11 +31,13 @@ public: private: Ui::ConnectionsListItem *ui; + Map *map; bool isSelected = false; + unsigned actionId = 0; protected: - void mousePressEvent(QMouseEvent *); - void mouseDoubleClickEvent(QMouseEvent *); + void mousePressEvent(QMouseEvent*) override; + void mouseDoubleClickEvent(QMouseEvent*) override; signals: void removed(MapConnection*); diff --git a/include/ui/divingmappixmapitem.h b/include/ui/divingmappixmapitem.h index 7596aeaf..7eceba07 100644 --- a/include/ui/divingmappixmapitem.h +++ b/include/ui/divingmappixmapitem.h @@ -5,30 +5,26 @@ #include #include +#include class DivingMapPixmapItem : public QObject, public QGraphicsPixmapItem { Q_OBJECT public: - DivingMapPixmapItem(MapConnection* connection) - : QGraphicsPixmapItem(getBasePixmap(connection)) - { - m_connection = connection; + DivingMapPixmapItem(MapConnection *connection, QComboBox *combo); + ~DivingMapPixmapItem(); - // Update pixmap if the connected map is swapped. - connect(m_connection, &MapConnection::targetMapNameChanged, this, &DivingMapPixmapItem::updatePixmap); - } MapConnection* connection() const { return m_connection; } + void updatePixmap(); private: QPointer m_connection; + QPointer m_combo; - static QPixmap getBasePixmap(MapConnection* connection) { - // If the map is connected to itself then rendering is pointless. - if (!connection || connection->targetMapName() == connection->parentMapName()) - return QPixmap(); - return connection->getPixmap(); - } - void updatePixmap() { setPixmap(getBasePixmap(m_connection)); } + void setComboText(const QString &text); + static QPixmap getBasePixmap(MapConnection* connection); + +private slots: + void onTargetMapChanged(); }; #endif // DIVINGMAPPIXMAPITEM_H diff --git a/include/ui/newmapconnectiondialog.h b/include/ui/newmapconnectiondialog.h index 3b37c454..2e0ba28e 100644 --- a/include/ui/newmapconnectiondialog.h +++ b/include/ui/newmapconnectiondialog.h @@ -17,10 +17,11 @@ public: explicit NewMapConnectionDialog(QWidget *parent, Map* map, const QStringList &mapNames); ~NewMapConnectionDialog(); - MapConnection *result; - virtual void accept() override; +signals: + void accepted(MapConnection *result); + private: Ui::NewMapConnectionDialog *ui; }; diff --git a/porymap.pro b/porymap.pro index e5cad6dd..744edaef 100644 --- a/porymap.pro +++ b/porymap.pro @@ -50,6 +50,7 @@ SOURCES += src/core/block.cpp \ src/ui/connectionslistitem.cpp \ src/ui/customscriptseditor.cpp \ src/ui/customscriptslistitem.cpp \ + src/ui/divingmappixmapitem.cpp \ src/ui/draggablepixmapitem.cpp \ src/ui/bordermetatilespixmapitem.cpp \ src/ui/collisionpixmapitem.cpp \ diff --git a/src/core/editcommands.cpp b/src/core/editcommands.cpp index 394c51cb..640cb43f 100644 --- a/src/core/editcommands.cpp +++ b/src/core/editcommands.cpp @@ -569,3 +569,205 @@ void ScriptEditMap::undo() { QUndoCommand::undo(); } + +/****************************************************************************** + ************************************************************************ + ******************************************************************************/ + +MapConnectionMove::MapConnectionMove(MapConnection *connection, int newOffset, unsigned actionId, + QUndoCommand *parent) : QUndoCommand(parent) { + setText("Move Map Connection"); + + this->connection = connection; + this->oldOffset = connection->offset(); + this->newOffset = newOffset; + + this->mirrored = porymapConfig.mirrorConnectingMaps; + + this->actionId = actionId; +} + +void MapConnectionMove::redo() { + QUndoCommand::redo(); + if (this->connection) + this->connection->setOffset(this->newOffset, this->mirrored); +} + +void MapConnectionMove::undo() { + if (this->connection) + this->connection->setOffset(this->oldOffset, this->mirrored); + QUndoCommand::undo(); +} + +bool MapConnectionMove::mergeWith(const QUndoCommand *command) { + if (this->id() != command->id()) + return false; + + const MapConnectionMove *other = static_cast(command); + if (this->connection != other->connection) + return false; + if (this->actionId != other->actionId) + return false; + + this->newOffset = other->newOffset; + return true; +} + +/****************************************************************************** + ************************************************************************ + ******************************************************************************/ + +MapConnectionChangeDirection::MapConnectionChangeDirection(MapConnection *connection, QString newDirection, + QUndoCommand *parent) : QUndoCommand(parent) { + setText("Change Map Connection Direction"); + + this->connection = connection; + + this->oldDirection = connection->direction(); + this->newDirection = newDirection; + + this->oldOffset = connection->offset(); + + // If the direction changes between vertical/horizontal then the old offset may not make sense, so we reset it. + if (MapConnection::isHorizontal(this->oldDirection) != MapConnection::isHorizontal(this->newDirection) + || MapConnection::isVertical(this->oldDirection) != MapConnection::isVertical(this->newDirection)) { + this->newOffset = 0; + } else { + this->newOffset = oldOffset; + } + + this->mirrored = porymapConfig.mirrorConnectingMaps; +} + +void MapConnectionChangeDirection::redo() { + QUndoCommand::redo(); + if (this->connection) { + this->connection->setDirection(this->newDirection, this->mirrored); + this->connection->setOffset(this->newOffset, this->mirrored); + } +} + +void MapConnectionChangeDirection::undo() { + if (this->connection) { + this->connection->setDirection(this->oldDirection, this->mirrored); + this->connection->setOffset(this->oldOffset, this->mirrored); + } + QUndoCommand::undo(); +} + +/****************************************************************************** + ************************************************************************ + ******************************************************************************/ + +MapConnectionChangeMap::MapConnectionChangeMap(MapConnection *connection, QString newMapName, + QUndoCommand *parent) : QUndoCommand(parent) { + setText("Change Map Connection Map"); + + this->connection = connection; + + this->oldMapName = connection->targetMapName(); + this->newMapName = newMapName; + + this->oldOffset = connection->offset(); + this->newOffset = 0; // The old offset may not make sense, so we reset it + + this->mirrored = porymapConfig.mirrorConnectingMaps; +} + +void MapConnectionChangeMap::redo() { + QUndoCommand::redo(); + if (this->connection) { + this->connection->setTargetMapName(this->newMapName, this->mirrored); + this->connection->setOffset(this->newOffset, this->mirrored); + } +} + +void MapConnectionChangeMap::undo() { + if (this->connection) { + this->connection->setTargetMapName(this->oldMapName, this->mirrored); + this->connection->setOffset(this->oldOffset, this->mirrored); + } + QUndoCommand::undo(); +} + +/****************************************************************************** + ************************************************************************ + ******************************************************************************/ + +MapConnectionAdd::MapConnectionAdd(Map *map, MapConnection *connection, + QUndoCommand *parent) : QUndoCommand(parent) { + setText("Add Map Connection"); + + this->map = map; + this->connection = connection; + + // Set this now because it's needed to create a mirror below. + // It would otherwise be set by Map::addConnection. + this->connection->setParentMap(this->map, false); + + if (porymapConfig.mirrorConnectingMaps) { + this->mirror = this->connection->createMirror(); + this->mirrorMap = this->connection->targetMap(); + } +} + +void MapConnectionAdd::redo() { + QUndoCommand::redo(); + + this->map->addConnection(this->connection); + if (this->mirrorMap) + this->mirrorMap->addConnection(this->mirror); +} + +void MapConnectionAdd::undo() { + if (this->mirrorMap) { + // We can't guarantee that the mirror we created earlier is still our connection's + // mirror because there is no strict source->mirror pairing for map connections + // (a different identical map connection can take its place during any mirrored change). + if (!MapConnection::areMirrored(this->connection, this->mirror)) + this->mirror = this->connection->findMirror(); + + this->mirrorMap->removeConnection(this->mirror); + } + this->map->removeConnection(this->connection); + + QUndoCommand::undo(); +} + +/****************************************************************************** + ************************************************************************ + ******************************************************************************/ + +MapConnectionRemove::MapConnectionRemove(Map *map, MapConnection *connection, + QUndoCommand *parent) : QUndoCommand(parent) { + setText("Remove Map Connection"); + + this->map = map; + this->connection = connection; + + if (porymapConfig.mirrorConnectingMaps) { + this->mirror = this->connection->findMirror(); + this->mirrorMap = this->connection->targetMap(); + } +} + +void MapConnectionRemove::redo() { + QUndoCommand::redo(); + + if (this->mirrorMap) { + // See comment in MapConnectionAdd::undo + if (!MapConnection::areMirrored(this->connection, this->mirror)) + this->mirror = this->connection->findMirror(); + + this->mirrorMap->removeConnection(this->mirror); + } + this->map->removeConnection(this->connection); +} + +void MapConnectionRemove::undo() { + this->map->addConnection(this->connection); + if (this->mirrorMap) + this->mirrorMap->addConnection(this->mirror); + + QUndoCommand::undo(); +} diff --git a/src/core/map.cpp b/src/core/map.cpp index ed011ec5..91e196fe 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -215,11 +215,13 @@ QPixmap Map::renderBorder(bool ignoreCache) { return layout->border_pixmap; } -QPixmap Map::renderConnection(const QString &direction, MapLayout * fromLayout) { - // Cardinal connections are rendered within the bounds of the border draw distance, - // and we need to render the nearest segment of the map. - // Dive/Emerge maps are rendered normally, but within the bounds of their parent map. - int x = 0, y = 0, w = getWidth(), h = getHeight(); +// Get the portion of the map that can be rendered when rendered as a map connection. +// Cardinal connections render the nearest segment of their map and within the bounds of the border draw distance, +// Dive/Emerge connections are rendered normally within the bounds of their parent map. +QRect Map::getConnectionRect(const QString &direction, MapLayout * fromLayout) { + int x = 0, y = 0; + int w = getWidth(), h = getHeight(); + if (direction == "up") { h = qMin(h, BORDER_DISTANCE); y = getHeight() - h; @@ -234,15 +236,26 @@ QPixmap Map::renderConnection(const QString &direction, MapLayout * fromLayout) if (fromLayout) { w = qMin(w, fromLayout->getWidth()); h = qMin(h, fromLayout->getHeight()); - fromLayout = nullptr; // This would be used for palettes later, but we want our own palettes, not the parent's. } } else { // Unknown direction - return QPixmap(); + return QRect(); } + return QRect(x, y, w, h); +} - render(true, fromLayout, QRect(x, y, w, h)); - QImage connection_image = image.copy(x * 16, y * 16, w * 16, h * 16); +QPixmap Map::renderConnection(const QString &direction, MapLayout * fromLayout) { + QRect bounds = getConnectionRect(direction, fromLayout); + if (!bounds.isValid()) + return QPixmap(); + + // 'fromLayout' will be used in 'render' to get the palettes from the parent map. + // Dive/Emerge connections render normally with their own palettes, so we ignore this. + if (MapConnection::isDiving(direction)) + fromLayout = nullptr; + + render(true, fromLayout, bounds); + QImage connection_image = image.copy(bounds.x() * 16, bounds.y() * 16, bounds.width() * 16, bounds.height() * 16); return QPixmap::fromImage(connection_image); } @@ -518,54 +531,62 @@ void Map::addEvent(Event *event) { } void Map::deleteConnections() { - qDeleteAll(connections); - connections.clear(); + qDeleteAll(this->ownedConnections); + this->ownedConnections.clear(); + this->connections.clear(); } QList Map::getConnections() const { return connections; } -void Map::addConnection(MapConnection *connection) { - if (!connection || connections.contains(connection)) - return; +bool Map::addConnection(MapConnection *connection) { + if (!connection || this->connections.contains(connection)) + return false; - // Adding new Dive/Emerge maps replaces an existing one. + // Maps should only have one Dive/Emerge connection at a time. + // (Users can technically have more by editing their data manually, but we will only display one at a time) + // Any additional connections being added (this can happen via mirroring) are tracked for deleting but otherwise ignored. if (MapConnection::isDiving(connection->direction())) { - for (auto connectionToReplace : connections) { - if (connectionToReplace->direction() != connection->direction()) - continue; - if (porymapConfig.mirrorConnectingMaps) { - auto mirror = connectionToReplace->findMirror(); - if (mirror && mirror->parentMap()) - mirror->parentMap()->removeConnection(mirror); - delete mirror; + for (auto i : this->connections) { + if (i->direction() == connection->direction()) { + this->ownedConnections.insert(connection); + connection->setParentMap(this, false); + return true; } - removeConnection(connectionToReplace); - delete connectionToReplace; - break; } } loadConnection(connection); modify(); emit connectionAdded(connection); + return true; } void Map::loadConnection(MapConnection *connection) { if (!connection) return; - connections.append(connection); + this->connections.append(connection); + this->ownedConnections.insert(connection); connection->setParentMap(this, false); } -// Caller takes ownership of connection -void Map::removeConnection(MapConnection *connection) { - if (!connections.removeOne(connection)) - return; - connection->setParentMap(nullptr, false); +// connection should not be deleted here, a pointer to it is allowed to persist in the edit history +bool Map::removeConnection(MapConnection *connection) { + if (!this->connections.removeOne(connection)) + return false; modify(); emit connectionRemoved(connection); + return true; +} + +// Same as Map::removeConnection but caller takes ownership of connection. +bool Map::takeConnection(MapConnection *connection) { + if (!this->removeConnection(connection)) + return false; + connection->setParentMap(nullptr, false); + this->ownedConnections.remove(connection); + return true; } void Map::modify() { @@ -580,6 +601,27 @@ bool Map::hasUnsavedChanges() { return !editHistory.isClean() || hasUnsavedDataChanges || !isPersistedToFile; } +void Map::pruneEditHistory() { + // Edit history for map connections gets messy because edits on other maps can affect the current map. + // To avoid complications we clear MapConnection edit history when the user opens a different map. + // No other edits within a single map depend on MapConnections so they can be pruned safely. + static const QSet mapConnectionIds = { + ID_MapConnectionMove, + ID_MapConnectionChangeDirection, + ID_MapConnectionChangeMap, + ID_MapConnectionAdd, + ID_MapConnectionRemove + }; + for (int i = 0; i < this->editHistory.count(); i++) { + // Qt really doesn't expect editing commands in the stack to be valid (fair). + // A better future design might be to have separate edit histories per map tab, + // and dumping the entire Connections tab history with QUndoStack::clear. + auto command = const_cast(this->editHistory.command(i)); + if (mapConnectionIds.contains(command->id())) + command->setObsolete(true); + } +} + bool Map::isWithinBounds(int x, int y) { return (x >= 0 && x < this->getWidth() && y >= 0 && y < this->getHeight()); } diff --git a/src/core/mapconnection.cpp b/src/core/mapconnection.cpp index 5fe2e730..022158a0 100644 --- a/src/core/mapconnection.cpp +++ b/src/core/mapconnection.cpp @@ -16,14 +16,14 @@ MapConnection::MapConnection(const QString &targetMapName, const QString &direct m_offset = offset; } -bool MapConnection::isMirror(const MapConnection* other) { - if (!other || !other->m_parentMap) +bool MapConnection::areMirrored(const MapConnection* a, const MapConnection* b) { + if (!a || !b || !a->m_parentMap || !b->m_parentMap) return false; - return parentMapName() == other->m_targetMapName - && m_targetMapName == other->parentMapName() - && m_offset == -other->m_offset - && m_direction == oppositeDirection(other->m_direction); + return a->parentMapName() == b->m_targetMapName + && a->m_targetMapName == b->parentMapName() + && a->m_offset == -b->m_offset + && a->m_direction == oppositeDirection(b->m_direction); } MapConnection* MapConnection::findMirror() { @@ -35,7 +35,7 @@ MapConnection* MapConnection::findMirror() { // Note: There is no strict source -> mirror pairing, i.e. we are not guaranteed // to always get the same MapConnection if there are multiple identical copies. for (auto connection : map->getConnections()) { - if (this != connection && this->isMirror(connection)) + if (this != connection && areMirrored(this, connection)) return connection; } return nullptr; @@ -72,14 +72,14 @@ void MapConnection::setParentMap(Map* map, bool mirror) { if (map == m_parentMap) return; - if (mirror && porymapConfig.mirrorConnectingMaps) { + if (mirror) { auto connection = findMirror(); if (connection) connection->setTargetMapName(map ? map->name : QString(), false); } if (m_parentMap) - m_parentMap->removeConnection(this); + m_parentMap->takeConnection(this); auto before = m_parentMap; m_parentMap = map; @@ -98,7 +98,7 @@ void MapConnection::setTargetMapName(const QString &targetMapName, bool mirror) if (targetMapName == m_targetMapName) return; - if (mirror && porymapConfig.mirrorConnectingMaps) { + if (mirror) { auto connection = findMirror(); if (connection) connection->setParentMap(getMap(targetMapName), false); @@ -114,7 +114,7 @@ void MapConnection::setDirection(const QString &direction, bool mirror) { if (direction == m_direction) return; - if (mirror && porymapConfig.mirrorConnectingMaps) { + if (mirror) { auto connection = findMirror(); if (connection) connection->setDirection(oppositeDirection(direction), false); @@ -130,7 +130,7 @@ void MapConnection::setOffset(int offset, bool mirror) { if (offset == m_offset) return; - if (mirror && porymapConfig.mirrorConnectingMaps) { + if (mirror) { auto connection = findMirror(); if (connection) connection->setOffset(-offset, false); diff --git a/src/editor.cpp b/src/editor.cpp index e2093665..c7acdea0 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -743,9 +743,7 @@ void Editor::displayConnection(MapConnection* connection) { } // Create connection image - QPixmap pixmap = connection->getPixmap(); - QPoint pos = calculateConnectionPosition(connection, pixmap); - ConnectionPixmapItem *pixmapItem = new ConnectionPixmapItem(pixmap, connection, pos.x(), pos.y()); + ConnectionPixmapItem *pixmapItem = new ConnectionPixmapItem(connection, getConnectionOrigin(connection)); pixmapItem->render(); scene->addItem(pixmapItem); maskNonVisibleConnectionTiles(); @@ -756,82 +754,70 @@ void Editor::displayConnection(MapConnection* connection) { // It's possible for a map connection to be displayed repeatedly if it's being // updated by mirroring and the target map is changing to/from the current map. - connection->disconnect(); + QObject::disconnect(connection, &MapConnection::targetMapNameChanged, nullptr, nullptr); + QObject::disconnect(connection, &MapConnection::directionChanged, nullptr, nullptr); + QObject::disconnect(connection, &MapConnection::offsetChanged, nullptr, nullptr); // Double clicking the list item or pixmap opens the connected map connect(listItem, &ConnectionsListItem::doubleClicked, this, &Editor::onMapConnectionDoubleClicked); connect(pixmapItem, &ConnectionPixmapItem::connectionItemDoubleClicked, this, &Editor::onMapConnectionDoubleClicked); // Sync the selection highlight between the list UI and the pixmap - connect(pixmapItem, &ConnectionPixmapItem::selectionChanged, [this, listItem, pixmapItem](bool selected) { + connect(pixmapItem, &ConnectionPixmapItem::selectionChanged, [=](bool selected) { listItem->setSelected(selected); if (selected) setSelectedConnectionItem(pixmapItem); }); - connect(listItem, &ConnectionsListItem::selected, [this, pixmapItem] { + connect(listItem, &ConnectionsListItem::selected, [=] { setSelectedConnectionItem(pixmapItem); }); // Sync edits to 'offset' between the list UI and the pixmap - connect(connection, &MapConnection::offsetChanged, [this, listItem, pixmapItem](int, int) { + connect(connection, &MapConnection::offsetChanged, [=](int, int) { listItem->updateUI(); - updateConnectionPixmapPos(pixmapItem); + pixmapItem->updatePos(); + maskNonVisibleConnectionTiles(); }); // Sync edits to 'direction' between the list UI and the pixmap - connect(connection, &MapConnection::directionChanged, [this, listItem, pixmapItem](QString before, QString after) { - if (MapConnection::isHorizontal(before) != MapConnection::isHorizontal(after) - || MapConnection::isVertical(before) != MapConnection::isVertical(after)) { - // If the direction has changed between vertical/horizontal then the old offset may not make sense, so we reset it - pixmapItem->connection->setOffset(0); - } + connect(connection, &MapConnection::directionChanged, [=](QString, QString) { listItem->updateUI(); updateConnectionPixmap(pixmapItem); }); // Sync edits to 'map' between the list UI and the pixmap - connect(connection, &MapConnection::targetMapNameChanged, [this, listItem, pixmapItem](QString, QString) { - // The old offset may not make sense, so we reset it - pixmapItem->connection->setOffset(0); + connect(connection, &MapConnection::targetMapNameChanged, [=](QString, QString) { listItem->updateUI(); updateConnectionPixmap(pixmapItem); }); - // User manually deleting connection via the remove button - connect(listItem, &ConnectionsListItem::removed, [this](MapConnection* connection) { removeConnection(connection); }); - // When the pixmap is deleted, remove its associated list item connect(pixmapItem, &ConnectionPixmapItem::destroyed, listItem, &ConnectionsListItem::deleteLater); connection_items.append(pixmapItem); + + // If this was a recent addition from the user we should select it. + // We intentionally exclude connections added programmatically, e.g. by mirroring. + if (connection_to_select == connection) { + connection_to_select = nullptr; + setSelectedConnectionItem(pixmapItem); + } } -void Editor::addConnection(MapConnection * connection) { +void Editor::addConnection(MapConnection* connection) { if (!connection) return; - this->map->addConnection(connection); - if (porymapConfig.mirrorConnectingMaps) - connection->createMirror(); + // Mark this connection to be selected once its display elements have been created. + // It's possible this is a Dive/Emerge connection, but that's ok (no selection will occur). + connection_to_select = connection; - // TODO: Edit history + this->map->editHistory.push(new MapConnectionAdd(this->map, connection)); } void Editor::removeConnection(MapConnection* connection) { if (!connection) return; - - if (porymapConfig.mirrorConnectingMaps) { - auto mirror = connection->findMirror(); - if (mirror && mirror->parentMap()) - mirror->parentMap()->removeConnection(mirror); - delete mirror; - } - - if (connection->parentMap()) - connection->parentMap()->removeConnection(connection); - delete connection; - - // TODO: Edit history + this->map->editHistory.push(new MapConnectionRemove(this->map, connection)); } void Editor::removeSelectedConnection() { @@ -887,10 +873,6 @@ void Editor::displayDivingConnection(MapConnection* connection) { if (!MapConnection::isDiving(direction)) return; - // Save some rendering time if it won't be displayed - if (!porymapConfig.showDiveEmergeMaps) - return; - // Note: We only support editing 1 Dive and Emerge connection per map. // In a vanilla game only the first Dive/Emerge connection is considered, so allowing // users to have multiple is likely to lead to confusion. In case users have changed @@ -898,15 +880,18 @@ void Editor::displayDivingConnection(MapConnection* connection) { if (diving_map_items.value(direction)) return; - // Create map image - auto item = new DivingMapPixmapItem(connection); + // Create map display + auto comboBox = (direction == "dive") ? ui->comboBox_DiveMap : ui->comboBox_EmergeMap; + auto item = new DivingMapPixmapItem(connection, comboBox); scene->addItem(item); diving_map_items.insert(direction, item); - // Display map name in combo box - auto comboBox = (direction == "dive") ? ui->comboBox_DiveMap : ui->comboBox_EmergeMap; - const QSignalBlocker blocker(comboBox); - comboBox->setCurrentText(connection->targetMapName()); + updateDivingMapsVisibility(); +} + +void Editor::renderDivingConnections() { + for (auto item : diving_map_items.values()) + item->updatePixmap(); } void Editor::removeDivingMapPixmap(MapConnection *connection) { @@ -917,7 +902,7 @@ void Editor::removeDivingMapPixmap(MapConnection *connection) { if (!diving_map_items.contains(direction)) return; - // If the diving map being displayed is different than the one being removed we don't need to do anything. + // If the diving map being removed is different than the one that's currently displayed we don't need to do anything. if (diving_map_items.value(direction)->connection() != connection) return; @@ -927,11 +912,6 @@ void Editor::removeDivingMapPixmap(MapConnection *connection) { pixmapItem->scene()->removeItem(pixmapItem); delete pixmapItem; - // Clear map name from combo box - auto comboBox = (direction == "dive") ? ui->comboBox_DiveMap : ui->comboBox_EmergeMap; - const QSignalBlocker blocker(comboBox); - comboBox->setCurrentText(""); - // Reveal any previously-hidden connection (because we only ever display one diving map of each type). // Note: When this occurs as a result of the user clicking the 'X' clear button it seems the QComboBox // doesn't expect the line edit to be immediately repopulated, and the 'X' doesn't reappear. @@ -942,6 +922,7 @@ void Editor::removeDivingMapPixmap(MapConnection *connection) { break; } } + updateDivingMapsVisibility(); } void Editor::updateDiveMap(QString mapName) { @@ -953,27 +934,24 @@ void Editor::updateEmergeMap(QString mapName) { } void Editor::setDivingMapName(QString mapName, QString direction) { - // Only the first Dive/Emerge map (if present) is considered, as in-game. - MapConnection* connection = nullptr; - for (auto i : map->getConnections()) { - if (i->direction() == direction) { - connection = i; - break; - } - } + auto pixmapItem = diving_map_items.value(direction); + MapConnection *connection = pixmapItem ? pixmapItem->connection() : nullptr; + // TODO: Test edit history if (connection) { + if (mapName == connection->targetMapName()) + return; // No change + // Update existing connection if (mapName.isEmpty()) { removeConnection(connection); } else { - connection->setTargetMapName(mapName); + map->editHistory.push(new MapConnectionChangeMap(connection, mapName)); } } else if (!mapName.isEmpty()) { // Create new connection addConnection(new MapConnection(mapName, direction)); } - updateDivingMapsVisibility(); } void Editor::updateDivingMapsVisibility() { @@ -994,58 +972,36 @@ void Editor::updateDivingMapsVisibility() { } } -QPoint Editor::calculateConnectionPosition(MapConnection *connection, const QPixmap &pixmap) { +// Get the 'origin' point for the connection's pixmap, i.e. where it should be positioned in the editor when connection->offset() == 0. +// This differs depending on the connection's direction and the dimensions of its target map or parent map. +QPoint Editor::getConnectionOrigin(MapConnection *connection) { + if (!connection) + return QPoint(0, 0); + + Map *parentMap = connection->parentMap(); + Map *targetMap = connection->targetMap(); const QString direction = connection->direction(); - int offset = connection->offset(); int x = 0, y = 0; - const int mWidth = 16, mHeight = 16; - if (direction == "up") { - x = offset * mWidth; - y = -pixmap.height(); + + if (direction == "right") { + if (parentMap) x = parentMap->getWidth(); } else if (direction == "down") { - x = offset * mWidth; - y = map->getHeight() * mHeight; + if (parentMap) y = parentMap->getHeight(); } else if (direction == "left") { - x = -pixmap.width(); - y = offset * mHeight; - } else if (direction == "right") { - x = map->getWidth() * mWidth; - y = offset * mHeight; + if (targetMap) x = -targetMap->getConnectionRect(direction).width(); + } else if (direction == "up") { + if (targetMap) y = -targetMap->getConnectionRect(direction).height(); } - return QPoint(x, y); + return QPoint(x * 16, y * 16); } void Editor::updateConnectionPixmap(ConnectionPixmapItem* pixmapItem) { - if (!pixmapItem || !pixmapItem->connection) + if (!pixmapItem) return; - pixmapItem->initialOffset = pixmapItem->connection->offset(); - pixmapItem->basePixmap = pixmapItem->connection->getPixmap(); - QPoint pos = calculateConnectionPosition(pixmapItem->connection, pixmapItem->basePixmap); + pixmapItem->setOrigin(getConnectionOrigin(pixmapItem->connection)); + pixmapItem->render(true); // Full render to reflect map changes - const QSignalBlocker blocker(pixmapItem); - pixmapItem->setPixmap(pixmapItem->basePixmap); - pixmapItem->initialX = pos.x(); - pixmapItem->initialY = pos.y(); - pixmapItem->setPos(pos); - pixmapItem->render(); - - maskNonVisibleConnectionTiles(); -} - -void Editor::updateConnectionPixmapPos(ConnectionPixmapItem* pixmapItem) { - if (!pixmapItem || !pixmapItem->connection) - return; - - const QSignalBlocker blocker(pixmapItem); - MapConnection *connection = pixmapItem->connection; - if (MapConnection::isVertical(connection->direction())) { - qreal x = pixmapItem->initialX + (connection->offset() - pixmapItem->initialOffset) * 16; - if (x != pixmapItem->x()) pixmapItem->setX(x); - } else if (MapConnection::isHorizontal(connection->direction())) { - qreal y = pixmapItem->initialY + (connection->offset() - pixmapItem->initialOffset) * 16; - if (y != pixmapItem->y()) pixmapItem->setY(y); - } maskNonVisibleConnectionTiles(); } @@ -1257,9 +1213,14 @@ bool Editor::setMap(QString map_name) { // disconnect previous map's signals so they are not firing // multiple times if set again in the future if (map) { + map->pruneEditHistory(); map->disconnect(this); - for (auto connection : map->getConnections()) - connection->disconnect(); + for (auto connection : map->getConnections()) { + // Disconnect signals used by the display + QObject::disconnect(connection, &MapConnection::targetMapNameChanged, nullptr, nullptr); + QObject::disconnect(connection, &MapConnection::directionChanged, nullptr, nullptr); + QObject::disconnect(connection, &MapConnection::offsetChanged, nullptr, nullptr); + } } if (project) { @@ -1777,19 +1738,20 @@ void Editor::clearMapConnections() { } diving_map_items.clear(); + // Reset to single opacity slider + ui->stackedWidget_DiveMapOpacity->setCurrentIndex(1); + selected_connection_item = nullptr; } void Editor::displayMapConnections() { clearMapConnections(); - for (MapConnection *connection : map->getConnections()) + for (auto connection : map->getConnections()) displayConnection(connection); if (!connection_items.isEmpty()) setSelectedConnectionItem(connection_items.first()); - - updateDivingMapsVisibility(); } void Editor::clearConnectionMask() { @@ -1859,16 +1821,13 @@ void Editor::updateMapBorder() { } void Editor::updateMapConnections() { - for (auto item : connection_items) { - if (!item->connection) - continue; - item->basePixmap = item->connection->getPixmap(); - item->render(); - } + for (auto item : connection_items) + item->render(true); maskNonVisibleConnectionTiles(); } +// TODO: Check first condition, move to Map int Editor::getBorderDrawDistance(int dimension) { // Draw sufficient border blocks to fill the player's view (BORDER_DISTANCE) if (dimension >= BORDER_DISTANCE) { diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 0f1b83df..e95b3bb0 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2306,7 +2306,7 @@ void MainWindow::setDivingMapsVisible(bool visible) { if (visible) { // We skip rendering diving maps if this setting is not enabled, // so when we enable it we need to make sure they've rendered. - this->editor->displayDivingConnections(); + this->editor->renderDivingConnections(); } this->editor->updateDivingMapsVisibility(); } @@ -2662,14 +2662,12 @@ void MainWindow::showExportMapImageWindow(ImageExporterMode mode) { } void MainWindow::on_pushButton_AddConnection_clicked() { - if (!this->editor || !this->editor->map) + if (!this->editor || !this->editor->map || !this->editor->project) return; auto dialog = new NewMapConnectionDialog(this, this->editor->map, this->editor->project->mapNames); - if (dialog->exec() == QDialog::Accepted) { - this->editor->addConnection(dialog->result); - this->editor->setSelectedConnection(dialog->result); - } + connect(dialog, &NewMapConnectionDialog::accepted, this->editor, &Editor::addConnection); + dialog->exec(); } void MainWindow::on_pushButton_NewWildMonGroup_clicked() { diff --git a/src/ui/connectionpixmapitem.cpp b/src/ui/connectionpixmapitem.cpp index c98d3ba7..f8412012 100644 --- a/src/ui/connectionpixmapitem.cpp +++ b/src/ui/connectionpixmapitem.cpp @@ -1,8 +1,27 @@ #include "connectionpixmapitem.h" +#include "editcommands.h" +#include "map.h" #include -void ConnectionPixmapItem::render() { +ConnectionPixmapItem::ConnectionPixmapItem(MapConnection* connection, int x, int y) + : QGraphicsPixmapItem(connection->getPixmap()), + connection(connection) +{ + this->setEditable(true); + this->basePixmap = pixmap(); + this->setOrigin(x, y); +} + +ConnectionPixmapItem::ConnectionPixmapItem(MapConnection* connection, QPoint pos) + : ConnectionPixmapItem(connection, pos.x(), pos.y()) +{} + +// Render additional visual effects on top of the base map image. +void ConnectionPixmapItem::render(bool ignoreCache) { + if (ignoreCache) + this->basePixmap = this->connection->getPixmap(); + QPixmap pixmap = this->basePixmap.copy(0, 0, this->basePixmap.width(), this->basePixmap.height()); this->setZValue(-1); @@ -31,27 +50,23 @@ QVariant ConnectionPixmapItem::itemChange(GraphicsItemChange change, const QVari if (change == ItemPositionChange) { QPointF newPos = value.toPointF(); - qreal x, y; - int newOffset = this->initialOffset; + qreal x = this->originX; + qreal y = this->originY; + int newOffset = this->connection->offset(); + + // Restrict movement to the metatile grid and perpendicular to the connection direction. if (MapConnection::isVertical(this->connection->direction())) { - x = round(newPos.x() / 16) * 16; - newOffset += (x - initialX) / 16; - x = newOffset * 16; - } - else { - x = this->initialX; + x = (round(newPos.x() / this->mWidth) * this->mWidth) - this->originX; + newOffset = x / this->mWidth; + } else if (MapConnection::isHorizontal(this->connection->direction())) { + y = (round(newPos.y() / this->mHeight) * this->mHeight) - this->originY; + newOffset = y / this->mHeight; } - if (MapConnection::isHorizontal(this->connection->direction())) { - y = round(newPos.y() / 16) * 16; - newOffset += (y - this->initialY) / 16; - y = newOffset * 16; - } - else { - y = this->initialY; - } + // This is convoluted because of how our edit history works; this would otherwise just be 'this->connection->setOffset(newOffset);' + if (this->connection->parentMap() && newOffset != this->connection->offset()) + this->connection->parentMap()->editHistory.push(new MapConnectionMove(this->connection, newOffset, this->actionId)); - this->connection->setOffset(newOffset); return QPointF(x, y); } else { @@ -59,6 +74,32 @@ QVariant ConnectionPixmapItem::itemChange(GraphicsItemChange change, const QVari } } +// If connection->offset changed externally we call this to correct our position. +void ConnectionPixmapItem::updatePos() { + const QSignalBlocker blocker(this); + + qreal x = this->originX; + qreal y = this->originY; + + if (MapConnection::isVertical(this->connection->direction())) { + x += this->connection->offset() * this->mWidth; + } else if (MapConnection::isHorizontal(this->connection->direction())) { + y += this->connection->offset() * this->mHeight; + } + + this->setPos(x, y); +} + +// Set the pixmap's external origin point, i.e. the pixmap's position when connection->offset == 0 +void ConnectionPixmapItem::setOrigin(int x, int y) { + this->originX = x; + this->originY = y; + updatePos(); +} +void ConnectionPixmapItem::setOrigin(QPoint pos) { + this->setOrigin(pos.x(), pos.y()); +} + void ConnectionPixmapItem::setEditable(bool editable) { setFlag(ItemIsMovable, editable); setFlag(ItemSendsGeometryChanges, editable); @@ -82,6 +123,11 @@ void ConnectionPixmapItem::mousePressEvent(QGraphicsSceneMouseEvent *) { this->setSelected(true); } +void ConnectionPixmapItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { + this->actionId++; // Distinguish between move actions for the edit history + QGraphicsPixmapItem::mouseReleaseEvent(event); +} + void ConnectionPixmapItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *) { emit connectionItemDoubleClicked(this->connection); } diff --git a/src/ui/connectionslistitem.cpp b/src/ui/connectionslistitem.cpp index 049bb17d..eb7f94a6 100644 --- a/src/ui/connectionslistitem.cpp +++ b/src/ui/connectionslistitem.cpp @@ -1,5 +1,9 @@ #include "connectionslistitem.h" #include "ui_connectionslistitem.h" +#include "editcommands.h" +#include "map.h" + +#include ConnectionsListItem::ConnectionsListItem(QWidget *parent, MapConnection * connection, const QStringList &mapNames) : QFrame(parent), @@ -18,11 +22,23 @@ ConnectionsListItem::ConnectionsListItem(QWidget *parent, MapConnection * connec ui->comboBox_Map->setMinimumContentsLength(6); ui->comboBox_Map->addItems(mapNames); ui->comboBox_Map->setFocusedScrollingEnabled(false); // Scrolling could cause rapid changes to many different maps + ui->comboBox_Map->setInsertPolicy(QComboBox::NoInsert); ui->spinBox_Offset->setMinimum(INT_MIN); ui->spinBox_Offset->setMaximum(INT_MAX); + // Invalid map names are not considered a change. If editing finishes with an invalid name, restore the previous name. + connect(ui->comboBox_Map->lineEdit(), &QLineEdit::editingFinished, [this] { + const QSignalBlocker blocker(ui->comboBox_Map); + if (ui->comboBox_Map->findText(ui->comboBox_Map->currentText()) < 0) + ui->comboBox_Map->setTextItem(this->connection->targetMapName()); + }); + + // Distinguish between move actions for the edit history + connect(ui->spinBox_Offset, &QSpinBox::editingFinished, [this] { this->actionId++; }); + this->connection = connection; + this->map = connection->parentMap(); this->updateUI(); } @@ -32,6 +48,9 @@ ConnectionsListItem::~ConnectionsListItem() } void ConnectionsListItem::updateUI() { + if (!this->connection) + return; + const QSignalBlocker blocker1(ui->comboBox_Direction); const QSignalBlocker blocker2(ui->comboBox_Map); const QSignalBlocker blocker3(ui->spinBox_Offset); @@ -56,26 +75,30 @@ void ConnectionsListItem::mousePressEvent(QMouseEvent *) { this->setSelected(true); } +// TODO: This happens by accident a lot. Convert to button? void ConnectionsListItem::mouseDoubleClickEvent(QMouseEvent *) { emit doubleClicked(this->connection); } void ConnectionsListItem::on_comboBox_Direction_currentTextChanged(const QString &direction) { this->setSelected(true); - this->connection->setDirection(direction); + if (this->map) + this->map->editHistory.push(new MapConnectionChangeDirection(this->connection, direction)); } void ConnectionsListItem::on_comboBox_Map_currentTextChanged(const QString &mapName) { this->setSelected(true); - if (ui->comboBox_Map->findText(mapName) >= 0) - this->connection->setTargetMapName(mapName); + if (this->map && ui->comboBox_Map->findText(mapName) >= 0) + this->map->editHistory.push(new MapConnectionChangeMap(this->connection, mapName)); } void ConnectionsListItem::on_spinBox_Offset_valueChanged(int offset) { this->setSelected(true); - this->connection->setOffset(offset); + if (this->map) + this->map->editHistory.push(new MapConnectionMove(this->connection, offset, this->actionId)); } void ConnectionsListItem::on_button_Delete_clicked() { - emit this->removed(this->connection); + if (this->map) + this->map->editHistory.push(new MapConnectionRemove(this->map, this->connection)); } diff --git a/src/ui/divingmappixmapitem.cpp b/src/ui/divingmappixmapitem.cpp new file mode 100644 index 00000000..70a611fa --- /dev/null +++ b/src/ui/divingmappixmapitem.cpp @@ -0,0 +1,46 @@ +#include "divingmappixmapitem.h" +#include "config.h" + +DivingMapPixmapItem::DivingMapPixmapItem(MapConnection *connection, QComboBox *combo) + : QGraphicsPixmapItem(getBasePixmap(connection)) +{ + m_connection = connection; + m_combo = combo; + + setComboText(connection->targetMapName()); + + // Update display if the connected map is swapped. + connect(m_connection, &MapConnection::targetMapNameChanged, this, &DivingMapPixmapItem::onTargetMapChanged); +} + +DivingMapPixmapItem::~DivingMapPixmapItem() { + // Clear map name from combo box + setComboText(""); +} + +QPixmap DivingMapPixmapItem::getBasePixmap(MapConnection* connection) { + if (!connection) + return QPixmap(); + if (!porymapConfig.showDiveEmergeMaps) + return QPixmap(); // Save some rendering time if it won't be displayed + if (connection->targetMapName() == connection->parentMapName()) + return QPixmap(); // If the map is connected to itself then rendering is pointless. + return connection->getPixmap(); +} + +void DivingMapPixmapItem::updatePixmap() { + setPixmap(getBasePixmap(m_connection)); +} + +void DivingMapPixmapItem::onTargetMapChanged() { + updatePixmap(); + setComboText(m_connection->targetMapName()); +} + +void DivingMapPixmapItem::setComboText(const QString &text) { + if (!m_combo) + return; + + const QSignalBlocker blocker(m_combo); + m_combo->setCurrentText(text); +} diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index 03cbc2b8..b61d3738 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -192,6 +192,12 @@ bool MapImageExporter::historyItemAppliesToFrame(const QUndoCommand *command) { return this->showCollision; case CommandId::ID_PaintBorder: return this->showBorder; + case CommandId::ID_MapConnectionMove: + case CommandId::ID_MapConnectionChangeDirection: + case CommandId::ID_MapConnectionChangeMap: + case CommandId::ID_MapConnectionAdd: + case CommandId::ID_MapConnectionRemove: + return this->showUpConnections || this->showDownConnections || this->showLeftConnections || this->showRightConnections; case CommandId::ID_EventMove: case CommandId::ID_EventShift: case CommandId::ID_EventCreate: @@ -398,14 +404,15 @@ QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { if (!this->mode) { // if showing connections, draw on outside of image QPainter connectionPainter(&pixmap); + // TODO: Is this still the most sensible way to do this (esp. rendering pixmap) for (auto connectionItem : editor->connection_items) { const QString direction = connectionItem->connection->direction(); if ((showUpConnections && direction == "up") || (showDownConnections && direction == "down") || (showLeftConnections && direction == "left") || (showRightConnections && direction == "right")) - connectionPainter.drawImage(connectionItem->initialX + borderWidth, connectionItem->initialY + borderHeight, - connectionItem->basePixmap.toImage()); + connectionPainter.drawImage(connectionItem->x() + borderWidth, connectionItem->y() + borderHeight, + connectionItem->connection->getPixmap().toImage()); } connectionPainter.end(); } diff --git a/src/ui/newmapconnectiondialog.cpp b/src/ui/newmapconnectiondialog.cpp index aa7c05cf..254b4b6e 100644 --- a/src/ui/newmapconnectiondialog.cpp +++ b/src/ui/newmapconnectiondialog.cpp @@ -8,14 +8,13 @@ NewMapConnectionDialog::NewMapConnectionDialog(QWidget *parent, Map* map, const ui->setupUi(this); setAttribute(Qt::WA_DeleteOnClose); - this->result = nullptr; - ui->comboBox_Direction->setEditable(false); ui->comboBox_Direction->setMinimumContentsLength(0); ui->comboBox_Direction->addItems(MapConnection::cardinalDirections); ui->comboBox_Map->setMinimumContentsLength(6); ui->comboBox_Map->addItems(mapNames); + ui->comboBox_Map->setInsertPolicy(QComboBox::NoInsert); // Choose default direction QMap directionCounts; @@ -51,6 +50,11 @@ NewMapConnectionDialog::~NewMapConnectionDialog() } void NewMapConnectionDialog::accept() { - this->result = new MapConnection(ui->comboBox_Map->currentText(), ui->comboBox_Direction->currentText()); + // Invalid map names are not allowed + if (ui->comboBox_Map->findText(ui->comboBox_Map->currentText()) < 0) { + // TODO: Display error message + return; + } + emit accepted(new MapConnection(ui->comboBox_Map->currentText(), ui->comboBox_Direction->currentText())); QDialog::accept(); } From 7a0ae5314346a1bbb164810cece24e45e21412cc Mon Sep 17 00:00:00 2001 From: GriffinR Date: Mon, 19 Aug 2024 18:49:57 -0400 Subject: [PATCH 46/76] Final MapConnection UI updates --- forms/connectionslistitem.ui | 22 +++++++++++--- forms/mainwindow.ui | 7 +++-- forms/newmapconnectiondialog.ui | 47 +++++++++++++++++++++++++++-- include/ui/connectionslistitem.h | 12 +++----- include/ui/newmapconnectiondialog.h | 3 ++ src/editor.cpp | 15 ++++----- src/ui/connectionslistitem.cpp | 9 +++--- src/ui/newmapconnectiondialog.cpp | 22 +++++++++++--- 8 files changed, 104 insertions(+), 33 deletions(-) diff --git a/forms/connectionslistitem.ui b/forms/connectionslistitem.ui index c39c86d0..bf04e8be 100644 --- a/forms/connectionslistitem.ui +++ b/forms/connectionslistitem.ui @@ -65,7 +65,7 @@ - Remove this connection + Remove this connection. ... @@ -79,21 +79,35 @@ - Where the connected map should be positioned relative to the current map + Where the connected map should be positioned relative to the current map. - The name of the map to connect to the current map + The name of the map to connect to the current map. - The number of spaces to move the connected map perpendicular to its connected direction + The number of spaces to move the connected map perpendicular to its connected direction. + + + + + + + Open the connected map. + + + ... + + + + :/icons/map_go.ico:/icons/map_go.ico diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index ff12136e..8c9059bb 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -2664,6 +2664,9 @@ + + If enabled, the connected Emerge and/or Dive maps will be displayed with an opacity set using the slider. + Show Emerge/Dive Maps @@ -2874,8 +2877,8 @@ 0 0 - 100 - 30 + 365 + 651 diff --git a/forms/newmapconnectiondialog.ui b/forms/newmapconnectiondialog.ui index da109604..9b3a3b6e 100644 --- a/forms/newmapconnectiondialog.ui +++ b/forms/newmapconnectiondialog.ui @@ -7,7 +7,7 @@ 0 0 234 - 120 + 162 @@ -43,7 +43,11 @@ - + + + The name of the map to connect to the current map. + + @@ -53,7 +57,24 @@ - + + + Where the connected map should be positioned relative to the current map. + + + + + + + color: rgb(255, 0, 0) + + + 'Map' must be the name of an existing map. + + + true + +
@@ -84,12 +105,32 @@ accepted() NewMapConnectionDialog accept() + + + 20 + 20 + + + 20 + 20 + + buttonBox rejected() NewMapConnectionDialog reject() + + + 20 + 20 + + + 20 + 20 + + diff --git a/include/ui/connectionslistitem.h b/include/ui/connectionslistitem.h index 87051c37..18c3434c 100644 --- a/include/ui/connectionslistitem.h +++ b/include/ui/connectionslistitem.h @@ -14,8 +14,7 @@ class ConnectionsListItem; // We show the data for each map connection in the panel on the right side of the Connections tab. // An instance of this class is used for each item in that list. -// It communicates with the ConnectionPixmapItem on the map through a shared MapConnection pointer, -// and the two classes should signal one another when they change this data. +// It communicates with the ConnectionPixmapItem on the map through a shared MapConnection pointer. class ConnectionsListItem : public QFrame { Q_OBJECT @@ -27,28 +26,27 @@ public: void updateUI(); void setSelected(bool selected); - QPointer connection; - private: Ui::ConnectionsListItem *ui; + QPointer connection; Map *map; bool isSelected = false; unsigned actionId = 0; protected: void mousePressEvent(QMouseEvent*) override; - void mouseDoubleClickEvent(QMouseEvent*) override; signals: - void removed(MapConnection*); void selected(); - void doubleClicked(MapConnection*); + void removed(MapConnection*); + void openMapClicked(MapConnection*); private slots: void on_comboBox_Direction_currentTextChanged(const QString &direction); void on_comboBox_Map_currentTextChanged(const QString &mapName); void on_spinBox_Offset_valueChanged(int offset); void on_button_Delete_clicked(); + void on_button_OpenMap_clicked(); }; #endif // CONNECTIONSLISTITEM_H diff --git a/include/ui/newmapconnectiondialog.h b/include/ui/newmapconnectiondialog.h index 2e0ba28e..4781c971 100644 --- a/include/ui/newmapconnectiondialog.h +++ b/include/ui/newmapconnectiondialog.h @@ -24,6 +24,9 @@ signals: private: Ui::NewMapConnectionDialog *ui; + + bool mapNameIsValid(); + void setWarningVisible(bool visible); }; #endif // NEWMAPCONNECTIONDIALOG_H diff --git a/src/editor.cpp b/src/editor.cpp index c7acdea0..de1caa9a 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -758,8 +758,8 @@ void Editor::displayConnection(MapConnection* connection) { QObject::disconnect(connection, &MapConnection::directionChanged, nullptr, nullptr); QObject::disconnect(connection, &MapConnection::offsetChanged, nullptr, nullptr); - // Double clicking the list item or pixmap opens the connected map - connect(listItem, &ConnectionsListItem::doubleClicked, this, &Editor::onMapConnectionDoubleClicked); + // Double clicking the pixmap or clicking the list item's map button opens the connected map + connect(listItem, &ConnectionsListItem::openMapClicked, this, &Editor::onMapConnectionDoubleClicked); connect(pixmapItem, &ConnectionPixmapItem::connectionItemDoubleClicked, this, &Editor::onMapConnectionDoubleClicked); // Sync the selection highlight between the list UI and the pixmap @@ -937,7 +937,6 @@ void Editor::setDivingMapName(QString mapName, QString direction) { auto pixmapItem = diving_map_items.value(direction); MapConnection *connection = pixmapItem ? pixmapItem->connection() : nullptr; - // TODO: Test edit history if (connection) { if (mapName == connection->targetMapName()) return; // No change @@ -1823,11 +1822,8 @@ void Editor::updateMapBorder() { void Editor::updateMapConnections() { for (auto item : connection_items) item->render(true); - - maskNonVisibleConnectionTiles(); } -// TODO: Check first condition, move to Map int Editor::getBorderDrawDistance(int dimension) { // Draw sufficient border blocks to fill the player's view (BORDER_DISTANCE) if (dimension >= BORDER_DISTANCE) { @@ -1920,7 +1916,12 @@ void Editor::updateBorderVisibility() { item->setVisible(visible); item->setEditable(editingConnections); item->setEnabled(visible); - item->render(); + + // When connecting a map to itself we don't bother to re-render the map connections in real-time, + // i.e. if the user paints a new metatile on the map this isn't immediately reflected in the connection. + // We're rendering them now, so we take the opportunity to do a full re-render for self-connections. + bool fullRender = (this->map && item->connection && this->map->name == item->connection->targetMapName()); + item->render(fullRender); } } diff --git a/src/ui/connectionslistitem.cpp b/src/ui/connectionslistitem.cpp index eb7f94a6..337623b4 100644 --- a/src/ui/connectionslistitem.cpp +++ b/src/ui/connectionslistitem.cpp @@ -75,11 +75,6 @@ void ConnectionsListItem::mousePressEvent(QMouseEvent *) { this->setSelected(true); } -// TODO: This happens by accident a lot. Convert to button? -void ConnectionsListItem::mouseDoubleClickEvent(QMouseEvent *) { - emit doubleClicked(this->connection); -} - void ConnectionsListItem::on_comboBox_Direction_currentTextChanged(const QString &direction) { this->setSelected(true); if (this->map) @@ -102,3 +97,7 @@ void ConnectionsListItem::on_button_Delete_clicked() { if (this->map) this->map->editHistory.push(new MapConnectionRemove(this->map, this->connection)); } + +void ConnectionsListItem::on_button_OpenMap_clicked() { + emit openMapClicked(this->connection); +} diff --git a/src/ui/newmapconnectiondialog.cpp b/src/ui/newmapconnectiondialog.cpp index 254b4b6e..af06789f 100644 --- a/src/ui/newmapconnectiondialog.cpp +++ b/src/ui/newmapconnectiondialog.cpp @@ -9,10 +9,8 @@ NewMapConnectionDialog::NewMapConnectionDialog(QWidget *parent, Map* map, const setAttribute(Qt::WA_DeleteOnClose); ui->comboBox_Direction->setEditable(false); - ui->comboBox_Direction->setMinimumContentsLength(0); ui->comboBox_Direction->addItems(MapConnection::cardinalDirections); - ui->comboBox_Map->setMinimumContentsLength(6); ui->comboBox_Map->addItems(mapNames); ui->comboBox_Map->setInsertPolicy(QComboBox::NoInsert); @@ -42,6 +40,12 @@ NewMapConnectionDialog::NewMapConnectionDialog(QWidget *parent, Map* map, const defaultMapName = mapNames.first(); } ui->comboBox_Map->setTextItem(defaultMapName); + + connect(ui->comboBox_Map, &QComboBox::currentTextChanged, [this] { + if (ui->label_Warning->isVisible() && mapNameIsValid()) + setWarningVisible(false); + }); + setWarningVisible(false); } NewMapConnectionDialog::~NewMapConnectionDialog() @@ -49,10 +53,18 @@ NewMapConnectionDialog::~NewMapConnectionDialog() delete ui; } +bool NewMapConnectionDialog::mapNameIsValid() { + return ui->comboBox_Map->findText(ui->comboBox_Map->currentText()) >= 0; +} + +void NewMapConnectionDialog::setWarningVisible(bool visible) { + ui->label_Warning->setVisible(visible); + adjustSize(); +} + void NewMapConnectionDialog::accept() { - // Invalid map names are not allowed - if (ui->comboBox_Map->findText(ui->comboBox_Map->currentText()) < 0) { - // TODO: Display error message + if (!mapNameIsValid()) { + setWarningVisible(true); return; } emit accepted(new MapConnection(ui->comboBox_Map->currentText(), ui->comboBox_Direction->currentText())); From f70e77ab66bf6dfb93a2a1ac1ae5af905b2df4d2 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Mon, 19 Aug 2024 21:31:36 -0400 Subject: [PATCH 47/76] Final MapConnection testing fixes --- include/core/map.h | 6 ++-- include/core/mapconnection.h | 4 +-- include/editor.h | 15 ++++----- include/ui/connectionslistitem.h | 4 +-- include/ui/mapimageexporter.h | 3 +- src/core/map.cpp | 53 ++++++++++++++++------------- src/core/mapconnection.cpp | 2 +- src/editor.cpp | 57 ++++++++++++-------------------- src/mainwindow.cpp | 4 ++- src/ui/connectionslistitem.cpp | 4 +-- src/ui/mapimageexporter.cpp | 41 +++++++++++++++++------ 11 files changed, 104 insertions(+), 89 deletions(-) diff --git a/include/core/map.h b/include/core/map.h index 27020236..2333c862 100644 --- a/include/core/map.h +++ b/include/core/map.h @@ -99,9 +99,8 @@ public: void addEvent(Event *); void deleteConnections(); QList getConnections() const; - bool takeConnection(MapConnection *); - bool removeConnection(MapConnection *); - bool addConnection(MapConnection *); + void removeConnection(MapConnection *); + void addConnection(MapConnection *); void loadConnection(MapConnection *); QRect getConnectionRect(const QString &direction, MapLayout *fromLayout = nullptr); QPixmap renderConnection(const QString &direction, MapLayout *fromLayout = nullptr); @@ -133,6 +132,7 @@ public: private: void setNewDimensionsBlockdata(int newWidth, int newHeight); void setNewBorderDimensionsBlockdata(int newWidth, int newHeight); + void trackConnection(MapConnection*); // MapConnections in 'ownedConnections' but not 'connections' persist in the edit history. QList connections; diff --git a/include/core/mapconnection.h b/include/core/mapconnection.h index 6ffa85b7..21c00ac6 100644 --- a/include/core/mapconnection.h +++ b/include/core/mapconnection.h @@ -55,8 +55,8 @@ private: signals: void parentMapChanged(Map* before, Map* after); - void targetMapNameChanged(const QString &before, const QString &after); - void directionChanged(const QString &before, const QString &after); + void targetMapNameChanged(QString before, QString after); + void directionChanged(QString before, QString after); void offsetChanged(int before, int after); }; diff --git a/include/editor.h b/include/editor.h index 3b2c9ae6..89b6ccb5 100644 --- a/include/editor.h +++ b/include/editor.h @@ -78,7 +78,6 @@ public: void setMapEditingButtonsEnabled(bool enabled); void setConnectionsVisibility(bool visible); void updateDivingMapsVisibility(); - void displayDivingConnections(); void renderDivingConnections(); void addConnection(MapConnection* connection); void removeConnection(MapConnection* connection); @@ -185,13 +184,14 @@ private: void clearMapGrid(); void clearWildMonTables(); void updateBorderVisibility(); + void disconnectMapConnection(MapConnection *connection); QPoint getConnectionOrigin(MapConnection *connection); - void removeConnectionPixmap(MapConnection* connection); - void updateConnectionPixmap(ConnectionPixmapItem* connectionItem); - void displayConnection(MapConnection* connection); - void displayDivingConnection(MapConnection* connection); + void removeConnectionPixmap(MapConnection *connection); + void updateConnectionPixmap(ConnectionPixmapItem *connectionItem); + void displayConnection(MapConnection *connection); + void displayDivingConnection(MapConnection *connection); void setDivingMapName(QString mapName, QString direction); - void removeDivingMapPixmap(MapConnection* connection); + void removeDivingMapPixmap(MapConnection *connection); void updateEncounterFields(EncounterFields newFields); QString getMovementPermissionText(uint16_t collision, uint16_t elevation); QString getMetatileDisplayMessage(uint16_t metatileId); @@ -207,7 +207,7 @@ private slots: void setStraightPathCursorMode(QGraphicsSceneMouseEvent *event); void mouseEvent_map(QGraphicsSceneMouseEvent *event, MapPixmapItem *item); void mouseEvent_collision(QGraphicsSceneMouseEvent *event, CollisionPixmapItem *item); - void setSelectedConnectionItem(ConnectionPixmapItem* connectionItem); + void setSelectedConnectionItem(ConnectionPixmapItem *connectionItem); void onHoveredMovementPermissionChanged(uint16_t, uint16_t); void onHoveredMovementPermissionCleared(); void onHoveredMetatileSelectionChanged(uint16_t); @@ -219,7 +219,6 @@ private slots: void onSelectedMetatilesChanged(); void onWheelZoom(int); void onToggleGridClicked(bool); - void onMapConnectionDoubleClicked(MapConnection*); signals: void objectsChanged(); diff --git a/include/ui/connectionslistitem.h b/include/ui/connectionslistitem.h index 18c3434c..bbe0f2d3 100644 --- a/include/ui/connectionslistitem.h +++ b/include/ui/connectionslistitem.h @@ -42,8 +42,8 @@ signals: void openMapClicked(MapConnection*); private slots: - void on_comboBox_Direction_currentTextChanged(const QString &direction); - void on_comboBox_Map_currentTextChanged(const QString &mapName); + void on_comboBox_Direction_currentTextChanged(QString direction); + void on_comboBox_Map_currentTextChanged(QString mapName); void on_spinBox_Offset_valueChanged(int offset); void on_button_Delete_clicked(); void on_button_OpenMap_clicked(); diff --git a/include/ui/mapimageexporter.h b/include/ui/mapimageexporter.h index 6d8ae643..b9cc20bd 100644 --- a/include/ui/mapimageexporter.h +++ b/include/ui/mapimageexporter.h @@ -50,9 +50,10 @@ private: ImageExporterMode mode = ImageExporterMode::Normal; void updatePreview(); + void updateShowBorderState(); void saveImage(); QPixmap getStitchedImage(QProgressDialog *progress, bool includeBorder); - QPixmap getFormattedMapPixmap(Map *map, bool ignoreBorder); + QPixmap getFormattedMapPixmap(Map *map, bool ignoreBorder = false); bool historyItemAppliesToFrame(const QUndoCommand *command); private slots: diff --git a/src/core/map.cpp b/src/core/map.cpp index 91e196fe..86df7947 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -537,12 +537,12 @@ void Map::deleteConnections() { } QList Map::getConnections() const { - return connections; + return this->connections; } -bool Map::addConnection(MapConnection *connection) { +void Map::addConnection(MapConnection *connection) { if (!connection || this->connections.contains(connection)) - return false; + return; // Maps should only have one Dive/Emerge connection at a time. // (Users can technically have more by editing their data manually, but we will only display one at a time) @@ -550,9 +550,8 @@ bool Map::addConnection(MapConnection *connection) { if (MapConnection::isDiving(connection->direction())) { for (auto i : this->connections) { if (i->direction() == connection->direction()) { - this->ownedConnections.insert(connection); - connection->setParentMap(this, false); - return true; + trackConnection(connection); + return; } } } @@ -560,33 +559,41 @@ bool Map::addConnection(MapConnection *connection) { loadConnection(connection); modify(); emit connectionAdded(connection); - return true; + return; } void Map::loadConnection(MapConnection *connection) { if (!connection) return; - this->connections.append(connection); - this->ownedConnections.insert(connection); - connection->setParentMap(this, false); + + if (!this->connections.contains(connection)) + this->connections.append(connection); + + trackConnection(connection); } -// connection should not be deleted here, a pointer to it is allowed to persist in the edit history -bool Map::removeConnection(MapConnection *connection) { +void Map::trackConnection(MapConnection *connection) { + connection->setParentMap(this, false); + + if (!this->ownedConnections.contains(connection)) { + this->ownedConnections.insert(connection); + connect(connection, &MapConnection::parentMapChanged, [=](Map *, Map *after) { + if (after != this && after != nullptr) { + // MapConnection's parent has been reassigned, it's no longer our responsibility + this->ownedConnections.remove(connection); + QObject::disconnect(connection, &MapConnection::parentMapChanged, this, nullptr); + } + }); + } +} + +// We retain ownership of this MapConnection until it's assigned to a new parent map. +void Map::removeConnection(MapConnection *connection) { if (!this->connections.removeOne(connection)) - return false; + return; + connection->setParentMap(nullptr, false); modify(); emit connectionRemoved(connection); - return true; -} - -// Same as Map::removeConnection but caller takes ownership of connection. -bool Map::takeConnection(MapConnection *connection) { - if (!this->removeConnection(connection)) - return false; - connection->setParentMap(nullptr, false); - this->ownedConnections.remove(connection); - return true; } void Map::modify() { diff --git a/src/core/mapconnection.cpp b/src/core/mapconnection.cpp index 022158a0..b80b8d09 100644 --- a/src/core/mapconnection.cpp +++ b/src/core/mapconnection.cpp @@ -79,7 +79,7 @@ void MapConnection::setParentMap(Map* map, bool mirror) { } if (m_parentMap) - m_parentMap->takeConnection(this); + m_parentMap->removeConnection(this); auto before = m_parentMap; m_parentMap = map; diff --git a/src/editor.cpp b/src/editor.cpp index de1caa9a..586765f4 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -733,7 +733,15 @@ void Editor::updateEncounterFields(EncounterFields newFields) { project->wildMonFields = newFields; } -void Editor::displayConnection(MapConnection* connection) { +void Editor::disconnectMapConnection(MapConnection *connection) { + // Disconnect MapConnection's signals used by the display. + // It'd be nice if we could just 'connection->disconnect(this)' but that doesn't account for lambda functions. + QObject::disconnect(connection, &MapConnection::targetMapNameChanged, nullptr, nullptr); + QObject::disconnect(connection, &MapConnection::directionChanged, nullptr, nullptr); + QObject::disconnect(connection, &MapConnection::offsetChanged, nullptr, nullptr); +} + +void Editor::displayConnection(MapConnection *connection) { if (!connection) return; @@ -752,15 +760,9 @@ void Editor::displayConnection(MapConnection* connection) { ConnectionsListItem *listItem = new ConnectionsListItem(ui->scrollAreaContents_ConnectionsList, pixmapItem->connection, project->mapNames); ui->layout_ConnectionsList->insertWidget(ui->layout_ConnectionsList->count() - 1, listItem); // Insert above the vertical spacer - // It's possible for a map connection to be displayed repeatedly if it's being - // updated by mirroring and the target map is changing to/from the current map. - QObject::disconnect(connection, &MapConnection::targetMapNameChanged, nullptr, nullptr); - QObject::disconnect(connection, &MapConnection::directionChanged, nullptr, nullptr); - QObject::disconnect(connection, &MapConnection::offsetChanged, nullptr, nullptr); - // Double clicking the pixmap or clicking the list item's map button opens the connected map - connect(listItem, &ConnectionsListItem::openMapClicked, this, &Editor::onMapConnectionDoubleClicked); - connect(pixmapItem, &ConnectionPixmapItem::connectionItemDoubleClicked, this, &Editor::onMapConnectionDoubleClicked); + connect(listItem, &ConnectionsListItem::openMapClicked, this, &Editor::openConnectedMap); + connect(pixmapItem, &ConnectionPixmapItem::connectionItemDoubleClicked, this, &Editor::openConnectedMap); // Sync the selection highlight between the list UI and the pixmap connect(pixmapItem, &ConnectionPixmapItem::selectionChanged, [=](bool selected) { @@ -803,7 +805,7 @@ void Editor::displayConnection(MapConnection* connection) { } } -void Editor::addConnection(MapConnection* connection) { +void Editor::addConnection(MapConnection *connection) { if (!connection) return; @@ -814,7 +816,7 @@ void Editor::addConnection(MapConnection* connection) { this->map->editHistory.push(new MapConnectionAdd(this->map, connection)); } -void Editor::removeConnection(MapConnection* connection) { +void Editor::removeConnection(MapConnection *connection) { if (!connection) return; this->map->editHistory.push(new MapConnectionRemove(this->map, connection)); @@ -825,10 +827,12 @@ void Editor::removeSelectedConnection() { removeConnection(selected_connection_item->connection); } -void Editor::removeConnectionPixmap(MapConnection* connection) { +void Editor::removeConnectionPixmap(MapConnection *connection) { if (!connection) return; + disconnectMapConnection(connection); + if (MapConnection::isDiving(connection->direction())) { removeDivingMapPixmap(connection); return; @@ -857,15 +861,7 @@ void Editor::removeConnectionPixmap(MapConnection* connection) { delete pixmapItem; } -void Editor::displayDivingConnections() { - if (!this->map) - return; - - for (auto connection : this->map->getConnections()) - displayDivingConnection(connection); -} - -void Editor::displayDivingConnection(MapConnection* connection) { +void Editor::displayDivingConnection(MapConnection *connection) { if (!connection) return; @@ -994,7 +990,7 @@ QPoint Editor::getConnectionOrigin(MapConnection *connection) { return QPoint(x * 16, y * 16); } -void Editor::updateConnectionPixmap(ConnectionPixmapItem* pixmapItem) { +void Editor::updateConnectionPixmap(ConnectionPixmapItem *pixmapItem) { if (!pixmapItem) return; @@ -1004,7 +1000,7 @@ void Editor::updateConnectionPixmap(ConnectionPixmapItem* pixmapItem) { maskNonVisibleConnectionTiles(); } -void Editor::setSelectedConnectionItem(ConnectionPixmapItem* pixmapItem) { +void Editor::setSelectedConnectionItem(ConnectionPixmapItem *pixmapItem) { if (!pixmapItem || pixmapItem == selected_connection_item) return; @@ -1025,14 +1021,9 @@ void Editor::setSelectedConnection(MapConnection *connection) { } } -void Editor::onMapConnectionDoubleClicked(MapConnection* connection) { - if (connection) - emit openConnectedMap(connection); -} - void Editor::onBorderMetatilesChanged() { displayMapBorder(); - updateBorderVisibility(); // TODO: Why do we need to call this here + updateBorderVisibility(); } void Editor::onHoveredMovementPermissionChanged(uint16_t collision, uint16_t elevation) { @@ -1214,12 +1205,8 @@ bool Editor::setMap(QString map_name) { if (map) { map->pruneEditHistory(); map->disconnect(this); - for (auto connection : map->getConnections()) { - // Disconnect signals used by the display - QObject::disconnect(connection, &MapConnection::targetMapNameChanged, nullptr, nullptr); - QObject::disconnect(connection, &MapConnection::directionChanged, nullptr, nullptr); - QObject::disconnect(connection, &MapConnection::offsetChanged, nullptr, nullptr); - } + for (auto connection : map->getConnections()) + disconnectMapConnection(connection); } if (project) { diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index cc2b5f7f..1d60c9ad 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1892,7 +1892,7 @@ void MainWindow::on_mainTabBar_tabBarClicked(int index) } else if (index == MainTab::Connections) { editor->setEditingConnections(); // Stop the Dive/Emerge combo boxes from getting the initial focus - ui->graphicsView_Map->setFocus(); + ui->graphicsView_Connections->setFocus(); } if (index != MainTab::WildPokemon) { if (editor->project && editor->project->wildEncountersLoaded) @@ -2542,6 +2542,8 @@ void MainWindow::clickToolButtonFromEditMode(QString editMode) { } void MainWindow::onOpenConnectedMap(MapConnection *connection) { + if (!connection) + return; if (userSetMap(connection->targetMapName(), true)) editor->setSelectedConnection(connection->findMirror()); } diff --git a/src/ui/connectionslistitem.cpp b/src/ui/connectionslistitem.cpp index 337623b4..a5b8759a 100644 --- a/src/ui/connectionslistitem.cpp +++ b/src/ui/connectionslistitem.cpp @@ -75,13 +75,13 @@ void ConnectionsListItem::mousePressEvent(QMouseEvent *) { this->setSelected(true); } -void ConnectionsListItem::on_comboBox_Direction_currentTextChanged(const QString &direction) { +void ConnectionsListItem::on_comboBox_Direction_currentTextChanged(QString direction) { this->setSelected(true); if (this->map) this->map->editHistory.push(new MapConnectionChangeDirection(this->connection, direction)); } -void ConnectionsListItem::on_comboBox_Map_currentTextChanged(const QString &mapName) { +void ConnectionsListItem::on_comboBox_Map_currentTextChanged(QString mapName) { this->setSelected(true); if (this->map && ui->comboBox_Map->findText(mapName) >= 0) this->map->editHistory.push(new MapConnectionChangeMap(this->connection, mapName)); diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index b61d3738..65811474 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -33,7 +33,7 @@ MapImageExporter::MapImageExporter(QWidget *parent_, Editor *editor_, ImageExpor this->editor = editor_; this->mode = mode; this->setWindowTitle(getTitle(this->mode)); - this->ui->groupBox_Connections->setVisible(this->mode == ImageExporterMode::Normal); + this->ui->groupBox_Connections->setVisible(this->mode != ImageExporterMode::Stitch); this->ui->groupBox_Timelapse->setVisible(this->mode == ImageExporterMode::Timelapse); this->ui->comboBox_MapSelection->addItems(editor->project->mapNames); @@ -144,7 +144,7 @@ void MapImageExporter::saveImage() { this->map->editHistory.redo(); } progress.setValue(progress.maximum() - i); - QPixmap pixmap = this->getFormattedMapPixmap(this->map, !this->showBorder); + QPixmap pixmap = this->getFormattedMapPixmap(this->map); if (pixmap.width() < maxWidth || pixmap.height() < maxHeight) { QPixmap pixmap2 = QPixmap(maxWidth, maxHeight); QPainter painter(&pixmap2); @@ -167,7 +167,7 @@ void MapImageExporter::saveImage() { } } // The latest map state is the last animated frame. - QPixmap pixmap = this->getFormattedMapPixmap(this->map, !this->showBorder); + QPixmap pixmap = this->getFormattedMapPixmap(this->map); timelapseImg.addFrame(pixmap.toImage()); timelapseImg.save(filepath); progress.close(); @@ -178,6 +178,9 @@ void MapImageExporter::saveImage() { } bool MapImageExporter::historyItemAppliesToFrame(const QUndoCommand *command) { + if (command->isObsolete()) + return false; + switch (command->id() & 0xFF) { case CommandId::ID_PaintMetatile: case CommandId::ID_BucketFillMetatile: @@ -249,7 +252,9 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool inclu int x = cur.x; int y = cur.y; int offset = connection->offset(); - Map *connectionMap = this->editor->project->loadMap(connection->targetMapName()); + Map *connectionMap = connection->targetMap(); + if (!connectionMap) + continue; if (direction == "up") { x += offset; y -= connectionMap->getHeight(); @@ -318,7 +323,7 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool inclu pixelX -= STITCH_MODE_BORDER_DISTANCE * 16; pixelY -= STITCH_MODE_BORDER_DISTANCE * 16; } - QPixmap pixmap = this->getFormattedMapPixmap(map.map, false); + QPixmap pixmap = this->getFormattedMapPixmap(map.map); painter.drawPixmap(pixelX, pixelY, pixmap); } @@ -353,7 +358,7 @@ void MapImageExporter::updatePreview() { scene = nullptr; } - preview = getFormattedMapPixmap(this->map, false); + preview = getFormattedMapPixmap(this->map); scene = new QGraphicsScene; scene->addPixmap(preview); this->scene->setSceneRect(this->scene->itemsBoundingRect()); @@ -381,8 +386,7 @@ QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { // draw map border // note: this will break when allowing map to be selected from drop down maybe int borderHeight = 0, borderWidth = 0; - bool forceDrawBorder = showUpConnections || showDownConnections || showLeftConnections || showRightConnections; - if (!ignoreBorder && (showBorder || forceDrawBorder)) { + if (!ignoreBorder && this->showBorder) { int borderDistance = this->mode ? STITCH_MODE_BORDER_DISTANCE : BORDER_DISTANCE; map->renderBorder(); int borderHorzDist = editor->getBorderDrawDistance(map->getBorderWidth()); @@ -401,10 +405,9 @@ QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { pixmap = newPixmap; } - if (!this->mode) { + if (!ignoreBorder && (this->showUpConnections || this->showDownConnections || this->showLeftConnections || this->showRightConnections)) { // if showing connections, draw on outside of image QPainter connectionPainter(&pixmap); - // TODO: Is this still the most sensible way to do this (esp. rendering pixmap) for (auto connectionItem : editor->connection_items) { const QString direction = connectionItem->connection->direction(); if ((showUpConnections && direction == "up") @@ -421,7 +424,7 @@ QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { QPainter eventPainter(&pixmap); QList events = map->getAllEvents(); int pixelOffset = 0; - if (!ignoreBorder && showBorder) { + if (!ignoreBorder && this->showBorder) { pixelOffset = this->mode == ImageExporterMode::Normal ? BORDER_DISTANCE * 16 : STITCH_MODE_BORDER_DISTANCE * 16; } for (Event *event : events) { @@ -459,6 +462,18 @@ QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { return pixmap; } +void MapImageExporter::updateShowBorderState() { + // If any of the Connections settings are enabled then this setting is locked (it's implicitly enabled) + const QSignalBlocker blocker(ui->checkBox_Border); + if (showUpConnections || showDownConnections || showLeftConnections || showRightConnections) { + ui->checkBox_Border->setChecked(true); + ui->checkBox_Border->setDisabled(true); + showBorder = true; + } else { + ui->checkBox_Border->setDisabled(false); + } +} + void MapImageExporter::on_checkBox_Elevation_stateChanged(int state) { showCollision = (state == Qt::Checked); updatePreview(); @@ -501,21 +516,25 @@ void MapImageExporter::on_checkBox_HealSpots_stateChanged(int state) { void MapImageExporter::on_checkBox_ConnectionUp_stateChanged(int state) { showUpConnections = (state == Qt::Checked); + updateShowBorderState(); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionDown_stateChanged(int state) { showDownConnections = (state == Qt::Checked); + updateShowBorderState(); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionLeft_stateChanged(int state) { showLeftConnections = (state == Qt::Checked); + updateShowBorderState(); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionRight_stateChanged(int state) { showRightConnections = (state == Qt::Checked); + updateShowBorderState(); updatePreview(); } From 9927d66389541d69845e85860b947b32727e5ff7 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 20 Aug 2024 14:47:02 -0400 Subject: [PATCH 48/76] Update changelog and manual --- CHANGELOG.md | 10 +++++++++ docsrc/manual/editing-map-connections.rst | 21 +++++++++++------- .../add-connection-button.png | Bin 890 -> 10387 bytes .../map-connections.png | Bin 116336 -> 441334 bytes .../open-connection-button.png | Bin 0 -> 5606 bytes 5 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 docsrc/manual/images/editing-map-connections/open-connection-button.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 90ea613c..62f52507 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,14 @@ The **"Breaking Changes"** listed below are changes that have been made in the d ## [Unreleased] ### Added +- Redesigned the Connections tab, adding a number of new features including the option to open or display diving maps and a list UI for easier edit access. - Add a `Close Project` option - An alert will be displayed when attempting to open a seemingly invalid project. ### Changed +- Edits to map connections now have Undo/Redo and can be viewed in exported timelapses. +- Changes to the "Mirror to Connecting Maps" setting will now be saved between sessions. +- A notice will be displayed when attempting to open the "Dyamic" map, rather than nothing happening. - The base game version is now auto-detected if the project name contains only one of "emerald", "firered/leafgreen", or "ruby/sapphire". - It's now possible to cancel quitting if there are unsaved changes in sub-windows. @@ -25,6 +29,12 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - Fix `About porymap` opening a new window each time it's activated. - Fix the `Edit History` window not raising to the front when reactivated. - New maps are now always inserted in map dropdowns at the correct position, rather than at the bottom of the list until the project is reloaded. +- Fix changes to map connections not marking connected maps as unsaved. +- Fix numerous issues related to connecting a map to itself. +- Fix incorrect map connections getting selected when opening a map by double-clicking a map connection. +- Fix a visual issue when quickly dragging map connections around. +- Fix map connections rendering incorrectly if their direction name was unknown. +- Fix map connections rendering incorrectly if their dimensions were smaller than the border draw distance. ## [5.4.1] - 2024-03-21 ### Fixed diff --git a/docsrc/manual/editing-map-connections.rst b/docsrc/manual/editing-map-connections.rst index 88194cee..46af0120 100644 --- a/docsrc/manual/editing-map-connections.rst +++ b/docsrc/manual/editing-map-connections.rst @@ -2,35 +2,40 @@ Editing Map Connections *********************** -Maps can be connected together so that the player can seamlessly walk between them. These connections can be edited in the Connections tab. +Maps can be connected together so that the player can seamlessly walk between them. These connections can be edited in the Connections tab. .. figure:: images/editing-map-connections/map-connections.png :alt: Map Connections View Map Connections View -A connection has a direction, offset, and destination map. To add new connection, press the plus button |add-connection-button|. To delete a connection, select it and press the delete button |remove-connection-button|. +A connection has a direction, offset, and destination map. To add a new connection, press the |add-connection-button| button. To delete a connection you can either press the |remove-connection-button| button on its entry in the list, or select the connection and press the delete key. + +The |open-connection-button| button will open the destination map. You may also open the destination map by double-clicking the connection itself (this can be done from the ``Map`` and ``Events`` tabs as well). .. |add-connection-button| image:: images/editing-map-connections/add-connection-button.png + :height: 24 .. |remove-connection-button| image:: images/editing-map-connections/remove-connection-button.png + :height: 24 + +.. |open-connection-button| + image:: images/editing-map-connections/open-connection-button.png + :height: 24 To change the connection's vertical or horizontal offset, it's easiest to click and drag the connection to the desired offset. Dive & Emerge Warps ------------------- -Dive & emerge warps are used for the HM move Dive. They don't have offsets or directions--only a destination map. To add a dive or emerge warp, simply add a value in the Dive Map and/or Emerge Map dropdown menus. +Dive & emerge warps are used for the HM move Dive. They don't have offsets or directions--only a destination map. To add a dive or emerge warp, simply add a value in the Dive Map and/or Emerge Map dropdown menus. + +You can select the ``Show Emerge/Dive Maps`` checkbox to view your connected dive/emerge maps overlaid on the current map. The slider will change the opacity of this overlay. If you have both an emerge and a dive map connected you will see two sliders; the top slider is for the emerge map's opacity, and the bottom slider is for the dive map's opacity. Mirror Connections ------------------ An extremely useful feature is the *Mirror to Connecting Maps* checkbox in the top-right corner. Connections are one-way, which means that you must keep the two connections in sync between the two maps. For example, Petalburg City has a west connection to Route 104, and Route 104 has an east connection to Petalburg City. If *Mirror to Connecting Maps* is enabled, then Porymap will automatically update both sides of the connection. Be sure to *File -> Save All* (``Ctrl+Shift+S``) when saving, since you will need to save both maps' connections. - -Follow Connections ------------------- - -Double-clicking on a connection will open the destination map. This is very useful for navigating through your maps, similar to double-clicking on :ref:`Warp Events `. diff --git a/docsrc/manual/images/editing-map-connections/add-connection-button.png b/docsrc/manual/images/editing-map-connections/add-connection-button.png index a0419ed3ee43471f526469623dabcf6ec4997ac9..fe897bc6c9956b94deb803b8bdeb0922497055d8 100644 GIT binary patch literal 10387 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

JVO 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

M_1qtTT)h)b6D2K<_ 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%0D6c|OAH2Gr=zL!G5pY}b{Udcbu`Vu< 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

o=t7K518KH$R&;IZ&SS#j5BsRQrSt($bu$ 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 LS2g#d+0*|3!+i1l diff --git a/docsrc/manual/images/editing-map-connections/open-connection-button.png b/docsrc/manual/images/editing-map-connections/open-connection-button.png new file mode 100644 index 0000000000000000000000000000000000000000..e16007030383a3274a6fc4e3928534d8c581b2f9 GIT binary patch literal 5606 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 mldp}xP1$1D?hLaEN Date: Tue, 20 Aug 2024 15:31:40 -0400 Subject: [PATCH 49/76] Fix 'Dynamic' typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62f52507..bfd00cf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d ### Changed - Edits to map connections now have Undo/Redo and can be viewed in exported timelapses. - Changes to the "Mirror to Connecting Maps" setting will now be saved between sessions. -- A notice will be displayed when attempting to open the "Dyamic" map, rather than nothing happening. +- A notice will be displayed when attempting to open the "Dynamic" map, rather than nothing happening. - The base game version is now auto-detected if the project name contains only one of "emerald", "firered/leafgreen", or "ruby/sapphire". - It's now possible to cancel quitting if there are unsaved changes in sub-windows. From 2f24d45ca4996dcc702f9590802fd2db182f5d14 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 20 Aug 2024 23:53:55 -0400 Subject: [PATCH 50/76] Fix map sort order regression --- include/mainwindow.h | 3 --- src/mainwindow.cpp | 7 +++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/include/mainwindow.h b/include/mainwindow.h index 93f0d265..83eea1f5 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -339,9 +339,6 @@ private: bool isProgrammaticEventTabChange; bool projectHasUnsavedChanges; bool newMapDefaultsSet = false; - - MapSortOrder mapSortOrder; - bool tilesetNeedsRedraw = false; bool setMap(QString, bool scrollTreeView = false); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 37f45f20..8595fbcc 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -390,7 +390,7 @@ void MainWindow::initMapSortOrder() { connect(mapSortOrderActionGroup, &QActionGroup::triggered, this, &MainWindow::mapSortOrder_changed); - QAction* sortOrder = ui->toolButton_MapSortOrder->menu()->actions()[mapSortOrder]; + QAction* sortOrder = ui->toolButton_MapSortOrder->menu()->actions()[porymapConfig.mapSortOrder]; ui->toolButton_MapSortOrder->setIcon(sortOrder->icon()); sortOrder->setChecked(true); } @@ -424,7 +424,7 @@ void MainWindow::mapSortOrder_changed(QAction *action) } } - if (i != mapSortOrder) + if (i != porymapConfig.mapSortOrder) { ui->toolButton_MapSortOrder->setIcon(action->icon()); porymapConfig.mapSortOrder = static_cast(i); @@ -462,7 +462,6 @@ void MainWindow::loadUserSettings() { this->editor->settings->cursorTileRectEnabled = porymapConfig.showCursorTile; ui->checkBox_ToggleBorder->setChecked(porymapConfig.showBorder); ui->checkBox_ToggleGrid->setChecked(porymapConfig.showGrid); - mapSortOrder = porymapConfig.mapSortOrder; ui->horizontalSlider_CollisionTransparency->blockSignals(true); this->editor->collisionOpacity = static_cast(porymapConfig.collisionOpacity) / 100; ui->horizontalSlider_CollisionTransparency->setValue(porymapConfig.collisionOpacity); @@ -1092,7 +1091,7 @@ void MainWindow::sortMapList() { mapGroupItemsList->clear(); QStandardItem *root = mapListModel->invisibleRootItem(); - switch (mapSortOrder) + switch (porymapConfig.mapSortOrder) { case MapSortOrder::Group: for (int i = 0; i < project->groupNames.length(); i++) { From baaf9c8c9d50e1359aea64c4eedfeb305bbce7c9 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Wed, 21 Aug 2024 02:13:10 -0400 Subject: [PATCH 51/76] Fix group Event selections not clearing --- CHANGELOG.md | 1 + include/mainwindow.h | 8 +------ src/editor.cpp | 30 +++++++++++++++---------- src/mainwindow.cpp | 52 ++++++++++++++------------------------------ 4 files changed, 36 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51520783..dfee0051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - Fix config files being written before the project is opened successfully. - Fix the map and other project info still displaying if a new project fails to open. - Fix unsaved changes being ignored when quitting (such as with Cmd+Q on macOS). +- Fix selections with multiple Events not always clearing when making a new selection. - Fix `About porymap` opening a new window each time it's activated. - Fix the `Edit History` window not raising to the front when reactivated. diff --git a/include/mainwindow.h b/include/mainwindow.h index 83eea1f5..d58d11c0 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -222,7 +222,7 @@ private slots: void on_toolButton_deleteObject_clicked(); void addNewEvent(Event::Type type); - void tryAddEventTab(QWidget * tab, Event::Group group); + void tryAddEventTab(QWidget * tab); void displayEventTabs(); void updateSelectedObjects(); void updateObjects(); @@ -328,12 +328,6 @@ private: QAction *copyAction = nullptr; QAction *pasteAction = nullptr; - QWidget *eventTabObjectWidget; - QWidget *eventTabWarpWidget; - QWidget *eventTabTriggerWidget; - QWidget *eventTabBGWidget; - QWidget *eventTabHealspotWidget; - QWidget *eventTabMultipleWidget; QMap lastSelectedEvent; bool isProgrammaticEventTabChange; diff --git a/src/editor.cpp b/src/editor.cpp index 54194a96..46aa0458 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -2116,19 +2116,25 @@ void Editor::selectMapEvent(DraggablePixmapItem *object) { } void Editor::selectMapEvent(DraggablePixmapItem *object, bool toggle) { - if (selected_events && object) { - if (selected_events->contains(object)) { - if (toggle) { - selected_events->removeOne(object); - } - } else { - if (!toggle) { - selected_events->clear(); - } - selected_events->append(object); - } - updateSelectedEvents(); + if (!selected_events || !object) + return; + + if (!toggle) { + // Selecting just this event + selected_events->clear(); + selected_events->append(object); + } else if (!selected_events->contains(object)) { + // Adding event to selection + selected_events->append(object); + } else if (selected_events->length() > 1) { + // Removing from group selection + selected_events->removeOne(object); + } else { + // Attempting to toggle the only currently-selected event. + // Unselecting an event this way would be unexpected, so we ignore it. + return; } + updateSelectedEvents(); } void Editor::selectedEventIndexChanged(int index, Event::Group eventGroup) { diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 8595fbcc..39167597 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -366,12 +366,6 @@ void MainWindow::initMiscHeapObjects() { mapListProxyModel->setSourceModel(mapListModel); ui->mapList->setModel(mapListProxyModel); - eventTabObjectWidget = ui->tab_Objects; - eventTabWarpWidget = ui->tab_Warps; - eventTabTriggerWidget = ui->tab_Triggers; - eventTabBGWidget = ui->tab_BGs; - eventTabHealspotWidget = ui->tab_Healspots; - eventTabMultipleWidget = ui->tab_Multiple; ui->tabWidget_EventType->clear(); } @@ -1977,7 +1971,8 @@ void MainWindow::addNewEvent(Event::Type type) { } } -void MainWindow::tryAddEventTab(QWidget * tab, Event::Group group) { +void MainWindow::tryAddEventTab(QWidget * tab) { + auto group = getEventGroupFromTabWidget(tab); if (editor->map->events.value(group).length()) ui->tabWidget_EventType->addTab(tab, QString("%1s").arg(Event::eventGroupToString(group))); } @@ -1986,11 +1981,11 @@ void MainWindow::displayEventTabs() { const QSignalBlocker blocker(ui->tabWidget_EventType); ui->tabWidget_EventType->clear(); - tryAddEventTab(eventTabObjectWidget, Event::Group::Object); - tryAddEventTab(eventTabWarpWidget, Event::Group::Warp); - tryAddEventTab(eventTabTriggerWidget, Event::Group::Coord); - tryAddEventTab(eventTabBGWidget, Event::Group::Bg); - tryAddEventTab(eventTabHealspotWidget, Event::Group::Heal); + tryAddEventTab(ui->tab_Objects); + tryAddEventTab(ui->tab_Warps); + tryAddEventTab(ui->tab_Triggers); + tryAddEventTab(ui->tab_BGs); + tryAddEventTab(ui->tab_Healspots); } void MainWindow::updateObjects() { @@ -2150,30 +2145,15 @@ void MainWindow::updateSelectedObjects() { } } -Event::Group MainWindow::getEventGroupFromTabWidget(QWidget *tab) -{ - Event::Group ret = Event::Group::None; - if (tab == eventTabObjectWidget) - { - ret = Event::Group::Object; - } - else if (tab == eventTabWarpWidget) - { - ret = Event::Group::Warp; - } - else if (tab == eventTabTriggerWidget) - { - ret = Event::Group::Coord; - } - else if (tab == eventTabBGWidget) - { - ret = Event::Group::Bg; - } - else if (tab == eventTabHealspotWidget) - { - ret = Event::Group::Heal; - } - return ret; +Event::Group MainWindow::getEventGroupFromTabWidget(QWidget *tab) { + static const QMap tabToGroup = { + {ui->tab_Objects, Event::Group::Object}, + {ui->tab_Warps, Event::Group::Warp}, + {ui->tab_Triggers, Event::Group::Coord}, + {ui->tab_BGs, Event::Group::Bg}, + {ui->tab_Healspots, Event::Group::Heal}, + }; + return tabToGroup.value(tab, Event::Group::None); } void MainWindow::eventTabChanged(int index) { From b1814e0e3f55fbec581e7c67c50f1e4a4f348882 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Wed, 21 Aug 2024 13:06:30 -0400 Subject: [PATCH 52/76] Initial wild pokemon summary chart --- forms/mainwindow.ui | 7 +++ forms/wildmonchart.ui | 43 +++++++++++++++ include/editor.h | 1 + include/mainwindow.h | 3 + include/ui/encountertablemodel.h | 1 + include/ui/wildmonchart.h | 28 ++++++++++ porymap.pro | 11 ++-- src/editor.cpp | 6 ++ src/mainwindow.cpp | 10 ++++ src/ui/wildmonchart.cpp | 94 ++++++++++++++++++++++++++++++++ 10 files changed, 200 insertions(+), 4 deletions(-) create mode 100644 forms/wildmonchart.ui create mode 100644 include/ui/wildmonchart.h create mode 100644 src/ui/wildmonchart.cpp diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index 8e586198..899c9376 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -3010,6 +3010,13 @@ + + + + Summary Chart... + + + diff --git a/forms/wildmonchart.ui b/forms/wildmonchart.ui new file mode 100644 index 00000000..035272c2 --- /dev/null +++ b/forms/wildmonchart.ui @@ -0,0 +1,43 @@ + + + WildMonChart + + + + 0 + 0 + 400 + 300 + + + + Wild Pokémon Summary + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + QChartView + QGraphicsView +

QtCharts
+ +

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%0A5de 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%0AbODA#*g9chZVToNw8X_Oqw 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�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

+ + + diff --git a/include/editor.h b/include/editor.h index 8e30ae70..939f5a59 100644 --- a/include/editor.h +++ b/include/editor.h @@ -83,6 +83,7 @@ public: void removeCurrentConnection(); void addNewWildMonGroup(QWidget *window); void deleteWildMonGroup(); + QTableView* getCurrentWildMonTable(); void updateDiveMap(QString mapName); void updateEmergeMap(QString mapName); void setSelectedConnectionFromMap(QString mapName); diff --git a/include/mainwindow.h b/include/mainwindow.h index d58d11c0..4f35d551 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -27,6 +27,7 @@ #include "preferenceeditor.h" #include "projectsettingseditor.h" #include "customscriptseditor.h" +#include "wildmonchart.h" #include "updatepromoter.h" #include "aboutporymap.h" @@ -285,6 +286,7 @@ private slots: void on_horizontalSlider_CollisionZoom_valueChanged(int value); void on_pushButton_NewWildMonGroup_clicked(); void on_pushButton_DeleteWildMonGroup_clicked(); + void on_pushButton_SummaryChart_clicked(); void on_pushButton_ConfigureEncountersJSON_clicked(); void on_pushButton_CreatePrefab_clicked(); void on_spinBox_SelectedElevation_valueChanged(int elevation); @@ -314,6 +316,7 @@ private: QPointer updatePromoter = nullptr; QPointer networkAccessManager = nullptr; QPointer aboutWindow = nullptr; + QPointer wildMonChart = nullptr; FilterChildrenProxyModel *mapListProxyModel; QStandardItemModel *mapListModel; QList *mapGroupItemsList; diff --git a/include/ui/encountertablemodel.h b/include/ui/encountertablemodel.h index 6aacbf30..1a05476a 100644 --- a/include/ui/encountertablemodel.h +++ b/include/ui/encountertablemodel.h @@ -29,6 +29,7 @@ public: }; WildMonInfo encounterData(); + QList percentages() const { return slotPercentages; } void resize(int rows, int cols); private: diff --git a/include/ui/wildmonchart.h b/include/ui/wildmonchart.h new file mode 100644 index 00000000..62560207 --- /dev/null +++ b/include/ui/wildmonchart.h @@ -0,0 +1,28 @@ +#ifndef WILDMONCHART_H +#define WILDMONCHART_H + +#include "encountertablemodel.h" + +#include + +namespace Ui { +class WildMonChart; +} + +class WildMonChart : public QWidget +{ + Q_OBJECT +public: + explicit WildMonChart(QWidget *parent, EncounterTableModel *data); + ~WildMonChart(); + +public slots: + void setChartData(EncounterTableModel *data); + void updateChart(); + +private: + Ui::WildMonChart *ui; + EncounterTableModel *data; +}; + +#endif // WILDMONCHART_H diff --git a/porymap.pro b/porymap.pro index dfc3306e..b7a6f789 100644 --- a/porymap.pro +++ b/porymap.pro @@ -4,7 +4,7 @@ # #------------------------------------------------- -QT += core gui qml network +QT += core gui qml network charts greaterThan(QT_MAJOR_VERSION, 4): QT += widgets @@ -106,7 +106,8 @@ SOURCES += src/core/block.cpp \ src/settings.cpp \ src/log.cpp \ src/ui/uintspinbox.cpp \ - src/ui/updatepromoter.cpp + src/ui/updatepromoter.cpp \ + src/ui/wildmonchart.cpp HEADERS += include/core/block.h \ include/core/bitpacker.h \ @@ -202,7 +203,8 @@ HEADERS += include/core/block.h \ include/settings.h \ include/log.h \ include/ui/uintspinbox.h \ - include/ui/updatepromoter.h + include/ui/updatepromoter.h \ + include/ui/wildmonchart.h FORMS += forms/mainwindow.ui \ forms/prefabcreationdialog.ui \ @@ -221,7 +223,8 @@ FORMS += forms/mainwindow.ui \ forms/projectsettingseditor.ui \ forms/customscriptseditor.ui \ forms/customscriptslistitem.ui \ - forms/updatepromoter.ui + forms/updatepromoter.ui \ + forms/wildmonchart.ui RESOURCES += \ resources/images.qrc \ diff --git a/src/editor.cpp b/src/editor.cpp index 46aa0458..604df065 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -698,6 +698,12 @@ void Editor::saveEncounterTabData() { } } +QTableView* Editor::getCurrentWildMonTable() { + QStackedWidget *stack = ui->stackedWidget_WildMons; + auto tabWidget = static_cast(stack->widget(stack->currentIndex())); + return tabWidget->tableAt(tabWidget->currentIndex()); +} + void Editor::updateEncounterFields(EncounterFields newFields) { EncounterFields oldFields = project->wildMonFields; // Go through fields and determine whether we need to update a field. diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 39167597..f4557ac4 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2572,6 +2572,16 @@ void MainWindow::on_pushButton_DeleteWildMonGroup_clicked() { editor->deleteWildMonGroup(); } +void MainWindow::on_pushButton_SummaryChart_clicked() { + if (!this->wildMonChart) { + // TODO: Move to editor, connect to signals for when the table data changes + QTableView *table = this->editor->getCurrentWildMonTable(); + EncounterTableModel *data = table ? static_cast(table->model()) : nullptr; + this->wildMonChart = new WildMonChart(this, data); + } + openSubWindow(this->wildMonChart); +} + void MainWindow::on_pushButton_ConfigureEncountersJSON_clicked() { editor->configureEncounterJSON(this); } diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp new file mode 100644 index 00000000..1047e92b --- /dev/null +++ b/src/ui/wildmonchart.cpp @@ -0,0 +1,94 @@ +#include "wildmonchart.h" +#include "ui_wildmonchart.h" + +#include + +WildMonChart::WildMonChart(QWidget *parent, EncounterTableModel *data) : + QWidget(parent), + ui(new Ui::WildMonChart) +{ + ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose); + setWindowFlags(Qt::Window); + + ui->chartView->setRenderHint(QPainter::Antialiasing); + + setChartData(data); +}; + +WildMonChart::~WildMonChart() { + delete ui; +}; + +void WildMonChart::setChartData(EncounterTableModel *data) { + this->data = data; + updateChart(); +} + +void WildMonChart::updateChart() { + // TODO: Handle empty chart + if (!this->data) + return; + + const QList inputValues = data->percentages(); + const QVector inputPokemon = data->encounterData().wildPokemon; + + QList chartValues; + QList chartPokemon; + + // Combine data for duplicate species entries + QList seenSpecies; + for (int i = 0; i < qMin(inputValues.length(), inputPokemon.length()); i++) { + const double percent = inputValues.at(i); + const WildPokemon pokemon = inputPokemon.at(i); + + int existingIndex = seenSpecies.indexOf(pokemon.species); + if (existingIndex >= 0) { + // Duplicate species entry + chartValues[existingIndex] += percent; + if (pokemon.minLevel < chartPokemon.at(existingIndex).minLevel) + chartPokemon[existingIndex].minLevel = pokemon.minLevel; + if (pokemon.maxLevel > chartPokemon.at(existingIndex).maxLevel) + chartPokemon[existingIndex].maxLevel = pokemon.maxLevel; + } else { + // New species entry + chartValues.append(percent); + chartPokemon.append(pokemon); + seenSpecies.append(pokemon.species); + } + } + + // TODO: If pokemon < values, fill remainder with an "empty" slice + + // Populate chart + //const QString speciesPrefix = projectConfig.getIdentifier(ProjectIdentifier::regex_species); // TODO: Change regex to prefix + const QString speciesPrefix = "SPECIES_"; + QPieSeries *series = new QPieSeries(); + for (int i = 0; i < qMin(chartValues.length(), chartPokemon.length()); i++) { + const double percent = chartValues.at(i); + const WildPokemon pokemon = chartPokemon.at(i); + + // Strip 'SPECIES_' prefix + QString name = pokemon.species; + if (name.startsWith(speciesPrefix)) + name.remove(0, speciesPrefix.length()); + + QString label = QString("%1\nLv %2").arg(name).arg(pokemon.minLevel); + if (pokemon.minLevel != pokemon.maxLevel) + label.append(QString("-%1").arg(pokemon.maxLevel)); + label.append(QString(" (%1%)").arg(percent * 100)); + + QPieSlice *slice = new QPieSlice(label, percent); + //slice->setLabelPosition(QPieSlice::LabelInsideNormal); + slice->setLabelVisible(); + series->append(slice); + } + + QChart *chart = new QChart(); + chart->addSeries(series); + chart->legend()->hide(); + + ui->chartView->setChart(chart); // TODO: Leaking old chart + + // TODO: Draw icons onto slices +} From 2c65c22b308dcaee652069a8eaa39edcb221d805 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Wed, 21 Aug 2024 16:52:43 -0400 Subject: [PATCH 53/76] Summary chart to horizontal percent bar chart --- include/core/wildmoninfo.h | 4 +- include/ui/encountertablemodel.h | 5 +- include/ui/wildmonchart.h | 6 +- src/ui/encountertablemodel.cpp | 4 - src/ui/wildmonchart.cpp | 147 +++++++++++++++++++++---------- 5 files changed, 107 insertions(+), 59 deletions(-) diff --git a/include/core/wildmoninfo.h b/include/core/wildmoninfo.h index 7c16d1a0..6c93d0d2 100644 --- a/include/core/wildmoninfo.h +++ b/include/core/wildmoninfo.h @@ -22,9 +22,9 @@ struct WildPokemonHeader { }; struct EncounterField { - QString name; + QString name; // Ex: "fishing_mons" QVector encounterRates; - tsl::ordered_map> groups; + tsl::ordered_map> groups; // Ex: "good_rod", {2, 3, 4} }; typedef QVector EncounterFields; diff --git a/include/ui/encountertablemodel.h b/include/ui/encountertablemodel.h index 1a05476a..b2a39ad8 100644 --- a/include/ui/encountertablemodel.h +++ b/include/ui/encountertablemodel.h @@ -28,8 +28,9 @@ public: Slot, Group, Species, MinLevel, MaxLevel, EncounterChance, SlotRatio, EncounterRate, Count }; - WildMonInfo encounterData(); - QList percentages() const { return slotPercentages; } + WildMonInfo encounterData() const { return this->monInfo; } + EncounterField encounterField() const { return this->encounterFields.at(this->fieldIndex); } + QList percentages() const { return this->slotPercentages; } void resize(int rows, int cols); private: diff --git a/include/ui/wildmonchart.h b/include/ui/wildmonchart.h index 62560207..97cb53fa 100644 --- a/include/ui/wildmonchart.h +++ b/include/ui/wildmonchart.h @@ -13,16 +13,16 @@ class WildMonChart : public QWidget { Q_OBJECT public: - explicit WildMonChart(QWidget *parent, EncounterTableModel *data); + explicit WildMonChart(QWidget *parent, EncounterTableModel *table); ~WildMonChart(); public slots: - void setChartData(EncounterTableModel *data); + void setTable(EncounterTableModel *table); void updateChart(); private: Ui::WildMonChart *ui; - EncounterTableModel *data; + EncounterTableModel *table; }; #endif // WILDMONCHART_H diff --git a/src/ui/encountertablemodel.cpp b/src/ui/encountertablemodel.cpp index fd870e40..00199735 100644 --- a/src/ui/encountertablemodel.cpp +++ b/src/ui/encountertablemodel.cpp @@ -199,7 +199,3 @@ Qt::ItemFlags EncounterTableModel::flags(const QModelIndex &index) const { } return flags | QAbstractTableModel::flags(index); } - -WildMonInfo EncounterTableModel::encounterData() { - return this->monInfo; -} diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp index 1047e92b..ee73a4dd 100644 --- a/src/ui/wildmonchart.cpp +++ b/src/ui/wildmonchart.cpp @@ -1,9 +1,24 @@ #include "wildmonchart.h" #include "ui_wildmonchart.h" +#include "log.h" + #include -WildMonChart::WildMonChart(QWidget *parent, EncounterTableModel *data) : +// TODO: Conditional QtCharts -> QtGraphs +// TODO: Handle if num pokemon < values +// TODO: Smarter value precision for %s +// TODO: Move level range onto graph? +// TODO: Draw species icons below legend icons? +// TODO: Match group order in chart visually to group order in table + +struct ChartData { + int minLevel; + int maxLevel; + QMap values; // One value for each wild encounter group +}; + +WildMonChart::WildMonChart(QWidget *parent, EncounterTableModel *table) : QWidget(parent), ui(new Ui::WildMonChart) { @@ -13,82 +28,118 @@ WildMonChart::WildMonChart(QWidget *parent, EncounterTableModel *data) : ui->chartView->setRenderHint(QPainter::Antialiasing); - setChartData(data); + setTable(table); }; WildMonChart::~WildMonChart() { delete ui; }; -void WildMonChart::setChartData(EncounterTableModel *data) { - this->data = data; +void WildMonChart::setTable(EncounterTableModel *table) { + this->table = table; updateChart(); } void WildMonChart::updateChart() { - // TODO: Handle empty chart - if (!this->data) + if (!this->table) return; - const QList inputValues = data->percentages(); - const QVector inputPokemon = data->encounterData().wildPokemon; - - QList chartValues; - QList chartPokemon; + // Read data about encounter groups, e.g. for "fishing_mons" we want to know indexes 2-4 belong to good_rod (group index 1). + // Each group will be represented as a separate bar on the graph. + QList groupNames; + QMap tableIndexToGroupIndex; + int groupIndex = 0; + for (auto groupPair : table->encounterField().groups) { + groupNames.append(groupPair.first); + for (auto i : groupPair.second) { + tableIndexToGroupIndex.insert(i, groupIndex); + } + groupIndex++; + } + const int numGroups = qMax(1, groupNames.length()); // Implicitly 1 group when none are listed - // Combine data for duplicate species entries - QList seenSpecies; - for (int i = 0; i < qMin(inputValues.length(), inputPokemon.length()); i++) { - const double percent = inputValues.at(i); - const WildPokemon pokemon = inputPokemon.at(i); + // Read data from the table, combining data for duplicate species entries + const QList tableValues = table->percentages(); + const QVector tablePokemon = table->encounterData().wildPokemon; + QMap speciesToChartData; + for (int i = 0; i < qMin(tableValues.length(), tablePokemon.length()); i++) { + const double value = tableValues.at(i); + const WildPokemon pokemon = tablePokemon.at(i); + groupIndex = tableIndexToGroupIndex.value(i, 0); - int existingIndex = seenSpecies.indexOf(pokemon.species); - if (existingIndex >= 0) { + if (speciesToChartData.contains(pokemon.species)) { // Duplicate species entry - chartValues[existingIndex] += percent; - if (pokemon.minLevel < chartPokemon.at(existingIndex).minLevel) - chartPokemon[existingIndex].minLevel = pokemon.minLevel; - if (pokemon.maxLevel > chartPokemon.at(existingIndex).maxLevel) - chartPokemon[existingIndex].maxLevel = pokemon.maxLevel; + ChartData *entry = &speciesToChartData[pokemon.species]; + entry->values[groupIndex] += value; + if (entry->minLevel > pokemon.minLevel) + entry->minLevel = pokemon.minLevel; + if (entry->maxLevel < pokemon.maxLevel) + entry->maxLevel = pokemon.maxLevel; } else { // New species entry - chartValues.append(percent); - chartPokemon.append(pokemon); - seenSpecies.append(pokemon.species); + ChartData entry; + entry.minLevel = pokemon.minLevel; + entry.maxLevel = pokemon.maxLevel; + entry.values.insert(groupIndex, value); + speciesToChartData.insert(pokemon.species, entry); } } - // TODO: If pokemon < values, fill remainder with an "empty" slice - // Populate chart //const QString speciesPrefix = projectConfig.getIdentifier(ProjectIdentifier::regex_species); // TODO: Change regex to prefix + QList barSets; const QString speciesPrefix = "SPECIES_"; - QPieSeries *series = new QPieSeries(); - for (int i = 0; i < qMin(chartValues.length(), chartPokemon.length()); i++) { - const double percent = chartValues.at(i); - const WildPokemon pokemon = chartPokemon.at(i); - + for (auto mapPair = speciesToChartData.cbegin(), end = speciesToChartData.cend(); mapPair != end; mapPair++) { + const ChartData entry = mapPair.value(); + // Strip 'SPECIES_' prefix - QString name = pokemon.species; - if (name.startsWith(speciesPrefix)) - name.remove(0, speciesPrefix.length()); + QString species = mapPair.key(); + if (species.startsWith(speciesPrefix)) + species.remove(0, speciesPrefix.length()); - QString label = QString("%1\nLv %2").arg(name).arg(pokemon.minLevel); - if (pokemon.minLevel != pokemon.maxLevel) - label.append(QString("-%1").arg(pokemon.maxLevel)); - label.append(QString(" (%1%)").arg(percent * 100)); + // Create label for legend + QString label = QString("%1\nLv %2").arg(species).arg(entry.minLevel); + if (entry.minLevel != entry.maxLevel) + label.append(QString("-%1").arg(entry.maxLevel)); - QPieSlice *slice = new QPieSlice(label, percent); - //slice->setLabelPosition(QPieSlice::LabelInsideNormal); - slice->setLabelVisible(); - series->append(slice); + // Add encounter chance data + auto set = new QBarSet(label); + for (int i = 0; i < numGroups; i++) + set->append(entry.values.value(i, 0)); + + // Insert bar set in order of total value + int i = 0; + for (; i < barSets.length(); i++){ + if (barSets.at(i)->sum() > set->sum()) + break; + } + barSets.insert(i, set); } - QChart *chart = new QChart(); + auto series = new QHorizontalPercentBarSeries(); + series->setLabelsVisible(); + series->append(barSets); + + auto chart = new QChart(); chart->addSeries(series); - chart->legend()->hide(); + chart->setAnimationOptions(QChart::SeriesAnimations); + chart->legend()->setVisible(true); + chart->legend()->setShowToolTips(true); + chart->legend()->setAlignment(Qt::AlignBottom); - ui->chartView->setChart(chart); // TODO: Leaking old chart + // X-axis is the values (percentages) + auto axisX = new QValueAxis(); + chart->addAxis(axisX, Qt::AlignBottom); + series->attachAxis(axisX); - // TODO: Draw icons onto slices + // Y-axis is the names of encounter groups (e.g. Old Rod, Good Rod...) + if (numGroups > 1) { + auto axisY = new QBarCategoryAxis(); + axisY->setCategories(groupNames); + chart->addAxis(axisY, Qt::AlignLeft); + series->attachAxis(axisY); + } + + delete ui->chartView->chart(); + ui->chartView->setChart(chart); } From 9b09637b47a0a91475e688236aa449b7a7dfaad6 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Wed, 21 Aug 2024 23:28:33 -0400 Subject: [PATCH 54/76] Summary chart adjustments --- docsrc/manual/project-files.rst | 2 +- forms/wildmonchart.ui | 4 +- include/config.h | 2 +- include/ui/wildmonchart.h | 6 +-- src/config.cpp | 2 +- src/project.cpp | 2 +- src/ui/wildmonchart.cpp | 66 +++++++++++++++++++++------------ 7 files changed, 51 insertions(+), 33 deletions(-) diff --git a/docsrc/manual/project-files.rst b/docsrc/manual/project-files.rst index 20849ba3..1da0fbc6 100644 --- a/docsrc/manual/project-files.rst +++ b/docsrc/manual/project-files.rst @@ -120,6 +120,7 @@ In addition to these files, there are some specific symbol and macro names that ``define_map_section_prefix``, ``MAPSEC_``, expected prefix for location macro names ``define_map_section_empty``, ``NONE``, macro name after prefix for empty region map sections ``define_map_section_count``, ``COUNT``, macro name after prefix for total number of region map sections + ``define_species_prefix``, ``SPECIES_``, expected prefix for species macro names ``regex_behaviors``, ``\bMB_``, regex to find metatile behavior macro names ``regex_obj_event_gfx``, ``\bOBJ_EVENT_GFX_``, regex to find Object Event graphics ID macro names ``regex_items``, ``\bITEM_(?!(B_)?USE_)``, regex to find item macro names @@ -134,4 +135,3 @@ In addition to these files, there are some specific symbol and macro names that ``regex_sign_facing_directions``, ``\bBG_EVENT_PLAYER_FACING_``, regex to find sign facing direction macro names ``regex_trainer_types``, ``\bTRAINER_TYPE_``, regex to find trainer type macro names ``regex_music``, ``\b(SE|MUS)_``, regex to find music macro names - ``regex_species``, ``\bSPECIES_``, regex to find species macro names diff --git a/forms/wildmonchart.ui b/forms/wildmonchart.ui index 035272c2..5542afc3 100644 --- a/forms/wildmonchart.ui +++ b/forms/wildmonchart.ui @@ -6,8 +6,8 @@ 0 0 - 400 - 300 + 697 + 349 diff --git a/include/config.h b/include/config.h index 3f77aa90..bf80ecb1 100644 --- a/include/config.h +++ b/include/config.h @@ -205,6 +205,7 @@ enum ProjectIdentifier { define_map_section_prefix, define_map_section_empty, define_map_section_count, + define_species_prefix, regex_behaviors, regex_obj_event_gfx, regex_items, @@ -219,7 +220,6 @@ enum ProjectIdentifier { regex_sign_facing_directions, regex_trainer_types, regex_music, - regex_species, }; enum ProjectFilePath { diff --git a/include/ui/wildmonchart.h b/include/ui/wildmonchart.h index 97cb53fa..36ab8619 100644 --- a/include/ui/wildmonchart.h +++ b/include/ui/wildmonchart.h @@ -13,16 +13,16 @@ class WildMonChart : public QWidget { Q_OBJECT public: - explicit WildMonChart(QWidget *parent, EncounterTableModel *table); + explicit WildMonChart(QWidget *parent, const EncounterTableModel *table); ~WildMonChart(); public slots: - void setTable(EncounterTableModel *table); + void setTable(const EncounterTableModel *table); void updateChart(); private: Ui::WildMonChart *ui; - EncounterTableModel *table; + const EncounterTableModel *table; }; #endif // WILDMONCHART_H diff --git a/src/config.cpp b/src/config.cpp index 16853e8f..fae8bac7 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -109,6 +109,7 @@ const QMap> ProjectConfig::defaultIde {ProjectIdentifier::define_map_section_prefix, {"define_map_section_prefix", "MAPSEC_"}}, {ProjectIdentifier::define_map_section_empty, {"define_map_section_empty", "NONE"}}, {ProjectIdentifier::define_map_section_count, {"define_map_section_count", "COUNT"}}, + {ProjectIdentifier::define_species_prefix, {"define_species_prefix", "SPECIES_"}}, // Regex {ProjectIdentifier::regex_behaviors, {"regex_behaviors", "\\bMB_"}}, {ProjectIdentifier::regex_obj_event_gfx, {"regex_obj_event_gfx", "\\bOBJ_EVENT_GFX_"}}, @@ -124,7 +125,6 @@ const QMap> ProjectConfig::defaultIde {ProjectIdentifier::regex_sign_facing_directions, {"regex_sign_facing_directions", "\\bBG_EVENT_PLAYER_FACING_"}}, {ProjectIdentifier::regex_trainer_types, {"regex_trainer_types", "\\bTRAINER_TYPE_"}}, {ProjectIdentifier::regex_music, {"regex_music", "\\b(SE|MUS)_"}}, - {ProjectIdentifier::regex_species, {"regex_species", "\\bSPECIES_"}}, }; const QMap> ProjectConfig::defaultPaths = { diff --git a/src/project.cpp b/src/project.cpp index 7048812a..1d37b3ea 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -2606,7 +2606,7 @@ bool Project::readSpeciesIconPaths() { const QMap iconIncbins = parser.readCIncbinMulti(incfilename); // Read species constants. If this fails we can get them from the icon table (but we shouldn't rely on it). - const QStringList prefixes = {projectConfig.getIdentifier(ProjectIdentifier::regex_species)}; + const QStringList prefixes = {projectConfig.getIdentifier(ProjectIdentifier::define_species_prefix)}; const QString constantsFilename = projectConfig.getFilePath(ProjectFilePath::constants_species); fileWatcher.addPath(root + "/" + constantsFilename); QStringList speciesNames = parser.readCDefineNames(constantsFilename, prefixes); diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp index ee73a4dd..eccec380 100644 --- a/src/ui/wildmonchart.cpp +++ b/src/ui/wildmonchart.cpp @@ -1,16 +1,14 @@ #include "wildmonchart.h" #include "ui_wildmonchart.h" +#include "config.h" #include "log.h" #include -// TODO: Conditional QtCharts -> QtGraphs -// TODO: Handle if num pokemon < values -// TODO: Smarter value precision for %s -// TODO: Move level range onto graph? +// TODO: Make level range its own chart(s)? // TODO: Draw species icons below legend icons? -// TODO: Match group order in chart visually to group order in table +// TODO: Add hover behavior to display species name (and click prompt?) struct ChartData { int minLevel; @@ -18,7 +16,7 @@ struct ChartData { QMap values; // One value for each wild encounter group }; -WildMonChart::WildMonChart(QWidget *parent, EncounterTableModel *table) : +WildMonChart::WildMonChart(QWidget *parent, const EncounterTableModel *table) : QWidget(parent), ui(new Ui::WildMonChart) { @@ -35,22 +33,26 @@ WildMonChart::~WildMonChart() { delete ui; }; -void WildMonChart::setTable(EncounterTableModel *table) { +void WildMonChart::setTable(const EncounterTableModel *table) { this->table = table; updateChart(); } +static const bool showLevelRange = false; + void WildMonChart::updateChart() { if (!this->table) return; + setWindowTitle(QString("Wild Pokémon Summary -- %1").arg(this->table->encounterField().name)); + // Read data about encounter groups, e.g. for "fishing_mons" we want to know indexes 2-4 belong to good_rod (group index 1). // Each group will be represented as a separate bar on the graph. QList groupNames; QMap tableIndexToGroupIndex; int groupIndex = 0; - for (auto groupPair : table->encounterField().groups) { - groupNames.append(groupPair.first); + for (auto groupPair : this->table->encounterField().groups) { + groupNames.prepend(groupPair.first); for (auto i : groupPair.second) { tableIndexToGroupIndex.insert(i, groupIndex); } @@ -59,8 +61,8 @@ void WildMonChart::updateChart() { const int numGroups = qMax(1, groupNames.length()); // Implicitly 1 group when none are listed // Read data from the table, combining data for duplicate species entries - const QList tableValues = table->percentages(); - const QVector tablePokemon = table->encounterData().wildPokemon; + const QList tableValues = this->table->percentages(); + const QVector tablePokemon = this->table->encounterData().wildPokemon; QMap speciesToChartData; for (int i = 0; i < qMin(tableValues.length(), tablePokemon.length()); i++) { const double value = tableValues.at(i); @@ -86,28 +88,31 @@ void WildMonChart::updateChart() { } // Populate chart - //const QString speciesPrefix = projectConfig.getIdentifier(ProjectIdentifier::regex_species); // TODO: Change regex to prefix QList barSets; - const QString speciesPrefix = "SPECIES_"; + const QString speciesPrefix = projectConfig.getIdentifier(ProjectIdentifier::define_species_prefix); for (auto mapPair = speciesToChartData.cbegin(), end = speciesToChartData.cend(); mapPair != end; mapPair++) { const ChartData entry = mapPair.value(); // Strip 'SPECIES_' prefix - QString species = mapPair.key(); - if (species.startsWith(speciesPrefix)) - species.remove(0, speciesPrefix.length()); + QString label = mapPair.key(); + if (label.startsWith(speciesPrefix)) + label.remove(0, speciesPrefix.length()); - // Create label for legend - QString label = QString("%1\nLv %2").arg(species).arg(entry.minLevel); - if (entry.minLevel != entry.maxLevel) - label.append(QString("-%1").arg(entry.maxLevel)); + // Add level range to label + if (showLevelRange) { + if (entry.minLevel == entry.maxLevel) + label.append(QString(" (Lv %1)").arg(entry.minLevel)); + else + label.append(QString(" (Lv %1-%2)").arg(entry.minLevel).arg(entry.maxLevel)); + } - // Add encounter chance data auto set = new QBarSet(label); - for (int i = 0; i < numGroups; i++) + + // Add encounter chance data (in reverse order, to match the table's group order visually) + for (int i = numGroups - 1; i >= 0; i--) set->append(entry.values.value(i, 0)); - // Insert bar set in order of total value + // Insert bar set. We order them from lowest to highest total, left-to-right. int i = 0; for (; i < barSets.length(); i++){ if (barSets.at(i)->sum() > set->sum()) @@ -118,6 +123,7 @@ void WildMonChart::updateChart() { auto series = new QHorizontalPercentBarSeries(); series->setLabelsVisible(); + //series->setLabelsPrecision(x); // This appears to have no effect for any value 'x'? Ideally we'd display 1-2 decimal places series->append(barSets); auto chart = new QChart(); @@ -127,8 +133,14 @@ void WildMonChart::updateChart() { chart->legend()->setShowToolTips(true); chart->legend()->setAlignment(Qt::AlignBottom); - // X-axis is the values (percentages) + // X-axis is the values (percentages). We're already showing percentages on the bar, so we just display 0/50/100% auto axisX = new QValueAxis(); +#if (QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)) + // Not critical, but the percentage ticks on the x-axis have no need for decimals. + // This property doesn't exist prior to Qt 6.7 + axisX->setLabelDecimals(0); +#endif + axisX->setTickCount(3); chart->addAxis(axisX, Qt::AlignBottom); series->attachAxis(axisX); @@ -142,4 +154,10 @@ void WildMonChart::updateChart() { delete ui->chartView->chart(); ui->chartView->setChart(chart); + + // Turn off the animation once it's played, otherwise it replays any time the window changes size. + QTimer::singleShot(chart->animationDuration() + 500, [this] { + if (ui->chartView->chart()) + ui->chartView->chart()->setAnimationOptions(QChart::NoAnimation); + }); } From c33e72f404d769e14b977cee5dac86ce69b7bdce Mon Sep 17 00:00:00 2001 From: GriffinR Date: Thu, 22 Aug 2024 02:09:28 -0400 Subject: [PATCH 55/76] Add level distribution chart, more adjustments --- forms/wildmonchart.ui | 126 +++++++++++++++++- include/ui/wildmonchart.h | 32 ++++- src/mainwindow.cpp | 6 + src/ui/wildmonchart.cpp | 270 ++++++++++++++++++++++++++------------ 4 files changed, 342 insertions(+), 92 deletions(-) diff --git a/forms/wildmonchart.ui b/forms/wildmonchart.ui index 5542afc3..88d3a590 100644 --- a/forms/wildmonchart.ui +++ b/forms/wildmonchart.ui @@ -6,8 +6,8 @@ 0 0 - 697 - 349 + 776 + 445 @@ -15,19 +15,131 @@ - 0 + 4 - 0 + 4 - 0 + 4 - 0 + 4 - + + + + Species Distribution + + + + 0 + + + 0 + + + 0 + + + + + QPainter::Antialiasing + + + + + + + + Level Distribution + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::NoFrame + + + QFrame::Plain + + + + 12 + + + 0 + + + + + Group + + + + + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Species + + + + + + + true + + + QComboBox::NoInsert + + + + + + + + + + QPainter::Antialiasing + + + + + + diff --git a/include/ui/wildmonchart.h b/include/ui/wildmonchart.h index 36ab8619..7c523355 100644 --- a/include/ui/wildmonchart.h +++ b/include/ui/wildmonchart.h @@ -4,6 +4,7 @@ #include "encountertablemodel.h" #include +#include namespace Ui { class WildMonChart; @@ -18,11 +19,40 @@ public: public slots: void setTable(const EncounterTableModel *table); - void updateChart(); + void createCharts(); private: Ui::WildMonChart *ui; const EncounterTableModel *table; + + QStringList groupNames; + QMap tableIndexToGroupName; + + struct Summary { + double speciesFrequency = 0.0; + QMap levelFrequencies; + }; + + int tableMinLevel; + int tableMaxLevel; + + // GroupedData maps a group name ("old_rod", "good_rod"...) + // to any summarized data needed for the charts. + typedef QMap GroupedData; + + QMap speciesToGroupedData; + + QStringList getSpeciesNames() const; + double getSpeciesFrequency(const QString&, const QString&) const; + QMap getLevelFrequencies(const QString &, const QString &) const; + bool usesGroupLabels() const; + + void clearTableData(); + void readTable(); + void createSpeciesDistributionChart(); + void createLevelDistributionChart(); + + void stopChartAnimation(); }; #endif // WILDMONCHART_H diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index f4557ac4..da39f555 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2578,6 +2578,8 @@ void MainWindow::on_pushButton_SummaryChart_clicked() { QTableView *table = this->editor->getCurrentWildMonTable(); EncounterTableModel *data = table ? static_cast(table->model()) : nullptr; this->wildMonChart = new WildMonChart(this, data); + } else { + this->wildMonChart->createCharts(); } openSubWindow(this->wildMonChart); } @@ -3027,6 +3029,10 @@ bool MainWindow::closeSupplementaryWindows() { return false; this->customScriptsEditor = nullptr; + if (this->wildMonChart && !this->wildMonChart->close()) + return false; + this->wildMonChart = nullptr; + if (this->projectSettingsEditor) this->projectSettingsEditor->closeQuietly(); this->projectSettingsEditor = nullptr; diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp index eccec380..b7080aae 100644 --- a/src/ui/wildmonchart.cpp +++ b/src/ui/wildmonchart.cpp @@ -4,16 +4,16 @@ #include "log.h" -#include - // TODO: Make level range its own chart(s)? // TODO: Draw species icons below legend icons? -// TODO: Add hover behavior to display species name (and click prompt?) +// TODO: NoScrollComboBoxes + +static const QString baseWindowTitle = QString("Wild Pokémon Summary Charts"); struct ChartData { int minLevel; int maxLevel; - QMap values; // One value for each wild encounter group + QMap valueMap; // One value for each wild encounter group }; WildMonChart::WildMonChart(QWidget *parent, const EncounterTableModel *table) : @@ -24,7 +24,8 @@ WildMonChart::WildMonChart(QWidget *parent, const EncounterTableModel *table) : setAttribute(Qt::WA_DeleteOnClose); setWindowFlags(Qt::Window); - ui->chartView->setRenderHint(QPainter::Antialiasing); + connect(ui->comboBox_Species, &QComboBox::currentTextChanged, this, &WildMonChart::createLevelDistributionChart); + connect(ui->comboBox_Group, &QComboBox::currentTextChanged, this, &WildMonChart::createLevelDistributionChart); setTable(table); }; @@ -35,95 +36,132 @@ WildMonChart::~WildMonChart() { void WildMonChart::setTable(const EncounterTableModel *table) { this->table = table; - updateChart(); + readTable(); + createCharts(); } -static const bool showLevelRange = false; +void WildMonChart::clearTableData() { + this->groupNames.clear(); + this->tableIndexToGroupName.clear(); + this->speciesToGroupedData.clear(); + this->tableMinLevel = INT_MAX; + this->tableMaxLevel = INT_MIN; + setWindowTitle(baseWindowTitle); +} -void WildMonChart::updateChart() { +// Extract all the data from the table that we need for the charts +void WildMonChart::readTable() { + clearTableData(); if (!this->table) return; - setWindowTitle(QString("Wild Pokémon Summary -- %1").arg(this->table->encounterField().name)); + setWindowTitle(QString("%1 - %2").arg(baseWindowTitle).arg(this->table->encounterField().name)); - // Read data about encounter groups, e.g. for "fishing_mons" we want to know indexes 2-4 belong to good_rod (group index 1). - // Each group will be represented as a separate bar on the graph. - QList groupNames; - QMap tableIndexToGroupIndex; - int groupIndex = 0; + // Read data about encounter groups, e.g. for "fishing_mons" we want to know table indexes 2-4 belong to "good_rod" for (auto groupPair : this->table->encounterField().groups) { - groupNames.prepend(groupPair.first); - for (auto i : groupPair.second) { - tableIndexToGroupIndex.insert(i, groupIndex); - } - groupIndex++; + // Prepending names here instead of appending so that charts can match the order in the table visually. + this->groupNames.prepend(groupPair.first); + for (auto i : groupPair.second) + this->tableIndexToGroupName.insert(i, groupPair.first); } - const int numGroups = qMax(1, groupNames.length()); // Implicitly 1 group when none are listed + if (this->groupNames.isEmpty()) + this->groupNames.append(QString()); // Implicitly 1 unnamed group when none are listed - // Read data from the table, combining data for duplicate species entries - const QList tableValues = this->table->percentages(); + // Read data from the table, combining data for duplicate entries + const QList tableFrequencies = this->table->percentages(); const QVector tablePokemon = this->table->encounterData().wildPokemon; - QMap speciesToChartData; - for (int i = 0; i < qMin(tableValues.length(), tablePokemon.length()); i++) { - const double value = tableValues.at(i); - const WildPokemon pokemon = tablePokemon.at(i); - groupIndex = tableIndexToGroupIndex.value(i, 0); - - if (speciesToChartData.contains(pokemon.species)) { - // Duplicate species entry - ChartData *entry = &speciesToChartData[pokemon.species]; - entry->values[groupIndex] += value; - if (entry->minLevel > pokemon.minLevel) - entry->minLevel = pokemon.minLevel; - if (entry->maxLevel < pokemon.maxLevel) - entry->maxLevel = pokemon.maxLevel; - } else { - // New species entry - ChartData entry; - entry.minLevel = pokemon.minLevel; - entry.maxLevel = pokemon.maxLevel; - entry.values.insert(groupIndex, value); - speciesToChartData.insert(pokemon.species, entry); - } - } - - // Populate chart - QList barSets; + const int numRows = qMin(tableFrequencies.length(), tablePokemon.length()); const QString speciesPrefix = projectConfig.getIdentifier(ProjectIdentifier::define_species_prefix); - for (auto mapPair = speciesToChartData.cbegin(), end = speciesToChartData.cend(); mapPair != end; mapPair++) { - const ChartData entry = mapPair.value(); + for (int i = 0; i < numRows; i++) { + const double frequency = tableFrequencies.at(i); + const WildPokemon pokemon = tablePokemon.at(i); + const QString groupName = this->tableIndexToGroupName.value(i); - // Strip 'SPECIES_' prefix - QString label = mapPair.key(); + // Create species label (strip 'SPECIES_' prefix). + QString label = pokemon.species; if (label.startsWith(speciesPrefix)) label.remove(0, speciesPrefix.length()); - // Add level range to label - if (showLevelRange) { - if (entry.minLevel == entry.maxLevel) - label.append(QString(" (Lv %1)").arg(entry.minLevel)); - else - label.append(QString(" (Lv %1-%2)").arg(entry.minLevel).arg(entry.maxLevel)); - } + // Add species/level frequency data + Summary *summary = &this->speciesToGroupedData[label][groupName]; + summary->speciesFrequency += frequency; + if (pokemon.minLevel > pokemon.maxLevel) + continue; // Invalid + int numLevels = pokemon.maxLevel - pokemon.minLevel + 1; + for (int level = pokemon.minLevel; level <= pokemon.maxLevel; level++) + summary->levelFrequencies[level] += frequency / numLevels; - auto set = new QBarSet(label); + if (this->tableMinLevel > pokemon.minLevel) + this->tableMinLevel = pokemon.minLevel; + if (this->tableMaxLevel < pokemon.maxLevel) + this->tableMaxLevel = pokemon.maxLevel; + } - // Add encounter chance data (in reverse order, to match the table's group order visually) - for (int i = numGroups - 1; i >= 0; i--) - set->append(entry.values.value(i, 0)); + // Populate combo boxes + const QSignalBlocker blocker1(ui->comboBox_Species); + const QSignalBlocker blocker2(ui->comboBox_Group); + ui->comboBox_Species->clear(); + ui->comboBox_Species->addItems(getSpeciesNames()); + ui->comboBox_Group->clear(); + if (usesGroupLabels()) { + ui->comboBox_Group->addItems(this->groupNames); + ui->comboBox_Group->setEnabled(true); + } else { + ui->comboBox_Group->setEnabled(false); + } +} - // Insert bar set. We order them from lowest to highest total, left-to-right. - int i = 0; - for (; i < barSets.length(); i++){ - if (barSets.at(i)->sum() > set->sum()) +void WildMonChart::createCharts() { + createSpeciesDistributionChart(); + createLevelDistributionChart(); + + // Turn off the animation once it's played, otherwise it replays any time the window changes size. + // TODO: Store timer, disable if closing or creating new chart + //QTimer::singleShot(chart->animationDuration() + 500, this, &WildMonChart::stopChartAnimation); +} + +QStringList WildMonChart::getSpeciesNames() const { + return this->speciesToGroupedData.keys(); +} + +double WildMonChart::getSpeciesFrequency(const QString &species, const QString &groupName) const { + return this->speciesToGroupedData[species][groupName].speciesFrequency; +} + +QMap WildMonChart::getLevelFrequencies(const QString &species, const QString &groupName) const { + return this->speciesToGroupedData[species][groupName].levelFrequencies; +} + +bool WildMonChart::usesGroupLabels() const { + return this->groupNames.length() > 1; +} + +void WildMonChart::createSpeciesDistributionChart() { + QList barSets; + for (const auto species : getSpeciesNames()) { + // Add encounter chance data + auto set = new QBarSet(species); + for (auto groupName : this->groupNames) + set->append(getSpeciesFrequency(species, groupName) * 100); + + // We order the bar sets from lowest to highest total, left-to-right. + for (int i = 0; i < barSets.length() + 1; i++){ + if (i >= barSets.length() || barSets.at(i)->sum() > set->sum()) { + barSets.insert(i, set); break; + } } - barSets.insert(i, set); + + // Show species name and % when hovering over a bar set. This covers some shortfalls in our ability to control the chart design + // (i.e. bar segments may be too narrow to see the % label, or colors may be hard to match to the legend). + connect(set, &QBarSet::hovered, [set, species] (bool on, int i) { + QString text = on ? QString("%1 - %2%").arg(species).arg(set->at(i)) : ""; + QToolTip::showText(QCursor::pos(), text); + }); } auto series = new QHorizontalPercentBarSeries(); series->setLabelsVisible(); - //series->setLabelsPrecision(x); // This appears to have no effect for any value 'x'? Ideally we'd display 1-2 decimal places series->append(barSets); auto chart = new QChart(); @@ -135,29 +173,93 @@ void WildMonChart::updateChart() { // X-axis is the values (percentages). We're already showing percentages on the bar, so we just display 0/50/100% auto axisX = new QValueAxis(); -#if (QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)) - // Not critical, but the percentage ticks on the x-axis have no need for decimals. - // This property doesn't exist prior to Qt 6.7 - axisX->setLabelDecimals(0); -#endif + axisX->setLabelFormat("%u%%"); axisX->setTickCount(3); chart->addAxis(axisX, Qt::AlignBottom); series->attachAxis(axisX); // Y-axis is the names of encounter groups (e.g. Old Rod, Good Rod...) - if (numGroups > 1) { + if (usesGroupLabels()) { auto axisY = new QBarCategoryAxis(); - axisY->setCategories(groupNames); + axisY->setCategories(this->groupNames); chart->addAxis(axisY, Qt::AlignLeft); series->attachAxis(axisY); + } else { + // TODO: y-axis has weird labels for a few frames on opening } - delete ui->chartView->chart(); - ui->chartView->setChart(chart); - - // Turn off the animation once it's played, otherwise it replays any time the window changes size. - QTimer::singleShot(chart->animationDuration() + 500, [this] { - if (ui->chartView->chart()) - ui->chartView->chart()->setAnimationOptions(QChart::NoAnimation); - }); + // TODO: Delete old chart + ui->chartView_SpeciesDistribution->setChart(chart); +} + +void WildMonChart::createLevelDistributionChart() { + // TODO: Handle combined chart + const QString species = ui->comboBox_Species->currentText(); + const QString groupName = ui->comboBox_Group->currentText(); + + const double speciesFrequency = getSpeciesFrequency(species, groupName); + const QMap levelFrequencies = getLevelFrequencies(species, groupName); + const QList levels = levelFrequencies.keys(); + + int minLevel = !levels.isEmpty() ? levels.first() : 0; + int maxLevel = !levels.isEmpty() ? levels.last() : 0; + if (maxLevel < minLevel) + return; + + double maxPercent = 0.0; + QStringList categories; + auto set = new QBarSet(species); + for (int i = minLevel; i <= maxLevel; i++) { + double percent = (levelFrequencies.value(i, 0) / speciesFrequency) * 100; + if (maxPercent < percent) + maxPercent = percent; + set->append(percent); + categories.append(QString::number(i)); + } + + // Show level and % when hovering over a bar set. This covers some shortfalls in our ability to control the chart design. + connect(set, &QBarSet::hovered, [set, categories] (bool on, int i) { + QString text = on ? QString("Lv%1 - %2%").arg(categories.at(i)).arg(set->at(i)) : ""; + QToolTip::showText(QCursor::pos(), text); + }); + + auto series = new QBarSeries(); + series->append(set); + //series->setLabelsVisible(); + + auto chart = new QChart(); + chart->addSeries(series); + //chart->setTitle(""); + chart->setAnimationOptions(QChart::SeriesAnimations); + chart->legend()->setVisible(true); + chart->legend()->setShowToolTips(true); + chart->legend()->setAlignment(Qt::AlignBottom); + + QBarCategoryAxis *axisX = new QBarCategoryAxis(); + axisX->append(categories); + chart->addAxis(axisX, Qt::AlignBottom); + series->attachAxis(axisX); + + auto roundUp = [](int num, int multiple) { + auto remainder = num % multiple; + if (remainder == 0) + return num; + return num + multiple - remainder; + }; + + QValueAxis *axisY = new QValueAxis(); + axisY->setMax(roundUp(qCeil(maxPercent), 5)); + //axisY->setTickType(QValueAxis::TicksDynamic); + //axisY->setTickInterval(5); + axisY->setLabelFormat("%u%%"); + chart->addAxis(axisY, Qt::AlignLeft); + series->attachAxis(axisY); + + // TODO: Cache old chart + ui->chartView_LevelDistribution->setChart(chart); +} + +void WildMonChart::stopChartAnimation() { + if (ui->chartView_SpeciesDistribution->chart()) + ui->chartView_SpeciesDistribution->chart()->setAnimationOptions(QChart::NoAnimation); } From 8f6871dae09a3cba2fe269009ceb1114f2da51ba Mon Sep 17 00:00:00 2001 From: GriffinR Date: Fri, 23 Aug 2024 12:06:50 -0400 Subject: [PATCH 56/76] Add grouped level distribution chart --- forms/wildmonchart.ui | 50 +++++++++++--- include/ui/wildmonchart.h | 14 ++-- src/ui/wildmonchart.cpp | 140 +++++++++++++++++++++++++------------- 3 files changed, 141 insertions(+), 63 deletions(-) diff --git a/forms/wildmonchart.ui b/forms/wildmonchart.ui index 88d3a590..21652f53 100644 --- a/forms/wildmonchart.ui +++ b/forms/wildmonchart.ui @@ -28,6 +28,9 @@ + + 0 + Species Distribution @@ -111,20 +114,47 @@ - - - Species + + + Individual Mode - - - - - + true - - QComboBox::NoInsert + + false + + + 4 + + + 4 + + + 4 + + + 4 + + + + + Species + + + + + + + true + + + QComboBox::NoInsert + + + +
diff --git a/include/ui/wildmonchart.h b/include/ui/wildmonchart.h index 7c523355..59a23a80 100644 --- a/include/ui/wildmonchart.h +++ b/include/ui/wildmonchart.h @@ -28,16 +28,16 @@ private: QStringList groupNames; QMap tableIndexToGroupName; + struct LevelRange { + int min; + int max; + }; + QMap groupedLevelRanges; + struct Summary { double speciesFrequency = 0.0; QMap levelFrequencies; }; - - int tableMinLevel; - int tableMaxLevel; - - // GroupedData maps a group name ("old_rod", "good_rod"...) - // to any summarized data needed for the charts. typedef QMap GroupedData; QMap speciesToGroupedData; @@ -45,12 +45,14 @@ private: QStringList getSpeciesNames() const; double getSpeciesFrequency(const QString&, const QString&) const; QMap getLevelFrequencies(const QString &, const QString &) const; + LevelRange getLevelRange(const QString &species, const QString &groupName) const; bool usesGroupLabels() const; void clearTableData(); void readTable(); void createSpeciesDistributionChart(); void createLevelDistributionChart(); + QBarSet* createLevelDistributionBarSet(const QString &, const QString &, bool, double *); void stopChartAnimation(); }; diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp index b7080aae..c6642002 100644 --- a/src/ui/wildmonchart.cpp +++ b/src/ui/wildmonchart.cpp @@ -4,18 +4,12 @@ #include "log.h" -// TODO: Make level range its own chart(s)? // TODO: Draw species icons below legend icons? // TODO: NoScrollComboBoxes +// TODO: Consistent species->color across charts static const QString baseWindowTitle = QString("Wild Pokémon Summary Charts"); -struct ChartData { - int minLevel; - int maxLevel; - QMap valueMap; // One value for each wild encounter group -}; - WildMonChart::WildMonChart(QWidget *parent, const EncounterTableModel *table) : QWidget(parent), ui(new Ui::WildMonChart) @@ -24,6 +18,8 @@ WildMonChart::WildMonChart(QWidget *parent, const EncounterTableModel *table) : setAttribute(Qt::WA_DeleteOnClose); setWindowFlags(Qt::Window); + // Changing these settings changes which level distribution chart is shown + connect(ui->groupBox_Species, &QGroupBox::clicked, this, &WildMonChart::createLevelDistributionChart); connect(ui->comboBox_Species, &QComboBox::currentTextChanged, this, &WildMonChart::createLevelDistributionChart); connect(ui->comboBox_Group, &QComboBox::currentTextChanged, this, &WildMonChart::createLevelDistributionChart); @@ -44,8 +40,7 @@ void WildMonChart::clearTableData() { this->groupNames.clear(); this->tableIndexToGroupName.clear(); this->speciesToGroupedData.clear(); - this->tableMinLevel = INT_MAX; - this->tableMaxLevel = INT_MIN; + this->groupedLevelRanges.clear(); setWindowTitle(baseWindowTitle); } @@ -91,13 +86,22 @@ void WildMonChart::readTable() { for (int level = pokemon.minLevel; level <= pokemon.maxLevel; level++) summary->levelFrequencies[level] += frequency / numLevels; - if (this->tableMinLevel > pokemon.minLevel) - this->tableMinLevel = pokemon.minLevel; - if (this->tableMaxLevel < pokemon.maxLevel) - this->tableMaxLevel = pokemon.maxLevel; + // Update level min/max for showing level distribution across a group + if (!this->groupedLevelRanges.contains(groupName)) { + LevelRange *levelRange = &this->groupedLevelRanges[groupName]; + levelRange->min = pokemon.minLevel; + levelRange->max = pokemon.maxLevel; + } else { + LevelRange *levelRange = &this->groupedLevelRanges[groupName]; + if (levelRange->min > pokemon.minLevel) + levelRange->min = pokemon.minLevel; + if (levelRange->max < pokemon.maxLevel) + levelRange->max = pokemon.maxLevel; + } } // Populate combo boxes + // TODO: Limit width const QSignalBlocker blocker1(ui->comboBox_Species); const QSignalBlocker blocker2(ui->comboBox_Group); ui->comboBox_Species->clear(); @@ -132,6 +136,20 @@ QMap WildMonChart::getLevelFrequencies(const QString &species, cons return this->speciesToGroupedData[species][groupName].levelFrequencies; } +WildMonChart::LevelRange WildMonChart::getLevelRange(const QString &species, const QString &groupName) const { + const QList levels = getLevelFrequencies(species, groupName).keys(); + + LevelRange range; + if (levels.isEmpty()) { + range.min = 0; + range.max = 0; + } else { + range.min = levels.first(); + range.max = levels.last(); + } + return range; +} + bool WildMonChart::usesGroupLabels() const { return this->groupNames.length() > 1; } @@ -155,15 +173,17 @@ void WildMonChart::createSpeciesDistributionChart() { // Show species name and % when hovering over a bar set. This covers some shortfalls in our ability to control the chart design // (i.e. bar segments may be too narrow to see the % label, or colors may be hard to match to the legend). connect(set, &QBarSet::hovered, [set, species] (bool on, int i) { - QString text = on ? QString("%1 - %2%").arg(species).arg(set->at(i)) : ""; + QString text = on ? QString("%1 (%2%)").arg(species).arg(set->at(i)) : ""; QToolTip::showText(QCursor::pos(), text); }); } + // Set up series auto series = new QHorizontalPercentBarSeries(); series->setLabelsVisible(); series->append(barSets); + // Set up chart auto chart = new QChart(); chart->addSeries(series); chart->setAnimationOptions(QChart::SeriesAnimations); @@ -171,7 +191,7 @@ void WildMonChart::createSpeciesDistributionChart() { chart->legend()->setShowToolTips(true); chart->legend()->setAlignment(Qt::AlignBottom); - // X-axis is the values (percentages). We're already showing percentages on the bar, so we just display 0/50/100% + // X-axis is the % frequency. We're already showing percentages on the bar, so we just display 0/50/100% auto axisX = new QValueAxis(); axisX->setLabelFormat("%u%%"); axisX->setTickCount(3); @@ -184,76 +204,102 @@ void WildMonChart::createSpeciesDistributionChart() { axisY->setCategories(this->groupNames); chart->addAxis(axisY, Qt::AlignLeft); series->attachAxis(axisY); - } else { - // TODO: y-axis has weird labels for a few frames on opening } // TODO: Delete old chart ui->chartView_SpeciesDistribution->setChart(chart); } -void WildMonChart::createLevelDistributionChart() { - // TODO: Handle combined chart - const QString species = ui->comboBox_Species->currentText(); - const QString groupName = ui->comboBox_Group->currentText(); - - const double speciesFrequency = getSpeciesFrequency(species, groupName); +QBarSet* WildMonChart::createLevelDistributionBarSet(const QString &species, const QString &groupName, bool individual, double *barMax) { + const double totalFrequency = individual ? getSpeciesFrequency(species, groupName) : 1.0; const QMap levelFrequencies = getLevelFrequencies(species, groupName); - const QList levels = levelFrequencies.keys(); - int minLevel = !levels.isEmpty() ? levels.first() : 0; - int maxLevel = !levels.isEmpty() ? levels.last() : 0; - if (maxLevel < minLevel) - return; - - double maxPercent = 0.0; - QStringList categories; auto set = new QBarSet(species); - for (int i = minLevel; i <= maxLevel; i++) { - double percent = (levelFrequencies.value(i, 0) / speciesFrequency) * 100; - if (maxPercent < percent) - maxPercent = percent; + LevelRange levelRange = individual ? getLevelRange(species, groupName) : this->groupedLevelRanges.value(groupName); + for (int i = levelRange.min; i <= levelRange.max; i++) { + double percent = levelFrequencies.value(i, 0) / totalFrequency * 100; + if (*barMax < percent) + *barMax = percent; set->append(percent); - categories.append(QString::number(i)); } - // Show level and % when hovering over a bar set. This covers some shortfalls in our ability to control the chart design. - connect(set, &QBarSet::hovered, [set, categories] (bool on, int i) { - QString text = on ? QString("Lv%1 - %2%").arg(categories.at(i)).arg(set->at(i)) : ""; + // Show data when hovering over a bar set. This covers some shortfalls in our ability to control the chart design. + connect(set, &QBarSet::hovered, [=] (bool on, int i) { + QString text = on ? QString("%1 Lv%2 (%3%)") + .arg(individual ? "" : species) + .arg(QString::number(i + levelRange.min)) + .arg(set->at(i)) + : ""; QToolTip::showText(QCursor::pos(), text); }); - auto series = new QBarSeries(); - series->append(set); - //series->setLabelsVisible(); + return set; +} +void WildMonChart::createLevelDistributionChart() { + const QString groupName = ui->comboBox_Group->currentText(); + + LevelRange levelRange; + double maxPercent = 0.0; + QList barSets; + + // Create bar sets + if (ui->groupBox_Species->isChecked()) { + // Species box is active, we just display data for the selected species. + const QString species = ui->comboBox_Species->currentText(); + barSets.append(createLevelDistributionBarSet(species, groupName, true, &maxPercent)); + levelRange = getLevelRange(species, groupName); + } else { + // Species box is inactive, we display data for all species in the table. + for (const auto species : getSpeciesNames()) + barSets.append(createLevelDistributionBarSet(species, groupName, false, &maxPercent)); + levelRange = this->groupedLevelRanges.value(groupName); + } + + // Set up chart auto chart = new QChart(); - chart->addSeries(series); //chart->setTitle(""); chart->setAnimationOptions(QChart::SeriesAnimations); chart->legend()->setVisible(true); chart->legend()->setShowToolTips(true); chart->legend()->setAlignment(Qt::AlignBottom); + // X-axis is the level range. QBarCategoryAxis *axisX = new QBarCategoryAxis(); - axisX->append(categories); + for (int i = levelRange.min; i <= levelRange.max; i++) + axisX->append(QString::number(i)); chart->addAxis(axisX, Qt::AlignBottom); - series->attachAxis(axisX); + // Y-axis is the % frequency. We round the max up to a multiple of 5. auto roundUp = [](int num, int multiple) { auto remainder = num % multiple; if (remainder == 0) return num; return num + multiple - remainder; }; - QValueAxis *axisY = new QValueAxis(); axisY->setMax(roundUp(qCeil(maxPercent), 5)); //axisY->setTickType(QValueAxis::TicksDynamic); //axisY->setTickInterval(5); axisY->setLabelFormat("%u%%"); chart->addAxis(axisY, Qt::AlignLeft); - series->attachAxis(axisY); + + // Set up series. Grouped mode uses a stacked bar series. + if (barSets.length() < 2) { + auto series = new QBarSeries(); + series->append(barSets); + series->attachAxis(axisX); + series->attachAxis(axisY); + //series->setLabelsVisible(); + chart->addSeries(series); + } else { + auto series = new QStackedBarSeries(); + series->append(barSets); + series->attachAxis(axisX); + series->attachAxis(axisY); + //series->setLabelsVisible(); + chart->addSeries(series); + } // TODO: Cache old chart ui->chartView_LevelDistribution->setChart(chart); From 7c9fcb6616b919593ba633aba9221802d495aad0 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Fri, 23 Aug 2024 13:15:48 -0400 Subject: [PATCH 57/76] Add chart theme selector --- forms/wildmonchart.ui | 63 +++++++++++++++++++++++++-------- include/ui/wildmonchart.h | 7 +++- src/ui/wildmonchart.cpp | 73 +++++++++++++++++++++++++++++++++++---- 3 files changed, 122 insertions(+), 21 deletions(-) diff --git a/forms/wildmonchart.ui b/forms/wildmonchart.ui index 21652f53..b01b8951 100644 --- a/forms/wildmonchart.ui +++ b/forms/wildmonchart.ui @@ -6,26 +6,61 @@ 0 0 - 776 - 445 + 785 + 492 Wild Pokémon Summary - - 4 - - - 4 - - - 4 - - - 4 - + + + + QFrame::NoFrame + + + QFrame::Plain + + + + 0 + + + 0 + + + 0 + + + 4 + + + + + Theme + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + diff --git a/include/ui/wildmonchart.h b/include/ui/wildmonchart.h index 59a23a80..39d59ba1 100644 --- a/include/ui/wildmonchart.h +++ b/include/ui/wildmonchart.h @@ -41,11 +41,13 @@ private: typedef QMap GroupedData; QMap speciesToGroupedData; + QMap speciesToColor; + QStringList getSpeciesNames() const; double getSpeciesFrequency(const QString&, const QString&) const; QMap getLevelFrequencies(const QString &, const QString &) const; - LevelRange getLevelRange(const QString &species, const QString &groupName) const; + LevelRange getLevelRange(const QString &, const QString &) const; bool usesGroupLabels() const; void clearTableData(); @@ -54,6 +56,9 @@ private: void createLevelDistributionChart(); QBarSet* createLevelDistributionBarSet(const QString &, const QString &, bool, double *); + void applySpeciesColors(QAbstractBarSeries *); + QChart::ChartTheme currentTheme() const; + void updateTheme(); void stopChartAnimation(); }; diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp index c6642002..526fc637 100644 --- a/src/ui/wildmonchart.cpp +++ b/src/ui/wildmonchart.cpp @@ -6,10 +6,22 @@ // TODO: Draw species icons below legend icons? // TODO: NoScrollComboBoxes -// TODO: Consistent species->color across charts +// TODO: Save window size, theme selection in config +// TODO: Help button that explains the charts static const QString baseWindowTitle = QString("Wild Pokémon Summary Charts"); +static const QList> themes = { + {"Light", QChart::ChartThemeLight}, + {"Dark", QChart::ChartThemeDark}, + {"Blue Cerulean", QChart::ChartThemeBlueCerulean}, + {"Brown Sand", QChart::ChartThemeBrownSand}, + {"Blue NCS", QChart::ChartThemeBlueNcs}, + {"High Contrast", QChart::ChartThemeHighContrast}, + {"Blue Icy", QChart::ChartThemeBlueIcy}, + {"Qt", QChart::ChartThemeQt}, +}; + WildMonChart::WildMonChart(QWidget *parent, const EncounterTableModel *table) : QWidget(parent), ui(new Ui::WildMonChart) @@ -23,6 +35,11 @@ WildMonChart::WildMonChart(QWidget *parent, const EncounterTableModel *table) : connect(ui->comboBox_Species, &QComboBox::currentTextChanged, this, &WildMonChart::createLevelDistributionChart); connect(ui->comboBox_Group, &QComboBox::currentTextChanged, this, &WildMonChart::createLevelDistributionChart); + // Set up Theme combo box + for (auto i : themes) + ui->comboBox_Theme->addItem(i.first, i.second); + connect(ui->comboBox_Theme, &QComboBox::currentTextChanged, this, &WildMonChart::updateTheme); + setTable(table); }; @@ -39,8 +56,9 @@ void WildMonChart::setTable(const EncounterTableModel *table) { void WildMonChart::clearTableData() { this->groupNames.clear(); this->tableIndexToGroupName.clear(); - this->speciesToGroupedData.clear(); this->groupedLevelRanges.clear(); + this->speciesToGroupedData.clear(); + this->speciesToColor.clear(); setWindowTitle(baseWindowTitle); } @@ -172,8 +190,8 @@ void WildMonChart::createSpeciesDistributionChart() { // Show species name and % when hovering over a bar set. This covers some shortfalls in our ability to control the chart design // (i.e. bar segments may be too narrow to see the % label, or colors may be hard to match to the legend). - connect(set, &QBarSet::hovered, [set, species] (bool on, int i) { - QString text = on ? QString("%1 (%2%)").arg(species).arg(set->at(i)) : ""; + connect(set, &QBarSet::hovered, [set] (bool on, int i) { + QString text = on ? QString("%1 (%2%)").arg(set->label()).arg(set->at(i)) : ""; QToolTip::showText(QCursor::pos(), text); }); } @@ -186,6 +204,7 @@ void WildMonChart::createSpeciesDistributionChart() { // Set up chart auto chart = new QChart(); chart->addSeries(series); + chart->setTheme(currentTheme()); chart->setAnimationOptions(QChart::SeriesAnimations); chart->legend()->setVisible(true); chart->legend()->setShowToolTips(true); @@ -206,6 +225,8 @@ void WildMonChart::createSpeciesDistributionChart() { series->attachAxis(axisY); } + applySpeciesColors(series); + // TODO: Delete old chart ui->chartView_SpeciesDistribution->setChart(chart); } @@ -226,7 +247,7 @@ QBarSet* WildMonChart::createLevelDistributionBarSet(const QString &species, con // Show data when hovering over a bar set. This covers some shortfalls in our ability to control the chart design. connect(set, &QBarSet::hovered, [=] (bool on, int i) { QString text = on ? QString("%1 Lv%2 (%3%)") - .arg(individual ? "" : species) + .arg(individual ? "" : set->label()) .arg(QString::number(i + levelRange.min)) .arg(set->at(i)) : ""; @@ -259,6 +280,7 @@ void WildMonChart::createLevelDistributionChart() { // Set up chart auto chart = new QChart(); //chart->setTitle(""); + chart->setTheme(currentTheme()); chart->setAnimationOptions(QChart::SeriesAnimations); chart->legend()->setVisible(true); chart->legend()->setShowToolTips(true); @@ -278,7 +300,7 @@ void WildMonChart::createLevelDistributionChart() { return num + multiple - remainder; }; QValueAxis *axisY = new QValueAxis(); - axisY->setMax(roundUp(qCeil(maxPercent), 5)); + axisY->setMax(roundUp(qCeil(maxPercent), 5)); // TODO: This isn't taking stacking into account //axisY->setTickType(QValueAxis::TicksDynamic); //axisY->setTickInterval(5); axisY->setLabelFormat("%u%%"); @@ -292,6 +314,7 @@ void WildMonChart::createLevelDistributionChart() { series->attachAxis(axisY); //series->setLabelsVisible(); chart->addSeries(series); + applySpeciesColors(series); } else { auto series = new QStackedBarSeries(); series->append(barSets); @@ -299,12 +322,50 @@ void WildMonChart::createLevelDistributionChart() { series->attachAxis(axisY); //series->setLabelsVisible(); chart->addSeries(series); + applySpeciesColors(series); } // TODO: Cache old chart ui->chartView_LevelDistribution->setChart(chart); } +QChart::ChartTheme WildMonChart::currentTheme() const { + return static_cast(ui->comboBox_Theme->currentData().toInt()); +} + +void WildMonChart::updateTheme() { + auto theme = currentTheme(); + + // In order to keep the color of each species in the legend consistent across + // charts we save species->color mappings. The legend colors are overwritten + // when we change themes, so we need to recalculate them. We let the species + // distribution chart determine what those mapping are (it always includes every + // species in the table) and then we apply those mappings to subsequent charts. + QChart *chart = ui->chartView_SpeciesDistribution->chart(); + if (!chart) + return; + this->speciesToColor.clear(); + chart->setTheme(theme); + applySpeciesColors(static_cast(chart->series().at(0))); + + chart = ui->chartView_LevelDistribution->chart(); + if (chart) { + chart->setTheme(theme); + applySpeciesColors(static_cast(chart->series().at(0))); + } +} + +void WildMonChart::applySpeciesColors(QAbstractBarSeries *series) { + for (auto set : series->barSets()) { + const QString species = set->label(); + if (speciesToColor.contains(species)) { + set->setColor(speciesToColor.value(species)); + } else { + speciesToColor.insert(species, set->color()); + } + } +} + void WildMonChart::stopChartAnimation() { if (ui->chartView_SpeciesDistribution->chart()) ui->chartView_SpeciesDistribution->chart()->setAnimationOptions(QChart::NoAnimation); From 6f74909a3cf4c0f2b83dc0f6333b258e0a1a67cc Mon Sep 17 00:00:00 2001 From: GriffinR Date: Fri, 23 Aug 2024 15:00:00 -0400 Subject: [PATCH 58/76] Fixes for some visuals in chart window --- forms/wildmonchart.ui | 12 ++++++++++ include/ui/wildmonchart.h | 4 +++- src/ui/wildmonchart.cpp | 47 ++++++++++++++++++++++++--------------- 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/forms/wildmonchart.ui b/forms/wildmonchart.ui index b01b8951..87d626ed 100644 --- a/forms/wildmonchart.ui +++ b/forms/wildmonchart.ui @@ -133,6 +133,12 @@ false + + QComboBox::AdjustToMinimumContentsLength + + + 8 + @@ -187,6 +193,12 @@ QComboBox::NoInsert + + QComboBox::AdjustToMinimumContentsLengthWithIcon + + + 12 + diff --git a/include/ui/wildmonchart.h b/include/ui/wildmonchart.h index 39d59ba1..4de8d159 100644 --- a/include/ui/wildmonchart.h +++ b/include/ui/wildmonchart.h @@ -26,6 +26,8 @@ private: const EncounterTableModel *table; QStringList groupNames; + QStringList groupNamesReversed; + QStringList speciesInLegendOrder; QMap tableIndexToGroupName; struct LevelRange { @@ -44,7 +46,7 @@ private: QMap speciesToColor; - QStringList getSpeciesNames() const; + QStringList getSpeciesNamesAlphabetical() const; double getSpeciesFrequency(const QString&, const QString&) const; QMap getLevelFrequencies(const QString &, const QString &) const; LevelRange getLevelRange(const QString &, const QString &) const; diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp index 526fc637..f2fd0afb 100644 --- a/src/ui/wildmonchart.cpp +++ b/src/ui/wildmonchart.cpp @@ -5,7 +5,6 @@ #include "log.h" // TODO: Draw species icons below legend icons? -// TODO: NoScrollComboBoxes // TODO: Save window size, theme selection in config // TODO: Help button that explains the charts @@ -55,11 +54,18 @@ void WildMonChart::setTable(const EncounterTableModel *table) { void WildMonChart::clearTableData() { this->groupNames.clear(); + this->groupNamesReversed.clear(); + this->speciesInLegendOrder.clear(); this->tableIndexToGroupName.clear(); this->groupedLevelRanges.clear(); this->speciesToGroupedData.clear(); this->speciesToColor.clear(); setWindowTitle(baseWindowTitle); + + const QSignalBlocker blocker1(ui->comboBox_Species); + const QSignalBlocker blocker2(ui->comboBox_Group); + ui->comboBox_Species->clear(); + ui->comboBox_Group->clear(); } // Extract all the data from the table that we need for the charts @@ -72,13 +78,18 @@ void WildMonChart::readTable() { // Read data about encounter groups, e.g. for "fishing_mons" we want to know table indexes 2-4 belong to "good_rod" for (auto groupPair : this->table->encounterField().groups) { - // Prepending names here instead of appending so that charts can match the order in the table visually. - this->groupNames.prepend(groupPair.first); + // Frustratingly when adding categories to the charts they insert bottom-to-top, but the table and combo box + // insert top-to-bottom. To keep the order visually the same across displays we keep separate ordered lists. + this->groupNames.append(groupPair.first); + this->groupNamesReversed.prepend(groupPair.first); for (auto i : groupPair.second) this->tableIndexToGroupName.insert(i, groupPair.first); } - if (this->groupNames.isEmpty()) - this->groupNames.append(QString()); // Implicitly 1 unnamed group when none are listed + // Implicitly 1 unnamed group when none are listed + if (this->groupNames.isEmpty()) { + this->groupNames.append(QString()); + this->groupNamesReversed.append(QString()); + } // Read data from the table, combining data for duplicate entries const QList tableFrequencies = this->table->percentages(); @@ -119,18 +130,13 @@ void WildMonChart::readTable() { } // Populate combo boxes - // TODO: Limit width const QSignalBlocker blocker1(ui->comboBox_Species); const QSignalBlocker blocker2(ui->comboBox_Group); ui->comboBox_Species->clear(); - ui->comboBox_Species->addItems(getSpeciesNames()); + ui->comboBox_Species->addItems(getSpeciesNamesAlphabetical()); ui->comboBox_Group->clear(); - if (usesGroupLabels()) { - ui->comboBox_Group->addItems(this->groupNames); - ui->comboBox_Group->setEnabled(true); - } else { - ui->comboBox_Group->setEnabled(false); - } + ui->comboBox_Group->addItems(this->groupNames); + ui->comboBox_Group->setEnabled(usesGroupLabels()); } void WildMonChart::createCharts() { @@ -142,7 +148,7 @@ void WildMonChart::createCharts() { //QTimer::singleShot(chart->animationDuration() + 500, this, &WildMonChart::stopChartAnimation); } -QStringList WildMonChart::getSpeciesNames() const { +QStringList WildMonChart::getSpeciesNamesAlphabetical() const { return this->speciesToGroupedData.keys(); } @@ -174,10 +180,10 @@ bool WildMonChart::usesGroupLabels() const { void WildMonChart::createSpeciesDistributionChart() { QList barSets; - for (const auto species : getSpeciesNames()) { + for (const auto species : getSpeciesNamesAlphabetical()) { // Add encounter chance data auto set = new QBarSet(species); - for (auto groupName : this->groupNames) + for (auto groupName : this->groupNamesReversed) set->append(getSpeciesFrequency(species, groupName) * 100); // We order the bar sets from lowest to highest total, left-to-right. @@ -196,6 +202,11 @@ void WildMonChart::createSpeciesDistributionChart() { }); } + // Preserve the order we set earlier so that the legend isn't shuffling around for the other all-species charts. + this->speciesInLegendOrder.clear(); + for (auto set : barSets) + this->speciesInLegendOrder.append(set->label()); + // Set up series auto series = new QHorizontalPercentBarSeries(); series->setLabelsVisible(); @@ -220,7 +231,7 @@ void WildMonChart::createSpeciesDistributionChart() { // Y-axis is the names of encounter groups (e.g. Old Rod, Good Rod...) if (usesGroupLabels()) { auto axisY = new QBarCategoryAxis(); - axisY->setCategories(this->groupNames); + axisY->setCategories(this->groupNamesReversed); chart->addAxis(axisY, Qt::AlignLeft); series->attachAxis(axisY); } @@ -272,7 +283,7 @@ void WildMonChart::createLevelDistributionChart() { levelRange = getLevelRange(species, groupName); } else { // Species box is inactive, we display data for all species in the table. - for (const auto species : getSpeciesNames()) + for (const auto species : this->speciesInLegendOrder) barSets.append(createLevelDistributionBarSet(species, groupName, false, &maxPercent)); levelRange = this->groupedLevelRanges.value(groupName); } From 2ec9012c07a6c6ede53f9e694eda96b8de43bf0c Mon Sep 17 00:00:00 2001 From: GriffinR Date: Fri, 23 Aug 2024 16:00:42 -0400 Subject: [PATCH 59/76] Save summary chart settings in config --- include/config.h | 3 +++ include/ui/wildmonchart.h | 2 ++ src/config.cpp | 6 ++++++ src/ui/wildmonchart.cpp | 37 ++++++++++++++++++++++++++++++++++--- 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/include/config.h b/include/config.h index bf80ecb1..0e5e0acb 100644 --- a/include/config.h +++ b/include/config.h @@ -72,6 +72,7 @@ public: this->monitorFiles = true; this->tilesetCheckerboardFill = true; this->theme = "default"; + this->wildMonChartTheme = ""; this->textEditorOpenFolder = ""; this->textEditorGotoLine = ""; this->paletteEditorBitDepth = 24; @@ -117,6 +118,7 @@ public: bool monitorFiles; bool tilesetCheckerboardFill; QString theme; + QString wildMonChartTheme; QString textEditorOpenFolder; QString textEditorGotoLine; int paletteEditorBitDepth; @@ -126,6 +128,7 @@ public: QDateTime lastUpdateCheckTime; QVersionNumber lastUpdateCheckVersion; QMap rateLimitTimes; + QByteArray wildMonChartGeometry; protected: virtual QString getConfigFilepath() override; diff --git a/include/ui/wildmonchart.h b/include/ui/wildmonchart.h index 4de8d159..58371219 100644 --- a/include/ui/wildmonchart.h +++ b/include/ui/wildmonchart.h @@ -17,6 +17,8 @@ public: explicit WildMonChart(QWidget *parent, const EncounterTableModel *table); ~WildMonChart(); + virtual void closeEvent(QCloseEvent *event) override; + public slots: void setTable(const EncounterTableModel *table); void createCharts(); diff --git a/src/config.cpp b/src/config.cpp index fae8bac7..8335126f 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -354,6 +354,8 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) { this->customScriptsEditorGeometry = bytesFromString(value); } else if (key == "custom_scripts_editor_state") { this->customScriptsEditorState = bytesFromString(value); + } else if (key == "wild_mon_chart_geometry") { + this->wildMonChartGeometry = bytesFromString(value); } else if (key == "metatiles_zoom") { this->metatilesZoom = getConfigInteger(key, value, 10, 100, 30); } else if (key == "collision_zoom") { @@ -380,6 +382,8 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) { this->tilesetCheckerboardFill = getConfigBool(key, value); } else if (key == "theme") { this->theme = value; + } else if (key == "wild_mon_chart_theme") { + this->wildMonChartTheme = value; } else if (key == "text_editor_open_directory") { this->textEditorOpenFolder = value; } else if (key == "text_editor_goto_line") { @@ -439,6 +443,7 @@ QMap PorymapConfig::getKeyValueMap() { map.insert("project_settings_editor_state", stringFromByteArray(this->projectSettingsEditorState)); map.insert("custom_scripts_editor_geometry", stringFromByteArray(this->customScriptsEditorGeometry)); map.insert("custom_scripts_editor_state", stringFromByteArray(this->customScriptsEditorState)); + map.insert("wild_mon_chart_geometry", stringFromByteArray(this->wildMonChartGeometry)); map.insert("collision_opacity", QString::number(this->collisionOpacity)); map.insert("collision_zoom", QString::number(this->collisionZoom)); map.insert("metatiles_zoom", QString::number(this->metatilesZoom)); @@ -453,6 +458,7 @@ QMap PorymapConfig::getKeyValueMap() { map.insert("monitor_files", this->monitorFiles ? "1" : "0"); map.insert("tileset_checkerboard_fill", this->tilesetCheckerboardFill ? "1" : "0"); map.insert("theme", this->theme); + map.insert("wild_mon_chart_theme", this->wildMonChartTheme); map.insert("text_editor_open_directory", this->textEditorOpenFolder); map.insert("text_editor_goto_line", this->textEditorGotoLine); map.insert("palette_editor_bit_depth", QString::number(this->paletteEditorBitDepth)); diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp index f2fd0afb..9c1a17d5 100644 --- a/src/ui/wildmonchart.cpp +++ b/src/ui/wildmonchart.cpp @@ -5,7 +5,6 @@ #include "log.h" // TODO: Draw species icons below legend icons? -// TODO: Save window size, theme selection in config // TODO: Help button that explains the charts static const QString baseWindowTitle = QString("Wild Pokémon Summary Charts"); @@ -39,6 +38,18 @@ WildMonChart::WildMonChart(QWidget *parent, const EncounterTableModel *table) : ui->comboBox_Theme->addItem(i.first, i.second); connect(ui->comboBox_Theme, &QComboBox::currentTextChanged, this, &WildMonChart::updateTheme); + // User's theme choice is saved in the config + int configThemeIndex = ui->comboBox_Theme->findText(porymapConfig.wildMonChartTheme); + if (configThemeIndex >= 0) { + const QSignalBlocker blocker(ui->comboBox_Theme); + ui->comboBox_Theme->setCurrentIndex(configThemeIndex); + } else { + porymapConfig.wildMonChartTheme = ui->comboBox_Theme->currentText(); + } + + // TODO: Re-enable once finished + //restoreGeometry(porymapConfig.wildMonChartGeometry); + setTable(table); }; @@ -238,7 +249,8 @@ void WildMonChart::createSpeciesDistributionChart() { applySpeciesColors(series); - // TODO: Delete old chart + if (ui->chartView_SpeciesDistribution->chart()) + ui->chartView_SpeciesDistribution->chart()->deleteLater(); ui->chartView_SpeciesDistribution->setChart(chart); } @@ -265,6 +277,17 @@ QBarSet* WildMonChart::createLevelDistributionBarSet(const QString &species, con QToolTip::showText(QCursor::pos(), text); }); + // Clicking on a bar set in the stacked chart opens its individual chart + if (!individual) { + connect(set, &QBarSet::clicked, [this, species](int) { + const QSignalBlocker blocker1(ui->groupBox_Species); + const QSignalBlocker blocker2(ui->comboBox_Species); + ui->groupBox_Species->setChecked(true); + ui->comboBox_Species->setCurrentText(species); + createLevelDistributionChart(); + }); + } + return set; } @@ -336,7 +359,9 @@ void WildMonChart::createLevelDistributionChart() { applySpeciesColors(series); } - // TODO: Cache old chart + // TODO: Cache old charts? + if (ui->chartView_LevelDistribution->chart()) + ui->chartView_LevelDistribution->chart()->deleteLater(); ui->chartView_LevelDistribution->setChart(chart); } @@ -346,6 +371,7 @@ QChart::ChartTheme WildMonChart::currentTheme() const { void WildMonChart::updateTheme() { auto theme = currentTheme(); + porymapConfig.wildMonChartTheme = ui->comboBox_Theme->currentText(); // In order to keep the color of each species in the legend consistent across // charts we save species->color mappings. The legend colors are overwritten @@ -381,3 +407,8 @@ void WildMonChart::stopChartAnimation() { if (ui->chartView_SpeciesDistribution->chart()) ui->chartView_SpeciesDistribution->chart()->setAnimationOptions(QChart::NoAnimation); } + +void WildMonChart::closeEvent(QCloseEvent *event) { + porymapConfig.wildMonChartGeometry = saveGeometry(); + QWidget::closeEvent(event); +} From 76e5fd4834c865c0d8cb4b1c48214ad3cc0b06a3 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Fri, 23 Aug 2024 16:32:37 -0400 Subject: [PATCH 60/76] Add summary charts help button --- forms/wildmonchart.ui | 19 ++++++++++++++++--- include/ui/wildmonchart.h | 2 ++ resources/icons/help.ico | Bin 0 -> 2271 bytes resources/images.qrc | 1 + src/ui/wildmonchart.cpp | 15 ++++++++++++++- 5 files changed, 33 insertions(+), 4 deletions(-) create mode 100755 resources/icons/help.ico diff --git a/forms/wildmonchart.ui b/forms/wildmonchart.ui index 87d626ed..488e066e 100644 --- a/forms/wildmonchart.ui +++ b/forms/wildmonchart.ui @@ -58,6 +58,17 @@ + + + + ... + + + + :/icons/help.ico:/icons/help.ico + + +
@@ -66,7 +77,7 @@ 0 - + Species Distribution @@ -89,7 +100,7 @@
- + Level Distribution @@ -227,6 +238,8 @@
QtCharts
- + + + diff --git a/include/ui/wildmonchart.h b/include/ui/wildmonchart.h index 58371219..087eb3ea 100644 --- a/include/ui/wildmonchart.h +++ b/include/ui/wildmonchart.h @@ -64,6 +64,8 @@ private: QChart::ChartTheme currentTheme() const; void updateTheme(); void stopChartAnimation(); + + void showHelpDialog(); }; #endif // WILDMONCHART_H diff --git a/resources/icons/help.ico b/resources/icons/help.ico new file mode 100755 index 0000000000000000000000000000000000000000..68f51bac41f5c0f5b1c9580bc9ee012c480aadc6 GIT binary patch literal 2271 zcmV<52q5=~P)g4LrApCOKh#d!=@; zWh)jmd$6Fz1B<30m&?O2a>yBZ#FIL%`6e(lI*GHF{V=k6kCdF)6#UzZgQO?_RRAc} zPw)GaqFG*e`iT`-yRrp#3*fW^b{l!iTJr@V`^e^iNZi0gB!dvinZ6r1dc2>3{3ZDQ z)|cpd?tTDhmD@h_Em_!5|J+ln(O9FRsvJ0RDU1`n{ zUW=t|RT#OMM87YL&pz+Rs6R9|vG=JKx~AU?0IhuM{^2c;uV`4mZUGuSz%_pYzx(JM z!qXXOiVRtiDBse|%XtxvyhuehXQ13+#r6%IsIPM1{6GjNFOK4ivxEN0_qTX&2cg{Q z`TpyhA74?=#=Z5x?jwWPd8kiVv0GGVsvJzlSdRG(68?N(0H2>9Lzz`0!zvUqAc?q3 zB>^l5#RM{g5#VG2)Hgnjsb~t1KGck4I*;+mxHIqlPD7%n`;b|fCIG48kyqQb%9gi( z_Ut#&)~I6NQ6K(&!A}Lp#tX_h=~)VOiabn`cPZUKn*Qn(dV|g-j)VYF5RQm{GC{_5 ztm&*nJS$O`Os%wf7JiT%IyT9MRWtN*O;`8k^~)*eR`>!*{PWBxd0z0Ee+C)R$*{+1 z!Ed%K#L5=;tlXXUMe+KfAq?D1z*dqP27dDLI6A%6=U=ZXV+Up!R^2=3s80X(AI{WaY#li3dowJ!$CIrJqDoII=;Vs&9Wx)7&t!^B~rJj%=z9QFGH51#qXF=;1s6PSqudtsI9c4uGWpRC7ZUhZ3bY; zYtCo9O;vC?6!cu1BGd)+GgU5V7VvuP!iY|2e)Mi1E>U~-`D1u?XD>!V=|UyTh3ew_ zGXRR+am^n_y~hQIgP6?XdWLOt4cPLsrK7pA5-!p|G!`bBRYeh*KPqb2DXhQPKY)&U z2mIkQohfi?lBkO+s`&D@lh?wLQkm%Ss0ZBV`iy! zi_aw0q>~&eWW_3lmWkwc0Hr^s+zgGDzy72P6~)j8r**t?U=Y_s8H%eq%X?L+D^ld1@+APWhJ>~AZ*jq!$NZu$Y?ix6j=gtTS$7s0(r;c9V;#BVz_5P` z6JZ_A^IXLqmhURuRs+G*jPfw?hK)K$Q!E7ZXo zW34~;Pb4tE-c9`)6)Bj@84*dsee7xkhkK{QnUjjHj6z@9y0^c>`I%Sg#F_p8(kUI; z@W?^7%>Yud)9>!NQrjT_EN-cXt4u@sE@|Lg>ZJ08b@lk}(kgNGkBcGfK6$gSBS>?H z-%bmf>a3e%@{?qS=q=)hqAcrFzm?7JGE3lV&6X`>jh(j2K^J}{`5Oym6H z|MM9?o9jM@znyl;QjqTISC6<4-uw*w7TF6{oa7m=xH zWT!?BM2`RQJ$l3ilq+3roFuKDAM882cu`Yz=b}11wz>&dN5dEiMoFPOlV9c$o&h|R zOz#V8T1nB-ztt`WjrTI_I(!jhGzsW{^COMJED# zwrwOEBP3CCFH~;pmJCnZL~r+}>4x=BPDRr8(J3A-Dwem^iYivbFd^3DW;FK&(qbc{ zZl?{kvBE(;%ll)7u=5*eEI5h^}icons/folder_map_opened.ico icons/folder_map.ico icons/folder.ico + icons/help.ico icons/map_edited.ico icons/map_opened.ico icons/map.ico diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp index 9c1a17d5..a69f2f2b 100644 --- a/src/ui/wildmonchart.cpp +++ b/src/ui/wildmonchart.cpp @@ -5,7 +5,6 @@ #include "log.h" // TODO: Draw species icons below legend icons? -// TODO: Help button that explains the charts static const QString baseWindowTitle = QString("Wild Pokémon Summary Charts"); @@ -28,6 +27,8 @@ WildMonChart::WildMonChart(QWidget *parent, const EncounterTableModel *table) : setAttribute(Qt::WA_DeleteOnClose); setWindowFlags(Qt::Window); + connect(ui->button_Help, &QAbstractButton::clicked, this, &WildMonChart::showHelpDialog); + // Changing these settings changes which level distribution chart is shown connect(ui->groupBox_Species, &QGroupBox::clicked, this, &WildMonChart::createLevelDistributionChart); connect(ui->comboBox_Species, &QComboBox::currentTextChanged, this, &WildMonChart::createLevelDistributionChart); @@ -408,6 +409,18 @@ void WildMonChart::stopChartAnimation() { ui->chartView_SpeciesDistribution->chart()->setAnimationOptions(QChart::NoAnimation); } +void WildMonChart::showHelpDialog() { + static const QString text = "This window provides some visualizations of the data in your current Wild Pokémon tab"; + static const QString informative = "The Species Distribution tab shows the cumulative encounter chance for each species " + "in the table. In other words, it answers the question \"For a given encounter of this type, " + "what is the likelihood that the pokémon encountered will be of that species?\"

" + "The Level Distribution tab..."; // TODO + QMessageBox msgBox(QMessageBox::Information, "porymap", text, QMessageBox::Close, this); + msgBox.setTextFormat(Qt::RichText); + msgBox.setInformativeText(informative); + msgBox.exec(); +} + void WildMonChart::closeEvent(QCloseEvent *event) { porymapConfig.wildMonChartGeometry = saveGeometry(); QWidget::closeEvent(event); From 6b5d191746becbba9ac3b6b1d25b4b54942079fb Mon Sep 17 00:00:00 2001 From: GriffinR Date: Fri, 23 Aug 2024 23:59:54 -0400 Subject: [PATCH 61/76] Fix level distribution max, theme issue --- include/ui/wildmonchart.h | 5 ++- src/ui/wildmonchart.cpp | 80 +++++++++++++++------------------------ 2 files changed, 34 insertions(+), 51 deletions(-) diff --git a/include/ui/wildmonchart.h b/include/ui/wildmonchart.h index 087eb3ea..5f9a0c4d 100644 --- a/include/ui/wildmonchart.h +++ b/include/ui/wildmonchart.h @@ -58,9 +58,10 @@ private: void readTable(); void createSpeciesDistributionChart(); void createLevelDistributionChart(); - QBarSet* createLevelDistributionBarSet(const QString &, const QString &, bool, double *); + QBarSet* createLevelDistributionBarSet(const QString &, const QString &, bool); - void applySpeciesColors(QAbstractBarSeries *); + void saveSpeciesColors(const QList &); + void applySpeciesColors(const QList &); QChart::ChartTheme currentTheme() const; void updateTheme(); void stopChartAnimation(); diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp index a69f2f2b..257959d9 100644 --- a/src/ui/wildmonchart.cpp +++ b/src/ui/wildmonchart.cpp @@ -232,6 +232,7 @@ void WildMonChart::createSpeciesDistributionChart() { chart->legend()->setVisible(true); chart->legend()->setShowToolTips(true); chart->legend()->setAlignment(Qt::AlignBottom); + saveSpeciesColors(barSets); // X-axis is the % frequency. We're already showing percentages on the bar, so we just display 0/50/100% auto axisX = new QValueAxis(); @@ -248,24 +249,19 @@ void WildMonChart::createSpeciesDistributionChart() { series->attachAxis(axisY); } - applySpeciesColors(series); - if (ui->chartView_SpeciesDistribution->chart()) ui->chartView_SpeciesDistribution->chart()->deleteLater(); ui->chartView_SpeciesDistribution->setChart(chart); } -QBarSet* WildMonChart::createLevelDistributionBarSet(const QString &species, const QString &groupName, bool individual, double *barMax) { +QBarSet* WildMonChart::createLevelDistributionBarSet(const QString &species, const QString &groupName, bool individual) { const double totalFrequency = individual ? getSpeciesFrequency(species, groupName) : 1.0; const QMap levelFrequencies = getLevelFrequencies(species, groupName); auto set = new QBarSet(species); LevelRange levelRange = individual ? getLevelRange(species, groupName) : this->groupedLevelRanges.value(groupName); for (int i = levelRange.min; i <= levelRange.max; i++) { - double percent = levelFrequencies.value(i, 0) / totalFrequency * 100; - if (*barMax < percent) - *barMax = percent; - set->append(percent); + set->append(levelFrequencies.value(i, 0) / totalFrequency * 100); } // Show data when hovering over a bar set. This covers some shortfalls in our ability to control the chart design. @@ -296,71 +292,58 @@ void WildMonChart::createLevelDistributionChart() { const QString groupName = ui->comboBox_Group->currentText(); LevelRange levelRange; - double maxPercent = 0.0; QList barSets; // Create bar sets if (ui->groupBox_Species->isChecked()) { // Species box is active, we just display data for the selected species. const QString species = ui->comboBox_Species->currentText(); - barSets.append(createLevelDistributionBarSet(species, groupName, true, &maxPercent)); + barSets.append(createLevelDistributionBarSet(species, groupName, true)); levelRange = getLevelRange(species, groupName); } else { // Species box is inactive, we display data for all species in the table. for (const auto species : this->speciesInLegendOrder) - barSets.append(createLevelDistributionBarSet(species, groupName, false, &maxPercent)); + barSets.append(createLevelDistributionBarSet(species, groupName, false)); levelRange = this->groupedLevelRanges.value(groupName); } + // Set up series + auto series = new QStackedBarSeries(); + //series->setLabelsVisible(); + series->append(barSets); + // Set up chart auto chart = new QChart(); - //chart->setTitle(""); + chart->addSeries(series); chart->setTheme(currentTheme()); chart->setAnimationOptions(QChart::SeriesAnimations); chart->legend()->setVisible(true); chart->legend()->setShowToolTips(true); chart->legend()->setAlignment(Qt::AlignBottom); + applySpeciesColors(barSets); // Has to happen after theme is set // X-axis is the level range. QBarCategoryAxis *axisX = new QBarCategoryAxis(); for (int i = levelRange.min; i <= levelRange.max; i++) axisX->append(QString::number(i)); chart->addAxis(axisX, Qt::AlignBottom); + series->attachAxis(axisX); - // Y-axis is the % frequency. We round the max up to a multiple of 5. + // Y-axis is the % frequency + QValueAxis *axisY = new QValueAxis(); + axisY->setLabelFormat("%u%%"); + chart->addAxis(axisY, Qt::AlignLeft); + series->attachAxis(axisY); + + // We round the y-axis max up to a multiple of 5. auto roundUp = [](int num, int multiple) { auto remainder = num % multiple; if (remainder == 0) return num; return num + multiple - remainder; }; - QValueAxis *axisY = new QValueAxis(); - axisY->setMax(roundUp(qCeil(maxPercent), 5)); // TODO: This isn't taking stacking into account - //axisY->setTickType(QValueAxis::TicksDynamic); - //axisY->setTickInterval(5); - axisY->setLabelFormat("%u%%"); - chart->addAxis(axisY, Qt::AlignLeft); + axisY->setMax(roundUp(qCeil(axisY->max()), 5)); - // Set up series. Grouped mode uses a stacked bar series. - if (barSets.length() < 2) { - auto series = new QBarSeries(); - series->append(barSets); - series->attachAxis(axisX); - series->attachAxis(axisY); - //series->setLabelsVisible(); - chart->addSeries(series); - applySpeciesColors(series); - } else { - auto series = new QStackedBarSeries(); - series->append(barSets); - series->attachAxis(axisX); - series->attachAxis(axisY); - //series->setLabelsVisible(); - chart->addSeries(series); - applySpeciesColors(series); - } - - // TODO: Cache old charts? if (ui->chartView_LevelDistribution->chart()) ui->chartView_LevelDistribution->chart()->deleteLater(); ui->chartView_LevelDistribution->setChart(chart); @@ -384,24 +367,23 @@ void WildMonChart::updateTheme() { return; this->speciesToColor.clear(); chart->setTheme(theme); - applySpeciesColors(static_cast(chart->series().at(0))); + saveSpeciesColors(static_cast(chart->series().at(0))->barSets()); chart = ui->chartView_LevelDistribution->chart(); if (chart) { chart->setTheme(theme); - applySpeciesColors(static_cast(chart->series().at(0))); + applySpeciesColors(static_cast(chart->series().at(0))->barSets()); } } -void WildMonChart::applySpeciesColors(QAbstractBarSeries *series) { - for (auto set : series->barSets()) { - const QString species = set->label(); - if (speciesToColor.contains(species)) { - set->setColor(speciesToColor.value(species)); - } else { - speciesToColor.insert(species, set->color()); - } - } +void WildMonChart::saveSpeciesColors(const QList &barSets) { + for (auto set : barSets) + this->speciesToColor.insert(set->label(), set->color()); +} + +void WildMonChart::applySpeciesColors(const QList &barSets) { + for (auto set : barSets) + set->setColor(this->speciesToColor.value(set->label())); } void WildMonChart::stopChartAnimation() { From 2ae87738fd0514640930a5cd786bf8a614ff564f Mon Sep 17 00:00:00 2001 From: GriffinR Date: Thu, 28 Sep 2023 13:29:06 -0400 Subject: [PATCH 62/76] Handle discontinous mapsec values in map list --- src/mainwindow.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 92fbe5b5..49c04394 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1156,18 +1156,18 @@ void MainWindow::sortMapList() { case MapSortOrder::Area: { QMap mapsecToGroupNum; - for (int i = 0; i < project->mapSectionNameToValue.size(); i++) { - QString mapsec_name = project->mapSectionValueToName.value(i); + int row = 0; + for (auto mapsec_value : project->mapSectionValueToName.keys()) { + QString mapsec_name = project->mapSectionValueToName.value(mapsec_value); QStandardItem *mapsec = new QStandardItem; mapsec->setText(mapsec_name); mapsec->setIcon(folderIcon); mapsec->setEditable(false); mapsec->setData(mapsec_name, Qt::UserRole); mapsec->setData("map_sec", MapListUserRoles::TypeRole); - mapsec->setData(i, MapListUserRoles::GroupRole); root->appendRow(mapsec); mapGroupItemsList->append(mapsec); - mapsecToGroupNum.insert(mapsec_name, i); + mapsecToGroupNum.insert(mapsec_name, row++); } for (int i = 0; i < project->groupNames.length(); i++) { QStringList names = project->groupedMapNames.value(i); From 97d9c5c03e93f2666bb1658392b432d83581a0e6 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 27 Aug 2024 14:19:15 -0400 Subject: [PATCH 63/76] Clear map list filter on project close --- src/mainwindow.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 49c04394..23b7eb06 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1098,6 +1098,7 @@ void MainWindow::clearProjectUI() { const QSignalBlocker blocker7(ui->comboBox_Type); const QSignalBlocker blocker8(ui->comboBox_DiveMap); const QSignalBlocker blocker9(ui->comboBox_EmergeMap); + const QSignalBlocker blockerA(ui->lineEdit_filterBox); ui->comboBox_Song->clear(); ui->comboBox_Location->clear(); @@ -1108,6 +1109,7 @@ void MainWindow::clearProjectUI() { ui->comboBox_Type->clear(); ui->comboBox_DiveMap->clear(); ui->comboBox_EmergeMap->clear(); + ui->lineEdit_filterBox->clear(); // Clear map list mapListModel->clear(); From dc79d5d258e23fc6a47462ed682248e71c89355e Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 27 Aug 2024 14:21:16 -0400 Subject: [PATCH 64/76] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db886e70..86009d49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,8 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - Fix a visual issue when quickly dragging map connections around. - Fix map connections rendering incorrectly if their direction name was unknown. - Fix map connections rendering incorrectly if their dimensions were smaller than the border draw distance. +- Fix the map list filter retaining text between project open/close. +- Fix the map list mishandling value gaps when sorting by Area. ## [5.4.1] - 2024-03-21 ### Fixed From d8d196b79de8c32534f2abe78b8c27b542e2b4d5 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Mon, 26 Aug 2024 23:53:08 -0400 Subject: [PATCH 65/76] Connect wild mon chart to editor --- include/editor.h | 7 ++- include/mainwindow.h | 1 - include/ui/wildmonchart.h | 11 ++-- src/editor.cpp | 36 +++++++++---- src/mainwindow.cpp | 23 +++------ src/ui/encountertabledelegates.cpp | 5 +- src/ui/encountertablemodel.cpp | 82 +++++++++++++++++------------- src/ui/montabwidget.cpp | 11 ++-- src/ui/wildmonchart.cpp | 60 ++++++++++++++-------- 9 files changed, 139 insertions(+), 97 deletions(-) diff --git a/include/editor.h b/include/editor.h index bb211cba..31b5c11b 100644 --- a/include/editor.h +++ b/include/editor.h @@ -26,6 +26,7 @@ #include "movablerect.h" #include "cursortilerect.h" #include "mapruler.h" +#include "encountertablemodel.h" class DraggablePixmapItem; class MetatilesPixmapItem; @@ -84,7 +85,7 @@ public: void removeSelectedConnection(); void addNewWildMonGroup(QWidget *window); void deleteWildMonGroup(); - QTableView* getCurrentWildMonTable(); + EncounterTableModel* getCurrentWildMonTable(); void updateDiveMap(QString mapName); void updateEmergeMap(QString mapName); void setSelectedConnection(MapConnection *connection); @@ -224,7 +225,9 @@ private slots: signals: void objectsChanged(); void openConnectedMap(MapConnection*); - void wildMonDataChanged(); + void wildMonTableOpened(EncounterTableModel*); + void wildMonTableClosed(); + void wildMonTableEdited(); void warpEventDoubleClicked(QString, int, Event::Group); void currentMetatilesSelectionChanged(); void mapRulerStatusChanged(const QString &); diff --git a/include/mainwindow.h b/include/mainwindow.h index b0b2c3a6..c2726efc 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -183,7 +183,6 @@ private slots: void onOpenConnectedMap(MapConnection*); void onMapNeedsRedrawing(); void onTilesetsSaved(QString, QString); - void onWildMonDataChanged(); void openNewMapPopupWindow(); void onNewMapCreated(); void onMapCacheCleared(); diff --git a/include/ui/wildmonchart.h b/include/ui/wildmonchart.h index 5f9a0c4d..07ea7b7b 100644 --- a/include/ui/wildmonchart.h +++ b/include/ui/wildmonchart.h @@ -21,7 +21,8 @@ public: public slots: void setTable(const EncounterTableModel *table); - void createCharts(); + void clearTable(); + void refresh(); private: Ui::WildMonChart *ui; @@ -56,15 +57,17 @@ private: void clearTableData(); void readTable(); - void createSpeciesDistributionChart(); - void createLevelDistributionChart(); + QChart* createSpeciesDistributionChart(); + QChart* createLevelDistributionChart(); QBarSet* createLevelDistributionBarSet(const QString &, const QString &, bool); + void refreshSpeciesDistributionChart(); + void refreshLevelDistributionChart(); void saveSpeciesColors(const QList &); void applySpeciesColors(const QList &); QChart::ChartTheme currentTheme() const; void updateTheme(); - void stopChartAnimation(); + void stopChartAnimation(QChart*); void showHelpDialog(); }; diff --git a/src/editor.cpp b/src/editor.cpp index 6e2ff76f..d2cbbf50 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -7,7 +7,6 @@ #include "mapsceneeventfilter.h" #include "metatile.h" #include "montabwidget.h" -#include "encountertablemodel.h" #include "editcommands.h" #include "config.h" #include "scripting.h" @@ -43,6 +42,11 @@ Editor::Editor(Ui::MainWindow* ui) selectNewEvents = false; } }); + + // Send signals used for updating the wild pokemon summary chart + connect(ui->stackedWidget_WildMons, &QStackedWidget::currentChanged, [this] { + emit wildMonTableOpened(getCurrentWildMonTable()); + }); } Editor::~Editor() @@ -79,9 +83,7 @@ void Editor::saveUiFields() { } void Editor::setProject(Project * project) { - if (this->project) { - closeProject(); - } + closeProject(); this->project = project; MapConnection::project = project; } @@ -194,6 +196,7 @@ void Editor::setEditingConnections() { void Editor::clearWildMonTables() { QStackedWidget *stack = ui->stackedWidget_WildMons; + const QSignalBlocker blocker(stack); // delete widgets from previous map data if they exist while (stack->count()) { @@ -203,6 +206,7 @@ void Editor::clearWildMonTables() { } ui->comboBox_EncounterGroupLabel->clear(); + emit wildMonTableClosed(); } void Editor::displayWildMonTables() { @@ -243,8 +247,12 @@ void Editor::displayWildMonTables() { } tabIndex++; } + connect(tabWidget, &MonTabWidget::currentChanged, [this] { + emit wildMonTableOpened(getCurrentWildMonTable()); + }); } stack->setCurrentIndex(0); + emit wildMonTableOpened(getCurrentWildMonTable()); } void Editor::addNewWildMonGroup(QWidget *window) { @@ -359,7 +367,7 @@ void Editor::addNewWildMonGroup(QWidget *window) { tabIndex++; } saveEncounterTabData(); - emit wildMonDataChanged(); + emit wildMonTableEdited(); } } @@ -397,7 +405,8 @@ void Editor::deleteWildMonGroup() { project->encounterGroupLabels.remove(i); displayWildMonTables(); - emit wildMonDataChanged(); + saveEncounterTabData(); + emit wildMonTableEdited(); } } @@ -650,7 +659,8 @@ void Editor::configureEncounterJSON(QWidget *window) { // Re-draw the tab accordingly. displayWildMonTables(); - emit wildMonDataChanged(); + saveEncounterTabData(); + emit wildMonTableEdited(); } } @@ -684,10 +694,14 @@ void Editor::saveEncounterTabData() { } } -QTableView* Editor::getCurrentWildMonTable() { - QStackedWidget *stack = ui->stackedWidget_WildMons; - auto tabWidget = static_cast(stack->widget(stack->currentIndex())); - return tabWidget->tableAt(tabWidget->currentIndex()); +EncounterTableModel* Editor::getCurrentWildMonTable() { + auto tabWidget = static_cast(ui->stackedWidget_WildMons->currentWidget()); + if (!tabWidget) return nullptr; + + auto tableView = tabWidget->tableAt(tabWidget->currentIndex()); + if (!tableView) return nullptr; + + return static_cast(tableView->model()); } void Editor::updateEncounterFields(EncounterFields newFields) { diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index aa1d0122..c4e23b81 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -239,10 +239,8 @@ void MainWindow::initExtraSignals() { connect(ui->tabWidget_EventType, &QTabWidget::currentChanged, this, &MainWindow::eventTabChanged); // Change pages on wild encounter groups - QStackedWidget *stack = ui->stackedWidget_WildMons; - QComboBox *labelCombo = ui->comboBox_EncounterGroupLabel; - connect(labelCombo, QOverload::of(&QComboBox::currentIndexChanged), [=](int index){ - stack->setCurrentIndex(index); + connect(ui->comboBox_EncounterGroupLabel, QOverload::of(&QComboBox::currentIndexChanged), [this](int index){ + ui->stackedWidget_WildMons->setCurrentIndex(index); }); // Convert the layout of the map tools' frame into an adjustable FlowLayout @@ -309,7 +307,7 @@ void MainWindow::initEditor() { connect(this->editor, &Editor::openConnectedMap, this, &MainWindow::onOpenConnectedMap); connect(this->editor, &Editor::warpEventDoubleClicked, this, &MainWindow::openWarpMap); connect(this->editor, &Editor::currentMetatilesSelectionChanged, this, &MainWindow::currentMetatilesSelectionChanged); - connect(this->editor, &Editor::wildMonDataChanged, this, &MainWindow::onWildMonDataChanged); + connect(this->editor, &Editor::wildMonTableEdited, [this] { this->markMapEdited(); }); connect(this->editor, &Editor::mapRulerStatusChanged, this, &MainWindow::onMapRulerStatusChanged); connect(this->editor, &Editor::tilesetUpdated, this, &Scripting::cb_TilesetUpdated); connect(ui->toolButton_Open_Scripts, &QToolButton::pressed, this->editor, &Editor::openMapScripts); @@ -2560,11 +2558,6 @@ void MainWindow::onTilesetsSaved(QString primaryTilesetLabel, QString secondaryT redrawMapScene(); } -void MainWindow::onWildMonDataChanged() { - editor->saveEncounterTabData(); - markMapEdited(); -} - void MainWindow::onMapRulerStatusChanged(const QString &status) { if (status.isEmpty()) { label_MapRulerStatus->hide(); @@ -2659,12 +2652,10 @@ void MainWindow::on_pushButton_DeleteWildMonGroup_clicked() { void MainWindow::on_pushButton_SummaryChart_clicked() { if (!this->wildMonChart) { - // TODO: Move to editor, connect to signals for when the table data changes - QTableView *table = this->editor->getCurrentWildMonTable(); - EncounterTableModel *data = table ? static_cast(table->model()) : nullptr; - this->wildMonChart = new WildMonChart(this, data); - } else { - this->wildMonChart->createCharts(); + this->wildMonChart = new WildMonChart(this, this->editor->getCurrentWildMonTable()); + connect(this->editor, &Editor::wildMonTableOpened, this->wildMonChart, &WildMonChart::setTable); + connect(this->editor, &Editor::wildMonTableClosed, this->wildMonChart, &WildMonChart::clearTable); + connect(this->editor, &Editor::wildMonTableEdited, this->wildMonChart, &WildMonChart::refresh); } openSubWindow(this->wildMonChart); } diff --git a/src/ui/encountertabledelegates.cpp b/src/ui/encountertabledelegates.cpp index 7824087e..e2901d16 100644 --- a/src/ui/encountertabledelegates.cpp +++ b/src/ui/encountertabledelegates.cpp @@ -79,8 +79,11 @@ QWidget *SpinBoxDelegate::createEditor(QWidget *parent, const QStyleOptionViewIt editor->setMaximum(this->project->miscConstants.value("max_level_define").toInt()); } else if (col == EncounterTableModel::ColumnType::EncounterRate) { + // The games multiply the encounter rate value provided here by 16, so the input limit is the max/16. + // TODO: Read MAX_ENCOUNTER_RATE? + static const int maxEncounterRate = 2880; editor->setMinimum(0); - editor->setMaximum(180); + editor->setMaximum(maxEncounterRate / 16); } return editor; diff --git a/src/ui/encountertablemodel.cpp b/src/ui/encountertablemodel.cpp index 00199735..f7e73996 100644 --- a/src/ui/encountertablemodel.cpp +++ b/src/ui/encountertablemodel.cpp @@ -142,45 +142,57 @@ QVariant EncounterTableModel::headerData(int section, Qt::Orientation orientatio } bool EncounterTableModel::setData(const QModelIndex &index, const QVariant &value, int role) { - if (role == Qt::EditRole) { - if (!checkIndex(index)) - return false; + if (role != Qt::EditRole) + return false; + if (!checkIndex(index)) + return false; - int row = index.row(); - int col = index.column(); + int row = index.row(); + int col = index.column(); - switch (col) { - case ColumnType::Species: - this->monInfo.wildPokemon[row].species = value.toString(); - break; - - case ColumnType::MinLevel: { - int minLevel = value.toInt(); - this->monInfo.wildPokemon[row].minLevel = minLevel; - if (minLevel > this->monInfo.wildPokemon[row].maxLevel) - this->monInfo.wildPokemon[row].maxLevel = minLevel; - break; + switch (col) { + case ColumnType::Species: { + QString species = value.toString(); + if (this->monInfo.wildPokemon[row].species != species) { + this->monInfo.wildPokemon[row].species = species; + emit edited(); } - - case ColumnType::MaxLevel: { - int maxLevel = value.toInt(); - this->monInfo.wildPokemon[row].maxLevel = maxLevel; - if (maxLevel < this->monInfo.wildPokemon[row].minLevel) - this->monInfo.wildPokemon[row].minLevel = maxLevel; - break; - } - - case ColumnType::EncounterRate: - this->monInfo.encounterRate = value.toInt(); - break; - - default: - return false; - } - emit edited(); - return true; + break; } - return false; + + case ColumnType::MinLevel: { + int minLevel = value.toInt(); + if (this->monInfo.wildPokemon[row].minLevel != minLevel) { + this->monInfo.wildPokemon[row].minLevel = minLevel; + this->monInfo.wildPokemon[row].maxLevel = qMax(minLevel, this->monInfo.wildPokemon[row].maxLevel); + emit edited(); + } + break; + } + + case ColumnType::MaxLevel: { + int maxLevel = value.toInt(); + if (this->monInfo.wildPokemon[row].maxLevel != maxLevel) { + this->monInfo.wildPokemon[row].maxLevel = maxLevel; + this->monInfo.wildPokemon[row].minLevel = qMin(maxLevel, this->monInfo.wildPokemon[row].minLevel); + emit edited(); + } + break; + } + + case ColumnType::EncounterRate: { + int encounterRate = value.toInt(); + if (this->monInfo.encounterRate != encounterRate) { + this->monInfo.encounterRate = encounterRate; + emit edited(); + } + break; + } + + default: + return false; + } + return true; } Qt::ItemFlags EncounterTableModel::flags(const QModelIndex &index) const { diff --git a/src/ui/montabwidget.cpp b/src/ui/montabwidget.cpp index 6845a02b..74c52dc5 100644 --- a/src/ui/montabwidget.cpp +++ b/src/ui/montabwidget.cpp @@ -68,7 +68,7 @@ void MonTabWidget::paste(int index) { WildMonInfo newInfo = getDefaultMonInfo(this->editor->project->wildMonFields.at(index)); combineEncounters(newInfo, encounterClipboard); populateTab(index, newInfo); - emit editor->wildMonDataChanged(); + emit editor->wildMonTableEdited(); } void MonTabWidget::actionCopyTab(int index) { @@ -88,21 +88,19 @@ void MonTabWidget::actionCopyTab(int index) { } void MonTabWidget::actionAddDeleteTab(int index) { + clearTableAt(index); if (activeTabs[index]) { // delete tab - clearTableAt(index); deactivateTab(index); editor->saveEncounterTabData(); - emit editor->wildMonDataChanged(); } else { // add tab - clearTableAt(index); populateTab(index, getDefaultMonInfo(editor->project->wildMonFields.at(index))); editor->saveEncounterTabData(); setCurrentIndex(index); - emit editor->wildMonDataChanged(); } + emit editor->wildMonTableEdited(); } void MonTabWidget::clearTableAt(int tabIndex) { @@ -130,6 +128,7 @@ void MonTabWidget::populateTab(int tabIndex, WildMonInfo monInfo) { EncounterTableModel *model = new EncounterTableModel(monInfo, editor->project->wildMonFields, tabIndex, this); connect(model, &EncounterTableModel::edited, editor, &Editor::saveEncounterTabData); + connect(model, &EncounterTableModel::edited, editor, &Editor::wildMonTableEdited); speciesTable->setModel(model); speciesTable->setItemDelegateForColumn(EncounterTableModel::ColumnType::Species, new SpeciesComboDelegate(editor->project, this)); @@ -158,6 +157,8 @@ void MonTabWidget::populateTab(int tabIndex, WildMonInfo monInfo) { } QTableView *MonTabWidget::tableAt(int tabIndex) { + if (tabIndex < 0) + return nullptr; return static_cast(this->widget(tabIndex)); } diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp index 257959d9..dfdda38d 100644 --- a/src/ui/wildmonchart.cpp +++ b/src/ui/wildmonchart.cpp @@ -30,9 +30,9 @@ WildMonChart::WildMonChart(QWidget *parent, const EncounterTableModel *table) : connect(ui->button_Help, &QAbstractButton::clicked, this, &WildMonChart::showHelpDialog); // Changing these settings changes which level distribution chart is shown - connect(ui->groupBox_Species, &QGroupBox::clicked, this, &WildMonChart::createLevelDistributionChart); - connect(ui->comboBox_Species, &QComboBox::currentTextChanged, this, &WildMonChart::createLevelDistributionChart); - connect(ui->comboBox_Group, &QComboBox::currentTextChanged, this, &WildMonChart::createLevelDistributionChart); + connect(ui->groupBox_Species, &QGroupBox::clicked, this, &WildMonChart::refreshLevelDistributionChart); + connect(ui->comboBox_Species, &QComboBox::currentTextChanged, this, &WildMonChart::refreshLevelDistributionChart); + connect(ui->comboBox_Group, &QComboBox::currentTextChanged, this, &WildMonChart::refreshLevelDistributionChart); // Set up Theme combo box for (auto i : themes) @@ -59,9 +59,14 @@ WildMonChart::~WildMonChart() { }; void WildMonChart::setTable(const EncounterTableModel *table) { + if (this->table == table) + return; this->table = table; - readTable(); - createCharts(); + refresh(); +} + +void WildMonChart::clearTable() { + setTable(nullptr); } void WildMonChart::clearTableData() { @@ -78,6 +83,7 @@ void WildMonChart::clearTableData() { const QSignalBlocker blocker2(ui->comboBox_Group); ui->comboBox_Species->clear(); ui->comboBox_Group->clear(); + ui->comboBox_Group->setEnabled(false); } // Extract all the data from the table that we need for the charts @@ -151,15 +157,28 @@ void WildMonChart::readTable() { ui->comboBox_Group->setEnabled(usesGroupLabels()); } -void WildMonChart::createCharts() { - createSpeciesDistributionChart(); - createLevelDistributionChart(); - +void WildMonChart::refresh() { + readTable(); + refreshSpeciesDistributionChart(); + refreshLevelDistributionChart(); + // Turn off the animation once it's played, otherwise it replays any time the window changes size. // TODO: Store timer, disable if closing or creating new chart //QTimer::singleShot(chart->animationDuration() + 500, this, &WildMonChart::stopChartAnimation); } +void WildMonChart::refreshSpeciesDistributionChart() { + if (ui->chartView_SpeciesDistribution->chart()) + ui->chartView_SpeciesDistribution->chart()->deleteLater(); + ui->chartView_SpeciesDistribution->setChart(createSpeciesDistributionChart()); +} + +void WildMonChart::refreshLevelDistributionChart() { + if (ui->chartView_LevelDistribution->chart()) + ui->chartView_LevelDistribution->chart()->deleteLater(); + ui->chartView_LevelDistribution->setChart(createLevelDistributionChart()); +} + QStringList WildMonChart::getSpeciesNamesAlphabetical() const { return this->speciesToGroupedData.keys(); } @@ -190,7 +209,7 @@ bool WildMonChart::usesGroupLabels() const { return this->groupNames.length() > 1; } -void WildMonChart::createSpeciesDistributionChart() { +QChart* WildMonChart::createSpeciesDistributionChart() { QList barSets; for (const auto species : getSpeciesNamesAlphabetical()) { // Add encounter chance data @@ -249,9 +268,7 @@ void WildMonChart::createSpeciesDistributionChart() { series->attachAxis(axisY); } - if (ui->chartView_SpeciesDistribution->chart()) - ui->chartView_SpeciesDistribution->chart()->deleteLater(); - ui->chartView_SpeciesDistribution->setChart(chart); + return chart; } QBarSet* WildMonChart::createLevelDistributionBarSet(const QString &species, const QString &groupName, bool individual) { @@ -281,14 +298,14 @@ QBarSet* WildMonChart::createLevelDistributionBarSet(const QString &species, con const QSignalBlocker blocker2(ui->comboBox_Species); ui->groupBox_Species->setChecked(true); ui->comboBox_Species->setCurrentText(species); - createLevelDistributionChart(); + refreshLevelDistributionChart(); }); } return set; } -void WildMonChart::createLevelDistributionChart() { +QChart* WildMonChart::createLevelDistributionChart() { const QString groupName = ui->comboBox_Group->currentText(); LevelRange levelRange; @@ -344,9 +361,7 @@ void WildMonChart::createLevelDistributionChart() { }; axisY->setMax(roundUp(qCeil(axisY->max()), 5)); - if (ui->chartView_LevelDistribution->chart()) - ui->chartView_LevelDistribution->chart()->deleteLater(); - ui->chartView_LevelDistribution->setChart(chart); + return chart; } QChart::ChartTheme WildMonChart::currentTheme() const { @@ -365,7 +380,6 @@ void WildMonChart::updateTheme() { QChart *chart = ui->chartView_SpeciesDistribution->chart(); if (!chart) return; - this->speciesToColor.clear(); chart->setTheme(theme); saveSpeciesColors(static_cast(chart->series().at(0))->barSets()); @@ -377,6 +391,7 @@ void WildMonChart::updateTheme() { } void WildMonChart::saveSpeciesColors(const QList &barSets) { + this->speciesToColor.clear(); for (auto set : barSets) this->speciesToColor.insert(set->label(), set->color()); } @@ -386,9 +401,10 @@ void WildMonChart::applySpeciesColors(const QList &barSets) { set->setColor(this->speciesToColor.value(set->label())); } -void WildMonChart::stopChartAnimation() { - if (ui->chartView_SpeciesDistribution->chart()) - ui->chartView_SpeciesDistribution->chart()->setAnimationOptions(QChart::NoAnimation); +void WildMonChart::stopChartAnimation(QChart *chart) { + if (!chart) + return; + chart->setAnimationOptions(QChart::NoAnimation); } void WildMonChart::showHelpDialog() { From a7272191f43abfa4a75e8d40f38a661b1ddc42d0 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 27 Aug 2024 15:32:49 -0400 Subject: [PATCH 66/76] Fix some species name issues for encounter table --- src/project.cpp | 2 +- src/ui/encountertabledelegates.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/project.cpp b/src/project.cpp index d981fa2d..d1c3f63c 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -2611,7 +2611,7 @@ bool Project::readSpeciesIconPaths() { const QMap iconIncbins = parser.readCIncbinMulti(incfilename); // Read species constants. If this fails we can get them from the icon table (but we shouldn't rely on it). - const QStringList prefixes = {projectConfig.getIdentifier(ProjectIdentifier::define_species_prefix)}; + const QStringList prefixes = {QString("\\b%1").arg(projectConfig.getIdentifier(ProjectIdentifier::define_species_prefix))}; const QString constantsFilename = projectConfig.getFilePath(ProjectFilePath::constants_species); fileWatcher.addPath(root + "/" + constantsFilename); QStringList speciesNames = parser.readCDefineNames(constantsFilename, prefixes); diff --git a/src/ui/encountertabledelegates.cpp b/src/ui/encountertabledelegates.cpp index e2901d16..89d0ec89 100644 --- a/src/ui/encountertabledelegates.cpp +++ b/src/ui/encountertabledelegates.cpp @@ -50,7 +50,7 @@ QWidget *SpeciesComboDelegate::createEditor(QWidget *parent, const QStyleOptionV void SpeciesComboDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { QString species = index.data(Qt::EditRole).toString(); NoScrollComboBox *combo = static_cast(editor); - combo->setCurrentIndex(combo->findText(species)); + combo->setCurrentText(species); } void SpeciesComboDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { From 0e9dacd15962b29f1df5c8e01b63d09142531a39 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 27 Aug 2024 16:14:02 -0400 Subject: [PATCH 67/76] Read MAX_ENCOUNTER_RATE from project --- docsrc/manual/project-files.rst | 2 ++ include/config.h | 2 ++ include/project.h | 4 ++- src/config.cpp | 2 ++ src/project.cpp | 47 ++++++++++++++++++++---------- src/ui/encountertabledelegates.cpp | 9 ++---- 6 files changed, 44 insertions(+), 22 deletions(-) diff --git a/docsrc/manual/project-files.rst b/docsrc/manual/project-files.rst index 1da0fbc6..b99e74d7 100644 --- a/docsrc/manual/project-files.rst +++ b/docsrc/manual/project-files.rst @@ -67,6 +67,7 @@ The filepath that Porymap expects for each file can be overridden on the ``Files include/fieldmap.h, yes, no, ``constants_fieldmap``, reads tileset related constants src/fieldmap.c, yes, no, ``fieldmap``, reads ``symbol_attribute_table`` src/event_object_movement.c, yes, no, ``initial_facing_table``, reads ``symbol_facing_directions`` + src/wild_encounter.c, yes, no, ``wild_encounter``, reads ``define_max_encounter_rate`` src/pokemon_icon.c, yes, no, ``pokemon_icon_table``, reads files in ``symbol_pokemon_icon_table`` graphics/pokemon/\*/icon.png, yes, no, ``pokemon_gfx``, to search for Pokémon icons if they aren't found in ``symbol_pokemon_icon_table`` @@ -96,6 +97,7 @@ In addition to these files, there are some specific symbol and macro names that ``define_obj_event_count``, ``OBJECT_EVENT_TEMPLATES_COUNT``, to limit total Object Events ``define_min_level``, ``MIN_LEVEL``, minimum wild encounters level ``define_max_level``, ``MAX_LEVEL``, maximum wild encounters level + ``define_max_encounter_rate``, ``MAX_ENCOUNTER_RATE``, this value / 16 will be the maximum encounter rate on the ``Wild Pokémon`` tab ``define_tiles_primary``, ``NUM_TILES_IN_PRIMARY``, ``define_tiles_total``, ``NUM_TILES_TOTAL``, ``define_metatiles_primary``, ``NUM_METATILES_IN_PRIMARY``, total metatiles are calculated using metatile ID mask diff --git a/include/config.h b/include/config.h index 25138ff8..a2fead2e 100644 --- a/include/config.h +++ b/include/config.h @@ -194,6 +194,7 @@ enum ProjectIdentifier { define_obj_event_count, define_min_level, define_max_level, + define_max_encounter_rate, define_tiles_primary, define_tiles_total, define_metatiles_primary, @@ -281,6 +282,7 @@ enum ProjectFilePath { global_fieldmap, fieldmap, initial_facing_table, + wild_encounter, pokemon_icon_table, pokemon_gfx, }; diff --git a/include/project.h b/include/project.h index 8c08e508..c3fc1677 100644 --- a/include/project.h +++ b/include/project.h @@ -39,7 +39,6 @@ public: QMap mapGroups; QList groupedMapNames; QStringList mapNames; - QMap miscConstants; QList healLocations; QMap healLocationNameToValue; QMap mapConstantsToMapNames; @@ -79,6 +78,9 @@ public: bool usingAsmTilesets; QString importExportPath; QSet disabledSettingsNames; + int pokemonMinLevel; + int pokemonMaxLevel; + int maxEncounterRate; bool wildEncountersLoaded; void set_root(QString); diff --git a/src/config.cpp b/src/config.cpp index 5f1b3bd9..20869171 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -85,6 +85,7 @@ const QMap> ProjectConfig::defaultIde {ProjectIdentifier::define_obj_event_count, {"define_obj_event_count", "OBJECT_EVENT_TEMPLATES_COUNT"}}, {ProjectIdentifier::define_min_level, {"define_min_level", "MIN_LEVEL"}}, {ProjectIdentifier::define_max_level, {"define_max_level", "MAX_LEVEL"}}, + {ProjectIdentifier::define_max_encounter_rate, {"define_max_encounter_rate", "MAX_ENCOUNTER_RATE"}}, {ProjectIdentifier::define_tiles_primary, {"define_tiles_primary", "NUM_TILES_IN_PRIMARY"}}, {ProjectIdentifier::define_tiles_total, {"define_tiles_total", "NUM_TILES_TOTAL"}}, {ProjectIdentifier::define_metatiles_primary, {"define_metatiles_primary", "NUM_METATILES_IN_PRIMARY"}}, @@ -174,6 +175,7 @@ const QMap> ProjectConfig::defaultPaths {ProjectFilePath::fieldmap, { "fieldmap", "src/fieldmap.c"}}, {ProjectFilePath::pokemon_icon_table, { "pokemon_icon_table", "src/pokemon_icon.c"}}, {ProjectFilePath::initial_facing_table, { "initial_facing_table", "src/event_object_movement.c"}}, + {ProjectFilePath::wild_encounter, { "wild_encounter", "src/wild_encounter.c"}}, {ProjectFilePath::pokemon_gfx, { "pokemon_gfx", "graphics/pokemon/"}}, }; diff --git a/src/project.cpp b/src/project.cpp index d1c3f63c..3719917e 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -1654,15 +1654,43 @@ void Project::deleteFile(QString path) { } bool Project::readWildMonData() { - extraEncounterGroups.clear(); - wildMonFields.clear(); - wildMonData.clear(); - encounterGroupLabels.clear(); + this->extraEncounterGroups.clear(); + this->wildMonFields.clear(); + this->wildMonData.clear(); + this->encounterGroupLabels.clear(); + this->pokemonMinLevel = 0; + this->pokemonMaxLevel = 100; + this->maxEncounterRate = 2880/16; this->wildEncountersLoaded = false; if (!userConfig.useEncounterJson) { return true; } + // Read max encounter rate. The games multiply the encounter rate value in the map data by 16, so our input limit is the max/16. + const QString encounterRateFile = projectConfig.getFilePath(ProjectFilePath::wild_encounter); + const QString maxEncounterRateName = projectConfig.getIdentifier(ProjectIdentifier::define_max_encounter_rate); + + fileWatcher.addPath(QString("%1/%2").arg(root).arg(encounterRateFile)); + auto defines = parser.readCDefinesByName(encounterRateFile, {maxEncounterRateName}); + if (defines.contains(maxEncounterRateName)) + this->maxEncounterRate = defines.value(maxEncounterRateName)/16; + + // Read min/max level + const QString levelRangeFile = projectConfig.getFilePath(ProjectFilePath::constants_pokemon); + const QString minLevelName = projectConfig.getIdentifier(ProjectIdentifier::define_min_level); + const QString maxLevelName = projectConfig.getIdentifier(ProjectIdentifier::define_max_level); + + fileWatcher.addPath(QString("%1/%2").arg(root).arg(levelRangeFile)); + defines = parser.readCDefinesByName(levelRangeFile, {minLevelName, maxLevelName}); + if (defines.contains(minLevelName)) + this->pokemonMinLevel = defines.value(minLevelName); + if (defines.contains(maxLevelName)) + this->pokemonMaxLevel = defines.value(maxLevelName); + + this->pokemonMinLevel = qMin(this->pokemonMinLevel, this->pokemonMaxLevel); + this->pokemonMaxLevel = qMax(this->pokemonMinLevel, this->pokemonMaxLevel); + + // Read encounter data QString wildMonJsonFilepath = QString("%1/%2").arg(root).arg(projectConfig.getFilePath(ProjectFilePath::json_wild_encounters)); fileWatcher.addPath(wildMonJsonFilepath); @@ -2412,17 +2440,6 @@ bool Project::readObjEventGfxConstants() { } bool Project::readMiscellaneousConstants() { - miscConstants.clear(); - if (userConfig.useEncounterJson) { - const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_pokemon); - const QString minLevelName = projectConfig.getIdentifier(ProjectIdentifier::define_min_level); - const QString maxLevelName = projectConfig.getIdentifier(ProjectIdentifier::define_max_level); - fileWatcher.addPath(root + "/" + filename); - QMap pokemonDefines = parser.readCDefinesByName(filename, {minLevelName, maxLevelName}); - miscConstants.insert("max_level_define", pokemonDefines.value(maxLevelName) > pokemonDefines.value(minLevelName) ? pokemonDefines.value(maxLevelName) : 100); - miscConstants.insert("min_level_define", pokemonDefines.value(minLevelName) < pokemonDefines.value(maxLevelName) ? pokemonDefines.value(minLevelName) : 1); - } - const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_global); const QString maxObjectEventsName = projectConfig.getIdentifier(ProjectIdentifier::define_obj_event_count); fileWatcher.addPath(root + "/" + filename); diff --git a/src/ui/encountertabledelegates.cpp b/src/ui/encountertabledelegates.cpp index 89d0ec89..230abc7b 100644 --- a/src/ui/encountertabledelegates.cpp +++ b/src/ui/encountertabledelegates.cpp @@ -75,15 +75,12 @@ QWidget *SpinBoxDelegate::createEditor(QWidget *parent, const QStyleOptionViewIt int col = index.column(); if (col == EncounterTableModel::ColumnType::MinLevel || col == EncounterTableModel::ColumnType::MaxLevel) { - editor->setMinimum(this->project->miscConstants.value("min_level_define").toInt()); - editor->setMaximum(this->project->miscConstants.value("max_level_define").toInt()); + editor->setMinimum(this->project->pokemonMinLevel); + editor->setMaximum(this->project->pokemonMaxLevel); } else if (col == EncounterTableModel::ColumnType::EncounterRate) { - // The games multiply the encounter rate value provided here by 16, so the input limit is the max/16. - // TODO: Read MAX_ENCOUNTER_RATE? - static const int maxEncounterRate = 2880; editor->setMinimum(0); - editor->setMaximum(maxEncounterRate / 16); + editor->setMaximum(this->project->maxEncounterRate); } return editor; From 8880aca829aa317f53b5655e18f478045336d886 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 27 Aug 2024 18:02:01 -0400 Subject: [PATCH 68/76] Fix crash when closing project while editing table --- src/mainwindow.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index c4e23b81..f342396f 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1109,6 +1109,7 @@ void MainWindow::clearProjectUI() { // Clear map list mapListModel->clear(); + mapListIndexes.clear(); mapGroupItemsList->clear(); } @@ -1125,6 +1126,7 @@ void MainWindow::sortMapList() { ui->mapList->setUpdatesEnabled(false); mapListModel->clear(); + mapListIndexes.clear(); mapGroupItemsList->clear(); QStandardItem *root = mapListModel->invisibleRootItem(); From 325757d4ee750228ad4e06b548d136db201f1999 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 27 Aug 2024 18:20:29 -0400 Subject: [PATCH 69/76] Clean up in wildmonchart --- include/ui/wildmonchart.h | 2 +- src/ui/wildmonchart.cpp | 60 +++++++++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/include/ui/wildmonchart.h b/include/ui/wildmonchart.h index 07ea7b7b..a7c56a47 100644 --- a/include/ui/wildmonchart.h +++ b/include/ui/wildmonchart.h @@ -67,7 +67,7 @@ private: void applySpeciesColors(const QList &); QChart::ChartTheme currentTheme() const; void updateTheme(); - void stopChartAnimation(QChart*); + void limitChartAnimation(QChart*); void showHelpDialog(); }; diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp index dfdda38d..011dc326 100644 --- a/src/ui/wildmonchart.cpp +++ b/src/ui/wildmonchart.cpp @@ -2,10 +2,6 @@ #include "ui_wildmonchart.h" #include "config.h" -#include "log.h" - -// TODO: Draw species icons below legend icons? - static const QString baseWindowTitle = QString("Wild Pokémon Summary Charts"); static const QList> themes = { @@ -48,8 +44,7 @@ WildMonChart::WildMonChart(QWidget *parent, const EncounterTableModel *table) : porymapConfig.wildMonChartTheme = ui->comboBox_Theme->currentText(); } - // TODO: Re-enable once finished - //restoreGeometry(porymapConfig.wildMonChartGeometry); + restoreGeometry(porymapConfig.wildMonChartGeometry); setTable(table); }; @@ -158,25 +153,36 @@ void WildMonChart::readTable() { } void WildMonChart::refresh() { + const QSignalBlocker blocker1(ui->comboBox_Species); + const QSignalBlocker blocker2(ui->comboBox_Group); + const QString oldSpecies = ui->comboBox_Species->currentText(); + const QString oldGroup = ui->comboBox_Group->currentText(); + readTable(); + + // If the old UI settings are still valid we should restore them + int index = ui->comboBox_Species->findText(oldSpecies); + if (index >= 0) ui->comboBox_Species->setCurrentIndex(index); + + index = ui->comboBox_Group->findText(oldGroup); + if (index >= 0) ui->comboBox_Group->setCurrentIndex(index); + refreshSpeciesDistributionChart(); refreshLevelDistributionChart(); - - // Turn off the animation once it's played, otherwise it replays any time the window changes size. - // TODO: Store timer, disable if closing or creating new chart - //QTimer::singleShot(chart->animationDuration() + 500, this, &WildMonChart::stopChartAnimation); } void WildMonChart::refreshSpeciesDistributionChart() { if (ui->chartView_SpeciesDistribution->chart()) ui->chartView_SpeciesDistribution->chart()->deleteLater(); ui->chartView_SpeciesDistribution->setChart(createSpeciesDistributionChart()); + limitChartAnimation(ui->chartView_SpeciesDistribution->chart()); } void WildMonChart::refreshLevelDistributionChart() { if (ui->chartView_LevelDistribution->chart()) ui->chartView_LevelDistribution->chart()->deleteLater(); ui->chartView_LevelDistribution->setChart(createLevelDistributionChart()); + limitChartAnimation(ui->chartView_LevelDistribution->chart()); } QStringList WildMonChart::getSpeciesNamesAlphabetical() const { @@ -401,18 +407,36 @@ void WildMonChart::applySpeciesColors(const QList &barSets) { set->setColor(this->speciesToColor.value(set->label())); } -void WildMonChart::stopChartAnimation(QChart *chart) { - if (!chart) - return; - chart->setAnimationOptions(QChart::NoAnimation); +// Turn off the animation once it's played, otherwise it replays any time the window changes size. +void WildMonChart::limitChartAnimation(QChart *chart) { + // Chart may be destroyed before the animation finishes + QPointer safeChart = chart; + + // QChart has no signal for when the animation is finished, so we use a timer to stop the animation. + // It is technically possible to get the chart to freeze mid-animation by resizing the window after + // the timer starts but before it finishes, but 1. animations are short so this is difficult to do, + // and 2. the issue resolves if the window is resized afterwards, so it's probably fine. + QTimer::singleShot(chart->animationDuration() + 500, [safeChart] { + if (safeChart) safeChart->setAnimationOptions(QChart::NoAnimation); + }); } void WildMonChart::showHelpDialog() { static const QString text = "This window provides some visualizations of the data in your current Wild Pokémon tab"; - static const QString informative = "The Species Distribution tab shows the cumulative encounter chance for each species " - "in the table. In other words, it answers the question \"For a given encounter of this type, " - "what is the likelihood that the pokémon encountered will be of that species?\"

" - "The Level Distribution tab..."; // TODO + static const QString informative = + "The Species Distribution tab shows the cumulative encounter chance for each species " + "in the table. In other words, it answers the question \"What is the likelihood of encountering " + "each species in a single encounter?\"" + "

" + "The Level Distribution tab shows the chance of encountering each species at a particular level. " + "In the top left under Group you can select which encounter group to show data for. " + "In the top right under Species you can select which species to show data for. " + "

" + "Individual Mode on the Level Distribution tab toggles whether data is shown for all species in the table. " + "The percentages will update to reflect whether you're showing all species or just that individual species. " + "In other words, while Individual Mode is checked the chart is answering the question \"If a species x " + "is encountered, what is the likelihood that it will be level y\", and while Individual Mode is not checked, " + "it answers the question \"For a single encounter, what is the likelihood of encountering a species x at level y.\""; QMessageBox msgBox(QMessageBox::Information, "porymap", text, QMessageBox::Close, this); msgBox.setTextFormat(Qt::RichText); msgBox.setInformativeText(informative); From febb82e0e92d0c40cb5ca0fe796b706a2fe46614 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 27 Aug 2024 19:09:00 -0400 Subject: [PATCH 70/76] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db886e70..d17d4388 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d ### Added - Redesigned the Connections tab, adding a number of new features including the option to open or display diving maps and a list UI for easier edit access. - Add a `Close Project` option +- Add charts to the `Wild Pokémon` tab that show species and level distributions. - An alert will be displayed when attempting to open a seemingly invalid project. ### Changed @@ -17,6 +18,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - Changes to the "Mirror to Connecting Maps" setting will now be saved between sessions. - A notice will be displayed when attempting to open the "Dynamic" map, rather than nothing happening. - The base game version is now auto-detected if the project name contains only one of "emerald", "firered/leafgreen", or "ruby/sapphire". +- The max encounter rate is now read from the project, rather than assuming the default value from RSE. - It's now possible to cancel quitting if there are unsaved changes in sub-windows. ### Fixed @@ -30,6 +32,8 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - Fix `About porymap` opening a new window each time it's activated. - Fix the `Edit History` window not raising to the front when reactivated. - New maps are now always inserted in map dropdowns at the correct position, rather than at the bottom of the list until the project is reloaded. +- Fix invalid species names clearing from wild pokémon data when revisited. +- Fix editing wild pokémon data not marking the map as edited. - Fix changes to map connections not marking connected maps as unsaved. - Fix numerous issues related to connecting a map to itself. - Fix incorrect map connections getting selected when opening a map by double-clicking a map connection. From c347fb174c0c936cc3c6a45133b4174f1afc0bfc Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 27 Aug 2024 20:17:22 -0400 Subject: [PATCH 71/76] Include qtcharts module for GitHub actions --- .github/workflows/main.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9c997ad1..f71ab5c5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -33,7 +33,7 @@ jobs: uses: jurplel/install-qt-action@v2 with: version: '5.14.2' - modules: 'qtwidgets qtqml' + modules: 'qtwidgets qtqml qtcharts' cached: ${{ steps.cache-qt.outputs.cache-hit }} - name: Configure @@ -58,7 +58,8 @@ jobs: - name: Install Qt uses: jurplel/install-qt-action@v3 with: - version: '6.5.*' + version: '6.7.*' + modules: 'qtcharts' cached: ${{ steps.cache-qt.outputs.cache-hit }} - name: Configure From 8240cf55ef425e2d7b2e49f7c24e8f0ebbb8f60c Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 27 Aug 2024 21:39:33 -0400 Subject: [PATCH 72/76] Exclude charts features for our Windows builds --- include/ui/wildmonchart.h | 23 +++++++++++++++++++++++ porymap.pro | 6 +++++- src/mainwindow.cpp | 4 ++++ src/ui/wildmonchart.cpp | 3 +++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/include/ui/wildmonchart.h b/include/ui/wildmonchart.h index a7c56a47..d4e3d83f 100644 --- a/include/ui/wildmonchart.h +++ b/include/ui/wildmonchart.h @@ -4,6 +4,8 @@ #include "encountertablemodel.h" #include + +#if __has_include() #include namespace Ui { @@ -72,4 +74,25 @@ private: void showHelpDialog(); }; +#else + +// As of writing our static Qt build for Windows doesn't include the QtCharts module, so we dummy the class out here. +// The charts module is additionally excluded from Windows in porymap.pro +#define DISABLE_CHARTS_MODULE + +class WildMonChart : public QWidget +{ + Q_OBJECT +public: + explicit WildMonChart(QWidget *, const EncounterTableModel *) {}; + ~WildMonChart() {}; + +public slots: + void setTable(const EncounterTableModel *) {}; + void clearTable() {}; + void refresh() {}; +}; + +#endif // __has_include() + #endif // WILDMONCHART_H diff --git a/porymap.pro b/porymap.pro index a7923595..f36f536f 100644 --- a/porymap.pro +++ b/porymap.pro @@ -4,7 +4,11 @@ # #------------------------------------------------- -QT += core gui qml network charts +QT += core gui qml network + +!win32 { + QT += charts +} greaterThan(QT_MAJOR_VERSION, 4): QT += widgets diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index f342396f..00251e6c 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -124,6 +124,10 @@ void MainWindow::initWindow() { ui->actionCheck_for_Updates->setVisible(false); #endif +#ifdef DISABLE_CHARTS_MODULE + ui->pushButton_SummaryChart->setVisible(false); +#endif + setWindowDisabled(true); } diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp index 011dc326..4594e2bd 100644 --- a/src/ui/wildmonchart.cpp +++ b/src/ui/wildmonchart.cpp @@ -1,3 +1,4 @@ +#if __has_include() #include "wildmonchart.h" #include "ui_wildmonchart.h" #include "config.h" @@ -447,3 +448,5 @@ void WildMonChart::closeEvent(QCloseEvent *event) { porymapConfig.wildMonChartGeometry = saveGeometry(); QWidget::closeEvent(event); } + +#endif // __has_include() From d41d4d53e7840261758c6707d05dd6887b38975a Mon Sep 17 00:00:00 2001 From: GriffinR Date: Thu, 29 Aug 2024 12:39:25 -0400 Subject: [PATCH 73/76] Interpret triple-layer metatiles from NUM_TILES_PER_METATILE --- CHANGELOG.md | 1 + docsrc/manual/project-files.rst | 1 + include/config.h | 1 + include/ui/projectsettingseditor.h | 2 +- src/config.cpp | 1 + src/project.cpp | 22 +++++++++- src/ui/projectsettingseditor.cpp | 69 ++++++++++++------------------ 7 files changed, 54 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86009d49..a0ad5950 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 - A notice will be displayed when attempting to open the "Dynamic" map, rather than nothing happening. - The base game version is now auto-detected if the project name contains only one of "emerald", "firered/leafgreen", or "ruby/sapphire". - It's now possible to cancel quitting if there are unsaved changes in sub-windows. +- The triple-layer metatiles setting can now be set automatically using a project constant. ### Fixed - Fix `Add Region Map...` not updating the region map settings file. diff --git a/docsrc/manual/project-files.rst b/docsrc/manual/project-files.rst index 20849ba3..678ded32 100644 --- a/docsrc/manual/project-files.rst +++ b/docsrc/manual/project-files.rst @@ -101,6 +101,7 @@ In addition to these files, there are some specific symbol and macro names that ``define_metatiles_primary``, ``NUM_METATILES_IN_PRIMARY``, total metatiles are calculated using metatile ID mask ``define_pals_primary``, ``NUM_PALS_IN_PRIMARY``, ``define_pals_total``, ``NUM_PALS_TOTAL``, + ``define_tiles_per_metatile``, ``NUM_TILES_PER_METATILE``, to determine if triple-layer metatiles are in use. Values other than 8 or 12 are ignored ``define_map_size``, ``MAX_MAP_DATA_SIZE``, to limit map dimensions ``define_mask_metatile``, ``MAPGRID_METATILE_ID_MASK``, optionally read to get settings on ``Maps`` tab ``define_mask_collision``, ``MAPGRID_COLLISION_MASK``, optionally read to get settings on ``Maps`` tab diff --git a/include/config.h b/include/config.h index 7486919f..d5f975a2 100644 --- a/include/config.h +++ b/include/config.h @@ -196,6 +196,7 @@ enum ProjectIdentifier { define_metatiles_primary, define_pals_primary, define_pals_total, + define_tiles_per_metatile, define_map_size, define_mask_metatile, define_mask_collision, diff --git a/include/ui/projectsettingseditor.h b/include/ui/projectsettingseditor.h index 7ebf544b..41affa8b 100644 --- a/include/ui/projectsettingseditor.h +++ b/include/ui/projectsettingseditor.h @@ -61,7 +61,7 @@ private: void chooseImageFile(QLineEdit * filepathEdit); void chooseFile(QLineEdit * filepathEdit, const QString &description, const QString &extensions); QString stripProjectDir(QString s); - void disableParsedSetting(QWidget * widget, const QString &name, const QString &filepath); + bool disableParsedSetting(QWidget * widget, const QString &identifier, const QString &filepath); void updateMaskOverlapWarning(QLabel * warning, QList masks); QStringList getWarpBehaviorsList(); void setWarpBehaviorsList(QStringList list); diff --git a/src/config.cpp b/src/config.cpp index 818dd714..1515a725 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -90,6 +90,7 @@ const QMap> ProjectConfig::defaultIde {ProjectIdentifier::define_metatiles_primary, {"define_metatiles_primary", "NUM_METATILES_IN_PRIMARY"}}, {ProjectIdentifier::define_pals_primary, {"define_pals_primary", "NUM_PALS_IN_PRIMARY"}}, {ProjectIdentifier::define_pals_total, {"define_pals_total", "NUM_PALS_TOTAL"}}, + {ProjectIdentifier::define_tiles_per_metatile, {"define_tiles_per_metatile", "NUM_TILES_PER_METATILE"}}, {ProjectIdentifier::define_map_size, {"define_map_size", "MAX_MAP_DATA_SIZE"}}, {ProjectIdentifier::define_mask_metatile, {"define_mask_metatile", "MAPGRID_METATILE_ID_MASK"}}, {ProjectIdentifier::define_mask_collision, {"define_mask_collision", "MAPGRID_COLLISION_MASK"}}, diff --git a/src/project.cpp b/src/project.cpp index 10ace396..b5614ffb 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -113,6 +113,7 @@ bool Project::sanityCheck() { } bool Project::load() { + this->disabledSettingsNames.clear(); bool success = readMapLayouts() && readRegionMapSections() && readItemNames() @@ -1960,6 +1961,7 @@ bool Project::readFieldmapProperties() { const QString numPalsPrimaryName = projectConfig.getIdentifier(ProjectIdentifier::define_pals_primary); const QString numPalsTotalName = projectConfig.getIdentifier(ProjectIdentifier::define_pals_total); const QString maxMapSizeName = projectConfig.getIdentifier(ProjectIdentifier::define_map_size); + const QString numTilesPerMetatileName = projectConfig.getIdentifier(ProjectIdentifier::define_tiles_per_metatile); const QStringList names = { numTilesPrimaryName, numTilesTotalName, @@ -1967,6 +1969,7 @@ bool Project::readFieldmapProperties() { numPalsPrimaryName, numPalsTotalName, maxMapSizeName, + numTilesPerMetatileName, }; const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_fieldmap); fileWatcher.addPath(root + "/" + filename); @@ -2007,6 +2010,22 @@ bool Project::readFieldmapProperties() { .arg(Project::max_map_data_size)); } + it = defines.find(numTilesPerMetatileName); + if (it != defines.end()) { + // We can determine whether triple-layer metatiles are in-use by reading this constant. + // If the constant is missing (or is using a value other than 8 or 12) the user must tell + // us whether they're using triple-layer metatiles under Project Settings. + static const int numTilesPerLayer = 4; + int numTilesPerMetatile = it.value(); + if (numTilesPerMetatile == 2 * numTilesPerLayer) { + projectConfig.tripleLayerMetatilesEnabled = false; + this->disabledSettingsNames.insert(numTilesPerMetatileName); + } else if (numTilesPerMetatile == 3 * numTilesPerLayer) { + projectConfig.tripleLayerMetatilesEnabled = true; + this->disabledSettingsNames.insert(numTilesPerMetatileName); + } + } + return true; } @@ -2032,7 +2051,8 @@ bool Project::readFieldmapMasks() { // If users do have the defines we disable them in the settings editor and direct them to their project files. // Record the names we read so we know later which settings to disable. const QStringList defineNames = defines.keys(); - this->disabledSettingsNames = QSet(defineNames.constBegin(), defineNames.constEnd()); + for (auto name : defineNames) + this->disabledSettingsNames.insert(name); // Read Block masks auto readBlockMask = [defines](const QString name, uint16_t *value) { diff --git a/src/ui/projectsettingseditor.cpp b/src/ui/projectsettingseditor.cpp index 8a360eb2..d84ef56b 100644 --- a/src/ui/projectsettingseditor.cpp +++ b/src/ui/projectsettingseditor.cpp @@ -133,53 +133,40 @@ void ProjectSettingsEditor::initUi() { ui->spinBox_CollisionMask->setMaximum(Block::maxValue); ui->spinBox_ElevationMask->setMaximum(Block::maxValue); - // Some settings can be determined by constants in the project. - // We reflect that here by disabling their UI elements. - if (project) { - const QString maskFilepath = projectConfig.getFilePath(ProjectFilePath::global_fieldmap); - const QString attrTableFilepath = projectConfig.getFilePath(ProjectFilePath::fieldmap); - const QString metatileIdMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_metatile); - const QString collisionMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_collision); - const QString elevationMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_elevation); - const QString behaviorMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_behavior); - const QString layerTypeMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_layer); - const QString behaviorTableName = projectConfig.getIdentifier(ProjectIdentifier::define_attribute_behavior); - const QString layerTypeTableName = projectConfig.getIdentifier(ProjectIdentifier::define_attribute_layer); - const QString encounterTypeTableName = projectConfig.getIdentifier(ProjectIdentifier::define_attribute_encounter); - const QString terrainTypeTableName = projectConfig.getIdentifier(ProjectIdentifier::define_attribute_terrain); - const QString attrTableName = projectConfig.getIdentifier(ProjectIdentifier::symbol_attribute_table); + // The values for some of the settings we provide in this window can be determined using constants in the user's projects. + // If the user has these constants we disable these settings in the UI -- they can modify them using their constants. + const QString globalFieldmapPath = projectConfig.getFilePath(ProjectFilePath::global_fieldmap); + const QString constantsFieldmapPath = projectConfig.getFilePath(ProjectFilePath::constants_fieldmap); + const QString fieldmapPath = projectConfig.getFilePath(ProjectFilePath::fieldmap); - // Block masks - if (project->disabledSettingsNames.contains(metatileIdMaskName)) - this->disableParsedSetting(ui->spinBox_MetatileIdMask, metatileIdMaskName, maskFilepath); - if (project->disabledSettingsNames.contains(collisionMaskName)) - this->disableParsedSetting(ui->spinBox_CollisionMask, collisionMaskName, maskFilepath); - if (project->disabledSettingsNames.contains(elevationMaskName)) - this->disableParsedSetting(ui->spinBox_ElevationMask, elevationMaskName, maskFilepath); + // Block masks + this->disableParsedSetting(ui->spinBox_MetatileIdMask, projectConfig.getIdentifier(ProjectIdentifier::define_mask_metatile), globalFieldmapPath); + this->disableParsedSetting(ui->spinBox_CollisionMask, projectConfig.getIdentifier(ProjectIdentifier::define_mask_collision), globalFieldmapPath); + this->disableParsedSetting(ui->spinBox_ElevationMask, projectConfig.getIdentifier(ProjectIdentifier::define_mask_elevation), globalFieldmapPath); - // Behavior mask - if (project->disabledSettingsNames.contains(behaviorMaskName)) - this->disableParsedSetting(ui->spinBox_BehaviorMask, behaviorMaskName, maskFilepath); - else if (project->disabledSettingsNames.contains(behaviorTableName)) - this->disableParsedSetting(ui->spinBox_BehaviorMask, attrTableName, attrTableFilepath); + // Behavior mask + if (!this->disableParsedSetting(ui->spinBox_BehaviorMask, projectConfig.getIdentifier(ProjectIdentifier::define_mask_behavior), globalFieldmapPath)) + this->disableParsedSetting(ui->spinBox_BehaviorMask, projectConfig.getIdentifier(ProjectIdentifier::define_attribute_behavior), fieldmapPath); - // Layer type mask - if (project->disabledSettingsNames.contains(layerTypeMaskName)) - this->disableParsedSetting(ui->spinBox_LayerTypeMask, layerTypeMaskName, maskFilepath); - else if (project->disabledSettingsNames.contains(layerTypeTableName)) - this->disableParsedSetting(ui->spinBox_LayerTypeMask, attrTableName, attrTableFilepath); + // Layer type mask + if (!this->disableParsedSetting(ui->spinBox_LayerTypeMask, projectConfig.getIdentifier(ProjectIdentifier::define_mask_layer), globalFieldmapPath)) + this->disableParsedSetting(ui->spinBox_LayerTypeMask, projectConfig.getIdentifier(ProjectIdentifier::define_attribute_layer), fieldmapPath); - // Encounter and terrain type masks - if (project->disabledSettingsNames.contains(encounterTypeTableName)) - this->disableParsedSetting(ui->spinBox_EncounterTypeMask, attrTableName, attrTableFilepath); - if (project->disabledSettingsNames.contains(terrainTypeTableName)) - this->disableParsedSetting(ui->spinBox_TerrainTypeMask, attrTableName, attrTableFilepath); - } + // Encounter and terrain type masks + this->disableParsedSetting(ui->spinBox_EncounterTypeMask, projectConfig.getIdentifier(ProjectIdentifier::define_attribute_encounter), fieldmapPath); + this->disableParsedSetting(ui->spinBox_TerrainTypeMask, projectConfig.getIdentifier(ProjectIdentifier::define_attribute_terrain), fieldmapPath); + + // Tripe layer metatiles + this->disableParsedSetting(ui->checkBox_EnableTripleLayerMetatiles, projectConfig.getIdentifier(ProjectIdentifier::define_tiles_per_metatile), constantsFieldmapPath); } -void ProjectSettingsEditor::disableParsedSetting(QWidget * widget, const QString &name, const QString &filepath) { - widget->setEnabled(false); - widget->setToolTip(QString("This value has been read from '%1' in %2").arg(name).arg(filepath)); +bool ProjectSettingsEditor::disableParsedSetting(QWidget * widget, const QString &identifier, const QString &filepath) { + if (project && project->disabledSettingsNames.contains(identifier)) { + widget->setEnabled(false); + widget->setToolTip(QString("This value has been set using '%1' in %2").arg(identifier).arg(filepath)); + return true; + } + return false; } // Remember the current settings tab for future sessions From 1a456bc47b3a8c25518c66978d48838e85a0ae61 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Fri, 30 Aug 2024 11:56:35 -0400 Subject: [PATCH 74/76] Save config in custom scripts editor, stop accidental 'refreshed' popup --- include/ui/customscriptseditor.h | 3 ++- src/ui/customscriptseditor.cpp | 18 +++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/include/ui/customscriptseditor.h b/include/ui/customscriptseditor.h index 27238c4e..0e5d3489 100644 --- a/include/ui/customscriptseditor.h +++ b/include/ui/customscriptseditor.h @@ -55,7 +55,8 @@ private slots: void dialogButtonClicked(QAbstractButton *button); void createNewScript(); void loadScript(); - void refreshScripts(); + bool refreshScripts(); + void userRefreshScripts(); void removeSelectedScripts(); void openSelectedScripts(); }; diff --git a/src/ui/customscriptseditor.cpp b/src/ui/customscriptseditor.cpp index 13650053..b7180b92 100644 --- a/src/ui/customscriptseditor.cpp +++ b/src/ui/customscriptseditor.cpp @@ -27,7 +27,7 @@ CustomScriptsEditor::CustomScriptsEditor(QWidget *parent) : connect(ui->button_CreateNewScript, &QAbstractButton::clicked, this, &CustomScriptsEditor::createNewScript); connect(ui->button_LoadScript, &QAbstractButton::clicked, this, &CustomScriptsEditor::loadScript); - connect(ui->button_RefreshScripts, &QAbstractButton::clicked, this, &CustomScriptsEditor::refreshScripts); + connect(ui->button_RefreshScripts, &QAbstractButton::clicked, this, &CustomScriptsEditor::userRefreshScripts); connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &CustomScriptsEditor::dialogButtonClicked); this->initShortcuts(); @@ -57,7 +57,7 @@ void CustomScriptsEditor::initShortcuts() { shortcut_load->setObjectName("shortcut_load"); shortcut_load->setWhatsThis("Load Script..."); - auto *shortcut_refresh = new Shortcut(QKeySequence(), this, SLOT(refreshScripts())); + auto *shortcut_refresh = new Shortcut(QKeySequence(), this, SLOT(userRefreshScripts())); shortcut_refresh->setObjectName("shortcut_refresh"); shortcut_refresh->setWhatsThis("Refresh Scripts"); @@ -238,14 +238,21 @@ void CustomScriptsEditor::openSelectedScripts() { this->openScript(item); } -void CustomScriptsEditor::refreshScripts() { +// When the user refreshes the scripts we show a little tooltip as feedback. +// We don't want this tooltip to display when we refresh programmatically, like when changes are saved. +void CustomScriptsEditor::userRefreshScripts() { + if (refreshScripts()) + QToolTip::showText(ui->button_RefreshScripts->mapToGlobal(QPoint(0, 0)), "Refreshed!"); +} + +bool CustomScriptsEditor::refreshScripts() { if (this->hasUnsavedChanges) { if (this->prompt("Scripts have been modified, save changes and reload the script engine?", QMessageBox::Yes) == QMessageBox::No) - return; + return false; this->save(); } - QToolTip::showText(ui->button_RefreshScripts->mapToGlobal(QPoint(0, 0)), "Refreshed!"); emit reloadScriptEngine(); + return true; } void CustomScriptsEditor::save() { @@ -264,6 +271,7 @@ void CustomScriptsEditor::save() { } userConfig.setCustomScripts(paths, enabledStates); + userConfig.save(); this->hasUnsavedChanges = false; this->refreshScripts(); } From 0b4f02779b073b3578b1ed8a2b4df762224621c1 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Fri, 30 Aug 2024 13:15:39 -0400 Subject: [PATCH 75/76] Fix regression for dragging multiple events --- include/editor.h | 3 +- include/ui/draggablepixmapitem.h | 12 +++--- src/core/editcommands.cpp | 2 +- src/editor.cpp | 12 ++---- src/mainwindow.cpp | 2 +- src/ui/draggablepixmapitem.cpp | 70 ++++++++++++++++++++++---------- 6 files changed, 60 insertions(+), 41 deletions(-) diff --git a/include/editor.h b/include/editor.h index 89b6ccb5..3e5cefd1 100644 --- a/include/editor.h +++ b/include/editor.h @@ -95,8 +95,7 @@ public: Tileset *getCurrentMapPrimaryTileset(); DraggablePixmapItem *addMapEvent(Event *event); - void selectMapEvent(DraggablePixmapItem *object); - void selectMapEvent(DraggablePixmapItem *object, bool toggle); + void selectMapEvent(DraggablePixmapItem *object, bool toggle = false); DraggablePixmapItem *addNewEvent(Event::Type type); void updateSelectedEvents(); void duplicateSelectedEvents(); diff --git a/include/ui/draggablepixmapitem.h b/include/ui/draggablepixmapitem.h index 9c318962..aeda4daf 100644 --- a/include/ui/draggablepixmapitem.h +++ b/include/ui/draggablepixmapitem.h @@ -24,13 +24,7 @@ public: updatePosition(); } - Editor *editor = nullptr; Event *event = nullptr; - QGraphicsItemAnimation *pos_anim = nullptr; - - bool active; - int last_x; - int last_y; void updatePosition(); void move(int dx, int dy); @@ -38,6 +32,12 @@ public: void emitPositionChanged(); void updatePixmap(); +private: + Editor *editor = nullptr; + QPoint lastPos; + bool active = false; + bool releaseSelectionQueued = false; + signals: void positionChanged(Event *event); void xChanged(int); diff --git a/src/core/editcommands.cpp b/src/core/editcommands.cpp index 640cb43f..f557dddd 100644 --- a/src/core/editcommands.cpp +++ b/src/core/editcommands.cpp @@ -331,7 +331,7 @@ void EventCreate::redo() { // select this event editor->selected_events->clear(); - editor->selectMapEvent(event->getPixmapItem(), false); + editor->selectMapEvent(event->getPixmapItem()); } void EventCreate::undo() { diff --git a/src/editor.cpp b/src/editor.cpp index 22957790..fc0c79ae 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -1358,7 +1358,7 @@ void Editor::mouseEvent_map(QGraphicsSceneMouseEvent *event, MapPixmapItem *item if (newEvent) { newEvent->move(pos.x(), pos.y()); emit objectsChanged(); - selectMapEvent(newEvent, false); + selectMapEvent(newEvent); } } } @@ -1994,10 +1994,6 @@ void Editor::updateSelectedEvents() { emit objectsChanged(); } -void Editor::selectMapEvent(DraggablePixmapItem *object) { - selectMapEvent(object, false); -} - void Editor::selectMapEvent(DraggablePixmapItem *object, bool toggle) { if (!selected_events || !object) return; @@ -2228,10 +2224,8 @@ void Editor::objectsView_onMousePress(QMouseEvent *event) { bool multiSelect = event->modifiers() & Qt::ControlModifier; if (!selectingEvent && !multiSelect && selected_events->length() > 1) { - DraggablePixmapItem *first = selected_events->first(); - selected_events->clear(); - selected_events->append(first); - updateSelectedEvents(); + // User is clearing group selection by clicking on the background + this->selectMapEvent(selected_events->first()); } selectingEvent = false; } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 23b7eb06..e441fa19 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2014,7 +2014,7 @@ void MainWindow::addNewEvent(Event::Type type) { auto centerPos = ui->graphicsView_Map->mapToScene(halfSize.width(), halfSize.height()); object->moveTo(Metatile::coordFromPixmapCoord(centerPos)); updateObjects(); - editor->selectMapEvent(object, false); + editor->selectMapEvent(object); } else { QMessageBox msgBox(this); msgBox.setText("Failed to add new event"); diff --git a/src/ui/draggablepixmapitem.cpp b/src/ui/draggablepixmapitem.cpp index 22511643..59b069dd 100644 --- a/src/ui/draggablepixmapitem.cpp +++ b/src/ui/draggablepixmapitem.cpp @@ -34,11 +34,26 @@ void DraggablePixmapItem::updatePixmap() { } void DraggablePixmapItem::mousePressEvent(QGraphicsSceneMouseEvent *mouse) { - active = true; - QPoint pos = Metatile::coordFromPixmapCoord(mouse->scenePos()); - last_x = pos.x(); - last_y = pos.y(); - this->editor->selectMapEvent(this, mouse->modifiers() & Qt::ControlModifier); + if (this->active) + return; + this->active = true; + this->lastPos = Metatile::coordFromPixmapCoord(mouse->scenePos()); + + bool selectionToggle = mouse->modifiers() & Qt::ControlModifier; + if (selectionToggle || !editor->selected_events->contains(this)) { + // User is either toggling this selection on/off as part of a group selection, + // or they're newly selecting just this item. + this->editor->selectMapEvent(this, selectionToggle); + } else { + // This item is already selected and the user isn't toggling the selection, so there are 4 possibilities: + // 1. This is the only selected event, and the selection is pointless. + // 2. This is the only selected event, and they want to drag the item around. + // 3. There's a group selection, and they want to start a new selection with just this item. + // 4. There's a group selection, and they want to drag the group around. + // 'selectMapEvent' will immediately clear the rest of the selection, which supports #1-3 but prevents #4. + // To support #4 we set the flag below, and we only call 'selectMapEvent' on mouse release if no move occurred. + this->releaseSelectionQueued = true; + } this->editor->selectingEvent = true; } @@ -57,28 +72,39 @@ void DraggablePixmapItem::moveTo(const QPoint &pos) { } void DraggablePixmapItem::mouseMoveEvent(QGraphicsSceneMouseEvent *mouse) { - if (active) { - QPoint pos = Metatile::coordFromPixmapCoord(mouse->scenePos()); - if (pos.x() != last_x || pos.y() != last_y) { - emit this->editor->map_item->hoveredMapMetatileChanged(pos); - QList selectedEvents; - if (editor->selected_events->contains(this)) { - for (DraggablePixmapItem *item : *editor->selected_events) { - selectedEvents.append(item->event); - } - } else { - selectedEvents.append(this->event); - } - editor->map->editHistory.push(new EventMove(selectedEvents, pos.x() - last_x, pos.y() - last_y, currentActionId)); - last_x = pos.x(); - last_y = pos.y(); + if (!this->active) + return; + + QPoint pos = Metatile::coordFromPixmapCoord(mouse->scenePos()); + if (pos == this->lastPos) + return; + + QPoint moveDistance = pos - this->lastPos; + this->lastPos = pos; + emit this->editor->map_item->hoveredMapMetatileChanged(pos); + + QList selectedEvents; + if (editor->selected_events->contains(this)) { + for (DraggablePixmapItem *item : *editor->selected_events) { + selectedEvents.append(item->event); } + } else { + selectedEvents.append(this->event); } + editor->map->editHistory.push(new EventMove(selectedEvents, moveDistance.x(), moveDistance.y(), currentActionId)); + this->releaseSelectionQueued = false; } -void DraggablePixmapItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *) { - active = false; +void DraggablePixmapItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *mouse) { + if (!this->active) + return; + this->active = false; currentActionId++; + if (this->releaseSelectionQueued) { + this->releaseSelectionQueued = false; + if (Metatile::coordFromPixmapCoord(mouse->scenePos()) == this->lastPos) + this->editor->selectMapEvent(this); + } } void DraggablePixmapItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *) { From 9ae336080edd6ac6fe0378ef8d2b0f55c4590444 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Fri, 30 Aug 2024 23:33:46 -0400 Subject: [PATCH 76/76] Consolidate event constructor calls --- include/core/events.h | 2 ++ src/core/events.cpp | 15 +++++++++++ src/editor.cpp | 62 +++++++++++-------------------------------- src/mainwindow.cpp | 61 +++++++++++------------------------------- 4 files changed, 47 insertions(+), 93 deletions(-) diff --git a/include/core/events.h b/include/core/events.h index 8be1bbfc..2e00ef72 100644 --- a/include/core/events.h +++ b/include/core/events.h @@ -118,6 +118,8 @@ public: } } + static Event* create(Event::Type type); + static QMap icons; // standard public methods diff --git a/src/core/events.cpp b/src/core/events.cpp index b0ea3e39..13be4153 100644 --- a/src/core/events.cpp +++ b/src/core/events.cpp @@ -6,6 +6,21 @@ QMap Event::icons; +Event* Event::create(Event::Type type) { + switch (type) { + case Event::Type::Object: return new ObjectEvent(); + case Event::Type::CloneObject: return new CloneObjectEvent(); + case Event::Type::Warp: return new WarpEvent(); + case Event::Type::Trigger: return new TriggerEvent(); + case Event::Type::WeatherTrigger: return new WeatherTriggerEvent(); + case Event::Type::Sign: return new SignEvent(); + case Event::Type::HiddenItem: return new HiddenItemEvent(); + case Event::Type::SecretBase: return new SecretBaseEvent(); + case Event::Type::HealLocation: return new HealLocationEvent(); + default: return nullptr; + } +} + Event::~Event() { if (this->eventFrame) this->eventFrame->deleteLater(); diff --git a/src/editor.cpp b/src/editor.cpp index fc0c79ae..b2e674da 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -2003,10 +2003,10 @@ void Editor::selectMapEvent(DraggablePixmapItem *object, bool toggle) { selected_events->clear(); selected_events->append(object); } else if (!selected_events->contains(object)) { - // Adding event to selection + // Adding event to group selection selected_events->append(object); } else if (selected_events->length() > 1) { - // Removing from group selection + // Removing event from group selection selected_events->removeOne(object); } else { // Attempting to toggle the only currently-selected event. @@ -2068,56 +2068,24 @@ void Editor::duplicateSelectedEvents() { } DraggablePixmapItem *Editor::addNewEvent(Event::Type type) { - Event *event = nullptr; + if (!project || !map || eventLimitReached(type)) + return nullptr; - if (project && map && !eventLimitReached(type)) { - switch (type) { - case Event::Type::Object: - event = new ObjectEvent(); - break; - case Event::Type::CloneObject: - event = new CloneObjectEvent(); - break; - case Event::Type::Warp: - event = new WarpEvent(); - break; - case Event::Type::Trigger: - event = new TriggerEvent(); - break; - case Event::Type::WeatherTrigger: - event = new WeatherTriggerEvent(); - break; - case Event::Type::Sign: - event = new SignEvent(); - break; - case Event::Type::HiddenItem: - event = new HiddenItemEvent(); - break; - case Event::Type::SecretBase: - event = new SecretBaseEvent(); - break; - case Event::Type::HealLocation: { - event = new HealLocationEvent(); - event->setMap(this->map); - event->setDefaultValues(this->project); - HealLocation healLocation = HealLocation::fromEvent(event); - project->healLocations.append(healLocation); - ((HealLocationEvent *)event)->setIndex(project->healLocations.length()); - break; - } - default: - break; - } - if (!event) return nullptr; + Event *event = Event::create(type); + if (!event) + return nullptr; - event->setMap(this->map); - event->setDefaultValues(this->project); + event->setMap(this->map); + event->setDefaultValues(this->project); - map->editHistory.push(new EventCreate(this, map, event)); - return event->getPixmapItem(); + if (type == Event::Type::HealLocation) { + HealLocation healLocation = HealLocation::fromEvent(event); + project->healLocations.append(healLocation); + ((HealLocationEvent *)event)->setIndex(project->healLocations.length()); } - return nullptr; + map->editHistory.push(new EventCreate(this, map, event)); + return event->getPixmapItem(); } // Currently only object events have an explicit limit diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index e441fa19..b9747e10 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1761,7 +1761,7 @@ void MainWindow::paste() { editor->metatile_selector_item->setExternalSelection(width, height, metatiles, collisions); break; } - case 1: + case MainTab::Events: { // can only paste events to this tab if (pasteObject["object"].toString() != "events") { @@ -1773,56 +1773,25 @@ void MainWindow::paste() { QJsonArray events = pasteObject["events"].toArray(); for (QJsonValue event : events) { // paste the event to the map - Event *pasteEvent = nullptr; - - Event::Type type = Event::eventTypeFromString(event["event_type"].toString()); + const QString typeString = event["event_type"].toString(); + Event::Type type = Event::eventTypeFromString(typeString); if (this->editor->eventLimitReached(type)) { - logWarn(QString("Cannot paste event, the limit for type '%1' has been reached.").arg(event["event_type"].toString())); - break; + logWarn(QString("Cannot paste event, the limit for type '%1' has been reached.").arg(typeString)); + continue; + } + if (type == Event::Type::HealLocation) { + logWarn(QString("Cannot paste events of type '%1'").arg(typeString)); + continue; } - switch (type) { - case Event::Type::Object: - pasteEvent = new ObjectEvent(); - pasteEvent->loadFromJson(event["event"].toObject(), this->editor->project); - break; - case Event::Type::CloneObject: - pasteEvent = new CloneObjectEvent(); - pasteEvent->loadFromJson(event["event"].toObject(), this->editor->project); - break; - case Event::Type::Warp: - pasteEvent = new WarpEvent(); - pasteEvent->loadFromJson(event["event"].toObject(), this->editor->project); - break; - case Event::Type::Trigger: - pasteEvent = new TriggerEvent(); - pasteEvent->loadFromJson(event["event"].toObject(), this->editor->project); - break; - case Event::Type::WeatherTrigger: - pasteEvent = new WeatherTriggerEvent(); - pasteEvent->loadFromJson(event["event"].toObject(), this->editor->project); - break; - case Event::Type::Sign: - pasteEvent = new SignEvent(); - pasteEvent->loadFromJson(event["event"].toObject(), this->editor->project); - break; - case Event::Type::HiddenItem: - pasteEvent = new HiddenItemEvent(); - pasteEvent->loadFromJson(event["event"].toObject(), this->editor->project); - break; - case Event::Type::SecretBase: - pasteEvent = new SecretBaseEvent(); - pasteEvent->loadFromJson(event["event"].toObject(), this->editor->project); - break; - default: - break; - } + Event *pasteEvent = Event::create(type); + if (!pasteEvent) + continue; - if (pasteEvent) { - pasteEvent->setMap(this->editor->map); - newEvents.append(pasteEvent); - } + pasteEvent->loadFromJson(event["event"].toObject(), this->editor->project); + pasteEvent->setMap(this->editor->map); + newEvents.append(pasteEvent); } if (!newEvents.empty()) {