diff --git a/include/config.h b/include/config.h index 9a1f5d8d..1faf0b89 100644 --- a/include/config.h +++ b/include/config.h @@ -29,6 +29,7 @@ public: void save(); void load(); void setSaveDisabled(bool disabled); + void logInvalidKey(const QString &key); virtual ~KeyValueConfigBase(); virtual void reset() = 0; protected: @@ -319,6 +320,8 @@ public: this->blockCollisionMask = 0x0C00; this->blockElevationMask = 0xF000; this->identifiers.clear(); + this->defaultEventCustomAttributes.clear(); + this->defaultMapCustomAttributes.clear(); this->readKeys.clear(); } static const QMap> defaultIdentifiers; @@ -417,6 +420,12 @@ public: int getCollisionSheetHeight(); void setWarpBehaviors(const QSet &behaviors); QSet getWarpBehaviors(); + void insertDefaultEventCustomAttribute(Event::Type eventType, const QString &key, QJsonValue value); + void insertDefaultMapCustomAttribute(const QString &key, QJsonValue value); + void removeDefaultEventCustomAttribute(Event::Type eventType, const QString &key); + void removeDefaultMapCustomAttribute(const QString &key); + QMap getDefaultEventCustomAttributes(Event::Type eventType); + QMap getDefaultMapCustomAttributes(); protected: virtual QString getConfigFilepath() override; @@ -425,6 +434,9 @@ protected: virtual void onNewConfigFileCreated() override; virtual void setUnreadKeys() override; private: + void parseCustomAttributes(const QString &key, const QString &value); + QString customAttributesToString(const QMap attributes); + BaseGameVersion baseGameVersion; QString projectDir; QMap identifiers; @@ -466,6 +478,8 @@ private: int collisionSheetWidth; int collisionSheetHeight; QSet warpBehaviors; + QMap> defaultEventCustomAttributes; + QMap defaultMapCustomAttributes; }; extern ProjectConfig projectConfig; diff --git a/include/core/events.h b/include/core/events.h index 8be1bbfc..681f11aa 100644 --- a/include/core/events.h +++ b/include/core/events.h @@ -158,10 +158,11 @@ 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(QJsonObject values); + void addCustomAttributesTo(OrderedJson::object *obj); + const QMap getCustomAttributes() { return this->customAttributes; } + void setCustomAttributes(const QMap newCustomAttributes) { this->customAttributes = newCustomAttributes; } + void setDefaultCustomAttributes(); virtual void loadPixmap(Project *project); @@ -203,7 +204,7 @@ protected: int spriteHeight = 16; bool usingSprite = false; - QMap customValues; + QMap customAttributes; QPixmap pixmap; DraggablePixmapItem *pixmapItem = nullptr; diff --git a/include/mainwindow.h b/include/mainwindow.h index 352e6ccf..827eafaa 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -379,7 +379,7 @@ private: Event::Group getEventGroupFromTabWidget(QWidget *tab); void closeSupplementaryWindows(); void setWindowDisabled(bool); - + void resetMapCustomAttributesTable(); void initTilesetEditor(); bool initRegionMapEditor(bool silent = false); void initShortcutsEditor(); diff --git a/include/ui/customattributesdialog.h b/include/ui/customattributesdialog.h index 749a6691..33dea972 100644 --- a/include/ui/customattributesdialog.h +++ b/include/ui/customattributesdialog.h @@ -23,7 +23,8 @@ private: CustomAttributesTable *const table; void addNewAttribute(); - bool verifyName(); + bool verifyInput(); + QVariant getValue() const; void setNameEditHighlight(bool highlight); }; diff --git a/include/ui/customattributestable.h b/include/ui/customattributestable.h index 425b5f1f..629597cc 100644 --- a/include/ui/customattributestable.h +++ b/include/ui/customattributestable.h @@ -16,27 +16,31 @@ public: QMap getAttributes() const; void setAttributes(const QMap attributes); - void addNewAttribute(const QString &key, QJsonValue value); - - void setDefaultAttribute(const QString &key, QJsonValue value); - void unsetDefaultAttribute(const QString &key); + void addNewAttribute(const QString &key, QJsonValue value, bool setAsDefault); QSet keys() const; + QSet defaultKeys() const; QSet restrictedKeys() const; + void setDefaultKeys(const QSet keys); void setRestrictedKeys(const QSet keys); signals: void edited(); + void defaultSet(QString key, QJsonValue value); + void defaultRemoved(QString key); private: QTableWidget *table; - QSet m_keys; - QSet m_restrictedKeys; + QSet m_keys; // All keys currently in the table + QSet m_defaultKeys; // All keys that are in this table by default (whether or not they're present) + QSet m_restrictedKeys; // All keys not allowed in the table int addAttribute(const QString &key, QJsonValue value); void removeAttribute(const QString &key); bool deleteSelectedAttributes(); + void setDefaultAttribute(const QString &key, QJsonValue value); + void unsetDefaultAttribute(const QString &key); void resizeVertically(); }; diff --git a/src/config.cpp b/src/config.cpp index 2dba96e4..2d8c4ede 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -292,6 +292,10 @@ void KeyValueConfigBase::setSaveDisabled(bool disabled) { this->saveDisabled = disabled; } +void KeyValueConfigBase::logInvalidKey(const QString &key) { + logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key)); +} + const QMap mapSortOrderMap = { {MapSortOrder::Group, "group"}, {MapSortOrder::Layout, "layout"}, @@ -400,7 +404,7 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) { } else if (key == "warp_behavior_warning_disabled") { this->warpBehaviorWarningDisabled = getConfigBool(key, value); } else { - logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key)); + logInvalidKey(key); } } @@ -872,14 +876,14 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { if (k != static_cast(-1)) { this->setFilePath(k, value); } else { - logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key)); + logInvalidKey(key); } } else if (key.startsWith("ident/")) { auto identifierId = reverseDefaultIdentifier(key.mid(6)); if (identifierId != static_cast(-1)) { this->setIdentifier(identifierId, value); } else { - logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key)); + logInvalidKey(key); } } else if (key == "prefabs_filepath") { this->prefabFilepath = value; @@ -913,8 +917,10 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { QStringList behaviorList = value.split(",", Qt::SkipEmptyParts); for (auto s : behaviorList) this->warpBehaviors.insert(getConfigUint32(key, s)); + } else if (key.startsWith("custom_attributes/")) { + this->parseCustomAttributes(key, value); } else { - logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key)); + logInvalidKey(key); } readKeys.append(key); } @@ -1004,6 +1010,12 @@ QMap ProjectConfig::getKeyValueMap() { for (auto value : this->warpBehaviors) warpBehaviorStrs.append("0x" + QString("%1").arg(value, 2, 16, QChar('0')).toUpper()); map.insert("warp_behaviors", warpBehaviorStrs.join(",")); + if (!this->defaultMapCustomAttributes.isEmpty()) + map.insert("custom_attributes/header", this->customAttributesToString(this->defaultMapCustomAttributes)); + for (auto i = this->defaultEventCustomAttributes.cbegin(), end = this->defaultEventCustomAttributes.cend(); i != end; i++) { + if (!i.value().isEmpty()) + map.insert("custom_attributes/" + Event::eventTypeToString(i.key()), this->customAttributesToString(i.value())); + } return map; } @@ -1466,6 +1478,123 @@ QSet ProjectConfig::getWarpBehaviors() { return this->warpBehaviors; } +void ProjectConfig::insertDefaultEventCustomAttribute(Event::Type eventType, const QString &key, QJsonValue value) { + this->defaultEventCustomAttributes[eventType].insert(key, value); + this->save(); +} + +void ProjectConfig::insertDefaultMapCustomAttribute(const QString &key, QJsonValue value) { + this->defaultMapCustomAttributes.insert(key, value); + this->save(); +} + +void ProjectConfig::removeDefaultEventCustomAttribute(Event::Type eventType, const QString &key) { + this->defaultEventCustomAttributes[eventType].remove(key); + this->save(); +} + +void ProjectConfig::removeDefaultMapCustomAttribute(const QString &key) { + this->defaultMapCustomAttributes.remove(key); + this->save(); +} + +QMap ProjectConfig::getDefaultEventCustomAttributes(Event::Type eventType) { + return this->defaultEventCustomAttributes.value(eventType); +} + +QMap ProjectConfig::getDefaultMapCustomAttributes() { + return this->defaultMapCustomAttributes; +} + +void ProjectConfig::parseCustomAttributes(const QString &key, const QString &value) { + static const QRegularExpression regex("custom_attributes/(?\\w+)"); + auto match = regex.match(key); + if (!match.hasMatch()){ + logInvalidKey(key); + return; + } + + // Value should be a comma-separated list of sequences of the form 'key:type:value'. + // Some day if this config file is formatted as JSON data we wouldn't need to store 'type' (among other simplifications). + QMap map; + const QStringList attributeSequences = value.split(",", Qt::SkipEmptyParts); + if (attributeSequences.isEmpty()) + return; + for (auto sequence : attributeSequences) { + // Parse each 'type:key:value' sequence + const QStringList attributeData = sequence.split(":"); + if (attributeData.length() != 3) { + logWarn(QString("Invalid value '%1' for custom attribute in '%2'").arg(sequence).arg(key)); + continue; + } + const QString attrKey = attributeData.at(0); + const QString attrType = attributeData.at(1); + const QString attrValue = attributeData.at(2); + + QJsonValue value; + if (attrType == "string") { + value = QJsonValue(attrValue); + } else if (attrType == "number") { + bool ok; + int num = attrValue.toInt(&ok, 0); + if (!ok) + logWarn(QString("Invalid value '%1' for custom attribute '%2' in '%3'").arg(attrValue).arg(attrKey).arg(key)); + value = QJsonValue(num); + } else if (attrType == "bool") { + bool ok; + int num = attrValue.toInt(&ok, 0); + if (!ok || (num != 0 && num != 1)) + logWarn(QString("Invalid value '%1' for custom attribute '%2' in '%3'").arg(attrValue).arg(attrKey).arg(key)); + value = QJsonValue(num == 1); + } else { + logWarn(QString("Invalid value type '%1' for custom attribute '%2' in '%3'").arg(attrType).arg(attrKey).arg(key)); + continue; + } + // Successfully parsed a 'type:key:value' sequence + map.insert(attrKey, value); + } + + // Determine who the custom attribute map belongs to (either the map header or some Event type) + const QString identifier = match.captured("identifier"); + + if (identifier == "header") { + this->defaultMapCustomAttributes = map; + return; + } + + Event::Type eventType = Event::eventTypeFromString(identifier); + if (eventType != Event::Type::None) { + this->defaultEventCustomAttributes[eventType] = map; + return; + } + + logWarn(QString("Invalid custom attributes identifier '%1' in '%2'").arg(identifier).arg(key)); +} + +// Assemble comma-separated list of sequences of the form 'key:type:value'. +QString ProjectConfig::customAttributesToString(const QMap attributes) { + QStringList output; + for (auto i = attributes.cbegin(), end = attributes.cend(); i != end; i++) { + QString value; + QString typeStr; + QJsonValue::Type type = i.value().type(); + if (type == QJsonValue::Type::String) { + typeStr = "string"; + value = i.value().toString(); + } else if (type == QJsonValue::Type::Double) { + typeStr = "number"; + value = QString::number(i.value().toInt()); + } else if (type == QJsonValue::Type::Bool) { + typeStr = "bool"; + value = QString::number(i.value().toBool()); + } else { + continue; + } + output.append(QString("%1:%2:%3").arg(i.key()).arg(typeStr).arg(value)); + } + return output.join(","); +} + UserConfig userConfig; @@ -1482,7 +1611,7 @@ void UserConfig::parseConfigKeyValue(QString key, QString value) { } else if (key == "custom_scripts") { this->parseCustomScripts(value); } else { - logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key)); + logInvalidKey(key); } readKeys.append(key); } diff --git a/src/core/events.cpp b/src/core/events.cpp index 1b9605fb..c0b7f3f6 100644 --- a/src/core/events.cpp +++ b/src/core/events.cpp @@ -36,22 +36,27 @@ void Event::setDefaultValues(Project *) { this->setX(0); this->setY(0); this->setElevation(projectConfig.getDefaultElevation()); + this->setDefaultCustomAttributes(); } -void Event::readCustomValues(QJsonObject values) { - this->customValues.clear(); +void Event::setDefaultCustomAttributes() { + this->setCustomAttributes(projectConfig.getDefaultEventCustomAttributes(this->getEventType())); +} + +void Event::readCustomAttributes(QJsonObject values) { + this->customAttributes.clear(); QSet expectedFields = this->getExpectedFields(); for (QString key : values.keys()) { if (!expectedFields.contains(key)) { - this->customValues[key] = values[key]; + this->customAttributes[key] = values[key]; } } } -void Event::addCustomValuesTo(OrderedJson::object *obj) { - for (QString key : this->customValues.keys()) { +void Event::addCustomAttributesTo(OrderedJson::object *obj) { + for (QString key : this->customAttributes.keys()) { if (!obj->contains(key)) { - (*obj)[key] = OrderedJson::fromQJsonValue(this->customValues[key]); + (*obj)[key] = OrderedJson::fromQJsonValue(this->customAttributes[key]); } } } @@ -179,7 +184,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; } @@ -209,7 +214,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; } @@ -227,7 +232,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; } @@ -242,6 +247,7 @@ void ObjectEvent::setDefaultValues(Project *project) { this->setRadiusY(0); this->setSightRadiusBerryTreeID("0"); this->setFrameFromMovement(project->facingDirections.value(this->getMovement())); + this->setDefaultCustomAttributes(); } const QSet expectedObjectFields = { @@ -353,7 +359,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; } @@ -375,7 +381,7 @@ OrderedJson::object CloneObjectEvent::buildEventJson(Project *project) { cloneJson["y"] = this->getY(); cloneJson["target_local_id"] = this->getTargetID(); cloneJson["target_map"] = project->mapNamesToMapConstants.value(this->getTargetMap()); - this->addCustomValuesTo(&cloneJson); + this->addCustomAttributesTo(&cloneJson); return cloneJson; } @@ -398,7 +404,7 @@ bool CloneObjectEvent::loadFromJson(QJsonObject json, Project *project) { this->setTargetMap(DYNAMIC_MAP_NAME); } - this->readCustomValues(json); + this->readCustomAttributes(json); return true; } @@ -407,6 +413,7 @@ void CloneObjectEvent::setDefaultValues(Project *project) { this->setGfx(project->gfxDefines.keys().first()); this->setTargetID(1); if (this->getMap()) this->setTargetMap(this->getMap()->name); + this->setDefaultCustomAttributes(); } const QSet expectedCloneObjectFields = { @@ -465,7 +472,7 @@ Event *WarpEvent::duplicate() { copy->setDestinationMap(this->getDestinationMap()); copy->setDestinationWarpID(this->getDestinationWarpID()); - copy->setCustomValues(this->getCustomValues()); + copy->setCustomAttributes(this->getCustomAttributes()); return copy; } @@ -487,7 +494,7 @@ OrderedJson::object WarpEvent::buildEventJson(Project *project) { warpJson["dest_map"] = project->mapNamesToMapConstants.value(this->getDestinationMap()); warpJson["dest_warp_id"] = this->getDestinationWarpID(); - this->addCustomValuesTo(&warpJson); + this->addCustomAttributesTo(&warpJson); return warpJson; } @@ -510,7 +517,7 @@ bool WarpEvent::loadFromJson(QJsonObject json, Project *project) { this->setDestinationMap(DYNAMIC_MAP_NAME); } - this->readCustomValues(json); + this->readCustomAttributes(json); return true; } @@ -519,6 +526,7 @@ void WarpEvent::setDefaultValues(Project *) { if (this->getMap()) this->setDestinationMap(this->getMap()->name); this->setDestinationWarpID("0"); this->setElevation(0); + this->setDefaultCustomAttributes(); } const QSet expectedWarpFields = { @@ -552,7 +560,7 @@ Event *TriggerEvent::duplicate() { copy->setScriptVarValue(this->getScriptVarValue()); copy->setScriptLabel(this->getScriptLabel()); - copy->setCustomValues(this->getCustomValues()); + copy->setCustomAttributes(this->getCustomAttributes()); return copy; } @@ -576,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; } @@ -589,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; } @@ -599,6 +607,7 @@ void TriggerEvent::setDefaultValues(Project *project) { this->setScriptVar(project->varNames.first()); this->setScriptVarValue("0"); this->setElevation(0); + this->setDefaultCustomAttributes(); } const QSet expectedTriggerFields = { @@ -626,7 +635,7 @@ Event *WeatherTriggerEvent::duplicate() { copy->setElevation(this->getElevation()); copy->setWeather(this->getWeather()); - copy->setCustomValues(this->getCustomValues()); + copy->setCustomAttributes(this->getCustomAttributes()); return copy; } @@ -648,7 +657,7 @@ OrderedJson::object WeatherTriggerEvent::buildEventJson(Project *) { weatherJson["elevation"] = this->getElevation(); weatherJson["weather"] = this->getWeather(); - this->addCustomValuesTo(&weatherJson); + this->addCustomAttributesTo(&weatherJson); return weatherJson; } @@ -659,7 +668,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; } @@ -667,6 +676,7 @@ bool WeatherTriggerEvent::loadFromJson(QJsonObject json, Project *) { void WeatherTriggerEvent::setDefaultValues(Project *project) { this->setWeather(project->coordEventWeatherNames.first()); this->setElevation(0); + this->setDefaultCustomAttributes(); } const QSet expectedWeatherTriggerFields = { @@ -693,7 +703,7 @@ Event *SignEvent::duplicate() { copy->setFacingDirection(this->getFacingDirection()); copy->setScriptLabel(this->getScriptLabel()); - copy->setCustomValues(this->getCustomValues()); + copy->setCustomAttributes(this->getCustomAttributes()); return copy; } @@ -716,7 +726,7 @@ OrderedJson::object SignEvent::buildEventJson(Project *) { signJson["player_facing_dir"] = this->getFacingDirection(); signJson["script"] = this->getScriptLabel(); - this->addCustomValuesTo(&signJson); + this->addCustomAttributesTo(&signJson); return signJson; } @@ -728,7 +738,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; } @@ -737,6 +747,7 @@ void SignEvent::setDefaultValues(Project *project) { this->setFacingDirection(project->bgEventFacingDirections.first()); this->setScriptLabel("NULL"); this->setElevation(0); + this->setDefaultCustomAttributes(); } const QSet expectedSignFields = { @@ -766,7 +777,7 @@ Event *HiddenItemEvent::duplicate() { copy->setQuantity(this->getQuantity()); copy->setQuantity(this->getQuantity()); - copy->setCustomValues(this->getCustomValues()); + copy->setCustomAttributes(this->getCustomAttributes()); return copy; } @@ -795,7 +806,7 @@ OrderedJson::object HiddenItemEvent::buildEventJson(Project *) { hiddenItemJson["underfoot"] = this->getUnderfoot(); } - this->addCustomValuesTo(&hiddenItemJson); + this->addCustomAttributesTo(&hiddenItemJson); return hiddenItemJson; } @@ -813,7 +824,7 @@ bool HiddenItemEvent::loadFromJson(QJsonObject json, Project *) { this->setUnderfoot(ParseUtil::jsonToBool(json["underfoot"])); } - this->readCustomValues(json); + this->readCustomAttributes(json); return true; } @@ -827,6 +838,7 @@ void HiddenItemEvent::setDefaultValues(Project *project) { if (projectConfig.getHiddenItemRequiresItemfinderEnabled()) { this->setUnderfoot(false); } + this->setDefaultCustomAttributes(); } const QSet expectedHiddenItemFields = { @@ -859,7 +871,7 @@ Event *SecretBaseEvent::duplicate() { copy->setElevation(this->getElevation()); copy->setBaseID(this->getBaseID()); - copy->setCustomValues(this->getCustomValues()); + copy->setCustomAttributes(this->getCustomAttributes()); return copy; } @@ -881,7 +893,7 @@ OrderedJson::object SecretBaseEvent::buildEventJson(Project *) { secretBaseJson["elevation"] = this->getElevation(); secretBaseJson["secret_base_id"] = this->getBaseID(); - this->addCustomValuesTo(&secretBaseJson); + this->addCustomAttributesTo(&secretBaseJson); return secretBaseJson; } @@ -892,7 +904,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; } @@ -900,6 +912,7 @@ bool SecretBaseEvent::loadFromJson(QJsonObject json, Project *) { void SecretBaseEvent::setDefaultValues(Project *project) { this->setBaseID(project->secretBaseIds.first()); this->setElevation(0); + this->setDefaultCustomAttributes(); } const QSet expectedSecretBaseFields = { @@ -931,6 +944,7 @@ OrderedJson::object HealLocationEvent::buildEventJson(Project *) { void HealLocationEvent::setDefaultValues(Project *) { this->setElevation(projectConfig.getDefaultElevation()); + this->setDefaultCustomAttributes(); if (!this->getMap()) return; bool respawnEnabled = projectConfig.getHealLocationRespawnDataEnabled(); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 84200198..e2f29b2d 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -302,10 +302,23 @@ void MainWindow::initEditor() { this->editor->selectedEventIndexChanged(value, Event::Group::Heal); }); + // Connect the Custom Attributes table on the Header tab connect(ui->mapCustomAttributesTable, &CustomAttributesTable::edited, [this]() { this->markMapEdited(); this->editor->updateCustomMapHeaderValues(); }); + connect(ui->mapCustomAttributesTable, &CustomAttributesTable::defaultSet, [](QString key, QJsonValue value) { + projectConfig.insertDefaultMapCustomAttribute(key, value); + }); + connect(ui->mapCustomAttributesTable, &CustomAttributesTable::defaultRemoved, [](QString key) { + projectConfig.removeDefaultMapCustomAttribute(key); + }); +} + +void MainWindow::resetMapCustomAttributesTable() { + QStringList keys = projectConfig.getDefaultMapCustomAttributes().keys(); + ui->mapCustomAttributesTable->setDefaultKeys(QSet(keys.begin(), keys.end())); + ui->mapCustomAttributesTable->setRestrictedKeys(Project::getTopLevelMapFields()); } void MainWindow::initMiscHeapObjects() { @@ -512,8 +525,8 @@ bool MainWindow::openProject(const QString &dir, bool initial) { projectConfig.load(); this->closeSupplementaryWindows(); + this->resetMapCustomAttributesTable(); this->newMapDefaultsSet = false; - ui->mapCustomAttributesTable->setRestrictedKeys(Project::getTopLevelMapFields()); if (isProjectOpen()) Scripting::cb_ProjectClosed(editor->project->root); diff --git a/src/ui/customattributesdialog.cpp b/src/ui/customattributesdialog.cpp index d7845b62..2bc87e56 100644 --- a/src/ui/customattributesdialog.cpp +++ b/src/ui/customattributesdialog.cpp @@ -31,16 +31,23 @@ CustomAttributesDialog::CustomAttributesDialog(CustomAttributesTable *parent) : this->setNameEditHighlight(false); }); + // Exclude delimiters used in the config + static const QRegularExpression expression("[^:,=/]*"); + ui->lineEdit_Name->setValidator(new QRegularExpressionValidator(expression)); + // Button box connect(ui->buttonBox, &QDialogButtonBox::clicked, [this](QAbstractButton *button) { auto buttonRole = ui->buttonBox->buttonRole(button); - if (buttonRole == QDialogButtonBox::AcceptRole && this->verifyName()) { + if (buttonRole == QDialogButtonBox::AcceptRole && this->verifyInput()) { this->addNewAttribute(); this->done(QDialog::Accepted); } else if (buttonRole == QDialogButtonBox::RejectRole) { this->done(QDialog::Rejected); } }); + + ui->spinBox_Value->setMinimum(INT_MIN); + ui->spinBox_Value->setMaximum(INT_MAX); } CustomAttributesDialog::~CustomAttributesDialog() { @@ -51,7 +58,7 @@ void CustomAttributesDialog::setNameEditHighlight(bool highlight) { ui->lineEdit_Name->setStyleSheet(highlight ? "QLineEdit { background-color: rgba(255, 0, 0, 25%) }" : ""); } -bool CustomAttributesDialog::verifyName() { +bool CustomAttributesDialog::verifyInput() { const QString name = ui->lineEdit_Name->text(); if (name.isEmpty()) { @@ -67,8 +74,16 @@ bool CustomAttributesDialog::verifyName() { return false; } + // Warn user if changing the default value of a "Default" custom attribute + if (this->table->defaultKeys().contains(name) && ui->checkBox_Default->isChecked()) { + const QString msg = QString("'%1' is already a default attribute. Replace its default value with '%2'?") + .arg(name) + .arg(this->getValue().toString()); + if (QMessageBox::warning(this, "Warning", msg, QMessageBox::Yes | QMessageBox::Cancel) == QMessageBox::Cancel) + return false; + } // Warn user if key name would overwrite an existing custom attribute - if (this->table->keys().contains(name)) { + else if (this->table->keys().contains(name)) { const QString msg = QString("Overwrite value for existing attribute '%1'?").arg(name); if (QMessageBox::warning(this, "Warning", msg, QMessageBox::Yes | QMessageBox::Cancel) == QMessageBox::Cancel) return false; @@ -77,20 +92,19 @@ bool CustomAttributesDialog::verifyName() { return true; } -void CustomAttributesDialog::addNewAttribute() { - QJsonValue value; +QVariant CustomAttributesDialog::getValue() const { + QVariant value; const QString type = ui->comboBox_Type->currentText(); if (type == "String") { - value = QJsonValue(ui->lineEdit_Value->text()); + value = QVariant(ui->lineEdit_Value->text()); } else if (type == "Number") { - value = QJsonValue(ui->spinBox_Value->value()); + value = QVariant(ui->spinBox_Value->value()); } else if (type == "Boolean") { - value = QJsonValue(ui->checkBox_Value->isChecked()); + value = QVariant(ui->checkBox_Value->isChecked()); } - - const QString key = ui->lineEdit_Name->text(); - this->table->addNewAttribute(key, value); - - if (ui->checkBox_Default->isChecked()) - this->table->setDefaultAttribute(key, value); + return value; +} + +void CustomAttributesDialog::addNewAttribute() { + this->table->addNewAttribute(ui->lineEdit_Name->text(), QJsonValue::fromVariant(this->getValue()), ui->checkBox_Default->isChecked()); } diff --git a/src/ui/customattributestable.cpp b/src/ui/customattributestable.cpp index bc4dea2b..7f08471e 100644 --- a/src/ui/customattributestable.cpp +++ b/src/ui/customattributestable.cpp @@ -15,14 +15,11 @@ enum Column { Count }; -// TODO: Tooltip-- "Custom fields will be added to the map.json file for the current map."? -// TODO: Fix squishing when first element is added -// TODO: Fix Header table size on first open -// TODO: Take control of Delete key? -// TODO: Edit history? CustomAttributesTable::CustomAttributesTable(QWidget *parent) : QFrame(parent) { + this->setAttribute(Qt::WA_DeleteOnClose); + QVBoxLayout *layout = new QVBoxLayout(this); QLabel *label = new QLabel("Custom Attributes"); layout->addWidget(label); @@ -72,9 +69,19 @@ CustomAttributesTable::CustomAttributesTable(QWidget *parent) : // 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. + // Double clicking the Key cell will begin editing the value cell, as it would for double-clicking the value cell. connect(this->table, &QTableWidget::cellClicked, [this](int row, int column) { - if (column == Column::Key) this->table->selectRow(row); + if (column == Column::Key) { + this->table->selectRow(row); + } }); + connect(this->table, &QTableWidget::cellDoubleClicked, [this](int row, int column) { + if (column == Column::Key) { + auto index = this->table->model()->index(row, Column::Value); + this->table->edit(index); + } + }); + // TODO: Right-click for context menu to set a default? } CustomAttributesTable::~CustomAttributesTable() @@ -142,7 +149,7 @@ int CustomAttributesTable::addAttribute(const QString &key, QJsonValue value) { return -1; // Overwrite existing key (if present) - if (this->m_keys.remove(key)) + if (this->m_keys.contains(key)) this->removeAttribute(key); // Add new row @@ -170,6 +177,7 @@ int CustomAttributesTable::addAttribute(const QString &key, QJsonValue value) { 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]() { emit this->edited(); }); this->table->setCellWidget(rowIndex, Column::Value, spinBox); break; @@ -194,9 +202,10 @@ int CustomAttributesTable::addAttribute(const QString &key, QJsonValue value) { } // For the user adding an attribute by interacting with the table -void CustomAttributesTable::addNewAttribute(const QString &key, QJsonValue value) { +void CustomAttributesTable::addNewAttribute(const QString &key, QJsonValue value, bool setAsDefault) { int row = this->addAttribute(key, value); if (row < 0) return; + if (setAsDefault) this->setDefaultAttribute(key, value); this->table->selectRow(row); this->resizeVertically(); } @@ -210,21 +219,25 @@ void CustomAttributesTable::setAttributes(const QMap attrib } void CustomAttributesTable::setDefaultAttribute(const QString &key, QJsonValue value) { - // TODO + m_defaultKeys.insert(key); + emit this->defaultSet(key, value); } void CustomAttributesTable::unsetDefaultAttribute(const QString &key) { - // TODO + if (m_defaultKeys.remove(key)) + emit this->defaultRemoved(key); } void CustomAttributesTable::removeAttribute(const QString &key) { for (int row = 0; row < this->table->rowCount(); row++) { auto keyItem = this->table->item(row, Column::Key); if (keyItem && keyItem->text() == key) { + this->m_keys.remove(key); this->table->removeRow(row); break; } } + // No need to adjust size because this is (at the moment) only used for replacement } bool CustomAttributesTable::deleteSelectedAttributes() { @@ -243,7 +256,10 @@ bool CustomAttributesTable::deleteSelectedAttributes() { return false; for (QPersistentModelIndex index : persistentIndexes) { - this->table->removeRow(index.row()); + auto row = index.row(); + auto item = this->table->item(row, Column::Key); + if (item) this->m_keys.remove(item->text()); + this->table->removeRow(row); } if (this->table->rowCount() > 0) { @@ -257,10 +273,18 @@ QSet CustomAttributesTable::keys() const { return this->m_keys; } +QSet CustomAttributesTable::defaultKeys() const { + return this->m_defaultKeys; +} + QSet CustomAttributesTable::restrictedKeys() const { return this->m_restrictedKeys; } +void CustomAttributesTable::setDefaultKeys(const QSet keys) { + this->m_defaultKeys = keys; +} + void CustomAttributesTable::setRestrictedKeys(const QSet keys) { this->m_restrictedKeys = keys; } diff --git a/src/ui/eventframes.cpp b/src/ui/eventframes.cpp index 08eb2ec5..2231d61f 100644 --- a/src/ui/eventframes.cpp +++ b/src/ui/eventframes.cpp @@ -103,8 +103,9 @@ void EventFrame::setup() { void EventFrame::initCustomAttributesTable() { this->custom_attributes = new CustomAttributesTable(this); + QStringList keys = projectConfig.getDefaultEventCustomAttributes(this->event->getEventType()).keys(); + this->custom_attributes->setDefaultKeys(QSet(keys.begin(), keys.end())); this->custom_attributes->setRestrictedKeys(this->event->getExpectedFields()); - this->custom_attributes->setAttributes(this->event->getCustomValues()); this->layout_contents->addWidget(this->custom_attributes); } @@ -138,9 +139,15 @@ void EventFrame::connectSignals(MainWindow *) { this->custom_attributes->disconnect(); connect(this->custom_attributes, &CustomAttributesTable::edited, [this]() { - this->event->setCustomValues(this->custom_attributes->getAttributes()); + this->event->setCustomAttributes(this->custom_attributes->getAttributes()); this->event->modify(); }); + connect(this->custom_attributes, &CustomAttributesTable::defaultSet, [this](QString key, QJsonValue value) { + projectConfig.insertDefaultEventCustomAttribute(this->event->getEventType(), key, value); + }); + connect(this->custom_attributes, &CustomAttributesTable::defaultRemoved, [this](QString key) { + projectConfig.removeDefaultEventCustomAttribute(this->event->getEventType(), key); + }); } void EventFrame::initialize() { @@ -152,6 +159,8 @@ void EventFrame::initialize() { this->spinner_y->setValue(this->event->getY()); this->spinner_z->setValue(this->event->getZ()); + this->custom_attributes->setAttributes(this->event->getCustomAttributes()); + this->label_icon->setPixmap(this->event->getPixmap()); } diff --git a/src/ui/newmappopup.cpp b/src/ui/newmappopup.cpp index 0239b710..c7a3d0a9 100644 --- a/src/ui/newmappopup.cpp +++ b/src/ui/newmappopup.cpp @@ -303,6 +303,7 @@ void NewMapPopup::on_pushButton_NewMap_Accept_clicked() { if (projectConfig.getFloorNumberEnabled()) { newMap->floorNumber = this->ui->spinBox_NewMap_Floor_Number->value(); } + newMap->customHeaders = projectConfig.getDefaultMapCustomAttributes(); newMap->layout = layout; newMap->layoutId = layout->id;