forked from qt-creator/qt-creator
QmlDesigner: Remove Model Editor
Task-number: QDS-12808 Change-Id: I9d1a716cda8d4a972b335270961ee7b489ed07c3 Reviewed-by: Qt CI Patch Build Bot <ci_patchbuild_bot@qt.io> Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io>
This commit is contained in:
@@ -1,221 +0,0 @@
|
||||
// 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 CollectionDetails 1.0 as CollectionDetails
|
||||
import StudioControls 1.0 as StudioControls
|
||||
import StudioHelpers as StudioHelpers
|
||||
import StudioTheme 1.0 as StudioTheme
|
||||
import QtQuick.Templates as T
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var columnType
|
||||
|
||||
TableView.onCommit: {
|
||||
if (editorLoader.changesAccepted && edit !== editorLoader.acceptedValue)
|
||||
edit = editorLoader.acceptedValue
|
||||
}
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if (root.activeFocus && !editorLoader.triggered && editorLoader.item) {
|
||||
editorLoader.triggered = true
|
||||
editorLoader.item.open()
|
||||
}
|
||||
|
||||
// active focus should be checked again, because it might be affected by editorLoader.item
|
||||
if (root.activeFocus && editorLoader.editor)
|
||||
editorLoader.editor.forceActiveFocus()
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: editorLoader
|
||||
|
||||
active: true
|
||||
|
||||
property var editor: editorLoader.item ? editorLoader.item.editor : null
|
||||
property var editValue: editorLoader.editor ? editorLoader.editor.editValue : null
|
||||
property var acceptedValue: null
|
||||
property bool changesAccepted: true
|
||||
property bool triggered: false
|
||||
|
||||
Connections {
|
||||
id: modifierFocusConnection
|
||||
|
||||
target: editorLoader.editor
|
||||
enabled: editorLoader.item !== undefined
|
||||
|
||||
function onActiveFocusChanged() {
|
||||
if (!modifierFocusConnection.target.activeFocus) {
|
||||
editorLoader.acceptedValue = editorLoader.editValue
|
||||
root.TableView.commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: textEditor
|
||||
|
||||
EditorPopup {
|
||||
editor: textField
|
||||
|
||||
StudioControls.TextField {
|
||||
id: textField
|
||||
|
||||
property alias editValue: textField.text
|
||||
|
||||
actionIndicator.visible: false
|
||||
translationIndicatorVisible: false
|
||||
|
||||
onRejected: editorLoader.changesAccepted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: realEditor
|
||||
|
||||
EditorPopup {
|
||||
|
||||
editor: realField
|
||||
|
||||
StudioControls.RealSpinBox {
|
||||
id: realField
|
||||
|
||||
property alias editValue: realField.realValue
|
||||
|
||||
actionIndicator.visible: false
|
||||
realFrom: -9e9
|
||||
realTo: 9e9
|
||||
realStepSize: 1.0
|
||||
decimals: 6
|
||||
trailingZeroes: false
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if (realField.activeFocus)
|
||||
realField.contentItem.focus = true
|
||||
}
|
||||
|
||||
textFromValue: function (value, locale) {
|
||||
locale.numberOptions = Locale.OmitGroupSeparator
|
||||
var decimals = realField.trailingZeroes ? realField.decimals : decimalCounter(realField.realValue)
|
||||
if (decimals > 0) {
|
||||
var text = Number(realField.realValue).toLocaleString(locale, 'f', decimals + 1)
|
||||
return text.substring(0, text.length - 1)
|
||||
}
|
||||
return Number(realField.realValue).toLocaleString(locale, 'f', decimals)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: integerEditor
|
||||
|
||||
EditorPopup {
|
||||
|
||||
editor: integerField
|
||||
|
||||
StudioControls.SpinBox {
|
||||
id: integerField
|
||||
|
||||
property alias editValue: integerField.value
|
||||
|
||||
actionIndicatorVisible: false
|
||||
spinBoxIndicatorVisible: true
|
||||
from: -2147483647
|
||||
to: 2147483647
|
||||
decimals: 0
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if (integerField.activeFocus)
|
||||
integerField.contentItem.focus = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component EditorPopup: T.Popup {
|
||||
id: editorPopup
|
||||
|
||||
required property Item editor
|
||||
|
||||
implicitHeight: contentHeight
|
||||
implicitWidth: contentWidth
|
||||
|
||||
focus: true
|
||||
visible: false
|
||||
|
||||
Connections {
|
||||
target: editorPopup.editor
|
||||
|
||||
function onActiveFocusChanged() {
|
||||
if (!editorPopup.editor.activeFocus)
|
||||
editorPopup.close()
|
||||
else if (edit)
|
||||
editorPopup.editor.editValue = edit
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: editorPopup.editor.Keys
|
||||
|
||||
function onEscapePressed() {
|
||||
editorLoader.changesAccepted = false
|
||||
editorPopup.close()
|
||||
}
|
||||
|
||||
function onReturnPressed() {
|
||||
editorPopup.close()
|
||||
}
|
||||
|
||||
function onEnterPressed() {
|
||||
editorPopup.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "default"
|
||||
when: columnType !== CollectionDetails.DataType.Boolean
|
||||
&& columnType !== CollectionDetails.DataType.Color
|
||||
&& columnType !== CollectionDetails.DataType.Integer
|
||||
&& columnType !== CollectionDetails.DataType.Real
|
||||
|
||||
PropertyChanges {
|
||||
target: editorLoader
|
||||
sourceComponent: textEditor
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "integer"
|
||||
when: columnType === CollectionDetails.DataType.Integer
|
||||
|
||||
PropertyChanges {
|
||||
target: editorLoader
|
||||
sourceComponent: integerEditor
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "real"
|
||||
when: columnType === CollectionDetails.DataType.Real
|
||||
|
||||
PropertyChanges {
|
||||
target: editorLoader
|
||||
sourceComponent: realEditor
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "color"
|
||||
when: columnType === CollectionDetails.DataType.Color
|
||||
|
||||
PropertyChanges {
|
||||
target: editorLoader
|
||||
sourceComponent: null
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@@ -1,291 +0,0 @@
|
||||
// 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 QtQuick.Layouts
|
||||
import Qt.labs.platform as PlatformWidgets
|
||||
import HelperWidgets 2.0 as HelperWidgets
|
||||
import StudioControls 1.0 as StudioControls
|
||||
import StudioTheme 1.0 as StudioTheme
|
||||
import CollectionDetails
|
||||
import CollectionEditorBackend
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
required property var model
|
||||
required property var backend
|
||||
property int selectedRow: -1
|
||||
|
||||
implicitHeight: StudioTheme.Values.toolbarHeight
|
||||
color: StudioTheme.Values.themeToolbarBackground
|
||||
|
||||
function addNewColumn() {
|
||||
addColumnDialog.popUp(root.model.columnCount())
|
||||
}
|
||||
|
||||
function addNewRow() {
|
||||
root.model.insertRow(root.model.rowCount())
|
||||
}
|
||||
|
||||
function closeDialogs() {
|
||||
addColumnDialog.reject()
|
||||
fileDialog.reject()
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: container
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: StudioTheme.Values.toolbarVerticalMargin
|
||||
anchors.bottomMargin: StudioTheme.Values.toolbarVerticalMargin
|
||||
|
||||
spacing: StudioTheme.Values.sectionRowSpacing
|
||||
|
||||
RowLayout {
|
||||
id: leftSideToolbar
|
||||
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
||||
Layout.leftMargin: StudioTheme.Values.toolbarHorizontalMargin
|
||||
spacing: StudioTheme.Values.sectionRowSpacing
|
||||
|
||||
IconButton {
|
||||
id: addColumnLeftButton
|
||||
|
||||
buttonIcon: StudioTheme.Constants.addcolumnleft_medium
|
||||
tooltip: qsTr("Add column left")
|
||||
enabled: root.model.selectedColumn > -1
|
||||
onClicked: addColumnDialog.popUp(root.model.selectedColumn)
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: addColumnRightButton
|
||||
|
||||
buttonIcon: StudioTheme.Constants.addcolumnright_medium
|
||||
tooltip: qsTr("Add column right")
|
||||
enabled: root.model.selectedColumn > -1
|
||||
onClicked: addColumnDialog.popUp(root.model.selectedColumn + 1)
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: deleteColumnButton
|
||||
|
||||
buttonIcon: StudioTheme.Constants.deletecolumn_medium
|
||||
tooltip: qsTr("Delete selected column")
|
||||
enabled: root.model.selectedColumn > -1
|
||||
onClicked: root.model.removeColumn(root.model.selectedColumn)
|
||||
}
|
||||
|
||||
Item { // spacer
|
||||
implicitWidth: StudioTheme.Values.toolbarSpacing
|
||||
implicitHeight: 1
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: addRowBelowButton
|
||||
|
||||
buttonIcon: StudioTheme.Constants.addrowbelow_medium
|
||||
tooltip: qsTr("Add row below")
|
||||
enabled: root.model.selectedRow > -1
|
||||
onClicked: root.model.insertRow(root.model.selectedRow + 1)
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: addRowAboveButton
|
||||
|
||||
buttonIcon: StudioTheme.Constants.addrowabove_medium
|
||||
tooltip: qsTr("Add row above")
|
||||
enabled: root.model.selectedRow > -1
|
||||
onClicked: root.model.insertRow(root.model.selectedRow)
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: deleteSelectedRowButton
|
||||
|
||||
buttonIcon: StudioTheme.Constants.deleterow_medium
|
||||
tooltip: qsTr("Delete selected row")
|
||||
enabled: root.model.selectedRow > -1
|
||||
onClicked: root.model.removeRow(root.model.selectedRow)
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rightSideToolbar
|
||||
|
||||
spacing: StudioTheme.Values.sectionRowSpacing
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
Layout.rightMargin: StudioTheme.Values.toolbarHorizontalMargin
|
||||
|
||||
IconButton {
|
||||
id: saveCollectionButton
|
||||
|
||||
buttonIcon: StudioTheme.Constants.save_medium
|
||||
tooltip: qsTr("Save changes")
|
||||
enabled: root.model.collectionName !== "" && root.model.hasUnsavedChanges
|
||||
onClicked: root.model.saveDataStoreCollections()
|
||||
|
||||
Rectangle {
|
||||
width: StudioTheme.Values.smallStatusIndicatorDiameter
|
||||
height: StudioTheme.Values.smallStatusIndicatorDiameter
|
||||
radius: StudioTheme.Values.smallStatusIndicatorDiameter / 2
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
visible: root.model.hasUnsavedChanges
|
||||
color: StudioTheme.Values.themeIconColorSelected
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: exportCollectionButton
|
||||
|
||||
buttonIcon: StudioTheme.Constants.export_medium
|
||||
tooltip: qsTr("Export model")
|
||||
enabled: root.model.collectionName !== ""
|
||||
onClicked: fileDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PlatformWidgets.FileDialog {
|
||||
id: fileDialog
|
||||
|
||||
fileMode: PlatformWidgets.FileDialog.SaveFile
|
||||
|
||||
nameFilters: ["JSON Files (*.json)",
|
||||
"Comma-Separated Values (*.csv)"
|
||||
]
|
||||
|
||||
selectedNameFilter.index: 0
|
||||
|
||||
onAccepted: {
|
||||
let filePath = fileDialog.file.toString()
|
||||
root.model.exportCollection(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
component IconButton: HelperWidgets.AbstractButton {
|
||||
style: StudioTheme.Values.viewBarButtonStyle
|
||||
}
|
||||
|
||||
component Spacer: Item {
|
||||
implicitWidth: 1
|
||||
implicitHeight: StudioTheme.Values.columnGap
|
||||
}
|
||||
|
||||
RegularExpressionValidator {
|
||||
id: nameValidator
|
||||
regularExpression: /^\w+$/
|
||||
}
|
||||
|
||||
StudioControls.Dialog {
|
||||
id: addColumnDialog
|
||||
|
||||
property int clickedIndex: -1
|
||||
property bool nameIsValid
|
||||
|
||||
title: qsTr("Add Column")
|
||||
|
||||
function popUp(index)
|
||||
{
|
||||
addColumnDialog.clickedIndex = index
|
||||
columnName.text = ""
|
||||
columnName.forceActiveFocus()
|
||||
addedPropertyType.currentIndex = addedPropertyType.find("String")
|
||||
|
||||
addColumnDialog.open()
|
||||
}
|
||||
|
||||
function addColumnName() {
|
||||
if (addColumnDialog.nameIsValid) {
|
||||
root.model.addColumn(addColumnDialog.clickedIndex, columnName.text, addedPropertyType.currentText)
|
||||
addColumnDialog.accept()
|
||||
} else {
|
||||
addColumnDialog.reject()
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
text: qsTr("Column name:")
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
}
|
||||
|
||||
StudioControls.TextField {
|
||||
id: columnName
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
actionIndicator.visible: false
|
||||
translationIndicator.visible: false
|
||||
validator: nameValidator
|
||||
|
||||
Keys.onEnterPressed: addColumnDialog.addColumnName()
|
||||
Keys.onReturnPressed: addColumnDialog.addColumnName()
|
||||
Keys.onEscapePressed: addColumnDialog.reject()
|
||||
|
||||
onTextChanged: {
|
||||
addColumnDialog.nameIsValid = (columnName.text !== ""
|
||||
&& !root.model.isPropertyAvailable(columnName.text))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer { implicitHeight: StudioTheme.Values.controlLabelGap }
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: qsTr("The model already contains \"%1\"!").arg(columnName.text)
|
||||
visible: columnName.text !== "" && !addColumnDialog.nameIsValid
|
||||
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
wrapMode: Label.WordWrap
|
||||
padding: 5
|
||||
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
border.width: StudioTheme.Values.border
|
||||
border.color: StudioTheme.Values.themeWarning
|
||||
}
|
||||
}
|
||||
|
||||
Spacer {}
|
||||
|
||||
Text {
|
||||
text: qsTr("Type:")
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
}
|
||||
|
||||
StudioControls.ComboBox {
|
||||
id: addedPropertyType
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
model: CollectionDataTypeModel{}
|
||||
textRole: "display"
|
||||
tooltipRole: "toolTip"
|
||||
actionIndicatorVisible: false
|
||||
}
|
||||
|
||||
Spacer {}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
spacing: StudioTheme.Values.sectionRowSpacing
|
||||
|
||||
HelperWidgets.Button {
|
||||
enabled: addColumnDialog.nameIsValid
|
||||
text: qsTr("Add")
|
||||
onClicked: addColumnDialog.addColumnName()
|
||||
}
|
||||
|
||||
HelperWidgets.Button {
|
||||
text: qsTr("Cancel")
|
||||
onClicked: addColumnDialog.reject()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,813 +0,0 @@
|
||||
// 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 QtQuick.Layouts
|
||||
import CollectionDetails 1.0 as CollectionDetails
|
||||
import HelperWidgets 2.0 as HelperWidgets
|
||||
import StudioControls 1.0 as StudioControls
|
||||
import StudioTheme 1.0 as StudioTheme
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
required property var model
|
||||
required property var backend
|
||||
required property var sortedModel
|
||||
|
||||
implicitWidth: 300
|
||||
implicitHeight: 400
|
||||
color: StudioTheme.Values.themeControlBackground
|
||||
|
||||
function closeDialogs() {
|
||||
editPropertyDialog.reject()
|
||||
deleteColumnDialog.reject()
|
||||
toolbar.closeDialogs()
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: tableView.model.deselectAll()
|
||||
}
|
||||
|
||||
Column {
|
||||
id: topRow
|
||||
readonly property real maxAvailableHeight: root.height
|
||||
|
||||
visible: root.model.collectionName !== ""
|
||||
width: parent.width
|
||||
spacing: 10
|
||||
|
||||
CollectionDetailsToolbar {
|
||||
id: toolbar
|
||||
model: root.model
|
||||
backend: root.backend
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
id: gridLayout
|
||||
readonly property real maxAvailableHeight: topRow.maxAvailableHeight
|
||||
- topRow.spacing
|
||||
- toolbar.height
|
||||
|
||||
columns: 3
|
||||
rowSpacing: 1
|
||||
columnSpacing: 1
|
||||
width: parent.width
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: StudioTheme.Values.collectionTableHorizontalMargin
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: tableTopLeftCorner
|
||||
|
||||
clip: true
|
||||
visible: !tableView.model.isEmpty
|
||||
color: StudioTheme.Values.themeControlBackgroundInteraction
|
||||
border.color: StudioTheme.Values.themeControlBackgroundInteraction
|
||||
border.width: 2
|
||||
|
||||
Layout.preferredWidth: rowIdView.width
|
||||
Layout.preferredHeight: headerView.height
|
||||
Layout.minimumWidth: rowIdView.width
|
||||
Layout.minimumHeight: headerView.height
|
||||
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
font: headerTextMetrics.font
|
||||
text: "#"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton
|
||||
property int order: Qt.AscendingOrder
|
||||
onClicked: {
|
||||
order = (order == Qt.AscendingOrder) ? Qt.DescendingOrder : Qt.AscendingOrder;
|
||||
tableView.closeEditors()
|
||||
tableView.model.sort(-1, order)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalHeaderView {
|
||||
id: headerView
|
||||
|
||||
property real topPadding: 5
|
||||
property real bottomPadding: 5
|
||||
|
||||
Layout.preferredHeight: headerTextMetrics.height + topPadding + bottomPadding
|
||||
Layout.columnSpan: 2
|
||||
syncView: tableView
|
||||
clip: true
|
||||
|
||||
delegate: HeaderDelegate {
|
||||
selectedItem: tableView.model.selectedColumn
|
||||
color: StudioTheme.Values.themeControlBackgroundInteraction
|
||||
|
||||
MouseArea {
|
||||
id: topHeaderMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: StudioTheme.Values.borderHover
|
||||
anchors.rightMargin: StudioTheme.Values.borderHover
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
hoverEnabled: true
|
||||
onClicked: (mouse) => {
|
||||
tableView.model.selectColumn(index)
|
||||
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
let posX = index === root.model.columnCount() - 1 ? parent.width - editPropertyDialog.width : 0
|
||||
|
||||
headerMenu.clickedHeaderIndex = index
|
||||
headerMenu.dialogPos = parent.mapToGlobal(posX, parent.height)
|
||||
headerMenu.popup()
|
||||
} else {
|
||||
headerMenu.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ToolTip {
|
||||
id: topHeaderToolTip
|
||||
|
||||
property bool expectedToBeShown: topHeaderMouseArea.containsMouse
|
||||
visible: expectedToBeShown && text !== ""
|
||||
delay: 1000
|
||||
|
||||
onExpectedToBeShownChanged: {
|
||||
if (expectedToBeShown)
|
||||
text = root.model.propertyType(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StudioControls.Menu {
|
||||
id: headerMenu
|
||||
|
||||
property int clickedHeaderIndex: -1
|
||||
property point dialogPos
|
||||
|
||||
onClosed: {
|
||||
headerMenu.clickedHeaderIndex = -1
|
||||
}
|
||||
|
||||
StudioControls.MenuItem {
|
||||
text: qsTr("Edit")
|
||||
onTriggered: editPropertyDialog.openDialog(headerMenu.clickedHeaderIndex,
|
||||
headerMenu.dialogPos)
|
||||
}
|
||||
|
||||
StudioControls.MenuItem {
|
||||
text: qsTr("Delete")
|
||||
onTriggered: deleteColumnDialog.popUp(headerMenu.clickedHeaderIndex)
|
||||
}
|
||||
|
||||
StudioControls.MenuItem {
|
||||
text: qsTr("Sort Ascending")
|
||||
onTriggered: {
|
||||
tableView.closeEditors()
|
||||
tableView.model.sort(headerMenu.clickedHeaderIndex, Qt.AscendingOrder)
|
||||
}
|
||||
}
|
||||
|
||||
StudioControls.MenuItem {
|
||||
text: qsTr("Sort Descending")
|
||||
onTriggered: {
|
||||
tableView.closeEditors()
|
||||
tableView.model.sort(headerMenu.clickedHeaderIndex, Qt.DescendingOrder)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VerticalHeaderView {
|
||||
id: rowIdView
|
||||
|
||||
syncView: tableView
|
||||
clip: true
|
||||
|
||||
Layout.preferredHeight: tableView.height
|
||||
Layout.rowSpan: 2
|
||||
Layout.alignment: Qt.AlignTop + Qt.AlignLeft
|
||||
width: implicitWidth // suppresses GridLayout warnings when resizing
|
||||
|
||||
delegate: HeaderDelegate {
|
||||
selectedItem: tableView.model.selectedRow
|
||||
color: StudioTheme.Values.themeControlBackgroundHover
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: StudioTheme.Values.borderHover
|
||||
anchors.bottomMargin: StudioTheme.Values.borderHover
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onClicked: tableView.model.selectRow(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TableView {
|
||||
id: tableView
|
||||
|
||||
model: root.sortedModel
|
||||
clip: true
|
||||
|
||||
readonly property real maxAvailableHeight: gridLayout.maxAvailableHeight
|
||||
- addRowButton.height
|
||||
- headerView.height
|
||||
- (2 * gridLayout.rowSpacing)
|
||||
readonly property real maxAvailableWidth: gridLayout.width
|
||||
- StudioTheme.Values.collectionTableHorizontalMargin
|
||||
- rowIdView.width
|
||||
- addColumnButton.width
|
||||
- gridLayout.columnSpacing
|
||||
|
||||
property real childrenWidth: tableView.contentItem.childrenRect.width
|
||||
property real childrenHeight: tableView.contentItem.childrenRect.height
|
||||
|
||||
property int targetRow
|
||||
property int targetColumn
|
||||
|
||||
property Item popupEditingItem
|
||||
|
||||
Layout.alignment: Qt.AlignTop + Qt.AlignLeft
|
||||
Layout.preferredWidth: tableView.contentWidth
|
||||
Layout.preferredHeight: tableView.contentHeight
|
||||
Layout.minimumWidth: 100
|
||||
Layout.minimumHeight: 20
|
||||
Layout.maximumWidth: maxAvailableWidth
|
||||
Layout.maximumHeight: maxAvailableHeight
|
||||
|
||||
columnWidthProvider: function(column) {
|
||||
if (!isColumnLoaded(column))
|
||||
return -1
|
||||
let w = explicitColumnWidth(column)
|
||||
if (w < 0)
|
||||
w = implicitColumnWidth(column)
|
||||
return Math.max(w, StudioTheme.Values.collectionCellMinimumWidth)
|
||||
}
|
||||
|
||||
rowHeightProvider: function(row) {
|
||||
if (!isRowLoaded(row))
|
||||
return -1
|
||||
let h = explicitRowHeight(row)
|
||||
if (h < 0)
|
||||
h = implicitRowHeight(row)
|
||||
return Math.max(h, StudioTheme.Values.collectionCellMinimumHeight)
|
||||
}
|
||||
|
||||
function closePopupEditor() {
|
||||
if (tableView.popupEditingItem)
|
||||
tableView.popupEditingItem.closeEditor()
|
||||
tableView.popupEditingItem = null
|
||||
}
|
||||
|
||||
function openNewPopupEditor(item, editor) {
|
||||
if (tableView.popupEditingItem !== item) {
|
||||
closePopupEditor()
|
||||
tableView.popupEditingItem = item
|
||||
}
|
||||
}
|
||||
|
||||
function closeEditors() {
|
||||
closeEditor()
|
||||
closePopupEditor()
|
||||
}
|
||||
|
||||
function ensureRowIsVisible(row) {
|
||||
let rows = tableView.model.rowCount()
|
||||
let rowIsLoaded = tableView.isRowLoaded(row)
|
||||
|
||||
if (row < 0 || row >= rows || rowIsLoaded) {
|
||||
if (rowIsLoaded)
|
||||
tableView.positionViewAtRow(row, Qt.AlignLeft | Qt.AlignTop)
|
||||
|
||||
tableView.targetRow = -1
|
||||
return
|
||||
}
|
||||
|
||||
tableView.targetRow = row
|
||||
tableView.positionViewAtRow(row, Qt.AlignLeft | Qt.AlignTop)
|
||||
ensureTimer.start()
|
||||
}
|
||||
|
||||
function ensureColumnIsVisible(column) {
|
||||
let columns = tableView.model.columnCount()
|
||||
let columnIsLoaded = tableView.isColumnLoaded(column)
|
||||
|
||||
if (column < 0 || column >= columns || columnIsLoaded) {
|
||||
if (columnIsLoaded)
|
||||
tableView.positionViewAtColumn(column, Qt.AlignLeft | Qt.AlignTop)
|
||||
|
||||
tableView.targetColumn = -1
|
||||
return
|
||||
}
|
||||
|
||||
tableView.targetColumn = column
|
||||
tableView.positionViewAtColumn(column, Qt.AlignLeft | Qt.AlignTop)
|
||||
ensureTimer.start()
|
||||
}
|
||||
|
||||
onMaxAvailableHeightChanged: resetSizeTimer.start()
|
||||
onMaxAvailableWidthChanged: resetSizeTimer.start()
|
||||
onChildrenWidthChanged: resetSizeTimer.start()
|
||||
onChildrenHeightChanged: resetSizeTimer.start()
|
||||
|
||||
delegate: Rectangle {
|
||||
id: itemCell
|
||||
|
||||
clip: true
|
||||
implicitWidth: 100
|
||||
implicitHeight: StudioTheme.Values.baseHeight
|
||||
color: itemSelected ? StudioTheme.Values.themeControlBackgroundInteraction
|
||||
: StudioTheme.Values.themeControlBackground
|
||||
border.width: 1
|
||||
border.color: {
|
||||
if (dataTypeWarning !== CollectionDetails.Warning.None)
|
||||
return StudioTheme.Values.themeWarning
|
||||
|
||||
if (itemSelected)
|
||||
return StudioTheme.Values.themeControlOutlineInteraction
|
||||
|
||||
return StudioTheme.Values.themeControlBackgroundInteraction
|
||||
}
|
||||
|
||||
HelperWidgets.ToolTipArea {
|
||||
anchors.fill: parent
|
||||
text: root.model.warningToString(dataTypeWarning)
|
||||
enabled: dataTypeWarning !== CollectionDetails.Warning.None && text !== ""
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: (mouse) => {
|
||||
let row = index % tableView.model.rowCount()
|
||||
|
||||
tableView.model.selectRow(row)
|
||||
cellContextMenu.showMenu(row)
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: cellContentLoader
|
||||
|
||||
property int cellColumnType: columnType ? columnType : 0
|
||||
|
||||
Component {
|
||||
id: cellText
|
||||
|
||||
Text {
|
||||
text: display ?? ""
|
||||
color: itemSelected ? StudioTheme.Values.themeInteraction
|
||||
: StudioTheme.Values.themeTextColor
|
||||
leftPadding: 5
|
||||
topPadding: 3
|
||||
bottomPadding: 3
|
||||
font.pixelSize: StudioTheme.Values.baseFontSize
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: checkBoxComponent
|
||||
|
||||
StudioControls.CheckBox {
|
||||
id: checkBoxDelegate
|
||||
|
||||
readonly property bool editValue: edit
|
||||
|
||||
text: ""
|
||||
actionIndicatorVisible: false
|
||||
checked: checkBoxDelegate.editValue
|
||||
onCheckedChanged: {
|
||||
if (checkBoxDelegate.editValue !== checkBoxDelegate.checked)
|
||||
edit = checkBoxDelegate.checked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: colorEditorComponent
|
||||
|
||||
ColorViewDelegate {
|
||||
id: colorEditorItem
|
||||
|
||||
readonly property color editValue: edit
|
||||
readonly property color displayValue: display
|
||||
property string _frontColorStr
|
||||
property string _backendColorStr
|
||||
|
||||
actionIndicatorVisible: false
|
||||
|
||||
onColorChanged: {
|
||||
_frontColorStr = colorEditorItem.color.toString()
|
||||
if (_frontColorStr != _backendColorStr)
|
||||
edit = colorEditorItem.color
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
colorEditorItem.color = display
|
||||
}
|
||||
|
||||
onDisplayValueChanged: {
|
||||
_backendColorStr = colorEditorItem.displayValue.toString()
|
||||
if (_frontColorStr != _backendColorStr)
|
||||
colorEditorItem.color = colorEditorItem.displayValue
|
||||
}
|
||||
|
||||
onEditorOpened: (item, editor) => {
|
||||
tableView.openNewPopupEditor(item, editor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetSource() {
|
||||
if (columnType === CollectionDetails.DataType.Color)
|
||||
cellContentLoader.sourceComponent = colorEditorComponent
|
||||
else if (columnType === CollectionDetails.DataType.Boolean)
|
||||
cellContentLoader.sourceComponent = checkBoxComponent
|
||||
else
|
||||
cellContentLoader.sourceComponent = cellText
|
||||
}
|
||||
|
||||
Component.onCompleted: resetSource()
|
||||
onCellColumnTypeChanged: resetSource()
|
||||
}
|
||||
|
||||
TableView.editDelegate: CollectionDetailsEditDelegate {
|
||||
anchors {
|
||||
top: itemCell.top
|
||||
left: itemCell.left
|
||||
}
|
||||
Component.onCompleted: tableView.model.deselectAll()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: resetSizeTimer
|
||||
|
||||
interval: 100
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
let cWidth = Math.min(tableView.maxAvailableWidth, tableView.childrenWidth)
|
||||
let cHeight = Math.min(tableView.maxAvailableHeight, tableView.childrenHeight)
|
||||
|
||||
if (tableView.contentWidth !== cWidth || tableView.contentHeight !== cHeight)
|
||||
tableView.returnToBounds()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: ensureTimer
|
||||
|
||||
interval: 100
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
tableView.ensureRowIsVisible(tableView.targetRow)
|
||||
tableView.ensureColumnIsVisible(tableView.targetColumn)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: tableView.model
|
||||
|
||||
function onModelReset() {
|
||||
root.closeDialogs()
|
||||
tableView.clearColumnWidths()
|
||||
tableView.clearRowHeights()
|
||||
}
|
||||
|
||||
function onRowsInserted(parent, first, last) {
|
||||
tableView.closeEditors()
|
||||
tableView.model.selectRow(first)
|
||||
tableView.ensureRowIsVisible(first)
|
||||
}
|
||||
|
||||
function onColumnsInserted(parent, first, last) {
|
||||
tableView.closeEditors()
|
||||
tableView.model.selectColumn(first)
|
||||
tableView.ensureColumnIsVisible(first)
|
||||
}
|
||||
|
||||
function onRowsRemoved(parent, first, last) {
|
||||
let nextRow = first - 1
|
||||
if (nextRow < 0 && tableView.model.rowCount(parent) > 0)
|
||||
nextRow = 0
|
||||
|
||||
tableView.model.selectRow(nextRow)
|
||||
}
|
||||
|
||||
function onColumnsRemoved(parent, first, last) {
|
||||
let nextColumn = first - 1
|
||||
if (nextColumn < 0 && tableView.model.columnCount(parent) > 0)
|
||||
nextColumn = 0
|
||||
|
||||
tableView.model.selectColumn(nextColumn)
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler { id: hoverHandler }
|
||||
|
||||
ScrollBar.horizontal: StudioControls.TransientScrollBar {
|
||||
id: horizontalScrollBar
|
||||
style: StudioTheme.Values.viewStyle
|
||||
orientation: Qt.Horizontal
|
||||
|
||||
show: (hoverHandler.hovered || tableView.focus || horizontalScrollBar.inUse)
|
||||
&& horizontalScrollBar.isNeeded
|
||||
}
|
||||
|
||||
ScrollBar.vertical: StudioControls.TransientScrollBar {
|
||||
id: verticalScrollBar
|
||||
style: StudioTheme.Values.viewStyle
|
||||
orientation: Qt.Vertical
|
||||
|
||||
show: (hoverHandler.hovered || tableView.focus || verticalScrollBar.inUse)
|
||||
&& verticalScrollBar.isNeeded
|
||||
}
|
||||
}
|
||||
|
||||
HelperWidgets.IconButton {
|
||||
id: addColumnButton
|
||||
|
||||
iconSize:16
|
||||
Layout.preferredWidth: 24
|
||||
Layout.preferredHeight: tableView.height
|
||||
Layout.minimumHeight: 24
|
||||
Layout.alignment: Qt.AlignLeft + Qt.AlignVCenter
|
||||
|
||||
icon: StudioTheme.Constants.create_medium
|
||||
tooltip: "Add Column"
|
||||
|
||||
onClicked: {
|
||||
tableView.closeEditors()
|
||||
toolbar.addNewColumn()
|
||||
}
|
||||
}
|
||||
|
||||
HelperWidgets.IconButton {
|
||||
id: addRowButton
|
||||
|
||||
iconSize:16
|
||||
Layout.preferredWidth: tableView.width
|
||||
Layout.preferredHeight: 24
|
||||
Layout.minimumWidth: 24
|
||||
Layout.alignment: Qt.AlignTop + Qt.AlignHCenter
|
||||
|
||||
icon: StudioTheme.Constants.create_medium
|
||||
tooltip: "Add Row"
|
||||
|
||||
onClicked: {
|
||||
tableView.closeEditors()
|
||||
toolbar.addNewRow()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: importsProblem
|
||||
|
||||
visible: !topRow.visible && rootView.dataStoreExists && !rootView.projectImportExists
|
||||
width: parent.width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
clip: true
|
||||
|
||||
Text {
|
||||
text: qsTr("Import the project to your design document to make the Model Editor enabled.")
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.maximumWidth: parent.width
|
||||
leftPadding: StudioTheme.Values.collectionItemTextPadding
|
||||
rightPadding: StudioTheme.Values.collectionItemTextPadding
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
font.pixelSize: StudioTheme.Values.mediumFontSize
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
HelperWidgets.Button {
|
||||
text: qsTr("Enable DataStore (This will add the required import)")
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
onClicked: rootView.addProjectImport()
|
||||
leftPadding: StudioTheme.Values.collectionItemTextPadding
|
||||
rightPadding: StudioTheme.Values.collectionItemTextPadding
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("There are no models in this project.\nAdd or import a model.")
|
||||
visible: !topRow.visible && !importsProblem.visible
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
font.pixelSize: StudioTheme.Values.mediumFontSize
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: headerTextMetrics
|
||||
|
||||
font.pixelSize: StudioTheme.Values.baseFontSize
|
||||
text: "Xq"
|
||||
}
|
||||
|
||||
StudioControls.Menu {
|
||||
id: cellContextMenu
|
||||
|
||||
property int rowIndex: -1
|
||||
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
function showMenu(rowIndex) {
|
||||
cellContextMenu.rowIndex = rowIndex
|
||||
cellContextMenu.popup()
|
||||
}
|
||||
|
||||
CellContextMenuItem {
|
||||
id: addRowAboveCellMenuItem
|
||||
|
||||
itemText: qsTr("Add row above")
|
||||
itemIcon: StudioTheme.Constants.addrowabove_medium
|
||||
onTriggered: root.model.insertRow(cellContextMenu.rowIndex)
|
||||
}
|
||||
CellContextMenuItem {
|
||||
id: addRowBelowCellMenuItem
|
||||
|
||||
itemText: qsTr("Add row below")
|
||||
itemIcon: StudioTheme.Constants.addrowbelow_medium
|
||||
onTriggered: root.model.insertRow(cellContextMenu.rowIndex + 1)
|
||||
}
|
||||
CellContextMenuItem {
|
||||
id: deleteRowCellMenuItem
|
||||
|
||||
itemText: qsTr("Delete row")
|
||||
itemIcon: StudioTheme.Constants.deleterow_medium
|
||||
onTriggered: root.model.removeRows(cellContextMenu.rowIndex, 1)
|
||||
}
|
||||
}
|
||||
|
||||
component HeaderDelegate: Rectangle {
|
||||
id: headerItem
|
||||
|
||||
required property int selectedItem
|
||||
property alias horizontalAlignment: headerText.horizontalAlignment
|
||||
property alias verticalAlignment: headerText.verticalAlignment
|
||||
|
||||
implicitWidth: headerText.implicitWidth
|
||||
implicitHeight: headerText.implicitHeight
|
||||
border.width: 1
|
||||
clip: true
|
||||
|
||||
Text {
|
||||
id: headerText
|
||||
|
||||
topPadding: headerView.topPadding
|
||||
bottomPadding: headerView.bottomPadding
|
||||
leftPadding: 5
|
||||
rightPadding: 5
|
||||
text: display
|
||||
font: headerTextMetrics.font
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "default"
|
||||
when: index !== selectedItem
|
||||
PropertyChanges {
|
||||
target: headerItem
|
||||
border.color: StudioTheme.Values.themeControlBackgroundInteraction
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: headerText
|
||||
font.bold: false
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "selected"
|
||||
when: index === selectedItem
|
||||
|
||||
PropertyChanges {
|
||||
target: headerItem
|
||||
border.color: StudioTheme.Values.themeControlBackground
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: headerText
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
component CellContextMenuItem: StudioControls.MenuItem {
|
||||
id: cellContextMenuItemComponent
|
||||
|
||||
property alias itemText: cellContextMenuText.text
|
||||
property alias itemIcon: cellContextMenuIcon.text
|
||||
text: ""
|
||||
|
||||
implicitWidth: cellContextMenuRow.width
|
||||
implicitHeight: cellContextMenuRow.height
|
||||
|
||||
Row {
|
||||
id: cellContextMenuRow
|
||||
|
||||
property color textColor : cellContextMenuItemComponent.enabled
|
||||
? cellContextMenuItemComponent.highlighted
|
||||
? cellContextMenuItemComponent.style.text.selectedText
|
||||
: cellContextMenuItemComponent.style.text.idle
|
||||
: cellContextMenuItemComponent.style.text.disabled
|
||||
|
||||
spacing: 2 * StudioTheme.Values.contextMenuHorizontalPadding
|
||||
height: StudioTheme.Values.defaultControlHeight
|
||||
leftPadding: StudioTheme.Values.contextMenuHorizontalPadding
|
||||
rightPadding: StudioTheme.Values.contextMenuHorizontalPadding
|
||||
|
||||
Text {
|
||||
id: cellContextMenuIcon
|
||||
|
||||
color: cellContextMenuRow.textColor
|
||||
text: StudioTheme.Constants.addrowabove_medium
|
||||
font.family: StudioTheme.Constants.iconFont.family
|
||||
font.pixelSize: StudioTheme.Values.myIconFontSize
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
id: cellContextMenuText
|
||||
|
||||
color: cellContextMenuRow.textColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EditPropertyDialog {
|
||||
id: editPropertyDialog
|
||||
model: root.model
|
||||
}
|
||||
|
||||
StudioControls.Dialog {
|
||||
id: deleteColumnDialog
|
||||
|
||||
property int clickedIndex: -1
|
||||
|
||||
title: qsTr("Delete Column")
|
||||
width: 400
|
||||
|
||||
onAccepted: {
|
||||
root.model.removeColumn(clickedIndex)
|
||||
}
|
||||
|
||||
function popUp(index)
|
||||
{
|
||||
deleteColumnDialog.clickedIndex = index
|
||||
deleteColumnDialog.open()
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: StudioTheme.Values.sectionColumnSpacing
|
||||
|
||||
Text {
|
||||
text: qsTr("Are you sure that you want to delete column \"%1\"?").arg(
|
||||
root.model.headerData(
|
||||
deleteColumnDialog.clickedIndex, Qt.Horizontal))
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
spacing: StudioTheme.Values.sectionRowSpacing
|
||||
|
||||
HelperWidgets.Button {
|
||||
text: qsTr("Delete")
|
||||
onClicked: deleteColumnDialog.accept()
|
||||
}
|
||||
|
||||
HelperWidgets.Button {
|
||||
text: qsTr("Cancel")
|
||||
onClicked: deleteColumnDialog.reject()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,154 +0,0 @@
|
||||
// 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 QtQuick.Layouts
|
||||
import HelperWidgets 2.0 as HelperWidgets
|
||||
import StudioControls 1.0 as StudioControls
|
||||
import StudioTheme as StudioTheme
|
||||
import CollectionEditorBackend
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitWidth: 300
|
||||
implicitHeight: boundingRect.height + 3
|
||||
|
||||
property color textColor
|
||||
readonly property string name: collectionName ?? ""
|
||||
readonly property bool isSelected: collectionIsSelected
|
||||
readonly property int id: index
|
||||
|
||||
function rename(newName) {
|
||||
collectionName = newName
|
||||
}
|
||||
|
||||
signal selectItem(int itemIndex)
|
||||
signal deleteItem()
|
||||
signal contextMenuRequested()
|
||||
|
||||
Item {
|
||||
id: boundingRect
|
||||
|
||||
width: parent.width
|
||||
height: itemLayout.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
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: itemLayout
|
||||
|
||||
width: parent.width
|
||||
|
||||
Text {
|
||||
id: nameHolder
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
||||
Layout.leftMargin: StudioTheme.Values.collectionItemTextSideMargin
|
||||
Layout.topMargin: StudioTheme.Values.collectionItemTextMargin
|
||||
Layout.bottomMargin: StudioTheme.Values.collectionItemTextMargin
|
||||
|
||||
text: collectionName
|
||||
font.pixelSize: StudioTheme.Values.baseFontSize
|
||||
color: root.textColor
|
||||
topPadding: StudioTheme.Values.collectionItemTextPadding
|
||||
bottomPadding: StudioTheme.Values.collectionItemTextPadding
|
||||
elide: Text.ElideMiddle
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
id: threeDots
|
||||
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
Layout.topMargin: StudioTheme.Values.collectionItemTextMargin
|
||||
Layout.bottomMargin: StudioTheme.Values.collectionItemTextMargin
|
||||
Layout.rightMargin: StudioTheme.Values.collectionItemTextSideMargin
|
||||
|
||||
text: StudioTheme.Constants.more_medium
|
||||
font.family: StudioTheme.Constants.iconFont.family
|
||||
font.pixelSize: StudioTheme.Values.baseIconFontSize
|
||||
color: root.textColor
|
||||
padding: StudioTheme.Values.collectionItemTextPadding
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton | Qt.LeftButton
|
||||
onClicked: root.contextMenuRequested()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.themeIconColorSelected
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: root
|
||||
textColor: StudioTheme.Values.themeTextSelectedTextColor
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@@ -1,124 +0,0 @@
|
||||
// Copyright (C) 2024 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 QtQuick.Layouts
|
||||
import HelperWidgets 2.0 as HelperWidgets
|
||||
import StudioControls 1.0 as StudioControls
|
||||
import StudioTheme as StudioTheme
|
||||
import CollectionEditorBackend
|
||||
|
||||
ListView {
|
||||
id: root
|
||||
|
||||
model: CollectionEditorBackend.model
|
||||
clip: true
|
||||
|
||||
function closeDialogs() {
|
||||
currentCollection.dereference()
|
||||
collectionMenu.close()
|
||||
deleteDialog.reject()
|
||||
renameDialog.reject()
|
||||
}
|
||||
|
||||
function deleteCurrentCollection() {
|
||||
deleteDialog.open()
|
||||
}
|
||||
|
||||
delegate: CollectionItem {
|
||||
implicitWidth: root.width
|
||||
onDeleteItem: root.model.removeRow(index)
|
||||
onContextMenuRequested: collectionMenu.openMenu(this)
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: currentCollection
|
||||
|
||||
property CollectionItem item
|
||||
readonly property string name: item ? item.name : ""
|
||||
readonly property bool selected: item ? item.isSelected : false
|
||||
readonly property int index: item ? item.id : -1
|
||||
|
||||
function updateItem() {
|
||||
currentCollection.item = collectionMenu.clickedItem ?? root.itemAtIndex(root.model.selectedIndex)
|
||||
}
|
||||
|
||||
function rename(newName) {
|
||||
if (item)
|
||||
item.rename(newName)
|
||||
}
|
||||
|
||||
function deleteItem() {
|
||||
if (item)
|
||||
item.deleteItem()
|
||||
}
|
||||
|
||||
function dereference() {
|
||||
item = null
|
||||
}
|
||||
}
|
||||
|
||||
StudioControls.Menu {
|
||||
id: collectionMenu
|
||||
|
||||
property CollectionItem clickedItem
|
||||
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
enabled: root.count
|
||||
|
||||
function openMenu(item) {
|
||||
collectionMenu.clickedItem = item
|
||||
currentCollection.updateItem()
|
||||
collectionMenu.popup()
|
||||
}
|
||||
|
||||
onClosed: collectionMenu.clickedItem = null
|
||||
|
||||
Action {
|
||||
id: menuDeleteAction
|
||||
|
||||
text: qsTr("Delete")
|
||||
onTriggered: deleteDialog.open()
|
||||
}
|
||||
|
||||
Action {
|
||||
id: menuRenameAction
|
||||
|
||||
text: qsTr("Rename")
|
||||
onTriggered: renameDialog.open()
|
||||
}
|
||||
|
||||
Action {
|
||||
id: menuAssignAction
|
||||
|
||||
text: qsTr("Assign to the selected node")
|
||||
enabled: CollectionEditorBackend.rootView.targetNodeSelected
|
||||
onTriggered: rootView.assignCollectionToSelectedNode(currentCollection.name)
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmDeleteCollectionDialog {
|
||||
id: deleteDialog
|
||||
|
||||
collectionName: currentCollection.name
|
||||
onAboutToShow: currentCollection.updateItem()
|
||||
onAccepted: currentCollection.deleteItem()
|
||||
}
|
||||
|
||||
RenameCollectionDialog {
|
||||
id: renameDialog
|
||||
|
||||
collectionName: currentCollection.name
|
||||
onAboutToShow: currentCollection.updateItem()
|
||||
onAccepted: currentCollection.rename(renameDialog.newCollectionName)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.model
|
||||
|
||||
function onModelReset() {
|
||||
root.closeDialogs()
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,206 +0,0 @@
|
||||
// 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 QtQuick.Layouts
|
||||
import HelperWidgets as HelperWidgets
|
||||
import StudioTheme as StudioTheme
|
||||
import CollectionEditorBackend
|
||||
|
||||
Item {
|
||||
id: root
|
||||
focus: true
|
||||
|
||||
property var rootView: CollectionEditorBackend.rootView
|
||||
property var model: CollectionEditorBackend.model
|
||||
property var collectionDetailsModel: CollectionEditorBackend.collectionDetailsModel
|
||||
property var collectionDetailsSortFilterModel: CollectionEditorBackend.collectionDetailsSortFilterModel
|
||||
|
||||
function showWarning(title, message) {
|
||||
warningDialog.title = title
|
||||
warningDialog.message = message
|
||||
warningDialog.open()
|
||||
}
|
||||
|
||||
// called from C++ when using the delete key
|
||||
function deleteSelectedCollection() {
|
||||
collectionListView.deleteCurrentCollection()
|
||||
}
|
||||
|
||||
function closeDialogs() {
|
||||
importDialog.reject()
|
||||
newCollection.reject()
|
||||
warningDialog.reject()
|
||||
}
|
||||
|
||||
ImportDialog {
|
||||
id: importDialog
|
||||
|
||||
backendValue: root.rootView
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
NewCollectionDialog {
|
||||
id: newCollection
|
||||
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
Message {
|
||||
id: warningDialog
|
||||
|
||||
title: ""
|
||||
message: ""
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
// Covers the toolbar color on top to prevent the background
|
||||
// color for the margin of splitter
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: topToolbar.height
|
||||
color: topToolbar.color
|
||||
}
|
||||
|
||||
SplitView {
|
||||
id: splitView
|
||||
|
||||
readonly property bool isHorizontal: splitView.orientation === Qt.Horizontal
|
||||
|
||||
orientation: width >= 500 ? Qt.Horizontal : Qt.Vertical
|
||||
anchors.fill: parent
|
||||
|
||||
onOrientationChanged: detailsView.closeDialogs()
|
||||
|
||||
handle: Item {
|
||||
id: handleDelegate
|
||||
|
||||
property color color: SplitHandle.pressed ? StudioTheme.Values.themeControlOutlineInteraction
|
||||
: SplitHandle.hovered ? StudioTheme.Values.themeControlOutlineHover
|
||||
: StudioTheme.Values.themeControlOutline
|
||||
|
||||
implicitWidth: 1
|
||||
implicitHeight: 1
|
||||
|
||||
Rectangle {
|
||||
id: handleRect
|
||||
|
||||
property real verticalMargin: splitView.isHorizontal ? StudioTheme.Values.splitterMargin : 0
|
||||
property real horizontalMargin: splitView.isHorizontal ? 0 : StudioTheme.Values.splitterMargin
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: handleRect.verticalMargin
|
||||
anchors.bottomMargin: handleRect.verticalMargin
|
||||
anchors.leftMargin: handleRect.horizontalMargin
|
||||
anchors.rightMargin: handleRect.horizontalMargin
|
||||
|
||||
color: handleDelegate.color
|
||||
}
|
||||
|
||||
containmentMask: Item {
|
||||
x: splitView.isHorizontal ? ((handleDelegate.width - width) / 2) : 0
|
||||
y: splitView.isHorizontal ? 0 : ((handleDelegate.height - height) / 2)
|
||||
height: splitView.isHorizontal ? handleDelegate.height : StudioTheme.Values.borderHover
|
||||
width: splitView.isHorizontal ? StudioTheme.Values.borderHover : handleDelegate.width
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: collectionsSideBar
|
||||
spacing: 0
|
||||
|
||||
SplitView.minimumWidth: 200
|
||||
SplitView.maximumWidth: 450
|
||||
SplitView.minimumHeight: 200
|
||||
SplitView.maximumHeight: 400
|
||||
SplitView.fillWidth: !splitView.isHorizontal
|
||||
SplitView.fillHeight: splitView.isHorizontal
|
||||
|
||||
Rectangle {
|
||||
id: topToolbar
|
||||
color: StudioTheme.Values.themeToolbarBackground
|
||||
|
||||
Layout.preferredHeight: StudioTheme.Values.toolbarHeight
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: StudioTheme.Values.toolbarHorizontalMargin
|
||||
|
||||
text: qsTr("Data Models")
|
||||
font.pixelSize: StudioTheme.Values.baseFontSize
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
}
|
||||
|
||||
HelperWidgets.AbstractButton {
|
||||
id: importCollectionButton
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: StudioTheme.Values.toolbarHorizontalMargin
|
||||
|
||||
style: StudioTheme.Values.viewBarButtonStyle
|
||||
buttonIcon: StudioTheme.Constants.import_medium
|
||||
tooltip: qsTr("Import a model")
|
||||
|
||||
onClicked: importDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
CollectionListView { // Model Groups
|
||||
id: collectionListView
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: bottomSpacer.isExpanded ? 150 : 0
|
||||
Layout.fillHeight: !bottomSpacer.isExpanded
|
||||
Layout.preferredHeight: contentHeight
|
||||
Layout.maximumHeight: contentHeight
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
|
||||
}
|
||||
|
||||
HelperWidgets.IconButton {
|
||||
id: addCollectionButton
|
||||
|
||||
iconSize: 16
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: 24
|
||||
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
|
||||
|
||||
tooltip: qsTr("Add a new model")
|
||||
icon: StudioTheme.Constants.create_medium
|
||||
onClicked: newCollection.open()
|
||||
}
|
||||
|
||||
Item {
|
||||
id: bottomSpacer
|
||||
|
||||
readonly property bool isExpanded: height > 0
|
||||
Layout.minimumWidth: 1
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
|
||||
CollectionDetailsView {
|
||||
id: detailsView
|
||||
|
||||
model: root.collectionDetailsModel
|
||||
backend: root.model
|
||||
sortedModel: root.collectionDetailsSortFilterModel
|
||||
SplitView.fillHeight: true
|
||||
SplitView.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.model
|
||||
|
||||
function onModelReset() {
|
||||
root.closeDialogs()
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,182 +0,0 @@
|
||||
// Copyright (C) 2024 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.Layouts
|
||||
import QtQuick.Shapes
|
||||
import QtQuick.Templates as T
|
||||
import HelperWidgets 2.0 as HelperWidgets
|
||||
import StudioTheme as StudioTheme
|
||||
import StudioControls as StudioControls
|
||||
import QtQuickDesignerTheme
|
||||
import QtQuickDesignerColorPalette
|
||||
import StudioHelpers
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property StudioTheme.ControlStyle style: StudioTheme.Values.controlStyle
|
||||
|
||||
property alias color: colorBackend.color
|
||||
|
||||
property alias actionIndicatorVisible: actionIndicator.visible
|
||||
property real __actionIndicatorWidth: root.style.actionIndicatorSize.width
|
||||
property real __actionIndicatorHeight: root.style.actionIndicatorSize.height
|
||||
|
||||
property alias showHexTextField: hexTextField.visible
|
||||
|
||||
readonly property real padding: 2
|
||||
readonly property real innerItemsHeight: root.height - 2 * root.padding
|
||||
|
||||
width: root.style.controlSize.width
|
||||
height: root.style.controlSize.height
|
||||
|
||||
clip: true
|
||||
|
||||
signal editorOpened(var item, var editorPopup)
|
||||
|
||||
function closeEditor() {
|
||||
loader.close()
|
||||
}
|
||||
|
||||
ColorBackend {
|
||||
id: colorBackend
|
||||
}
|
||||
|
||||
StudioControls.ActionIndicator {
|
||||
id: actionIndicator
|
||||
style: root.style
|
||||
__parentControl: root
|
||||
x: root.padding
|
||||
y: root.padding
|
||||
width: actionIndicator.visible ? root.__actionIndicatorWidth : 0
|
||||
height: actionIndicator.visible ? root.__actionIndicatorHeight : 0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: preview
|
||||
x: root.padding + actionIndicator.width
|
||||
y: root.padding
|
||||
z: previewMouseArea.containsMouse ? 10 : 0
|
||||
implicitWidth: root.innerItemsHeight
|
||||
implicitHeight: root.innerItemsHeight
|
||||
color: root.color
|
||||
border.color: previewMouseArea.containsMouse ? root.style.border.hover : root.style.border.idle
|
||||
border.width: root.style.borderWidth
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: "qrc:/navigator/icon/checkers.png"
|
||||
fillMode: Image.Tile
|
||||
z: -1
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: previewMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
loader.toggle()
|
||||
previewMouseArea.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
|
||||
function ensureLoader() {
|
||||
if (!loader.active)
|
||||
loader.active = true
|
||||
if (loader.sourceComponent === null)
|
||||
loader.sourceComponent = popupDialogComponent
|
||||
}
|
||||
|
||||
function open() {
|
||||
loader.ensureLoader()
|
||||
loader.item.show(preview)
|
||||
|
||||
if (loader.status === Loader.Ready)
|
||||
loader.item.originalColor = root.color
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (loader.item)
|
||||
loader.item.close()
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (loader.item)
|
||||
loader.close()
|
||||
else
|
||||
loader.open()
|
||||
}
|
||||
|
||||
Component {
|
||||
id: popupDialogComponent
|
||||
|
||||
|
||||
StudioControls.PopupDialog {
|
||||
id: popupDialog
|
||||
|
||||
property alias color: popup.color
|
||||
property alias originalColor: popup.originalColor
|
||||
titleBar: popup.titleBarContent
|
||||
|
||||
width: 260
|
||||
|
||||
StudioControls.ColorEditorPopup {
|
||||
id: popup
|
||||
width: popupDialog.contentWidth
|
||||
|
||||
onActivateColor: function(color) {
|
||||
colorBackend.activateColor(color)
|
||||
}
|
||||
}
|
||||
|
||||
onClosing: {
|
||||
loader.sourceComponent = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: null
|
||||
|
||||
Binding {
|
||||
target: loader.item
|
||||
property: "color"
|
||||
value: root.color
|
||||
when: loader.status === Loader.Ready
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
loader.item.originalColor = root.color
|
||||
root.editorOpened(root, loader.item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StudioControls.TextField {
|
||||
id: hexTextField
|
||||
style: root.style
|
||||
x: root.padding + actionIndicator.width + preview.width - preview.border.width
|
||||
y: root.padding
|
||||
width: root.width - hexTextField.x - 2 * root.padding
|
||||
height: root.innerItemsHeight
|
||||
text: root.color
|
||||
actionIndicatorVisible: false
|
||||
translationIndicatorVisible: false
|
||||
indicatorVisible: true
|
||||
indicator.icon.text: StudioTheme.Constants.copy_small
|
||||
indicator.onClicked: {
|
||||
hexTextField.selectAll()
|
||||
hexTextField.copy()
|
||||
hexTextField.deselect()
|
||||
}
|
||||
|
||||
validator: RegularExpressionValidator {
|
||||
regularExpression: /#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?/g
|
||||
}
|
||||
|
||||
onAccepted: colorBackend.activateColor(colorFromString(hexTextField.text))
|
||||
}
|
||||
}
|
@@ -1,57 +0,0 @@
|
||||
// Copyright (C) 2024 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 QtQuick.Layouts
|
||||
import HelperWidgets 2.0 as HelperWidgets
|
||||
import StudioControls 1.0 as StudioControls
|
||||
import StudioTheme as StudioTheme
|
||||
|
||||
StudioControls.Dialog {
|
||||
id: root
|
||||
|
||||
required property string collectionName
|
||||
|
||||
title: qsTr("Deleting the model")
|
||||
clip: true
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
id: deleteDialogContent // Keep the id here even if it's not used, because the dialog might lose implicitSize
|
||||
|
||||
width: 300
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
|
||||
wrapMode: Text.WordWrap
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
text: qsTr("Are you sure that you want to delete model \"%1\"?"
|
||||
+ "\nThe model will be deleted permanently.").arg(root.collectionName)
|
||||
|
||||
}
|
||||
|
||||
Item { // spacer
|
||||
implicitWidth: 1
|
||||
implicitHeight: StudioTheme.Values.columnGap
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: StudioTheme.Values.sectionRowSpacing
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 40
|
||||
|
||||
HelperWidgets.Button {
|
||||
text: qsTr("Delete")
|
||||
onClicked: root.accept()
|
||||
}
|
||||
|
||||
HelperWidgets.Button {
|
||||
text: qsTr("Cancel")
|
||||
onClicked: root.reject()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,158 +0,0 @@
|
||||
// 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.Layouts
|
||||
import StudioTheme 1.0 as StudioTheme
|
||||
import StudioControls 1.0 as StudioControls
|
||||
import HelperWidgets 2.0 as HelperWidgets
|
||||
import CollectionDetails
|
||||
|
||||
StudioControls.Dialog {
|
||||
id: root
|
||||
|
||||
required property var model
|
||||
property int __propertyIndex: -1
|
||||
property string __currentName
|
||||
|
||||
title: qsTr("Edit Column")
|
||||
|
||||
function openDialog(index, initialPosition) {
|
||||
root.__propertyIndex = index
|
||||
|
||||
if (root.__propertyIndex < 0)
|
||||
return
|
||||
|
||||
root.__currentName = root.model.propertyName(root.__propertyIndex)
|
||||
nameTextField.text = root.__currentName
|
||||
nameTextField.selectAll()
|
||||
nameTextField.forceActiveFocus()
|
||||
|
||||
typeComboBox.initialType = root.model.propertyType(root.__propertyIndex)
|
||||
typeComboBox.currentIndex = typeComboBox.find(typeComboBox.initialType)
|
||||
|
||||
let newPoint = mapFromGlobal(initialPosition.x, initialPosition.y)
|
||||
x = newPoint.x
|
||||
y = newPoint.y
|
||||
|
||||
root.open()
|
||||
}
|
||||
|
||||
onWidthChanged: {
|
||||
if (visible && x > parent.width)
|
||||
root.close()
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
if (nameTextField.text !== "" && nameTextField.text !== root.__currentName)
|
||||
root.model.renameColumn(root.__propertyIndex, nameTextField.text)
|
||||
|
||||
if (typeComboBox.initialType !== typeComboBox.currentText)
|
||||
root.model.setPropertyType(root.__propertyIndex, typeComboBox.currentText)
|
||||
}
|
||||
|
||||
contentItem: Column {
|
||||
spacing: 5
|
||||
|
||||
Grid {
|
||||
columns: 2
|
||||
rows: 2
|
||||
rowSpacing: 2
|
||||
columnSpacing: 25
|
||||
verticalItemAlignment: Grid.AlignVCenter
|
||||
|
||||
Text {
|
||||
text: qsTr("Name")
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StudioControls.TextField {
|
||||
id: nameTextField
|
||||
|
||||
actionIndicator.visible: false
|
||||
translationIndicator.visible: false
|
||||
|
||||
Keys.onEnterPressed: root.accept()
|
||||
Keys.onReturnPressed: root.accept()
|
||||
Keys.onEscapePressed: root.reject()
|
||||
|
||||
validator: RegularExpressionValidator {
|
||||
regularExpression: /^\w+$/
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("Type")
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
}
|
||||
|
||||
StudioControls.ComboBox {
|
||||
id: typeComboBox
|
||||
|
||||
property string initialType
|
||||
|
||||
model: CollectionDataTypeModel{}
|
||||
textRole: "display"
|
||||
tooltipRole: "toolTip"
|
||||
actionIndicatorVisible: false
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: warningBox
|
||||
|
||||
visible: typeComboBox.initialType !== typeComboBox.currentText
|
||||
color: "transparent"
|
||||
clip: true
|
||||
border.color: StudioTheme.Values.themeWarning
|
||||
width: parent.width
|
||||
height: warning.height
|
||||
|
||||
Row {
|
||||
id: warning
|
||||
|
||||
padding: 5
|
||||
spacing: 5
|
||||
|
||||
HelperWidgets.IconLabel {
|
||||
icon: StudioTheme.Constants.warning_medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: qsTr("Conversion from %1 to %2 may lead to data loss")
|
||||
.arg(typeComboBox.initialType)
|
||||
.arg(typeComboBox.currentText)
|
||||
|
||||
width: warningBox.width - 20
|
||||
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
height: 40
|
||||
spacing: 5
|
||||
|
||||
HelperWidgets.Button {
|
||||
id: editButton
|
||||
|
||||
text: qsTr("Apply")
|
||||
enabled: nameTextField.text !== ""
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
onClicked: root.accept()
|
||||
}
|
||||
|
||||
HelperWidgets.Button {
|
||||
text: qsTr("Cancel")
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
onClicked: root.reject()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,97 +0,0 @@
|
||||
// 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 QtQuick.Layouts
|
||||
import StudioTheme as StudioTheme
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
required property string text
|
||||
required property string icon
|
||||
|
||||
property alias tooltip: toolTip.text
|
||||
property StudioTheme.ControlStyle style: StudioTheme.Values.viewBarButtonStyle
|
||||
property int fontSize: StudioTheme.Values.baseFontSize
|
||||
|
||||
implicitHeight: style.squareControlSize.height
|
||||
implicitWidth: rowAlign.width
|
||||
|
||||
signal clicked()
|
||||
|
||||
RowLayout {
|
||||
id: rowAlign
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: StudioTheme.Values.inputHorizontalPadding
|
||||
|
||||
Text {
|
||||
id: iconItem
|
||||
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
||||
text: root.icon
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
font.family: StudioTheme.Constants.iconFont.family
|
||||
font.pixelSize: StudioTheme.Values.bigFont
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: StudioTheme.Values.inputHorizontalPadding
|
||||
}
|
||||
|
||||
Text {
|
||||
id: textItem
|
||||
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
||||
text: root.text
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
font.family: StudioTheme.Constants.font.family
|
||||
font.pixelSize: StudioTheme.Values.baseFontSize
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
rightPadding: StudioTheme.Values.inputHorizontalPadding
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
|
||||
ToolTip {
|
||||
id: toolTip
|
||||
|
||||
visible: mouseArea.containsMouse && text !== ""
|
||||
delay: 1000
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "default"
|
||||
when: !mouseArea.pressed && !mouseArea.containsMouse
|
||||
PropertyChanges {
|
||||
target: root
|
||||
color: StudioTheme.Values.themeControlBackground
|
||||
}
|
||||
},
|
||||
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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@@ -1,225 +0,0 @@
|
||||
// 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 QtQuick.Layouts
|
||||
import Qt.labs.platform as PlatformWidgets
|
||||
import HelperWidgets as HelperWidgets
|
||||
import StudioControls as StudioControls
|
||||
import StudioTheme as StudioTheme
|
||||
|
||||
StudioControls.Dialog {
|
||||
id: root
|
||||
|
||||
title: qsTr("Import a model")
|
||||
anchors.centerIn: parent
|
||||
closePolicy: Popup.CloseOnEscape
|
||||
modal: true
|
||||
|
||||
required property var backendValue
|
||||
|
||||
property bool fileExists: false
|
||||
|
||||
onOpened: {
|
||||
collectionName.text = "Model"
|
||||
fileName.text = qsTr("Model path")
|
||||
fileName.selectAll()
|
||||
fileName.forceActiveFocus()
|
||||
}
|
||||
|
||||
onRejected: {
|
||||
fileName.text = ""
|
||||
}
|
||||
|
||||
function acceptIfIsValid() {
|
||||
if (btnImport.enabled)
|
||||
btnImport.onClicked()
|
||||
}
|
||||
|
||||
RegularExpressionValidator {
|
||||
id: fileNameValidator
|
||||
regularExpression: /^(\w[^*><?|]*)[^/\\:*><?|]$/
|
||||
}
|
||||
|
||||
PlatformWidgets.FileDialog {
|
||||
id: fileDialog
|
||||
nameFilters : ["All Model Files (*.json *.csv)",
|
||||
"JSON Files (*.json)",
|
||||
"Comma-Separated Values (*.csv)"]
|
||||
|
||||
title: qsTr("Select a model file")
|
||||
fileMode: PlatformWidgets.FileDialog.OpenFile
|
||||
acceptLabel: qsTr("Open")
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
component Spacer: Item {
|
||||
implicitWidth: 1
|
||||
implicitHeight: StudioTheme.Values.columnGap
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 2
|
||||
|
||||
Keys.onEnterPressed: root.acceptIfIsValid()
|
||||
Keys.onReturnPressed: root.acceptIfIsValid()
|
||||
Keys.onEscapePressed: root.reject()
|
||||
|
||||
Text {
|
||||
text: qsTr("File name")
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: StudioTheme.Values.sectionRowSpacing
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
StudioControls.TextField {
|
||||
id: fileName
|
||||
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
actionIndicator.visible: false
|
||||
translationIndicator.visible: false
|
||||
validator: fileNameValidator
|
||||
|
||||
onTextChanged: root.fileExists = root.backendValue.isValidUrlToImport(fileName.text)
|
||||
}
|
||||
|
||||
HelperWidgets.Button {
|
||||
id: fileDialogButton
|
||||
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
|
||||
text: qsTr("Open")
|
||||
onClicked: fileDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
Spacer {}
|
||||
|
||||
Text {
|
||||
text: qsTr("The model name")
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
}
|
||||
|
||||
StudioControls.TextField {
|
||||
id: collectionName
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
actionIndicator.visible: false
|
||||
translationIndicator.visible: false
|
||||
validator: RegularExpressionValidator {
|
||||
regularExpression: /^[\w ]+$/
|
||||
}
|
||||
}
|
||||
|
||||
Spacer { implicitHeight: StudioTheme.Values.controlLabelGap }
|
||||
|
||||
Label {
|
||||
id: fieldErrorText
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
wrapMode: Label.WordWrap
|
||||
padding: 5
|
||||
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
border.width: StudioTheme.Values.border
|
||||
border.color: StudioTheme.Values.themeWarning
|
||||
}
|
||||
|
||||
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("The model name can not be empty")
|
||||
visible: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Spacer {}
|
||||
|
||||
StudioControls.CheckBox {
|
||||
id: csvFirstRowIsHeader
|
||||
|
||||
visible: root.fileExists && fileName.text.endsWith(".csv")
|
||||
text: qsTr("Consider first row as headers")
|
||||
checked: true
|
||||
actionIndicatorVisible: false
|
||||
}
|
||||
|
||||
Spacer {}
|
||||
|
||||
RowLayout {
|
||||
spacing: StudioTheme.Values.sectionRowSpacing
|
||||
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
|
||||
HelperWidgets.Button {
|
||||
id: btnImport
|
||||
|
||||
text: qsTr("Import")
|
||||
enabled: root.fileExists && collectionName.text !== ""
|
||||
|
||||
onClicked: {
|
||||
let collectionImported = root.backendValue.importFile(
|
||||
collectionName.text,
|
||||
fileName.text,
|
||||
csvFirstRowIsHeader.checked)
|
||||
|
||||
if (collectionImported)
|
||||
root.accept()
|
||||
else
|
||||
creationFailedDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
HelperWidgets.Button {
|
||||
text: qsTr("Cancel")
|
||||
onClicked: root.reject()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,42 +0,0 @@
|
||||
// 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 QtQuick.Layouts
|
||||
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: ColumnLayout {
|
||||
spacing: StudioTheme.Values.sectionColumnSpacing
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: root.message
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
wrapMode: Text.WordWrap
|
||||
leftPadding: 10
|
||||
rightPadding: 10
|
||||
}
|
||||
|
||||
HelperWidgets.Button {
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
|
||||
text: qsTr("Close")
|
||||
onClicked: root.reject()
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: root.forceActiveFocus()
|
||||
}
|
@@ -1,111 +0,0 @@
|
||||
// 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 QtQuick.Layouts
|
||||
import HelperWidgets as HelperWidgets
|
||||
import StudioControls as StudioControls
|
||||
import StudioTheme as StudioTheme
|
||||
import CollectionEditorBackend
|
||||
|
||||
StudioControls.Dialog {
|
||||
id: root
|
||||
|
||||
property bool nameExists: false
|
||||
readonly property bool isValid: collectionName.text !== "" && !root.nameExists
|
||||
|
||||
title: qsTr("Add a new model")
|
||||
|
||||
closePolicy: Popup.CloseOnEscape
|
||||
modal: true
|
||||
|
||||
onOpened: {
|
||||
collectionName.text = CollectionEditorBackend.model.getUniqueCollectionName()
|
||||
}
|
||||
|
||||
onRejected: {
|
||||
collectionName.text = ""
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
if (root.isValid)
|
||||
root.CollectionEditorBackend.rootView.addCollectionToDataStore(collectionName.text);
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 5
|
||||
|
||||
NameField {
|
||||
text: qsTr("Name")
|
||||
}
|
||||
|
||||
StudioControls.TextField {
|
||||
id: collectionName
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
actionIndicator.visible: false
|
||||
translationIndicator.visible: false
|
||||
validator: RegularExpressionValidator {
|
||||
regularExpression: /^[\w ]+$/
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: btnCreate.onClicked()
|
||||
Keys.onReturnPressed: btnCreate.onClicked()
|
||||
Keys.onEscapePressed: root.reject()
|
||||
|
||||
onTextChanged: {
|
||||
root.nameExists = CollectionEditorBackend.model.collectionExists(collectionName.text)
|
||||
}
|
||||
}
|
||||
|
||||
ErrorField {
|
||||
id: errorField
|
||||
text: {
|
||||
if (collectionName.text === "")
|
||||
return qsTr("Name can not be empty")
|
||||
else if (root.nameExists)
|
||||
return qsTr("Name '%1' already exists").arg(collectionName.text)
|
||||
else
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
Spacer {}
|
||||
|
||||
Row {
|
||||
spacing: StudioTheme.Values.sectionRowSpacing
|
||||
|
||||
HelperWidgets.Button {
|
||||
id: btnCreate
|
||||
|
||||
text: qsTr("Create")
|
||||
enabled: root.isValid
|
||||
onClicked: root.accept()
|
||||
}
|
||||
|
||||
HelperWidgets.Button {
|
||||
text: qsTr("Cancel")
|
||||
onClicked: root.reject()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component NameField: Text {
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
font.family: StudioTheme.Constants.font.family
|
||||
font.pixelSize: StudioTheme.Values.baseIconFontSize
|
||||
}
|
||||
|
||||
component ErrorField: Text {
|
||||
color: StudioTheme.Values.themeError
|
||||
font.family: StudioTheme.Constants.font.family
|
||||
font.pixelSize: StudioTheme.Values.baseIconFontSize
|
||||
}
|
||||
|
||||
component Spacer: Item {
|
||||
width: 1
|
||||
height: StudioTheme.Values.columnGap
|
||||
}
|
||||
}
|
@@ -1,88 +0,0 @@
|
||||
// Copyright (C) 2024 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 QtQuick.Layouts
|
||||
import HelperWidgets 2.0 as HelperWidgets
|
||||
import StudioControls 1.0 as StudioControls
|
||||
import StudioTheme as StudioTheme
|
||||
|
||||
StudioControls.Dialog {
|
||||
id: root
|
||||
|
||||
required property string collectionName
|
||||
readonly property alias newCollectionName: newNameField.text
|
||||
readonly property bool isValid: newNameField.text !== ""
|
||||
|
||||
title: qsTr("Rename model")
|
||||
|
||||
onOpened: {
|
||||
newNameField.text = root.collectionName
|
||||
newNameField.forceActiveFocus()
|
||||
}
|
||||
|
||||
function acceptIfVerified() {
|
||||
if (root.isValid)
|
||||
root.accept()
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
id: renameDialogContent
|
||||
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
text: qsTr("Previous name: " + root.collectionName)
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
}
|
||||
|
||||
Spacer {}
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
||||
text: qsTr("New name:")
|
||||
color: StudioTheme.Values.themeTextColor
|
||||
}
|
||||
|
||||
StudioControls.TextField {
|
||||
id: newNameField
|
||||
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
actionIndicator.visible: false
|
||||
translationIndicator.visible: false
|
||||
validator: RegularExpressionValidator {
|
||||
regularExpression: /^\w+$/
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: root.acceptIfVerified()
|
||||
Keys.onReturnPressed: root.acceptIfVerified()
|
||||
Keys.onEscapePressed: root.reject()
|
||||
}
|
||||
|
||||
Spacer {}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
|
||||
spacing: StudioTheme.Values.sectionRowSpacing
|
||||
|
||||
HelperWidgets.Button {
|
||||
text: qsTr("Rename")
|
||||
enabled: root.isValid
|
||||
onClicked: root.acceptIfVerified()
|
||||
}
|
||||
|
||||
HelperWidgets.Button {
|
||||
text: qsTr("Cancel")
|
||||
onClicked: root.reject()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Spacer: Item {
|
||||
implicitWidth: 1
|
||||
implicitHeight: StudioTheme.Values.columnGap
|
||||
}
|
||||
}
|
@@ -243,16 +243,6 @@ QtObject {
|
||||
property real dialogButtonSpacing: 10
|
||||
property real dialogButtonPadding: 4
|
||||
|
||||
// Collection Editor
|
||||
property real collectionItemTextSideMargin: 10
|
||||
property real collectionItemTextMargin: 5
|
||||
property real collectionItemTextPadding: 5
|
||||
property real collectionTableHorizontalMargin: 10
|
||||
property real collectionTableVerticalMargin: 10
|
||||
property real collectionCellMinimumWidth: 60
|
||||
property real collectionCellMinimumHeight: 20
|
||||
property real smallStatusIndicatorDiameter: 6
|
||||
|
||||
// NEW NEW NEW
|
||||
readonly property int flowMargin: 7
|
||||
readonly property int flowSpacing: 7 // Odd so cursor has a center location
|
||||
|
@@ -1,18 +0,0 @@
|
||||
[
|
||||
{
|
||||
"colorCode": "#ff0000",
|
||||
"name": "Red"
|
||||
},
|
||||
{
|
||||
"colorCode": "#00ff00",
|
||||
"name": "Green"
|
||||
},
|
||||
{
|
||||
"colorCode": "#0000ff",
|
||||
"name": "Blue"
|
||||
},
|
||||
{
|
||||
"colorCode": "#ffffff",
|
||||
"name": "White"
|
||||
}
|
||||
]
|
@@ -1,17 +0,0 @@
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
||||
|
||||
pragma Singleton
|
||||
import QtQuick 6.5
|
||||
import QtQuick.Studio.Utils 1.0
|
||||
|
||||
JsonListModel {
|
||||
id: models
|
||||
source: Qt.resolvedUrl("models.json")
|
||||
|
||||
property ChildListModel book: ChildListModel {
|
||||
modelName: "book"
|
||||
}
|
||||
|
||||
property JsonData backend: JsonData {}
|
||||
}
|
@@ -1,56 +0,0 @@
|
||||
{
|
||||
"book": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "author",
|
||||
"type": "String"
|
||||
},
|
||||
{
|
||||
"name": "category",
|
||||
"type": "String"
|
||||
},
|
||||
{
|
||||
"name": "isbn",
|
||||
"type": "String"
|
||||
},
|
||||
{
|
||||
"name": "price",
|
||||
"type": "Real"
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"type": "String"
|
||||
}
|
||||
],
|
||||
"data": [
|
||||
[
|
||||
"Nigel Rees",
|
||||
"reference",
|
||||
"",
|
||||
8.95,
|
||||
"Sayings of the Century"
|
||||
],
|
||||
[
|
||||
"Evelyn Waugh",
|
||||
"fiction",
|
||||
"",
|
||||
12.99,
|
||||
"Sword of Honor"
|
||||
],
|
||||
[
|
||||
"Herman Melville",
|
||||
"fiction",
|
||||
"0-553-21311-3",
|
||||
8.99,
|
||||
"Moby Dick"
|
||||
],
|
||||
[
|
||||
"J. R. R. Tolkien",
|
||||
"fiction",
|
||||
"0-395-19395-8",
|
||||
22.99,
|
||||
"The Lord of the Rings"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
@@ -500,7 +500,6 @@ 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
|
||||
@@ -847,22 +846,6 @@ extend_qtc_plugin(QmlDesigner
|
||||
materialeditor.qrc
|
||||
)
|
||||
|
||||
extend_qtc_plugin(QmlDesigner
|
||||
SOURCES_PREFIX components/collectioneditor
|
||||
SOURCES
|
||||
collectiondatatypemodel.cpp collectiondatatypemodel.h
|
||||
collectiondetails.cpp collectiondetails.h
|
||||
collectiondetailsmodel.cpp collectiondetailsmodel.h
|
||||
collectiondetailssortfiltermodel.cpp collectiondetailssortfiltermodel.h
|
||||
collectioneditorconstants.h
|
||||
collectioneditorutils.cpp collectioneditorutils.h
|
||||
collectionjsonparser.cpp collectionjsonparser.h
|
||||
collectionlistmodel.cpp collectionlistmodel.h
|
||||
collectionview.cpp collectionview.h
|
||||
collectionwidget.cpp collectionwidget.h
|
||||
datastoremodelnode.cpp datastoremodelnode.h
|
||||
)
|
||||
|
||||
extend_qtc_plugin(QmlDesigner
|
||||
SOURCES_PREFIX components/textureeditor
|
||||
SOURCES
|
||||
|
@@ -1,86 +0,0 @@
|
||||
// Copyright (C) 2024 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#include "collectiondatatypemodel.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QtQml/QmlTypeAndRevisionsRegistration>
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
struct CollectionDataTypeModel::Details
|
||||
{
|
||||
CollectionDetails::DataType type;
|
||||
QString name;
|
||||
QString description;
|
||||
};
|
||||
|
||||
const QList<CollectionDataTypeModel::Details> CollectionDataTypeModel::m_orderedDetails{
|
||||
{DataType::String, "String", "Text"},
|
||||
{DataType::Integer, "Integer", "Whole number that can be positive, negative, or zero"},
|
||||
{DataType::Real, "Real", "Number with a decimal"},
|
||||
{DataType::Image, "Image", "Image resource"},
|
||||
{DataType::Color, "Color", "HEX value"},
|
||||
{DataType::Url, "Url", "Resource locator"},
|
||||
{DataType::Boolean, "Boolean", "True/false"},
|
||||
};
|
||||
|
||||
CollectionDataTypeModel::CollectionDataTypeModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
int CollectionDataTypeModel::rowCount([[maybe_unused]] const QModelIndex &parent) const
|
||||
{
|
||||
return m_orderedDetails.size();
|
||||
}
|
||||
|
||||
QVariant CollectionDataTypeModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return {};
|
||||
|
||||
if (role == Qt::DisplayRole)
|
||||
return m_orderedDetails.at(index.row()).name;
|
||||
if (role == Qt::ToolTipRole)
|
||||
return m_orderedDetails.at(index.row()).description;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
QString CollectionDataTypeModel::dataTypeToString(DataType dataType)
|
||||
{
|
||||
static const QHash<DataType, QString> dataTypeHash = []() -> QHash<DataType, QString> {
|
||||
QHash<DataType, QString> result;
|
||||
for (const Details &details : m_orderedDetails)
|
||||
result.insert(details.type, details.name);
|
||||
return result;
|
||||
}();
|
||||
|
||||
if (dataTypeHash.contains(dataType))
|
||||
return dataTypeHash.value(dataType);
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
CollectionDetails::DataType CollectionDataTypeModel::dataTypeFromString(const QString &dataType)
|
||||
{
|
||||
static const QHash<QString, DataType> stringTypeHash = []() -> QHash<QString, DataType> {
|
||||
QHash<QString, DataType> result;
|
||||
for (const Details &details : m_orderedDetails)
|
||||
result.insert(details.name, details.type);
|
||||
return result;
|
||||
}();
|
||||
|
||||
if (stringTypeHash.contains(dataType))
|
||||
return stringTypeHash.value(dataType);
|
||||
|
||||
return DataType::String;
|
||||
}
|
||||
|
||||
void CollectionDataTypeModel::registerDeclarativeType()
|
||||
{
|
||||
qmlRegisterType<CollectionDataTypeModel>("CollectionDetails", 1, 0, "CollectionDataTypeModel");
|
||||
}
|
||||
|
||||
} // namespace QmlDesigner
|
@@ -1,34 +0,0 @@
|
||||
// Copyright (C) 2024 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "collectiondetails.h"
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QList>
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
class CollectionDataTypeModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
using DataType = CollectionDetails::DataType;
|
||||
CollectionDataTypeModel(QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
static Q_INVOKABLE QString dataTypeToString(DataType dataType);
|
||||
static Q_INVOKABLE DataType dataTypeFromString(const QString &dataType);
|
||||
|
||||
static void registerDeclarativeType();
|
||||
|
||||
private:
|
||||
struct Details;
|
||||
static const QList<Details> m_orderedDetails;
|
||||
};
|
||||
|
||||
} // namespace QmlDesigner
|
@@ -1,882 +0,0 @@
|
||||
// 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 "collectiondetails.h"
|
||||
|
||||
#include "collectiondatatypemodel.h"
|
||||
#include "collectioneditorutils.h"
|
||||
#include "collectionjsonparser.h"
|
||||
|
||||
#include <qqml.h>
|
||||
#include <utils/algorithm.h>
|
||||
#include <utils/qtcassert.h>
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QRegularExpression>
|
||||
#include <QTextStream>
|
||||
#include <QUrl>
|
||||
#include <QVariant>
|
||||
|
||||
namespace QmlDesigner {
|
||||
#define COLLERR_OK QT_TRANSLATE_NOOP("CollectioParseError", "no error occurred")
|
||||
#define COLLERR_MAINOBJECT QT_TRANSLATE_NOOP("CollectioParseError", "Document object not found")
|
||||
#define COLLERR_COLLECTIONNAME QT_TRANSLATE_NOOP("CollectioParseError", "Model name not found")
|
||||
#define COLLERR_COLLECTIONOBJ QT_TRANSLATE_NOOP("CollectioParseError", "Model is not an object")
|
||||
#define COLLERR_COLUMNARRAY QT_TRANSLATE_NOOP("CollectioParseError", "Column is not an array")
|
||||
#define COLLERR_UNKNOWN QT_TRANSLATE_NOOP("CollectioParseError", "Unknown error")
|
||||
|
||||
struct CollectionProperty
|
||||
{
|
||||
using DataType = CollectionDetails::DataType;
|
||||
|
||||
QString name;
|
||||
DataType type;
|
||||
};
|
||||
|
||||
const QMap<DataTypeWarning::Warning, QString> DataTypeWarning::dataTypeWarnings = {
|
||||
{DataTypeWarning::CellDataTypeMismatch, "Cell and column data types do not match."}
|
||||
};
|
||||
|
||||
class CollectionDetails::Private
|
||||
{
|
||||
public:
|
||||
QList<CollectionProperty> properties;
|
||||
QList<QJsonArray> dataRecords;
|
||||
CollectionReference reference;
|
||||
bool isChanged = false;
|
||||
|
||||
bool isValidColumnId(int column) const { return column > -1 && column < properties.size(); }
|
||||
|
||||
bool isValidRowId(int row) const { return row > -1 && row < dataRecords.size(); }
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief getCustomUrl
|
||||
* Address = <Url|LocalFile>
|
||||
*
|
||||
* @param value The input value to be evaluated
|
||||
* @param dataType if the value is a valid url, the data type
|
||||
* will be stored to this parameter, otherwise, it will be String
|
||||
* @param urlResult if the value is a valid url, the address
|
||||
* will be stored in this parameter, otherwise it will be empty.
|
||||
* @return true if the result is url
|
||||
*/
|
||||
static bool getCustomUrl(const QString &value,
|
||||
CollectionDetails::DataType &dataType,
|
||||
QUrl *urlResult = nullptr)
|
||||
{
|
||||
static const QRegularExpression urlRegex{
|
||||
"^(?<Address>"
|
||||
"(?<Url>https?:\\/\\/"
|
||||
"(?:www\\.|(?!www))[A-z0-9][A-z0-9-]+[A-z0-9]\\.[^\\s]{2,}|www\\.[A-z0-9][A-z0-9-]+"
|
||||
"[A-z0-9]\\.[^\\s]{2,}|https?:\\/\\/"
|
||||
"(?:www\\.|(?!www))[A-z0-9]+\\.[^\\s]{2,}|www\\.[A-z0-9]+\\.[^\\s]{2,})|" // end of Url
|
||||
"(?<LocalFile>("
|
||||
"?:(?:[A-z]:)|(?:(?:\\\\|\\/){1,2}\\w+)\\$?)(?:(?:\\\\|\\/)(?:\\w[\\w ]*.*))+)" // end of LocalFile
|
||||
"){1}$" // end of Address
|
||||
};
|
||||
|
||||
const QRegularExpressionMatch match = urlRegex.match(value.trimmed());
|
||||
if (match.hasCaptured("Address")) {
|
||||
dataType = CollectionDetails::DataType::Url;
|
||||
|
||||
if (urlResult)
|
||||
urlResult->setUrl(match.captured("Address"));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (urlResult)
|
||||
urlResult->clear();
|
||||
|
||||
dataType = CollectionDetails::DataType::String;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief dataTypeFromString
|
||||
* @param value The string value to be evaluated
|
||||
* @return Bool, Color, Integer, Real, Url,
|
||||
* Image if these types are detected within the non-empty string,
|
||||
* Otherwise it returns String.
|
||||
* If the value is integer, but it's out of the int range, it will be
|
||||
* considered as a Real.
|
||||
*/
|
||||
static CollectionDetails::DataType dataTypeFromString(const QString &value)
|
||||
{
|
||||
using DataType = CollectionDetails::DataType;
|
||||
static const QRegularExpression validator{
|
||||
"(?<boolean>^(?:true|false)$)|"
|
||||
"(?<color>^(?:#(?:(?:[0-9a-fA-F]{2}){3,4}|(?:[0-9a-fA-F]){3,4}))$)|"
|
||||
"(?<integer>^\\d+$)|"
|
||||
"(?<real>^(?:-?(?:0|[1-9]\\d*)?(?:\\.\\d*)?(?<=\\d|\\.)"
|
||||
"(?:e-?(?:0|[1-9]\\d*))?|0x[0-9a-f]+)$)"};
|
||||
static const int boolIndex = validator.namedCaptureGroups().indexOf("boolean");
|
||||
static const int colorIndex = validator.namedCaptureGroups().indexOf("color");
|
||||
static const int integerIndex = validator.namedCaptureGroups().indexOf("integer");
|
||||
static const int realIndex = validator.namedCaptureGroups().indexOf("real");
|
||||
|
||||
[[maybe_unused]] static const bool allIndexesFound =
|
||||
[](const std::initializer_list<int> &captureIndexes) {
|
||||
QTC_ASSERT(Utils::allOf(captureIndexes, [](int val) { return val > -1; }), return false);
|
||||
return true;
|
||||
}({boolIndex, colorIndex, integerIndex, realIndex});
|
||||
|
||||
if (value.isEmpty())
|
||||
return DataType::String;
|
||||
|
||||
const QString trimmedValue = value.trimmed();
|
||||
QRegularExpressionMatch match = validator.match(trimmedValue);
|
||||
|
||||
if (match.hasCaptured(boolIndex))
|
||||
return DataType::Boolean;
|
||||
if (match.hasCaptured(colorIndex))
|
||||
return DataType::Color;
|
||||
if (match.hasCaptured(integerIndex)) {
|
||||
bool isInt = false;
|
||||
trimmedValue.toInt(&isInt);
|
||||
return isInt ? DataType::Integer : DataType::Real;
|
||||
}
|
||||
if (match.hasCaptured(realIndex))
|
||||
return DataType::Real;
|
||||
|
||||
DataType urlType;
|
||||
if (getCustomUrl(trimmedValue, urlType))
|
||||
return urlType;
|
||||
|
||||
return DataType::String;
|
||||
}
|
||||
|
||||
static CollectionProperty::DataType dataTypeFromJsonValue(const QJsonValue &value)
|
||||
{
|
||||
using DataType = CollectionDetails::DataType;
|
||||
using JsonType = QJsonValue::Type;
|
||||
|
||||
switch (value.type()) {
|
||||
case JsonType::Null:
|
||||
case JsonType::Undefined:
|
||||
return DataType::String;
|
||||
case JsonType::Bool:
|
||||
return DataType::Boolean;
|
||||
case JsonType::Double: {
|
||||
if (qFuzzyIsNull(std::remainder(value.toDouble(), 1)))
|
||||
return DataType::Integer;
|
||||
return DataType::Real;
|
||||
}
|
||||
case JsonType::String:
|
||||
return dataTypeFromString(value.toString());
|
||||
default:
|
||||
return DataType::String;
|
||||
}
|
||||
}
|
||||
|
||||
static QList<CollectionProperty> getColumnsFromImportedJsonArray(const QJsonArray &importedArray)
|
||||
{
|
||||
using DataType = CollectionDetails::DataType;
|
||||
|
||||
QHash<QString, int> resultSet;
|
||||
QList<CollectionProperty> result;
|
||||
|
||||
for (const QJsonValue &value : importedArray) {
|
||||
if (value.isObject()) {
|
||||
const QJsonObject object = value.toObject();
|
||||
QJsonObject::ConstIterator element = object.constBegin();
|
||||
const QJsonObject::ConstIterator stopItem = object.constEnd();
|
||||
|
||||
while (element != stopItem) {
|
||||
const QString propertyName = element.key();
|
||||
if (resultSet.contains(propertyName)) {
|
||||
CollectionProperty &property = result[resultSet.value(propertyName)];
|
||||
if (property.type == DataType::Integer) {
|
||||
const DataType currentCellDataType = dataTypeFromJsonValue(element.value());
|
||||
if (currentCellDataType == DataType::Real)
|
||||
property.type = currentCellDataType;
|
||||
}
|
||||
} else {
|
||||
result.append({propertyName, dataTypeFromJsonValue(element.value())});
|
||||
resultSet.insert(propertyName, resultSet.size());
|
||||
}
|
||||
++element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static QVariant valueToVariant(const QJsonValue &value, CollectionDetails::DataType type)
|
||||
{
|
||||
using DataType = CollectionDetails::DataType;
|
||||
QVariant variantValue = value.toVariant();
|
||||
|
||||
switch (type) {
|
||||
case DataType::String:
|
||||
return variantValue.toString();
|
||||
case DataType::Integer:
|
||||
return variantValue.toInt();
|
||||
case DataType::Real:
|
||||
return variantValue.toDouble();
|
||||
case DataType::Boolean:
|
||||
return variantValue.toBool();
|
||||
case DataType::Color:
|
||||
return variantValue.value<QColor>();
|
||||
case DataType::Url:
|
||||
case DataType::Image:
|
||||
return variantValue.value<QUrl>();
|
||||
default:
|
||||
return variantValue;
|
||||
}
|
||||
}
|
||||
|
||||
static QJsonValue variantToJsonValue(
|
||||
const QVariant &variant, CollectionDetails::DataType type = CollectionDetails::DataType::String)
|
||||
{
|
||||
using DataType = CollectionDetails::DataType;
|
||||
|
||||
switch (type) {
|
||||
case DataType::Boolean:
|
||||
return variant.toBool();
|
||||
case DataType::Real:
|
||||
return variant.toDouble();
|
||||
case DataType::Integer:
|
||||
return variant.toInt();
|
||||
case DataType::Image:
|
||||
case DataType::String:
|
||||
case DataType::Color:
|
||||
case DataType::Url:
|
||||
default:
|
||||
return variant.toString();
|
||||
}
|
||||
}
|
||||
|
||||
inline static bool isEmptyJsonValue(const QJsonValue &value)
|
||||
{
|
||||
return value.isNull() || value.isUndefined() || (value.isString() && value.toString().isEmpty());
|
||||
}
|
||||
|
||||
QStringList csvReadLine(const QString &line)
|
||||
{
|
||||
static const QRegularExpression lineRegex{
|
||||
"(?:,\\\"|^\\\")(?<value>\\\"\\\"|[\\w\\W]*?)(?=\\\",|\\\"$)"
|
||||
"|(?:,(?!\\\")|^(?!\\\"))(?<quote>[^,]*?)(?=$|,)|(\\\\r\\\\n|\\\\n)"};
|
||||
static const int valueIndex = lineRegex.namedCaptureGroups().indexOf("value");
|
||||
static const int quoteIndex = lineRegex.namedCaptureGroups().indexOf("quote");
|
||||
Q_ASSERT(valueIndex > 0 && quoteIndex > 0);
|
||||
|
||||
QStringList result;
|
||||
QRegularExpressionMatchIterator iterator = lineRegex.globalMatch(line, 0);
|
||||
while (iterator.hasNext()) {
|
||||
const QRegularExpressionMatch match = iterator.next();
|
||||
|
||||
if (match.hasCaptured(valueIndex))
|
||||
result.append(match.captured(valueIndex));
|
||||
else if (match.hasCaptured(quoteIndex))
|
||||
result.append(match.captured(quoteIndex));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QString CollectionParseError::errorString() const
|
||||
{
|
||||
switch (errorNo) {
|
||||
case NoError:
|
||||
return COLLERR_OK;
|
||||
case MainObjectMissing:
|
||||
return COLLERR_MAINOBJECT;
|
||||
case CollectionNameNotFound:
|
||||
return COLLERR_COLLECTIONNAME;
|
||||
case CollectionIsNotObject:
|
||||
return COLLERR_COLLECTIONOBJ;
|
||||
case ColumnsBlockIsNotArray:
|
||||
return COLLERR_COLUMNARRAY;
|
||||
case UnknownError:
|
||||
default:
|
||||
return COLLERR_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
CollectionDetails::CollectionDetails()
|
||||
: d(new Private())
|
||||
{}
|
||||
|
||||
CollectionDetails::CollectionDetails(const CollectionReference &reference)
|
||||
: CollectionDetails()
|
||||
{
|
||||
d->reference = reference;
|
||||
}
|
||||
|
||||
void CollectionDetails::resetData(const QJsonDocument &localDocument,
|
||||
const QString &collectionToImport,
|
||||
CollectionParseError *error)
|
||||
{
|
||||
CollectionDetails importedCollection = fromLocalJson(localDocument, collectionToImport, error);
|
||||
d->properties.swap(importedCollection.d->properties);
|
||||
d->dataRecords.swap(importedCollection.d->dataRecords);
|
||||
}
|
||||
|
||||
CollectionDetails::CollectionDetails(const CollectionDetails &other) = default;
|
||||
|
||||
CollectionDetails::~CollectionDetails() = default;
|
||||
|
||||
void CollectionDetails::insertColumn(const QString &propertyName,
|
||||
int colIdx,
|
||||
const QVariant &defaultValue,
|
||||
DataType type)
|
||||
{
|
||||
if (containsPropertyName(propertyName))
|
||||
return;
|
||||
|
||||
CollectionProperty property = {propertyName, type};
|
||||
if (d->isValidColumnId(colIdx)) {
|
||||
d->properties.insert(colIdx, property);
|
||||
} else {
|
||||
colIdx = d->properties.size();
|
||||
d->properties.append(property);
|
||||
}
|
||||
|
||||
const QJsonValue defaultJsonValue = QJsonValue::fromVariant(defaultValue);
|
||||
for (QJsonArray &record : d->dataRecords)
|
||||
record.insert(colIdx, defaultJsonValue);
|
||||
|
||||
markChanged();
|
||||
}
|
||||
|
||||
bool CollectionDetails::removeColumns(int colIdx, int count)
|
||||
{
|
||||
if (!d->isValidColumnId(colIdx))
|
||||
return false;
|
||||
|
||||
int maxCount = d->properties.count() - colIdx;
|
||||
count = std::min(maxCount, count);
|
||||
|
||||
if (count < 1)
|
||||
return false;
|
||||
|
||||
d->properties.remove(colIdx, count);
|
||||
|
||||
for (QJsonArray &record : d->dataRecords) {
|
||||
QJsonArray newElement;
|
||||
|
||||
auto elementItr = record.constBegin();
|
||||
auto elementEnd = elementItr + colIdx;
|
||||
while (elementItr != elementEnd)
|
||||
newElement.append(*(elementItr++));
|
||||
|
||||
elementItr += count;
|
||||
elementEnd = record.constEnd();
|
||||
|
||||
while (elementItr != elementEnd)
|
||||
newElement.append(*(elementItr++));
|
||||
|
||||
record = newElement;
|
||||
}
|
||||
|
||||
markChanged();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void CollectionDetails::insertEmptyRows(int row, int count)
|
||||
{
|
||||
if (count < 1)
|
||||
return;
|
||||
|
||||
row = qBound(0, row, rows());
|
||||
|
||||
insertRecords({}, row, count);
|
||||
|
||||
markChanged();
|
||||
}
|
||||
|
||||
bool CollectionDetails::removeRows(int row, int count)
|
||||
{
|
||||
if (!d->isValidRowId(row))
|
||||
return false;
|
||||
|
||||
int maxCount = d->dataRecords.count() - row;
|
||||
count = std::min(maxCount, count);
|
||||
|
||||
if (count < 1)
|
||||
return false;
|
||||
|
||||
d->dataRecords.remove(row, count);
|
||||
markChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CollectionDetails::setPropertyValue(int row, int column, const QVariant &value)
|
||||
{
|
||||
if (!d->isValidRowId(row) || !d->isValidColumnId(column))
|
||||
return false;
|
||||
|
||||
QVariant currentValue = data(row, column);
|
||||
if (value == currentValue)
|
||||
return false;
|
||||
|
||||
QJsonArray &record = d->dataRecords[row];
|
||||
record.replace(column, variantToJsonValue(value, typeAt(column)));
|
||||
markChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CollectionDetails::setPropertyName(int column, const QString &value)
|
||||
{
|
||||
if (!d->isValidColumnId(column))
|
||||
return false;
|
||||
|
||||
const CollectionProperty &oldProperty = d->properties.at(column);
|
||||
if (oldProperty.name == value)
|
||||
return false;
|
||||
|
||||
d->properties.replace(column, {value, oldProperty.type});
|
||||
|
||||
markChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CollectionDetails::setPropertyType(int column, DataType type)
|
||||
{
|
||||
if (!d->isValidColumnId(column))
|
||||
return false;
|
||||
|
||||
bool changed = false;
|
||||
CollectionProperty &property = d->properties[column];
|
||||
if (property.type != type)
|
||||
changed = true;
|
||||
|
||||
const DataType formerType = property.type;
|
||||
property.type = type;
|
||||
|
||||
for (QJsonArray &rowData : d->dataRecords) {
|
||||
if (column < rowData.size()) {
|
||||
const QJsonValue value = rowData.at(column);
|
||||
const QVariant properTypedValue = valueToVariant(value, formerType);
|
||||
const QJsonValue properTypedJsonValue = variantToJsonValue(properTypedValue, type);
|
||||
rowData.replace(column, properTypedJsonValue);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
markChanged();
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
CollectionReference CollectionDetails::reference() const
|
||||
{
|
||||
return d->reference;
|
||||
}
|
||||
|
||||
QVariant CollectionDetails::data(int row, int column) const
|
||||
{
|
||||
if (!d->isValidRowId(row))
|
||||
return {};
|
||||
|
||||
if (!d->isValidColumnId(column))
|
||||
return {};
|
||||
|
||||
const QJsonValue cellValue = d->dataRecords.at(row).at(column);
|
||||
|
||||
return cellValue.toVariant();
|
||||
}
|
||||
|
||||
QString CollectionDetails::propertyAt(int column) const
|
||||
{
|
||||
if (!d->isValidColumnId(column))
|
||||
return {};
|
||||
|
||||
return d->properties.at(column).name;
|
||||
}
|
||||
|
||||
CollectionDetails::DataType CollectionDetails::typeAt(int column) const
|
||||
{
|
||||
if (!d->isValidColumnId(column))
|
||||
return {};
|
||||
|
||||
return d->properties.at(column).type;
|
||||
}
|
||||
|
||||
CollectionDetails::DataType CollectionDetails::typeAt(int row, int column) const
|
||||
{
|
||||
if (!d->isValidRowId(row) || !d->isValidColumnId(column))
|
||||
return {};
|
||||
|
||||
const QJsonValue cellData = d->dataRecords.at(row).at(column);
|
||||
return dataTypeFromJsonValue(cellData);
|
||||
}
|
||||
|
||||
DataTypeWarning::Warning CollectionDetails::cellWarningCheck(int row, int column) const
|
||||
{
|
||||
const QJsonValue cellValue = d->dataRecords.at(row).at(column);
|
||||
|
||||
const DataType columnType = typeAt(column);
|
||||
const DataType cellType = typeAt(row, column);
|
||||
|
||||
if (isEmptyJsonValue(cellValue))
|
||||
return DataTypeWarning::Warning::None;
|
||||
|
||||
if ((columnType == DataType::String || columnType == DataType::Real) && cellType == DataType::Integer)
|
||||
return DataTypeWarning::Warning::None;
|
||||
|
||||
if (columnType == DataType::Url && cellType == DataType::String)
|
||||
return DataTypeWarning::Warning::None;
|
||||
|
||||
if (columnType == DataType::Image && (cellType == DataType::Url || cellType == DataType::String))
|
||||
return DataTypeWarning::Warning::None;
|
||||
|
||||
if (columnType != cellType)
|
||||
return DataTypeWarning::Warning::CellDataTypeMismatch;
|
||||
|
||||
return DataTypeWarning::Warning::None;
|
||||
}
|
||||
|
||||
bool CollectionDetails::containsPropertyName(const QString &propertyName) const
|
||||
{
|
||||
return Utils::anyOf(d->properties, [&propertyName](const CollectionProperty &property) {
|
||||
return property.name == propertyName;
|
||||
});
|
||||
}
|
||||
|
||||
bool CollectionDetails::hasValidReference() const
|
||||
{
|
||||
return d->reference.node.isValid() && d->reference.name.size();
|
||||
}
|
||||
|
||||
bool CollectionDetails::isChanged() const
|
||||
{
|
||||
return d->isChanged;
|
||||
}
|
||||
|
||||
int CollectionDetails::columns() const
|
||||
{
|
||||
return d->properties.size();
|
||||
}
|
||||
|
||||
int CollectionDetails::rows() const
|
||||
{
|
||||
return d->dataRecords.size();
|
||||
}
|
||||
|
||||
bool CollectionDetails::markSaved()
|
||||
{
|
||||
if (d->isChanged) {
|
||||
d->isChanged = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void CollectionDetails::swap(CollectionDetails &other)
|
||||
{
|
||||
d.swap(other.d);
|
||||
}
|
||||
|
||||
void CollectionDetails::resetReference(const CollectionReference &reference)
|
||||
{
|
||||
if (d->reference != reference) {
|
||||
d->reference = reference;
|
||||
markChanged();
|
||||
}
|
||||
}
|
||||
|
||||
QString CollectionDetails::toJson() const
|
||||
{
|
||||
QJsonArray exportedArray;
|
||||
const int propertyCount = d->properties.count();
|
||||
|
||||
for (const QJsonArray &record : std::as_const(d->dataRecords)) {
|
||||
const int valueCount = std::min(int(record.count()), propertyCount);
|
||||
|
||||
QJsonObject exportedElement;
|
||||
for (int i = 0; i < valueCount; ++i) {
|
||||
const QJsonValue &value = record.at(i);
|
||||
if (isEmptyJsonValue(value))
|
||||
exportedElement.insert(d->properties.at(i).name, QJsonValue::Null);
|
||||
else
|
||||
exportedElement.insert(d->properties.at(i).name, value);
|
||||
}
|
||||
|
||||
exportedArray.append(exportedElement);
|
||||
}
|
||||
|
||||
return QString::fromUtf8(QJsonDocument(exportedArray).toJson());
|
||||
}
|
||||
|
||||
QString CollectionDetails::toCsv() const
|
||||
{
|
||||
QString content;
|
||||
|
||||
auto gotoNextLine = [&content]() {
|
||||
if (content.size() && content.back() == ',')
|
||||
content.back() = '\n';
|
||||
else
|
||||
content += "\n";
|
||||
};
|
||||
|
||||
const int propertyCount = d->properties.count();
|
||||
if (propertyCount <= 0)
|
||||
return "";
|
||||
|
||||
for (const CollectionProperty &property : std::as_const(d->properties))
|
||||
content += property.name + ',';
|
||||
|
||||
gotoNextLine();
|
||||
|
||||
for (const QJsonArray &record : std::as_const(d->dataRecords)) {
|
||||
const int valueCount = std::min(int(record.count()), propertyCount);
|
||||
int i = 0;
|
||||
for (; i < valueCount; ++i) {
|
||||
const QJsonValue &value = record.at(i);
|
||||
|
||||
if (value.isDouble())
|
||||
content += QString::number(value.toDouble()) + ',';
|
||||
else if (value.isBool())
|
||||
content += value.toBool() ? "true," : "false,";
|
||||
else
|
||||
content += value.toString() + ',';
|
||||
}
|
||||
|
||||
for (; i < propertyCount; ++i)
|
||||
content += ',';
|
||||
|
||||
gotoNextLine();
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
QJsonObject CollectionDetails::toLocalJson() const
|
||||
{
|
||||
QJsonObject collectionObject;
|
||||
QJsonArray columnsArray;
|
||||
QJsonArray dataArray;
|
||||
|
||||
for (const CollectionProperty &property : std::as_const(d->properties)) {
|
||||
QJsonObject columnObject;
|
||||
columnObject.insert("name", property.name);
|
||||
columnObject.insert("type", CollectionDataTypeModel::dataTypeToString(property.type));
|
||||
columnsArray.append(columnObject);
|
||||
}
|
||||
|
||||
for (const QJsonArray &record : std::as_const(d->dataRecords))
|
||||
dataArray.append(record);
|
||||
|
||||
collectionObject.insert("columns", columnsArray);
|
||||
collectionObject.insert("data", dataArray);
|
||||
|
||||
return collectionObject;
|
||||
}
|
||||
|
||||
void CollectionDetails::registerDeclarativeType()
|
||||
{
|
||||
typedef CollectionDetails::DataType DataType;
|
||||
qRegisterMetaType<DataType>("DataType");
|
||||
qmlRegisterUncreatableType<CollectionDetails>("CollectionDetails", 1, 0, "DataType", "Enum type");
|
||||
|
||||
qRegisterMetaType<DataTypeWarning::Warning>("Warning");
|
||||
qmlRegisterUncreatableType<DataTypeWarning>("CollectionDetails", 1, 0, "Warning", "Enum type");
|
||||
}
|
||||
|
||||
CollectionDetails CollectionDetails::fromImportedCsv(const QByteArray &document,
|
||||
const bool &firstRowIsHeader)
|
||||
{
|
||||
QStringList headers;
|
||||
QJsonArray importedArray;
|
||||
|
||||
QTextStream stream(document);
|
||||
stream.setEncoding(QStringConverter::Latin1);
|
||||
|
||||
if (firstRowIsHeader && !stream.atEnd()) {
|
||||
headers = Utils::transform(csvReadLine(stream.readLine()),
|
||||
[](const QString &value) -> QString { return value.trimmed(); });
|
||||
}
|
||||
|
||||
while (!stream.atEnd()) {
|
||||
const QStringList recordDataList = csvReadLine(stream.readLine());
|
||||
int column = -1;
|
||||
QJsonObject recordData;
|
||||
for (const QString &cellData : recordDataList) {
|
||||
if (++column == headers.size()) {
|
||||
QString proposalName;
|
||||
int proposalId = column;
|
||||
do
|
||||
proposalName = QString("Column %1").arg(++proposalId);
|
||||
while (headers.contains(proposalName));
|
||||
headers.append(proposalName);
|
||||
}
|
||||
recordData.insert(headers.at(column), cellData);
|
||||
}
|
||||
importedArray.append(recordData);
|
||||
}
|
||||
|
||||
return fromImportedJson(importedArray, headers);
|
||||
}
|
||||
|
||||
QList<CollectionDetails> CollectionDetails::fromImportedJson(const QByteArray &jsonContent,
|
||||
QJsonParseError *error)
|
||||
{
|
||||
QJsonParseError parseError;
|
||||
|
||||
QList<CollectionObject> collectionObjects = JsonCollectionParser::parseCollectionObjects(jsonContent,
|
||||
error);
|
||||
|
||||
if (error)
|
||||
*error = parseError;
|
||||
|
||||
if (parseError.error != QJsonParseError::NoError)
|
||||
return {};
|
||||
return Utils::transform(collectionObjects, [](const CollectionObject &object) {
|
||||
CollectionDetails result = fromImportedJson(object.array, object.propertyOrder);
|
||||
result.d->reference.name = object.name;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
CollectionDetails CollectionDetails::fromLocalJson(const QJsonDocument &document,
|
||||
const QString &collectionName,
|
||||
CollectionParseError *error)
|
||||
{
|
||||
auto setError = [&error](CollectionParseError::ParseError parseError) {
|
||||
if (error)
|
||||
error->errorNo = parseError;
|
||||
};
|
||||
|
||||
setError(CollectionParseError::NoError);
|
||||
|
||||
if (document.isObject()) {
|
||||
QJsonObject collectionMap = document.object();
|
||||
if (collectionMap.contains(collectionName)) {
|
||||
QJsonValue collectionValue = collectionMap.value(collectionName);
|
||||
if (collectionValue.isObject())
|
||||
return fromLocalCollection(collectionValue.toObject());
|
||||
else
|
||||
setError(CollectionParseError::CollectionIsNotObject);
|
||||
} else {
|
||||
setError(CollectionParseError::CollectionNameNotFound);
|
||||
}
|
||||
} else {
|
||||
setError(CollectionParseError::MainObjectMissing);
|
||||
}
|
||||
|
||||
return CollectionDetails{};
|
||||
}
|
||||
|
||||
CollectionDetails &CollectionDetails::operator=(const CollectionDetails &other)
|
||||
{
|
||||
CollectionDetails value(other);
|
||||
swap(value);
|
||||
return *this;
|
||||
}
|
||||
|
||||
void CollectionDetails::markChanged()
|
||||
{
|
||||
d->isChanged = true;
|
||||
}
|
||||
|
||||
void CollectionDetails::insertRecords(const QJsonArray &record, int idx, int count)
|
||||
{
|
||||
if (count < 1)
|
||||
return;
|
||||
|
||||
QJsonArray localRecord;
|
||||
const int columnsCount = columns();
|
||||
for (int i = 0; i < columnsCount; i++) {
|
||||
const QJsonValue originalCellData = record.at(i);
|
||||
if (originalCellData.isArray())
|
||||
localRecord.append({});
|
||||
else
|
||||
localRecord.append(originalCellData);
|
||||
}
|
||||
|
||||
if (idx > d->dataRecords.size() || idx < 0)
|
||||
idx = d->dataRecords.size();
|
||||
|
||||
d->dataRecords.insert(idx, count, localRecord);
|
||||
}
|
||||
|
||||
CollectionDetails CollectionDetails::fromImportedJson(const QJsonArray &importedArray,
|
||||
const QStringList &propertyPriority)
|
||||
{
|
||||
QList<CollectionProperty> columnData = getColumnsFromImportedJsonArray(importedArray);
|
||||
if (!propertyPriority.isEmpty()) {
|
||||
QMap<QString, int> priorityMap;
|
||||
for (const QString &propertyName : propertyPriority) {
|
||||
if (!priorityMap.contains(propertyName))
|
||||
priorityMap.insert(propertyName, priorityMap.size());
|
||||
}
|
||||
const int lowestPriority = priorityMap.size();
|
||||
|
||||
Utils::sort(columnData, [&](const CollectionProperty &a, const CollectionProperty &b) {
|
||||
return priorityMap.value(a.name, lowestPriority)
|
||||
< priorityMap.value(b.name, lowestPriority);
|
||||
});
|
||||
}
|
||||
|
||||
QList<QJsonArray> localJsonArray;
|
||||
for (const QJsonValue &importedRowValue : importedArray) {
|
||||
QJsonObject importedRowObject = importedRowValue.toObject();
|
||||
QJsonArray localRow;
|
||||
for (const CollectionProperty &property : columnData)
|
||||
localRow.append(importedRowObject.value(property.name));
|
||||
localJsonArray.append(localRow);
|
||||
}
|
||||
CollectionDetails result;
|
||||
result.d->properties = columnData;
|
||||
result.d->dataRecords = localJsonArray;
|
||||
result.markSaved();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
CollectionDetails CollectionDetails::fromLocalCollection(const QJsonObject &localCollection,
|
||||
CollectionParseError *error)
|
||||
{
|
||||
auto setError = [&error](CollectionParseError::ParseError parseError) {
|
||||
if (error)
|
||||
error->errorNo = parseError;
|
||||
};
|
||||
|
||||
CollectionDetails result;
|
||||
setError(CollectionParseError::NoError);
|
||||
|
||||
if (localCollection.contains("columns")) {
|
||||
const QJsonValue columnsValue = localCollection.value("columns");
|
||||
if (columnsValue.isArray()) {
|
||||
const QJsonArray columns = columnsValue.toArray();
|
||||
for (const QJsonValue &columnValue : columns) {
|
||||
if (columnValue.isObject()) {
|
||||
const QJsonObject column = columnValue.toObject();
|
||||
const QString columnName = column.value("name").toString();
|
||||
if (!columnName.isEmpty()) {
|
||||
result.insertColumn(columnName,
|
||||
-1,
|
||||
{},
|
||||
CollectionDataTypeModel::dataTypeFromString(
|
||||
column.value("type").toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (int columnsCount = result.columns()) {
|
||||
const QJsonArray dataRecords = localCollection.value("data").toArray();
|
||||
for (const QJsonValue &dataRecordValue : dataRecords) {
|
||||
QJsonArray dataRecord = dataRecordValue.toArray();
|
||||
while (dataRecord.count() > columnsCount)
|
||||
dataRecord.removeLast();
|
||||
|
||||
result.insertRecords(dataRecord);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setError(CollectionParseError::ColumnsBlockIsNotArray);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace QmlDesigner
|
@@ -1,152 +0,0 @@
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "modelnode.h"
|
||||
|
||||
#include <QSharedPointer>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QJsonObject;
|
||||
struct QJsonParseError;
|
||||
class QVariant;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
struct CollectionReference
|
||||
{
|
||||
ModelNode node;
|
||||
QString name;
|
||||
|
||||
friend auto qHash(const CollectionReference &collection)
|
||||
{
|
||||
return qHash(collection.node) ^ ::qHash(collection.name);
|
||||
}
|
||||
|
||||
bool operator==(const CollectionReference &other) const
|
||||
{
|
||||
return node == other.node && name == other.name;
|
||||
}
|
||||
|
||||
bool operator!=(const CollectionReference &other) const { return !(*this == other); }
|
||||
};
|
||||
|
||||
struct CollectionProperty;
|
||||
|
||||
struct DataTypeWarning {
|
||||
public:
|
||||
enum Warning { None, CellDataTypeMismatch };
|
||||
Q_ENUM(Warning)
|
||||
|
||||
Warning warning = None;
|
||||
DataTypeWarning(Warning warning)
|
||||
: warning(warning)
|
||||
{}
|
||||
|
||||
static QString getDataTypeWarningString(Warning warning)
|
||||
{
|
||||
return dataTypeWarnings.value(warning);
|
||||
}
|
||||
|
||||
private:
|
||||
Q_GADGET
|
||||
static const QMap<Warning, QString> dataTypeWarnings;
|
||||
};
|
||||
|
||||
struct CollectionParseError
|
||||
{
|
||||
enum ParseError {
|
||||
NoError,
|
||||
MainObjectMissing,
|
||||
CollectionNameNotFound,
|
||||
CollectionIsNotObject,
|
||||
ColumnsBlockIsNotArray,
|
||||
UnknownError
|
||||
};
|
||||
|
||||
ParseError errorNo = ParseError::NoError;
|
||||
QString errorString() const;
|
||||
};
|
||||
|
||||
class CollectionDetails
|
||||
{
|
||||
Q_GADGET
|
||||
|
||||
public:
|
||||
enum class DataType { Unknown, String, Url, Integer, Real, Boolean, Image, Color };
|
||||
Q_ENUM(DataType)
|
||||
|
||||
explicit CollectionDetails();
|
||||
CollectionDetails(const CollectionReference &reference);
|
||||
CollectionDetails(const CollectionDetails &other);
|
||||
~CollectionDetails();
|
||||
|
||||
void resetData(const QJsonDocument &localDocument,
|
||||
const QString &collectionToImport,
|
||||
CollectionParseError *error = nullptr);
|
||||
|
||||
void insertColumn(const QString &propertyName,
|
||||
int colIdx = -1,
|
||||
const QVariant &defaultValue = {},
|
||||
DataType type = DataType::String);
|
||||
bool removeColumns(int colIdx, int count = 1);
|
||||
|
||||
void insertEmptyRows(int row = 0, int count = 1);
|
||||
bool removeRows(int row, int count = 1);
|
||||
bool setPropertyValue(int row, int column, const QVariant &value);
|
||||
|
||||
bool setPropertyName(int column, const QString &value);
|
||||
bool setPropertyType(int column, DataType type);
|
||||
|
||||
CollectionReference reference() const;
|
||||
QVariant data(int row, int column) const;
|
||||
QString propertyAt(int column) const;
|
||||
DataType typeAt(int column) const;
|
||||
DataType typeAt(int row, int column) const;
|
||||
DataTypeWarning::Warning cellWarningCheck(int row, int column) const;
|
||||
bool containsPropertyName(const QString &propertyName) const;
|
||||
|
||||
bool hasValidReference() const;
|
||||
bool isChanged() const;
|
||||
|
||||
int columns() const;
|
||||
int rows() const;
|
||||
|
||||
bool markSaved();
|
||||
|
||||
void swap(CollectionDetails &other);
|
||||
void resetReference(const CollectionReference &reference);
|
||||
|
||||
QString toJson() const;
|
||||
QString toCsv() const;
|
||||
QJsonObject toLocalJson() const;
|
||||
|
||||
static void registerDeclarativeType();
|
||||
|
||||
static CollectionDetails fromImportedCsv(const QByteArray &document,
|
||||
const bool &firstRowIsHeader = true);
|
||||
static QList<CollectionDetails> fromImportedJson(const QByteArray &jsonContent,
|
||||
QJsonParseError *error = nullptr);
|
||||
static CollectionDetails fromLocalJson(const QJsonDocument &document,
|
||||
const QString &collectionName,
|
||||
CollectionParseError *error = nullptr);
|
||||
|
||||
CollectionDetails &operator=(const CollectionDetails &other);
|
||||
|
||||
private:
|
||||
void markChanged();
|
||||
void insertRecords(const QJsonArray &record, int idx = -1, int count = 1);
|
||||
|
||||
static CollectionDetails fromImportedJson(const QJsonArray &importedArray,
|
||||
const QStringList &propertyPriority = {});
|
||||
static CollectionDetails fromLocalCollection(const QJsonObject &localCollection,
|
||||
CollectionParseError *error = nullptr);
|
||||
|
||||
// The private data is supposed to be shared between the copies
|
||||
class Private;
|
||||
QSharedPointer<Private> d;
|
||||
};
|
||||
|
||||
} // namespace QmlDesigner
|
@@ -1,632 +0,0 @@
|
||||
// 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 "collectiondetailsmodel.h"
|
||||
|
||||
#include "collectiondatatypemodel.h"
|
||||
#include "collectioneditorutils.h"
|
||||
#include "modelnode.h"
|
||||
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
|
||||
#include <utils/fileutils.h>
|
||||
#include <utils/qtcassert.h>
|
||||
#include <utils/textfileformat.h>
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
CollectionDetailsModel::CollectionDetailsModel(QObject *parent)
|
||||
: QAbstractTableModel(parent)
|
||||
{
|
||||
connect(this, &CollectionDetailsModel::modelReset, this, &CollectionDetailsModel::updateEmpty);
|
||||
connect(this, &CollectionDetailsModel::rowsInserted, this, &CollectionDetailsModel::updateEmpty);
|
||||
connect(this, &CollectionDetailsModel::rowsRemoved, this, &CollectionDetailsModel::updateEmpty);
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> CollectionDetailsModel::roleNames() const
|
||||
{
|
||||
static QHash<int, QByteArray> roles;
|
||||
if (roles.isEmpty()) {
|
||||
roles.insert(QAbstractTableModel::roleNames());
|
||||
roles.insert(SelectedRole, "itemSelected");
|
||||
roles.insert(DataTypeRole, "dataType");
|
||||
roles.insert(ColumnDataTypeRole, "columnType");
|
||||
roles.insert(DataTypeWarningRole, "dataTypeWarning");
|
||||
}
|
||||
return roles;
|
||||
}
|
||||
|
||||
int CollectionDetailsModel::rowCount([[maybe_unused]] const QModelIndex &parent) const
|
||||
{
|
||||
return m_currentCollection.rows();
|
||||
}
|
||||
|
||||
int CollectionDetailsModel::columnCount([[maybe_unused]] const QModelIndex &parent) const
|
||||
{
|
||||
return m_currentCollection.columns();
|
||||
}
|
||||
|
||||
QVariant CollectionDetailsModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return {};
|
||||
|
||||
QTC_ASSERT(m_currentCollection.hasValidReference(), return {});
|
||||
|
||||
if (role == SelectedRole)
|
||||
return (index.column() == m_selectedColumn || index.row() == m_selectedRow);
|
||||
|
||||
if (role == DataTypeRole)
|
||||
return QVariant::fromValue(m_currentCollection.typeAt(index.row(), index.column()));
|
||||
|
||||
if (role == ColumnDataTypeRole)
|
||||
return QVariant::fromValue(m_currentCollection.typeAt(index.column()));
|
||||
|
||||
if (role == Qt::EditRole)
|
||||
return m_currentCollection.data(index.row(), index.column());
|
||||
|
||||
if (role == DataTypeWarningRole )
|
||||
return QVariant::fromValue(m_currentCollection.cellWarningCheck(index.row(), index.column()));
|
||||
|
||||
return m_currentCollection.data(index.row(), index.column()).toString();
|
||||
}
|
||||
|
||||
bool CollectionDetailsModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
||||
{
|
||||
QTC_ASSERT(m_currentCollection.hasValidReference(), return false);
|
||||
|
||||
if (!index.isValid())
|
||||
return {};
|
||||
|
||||
if (role == Qt::EditRole) {
|
||||
DataTypeWarning::Warning prevWarning = m_currentCollection.cellWarningCheck(index.row(), index.column());
|
||||
bool changed = m_currentCollection.setPropertyValue(index.row(), index.column(), value);
|
||||
|
||||
if (changed) {
|
||||
QList<int> roles = {Qt::DisplayRole, Qt::EditRole};
|
||||
|
||||
if (prevWarning != m_currentCollection.cellWarningCheck(index.row(), index.column()))
|
||||
roles << DataTypeWarningRole;
|
||||
|
||||
setHasUnsavedChanges(true);
|
||||
emit dataChanged(index, index, roles);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CollectionDetailsModel::setHeaderData(int section,
|
||||
Qt::Orientation orientation,
|
||||
const QVariant &value,
|
||||
[[maybe_unused]] int role)
|
||||
{
|
||||
QTC_ASSERT(m_currentCollection.hasValidReference(), return false);
|
||||
|
||||
if (orientation == Qt::Vertical)
|
||||
return false;
|
||||
|
||||
bool headerChanged = m_currentCollection.setPropertyName(section, value.toString());
|
||||
if (headerChanged)
|
||||
emit this->headerDataChanged(orientation, section, section);
|
||||
|
||||
return headerChanged;
|
||||
}
|
||||
|
||||
bool CollectionDetailsModel::insertRows(int row, int count, [[maybe_unused]] const QModelIndex &parent)
|
||||
{
|
||||
QTC_ASSERT(m_currentCollection.hasValidReference(), return false);
|
||||
|
||||
if (count < 1)
|
||||
return false;
|
||||
|
||||
row = qBound(0, row, rowCount());
|
||||
|
||||
beginInsertRows({}, row, row + count - 1);
|
||||
m_currentCollection.insertEmptyRows(row, count);
|
||||
endInsertRows();
|
||||
setHasUnsavedChanges(true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CollectionDetailsModel::removeColumns(int column, int count, const QModelIndex &parent)
|
||||
{
|
||||
QTC_ASSERT(m_currentCollection.hasValidReference(), return false);
|
||||
|
||||
if (column < 0 || column >= columnCount(parent) || count < 1)
|
||||
return false;
|
||||
|
||||
count = std::min(count, columnCount(parent) - column);
|
||||
beginRemoveColumns(parent, column, column + count - 1);
|
||||
bool columnsRemoved = m_currentCollection.removeColumns(column, count);
|
||||
endRemoveColumns();
|
||||
|
||||
if (!columnCount(parent))
|
||||
removeRows(0, rowCount(parent), parent);
|
||||
|
||||
ensureSingleCell();
|
||||
setHasUnsavedChanges(true);
|
||||
return columnsRemoved;
|
||||
}
|
||||
|
||||
bool CollectionDetailsModel::removeRows(int row, int count, const QModelIndex &parent)
|
||||
{
|
||||
QTC_ASSERT(m_currentCollection.hasValidReference(), return false);
|
||||
|
||||
if (row < 0 || row >= rowCount(parent) || count < 1)
|
||||
return false;
|
||||
|
||||
count = std::min(count, rowCount(parent) - row);
|
||||
beginRemoveRows(parent, row, row + count - 1);
|
||||
bool rowsRemoved = m_currentCollection.removeRows(row, count);
|
||||
endRemoveRows();
|
||||
|
||||
ensureSingleCell();
|
||||
setHasUnsavedChanges(true);
|
||||
return rowsRemoved;
|
||||
}
|
||||
|
||||
Qt::ItemFlags CollectionDetailsModel::flags(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return {};
|
||||
|
||||
return {Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable};
|
||||
}
|
||||
|
||||
QVariant CollectionDetailsModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
if (orientation == Qt::Horizontal) {
|
||||
if (role == DataTypeRole)
|
||||
return CollectionDataTypeModel::dataTypeToString(m_currentCollection.typeAt(section));
|
||||
else
|
||||
return m_currentCollection.propertyAt(section);
|
||||
}
|
||||
|
||||
if (orientation == Qt::Vertical)
|
||||
return section + 1;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
CollectionDetails::DataType CollectionDetailsModel::propertyDataType(int column) const
|
||||
{
|
||||
QTC_ASSERT(m_currentCollection.hasValidReference(), return CollectionDetails::DataType::String);
|
||||
|
||||
return m_currentCollection.typeAt(column);
|
||||
}
|
||||
|
||||
int CollectionDetailsModel::selectedColumn() const
|
||||
{
|
||||
return m_selectedColumn;
|
||||
}
|
||||
|
||||
int CollectionDetailsModel::selectedRow() const
|
||||
{
|
||||
return m_selectedRow;
|
||||
}
|
||||
|
||||
QString CollectionDetailsModel::propertyName(int column) const
|
||||
{
|
||||
QTC_ASSERT(m_currentCollection.hasValidReference(), return {});
|
||||
|
||||
return m_currentCollection.propertyAt(column);
|
||||
}
|
||||
|
||||
QString CollectionDetailsModel::propertyType(int column) const
|
||||
{
|
||||
QTC_ASSERT(m_currentCollection.hasValidReference(), return {});
|
||||
|
||||
return CollectionDataTypeModel::dataTypeToString(m_currentCollection.typeAt(column));
|
||||
}
|
||||
|
||||
bool CollectionDetailsModel::isPropertyAvailable(const QString &name)
|
||||
{
|
||||
QTC_ASSERT(m_currentCollection.hasValidReference(), return false);
|
||||
|
||||
return m_currentCollection.containsPropertyName(name);
|
||||
}
|
||||
|
||||
bool CollectionDetailsModel::addColumn(int column, const QString &name, const QString &propertyType)
|
||||
{
|
||||
QTC_ASSERT(m_currentCollection.hasValidReference(), return false);
|
||||
|
||||
if (m_currentCollection.containsPropertyName(name))
|
||||
return false;
|
||||
|
||||
if (column < 0 || column > columnCount())
|
||||
column = columnCount();
|
||||
|
||||
beginInsertColumns({}, column, column);
|
||||
m_currentCollection.insertColumn(name,
|
||||
column,
|
||||
{},
|
||||
CollectionDataTypeModel::dataTypeFromString(propertyType));
|
||||
endInsertColumns();
|
||||
setHasUnsavedChanges(true);
|
||||
return m_currentCollection.containsPropertyName(name);
|
||||
}
|
||||
|
||||
bool CollectionDetailsModel::selectColumn(int section)
|
||||
{
|
||||
if (m_selectedColumn == section)
|
||||
return false;
|
||||
|
||||
const int columns = columnCount();
|
||||
|
||||
if (section >= columns)
|
||||
section = columns - 1;
|
||||
|
||||
selectRow(-1);
|
||||
|
||||
const int rows = rowCount();
|
||||
const int previousColumn = m_selectedColumn;
|
||||
|
||||
m_selectedColumn = section;
|
||||
emit this->selectedColumnChanged(m_selectedColumn);
|
||||
|
||||
auto notifySelectedDataChanged = [this, columns, rows](int notifyingColumn) {
|
||||
if (notifyingColumn > -1 && notifyingColumn < columns && rows) {
|
||||
emit dataChanged(index(0, notifyingColumn),
|
||||
index(rows - 1, notifyingColumn),
|
||||
{SelectedRole});
|
||||
}
|
||||
};
|
||||
|
||||
notifySelectedDataChanged(previousColumn);
|
||||
notifySelectedDataChanged(m_selectedColumn);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CollectionDetailsModel::renameColumn(int section, const QString &newValue)
|
||||
{
|
||||
return setHeaderData(section, Qt::Horizontal, newValue);
|
||||
}
|
||||
|
||||
bool CollectionDetailsModel::setPropertyType(int column, const QString &newValue)
|
||||
{
|
||||
QTC_ASSERT(m_currentCollection.hasValidReference(), return false);
|
||||
|
||||
bool changed = m_currentCollection.setPropertyType(column,
|
||||
CollectionDataTypeModel::dataTypeFromString(
|
||||
newValue));
|
||||
if (changed) {
|
||||
emit headerDataChanged(Qt::Horizontal, column, column);
|
||||
emit dataChanged(
|
||||
index(0, column),
|
||||
index(rowCount() - 1, column),
|
||||
{Qt::DisplayRole, Qt::EditRole, DataTypeRole, DataTypeWarningRole, ColumnDataTypeRole});
|
||||
}
|
||||
|
||||
setHasUnsavedChanges(true);
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool CollectionDetailsModel::selectRow(int row)
|
||||
{
|
||||
if (m_selectedRow == row)
|
||||
return false;
|
||||
|
||||
const int rows = rowCount();
|
||||
|
||||
if (row >= rows)
|
||||
row = rows - 1;
|
||||
|
||||
selectColumn(-1);
|
||||
|
||||
const int columns = columnCount();
|
||||
const int previousRow = m_selectedRow;
|
||||
|
||||
m_selectedRow = row;
|
||||
emit this->selectedRowChanged(m_selectedRow);
|
||||
|
||||
auto notifySelectedDataChanged = [this, rows, columns](int notifyingRow) {
|
||||
if (notifyingRow > -1 && notifyingRow < rows && columns)
|
||||
emit dataChanged(index(notifyingRow, 0), index(notifyingRow, columns - 1), {SelectedRole});
|
||||
};
|
||||
|
||||
notifySelectedDataChanged(previousRow);
|
||||
notifySelectedDataChanged(m_selectedRow);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void CollectionDetailsModel::deselectAll()
|
||||
{
|
||||
selectColumn(-1);
|
||||
selectRow(-1);
|
||||
}
|
||||
|
||||
void CollectionDetailsModel::loadCollection(const ModelNode &sourceNode, const QString &collection)
|
||||
{
|
||||
QString fileName = CollectionEditorUtils::getSourceCollectionPath(sourceNode);
|
||||
|
||||
CollectionReference newReference{sourceNode, collection};
|
||||
bool alreadyOpen = m_openedCollections.contains(newReference);
|
||||
|
||||
if (alreadyOpen) {
|
||||
if (m_currentCollection.reference() != newReference) {
|
||||
deselectAll();
|
||||
beginResetModel();
|
||||
switchToCollection(newReference);
|
||||
ensureSingleCell();
|
||||
endResetModel();
|
||||
}
|
||||
} else {
|
||||
deselectAll();
|
||||
switchToCollection(newReference);
|
||||
loadJsonCollection(fileName, collection);
|
||||
}
|
||||
}
|
||||
|
||||
void CollectionDetailsModel::removeCollection(const ModelNode &sourceNode, const QString &collection)
|
||||
{
|
||||
CollectionReference collectionRef{sourceNode, collection};
|
||||
if (!m_openedCollections.contains(collectionRef))
|
||||
return;
|
||||
|
||||
if (m_currentCollection.reference() == collectionRef)
|
||||
loadCollection({}, {});
|
||||
|
||||
m_openedCollections.remove(collectionRef);
|
||||
}
|
||||
|
||||
void CollectionDetailsModel::removeAllCollections()
|
||||
{
|
||||
loadCollection({}, {});
|
||||
m_openedCollections.clear();
|
||||
}
|
||||
|
||||
void CollectionDetailsModel::renameCollection(const ModelNode &sourceNode,
|
||||
const QString &oldName,
|
||||
const QString &newName)
|
||||
{
|
||||
CollectionReference oldRef{sourceNode, oldName};
|
||||
if (!m_openedCollections.contains(oldRef))
|
||||
return;
|
||||
|
||||
CollectionReference newReference{sourceNode, newName};
|
||||
bool collectionIsSelected = m_currentCollection.reference() == oldRef;
|
||||
CollectionDetails collection = m_openedCollections.take(oldRef);
|
||||
collection.resetReference(newReference);
|
||||
m_openedCollections.insert(newReference, collection);
|
||||
|
||||
if (collectionIsSelected)
|
||||
setCollectionName(newName);
|
||||
}
|
||||
|
||||
bool CollectionDetailsModel::saveDataStoreCollections()
|
||||
{
|
||||
const ModelNode node = m_currentCollection.reference().node;
|
||||
Utils::expected_str<QByteArray> jsonContents = m_jsonFilePath.fileContents();
|
||||
if (!jsonContents.has_value()) {
|
||||
qWarning() << __FUNCTION__ << jsonContents.error();
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonParseError jpe;
|
||||
QJsonDocument document = QJsonDocument::fromJson(jsonContents.value(), &jpe);
|
||||
|
||||
if (jpe.error == QJsonParseError::NoError) {
|
||||
QJsonObject obj = document.object();
|
||||
|
||||
QList<CollectionDetails> collectionsToBeSaved;
|
||||
for (CollectionDetails &openedCollection : m_openedCollections) {
|
||||
const CollectionReference reference = openedCollection.reference();
|
||||
if (reference.node == node) {
|
||||
obj.insert(reference.name, openedCollection.toLocalJson());
|
||||
collectionsToBeSaved << openedCollection;
|
||||
}
|
||||
}
|
||||
|
||||
document.setObject(obj);
|
||||
|
||||
if (CollectionEditorUtils::writeToJsonDocument(m_jsonFilePath, document)) {
|
||||
const CollectionReference currentReference = m_currentCollection.reference();
|
||||
for (CollectionDetails &collection : collectionsToBeSaved) {
|
||||
collection.markSaved();
|
||||
const CollectionReference reference = collection.reference();
|
||||
if (reference != currentReference)
|
||||
closeCollectionIfSaved(reference);
|
||||
}
|
||||
setHasUnsavedChanges(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CollectionDetailsModel::exportCollection(const QUrl &url)
|
||||
{
|
||||
using Core::EditorManager;
|
||||
using Utils::FilePath;
|
||||
using Utils::TextFileFormat;
|
||||
|
||||
QTC_ASSERT(m_currentCollection.hasValidReference(), return false);
|
||||
|
||||
bool saved = false;
|
||||
const FilePath filePath = FilePath::fromUserInput(url.isLocalFile() ? url.toLocalFile()
|
||||
: url.toString());
|
||||
const QString saveFormat = filePath.toFileInfo().suffix().toLower();
|
||||
const QString content = saveFormat == "csv" ? m_currentCollection.toCsv()
|
||||
: m_currentCollection.toJson();
|
||||
|
||||
TextFileFormat textFileFormat;
|
||||
textFileFormat.codec = EditorManager::defaultTextCodec();
|
||||
textFileFormat.lineTerminationMode = EditorManager::defaultLineEnding();
|
||||
QString errorMessage;
|
||||
saved = textFileFormat.writeFile(filePath, content, &errorMessage);
|
||||
|
||||
if (!saved)
|
||||
qWarning() << Q_FUNC_INFO << "Unable to write file" << errorMessage;
|
||||
|
||||
return saved;
|
||||
}
|
||||
|
||||
const CollectionDetails CollectionDetailsModel::upToDateConstCollection(
|
||||
const CollectionReference &reference) const
|
||||
{
|
||||
using Utils::FilePath;
|
||||
using Utils::FileReader;
|
||||
CollectionDetails collection;
|
||||
|
||||
if (m_openedCollections.contains(reference)) {
|
||||
collection = m_openedCollections.value(reference);
|
||||
} else {
|
||||
QUrl url = CollectionEditorUtils::getSourceCollectionPath(reference.node);
|
||||
FilePath path = FilePath::fromUserInput(url.isLocalFile() ? url.toLocalFile()
|
||||
: url.toString());
|
||||
FileReader file;
|
||||
|
||||
if (!file.fetch(path))
|
||||
return collection;
|
||||
|
||||
QJsonParseError jpe;
|
||||
QJsonDocument document = QJsonDocument::fromJson(file.data(), &jpe);
|
||||
|
||||
if (jpe.error != QJsonParseError::NoError)
|
||||
return collection;
|
||||
|
||||
collection = CollectionDetails::fromLocalJson(document, reference.name);
|
||||
collection.resetReference(reference);
|
||||
}
|
||||
return collection;
|
||||
}
|
||||
|
||||
bool CollectionDetailsModel::collectionHasColumn(const CollectionReference &reference,
|
||||
const QString &columnName) const
|
||||
{
|
||||
const CollectionDetails collection = upToDateConstCollection(reference);
|
||||
return collection.containsPropertyName(columnName);
|
||||
}
|
||||
|
||||
QString CollectionDetailsModel::getFirstColumnName(const CollectionReference &reference) const
|
||||
{
|
||||
const CollectionDetails collection = upToDateConstCollection(reference);
|
||||
return collection.propertyAt(0);
|
||||
}
|
||||
|
||||
void CollectionDetailsModel::updateEmpty()
|
||||
{
|
||||
bool isEmptyNow = rowCount() == 0;
|
||||
if (m_isEmpty != isEmptyNow) {
|
||||
m_isEmpty = isEmptyNow;
|
||||
emit isEmptyChanged(m_isEmpty);
|
||||
}
|
||||
}
|
||||
|
||||
void CollectionDetailsModel::switchToCollection(const CollectionReference &collection)
|
||||
{
|
||||
if (m_currentCollection.reference() == collection)
|
||||
return;
|
||||
|
||||
closeCurrentCollectionIfSaved();
|
||||
|
||||
if (!m_openedCollections.contains(collection))
|
||||
m_openedCollections.insert(collection, CollectionDetails(collection));
|
||||
|
||||
m_currentCollection = m_openedCollections.value(collection);
|
||||
|
||||
setCollectionName(collection.name);
|
||||
}
|
||||
|
||||
void CollectionDetailsModel::closeCollectionIfSaved(const CollectionReference &collection)
|
||||
{
|
||||
if (!m_openedCollections.contains(collection))
|
||||
return;
|
||||
|
||||
const CollectionDetails &collectionDetails = m_openedCollections.value(collection);
|
||||
|
||||
if (!collectionDetails.isChanged())
|
||||
m_openedCollections.remove(collection);
|
||||
}
|
||||
|
||||
void CollectionDetailsModel::closeCurrentCollectionIfSaved()
|
||||
{
|
||||
if (m_currentCollection.hasValidReference()) {
|
||||
closeCollectionIfSaved(m_currentCollection.reference());
|
||||
m_currentCollection = CollectionDetails{};
|
||||
}
|
||||
}
|
||||
|
||||
void CollectionDetailsModel::loadJsonCollection(const QString &filePath, const QString &collection)
|
||||
{
|
||||
QJsonDocument document = readJsonFile(filePath);
|
||||
|
||||
beginResetModel();
|
||||
m_currentCollection.resetData(document, collection);
|
||||
ensureSingleCell();
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void CollectionDetailsModel::ensureSingleCell()
|
||||
{
|
||||
if (!m_currentCollection.hasValidReference())
|
||||
return;
|
||||
|
||||
if (!columnCount())
|
||||
addColumn(0, "Column 1", "String");
|
||||
|
||||
if (!rowCount())
|
||||
insertRow(0);
|
||||
|
||||
updateEmpty();
|
||||
}
|
||||
|
||||
QJsonDocument CollectionDetailsModel::readJsonFile(const QUrl &url)
|
||||
{
|
||||
using Utils::FilePath;
|
||||
using Utils::FileReader;
|
||||
FilePath path = FilePath::fromUserInput(url.isLocalFile() ? url.toLocalFile() : url.toString());
|
||||
FileReader file;
|
||||
|
||||
if (!file.fetch(path)) {
|
||||
emit warning(tr("File reading problem"), file.errorString());
|
||||
return {};
|
||||
}
|
||||
|
||||
QJsonParseError jpe;
|
||||
QJsonDocument document = QJsonDocument::fromJson(file.data(), &jpe);
|
||||
|
||||
if (jpe.error != QJsonParseError::NoError)
|
||||
emit warning(tr("Json parse error"), jpe.errorString());
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
void CollectionDetailsModel::setCollectionName(const QString &newCollectionName)
|
||||
{
|
||||
if (m_collectionName != newCollectionName) {
|
||||
m_collectionName = newCollectionName;
|
||||
emit this->collectionNameChanged(m_collectionName);
|
||||
}
|
||||
}
|
||||
|
||||
QString CollectionDetailsModel::warningToString(DataTypeWarning::Warning warning) const
|
||||
{
|
||||
return DataTypeWarning::getDataTypeWarningString(warning);
|
||||
}
|
||||
|
||||
void CollectionDetailsModel::setJsonFilePath(const Utils::FilePath &filePath)
|
||||
{
|
||||
m_jsonFilePath = filePath;
|
||||
}
|
||||
|
||||
void CollectionDetailsModel::setHasUnsavedChanges(bool val)
|
||||
{
|
||||
if (m_hasUnsavedChanges == val)
|
||||
return;
|
||||
m_hasUnsavedChanges = val;
|
||||
emit hasUnsavedChangesChanged();
|
||||
}
|
||||
|
||||
} // namespace QmlDesigner
|
@@ -1,110 +0,0 @@
|
||||
// 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 "collectiondetails.h"
|
||||
|
||||
#include <utils/fileutils.h>
|
||||
|
||||
#include <QAbstractTableModel>
|
||||
#include <QHash>
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
class ModelNode;
|
||||
|
||||
class CollectionDetailsModel : public QAbstractTableModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QString collectionName MEMBER m_collectionName NOTIFY collectionNameChanged)
|
||||
Q_PROPERTY(int selectedColumn READ selectedColumn WRITE selectColumn NOTIFY selectedColumnChanged)
|
||||
Q_PROPERTY(int selectedRow READ selectedRow WRITE selectRow NOTIFY selectedRowChanged)
|
||||
Q_PROPERTY(bool isEmpty MEMBER m_isEmpty NOTIFY isEmptyChanged)
|
||||
Q_PROPERTY(bool hasUnsavedChanges MEMBER m_hasUnsavedChanges WRITE setHasUnsavedChanges NOTIFY hasUnsavedChangesChanged)
|
||||
|
||||
public:
|
||||
enum DataRoles { SelectedRole = Qt::UserRole + 1, DataTypeRole, ColumnDataTypeRole, DataTypeWarningRole };
|
||||
explicit CollectionDetailsModel(QObject *parent = nullptr);
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
int rowCount(const QModelIndex &parent = {}) const override;
|
||||
int columnCount(const QModelIndex &parent = {}) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
|
||||
bool setHeaderData(int section,
|
||||
Qt::Orientation orientation,
|
||||
const QVariant &value,
|
||||
int role = Qt::EditRole) override;
|
||||
bool insertRows(int row, int count, const QModelIndex &parent = {}) override;
|
||||
bool removeColumns(int column, int count, const QModelIndex &parent = {}) override;
|
||||
bool removeRows(int row, int count, const QModelIndex &parent = {}) override;
|
||||
|
||||
Qt::ItemFlags flags(const QModelIndex &index) const override;
|
||||
QVariant headerData(int section,
|
||||
Qt::Orientation orientation,
|
||||
int role = Qt::DisplayRole) const override;
|
||||
|
||||
CollectionDetails::DataType propertyDataType(int column) const;
|
||||
|
||||
int selectedColumn() const;
|
||||
int selectedRow() const;
|
||||
Q_INVOKABLE QString propertyName(int column) const;
|
||||
Q_INVOKABLE QString propertyType(int column) const;
|
||||
|
||||
Q_INVOKABLE bool isPropertyAvailable(const QString &name);
|
||||
Q_INVOKABLE bool addColumn(int column, const QString &name, const QString &propertyType = {});
|
||||
Q_INVOKABLE bool selectColumn(int section);
|
||||
Q_INVOKABLE bool renameColumn(int section, const QString &newValue);
|
||||
Q_INVOKABLE bool setPropertyType(int column, const QString &newValue);
|
||||
Q_INVOKABLE bool selectRow(int row);
|
||||
Q_INVOKABLE void deselectAll();
|
||||
Q_INVOKABLE QString warningToString(DataTypeWarning::Warning warning) const;
|
||||
|
||||
void setJsonFilePath(const Utils::FilePath &filePath);
|
||||
void loadCollection(const ModelNode &sourceNode, const QString &collection);
|
||||
void removeCollection(const ModelNode &sourceNode, const QString &collection);
|
||||
void removeAllCollections();
|
||||
void renameCollection(const ModelNode &sourceNode, const QString &oldName, const QString &newName);
|
||||
|
||||
Q_INVOKABLE bool saveDataStoreCollections();
|
||||
Q_INVOKABLE bool exportCollection(const QUrl &url);
|
||||
|
||||
const CollectionDetails upToDateConstCollection(const CollectionReference &reference) const;
|
||||
bool collectionHasColumn(const CollectionReference &reference, const QString &columnName) const;
|
||||
QString getFirstColumnName(const CollectionReference &reference) const;
|
||||
void setHasUnsavedChanges(bool val);
|
||||
|
||||
signals:
|
||||
void collectionNameChanged(const QString &collectionName);
|
||||
void selectedColumnChanged(int);
|
||||
void selectedRowChanged(int);
|
||||
void isEmptyChanged(bool);
|
||||
void hasUnsavedChangesChanged();
|
||||
void warning(const QString &title, const QString &body);
|
||||
|
||||
private slots:
|
||||
void updateEmpty();
|
||||
|
||||
private:
|
||||
void switchToCollection(const CollectionReference &collection);
|
||||
void closeCollectionIfSaved(const CollectionReference &collection);
|
||||
void closeCurrentCollectionIfSaved();
|
||||
void setCollectionName(const QString &newCollectionName);
|
||||
void loadJsonCollection(const QString &filePath, const QString &collection);
|
||||
void ensureSingleCell();
|
||||
QJsonDocument readJsonFile(const QUrl &url);
|
||||
|
||||
Utils::FilePath m_jsonFilePath;
|
||||
QHash<CollectionReference, CollectionDetails> m_openedCollections;
|
||||
CollectionDetails m_currentCollection;
|
||||
bool m_isEmpty = true;
|
||||
bool m_hasUnsavedChanges = false;
|
||||
int m_selectedColumn = -1;
|
||||
int m_selectedRow = -1;
|
||||
|
||||
QString m_collectionName;
|
||||
};
|
||||
|
||||
} // namespace QmlDesigner
|
@@ -1,163 +0,0 @@
|
||||
// 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 "collectiondetailssortfiltermodel.h"
|
||||
|
||||
#include "collectiondetailsmodel.h"
|
||||
#include "collectioneditorutils.h"
|
||||
|
||||
#include <utils/qtcassert.h>
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
CollectionDetailsSortFilterModel::CollectionDetailsSortFilterModel(QObject *parent)
|
||||
: QSortFilterProxyModel(parent)
|
||||
{
|
||||
connect(this, &CollectionDetailsSortFilterModel::rowsInserted,
|
||||
this, &CollectionDetailsSortFilterModel::updateRowCountChanges);
|
||||
connect(this, &CollectionDetailsSortFilterModel::rowsRemoved,
|
||||
this, &CollectionDetailsSortFilterModel::updateRowCountChanges);
|
||||
connect(this, &CollectionDetailsSortFilterModel::modelReset,
|
||||
this, &CollectionDetailsSortFilterModel::updateRowCountChanges);
|
||||
|
||||
setDynamicSortFilter(true);
|
||||
}
|
||||
|
||||
void CollectionDetailsSortFilterModel::setSourceModel(CollectionDetailsModel *model)
|
||||
{
|
||||
m_source = model;
|
||||
Super::setSourceModel(model);
|
||||
connect(m_source, &CollectionDetailsModel::selectedColumnChanged,
|
||||
this, &CollectionDetailsSortFilterModel::updateSelectedColumn);
|
||||
|
||||
connect(m_source, &CollectionDetailsModel::selectedRowChanged,
|
||||
this, &CollectionDetailsSortFilterModel::updateSelectedRow);
|
||||
}
|
||||
|
||||
int CollectionDetailsSortFilterModel::selectedRow() const
|
||||
{
|
||||
QTC_ASSERT(m_source, return -1);
|
||||
|
||||
return mapFromSource(m_source->index(m_source->selectedRow(), 0)).row();
|
||||
}
|
||||
|
||||
int CollectionDetailsSortFilterModel::selectedColumn() const
|
||||
{
|
||||
QTC_ASSERT(m_source, return -1);
|
||||
|
||||
return mapFromSource(m_source->index(0, m_source->selectedColumn())).column();
|
||||
}
|
||||
|
||||
bool CollectionDetailsSortFilterModel::selectRow(int row)
|
||||
{
|
||||
QTC_ASSERT(m_source, return false);
|
||||
|
||||
return m_source->selectRow(mapToSource(index(row, 0)).row());
|
||||
}
|
||||
|
||||
bool CollectionDetailsSortFilterModel::selectColumn(int column)
|
||||
{
|
||||
QTC_ASSERT(m_source, return false);
|
||||
|
||||
return m_source->selectColumn(mapToSource(index(0, column)).column());
|
||||
}
|
||||
|
||||
void CollectionDetailsSortFilterModel::deselectAll()
|
||||
{
|
||||
QTC_ASSERT(m_source, return);
|
||||
m_source->deselectAll();
|
||||
}
|
||||
|
||||
CollectionDetailsSortFilterModel::~CollectionDetailsSortFilterModel() = default;
|
||||
|
||||
bool CollectionDetailsSortFilterModel::filterAcceptsRow(int sourceRow,
|
||||
const QModelIndex &sourceParent) const
|
||||
{
|
||||
QTC_ASSERT(m_source, return false);
|
||||
QModelIndex sourceIndex(m_source->index(sourceRow, 0, sourceParent));
|
||||
return sourceIndex.isValid();
|
||||
}
|
||||
|
||||
bool CollectionDetailsSortFilterModel::lessThan(const QModelIndex &sourceleft,
|
||||
const QModelIndex &sourceRight) const
|
||||
{
|
||||
QTC_ASSERT(m_source, return false);
|
||||
|
||||
if (sourceleft.column() == sourceRight.column()) {
|
||||
int column = sourceleft.column();
|
||||
CollectionDetails::DataType columnType = m_source->propertyDataType(column);
|
||||
return CollectionEditorUtils::variantIslessThan(sourceleft.data(),
|
||||
sourceRight.data(),
|
||||
columnType);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void CollectionDetailsSortFilterModel::updateEmpty()
|
||||
{
|
||||
bool newValue = rowCount() == 0;
|
||||
if (m_isEmpty != newValue) {
|
||||
m_isEmpty = newValue;
|
||||
emit isEmptyChanged(m_isEmpty);
|
||||
}
|
||||
}
|
||||
|
||||
void CollectionDetailsSortFilterModel::updateSelectedRow()
|
||||
{
|
||||
const int upToDateSelectedRow = selectedRow();
|
||||
if (m_selectedRow == upToDateSelectedRow)
|
||||
return;
|
||||
|
||||
const int rows = rowCount();
|
||||
const int columns = columnCount();
|
||||
const int previousRow = m_selectedRow;
|
||||
|
||||
m_selectedRow = upToDateSelectedRow;
|
||||
emit this->selectedRowChanged(m_selectedRow);
|
||||
|
||||
auto notifySelectedDataChanged = [this, rows, columns](int notifyingRow) {
|
||||
if (notifyingRow > -1 && notifyingRow < rows && columns) {
|
||||
emit dataChanged(index(notifyingRow, 0),
|
||||
index(notifyingRow, columns - 1),
|
||||
{CollectionDetailsModel::SelectedRole});
|
||||
}
|
||||
};
|
||||
|
||||
notifySelectedDataChanged(previousRow);
|
||||
notifySelectedDataChanged(m_selectedRow);
|
||||
}
|
||||
|
||||
void CollectionDetailsSortFilterModel::updateSelectedColumn()
|
||||
{
|
||||
const int upToDateSelectedColumn = selectedColumn();
|
||||
if (m_selectedColumn == upToDateSelectedColumn)
|
||||
return;
|
||||
|
||||
const int rows = rowCount();
|
||||
const int columns = columnCount();
|
||||
const int previousColumn = m_selectedColumn;
|
||||
|
||||
m_selectedColumn = upToDateSelectedColumn;
|
||||
emit this->selectedColumnChanged(m_selectedColumn);
|
||||
|
||||
auto notifySelectedDataChanged = [this, rows, columns](int notifyingCol) {
|
||||
if (notifyingCol > -1 && notifyingCol < columns && rows) {
|
||||
emit dataChanged(index(0, notifyingCol),
|
||||
index(rows - 1, notifyingCol),
|
||||
{CollectionDetailsModel::SelectedRole});
|
||||
}
|
||||
};
|
||||
|
||||
notifySelectedDataChanged(previousColumn);
|
||||
notifySelectedDataChanged(m_selectedColumn);
|
||||
}
|
||||
|
||||
void CollectionDetailsSortFilterModel::updateRowCountChanges()
|
||||
{
|
||||
updateEmpty();
|
||||
updateSelectedRow();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
} // namespace QmlDesigner
|
@@ -1,58 +0,0 @@
|
||||
// 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 <QPointer>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
class CollectionDetailsModel;
|
||||
|
||||
class CollectionDetailsSortFilterModel : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(int selectedColumn READ selectedColumn WRITE selectColumn NOTIFY selectedColumnChanged)
|
||||
Q_PROPERTY(int selectedRow READ selectedRow WRITE selectRow NOTIFY selectedRowChanged)
|
||||
Q_PROPERTY(bool isEmpty MEMBER m_isEmpty NOTIFY isEmptyChanged)
|
||||
|
||||
using Super = QSortFilterProxyModel;
|
||||
|
||||
public:
|
||||
explicit CollectionDetailsSortFilterModel(QObject *parent = nullptr);
|
||||
virtual ~CollectionDetailsSortFilterModel();
|
||||
|
||||
void setSourceModel(CollectionDetailsModel *model);
|
||||
|
||||
int selectedRow() const;
|
||||
int selectedColumn() const;
|
||||
|
||||
Q_INVOKABLE bool selectRow(int row);
|
||||
Q_INVOKABLE bool selectColumn(int column);
|
||||
Q_INVOKABLE void deselectAll();
|
||||
|
||||
signals:
|
||||
void selectedColumnChanged(int);
|
||||
void selectedRowChanged(int);
|
||||
void isEmptyChanged(bool);
|
||||
|
||||
protected:
|
||||
using Super::setSourceModel;
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
|
||||
bool lessThan(const QModelIndex &sourceleft, const QModelIndex &sourceRight) const override;
|
||||
|
||||
private:
|
||||
void updateEmpty();
|
||||
void updateSelectedRow();
|
||||
void updateSelectedColumn();
|
||||
void updateRowCountChanges();
|
||||
|
||||
QPointer<CollectionDetailsModel> m_source;
|
||||
int m_selectedColumn = -1;
|
||||
int m_selectedRow = -1;
|
||||
bool m_isEmpty = true;
|
||||
};
|
||||
|
||||
} // namespace QmlDesigner
|
@@ -1,24 +0,0 @@
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace QmlDesigner::CollectionEditorConstants {
|
||||
|
||||
enum class SourceFormat { Unknown, Json };
|
||||
|
||||
inline constexpr char SOURCEFILE_PROPERTY[] = "source";
|
||||
inline constexpr char ALLMODELS_PROPERTY[] = "allModels";
|
||||
inline constexpr char JSONCHILDMODELNAME_PROPERTY[] = "modelName";
|
||||
|
||||
inline constexpr char COLLECTIONMODEL_IMPORT[] = "QtQuick.Studio.Utils";
|
||||
inline constexpr char JSONCOLLECTIONMODEL_TYPENAME[] = "QtQuick.Studio.Utils.JsonListModel";
|
||||
inline constexpr char JSONCOLLECTIONCHILDMODEL_TYPENAME[] = "QtQuick.Studio.Utils.ChildListModel";
|
||||
inline constexpr char JSONBACKEND_TYPENAME[] = "JsonData";
|
||||
|
||||
inline constexpr QStringView DEFAULT_DATA_JSON_FILENAME = u"data.json";
|
||||
inline constexpr QStringView DEFAULT_MODELS_JSON_FILENAME = u"models.json";
|
||||
inline constexpr QStringView DEFAULT_DATASTORE_QML_FILENAME = u"DataStore.qml";
|
||||
inline constexpr QStringView DEFAULT_JSONDATA_QML_FILENAME = u"JsonData.qml";
|
||||
|
||||
} // namespace QmlDesigner::CollectionEditorConstants
|
@@ -1,218 +0,0 @@
|
||||
// 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 "collectioneditorutils.h"
|
||||
|
||||
#include "collectiondatatypemodel.h"
|
||||
#include "collectioneditorconstants.h"
|
||||
#include "model.h"
|
||||
#include "nodemetainfo.h"
|
||||
#include "propertymetainfo.h"
|
||||
#include "variantproperty.h"
|
||||
|
||||
#include <coreplugin/documentmanager.h>
|
||||
#include <coreplugin/icore.h>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectexplorer.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <qmljs/qmljsmodelmanagerinterface.h>
|
||||
#include <utils/qtcassert.h>
|
||||
|
||||
#include <variant>
|
||||
|
||||
#include <QColor>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QJsonValue>
|
||||
#include <QUrl>
|
||||
|
||||
using DataType = QmlDesigner::CollectionDetails::DataType;
|
||||
|
||||
namespace {
|
||||
|
||||
using CollectionDataVariant = std::variant<QString, bool, double, int, QUrl, QColor>;
|
||||
|
||||
inline bool operator<(const QColor &a, const QColor &b)
|
||||
{
|
||||
return a.name(QColor::HexArgb) < b.name(QColor::HexArgb);
|
||||
}
|
||||
|
||||
inline CollectionDataVariant valueToVariant(const QVariant &value, DataType type)
|
||||
{
|
||||
switch (type) {
|
||||
case DataType::String:
|
||||
return value.toString();
|
||||
case DataType::Real:
|
||||
return value.toDouble();
|
||||
case DataType::Integer:
|
||||
return value.toInt();
|
||||
case DataType::Boolean:
|
||||
return value.toBool();
|
||||
case DataType::Color:
|
||||
return value.value<QColor>();
|
||||
case DataType::Image:
|
||||
case DataType::Url:
|
||||
return value.value<QUrl>();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
struct LessThanVisitor
|
||||
{
|
||||
template<typename T1, typename T2>
|
||||
bool operator()(const T1 &a, const T2 &b) const
|
||||
{
|
||||
return CollectionDataVariant(a).index() < CollectionDataVariant(b).index();
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
bool operator()(const T &a, const T &b) const
|
||||
{
|
||||
return a < b;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace QmlDesigner::CollectionEditorUtils {
|
||||
|
||||
bool variantIslessThan(const QVariant &a, const QVariant &b, DataType type)
|
||||
{
|
||||
return std::visit(LessThanVisitor{}, valueToVariant(a, type), valueToVariant(b, type));
|
||||
}
|
||||
|
||||
bool canAcceptCollectionAsModel(const ModelNode &node)
|
||||
{
|
||||
const NodeMetaInfo nodeMetaInfo = node.metaInfo();
|
||||
if (!nodeMetaInfo.isValid())
|
||||
return false;
|
||||
|
||||
const PropertyMetaInfo modelProperty = nodeMetaInfo.property("model");
|
||||
if (!modelProperty.isValid())
|
||||
return false;
|
||||
|
||||
return modelProperty.isWritable() && !modelProperty.isPrivate()
|
||||
&& modelProperty.propertyType().isVariant();
|
||||
}
|
||||
|
||||
bool hasTextRoleProperty(const ModelNode &node)
|
||||
{
|
||||
const NodeMetaInfo nodeMetaInfo = node.metaInfo();
|
||||
if (!nodeMetaInfo.isValid())
|
||||
return false;
|
||||
|
||||
const PropertyMetaInfo textRoleProperty = nodeMetaInfo.property("textRole");
|
||||
if (!textRoleProperty.isValid())
|
||||
return false;
|
||||
|
||||
return textRoleProperty.isWritable() && !textRoleProperty.isPrivate()
|
||||
&& textRoleProperty.propertyType().isString();
|
||||
}
|
||||
|
||||
QString getSourceCollectionPath(const ModelNode &dataStoreNode)
|
||||
{
|
||||
using Utils::FilePath;
|
||||
if (!dataStoreNode.isValid())
|
||||
return {};
|
||||
|
||||
const QUrl dataStoreUrl = dataStoreNode.model()->fileUrl();
|
||||
QUrl sourceValue = dataStoreNode.property("source").toVariantProperty().value().toUrl();
|
||||
|
||||
QUrl sourceUrl = sourceValue.isRelative() ? dataStoreUrl.resolved(sourceValue) : sourceValue;
|
||||
|
||||
const FilePath expectedFile = FilePath::fromUrl(sourceUrl);
|
||||
|
||||
if (expectedFile.isFile() && expectedFile.exists())
|
||||
return expectedFile.toFSPathString();
|
||||
|
||||
const FilePath defaultJsonFile = FilePath::fromUrl(
|
||||
dataStoreUrl.resolved(CollectionEditorConstants::DEFAULT_MODELS_JSON_FILENAME.toString()));
|
||||
|
||||
if (defaultJsonFile.exists())
|
||||
return defaultJsonFile.toFSPathString();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
QJsonObject defaultCollection()
|
||||
{
|
||||
QJsonObject collectionObject;
|
||||
|
||||
QJsonArray columns;
|
||||
QJsonObject defaultColumn;
|
||||
defaultColumn.insert("name", "Column 1");
|
||||
defaultColumn.insert("type", CollectionDataTypeModel::dataTypeToString(DataType::String));
|
||||
columns.append(defaultColumn);
|
||||
|
||||
QJsonArray collectionData;
|
||||
QJsonArray cellData;
|
||||
cellData.append(QString{});
|
||||
collectionData.append(cellData);
|
||||
|
||||
collectionObject.insert("columns", columns);
|
||||
collectionObject.insert("data", collectionData);
|
||||
|
||||
return collectionObject;
|
||||
}
|
||||
|
||||
QJsonObject defaultColorCollection()
|
||||
{
|
||||
using Utils::FilePath;
|
||||
using Utils::FileReader;
|
||||
const FilePath templatePath = findFile(Core::ICore::resourcePath(), "Colors.json.tpl");
|
||||
|
||||
FileReader fileReader;
|
||||
if (!fileReader.fetch(templatePath)) {
|
||||
qWarning() << __FUNCTION__ << "Can't read the content of the file" << templatePath;
|
||||
return {};
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QList<CollectionDetails> collections = CollectionDetails::fromImportedJson(fileReader.data(),
|
||||
&parseError);
|
||||
|
||||
if (parseError.error != QJsonParseError::NoError) {
|
||||
qWarning() << __FUNCTION__ << "Error in template file" << parseError.errorString();
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!collections.size()) {
|
||||
qWarning() << __FUNCTION__ << "Can not generate collections from template file!";
|
||||
return {};
|
||||
}
|
||||
|
||||
const CollectionDetails collection = collections.first();
|
||||
return collection.toLocalJson();
|
||||
}
|
||||
|
||||
Utils::FilePath findFile(const Utils::FilePath &path, const QString &fileName)
|
||||
{
|
||||
QDirIterator it(path.toString(), QDirIterator::Subdirectories);
|
||||
|
||||
while (it.hasNext()) {
|
||||
QFileInfo file(it.next());
|
||||
if (file.isDir())
|
||||
continue;
|
||||
|
||||
if (file.fileName() == fileName)
|
||||
return Utils::FilePath::fromFileInfo(file);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool writeToJsonDocument(const Utils::FilePath &path, const QJsonDocument &document, QString *errorString)
|
||||
{
|
||||
Core::FileChangeBlocker fileBlocker(path);
|
||||
Utils::FileSaver jsonFile(path);
|
||||
if (jsonFile.write(document.toJson()))
|
||||
jsonFile.finalize();
|
||||
if (errorString)
|
||||
*errorString = jsonFile.errorString();
|
||||
|
||||
return !jsonFile.hasError();
|
||||
}
|
||||
|
||||
} // namespace QmlDesigner::CollectionEditorUtils
|
@@ -1,37 +0,0 @@
|
||||
// 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 "collectiondetails.h"
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QJsonArray;
|
||||
class QJsonObject;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
namespace Utils {
|
||||
class FilePath;
|
||||
}
|
||||
|
||||
namespace QmlDesigner::CollectionEditorUtils {
|
||||
|
||||
bool variantIslessThan(const QVariant &a, const QVariant &b, CollectionDetails::DataType type);
|
||||
|
||||
QString getSourceCollectionPath(const QmlDesigner::ModelNode &dataStoreNode);
|
||||
|
||||
Utils::FilePath findFile(const Utils::FilePath &path, const QString &fileName);
|
||||
|
||||
bool writeToJsonDocument(const Utils::FilePath &path,
|
||||
const QJsonDocument &document,
|
||||
QString *errorString = nullptr);
|
||||
|
||||
bool canAcceptCollectionAsModel(const ModelNode &node);
|
||||
|
||||
bool hasTextRoleProperty(const ModelNode &node);
|
||||
|
||||
QJsonObject defaultCollection();
|
||||
|
||||
QJsonObject defaultColorCollection();
|
||||
|
||||
} // namespace QmlDesigner::CollectionEditorUtils
|
@@ -1,257 +0,0 @@
|
||||
// Copyright (C) 2024 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#include "collectionjsonparser.h"
|
||||
|
||||
#include <qmljs/parser/qmljsast_p.h>
|
||||
#include <qmljs/qmljsdocument.h>
|
||||
#include <utils/algorithm.h>
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
/**
|
||||
* @brief A json object is a plain object, if it has only primitive properties (not arrays or objects)
|
||||
* @return true if @param jsonObject is a plain object
|
||||
*/
|
||||
inline static bool isPlainObject(const QJsonObject &jsonObj)
|
||||
{
|
||||
return !Utils::anyOf(jsonObj, [](const QJsonValueConstRef &val) {
|
||||
return val.isArray() || val.isObject();
|
||||
});
|
||||
}
|
||||
|
||||
static bool isPlainObject(const QJsonValueConstRef &value)
|
||||
{
|
||||
if (!value.isObject())
|
||||
return false;
|
||||
return isPlainObject(value.toObject());
|
||||
}
|
||||
|
||||
static QJsonArray parsePlainObject(const QJsonObject &jsonObj)
|
||||
{
|
||||
QJsonObject result;
|
||||
auto item = jsonObj.constBegin();
|
||||
const auto itemEnd = jsonObj.constEnd();
|
||||
while (item != itemEnd) {
|
||||
QJsonValueConstRef ref = item.value();
|
||||
if (!ref.isArray() && !ref.isObject())
|
||||
result.insert(item.key(), ref);
|
||||
++item;
|
||||
}
|
||||
if (!result.isEmpty())
|
||||
return QJsonArray{result};
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static QJsonArray parseArray(const QJsonArray &array,
|
||||
QList<CollectionObject> &plainCollections,
|
||||
JsonKeyChain &chainTracker)
|
||||
{
|
||||
chainTracker.append(0);
|
||||
QJsonArray plainArray;
|
||||
int i = -1;
|
||||
for (const QJsonValueConstRef &item : array) {
|
||||
chainTracker.last() = ++i;
|
||||
if (isPlainObject(item)) {
|
||||
const QJsonObject plainObject = item.toObject();
|
||||
if (plainObject.count())
|
||||
plainArray.append(plainObject);
|
||||
} else if (item.isArray()) {
|
||||
parseArray(item.toArray(), plainCollections, chainTracker);
|
||||
}
|
||||
}
|
||||
chainTracker.removeLast();
|
||||
return plainArray;
|
||||
}
|
||||
|
||||
static void parseObject(const QJsonObject &jsonObj,
|
||||
QList<CollectionObject> &plainCollections,
|
||||
JsonKeyChain &chainTracker)
|
||||
{
|
||||
chainTracker.append(QString{});
|
||||
auto item = jsonObj.constBegin();
|
||||
const auto itemEnd = jsonObj.constEnd();
|
||||
while (item != itemEnd) {
|
||||
chainTracker.last() = item.key();
|
||||
QJsonValueConstRef ref = item.value();
|
||||
QJsonArray parsedArray;
|
||||
if (ref.isArray()) {
|
||||
parsedArray = parseArray(ref.toArray(), plainCollections, chainTracker);
|
||||
} else if (ref.isObject()) {
|
||||
if (isPlainObject(ref))
|
||||
parsedArray = parsePlainObject(ref.toObject());
|
||||
else
|
||||
parseObject(ref.toObject(), plainCollections, chainTracker);
|
||||
}
|
||||
if (!parsedArray.isEmpty())
|
||||
plainCollections.append({item.key(), parsedArray, chainTracker});
|
||||
++item;
|
||||
}
|
||||
chainTracker.removeLast();
|
||||
}
|
||||
|
||||
static QList<CollectionObject> parseDocument(const QJsonDocument &document,
|
||||
const QString &defaultName = "Model")
|
||||
{
|
||||
QList<CollectionObject> plainCollections;
|
||||
JsonKeyChain chainTracker;
|
||||
if (document.isObject()) {
|
||||
const QJsonObject documentObject = document.object();
|
||||
if (isPlainObject(documentObject)) {
|
||||
QJsonArray parsedArray = parsePlainObject(documentObject);
|
||||
if (!parsedArray.isEmpty())
|
||||
plainCollections.append({defaultName, parsedArray});
|
||||
} else {
|
||||
parseObject(document.object(), plainCollections, chainTracker);
|
||||
}
|
||||
} else {
|
||||
QJsonArray parsedArray = parseArray(document.array(), plainCollections, chainTracker);
|
||||
if (!parsedArray.isEmpty())
|
||||
plainCollections.append({defaultName, parsedArray, {0}});
|
||||
}
|
||||
return plainCollections;
|
||||
}
|
||||
|
||||
QList<CollectionObject> JsonCollectionParser::parseCollectionObjects(const QByteArray &json,
|
||||
QJsonParseError *error)
|
||||
{
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument document = QJsonDocument::fromJson(json, &parseError);
|
||||
if (error)
|
||||
*error = parseError;
|
||||
|
||||
if (parseError.error != QJsonParseError::NoError)
|
||||
return {};
|
||||
|
||||
QList<CollectionObject> allCollections = parseDocument(document);
|
||||
QList<JsonKeyChain> keyChains = Utils::transform(allCollections, [](const CollectionObject &obj) {
|
||||
return obj.keyChain;
|
||||
});
|
||||
|
||||
JsonCollectionParser jsonVisitor(QString::fromLatin1(json), keyChains);
|
||||
|
||||
for (CollectionObject &collection : allCollections)
|
||||
collection.propertyOrder = jsonVisitor.collectionPaths.value(collection.keyChain);
|
||||
|
||||
return allCollections;
|
||||
}
|
||||
|
||||
JsonCollectionParser::JsonCollectionParser(const QString &jsonContent,
|
||||
const QList<JsonKeyChain> &keyChains)
|
||||
{
|
||||
for (const JsonKeyChain &chain : keyChains)
|
||||
collectionPaths.insert(chain, {});
|
||||
|
||||
QmlJS::Document::MutablePtr newDoc = QmlJS::Document::create(Utils::FilePath::fromString(
|
||||
"<expression>"),
|
||||
QmlJS::Dialect::Json);
|
||||
|
||||
newDoc->setSource(jsonContent);
|
||||
newDoc->parseExpression();
|
||||
|
||||
if (!newDoc->isParsedCorrectly())
|
||||
return;
|
||||
|
||||
newDoc->ast()->accept(this);
|
||||
}
|
||||
|
||||
bool JsonCollectionParser::visit([[maybe_unused]] QmlJS::AST::ObjectPattern *objectPattern)
|
||||
{
|
||||
propertyOrderStack.push({});
|
||||
return true;
|
||||
}
|
||||
|
||||
void JsonCollectionParser::endVisit([[maybe_unused]] QmlJS::AST::ObjectPattern *objectPattern)
|
||||
|
||||
{
|
||||
if (!propertyOrderStack.isEmpty()) {
|
||||
QStringList objectProperties = propertyOrderStack.top();
|
||||
propertyOrderStack.pop();
|
||||
checkPropertyUpdates(keyStack, objectProperties);
|
||||
}
|
||||
}
|
||||
|
||||
bool JsonCollectionParser::visit(QmlJS::AST::PatternProperty *patternProperty)
|
||||
{
|
||||
const QString propertyName = patternProperty->name->asString();
|
||||
if (!propertyOrderStack.isEmpty())
|
||||
propertyOrderStack.top().append(propertyName);
|
||||
|
||||
keyStack.push(propertyName);
|
||||
return true;
|
||||
}
|
||||
|
||||
void JsonCollectionParser::endVisit(QmlJS::AST::PatternProperty *patternProperty)
|
||||
{
|
||||
const QString propertyName = patternProperty->name->asString();
|
||||
|
||||
if (auto curIndex = std::get_if<QString>(&keyStack.top())) {
|
||||
if (*curIndex == propertyName)
|
||||
keyStack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
bool JsonCollectionParser::visit([[maybe_unused]] QmlJS::AST::PatternElementList *patternElementList)
|
||||
{
|
||||
keyStack.push(-1);
|
||||
return true;
|
||||
}
|
||||
|
||||
void JsonCollectionParser::endVisit([[maybe_unused]] QmlJS::AST::PatternElementList *patternElementList)
|
||||
{
|
||||
if (std::get_if<int>(&keyStack.top()))
|
||||
keyStack.pop();
|
||||
}
|
||||
|
||||
bool JsonCollectionParser::visit([[maybe_unused]] QmlJS::AST::PatternElement *patternElement)
|
||||
{
|
||||
if (auto curIndex = std::get_if<int>(&keyStack.top()))
|
||||
*curIndex += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
void JsonCollectionParser::checkPropertyUpdates(QStack<JsonKey> stack,
|
||||
const QStringList &objectProperties)
|
||||
{
|
||||
bool shouldUpdate = collectionPaths.contains(stack);
|
||||
if (!shouldUpdate && !stack.isEmpty()) {
|
||||
if (std::get_if<int>(&stack.top())) {
|
||||
stack.pop();
|
||||
shouldUpdate = collectionPaths.contains(stack);
|
||||
}
|
||||
}
|
||||
if (!shouldUpdate)
|
||||
return;
|
||||
|
||||
QStringList propertyList = collectionPaths.value(stack);
|
||||
QSet<QString> allKeys;
|
||||
for (const QString &val : std::as_const(propertyList))
|
||||
allKeys.insert(val);
|
||||
|
||||
std::optional<QString> prevVal;
|
||||
for (const QString &val : objectProperties) {
|
||||
if (!allKeys.contains(val)) {
|
||||
if (prevVal.has_value()) {
|
||||
const int idx = propertyList.indexOf(prevVal);
|
||||
propertyList.insert(idx + 1, val);
|
||||
} else {
|
||||
propertyList.append(val);
|
||||
}
|
||||
allKeys.insert(val);
|
||||
}
|
||||
prevVal = val;
|
||||
}
|
||||
collectionPaths.insert(stack, propertyList);
|
||||
}
|
||||
|
||||
void JsonCollectionParser::throwRecursionDepthError()
|
||||
{
|
||||
qWarning() << __FUNCTION__ << "Recursion Depth Error";
|
||||
}
|
||||
|
||||
} // namespace QmlDesigner
|
@@ -1,58 +0,0 @@
|
||||
// Copyright (C) 2024 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <qmljs/parser/qmljsastvisitor_p.h>
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QStack>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
struct QJsonParseError;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
using JsonKey = std::variant<int, QString>; // Key can be either int (index) or string (property name)
|
||||
|
||||
using JsonKeyChain = QList<JsonKey>; // A chain of keys leading to a specific json value
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
struct CollectionObject
|
||||
{
|
||||
QString name;
|
||||
QJsonArray array = {};
|
||||
JsonKeyChain keyChain = {};
|
||||
QStringList propertyOrder = {};
|
||||
};
|
||||
|
||||
class JsonCollectionParser : public QmlJS::AST::Visitor
|
||||
{
|
||||
public:
|
||||
static QList<CollectionObject> parseCollectionObjects(const QByteArray &json,
|
||||
QJsonParseError *error = nullptr);
|
||||
|
||||
private:
|
||||
JsonCollectionParser(const QString &jsonContent, const QList<JsonKeyChain> &keyChains);
|
||||
|
||||
bool visit(QmlJS::AST::ObjectPattern *objectPattern) override;
|
||||
void endVisit(QmlJS::AST::ObjectPattern *objectPattern) override;
|
||||
|
||||
bool visit(QmlJS::AST::PatternProperty *patternProperty) override;
|
||||
void endVisit(QmlJS::AST::PatternProperty *patternProperty) override;
|
||||
|
||||
bool visit(QmlJS::AST::PatternElementList *patternElementList) override;
|
||||
void endVisit(QmlJS::AST::PatternElementList *patternElementList) override;
|
||||
|
||||
bool visit(QmlJS::AST::PatternElement *patternElement) override;
|
||||
|
||||
void checkPropertyUpdates(QStack<JsonKey> stack, const QStringList &objectProperties);
|
||||
|
||||
void throwRecursionDepthError() override;
|
||||
|
||||
QStack<JsonKey> keyStack;
|
||||
QStack<QStringList> propertyOrderStack;
|
||||
QMap<JsonKeyChain, QStringList> collectionPaths; // Key chains, Priorities
|
||||
};
|
||||
|
||||
} // namespace QmlDesigner
|
@@ -1,522 +0,0 @@
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#include "collectionlistmodel.h"
|
||||
|
||||
#include "collectioneditorconstants.h"
|
||||
#include "collectioneditorutils.h"
|
||||
|
||||
#include <utils/algorithm.h>
|
||||
#include <utils/fileutils.h>
|
||||
#include <utils/qtcassert.h>
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
|
||||
namespace {
|
||||
|
||||
template<typename ValueType>
|
||||
bool containsItem(const std::initializer_list<ValueType> &container, const ValueType &value)
|
||||
{
|
||||
auto begin = std::cbegin(container);
|
||||
auto end = std::cend(container);
|
||||
|
||||
auto it = std::find(begin, end, value);
|
||||
return it != end;
|
||||
}
|
||||
|
||||
bool sameCollectionNames(QStringList a, QStringList b)
|
||||
{
|
||||
if (a.size() != b.size())
|
||||
return false;
|
||||
|
||||
a.sort(Qt::CaseSensitive);
|
||||
b.sort(Qt::CaseSensitive);
|
||||
|
||||
return a == b;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
CollectionListModel::CollectionListModel()
|
||||
: QAbstractListModel()
|
||||
{
|
||||
connect(this, &CollectionListModel::modelReset, this, &CollectionListModel::updateEmpty);
|
||||
connect(this, &CollectionListModel::rowsRemoved, this, &CollectionListModel::updateEmpty);
|
||||
connect(this, &CollectionListModel::rowsInserted, this, &CollectionListModel::updateEmpty);
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> CollectionListModel::roleNames() const
|
||||
{
|
||||
static QHash<int, QByteArray> roles;
|
||||
if (roles.isEmpty()) {
|
||||
roles.insert(Super::roleNames());
|
||||
roles.insert({
|
||||
{IdRole, "collectionId"},
|
||||
{NameRole, "collectionName"},
|
||||
{SelectedRole, "collectionIsSelected"},
|
||||
});
|
||||
}
|
||||
return roles;
|
||||
}
|
||||
|
||||
int CollectionListModel::rowCount([[maybe_unused]] const QModelIndex &parent) const
|
||||
{
|
||||
return m_data.count();
|
||||
}
|
||||
|
||||
bool CollectionListModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
||||
{
|
||||
if (!index.isValid())
|
||||
return false;
|
||||
|
||||
if (containsItem<int>({Qt::EditRole, Qt::DisplayRole, NameRole}, role)) {
|
||||
if (collectionExists(value.toString()))
|
||||
return false;
|
||||
|
||||
QString oldName = collectionNameAt(index.row());
|
||||
bool nameChanged = value != data(index);
|
||||
if (nameChanged) {
|
||||
QString newName = value.toString();
|
||||
QString errorString;
|
||||
if (renameCollectionInDataStore(oldName, newName, errorString)) {
|
||||
m_data.replace(index.row(), newName);
|
||||
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole, NameRole});
|
||||
emit this->collectionNameChanged(oldName, newName);
|
||||
if (m_selectedCollectionName == oldName)
|
||||
updateSelectedCollectionName();
|
||||
return true;
|
||||
} else {
|
||||
emit warning("Rename Model", errorString);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (role == SelectedRole) {
|
||||
if (value.toBool() != index.data(SelectedRole).toBool()) {
|
||||
setSelectedIndex(value.toBool() ? index.row() : -1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CollectionListModel::removeRows(int row, int count, const QModelIndex &parent)
|
||||
{
|
||||
const int rows = rowCount(parent);
|
||||
if (row >= rows)
|
||||
return false;
|
||||
|
||||
row = qBound(0, row, rows - 1);
|
||||
count = qBound(0, count, rows - row);
|
||||
|
||||
if (count < 1)
|
||||
return false;
|
||||
|
||||
QString errorString;
|
||||
QStringList removedCollections = m_data.mid(row, count);
|
||||
if (removeCollectionsFromDataStore(removedCollections, errorString)) {
|
||||
beginRemoveRows(parent, row, row + count - 1);
|
||||
m_data.remove(row, count);
|
||||
endRemoveRows();
|
||||
|
||||
emit collectionsRemoved(removedCollections);
|
||||
if (m_selectedIndex >= row) {
|
||||
int preferredIndex = m_selectedIndex - count;
|
||||
if (preferredIndex < 0) // If the selected item is deleted, reset selection
|
||||
selectCollectionIndex(-1);
|
||||
selectCollectionIndex(preferredIndex, true);
|
||||
}
|
||||
|
||||
updateSelectedCollectionName();
|
||||
return true;
|
||||
} else {
|
||||
emit warning("Remove Model", errorString);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
QVariant CollectionListModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
QTC_ASSERT(index.isValid(), return {});
|
||||
|
||||
switch (role) {
|
||||
case IdRole:
|
||||
return index.row();
|
||||
case SelectedRole:
|
||||
return index.row() == m_selectedIndex;
|
||||
case NameRole:
|
||||
default:
|
||||
return m_data.at(index.row());
|
||||
}
|
||||
}
|
||||
|
||||
void CollectionListModel::setDataStoreNode(const ModelNode &dataStoreNode)
|
||||
{
|
||||
m_dataStoreNode = dataStoreNode;
|
||||
update();
|
||||
}
|
||||
|
||||
int CollectionListModel::selectedIndex() const
|
||||
{
|
||||
return m_selectedIndex;
|
||||
}
|
||||
|
||||
ModelNode CollectionListModel::sourceNode() const
|
||||
{
|
||||
return m_dataStoreNode;
|
||||
}
|
||||
|
||||
bool CollectionListModel::collectionExists(const QString &collectionName) const
|
||||
{
|
||||
return m_data.contains(collectionName);
|
||||
}
|
||||
|
||||
QStringList CollectionListModel::collections() const
|
||||
{
|
||||
return m_data;
|
||||
}
|
||||
|
||||
QString CollectionListModel::getUniqueCollectionName(const QString &baseName) const
|
||||
{
|
||||
QString name = baseName.isEmpty() ? "Model" : baseName;
|
||||
QString nameTemplate = name + "%1";
|
||||
|
||||
int num = 0;
|
||||
|
||||
while (collectionExists(name))
|
||||
name = nameTemplate.arg(++num, 2, 10, QChar('0'));
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
void CollectionListModel::selectCollectionIndex(int idx, bool selectAtLeastOne)
|
||||
{
|
||||
int collectionCount = m_data.size();
|
||||
int preferredIndex = -1;
|
||||
if (collectionCount) {
|
||||
if (selectAtLeastOne)
|
||||
preferredIndex = std::max(0, std::min(idx, collectionCount - 1));
|
||||
else if (idx > -1 && idx < collectionCount)
|
||||
preferredIndex = idx;
|
||||
}
|
||||
|
||||
setSelectedIndex(preferredIndex);
|
||||
}
|
||||
|
||||
void CollectionListModel::selectCollectionName(QString collectionName, bool selectAtLeastOne)
|
||||
{
|
||||
int idx = m_data.indexOf(collectionName);
|
||||
if (idx > -1)
|
||||
selectCollectionIndex(idx);
|
||||
else
|
||||
selectCollectionIndex(selectedIndex(), selectAtLeastOne);
|
||||
|
||||
collectionName = collectionNameAt(selectedIndex());
|
||||
if (m_selectedCollectionName == collectionName)
|
||||
return;
|
||||
|
||||
m_selectedCollectionName = collectionName;
|
||||
emit selectedCollectionNameChanged(m_selectedCollectionName);
|
||||
}
|
||||
|
||||
QString CollectionListModel::collectionNameAt(int idx) const
|
||||
{
|
||||
return index(idx).data(NameRole).toString();
|
||||
}
|
||||
|
||||
QString CollectionListModel::selectedCollectionName() const
|
||||
{
|
||||
return m_selectedCollectionName;
|
||||
}
|
||||
|
||||
void CollectionListModel::update()
|
||||
{
|
||||
using Utils::FilePath;
|
||||
using Utils::FileReader;
|
||||
|
||||
FileReader sourceFile;
|
||||
QString sourceFileAddress = CollectionEditorUtils::getSourceCollectionPath(m_dataStoreNode);
|
||||
FilePath path = FilePath::fromUserInput(sourceFileAddress);
|
||||
bool fileRead = false;
|
||||
if (path.exists()) {
|
||||
fileRead = sourceFile.fetch(path);
|
||||
if (!fileRead)
|
||||
emit this->warning(tr("Model Editor"),
|
||||
tr("Cannot read the dataStore file\n%1").arg(sourceFile.errorString()));
|
||||
}
|
||||
|
||||
QStringList collectionNames;
|
||||
if (fileRead) {
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument document = QJsonDocument::fromJson(sourceFile.data(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError) {
|
||||
emit this->warning(tr("Model Editor"),
|
||||
tr("There is an error in the JSON file.\n%1")
|
||||
.arg(parseError.errorString()));
|
||||
} else {
|
||||
if (document.isObject())
|
||||
collectionNames = document.object().toVariantMap().keys();
|
||||
else
|
||||
emit this->warning(tr("Model Editor"), tr("The JSON document be an object."));
|
||||
}
|
||||
}
|
||||
|
||||
if (!sameCollectionNames(m_data, collectionNames)) {
|
||||
QString prevSelectedCollection = selectedIndex() > -1 ? m_data.at(selectedIndex())
|
||||
: QString();
|
||||
beginResetModel();
|
||||
m_data = collectionNames;
|
||||
endResetModel();
|
||||
emit this->collectionNamesChanged(collections());
|
||||
selectCollectionName(prevSelectedCollection, true);
|
||||
}
|
||||
}
|
||||
|
||||
bool CollectionListModel::addCollection(const QString &collectionName,
|
||||
const QJsonObject &localCollection)
|
||||
{
|
||||
if (collectionExists(collectionName)) {
|
||||
emit warning(tr("Add Model"), tr("Model \"%1\" already exists.").arg(collectionName));
|
||||
return false;
|
||||
}
|
||||
|
||||
QString errorMessage;
|
||||
if (addCollectionToDataStore(collectionName, localCollection, errorMessage)) {
|
||||
int row = rowCount();
|
||||
beginInsertRows({}, row, row);
|
||||
m_data.append(collectionName);
|
||||
endInsertRows();
|
||||
|
||||
selectCollectionName(collectionName);
|
||||
emit collectionAdded(collectionName);
|
||||
return true;
|
||||
} else {
|
||||
emit warning(tr("Add Collection"), errorMessage);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void CollectionListModel::setSelectedIndex(int idx)
|
||||
{
|
||||
idx = (idx > -1 && idx < rowCount()) ? idx : -1;
|
||||
|
||||
if (m_selectedIndex != idx) {
|
||||
QModelIndex previousIndex = index(m_selectedIndex);
|
||||
QModelIndex newIndex = index(idx);
|
||||
|
||||
m_selectedIndex = idx;
|
||||
|
||||
if (previousIndex.isValid())
|
||||
emit dataChanged(previousIndex, previousIndex, {SelectedRole});
|
||||
|
||||
if (newIndex.isValid())
|
||||
emit dataChanged(newIndex, newIndex, {SelectedRole});
|
||||
|
||||
emit selectedIndexChanged(idx);
|
||||
updateSelectedCollectionName();
|
||||
}
|
||||
}
|
||||
|
||||
bool CollectionListModel::removeCollectionsFromDataStore(const QStringList &removedCollections,
|
||||
QString &error) const
|
||||
{
|
||||
using Utils::FilePath;
|
||||
using Utils::FileReader;
|
||||
auto setErrorAndReturn = [&error](const QString &msg) -> bool {
|
||||
error = msg;
|
||||
return false;
|
||||
};
|
||||
|
||||
if (m_dataStoreNode.type() != CollectionEditorConstants::JSONCOLLECTIONMODEL_TYPENAME)
|
||||
return setErrorAndReturn(tr("Invalid node type"));
|
||||
|
||||
QString sourceFileAddress = CollectionEditorUtils::getSourceCollectionPath(m_dataStoreNode);
|
||||
|
||||
QFileInfo sourceFileInfo(sourceFileAddress);
|
||||
if (!sourceFileInfo.isFile())
|
||||
return setErrorAndReturn(tr("The selected node has an invalid source address"));
|
||||
|
||||
FilePath jsonPath = FilePath::fromUserInput(sourceFileAddress);
|
||||
FileReader jsonFile;
|
||||
if (!jsonFile.fetch(jsonPath)) {
|
||||
return setErrorAndReturn(tr("Can't read file \"%1\".\n%2")
|
||||
.arg(sourceFileInfo.absoluteFilePath(), jsonFile.errorString()));
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument document = QJsonDocument::fromJson(jsonFile.data(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError) {
|
||||
return setErrorAndReturn(tr("\"%1\" is corrupted.\n%2")
|
||||
.arg(sourceFileInfo.absoluteFilePath(), parseError.errorString()));
|
||||
}
|
||||
|
||||
if (document.isObject()) {
|
||||
QJsonObject rootObject = document.object();
|
||||
|
||||
for (const QString &collectionName : removedCollections) {
|
||||
bool sourceContainsCollection = rootObject.contains(collectionName);
|
||||
if (sourceContainsCollection) {
|
||||
rootObject.remove(collectionName);
|
||||
} else {
|
||||
setErrorAndReturn(tr("The model group doesn't contain the model name (%1).")
|
||||
.arg(sourceContainsCollection));
|
||||
}
|
||||
}
|
||||
|
||||
document.setObject(rootObject);
|
||||
|
||||
if (CollectionEditorUtils::writeToJsonDocument(jsonPath, document)) {
|
||||
error.clear();
|
||||
return true;
|
||||
} else {
|
||||
return setErrorAndReturn(
|
||||
tr("Can't write to \"%1\".").arg(sourceFileInfo.absoluteFilePath()));
|
||||
}
|
||||
} else {
|
||||
return setErrorAndReturn(tr("Local Json Document should be an object"));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CollectionListModel::renameCollectionInDataStore(const QString &oldName,
|
||||
const QString &newName,
|
||||
QString &error)
|
||||
{
|
||||
using Utils::FilePath;
|
||||
using Utils::FileReader;
|
||||
using Utils::FileSaver;
|
||||
|
||||
auto setErrorAndReturn = [&error](const QString &msg) -> bool {
|
||||
error = msg;
|
||||
return false;
|
||||
};
|
||||
|
||||
if (m_dataStoreNode.type() != CollectionEditorConstants::JSONCOLLECTIONMODEL_TYPENAME)
|
||||
return setErrorAndReturn(tr("Invalid node type"));
|
||||
|
||||
QString sourceFileAddress = CollectionEditorUtils::getSourceCollectionPath(m_dataStoreNode);
|
||||
|
||||
QFileInfo sourceFileInfo(sourceFileAddress);
|
||||
if (!sourceFileInfo.isFile())
|
||||
return setErrorAndReturn(tr("Selected node must have a valid source file address"));
|
||||
|
||||
FilePath jsonPath = FilePath::fromUserInput(sourceFileAddress);
|
||||
FileReader jsonFile;
|
||||
if (!jsonFile.fetch(jsonPath)) {
|
||||
return setErrorAndReturn(
|
||||
tr("Can't read \"%1\".\n%2").arg(sourceFileInfo.absoluteFilePath(), jsonFile.errorString()));
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument document = QJsonDocument::fromJson(jsonFile.data(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError) {
|
||||
return setErrorAndReturn(tr("\"%1\" is corrupted.\n%2")
|
||||
.arg(sourceFileInfo.absoluteFilePath(), parseError.errorString()));
|
||||
}
|
||||
|
||||
if (document.isObject()) {
|
||||
QJsonObject rootObject = document.object();
|
||||
|
||||
bool collectionContainsOldName = rootObject.contains(oldName);
|
||||
bool collectionContainsNewName = rootObject.contains(newName);
|
||||
|
||||
if (!collectionContainsOldName) {
|
||||
return setErrorAndReturn(
|
||||
tr("The model group doesn't contain the old model name (%1).").arg(oldName));
|
||||
}
|
||||
|
||||
if (collectionContainsNewName) {
|
||||
return setErrorAndReturn(
|
||||
tr("The model name \"%1\" already exists in the model group.").arg(newName));
|
||||
}
|
||||
|
||||
QJsonValue oldValue = rootObject.value(oldName);
|
||||
rootObject.insert(newName, oldValue);
|
||||
rootObject.remove(oldName);
|
||||
|
||||
document.setObject(rootObject);
|
||||
|
||||
if (CollectionEditorUtils::writeToJsonDocument(jsonPath, document)) {
|
||||
error.clear();
|
||||
return true;
|
||||
} else {
|
||||
return setErrorAndReturn(
|
||||
tr("Can't write to \"%1\".").arg(sourceFileInfo.absoluteFilePath()));
|
||||
}
|
||||
} else {
|
||||
return setErrorAndReturn(tr("Local Json Document should be an object"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CollectionListModel::addCollectionToDataStore(const QString &collectionName,
|
||||
const QJsonObject &localCollection,
|
||||
QString &errorString) const
|
||||
{
|
||||
using Utils::FilePath;
|
||||
using Utils::FileReader;
|
||||
auto returnError = [&errorString](const QString &msg) -> bool {
|
||||
errorString = msg;
|
||||
return false;
|
||||
};
|
||||
|
||||
if (collectionExists(collectionName))
|
||||
return returnError(tr("A model with the identical name already exists."));
|
||||
|
||||
QString sourceFileAddress = CollectionEditorUtils::getSourceCollectionPath(m_dataStoreNode);
|
||||
|
||||
QFileInfo sourceFileInfo(sourceFileAddress);
|
||||
if (!sourceFileInfo.isFile())
|
||||
return returnError(tr("Selected node must have a valid source file address"));
|
||||
|
||||
FilePath jsonPath = FilePath::fromUserInput(sourceFileAddress);
|
||||
FileReader jsonFile;
|
||||
if (!jsonFile.fetch(jsonPath)) {
|
||||
return returnError(
|
||||
tr("Can't read \"%1\".\n%2").arg(sourceFileInfo.absoluteFilePath(), jsonFile.errorString()));
|
||||
}
|
||||
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument document = QJsonDocument::fromJson(jsonFile.data(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError)
|
||||
return returnError(tr("\"%1\" is corrupted.\n%2")
|
||||
.arg(sourceFileInfo.absoluteFilePath(), parseError.errorString()));
|
||||
|
||||
if (document.isObject()) {
|
||||
QJsonObject sourceObject = document.object();
|
||||
sourceObject.insert(collectionName, localCollection);
|
||||
document.setObject(sourceObject);
|
||||
|
||||
if (CollectionEditorUtils::writeToJsonDocument(jsonPath, document))
|
||||
return true;
|
||||
else
|
||||
return returnError(tr("Can't write to \"%1\".").arg(sourceFileInfo.absoluteFilePath()));
|
||||
} else {
|
||||
return returnError(tr("JSON document type should be an object containing models."));
|
||||
}
|
||||
}
|
||||
|
||||
void CollectionListModel::updateEmpty()
|
||||
{
|
||||
bool isEmptyNow = m_data.isEmpty();
|
||||
if (m_isEmpty != isEmptyNow) {
|
||||
m_isEmpty = isEmptyNow;
|
||||
emit isEmptyChanged(m_isEmpty);
|
||||
|
||||
if (m_isEmpty)
|
||||
setSelectedIndex(-1);
|
||||
}
|
||||
}
|
||||
|
||||
void CollectionListModel::updateSelectedCollectionName()
|
||||
{
|
||||
QString selectedCollectionByIndex = collectionNameAt(selectedIndex());
|
||||
if (selectedCollectionByIndex != selectedCollectionName())
|
||||
selectCollectionName(selectedCollectionByIndex);
|
||||
}
|
||||
|
||||
} // namespace QmlDesigner
|
@@ -1,79 +0,0 @@
|
||||
// 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 <QAbstractListModel>
|
||||
#include <QHash>
|
||||
|
||||
#include "modelnode.h"
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
class CollectionListModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(int selectedIndex MEMBER m_selectedIndex NOTIFY selectedIndexChanged)
|
||||
Q_PROPERTY(bool isEmpty MEMBER m_isEmpty NOTIFY isEmptyChanged)
|
||||
Q_PROPERTY(QString selectedCollectionName
|
||||
READ selectedCollectionName
|
||||
WRITE selectCollectionName
|
||||
NOTIFY selectedCollectionNameChanged)
|
||||
|
||||
public:
|
||||
enum Roles { IdRole = Qt::UserRole + 1, NameRole, SelectedRole };
|
||||
|
||||
explicit CollectionListModel();
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
|
||||
bool removeRows(int row, int count, const QModelIndex &parent = {}) override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
void setDataStoreNode(const ModelNode &dataStoreNode = {});
|
||||
|
||||
Q_INVOKABLE int selectedIndex() const;
|
||||
Q_INVOKABLE ModelNode sourceNode() const;
|
||||
Q_INVOKABLE bool collectionExists(const QString &collectionName) const;
|
||||
Q_INVOKABLE QStringList collections() const;
|
||||
Q_INVOKABLE QString getUniqueCollectionName(const QString &baseName = {}) const;
|
||||
|
||||
void selectCollectionIndex(int idx, bool selectAtLeastOne = false);
|
||||
void selectCollectionName(QString collectionName, bool selectAtLeastOne = false);
|
||||
QString collectionNameAt(int idx) const;
|
||||
QString selectedCollectionName() const;
|
||||
|
||||
void update();
|
||||
bool addCollection(const QString &collectionName, const QJsonObject &localCollection);
|
||||
|
||||
signals:
|
||||
void selectedIndexChanged(int idx);
|
||||
void isEmptyChanged(bool);
|
||||
void collectionNameChanged(const QString &oldName, const QString &newName);
|
||||
void collectionNamesChanged(const QStringList &collectionNames);
|
||||
void collectionsRemoved(const QStringList &names);
|
||||
void collectionAdded(const QString &name);
|
||||
void selectedCollectionNameChanged(const QString &selectedCollectionName);
|
||||
void warning(const QString &title, const QString &body);
|
||||
|
||||
private:
|
||||
void setSelectedIndex(int idx);
|
||||
bool removeCollectionsFromDataStore(const QStringList &removedCollections, QString &error) const;
|
||||
bool renameCollectionInDataStore(const QString &oldName, const QString &newName, QString &error);
|
||||
bool addCollectionToDataStore(const QString &collectionName,
|
||||
const QJsonObject &localCollection,
|
||||
QString &errorString) const;
|
||||
|
||||
void updateEmpty();
|
||||
void updateSelectedCollectionName();
|
||||
|
||||
using Super = QAbstractListModel;
|
||||
int m_selectedIndex = -1;
|
||||
bool m_isEmpty = false;
|
||||
ModelNode m_dataStoreNode;
|
||||
QString m_selectedCollectionName;
|
||||
QStringList m_data;
|
||||
};
|
||||
|
||||
} // namespace QmlDesigner
|
@@ -1,686 +0,0 @@
|
||||
// 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 "collectiondatatypemodel.h"
|
||||
#include "collectiondetailsmodel.h"
|
||||
#include "collectioneditorconstants.h"
|
||||
#include "collectioneditorutils.h"
|
||||
#include "collectionlistmodel.h"
|
||||
#include "collectionwidget.h"
|
||||
#include "datastoremodelnode.h"
|
||||
#include "designmodecontext.h"
|
||||
#include "nodeabstractproperty.h"
|
||||
#include "nodemetainfo.h"
|
||||
#include "nodeproperty.h"
|
||||
#include "qmldesignerplugin.h"
|
||||
#include "variantproperty.h"
|
||||
|
||||
#include <designercore/generatedcomponentutils.h>
|
||||
#include <projectexplorer/project.h>
|
||||
#include <projectexplorer/projectexplorer.h>
|
||||
#include <projectexplorer/projectmanager.h>
|
||||
#include <qmljs/qmljsmodelmanagerinterface.h>
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
#include <utils/algorithm.h>
|
||||
#include <utils/qtcassert.h>
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
namespace {
|
||||
|
||||
bool isStudioCollectionModel(const QmlDesigner::ModelNode &node)
|
||||
{
|
||||
return node.metaInfo().isQtQuickStudioUtilsJsonListModel();
|
||||
}
|
||||
|
||||
inline bool isProjectImport(const QmlDesigner::Import &import)
|
||||
{
|
||||
ProjectExplorer::Project *currentProject = ProjectExplorer::ProjectManager::startupProject();
|
||||
return currentProject && import.toString() == currentProject->displayName();
|
||||
}
|
||||
|
||||
inline void setVariantPropertyValue(const QmlDesigner::ModelNode &node,
|
||||
const QmlDesigner::PropertyName &propertyName,
|
||||
const QVariant &value)
|
||||
{
|
||||
QmlDesigner::VariantProperty property = node.variantProperty(propertyName);
|
||||
property.setValue(value);
|
||||
}
|
||||
|
||||
inline void setNodePropertyValue(const QmlDesigner::ModelNode &node,
|
||||
const QmlDesigner::PropertyName &propertyName,
|
||||
const QmlDesigner::ModelNode &value)
|
||||
{
|
||||
QmlDesigner::NodeProperty nodeProperty = node.nodeProperty(propertyName);
|
||||
// Remove the old model node if is available
|
||||
if (nodeProperty.modelNode())
|
||||
nodeProperty.modelNode().destroy();
|
||||
|
||||
nodeProperty.setModelNode(value);
|
||||
}
|
||||
|
||||
inline void setBindingPropertyExpression(const QmlDesigner::ModelNode &node,
|
||||
const QmlDesigner::PropertyName &propertyName,
|
||||
const QString &expression)
|
||||
{
|
||||
QmlDesigner::BindingProperty property = node.bindingProperty(propertyName);
|
||||
property.setExpression(expression);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
CollectionView::CollectionView(ExternalDependenciesInterface &externalDependencies)
|
||||
: AbstractView(externalDependencies)
|
||||
, m_dataStore(std::make_unique<DataStoreModelNode>())
|
||||
|
||||
{}
|
||||
|
||||
CollectionView::~CollectionView() = default;
|
||||
|
||||
bool CollectionView::hasWidget() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
QmlDesigner::WidgetInfo CollectionView::widgetInfo()
|
||||
{
|
||||
if (!m_widget) {
|
||||
m_widget = Utils::makeUniqueObjectPtr<CollectionWidget>(this);
|
||||
m_widget->setMinimumSize(m_widget->minimumSizeHint());
|
||||
connect(ProjectExplorer::ProjectManager::instance(),
|
||||
&ProjectExplorer::ProjectManager::startupProjectChanged, m_widget.get(), [&] {
|
||||
resetDataStoreNode();
|
||||
m_widget->collectionDetailsModel()->removeAllCollections();
|
||||
});
|
||||
|
||||
auto collectionEditorContext = new Internal::CollectionEditorContext(m_widget.get());
|
||||
Core::ICore::addContextObject(collectionEditorContext);
|
||||
CollectionListModel *listModel = m_widget->listModel().data();
|
||||
|
||||
connect(listModel,
|
||||
&CollectionListModel::selectedCollectionNameChanged,
|
||||
this,
|
||||
[this](const QString &collection) {
|
||||
m_widget->collectionDetailsModel()->loadCollection(dataStoreNode(), collection);
|
||||
});
|
||||
|
||||
connect(listModel, &CollectionListModel::isEmptyChanged, this, [this](bool isEmpty) {
|
||||
if (isEmpty)
|
||||
m_widget->collectionDetailsModel()->loadCollection({}, {});
|
||||
});
|
||||
|
||||
connect(listModel, &CollectionListModel::modelReset, this, [this] {
|
||||
CollectionListModel *listModel = m_widget->listModel().data();
|
||||
if (listModel->sourceNode() == dataStoreNode())
|
||||
m_dataStore->setCollectionNames(listModel->collections());
|
||||
});
|
||||
|
||||
connect(listModel,
|
||||
&CollectionListModel::collectionAdded,
|
||||
this,
|
||||
[this](const QString &collectionName) { m_dataStore->addCollection(collectionName); });
|
||||
|
||||
connect(listModel,
|
||||
&CollectionListModel::collectionNameChanged,
|
||||
this,
|
||||
[this](const QString &oldName, const QString &newName) {
|
||||
m_dataStore->renameCollection(oldName, newName);
|
||||
m_widget->collectionDetailsModel()->renameCollection(dataStoreNode(),
|
||||
oldName,
|
||||
newName);
|
||||
});
|
||||
|
||||
connect(listModel,
|
||||
&CollectionListModel::collectionsRemoved,
|
||||
this,
|
||||
[this](const QStringList &collectionNames) {
|
||||
m_dataStore->removeCollections(collectionNames);
|
||||
for (const QString &collectionName : collectionNames) {
|
||||
m_widget->collectionDetailsModel()->removeCollection(dataStoreNode(),
|
||||
collectionName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return createWidgetInfo(m_widget.get(),
|
||||
"CollectionEditor",
|
||||
WidgetInfo::LeftPane,
|
||||
0,
|
||||
tr("Model Editor [beta]"),
|
||||
tr("Model Editor view"));
|
||||
}
|
||||
|
||||
void CollectionView::modelAttached(Model *model)
|
||||
{
|
||||
AbstractView::modelAttached(model);
|
||||
m_widget->setProjectImportExists(Utils::anyOf(model->imports(), isProjectImport));
|
||||
resetDataStoreNode();
|
||||
}
|
||||
|
||||
void CollectionView::modelAboutToBeDetached([[maybe_unused]] Model *model)
|
||||
{
|
||||
unloadDataStore();
|
||||
m_widget->setProjectImportExists(false);
|
||||
}
|
||||
|
||||
void CollectionView::selectedNodesChanged(const QList<ModelNode> &selectedNodeList,
|
||||
[[maybe_unused]] const QList<ModelNode> &lastSelectedNodeList)
|
||||
{
|
||||
if (!m_widget)
|
||||
return;
|
||||
|
||||
QList<ModelNode> selectedCollectionNodes = Utils::filtered(selectedNodeList,
|
||||
&isStudioCollectionModel);
|
||||
|
||||
bool singleNonCollectionNodeSelected = selectedNodeList.size() == 1
|
||||
&& selectedCollectionNodes.isEmpty();
|
||||
|
||||
bool singleSelectedHasModelProperty = false;
|
||||
if (singleNonCollectionNodeSelected) {
|
||||
const ModelNode selectedNode = selectedNodeList.first();
|
||||
singleSelectedHasModelProperty = CollectionEditorUtils::canAcceptCollectionAsModel(
|
||||
selectedNode);
|
||||
}
|
||||
|
||||
m_widget->setTargetNodeSelected(singleSelectedHasModelProperty);
|
||||
}
|
||||
|
||||
void CollectionView::importsChanged(const Imports &addedImports, const Imports &removedImports)
|
||||
{
|
||||
if (Utils::anyOf(addedImports, isProjectImport)) {
|
||||
m_widget->setProjectImportExists(true);
|
||||
resetDataStoreNode();
|
||||
} else if (Utils::anyOf(removedImports, isProjectImport)) {
|
||||
m_widget->setProjectImportExists(false);
|
||||
unloadDataStore();
|
||||
}
|
||||
}
|
||||
|
||||
void CollectionView::customNotification(const AbstractView *,
|
||||
const QString &identifier,
|
||||
const QList<ModelNode> &nodeList,
|
||||
const QList<QVariant> &data)
|
||||
{
|
||||
if (!m_widget)
|
||||
return;
|
||||
|
||||
if (identifier == QLatin1String("item_library_created_by_drop") && !nodeList.isEmpty())
|
||||
onItemLibraryNodeCreated(nodeList.first());
|
||||
else if (identifier == QLatin1String("open_collection_by_id") && !data.isEmpty())
|
||||
m_widget->openCollection(collectionNameFromDataStoreChildren(data.first().toByteArray()));
|
||||
else if (identifier == "delete_selected_collection")
|
||||
m_widget->deleteSelectedCollection();
|
||||
}
|
||||
|
||||
void CollectionView::addResource(const QUrl &url, const QString &name)
|
||||
{
|
||||
executeInTransaction(Q_FUNC_INFO, [this, &url, &name]() {
|
||||
ensureStudioModelImport();
|
||||
QString sourceAddress;
|
||||
if (url.isLocalFile()) {
|
||||
Utils::FilePath fp = QmlDesignerPlugin::instance()->currentDesignDocument()->fileName().parentDir();
|
||||
sourceAddress = Utils::FilePath::calcRelativePath(url.toLocalFile(),
|
||||
fp.absoluteFilePath().toString());
|
||||
} else {
|
||||
sourceAddress = url.toString();
|
||||
}
|
||||
#ifdef QDS_USE_PROJECTSTORAGE
|
||||
ModelNode resourceNode = createModelNode("JsonListModel");
|
||||
#else
|
||||
const NodeMetaInfo resourceMetaInfo = jsonCollectionMetaInfo();
|
||||
ModelNode resourceNode = createModelNode(resourceMetaInfo.typeName(),
|
||||
resourceMetaInfo.majorVersion(),
|
||||
resourceMetaInfo.minorVersion());
|
||||
#endif
|
||||
VariantProperty sourceProperty = resourceNode.variantProperty(
|
||||
CollectionEditorConstants::SOURCEFILE_PROPERTY);
|
||||
VariantProperty nameProperty = resourceNode.variantProperty("objectName");
|
||||
sourceProperty.setValue(sourceAddress);
|
||||
nameProperty.setValue(name);
|
||||
resourceNode.setIdWithoutRefactoring(model()->generateNewId(name, "model"));
|
||||
rootModelNode().defaultNodeAbstractProperty().reparentHere(resourceNode);
|
||||
});
|
||||
}
|
||||
|
||||
void CollectionView::addProjectImport()
|
||||
{
|
||||
if (!m_widget)
|
||||
return;
|
||||
|
||||
ProjectExplorer::Project *currentProject = ProjectExplorer::ProjectManager::startupProject();
|
||||
if (!currentProject)
|
||||
return;
|
||||
|
||||
executeInTransaction(__FUNCTION__, [&] {
|
||||
Import import = Import::createLibraryImport(currentProject->displayName());
|
||||
if (!model()->hasImport(import, true, true))
|
||||
model()->changeImports({import}, {});
|
||||
});
|
||||
}
|
||||
|
||||
void CollectionView::assignCollectionToNode(const QString &collectionName, const ModelNode &node)
|
||||
{
|
||||
if (!m_widget)
|
||||
return;
|
||||
|
||||
executeInTransaction("CollectionView::assignCollectionToNode", [&]() {
|
||||
m_dataStore->assignCollectionToNode(
|
||||
this,
|
||||
node,
|
||||
collectionName,
|
||||
[&](const QString &collectionName, const QString &columnName) -> bool {
|
||||
const CollectionReference reference{dataStoreNode(), collectionName};
|
||||
return m_widget->collectionDetailsModel()->collectionHasColumn(reference, columnName);
|
||||
},
|
||||
[&](const QString &collectionName) -> QString {
|
||||
const CollectionReference reference{dataStoreNode(), collectionName};
|
||||
return m_widget->collectionDetailsModel()->getFirstColumnName(reference);
|
||||
});
|
||||
|
||||
// Create and assign a delegate to the list view item
|
||||
if (node.metaInfo().isQtQuickListView()) {
|
||||
::setNodePropertyValue(node, "delegate", createListViewDelegate(collectionName));
|
||||
} else if (node.metaInfo().isQtQuickGridView()) {
|
||||
QSize delegateSize;
|
||||
::setNodePropertyValue(node,
|
||||
"delegate",
|
||||
createGridViewDelegate(collectionName, delegateSize));
|
||||
::setVariantPropertyValue(node, "cellWidth", delegateSize.width());
|
||||
::setVariantPropertyValue(node, "cellHeight", delegateSize.height());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void CollectionView::assignCollectionToSelectedNode(const QString &collectionName)
|
||||
{
|
||||
QTC_ASSERT(dataStoreNode() && hasSingleSelectedModelNode(), return);
|
||||
assignCollectionToNode(collectionName, singleSelectedModelNode());
|
||||
}
|
||||
|
||||
void CollectionView::addNewCollection(const QString &collectionName, const QJsonObject &localCollection)
|
||||
{
|
||||
if (!m_widget)
|
||||
return;
|
||||
|
||||
addTask(QSharedPointer<CollectionTask>(
|
||||
new AddCollectionTask(this, m_widget->listModel(), localCollection, collectionName)));
|
||||
}
|
||||
|
||||
void CollectionView::openCollection(const QString &collectionName)
|
||||
{
|
||||
if (!m_widget)
|
||||
return;
|
||||
|
||||
m_widget->openCollection(collectionName);
|
||||
}
|
||||
|
||||
void CollectionView::registerDeclarativeType()
|
||||
{
|
||||
CollectionDetails::registerDeclarativeType();
|
||||
CollectionDataTypeModel::registerDeclarativeType();
|
||||
}
|
||||
|
||||
void CollectionView::resetDataStoreNode()
|
||||
{
|
||||
if (!m_widget)
|
||||
return;
|
||||
|
||||
auto compUtils = QmlDesignerPlugin::instance()->documentManager().generatedComponentUtils();
|
||||
m_dataStore->reloadModel(compUtils.projectModulePath());
|
||||
|
||||
ModelNode dataStore = dataStoreNode();
|
||||
m_widget->setDataStoreExists(dataStore.isValid());
|
||||
if (!dataStore || m_widget->listModel()->sourceNode() == dataStore)
|
||||
return;
|
||||
|
||||
bool dataStoreSingletonFound = m_dataStoreTypeFound;
|
||||
if (!dataStoreSingletonFound && rewriterView() && rewriterView()->isAttached()) {
|
||||
const QList<QmlTypeData> types = rewriterView()->getQMLTypes();
|
||||
for (const QmlTypeData &cppTypeData : types) {
|
||||
if (cppTypeData.isSingleton && cppTypeData.typeName == "DataStore") {
|
||||
dataStoreSingletonFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!dataStoreSingletonFound && !m_rewriterAmended) {
|
||||
rewriterView()->forceAmend();
|
||||
m_rewriterAmended = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (dataStoreSingletonFound) {
|
||||
m_widget->listModel()->setDataStoreNode(dataStore);
|
||||
m_widget->collectionDetailsModel()->setJsonFilePath(m_dataStore->jsonFilePath());
|
||||
m_dataStoreTypeFound = true;
|
||||
resetPuppet();
|
||||
|
||||
while (!m_delayedTasks.isEmpty())
|
||||
m_delayedTasks.takeFirst()->process();
|
||||
} else if (++m_reloadCounter < 50) {
|
||||
QTimer::singleShot(200, this, &CollectionView::resetDataStoreNode);
|
||||
} else {
|
||||
QTC_ASSERT(false, m_delayedTasks.clear());
|
||||
}
|
||||
}
|
||||
|
||||
ModelNode CollectionView::dataStoreNode() const
|
||||
{
|
||||
return m_dataStore->modelNode();
|
||||
}
|
||||
|
||||
void CollectionView::ensureDataStoreExists()
|
||||
{
|
||||
bool filesJustCreated = false;
|
||||
bool filesExist = createDataStore(filesJustCreated);
|
||||
if (filesExist && filesJustCreated) {
|
||||
// Force code model reset to notice changes to existing module
|
||||
if (auto modelManager = QmlJS::ModelManagerInterface::instance())
|
||||
modelManager->resetCodeModel();
|
||||
resetDataStoreNode();
|
||||
}
|
||||
}
|
||||
|
||||
QString CollectionView::collectionNameFromDataStoreChildren(const PropertyName &childPropertyName) const
|
||||
{
|
||||
return dataStoreNode()
|
||||
.nodeProperty(childPropertyName)
|
||||
.modelNode()
|
||||
.property(CollectionEditorConstants::JSONCHILDMODELNAME_PROPERTY)
|
||||
.toVariantProperty()
|
||||
.value()
|
||||
.toString();
|
||||
}
|
||||
|
||||
NodeMetaInfo CollectionView::jsonCollectionMetaInfo() const
|
||||
{
|
||||
return model()->metaInfo(CollectionEditorConstants::JSONCOLLECTIONMODEL_TYPENAME);
|
||||
}
|
||||
|
||||
void CollectionView::unloadDataStore()
|
||||
{
|
||||
m_reloadCounter = 0;
|
||||
m_rewriterAmended = false;
|
||||
m_dataStoreTypeFound = false;
|
||||
QTC_ASSERT(m_delayedTasks.isEmpty(), m_delayedTasks.clear());
|
||||
if (m_widget) {
|
||||
m_widget->setDataStoreExists(dataStoreNode().isValid());
|
||||
m_widget->listModel()->setDataStoreNode();
|
||||
}
|
||||
}
|
||||
|
||||
bool CollectionView::createDataStore(bool &justCreated) const
|
||||
{
|
||||
using Utils::FilePath;
|
||||
using Utils::FileReader;
|
||||
using Utils::FileSaver;
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
auto compUtils = QmlDesignerPlugin::instance()->documentManager().generatedComponentUtils();
|
||||
FilePath projectModulePath = compUtils.projectModulePath(true);
|
||||
|
||||
FilePath qmlTargetPath = projectModulePath.resolvePath(
|
||||
CollectionEditorConstants::DEFAULT_DATASTORE_QML_FILENAME.toString());
|
||||
justCreated = false;
|
||||
|
||||
auto extractDependency = [&justCreated](const FilePath &filePath) -> bool {
|
||||
if (filePath.exists())
|
||||
return true;
|
||||
|
||||
const QString templateFileName = filePath.fileName() + u".tpl";
|
||||
const FilePath templatePath = CollectionEditorUtils::findFile(Core::ICore::resourcePath(),
|
||||
templateFileName);
|
||||
if (!templatePath.exists()) {
|
||||
qWarning() << Q_FUNC_INFO << __LINE__ << templateFileName << "does not exist";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!filePath.parentDir().ensureWritableDir()) {
|
||||
qWarning() << Q_FUNC_INFO << __LINE__ << "Cannot create directory"
|
||||
<< filePath.parentDir();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (templatePath.copyFile(filePath)) {
|
||||
justCreated = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
qWarning() << Q_FUNC_INFO << __LINE__ << "Cannot copy" << templateFileName << "to" << filePath;
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!extractDependency(projectModulePath.resolvePath(
|
||||
CollectionEditorConstants::DEFAULT_MODELS_JSON_FILENAME.toString()))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!extractDependency(projectModulePath.resolvePath(
|
||||
CollectionEditorConstants::DEFAULT_DATA_JSON_FILENAME.toString()))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!extractDependency(projectModulePath.resolvePath(
|
||||
CollectionEditorConstants::DEFAULT_JSONDATA_QML_FILENAME.toString()))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!qmlTargetPath.exists()) {
|
||||
if (qmlTargetPath.ensureExistingFile()) {
|
||||
justCreated = true;
|
||||
} else {
|
||||
qWarning() << Q_FUNC_INFO << __LINE__ << "Can't create DataStore Qml File";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
FilePath qmlDirPath = projectModulePath.resolvePath("qmldir"_L1);
|
||||
qmlDirPath.ensureExistingFile();
|
||||
|
||||
FileReader qmlDirReader;
|
||||
if (!qmlDirReader.fetch(qmlDirPath)) {
|
||||
qWarning() << Q_FUNC_INFO << __LINE__ << "Can't read the content of the qmldir";
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray qmlDirContent = qmlDirReader.data();
|
||||
const QList<QByteArray> qmlDirLines = qmlDirContent.split('\n');
|
||||
for (const QByteArray &line : qmlDirLines) {
|
||||
if (line.startsWith("singleton DataStore "))
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!qmlDirContent.isEmpty() && qmlDirContent.back() != '\n')
|
||||
qmlDirContent.append("\n");
|
||||
qmlDirContent.append("singleton DataStore 1.0 DataStore.qml\n");
|
||||
|
||||
FileSaver qmlDirSaver(qmlDirPath);
|
||||
qmlDirSaver.write(qmlDirContent);
|
||||
|
||||
if (qmlDirSaver.finalize()) {
|
||||
justCreated = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
qWarning() << Q_FUNC_INFO << __LINE__ << "Can't write to the qmldir file";
|
||||
return false;
|
||||
}
|
||||
|
||||
void CollectionView::ensureStudioModelImport()
|
||||
{
|
||||
executeInTransaction(__FUNCTION__, [&] {
|
||||
Import import = Import::createLibraryImport(CollectionEditorConstants::COLLECTIONMODEL_IMPORT);
|
||||
try {
|
||||
if (!model()->hasImport(import, true, true))
|
||||
model()->changeImports({import}, {});
|
||||
} catch (const Exception &) {
|
||||
QTC_ASSERT(false, return);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void CollectionView::onItemLibraryNodeCreated(const ModelNode &node)
|
||||
{
|
||||
if (!m_widget)
|
||||
return;
|
||||
|
||||
if (node.metaInfo().isListOrGridView()) {
|
||||
addTask(QSharedPointer<CollectionTask>(
|
||||
new DropListViewTask(this, m_widget->listModel(), node)));
|
||||
}
|
||||
}
|
||||
|
||||
void CollectionView::addTask(QSharedPointer<CollectionTask> task)
|
||||
{
|
||||
ensureDataStoreExists();
|
||||
if (m_dataStoreTypeFound)
|
||||
task->process();
|
||||
else if (dataStoreNode())
|
||||
m_delayedTasks << task;
|
||||
}
|
||||
|
||||
ModelNode CollectionView::createListViewDelegate(const QString &collectionName)
|
||||
{
|
||||
using DataType = CollectionDetails::DataType;
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
CollectionDetails collection = m_widget->collectionDetailsModel()->upToDateConstCollection(
|
||||
{dataStoreNode(), collectionName});
|
||||
|
||||
ModelNode rowItem(createModelNode("QtQuick.Row"));
|
||||
::setVariantPropertyValue(rowItem, "spacing", 5);
|
||||
|
||||
const int columnsCount = collection.columns();
|
||||
for (int column = 0; column < columnsCount; ++column) {
|
||||
const DataType dataType = collection.typeAt(column);
|
||||
const QString columnName = collection.propertyAt(column);
|
||||
ModelNode cellItem;
|
||||
if (dataType == DataType::Color) {
|
||||
cellItem = createModelNode("QtQuick.Rectangle");
|
||||
::setBindingPropertyExpression(cellItem, "color", columnName);
|
||||
::setVariantPropertyValue(cellItem, "height", 20);
|
||||
} else {
|
||||
cellItem = createModelNode("QtQuick.Text");
|
||||
::setBindingPropertyExpression(cellItem, "text", columnName);
|
||||
::setVariantPropertyValue(cellItem, "clip", true);
|
||||
::setBindingPropertyExpression(cellItem, "elide", "Text.ElideRight"_L1);
|
||||
::setVariantPropertyValue(cellItem, "leftPadding", 1);
|
||||
::setVariantPropertyValue(cellItem, "rightPadding", 1);
|
||||
}
|
||||
::setVariantPropertyValue(cellItem, "width", 100);
|
||||
rowItem.defaultNodeAbstractProperty().reparentHere(cellItem);
|
||||
}
|
||||
return rowItem;
|
||||
}
|
||||
|
||||
ModelNode CollectionView::createGridViewDelegate(const QString &collectionName, QSize &delegateSize)
|
||||
{
|
||||
using DataType = CollectionDetails::DataType;
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
CollectionDetails collection = m_widget->collectionDetailsModel()->upToDateConstCollection(
|
||||
{dataStoreNode(), collectionName});
|
||||
|
||||
constexpr int spacing = 5;
|
||||
int delegateWidth = 70;
|
||||
int delegateHeight = 0;
|
||||
|
||||
ModelNode rectItem(createModelNode("QtQuick.Rectangle"));
|
||||
::setVariantPropertyValue(rectItem, "clip", true);
|
||||
::setVariantPropertyValue(rectItem, "border.color", QColor(Qt::blue));
|
||||
::setVariantPropertyValue(rectItem, "border.width", 1);
|
||||
|
||||
ModelNode colItem(createModelNode("QtQuick.Column"));
|
||||
::setVariantPropertyValue(colItem, "spacing", spacing);
|
||||
::setVariantPropertyValue(colItem, "topPadding", spacing);
|
||||
::setBindingPropertyExpression(colItem, "anchors.fill", "parent"_L1);
|
||||
|
||||
const int columnsCount = collection.columns();
|
||||
for (int column = 0; column < columnsCount; ++column) {
|
||||
const DataType dataType = collection.typeAt(column);
|
||||
const QString columnName = collection.propertyAt(column);
|
||||
ModelNode cellItem;
|
||||
if (dataType == DataType::Color) {
|
||||
cellItem = createModelNode("QtQuick.Rectangle");
|
||||
::setBindingPropertyExpression(cellItem, "color", columnName);
|
||||
::setVariantPropertyValue(cellItem, "width", 40);
|
||||
::setVariantPropertyValue(cellItem, "height", 40);
|
||||
delegateHeight += 40;
|
||||
} else {
|
||||
cellItem = createModelNode("QtQuick.Text");
|
||||
::setBindingPropertyExpression(cellItem, "width", "parent.width"_L1);
|
||||
::setVariantPropertyValue(cellItem, "height", 20);
|
||||
::setBindingPropertyExpression(cellItem, "text", columnName);
|
||||
::setVariantPropertyValue(cellItem, "clip", true);
|
||||
::setBindingPropertyExpression(cellItem, "elide", "Text.ElideRight"_L1);
|
||||
::setVariantPropertyValue(cellItem, "leftPadding", 1);
|
||||
::setVariantPropertyValue(cellItem, "rightPadding", 1);
|
||||
::setBindingPropertyExpression(cellItem, "horizontalAlignment", "Text.AlignHCenter"_L1);
|
||||
delegateHeight += 20;
|
||||
}
|
||||
delegateHeight += spacing;
|
||||
::setBindingPropertyExpression(cellItem,
|
||||
"anchors.horizontalCenter",
|
||||
"parent.horizontalCenter"_L1);
|
||||
colItem.defaultNodeAbstractProperty().reparentHere(cellItem);
|
||||
}
|
||||
|
||||
rectItem.defaultNodeAbstractProperty().reparentHere(colItem);
|
||||
::setVariantPropertyValue(rectItem, "width", delegateWidth);
|
||||
::setVariantPropertyValue(rectItem, "height", delegateHeight);
|
||||
delegateSize.setWidth(delegateWidth);
|
||||
delegateSize.setHeight(delegateHeight);
|
||||
|
||||
return rectItem;
|
||||
}
|
||||
|
||||
CollectionTask::CollectionTask(CollectionView *view, CollectionListModel *listModel)
|
||||
: m_collectionView(view)
|
||||
, m_listModel(listModel)
|
||||
{}
|
||||
|
||||
DropListViewTask::DropListViewTask(CollectionView *view,
|
||||
CollectionListModel *listModel,
|
||||
const ModelNode &node)
|
||||
: CollectionTask(view, listModel)
|
||||
, m_node(node)
|
||||
{}
|
||||
|
||||
void DropListViewTask::process()
|
||||
{
|
||||
AbstractView *view = m_node.view();
|
||||
if (!m_node || !m_collectionView || !m_listModel || !view)
|
||||
return;
|
||||
|
||||
const QString newCollectionName = m_listModel->getUniqueCollectionName("ListModel");
|
||||
m_listModel->addCollection(newCollectionName, CollectionEditorUtils::defaultColorCollection());
|
||||
m_collectionView->openCollection(newCollectionName);
|
||||
m_collectionView->assignCollectionToNode(newCollectionName, m_node);
|
||||
}
|
||||
|
||||
AddCollectionTask::AddCollectionTask(CollectionView *view,
|
||||
CollectionListModel *listModel,
|
||||
const QJsonObject &localJsonObject,
|
||||
const QString &collectionName)
|
||||
: CollectionTask(view, listModel)
|
||||
, m_localJsonObject(localJsonObject)
|
||||
, m_name(collectionName)
|
||||
{}
|
||||
|
||||
void AddCollectionTask::process()
|
||||
{
|
||||
if (!m_listModel)
|
||||
return;
|
||||
|
||||
const QString newCollectionName = m_listModel->collectionExists(m_name)
|
||||
? m_listModel->getUniqueCollectionName(m_name)
|
||||
: m_name;
|
||||
|
||||
m_listModel->addCollection(newCollectionName, m_localJsonObject);
|
||||
}
|
||||
|
||||
} // namespace QmlDesigner
|
@@ -1,128 +0,0 @@
|
||||
// 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 "datastoremodelnode.h"
|
||||
|
||||
#include <abstractview.h>
|
||||
#include <modelnode.h>
|
||||
|
||||
#include <utils/uniqueobjectptr.h>
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
namespace QmlJS {
|
||||
class Document;
|
||||
}
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
class CollectionDetails;
|
||||
class CollectionListModel;
|
||||
class CollectionTask;
|
||||
class CollectionWidget;
|
||||
class DataStoreModelNode;
|
||||
class GeneratedComponentUtils;
|
||||
|
||||
class CollectionView : public AbstractView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CollectionView(ExternalDependenciesInterface &externalDependencies);
|
||||
~CollectionView();
|
||||
|
||||
bool hasWidget() const override;
|
||||
WidgetInfo widgetInfo() override;
|
||||
|
||||
void modelAttached(Model *model) override;
|
||||
void modelAboutToBeDetached(Model *model) override;
|
||||
|
||||
void selectedNodesChanged(const QList<ModelNode> &selectedNodeList,
|
||||
const QList<ModelNode> &lastSelectedNodeList) override;
|
||||
|
||||
void importsChanged(const Imports &addedImports, const Imports &removedImports) override;
|
||||
|
||||
void customNotification(const AbstractView *view,
|
||||
const QString &identifier,
|
||||
const QList<ModelNode> &nodeList,
|
||||
const QList<QVariant> &data) override;
|
||||
|
||||
void addResource(const QUrl &url, const QString &name);
|
||||
|
||||
void addProjectImport();
|
||||
void assignCollectionToNode(const QString &collectionName, const ModelNode &node);
|
||||
void assignCollectionToSelectedNode(const QString &collectionName);
|
||||
void addNewCollection(const QString &collectionName, const QJsonObject &localCollection);
|
||||
|
||||
void openCollection(const QString &collectionName);
|
||||
|
||||
static void registerDeclarativeType();
|
||||
|
||||
void resetDataStoreNode();
|
||||
ModelNode dataStoreNode() const;
|
||||
void ensureDataStoreExists();
|
||||
QString collectionNameFromDataStoreChildren(const PropertyName &childPropertyName) const;
|
||||
|
||||
private:
|
||||
friend class CollectionTask;
|
||||
|
||||
NodeMetaInfo jsonCollectionMetaInfo() const;
|
||||
void unloadDataStore();
|
||||
bool createDataStore(bool &justCreated) const;
|
||||
void ensureStudioModelImport();
|
||||
void onItemLibraryNodeCreated(const ModelNode &node);
|
||||
void addTask(QSharedPointer<CollectionTask> task);
|
||||
ModelNode createListViewDelegate(const QString &collectionName);
|
||||
ModelNode createGridViewDelegate(const QString &collectionName, QSize &delegateSize);
|
||||
|
||||
std::unique_ptr<DataStoreModelNode> m_dataStore;
|
||||
Utils::UniqueObjectPtr<CollectionWidget> m_widget;
|
||||
QList<QSharedPointer<CollectionTask>> m_delayedTasks;
|
||||
bool m_dataStoreTypeFound = false;
|
||||
bool m_rewriterAmended = false;
|
||||
int m_reloadCounter = 0;
|
||||
};
|
||||
|
||||
class CollectionTask
|
||||
{
|
||||
public:
|
||||
CollectionTask(CollectionView *view, CollectionListModel *listModel);
|
||||
CollectionTask() = delete;
|
||||
virtual ~CollectionTask() = default;
|
||||
|
||||
virtual void process() = 0;
|
||||
|
||||
protected:
|
||||
QPointer<CollectionView> m_collectionView;
|
||||
QPointer<CollectionListModel> m_listModel;
|
||||
};
|
||||
|
||||
class DropListViewTask : public CollectionTask
|
||||
{
|
||||
public:
|
||||
DropListViewTask(CollectionView *view, CollectionListModel *listModel, const ModelNode &node);
|
||||
|
||||
void process() override;
|
||||
|
||||
private:
|
||||
ModelNode m_node;
|
||||
};
|
||||
|
||||
class AddCollectionTask : public CollectionTask
|
||||
{
|
||||
public:
|
||||
AddCollectionTask(CollectionView *view,
|
||||
CollectionListModel *listModel,
|
||||
const QJsonObject &localJsonObject,
|
||||
const QString &collectionName);
|
||||
|
||||
void process() override;
|
||||
|
||||
private:
|
||||
QJsonObject m_localJsonObject;
|
||||
QString m_name;
|
||||
};
|
||||
|
||||
} // namespace QmlDesigner
|
@@ -1,336 +0,0 @@
|
||||
// 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 "collectiondetails.h"
|
||||
#include "collectiondetailsmodel.h"
|
||||
#include "collectiondetailssortfiltermodel.h"
|
||||
#include "collectioneditorutils.h"
|
||||
#include "collectionlistmodel.h"
|
||||
#include "collectionview.h"
|
||||
#include "designmodewidget.h"
|
||||
#include "qmldesignerconstants.h"
|
||||
#include "qmldesignerplugin.h"
|
||||
#include "theme.h"
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
#include <coreplugin/messagebox.h>
|
||||
#include <studioquickwidget.h>
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#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();
|
||||
}
|
||||
|
||||
QString getPreferredCollectionName(const QUrl &url, const QString &collectionName)
|
||||
{
|
||||
if (collectionName.isEmpty()) {
|
||||
QFileInfo fileInfo(url.isLocalFile() ? url.toLocalFile() : url.toString());
|
||||
return fileInfo.completeBaseName();
|
||||
}
|
||||
|
||||
return collectionName;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
CollectionWidget::CollectionWidget(CollectionView *view)
|
||||
: m_view(view)
|
||||
, m_listModel(new CollectionListModel)
|
||||
, m_collectionDetailsModel(new CollectionDetailsModel)
|
||||
, m_collectionDetailsSortFilterModel(std::make_unique<CollectionDetailsSortFilterModel>())
|
||||
, m_quickWidget(Utils::makeUniqueObjectPtr<StudioQuickWidget>(this))
|
||||
{
|
||||
setWindowTitle(tr("Model Editor", "Title of model editor widget"));
|
||||
|
||||
Core::Context context(Constants::C_QMLCOLLECTIONEDITOR);
|
||||
m_iContext = new Core::IContext(this);
|
||||
m_iContext->setContext(context);
|
||||
m_iContext->setWidget(this);
|
||||
Core::ICore::addContextObject(m_iContext);
|
||||
|
||||
connect(m_listModel, &CollectionListModel::warning, this, &CollectionWidget::warn);
|
||||
|
||||
m_collectionDetailsSortFilterModel->setSourceModel(m_collectionDetailsModel);
|
||||
|
||||
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.get());
|
||||
|
||||
qmlRegisterAnonymousType<CollectionWidget>("CollectionEditorBackend", 1);
|
||||
auto map = m_quickWidget->registerPropertyMap("CollectionEditorBackend");
|
||||
map->setProperties({
|
||||
{"rootView", QVariant::fromValue(this)},
|
||||
{"model", QVariant::fromValue(m_listModel.data())},
|
||||
{"collectionDetailsModel", QVariant::fromValue(m_collectionDetailsModel.data())},
|
||||
{"collectionDetailsSortFilterModel",
|
||||
QVariant::fromValue(m_collectionDetailsSortFilterModel.get())},
|
||||
});
|
||||
|
||||
auto hotReloadShortcut = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_F4), this);
|
||||
connect(hotReloadShortcut, &QShortcut::activated, this, &CollectionWidget::reloadQmlSource);
|
||||
|
||||
reloadQmlSource();
|
||||
|
||||
QmlDesignerPlugin::trackWidgetFocusTime(this, Constants::EVENT_MODELEDITOR_TIME);
|
||||
}
|
||||
|
||||
CollectionWidget::~CollectionWidget() = default;
|
||||
|
||||
void CollectionWidget::contextHelp(const Core::IContext::HelpCallback &callback) const
|
||||
{
|
||||
if (m_view)
|
||||
QmlDesignerPlugin::contextHelp(callback, m_view->contextHelpId());
|
||||
else
|
||||
callback({});
|
||||
}
|
||||
|
||||
QPointer<CollectionListModel> CollectionWidget::listModel() const
|
||||
{
|
||||
return m_listModel;
|
||||
}
|
||||
|
||||
QPointer<CollectionDetailsModel> CollectionWidget::collectionDetailsModel() const
|
||||
{
|
||||
return m_collectionDetailsModel;
|
||||
}
|
||||
|
||||
void CollectionWidget::reloadQmlSource()
|
||||
{
|
||||
const QString collectionViewQmlPath = collectionViewResourcesPath() + "/CollectionView.qml";
|
||||
|
||||
QTC_ASSERT(QFileInfo::exists(collectionViewQmlPath), return);
|
||||
|
||||
m_quickWidget->setSource(QUrl::fromLocalFile(collectionViewQmlPath));
|
||||
|
||||
if (!m_quickWidget->rootObject()) {
|
||||
QString errorString;
|
||||
const auto errors = m_quickWidget->errors();
|
||||
for (const QQmlError &error : errors)
|
||||
errorString.append("\n" + error.toString());
|
||||
|
||||
Core::AsynchronousMessageBox::warning(tr("Cannot Create QtQuick View"),
|
||||
tr("StatesEditorWidget: %1 cannot be created.%2")
|
||||
.arg(collectionViewQmlPath, errorString));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QSize CollectionWidget::minimumSizeHint() const
|
||||
{
|
||||
return {300, 300};
|
||||
}
|
||||
|
||||
bool CollectionWidget::loadJsonFile(const QUrl &url, const QString &collectionName)
|
||||
{
|
||||
if (!isJsonFile(url))
|
||||
return false;
|
||||
|
||||
m_view->addResource(url, getPreferredCollectionName(url, collectionName));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CollectionWidget::loadCsvFile(const QUrl &url, const QString &collectionName)
|
||||
{
|
||||
m_view->addResource(url, getPreferredCollectionName(url, collectionName));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CollectionWidget::isJsonFile(const QUrl &url) const
|
||||
{
|
||||
Utils::FilePath filePath = Utils::FilePath::fromUserInput(url.isLocalFile() ? url.toLocalFile()
|
||||
: url.toString());
|
||||
Utils::FileReader file;
|
||||
if (!file.fetch(filePath))
|
||||
return false;
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument::fromJson(file.data(), &error);
|
||||
if (error.error)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CollectionWidget::isCsvFile(const QUrl &url) const
|
||||
{
|
||||
QString filePath = url.isLocalFile() ? url.toLocalFile() : url.toString();
|
||||
QFileInfo fileInfo(filePath);
|
||||
return fileInfo.exists() && !fileInfo.suffix().compare("csv", Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
bool CollectionWidget::isValidUrlToImport(const QUrl &url) const
|
||||
{
|
||||
using Utils::FilePath;
|
||||
FilePath fileInfo = FilePath::fromUserInput(url.isLocalFile() ? url.toLocalFile()
|
||||
: url.toString());
|
||||
if (fileInfo.suffix() == "json")
|
||||
return isJsonFile(url);
|
||||
|
||||
if (fileInfo.suffix() == "csv")
|
||||
return isCsvFile(url);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CollectionWidget::importFile(const QString &collectionName,
|
||||
const QUrl &url,
|
||||
const bool &firstRowIsHeader)
|
||||
{
|
||||
using Utils::FilePath;
|
||||
|
||||
FilePath fileInfo = FilePath::fromUserInput(url.isLocalFile() ? url.toLocalFile()
|
||||
: url.toString());
|
||||
QByteArray fileContent;
|
||||
|
||||
auto loadUrlContent = [&]() -> bool {
|
||||
Utils::FileReader file;
|
||||
if (file.fetch(fileInfo)) {
|
||||
fileContent = file.data();
|
||||
return true;
|
||||
}
|
||||
|
||||
warn(tr("Import from file"), tr("Cannot import from file \"%1\"").arg(fileInfo.fileName()));
|
||||
return false;
|
||||
};
|
||||
|
||||
if (fileInfo.suffix() == "json") {
|
||||
if (!loadUrlContent())
|
||||
return false;
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QList<CollectionDetails> loadedCollections = CollectionDetails::fromImportedJson(
|
||||
fileContent, &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError) {
|
||||
warn(tr("Json file Import error"),
|
||||
tr("Cannot parse json content\n%1").arg(parseError.errorString()));
|
||||
return false;
|
||||
}
|
||||
if (loadedCollections.size() > 1) {
|
||||
for (const CollectionDetails &loadedCollection : loadedCollections) {
|
||||
m_view->addNewCollection(loadedCollection.reference().name,
|
||||
loadedCollection.toLocalJson());
|
||||
}
|
||||
return true;
|
||||
} else if (loadedCollections.size() == 1) {
|
||||
m_view->addNewCollection(collectionName, loadedCollections.first().toLocalJson());
|
||||
return true;
|
||||
} else {
|
||||
warn(tr("Can not add a model to the JSON file"),
|
||||
tr("The imported model is empty or is not supported."));
|
||||
}
|
||||
} else if (fileInfo.suffix() == "csv") {
|
||||
CollectionDetails loadedCollection;
|
||||
if (!loadUrlContent())
|
||||
return false;
|
||||
loadedCollection = CollectionDetails::fromImportedCsv(fileContent, firstRowIsHeader);
|
||||
if (loadedCollection.columns()) {
|
||||
m_view->addNewCollection(collectionName, loadedCollection.toLocalJson());
|
||||
return true;
|
||||
} else {
|
||||
warn(tr("Can not add a model to the JSON file"),
|
||||
tr("The imported model is empty or is not supported."));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void CollectionWidget::addProjectImport()
|
||||
{
|
||||
m_view->addProjectImport();
|
||||
}
|
||||
|
||||
void CollectionWidget::addCollectionToDataStore(const QString &collectionName)
|
||||
{
|
||||
m_view->addNewCollection(collectionName, CollectionEditorUtils::defaultCollection());
|
||||
}
|
||||
|
||||
void CollectionWidget::assignCollectionToSelectedNode(const QString collectionName)
|
||||
{
|
||||
m_view->assignCollectionToSelectedNode(collectionName);
|
||||
}
|
||||
|
||||
void CollectionWidget::openCollection(const QString &collectionName)
|
||||
{
|
||||
m_listModel->selectCollectionName(collectionName);
|
||||
QmlDesignerPlugin::instance()->mainWidget()->showDockWidget("CollectionEditor", true);
|
||||
}
|
||||
|
||||
ModelNode CollectionWidget::dataStoreNode() const
|
||||
{
|
||||
return m_view->dataStoreNode();
|
||||
}
|
||||
|
||||
void CollectionWidget::warn(const QString &title, const QString &body)
|
||||
{
|
||||
QMetaObject::invokeMethod(m_quickWidget->rootObject(),
|
||||
"showWarning",
|
||||
Q_ARG(QVariant, title),
|
||||
Q_ARG(QVariant, body));
|
||||
}
|
||||
|
||||
void CollectionWidget::setTargetNodeSelected(bool selected)
|
||||
{
|
||||
if (m_targetNodeSelected == selected)
|
||||
return;
|
||||
|
||||
m_targetNodeSelected = selected;
|
||||
emit targetNodeSelectedChanged(m_targetNodeSelected);
|
||||
}
|
||||
|
||||
void CollectionWidget::setProjectImportExists(bool exists)
|
||||
{
|
||||
if (m_projectImportExists == exists)
|
||||
return;
|
||||
|
||||
m_projectImportExists = exists;
|
||||
emit projectImportExistsChanged(m_projectImportExists);
|
||||
}
|
||||
|
||||
void CollectionWidget::setDataStoreExists(bool exists)
|
||||
{
|
||||
if (m_dataStoreExists == exists)
|
||||
return;
|
||||
|
||||
m_dataStoreExists = exists;
|
||||
emit dataStoreExistsChanged(m_dataStoreExists);
|
||||
}
|
||||
|
||||
void CollectionWidget::deleteSelectedCollection()
|
||||
{
|
||||
QMetaObject::invokeMethod(m_quickWidget->quickWidget()->rootObject(), "deleteSelectedCollection");
|
||||
}
|
||||
|
||||
} // namespace QmlDesigner
|
@@ -1,84 +0,0 @@
|
||||
// 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>
|
||||
|
||||
#include <utils/uniqueobjectptr.h>
|
||||
|
||||
class StudioQuickWidget;
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
class CollectionDetailsModel;
|
||||
class CollectionDetailsSortFilterModel;
|
||||
class CollectionListModel;
|
||||
class CollectionView;
|
||||
class ModelNode;
|
||||
|
||||
class CollectionWidget : public QFrame
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(bool targetNodeSelected MEMBER m_targetNodeSelected NOTIFY targetNodeSelectedChanged)
|
||||
Q_PROPERTY(bool projectImportExists MEMBER m_projectImportExists NOTIFY projectImportExistsChanged)
|
||||
Q_PROPERTY(bool dataStoreExists MEMBER m_dataStoreExists NOTIFY dataStoreExistsChanged)
|
||||
|
||||
public:
|
||||
CollectionWidget(CollectionView *view);
|
||||
~CollectionWidget();
|
||||
void contextHelp(const Core::IContext::HelpCallback &callback) const;
|
||||
|
||||
QPointer<CollectionListModel> listModel() const;
|
||||
QPointer<CollectionDetailsModel> collectionDetailsModel() const;
|
||||
|
||||
void reloadQmlSource();
|
||||
|
||||
QSize minimumSizeHint() const override;
|
||||
|
||||
Q_INVOKABLE bool loadJsonFile(const QUrl &url, const QString &collectionName = {});
|
||||
Q_INVOKABLE bool loadCsvFile(const QUrl &url, const QString &collectionName = {});
|
||||
Q_INVOKABLE bool isJsonFile(const QUrl &url) const;
|
||||
Q_INVOKABLE bool isCsvFile(const QUrl &url) const;
|
||||
Q_INVOKABLE bool isValidUrlToImport(const QUrl &url) const;
|
||||
|
||||
Q_INVOKABLE bool importFile(const QString &collectionName,
|
||||
const QUrl &url,
|
||||
const bool &firstRowIsHeader = true);
|
||||
|
||||
Q_INVOKABLE void addProjectImport();
|
||||
Q_INVOKABLE void addCollectionToDataStore(const QString &collectionName);
|
||||
Q_INVOKABLE void assignCollectionToSelectedNode(const QString collectionName);
|
||||
Q_INVOKABLE void openCollection(const QString &collectionName);
|
||||
Q_INVOKABLE ModelNode dataStoreNode() const;
|
||||
|
||||
void warn(const QString &title, const QString &body);
|
||||
void setTargetNodeSelected(bool selected);
|
||||
void setProjectImportExists(bool exists);
|
||||
void setDataStoreExists(bool exists);
|
||||
|
||||
void deleteSelectedCollection();
|
||||
|
||||
signals:
|
||||
void targetNodeSelectedChanged(bool);
|
||||
void projectImportExistsChanged(bool);
|
||||
void dataStoreExistsChanged(bool);
|
||||
|
||||
private:
|
||||
QString generateUniqueCollectionName(const ModelNode &node, const QString &name);
|
||||
|
||||
QPointer<CollectionView> m_view;
|
||||
QPointer<CollectionListModel> m_listModel;
|
||||
QPointer<CollectionDetailsModel> m_collectionDetailsModel;
|
||||
QPointer<Core::IContext> m_iContext;
|
||||
std::unique_ptr<CollectionDetailsSortFilterModel> m_collectionDetailsSortFilterModel;
|
||||
Utils::UniqueObjectPtr<StudioQuickWidget> m_quickWidget;
|
||||
bool m_targetNodeSelected = false;
|
||||
bool m_projectImportExists = false;
|
||||
bool m_dataStoreExists = false;
|
||||
};
|
||||
|
||||
} // namespace QmlDesigner
|
@@ -1,517 +0,0 @@
|
||||
// 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 "datastoremodelnode.h"
|
||||
|
||||
#include "abstractview.h"
|
||||
#include "collectioneditorconstants.h"
|
||||
#include "collectioneditorutils.h"
|
||||
#include "model/qmltextgenerator.h"
|
||||
#include "plaintexteditmodifier.h"
|
||||
#include "qmldesignerbase/qmldesignerbaseplugin.h"
|
||||
#include "qmldesignerexternaldependencies.h"
|
||||
#include "rewriterview.h"
|
||||
|
||||
#include <model.h>
|
||||
#include <nodemetainfo.h>
|
||||
#include <nodeproperty.h>
|
||||
#include <variantproperty.h>
|
||||
|
||||
#include <qmljstools/qmljscodestylepreferences.h>
|
||||
#include <qmljstools/qmljstoolssettings.h>
|
||||
|
||||
#include <coreplugin/documentmanager.h>
|
||||
#include <projectexplorer/projectexplorer.h>
|
||||
|
||||
#include <utils/fileutils.h>
|
||||
#include <utils/qtcassert.h>
|
||||
|
||||
#include <QPlainTextEdit>
|
||||
#include <QRegularExpression>
|
||||
#include <QRegularExpressionMatch>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace {
|
||||
|
||||
inline constexpr char CHILDLISTMODEL_TYPENAME[] = "ChildListModel";
|
||||
|
||||
QmlDesigner::PropertyNameList createNameList(const QmlDesigner::ModelNode &node)
|
||||
{
|
||||
using QmlDesigner::AbstractProperty;
|
||||
using QmlDesigner::PropertyName;
|
||||
using QmlDesigner::PropertyNameList;
|
||||
static PropertyNameList defaultsNodeProps = {
|
||||
"id",
|
||||
QmlDesigner::CollectionEditorConstants::SOURCEFILE_PROPERTY,
|
||||
QmlDesigner::CollectionEditorConstants::JSONCHILDMODELNAME_PROPERTY,
|
||||
"backend"};
|
||||
PropertyNameList dynamicPropertyNames = Utils::transform(
|
||||
node.dynamicProperties(),
|
||||
[](const AbstractProperty &property) -> PropertyName { return property.name(); });
|
||||
|
||||
Utils::sort(dynamicPropertyNames);
|
||||
|
||||
return defaultsNodeProps + dynamicPropertyNames;
|
||||
}
|
||||
|
||||
bool isValidCollectionPropertyName(const QString &collectionId)
|
||||
{
|
||||
static const QmlDesigner::PropertyNameList reservedKeywords = {
|
||||
QmlDesigner::CollectionEditorConstants::SOURCEFILE_PROPERTY,
|
||||
QmlDesigner::CollectionEditorConstants::JSONBACKEND_TYPENAME,
|
||||
"backend",
|
||||
"models",
|
||||
};
|
||||
|
||||
return QmlDesigner::ModelNode::isValidId(collectionId)
|
||||
&& !reservedKeywords.contains(collectionId.toLatin1());
|
||||
}
|
||||
|
||||
QMap<QString, QmlDesigner::PropertyName> getModelIdMap(const QmlDesigner::ModelNode &rootNode)
|
||||
{
|
||||
using namespace QmlDesigner;
|
||||
QMap<QString, PropertyName> modelNameForId;
|
||||
|
||||
const QList<AbstractProperty> propertyNames = rootNode.dynamicProperties();
|
||||
|
||||
for (const AbstractProperty &property : std::as_const(propertyNames)) {
|
||||
if (!property.isNodeProperty())
|
||||
continue;
|
||||
|
||||
NodeProperty nodeProperty = property.toNodeProperty();
|
||||
if (!nodeProperty.hasDynamicTypeName(CHILDLISTMODEL_TYPENAME))
|
||||
continue;
|
||||
|
||||
ModelNode childNode = nodeProperty.modelNode();
|
||||
if (childNode.hasProperty(CollectionEditorConstants::JSONCHILDMODELNAME_PROPERTY)) {
|
||||
QString modelName = childNode
|
||||
.property(CollectionEditorConstants::JSONCHILDMODELNAME_PROPERTY)
|
||||
.toVariantProperty()
|
||||
.value()
|
||||
.toString();
|
||||
|
||||
if (!modelName.isEmpty())
|
||||
modelNameForId.insert(modelName, property.name());
|
||||
}
|
||||
}
|
||||
return modelNameForId;
|
||||
}
|
||||
|
||||
void setQmlContextToModel(QmlDesigner::Model *model, const QString &qmlContext)
|
||||
{
|
||||
using namespace QmlDesigner;
|
||||
Q_ASSERT(model);
|
||||
|
||||
std::unique_ptr<QPlainTextEdit> textEdit = std::make_unique<QPlainTextEdit>();
|
||||
std::unique_ptr<NotIndentingTextEditModifier> modifier = std::make_unique<NotIndentingTextEditModifier>(
|
||||
textEdit.get());
|
||||
textEdit->hide();
|
||||
textEdit->setPlainText(qmlContext);
|
||||
QmlDesigner::ExternalDependencies externalDependencies{QmlDesignerBasePlugin::settings()};
|
||||
std::unique_ptr<RewriterView> rewriter = std::make_unique<RewriterView>(
|
||||
externalDependencies, QmlDesigner::RewriterView::Validate);
|
||||
|
||||
rewriter->setTextModifier(modifier.get());
|
||||
rewriter->setCheckSemanticErrors(false);
|
||||
|
||||
model->attachView(rewriter.get());
|
||||
model->detachView(rewriter.get());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
DataStoreModelNode::DataStoreModelNode() = default;
|
||||
|
||||
void DataStoreModelNode::reloadModel(const Utils::FilePath &projectModulePath)
|
||||
{
|
||||
using Utils::FilePath;
|
||||
if (!projectModulePath.exists()) {
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
bool forceUpdate = false;
|
||||
|
||||
const FilePath dataStoreQmlPath = projectModulePath.resolvePath(
|
||||
CollectionEditorConstants::DEFAULT_DATASTORE_QML_FILENAME.toString());
|
||||
const FilePath dataStoreJsonPath = projectModulePath.resolvePath(
|
||||
CollectionEditorConstants::DEFAULT_MODELS_JSON_FILENAME.toString());
|
||||
QUrl dataStoreQmlUrl = dataStoreQmlPath.toUrl();
|
||||
|
||||
if (dataStoreQmlPath.exists() && dataStoreJsonPath.exists()) {
|
||||
if (!m_model.get() || m_model->fileUrl() != dataStoreQmlUrl) {
|
||||
#ifdef QDS_USE_PROJECTSTORAGE
|
||||
m_model = model()->createModel("JsonListModel");
|
||||
forceUpdate = true;
|
||||
Import import = Import::createLibraryImport("QtQuick.Studio.Utils");
|
||||
m_model->changeImports({import}, {});
|
||||
#else
|
||||
m_model = Model::create(CollectionEditorConstants::JSONCOLLECTIONMODEL_TYPENAME, 1, 1);
|
||||
forceUpdate = true;
|
||||
Import import = Import::createLibraryImport(
|
||||
CollectionEditorConstants::COLLECTIONMODEL_IMPORT);
|
||||
try {
|
||||
if (!m_model->hasImport(import, true, true))
|
||||
m_model->changeImports({import}, {});
|
||||
} catch (const Exception &) {
|
||||
QTC_ASSERT(false, return);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
|
||||
if (!m_model.get())
|
||||
return;
|
||||
|
||||
if (forceUpdate) {
|
||||
m_model->setFileUrl(dataStoreQmlUrl);
|
||||
m_dataRelativePath = dataStoreJsonPath.relativePathFrom(dataStoreQmlPath).toFSPathString();
|
||||
preloadFile();
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
QStringList DataStoreModelNode::collectionNames() const
|
||||
{
|
||||
return m_collectionPropertyNames.keys();
|
||||
}
|
||||
|
||||
Model *DataStoreModelNode::model() const
|
||||
{
|
||||
return m_model.get();
|
||||
}
|
||||
|
||||
ModelNode DataStoreModelNode::modelNode() const
|
||||
{
|
||||
if (!m_model.get())
|
||||
return {};
|
||||
return m_model->rootModelNode();
|
||||
}
|
||||
|
||||
Utils::FilePath DataStoreModelNode::jsonFilePath() const
|
||||
{
|
||||
QUrl modelUrl = m_model->fileUrl();
|
||||
return Utils::FilePath::fromUserInput(modelUrl.isLocalFile() ? modelUrl.toLocalFile()
|
||||
: modelUrl.toString())
|
||||
.parentDir()
|
||||
.resolvePath(CollectionEditorConstants::DEFAULT_MODELS_JSON_FILENAME.toString());
|
||||
}
|
||||
|
||||
QString DataStoreModelNode::getModelQmlText()
|
||||
{
|
||||
ModelNode node = modelNode();
|
||||
QTC_ASSERT(node, return {});
|
||||
|
||||
Internal::QmlTextGenerator textGen(createNameList(node),
|
||||
QmlJSTools::QmlJSToolsSettings::globalCodeStyle()->tabSettings());
|
||||
|
||||
QString genText = textGen(node);
|
||||
return genText;
|
||||
}
|
||||
|
||||
void DataStoreModelNode::reset()
|
||||
{
|
||||
if (m_model)
|
||||
m_model.reset();
|
||||
|
||||
m_dataRelativePath.clear();
|
||||
setCollectionNames({});
|
||||
}
|
||||
|
||||
void DataStoreModelNode::preloadFile()
|
||||
{
|
||||
using Utils::FilePath;
|
||||
using Utils::FileReader;
|
||||
|
||||
if (!m_model)
|
||||
return;
|
||||
|
||||
const FilePath dataStoreQmlPath = dataStoreQmlFilePath();
|
||||
FileReader dataStoreQmlFile;
|
||||
QString sourceQmlContext;
|
||||
|
||||
if (dataStoreQmlFile.fetch(dataStoreQmlPath))
|
||||
sourceQmlContext = QString::fromLatin1(dataStoreQmlFile.data());
|
||||
|
||||
setQmlContextToModel(m_model.get(), sourceQmlContext);
|
||||
m_collectionPropertyNames = getModelIdMap(m_model->rootModelNode());
|
||||
}
|
||||
|
||||
void DataStoreModelNode::updateDataStoreProperties()
|
||||
{
|
||||
QTC_ASSERT(model(), return);
|
||||
|
||||
ModelNode rootNode = modelNode();
|
||||
QTC_ASSERT(rootNode.isValid(), return);
|
||||
|
||||
QSet<QString> collectionNamesToBeAdded;
|
||||
const QStringList allCollectionNames = m_collectionPropertyNames.keys();
|
||||
for (const QString &collectionName : allCollectionNames)
|
||||
collectionNamesToBeAdded << collectionName;
|
||||
|
||||
const QList<AbstractProperty> formerPropertyNames = rootNode.dynamicProperties();
|
||||
|
||||
// Remove invalid collection names from the properties
|
||||
for (const AbstractProperty &property : formerPropertyNames) {
|
||||
if (!property.isNodeProperty())
|
||||
continue;
|
||||
|
||||
NodeProperty nodeProprty = property.toNodeProperty();
|
||||
if (!nodeProprty.hasDynamicTypeName(CHILDLISTMODEL_TYPENAME))
|
||||
continue;
|
||||
|
||||
ModelNode childNode = nodeProprty.modelNode();
|
||||
if (childNode.hasProperty(CollectionEditorConstants::JSONCHILDMODELNAME_PROPERTY)) {
|
||||
QString modelName = childNode
|
||||
.property(CollectionEditorConstants::JSONCHILDMODELNAME_PROPERTY)
|
||||
.toVariantProperty()
|
||||
.value()
|
||||
.toString();
|
||||
if (collectionNamesToBeAdded.contains(modelName)) {
|
||||
m_collectionPropertyNames.insert(modelName, property.name());
|
||||
collectionNamesToBeAdded.remove(modelName);
|
||||
} else {
|
||||
rootNode.removeProperty(property.name());
|
||||
}
|
||||
} else {
|
||||
rootNode.removeProperty(property.name());
|
||||
}
|
||||
}
|
||||
|
||||
rootNode.setIdWithoutRefactoring("models");
|
||||
|
||||
QStringList collectionNamesLeft = collectionNamesToBeAdded.values();
|
||||
Utils::sort(collectionNamesLeft);
|
||||
for (const QString &collectionName : std::as_const(collectionNamesLeft))
|
||||
addCollectionNameToTheModel(collectionName, getUniquePropertyName(collectionName));
|
||||
|
||||
// Backend Property
|
||||
ModelNode backendNode = model()->createModelNode(CollectionEditorConstants::JSONBACKEND_TYPENAME);
|
||||
NodeProperty backendProperty = rootNode.nodeProperty("backend");
|
||||
backendProperty.setDynamicTypeNameAndsetModelNode(CollectionEditorConstants::JSONBACKEND_TYPENAME,
|
||||
backendNode);
|
||||
// Source Property
|
||||
VariantProperty sourceProp = rootNode.variantProperty(
|
||||
CollectionEditorConstants::SOURCEFILE_PROPERTY);
|
||||
sourceProp.setValue(m_dataRelativePath);
|
||||
}
|
||||
|
||||
void DataStoreModelNode::updateSingletonFile()
|
||||
{
|
||||
using Utils::FilePath;
|
||||
using Utils::FileSaver;
|
||||
QTC_ASSERT(m_model.get(), return);
|
||||
|
||||
const QString pragmaSingleTone = "pragma Singleton\n";
|
||||
QString imports;
|
||||
|
||||
for (const Import &import : m_model->imports())
|
||||
imports += QStringLiteral("import %1\n").arg(import.toString(true));
|
||||
|
||||
QString content = pragmaSingleTone + imports + getModelQmlText();
|
||||
Core::DocumentManager::expectFileChange(dataStoreQmlFilePath());
|
||||
FileSaver file(dataStoreQmlFilePath());
|
||||
file.write(content.toLatin1());
|
||||
file.finalize();
|
||||
}
|
||||
|
||||
void DataStoreModelNode::update()
|
||||
{
|
||||
if (!m_model.get())
|
||||
return;
|
||||
|
||||
updateDataStoreProperties();
|
||||
updateSingletonFile();
|
||||
}
|
||||
|
||||
void DataStoreModelNode::addCollectionNameToTheModel(const QString &collectionName,
|
||||
const PropertyName &dataStorePropertyName)
|
||||
{
|
||||
ModelNode rootNode = modelNode();
|
||||
QTC_ASSERT(rootNode.isValid(), return);
|
||||
|
||||
if (dataStorePropertyName.isEmpty()) {
|
||||
qWarning() << __FUNCTION__ << __LINE__
|
||||
<< QString("The property name cannot be generated from \"%1\"").arg(collectionName);
|
||||
return;
|
||||
}
|
||||
|
||||
ModelNode collectionNode = model()->createModelNode(CHILDLISTMODEL_TYPENAME);
|
||||
VariantProperty modelNameProperty = collectionNode.variantProperty(
|
||||
CollectionEditorConstants::JSONCHILDMODELNAME_PROPERTY);
|
||||
modelNameProperty.setValue(collectionName);
|
||||
|
||||
NodeProperty nodeProp = rootNode.nodeProperty(dataStorePropertyName);
|
||||
nodeProp.setDynamicTypeNameAndsetModelNode(CHILDLISTMODEL_TYPENAME, collectionNode);
|
||||
|
||||
m_collectionPropertyNames.insert(collectionName, dataStorePropertyName);
|
||||
}
|
||||
|
||||
Utils::FilePath DataStoreModelNode::dataStoreQmlFilePath() const
|
||||
{
|
||||
QUrl modelUrl = m_model->fileUrl();
|
||||
return Utils::FilePath::fromUserInput(modelUrl.isLocalFile() ? modelUrl.toLocalFile()
|
||||
: modelUrl.toString());
|
||||
}
|
||||
|
||||
PropertyName DataStoreModelNode::getUniquePropertyName(const QString &collectionName)
|
||||
{
|
||||
ModelNode dataStoreNode = modelNode();
|
||||
QTC_ASSERT(!collectionName.isEmpty() && dataStoreNode.isValid(), return {});
|
||||
|
||||
QString newProperty;
|
||||
|
||||
// convert to camel case
|
||||
QStringList nameWords = collectionName.split(' ');
|
||||
nameWords[0] = nameWords[0].at(0).toLower() + nameWords[0].mid(1);
|
||||
for (int i = 1; i < nameWords.size(); ++i)
|
||||
nameWords[i] = nameWords[i].at(0).toUpper() + nameWords[i].mid(1);
|
||||
newProperty = nameWords.join("");
|
||||
|
||||
// if id starts with a number prepend an underscore
|
||||
if (newProperty.at(0).isDigit())
|
||||
newProperty.prepend('_');
|
||||
|
||||
// If the new id is not valid (e.g. qml keyword match), prepend an underscore
|
||||
if (!isValidCollectionPropertyName(newProperty))
|
||||
newProperty.prepend('_');
|
||||
|
||||
static const QRegularExpression rgx("\\d+$"); // matches a number at the end of a string
|
||||
while (dataStoreNode.hasProperty(newProperty.toLatin1())) { // id exists
|
||||
QRegularExpressionMatch match = rgx.match(newProperty);
|
||||
if (match.hasMatch()) { // ends with a number, increment it
|
||||
QString numStr = match.captured();
|
||||
int num = numStr.toInt() + 1;
|
||||
newProperty = newProperty.mid(0, match.capturedStart()) + QString::number(num);
|
||||
} else {
|
||||
newProperty.append('1');
|
||||
}
|
||||
}
|
||||
|
||||
return newProperty.toLatin1();
|
||||
}
|
||||
|
||||
void DataStoreModelNode::setCollectionNames(const QStringList &newCollectionNames)
|
||||
{
|
||||
m_collectionPropertyNames.clear();
|
||||
for (const QString &collectionName : newCollectionNames)
|
||||
m_collectionPropertyNames.insert(collectionName, {});
|
||||
update();
|
||||
}
|
||||
|
||||
void DataStoreModelNode::addCollection(const QString &collectionName)
|
||||
{
|
||||
if (!m_collectionPropertyNames.contains(collectionName)) {
|
||||
m_collectionPropertyNames.insert(collectionName, {});
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void DataStoreModelNode::renameCollection(const QString &oldName, const QString &newName)
|
||||
{
|
||||
ModelNode dataStoreNode = modelNode();
|
||||
QTC_ASSERT(dataStoreNode.isValid(), return);
|
||||
|
||||
if (m_collectionPropertyNames.contains(oldName)) {
|
||||
const PropertyName oldPropertyName = m_collectionPropertyNames.value(oldName);
|
||||
if (!oldPropertyName.isEmpty() && dataStoreNode.hasProperty(oldPropertyName)) {
|
||||
NodeProperty collectionNode = dataStoreNode.property(oldPropertyName).toNodeProperty();
|
||||
if (collectionNode.isValid()) {
|
||||
VariantProperty modelNameProperty = collectionNode.modelNode().variantProperty(
|
||||
CollectionEditorConstants::JSONCHILDMODELNAME_PROPERTY);
|
||||
modelNameProperty.setValue(newName);
|
||||
m_collectionPropertyNames.remove(oldName);
|
||||
m_collectionPropertyNames.insert(newName, collectionNode.name());
|
||||
update();
|
||||
return;
|
||||
}
|
||||
qWarning() << __FUNCTION__ << __LINE__
|
||||
<< "There is no valid node for the old collection name";
|
||||
return;
|
||||
}
|
||||
qWarning() << __FUNCTION__ << __LINE__ << QString("Invalid old property name")
|
||||
<< oldPropertyName;
|
||||
return;
|
||||
}
|
||||
qWarning() << __FUNCTION__ << __LINE__
|
||||
<< QString("There is no old collection name registered with this name \"%1\"").arg(oldName);
|
||||
}
|
||||
|
||||
void DataStoreModelNode::removeCollections(const QStringList &collectionNames)
|
||||
{
|
||||
bool updateRequired = false;
|
||||
for (const QString &collectionName : collectionNames) {
|
||||
if (m_collectionPropertyNames.contains(collectionName)) {
|
||||
m_collectionPropertyNames.remove(collectionName);
|
||||
updateRequired = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateRequired)
|
||||
update();
|
||||
}
|
||||
|
||||
void DataStoreModelNode::assignCollectionToNode(AbstractView *view,
|
||||
const ModelNode &targetNode,
|
||||
const QString &collectionName,
|
||||
CollectionColumnFinder collectionHasColumn,
|
||||
FirstColumnProvider firstColumnProvider)
|
||||
{
|
||||
QTC_ASSERT(targetNode.isValid(), return);
|
||||
|
||||
if (!CollectionEditorUtils::canAcceptCollectionAsModel(targetNode))
|
||||
return;
|
||||
|
||||
if (!m_collectionPropertyNames.contains(collectionName)) {
|
||||
qWarning() << __FUNCTION__ << __LINE__ << "Collection doesn't exist in the DataStore"
|
||||
<< collectionName;
|
||||
return;
|
||||
}
|
||||
|
||||
PropertyName propertyName = m_collectionPropertyNames.value(collectionName);
|
||||
|
||||
const ModelNode dataStore = modelNode();
|
||||
VariantProperty sourceProperty = dataStore.variantProperty(propertyName);
|
||||
if (!sourceProperty.exists()) {
|
||||
qWarning() << __FUNCTION__ << __LINE__
|
||||
<< "The source property doesn't exist in the DataStore.";
|
||||
return;
|
||||
}
|
||||
|
||||
view->executeInTransaction("assignCollectionToNode", [&]() {
|
||||
QString identifier = QString("DataStore.%1").arg(QString::fromLatin1(sourceProperty.name()));
|
||||
|
||||
// Remove the old model node property if exists
|
||||
NodeProperty modelNodeProperty = targetNode.nodeProperty("model");
|
||||
if (modelNodeProperty.modelNode())
|
||||
modelNodeProperty.modelNode().destroy();
|
||||
|
||||
// Assign the collection to the node
|
||||
BindingProperty modelProperty = targetNode.bindingProperty("model");
|
||||
modelProperty.setExpression(identifier);
|
||||
|
||||
if (CollectionEditorUtils::hasTextRoleProperty(targetNode)) {
|
||||
VariantProperty textRoleProperty = targetNode.variantProperty("textRole");
|
||||
const QVariant currentTextRoleValue = textRoleProperty.value();
|
||||
|
||||
if (currentTextRoleValue.isValid() && !currentTextRoleValue.isNull()) {
|
||||
if (currentTextRoleValue.type() == QVariant::String) {
|
||||
const QString currentTextRole = currentTextRoleValue.toString();
|
||||
if (collectionHasColumn(collectionName, currentTextRole))
|
||||
return;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QString textRoleValue = firstColumnProvider(collectionName);
|
||||
textRoleProperty.setValue(textRoleValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace QmlDesigner
|
@@ -1,64 +0,0 @@
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <modelnode.h>
|
||||
|
||||
#include <QMap>
|
||||
|
||||
namespace Utils {
|
||||
class FilePath;
|
||||
}
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
class Model;
|
||||
|
||||
class DataStoreModelNode
|
||||
{
|
||||
public:
|
||||
using CollectionColumnFinder = std::function<bool(const QString &collectionName,
|
||||
const QString &columnName)>;
|
||||
using FirstColumnProvider = std::function<QString(const QString &collectionName)>;
|
||||
|
||||
DataStoreModelNode();
|
||||
|
||||
void reloadModel(const Utils::FilePath &projectModulePath);
|
||||
QStringList collectionNames() const;
|
||||
|
||||
Model *model() const;
|
||||
ModelNode modelNode() const;
|
||||
Utils::FilePath jsonFilePath() const;
|
||||
|
||||
void setCollectionNames(const QStringList &newCollectionNames);
|
||||
void addCollection(const QString &collectionName);
|
||||
void renameCollection(const QString &oldName, const QString &newName);
|
||||
void removeCollections(const QStringList &collectionNames);
|
||||
|
||||
void assignCollectionToNode(AbstractView *view,
|
||||
const ModelNode &targetNode,
|
||||
const QString &collectionName,
|
||||
CollectionColumnFinder collectionHasColumn,
|
||||
FirstColumnProvider firstColumnProvider);
|
||||
|
||||
private:
|
||||
QString getModelQmlText();
|
||||
|
||||
void reset();
|
||||
void preloadFile();
|
||||
void updateDataStoreProperties();
|
||||
void updateSingletonFile();
|
||||
void update();
|
||||
void addCollectionNameToTheModel(const QString &collectionName,
|
||||
const PropertyName &dataStorePropertyName);
|
||||
Utils::FilePath dataStoreQmlFilePath() const;
|
||||
|
||||
PropertyName getUniquePropertyName(const QString &collectionName);
|
||||
|
||||
ModelPointer m_model;
|
||||
QMap<QString, PropertyName> m_collectionPropertyNames;
|
||||
QString m_dataRelativePath;
|
||||
};
|
||||
|
||||
} // namespace QmlDesigner
|
@@ -69,7 +69,6 @@ const char mergeTemplateCommandId[] = "MergeTemplate";
|
||||
const char goToImplementationCommandId[] = "GoToImplementation";
|
||||
const char makeComponentCommandId[] = "MakeComponent";
|
||||
const char editMaterialCommandId[] = "EditMaterial";
|
||||
const char editCollectionCommandId[] = "EditCollection";
|
||||
const char addItemToStackedContainerCommandId[] = "AddItemToStackedContainer";
|
||||
const char addTabBarToStackedContainerCommandId[] = "AddTabBarToStackedContainer";
|
||||
const char increaseIndexOfStackedContainerCommandId[] = "IncreaseIndexOfStackedContainer";
|
||||
@@ -128,7 +127,6 @@ const char mergeTemplateDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMen
|
||||
const char goToImplementationDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Go to Implementation");
|
||||
const char makeComponentDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Create Component");
|
||||
const char editMaterialDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Edit Material");
|
||||
const char editCollectionDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Edit Model");
|
||||
const char editAnnotationsDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Edit Annotations");
|
||||
const char addMouseAreaFillDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Add Mouse Area");
|
||||
const char editIn3dViewDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Edit in 3D View");
|
||||
@@ -214,7 +212,6 @@ enum PrioritiesEnum : int {
|
||||
ArrangeCategory,
|
||||
EditCategory,
|
||||
EditListModel,
|
||||
EditCollection,
|
||||
/******** Section *****************************/
|
||||
PositionSection = 2000,
|
||||
SnappingCategory,
|
||||
|
@@ -113,7 +113,6 @@ void DesignerActionManager::polishActions() const
|
||||
Core::Context qmlDesignerNavigatorContext(Constants::C_QMLNAVIGATOR);
|
||||
Core::Context qmlDesignerMaterialBrowserContext(Constants::C_QMLMATERIALBROWSER);
|
||||
Core::Context qmlDesignerAssetsLibraryContext(Constants::C_QMLASSETSLIBRARY);
|
||||
Core::Context qmlDesignerCollectionEditorContext(Constants::C_QMLCOLLECTIONEDITOR);
|
||||
|
||||
Core::Context qmlDesignerUIContext;
|
||||
qmlDesignerUIContext.add(qmlDesignerFormEditorContext);
|
||||
@@ -121,7 +120,6 @@ void DesignerActionManager::polishActions() const
|
||||
qmlDesignerUIContext.add(qmlDesignerNavigatorContext);
|
||||
qmlDesignerUIContext.add(qmlDesignerMaterialBrowserContext);
|
||||
qmlDesignerUIContext.add(qmlDesignerAssetsLibraryContext);
|
||||
qmlDesignerUIContext.add(qmlDesignerCollectionEditorContext);
|
||||
|
||||
for (auto *action : actions) {
|
||||
if (!action->menuId().isEmpty()) {
|
||||
@@ -2011,16 +2009,6 @@ void DesignerActionManager::createDefaultDesignerActions()
|
||||
|
||||
addDesignerAction(new EditListModelAction);
|
||||
|
||||
addDesignerAction(new ModelNodeContextMenuAction(editCollectionCommandId,
|
||||
editCollectionDisplayName,
|
||||
contextIcon(DesignerIcons::EditIcon),
|
||||
rootCategory,
|
||||
QKeySequence("Alt+e"),
|
||||
ComponentCoreConstants::Priorities::EditCollection,
|
||||
&editCollection,
|
||||
&hasCollectionAsModel,
|
||||
&hasCollectionAsModel));
|
||||
|
||||
addDesignerAction(new ModelNodeContextMenuAction(openSignalDialogCommandId,
|
||||
openSignalDialogDisplayName,
|
||||
{},
|
||||
|
@@ -82,18 +82,6 @@ inline bool hasEditableMaterial(const SelectionContext &selectionState)
|
||||
return prop.exists() && (!prop.expression().isEmpty() || !prop.resolveToModelNodeList().empty());
|
||||
}
|
||||
|
||||
inline bool hasCollectionAsModel(const SelectionContext &selectionState)
|
||||
{
|
||||
if (!selectionState.isInBaseState() || !selectionState.singleNodeIsSelected())
|
||||
return false;
|
||||
|
||||
const ModelNode singleSelectedNode = selectionState.currentSingleSelectedNode();
|
||||
|
||||
return singleSelectedNode.metaInfo().isListOrGridView()
|
||||
&& singleSelectedNode.property("model").toBindingProperty().expression().startsWith(
|
||||
"DataStore.");
|
||||
}
|
||||
|
||||
inline bool selectionEnabled(const SelectionContext &selectionState)
|
||||
{
|
||||
return selectionState.showSelectionTools();
|
||||
|
@@ -846,30 +846,6 @@ void editMaterial(const SelectionContext &selectionContext)
|
||||
}
|
||||
}
|
||||
|
||||
// Open a collection in the collection editor
|
||||
void editCollection(const SelectionContext &selectionContext)
|
||||
{
|
||||
ModelNode modelNode = selectionContext.targetNode();
|
||||
|
||||
if (!modelNode)
|
||||
modelNode = selectionContext.currentSingleSelectedNode();
|
||||
|
||||
if (!modelNode)
|
||||
return;
|
||||
|
||||
const QString dataStoreExpression = "DataStore.";
|
||||
|
||||
BindingProperty prop = modelNode.bindingProperty("model");
|
||||
if (!prop.exists() || !prop.expression().startsWith(dataStoreExpression))
|
||||
return;
|
||||
|
||||
AbstractView *view = selectionContext.view();
|
||||
const QString collectionId = prop.expression().mid(dataStoreExpression.size());
|
||||
|
||||
// to CollectionEditor...
|
||||
view->emitCustomNotification("open_collection_by_id", {}, {collectionId});
|
||||
}
|
||||
|
||||
void addItemToStackedContainer(const SelectionContext &selectionContext)
|
||||
{
|
||||
AbstractView *view = selectionContext.view();
|
||||
|
@@ -92,7 +92,6 @@ void layoutGridLayout(const SelectionContext &selectionState);
|
||||
void goImplementation(const SelectionContext &selectionState);
|
||||
void addNewSignalHandler(const SelectionContext &selectionState);
|
||||
void editMaterial(const SelectionContext &selectionContext);
|
||||
void editCollection(const SelectionContext &selectionContext);
|
||||
void addSignalHandlerOrGotoImplementation(const SelectionContext &selectionState, bool addAlwaysNewSlot);
|
||||
void removeLayout(const SelectionContext &selectionContext);
|
||||
void removePositioner(const SelectionContext &selectionContext);
|
||||
|
@@ -7,7 +7,6 @@
|
||||
#include <abstractview.h>
|
||||
#include <assetslibraryview.h>
|
||||
#include <capturingconnectionmanager.h>
|
||||
#include <collectionview.h>
|
||||
#include <componentaction.h>
|
||||
#include <componentview.h>
|
||||
#include <contentlibraryview.h>
|
||||
@@ -42,14 +41,6 @@
|
||||
|
||||
namespace QmlDesigner {
|
||||
|
||||
static bool enableModelEditor()
|
||||
{
|
||||
Utils::QtcSettings *settings = Core::ICore::settings();
|
||||
const Utils::Key enableModelManagerKey = "QML/Designer/UseExperimentalFeatures44";
|
||||
|
||||
return settings->value(enableModelManagerKey, false).toBool();
|
||||
}
|
||||
|
||||
static Q_LOGGING_CATEGORY(viewBenchmark, "qtc.viewmanager.attach", QtWarningMsg)
|
||||
|
||||
class ViewManagerData
|
||||
@@ -64,7 +55,6 @@ public:
|
||||
: connectionManager,
|
||||
externalDependencies,
|
||||
true)
|
||||
, collectionView{externalDependencies}
|
||||
, contentLibraryView{imageCache, externalDependencies}
|
||||
, componentView{externalDependencies}
|
||||
#ifndef QTC_USE_QML_DESIGNER_LITE
|
||||
@@ -90,7 +80,6 @@ public:
|
||||
Internal::DebugView debugView;
|
||||
DesignerActionManagerView designerActionManagerView;
|
||||
NodeInstanceView nodeInstanceView;
|
||||
CollectionView collectionView;
|
||||
ContentLibraryView contentLibraryView;
|
||||
ComponentView componentView;
|
||||
#ifndef QTC_USE_QML_DESIGNER_LITE
|
||||
@@ -235,9 +224,6 @@ QList<AbstractView *> ViewManager::standardViews() const
|
||||
&d->designerActionManagerView};
|
||||
#endif
|
||||
|
||||
if (enableModelEditor())
|
||||
list.append(&d->collectionView);
|
||||
|
||||
if (QmlDesignerPlugin::instance()
|
||||
->settings()
|
||||
.value(DesignerSettingsKey::ENABLE_DEBUGVIEW)
|
||||
@@ -418,8 +404,6 @@ QList<WidgetInfo> ViewManager::widgetInfos() const
|
||||
widgetInfoList.append(d->textureEditorView.widgetInfo());
|
||||
#endif
|
||||
widgetInfoList.append(d->statesEditorView.widgetInfo());
|
||||
if (enableModelEditor())
|
||||
widgetInfoList.append(d->collectionView.widgetInfo());
|
||||
|
||||
if (checkEnterpriseLicense())
|
||||
widgetInfoList.append(d->contentLibraryView.widgetInfo());
|
||||
|
@@ -3,7 +3,6 @@
|
||||
|
||||
#include "designmodecontext.h"
|
||||
#include "assetslibrarywidget.h"
|
||||
#include "collectionwidget.h"
|
||||
#include "designmodewidget.h"
|
||||
#include "edit3dwidget.h"
|
||||
#include "formeditorwidget.h"
|
||||
@@ -98,15 +97,4 @@ void TextEditorContext::contextHelp(const HelpCallback &callback) const
|
||||
qobject_cast<TextEditorWidget *>(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
|
||||
|
@@ -73,14 +73,5 @@ public:
|
||||
TextEditorContext(QWidget *widget);
|
||||
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
|
||||
|
@@ -19,7 +19,6 @@ inline constexpr char C_QMLNAVIGATOR[] = "QmlDesigner::Navigator";
|
||||
inline constexpr char C_QMLTEXTEDITOR[] = "QmlDesigner::TextEditor";
|
||||
inline constexpr char C_QMLMATERIALBROWSER[] = "QmlDesigner::MaterialBrowser";
|
||||
inline constexpr char C_QMLASSETSLIBRARY[] = "QmlDesigner::AssetsLibrary";
|
||||
inline constexpr char C_QMLCOLLECTIONEDITOR[] = "QmlDesigner::CollectionEditor";
|
||||
|
||||
// Special context for preview menu, shared b/w designer and text editor
|
||||
inline constexpr char C_QT_QUICK_TOOLS_MENU[] = "QmlDesigner::ToolsMenu";
|
||||
@@ -186,7 +185,6 @@ inline constexpr char OBJECT_NAME_EFFECT_COMPOSER[] = "QQuickWidgetEffectCompose
|
||||
inline constexpr char OBJECT_NAME_MATERIAL_BROWSER[] = "QQuickWidgetMaterialBrowser";
|
||||
inline constexpr char OBJECT_NAME_MATERIAL_EDITOR[] = "QQuickWidgetMaterialEditor";
|
||||
inline constexpr char OBJECT_NAME_PROPERTY_EDITOR[] = "QQuickWidgetPropertyEditor";
|
||||
inline constexpr char OBJECT_NAME_COLLECTION_EDITOR[] = "QQuickWidgetQDSCollectionEditor";
|
||||
inline constexpr char OBJECT_NAME_STATES_EDITOR[] = "QQuickWidgetStatesEditor";
|
||||
inline constexpr char OBJECT_NAME_TEXTURE_EDITOR[] = "QQuickWidgetTextureEditor";
|
||||
inline constexpr char OBJECT_NAME_TOP_TOOLBAR[] = "QQuickWidgetTopToolbar";
|
||||
|
@@ -4,7 +4,6 @@
|
||||
#include "qmldesignerplugin.h"
|
||||
#include "qmldesignertr.h"
|
||||
|
||||
#include "collectioneditor/collectionview.h"
|
||||
#include "coreplugin/iwizardfactory.h"
|
||||
#include "designmodecontext.h"
|
||||
#include "designmodewidget.h"
|
||||
@@ -298,7 +297,6 @@ bool QmlDesignerPlugin::initialize(const QStringList & /*arguments*/, QString *e
|
||||
|
||||
//TODO Move registering those types out of the property editor, since they are used also in the states editor
|
||||
Quick2PropertyEditorView::registerQmlTypes();
|
||||
CollectionView::registerDeclarativeType();
|
||||
StudioQuickWidget::registerDeclarativeType();
|
||||
QmlDesignerBase::WindowManager::registerDeclarativeType();
|
||||
|
||||
@@ -392,7 +390,6 @@ void QmlDesignerPlugin::integrateIntoQtCreator(QWidget *modeWidget)
|
||||
Core::Context qmlDesignerNavigatorContext(Constants::C_QMLNAVIGATOR);
|
||||
Core::Context qmlDesignerMaterialBrowserContext(Constants::C_QMLMATERIALBROWSER);
|
||||
Core::Context qmlDesignerAssetsLibraryContext(Constants::C_QMLASSETSLIBRARY);
|
||||
Core::Context qmlDesignerCollectionEditorContext(Constants::C_QMLCOLLECTIONEDITOR);
|
||||
|
||||
context->context().add(qmlDesignerMainContext);
|
||||
context->context().add(qmlDesignerFormEditorContext);
|
||||
@@ -400,7 +397,6 @@ void QmlDesignerPlugin::integrateIntoQtCreator(QWidget *modeWidget)
|
||||
context->context().add(qmlDesignerNavigatorContext);
|
||||
context->context().add(qmlDesignerMaterialBrowserContext);
|
||||
context->context().add(qmlDesignerAssetsLibraryContext);
|
||||
context->context().add(qmlDesignerCollectionEditorContext);
|
||||
context->context().add(ProjectExplorer::Constants::QMLJS_LANGUAGE_ID);
|
||||
|
||||
d->shortCutManager.registerActions(qmlDesignerMainContext, qmlDesignerFormEditorContext,
|
||||
|
@@ -231,12 +231,10 @@ void ShortCutManager::registerActions(const Core::Context &qmlDesignerMainContex
|
||||
connect(Core::ICore::instance(), &Core::ICore::contextChanged, this, [&](const Core::Context &context) {
|
||||
isMatBrowserActive = context.contains(Constants::C_QMLMATERIALBROWSER);
|
||||
isAssetsLibraryActive = context.contains(Constants::C_QMLASSETSLIBRARY);
|
||||
isCollectionEditorActive = context.contains(Constants::C_QMLCOLLECTIONEDITOR);
|
||||
|
||||
if (!context.contains(Constants::C_QMLFORMEDITOR) && !context.contains(Constants::C_QMLEDITOR3D)
|
||||
&& !context.contains(Constants::C_QMLNAVIGATOR)) {
|
||||
m_deleteAction.setEnabled(isMatBrowserActive || isAssetsLibraryActive
|
||||
|| isCollectionEditorActive);
|
||||
m_deleteAction.setEnabled(isMatBrowserActive || isAssetsLibraryActive);
|
||||
m_cutAction.setEnabled(false);
|
||||
m_copyAction.setEnabled(false);
|
||||
m_pasteAction.setEnabled(false);
|
||||
@@ -293,8 +291,6 @@ void ShortCutManager::deleteSelected()
|
||||
actionManager.view()->emitCustomNotification("delete_selected_material");
|
||||
else if (isAssetsLibraryActive)
|
||||
actionManager.view()->emitCustomNotification("delete_selected_assets");
|
||||
else if (isCollectionEditorActive)
|
||||
actionManager.view()->emitCustomNotification("delete_selected_collection");
|
||||
else if (currentDesignDocument())
|
||||
currentDesignDocument()->deleteSelected();
|
||||
}
|
||||
|
@@ -64,7 +64,6 @@ private:
|
||||
|
||||
bool isMatBrowserActive = false;
|
||||
bool isAssetsLibraryActive = false;
|
||||
bool isCollectionEditorActive = false;
|
||||
};
|
||||
|
||||
} // namespace QmlDesigner
|
||||
|
Reference in New Issue
Block a user