diff --git a/src/plugins/qmldesigner/CMakeLists.txt b/src/plugins/qmldesigner/CMakeLists.txt index 0d7c39784c9..d2b091d1e4c 100644 --- a/src/plugins/qmldesigner/CMakeLists.txt +++ b/src/plugins/qmldesigner/CMakeLists.txt @@ -792,6 +792,7 @@ extend_qtc_plugin(QmlDesigner extend_qtc_plugin(QmlDesigner SOURCES_PREFIX components/collectioneditor SOURCES + collectiondetails.cpp collectiondetails.h collectioneditorconstants.h collectionlistmodel.cpp collectionlistmodel.h collectionsourcemodel.cpp collectionsourcemodel.h diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectiondetails.cpp b/src/plugins/qmldesigner/components/collectioneditor/collectiondetails.cpp new file mode 100644 index 00000000000..4b8a297140f --- /dev/null +++ b/src/plugins/qmldesigner/components/collectioneditor/collectiondetails.cpp @@ -0,0 +1,199 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "collectiondetails.h" + +#include +#include + +namespace QmlDesigner { + +class CollectionDetails::Private +{ + using SourceFormat = CollectionEditor::SourceFormat; + +public: + QStringList headers; + QList elements; + SourceFormat sourceFormat = SourceFormat::Unknown; + CollectionReference reference; + bool isChanged = false; + + bool isValidColumnId(int column) const { return column > -1 && column < headers.size(); } + + bool isValidRowId(int row) const { return row > -1 && row < elements.size(); } +}; + +CollectionDetails::CollectionDetails() + : d(new Private()) +{} + +CollectionDetails::CollectionDetails(const CollectionReference &reference) + : CollectionDetails() +{ + d->reference = reference; +} + +CollectionDetails::CollectionDetails(const CollectionDetails &other) = default; + +CollectionDetails::~CollectionDetails() = default; + +void CollectionDetails::resetDetails(const QStringList &headers, + const QList &elements, + CollectionEditor::SourceFormat format) +{ + if (!isValid()) + return; + + d->headers = headers; + d->elements = elements; + d->sourceFormat = format; + + markSaved(); +} + +void CollectionDetails::insertHeader(const QString &header, int place, const QVariant &defaultValue) +{ + if (!isValid()) + return; + + if (d->headers.contains(header)) + return; + + if (d->isValidColumnId(place)) + d->headers.insert(place, header); + else + d->headers.append(header); + + QJsonValue defaultJsonValue = QJsonValue::fromVariant(defaultValue); + for (QJsonObject &element : d->elements) + element.insert(header, defaultJsonValue); + + markChanged(); +} + +void CollectionDetails::removeHeader(int place) +{ + if (!isValid()) + return; + + if (!d->isValidColumnId(place)) + return; + + const QString header = d->headers.takeAt(place); + + for (QJsonObject &element : d->elements) + element.remove(header); + + markChanged(); +} + +void CollectionDetails::insertElementAt(std::optional object, int row) +{ + if (!isValid()) + return; + + auto insertJson = [this, row](const QJsonObject &jsonObject) { + if (d->isValidRowId(row)) + d->elements.insert(row, jsonObject); + else + d->elements.append(jsonObject); + }; + + if (object.has_value()) { + insertJson(object.value()); + } else { + QJsonObject defaultObject; + for (const QString &header : std::as_const(d->headers)) + defaultObject.insert(header, {}); + insertJson(defaultObject); + } + + markChanged(); +} + +CollectionReference CollectionDetails::reference() const +{ + return d->reference; +} + +CollectionEditor::SourceFormat CollectionDetails::sourceFormat() const +{ + return d->sourceFormat; +} + +QVariant CollectionDetails::data(int row, int column) const +{ + if (!isValid()) + return {}; + + if (!d->isValidRowId(row)) + return {}; + + if (!d->isValidColumnId(column)) + return {}; + + const QString &propertyName = d->headers.at(column); + const QJsonObject &elementNode = d->elements.at(row); + + if (elementNode.contains(propertyName)) + return elementNode.value(propertyName).toVariant(); + + return {}; +} + +QString CollectionDetails::headerAt(int column) const +{ + if (!d->isValidColumnId(column)) + return {}; + + return d->headers.at(column); +} + +bool CollectionDetails::isValid() const +{ + return d->reference.node.isValid() && d->reference.name.size(); +} + +bool CollectionDetails::isChanged() const +{ + return d->isChanged; +} + +int CollectionDetails::columns() const +{ + return d->headers.size(); +} + +int CollectionDetails::rows() const +{ + return d->elements.size(); +} + +bool CollectionDetails::markSaved() +{ + if (d->isChanged) { + d->isChanged = false; + return true; + } + return false; +} + +void CollectionDetails::swap(CollectionDetails &other) +{ + d.swap(other.d); +} + +CollectionDetails &CollectionDetails::operator=(const CollectionDetails &other) +{ + CollectionDetails value(other); + swap(value); + return *this; +} + +void CollectionDetails::markChanged() +{ + d->isChanged = true; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectiondetails.h b/src/plugins/qmldesigner/components/collectioneditor/collectiondetails.h new file mode 100644 index 00000000000..c3ec59f5d8f --- /dev/null +++ b/src/plugins/qmldesigner/components/collectioneditor/collectiondetails.h @@ -0,0 +1,76 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include "collectioneditorconstants.h" +#include "modelnode.h" + +#include + +QT_BEGIN_NAMESPACE +class QJsonObject; +class QVariant; +QT_END_NAMESPACE + +namespace QmlDesigner { + +struct CollectionReference +{ + ModelNode node; + QString name; + + friend auto qHash(const CollectionReference &collection) + { + return qHash(collection.node) ^ ::qHash(collection.name); + } + + bool operator==(const CollectionReference &other) const + { + return node == other.node && name == other.name; + } + + bool operator!=(const CollectionReference &other) const { return !(*this == other); } +}; + +class CollectionDetails +{ +public: + explicit CollectionDetails(); + CollectionDetails(const CollectionReference &reference); + CollectionDetails(const CollectionDetails &other); + ~CollectionDetails(); + + void resetDetails(const QStringList &headers, + const QList &elements, + CollectionEditor::SourceFormat format); + void insertHeader(const QString &header, int place = -1, const QVariant &defaultValue = {}); + void removeHeader(int place); + + void insertElementAt(std::optional object, int row = -1); + + CollectionReference reference() const; + CollectionEditor::SourceFormat sourceFormat() const; + QVariant data(int row, int column) const; + QString headerAt(int column) const; + + bool isValid() const; + bool isChanged() const; + + int columns() const; + int rows() const; + + bool markSaved(); + + void swap(CollectionDetails &other); + CollectionDetails &operator=(const CollectionDetails &other); + +private: + void markChanged(); + + // The private data is supposed to be shared between the copies + class Private; + QSharedPointer d; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectioneditorconstants.h b/src/plugins/qmldesigner/components/collectioneditor/collectioneditorconstants.h index d75fe221e91..f5e18a93ec3 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/collectioneditorconstants.h +++ b/src/plugins/qmldesigner/components/collectioneditor/collectioneditorconstants.h @@ -5,6 +5,8 @@ namespace QmlDesigner::CollectionEditor { +enum class SourceFormat { Unknown, Json, Csv }; + inline constexpr char SOURCEFILE_PROPERTY[] = "sourceFile"; inline constexpr char COLLECTIONMODEL_IMPORT[] = "QtQuick.Studio.Models"; diff --git a/src/plugins/qmldesigner/components/collectioneditor/singlecollectionmodel.cpp b/src/plugins/qmldesigner/components/collectioneditor/singlecollectionmodel.cpp index 62a95474a55..a86b42cd169 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/singlecollectionmodel.cpp +++ b/src/plugins/qmldesigner/components/collectioneditor/singlecollectionmodel.cpp @@ -11,6 +11,7 @@ #include #include +#include #include namespace { @@ -39,26 +40,19 @@ SingleCollectionModel::SingleCollectionModel(QObject *parent) int SingleCollectionModel::rowCount([[maybe_unused]] const QModelIndex &parent) const { - return m_elements.count(); + return m_currentCollection.rows(); } int SingleCollectionModel::columnCount([[maybe_unused]] const QModelIndex &parent) const { - return m_headers.count(); + return m_currentCollection.columns(); } QVariant SingleCollectionModel::data(const QModelIndex &index, int) const { if (!index.isValid()) return {}; - - const QString &propertyName = m_headers.at(index.column()); - const QJsonObject &elementNode = m_elements.at(index.row()); - - if (elementNode.contains(propertyName)) - return elementNode.value(propertyName).toVariant(); - - return {}; + return m_currentCollection.data(index.row(), index.column()); } bool SingleCollectionModel::setData(const QModelIndex &, const QVariant &, int) @@ -79,7 +73,7 @@ QVariant SingleCollectionModel::headerData(int section, [[maybe_unused]] int role) const { if (orientation == Qt::Horizontal) - return m_headers.at(section); + return m_currentCollection.headerAt(section); return {}; } @@ -88,16 +82,62 @@ void SingleCollectionModel::loadCollection(const ModelNode &sourceNode, const QS { QString fileName = sourceNode.variantProperty(CollectionEditor::SOURCEFILE_PROPERTY).value().toString(); - if (sourceNode.type() == CollectionEditor::JSONCOLLECTIONMODEL_TYPENAME) - loadJsonCollection(fileName, collection); - else if (sourceNode.type() == CollectionEditor::CSVCOLLECTIONMODEL_TYPENAME) - loadCsvCollection(fileName, collection); + CollectionReference newReference{sourceNode, collection}; + bool alreadyOpen = m_openedCollections.contains(newReference); + + if (alreadyOpen) { + if (m_currentCollection.reference() != newReference) { + beginResetModel(); + switchToCollection(newReference); + endResetModel(); + } + } else { + switchToCollection(newReference); + if (sourceNode.type() == CollectionEditor::JSONCOLLECTIONMODEL_TYPENAME) + loadJsonCollection(fileName, collection); + else if (sourceNode.type() == CollectionEditor::CSVCOLLECTIONMODEL_TYPENAME) + loadCsvCollection(fileName, collection); + } +} + +void SingleCollectionModel::switchToCollection(const CollectionReference &collection) +{ + if (m_currentCollection.reference() == collection) + return; + + closeCurrentCollectionIfSaved(); + + if (!m_openedCollections.contains(collection)) + m_openedCollections.insert(collection, CollectionDetails(collection)); + + m_currentCollection = m_openedCollections.value(collection); + + setCollectionName(collection.name); +} + +void SingleCollectionModel::closeCollectionIfSaved(const CollectionReference &collection) +{ + if (!m_openedCollections.contains(collection)) + return; + + const CollectionDetails &collectionDetails = m_openedCollections.value(collection); + + if (!collectionDetails.isChanged()) + m_openedCollections.remove(collection); + + m_currentCollection = CollectionDetails{}; +} + +void SingleCollectionModel::closeCurrentCollectionIfSaved() +{ + if (m_currentCollection.isValid()) + closeCollectionIfSaved(m_currentCollection.reference()); } void SingleCollectionModel::loadJsonCollection(const QString &source, const QString &collection) { - beginResetModel(); - setCollectionName(collection); + using CollectionEditor::SourceFormat; + QFile sourceFile(source); QJsonArray collectionNodes; bool jsonFileIsOk = false; @@ -119,62 +159,64 @@ void SingleCollectionModel::loadJsonCollection(const QString &source, const QStr } } - setCollectionSourceFormat(jsonFileIsOk ? SourceFormat::Json : SourceFormat::Unknown); - if (collectionNodes.isEmpty()) { - m_headers.clear(); - m_elements.clear(); + closeCurrentCollectionIfSaved(); endResetModel(); return; - } + }; - m_headers = getJsonHeaders(collectionNodes); - - m_elements.clear(); + QList elements; for (const QJsonValue &value : std::as_const(collectionNodes)) { if (value.isObject()) { QJsonObject object = value.toObject(); - m_elements.append(object); + elements.append(object); } } + SourceFormat sourceFormat = jsonFileIsOk ? SourceFormat::Json : SourceFormat::Unknown; + + beginResetModel(); + m_currentCollection.resetDetails(getJsonHeaders(collectionNodes), elements, sourceFormat); endResetModel(); } -void SingleCollectionModel::loadCsvCollection(const QString &source, const QString &collectionName) +void SingleCollectionModel::loadCsvCollection(const QString &source, + [[maybe_unused]] const QString &collectionName) { - beginResetModel(); + using CollectionEditor::SourceFormat; - setCollectionName(collectionName); QFile sourceFile(source); - m_headers.clear(); - m_elements.clear(); + QStringList headers; + QList elements; bool csvFileIsOk = false; if (sourceFile.open(QFile::ReadOnly)) { QTextStream stream(&sourceFile); if (!stream.atEnd()) - m_headers = stream.readLine().split(','); + headers = stream.readLine().split(','); - if (!m_headers.isEmpty()) { + if (!headers.isEmpty()) { while (!stream.atEnd()) { const QStringList recordDataList = stream.readLine().split(','); int column = -1; QJsonObject recordData; for (const QString &cellData : recordDataList) { - if (++column == m_headers.size()) + if (++column == headers.size()) break; - recordData.insert(m_headers.at(column), cellData); + recordData.insert(headers.at(column), cellData); } if (recordData.count()) - m_elements.append(recordData); + elements.append(recordData); } csvFileIsOk = true; } } - setCollectionSourceFormat(csvFileIsOk ? SourceFormat::Csv : SourceFormat::Unknown); + SourceFormat sourceFormat = csvFileIsOk ? SourceFormat::Csv : SourceFormat::Unknown; + + beginResetModel(); + m_currentCollection.resetDetails(headers, elements, sourceFormat); endResetModel(); } @@ -186,8 +228,4 @@ void SingleCollectionModel::setCollectionName(const QString &newCollectionName) } } -void SingleCollectionModel::setCollectionSourceFormat(SourceFormat sourceFormat) -{ - m_sourceFormat = sourceFormat; -} } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/collectioneditor/singlecollectionmodel.h b/src/plugins/qmldesigner/components/collectioneditor/singlecollectionmodel.h index 471e43b9674..a545f4b0e78 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/singlecollectionmodel.h +++ b/src/plugins/qmldesigner/components/collectioneditor/singlecollectionmodel.h @@ -3,8 +3,10 @@ #pragma once +#include "collectiondetails.h" + #include -#include +#include namespace QmlDesigner { @@ -17,7 +19,6 @@ class SingleCollectionModel : public QAbstractTableModel Q_PROPERTY(QString collectionName MEMBER m_collectionName NOTIFY collectionNameChanged) public: - enum class SourceFormat { Unknown, Json, Csv }; explicit SingleCollectionModel(QObject *parent = nullptr); int rowCount(const QModelIndex &parent) const override; @@ -35,15 +36,17 @@ signals: void collectionNameChanged(const QString &collectionName); private: + void switchToCollection(const CollectionReference &collection); + void closeCollectionIfSaved(const CollectionReference &collection); + void closeCurrentCollectionIfSaved(); void setCollectionName(const QString &newCollectionName); - void setCollectionSourceFormat(SourceFormat sourceFormat); void loadJsonCollection(const QString &source, const QString &collection); void loadCsvCollection(const QString &source, const QString &collectionName); - QStringList m_headers; - QList m_elements; + QHash m_openedCollections; + CollectionDetails m_currentCollection; + QString m_collectionName; - SourceFormat m_sourceFormat = SourceFormat::Unknown; }; } // namespace QmlDesigner