diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 55edb7b6..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.2.*' + version: '6.7.*' + modules: 'qtcharts' cached: ${{ steps.cache-qt.outputs.cache-hit }} - name: Configure diff --git a/CHANGELOG.md b/CHANGELOG.md index a6d13ce3..f5e33e44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,42 @@ 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. +### 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 +- 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 "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. +- 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. +- 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). +- 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. +- 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. +- 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 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 a0419ed3..fe897bc6 100644 Binary files a/docsrc/manual/images/editing-map-connections/add-connection-button.png and b/docsrc/manual/images/editing-map-connections/add-connection-button.png differ diff --git a/docsrc/manual/images/editing-map-connections/map-connections.png b/docsrc/manual/images/editing-map-connections/map-connections.png index 8d8516c5..1cf7b660 100644 Binary files a/docsrc/manual/images/editing-map-connections/map-connections.png and b/docsrc/manual/images/editing-map-connections/map-connections.png differ 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 00000000..e1600703 Binary files /dev/null and b/docsrc/manual/images/editing-map-connections/open-connection-button.png differ diff --git a/docsrc/manual/project-files.rst b/docsrc/manual/project-files.rst index 20849ba3..30f0acc8 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,11 +97,13 @@ 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 ``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 @@ -120,6 +123,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 +138,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/connectionslistitem.ui b/forms/connectionslistitem.ui new file mode 100644 index 00000000..bf04e8be --- /dev/null +++ b/forms/connectionslistitem.ui @@ -0,0 +1,132 @@ + + + ConnectionsListItem + + + + 0 + 0 + 178 + 157 + + + + + 0 + 0 + + + + .ConnectionsListItem { border-width: 1px; } + + + QFrame::StyledPanel + + + + + + + 0 + 0 + + + + Map + + + + + + + + 0 + 0 + + + + Offset + + + + + + + + 0 + 0 + + + + Direction + + + + + + + Remove this connection. + + + ... + + + + :/icons/delete.ico:/icons/delete.ico + + + + + + + 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. + + + + + + + Open the connected map. + + + ... + + + + :/icons/map_go.ico:/icons/map_go.ico + + + + + + + + NoScrollComboBox + QComboBox +
noscrollcombobox.h
+
+ + NoScrollSpinBox + QSpinBox +
noscrollspinbox.h
+
+
+ + + + +
diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index 91c120a9..e954f701 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -2090,8 +2090,8 @@ 0 0 - 85 - 16 + 100 + 30 @@ -2184,8 +2184,8 @@ 0 0 - 85 - 16 + 100 + 30 @@ -2278,8 +2278,8 @@ 0 0 - 85 - 16 + 100 + 30 @@ -2378,8 +2378,8 @@ 0 0 - 85 - 16 + 100 + 30 @@ -2472,8 +2472,8 @@ 0 0 - 85 - 16 + 100 + 30 @@ -2917,7 +2917,7 @@ 0 - + 0 @@ -2930,24 +2930,9 @@ QFrame::Raised - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - + + + 0 @@ -2966,10 +2951,7 @@ QFrame::Raised - - - 4 - + 4 @@ -2982,82 +2964,20 @@ 4 - - - - - 0 - 0 - - + + - <html><head/><body><p>Add a new connection.</p></body></html> + <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> - - - - - - :/icons/add.ico - + + true - - - - <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> + If enabled, connections will automatically be updated on the connected map. Mirror to Connecting Maps @@ -3067,144 +2987,31 @@ - - - - - - - - 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 + Dive Map - - + + + + Emerge Map + + + + + - <html><head/><body><p>The destination map name of the connection.</p></body></html> + <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 - - - - 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 @@ -3217,87 +3024,274 @@ + + + + Open the selected Dive Map + + + ... + + + + :/icons/map_go.ico:/icons/map_go.ico + + + + + + + If enabled, the connected Emerge and/or Dive maps will be displayed with an opacity set using the slider. + + + 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 + + + - - - - - 0 - 0 - + + + + Qt::Horizontal - - QFrame::StyledPanel - - - QFrame::Raised - - - - 4 + + + + 0 + 0 + - - 4 + + false - - 4 + + false - - 4 + + Qt::ScrollBarAsNeeded - - 4 + + Qt::ScrollBarAsNeeded - - - - 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 - - - - - - - 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> - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - + + QAbstractScrollArea::AdjustIgnored + + + QGraphicsView::NoDrag + + + QGraphicsView::AnchorUnderMouse + + + QGraphicsView::AnchorUnderMouse + + + + + + 230 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + Add Connection + + + + :/icons/add.ico:/icons/add.ico + + + + + + + QFrame::NoFrame + + + Qt::ScrollBarAlwaysOff + + + true + + + + + 0 + 0 + 365 + 651 + + + + + 8 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + @@ -3386,6 +3380,13 @@ + + + + Summary Chart... + + + @@ -3437,6 +3438,7 @@ + @@ -3460,6 +3462,7 @@ + @@ -3791,6 +3794,25 @@ QAction::ApplicationSpecificRole + + + Close Project + + + Ctrl+W + + + + + true + + + true + + + Dive/Emerge Map + + diff --git a/forms/newmapconnectiondialog.ui b/forms/newmapconnectiondialog.ui new file mode 100644 index 00000000..9b3a3b6e --- /dev/null +++ b/forms/newmapconnectiondialog.ui @@ -0,0 +1,136 @@ + + + NewMapConnectionDialog + + + + 0 + 0 + 234 + 162 + + + + Add New Map Connection + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Map + + + + + + + The name of the map to connect to the current map. + + + + + + + Direction + + + + + + + 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 + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + NoScrollComboBox + QComboBox +
noscrollcombobox.h
+
+
+ + + + buttonBox + accepted() + NewMapConnectionDialog + accept() + + + 20 + 20 + + + 20 + 20 + + + + + buttonBox + rejected() + NewMapConnectionDialog + reject() + + + 20 + 20 + + + 20 + 20 + + + + +
diff --git a/forms/wildmonchart.ui b/forms/wildmonchart.ui new file mode 100644 index 00000000..488e066e --- /dev/null +++ b/forms/wildmonchart.ui @@ -0,0 +1,245 @@ + + + WildMonChart + + + + 0 + 0 + 785 + 492 + + + + Wild Pokémon Summary + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + + 0 + + + 0 + + + 0 + + + 4 + + + + + Theme + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + ... + + + + :/icons/help.ico:/icons/help.ico + + + + + + + + + + 0 + + + + Species Distribution + + + + 0 + + + 0 + + + 0 + + + + + QPainter::Antialiasing + + + + + + + + Level Distribution + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::NoFrame + + + QFrame::Plain + + + + 12 + + + 0 + + + + + Group + + + + + + + false + + + QComboBox::AdjustToMinimumContentsLength + + + 8 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Individual Mode + + + true + + + false + + + + 4 + + + 4 + + + 4 + + + 4 + + + + + Species + + + + + + + true + + + QComboBox::NoInsert + + + QComboBox::AdjustToMinimumContentsLengthWithIcon + + + 12 + + + + + + + + + + + + + QPainter::Antialiasing + + + + + + + + + + + + QChartView + QGraphicsView +
QtCharts
+
+
+ + + + +
diff --git a/include/config.h b/include/config.h index 84af6dcf..79e7e56e 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 @@ -57,9 +54,15 @@ public: } virtual void reset() override { this->recentProjects.clear(); + this->projectManuallyClosed = false; this->reopenOnLaunch = true; this->mapSortOrder = MapSortOrder::SortByGroup; this->prettyCursors = true; + this->mirrorConnectingMaps = true; + this->showDiveEmergeMaps = false; + this->diveEmergeMapOpacity = 30; + this->diveMapOpacity = 15; + this->emergeMapOpacity = 15; this->collisionOpacity = 50; this->collisionZoom = 30; this->metatilesZoom = 30; @@ -74,6 +77,7 @@ public: this->monitorFiles = true; this->tilesetCheckerboardFill = true; this->theme = "default"; + this->wildMonChartTheme = ""; this->textEditorOpenFolder = ""; this->textEditorGotoLine = ""; this->paletteEditorBitDepth = 24; @@ -86,85 +90,68 @@ 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(); + + bool reopenOnLaunch; + bool projectManuallyClosed; + MapSortOrder mapSortOrder; + bool prettyCursors; + bool mirrorConnectingMaps; + bool showDiveEmergeMaps; + int diveEmergeMapOpacity; + int diveMapOpacity; + int emergeMapOpacity; + int collisionOpacity; + int collisionZoom; + int metatilesZoom; + int tilesetEditorMetatilesZoom; + int tilesetEditorTilesZoom; + bool showPlayerView; + bool showCursorTile; + bool showBorder; + bool showGrid; + bool showTilesetEditorMetatileGrid; + bool showTilesetEditorLayerGrid; + bool monitorFiles; + bool tilesetCheckerboardFill; + QString theme; + QString wildMonChartTheme; + QString textEditorOpenFolder; + QString textEditorGotoLine; + int paletteEditorBitDepth; + int projectSettingsTab; + bool warpBehaviorWarningDisabled; + bool checkForUpdates; + QDateTime lastUpdateCheckTime; + QVersionNumber lastUpdateCheckVersion; + QMap rateLimitTimes; + QByteArray wildMonChartGeometry; + 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: - QStringList recentProjects; - bool reopenOnLaunch; QString stringFromByteArray(QByteArray); QByteArray bytesFromString(QString); - MapSortOrder mapSortOrder; - bool prettyCursors; + + QStringList recentProjects; QByteArray mainWindowGeometry; QByteArray mainWindowState; QByteArray mapSplitterState; @@ -181,34 +168,12 @@ private: QByteArray projectSettingsEditorState; QByteArray customScriptsEditorGeometry; QByteArray customScriptsEditorState; - int collisionOpacity; - int collisionZoom; - int metatilesZoom; - int tilesetEditorMetatilesZoom; - int tilesetEditorTilesZoom; - bool showPlayerView; - bool showCursorTile; - bool showBorder; - bool showGrid; - bool showTilesetEditorMetatileGrid; - bool showTilesetEditorLayerGrid; - bool monitorFiles; - bool tilesetCheckerboardFill; - QString theme; - QString textEditorOpenFolder; - QString textEditorGotoLine; - int paletteEditorBitDepth; - int projectSettingsTab; - bool warpBehaviorWarningDisabled; - bool checkForUpdates; - QDateTime lastUpdateCheckTime; - QVersionNumber lastUpdateCheckVersion; - QMap rateLimitTimes; }; extern PorymapConfig porymapConfig; enum BaseGameVersion { + none, pokeruby, pokefirered, pokeemerald, @@ -229,11 +194,13 @@ 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, define_pals_primary, define_pals_total, + define_tiles_per_metatile, define_map_size, define_mask_metatile, define_mask_collision, @@ -253,6 +220,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, @@ -267,7 +235,6 @@ enum ProjectIdentifier { regex_sign_facing_directions, regex_trainer_types, regex_music, - regex_species, }; enum ProjectFilePath { @@ -316,6 +283,7 @@ enum ProjectFilePath { global_fieldmap, fieldmap, initial_facing_table, + wild_encounter, pokemon_icon_table, pokemon_gfx, }; @@ -330,7 +298,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 +322,11 @@ public: static const QMap> defaultIdentifiers; static const QMap> defaultPaths; static const QStringList versionStrings; + static BaseGameVersion stringToBaseGameVersion(const QString &string); + 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 +335,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 +376,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; @@ -513,35 +412,30 @@ public: this->customScripts.clear(); this->readKeys.clear(); } - void setRecentMap(const QString &map); - QString getRecentMap(); - void setRecentLayout(const QString &map); - QString getRecentLayout(); - 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(); -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; -#ifdef CONFIG_BACKWARDS_COMPATABILITY - friend class ProjectConfig; -#endif -private: + QString projectDir; QString recentMap; QString recentLayout; bool useEncounterJson; - QMap customScripts; + +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; +#ifdef CONFIG_BACKWARDS_COMPATABILITY + friend class ProjectConfig; +#endif + +private: QStringList readKeys; + QMap customScripts; }; extern UserConfig userConfig; @@ -571,7 +465,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/core/editcommands.h b/include/core/editcommands.h index 691746c0..5bf99784 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 Map; class Layout; @@ -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/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/include/core/map.h b/include/core/map.h index 08bbcbb1..03a8931d 100644 --- a/include/core/map.h +++ b/include/core/map.h @@ -72,8 +72,6 @@ public: QMap> events; QList ownedEvents; // for memory management - QList connections; - QList metatileLayerOrder; QList metatileLayerOpacity; @@ -92,21 +90,34 @@ public: void removeEvent(Event *); void addEvent(Event *); + void deleteConnections(); + QList getConnections() const; + void removeConnection(MapConnection *); + void addConnection(MapConnection *); + void loadConnection(MapConnection *); + QRect getConnectionRect(const QString &direction, Layout *fromLayout = nullptr); + QPixmap renderConnection(const QString &direction, Layout *fromLayout = nullptr); + QUndoStack editHistory; void modify(); void clean(); bool hasUnsavedChanges(); + void pruneEditHistory(); - QPixmap renderConnection(MapConnection, Layout *); - +private: + void trackConnection(MapConnection*); + // MapConnections in 'ownedConnections' but not 'connections' persist in the edit history. + QList connections; + QSet ownedConnections; signals: - void mapChanged(Map *map); 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 11aa0e59..21c00ac6 100644 --- a/include/core/mapconnection.h +++ b/include/core/mapconnection.h @@ -3,21 +3,61 @@ #define MAPCONNECTION_H #include -#include +#include +#include -class MapConnection { +class Project; +class Map; + +class MapConnection : public QObject +{ + Q_OBJECT public: - QString direction; - int offset; - QString map_name; + 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, bool mirror = true); + + int offset() const { return m_offset; } + void setOffset(int offset, bool mirror = true); + + 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 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; + QString m_targetMapName; + QString m_direction; + int m_offset; + + void markMapEdited(); + Map* getMap(const QString& mapName) const; + +signals: + void parentMapChanged(Map* before, Map* after); + void targetMapNameChanged(QString before, QString after); + void directionChanged(QString before, QString after); + void offsetChanged(int before, int after); }; -inline bool operator==(const MapConnection &c1, const MapConnection &c2) { - return c1.map_name == c2.map_name; -} - -inline uint qHash(const MapConnection &key) { - return qHash(key.map_name); -} - #endif // MAPCONNECTION_H 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/editor.h b/include/editor.h index 370727e4..93869fc4 100644 --- a/include/editor.h +++ b/include/editor.h @@ -19,6 +19,7 @@ #include "ui_mainwindow.h" #include "bordermetatilespixmapitem.h" #include "connectionpixmapitem.h" +#include "divingmappixmapitem.h" #include "currentselectedmetatilespixmapitem.h" #include "collisionpixmapitem.h" #include "layoutpixmapitem.h" @@ -26,6 +27,7 @@ #include "movablerect.h" #include "cursortilerect.h" #include "mapruler.h" +#include "encountertablemodel.h" class DraggablePixmapItem; class MetatilesPixmapItem; @@ -45,7 +47,7 @@ public: Ui::MainWindow* ui; QObject *parent = nullptr; - Project *project = nullptr; + QPointer project = nullptr; QPointer map = nullptr; QPointer layout = nullptr; @@ -53,6 +55,7 @@ public: Settings *settings; + void setProject(Project * project); void save(); void saveProject(); void saveUiFields(); @@ -85,20 +88,19 @@ public: void updateMapBorder(); void updateMapConnections(); - 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 updateDiveMap(QString mapName); - void updateEmergeMap(QString mapName); - void setSelectedConnectionFromMap(QString mapName); - + void updateDivingMapsVisibility(); + void renderDivingConnections(); + void addConnection(MapConnection* connection); + void removeConnection(MapConnection* connection); + void removeSelectedConnection(); void addNewWildMonGroup(QWidget *window); void deleteWildMonGroup(); void configureEncounterJSON(QWidget *); + EncounterTableModel* getCurrentWildMonTable(); + void updateDiveMap(QString mapName); + void updateEmergeMap(QString mapName); + void setSelectedConnection(MapConnection *connection); void updatePrimaryTileset(QString tilesetLabel, bool forceLoad = false); void updateSecondaryTileset(QString tilesetLabel, bool forceLoad = false); @@ -107,8 +109,7 @@ public: DraggablePixmapItem *addMapEvent(Event *event); bool eventLimitReached(Map *, Event::Type); - 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(); @@ -121,13 +122,13 @@ public: void updateWarpEventWarning(Event *event); void updateWarpEventWarnings(); - QGraphicsScene *scene = nullptr; + QPointer scene = nullptr; QGraphicsPixmapItem *current_view = nullptr; - LayoutPixmapItem *map_item = nullptr; - ConnectionPixmapItem* selected_connection_item = nullptr; - QList connection_items; + QPointer map_item = nullptr; + QList> connection_items; + QMap> diving_map_items; QGraphicsPathItem *connection_mask = nullptr; - CollisionPixmapItem *collision_item = nullptr; + QPointer collision_item = nullptr; QGraphicsItemGroup *events_group = nullptr; QList borderItems; @@ -137,19 +138,19 @@ public: MovableRect *playerViewRect = nullptr; CursorTileRect *cursorMapTileRect = 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; + 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; - MetatileSelector *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; + QPointer selected_connection_item = nullptr; + QPointer connection_to_select = nullptr; enum class EditAction { None, Paint, Select, Fill, Shift, Pick, Move }; EditAction mapEditAction = EditAction::Paint; @@ -202,21 +203,28 @@ private: const QImage collisionPlaceholder = QImage(":/images/collisions_unknown.png"); QPixmap collisionSheetPixmap; - 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 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); + void clearMap(); + void clearMetatileSelector(); + void clearMovementPermissionSelector(); + void clearMapMetatiles(); + void clearMapMovementPermissions(); + void clearBorderMetatiles(); + void clearCurrentMetatilesSelection(); + void clearMapEvents(); + void clearMapConnections(); + void clearConnectionMask(); + void clearMapBorder(); + 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 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); @@ -232,10 +240,7 @@ private slots: void setStraightPathCursorMode(QGraphicsSceneMouseEvent *event); void mouseEvent_map(QGraphicsSceneMouseEvent *event, LayoutPixmapItem *item); void mouseEvent_collision(QGraphicsSceneMouseEvent *event, CollisionPixmapItem *item); - void onConnectionMoved(MapConnection*); - void onConnectionItemSelected(ConnectionPixmapItem* connectionItem); - void onConnectionItemDoubleClicked(ConnectionPixmapItem* connectionItem); - void onConnectionDirectionChanged(QString newDirection); + void setSelectedConnectionItem(ConnectionPixmapItem *connectionItem); void onHoveredMovementPermissionChanged(uint16_t, uint16_t); void onHoveredMovementPermissionCleared(); void onHoveredMetatileSelectionChanged(uint16_t); @@ -250,12 +255,13 @@ private slots: signals: void objectsChanged(); - void loadMapRequested(QString, QString); - void wildMonDataChanged(); + void openConnectedMap(MapConnection*); + void wildMonTableOpened(EncounterTableModel*); + void wildMonTableClosed(); + void wildMonTableEdited(); void warpEventDoubleClicked(QString, int, Event::Group); void currentMetatilesSelectionChanged(); void mapRulerStatusChanged(const QString &); - void editedMapData(); void tilesetUpdated(QString); }; diff --git a/include/mainwindow.h b/include/mainwindow.h index 7ee2786c..c8502d82 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -28,7 +28,9 @@ #include "preferenceeditor.h" #include "projectsettingseditor.h" #include "customscriptseditor.h" +#include "wildmonchart.h" #include "updatepromoter.h" +#include "aboutporymap.h" @@ -168,6 +170,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_areaList_activated(const QModelIndex &index); @@ -183,18 +186,19 @@ private slots: void copy(); void paste(); - void onLoadMapRequested(QString, QString); - void onMapChanged(Map *map); void onLayoutChanged(Layout *layout); + void onOpenConnectedMap(MapConnection*); void onMapNeedsRedrawing(); void onLayoutNeedsRedrawing(); void onTilesetsSaved(QString, QString); - void onWildMonDataChanged(); void openNewMapPopupWindow(); void onNewMapCreated(); + void onMapLoaded(Map *map); void importMapFromAdvanceMap1_92(); void onMapRulerStatusChanged(const QString &); void applyUserShortcuts(); + void markMapEdited(); + void markSpecificMapEdited(Map*); void on_action_NewMap_triggered(); void on_actionNew_Tileset_triggered(); @@ -225,10 +229,11 @@ private slots: void on_actionMove_triggered(); void on_actionMap_Shift_triggered(); + void onDeleteKeyPressed(); 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(); @@ -252,11 +257,9 @@ 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); void on_comboBox_EmergeMap_currentTextChanged(const QString &mapName); void on_comboBox_PrimaryTileset_currentTextChanged(const QString &arg1); @@ -279,6 +282,12 @@ 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); + void on_slider_DiveMapOpacity_valueChanged(int value); + void on_slider_EmergeMapOpacity_valueChanged(int value); void on_horizontalSlider_CollisionTransparency_valueChanged(int value); void do_HideShow(); @@ -305,6 +314,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); @@ -319,7 +329,7 @@ private slots: public: Ui::MainWindow *ui; - Editor *editor = nullptr; + QPointer editor = nullptr; private: QLabel *label_MapRulerStatus = nullptr; @@ -343,6 +353,8 @@ private: QPointer updatePromoter = nullptr; QPointer networkAccessManager = nullptr; + QPointer aboutWindow = nullptr; + QPointer wildMonChart = nullptr; QAction *undoAction = nullptr; QAction *redoAction = nullptr; @@ -350,22 +362,11 @@ private: QAction *copyAction = nullptr; QAction *pasteAction = nullptr; - QWidget *eventTabObjectWidget; - QWidget *eventTabWarpWidget; - QWidget *eventTabTriggerWidget; - QWidget *eventTabBGWidget; - QWidget *eventTabHealspotWidget; - QWidget *eventTabMultipleWidget; QMap lastSelectedEvent; bool isProgrammaticEventTabChange; - bool projectHasUnsavedChanges; - bool projectOpenFailure = false; bool newMapDefaultsSet = false; - MapSortOrder mapSortOrder; - enum MapListTab { Groups = 0, Areas, Layouts }; - bool tilesetNeedsRedraw = false; bool setDefaultView(); @@ -373,26 +374,34 @@ private: bool setLayout(QString layoutId); bool setMap(QString, bool scroll = false); void unsetMap(); - + bool userSetMap(QString, bool scrollTreeView = false); void redrawMapScene(); void redrawLayoutScene(); void refreshMapScene(); - bool loadDataStructures(); - bool loadProjectCombos(); - bool populateMapList(); + + bool checkProjectSanity(); + bool loadProjectData(); + bool setProjectUI(); + void clearProjectUI(); + void openSubWindow(QWidget * window); void scrollTreeView(QString itemName); QString getExistingDirectory(QString); - bool openProject(const QString &dir, bool initial = false); + bool openProject(QString dir, bool initial = false); + bool closeProject(); void showProjectOpenFailure(); - QString getDefaultMap(); - QString getDefaultLayout(); - void setRecentMapConfig(QString map_name); - void setRecentLayoutConfig(QString layoutId); + + QStandardItem* createMapItem(QString mapName, int groupNum, int inGroupNum); + bool setInitialMap(); bool setInitialLayout(); - void setRecentMap(QString map_name); - QStandardItem* createMapItem(QString mapName, int groupNum, int inGroupNum); + QString getDefaultMap(); + QString getDefaultLayout(); + + void setRecentMapConfig(QString map_name); + void setRecentLayoutConfig(QString layoutId); + void saveGlobalConfigs(); + void refreshRecentProjectsMenu(); void updateMapList(); @@ -409,7 +418,6 @@ private: void checkToolButtons(); void clickToolButtonFromEditAction(Editor::EditAction editAction); - void markMapEdited(); void showWindowTitle(); void initWindow(); @@ -420,18 +428,18 @@ private: void initMapSortOrder(); void initShortcuts(); void initExtraShortcuts(); - void setProjectSpecificUI(); void loadUserSettings(); void applyMapListFilter(QString filterText); void restoreWindowState(); void setTheme(QString); void updateTilesetEditor(); Event::Group getEventGroupFromTabWidget(QWidget *tab); - void closeSupplementaryWindows(); + bool closeSupplementaryWindows(); void setWindowDisabled(bool); void initTilesetEditor(); bool initRegionMapEditor(bool silent = false); + bool askToFixRegionMapEditor(); void initShortcutsEditor(); void initCustomScriptsEditor(); void connectSubEditorsToShortcutsEditor(); @@ -447,6 +455,32 @@ private: int insertTilesetLabel(QStringList * list, QString label); void checkForUpdates(bool requestedByUser); + void setDivingMapsVisible(bool visible); +}; + +// 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, + }; +}; + +struct MapListTab { + enum { + Groups = 0, Areas, Layouts + }; }; #endif // MAINWINDOW_H diff --git a/include/project.h b/include/project.h index 7d452cc3..e5deb75e 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; @@ -80,6 +79,9 @@ public: bool usingAsmTilesets; QString importExportPath; QSet disabledSettingsNames; + int pokemonMinLevel; + int pokemonMaxLevel; + int maxEncounterRate; bool wildEncountersLoaded; // For files that are read and could contain extra text @@ -102,6 +104,9 @@ public: DataQualifiers healLocationDataQualifiers; QString healLocationsTableName; + bool sanityCheck(); + bool load(); + QMap mapCache; Map* loadMap(QString); Map* getMap(QString); @@ -164,6 +169,7 @@ public: void saveAllMaps(); void saveMap(Map *); void saveAllDataStructures(); + void saveConfig(); void saveMapLayouts(); void saveMapGroups(); void saveMapSections(); @@ -264,6 +270,7 @@ private: signals: void reloadProject(); void uncheckMonitorFilesAction(); + void mapLoaded(Map *map); }; #endif // PROJECT_H diff --git a/include/scripting.h b/include/scripting.h index fb96e7ae..c08bbd74 100644 --- a/include/scripting.h +++ b/include/scripting.h @@ -30,7 +30,9 @@ class Scripting { public: Scripting(MainWindow *mainWindow); + ~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/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/include/ui/connectionpixmapitem.h b/include/ui/connectionpixmapitem.h index c3d98cff..62eda6fe 100644 --- a/include/ui/connectionpixmapitem.h +++ b/include/ui/connectionpixmapitem.h @@ -4,44 +4,46 @@ #include "mapconnection.h" #include #include +#include class ConnectionPixmapItem : public QObject, public QGraphicsPixmapItem { Q_OBJECT public: - ConnectionPixmapItem(QPixmap pixmap, MapConnection* connection, int x, int y, int baseMapWidth, int baseMapHeight): QGraphicsPixmapItem(pixmap) { - this->basePixmap = pixmap; - this->connection = connection; - setFlag(ItemIsMovable); - setFlag(ItemSendsGeometryChanges); - this->initialX = x; - this->initialY = y; - this->initialOffset = connection->offset; - this->baseMapWidth = baseMapWidth; - this->baseMapHeight = baseMapHeight; - } - QPixmap basePixmap; - MapConnection* connection; - int initialX; - int initialY; - int initialOffset; - int baseMapWidth; - int baseMapHeight; - void render(qreal opacity = 1); - int getMinOffset(); - int getMaxOffset(); + 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 updateHighlight(bool selected); + + void setSelected(bool selected); + + 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 connectionItemSelected(ConnectionPixmapItem* connectionItem); - void connectionItemDoubleClicked(ConnectionPixmapItem* connectionItem); - void connectionMoved(MapConnection*); + void connectionItemDoubleClicked(MapConnection*); + void selectionChanged(bool selected); }; #endif // CONNECTIONPIXMAPITEM_H diff --git a/include/ui/connectionslistitem.h b/include/ui/connectionslistitem.h new file mode 100644 index 00000000..bbe0f2d3 --- /dev/null +++ b/include/ui/connectionslistitem.h @@ -0,0 +1,52 @@ +#ifndef CONNECTIONSLISTITEM_H +#define CONNECTIONSLISTITEM_H + +#include "mapconnection.h" +#include "map.h" + +#include +#include +#include + +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. +class ConnectionsListItem : public QFrame +{ + Q_OBJECT + +public: + explicit ConnectionsListItem(QWidget *parent, MapConnection *connection, const QStringList &mapNames); + ~ConnectionsListItem(); + + void updateUI(); + void setSelected(bool selected); + +private: + Ui::ConnectionsListItem *ui; + QPointer connection; + Map *map; + bool isSelected = false; + unsigned actionId = 0; + +protected: + void mousePressEvent(QMouseEvent*) override; + +signals: + void selected(); + void removed(MapConnection*); + void openMapClicked(MapConnection*); + +private slots: + 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(); +}; + +#endif // CONNECTIONSLISTITEM_H 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/include/ui/divingmappixmapitem.h b/include/ui/divingmappixmapitem.h new file mode 100644 index 00000000..7eceba07 --- /dev/null +++ b/include/ui/divingmappixmapitem.h @@ -0,0 +1,30 @@ +#ifndef DIVINGMAPPIXMAPITEM_H +#define DIVINGMAPPIXMAPITEM_H + +#include "mapconnection.h" + +#include +#include +#include + +class DivingMapPixmapItem : public QObject, public QGraphicsPixmapItem { + Q_OBJECT +public: + DivingMapPixmapItem(MapConnection *connection, QComboBox *combo); + ~DivingMapPixmapItem(); + + MapConnection* connection() const { return m_connection; } + void updatePixmap(); + +private: + QPointer m_connection; + QPointer m_combo; + + void setComboText(const QString &text); + static QPixmap getBasePixmap(MapConnection* connection); + +private slots: + void onTargetMapChanged(); +}; + +#endif // DIVINGMAPPIXMAPITEM_H 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/include/ui/encountertablemodel.h b/include/ui/encountertablemodel.h index 6aacbf30..b2a39ad8 100644 --- a/include/ui/encountertablemodel.h +++ b/include/ui/encountertablemodel.h @@ -28,7 +28,9 @@ public: Slot, Group, Species, MinLevel, MaxLevel, EncounterChance, SlotRatio, EncounterRate, Count }; - WildMonInfo encounterData(); + 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/mapimageexporter.h b/include/ui/mapimageexporter.h index 39cc24aa..b61348bf 100644 --- a/include/ui/mapimageexporter.h +++ b/include/ui/mapimageexporter.h @@ -51,9 +51,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/include/ui/maplistmodels.h b/include/ui/maplistmodels.h index 7d56da17..d99c8815 100644 --- a/include/ui/maplistmodels.h +++ b/include/ui/maplistmodels.h @@ -12,7 +12,7 @@ class Project; -enum MapListRoles { +enum MapListUserRoles { GroupRole = Qt::UserRole + 1, // Used to hold the map group number. TypeRole, // Used to differentiate between the different layers of the map list tree view. TypeRole2, // Used for various extra data needed. diff --git a/include/ui/newmapconnectiondialog.h b/include/ui/newmapconnectiondialog.h new file mode 100644 index 00000000..4781c971 --- /dev/null +++ b/include/ui/newmapconnectiondialog.h @@ -0,0 +1,32 @@ +#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(); + + virtual void accept() override; + +signals: + void accepted(MapConnection *result); + +private: + Ui::NewMapConnectionDialog *ui; + + bool mapNameIsValid(); + void setWarningVisible(bool visible); +}; + +#endif // NEWMAPCONNECTIONDIALOG_H diff --git a/include/ui/noscrollcombobox.h b/include/ui/noscrollcombobox.h index 59c680a3..32966b3a 100644 --- a/include/ui/noscrollcombobox.h +++ b/include/ui/noscrollcombobox.h @@ -13,9 +13,15 @@ public: void setTextItem(const QString &text); void setNumberItem(int value); void setHexItem(uint32_t value); + 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/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/include/ui/regionmapeditor.h b/include/ui/regionmapeditor.h index 3cbc2515..3d889f88 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); @@ -41,6 +42,8 @@ public: void resizeTilemap(int width, int height); + bool reconfigure(); + QObjectList shortcutableObjects() const; public slots: @@ -53,9 +56,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/include/ui/wildmonchart.h b/include/ui/wildmonchart.h new file mode 100644 index 00000000..d4e3d83f --- /dev/null +++ b/include/ui/wildmonchart.h @@ -0,0 +1,98 @@ +#ifndef WILDMONCHART_H +#define WILDMONCHART_H + +#include "encountertablemodel.h" + +#include + +#if __has_include() +#include + +namespace Ui { +class WildMonChart; +} + +class WildMonChart : public QWidget +{ + Q_OBJECT +public: + explicit WildMonChart(QWidget *parent, const EncounterTableModel *table); + ~WildMonChart(); + + virtual void closeEvent(QCloseEvent *event) override; + +public slots: + void setTable(const EncounterTableModel *table); + void clearTable(); + void refresh(); + +private: + Ui::WildMonChart *ui; + const EncounterTableModel *table; + + QStringList groupNames; + QStringList groupNamesReversed; + QStringList speciesInLegendOrder; + QMap tableIndexToGroupName; + + struct LevelRange { + int min; + int max; + }; + QMap groupedLevelRanges; + + struct Summary { + double speciesFrequency = 0.0; + QMap levelFrequencies; + }; + typedef QMap GroupedData; + + QMap speciesToGroupedData; + QMap speciesToColor; + + + QStringList getSpeciesNamesAlphabetical() const; + double getSpeciesFrequency(const QString&, const QString&) const; + QMap getLevelFrequencies(const QString &, const QString &) const; + LevelRange getLevelRange(const QString &, const QString &) const; + bool usesGroupLabels() const; + + void clearTableData(); + void readTable(); + 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 limitChartAnimation(QChart*); + + 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 da0a8bd6..14e83635 100644 --- a/porymap.pro +++ b/porymap.pro @@ -6,6 +6,10 @@ QT += core gui qml network +!win32 { + QT += charts +} + greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = porymap @@ -24,6 +28,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 \ @@ -46,13 +51,16 @@ 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/divingmappixmapitem.cpp \ src/ui/draggablepixmapitem.cpp \ src/ui/bordermetatilespixmapitem.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 \ @@ -107,7 +115,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 \ @@ -140,13 +149,16 @@ 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/divingmappixmapitem.h \ include/ui/draggablepixmapitem.h \ include/ui/bordermetatilespixmapitem.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 \ @@ -204,9 +216,12 @@ 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/connectionslistitem.ui \ + forms/newmapconnectiondialog.ui \ forms/prefabcreationdialog.ui \ forms/prefabframe.ui \ forms/tileseteditor.ui \ @@ -223,7 +238,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/resources/icons/help.ico b/resources/icons/help.ico new file mode 100755 index 00000000..68f51bac Binary files /dev/null and b/resources/icons/help.ico differ diff --git a/resources/icons/map_go.ico b/resources/icons/map_go.ico new file mode 100755 index 00000000..8ca93206 Binary files /dev/null and b/resources/icons/map_go.ico differ diff --git a/resources/images.qrc b/resources/images.qrc index 792c5705..65bff5d6 100644 --- a/resources/images.qrc +++ b/resources/images.qrc @@ -20,6 +20,7 @@ icons/folder.ico icons/lock_edit.ico icons/unlock_edit.ico + icons/help.ico icons/map_edited.ico icons/map_opened.ico icons/map.ico @@ -73,5 +74,6 @@ images/Entities_16x16.png images/pokemon_icon_placeholder.png icons/clipboard.ico + icons/map_go.ico diff --git a/src/config.cpp b/src/config.cpp index cfdd6fbd..a1029b92 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -85,11 +85,13 @@ 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"}}, {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"}}, @@ -109,6 +111,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 +127,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 = { @@ -174,6 +176,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/"}}, }; @@ -199,15 +202,8 @@ 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)) { + this->init(); + } else if (!file.open(QIODevice::ReadOnly)) { logError(QString("Could not open config file '%1': ").arg(this->getConfigFilepath()) + file.errorString()); } @@ -234,15 +230,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 +279,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::SortByGroup, "group"}, {MapSortOrder::SortByLayout, "layout"}, @@ -322,6 +309,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") { @@ -344,6 +333,16 @@ 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") { + 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") { @@ -368,6 +367,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") { @@ -394,6 +395,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") { @@ -433,6 +436,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)); @@ -452,6 +456,12 @@ 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("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)); + 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)); @@ -466,6 +476,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)); @@ -504,183 +515,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 +529,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 +623,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", @@ -865,20 +635,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; @@ -890,33 +672,34 @@ 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") { 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 +739,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 +811,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 +826,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 +835,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 +865,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,11 +889,13 @@ QMap ProjectConfig::getKeyValueMap() { return map; } -void ProjectConfig::onNewConfigFileCreated() { +void ProjectConfig::init() { QString dirName = QDir(this->projectDir).dirName().toLower(); - if (baseGameVersionReverseMap.contains(dirName)) { - 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"); @@ -1124,25 +909,21 @@ void ProjectConfig::onNewConfigFileCreated() { baseGameVersionComboBox->addItem("pokeemerald", BaseGameVersion::pokeemerald); form.addRow(new QLabel("Game Version"), baseGameVersionComboBox); + // 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); form.addRow(&buttonBox); if (dialog.exec() == QDialog::Accepted) { this->baseGameVersion = static_cast(baseGameVersionComboBox->currentData().toInt()); + } else { + logWarn(QString("No base_game_version selected, using default '%1'").arg(getBaseGameVersionString(this->baseGameVersion))); } } 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 +989,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 +1000,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 +1018,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 +1028,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() { @@ -1599,47 +1062,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::setRecentLayout(const QString &layout) { - this->recentLayout = layout; - this->save(); -} - -QString UserConfig::getRecentLayout() { - return this->recentLayout; -} - -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 @@ -1676,7 +1104,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() { @@ -1720,7 +1147,6 @@ QMap ShortcutsConfig::getKeyValueMap() { void ShortcutsConfig::setDefaultShortcuts(const QObjectList &objects) { storeShortcutsFromList(StoreType::Default, objects); - save(); } QList ShortcutsConfig::defaultShortcuts(const QObject *object) const { @@ -1729,14 +1155,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/editcommands.cpp b/src/core/editcommands.cpp index 22227b42..224aad21 100644 --- a/src/core/editcommands.cpp +++ b/src/core/editcommands.cpp @@ -330,7 +330,7 @@ void EventCreate::redo() { // select this event editor->selected_events->clear(); - editor->selectMapEvent(event->getPixmapItem(), false); + editor->selectMapEvent(event->getPixmapItem()); } void EventCreate::undo() { @@ -568,3 +568,205 @@ void ScriptEditLayout::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/events.cpp b/src/core/events.cpp index 40067e22..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(); @@ -35,7 +50,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 +210,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 +274,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 +803,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 +821,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 +836,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 +854,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 +945,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 1d52a0ba..01ab95b0 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) { @@ -69,40 +67,47 @@ int Map::getBorderHeight() { return layout->getBorderHeight(); } -QPixmap Map::renderConnection(MapConnection connection, Layout * fromLayout) { - int x, y, w, h; - 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(); - } +// 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, Layout * fromLayout) { + int x = 0, y = 0; + int w = getWidth(), h = getHeight(); - //render(true, fromLayout, QRect(x, y, w, h)); - //QImage connection_image = image.copy(x * 16, y * 16, w * 16, h * 16); - return this->layout->render(true, fromLayout, QRect(x, y, w, h)).copy(x * 16, y * 16, w * 16, h * 16); - //return QPixmap::fromImage(connection_image); + if (direction == "up") { + h = qMin(h, BORDER_DISTANCE); + y = getHeight() - h; + } else if (direction == "down") { + h = qMin(h, BORDER_DISTANCE); + } else if (direction == "left") { + w = qMin(w, BORDER_DISTANCE); + x = getWidth() - w; + } else if (direction == "right") { + w = qMin(w, BORDER_DISTANCE); + } else if (MapConnection::isDiving(direction)) { + if (fromLayout) { + w = qMin(w, fromLayout->getWidth()); + h = qMin(h, fromLayout->getHeight()); + } + } else { + // Unknown direction + return QRect(); + } + return QRect(x, y, w, h); +} + +QPixmap Map::renderConnection(const QString &direction, Layout * 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; + + QPixmap connectionPixmap = this->layout->render(true, fromLayout, bounds); + return connectionPixmap.copy(bounds.x() * 16, bounds.y() * 16, bounds.width() * 16, bounds.height() * 16); } void Map::openScript(QString label) { @@ -153,9 +158,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); @@ -177,6 +182,72 @@ void Map::addEvent(Event *event) { if (!ownedEvents.contains(event)) ownedEvents.append(event); } +void Map::deleteConnections() { + qDeleteAll(this->ownedConnections); + this->ownedConnections.clear(); + this->connections.clear(); +} + +QList Map::getConnections() const { + return this->connections; +} + +void Map::addConnection(MapConnection *connection) { + if (!connection || this->connections.contains(connection)) + 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) + // Any additional connections being added (this can happen via mirroring) are tracked for deleting but otherwise ignored. + if (MapConnection::isDiving(connection->direction())) { + for (auto i : this->connections) { + if (i->direction() == connection->direction()) { + trackConnection(connection); + return; + } + } + } + + loadConnection(connection); + modify(); + emit connectionAdded(connection); + return; +} + +void Map::loadConnection(MapConnection *connection) { + if (!connection) + return; + + if (!this->connections.contains(connection)) + this->connections.append(connection); + + trackConnection(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; + connection->setParentMap(nullptr, false); + modify(); + emit connectionRemoved(connection); +} + void Map::modify() { emit modified(); } @@ -188,3 +259,24 @@ void Map::clean() { bool Map::hasUnsavedChanges() { return !editHistory.isClean() || !this->layout->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); + } +} diff --git a/src/core/mapconnection.cpp b/src/core/mapconnection.cpp new file mode 100644 index 00000000..b80b8d09 --- /dev/null +++ b/src/core/mapconnection.cpp @@ -0,0 +1,163 @@ +#include "mapconnection.h" +#include "project.h" + +QPointer MapConnection::project = nullptr; + +const QMap MapConnection::oppositeDirections = { + {"up", "down"}, {"down", "up"}, + {"right", "left"}, {"left", "right"}, + {"dive", "emerge"}, {"emerge", "dive"} +}; + +MapConnection::MapConnection(const QString &targetMapName, const QString &direction, int offset) { + m_parentMap = nullptr; + m_targetMapName = targetMapName; + m_direction = direction; + m_offset = offset; +} + +bool MapConnection::areMirrored(const MapConnection* a, const MapConnection* b) { + if (!a || !b || !a->m_parentMap || !b->m_parentMap) + return false; + + 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() { + 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 : map->getConnections()) { + if (this != connection && areMirrored(this, connection)) + return connection; + } + return nullptr; +} + +MapConnection * MapConnection::createMirror() { + auto mirror = new MapConnection(parentMapName(), oppositeDirection(m_direction), -m_offset); + mirror->setParentMap(targetMap(), false); + return mirror; +} + +void MapConnection::markMapEdited() { + 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() { + auto map = targetMap(); + if (!map) + return QPixmap(); + + return map->renderConnection(m_direction, m_parentMap ? m_parentMap->layout : nullptr); +} + +void MapConnection::setParentMap(Map* map, bool mirror) { + if (map == m_parentMap) + return; + + if (mirror) { + auto connection = findMirror(); + if (connection) + connection->setTargetMapName(map ? map->name : QString(), false); + } + + 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); +} + +QString MapConnection::parentMapName() const { + return m_parentMap ? m_parentMap->name : QString(); +} + +void MapConnection::setTargetMapName(const QString &targetMapName, bool mirror) { + if (targetMapName == m_targetMapName) + return; + + if (mirror) { + 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::setDirection(const QString &direction, bool mirror) { + if (direction == m_direction) + return; + + if (mirror) { + 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, bool mirror) { + if (offset == m_offset) + return; + + if (mirror) { + auto connection = findMirror(); + if (connection) + connection->setOffset(-offset, false); + } + + auto before = m_offset; + m_offset = offset; + emit offsetChanged(before, m_offset); + markMapEdited(); +} + +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"; +} + +bool MapConnection::isVertical(const QString &direction) { + return direction == "up" || direction == "down"; +} + +bool MapConnection::isDiving(const QString &direction) { + return direction == "dive" || direction == "emerge"; +} 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/regionmap.cpp b/src/core/regionmap.cpp index 68da1fa7..7dbd4c5f 100644 --- a/src/core/regionmap.cpp +++ b/src/core/regionmap.cpp @@ -79,14 +79,25 @@ bool RegionMap::loadTilemap(poryjson::Json tilemapJson) { this->palette_path = tilemapObject["palette"].string_value(); } + 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)); + 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; } @@ -297,7 +308,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/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 1ae94b40..52c10522 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -2,12 +2,11 @@ #include "draggablepixmapitem.h" #include "imageproviders.h" #include "log.h" -#include "mapconnection.h" +#include "connectionslistitem.h" #include "currentselectedmetatilespixmapitem.h" #include "eventfilters.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() @@ -82,11 +86,20 @@ void Editor::saveUiFields() { saveEncounterTabData(); } +void Editor::setProject(Project * project) { + closeProject(); + this->project = project; + MapConnection::project = project; +} + void Editor::closeProject() { - if (this->project) { - delete this->project; - this->project = nullptr; - } + if (!this->project) + return; + this->project->saveConfig(); + Scripting::cb_ProjectClosed(this->project->root); + Scripting::stop(); + clearMap(); + delete this->project; } bool Editor::getEditingLayout() { @@ -117,13 +130,10 @@ void Editor::setEditorView() { map_item->draw(); collision_item->draw(); - displayMapConnections(); current_view->setVisible(true); - setBorderItemsVisible(ui->checkBox_ToggleBorder->isChecked()); - setConnectionItemsVisible(ui->checkBox_ToggleBorder->isChecked()); - setConnectionsEditable(false); + updateBorderVisibility(); this->cursorMapTileRect->setSingleTileMode(); this->cursorMapTileRect->setActive(true); @@ -133,29 +143,14 @@ void Editor::setEditorView() { map_item->setEditsEnabled(true); this->editGroup.setActiveStack(&this->layout->editHistory); break; + case EditMode::Connections: + this->cursorMapTileRect->setActive(false); + map_item->setEditsEnabled(false); case EditMode::Events: if (this->map) { this->editGroup.setActiveStack(&this->map->editHistory); } break; - case EditMode::Connections: - populateConnectionMapPickers(); - ui->label_NumConnections->setText(QString::number(map->connections.length())); - setDiveEmergeControls(); - - setConnectionEditControlsEnabled(selected_connection_item != nullptr); - if (selected_connection_item) { - onConnectionOffsetChanged(selected_connection_item->connection->offset); - setConnectionMap(selected_connection_item->connection->map_name); - setCurrentConnectionDirection(selected_connection_item->connection->direction); - } - maskNonVisibleConnectionTiles(); - - setBorderItemsVisible(true, 0.4); - setConnectionItemsVisible(true); - setConnectionsEditable(true); - this->cursorMapTileRect->setActive(false); - map_item->setEditsEnabled(false); case EditMode::Header: case EditMode::Encounters: default: @@ -191,6 +186,7 @@ void Editor::setEditingObjects() { this->editMode = EditMode::Events; setEditorView(); + updateWarpEventWarnings(); } void Editor::setEditingConnections() { @@ -221,9 +217,9 @@ void Editor::setMapEditingButtonsEnabled(bool enabled) { this->ui->checkBox_smartPaths->setEnabled(enabled); } -void Editor::displayWildMonTables() { +void Editor::clearWildMonTables() { QStackedWidget *stack = ui->stackedWidget_WildMons; - QComboBox *labelCombo = ui->comboBox_EncounterGroupLabel; + const QSignalBlocker blocker(stack); // delete widgets from previous map data if they exist while (stack->count()) { @@ -232,18 +228,25 @@ void Editor::displayWildMonTables() { delete oldWidget; } - labelCombo->clear(); + ui->comboBox_EncounterGroupLabel->clear(); + emit wildMonTableClosed(); +} + +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]) { @@ -267,8 +270,12 @@ void Editor::displayWildMonTables() { } tabIndex++; } + connect(tabWidget, &MonTabWidget::currentChanged, [this] { + emit wildMonTableOpened(getCurrentWildMonTable()); + }); } stack->setCurrentIndex(0); + emit wildMonTableOpened(getCurrentWildMonTable()); } void Editor::addNewWildMonGroup(QWidget *window) { @@ -383,7 +390,7 @@ void Editor::addNewWildMonGroup(QWidget *window) { tabIndex++; } saveEncounterTabData(); - emit wildMonDataChanged(); + emit wildMonTableEdited(); } } @@ -421,7 +428,8 @@ void Editor::deleteWildMonGroup() { project->encounterGroupLabels.remove(i); displayWildMonTables(); - emit wildMonDataChanged(); + saveEncounterTabData(); + emit wildMonTableEdited(); } } @@ -674,7 +682,8 @@ void Editor::configureEncounterJSON(QWidget *window) { // Re-draw the tab accordingly. displayWildMonTables(); - emit wildMonDataChanged(); + saveEncounterTabData(); + emit wildMonTableEdited(); } } @@ -708,6 +717,16 @@ void Editor::saveEncounterTabData() { } } +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) { EncounterFields oldFields = project->wildMonFields; // Go through fields and determine whether we need to update a field. @@ -757,193 +776,297 @@ void Editor::updateEncounterFields(EncounterFields newFields) { project->wildMonFields = newFields; } -void Editor::setDiveEmergeControls() { - ui->comboBox_DiveMap->blockSignals(true); - ui->comboBox_EmergeMap->blockSignals(true); - ui->comboBox_DiveMap->setCurrentText(""); - ui->comboBox_EmergeMap->setCurrentText(""); - 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); +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; + + if (MapConnection::isDiving(connection->direction())) { + displayDivingConnection(connection); + return; + } + + // Create connection image + ConnectionPixmapItem *pixmapItem = new ConnectionPixmapItem(connection, getConnectionOrigin(connection)); + pixmapItem->render(); + scene->addItem(pixmapItem); + maskNonVisibleConnectionTiles(); + + // Create item for the list panel + 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 + + // Double clicking the pixmap or clicking the list item's map button opens the connected map + 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) { + listItem->setSelected(selected); + if (selected) setSelectedConnectionItem(pixmapItem); + }); + connect(listItem, &ConnectionsListItem::selected, [=] { + setSelectedConnectionItem(pixmapItem); + }); + + // Sync edits to 'offset' between the list UI and the pixmap + connect(connection, &MapConnection::offsetChanged, [=](int, int) { + listItem->updateUI(); + pixmapItem->updatePos(); + maskNonVisibleConnectionTiles(); + }); + + // Sync edits to 'direction' between the list UI and the pixmap + 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, [=](QString, QString) { + listItem->updateUI(); + updateConnectionPixmap(pixmapItem); + }); + + // 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) { + if (!connection) + return; + + // 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; + + this->map->editHistory.push(new MapConnectionAdd(this->map, connection)); +} + +void Editor::removeConnection(MapConnection *connection) { + if (!connection) + return; + this->map->editHistory.push(new MapConnectionRemove(this->map, connection)); +} + +void Editor::removeSelectedConnection() { + if (selected_connection_item) + removeConnection(selected_connection_item->connection); +} + +void Editor::removeConnectionPixmap(MapConnection *connection) { + if (!connection) + return; + + disconnectMapConnection(connection); + + 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()) + pixmapItem->scene()->removeItem(pixmapItem); + + delete pixmapItem; +} + +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 (diving_map_items.value(direction)) + return; + + // 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); + + updateDivingMapsVisibility(); +} + +void Editor::renderDivingConnections() { + for (auto item : diving_map_items.values()) + item->updatePixmap(); +} + +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 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; + + // Delete map image + auto pixmapItem = diving_map_items.take(direction); + if (pixmapItem->scene()) + pixmapItem->scene()->removeItem(pixmapItem); + delete pixmapItem; + + // 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; } } - ui->comboBox_DiveMap->blockSignals(false); - ui->comboBox_EmergeMap->blockSignals(false); + updateDivingMapsVisibility(); } -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); +void Editor::updateDiveMap(QString mapName) { + setDivingMapName(mapName, "dive"); } -void Editor::setConnectionItemsVisible(bool visible) { - for (ConnectionPixmapItem* item : connection_items) { - item->setVisible(visible); - item->setEnabled(visible); +void Editor::updateEmergeMap(QString mapName) { + setDivingMapName(mapName, "emerge"); +} + +void Editor::setDivingMapName(QString mapName, QString direction) { + auto pixmapItem = diving_map_items.value(direction); + MapConnection *connection = pixmapItem ? pixmapItem->connection() : nullptr; + + if (connection) { + if (mapName == connection->targetMapName()) + return; // No change + + // Update existing connection + if (mapName.isEmpty()) { + removeConnection(connection); + } else { + map->editHistory.push(new MapConnectionChangeMap(connection, mapName)); + } + } else if (!mapName.isEmpty()) { + // Create new connection + addConnection(new MapConnection(mapName, direction)); } } -void Editor::setBorderItemsVisible(bool visible, qreal opacity) { - for (QGraphicsPixmapItem* item : borderItems) { - item->setVisible(visible); - item->setOpacity(opacity); +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->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) dive->setOpacity(opacity); + else if (emerge) emerge->setOpacity(opacity); } } -void Editor::setCurrentConnectionDirection(QString curDirection) { - if (!selected_connection_item) - return; - Map *connected_map = project->getMap(selected_connection_item->connection->map_name); - if (!connected_map) { - return; - } +// 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); - selected_connection_item->connection->direction = curDirection; - - QPixmap pixmap = connected_map->renderConnection(*selected_connection_item->connection, map->layout); - int offset = selected_connection_item->connection->offset; - selected_connection_item->initialOffset = offset; + Map *parentMap = connection->parentMap(); + Map *targetMap = connection->targetMap(); + const QString direction = connection->direction(); int x = 0, y = 0; - if (selected_connection_item->connection->direction == "up") { - x = offset * 16; - y = -pixmap.height(); - } else if (selected_connection_item->connection->direction == "down") { - x = offset * 16; - y = map->getHeight() * 16; - } else if (selected_connection_item->connection->direction == "left") { - x = -pixmap.width(); - y = offset * 16; - } else if (selected_connection_item->connection->direction == "right") { - x = map->getWidth() * 16; - y = offset * 16; + + if (direction == "right") { + if (parentMap) x = parentMap->getWidth(); + } else if (direction == "down") { + if (parentMap) y = parentMap->getHeight(); + } else if (direction == "left") { + if (targetMap) x = -targetMap->getConnectionRect(direction).width(); + } else if (direction == "up") { + if (targetMap) y = -targetMap->getConnectionRect(direction).height(); } - - 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); - - setConnectionEditControlValues(selected_connection_item->connection); + return QPoint(x * 16, y * 16); } -void Editor::updateCurrentConnectionDirection(QString curDirection) { - if (!selected_connection_item) +void Editor::updateConnectionPixmap(ConnectionPixmapItem *pixmapItem) { + if (!pixmapItem) return; - QString originalDirection = selected_connection_item->connection->direction; - setCurrentConnectionDirection(curDirection); - updateMirroredConnectionDirection(selected_connection_item->connection, originalDirection); + pixmapItem->setOrigin(getConnectionOrigin(pixmapItem->connection)); + pixmapItem->render(true); // Full render to reflect map changes + maskNonVisibleConnectionTiles(); } -void Editor::onConnectionMoved(MapConnection* connection) { - updateMirroredConnectionOffset(connection); - onConnectionOffsetChanged(connection->offset); - maskNonVisibleConnectionTiles(); -} - -void Editor::onConnectionOffsetChanged(int newOffset) { - 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); - } -} - -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) +void Editor::setSelectedConnectionItem(ConnectionPixmapItem *pixmapItem) { + if (!pixmapItem || pixmapItem == selected_connection_item) return; - selected_connection_item = connectionItem; - for (ConnectionPixmapItem* item : connection_items) - item->updateHighlight(item == selected_connection_item); - setConnectionEditControlsEnabled(true); - setConnectionEditControlValues(selected_connection_item->connection); - ui->spinBox_ConnectionOffset->setMaximum(selected_connection_item->getMaxOffset()); - ui->spinBox_ConnectionOffset->setMinimum(selected_connection_item->getMinOffset()); - onConnectionOffsetChanged(selected_connection_item->connection->offset); + if (selected_connection_item) selected_connection_item->setSelected(false); + selected_connection_item = pixmapItem; + selected_connection_item->setSelected(true); } -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); +void Editor::setSelectedConnection(MapConnection *connection) { + if (!connection) + return; + + for (auto item : connection_items) { + if (item->connection == connection) { + setSelectedConnectionItem(item); break; } } } -void Editor::onConnectionItemDoubleClicked(ConnectionPixmapItem* connectionItem) { - emit loadMapRequested(connectionItem->connection->map_name, map->name); -} - -void Editor::onConnectionDirectionChanged(QString newDirection) { - 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(); } void Editor::onHoveredMovementPermissionChanged(uint16_t collision, uint16_t elevation) { @@ -1118,8 +1241,11 @@ QString Editor::getMovementPermissionText(uint16_t collision, uint16_t elevation void Editor::unsetMap() { // disconnect previous map's signals so they are not firing // multiple times if set again in the future - if (this->map) { - this->map->disconnect(this); + if (map) { + map->pruneEditHistory(); + map->disconnect(this); + for (auto connection : map->getConnections()) + disconnectMapConnection(connection); } this->map = nullptr; @@ -1151,6 +1277,8 @@ bool Editor::setMap(QString map_name) { } connect(map, &Map::openScriptRequested, this, &Editor::openScript); + connect(map, &Map::connectionAdded, this, &Editor::displayConnection); + connect(map, &Map::connectionRemoved, this, &Editor::removeConnectionPixmap); updateSelectedEvents(); } @@ -1310,7 +1438,7 @@ void Editor::mouseEvent_map(QGraphicsSceneMouseEvent *event, LayoutPixmapItem *i if (newEvent) { newEvent->move(pos.x(), pos.y()); emit objectsChanged(); - selectMapEvent(newEvent, false); + selectMapEvent(newEvent); } } } @@ -1391,11 +1519,38 @@ 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(); + clearConnectionMask(); + + // Clear pointers to objects deleted elsewhere + 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() { displayMapEvents(); displayMapConnections(); displayWildMonTables(); + maskNonVisibleConnectionTiles(); if (events_group) { events_group->setVisible(false); @@ -1412,12 +1567,6 @@ bool Editor::displayLayout() { scene->installEventFilter(this->map_ruler); } - if (map_item && scene) { - scene->removeItem(map_item); - delete map_item; - scene->removeItem(this->map_ruler); - } - displayMetatileSelector(); displayMapMetatiles(); displayMovementPermissionSelector(); @@ -1440,11 +1589,16 @@ bool Editor::displayLayout() { 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, this->layout); @@ -1469,7 +1623,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 LayoutPixmapItem(this->layout, this->metatile_selector_item, this->settings); connect(map_item, &LayoutPixmapItem::mouseEvent, this, &Editor::mouseEvent_map); connect(map_item, &LayoutPixmapItem::startPaint, this, &Editor::onMapStartPaint); @@ -1490,11 +1654,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(this->layout, ui->spinBox_SelectedCollision, ui->spinBox_SelectedElevation, this->metatile_selector_item, this->settings, &this->collisionOpacity); connect(collision_item, &CollisionPixmapItem::mouseEvent, this, &Editor::mouseEvent_collision); @@ -1507,11 +1676,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(this->layout, this->metatile_selector_item); @@ -1526,11 +1700,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(this->layout, this->metatile_selector_item); @@ -1546,11 +1726,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) { @@ -1562,13 +1746,13 @@ 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); } -void Editor::displayMapEvents() { +void Editor::clearMapEvents() { if (events_group) { for (QGraphicsItem *child : events_group->childItems()) { events_group->removeFromGroup(child); @@ -1580,9 +1764,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); @@ -1603,68 +1791,45 @@ DraggablePixmapItem *Editor::addMapEvent(Event *event) { return object; } -void Editor::displayMapConnections() { - for (ConnectionPixmapItem* item : connection_items) { - if (item->scene()) { +void Editor::clearMapConnections() { + for (auto item : connection_items) { + if (item->scene()) item->scene()->removeItem(item); - } delete item; } - selected_connection_item = nullptr; connection_items.clear(); + const QSignalBlocker blocker1(ui->comboBox_DiveMap); + const QSignalBlocker blocker2(ui->comboBox_EmergeMap); + ui->comboBox_DiveMap->setCurrentText(""); + ui->comboBox_EmergeMap->setCurrentText(""); + + for (auto item : diving_map_items.values()) { + if (item->scene()) + item->scene()->removeItem(item); + delete item; + } + diving_map_items.clear(); + + // Reset to single opacity slider + ui->stackedWidget_DiveMapOpacity->setCurrentIndex(1); + + selected_connection_item = nullptr; +} + +void Editor::displayMapConnections() { + clearMapConnections(); + if (map) { - for (MapConnection *connection : map->connections) { - if (connection->direction == "dive" || connection->direction == "emerge") { - continue; - } - createConnectionItem(connection); - } + for (auto connection : map->getConnections()) + displayConnection(connection); } - if (!connection_items.empty()) { - onConnectionItemSelected(connection_items.first()); - } - - maskNonVisibleConnectionTiles(); + if (!connection_items.isEmpty()) + setSelectedConnectionItem(connection_items.first()); } -void Editor::createConnectionItem(MapConnection* connection) { - Map *connected_map = project->getMap(connection->map_name); - if (!connected_map) { - return; - } - - 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; - } - - ConnectionPixmapItem *item = new ConnectionPixmapItem(pixmap, connection, x, y, map->getWidth(), map->getHeight()); - item->setX(x); - item->setY(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); -} - -// 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); @@ -1672,6 +1837,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()); @@ -1689,7 +1859,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); @@ -1697,6 +1867,10 @@ void Editor::displayMapBorder() { delete item; } borderItems.clear(); +} + +void Editor::displayMapBorder() { + clearMapBorder(); int borderWidth = this->layout->getBorderWidth(); int borderHeight = this->layout->getBorderHeight(); @@ -1722,17 +1896,8 @@ 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) - continue; - - QPixmap pixmap = connected_map->renderConnection(*(connection_items[i]->connection), map->layout); - connection_items[i]->basePixmap = pixmap; - connection_items[i]->setPixmap(pixmap); - } - - maskNonVisibleConnectionTiles(); + for (auto item : connection_items) + item->render(true); } int Editor::getBorderDrawDistance(int dimension) { @@ -1747,16 +1912,20 @@ 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(); } -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 = this->layout->getWidth() * 16; @@ -1778,214 +1947,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; - 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); - 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}}); - 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); - onConnectionItemSelected(connection_items.last()); - ui->label_NumConnections->setText(QString::number(map->connections.length())); - - 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::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; - setConnectionEditControlsEnabled(false); - ui->spinBox_ConnectionOffset->setValue(0); - ui->label_NumConnections->setText(QString::number(map->connections.length())); - - if (connection_items.length() > 0) { - onConnectionItemSelected(connection_items.last()); - } -} - -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); - } - } - - ui->label_NumConnections->setText(QString::number(map->connections.length())); -} - void Editor::updatePrimaryTileset(QString tilesetLabel, bool forceLoad) { if (this->layout->tileset_primary_label != tilesetLabel || forceLoad) @@ -2008,17 +1969,42 @@ void Editor::updateSecondaryTileset(QString tilesetLabel, bool forceLoad) void Editor::toggleBorderVisibility(bool visible, bool enableScriptCallback) { - this->setBorderItemsVisible(visible); - this->setConnectionItemsVisible(visible); - porymapConfig.setShowBorder(visible); + porymapConfig.showBorder = 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() == MainTab::Connections); + 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); + + // 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); + } +} + void Editor::updateCustomMapHeaderValues(QTableWidget *table) { map->customHeaders = CustomAttributesTable::getAttributes(table); - emit editedMapData(); + map->modify(); } Tileset* Editor::getCurrentMapPrimaryTileset() @@ -2056,7 +2042,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 || !map->layout || !event || event->getEventType() != Event::Type::Warp) return; @@ -2067,7 +2053,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); } @@ -2077,7 +2063,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) @@ -2097,24 +2083,26 @@ void Editor::updateSelectedEvents() { emit objectsChanged(); } -void Editor::selectMapEvent(DraggablePixmapItem *object) { - selectMapEvent(object, false); -} - 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 group selection + selected_events->append(object); + } else if (selected_events->length() > 1) { + // Removing event 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) { @@ -2169,56 +2157,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 @@ -2252,7 +2208,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)); @@ -2269,7 +2225,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 @@ -2325,10 +2281,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; } @@ -2342,7 +2296,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()) { @@ -2362,8 +2316,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 e311eb21..3df6b0e1 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" @@ -21,6 +20,8 @@ #include "imageexport.h" #include "maplistmodels.h" #include "eventfilters.h" +#include "newmapconnectiondialog.h" +#include "config.h" #include #include @@ -45,7 +46,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 @@ -70,20 +73,21 @@ 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 && !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 // 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); } MainWindow::~MainWindow() { delete label_MapRulerStatus; + delete editor; delete ui; } @@ -123,6 +127,10 @@ void MainWindow::initWindow() { ui->actionCheck_for_Updates->setVisible(false); #endif +#ifdef DISABLE_CHARTS_MODULE + ui->pushButton_SummaryChart->setVisible(false); +#endif + setWindowDisabled(true); } @@ -150,9 +158,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"); @@ -212,19 +220,30 @@ 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"}, +}; + +static const QMap mainTabIcons = { + {MainTab::Map, QIcon(QStringLiteral(":/icons/minimap.ico"))}, + {MainTab::Events, QIcon(QStringLiteral(":/icons/viewsprites.ico"))}, + {MainTab::Header, QIcon(QStringLiteral(":/icons/application_form_edit.ico"))}, + {MainTab::Connections, QIcon(QStringLiteral(":/icons/connections.ico"))}, + {MainTab::WildPokemon, QIcon(QStringLiteral(":/icons/tall_grass.ico"))}, +}; + 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/minimap.ico"))); - ui->mainTabBar->addTab("Events"); - ui->mainTabBar->setTabIcon(1, QIcon(QStringLiteral(":/icons/viewsprites.ico"))); - ui->mainTabBar->addTab("Header"); - ui->mainTabBar->setTabIcon(2, QIcon(QStringLiteral(":/icons/application_form_edit.ico"))); - ui->mainTabBar->addTab("Connections"); - ui->mainTabBar->setTabIcon(3, QIcon(QStringLiteral(":/icons/connections.ico"))); - 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(i, mainTabIcons.value(i)); + } WheelFilter *wheelFilter = new WheelFilter(this); ui->mainTabBar->installEventFilter(wheelFilter); @@ -268,10 +287,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 @@ -321,12 +338,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) {} @@ -335,12 +352,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::loadMapRequested, this, &MainWindow::onLoadMapRequested); + 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::editedMapData, this, &MainWindow::markMapEdited); 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); @@ -366,7 +382,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); @@ -392,18 +408,11 @@ void MainWindow::initEditor() { } void MainWindow::initMiscHeapObjects() { - 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(); } void MainWindow::initMapSortOrder() { - mapSortOrder = porymapConfig.getMapSortOrder(); - this->ui->mapListContainer->setCurrentIndex(static_cast(this->mapSortOrder)); + this->ui->mapListContainer->setCurrentIndex(static_cast(porymapConfig.mapSortOrder)); } void MainWindow::showWindowTitle() { @@ -434,39 +443,16 @@ void MainWindow::showWindowTitle() { } void MainWindow::markMapEdited() { - if (editor && editor->map) { - editor->map->hasUnsavedDataChanges = true; - showWindowTitle(); - } + if (editor) markSpecificMapEdited(editor->map); } -// 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); +void MainWindow::markSpecificMapEdited(Map* map) { + if (!map) + return; + map->hasUnsavedDataChanges = true; - bool hasFlags = projectConfig.getMapAllowFlagsEnabled(); - 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.getEventWeatherTriggerEnabled()); - ui->newEventToolButton->newSecretBaseAction->setVisible(projectConfig.getEventSecretBaseEnabled()); - ui->newEventToolButton->newCloneObjectAction->setVisible(projectConfig.getEventCloneObjectEnabled()); - - bool floorNumEnabled = projectConfig.getFloorNumberEnabled(); - 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()); + if (editor && editor->map == map) + showWindowTitle(); } void MainWindow::on_lineEdit_filterBox_textChanged(const QString &text) { @@ -485,7 +471,7 @@ void MainWindow::applyMapListFilter(QString filterText) { FilterChildrenProxyModel *proxy; QTreeView *list; QModelIndex sourceIndex; - switch (this->mapSortOrder) { + switch (porymapConfig.mapSortOrder) { case MapSortOrder::SortByGroup: proxy = this->groupListProxyModel; list = this->ui->mapList; @@ -515,25 +501,32 @@ 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()); - ui->horizontalSlider_CollisionTransparency->blockSignals(true); - this->editor->collisionOpacity = static_cast(porymapConfig.getCollisionOpacity()) / 100; - ui->horizontalSlider_CollisionTransparency->setValue(porymapConfig.getCollisionOpacity()); - ui->horizontalSlider_CollisionTransparency->blockSignals(false); - ui->horizontalSlider_MetatileZoom->blockSignals(true); - ui->horizontalSlider_MetatileZoom->setValue(porymapConfig.getMetatilesZoom()); - ui->horizontalSlider_MetatileZoom->blockSignals(false); - ui->horizontalSlider_CollisionZoom->blockSignals(true); - ui->horizontalSlider_CollisionZoom->setValue(porymapConfig.getCollisionZoom()); - ui->horizontalSlider_CollisionZoom->blockSignals(false); - setTheme(porymapConfig.getTheme()); + 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); + 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); + ui->checkBox_MirrorConnections->setChecked(porymapConfig.mirrorConnectingMaps); + this->editor->collisionOpacity = static_cast(porymapConfig.collisionOpacity) / 100; + ui->horizontalSlider_CollisionTransparency->setValue(porymapConfig.collisionOpacity); + 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_CollisionZoom->setValue(porymapConfig.collisionZoom); + + setTheme(porymapConfig.theme); + setDivingMapsVisible(porymapConfig.showDiveEmergeMaps); } void MainWindow::restoreWindowState() { @@ -557,17 +550,20 @@ 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) { - projectOpenFailure = true; - 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; } 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) { @@ -580,53 +576,60 @@ 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 (!closeProject()) { + logInfo("Aborted project open."); + return false; + } + const QString openMessage = QString("Opening %1").arg(projectString); this->statusBar()->showMessage(openMessage); logInfo(openMessage); - userConfig.setProjectDir(dir); + userConfig.projectDir = dir; userConfig.load(); - projectConfig.setProjectDir(dir); + projectConfig.projectDir = 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(editor); - QObject::connect(editor->project, &Project::reloadProject, this, &MainWindow::on_action_Reload_Project_triggered); - 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->clearLayoutsTable(); - editor->project->clearMapCache(); - editor->project->clearTilesetCache(); - } + // Create the project + auto project = new Project(editor); + project->set_root(dir); + QObject::connect(project, &Project::reloadProject, this, &MainWindow::on_action_Reload_Project_triggered); + 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->setProject(project); - this->projectOpenFailure = !(loadDataStructures() - && populateMapList() - && (this->mapSortOrder == MapSortOrder::SortByLayout ? setInitialLayout() : setInitialMap())); - - if (this->projectOpenFailure) { - this->statusBar()->showMessage(QString("Failed to open %1").arg(projectString)); - showProjectOpenFailure(); + // Make sure project looks reasonable before attempting to load it + if (!checkProjectSanity()) { + delete this->editor->project; return false; } + + // Load the project + if (!(loadProjectData() && setProjectUI() && 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 + this->editor->project->saveConfig(); showWindowTitle(); this->statusBar()->showMessage(QString("Opened %1").arg(projectString)); + porymapConfig.projectManuallyClosed = false; porymapConfig.addRecentProject(dir); refreshRecentProjectsMenu(); @@ -640,20 +643,49 @@ 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; + + 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)); + 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() { - return !projectOpenFailure && editor && editor->project; + return editor && editor->project; } bool MainWindow::setDefaultView() { - if (this->mapSortOrder == MapSortOrder::SortByLayout) { + if (porymapConfig.mapSortOrder == MapSortOrder::SortByLayout) { return setLayout(getDefaultLayout()); } else { return setMap(getDefaultMap(), true); @@ -661,10 +693,10 @@ bool MainWindow::setDefaultView() { } bool MainWindow::setRecentView() { - if (this->mapSortOrder == MapSortOrder::SortByLayout) { - return setLayout(userConfig.getRecentLayout()); + if (porymapConfig.mapSortOrder == MapSortOrder::SortByLayout) { + return setLayout(userConfig.recentLayout); } else { - return setMap(userConfig.getRecentMap(), true); + return setMap(userConfig.recentMap, true); } } @@ -672,7 +704,7 @@ QString MainWindow::getDefaultMap() { if (editor && editor->project) { QList names = editor->project->groupedMapNames; if (!names.isEmpty()) { - QString recentMap = userConfig.getRecentMap(); + QString recentMap = userConfig.recentMap; if (!recentMap.isNull() && recentMap.length() > 0) { for (int i = 0; i < names.length(); i++) { if (names.value(i).contains(recentMap)) { @@ -698,7 +730,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; @@ -718,7 +750,7 @@ bool MainWindow::setInitialLayout() { names = editor->project->mapLayoutsTable; // Try to set most recently-opened layout, if it's still in the list. - QString recentLayout = userConfig.getRecentLayout(); + QString recentLayout = userConfig.recentLayout; if (!recentLayout.isEmpty() && names.contains(recentLayout) && setLayout(recentLayout)) return true; @@ -782,7 +814,7 @@ void MainWindow::openSubWindow(QWidget * window) { QString MainWindow::getDefaultLayout() { if (editor && editor->project) { - QString recentLayout = userConfig.getRecentLayout(); + QString recentLayout = userConfig.recentLayout; if (!recentLayout.isEmpty() && editor->project->mapLayoutsTable.contains(recentLayout)) { return recentLayout; } else if (!editor->project->mapLayoutsTable.isEmpty()) { @@ -798,11 +830,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); } @@ -820,6 +848,11 @@ void MainWindow::on_action_Reload_Project_triggered() { openProject(editor->project->root); } +void MainWindow::on_action_Close_Project_triggered() { + closeProject(); + porymapConfig.projectManuallyClosed = true; +} + void MainWindow::unsetMap() { this->editor->unsetMap(); @@ -832,9 +865,34 @@ void MainWindow::unsetMap() { this->ui->comboBox_LayoutSelector->setEnabled(false); } +// 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); + 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") + .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 scroll) { // if map name is empty, clear & disable map ui - if (map_name.isEmpty()) { + if (map_name.isEmpty() || map_name == DYNAMIC_MAP_NAME) { unsetMap(); return false; } @@ -868,15 +926,13 @@ bool MainWindow::setMap(QString map_name, bool scroll) { showWindowTitle(); - connect(editor->map, &Map::mapChanged, this, &MainWindow::onMapChanged, Qt::UniqueConnection); connect(editor->map, &Map::mapNeedsRedrawing, this, &MainWindow::onMapNeedsRedrawing, Qt::UniqueConnection); connect(editor->map, &Map::modified, this, &MainWindow::markMapEdited, Qt::UniqueConnection); connect(editor->layout, &Layout::layoutChanged, this, &MainWindow::onLayoutChanged, Qt::UniqueConnection); connect(editor->layout, &Layout::needsRedrawing, this, &MainWindow::onLayoutNeedsRedrawing, Qt::UniqueConnection); - setRecentMapConfig(map_name); - updateMapList(); + userConfig.recentMap = map_name; Scripting::cb_MapOpened(map_name); prefab.updatePrefabUi(editor->layout); @@ -954,10 +1010,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)); @@ -965,15 +1017,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); @@ -994,13 +1039,13 @@ void MainWindow::openWarpMap(QString map_name, int event_id, Event::Group event_ } void MainWindow::setRecentMapConfig(QString mapName) { - userConfig.setRecentMap(mapName); - userConfig.setRecentLayout(""); + userConfig.recentMap = mapName; + userConfig.recentLayout = ""; } void MainWindow::setRecentLayoutConfig(QString layoutId) { - userConfig.setRecentLayout(layoutId); - userConfig.setRecentMap(""); + userConfig.recentLayout = layoutId; + userConfig.recentMap = ""; } void MainWindow::displayMapProperties() { @@ -1032,12 +1077,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); @@ -1158,45 +1199,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 @@ -1207,8 +1211,11 @@ bool MainWindow::loadProjectCombos() { const QSignalBlocker blocker5(ui->comboBox_Weather); const QSignalBlocker blocker6(ui->comboBox_BattleScene); const QSignalBlocker blocker7(ui->comboBox_Type); - const QSignalBlocker blocker8(ui->comboBox_LayoutSelector); + const QSignalBlocker blocker8(ui->comboBox_DiveMap); + const QSignalBlocker blocker9(ui->comboBox_EmergeMap); + const QSignalBlocker blocker10(ui->comboBox_LayoutSelector); + // Set up project comboboxes ui->comboBox_Song->clear(); ui->comboBox_Song->addItems(project->songNames); ui->comboBox_Location->clear(); @@ -1225,13 +1232,43 @@ bool MainWindow::loadProjectCombos() { ui->comboBox_Type->addItems(project->mapTypes); ui->comboBox_LayoutSelector->clear(); ui->comboBox_LayoutSelector->addItems(project->mapLayoutsTable); + 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); - return true; -} + // Show/hide parts of the UI that are dependent on the user's project settings -bool MainWindow::populateMapList() { - bool success = editor->project->readMapGroups(); + // Wild Encounters tab + ui->mainTabBar->setTabEnabled(MainTab::WildPokemon, 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()); + + // map models + // !TODO: delete these on close this->mapGroupModel = new MapGroupModel(editor->project); this->groupListProxyModel = new FilterChildrenProxyModel(); groupListProxyModel->setSourceModel(this->mapGroupModel); @@ -1252,7 +1289,52 @@ bool MainWindow::populateMapList() { on_toolButton_EnableDisable_EditGroups_clicked(); - return success; + 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); + 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(); + ui->comboBox_PrimaryTileset->clear(); + ui->comboBox_SecondaryTileset->clear(); + ui->comboBox_Weather->clear(); + ui->comboBox_BattleScene->clear(); + ui->comboBox_Type->clear(); + ui->comboBox_DiveMap->clear(); + ui->comboBox_EmergeMap->clear(); + ui->lineEdit_filterBox->clear(); + + // Clear map models + if (this->mapGroupModel) { + delete this->mapGroupModel; + this->mapGroupModel = nullptr; + delete this->groupListProxyModel; + this->groupListProxyModel = nullptr; + } + if (this->mapAreaModel) { + delete this->mapAreaModel; + this->mapAreaModel = nullptr; + delete this->areaListProxyModel; + this->areaListProxyModel = nullptr; + } + if (this->layoutTreeModel) { + delete this->layoutTreeModel; + this->layoutTreeModel = nullptr; + delete this->layoutListProxyModel; + this->layoutListProxyModel = nullptr; + } } void MainWindow::scrollTreeView(QString itemName) { @@ -1283,10 +1365,10 @@ void MainWindow::onOpenMapListContextMenu(const QPoint &point) { void (MainWindow::*addFunction)(QAction *); QString actionText; - switch (this->mapSortOrder) { + switch (porymapConfig.mapSortOrder) { case MapSortOrder::SortByGroup: model = this->mapGroupModel; - dataRole = MapListRoles::GroupRole; + dataRole = MapListUserRoles::GroupRole; proxy = this->groupListProxyModel; list = this->ui->mapList; addFunction = &MainWindow::onAddNewMapToGroupClick; @@ -1532,7 +1614,7 @@ void MainWindow::mapListRemoveGroup() { QModelIndex index = this->groupListProxyModel->mapToSource(proxyIndex); QStandardItem *item = this->mapGroupModel->getItem(index)->child(index.row(), index.column()); if (!item) continue; - QString type = item->data(MapListRoles::TypeRole).toString(); + QString type = item->data(MapListUserRoles::TypeRole).toString(); if (type == "map_group" && !item->hasChildren()) { QString groupName = item->data(Qt::UserRole).toString(); // delete empty group @@ -1550,7 +1632,7 @@ void MainWindow::mapListRemoveArea() { QModelIndex index = this->areaListProxyModel->mapToSource(proxyIndex); QStandardItem *item = this->mapAreaModel->getItem(index)->child(index.row(), index.column()); if (!item) continue; - QString type = item->data(MapListRoles::TypeRole).toString(); + QString type = item->data(MapListUserRoles::TypeRole).toString(); if (type == "map_section" && !item->hasChildren()) { QString groupName = item->data(Qt::UserRole).toString(); // delete empty section @@ -1617,7 +1699,7 @@ void MainWindow::onNewMapCreated() { editor->project->saveMap(newMap); editor->project->saveAllDataStructures(); - loadProjectCombos(); // need to maybe repopulate layout combo + setProjectUI(); // need to maybe repopulate layout combo // Add new Map / Layout to the mapList models this->mapGroupModel->insertMapItem(newMapName, editor->project->groupNames[newMapGroup]); @@ -1626,6 +1708,16 @@ void MainWindow::onNewMapCreated() { setMap(newMapName, true); + // Refresh any combo box that displays map names and persists between maps + // (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); + 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); @@ -1643,12 +1735,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() { @@ -1762,7 +1852,7 @@ void MainWindow::on_actionNew_Tileset_triggered() { } insertTilesetLabel(&editor->project->tilesetLabelsOrdered, createTilesetDialog->fullSymbolName); - loadProjectCombos(); // need to reload tileset combos + setProjectUI(); // need to reload tileset combos QMessageBox msgBox(this); msgBox.setText("Successfully created tileset."); @@ -1785,7 +1875,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() { @@ -1835,33 +1925,25 @@ void MainWindow::currentMetatilesSelectionChanged() { void MainWindow::on_mapListContainer_currentChanged(int index) { switch (index) { case MapListTab::Groups: - this->mapSortOrder = MapSortOrder::SortByGroup; + porymapConfig.mapSortOrder = MapSortOrder::SortByGroup; if (this->editor && this->editor->map) scrollTreeView(this->editor->map->name); break; case MapListTab::Areas: - this->mapSortOrder = MapSortOrder::SortByArea; + porymapConfig.mapSortOrder = MapSortOrder::SortByArea; if (this->editor && this->editor->map) scrollTreeView(this->editor->map->name); break; case MapListTab::Layouts: - this->mapSortOrder = MapSortOrder::SortByLayout; + porymapConfig.mapSortOrder = MapSortOrder::SortByLayout; if (this->editor && this->editor->layout) scrollTreeView(this->editor->layout->id); break; } - porymapConfig.setMapSortOrder(this->mapSortOrder); } void MainWindow::on_mapList_activated(const QModelIndex &index) { QVariant data = index.data(Qt::UserRole); - if (index.data(MapListRoles::TypeRole) == "map_name" && !data.isNull()) { + 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); - } + userSetMap(mapName); } } @@ -1873,7 +1955,7 @@ void MainWindow::on_layoutList_activated(const QModelIndex &index) { if (!index.isValid()) return; QVariant data = index.data(Qt::UserRole); - if (index.data(MapListRoles::TypeRole) == "map_layout" && !data.isNull()) { + if (index.data(MapListUserRoles::TypeRole) == "map_layout" && !data.isNull()) { QString layoutId = data.toString(); if (!setLayout(layoutId)) { @@ -1920,6 +2002,12 @@ void MainWindow::on_action_Save_Project_triggered() { showWindowTitle(); } +void MainWindow::on_action_Save_triggered() { + editor->save(); + updateMapList(); + showWindowTitle(); +} + void MainWindow::duplicate() { editor->duplicateSelectedEvents(); } @@ -1951,8 +2039,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); } } @@ -1969,7 +2057,7 @@ void MainWindow::copy() { { default: break; - case 0: + case MainTab::Map: { // copy the map image QPixmap pixmap = editor->layout ? editor->layout->render(true) : QPixmap(); @@ -1977,7 +2065,7 @@ void MainWindow::copy() { logInfo("Copied current map image to clipboard"); break; } - case 1: + case MainTab::Events: { if (!editor || !editor->project) break; @@ -2017,7 +2105,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); @@ -2048,7 +2136,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(); @@ -2076,7 +2164,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") { @@ -2097,7 +2185,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") { @@ -2109,56 +2197,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()) { @@ -2172,12 +2229,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(); @@ -2185,27 +2236,22 @@ void MainWindow::on_mapViewTab_tabBarClicked(int index) if (index != oldIndex) Scripting::cb_MapViewTabChanged(oldIndex, index); - if (index == 0) { + if (index == MapViewTab::Metatiles) { editor->setEditingMetatiles(); - } else if (index == 1) { + } else if (index == MapViewTab::Collision) { editor->setEditingCollision(); - } else if (index == 2) { + } else if (index == MapViewTab::Prefabs) { editor->setEditingMetatiles(); - 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->layout); } } editor->setCursorRectVisible(false); } -void MainWindow::on_action_Exit_triggered() -{ - QApplication::quit(); -} - void MainWindow::on_mainTabBar_tabBarClicked(int index) { int oldIndex = ui->mainTabBar->currentIndex(); @@ -2213,29 +2259,35 @@ 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()); clickToolButtonFromEditAction(editor->mapEditAction); - } else if (index == 1) { + } else if (index == MainTab::Events) { ui->stackedWidget_MapEvents->setCurrentIndex(1); editor->setEditingObjects(); clickToolButtonFromEditAction(editor->objectEditAction); - } else if (index == 3) { + } else if (index == MainTab::Connections) { editor->setEditingConnections(); - } else if (index == 4) { + } else if (index == MainTab::WildPokemon) { editor->setEditingEncounters(); } if (!editor->map) return; - 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); } } @@ -2249,14 +2301,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)) { @@ -2268,7 +2320,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)) { @@ -2357,7 +2409,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"); @@ -2375,7 +2427,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))); } @@ -2384,11 +2437,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() { @@ -2548,30 +2601,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) { @@ -2615,17 +2653,73 @@ void MainWindow::eventTabChanged(int index) { isProgrammaticEventTabChange = false; } +void MainWindow::on_actionDive_Emerge_Map_triggered() { + setDivingMapsVisible(ui->actionDive_Emerge_Map->isChecked()); +} + +void MainWindow::on_groupBox_DiveMapOpacity_toggled(bool on) { + setDivingMapsVisible(on); +} + +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; }"; + ui->slider_DiveEmergeMapOpacity->setStyleSheet(stylesheet); + ui->slider_DiveMapOpacity->setStyleSheet(stylesheet); + ui->slider_EmergeMapOpacity->setStyleSheet(stylesheet); + + // 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; + + 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->renderDivingConnections(); + } + this->editor->updateDivingMapsVisibility(); +} + +// 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->updateDivingMapsVisibility(); +} + +void MainWindow::on_slider_DiveMapOpacity_valueChanged(int value) { + porymapConfig.diveMapOpacity = value; + this->editor->updateDivingMapsVisibility(); +} + +void MainWindow::on_slider_EmergeMapOpacity_valueChanged(int value) { + porymapConfig.emergeMapOpacity = value; + this->editor->updateDivingMapsVisibility(); +} + 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); } -void MainWindow::on_toolButton_deleteObject_clicked() { - if (ui->mainTabBar->currentIndex() != 1) { - // 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; @@ -2672,15 +2766,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->mapEditAction = Editor::EditAction::Paint; else editor->objectEditAction = Editor::EditAction::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); @@ -2694,7 +2787,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->mapEditAction = Editor::EditAction::Select; else editor->objectEditAction = Editor::EditAction::Select; @@ -2713,7 +2806,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->mapEditAction = Editor::EditAction::Fill; else editor->objectEditAction = Editor::EditAction::Fill; @@ -2732,7 +2825,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->mapEditAction = Editor::EditAction::Pick; else editor->objectEditAction = Editor::EditAction::Pick; @@ -2751,7 +2844,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->mapEditAction = Editor::EditAction::Move; else editor->objectEditAction = Editor::EditAction::Move; @@ -2770,7 +2863,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->mapEditAction = Editor::EditAction::Shift; else editor->objectEditAction = Editor::EditAction::Shift; @@ -2789,7 +2882,7 @@ void MainWindow::on_toolButton_Shift_clicked() void MainWindow::checkToolButtons() { Editor::EditAction editAction; - if (ui->mainTabBar->currentIndex() == 0) { + if (ui->mainTabBar->currentIndex() == MainTab::Map) { editAction = editor->mapEditAction; } else { editAction = editor->objectEditAction; @@ -2823,21 +2916,11 @@ void MainWindow::clickToolButtonFromEditAction(Editor::EditAction editAction) { } } -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); +void MainWindow::onOpenConnectedMap(MapConnection *connection) { + if (!connection) return; - } - editor->setSelectedConnectionFromMap(fromMapName); -} - -void MainWindow::onMapChanged(Map *) { - updateMapList(); + if (userSetMap(connection->targetMapName(), true)) + editor->setSelectedConnection(connection->findMirror()); } void MainWindow::onLayoutChanged(Layout *) { @@ -2849,10 +2932,13 @@ void MainWindow::onMapNeedsRedrawing() { } void MainWindow::onLayoutNeedsRedrawing() { - qDebug() << "MainWindow::onLayoutNeedsRedrawing"; redrawLayoutScene(); } +void MainWindow::onMapLoaded(Map *map) { + connect(map, &Map::modified, [this, map] { this->markSpecificMapEdited(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 @@ -2877,11 +2963,6 @@ void MainWindow::onTilesetsSaved(QString primaryTilesetLabel, QString secondaryT } } -void MainWindow::onWildMonDataChanged() { - editor->saveEncounterTabData(); - markMapEdited(); -} - void MainWindow::onMapRulerStatusChanged(const QString &status) { if (status.isEmpty()) { label_MapRulerStatus->hide(); @@ -2958,45 +3039,23 @@ void MainWindow::importMapFromAdvanceMap1_92() void MainWindow::showExportMapImageWindow(ImageExporterMode mode) { if (!editor->project) return; + // 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); - this->mapImageExporter->setAttribute(Qt::WA_DeleteOnClose); openSubWindow(this->mapImageExporter); } -void MainWindow::on_comboBox_ConnectionDirection_currentTextChanged(const QString &direction) -{ - editor->updateCurrentConnectionDirection(direction); - markMapEdited(); -} +void MainWindow::on_pushButton_AddConnection_clicked() { + if (!this->editor || !this->editor->map || !this->editor->project) + return; -void MainWindow::on_spinBox_ConnectionOffset_valueChanged(int offset) -{ - editor->updateConnectionOffset(offset); - markMapEdited(); -} - -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() -{ - editor->addNewConnection(); - markMapEdited(); -} - -void MainWindow::on_pushButton_RemoveConnection_clicked() -{ - editor->removeCurrentConnection(); - markMapEdited(); + auto dialog = new NewMapConnectionDialog(this, this->editor->map, this->editor->project->mapNames); + connect(dialog, &NewMapConnectionDialog::accepted, this->editor, &Editor::addConnection); + dialog->exec(); } void MainWindow::on_pushButton_NewWildMonGroup_clicked() { @@ -3007,24 +3066,41 @@ void MainWindow::on_pushButton_DeleteWildMonGroup_clicked() { editor->deleteWildMonGroup(); } +void MainWindow::on_pushButton_SummaryChart_clicked() { + if (!this->wildMonChart) { + 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); +} + 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)) { - editor->updateDiveMap(mapName); - markMapEdited(); - } +void MainWindow::on_button_OpenDiveMap_clicked() { + const QString mapName = ui->comboBox_DiveMap->currentText(); + if (editor->project->mapNames.contains(mapName)) + userSetMap(mapName, true); } -void MainWindow::on_comboBox_EmergeMap_currentTextChanged(const QString &mapName) -{ - if (mapName.isEmpty() || editor->project->mapNames.contains(mapName)) { +void MainWindow::on_button_OpenEmergeMap_clicked() { + const QString mapName = ui->comboBox_EmergeMap->currentText(); + if (editor->project->mapNames.contains(mapName)) + userSetMap(mapName, true); +} + +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)) + editor->updateDiveMap(mapName); +} + +void MainWindow::on_comboBox_EmergeMap_currentTextChanged(const QString &mapName) { + if (mapName.isEmpty() || editor->project->mapNames.contains(mapName)) editor->updateEmergeMap(mapName); - markMapEdited(); - } } void MainWindow::on_comboBox_PrimaryTileset_currentTextChanged(const QString &tilesetLabel) @@ -3076,7 +3152,7 @@ void MainWindow::on_pushButton_ChangeDimensions_clicked() { heightSpinBox->setValue(editor->layout->getHeight()); bwidthSpinBox->setValue(editor->layout->getBorderWidth()); bheightSpinBox->setValue(editor->layout->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); @@ -3151,8 +3227,12 @@ 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_checkBox_MirrorConnections_stateChanged(int selected) +{ + porymapConfig.mirrorConnectingMaps = (selected == Qt::Checked); } void MainWindow::on_actionTileset_Editor_triggered() @@ -3292,9 +3372,9 @@ void MainWindow::on_toolButton_CollapseAll_Layouts_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() { @@ -3322,7 +3402,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); @@ -3342,7 +3422,7 @@ void MainWindow::openProjectSettingsEditor(int tab) { } void MainWindow::on_actionProject_Settings_triggered() { - this->openProjectSettingsEditor(porymapConfig.getProjectSettingsTab()); + this->openProjectSettingsEditor(porymapConfig.projectSettingsTab); } void MainWindow::onWarpBehaviorWarningClicked() { @@ -3388,7 +3468,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); } @@ -3415,7 +3495,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; @@ -3440,7 +3520,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; @@ -3486,53 +3566,125 @@ 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) { - delete this->regionMapEditor; - this->regionMapEditor = nullptr; - if (!silent) { - QMessageBox msgBox(this); - QString errorMsg = QString("There was an error opening the region map data. Please see %1 for full error details.\n\n%3") - .arg(getLogPath()) - .arg(getMostRecentError()); - msgBox.critical(nullptr, "Error Opening Region Map Editor", errorMsg); + if (!this->regionMapEditor->load(silent)) { + // The region map editor either failed to load, + // or the user declined configuring their settings. + if (!silent && this->regionMapEditor->setupErrored()) { + if (this->askToFixRegionMapEditor()) + return true; } + delete this->regionMapEditor; return false; } return true; } -void MainWindow::closeSupplementaryWindows() { - delete this->tilesetEditor; - delete this->regionMapEditor; - delete this->mapImageExporter; - delete this->newMapPrompt; - delete this->shortcutsEditor; - delete this->customScriptsEditor; - if (this->projectSettingsEditor) this->projectSettingsEditor->closeQuietly(); +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::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); +// Attempt to close any open sub-windows of the main window, giving each a chance to abort the process. +// 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 (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 (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->wildMonChart && !this->wildMonChart->close()) + return false; + this->wildMonChart = nullptr; + + if (this->projectSettingsEditor) this->projectSettingsEditor->closeQuietly(); + this->projectSettingsEditor = nullptr; + + return true; +} + +bool MainWindow::closeProject() { + if (!closeSupplementaryWindows()) + return false; + + if (!isProjectOpen()) + return true; + + // Check loaded maps for unsaved changes + bool unsavedChanges = false; + for (auto map : editor->project->mapCache.values()) { + if (map && map->hasUnsavedChanges()) { + unsavedChanges = true; + break; } - projectConfig.save(); - userConfig.save(); } + if (unsavedChanges) { + 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; + } + } + clearProjectUI(); + editor->closeProject(); + setWindowDisabled(true); + setWindowTitle(QCoreApplication::applicationName()); + + return true; +} + +void MainWindow::saveGlobalConfigs() { porymapConfig.setMainGeometry( this->saveGeometry(), this->saveState(), @@ -3542,6 +3694,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/project.cpp b/src/project.cpp index f74eaffe..37df536a 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -50,7 +50,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; @@ -78,7 +78,7 @@ void Project::initSignals() { emit reloadProject(); } else if (choice == QMessageBox::No) { if (showAgainCheck.isChecked()) { - porymapConfig.setMonitorFiles(false); + porymapConfig.monitorFiles = false; emit uncheckMonitorFilesAction(); } } @@ -92,6 +92,61 @@ 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() { + this->disabledSettingsNames.clear(); + 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); @@ -128,6 +183,9 @@ void Project::clearLayoutsTable() { } 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); @@ -140,17 +198,16 @@ 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; } -void Project::setNewMapConnections(Map *map) { - map->connections.clear(); -} - const QSet defaultTopLevelMapFields = { "id", "name", @@ -173,13 +230,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; @@ -208,12 +265,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"]); @@ -222,11 +279,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); @@ -308,11 +364,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); } @@ -320,18 +376,17 @@ 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++) { 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); - map->connections.append(connection); + // Successully read map connection + map->loadConnection(new MapConnection(mapConstantsToMapNames.value(mapConstant), direction, offset)); } else { logError(QString("Failed to find connected map for map constant '%1'").arg(mapConstant)); } @@ -526,7 +581,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()) @@ -567,7 +621,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.") @@ -631,7 +685,6 @@ void Project::saveMapLayouts() { OrderedJson::object layoutsObj; layoutsObj["layouts_table_label"] = layoutsLabel; - bool useCustomBorderSize = projectConfig.getUseCustomBorderSize(); OrderedJson::array layoutsArr; for (QString layoutId : mapLayoutsTableMaster) { Layout *layout = mapLayoutsMaster.value(layoutId); @@ -640,7 +693,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; } @@ -884,7 +937,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 " : ""); @@ -1067,10 +1120,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); @@ -1206,7 +1258,7 @@ void Project::setNewLayoutBlockdata(Layout *layout) { layout->blockdata.clear(); int width = layout->getWidth(); int height = layout->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++) { layout->blockdata.append(block); } @@ -1235,8 +1287,7 @@ void Project::setNewLayoutBorder(Layout *layout) { int width = layout->getBorderWidth(); int height = layout->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++) { @@ -1245,7 +1296,7 @@ void Project::setNewLayoutBorder(Layout *layout) { } else { // Fill the border with the default metatiles from the config. for (int i = 0; i < width * height; i++) { - layout->border.append(configMetatileIds.at(i)); + layout->border.append(projectConfig.newMapBorderMetatileIds.at(i)); } } @@ -1288,18 +1339,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); @@ -1330,29 +1380,30 @@ 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; // Connections - if (map->connections.length() > 0) { + auto connections = map->getConnections(); + if (connections.length() > 0) { OrderedJson::array connectionsArr; - for (MapConnection* connection : map->connections) { - if (mapNamesToMapConstants.contains(connection->map_name)) { + for (auto connection : connections) { + 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; @@ -1453,6 +1504,12 @@ void Project::saveAllDataStructures() { saveMapSections(); saveMapConstantsHeader(); saveWildMonData(); + saveConfig(); +} + +void Project::saveConfig() { + projectConfig.save(); + userConfig.save(); } void Project::loadTilesetAssets(Tileset* tileset) { @@ -1591,7 +1648,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)); @@ -1723,15 +1780,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.getEncounterJsonActive()) { + 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); @@ -1887,19 +1972,23 @@ 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); - layoutIdsToNames.insert(newMap->layout->id, newMap->layout->name); + this->mapLayouts.insert(newMap->layoutId, newMap->layout); + this->mapLayoutsTable.append(newMap->layoutId); + this->layoutIdsToNames.insert(newMap->layout->id, newMap->layout->name); if (!importedMap) { setNewLayoutBlockdata(newMap->layout); } @@ -1910,7 +1999,6 @@ Map* Project::addNewMapToGroup(QString mapName, int groupNum, Map *newMap, bool loadLayoutTilesets(newMap->layout); setNewMapEvents(newMap); - setNewMapConnections(newMap); return newMap; } @@ -1939,7 +2027,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)); @@ -1949,7 +2037,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)); @@ -2027,6 +2115,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, @@ -2034,6 +2123,7 @@ bool Project::readFieldmapProperties() { numPalsPrimaryName, numPalsTotalName, maxMapSizeName, + numTilesPerMetatileName, }; const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_fieldmap); fileWatcher.addPath(root + "/" + filename); @@ -2074,6 +2164,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; } @@ -2099,10 +2205,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()); - - // Avoid repeatedly writing the config file - projectConfig.setSaveDisabled(true); + for (auto name : defineNames) + this->disabledSettingsNames.insert(name); // Read Block masks auto readBlockMask = [defines](const QString name, uint16_t *value) { @@ -2118,21 +2222,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); @@ -2149,13 +2254,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 @@ -2163,7 +2268,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); } } @@ -2171,12 +2276,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; } @@ -2265,7 +2369,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))); @@ -2423,7 +2527,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)}; @@ -2436,7 +2540,7 @@ bool Project::readCoordEventWeatherNames() { } bool Project::readSecretBaseIds() { - if (!projectConfig.getEventSecretBaseEnabled()) + if (!projectConfig.eventSecretBaseEnabled) return true; const QStringList prefixes = {projectConfig.getIdentifier(ProjectIdentifier::regex_secret_bases)}; @@ -2479,7 +2583,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; } @@ -2519,17 +2623,6 @@ bool Project::readObjEventGfxConstants() { } bool Project::readMiscellaneousConstants() { - miscConstants.clear(); - if (userConfig.getEncounterJsonActive()) { - 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); @@ -2599,9 +2692,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(); @@ -2719,7 +2811,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 = {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); @@ -2902,7 +2994,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; @@ -2915,25 +3007,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 408ee68b..d40d3e1c 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->layout || !projectConfig.getUseCustomBorderSize()) + if (!this->editor || !this->editor->layout || !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->layout || !projectConfig.getUseCustomBorderSize()) + if (!this->editor || !this->editor->layout || !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->layout || !projectConfig.getUseCustomBorderSize()) + if (!this->editor || !this->editor->layout || !projectConfig.useCustomBorderSize) return; if (height < 1 || height > MAX_BORDER_HEIGHT) return; diff --git a/src/scriptapi/apiutility.cpp b/src/scriptapi/apiutility.cpp index 46dc142c..c12db39a 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); } @@ -157,7 +167,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); } @@ -171,7 +184,7 @@ bool ScriptUtility::getGridVisibility() { } void ScriptUtility::setBorderVisibility(bool visible) { - window->editor->toggleBorderVisibility(visible, false); + window->ui->checkBox_ToggleBorder->setChecked(visible); } bool ScriptUtility::getBorderVisibility() { diff --git a/src/scriptapi/scripting.cpp b/src/scriptapi/scripting.cpp index 112585c7..71fbabca 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"}, @@ -21,20 +23,21 @@ QMap callbackFunctions = { Scripting *instance = nullptr; +void Scripting::stop() { + 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); } 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(); @@ -46,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); @@ -74,6 +84,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 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")); 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/connectionpixmapitem.cpp b/src/ui/connectionpixmapitem.cpp index 256c556a..f8412012 100644 --- a/src/ui/connectionpixmapitem.cpp +++ b/src/ui/connectionpixmapitem.cpp @@ -1,30 +1,48 @@ #include "connectionpixmapitem.h" +#include "editcommands.h" +#include "map.h" #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(); +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); + + // 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); -} - -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; + this->setPixmap(pixmap); } QVariant ConnectionPixmapItem::itemChange(GraphicsItemChange change, const QVariant &value) @@ -32,32 +50,23 @@ QVariant ConnectionPixmapItem::itemChange(GraphicsItemChange change, const QVari if (change == ItemPositionChange) { QPointF newPos = value.toPointF(); - qreal x, y; - int newOffset = this->initialOffset; - 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 { - x = this->initialX; + 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() / 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 (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 { - 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->offset = newOffset; - emit connectionMoved(this->connection); return QPointF(x, y); } else { @@ -65,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); @@ -74,28 +109,25 @@ bool ConnectionPixmapItem::getEditable() { return (this->flags() & ItemIsMovable) != 0; } -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) { - 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); - } +void ConnectionPixmapItem::setSelected(bool selected) { + if (this->selected == selected) + return; + this->selected = selected; + this->render(); + emit selectionChanged(selected); } void ConnectionPixmapItem::mousePressEvent(QGraphicsSceneMouseEvent *) { if (!this->getEditable()) return; - emit connectionItemSelected(this); + 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); + emit connectionItemDoubleClicked(this->connection); } diff --git a/src/ui/connectionslistitem.cpp b/src/ui/connectionslistitem.cpp new file mode 100644 index 00000000..a5b8759a --- /dev/null +++ b/src/ui/connectionslistitem.cpp @@ -0,0 +1,103 @@ +#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), + 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(MapConnection::cardinalDirections); + + 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(); +} + +ConnectionsListItem::~ConnectionsListItem() +{ + delete ui; +} + +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); + + ui->comboBox_Direction->setTextItem(this->connection->direction()); + ui->comboBox_Map->setTextItem(this->connection->targetMapName()); + ui->spinBox_Offset->setValue(this->connection->offset()); +} + +void ConnectionsListItem::setSelected(bool selected) { + if (selected == this->isSelected) + return; + this->isSelected = selected; + + this->setStyleSheet(selected ? ".ConnectionsListItem { border: 1px solid rgb(255, 0, 255); }" + : ".ConnectionsListItem { border-width: 1px; }"); + if (selected) + emit this->selected(); +} + +void ConnectionsListItem::mousePressEvent(QMouseEvent *) { + this->setSelected(true); +} + +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(QString mapName) { + this->setSelected(true); + 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); + if (this->map) + this->map->editHistory.push(new MapConnectionMove(this->connection, offset, this->actionId)); +} + +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/customscriptseditor.cpp b/src/ui/customscriptseditor.cpp index 31e2ad7a..b7180b92 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,11 +23,11 @@ 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); - 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(); } 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/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 *) { diff --git a/src/ui/encountertabledelegates.cpp b/src/ui/encountertabledelegates.cpp index 7824087e..230abc7b 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 { @@ -75,12 +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) { editor->setMinimum(0); - editor->setMaximum(180); + editor->setMaximum(this->project->maxEncounterRate); } return editor; diff --git a/src/ui/encountertablemodel.cpp b/src/ui/encountertablemodel.cpp index fd870e40..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 { @@ -199,7 +211,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/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/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index f649ce35..c1f9e45e 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -27,13 +27,14 @@ 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->layout = editor_->layout; 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); if (this->map) { @@ -175,7 +176,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()); progress.close(); }; @@ -194,6 +195,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: @@ -208,6 +212,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: @@ -254,25 +264,29 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool inclu visited.insert(cur.map->name); stitchedMaps.append(cur); - for (MapConnection *connection : cur.map->connections) { - if (connection->direction == "dive" || connection->direction == "emerge") - continue; + for (MapConnection *connection : cur.map->getConnections()) { + 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 = connection->targetMap(); + if (!connectionMap) + continue; + 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}); } @@ -326,7 +340,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); } @@ -361,7 +375,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()); @@ -399,8 +413,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; layout->renderBorder(); int borderHorzDist = editor->getBorderDrawDistance(layout->getBorderWidth()); @@ -423,17 +436,17 @@ QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { return pixmap; } - if (!this->mode) { + if (!ignoreBorder && (this->showUpConnections || this->showDownConnections || this->showLeftConnections || this->showRightConnections)) { // 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") || (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(); } @@ -442,7 +455,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) { @@ -480,6 +493,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(); @@ -522,21 +547,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(); } diff --git a/src/ui/maplistmodels.cpp b/src/ui/maplistmodels.cpp index 880a98ca..f5835357 100644 --- a/src/ui/maplistmodels.cpp +++ b/src/ui/maplistmodels.cpp @@ -73,7 +73,7 @@ QMimeData *MapGroupModel::mimeData(const QModelIndexList &indexes) const { // if dropping a selection containing a group(s) and map(s), clear all selection but first group. for (const QModelIndex &index : indexes) { - if (index.isValid() && data(index, MapListRoles::TypeRole).toString() == "map_group") { + if (index.isValid() && data(index, MapListUserRoles::TypeRole).toString() == "map_group") { QString groupName = data(index, Qt::UserRole).toString(); stream << groupName; mimeData->setData("application/porymap.mapgroupmodel.group", encodedData); @@ -221,8 +221,8 @@ QStandardItem *MapGroupModel::createGroupItem(QString groupName, int groupIndex, if (!group) group = new QStandardItem; group->setText(groupName); group->setData(groupName, Qt::UserRole); - group->setData("map_group", MapListRoles::TypeRole); - group->setData(groupIndex, MapListRoles::GroupRole); + group->setData("map_group", MapListUserRoles::TypeRole); + group->setData(groupIndex, MapListUserRoles::GroupRole); group->setFlags(Qt::ItemIsEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | Qt::ItemIsSelectable); this->groupItems.insert(groupName, group); return group; @@ -231,7 +231,7 @@ QStandardItem *MapGroupModel::createGroupItem(QString groupName, int groupIndex, QStandardItem *MapGroupModel::createMapItem(QString mapName, QStandardItem *map) { if (!map) map = new QStandardItem; map->setData(mapName, Qt::UserRole); - map->setData("map_name", MapListRoles::TypeRole); + map->setData("map_name", MapListUserRoles::TypeRole); map->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled); this->mapItems[mapName] = map; return map; @@ -315,7 +315,7 @@ QVariant MapGroupModel::data(const QModelIndex &index, int role) const { } QStandardItem *item = this->getItem(index)->child(row, col); - QString type = item->data(MapListRoles::TypeRole).toString(); + QString type = item->data(MapListUserRoles::TypeRole).toString(); if (type == "map_group") { if (!item->hasChildren()) { @@ -340,7 +340,7 @@ QVariant MapGroupModel::data(const QModelIndex &index, int role) const { } else if (role == Qt::DisplayRole) { QStandardItem *item = this->getItem(index)->child(row, col); - QString type = item->data(MapListRoles::TypeRole).toString(); + QString type = item->data(MapListUserRoles::TypeRole).toString(); if (type == "map_name") { return QString("[%1.%2] ").arg(this->getItem(index)->row()).arg(row, 2, 10, QLatin1Char('0')) + item->data(Qt::UserRole).toString(); @@ -354,7 +354,7 @@ QVariant MapGroupModel::data(const QModelIndex &index, int role) const { } bool MapGroupModel::setData(const QModelIndex &index, const QVariant &value, int role) { - if (role == Qt::UserRole && data(index, MapListRoles::TypeRole).toString() == "map_group") { + if (role == Qt::UserRole && data(index, MapListUserRoles::TypeRole).toString() == "map_group") { // verify uniqueness of new group name if (this->project->groupNames.contains(value.toString())) { return false; @@ -379,8 +379,8 @@ QStandardItem *MapAreaModel::createAreaItem(QString mapsecName, int areaIndex) { area->setText(mapsecName); area->setEditable(false); area->setData(mapsecName, Qt::UserRole); - area->setData("map_section", MapListRoles::TypeRole); - area->setData(areaIndex, MapListRoles::GroupRole); + area->setData("map_section", MapListUserRoles::TypeRole); + area->setData(areaIndex, MapListUserRoles::GroupRole); // group->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled); this->areaItems.insert(mapsecName, area); return area; @@ -391,7 +391,7 @@ QStandardItem *MapAreaModel::createMapItem(QString mapName, int groupIndex, int map->setText(mapName); map->setEditable(false); map->setData(mapName, Qt::UserRole); - map->setData("map_name", MapListRoles::TypeRole); + map->setData("map_name", MapListUserRoles::TypeRole); // map->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled); this->mapItems.insert(mapName, map); return map; @@ -401,7 +401,7 @@ QStandardItem *MapAreaModel::insertAreaItem(QString areaName) { int newAreaIndex = this->project->appendMapsec(areaName); QStandardItem *item = createAreaItem(areaName, newAreaIndex); this->root->insertRow(newAreaIndex, item); - this->areaItems["MAPSEC_NONE"]->setData(newAreaIndex + 1, MapListRoles::GroupRole); + this->areaItems["MAPSEC_NONE"]->setData(newAreaIndex + 1, MapListUserRoles::GroupRole); return item; } @@ -425,7 +425,7 @@ void MapAreaModel::removeArea(int areaIndex) { void MapAreaModel::initialize() { this->areaItems.clear(); this->mapItems.clear(); - this->setSortRole(MapListRoles::GroupRole); + this->setSortRole(MapListUserRoles::GroupRole); for (int i : this->project->mapSectionNameToValue) { QString mapsecName = project->mapSectionValueToName.value(i); @@ -487,7 +487,7 @@ QVariant MapAreaModel::data(const QModelIndex &index, int role) const { } QStandardItem *item = this->getItem(index)->child(row, col); - QString type = item->data(MapListRoles::TypeRole).toString(); + QString type = item->data(MapListUserRoles::TypeRole).toString(); if (type == "map_section") { if (item->hasChildren()) { @@ -512,11 +512,11 @@ QVariant MapAreaModel::data(const QModelIndex &index, int role) const { } else if (role == Qt::DisplayRole) { QStandardItem *item = this->getItem(index)->child(row, col); - QString type = item->data(MapListRoles::TypeRole).toString(); + QString type = item->data(MapListUserRoles::TypeRole).toString(); if (type == "map_section") { return QString("[0x%1] %2") - .arg(QString("%1").arg(item->data(MapListRoles::GroupRole).toInt(), 2, 16, QLatin1Char('0')).toUpper()) + .arg(QString("%1").arg(item->data(MapListUserRoles::GroupRole).toInt(), 2, 16, QLatin1Char('0')).toUpper()) .arg(item->data(Qt::UserRole).toString()); } } @@ -538,7 +538,7 @@ QStandardItem *LayoutTreeModel::createLayoutItem(QString layoutId) { layout->setText(this->project->layoutIdsToNames[layoutId]); layout->setEditable(false); layout->setData(layoutId, Qt::UserRole); - layout->setData("map_layout", MapListRoles::TypeRole); + layout->setData("map_layout", MapListUserRoles::TypeRole); // // group->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled); this->layoutItems.insert(layoutId, layout); return layout; @@ -549,7 +549,7 @@ QStandardItem *LayoutTreeModel::createMapItem(QString mapName) { map->setText(mapName); map->setEditable(false); map->setData(mapName, Qt::UserRole); - map->setData("map_name", MapListRoles::TypeRole); + map->setData("map_name", MapListUserRoles::TypeRole); map->setFlags(Qt::NoItemFlags | Qt::ItemNeverHasChildren); this->mapItems.insert(mapName, map); return map; @@ -624,7 +624,7 @@ QVariant LayoutTreeModel::data(const QModelIndex &index, int role) const { static QIcon mapOpenedIcon = QIcon(QStringLiteral(":/icons/map_opened.ico")); QStandardItem *item = this->getItem(index)->child(row, col); - QString type = item->data(MapListRoles::TypeRole).toString(); + QString type = item->data(MapListUserRoles::TypeRole).toString(); if (type == "map_layout") { QString layoutId = item->data(Qt::UserRole).toString(); diff --git a/src/ui/montabwidget.cpp b/src/ui/montabwidget.cpp index e31739bd..7b196c75 100644 --- a/src/ui/montabwidget.cpp +++ b/src/ui/montabwidget.cpp @@ -62,7 +62,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) { @@ -82,21 +82,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) { @@ -124,6 +122,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)); @@ -152,6 +151,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/newmapconnectiondialog.cpp b/src/ui/newmapconnectiondialog.cpp new file mode 100644 index 00000000..af06789f --- /dev/null +++ b/src/ui/newmapconnectiondialog.cpp @@ -0,0 +1,72 @@ +#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); + + ui->comboBox_Direction->setEditable(false); + ui->comboBox_Direction->addItems(MapConnection::cardinalDirections); + + ui->comboBox_Map->addItems(mapNames); + ui->comboBox_Map->setInsertPolicy(QComboBox::NoInsert); + + // Choose default direction + QMap directionCounts; + for (auto connection : map->getConnections()) { + 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); + + connect(ui->comboBox_Map, &QComboBox::currentTextChanged, [this] { + if (ui->label_Warning->isVisible() && mapNameIsValid()) + setWarningVisible(false); + }); + setWarningVisible(false); +} + +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() { + if (!mapNameIsValid()) { + setWarningVisible(true); + return; + } + emit accepted(new MapConnection(ui->comboBox_Map->currentText(), ui->comboBox_Direction->currentText())); + QDialog::accept(); +} diff --git a/src/ui/newmappopup.cpp b/src/ui/newmappopup.cpp index df3a7044..17c1ed29 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; @@ -52,7 +53,7 @@ void NewMapPopup::initUi() { 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); @@ -60,13 +61,13 @@ void NewMapPopup::initUi() { 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); @@ -314,7 +315,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 { @@ -338,12 +339,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/noscrollcombobox.cpp b/src/ui/noscrollcombobox.cpp index ee778df5..3db2a956 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) @@ -22,13 +23,31 @@ 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. - 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) { @@ -61,3 +80,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); +} 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 c2299cdd..10642178 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); @@ -339,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()); @@ -346,7 +348,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..d84ef56b 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) @@ -133,63 +133,50 @@ 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 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 +411,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 +422,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 +471,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 +485,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 +537,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 +552,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 +594,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 cb41d85b..01b3aa07 100644 --- a/src/ui/regionmapeditor.cpp +++ b/src/ui/regionmapeditor.cpp @@ -26,8 +26,11 @@ 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->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 +113,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 +141,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; } @@ -203,18 +202,18 @@ void buildFireredDefaults(poryjson::Json &json) { poryjson::Json RegionMapEditor::buildDefaultJson() { poryjson::Json defaultJson; - switch (projectConfig.getBaseGameVersion()) { + switch (projectConfig.baseGameVersion) { case BaseGameVersion::pokeemerald: buildEmeraldDefaults(defaultJson); break; - case BaseGameVersion::pokeruby: buildRubyDefaults(defaultJson); break; - case BaseGameVersion::pokefirered: buildFireredDefaults(defaultJson); break; + default: + break; } return defaultJson; @@ -298,7 +297,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,12 +309,13 @@ 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"); 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,8 +344,8 @@ bool RegionMapEditor::buildConfigDialog() { // for sake of convenience, option to just use defaults for each basegame version - QPushButton *config_useProjectDefault; - switch (projectConfig.getBaseGameVersion()) { + QPushButton *config_useProjectDefault = nullptr; + switch (projectConfig.baseGameVersion) { case BaseGameVersion::pokefirered: config_useProjectDefault = new QPushButton("\nUse pokefirered defaults\n"); break; @@ -403,16 +403,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; } @@ -476,6 +466,7 @@ bool RegionMapEditor::setup() { if (!newMap->loadMapData(o)) { delete newMap; // TODO: consider continue, just reporting error loading single map? + this->setupError = true; return false; } @@ -498,26 +489,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) { @@ -533,14 +519,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(); @@ -582,10 +569,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); @@ -614,6 +600,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); @@ -624,15 +615,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() { 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 f265839a..3aec22a4 100644 --- a/src/ui/tileseteditor.cpp +++ b/src/ui/tileseteditor.cpp @@ -20,6 +20,7 @@ TilesetEditor::TilesetEditor(Project *project, Layout *layout, QWidget *parent) layout(layout), hasUnsavedChanges(false) { + this->setAttribute(Qt::WA_DeleteOnClose); this->setTilesets(this->layout->tileset_primary_label, this->layout->tileset_secondary_label); this->initUi(); } @@ -113,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); } @@ -124,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); @@ -137,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); @@ -149,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 @@ -187,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; @@ -197,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() { @@ -212,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; @@ -238,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() { @@ -1069,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() { @@ -1187,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(); } @@ -1195,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; @@ -1212,7 +1213,7 @@ void TilesetEditor::redrawMetatileSelector() { } void TilesetEditor::on_horizontalSlider_TilesZoom_valueChanged(int value) { - porymapConfig.setTilesetEditorTilesZoom(value); + porymapConfig.tilesetEditorTilesZoom = value; this->redrawTileSelector(); } @@ -1220,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) { diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp new file mode 100644 index 00000000..4594e2bd --- /dev/null +++ b/src/ui/wildmonchart.cpp @@ -0,0 +1,452 @@ +#if __has_include() +#include "wildmonchart.h" +#include "ui_wildmonchart.h" +#include "config.h" + +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) +{ + ui->setupUi(this); + 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::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) + 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(); + } + + restoreGeometry(porymapConfig.wildMonChartGeometry); + + setTable(table); +}; + +WildMonChart::~WildMonChart() { + delete ui; +}; + +void WildMonChart::setTable(const EncounterTableModel *table) { + if (this->table == table) + return; + this->table = table; + refresh(); +} + +void WildMonChart::clearTable() { + setTable(nullptr); +} + +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(); + ui->comboBox_Group->setEnabled(false); +} + +// Extract all the data from the table that we need for the charts +void WildMonChart::readTable() { + clearTableData(); + if (!this->table) + return; + + 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 table indexes 2-4 belong to "good_rod" + for (auto groupPair : this->table->encounterField().groups) { + // 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); + } + // 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(); + const QVector tablePokemon = this->table->encounterData().wildPokemon; + const int numRows = qMin(tableFrequencies.length(), tablePokemon.length()); + const QString speciesPrefix = projectConfig.getIdentifier(ProjectIdentifier::define_species_prefix); + 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); + + // Create species label (strip 'SPECIES_' prefix). + QString label = pokemon.species; + if (label.startsWith(speciesPrefix)) + label.remove(0, speciesPrefix.length()); + + // 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; + + // 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 + const QSignalBlocker blocker1(ui->comboBox_Species); + const QSignalBlocker blocker2(ui->comboBox_Group); + ui->comboBox_Species->clear(); + ui->comboBox_Species->addItems(getSpeciesNamesAlphabetical()); + ui->comboBox_Group->clear(); + ui->comboBox_Group->addItems(this->groupNames); + ui->comboBox_Group->setEnabled(usesGroupLabels()); +} + +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(); +} + +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 { + 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; +} + +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; +} + +QChart* WildMonChart::createSpeciesDistributionChart() { + QList barSets; + for (const auto species : getSpeciesNamesAlphabetical()) { + // Add encounter chance data + auto set = new QBarSet(species); + for (auto groupName : this->groupNamesReversed) + 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; + } + } + + // 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] (bool on, int i) { + QString text = on ? QString("%1 (%2%)").arg(set->label()).arg(set->at(i)) : ""; + QToolTip::showText(QCursor::pos(), text); + }); + } + + // 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(); + series->append(barSets); + + // 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); + 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(); + 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 (usesGroupLabels()) { + auto axisY = new QBarCategoryAxis(); + axisY->setCategories(this->groupNamesReversed); + chart->addAxis(axisY, Qt::AlignLeft); + series->attachAxis(axisY); + } + + return chart; +} + +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++) { + 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. + connect(set, &QBarSet::hovered, [=] (bool on, int i) { + QString text = on ? QString("%1 Lv%2 (%3%)") + .arg(individual ? "" : set->label()) + .arg(QString::number(i + levelRange.min)) + .arg(set->at(i)) + : ""; + 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); + refreshLevelDistributionChart(); + }); + } + + return set; +} + +QChart* WildMonChart::createLevelDistributionChart() { + const QString groupName = ui->comboBox_Group->currentText(); + + LevelRange levelRange; + 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)); + 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)); + 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->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 + 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; + }; + axisY->setMax(roundUp(qCeil(axisY->max()), 5)); + + return chart; +} + +QChart::ChartTheme WildMonChart::currentTheme() const { + return static_cast(ui->comboBox_Theme->currentData().toInt()); +} + +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 + // 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; + chart->setTheme(theme); + 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))->barSets()); + } +} + +void WildMonChart::saveSpeciesColors(const QList &barSets) { + this->speciesToColor.clear(); + 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())); +} + +// 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 \"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); + msgBox.exec(); +} + +void WildMonChart::closeEvent(QCloseEvent *event) { + porymapConfig.wildMonChartGeometry = saveGeometry(); + QWidget::closeEvent(event); +} + +#endif // __has_include()