From 6bd2bf9f3389f7371c185a69cfe11edf332a1d91 Mon Sep 17 00:00:00 2001 From: Mike Trahearn Date: Mon, 23 Dec 2024 14:40:28 +1000 Subject: [PATCH] Make SpinBox editable Upgrade the contentItem to use TextInput with correct VKB interaction. Fixes #1104 --- components/controls/SpinBox.qml | 115 +++++++++++++++++--- components/dialogs/NumberSelectorDialog.qml | 12 +- 2 files changed, 113 insertions(+), 14 deletions(-) diff --git a/components/controls/SpinBox.qml b/components/controls/SpinBox.qml index 6075e54b2..33751f24f 100644 --- a/components/controls/SpinBox.qml +++ b/components/controls/SpinBox.qml @@ -7,6 +7,7 @@ import QtQuick import QtQuick.Controls as C import QtQuick.Templates as CT import QtQuick.Controls.impl as CP +import QtQuick.Layouts import Victron.VenusOS // SpinBox uses a binding to increase 'stepSize' when the user holds a button down for a while. This allows the spin box to quickly change arbitrarily large values. @@ -17,11 +18,12 @@ CT.SpinBox { id: root property alias label: primaryLabel - property string secondaryText + property alias secondaryText: secondaryLabel.text property int indicatorImplicitWidth: Theme.geometry_spinBox_indicator_minimumWidth property int orientation: Qt.Horizontal property int _scalingFactor: 1 property int _originalStepSize + property alias suffix: suffixLabel.text signal maxValueReached() signal minValueReached() @@ -45,29 +47,104 @@ CT.SpinBox { } contentItem: Item { + + // needed for QQuickSpinBoxPrivate to read the "text" property of the contentItem + // so that it can call the valueFromText() function + readonly property alias text: primaryLabel.text + Column { id: valueColumn - width: Math.max(primaryLabel.implicitWidth, secondaryLabel.implicitWidth) + width: Math.max(primaryTextInput.implicitWidth, secondaryLabel.implicitWidth) anchors.centerIn: parent - Label { - id: primaryLabel - - width: parent.width - text: root.textFromValue(root.value, root.locale) - color: root.enabled ? Theme.color_font_primary : Theme.color_background_disabled - font.pixelSize: root.secondaryText.length > 0 ? Theme.font_size_h2 : Theme.font_size_h3 - horizontalAlignment: Qt.AlignHCenter - verticalAlignment: Qt.AlignVCenter + Item { + id: primaryTextInput + + width: primaryRowLayout.implicitWidth + Theme.geometry_textField_horizontalMargin * 2 + height: primaryRowLayout.height + anchors.horizontalCenter: parent.horizontalCenter + + MouseArea { + anchors.fill: parent + enabled: root.editable + onClicked: primaryLabel.forceActiveFocus() + } + + Rectangle { + anchors.fill: parent + visible: root.editable + color: "transparent" + border.color: Theme.color_blue + border.width: Theme.geometry_button_border_width + radius: Theme.geometry_button_radius + } + + RowLayout { + id: primaryRowLayout + + anchors.centerIn: parent + + TextInput { + id: primaryLabel + + color: root.enabled ? Theme.color_font_primary : Theme.color_background_disabled + font.family: Global.fontFamily + font.pixelSize: root.secondaryText.length > 0 ? Theme.font_size_h2 : Theme.font_size_h3 + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + selectedTextColor: Theme.color_white + selectionColor : Theme.color_blue + readOnly: !root.editable + selectByMouse: !readOnly + validator: root.validator + inputMethodHints: root.inputMethodHints + + onAccepted: { + // Note that the text may at this time represent a value out of SpinBox + // to/from range, so clamp it here. + let v = root.valueFromText(text, root.locale) + if (v < root.from) { + v = root.from + } else if (v > root.to) { + v = root.to + } + + // Force-update the displayed text, to guarantee the text is in sync + // with the numeric value, even if the value has not changed due to the + // user entering an out-of-range value on consecutive attempts. + text = root.textFromValue(v, root.locale) + root.value = v + primaryLabel.focus = false + } + + Connections { + target: root + function onValueChanged() { + // Update the displayed text when the initial value is set or when + // the up/down buttons are pressed. + primaryLabel.text = root.textFromValue(root.value, root.locale) + } + } + } + + Label { + id: suffixLabel + + visible: text.length + color: primaryLabel.color + font: primaryLabel.font + horizontalAlignment: primaryLabel.horizontalAlignment + verticalAlignment: primaryLabel.verticalAlignment + } + + } } Label { id: secondaryLabel - width: primaryLabel.width height: text.length ? implicitHeight : 0 - text: root.secondaryText color: Theme.color_font_secondary font.pixelSize: Theme.font_size_caption horizontalAlignment: Qt.AlignHCenter @@ -116,6 +193,18 @@ CT.SpinBox { } } + textFromValue: function(value, locale) { + return Units.formatNumber(value) + } + valueFromText: function(text, locale) { + let value = Units.formattedNumberToReal(text) + if (isNaN(value)) { + // don't change the current value + value = root.value + } + return value + } + Timer { id: pressTimer diff --git a/components/dialogs/NumberSelectorDialog.qml b/components/dialogs/NumberSelectorDialog.qml index 9df6d7a64..da7078dbf 100644 --- a/components/dialogs/NumberSelectorDialog.qml +++ b/components/dialogs/NumberSelectorDialog.qml @@ -56,15 +56,25 @@ ModalDialog { anchors.horizontalCenter: parent.horizontalCenter width: parent.width - 2*Theme.geometry_modalDialog_content_horizontalMargin height: Theme.geometry_timeSelector_spinBox_height + editable: true indicatorImplicitWidth: root.decimals > 0 ? Theme.geometry_spinBox_indicator_minimumWidth : Theme.geometry_spinBox_indicator_maximumWidth textFromValue: function(value, locale) { - return Units.formatNumber(value / root._multiplier(), root.decimals) + root.suffix + return Units.formatNumber(value / root._multiplier(), root.decimals) + } + valueFromText: function(text, locale) { + let value = Units.formattedNumberToReal(text) * root._multiplier() + if (isNaN(value)) { + // don't change the current value + value = spinBox.value + } + return value } from: Math.max(Global.int32Min, root.from * root._multiplier()) to: Math.min(Global.int32Max, root.to * root._multiplier()) stepSize: root.stepSize * root._multiplier() + suffix: root.suffix onValueChanged: { if (_initialized) {