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
|
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
|
||||||
|
@@ -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 <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());
|
||||||
|
@@ -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,11 +13,10 @@
|
|||||||
#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)
|
||||||
{
|
{
|
||||||
setWidget(widget);
|
setWidget(widget);
|
||||||
setContext(Core::Context(Constants::C_QMLDESIGNER, Constants::C_QT_QUICK_TOOLS_MENU));
|
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);
|
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;
|
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";
|
const char C_DUPLICATE[] = "QmlDesigner.Duplicate";
|
||||||
|
|
||||||
// Context
|
// Context
|
||||||
const char C_QMLDESIGNER[] = "QmlDesigner::QmlDesignerMain";
|
const char C_QMLDESIGNER[] = "QmlDesigner::QmlDesignerMain";
|
||||||
const char C_QMLFORMEDITOR[] = "QmlDesigner::FormEditor";
|
const char C_QMLFORMEDITOR[] = "QmlDesigner::FormEditor";
|
||||||
const char C_QMLEDITOR3D[] = "QmlDesigner::Editor3D";
|
const char C_QMLEDITOR3D[] = "QmlDesigner::Editor3D";
|
||||||
const char C_QMLEFFECTMAKER[] = "QmlDesigner::EffectMaker";
|
const char C_QMLEFFECTMAKER[] = "QmlDesigner::EffectMaker";
|
||||||
const char C_QMLNAVIGATOR[] = "QmlDesigner::Navigator";
|
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";
|
||||||
|
Reference in New Issue
Block a user