QmlDesigner: Add a new collection to the collection editor

Task-number: QDS-11059
Change-Id: Iad622098ac7a95cbaf543d88f714e79cd5b3c153
Reviewed-by: Miikka Heikkinen <miikka.heikkinen@qt.io>
Reviewed-by: Qt CI Patch Build Bot <ci_patchbuild_bot@qt.io>
Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io>
This commit is contained in:
Ali Kianian
2023-10-27 15:21:50 +03:00
parent 30941d228e
commit afef8afc39
10 changed files with 475 additions and 36 deletions

View File

@@ -41,6 +41,7 @@ Item {
id: newCollection
backendValue: root.rootView
sourceModel: root.model
anchors.centerIn: parent
}

View File

@@ -3,23 +3,37 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuickDesignerTheme 1.0
import Qt.labs.platform as PlatformWidgets
import HelperWidgets 2.0 as HelperWidgets
import StudioControls 1.0 as StudioControls
import StudioTheme as StudioTheme
import CollectionEditor 1.0
StudioControls.Dialog {
id: root
enum SourceType { NewJson, NewCsv, ExistingCollection, NewCollectionToJson }
required property var backendValue
required property var sourceModel
readonly property alias collectionType: typeMode.collectionType
readonly property bool isValid: collectionName.isValid
&& jsonCollections.isValid
&& newCollectionPath.isValid
title: qsTr("Add a new Collection")
anchors.centerIn: parent
closePolicy: Popup.CloseOnEscape
modal: true
required property var backendValue
onOpened: {
collectionName.text = "Collection"
collectionName.text = qsTr("Collection")
updateType()
updateJsonSourceIndex()
updateCollectionExists()
}
onRejected: {
@@ -27,24 +41,197 @@ StudioControls.Dialog {
}
onAccepted: {
if (collectionName.text !== "")
root.backendValue.addCollection(collectionName.text)
if (root.isValid) {
root.backendValue.addCollection(collectionName.text,
root.collectionType,
newCollectionPath.text,
jsonCollections.currentValue)
}
}
contentItem: Column {
function updateType() {
newCollectionPath.text = ""
if (typeMode.currentValue === NewCollectionDialog.SourceType.NewJson) {
newCollectionFileDialog.nameFilters = ["Json Files (*.json)"]
newCollectionFileDialog.fileMode = PlatformWidgets.FileDialog.SaveFile
newCollectionPath.enabled = true
jsonCollections.enabled = false
typeMode.collectionType = "json"
} else if (typeMode.currentValue === NewCollectionDialog.SourceType.NewCsv) {
newCollectionFileDialog.nameFilters = ["Comma-Separated Values (*.csv)"]
newCollectionFileDialog.fileMode = PlatformWidgets.FileDialog.SaveFile
newCollectionPath.enabled = true
jsonCollections.enabled = false
typeMode.collectionType = "csv"
} else if (typeMode.currentValue === NewCollectionDialog.SourceType.ExistingCollection) {
newCollectionFileDialog.nameFilters = ["All Collection Files (*.json *.csv)",
"Json Files (*.json)",
"Comma-Separated Values (*.csv)"]
newCollectionFileDialog.fileMode = PlatformWidgets.FileDialog.OpenFile
newCollectionPath.enabled = true
jsonCollections.enabled = false
typeMode.collectionType = "existing"
} else if (typeMode.currentValue === NewCollectionDialog.SourceType.NewCollectionToJson) {
newCollectionFileDialog.nameFilters = [""]
newCollectionPath.enabled = false
jsonCollections.enabled = true
typeMode.collectionType = "json"
}
}
function updateJsonSourceIndex() {
if (!jsonCollections.enabled) {
jsonCollections.currentIndex = -1
return
}
if (jsonCollections.currentIndex === -1 && jsonCollections.model.rowCount())
jsonCollections.currentIndex = 0
}
function updateCollectionExists() {
collectionName.alreadyExists = sourceModel.collectionExists(jsonCollections.currentValue,
collectionName.text)
}
component NameField: Text {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
horizontalAlignment: Qt.AlignRight
verticalAlignment: Qt.AlignCenter
color: StudioTheme.Values.themeTextColor
font.family: StudioTheme.Constants.font.family
font.pixelSize: StudioTheme.Values.baseIconFontSize
}
component ErrorField: Text {
Layout.columnSpan: 2
color: StudioTheme.Values.themeError
text: qsTr("Collection name can not be empty")
font.family: StudioTheme.Constants.font.family
font.pixelSize: StudioTheme.Values.baseIconFontSize
}
contentItem: ColumnLayout {
spacing: 10
Row {
spacing: 10
Text {
text: qsTr("Collection name: ")
anchors.verticalCenter: parent.verticalCenter
color: StudioTheme.Values.themeTextColor
GridLayout {
columns: 2
rowSpacing: 10
NameField {
text: qsTr("Type")
}
StudioControls.ComboBox {
id: typeMode
property string collectionType
Layout.minimumWidth: 300
Layout.fillWidth: true
model: ListModel {
ListElement { text: qsTr("New Json collection"); value: NewCollectionDialog.SourceType.NewJson}
ListElement { text: qsTr("New CSV collection"); value: NewCollectionDialog.SourceType.NewCsv}
ListElement { text: qsTr("Import an existing collection"); value: NewCollectionDialog.SourceType.ExistingCollection}
ListElement { text: qsTr("Add collection to an available JSON"); value: NewCollectionDialog.SourceType.NewCollectionToJson}
}
textRole: "text"
valueRole: "value"
actionIndicatorVisible: false
onCurrentValueChanged: root.updateType()
}
NameField {
text: qsTr("File location")
visible: newCollectionPath.enabled
}
RowLayout {
visible: newCollectionPath.enabled
Text {
id: newCollectionPath
readonly property bool isValid: !enabled || text !== ""
Layout.fillWidth: true
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
elide: Text.ElideRight
font.family: StudioTheme.Constants.font.family
font.pixelSize: StudioTheme.Values.baseIconFontSize
color: StudioTheme.Values.themePlaceholderTextColor
}
HelperWidgets.Button {
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
text: qsTr("Select")
onClicked: newCollectionFileDialog.open()
PlatformWidgets.FileDialog {
id: newCollectionFileDialog
title: "Select source file"
fileMode: PlatformWidgets.FileDialog.OpenFile
acceptLabel: fileMode === PlatformWidgets.FileDialog.OpenFile ? qsTr("Open") : qsTr("Add")
onAccepted: newCollectionPath.text = newCollectionFileDialog.currentFile
}
}
}
ErrorField {
visible: !newCollectionPath.isValid
text: qsTr("Select a file to continue")
}
NameField {
text: qsTr("Json Collection")
visible: jsonCollections.enabled
}
StudioControls.ComboBox {
id: jsonCollections
readonly property bool isValid: !enabled || currentIndex !== -1
implicitWidth: 300
textRole: "sourceName"
valueRole: "sourceNode"
visible: enabled
actionIndicatorVisible: false
model: CollectionJsonSourceFilterModel {
sourceModel: root.sourceModel
onRowsInserted: root.updateJsonSourceIndex()
onModelReset: root.updateJsonSourceIndex()
onRowsRemoved: root.updateJsonSourceIndex()
}
onEnabledChanged: root.updateJsonSourceIndex()
onCurrentValueChanged: root.updateCollectionExists()
}
ErrorField {
visible: !jsonCollections.isValid
text: qsTr("Add a json resource to continue")
}
NameField {
text: qsTr("Collection name")
visible: collectionName.enabled
}
StudioControls.TextField {
id: collectionName
anchors.verticalCenter: parent.verticalCenter
readonly property bool isValid: !enabled || (text !== "" && !alreadyExists)
property bool alreadyExists
visible: enabled
actionIndicator.visible: false
translationIndicator.visible: false
validator: HelperWidgets.RegExpValidator {
@@ -54,38 +241,42 @@ StudioControls.Dialog {
Keys.onEnterPressed: btnCreate.onClicked()
Keys.onReturnPressed: btnCreate.onClicked()
Keys.onEscapePressed: root.reject()
onTextChanged: root.updateCollectionExists()
}
ErrorField {
text: qsTr("Collection name can not be empty")
visible: collectionName.enabled && collectionName.text === ""
}
ErrorField {
text: qsTr("Collection name already exists %1").arg(collectionName.text)
visible: collectionName.enabled && collectionName.alreadyExists
}
}
Text {
id: fieldErrorText
color: StudioTheme.Values.themeTextColor
anchors.right: parent.right
text: qsTr("Collection name can not be empty")
visible: collectionName.text === ""
}
Item { // spacer
width: 1
height: 20
Layout.fillHeight: true
Layout.preferredWidth: 1
}
Row {
anchors.right: parent.right
RowLayout {
spacing: 10
Layout.alignment: Qt.AlignRight | Qt.AlignBottom
HelperWidgets.Button {
id: btnCreate
anchors.verticalCenter: parent.verticalCenter
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
text: qsTr("Create")
enabled: collectionName.text !== ""
enabled: root.isValid
onClicked: root.accept()
}
HelperWidgets.Button {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
text: qsTr("Cancel")
anchors.verticalCenter: parent.verticalCenter
onClicked: root.reject()
}
}

View File

@@ -485,7 +485,6 @@ void CollectionDetailsModel::loadJsonCollection(const QString &source, const QSt
}
if (collectionNodes.isEmpty()) {
closeCurrentCollectionIfSaved();
endResetModel();
return;
};

View File

@@ -94,6 +94,11 @@ QString CollectionListModel::sourceAddress() const
return m_sourceNode.variantProperty(CollectionEditor::SOURCEFILE_PROPERTY).value().toString();
}
bool CollectionListModel::contains(const QString &collectionName) const
{
return stringList().contains(collectionName);
}
void CollectionListModel::selectCollectionIndex(int idx, bool selectAtLeastOne)
{
int collectionCount = stringList().size();
@@ -108,6 +113,13 @@ void CollectionListModel::selectCollectionIndex(int idx, bool selectAtLeastOne)
setSelectedIndex(preferredIndex);
}
void CollectionListModel::selectCollectionName(const QString &collectionName)
{
int idx = stringList().indexOf(collectionName);
if (idx > -1)
selectCollectionIndex(idx);
}
QString CollectionListModel::collectionNameAt(int idx) const
{
return index(idx).data(NameRole).toString();

View File

@@ -28,13 +28,16 @@ public:
Q_INVOKABLE int selectedIndex() const;
Q_INVOKABLE ModelNode sourceNode() const;
Q_INVOKABLE QString sourceAddress() const;
Q_INVOKABLE bool contains(const QString &collectionName) const;
void selectCollectionIndex(int idx, bool selectAtLeastOne = false);
void selectCollectionName(const QString &collectionName);
QString collectionNameAt(int idx) const;
signals:
void selectedIndexChanged(int idx);
void isEmptyChanged(bool);
void collectionNameChanged(const QString &oldName, const QString &newName);
private:
void setSelectedIndex(int idx);

View File

@@ -9,8 +9,11 @@
#include "variantproperty.h"
#include <utils/qtcassert.h>
#include <qqml.h>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
@@ -58,11 +61,26 @@ QSharedPointer<QmlDesigner::CollectionListModel> loadCollection(
}
return collectionsList;
}
QString getSourceCollectionType(const QmlDesigner::ModelNode &node)
{
using namespace QmlDesigner;
if (node.type() == CollectionEditor::JSONCOLLECTIONMODEL_TYPENAME)
return "json";
if (node.type() == CollectionEditor::CSVCOLLECTIONMODEL_TYPENAME)
return "csv";
return {};
}
} // namespace
namespace QmlDesigner {
CollectionSourceModel::CollectionSourceModel() {}
CollectionSourceModel::CollectionSourceModel(QObject *parent)
: Super(parent)
{}
int CollectionSourceModel::rowCount(const QModelIndex &) const
{
@@ -80,6 +98,10 @@ QVariant CollectionSourceModel::data(const QModelIndex &index, int role) const
return collectionSource->id();
case NameRole:
return collectionSource->variantProperty("objectName").value();
case NodeRole:
return QVariant::fromValue(*collectionSource);
case CollectionTypeRole:
return getSourceCollectionType(*collectionSource);
case SourceRole:
return collectionSource->variantProperty(CollectionEditor::SOURCEFILE_PROPERTY).value();
case SelectedRole:
@@ -188,6 +210,8 @@ QHash<int, QByteArray> CollectionSourceModel::roleNames() const
roles.insert(Super::roleNames());
roles.insert({{IdRole, "sourceId"},
{NameRole, "sourceName"},
{NodeRole, "sourceNode"},
{CollectionTypeRole, "sourceCollectionType"},
{SelectedRole, "sourceIsSelected"},
{SourceRole, "sourceAddress"},
{CollectionsRole, "collections"}});
@@ -265,6 +289,83 @@ void CollectionSourceModel::selectSource(const ModelNode &node)
selectSourceIndex(nodePlace, true);
}
bool CollectionSourceModel::collectionExists(const ModelNode &node, const QString &collectionName) const
{
int idx = sourceIndex(node);
if (idx < 0)
return false;
auto collections = m_collectionList.at(idx);
if (collections.isNull())
return false;
return collections->contains(collectionName);
}
bool CollectionSourceModel::addCollectionToSource(const ModelNode &node,
const QString &collectionName,
QString *errorString)
{
auto returnError = [errorString](const QString &msg) -> bool {
if (errorString)
*errorString = msg;
return false;
};
int idx = sourceIndex(node);
if (idx < 0)
return returnError(tr("Node is not indexed in the collections model."));
if (node.type() != CollectionEditor::JSONCOLLECTIONMODEL_TYPENAME)
return returnError(tr("Node should be a json collection model."));
if (collectionExists(node, collectionName))
return returnError(tr("Collection does not exist."));
QString sourceFileAddress = node.variantProperty(CollectionEditor::SOURCEFILE_PROPERTY)
.value()
.toString();
QFileInfo sourceFileInfo(sourceFileAddress);
if (!sourceFileInfo.isFile())
return returnError(tr("Selected node should have a valid source file address"));
QFile jsonFile(sourceFileAddress);
if (!jsonFile.open(QFile::ReadWrite))
return returnError(tr("Can't open the file to read.\n") + jsonFile.errorString());
QJsonParseError parseError;
QJsonDocument document = QJsonDocument::fromJson(jsonFile.readAll(), &parseError);
if (parseError.error != QJsonParseError::NoError)
return returnError(tr("Saved json file is messy.\n") + parseError.errorString());
if (document.isObject()) {
QJsonObject sourceObject = document.object();
sourceObject.insert(collectionName, QJsonArray{});
document.setObject(sourceObject);
if (!jsonFile.resize(0))
return returnError(tr("Can't clean the json file."));
QByteArray jsonData = document.toJson();
auto writtenBytes = jsonFile.write(jsonData);
jsonFile.close();
if (writtenBytes != jsonData.size())
return returnError(tr("Can't write to the json file."));
updateCollectionList(index(idx));
auto collections = m_collectionList.at(idx);
if (collections.isNull())
return returnError(tr("No collection is available for the json file."));
collections->selectCollectionName(collectionName);
return true;
} else {
return returnError(tr("Json document type should be an object containing collections."));
}
}
QmlDesigner::ModelNode CollectionSourceModel::sourceNodeAt(int idx)
{
QModelIndex data = index(idx);
@@ -309,6 +410,11 @@ void CollectionSourceModel::updateSelectedSource(bool selectAtLeastOne)
selectSourceIndex(idx, selectAtLeastOne);
}
bool CollectionSourceModel::collectionExists(const QVariant &node, const QString &collectionName) const
{
return collectionExists(node.value<ModelNode>(), collectionName);
}
void CollectionSourceModel::updateNodeName(const ModelNode &node)
{
QModelIndex index = indexOfNode(node);
@@ -412,4 +518,21 @@ QModelIndex CollectionSourceModel::indexOfNode(const ModelNode &node) const
{
return index(m_sourceIndexHash.value(node.internalId(), -1));
}
void CollectionJsonSourceFilterModel::registerDeclarativeType()
{
qmlRegisterType<CollectionJsonSourceFilterModel>("CollectionEditor",
1,
0,
"CollectionJsonSourceFilterModel");
}
bool CollectionJsonSourceFilterModel::filterAcceptsRow(int source_row, const QModelIndex &) const
{
if (!sourceModel())
return false;
QModelIndex sourceItem = sourceModel()->index(source_row, 0, {});
return sourceItem.data(CollectionSourceModel::Roles::CollectionTypeRole).toString() == "json";
}
} // namespace QmlDesigner

View File

@@ -7,9 +7,13 @@
#include <QAbstractListModel>
#include <QHash>
#include <QSortFilterProxyModel>
namespace QmlDesigner {
class CollectionJsonSourceFilterModel;
class CollectionListModel;
class CollectionSourceModel : public QAbstractListModel
{
Q_OBJECT
@@ -18,9 +22,17 @@ class CollectionSourceModel : public QAbstractListModel
Q_PROPERTY(bool isEmpty MEMBER m_isEmpty NOTIFY isEmptyChanged)
public:
enum Roles { IdRole = Qt::UserRole + 1, NameRole, SourceRole, SelectedRole, CollectionsRole };
enum Roles {
IdRole = Qt::UserRole + 1,
NameRole,
NodeRole,
CollectionTypeRole,
SourceRole,
SelectedRole,
CollectionsRole
};
explicit CollectionSourceModel();
explicit CollectionSourceModel(QObject *parent = nullptr);
virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override;
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
@@ -40,12 +52,19 @@ public:
void addSource(const ModelNode &node);
void selectSource(const ModelNode &node);
bool collectionExists(const ModelNode &node, const QString &collectionName) const;
bool addCollectionToSource(const ModelNode &node,
const QString &collectionName,
QString *errorString = nullptr);
ModelNode sourceNodeAt(int idx);
CollectionListModel *selectedCollectionList();
Q_INVOKABLE void selectSourceIndex(int idx, bool selectAtLeastOne = false);
Q_INVOKABLE void deselect();
Q_INVOKABLE void updateSelectedSource(bool selectAtLeastOne = false);
Q_INVOKABLE bool collectionExists(const QVariant &node, const QString &collectionName) const;
void updateNodeName(const ModelNode &node);
void updateNodeSource(const ModelNode &node);
void updateNodeId(const ModelNode &node);
@@ -64,10 +83,10 @@ private:
void setSelectedIndex(int idx);
void updateEmpty();
void updateCollectionList(QModelIndex index);
QModelIndex indexOfNode(const ModelNode &node) const;
using Super = QAbstractListModel;
QModelIndex indexOfNode(const ModelNode &node) const;
ModelNodes m_collectionSources;
QHash<qint32, int> m_sourceIndexHash; // internalId -> index
QList<QSharedPointer<CollectionListModel>> m_collectionList;
@@ -76,4 +95,15 @@ private:
bool m_isEmpty = true;
};
class CollectionJsonSourceFilterModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
static void registerDeclarativeType();
protected:
bool filterAcceptsRow(int source_row, const QModelIndex &) const override;
};
} // namespace QmlDesigner

View File

@@ -156,6 +156,7 @@ void CollectionView::addResource(const QUrl &url, const QString &name, const QSt
void CollectionView::registerDeclarativeType()
{
CollectionDetails::registerDeclarativeType();
CollectionJsonSourceFilterModel::registerDeclarativeType();
}
void CollectionView::refreshModel()

View File

@@ -16,7 +16,9 @@
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QMetaObject>
#include <QQmlEngine>
@@ -25,6 +27,7 @@
#include <QVBoxLayout>
namespace {
QString collectionViewResourcesPath()
{
#ifdef SHARE_QML_PATH
@@ -33,6 +36,22 @@ QString collectionViewResourcesPath()
#endif
return Core::ICore::resourcePath("qmldesigner/collectionEditorQmlSource").toString();
}
static QString urlToLocalPath(const QUrl &url)
{
QString localPath;
if (url.isLocalFile())
localPath = url.toLocalFile();
if (url.scheme() == QLatin1String("qrc")) {
const QString &path = url.path();
localPath = QStringLiteral(":") + path;
}
return localPath;
}
} // namespace
namespace QmlDesigner {
@@ -161,9 +180,65 @@ bool CollectionWidget::isCsvFile(const QString &csvFileAddress) const
return true;
}
bool CollectionWidget::addCollection([[maybe_unused]] const QString &collectionName) const
bool CollectionWidget::addCollection(const QString &collectionName,
const QString &collectionType,
const QString &sourceAddress,
const QVariant &sourceNode)
{
// TODO
const ModelNode node = sourceNode.value<ModelNode>();
bool isNewCollection = !node.isValid();
if (isNewCollection) {
QString sourcePath = ::urlToLocalPath(sourceAddress);
if (collectionType == "json") {
QJsonObject jsonObject;
jsonObject.insert(collectionName, QJsonArray());
QFile sourceFile(sourcePath);
if (!sourceFile.open(QFile::WriteOnly)) {
warn(tr("File error"),
tr("Can not open the file to write.\n") + sourceFile.errorString());
return false;
}
sourceFile.write(QJsonDocument(jsonObject).toJson());
sourceFile.close();
bool loaded = loadJsonFile(sourcePath);
if (!loaded)
sourceFile.remove();
return loaded;
} else if (collectionType == "csv") {
QFile sourceFile(sourcePath);
if (!sourceFile.open(QFile::WriteOnly)) {
warn(tr("File error"),
tr("Can not open the file to write.\n") + sourceFile.errorString());
return false;
}
sourceFile.close();
bool loaded = loadCsvFile(collectionName, sourcePath);
if (!loaded)
sourceFile.remove();
return loaded;
} else if (collectionType == "existing") {
QFileInfo fileInfo(sourcePath);
if (fileInfo.suffix() == "json")
return loadJsonFile(sourcePath);
else if (fileInfo.suffix() == "csv")
return loadCsvFile(collectionName, sourcePath);
}
} else if (collectionType == "json") {
QString errorMsg;
bool added = m_sourceModel->addCollectionToSource(node, collectionName, &errorMsg);
if (!added)
warn(tr("Can not add a collection to the json file"), errorMsg);
return added;
}
return false;
}

View File

@@ -15,6 +15,7 @@ class CollectionDetailsModel;
class CollectionDetailsSortFilterModel;
class CollectionSourceModel;
class CollectionView;
class ModelNode;
class CollectionWidget : public QFrame
{
@@ -33,7 +34,10 @@ public:
Q_INVOKABLE bool loadCsvFile(const QString &collectionName, const QString &csvFileAddress);
Q_INVOKABLE bool isJsonFile(const QString &jsonFileAddress) const;
Q_INVOKABLE bool isCsvFile(const QString &csvFileAddress) const;
Q_INVOKABLE bool addCollection(const QString &collectionName) const;
Q_INVOKABLE bool addCollection(const QString &collectionName,
const QString &collectionType,
const QString &sourceAddress,
const QVariant &sourceNode);
void warn(const QString &title, const QString &body);