diff --git a/src/plugins/qmldesigner/components/designsystemview/collectionmodel.cpp b/src/plugins/qmldesigner/components/designsystemview/collectionmodel.cpp index b4e65299cb1..df2908836c4 100644 --- a/src/plugins/qmldesigner/components/designsystemview/collectionmodel.cpp +++ b/src/plugins/qmldesigner/components/designsystemview/collectionmodel.cpp @@ -83,15 +83,132 @@ QHash CollectionModel::roleNames() const return roles; } +bool CollectionModel::insertColumns([[maybe_unused]] int column, int count, const QModelIndex &parent) +{ + // Append column only + if (parent.isValid() || count < 0) + return false; + + bool addSuccess = false; + while (count--) + addSuccess |= m_collection->addTheme(QByteArrayLiteral("theme")).has_value(); + + if (addSuccess) { + beginResetModel(); + updateCache(); + endResetModel(); + } + return true; +} + +bool CollectionModel::removeColumns(int column, int count, const QModelIndex &parent) +{ + const auto sentinelIndex = column + count; + if (parent.isValid() || column < 0 || count < 1 || sentinelIndex > columnCount(parent)) + return false; + + beginResetModel(); + while (column < sentinelIndex) + m_collection->removeTheme(m_themeIdList[column++]); + + updateCache(); + endResetModel(); + return true; +} + +bool CollectionModel::removeRows(int row, int count, const QModelIndex &parent) +{ + const auto sentinelIndex = row + count; + if (parent.isValid() || row < 0 || count < 1 || sentinelIndex > rowCount(parent)) + return false; + + beginResetModel(); + while (row < sentinelIndex) { + auto [groupType, name] = m_propertyInfoList[row++]; + m_collection->removeProperty(groupType, name); + } + updateCache(); + endResetModel(); + return true; +} + void CollectionModel::updateCache() { - m_themeIdList = m_collection->allThemeIds(); - + m_themeIdList.clear(); m_propertyInfoList.clear(); - m_collection->forAllGroups([this](GroupType gt, DSThemeGroup *themeGroup) { - for (auto propName : themeGroup->propertyNames()) - m_propertyInfoList.push_back({gt, propName}); - }); + + if (m_collection) { + m_themeIdList = m_collection->allThemeIds(); + + m_collection->forAllGroups([this](GroupType gt, DSThemeGroup *themeGroup) { + for (auto propName : themeGroup->propertyNames()) + m_propertyInfoList.push_back({gt, propName}); + }); + } +} + +void CollectionModel::addProperty(GroupType group, const QString &name, const QVariant &value, bool isBinding) +{ + if (m_collection->addProperty(group, {name.toUtf8(), value, isBinding})) { + beginResetModel(); + updateCache(); + endResetModel(); + } +} + +bool CollectionModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + switch (role) { + case Qt::EditRole: { + ThemeProperty p = value.value(); + const auto [groupType, propName] = m_propertyInfoList[index.row()]; + p.name = propName; + const ThemeId id = m_themeIdList[index.column()]; + if (m_collection->updateProperty(id, groupType, p)) { + beginResetModel(); + updateCache(); + endResetModel(); + } + } + default: + break; + } + return false; +} + +bool CollectionModel::setHeaderData(int section, + Qt::Orientation orientation, + const QVariant &value, + int role) +{ + if (role != Qt::EditRole) + return false; + + if (section < 0 || (orientation == Qt::Horizontal && section >= columnCount()) + || (orientation == Qt::Vertical && section >= rowCount())) { + return false; // Out of bounds + } + + const auto &newName = value.toString().toUtf8(); + bool success = false; + if (orientation == Qt::Horizontal) { + // Theme + success = m_collection->renameTheme(findThemeId(section), newName); + } else { + // Property Name + if (auto propInfo = findPropertyName(section)) { + auto [groupType, propName] = *propInfo; + success = m_collection->renameProperty(groupType, propName, newName); + } + } + + if (success) { + beginResetModel(); + updateCache(); + endResetModel(); + } + + return success; } ThemeId CollectionModel::findThemeId(int column) const diff --git a/src/plugins/qmldesigner/components/designsystemview/collectionmodel.h b/src/plugins/qmldesigner/components/designsystemview/collectionmodel.h index 2253b7e637a..1eda71702d6 100644 --- a/src/plugins/qmldesigner/components/designsystemview/collectionmodel.h +++ b/src/plugins/qmldesigner/components/designsystemview/collectionmodel.h @@ -14,11 +14,14 @@ using PropInfo = std::pair; class CollectionModel : public QAbstractItemModel { - enum class Roles { GroupRole = Qt::UserRole + 1, BindingRole }; + Q_OBJECT public: + enum class Roles { GroupRole = Qt::UserRole + 1, BindingRole }; + CollectionModel(DSThemeManager *collection); + // QAbstractItemModel Interface int columnCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; @@ -27,10 +30,26 @@ public: QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - QHash roleNames() const override; + // Add Themes + bool insertColumns(int column, int count, const QModelIndex &parent = QModelIndex()) override; + // Remove Themes + bool removeColumns(int column, int count, const QModelIndex &parent = QModelIndex()) override; + // Remove Properties + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; void updateCache(); + Q_INVOKABLE void addProperty(GroupType group, + const QString &name, + const QVariant &value, + bool isBinding); + // Edit property value + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + // Edit property name / Theme name + Q_INVOKABLE bool setHeaderData(int section, + Qt::Orientation orientation, + const QVariant &value, + int role = Qt::EditRole) override; private: ThemeId findThemeId(int column) const; diff --git a/src/plugins/qmldesigner/components/designsystemview/designsysteminterface.cpp b/src/plugins/qmldesigner/components/designsystemview/designsysteminterface.cpp index a62ab663c59..f708739ab50 100644 --- a/src/plugins/qmldesigner/components/designsystemview/designsysteminterface.cpp +++ b/src/plugins/qmldesigner/components/designsystemview/designsysteminterface.cpp @@ -5,6 +5,7 @@ #include "collectionmodel.h" #include +#include #include namespace QmlDesigner { @@ -21,30 +22,64 @@ void DesignSystemInterface::loadDesignSystem() { m_models.clear(); m_store->load(); - emit loadFinished(); + emit collectionsChanged(); } -QAbstractItemModel *DesignSystemInterface::model(const QString &typeName) +CollectionModel *DesignSystemInterface::model(const QString &typeName) { if (auto collection = m_store->collection(typeName)) { auto itr = m_models.find(typeName); if (itr != m_models.end()) return itr->second.get(); - auto [newItr, success] = m_models.try_emplace(typeName, - std::make_unique(*collection)); - if (success) { - // Otherwise the model will be deleted by the QML engine. - QQmlEngine::setObjectOwnership(newItr->second.get(), QQmlEngine::CppOwnership); - return newItr->second.get(); - } + return createModel(typeName, collection); } return nullptr; } +void DesignSystemInterface::addCollection(const QString &name) +{ + if (auto collection = m_store->addCollection(name)) + emit collectionsChanged(); +} + +void DesignSystemInterface::removeCollection(const QString &name) +{ + if (m_store->collection(name)) { + m_models.erase(name); + m_store->removeCollection(name); + emit collectionsChanged(); + } +} + +void DesignSystemInterface::renameCollection(const QString &oldName, const QString &newName) +{ + if (m_store->renameCollection(oldName, newName)) + emit collectionsChanged(); +} + +ThemeProperty DesignSystemInterface::createThemeProperty(const QString &name, + const QVariant &value, + bool isBinding) const +{ + return {name.toUtf8(), value, isBinding}; +} + QStringList DesignSystemInterface::collections() const { return m_store->collectionNames(); } + +CollectionModel *DesignSystemInterface::createModel(const QString &typeName, DSThemeManager *collection) +{ + auto [newItr, success] = m_models.try_emplace(typeName, + std::make_unique(collection)); + if (success) { + // Otherwise the model will be deleted by the QML engine. + QQmlEngine::setObjectOwnership(newItr->second.get(), QQmlEngine::CppOwnership); + return newItr->second.get(); + } + return nullptr; +} } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/designsystemview/designsysteminterface.h b/src/plugins/qmldesigner/components/designsystemview/designsysteminterface.h index e4728e3f0bd..fd732fb88b6 100644 --- a/src/plugins/qmldesigner/components/designsystemview/designsysteminterface.h +++ b/src/plugins/qmldesigner/components/designsystemview/designsysteminterface.h @@ -10,22 +10,35 @@ class QAbstractItemModel; namespace QmlDesigner { class CollectionModel; +class DSThemeManager; + class DesignSystemInterface : public QObject { Q_OBJECT - Q_PROPERTY(QStringList collections READ collections NOTIFY loadFinished FINAL) + Q_PROPERTY(QStringList collections READ collections NOTIFY collectionsChanged FINAL) public: DesignSystemInterface(DSStore *store); ~DesignSystemInterface(); Q_INVOKABLE void loadDesignSystem(); - Q_INVOKABLE QAbstractItemModel *model(const QString &typeName); + Q_INVOKABLE CollectionModel *model(const QString &typeName); + + Q_INVOKABLE void addCollection(const QString &name); + Q_INVOKABLE void removeCollection(const QString &name); + Q_INVOKABLE void renameCollection(const QString &oldName, const QString &newName); + + Q_INVOKABLE ThemeProperty createThemeProperty(const QString &name, + const QVariant &value, + bool isBinding = false) const; QStringList collections() const; signals: - void loadFinished(); + void collectionsChanged(); + +private: + CollectionModel *createModel(const QString &typeName, DSThemeManager *collection); private: class DSStore *m_store = nullptr; diff --git a/src/plugins/qmldesigner/libs/designsystem/dsstore.cpp b/src/plugins/qmldesigner/libs/designsystem/dsstore.cpp index aefba422464..ffa4050c9d5 100644 --- a/src/plugins/qmldesigner/libs/designsystem/dsstore.cpp +++ b/src/plugins/qmldesigner/libs/designsystem/dsstore.cpp @@ -209,6 +209,30 @@ std::optional DSStore::typeName(DSThemeManager *collection) const return {}; } +bool DSStore::removeCollection(const QString &name) +{ + return m_collections.erase(name); +} + +bool DSStore::renameCollection(const QString &oldName, const QString &newName) +{ + auto itr = m_collections.find(oldName); + if (itr == m_collections.end() || oldName == newName) + return false; + + const QString uniqueTypeName = uniqueCollectionName(newName); + + // newName is mutated to make it unique or compatible. Bail. + // Case update is tolerated. + if (uniqueTypeName.toLower() != newName.toLower()) + return false; + + auto handle = m_collections.extract(oldName); + handle.key() = uniqueTypeName; + m_collections.insert(std::move(handle)); + return true; +} + std::optional DSStore::moduleDirPath() const { return dsModuleDir(m_ed); @@ -231,13 +255,13 @@ QString DSStore::uniqueCollectionName(const QString &hint) const }); } -std::optional DSStore::collection(const QString &typeName) +DSThemeManager *DSStore::collection(const QString &typeName) { auto itr = m_collections.find(typeName); if (itr != m_collections.end()) return &itr->second; - return {}; + return nullptr; } std::optional DSStore::loadCollection(const QString &typeName, diff --git a/src/plugins/qmldesigner/libs/designsystem/dsstore.h b/src/plugins/qmldesigner/libs/designsystem/dsstore.h index fd37d13359b..8c0cc9c2d7f 100644 --- a/src/plugins/qmldesigner/libs/designsystem/dsstore.h +++ b/src/plugins/qmldesigner/libs/designsystem/dsstore.h @@ -33,8 +33,10 @@ public: size_t collectionCount() const { return m_collections.size(); } DSThemeManager *addCollection(const QString &qmlTypeName); - std::optional collection(const QString &typeName); + DSThemeManager *collection(const QString &typeName); std::optional typeName(DSThemeManager *collection) const; + bool removeCollection(const QString &name); + bool renameCollection(const QString &oldName, const QString &newName); std::optional moduleDirPath() const; QStringList collectionNames() const; diff --git a/src/plugins/qmldesigner/libs/designsystem/dsthemegroup.cpp b/src/plugins/qmldesigner/libs/designsystem/dsthemegroup.cpp index cc7eeda06fa..a49e21ae012 100644 --- a/src/plugins/qmldesigner/libs/designsystem/dsthemegroup.cpp +++ b/src/plugins/qmldesigner/libs/designsystem/dsthemegroup.cpp @@ -96,38 +96,29 @@ bool DSThemeGroup::hasProperty(const PropertyName &name) const return m_values.contains(name); } -void DSThemeGroup::updateProperty(ThemeId theme, PropertyName newName, const ThemeProperty &prop) +bool DSThemeGroup::updateProperty(ThemeId theme, const ThemeProperty &prop) { if (!m_values.contains(prop.name)) { qCDebug(dsLog) << "Property update failure. Can't find property" << prop; - return; + return false; } - if (!ThemeProperty{newName, prop.value, prop.isBinding}.isValid()) { - qCDebug(dsLog) << "Property update failure. Invalid property" << prop << newName; - return; - } - - if (newName != prop.name && m_values.contains(newName)) { - qCDebug(dsLog) << "Property update failure. Property name update already exists" << newName - << prop; - return; + if (!prop.isValid()) { + qCDebug(dsLog) << "Property update failure. Invalid property" << prop; + return false; } auto &tValues = m_values.at(prop.name); const auto itr = tValues.find(theme); if (itr == tValues.end()) { qCDebug(dsLog) << "Property update failure. No property for the theme" << theme << prop; - return; + return false; } auto &entry = tValues.at(theme); entry.value = prop.value; entry.isBinding = prop.isBinding; - if (newName != prop.name) { - m_values[newName] = std::move(tValues); - m_values.erase(prop.name); - } + return true; } void DSThemeGroup::removeProperty(const PropertyName &name) @@ -135,6 +126,25 @@ void DSThemeGroup::removeProperty(const PropertyName &name) m_values.erase(name); } +bool DSThemeGroup::renameProperty(const PropertyName &name, const PropertyName &newName) +{ + auto itr = m_values.find(name); + if (itr == m_values.end()) { + qCDebug(dsLog) << "Renaming non-existing property" << name; + return false; + } + + if (m_values.contains(newName) || newName.trimmed().isEmpty()) { + qCDebug(dsLog) << "Renaming failed. Invalid new name" << name; + return false; + } + + auto node = m_values.extract(itr); + node.key() = newName; + m_values.insert(std::move(node)); + return true; +} + size_t DSThemeGroup::count(ThemeId theme) const { return std::accumulate(m_values.cbegin(), diff --git a/src/plugins/qmldesigner/libs/designsystem/dsthemegroup.h b/src/plugins/qmldesigner/libs/designsystem/dsthemegroup.h index b549cb74095..edb78fd66ba 100644 --- a/src/plugins/qmldesigner/libs/designsystem/dsthemegroup.h +++ b/src/plugins/qmldesigner/libs/designsystem/dsthemegroup.h @@ -38,8 +38,9 @@ public: std::optional propertyValue(ThemeId theme, const PropertyName &name) const; bool hasProperty(const PropertyName &name) const; - void updateProperty(ThemeId theme, PropertyName newName, const ThemeProperty &prop); + bool updateProperty(ThemeId theme, const ThemeProperty &prop); void removeProperty(const PropertyName &name); + bool renameProperty(const PropertyName &name, const PropertyName &newName); size_t count(ThemeId theme) const; size_t count() const; diff --git a/src/plugins/qmldesigner/libs/designsystem/dsthememanager.cpp b/src/plugins/qmldesigner/libs/designsystem/dsthememanager.cpp index 5b980801bd5..b4faf0d5a4d 100644 --- a/src/plugins/qmldesigner/libs/designsystem/dsthememanager.cpp +++ b/src/plugins/qmldesigner/libs/designsystem/dsthememanager.cpp @@ -73,6 +73,24 @@ ThemeName DSThemeManager::themeName(ThemeId id) const return {}; } +bool DSThemeManager::renameTheme(ThemeId id, const ThemeName &newName) +{ + const ThemeName oldName = themeName(id); + if (oldName.isEmpty()) { + qCDebug(dsLog) << "Invalid theme rename. Theme does not exists. Id:" << id; + return false; + } + + const ThemeName sanitizedName = uniqueThemeName(newName); + if (sanitizedName != newName) { + qCDebug(dsLog) << "Theme rename fail. New name " << newName << " is not valid:"; + return false; + } + + m_themes[id] = sanitizedName; + return true; +} + const std::vector DSThemeManager::allThemeIds() const { std::vector ids; @@ -168,23 +186,29 @@ void DSThemeManager::removeProperty(GroupType gType, const PropertyName &name) dsGroup->removeProperty(name); } -void DSThemeManager::updateProperty(ThemeId id, GroupType gType, const ThemeProperty &p) -{ - updateProperty(id, gType, p, p.name); -} - -void DSThemeManager::updateProperty(ThemeId id, - GroupType gType, - const ThemeProperty &p, - const PropertyName &newName) +bool DSThemeManager::updateProperty(ThemeId id, GroupType gType, const ThemeProperty &prop) { if (!m_themes.contains(id)) - return; + return false; DSThemeGroup *dsGroup = propertyGroup(gType); - QTC_ASSERT(dsGroup, return); + QTC_ASSERT(dsGroup, return false); - dsGroup->updateProperty(id, newName, p); + return dsGroup->updateProperty(id, prop); +} + +bool DSThemeManager::renameProperty(GroupType gType, const PropertyName &name, const PropertyName &newName) +{ + DSThemeGroup *dsGroup = propertyGroup(gType); + QTC_ASSERT(dsGroup, return false); + + const auto generatedName = uniquePropertyName(newName); + if (generatedName != newName) { + qCDebug(dsLog) << "Can not rename property. Invalid property name"; + return false; + } + + return dsGroup->renameProperty(name, newName); } void DSThemeManager::decorate(ModelNode rootNode, const QByteArray &nodeType, bool isMCU) const diff --git a/src/plugins/qmldesigner/libs/designsystem/dsthememanager.h b/src/plugins/qmldesigner/libs/designsystem/dsthememanager.h index dc1396ae423..cf1de885c42 100644 --- a/src/plugins/qmldesigner/libs/designsystem/dsthememanager.h +++ b/src/plugins/qmldesigner/libs/designsystem/dsthememanager.h @@ -36,6 +36,7 @@ public: std::optional addTheme(const ThemeName &themeNameHint); std::optional themeId(const ThemeName &themeName) const; ThemeName themeName(ThemeId id) const; + bool renameTheme(ThemeId id, const ThemeName &newName); const std::vector allThemeIds() const; void forAllGroups(std::function callback) const; @@ -51,8 +52,9 @@ public: GroupType gType, const PropertyName &name) const; void removeProperty(GroupType gType, const PropertyName &p); - void updateProperty(ThemeId id, GroupType gType, const ThemeProperty &p); - void updateProperty(ThemeId id, GroupType gType, const ThemeProperty &p, const PropertyName &newName); + + bool updateProperty(ThemeId id, GroupType gType, const ThemeProperty &prop); + bool renameProperty(GroupType gType, const PropertyName &name, const PropertyName &newName); void decorate(ModelNode rootNode, const QByteArray &nodeType = "QtObject", bool isMCU = false) const; void decorateThemeInterface(ModelNode rootNode) const; diff --git a/tests/unit/tests/unittests/designsystem/dsthememgr-test.cpp b/tests/unit/tests/unittests/designsystem/dsthememgr-test.cpp index 4f04edd5bff..393af1c76d3 100644 --- a/tests/unit/tests/unittests/designsystem/dsthememgr-test.cpp +++ b/tests/unit/tests/unittests/designsystem/dsthememgr-test.cpp @@ -203,7 +203,7 @@ TEST_P(DesignSystemManagerTest, update_property_name) mgr.addProperty(groupType, testProp); // act - mgr.updateProperty(*themeId, groupType, testProp, testPropUpdated.name); + mgr.renameProperty(groupType, testPropNameFoo, testPropNameBar); // assert ASSERT_THAT(mgr, @@ -220,7 +220,7 @@ TEST_P(DesignSystemManagerTest, updating_invalid_property_fails) mgr.addProperty(groupType, testProp); // act - mgr.updateProperty(*themeId, groupType, testProp, testPropUpdated.name); + mgr.updateProperty(*themeId, groupType, testPropUpdated); // assert ASSERT_THAT(mgr,