diff --git a/src/qml/bitcoin_qml.qrc b/src/qml/bitcoin_qml.qrc
index ae36776d5a..1c98e6dc6d 100644
--- a/src/qml/bitcoin_qml.qrc
+++ b/src/qml/bitcoin_qml.qrc
@@ -26,6 +26,7 @@
controls/Header.qml
controls/Icon.qml
controls/InformationPage.qml
+ controls/IPAddressValueInput.qml
controls/NavButton.qml
controls/PageIndicator.qml
controls/NavigationBar.qml
diff --git a/src/qml/components/ProxySettings.qml b/src/qml/components/ProxySettings.qml
index 5165db7bce..0e6d7b99ce 100644
--- a/src/qml/components/ProxySettings.qml
+++ b/src/qml/components/ProxySettings.qml
@@ -47,11 +47,12 @@ ColumnLayout {
actionItem: ValueInput {
parentState: defaultProxy.state
description: "127.0.0.1:9050"
- onEditingFinished: {
- defaultProxy.forceActiveFocus()
- }
+ activeFocusOnTab: true
+ }
+ onClicked: {
+ loadedItem.filled = true
+ loadedItem.forceActiveFocus()
}
- onClicked: loadedItem.forceActiveFocus()
}
Separator { Layout.fillWidth: true }
Header {
@@ -93,11 +94,12 @@ ColumnLayout {
actionItem: ValueInput {
parentState: torProxy.state
description: "127.0.0.1:9050"
- onEditingFinished: {
- torProxy.forceActiveFocus()
- }
+ activeFocusOnTab: true
+ }
+ onClicked: {
+ loadedItem.filled = true
+ loadedItem.forceActiveFocus()
}
- onClicked: loadedItem.forceActiveFocus()
}
Separator { Layout.fillWidth: true }
}
diff --git a/src/qml/controls/IPAddressValueInput.qml b/src/qml/controls/IPAddressValueInput.qml
new file mode 100644
index 0000000000..102af1721b
--- /dev/null
+++ b/src/qml/controls/IPAddressValueInput.qml
@@ -0,0 +1,83 @@
+// Copyright (c) 2024 The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+
+TextInput {
+ id: root
+ required property string parentState
+ property string description: ""
+ property bool filled: false
+ property int descriptionSize: 18
+ property color textColor: root.filled ? Theme.color.neutral9 : Theme.color.neutral5
+ // Expose a property to indicate validity, initial value will be true (no error message displayed)
+ // if both properties errortText and description are set
+ property bool validInput: description!=="" && errorText!==""
+ enabled: true
+ state: root.parentState
+ validator: RegExpValidator { regExp: /[0-9.:]*/ } // Allow only digits, dots, and colons
+
+ maximumLength: 21
+
+ states: [
+ State {
+ name: "ACTIVE"
+ PropertyChanges { target: root; textColor: Theme.color.orange }
+ },
+ State {
+ name: "HOVER"
+ PropertyChanges {
+ target: root
+ textColor: root.filled ? Theme.color.orangeLight1 : Theme.color.neutral5
+ }
+ },
+ State {
+ name: "DISABLED"
+ PropertyChanges {
+ target: root
+ enabled: false
+ textColor: Theme.color.neutral4
+ }
+ }
+ ]
+
+ font.family: "Inter"
+ font.styleName: "Regular"
+ font.pixelSize: root.descriptionSize
+ color: root.textColor
+ text: root.description
+ horizontalAlignment: Text.AlignRight
+ wrapMode: Text.WordWrap
+
+ Behavior on color {
+ ColorAnimation { duration: 150 }
+ }
+
+ function isValidIPPort(input)
+ {
+ var parts = input.split(":");
+ if (parts.length !== 2) return false;
+ if (parts[1].length === 0) return false; // port part is empty
+ var ipAddress = parts[0];
+ var ipAddressParts = ipAddress.split(".");
+ if (ipAddressParts.length !== 4) return false;
+ for (var i = 0; (i < ipAddressParts.length); i++) {
+ if (ipAddressParts[i].length === 0) return false; // ip group number part is empty
+ if (parseInt(ipAddressParts[i]) > 255) return false;
+ }
+ var port = parseInt(parts[1]);
+ if (port < 1 || port > 65535) return false;
+ return true;
+ }
+
+ // Connections element to ensure validation on editing finished
+ Connections {
+ target: root
+ onEditingFinished: {
+ // Validate the input whenever editing is finished
+ validInput = isValidIPPort(root.text);
+ }
+ }
+}