QmlDesigner: Use Studio.Models as the source of the CollectionEditor

QtQuick.Studio.Models JSON and CSV components are used as the source
of the Collection Editor.
Collections are placed underneath the sources in the collections view

Task-number: QDS-10809
Task-number: QDS-10462
Change-Id: Ia0c9cb587c462fcba98934b15068582f3f9c19c5
Reviewed-by: Miikka Heikkinen <miikka.heikkinen@qt.io>
Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io>
This commit is contained in:
Ali Kianian
2023-09-22 10:50:06 +03:00
parent 42f231e4a2
commit 01a4f087c6
16 changed files with 981 additions and 627 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 <utils/algorithm.h>
#include <utils/qtcassert.h>
namespace {
template<typename ValueType>
bool containsItem(const std::initializer_list<ValueType> &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<int, QByteArray> CollectionListModel::roleNames() const
{
static QHash<int, QByteArray> 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<int>({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

View File

@@ -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 <QHash>
#include <QStringListModel>
#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<int, QByteArray> 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

View File

@@ -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 <utils/qtcassert.h>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
namespace {
QSharedPointer<QmlDesigner::CollectionListModel> loadCollection(
const QmlDesigner::ModelNode &sourceNode,
QSharedPointer<QmlDesigner::CollectionListModel> initialCollection = {})
{
using namespace QmlDesigner::CollectionEditor;
QString sourceFileAddress = sourceNode.variantProperty(SOURCEFILE_PROPERTY).value().toString();
QSharedPointer<QmlDesigner::CollectionListModel> 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<const ModelNode>(m_collections).subspan(row, count)) {
m_collectionsIndexHash.remove(node.internalId());
for (ModelNode node : Utils::span<const ModelNode>(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<const ModelNode>(m_collections).subspan(row))
m_collectionsIndexHash.insert(node.internalId(), ++idx);
for (const ModelNode &node : Utils::span<const ModelNode>(m_collectionSources).subspan(row))
m_sourceIndexHash.insert(node.internalId(), ++idx);
endRemoveRows();
if (selectionUpdateNeeded)
updateSelectedCollection();
updateSelectedSource();
updateEmpty();
return true;
@@ -121,82 +186,115 @@ QHash<int, QByteArray> CollectionSourceModel::roleNames() const
static QHash<int, QByteArray> 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<CollectionListModel *>();
}
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<CollectionListModel *>(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<CollectionListModel> currentList = m_collectionList.at(index.row());
QSharedPointer<CollectionListModel> 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

View File

@@ -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 <QAbstractListModel>
#include <QHash>
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<int, QByteArray> 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<qint32, int> m_collectionsIndexHash; // internalId -> index
ModelNodes m_collectionSources;
QHash<qint32, int> m_sourceIndexHash; // internalId -> index
QList<QSharedPointer<CollectionListModel>> m_collectionList;
QPointer<CollectionListModel> _previousSelectedList;
int m_selectedIndex = -1;
bool m_isEmpty = true;
};

View File

@@ -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 <utils/qtcassert.h>
namespace {
using Data = std::variant<bool, double, QString, QDateTime>;
using DataRecord = QMap<QString, Data>;
struct DataHeader
inline bool isStudioCollectionModel(const QmlDesigner::ModelNode &node)
{
enum class Type { Unknown, Bool, Numeric, String, DateTime };
Type type;
QString name;
};
using DataHeaderMap = QMap<QString, DataHeader>; // 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<QString, DataHeader::Type> 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<DataHeader> &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<DataRecord> &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<DataHeader> headers;
QList<DataRecord> 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<Collection> 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<VariantProperty> &propertyList,
@@ -424,9 +106,11 @@ void CollectionView::variantPropertiesChanged(const QList<VariantProperty> &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<VariantProperty> &prop
void CollectionView::selectedNodesChanged(const QList<ModelNode> &selectedNodeList,
[[maybe_unused]] const QList<ModelNode> &lastSelectedNodeList)
{
QList<ModelNode> selectedCollections = Utils::filtered(selectedNodeList, &isCollection);
QList<ModelNode> 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<ModelNode> 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<ModelNode> 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<Collection> &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

View File

@@ -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<ModelNode> &selectedNodeList,
const QList<ModelNode> &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<Collection> &newCollection);
void renameCollection(ModelNode &material, const QString &newName);
void ensureCollectionLibraryNode();
ModelNode collectionLibraryNode();
NodeMetaInfo jsonCollectionMetaInfo() const;
NodeMetaInfo csvCollectionMetaInfo() const;
void ensureStudioModelImport();
QPointer<CollectionWidget> m_widget;
};

View File

@@ -13,6 +13,7 @@
#include <coreplugin/icore.h>
#include <QFile>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonParseError>
#include <QMetaObject>
@@ -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)

View File

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

View File

@@ -3,31 +3,36 @@
#include "singlecollectionmodel.h"
#include "nodemetainfo.h"
#include "collectioneditorconstants.h"
#include "modelnode.h"
#include "variantproperty.h"
#include <utils/qtcassert.h>
namespace {
inline bool isListElement(const QmlDesigner::ModelNode &node)
{
return node.metaInfo().isQtQuickListElement();
}
#include <QFile>
#include <QJsonArray>
#include <QJsonParseError>
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<QString> 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

View File

@@ -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 <QAbstractTableModel>
QT_BEGIN_NAMESPACE
class QJsonArray;
QT_END_NAMESPACE
#include <QJsonObject>
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<QJsonObject> m_elements;
QString m_collectionName;
SourceFormat m_sourceFormat = SourceFormat::Unknown;
};
} // namespace QmlDesigner

View File

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