Files
qt-creator/share/qtcreator/qmldesigner/connectionseditor/ExpressionBuilder.qml
Henning Gruendl 2a135f5d14 QmlDesigner: Fix long expression pill texts
* Elide static texts
* Clip dynamic texts

Change-Id: I90a511ec6dd35d2b65f4064bc09c0a83e8d1e905
Reviewed-by: Qt CI Patch Build Bot <ci_patchbuild_bot@qt.io>
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
2023-09-18 15:43:02 +00:00

497 lines
16 KiB
QML

// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import StudioControls as StudioControls
import StudioTheme as StudioTheme
Rectangle {
id: root
property StudioTheme.ControlStyle style: StudioTheme.Values.controlStyle
property var conditionListModel: ConnectionsEditorEditorBackend.connectionModel.delegate.conditionListModel
property alias model: repeater.model
property int shadowPillIndex: -1
property bool shadowPillVisible: root.shadowPillIndex !== -1
// Make expression editor at least 20 px high, especially when empty
property int baseHeight: Math.max(20, flow.childrenRect.height)
property int heightBeforeShadowPill: root.baseHeight
property int expressionHeight: {
// If expression is empty or the only item is a shadow pill
if (repeater.count === 0 || (repeater.count === 1 && root.shadowPillVisible))
return root.heightBeforeShadowPill
if (popup.visible)
return root.heightBeforeShadowPill + flow.spacing + 20
return root.heightBeforeShadowPill
}
signal remove(int index)
signal update(int index, var value)
signal add(var value)
signal insert(int index, var value, int type)
signal setValue(int index, var value)
signal setValueType(int index, var value, int type)
width: 400
height: root.expressionHeight + 2 * StudioTheme.Values.flowMargin
color: {
if (focusScope.activeFocus || popup.searchActive)
return root.style.background.interaction
if (mouseArea.containsMouse)
return root.style.background.hover
return root.style.background.idle
}
border {
color: {
if (!root.conditionListModel.valid)
return StudioTheme.Values.themeError
if (focusScope.activeFocus)
return root.style.border.interaction
if (mouseArea.containsMouse)
return root.style.border.hover
return root.style.border.idle
}
width: root.style.borderWidth
}
onVisibleChanged: {
if (!root.visible)
popup.close()
}
// Is text input for creating new items currently used.
function textInputActive() { // TODO Make property
return newTextInput.activeFocus && newTextInput.visible
}
function getMappedItemRect(index: int) : rect {
let item = repeater.itemAt(index)
let itemRect = Qt.rect(item.x, item.y, item.width, item.height)
return flow.mapToItem(root, itemRect)
}
function placeCursor(index: int) : void {
var textInputPosition = Qt.point(0, 0)
if (!repeater.count) { // Empty repeater
let mappedItemRect = flow.mapToItem(root, 0, 0, 0, 0)
textInputPosition = Qt.point(mappedItemRect.x, mappedItemRect.y)
index = 0
} else { // Repeater is not empty
// Clamp index to 0 and num items in repeater
index = Math.min(Math.max(index, 0), repeater.count)
if (index === 0) {
// Needs to be placed in front of first repeater item
let mappedItemRect = root.getMappedItemRect(index)
textInputPosition = Qt.point(mappedItemRect.x - 4, // - 4 due to spacing of flow
mappedItemRect.y)
} else {
let mappedItemRect = root.getMappedItemRect(index - 1)
textInputPosition = Qt.point(mappedItemRect.x + mappedItemRect.width + 3,
mappedItemRect.y)
}
}
// Position text input, make it visible and set focus
newTextInput.x = textInputPosition.x
newTextInput.y = textInputPosition.y
newTextInput.index = index
newTextInput.visible = true
newTextInput.forceActiveFocus()
if (!root.shadowPillVisible)
popup.showOperators = root.conditionListModel.operatorAllowed(index)
// Open suggestion popup
popup.open()
}
ListModel {
id: __operatorModel
function convertValueToName(value) {
for (var i = 0; i < __operatorModel.count; ++i) {
let element = __operatorModel.get(i)
if (element.value === value )
return element.name
}
return value
}
ListElement {
name: "AND"
value: "&&"
tooltip: QT_TR_NOOP("This is AND (&&)")
}
ListElement {
name: "OR"
value: "||"
tooltip: QT_TR_NOOP("This is OR (||)")
}
ListElement {
name: "EQUAL"
value: "==="
tooltip: QT_TR_NOOP("This is EQUAL (===)")
}
ListElement {
name: "NOT EQUAL"
value: "!=="
tooltip: QT_TR_NOOP("This is NOT EQUAL (!==)")
}
ListElement {
name: "GREATER"
value: ">"
tooltip: QT_TR_NOOP("This is GREATER (>)")
}
ListElement {
name: "LESS"
value: "<"
tooltip: QT_TR_NOOP("This is LESS (<)")
}
ListElement {
name: "GREATER OR EQUAL"
value: ">="
tooltip: QT_TR_NOOP("This is GREATER OR EQUAL (>=)")
}
ListElement {
name: "LESS OR EQUAL"
value: "<="
tooltip: QT_TR_NOOP("This is LESS OR EQUAL (<=)")
}
}
StudioControls.ToolTip {
id: toolTip
visible: mouseArea.containsMouse && toolTip.text !== ""
delay: 1000
text: root.conditionListModel.error
}
MouseArea {
id: mouseArea
anchors.fill: parent
cursorShape: Qt.IBeamCursor
hoverEnabled: true
onPressed: function (event) {
// Check if empty
if (!repeater.count) {
root.placeCursor(0)
return
}
// Map to flow item
let point = mouseArea.mapToItem(flow, Qt.point(event.x, event.y))
let horizontalDistance = Number.MAX_VALUE
let verticalDistance = Number.MAX_VALUE
let cursorPosition = 0
for (var i = 0; i < repeater.count; ++i) {
let item = repeater.itemAt(i)
let y = item.y + (item.height / 2)
// Vertical distance
let vDistance = Math.abs(point.y - y)
// Horizontal distance
let hLeftDistance = Math.abs(point.x - item.x)
let hRightDistance = Math.abs(point.x - (item.x + item.width))
// Early return if vertical distance increases
if (vDistance > verticalDistance)
break
if (vDistance <= verticalDistance) {
// Rest horizontal distance if vertical distance is smaller than before
if (vDistance !== verticalDistance)
horizontalDistance = Number.MAX_VALUE
if (hLeftDistance < horizontalDistance) {
horizontalDistance = hLeftDistance
cursorPosition = i
}
if (hRightDistance < horizontalDistance) {
horizontalDistance = hRightDistance
cursorPosition = i + 1
}
verticalDistance = vDistance
}
}
root.placeCursor(cursorPosition)
}
}
Item {
anchors.fill: parent
anchors.margins: StudioTheme.Values.flowMargin
Text {
id: placeholder
height: 20
topPadding: 1
font.pixelSize: root.style.baseFontSize
color: (focusScope.activeFocus || popup.searchActive)
? root.style.text.placeholderInteraction
: root.style.text.placeholder
visible: !repeater.count
text: qsTr("Condition")
}
}
FocusScope {
id: focusScope
anchors.fill: parent
onActiveFocusChanged: {
if (!focusScope.activeFocus && !popup.searchActive)
popup.close()
}
Flow {
id: flow
property int focusIndex: -1
anchors.fill: parent
anchors.margins: StudioTheme.Values.flowMargin
spacing: StudioTheme.Values.flowSpacing
onPositioningComplete: {
if (root.textInputActive())
root.placeCursor(newTextInput.index)
if (!root.shadowPillVisible)
root.heightBeforeShadowPill = root.baseHeight
}
Repeater {
id: repeater
onItemRemoved: function(index, item) {
if (!root.textInputActive())
return
// Udpate the cursor position
if (index < newTextInput.index)
newTextInput.index = newTextInput.index - 1
}
onItemAdded: function(index, item) {
if (!root.textInputActive())
return
if (index >= newTextInput.index)
newTextInput.index = newTextInput.index + 1
}
Pill {
id: pill
operatorModel: __operatorModel
maxTextWidth: root.width - 2 * StudioTheme.Values.flowMargin
onRemove: function() {
// If pill has focus due to selection or keyboard navigation
if (pill.focus)
root.placeCursor(pill.index)
Qt.callLater(root.remove, pill.index)
}
onUpdate: function(value) {
if (value === "")
Qt.callLater(root.remove, pill.index) // Otherwise crash
else
Qt.callLater(root.update, pill.index, value)
}
onFocusChanged: function() {
if (pill.focus)
flow.focusIndex = pill.index
}
onSubmit: function (cursorPosition) {
let index = pill.index
// If cursor position is 0 the user moved the cursor out to left side,
// so place the cursor before the pill
if (cursorPosition !== 0)
index++
root.placeCursor(index)
}
}
}
}
TextInput {
id: newTextInput
property int index
height: 20
topPadding: 1
font.pixelSize: root.style.baseFontSize
color: root.style.text.idle
visible: false
validator: RegularExpressionValidator { regularExpression: /^\S.+/ }
onTextEdited: {
if (newTextInput.text === "")
return
newTextInput.visible = false
root.insert(newTextInput.index, newTextInput.text, ConditionListModel.Intermediate)
newTextInput.clear()
// Set focus on the newly created item
let newItem = repeater.itemAt(newTextInput.index)
newItem.forceActiveFocus()
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Backspace) {
if (root.textInputActive()) {
let previousIndex = newTextInput.index - 1
if (previousIndex < 0)
return
let item = repeater.itemAt(previousIndex)
item.setCursorEnd()
item.forceActiveFocus()
popup.close()
}
}
}
}
}
SuggestionPopup {
id: popup
style: StudioTheme.Values.connectionPopupControlStyle
x: 0
y: root.height
width: root.width
operatorModel: __operatorModel
//onOpened: console.log("POPUP opened")
//onClosed: console.log("POPUP closed")
onAboutToHide: {
newTextInput.visible = false
}
onSelect: function(value) {
newTextInput.visible = true
newTextInput.forceActiveFocus()
if (root.shadowPillVisible) { // Active shadow pill
root.remove(root.shadowPillIndex)
root.shadowPillIndex = -1
}
root.insert(newTextInput.index, value, ConditionListModel.Variable)
// Clear search, reset stack view and tree model
popup.reset()
}
onSearchActiveChanged: {
if (popup.searchActive) {
root.heightBeforeShadowPill = root.baseHeight
root.insert(newTextInput.index, "...", ConditionListModel.Shadow)
root.shadowPillIndex = newTextInput.index
} else {
if (!root.shadowPillVisible)
return
root.remove(root.shadowPillIndex)
root.shadowPillIndex = -1
}
}
onEntered: function(value) {
if (!popup.searchActive) {
if (!root.shadowPillVisible) {
root.heightBeforeShadowPill = root.baseHeight
root.shadowPillIndex = newTextInput.index
root.insert(newTextInput.index, value, ConditionListModel.Shadow)
} else {
root.setValue(root.shadowPillIndex, value)
}
} else {
root.setValue(root.shadowPillIndex, value)
}
}
onExited: function(value) {
let shadowItem = repeater.itemAt(root.shadowPillIndex)
if (!popup.searchActive) {
if (root.shadowPillVisible && shadowItem?.value === value) {
root.remove(root.shadowPillIndex)
root.shadowPillIndex = -1
}
} else {
// Reset to 3 dots if still the same value as the exited item
if (shadowItem?.value === value)
root.setValue(root.shadowPillIndex, "...")
}
}
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Left) {
if (root.textInputActive()) {
let previousIndex = newTextInput.index - 1
if (previousIndex < 0)
return
let item = repeater.itemAt(previousIndex)
item.setCursorEnd()
item.forceActiveFocus()
popup.close()
} else {
if (flow.focusIndex < 0)
return
root.placeCursor(flow.focusIndex)
}
} else if (event.key === Qt.Key_Right) {
if (root.textInputActive()) {
let nextIndex = newTextInput.index
if (nextIndex >= repeater.count)
return
let item = repeater.itemAt(nextIndex)
item.setCursorBegin()
item.forceActiveFocus()
popup.close()
} else {
root.placeCursor(flow.focusIndex + 1)
}
}
}
}