From 5e8b5ec1f01420d520c0ff7650b6ea431967272c Mon Sep 17 00:00:00 2001 From: Henning Gruendl Date: Mon, 4 Sep 2023 16:45:54 +0200 Subject: [PATCH] QmlDesigner: Integrate Expression Builder Task-number: QDS-10587 Change-Id: Ifc13a8364fccb74cb60d683f0e6c322d80baab50 Reviewed-by: Thomas Hartmann --- .../ConnectionsDialogForm.qml | 61 ++- .../connectionseditor/ExpressionBuilder.qml | 387 ++++++++++++++++++ .../connectionseditor/MyListViewDelegate.qml | 29 ++ .../connectionseditor/MyTreeViewDelegate.qml | 66 +++ .../qmldesigner/connectionseditor/Pill.qml | 167 ++++++++ .../connectionseditor/SuggestionPopup.qml | 257 ++++++++++++ .../connectionseditor/TabCheckButton.qml | 1 + .../imports/HelperWidgets/ToolTipArea.qml | 6 +- .../imports/StudioControls/SearchBox.qml | 4 +- .../imports/StudioTheme/Values.qml | 10 + .../connectioneditor/connectionmodel.cpp | 121 +++++- .../connectioneditor/connectionmodel.h | 21 +- .../connectioneditor/connectionview.cpp | 11 +- .../connectioneditor/propertytreemodel.cpp | 92 ++++- .../connectioneditor/propertytreemodel.h | 25 +- 15 files changed, 1205 insertions(+), 53 deletions(-) create mode 100644 share/qtcreator/qmldesigner/connectionseditor/ExpressionBuilder.qml create mode 100644 share/qtcreator/qmldesigner/connectionseditor/MyListViewDelegate.qml create mode 100644 share/qtcreator/qmldesigner/connectionseditor/MyTreeViewDelegate.qml create mode 100644 share/qtcreator/qmldesigner/connectionseditor/Pill.qml create mode 100644 share/qtcreator/qmldesigner/connectionseditor/SuggestionPopup.qml diff --git a/share/qtcreator/qmldesigner/connectionseditor/ConnectionsDialogForm.qml b/share/qtcreator/qmldesigner/connectionseditor/ConnectionsDialogForm.qml index e8909988de6..af3a44ffae1 100644 --- a/share/qtcreator/qmldesigner/connectionseditor/ConnectionsDialogForm.qml +++ b/share/qtcreator/qmldesigner/connectionseditor/ConnectionsDialogForm.qml @@ -107,19 +107,64 @@ Column { onClicked: backend.removeCondition() } + ExpressionBuilder { + style: StudioTheme.Values.connectionPopupControlStyle + width: root.width + + visible: backend.hasCondition + model: backend.conditionListModel + + onRemove: function(index) { + console.log("remove", index) + backend.conditionListModel.removeToken(index) + } + + onUpdate: function(index, value) { + console.log("update", index, value) + backend.conditionListModel.updateToken(index, value) + } + + onAdd: function(value) { + console.log("add", value) + backend.conditionListModel.appendToken(value) + } + + onInsert: function(index, value, type) { + console.log("insert", index, value, type) + + if (type === ConditionListModel.Intermediate) + backend.conditionListModel.insertIntermediateToken(index, value) + else if (type === ConditionListModel.Shadow) + backend.conditionListModel.insertShadowToken(index, value) + else + backend.conditionListModel.insertToken(index, value) + } + + onSetValue: function(index, value) { + console.log("setValue", index, value) + + backend.conditionListModel.setShadowToken(index, value) + } + } + Flow { spacing: root.horizontalSpacing width: root.width - Repeater { + Repeater { model: backend.conditionListModel + Text { text: value color: "white" + Rectangle { z: -1 opacity: 0.2 + anchors.fill: parent color: { + if (type === ConditionListModel.Intermediate) + return "darkorange" if (type === ConditionListModel.Invalid) return "red" if (type === ConditionListModel.Operator) @@ -128,8 +173,9 @@ Column { return "green" if (type === ConditionListModel.Variable) return "yellow" + if (type === ConditionListModel.Shadow) + return "hotpink" } - anchors.fill: parent } } } @@ -138,7 +184,7 @@ Column { TextInput { id: commandInput width: root.width - onAccepted: backend.conditionListModel.command(commandInput.text) + onAccepted: backend.conditionListModel.command(commandInput.text) } Text { @@ -153,7 +199,8 @@ Column { iconSize: StudioTheme.Values.baseFontSize iconFont: StudioTheme.Constants.font anchors.horizontalCenter: parent.horizontalCenter - visible: action.currentValue !== ConnectionModelStatementDelegate.Custom && backend.hasCondition && !backend.hasElse + visible: action.currentValue !== ConnectionModelStatementDelegate.Custom + && backend.hasCondition && !backend.hasElse onClicked: backend.addElse() } @@ -165,7 +212,8 @@ Column { iconSize: StudioTheme.Values.baseFontSize iconFont: StudioTheme.Constants.font anchors.horizontalCenter: parent.horizontalCenter - visible: action.currentValue !== ConnectionModelStatementDelegate.Custom && backend.hasCondition && backend.hasElse + visible: action.currentValue !== ConnectionModelStatementDelegate.Custom + && backend.hasCondition && backend.hasElse onClicked: backend.removeElse() } @@ -177,7 +225,8 @@ Column { columnWidth: root.columnWidth statement: backend.koStatement spacing: root.verticalSpacing - visible: action.currentValue !== ConnectionModelStatementDelegate.Custom && backend.hasCondition && backend.hasElse + visible: action.currentValue !== ConnectionModelStatementDelegate.Custom + && backend.hasCondition && backend.hasElse } // Editor diff --git a/share/qtcreator/qmldesigner/connectionseditor/ExpressionBuilder.qml b/share/qtcreator/qmldesigner/connectionseditor/ExpressionBuilder.qml new file mode 100644 index 00000000000..70d3f096173 --- /dev/null +++ b/share/qtcreator/qmldesigner/connectionseditor/ExpressionBuilder.qml @@ -0,0 +1,387 @@ +// 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 StudioControls as StudioControls +import StudioTheme as StudioTheme + +Rectangle { + id: root + + property StudioTheme.ControlStyle style: StudioTheme.Values.controlStyle + + property var conditionListModel: ConnectionsEditorEditorBackend.connectionModel.delegate.conditionListModel + + property alias model: repeater.model + property int shadowPillIndex: -1 + property bool shadowPillVisible: root.shadowPillIndex !== -1 + + property int heightBeforeShadowPill: Math.min(20, flow.childrenRect.height) // TODO Proper size value + property int expressionHeight: { + if (popup.visible) + return root.heightBeforeShadowPill + flow.spacing + 20 + + return root.heightBeforeShadowPill + } + + signal remove(int index) + signal update(int index, var value) + signal add(var value) + signal insert(int index, var value, int type) + + signal setValue(int index, var value) + signal setValueType(int index, var value, int type) + + width: 400 + height: root.expressionHeight + 2 * StudioTheme.Values.flowMargin + color: root.style.background.idle + border { + color: root.conditionListModel.valid ? root.style.border.idle + : StudioTheme.Values.themeError + width: root.style.borderWidth + } + + onVisibleChanged: { + if (!root.visible) + popup.close() + } + + // Is text input for creating new items currently used. + function textInputActive() { // TODO Make property + return newTextInput.activeFocus && newTextInput.visible + } + + function getMappedItemRect(index: int) : rect { + let item = repeater.itemAt(index) + let itemRect = Qt.rect(item.x, item.y, item.width, item.height) + return flow.mapToItem(root, itemRect) + } + + function placeCursor(index: int) : void { + var textInputPosition = Qt.point(0, 0) + + if (!repeater.count) { // Empty repeater + let mappedItemRect = flow.mapToItem(root, 0, 0, 0, 0) + + textInputPosition = Qt.point(mappedItemRect.x, mappedItemRect.y) + index = 0 + } else { // Repeater is not empty + // Clamp index to 0 and num items in repeater + index = Math.min(Math.max(index, 0), repeater.count) + + if (index === 0) { + // Needs to be placed in front of first repeater item + let mappedItemRect = root.getMappedItemRect(index) + textInputPosition = Qt.point(mappedItemRect.x - 4, // - 4 due to spacing of flow + mappedItemRect.y) + } else { + let mappedItemRect = root.getMappedItemRect(index - 1) + textInputPosition = Qt.point(mappedItemRect.x + mappedItemRect.width + 3, + mappedItemRect.y) + } + } + + // Position text input, make it visible and set focus + newTextInput.x = textInputPosition.x + newTextInput.y = textInputPosition.y + newTextInput.index = index + newTextInput.visible = true + newTextInput.forceActiveFocus() + // Open suggestion popup + popup.open() + } + + StudioControls.ToolTip { + id: toolTip + visible: mouseArea.containsMouse && toolTip.text !== "" + delay: 1000 + text: root.conditionListModel.error + } + + MouseArea { + id: mouseArea + anchors.fill: parent + cursorShape: Qt.IBeamCursor + hoverEnabled: true + + onPressed: function (event) { + // Check if empty + if (!repeater.count) { + root.placeCursor(0) + return + } + + // Map to flow item + let point = mouseArea.mapToItem(flow, Qt.point(event.x, event.y)) + + let horizontalDistance = Number.MAX_VALUE + let verticalDistance = Number.MAX_VALUE + let cursorPosition = 0 + + for (var i = 0; i < repeater.count; ++i) { + let item = repeater.itemAt(i) + + let y = item.y + (item.height / 2) + + // Vertical distance + let vDistance = Math.abs(point.y - y) + + // Horizontal distance + let hLeftDistance = Math.abs(point.x - item.x) + let hRightDistance = Math.abs(point.x - (item.x + item.width)) + + // Early return if vertical distance increases + if (vDistance > verticalDistance) + break + + if (vDistance <= verticalDistance) { + // Rest horizontal distance if vertical distance is smaller than before + if (vDistance !== verticalDistance) + horizontalDistance = Number.MAX_VALUE + + if (hLeftDistance < horizontalDistance) { + horizontalDistance = hLeftDistance + cursorPosition = i + } + + if (hRightDistance < horizontalDistance) { + horizontalDistance = hRightDistance + cursorPosition = i + 1 + } + + verticalDistance = vDistance + } + } + + root.placeCursor(cursorPosition) + } + } + + Flow { + id: flow + + property int focusIndex: -1 + + anchors.fill: parent + anchors.margins: StudioTheme.Values.flowMargin + spacing: StudioTheme.Values.flowSpacing + + onPositioningComplete: { + if (root.textInputActive()) + root.placeCursor(newTextInput.index) + + if (!root.shadowPillVisible) + root.heightBeforeShadowPill = flow.childrenRect.height + } + + Repeater { + id: repeater + + onItemRemoved: function(index, item) { + if (!root.textInputActive()) + return + + // Udpate the cursor position + if (index < newTextInput.index) + newTextInput.index = newTextInput.index - 1 + } + + onItemAdded: function(index, item) { + if (!root.textInputActive()) + return + + if (index >= newTextInput.index) + newTextInput.index = newTextInput.index + 1 + + if (!root.conditionListModel.valid && index === root.conditionListModel.errorIndex) + item.invalid = true + } + + Pill { + id: pill + + onRemove: function() { + // If pill has focus due to selection or keyboard navigation + if (pill.focus) + root.placeCursor(pill.index) + + Qt.callLater(root.remove, pill.index) + } + + onUpdate: function(value) { + if (value === "") + Qt.callLater(root.remove, pill.index) // Otherwise crash + else + Qt.callLater(root.update, pill.index, value) + } + + onFocusChanged: function() { + if (pill.focus) + flow.focusIndex = pill.index + } + + onSubmit: { + console.log("SUBMIT") + + //newTextInput.index = pill.index + 1 + newTextInput.visible = true + newTextInput.forceActiveFocus() + } + } + } + } + + TextInput { + id: newTextInput + + property int index + + height: 20 + topPadding: 1 + font.pixelSize: root.style.baseFontSize + color: root.style.text.idle + visible: false + validator: RegularExpressionValidator { regularExpression: /^\S.+/ } + + //onActiveFocusChanged: { + // if (!newTextInput.activeFocus && !root.shadowPillVisible) { + // console.log("CLOSE POPUP") + // popup.close() + // } + //} + + onTextEdited: { + if (newTextInput.text === "") + return + + newTextInput.visible = false + + root.insert(newTextInput.index, newTextInput.text, ConditionListModel.Intermediate) + + newTextInput.clear() + + // Set focus on the newly created item + let newItem = repeater.itemAt(newTextInput.index) + newItem.forceActiveFocus() + } + + Keys.onPressed: function (event) { + if (event.key === Qt.Key_Backspace) { + if (root.textInputActive()) { + let previousIndex = newTextInput.index - 1 + if (previousIndex < 0) + return + + let item = repeater.itemAt(previousIndex) + item.setCursorEnd() + item.forceActiveFocus() + popup.close() + } + } + } + } + + SuggestionPopup { + id: popup + + style: StudioTheme.Values.connectionPopupControlStyle + + x: 0 + y: root.height + width: root.width + + //onOpened: console.log("POPUP opened") + //onClosed: console.log("POPUP closed") + + onSelect: function(value) { + newTextInput.visible = true + newTextInput.forceActiveFocus() + + if (root.shadowPillVisible) { // Active shadow pill + root.remove(root.shadowPillIndex) + root.shadowPillIndex = -1 + } + + root.insert(newTextInput.index, value, ConditionListModel.Variable) + + // Clear search, reset stack view and tree model + popup.reset() + } + + onSearchActiveChanged: { + if (popup.searchActive) { + root.heightBeforeShadowPill = flow.childrenRect.height + root.insert(newTextInput.index, "...", ConditionListModel.Shadow) + root.shadowPillIndex = newTextInput.index + } else { + if (!root.shadowPillVisible) + return + + root.remove(root.shadowPillIndex) + root.shadowPillIndex = -1 + } + } + + onEntered: function(value) { + if (!popup.searchActive) { + if (!root.shadowPillVisible) { + root.heightBeforeShadowPill = flow.childrenRect.height + root.shadowPillIndex = newTextInput.index + root.insert(newTextInput.index, value, ConditionListModel.Shadow) + } else { + root.setValue(root.shadowPillIndex, value) + } + } else { + root.setValue(root.shadowPillIndex, value) + } + } + + onExited: function(value) { + let shadowItem = repeater.itemAt(root.shadowPillIndex) + + if (!popup.searchActive) { + if (root.shadowPillVisible && shadowItem?.value === value) { + root.remove(root.shadowPillIndex) + root.shadowPillIndex = -1 + } + } else { + // Reset to 3 dots if still the same value as the exited item + if (shadowItem?.value === value) + root.setValue(root.shadowPillIndex, "...") + } + } + } + + Keys.onPressed: function (event) { + if (event.key === Qt.Key_Left) { + if (root.textInputActive()) { + let previousIndex = newTextInput.index - 1 + if (previousIndex < 0) + return + + let item = repeater.itemAt(previousIndex) + item.setCursorEnd() + item.forceActiveFocus() + popup.close() + } else { + if (flow.focusIndex < 0) + return + + root.placeCursor(flow.focusIndex) + } + } else if (event.key === Qt.Key_Right) { + if (root.textInputActive()) { + let nextIndex = newTextInput.index + if (nextIndex >= repeater.count) + return + + let item = repeater.itemAt(nextIndex) + item.setCursorBegin() + item.forceActiveFocus() + popup.close() + } else { + root.placeCursor(flow.focusIndex + 1) + } + } + } +} diff --git a/share/qtcreator/qmldesigner/connectionseditor/MyListViewDelegate.qml b/share/qtcreator/qmldesigner/connectionseditor/MyListViewDelegate.qml new file mode 100644 index 00000000000..936d06daee6 --- /dev/null +++ b/share/qtcreator/qmldesigner/connectionseditor/MyListViewDelegate.qml @@ -0,0 +1,29 @@ +// 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 + +ItemDelegate { + id: control + hoverEnabled: true + + contentItem: Text { + leftPadding: 8 + rightPadding: 8 + text: control.text + font: control.font + opacity: enabled ? 1.0 : 0.3 + color: control.hovered ? "#111111" : "white" + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + background: Rectangle { + implicitWidth: 200 + implicitHeight: 30 + opacity: enabled ? 1 : 0.3 + color: control.hovered ? "#4DBFFF" : "transparent" + } +} diff --git a/share/qtcreator/qmldesigner/connectionseditor/MyTreeViewDelegate.qml b/share/qtcreator/qmldesigner/connectionseditor/MyTreeViewDelegate.qml new file mode 100644 index 00000000000..bccb6c645d6 --- /dev/null +++ b/share/qtcreator/qmldesigner/connectionseditor/MyTreeViewDelegate.qml @@ -0,0 +1,66 @@ +// 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.Templates as T +import StudioTheme as StudioTheme + +T.TreeViewDelegate { + id: control + hoverEnabled: true + + implicitWidth: 200 + implicitHeight: 30 + + //implicitWidth: leftMargin + __contentIndent + implicitContentWidth + rightPadding + rightMargin + //implicitHeight: Math.max(indicator ? indicator.height : 0, implicitContentHeight) * 1.25 + + indentation: 12 + //leftMargin: 4 + //rightMargin: 4 + //spacing: 4 + + //topPadding: contentItem ? (height - contentItem.implicitHeight) / 2 : 0 + leftPadding: control.leftMargin + control.__contentIndent + + //required property int row + //required property var model + readonly property real __contentIndent: !control.isTreeNode ? 0 + : (control.depth * control.indentation) + + (control.indicator ? control.indicator.width + control.spacing : 0) + + indicator: Item { + readonly property real __indicatorIndent: control.leftMargin + (control.depth * control.indentation) + + x: __indicatorIndent + width: 30 + height: 30 + + Text { + id: caret + font.family: StudioTheme.Constants.iconFont.family + font.pixelSize: StudioTheme.Values.smallIconFontSize + color: control.hovered ? "#111111" : "white" // TODO colors + text: StudioTheme.Constants.sectionToggle + rotation: control.expanded ? 0 : -90 + anchors.centerIn: parent + } + } + + background: Rectangle { + implicitWidth: 200 + implicitHeight: 30 + color: control.hovered ? "#4DBFFF" : "transparent" + } + + contentItem: Text { + text: control.text + font: control.font + opacity: enabled ? 1.0 : 0.3 + color: control.hovered ? "#111111" : "white" + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } +} diff --git a/share/qtcreator/qmldesigner/connectionseditor/Pill.qml b/share/qtcreator/qmldesigner/connectionseditor/Pill.qml new file mode 100644 index 00000000000..942a61b6db1 --- /dev/null +++ b/share/qtcreator/qmldesigner/connectionseditor/Pill.qml @@ -0,0 +1,167 @@ +// 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 StudioControls as StudioControls +import StudioTheme as StudioTheme + +FocusScope { + id: root + + required property int index + required property string value + required property int type + + function setCursorBegin() { textInput.cursorPosition = 0 } + function setCursorEnd() { textInput.cursorPosition = textInput.text.length } + + function isEditable() { return root.type === ConditionListModel.Intermediate + || root.type === ConditionListModel.Literal + || root.type === ConditionListModel.Invalid } + + function isIntermediate() { return root.type === ConditionListModel.Intermediate } + function isLiteral() { return root.type === ConditionListModel.Literal } + function isOperator() { return root.type === ConditionListModel.Operator } + function isProperty() { return root.type === ConditionListModel.Variable } + function isShadow() { return root.type === ConditionListModel.Shadow } + function isInvalid() { return root.type === ConditionListModel.Invalid || root.invalid } + + signal remove() + signal update(var value) + signal submit() + + readonly property int margin: StudioTheme.Values.flowPillMargin + + property bool invalid: false + + width: { + if (root.isEditable()) { + if (root.isInvalid()) + return textInput.width + 1 + 2 * root.margin + else + return textInput.width + 1 + } + return textItem.contentWidth + icon.width + root.margin + } + height: StudioTheme.Values.flowPillHeight + + onActiveFocusChanged: { + if (root.activeFocus && root.isEditable()) + textInput.forceActiveFocus() + } + + Keys.onPressed: function (event) { + if (root.isEditable()) + return + + if (event.key === Qt.Key_Backspace || event.key === Qt.Key_Delete) + root.remove() + } + + MouseArea { + id: rootMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: root.isEditable() ? Qt.IBeamCursor : Qt.ArrowCursor + onClicked: root.forceActiveFocus() + } + + Rectangle { + id: pill + anchors.fill: parent + color: { + if (root.isShadow()) + return StudioTheme.Values.themeInteraction + if (root.isEditable()) + return "transparent" + + return StudioTheme.Values.themePillBackground + } + border.color: root.isInvalid() ? StudioTheme.Values.themeWarning : "white" // TODO colors + border.width: { + if (root.isShadow()) + return 0 + if (root.isInvalid()) + return 1 + if (root.isEditable()) + return 0 + if (rootMouseArea.containsMouse || root.focus) + return 1 + + return 0 + } + radius: 4 + + Row { + id: row + anchors.left: parent.left + anchors.leftMargin: root.margin + anchors.verticalCenter: parent.verticalCenter + visible: root.isOperator() || root.isProperty() || root.isShadow() + + Text { + id: textItem + font.pixelSize: StudioTheme.Values.baseFontSize + color: root.isShadow() ? StudioTheme.Values.themeTextSelectedTextColor + : StudioTheme.Values.themeTextColor + text: root.value + anchors.verticalCenter: parent.verticalCenter + } + + Item { + id: icon + width: root.isShadow() ? root.margin : StudioTheme.Values.flowPillHeight + height: StudioTheme.Values.flowPillHeight + visible: !root.isShadow() + + Text { + font.family: StudioTheme.Constants.iconFont.family + font.pixelSize: StudioTheme.Values.smallIconFontSize + color: StudioTheme.Values.themeIconColor + text: StudioTheme.Constants.close_small + anchors.centerIn: parent + } + + MouseArea { + id: mouseArea + anchors.fill: parent + onClicked: root.remove() + } + } + } + + TextInput { + id: textInput + + property bool dirty: false + + x: root.isInvalid() ? root.margin : 0 + height: StudioTheme.Values.flowPillHeight + topPadding: 1 + font.pixelSize: StudioTheme.Values.baseFontSize + color: (rootMouseArea.containsMouse || textInput.activeFocus) ? StudioTheme.Values.themeIconColor + : StudioTheme.Values.themeTextColor + text: root.value + visible: root.isEditable() + enabled: root.isEditable() + + validator: RegularExpressionValidator { regularExpression: /^\S+/ } + + onEditingFinished: { + root.update(textInput.text) // emit + root.submit() // emit + } + + onTextEdited: textInput.dirty = true + + Keys.onPressed: function (event) { + if (event.key === Qt.Key_Backspace) { + if (textInput.text !== "") + return + + root.remove() // emit + } + } + } + } +} diff --git a/share/qtcreator/qmldesigner/connectionseditor/SuggestionPopup.qml b/share/qtcreator/qmldesigner/connectionseditor/SuggestionPopup.qml new file mode 100644 index 00000000000..a4a1789eb55 --- /dev/null +++ b/share/qtcreator/qmldesigner/connectionseditor/SuggestionPopup.qml @@ -0,0 +1,257 @@ +// 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 as Controls +import StudioTheme as StudioTheme +import StudioControls as StudioControls +import ConnectionsEditorEditorBackend + +Controls.Popup { + id: root + + property StudioTheme.ControlStyle style: StudioTheme.Values.controlStyle + + property var listModel: ConnectionsEditorEditorBackend.connectionModel.delegate.propertyListProxyModel + property var treeModel: ConnectionsEditorEditorBackend.connectionModel.delegate.propertyTreeModel + + signal select(var value) + signal entered(var value) + signal exited(var value) + + property alias searchActive: search.activeFocus + + function reset() { + search.clear() + stack.pop(null, Controls.StackView.Immediate) + root.listModel.reset() + } + + closePolicy: Controls.Popup.NoAutoClose + padding: 0 + + background: Rectangle { + implicitWidth: root.width + color: root.style.background.idle + border { + color: root.style.border.idle + width: root.style.borderWidth + } + } + + contentItem: Column { + StudioControls.SearchBox { + id: search + width: parent.width + + onSearchChanged: function(value) { + root.treeModel.setFilter(value) + } + } + + Controls.StackView { + id: stack + + width: parent.width + height: currentItem?.implicitHeight + + clip: true + + initialItem: mainView + } + + Component { + id: mainView + + Column { + Rectangle { + width: stack.width + height: 30 + visible: root.listModel.parentName !== "" + color: backMouseArea.containsMouse ? "#4DBFFF" : "transparent" + + MouseArea { + id: backMouseArea + anchors.fill: parent + hoverEnabled: true + + onClicked: { + stack.pop(Controls.StackView.Immediate) + root.listModel.goUp() //treeModel.pop() + } + } + + Row { + anchors.fill: parent + + Item { + width: 30 + height: 30 + + Text { + id: chevronLeft + font.family: StudioTheme.Constants.iconFont.family + font.pixelSize: root.style.baseIconFontSize + color: backMouseArea.containsMouse ? "#111111" : "white" // TODO colors + text: StudioTheme.Constants.back_medium + anchors.centerIn: parent + } + } + + Text { + anchors.verticalCenter: parent.verticalCenter + text: root.listModel.parentName + color: backMouseArea.containsMouse ? "#111111" : "white" // TODO colors + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + } + } + + Rectangle { + width: stack.width - 8 + height: 1 + visible: root.listModel.parentName !== "" + color: "#3C3C3C" + anchors.horizontalCenter: parent.horizontalCenter + } + + ListView { + id: listView + visible: search.empty + width: stack.width + implicitHeight: Math.min(380, childrenRect.height) + clip: true + model: root.listModel + + delegate: MyListViewDelegate { + id: listViewDelegate + + required property int index + + required property string propertyName + required property int childCount + required property string expression + + text: listViewDelegate.propertyName + implicitWidth: listView.width + + onClicked: { + if (!listViewDelegate.childCount) { + root.select(listViewDelegate.expression) + return + } + + stack.push(mainView, Controls.StackView.Immediate) + + ListView.view.model.goInto(listViewDelegate.index) + } + + onHoveredChanged: { + if (listViewDelegate.childCount) + return + + if (listViewDelegate.hovered) + root.entered(listViewDelegate.expression) + else + root.exited(listViewDelegate.expression) + } + } + } + + TreeView { + id: treeView + visible: !search.empty + width: stack.width + implicitHeight: Math.min(380, childrenRect.height) + clip: true + model: root.treeModel + + // This is currently a workaround and should be cleaned up. Calling + // expandRecursively every time the filter changes is performance wise not good. + //Connections { + // target: proxyModel + // function onFilterChanged() { treeView.expandRecursively() } + //} + + delegate: MyTreeViewDelegate { + id: treeViewDelegate + + required property int index + + required property string propertyName + required property int childCount + required property string expression + + text: treeViewDelegate.propertyName + implicitWidth: treeView.width + + onClicked: { + if (!treeViewDelegate.childCount) + root.select(treeViewDelegate.expression) + else + treeView.toggleExpanded(treeViewDelegate.index) + } + + onHoveredChanged: { + if (treeViewDelegate.childCount) + return + + if (treeViewDelegate.hovered) + root.entered(treeViewDelegate.expression) + else + root.exited(treeViewDelegate.expression) + } + } + } + } + } + + Item { + visible: false + width: stack.width + height: flow.childrenRect.height + 2 * StudioTheme.Values.flowMargin + + Flow { + id: flow + + anchors.fill: parent + anchors.margins: StudioTheme.Values.flowMargin + spacing: StudioTheme.Values.flowSpacing + + Repeater { + id: repeater + + // TODO actual value + tooltip + model: ["AND", "OR", "equal", "not equal", "greater", "less", "greater then", "less then"] + + Rectangle { + width: textItem.contentWidth + 14 + height: 26 + color: "#161616" + radius: 4 + border { + color: "white" + width: mouseArea.containsMouse ? 1 : 0 + } + + MouseArea { + id: mouseArea + hoverEnabled: true + anchors.fill: parent + } + + Text { + id: textItem + font.pixelSize: 12 + color: "white" + text: modelData + anchors.centerIn: parent + } + } + } + } + } + } +} diff --git a/share/qtcreator/qmldesigner/connectionseditor/TabCheckButton.qml b/share/qtcreator/qmldesigner/connectionseditor/TabCheckButton.qml index 8165a69a1b4..a16e18962cb 100644 --- a/share/qtcreator/qmldesigner/connectionseditor/TabCheckButton.qml +++ b/share/qtcreator/qmldesigner/connectionseditor/TabCheckButton.qml @@ -131,6 +131,7 @@ T.TabButton { State { name: "check" when: control.enabled && !control.pressed && control.checked + extend: "hoverCheck" PropertyChanges { target: controlBackground color: control.style.interaction diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/ToolTipArea.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/ToolTipArea.qml index b604710e67e..d54d64007a6 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/ToolTipArea.qml +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/ToolTipArea.qml @@ -1,9 +1,9 @@ // Copyright (C) 2021 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 -import QtQuick 2.15 -import QtQuick.Layouts 1.15 -import HelperWidgets 2.0 +import QtQuick +import QtQuick.Layouts +import HelperWidgets MouseArea { id: mouseArea diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/SearchBox.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/SearchBox.qml index 4891c969aec..d885271ecb7 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/SearchBox.qml +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/SearchBox.qml @@ -13,6 +13,8 @@ T.TextField { signal searchChanged(string searchText) + property bool empty: control.text === "" + function isEmpty() { return control.text === "" } @@ -81,7 +83,7 @@ T.TextField { */ } - onTextChanged: control.searchChanged(text) + onTextChanged: control.searchChanged(control.text) T.Label { id: searchIcon diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioTheme/Values.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioTheme/Values.qml index 388f82d0470..0e5b4e2c7b8 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioTheme/Values.qml +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioTheme/Values.qml @@ -238,6 +238,12 @@ QtObject { property real dialogButtonSpacing: 10 property real dialogButtonPadding: 4 + // NEW NEW NEW + readonly property int flowMargin: 7 + readonly property int flowSpacing: 7 // Odd so cursor has a center location + readonly property int flowPillMargin: 4 + readonly property int flowPillHeight: 20 + // Theme Colors property bool isLightTheme: values.themeControlBackground.hsvValue > values.themeTextColor.hsvValue @@ -434,6 +440,10 @@ QtObject { property color themeDialogBackground: values.themeThumbnailBackground property color themeDialogOutline: values.themeInteraction + // Expression Builder + property color themePillBackground: Theme.color(Theme.DSdockWidgetSplitter) + + // Control Style Mapping property ControlStyle controlStyle: DefaultStyle {} property ControlStyle connectionPopupControlStyle: ConnectionPopupControlStyle {} diff --git a/src/plugins/qmldesigner/components/connectioneditor/connectionmodel.cpp b/src/plugins/qmldesigner/components/connectioneditor/connectionmodel.cpp index 08e662953e9..f132d76ffc9 100644 --- a/src/plugins/qmldesigner/components/connectioneditor/connectionmodel.cpp +++ b/src/plugins/qmldesigner/components/connectioneditor/connectionmodel.cpp @@ -559,8 +559,13 @@ QHash ConnectionModel::roleNames() const } ConnectionModelBackendDelegate::ConnectionModelBackendDelegate(ConnectionModel *parent) - : QObject(parent), m_signalDelegate(parent->connectionView()), m_okStatementDelegate(parent), - m_koStatementDelegate(parent), m_conditionListModel(parent) + : QObject(parent) + , m_signalDelegate(parent->connectionView()) + , m_okStatementDelegate(parent) + , m_koStatementDelegate(parent) + , m_conditionListModel(parent) + , m_propertyTreeModel(parent->connectionView()) + , m_propertyListProxyModel(&m_propertyTreeModel) { connect(&m_signalDelegate, &PropertyTreeModelDelegate::commitData, this, [this]() { handleTargetChanged(); @@ -753,6 +758,9 @@ void ConnectionModelBackendDelegate::setCurrentRow(int i) m_currentRow = i; + m_propertyTreeModel.resetModel(); + m_propertyListProxyModel.setRowAndInternalId(0, -1); + //setup ConnectionModel *model = qobject_cast(parent()); @@ -870,6 +878,16 @@ void ConnectionModelBackendDelegate::setSource(const QString &source) emit sourceChanged(); } +PropertyTreeModel *ConnectionModelBackendDelegate::propertyTreeModel() +{ + return &m_propertyTreeModel; +} + +PropertyListProxyModel *ConnectionModelBackendDelegate::propertyListProxyModel() +{ + return &m_propertyListProxyModel; +} + void ConnectionModelBackendDelegate::setupCondition() { auto &condition = ConnectionEditorStatements::matchedCondition(m_handler); @@ -1575,30 +1593,83 @@ ConditionListModel::ConditionToken ConditionListModel::tokenFromComparativeState void ConditionListModel::insertToken(int index, const QString &value) { + beginInsertRows({}, index, index); + m_tokens.insert(index, valueToToken(value)); validateAndRebuildTokens(); - resetModel(); + + endInsertRows(); + //resetModel(); } void ConditionListModel::updateToken(int index, const QString &value) { m_tokens[index] = valueToToken(value); validateAndRebuildTokens(); - resetModel(); + + dataChanged(createIndex(index, 0), createIndex(index, 0)); + //resetModel(); } void ConditionListModel::appendToken(const QString &value) { + beginInsertRows({}, rowCount() - 1, rowCount() - 1); + insertToken(rowCount(), value); validateAndRebuildTokens(); - resetModel(); + + endInsertRows(); + //resetModel(); } void ConditionListModel::removeToken(int index) { + beginRemoveRows({}, index, index); + m_tokens.remove(index, 1); validateAndRebuildTokens(); - resetModel(); + + endRemoveRows(); + + //resetModel(); +} + +void ConditionListModel::insertIntermediateToken(int index, const QString &value) +{ + beginInsertRows({}, index, index); + + ConditionToken token; + token.type = Intermediate; + token.value = value; + + m_tokens.insert(index, token); + + endInsertRows(); + //resetModel(); +} + +void ConditionListModel::insertShadowToken(int index, const QString &value) +{ + beginInsertRows({}, index, index); + + ConditionToken token; + token.type = Shadow; + token.value = value; + + m_tokens.insert(index, token); + + endInsertRows(); + + //resetModel(); +} + +void ConditionListModel::setShadowToken(int index, const QString &value) +{ + m_tokens[index].type = Shadow; + m_tokens[index].value = value; + + dataChanged(createIndex(index, 0), createIndex(index, 0)); + //resetModel(); } bool ConditionListModel::valid() const @@ -1655,20 +1726,29 @@ void ConditionListModel::command(const QString &string) } } -void ConditionListModel::setInvalid(const QString &errorMessage) +void ConditionListModel::setInvalid(const QString &errorMessage, int index) { m_valid = false; m_errorMessage = errorMessage; + emit errorChanged(); emit validChanged(); + + if (index != -1) { + m_errorIndex = index; + emit errorIndexChanged(); + } } void ConditionListModel::setValid() { m_valid = true; m_errorMessage.clear(); + m_errorIndex = -1; + emit errorChanged(); emit validChanged(); + emit errorIndexChanged(); } QString ConditionListModel::error() const @@ -1676,6 +1756,11 @@ QString ConditionListModel::error() const return m_errorMessage; } +int ConditionListModel::errorIndex() const +{ + return m_errorIndex; +} + void ConditionListModel::internalSetup() { setInvalid(tr("No Valid Condition")); @@ -1766,11 +1851,26 @@ int ConditionListModel::checkOrder() const it++; ret++; } + + if (wasOperator) + return ret; + return -1; } void ConditionListModel::validateAndRebuildTokens() { + /// NEW + auto it = m_tokens.begin(); + + while (it != m_tokens.end()) { + if (it->type == Intermediate) + *it = valueToToken(it->value); + + it++; + } + // NEW + QString invalidValue; const bool invalidToken = Utils::contains(m_tokens, [&invalidValue](const ConditionToken &token) { @@ -1780,12 +1880,12 @@ void ConditionListModel::validateAndRebuildTokens() }); if (invalidToken) { - setInvalid(tr("Invalid token %").arg(invalidToken)); + setInvalid(tr("Invalid token %1").arg(invalidValue)); return; } if (int firstError = checkOrder() != -1) { - setInvalid(tr("Invalid order at %1").arg(firstError)); + setInvalid(tr("Invalid order at %1").arg(firstError), firstError); return; } @@ -1831,9 +1931,6 @@ ConnectionEditorStatements::ConditionToken ConditionListModel::toOperatorStateme if (token.value == "!==") return ConnectionEditorStatements::ConditionToken::Not; - if (token.value == "!==") - return ConnectionEditorStatements::ConditionToken::Not; - if (token.value == ">") return ConnectionEditorStatements::ConditionToken::LargerThan; diff --git a/src/plugins/qmldesigner/components/connectioneditor/connectionmodel.h b/src/plugins/qmldesigner/components/connectioneditor/connectionmodel.h index ecea5e7c058..59b92a405e9 100644 --- a/src/plugins/qmldesigner/components/connectioneditor/connectionmodel.h +++ b/src/plugins/qmldesigner/components/connectioneditor/connectionmodel.h @@ -96,9 +96,10 @@ class ConditionListModel : public QAbstractListModel Q_PROPERTY(bool valid READ valid NOTIFY validChanged) Q_PROPERTY(bool empty READ empty NOTIFY emptyChanged) Q_PROPERTY(QString error READ error NOTIFY errorChanged) + Q_PROPERTY(int errorIndex READ errorIndex NOTIFY errorIndexChanged) public: - enum ConditionType { Invalid, Operator, Literal, Variable }; + enum ConditionType { Intermediate, Invalid, Operator, Literal, Variable, Shadow }; Q_ENUM(ConditionType) struct ConditionToken @@ -129,22 +130,28 @@ public: Q_INVOKABLE void appendToken(const QString &value); Q_INVOKABLE void removeToken(int index); + Q_INVOKABLE void insertIntermediateToken(int index, const QString &value); + Q_INVOKABLE void insertShadowToken(int index, const QString &value); + Q_INVOKABLE void setShadowToken(int index, const QString &value); + bool valid() const; bool empty() const; //for debugging Q_INVOKABLE void command(const QString &string); - void setInvalid(const QString &errorMessage); + void setInvalid(const QString &errorMessage, int index = -1); void setValid(); QString error() const; + int errorIndex() const; signals: void validChanged(); void emptyChanged(); void conditionChanged(); void errorChanged(); + void errorIndexChanged(); private: void internalSetup(); @@ -162,6 +169,7 @@ private: QList m_tokens; bool m_valid = false; QString m_errorMessage; + int m_errorIndex = -1; }; class ConnectionModelStatementDelegate : public QObject @@ -245,6 +253,9 @@ class ConnectionModelBackendDelegate : public QObject Q_PROPERTY(bool hasElse READ hasElse NOTIFY hasElseChanged) Q_PROPERTY(QString source READ source NOTIFY sourceChanged) + Q_PROPERTY(PropertyTreeModel *propertyTreeModel READ propertyTreeModel CONSTANT) + Q_PROPERTY(PropertyListProxyModel *propertyListProxyModel READ propertyListProxyModel CONSTANT) + public: explicit ConnectionModelBackendDelegate(ConnectionModel *parent = nullptr); @@ -282,6 +293,10 @@ private: ConditionListModel *conditionListModel(); QString source() const; void setSource(const QString &source); + + PropertyTreeModel *propertyTreeModel(); + PropertyListProxyModel *propertyListProxyModel(); + void setupCondition(); void setupHandlerAndStatements(); @@ -303,6 +318,8 @@ private: bool m_hasCondition = false; bool m_hasElse = false; QString m_source; + PropertyTreeModel m_propertyTreeModel; + PropertyListProxyModel m_propertyListProxyModel; }; } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/connectioneditor/connectionview.cpp b/src/plugins/qmldesigner/components/connectioneditor/connectionview.cpp index d33e67d1164..41eee97be48 100644 --- a/src/plugins/qmldesigner/components/connectioneditor/connectionview.cpp +++ b/src/plugins/qmldesigner/components/connectioneditor/connectionview.cpp @@ -88,10 +88,13 @@ public: 0, "ConnectionModelStatementDelegate"); - qmlRegisterType("ConnectionsEditorEditorBackend", - 1, - 0, - "ConditionListModel"); + qmlRegisterType("ConnectionsEditorEditorBackend", 1, 0, "ConditionListModel"); + + qmlRegisterType("ConnectionsEditorEditorBackend", 1, 0, "PropertyTreeModel"); + qmlRegisterType("ConnectionsEditorEditorBackend", + 1, + 0, + "PropertyListProxyModel"); Theme::setupTheme(engine()); diff --git a/src/plugins/qmldesigner/components/connectioneditor/propertytreemodel.cpp b/src/plugins/qmldesigner/components/connectioneditor/propertytreemodel.cpp index dcbbe5c1a60..759d47037ae 100644 --- a/src/plugins/qmldesigner/components/connectioneditor/propertytreemodel.cpp +++ b/src/plugins/qmldesigner/components/connectioneditor/propertytreemodel.cpp @@ -155,7 +155,8 @@ QVariant PropertyTreeModel::data(const QModelIndex &index, int role) const if (role == RowRole) return index.row(); - if (role == PropertyNameRole || role == PropertyPriorityRole || role == ExpressionRole) { + if (role == PropertyNameRole || role == PropertyPriorityRole || role == ExpressionRole + || role == ChildCountRole) { if (!index.isValid()) return {}; @@ -166,26 +167,33 @@ QVariant PropertyTreeModel::data(const QModelIndex &index, int role) const DataCacheItem item = m_indexHash[index.internalId()]; - if (item.propertyName.isEmpty()) { //node - if (role == PropertyNameRole) - return item.modelNode.displayName(); - - return true; //nodes are always shown - } + if (role == ChildCountRole) + return rowCount(index); if (role == ExpressionRole) - return QString(item.modelNode.id() + item.propertyName); + return QString(item.modelNode.id() + "." + item.propertyName); - if (role == PropertyNameRole) - return item.propertyName; + if (role == PropertyNameRole) { + if (!item.propertyName.isEmpty()) + return QString::fromUtf8(item.propertyName); + else + return item.modelNode.displayName(); + } static const auto priority = properityLists(); if (std::find(priority.begin(), priority.end(), item.propertyName) != priority.end()) - return true; //listed priority properties + return true; // listed priority properties auto dynamic = getDynamicProperties(item.modelNode); if (std::find(dynamic.begin(), dynamic.end(), item.propertyName) != dynamic.end()) - return true; //dynamic properties have priority + return true; // dynamic properties have priority + + if (item.propertyName.isEmpty()) { //node + //if (role == PropertyNameRole) + // return item.modelNode.displayName(); + + return true; // nodes are always shown + } return false; } @@ -216,7 +224,7 @@ QVariant PropertyTreeModel::data(const QModelIndex &index, int role) const } if (role == Qt::DisplayRole) - return item.propertyName; + return QString::fromUtf8(item.propertyName); QFont f; auto priority = properityLists(); @@ -241,7 +249,7 @@ QModelIndex PropertyTreeModel::index(int row, int column, const QModelIndex &par return {}; if (!hasIndex(row, column, parent)) - return QModelIndex(); + return {}; const int rootId = -1; @@ -312,7 +320,7 @@ QModelIndex PropertyTreeModel::parent(const QModelIndex &index) const return ensureModelIndex(item.modelNode, row); } -QPersistentModelIndex PropertyTreeModel::indexForInernalIdAndRow(int internalId, int row) +QPersistentModelIndex PropertyTreeModel::indexForInternalIdAndRow(int internalId, int row) { return createIndex(row, 0, internalId); } @@ -323,7 +331,7 @@ int PropertyTreeModel::rowCount(const QModelIndex &parent) const return 0; if (!parent.isValid()) - return 1; + return 1; //m_nodeList.size(); int internalId = parent.internalId(); @@ -783,7 +791,8 @@ QHash PropertyTreeModel::roleNames() const { static QHash roleNames{{PropertyNameRole, "propertyName"}, {PropertyPriorityRole, "hasPriority"}, - {ExpressionRole, "expression"}}; + {ExpressionRole, "expression"}, + {ChildCountRole, "childCount"}}; return roleNames; } @@ -792,17 +801,24 @@ PropertyListProxyModel::PropertyListProxyModel(PropertyTreeModel *parent) : QAbstractListModel(), m_treeModel(parent) {} -void PropertyListProxyModel::setRowandInternalId(int row, int internalId) +void PropertyListProxyModel::resetModel() { + beginResetModel(); + endResetModel(); +} + +void PropertyListProxyModel::setRowAndInternalId(int row, int internalId) +{ + qDebug() << Q_FUNC_INFO << row << internalId; QTC_ASSERT(m_treeModel, return ); if (internalId == -1) m_parentIndex = m_treeModel->index(0, 0); else - m_parentIndex = m_treeModel->indexForInernalIdAndRow(internalId, row); + m_parentIndex = m_treeModel->index(row, 0, m_parentIndex); + //m_parentIndex = m_treeModel->indexForInternalIdAndRow(internalId, row); - beginResetModel(); - endResetModel(); + resetModel(); } int PropertyListProxyModel::rowCount(const QModelIndex &) const @@ -820,6 +836,40 @@ QVariant PropertyListProxyModel::data(const QModelIndex &index, int role) const return m_treeModel->data(treeIndex, role); } +QHash PropertyListProxyModel::roleNames() const +{ + return m_treeModel->roleNames(); +} + +void PropertyListProxyModel::goInto(int row) +{ + qDebug() << Q_FUNC_INFO << row << m_parentIndex.internalId(); + setRowAndInternalId(row, 0); //m_parentIndex.internalId()); + + emit parentNameChanged(); +} + +void PropertyListProxyModel::goUp() +{ + qDebug() << Q_FUNC_INFO; + m_parentIndex = m_treeModel->parent(m_parentIndex); + resetModel(); + + emit parentNameChanged(); +} + +void PropertyListProxyModel::reset() +{ + setRowAndInternalId(0, -1); // TODO ??? + + emit parentNameChanged(); +} + +QString PropertyListProxyModel::parentName() const +{ + return m_treeModel->data(m_parentIndex, PropertyTreeModel::UserRoles::PropertyNameRole).toString(); +} + PropertyTreeModelDelegate::PropertyTreeModelDelegate(ConnectionView *parent) : m_model(parent) { connect(&m_nameCombboBox, &StudioQmlComboBoxBackend::activated, this, [this]() { diff --git a/src/plugins/qmldesigner/components/connectioneditor/propertytreemodel.h b/src/plugins/qmldesigner/components/connectioneditor/propertytreemodel.h index ec96328c02c..9c1c876b466 100644 --- a/src/plugins/qmldesigner/components/connectioneditor/propertytreemodel.h +++ b/src/plugins/qmldesigner/components/connectioneditor/propertytreemodel.h @@ -31,6 +31,7 @@ public: PropertyNameRole = Qt::UserRole + 1, PropertyPriorityRole, ExpressionRole, + ChildCountRole, RowRole, InternalIdRole }; @@ -57,7 +58,7 @@ public: QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; QModelIndex parent(const QModelIndex &index) const override; - QPersistentModelIndex indexForInernalIdAndRow(int internalId, int row); + QPersistentModelIndex indexForInternalIdAndRow(int internalId, int row); int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; @@ -70,14 +71,13 @@ public: }; void setPropertyType(PropertyTypes type); - void setFilter(const QString &filter); + Q_INVOKABLE void setFilter(const QString &filter); QList nodeList() const; const std::vector getProperties(const ModelNode &modelNode) const; ModelNode getModelNodeForId(const QString &id) const; -protected: QHash roleNames() const override; private: @@ -127,12 +127,29 @@ private: class PropertyListProxyModel : public QAbstractListModel { Q_OBJECT + + Q_PROPERTY(QString parentName READ parentName NOTIFY parentNameChanged) + public: PropertyListProxyModel(PropertyTreeModel *parent); - void setRowandInternalId(int row, int internalId); + + void resetModel(); + + void setRowAndInternalId(int row, int internalId); int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + + Q_INVOKABLE void goInto(int row); + Q_INVOKABLE void goUp(); + Q_INVOKABLE void reset(); + + QString parentName() const; + +signals: + void parentNameChanged(); + private: ModelNode m_modelNode; PropertyName m_propertyName;