diff --git a/share/qtcreator/qmldesigner/designsystem/Main.qml b/share/qtcreator/qmldesigner/designsystem/Main.qml index 7c444c44e4f..9695f95270a 100644 --- a/share/qtcreator/qmldesigner/designsystem/Main.qml +++ b/share/qtcreator/qmldesigner/designsystem/Main.qml @@ -13,6 +13,7 @@ import DesignSystemControls as DSC import StudioControls as StudioControls import StudioTheme as StudioTheme +import StudioQuickUtils Rectangle { id: root @@ -33,6 +34,11 @@ Rectangle { readonly property int iconSize: 16 readonly property int leftPadding: 14 + readonly property int rightPadding: 14 + + property var customStyle: StudioTheme.ControlStyle { + border.idle: root.borderColor + } width: 400 height: 400 @@ -41,6 +47,10 @@ Rectangle { function loadModel(name) { root.currentCollectionName = name tableView.model = DesignSystemBackend.dsInterface.model(name) + + topLeftCell.visible = tableView.model.columnCount() + createModeButton.enabled = tableView.model.rowCount() + modelConnections.target = tableView.model } function groupString(gt) { @@ -56,6 +66,178 @@ Rectangle { return "unknow_group" } + function setValue(value: var, row: int, column: int, isBinding: bool): bool { + console.log("setValue(", value, row, column, isBinding, ")") + return tableView.model.setData(tableView.index(row, column), + DesignSystemBackend.dsInterface.createThemeProperty("", value, isBinding), + Qt.EditRole) + } + + Connections { + id: modelConnections + ignoreUnknownSignals: true // model might initially be null + + function onModelReset() { + topLeftCell.visible = tableView.model.columnCount() + createModeButton.enabled = tableView.model.rowCount() + } + } + + StudioControls.Dialog { + id: renameCollectionDialog + title: qsTr("Rename collection") + width: Math.min(300, root.width) + closePolicy: Popup.CloseOnEscape + anchors.centerIn: parent + modal: true + + onOpened: renameCollectionTextField.forceActiveFocus() + + contentItem: Column { + spacing: 20 + width: parent.width + + StudioControls.TextField { + id: renameCollectionTextField + actionIndicatorVisible: false + translationIndicatorVisible: false + width: parent.width + + onAccepted: renameCollectionDialog.accept() + onRejected: renameCollectionDialog.reject() + } + + Row { + spacing: 10 + anchors.right: parent.right + + StudioControls.DialogButton { + text: qsTr("Rename") + enabled: (renameCollectionDialog.previousString !== renameCollectionTextField.text) + onClicked: renameCollectionDialog.accept() + } + + StudioControls.DialogButton { + text: qsTr("Cancel") + onClicked: renameCollectionDialog.reject() + } + } + } + + onAccepted: { + DesignSystemBackend.dsInterface.renameCollection(collectionsComboBox.currentText, + renameCollectionTextField.text) + renameCollectionDialog.close() + } + + property string previousString + + onAboutToShow: { + renameCollectionTextField.text = collectionsComboBox.currentText + renameCollectionDialog.previousString = collectionsComboBox.currentText + } + } + + StudioControls.Dialog { + id: createCollectionDialog + title: qsTr("Create collection") + width: Math.min(300, root.width) + closePolicy: Popup.CloseOnEscape + anchors.centerIn: parent + modal: true + + onOpened: createCollectionTextField.forceActiveFocus() + + contentItem: Column { + spacing: 20 + width: parent.width + + StudioControls.TextField { + id: createCollectionTextField + actionIndicatorVisible: false + translationIndicatorVisible: false + width: parent.width + + text: qsTr("NewCollection") + + onAccepted: createCollectionDialog.accept() + onRejected: createCollectionDialog.reject() + } + + Row { + spacing: 10 + anchors.right: parent.right + + StudioControls.DialogButton { + text: qsTr("Create") + onClicked: createCollectionDialog.accept() + } + + StudioControls.DialogButton { + text: qsTr("Cancel") + onClicked: createCollectionDialog.reject() + } + } + } + + onAccepted: { + DesignSystemBackend.dsInterface.addCollection(createCollectionTextField.text) + root.loadModel(createCollectionTextField.text) + collectionsComboBox.currentIndex = collectionsComboBox.indexOfValue(createCollectionTextField.text) + createCollectionDialog.close() + } + } + + StudioControls.Dialog { + id: removeCollectionDialog + title: qsTr("Remove collection") + width: Math.min(300, root.width) + closePolicy: Popup.CloseOnEscape + anchors.centerIn: parent + modal: true + + onOpened: removeCollectionDialog.forceActiveFocus() + + contentItem: Column { + spacing: 20 + width: parent.width + + Text { + id: warningText + + text: qsTr("Are you sure? The action cannot be undone.") + color: StudioTheme.Values.themeTextColor + wrapMode: Text.WordWrap + width: parent.width + } + + Row { + spacing: 10 + anchors.right: parent.right + + StudioControls.DialogButton { + text: qsTr("Remove") + onClicked: removeCollectionDialog.accept() + } + + StudioControls.DialogButton { + text: qsTr("Cancel") + onClicked: removeCollectionDialog.reject() + } + } + } + + onAccepted: { + let currentCollectionName = collectionsComboBox.currentText + let currentCollectionIndex = collectionsComboBox.currentIndex + let previousCollectionIndex = (currentCollectionIndex === 0) ? 1 : currentCollectionIndex - 1 + + root.loadModel(collectionsComboBox.textAt(previousCollectionIndex)) + DesignSystemBackend.dsInterface.removeCollection(currentCollectionName) + removeCollectionDialog.close() + } + } + Rectangle { id: toolBar @@ -77,6 +259,15 @@ Rectangle { onActivated: root.loadModel(collectionsComboBox.currentText) } + StudioControls.ComboBox { + id: themesComboBox + style: StudioTheme.Values.viewBarControlStyle + anchors.verticalCenter: parent.verticalCenter + actionIndicatorVisible: false + model: tableView.model.themeNames + onActivated: tableView.model.setActiveTheme(themesComboBox.currentText) + } + StudioControls.IconTextButton { id: moreButton anchors.verticalCenter: parent.verticalCenter @@ -84,26 +275,33 @@ Rectangle { checkable: true checked: moreMenu.visible - onClicked: moreMenu.popup(0, moreButton.height) + onToggled: { + if (moreMenu.visible) + moreMenu.close() + else + moreMenu.popup(0, moreButton.height) + } StudioControls.Menu { id: moreMenu + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent + StudioControls.MenuItem { text: qsTr("Rename") - onTriggered: console.log(">>> Rename collection") + onTriggered: renameCollectionDialog.open() } StudioControls.MenuItem { text: qsTr("Delete") - onTriggered: console.log(">>> Delete collection") + onTriggered: removeCollectionDialog.open() } StudioControls.MenuSeparator {} StudioControls.MenuItem { text: qsTr("Create collection") - onTriggered: console.log(">>> Create collection") + onTriggered: createCollectionDialog.open() } } } @@ -127,9 +325,15 @@ Rectangle { required property bool editing + required property var resolvedValue + required property bool isActive required property bool isBinding required property var propertyValue + property bool creatingBinding: false + + readonly property bool bindingEditor: isBinding || creatingBinding + color: root.backgroundColor implicitWidth: root.cellWidth implicitHeight: root.cellHeight @@ -140,18 +344,24 @@ Rectangle { } component DataCell: Cell { + id: dataCell + HoverHandler { id: cellHoverHandler } - StudioControls.IconIndicator { - icon: isBinding ? StudioTheme.Constants.actionIconBinding - : StudioTheme.Constants.actionIcon + DSC.BindingIndicator { + icon.text: dataCell.isBinding ? StudioTheme.Constants.actionIconBinding + : StudioTheme.Constants.actionIcon + icon.color: dataCell.isBinding ? StudioTheme.Values.themeInteraction + : StudioTheme.Values.themeTextColor + anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: 10 - visible: cellHoverHandler.hovered + visible: !dataCell.editing && (cellHoverHandler.hovered || dataCell.isBinding) onClicked: { tableView.closeEditor() + menu.show(dataCell.row, dataCell.column) } } } @@ -171,37 +381,39 @@ Rectangle { leftPadding: root.leftPadding horizontalAlignment: TextInput.AlignLeft verticalAlignment: TextInput.AlignVCenter - color: StudioTheme.Values.themeTextColor - text: stringDelegate.display + color: stringDelegate.isBinding ? StudioTheme.Values.themeInteraction + : StudioTheme.Values.themeTextColor + text: stringDelegate.resolvedValue visible: !stringDelegate.editing } + // This edit delegate combines the binding editor and string value editor TableView.editDelegate: DSC.TextField { id: stringEditDelegate + style: root.customStyle + anchors.fill: parent leftPadding: root.leftPadding - // Only apply more to right padding when hovered - //rightPadding - horizontalAlignment: TextInput.AlignLeft verticalAlignment: TextInput.AlignVCenter - text: stringDelegate.display + text: stringDelegate.bindingEditor ? stringDelegate.propertyValue + : stringDelegate.resolvedValue + Component.onCompleted: stringEditDelegate.selectAll() + Component.onDestruction: stringDelegate.creatingBinding = false TableView.onCommit: { - console.log("onCommit", stringEditDelegate.text) - let index = TableView.view.index(stringDelegate.row, stringDelegate.column) - var prop = DesignSystemBackend.dsInterface.createThemeProperty("", - stringEditDelegate.text, - stringDelegate.isBinding) - TableView.view.model.setData(index, prop, Qt.EditRole) + root.setValue(stringEditDelegate.text, + stringDelegate.row, + stringDelegate.column, + stringDelegate.bindingEditor) } } - Component.onCompleted: console.log("DelegateChoice - string", stringDelegate.display) + //Component.onCompleted: console.log("DelegateChoice - string", stringDelegate.resolvedValue) } } @@ -214,38 +426,107 @@ Rectangle { Text { anchors.fill: parent leftPadding: root.leftPadding + rightPadding: root.rightPadding + horizontalAlignment: TextInput.AlignLeft verticalAlignment: TextInput.AlignVCenter - color: StudioTheme.Values.themeTextColor - text: numberDelegate.display - visible: !numberDelegate.editing + color: numberDelegate.isBinding ? StudioTheme.Values.themeInteraction + : StudioTheme.Values.themeTextColor + // -128 is the value of QLocale::FloatingPointShortest + text: Number(numberDelegate.resolvedValue).toLocaleString(Utils.locale, 'f', -128) } - TableView.editDelegate: SpinBox { - id: numberEditDelegate - + // This edit delegate has two different controls, one for number editing and one for + // binding editing. Depending on the mode one is hidden and the other is shown. + TableView.editDelegate: FocusScope { + id: numberEditDelegateFocusScope anchors.fill: parent - leftPadding: root.leftPadding - value: numberDelegate.display - from: -1000 // TODO define min/max - to: 1000 - editable: true + property bool alreadyCommited: false + + DSC.SpinBox { + id: numberEditDelegate + + property real previousValue: 0 + + style: root.customStyle + + anchors.fill: parent + + realValue: numberDelegate.resolvedValue + realFrom: -1000 // TODO define min/max + realTo: 1000 + editable: true + decimals: 2 + + focus: !numberDelegate.bindingEditor + visible: !numberDelegate.bindingEditor + + Component.onCompleted: { + numberEditDelegate.previousValue = numberDelegate.resolvedValue + numberEditDelegate.contentItem.selectAll() + } + Component.onDestruction: { + if (numberEditDelegateFocusScope.alreadyCommited || numberDelegate.bindingEditor) + return + + let val = numberEditDelegate.realValue + + if (numberEditDelegate.previousValue === val) + return + + root.setValue(val, + numberDelegate.row, + numberDelegate.column, + numberDelegate.bindingEditor) + } + } + + DSC.TextField { + id: numberBindingEditDelegate + + style: root.customStyle + + anchors.fill: parent + leftPadding: root.leftPadding + rightPadding: root.rightPadding + + horizontalAlignment: TextInput.AlignLeft + verticalAlignment: TextInput.AlignVCenter + + text: numberDelegate.propertyValue + + focus: numberDelegate.bindingEditor + visible: numberDelegate.bindingEditor + + Component.onCompleted: numberBindingEditDelegate.selectAll() + Component.onDestruction: numberDelegate.creatingBinding = false + } TableView.onCommit: { - let val = numberEditDelegate.valueFromText(numberEditDelegate.contentItem.text, - numberEditDelegate.locale) - console.log("onCommit", val) - let index = TableView.view.index(numberDelegate.row, numberDelegate.column) - var prop = DesignSystemBackend.dsInterface.createThemeProperty("", - val, - numberDelegate.isBinding) - TableView.view.model.setData(index, prop, Qt.EditRole) + // By default assume binding edit delegate is used + let val = numberBindingEditDelegate.text + + // If binding editor isn't used then the SpinBox value needs to be written + if (!numberDelegate.bindingEditor) { + numberEditDelegate.valueFromText(numberEditDelegate.contentItem.text, + numberEditDelegate.locale) + // Don't use return value of valueFromText as it is of type int. + // Internally the float value is set on realValue property. + val = numberEditDelegate.realValue + } + + root.setValue(val, + numberDelegate.row, + numberDelegate.column, + numberDelegate.bindingEditor) + + numberEditDelegateFocusScope.alreadyCommited = true } } - Component.onCompleted: console.log("DelegateChoice - number", display) + //Component.onCompleted: console.log("DelegateChoice - number", numberDelegate.resolvedValue) } } @@ -262,20 +543,62 @@ Rectangle { anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: root.leftPadding - checked: flagDelegate.display - text: flagDelegate.display + checked: flagDelegate.resolvedValue + text: flagDelegate.resolvedValue + + labelColor: flagDelegate.isBinding ? StudioTheme.Values.themeInteraction + : StudioTheme.Values.themeTextColor + + readonly: flagDelegate.isBinding onToggled: { - console.log("onCommit", flagEditDelegate.checked) - let index = flagDelegate.TableView.view.index(flagDelegate.row, flagDelegate.column) - var prop = DesignSystemBackend.dsInterface.createThemeProperty("", - flagEditDelegate.checked, - flagEditDelegate.isBinding) - flagDelegate.TableView.view.model.setData(index, prop, Qt.EditRole) + root.setValue(flagEditDelegate.checked, + flagDelegate.row, + flagDelegate.column, + false) } } - Component.onCompleted: console.log("DelegateChoice - bool", flagDelegate.display) + // Dummy item to show forbidden cursor when hovering over bound switch control + Item { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: root.leftPadding + + width: flagEditDelegate.indicator.width + height: flagEditDelegate.indicator.height + + visible: !flagEditDelegate.enabled + + HoverHandler { + enabled: !flagEditDelegate.enabled + cursorShape: Qt.ForbiddenCursor + } + } + + TableView.editDelegate: DSC.TextField { + id: flagBindingEditDelegate + + anchors.fill: parent + leftPadding: root.leftPadding + + horizontalAlignment: TextInput.AlignLeft + verticalAlignment: TextInput.AlignVCenter + + text: flagDelegate.bindingEditor ? flagDelegate.propertyValue + : flagDelegate.resolvedValue + Component.onCompleted: flagBindingEditDelegate.selectAll() + Component.onDestruction: flagDelegate.creatingBinding = false + + TableView.onCommit: { + root.setValue(flagBindingEditDelegate.text, + flagDelegate.row, + flagDelegate.column, + true) + } + } + + //Component.onCompleted: console.log("DelegateChoice - bool", flagDelegate.resolvedValue) } } @@ -286,18 +609,20 @@ Rectangle { id: colorDelegate Row { + id: colorDelegateRow anchors.fill: parent leftPadding: root.leftPadding spacing: 8 Rectangle { + id: colorDelegatePreview anchors.verticalCenter: parent.verticalCenter width: 20 height: 20 - color: colorDelegate.display + color: colorDelegate.resolvedValue border.color: "black" - border.width: 1 + border.width: StudioTheme.Values.border Image { anchors.fill: parent @@ -309,38 +634,338 @@ Rectangle { MouseArea { id: colorMouseArea anchors.fill: parent + enabled: !colorDelegate.isBinding - onClicked: { - if (popupDialog.visibility) { - popupDialog.close() + cursorShape: colorDelegate.isBinding ? Qt.ForbiddenCursor + : Qt.PointingHandCursor + + function togglePopup() { + if (colorPopup.visibility) { + colorPopup.close() } else { - popupDialog.ensureLoader() - popupDialog.show(colorDelegate) + colorPopup.modelIndex = tableView.index(colorDelegate.row, colorDelegate.column) - if (loader.status === Loader.Ready) - loader.item.originalColor = root.color + colorPopup.ensureLoader() + colorPopup.show(colorDelegate) + + if (loader.status === Loader.Ready) { + loader.item.color = colorDelegate.resolvedValue + loader.item.originalColor = colorDelegate.resolvedValue + } } + tableView.closeEditor() colorMouseArea.forceActiveFocus() } + + onClicked: colorMouseArea.togglePopup() } } Text { height: parent.height verticalAlignment: Qt.AlignVCenter - color: StudioTheme.Values.themeTextColor - text: colorDelegate.propertyValue + color: colorDelegate.isBinding ? StudioTheme.Values.themeInteraction + : StudioTheme.Values.themeTextColor + text: colorDelegate.resolvedValue } } - Component.onCompleted: console.log("DelegateChoice - color", colorDelegate.display) + // This edit delegate combines the binding editor and hex value editor + TableView.editDelegate: DSC.TextField { + id: colorEditDelegate + + anchors.fill: parent + leftPadding: root.leftPadding + + (colorDelegate.bindingEditor ? 0 + : (colorDelegatePreview.width + + colorDelegateRow.spacing)) + + horizontalAlignment: TextInput.AlignLeft + verticalAlignment: TextInput.AlignVCenter + + text: colorDelegate.bindingEditor ? colorDelegate.propertyValue + : colorDelegate.resolvedValue + + RegularExpressionValidator { + id: hexValidator + regularExpression: /#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?/g + } + + validator: colorDelegate.bindingEditor ? null : hexValidator + + Component.onCompleted: colorEditDelegate.selectAll() + Component.onDestruction: colorDelegate.creatingBinding = false + + TableView.onCommit: { + root.setValue(colorEditDelegate.text, + colorDelegate.row, + colorDelegate.column, + colorDelegate.bindingEditor) + } + + // Extra color Rectangle to be shown on top of edit delegate + Rectangle { + anchors.left: parent.left + anchors.leftMargin: root.leftPadding + anchors.verticalCenter: parent.verticalCenter + + width: colorDelegatePreview.width + height: colorDelegatePreview.height + color: colorDelegate.resolvedValue + border.color: "black" + border.width: StudioTheme.Values.border + + visible: !colorDelegate.bindingEditor + + Image { + anchors.fill: parent + source: "qrc:/navigator/icon/checkers.png" + fillMode: Image.Tile + z: -1 + } + + MouseArea { + anchors.fill: parent + enabled: !colorDelegate.isBinding + + onClicked: colorMouseArea.togglePopup() + } + } + } + + //Component.onCompleted: console.log("DelegateChoice - color", colorDelegate.resolvedValue) } } } - StudioControls.PopupDialog { - id: popupDialog + StudioControls.Menu { + id: menu + property var modelIndex + + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + function show(row, column) { + menu.modelIndex = tableView.index(row, column) + menu.popup() + } + + StudioControls.MenuItem { + enabled: { + if (menu.modelIndex) + return tableView.itemAtIndex(menu.modelIndex).isBinding + + return false + } + text: qsTr("Reset") + onTriggered: { + let data = tableView.model.data(menu.modelIndex, CollectionModel.ResolvedValueRole) + var prop = DesignSystemBackend.dsInterface.createThemeProperty("", data, false) + let result = tableView.model.setData(menu.modelIndex, prop, Qt.EditRole) + } + } + + StudioControls.MenuItem { + text: qsTr("Set Binding") + onTriggered: { + let cell = tableView.itemAtIndex(menu.modelIndex) + cell.creatingBinding = true + tableView.edit(menu.modelIndex) + } + } + } + + StudioControls.Menu { + id: modeMenu + + property int column + + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + function show(column) { // Qt.Horizontal + tableView.closeEditor() // Close all currently visible edit delegates + modeMenu.column = column + modeMenu.popup() + } + + StudioControls.MenuItem { + enabled: false + text: qsTr("Duplicate mode") + onTriggered: { console.log("Duplicate mode") } + } + + StudioControls.MenuItem { + text: qsTr("Rename mode") + onTriggered: overlay.show(modeMenu.column, Qt.Horizontal) + } + + StudioControls.MenuItem { + text: qsTr("Delete mode") + onTriggered: tableView.model.removeColumns(modeMenu.column, 1) + } + } + + StudioControls.Menu { + id: variableMenu + + property int row + + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + function show(row) { // Qt.Vertical + tableView.closeEditor() // Close all currently visible edit delegates + variableMenu.row = row + variableMenu.popup() + } + + StudioControls.MenuItem { + enabled: false + text: qsTr("Duplicate variable") + onTriggered: { console.log("Duplicate variable") } + } + + StudioControls.MenuItem { + text: qsTr("Rename variable") + onTriggered: overlay.show(variableMenu.row, Qt.Vertical) + } + + StudioControls.MenuItem { + text: qsTr("Delete variable") + onTriggered: tableView.model.removeRows(variableMenu.row, 1) + } + } + + Rectangle { + id: overlay + + property int section + property var orientation + property var group + + width: 200 + height: 200 + color: "red" + visible: false + + z: 111 + parent: tableView.contentItem + + DSC.TextField { + id: overlayTextField + anchors.fill: parent + leftPadding: root.leftPadding + (overlayIcon.visible ? overlayIcon.width + 8 : 0) + + horizontalAlignment: TextInput.AlignLeft + verticalAlignment: TextInput.AlignVCenter + + onActiveFocusChanged: { + if (!overlayTextField.activeFocus) + overlay.hide() + } + + onAccepted: { + let result = tableView.model.setHeaderData(overlay.section, + overlay.orientation, + overlayTextField.text, + Qt.EditRole) + + // Revoke active focus from text field by forcing active focus on another item + tableView.forceActiveFocus() + } + + Text { + id: overlayIcon + anchors.left: parent.left + anchors.leftMargin: root.leftPadding + + height: parent.height + verticalAlignment: Qt.AlignVCenter + font.family: StudioTheme.Constants.iconFont.family + font.pixelSize: root.iconSize + color: StudioTheme.Values.themeTextColor + + visible: overlay.group !== undefined + + text: { + if (overlay.group === GroupType.Strings) + return StudioTheme.Constants.string_medium + + if (overlay.group === GroupType.Numbers) + return StudioTheme.Constants.number_medium + + if (overlay.group === GroupType.Flags) + return StudioTheme.Constants.flag_medium + + if (overlay.group === GroupType.Colors) + return StudioTheme.Constants.colorSelection_medium + + return StudioTheme.Constants.error_medium + } + } + } + + function show(section, orientation) { + // Close all currently visible edit delegates + tableView.closeEditor() + + if (orientation === Qt.Horizontal) + overlay.parent = horizontalHeaderView.contentItem + else + overlay.parent = verticalHeaderView.contentItem + + overlay.visible = true + overlay.section = section + overlay.orientation = orientation + overlay.group = tableView.model.headerData(section, + orientation, + CollectionModel.GroupRole) + + overlay.layout() + + overlayTextField.text = tableView.model.headerData(section, + orientation, + CollectionModel.EditRole) + overlayTextField.forceActiveFocus() + overlayTextField.selectAll() + } + + function hide() { + overlay.visible = false + } + + function layout() { + if (!overlay.visible) + return + + let item = null + + if (overlay.orientation === Qt.Horizontal) + item = horizontalHeaderView.itemAtCell(Qt.point(overlay.section, 0)) + else + item = verticalHeaderView.itemAtCell(Qt.point(0, overlay.section)) + + let insideViewport = item !== null + + //overlay.visible = insideViewport + if (insideViewport) { + overlay.x = item.x + overlay.y = item.y + overlay.width = item.width + overlay.height = item.height + } + } + + Connections { + target: tableView + + function onLayoutChanged() { overlay.layout() } + } + } + + StudioControls.PopupDialog { + id: colorPopup + + property var modelIndex property QtObject loaderItem: loader.item keepOpen: loader.item?.eyeDropperActive ?? false @@ -357,27 +982,18 @@ Rectangle { sourceComponent: StudioControls.ColorEditorPopup { id: popup - width: popupDialog.contentWidth - visible: popupDialog.visible - parentWindow: popupDialog.window + width: colorPopup.contentWidth + visible: colorPopup.visible + parentWindow: colorPopup.window onActivateColor: function(color) { - console.log("set color", color) - //colorBackend.activateColor(color) + popup.color = color + var prop = DesignSystemBackend.dsInterface.createThemeProperty("", color, false) + let result = tableView.model.setData(colorPopup.modelIndex, prop, Qt.EditRole) } } - Binding { - target: loader.item - property: "color" - value: root.color - when: loader.status === Loader.Ready - } - - onLoaded: { - loader.item.originalColor = root.color - popupDialog.titleBar = loader.item.titleBarContent - } + onLoaded: colorPopup.titleBar = loader.item.titleBarContent } } @@ -403,21 +1019,22 @@ Rectangle { // top left cell // TODO Can't use Cell as it contains required properties Rectangle { - anchors.right: horizontalHeader.left - anchors.bottom: verticalHeader.top + id: topLeftCell + anchors.right: horizontalHeaderView.left + anchors.bottom: verticalHeaderView.top anchors.rightMargin: -root.borderWidth anchors.bottomMargin: -root.borderWidth color: root.backgroundColor - implicitWidth: verticalHeader.width - implicitHeight: horizontalHeader.height + implicitWidth: verticalHeaderView.width + implicitHeight: horizontalHeaderView.height border { width: root.borderWidth color: root.borderColor } - visible: tableView.model // TODO good enough? - z: 101 + visible: tableView.model + z: 99 Row { // TODO might not be necessary anchors.fill: parent @@ -428,13 +1045,13 @@ Rectangle { height: parent.height verticalAlignment: Qt.AlignVCenter color: StudioTheme.Values.themeTextColor - text: "Name" + text: qsTr("Name") } } } HorizontalHeaderView { - id: horizontalHeader + id: horizontalHeaderView anchors.left: scrollView.left anchors.top: parent.top @@ -442,62 +1059,52 @@ Rectangle { syncView: tableView clip: true - z: 100 + z: overlay.visible ? 102 : 100 delegate: Cell { id: horizontalHeaderDelegate - property bool customEditing: false - TapHandler { - onTapped: { - console.log("onTapped") - tableView.closeEditor() - horizontalHeaderDelegate.customEditing = true - horizontalHeaderTextField.forceActiveFocus() + acceptedButtons: Qt.LeftButton | Qt.RightButton + onTapped: function(eventPoint, button) { + if (button === Qt.LeftButton) + overlay.show(horizontalHeaderDelegate.column, Qt.Horizontal) + + if (button === Qt.RightButton) + modeMenu.show(horizontalHeaderDelegate.column) } } - Text { + Row { anchors.fill: parent - leftPadding: root.leftPadding - verticalAlignment: Qt.AlignVCenter - color: StudioTheme.Values.themeTextColor - text: horizontalHeaderDelegate.display - visible: !horizontalHeaderDelegate.customEditing - } + spacing: 8 - DSC.TextField { - id: horizontalHeaderTextField - - anchors.fill: parent - leftPadding: root.leftPadding - horizontalAlignment: TextInput.AlignLeft - verticalAlignment: TextInput.AlignVCenter - text: horizontalHeaderDelegate.display - - visible: horizontalHeaderDelegate.customEditing - - //Component.onCompleted: stringEditDelegate.selectAll() - - onActiveFocusChanged: { - if (!horizontalHeaderTextField.activeFocus) - horizontalHeaderDelegate.customEditing = false + Text { + id: activeIcon + height: parent.height + leftPadding: root.leftPadding + verticalAlignment: Qt.AlignVCenter + font.family: StudioTheme.Constants.iconFont.family + font.pixelSize: StudioTheme.Values.baseIconFontSize + color: StudioTheme.Values.themeTextColor + text: StudioTheme.Constants.apply_small + visible: horizontalHeaderDelegate.isActive } - onEditingFinished: { - console.log("onEditingFinished", horizontalHeaderTextField.text) - horizontalHeaderDelegate.TableView.view.model.setHeaderData(horizontalHeaderDelegate.column, - Qt.Horizontal, - horizontalHeaderTextField.text, - Qt.EditRole) + Text { + height: parent.height + + leftPadding: horizontalHeaderDelegate.isActive ? 0 : root.leftPadding + verticalAlignment: Qt.AlignVCenter + color: StudioTheme.Values.themeTextColor + text: horizontalHeaderDelegate.display } } } } VerticalHeaderView { - id: verticalHeader + id: verticalHeaderView anchors.top: scrollView.top anchors.left: parent.left @@ -505,13 +1112,24 @@ Rectangle { syncView: tableView clip: true - z: 100 + z: overlay.visible ? 102 : 100 delegate: Cell { id: verticalHeaderDelegate required property int group + TapHandler { + acceptedButtons: Qt.LeftButton | Qt.RightButton + onTapped: function(eventPoint, button) { + if (button === Qt.LeftButton) + overlay.show(verticalHeaderDelegate.row, Qt.Vertical) + + if (button === Qt.RightButton) + variableMenu.show(verticalHeaderDelegate.row) + } + } + Row { anchors.fill: parent leftPadding: root.leftPadding @@ -555,8 +1173,8 @@ Rectangle { ScrollView { id: scrollView - anchors.left: verticalHeader.right - anchors.top: horizontalHeader.bottom + anchors.left: verticalHeaderView.right + anchors.top: horizontalHeaderView.bottom anchors.right: parent.right anchors.bottom: parent.bottom @@ -564,6 +1182,8 @@ Rectangle { anchors.topMargin: -root.borderWidth anchors.rightMargin: -root.borderWidth + z: 101 + contentItem: TableView { id: tableView @@ -577,7 +1197,6 @@ Rectangle { } onModelChanged: { - console.log("onModelChanged") tableView.clearColumnWidths() tableView.contentX = 0 tableView.contentY = 0 @@ -588,8 +1207,14 @@ Rectangle { columnSpacing: -root.borderWidth rowSpacing: -root.borderWidth clip: true + boundsBehavior: Flickable.StopAtBounds delegate: chooser + + onActiveFocusChanged: { + if (!tableView.activeFocus) + tableView.closeEditor() + } } ScrollBar.horizontal: StudioControls.TransientScrollBar { @@ -634,14 +1259,14 @@ Rectangle { Layout.fillHeight: true StudioControls.IconTextButton { + id: createModeButton anchors.centerIn: parent buttonIcon: StudioTheme.Constants.add_medium text: qsTr("Create mode") rotation: -90 + enabled: false - onClicked: { - tableView.model.insertColumn(0) - } + onClicked: tableView.model.insertColumns(0, 1) } } @@ -671,15 +1296,22 @@ Rectangle { width: createVariableButton.width + function insertInitalMode() { + if (tableView.model.columnCount()) + return + + tableView.model.insertColumn(0) + } + DSC.MenuItem { text: qsTr("Color") buttonIcon: StudioTheme.Constants.colorSelection_medium onTriggered: { - console.log(">>> Add Color Property") + createVariableMenu.insertInitalMode() tableView.model.addProperty(GroupType.Colors, - "color_new", - "#800080", - false) + "color_new", + "#800080", + false) } } @@ -687,11 +1319,11 @@ Rectangle { text: qsTr("Number") buttonIcon: StudioTheme.Constants.number_medium onTriggered: { - console.log(">>> Add Number Property") + createVariableMenu.insertInitalMode() tableView.model.addProperty(GroupType.Numbers, - "number_new", - 0, - false) + "number_new", + 0, + false) } } @@ -699,11 +1331,11 @@ Rectangle { text: qsTr("String") buttonIcon: StudioTheme.Constants.string_medium onTriggered: { - console.log(">>> Add String Property") - tableView.model.addProperty(GroupType.Flags, - "string_new", - "String value", - false) + createVariableMenu.insertInitalMode() + tableView.model.addProperty(GroupType.Strings, + "string_new", + "String value", + false) } } @@ -711,11 +1343,11 @@ Rectangle { text: qsTr("Boolean") buttonIcon: StudioTheme.Constants.flag_medium onTriggered: { - console.log(">>> Add Boolean Property") + createVariableMenu.insertInitalMode() tableView.model.addProperty(GroupType.Flags, - "boolean_new", - true, - false) + "boolean_new", + true, + false) } } } diff --git a/share/qtcreator/qmldesigner/designsystem/imports/DesignSystemControls/BindingIndicator.qml b/share/qtcreator/qmldesigner/designsystem/imports/DesignSystemControls/BindingIndicator.qml new file mode 100644 index 00000000000..096b639e246 --- /dev/null +++ b/share/qtcreator/qmldesigner/designsystem/imports/DesignSystemControls/BindingIndicator.qml @@ -0,0 +1,62 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Templates as T +import StudioTheme as StudioTheme + +Item { + id: control + + property StudioTheme.ControlStyle style: StudioTheme.Values.controlStyle + property alias icon: icon + + property bool hover: mouseArea.containsMouse//false + property bool pressed: false + property bool forceVisible: false + + implicitWidth: control.style.actionIndicatorSize.width + implicitHeight: control.style.actionIndicatorSize.height + + signal clicked + z: 10 + + T.Label { + id: icon + anchors.fill: parent + text: StudioTheme.Constants.actionIcon + color: control.style.icon.idle + font.family: StudioTheme.Constants.iconFont.family + font.pixelSize: control.style.baseIconFontSize + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + states: [ + State { + name: "hover" + when: control.hover && !control.pressed && control.enabled + PropertyChanges { + target: icon + scale: 1.2 + visible: true + } + }, + State { + name: "disable" + when: !control.enabled + PropertyChanges { + target: icon + color: control.style.icon.disabled + } + } + ] + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + //onContainsMouseChanged: control.hover = mouseArea.containsMouse + onClicked: control.clicked() + } +} diff --git a/share/qtcreator/qmldesigner/designsystem/imports/DesignSystemControls/SpinBox.qml b/share/qtcreator/qmldesigner/designsystem/imports/DesignSystemControls/SpinBox.qml new file mode 100644 index 00000000000..165ca1f21de --- /dev/null +++ b/share/qtcreator/qmldesigner/designsystem/imports/DesignSystemControls/SpinBox.qml @@ -0,0 +1,365 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Templates as T +import StudioTheme as StudioTheme +import StudioQuickUtils + +T.SpinBox { + id: control + + property StudioTheme.ControlStyle style: StudioTheme.Values.controlStyle + + property real realFrom: 0.0 + property real realTo: 99.0 + property real realValue: 1.0 + property real realStepSize: 1.0 + + property alias labelColor: spinBoxInput.color + + property int decimals: 0 + + property real minStepSize: { + var tmpMinStepSize = Number((control.realStepSize * 0.1).toFixed(control.decimals)) + return (tmpMinStepSize) ? tmpMinStepSize : control.realStepSize + } + property real maxStepSize: { + var tmpMaxStepSize = Number((control.realStepSize * 10.0).toFixed(control.decimals)) + return (tmpMaxStepSize < control.realTo) ? tmpMaxStepSize : control.realStepSize + } + + property bool edit: spinBoxInput.activeFocus + // This property is used to indicate the global hover state + property bool hover: (spinBoxInput.hover || spinBoxIndicatorUp.hover || spinBoxIndicatorDown.hover) + && control.enabled + + property bool dirty: false // user modification flag + + property bool spinBoxIndicatorVisible: true + property real __spinBoxIndicatorWidth: control.style.spinBoxIndicatorSize.width + property real __spinBoxIndicatorHeight: control.height / 2 - control.style.borderWidth + + property alias compressedValueTimer: myTimer + + property string preFocusText: "" + + signal realValueModified + signal compressedRealValueModified + signal indicatorPressed + + locale: Utils.locale + + // Use custom wheel handling due to bugs + property bool __wheelEnabled: false + wheelEnabled: false + hoverEnabled: true + + width: control.style.controlSize.width + height: control.style.controlSize.height + + leftPadding: spinBoxIndicatorDown.x + spinBoxIndicatorDown.width + rightPadding: control.style.borderWidth + + font.pixelSize: control.style.baseFontSize + editable: true + + // Leave this in for now + from: -99 + value: 0 + to: 99 + + function checkAndClearFocus() { + if (!spinBoxIndicatorUp.activeFocus && !spinBoxIndicatorDown.activeFocus && !spinBoxInput.activeFocus) + control.focus = false + } + + DoubleValidator { + id: doubleValidator + locale: control.locale + notation: DoubleValidator.StandardNotation + decimals: control.decimals + bottom: Math.min(control.realFrom, control.realTo) + top: Math.max(control.realFrom, control.realTo) + } + + IntValidator { + id: intValidator + locale: control.locale + bottom: Math.round(Math.min(control.realFrom, control.realTo)) + top: Math.round(Math.max(control.realFrom, control.realTo)) + } + + validator: control.decimals === 0 ? intValidator : doubleValidator + + up.indicator: SpinBoxIndicator { + id: spinBoxIndicatorUp + style: control.style + parentHover: control.hover + parentEdit: control.edit + iconFlip: -1 + visible: control.spinBoxIndicatorVisible + onRealPressed: control.indicatorPressed() + onRealReleased: control.realIncrease() + onRealPressAndHold: control.realIncrease() + x: control.style.borderWidth + y: control.style.borderWidth + width: control.spinBoxIndicatorVisible ? control.__spinBoxIndicatorWidth : 0 + height: control.spinBoxIndicatorVisible ? control.__spinBoxIndicatorHeight : 0 + + realEnabled: (control.realFrom < control.realTo) ? (control.realValue < control.realTo) + : (control.realValue > control.realTo) + } + + down.indicator: SpinBoxIndicator { + id: spinBoxIndicatorDown + style: control.style + parentHover: control.hover + parentEdit: control.edit + visible: control.spinBoxIndicatorVisible + onRealPressed: control.indicatorPressed() + onRealReleased: control.realDecrease() + onRealPressAndHold: control.realDecrease() + x: control.style.borderWidth + y: spinBoxIndicatorUp.y + spinBoxIndicatorUp.height + width: control.spinBoxIndicatorVisible ? control.__spinBoxIndicatorWidth : 0 + height: control.spinBoxIndicatorVisible ? control.__spinBoxIndicatorHeight : 0 + + realEnabled: (control.realFrom < control.realTo) ? (control.realValue > control.realFrom) + : (control.realValue < control.realFrom) + } + + contentItem: SpinBoxInput { + id: spinBoxInput + style: control.style + validator: control.validator + font: control.font + readOnly: !control.editable + inputMethodHints: control.inputMethodHints + + function handleEditingFinished() { + control.checkAndClearFocus() + + // Keep the dirty state before calling setValueFromInput(), + // it will be set to false (cleared) internally + var valueModified = control.dirty + + control.setValueFromInput() + myTimer.stop() + + // Only trigger the signal, if the value was modified + if (valueModified) + control.compressedRealValueModified() + } + + onEditingFinished: { + spinBoxInput.focus = false + spinBoxInput.handleEditingFinished() + } + + onTextEdited: control.dirty = true + } + + background: Rectangle { + id: spinBoxBackground + color: control.style.background.idle + border.color: control.style.border.idle + border.width: control.style.borderWidth + x: 0 + width: control.width + height: control.height + } + + textFromValue: function (value, locale) { + // -128 is the value of QLocale::FloatingPointShortest + return Number(control.realValue).toLocaleString(locale, 'f', -128) + } + + valueFromText: function (text, locale) { + control.setRealValue(Number.fromLocaleString(locale, spinBoxInput.text)) + + return 0 + } + + states: [ + State { + name: "default" + when: control.enabled && !control.hover && !control.hovered + && !control.edit + PropertyChanges { + target: control + __wheelEnabled: false + } + PropertyChanges { + target: spinBoxInput + selectByMouse: false + } + PropertyChanges { + target: spinBoxBackground + color: control.style.background.idle + border.color: control.style.border.idle + } + }, + State { + name: "hover" + when: control.enabled && control.hover && control.hovered + && !control.edit + PropertyChanges { + target: spinBoxBackground + border.color: control.style.border.hover + } + }, + State { + name: "edit" + when: control.edit + PropertyChanges { + target: control + __wheelEnabled: true + } + PropertyChanges { + target: spinBoxInput + selectByMouse: true + } + PropertyChanges { + target: spinBoxBackground + border.color: control.style.border.interaction + } + }, + State { + name: "disable" + when: !control.enabled + PropertyChanges { + target: spinBoxBackground + border.color: control.style.border.disabled + } + } + ] + + Timer { + id: myTimer + repeat: false + running: false + interval: 400 + onTriggered: control.compressedRealValueModified() + } + + onRealValueChanged: { + control.setRealValue(control.realValue) // sanitize and clamp realValue + spinBoxInput.text = control.textFromValue(control.realValue, control.locale) + control.value = 0 // Without setting value back to 0, it can happen that one of + // the indicator will be disabled due to range logic. + } + onRealValueModified: myTimer.restart() + onFocusChanged: { + if (control.focus) + control.dirty = false + } + onDisplayTextChanged: spinBoxInput.text = control.displayText + onActiveFocusChanged: { + if (control.activeFocus) { // QTBUG-75862 && mySpinBox.focusReason === Qt.TabFocusReason) + control.preFocusText = spinBoxInput.text + spinBoxInput.selectAll() + } else { + // Make sure displayed value is correct after focus loss, as onEditingFinished + // doesn't trigger when value is something validator doesn't accept. + if (spinBoxInput.text === "") + spinBoxInput.text = control.textFromValue(control.realValue, control.locale) + + if (control.dirty) + spinBoxInput.handleEditingFinished() + } + } + onDecimalsChanged: spinBoxInput.text = control.textFromValue(control.realValue, control.locale) + + Keys.onPressed: function(event) { + if (event.key === Qt.Key_Up || event.key === Qt.Key_Down) { + event.accepted = true + + // Store current step size + var currStepSize = control.realStepSize + + // Set realStepSize according to used modifier key + if (event.modifiers & Qt.ControlModifier) + control.realStepSize = control.minStepSize + + if (event.modifiers & Qt.ShiftModifier) + control.realStepSize = control.maxStepSize + + if (event.key === Qt.Key_Up) + control.realIncrease() + else + control.realDecrease() + + // Reset realStepSize + control.realStepSize = currStepSize + } + + if (event.key === Qt.Key_Escape) { + spinBoxInput.text = control.preFocusText + control.dirty = true + spinBoxInput.handleEditingFinished() + } + } + + function clamp(v, lo, hi) { + return (v < lo || v > hi) ? Math.min(Math.max(lo, v), hi) : v + } + + function setValueFromInput() { + if (!control.dirty) + return + + // FIX: This is a temporary fix for QTBUG-74239 + var currValue = control.realValue + + // Call the function but don't use return value. The realValue property + // will be implicitly set inside the function/procedure. + control.valueFromText(spinBoxInput.text, control.locale) + + if (control.realValue !== currValue) { + control.realValueModified() + } else { + // Check if input text differs in format from the current value + var tmpInputValue = control.textFromValue(control.realValue, control.locale) + + if (tmpInputValue !== spinBoxInput.text) + spinBoxInput.text = tmpInputValue + } + + control.dirty = false + } + + function setRealValue(value) { + if (Number.isNaN(value)) + value = 0 + + if (control.decimals === 0) + value = Math.round(value) + + control.realValue = control.clamp(value, + control.validator.bottom, + control.validator.top) + } + + function realDecrease() { + // Store the current value for comparison + var currValue = control.realValue + control.valueFromText(spinBoxInput.text, control.locale) + + control.setRealValue(control.realValue - control.realStepSize) + + if (control.realValue !== currValue) + control.realValueModified() + } + + function realIncrease() { + // Store the current value for comparison + var currValue = control.realValue + control.valueFromText(spinBoxInput.text, control.locale) + + control.setRealValue(control.realValue + control.realStepSize) + + if (control.realValue !== currValue) + control.realValueModified() + } +} diff --git a/share/qtcreator/qmldesigner/designsystem/imports/DesignSystemControls/SpinBoxIndicator.qml b/share/qtcreator/qmldesigner/designsystem/imports/DesignSystemControls/SpinBoxIndicator.qml new file mode 100644 index 00000000000..6fd56d1a1b7 --- /dev/null +++ b/share/qtcreator/qmldesigner/designsystem/imports/DesignSystemControls/SpinBoxIndicator.qml @@ -0,0 +1,247 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Templates as T +import StudioTheme as StudioTheme + +Rectangle { + id: control + + property StudioTheme.ControlStyle style: StudioTheme.Values.controlStyle + + property bool hover: spinBoxIndicatorMouseArea.containsMouse + property bool pressed: spinBoxIndicatorMouseArea.containsPress + property bool released: false + property bool realEnabled: true + + property bool parentHover: false + property bool parentEdit: false + + signal realPressed + signal realPressAndHold + signal realReleased + + property alias iconFlip: spinBoxIndicatorIconScale.yScale + + + color: control.style.background.idle + border.width: 0 + + onEnabledChanged: control.syncEnabled() + onRealEnabledChanged: { + control.syncEnabled() + if (control.realEnabled === false) { + pressAndHoldTimer.stop() + spinBoxIndicatorMouseArea.pressedAndHeld = false + } + } + + // This function is meant to synchronize enabled with realEnabled to avoid + // the internal logic messing with the actual state. + function syncEnabled() { + control.enabled = control.realEnabled + } + + Timer { + id: pressAndHoldTimer + repeat: true + running: false + interval: 100 + onTriggered: control.realPressAndHold() + } + + // This MouseArea is a workaround to avoid some hover state related bugs + // when using the actual signal 'up.hovered'. QTBUG-74688 + MouseArea { + id: spinBoxIndicatorMouseArea + + property bool pressedAndHeld: false + + anchors.fill: parent + hoverEnabled: true + pressAndHoldInterval: 500 + onPressed: function(mouse) { + //if (control.__parentControl.activeFocus) + // control.forceActiveFocus() + + control.realPressed() + mouse.accepted = true + } + onPressAndHold: { + pressAndHoldTimer.restart() + spinBoxIndicatorMouseArea.pressedAndHeld = true + } + onReleased: function(mouse) { + // Only trigger real released when pressAndHold isn't active + if (!pressAndHoldTimer.running && containsMouse) + control.realReleased() + pressAndHoldTimer.stop() + mouse.accepted = true + spinBoxIndicatorMouseArea.pressedAndHeld = false + } + onEntered: { + if (spinBoxIndicatorMouseArea.pressedAndHeld) + pressAndHoldTimer.restart() + } + onExited: { + if (pressAndHoldTimer.running) + pressAndHoldTimer.stop() + } + } + + T.Label { + id: spinBoxIndicatorIcon + text: StudioTheme.Constants.upDownSquare2 + color: control.style.icon.idle + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pixelSize: control.style.smallIconFontSize + font.family: StudioTheme.Constants.iconFont.family + anchors.fill: parent + transform: Scale { + id: spinBoxIndicatorIconScale + origin.x: 0 + origin.y: spinBoxIndicatorIcon.height / 2 + yScale: 1 + } + + states: [ + State { + name: "default" + when: control.enabled && !control.hover && !control.parentEdit && !control.parentHover + PropertyChanges { + target: spinBoxIndicatorIcon + color: control.style.icon.idle + } + }, + State { + name: "globalHover" + when: control.enabled && !control.hover && !control.parentEdit && control.parentHover + PropertyChanges { + target: spinBoxIndicatorIcon + color: control.style.icon.idle + } + }, + State { + name: "hover" + when: control.enabled && control.hover && !control.pressed && control.parentHover + PropertyChanges { + target: spinBoxIndicatorIcon + color: control.style.icon.hover + } + }, + State { + name: "press" + when: control.enabled && control.pressed + PropertyChanges { + target: spinBoxIndicatorIcon + color: control.style.icon.idle + } + }, + State { + name: "parentEdit" + when: control.parentEdit && control.enabled + PropertyChanges { + target: spinBoxIndicatorIcon + color: control.style.icon.idle + } + }, + State { + name: "disable" + when: !control.enabled + PropertyChanges { + target: spinBoxIndicatorIcon + color: control.style.icon.disabled + } + } + ] + } + + states: [ + State { + name: "default" + when: !control.parentEdit && !control.hover && !control.parentHover + PropertyChanges { + target: spinBoxIndicatorIcon + visible: false + } + PropertyChanges { + target: control + color: control.style.background.idle + } + }, + State { + name: "globalHover" + when: control.enabled && !control.hover && !control.parentEdit && control.parentHover + PropertyChanges { + target: spinBoxIndicatorIcon + visible: true + } + PropertyChanges { + target: control + color: control.style.background.globalHover + } + }, + State { + name: "hover" + when: control.enabled && control.hover && !control.pressed && control.parentHover + PropertyChanges { + target: spinBoxIndicatorIcon + visible: true + } + PropertyChanges { + target: control + color: control.style.background.hover + } + }, + State { + name: "press" + when: control.enabled && control.pressed + PropertyChanges { + target: spinBoxIndicatorIcon + visible: true + } + PropertyChanges { + target: control + color: control.style.interaction + } + }, + State { + name: "parentEdit" + when: control.parentEdit && control.enabled + PropertyChanges { + target: spinBoxIndicatorIcon + visible: true + } + PropertyChanges { + target: control + color: control.style.background.idle + } + }, + State { + name: "disable" + when: !control.enabled + PropertyChanges { + target: spinBoxIndicatorIcon + visible: false + } + PropertyChanges { + target: control + color: control.style.background.disabled + } + }, + State { + name: "limit" + when: !control.enabled && !control.realEnabled && control.parentHover + PropertyChanges { + target: spinBoxIndicatorIcon + visible: true + } + PropertyChanges { + target: control + color: control.style.background.idle + } + } + ] +} diff --git a/share/qtcreator/qmldesigner/designsystem/imports/DesignSystemControls/SpinBoxInput.qml b/share/qtcreator/qmldesigner/designsystem/imports/DesignSystemControls/SpinBoxInput.qml new file mode 100644 index 00000000000..a4870eb9491 --- /dev/null +++ b/share/qtcreator/qmldesigner/designsystem/imports/DesignSystemControls/SpinBoxInput.qml @@ -0,0 +1,100 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import QtQuick +import QtQuick.Templates as T +import StudioTheme as StudioTheme + +TextInput { + id: control + + property StudioTheme.ControlStyle style: StudioTheme.Values.controlStyle + + property bool edit: control.activeFocus + property bool drag: false + property bool hover: hoverHandler.hovered && control.enabled + + z: 2 + color: control.style.text.idle + selectionColor: control.style.text.selection + selectedTextColor: control.style.text.selectedText + + horizontalAlignment: Qt.AlignLeft + verticalAlignment: Qt.AlignVCenter + leftPadding: control.style.inputHorizontalPadding + rightPadding: control.style.inputHorizontalPadding + + selectByMouse: false + activeFocusOnPress: false + clip: true + + // TextInput focus needs to be set to activeFocus whenever it changes, + // otherwise TextInput will get activeFocus whenever the parent SpinBox gets + // activeFocus. This will lead to weird side effects. + onActiveFocusChanged: control.focus = control.activeFocus + + HoverHandler { id: hoverHandler } + + Rectangle { + id: textInputBackground + x: 0 + y: control.style.borderWidth + z: -1 + width: control.width + height: control.height - (control.style.borderWidth * 2) + color: control.style.background.idle + border.width: 0 + } + + // Ensure that we get Up and Down key press events first + Keys.onShortcutOverride: function(event) { + event.accepted = (event.key === Qt.Key_Up || event.key === Qt.Key_Down) + } + + states: [ + State { + name: "default" + when: control.enabled && !control.edit && !control.hover + PropertyChanges { + target: textInputBackground + color: control.style.background.idle + } + }, + State { + name: "globalHover" + when: !control.hover && !control.edit + PropertyChanges { + target: textInputBackground + color: control.style.background.globalHover + } + }, + State { + name: "hover" + when: control.hover && !control.edit + PropertyChanges { + target: textInputBackground + color: control.style.background.hover + } + }, + State { + name: "edit" + when: control.edit + PropertyChanges { + target: textInputBackground + color: control.style.background.interaction + } + }, + State { + name: "disable" + when: !control.enabled + PropertyChanges { + target: textInputBackground + color: control.style.background.disabled + } + PropertyChanges { + target: control + color: control.style.text.disabled + } + } + ] +} diff --git a/share/qtcreator/qmldesigner/designsystem/imports/DesignSystemControls/Switch.qml b/share/qtcreator/qmldesigner/designsystem/imports/DesignSystemControls/Switch.qml index 4ccdca4486c..6f98f90f72b 100644 --- a/share/qtcreator/qmldesigner/designsystem/imports/DesignSystemControls/Switch.qml +++ b/share/qtcreator/qmldesigner/designsystem/imports/DesignSystemControls/Switch.qml @@ -4,7 +4,7 @@ import QtQuick import QtQuick.Templates as T -import StudioTheme 1.0 as StudioTheme +import StudioTheme as StudioTheme T.Switch { id: control @@ -14,6 +14,7 @@ T.Switch { // This property is used to indicate the global hover state property bool hover: control.hovered && control.enabled property bool edit: false + property bool readonly: false property alias labelVisible: label.visible property alias labelColor: label.color @@ -33,6 +34,8 @@ T.Switch { hoverEnabled: true activeFocusOnTab: false + enabled: !control.readonly + indicator: Rectangle { id: switchBackground x: 0 @@ -60,6 +63,7 @@ T.Switch { color: control.style.icon.idle border.width: 0 } + } contentItem: T.Label { @@ -133,7 +137,7 @@ T.Switch { }, State { name: "disable" - when: !control.enabled && !control.checked + when: !control.enabled && !control.checked && !control.readonly PropertyChanges { target: switchBackground color: control.style.background.disabled @@ -186,8 +190,19 @@ T.Switch { }, State { name: "disableChecked" - when: !control.enabled && control.checked + when: !control.enabled && control.checked && !control.readonly extend: "disable" + }, + + State { + name: "readonly" + when: !control.enabled && !control.checked && control.readonly + extend: "default" + }, + State { + name: "readonlyChecked" + when: !control.enabled && control.checked && control.readonly + extend: "defaultChecked" } ] } diff --git a/share/qtcreator/qmldesigner/designsystem/imports/DesignSystemControls/qmldir b/share/qtcreator/qmldesigner/designsystem/imports/DesignSystemControls/qmldir index 0f9345ed7d2..4556e1a8f3d 100644 --- a/share/qtcreator/qmldesigner/designsystem/imports/DesignSystemControls/qmldir +++ b/share/qtcreator/qmldesigner/designsystem/imports/DesignSystemControls/qmldir @@ -1,3 +1,7 @@ +BindingIndicator 1.0 BindingIndicator.qml MenuItem 1.0 MenuItem.qml +SpinBox 1.0 SpinBox.qml +SpinBoxIndicator 1.0 SpinBoxIndicator.qml +SpinBoxInput 1.0 SpinBoxInput.qml Switch 1.0 Switch.qml TextField 1.0 TextField.qml diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/DialogButton.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/DialogButton.qml index cce96797726..fff8db4ae84 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/DialogButton.qml +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/StudioControls/DialogButton.qml @@ -3,27 +3,29 @@ import QtQuick import QtQuick.Templates as T -import StudioTheme 1.0 as StudioTheme +import StudioTheme as StudioTheme T.Button { id: control property StudioTheme.ControlStyle style: StudioTheme.Values.controlStyle - implicitWidth: Math.max(buttonBackground ? buttonBackground.implicitWidth : 0, - textItem.implicitWidth + leftPadding + rightPadding) - implicitHeight: Math.max(buttonBackground ? buttonBackground.implicitHeight : 0, - textItem.implicitHeight + topPadding + bottomPadding) + implicitWidth: Math.max(control.style.squareControlSize.width, + implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(control.style.squareControlSize.height, + implicitBackgroundHeight + topInset + bottomInset, + implicitContentHeight + topPadding + bottomPadding) + leftPadding: control.style.dialogPadding rightPadding: control.style.dialogPadding background: Rectangle { - id: buttonBackground - implicitWidth: 70 - implicitHeight: 20 + id: controlBackground color: control.style.background.idle border.color: control.style.border.idle - anchors.fill: parent + border.width: control.style.borderWidth + radius: control.style.radius } contentItem: Text { @@ -41,7 +43,7 @@ T.Button { when: control.enabled && !control.down && !control.hovered && !control.checked PropertyChanges { - target: buttonBackground + target: controlBackground color: control.highlighted ? control.style.interaction : control.style.background.idle border.color: control.style.border.idle @@ -56,7 +58,7 @@ T.Button { when: control.enabled && control.hovered && !control.checked && !control.down PropertyChanges { - target: buttonBackground + target: controlBackground color: control.style.background.hover border.color: control.style.border.hover } @@ -70,9 +72,9 @@ T.Button { when: control.enabled && (control.checked || control.down) PropertyChanges { - target: buttonBackground - color: control.style.background.interaction - border.color: control.style.border.interaction + target: controlBackground + color: control.style.interaction + border.color: control.style.interaction } PropertyChanges { target: textItem @@ -83,7 +85,7 @@ T.Button { name: "disable" when: !control.enabled PropertyChanges { - target: buttonBackground + target: controlBackground color: control.style.background.disabled border.color: control.style.border.disabled } diff --git a/src/plugins/qmldesigner/components/designsystemview/collectionmodel.cpp b/src/plugins/qmldesigner/components/designsystemview/collectionmodel.cpp index a9c4922caf4..08caa829935 100644 --- a/src/plugins/qmldesigner/components/designsystemview/collectionmodel.cpp +++ b/src/plugins/qmldesigner/components/designsystemview/collectionmodel.cpp @@ -116,6 +116,11 @@ QVariant CollectionModel::headerData(int section, Qt::Orientation orientation, i Qt::ItemFlags CollectionModel::flags(const QModelIndex &index) const { + // If group type is FLAGS and not binding block editable + if (data(index, Roles::GroupRole).value() == GroupType::Flags + && !data(index, Roles::BindingRole).toBool()) + return QAbstractItemModel::flags(index); + return Qt::ItemIsEditable | QAbstractItemModel::flags(index); } @@ -214,9 +219,9 @@ bool CollectionModel::setData(const QModelIndex &index, const QVariant &value, i p.name = propName; const ThemeId id = m_themeIdList[index.column()]; if (m_collection->updateProperty(id, groupType, p)) { - beginResetModel(); updateCache(); - endResetModel(); + + emit dataChanged(index, index); } } default: diff --git a/src/plugins/qmldesigner/components/designsystemview/collectionmodel.h b/src/plugins/qmldesigner/components/designsystemview/collectionmodel.h index c0b43500012..118f55ce152 100644 --- a/src/plugins/qmldesigner/components/designsystemview/collectionmodel.h +++ b/src/plugins/qmldesigner/components/designsystemview/collectionmodel.h @@ -25,6 +25,7 @@ public: ResolvedValueRole, PropertyValueRole }; + Q_ENUM(Roles) Q_PROPERTY(QStringList themeNames READ themeNameList NOTIFY themeNameChanged FINAL) diff --git a/src/plugins/qmldesigner/components/designsystemview/designsysteminterface.cpp b/src/plugins/qmldesigner/components/designsystemview/designsysteminterface.cpp index e57131e9b98..3066e09e46a 100644 --- a/src/plugins/qmldesigner/components/designsystemview/designsysteminterface.cpp +++ b/src/plugins/qmldesigner/components/designsystemview/designsysteminterface.cpp @@ -16,6 +16,7 @@ DesignSystemInterface::DesignSystemInterface(DSStore *store) { qmlRegisterUncreatableMetaObject( QmlDesigner::staticMetaObject, "QmlDesigner.DesignSystem", 1, 0, "GroupType", ""); + qmlRegisterUncreatableType("QmlDesigner.DesignSystem", 1, 0, "CollectionModel", ""); } DesignSystemInterface::~DesignSystemInterface() {}