diff --git a/share/qtcreator/qmldesigner/newstateseditor/Main.qml b/share/qtcreator/qmldesigner/newstateseditor/Main.qml new file mode 100644 index 00000000000..8f50d8f3362 --- /dev/null +++ b/share/qtcreator/qmldesigner/newstateseditor/Main.qml @@ -0,0 +1,839 @@ +/**************************************************************************** +** +** 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 + orientation: Qt.Horizontal + } + + ScrollBar.vertical: StateScrollBar { + parent: scrollView + x: scrollView.mirrored ? 0 : scrollView.width - width + y: scrollView.topPadding + height: scrollView.availableHeight + 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 + } + flickableDirection: { + if (frame.contentHeight <= scrollView.height) + return Flickable.HorizontalFlick + + if (frame.contentWidth <= scrollView.width) + return Flickable.VerticalFlick + + return Flickable.HorizontalAndVerticalFlick + } + + 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..8d0bb8810c4 --- /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 && !root.hasExtend + 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..a284131bad7 --- /dev/null +++ b/share/qtcreator/qmldesigner/newstateseditor/StateScrollBar.qml @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** 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.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 + radius: width / 2 + opacity: 0.0 + color: scrollBar.pressed ? StudioTheme.Values.themeSliderActiveTrackHover + : StudioTheme.Values.themeSliderHandle + + states: State { + name: "active" + when: 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..83de652edb0 --- /dev/null +++ b/share/qtcreator/qmldesigner/newstateseditor/StateThumbnail.qml @@ -0,0 +1,756 @@ +/**************************************************************************** +** +** 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 { + 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. + 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 00000000000..72cb9f03506 Binary files /dev/null and b/share/qtcreator/qmldesigner/newstateseditor/images/checkers.png differ 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/QtQuick/ItemPane.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick/ItemPane.qml index 1805bc91095..3ae08a1249a 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick/ItemPane.qml +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick/ItemPane.qml @@ -17,6 +17,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 8d9ac9b5104..f4e80733718 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/Object3DPane.qml +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/QtQuick3D/Object3DPane.qml @@ -20,6 +20,7 @@ PropertyEditorPane { DynamicPropertiesSection { propertiesModel: SelectionDynamicPropertiesModel {} + visible: !hasMultiSelection } Loader { 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 { diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/AbstractButton.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/AbstractButton.qml index 0df262f3137..d4d898965c1 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/AbstractButton.qml +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/AbstractButton.qml @@ -21,6 +21,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, @@ -76,11 +79,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 { @@ -103,6 +107,7 @@ T.AbstractButton { PropertyChanges { target: buttonBackground color: StudioTheme.Values.themeControlBackground + border.color: StudioTheme.Values.themeControlOutline } PropertyChanges { target: myButton @@ -115,19 +120,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 @@ -138,6 +156,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..4320021f9d6 --- /dev/null +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/Dialog.qml @@ -0,0 +1,74 @@ +/**************************************************************************** +** +** 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.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..c32eae2e987 --- /dev/null +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/DialogButtonBox.qml @@ -0,0 +1,68 @@ +/**************************************************************************** +** +** 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.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/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/TextField.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/TextField.qml index 91b1aecb502..2ac98abbfc3 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/TextField.qml +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/TextField.qml @@ -8,22 +8,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 @@ -46,7 +48,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 @@ -122,6 +124,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 @@ -151,8 +161,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 @@ -167,7 +177,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..61c35a00490 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 @@ -18,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 diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioTheme/Values.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioTheme/Values.qml index 6853ac4048e..2355f33281c 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioTheme/Values.qml +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioTheme/Values.qml @@ -194,6 +194,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 @@ -266,7 +275,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) @@ -286,10 +295,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 @@ -312,9 +326,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 ba94d00a9b3..bb1d7bfec83 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 a13be30540c..06d36f669e9 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 566e95e145b..12691562788 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 9b0614c97c3..d0a2c1b3d6f 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 483dff1a5a8..203ab5e4159 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 7f5b9d9c86e..d3ff03a7985 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 1e90a0c1393..7d4c6d9608c 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/deviceshell.cpp b/src/libs/utils/deviceshell.cpp index a27a6b5401e..9ba61c5896b 100644 --- a/src/libs/utils/deviceshell.cpp +++ b/src/libs/utils/deviceshell.cpp @@ -237,7 +237,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())) diff --git a/src/libs/utils/theme/theme.h b/src/libs/utils/theme/theme.h index cd36efb3026..effd69aa1f2 100644 --- a/src/libs/utils/theme/theme.h +++ b/src/libs/utils/theme/theme.h @@ -416,6 +416,9 @@ public: DSgreenLight, DSamberLight, DSredLight, + + DSstatePanelBackground, + DSstateHighlight, }; enum ImageFile { diff --git a/src/plugins/qmldesigner/CMakeLists.txt b/src/plugins/qmldesigner/CMakeLists.txt index 92482c0a78e..6909ef7a405 100644 --- a/src/plugins/qmldesigner/CMakeLists.txt +++ b/src/plugins/qmldesigner/CMakeLists.txt @@ -424,6 +424,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/propertyeditorcontextobject.cpp b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorcontextobject.cpp index 8dc03c03f51..049985ddda8 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/propertyeditorcontextobject.cpp +++ b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorcontextobject.cpp @@ -410,6 +410,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 3d421acbc17..23a2bba47fb 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/propertyeditorcontextobject.h +++ b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorcontextobject.h @@ -46,6 +46,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); @@ -104,6 +107,10 @@ public: bool hasAliasExport() const { return m_aliasExport; } + bool hasMultiSelection() const; + + void setHasMultiSelection(bool); + signals: void specificsUrlChanged(); void specificQmlDataChanged(); @@ -120,6 +127,7 @@ signals: void hasAliasExportChanged(); void hasActiveTimelineChanged(); void activeDragSuffixChanged(); + void hasMultiSelectionChanged(); public slots: @@ -170,6 +178,8 @@ private: bool m_setHasActiveTimeline = false; QString m_activeDragSuffix; + + bool m_hasMultiSelection = false; }; class EasingCurveEditor : public QObject diff --git a/src/plugins/qmldesigner/components/propertyeditor/propertyeditorview.cpp b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorview.cpp index b3b5831f8c3..183197a452d 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/propertyeditorview.cpp +++ b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorview.cpp @@ -75,7 +75,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/components/propertyeditor/quick2propertyeditorview.cpp b/src/plugins/qmldesigner/components/propertyeditor/quick2propertyeditorview.cpp index f28479050cf..e5a21325ec2 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/quick2propertyeditorview.cpp +++ b/src/plugins/qmldesigner/components/propertyeditor/quick2propertyeditorview.cpp @@ -14,10 +14,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" @@ -56,6 +58,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..9e93b59aad4 --- /dev/null +++ b/src/plugins/qmldesigner/components/stateseditornew/propertymodel.cpp @@ -0,0 +1,173 @@ +/**************************************************************************** +** +** 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 {}; + // return propertyChanges.target().metaInfo().propertyType( + // 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..fa7213d344b --- /dev/null +++ b/src/plugins/qmldesigner/components/stateseditornew/stateseditormodel.cpp @@ -0,0 +1,457 @@ +/**************************************************************************** +** +** 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 +{ + return {}; + // 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 +{ + return {}; + // 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..004df81fd79 --- /dev/null +++ b/src/plugins/qmldesigner/components/stateseditornew/stateseditorview.cpp @@ -0,0 +1,937 @@ +/**************************************************************************** +** +** 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(), + "StatesEditor", + WidgetInfo::BottomPane, + 0, + tr("States")); +} + +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 1; + // 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 7301ff4a0ad..8fe62f38c1b 100644 --- a/src/plugins/qmldesigner/designercore/include/qmlchangeset.h +++ b/src/plugins/qmldesigner/designercore/include/qmlchangeset.h @@ -9,20 +9,24 @@ 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; explicit operator bool() const { return isValid(); } 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/qmlobjectnode.h b/src/plugins/qmldesigner/designercore/include/qmlobjectnode.h index 33d836f8639..e0b178a47bc 100644 --- a/src/plugins/qmldesigner/designercore/include/qmlobjectnode.h +++ b/src/plugins/qmldesigner/designercore/include/qmlobjectnode.h @@ -121,6 +121,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/include/qmlstate.h b/src/plugins/qmldesigner/designercore/include/qmlstate.h index e523788b591..1e409473595 100644 --- a/src/plugins/qmldesigner/designercore/include/qmlstate.h +++ b/src/plugins/qmldesigner/designercore/include/qmlstate.h @@ -16,9 +16,14 @@ class Annotation; class AnnotationEditor; class StatesEditorView; +namespace Experimental { +class StatesEditorView; +} + class QMLDESIGNERCORE_EXPORT QmlModelState final : public QmlModelNodeFacade { friend StatesEditorView; + friend Experimental::StatesEditorView; public: QmlModelState(); @@ -61,6 +66,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 750d554b335..ee0817d3737 100644 --- a/src/plugins/qmldesigner/designercore/include/qmlvisualnode.h +++ b/src/plugins/qmldesigner/designercore/include/qmlvisualnode.h @@ -103,6 +103,7 @@ class QMLDESIGNERCORE_EXPORT QmlModelStateGroup { friend class QmlObjectNode; friend class StatesEditorView; + friend class Experimental::StatesEditorView; public: QmlModelStateGroup() = default; diff --git a/src/plugins/qmldesigner/designercore/model/qmlchangeset.cpp b/src/plugins/qmldesigner/designercore/model/qmlchangeset.cpp index 8886a3d66f7..e4186840273 100644 --- a/src/plugins/qmldesigner/designercore/model/qmlchangeset.cpp +++ b/src/plugins/qmldesigner/designercore/model/qmlchangeset.cpp @@ -7,6 +7,8 @@ #include "abstractview.h" #include +#include + namespace QmlDesigner { ModelNode QmlModelStateOperation::target() const @@ -22,6 +24,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/qmlobjectnode.cpp b/src/plugins/qmldesigner/designercore/model/qmlobjectnode.cpp index ad4d40202a9..ad6e8596efb 100644 --- a/src/plugins/qmldesigner/designercore/model/qmlobjectnode.cpp +++ b/src/plugins/qmldesigner/designercore/model/qmlobjectnode.cpp @@ -25,6 +25,7 @@ #include #endif +#include #include #include @@ -593,6 +594,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. diff --git a/src/plugins/qmldesigner/designercore/model/qmlstate.cpp b/src/plugins/qmldesigner/designercore/model/qmlstate.cpp index 84d7f491bab..bba6d9c1d3a 100644 --- a/src/plugins/qmldesigner/designercore/model/qmlstate.cpp +++ b/src/plugins/qmldesigner/designercore/model/qmlstate.cpp @@ -22,7 +22,7 @@ QmlModelState::QmlModelState() } QmlModelState::QmlModelState(const ModelNode &modelNode) - : QmlModelNodeFacade(modelNode) + : QmlModelNodeFacade(modelNode) { } @@ -48,7 +48,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)) { @@ -67,7 +67,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 @@ -82,7 +82,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) @@ -110,7 +110,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 @@ -347,6 +347,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/viewmanager.cpp b/src/plugins/qmldesigner/designercore/model/viewmanager.cpp index d8a64e5e93a..fd729ae627c 100644 --- a/src/plugins/qmldesigner/designercore/model/viewmanager.cpp +++ b/src/plugins/qmldesigner/designercore/model/viewmanager.cpp @@ -6,6 +6,7 @@ #ifndef QMLDESIGNER_TEST #include +#include #include #include #include @@ -16,13 +17,13 @@ #include #include #include -#include +#include +#include #include #include #include -#include -#include #include +#include #include #include #include @@ -35,6 +36,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 @@ -64,6 +73,7 @@ public: MaterialEditorView materialEditorView; MaterialBrowserView materialBrowserView; StatesEditorView statesEditorView; + Experimental::StatesEditorView newStatesEditorView; std::vector> additionalViews; bool disableStandardViews = false; @@ -142,16 +152,30 @@ void ViewManager::detachRewriterView() void ViewManager::switchStateEditorViewToBaseState() { - if (d->statesEditorView.isAttached()) { - d->savedState = d->statesEditorView.currentState(); - d->statesEditorView.setCurrentState(d->statesEditorView.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); + 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 @@ -174,11 +198,19 @@ QList ViewManager::standardViews() const &d->materialEditorView, &d->materialBrowserView, &d->statesEditorView, + &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; } @@ -308,7 +340,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()); + 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 3cd86dfbcb2..4de691f6557 100644 --- a/src/plugins/qmldesigner/designersettings.cpp +++ b/src/plugins/qmldesigner/designersettings.cpp @@ -63,6 +63,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 d0c93836c68..9922f6cd6e7 100644 --- a/src/plugins/qmldesigner/designersettings.h +++ b/src/plugins/qmldesigner/designersettings.h @@ -52,6 +52,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 diff --git a/src/plugins/qmldesigner/qmldesignerconstants.h b/src/plugins/qmldesigner/qmldesignerconstants.h index ae113f1070e..0315601379b 100644 --- a/src/plugins/qmldesigner/qmldesignerconstants.h +++ b/src/plugins/qmldesigner/qmldesignerconstants.h @@ -90,6 +90,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.cpp b/src/plugins/qmldesigner/qmldesignerplugin.cpp index 265fa656019..e3bc34e05da 100644 --- a/src/plugins/qmldesigner/qmldesignerplugin.cpp +++ b/src/plugins/qmldesigner/qmldesignerplugin.cpp @@ -4,14 +4,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 @@ -258,7 +259,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; } diff --git a/src/plugins/qmldesigner/qmldesignerplugin.qbs b/src/plugins/qmldesigner/qmldesignerplugin.qbs index 9938c377247..fbb4a05f073 100644 --- a/src/plugins/qmldesigner/qmldesignerplugin.qbs +++ b/src/plugins/qmldesigner/qmldesignerplugin.qbs @@ -60,6 +60,7 @@ Project { "components/navigator", "components/pluginmanager", "components/stateseditor", + "components/stateseditornew", "components/texteditor", "components/timelineeditor", "components/listmodeleditor", @@ -821,6 +822,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", ] }