QmlDesigner: Implement Collection Editor

Data could be loaded from a csv or json file, and would be appended
to the collection node.

Task-number: QDS-10462
Change-Id: I60294582331ba20eb5ecb5d8fd591055c0eb6d1e
Reviewed-by: Miikka Heikkinen <miikka.heikkinen@qt.io>
Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io>
Reviewed-by: Qt CI Patch Build Bot <ci_patchbuild_bot@qt.io>
This commit is contained in:
Ali Kianian
2023-08-25 11:28:29 +03:00
parent 955534ce45
commit f12f3790da
18 changed files with 2215 additions and 15 deletions

View File

@@ -0,0 +1,288 @@
// 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 Qt.labs.platform as PlatformWidgets
import HelperWidgets 2.0 as HelperWidgets
import StudioControls 1.0 as StudioControls
import StudioTheme as StudioTheme
Item {
id: root
implicitWidth: 300
implicitHeight: innerRect.height + 6
property color textColor
signal selectItem(int itemIndex)
signal deleteItem()
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 (!collectionIsSelected) {
collectionIsSelected = true
event.accepted = true
}
}
}
Rectangle {
id: innerRect
anchors.fill: parent
}
Row {
width: parent.width - threeDots.width
leftPadding: 20
Text {
id: moveTool
property StudioTheme.ControlStyle style: StudioTheme.Values.viewBarButtonStyle
width: moveTool.style.squareControlSize.width
height: nameHolder.height
text: StudioTheme.Constants.dragmarks
font.family: StudioTheme.Constants.iconFont.family
font.pixelSize: moveTool.style.baseIconFontSize
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Text {
id: nameHolder
text: collectionName
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: "..."
font.pixelSize: StudioTheme.Values.baseFontSize
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.open()
event.accepted = true
}
}
}
}
PlatformWidgets.Menu {
id: collectionMenu
PlatformWidgets.MenuItem {
text: qsTr("Delete")
shortcut: StandardKey.Delete
onTriggered: deleteDialog.open()
}
PlatformWidgets.MenuItem {
text: qsTr("Rename")
shortcut: StandardKey.Replace
onTriggered: renameDialog.open()
}
}
StudioControls.Dialog {
id: deleteDialog
title: qsTr("Deleting whole collection")
contentItem: Column {
spacing: 2
Text {
text: qsTr("Are you sure that you want to delete collection \"" + collectionName + "\"?")
color: StudioTheme.Values.themeTextColor
}
Item { // spacer
width: 1
height: 20
}
Row {
anchors.right: parent.right
spacing: 10
HelperWidgets.Button {
id: btnDelete
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Delete")
onClicked: root.deleteItem(index)
}
HelperWidgets.Button {
text: qsTr("Cancel")
anchors.verticalCenter: parent.verticalCenter
onClicked: deleteDialog.reject()
}
}
}
}
StudioControls.Dialog {
id: renameDialog
title: qsTr("Rename collection")
onAccepted: {
if (newNameField.text !== "")
collectionName = newNameField.text
}
onOpened: {
newNameField.text = collectionName
}
contentItem: Column {
spacing: 2
Text {
text: qsTr("Previous name: " + collectionName)
color: StudioTheme.Values.themeTextColor
}
Row {
spacing: 10
Text {
text: qsTr("New name:")
color: StudioTheme.Values.themeTextColor
}
StudioControls.TextField {
id: newNameField
anchors.verticalCenter: parent.verticalCenter
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
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Rename")
onClicked: renameDialog.accept()
}
HelperWidgets.Button {
text: qsTr("Cancel")
anchors.verticalCenter: parent.verticalCenter
onClicked: renameDialog.reject()
}
}
}
}
HelperWidgets.RegExpValidator {
id: newNameValidator
regExp: /^\w+$/
}
states: [
State {
name: "default"
when: !collectionIsSelected && !itemMouse.containsMouse
PropertyChanges {
target: innerRect
opacity: 0.6
color: StudioTheme.Values.themeControlBackground
}
PropertyChanges {
target: root
textColor: StudioTheme.Values.themeTextColor
}
},
State {
name: "hovered"
when: !collectionIsSelected && itemMouse.containsMouse
PropertyChanges {
target: innerRect
opacity: 0.8
color: StudioTheme.Values.themeControlBackgroundHover
}
PropertyChanges {
target: root
textColor: StudioTheme.Values.themeTextColor
}
},
State {
name: "selected"
when: collectionIsSelected
PropertyChanges {
target: innerRect
opacity: 1
color: StudioTheme.Values.themeControlBackgroundInteraction
}
PropertyChanges {
target: root
textColor: StudioTheme.Values.themeIconColorSelected
}
}
]
}

View File

@@ -0,0 +1,156 @@
// 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 QtQuickDesignerTheme 1.0
import HelperWidgets 2.0 as HelperWidgets
import StudioTheme 1.0 as StudioTheme
import CollectionEditorBackend
Item {
id: root
focus: true
property var rootView: CollectionEditorBackend.rootView
property var model: CollectionEditorBackend.model
function showWarning(title, message) {
warningDialog.title = title
warningDialog.message = message
warningDialog.open()
}
JsonImport {
id: jsonImporter
backendValue: root.rootView
anchors.centerIn: parent
}
CsvImport {
id: csvImporter
backendValue: root.rootView
anchors.centerIn: parent
}
NewCollectionDialog {
id: newCollection
backendValue: root.rootView
anchors.centerIn: parent
}
Message {
id: warningDialog
title: ""
message: ""
}
Rectangle {
id: collectionsRect
color: StudioTheme.Values.themeToolbarBackground
width: 300
height: root.height
Column {
width: parent.width
Rectangle {
height: StudioTheme.Values.height + 5
color: StudioTheme.Values.themeToolbarBackground
width: parent.width
Text {
id: collectionText
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Collections")
font.pixelSize: StudioTheme.Values.mediumIconFont
color: StudioTheme.Values.themeTextColor
leftPadding: 15
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
rightPadding: 12
spacing: 2
HelperWidgets.IconButton {
icon: StudioTheme.Constants.translationImport
tooltip: qsTr("Import Json")
onClicked: jsonImporter.open()
}
HelperWidgets.IconButton {
icon: StudioTheme.Constants.translationImport
tooltip: qsTr("Import CSV")
onClicked: csvImporter.open()
}
}
}
Rectangle { // Collections
width: parent.width
color: StudioTheme.Values.themeBackgroundColorNormal
height: 330
MouseArea {
anchors.fill: parent
propagateComposedEvents: true
onClicked: (event) => {
root.model.deselect()
event.accepted = true
}
}
ListView {
id: collectionListView
width: parent.width
height: contentHeight
model: root.model
delegate: CollectionItem {
onDeleteItem: root.model.removeRow(index)
}
}
}
Rectangle {
width: parent.width
height: addCollectionButton.height
color: StudioTheme.Values.themeBackgroundColorNormal
IconTextButton {
id: addCollectionButton
anchors.centerIn: parent
text: qsTr("Add new collection")
icon: StudioTheme.Constants.create_medium
onClicked: newCollection.open()
}
}
}
}
Rectangle {
id: collectionRect
color: StudioTheme.Values.themeBackgroundColorAlternate
anchors {
left: collectionsRect.right
right: parent.right
top: parent.top
bottom: parent.bottom
}
}
}

View File

@@ -0,0 +1,188 @@
// 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 QtQuickDesignerTheme 1.0
import Qt.labs.platform as PlatformWidgets
import HelperWidgets 2.0 as HelperWidgets
import StudioControls 1.0 as StudioControls
import StudioTheme as StudioTheme
StudioControls.Dialog {
id: root
title: qsTr("Import A CSV File")
anchors.centerIn: parent
closePolicy: Popup.CloseOnEscape
modal: true
required property var backendValue
property bool fileExists: false
onOpened: {
collectionName.text = "Collection_"
fileName.text = qsTr("New CSV File")
fileName.selectAll()
fileName.forceActiveFocus()
}
onRejected: {
fileName.text = ""
}
HelperWidgets.RegExpValidator {
id: fileNameValidator
regExp: /^(\w[^*><?|]*)[^/\\:*><?|]$/
}
PlatformWidgets.FileDialog {
id: fileDialog
onAccepted: fileName.text = fileDialog.file
}
Message {
id: creationFailedDialog
title: qsTr("Could not load the file")
message: qsTr("An error occurred while trying to load the file.")
onClosed: root.reject()
}
contentItem: Column {
spacing: 10
Row {
spacing: 10
Text {
text: qsTr("File name: ")
anchors.verticalCenter: parent.verticalCenter
color: StudioTheme.Values.themeTextColor
}
StudioControls.TextField {
id: fileName
anchors.verticalCenter: parent.verticalCenter
actionIndicator.visible: false
translationIndicator.visible: false
validator: fileNameValidator
Keys.onEnterPressed: btnCreate.onClicked()
Keys.onReturnPressed: btnCreate.onClicked()
Keys.onEscapePressed: root.reject()
onTextChanged: {
root.fileExists = root.backendValue.isCsvFile(fileName.text)
}
}
HelperWidgets.Button {
id: fileDialogButton
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Open")
onClicked: fileDialog.open()
}
}
Row {
spacing: 10
Text {
text: qsTr("Collection name: ")
anchors.verticalCenter: parent.verticalCenter
color: StudioTheme.Values.themeTextColor
}
StudioControls.TextField {
id: collectionName
anchors.verticalCenter: parent.verticalCenter
actionIndicator.visible: false
translationIndicator.visible: false
validator: HelperWidgets.RegExpValidator {
regExp: /^\w+$/
}
Keys.onEnterPressed: btnCreate.onClicked()
Keys.onReturnPressed: btnCreate.onClicked()
Keys.onEscapePressed: root.reject()
}
}
Text {
id: fieldErrorText
color: StudioTheme.Values.themeTextColor
anchors.right: parent.right
states: [
State {
name: "default"
when: fileName.text !== "" && collectionName.text !== ""
PropertyChanges {
target: fieldErrorText
text: ""
visible: false
}
},
State {
name: "fileError"
when: fileName.text === ""
PropertyChanges {
target: fieldErrorText
text: qsTr("File name can not be empty")
visible: true
}
},
State {
name: "collectionNameError"
when: collectionName.text === ""
PropertyChanges {
target: fieldErrorText
text: qsTr("Collection name can not be empty")
visible: true
}
}
]
}
Item { // spacer
width: 1
height: 20
}
Row {
anchors.right: parent.right
spacing: 10
HelperWidgets.Button {
id: btnCreate
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Import")
enabled: root.fileExists && collectionName.text !== ""
onClicked: {
let csvLoaded = root.backendValue.loadCsvFile(collectionName.text, fileName.text)
if (csvLoaded)
root.accept()
else
creationFailedDialog.open()
}
}
HelperWidgets.Button {
text: qsTr("Cancel")
anchors.verticalCenter: parent.verticalCenter
onClicked: root.reject()
}
}
}
}

View File

@@ -0,0 +1,85 @@
// 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 StudioTheme as StudioTheme
Rectangle {
id: root
required property string text
required property string icon
property StudioTheme.ControlStyle style: StudioTheme.Values.viewBarButtonStyle
implicitHeight: style.squareControlSize.height
implicitWidth: rowAlign.width
signal clicked()
Row {
id: rowAlign
spacing: 0
leftPadding: StudioTheme.Values.inputHorizontalPadding
rightPadding: StudioTheme.Values.inputHorizontalPadding
Text {
id: iconItem
width: root.style.squareControlSize.width
height: root.height
anchors.verticalCenter: parent.verticalCenter
text: root.icon
color: StudioTheme.Values.themeTextColor
font.family: StudioTheme.Constants.iconFont.family
font.pixelSize: root.style.baseIconFontSize
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Text {
id: textItem
height: root.height
anchors.verticalCenter: parent.verticalCenter
text: root.text
color: StudioTheme.Values.themeTextColor
font.family: StudioTheme.Constants.font.family
font.pixelSize: root.style.baseIconFontSize
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: root.clicked()
}
states: [
State {
name: "default"
when: !mouseArea.pressed && !mouseArea.containsMouse
PropertyChanges {
target: root
color: StudioTheme.Values.themeBackgroundColorNormal
}
},
State {
name: "Pressed"
when: mouseArea.pressed
PropertyChanges {
target: root
color: StudioTheme.Values.themeControlBackgroundInteraction
}
},
State {
name: "Hovered"
when: !mouseArea.pressed && mouseArea.containsMouse
PropertyChanges {
target: root
color: StudioTheme.Values.themeControlBackgroundHover
}
}
]
}

View File

@@ -0,0 +1,127 @@
// 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 QtQuickDesignerTheme 1.0
import Qt.labs.platform as PlatformWidgets
import HelperWidgets 2.0 as HelperWidgets
import StudioControls 1.0 as StudioControls
import StudioTheme as StudioTheme
StudioControls.Dialog {
id: root
title: qsTr("Import Collections")
anchors.centerIn: parent
closePolicy: Popup.CloseOnEscape
modal: true
required property var backendValue
property bool fileExists: false
onOpened: {
fileName.text = qsTr("New Json File")
fileName.selectAll()
fileName.forceActiveFocus()
}
onRejected: {
fileName.text = ""
}
HelperWidgets.RegExpValidator {
id: fileNameValidator
regExp: /^(\w[^*><?|]*)[^/\\:*><?|]$/
}
PlatformWidgets.FileDialog {
id: fileDialog
onAccepted: fileName.text = fileDialog.file
}
Message {
id: creationFailedDialog
title: qsTr("Could not load the file")
message: qsTr("An error occurred while trying to load the file.")
onClosed: root.reject()
}
contentItem: Column {
spacing: 2
Row {
spacing: 10
Text {
text: qsTr("File name: ")
anchors.verticalCenter: parent.verticalCenter
color: StudioTheme.Values.themeTextColor
}
StudioControls.TextField {
id: fileName
anchors.verticalCenter: parent.verticalCenter
actionIndicator.visible: false
translationIndicator.visible: false
validator: fileNameValidator
Keys.onEnterPressed: btnCreate.onClicked()
Keys.onReturnPressed: btnCreate.onClicked()
Keys.onEscapePressed: root.reject()
onTextChanged: {
root.fileExists = root.backendValue.isJsonFile(fileName.text)
}
}
HelperWidgets.Button {
id: fileDialogButton
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Open")
onClicked: fileDialog.open()
}
}
Text {
text: qsTr("File name cannot be empty.")
color: StudioTheme.Values.themeTextColor
anchors.right: parent.right
visible: fileName.text === ""
}
Item { // spacer
width: 1
height: 20
}
Row {
anchors.right: parent.right
spacing: 10
HelperWidgets.Button {
id: btnCreate
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Import")
enabled: root.fileExists
onClicked: {
let jsonLoaded = root.backendValue.loadJsonFile(fileName.text)
if (jsonLoaded)
root.accept()
else
creationFailedDialog.open()
}
}
HelperWidgets.Button {
text: qsTr("Cancel")
anchors.verticalCenter: parent.verticalCenter
onClicked: root.reject()
}
}
}
}

View File

@@ -0,0 +1,41 @@
// 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
StudioControls.Dialog {
id: root
required property string message
anchors.centerIn: parent
closePolicy: Popup.CloseOnEscape
implicitWidth: 300
modal: true
contentItem: Column {
spacing: 20
width: parent.width
Text {
text: root.message
color: StudioTheme.Values.themeTextColor
wrapMode: Text.WordWrap
width: root.width
leftPadding: 10
rightPadding: 10
}
HelperWidgets.Button {
text: qsTr("Close")
anchors.right: parent.right
onClicked: root.reject()
}
}
onOpened: root.forceActiveFocus()
}

View File

@@ -0,0 +1,93 @@
// 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 QtQuickDesignerTheme 1.0
import HelperWidgets 2.0 as HelperWidgets
import StudioControls 1.0 as StudioControls
import StudioTheme as StudioTheme
StudioControls.Dialog {
id: root
title: qsTr("Add a new Collection")
anchors.centerIn: parent
closePolicy: Popup.CloseOnEscape
modal: true
required property var backendValue
onOpened: {
collectionName.text = "Collection"
}
onRejected: {
collectionName.text = ""
}
onAccepted: {
if (collectionName.text !== "")
root.backendValue.addCollection(collectionName.text)
}
contentItem: Column {
spacing: 10
Row {
spacing: 10
Text {
text: qsTr("Collection name: ")
anchors.verticalCenter: parent.verticalCenter
color: StudioTheme.Values.themeTextColor
}
StudioControls.TextField {
id: collectionName
anchors.verticalCenter: parent.verticalCenter
actionIndicator.visible: false
translationIndicator.visible: false
validator: HelperWidgets.RegExpValidator {
regExp: /^\w+$/
}
Keys.onEnterPressed: btnCreate.onClicked()
Keys.onReturnPressed: btnCreate.onClicked()
Keys.onEscapePressed: root.reject()
}
}
Text {
id: fieldErrorText
color: StudioTheme.Values.themeTextColor
anchors.right: parent.right
text: qsTr("Collection name can not be empty")
visible: collectionName.text === ""
}
Item { // spacer
width: 1
height: 20
}
Row {
anchors.right: parent.right
spacing: 10
HelperWidgets.Button {
id: btnCreate
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Create")
enabled: collectionName.text !== ""
onClicked: root.accept()
}
HelperWidgets.Button {
text: qsTr("Cancel")
anchors.verticalCenter: parent.verticalCenter
onClicked: root.reject()
}
}
}
}

View File

@@ -446,6 +446,7 @@ add_qtc_plugin(QmlDesigner
INCLUDES INCLUDES
${CMAKE_CURRENT_LIST_DIR}/components ${CMAKE_CURRENT_LIST_DIR}/components
${CMAKE_CURRENT_LIST_DIR}/components/assetslibrary ${CMAKE_CURRENT_LIST_DIR}/components/assetslibrary
${CMAKE_CURRENT_LIST_DIR}/components/collectioneditor
${CMAKE_CURRENT_LIST_DIR}/components/debugview ${CMAKE_CURRENT_LIST_DIR}/components/debugview
${CMAKE_CURRENT_LIST_DIR}/components/edit3d ${CMAKE_CURRENT_LIST_DIR}/components/edit3d
${CMAKE_CURRENT_LIST_DIR}/components/formeditor ${CMAKE_CURRENT_LIST_DIR}/components/formeditor
@@ -801,6 +802,14 @@ extend_qtc_plugin(QmlDesigner
materialeditor.qrc materialeditor.qrc
) )
extend_qtc_plugin(QmlDesigner
SOURCES_PREFIX components/collectioneditor
SOURCES
collectionmodel.cpp collectionmodel.h
collectionview.cpp collectionview.h
collectionwidget.cpp collectionwidget.h
)
extend_qtc_plugin(QmlDesigner extend_qtc_plugin(QmlDesigner
SOURCES_PREFIX components/textureeditor SOURCES_PREFIX components/textureeditor
SOURCES SOURCES

View File

@@ -0,0 +1,253 @@
// 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 "collectionmodel.h"
#include "abstractview.h"
#include "variantproperty.h"
#include <utils/qtcassert.h>
namespace QmlDesigner {
CollectionModel::CollectionModel() {}
int CollectionModel::rowCount(const QModelIndex &) const
{
return m_collections.size();
}
QVariant CollectionModel::data(const QModelIndex &index, int role) const
{
QTC_ASSERT(index.isValid(), return {});
const ModelNode *collection = &m_collections.at(index.row());
switch (role) {
case IdRole:
return collection->id();
case NameRole:
return collection->variantProperty("objectName").value();
case SelectedRole:
return index.row() == m_selectedIndex;
}
return {};
}
bool CollectionModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (!index.isValid())
return false;
ModelNode collection = m_collections.at(index.row());
switch (role) {
case IdRole: {
if (collection.id() == value)
return false;
bool duplicatedId = Utils::anyOf(std::as_const(m_collections),
[&collection, &value](const ModelNode &otherCollection) {
return (otherCollection.id() == value
&& otherCollection != collection);
});
if (duplicatedId)
return false;
collection.setIdWithRefactoring(value.toString());
} break;
case Qt::DisplayRole:
case NameRole: {
auto collectionName = collection.variantProperty("objectName");
if (collectionName.value() == value)
return false;
collectionName.setValue(value.toString());
} break;
case SelectedRole: {
if (value.toBool() != index.data(SelectedRole).toBool())
setSelectedIndex(value.toBool() ? index.row() : -1);
else
return false;
} break;
default:
return false;
}
return true;
}
bool CollectionModel::removeRows(int row, int count, [[maybe_unused]] const QModelIndex &parent)
{
const int rowMax = std::min(row + count, rowCount());
if (row >= rowMax || row < 0)
return false;
AbstractView *view = m_collections.at(row).view();
if (!view)
return false;
count = rowMax - row;
bool selectionUpdateNeeded = m_selectedIndex >= row && m_selectedIndex < rowMax;
// It's better to remove the group of nodes here because of the performance issue for the list,
// and update issue for the view
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());
node.destroy();
}
});
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);
endRemoveRows();
if (selectionUpdateNeeded)
updateSelectedCollection();
updateEmpty();
return true;
}
QHash<int, QByteArray> CollectionModel::roleNames() const
{
static QHash<int, QByteArray> roles;
if (roles.isEmpty()) {
roles.insert(Super::roleNames());
roles.insert({
{IdRole, "collectionId"},
{NameRole, "collectionName"},
{SelectedRole, "collectionIsSelected"},
});
}
return roles;
}
void CollectionModel::setCollections(const ModelNodes &collections)
{
beginResetModel();
bool wasEmpty = isEmpty();
m_collections = collections;
m_collectionsIndexHash.clear();
int i = 0;
for (const ModelNode &collection : collections)
m_collectionsIndexHash.insert(collection.internalId(), i++);
if (wasEmpty != isEmpty())
emit isEmptyChanged(isEmpty());
endResetModel();
updateSelectedCollection(true);
}
void CollectionModel::removeCollection(const ModelNode &node)
{
int nodePlace = m_collectionsIndexHash.value(node.internalId(), -1);
if (nodePlace < 0)
return;
removeRow(nodePlace);
}
int CollectionModel::collectionIndex(const ModelNode &node) const
{
return m_collectionsIndexHash.value(node.internalId(), -1);
}
void CollectionModel::selectCollection(const ModelNode &node)
{
int nodePlace = m_collectionsIndexHash.value(node.internalId(), -1);
if (nodePlace < 0)
return;
selectCollectionIndex(nodePlace, true);
}
bool CollectionModel::isEmpty() const
{
return m_collections.isEmpty();
}
void CollectionModel::selectCollectionIndex(int idx, bool selectAtLeastOne)
{
int collectionCount = m_collections.size();
int prefferedIndex = -1;
if (collectionCount) {
if (selectAtLeastOne)
prefferedIndex = std::max(0, std::min(idx, collectionCount - 1));
else if (idx > -1 && idx < collectionCount)
prefferedIndex = idx;
}
setSelectedIndex(prefferedIndex);
}
void CollectionModel::deselect()
{
setSelectedIndex(-1);
}
void CollectionModel::updateSelectedCollection(bool selectAtLeastOne)
{
int idx = m_selectedIndex;
m_selectedIndex = -1;
selectCollectionIndex(idx, selectAtLeastOne);
}
void CollectionModel::updateNodeName(const ModelNode &node)
{
QModelIndex index = indexOfNode(node);
emit dataChanged(index, index, {NameRole, Qt::DisplayRole});
}
void CollectionModel::updateNodeId(const ModelNode &node)
{
QModelIndex index = indexOfNode(node);
emit dataChanged(index, index, {IdRole});
}
void CollectionModel::setSelectedIndex(int idx)
{
idx = (idx > -1 && idx < m_collections.count()) ? 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 CollectionModel::updateEmpty()
{
bool isEmptyNow = isEmpty();
if (m_isEmpty != isEmptyNow) {
m_isEmpty = isEmptyNow;
emit isEmptyChanged(m_isEmpty);
if (m_isEmpty)
setSelectedIndex(-1);
}
}
QModelIndex CollectionModel::indexOfNode(const ModelNode &node) const
{
return index(m_collectionsIndexHash.value(node.internalId(), -1));
}
} // namespace QmlDesigner

View File

@@ -0,0 +1,70 @@
// 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"
#include <QAbstractListModel>
#include <QHash>
QT_BEGIN_NAMESPACE
class QJsonArray;
QT_END_NAMESPACE
namespace QmlDesigner {
class CollectionModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(int selectedIndex MEMBER m_selectedIndex NOTIFY selectedIndexChanged)
public:
enum Roles { IdRole = Qt::UserRole + 1, NameRole, SelectedRole };
explicit CollectionModel();
virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override;
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
virtual bool setData(const QModelIndex &index,
const QVariant &value,
int role = Qt::EditRole) override;
Q_INVOKABLE virtual bool removeRows(int row,
int count = 1,
const QModelIndex &parent = QModelIndex()) override;
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);
Q_INVOKABLE bool isEmpty() const;
Q_INVOKABLE void selectCollectionIndex(int idx, bool selectAtLeastOne = false);
Q_INVOKABLE void deselect();
Q_INVOKABLE void updateSelectedCollection(bool selectAtLeastOne = false);
void updateNodeName(const ModelNode &node);
void updateNodeId(const ModelNode &node);
signals:
void selectedIndexChanged(int idx);
void renameCollectionTriggered(const QmlDesigner::ModelNode &collection, const QString &newName);
void addNewCollectionTriggered();
void isEmptyChanged(bool);
private:
void setSelectedIndex(int idx);
void updateEmpty();
using Super = QAbstractListModel;
QModelIndex indexOfNode(const ModelNode &node) const;
ModelNodes m_collections;
QHash<qint32, int> m_collectionsIndexHash; // internalId -> index
int m_selectedIndex = -1;
bool m_isEmpty = true;
};
} // namespace QmlDesigner

View File

@@ -0,0 +1,598 @@
// 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 "collectionview.h"
#include "collectionmodel.h"
#include "collectionwidget.h"
#include "designmodecontext.h"
#include "nodelistproperty.h"
#include "nodemetainfo.h"
#include "qmldesignerconstants.h"
#include "qmldesignerplugin.h"
#include "variantproperty.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <coreplugin/icore.h>
#include <utils/algorithm.h>
#include <utils/qtcassert.h>
namespace {
using Data = std::variant<bool, double, QString, QDateTime>;
using DataRecord = QMap<QString, Data>;
struct DataHeader
{
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());
}
} // 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;
}
QmlDesigner::WidgetInfo CollectionView::widgetInfo()
{
if (m_widget.isNull()) {
m_widget = new CollectionWidget(this);
auto collectionEditorContext = new Internal::CollectionEditorContext(m_widget.data());
Core::ICore::addContextObject(collectionEditorContext);
}
return createWidgetInfo(m_widget.data(),
"CollectionEditor",
WidgetInfo::LeftPane,
0,
tr("Collection Editor"),
tr("Collection Editor view"));
}
void CollectionView::modelAttached(Model *model)
{
AbstractView::modelAttached(model);
refreshModel();
}
void CollectionView::nodeReparented(const ModelNode &node,
const NodeAbstractProperty &newPropertyParent,
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)
return;
refreshModel();
if (isCollection(node))
m_widget->collectionModel()->selectCollection(node);
}
void CollectionView::nodeAboutToBeRemoved(const ModelNode &removedNode)
{
// removing the collections lib node
if (isCollectionLib(removedNode)) {
m_widget->collectionModel()->setCollections({});
return;
}
if (isCollection(removedNode))
m_widget->collectionModel()->removeCollection(removedNode);
}
void CollectionView::nodeRemoved([[maybe_unused]] const ModelNode &removedNode,
const NodeAbstractProperty &parentProperty,
[[maybe_unused]] PropertyChangeFlags propertyChange)
{
if (parentProperty.parentModelNode().id() != Constants::COLLECTION_LIB_ID)
return;
m_widget->collectionModel()->updateSelectedCollection(true);
}
void CollectionView::variantPropertiesChanged(const QList<VariantProperty> &propertyList,
[[maybe_unused]] PropertyChangeFlags propertyChange)
{
for (const VariantProperty &property : propertyList) {
ModelNode node(property.parentModelNode());
if (isCollection(node)) {
if (property.name() == "objectName")
m_widget->collectionModel()->updateNodeName(node);
else if (property.name() == "id")
m_widget->collectionModel()->updateNodeId(node);
}
}
}
void CollectionView::selectedNodesChanged(const QList<ModelNode> &selectedNodeList,
[[maybe_unused]] const QList<ModelNode> &lastSelectedNodeList)
{
QList<ModelNode> selectedCollections = Utils::filtered(selectedNodeList, &isCollection);
// More than one collections are selected. So ignore them
if (selectedCollections.size() > 1)
return;
if (selectedCollections.size() == 1) { // If exactly one collection is selected
m_widget->collectionModel()->selectCollection(selectedCollections.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->collectionModel()->selectCollection(parentElement);
}
}
void CollectionView::addNewCollection(const QString &name)
{
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);
});
}
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->collectionModel()->setCollections(collections);
}
ModelNode CollectionView::getNewCollectionNode(const Collection &collection)
{
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;
}
void CollectionView::addLoadedModel(const QList<Collection> &newCollection)
{
executeInTransaction(__FUNCTION__, [&] {
ensureCollectionLibraryNode();
ModelNode collectionLib = collectionLibraryNode();
if (!collectionLib.isValid())
return;
for (const Collection &collection : newCollection) {
ModelNode collectionNode = getNewCollectionNode(collection);
collectionLib.defaultNodeListProperty().reparentHere(collectionNode);
}
});
}
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";
matLib = 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

@@ -0,0 +1,59 @@
// 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"
#include "modelnode.h"
#include <QDateTime>
namespace QmlDesigner {
struct Collection;
class CollectionWidget;
class CollectionView : public AbstractView
{
Q_OBJECT
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;
void modelAttached(Model *model) override;
void nodeReparented(const ModelNode &node,
const NodeAbstractProperty &newPropertyParent,
const NodeAbstractProperty &oldPropertyParent,
PropertyChangeFlags propertyChange) override;
void nodeAboutToBeRemoved(const ModelNode &removedNode) override;
void nodeRemoved(const ModelNode &removedNode,
const NodeAbstractProperty &parentProperty,
PropertyChangeFlags propertyChange) override;
void variantPropertiesChanged(const QList<VariantProperty> &propertyList,
PropertyChangeFlags propertyChange) override;
void selectedNodesChanged(const QList<ModelNode> &selectedNodeList,
const QList<ModelNode> &lastSelectedNodeList) override;
void addNewCollection(const QString &name);
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();
QPointer<CollectionWidget> m_widget;
};
} // namespace QmlDesigner

View File

@@ -0,0 +1,162 @@
// 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 "collectionwidget.h"
#include "collectionmodel.h"
#include "collectionview.h"
#include "qmldesignerconstants.h"
#include "qmldesignerplugin.h"
#include "theme.h"
#include <studioquickwidget.h>
#include <coreplugin/icore.h>
#include <QFile>
#include <QJsonDocument>
#include <QJsonParseError>
#include <QMetaObject>
#include <QQmlEngine>
#include <QQuickItem>
#include <QShortcut>
#include <QVBoxLayout>
namespace {
QString collectionViewResourcesPath()
{
#ifdef SHARE_QML_PATH
if (qEnvironmentVariableIsSet("LOAD_QML_FROM_SOURCE"))
return QLatin1String(SHARE_QML_PATH) + "/collectionEditorQmlSource";
#endif
return Core::ICore::resourcePath("qmldesigner/collectionEditorQmlSource").toString();
}
} // namespace
namespace QmlDesigner {
CollectionWidget::CollectionWidget(CollectionView *view)
: QFrame()
, m_view(view)
, m_model(new CollectionModel)
, m_quickWidget(new StudioQuickWidget(this))
{
setWindowTitle(tr("Collection View", "Title of collection view widget"));
Core::IContext *icontext = nullptr;
Core::Context context(Constants::C_QMLMATERIALBROWSER);
icontext = new Core::IContext(this);
icontext->setContext(context);
icontext->setWidget(this);
m_quickWidget->quickWidget()->setObjectName(Constants::OBJECT_NAME_COLLECTION_EDITOR);
m_quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
m_quickWidget->engine()->addImportPath(collectionViewResourcesPath() + "/imports");
m_quickWidget->setClearColor(Theme::getColor(Theme::Color::DSpanelBackground));
Theme::setupTheme(m_quickWidget->engine());
m_quickWidget->quickWidget()->installEventFilter(this);
auto layout = new QVBoxLayout(this);
layout->setContentsMargins({});
layout->setSpacing(0);
layout->addWidget(m_quickWidget.data());
qmlRegisterAnonymousType<CollectionWidget>("CollectionEditorBackend", 1);
auto map = m_quickWidget->registerPropertyMap("CollectionEditorBackend");
map->setProperties(
{{"rootView", QVariant::fromValue(this)}, {"model", QVariant::fromValue(m_model.data())}});
auto hotReloadShortcut = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_F4), this);
connect(hotReloadShortcut, &QShortcut::activated, this, &CollectionWidget::reloadQmlSource);
reloadQmlSource();
}
void CollectionWidget::contextHelp(const Core::IContext::HelpCallback &callback) const
{
if (m_view)
QmlDesignerPlugin::contextHelp(callback, m_view->contextHelpId());
else
callback({});
}
QPointer<CollectionModel> CollectionWidget::collectionModel() const
{
return m_model;
}
void CollectionWidget::reloadQmlSource()
{
const QString collectionViewQmlPath = collectionViewResourcesPath() + "/CollectionView.qml";
QTC_ASSERT(QFileInfo::exists(collectionViewQmlPath), return);
m_quickWidget->setSource(QUrl::fromLocalFile(collectionViewQmlPath));
}
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());
warn("Unable to open the file", file.errorString());
return false;
}
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());
warn("Unable to open the file", file.errorString());
return false;
}
bool CollectionWidget::isJsonFile(const QString &jsonFileAddress) const
{
QUrl jsonUrl(jsonFileAddress);
QString fileAddress = jsonUrl.isLocalFile() ? jsonUrl.toLocalFile() : jsonUrl.toString();
QFile file(fileAddress);
if (!file.exists() || !file.open(QFile::ReadOnly))
return false;
QJsonParseError error;
QJsonDocument::fromJson(file.readAll(), &error);
if (error.error)
return false;
return true;
}
bool CollectionWidget::isCsvFile(const QString &csvFileAddress) const
{
QUrl csvUrl(csvFileAddress);
QString fileAddress = csvUrl.isLocalFile() ? csvUrl.toLocalFile() : csvUrl.toString();
QFile file(fileAddress);
if (!file.exists())
return false;
// TODO: Evaluate the csv file
return true;
}
bool CollectionWidget::addCollection(const QString &collectionName) const
{
m_view->addNewCollection(collectionName);
return true;
}
void CollectionWidget::warn(const QString &title, const QString &body)
{
QMetaObject::invokeMethod(m_quickWidget->rootObject(),
"showWarning",
Q_ARG(QVariant, title),
Q_ARG(QVariant, body));
}
} // namespace QmlDesigner

View File

@@ -0,0 +1,42 @@
// 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>
#include <coreplugin/icontext.h>
class StudioQuickWidget;
namespace QmlDesigner {
class CollectionModel;
class CollectionView;
class CollectionWidget : public QFrame
{
Q_OBJECT
public:
CollectionWidget(CollectionView *view);
void contextHelp(const Core::IContext::HelpCallback &callback) const;
QPointer<CollectionModel> collectionModel() const;
void reloadQmlSource();
Q_INVOKABLE bool loadJsonFile(const QString &jsonFileAddress);
Q_INVOKABLE bool loadCsvFile(const QString &collectionName, const QString &csvFileAddress);
Q_INVOKABLE bool isJsonFile(const QString &jsonFileAddress) const;
Q_INVOKABLE bool isCsvFile(const QString &csvFileAddress) const;
Q_INVOKABLE bool addCollection(const QString &collectionName) const;
void warn(const QString &title, const QString &body);
private:
QPointer<CollectionView> m_view;
QPointer<CollectionModel> m_model;
QScopedPointer<StudioQuickWidget> m_quickWidget;
};
} // namespace QmlDesigner

View File

@@ -7,6 +7,7 @@
#include <abstractview.h> #include <abstractview.h>
#include <assetslibraryview.h> #include <assetslibraryview.h>
#include <capturingconnectionmanager.h> #include <capturingconnectionmanager.h>
#include <collectionview.h>
#include <componentaction.h> #include <componentaction.h>
#include <componentview.h> #include <componentview.h>
#include <contentlibraryview.h> #include <contentlibraryview.h>
@@ -23,11 +24,11 @@
#include <navigatorview.h> #include <navigatorview.h>
#include <nodeinstanceview.h> #include <nodeinstanceview.h>
#include <propertyeditorview.h> #include <propertyeditorview.h>
#include <qmldesignerplugin.h>
#include <rewriterview.h> #include <rewriterview.h>
#include <stateseditorview.h> #include <stateseditorview.h>
#include <texteditorview.h> #include <texteditorview.h>
#include <textureeditorview.h> #include <textureeditorview.h>
#include <qmldesignerplugin.h>
#include <utils/algorithm.h> #include <utils/algorithm.h>
@@ -51,6 +52,7 @@ public:
: connectionManager, : connectionManager,
externalDependencies, externalDependencies,
true) true)
, collectionView{externalDependencies}
, contentLibraryView{externalDependencies} , contentLibraryView{externalDependencies}
, componentView{externalDependencies} , componentView{externalDependencies}
, edit3DView{externalDependencies} , edit3DView{externalDependencies}
@@ -73,6 +75,7 @@ public:
Internal::DebugView debugView; Internal::DebugView debugView;
DesignerActionManagerView designerActionManagerView; DesignerActionManagerView designerActionManagerView;
NodeInstanceView nodeInstanceView; NodeInstanceView nodeInstanceView;
CollectionView collectionView;
ContentLibraryView contentLibraryView; ContentLibraryView contentLibraryView;
ComponentView componentView; ComponentView componentView;
Edit3DView edit3DView; Edit3DView edit3DView;
@@ -212,6 +215,9 @@ QList<AbstractView *> ViewManager::standardViews() const
if (qEnvironmentVariableIsSet("ENABLE_QDS_EFFECTMAKER")) if (qEnvironmentVariableIsSet("ENABLE_QDS_EFFECTMAKER"))
list.append(&d->effectMakerView); list.append(&d->effectMakerView);
if (qEnvironmentVariableIsSet("ENABLE_QDS_COLLECTIONVIEW"))
list.append(&d->collectionView);
#ifdef CHECK_LICENSE #ifdef CHECK_LICENSE
if (checkLicense() == FoundLicense::enterprise) if (checkLicense() == FoundLicense::enterprise)
list.append(&d->contentLibraryView); list.append(&d->contentLibraryView);
@@ -390,6 +396,9 @@ QList<WidgetInfo> ViewManager::widgetInfos() const
if (qEnvironmentVariableIsSet("ENABLE_QDS_EFFECTMAKER")) if (qEnvironmentVariableIsSet("ENABLE_QDS_EFFECTMAKER"))
widgetInfoList.append(d->effectMakerView.widgetInfo()); widgetInfoList.append(d->effectMakerView.widgetInfo());
if (qEnvironmentVariableIsSet("ENABLE_QDS_COLLECTIONVIEW"))
widgetInfoList.append(d->collectionView.widgetInfo());
#ifdef CHECK_LICENSE #ifdef CHECK_LICENSE
if (checkLicense() == FoundLicense::enterprise) if (checkLicense() == FoundLicense::enterprise)
widgetInfoList.append(d->contentLibraryView.widgetInfo()); widgetInfoList.append(d->contentLibraryView.widgetInfo());

View File

@@ -3,6 +3,7 @@
#include "designmodecontext.h" #include "designmodecontext.h"
#include "assetslibrarywidget.h" #include "assetslibrarywidget.h"
#include "collectionwidget.h"
#include "designmodewidget.h" #include "designmodewidget.h"
#include "edit3dwidget.h" #include "edit3dwidget.h"
#include "effectmakerwidget.h" #include "effectmakerwidget.h"
@@ -12,8 +13,7 @@
#include "qmldesignerconstants.h" #include "qmldesignerconstants.h"
#include "texteditorwidget.h" #include "texteditorwidget.h"
namespace QmlDesigner { namespace QmlDesigner::Internal {
namespace Internal {
DesignModeContext::DesignModeContext(QWidget *widget) DesignModeContext::DesignModeContext(QWidget *widget)
: IContext(widget) : IContext(widget)
@@ -111,6 +111,15 @@ void EffectMakerContext::contextHelp(const HelpCallback &callback) const
qobject_cast<EffectMakerWidget *>(m_widget)->contextHelp(callback); qobject_cast<EffectMakerWidget *>(m_widget)->contextHelp(callback);
} }
} CollectionEditorContext::CollectionEditorContext(QWidget *widget)
: IContext(widget)
{
setWidget(widget);
setContext(Core::Context(Constants::C_QMLCOLLECTIONEDITOR, Constants::C_QT_QUICK_TOOLS_MENU));
} }
void CollectionEditorContext::contextHelp(const HelpCallback &callback) const
{
qobject_cast<CollectionWidget *>(m_widget)->contextHelp(callback);
}
} // namespace QmlDesigner::Internal

View File

@@ -83,5 +83,13 @@ public:
void contextHelp(const Core::IContext::HelpCallback &callback) const override; void contextHelp(const Core::IContext::HelpCallback &callback) const override;
}; };
} class CollectionEditorContext : public Core::IContext
} {
Q_OBJECT
public:
CollectionEditorContext(QWidget *widget);
void contextHelp(const Core::IContext::HelpCallback &callback) const override;
};
} // namespace Internal
} // namespace QmlDesigner

View File

@@ -19,6 +19,7 @@ const char C_QMLNAVIGATOR[] = "QmlDesigner::Navigator";
const char C_QMLTEXTEDITOR[] = "QmlDesigner::TextEditor"; const char C_QMLTEXTEDITOR[] = "QmlDesigner::TextEditor";
const char C_QMLMATERIALBROWSER[] = "QmlDesigner::MaterialBrowser"; const char C_QMLMATERIALBROWSER[] = "QmlDesigner::MaterialBrowser";
const char C_QMLASSETSLIBRARY[] = "QmlDesigner::AssetsLibrary"; const char C_QMLASSETSLIBRARY[] = "QmlDesigner::AssetsLibrary";
const char C_QMLCOLLECTIONEDITOR[] = "QmlDesigner::CollectionEditor";
// Special context for preview menu, shared b/w designer and text editor // Special context for preview menu, shared b/w designer and text editor
const char C_QT_QUICK_TOOLS_MENU[] = "QmlDesigner::ToolsMenu"; const char C_QT_QUICK_TOOLS_MENU[] = "QmlDesigner::ToolsMenu";
@@ -77,6 +78,7 @@ const char QUICK_3D_ASSET_IMPORT_DATA_OPTIONS_KEY[] = "import_options";
const char QUICK_3D_ASSET_IMPORT_DATA_SOURCE_KEY[] = "source_scene"; const char QUICK_3D_ASSET_IMPORT_DATA_SOURCE_KEY[] = "source_scene";
const char DEFAULT_ASSET_IMPORT_FOLDER[] = "/asset_imports"; const char DEFAULT_ASSET_IMPORT_FOLDER[] = "/asset_imports";
const char MATERIAL_LIB_ID[] = "__materialLibrary__"; 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_ITEM_LIBRARY_INFO[] = "application/vnd.qtdesignstudio.itemlibraryinfo";
const char MIME_TYPE_ASSETS[] = "application/vnd.qtdesignstudio.assets"; const char MIME_TYPE_ASSETS[] = "application/vnd.qtdesignstudio.assets";
@@ -159,6 +161,7 @@ const char OBJECT_NAME_EFFECT_MAKER[] = "QQuickWidgetEffectMaker";
const char OBJECT_NAME_MATERIAL_BROWSER[] = "QQuickWidgetMaterialBrowser"; const char OBJECT_NAME_MATERIAL_BROWSER[] = "QQuickWidgetMaterialBrowser";
const char OBJECT_NAME_MATERIAL_EDITOR[] = "QQuickWidgetMaterialEditor"; const char OBJECT_NAME_MATERIAL_EDITOR[] = "QQuickWidgetMaterialEditor";
const char OBJECT_NAME_PROPERTY_EDITOR[] = "QQuickWidgetPropertyEditor"; const char OBJECT_NAME_PROPERTY_EDITOR[] = "QQuickWidgetPropertyEditor";
const char OBJECT_NAME_COLLECTION_EDITOR[] = "QQuickWidgetQDSCollectionEditor";
const char OBJECT_NAME_STATES_EDITOR[] = "QQuickWidgetStatesEditor"; const char OBJECT_NAME_STATES_EDITOR[] = "QQuickWidgetStatesEditor";
const char OBJECT_NAME_TEXTURE_EDITOR[] = "QQuickWidgetTextureEditor"; const char OBJECT_NAME_TEXTURE_EDITOR[] = "QQuickWidgetTextureEditor";
const char OBJECT_NAME_TOP_TOOLBAR[] = "QQuickWidgetTopToolbar"; const char OBJECT_NAME_TOP_TOOLBAR[] = "QQuickWidgetTopToolbar";