diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index a28f1db9..8e586198 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -3061,6 +3061,7 @@ + @@ -3415,6 +3416,14 @@ QAction::ApplicationSpecificRole + + + Close Project + + + Ctrl+W + + diff --git a/include/config.h b/include/config.h index 7e87e986..3f77aa90 100644 --- a/include/config.h +++ b/include/config.h @@ -54,6 +54,7 @@ public: } virtual void reset() override { this->recentProjects.clear(); + this->projectManuallyClosed = false; this->reopenOnLaunch = true; this->mapSortOrder = MapSortOrder::Group; this->prettyCursors = true; @@ -99,6 +100,7 @@ public: QMap getCustomScriptsEditorGeometry(); bool reopenOnLaunch; + bool projectManuallyClosed; MapSortOrder mapSortOrder; bool prettyCursors; int collisionOpacity; diff --git a/include/editor.h b/include/editor.h index 837b267c..8e30ae70 100644 --- a/include/editor.h +++ b/include/editor.h @@ -107,13 +107,13 @@ public: void updateWarpEventWarnings(); bool eventLimitReached(Map *, Event::Type); - QGraphicsScene *scene = nullptr; + QPointer scene = nullptr; QGraphicsPixmapItem *current_view = nullptr; - MapPixmapItem *map_item = nullptr; + QPointer map_item = nullptr; ConnectionPixmapItem* selected_connection_item = nullptr; QList connection_items; QGraphicsPathItem *connection_mask = nullptr; - CollisionPixmapItem *collision_item = nullptr; + QPointer collision_item = nullptr; QGraphicsItemGroup *events_group = nullptr; QList borderItems; QList gridLines; @@ -121,16 +121,15 @@ public: CursorTileRect *cursorMapTileRect = nullptr; MapRuler *map_ruler = nullptr; - QGraphicsScene *scene_metatiles = nullptr; - QGraphicsScene *scene_current_metatile_selection = nullptr; - QGraphicsScene *scene_selected_border_metatiles = nullptr; - QGraphicsScene *scene_collision_metatiles = nullptr; - QGraphicsScene *scene_elevation_metatiles = nullptr; - MetatileSelector *metatile_selector_item = nullptr; + QPointer scene_metatiles = nullptr; + QPointer scene_current_metatile_selection = nullptr; + QPointer scene_selected_border_metatiles = nullptr; + QPointer scene_collision_metatiles = nullptr; + QPointer metatile_selector_item = nullptr; - BorderMetatilesPixmapItem *selected_border_metatiles_item = nullptr; + QPointer selected_border_metatiles_item = nullptr; CurrentSelectedMetatilesPixmapItem *current_metatile_selection_item = nullptr; - MovementPermissionsSelector *movement_permissions_selector_item = nullptr; + QPointer movement_permissions_selector_item = nullptr; QList *selected_events = nullptr; @@ -168,6 +167,18 @@ private: const QImage collisionPlaceholder = QImage(":/images/collisions_unknown.png"); QPixmap collisionSheetPixmap; + void clearMap(); + void clearMetatileSelector(); + void clearMovementPermissionSelector(); + void clearMapMetatiles(); + void clearMapMovementPermissions(); + void clearBorderMetatiles(); + void clearCurrentMetatilesSelection(); + void clearMapEvents(); + //void clearMapConnections(); + void clearMapBorder(); + void clearMapGrid(); + void clearWildMonTables(); void setConnectionItemsVisible(bool); void setBorderItemsVisible(bool, qreal = 1); void setConnectionEditControlValues(MapConnection*); diff --git a/include/mainwindow.h b/include/mainwindow.h index e65605e0..41bf8a1e 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -167,6 +167,7 @@ public slots: private slots: void on_action_Open_Project_triggered(); void on_action_Reload_Project_triggered(); + void on_action_Close_Project_triggered(); void on_mapList_activated(const QModelIndex &index); void on_action_Save_Project_triggered(); void openWarpMap(QString map_name, int event_id, Event::Group event_group); @@ -347,10 +348,11 @@ private: bool checkProjectSanity(); bool loadProjectData(); bool setProjectUI(); + void clearProjectUI(); void sortMapList(); void openSubWindow(QWidget * window); QString getExistingDirectory(QString); - bool openProject(const QString &dir, bool initial = false); + bool openProject(QString dir, bool initial = false); bool closeProject(); void showProjectOpenFailure(); void saveGlobalConfigs(); diff --git a/include/scripting.h b/include/scripting.h index fb96e7ae..e2bfb1ff 100644 --- a/include/scripting.h +++ b/include/scripting.h @@ -30,6 +30,7 @@ class Scripting { public: Scripting(MainWindow *mainWindow); + static void stop(); static void init(MainWindow *mainWindow); static void populateGlobalObject(MainWindow *mainWindow); static QJSEngine *getEngine(); diff --git a/src/config.cpp b/src/config.cpp index c294a0ee..16853e8f 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -198,9 +198,9 @@ KeyValueConfigBase::~KeyValueConfigBase() { void KeyValueConfigBase::load() { reset(); QFile file(this->getConfigFilepath()); - if (!file.exists()) + if (!file.exists()) { this->init(); - else if (!file.open(QIODevice::ReadOnly)) { + } else if (!file.open(QIODevice::ReadOnly)) { logError(QString("Could not open config file '%1': ").arg(this->getConfigFilepath()) + file.errorString()); } @@ -306,6 +306,8 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) { if (key == "recent_project") { this->recentProjects = value.split(",", Qt::SkipEmptyParts); this->recentProjects.removeDuplicates(); + } else if (key == "project_manually_closed") { + this->projectManuallyClosed = getConfigBool(key, value); } else if (key == "reopen_on_launch") { this->reopenOnLaunch = getConfigBool(key, value); } else if (key == "pretty_cursors") { @@ -417,6 +419,7 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) { QMap PorymapConfig::getKeyValueMap() { QMap map; map.insert("recent_project", this->recentProjects.join(",")); + map.insert("project_manually_closed", this->projectManuallyClosed ? "1" : "0"); map.insert("reopen_on_launch", this->reopenOnLaunch ? "1" : "0"); map.insert("pretty_cursors", this->prettyCursors ? "1" : "0"); map.insert("map_sort_order", mapSortOrderMap.value(this->mapSortOrder)); @@ -882,7 +885,7 @@ void ProjectConfig::init() { baseGameVersionComboBox->addItem("pokeemerald", BaseGameVersion::pokeemerald); form.addRow(new QLabel("Game Version"), baseGameVersionComboBox); - // TODO: Advanced button to open the project settings window (with some settings disabled) + // TODO: Add an 'Advanced' button to open the project settings window (with some settings disabled) QDialogButtonBox buttonBox(QDialogButtonBox::Ok, Qt::Horizontal, &dialog); QObject::connect(&buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); @@ -891,7 +894,7 @@ void ProjectConfig::init() { if (dialog.exec() == QDialog::Accepted) { this->baseGameVersion = static_cast(baseGameVersionComboBox->currentData().toInt()); } else { - // TODO: If user closes window Porymap assumes pokeemerald; it should instead abort project opening + logWarn(QString("No base_game_version selected, using default '%1'").arg(getBaseGameVersionString(this->baseGameVersion))); } } this->setUnreadKeys(); // Initialize version-specific options diff --git a/src/editor.cpp b/src/editor.cpp index 9557ee13..d3116af4 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -81,8 +81,9 @@ void Editor::saveUiFields() { void Editor::closeProject() { if (!this->project) return; - Scripting::cb_ProjectClosed(this->project->root); + Scripting::stop(); + clearMap(); delete this->project; } @@ -204,9 +205,8 @@ void Editor::setEditingConnections() { this->cursorMapTileRect->setActive(false); } -void Editor::displayWildMonTables() { +void Editor::clearWildMonTables() { QStackedWidget *stack = ui->stackedWidget_WildMons; - QComboBox *labelCombo = ui->comboBox_EncounterGroupLabel; // delete widgets from previous map data if they exist while (stack->count()) { @@ -215,18 +215,24 @@ void Editor::displayWildMonTables() { delete oldWidget; } - labelCombo->clear(); + ui->comboBox_EncounterGroupLabel->clear(); +} + +void Editor::displayWildMonTables() { + clearWildMonTables(); // Don't try to read encounter data if it doesn't exist on disk for this map. if (!project->wildMonData.contains(map->constantName)) { return; } + QComboBox *labelCombo = ui->comboBox_EncounterGroupLabel; for (auto groupPair : project->wildMonData[map->constantName]) labelCombo->addItem(groupPair.first); labelCombo->setCurrentText(labelCombo->itemText(0)); + QStackedWidget *stack = ui->stackedWidget_WildMons; int labelIndex = 0; for (auto labelPair : project->wildMonData[map->constantName]) { @@ -1337,6 +1343,35 @@ void Editor::mouseEvent_collision(QGraphicsSceneMouseEvent *event, CollisionPixm } } +// On project close we want to leave the editor view empty. +// Otherwise a map is normally only cleared when a new one is being displayed. +void Editor::clearMap() { + clearMetatileSelector(); + clearMovementPermissionSelector(); + clearMapMetatiles(); + clearMapMovementPermissions(); + clearBorderMetatiles(); + clearCurrentMetatilesSelection(); + clearMapEvents(); + //clearMapConnections(); + clearMapBorder(); + clearMapGrid(); + clearWildMonTables(); + + // TODO: Handle connections after redesign PR. + selected_connection_item = nullptr; + connection_items.clear(); + connection_mask = nullptr; + + current_view = nullptr; + map = nullptr; + + // These are normally preserved between map displays, we only delete them now. + delete scene; + delete metatile_selector_item; + delete movement_permissions_selector_item; +} + bool Editor::displayMap() { if (!scene) { scene = new QGraphicsScene; @@ -1346,12 +1381,6 @@ bool Editor::displayMap() { scene->installEventFilter(this->map_ruler); } - if (map_item && scene) { - scene->removeItem(map_item); - delete map_item; - scene->removeItem(this->map_ruler); - } - displayMetatileSelector(); displayMovementPermissionSelector(); displayMapMetatiles(); @@ -1379,11 +1408,16 @@ bool Editor::displayMap() { return true; } -void Editor::displayMetatileSelector() { +void Editor::clearMetatileSelector() { if (metatile_selector_item && metatile_selector_item->scene()) { metatile_selector_item->scene()->removeItem(metatile_selector_item); delete scene_metatiles; } +} + +void Editor::displayMetatileSelector() { + clearMetatileSelector(); + scene_metatiles = new QGraphicsScene; if (!metatile_selector_item) { metatile_selector_item = new MetatileSelector(8, map); @@ -1408,7 +1442,17 @@ void Editor::displayMetatileSelector() { scene_metatiles->addItem(metatile_selector_item); } +void Editor::clearMapMetatiles() { + if (map_item && scene) { + scene->removeItem(map_item); + delete map_item; + scene->removeItem(this->map_ruler); + } +} + void Editor::displayMapMetatiles() { + clearMapMetatiles(); + map_item = new MapPixmapItem(map, this->metatile_selector_item, this->settings); connect(map_item, &MapPixmapItem::mouseEvent, this, &Editor::mouseEvent_map); connect(map_item, &MapPixmapItem::startPaint, this, &Editor::onMapStartPaint); @@ -1429,11 +1473,16 @@ void Editor::displayMapMetatiles() { ); } -void Editor::displayMapMovementPermissions() { +void Editor::clearMapMovementPermissions() { if (collision_item && scene) { scene->removeItem(collision_item); delete collision_item; } +} + +void Editor::displayMapMovementPermissions() { + clearMapMovementPermissions(); + collision_item = new CollisionPixmapItem(map, ui->spinBox_SelectedCollision, ui->spinBox_SelectedElevation, this->metatile_selector_item, this->settings, &this->collisionOpacity); connect(collision_item, &CollisionPixmapItem::mouseEvent, this, &Editor::mouseEvent_collision); @@ -1446,11 +1495,16 @@ void Editor::displayMapMovementPermissions() { scene->addItem(collision_item); } -void Editor::displayBorderMetatiles() { +void Editor::clearBorderMetatiles() { if (selected_border_metatiles_item && selected_border_metatiles_item->scene()) { selected_border_metatiles_item->scene()->removeItem(selected_border_metatiles_item); delete selected_border_metatiles_item; + delete scene_selected_border_metatiles; } +} + +void Editor::displayBorderMetatiles() { + clearBorderMetatiles(); scene_selected_border_metatiles = new QGraphicsScene; selected_border_metatiles_item = new BorderMetatilesPixmapItem(map, this->metatile_selector_item); @@ -1465,11 +1519,17 @@ void Editor::displayBorderMetatiles() { this, &Editor::onBorderMetatilesChanged); } -void Editor::displayCurrentMetatilesSelection() { +void Editor::clearCurrentMetatilesSelection() { if (current_metatile_selection_item && current_metatile_selection_item->scene()) { current_metatile_selection_item->scene()->removeItem(current_metatile_selection_item); delete current_metatile_selection_item; + current_metatile_selection_item = nullptr; + delete scene_current_metatile_selection; } +} + +void Editor::displayCurrentMetatilesSelection() { + clearCurrentMetatilesSelection(); scene_current_metatile_selection = new QGraphicsScene; current_metatile_selection_item = new CurrentSelectedMetatilesPixmapItem(map, this->metatile_selector_item); @@ -1485,11 +1545,15 @@ void Editor::redrawCurrentMetatilesSelection() { } } -void Editor::displayMovementPermissionSelector() { +void Editor::clearMovementPermissionSelector() { if (movement_permissions_selector_item && movement_permissions_selector_item->scene()) { movement_permissions_selector_item->scene()->removeItem(movement_permissions_selector_item); delete scene_collision_metatiles; } +} + +void Editor::displayMovementPermissionSelector() { + clearMovementPermissionSelector(); scene_collision_metatiles = new QGraphicsScene; if (!movement_permissions_selector_item) { @@ -1507,7 +1571,7 @@ void Editor::displayMovementPermissionSelector() { scene_collision_metatiles->addItem(movement_permissions_selector_item); } -void Editor::displayMapEvents() { +void Editor::clearMapEvents() { if (events_group) { for (QGraphicsItem *child : events_group->childItems()) { events_group->removeFromGroup(child); @@ -1519,9 +1583,13 @@ void Editor::displayMapEvents() { } delete events_group; + events_group = nullptr; } - selected_events->clear(); +} + +void Editor::displayMapEvents() { + clearMapEvents(); events_group = new QGraphicsItemGroup; scene->addItem(events_group); @@ -1626,7 +1694,7 @@ void Editor::maskNonVisibleConnectionTiles() { connection_mask = scene->addPath(mask, pen, brush); } -void Editor::displayMapBorder() { +void Editor::clearMapBorder() { for (QGraphicsPixmapItem* item : borderItems) { if (item->scene()) { item->scene()->removeItem(item); @@ -1634,6 +1702,10 @@ void Editor::displayMapBorder() { delete item; } borderItems.clear(); +} + +void Editor::displayMapBorder() { + clearMapBorder(); int borderWidth = map->getBorderWidth(); int borderHeight = map->getBorderHeight(); @@ -1646,7 +1718,7 @@ void Editor::displayMapBorder() { item->setX(x * 16); item->setY(y * 16); item->setZValue(-3); - scene->addItem(item); + scene->addItem(item); // TODO: If the scene is taking ownership here is a double-free possible? borderItems.append(item); } } @@ -1689,11 +1761,15 @@ void Editor::onToggleGridClicked(bool checked) { ui->graphicsView_Map->scene()->update(); } -void Editor::displayMapGrid() { +void Editor::clearMapGrid() { for (QGraphicsLineItem* item : gridLines) { if (item) delete item; } gridLines.clear(); +} + +void Editor::displayMapGrid() { + clearMapGrid(); ui->checkBox_ToggleGrid->disconnect(); int pixelWidth = map->getWidth() * 16; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 88c12103..3f90e133 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -70,7 +70,7 @@ MainWindow::MainWindow(QWidget *parent) : logInfo(QString("Launching Porymap v%1").arg(QCoreApplication::applicationVersion())); this->initWindow(); - if (porymapConfig.reopenOnLaunch && this->openProject(porymapConfig.getRecentProject(), true)) + if (porymapConfig.reopenOnLaunch && !porymapConfig.projectManuallyClosed && this->openProject(porymapConfig.getRecentProject(), true)) on_toolButton_Paint_clicked(); // there is a bug affecting macOS users, where the trackpad deilveres a bad touch-release gesture @@ -498,7 +498,7 @@ void MainWindow::setTheme(QString theme) { } } -bool MainWindow::openProject(const QString &dir, bool initial) { +bool MainWindow::openProject(QString dir, bool initial) { if (dir.isNull() || dir.length() <= 0) { // If this happened on startup it's because the user has no recent projects, which is fine. // This shouldn't happen otherwise, but if it does then display an error. @@ -526,7 +526,7 @@ bool MainWindow::openProject(const QString &dir, bool initial) { // The above checks can fail and the user will be allowed to continue with their currently-opened project (if there is one). // We close the current project below, after which either the new project will open successfully or the window will be disabled. - if (!this->closeProject()) { + if (!closeProject()) { logInfo("Aborted project open."); return false; } @@ -577,6 +577,7 @@ bool MainWindow::openProject(const QString &dir, bool initial) { showWindowTitle(); this->statusBar()->showMessage(QString("Opened %1").arg(projectString)); + porymapConfig.projectManuallyClosed = false; porymapConfig.addRecentProject(dir); refreshRecentProjectsMenu(); @@ -600,12 +601,14 @@ bool MainWindow::checkProjectSanity() { if (editor->project->sanityCheck()) return true; + logWarn(QString("The directory '%1' failed the project sanity check.").arg(editor->project->root)); + QMessageBox msgBox; msgBox.setIcon(QMessageBox::Critical); msgBox.setText(QString("The selected directory appears to be invalid.")); msgBox.setInformativeText(QString("The directory '%1' is missing key files.\n\n" "Make sure you selected the correct project directory " - "(the one used to make your .gba file, e.g. 'pokeemerald').").arg(editor->project->root)); + "(the one used to make your .gba file, e.g. 'pokeemerald').").arg(editor->project->root)); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setDefaultButton(QMessageBox::Ok); auto tryAnyway = msgBox.addButton("Try Anyway", QMessageBox::ActionRole); @@ -721,6 +724,11 @@ void MainWindow::on_action_Reload_Project_triggered() { openProject(editor->project->root); } +void MainWindow::on_action_Close_Project_triggered() { + closeProject(); + porymapConfig.projectManuallyClosed = true; +} + bool MainWindow::setMap(QString map_name, bool scrollTreeView) { logInfo(QString("Setting map to '%1'").arg(map_name)); if (map_name.isEmpty()) { @@ -1047,6 +1055,29 @@ bool MainWindow::setProjectUI() { return true; } +void MainWindow::clearProjectUI() { + // Block signals to the comboboxes while they are being modified + const QSignalBlocker blocker1(ui->comboBox_Song); + const QSignalBlocker blocker2(ui->comboBox_Location); + const QSignalBlocker blocker3(ui->comboBox_PrimaryTileset); + const QSignalBlocker blocker4(ui->comboBox_SecondaryTileset); + const QSignalBlocker blocker5(ui->comboBox_Weather); + const QSignalBlocker blocker6(ui->comboBox_BattleScene); + const QSignalBlocker blocker7(ui->comboBox_Type); + + ui->comboBox_Song->clear(); + ui->comboBox_Location->clear(); + ui->comboBox_PrimaryTileset->clear(); + ui->comboBox_SecondaryTileset->clear(); + ui->comboBox_Weather->clear(); + ui->comboBox_BattleScene->clear(); + ui->comboBox_Type->clear(); + + // Clear map list + mapListModel->clear(); + mapGroupItemsList->clear(); +} + void MainWindow::sortMapList() { Project *project = editor->project; @@ -2514,8 +2545,12 @@ void MainWindow::importMapFromAdvanceMap1_92() void MainWindow::showExportMapImageWindow(ImageExporterMode mode) { if (!editor->project) return; - if (!this->mapImageExporter) - this->mapImageExporter = new MapImageExporter(this, this->editor, mode); + // If the user is requesting this window again we assume it's for a new + // window (the map/mode may have changed), so delete the old window. + if (this->mapImageExporter) + delete this->mapImageExporter; + + this->mapImageExporter = new MapImageExporter(this, this->editor, mode); openSubWindow(this->mapImageExporter); } @@ -2972,24 +3007,41 @@ bool MainWindow::askToFixRegionMapEditor() { } // Attempt to close any open sub-windows of the main window, giving each a chance to abort the process. -// Each of these are expected to be a QPointer to a widget with WA_DeleteOnClose set, so manually deleting -// and nullifying the pointer members is not necessary here. +// Each of these windows is a widget with WA_DeleteOnClose set, so manually deleting them isn't necessary. +// Because they're tracked with QPointers nullifying them shouldn't be necessary either, but it seems the +// delete is happening too late and some of the pointers haven't been cleared by the time we need them to, +// so we nullify them all here anyway. bool MainWindow::closeSupplementaryWindows() { if (this->tilesetEditor && !this->tilesetEditor->close()) return false; + this->tilesetEditor = nullptr; + if (this->regionMapEditor && !this->regionMapEditor->close()) return false; + this->regionMapEditor = nullptr; + if (this->mapImageExporter && !this->mapImageExporter->close()) return false; + this->mapImageExporter = nullptr; + if (this->newMapPrompt && !this->newMapPrompt->close()) return false; + this->newMapPrompt = nullptr; + if (this->shortcutsEditor && !this->shortcutsEditor->close()) return false; + this->shortcutsEditor = nullptr; + if (this->preferenceEditor && !this->preferenceEditor->close()) return false; + this->preferenceEditor = nullptr; + if (this->customScriptsEditor && !this->customScriptsEditor->close()) return false; + this->customScriptsEditor = nullptr; + if (this->projectSettingsEditor) this->projectSettingsEditor->closeQuietly(); + this->projectSettingsEditor = nullptr; return true; } @@ -3014,8 +3066,10 @@ bool MainWindow::closeProject() { return false; } } + clearProjectUI(); editor->closeProject(); setWindowDisabled(true); + setWindowTitle(QCoreApplication::applicationName()); return true; } diff --git a/src/scriptapi/scripting.cpp b/src/scriptapi/scripting.cpp index 112585c7..82944be9 100644 --- a/src/scriptapi/scripting.cpp +++ b/src/scriptapi/scripting.cpp @@ -21,14 +21,19 @@ QMap callbackFunctions = { Scripting *instance = nullptr; +void Scripting::stop() { + if (!instance) return; + instance->engine->setInterrupted(true); + instance->scriptUtility->clearActions(); + qDeleteAll(instance->imageCache); + delete instance; + instance = nullptr; +} + void Scripting::init(MainWindow *mainWindow) { - mainWindow->ui->graphicsView_Map->clearOverlayMap(); - if (instance) { - instance->engine->setInterrupted(true); - instance->scriptUtility->clearActions(); - qDeleteAll(instance->imageCache); - delete instance; - } + if (mainWindow->ui->graphicsView_Map) + mainWindow->ui->graphicsView_Map->clearOverlayMap(); + Scripting::stop(); instance = new Scripting(mainWindow); }