diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d489b17..abdb4d9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - Primary/secondary metatile images are now kept on separate rows, rather than blending together if the primary size is not divisible by 8. - The prompt to reload the project when a file has changed will now only appear when Porymap is the active application. - `Script` dropdowns now autocomplete only with scripts from the current map, rather than every script in the project. The old behavior is available via a new setting. +- `Script` dropdowns now update automatically if the current map's scripts file is edited. - The options for `Encounter Type` and `Terrain Type` in the Tileset Editor are not hardcoded anymore, they're now read from the project. - The `symbol_wild_encounters` setting was replaced; this value is now read from the project. - The max encounter rate is now read from the project, rather than assuming the default value from RSE. diff --git a/forms/projectsettingseditor.ui b/forms/projectsettingseditor.ui index 05598b8f..c840d331 100644 --- a/forms/projectsettingseditor.ui +++ b/forms/projectsettingseditor.ui @@ -39,7 +39,7 @@ 0 0 570 - 692 + 680 @@ -94,7 +94,7 @@ ... - + :/icons/folder.ico:/icons/folder.ico @@ -164,7 +164,7 @@ ... - + :/icons/folder.ico:/icons/folder.ico @@ -248,7 +248,7 @@ ... - + :/icons/folder.ico:/icons/folder.ico @@ -1197,23 +1197,62 @@ 0 0 570 - 840 + 927 + + + + Tab Icon + + + + + + false + + + ... + + + + :/icons/folder.ico:/icons/folder.ico + + + + + + + false + + + <html><head/><body><p>The image file path to use for the icon of the Events tab.</p></body></html> + + + true + + + + + + + <html><head/><body><p>The icon that will be displayed for the Events tab in the editor. If 'Automatic' is chosen, the icon will be a random player character from the project's base game version. If 'Custom' is chosen an image file path may be specified.</p></body></html> + + + false + + + + + + Default Icons - - - - Triggers - - - @@ -1221,23 +1260,31 @@ - - + + - <html><head/><body><p>The icon that will be used to represent Warp events</p></body></html> + <html><head/><body><p>The icon that will be used to represent BG events</p></body></html> true - - - - <html><head/><body><p>The icon that will be used to represent Heal Location events</p></body></html> + + + + ... - - true + + + :/icons/folder.ico:/icons/folder.ico + + + + + + + Objects @@ -1248,6 +1295,24 @@ + + + + Triggers + + + + + + + ... + + + + :/icons/folder.ico:/icons/folder.ico + + + @@ -1265,10 +1330,56 @@ - - + + + + <html><head/><body><p>The icon that will be used to represent Warp events</p></body></html> + + + true + + + + + - Objects + ... + + + + :/icons/folder.ico:/icons/folder.ico + + + + + + + <html><head/><body><p>The icon that will be used to represent Heal Location events</p></body></html> + + + true + + + + + + + ... + + + + :/icons/folder.ico:/icons/folder.ico + + + + + + + ... + + + + :/icons/folder.ico:/icons/folder.ico @@ -1282,71 +1393,6 @@ - - - - <html><head/><body><p>The icon that will be used to represent BG events</p></body></html> - - - true - - - - - - - ... - - - - :/icons/folder.ico:/icons/folder.ico - - - - - - - ... - - - - :/icons/folder.ico:/icons/folder.ico - - - - - - - ... - - - - :/icons/folder.ico:/icons/folder.ico - - - - - - - ... - - - - :/icons/folder.ico:/icons/folder.ico - - - - - - - ... - - - - :/icons/folder.ico:/icons/folder.ico - - - @@ -1365,7 +1411,7 @@ ... - + :/icons/delete.ico:/icons/delete.ico @@ -1418,7 +1464,7 @@ ... - + :/icons/add.ico:/icons/add.ico @@ -1628,7 +1674,7 @@ 0 0 544 - 338 + 341 @@ -1657,7 +1703,7 @@ Add Global Constants File... - + :/icons/add.ico:/icons/add.ico @@ -1668,7 +1714,7 @@ ... - + :/icons/help.ico:/icons/help.ico @@ -1708,7 +1754,7 @@ ... - + :/icons/help.ico:/icons/help.ico @@ -1745,7 +1791,7 @@ 0 0 544 - 421 + 425 @@ -1774,7 +1820,7 @@ Add Global Constant... - + :/icons/add.ico:/icons/add.ico @@ -1840,8 +1886,6 @@
uintspinbox.h
- - - + diff --git a/include/config.h b/include/config.h index 26d2a353..2e99dc3e 100644 --- a/include/config.h +++ b/include/config.h @@ -335,6 +335,7 @@ public: this->filePaths.clear(); this->eventIconPaths.clear(); this->pokemonIconPaths.clear(); + this->eventsTabIconPath = QString(); this->collisionSheetPath = QString(); this->collisionSheetSize = QSize(2, 16); this->playerViewDistance = QMargins(GBA_H_DIST_TO_CENTER, GBA_V_DIST_TO_CENTER, GBA_H_DIST_TO_CENTER, GBA_V_DIST_TO_CENTER); @@ -345,6 +346,7 @@ public: this->unusedTileCovered = 0x0000; this->unusedTileSplit = 0x0000; this->maxEventsPerGroup = 255; + this->forcedMajorVersion = 0; this->globalConstantsFilepaths.clear(); this->globalConstants.clear(); this->identifiers.clear(); @@ -355,6 +357,9 @@ public: static const QStringList versionStrings; static BaseGameVersion stringToBaseGameVersion(const QString &string); + static QString getPlayerIconPath(BaseGameVersion baseGameVersion, int character); + static QIcon getPlayerIcon(BaseGameVersion baseGameVersion, int character); + void reset(BaseGameVersion baseGameVersion); void setFilePath(ProjectFilePath pathId, const QString &path); void setFilePath(const QString &pathId, const QString &path); @@ -414,11 +419,13 @@ public: uint16_t unusedTileCovered; uint16_t unusedTileSplit; bool mapAllowFlagsEnabled; + QString eventsTabIconPath; QString collisionSheetPath; QSize collisionSheetSize; QMargins playerViewDistance; QList warpBehaviors; int maxEventsPerGroup; + int forcedMajorVersion; QStringList globalConstantsFilepaths; QMap globalConstants; diff --git a/include/core/events.h b/include/core/events.h index 9da9b531..76a58dcf 100644 --- a/include/core/events.h +++ b/include/core/events.h @@ -34,15 +34,6 @@ class HiddenItemEvent; class SecretBaseEvent; class HealLocationEvent; -class EventVisitor { -public: - virtual void nothing() { } - virtual void visitObject(ObjectEvent *) = 0; - virtual void visitTrigger(TriggerEvent *) = 0; - virtual void visitSign(SignEvent *) = 0; -}; - - /// /// Event base class -- purely virtual /// @@ -121,8 +112,6 @@ public: void modify(); - virtual void accept(EventVisitor *) { } - void setX(int newX) { this->x = newX; } void setY(int newY) { this->y = newY; } void setZ(int newZ) { this->elevation = newZ; } @@ -150,6 +139,8 @@ public: virtual QSet getExpectedFields() = 0; + virtual QStringList getScripts() const { return QStringList(); } + QJsonObject getCustomAttributes() const { return this->customAttributes; } void setCustomAttributes(const QJsonObject &newCustomAttributes) { this->customAttributes = newCustomAttributes; } @@ -222,8 +213,6 @@ public: virtual Event *duplicate() const override; - virtual void accept(EventVisitor *visitor) override { visitor->visitObject(this); } - virtual EventFrame *createEventFrame() override; virtual OrderedJson::object buildEventJson(Project *project) override; @@ -233,6 +222,8 @@ public: virtual QSet getExpectedFields() override; + virtual QStringList getScripts() const override { return {getScript()}; } + virtual QPixmap loadPixmap(Project *project) override; void setGfx(QString newGfx) { this->gfx = newGfx; } @@ -392,8 +383,6 @@ public: virtual Event *duplicate() const override; - virtual void accept(EventVisitor *visitor) override { visitor->visitTrigger(this); } - virtual EventFrame *createEventFrame() override; virtual OrderedJson::object buildEventJson(Project *project) override; @@ -403,6 +392,8 @@ public: virtual QSet getExpectedFields() override; + virtual QStringList getScripts() const override { return {getScriptLabel()}; } + void setScriptVar(QString newScriptVar) { this->scriptVar = newScriptVar; } QString getScriptVar() const { return this->scriptVar; } @@ -490,8 +481,6 @@ public: virtual Event *duplicate() const override; - virtual void accept(EventVisitor *visitor) override { visitor->visitSign(this); } - virtual EventFrame *createEventFrame() override; virtual OrderedJson::object buildEventJson(Project *project) override; @@ -501,6 +490,8 @@ public: virtual QSet getExpectedFields() override; + virtual QStringList getScripts() const override { return {getScriptLabel()}; } + void setFacingDirection(QString newFacingDirection) { this->facingDirection = newFacingDirection; } QString getFacingDirection() const { return this->facingDirection; } @@ -633,21 +624,4 @@ inline uint qHash(const Event::Group &key, uint seed = 0) { return qHash(static_cast(key), seed); } - -/// -/// Keeps track of scripts -/// -class ScriptTracker : public EventVisitor { -public: - virtual void visitObject(ObjectEvent *object) override { this->scripts << object->getScript(); }; - virtual void visitTrigger(TriggerEvent *trigger) override { this->scripts << trigger->getScriptLabel(); }; - virtual void visitSign(SignEvent *sign) override { this->scripts << sign->getScriptLabel(); }; - - QStringList getScripts() const { return this->scripts; } - -private: - QStringList scripts; -}; - - #endif // EVENTS_H diff --git a/include/core/map.h b/include/core/map.h index 84791fa3..2ba172ce 100644 --- a/include/core/map.h +++ b/include/core/map.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #define DEFAULT_BORDER_WIDTH 2 @@ -56,7 +57,7 @@ public: MapHeader* header() const { return m_header; } void setSharedEventsMap(const QString &sharedEventsMap) { m_sharedEventsMap = sharedEventsMap; } - void setSharedScriptsMap(const QString &sharedScriptsMap) { m_sharedScriptsMap = sharedScriptsMap; } + void setSharedScriptsMap(const QString &sharedScriptsMap); QString sharedEventsMap() const { return m_sharedEventsMap; } QString sharedScriptsMap() const { return m_sharedScriptsMap; } @@ -75,14 +76,15 @@ public: Event* getEvent(Event::Group group, const QString &idName) const; QStringList getEventIdNames(Event::Group group) const; int getNumEvents(Event::Group group = Event::Group::None) const; - QStringList getScriptLabels(Event::Group group = Event::Group::None); - QString getScriptsFilePath() const; - void openScript(QString label); void removeEvent(Event *); void addEvent(Event *); int getIndexOfEvent(Event *) const; bool hasEvent(Event *) const; + QStringList getScriptLabels(Event::Group group = Event::Group::None); + QString getScriptsFilePath() const; + void openScript(const QString &label); + void deleteConnections(); QList getConnections() const { return m_connections; } MapConnection* getConnection(const QString &direction) const; @@ -108,7 +110,7 @@ private: QString m_sharedEventsMap = ""; QString m_sharedScriptsMap = ""; - QStringList m_scriptsFileLabels; + QStringList m_scriptLabels; QJsonObject m_customAttributes; MapHeader *m_header = nullptr; @@ -131,10 +133,14 @@ private: QList m_connections; QSet m_ownedConnections; - QUndoStack *m_editHistory = nullptr; + QPointer m_editHistory; + QPointer m_scriptFileWatcher; + + void invalidateScripts(); signals: void modified(); + void scriptsModified(); void mapDimensionsChanged(const QSize &size); void openScriptRequested(QString label); void connectionAdded(MapConnection*); diff --git a/include/mainwindow.h b/include/mainwindow.h index c65cac79..5de2a50a 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -351,7 +351,9 @@ private: void refreshCollisionSelector(); void setLayoutOnlyMode(bool layoutOnly); - bool checkProjectSanity(); + bool isInvalidProject(Project *project); + bool checkProjectSanity(Project *project); + bool checkProjectVersion(Project *project); bool loadProjectData(); bool setProjectUI(); void clearProjectUI(); diff --git a/include/project.h b/include/project.h index 3c11009f..6f981a85 100644 --- a/include/project.h +++ b/include/project.h @@ -85,6 +85,7 @@ public: void clearHealLocations(); bool sanityCheck(); + int getSupportedMajorVersion(QString *errorOut = nullptr); bool load(); Map* loadMap(const QString &mapName); diff --git a/include/ui/eventframes.h b/include/ui/eventframes.h index 09eae50b..3858e54e 100644 --- a/include/ui/eventframes.h +++ b/include/ui/eventframes.h @@ -56,9 +56,11 @@ protected: bool populated = false; bool initialized = false; bool connected = false; + QPointer project; void populateDropdown(NoScrollComboBox * combo, const QStringList &items); void populateScriptDropdown(NoScrollComboBox * combo, Project * project); + void populateMapNameDropdown(NoScrollComboBox * combo, Project * project); void populateIdNameDropdown(NoScrollComboBox * combo, Project * project, const QString &mapName, Event::Group group); private: diff --git a/resources/icons/player/brendan_em.ico b/resources/icons/player/brendan_em.ico new file mode 100644 index 00000000..2c896c44 Binary files /dev/null and b/resources/icons/player/brendan_em.ico differ diff --git a/resources/icons/player/brendan_rs.ico b/resources/icons/player/brendan_rs.ico new file mode 100644 index 00000000..c3385a4d Binary files /dev/null and b/resources/icons/player/brendan_rs.ico differ diff --git a/resources/icons/player/green.ico b/resources/icons/player/green.ico new file mode 100644 index 00000000..3cdada55 Binary files /dev/null and b/resources/icons/player/green.ico differ diff --git a/resources/icons/player/may_em.ico b/resources/icons/player/may_em.ico new file mode 100644 index 00000000..ffa56bab Binary files /dev/null and b/resources/icons/player/may_em.ico differ diff --git a/resources/icons/player/may_rs.ico b/resources/icons/player/may_rs.ico new file mode 100644 index 00000000..7450d620 Binary files /dev/null and b/resources/icons/player/may_rs.ico differ diff --git a/resources/icons/viewsprites.ico b/resources/icons/player/red.ico similarity index 100% rename from resources/icons/viewsprites.ico rename to resources/icons/player/red.ico diff --git a/resources/images.qrc b/resources/images.qrc index 1136caf1..b64c9e1e 100644 --- a/resources/images.qrc +++ b/resources/images.qrc @@ -49,9 +49,14 @@ icons/tall_grass.ico icons/warning.ico icons/minimap.ico - icons/viewsprites.ico icons/application_form_edit.ico icons/connections.ico + icons/player/brendan_em.ico + icons/player/brendan_rs.ico + icons/player/green.ico + icons/player/may_em.ico + icons/player/may_rs.ico + icons/player/red.ico icons/ui/dark/checkbox_checked_disabled.png icons/ui/dark/checkbox_checked_disabled@2x.png icons/ui/dark/checkbox_checked.png diff --git a/src/config.cpp b/src/config.cpp index 0e22840e..7914cdce 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -731,6 +731,32 @@ BaseGameVersion ProjectConfig::stringToBaseGameVersion(const QString &string) { return version; } +QString ProjectConfig::getPlayerIconPath(BaseGameVersion baseGameVersion, int character) { + switch (baseGameVersion) { + case BaseGameVersion::pokeemerald: { + static const QStringList paths = { QStringLiteral(":/icons/player/brendan_em.ico"), + QStringLiteral(":/icons/player/may_em.ico"), }; + return paths.value(character); + } + case BaseGameVersion::pokefirered: { + static const QStringList paths = { QStringLiteral(":/icons/player/red.ico"), + QStringLiteral(":/icons/player/green.ico"), }; + return paths.value(character); + } + case BaseGameVersion::pokeruby: { + static const QStringList paths = { QStringLiteral(":/icons/player/brendan_rs.ico"), + QStringLiteral(":/icons/player/may_rs.ico"), }; + return paths.value(character); + } + default: break; + } + return QString(); +} + +QIcon ProjectConfig::getPlayerIcon(BaseGameVersion baseGameVersion, int character) { + return QIcon(getPlayerIconPath(baseGameVersion, character)); +} + ProjectConfig projectConfig; QString ProjectConfig::getConfigFilepath() { @@ -868,6 +894,8 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { this->eventIconPaths[Event::Group::Heal] = value; } else if (key.startsWith("pokemon_icon_path/")) { this->pokemonIconPaths.insert(key.mid(QStringLiteral("pokemon_icon_path/").length()), value); + } else if (key == "events_tab_icon_path") { + this->eventsTabIconPath = value; } else if (key == "collision_sheet_path") { this->collisionSheetPath = value; } else if (key == "collision_sheet_width") { @@ -890,6 +918,8 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { this->warpBehaviors.append(getConfigUint32(key, s)); } else if (key == "max_events_per_group") { this->maxEventsPerGroup = getConfigInteger(key, value, 1, INT_MAX, 255); + } else if (key == "forced_major_version") { + this->forcedMajorVersion = getConfigInteger(key, value); } else { logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key)); } @@ -985,6 +1015,7 @@ QMap ProjectConfig::getKeyValueMap() { for (auto it = this->identifiers.constBegin(); it != this->identifiers.constEnd(); it++) { map.insert("ident/"+defaultIdentifiers.value(it.key()).first, it.value()); } + map.insert("events_tab_icon_path", this->eventsTabIconPath); map.insert("collision_sheet_path", this->collisionSheetPath); map.insert("collision_sheet_width", QString::number(this->collisionSheetSize.width())); map.insert("collision_sheet_height", QString::number(this->collisionSheetSize.height())); @@ -997,6 +1028,7 @@ QMap ProjectConfig::getKeyValueMap() { warpBehaviorStrs.append("0x" + QString("%1").arg(value, 2, 16, QChar('0')).toUpper()); map.insert("warp_behaviors", warpBehaviorStrs.join(",")); map.insert("max_events_per_group", QString::number(this->maxEventsPerGroup)); + map.insert("forced_major_version", QString::number(this->forcedMajorVersion)); return map; } diff --git a/src/core/events.cpp b/src/core/events.cpp index 22315211..ce376c44 100644 --- a/src/core/events.cpp +++ b/src/core/events.cpp @@ -114,7 +114,7 @@ QString Event::typeToString(Event::Type type) { } QPixmap Event::loadPixmap(Project *project) { - this->pixmap = project->getEventPixmap(this->getEventGroup()); + this->pixmap = project ? project->getEventPixmap(this->getEventGroup()) : QPixmap(); this->usesDefaultPixmap = true; return this->pixmap; } @@ -226,7 +226,7 @@ QSet ObjectEvent::getExpectedFields() { } QPixmap ObjectEvent::loadPixmap(Project *project) { - this->pixmap = project->getEventPixmap(this->gfx, this->movement); + this->pixmap = project ? project->getEventPixmap(this->gfx, this->movement) : QPixmap(); if (!this->pixmap.isNull()) { this->usesDefaultPixmap = false; return this->pixmap; @@ -316,7 +316,7 @@ QSet CloneObjectEvent::getExpectedFields() { QPixmap CloneObjectEvent::loadPixmap(Project *project) { // Try to get the targeted object to clone - Map *clonedMap = project->loadMap(this->targetMap); + Map *clonedMap = project ? project->loadMap(this->targetMap) : nullptr; Event *clonedEvent = clonedMap ? clonedMap->getEvent(Event::Group::Object, this->targetID) : nullptr; if (clonedEvent && clonedEvent->getEventType() == Event::Type::Object) { @@ -324,10 +324,13 @@ QPixmap CloneObjectEvent::loadPixmap(Project *project) { ObjectEvent *clonedObject = dynamic_cast(clonedEvent); this->gfx = clonedObject->getGfx(); this->movement = clonedObject->getMovement(); - } else { + } else if (project) { // Invalid object specified, use default graphics data (as would be shown in-game) this->gfx = project->gfxDefines.key(0, "0"); this->movement = project->movementTypes.value(0, "0"); + } else { + this->gfx = "0"; + this->movement = "0"; } return ObjectEvent::loadPixmap(project); } diff --git a/src/core/map.cpp b/src/core/map.cpp index 3cd087e1..c1905f0a 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -14,6 +14,10 @@ Map::Map(QObject *parent) : QObject(parent) { m_editHistory = new QUndoStack(this); + + m_scriptFileWatcher = new QFileSystemWatcher(this); + connect(m_scriptFileWatcher, &QFileSystemWatcher::fileChanged, this, &Map::invalidateScripts); + resetEvents(); m_header = new MapHeader(this); @@ -120,35 +124,40 @@ QPixmap Map::renderConnection(const QString &direction, Layout * fromLayout) { return connectionPixmap.copy(bounds.x() * 16, bounds.y() * 16, bounds.width() * 16, bounds.height() * 16); } -void Map::openScript(QString label) { +void Map::openScript(const QString &label) { emit openScriptRequested(label); } +void Map::setSharedScriptsMap(const QString &sharedScriptsMap) { + if (m_sharedScriptsMap == sharedScriptsMap) + return; + m_sharedScriptsMap = sharedScriptsMap; + invalidateScripts(); +} + +void Map::invalidateScripts() { + m_scriptsLoaded = false; + emit scriptsModified(); +} + QStringList Map::getScriptLabels(Event::Group group) { if (!m_scriptsLoaded) { - m_scriptsFileLabels = ParseUtil::getGlobalScriptLabels(getScriptsFilePath()); + const QString scriptsFilePath = getScriptsFilePath(); + m_scriptLabels = ParseUtil::getGlobalScriptLabels(scriptsFilePath); m_scriptsLoaded = true; + + // Track the scripts file for changes. Path may have changed, so stop tracking old files. + m_scriptFileWatcher->removePaths(m_scriptFileWatcher->files()); + m_scriptFileWatcher->addPath(scriptsFilePath); } - QStringList scriptLabels; + QStringList scriptLabels = m_scriptLabels; - // Get script labels currently in-use by the map's events - if (group == Event::Group::None) { - ScriptTracker scriptTracker; - for (const auto &event : getEvents()) { - event->accept(&scriptTracker); - } - scriptLabels = scriptTracker.getScripts(); - } else { - ScriptTracker scriptTracker; - for (const auto &event : m_events.value(group)) { - event->accept(&scriptTracker); - } - scriptLabels = scriptTracker.getScripts(); + // Add script labels currently in-use by the map's events + for (const auto &event : getEvents(group)) { + scriptLabels.append(event->getScripts()); } - // Add labels from the map's scripts file - scriptLabels.append(m_scriptsFileLabels); scriptLabels.sort(Qt::CaseInsensitive); scriptLabels.removeAll(""); scriptLabels.removeAll("0"); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 39432d19..7dc75114 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -250,7 +250,7 @@ void MainWindow::initCustomUI() { static const QMap mainTabIcons = { {MainTab::Map, QIcon(QStringLiteral(":/icons/minimap.ico"))}, - {MainTab::Events, QIcon(QStringLiteral(":/icons/viewsprites.ico"))}, + {MainTab::Events, ProjectConfig::getPlayerIcon(BaseGameVersion::pokefirered, 0)}, // Arbitrary default {MainTab::Header, QIcon(QStringLiteral(":/icons/application_form_edit.ico"))}, {MainTab::Connections, QIcon(QStringLiteral(":/icons/connections.ico"))}, {MainTab::WildPokemon, QIcon(QStringLiteral(":/icons/tall_grass.ico"))}, @@ -683,7 +683,7 @@ bool MainWindow::openProject(QString dir, bool initial) { // Make sure project looks reasonable before attempting to load it porysplash->showMessage("Verifying project"); - if (!checkProjectSanity()) { + if (isInvalidProject(this->editor->project)) { delete this->editor->project; porysplash->stop(); return false; @@ -725,16 +725,20 @@ bool MainWindow::loadProjectData() { return success; } -bool MainWindow::checkProjectSanity() { - if (editor->project->sanityCheck()) +bool MainWindow::isInvalidProject(Project *project) { + return !(checkProjectSanity(project) && checkProjectVersion(project)); +} + +bool MainWindow::checkProjectSanity(Project *project) { + if (project->sanityCheck()) return true; - logWarn(QString("The directory '%1' failed the project sanity check.").arg(editor->project->root)); + logWarn(QString("The directory '%1' failed the project sanity check.").arg(project->root)); ErrorMessage msgBox(QStringLiteral("The selected directory appears to be invalid."), porysplash); msgBox.setInformativeText(QString("The directory '%1' is missing key files.\n\n" "Make sure you selected the correct project directory " - "(the one used to make your .gba file, e.g. 'pokeemerald').").arg(editor->project->root)); + "(the one used to make your .gba file, e.g. 'pokeemerald').").arg(project->root)); auto tryAnyway = msgBox.addButton("Try Anyway", QMessageBox::ActionRole); msgBox.exec(); if (msgBox.clickedButton() == tryAnyway) { @@ -745,6 +749,43 @@ bool MainWindow::checkProjectSanity() { return false; } +bool MainWindow::checkProjectVersion(Project *project) { + QString error; + int projectVersion = project->getSupportedMajorVersion(&error); + if (projectVersion < 0) { + // Failed to identify a supported major version. + // We can't draw any conclusions from this, so we don't consider the project to be invalid. + QString msg = QStringLiteral("Failed to check project version"); + logWarn(error.isEmpty() ? msg : QString("%1: '%2'").arg(msg).arg(error)); + } else { + QString msg = QStringLiteral("Successfully checked project version. "); + logInfo(msg + ((projectVersion != 0) ? QString("Supports at least Porymap v%1").arg(projectVersion) + : QStringLiteral("Too old for any Porymap version"))); + + if (projectVersion < porymapVersion.majorVersion() && projectConfig.forcedMajorVersion < porymapVersion.majorVersion()) { + // We were unable to find the necessary changes for Porymap's current major version. + // Unless they have explicitly suppressed this message, warn the user that this might mean their project is missing breaking changes. + // Note: Do not report 'projectVersion' to the user in this message. We've already logged it for troubleshooting. + // It is very plausible that the user may have reproduced the required changes in an + // unknown commit, rather than merging the required changes directly from the base repo. + // In this case the 'projectVersion' may actually be too old to use for their repo. + ErrorMessage msgBox(QStringLiteral("Your project may be incompatible!"), porysplash); + msgBox.setInformativeText(QString("Make sure '%1' has all the required changes for Porymap version %2." + "") // TODO: Once we have a wiki or manual page describing breaking changes, link that here. + .arg(project->getProjectTitle()) + .arg(porymapVersion.majorVersion())); + auto tryAnyway = msgBox.addButton("Try Anyway", QMessageBox::ActionRole); + msgBox.exec(); + if (msgBox.clickedButton() != tryAnyway){ + return false; + } + // User opted to try with this version anyway. Don't warn them about this version again. + projectConfig.forcedMajorVersion = porymapVersion.majorVersion(); + } + } + return true; +} + void MainWindow::showProjectOpenFailure() { if (!this->isVisible()){ // The main window is not visible during the initial project open; the splash screen is busy providing visual feedback. @@ -1148,11 +1189,6 @@ void MainWindow::displayMapProperties() { ui->frame_HeaderData->setEnabled(true); this->mapHeaderForm->setHeader(editor->map->header()); - const QSignalBlocker b_PrimaryTileset(ui->comboBox_PrimaryTileset); - const QSignalBlocker b_SecondaryTileset(ui->comboBox_SecondaryTileset); - ui->comboBox_PrimaryTileset->setTextItem(editor->map->layout()->tileset_primary_label); - ui->comboBox_SecondaryTileset->setTextItem(editor->map->layout()->tileset_secondary_label); - ui->mapCustomAttributesFrame->table()->setAttributes(editor->map->customAttributes()); } @@ -1266,6 +1302,21 @@ bool MainWindow::setProjectUI() { ui->mapCustomAttributesFrame->table()->setRestrictedKeys(project->getTopLevelMapFields()); + // Set a version dependent player icon (or user-chosen icon) for the Events tab. + QIcon eventTabIcon; + if (!projectConfig.eventsTabIconPath.isEmpty()) { + eventTabIcon = QIcon(project->getExistingFilepath(projectConfig.eventsTabIconPath)); + if (eventTabIcon.isNull()) { + logWarn(QString("Failed to load custom Events tab icon '%1'.").arg(projectConfig.eventsTabIconPath)); + } + } + if (eventTabIcon.isNull()) { + // We randomly choose between the available characters for ~flavor~. + // For now, this correctly assumes all versions have 2 icons. + eventTabIcon = ProjectConfig::getPlayerIcon(projectConfig.baseGameVersion, QRandomGenerator::global()->bounded(0, 2)); + } + ui->mainTabBar->setTabIcon(MainTab::Events, eventTabIcon); + return true; } diff --git a/src/project.cpp b/src/project.cpp index bf43143e..f378cfcb 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -75,6 +75,117 @@ bool Project::sanityCheck() { return false; } +// Porymap projects have no standardized way for Porymap to determine whether they're compatible as of the latest breaking changes. +// We can use the project's git history (if it has one, and we're able to get it) to make a reasonable guess. +// We know the hashes of the commits in the base repos that contain breaking changes, so if we find one of these then the project +// should support at least up to that Porymap major version. If this fails for any reason it returns a version of -1. +int Project::getSupportedMajorVersion(QString *errorOut) { + // This has relatively tight timeout windows (500ms for each process, compared to the default 30,000ms). This version check + // is not important enough to significantly slow down project launch, we'd rather just timeout. + const int timeoutLimit = 500; + const int failureVersion = -1; + QString gitName = "git"; + QString gitPath = QStandardPaths::findExecutable(gitName); + if (gitPath.isEmpty()) { + if (errorOut) *errorOut = QString("Unable to locate %1.").arg(gitName); + return failureVersion; + } + + QProcess process; + process.setWorkingDirectory(this->root); + process.setProgram(gitPath); + process.setReadChannel(QProcess::StandardOutput); + process.setStandardInputFile(QProcess::nullDevice()); // We won't have any writing to do. + + // First we need to know which (if any) known history this project belongs to. + // We'll get the root commit, then compare it to the known root commits for the base project repos. + static const QStringList args_getRootCommit = { "rev-list", "--max-parents=0", "HEAD" }; + process.setArguments(args_getRootCommit); + process.start(); + if (!process.waitForFinished(timeoutLimit) || process.exitStatus() != QProcess::ExitStatus::NormalExit || process.exitCode() != 0) { + if (errorOut) { + *errorOut = QStringLiteral("Failed to identify commit history"); + if (process.error() != QProcess::UnknownError && !process.errorString().isEmpty()) { + errorOut->append(QString(": %1").arg(process.errorString())); + } else { + process.setReadChannel(QProcess::StandardError); + QString error = QString(process.readLine()).remove('\n'); + if (!error.isEmpty()) errorOut->append(QString(": %1").arg(error)); + } + } + return failureVersion; + } + const QString rootCommit = QString(process.readLine()).remove('\n'); + + // The keys in this map are the hashes of the root commits for each of the 3 base repos. + // The values are a list of pairs, where the first element is a major version number, and the + // second element is the hash of the earliest commit that supports that major version. + static const QMap>> historyMap = { + // pokeemerald + {"33b799c967fd63d04afe82eecc4892f3e45781b3", { + {6, "07c897ad48c36b178093bde8ca360823127d812b"}, // TODO: Update to merge commit for pokeemerald's porymap-6 branch + {5, "c76beed98990a57c84d3930190fd194abfedf7e8"}, + {4, "cb5b8da77b9ba6837fcc8c5163bedc5008b12c2c"}, + {3, "204c431993dad29661a9ff47326787cd0cf381e6"}, + {2, "cdae0c1444bed98e652c87dc3e3edcecacfef8be"}, + {1, ""} + }}, + // pokefirered + {"670fef77ac4d9116d5fdc28c0da40622919a062b", { + {6, "7722e7a92ca5fa69925dcef82f6c89c35ec48171"}, // TODO: Update to merge commit for pokefirered's porymap-6 branch + {5, "52591dcee42933d64f60c59276fc13c3bb89c47b"}, + {4, "200c82e01a94dbe535e6ed8768d8afad4444d4d2"}, + }}, + // pokeruby + {"1362b60f3467f0894d55e82f3294980b6373021d", { + {6, "bc5aeaa64ecad03aa4ab9e1000ba94916276c936"}, // TODO: Update to merge commit for pokeruby's porymap-6 branch + {5, "d99cb43736dd1d4ee4820f838cb259d773d8bf25"}, + {4, "f302fcc134bf354c3655e3423be68fd7a99cb396"}, + {3, "b4f4d2c0f03462dcdf3492aad27890294600eb2e"}, + {2, "0e8ccfc4fd3544001f4c25fafd401f7558bdefba"}, + {1, ""} + }}, + }; + if (!historyMap.contains(rootCommit)) { + // Either this repo does not share history with one of the base repos, or we got some unexpected result. + if (errorOut) *errorOut = QStringLiteral("Unrecognized commit history"); + return failureVersion; + } + + // We now know which base repo that the user's repo shares history with. + // Next we check to see if it contains the changes required to support particular major versions of Porymap. + // We'll start with the most recent major version and work backwards. + for (const auto &pair : historyMap.value(rootCommit)) { + int versionNum = pair.first; + QString commitHash = pair.second; + if (commitHash.isEmpty()) { + // An empty commit hash means 'consider any point in the history a supported version' + return versionNum; + } + process.setArguments({ "merge-base", "--is-ancestor", commitHash, "HEAD" }); + process.start(); + if (!process.waitForFinished(timeoutLimit) || process.exitStatus() != QProcess::ExitStatus::NormalExit) { + if (errorOut) { + *errorOut = QStringLiteral("Failed to search commit history"); + if (process.error() != QProcess::UnknownError && !process.errorString().isEmpty()) { + errorOut->append(QString(": %1").arg(process.errorString())); + } else { + process.setReadChannel(QProcess::StandardError); + QString error = QString(process.readLine()).remove('\n'); + if (!error.isEmpty()) errorOut->append(QString(": %1").arg(error)); + } + } + return failureVersion; + } + if (process.exitCode() == 0) { + // Identified a supported major version + return versionNum; + } + } + // We recognized the commit history, but it's too old for any version of Porymap to support. + return 0; +} + bool Project::load() { this->parser.setUpdatesSplashScreen(true); resetFileCache(); diff --git a/src/ui/eventframes.cpp b/src/ui/eventframes.cpp index 51293dd5..35d58831 100644 --- a/src/ui/eventframes.cpp +++ b/src/ui/eventframes.cpp @@ -152,8 +152,12 @@ void EventFrame::initialize() { this->label_icon->setPixmap(this->event->getPixmap()); } -void EventFrame::populate(Project *) { +void EventFrame::populate(Project *project) { this->populated = true; + if (this->project && this->project != project) { + this->project->disconnect(this); + } + this->project = project; } void EventFrame::invalidateConnections() { @@ -166,6 +170,10 @@ void EventFrame::invalidateUi() { void EventFrame::invalidateValues() { this->populated = false; + if (this->isVisible()) { + // Repopulate immediately + this->populate(this->project); + } } void EventFrame::setActive(bool active) { @@ -186,14 +194,15 @@ void EventFrame::populateDropdown(NoScrollComboBox * combo, const QStringList &i void EventFrame::populateScriptDropdown(NoScrollComboBox * combo, Project * project) { // The script dropdown and autocomplete are populated with scripts used by the map's events and from its scripts file. - if (!this->event->getMap()) + Map *map = this->event ? this->event->getMap() : nullptr; + if (!map) return; - QStringList scripts = this->event->getMap()->getScriptLabels(this->event->getEventGroup()); + QStringList scripts = map->getScriptLabels(this->event->getEventGroup()); populateDropdown(combo, scripts); // Depending on the settings, the autocomplete may also contain all global scripts. - if (porymapConfig.loadAllEventScripts) { + if (project && porymapConfig.loadAllEventScripts) { project->insertGlobalScriptLabels(scripts); } @@ -209,14 +218,23 @@ void EventFrame::populateScriptDropdown(NoScrollComboBox * combo, Project * proj combo->setCompleter(completer); - // If the project changes the script labels, update the EventFrame. - // TODO: At the moment this only happens when the user changes script settings (i.e. when 'porymapConfig.loadAllEventScripts' changes). - // This should ultimately be connected to a file watcher so that we can also update the dropdown when the scripts file changes. - connect(project, &Project::eventScriptLabelsRead, this, &EventFrame::invalidateValues, Qt::UniqueConnection); + // If the script labels change then we need to update the EventFrame. + if (project) connect(project, &Project::eventScriptLabelsRead, this, &EventFrame::invalidateValues, Qt::UniqueConnection); + connect(map, &Map::scriptsModified, this, &EventFrame::invalidateValues, Qt::UniqueConnection); +} + +void EventFrame::populateMapNameDropdown(NoScrollComboBox * combo, Project * project) { + if (!project) + return; + + populateDropdown(combo, project->mapNames); + + // This frame type displays map names, so when a new map is created we need to repopulate it. + connect(project, &Project::mapCreated, this, &EventFrame::invalidateValues, Qt::UniqueConnection); } void EventFrame::populateIdNameDropdown(NoScrollComboBox * combo, Project * project, const QString &mapName, Event::Group group) { - if (!project->mapNames.contains(mapName)) + if (!project || !project->mapNames.contains(mapName)) return; Map *map = project->loadMap(mapName); @@ -325,7 +343,6 @@ void ObjectFrame::connectSignals(MainWindow *window) { if (this->connected) return; EventFrame::connectSignals(window); - Project *project = window->editor->project; // local id this->line_edit_local_id->disconnect(); @@ -336,18 +353,18 @@ void ObjectFrame::connectSignals(MainWindow *window) { // sprite update this->combo_sprite->disconnect(); - connect(this->combo_sprite, &QComboBox::currentTextChanged, [this, project](const QString &text) { + connect(this->combo_sprite, &QComboBox::currentTextChanged, [this](const QString &text) { this->object->setGfx(text); - this->object->getPixmapItem()->render(project); + this->object->getPixmapItem()->render(this->project); this->object->modify(); }); connect(this->object->getPixmapItem(), &EventPixmapItem::rendered, this->label_icon, &QLabel::setPixmap); // movement this->combo_movement->disconnect(); - connect(this->combo_movement, &QComboBox::currentTextChanged, [this, project](const QString &text) { + connect(this->combo_movement, &QComboBox::currentTextChanged, [this](const QString &text) { this->object->setMovement(text); - this->object->getPixmapItem()->render(project); + this->object->getPixmapItem()->render(this->project); this->object->modify(); }); @@ -498,7 +515,6 @@ void CloneObjectFrame::connectSignals(MainWindow *window) { if (this->connected) return; EventFrame::connectSignals(window); - Project *project = window->editor->project; // local id this->line_edit_local_id->disconnect(); @@ -512,26 +528,23 @@ void CloneObjectFrame::connectSignals(MainWindow *window) { // target map this->combo_target_map->disconnect(); - connect(this->combo_target_map, &QComboBox::currentTextChanged, [this, project](const QString &mapName) { + connect(this->combo_target_map, &QComboBox::currentTextChanged, [this](const QString &mapName) { this->clone->setTargetMap(mapName); - this->clone->getPixmapItem()->render(project); + this->clone->getPixmapItem()->render(this->project); this->combo_sprite->setTextItem(this->clone->getGfx()); this->clone->modify(); - populateIdNameDropdown(this->combo_target_id, project, mapName, Event::Group::Object); + populateIdNameDropdown(this->combo_target_id, this->project, mapName, Event::Group::Object); }); connect(window, &MainWindow::mapOpened, this, &CloneObjectFrame::tryInvalidateIdDropdown, Qt::UniqueConnection); // target id this->combo_target_id->disconnect(); - connect(this->combo_target_id, &QComboBox::currentTextChanged, [this, project](const QString &text) { + connect(this->combo_target_id, &QComboBox::currentTextChanged, [this](const QString &text) { this->clone->setTargetID(text); - this->clone->getPixmapItem()->render(project); + this->clone->getPixmapItem()->render(this->project); this->combo_sprite->setTextItem(this->clone->getGfx()); this->clone->modify(); }); - - // This frame type displays map names, so when a new map is created we need to repopulate it. - connect(project, &Project::mapCreated, this, &EventFrame::invalidateValues, Qt::UniqueConnection); } void CloneObjectFrame::tryInvalidateIdDropdown(Map *map) { @@ -567,7 +580,7 @@ void CloneObjectFrame::populate(Project *project) { const QSignalBlocker blocker(this); EventFrame::populate(project); - populateDropdown(this->combo_target_map, project->mapNames); + populateMapNameDropdown(this->combo_target_map, project); populateIdNameDropdown(this->combo_target_id, project, this->clone->getTargetMap(), Event::Group::Object); } @@ -618,7 +631,6 @@ void WarpFrame::connectSignals(MainWindow *window) { if (this->connected) return; EventFrame::connectSignals(window); - Project *project = window->editor->project; // id this->line_edit_id->disconnect(); @@ -629,10 +641,10 @@ void WarpFrame::connectSignals(MainWindow *window) { // dest map this->combo_dest_map->disconnect(); - connect(this->combo_dest_map, &QComboBox::currentTextChanged, [this, project](const QString &mapName) { + connect(this->combo_dest_map, &QComboBox::currentTextChanged, [this](const QString &mapName) { this->warp->setDestinationMap(mapName); this->warp->modify(); - populateIdNameDropdown(this->combo_dest_warp, project, mapName, Event::Group::Warp); + populateIdNameDropdown(this->combo_dest_warp, this->project, mapName, Event::Group::Warp); }); connect(window, &MainWindow::mapOpened, this, &WarpFrame::tryInvalidateIdDropdown, Qt::UniqueConnection); @@ -646,9 +658,6 @@ void WarpFrame::connectSignals(MainWindow *window) { // warning this->warning->disconnect(); connect(this->warning, &QPushButton::clicked, window, &MainWindow::onWarpBehaviorWarningClicked); - - // This frame type displays map names, so when a new map is created we need to repopulate it. - connect(project, &Project::mapCreated, this, &EventFrame::invalidateValues, Qt::UniqueConnection); } void WarpFrame::tryInvalidateIdDropdown(Map *map) { @@ -681,7 +690,7 @@ void WarpFrame::populate(Project *project) { const QSignalBlocker blocker(this); EventFrame::populate(project); - populateDropdown(this->combo_dest_map, project->mapNames); + populateMapNameDropdown(this->combo_dest_map, project); populateIdNameDropdown(this->combo_dest_warp, project, this->warp->getDestinationMap(), Event::Group::Warp); } @@ -1102,7 +1111,6 @@ void HealLocationFrame::connectSignals(MainWindow *window) { if (this->connected) return; EventFrame::connectSignals(window); - Project *project = window->editor->project; this->line_edit_id->disconnect(); connect(this->line_edit_id, &QLineEdit::textChanged, [this](const QString &text) { @@ -1111,10 +1119,10 @@ void HealLocationFrame::connectSignals(MainWindow *window) { }); this->combo_respawn_map->disconnect(); - connect(this->combo_respawn_map, &QComboBox::currentTextChanged, [this, project](const QString &mapName) { + connect(this->combo_respawn_map, &QComboBox::currentTextChanged, [this](const QString &mapName) { this->healLocation->setRespawnMapName(mapName); this->healLocation->modify(); - populateIdNameDropdown(this->combo_respawn_npc, project, mapName, Event::Group::Object); + populateIdNameDropdown(this->combo_respawn_npc, this->project, mapName, Event::Group::Object); }); connect(window, &MainWindow::mapOpened, this, &HealLocationFrame::tryInvalidateIdDropdown, Qt::UniqueConnection); @@ -1123,9 +1131,6 @@ void HealLocationFrame::connectSignals(MainWindow *window) { this->healLocation->setRespawnNPC(text); this->healLocation->modify(); }); - - // This frame type displays map names, so when a new map is created we need to repopulate it. - connect(project, &Project::mapCreated, this, &EventFrame::invalidateValues, Qt::UniqueConnection); } void HealLocationFrame::tryInvalidateIdDropdown(Map *map) { @@ -1158,7 +1163,7 @@ void HealLocationFrame::populate(Project *project) { EventFrame::populate(project); if (projectConfig.healLocationRespawnDataEnabled) { - populateDropdown(this->combo_respawn_map, project->mapNames); + populateMapNameDropdown(this->combo_respawn_map, project); populateIdNameDropdown(this->combo_respawn_npc, project, this->healLocation->getRespawnMapName(), Event::Group::Object); } } diff --git a/src/ui/projectsettingseditor.cpp b/src/ui/projectsettingseditor.cpp index 166874ee..fa2d3ee2 100644 --- a/src/ui/projectsettingseditor.cpp +++ b/src/ui/projectsettingseditor.cpp @@ -67,6 +67,7 @@ void ProjectSettingsEditor::connectSignals() { connect(ui->button_BGsIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_BGsIcon); }); connect(ui->button_HealLocationsIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_HealLocationsIcon); }); connect(ui->button_PokemonIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_PokemonIcon); }); + connect(ui->button_EventsTabIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_EventsTabIcon); }); // Display a warning if a mask value overlaps with another mask in its group. @@ -113,6 +114,20 @@ void ProjectSettingsEditor::initUi() { ui->comboBox_BaseGameVersion->addItems(ProjectConfig::versionStrings); ui->comboBox_AttributesSize->addItems({"1", "2", "4"}); + ui->comboBox_EventsTabIcon->addItem("Automatic", ""); + ui->comboBox_EventsTabIcon->addItem("Brendan (Emerald)", ProjectConfig::getPlayerIconPath(BaseGameVersion::pokeemerald, 0)); + ui->comboBox_EventsTabIcon->addItem("Brendan (R/S)", ProjectConfig::getPlayerIconPath(BaseGameVersion::pokeruby, 0)); + ui->comboBox_EventsTabIcon->addItem("May (Emerald)", ProjectConfig::getPlayerIconPath(BaseGameVersion::pokeemerald, 1)); + ui->comboBox_EventsTabIcon->addItem("May (R/S)", ProjectConfig::getPlayerIconPath(BaseGameVersion::pokeruby, 1)); + ui->comboBox_EventsTabIcon->addItem("Red", ProjectConfig::getPlayerIconPath(BaseGameVersion::pokefirered, 0)); + ui->comboBox_EventsTabIcon->addItem("Green", ProjectConfig::getPlayerIconPath(BaseGameVersion::pokefirered, 1)); + ui->comboBox_EventsTabIcon->addItem("Custom", "Custom"); + connect(ui->comboBox_EventsTabIcon, QOverload::of(&NoScrollComboBox::currentIndexChanged), [this](int index) { + bool usingCustom = (index == ui->comboBox_EventsTabIcon->findText("Custom")); + ui->lineEdit_EventsTabIcon->setVisible(usingCustom); + ui->button_EventsTabIcon->setVisible(usingCustom); + }); + // Validate that the border metatiles text is a comma-separated list of metatile values static const QString regex_Hex = "(0[xX])?[A-Fa-f0-9]+"; static const QRegularExpression expression_HexList(QString("^(%1,)*%1$").arg(regex_Hex)); // Comma-separated list of hex values @@ -520,6 +535,15 @@ void ProjectSettingsEditor::refresh() { } this->setWarpBehaviorsList(behaviorNames); + int index = ui->comboBox_EventsTabIcon->findData(projectConfig.eventsTabIconPath); + if (index < 0) { + index = ui->comboBox_EventsTabIcon->findData("Custom"); + ui->lineEdit_EventsTabIcon->setText(projectConfig.eventsTabIconPath); + } else { + ui->lineEdit_EventsTabIcon->setText(""); + } + ui->comboBox_EventsTabIcon->setCurrentIndex(index); + this->refreshing = false; // Allow signals } @@ -608,6 +632,16 @@ void ProjectSettingsEditor::save() { for (auto i = this->editedPokemonIconPaths.cbegin(), end = this->editedPokemonIconPaths.cend(); i != end; i++) projectConfig.setPokemonIconPath(i.key(), i.value()); + QString eventsTabIconPath; + QVariant data = ui->comboBox_EventsTabIcon->currentData(); + if (data.isValid() && data.canConvert()) { + eventsTabIconPath = data.toString(); + if (eventsTabIconPath == "Custom") { + eventsTabIconPath = ui->lineEdit_EventsTabIcon->text(); + } + } + projectConfig.eventsTabIconPath = eventsTabIconPath; + projectConfig.save(); userConfig.save(); porymapConfig.save();