diff --git a/share/qtcreator/qmldesigner/collectionEditorQmlSource/CollectionItem.qml b/share/qtcreator/qmldesigner/collectionEditorQmlSource/CollectionItem.qml index 5be2dacb8d7..a92cdf065d8 100644 --- a/share/qtcreator/qmldesigner/collectionEditorQmlSource/CollectionItem.qml +++ b/share/qtcreator/qmldesigner/collectionEditorQmlSource/CollectionItem.qml @@ -84,8 +84,9 @@ Item { Text { id: threeDots - text: "..." - font.pixelSize: StudioTheme.Values.baseFontSize + text: StudioTheme.Constants.more_medium + font.family: StudioTheme.Constants.iconFont.family + font.pixelSize: StudioTheme.Values.baseIconFontSize color: textColor anchors.right: boundingRect.right anchors.verticalCenter: parent.verticalCenter diff --git a/share/qtcreator/qmldesigner/collectionEditorQmlSource/CollectionView.qml b/share/qtcreator/qmldesigner/collectionEditorQmlSource/CollectionView.qml index 30128ecadf4..e6fddf33ee4 100644 --- a/share/qtcreator/qmldesigner/collectionEditorQmlSource/CollectionView.qml +++ b/share/qtcreator/qmldesigner/collectionEditorQmlSource/CollectionView.qml @@ -82,14 +82,14 @@ Item { spacing: 2 HelperWidgets.IconButton { - icon: StudioTheme.Constants.translationImport + icon: StudioTheme.Constants.downloadjson_large tooltip: qsTr("Import Json") onClicked: jsonImporter.open() } HelperWidgets.IconButton { - icon: StudioTheme.Constants.translationImport + icon: StudioTheme.Constants.downloadcsv_large tooltip: qsTr("Import CSV") onClicked: csvImporter.open() @@ -112,13 +112,13 @@ Item { } ListView { - id: collectionListView + id: sourceListView width: parent.width height: contentHeight model: root.model - delegate: CollectionItem { + delegate: ModelSourceItem { onDeleteItem: root.model.removeRow(index) } diff --git a/share/qtcreator/qmldesigner/collectionEditorQmlSource/ModelSourceItem.qml b/share/qtcreator/qmldesigner/collectionEditorQmlSource/ModelSourceItem.qml new file mode 100644 index 00000000000..9496ee394a7 --- /dev/null +++ b/share/qtcreator/qmldesigner/collectionEditorQmlSource/ModelSourceItem.qml @@ -0,0 +1,331 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Controls +import HelperWidgets 2.0 as HelperWidgets +import StudioControls 1.0 as StudioControls +import StudioTheme as StudioTheme + +Item { + id: root + + implicitWidth: 300 + implicitHeight: wholeColumn.height + 6 + + property color textColor + property var collectionModel + + property bool expanded: false + + signal selectItem(int itemIndex) + signal deleteItem() + + Column { + id: wholeColumn + + Item { + id: boundingRect + + anchors.centerIn: root + width: root.width - 24 + height: nameHolder.height + clip: true + + MouseArea { + id: itemMouse + + anchors.fill: parent + acceptedButtons: Qt.LeftButton + propagateComposedEvents: true + hoverEnabled: true + + onClicked: (event) => { + if (!sourceIsSelected) { + sourceIsSelected = true + event.accepted = true + } + } + + onDoubleClicked: (event) => { + if (collectionListView.count > 0) + root.expanded = !root.expanded; + } + } + + Rectangle { + id: innerRect + anchors.fill: parent + } + + Row { + width: parent.width - threeDots.width + leftPadding: 20 + + Text { + id: expandButton + + property StudioTheme.ControlStyle style: StudioTheme.Values.viewBarButtonStyle + + width: expandButton.style.squareControlSize.width + height: nameHolder.height + + text: StudioTheme.Constants.startNode + font.family: StudioTheme.Constants.iconFont.family + font.pixelSize: expandButton.style.baseIconFontSize + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: textColor + + rotation: root.expanded ? 90 : 0 + + Behavior on rotation { + SpringAnimation { spring: 2; damping: 0.2 } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton + Qt.LeftButton + onClicked: (event) => { + root.expanded = !root.expanded + event.accepted = true + } + } + visible: collectionListView.count > 0 + } + + Text { + id: nameHolder + + text: sourceName + font.pixelSize: StudioTheme.Values.baseFontSize + color: textColor + leftPadding: 5 + topPadding: 8 + rightPadding: 8 + bottomPadding: 8 + elide: Text.ElideMiddle + verticalAlignment: Text.AlignVCenter + } + } + + Text { + id: threeDots + + text: StudioTheme.Constants.more_medium + font.family: StudioTheme.Constants.iconFont.family + font.pixelSize: StudioTheme.Values.baseIconFontSize + color: textColor + anchors.right: boundingRect.right + anchors.verticalCenter: parent.verticalCenter + rightPadding: 12 + topPadding: nameHolder.topPadding + bottomPadding: nameHolder.bottomPadding + verticalAlignment: Text.AlignVCenter + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton + Qt.LeftButton + onClicked: (event) => { + collectionMenu.popup() + event.accepted = true + } + } + } + } + + ListView { + id: collectionListView + + width: parent.width + height: root.expanded ? contentHeight : 0 + model: collections + clip: true + + Behavior on height { + NumberAnimation {duration: 500} + } + + delegate: CollectionItem { + width: parent.width + onDeleteItem: root.model.removeRow(index) + } + } + } + + StudioControls.Menu { + id: collectionMenu + + StudioControls.MenuItem { + text: qsTr("Delete") + shortcut: StandardKey.Delete + onTriggered: deleteDialog.open() + } + + StudioControls.MenuItem { + text: qsTr("Rename") + shortcut: StandardKey.Replace + onTriggered: renameDialog.open() + } + } + + StudioControls.Dialog { + id: deleteDialog + + title: qsTr("Deleting source") + + contentItem: Column { + spacing: 2 + + Text { + text: qsTr("Are you sure that you want to delete source \"" + sourceName + "\"?") + color: StudioTheme.Values.themeTextColor + } + + Item { // spacer + width: 1 + height: 20 + } + + Row { + anchors.right: parent.right + spacing: 10 + + HelperWidgets.Button { + id: btnDelete + + text: qsTr("Delete") + onClicked: root.deleteItem(index) + } + + HelperWidgets.Button { + text: qsTr("Cancel") + onClicked: deleteDialog.reject() + } + } + } + } + + StudioControls.Dialog { + id: renameDialog + + title: qsTr("Rename source") + + onAccepted: { + if (newNameField.text !== "") + sourceName = newNameField.text + } + + onOpened: { + newNameField.text = sourceName + } + + contentItem: Column { + spacing: 2 + + Text { + text: qsTr("Previous name: " + sourceName) + color: StudioTheme.Values.themeTextColor + } + + Row { + spacing: 10 + Text { + text: qsTr("New name:") + color: StudioTheme.Values.themeTextColor + } + + StudioControls.TextField { + id: newNameField + + actionIndicator.visible: false + translationIndicator.visible: false + validator: newNameValidator + + Keys.onEnterPressed: renameDialog.accept() + Keys.onReturnPressed: renameDialog.accept() + Keys.onEscapePressed: renameDialog.reject() + + onTextChanged: { + btnRename.enabled = newNameField.text !== "" + } + } + } + + Item { // spacer + width: 1 + height: 20 + } + + Row { + anchors.right: parent.right + spacing: 10 + + HelperWidgets.Button { + id: btnRename + + text: qsTr("Rename") + onClicked: renameDialog.accept() + } + + HelperWidgets.Button { + text: qsTr("Cancel") + onClicked: renameDialog.reject() + } + } + } + } + + HelperWidgets.RegExpValidator { + id: newNameValidator + regExp: /^\w+$/ + } + + states: [ + State { + name: "default" + when: !sourceIsSelected && !itemMouse.containsMouse + + PropertyChanges { + target: innerRect + opacity: 0.4 + color: StudioTheme.Values.themeControlBackground + } + + PropertyChanges { + target: root + textColor: StudioTheme.Values.themeTextColor + } + }, + State { + name: "hovered" + when: !sourceIsSelected && itemMouse.containsMouse + + PropertyChanges { + target: innerRect + opacity: 0.5 + color: StudioTheme.Values.themeControlBackgroundHover + } + + PropertyChanges { + target: root + textColor: StudioTheme.Values.themeTextColor + } + }, + State { + name: "selected" + when: sourceIsSelected + + PropertyChanges { + target: innerRect + opacity: 0.6 + color: StudioTheme.Values.themeControlBackgroundInteraction + } + + PropertyChanges { + target: root + textColor: StudioTheme.Values.themeIconColorSelected + } + } + ] +} diff --git a/src/plugins/qmldesigner/CMakeLists.txt b/src/plugins/qmldesigner/CMakeLists.txt index 0556d95a7ab..78810346650 100644 --- a/src/plugins/qmldesigner/CMakeLists.txt +++ b/src/plugins/qmldesigner/CMakeLists.txt @@ -787,6 +787,8 @@ extend_qtc_plugin(QmlDesigner extend_qtc_plugin(QmlDesigner SOURCES_PREFIX components/collectioneditor SOURCES + collectioneditorconstants.h + collectionlistmodel.cpp collectionlistmodel.h collectionsourcemodel.cpp collectionsourcemodel.h collectionview.cpp collectionview.h collectionwidget.cpp collectionwidget.h diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectioneditorconstants.h b/src/plugins/qmldesigner/components/collectioneditor/collectioneditorconstants.h new file mode 100644 index 00000000000..d75fe221e91 --- /dev/null +++ b/src/plugins/qmldesigner/components/collectioneditor/collectioneditorconstants.h @@ -0,0 +1,14 @@ +// 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 + +namespace QmlDesigner::CollectionEditor { + +inline constexpr char SOURCEFILE_PROPERTY[] = "sourceFile"; + +inline constexpr char COLLECTIONMODEL_IMPORT[] = "QtQuick.Studio.Models"; +inline constexpr char JSONCOLLECTIONMODEL_TYPENAME[] = "QtQuick.Studio.Models.JsonSourceModel"; +inline constexpr char CSVCOLLECTIONMODEL_TYPENAME[] = "QtQuick.Studio.Models.CsvSourceModel"; + +} // namespace QmlDesigner::CollectionEditor diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionlistmodel.cpp b/src/plugins/qmldesigner/components/collectioneditor/collectionlistmodel.cpp new file mode 100644 index 00000000000..4c49d5e4954 --- /dev/null +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionlistmodel.cpp @@ -0,0 +1,147 @@ +// 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 "collectionlistmodel.h" + +#include "collectioneditorconstants.h" +#include "variantproperty.h" + +#include +#include + +namespace { + +template +bool containsItem(const std::initializer_list &container, const ValueType &value) +{ + auto begin = std::cbegin(container); + auto end = std::cend(container); + + auto it = std::find(begin, end, value); + return it != end; +} +} // namespace + +namespace QmlDesigner { + +CollectionListModel::CollectionListModel(const ModelNode &sourceModel) + : QStringListModel() + , m_sourceNode(sourceModel) +{ + connect(this, &CollectionListModel::modelReset, this, &CollectionListModel::updateEmpty); + connect(this, &CollectionListModel::rowsRemoved, this, &CollectionListModel::updateEmpty); + connect(this, &CollectionListModel::rowsInserted, this, &CollectionListModel::updateEmpty); +} + +QHash CollectionListModel::roleNames() const +{ + static QHash roles; + if (roles.isEmpty()) { + roles.insert(Super::roleNames()); + roles.insert({ + {IdRole, "collectionId"}, + {NameRole, "collectionName"}, + {SelectedRole, "collectionIsSelected"}, + }); + } + return roles; +} + +bool CollectionListModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid()) + return false; + + if (containsItem({IdRole, Qt::EditRole, Qt::DisplayRole}, role)) { + return Super::setData(index, value); + } else if (role == SelectedRole) { + if (value.toBool() != index.data(SelectedRole).toBool()) { + setSelectedIndex(value.toBool() ? index.row() : -1); + return true; + } + } + return false; +} + +QVariant CollectionListModel::data(const QModelIndex &index, int role) const +{ + QTC_ASSERT(index.isValid(), return {}); + + switch (role) { + case IdRole: + return index.row(); + case NameRole: + return Super::data(index); + case SelectedRole: + return index.row() == m_selectedIndex; + } + + return Super::data(index, role); +} + +int CollectionListModel::selectedIndex() const +{ + return m_selectedIndex; +} + +ModelNode CollectionListModel::sourceNode() const +{ + return m_sourceNode; +} + +QString CollectionListModel::sourceAddress() const +{ + return m_sourceNode.variantProperty(CollectionEditor::SOURCEFILE_PROPERTY).value().toString(); +} + +void CollectionListModel::selectCollectionIndex(int idx, bool selectAtLeastOne) +{ + int collectionCount = stringList().size(); + int preferredIndex = -1; + if (collectionCount) { + if (selectAtLeastOne) + preferredIndex = std::max(0, std::min(idx, collectionCount - 1)); + else if (idx > -1 && idx < collectionCount) + preferredIndex = idx; + } + + setSelectedIndex(preferredIndex); +} + +QString CollectionListModel::collectionNameAt(int idx) const +{ + return index(idx).data(NameRole).toString(); +} + +void CollectionListModel::setSelectedIndex(int idx) +{ + idx = (idx > -1 && idx < rowCount()) ? idx : -1; + + if (m_selectedIndex != idx) { + QModelIndex previousIndex = index(m_selectedIndex); + QModelIndex newIndex = index(idx); + + m_selectedIndex = idx; + + if (previousIndex.isValid()) + emit dataChanged(previousIndex, previousIndex, {SelectedRole}); + + if (newIndex.isValid()) + emit dataChanged(newIndex, newIndex, {SelectedRole}); + + emit selectedIndexChanged(idx); + } +} + +void CollectionListModel::updateEmpty() +{ + bool isEmptyNow = stringList().isEmpty(); + if (m_isEmpty != isEmptyNow) { + m_isEmpty = isEmptyNow; + emit isEmptyChanged(m_isEmpty); + + if (m_isEmpty) + setSelectedIndex(-1); + } +} +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionlistmodel.h b/src/plugins/qmldesigner/components/collectioneditor/collectionlistmodel.h new file mode 100644 index 00000000000..e30b16a5ff7 --- /dev/null +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionlistmodel.h @@ -0,0 +1,50 @@ +// 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 +#include + +#include "modelnode.h" + +namespace QmlDesigner { + +class CollectionListModel : public QStringListModel +{ + Q_OBJECT + Q_PROPERTY(int selectedIndex MEMBER m_selectedIndex NOTIFY selectedIndexChanged) + Q_PROPERTY(bool isEmpty MEMBER m_isEmpty NOTIFY isEmptyChanged) + +public: + enum Roles { IdRole = Qt::UserRole + 1, NameRole, SourceRole, SelectedRole, CollectionsRole }; + + explicit CollectionListModel(const ModelNode &sourceModel); + virtual QHash roleNames() const override; + + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + Q_INVOKABLE int selectedIndex() const; + Q_INVOKABLE ModelNode sourceNode() const; + Q_INVOKABLE QString sourceAddress() const; + + void selectCollectionIndex(int idx, bool selectAtLeastOne = false); + QString collectionNameAt(int idx) const; + +signals: + void selectedIndexChanged(int idx); + void isEmptyChanged(bool); + +private: + void setSelectedIndex(int idx); + + void updateEmpty(); + + using Super = QStringListModel; + int m_selectedIndex = -1; + bool m_isEmpty = false; + const ModelNode m_sourceNode; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionsourcemodel.cpp b/src/plugins/qmldesigner/components/collectioneditor/collectionsourcemodel.cpp index eae5d645e16..999ebe449ba 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/collectionsourcemodel.cpp +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionsourcemodel.cpp @@ -1,33 +1,91 @@ // 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 "collectionsourcemodel.h" #include "abstractview.h" +#include "collectioneditorconstants.h" +#include "collectionlistmodel.h" #include "variantproperty.h" #include +#include +#include +#include +#include + +namespace { + +QSharedPointer loadCollection( + const QmlDesigner::ModelNode &sourceNode, + QSharedPointer initialCollection = {}) +{ + using namespace QmlDesigner::CollectionEditor; + QString sourceFileAddress = sourceNode.variantProperty(SOURCEFILE_PROPERTY).value().toString(); + + QSharedPointer collectionsList; + auto setupCollectionList = [&sourceNode, &initialCollection, &collectionsList]() { + if (initialCollection.isNull()) + collectionsList.reset(new QmlDesigner::CollectionListModel(sourceNode)); + else if (initialCollection->sourceNode() == sourceNode) + collectionsList = initialCollection; + else + collectionsList.reset(new QmlDesigner::CollectionListModel(sourceNode)); + }; + + if (sourceNode.type() == JSONCOLLECTIONMODEL_TYPENAME) { + QFile sourceFile(sourceFileAddress); + if (!sourceFile.open(QFile::ReadOnly)) + return {}; + + QJsonParseError parseError; + QJsonDocument document = QJsonDocument::fromJson(sourceFile.readAll(), &parseError); + if (parseError.error != QJsonParseError::NoError) + return {}; + + setupCollectionList(); + + if (document.isObject()) { + const QJsonObject sourceObject = document.object(); + collectionsList->setStringList(sourceObject.toVariantMap().keys()); + } + } else if (sourceNode.type() == CSVCOLLECTIONMODEL_TYPENAME) { + QmlDesigner::VariantProperty collectionNameProperty = sourceNode.variantProperty( + "objectName"); + setupCollectionList(); + collectionsList->setStringList({collectionNameProperty.value().toString()}); + } + return collectionsList; +} +} // namespace + namespace QmlDesigner { + CollectionSourceModel::CollectionSourceModel() {} int CollectionSourceModel::rowCount(const QModelIndex &) const { - return m_collections.size(); + return m_collectionSources.size(); } QVariant CollectionSourceModel::data(const QModelIndex &index, int role) const { QTC_ASSERT(index.isValid(), return {}); - const ModelNode *collection = &m_collections.at(index.row()); + const ModelNode *collectionSource = &m_collectionSources.at(index.row()); switch (role) { case IdRole: - return collection->id(); + return collectionSource->id(); case NameRole: - return collection->variantProperty("objectName").value(); + return collectionSource->variantProperty("objectName").value(); + case SourceRole: + return collectionSource->variantProperty(CollectionEditor::SOURCEFILE_PROPERTY).value(); case SelectedRole: return index.row() == m_selectedIndex; + case CollectionsRole: + return QVariant::fromValue(m_collectionList.at(index.row()).data()); } return {}; @@ -38,30 +96,37 @@ bool CollectionSourceModel::setData(const QModelIndex &index, const QVariant &va if (!index.isValid()) return false; - ModelNode collection = m_collections.at(index.row()); + ModelNode collectionSource = m_collectionSources.at(index.row()); switch (role) { case IdRole: { - if (collection.id() == value) + if (collectionSource.id() == value) return false; - bool duplicatedId = Utils::anyOf(std::as_const(m_collections), - [&collection, &value](const ModelNode &otherCollection) { + bool duplicatedId = Utils::anyOf(std::as_const(m_collectionSources), + [&collectionSource, &value](const ModelNode &otherCollection) { return (otherCollection.id() == value - && otherCollection != collection); + && otherCollection != collectionSource); }); if (duplicatedId) return false; - collection.setIdWithRefactoring(value.toString()); + collectionSource.setIdWithRefactoring(value.toString()); } break; case Qt::DisplayRole: case NameRole: { - auto collectionName = collection.variantProperty("objectName"); + auto collectionName = collectionSource.variantProperty("objectName"); if (collectionName.value() == value) return false; collectionName.setValue(value.toString()); } break; + case SourceRole: { + auto sourceAddress = collectionSource.variantProperty(CollectionEditor::SOURCEFILE_PROPERTY); + if (sourceAddress.value() == value) + return false; + + sourceAddress.setValue(value.toString()); + } break; case SelectedRole: { if (value.toBool() != index.data(SelectedRole).toBool()) setSelectedIndex(value.toBool() ? index.row() : -1); @@ -82,7 +147,7 @@ bool CollectionSourceModel::removeRows(int row, int count, [[maybe_unused]] cons if (row >= rowMax || row < 0) return false; - AbstractView *view = m_collections.at(row).view(); + AbstractView *view = m_collectionSources.at(row).view(); if (!view) return false; @@ -95,22 +160,22 @@ bool CollectionSourceModel::removeRows(int row, int count, [[maybe_unused]] cons beginRemoveRows({}, row, rowMax - 1); view->executeInTransaction(Q_FUNC_INFO, [row, count, this]() { - for (ModelNode node : Utils::span(m_collections).subspan(row, count)) { - m_collectionsIndexHash.remove(node.internalId()); + for (ModelNode node : Utils::span(m_collectionSources).subspan(row, count)) { + m_sourceIndexHash.remove(node.internalId()); node.destroy(); } + m_collectionSources.remove(row, count); + m_collectionList.remove(row, count); }); - m_collections.remove(row, count); - int idx = row; - for (const ModelNode &node : Utils::span(m_collections).subspan(row)) - m_collectionsIndexHash.insert(node.internalId(), ++idx); + for (const ModelNode &node : Utils::span(m_collectionSources).subspan(row)) + m_sourceIndexHash.insert(node.internalId(), ++idx); endRemoveRows(); if (selectionUpdateNeeded) - updateSelectedCollection(); + updateSelectedSource(); updateEmpty(); return true; @@ -121,82 +186,115 @@ QHash CollectionSourceModel::roleNames() const static QHash roles; if (roles.isEmpty()) { roles.insert(Super::roleNames()); - roles.insert({ - {IdRole, "collectionId"}, - {NameRole, "collectionName"}, - {SelectedRole, "collectionIsSelected"}, - }); + roles.insert({{IdRole, "sourceId"}, + {NameRole, "sourceName"}, + {SelectedRole, "sourceIsSelected"}, + {SourceRole, "sourceAddress"}, + {CollectionsRole, "collections"}}); } return roles; } -void CollectionSourceModel::setCollections(const ModelNodes &collections) +void CollectionSourceModel::setSources(const ModelNodes &sources) { beginResetModel(); - bool wasEmpty = isEmpty(); - m_collections = collections; - m_collectionsIndexHash.clear(); - int i = 0; - for (const ModelNode &collection : collections) - m_collectionsIndexHash.insert(collection.internalId(), i++); + m_collectionSources = sources; + m_sourceIndexHash.clear(); + m_collectionList.clear(); + int i = -1; + for (const ModelNode &collectionSource : sources) { + m_sourceIndexHash.insert(collectionSource.internalId(), ++i); - if (wasEmpty != isEmpty()) - emit isEmptyChanged(isEmpty()); + auto loadedCollection = loadCollection(collectionSource); + m_collectionList.append(loadedCollection); + connect(loadedCollection.data(), + &CollectionListModel::selectedIndexChanged, + this, + &CollectionSourceModel::onSelectedCollectionChanged, + Qt::UniqueConnection); + } + + updateEmpty(); endResetModel(); - updateSelectedCollection(true); + updateSelectedSource(true); } -void CollectionSourceModel::removeCollection(const ModelNode &node) +void CollectionSourceModel::removeSource(const ModelNode &node) { - int nodePlace = m_collectionsIndexHash.value(node.internalId(), -1); + int nodePlace = m_sourceIndexHash.value(node.internalId(), -1); if (nodePlace < 0) return; removeRow(nodePlace); } -int CollectionSourceModel::collectionIndex(const ModelNode &node) const +int CollectionSourceModel::sourceIndex(const ModelNode &node) const { - return m_collectionsIndexHash.value(node.internalId(), -1); + return m_sourceIndexHash.value(node.internalId(), -1); } -void CollectionSourceModel::selectCollection(const ModelNode &node) +void CollectionSourceModel::addSource(const ModelNode &node) { - int nodePlace = m_collectionsIndexHash.value(node.internalId(), -1); + int newRowId = m_collectionSources.count(); + beginInsertRows({}, newRowId, newRowId); + m_collectionSources.append(node); + m_sourceIndexHash.insert(node.internalId(), newRowId); + + auto loadedCollection = loadCollection(node); + m_collectionList.append(loadedCollection); + + connect(loadedCollection.data(), + &CollectionListModel::selectedIndexChanged, + this, + &CollectionSourceModel::onSelectedCollectionChanged, + Qt::UniqueConnection); + + updateEmpty(); + endInsertRows(); + updateSelectedSource(true); +} + +void CollectionSourceModel::selectSource(const ModelNode &node) +{ + int nodePlace = m_sourceIndexHash.value(node.internalId(), -1); if (nodePlace < 0) return; - selectCollectionIndex(nodePlace, true); + selectSourceIndex(nodePlace, true); } -QmlDesigner::ModelNode CollectionSourceModel::collectionNodeAt(int idx) +QmlDesigner::ModelNode CollectionSourceModel::sourceNodeAt(int idx) { QModelIndex data = index(idx); if (!data.isValid()) return {}; - return m_collections.at(idx); + return m_collectionSources.at(idx); } -bool CollectionSourceModel::isEmpty() const +CollectionListModel *CollectionSourceModel::selectedCollectionList() { - return m_collections.isEmpty(); + QModelIndex idx = index(m_selectedIndex); + if (!idx.isValid()) + return {}; + + return idx.data(CollectionsRole).value(); } -void CollectionSourceModel::selectCollectionIndex(int idx, bool selectAtLeastOne) +void CollectionSourceModel::selectSourceIndex(int idx, bool selectAtLeastOne) { - int collectionCount = m_collections.size(); - int prefferedIndex = -1; + int collectionCount = m_collectionSources.size(); + int preferredIndex = -1; if (collectionCount) { if (selectAtLeastOne) - prefferedIndex = std::max(0, std::min(idx, collectionCount - 1)); + preferredIndex = std::max(0, std::min(idx, collectionCount - 1)); else if (idx > -1 && idx < collectionCount) - prefferedIndex = idx; + preferredIndex = idx; } - setSelectedIndex(prefferedIndex); + setSelectedIndex(preferredIndex); } void CollectionSourceModel::deselect() @@ -204,17 +302,25 @@ void CollectionSourceModel::deselect() setSelectedIndex(-1); } -void CollectionSourceModel::updateSelectedCollection(bool selectAtLeastOne) +void CollectionSourceModel::updateSelectedSource(bool selectAtLeastOne) { int idx = m_selectedIndex; m_selectedIndex = -1; - selectCollectionIndex(idx, selectAtLeastOne); + selectSourceIndex(idx, selectAtLeastOne); } void CollectionSourceModel::updateNodeName(const ModelNode &node) { QModelIndex index = indexOfNode(node); emit dataChanged(index, index, {NameRole, Qt::DisplayRole}); + updateCollectionList(index); +} + +void CollectionSourceModel::updateNodeSource(const ModelNode &node) +{ + QModelIndex index = indexOfNode(node); + emit dataChanged(index, index, {SourceRole}); + updateCollectionList(index); } void CollectionSourceModel::updateNodeId(const ModelNode &node) @@ -223,9 +329,28 @@ void CollectionSourceModel::updateNodeId(const ModelNode &node) emit dataChanged(index, index, {IdRole}); } +QString CollectionSourceModel::selectedSourceAddress() const +{ + return index(m_selectedIndex).data(SourceRole).toString(); +} + +void CollectionSourceModel::onSelectedCollectionChanged(int collectionIndex) +{ + CollectionListModel *collectionList = qobject_cast(sender()); + if (collectionIndex > -1 && collectionList) { + if (_previousSelectedList && _previousSelectedList != collectionList) + _previousSelectedList->selectCollectionIndex(-1); + + emit collectionSelected(collectionList->sourceNode(), + collectionList->collectionNameAt(collectionIndex)); + + _previousSelectedList = collectionList; + } +} + void CollectionSourceModel::setSelectedIndex(int idx) { - idx = (idx > -1 && idx < m_collections.count()) ? idx : -1; + idx = (idx > -1 && idx < m_collectionSources.count()) ? idx : -1; if (m_selectedIndex != idx) { QModelIndex previousIndex = index(m_selectedIndex); @@ -245,7 +370,7 @@ void CollectionSourceModel::setSelectedIndex(int idx) void CollectionSourceModel::updateEmpty() { - bool isEmptyNow = isEmpty(); + bool isEmptyNow = m_collectionSources.isEmpty(); if (m_isEmpty != isEmptyNow) { m_isEmpty = isEmptyNow; emit isEmptyChanged(m_isEmpty); @@ -255,8 +380,22 @@ void CollectionSourceModel::updateEmpty() } } +void CollectionSourceModel::updateCollectionList(QModelIndex index) +{ + if (!index.isValid()) + return; + + ModelNode sourceNode = sourceNodeAt(index.row()); + QSharedPointer currentList = m_collectionList.at(index.row()); + QSharedPointer newList = loadCollection(sourceNode, currentList); + if (currentList != newList) { + m_collectionList.replace(index.row(), newList); + emit this->dataChanged(index, index, {CollectionsRole}); + } +} + QModelIndex CollectionSourceModel::indexOfNode(const ModelNode &node) const { - return index(m_collectionsIndexHash.value(node.internalId(), -1)); + return index(m_sourceIndexHash.value(node.internalId(), -1)); } } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionsourcemodel.h b/src/plugins/qmldesigner/components/collectioneditor/collectionsourcemodel.h index cdde4362835..bd22d833ad9 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/collectionsourcemodel.h +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionsourcemodel.h @@ -1,5 +1,6 @@ // Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + #pragma once #include "modelnode.h" @@ -7,20 +8,17 @@ #include #include -QT_BEGIN_NAMESPACE -class QJsonArray; -QT_END_NAMESPACE - namespace QmlDesigner { - +class CollectionListModel; class CollectionSourceModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(int selectedIndex MEMBER m_selectedIndex NOTIFY selectedIndexChanged) + Q_PROPERTY(bool isEmpty MEMBER m_isEmpty NOTIFY isEmptyChanged) public: - enum Roles { IdRole = Qt::UserRole + 1, NameRole, SelectedRole }; + enum Roles { IdRole = Qt::UserRole + 1, NameRole, SourceRole, SelectedRole, CollectionsRole }; explicit CollectionSourceModel(); @@ -36,35 +34,44 @@ public: virtual QHash roleNames() const override; - void setCollections(const ModelNodes &collections); - void removeCollection(const ModelNode &node); - int collectionIndex(const ModelNode &node) const; - void selectCollection(const ModelNode &node); + void setSources(const ModelNodes &sources); + void removeSource(const ModelNode &node); + int sourceIndex(const ModelNode &node) const; + void addSource(const ModelNode &node); + void selectSource(const ModelNode &node); - ModelNode collectionNodeAt(int idx); + ModelNode sourceNodeAt(int idx); + CollectionListModel *selectedCollectionList(); - Q_INVOKABLE bool isEmpty() const; - Q_INVOKABLE void selectCollectionIndex(int idx, bool selectAtLeastOne = false); + Q_INVOKABLE void selectSourceIndex(int idx, bool selectAtLeastOne = false); Q_INVOKABLE void deselect(); - Q_INVOKABLE void updateSelectedCollection(bool selectAtLeastOne = false); + Q_INVOKABLE void updateSelectedSource(bool selectAtLeastOne = false); void updateNodeName(const ModelNode &node); + void updateNodeSource(const ModelNode &node); void updateNodeId(const ModelNode &node); + QString selectedSourceAddress() const; + signals: void selectedIndexChanged(int idx); - void renameCollectionTriggered(const QmlDesigner::ModelNode &collection, const QString &newName); - void addNewCollectionTriggered(); + void collectionSelected(const ModelNode &sourceNode, const QString &collectionName); void isEmptyChanged(bool); +private slots: + void onSelectedCollectionChanged(int collectionIndex); + private: void setSelectedIndex(int idx); void updateEmpty(); + void updateCollectionList(QModelIndex index); using Super = QAbstractListModel; QModelIndex indexOfNode(const ModelNode &node) const; - ModelNodes m_collections; - QHash m_collectionsIndexHash; // internalId -> index + ModelNodes m_collectionSources; + QHash m_sourceIndexHash; // internalId -> index + QList> m_collectionList; + QPointer _previousSelectedList; int m_selectedIndex = -1; bool m_isEmpty = true; }; diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionview.cpp b/src/plugins/qmldesigner/components/collectioneditor/collectionview.cpp index 2870e42db54..53cf76312d4 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/collectionview.cpp +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionview.cpp @@ -2,12 +2,13 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "collectionview.h" + +#include "collectioneditorconstants.h" #include "collectionsourcemodel.h" #include "collectionwidget.h" #include "designmodecontext.h" -#include "nodelistproperty.h" +#include "nodeabstractproperty.h" #include "nodemetainfo.h" -#include "qmldesignerconstants.h" #include "qmldesignerplugin.h" #include "singlecollectionmodel.h" #include "variantproperty.h" @@ -21,326 +22,21 @@ #include namespace { -using Data = std::variant; -using DataRecord = QMap; - -struct DataHeader +inline bool isStudioCollectionModel(const QmlDesigner::ModelNode &node) { - enum class Type { Unknown, Bool, Numeric, String, DateTime }; - Type type; - QString name; -}; - -using DataHeaderMap = QMap; // Lowercase Name - Header Data - -inline constexpr QStringView BoolDataType{u"Bool"}; -inline constexpr QStringView NumberDataType{u"Number"}; -inline constexpr QStringView StringDataType{u"String"}; -inline constexpr QStringView DateTimeDataType{u"Date/Time"}; - -QString removeSpaces(QString string) -{ - string.replace(" ", "_"); - string.replace("-", "_"); - return string; -} - -DataHeader getDataType(const QString &type, const QString &name) -{ - static const QMap typeMap = { - {BoolDataType.toString().toLower(), DataHeader::Type::Bool}, - {NumberDataType.toString().toLower(), DataHeader::Type::Numeric}, - {StringDataType.toString().toLower(), DataHeader::Type::String}, - {DateTimeDataType.toString().toLower(), DataHeader::Type::DateTime}}; - if (name.isEmpty()) - return {}; - - if (type.isEmpty()) - return {DataHeader::Type::String, removeSpaces(name)}; - - return {typeMap.value(type.toLower(), DataHeader::Type::Unknown), removeSpaces(name)}; -} - -struct JsonDocumentError : public std::exception -{ - enum Error { - InvalidDocumentType, - InvalidCollectionName, - InvalidCollectionId, - InvalidCollectionObject, - InvalidArrayPosition, - InvalidLiteralType, - InvalidCollectionHeader, - IsNotJsonArray, - CollectionHeaderNotFound - }; - - const Error error; - - JsonDocumentError(Error error) - : error(error) - {} - - const char *what() const noexcept override - { - switch (error) { - case InvalidDocumentType: - return "Current JSON document contains errors."; - case InvalidCollectionName: - return "Invalid collection name."; - case InvalidCollectionId: - return "Invalid collection Id."; - case InvalidCollectionObject: - return "A collection should be a json object."; - case InvalidArrayPosition: - return "Arrays are not supported inside the collection."; - case InvalidLiteralType: - return "Invalid literal type for collection items"; - case InvalidCollectionHeader: - return "Invalid Collection Header"; - case IsNotJsonArray: - return "Json file should be an array"; - case CollectionHeaderNotFound: - return "Collection Header not found"; - default: - return "Unknown Json Error"; - } - } -}; - -struct CsvDocumentError : public std::exception -{ - enum Error { - HeaderNotFound, - DataNotFound, - }; - - const Error error; - - CsvDocumentError(Error error) - : error(error) - {} - - const char *what() const noexcept override - { - switch (error) { - case HeaderNotFound: - return "CSV Header not found"; - case DataNotFound: - return "CSV data not found"; - default: - return "Unknown CSV Error"; - } - } -}; - -Data getLiteralDataValue(const QVariant &value, const DataHeader &header, bool *typeWarningCheck = nullptr) -{ - if (header.type == DataHeader::Type::Bool) - return value.toBool(); - - if (header.type == DataHeader::Type::Numeric) - return value.toDouble(); - - if (header.type == DataHeader::Type::String) - return value.toString(); - - if (header.type == DataHeader::Type::DateTime) { - QDateTime dateTimeStr = QDateTime::fromString(value.toString()); - if (dateTimeStr.isValid()) - return dateTimeStr; - } - - if (typeWarningCheck) - *typeWarningCheck = true; - - return value.toString(); -} - -void loadJsonHeaders(QList &collectionHeaders, - DataHeaderMap &headerDataMap, - const QJsonObject &collectionJsonObject) -{ - const QJsonArray collectionHeader = collectionJsonObject.value("headers").toArray(); - for (const QJsonValue &headerValue : collectionHeader) { - const QJsonObject headerJsonObject = headerValue.toObject(); - DataHeader dataHeader = getDataType(headerJsonObject.value("type").toString(), - headerJsonObject.value("name").toString()); - - if (dataHeader.type == DataHeader::Type::Unknown) - throw JsonDocumentError{JsonDocumentError::InvalidCollectionHeader}; - - collectionHeaders.append(dataHeader); - headerDataMap.insert(dataHeader.name.toLower(), dataHeader); - } - - if (collectionHeaders.isEmpty()) - throw JsonDocumentError{JsonDocumentError::CollectionHeaderNotFound}; -} - -void loadJsonRecords(QList &collectionItems, - DataHeaderMap &headerDataMap, - const QJsonObject &collectionJsonObject) -{ - auto addItemFromValue = [&headerDataMap, &collectionItems](const QJsonValue &jsonValue) { - const QVariantMap dataMap = jsonValue.toObject().toVariantMap(); - DataRecord recordData; - for (const auto &dataPair : dataMap.asKeyValueRange()) { - const DataHeader correspondingHeader = headerDataMap.value(removeSpaces( - dataPair.first.toLower()), - {}); - - const QString &fieldName = correspondingHeader.name; - if (fieldName.size()) - recordData.insert(fieldName, - getLiteralDataValue(dataPair.second, correspondingHeader)); - } - if (!recordData.isEmpty()) - collectionItems.append(recordData); - }; - - const QJsonValue jsonDataValue = collectionJsonObject.value("data"); - if (jsonDataValue.isObject()) { - addItemFromValue(jsonDataValue); - } else if (jsonDataValue.isArray()) { - const QJsonArray jsonDataArray = jsonDataValue.toArray(); - for (const QJsonValue &jsonItem : jsonDataArray) { - if (jsonItem.isObject()) - addItemFromValue(jsonItem); - } - } -} - -inline bool isCollectionLib(const QmlDesigner::ModelNode &node) -{ - return node.parentProperty().parentModelNode().isRootNode() - && node.id() == QmlDesigner::Constants::COLLECTION_LIB_ID; -} - -inline bool isListModel(const QmlDesigner::ModelNode &node) -{ - return node.metaInfo().isQtQuickListModel(); -} - -inline bool isListElement(const QmlDesigner::ModelNode &node) -{ - return node.metaInfo().isQtQuickListElement(); -} - -inline bool isCollection(const QmlDesigner::ModelNode &node) -{ - return isCollectionLib(node.parentProperty().parentModelNode()) && isListModel(node); -} - -inline bool isCollectionElement(const QmlDesigner::ModelNode &node) -{ - return isListElement(node) && isCollection(node.parentProperty().parentModelNode()); + using namespace QmlDesigner::CollectionEditor; + return node.metaInfo().typeName() == JSONCOLLECTIONMODEL_TYPENAME + || node.metaInfo().typeName() == CSVCOLLECTIONMODEL_TYPENAME; } } // namespace namespace QmlDesigner { -struct Collection -{ - QString name; - QString id; - QList headers; - QList items; -}; - CollectionView::CollectionView(ExternalDependenciesInterface &externalDependencies) : AbstractView(externalDependencies) {} -bool CollectionView::loadJson(const QByteArray &data) -{ - try { - QJsonParseError parseError; - QJsonDocument document = QJsonDocument::fromJson(data, &parseError); - if (parseError.error != QJsonParseError::NoError) - throw JsonDocumentError{JsonDocumentError::InvalidDocumentType}; - - QList collections; - if (document.isArray()) { - const QJsonArray collectionsJsonArray = document.array(); - - for (const QJsonValue &collectionJson : collectionsJsonArray) { - Collection collection; - if (!collectionJson.isObject()) - throw JsonDocumentError{JsonDocumentError::InvalidCollectionObject}; - - QJsonObject collectionJsonObject = collectionJson.toObject(); - - const QString &collectionName = collectionJsonObject.value(u"name").toString(); - if (!collectionName.size()) - throw JsonDocumentError{JsonDocumentError::InvalidCollectionName}; - - const QString &collectionId = collectionJsonObject.value(u"id").toString(); - if (!collectionId.size()) - throw JsonDocumentError{JsonDocumentError::InvalidCollectionId}; - - DataHeaderMap headerDataMap; - - loadJsonHeaders(collection.headers, headerDataMap, collectionJsonObject); - loadJsonRecords(collection.items, headerDataMap, collectionJsonObject); - - if (collection.items.count()) - collections.append(collection); - } - } else { - throw JsonDocumentError{JsonDocumentError::InvalidDocumentType}; - } - - addLoadedModel(collections); - } catch (const std::exception &error) { - m_widget->warn("Json Import Problem", QString::fromLatin1(error.what())); - return false; - } - - return true; -} - -bool CollectionView::loadCsv(const QString &collectionName, const QByteArray &data) -{ - QTextStream stream(data); - Collection collection; - collection.name = collectionName; - - try { - if (!stream.atEnd()) { - const QStringList recordData = stream.readLine().split(','); - for (const QString &name : recordData) - collection.headers.append(getDataType({}, name)); - } - if (collection.headers.isEmpty()) - throw CsvDocumentError{CsvDocumentError::HeaderNotFound}; - - while (!stream.atEnd()) { - const QStringList recordDataList = stream.readLine().split(','); - DataRecord recordData; - int column = -1; - for (const QString &cellData : recordDataList) { - if (++column == collection.headers.size()) - break; - recordData.insert(collection.headers.at(column).name, cellData); - } - if (recordData.count()) - collection.items.append(recordData); - } - - if (collection.items.isEmpty()) - throw CsvDocumentError{CsvDocumentError::DataNotFound}; - - addLoadedModel({collection}); - } catch (const std::exception &error) { - m_widget->warn("Json Import Problem", QString::fromLatin1(error.what())); - return false; - } - - return true; -} - bool CollectionView::hasWidget() const { return true; @@ -355,10 +51,12 @@ QmlDesigner::WidgetInfo CollectionView::widgetInfo() Core::ICore::addContextObject(collectionEditorContext); CollectionSourceModel *sourceModel = m_widget->sourceModel().data(); - connect(sourceModel, &CollectionSourceModel::selectedIndexChanged, this, [&](int selectedIndex) { - m_widget->singleCollectionModel()->setCollection( - m_widget->sourceModel()->collectionNodeAt(selectedIndex)); - }); + connect(sourceModel, + &CollectionSourceModel::collectionSelected, + this, + [this](const ModelNode &sourceNode, const QString &collection) { + m_widget->singleCollectionModel()->loadCollection(sourceNode, collection); + }); } return createWidgetInfo(m_widget.data(), @@ -376,47 +74,31 @@ void CollectionView::modelAttached(Model *model) } void CollectionView::nodeReparented(const ModelNode &node, - const NodeAbstractProperty &newPropertyParent, - const NodeAbstractProperty &oldPropertyParent, + [[maybe_unused]] const NodeAbstractProperty &newPropertyParent, + [[maybe_unused]] const NodeAbstractProperty &oldPropertyParent, [[maybe_unused]] PropertyChangeFlags propertyChange) { - if (!isListModel(node)) - return; - - ModelNode newParentNode = newPropertyParent.parentModelNode(); - ModelNode oldParentNode = oldPropertyParent.parentModelNode(); - bool added = isCollectionLib(newParentNode); - bool removed = isCollectionLib(oldParentNode); - - if (!added && !removed) + if (!isStudioCollectionModel(node)) return; refreshModel(); - if (isCollection(node)) - m_widget->sourceModel()->selectCollection(node); + m_widget->sourceModel()->selectSource(node); } void CollectionView::nodeAboutToBeRemoved(const ModelNode &removedNode) { // removing the collections lib node - if (isCollectionLib(removedNode)) { - m_widget->sourceModel()->setCollections({}); - return; - } - - if (isCollection(removedNode)) - m_widget->sourceModel()->removeCollection(removedNode); + if (isStudioCollectionModel(removedNode)) + m_widget->sourceModel()->removeSource(removedNode); } -void CollectionView::nodeRemoved([[maybe_unused]] const ModelNode &removedNode, - const NodeAbstractProperty &parentProperty, +void CollectionView::nodeRemoved(const ModelNode &removedNode, + [[maybe_unused]] const NodeAbstractProperty &parentProperty, [[maybe_unused]] PropertyChangeFlags propertyChange) { - if (parentProperty.parentModelNode().id() != Constants::COLLECTION_LIB_ID) - return; - - m_widget->sourceModel()->updateSelectedCollection(true); + if (isStudioCollectionModel(removedNode)) + m_widget->sourceModel()->updateSelectedSource(true); } void CollectionView::variantPropertiesChanged(const QList &propertyList, @@ -424,9 +106,11 @@ void CollectionView::variantPropertiesChanged(const QList &prop { for (const VariantProperty &property : propertyList) { ModelNode node(property.parentModelNode()); - if (isCollection(node)) { + if (isStudioCollectionModel(node)) { if (property.name() == "objectName") m_widget->sourceModel()->updateNodeName(node); + else if (property.name() == CollectionEditor::SOURCEFILE_PROPERTY) + m_widget->sourceModel()->updateNodeSource(node); else if (property.name() == "id") m_widget->sourceModel()->updateNodeId(node); } @@ -436,50 +120,36 @@ void CollectionView::variantPropertiesChanged(const QList &prop void CollectionView::selectedNodesChanged(const QList &selectedNodeList, [[maybe_unused]] const QList &lastSelectedNodeList) { - QList selectedCollections = Utils::filtered(selectedNodeList, &isCollection); + QList selectedJsonCollections = Utils::filtered(selectedNodeList, + &isStudioCollectionModel); // More than one collections are selected. So ignore them - if (selectedCollections.size() > 1) + if (selectedJsonCollections.size() > 1) return; - if (selectedCollections.size() == 1) { // If exactly one collection is selected - m_widget->sourceModel()->selectCollection(selectedCollections.first()); + if (selectedJsonCollections.size() == 1) { // If exactly one collection is selected + m_widget->sourceModel()->selectSource(selectedJsonCollections.first()); return; } - - // If no collection is selected, check the elements - QList selectedElements = Utils::filtered(selectedNodeList, &isCollectionElement); - if (selectedElements.size()) { - const ModelNode parentElement = selectedElements.first().parentProperty().parentModelNode(); - bool haveSameParent = Utils::allOf(selectedElements, [&parentElement](const ModelNode &element) { - return element.parentProperty().parentModelNode() == parentElement; - }); - if (haveSameParent) - m_widget->sourceModel()->selectCollection(parentElement); - } } -void CollectionView::addNewCollection(const QString &name) +void CollectionView::addResource(const QUrl &url, const QString &name, const QString &type) { - executeInTransaction(__FUNCTION__, [&] { - ensureCollectionLibraryNode(); - ModelNode collectionLib = collectionLibraryNode(); - if (!collectionLib.isValid()) - return; - - NodeMetaInfo listModelMetaInfo = model()->qtQmlModelsListModelMetaInfo(); - ModelNode collectionNode = createModelNode(listModelMetaInfo.typeName(), - listModelMetaInfo.majorVersion(), - listModelMetaInfo.minorVersion()); - QString collectionName = name.isEmpty() ? "Collection" : name; - renameCollection(collectionNode, collectionName); - - QmlDesignerPlugin::emitUsageStatistics(Constants::EVENT_PROPERTY_ADDED); - - auto headersProperty = collectionNode.variantProperty("headers"); - headersProperty.setDynamicTypeNameAndValue("string", {}); - - collectionLib.defaultNodeListProperty().reparentHere(collectionNode); + executeInTransaction(Q_FUNC_INFO, [this, &url, &name, &type]() { + ensureStudioModelImport(); + QString sourceAddress = url.isLocalFile() ? url.toLocalFile() : url.toString(); + const NodeMetaInfo resourceMetaInfo = type.compare("json", Qt::CaseInsensitive) == 0 + ? jsonCollectionMetaInfo() + : csvCollectionMetaInfo(); + ModelNode resourceNode = createModelNode(resourceMetaInfo.typeName(), + resourceMetaInfo.majorVersion(), + resourceMetaInfo.minorVersion()); + VariantProperty sourceProperty = resourceNode.variantProperty( + CollectionEditor::SOURCEFILE_PROPERTY); + VariantProperty nameProperty = resourceNode.variantProperty("objectName"); + sourceProperty.setValue(sourceAddress); + nameProperty.setValue(name); + rootModelNode().defaultNodeAbstractProperty().reparentHere(resourceNode); }); } @@ -488,118 +158,32 @@ void CollectionView::refreshModel() if (!model()) return; - ModelNode collectionLib = modelNodeForId(Constants::COLLECTION_LIB_ID); - ModelNodes collections; - - if (collectionLib.isValid()) { - const QList collectionLibNodes = collectionLib.directSubModelNodes(); - for (const ModelNode &node : collectionLibNodes) { - if (isCollection(node)) - collections.append(node); - } - } - - m_widget->sourceModel()->setCollections(collections); + // Load Json Collections + const ModelNodes jsonSourceNodes = rootModelNode().subModelNodesOfType(jsonCollectionMetaInfo()); + m_widget->sourceModel()->setSources(jsonSourceNodes); } -ModelNode CollectionView::getNewCollectionNode(const Collection &collection) +NodeMetaInfo CollectionView::jsonCollectionMetaInfo() const { - QTC_ASSERT(model(), return {}); - ModelNode collectionNode; - executeInTransaction(__FUNCTION__, [&] { - NodeMetaInfo listModelMetaInfo = model()->qtQmlModelsListModelMetaInfo(); - collectionNode = createModelNode(listModelMetaInfo.typeName(), - listModelMetaInfo.majorVersion(), - listModelMetaInfo.minorVersion()); - QString collectionName = collection.name.isEmpty() ? "Collection" : collection.name; - renameCollection(collectionNode, collectionName); - QStringList headers; - for (const DataHeader &header : collection.headers) - headers.append(header.name); - - QmlDesignerPlugin::emitUsageStatistics(Constants::EVENT_PROPERTY_ADDED); - - auto headersProperty = collectionNode.variantProperty("headers"); - headersProperty.setDynamicTypeNameAndValue("string", headers.join(",")); - - NodeMetaInfo listElementMetaInfo = model()->qtQmlModelsListElementMetaInfo(); - for (const DataRecord &item : collection.items) { - ModelNode elementNode = createModelNode(listElementMetaInfo.typeName(), - listElementMetaInfo.majorVersion(), - listElementMetaInfo.minorVersion()); - for (const auto &headerMapElement : item.asKeyValueRange()) { - auto property = elementNode.variantProperty(headerMapElement.first.toLatin1()); - QVariant value = std::visit([](const auto &data) - -> QVariant { return QVariant::fromValue(data); }, - headerMapElement.second); - property.setValue(value); - } - collectionNode.defaultNodeListProperty().reparentHere(elementNode); - } - }); - return collectionNode; + return model()->metaInfo(CollectionEditor::JSONCOLLECTIONMODEL_TYPENAME); } -void CollectionView::addLoadedModel(const QList &newCollection) +NodeMetaInfo CollectionView::csvCollectionMetaInfo() const +{ + return model()->metaInfo(CollectionEditor::CSVCOLLECTIONMODEL_TYPENAME); +} + +void CollectionView::ensureStudioModelImport() { executeInTransaction(__FUNCTION__, [&] { - ensureCollectionLibraryNode(); - ModelNode collectionLib = collectionLibraryNode(); - if (!collectionLib.isValid()) - return; - - for (const Collection &collection : newCollection) { - ModelNode collectionNode = getNewCollectionNode(collection); - collectionLib.defaultNodeListProperty().reparentHere(collectionNode); + Import import = Import::createLibraryImport(CollectionEditor::COLLECTIONMODEL_IMPORT); + try { + if (!model()->hasImport(import, true, true)) + model()->changeImports({import}, {}); + } catch (const Exception &) { + QTC_ASSERT(false, return); } }); } -void CollectionView::renameCollection(ModelNode &collection, const QString &newName) -{ - QTC_ASSERT(collection.isValid(), return); - - QVariant objName = collection.variantProperty("objectName").value(); - if (objName.isValid() && objName.toString() == newName) - return; - - executeInTransaction(__FUNCTION__, [&] { - collection.setIdWithRefactoring(model()->generateIdFromName(newName, "collection")); - - VariantProperty objNameProp = collection.variantProperty("objectName"); - objNameProp.setValue(newName); - }); -} - -void CollectionView::ensureCollectionLibraryNode() -{ - ModelNode collectionLib = modelNodeForId(Constants::COLLECTION_LIB_ID); - if (collectionLib.isValid() - || (!rootModelNode().metaInfo().isQtQuick3DNode() - && !rootModelNode().metaInfo().isQtQuickItem())) { - return; - } - - executeInTransaction(__FUNCTION__, [&] { - // Create collection library node -#ifdef QDS_USE_PROJECTSTORAGE - TypeName nodeTypeName = rootModelNode().metaInfo().isQtQuick3DNode() ? "Node" : "Item"; - collectionLib = createModelNode(nodeTypeName, -1, -1); -#else - auto nodeType = rootModelNode().metaInfo().isQtQuick3DNode() - ? model()->qtQuick3DNodeMetaInfo() - : model()->qtQuickItemMetaInfo(); - collectionLib = createModelNode(nodeType.typeName(), - nodeType.majorVersion(), - nodeType.minorVersion()); -#endif - collectionLib.setIdWithoutRefactoring(Constants::COLLECTION_LIB_ID); - rootModelNode().defaultNodeListProperty().reparentHere(collectionLib); - }); -} - -ModelNode CollectionView::collectionLibraryNode() -{ - return modelNodeForId(Constants::COLLECTION_LIB_ID); -} } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionview.h b/src/plugins/qmldesigner/components/collectioneditor/collectionview.h index 9372c74150f..6d81d00f064 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/collectionview.h +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionview.h @@ -1,5 +1,6 @@ // 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 "abstractview.h" @@ -9,7 +10,6 @@ namespace QmlDesigner { -struct Collection; class CollectionWidget; class CollectionView : public AbstractView @@ -19,9 +19,6 @@ class CollectionView : public AbstractView public: explicit CollectionView(ExternalDependenciesInterface &externalDependencies); - bool loadJson(const QByteArray &data); - bool loadCsv(const QString &collectionName, const QByteArray &data); - bool hasWidget() const override; WidgetInfo widgetInfo() override; @@ -44,15 +41,13 @@ public: void selectedNodesChanged(const QList &selectedNodeList, const QList &lastSelectedNodeList) override; - void addNewCollection(const QString &name); + void addResource(const QUrl &url, const QString &name, const QString &type); private: void refreshModel(); - ModelNode getNewCollectionNode(const Collection &collection); - void addLoadedModel(const QList &newCollection); - void renameCollection(ModelNode &material, const QString &newName); - void ensureCollectionLibraryNode(); - ModelNode collectionLibraryNode(); + NodeMetaInfo jsonCollectionMetaInfo() const; + NodeMetaInfo csvCollectionMetaInfo() const; + void ensureStudioModelImport(); QPointer m_widget; }; diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.cpp b/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.cpp index 4bbc1d21e42..c4d9631cefe 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.cpp +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -103,26 +104,23 @@ void CollectionWidget::reloadQmlSource() bool CollectionWidget::loadJsonFile(const QString &jsonFileAddress) { - QUrl jsonUrl(jsonFileAddress); - QString fileAddress = jsonUrl.isLocalFile() ? jsonUrl.toLocalFile() : jsonUrl.toString(); - QFile file(fileAddress); - if (file.open(QFile::ReadOnly)) - return m_view->loadJson(file.readAll()); + if (!isJsonFile(jsonFileAddress)) + return false; - warn("Unable to open the file", file.errorString()); - return false; + QUrl jsonUrl(jsonFileAddress); + QFileInfo fileInfo(jsonUrl.isLocalFile() ? jsonUrl.toLocalFile() : jsonUrl.toString()); + + m_view->addResource(jsonUrl, fileInfo.completeBaseName(), "json"); + + return true; } bool CollectionWidget::loadCsvFile(const QString &collectionName, const QString &csvFileAddress) { QUrl csvUrl(csvFileAddress); - QString fileAddress = csvUrl.isLocalFile() ? csvUrl.toLocalFile() : csvUrl.toString(); - QFile file(fileAddress); - if (file.open(QFile::ReadOnly)) - return m_view->loadCsv(collectionName, file.readAll()); + m_view->addResource(csvUrl, collectionName, "csv"); - warn("Unable to open the file", file.errorString()); - return false; + return true; } bool CollectionWidget::isJsonFile(const QString &jsonFileAddress) const @@ -155,10 +153,10 @@ bool CollectionWidget::isCsvFile(const QString &csvFileAddress) const return true; } -bool CollectionWidget::addCollection(const QString &collectionName) const +bool CollectionWidget::addCollection([[maybe_unused]] const QString &collectionName) const { - m_view->addNewCollection(collectionName); - return true; + // TODO + return false; } void CollectionWidget::warn(const QString &title, const QString &body) diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.h b/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.h index 55f0d3b9587..de2b4d8d9fc 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.h +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.h @@ -1,5 +1,6 @@ // 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 diff --git a/src/plugins/qmldesigner/components/collectioneditor/singlecollectionmodel.cpp b/src/plugins/qmldesigner/components/collectioneditor/singlecollectionmodel.cpp index 040f9307b67..62a95474a55 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/singlecollectionmodel.cpp +++ b/src/plugins/qmldesigner/components/collectioneditor/singlecollectionmodel.cpp @@ -3,31 +3,36 @@ #include "singlecollectionmodel.h" -#include "nodemetainfo.h" +#include "collectioneditorconstants.h" +#include "modelnode.h" #include "variantproperty.h" #include -namespace { -inline bool isListElement(const QmlDesigner::ModelNode &node) -{ - return node.metaInfo().isQtQuickListElement(); -} +#include +#include +#include -inline QByteArrayList getHeaders(const QByteArray &headersValue) +namespace { + +QStringList getJsonHeaders(const QJsonArray &collectionArray) { - QByteArrayList result; - const QByteArrayList initialHeaders = headersValue.split(','); - for (QByteArray header : initialHeaders) { - header = header.trimmed(); - if (header.size()) - result.append(header); + QSet result; + for (const QJsonValue &value : collectionArray) { + if (value.isObject()) { + const QJsonObject object = value.toObject(); + const QStringList headers = object.toVariantMap().keys(); + for (const QString &header : headers) + result.insert(header); + } } - return result; + + return result.values(); } } // namespace namespace QmlDesigner { + SingleCollectionModel::SingleCollectionModel(QObject *parent) : QAbstractTableModel(parent) {} @@ -47,11 +52,11 @@ QVariant SingleCollectionModel::data(const QModelIndex &index, int) const if (!index.isValid()) return {}; - const QByteArray &propertyName = m_headers.at(index.column()); - const ModelNode &elementNode = m_elements.at(index.row()); + const QString &propertyName = m_headers.at(index.column()); + const QJsonObject &elementNode = m_elements.at(index.row()); - if (elementNode.hasVariantProperty(propertyName)) - return elementNode.variantProperty(propertyName).value(); + if (elementNode.contains(propertyName)) + return elementNode.value(propertyName).toVariant(); return {}; } @@ -79,32 +84,110 @@ QVariant SingleCollectionModel::headerData(int section, return {}; } -void SingleCollectionModel::setCollection(const ModelNode &collection) +void SingleCollectionModel::loadCollection(const ModelNode &sourceNode, const QString &collection) +{ + 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); +} + +void SingleCollectionModel::loadJsonCollection(const QString &source, const QString &collection) { beginResetModel(); - m_collectionNode = collection; - updateCollectionName(); + setCollectionName(collection); + QFile sourceFile(source); + QJsonArray collectionNodes; + bool jsonFileIsOk = false; + if (sourceFile.open(QFile::ReadOnly)) { + QJsonParseError jpe; + QJsonDocument document = QJsonDocument::fromJson(sourceFile.readAll(), &jpe); + if (jpe.error == QJsonParseError::NoError) { + jsonFileIsOk = true; + if (document.isObject()) { + QJsonObject collectionMap = document.object(); + if (collectionMap.contains(collection)) { + QJsonValue collectionVal = collectionMap.value(collection); + if (collectionVal.isArray()) + collectionNodes = collectionVal.toArray(); + else + collectionNodes.append(collectionVal); + } + } + } + } - QTC_ASSERT(collection.isValid() && collection.hasVariantProperty("headers"), { + setCollectionSourceFormat(jsonFileIsOk ? SourceFormat::Json : SourceFormat::Unknown); + + if (collectionNodes.isEmpty()) { m_headers.clear(); m_elements.clear(); endResetModel(); return; - }); + } + + m_headers = getJsonHeaders(collectionNodes); + + m_elements.clear(); + for (const QJsonValue &value : std::as_const(collectionNodes)) { + if (value.isObject()) { + QJsonObject object = value.toObject(); + m_elements.append(object); + } + } - m_headers = getHeaders(collection.variantProperty("headers").value().toByteArray()); - m_elements = Utils::filtered(collection.allSubModelNodes(), &isListElement); endResetModel(); } -void SingleCollectionModel::updateCollectionName() +void SingleCollectionModel::loadCsvCollection(const QString &source, const QString &collectionName) +{ + beginResetModel(); + + setCollectionName(collectionName); + QFile sourceFile(source); + m_headers.clear(); + m_elements.clear(); + bool csvFileIsOk = false; + + if (sourceFile.open(QFile::ReadOnly)) { + QTextStream stream(&sourceFile); + + if (!stream.atEnd()) + m_headers = stream.readLine().split(','); + + if (!m_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()) + break; + recordData.insert(m_headers.at(column), cellData); + } + if (recordData.count()) + m_elements.append(recordData); + } + csvFileIsOk = true; + } + } + + setCollectionSourceFormat(csvFileIsOk ? SourceFormat::Csv : SourceFormat::Unknown); + endResetModel(); +} + +void SingleCollectionModel::setCollectionName(const QString &newCollectionName) { - QString newCollectionName = m_collectionNode.isValid() - ? m_collectionNode.variantProperty("objectName").value().toString() - : ""; if (m_collectionName != newCollectionName) { m_collectionName = newCollectionName; emit this->collectionNameChanged(m_collectionName); } } + +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 8d2c6c41b4f..471e43b9674 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/singlecollectionmodel.h +++ b/src/plugins/qmldesigner/components/collectioneditor/singlecollectionmodel.h @@ -1,16 +1,15 @@ // 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 "modelnode.h" - #include - -QT_BEGIN_NAMESPACE -class QJsonArray; -QT_END_NAMESPACE +#include namespace QmlDesigner { + +class ModelNode; + class SingleCollectionModel : public QAbstractTableModel { Q_OBJECT @@ -18,6 +17,7 @@ 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; @@ -29,18 +29,21 @@ public: Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - void setCollection(const ModelNode &collection); + void loadCollection(const ModelNode &sourceNode, const QString &collection); signals: void collectionNameChanged(const QString &collectionName); private: - void updateCollectionName(); + 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); - QByteArrayList m_headers; - ModelNodes m_elements; - ModelNode m_collectionNode; + QStringList m_headers; + QList m_elements; QString m_collectionName; + SourceFormat m_sourceFormat = SourceFormat::Unknown; }; } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/qmldesignerconstants.h b/src/plugins/qmldesigner/qmldesignerconstants.h index 60e1ded8474..ec29c3726a9 100644 --- a/src/plugins/qmldesigner/qmldesignerconstants.h +++ b/src/plugins/qmldesigner/qmldesignerconstants.h @@ -78,7 +78,6 @@ const char QUICK_3D_ASSET_IMPORT_DATA_OPTIONS_KEY[] = "import_options"; const char QUICK_3D_ASSET_IMPORT_DATA_SOURCE_KEY[] = "source_scene"; const char DEFAULT_ASSET_IMPORT_FOLDER[] = "/asset_imports"; const char MATERIAL_LIB_ID[] = "__materialLibrary__"; -const char COLLECTION_LIB_ID[] = "__collectionLibrary__"; const char MIME_TYPE_ITEM_LIBRARY_INFO[] = "application/vnd.qtdesignstudio.itemlibraryinfo"; const char MIME_TYPE_ASSETS[] = "application/vnd.qtdesignstudio.assets";