forked from qt-creator/qt-creator
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:
@@ -83,15 +83,132 @@ QHash<int, QByteArray> 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.clear();
|
||||
m_propertyInfoList.clear();
|
||||
|
||||
if (m_collection) {
|
||||
m_themeIdList = m_collection->allThemeIds();
|
||||
|
||||
m_propertyInfoList.clear();
|
||||
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<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
|
||||
|
@@ -14,11 +14,14 @@ using PropInfo = std::pair<GroupType, PropertyName>;
|
||||
|
||||
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<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();
|
||||
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;
|
||||
|
@@ -5,6 +5,7 @@
|
||||
#include "collectionmodel.h"
|
||||
|
||||
#include <designsystem/dsconstants.h>
|
||||
#include <designsystem/dsthememanager.h>
|
||||
#include <QQmlEngine>
|
||||
|
||||
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<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 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<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
|
||||
|
@@ -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;
|
||||
|
@@ -209,6 +209,30 @@ std::optional<QString> 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<Utils::FilePath> DSStore::moduleDirPath() const
|
||||
{
|
||||
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);
|
||||
if (itr != m_collections.end())
|
||||
return &itr->second;
|
||||
|
||||
return {};
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::optional<QString> DSStore::loadCollection(const QString &typeName,
|
||||
|
@@ -33,8 +33,10 @@ public:
|
||||
size_t collectionCount() const { return m_collections.size(); }
|
||||
|
||||
DSThemeManager *addCollection(const QString &qmlTypeName);
|
||||
std::optional<DSThemeManager *> collection(const QString &typeName);
|
||||
DSThemeManager *collection(const QString &typeName);
|
||||
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;
|
||||
QStringList collectionNames() const;
|
||||
|
@@ -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(),
|
||||
|
@@ -38,8 +38,9 @@ public:
|
||||
std::optional<ThemeProperty> 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;
|
||||
|
@@ -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<ThemeId> DSThemeManager::allThemeIds() const
|
||||
{
|
||||
std::vector<ThemeId> 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
|
||||
|
@@ -36,6 +36,7 @@ public:
|
||||
std::optional<ThemeId> addTheme(const ThemeName &themeNameHint);
|
||||
std::optional<ThemeId> themeId(const ThemeName &themeName) const;
|
||||
ThemeName themeName(ThemeId id) const;
|
||||
bool renameTheme(ThemeId id, const ThemeName &newName);
|
||||
const std::vector<ThemeId> allThemeIds() const;
|
||||
|
||||
void forAllGroups(std::function<void(GroupType, DSThemeGroup *)> 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;
|
||||
|
@@ -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,
|
||||
|
Reference in New Issue
Block a user