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:
Ali Kianian
2024-05-22 14:22:43 +03:00
parent d9f4987d71
commit b5db9f8fb3
52 changed files with 1 additions and 8115 deletions

View File

@@ -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
}
}
]
}

View File

@@ -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()
}
}
}
}
}

View File

@@ -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()
}
}
}
}
}

View File

@@ -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
}
}
]
}

View File

@@ -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()
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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))
}
}

View File

@@ -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()
}
}
}
}

View File

@@ -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()
}
}
}
}

View File

@@ -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
}
}
]
}

View File

@@ -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()
}
}
}
}

View File

@@ -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()
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -243,16 +243,6 @@ QtObject {
property real dialogButtonSpacing: 10 property real dialogButtonSpacing: 10
property real dialogButtonPadding: 4 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 // NEW NEW NEW
readonly property int flowMargin: 7 readonly property int flowMargin: 7
readonly property int flowSpacing: 7 // Odd so cursor has a center location readonly property int flowSpacing: 7 // Odd so cursor has a center location

View File

@@ -1,18 +0,0 @@
[
{
"colorCode": "#ff0000",
"name": "Red"
},
{
"colorCode": "#00ff00",
"name": "Green"
},
{
"colorCode": "#0000ff",
"name": "Blue"
},
{
"colorCode": "#ffffff",
"name": "White"
}
]

View File

@@ -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 {}
}

View File

@@ -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"
]
]
}
}

View File

@@ -500,7 +500,6 @@ add_qtc_plugin(QmlDesigner
INCLUDES INCLUDES
${CMAKE_CURRENT_LIST_DIR}/components ${CMAKE_CURRENT_LIST_DIR}/components
${CMAKE_CURRENT_LIST_DIR}/components/assetslibrary ${CMAKE_CURRENT_LIST_DIR}/components/assetslibrary
${CMAKE_CURRENT_LIST_DIR}/components/collectioneditor
${CMAKE_CURRENT_LIST_DIR}/components/debugview ${CMAKE_CURRENT_LIST_DIR}/components/debugview
${CMAKE_CURRENT_LIST_DIR}/components/edit3d ${CMAKE_CURRENT_LIST_DIR}/components/edit3d
${CMAKE_CURRENT_LIST_DIR}/components/formeditor ${CMAKE_CURRENT_LIST_DIR}/components/formeditor
@@ -847,22 +846,6 @@ extend_qtc_plugin(QmlDesigner
materialeditor.qrc 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 extend_qtc_plugin(QmlDesigner
SOURCES_PREFIX components/textureeditor SOURCES_PREFIX components/textureeditor
SOURCES SOURCES

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -69,7 +69,6 @@ const char mergeTemplateCommandId[] = "MergeTemplate";
const char goToImplementationCommandId[] = "GoToImplementation"; const char goToImplementationCommandId[] = "GoToImplementation";
const char makeComponentCommandId[] = "MakeComponent"; const char makeComponentCommandId[] = "MakeComponent";
const char editMaterialCommandId[] = "EditMaterial"; const char editMaterialCommandId[] = "EditMaterial";
const char editCollectionCommandId[] = "EditCollection";
const char addItemToStackedContainerCommandId[] = "AddItemToStackedContainer"; const char addItemToStackedContainerCommandId[] = "AddItemToStackedContainer";
const char addTabBarToStackedContainerCommandId[] = "AddTabBarToStackedContainer"; const char addTabBarToStackedContainerCommandId[] = "AddTabBarToStackedContainer";
const char increaseIndexOfStackedContainerCommandId[] = "IncreaseIndexOfStackedContainer"; 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 goToImplementationDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Go to Implementation");
const char makeComponentDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Create Component"); const char makeComponentDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Create Component");
const char editMaterialDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Edit Material"); 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 editAnnotationsDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Edit Annotations");
const char addMouseAreaFillDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Add Mouse Area"); const char addMouseAreaFillDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Add Mouse Area");
const char editIn3dViewDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Edit in 3D View"); const char editIn3dViewDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Edit in 3D View");
@@ -214,7 +212,6 @@ enum PrioritiesEnum : int {
ArrangeCategory, ArrangeCategory,
EditCategory, EditCategory,
EditListModel, EditListModel,
EditCollection,
/******** Section *****************************/ /******** Section *****************************/
PositionSection = 2000, PositionSection = 2000,
SnappingCategory, SnappingCategory,

View File

@@ -113,7 +113,6 @@ void DesignerActionManager::polishActions() const
Core::Context qmlDesignerNavigatorContext(Constants::C_QMLNAVIGATOR); Core::Context qmlDesignerNavigatorContext(Constants::C_QMLNAVIGATOR);
Core::Context qmlDesignerMaterialBrowserContext(Constants::C_QMLMATERIALBROWSER); Core::Context qmlDesignerMaterialBrowserContext(Constants::C_QMLMATERIALBROWSER);
Core::Context qmlDesignerAssetsLibraryContext(Constants::C_QMLASSETSLIBRARY); Core::Context qmlDesignerAssetsLibraryContext(Constants::C_QMLASSETSLIBRARY);
Core::Context qmlDesignerCollectionEditorContext(Constants::C_QMLCOLLECTIONEDITOR);
Core::Context qmlDesignerUIContext; Core::Context qmlDesignerUIContext;
qmlDesignerUIContext.add(qmlDesignerFormEditorContext); qmlDesignerUIContext.add(qmlDesignerFormEditorContext);
@@ -121,7 +120,6 @@ void DesignerActionManager::polishActions() const
qmlDesignerUIContext.add(qmlDesignerNavigatorContext); qmlDesignerUIContext.add(qmlDesignerNavigatorContext);
qmlDesignerUIContext.add(qmlDesignerMaterialBrowserContext); qmlDesignerUIContext.add(qmlDesignerMaterialBrowserContext);
qmlDesignerUIContext.add(qmlDesignerAssetsLibraryContext); qmlDesignerUIContext.add(qmlDesignerAssetsLibraryContext);
qmlDesignerUIContext.add(qmlDesignerCollectionEditorContext);
for (auto *action : actions) { for (auto *action : actions) {
if (!action->menuId().isEmpty()) { if (!action->menuId().isEmpty()) {
@@ -2011,16 +2009,6 @@ void DesignerActionManager::createDefaultDesignerActions()
addDesignerAction(new EditListModelAction); 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, addDesignerAction(new ModelNodeContextMenuAction(openSignalDialogCommandId,
openSignalDialogDisplayName, openSignalDialogDisplayName,
{}, {},

View File

@@ -82,18 +82,6 @@ inline bool hasEditableMaterial(const SelectionContext &selectionState)
return prop.exists() && (!prop.expression().isEmpty() || !prop.resolveToModelNodeList().empty()); 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) inline bool selectionEnabled(const SelectionContext &selectionState)
{ {
return selectionState.showSelectionTools(); return selectionState.showSelectionTools();

View File

@@ -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) void addItemToStackedContainer(const SelectionContext &selectionContext)
{ {
AbstractView *view = selectionContext.view(); AbstractView *view = selectionContext.view();

View File

@@ -92,7 +92,6 @@ void layoutGridLayout(const SelectionContext &selectionState);
void goImplementation(const SelectionContext &selectionState); void goImplementation(const SelectionContext &selectionState);
void addNewSignalHandler(const SelectionContext &selectionState); void addNewSignalHandler(const SelectionContext &selectionState);
void editMaterial(const SelectionContext &selectionContext); void editMaterial(const SelectionContext &selectionContext);
void editCollection(const SelectionContext &selectionContext);
void addSignalHandlerOrGotoImplementation(const SelectionContext &selectionState, bool addAlwaysNewSlot); void addSignalHandlerOrGotoImplementation(const SelectionContext &selectionState, bool addAlwaysNewSlot);
void removeLayout(const SelectionContext &selectionContext); void removeLayout(const SelectionContext &selectionContext);
void removePositioner(const SelectionContext &selectionContext); void removePositioner(const SelectionContext &selectionContext);

View File

@@ -7,7 +7,6 @@
#include <abstractview.h> #include <abstractview.h>
#include <assetslibraryview.h> #include <assetslibraryview.h>
#include <capturingconnectionmanager.h> #include <capturingconnectionmanager.h>
#include <collectionview.h>
#include <componentaction.h> #include <componentaction.h>
#include <componentview.h> #include <componentview.h>
#include <contentlibraryview.h> #include <contentlibraryview.h>
@@ -42,14 +41,6 @@
namespace QmlDesigner { 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) static Q_LOGGING_CATEGORY(viewBenchmark, "qtc.viewmanager.attach", QtWarningMsg)
class ViewManagerData class ViewManagerData
@@ -64,7 +55,6 @@ public:
: connectionManager, : connectionManager,
externalDependencies, externalDependencies,
true) true)
, collectionView{externalDependencies}
, contentLibraryView{imageCache, externalDependencies} , contentLibraryView{imageCache, externalDependencies}
, componentView{externalDependencies} , componentView{externalDependencies}
#ifndef QTC_USE_QML_DESIGNER_LITE #ifndef QTC_USE_QML_DESIGNER_LITE
@@ -90,7 +80,6 @@ public:
Internal::DebugView debugView; Internal::DebugView debugView;
DesignerActionManagerView designerActionManagerView; DesignerActionManagerView designerActionManagerView;
NodeInstanceView nodeInstanceView; NodeInstanceView nodeInstanceView;
CollectionView collectionView;
ContentLibraryView contentLibraryView; ContentLibraryView contentLibraryView;
ComponentView componentView; ComponentView componentView;
#ifndef QTC_USE_QML_DESIGNER_LITE #ifndef QTC_USE_QML_DESIGNER_LITE
@@ -235,9 +224,6 @@ QList<AbstractView *> ViewManager::standardViews() const
&d->designerActionManagerView}; &d->designerActionManagerView};
#endif #endif
if (enableModelEditor())
list.append(&d->collectionView);
if (QmlDesignerPlugin::instance() if (QmlDesignerPlugin::instance()
->settings() ->settings()
.value(DesignerSettingsKey::ENABLE_DEBUGVIEW) .value(DesignerSettingsKey::ENABLE_DEBUGVIEW)
@@ -418,8 +404,6 @@ QList<WidgetInfo> ViewManager::widgetInfos() const
widgetInfoList.append(d->textureEditorView.widgetInfo()); widgetInfoList.append(d->textureEditorView.widgetInfo());
#endif #endif
widgetInfoList.append(d->statesEditorView.widgetInfo()); widgetInfoList.append(d->statesEditorView.widgetInfo());
if (enableModelEditor())
widgetInfoList.append(d->collectionView.widgetInfo());
if (checkEnterpriseLicense()) if (checkEnterpriseLicense())
widgetInfoList.append(d->contentLibraryView.widgetInfo()); widgetInfoList.append(d->contentLibraryView.widgetInfo());

View File

@@ -3,7 +3,6 @@
#include "designmodecontext.h" #include "designmodecontext.h"
#include "assetslibrarywidget.h" #include "assetslibrarywidget.h"
#include "collectionwidget.h"
#include "designmodewidget.h" #include "designmodewidget.h"
#include "edit3dwidget.h" #include "edit3dwidget.h"
#include "formeditorwidget.h" #include "formeditorwidget.h"
@@ -98,15 +97,4 @@ void TextEditorContext::contextHelp(const HelpCallback &callback) const
qobject_cast<TextEditorWidget *>(m_widget)->contextHelp(callback); 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 } // namespace QmlDesigner::Internal

View File

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

View File

@@ -19,7 +19,6 @@ inline constexpr char C_QMLNAVIGATOR[] = "QmlDesigner::Navigator";
inline constexpr char C_QMLTEXTEDITOR[] = "QmlDesigner::TextEditor"; inline constexpr char C_QMLTEXTEDITOR[] = "QmlDesigner::TextEditor";
inline constexpr char C_QMLMATERIALBROWSER[] = "QmlDesigner::MaterialBrowser"; inline constexpr char C_QMLMATERIALBROWSER[] = "QmlDesigner::MaterialBrowser";
inline constexpr char C_QMLASSETSLIBRARY[] = "QmlDesigner::AssetsLibrary"; 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 // Special context for preview menu, shared b/w designer and text editor
inline constexpr char C_QT_QUICK_TOOLS_MENU[] = "QmlDesigner::ToolsMenu"; 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_BROWSER[] = "QQuickWidgetMaterialBrowser";
inline constexpr char OBJECT_NAME_MATERIAL_EDITOR[] = "QQuickWidgetMaterialEditor"; inline constexpr char OBJECT_NAME_MATERIAL_EDITOR[] = "QQuickWidgetMaterialEditor";
inline constexpr char OBJECT_NAME_PROPERTY_EDITOR[] = "QQuickWidgetPropertyEditor"; 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_STATES_EDITOR[] = "QQuickWidgetStatesEditor";
inline constexpr char OBJECT_NAME_TEXTURE_EDITOR[] = "QQuickWidgetTextureEditor"; inline constexpr char OBJECT_NAME_TEXTURE_EDITOR[] = "QQuickWidgetTextureEditor";
inline constexpr char OBJECT_NAME_TOP_TOOLBAR[] = "QQuickWidgetTopToolbar"; inline constexpr char OBJECT_NAME_TOP_TOOLBAR[] = "QQuickWidgetTopToolbar";

View File

@@ -4,7 +4,6 @@
#include "qmldesignerplugin.h" #include "qmldesignerplugin.h"
#include "qmldesignertr.h" #include "qmldesignertr.h"
#include "collectioneditor/collectionview.h"
#include "coreplugin/iwizardfactory.h" #include "coreplugin/iwizardfactory.h"
#include "designmodecontext.h" #include "designmodecontext.h"
#include "designmodewidget.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 //TODO Move registering those types out of the property editor, since they are used also in the states editor
Quick2PropertyEditorView::registerQmlTypes(); Quick2PropertyEditorView::registerQmlTypes();
CollectionView::registerDeclarativeType();
StudioQuickWidget::registerDeclarativeType(); StudioQuickWidget::registerDeclarativeType();
QmlDesignerBase::WindowManager::registerDeclarativeType(); QmlDesignerBase::WindowManager::registerDeclarativeType();
@@ -392,7 +390,6 @@ void QmlDesignerPlugin::integrateIntoQtCreator(QWidget *modeWidget)
Core::Context qmlDesignerNavigatorContext(Constants::C_QMLNAVIGATOR); Core::Context qmlDesignerNavigatorContext(Constants::C_QMLNAVIGATOR);
Core::Context qmlDesignerMaterialBrowserContext(Constants::C_QMLMATERIALBROWSER); Core::Context qmlDesignerMaterialBrowserContext(Constants::C_QMLMATERIALBROWSER);
Core::Context qmlDesignerAssetsLibraryContext(Constants::C_QMLASSETSLIBRARY); Core::Context qmlDesignerAssetsLibraryContext(Constants::C_QMLASSETSLIBRARY);
Core::Context qmlDesignerCollectionEditorContext(Constants::C_QMLCOLLECTIONEDITOR);
context->context().add(qmlDesignerMainContext); context->context().add(qmlDesignerMainContext);
context->context().add(qmlDesignerFormEditorContext); context->context().add(qmlDesignerFormEditorContext);
@@ -400,7 +397,6 @@ void QmlDesignerPlugin::integrateIntoQtCreator(QWidget *modeWidget)
context->context().add(qmlDesignerNavigatorContext); context->context().add(qmlDesignerNavigatorContext);
context->context().add(qmlDesignerMaterialBrowserContext); context->context().add(qmlDesignerMaterialBrowserContext);
context->context().add(qmlDesignerAssetsLibraryContext); context->context().add(qmlDesignerAssetsLibraryContext);
context->context().add(qmlDesignerCollectionEditorContext);
context->context().add(ProjectExplorer::Constants::QMLJS_LANGUAGE_ID); context->context().add(ProjectExplorer::Constants::QMLJS_LANGUAGE_ID);
d->shortCutManager.registerActions(qmlDesignerMainContext, qmlDesignerFormEditorContext, d->shortCutManager.registerActions(qmlDesignerMainContext, qmlDesignerFormEditorContext,

View File

@@ -231,12 +231,10 @@ void ShortCutManager::registerActions(const Core::Context &qmlDesignerMainContex
connect(Core::ICore::instance(), &Core::ICore::contextChanged, this, [&](const Core::Context &context) { connect(Core::ICore::instance(), &Core::ICore::contextChanged, this, [&](const Core::Context &context) {
isMatBrowserActive = context.contains(Constants::C_QMLMATERIALBROWSER); isMatBrowserActive = context.contains(Constants::C_QMLMATERIALBROWSER);
isAssetsLibraryActive = context.contains(Constants::C_QMLASSETSLIBRARY); isAssetsLibraryActive = context.contains(Constants::C_QMLASSETSLIBRARY);
isCollectionEditorActive = context.contains(Constants::C_QMLCOLLECTIONEDITOR);
if (!context.contains(Constants::C_QMLFORMEDITOR) && !context.contains(Constants::C_QMLEDITOR3D) if (!context.contains(Constants::C_QMLFORMEDITOR) && !context.contains(Constants::C_QMLEDITOR3D)
&& !context.contains(Constants::C_QMLNAVIGATOR)) { && !context.contains(Constants::C_QMLNAVIGATOR)) {
m_deleteAction.setEnabled(isMatBrowserActive || isAssetsLibraryActive m_deleteAction.setEnabled(isMatBrowserActive || isAssetsLibraryActive);
|| isCollectionEditorActive);
m_cutAction.setEnabled(false); m_cutAction.setEnabled(false);
m_copyAction.setEnabled(false); m_copyAction.setEnabled(false);
m_pasteAction.setEnabled(false); m_pasteAction.setEnabled(false);
@@ -293,8 +291,6 @@ void ShortCutManager::deleteSelected()
actionManager.view()->emitCustomNotification("delete_selected_material"); actionManager.view()->emitCustomNotification("delete_selected_material");
else if (isAssetsLibraryActive) else if (isAssetsLibraryActive)
actionManager.view()->emitCustomNotification("delete_selected_assets"); actionManager.view()->emitCustomNotification("delete_selected_assets");
else if (isCollectionEditorActive)
actionManager.view()->emitCustomNotification("delete_selected_collection");
else if (currentDesignDocument()) else if (currentDesignDocument())
currentDesignDocument()->deleteSelected(); currentDesignDocument()->deleteSelected();
} }

View File

@@ -64,7 +64,6 @@ private:
bool isMatBrowserActive = false; bool isMatBrowserActive = false;
bool isAssetsLibraryActive = false; bool isAssetsLibraryActive = false;
bool isCollectionEditorActive = false;
}; };
} // namespace QmlDesigner } // namespace QmlDesigner