forked from qt-creator/qt-creator
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:
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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()
|
||||
}
|
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -446,6 +446,7 @@ add_qtc_plugin(QmlDesigner
|
||||
INCLUDES
|
||||
${CMAKE_CURRENT_LIST_DIR}/components
|
||||
${CMAKE_CURRENT_LIST_DIR}/components/assetslibrary
|
||||
${CMAKE_CURRENT_LIST_DIR}/components/collectioneditor
|
||||
${CMAKE_CURRENT_LIST_DIR}/components/debugview
|
||||
${CMAKE_CURRENT_LIST_DIR}/components/edit3d
|
||||
${CMAKE_CURRENT_LIST_DIR}/components/formeditor
|
||||
@@ -801,6 +802,14 @@ extend_qtc_plugin(QmlDesigner
|
||||
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
|
||||
SOURCES_PREFIX components/textureeditor
|
||||
SOURCES
|
||||
|
@@ -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
|
@@ -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
|
@@ -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
|
@@ -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
|
@@ -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
|
@@ -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
|
@@ -7,6 +7,7 @@
|
||||
#include <abstractview.h>
|
||||
#include <assetslibraryview.h>
|
||||
#include <capturingconnectionmanager.h>
|
||||
#include <collectionview.h>
|
||||
#include <componentaction.h>
|
||||
#include <componentview.h>
|
||||
#include <contentlibraryview.h>
|
||||
@@ -23,11 +24,11 @@
|
||||
#include <navigatorview.h>
|
||||
#include <nodeinstanceview.h>
|
||||
#include <propertyeditorview.h>
|
||||
#include <qmldesignerplugin.h>
|
||||
#include <rewriterview.h>
|
||||
#include <stateseditorview.h>
|
||||
#include <texteditorview.h>
|
||||
#include <textureeditorview.h>
|
||||
#include <qmldesignerplugin.h>
|
||||
|
||||
#include <utils/algorithm.h>
|
||||
|
||||
@@ -51,6 +52,7 @@ public:
|
||||
: connectionManager,
|
||||
externalDependencies,
|
||||
true)
|
||||
, collectionView{externalDependencies}
|
||||
, contentLibraryView{externalDependencies}
|
||||
, componentView{externalDependencies}
|
||||
, edit3DView{externalDependencies}
|
||||
@@ -73,6 +75,7 @@ public:
|
||||
Internal::DebugView debugView;
|
||||
DesignerActionManagerView designerActionManagerView;
|
||||
NodeInstanceView nodeInstanceView;
|
||||
CollectionView collectionView;
|
||||
ContentLibraryView contentLibraryView;
|
||||
ComponentView componentView;
|
||||
Edit3DView edit3DView;
|
||||
@@ -212,6 +215,9 @@ QList<AbstractView *> ViewManager::standardViews() const
|
||||
if (qEnvironmentVariableIsSet("ENABLE_QDS_EFFECTMAKER"))
|
||||
list.append(&d->effectMakerView);
|
||||
|
||||
if (qEnvironmentVariableIsSet("ENABLE_QDS_COLLECTIONVIEW"))
|
||||
list.append(&d->collectionView);
|
||||
|
||||
#ifdef CHECK_LICENSE
|
||||
if (checkLicense() == FoundLicense::enterprise)
|
||||
list.append(&d->contentLibraryView);
|
||||
@@ -390,6 +396,9 @@ QList<WidgetInfo> ViewManager::widgetInfos() const
|
||||
if (qEnvironmentVariableIsSet("ENABLE_QDS_EFFECTMAKER"))
|
||||
widgetInfoList.append(d->effectMakerView.widgetInfo());
|
||||
|
||||
if (qEnvironmentVariableIsSet("ENABLE_QDS_COLLECTIONVIEW"))
|
||||
widgetInfoList.append(d->collectionView.widgetInfo());
|
||||
|
||||
#ifdef CHECK_LICENSE
|
||||
if (checkLicense() == FoundLicense::enterprise)
|
||||
widgetInfoList.append(d->contentLibraryView.widgetInfo());
|
||||
|
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "designmodecontext.h"
|
||||
#include "assetslibrarywidget.h"
|
||||
#include "collectionwidget.h"
|
||||
#include "designmodewidget.h"
|
||||
#include "edit3dwidget.h"
|
||||
#include "effectmakerwidget.h"
|
||||
@@ -12,11 +13,10 @@
|
||||
#include "qmldesignerconstants.h"
|
||||
#include "texteditorwidget.h"
|
||||
|
||||
namespace QmlDesigner {
|
||||
namespace Internal {
|
||||
namespace QmlDesigner::Internal {
|
||||
|
||||
DesignModeContext::DesignModeContext(QWidget *widget)
|
||||
: IContext(widget)
|
||||
: IContext(widget)
|
||||
{
|
||||
setWidget(widget);
|
||||
setContext(Core::Context(Constants::C_QMLDESIGNER, Constants::C_QT_QUICK_TOOLS_MENU));
|
||||
@@ -111,6 +111,15 @@ void EffectMakerContext::contextHelp(const HelpCallback &callback) const
|
||||
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
|
||||
|
@@ -83,5 +83,13 @@ public:
|
||||
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
|
||||
|
@@ -11,14 +11,15 @@ const char C_DELETE[] = "QmlDesigner.Delete";
|
||||
const char C_DUPLICATE[] = "QmlDesigner.Duplicate";
|
||||
|
||||
// Context
|
||||
const char C_QMLDESIGNER[] = "QmlDesigner::QmlDesignerMain";
|
||||
const char C_QMLFORMEDITOR[] = "QmlDesigner::FormEditor";
|
||||
const char C_QMLEDITOR3D[] = "QmlDesigner::Editor3D";
|
||||
const char C_QMLEFFECTMAKER[] = "QmlDesigner::EffectMaker";
|
||||
const char C_QMLNAVIGATOR[] = "QmlDesigner::Navigator";
|
||||
const char C_QMLTEXTEDITOR[] = "QmlDesigner::TextEditor";
|
||||
const char C_QMLMATERIALBROWSER[] = "QmlDesigner::MaterialBrowser";
|
||||
const char C_QMLASSETSLIBRARY[] = "QmlDesigner::AssetsLibrary";
|
||||
const char C_QMLDESIGNER[] = "QmlDesigner::QmlDesignerMain";
|
||||
const char C_QMLFORMEDITOR[] = "QmlDesigner::FormEditor";
|
||||
const char C_QMLEDITOR3D[] = "QmlDesigner::Editor3D";
|
||||
const char C_QMLEFFECTMAKER[] = "QmlDesigner::EffectMaker";
|
||||
const char C_QMLNAVIGATOR[] = "QmlDesigner::Navigator";
|
||||
const char C_QMLTEXTEDITOR[] = "QmlDesigner::TextEditor";
|
||||
const char C_QMLMATERIALBROWSER[] = "QmlDesigner::MaterialBrowser";
|
||||
const char C_QMLASSETSLIBRARY[] = "QmlDesigner::AssetsLibrary";
|
||||
const char C_QMLCOLLECTIONEDITOR[] = "QmlDesigner::CollectionEditor";
|
||||
|
||||
// Special context for preview menu, shared b/w designer and text editor
|
||||
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 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";
|
||||
@@ -159,6 +161,7 @@ const char OBJECT_NAME_EFFECT_MAKER[] = "QQuickWidgetEffectMaker";
|
||||
const char OBJECT_NAME_MATERIAL_BROWSER[] = "QQuickWidgetMaterialBrowser";
|
||||
const char OBJECT_NAME_MATERIAL_EDITOR[] = "QQuickWidgetMaterialEditor";
|
||||
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_TEXTURE_EDITOR[] = "QQuickWidgetTextureEditor";
|
||||
const char OBJECT_NAME_TOP_TOOLBAR[] = "QQuickWidgetTopToolbar";
|
||||
|
Reference in New Issue
Block a user