QmlDesigner: Add invalid input notification

* Add notifications that show the user when invalid was made and
  automatically reverted by the backend
* Rename Design System to Design Tokens
* Show warning for cyclic dependencies
* Fix invalid binding not being able to reset

Change-Id: I6effc9bf47e086dd3007f96c15b083a972ee79b5
Reviewed-by: Vikas Pachdha <vikas.pachdha@qt.io>
This commit is contained in:
Henning Gruendl
2025-03-28 13:05:42 +01:00
committed by Henning Gründl
parent 4fdf1522ff
commit 3d8237845e
5 changed files with 220 additions and 23 deletions

View File

@@ -81,10 +81,190 @@ Rectangle {
} }
function setValue(value: var, row: int, column: int, isBinding: bool): bool { function setValue(value: var, row: int, column: int, isBinding: bool): bool {
console.log("setValue(", value, row, column, isBinding, ")") let result = tableView.model.setData(tableView.index(row, column),
return tableView.model.setData(tableView.index(row, column),
DesignSystemBackend.dsInterface.createThemeProperty("", value, isBinding), DesignSystemBackend.dsInterface.createThemeProperty("", value, isBinding),
Qt.EditRole) Qt.EditRole)
if (!result)
overlayInvalid.showData(row, column)
return result
}
function dismissInvalidOverlay() {
overlayInvalid.hide()
notification.visible = false
}
Rectangle {
id: overlayInvalid
property Item cellItem
color: "transparent"
border {
width: StudioTheme.Values.border
color: StudioTheme.Values.themeAmberLight
}
visible: false
z: 112
function show() {
overlayInvalid.visible = true
overlayInvalid.layout()
notification.visible = true
notification.forceActiveFocus()
}
function showData(row: int, column: int) {
overlayInvalid.parent = tableView.contentItem
overlayInvalid.cellItem = tableView.itemAtCell(Qt.point(column, row))
notification.message = qsTr("Invalid binding. Please use a valid non-cyclic binding.")
overlayInvalid.show()
}
function showHeaderData(section: int, orientation: var) {
if (orientation === Qt.Horizontal) {
overlayInvalid.parent = horizontalHeaderView.contentItem
overlayInvalid.cellItem = horizontalHeaderView.itemAtCell(Qt.point(overlay.section, 0))
} else {
overlayInvalid.parent = verticalHeaderView.contentItem
overlayInvalid.cellItem = verticalHeaderView.itemAtCell(Qt.point(0, overlay.section))
}
notification.message = qsTr("This name is already in use, please use a different name.")
overlayInvalid.show()
}
function hide() {
overlayInvalid.visible = false
}
function layout() {
if (!overlayInvalid.visible)
return
if (overlayInvalid.cellItem !== null) {
overlayInvalid.x = overlayInvalid.cellItem.x + 1
overlayInvalid.y = overlayInvalid.cellItem.y + 1
overlayInvalid.width = overlayInvalid.cellItem.width - 2
overlayInvalid.height = overlayInvalid.cellItem.height - 2
}
}
Connections {
target: tableView
function onLayoutChanged() { overlayInvalid.layout() }
}
}
Rectangle {
id: notification
property alias message: contentItemText.text
width: 260
height: 78
z: 666
visible: false
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 20
color: StudioTheme.Values.themePopoutBackground
border.color: "#636363"
border.width: StudioTheme.Values.border
onActiveFocusChanged: {
if (!notification.activeFocus)
root.dismissInvalidOverlay()
}
Column {
id: column
anchors.fill: parent
anchors.margins: StudioTheme.Values.border
Item {
id: titleBarItem
width: parent.width
height: StudioTheme.Values.height
Row {
id: row
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 4
spacing: 4
Item {
id: titleBarContent
width: row.width - row.spacing - closeIndicator.width
height: row.height
Row {
anchors.fill: parent
spacing: 10
T.Label {
anchors.verticalCenter: parent.verticalCenter
font.pixelSize: root.customStyle.mediumIconFontSize
text: StudioTheme.Constants.warning2_medium
font.family: StudioTheme.Constants.iconFont.family
color: StudioTheme.Values.themeAmberLight
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Warning")
color: StudioTheme.Values.themeTextColor
}
}
}
StudioControls.IconIndicator {
id: closeIndicator
anchors.verticalCenter: parent.verticalCenter
icon: StudioTheme.Constants.colorPopupClose
pixelSize: StudioTheme.Values.myIconFontSize
onClicked: root.dismissInvalidOverlay()
}
}
}
Item {
id: contentItem
width: parent.width
height: parent.height - titleBarItem.height
Column {
anchors.fill: parent
anchors.margins: 8
anchors.topMargin: 4
Item {
width: parent.width
height: parent.height
Text {
id: contentItemText
anchors.fill: parent
wrapMode: Text.Wrap
elide: Text.ElideRight
color: StudioTheme.Values.themeTextColor
}
}
}
}
}
} }
Connections { Connections {
@@ -154,7 +334,7 @@ Rectangle {
StudioControls.Dialog { StudioControls.Dialog {
id: createCollectionDialog id: createCollectionDialog
property alias newCollectionName: createCollectionTextField.text; property alias newCollectionName: createCollectionTextField.text
title: qsTr("Create collection") title: qsTr("Create collection")
width: Math.min(300, root.width) width: Math.min(300, root.width)
closePolicy: Popup.CloseOnEscape closePolicy: Popup.CloseOnEscape
@@ -318,7 +498,7 @@ Rectangle {
StudioControls.MenuItem { StudioControls.MenuItem {
text: qsTr("Create collection") text: qsTr("Create collection")
onTriggered: { onTriggered: {
createCollectionDialog.newCollectionName = DesignSystemBackend.dsInterface.generateCollectionName(qsTr("NewCollection")); createCollectionDialog.newCollectionName = DesignSystemBackend.dsInterface.generateCollectionName(qsTr("NewCollection"))
createCollectionDialog.open() createCollectionDialog.open()
} }
} }
@@ -338,10 +518,7 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
buttonIcon: StudioTheme.Constants.updateContent_medium buttonIcon: StudioTheme.Constants.updateContent_medium
tooltip: qsTr("Refresh") tooltip: qsTr("Refresh")
onClicked: { onClicked: DesignSystemBackend.dsInterface.loadDesignSystem()
DesignSystemBackend.dsInterface.loadDesignSystem()
//root.loadModel(DesignSystemBackend.dsInterface.collections[0])
}
} }
} }
} }
@@ -361,6 +538,7 @@ Rectangle {
required property var propertyValue required property var propertyValue
readonly property bool bindingEditor: cell.isBinding || tableView.model.editableOverride readonly property bool bindingEditor: cell.isBinding || tableView.model.editableOverride
readonly property bool isValid: cell.resolvedValue !== undefined
color: root.backgroundColor color: root.backgroundColor
implicitWidth: root.cellWidth implicitWidth: root.cellWidth
@@ -377,9 +555,12 @@ Rectangle {
HoverHandler { id: cellHoverHandler } HoverHandler { id: cellHoverHandler }
DSC.BindingIndicator { DSC.BindingIndicator {
icon.text: dataCell.isBinding ? StudioTheme.Constants.actionIconBinding id: bindingIndicator
icon.text: !dataCell.isValid ? StudioTheme.Constants.warning2_medium
: dataCell.isBinding ? StudioTheme.Constants.actionIconBinding
: StudioTheme.Constants.actionIcon : StudioTheme.Constants.actionIcon
icon.color: dataCell.isBinding ? StudioTheme.Values.themeInteraction icon.color: !dataCell.isValid ? StudioTheme.Values.themeAmberLight
: dataCell.isBinding ? StudioTheme.Values.themeInteraction
: StudioTheme.Values.themeTextColor : StudioTheme.Values.themeTextColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -391,8 +572,23 @@ Rectangle {
tableView.closeEditor() tableView.closeEditor()
menu.show(dataCell.row, dataCell.column) menu.show(dataCell.row, dataCell.column)
} }
onHoverChanged: {
if (dataCell.isValid)
return
if (bindingIndicator.hover)
toolTipInvalid.showText(dataCell,
Qt.point(bindingIndicator.x + bindingIndicator.width,
bindingIndicator.y),
qsTr("Invalid binding. Cyclic binding is not allowed."))
else
toolTipInvalid.hideText()
} }
} }
}
StudioControls.ToolTipExt { id: toolTipInvalid }
DelegateChooser { DelegateChooser {
id: chooser id: chooser
@@ -783,7 +979,7 @@ Rectangle {
} }
text: qsTr("Reset") text: qsTr("Reset")
onTriggered: { onTriggered: {
let data = tableView.model.data(menu.modelIndex, CollectionModel.ResolvedValueRole) let data = tableView.model.data(menu.modelIndex, CollectionModel.ResolvedValueRole) ?? ""
var prop = DesignSystemBackend.dsInterface.createThemeProperty("", data, false) var prop = DesignSystemBackend.dsInterface.createThemeProperty("", data, false)
let result = tableView.model.setData(menu.modelIndex, prop, Qt.EditRole) let result = tableView.model.setData(menu.modelIndex, prop, Qt.EditRole)
} }
@@ -895,6 +1091,10 @@ Rectangle {
// Revoke active focus from text field by forcing active focus on another item // Revoke active focus from text field by forcing active focus on another item
tableView.forceActiveFocus() tableView.forceActiveFocus()
if (!result)
overlayInvalid.showHeaderData(overlay.section, overlay.orientation)
} }
Text { Text {
@@ -929,8 +1129,7 @@ Rectangle {
} }
function show(section, orientation) { function show(section, orientation) {
// Close all currently visible edit delegates tableView.closeEditor() // Close all currently visible edit delegates
tableView.closeEditor()
if (orientation === Qt.Horizontal) if (orientation === Qt.Horizontal)
overlay.parent = horizontalHeaderView.contentItem overlay.parent = horizontalHeaderView.contentItem
@@ -970,7 +1169,6 @@ Rectangle {
let insideViewport = item !== null let insideViewport = item !== null
//overlay.visible = insideViewport
if (insideViewport) { if (insideViewport) {
overlay.x = item.x overlay.x = item.x
overlay.y = item.y overlay.y = item.y

View File

@@ -11,7 +11,7 @@ Item {
property StudioTheme.ControlStyle style: StudioTheme.Values.controlStyle property StudioTheme.ControlStyle style: StudioTheme.Values.controlStyle
property alias icon: icon property alias icon: icon
property bool hover: mouseArea.containsMouse//false property bool hover: mouseArea.containsMouse
property bool pressed: false property bool pressed: false
property bool forceVisible: false property bool forceVisible: false
@@ -19,6 +19,7 @@ Item {
implicitHeight: control.style.actionIndicatorSize.height implicitHeight: control.style.actionIndicatorSize.height
signal clicked signal clicked
z: 10 z: 10
T.Label { T.Label {
@@ -56,7 +57,6 @@ Item {
id: mouseArea id: mouseArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
//onContainsMouseChanged: control.hover = mouseArea.containsMouse
onClicked: control.clicked() onClicked: control.clicked()
} }
} }

View File

@@ -3,7 +3,7 @@
import QtQuick import QtQuick
import QtQuick.Templates as T import QtQuick.Templates as T
import StudioTheme 1.0 as StudioTheme import StudioTheme as StudioTheme
T.ToolTip { T.ToolTip {
id: control id: control
@@ -38,4 +38,3 @@ T.ToolTip {
border.color: control.style.toolTip.border border.color: control.style.toolTip.border
} }
} }

View File

@@ -53,8 +53,8 @@ WidgetInfo DesignSystemView::widgetInfo()
return createWidgetInfo(m_designSystemWidget, return createWidgetInfo(m_designSystemWidget,
"DesignSystemView", "DesignSystemView",
WidgetInfo::RightPane, WidgetInfo::RightPane,
Tr::tr("Design System"), Tr::tr("Design Tokens"),
Tr::tr("Design System view"), Tr::tr("Design Tokens view"),
DesignerWidgetFlags::IgnoreErrors); DesignerWidgetFlags::IgnoreErrors);
} }

View File

@@ -62,7 +62,7 @@ DesignSystemWidget::DesignSystemWidget(DesignSystemView *view, DesignSystemInter
Theme::setupTheme(engine()); Theme::setupTheme(engine());
setWindowTitle(tr("Design System", "Title of Editor widget")); setWindowTitle(tr("Design Tokens", "Title of Editor widget"));
setMinimumSize(QSize(195, 195)); setMinimumSize(QSize(195, 195));
// init the first load of the QML UI elements // init the first load of the QML UI elements