From d0101d807e985615771274e1eb7666ebdbf3dd5f Mon Sep 17 00:00:00 2001 From: GriffinR Date: Thu, 21 Nov 2024 15:04:42 -0500 Subject: [PATCH] Finish new layout dialog redesign --- forms/mainwindow.ui | 4 +- forms/newlayoutdialog.ui | 93 ++++++++----- forms/newmapdialog.ui | 178 ++++++++++++++---------- include/core/maplayout.h | 7 +- include/mainwindow.h | 10 +- include/project.h | 14 +- include/ui/mapheaderform.h | 4 +- include/ui/newlayoutdialog.h | 14 +- include/ui/newmapdialog.h | 22 +-- src/core/maplayout.cpp | 27 ++-- src/mainwindow.cpp | 183 ++++--------------------- src/project.cpp | 165 +++++++++++------------ src/ui/mapheaderform.cpp | 7 +- src/ui/newlayoutdialog.cpp | 104 +++++++------- src/ui/newmapdialog.cpp | 255 +++++++++++++++++++---------------- src/ui/newtilesetdialog.cpp | 3 +- 16 files changed, 529 insertions(+), 561 deletions(-) diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index 50e03e88..e9a3c440 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -2926,7 +2926,7 @@ - + @@ -3283,7 +3283,7 @@ Grid Settings... - + New Layout... diff --git a/forms/newlayoutdialog.ui b/forms/newlayoutdialog.ui index 8e623ba0..53d950fe 100644 --- a/forms/newlayoutdialog.ui +++ b/forms/newlayoutdialog.ui @@ -2,8 +2,16 @@ NewLayoutDialog + + + 0 + 0 + 264 + 173 + + - New Map Options + New Layout Options @@ -17,13 +25,46 @@ 0 0 238 - 146 + 106 10 + + + + false + + + color: rgb(255, 0, 0) + + + + + + true + + + + + + + + + + <html><head/><body><p>The constant that will be used to refer to this layout. It cannot be the same as any other existing layout.</p></body></html> + + + + + + + Layout Name + + + @@ -34,20 +75,13 @@ - <html><head/><body><p>The name of the new map. The name cannot be the same as any other existing map.</p></body></html> + <html><head/><body><p>The name of the new layout. The name cannot be the same as any other existing layout.</p></body></html> true - - - - Layout Name - - - @@ -64,32 +98,6 @@ - - - - false - - - color: rgb(255, 0, 0) - - - - - - true - - - - - - - <html><head/><body><p>The constant that will be used to refer to this map. It cannot be the same as any other existing map, and it must start with the specified prefix.</p></body></html> - - - - - - @@ -107,6 +115,19 @@ + + + + color: rgb(255, 0, 0) + + + + + + true + + + diff --git a/forms/newmapdialog.ui b/forms/newmapdialog.ui index ae3ba2d9..e6aece18 100644 --- a/forms/newmapdialog.ui +++ b/forms/newmapdialog.ui @@ -2,6 +2,14 @@ NewMapDialog + + + 0 + 0 + 255 + 320 + + New Map Options @@ -17,32 +25,51 @@ 0 0 229 - 254 + 306 10 - - + + + + false + + + color: rgb(255, 0, 0) + - Map ID + + + + true - - - - Qt::Orientation::Vertical + + + + true - - - 20 - 40 - + + QComboBox::InsertPolicy::NoInsert - + + + + + + color: rgb(255, 0, 0) + + + + + + true + + @@ -60,17 +87,21 @@ - - - - true - - - QComboBox::InsertPolicy::NoInsert + + + + <html><head/><body><p>The constant that will be used to refer to this map. It cannot be the same as any other existing map, and it must start with the specified prefix.</p></body></html> - + + + + Map ID + + + + @@ -88,39 +119,20 @@ - - - - false - - - color: rgb(255, 0, 0) - - - - - - true - - - - - - - <html><head/><body><p>The name of the new map. The name cannot be the same as any other existing map.</p></body></html> - - - true - - - - + Can Fly To + + + + Map Name + + + @@ -134,10 +146,23 @@ - - + + + + <html><head/><body><p>The name of the new map. The name cannot be the same as any other existing map.</p></body></html> + + + true + + + + + + + <html><head/><body><p>If checked, a Heal Location will be added to this map automatically.</p></body></html> + - Map Group + @@ -157,20 +182,13 @@ - - - - Map Name - - + + - - - - <html><head/><body><p>If checked, a Heal Location will be added to this map automatically.</p></body></html> - + + - + Map Group @@ -181,20 +199,36 @@ - - - - <html><head/><body><p>The constant that will be used to refer to this map. It cannot be the same as any other existing map, and it must start with the specified prefix.</p></body></html> + + + + Qt::Orientation::Vertical - - - - + + + 20 + 40 + + + + + + + color: rgb(255, 0, 0) + + + + + + true + + + diff --git a/include/core/maplayout.h b/include/core/maplayout.h index caef753a..8dea0795 100644 --- a/include/core/maplayout.h +++ b/include/core/maplayout.h @@ -21,6 +21,7 @@ class Layout : public QObject { public: Layout() {} + static QString layoutNameFromMapName(const QString &mapName); static QString layoutConstantFromName(QString mapName); bool loaded = false; @@ -83,10 +84,10 @@ public: QString primaryTilesetLabel; QString secondaryTilesetLabel; }; + Settings settings() const; -public: - Layout *copy(); - void copyFrom(Layout *other); + Layout *copy() const; + void copyFrom(const Layout *other); int getWidth() const { return width; } int getHeight() const { return height; } diff --git a/include/mainwindow.h b/include/mainwindow.h index 53889dff..775d364e 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -22,8 +22,6 @@ #include "mapimageexporter.h" #include "filterchildrenproxymodel.h" #include "maplistmodels.h" -#include "newmapdialog.h" -#include "newlayoutdialog.h" #include "newtilesetdialog.h" #include "shortcutseditor.h" #include "preferenceeditor.h" @@ -188,8 +186,6 @@ private slots: void onLayoutChanged(Layout *layout); void onOpenConnectedMap(MapConnection*); void onTilesetsSaved(QString, QString); - void openNewMapDialog(); - void openNewLayoutDialog(); void onNewMapCreated(Map *newMap, const QString &groupName); void onNewMapGroupCreated(const QString &groupName); void onNewLayoutCreated(Layout *layout); @@ -199,7 +195,6 @@ private slots: void markMapEdited(); void markSpecificMapEdited(Map*); - void on_action_NewMap_triggered(); void on_actionNew_Tileset_triggered(); void on_action_Save_triggered(); void on_action_Exit_triggered(); @@ -306,8 +301,6 @@ private: QPointer regionMapEditor = nullptr; QPointer shortcutsEditor = nullptr; QPointer mapImageExporter = nullptr; - QPointer newMapDialog = nullptr; - QPointer newLayoutDialog = nullptr; QPointer preferenceEditor = nullptr; QPointer projectSettingsEditor = nullptr; QPointer gridSettingsDialog = nullptr; @@ -353,6 +346,8 @@ private: bool setProjectUI(); void clearProjectUI(); + void openNewMapDialog(); + void openNewLayoutDialog(); void openSubWindow(QWidget * window); void scrollMapList(MapTree *list, QString itemName); void scrollMapListToCurrentMap(MapTree *list); @@ -371,7 +366,6 @@ private: void updateMapList(); void mapListAddGroup(); - void mapListAddLayout(); void mapListAddArea(); void openMapListItem(const QModelIndex &index); void saveMapListTab(int index); diff --git a/include/project.h b/include/project.h index c1a36a80..41593350 100644 --- a/include/project.h +++ b/include/project.h @@ -82,8 +82,8 @@ public: bool saveEmptyMapsec; struct NewMapSettings { - QString mapName; - QString mapId; + QString name; + QString id; QString group; bool canFlyTo; Layout::Settings layout; @@ -133,9 +133,9 @@ public: void addNewMap(Map* newMap, const QString &groupName); void addNewMapGroup(const QString &groupName); void addNewLayout(Layout* newLayout); - QString getNewMapName(); - QString getNewLayoutName(); - bool isLayoutNameUnique(const QString &name); + NewMapSettings getNewMapSettings() const; + Layout::Settings getNewLayoutSettings() const; + bool isIdentifierUnique(const QString &identifier) const; QString getProjectTitle(); bool readWildMonData(); @@ -159,7 +159,7 @@ public: bool loadMapData(Map*); bool readMapLayouts(); Layout *loadLayout(QString layoutId); - Layout *createNewLayout(const Layout::Settings &layoutSettings); + Layout *createNewLayout(const Layout::Settings &layoutSettings, const Layout* toDuplicate = nullptr); bool loadLayout(Layout *); bool loadMapLayout(Map*); bool loadLayoutTilesets(Layout *); @@ -234,8 +234,6 @@ public: static QString getExistingFilepath(QString filepath); void applyParsedLimits(); - void initNewMapSettings(); - void initNewLayoutSettings(); static QString getDynamicMapDefineName(); static QString getDynamicMapName(); diff --git a/include/ui/mapheaderform.h b/include/ui/mapheaderform.h index 897b8e9a..136499e5 100644 --- a/include/ui/mapheaderform.h +++ b/include/ui/mapheaderform.h @@ -32,11 +32,13 @@ public: MapHeader headerData() const; void setLocations(QStringList locations); - void setLocationsDisabled(bool disabled); + void setLocationDisabled(bool disabled); + bool isLocationDisabled() const { return m_locationDisabled; } private: Ui::MapHeaderForm *ui; QPointer m_header = nullptr; + bool m_locationDisabled = false; void updateUi(); void updateSong(); diff --git a/include/ui/newlayoutdialog.h b/include/ui/newlayoutdialog.h index c90a408b..ba4396b9 100644 --- a/include/ui/newlayoutdialog.h +++ b/include/ui/newlayoutdialog.h @@ -18,10 +18,11 @@ class NewLayoutDialog : public QDialog { Q_OBJECT public: - explicit NewLayoutDialog(QWidget *parent = nullptr, Project *project = nullptr); + explicit NewLayoutDialog(Project *project, QWidget *parent = nullptr); + explicit NewLayoutDialog(Project *project, const Layout *layoutToCopy, QWidget *parent = nullptr); ~NewLayoutDialog(); - void copyFrom(const Layout &); - void accept() override; + + virtual void accept() override; signals: void applied(const QString &newLayoutId); @@ -30,18 +31,21 @@ private: Ui::NewLayoutDialog *ui; Project *project; Layout *importedLayout = nullptr; - Layout::Settings *settings = nullptr; + + static Layout::Settings settings; + static bool initializedSettings; // Each of these validation functions will allow empty names up until `OK` is selected, // because clearing the text during editing is common and we don't want to flash errors for this. bool validateLayoutID(bool allowEmpty = false); bool validateName(bool allowEmpty = false); + void refresh(); + void saveSettings(); bool isExistingLayout() const; private slots: - //void on_comboBox_Layout_currentTextChanged(const QString &text);//TODO void dialogButtonClicked(QAbstractButton *button); void on_lineEdit_Name_textChanged(const QString &); void on_lineEdit_LayoutID_textChanged(const QString &); diff --git a/include/ui/newmapdialog.h b/include/ui/newmapdialog.h index 8a769740..cdc324e2 100644 --- a/include/ui/newmapdialog.h +++ b/include/ui/newmapdialog.h @@ -18,12 +18,12 @@ class NewMapDialog : public QDialog { Q_OBJECT public: - explicit NewMapDialog(QWidget *parent = nullptr, Project *project = nullptr); + explicit NewMapDialog(Project *project, QWidget *parent = nullptr); + explicit NewMapDialog(Project *project, int mapListTab, const QString &mapListItem, QWidget *parent = nullptr); + explicit NewMapDialog(Project *project, const Map *mapToCopy, QWidget *parent = nullptr); ~NewMapDialog(); - void init(); - void init(int tabIndex, QString data); - void init(Layout *); - void accept() override; + + virtual void accept() override; signals: void applied(const QString &newMapName); @@ -33,25 +33,29 @@ private: Project *project; CollapsibleSection *headerSection; MapHeaderForm *headerForm; - Layout *importedLayout = nullptr; - Project::NewMapSettings *settings = nullptr; + Map *importedMap = nullptr; + + static Project::NewMapSettings settings; + static bool initializedSettings; // Each of these validation functions will allow empty names up until `OK` is selected, // because clearing the text during editing is common and we don't want to flash errors for this. bool validateMapID(bool allowEmpty = false); bool validateName(bool allowEmpty = false); bool validateGroup(bool allowEmpty = false); + bool validateLayoutID(bool allowEmpty = false); + + void refresh(); void saveSettings(); - bool isExistingLayout() const; void useLayoutSettings(const Layout *mapLayout); - void useLayoutIdSettings(const QString &layoutId); private slots: void dialogButtonClicked(QAbstractButton *button); void on_lineEdit_Name_textChanged(const QString &); void on_lineEdit_MapID_textChanged(const QString &); void on_comboBox_Group_currentTextChanged(const QString &text); + void on_comboBox_LayoutID_currentTextChanged(const QString &text); }; #endif // NEWMAPDIALOG_H diff --git a/src/core/maplayout.cpp b/src/core/maplayout.cpp index 11e9943d..921c0894 100644 --- a/src/core/maplayout.cpp +++ b/src/core/maplayout.cpp @@ -7,13 +7,13 @@ -Layout *Layout::copy() { +Layout *Layout::copy() const { Layout *layout = new Layout; layout->copyFrom(this); return layout; } -void Layout::copyFrom(Layout *other) { +void Layout::copyFrom(const Layout *other) { this->id = other->id; this->name = other->name; this->width = other->width; @@ -30,19 +30,30 @@ void Layout::copyFrom(Layout *other) { this->border = other->border; } +QString Layout::layoutNameFromMapName(const QString &mapName) { + return QString("%1_Layout").arg(mapName); +} + QString Layout::layoutConstantFromName(QString mapName) { // Transform map names of the form 'GraniteCave_B1F` into layout constants like 'LAYOUT_GRANITE_CAVE_B1F'. static const QRegularExpression caseChange("([a-z])([A-Z])"); QString nameWithUnderscores = mapName.replace(caseChange, "\\1_\\2"); QString withMapAndUppercase = "LAYOUT_" + nameWithUnderscores.toUpper(); static const QRegularExpression underscores("_+"); - QString constantName = withMapAndUppercase.replace(underscores, "_"); + return withMapAndUppercase.replace(underscores, "_"); +} - // Handle special cases. - // SSTidal should be SS_TIDAL, rather than SSTIDAL - constantName = constantName.replace("SSTIDAL", "SS_TIDAL"); - - return constantName; +Layout::Settings Layout::settings() const { + Layout::Settings settings; + settings.id = this->id; + settings.name = this->name; + settings.width = this->width; + settings.height = this->height; + settings.borderWidth = this->border_width; + settings.borderHeight = this->border_height; + settings.primaryTilesetLabel = this->tileset_primary_label; + settings.secondaryTilesetLabel = this->tileset_secondary_label; + return settings; } bool Layout::isWithinBounds(int x, int y) { diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 7af3d100..41f85724 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -23,6 +23,8 @@ #include "newmapconnectiondialog.h" #include "config.h" #include "filedialog.h" +#include "newmapdialog.h" +#include "newlayoutdialog.h" #include #include @@ -291,7 +293,8 @@ void MainWindow::initExtraSignals() { label_MapRulerStatus->setTextFormat(Qt::PlainText); label_MapRulerStatus->setTextInteractionFlags(Qt::TextSelectableByMouse); - connect(ui->actionNew_Layout, &QAction::triggered, this, &MainWindow::openNewLayoutDialog); + connect(ui->action_NewMap, &QAction::triggered, this, &MainWindow::openNewMapDialog); + connect(ui->action_NewLayout, &QAction::triggered, this, &MainWindow::openNewLayoutDialog); } void MainWindow::on_actionCheck_for_Updates_triggered() { @@ -406,7 +409,7 @@ void MainWindow::initMapList() { // Create add map/layout button // TODO: Tool tip QPushButton *buttonAdd = new QPushButton(QIcon(":/icons/add.ico"), ""); - connect(buttonAdd, &QPushButton::clicked, this, &MainWindow::on_action_NewMap_triggered); + connect(buttonAdd, &QPushButton::clicked, this, &MainWindow::openNewMapDialog); layout->addWidget(buttonAdd); /* TODO: Remove button disabled, no current support for deleting maps/layouts @@ -932,6 +935,10 @@ bool MainWindow::userSetLayout(QString layoutId) { msgBox.critical(nullptr, "Error Opening Layout", errorMsg); return false; } + + // Only the Layouts tab of the map list shows Layouts, so if we're not already on that tab we'll open it now. + ui->mapListContainer->setCurrentIndex(MapListTab::Layouts); + return true; } @@ -1228,8 +1235,9 @@ void MainWindow::onOpenMapListContextMenu(const QPoint &point) { if (addToFolderAction) { // All folders only contain maps, so adding an item to any folder is adding a new map. connect(addToFolderAction, &QAction::triggered, [this, itemName] { - openNewMapDialog(); - this->newMapDialog->init(ui->mapListContainer->currentIndex(), itemName); + auto dialog = new NewMapDialog(this->editor->project, ui->mapListContainer->currentIndex(), itemName, this); + connect(dialog, &NewMapDialog::applied, this, &MainWindow::userSetMap); + dialog->open(); }); } if (deleteFolderAction) { @@ -1267,8 +1275,8 @@ void MainWindow::mapListAddGroup() { connect(&newItemButtonBox, &QDialogButtonBox::accepted, [&](){ const QString mapGroupName = newNameEdit->text(); - if (this->editor->project->groupNames.contains(mapGroupName)) { - errorMessageLabel->setText(QString("A map group with the name '%1' already exists").arg(mapGroupName)); + if (!this->editor->project->isIdentifierUnique(mapGroupName)) { + errorMessageLabel->setText(QString("The name '%1' is not unique.").arg(mapGroupName)); errorMessageLabel->setVisible(true); } else { dialog.accept(); @@ -1288,126 +1296,6 @@ void MainWindow::mapListAddGroup() { } } -// TODO: Pull this all out into a custom window. Connect that to an action in the main menu as well. -// (or, re-use the new map dialog with some tweaks) -// TODO: This needs to take the same default settings you would get for a new map (tilesets, dimensions, etc.) -// and initialize it with the same fill settings (default metatile/collision/elevation, default border) -// TODO: Remove -void MainWindow::mapListAddLayout() { - /* - if (!editor || !editor->project) return; - - QDialog dialog(this, Qt::WindowTitleHint | Qt::WindowCloseButtonHint); - dialog.setWindowModality(Qt::ApplicationModal); - QDialogButtonBox newItemButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &dialog); - connect(&newItemButtonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); - - QLineEdit *newNameEdit = new QLineEdit(&dialog); - newNameEdit->setClearButtonEnabled(true); - - static const QRegularExpression re_validChars("[A-Za-z_]+[\\w]*"); - newNameEdit->setValidator(new QRegularExpressionValidator(re_validChars, newNameEdit)); - - // TODO: Support arbitrary LAYOUT_ ID names (Note from GriffinR: This is already handled in an unopened PR) - QLabel *newId = new QLabel("LAYOUT_", &dialog); - connect(newNameEdit, &QLineEdit::textChanged, [&](QString text){ - newId->setText(Layout::layoutConstantFromName(text.remove("_Layout"))); - }); - - NoScrollComboBox *useExistingCombo = new NoScrollComboBox(&dialog); - useExistingCombo->addItems(this->editor->project->layoutIds); - useExistingCombo->setEnabled(false); - - QCheckBox *useExistingCheck = new QCheckBox(&dialog); - - QLabel *errorMessageLabel = new QLabel(&dialog); - errorMessageLabel->setVisible(false); - errorMessageLabel->setStyleSheet("QLabel { background-color: rgba(255, 0, 0, 25%) }"); - - QComboBox *primaryCombo = new QComboBox(&dialog); - primaryCombo->addItems(this->editor->project->primaryTilesetLabels); - QComboBox *secondaryCombo = new QComboBox(&dialog); - secondaryCombo->addItems(this->editor->project->secondaryTilesetLabels); - - QSpinBox *widthSpin = new QSpinBox(&dialog); - QSpinBox *heightSpin = new QSpinBox(&dialog); - - widthSpin->setMinimum(1); - heightSpin->setMinimum(1); - widthSpin->setMaximum(this->editor->project->getMaxMapWidth()); - heightSpin->setMaximum(this->editor->project->getMaxMapHeight()); - - connect(useExistingCheck, &QCheckBox::stateChanged, [&](int state){ - bool useExisting = (state == Qt::Checked); - useExistingCombo->setEnabled(useExisting); - primaryCombo->setEnabled(!useExisting); - secondaryCombo->setEnabled(!useExisting); - widthSpin->setEnabled(!useExisting); - heightSpin->setEnabled(!useExisting); - }); - - QFormLayout form(&dialog); - form.addRow("New Layout Name", newNameEdit); - form.addRow("New Layout ID", newId); - form.addRow("Copy Existing Layout", useExistingCheck); - form.addRow("", useExistingCombo); - form.addRow("Primary Tileset", primaryCombo); - form.addRow("Secondary Tileset", secondaryCombo); - form.addRow("Layout Width", widthSpin); - form.addRow("Layout Height", heightSpin); - form.addRow("", errorMessageLabel); - - connect(&newItemButtonBox, &QDialogButtonBox::accepted, [&](){ - // verify some things - QString errorMessage; - QString tryLayoutName = newNameEdit->text(); - // name not empty - if (tryLayoutName.isEmpty()) { - errorMessage = "Name cannot be empty"; - } - // unique layout name & id - else if (this->editor->project->layoutIds.contains(newId->text()) - || this->editor->project->layoutIdsToNames.find(tryLayoutName) != this->editor->project->layoutIdsToNames.end()) { - errorMessage = "Layout Name / ID is not unique"; - } - // from id is existing value - else if (useExistingCheck->isChecked()) { - if (!this->editor->project->layoutIds.contains(useExistingCombo->currentText())) { - errorMessage = "Existing layout ID is not valid"; - } - } - - if (!errorMessage.isEmpty()) { - // show error - errorMessageLabel->setText(errorMessage); - errorMessageLabel->setVisible(true); - } - else { - dialog.accept(); - } - }); - - form.addRow(&newItemButtonBox); - - if (dialog.exec() == QDialog::Accepted) { - Layout::SimpleSettings layoutSettings; - QString layoutName = newNameEdit->text(); - layoutSettings.name = layoutName; - layoutSettings.id = Layout::layoutConstantFromName(layoutName.remove("_Layout")); - if (useExistingCheck->isChecked()) { - layoutSettings.from_id = useExistingCombo->currentText(); - } else { - layoutSettings.width = widthSpin->value(); - layoutSettings.height = heightSpin->value(); - layoutSettings.tileset_primary_label = primaryCombo->currentText(); - layoutSettings.tileset_secondary_label = secondaryCombo->currentText(); - } - Layout *newLayout = this->editor->project->createNewLayout(layoutSettings); - setLayout(newLayout->id); - } - */ -} - void MainWindow::mapListAddArea() { QDialog dialog(this, Qt::WindowTitleHint | Qt::WindowCloseButtonHint); dialog.setWindowModality(Qt::ApplicationModal); @@ -1433,8 +1321,8 @@ void MainWindow::mapListAddArea() { connect(&newItemButtonBox, &QDialogButtonBox::accepted, [&](){ const QString newAreaName = newNameDisplay->text(); - if (this->editor->project->mapSectionIdNames.contains(newAreaName)){ - errorMessageLabel->setText(QString("An area with the name '%1' already exists").arg(newAreaName)); + if (!this->editor->project->isIdentifierUnique(newAreaName)) { + errorMessageLabel->setText(QString("The name '%1' is not unique.").arg(newAreaName)); errorMessageLabel->setVisible(true); } else { dialog.accept(); @@ -1485,6 +1373,7 @@ void MainWindow::onNewMapCreated(Map *newMap, const QString &groupName) { } } +// Called any time a new layout is created (including as a byproduct of creating a new map) void MainWindow::onNewLayoutCreated(Layout *layout) { logInfo(QString("Created a new layout named %1.").arg(layout->name)); @@ -1504,31 +1393,16 @@ void MainWindow::onNewMapGroupCreated(const QString &groupName) { this->mapGroupModel->insertGroupItem(groupName); } -// TODO: This and the new layout dialog are modal. We shouldn't need to reference their dialogs outside these open functions, -// so we should be able to remove them as members of MainWindow. -// (plus, the opening then init() call after showing for NewMapDialog is Bad) void MainWindow::openNewMapDialog() { - if (!this->newMapDialog) { - this->newMapDialog = new NewMapDialog(this, this->editor->project); - connect(this->newMapDialog, &NewMapDialog::applied, this, &MainWindow::userSetMap); - } - - openSubWindow(this->newMapDialog); -} - -void MainWindow::on_action_NewMap_triggered() { - openNewMapDialog(); - //this->newMapDialog->initUi();//TODO - this->newMapDialog->init(); + auto dialog = new NewMapDialog(this->editor->project, this); + connect(dialog, &NewMapDialog::applied, this, &MainWindow::userSetMap); + dialog->open(); } void MainWindow::openNewLayoutDialog() { - if (!this->newLayoutDialog) { - this->newLayoutDialog = new NewLayoutDialog(this, this->editor->project); - connect(this->newLayoutDialog, &NewLayoutDialog::applied, this, &MainWindow::userSetLayout); - } - - openSubWindow(this->newLayoutDialog); + auto dialog = new NewLayoutDialog(this->editor->project, this); + connect(dialog, &NewLayoutDialog::applied, this, &MainWindow::userSetLayout); + dialog->open(); } // Insert label for newly-created tileset into sorted list of existing labels @@ -2732,8 +2606,9 @@ void MainWindow::on_actionImport_Layout_from_Advance_Map_1_92_triggered() { return; } - openNewLayoutDialog(); - this->newLayoutDialog->copyFrom(*mapLayout); + auto dialog = new NewLayoutDialog(this->editor->project, mapLayout, this); + connect(dialog, &NewLayoutDialog::applied, this, &MainWindow::userSetLayout); + dialog->open(); delete mapLayout; } @@ -2756,7 +2631,7 @@ void MainWindow::on_pushButton_AddConnection_clicked() { auto dialog = new NewMapConnectionDialog(this, this->editor->map, this->editor->project->mapNames); connect(dialog, &NewMapConnectionDialog::accepted, this->editor, &Editor::addConnection); - dialog->exec(); + dialog->open(); } void MainWindow::on_pushButton_NewWildMonGroup_clicked() { @@ -3244,10 +3119,6 @@ bool MainWindow::closeSupplementaryWindows() { return false; this->mapImageExporter = nullptr; - if (this->newMapDialog && !this->newMapDialog->close()) - return false; - this->newMapDialog = nullptr; - if (this->shortcutsEditor && !this->shortcutsEditor->close()) return false; this->shortcutsEditor = nullptr; diff --git a/src/project.cpp b/src/project.cpp index 2f37e24a..9976c549 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -107,7 +107,6 @@ bool Project::load() { && readSongNames() && readMapGroups(); applyParsedLimits(); - initNewMapSettings(); return success; } @@ -348,12 +347,7 @@ bool Project::loadMapData(Map* map) { const QString direction = ParseUtil::jsonToQString(connectionObj["direction"]); const int offset = ParseUtil::jsonToInt(connectionObj["offset"]); const QString mapConstant = ParseUtil::jsonToQString(connectionObj["map"]); - if (this->mapConstantsToMapNames.contains(mapConstant)) { - // Successully read map connection - map->loadConnection(new MapConnection(this->mapConstantsToMapNames.value(mapConstant), direction, offset)); - } else { - logError(QString("Failed to find connected map for map constant '%1'").arg(mapConstant)); - } + map->loadConnection(new MapConnection(this->mapConstantsToMapNames.value(mapConstant, mapConstant), direction, offset)); } } @@ -369,40 +363,7 @@ bool Project::loadMapData(Map* map) { return true; } -/* -void Project::addNewLayout(Layout* newLayout) { - - if (newLayout->blockdata.isEmpty()) { - // Fill layout using default fill settings - setNewLayoutBlockdata(newLayout); - } - if (newLayout->border.isEmpty()) { - // Fill border using default fill settings - setNewLayoutBorder(newLayout); - } - - emit layoutAdded(newLayout); -} -*/ - -// TODO: Fold back into createNewLayout? -/* -Layout *Project::duplicateLayout(const Layout *toDuplicate) { - //TODO - if (!settings.from_id.isEmpty()) { - // load from layout - loadLayout(mapLayouts[settings.from_id]); - layout = mapLayouts[settings.from_id]->copy(); - layout->name = settings.name; - layout->id = settings.id; - layout->border_path = QString("%1%2/border.bin").arg(basePath, layout->name); - layout->blockdata_path = QString("%1%2/map.bin").arg(basePath, layout->name); - } -} -*/ - -// TODO: Refactor, we're duplicating logic between here, the new map dialog, and addNewLayout -Layout *Project::createNewLayout(const Layout::Settings &settings) { +Layout *Project::createNewLayout(const Layout::Settings &settings, const Layout *toDuplicate) { Layout *layout = new Layout; layout->id = settings.id; layout->name = settings.name; @@ -413,6 +374,13 @@ Layout *Project::createNewLayout(const Layout::Settings &settings) { layout->tileset_primary_label = settings.primaryTilesetLabel; layout->tileset_secondary_label = settings.secondaryTilesetLabel; + if (toDuplicate) { + // If we're duplicating an existing layout we'll copy over the blockdata. + // Otherwise addNewLayout will fill our new layout using the default settings. + layout->blockdata = toDuplicate->blockdata; + layout->border = toDuplicate->border; + } + const QString basePath = projectConfig.getFilePath(ProjectFilePath::data_layouts_folders); layout->border_path = QString("%1%2/border.bin").arg(basePath, layout->name); layout->blockdata_path = QString("%1%2/map.bin").arg(basePath, layout->name); @@ -1276,6 +1244,7 @@ void Project::saveMap(Map *map) { } appendTextFile(root + "/" + projectConfig.getFilePath(ProjectFilePath::data_event_scripts), text); + // TODO: Either simplify this redundancy or explain why we need it (to create folders without the _Layout suffix) if (map->needsLayoutDir()) { QString newLayoutDir = QString(root + "/%1%2").arg(projectConfig.getFilePath(ProjectFilePath::data_layouts_folders), map->name()); if (!QDir::root().mkdir(newLayoutDir)) { @@ -1314,19 +1283,15 @@ void Project::saveMap(Map *map) { mapObj["battle_scene"] = map->header()->battleScene(); // Connections - auto connections = map->getConnections(); + const auto connections = map->getConnections(); if (connections.length() > 0) { OrderedJson::array connectionsArr; - for (auto connection : connections) { - if (this->mapNamesToMapConstants.contains(connection->targetMapName())) { - OrderedJson::object connectionObj; - connectionObj["map"] = this->mapNamesToMapConstants.value(connection->targetMapName()); - connectionObj["offset"] = connection->offset(); - connectionObj["direction"] = connection->direction(); - connectionsArr.append(connectionObj); - } else { - logError(QString("Failed to write map connection. '%1' is not a valid map name").arg(connection->targetMapName())); - } + for (const auto &connection : connections) { + OrderedJson::object connectionObj; + connectionObj["map"] = this->mapNamesToMapConstants.value(connection->targetMapName(), connection->targetMapName()); + connectionObj["offset"] = connection->offset(); + connectionObj["direction"] = connection->direction(); + connectionsArr.append(connectionObj); } mapObj["connections"] = connectionsArr; } else { @@ -1986,31 +1951,81 @@ void Project::addNewMapGroup(const QString &groupName) { emit mapGroupAdded(groupName); } -QString Project::getNewMapName() { - // Ensure default name doesn't already exist. +Project::NewMapSettings Project::getNewMapSettings() const { + // Ensure default name/ID doesn't already exist. int i = 0; QString newMapName; + QString newMapId; do { newMapName = QString("NewMap%1").arg(++i); - } while (this->mapNames.contains(newMapName)); + newMapId = Map::mapConstantFromName(newMapName); + } while (!isIdentifierUnique(newMapName) || !isIdentifierUnique(newMapId)); - return newMapName; + NewMapSettings settings; + settings.name = newMapName; + settings.id = newMapId; + settings.group = this->groupNames.at(0); + settings.canFlyTo = false; + settings.layout = getNewLayoutSettings(); + settings.layout.id = Layout::layoutConstantFromName(newMapName); + settings.layout.name = Layout::layoutNameFromMapName(newMapName); + settings.header.setSong(this->defaultSong); + settings.header.setLocation(this->mapSectionIdNames.value(0, "0")); + settings.header.setRequiresFlash(false); + settings.header.setWeather(this->weatherNames.value(0, "0")); + settings.header.setType(this->mapTypes.value(0, "0")); + settings.header.setBattleScene(this->mapBattleScenes.value(0, "0")); + settings.header.setShowsLocationName(true); + settings.header.setAllowsRunning(false); + settings.header.setAllowsBiking(false); + settings.header.setAllowsEscaping(false); + settings.header.setFloorNumber(0); + return settings; } -QString Project::getNewLayoutName() { - // Ensure default name doesn't already exist. +Layout::Settings Project::getNewLayoutSettings() const { + // Ensure default name/ID doesn't already exist. int i = 0; QString newLayoutName; + QString newLayoutId; do { newLayoutName = QString("NewLayout%1").arg(++i); - } while (!isLayoutNameUnique(newLayoutName)); + newLayoutId = Layout::layoutConstantFromName(newLayoutName); + } while (!isIdentifierUnique(newLayoutId) || !isIdentifierUnique(newLayoutName)); - return newLayoutName; + Layout::Settings settings; + settings.name = newLayoutName; + settings.id = newLayoutId; + settings.width = getDefaultMapDimension(); + settings.height = getDefaultMapDimension(); + settings.borderWidth = DEFAULT_BORDER_WIDTH; + settings.borderHeight = DEFAULT_BORDER_HEIGHT; + settings.primaryTilesetLabel = getDefaultPrimaryTilesetLabel(); + settings.secondaryTilesetLabel = getDefaultSecondaryTilesetLabel(); + return settings; } -bool Project::isLayoutNameUnique(const QString &name) { +// When we ask the user to provide a new identifier for something (like a map/layout name or ID) +// we use this to make sure that it doesn't collide with any known identifiers first. +// Porymap knows of many more identifiers than this, but for simplicity we only check the lists that users can add to via Porymap. +// In general this only matters to Porymap if the identifier will be added to the group it collides with, +// but name collisions are likely undesirable in the project. +// TODO: Use elsewhere +bool Project::isIdentifierUnique(const QString &identifier) const { + if (this->mapNames.contains(identifier)) + return false; + if (this->mapConstantsToMapNames.contains(identifier)) + return false; + if (this->groupNames.contains(identifier)) + return false; + if (this->mapSectionIdNames.contains(identifier)) + return false; + if (this->tilesetLabelsOrdered.contains(identifier)) + return false; + if (this->layoutIds.contains(identifier)) + return false; for (const auto &layout : this->mapLayouts) { - if (layout->name == name) { + if (layout->name == identifier) { return false; } } @@ -3056,32 +3071,6 @@ void Project::applyParsedLimits() { projectConfig.collisionSheetWidth = qMin(projectConfig.collisionSheetWidth, Block::getMaxCollision() + 1); } -void Project::initNewMapSettings() { - this->newMapSettings.group = this->groupNames.at(0); - this->newMapSettings.canFlyTo = false; - this->newMapSettings.header.setSong(this->defaultSong); - this->newMapSettings.header.setLocation(this->mapSectionIdNames.value(0, "0")); - this->newMapSettings.header.setRequiresFlash(false); - this->newMapSettings.header.setWeather(this->weatherNames.value(0, "0")); - this->newMapSettings.header.setType(this->mapTypes.value(0, "0")); - this->newMapSettings.header.setBattleScene(this->mapBattleScenes.value(0, "0")); - this->newMapSettings.header.setShowsLocationName(true); - this->newMapSettings.header.setAllowsRunning(false); - this->newMapSettings.header.setAllowsBiking(false); - this->newMapSettings.header.setAllowsEscaping(false); - this->newMapSettings.header.setFloorNumber(0); - initNewLayoutSettings(); -} - -void Project::initNewLayoutSettings() { - this->newMapSettings.layout.width = getDefaultMapDimension(); - this->newMapSettings.layout.height = getDefaultMapDimension(); - this->newMapSettings.layout.borderWidth = DEFAULT_BORDER_WIDTH; - this->newMapSettings.layout.borderHeight = DEFAULT_BORDER_HEIGHT; - this->newMapSettings.layout.primaryTilesetLabel = getDefaultPrimaryTilesetLabel(); - this->newMapSettings.layout.secondaryTilesetLabel = getDefaultSecondaryTilesetLabel(); -} - bool Project::hasUnsavedChanges() { if (this->hasUnsavedDataChanges) return true; diff --git a/src/ui/mapheaderform.cpp b/src/ui/mapheaderform.cpp index 1fd9084c..fa0e2c1c 100644 --- a/src/ui/mapheaderform.cpp +++ b/src/ui/mapheaderform.cpp @@ -154,9 +154,10 @@ MapHeader MapHeaderForm::headerData() const { return header; } -void MapHeaderForm::setLocationsDisabled(bool disabled) { - ui->label_Location->setDisabled(disabled); - ui->comboBox_Location->setDisabled(disabled); +void MapHeaderForm::setLocationDisabled(bool disabled) { + m_locationDisabled = disabled; + ui->label_Location->setDisabled(m_locationDisabled); + ui->comboBox_Location->setDisabled(m_locationDisabled); } void MapHeaderForm::updateSong() { diff --git a/src/ui/newlayoutdialog.cpp b/src/ui/newlayoutdialog.cpp index 73822ba8..3cf7b9a9 100644 --- a/src/ui/newlayoutdialog.cpp +++ b/src/ui/newlayoutdialog.cpp @@ -9,31 +9,55 @@ const QString lineEdit_ErrorStylesheet = "QLineEdit { background-color: rgba(255, 0, 0, 25%) }"; -NewLayoutDialog::NewLayoutDialog(QWidget *parent, Project *project) : +Layout::Settings NewLayoutDialog::settings = {}; +bool NewLayoutDialog::initializedSettings = false; + +NewLayoutDialog::NewLayoutDialog(Project *project, QWidget *parent) : QDialog(parent), ui(new Ui::NewLayoutDialog) { setAttribute(Qt::WA_DeleteOnClose); setModal(true); ui->setupUi(this); + ui->label_GenericError->setVisible(false); this->project = project; - this->settings = &project->newMapSettings.layout; - - ui->lineEdit_Name->setText(project->getNewLayoutName()); + Layout::Settings newSettings = project->getNewLayoutSettings(); + if (!initializedSettings) { + // The first time this dialog is opened we initialize all the default settings. + settings = newSettings; + initializedSettings = true; + } else { + // On subsequent openings we only initialize the settings that should be unique, + // preserving all other settings from the last time the dialog was open. + settings.name = newSettings.name; + settings.id = newSettings.id; + } ui->newLayoutForm->initUi(project); - ui->newLayoutForm->setSettings(*this->settings); - // Names and IDs can only contain word characters, and cannot start with a digit. + // Identifiers can only contain word characters, and cannot start with a digit. static const QRegularExpression re("[A-Za-z_]+[\\w]*"); auto validator = new QRegularExpressionValidator(re, this); ui->lineEdit_Name->setValidator(validator); ui->lineEdit_LayoutID->setValidator(validator); connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &NewLayoutDialog::dialogButtonClicked); + + refresh(); adjustSize(); } +// Creating new layout from AdvanceMap import +// TODO: Re-use for a "Duplicate Layout" option +NewLayoutDialog::NewLayoutDialog(Project *project, const Layout *layout, QWidget *parent) : + NewLayoutDialog(project, parent) +{ + if (layout) { + this->importedLayout = layout->copy(); + refresh(); + } +} + NewLayoutDialog::~NewLayoutDialog() { saveSettings(); @@ -41,33 +65,24 @@ NewLayoutDialog::~NewLayoutDialog() delete ui; } -// Creating new layout from AdvanceMap import -// TODO: Re-use for a "Duplicate Layout" option? -void NewLayoutDialog::copyFrom(const Layout &layoutToCopy) { - if (this->importedLayout) - delete this->importedLayout; +void NewLayoutDialog::refresh() { + if (this->importedLayout) { + // If we're importing a layout then some settings will be enforced. + ui->newLayoutForm->setSettings(this->importedLayout->settings()); + ui->newLayoutForm->setDisabled(true); + } else { + ui->newLayoutForm->setSettings(settings); + ui->newLayoutForm->setDisabled(false); + } - this->importedLayout = new Layout(); - this->importedLayout->blockdata = layoutToCopy.blockdata; - if (!layoutToCopy.border.isEmpty()) - this->importedLayout->border = layoutToCopy.border; - - this->settings->width = layoutToCopy.width; - this->settings->height = layoutToCopy.height; - this->settings->borderWidth = layoutToCopy.border_width; - this->settings->borderHeight = layoutToCopy.border_height; - this->settings->primaryTilesetLabel = layoutToCopy.tileset_primary_label; - this->settings->secondaryTilesetLabel = layoutToCopy.tileset_secondary_label; - - // Don't allow changes to the layout settings - ui->newLayoutForm->setSettings(*this->settings); - ui->newLayoutForm->setDisabled(true); + ui->lineEdit_Name->setText(settings.name); + ui->lineEdit_LayoutID->setText(settings.id); } void NewLayoutDialog::saveSettings() { - *this->settings = ui->newLayoutForm->settings(); - this->settings->id = ui->lineEdit_LayoutID->text(); - this->settings->name = ui->lineEdit_Name->text(); + settings = ui->newLayoutForm->settings(); + settings.id = ui->lineEdit_LayoutID->text(); + settings.name = ui->lineEdit_Name->text(); } bool NewLayoutDialog::validateLayoutID(bool allowEmpty) { @@ -76,8 +91,8 @@ bool NewLayoutDialog::validateLayoutID(bool allowEmpty) { QString errorText; if (id.isEmpty()) { if (!allowEmpty) errorText = QString("%1 cannot be empty.").arg(ui->label_LayoutID->text()); - } else if (this->project->mapLayouts.contains(id)) { - errorText = QString("%1 '%2' is already in use.").arg(ui->label_LayoutID->text()).arg(id); + } else if (!this->project->isIdentifierUnique(id)) { + errorText = QString("%1 '%2' is not unique.").arg(ui->label_LayoutID->text()).arg(id); } bool isValid = errorText.isEmpty(); @@ -97,8 +112,8 @@ bool NewLayoutDialog::validateName(bool allowEmpty) { QString errorText; if (name.isEmpty()) { if (!allowEmpty) errorText = QString("%1 cannot be empty.").arg(ui->label_Name->text()); - } else if (!this->project->isLayoutNameUnique(name)) { - errorText = QString("%1 '%2' is already in use.").arg(ui->label_Name->text()).arg(name); + } else if (!this->project->isIdentifierUnique(name)) { + errorText = QString("%1 '%2' is not unique.").arg(ui->label_Name->text()).arg(name); } bool isValid = errorText.isEmpty(); @@ -110,6 +125,8 @@ bool NewLayoutDialog::validateName(bool allowEmpty) { void NewLayoutDialog::on_lineEdit_Name_textChanged(const QString &text) { validateName(true); + + // Changing the layout name updates the layout ID field to match. ui->lineEdit_LayoutID->setText(Layout::layoutConstantFromName(text)); } @@ -118,8 +135,8 @@ void NewLayoutDialog::dialogButtonClicked(QAbstractButton *button) { if (role == QDialogButtonBox::RejectRole){ reject(); } else if (role == QDialogButtonBox::ResetRole) { - this->project->initNewLayoutSettings(); // TODO: Don't allow this to change locked settings - ui->newLayoutForm->setSettings(*this->settings); + settings = this->project->getNewLayoutSettings(); + refresh(); } else if (role == QDialogButtonBox::AcceptRole) { accept(); } @@ -137,18 +154,13 @@ void NewLayoutDialog::accept() { // Update settings from UI saveSettings(); - /* - if (this->importedLayout) { - // Copy layout data from imported layout - layout->blockdata = this->importedLayout->blockdata; - if (!this->importedLayout->border.isEmpty()) - layout->border = this->importedLayout->border; - } - */ - - Layout *layout = this->project->createNewLayout(*this->settings); - if (!layout) + Layout *layout = this->project->createNewLayout(settings, this->importedLayout); + if (!layout) { + ui->label_GenericError->setText(QString("Failed to create layout. See %1 for details.").arg(getLogPath())); + ui->label_GenericError->setVisible(true); return; + } + ui->label_GenericError->setVisible(false); emit applied(layout->id); QDialog::accept(); diff --git a/src/ui/newmapdialog.cpp b/src/ui/newmapdialog.cpp index ef31a7b1..a97fd2ab 100644 --- a/src/ui/newmapdialog.cpp +++ b/src/ui/newmapdialog.cpp @@ -10,23 +10,36 @@ const QString lineEdit_ErrorStylesheet = "QLineEdit { background-color: rgba(255, 0, 0, 25%) }"; -NewMapDialog::NewMapDialog(QWidget *parent, Project *project) : +Project::NewMapSettings NewMapDialog::settings = {}; +bool NewMapDialog::initializedSettings = false; + +NewMapDialog::NewMapDialog(Project *project, QWidget *parent) : QDialog(parent), ui(new Ui::NewMapDialog) { setAttribute(Qt::WA_DeleteOnClose); setModal(true); ui->setupUi(this); + ui->label_GenericError->setVisible(false); this->project = project; - this->settings = &project->newMapSettings; - // Populate UI using data from project - this->settings->mapName = project->getNewMapName(); + Project::NewMapSettings newSettings = project->getNewMapSettings(); + if (!initializedSettings) { + // The first time this dialog is opened we initialize all the default settings. + settings = newSettings; + initializedSettings = true; + } else { + // On subsequent openings we only initialize the settings that should be unique, + // preserving all other settings from the last time the dialog was open. + settings.name = newSettings.name; + settings.id = newSettings.id; + } ui->newLayoutForm->initUi(project); + ui->comboBox_Group->addItems(project->groupNames); ui->comboBox_LayoutID->addItems(project->layoutIds); - // Names and IDs can only contain word characters, and cannot start with a digit. + // Identifiers can only contain word characters, and cannot start with a digit. static const QRegularExpression re("[A-Za-z_]+[\\w]*"); auto validator = new QRegularExpressionValidator(re, this); ui->lineEdit_Name->setValidator(validator); @@ -37,7 +50,7 @@ NewMapDialog::NewMapDialog(QWidget *parent, Project *project) : // Create a collapsible section that has all the map header data. this->headerForm = new MapHeaderForm(); this->headerForm->init(project); - this->headerForm->setHeader(&this->settings->header); + this->headerForm->setHeader(&settings.header); auto sectionLayout = new QVBoxLayout(); sectionLayout->addWidget(this->headerForm); @@ -47,103 +60,91 @@ NewMapDialog::NewMapDialog(QWidget *parent, Project *project) : ui->layout_HeaderData->addItem(new QSpacerItem(0, 0, QSizePolicy::Ignored, QSizePolicy::Expanding)); connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &NewMapDialog::dialogButtonClicked); - connect(ui->comboBox_LayoutID, &QComboBox::currentTextChanged, this, &NewMapDialog::useLayoutIdSettings); + refresh(); adjustSize(); // TODO: Save geometry? } +// Adding new map to existing map list folder. +NewMapDialog::NewMapDialog(Project *project, int mapListTab, const QString &mapListItem, QWidget *parent) : + NewMapDialog(project, parent) +{ + switch (mapListTab) + { + case MapListTab::Groups: + settings.group = mapListItem; + ui->label_Group->setDisabled(true); + ui->comboBox_Group->setDisabled(true); + ui->comboBox_Group->setTextItem(settings.group); + break; + case MapListTab::Areas: + settings.header.setLocation(mapListItem); + this->headerForm->setLocationDisabled(true); + // Header UI is kept in sync automatically by MapHeaderForm + break; + case MapListTab::Layouts: + settings.layout.id = mapListItem; + ui->label_LayoutID->setDisabled(true); + ui->comboBox_LayoutID->setDisabled(true); + ui->comboBox_LayoutID->setTextItem(settings.layout.id); + break; + } +} + +// TODO: Use for a "Duplicate Map" option +NewMapDialog::NewMapDialog(Project *project, const Map *mapToCopy, QWidget *parent) : + NewMapDialog(project, parent) +{ + /* + if (this->importedMap) + delete this->importedMap; + + this->importedMap = new Map(mapToCopy); + useLayoutSettings(this->importedMap->layout()); + */ +} + NewMapDialog::~NewMapDialog() { saveSettings(); - delete this->importedLayout; + delete this->importedMap; delete ui; } -void NewMapDialog::init() { - const QSignalBlocker b_LayoutId(ui->comboBox_LayoutID); - ui->comboBox_LayoutID->setCurrentText(this->settings->layout.id); - - ui->lineEdit_Name->setText(this->settings->mapName); - ui->comboBox_Group->setTextItem(this->settings->group); - ui->checkBox_CanFlyTo->setChecked(this->settings->canFlyTo); - ui->newLayoutForm->setSettings(this->settings->layout); -} - -// Creating new map by right-clicking in the map list -void NewMapDialog::init(int tabIndex, QString fieldName) { - switch (tabIndex) - { - case MapListTab::Groups: - this->settings->group = fieldName; - ui->label_Group->setDisabled(true); - ui->comboBox_Group->setDisabled(true); - break; - case MapListTab::Areas: - this->settings->header.setLocation(fieldName); - this->headerForm->setLocationsDisabled(true); - break; - case MapListTab::Layouts: - ui->label_LayoutID->setDisabled(true); - ui->comboBox_LayoutID->setDisabled(true); - useLayoutIdSettings(fieldName); - break; - } - init(); -} - -// Creating new map from AdvanceMap import -// TODO: Re-use for a "Duplicate Map/Layout" option? -void NewMapDialog::init(Layout *layoutToCopy) { - if (this->importedLayout) - delete this->importedLayout; - - this->importedLayout = new Layout(); - this->importedLayout->blockdata = layoutToCopy->blockdata; - if (!layoutToCopy->border.isEmpty()) - this->importedLayout->border = layoutToCopy->border; - - useLayoutSettings(this->importedLayout); - init(); +// Sync UI with settings. If any UI elements are disabled (because their settings are being enforced) +// then we don't update them using the settings here. +void NewMapDialog::refresh() { + ui->lineEdit_Name->setText(settings.name); + ui->lineEdit_MapID->setText(settings.id); + + ui->comboBox_Group->setTextItem(settings.group); + ui->checkBox_CanFlyTo->setChecked(settings.canFlyTo); + ui->comboBox_LayoutID->setTextItem(settings.layout.id); + ui->newLayoutForm->setSettings(settings.layout); + // Header UI is kept in sync automatically by MapHeaderForm } void NewMapDialog::saveSettings() { - this->settings->mapName = ui->lineEdit_Name->text(); - this->settings->mapId = ui->lineEdit_MapID->text(); - this->settings->group = ui->comboBox_Group->currentText(); - this->settings->canFlyTo = ui->checkBox_CanFlyTo->isChecked(); - this->settings->layout = ui->newLayoutForm->settings(); - this->settings->layout.id = ui->comboBox_LayoutID->currentText(); - this->settings->layout.name = QString("%1_Layout").arg(this->settings->mapName); - this->settings->header = this->headerForm->headerData(); + settings.name = ui->lineEdit_Name->text(); + settings.id = ui->lineEdit_MapID->text(); + settings.group = ui->comboBox_Group->currentText(); + settings.canFlyTo = ui->checkBox_CanFlyTo->isChecked(); + settings.layout = ui->newLayoutForm->settings(); + settings.layout.id = ui->comboBox_LayoutID->currentText(); + // We don't provide full control for naming new layouts here (just via the ID). + // If a user wants to explicitly name a layout they can create it individually before creating the map. + settings.layout.name = Layout::layoutNameFromMapName(settings.name); + settings.header = this->headerForm->headerData(); porymapConfig.newMapHeaderSectionExpanded = this->headerSection->isExpanded(); } void NewMapDialog::useLayoutSettings(const Layout *layout) { - if (!layout) { + if (layout) { + ui->newLayoutForm->setSettings(layout->settings()); + ui->newLayoutForm->setDisabled(true); + } else { ui->newLayoutForm->setDisabled(false); - return; } - - this->settings->layout.width = layout->width; - this->settings->layout.height = layout->height; - this->settings->layout.borderWidth = layout->border_width; - this->settings->layout.borderHeight = layout->border_height; - this->settings->layout.primaryTilesetLabel = layout->tileset_primary_label; - this->settings->layout.secondaryTilesetLabel = layout->tileset_secondary_label; - - // Don't allow changes to the layout settings - ui->newLayoutForm->setSettings(this->settings->layout); - ui->newLayoutForm->setDisabled(true); -} - -void NewMapDialog::useLayoutIdSettings(const QString &layoutId) { - this->settings->layout.id = layoutId; - useLayoutSettings(this->project->mapLayouts.value(layoutId)); -} - -// Return true if the "layout ID" field is specifying a layout that already exists. -bool NewMapDialog::isExistingLayout() const { - return this->project->mapLayouts.contains(this->settings->layout.id); } bool NewMapDialog::validateMapID(bool allowEmpty) { @@ -155,13 +156,8 @@ bool NewMapDialog::validateMapID(bool allowEmpty) { if (!allowEmpty) errorText = QString("%1 cannot be empty.").arg(ui->label_MapID->text()); } else if (!id.startsWith(expectedPrefix)) { errorText = QString("%1 '%2' must start with '%3'.").arg(ui->label_MapID->text()).arg(id).arg(expectedPrefix); - } else { - for (auto i = this->project->mapNamesToMapConstants.constBegin(), end = this->project->mapNamesToMapConstants.constEnd(); i != end; i++) { - if (id == i.value()) { - errorText = QString("%1 '%2' is already in use.").arg(ui->label_MapID->text()).arg(id); - break; - } - } + } else if (!this->project->isIdentifierUnique(id)) { + errorText = QString("%1 '%2' is not unique.").arg(ui->label_MapID->text()).arg(id); } bool isValid = errorText.isEmpty(); @@ -181,8 +177,8 @@ bool NewMapDialog::validateName(bool allowEmpty) { QString errorText; if (name.isEmpty()) { if (!allowEmpty) errorText = QString("%1 cannot be empty.").arg(ui->label_Name->text()); - } else if (project->mapNames.contains(name)) { - errorText = QString("%1 '%2' is already in use.").arg(ui->label_Name->text()).arg(name); + } else if (!this->project->isIdentifierUnique(name)) { + errorText = QString("%1 '%2' is not unique.").arg(ui->label_Name->text()).arg(name); } bool isValid = errorText.isEmpty(); @@ -206,6 +202,8 @@ bool NewMapDialog::validateGroup(bool allowEmpty) { QString errorText; if (groupName.isEmpty()) { if (!allowEmpty) errorText = QString("%1 cannot be empty.").arg(ui->label_Group->text()); + } else if (!this->project->groupNames.contains(groupName) && !this->project->isIdentifierUnique(groupName)) { + errorText = QString("%1 must either be the name of an existing map group, or a unique identifier for a new map group.").arg(ui->label_Group->text()); } bool isValid = errorText.isEmpty(); @@ -219,13 +217,43 @@ void NewMapDialog::on_comboBox_Group_currentTextChanged(const QString &) { validateGroup(true); } +bool NewMapDialog::validateLayoutID(bool allowEmpty) { + QString layoutId = ui->comboBox_LayoutID->currentText(); + + QString errorText; + if (layoutId.isEmpty()) { + if (!allowEmpty) errorText = QString("%1 cannot be empty.").arg(ui->label_LayoutID->text()); + } else if (!this->project->layoutIds.contains(layoutId) && !this->project->isIdentifierUnique(layoutId)) { + errorText = QString("%1 must either be the ID for an existing layout, or a unique identifier for a new layout.").arg(ui->label_LayoutID->text()); + } + + bool isValid = errorText.isEmpty(); + ui->label_LayoutIDError->setText(errorText); + ui->label_LayoutIDError->setVisible(!isValid); + ui->comboBox_LayoutID->lineEdit()->setStyleSheet(!isValid ? lineEdit_ErrorStylesheet : ""); + return isValid; +} + +void NewMapDialog::on_comboBox_LayoutID_currentTextChanged(const QString &text) { + validateLayoutID(true); + useLayoutSettings(this->project->mapLayouts.value(text)); +} + void NewMapDialog::dialogButtonClicked(QAbstractButton *button) { auto role = ui->buttonBox->buttonRole(button); if (role == QDialogButtonBox::RejectRole){ reject(); } else if (role == QDialogButtonBox::ResetRole) { - this->project->initNewMapSettings(); // TODO: Don't allow this to change locked settings - init(); + auto newSettings = this->project->getNewMapSettings(); + + // If the location setting is disabled we need to enforce that setting on the new header. + if (this->headerForm->isLocationDisabled()) + newSettings.header.setLocation(settings.header.location()); + + settings = newSettings; + this->headerForm->setHeader(&settings.header); // TODO: Unnecessary? + refresh(); + } else if (role == QDialogButtonBox::AcceptRole) { accept(); } @@ -238,6 +266,7 @@ void NewMapDialog::accept() { if (!validateMapID()) success = false; if (!validateName()) success = false; if (!validateGroup()) success = false; + if (!validateLayoutID()) success = false; if (!success) return; @@ -245,33 +274,29 @@ void NewMapDialog::accept() { saveSettings(); Map *newMap = new Map; - newMap->setName(this->settings->mapName); - newMap->setConstantName(this->settings->mapId); - newMap->setHeader(this->settings->header); - newMap->setNeedsHealLocation(this->settings->canFlyTo); + newMap->setName(settings.name); + newMap->setConstantName(settings.id); + newMap->setHeader(settings.header); + newMap->setNeedsHealLocation(settings.canFlyTo); - Layout *layout = nullptr; - const bool existingLayout = isExistingLayout(); - if (existingLayout) { - layout = this->project->mapLayouts.value(this->settings->layout.id); - newMap->setNeedsLayoutDir(false); // TODO: Remove this member + Layout *layout = this->project->mapLayouts.value(settings.layout.id); + if (layout) { + // Layout already exists + newMap->setNeedsLayoutDir(false); // TODO: Remove this member? } else { - /* TODO: Re-implement (make sure this won't ever override an existing layout) - if (this->importedLayout) { - // Copy layout data from imported layout - layout->blockdata = this->importedLayout->blockdata; - if (!this->importedLayout->border.isEmpty()) - layout->border = this->importedLayout->border; - } - */ - layout = this->project->createNewLayout(this->settings->layout); + layout = this->project->createNewLayout(settings.layout); } - if (!layout) + if (!layout) { + ui->label_GenericError->setText(QString("Failed to create layout for map. See %1 for details.").arg(getLogPath())); + ui->label_GenericError->setVisible(true); + delete newMap; return; + } + ui->label_GenericError->setVisible(false); newMap->setLayout(layout); - this->project->addNewMap(newMap, this->settings->group); + this->project->addNewMap(newMap, settings.group); emit applied(newMap->name()); QDialog::accept(); } diff --git a/src/ui/newtilesetdialog.cpp b/src/ui/newtilesetdialog.cpp index e9eee946..8cb4c5f9 100644 --- a/src/ui/newtilesetdialog.cpp +++ b/src/ui/newtilesetdialog.cpp @@ -10,7 +10,7 @@ NewTilesetDialog::NewTilesetDialog(Project* project, QWidget *parent) : this->setFixedSize(this->width(), this->height()); this->project = project; //only allow characters valid for a symbol - static const QRegularExpression expression("[_A-Za-z0-9]+$"); + static const QRegularExpression expression("[_A-Za-z0-9]+$"); // TODO: Incorrect, allows digits at beginning QRegularExpressionValidator *validator = new QRegularExpressionValidator(expression); this->ui->nameLineEdit->setValidator(validator); @@ -35,6 +35,7 @@ void NewTilesetDialog::SecondaryChanged(){ NameOrSecondaryChanged(); } +// TODO: No validation void NewTilesetDialog::NameOrSecondaryChanged() { this->friendlyName = this->ui->nameLineEdit->text(); this->fullSymbolName = projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix) + this->friendlyName;