QmlDesigner: Keep the unsaved collections open while switching

Task-number: QDS-10813
Change-Id: Ia61260eb6ab23036142b5645a1288baf25f2eaf8
Reviewed-by: Miikka Heikkinen <miikka.heikkinen@qt.io>
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
Reviewed-by: Qt CI Patch Build Bot <ci_patchbuild_bot@qt.io>
This commit is contained in:
Ali Kianian
2023-10-02 17:12:29 +03:00
parent 1c76217a70
commit be84072066
6 changed files with 366 additions and 47 deletions

View File

@@ -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

View File

@@ -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 <QJsonObject>
#include <QVariant>
namespace QmlDesigner {
class CollectionDetails::Private
{
using SourceFormat = CollectionEditor::SourceFormat;
public:
QStringList headers;
QList<QJsonObject> 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<QJsonObject> &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<QJsonObject> 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

View File

@@ -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 <QSharedPointer>
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<QJsonObject> &elements,
CollectionEditor::SourceFormat format);
void insertHeader(const QString &header, int place = -1, const QVariant &defaultValue = {});
void removeHeader(int place);
void insertElementAt(std::optional<QJsonObject> 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<Private> d;
};
} // namespace QmlDesigner

View File

@@ -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";

View File

@@ -11,6 +11,7 @@
#include <QFile>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonParseError>
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<QJsonObject> 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<QJsonObject> 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

View File

@@ -3,8 +3,10 @@
#pragma once
#include "collectiondetails.h"
#include <QAbstractTableModel>
#include <QJsonObject>
#include <QHash>
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<QJsonObject> m_elements;
QHash<CollectionReference, CollectionDetails> m_openedCollections;
CollectionDetails m_currentCollection;
QString m_collectionName;
SourceFormat m_sourceFormat = SourceFormat::Unknown;
};
} // namespace QmlDesigner