diff --git a/forms/customattributesdialog.ui b/forms/customattributesdialog.ui new file mode 100644 index 00000000..b1f1ee4b --- /dev/null +++ b/forms/customattributesdialog.ui @@ -0,0 +1,172 @@ + + + CustomAttributesDialog + + + + 0 + 0 + 410 + 192 + + + + Add New Custom Attribute + + + + + + + 0 + + + 0 + + + + + Name + + + + + + + The key name for the new JSON field + + + true + + + + + + + Type + + + + + + + The data type for the new JSON field + + + + + + + Value + + + + + + + + 0 + 0 + + + + The value for the new JSON field + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + + + + + false + + + color: rgb(255, 0, 0) + + + + + + + + + + + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + + NoScrollComboBox + QComboBox +
noscrollcombobox.h
+
+
+ + +
diff --git a/forms/customattributesframe.ui b/forms/customattributesframe.ui new file mode 100644 index 00000000..87619e63 --- /dev/null +++ b/forms/customattributesframe.ui @@ -0,0 +1,89 @@ + + + CustomAttributesFrame + + + + 0 + 0 + 400 + 300 + + + + + + + Custom Attributes + + + + + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Plain + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Add + + + + + + + false + + + Delete + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + CustomAttributesTable + QTableWidget +
customattributestable.h
+
+
+ + +
diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index dfea9752..4a9e439a 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -2244,113 +2244,69 @@ - + - QFrame::Shape::StyledPanel + QFrame::Shape::NoFrame - QFrame::Shadow::Raised + QFrame::Shadow::Plain - - - - - Custom Fields - - - - - - - QFrame::Shape::NoFrame - - - QFrame::Shadow::Plain - - - - 0 + + true + + + + + 0 + 0 + 1011 + 806 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + - - 0 + + QFrame::Shape::StyledPanel - - 0 + + QFrame::Shadow::Raised - - 0 + + + + + + Qt::Orientation::Vertical - - - - Add - - - - - - - Delete - - - - - - - Qt::Orientation::Horizontal - - - - 40 - 20 - - - - - - - - - - - Custom fields will be added to the map.json file for the current map. - - - false - - - false - - - true - - - false - - - true - - - false - - - - Type + + + 1 + 767 + - - - - Key - - - - - Value - - - - - + + + + @@ -3349,6 +3305,12 @@
maplisttoolbar.h
1 + + CustomAttributesFrame + QFrame +
customattributesframe.h
+ 1 +
diff --git a/include/core/events.h b/include/core/events.h index c0a5a625..081ff506 100644 --- a/include/core/events.h +++ b/include/core/events.h @@ -160,10 +160,10 @@ public: virtual void setDefaultValues(Project *project); virtual QSet getExpectedFields() = 0; - void readCustomValues(QJsonObject values); - void addCustomValuesTo(OrderedJson::object *obj); - const QMap getCustomValues() { return this->customValues; } - void setCustomValues(const QMap newCustomValues) { this->customValues = newCustomValues; } + void readCustomAttributes(const QJsonObject &json); + void addCustomAttributesTo(OrderedJson::object *obj) const; + const QMap getCustomAttributes() const { return this->customAttributes; } + void setCustomAttributes(const QMap newCustomAttributes) { this->customAttributes = newCustomAttributes; } virtual void loadPixmap(Project *project); @@ -206,7 +206,7 @@ protected: int spriteHeight = 16; bool usingSprite = false; - QMap customValues; + QMap customAttributes; QPixmap pixmap; DraggablePixmapItem *pixmapItem = nullptr; diff --git a/include/editor.h b/include/editor.h index 434415e9..81bf1611 100644 --- a/include/editor.h +++ b/include/editor.h @@ -107,7 +107,7 @@ public: void updatePrimaryTileset(QString tilesetLabel, bool forceLoad = false); void updateSecondaryTileset(QString tilesetLabel, bool forceLoad = false); void toggleBorderVisibility(bool visible, bool enableScriptCallback = true); - void updateCustomMapHeaderValues(QTableWidget *); + void updateCustomMapAttributes(); DraggablePixmapItem *addMapEvent(Event *event); bool eventLimitReached(Map *, Event::Type); diff --git a/include/mainwindow.h b/include/mainwindow.h index cf37a298..42d346f3 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -274,9 +274,6 @@ private slots: void on_actionAbout_Porymap_triggered(); void on_actionOpen_Log_File_triggered(); void on_actionOpen_Config_Folder_triggered(); - void on_pushButton_AddCustomHeaderField_clicked(); - void on_pushButton_DeleteCustomHeaderField_clicked(); - void on_tableWidget_CustomHeaderFields_cellChanged(int row, int column); void on_horizontalSlider_MetatileZoom_valueChanged(int value); void on_horizontalSlider_CollisionZoom_valueChanged(int value); void on_pushButton_NewWildMonGroup_clicked(); @@ -406,7 +403,7 @@ private: Event::Group getEventGroupFromTabWidget(QWidget *tab); bool closeSupplementaryWindows(); void setWindowDisabled(bool); - + void resetMapCustomAttributesTable(); void initTilesetEditor(); bool initRegionMapEditor(bool silent = false); bool askToFixRegionMapEditor(); diff --git a/include/ui/customattributesdialog.h b/include/ui/customattributesdialog.h new file mode 100644 index 00000000..7048e16c --- /dev/null +++ b/include/ui/customattributesdialog.h @@ -0,0 +1,34 @@ +#ifndef CUSTOMATTRIBUTESDIALOG_H +#define CUSTOMATTRIBUTESDIALOG_H + +#include +#include + +#include "customattributestable.h" + +namespace Ui { +class CustomAttributesDialog; +} + +class CustomAttributesDialog : public QDialog +{ + Q_OBJECT + +public: + explicit CustomAttributesDialog(CustomAttributesTable *table); + ~CustomAttributesDialog(); + +private: + Ui::CustomAttributesDialog *ui; + CustomAttributesTable *const m_table; + + void setInputType(int inputType); + void onNameChanged(const QString &); + bool validateName(bool allowEmpty = false); + void clickedButton(QAbstractButton *button); + void addNewAttribute(); + QVariant getValue() const; +}; + + +#endif // CUSTOMATTRIBUTESDIALOG_H diff --git a/include/ui/customattributesframe.h b/include/ui/customattributesframe.h new file mode 100644 index 00000000..f19d0601 --- /dev/null +++ b/include/ui/customattributesframe.h @@ -0,0 +1,36 @@ +#ifndef CUSTOMATTRIBUTESFRAME_H +#define CUSTOMATTRIBUTESFRAME_H + +/* + The frame containing the Custom Attributes table and its Add/Delete buttons. + Shared by the map's Header tab and Events. +*/ + +#include "customattributestable.h" + +#include +#include + +namespace Ui { +class CustomAttributesFrame; +} + +class CustomAttributesFrame : public QFrame +{ + Q_OBJECT + +public: + explicit CustomAttributesFrame(QWidget *parent = nullptr); + ~CustomAttributesFrame(); + + CustomAttributesTable* table() const; + +private: + Ui::CustomAttributesFrame *ui; + + void addAttribute(); + void deleteAttribute(); + void updateDeleteButton(); +}; + +#endif // CUSTOMATTRIBUTESFRAME_H diff --git a/include/ui/customattributestable.h b/include/ui/customattributestable.h index f17f4877..21cac4de 100644 --- a/include/ui/customattributestable.h +++ b/include/ui/customattributestable.h @@ -1,25 +1,44 @@ #ifndef CUSTOMATTRIBUTESTABLE_H #define CUSTOMATTRIBUTESTABLE_H -#include "events.h" #include -#include +#include #include -class CustomAttributesTable : public QFrame +class CustomAttributesTable : public QTableWidget { -public: - explicit CustomAttributesTable(Event *event, QWidget *parent = nullptr); - ~CustomAttributesTable(); + Q_OBJECT - static const QMap getAttributes(QTableWidget * table); - static QJsonValue pickType(QWidget * parent, bool * ok = nullptr); - static void addAttribute(QTableWidget * table, QString key, QJsonValue value, bool isNew = false); - static bool deleteSelectedAttributes(QTableWidget * table); +public: + explicit CustomAttributesTable(QWidget *parent = nullptr); + ~CustomAttributesTable() {}; + + QMap getAttributes() const; + void setAttributes(const QMap &attributes); + + void addNewAttribute(const QString &key, const QJsonValue &value); + bool deleteSelectedAttributes(); + + bool isEmpty() const; + bool isSelectionEmpty() const; + + QSet keys() const { return m_keys; } + QSet restrictedKeys() const { return m_restrictedKeys; } + void setRestrictedKeys(const QSet &keys) { m_restrictedKeys = keys; } + +signals: + void edited(); + +protected: + virtual void resizeEvent(QResizeEvent *event) override; private: - Event *event; - QTableWidget *table; + QSet m_keys; // All keys currently in the table + QSet m_restrictedKeys; // All keys not allowed in the table + + QPair getAttribute(int row) const; + int addAttribute(const QString &key, const QJsonValue &value); + void removeAttribute(const QString &key); void resizeVertically(); }; diff --git a/include/ui/eventframes.h b/include/ui/eventframes.h index c11cf8e6..2ad55750 100644 --- a/include/ui/eventframes.h +++ b/include/ui/eventframes.h @@ -52,6 +52,8 @@ public: QFrame *frame_contents; QVBoxLayout *layout_contents; + CustomAttributesFrame *custom_attributes; + protected: bool populated = false; bool initialized = false; diff --git a/porymap.pro b/porymap.pro index b6758c2e..76d19d21 100644 --- a/porymap.pro +++ b/porymap.pro @@ -67,6 +67,8 @@ SOURCES += src/core/advancemapparser.cpp \ src/ui/aboutporymap.cpp \ src/ui/colorinputwidget.cpp \ src/ui/connectionslistitem.cpp \ + src/ui/customattributesdialog.cpp \ + src/ui/customattributestable.cpp \ src/ui/customscriptseditor.cpp \ src/ui/customscriptslistitem.cpp \ src/ui/divingmappixmapitem.cpp \ @@ -83,7 +85,7 @@ SOURCES += src/core/advancemapparser.cpp \ src/ui/regionmaplayoutpixmapitem.cpp \ src/ui/regionmapentriespixmapitem.cpp \ src/ui/cursortilerect.cpp \ - src/ui/customattributestable.cpp \ + src/ui/customattributesframe.cpp \ src/ui/eventframes.cpp \ src/ui/eventfilters.cpp \ src/ui/filterchildrenproxymodel.cpp \ @@ -176,6 +178,8 @@ HEADERS += include/core/advancemapparser.h \ include/lib/orderedjson.h \ include/ui/aboutporymap.h \ include/ui/connectionslistitem.h \ + include/ui/customattributesdialog.h \ + include/ui/customattributestable.h \ include/ui/customscriptseditor.h \ include/ui/customscriptslistitem.h \ include/ui/divingmappixmapitem.h \ @@ -192,7 +196,7 @@ HEADERS += include/core/advancemapparser.h \ include/ui/regionmaplayoutpixmapitem.h \ include/ui/regionmapentriespixmapitem.h \ include/ui/cursortilerect.h \ - include/ui/customattributestable.h \ + include/ui/customattributesframe.h \ include/ui/eventframes.h \ include/ui/eventfilters.h \ include/ui/filterchildrenproxymodel.h \ @@ -258,6 +262,7 @@ HEADERS += include/core/advancemapparser.h \ FORMS += forms/mainwindow.ui \ forms/colorinputwidget.ui \ forms/connectionslistitem.ui \ + forms/customattributesframe.ui \ forms/gridsettingsdialog.ui \ forms/mapheaderform.ui \ forms/maplisttoolbar.ui \ @@ -282,6 +287,7 @@ FORMS += forms/mainwindow.ui \ forms/projectsettingseditor.ui \ forms/customscriptseditor.ui \ forms/customscriptslistitem.ui \ + forms/customattributesdialog.ui \ forms/updatepromoter.ui \ forms/wildmonchart.ui \ forms/wildmonsearch.ui diff --git a/src/core/events.cpp b/src/core/events.cpp index c0cf7b7a..2da13da0 100644 --- a/src/core/events.cpp +++ b/src/core/events.cpp @@ -53,20 +53,20 @@ void Event::setDefaultValues(Project *) { this->setElevation(projectConfig.defaultElevation); } -void Event::readCustomValues(QJsonObject values) { - this->customValues.clear(); - QSet expectedFields = this->getExpectedFields(); - for (QString key : values.keys()) { - if (!expectedFields.contains(key)) { - this->customValues[key] = values[key]; +void Event::readCustomAttributes(const QJsonObject &json) { + this->customAttributes.clear(); + const QSet expectedFields = this->getExpectedFields(); + for (auto i = json.constBegin(); i != json.constEnd(); i++) { + if (!expectedFields.contains(i.key())) { + this->customAttributes[i.key()] = i.value(); } } } -void Event::addCustomValuesTo(OrderedJson::object *obj) { - for (QString key : this->customValues.keys()) { - if (!obj->contains(key)) { - (*obj)[key] = OrderedJson::fromQJsonValue(this->customValues[key]); +void Event::addCustomAttributesTo(OrderedJson::object *obj) const { + for (auto i = this->customAttributes.constBegin(); i != this->customAttributes.constEnd(); i++) { + if (!obj->contains(i.key())) { + (*obj)[i.key()] = OrderedJson::fromQJsonValue(i.value()); } } } @@ -197,7 +197,7 @@ Event *ObjectEvent::duplicate() { copy->setSightRadiusBerryTreeID(this->getSightRadiusBerryTreeID()); copy->setScript(this->getScript()); copy->setFlag(this->getFlag()); - copy->setCustomValues(this->getCustomValues()); + copy->setCustomAttributes(this->getCustomAttributes()); return copy; } @@ -227,7 +227,7 @@ OrderedJson::object ObjectEvent::buildEventJson(Project *) { objectJson["trainer_sight_or_berry_tree_id"] = this->getSightRadiusBerryTreeID(); objectJson["script"] = this->getScript(); objectJson["flag"] = this->getFlag(); - this->addCustomValuesTo(&objectJson); + this->addCustomAttributesTo(&objectJson); return objectJson; } @@ -245,7 +245,7 @@ bool ObjectEvent::loadFromJson(QJsonObject json, Project *) { this->setScript(ParseUtil::jsonToQString(json["script"])); this->setFlag(ParseUtil::jsonToQString(json["flag"])); - this->readCustomValues(json); + this->readCustomAttributes(json); return true; } @@ -371,7 +371,7 @@ Event *CloneObjectEvent::duplicate() { copy->setGfx(this->getGfx()); copy->setTargetID(this->getTargetID()); copy->setTargetMap(this->getTargetMap()); - copy->setCustomValues(this->getCustomValues()); + copy->setCustomAttributes(this->getCustomAttributes()); return copy; } @@ -394,7 +394,7 @@ OrderedJson::object CloneObjectEvent::buildEventJson(Project *project) { cloneJson["target_local_id"] = this->getTargetID(); const QString mapName = this->getTargetMap(); cloneJson["target_map"] = project->mapNamesToMapConstants.value(mapName, mapName); - this->addCustomValuesTo(&cloneJson); + this->addCustomAttributesTo(&cloneJson); return cloneJson; } @@ -411,7 +411,7 @@ bool CloneObjectEvent::loadFromJson(QJsonObject json, Project *project) { logWarn(QString("Unknown Target Map constant '%1'.").arg(mapConstant)); this->setTargetMap(project->mapConstantsToMapNames.value(mapConstant, mapConstant)); - this->readCustomValues(json); + this->readCustomAttributes(json); return true; } @@ -478,7 +478,7 @@ Event *WarpEvent::duplicate() { copy->setDestinationMap(this->getDestinationMap()); copy->setDestinationWarpID(this->getDestinationWarpID()); - copy->setCustomValues(this->getCustomValues()); + copy->setCustomAttributes(this->getCustomAttributes()); return copy; } @@ -501,7 +501,7 @@ OrderedJson::object WarpEvent::buildEventJson(Project *project) { warpJson["dest_map"] = project->mapNamesToMapConstants.value(mapName, mapName); warpJson["dest_warp_id"] = this->getDestinationWarpID(); - this->addCustomValuesTo(&warpJson); + this->addCustomAttributesTo(&warpJson); return warpJson; } @@ -518,7 +518,7 @@ bool WarpEvent::loadFromJson(QJsonObject json, Project *project) { logWarn(QString("Unknown Destination Map constant '%1'.").arg(mapConstant)); this->setDestinationMap(project->mapConstantsToMapNames.value(mapConstant, mapConstant)); - this->readCustomValues(json); + this->readCustomAttributes(json); return true; } @@ -560,7 +560,7 @@ Event *TriggerEvent::duplicate() { copy->setScriptVarValue(this->getScriptVarValue()); copy->setScriptLabel(this->getScriptLabel()); - copy->setCustomValues(this->getCustomValues()); + copy->setCustomAttributes(this->getCustomAttributes()); return copy; } @@ -584,7 +584,7 @@ OrderedJson::object TriggerEvent::buildEventJson(Project *) { triggerJson["var_value"] = this->getScriptVarValue(); triggerJson["script"] = this->getScriptLabel(); - this->addCustomValuesTo(&triggerJson); + this->addCustomAttributesTo(&triggerJson); return triggerJson; } @@ -597,7 +597,7 @@ bool TriggerEvent::loadFromJson(QJsonObject json, Project *) { this->setScriptVarValue(ParseUtil::jsonToQString(json["var_value"])); this->setScriptLabel(ParseUtil::jsonToQString(json["script"])); - this->readCustomValues(json); + this->readCustomAttributes(json); return true; } @@ -634,7 +634,7 @@ Event *WeatherTriggerEvent::duplicate() { copy->setElevation(this->getElevation()); copy->setWeather(this->getWeather()); - copy->setCustomValues(this->getCustomValues()); + copy->setCustomAttributes(this->getCustomAttributes()); return copy; } @@ -656,7 +656,7 @@ OrderedJson::object WeatherTriggerEvent::buildEventJson(Project *) { weatherJson["elevation"] = this->getElevation(); weatherJson["weather"] = this->getWeather(); - this->addCustomValuesTo(&weatherJson); + this->addCustomAttributesTo(&weatherJson); return weatherJson; } @@ -667,7 +667,7 @@ bool WeatherTriggerEvent::loadFromJson(QJsonObject json, Project *) { this->setElevation(ParseUtil::jsonToInt(json["elevation"])); this->setWeather(ParseUtil::jsonToQString(json["weather"])); - this->readCustomValues(json); + this->readCustomAttributes(json); return true; } @@ -701,7 +701,7 @@ Event *SignEvent::duplicate() { copy->setFacingDirection(this->getFacingDirection()); copy->setScriptLabel(this->getScriptLabel()); - copy->setCustomValues(this->getCustomValues()); + copy->setCustomAttributes(this->getCustomAttributes()); return copy; } @@ -724,7 +724,7 @@ OrderedJson::object SignEvent::buildEventJson(Project *) { signJson["player_facing_dir"] = this->getFacingDirection(); signJson["script"] = this->getScriptLabel(); - this->addCustomValuesTo(&signJson); + this->addCustomAttributesTo(&signJson); return signJson; } @@ -736,7 +736,7 @@ bool SignEvent::loadFromJson(QJsonObject json, Project *) { this->setFacingDirection(ParseUtil::jsonToQString(json["player_facing_dir"])); this->setScriptLabel(ParseUtil::jsonToQString(json["script"])); - this->readCustomValues(json); + this->readCustomAttributes(json); return true; } @@ -774,7 +774,7 @@ Event *HiddenItemEvent::duplicate() { copy->setQuantity(this->getQuantity()); copy->setQuantity(this->getQuantity()); - copy->setCustomValues(this->getCustomValues()); + copy->setCustomAttributes(this->getCustomAttributes()); return copy; } @@ -803,7 +803,7 @@ OrderedJson::object HiddenItemEvent::buildEventJson(Project *) { hiddenItemJson["underfoot"] = this->getUnderfoot(); } - this->addCustomValuesTo(&hiddenItemJson); + this->addCustomAttributesTo(&hiddenItemJson); return hiddenItemJson; } @@ -821,7 +821,7 @@ bool HiddenItemEvent::loadFromJson(QJsonObject json, Project *) { this->setUnderfoot(ParseUtil::jsonToBool(json["underfoot"])); } - this->readCustomValues(json); + this->readCustomAttributes(json); return true; } @@ -867,7 +867,7 @@ Event *SecretBaseEvent::duplicate() { copy->setElevation(this->getElevation()); copy->setBaseID(this->getBaseID()); - copy->setCustomValues(this->getCustomValues()); + copy->setCustomAttributes(this->getCustomAttributes()); return copy; } @@ -889,7 +889,7 @@ OrderedJson::object SecretBaseEvent::buildEventJson(Project *) { secretBaseJson["elevation"] = this->getElevation(); secretBaseJson["secret_base_id"] = this->getBaseID(); - this->addCustomValuesTo(&secretBaseJson); + this->addCustomAttributesTo(&secretBaseJson); return secretBaseJson; } @@ -900,7 +900,7 @@ bool SecretBaseEvent::loadFromJson(QJsonObject json, Project *) { this->setElevation(ParseUtil::jsonToInt(json["elevation"])); this->setBaseID(ParseUtil::jsonToQString(json["secret_base_id"])); - this->readCustomValues(json); + this->readCustomAttributes(json); return true; } diff --git a/src/editor.cpp b/src/editor.cpp index f3fb1c99..3c7fee27 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -10,7 +10,7 @@ #include "editcommands.h" #include "config.h" #include "scripting.h" -#include "customattributestable.h" +#include "customattributesframe.h" #include "validator.h" #include #include @@ -52,6 +52,7 @@ Editor::Editor(Ui::MainWindow* ui) connect(ui->toolButton_Open_Scripts, &QToolButton::pressed, this, &Editor::openMapScripts); connect(ui->actionOpen_Project_in_Text_Editor, &QAction::triggered, this, &Editor::openProjectInTextEditor); connect(ui->checkBox_ToggleGrid, &QCheckBox::toggled, this, &Editor::toggleGrid); + connect(ui->mapCustomAttributesFrame->table(), &CustomAttributesTable::edited, this, &Editor::updateCustomMapAttributes); } Editor::~Editor() @@ -1975,9 +1976,9 @@ void Editor::updateBorderVisibility() { } } -void Editor::updateCustomMapHeaderValues(QTableWidget *table) +void Editor::updateCustomMapAttributes() { - map->setCustomAttributes(CustomAttributesTable::getAttributes(table)); + map->setCustomAttributes(ui->mapCustomAttributesFrame->table()->getAttributes()); map->modify(); } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index bf9a91e8..d0ba8040 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -7,7 +7,7 @@ #include "eventframes.h" #include "bordermetatilespixmapitem.h" #include "currentselectedmetatilespixmapitem.h" -#include "customattributestable.h" +#include "customattributesframe.h" #include "scripting.h" #include "adjustingstackedwidget.h" #include "draggablepixmapitem.h" @@ -1033,16 +1033,7 @@ void MainWindow::displayMapProperties() { ui->comboBox_PrimaryTileset->setCurrentText(editor->map->layout()->tileset_primary_label); ui->comboBox_SecondaryTileset->setCurrentText(editor->map->layout()->tileset_secondary_label); - - // Custom fields table. -/* // TODO: Re-enable - ui->tableWidget_CustomHeaderFields->blockSignals(true); - ui->tableWidget_CustomHeaderFields->setRowCount(0); - for (auto it = map->customHeaders.begin(); it != map->customHeaders.end(); it++) - CustomAttributesTable::addAttribute(ui->tableWidget_CustomHeaderFields, it.key(), it.value()); - ui->tableWidget_CustomHeaderFields->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); - ui->tableWidget_CustomHeaderFields->blockSignals(false); -*/ + ui->mapCustomAttributesFrame->table()->setAttributes(editor->map->customAttributes()); } void MainWindow::on_comboBox_LayoutSelector_currentTextChanged(const QString &text) { @@ -1119,6 +1110,8 @@ bool MainWindow::setProjectUI() { this->layoutListProxyModel->setSourceModel(this->layoutTreeModel); ui->layoutList->setModel(layoutListProxyModel); + ui->mapCustomAttributesFrame->table()->setRestrictedKeys(project->topLevelMapFields); + return true; } @@ -2867,27 +2860,6 @@ void MainWindow::reloadScriptEngine() { Scripting::cb_MapOpened(editor->map->name()); // TODO: API should have equivalent for layout } -void MainWindow::on_pushButton_AddCustomHeaderField_clicked() -{ - bool ok; - QJsonValue value = CustomAttributesTable::pickType(this, &ok); - if (ok){ - CustomAttributesTable::addAttribute(this->ui->tableWidget_CustomHeaderFields, "", value, true); - this->editor->updateCustomMapHeaderValues(this->ui->tableWidget_CustomHeaderFields); - } -} - -void MainWindow::on_pushButton_DeleteCustomHeaderField_clicked() -{ - if (CustomAttributesTable::deleteSelectedAttributes(this->ui->tableWidget_CustomHeaderFields)) - this->editor->updateCustomMapHeaderValues(this->ui->tableWidget_CustomHeaderFields); -} - -void MainWindow::on_tableWidget_CustomHeaderFields_cellChanged(int, int) -{ - this->editor->updateCustomMapHeaderValues(this->ui->tableWidget_CustomHeaderFields); -} - void MainWindow::on_horizontalSlider_MetatileZoom_valueChanged(int value) { porymapConfig.metatilesZoom = value; double scale = pow(3.0, static_cast(value - 30) / 30.0); diff --git a/src/project.cpp b/src/project.cpp index 712ee87f..9e0a8c3a 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -353,14 +353,13 @@ bool Project::loadMapData(Map* map) { } } - // Check for custom fields -/* // TODO: Re-enable - for (QString key : mapObj.keys()) { - if (!this->topLevelMapFields.contains(key)) { - map->customHeaders.insert(key, mapObj[key]); + QMap customAttributes; + for (auto i = mapObj.constBegin(); i != mapObj.constEnd(); i++) { + if (!this->topLevelMapFields.contains(i.key())) { + customAttributes.insert(i.key(), i.value()); } } -*/ + map->setCustomAttributes(customAttributes); return true; } @@ -1338,11 +1337,10 @@ void Project::saveMap(Map *map) { } // Custom header fields. -/* // TODO: Re-enable - for (QString key : map->customHeaders.keys()) { - mapObj[key] = OrderedJson::fromQJsonValue(map->customHeaders[key]); + const auto customAttributes = map->customAttributes(); + for (auto i = customAttributes.constBegin(); i != customAttributes.constEnd(); i++) { + mapObj[i.key()] = OrderedJson::fromQJsonValue(i.value()); } -*/ OrderedJson mapJson(mapObj); OrderedJsonDoc jsonDoc(&mapJson); diff --git a/src/ui/customattributesdialog.cpp b/src/ui/customattributesdialog.cpp new file mode 100644 index 00000000..7b4a4b84 --- /dev/null +++ b/src/ui/customattributesdialog.cpp @@ -0,0 +1,101 @@ +#include "customattributesdialog.h" +#include "ui_customattributesdialog.h" + +#include + +static int curInputType = 0; + +CustomAttributesDialog::CustomAttributesDialog(CustomAttributesTable *table) : + QDialog(table), + ui(new Ui::CustomAttributesDialog), + m_table(table) +{ + setAttribute(Qt::WA_DeleteOnClose); + ui->setupUi(this); + + // Type combo box + ui->comboBox_Type->addItems({"String", "Number", "Boolean"}); + ui->comboBox_Type->setEditable(false); + + // When the value type is changed, update the value input widget + connect(ui->comboBox_Type, QOverload::of(&QComboBox::currentIndexChanged), this, &CustomAttributesDialog::setInputType); + ui->comboBox_Type->setCurrentIndex(curInputType); + + ui->spinBox_Value->setMinimum(INT_MIN); + ui->spinBox_Value->setMaximum(INT_MAX); + + connect(ui->lineEdit_Name, &QLineEdit::textChanged, this, &CustomAttributesDialog::onNameChanged); + connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &CustomAttributesDialog::clickedButton); + + adjustSize(); +} + +CustomAttributesDialog::~CustomAttributesDialog() { + delete ui; +} + +void CustomAttributesDialog::setInputType(int inputType) { + if (inputType < 0 || inputType >= ui->stackedWidget_Value->count()) + return; + + ui->stackedWidget_Value->setCurrentIndex(inputType); + + // Preserve input widget for later dialogs + curInputType = inputType; +} + +void CustomAttributesDialog::onNameChanged(const QString &) { + validateName(true); +} + +bool CustomAttributesDialog::validateName(bool allowEmpty) { + const QString name = ui->lineEdit_Name->text(); + + QString errorText; + if (name.isEmpty()) { + if (!allowEmpty) errorText = QString("%1 cannot be empty.").arg(ui->label_Name->text()); + } else if (m_table->restrictedKeys().contains(name)) { + errorText = QString("The name '%1' is reserved, please choose a different name.").arg(name); + } + + bool isValid = errorText.isEmpty(); + ui->label_NameError->setText(errorText); + ui->label_NameError->setVisible(!isValid); + ui->lineEdit_Name->setStyleSheet(!isValid ? "QLineEdit { background-color: rgba(255, 0, 0, 25%) }" : ""); + return isValid; +} + +QVariant CustomAttributesDialog::getValue() const { + QVariant value; + auto widget = ui->stackedWidget_Value->currentWidget(); + if (widget == ui->page_String) { + value = QVariant(ui->lineEdit_Value->text()); + } else if (widget == ui->page_Number) { + value = QVariant(ui->spinBox_Value->value()); + } else if (widget == ui->page_Boolean) { + value = QVariant(ui->checkBox_Value->isChecked()); + } + return value; +} + +void CustomAttributesDialog::addNewAttribute() { + m_table->addNewAttribute(ui->lineEdit_Name->text(), QJsonValue::fromVariant(getValue())); +} + +void CustomAttributesDialog::clickedButton(QAbstractButton *button) { + auto buttonRole = ui->buttonBox->buttonRole(button); + if (buttonRole == QDialogButtonBox::AcceptRole && validateName()) { + const QString key = ui->lineEdit_Name->text(); + if (m_table->keys().contains(key)) { + // Warn user if key name would overwrite an existing custom attribute + const QString msg = QString("Overwrite value for existing attribute '%1'?").arg(key); + if (QMessageBox::warning(this, "Warning", msg, QMessageBox::Yes | QMessageBox::Cancel) == QMessageBox::Cancel){ + return; + } + } + addNewAttribute(); + done(QDialog::Accepted); + } else if (buttonRole == QDialogButtonBox::RejectRole) { + done(QDialog::Rejected); + } +} diff --git a/src/ui/customattributesframe.cpp b/src/ui/customattributesframe.cpp new file mode 100644 index 00000000..e9cb6663 --- /dev/null +++ b/src/ui/customattributesframe.cpp @@ -0,0 +1,39 @@ +#include "customattributesframe.h" +#include "ui_customattributesframe.h" +#include "customattributesdialog.h" +#include +#include +#include + +CustomAttributesFrame::CustomAttributesFrame(QWidget *parent) : + QFrame(parent), + ui(new Ui::CustomAttributesFrame) +{ + ui->setupUi(this); + + connect(ui->button_Add, &QPushButton::clicked, this, &CustomAttributesFrame::addAttribute); + connect(ui->button_Delete, &QPushButton::clicked, this, &CustomAttributesFrame::deleteAttribute); + connect(ui->tableWidget, &CustomAttributesTable::itemSelectionChanged, this, &CustomAttributesFrame::updateDeleteButton); + connect(ui->tableWidget, &CustomAttributesTable::edited, this, &CustomAttributesFrame::updateDeleteButton); +} + +CustomAttributesFrame::~CustomAttributesFrame() { + delete ui; +} + +CustomAttributesTable* CustomAttributesFrame::table() const { + return ui->tableWidget; +} + +void CustomAttributesFrame::addAttribute() { + auto dialog = new CustomAttributesDialog(ui->tableWidget); + dialog->open(); +} + +void CustomAttributesFrame::deleteAttribute() { + ui->tableWidget->deleteSelectedAttributes(); +} + +void CustomAttributesFrame::updateDeleteButton() { + ui->button_Delete->setDisabled(ui->tableWidget->isSelectionEmpty()); +} diff --git a/src/ui/customattributestable.cpp b/src/ui/customattributestable.cpp index d87b3f8a..65381443 100644 --- a/src/ui/customattributestable.cpp +++ b/src/ui/customattributestable.cpp @@ -1,196 +1,176 @@ #include "customattributestable.h" #include "parseutil.h" -#include +#include "noscrollspinbox.h" #include -#include -#include -#include #include -#include -CustomAttributesTable::CustomAttributesTable(Event *event, QWidget *parent) : - QFrame(parent) +enum Column { + Key, + Value, + Count +}; + +enum DataRole { + JsonType = Qt::UserRole, + OriginalValue, +}; + +CustomAttributesTable::CustomAttributesTable(QWidget *parent) : + QTableWidget(parent) { - this->event = event; + this->setColumnCount(Column::Count); + this->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); + this->setHorizontalHeaderLabels(QStringList({"Key", "Value"})); + this->horizontalHeader()->setStretchLastSection(true); + this->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); + this->horizontalHeader()->setVisible(false); + this->verticalHeader()->setVisible(false); - QVBoxLayout *layout = new QVBoxLayout(this); - QLabel *label = new QLabel("Custom Attributes"); - layout->addWidget(label); + connect(this, &QTableWidget::cellChanged, this, &CustomAttributesTable::edited); - QFrame *buttonsFrame = new QFrame(this); - buttonsFrame->setLayout(new QHBoxLayout()); - QPushButton *addButton = new QPushButton(this); - QPushButton *deleteButton = new QPushButton(this); - addButton->setText("Add"); - deleteButton->setText("Delete"); - buttonsFrame->layout()->addWidget(addButton); - buttonsFrame->layout()->addWidget(deleteButton); - buttonsFrame->layout()->addItem(new QSpacerItem(1, 1, QSizePolicy::Expanding, QSizePolicy::Fixed)); - buttonsFrame->layout()->setContentsMargins(0, 0, 0, 0); - layout->addWidget(buttonsFrame); - - this->table = new QTableWidget(this); - this->table->setColumnCount(3); - this->table->setHorizontalHeaderLabels(QStringList({"Type", "Key", "Value"})); - this->table->horizontalHeader()->setStretchLastSection(true); - layout->addWidget(this->table); - - QMap customValues = this->event->getCustomValues(); - for (auto it = customValues.begin(); it != customValues.end(); it++) - CustomAttributesTable::addAttribute(this->table, it.key(), it.value()); - - connect(addButton, &QPushButton::clicked, [=]() { - bool ok; - QJsonValue value = CustomAttributesTable::pickType(this, &ok); - if (ok){ - CustomAttributesTable::addAttribute(this->table, "", value, true); - this->event->setCustomValues(CustomAttributesTable::getAttributes(this->table)); - this->resizeVertically(); + // Key cells are uneditable, but users should be allowed to select one and press delete to remove the row. + // Adding the "Selectable" flag to the Key cell changes its appearance to match the Value cell, which + // makes it confusing that you can't edit the Key cell. To keep the uneditable appearance and allow + // deleting rows by selecting Key cells, we select the full row when a Key cell is selected. + connect(this, &QTableWidget::cellPressed, [this](int row, int column) { + if (column == Column::Key) { + this->selectRow(row); } }); - - connect(deleteButton, &QPushButton::clicked, [=]() { - if (CustomAttributesTable::deleteSelectedAttributes(this->table)) { - this->event->setCustomValues(CustomAttributesTable::getAttributes(this->table)); - this->resizeVertically(); - } - }); - - connect(this->table, &QTableWidget::cellChanged, [=]() { - this->event->setCustomValues(CustomAttributesTable::getAttributes(this->table)); - }); - - this->resizeVertically(); } -CustomAttributesTable::~CustomAttributesTable() -{ -} - -void CustomAttributesTable::resizeVertically() { - int horizontalHeaderHeight = this->table->horizontalHeader()->height(); - int rowHeight = 0; - for (int i = 0; i < this->table->rowCount(); i++) { - rowHeight += this->table->rowHeight(0); - } - int totalHeight = horizontalHeaderHeight + rowHeight; - if (this->table->rowCount() == 0) { - totalHeight += 1; - } else { - totalHeight += 2; - } - this->table->setMinimumHeight(totalHeight); - this->table->setMaximumHeight(totalHeight); -} - -const QMap CustomAttributesTable::getAttributes(QTableWidget * table) { +QMap CustomAttributesTable::getAttributes() const { QMap fields; - if (!table) return fields; - - for (int row = 0; row < table->rowCount(); row++) { - QString key = ""; - QTableWidgetItem *typeItem = table->item(row, 0); - QTableWidgetItem *keyItem = table->item(row, 1); - QTableWidgetItem *valueItem = table->item(row, 2); - - if (keyItem) key = keyItem->text(); - if (key.isEmpty() || !typeItem || !valueItem) - continue; - - // Read from the table data which JSON type to save the value as - QJsonValue::Type type = static_cast(typeItem->data(Qt::UserRole).toInt()); - QJsonValue value; - switch (type) - { - case QJsonValue::String: - value = QJsonValue(valueItem->text()); - break; - case QJsonValue::Double: - value = QJsonValue(valueItem->text().toInt()); - break; - case QJsonValue::Bool: - value = QJsonValue(valueItem->checkState() == Qt::Checked); - break; - default: - // All other types will just be preserved - value = valueItem->data(Qt::UserRole).toJsonValue(); - break; - } - fields[key] = value; + for (int row = 0; row < this->rowCount(); row++) { + auto keyValuePair = this->getAttribute(row); + if (!keyValuePair.first.isEmpty()) + fields[keyValuePair.first] = keyValuePair.second; } return fields; } -QJsonValue CustomAttributesTable::pickType(QWidget * parent, bool * ok) { - const QMap valueTypes = { - {"String", QJsonValue(QString(""))}, - {"Number", QJsonValue(0)}, - {"Boolean", QJsonValue(false)}, - }; - QStringList typeNames = valueTypes.keys(); - QString selection = QInputDialog::getItem(parent, "", "Choose Value Type", typeNames, typeNames.indexOf("String"), false, ok); - return valueTypes.value(selection); +QPair CustomAttributesTable::getAttribute(int row) const { + auto keyItem = this->item(row, Column::Key); + if (!keyItem) + return {}; + + // Read from the table data which JSON type to save the value as + QJsonValue::Type type = static_cast(keyItem->data(DataRole::JsonType).toInt()); + + QJsonValue value; + if (type == QJsonValue::String) { + value = QJsonValue(this->item(row, Column::Value)->text()); + } else if (type == QJsonValue::Double) { + auto spinBox = static_cast(this->cellWidget(row, Column::Value)); + value = QJsonValue(spinBox->value()); + } else if (type == QJsonValue::Bool) { + value = QJsonValue(this->item(row, Column::Value)->checkState() == Qt::Checked); + } else { + // All other types will just be preserved + value = this->item(row, Column::Value)->data(DataRole::OriginalValue).toJsonValue(); + } + + return {keyItem->text(), value}; } -void CustomAttributesTable::addAttribute(QTableWidget * table, QString key, QJsonValue value, bool isNew) { - if (!table) return; - QTableWidgetItem * valueItem; +int CustomAttributesTable::addAttribute(const QString &key, const QJsonValue &value) { + // Stop 'edited' signals from being emitted before we finish creating the new table data. + const QSignalBlocker blocker(this); + + // Certain key names cannot be used (if they would overwrite a field used outside this table) + if (m_restrictedKeys.contains(key)) + return -1; + + // Overwrite existing key (if present) + if (m_keys.contains(key)) + this->removeAttribute(key); + + // Add new row + int rowIndex = this->rowCount(); + this->insertRow(rowIndex); + QJsonValue::Type type = value.type(); - switch (type) - { - case QJsonValue::String: - case QJsonValue::Double: - valueItem = new QTableWidgetItem(ParseUtil::jsonToQString(value)); + + // Add key name to table + auto keyItem = new QTableWidgetItem(key); + keyItem->setFlags(Qt::ItemIsEnabled); + keyItem->setData(DataRole::JsonType, type); // Record the type for writing to the file + keyItem->setTextAlignment(Qt::AlignCenter); + keyItem->setToolTip(key); // Display name as tool tip in case it's too long to see in the cell + this->setItem(rowIndex, Column::Key, keyItem); + + // Add value to table + switch (type) { + case QJsonValue::String: { + // Add a regular cell item for editing text + this->setItem(rowIndex, Column::Value, new QTableWidgetItem(ParseUtil::jsonToQString(value))); break; - case QJsonValue::Bool: - valueItem = new QTableWidgetItem(""); + } case QJsonValue::Double: { + // Add a spin box for editing number values + auto spinBox = new NoScrollSpinBox(this); + spinBox->setMinimum(INT_MIN); + spinBox->setMaximum(INT_MAX); + spinBox->setValue(ParseUtil::jsonToInt(value)); + // This connection will be handled by QTableWidget::cellChanged for other cell types + connect(spinBox, QOverload::of(&QSpinBox::valueChanged), this, &CustomAttributesTable::edited); + this->setCellWidget(rowIndex, Column::Value, spinBox); + break; + } case QJsonValue::Bool: { + // Add a checkable cell item for editing bools + auto valueItem = new QTableWidgetItem(""); valueItem->setCheckState(value.toBool() ? Qt::Checked : Qt::Unchecked); valueItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable); + this->setItem(rowIndex, Column::Value, valueItem); break; - default: - valueItem = new QTableWidgetItem("This value cannot be edited from this table"); - valueItem->setFlags(Qt::ItemIsSelectable); - valueItem->setData(Qt::UserRole, value); // Preserve the value for writing to the file + } default: { + // Arrays, objects, or null/undefined values cannot be edited + auto valueItem = new QTableWidgetItem("This value cannot be edited from this table"); + valueItem->setFlags(Qt::NoItemFlags); + valueItem->setData(DataRole::OriginalValue, value); // Preserve the value for writing to the file + this->setItem(rowIndex, Column::Value, valueItem); break; - } + }} + m_keys.insert(key); - const QHash typeToName = { - {QJsonValue::Bool, "Bool"}, - {QJsonValue::Double, "Number"}, - {QJsonValue::String, "String"}, - {QJsonValue::Array, "Array"}, - {QJsonValue::Object, "Object"}, - {QJsonValue::Null, "Null"}, - {QJsonValue::Undefined, "Null"}, - }; - QTableWidgetItem * typeItem = new QTableWidgetItem(typeToName[type]); - typeItem->setFlags(Qt::ItemIsEnabled); - typeItem->setData(Qt::UserRole, type); // Record the type for writing to the file - typeItem->setTextAlignment(Qt::AlignCenter); + return rowIndex; +} - int rowIndex = table->rowCount(); - table->insertRow(rowIndex); - table->setItem(rowIndex, 0, typeItem); - table->setItem(rowIndex, 1, new QTableWidgetItem(key)); - table->setItem(rowIndex, 2, valueItem); +// For the user adding an attribute by interacting with the table +void CustomAttributesTable::addNewAttribute(const QString &key, const QJsonValue &value) { + int row = this->addAttribute(key, value); + if (row < 0) return; + this->resizeVertically(); + this->selectRow(row); + emit this->edited(); +} - if (isNew) { - valueItem->setText(""); // Erase the "0" in new numbers - table->selectRow(rowIndex); +// For programmatically populating the table +void CustomAttributesTable::setAttributes(const QMap &attributes) { + m_keys.clear(); + this->setRowCount(0); // Clear old values + for (auto it = attributes.cbegin(); it != attributes.cend(); it++) + this->addAttribute(it.key(), it.value()); + this->resizeVertically(); +} + +void CustomAttributesTable::removeAttribute(const QString &key) { + for (int row = 0; row < this->rowCount(); row++) { + auto keyItem = this->item(row, Column::Key); + if (keyItem && keyItem->text() == key) { + m_keys.remove(key); + this->removeRow(row); + break; + } } } -bool CustomAttributesTable::deleteSelectedAttributes(QTableWidget * table) { - if (!table) +bool CustomAttributesTable::deleteSelectedAttributes() { + if (this->isEmpty()) return false; - int rowCount = table->rowCount(); - if (rowCount <= 0) - return false; - - QModelIndexList indexList = table->selectionModel()->selectedIndexes(); + QModelIndexList indexList = this->selectionModel()->selectedIndexes(); QList persistentIndexes; - for (QModelIndex index : indexList) { + for (const auto &index : indexList) { QPersistentModelIndex persistentIndex(index); persistentIndexes.append(persistentIndex); } @@ -198,12 +178,51 @@ bool CustomAttributesTable::deleteSelectedAttributes(QTableWidget * table) { if (persistentIndexes.isEmpty()) return false; - for (QPersistentModelIndex index : persistentIndexes) { - table->removeRow(index.row()); + for (const auto &index : persistentIndexes) { + auto row = index.row(); + auto item = this->item(row, Column::Key); + if (item) m_keys.remove(item->text()); + this->removeRow(row); } + this->resizeVertically(); - if (table->rowCount() > 0) { - table->selectRow(0); + if (this->rowCount() > 0) { + this->selectRow(0); } + emit this->edited(); return true; } + +void CustomAttributesTable::resizeVertically() { + int height = 0; + if (this->isEmpty()) { + // Hide header when table is empty + this->horizontalHeader()->setVisible(false); + } else { + for (int i = 0; i < this->rowCount(); i++) + height += this->rowHeight(i); + + // Account for header and horizontal scroll bar + this->horizontalHeader()->setVisible(true); + height += this->horizontalHeader()->height(); + if (this->horizontalScrollBar()->isVisible()) + height += this->horizontalScrollBar()->height(); + height += 2; // Border + } + + this->setMinimumHeight(height); + this->setMaximumHeight(height); +} + +void CustomAttributesTable::resizeEvent(QResizeEvent *event) { + QTableWidget::resizeEvent(event); + this->resizeVertically(); +} + +bool CustomAttributesTable::isEmpty() const { + return this->rowCount() <= 0; +} + +bool CustomAttributesTable::isSelectionEmpty() const { + return this->selectedIndexes().isEmpty(); +} diff --git a/src/ui/eventframes.cpp b/src/ui/eventframes.cpp index cb06fea1..61794c41 100644 --- a/src/ui/eventframes.cpp +++ b/src/ui/eventframes.cpp @@ -1,5 +1,5 @@ #include "eventframes.h" -#include "customattributestable.h" +#include "customattributesframe.h" #include "editcommands.h" #include "draggablepixmapitem.h" @@ -97,8 +97,9 @@ void EventFrame::setup() { } void EventFrame::initCustomAttributesTable() { - CustomAttributesTable *customAttributes = new CustomAttributesTable(this->event, this); - this->layout_contents->addWidget(customAttributes); + this->custom_attributes = new CustomAttributesFrame(this); + this->custom_attributes->table()->setRestrictedKeys(this->event->getExpectedFields()); + this->layout_contents->addWidget(this->custom_attributes); } void EventFrame::connectSignals(MainWindow *) { @@ -128,6 +129,12 @@ void EventFrame::connectSignals(MainWindow *) { this->event->setZ(value); this->event->modify(); }); + + this->custom_attributes->disconnect(); + connect(this->custom_attributes->table(), &CustomAttributesTable::edited, [this]() { + this->event->setCustomAttributes(this->custom_attributes->table()->getAttributes()); + this->event->modify(); + }); } void EventFrame::initialize() { @@ -139,6 +146,8 @@ void EventFrame::initialize() { this->spinner_y->setValue(this->event->getY()); this->spinner_z->setValue(this->event->getZ()); + this->custom_attributes->table()->setAttributes(this->event->getCustomAttributes()); + this->label_icon->setPixmap(this->event->getPixmap()); }