DesignSystem: Enable design system data model editable

Task-number: QDS-13881
Change-Id: Icead4959b098a7b4cf1a440e69ff462d0645be8f
Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
This commit is contained in:
Vikas Pachdha
2024-11-19 11:17:51 +01:00
parent 8d7da1f7c2
commit c3c7cb653d
11 changed files with 303 additions and 56 deletions

View File

@@ -83,15 +83,132 @@ QHash<int, QByteArray> CollectionModel::roleNames() const
return roles; 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() void CollectionModel::updateCache()
{ {
m_themeIdList.clear();
m_propertyInfoList.clear();
if (m_collection) {
m_themeIdList = m_collection->allThemeIds(); m_themeIdList = m_collection->allThemeIds();
m_propertyInfoList.clear();
m_collection->forAllGroups([this](GroupType gt, DSThemeGroup *themeGroup) { m_collection->forAllGroups([this](GroupType gt, DSThemeGroup *themeGroup) {
for (auto propName : themeGroup->propertyNames()) for (auto propName : themeGroup->propertyNames())
m_propertyInfoList.push_back({gt, propName}); 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<ThemeProperty>();
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 ThemeId CollectionModel::findThemeId(int column) const

View File

@@ -14,11 +14,14 @@ using PropInfo = std::pair<GroupType, PropertyName>;
class CollectionModel : public QAbstractItemModel class CollectionModel : public QAbstractItemModel
{ {
enum class Roles { GroupRole = Qt::UserRole + 1, BindingRole }; Q_OBJECT
public: public:
enum class Roles { GroupRole = Qt::UserRole + 1, BindingRole };
CollectionModel(DSThemeManager *collection); CollectionModel(DSThemeManager *collection);
// QAbstractItemModel Interface
int columnCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) 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; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
@@ -27,10 +30,26 @@ public:
QVariant headerData(int section, QVariant headerData(int section,
Qt::Orientation orientation, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override; int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> 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(); 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: private:
ThemeId findThemeId(int column) const; ThemeId findThemeId(int column) const;

View File

@@ -5,6 +5,7 @@
#include "collectionmodel.h" #include "collectionmodel.h"
#include <designsystem/dsconstants.h> #include <designsystem/dsconstants.h>
#include <designsystem/dsthememanager.h>
#include <QQmlEngine> #include <QQmlEngine>
namespace QmlDesigner { namespace QmlDesigner {
@@ -21,30 +22,64 @@ void DesignSystemInterface::loadDesignSystem()
{ {
m_models.clear(); m_models.clear();
m_store->load(); 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)) { if (auto collection = m_store->collection(typeName)) {
auto itr = m_models.find(typeName); auto itr = m_models.find(typeName);
if (itr != m_models.end()) if (itr != m_models.end())
return itr->second.get(); return itr->second.get();
auto [newItr, success] = m_models.try_emplace(typeName, return createModel(typeName, collection);
std::make_unique<CollectionModel>(*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; 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 QStringList DesignSystemInterface::collections() const
{ {
return m_store->collectionNames(); return m_store->collectionNames();
} }
CollectionModel *DesignSystemInterface::createModel(const QString &typeName, DSThemeManager *collection)
{
auto [newItr, success] = m_models.try_emplace(typeName,
std::make_unique<CollectionModel>(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 } // namespace QmlDesigner

View File

@@ -10,22 +10,35 @@ class QAbstractItemModel;
namespace QmlDesigner { namespace QmlDesigner {
class CollectionModel; class CollectionModel;
class DSThemeManager;
class DesignSystemInterface : public QObject class DesignSystemInterface : public QObject
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QStringList collections READ collections NOTIFY loadFinished FINAL) Q_PROPERTY(QStringList collections READ collections NOTIFY collectionsChanged FINAL)
public: public:
DesignSystemInterface(DSStore *store); DesignSystemInterface(DSStore *store);
~DesignSystemInterface(); ~DesignSystemInterface();
Q_INVOKABLE void loadDesignSystem(); 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; QStringList collections() const;
signals: signals:
void loadFinished(); void collectionsChanged();
private:
CollectionModel *createModel(const QString &typeName, DSThemeManager *collection);
private: private:
class DSStore *m_store = nullptr; class DSStore *m_store = nullptr;

View File

@@ -209,6 +209,30 @@ std::optional<QString> DSStore::typeName(DSThemeManager *collection) const
return {}; 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<Utils::FilePath> DSStore::moduleDirPath() const std::optional<Utils::FilePath> DSStore::moduleDirPath() const
{ {
return dsModuleDir(m_ed); return dsModuleDir(m_ed);
@@ -231,13 +255,13 @@ QString DSStore::uniqueCollectionName(const QString &hint) const
}); });
} }
std::optional<DSThemeManager *> DSStore::collection(const QString &typeName) DSThemeManager *DSStore::collection(const QString &typeName)
{ {
auto itr = m_collections.find(typeName); auto itr = m_collections.find(typeName);
if (itr != m_collections.end()) if (itr != m_collections.end())
return &itr->second; return &itr->second;
return {}; return nullptr;
} }
std::optional<QString> DSStore::loadCollection(const QString &typeName, std::optional<QString> DSStore::loadCollection(const QString &typeName,

View File

@@ -33,8 +33,10 @@ public:
size_t collectionCount() const { return m_collections.size(); } size_t collectionCount() const { return m_collections.size(); }
DSThemeManager *addCollection(const QString &qmlTypeName); DSThemeManager *addCollection(const QString &qmlTypeName);
std::optional<DSThemeManager *> collection(const QString &typeName); DSThemeManager *collection(const QString &typeName);
std::optional<QString> typeName(DSThemeManager *collection) const; std::optional<QString> typeName(DSThemeManager *collection) const;
bool removeCollection(const QString &name);
bool renameCollection(const QString &oldName, const QString &newName);
std::optional<Utils::FilePath> moduleDirPath() const; std::optional<Utils::FilePath> moduleDirPath() const;
QStringList collectionNames() const; QStringList collectionNames() const;

View File

@@ -96,38 +96,29 @@ bool DSThemeGroup::hasProperty(const PropertyName &name) const
return m_values.contains(name); 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)) { if (!m_values.contains(prop.name)) {
qCDebug(dsLog) << "Property update failure. Can't find property" << prop; qCDebug(dsLog) << "Property update failure. Can't find property" << prop;
return; return false;
} }
if (!ThemeProperty{newName, prop.value, prop.isBinding}.isValid()) { if (!prop.isValid()) {
qCDebug(dsLog) << "Property update failure. Invalid property" << prop << newName; qCDebug(dsLog) << "Property update failure. Invalid property" << prop;
return; return false;
}
if (newName != prop.name && m_values.contains(newName)) {
qCDebug(dsLog) << "Property update failure. Property name update already exists" << newName
<< prop;
return;
} }
auto &tValues = m_values.at(prop.name); auto &tValues = m_values.at(prop.name);
const auto itr = tValues.find(theme); const auto itr = tValues.find(theme);
if (itr == tValues.end()) { if (itr == tValues.end()) {
qCDebug(dsLog) << "Property update failure. No property for the theme" << theme << prop; qCDebug(dsLog) << "Property update failure. No property for the theme" << theme << prop;
return; return false;
} }
auto &entry = tValues.at(theme); auto &entry = tValues.at(theme);
entry.value = prop.value; entry.value = prop.value;
entry.isBinding = prop.isBinding; entry.isBinding = prop.isBinding;
if (newName != prop.name) { return true;
m_values[newName] = std::move(tValues);
m_values.erase(prop.name);
}
} }
void DSThemeGroup::removeProperty(const PropertyName &name) void DSThemeGroup::removeProperty(const PropertyName &name)
@@ -135,6 +126,25 @@ void DSThemeGroup::removeProperty(const PropertyName &name)
m_values.erase(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 size_t DSThemeGroup::count(ThemeId theme) const
{ {
return std::accumulate(m_values.cbegin(), return std::accumulate(m_values.cbegin(),

View File

@@ -38,8 +38,9 @@ public:
std::optional<ThemeProperty> propertyValue(ThemeId theme, const PropertyName &name) const; std::optional<ThemeProperty> propertyValue(ThemeId theme, const PropertyName &name) const;
bool hasProperty(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); void removeProperty(const PropertyName &name);
bool renameProperty(const PropertyName &name, const PropertyName &newName);
size_t count(ThemeId theme) const; size_t count(ThemeId theme) const;
size_t count() const; size_t count() const;

View File

@@ -73,6 +73,24 @@ ThemeName DSThemeManager::themeName(ThemeId id) const
return {}; 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<ThemeId> DSThemeManager::allThemeIds() const const std::vector<ThemeId> DSThemeManager::allThemeIds() const
{ {
std::vector<ThemeId> ids; std::vector<ThemeId> ids;
@@ -168,23 +186,29 @@ void DSThemeManager::removeProperty(GroupType gType, const PropertyName &name)
dsGroup->removeProperty(name); dsGroup->removeProperty(name);
} }
void DSThemeManager::updateProperty(ThemeId id, GroupType gType, const ThemeProperty &p) bool DSThemeManager::updateProperty(ThemeId id, GroupType gType, const ThemeProperty &prop)
{
updateProperty(id, gType, p, p.name);
}
void DSThemeManager::updateProperty(ThemeId id,
GroupType gType,
const ThemeProperty &p,
const PropertyName &newName)
{ {
if (!m_themes.contains(id)) if (!m_themes.contains(id))
return; return false;
DSThemeGroup *dsGroup = propertyGroup(gType); 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 void DSThemeManager::decorate(ModelNode rootNode, const QByteArray &nodeType, bool isMCU) const

View File

@@ -36,6 +36,7 @@ public:
std::optional<ThemeId> addTheme(const ThemeName &themeNameHint); std::optional<ThemeId> addTheme(const ThemeName &themeNameHint);
std::optional<ThemeId> themeId(const ThemeName &themeName) const; std::optional<ThemeId> themeId(const ThemeName &themeName) const;
ThemeName themeName(ThemeId id) const; ThemeName themeName(ThemeId id) const;
bool renameTheme(ThemeId id, const ThemeName &newName);
const std::vector<ThemeId> allThemeIds() const; const std::vector<ThemeId> allThemeIds() const;
void forAllGroups(std::function<void(GroupType, DSThemeGroup *)> callback) const; void forAllGroups(std::function<void(GroupType, DSThemeGroup *)> callback) const;
@@ -51,8 +52,9 @@ public:
GroupType gType, GroupType gType,
const PropertyName &name) const; const PropertyName &name) const;
void removeProperty(GroupType gType, const PropertyName &p); 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 decorate(ModelNode rootNode, const QByteArray &nodeType = "QtObject", bool isMCU = false) const;
void decorateThemeInterface(ModelNode rootNode) const; void decorateThemeInterface(ModelNode rootNode) const;

View File

@@ -203,7 +203,7 @@ TEST_P(DesignSystemManagerTest, update_property_name)
mgr.addProperty(groupType, testProp); mgr.addProperty(groupType, testProp);
// act // act
mgr.updateProperty(*themeId, groupType, testProp, testPropUpdated.name); mgr.renameProperty(groupType, testPropNameFoo, testPropNameBar);
// assert // assert
ASSERT_THAT(mgr, ASSERT_THAT(mgr,
@@ -220,7 +220,7 @@ TEST_P(DesignSystemManagerTest, updating_invalid_property_fails)
mgr.addProperty(groupType, testProp); mgr.addProperty(groupType, testProp);
// act // act
mgr.updateProperty(*themeId, groupType, testProp, testPropUpdated.name); mgr.updateProperty(*themeId, groupType, testPropUpdated);
// assert // assert
ASSERT_THAT(mgr, ASSERT_THAT(mgr,