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

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

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

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

" + "Individual Mode on the Level Distribution tab toggles whether data is shown for all species in the table. " + "The percentages will update to reflect whether you're showing all species or just that individual species. " + "In other words, while Individual Mode is checked the chart is answering the question \"If a species x " + "is encountered, what is the likelihood that it will be level y\", and while Individual Mode is not checked, " + "it answers the question \"For a single encounter, what is the likelihood of encountering a species x at level y.\""; QMessageBox msgBox(QMessageBox::Information, "porymap", text, QMessageBox::Close, this); msgBox.setTextFormat(Qt::RichText); msgBox.setInformativeText(informative); From febb82e0e92d0c40cb5ca0fe796b706a2fe46614 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 27 Aug 2024 19:09:00 -0400 Subject: [PATCH 16/18] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db886e709..d17d43883 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d ### Added - Redesigned the Connections tab, adding a number of new features including the option to open or display diving maps and a list UI for easier edit access. - Add a `Close Project` option +- Add charts to the `Wild Pokémon` tab that show species and level distributions. - An alert will be displayed when attempting to open a seemingly invalid project. ### Changed @@ -17,6 +18,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - Changes to the "Mirror to Connecting Maps" setting will now be saved between sessions. - A notice will be displayed when attempting to open the "Dynamic" map, rather than nothing happening. - The base game version is now auto-detected if the project name contains only one of "emerald", "firered/leafgreen", or "ruby/sapphire". +- The max encounter rate is now read from the project, rather than assuming the default value from RSE. - It's now possible to cancel quitting if there are unsaved changes in sub-windows. ### Fixed @@ -30,6 +32,8 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - Fix `About porymap` opening a new window each time it's activated. - Fix the `Edit History` window not raising to the front when reactivated. - New maps are now always inserted in map dropdowns at the correct position, rather than at the bottom of the list until the project is reloaded. +- Fix invalid species names clearing from wild pokémon data when revisited. +- Fix editing wild pokémon data not marking the map as edited. - Fix changes to map connections not marking connected maps as unsaved. - Fix numerous issues related to connecting a map to itself. - Fix incorrect map connections getting selected when opening a map by double-clicking a map connection. From c347fb174c0c936cc3c6a45133b4174f1afc0bfc Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 27 Aug 2024 20:17:22 -0400 Subject: [PATCH 17/18] Include qtcharts module for GitHub actions --- .github/workflows/main.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9c997ad17..f71ab5c53 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -33,7 +33,7 @@ jobs: uses: jurplel/install-qt-action@v2 with: version: '5.14.2' - modules: 'qtwidgets qtqml' + modules: 'qtwidgets qtqml qtcharts' cached: ${{ steps.cache-qt.outputs.cache-hit }} - name: Configure @@ -58,7 +58,8 @@ jobs: - name: Install Qt uses: jurplel/install-qt-action@v3 with: - version: '6.5.*' + version: '6.7.*' + modules: 'qtcharts' cached: ${{ steps.cache-qt.outputs.cache-hit }} - name: Configure From 8240cf55ef425e2d7b2e49f7c24e8f0ebbb8f60c Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 27 Aug 2024 21:39:33 -0400 Subject: [PATCH 18/18] Exclude charts features for our Windows builds --- include/ui/wildmonchart.h | 23 +++++++++++++++++++++++ porymap.pro | 6 +++++- src/mainwindow.cpp | 4 ++++ src/ui/wildmonchart.cpp | 3 +++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/include/ui/wildmonchart.h b/include/ui/wildmonchart.h index a7c56a479..d4e3d83f0 100644 --- a/include/ui/wildmonchart.h +++ b/include/ui/wildmonchart.h @@ -4,6 +4,8 @@ #include "encountertablemodel.h" #include + +#if __has_include() #include namespace Ui { @@ -72,4 +74,25 @@ public slots: 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 a7923595d..f36f536f0 100644 --- a/porymap.pro +++ b/porymap.pro @@ -4,7 +4,11 @@ # #------------------------------------------------- -QT += core gui qml network charts +QT += core gui qml network + +!win32 { + QT += charts +} greaterThan(QT_MAJOR_VERSION, 4): QT += widgets diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index f342396fa..00251e6cd 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -124,6 +124,10 @@ void MainWindow::initWindow() { ui->actionCheck_for_Updates->setVisible(false); #endif +#ifdef DISABLE_CHARTS_MODULE + ui->pushButton_SummaryChart->setVisible(false); +#endif + setWindowDisabled(true); } diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp index 011dc326a..4594e2bd7 100644 --- a/src/ui/wildmonchart.cpp +++ b/src/ui/wildmonchart.cpp @@ -1,3 +1,4 @@ +#if __has_include() #include "wildmonchart.h" #include "ui_wildmonchart.h" #include "config.h" @@ -447,3 +448,5 @@ void WildMonChart::closeEvent(QCloseEvent *event) { porymapConfig.wildMonChartGeometry = saveGeometry(); QWidget::closeEvent(event); } + +#endif // __has_include()