diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9c997ad1..f71ab5c5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -33,7 +33,7 @@ jobs: uses: jurplel/install-qt-action@v2 with: version: '5.14.2' - modules: 'qtwidgets qtqml' + modules: 'qtwidgets qtqml qtcharts' cached: ${{ steps.cache-qt.outputs.cache-hit }} - name: Configure @@ -58,7 +58,8 @@ jobs: - name: Install Qt uses: jurplel/install-qt-action@v3 with: - version: '6.5.*' + version: '6.7.*' + modules: 'qtcharts' cached: ${{ steps.cache-qt.outputs.cache-hit }} - name: Configure diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d7d9f02..fef37e88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d ### Added - Redesigned the Connections tab, adding a number of new features including the option to open or display diving maps and a list UI for easier edit access. - Add a `Close Project` option +- Add charts to the `Wild Pokémon` tab that show species and level distributions. - An alert will be displayed when attempting to open a seemingly invalid project. - Add support for defining project values with `enum` where `#define` was expected. @@ -18,6 +19,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - Changes to the "Mirror to Connecting Maps" setting will now be saved between sessions. - A notice will be displayed when attempting to open the "Dynamic" map, rather than nothing happening. - The base game version is now auto-detected if the project name contains only one of "emerald", "firered/leafgreen", or "ruby/sapphire". +- The max encounter rate is now read from the project, rather than assuming the default value from RSE. - It's now possible to cancel quitting if there are unsaved changes in sub-windows. ### Fixed @@ -31,6 +33,8 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - Fix `About porymap` opening a new window each time it's activated. - Fix the `Edit History` window not raising to the front when reactivated. - New maps are now always inserted in map dropdowns at the correct position, rather than at the bottom of the list until the project is reloaded. +- Fix invalid species names clearing from wild pokémon data when revisited. +- Fix editing wild pokémon data not marking the map as edited. - Fix changes to map connections not marking connected maps as unsaved. - Fix numerous issues related to connecting a map to itself. - Fix incorrect map connections getting selected when opening a map by double-clicking a map connection. diff --git a/docsrc/manual/project-files.rst b/docsrc/manual/project-files.rst index 20849ba3..b99e74d7 100644 --- a/docsrc/manual/project-files.rst +++ b/docsrc/manual/project-files.rst @@ -67,6 +67,7 @@ The filepath that Porymap expects for each file can be overridden on the ``Files include/fieldmap.h, yes, no, ``constants_fieldmap``, reads tileset related constants src/fieldmap.c, yes, no, ``fieldmap``, reads ``symbol_attribute_table`` src/event_object_movement.c, yes, no, ``initial_facing_table``, reads ``symbol_facing_directions`` + src/wild_encounter.c, yes, no, ``wild_encounter``, reads ``define_max_encounter_rate`` src/pokemon_icon.c, yes, no, ``pokemon_icon_table``, reads files in ``symbol_pokemon_icon_table`` graphics/pokemon/\*/icon.png, yes, no, ``pokemon_gfx``, to search for Pokémon icons if they aren't found in ``symbol_pokemon_icon_table`` @@ -96,6 +97,7 @@ In addition to these files, there are some specific symbol and macro names that ``define_obj_event_count``, ``OBJECT_EVENT_TEMPLATES_COUNT``, to limit total Object Events ``define_min_level``, ``MIN_LEVEL``, minimum wild encounters level ``define_max_level``, ``MAX_LEVEL``, maximum wild encounters level + ``define_max_encounter_rate``, ``MAX_ENCOUNTER_RATE``, this value / 16 will be the maximum encounter rate on the ``Wild Pokémon`` tab ``define_tiles_primary``, ``NUM_TILES_IN_PRIMARY``, ``define_tiles_total``, ``NUM_TILES_TOTAL``, ``define_metatiles_primary``, ``NUM_METATILES_IN_PRIMARY``, total metatiles are calculated using metatile ID mask @@ -120,6 +122,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 +137,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/mainwindow.ui b/forms/mainwindow.ui index 8c9059bb..f7253615 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -3004,6 +3004,13 @@ + + + + Summary Chart... + + + 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 7486919f..a2fead2e 100644 --- a/include/config.h +++ b/include/config.h @@ -77,6 +77,7 @@ public: this->monitorFiles = true; this->tilesetCheckerboardFill = true; this->theme = "default"; + this->wildMonChartTheme = ""; this->textEditorOpenFolder = ""; this->textEditorGotoLine = ""; this->paletteEditorBitDepth = 24; @@ -127,6 +128,7 @@ public: bool monitorFiles; bool tilesetCheckerboardFill; QString theme; + QString wildMonChartTheme; QString textEditorOpenFolder; QString textEditorGotoLine; int paletteEditorBitDepth; @@ -136,6 +138,7 @@ public: QDateTime lastUpdateCheckTime; QVersionNumber lastUpdateCheckVersion; QMap rateLimitTimes; + QByteArray wildMonChartGeometry; protected: virtual QString getConfigFilepath() override; @@ -191,6 +194,7 @@ enum ProjectIdentifier { define_obj_event_count, define_min_level, define_max_level, + define_max_encounter_rate, define_tiles_primary, define_tiles_total, define_metatiles_primary, @@ -215,6 +219,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, @@ -229,7 +234,6 @@ enum ProjectIdentifier { regex_sign_facing_directions, regex_trainer_types, regex_music, - regex_species, }; enum ProjectFilePath { @@ -278,6 +282,7 @@ enum ProjectFilePath { global_fieldmap, fieldmap, initial_facing_table, + wild_encounter, pokemon_icon_table, pokemon_gfx, }; 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/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 89b6ccb5..7ef5ba10 100644 --- a/include/editor.h +++ b/include/editor.h @@ -26,6 +26,7 @@ #include "movablerect.h" #include "cursortilerect.h" #include "mapruler.h" +#include "encountertablemodel.h" class DraggablePixmapItem; class MetatilesPixmapItem; @@ -84,6 +85,7 @@ public: void removeSelectedConnection(); void addNewWildMonGroup(QWidget *window); void deleteWildMonGroup(); + EncounterTableModel* getCurrentWildMonTable(); void updateDiveMap(QString mapName); void updateEmergeMap(QString mapName); void setSelectedConnection(MapConnection *connection); @@ -95,8 +97,7 @@ public: Tileset *getCurrentMapPrimaryTileset(); DraggablePixmapItem *addMapEvent(Event *event); - void selectMapEvent(DraggablePixmapItem *object); - void selectMapEvent(DraggablePixmapItem *object, bool toggle); + void selectMapEvent(DraggablePixmapItem *object, bool toggle = false); DraggablePixmapItem *addNewEvent(Event::Type type); void updateSelectedEvents(); void duplicateSelectedEvents(); @@ -223,7 +224,9 @@ private slots: signals: void objectsChanged(); void openConnectedMap(MapConnection*); - void wildMonDataChanged(); + void wildMonTableOpened(EncounterTableModel*); + void wildMonTableClosed(); + void wildMonTableEdited(); void warpEventDoubleClicked(QString, int, Event::Group); void currentMetatilesSelectionChanged(); void mapRulerStatusChanged(const QString &); diff --git a/include/mainwindow.h b/include/mainwindow.h index 01823945..c2726efc 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -27,6 +27,7 @@ #include "preferenceeditor.h" #include "projectsettingseditor.h" #include "customscriptseditor.h" +#include "wildmonchart.h" #include "updatepromoter.h" #include "aboutporymap.h" @@ -182,7 +183,6 @@ private slots: void onOpenConnectedMap(MapConnection*); void onMapNeedsRedrawing(); void onTilesetsSaved(QString, QString); - void onWildMonDataChanged(); void openNewMapPopupWindow(); void onNewMapCreated(); void onMapCacheCleared(); @@ -290,6 +290,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,6 +320,7 @@ private: QPointer updatePromoter = nullptr; QPointer networkAccessManager = nullptr; QPointer aboutWindow = nullptr; + QPointer wildMonChart = nullptr; FilterChildrenProxyModel *mapListProxyModel; QStandardItemModel *mapListModel; QList *mapGroupItemsList; diff --git a/include/project.h b/include/project.h index 8c08e508..c3fc1677 100644 --- a/include/project.h +++ b/include/project.h @@ -39,7 +39,6 @@ public: QMap mapGroups; QList groupedMapNames; QStringList mapNames; - QMap miscConstants; QList healLocations; QMap healLocationNameToValue; QMap mapConstantsToMapNames; @@ -79,6 +78,9 @@ public: bool usingAsmTilesets; QString importExportPath; QSet disabledSettingsNames; + int pokemonMinLevel; + int pokemonMaxLevel; + int maxEncounterRate; bool wildEncountersLoaded; void set_root(QString); diff --git a/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/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/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 744edaef..f36f536f 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 @@ -110,7 +114,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 \ @@ -209,7 +214,8 @@ HEADERS += include/core/block.h \ include/settings.h \ include/log.h \ include/ui/uintspinbox.h \ - include/ui/updatepromoter.h + include/ui/updatepromoter.h \ + include/ui/wildmonchart.h FORMS += forms/mainwindow.ui \ forms/connectionslistitem.ui \ @@ -230,7 +236,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/images.qrc b/resources/images.qrc index bdac1a47..73ed0620 100644 --- a/resources/images.qrc +++ b/resources/images.qrc @@ -16,6 +16,7 @@ icons/folder_map_opened.ico icons/folder_map.ico icons/folder.ico + icons/help.ico icons/map_edited.ico icons/map_opened.ico icons/map.ico diff --git a/src/config.cpp b/src/config.cpp index 818dd714..20869171 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -85,6 +85,7 @@ const QMap> ProjectConfig::defaultIde {ProjectIdentifier::define_obj_event_count, {"define_obj_event_count", "OBJECT_EVENT_TEMPLATES_COUNT"}}, {ProjectIdentifier::define_min_level, {"define_min_level", "MIN_LEVEL"}}, {ProjectIdentifier::define_max_level, {"define_max_level", "MAX_LEVEL"}}, + {ProjectIdentifier::define_max_encounter_rate, {"define_max_encounter_rate", "MAX_ENCOUNTER_RATE"}}, {ProjectIdentifier::define_tiles_primary, {"define_tiles_primary", "NUM_TILES_IN_PRIMARY"}}, {ProjectIdentifier::define_tiles_total, {"define_tiles_total", "NUM_TILES_TOTAL"}}, {ProjectIdentifier::define_metatiles_primary, {"define_metatiles_primary", "NUM_METATILES_IN_PRIMARY"}}, @@ -109,6 +110,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 +126,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 +175,7 @@ const QMap> ProjectConfig::defaultPaths {ProjectFilePath::fieldmap, { "fieldmap", "src/fieldmap.c"}}, {ProjectFilePath::pokemon_icon_table, { "pokemon_icon_table", "src/pokemon_icon.c"}}, {ProjectFilePath::initial_facing_table, { "initial_facing_table", "src/event_object_movement.c"}}, + {ProjectFilePath::wild_encounter, { "wild_encounter", "src/wild_encounter.c"}}, {ProjectFilePath::pokemon_gfx, { "pokemon_gfx", "graphics/pokemon/"}}, }; @@ -364,6 +366,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") { @@ -390,6 +394,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") { @@ -449,6 +455,7 @@ QMap PorymapConfig::getKeyValueMap() { map.insert("project_settings_editor_state", stringFromByteArray(this->projectSettingsEditorState)); map.insert("custom_scripts_editor_geometry", stringFromByteArray(this->customScriptsEditorGeometry)); map.insert("custom_scripts_editor_state", stringFromByteArray(this->customScriptsEditorState)); + map.insert("wild_mon_chart_geometry", stringFromByteArray(this->wildMonChartGeometry)); map.insert("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)); @@ -468,6 +475,7 @@ QMap PorymapConfig::getKeyValueMap() { map.insert("monitor_files", this->monitorFiles ? "1" : "0"); map.insert("tileset_checkerboard_fill", this->tilesetCheckerboardFill ? "1" : "0"); map.insert("theme", this->theme); + map.insert("wild_mon_chart_theme", this->wildMonChartTheme); map.insert("text_editor_open_directory", this->textEditorOpenFolder); map.insert("text_editor_goto_line", this->textEditorGotoLine); map.insert("palette_editor_bit_depth", QString::number(this->paletteEditorBitDepth)); diff --git a/src/core/editcommands.cpp b/src/core/editcommands.cpp index 640cb43f..f557dddd 100644 --- a/src/core/editcommands.cpp +++ b/src/core/editcommands.cpp @@ -331,7 +331,7 @@ void EventCreate::redo() { // select this event editor->selected_events->clear(); - editor->selectMapEvent(event->getPixmapItem(), false); + editor->selectMapEvent(event->getPixmapItem()); } void EventCreate::undo() { diff --git a/src/core/events.cpp b/src/core/events.cpp index b0ea3e39..13be4153 100644 --- a/src/core/events.cpp +++ b/src/core/events.cpp @@ -6,6 +6,21 @@ QMap Event::icons; +Event* Event::create(Event::Type type) { + switch (type) { + case Event::Type::Object: return new ObjectEvent(); + case Event::Type::CloneObject: return new CloneObjectEvent(); + case Event::Type::Warp: return new WarpEvent(); + case Event::Type::Trigger: return new TriggerEvent(); + case Event::Type::WeatherTrigger: return new WeatherTriggerEvent(); + case Event::Type::Sign: return new SignEvent(); + case Event::Type::HiddenItem: return new HiddenItemEvent(); + case Event::Type::SecretBase: return new SecretBaseEvent(); + case Event::Type::HealLocation: return new HealLocationEvent(); + default: return nullptr; + } +} + Event::~Event() { if (this->eventFrame) this->eventFrame->deleteLater(); diff --git a/src/editor.cpp b/src/editor.cpp index 22957790..69b2ffb9 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -7,7 +7,6 @@ #include "mapsceneeventfilter.h" #include "metatile.h" #include "montabwidget.h" -#include "encountertablemodel.h" #include "editcommands.h" #include "config.h" #include "scripting.h" @@ -43,6 +42,11 @@ Editor::Editor(Ui::MainWindow* ui) selectNewEvents = false; } }); + + // Send signals used for updating the wild pokemon summary chart + connect(ui->stackedWidget_WildMons, &QStackedWidget::currentChanged, [this] { + emit wildMonTableOpened(getCurrentWildMonTable()); + }); } Editor::~Editor() @@ -79,9 +83,7 @@ void Editor::saveUiFields() { } void Editor::setProject(Project * project) { - if (this->project) { - closeProject(); - } + closeProject(); this->project = project; MapConnection::project = project; } @@ -194,6 +196,7 @@ void Editor::setEditingConnections() { void Editor::clearWildMonTables() { QStackedWidget *stack = ui->stackedWidget_WildMons; + const QSignalBlocker blocker(stack); // delete widgets from previous map data if they exist while (stack->count()) { @@ -203,6 +206,7 @@ void Editor::clearWildMonTables() { } ui->comboBox_EncounterGroupLabel->clear(); + emit wildMonTableClosed(); } void Editor::displayWildMonTables() { @@ -243,8 +247,12 @@ void Editor::displayWildMonTables() { } tabIndex++; } + connect(tabWidget, &MonTabWidget::currentChanged, [this] { + emit wildMonTableOpened(getCurrentWildMonTable()); + }); } stack->setCurrentIndex(0); + emit wildMonTableOpened(getCurrentWildMonTable()); } void Editor::addNewWildMonGroup(QWidget *window) { @@ -359,7 +367,7 @@ void Editor::addNewWildMonGroup(QWidget *window) { tabIndex++; } saveEncounterTabData(); - emit wildMonDataChanged(); + emit wildMonTableEdited(); } } @@ -397,7 +405,8 @@ void Editor::deleteWildMonGroup() { project->encounterGroupLabels.remove(i); displayWildMonTables(); - emit wildMonDataChanged(); + saveEncounterTabData(); + emit wildMonTableEdited(); } } @@ -650,7 +659,8 @@ void Editor::configureEncounterJSON(QWidget *window) { // Re-draw the tab accordingly. displayWildMonTables(); - emit wildMonDataChanged(); + saveEncounterTabData(); + emit wildMonTableEdited(); } } @@ -684,6 +694,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. @@ -1358,7 +1378,7 @@ void Editor::mouseEvent_map(QGraphicsSceneMouseEvent *event, MapPixmapItem *item if (newEvent) { newEvent->move(pos.x(), pos.y()); emit objectsChanged(); - selectMapEvent(newEvent, false); + selectMapEvent(newEvent); } } } @@ -1994,10 +2014,6 @@ void Editor::updateSelectedEvents() { emit objectsChanged(); } -void Editor::selectMapEvent(DraggablePixmapItem *object) { - selectMapEvent(object, false); -} - void Editor::selectMapEvent(DraggablePixmapItem *object, bool toggle) { if (!selected_events || !object) return; @@ -2007,10 +2023,10 @@ void Editor::selectMapEvent(DraggablePixmapItem *object, bool toggle) { selected_events->clear(); selected_events->append(object); } else if (!selected_events->contains(object)) { - // Adding event to selection + // Adding event to group selection selected_events->append(object); } else if (selected_events->length() > 1) { - // Removing from group selection + // Removing event from group selection selected_events->removeOne(object); } else { // Attempting to toggle the only currently-selected event. @@ -2072,56 +2088,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 @@ -2228,10 +2212,8 @@ void Editor::objectsView_onMousePress(QMouseEvent *event) { bool multiSelect = event->modifiers() & Qt::ControlModifier; if (!selectingEvent && !multiSelect && selected_events->length() > 1) { - DraggablePixmapItem *first = selected_events->first(); - selected_events->clear(); - selected_events->append(first); - updateSelectedEvents(); + // User is clearing group selection by clicking on the background + this->selectMapEvent(selected_events->first()); } selectingEvent = false; } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 23b7eb06..ecac1703 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -124,6 +124,10 @@ void MainWindow::initWindow() { ui->actionCheck_for_Updates->setVisible(false); #endif +#ifdef DISABLE_CHARTS_MODULE + ui->pushButton_SummaryChart->setVisible(false); +#endif + setWindowDisabled(true); } @@ -239,10 +243,8 @@ void MainWindow::initExtraSignals() { connect(ui->tabWidget_EventType, &QTabWidget::currentChanged, this, &MainWindow::eventTabChanged); // Change pages on wild encounter groups - QStackedWidget *stack = ui->stackedWidget_WildMons; - QComboBox *labelCombo = ui->comboBox_EncounterGroupLabel; - connect(labelCombo, QOverload::of(&QComboBox::currentIndexChanged), [=](int index){ - stack->setCurrentIndex(index); + connect(ui->comboBox_EncounterGroupLabel, QOverload::of(&QComboBox::currentIndexChanged), [this](int index){ + ui->stackedWidget_WildMons->setCurrentIndex(index); }); // Convert the layout of the map tools' frame into an adjustable FlowLayout @@ -309,7 +311,7 @@ void MainWindow::initEditor() { connect(this->editor, &Editor::openConnectedMap, this, &MainWindow::onOpenConnectedMap); connect(this->editor, &Editor::warpEventDoubleClicked, this, &MainWindow::openWarpMap); connect(this->editor, &Editor::currentMetatilesSelectionChanged, this, &MainWindow::currentMetatilesSelectionChanged); - connect(this->editor, &Editor::wildMonDataChanged, this, &MainWindow::onWildMonDataChanged); + connect(this->editor, &Editor::wildMonTableEdited, [this] { this->markMapEdited(); }); connect(this->editor, &Editor::mapRulerStatusChanged, this, &MainWindow::onMapRulerStatusChanged); connect(this->editor, &Editor::tilesetUpdated, this, &Scripting::cb_TilesetUpdated); connect(ui->toolButton_Open_Scripts, &QToolButton::pressed, this->editor, &Editor::openMapScripts); @@ -1113,6 +1115,7 @@ void MainWindow::clearProjectUI() { // Clear map list mapListModel->clear(); + mapListIndexes.clear(); mapGroupItemsList->clear(); } @@ -1129,6 +1132,7 @@ void MainWindow::sortMapList() { ui->mapList->setUpdatesEnabled(false); mapListModel->clear(); + mapListIndexes.clear(); mapGroupItemsList->clear(); QStandardItem *root = mapListModel->invisibleRootItem(); @@ -1761,7 +1765,7 @@ void MainWindow::paste() { editor->metatile_selector_item->setExternalSelection(width, height, metatiles, collisions); break; } - case 1: + case MainTab::Events: { // can only paste events to this tab if (pasteObject["object"].toString() != "events") { @@ -1773,56 +1777,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()) { @@ -2014,7 +1987,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"); @@ -2562,11 +2535,6 @@ void MainWindow::onTilesetsSaved(QString primaryTilesetLabel, QString secondaryT redrawMapScene(); } -void MainWindow::onWildMonDataChanged() { - editor->saveEncounterTabData(); - markMapEdited(); -} - void MainWindow::onMapRulerStatusChanged(const QString &status) { if (status.isEmpty()) { label_MapRulerStatus->hide(); @@ -2659,6 +2627,16 @@ 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); } @@ -3115,6 +3093,10 @@ bool MainWindow::closeSupplementaryWindows() { return false; this->customScriptsEditor = nullptr; + if (this->wildMonChart && !this->wildMonChart->close()) + return false; + this->wildMonChart = nullptr; + if (this->projectSettingsEditor) this->projectSettingsEditor->closeQuietly(); this->projectSettingsEditor = nullptr; diff --git a/src/project.cpp b/src/project.cpp index 8c1dbba5..bfeaab38 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -1654,15 +1654,43 @@ void Project::deleteFile(QString path) { } bool Project::readWildMonData() { - extraEncounterGroups.clear(); - wildMonFields.clear(); - wildMonData.clear(); - encounterGroupLabels.clear(); + this->extraEncounterGroups.clear(); + this->wildMonFields.clear(); + this->wildMonData.clear(); + this->encounterGroupLabels.clear(); + this->pokemonMinLevel = 0; + this->pokemonMaxLevel = 100; + this->maxEncounterRate = 2880/16; this->wildEncountersLoaded = false; if (!userConfig.useEncounterJson) { return true; } + // Read max encounter rate. The games multiply the encounter rate value in the map data by 16, so our input limit is the max/16. + const QString encounterRateFile = projectConfig.getFilePath(ProjectFilePath::wild_encounter); + const QString maxEncounterRateName = projectConfig.getIdentifier(ProjectIdentifier::define_max_encounter_rate); + + fileWatcher.addPath(QString("%1/%2").arg(root).arg(encounterRateFile)); + auto defines = parser.readCDefinesByName(encounterRateFile, {maxEncounterRateName}); + if (defines.contains(maxEncounterRateName)) + this->maxEncounterRate = defines.value(maxEncounterRateName)/16; + + // Read min/max level + const QString levelRangeFile = projectConfig.getFilePath(ProjectFilePath::constants_pokemon); + const QString minLevelName = projectConfig.getIdentifier(ProjectIdentifier::define_min_level); + const QString maxLevelName = projectConfig.getIdentifier(ProjectIdentifier::define_max_level); + + fileWatcher.addPath(QString("%1/%2").arg(root).arg(levelRangeFile)); + defines = parser.readCDefinesByName(levelRangeFile, {minLevelName, maxLevelName}); + if (defines.contains(minLevelName)) + this->pokemonMinLevel = defines.value(minLevelName); + if (defines.contains(maxLevelName)) + this->pokemonMaxLevel = defines.value(maxLevelName); + + this->pokemonMinLevel = qMin(this->pokemonMinLevel, this->pokemonMaxLevel); + this->pokemonMaxLevel = qMax(this->pokemonMinLevel, this->pokemonMaxLevel); + + // Read encounter data QString wildMonJsonFilepath = QString("%1/%2").arg(root).arg(projectConfig.getFilePath(ProjectFilePath::json_wild_encounters)); fileWatcher.addPath(wildMonJsonFilepath); @@ -2412,17 +2440,6 @@ bool Project::readObjEventGfxConstants() { } bool Project::readMiscellaneousConstants() { - miscConstants.clear(); - if (userConfig.useEncounterJson) { - const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_pokemon); - const QString minLevelName = projectConfig.getIdentifier(ProjectIdentifier::define_min_level); - const QString maxLevelName = projectConfig.getIdentifier(ProjectIdentifier::define_max_level); - fileWatcher.addPath(root + "/" + filename); - QMap pokemonDefines = parser.readCDefinesByName(filename, {minLevelName, maxLevelName}); - miscConstants.insert("max_level_define", pokemonDefines.value(maxLevelName) > pokemonDefines.value(minLevelName) ? pokemonDefines.value(maxLevelName) : 100); - miscConstants.insert("min_level_define", pokemonDefines.value(minLevelName) < pokemonDefines.value(maxLevelName) ? pokemonDefines.value(minLevelName) : 1); - } - const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_global); const QString maxObjectEventsName = projectConfig.getIdentifier(ProjectIdentifier::define_obj_event_count); fileWatcher.addPath(root + "/" + filename); @@ -2611,7 +2628,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 regexList = {projectConfig.getIdentifier(ProjectIdentifier::regex_species)}; + const QStringList regexList = {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, regexList); diff --git a/src/ui/customscriptseditor.cpp b/src/ui/customscriptseditor.cpp index 13650053..b7180b92 100644 --- a/src/ui/customscriptseditor.cpp +++ b/src/ui/customscriptseditor.cpp @@ -27,7 +27,7 @@ CustomScriptsEditor::CustomScriptsEditor(QWidget *parent) : connect(ui->button_CreateNewScript, &QAbstractButton::clicked, this, &CustomScriptsEditor::createNewScript); connect(ui->button_LoadScript, &QAbstractButton::clicked, this, &CustomScriptsEditor::loadScript); - connect(ui->button_RefreshScripts, &QAbstractButton::clicked, this, &CustomScriptsEditor::refreshScripts); + connect(ui->button_RefreshScripts, &QAbstractButton::clicked, this, &CustomScriptsEditor::userRefreshScripts); connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &CustomScriptsEditor::dialogButtonClicked); this->initShortcuts(); @@ -57,7 +57,7 @@ void CustomScriptsEditor::initShortcuts() { shortcut_load->setObjectName("shortcut_load"); shortcut_load->setWhatsThis("Load Script..."); - auto *shortcut_refresh = new Shortcut(QKeySequence(), this, SLOT(refreshScripts())); + auto *shortcut_refresh = new Shortcut(QKeySequence(), this, SLOT(userRefreshScripts())); shortcut_refresh->setObjectName("shortcut_refresh"); shortcut_refresh->setWhatsThis("Refresh Scripts"); @@ -238,14 +238,21 @@ void CustomScriptsEditor::openSelectedScripts() { this->openScript(item); } -void CustomScriptsEditor::refreshScripts() { +// When the user refreshes the scripts we show a little tooltip as feedback. +// We don't want this tooltip to display when we refresh programmatically, like when changes are saved. +void CustomScriptsEditor::userRefreshScripts() { + if (refreshScripts()) + QToolTip::showText(ui->button_RefreshScripts->mapToGlobal(QPoint(0, 0)), "Refreshed!"); +} + +bool CustomScriptsEditor::refreshScripts() { if (this->hasUnsavedChanges) { if (this->prompt("Scripts have been modified, save changes and reload the script engine?", QMessageBox::Yes) == QMessageBox::No) - return; + return false; this->save(); } - QToolTip::showText(ui->button_RefreshScripts->mapToGlobal(QPoint(0, 0)), "Refreshed!"); emit reloadScriptEngine(); + return true; } void CustomScriptsEditor::save() { @@ -264,6 +271,7 @@ void CustomScriptsEditor::save() { } userConfig.setCustomScripts(paths, enabledStates); + userConfig.save(); this->hasUnsavedChanges = false; this->refreshScripts(); } 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/montabwidget.cpp b/src/ui/montabwidget.cpp index 6845a02b..74c52dc5 100644 --- a/src/ui/montabwidget.cpp +++ b/src/ui/montabwidget.cpp @@ -68,7 +68,7 @@ void MonTabWidget::paste(int index) { WildMonInfo newInfo = getDefaultMonInfo(this->editor->project->wildMonFields.at(index)); combineEncounters(newInfo, encounterClipboard); populateTab(index, newInfo); - emit editor->wildMonDataChanged(); + emit editor->wildMonTableEdited(); } void MonTabWidget::actionCopyTab(int index) { @@ -88,21 +88,19 @@ void MonTabWidget::actionCopyTab(int index) { } void MonTabWidget::actionAddDeleteTab(int index) { + clearTableAt(index); if (activeTabs[index]) { // delete tab - clearTableAt(index); deactivateTab(index); editor->saveEncounterTabData(); - emit editor->wildMonDataChanged(); } else { // add tab - clearTableAt(index); populateTab(index, getDefaultMonInfo(editor->project->wildMonFields.at(index))); editor->saveEncounterTabData(); setCurrentIndex(index); - emit editor->wildMonDataChanged(); } + emit editor->wildMonTableEdited(); } void MonTabWidget::clearTableAt(int tabIndex) { @@ -130,6 +128,7 @@ void MonTabWidget::populateTab(int tabIndex, WildMonInfo monInfo) { EncounterTableModel *model = new EncounterTableModel(monInfo, editor->project->wildMonFields, tabIndex, this); connect(model, &EncounterTableModel::edited, editor, &Editor::saveEncounterTabData); + connect(model, &EncounterTableModel::edited, editor, &Editor::wildMonTableEdited); speciesTable->setModel(model); speciesTable->setItemDelegateForColumn(EncounterTableModel::ColumnType::Species, new SpeciesComboDelegate(editor->project, this)); @@ -158,6 +157,8 @@ void MonTabWidget::populateTab(int tabIndex, WildMonInfo monInfo) { } QTableView *MonTabWidget::tableAt(int tabIndex) { + if (tabIndex < 0) + return nullptr; return static_cast(this->widget(tabIndex)); } diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp 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()