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 bb4ee6d6d8..ca479edc78 100644 --- a/src/qml/components/ProxySettings.qml +++ b/src/qml/components/ProxySettings.qml @@ -43,15 +43,25 @@ ColumnLayout { Layout.fillWidth: true header: qsTr("IP and Port") errorText: qsTr("Invalid IP address or port format. Please use the format '255.255.255.255:65535'.") + showErrorText: !loadedItem.isValidIPPort(loadedItem.description) state: !defaultProxyEnable.loadedItem.checked ? "DISABLED" : "ACTIVE" - actionItem: ValueInput { + actionItem: IPAddressValueInput { parentState: defaultProxy.state description: "127.0.0.1:9050" + activeFocusOnTab: true onEditingFinished: { - defaultProxy.forceActiveFocus() + if (isValidIPPort(text)) { + defaultProxy.forceActiveFocus() + defaultProxy.showErrorText = false + } else { + defaultProxy.showErrorText = true + } } } - onClicked: loadedItem.forceActiveFocus() + onClicked: { + loadedItem.filled = true + loadedItem.forceActiveFocus() + } } Separator { Layout.fillWidth: true } Header { @@ -90,15 +100,24 @@ ColumnLayout { Layout.fillWidth: true header: qsTr("IP and Port") errorText: qsTr("Invalid IP address or port format. Please use the format '255.255.255.255:65535'.") + showErrorText: !loadedItem.isValidIPPort(loadedItem.description) state: !torProxyEnable.loadedItem.checked ? "DISABLED" : "ACTIVE" - actionItem: ValueInput { + actionItem: IPAddressValueInput { parentState: torProxy.state description: "127.0.0.1:9050" onEditingFinished: { - torProxy.forceActiveFocus() + if (isValidIPPort(text)) { + torProxy.forceActiveFocus() + torProxy.showErrorText = false + } else { + torProxy.showErrorText = true + } } } - onClicked: loadedItem.forceActiveFocus() + onClicked: { + loadedItem.filled = true + 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..d8edef299d --- /dev/null +++ b/src/qml/controls/IPAddressValueInput.qml @@ -0,0 +1,72 @@ +// 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 + enabled: true + state: root.parentState + validator: RegExpValidator { regExp: /[0-9.:]*/ } // Allow only digits, dots, and colons + selectByMouse: true // Enable text selection with the mouse + + 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; + } +}