forked from qt-creator/qt-creator
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:
@@ -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
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@@ -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
|
||||
|
@@ -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
|
@@ -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
|
@@ -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
|
@@ -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
|
||||
|
@@ -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;
|
||||
};
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
};
|
||||
|
@@ -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)
|
||||
|
@@ -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>
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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";
|
||||
|
Reference in New Issue
Block a user