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 96cf3676bf..da069b78c6 100644 --- a/src/qml/components/ProxySettings.qml +++ b/src/qml/components/ProxySettings.qml @@ -20,9 +20,18 @@ ColumnLayout { } Separator { Layout.fillWidth: true } Setting { + id: defaultProxyEnable Layout.fillWidth: true header: qsTr("Enable") - actionItem: OptionSwitch {} + actionItem: OptionSwitch { + onCheckedChanged: { + if (checked == false) { + defaultProxy.state = "DISABLED" + } else { + defaultProxy.state = "FILLED" + } + } + } onClicked: { loadedItem.toggle() loadedItem.toggled() @@ -33,14 +42,18 @@ ColumnLayout { id: defaultProxy Layout.fillWidth: true header: qsTr("IP and Port") - actionItem: ValueInput { + errorText: qsTr("Invalid IP address or port format. Please use the format '255.255.255.255:65535'.") + state: !defaultProxyEnable.loadedItem.checked ? "DISABLED" : "FILLED" + showErrorText: !defaultProxy.loadedItem.validInput && defaultProxyEnable.loadedItem.checked + actionItem: IPAddressValueInput { 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 { @@ -55,10 +68,18 @@ ColumnLayout { } Separator { Layout.fillWidth: true } Setting { + id: torProxyEnable Layout.fillWidth: true header: qsTr("Enable") - actionItem: OptionSwitch {} - description: qsTr("When disabled, Tor connections will use the default proxy (if enabled).") + actionItem: OptionSwitch { + onCheckedChanged: { + if (checked == false) { + torProxy.state = "DISABLED" + } else { + torProxy.state = "FILLED" + } + } + } onClicked: { loadedItem.toggle() loadedItem.toggled() @@ -69,14 +90,18 @@ ColumnLayout { id: torProxy Layout.fillWidth: true header: qsTr("IP and Port") - actionItem: ValueInput { + errorText: qsTr("Invalid IP address or port format. Please use the format '255.255.255.255:65535'.") + state: !torProxyEnable.loadedItem.checked ? "DISABLED" : "FILLED" + showErrorText: !torProxy.loadedItem.validInput && torProxyEnable.loadedItem.checked + actionItem: IPAddressValueInput { 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..d2ce4c7f16 --- /dev/null +++ b/src/qml/controls/IPAddressValueInput.qml @@ -0,0 +1,82 @@ +// 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) + property bool validInput: true + 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 + function onTextChanged() { + // Validate the input whenever editing is finished + validInput = isValidIPPort(root.text); + } + } +}