#include "projectsettingseditor.h" #include "config.h" #include "noscrollcombobox.h" #include "prefab.h" #include #include /* Editor for the settings in a user's porymap.project.cfg file (and 'use_encounter_json' in porymap.user.cfg). Disabling the warp behavior warning is actually part of porymap.cfg, but it's on this window because the related settings are here (and project-specific). */ const int ProjectSettingsEditor::eventsTab = 3; ProjectSettingsEditor::ProjectSettingsEditor(QWidget *parent, Project *project) : QMainWindow(parent), ui(new Ui::ProjectSettingsEditor), project(project), baseDir(userConfig.getProjectDir() + QDir::separator()) { ui->setupUi(this); setAttribute(Qt::WA_DeleteOnClose); this->initUi(); this->createProjectPathsTable(); this->createProjectIdentifiersTable(); this->connectSignals(); this->refresh(); this->restoreWindowState(); } ProjectSettingsEditor::~ProjectSettingsEditor() { delete ui; } void ProjectSettingsEditor::connectSignals() { connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &ProjectSettingsEditor::dialogButtonClicked); connect(ui->button_ImportDefaultPrefabs, &QAbstractButton::clicked, this, &ProjectSettingsEditor::importDefaultPrefabsClicked); connect(ui->comboBox_BaseGameVersion, &QComboBox::currentTextChanged, this, &ProjectSettingsEditor::promptRestoreDefaults); connect(ui->comboBox_AttributesSize, &QComboBox::currentTextChanged, this, &ProjectSettingsEditor::updateAttributeLimits); connect(ui->comboBox_IconSpecies, &QComboBox::currentTextChanged, this, &ProjectSettingsEditor::updatePokemonIconPath); connect(ui->checkBox_EnableCustomBorderSize, &QCheckBox::stateChanged, [this](int state) { bool customSize = (state == Qt::Checked); // When switching between the spin boxes or line edit for border metatiles we set // the newly-shown UI using the values from the hidden UI. this->setBorderMetatileIds(customSize, this->getBorderMetatileIds(!customSize)); this->setBorderMetatilesUi(customSize); }); connect(ui->button_AddWarpBehavior, &QAbstractButton::clicked, [this](bool) { this->updateWarpBehaviorsList(true); }); connect(ui->button_RemoveWarpBehavior, &QAbstractButton::clicked, [this](bool) { this->updateWarpBehaviorsList(false); }); // Connect file selection buttons connect(ui->button_ChoosePrefabs, &QAbstractButton::clicked, [this](bool) { this->choosePrefabsFile(); }); connect(ui->button_CollisionGraphics, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_CollisionGraphics); }); connect(ui->button_ObjectsIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_ObjectsIcon); }); connect(ui->button_WarpsIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_WarpsIcon); }); connect(ui->button_TriggersIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_TriggersIcon); }); connect(ui->button_BGsIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_BGsIcon); }); connect(ui->button_HealspotsIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_HealspotsIcon); }); connect(ui->button_PokemonIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_PokemonIcon); }); // Display a warning if a mask value overlaps with another mask in its group. connect(ui->spinBox_MetatileIdMask, &UIntSpinBox::textChanged, this, &ProjectSettingsEditor::updateBlockMaskOverlapWarning); connect(ui->spinBox_CollisionMask, &UIntSpinBox::textChanged, this, &ProjectSettingsEditor::updateBlockMaskOverlapWarning); connect(ui->spinBox_ElevationMask, &UIntSpinBox::textChanged, this, &ProjectSettingsEditor::updateBlockMaskOverlapWarning); connect(ui->spinBox_BehaviorMask, &UIntSpinBox::textChanged, this, &ProjectSettingsEditor::updateAttributeMaskOverlapWarning); connect(ui->spinBox_LayerTypeMask, &UIntSpinBox::textChanged, this, &ProjectSettingsEditor::updateAttributeMaskOverlapWarning); connect(ui->spinBox_EncounterTypeMask, &UIntSpinBox::textChanged, this, &ProjectSettingsEditor::updateAttributeMaskOverlapWarning); connect(ui->spinBox_TerrainTypeMask, &UIntSpinBox::textChanged, this, &ProjectSettingsEditor::updateAttributeMaskOverlapWarning); // Record that there are unsaved changes if any of the settings are modified for (auto combo : ui->centralwidget->findChildren()){ // Changes to these two combo boxes are just for info display, don't mark as unsaved if (combo != ui->comboBox_IconSpecies && combo != ui->comboBox_WarpBehaviors) connect(combo, &QComboBox::currentTextChanged, this, &ProjectSettingsEditor::markEdited); } for (auto checkBox : ui->centralwidget->findChildren()) connect(checkBox, &QCheckBox::stateChanged, this, &ProjectSettingsEditor::markEdited); for (auto lineEdit : ui->centralwidget->findChildren()) connect(lineEdit, &QLineEdit::textEdited, this, &ProjectSettingsEditor::markEdited); for (auto spinBox : ui->centralwidget->findChildren()) connect(spinBox, &QSpinBox::textChanged, this, &ProjectSettingsEditor::markEdited); for (auto spinBox : ui->centralwidget->findChildren()) connect(spinBox, &UIntSpinBox::textChanged, this, &ProjectSettingsEditor::markEdited); } void ProjectSettingsEditor::markEdited() { // Don't treat signals emitted while the UI is refreshing as edits if (!this->refreshing) this->hasUnsavedChanges = true; } void ProjectSettingsEditor::initUi() { // Populate combo boxes if (project) { ui->comboBox_DefaultPrimaryTileset->addItems(project->primaryTilesetLabels); ui->comboBox_DefaultSecondaryTileset->addItems(project->secondaryTilesetLabels); ui->comboBox_IconSpecies->addItems(project->speciesToIconPath.keys()); ui->comboBox_WarpBehaviors->addItems(project->metatileBehaviorMap.keys()); } ui->comboBox_BaseGameVersion->addItems(ProjectConfig::versionStrings); ui->comboBox_AttributesSize->addItems({"1", "2", "4"}); // 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 QRegularExpressionValidator *validator_HexList = new QRegularExpressionValidator(expression_HexList); ui->lineEdit_BorderMetatiles->setValidator(validator_HexList); this->setBorderMetatilesUi(projectConfig.getUseCustomBorderSize()); // Validate that the text added to the warp behavior list could be a valid define // (we don't care whether it actually is a metatile behavior define) static const QRegularExpression expression_Word("^[A-Za-z0-9_]*$"); QRegularExpressionValidator *validator_Word = new QRegularExpressionValidator(expression_Word); ui->comboBox_WarpBehaviors->setValidator(validator_Word); ui->textEdit_WarpBehaviors->setTextColor(Qt::gray); // Set spin box limits uint16_t maxMetatileId = Block::getMaxMetatileId(); ui->spinBox_FillMetatile->setMaximum(maxMetatileId); ui->spinBox_BorderMetatile1->setMaximum(maxMetatileId); ui->spinBox_BorderMetatile2->setMaximum(maxMetatileId); ui->spinBox_BorderMetatile3->setMaximum(maxMetatileId); ui->spinBox_BorderMetatile4->setMaximum(maxMetatileId); ui->spinBox_Elevation->setMaximum(Block::getMaxElevation()); ui->spinBox_Collision->setMaximum(Block::getMaxCollision()); ui->spinBox_MaxElevation->setMaximum(Block::getMaxElevation()); ui->spinBox_MaxCollision->setMaximum(Block::getMaxCollision()); ui->spinBox_MetatileIdMask->setMaximum(Block::maxValue); ui->spinBox_CollisionMask->setMaximum(Block::maxValue); ui->spinBox_ElevationMask->setMaximum(Block::maxValue); // Some settings can be determined by constants in the project. // We reflect that here by disabling their UI elements. if (project) { const QString maskFilepath = projectConfig.getFilePath(ProjectFilePath::global_fieldmap); const QString attrTableFilepath = projectConfig.getFilePath(ProjectFilePath::fieldmap); const QString metatileIdMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_metatile); const QString collisionMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_collision); const QString elevationMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_elevation); const QString behaviorMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_behavior); const QString layerTypeMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_layer); const QString behaviorTableName = projectConfig.getIdentifier(ProjectIdentifier::define_attribute_behavior); const QString layerTypeTableName = projectConfig.getIdentifier(ProjectIdentifier::define_attribute_layer); const QString encounterTypeTableName = projectConfig.getIdentifier(ProjectIdentifier::define_attribute_encounter); const QString terrainTypeTableName = projectConfig.getIdentifier(ProjectIdentifier::define_attribute_terrain); const QString attrTableName = projectConfig.getIdentifier(ProjectIdentifier::symbol_attribute_table); // Block masks if (project->disabledSettingsNames.contains(metatileIdMaskName)) this->disableParsedSetting(ui->spinBox_MetatileIdMask, metatileIdMaskName, maskFilepath); if (project->disabledSettingsNames.contains(collisionMaskName)) this->disableParsedSetting(ui->spinBox_CollisionMask, collisionMaskName, maskFilepath); if (project->disabledSettingsNames.contains(elevationMaskName)) this->disableParsedSetting(ui->spinBox_ElevationMask, elevationMaskName, maskFilepath); // Behavior mask if (project->disabledSettingsNames.contains(behaviorMaskName)) this->disableParsedSetting(ui->spinBox_BehaviorMask, behaviorMaskName, maskFilepath); else if (project->disabledSettingsNames.contains(behaviorTableName)) this->disableParsedSetting(ui->spinBox_BehaviorMask, attrTableName, attrTableFilepath); // Layer type mask if (project->disabledSettingsNames.contains(layerTypeMaskName)) this->disableParsedSetting(ui->spinBox_LayerTypeMask, layerTypeMaskName, maskFilepath); else if (project->disabledSettingsNames.contains(layerTypeTableName)) this->disableParsedSetting(ui->spinBox_LayerTypeMask, attrTableName, attrTableFilepath); // Encounter and terrain type masks if (project->disabledSettingsNames.contains(encounterTypeTableName)) this->disableParsedSetting(ui->spinBox_EncounterTypeMask, attrTableName, attrTableFilepath); if (project->disabledSettingsNames.contains(terrainTypeTableName)) this->disableParsedSetting(ui->spinBox_TerrainTypeMask, attrTableName, attrTableFilepath); } } void ProjectSettingsEditor::disableParsedSetting(QWidget * widget, const QString &name, const QString &filepath) { widget->setEnabled(false); widget->setToolTip(QString("This value has been read from '%1' in %2").arg(name).arg(filepath)); } // Remember the current settings tab for future sessions void ProjectSettingsEditor::on_mainTabs_tabBarClicked(int index) { porymapConfig.setProjectSettingsTab(index); } void ProjectSettingsEditor::setTab(int index) { ui->mainTabs->setCurrentIndex(index); porymapConfig.setProjectSettingsTab(index); } void ProjectSettingsEditor::setBorderMetatilesUi(bool customSize) { ui->stackedWidget_BorderMetatiles->setCurrentIndex(customSize ? 0 : 1); } void ProjectSettingsEditor::setBorderMetatileIds(bool customSize, QList metatileIds) { if (customSize) { ui->lineEdit_BorderMetatiles->setText(Metatile::getMetatileIdStringList(metatileIds)); } else { ui->spinBox_BorderMetatile1->setValue(metatileIds.value(0, 0x0)); ui->spinBox_BorderMetatile2->setValue(metatileIds.value(1, 0x0)); ui->spinBox_BorderMetatile3->setValue(metatileIds.value(2, 0x0)); ui->spinBox_BorderMetatile4->setValue(metatileIds.value(3, 0x0)); } } QList ProjectSettingsEditor::getBorderMetatileIds(bool customSize) { QList metatileIds; if (customSize) { // Custom border size, read metatiles from line edit for (auto s : ui->lineEdit_BorderMetatiles->text().split(",")) { uint16_t metatileId = s.toUInt(nullptr, 0); metatileIds.append(qMin(metatileId, static_cast(Project::getNumMetatilesTotal() - 1))); } } else { // Default border size, read metatiles from spin boxes metatileIds.append(ui->spinBox_BorderMetatile1->value()); metatileIds.append(ui->spinBox_BorderMetatile2->value()); metatileIds.append(ui->spinBox_BorderMetatile3->value()); metatileIds.append(ui->spinBox_BorderMetatile4->value()); } return metatileIds; } // Show/hide warning for overlapping mask values. These are technically ok, but probably not intended. // Additionally, Porymap will not properly reflect that the values are linked. void ProjectSettingsEditor::updateMaskOverlapWarning(QLabel * warning, QList masks) { // Find any overlapping masks QMap overlapping; for (int i = 0; i < masks.length(); i++) for (int j = i + 1; j < masks.length(); j++) { if (masks.at(i)->value() & masks.at(j)->value()) overlapping[i] = overlapping[j] = true; } // It'de nice if we could style this as a persistent red border around the line edit for any // overlapping masks. As it is editing the border undesirably modifies the arrow buttons. // This stylesheet will just highlight the currently selected line edit, which is fine enough. static const QString styleSheet = "QAbstractSpinBox { selection-background-color: rgba(255, 0, 0, 25%) }"; // Update warning display if (warning) warning->setHidden(overlapping.isEmpty()); for (int i = 0; i < masks.length(); i++) masks.at(i)->setStyleSheet(overlapping.contains(i) ? styleSheet : ""); } void ProjectSettingsEditor::updateBlockMaskOverlapWarning() { const auto masks = QList{ ui->spinBox_MetatileIdMask, ui->spinBox_CollisionMask, ui->spinBox_ElevationMask, }; this->updateMaskOverlapWarning(ui->label_OverlapWarningBlocks, masks); } void ProjectSettingsEditor::updateAttributeMaskOverlapWarning() { const auto masks = QList{ ui->spinBox_BehaviorMask, ui->spinBox_LayerTypeMask, ui->spinBox_EncounterTypeMask, ui->spinBox_TerrainTypeMask, }; this->updateMaskOverlapWarning(ui->label_OverlapWarningMetatiles, masks); } void ProjectSettingsEditor::updateAttributeLimits(const QString &attrSize) { static const QMap limits { {"1", 0xFF}, {"2", 0xFFFF}, {"4", 0xFFFFFFFF}, }; uint32_t max = limits.value(attrSize, UINT_MAX); ui->spinBox_BehaviorMask->setMaximum(max); ui->spinBox_EncounterTypeMask->setMaximum(max); ui->spinBox_LayerTypeMask->setMaximum(max); ui->spinBox_TerrainTypeMask->setMaximum(max); } // Only one icon path is displayed at a time, so we need to keep track of the rest, // and update the path edit when the user changes the selected species. // The existing icon path map in ProjectConfig is left alone to allow unsaved changes. void ProjectSettingsEditor::updatePokemonIconPath(const QString &newSpecies) { if (!project) return; // If user was editing a path for a valid species, record filepath text before we wipe it. if (!this->prevIconSpecies.isEmpty() && this->project->speciesToIconPath.contains(this->prevIconSpecies)) this->editedPokemonIconPaths[this->prevIconSpecies] = ui->lineEdit_PokemonIcon->text(); QString editedPath = this->editedPokemonIconPaths.value(newSpecies); QString defaultPath = this->project->speciesToIconPath.value(newSpecies); ui->lineEdit_PokemonIcon->setText(this->stripProjectDir(editedPath)); ui->lineEdit_PokemonIcon->setPlaceholderText(this->stripProjectDir(defaultPath)); this->prevIconSpecies = newSpecies; } QStringList ProjectSettingsEditor::getWarpBehaviorsList() { return ui->textEdit_WarpBehaviors->toPlainText().split("\n"); } void ProjectSettingsEditor::setWarpBehaviorsList(QStringList list) { list.removeDuplicates(); list.sort(); ui->textEdit_WarpBehaviors->setText(list.join("\n")); } void ProjectSettingsEditor::updateWarpBehaviorsList(bool adding) { QString input = ui->comboBox_WarpBehaviors->currentText(); if (input.isEmpty()) return; // Check if input was a value string for a named behavior bool ok; uint32_t value = input.toUInt(&ok, 0); if (ok && project->metatileBehaviorMapInverse.contains(value)) input = project->metatileBehaviorMapInverse.value(value); if (!project->metatileBehaviorMap.contains(input)) return; QStringList list = this->getWarpBehaviorsList(); int pos = list.indexOf(input); if (adding && pos < 0) { // Add text to list list.prepend(input); } else if (!adding && pos >= 0) { // Remove text from list list.removeAt(pos); } else { // Trying to add text already in list, // or trying to remove text not in list return; } this->setWarpBehaviorsList(list); this->hasUnsavedChanges = true; } // Dynamically populate the tabs for project files and identifiers void ProjectSettingsEditor::createConfigTextTable(const QList> configPairs, bool filesTab) { for (auto pair : configPairs) { const QString idName = pair.first; const QString defaultText = pair.second; auto name = new QLabel(); name->setAlignment(Qt::AlignBottom); name->setText(idName); auto lineEdit = new QLineEdit(); lineEdit->setObjectName(idName); // Used when saving lineEdit->setPlaceholderText(defaultText); lineEdit->setClearButtonEnabled(true); // Add to list auto editArea = new QWidget(); auto layout = new QHBoxLayout(editArea); layout->addWidget(lineEdit); if (filesTab) { // "Choose file" button auto button = new QToolButton(); button->setIcon(QIcon(":/icons/folder.ico")); connect(button, &QAbstractButton::clicked, [this, lineEdit](bool) { const QString path = this->chooseProjectFile(lineEdit->placeholderText()); if (!path.isEmpty()) { lineEdit->setText(path); this->markEdited(); } }); layout->addWidget(button); ui->layout_ProjectPaths->addRow(name, editArea); } else { ui->layout_Identifiers->addRow(name, editArea); } } } void ProjectSettingsEditor::createProjectPathsTable() { auto pairs = ProjectConfig::defaultPaths.values(); this->createConfigTextTable(pairs, true); } void ProjectSettingsEditor::createProjectIdentifiersTable() { auto pairs = ProjectConfig::defaultIdentifiers.values(); this->createConfigTextTable(pairs, false); } QString ProjectSettingsEditor::chooseProjectFile(const QString &defaultFilepath) { const QString startDir = this->baseDir + defaultFilepath; QString path; if (defaultFilepath.endsWith("/")){ // Default filepath is a folder, choose a new folder path = QFileDialog::getExistingDirectory(this, "Choose Project File Folder", startDir) + QDir::separator(); } else{ // Default filepath is not a folder, choose a new file path = QFileDialog::getOpenFileName(this, "Choose Project File", startDir); } if (!path.startsWith(this->baseDir)){ // Most of Porymap's file-parsing code for project files will assume that filepaths // are relative to the root project folder, so we enforce that here. QMessageBox::warning(this, "Failed to set custom filepath", QString("Custom filepaths must be inside the root project folder '%1'").arg(this->baseDir)); return QString(); } return path.remove(0, this->baseDir.length()); } void ProjectSettingsEditor::restoreWindowState() { logInfo("Restoring project settings editor geometry from previous session."); const QMap geometry = porymapConfig.getProjectSettingsEditorGeometry(); this->restoreGeometry(geometry.value("project_settings_editor_geometry")); this->restoreState(geometry.value("project_settings_editor_state")); } // Set UI states using config data void ProjectSettingsEditor::refresh() { this->refreshing = true; // Block signals // Set combo box texts ui->comboBox_DefaultPrimaryTileset->setTextItem(projectConfig.getDefaultPrimaryTileset()); ui->comboBox_DefaultSecondaryTileset->setTextItem(projectConfig.getDefaultSecondaryTileset()); ui->comboBox_BaseGameVersion->setTextItem(projectConfig.getBaseGameVersionString()); ui->comboBox_AttributesSize->setTextItem(QString::number(projectConfig.getMetatileAttributesSize())); this->updateAttributeLimits(ui->comboBox_AttributesSize->currentText()); this->prevIconSpecies = QString(); this->editedPokemonIconPaths = projectConfig.getPokemonIconPaths(); this->updatePokemonIconPath(ui->comboBox_IconSpecies->currentText()); // Set check box states ui->checkBox_UsePoryscript->setChecked(projectConfig.getUsePoryScript()); ui->checkBox_ShowWildEncounterTables->setChecked(userConfig.getEncounterJsonActive()); ui->checkBox_CreateTextFile->setChecked(projectConfig.getCreateMapTextFileEnabled()); ui->checkBox_EnableTripleLayerMetatiles->setChecked(projectConfig.getTripleLayerMetatilesEnabled()); ui->checkBox_EnableRequiresItemfinder->setChecked(projectConfig.getHiddenItemRequiresItemfinderEnabled()); ui->checkBox_EnableQuantity->setChecked(projectConfig.getHiddenItemQuantityEnabled()); ui->checkBox_EnableCloneObjects->setChecked(projectConfig.getEventCloneObjectEnabled()); ui->checkBox_EnableWeatherTriggers->setChecked(projectConfig.getEventWeatherTriggerEnabled()); ui->checkBox_EnableSecretBases->setChecked(projectConfig.getEventSecretBaseEnabled()); ui->checkBox_EnableRespawn->setChecked(projectConfig.getHealLocationRespawnDataEnabled()); ui->checkBox_EnableAllowFlags->setChecked(projectConfig.getMapAllowFlagsEnabled()); ui->checkBox_EnableFloorNumber->setChecked(projectConfig.getFloorNumberEnabled()); ui->checkBox_EnableCustomBorderSize->setChecked(projectConfig.getUseCustomBorderSize()); ui->checkBox_OutputCallback->setChecked(projectConfig.getTilesetsHaveCallback()); ui->checkBox_OutputIsCompressed->setChecked(projectConfig.getTilesetsHaveIsCompressed()); ui->checkBox_DisableWarning->setChecked(porymapConfig.getWarpBehaviorWarningDisabled()); // Set spin box values ui->spinBox_Elevation->setValue(projectConfig.getDefaultElevation()); ui->spinBox_Collision->setValue(projectConfig.getDefaultCollision()); ui->spinBox_FillMetatile->setValue(projectConfig.getDefaultMetatileId()); ui->spinBox_MaxElevation->setValue(projectConfig.getCollisionSheetHeight() - 1); ui->spinBox_MaxCollision->setValue(projectConfig.getCollisionSheetWidth() - 1); ui->spinBox_BehaviorMask->setValue(projectConfig.getMetatileBehaviorMask() & ui->spinBox_BehaviorMask->maximum()); ui->spinBox_EncounterTypeMask->setValue(projectConfig.getMetatileEncounterTypeMask() & ui->spinBox_EncounterTypeMask->maximum()); ui->spinBox_LayerTypeMask->setValue(projectConfig.getMetatileLayerTypeMask() & ui->spinBox_LayerTypeMask->maximum()); ui->spinBox_TerrainTypeMask->setValue(projectConfig.getMetatileTerrainTypeMask() & ui->spinBox_TerrainTypeMask->maximum()); ui->spinBox_MetatileIdMask->setValue(projectConfig.getBlockMetatileIdMask() & ui->spinBox_MetatileIdMask->maximum()); ui->spinBox_CollisionMask->setValue(projectConfig.getBlockCollisionMask() & ui->spinBox_CollisionMask->maximum()); ui->spinBox_ElevationMask->setValue(projectConfig.getBlockElevationMask() & ui->spinBox_ElevationMask->maximum()); // Set (and sync) border metatile IDs auto metatileIds = projectConfig.getNewMapBorderMetatileIds(); this->setBorderMetatileIds(false, metatileIds); this->setBorderMetatileIds(true, metatileIds); // Set line edit texts ui->lineEdit_PrefabsPath->setText(projectConfig.getPrefabFilepath()); ui->lineEdit_CollisionGraphics->setText(projectConfig.getCollisionSheetPath()); ui->lineEdit_ObjectsIcon->setText(projectConfig.getEventIconPath(Event::Group::Object)); ui->lineEdit_WarpsIcon->setText(projectConfig.getEventIconPath(Event::Group::Warp)); ui->lineEdit_TriggersIcon->setText(projectConfig.getEventIconPath(Event::Group::Coord)); ui->lineEdit_BGsIcon->setText(projectConfig.getEventIconPath(Event::Group::Bg)); ui->lineEdit_HealspotsIcon->setText(projectConfig.getEventIconPath(Event::Group::Heal)); for (auto lineEdit : ui->scrollAreaContents_ProjectPaths->findChildren()) lineEdit->setText(projectConfig.getCustomFilePath(lineEdit->objectName())); for (auto lineEdit : ui->scrollAreaContents_Identifiers->findChildren()) lineEdit->setText(projectConfig.getCustomIdentifier(lineEdit->objectName())); // Set warp behaviors auto behaviorValues = projectConfig.getWarpBehaviors(); QStringList behaviorNames; for (auto value : behaviorValues) { if (project->metatileBehaviorMapInverse.contains(value)) behaviorNames.append(project->metatileBehaviorMapInverse.value(value)); } this->setWarpBehaviorsList(behaviorNames); this->refreshing = false; // Allow signals } void ProjectSettingsEditor::save() { if (!this->hasUnsavedChanges) return; // Prevent a call to save() for each of the config settings projectConfig.setSaveDisabled(true); // Save combo box settings projectConfig.setDefaultPrimaryTileset(ui->comboBox_DefaultPrimaryTileset->currentText()); projectConfig.setDefaultSecondaryTileset(ui->comboBox_DefaultSecondaryTileset->currentText()); projectConfig.setBaseGameVersion(projectConfig.stringToBaseGameVersion(ui->comboBox_BaseGameVersion->currentText())); projectConfig.setMetatileAttributesSize(ui->comboBox_AttributesSize->currentText().toInt()); // Save check box settings projectConfig.setUsePoryScript(ui->checkBox_UsePoryscript->isChecked()); userConfig.setEncounterJsonActive(ui->checkBox_ShowWildEncounterTables->isChecked()); projectConfig.setCreateMapTextFileEnabled(ui->checkBox_CreateTextFile->isChecked()); projectConfig.setTripleLayerMetatilesEnabled(ui->checkBox_EnableTripleLayerMetatiles->isChecked()); projectConfig.setHiddenItemRequiresItemfinderEnabled(ui->checkBox_EnableRequiresItemfinder->isChecked()); projectConfig.setHiddenItemQuantityEnabled(ui->checkBox_EnableQuantity->isChecked()); projectConfig.setEventCloneObjectEnabled(ui->checkBox_EnableCloneObjects->isChecked()); projectConfig.setEventWeatherTriggerEnabled(ui->checkBox_EnableWeatherTriggers->isChecked()); projectConfig.setEventSecretBaseEnabled(ui->checkBox_EnableSecretBases->isChecked()); projectConfig.setHealLocationRespawnDataEnabled(ui->checkBox_EnableRespawn->isChecked()); projectConfig.setMapAllowFlagsEnabled(ui->checkBox_EnableAllowFlags->isChecked()); projectConfig.setFloorNumberEnabled(ui->checkBox_EnableFloorNumber->isChecked()); projectConfig.setUseCustomBorderSize(ui->checkBox_EnableCustomBorderSize->isChecked()); projectConfig.setTilesetsHaveCallback(ui->checkBox_OutputCallback->isChecked()); projectConfig.setTilesetsHaveIsCompressed(ui->checkBox_OutputIsCompressed->isChecked()); porymapConfig.setWarpBehaviorWarningDisabled(ui->checkBox_DisableWarning->isChecked()); // Save spin box settings projectConfig.setDefaultElevation(ui->spinBox_Elevation->value()); projectConfig.setDefaultCollision(ui->spinBox_Collision->value()); projectConfig.setDefaultMetatileId(ui->spinBox_FillMetatile->value()); projectConfig.setCollisionSheetHeight(ui->spinBox_MaxElevation->value() + 1); projectConfig.setCollisionSheetWidth(ui->spinBox_MaxCollision->value() + 1); projectConfig.setMetatileBehaviorMask(ui->spinBox_BehaviorMask->value()); projectConfig.setMetatileTerrainTypeMask(ui->spinBox_TerrainTypeMask->value()); projectConfig.setMetatileEncounterTypeMask(ui->spinBox_EncounterTypeMask->value()); projectConfig.setMetatileLayerTypeMask(ui->spinBox_LayerTypeMask->value()); projectConfig.setBlockMetatileIdMask(ui->spinBox_MetatileIdMask->value()); projectConfig.setBlockCollisionMask(ui->spinBox_CollisionMask->value()); projectConfig.setBlockElevationMask(ui->spinBox_ElevationMask->value()); // Save line edit settings projectConfig.setPrefabFilepath(ui->lineEdit_PrefabsPath->text()); projectConfig.setCollisionSheetPath(ui->lineEdit_CollisionGraphics->text()); projectConfig.setEventIconPath(Event::Group::Object, ui->lineEdit_ObjectsIcon->text()); projectConfig.setEventIconPath(Event::Group::Warp, ui->lineEdit_WarpsIcon->text()); projectConfig.setEventIconPath(Event::Group::Coord, ui->lineEdit_TriggersIcon->text()); projectConfig.setEventIconPath(Event::Group::Bg, ui->lineEdit_BGsIcon->text()); projectConfig.setEventIconPath(Event::Group::Heal, ui->lineEdit_HealspotsIcon->text()); for (auto lineEdit : ui->scrollAreaContents_ProjectPaths->findChildren()) projectConfig.setFilePath(lineEdit->objectName(), lineEdit->text()); for (auto lineEdit : ui->scrollAreaContents_Identifiers->findChildren()) projectConfig.setIdentifier(lineEdit->objectName(), lineEdit->text()); // Save warp behaviors QStringList behaviorNames = this->getWarpBehaviorsList(); QSet behaviorValues; for (auto name : behaviorNames) behaviorValues.insert(project->metatileBehaviorMap.value(name)); projectConfig.setWarpBehaviors(behaviorValues); // Save border metatile IDs projectConfig.setNewMapBorderMetatileIds(this->getBorderMetatileIds(ui->checkBox_EnableCustomBorderSize->isChecked())); // Save pokemon icon paths const QString species = ui->comboBox_IconSpecies->currentText(); if (this->project->speciesToIconPath.contains(species)) this->editedPokemonIconPaths.insert(species, ui->lineEdit_PokemonIcon->text()); for (auto i = this->editedPokemonIconPaths.cbegin(), end = this->editedPokemonIconPaths.cend(); i != end; i++) projectConfig.setPokemonIconPath(i.key(), i.value()); projectConfig.setSaveDisabled(false); projectConfig.save(); this->hasUnsavedChanges = false; // Technically, a reload is not required for several of the config settings. // For simplicity we prompt the user to reload when any setting is changed regardless. this->projectNeedsReload = true; } // Pick a file to use as the new prefabs file path void ProjectSettingsEditor::choosePrefabsFile() { this->chooseFile(ui->lineEdit_PrefabsPath, "Choose Prefabs File", "JSON Files (*.json)"); } void ProjectSettingsEditor::chooseImageFile(QLineEdit * filepathEdit) { this->chooseFile(filepathEdit, "Choose Image File", "Images (*.png *.jpg)"); } void ProjectSettingsEditor::chooseFile(QLineEdit * filepathEdit, const QString &description, const QString &extensions) { QString filepath = QFileDialog::getOpenFileName(this, description, this->project->importExportPath, extensions); if (filepath.isEmpty()) return; this->project->setImportExportPath(filepath); if (filepathEdit) filepathEdit->setText(this->stripProjectDir(filepath)); this->hasUnsavedChanges = true; } // Display relative path if this file is in the project folder QString ProjectSettingsEditor::stripProjectDir(QString s) { if (s.startsWith(this->baseDir)) s.remove(0, this->baseDir.length()); return s; } void ProjectSettingsEditor::importDefaultPrefabsClicked(bool) { // If the prompt is accepted the prefabs file will be created and its filepath will be saved in the config. // No need to set hasUnsavedChanges here. BaseGameVersion version = projectConfig.stringToBaseGameVersion(ui->comboBox_BaseGameVersion->currentText()); if (prefab.tryImportDefaultPrefabs(this, version, ui->lineEdit_PrefabsPath->text())) { ui->lineEdit_PrefabsPath->setText(projectConfig.getPrefabFilepath()); // Refresh with new filepath this->projectNeedsReload = true; } } int ProjectSettingsEditor::prompt(const QString &text, QMessageBox::StandardButton defaultButton) { QMessageBox messageBox(this); messageBox.setText(text); messageBox.setIcon(QMessageBox::Question); messageBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No | defaultButton); messageBox.setDefaultButton(defaultButton); return messageBox.exec(); } bool ProjectSettingsEditor::promptSaveChanges() { if (!this->hasUnsavedChanges) return true; int result = this->prompt("Settings have been modified, save changes?", QMessageBox::Cancel); if (result == QMessageBox::Cancel) return false; if (result == QMessageBox::Yes) this->save(); else if (result == QMessageBox::No) this->hasUnsavedChanges = false; // Discarding changes return true; } bool ProjectSettingsEditor::promptRestoreDefaults() { if (this->refreshing) return false; const QString versionText = ui->comboBox_BaseGameVersion->currentText(); if (this->prompt(QString("Restore default config settings for %1?").arg(versionText)) == QMessageBox::No) return false; // Restore defaults by resetting config in memory, refreshing the UI, then restoring the config. // Don't want to save changes until user accepts them. ProjectConfig tempProject = projectConfig; projectConfig.reset(projectConfig.stringToBaseGameVersion(versionText)); this->refresh(); projectConfig = tempProject; this->hasUnsavedChanges = true; return true; } void ProjectSettingsEditor::dialogButtonClicked(QAbstractButton *button) { auto buttonRole = ui->buttonBox->buttonRole(button); if (buttonRole == QDialogButtonBox::AcceptRole) { // "OK" button this->save(); this->close(); } else if (buttonRole == QDialogButtonBox::RejectRole) { // "Cancel" button this->close(); } else if (buttonRole == QDialogButtonBox::ResetRole) { // "Restore Defaults" button this->promptRestoreDefaults(); } } // Close event triggered by a project reload. User doesn't need any prompts, just close the window. void ProjectSettingsEditor::closeQuietly() { // Turn off flags that trigger prompts this->hasUnsavedChanges = false; this->projectNeedsReload = false; this->close(); } void ProjectSettingsEditor::closeEvent(QCloseEvent* event) { if (!this->promptSaveChanges()) { event->ignore(); return; } porymapConfig.setProjectSettingsEditorGeometry( this->saveGeometry(), this->saveState() ); if (this->projectNeedsReload) { // Note: Declining this prompt with changes that need a reload may cause problems if (this->prompt("Settings changed, reload project to apply changes?") == QMessageBox::Yes) emit this->reloadProject(); } QMainWindow::closeEvent(event); }