From 608fa309a7135fa96df827ed073d6b97426738cd Mon Sep 17 00:00:00 2001 From: Thomas Hartmann Date: Fri, 16 Sep 2022 17:20:05 +0200 Subject: [PATCH 01/12] QmlDesigner: Add RegExp for dynamic properties Task-number: QDS-7701 Change-Id: I9ef218c1b72789c527af5eabe923cf664b951ab2 Reviewed-by: Reviewed-by: Miikka Heikkinen --- .../imports/HelperWidgets/DynamicPropertiesSection.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/DynamicPropertiesSection.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/DynamicPropertiesSection.qml index 773f08f6bb2..4bd99e3f68e 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/DynamicPropertiesSection.qml +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/DynamicPropertiesSection.qml @@ -677,6 +677,7 @@ Section { translationIndicatorVisible: false width: cePopup.itemWidth rightPadding: 8 + validator: RegExpValidator { regExp: /[a-z]+[0-9A-Za-z]*/ } } } RowLayout { From 89535f10256cc1db2d5d04f4b6427bf918b5f8cd Mon Sep 17 00:00:00 2001 From: Thomas Hartmann Date: Fri, 16 Sep 2022 17:19:02 +0200 Subject: [PATCH 02/12] QmlDesigner: Hide dynamic properties if multiple items are selected If multiple items get selected it is not clear to which item the property belongs. Therefore we hide the section. Task-number: QDS-7688 Change-Id: Ieaa0114d87974fea3135adc2d1bcd979c2236ed2 Reviewed-by: Miikka Heikkinen --- .../propertyEditorQmlSources/QtQuick/ItemPane.qml | 1 + .../QtQuick3D/Object3DPane.qml | 1 + .../propertyeditor/propertyeditorcontextobject.cpp | 14 ++++++++++++++ .../propertyeditor/propertyeditorcontextobject.h | 10 ++++++++++ 4 files changed, 26 insertions(+) diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick/ItemPane.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick/ItemPane.qml index 140f05c1071..2a724bd58de 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick/ItemPane.qml +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick/ItemPane.qml @@ -39,6 +39,7 @@ PropertyEditorPane { DynamicPropertiesSection { propertiesModel: SelectionDynamicPropertiesModel {} + visible: !hasMultiSelection } GeometrySection {} diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/Object3DPane.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/Object3DPane.qml index f0261c7c753..5483ca67ac4 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/Object3DPane.qml +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/Object3DPane.qml @@ -42,6 +42,7 @@ PropertyEditorPane { DynamicPropertiesSection { propertiesModel: SelectionDynamicPropertiesModel {} + visible: !hasMultiSelection } Loader { diff --git a/src/plugins/qmldesigner/components/propertyeditor/propertyeditorcontextobject.cpp b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorcontextobject.cpp index 8ba1325efaf..75c63159247 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/propertyeditorcontextobject.cpp +++ b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorcontextobject.cpp @@ -429,6 +429,20 @@ QQmlComponent *PropertyEditorContextObject::specificQmlComponent() return m_qmlComponent; } +bool PropertyEditorContextObject::hasMultiSelection() const +{ + return m_hasMultiSelection; +} + +void PropertyEditorContextObject::setHasMultiSelection(bool b) +{ + if (b == m_hasMultiSelection) + return; + + m_hasMultiSelection = b; + emit hasMultiSelectionChanged(); +} + void PropertyEditorContextObject::setSpecificsUrl(const QUrl &newSpecificsUrl) { if (newSpecificsUrl == m_specificsUrl) diff --git a/src/plugins/qmldesigner/components/propertyeditor/propertyeditorcontextobject.h b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorcontextobject.h index 05ebe47ec28..fba0e4a66cb 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/propertyeditorcontextobject.h +++ b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorcontextobject.h @@ -68,6 +68,9 @@ class PropertyEditorContextObject : public QObject Q_PROPERTY(QQmlComponent* specificQmlComponent READ specificQmlComponent NOTIFY specificQmlComponentChanged) + Q_PROPERTY(bool hasMultiSelection READ hasMultiSelection WRITE setHasMultiSelection NOTIFY + hasMultiSelectionChanged) + public: PropertyEditorContextObject(QObject *parent = nullptr); @@ -126,6 +129,10 @@ public: bool hasAliasExport() const { return m_aliasExport; } + bool hasMultiSelection() const; + + void setHasMultiSelection(bool); + signals: void specificsUrlChanged(); void specificQmlDataChanged(); @@ -142,6 +149,7 @@ signals: void hasAliasExportChanged(); void hasActiveTimelineChanged(); void activeDragSuffixChanged(); + void hasMultiSelectionChanged(); public slots: @@ -192,6 +200,8 @@ private: bool m_setHasActiveTimeline = false; QString m_activeDragSuffix; + + bool m_hasMultiSelection = false; }; class EasingCurveEditor : public QObject From 9ffc004bebb3c32614f861291ddd2cf1c13d180d Mon Sep 17 00:00:00 2001 From: Henning Gruendl Date: Tue, 13 Sep 2022 14:25:16 +0200 Subject: [PATCH 03/12] QmlDesigner: Add new states editor * Add responsive layout algorithm for states editor * Add support for extending states * Drag and drop states * Display when condition in state thumbnail * Toggle between property changes and thumbnail * Display PropertyChanges properties * Make property changes removable * Add StateGroup support * Add toolbar Task-number: QDS-7639 Change-Id: Iabcbb0a02f7728c29abbdc1000102582c5638eba Reviewed-by: Qt CI Bot Reviewed-by: Thomas Hartmann --- .../qmldesigner/newstateseditor/Main.qml | 832 ++++++++++++++++ .../newstateseditor/MenuButton.qml | 134 +++ .../qmldesigner/newstateseditor/StateMenu.qml | 108 ++ .../newstateseditor/StateScrollBar.qml | 66 ++ .../newstateseditor/StateThumbnail.qml | 746 ++++++++++++++ .../newstateseditor/images/checkers.png | Bin 0 -> 80 bytes .../imports/StatesEditor/Constants.qml | 39 + .../imports/StatesEditor/qmldir | 1 + .../imports/StudioControls/AbstractButton.qml | 37 +- .../imports/StudioControls/Dialog.qml | 75 ++ .../imports/StudioControls/DialogButton.qml | 105 ++ .../StudioControls/DialogButtonBox.qml | 69 ++ .../imports/StudioControls/Indicator.qml | 80 ++ .../imports/StudioControls/TextField.qml | 26 +- .../imports/StudioControls/qmldir | 4 + .../imports/StudioTheme/Values.qml | 22 +- share/qtcreator/themes/dark.creatortheme | 3 + share/qtcreator/themes/default.creatortheme | 3 + .../themes/design-light.creatortheme | 3 + share/qtcreator/themes/design.creatortheme | 3 + share/qtcreator/themes/flat-dark.creatortheme | 3 + .../qtcreator/themes/flat-light.creatortheme | 3 + share/qtcreator/themes/flat.creatortheme | 3 + src/libs/utils/theme/theme.h | 3 + src/plugins/qmldesigner/CMakeLists.txt | 11 + .../quick2propertyeditorview.cpp | 4 + .../stateseditornew/propertychangesmodel.cpp | 152 +++ .../stateseditornew/propertychangesmodel.h | 80 ++ .../stateseditornew/propertymodel.cpp | 172 ++++ .../stateseditornew/propertymodel.h | 73 ++ .../stateseditorimageprovider.cpp | 82 ++ .../stateseditorimageprovider.h | 54 + .../stateseditornew/stateseditormodel.cpp | 455 +++++++++ .../stateseditornew/stateseditormodel.h | 127 +++ .../stateseditornew/stateseditorview.cpp | 936 ++++++++++++++++++ .../stateseditornew/stateseditorview.h | 158 +++ .../stateseditornew/stateseditorwidget.cpp | 199 ++++ .../stateseditornew/stateseditorwidget.h | 80 ++ .../componentsplugin/componentsplugin.qbs | 1 + .../designercore/include/qmlchangeset.h | 10 +- .../designercore/include/qmlstate.h | 9 + .../designercore/include/qmlvisualnode.h | 1 + .../designercore/model/qmlchangeset.cpp | 36 + .../designercore/model/qmlstate.cpp | 32 +- .../designercore/model/texttomodelmerger.cpp | 2 +- .../designercore/model/viewmanager.cpp | 20 +- .../qmldesigner/qmldesignerconstants.h | 2 + src/plugins/qmldesigner/qmldesignerplugin.qbs | 13 + 48 files changed, 5051 insertions(+), 26 deletions(-) create mode 100644 share/qtcreator/qmldesigner/newstateseditor/Main.qml create mode 100644 share/qtcreator/qmldesigner/newstateseditor/MenuButton.qml create mode 100644 share/qtcreator/qmldesigner/newstateseditor/StateMenu.qml create mode 100644 share/qtcreator/qmldesigner/newstateseditor/StateScrollBar.qml create mode 100644 share/qtcreator/qmldesigner/newstateseditor/StateThumbnail.qml create mode 100644 share/qtcreator/qmldesigner/newstateseditor/images/checkers.png create mode 100644 share/qtcreator/qmldesigner/newstateseditor/imports/StatesEditor/Constants.qml create mode 100644 share/qtcreator/qmldesigner/newstateseditor/imports/StatesEditor/qmldir create mode 100644 share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/Dialog.qml create mode 100644 share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/DialogButton.qml create mode 100644 share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/DialogButtonBox.qml create mode 100644 share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/Indicator.qml create mode 100644 src/plugins/qmldesigner/components/stateseditornew/propertychangesmodel.cpp create mode 100644 src/plugins/qmldesigner/components/stateseditornew/propertychangesmodel.h create mode 100644 src/plugins/qmldesigner/components/stateseditornew/propertymodel.cpp create mode 100644 src/plugins/qmldesigner/components/stateseditornew/propertymodel.h create mode 100644 src/plugins/qmldesigner/components/stateseditornew/stateseditorimageprovider.cpp create mode 100644 src/plugins/qmldesigner/components/stateseditornew/stateseditorimageprovider.h create mode 100644 src/plugins/qmldesigner/components/stateseditornew/stateseditormodel.cpp create mode 100644 src/plugins/qmldesigner/components/stateseditornew/stateseditormodel.h create mode 100644 src/plugins/qmldesigner/components/stateseditornew/stateseditorview.cpp create mode 100644 src/plugins/qmldesigner/components/stateseditornew/stateseditorview.h create mode 100644 src/plugins/qmldesigner/components/stateseditornew/stateseditorwidget.cpp create mode 100644 src/plugins/qmldesigner/components/stateseditornew/stateseditorwidget.h diff --git a/share/qtcreator/qmldesigner/newstateseditor/Main.qml b/share/qtcreator/qmldesigner/newstateseditor/Main.qml new file mode 100644 index 00000000000..3b3162f322e --- /dev/null +++ b/share/qtcreator/qmldesigner/newstateseditor/Main.qml @@ -0,0 +1,832 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +import QtQuick +import QtQuick.Controls +import StatesEditor +import StudioControls 1.0 as StudioControls +import StudioTheme as StudioTheme + +Rectangle { + id: root + + signal createNewState + signal cloneState(int internalNodeId) + signal extendState(int internalNodeId) + signal deleteState(int internalNodeId) + + property bool isLandscape: true + + color: StudioTheme.Values.themeStatePanelBackground + + onWidthChanged: root.responsiveResize(root.width, root.height) + onHeightChanged: root.responsiveResize(root.width, root.height) + + Component.onCompleted: root.responsiveResize(root.width, root.height) + + function numFit(overall, size, space) { + let tmpNum = Math.floor(overall / size) + let spaceLeft = overall - (tmpNum * size) + return spaceLeft - (space * (tmpNum - 1)) >= 0 ? tmpNum : tmpNum - 1 + } + + function responsiveResize(width, height) { + height -= toolBar.height + (2 * root.padding) + width -= (2 * root.padding) + + var numStates = statesRepeater.count - 1 // Subtract base state + var numRows = 0 + var numColumns = 0 + + // Size extension in case of extend groups are shown + var sizeExtension = root.showExtendGroups ? root.extend : 0 + var doubleSizeExtension = root.showExtendGroups ? 2 * root.extend : 0 + + // Get view orientation (LANDSCAPE, PORTRAIT) + if (width >= height) { + root.isLandscape = true + outerGrid.columns = 3 + outerGrid.rows = 1 + // Three outer section height (base state, middle, plus button) + baseStateWrapper.height = height + root.scrollViewHeight = height + addWrapper.height = height + + height -= doubleSizeExtension + + if (height > Constants.maxThumbSize) { + // In this case we want to have a multi row grid in the center + root.thumbSize = Constants.maxThumbSize + + let tmpScrollViewWidth = width - root.thumbSize * 1.5 - 2 * root.outerGridSpacing + + // Inner grid calculation + numRows = root.numFit(height, Constants.maxThumbSize, root.innerGridSpacing) + numColumns = Math.min(numStates, root.numFit(tmpScrollViewWidth, root.thumbSize, + root.innerGridSpacing)) + + let tmpRows = Math.ceil(numStates / numColumns) + + if (tmpRows <= numRows) + numRows = tmpRows + else + numColumns = Math.ceil(numStates / numRows) + } else { + // This case is for single row layout and small thumb view + root.thumbSize = Math.max(height, Constants.minThumbSize) + + // Inner grid calculation + numColumns = numStates + numRows = 1 + } + + Constants.thumbnailSize = root.thumbSize + + let tmpWidth = root.thumbSize * numColumns + root.innerGridSpacing * (numColumns - 1) + doubleSizeExtension + let remainingSpace = width - root.thumbSize - 2 * root.outerGridSpacing + let space = remainingSpace - tmpWidth + + if (space >= root.thumbSize) { + root.scrollViewWidth = tmpWidth + addWrapper.width = space + } else { + addWrapper.width = Math.max(space, 0.5 * root.thumbSize) + root.scrollViewWidth = remainingSpace - addWrapper.width + } + + root.topMargin = (root.scrollViewHeight - (root.thumbSize * numRows) + - root.innerGridSpacing * (numRows - 1)) * 0.5 - sizeExtension + + addCanvas.width = Math.min(addWrapper.width, root.thumbSize) + addCanvas.height = root.thumbSize + + baseStateWrapper.width = root.thumbSize + + baseStateThumbnail.anchors.verticalCenter = baseStateWrapper.verticalCenter + baseStateThumbnail.anchors.horizontalCenter = undefined + + addCanvas.anchors.verticalCenter = addWrapper.verticalCenter + addCanvas.anchors.horizontalCenter = undefined + addCanvas.anchors.top = undefined + addCanvas.anchors.left = addWrapper.left + + root.leftMargin = 0 // resetting left margin in case of orientation switch + } else { + root.isLandscape = false + outerGrid.rows = 3 + outerGrid.columns = 1 + // Three outer section width (base state, middle, plus button) + baseStateWrapper.width = width + root.scrollViewWidth = width + addWrapper.width = width + + width -= doubleSizeExtension + + if (width > Constants.maxThumbSize) { + // In this case we want to have a multi column grid in the center + root.thumbSize = Constants.maxThumbSize + + let tmpScrollViewHeight = height - root.thumbSize * 1.5 - 2 * root.outerGridSpacing + + // Inner grid calculation + numRows = Math.min(numStates, root.numFit(tmpScrollViewHeight, root.thumbSize, + root.innerGridSpacing)) + numColumns = root.numFit(width, Constants.maxThumbSize, root.innerGridSpacing) + + let tmpColumns = Math.ceil(numStates / numRows) + + if (tmpColumns <= numColumns) + numColumns = tmpColumns + else + numRows = Math.ceil(numStates / numColumns) + } else { + // This case is for single column layout and small thumb view + root.thumbSize = Math.max(width, Constants.minThumbSize) + + // Inner grid calculation + numRows = numStates + numColumns = 1 + } + + Constants.thumbnailSize = root.thumbSize + + let tmpHeight = root.thumbSize * numRows + root.innerGridSpacing * (numRows - 1) + doubleSizeExtension + let remainingSpace = height - root.thumbSize - 2 * root.outerGridSpacing + let space = remainingSpace - tmpHeight + + if (space >= root.thumbSize) { + root.scrollViewHeight = tmpHeight + addWrapper.height = space + } else { + addWrapper.height = Math.max(space, 0.5 * root.thumbSize) + root.scrollViewHeight = remainingSpace - addWrapper.height + } + + root.leftMargin = (root.scrollViewWidth - (root.thumbSize * numColumns) + - root.innerGridSpacing * (numColumns - 1)) * 0.5 - sizeExtension + + addCanvas.width = root.thumbSize + addCanvas.height = Math.min(addWrapper.height, root.thumbSize) + + baseStateWrapper.height = root.thumbSize + + baseStateThumbnail.anchors.verticalCenter = undefined + baseStateThumbnail.anchors.horizontalCenter = baseStateWrapper.horizontalCenter + + addCanvas.anchors.verticalCenter = undefined + addCanvas.anchors.horizontalCenter = addWrapper.horizontalCenter + addCanvas.anchors.top = addWrapper.top + addCanvas.anchors.left = undefined + + root.topMargin = 0 // resetting top margin in case of orientation switch + } + + // Always assign the bigger one first otherwise there will be console output complaining + if (numRows > innerGrid.rows) { + innerGrid.rows = numRows + innerGrid.columns = numColumns + } else { + innerGrid.columns = numColumns + innerGrid.rows = numRows + } + } + + // These function assume that the order of the states is as follows: + // State A, State B (extends State A), ... so the extended state always comes first + function isInRange(i) { + return i >= 0 && i < statesEditorModel.count() + } + + function nextStateHasExtend(i) { + let next = i + 1 + return root.isInRange(next) ? statesEditorModel.get(next).hasExtend : false + } + + function previousStateHasExtend(i) { + let prev = i - 1 + return root.isInRange(prev) ? statesEditorModel.get(prev).hasExtend : false + } + + property bool showExtendGroups: statesEditorModel.hasExtend + + onShowExtendGroupsChanged: root.responsiveResize(root.width, root.height) + + property int extend: 16 + + property int thumbSize: 250 + + property int padding: 10 + + property int scrollViewWidth: 640 + property int scrollViewHeight: 480 + property int outerGridSpacing: 10 + property int innerGridSpacing: root.showExtendGroups ? 40 : root.outerGridSpacing + + // These margins are used to push the inner grid down or to the left depending on the views + // orientation to align to the outer grid + property int topMargin: 0 + property int leftMargin: 0 + + property bool tinyMode: Constants.thumbnailSize <= Constants.thumbnailBreak + + property int currentStateInternalId: 0 + + // This timer is used to delay the current state animation as it didn't work due to the + // repeaters item not being positioned in time resulting in 0 x and y position if the grids + // row and column were not changed during the layout algorithm . + Timer { + id: layoutTimer + interval: 50 + running: false + repeat: false + onTriggered: { + // Move the current state into view if outside + if (root.currentStateInternalId === 0) + // Not for base state + return + + var x = 0 + var y = 0 + for (var i = 0; i < statesRepeater.count; ++i) { + let item = statesRepeater.itemAt(i) + + if (item.internalNodeId === root.currentStateInternalId) { + x = item.x + y = item.y + break + } + } + + // Check if it is in view + if (x <= frame.contentX + || x >= (frame.contentX + root.scrollViewWidth - root.thumbSize)) + frame.contentX = x - root.scrollViewWidth * 0.5 + root.thumbSize * 0.5 + + if (y <= frame.contentY + || y >= (frame.contentY + root.scrollViewHeight - root.thumbSize)) + frame.contentY = y - root.scrollViewHeight * 0.5 + root.thumbSize * 0.5 + } + } + + onCurrentStateInternalIdChanged: layoutTimer.start() + + StudioControls.Dialog { + id: editDialog + title: qsTr("Rename state group") + standardButtons: Dialog.Apply | Dialog.Cancel + x: editButton.x - Math.max(0, editButton.x + editDialog.width - root.width) + y: toolBar.height + closePolicy: Popup.NoAutoClose + + width: Math.min(300, root.width) + + onApplied: { + let renamed = statesEditorModel.renameActiveStateGroup(editTextField.text) + if (renamed) + editDialog.close() + } + + StudioControls.TextField { + id: editTextField + text: statesEditorModel.activeStateGroup + actionIndicatorVisible: false + translationIndicatorVisible: false + anchors.fill: parent + } + } + + Rectangle { + id: toolBar + + property bool doubleRow: root.width < 450 + + onDoubleRowChanged: { + if (toolBar.doubleRow) { + toolBarGrid.rows = 2 + toolBarGrid.columns = 1 + } else { + toolBarGrid.columns = 2 + toolBarGrid.rows = 1 + } + } + + color: StudioTheme.Values.themeSectionHeadBackground + width: root.width + height: (toolBar.doubleRow ? 2 : 1) * StudioTheme.Values.toolbarHeight + + Grid { + id: toolBarGrid + columns: 2 + rows: 1 + columnSpacing: StudioTheme.Values.toolbarSpacing + + Row { + id: stateGroupSelectionRow + height: StudioTheme.Values.toolbarHeight + spacing: StudioTheme.Values.toolbarSpacing + leftPadding: root.padding + + Text { + id: stateGroupLabel + color: StudioTheme.Values.themeTextColor + text: qsTr("State Group") + font.pixelSize: StudioTheme.Values.baseFontSize + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + height: StudioTheme.Values.height + anchors.verticalCenter: parent.verticalCenter + visible: root.width > 240 + } + + StudioControls.ComboBox { + id: stateGroupComboBox + actionIndicatorVisible: false + model: statesEditorModel.stateGroups + currentIndex: statesEditorModel.activeStateGroupIndex + anchors.verticalCenter: parent.verticalCenter + width: stateGroupLabel.visible ? StudioTheme.Values.defaultControlWidth + : root.width - 2 * root.padding + + popup.onOpened: editDialog.close() + + // currentIndex needs special treatment, because if model is changed, it will be + // reset regardless of binding. + Connections { + target: statesEditorModel + function onActiveStateGroupIndexChanged() { + stateGroupComboBox.currentIndex = statesEditorModel.activeStateGroupIndex + } + } + + onModelChanged: { + stateGroupComboBox.currentIndex = statesEditorModel.activeStateGroupIndex + } + + onCompressedActivated: function (index, reason) { + statesEditorModel.activeStateGroupIndex = index + root.responsiveResize(root.width, root.height) + } + } + } + + Row { + Row { + id: stateGroupEditRow + height: StudioTheme.Values.toolbarHeight + spacing: StudioTheme.Values.toolbarSpacing + leftPadding: toolBar.doubleRow ? root.padding : 0 + + StudioControls.AbstractButton { + buttonIcon: StudioTheme.Constants.plus + anchors.verticalCenter: parent.verticalCenter + onClicked: statesEditorModel.addStateGroup("stateGroup") + } + + StudioControls.AbstractButton { + buttonIcon: StudioTheme.Constants.minus + anchors.verticalCenter: parent.verticalCenter + enabled: statesEditorModel.activeStateGroupIndex !== 0 + onClicked: statesEditorModel.removeStateGroup() + } + + StudioControls.AbstractButton { + id: editButton + buttonIcon: StudioTheme.Constants.edit + anchors.verticalCenter: parent.verticalCenter + enabled: statesEditorModel.activeStateGroupIndex !== 0 + checked: editDialog.visible + onClicked: { + if (editDialog.opened) + editDialog.close() + else + editDialog.open() + } + } + } + + Item { + width: Math.max(0, toolBar.width - (toolBar.doubleRow ? 0 : (stateGroupSelectionRow.width + + toolBarGrid.columnSpacing)) + - stateGroupEditRow.width - thumbnailToggleRow.width) + height: 1 + } + + Row { + id: thumbnailToggleRow + height: StudioTheme.Values.toolbarHeight + spacing: StudioTheme.Values.toolbarSpacing + rightPadding: root.padding + + StudioControls.AbstractButton { + buttonIcon: StudioTheme.Constants.gridView + anchors.verticalCenter: parent.verticalCenter + enabled: !root.tinyMode + onClicked: { + for (var i = 0; i < statesRepeater.count; ++i) + statesRepeater.itemAt(i).setPropertyChangesVisible(false) + } + } + + StudioControls.AbstractButton { + buttonIcon: StudioTheme.Constants.textFullJustification + anchors.verticalCenter: parent.verticalCenter + enabled: !root.tinyMode + onClicked: { + for (var i = 0; i < statesRepeater.count; ++i) + statesRepeater.itemAt(i).setPropertyChangesVisible(true) + } + } + } + } + } + } + + Grid { + id: outerGrid + x: root.padding + y: toolBar.height + root.padding + columns: 3 + rows: 1 + spacing: root.outerGridSpacing + + Item { + id: baseStateWrapper + + StateThumbnail { + // Base State + id: baseStateThumbnail + width: Constants.thumbnailSize + height: Constants.thumbnailSize + baseState: true + defaultChecked: !statesEditorModel.baseState.modelHasDefaultState // TODO Make this one a model property + isChecked: root.currentStateInternalId === 0 + thumbnailImageSource: statesEditorModel.baseState.stateImageSource // TODO Get rid of the QVariantMap + isTiny: root.tinyMode + + onFocusSignal: root.currentStateInternalId = 0 + onDefaultClicked: statesEditorModel.resetDefaultState() + } + } + + Item { + id: scrollViewWrapper + width: root.isLandscape ? root.scrollViewWidth : root.width - (2 * root.padding) + height: root.isLandscape ? root.height - toolBar.height - (2 * root.padding) : root.scrollViewHeight + clip: true + + ScrollView { + id: scrollView + anchors.fill: parent + anchors.topMargin: root.topMargin + anchors.leftMargin: root.leftMargin + + ScrollBar.horizontal: StateScrollBar { + parent: scrollView + x: scrollView.leftPadding + y: scrollView.height - height + width: scrollView.availableWidth + active: scrollView.ScrollBar.vertical.active + orientation: Qt.Horizontal + } + + ScrollBar.vertical: StateScrollBar { + parent: scrollView + x: scrollView.mirrored ? 0 : scrollView.width - width + y: scrollView.topPadding + height: scrollView.availableHeight + active: scrollView.ScrollBar.horizontal.active + orientation: Qt.Vertical + } + + Flickable { + id: frame + boundsMovement: Flickable.StopAtBounds + boundsBehavior: Flickable.StopAtBounds + interactive: true + contentWidth: { + let ext = root.showExtendGroups ? (2 * root.extend) : 0 + return innerGrid.width + ext + } + contentHeight: { + let ext = root.showExtendGroups ? (2 * root.extend) : 0 + return innerGrid.height + ext + } + + Behavior on contentY { + NumberAnimation { + duration: 1000 + easing.type: Easing.InOutCubic + } + } + + Behavior on contentX { + NumberAnimation { + duration: 1000 + easing.type: Easing.InOutCubic + } + } + + Grid { + id: innerGrid + + x: root.showExtendGroups ? root.extend : 0 + y: root.showExtendGroups ? root.extend : 0 + + rows: 1 + spacing: root.innerGridSpacing + + move: Transition { + NumberAnimation { + properties: "x,y" + easing.type: Easing.OutQuad + } + } + + Repeater { + id: statesRepeater + + property int grabIndex: -1 + + function executeDrop(from, to) { + statesEditorModel.drop(from, to) + statesRepeater.grabIndex = -1 + } + + model: statesEditorModel + + onItemAdded: root.responsiveResize(root.width, root.height) + onItemRemoved: root.responsiveResize(root.width, root.height) + + delegate: DropArea { + id: delegateRoot + + required property int index + + required property string stateName + required property var stateImageSource + required property int internalNodeId + required property var hasWhenCondition + required property var whenConditionString + required property bool isDefault + required property var modelHasDefaultState + required property bool hasExtend + required property var extendString + + function setPropertyChangesVisible(value) { + stateThumbnail.propertyChangesVisible = value + } + + width: Constants.thumbnailSize + height: Constants.thumbnailSize + + visible: delegateRoot.internalNodeId // Skip base state + + property int visualIndex: index + + onEntered: function (drag) { + let dragSource = (drag.source as StateThumbnail) + + if (dragSource === undefined) + return + + if (dragSource.extendString !== stateThumbnail.extendString + || stateThumbnail.extendedState) { + return + } + + statesEditorModel.move( + (drag.source as StateThumbnail).visualIndex, + stateThumbnail.visualIndex) + } + + onDropped: function (drop) { + let dragSource = (drop.source as StateThumbnail) + + if (dragSource === undefined) + return + + if (dragSource.extendString !== stateThumbnail.extendString + || stateThumbnail.extendedState) { + return + } + + statesRepeater.executeDrop(statesRepeater.grabIndex, + stateThumbnail.visualIndex) + } + + // Extend Groups Visualization + Rectangle { + id: extendBackground + x: -root.extend + y: -root.extend + width: Constants.thumbnailSize + 2 * root.extend + height: Constants.thumbnailSize + 2 * root.extend + color: StudioTheme.Values.themeStateHighlight + + radius: { + if (root.nextStateHasExtend(delegateRoot.index)) + return delegateRoot.hasExtend ? 0 : root.extend + + return root.extend + } + + visible: (delegateRoot.hasExtend + || stateThumbnail.extendedState) + } + // Fill the gap between extend group states and also cover up radius + // of start and end states of an extend group in case of line break + Rectangle { + id: extendGap + property bool portraitOneColumn: !root.isLandscape + && innerGrid.columns === 1 + + property bool leftOrTop: { + if (delegateRoot.hasExtend) + return true + + if (root.previousStateHasExtend(delegateRoot.index)) + return true + + return false + } + property bool rightOrBottom: { + if (stateThumbnail.extendedState) + return true + + if (root.nextStateHasExtend(delegateRoot.index)) + return true + + return false + } + + property bool firstInRow: ((delegateRoot.index - 1) % innerGrid.columns) === 0 + property bool lastInRow: ((delegateRoot.index - 1) % innerGrid.columns) + === (innerGrid.columns - 1) + + x: { + if (!extendGap.portraitOneColumn) { + if (extendGap.rightOrBottom) + return extendGap.lastInRow ? Constants.thumbnailSize + - (root.innerGridSpacing + - root.extend) : Constants.thumbnailSize + if (extendGap.leftOrTop) + return extendGap.firstInRow ? -root.extend : -root.innerGridSpacing + + return 0 + } + + return -root.extend + } + y: { + if (extendGap.portraitOneColumn) { + if (extendGap.rightOrBottom) + return Constants.thumbnailSize + if (extendGap.leftOrTop) + return -root.innerGridSpacing + + return 0 + } + + return -root.extend + } + width: extendGap.portraitOneColumn ? Constants.thumbnailSize + 2 + * root.extend : root.innerGridSpacing + height: extendGap.portraitOneColumn ? root.innerGridSpacing : Constants.thumbnailSize + + 2 * root.extend + color: StudioTheme.Values.themeStateHighlight + visible: extendBackground.radius !== 0 + && extendBackground.visible + } + + StateThumbnail { + id: stateThumbnail + width: Constants.thumbnailSize + height: Constants.thumbnailSize + visualIndex: delegateRoot.visualIndex + internalNodeId: delegateRoot.internalNodeId + isTiny: root.tinyMode + + hasExtend: delegateRoot.hasExtend + extendString: delegateRoot.extendString + extendedState: statesEditorModel.extendedStates.includes( + delegateRoot.stateName) + + hasWhenCondition: delegateRoot.hasWhenCondition + + // Fix ScrollView taking over the dragging event + onGrabbing: { + frame.interactive = false + statesRepeater.grabIndex = stateThumbnail.visualIndex + } + onLetGo: frame.interactive = true + + // Fix for ScrollView clipping while dragging of StateThumbnail + onDragActiveChanged: { + if (stateThumbnail.dragActive) + parent = scrollViewWrapper + else + parent = delegateRoot + } + + stateName: delegateRoot.stateName + thumbnailImageSource: delegateRoot.stateImageSource + whenCondition: delegateRoot.whenConditionString + + baseState: !delegateRoot.internalNodeId + defaultChecked: delegateRoot.isDefault + isChecked: root.currentStateInternalId === delegateRoot.internalNodeId + + onFocusSignal: root.currentStateInternalId = delegateRoot.internalNodeId + onDefaultClicked: statesEditorModel.setStateAsDefault( + delegateRoot.internalNodeId) + + onClone: root.cloneState(delegateRoot.internalNodeId) + onExtend: root.extendState(delegateRoot.internalNodeId) + onRemove: root.deleteState(delegateRoot.internalNodeId) + + onStateNameFinished: statesEditorModel.renameState( + delegateRoot.internalNodeId, + stateThumbnail.stateName) + + onWhenConditionFinished: statesEditorModel.setWhenCondition( + delegateRoot.internalNodeId, + stateThumbnail.whenCondition) + } + } + } + } + } + } + } + + Item { + id: addWrapper + + Canvas { + id: addCanvas + width: root.thumbWidth + height: root.thumbHeight + + onPaint: { + var ctx = getContext("2d") + + ctx.strokeStyle = StudioTheme.Values.themeStateHighlight + ctx.lineWidth = 6 + + var plusExtend = 20 + var halfWidth = addCanvas.width / 2 + var halfHeight = addCanvas.height / 2 + + ctx.beginPath() + ctx.moveTo(halfWidth, halfHeight - plusExtend) + ctx.lineTo(halfWidth, halfHeight + plusExtend) + + ctx.moveTo(halfWidth - plusExtend, halfHeight) + ctx.lineTo(halfWidth + plusExtend, halfHeight) + ctx.stroke() + + ctx.save() + ctx.setLineDash([2, 2]) + ctx.strokeRect(0, 0, addCanvas.width, addCanvas.height) + ctx.restore() + } + + MouseArea { + id: addMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: root.createNewState() + } + + Rectangle { + // temporary hover indicator for add button + anchors.fill: parent + opacity: 0.1 + color: addMouseArea.containsMouse ? "#ffffff" : "#000000" + } + } + } + } +} diff --git a/share/qtcreator/qmldesigner/newstateseditor/MenuButton.qml b/share/qtcreator/qmldesigner/newstateseditor/MenuButton.qml new file mode 100644 index 00000000000..6eda4582122 --- /dev/null +++ b/share/qtcreator/qmldesigner/newstateseditor/MenuButton.qml @@ -0,0 +1,134 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +import QtQuick +import QtQuick.Controls +import StudioTheme 1.0 as StudioTheme +import StudioControls 1.0 as StudioControls + +Item { + id: root + width: 25 + height: 25 + + property bool hovered: mouseArea.containsMouse + property bool checked: false + signal pressed() + + Rectangle { + id: background + color: "transparent" + anchors.fill: parent + } + + // Burger menu icon + Column { + id: menuIcon + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + spacing: 3 + + property color iconColor: StudioTheme.Values.themeTextColor + + Rectangle { + id: rectangle + width: 19 + height: 3 + color: menuIcon.iconColor + } + + Rectangle { + id: rectangle1 + width: 19 + height: 3 + color: menuIcon.iconColor + } + + Rectangle { + id: rectangle2 + width: 19 + height: 3 + color: menuIcon.iconColor + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + onPressed: root.pressed() + } + + states: [ + State { + name: "default" + when: !root.hovered && !root.checked + + PropertyChanges { + target: background + color: "transparent" + } + PropertyChanges { + target: menuIcon + iconColor: StudioTheme.Values.themeTextColor + } + }, + State { + name: "hover" + when: root.hovered && !root.checked + + PropertyChanges { + target: background + color: StudioTheme.Values.themeControlBackgroundHover + } + PropertyChanges { + target: menuIcon + iconColor: StudioTheme.Values.themeTextColor + } + }, + State { + name: "checked" + when: !root.hovered && root.checked + + PropertyChanges { + target: menuIcon + iconColor: StudioTheme.Values.themeInteraction + } + }, + State { + name: "hoverChecked" + when: root.hovered && root.checked + + PropertyChanges { + target: background + color: StudioTheme.Values.themeControlBackgroundHover + } + PropertyChanges { + target: menuIcon + iconColor: StudioTheme.Values.themeInteraction + } + } + ] +} diff --git a/share/qtcreator/qmldesigner/newstateseditor/StateMenu.qml b/share/qtcreator/qmldesigner/newstateseditor/StateMenu.qml new file mode 100644 index 00000000000..3ecf13e0841 --- /dev/null +++ b/share/qtcreator/qmldesigner/newstateseditor/StateMenu.qml @@ -0,0 +1,108 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +import QtQuick +import QtQuick.Controls +import StudioTheme as StudioTheme +import StudioControls as StudioControls +import QtQuick.Layouts + +StudioControls.Menu { + id: root + + property bool isBaseState: false + property bool isTiny: false + property bool hasExtend: false + property bool propertyChangesVisible: false + property bool hasAnnotation: false + property bool hasWhenCondition: false + + signal clone() + signal extend() + signal remove() + signal toggle() + signal resetWhenCondition() + signal editAnnotation() + signal removeAnnotation() + + closePolicy: Popup.CloseOnReleaseOutside | Popup.CloseOnEscape + + StudioControls.MenuItem { + id: clone + visible: !root.isBaseState + text: qsTr("Clone") + height: clone.visible ? clone.implicitHeight : 0 + onTriggered: root.clone() + } + + StudioControls.MenuItem { + id: deleteState + visible: !root.isBaseState + text: qsTr("Delete") + height: deleteState.visible ? deleteState.implicitHeight : 0 + onTriggered: root.remove() + } + + StudioControls.MenuItem { + id: showChanges + visible: !root.isBaseState + enabled: !root.isTiny + text: root.propertyChangesVisible ? qsTr("Show Thumbnail") : qsTr("Show Changes") + height: showChanges.visible ? showChanges.implicitHeight : 0 + onTriggered: root.toggle() + } + + StudioControls.MenuItem { + id: extend + visible: !root.isBaseState + text: qsTr("Extend") + height: extend.visible ? extend.implicitHeight : 0 + onTriggered: root.extend() + } + + StudioControls.MenuSeparator {} + + StudioControls.MenuItem { + enabled: !root.isBaseState && root.hasWhenCondition + text: qsTr("Reset when Condition") + onTriggered: { + statesEditorModel.resetWhenCondition(internalNodeId) + } + } + + StudioControls.MenuSeparator {} + + StudioControls.MenuItem { + enabled: !root.isBaseState + text: root.hasAnnotation ? qsTr("Edit Annotation") : qsTr("Add Annotation") + onTriggered: root.editAnnotation() + } + + StudioControls.MenuItem { + enabled: !isBaseState && hasAnnotation + text: qsTr("Remove Annotation") + onTriggered: root.removeAnnotation() + } +} diff --git a/share/qtcreator/qmldesigner/newstateseditor/StateScrollBar.qml b/share/qtcreator/qmldesigner/newstateseditor/StateScrollBar.qml new file mode 100644 index 00000000000..68f766a10ee --- /dev/null +++ b/share/qtcreator/qmldesigner/newstateseditor/StateScrollBar.qml @@ -0,0 +1,66 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +import QtQuick +import QtQuick.Controls +import StudioTheme 1.0 as StudioTheme + +ScrollBar { + id: scrollBar + + contentItem: Rectangle { + implicitWidth: scrollBar.interactive ? 6 : 2 + implicitHeight: scrollBar.interactive ? 6 : 2 + radius: width / 2 + opacity: 0.0 + color: scrollBar.pressed ? StudioTheme.Values.themeSliderActiveTrackHover + : StudioTheme.Values.themeSliderHandle + + states: State { + name: "active" + when: scrollBar.policy === ScrollBar.AlwaysOn + || (scrollBar.active && scrollBar.size < 1.0) + PropertyChanges { + target: scrollBar.contentItem + opacity: 0.75 + } + } + + transitions: Transition { + from: "active" + SequentialAnimation { + PauseAnimation { + duration: 450 + } + NumberAnimation { + target: scrollBar.contentItem + duration: 200 + property: "opacity" + to: 0.0 + } + } + } + } +} diff --git a/share/qtcreator/qmldesigner/newstateseditor/StateThumbnail.qml b/share/qtcreator/qmldesigner/newstateseditor/StateThumbnail.qml new file mode 100644 index 00000000000..a5d20f85331 --- /dev/null +++ b/share/qtcreator/qmldesigner/newstateseditor/StateThumbnail.qml @@ -0,0 +1,746 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +import QtQuick +import QtQuick.Controls +import StudioTheme 1.0 as StudioTheme +import StudioControls 1.0 as StudioControls +import QtQuick.Layouts 6.0 + +Item { + id: root + clip: true + + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + + property alias thumbnailImageSource: thumbnailImage.source + property alias stateName: stateNameField.text + property alias whenCondition: whenCondition.text + + property alias defaultChecked: defaultButton.checked + property alias menuChecked: menuButton.checked + property bool baseState: false + property bool isTiny: false + property bool propertyChangesVisible: false + property bool isChecked: false + + property bool hasExtend: false + property string extendString: "" + property bool extendedState: false + + property bool hasWhenCondition: false + + property int visualIndex: 0 + + property int internalNodeId + + signal focusSignal + signal defaultClicked + signal clone + signal extend + signal remove + signal stateNameFinished + signal whenConditionFinished + + signal grabbing + signal letGo + + property alias dragActive: dragHandler.active + + function checkAnnotation() { + return statesEditorModel.hasAnnotation(root.internalNodeId) + } + + onIsTinyChanged: { + if (root.isTiny) { + buttonGrid.rows = 2 + buttonGrid.columns = 1 + } else { + buttonGrid.columns = 2 + buttonGrid.rows = 1 + } + } + + DragHandler { + id: dragHandler + enabled: !root.baseState && !root.extendedState + onGrabChanged: function (transition, point) { + if (transition === PointerDevice.GrabPassive + || transition === PointerDevice.GrabExclusive) + root.grabbing() + + if (transition === PointerDevice.UngrabPassive + || transition === PointerDevice.CancelGrabPassive + || transition === PointerDevice.UngrabExclusive + || transition === PointerDevice.CancelGrabExclusive) + root.letGo() + } + } + + onDragActiveChanged: { + if (root.dragActive) + Drag.start() + else + Drag.drop() + } + + Drag.active: dragHandler.active + Drag.source: root + Drag.hotSpot.x: root.width * 0.5 + Drag.hotSpot.y: root.height * 0.5 + + Rectangle { + id: stateBackground + color: StudioTheme.Values.themeControlBackground + border.color: StudioTheme.Values.themeInteraction + border.width: root.isChecked ? 4 : 0 + anchors.fill: parent + + readonly property int controlHeight: 25 + readonly property int thumbPadding: 10 + readonly property int thumbSpacing: 7 + + property int innerWidth: root.width - 2 * stateBackground.thumbPadding + property int innerHeight: root.height - 2 * stateBackground.thumbPadding - 2 + * stateBackground.thumbSpacing - 2 * stateBackground.controlHeight + + MouseArea { + id: mouseArea + anchors.fill: parent + enabled: true + hoverEnabled: true + propagateComposedEvents: true + onClicked: root.focusSignal() + } + + Column { + padding: stateBackground.thumbPadding + spacing: stateBackground.thumbSpacing + + Grid { + id: buttonGrid + columns: 2 + rows: 1 + spacing: stateBackground.thumbSpacing + + StudioControls.AbstractButton { + id: defaultButton + width: 50 + height: stateBackground.controlHeight + checkedInverted: true + buttonIcon: qsTr("Default") + iconFont: StudioTheme.Constants.font + onClicked: { + root.defaultClicked() + root.focusSignal() + } + } + + StudioControls.TextField { + id: stateNameField + + property string previousText + + // This is the width for the "big" state + property int bigWidth: stateBackground.innerWidth - 2 * stateBackground.thumbSpacing + - defaultButton.width - menuButton.width + + width: root.isTiny ? stateBackground.innerWidth : stateNameField.bigWidth + height: stateBackground.controlHeight + actionIndicatorVisible: false + translationIndicatorVisible: false + placeholderText: qsTr("State Name") + visible: !root.baseState + + onActiveFocusChanged: { + if (stateNameField.activeFocus) + root.focusSignal() + } + + onEditingFinished: { + if (stateNameField.previousText === stateNameField.text) + return + + stateNameField.previousText = stateNameField.text + root.stateNameFinished() + } + + Component.onCompleted: stateNameField.previousText = stateNameField.text + } + + Text { + id: baseStateLabel + width: root.isTiny ? stateBackground.innerWidth : stateNameField.bigWidth + height: stateBackground.controlHeight + visible: root.baseState + color: StudioTheme.Values.themeTextColor + text: qsTr("Base State") + font.pixelSize: StudioTheme.Values.baseFontSize + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + leftPadding: 5 + elide: Text.ElideRight + } + } + + Item { + visible: !root.isTiny && !root.propertyChangesVisible + width: stateBackground.innerWidth + height: stateBackground.innerHeight + + Image { + anchors.fill: stateImageBackground + source: "images/checkers.png" + fillMode: Image.Tile + } + + Rectangle { + id: stateImageBackground + x: Math.floor( + (stateBackground.innerWidth - thumbnailImage.paintedWidth) / 2) - StudioTheme.Values.border + y: Math.floor( + (stateBackground.innerHeight - thumbnailImage.paintedHeight) / 2) - StudioTheme.Values.border + width: Math.round(thumbnailImage.paintedWidth) + 2 * StudioTheme.Values.border + height: Math.round(thumbnailImage.paintedHeight) + 2 * StudioTheme.Values.border + color: "transparent" + border.width: StudioTheme.Values.border + border.color: StudioTheme.Values.themeStatePreviewOutline + } + + Image { + id: thumbnailImage + anchors.centerIn: parent + anchors.fill: parent + fillMode: Image.PreserveAspectFit + mipmap: true + } + } + + PropertyChangesModel { + id: propertyChangesModel + modelNodeBackendProperty: statesEditorModel.stateModelNode(root.internalNodeId) + } + + Text { + visible: !root.isTiny && root.propertyChangesVisible && !propertyChangesModel.count + width: stateBackground.innerWidth + height: stateBackground.innerHeight + text: qsTr("No Property Changes Available") + color: StudioTheme.Values.themeTextColor + font.pixelSize: StudioTheme.Values.baseFontSize + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + ScrollView { + id: scrollView + visible: !root.isTiny && root.propertyChangesVisible && propertyChangesModel.count + width: stateBackground.innerWidth + height: stateBackground.innerHeight + clip: true + + ScrollBar.horizontal: StateScrollBar { + parent: scrollView + x: scrollView.leftPadding + y: scrollView.height - height + width: scrollView.availableWidth + active: scrollView.ScrollBar.vertical.active + orientation: Qt.Horizontal + onPressedChanged: root.focusSignal() + } + + ScrollBar.vertical: StateScrollBar { + parent: scrollView + x: scrollView.mirrored ? 0 : scrollView.width - width + y: scrollView.topPadding + height: scrollView.availableHeight + active: scrollView.ScrollBar.horizontal.active + orientation: Qt.Vertical + onPressedChanged: root.focusSignal() + } + + Flickable { + boundsMovement: Flickable.StopAtBounds + boundsBehavior: Flickable.StopAtBounds + interactive: true + contentWidth: column.width + contentHeight: column.height + + // ScrollView needs an extra TapHandler on top in order to receive click + // events. MouseAreas below ScrollView do not let clicks through. + TapHandler { + id: tapHandler + onTapped: root.focusSignal() + } + + Column { + id: column + + // Grid sizes + property int gridSpacing: 20 + property int gridRowSpacing: 5 + property int gridPadding: 5 + property int col1Width: 100 // labels + property int col2Width: stateBackground.innerWidth - column.gridSpacing - 2 + * column.gridPadding - column.col1Width // controls + + width: stateBackground.innerWidth + spacing: stateBackground.thumbSpacing + + Repeater { + model: propertyChangesModel + + delegate: Rectangle { + id: propertyChanges + + required property int index + + required property string target + required property bool explicit + required property bool restoreEntryValues + required property var propertyModelNode + + width: column.width + height: propertyChangesColumn.height + color: StudioTheme.Values.themeBackgroundColorAlternate + + PropertyModel { + id: propertyModel + modelNodeBackendProperty: propertyChanges.propertyModelNode + } + + Column { + id: propertyChangesColumn + + Item { + id: section + property int animationDuration: 120 + property bool expanded: false + + clip: true + width: stateBackground.innerWidth + height: Math.round(sectionColumn.height + header.height) + + Rectangle { + id: header + anchors.left: parent.left + anchors.right: parent.right + width: stateBackground.innerWidth + height: StudioTheme.Values.sectionHeadHeight + color: StudioTheme.Values.themeSectionHeadBackground + + Row { + x: column.gridPadding + anchors.verticalCenter: parent.verticalCenter + spacing: column.gridSpacing + + Text { + color: StudioTheme.Values.themeTextColor + text: qsTr("Target") + font.pixelSize: StudioTheme.Values.baseFontSize + horizontalAlignment: Text.AlignRight + width: column.col1Width + } + + Text { + color: StudioTheme.Values.themeTextColor + text: propertyChanges.target + font.pixelSize: StudioTheme.Values.baseFontSize + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + width: column.col2Width + } + } + + Text { + id: arrow + width: StudioTheme.Values.spinControlIconSizeMulti + height: StudioTheme.Values.spinControlIconSizeMulti + text: StudioTheme.Constants.sectionToggle + color: StudioTheme.Values.themeTextColor + renderType: Text.NativeRendering + anchors.left: parent.left + anchors.leftMargin: 4 + anchors.verticalCenter: parent.verticalCenter + font.pixelSize: StudioTheme.Values.spinControlIconSizeMulti + font.family: StudioTheme.Constants.iconFont.family + + Behavior on rotation { + NumberAnimation { + easing.type: Easing.OutCubic + duration: section.animationDuration + } + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + section.expanded = !section.expanded + if (!section.expanded) + section.forceActiveFocus() + root.focusSignal() + } + } + } + + Column { + id: sectionColumn + y: header.height + padding: column.gridPadding + spacing: column.gridRowSpacing + + Row { + spacing: column.gridSpacing + + Text { + color: StudioTheme.Values.themeTextColor + text: qsTr("Explicit") + font.pixelSize: StudioTheme.Values.baseFontSize + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + width: column.col1Width + height: explicitSwitch.height + } + + StudioControls.Switch { + id: explicitSwitch + actionIndicatorVisible: false + checked: propertyChanges.explicit + onToggled: { + root.focusSignal() + propertyModel.setExplicit( + explicitSwitch.checked) + } + } + } + + Row { + spacing: column.gridSpacing + + Text { + color: StudioTheme.Values.themeTextColor + text: qsTr("Restore values") + font.pixelSize: StudioTheme.Values.baseFontSize + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + width: column.col1Width + height: restoreSwitch.height + } + + StudioControls.Switch { + id: restoreSwitch + actionIndicatorVisible: false + checked: propertyChanges.restoreEntryValues + onToggled: { + root.focusSignal() + propertyModel.setRestoreEntryValues( + restoreSwitch.checked) + } + } + } + } + + Behavior on height { + NumberAnimation { + easing.type: Easing.OutCubic + duration: section.animationDuration + } + } + + states: [ + State { + name: "Expanded" + when: section.expanded + PropertyChanges { + target: arrow + rotation: 0 + } + }, + State { + name: "Collapsed" + when: !section.expanded + PropertyChanges { + target: section + height: header.height + } + PropertyChanges { + target: arrow + rotation: -90 + } + } + ] + } + + Column { + id: propertyColumn + padding: column.gridPadding + spacing: column.gridRowSpacing + + Repeater { + model: propertyModel + + delegate: ItemDelegate { + id: propertyDelegate + + required property string name + required property var value + required property string type + + width: stateBackground.innerWidth - 2 * column.gridPadding + height: 26 + hoverEnabled: true + + onClicked: root.focusSignal() + + background: Rectangle { + color: "transparent" + border.color: StudioTheme.Values.themeInteraction + border.width: propertyDelegate.hovered ? StudioTheme.Values.border : 0 + } + + Item { + id: removeItem + visible: propertyDelegate.hovered + x: propertyDelegate.width - removeItem.width + z: 10 + width: removeItem.visible ? propertyDelegate.height : 0 + height: removeItem.visible ? propertyDelegate.height : 0 + + Label { + id: removeIcon + anchors.fill: parent + text: StudioTheme.Constants.closeCross + color: StudioTheme.Values.themeTextColor + font.family: StudioTheme.Constants.iconFont.family + font.pixelSize: StudioTheme.Values.myIconFontSize + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + scale: propertyDelegateMouseArea.containsMouse ? 1.2 : 1.0 + } + + MouseArea { + id: propertyDelegateMouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + root.focusSignal() + propertyModel.removeProperty( + propertyDelegate.name) + } + } + } + + Row { + anchors.verticalCenter: parent.verticalCenter + spacing: column.gridSpacing + + Text { + color: StudioTheme.Values.themeTextColor + text: propertyDelegate.name + font.pixelSize: StudioTheme.Values.baseFontSize + horizontalAlignment: Text.AlignRight + elide: Text.ElideRight + width: column.col1Width + } + + Text { + color: StudioTheme.Values.themeTextColor + text: propertyDelegate.value + font.pixelSize: StudioTheme.Values.baseFontSize + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + width: column.col2Width - removeItem.width + } + } + } + } + } + } + } + } + } + } + } + + BindingEditor { + id: bindingEditor + + property string newWhenCondition + + property Timer timer: Timer { + id: timer + running: false + interval: 50 + repeat: false + onTriggered: statesEditorModel.setWhenCondition(root.internalNodeId, + bindingEditor.newWhenCondition) + } + + stateModelNodeProperty: statesEditorModel.stateModelNode(root.internalNodeId) + stateNameProperty: root.stateName + + onRejected: bindingEditor.hideWidget() + onAccepted: { + bindingEditor.newWhenCondition = bindingEditor.text.trim() + timer.start() + bindingEditor.hideWidget() + } + } + + StudioControls.TextField { + id: whenCondition + + property string previousCondition + + width: stateBackground.innerWidth + height: stateBackground.controlHeight + + visible: !root.baseState + indicatorVisible: true + indicator.icon.text: StudioTheme.Constants.edit + indicator.onClicked: { + bindingEditor.showWidget() + bindingEditor.text = whenCondition.text + bindingEditor.prepareBindings() + bindingEditor.updateWindowName() + } + + actionIndicatorVisible: false + translationIndicatorVisible: false + placeholderText: qsTr("When Condition") + + onActiveFocusChanged: { + if (whenCondition.activeFocus) + root.focusSignal() + } + + onEditingFinished: { + if (whenCondition.previousCondition === whenCondition.text) + return + + whenCondition.previousCondition = whenCondition.text + root.whenConditionFinished() + } + + Component.onCompleted: whenCondition.previousCondition = whenCondition.text + } + } + + MenuButton { + id: menuButton + anchors.top: parent.top + anchors.topMargin: 10 + anchors.right: parent.right + anchors.rightMargin: 10 + visible: !root.baseState + checked: stateMenu.opened + + onPressed: { + if (!stateMenu.opened) + stateMenu.popup() + + root.focusSignal() + } + } + } + + StateMenu { + id: stateMenu + x: 56 + + isBaseState: root.baseState + isTiny: root.isTiny + hasExtend: root.hasExtend + propertyChangesVisible: root.propertyChangesVisible + hasAnnotation: root.checkAnnotation() + hasWhenCondition: root.hasWhenCondition + + onClone: root.clone() + onExtend: root.extend() + onRemove: root.remove() + onToggle: root.propertyChangesVisible = !root.propertyChangesVisible + onEditAnnotation: { + statesEditorModel.setAnnotation(root.internalNodeId) + stateMenu.hasAnnotation = root.checkAnnotation() + } + onRemoveAnnotation: { + statesEditorModel.removeAnnotation(root.internalNodeId) + stateMenu.hasAnnotation = root.checkAnnotation() + } + + onOpened: stateMenu.hasAnnotation = root.checkAnnotation() + } + + property bool anyControlHovered: defaultButton.hovered || menuButton.hovered + || scrollView.hovered || stateNameField.hover + || whenCondition.hover + + states: [ + State { + name: "default" + when: !mouseArea.containsMouse && !root.anyControlHovered && !dragHandler.active + && !root.baseState + + PropertyChanges { + target: stateBackground + color: StudioTheme.Values.themeControlBackground + } + }, + State { + name: "hover" + when: (mouseArea.containsMouse || root.anyControlHovered) && !dragHandler.active + + PropertyChanges { + target: stateBackground + color: StudioTheme.Values.themeControlBackgroundHover + } + }, + State { + name: "baseState" + when: root.baseState && !mouseArea.containsMouse && !root.anyControlHovered + && !dragHandler.active + + PropertyChanges { + target: stateBackground + color: StudioTheme.Values.themeStateHighlight + } + }, + State { + name: "drag" + when: dragHandler.active + + AnchorChanges { + target: root + anchors.horizontalCenter: undefined + anchors.verticalCenter: undefined + } + } + ] +} diff --git a/share/qtcreator/qmldesigner/newstateseditor/images/checkers.png b/share/qtcreator/qmldesigner/newstateseditor/images/checkers.png new file mode 100644 index 0000000000000000000000000000000000000000..72cb9f0350646967cbd0bdc5c69981796f43a2dd GIT binary patch literal 80 zcmeAS@N?(olHy`uVBq!ia0y~yVBi5^4h9AWhGIEpYX$}eaZeY=5RT~N3Kq8i2OAob f_*$Ac0?#lqSbc8ylogof3sUOo>gTe~DWM4fJ|+>W literal 0 HcmV?d00001 diff --git a/share/qtcreator/qmldesigner/newstateseditor/imports/StatesEditor/Constants.qml b/share/qtcreator/qmldesigner/newstateseditor/imports/StatesEditor/Constants.qml new file mode 100644 index 00000000000..c08adcf6360 --- /dev/null +++ b/share/qtcreator/qmldesigner/newstateseditor/imports/StatesEditor/Constants.qml @@ -0,0 +1,39 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +pragma Singleton +import QtQuick + +QtObject { + readonly property int width: 1920 + readonly property int height: 1080 + + property int thumbnailSize: 250 + // Breakpoint to control when the state thumbnail view toggles from normal to tiny + readonly property int thumbnailBreak: 150 + + readonly property int minThumbSize: 100 + readonly property int maxThumbSize: 350 +} diff --git a/share/qtcreator/qmldesigner/newstateseditor/imports/StatesEditor/qmldir b/share/qtcreator/qmldesigner/newstateseditor/imports/StatesEditor/qmldir new file mode 100644 index 00000000000..616ac203530 --- /dev/null +++ b/share/qtcreator/qmldesigner/newstateseditor/imports/StatesEditor/qmldir @@ -0,0 +1 @@ +singleton Constants 1.0 Constants.qml diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/AbstractButton.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/AbstractButton.qml index c594220d5e0..5c64caa35f3 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/AbstractButton.qml +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/AbstractButton.qml @@ -43,6 +43,9 @@ T.AbstractButton { property alias backgroundVisible: buttonBackground.visible property alias backgroundRadius: buttonBackground.radius + // Inverts the checked style + property bool checkedInverted: false + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, implicitContentWidth + leftPadding + rightPadding) implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, @@ -98,11 +101,12 @@ T.AbstractButton { } }, State { - name: "select" + name: "check" when: myButton.enabled && !myButton.pressed && myButton.checked PropertyChanges { target: buttonIcon - color: StudioTheme.Values.themeIconColorSelected + color: myButton.checkedInverted ? StudioTheme.Values.themeTextSelectedTextColor + : StudioTheme.Values.themeIconColorSelected } }, State { @@ -125,6 +129,7 @@ T.AbstractButton { PropertyChanges { target: buttonBackground color: StudioTheme.Values.themeControlBackground + border.color: StudioTheme.Values.themeControlOutline } PropertyChanges { target: myButton @@ -137,19 +142,32 @@ T.AbstractButton { PropertyChanges { target: buttonBackground color: StudioTheme.Values.themeControlBackground + border.color: StudioTheme.Values.themeControlOutline } }, State { name: "hover" - when: myButton.hover && !myButton.pressed && myButton.enabled + when: !myButton.checked && myButton.hover && !myButton.pressed && myButton.enabled PropertyChanges { target: buttonBackground color: StudioTheme.Values.themeControlBackgroundHover + border.color: StudioTheme.Values.themeControlOutline + } + }, + State { + name: "hoverCheck" + when: myButton.checked && myButton.hover && !myButton.pressed && myButton.enabled + PropertyChanges { + target: buttonBackground + color: myButton.checkedInverted ? StudioTheme.Values.themeInteractionHover + : StudioTheme.Values.themeControlBackgroundHover + border.color: myButton.checkedInverted ? StudioTheme.Values.themeInteractionHover + : StudioTheme.Values.themeControlOutline } }, State { name: "press" - when: myButton.hover && myButton.pressed + when: myButton.hover && myButton.pressed && myButton.enabled PropertyChanges { target: buttonBackground color: StudioTheme.Values.themeInteraction @@ -160,6 +178,17 @@ T.AbstractButton { z: 100 } }, + State { + name: "check" + when: myButton.enabled && !myButton.pressed && myButton.checked + PropertyChanges { + target: buttonBackground + color: myButton.checkedInverted ? StudioTheme.Values.themeInteraction + : StudioTheme.Values.themeControlBackground + border.color: myButton.checkedInverted ? StudioTheme.Values.themeInteraction + : StudioTheme.Values.themeControlOutline + } + }, State { name: "disable" when: !myButton.enabled diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/Dialog.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/Dialog.qml new file mode 100644 index 00000000000..c90409e5dee --- /dev/null +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/Dialog.qml @@ -0,0 +1,75 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +import QtQuick +import QtQuick.Controls as C +import QtQuick.Templates as T +import StudioTheme 1.0 as StudioTheme + +T.Dialog { + id: root + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + contentWidth + leftPadding + rightPadding, + implicitHeaderWidth, + implicitFooterWidth) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + contentHeight + topPadding + bottomPadding + + (implicitHeaderHeight > 0 ? implicitHeaderHeight + spacing : 0) + + (implicitFooterHeight > 0 ? implicitFooterHeight + spacing : 0)) + + padding: StudioTheme.Values.dialogPadding + + background: Rectangle { + color: StudioTheme.Values.themeDialogBackground + border.color: StudioTheme.Values.themeDialogOutline + border.width: StudioTheme.Values.border + } + + header: T.Label { + text: root.title + visible: root.title + elide: T.Label.ElideRight + font.bold: true + padding: StudioTheme.Values.dialogPadding + color: StudioTheme.Values.themeTextColor + + background: Rectangle { + x: StudioTheme.Values.border + y: StudioTheme.Values.border + width: parent.width - (2 * StudioTheme.Values.border) + height: parent.height - (2 * StudioTheme.Values.border) + color: StudioTheme.Values.themeDialogBackground + } + } + + footer: DialogButtonBox { + visible: count > 0 + } + + T.Overlay.modal: Rectangle { + color: Qt.alpha(StudioTheme.Values.themeDialogBackground, 0.5) + } +} diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/DialogButton.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/DialogButton.qml new file mode 100644 index 00000000000..6f0c33264fc --- /dev/null +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/DialogButton.qml @@ -0,0 +1,105 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +import QtQuick +import QtQuick.Templates as T +import StudioTheme 1.0 as StudioTheme + +T.Button { + id: root + + implicitWidth: Math.max( + background ? background.implicitWidth : 0, + textItem.implicitWidth + leftPadding + rightPadding) + implicitHeight: Math.max( + background ? background.implicitHeight : 0, + textItem.implicitHeight + topPadding + bottomPadding) + leftPadding: StudioTheme.Values.dialogButtonPadding + rightPadding: StudioTheme.Values.dialogButtonPadding + + background: Rectangle { + id: background + implicitWidth: 70 + implicitHeight: 20 + color: StudioTheme.Values.themeControlBackground + border.color: StudioTheme.Values.themeControlOutline + anchors.fill: parent + } + + contentItem: Text { + id: textItem + text: root.text + font.pixelSize: StudioTheme.Values.baseFontSize + color: StudioTheme.Values.themeTextColor + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + states: [ + State { + name: "default" + when: !root.down && !root.hovered && !root.checked + + PropertyChanges { + target: background + color: root.highlighted ? StudioTheme.Values.themeInteraction + : StudioTheme.Values.themeControlBackground + border.color: StudioTheme.Values.themeControlOutline + } + PropertyChanges { + target: textItem + color: StudioTheme.Values.themeTextColor + } + }, + State { + name: "hover" + when: root.hovered && !root.checked && !root.down + + PropertyChanges { + target: background + color: StudioTheme.Values.themeControlBackgroundHover + border.color: StudioTheme.Values.themeControlOutline + } + PropertyChanges { + target: textItem + color: StudioTheme.Values.themeTextColor + } + }, + State { + name: "pressed" + when: root.checked || root.down + + PropertyChanges { + target: background + color: StudioTheme.Values.themeControlBackgroundInteraction + border.color: StudioTheme.Values.themeControlOutlineInteraction + } + PropertyChanges { + target: textItem + color: StudioTheme.Values.themeTextColor + } + } + ] +} diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/DialogButtonBox.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/DialogButtonBox.qml new file mode 100644 index 00000000000..fc313a66099 --- /dev/null +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/DialogButtonBox.qml @@ -0,0 +1,69 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +import QtQuick +import QtQuick.Controls as C +import QtQuick.Templates as T +import StudioTheme 1.0 as StudioTheme + +T.DialogButtonBox { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + (control.count === 1 ? implicitContentWidth * 2 : implicitContentWidth) + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding) + contentWidth: contentItem.contentWidth + + spacing: StudioTheme.Values.dialogButtonSpacing + padding: StudioTheme.Values.dialogPadding + alignment: Qt.AlignRight | Qt.AlignBottom + + + delegate: DialogButton { + width: control.count === 1 ? control.availableWidth / 2 : undefined + implicitHeight: StudioTheme.Values.height + highlighted: DialogButtonBox.buttonRole === DialogButtonBox.AcceptRole + || DialogButtonBox.buttonRole === DialogButtonBox.ApplyRole + } + + contentItem: ListView { + implicitWidth: contentWidth + model: control.contentModel + spacing: control.spacing + orientation: ListView.Horizontal + boundsBehavior: Flickable.StopAtBounds + snapMode: ListView.SnapToItem + } + + background: Rectangle { + implicitHeight: 30 + x: StudioTheme.Values.border + y: StudioTheme.Values.border + width: parent.width - (2 * StudioTheme.Values.border) + height: parent.height - (2 * StudioTheme.Values.border) + color: StudioTheme.Values.themeDialogBackground + } +} diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/Indicator.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/Indicator.qml new file mode 100644 index 00000000000..cc5cf43b869 --- /dev/null +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/Indicator.qml @@ -0,0 +1,80 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +import QtQuick 2.15 +import QtQuick.Templates 2.15 as T +import StudioTheme 1.0 as StudioTheme + +Item { + id: root + + property alias icon: icon + property bool hover: mouseArea.containsMouse + property bool pressed: mouseArea.pressed + + implicitWidth: StudioTheme.Values.height + implicitHeight: StudioTheme.Values.height + + signal clicked + z: 10 + + T.Label { + id: icon + anchors.fill: parent + text: StudioTheme.Constants.actionIcon + color: StudioTheme.Values.themeTextColor + font.family: StudioTheme.Constants.iconFont.family + font.pixelSize: StudioTheme.Values.myIconFontSize + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + states: [ + State { + name: "hover" + when: root.hover && !root.pressed && root.enabled + PropertyChanges { + target: icon + scale: 1.2 + visible: true + } + }, + State { + name: "disable" + when: !root.enabled + PropertyChanges { + target: icon + color: StudioTheme.Values.themeTextColorDisabled + } + } + ] + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: root.clicked() + } +} diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/TextField.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/TextField.qml index 11a6106f523..702d80ab8a1 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/TextField.qml +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/TextField.qml @@ -30,22 +30,24 @@ import StudioTheme 1.0 as StudioTheme T.TextField { id: root - property alias actionIndicator: actionIndicator - property alias translationIndicator: translationIndicator - // This property is used to indicate the global hover state - property bool hover: (actionIndicator.hover || mouseArea.containsMouse + property bool hover: (actionIndicator.hover || mouseArea.containsMouse || indicator.hover || translationIndicator.hover) && root.enabled property bool edit: root.activeFocus + property alias actionIndicator: actionIndicator property alias actionIndicatorVisible: actionIndicator.visible property real __actionIndicatorWidth: StudioTheme.Values.actionIndicatorWidth property real __actionIndicatorHeight: StudioTheme.Values.actionIndicatorHeight + property alias translationIndicator: translationIndicator property alias translationIndicatorVisible: translationIndicator.visible property real __translationIndicatorWidth: StudioTheme.Values.translationIndicatorWidth property real __translationIndicatorHeight: StudioTheme.Values.translationIndicatorHeight + property alias indicator: indicator + property alias indicatorVisible: indicator.visible + property string preFocusText: "" horizontalAlignment: Qt.AlignLeft @@ -68,7 +70,7 @@ T.TextField { implicitHeight: StudioTheme.Values.defaultControlHeight leftPadding: StudioTheme.Values.inputHorizontalPadding + actionIndicator.width - rightPadding: StudioTheme.Values.inputHorizontalPadding + translationIndicator.width + rightPadding: StudioTheme.Values.inputHorizontalPadding + translationIndicator.width + indicator.width MouseArea { id: mouseArea @@ -144,6 +146,14 @@ T.TextField { height: root.height } + Indicator { + id: indicator + visible: false + x: root.width - translationIndicator.width - indicator.width + width: indicator.visible ? root.height : 0 + height: indicator.visible ? root.height : 0 + } + TranslationIndicator { id: translationIndicator myControl: root @@ -173,8 +183,8 @@ T.TextField { }, State { name: "globalHover" - when: (actionIndicator.hover || translationIndicator.hover) && !root.edit - && root.enabled + when: (actionIndicator.hover || translationIndicator.hover || indicator.hover) + && !root.edit && root.enabled PropertyChanges { target: textFieldBackground color: StudioTheme.Values.themeControlBackgroundGlobalHover @@ -189,7 +199,7 @@ T.TextField { State { name: "hover" when: mouseArea.containsMouse && !actionIndicator.hover && !translationIndicator.hover - && !root.edit && root.enabled + && !indicator.hover && !root.edit && root.enabled PropertyChanges { target: textFieldBackground color: StudioTheme.Values.themeControlBackgroundHover diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/qmldir b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/qmldir index 25be4d0acf2..dbc558ca105 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/qmldir +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/qmldir @@ -8,7 +8,11 @@ CheckIndicator 1.0 CheckIndicator.qml ComboBox 1.0 ComboBox.qml ComboBoxInput 1.0 ComboBoxInput.qml ContextMenu 1.0 ContextMenu.qml +Dialog 1.0 Dialog.qml +DialogButton 1.0 DialogButton.qml +DialogButtonBox 1.0 DialogButtonBox.qml FilterComboBox 1.0 FilterComboBox.qml +Indicator 1.0 Indicator.qml InfinityLoopIndicator 1.0 InfinityLoopIndicator.qml ItemDelegate 1.0 ItemDelegate.qml LinkIndicator2D 1.0 LinkIndicator2D.qml diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioTheme/Values.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioTheme/Values.qml index d9bf43db746..961781e5e50 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioTheme/Values.qml +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioTheme/Values.qml @@ -216,6 +216,15 @@ QtObject { property real colorEditorPopupCmoboBoxWidth: 110 property real colorEditorPopupSpinBoxWidth: 54 + // Toolbar + property real toolbarHeight: 35 + property real toolbarSpacing: 8 + + // Dialog + property real dialogPadding: 12 + property real dialogButtonSpacing: 10 + property real dialogButtonPadding: 4 + // Theme Colors property bool isLightTheme: themeControlBackground.hsvValue > themeTextColor.hsvValue @@ -288,7 +297,7 @@ QtObject { // Slider colors property string themeSliderActiveTrack: Theme.color(Theme.DSsliderActiveTrack) - property string themeSliderActiveTrackHover: Theme.color(Theme.DSactiveTrackHover) + property string themeSliderActiveTrackHover: Theme.color(Theme.DSsliderActiveTrackHover) property string themeSliderActiveTrackFocus: Theme.color(Theme.DSsliderActiveTrackFocus) property string themeSliderInactiveTrack: Theme.color(Theme.DSsliderInactiveTrack) property string themeSliderInactiveTrackHover: Theme.color(Theme.DSsliderInactiveTrackHover) @@ -308,10 +317,15 @@ QtObject { property string themeTabInactiveBackground: Theme.color(Theme.DStabInactiveBackground) property string themeTabInactiveText: Theme.color(Theme.DStabInactiveText) + // State Editor property string themeStateSeparator: Theme.color(Theme.DSstateSeparatorColor) property string themeStateBackground: Theme.color(Theme.DSstateBackgroundColor) property string themeStatePreviewOutline: Theme.color(Theme.DSstatePreviewOutline) + // State Editor *new* + property color themeStatePanelBackground: Theme.color(Theme.DSstatePanelBackground) + property color themeStateHighlight: Theme.color(Theme.DSstateHighlight) + property string themeUnimportedModuleColor: Theme.color(Theme.DSUnimportedModuleColor) // Taken out of Constants.js @@ -334,9 +348,13 @@ QtObject { property string themeListItemTextHover: Theme.color(Theme.DSnavigatorTextHover) property string themeListItemTextPress: Theme.color(Theme.DSnavigatorTextSelected) - //Welcome Page + // Welcome Page property string welcomeScreenBackground: Theme.color(Theme.DSwelcomeScreenBackground) property string themeSubPanelBackground: Theme.color(Theme.DSsubPanelBackground) property string themeThumbnailBackground: Theme.color(Theme.DSthumbnailBackground) property string themeThumbnailLabelBackground: Theme.color(Theme.DSthumbnailLabelBackground) + + // Dialog + property color themeDialogBackground: values.themeThumbnailBackground + property color themeDialogOutline: values.themeInteraction } diff --git a/share/qtcreator/themes/dark.creatortheme b/share/qtcreator/themes/dark.creatortheme index 8ee97c0042c..7b124ca0d67 100644 --- a/share/qtcreator/themes/dark.creatortheme +++ b/share/qtcreator/themes/dark.creatortheme @@ -94,6 +94,9 @@ DSstateSeparatorColor=ff7c7b7b DSstateBackgroundColor=ff383838 DSstatePreviewOutline=ffaaaaaa +DSstatePanelBackground=ff252525 +DSstateHighlight=ff727272 + DSchangedStateText=ff99ccff DS3DAxisXColor=ffd00000 diff --git a/share/qtcreator/themes/default.creatortheme b/share/qtcreator/themes/default.creatortheme index 316d68df085..190edd514af 100644 --- a/share/qtcreator/themes/default.creatortheme +++ b/share/qtcreator/themes/default.creatortheme @@ -85,6 +85,9 @@ DSstateSeparatorColor=ffadadad DSstateBackgroundColor=ffe0e0e0 DSstatePreviewOutline=ff363636 +DSstatePanelBackground=ffdadada +DSstateHighlight=ff8d8d8d + DSchangedStateText=ff99ccff DS3DAxisXColor=ffd00000 diff --git a/share/qtcreator/themes/design-light.creatortheme b/share/qtcreator/themes/design-light.creatortheme index b5e54f2a34b..87ead1921a6 100644 --- a/share/qtcreator/themes/design-light.creatortheme +++ b/share/qtcreator/themes/design-light.creatortheme @@ -99,6 +99,9 @@ DSstateSeparatorColor=ffadadad DSstateBackgroundColor=ffe0e0e0 DSstatePreviewOutline=ff363636 +DSstatePanelBackground=ffdadada +DSstateHighlight=ff8d8d8d + DSchangedStateText=ff99ccff DS3DAxisXColor=ffd00000 diff --git a/share/qtcreator/themes/design.creatortheme b/share/qtcreator/themes/design.creatortheme index 613d89de6b6..52132b189df 100644 --- a/share/qtcreator/themes/design.creatortheme +++ b/share/qtcreator/themes/design.creatortheme @@ -96,6 +96,9 @@ DSstateSeparatorColor=ff7c7b7b DSstateBackgroundColor=ff383838 DSstatePreviewOutline=ffaaaaaa +DSstatePanelBackground=ff252525 +DSstateHighlight=ff727272 + DSchangedStateText=ff99ccff DS3DAxisXColor=ffd00000 diff --git a/share/qtcreator/themes/flat-dark.creatortheme b/share/qtcreator/themes/flat-dark.creatortheme index de26713cf55..e67e0e1b9f7 100644 --- a/share/qtcreator/themes/flat-dark.creatortheme +++ b/share/qtcreator/themes/flat-dark.creatortheme @@ -98,6 +98,9 @@ DSstateSeparatorColor=ff7c7b7b DSstateBackgroundColor=ff383838 DSstatePreviewOutline=ffaaaaaa +DSstatePanelBackground=ff252525 +DSstateHighlight=ff727272 + DSchangedStateText=ff99ccff DS3DAxisXColor=ffd00000 diff --git a/share/qtcreator/themes/flat-light.creatortheme b/share/qtcreator/themes/flat-light.creatortheme index e0a79eae1e5..5cd055f6530 100644 --- a/share/qtcreator/themes/flat-light.creatortheme +++ b/share/qtcreator/themes/flat-light.creatortheme @@ -94,6 +94,9 @@ DSstateSeparatorColor=ffadadad DSstateBackgroundColor=ffe0e0e0 DSstatePreviewOutline=ff363636 +DSstatePanelBackground=ffdadada +DSstateHighlight=ff8d8d8d + DSchangedStateText=ff99ccff DS3DAxisXColor=ffd00000 diff --git a/share/qtcreator/themes/flat.creatortheme b/share/qtcreator/themes/flat.creatortheme index 0e93ef19c01..e1006048791 100644 --- a/share/qtcreator/themes/flat.creatortheme +++ b/share/qtcreator/themes/flat.creatortheme @@ -92,6 +92,9 @@ DSstateSeparatorColor=ff7c7b7b DSstateBackgroundColor=ff383838 DSstatePreviewOutline=ffaaaaaa +DSstatePanelBackground=ff252525 +DSstateHighlight=ff727272 + DSchangedStateText=ff99ccff DS3DAxisXColor=ffd00000 diff --git a/src/libs/utils/theme/theme.h b/src/libs/utils/theme/theme.h index cbcf3658577..a22946c6027 100644 --- a/src/libs/utils/theme/theme.h +++ b/src/libs/utils/theme/theme.h @@ -438,6 +438,9 @@ public: DSgreenLight, DSamberLight, DSredLight, + + DSstatePanelBackground, + DSstateHighlight, }; enum Gradient { diff --git a/src/plugins/qmldesigner/CMakeLists.txt b/src/plugins/qmldesigner/CMakeLists.txt index 41ec84aa8b7..bcd959cf0d8 100644 --- a/src/plugins/qmldesigner/CMakeLists.txt +++ b/src/plugins/qmldesigner/CMakeLists.txt @@ -353,6 +353,17 @@ extend_qtc_plugin(QmlDesigner stateseditorwidget.cpp stateseditorwidget.h ) +extend_qtc_plugin(QmlDesigner + SOURCES_PREFIX components/stateseditornew + SOURCES + propertychangesmodel.cpp propertychangesmodel.h + propertymodel.cpp propertymodel.h + stateseditorimageprovider.cpp stateseditorimageprovider.h + stateseditormodel.cpp stateseditormodel.h + stateseditorview.cpp stateseditorview.h + stateseditorwidget.cpp stateseditorwidget.h +) + extend_qtc_plugin(QmlDesigner SOURCES_PREFIX components/texteditor SOURCES diff --git a/src/plugins/qmldesigner/components/propertyeditor/quick2propertyeditorview.cpp b/src/plugins/qmldesigner/components/propertyeditor/quick2propertyeditorview.cpp index 9523617637a..968d384ed0c 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/quick2propertyeditorview.cpp +++ b/src/plugins/qmldesigner/components/propertyeditor/quick2propertyeditorview.cpp @@ -36,10 +36,12 @@ #include "gradientpresetcustomlistmodel.h" #include "gradientpresetdefaultlistmodel.h" #include "itemfiltermodel.h" +#include "propertychangesmodel.h" #include "propertyeditorcontextobject.h" #include "propertyeditorimageprovider.h" #include "propertyeditorqmlbackend.h" #include "propertyeditorvalue.h" +#include "propertymodel.h" #include "qmlanchorbindingproxy.h" #include "richtexteditor/richtexteditorproxy.h" #include "theme.h" @@ -78,6 +80,8 @@ void Quick2PropertyEditorView::registerQmlTypes() RichTextEditorProxy::registerDeclarativeType(); SelectionDynamicPropertiesProxyModel::registerDeclarativeType(); DynamicPropertyRow::registerDeclarativeType(); + Experimental::PropertyChangesModel::registerDeclarativeType(); + Experimental::PropertyModel::registerDeclarativeType(); const QString resourcePath = PropertyEditorQmlBackend::propertyEditorResourcesPath(); diff --git a/src/plugins/qmldesigner/components/stateseditornew/propertychangesmodel.cpp b/src/plugins/qmldesigner/components/stateseditornew/propertychangesmodel.cpp new file mode 100644 index 00000000000..a2f022c1194 --- /dev/null +++ b/src/plugins/qmldesigner/components/stateseditornew/propertychangesmodel.cpp @@ -0,0 +1,152 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "propertychangesmodel.h" + +#include "stateseditorview.h" +#include + +#include + +#include +#include +#include + +enum { + debug = false +}; + +namespace QmlDesigner { +namespace Experimental { + +PropertyChangesModel::PropertyChangesModel(QObject *parent) + : QAbstractListModel(parent) +{} + +PropertyChangesModel::~PropertyChangesModel() +{ + if (m_view) + m_view->deregisterPropertyChangesModel(this); +} + +int PropertyChangesModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + QmlModelState modelState(m_modelNode); + + if (!modelState.isValid() || modelState.isBaseState()) + return 0; + + return modelState.propertyChanges().size(); +} + +QVariant PropertyChangesModel::data(const QModelIndex &index, int role) const +{ + if (index.parent().isValid() || index.column() != 0) + return {}; + + QmlModelState modelState(m_modelNode); + if (!modelState.isValid() || modelState.isBaseState()) + return {}; + + QList propertyChanges = modelState.propertyChanges(); + + switch (role) { + case Target: { + const ModelNode target = propertyChanges.at(index.row()).target(); + if (target.isValid()) + return target.displayName(); + return {}; + } + + case Explicit: { + return propertyChanges.at(index.row()).explicitValue(); + } + + case RestoreEntryValues: { + return propertyChanges.at(index.row()).restoreEntryValues(); + } + + case PropertyModelNode: { + return propertyChanges.at(index.row()).modelNode().toVariant(); + } + } + return {}; +} + +QHash PropertyChangesModel::roleNames() const +{ + static QHash roleNames{{Target, "target"}, + {Explicit, "explicit"}, + {RestoreEntryValues, "restoreEntryValues"}, + {PropertyModelNode, "propertyModelNode"}}; + return roleNames; +} + +void PropertyChangesModel::setModelNodeBackend(const QVariant &modelNodeBackend) +{ + ModelNode modelNode = modelNodeBackend.value(); + + if (!modelNode.isValid() || modelNode.isRootNode()) + return; + + m_modelNode = modelNode; + + QTC_ASSERT(m_modelNode.simplifiedTypeName() == "State", return ); + + m_view = qobject_cast(m_modelNode.view()); + if (m_view) + m_view->registerPropertyChangesModel(this); + + emit modelNodeBackendChanged(); +} + +void PropertyChangesModel::reset() +{ + QAbstractListModel::beginResetModel(); + QAbstractListModel::endResetModel(); + + emit countChanged(); +} + +int PropertyChangesModel::count() const +{ + return rowCount(); +} + +void PropertyChangesModel::registerDeclarativeType() +{ + qmlRegisterType("HelperWidgets", 2, 0, "PropertyChangesModel"); +} + +QVariant PropertyChangesModel::modelNodeBackend() const +{ + return QVariant(); +} + +} // namespace Experimental +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/stateseditornew/propertychangesmodel.h b/src/plugins/qmldesigner/components/stateseditornew/propertychangesmodel.h new file mode 100644 index 00000000000..622a1d29467 --- /dev/null +++ b/src/plugins/qmldesigner/components/stateseditornew/propertychangesmodel.h @@ -0,0 +1,80 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include +#include + +#include + +namespace QmlDesigner { +namespace Experimental { + +class StatesEditorView; + +class PropertyChangesModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(int count READ count NOTIFY countChanged) + Q_PROPERTY(QVariant modelNodeBackendProperty READ modelNodeBackend WRITE setModelNodeBackend + NOTIFY modelNodeBackendChanged) + + enum { + Target = Qt::DisplayRole, + Explicit = Qt::UserRole, + RestoreEntryValues, + PropertyModelNode + }; + +public: + PropertyChangesModel(QObject *parent = nullptr); + ~PropertyChangesModel(); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + void setModelNodeBackend(const QVariant &modelNodeBackend); + void reset(); + int count() const; + + static void registerDeclarativeType(); + +signals: + void modelNodeBackendChanged(); + void countChanged(); + +private: + QVariant modelNodeBackend() const; + +private: + ModelNode m_modelNode; + QPointer m_view; +}; + +} // namespace Experimental +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/stateseditornew/propertymodel.cpp b/src/plugins/qmldesigner/components/stateseditornew/propertymodel.cpp new file mode 100644 index 00000000000..ef589465b97 --- /dev/null +++ b/src/plugins/qmldesigner/components/stateseditornew/propertymodel.cpp @@ -0,0 +1,172 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "propertymodel.h" + +#include "abstractproperty.h" +#include "abstractview.h" +#include "bindingproperty.h" +#include "nodemetainfo.h" +#include "variantproperty.h" + +#include + +#include + +#include +#include +#include + +enum { + debug = false +}; + +namespace QmlDesigner { +namespace Experimental { + +PropertyModel::PropertyModel(QObject *parent) + : QAbstractListModel(parent) +{} + +int PropertyModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return m_properties.size(); +} + +QVariant PropertyModel::data(const QModelIndex &index, int role) const +{ + if (index.parent().isValid() || index.column() != 0) + return QVariant(); + + switch (role) { + case Name: { + return m_properties.at(index.row()).name(); + } + + case Value: { + AbstractProperty property = m_properties.at(index.row()); + + if (property.isBindingProperty()) + return property.toBindingProperty().expression(); + + if (property.isVariantProperty()) + return property.toVariantProperty().value(); + + return {}; + } + + case Type: { + QmlPropertyChanges propertyChanges(m_modelNode); + if (!propertyChanges.isValid()) + return {}; + + if (!propertyChanges.target().isValid()) + return {}; + + return propertyChanges.target().metaInfo().propertyTypeName( + m_properties.at(index.row()).name()); + } + } + return {}; +} + +QHash PropertyModel::roleNames() const +{ + static QHash roleNames{{Name, "name"}, {Value, "value"}, {Type, "type"}}; + return roleNames; +} + +void PropertyModel::setModelNodeBackend(const QVariant &modelNodeBackend) +{ + ModelNode modelNode = modelNodeBackend.value(); + + if (!modelNode.isValid()) + return; + + m_modelNode = modelNode; + + QTC_ASSERT(m_modelNode.simplifiedTypeName() == "PropertyChanges", return ); + + setupModel(); + emit modelNodeBackendChanged(); +} + +void PropertyModel::setExplicit(bool value) +{ + if (!m_modelNode.isValid() || !m_modelNode.view()->isAttached()) + return; + + QmlPropertyChanges propertyChanges(m_modelNode); + + if (propertyChanges.isValid()) + propertyChanges.setExplicitValue(value); +} + +void PropertyModel::setRestoreEntryValues(bool value) +{ + if (!m_modelNode.isValid() || !m_modelNode.view()->isAttached()) + return; + + QmlPropertyChanges propertyChanges(m_modelNode); + + if (propertyChanges.isValid()) + propertyChanges.setRestoreEntryValues(value); +} + +void PropertyModel::removeProperty(const QString &name) +{ + if (!m_modelNode.isValid() || !m_modelNode.view()->isAttached()) + return; + + m_modelNode.removeProperty(name.toUtf8()); +} + +void PropertyModel::registerDeclarativeType() +{ + qmlRegisterType("HelperWidgets", 2, 0, "PropertyModel"); +} + +QVariant PropertyModel::modelNodeBackend() const +{ + return QVariant(); +} + +void PropertyModel::setupModel() +{ + if (!m_modelNode.isValid() || !m_modelNode.view()->isAttached()) + return; + + QmlPropertyChanges propertyChanges(m_modelNode); + if (!propertyChanges.isValid()) + return; + + m_properties = propertyChanges.targetProperties(); +} + +} // namespace Experimental +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/stateseditornew/propertymodel.h b/src/plugins/qmldesigner/components/stateseditornew/propertymodel.h new file mode 100644 index 00000000000..26c92cb7634 --- /dev/null +++ b/src/plugins/qmldesigner/components/stateseditornew/propertymodel.h @@ -0,0 +1,73 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include +#include + +#include + +namespace QmlDesigner { +namespace Experimental { + +class PropertyModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(QVariant modelNodeBackendProperty READ modelNodeBackend WRITE setModelNodeBackend + NOTIFY modelNodeBackendChanged) + + enum { Name = Qt::DisplayRole, Value = Qt::UserRole, Type }; + +public: + PropertyModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + void setModelNodeBackend(const QVariant &modelNodeBackend); + + Q_INVOKABLE void setExplicit(bool value); + Q_INVOKABLE void setRestoreEntryValues(bool value); + Q_INVOKABLE void removeProperty(const QString &name); + + static void registerDeclarativeType(); + +signals: + void modelNodeBackendChanged(); + +private: + QVariant modelNodeBackend() const; + void setupModel(); + +private: + ModelNode m_modelNode; + QList m_properties; +}; + +} // namespace Experimental +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/stateseditornew/stateseditorimageprovider.cpp b/src/plugins/qmldesigner/components/stateseditornew/stateseditorimageprovider.cpp new file mode 100644 index 00000000000..2fa0a0e5dff --- /dev/null +++ b/src/plugins/qmldesigner/components/stateseditornew/stateseditorimageprovider.cpp @@ -0,0 +1,82 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "stateseditorimageprovider.h" +#include "nodeinstanceview.h" + +#include + +namespace QmlDesigner { +namespace Experimental { +namespace Internal { + +StatesEditorImageProvider::StatesEditorImageProvider() + : QQuickImageProvider(QQuickImageProvider::Image) +{ +} + +QImage StatesEditorImageProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize) +{ + QImage image; + + bool nodeInstanceViewIsDetached = m_nodeInstanceView.isNull() || !m_nodeInstanceView->model(); + if (!nodeInstanceViewIsDetached) { + QString imageId = id.split(QLatin1Char('-')).constFirst(); + if (imageId == QLatin1String("baseState")) { + image = m_nodeInstanceView->statePreviewImage(m_nodeInstanceView->rootModelNode()); + } else { + bool canBeConverted; + int instanceId = imageId.toInt(&canBeConverted); + if (canBeConverted && m_nodeInstanceView->hasModelNodeForInternalId(instanceId)) { + image = m_nodeInstanceView->statePreviewImage(m_nodeInstanceView->modelNodeForInternalId(instanceId)); + } + } + } + + if (image.isNull()) { + //creating white QImage + QSize newSize = requestedSize; + if (newSize.isEmpty()) + newSize = QSize (100, 100); + + QImage image(newSize, QImage::Format_ARGB32); + image.fill(0xFFFFFFFF); + return image; + } + + *size = image.size(); + + return image; +} + +void StatesEditorImageProvider::setNodeInstanceView(NodeInstanceView *nodeInstanceView) +{ + m_nodeInstanceView = nodeInstanceView; +} + +} // namespace Internal +} // namespace Experimental +} // namespace QmlDesigner + diff --git a/src/plugins/qmldesigner/components/stateseditornew/stateseditorimageprovider.h b/src/plugins/qmldesigner/components/stateseditornew/stateseditorimageprovider.h new file mode 100644 index 00000000000..c2f43d46c46 --- /dev/null +++ b/src/plugins/qmldesigner/components/stateseditornew/stateseditorimageprovider.h @@ -0,0 +1,54 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include"abstractview.h" + +#include +#include + +namespace QmlDesigner { +namespace Experimental { +namespace Internal { + +class StatesEditorView; + +class StatesEditorImageProvider : public QQuickImageProvider +{ +public: + StatesEditorImageProvider(); + + QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override; + + void setNodeInstanceView(NodeInstanceView *nodeInstanceView); + +private: + QPointer m_nodeInstanceView; +}; + +} // namespace Internal +} // namespace Experimental +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/stateseditornew/stateseditormodel.cpp b/src/plugins/qmldesigner/components/stateseditornew/stateseditormodel.cpp new file mode 100644 index 00000000000..efe1c305f66 --- /dev/null +++ b/src/plugins/qmldesigner/components/stateseditornew/stateseditormodel.cpp @@ -0,0 +1,455 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "stateseditormodel.h" +#include "stateseditorview.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include + +enum { + debug = false +}; + +namespace QmlDesigner { +namespace Experimental { + +StatesEditorModel::StatesEditorModel(StatesEditorView *view) + : QAbstractListModel(view) + , m_statesEditorView(view) + , m_hasExtend(false) + , m_extendedStates() +{ + QObject::connect(this, &StatesEditorModel::dataChanged, [this]() { emit baseStateChanged(); }); +} + +int StatesEditorModel::count() const +{ + return rowCount(); +} + +QModelIndex StatesEditorModel::index(int row, int column, const QModelIndex &parent) const +{ + if (m_statesEditorView.isNull()) + return {}; + + int internalNodeId = 0; + if (row > 0) + internalNodeId = m_statesEditorView->activeStatesGroupNode() + .nodeListProperty("states") + .at(row - 1) + .internalId(); + + return hasIndex(row, column, parent) ? createIndex(row, column, internalNodeId) : QModelIndex(); +} + +int StatesEditorModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid() || m_statesEditorView.isNull() || !m_statesEditorView->model()) + return 0; + + if (!m_statesEditorView->activeStatesGroupNode().hasNodeListProperty("states")) + return 1; // base state + + return m_statesEditorView->activeStatesGroupNode().nodeListProperty("states").count() + 1; +} + +void StatesEditorModel::reset() +{ + QAbstractListModel::beginResetModel(); + QAbstractListModel::endResetModel(); + + evaluateExtend(); +} + +QVariant StatesEditorModel::data(const QModelIndex &index, int role) const +{ + if (index.parent().isValid() || index.column() != 0 || m_statesEditorView.isNull() + || !m_statesEditorView->hasModelNodeForInternalId(index.internalId())) + return QVariant(); + + ModelNode stateNode; + + if (index.internalId() > 0) + stateNode = m_statesEditorView->modelNodeForInternalId(index.internalId()); + + switch (role) { + case StateNameRole: { + if (index.row() == 0) { + return tr("base state", "Implicit default state"); + } else { + if (stateNode.hasVariantProperty("name")) + return stateNode.variantProperty("name").value(); + else + return QVariant(); + } + } + + case StateImageSourceRole: { + static int randomNumber = 0; + randomNumber++; + if (index.row() == 0) + return QString("image://qmldesigner_stateseditor/baseState-%1").arg(randomNumber); + else + return QString("image://qmldesigner_stateseditor/%1-%2").arg(index.internalId()).arg(randomNumber); + } + + case InternalNodeId: + return index.internalId(); + + case HasWhenCondition: + return stateNode.isValid() && stateNode.hasProperty("when"); + + case WhenConditionString: { + if (stateNode.isValid() && stateNode.hasBindingProperty("when")) + return stateNode.bindingProperty("when").expression(); + else + return QString(); + } + + case IsDefault: { + QmlModelState modelState(stateNode); + if (modelState.isValid()) + return modelState.isDefault(); + return false; + } + + case ModelHasDefaultState: + return hasDefaultState(); + + case HasExtend: + return stateNode.isValid() && stateNode.hasProperty("extend"); + + case ExtendString: { + if (stateNode.isValid() && stateNode.hasVariantProperty("extend")) + return stateNode.variantProperty("extend").value(); + else + return QString(); + } + } + + return QVariant(); +} + +QHash StatesEditorModel::roleNames() const +{ + static QHash roleNames{{StateNameRole, "stateName"}, + {StateImageSourceRole, "stateImageSource"}, + {InternalNodeId, "internalNodeId"}, + {HasWhenCondition, "hasWhenCondition"}, + {WhenConditionString, "whenConditionString"}, + {IsDefault, "isDefault"}, + {ModelHasDefaultState, "modelHasDefaultState"}, + {HasExtend, "hasExtend"}, + {ExtendString, "extendString"}}; + return roleNames; +} + +void StatesEditorModel::insertState(int stateIndex) +{ + if (stateIndex >= 0) { + const int updateIndex = stateIndex + 1; + beginInsertRows(QModelIndex(), updateIndex, updateIndex); + endInsertRows(); + + emit dataChanged(index(updateIndex, 0), index(updateIndex, 0)); + } +} + +void StatesEditorModel::updateState(int beginIndex, int endIndex) +{ + if (beginIndex >= 0 && endIndex >= 0) + emit dataChanged(index(beginIndex, 0), index(endIndex, 0)); +} + +void StatesEditorModel::removeState(int stateIndex) +{ + if (stateIndex >= 0) { + beginRemoveRows(QModelIndex(), 0, stateIndex); + endRemoveRows(); + } +} + +void StatesEditorModel::renameState(int internalNodeId, const QString &newName) +{ + if (newName == m_statesEditorView->currentStateName()) + return; + + if (newName.isEmpty() ||! m_statesEditorView->validStateName(newName)) { + QTimer::singleShot(0, this, [newName] { + Core::AsynchronousMessageBox::warning( + tr("Invalid state name"), + newName.isEmpty() ? + tr("The empty string as a name is reserved for the base state.") : + tr("Name already used in another state")); + }); + reset(); + } else { + m_statesEditorView->renameState(internalNodeId, newName); + } +} + +void StatesEditorModel::setWhenCondition(int internalNodeId, const QString &condition) +{ + m_statesEditorView->setWhenCondition(internalNodeId, condition); +} + +void StatesEditorModel::resetWhenCondition(int internalNodeId) +{ + m_statesEditorView->resetWhenCondition(internalNodeId); +} + +QStringList StatesEditorModel::autoComplete(const QString &text, int pos, bool explicitComplete) +{ + Model *model = m_statesEditorView->model(); + if (model && model->rewriterView()) + return model->rewriterView()->autoComplete(text, pos, explicitComplete); + + return QStringList(); +} + +QVariant StatesEditorModel::stateModelNode(int internalNodeId) +{ + if (!m_statesEditorView->model()) + return QVariant(); + + ModelNode node = m_statesEditorView->modelNodeForInternalId(internalNodeId); + + return QVariant::fromValue(m_statesEditorView->modelNodeForInternalId(internalNodeId)); +} + +void StatesEditorModel::setStateAsDefault(int internalNodeId) +{ + m_statesEditorView->setStateAsDefault(internalNodeId); +} + +void StatesEditorModel::resetDefaultState() +{ + m_statesEditorView->resetDefaultState(); +} + +bool StatesEditorModel::hasDefaultState() const +{ + return m_statesEditorView->hasDefaultState(); +} + +void StatesEditorModel::setAnnotation(int internalNodeId) +{ + m_statesEditorView->setAnnotation(internalNodeId); +} + +void StatesEditorModel::removeAnnotation(int internalNodeId) +{ + m_statesEditorView->removeAnnotation(internalNodeId); +} + +bool StatesEditorModel::hasAnnotation(int internalNodeId) const +{ + return m_statesEditorView->hasAnnotation(internalNodeId); +} + +QStringList StatesEditorModel::stateGroups() const +{ + auto stateGroups = Utils::transform(m_statesEditorView->allModelNodesOfType( + "QtQuick.StateGroup"), + [](const ModelNode &node) { return node.displayName(); }); + stateGroups.prepend(tr("Root")); + return stateGroups; +} + +QString StatesEditorModel::activeStateGroup() const +{ + auto stateGroup = m_statesEditorView->activeStatesGroupNode(); + + if (!stateGroup.isValid()) + return QString(); + + return stateGroup.displayName(); +} + +void StatesEditorModel::setActiveStateGroup(const QString &name) +{ + auto modelNode = Utils::findOrDefault(m_statesEditorView->allModelNodesOfType( + "QtQuick.StateGroup"), + [&name](const ModelNode &node) { + return node.displayName() == name; + }); + + QTC_ASSERT(!modelNode.isValid(), return ); + + if (modelNode.isValid()) + m_statesEditorView->setActiveStatesGroupNode(modelNode); +} + +int StatesEditorModel::activeStateGroupIndex() const +{ + return m_statesEditorView->activeStatesGroupIndex(); +} + +void StatesEditorModel::setActiveStateGroupIndex(int index) +{ + m_statesEditorView->setActiveStatesGroupIndex(index); +} + +bool StatesEditorModel::renameActiveStateGroup(const QString &name) +{ + auto stateGroup = m_statesEditorView->activeStatesGroupNode(); + + if (!stateGroup.isValid() || stateGroup.isRootNode()) + return false; + + if (!QmlDesigner::ModelNode::isValidId(name) || m_statesEditorView->hasId(name)) { + QString errMsg = QmlDesigner::ModelNode::getIdValidityErrorMessage(name); + if (!errMsg.isEmpty()) + Core::AsynchronousMessageBox::warning(tr("Invalid ID"), errMsg); + else + Core::AsynchronousMessageBox::warning(tr("Invalid ID"), + tr("%1 already exists.").arg(name)); + return false; + } + + stateGroup.setIdWithRefactoring(name); + emit stateGroupsChanged(); + return true; +} + +void StatesEditorModel::addStateGroup(const QString &name) +{ + m_statesEditorView->executeInTransaction("createStateGroup", [this, name]() { + const TypeName typeName = "QtQuick.StateGroup"; + auto metaInfo = m_statesEditorView->model()->metaInfo(typeName); + int minorVersion = metaInfo.minorVersion(); + int majorVersion = metaInfo.majorVersion(); + auto stateGroupNode = m_statesEditorView->createModelNode(typeName, + majorVersion, + minorVersion); + stateGroupNode.setIdWithoutRefactoring(m_statesEditorView->model()->generateNewId(name)); + + m_statesEditorView->rootModelNode().defaultNodeAbstractProperty().reparentHere( + stateGroupNode); + m_statesEditorView->setActiveStatesGroupNode(stateGroupNode); + }); +} + +void StatesEditorModel::removeStateGroup() +{ + if (m_statesEditorView->activeStatesGroupNode().isRootNode()) + return; + + m_statesEditorView->executeInTransaction("removeStateGroup", [this]() { + m_statesEditorView->activeStatesGroupNode().destroy(); + }); +} + +QVariantMap StatesEditorModel::get(int idx) const +{ + const QHash &names = roleNames(); + QHash::const_iterator i = names.constBegin(); + + QVariantMap res; + QModelIndex modelIndex = index(idx); + + while (i != names.constEnd()) { + QVariant data = modelIndex.data(i.key()); + + res[QString::fromUtf8(i.value())] = data; + ++i; + } + return res; +} + +QVariantMap StatesEditorModel::baseState() const +{ + return get(0); +} + +bool StatesEditorModel::hasExtend() const +{ + return m_hasExtend; +} + +QStringList StatesEditorModel::extendedStates() const +{ + return m_extendedStates; +} + +void StatesEditorModel::move(int from, int to) +{ + // This does not alter the code (rewriter) which means the reordering is not presistent + + if (from == to) + return; + + int specialIndex = (from < to ? to + 1 : to); + beginMoveRows(QModelIndex(), from, from, QModelIndex(), specialIndex); + endMoveRows(); +} + +void StatesEditorModel::drop(int from, int to) +{ + m_statesEditorView->moveStates(from, to); +} + +void StatesEditorModel::evaluateExtend() +{ + bool hasExtend = m_statesEditorView->hasExtend(); + + if (m_hasExtend != hasExtend) { + m_hasExtend = hasExtend; + emit hasExtendChanged(); + } + + auto extendedStates = m_statesEditorView->extendedStates(); + + if (extendedStates.size() != m_extendedStates.size()) { + m_extendedStates = extendedStates; + emit extendedStatesChanged(); + return; + } + + for (int i = 0; i != m_extendedStates.size(); ++i) { + if (extendedStates[i] != m_extendedStates[i]) { + m_extendedStates = extendedStates; + emit extendedStatesChanged(); + return; + } + } +} + +} // namespace Experimental +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/stateseditornew/stateseditormodel.h b/src/plugins/qmldesigner/components/stateseditornew/stateseditormodel.h new file mode 100644 index 00000000000..c1f12878387 --- /dev/null +++ b/src/plugins/qmldesigner/components/stateseditornew/stateseditormodel.h @@ -0,0 +1,127 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include +#include + +namespace QmlDesigner { +namespace Experimental { + +class StatesEditorView; + +class StatesEditorModel : public QAbstractListModel +{ + Q_OBJECT + + enum { + StateNameRole = Qt::DisplayRole, + StateImageSourceRole = Qt::UserRole, + InternalNodeId, + HasWhenCondition, + WhenConditionString, + IsDefault, + ModelHasDefaultState, + HasExtend, + ExtendString + }; + +public: + StatesEditorModel(StatesEditorView *view); + + Q_INVOKABLE int count() const; + QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + void insertState(int stateIndex); + void removeState(int stateIndex); + void updateState(int beginIndex, int endIndex); + Q_INVOKABLE void renameState(int internalNodeId, const QString &newName); + Q_INVOKABLE void setWhenCondition(int internalNodeId, const QString &condition); + Q_INVOKABLE void resetWhenCondition(int internalNodeId); + Q_INVOKABLE QStringList autoComplete(const QString &text, int pos, bool explicitComplete); + Q_INVOKABLE QVariant stateModelNode(int internalNodeId); + + Q_INVOKABLE void setStateAsDefault(int internalNodeId); + Q_INVOKABLE void resetDefaultState(); + Q_INVOKABLE bool hasDefaultState() const; + Q_INVOKABLE void setAnnotation(int internalNodeId); + Q_INVOKABLE void removeAnnotation(int internalNodeId); + Q_INVOKABLE bool hasAnnotation(int internalNodeId) const; + + QStringList stateGroups() const; + QString activeStateGroup() const; + void setActiveStateGroup(const QString &name); + int activeStateGroupIndex() const; + void setActiveStateGroupIndex(int index); + + Q_INVOKABLE bool renameActiveStateGroup(const QString &name); + + Q_INVOKABLE void addStateGroup(const QString &name); + Q_INVOKABLE void removeStateGroup(); + + Q_INVOKABLE QVariantMap get(int idx) const; + + QVariantMap baseState() const; + bool hasExtend() const; + QStringList extendedStates() const; + + Q_PROPERTY(QVariantMap baseState READ baseState NOTIFY baseStateChanged) + Q_PROPERTY(QStringList extendedStates READ extendedStates NOTIFY extendedStatesChanged) + + Q_PROPERTY(bool hasExtend READ hasExtend NOTIFY hasExtendChanged) + + Q_PROPERTY(QString activeStateGroup READ activeStateGroup WRITE setActiveStateGroup NOTIFY + activeStateGroupChanged) + Q_PROPERTY(int activeStateGroupIndex READ activeStateGroupIndex WRITE setActiveStateGroupIndex + NOTIFY activeStateGroupIndexChanged) + Q_PROPERTY(QStringList stateGroups READ stateGroups NOTIFY stateGroupsChanged) + + Q_INVOKABLE void move(int from, int to); + Q_INVOKABLE void drop(int from, int to); + + void reset(); + void evaluateExtend(); + +signals: + void changedToState(int n); + void baseStateChanged(); + void hasExtendChanged(); + void extendedStatesChanged(); + void activeStateGroupChanged(); + void activeStateGroupIndexChanged(); + void stateGroupsChanged(); + +private: + QPointer m_statesEditorView; + bool m_hasExtend; + QStringList m_extendedStates; +}; + +} // namespace Experimental +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/stateseditornew/stateseditorview.cpp b/src/plugins/qmldesigner/components/stateseditornew/stateseditorview.cpp new file mode 100644 index 00000000000..6d3d853a987 --- /dev/null +++ b/src/plugins/qmldesigner/components/stateseditornew/stateseditorview.cpp @@ -0,0 +1,936 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "stateseditorview.h" +#include "propertychangesmodel.h" +#include "stateseditormodel.h" +#include "stateseditorwidget.h" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace QmlDesigner { +namespace Experimental { + +/** + We always have 'one' current state, where we get updates from (see sceneChanged()). In case + the current state is the base state, we render the base state + all other states. + */ +StatesEditorView::StatesEditorView(QObject *parent) : + AbstractView(parent), + m_statesEditorModel(new StatesEditorModel(this)), + m_lastIndex(-1), + m_editor(nullptr) +{ + Q_ASSERT(m_statesEditorModel); + // base state +} + +StatesEditorView::~StatesEditorView() +{ + if (m_editor) + delete m_editor; + delete m_statesEditorWidget.data(); +} + +WidgetInfo StatesEditorView::widgetInfo() +{ + if (!m_statesEditorWidget) + m_statesEditorWidget = new StatesEditorWidget(this, m_statesEditorModel.data()); + + return createWidgetInfo(m_statesEditorWidget.data(), + QLatin1String("StatesEditorNew"), + WidgetInfo::BottomPane, + 0, + tr("States New")); +} + +void StatesEditorView::rootNodeTypeChanged(const QString &/*type*/, int /*majorVersion*/, int /*minorVersion*/) +{ + checkForStatesAvailability(); +} + +ModelNode StatesEditorView::activeStatesGroupNode() const +{ + return m_activeStatesGroupNode; +} + +void StatesEditorView::setActiveStatesGroupNode(const ModelNode &modelNode) +{ + if (m_activeStatesGroupNode == modelNode) + return; + + m_activeStatesGroupNode = modelNode; + resetModel(); + + emit m_statesEditorModel->activeStateGroupChanged(); + emit m_statesEditorModel->activeStateGroupIndexChanged(); +} + +int StatesEditorView::activeStatesGroupIndex() const +{ + return Utils::indexOf(allModelNodesOfType("QtQuick.StateGroup"), + [this](const ModelNode &node) { return node == m_activeStatesGroupNode; }) + + 1; +} + +void StatesEditorView::setActiveStatesGroupIndex(int index) +{ + if (index > 0) { + const ModelNode statesGroup = allModelNodesOfType("QtQuick.StateGroup").at(index - 1); + if (statesGroup.isValid()) + setActiveStatesGroupNode(statesGroup); + } else { + setActiveStatesGroupNode(rootModelNode()); + } +} + +void StatesEditorView::registerPropertyChangesModel(PropertyChangesModel *model) +{ + m_propertyChangedModels.insert(model); +} + +void StatesEditorView::deregisterPropertyChangesModel(PropertyChangesModel *model) +{ + m_propertyChangedModels.remove(model); +} + +void StatesEditorView::synchonizeCurrentStateFromWidget() +{ + if (!model()) + return; + + if (m_block) + return; + + int internalId = m_statesEditorWidget->currentStateInternalId(); + + if (internalId > 0 && hasModelNodeForInternalId(internalId)) { + ModelNode node = modelNodeForInternalId(internalId); + QmlModelState modelState(node); + if (modelState.isValid() && modelState != currentState()) + setCurrentState(modelState); + } else { + setCurrentState(baseState()); + } +} + +void StatesEditorView::createNewState() +{ + // can happen when root node is e.g. a ListModel + if (!QmlVisualNode::isValidQmlVisualNode(activeStatesGroupNode()) + && m_activeStatesGroupNode.type() != "QtQuick.StateGroup") + return; + + QmlDesignerPlugin::emitUsageStatistics(Constants::EVENT_STATE_ADDED); + + QStringList modelStateNames = activeStateGroup().names(); + + QString newStateName; + int index = 1; + while (true) { + newStateName = QString(QStringLiteral("State%1")).arg(index++); + if (!modelStateNames.contains(newStateName)) + break; + } + + executeInTransaction("createNewState", [this, newStateName]() { + activeStatesGroupNode().validId(); + + ModelNode newState = activeStateGroup().addState(newStateName); + setCurrentState(newState); + }); +} + +void StatesEditorView::cloneState(int nodeId) +{ + if (!(nodeId > 0 && hasModelNodeForInternalId(nodeId))) + return; + + ModelNode stateNode(modelNodeForInternalId(nodeId)); + QTC_ASSERT(stateNode.simplifiedTypeName() == "State", return ); + + QmlModelState modelState(stateNode); + if (!modelState.isValid() || modelState.isBaseState()) + return; + + QmlDesignerPlugin::emitUsageStatistics(Constants::EVENT_STATE_CLONED); + + QString newName = modelState.name(); + + // Strip out numbers at the end of the string + QRegularExpression regEx(QLatin1String("[0-9]+$")); + const QRegularExpressionMatch match = regEx.match(newName); + if (match.hasMatch() && (match.capturedStart() + match.capturedLength() == newName.length())) + newName = newName.left(match.capturedStart()); + + int i = 1; + QStringList stateNames = activeStateGroup().names(); + while (stateNames.contains(newName + QString::number(i))) + i++; + const QString newStateName = newName + QString::number(i); + + QmlModelState newState; + + executeInTransaction("cloneState", [newStateName, modelState, &newState]() { + newState = modelState.duplicate(newStateName); + }); + + ModelNode newNode = newState.modelNode(); + int from = newNode.parentProperty().indexOf(newNode); + int to = stateNode.parentProperty().indexOf(stateNode) + 1; + + executeInTransaction("moveState", [this, &newState, from, to]() { + activeStatesGroupNode().nodeListProperty("states").slide(from, to); + setCurrentState(newState); + }); +} + +void StatesEditorView::extendState(int nodeId) +{ + if (!(nodeId > 0 && hasModelNodeForInternalId(nodeId))) + return; + + ModelNode stateNode(modelNodeForInternalId(nodeId)); + QTC_ASSERT(stateNode.simplifiedTypeName() == "State", return ); + + QmlModelState modelState(stateNode); + if (!modelState.isValid() || modelState.isBaseState()) + return; + + QmlDesignerPlugin::emitUsageStatistics(Constants::EVENT_STATE_EXTENDED); + + QString newName = modelState.name(); + + // Strip out numbers at the end of the string + QRegularExpression regEx(QLatin1String("[0-9]+$")); + const QRegularExpressionMatch match = regEx.match(newName); + if (match.hasMatch() && (match.capturedStart() + match.capturedLength() == newName.length())) + newName = newName.left(match.capturedStart()); + + int i = 1; + QStringList stateNames = activeStateGroup().names(); + while (stateNames.contains(newName + QString::number(i))) + i++; + const QString newStateName = newName + QString::number(i); + + QmlModelState newState; + + executeInTransaction("extendState", [this, newStateName, modelState, &newState]() { + newState = activeStateGroup().addState(newStateName); + newState.setExtend(modelState.name()); + }); + + ModelNode newNode = newState.modelNode(); + int from = newNode.parentProperty().indexOf(newNode); + int to = stateNode.parentProperty().indexOf(stateNode) + 1; + + executeInTransaction("moveState", [this, &newState, from, to]() { + activeStatesGroupNode().nodeListProperty("states").slide(from, to); + setCurrentState(newState); + }); +} + +void StatesEditorView::removeState(int nodeId) +{ + try { + if (nodeId > 0 && hasModelNodeForInternalId(nodeId)) { + ModelNode stateNode(modelNodeForInternalId(nodeId)); + QTC_ASSERT(stateNode.simplifiedTypeName() == "State", return ); + + QmlModelState modelState(stateNode); + if (modelState.isValid()) { + QStringList lockedTargets; + const auto propertyChanges = modelState.propertyChanges(); + + // confirm removing not empty states + if (!propertyChanges.isEmpty()) { + QMessageBox msgBox; + msgBox.setTextFormat(Qt::RichText); + msgBox.setIcon(QMessageBox::Question); + msgBox.setWindowTitle(tr("Remove State")); + msgBox.setText( + tr("This state is not empty. Are you sure you want to remove it?")); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Yes); + + if (msgBox.exec() == QMessageBox::Cancel) + return; + } + + // confirm removing states with locked targets + for (const QmlPropertyChanges &change : propertyChanges) { + const ModelNode target = change.target(); + QTC_ASSERT(target.isValid(), continue); + if (target.locked()) + lockedTargets.push_back(target.id()); + } + + if (!lockedTargets.empty()) { + Utils::sort(lockedTargets); + QString detailedText = QString("" + tr("Locked components:") + "
"); + + for (const auto &id : qAsConst(lockedTargets)) + detailedText.append("- " + id + "
"); + + detailedText.chop(QString("
").size()); + + QMessageBox msgBox; + msgBox.setTextFormat(Qt::RichText); + msgBox.setIcon(QMessageBox::Question); + msgBox.setWindowTitle(tr("Remove State")); + msgBox.setText(QString(tr("Removing this state will modify locked components.") + + "

%1") + .arg(detailedText)); + msgBox.setInformativeText(tr("Continue by removing the state?")); + msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Ok); + + if (msgBox.exec() == QMessageBox::Cancel) + return; + } + } + + NodeListProperty parentProperty = stateNode.parentProperty().toNodeListProperty(); + + if (parentProperty.count() <= 1) { + setCurrentState(baseState()); + } else if (parentProperty.isValid()) { + int index = parentProperty.indexOf(stateNode); + if (index == 0) + setCurrentState(parentProperty.at(1)); + else + setCurrentState(parentProperty.at(index - 1)); + } + + stateNode.destroy(); + } + } catch (const RewritingException &e) { + e.showException(); + } +} + +void StatesEditorView::resetModel() +{ + if (m_bulkChange) { + m_modelDirty = true; + return; + } + + if (m_statesEditorModel) + m_statesEditorModel->reset(); + + if (m_statesEditorWidget) { + if (currentState().isBaseState()) + m_statesEditorWidget->setCurrentStateInternalId(0); + else + m_statesEditorWidget->setCurrentStateInternalId(currentState().modelNode().internalId()); + } + + m_modelDirty = false; +} + +void StatesEditorView::resetPropertyChangesModels() +{ + if (m_bulkChange) { + m_propertyChangesDirty = true; + return; + } + + std::for_each(m_propertyChangedModels.begin(), + m_propertyChangedModels.end(), + [](PropertyChangesModel *model) { model->reset(); }); + + m_propertyChangesDirty = false; +} + +void StatesEditorView::resetExtend() +{ + if (m_bulkChange) { + m_extendDirty = true; + return; + } + + m_statesEditorModel->evaluateExtend(); + + m_extendDirty = false; +} + +void StatesEditorView::resetStateGroups() +{ + if (m_bulkChange) { + m_stateGroupsDirty = true; + return; + } + + emit m_statesEditorModel->stateGroupsChanged(); + + m_stateGroupsDirty = false; +} + +void StatesEditorView::checkForStatesAvailability() +{ + if (m_statesEditorWidget) { + const bool isVisual = QmlVisualNode::isValidQmlVisualNode(activeStatesGroupNode()); + m_statesEditorWidget->showAddNewStatesButton(isVisual); + } +} + +void StatesEditorView::beginBulkChange() +{ + m_bulkChange = true; +} + +void StatesEditorView::endBulkChange() +{ + if (!m_bulkChange) + return; + + m_bulkChange = false; + + if (m_modelDirty) + resetModel(); + + if (m_propertyChangesDirty) + resetPropertyChangesModels(); + + if (m_extendDirty) + resetExtend(); + + if (m_stateGroupsDirty) + resetStateGroups(); +} + +void StatesEditorView::setCurrentState(const QmlModelState &state) +{ + if (!model() && !state.isValid()) + return; + + if (currentStateNode() != state.modelNode()) + setCurrentStateNode(state.modelNode()); +} + +QmlModelState StatesEditorView::baseState() const +{ + return QmlModelState::createBaseState(this); +} + +QmlModelStateGroup StatesEditorView::activeStateGroup() const +{ + return QmlModelStateGroup(activeStatesGroupNode()); +} + +bool StatesEditorView::validStateName(const QString &name) const +{ + if (name == tr("base state")) + return false; + const QList modelStates = activeStateGroup().allStates(); + for (const QmlModelState &state : modelStates) { + if (state.name() == name) + return false; + } + return true; +} + +bool StatesEditorView::hasExtend() const +{ + if (!model()) + return false; + + const QList modelStates = activeStateGroup().allStates(); + for (const QmlModelState &state : modelStates) { + if (state.hasExtend()) + return true; + } + return false; +} + +QStringList StatesEditorView::extendedStates() const +{ + if (!model()) + return QStringList(); + + QStringList states; + + const QList modelStates = activeStateGroup().allStates(); + for (const QmlModelState &state : modelStates) { + if (state.hasExtend()) + states.append(state.extend()); + } + states.removeDuplicates(); + return states; +} + +QString StatesEditorView::currentStateName() const +{ + return currentState().isValid() ? currentState().name() : QString(); +} + +void StatesEditorView::renameState(int internalNodeId, const QString &newName) +{ + if (hasModelNodeForInternalId(internalNodeId)) { + QmlModelState renamedState(modelNodeForInternalId(internalNodeId)); + try { + if (renamedState.isValid() && renamedState.name() != newName) { + executeInTransaction("renameState", [this, &renamedState, &newName]() { + // Jump to base state for the change + QmlModelState oldState = currentState(); + setCurrentState(baseState()); + const bool updateDefault = renamedState.isDefault(); + + // If state is extended rename all references + QList states; + const QList modelStates = activeStateGroup().allStates(); + for (const QmlModelState &state : modelStates) { + if (state.hasExtend() && state.extend() == renamedState.name()) + states.append(state); + } + + renamedState.setName(newName.trimmed()); + + for (QmlModelState &state : states) + state.setExtend(newName.trimmed()); + + if (updateDefault) + renamedState.setAsDefault(); + + setCurrentState(oldState); + }); + } + } catch (const RewritingException &e) { + e.showException(); + } + } +} + +void StatesEditorView::setWhenCondition(int internalNodeId, const QString &condition) +{ + if (m_block) + return; + + m_block = true; + auto guard = qScopeGuard([&]() { m_block = false; }); + + if (hasModelNodeForInternalId(internalNodeId)) { + QmlModelState state(modelNodeForInternalId(internalNodeId)); + try { + if (state.isValid()) + state.modelNode().bindingProperty("when").setExpression(condition); + + } catch (const Exception &e) { + e.showException(); + } + } +} + +void StatesEditorView::resetWhenCondition(int internalNodeId) +{ + if (m_block) + return; + + m_block = true; + auto guard = qScopeGuard([&]() { m_block = false; }); + + if (hasModelNodeForInternalId(internalNodeId)) { + QmlModelState state(modelNodeForInternalId(internalNodeId)); + try { + if (state.isValid() && state.modelNode().hasProperty("when")) + state.modelNode().removeProperty("when"); + + } catch (const RewritingException &e) { + e.showException(); + } + } +} + +void StatesEditorView::setStateAsDefault(int internalNodeId) +{ + if (m_block) + return; + + m_block = true; + auto guard = qScopeGuard([&]() { m_block = false; }); + + if (hasModelNodeForInternalId(internalNodeId)) { + QmlModelState state(modelNodeForInternalId(internalNodeId)); + try { + if (state.isValid()) + state.setAsDefault(); + + } catch (const RewritingException &e) { + e.showException(); + } + } +} + +void StatesEditorView::resetDefaultState() +{ + if (m_block) + return; + + m_block = true; + auto guard = qScopeGuard([&]() { m_block = false; }); + + try { + if (activeStatesGroupNode().hasProperty("state")) + activeStatesGroupNode().removeProperty("state"); + + } catch (const RewritingException &e) { + e.showException(); + } +} + +bool StatesEditorView::hasDefaultState() const +{ + return activeStatesGroupNode().hasProperty("state"); +} + +void StatesEditorView::setAnnotation(int internalNodeId) +{ + if (m_block) + return; + + m_block = true; + auto guard = qScopeGuard([&]() { m_block = false; }); + + if (hasModelNodeForInternalId(internalNodeId)) { + QmlModelState state(modelNodeForInternalId(internalNodeId)); + try { + if (state.isValid()) { + ModelNode modelNode = state.modelNode(); + + if (modelNode.isValid()) { + if (!m_editor) + m_editor = new AnnotationEditor(this); + + m_editor->setModelNode(modelNode); + m_editor->showWidget(); + } + } + + } catch (const RewritingException &e) { + e.showException(); + } + } +} + +void StatesEditorView::removeAnnotation(int internalNodeId) +{ + if (m_block) + return; + + m_block = true; + auto guard = qScopeGuard([&]() { m_block = false; }); + + if (hasModelNodeForInternalId(internalNodeId)) { + QmlModelState state(modelNodeForInternalId(internalNodeId)); + try { + if (state.isValid()) + state.removeAnnotation(); + + } catch (const RewritingException &e) { + e.showException(); + } + } +} + +bool StatesEditorView::hasAnnotation(int internalNodeId) const +{ + if (!model()) + return false; + + if (hasModelNodeForInternalId(internalNodeId)) { + QmlModelState state(modelNodeForInternalId(internalNodeId)); + if (state.isValid()) + return state.hasAnnotation(); + } + + return false; +} + +void StatesEditorView::modelAttached(Model *model) +{ + if (model == AbstractView::model()) + return; + + QTC_ASSERT(model, return ); + AbstractView::modelAttached(model); + + m_activeStatesGroupNode = rootModelNode(); + + if (m_statesEditorWidget) + m_statesEditorWidget->setNodeInstanceView(nodeInstanceView()); + + checkForStatesAvailability(); + + resetModel(); + resetStateGroups(); +} + +void StatesEditorView::modelAboutToBeDetached(Model *model) +{ + AbstractView::modelAboutToBeDetached(model); + resetModel(); +} + +void StatesEditorView::propertiesRemoved(const QList& propertyList) +{ + for (const AbstractProperty &property : propertyList) { + if (property.name() == "states" && property.parentModelNode() == activeStateGroup().modelNode()) + resetModel(); + if (property.name() == "when" + && QmlModelState::isValidQmlModelState(property.parentModelNode())) + resetModel(); + if (property.name() == "extend") + resetExtend(); + if (property.parentModelNode().simplifiedTypeName() == "PropertyChanges" + || (property.name() == "changes" + && property.parentModelNode().simplifiedTypeName() == "State")) + resetPropertyChangesModels(); + } +} + +void StatesEditorView::nodeAboutToBeRemoved(const ModelNode &removedNode) +{ + if (removedNode.hasParentProperty()) { + const NodeAbstractProperty propertyParent = removedNode.parentProperty(); + if (propertyParent.parentModelNode() == activeStateGroup().modelNode() + && propertyParent.name() == "states") + m_lastIndex = propertyParent.indexOf(removedNode); + } + if (currentState().isValid() && removedNode == currentState()) + setCurrentState(baseState()); + + if (removedNode.simplifiedTypeName() == "PropertyChanges") + m_propertyChangesRemoved = true; + + if (removedNode.simplifiedTypeName() == "StateGroup") { + if (removedNode == activeStatesGroupNode()) + setActiveStatesGroupNode(rootModelNode()); + + m_stateGroupRemoved = true; + } +} + +void StatesEditorView::nodeRemoved(const ModelNode & /*removedNode*/, + const NodeAbstractProperty &parentProperty, + PropertyChangeFlags /*propertyChange*/) +{ + if (parentProperty.isValid() + && parentProperty.parentModelNode() == activeStateGroup().modelNode() + && parentProperty.name() == "states") { + m_statesEditorModel->removeState(m_lastIndex); + m_lastIndex = -1; + resetModel(); + } + + if (m_propertyChangesRemoved) { + m_propertyChangesRemoved = false; + resetPropertyChangesModels(); + } + + if (m_stateGroupRemoved) { + m_stateGroupRemoved = false; + resetStateGroups(); + } +} + +void StatesEditorView::nodeAboutToBeReparented(const ModelNode &node, + const NodeAbstractProperty & /*newPropertyParent*/, + const NodeAbstractProperty &oldPropertyParent, + AbstractView::PropertyChangeFlags /*propertyChange*/) +{ + if (oldPropertyParent.isValid() + && oldPropertyParent.parentModelNode() == activeStateGroup().modelNode() + && oldPropertyParent.name() == "states") + m_lastIndex = oldPropertyParent.indexOf(node); + + if (node.simplifiedTypeName() == "StateGroup") + resetStateGroups(); +} + +void StatesEditorView::nodeReparented(const ModelNode &node, + const NodeAbstractProperty &newPropertyParent, + const NodeAbstractProperty &oldPropertyParent, + AbstractView::PropertyChangeFlags /*propertyChange*/) +{ + if (oldPropertyParent.isValid() + && oldPropertyParent.parentModelNode() == activeStateGroup().modelNode() + && oldPropertyParent.name() == "states") { + m_statesEditorModel->removeState(m_lastIndex); + resetModel(); + m_lastIndex = -1; + } + + if (newPropertyParent.isValid() + && newPropertyParent.parentModelNode() == activeStateGroup().modelNode() + && newPropertyParent.name() == "states") { + int index = newPropertyParent.indexOf(node); + m_statesEditorModel->insertState(index); + } + + if (node.simplifiedTypeName() == "PropertyChanges") + resetPropertyChangesModels(); +} + +void StatesEditorView::nodeOrderChanged(const NodeListProperty &listProperty) +{ + if (m_block) + return; + + if (listProperty.isValid() && listProperty.parentModelNode() == activeStateGroup().modelNode() + && listProperty.name() == "states") + resetModel(); +} + +void StatesEditorView::bindingPropertiesChanged(const QList &propertyList, AbstractView::PropertyChangeFlags propertyChange) +{ + Q_UNUSED(propertyChange) + + for (const BindingProperty &property : propertyList) { + if (property.name() == "when" + && QmlModelState::isValidQmlModelState(property.parentModelNode())) + resetModel(); + if (property.parentModelNode().simplifiedTypeName() == "PropertyChanges") + resetPropertyChangesModels(); + } +} + +void StatesEditorView::variantPropertiesChanged(const QList &propertyList, + AbstractView::PropertyChangeFlags /*propertyChange*/) +{ + if (m_block) + return; + + m_block = true; + auto guard = qScopeGuard([&]() { m_block = false; }); + + for (const VariantProperty &property : propertyList) { + if (property.name() == "name" && QmlModelState::isValidQmlModelState(property.parentModelNode())) + resetModel(); + else if (property.name() == "state" + && property.parentModelNode() == activeStateGroup().modelNode()) + resetModel(); + else if (property.name() == "extend") + resetExtend(); + + if (property.parentModelNode().simplifiedTypeName() == "PropertyChanges") + resetPropertyChangesModels(); + } +} + +void StatesEditorView::customNotification(const AbstractView * /*view*/, + const QString &identifier, + const QList & /*nodeList*/, + const QList & /*data*/) +{ + if (identifier == StartRewriterAmend) + beginBulkChange(); + + if (identifier == EndRewriterAmend) + endBulkChange(); +} + +void StatesEditorView::rewriterBeginTransaction() +{ + beginBulkChange(); +} + +void StatesEditorView::rewriterEndTransaction() +{ + endBulkChange(); +} + +void StatesEditorView::currentStateChanged(const ModelNode &node) +{ + QmlModelState newQmlModelState(node); + + if (newQmlModelState.isBaseState()) + m_statesEditorWidget->setCurrentStateInternalId(0); + else + m_statesEditorWidget->setCurrentStateInternalId(newQmlModelState.modelNode().internalId()); +} + +void StatesEditorView::instancesPreviewImageChanged(const QVector &nodeList) +{ + if (!model()) + return; + + int minimumIndex = 10000; + int maximumIndex = -1; + for (const ModelNode &node : nodeList) { + if (node.isRootNode()) { + minimumIndex = qMin(minimumIndex, 0); + maximumIndex = qMax(maximumIndex, 0); + } else { + int index = activeStateGroup().allStates().indexOf(QmlModelState(node)) + 1; + if (index > 0) { + minimumIndex = qMin(minimumIndex, index); + maximumIndex = qMax(maximumIndex, index); + } + } + } + + if (maximumIndex >= 0) + m_statesEditorModel->updateState(minimumIndex, maximumIndex); +} + +void StatesEditorView::moveStates(int from, int to) +{ + if (m_block) + return; + + m_block = true; + auto guard = qScopeGuard([&]() { m_block = false; }); + + if (!activeStatesGroupNode().hasNodeListProperty("states")) + return; + + executeInTransaction("moveState", [this, from, to]() { + activeStatesGroupNode().nodeListProperty("states").slide(from - 1, to - 1); + }); +} + +} // namespace Experimental +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/stateseditornew/stateseditorview.h b/src/plugins/qmldesigner/components/stateseditornew/stateseditorview.h new file mode 100644 index 00000000000..5fc9ae5511a --- /dev/null +++ b/src/plugins/qmldesigner/components/stateseditornew/stateseditorview.h @@ -0,0 +1,158 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include + +#include + +#include + +namespace QmlDesigner { + +class AnnotationEditor; + +namespace Experimental { + +class StatesEditorModel; +class StatesEditorWidget; +class PropertyChangesModel; + +class StatesEditorView : public AbstractView { + Q_OBJECT + +public: + explicit StatesEditorView(QObject *parent = nullptr); + ~StatesEditorView() override; + + void renameState(int internalNodeId,const QString &newName); + void setWhenCondition(int internalNodeId, const QString &condition); + void resetWhenCondition(int internalNodeId); + void setStateAsDefault(int internalNodeId); + void resetDefaultState(); + bool hasDefaultState() const; + void setAnnotation(int internalNodeId); + void removeAnnotation(int internalNodeId); + bool hasAnnotation(int internalNodeId) const; + bool validStateName(const QString &name) const; + bool hasExtend() const; + QStringList extendedStates() const; + QString currentStateName() const; + void setCurrentState(const QmlModelState &state); + QmlModelState baseState() const; + QmlModelStateGroup activeStateGroup() const; + + void moveStates(int from, int to); + + // AbstractView + void modelAttached(Model *model) override; + void modelAboutToBeDetached(Model *model) override; + void propertiesRemoved(const QList& propertyList) override; + void nodeAboutToBeRemoved(const ModelNode &removedNode) override; + void nodeRemoved(const ModelNode &removedNode, + const NodeAbstractProperty &parentProperty, + PropertyChangeFlags propertyChange) override; + void nodeAboutToBeReparented(const ModelNode &node, + const NodeAbstractProperty &newPropertyParent, + const NodeAbstractProperty &oldPropertyParent, + AbstractView::PropertyChangeFlags propertyChange) override; + void nodeReparented(const ModelNode &node, + const NodeAbstractProperty &newPropertyParent, + const NodeAbstractProperty &oldPropertyParent, + AbstractView::PropertyChangeFlags propertyChange) override; + void nodeOrderChanged(const NodeListProperty &listProperty) override; + void bindingPropertiesChanged(const QList &propertyList, + PropertyChangeFlags propertyChange) override; + void variantPropertiesChanged(const QList &propertyList, + PropertyChangeFlags propertyChange) override; + + void customNotification(const AbstractView *view, + const QString &identifier, + const QList &nodeList, + const QList &data) override; + void rewriterBeginTransaction() override; + void rewriterEndTransaction() override; + + // AbstractView + void currentStateChanged(const ModelNode &node) override; + + void instancesPreviewImageChanged(const QVector &nodeList) override; + + WidgetInfo widgetInfo() override; + + void rootNodeTypeChanged(const QString &type, int majorVersion, int minorVersion) override; + + ModelNode activeStatesGroupNode() const; + void setActiveStatesGroupNode(const ModelNode &modelNode); + + int activeStatesGroupIndex() const; + void setActiveStatesGroupIndex(int index); + + void registerPropertyChangesModel(PropertyChangesModel *model); + void deregisterPropertyChangesModel(PropertyChangesModel *model); + +public slots: + void synchonizeCurrentStateFromWidget(); + void createNewState(); + void cloneState(int nodeId); + void extendState(int nodeId); + void removeState(int nodeId); + +private: + StatesEditorWidget *statesEditorWidget() const; + void resetModel(); + void resetPropertyChangesModels(); + void resetExtend(); + void resetStateGroups(); + + void checkForStatesAvailability(); + + void beginBulkChange(); + void endBulkChange(); + +private: + QPointer m_statesEditorModel; + QPointer m_statesEditorWidget; + int m_lastIndex; + bool m_block = false; + QPointer m_editor; + ModelNode m_activeStatesGroupNode; + + bool m_propertyChangesRemoved = false; + bool m_stateGroupRemoved = false; + + bool m_bulkChange = false; + + bool m_modelDirty = false; + bool m_extendDirty = false; + bool m_propertyChangesDirty = false; + bool m_stateGroupsDirty = false; + + QSet m_propertyChangedModels; +}; + +} // namespace Experimental +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/stateseditornew/stateseditorwidget.cpp b/src/plugins/qmldesigner/components/stateseditornew/stateseditorwidget.cpp new file mode 100644 index 00000000000..8789772a2e3 --- /dev/null +++ b/src/plugins/qmldesigner/components/stateseditornew/stateseditorwidget.cpp @@ -0,0 +1,199 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "stateseditorwidget.h" +#include "stateseditormodel.h" +#include "stateseditorview.h" +#include "stateseditorimageprovider.h" + +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include +#include + +enum { + debug = false +}; + +namespace QmlDesigner { +namespace Experimental { + +static QString propertyEditorResourcesPath() +{ +#ifdef SHARE_QML_PATH + if (qEnvironmentVariableIsSet("LOAD_QML_FROM_SOURCE")) + return QLatin1String(SHARE_QML_PATH) + "/propertyEditorQmlSources"; +#endif + return Core::ICore::resourcePath("qmldesigner/propertyEditorQmlSources").toString(); +} + +int StatesEditorWidget::currentStateInternalId() const +{ + QTC_ASSERT(rootObject(), return -1); + QTC_ASSERT(rootObject()->property("currentStateInternalId").isValid(), return -1); + + return rootObject()->property("currentStateInternalId").toInt(); +} + +void StatesEditorWidget::setCurrentStateInternalId(int internalId) +{ + QTC_ASSERT(rootObject(), return); + rootObject()->setProperty("currentStateInternalId", internalId); +} + +void StatesEditorWidget::setNodeInstanceView(NodeInstanceView *nodeInstanceView) +{ + m_imageProvider->setNodeInstanceView(nodeInstanceView); +} + +void StatesEditorWidget::showAddNewStatesButton(bool showAddNewStatesButton) +{ + rootContext()->setContextProperty(QLatin1String("canAddNewStates"), showAddNewStatesButton); +} + +StatesEditorWidget::StatesEditorWidget(StatesEditorView *statesEditorView, + StatesEditorModel *statesEditorModel) + : m_statesEditorView(statesEditorView) + , m_imageProvider(nullptr) + , m_qmlSourceUpdateShortcut(nullptr) +{ + m_imageProvider = new Internal::StatesEditorImageProvider; + m_imageProvider->setNodeInstanceView(statesEditorView->nodeInstanceView()); + + engine()->addImageProvider(QStringLiteral("qmldesigner_stateseditor"), m_imageProvider); + engine()->addImportPath(qmlSourcesPath()); + engine()->addImportPath(propertyEditorResourcesPath() + "/imports"); + engine()->addImportPath(qmlSourcesPath() + "/imports"); + + m_qmlSourceUpdateShortcut = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_F10), this); + connect(m_qmlSourceUpdateShortcut, &QShortcut::activated, this, &StatesEditorWidget::reloadQmlSource); + + setResizeMode(QQuickWidget::SizeRootObjectToView); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + rootContext()->setContextProperties( + QVector{{{"statesEditorModel"}, + QVariant::fromValue(statesEditorModel)}, + {{"canAddNewStates"}, true}}); + + Theme::setupTheme(engine()); + + setWindowTitle(tr("States New", "Title of Editor widget")); + setMinimumWidth(195); + setMinimumHeight(195); + + // init the first load of the QML UI elements + reloadQmlSource(); +} + +StatesEditorWidget::~StatesEditorWidget() = default; + +QString StatesEditorWidget::qmlSourcesPath() +{ +#ifdef SHARE_QML_PATH + if (qEnvironmentVariableIsSet("LOAD_QML_FROM_SOURCE")) + return QLatin1String(SHARE_QML_PATH) + "/newstateseditor"; +#endif + return Core::ICore::resourcePath("qmldesigner/newstateseditor").toString(); +} + +void StatesEditorWidget::showEvent(QShowEvent *event) +{ + QQuickWidget::showEvent(event); + update(); +} + +void StatesEditorWidget::focusOutEvent(QFocusEvent *focusEvent) +{ + QmlDesignerPlugin::emitUsageStatisticsTime(Constants::EVENT_STATESEDITOR_TIME, + m_usageTimer.elapsed()); + QQuickWidget::focusOutEvent(focusEvent); +} + +void StatesEditorWidget::focusInEvent(QFocusEvent *focusEvent) +{ + m_usageTimer.restart(); + QQuickWidget::focusInEvent(focusEvent); +} + +void StatesEditorWidget::reloadQmlSource() +{ + QString statesListQmlFilePath = qmlSourcesPath() + QStringLiteral("/Main.qml"); + QTC_ASSERT(QFileInfo::exists(statesListQmlFilePath), return ); + engine()->clearComponentCache(); + setSource(QUrl::fromLocalFile(statesListQmlFilePath)); + + if (!rootObject()) { + QString errorString; + for (const QQmlError &error : errors()) + errorString += "\n" + error.toString(); + + Core::AsynchronousMessageBox::warning(tr("Cannot Create QtQuick View"), + tr("StatesEditorWidget: %1 cannot be created.%2") + .arg(qmlSourcesPath(), errorString)); + return; + } + + connect(rootObject(), + SIGNAL(currentStateInternalIdChanged()), + m_statesEditorView.data(), + SLOT(synchonizeCurrentStateFromWidget())); + connect(rootObject(), + SIGNAL(createNewState()), + m_statesEditorView.data(), + SLOT(createNewState())); + connect(rootObject(), SIGNAL(cloneState(int)), m_statesEditorView.data(), SLOT(cloneState(int))); + connect(rootObject(), + SIGNAL(extendState(int)), + m_statesEditorView.data(), + SLOT(extendState(int))); + connect(rootObject(), + SIGNAL(deleteState(int)), + m_statesEditorView.data(), + SLOT(removeState(int))); + m_statesEditorView.data()->synchonizeCurrentStateFromWidget(); +} + +} // namespace Experimental +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/stateseditornew/stateseditorwidget.h b/src/plugins/qmldesigner/components/stateseditornew/stateseditorwidget.h new file mode 100644 index 00000000000..9bdea2e4aae --- /dev/null +++ b/src/plugins/qmldesigner/components/stateseditornew/stateseditorwidget.h @@ -0,0 +1,80 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE +class QShortcut; +QT_END_NAMESPACE + +namespace QmlDesigner { + +class NodeInstanceView; + +namespace Experimental { + +class StatesEditorModel; +class StatesEditorView; + +namespace Internal { class StatesEditorImageProvider; } + +class StatesEditorWidget : public QQuickWidget +{ + Q_OBJECT + +public: + StatesEditorWidget(StatesEditorView *m_statesEditorView, StatesEditorModel *statesEditorModel); + ~StatesEditorWidget() override; + + int currentStateInternalId() const; + void setCurrentStateInternalId(int internalId); + void setNodeInstanceView(NodeInstanceView *nodeInstanceView); + + void showAddNewStatesButton(bool showAddNewStatesButton); + + static QString qmlSourcesPath(); + +protected: + void showEvent(QShowEvent *) override; + void focusOutEvent(QFocusEvent *focusEvent) override; + void focusInEvent(QFocusEvent *focusEvent) override; + +private: + void reloadQmlSource(); + +private: + QPointer m_statesEditorView; + Internal::StatesEditorImageProvider *m_imageProvider; + QShortcut *m_qmlSourceUpdateShortcut; + QElapsedTimer m_usageTimer; +}; + +} // namespace Experimental +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/componentsplugin/componentsplugin.qbs b/src/plugins/qmldesigner/componentsplugin/componentsplugin.qbs index 520e533af5b..194591a2732 100644 --- a/src/plugins/qmldesigner/componentsplugin/componentsplugin.qbs +++ b/src/plugins/qmldesigner/componentsplugin/componentsplugin.qbs @@ -24,6 +24,7 @@ QtcProduct { "../components/navigator", "../components/propertyeditor", "../components/stateseditor", + "../components/stateseditornew", "../designercore", "../designercore/include", "../../../../share/qtcreator/qml/qmlpuppet/interfaces", diff --git a/src/plugins/qmldesigner/designercore/include/qmlchangeset.h b/src/plugins/qmldesigner/designercore/include/qmlchangeset.h index a9af4e18796..3052ab6fd1c 100644 --- a/src/plugins/qmldesigner/designercore/include/qmlchangeset.h +++ b/src/plugins/qmldesigner/designercore/include/qmlchangeset.h @@ -31,19 +31,23 @@ namespace QmlDesigner { -class QMLDESIGNERCORE_EXPORT QmlModelStateOperation : public QmlModelNodeFacade +class QMLDESIGNERCORE_EXPORT QmlModelStateOperation : public QmlModelNodeFacade { public: QmlModelStateOperation() : QmlModelNodeFacade() {} QmlModelStateOperation(const ModelNode &modelNode) : QmlModelNodeFacade(modelNode) {} ModelNode target() const; void setTarget(const ModelNode &target); + bool explicitValue() const; + void setExplicitValue(bool value); + bool restoreEntryValues() const; + void setRestoreEntryValues(bool value); + QList targetProperties() const; bool isValid() const override; static bool isValidQmlModelStateOperation(const ModelNode &modelNode); }; - -class QMLDESIGNERCORE_EXPORT QmlPropertyChanges : public QmlModelStateOperation +class QMLDESIGNERCORE_EXPORT QmlPropertyChanges : public QmlModelStateOperation { public: QmlPropertyChanges() : QmlModelStateOperation() {} diff --git a/src/plugins/qmldesigner/designercore/include/qmlstate.h b/src/plugins/qmldesigner/designercore/include/qmlstate.h index ce9a8bf3f74..3e8da337342 100644 --- a/src/plugins/qmldesigner/designercore/include/qmlstate.h +++ b/src/plugins/qmldesigner/designercore/include/qmlstate.h @@ -38,9 +38,14 @@ class Annotation; class AnnotationEditor; class StatesEditorView; +namespace Experimental { +class StatesEditorView; +} + class QMLDESIGNERCORE_EXPORT QmlModelState : public QmlModelNodeFacade { friend StatesEditorView; + friend Experimental::StatesEditorView; public: QmlModelState(); @@ -82,6 +87,10 @@ public: bool hasAnnotation() const; void removeAnnotation(); + QString extend() const; + void setExtend(const QString &name); + bool hasExtend() const; + protected: void addChangeSetIfNotExists(const ModelNode &node); static QmlModelState createBaseState(const AbstractView *view); diff --git a/src/plugins/qmldesigner/designercore/include/qmlvisualnode.h b/src/plugins/qmldesigner/designercore/include/qmlvisualnode.h index da925e4f110..5c62afe68e0 100644 --- a/src/plugins/qmldesigner/designercore/include/qmlvisualnode.h +++ b/src/plugins/qmldesigner/designercore/include/qmlvisualnode.h @@ -122,6 +122,7 @@ class QMLDESIGNERCORE_EXPORT QmlModelStateGroup { friend class QmlObjectNode; friend class StatesEditorView; + friend class Experimental::StatesEditorView; public: diff --git a/src/plugins/qmldesigner/designercore/model/qmlchangeset.cpp b/src/plugins/qmldesigner/designercore/model/qmlchangeset.cpp index cb9a43753e5..cfc8d8cb996 100644 --- a/src/plugins/qmldesigner/designercore/model/qmlchangeset.cpp +++ b/src/plugins/qmldesigner/designercore/model/qmlchangeset.cpp @@ -29,6 +29,8 @@ #include "abstractview.h" #include +#include + namespace QmlDesigner { ModelNode QmlModelStateOperation::target() const @@ -44,6 +46,40 @@ void QmlModelStateOperation::setTarget(const ModelNode &target) modelNode().bindingProperty("target").setExpression(target.id()); } +bool QmlModelStateOperation::explicitValue() const +{ + if (modelNode().property("explicit").isVariantProperty()) + return modelNode().variantProperty("explicit").value().toBool(); + + return false; +} + +void QmlModelStateOperation::setExplicitValue(bool value) +{ + modelNode().variantProperty("explicit").setValue(value); +} + +bool QmlModelStateOperation::restoreEntryValues() const +{ + if (modelNode().property("restoreEntryValues").isVariantProperty()) + return modelNode().variantProperty("restoreEntryValues").value().toBool(); + + return false; +} + +void QmlModelStateOperation::setRestoreEntryValues(bool value) +{ + modelNode().variantProperty("restoreEntryValues").setValue(value); +} + +QList QmlModelStateOperation::targetProperties() const +{ + return Utils::filtered(modelNode().properties(), [](const AbstractProperty &property) { + const QList ignore = {"target", "explicit", "restoreEntryValues"}; + return !ignore.contains(property.name()); + }); +} + bool QmlPropertyChanges::isValid() const { return isValidQmlPropertyChanges(modelNode()); diff --git a/src/plugins/qmldesigner/designercore/model/qmlstate.cpp b/src/plugins/qmldesigner/designercore/model/qmlstate.cpp index a4ce30278e2..f287df53b9b 100644 --- a/src/plugins/qmldesigner/designercore/model/qmlstate.cpp +++ b/src/plugins/qmldesigner/designercore/model/qmlstate.cpp @@ -44,7 +44,7 @@ QmlModelState::QmlModelState() } QmlModelState::QmlModelState(const ModelNode &modelNode) - : QmlModelNodeFacade(modelNode) + : QmlModelNodeFacade(modelNode) { } @@ -70,7 +70,7 @@ QList QmlModelState::stateOperations(const ModelNode &no { QList returnList; - if (!isBaseState() && modelNode().hasNodeListProperty("changes")) { + if (!isBaseState() && modelNode().hasNodeListProperty("changes")) { const QList nodes = modelNode().nodeListProperty("changes").toModelNodeList(); for (const ModelNode &childNode : nodes) { if (QmlModelStateOperation::isValidQmlModelStateOperation(childNode)) { @@ -89,7 +89,7 @@ QList QmlModelState::propertyChanges() const { QList returnList; - if (!isBaseState() && modelNode().hasNodeListProperty("changes")) { + if (!isBaseState() && modelNode().hasNodeListProperty("changes")) { const QList nodes = modelNode().nodeListProperty("changes").toModelNodeList(); for (const ModelNode &childNode : nodes) { //### exception if not valid QmlModelStateOperation @@ -104,7 +104,7 @@ QList QmlModelState::propertyChanges() const bool QmlModelState::hasPropertyChanges(const ModelNode &node) const { - if (!isBaseState() && modelNode().hasNodeListProperty("changes")) { + if (!isBaseState() && modelNode().hasNodeListProperty("changes")) { const QList changes = propertyChanges(); for (const QmlPropertyChanges &changeSet : changes) { if (changeSet.target().isValid() && changeSet.target() == node) @@ -132,7 +132,7 @@ QList QmlModelState::stateOperations() const //### exception if not valid QList returnList; - if (!isBaseState() && modelNode().hasNodeListProperty("changes")) { + if (!isBaseState() && modelNode().hasNodeListProperty("changes")) { const QList nodes = modelNode().nodeListProperty("changes").toModelNodeList(); for (const ModelNode &childNode : nodes) { //### exception if not valid QmlModelStateOperation @@ -368,6 +368,28 @@ void QmlModelState::removeAnnotation() } } +QString QmlModelState::extend() const +{ + if (isBaseState()) + return QString(); + + return modelNode().variantProperty("extend").value().toString(); +} + +void QmlModelState::setExtend(const QString &name) +{ + if ((!isBaseState()) && (modelNode().isValid())) + modelNode().variantProperty("extend").setValue(name); +} + +bool QmlModelState::hasExtend() const +{ + if (!isBaseState() && modelNode().isValid()) + return modelNode().hasVariantProperty("extend"); + + return false; +} + QmlModelState QmlModelState::createBaseState(const AbstractView *view) { QmlModelState qmlModelState(view->rootModelNode()); diff --git a/src/plugins/qmldesigner/designercore/model/texttomodelmerger.cpp b/src/plugins/qmldesigner/designercore/model/texttomodelmerger.cpp index 6e7884fca29..fa56d855f80 100644 --- a/src/plugins/qmldesigner/designercore/model/texttomodelmerger.cpp +++ b/src/plugins/qmldesigner/designercore/model/texttomodelmerger.cpp @@ -1960,7 +1960,7 @@ void ModelValidator::typeDiffers(bool /*isRootNode*/, Q_UNUSED(minorVersion) Q_UNUSED(majorVersion) - QTC_ASSERT(modelNode.type() == typeName, return); + QTC_ASSERT(modelNode.type() == typeName, return ); if (modelNode.majorVersion() != majorVersion) { qDebug() << Q_FUNC_INFO << modelNode; diff --git a/src/plugins/qmldesigner/designercore/model/viewmanager.cpp b/src/plugins/qmldesigner/designercore/model/viewmanager.cpp index d1b53b05ae2..c3fb2118547 100644 --- a/src/plugins/qmldesigner/designercore/model/viewmanager.cpp +++ b/src/plugins/qmldesigner/designercore/model/viewmanager.cpp @@ -28,6 +28,7 @@ #ifndef QMLDESIGNER_TEST #include +#include #include #include #include @@ -38,13 +39,13 @@ #include #include #include -#include +#include +#include #include #include #include -#include -#include #include +#include #include #include #include @@ -86,6 +87,7 @@ public: MaterialEditorView materialEditorView; MaterialBrowserView materialBrowserView; StatesEditorView statesEditorView; + Experimental::StatesEditorView newStatesEditorView; std::vector> additionalViews; bool disableStandardViews = false; @@ -168,12 +170,22 @@ void ViewManager::switchStateEditorViewToBaseState() d->savedState = d->statesEditorView.currentState(); d->statesEditorView.setCurrentState(d->statesEditorView.baseState()); } + + // TODO Settings branch + if (d->newStatesEditorView.isAttached()) { + d->savedState = d->newStatesEditorView.currentState(); + d->newStatesEditorView.setCurrentState(d->newStatesEditorView.baseState()); + } } void ViewManager::switchStateEditorViewToSavedState() { if (d->savedState.isValid() && d->statesEditorView.isAttached()) d->statesEditorView.setCurrentState(d->savedState); + + // TODO Settings branch + if (d->savedState.isValid() && d->newStatesEditorView.isAttached()) + d->newStatesEditorView.setCurrentState(d->savedState); } QList ViewManager::views() const @@ -196,6 +208,7 @@ QList ViewManager::standardViews() const &d->materialEditorView, &d->materialBrowserView, &d->statesEditorView, + &d->newStatesEditorView, // TODO &d->designerActionManagerView}; if (QmlDesignerPlugin::instance()->settings().value( @@ -331,6 +344,7 @@ QList ViewManager::widgetInfos() const widgetInfoList.append(d->materialEditorView.widgetInfo()); widgetInfoList.append(d->materialBrowserView.widgetInfo()); widgetInfoList.append(d->statesEditorView.widgetInfo()); + widgetInfoList.append(d->newStatesEditorView.widgetInfo()); // TODO if (d->debugView.hasWidget()) widgetInfoList.append(d->debugView.widgetInfo()); diff --git a/src/plugins/qmldesigner/qmldesignerconstants.h b/src/plugins/qmldesigner/qmldesignerconstants.h index 2ffb56fb533..5d522dfbaf0 100644 --- a/src/plugins/qmldesigner/qmldesignerconstants.h +++ b/src/plugins/qmldesigner/qmldesignerconstants.h @@ -112,6 +112,8 @@ const int MODELNODE_PREVIEW_IMAGE_DIMENSIONS = 150; const char EVENT_TIMELINE_ADDED[] = "timelineAdded"; const char EVENT_TRANSITION_ADDED[] = "transitionAdded"; const char EVENT_STATE_ADDED[] = "stateAdded"; +const char EVENT_STATE_CLONED[] = "stateCloned"; +const char EVENT_STATE_EXTENDED[] = "stateExtended"; const char EVENT_CONNECTION_ADDED[] = "connectionAdded"; const char EVENT_PROPERTY_ADDED[] = "propertyAdded"; const char EVENT_ANNOTATION_ADDED[] = "annotationAdded"; diff --git a/src/plugins/qmldesigner/qmldesignerplugin.qbs b/src/plugins/qmldesigner/qmldesignerplugin.qbs index 9bbfd1ce8b0..1744520f5b0 100644 --- a/src/plugins/qmldesigner/qmldesignerplugin.qbs +++ b/src/plugins/qmldesigner/qmldesignerplugin.qbs @@ -59,6 +59,7 @@ Project { "components/navigator", "components/pluginmanager", "components/stateseditor", + "components/stateseditornew", "components/texteditor", "components/timelineeditor", "components/listmodeleditor", @@ -777,6 +778,18 @@ Project { "stateseditor/stateseditorview.h", "stateseditor/stateseditorwidget.cpp", "stateseditor/stateseditorwidget.h", + "stateseditornew/propertychangesmodel.cpp", + "stateseditornew/propertychangesmodel.h", + "stateseditornew/propertymodel.cpp", + "stateseditornew/propertymodel.h", + "stateseditornew/stateseditorimageprovider.cpp", + "stateseditornew/stateseditorimageprovider.h", + "stateseditornew/stateseditormodel.cpp", + "stateseditornew/stateseditormodel.h", + "stateseditornew/stateseditorview.cpp", + "stateseditornew/stateseditorview.h", + "stateseditornew/stateseditorwidget.cpp", + "stateseditornew/stateseditorwidget.h", ] } From f639f72c14a3cbfc4eefd6db93963551df03e0d7 Mon Sep 17 00:00:00 2001 From: Thomas Hartmann Date: Mon, 19 Sep 2022 17:07:40 +0200 Subject: [PATCH 04/12] QmlDesigner: Add QmlObjectNode::getAllConnections Change-Id: Id6fa6ecdf1173eea1087057f4317dd1a9c3f6b17 Reviewed-by: Aleksei German --- .../designercore/include/qmlobjectnode.h | 2 ++ .../designercore/model/qmlobjectnode.cpp | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/plugins/qmldesigner/designercore/include/qmlobjectnode.h b/src/plugins/qmldesigner/designercore/include/qmlobjectnode.h index a37dbe77532..63e36e22f12 100644 --- a/src/plugins/qmldesigner/designercore/include/qmlobjectnode.h +++ b/src/plugins/qmldesigner/designercore/include/qmlobjectnode.h @@ -140,6 +140,8 @@ public: QList allTimelines() const; + QList getAllConnections() const; + protected: NodeInstance nodeInstance() const; QmlObjectNode nodeForInstance(const NodeInstance &instance) const; diff --git a/src/plugins/qmldesigner/designercore/model/qmlobjectnode.cpp b/src/plugins/qmldesigner/designercore/model/qmlobjectnode.cpp index 9925af902a7..64a562c0296 100644 --- a/src/plugins/qmldesigner/designercore/model/qmlobjectnode.cpp +++ b/src/plugins/qmldesigner/designercore/model/qmlobjectnode.cpp @@ -45,6 +45,7 @@ #include #endif +#include #include #include @@ -608,6 +609,18 @@ QList QmlObjectNode::allTimelines() const return timelineNodes; } +QList QmlObjectNode::getAllConnections() const +{ + if (!isValid()) + return {}; + + auto list = view()->allModelNodesOfType("QtQuick.Connections"); + return Utils::filtered(list, [this](const ModelNode &connection) { + return connection.hasBindingProperty("target") + && connection.bindingProperty("target").resolveToModelNode() == modelNode(); + }); +} + /*! Removes a variant property of the object specified by \a name from the model. From bbffd88c1bc772068b72e0d271767001164ffa50 Mon Sep 17 00:00:00 2001 From: Thomas Hartmann Date: Mon, 19 Sep 2022 16:53:32 +0200 Subject: [PATCH 05/12] QmlDesigner: Add setting for new states editor This makes it easy to enable/disable the new states editor. [QML} Designer\OldStatesEditor=false Change-Id: Ib3545bf0be8547af06eb0e9dda0cee2a143659b1 Reviewed-by: Qt CI Bot Reviewed-by: Aleksei German --- .../stateseditornew/stateseditorview.cpp | 4 +- .../designercore/model/viewmanager.cpp | 62 +++++++++++++------ src/plugins/qmldesigner/designersettings.cpp | 1 + src/plugins/qmldesigner/designersettings.h | 1 + 4 files changed, 46 insertions(+), 22 deletions(-) diff --git a/src/plugins/qmldesigner/components/stateseditornew/stateseditorview.cpp b/src/plugins/qmldesigner/components/stateseditornew/stateseditorview.cpp index 6d3d853a987..9a0af786c38 100644 --- a/src/plugins/qmldesigner/components/stateseditornew/stateseditorview.cpp +++ b/src/plugins/qmldesigner/components/stateseditornew/stateseditorview.cpp @@ -80,10 +80,10 @@ WidgetInfo StatesEditorView::widgetInfo() m_statesEditorWidget = new StatesEditorWidget(this, m_statesEditorModel.data()); return createWidgetInfo(m_statesEditorWidget.data(), - QLatin1String("StatesEditorNew"), + "StatesEditor", WidgetInfo::BottomPane, 0, - tr("States New")); + tr("States")); } void StatesEditorView::rootNodeTypeChanged(const QString &/*type*/, int /*majorVersion*/, int /*minorVersion*/) diff --git a/src/plugins/qmldesigner/designercore/model/viewmanager.cpp b/src/plugins/qmldesigner/designercore/model/viewmanager.cpp index c3fb2118547..4ecf8aec291 100644 --- a/src/plugins/qmldesigner/designercore/model/viewmanager.cpp +++ b/src/plugins/qmldesigner/designercore/model/viewmanager.cpp @@ -58,6 +58,14 @@ namespace QmlDesigner { +static bool useOldStatesEditor() +{ + return QmlDesignerPlugin::instance() + ->settings() + .value(DesignerSettingsKey::OLD_STATES_EDITOR) + .toBool(); +} + static Q_LOGGING_CATEGORY(viewBenchmark, "qtc.viewmanager.attach", QtWarningMsg) class ViewManagerData @@ -166,26 +174,30 @@ void ViewManager::detachRewriterView() void ViewManager::switchStateEditorViewToBaseState() { - if (d->statesEditorView.isAttached()) { - d->savedState = d->statesEditorView.currentState(); - d->statesEditorView.setCurrentState(d->statesEditorView.baseState()); - } - - // TODO Settings branch - if (d->newStatesEditorView.isAttached()) { - d->savedState = d->newStatesEditorView.currentState(); - d->newStatesEditorView.setCurrentState(d->newStatesEditorView.baseState()); + if (useOldStatesEditor()) { + if (d->statesEditorView.isAttached()) { + d->savedState = d->statesEditorView.currentState(); + d->statesEditorView.setCurrentState(d->statesEditorView.baseState()); + } + } else { + // TODO remove old statesview + if (d->newStatesEditorView.isAttached()) { + d->savedState = d->newStatesEditorView.currentState(); + d->newStatesEditorView.setCurrentState(d->newStatesEditorView.baseState()); + } } } void ViewManager::switchStateEditorViewToSavedState() { - if (d->savedState.isValid() && d->statesEditorView.isAttached()) - d->statesEditorView.setCurrentState(d->savedState); - - // TODO Settings branch - if (d->savedState.isValid() && d->newStatesEditorView.isAttached()) - d->newStatesEditorView.setCurrentState(d->savedState); + if (useOldStatesEditor()) { + if (d->savedState.isValid() && d->statesEditorView.isAttached()) + d->statesEditorView.setCurrentState(d->savedState); + } else { + // TODO remove old statesview + if (d->savedState.isValid() && d->newStatesEditorView.isAttached()) + d->newStatesEditorView.setCurrentState(d->savedState); + } } QList ViewManager::views() const @@ -211,9 +223,16 @@ QList ViewManager::standardViews() const &d->newStatesEditorView, // TODO &d->designerActionManagerView}; - if (QmlDesignerPlugin::instance()->settings().value( - DesignerSettingsKey::ENABLE_DEBUGVIEW).toBool()) - list.append(&d->debugView); + if (useOldStatesEditor()) + list.removeAll(&d->newStatesEditorView); + else + list.removeAll(&d->statesEditorView); + + if (QmlDesignerPlugin::instance() + ->settings() + .value(DesignerSettingsKey::ENABLE_DEBUGVIEW) + .toBool()) + list.append(&d->debugView); return list; } @@ -343,8 +362,11 @@ QList ViewManager::widgetInfos() const widgetInfoList.append(d->propertyEditorView.widgetInfo()); widgetInfoList.append(d->materialEditorView.widgetInfo()); widgetInfoList.append(d->materialBrowserView.widgetInfo()); - widgetInfoList.append(d->statesEditorView.widgetInfo()); - widgetInfoList.append(d->newStatesEditorView.widgetInfo()); // TODO + if (useOldStatesEditor()) + widgetInfoList.append(d->statesEditorView.widgetInfo()); + else + widgetInfoList.append(d->newStatesEditorView.widgetInfo()); + if (d->debugView.hasWidget()) widgetInfoList.append(d->debugView.widgetInfo()); diff --git a/src/plugins/qmldesigner/designersettings.cpp b/src/plugins/qmldesigner/designersettings.cpp index c1b69574d91..8841267385e 100644 --- a/src/plugins/qmldesigner/designersettings.cpp +++ b/src/plugins/qmldesigner/designersettings.cpp @@ -85,6 +85,7 @@ void DesignerSettings::fromSettings(QSettings *settings) restoreValue(settings, DesignerSettingsKey::EDIT3DVIEW_GRID_COLOR, "#aaaaaa"); restoreValue(settings, DesignerSettingsKey::SMOOTH_RENDERING, false); restoreValue(settings, DesignerSettingsKey::SHOW_DEBUG_SETTINGS, false); + restoreValue(settings, DesignerSettingsKey::OLD_STATES_EDITOR, true); settings->endGroup(); settings->endGroup(); diff --git a/src/plugins/qmldesigner/designersettings.h b/src/plugins/qmldesigner/designersettings.h index f39268b1869..851a5e8ef24 100644 --- a/src/plugins/qmldesigner/designersettings.h +++ b/src/plugins/qmldesigner/designersettings.h @@ -74,6 +74,7 @@ const char ALWAYS_DESIGN_MODE[] = "AlwaysDesignMode"; const char DISABLE_ITEM_LIBRARY_UPDATE_TIMER[] = "DisableItemLibraryUpdateTimer"; const char ASK_BEFORE_DELETING_ASSET[] = "AskBeforeDeletingAsset"; const char SMOOTH_RENDERING[] = "SmoothRendering"; +const char OLD_STATES_EDITOR[] = "OldStatesEditor"; } class QMLDESIGNERCORE_EXPORT DesignerSettings : public QHash From a7b18f7860c5c7e8ff66dc4c1c4efa3d21236edb Mon Sep 17 00:00:00 2001 From: Thomas Hartmann Date: Mon, 19 Sep 2022 16:55:06 +0200 Subject: [PATCH 06/12] QmlDesigner: Register QML types already in the plugin The states editor is using some of the types, too. We have to move the type registration out of the property editor in the future. Change-Id: I8a8b38180592a5785592e2482f35aad70208e62c Reviewed-by: Qt CI Bot Reviewed-by: Aleksei German --- .../components/propertyeditor/propertyeditorview.cpp | 1 - src/plugins/qmldesigner/qmldesignerplugin.cpp | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/plugins/qmldesigner/components/propertyeditor/propertyeditorview.cpp b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorview.cpp index fb1be16dc73..26b93bfbd34 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/propertyeditorview.cpp +++ b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorview.cpp @@ -97,7 +97,6 @@ PropertyEditorView::PropertyEditorView(AsynchronousImageCache &imageCache) m_stackedWidget->insertWidget(0, new QWidget(m_stackedWidget)); - Quick2PropertyEditorView::registerQmlTypes(); m_stackedWidget->setWindowTitle(tr("Properties")); } diff --git a/src/plugins/qmldesigner/qmldesignerplugin.cpp b/src/plugins/qmldesigner/qmldesignerplugin.cpp index 69d3db51c32..4890205f42d 100644 --- a/src/plugins/qmldesigner/qmldesignerplugin.cpp +++ b/src/plugins/qmldesigner/qmldesignerplugin.cpp @@ -26,14 +26,15 @@ #include "qmldesignerplugin.h" #include "designmodecontext.h" #include "designmodewidget.h" +#include "dynamiclicensecheck.h" #include "exception.h" #include "generateresource.h" #include "nodeinstanceview.h" #include "openuiqmlfiledialog.h" #include "qmldesignerconstants.h" #include "qmldesignerprojectmanager.h" +#include "quick2propertyeditorview.h" #include "settingspage.h" -#include "dynamiclicensecheck.h" #include #include @@ -280,7 +281,8 @@ bool QmlDesignerPlugin::initialize(const QStringList & /*arguments*/, QString *e designerActionManager().addDesignerAction(shutDownNanotraceAction); #endif - + //TODO Move registering those types out of the property editor, since they are used also in the states editor + Quick2PropertyEditorView::registerQmlTypes(); return true; } From 21b9e6ac7fbec381a8fe8cb08d043a340dbccc46 Mon Sep 17 00:00:00 2001 From: Henning Gruendl Date: Mon, 19 Sep 2022 22:02:27 +0200 Subject: [PATCH 07/12] QmlDesigner: Add ProgressBar to StudioControls Change-Id: I2d79e4af008fe7ef66ebbe2bf31813230451ee18 Reviewed-by: Reviewed-by: Thomas Hartmann --- .../imports/StudioControls/ProgressBar.qml | 54 +++++++++++++++++++ .../imports/StudioControls/qmldir | 1 + 2 files changed, 55 insertions(+) create mode 100644 share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/ProgressBar.qml diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/ProgressBar.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/ProgressBar.qml new file mode 100644 index 00000000000..01e2a3a1c55 --- /dev/null +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/ProgressBar.qml @@ -0,0 +1,54 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +import QtQuick +import QtQuick.Templates as T +import StudioTheme 1.0 as StudioTheme + +T.ProgressBar { + id: control + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding) + + contentItem: Item { + implicitWidth: 200 + implicitHeight: 6 + + Rectangle { + width: control.visualPosition * parent.width + height: parent.height + color: StudioTheme.Values.themeInteraction + } + } + + background: Rectangle { + implicitWidth: 200 + implicitHeight: 6 + color: StudioTheme.Values.themeThumbnailLabelBackground + } +} diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/qmldir b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/qmldir index dbc558ca105..61c35a00490 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/qmldir +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/qmldir @@ -22,6 +22,7 @@ Menu 1.0 Menu.qml MenuItem 1.0 MenuItem.qml MenuItemWithIcon 1.0 MenuItemWithIcon.qml MenuSeparator 1.0 MenuSeparator.qml +ProgressBar 1.0 ProgressBar.qml RadioButton 1.0 RadioButton.qml RealSliderPopup 1.0 RealSliderPopup.qml RealSpinBox 1.0 RealSpinBox.qml From 7fc3bac762fafc3fd1407b54e70c0aec25afee15 Mon Sep 17 00:00:00 2001 From: Thomas Hartmann Date: Tue, 20 Sep 2022 11:35:52 +0200 Subject: [PATCH 08/12] QmlDesigner: Disable scrolling if content does fit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is required on windows, because the other scrollbar seems to be taken into account somehow. Change-Id: I890faf7493c38df1f2f2f7fae011d4946f1b9c33 Reviewed-by: Henning Gründl --- share/qtcreator/qmldesigner/newstateseditor/Main.qml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/share/qtcreator/qmldesigner/newstateseditor/Main.qml b/share/qtcreator/qmldesigner/newstateseditor/Main.qml index 3b3162f322e..1d6f6910dd1 100644 --- a/share/qtcreator/qmldesigner/newstateseditor/Main.qml +++ b/share/qtcreator/qmldesigner/newstateseditor/Main.qml @@ -520,6 +520,16 @@ Rectangle { orientation: Qt.Vertical } + flickableDirection: { + if (frame.contentHeight <= scrollView.height) + return Flickable.HorizontalFlick + + if (frame.contentWidth <= scrollView.width) + return Flickable.VerticalFlick + + return Flickable.HorizontalAndVerticalFlick + } + Flickable { id: frame boundsMovement: Flickable.StopAtBounds From 533090fb5e18f2cfcc599266ff6fd5f8e5374f8b Mon Sep 17 00:00:00 2001 From: Henning Gruendl Date: Tue, 20 Sep 2022 11:46:17 +0200 Subject: [PATCH 09/12] QmlDesigner: Fix StatesEditor scroll bar Change-Id: I241a33906d06c6011cb618b96c039dfe91323809 Reviewed-by: Thomas Hartmann --- .../qmldesigner/newstateseditor/Main.qml | 21 ++++++++----------- .../newstateseditor/StateScrollBar.qml | 12 +++++++---- .../newstateseditor/StateThumbnail.qml | 10 +++++++++ 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/share/qtcreator/qmldesigner/newstateseditor/Main.qml b/share/qtcreator/qmldesigner/newstateseditor/Main.qml index 1d6f6910dd1..8f50d8f3362 100644 --- a/share/qtcreator/qmldesigner/newstateseditor/Main.qml +++ b/share/qtcreator/qmldesigner/newstateseditor/Main.qml @@ -507,7 +507,6 @@ Rectangle { x: scrollView.leftPadding y: scrollView.height - height width: scrollView.availableWidth - active: scrollView.ScrollBar.vertical.active orientation: Qt.Horizontal } @@ -516,20 +515,9 @@ Rectangle { x: scrollView.mirrored ? 0 : scrollView.width - width y: scrollView.topPadding height: scrollView.availableHeight - active: scrollView.ScrollBar.horizontal.active orientation: Qt.Vertical } - flickableDirection: { - if (frame.contentHeight <= scrollView.height) - return Flickable.HorizontalFlick - - if (frame.contentWidth <= scrollView.width) - return Flickable.VerticalFlick - - return Flickable.HorizontalAndVerticalFlick - } - Flickable { id: frame boundsMovement: Flickable.StopAtBounds @@ -543,6 +531,15 @@ Rectangle { let ext = root.showExtendGroups ? (2 * root.extend) : 0 return innerGrid.height + ext } + flickableDirection: { + if (frame.contentHeight <= scrollView.height) + return Flickable.HorizontalFlick + + if (frame.contentWidth <= scrollView.width) + return Flickable.VerticalFlick + + return Flickable.HorizontalAndVerticalFlick + } Behavior on contentY { NumberAnimation { diff --git a/share/qtcreator/qmldesigner/newstateseditor/StateScrollBar.qml b/share/qtcreator/qmldesigner/newstateseditor/StateScrollBar.qml index 68f766a10ee..a284131bad7 100644 --- a/share/qtcreator/qmldesigner/newstateseditor/StateScrollBar.qml +++ b/share/qtcreator/qmldesigner/newstateseditor/StateScrollBar.qml @@ -24,12 +24,17 @@ ****************************************************************************/ import QtQuick -import QtQuick.Controls +import QtQuick.Templates as T import StudioTheme 1.0 as StudioTheme -ScrollBar { +T.ScrollBar { id: scrollBar + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding) + contentItem: Rectangle { implicitWidth: scrollBar.interactive ? 6 : 2 implicitHeight: scrollBar.interactive ? 6 : 2 @@ -40,8 +45,7 @@ ScrollBar { states: State { name: "active" - when: scrollBar.policy === ScrollBar.AlwaysOn - || (scrollBar.active && scrollBar.size < 1.0) + when: scrollBar.active && scrollBar.size < 1.0 PropertyChanges { target: scrollBar.contentItem opacity: 0.75 diff --git a/share/qtcreator/qmldesigner/newstateseditor/StateThumbnail.qml b/share/qtcreator/qmldesigner/newstateseditor/StateThumbnail.qml index a5d20f85331..83de652edb0 100644 --- a/share/qtcreator/qmldesigner/newstateseditor/StateThumbnail.qml +++ b/share/qtcreator/qmldesigner/newstateseditor/StateThumbnail.qml @@ -286,11 +286,21 @@ Item { } Flickable { + id: frame boundsMovement: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds interactive: true contentWidth: column.width contentHeight: column.height + flickableDirection: { + if (frame.contentHeight <= scrollView.height) + return Flickable.HorizontalFlick + + if (frame.contentWidth <= scrollView.width) + return Flickable.VerticalFlick + + return Flickable.HorizontalAndVerticalFlick + } // ScrollView needs an extra TapHandler on top in order to receive click // events. MouseAreas below ScrollView do not let clicks through. From b9ff06723a0bcbb1ffe74cb2af792e5349d59b17 Mon Sep 17 00:00:00 2001 From: Henning Gruendl Date: Tue, 20 Sep 2022 11:46:40 +0200 Subject: [PATCH 10/12] QmlDesigner: Fix extend state menu item Change-Id: I91be5ddb3020e86c33d4264b4e95918689b69f4e Reviewed-by: Thomas Hartmann --- share/qtcreator/qmldesigner/newstateseditor/StateMenu.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/qtcreator/qmldesigner/newstateseditor/StateMenu.qml b/share/qtcreator/qmldesigner/newstateseditor/StateMenu.qml index 3ecf13e0841..8d0bb8810c4 100644 --- a/share/qtcreator/qmldesigner/newstateseditor/StateMenu.qml +++ b/share/qtcreator/qmldesigner/newstateseditor/StateMenu.qml @@ -76,7 +76,7 @@ StudioControls.Menu { StudioControls.MenuItem { id: extend - visible: !root.isBaseState + visible: !root.isBaseState && !root.hasExtend text: qsTr("Extend") height: extend.visible ? extend.implicitHeight : 0 onTriggered: root.extend() From cddb19ef9c3f929a7696eb4b81f6225da1f15342 Mon Sep 17 00:00:00 2001 From: Henning Gruendl Date: Tue, 20 Sep 2022 11:47:03 +0200 Subject: [PATCH 11/12] QmlDesigner: Remove unnecessary imports Change-Id: Ib0566d580ff214a3773434f6eb07b63f8958409e Reviewed-by: Thomas Hartmann --- .../propertyEditorQmlSources/imports/StudioControls/Dialog.qml | 1 - .../imports/StudioControls/DialogButtonBox.qml | 1 - 2 files changed, 2 deletions(-) diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/Dialog.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/Dialog.qml index c90409e5dee..4320021f9d6 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/Dialog.qml +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/Dialog.qml @@ -24,7 +24,6 @@ ****************************************************************************/ import QtQuick -import QtQuick.Controls as C import QtQuick.Templates as T import StudioTheme 1.0 as StudioTheme diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/DialogButtonBox.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/DialogButtonBox.qml index fc313a66099..c32eae2e987 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/DialogButtonBox.qml +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/DialogButtonBox.qml @@ -24,7 +24,6 @@ ****************************************************************************/ import QtQuick -import QtQuick.Controls as C import QtQuick.Templates as T import StudioTheme 1.0 as StudioTheme From 1404c36e2b3e658e919cd344ca75caa5ad360414 Mon Sep 17 00:00:00 2001 From: Marcus Tillmanns Date: Mon, 19 Sep 2022 14:55:34 +0200 Subject: [PATCH 12/12] Deviceshell: Fix use after free Change-Id: I6caa22e53bee2db332ced7301534683d7a064905 Reviewed-by: Reviewed-by: Jarek Kobus --- src/libs/utils/deviceshell.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/utils/deviceshell.cpp b/src/libs/utils/deviceshell.cpp index 75a8cbb2604..cf9d2139550 100644 --- a/src/libs/utils/deviceshell.cpp +++ b/src/libs/utils/deviceshell.cpp @@ -247,7 +247,7 @@ DeviceShell::RunResult DeviceShell::run(const CommandLine &cmd, const QByteArray const int id = ++m_currentId; const auto it = m_commandOutput.insert(id, CommandRun{{-1, {}, {}}, &waiter}); - QMetaObject::invokeMethod(m_shellProcess, [this, id, &cmd, &stdInData]() { + QMetaObject::invokeMethod(m_shellProcess, [this, id, cmd, stdInData]() { const QString command = QString("%1 \"%2\" %3\n") .arg(id) .arg(QString::fromLatin1(stdInData.toBase64()))