From b53cdcff08e63de9d2bdcb3f55fbe6892bf89703 Mon Sep 17 00:00:00 2001 From: NickVs2015 Date: Wed, 12 Nov 2025 10:57:53 +0300 Subject: [PATCH] fix: fix self-hosted TextFields and Keyboard reset issue (#1983) Co-authored-by: vkamn --- client/resources.qrc | 1 + client/ui/controllers/settingsController.cpp | 6 +- client/ui/controllers/settingsController.h | 2 + client/ui/qml/Components/SmartScroll.qml | 55 ++++++++++++ .../qml/Controls2/TextFieldWithHeaderType.qml | 17 +++- .../Pages2/PageProtocolAwgClientSettings.qml | 84 ++++++++++++++++++ .../ui/qml/Pages2/PageProtocolAwgSettings.qml | 71 +++++++++++++++ .../Pages2/PageSettingsAppSplitTunneling.qml | 76 ++++++++-------- .../qml/Pages2/PageSettingsSplitTunneling.qml | 88 ++++++++++--------- client/ui/qml/main2.qml | 2 +- 10 files changed, 321 insertions(+), 81 deletions(-) create mode 100644 client/ui/qml/Components/SmartScroll.qml diff --git a/client/resources.qrc b/client/resources.qrc index 1472f66d..03ac6edd 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -247,6 +247,7 @@ ui/qml/Components/OtpCodeDrawer.qml ui/qml/Components/AwgTextField.qml ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml + ui/qml/Components/SmartScroll.qml images/flagKit/ZW.svg diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index 7eef384c..b32408c0 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -446,7 +446,11 @@ bool SettingsController::isOnTv() bool SettingsController::isEdgeToEdgeEnabled() { #ifdef Q_OS_ANDROID - return AndroidController::instance()->isEdgeToEdgeEnabled(); + if (!m_edgeToEdgeCached) { + m_cachedEdgeToEdgeEnabled = AndroidController::instance()->isEdgeToEdgeEnabled(); + m_edgeToEdgeCached = true; + } + return m_cachedEdgeToEdgeEnabled; #else return false; #endif diff --git a/client/ui/controllers/settingsController.h b/client/ui/controllers/settingsController.h index cc90de56..6ee5c631 100644 --- a/client/ui/controllers/settingsController.h +++ b/client/ui/controllers/settingsController.h @@ -150,6 +150,8 @@ private: mutable int m_cachedStatusBarHeight = -1; mutable int m_cachedNavigationBarHeight = -1; + mutable bool m_cachedEdgeToEdgeEnabled = false; + mutable bool m_edgeToEdgeCached = false; int m_imeHeight = 0; std::shared_ptr m_settings; diff --git a/client/ui/qml/Components/SmartScroll.qml b/client/ui/qml/Components/SmartScroll.qml new file mode 100644 index 00000000..374f7f4a --- /dev/null +++ b/client/ui/qml/Components/SmartScroll.qml @@ -0,0 +1,55 @@ +import QtQuick +import QtQuick.Controls + +QtObject { + id: root + + property var listView: null + property var scrollToItemTarget: null + + property Connections imeConnection: Connections { + target: SettingsController + function onImeHeightChanged() { + if (root.scrollToItemTarget && SettingsController.imeHeight > 0) { + scrollTimer.restart() + } + } + } + + property Timer scrollTimer: Timer { + interval: 100 + repeat: false + onTriggered: { + if (root.scrollToItemTarget && root.listView) { + if (SettingsController.imeHeight > 0) { + var item = root.scrollToItemTarget + var itemY = item.mapToItem(root.listView.contentItem, 0, 0).y + var itemHeight = item.height + var keyboardHeight = SettingsController.imeHeight + SettingsController.safeAreaBottomMargin + var visibleHeight = root.listView.height - keyboardHeight + + var desiredTopOffset = visibleHeight * 0.25 + var targetContentY = itemY - desiredTopOffset + + if (targetContentY < 0) { + targetContentY = 0 + } + + var maxContentY = root.listView.contentHeight - root.listView.height + if (targetContentY > maxContentY) { + targetContentY = maxContentY + } + + root.listView.contentY = targetContentY + root.scrollToItemTarget = null + } + } + } + } + + function scrollToItem(item) { + scrollToItemTarget = item + scrollTimer.restart() + } +} + diff --git a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml index fdb7a3ae..f8c74a59 100644 --- a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml +++ b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml @@ -19,6 +19,9 @@ Item { property string buttonText property string buttonImageSource + property string buttonImageColor: AmneziaStyle.color.midnightBlack + property string buttonBackgroundColor: AmneziaStyle.color.paleGray + property string buttonHoveredColor: AmneziaStyle.color.lightGray property var clickedFunc property alias textField: textField @@ -48,7 +51,7 @@ Item { Keys.onUpPressed: { FocusController.nextKeyUpItem() } - + Keys.onDownPressed: { FocusController.nextKeyDownItem() } @@ -67,7 +70,7 @@ Item { border.width: 1 Behavior on border.color { - PropertyAnimation { duration: 200 } + PropertyAnimation { duration: 100 } } RowLayout { @@ -194,6 +197,14 @@ Item { focusPolicy: Qt.NoFocus text: root.buttonText leftImageSource: root.buttonImageSource + leftImageColor: root.buttonImageColor + + defaultColor: root.buttonBackgroundColor + hoveredColor: root.buttonHoveredColor + pressedColor: root.buttonHoveredColor + disabledColor: AmneziaStyle.color.transparent + + borderWidth: 0 anchors.top: content.top anchors.bottom: content.bottom @@ -201,7 +212,7 @@ Item { height: content.implicitHeight width: content.implicitHeight - squareLeftSide: true + squareLeftSide: false clickedFunc: function() { if (root.clickedFunc && typeof root.clickedFunc === "function") { diff --git a/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml b/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml index 47b4fb58..d3f5be7f 100644 --- a/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolAwgClientSettings.qml @@ -31,6 +31,11 @@ PageType { } } + SmartScroll { + id: smartScroll + listView: listView + } + ListViewType { id: listView @@ -80,6 +85,13 @@ PageType { clientMtu = textField.text } } + + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(mtuTextField) + } + } + checkEmptyText: true } @@ -97,6 +109,12 @@ PageType { clientJunkPacketCount = textField.text } } + + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(junkPacketCountTextField) + } + } } AwgTextField { @@ -113,6 +131,12 @@ PageType { clientJunkPacketMinSize = textField.text } } + + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(junkPacketMinSizeTextField) + } + } } AwgTextField { @@ -129,6 +153,12 @@ PageType { clientJunkPacketMaxSize = textField.text } } + + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(junkPacketMaxSizeTextField) + } + } } AwgTextField { @@ -147,6 +177,12 @@ PageType { clientSpecialJunk1 = textField.text } } + + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(specialJunk1TextField) + } + } } AwgTextField { @@ -165,6 +201,12 @@ PageType { clientSpecialJunk2 = textField.text } } + + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(specialJunk2TextField) + } + } } AwgTextField { @@ -183,6 +225,12 @@ PageType { clientSpecialJunk3 = textField.text } } + + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(specialJunk3TextField) + } + } } AwgTextField { @@ -201,6 +249,12 @@ PageType { clientSpecialJunk4 = textField.text } } + + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(specialJunk4TextField) + } + } } AwgTextField { @@ -219,6 +273,12 @@ PageType { clientSpecialJunk5 = textField.text } } + + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(specialJunk5TextField) + } + } } AwgTextField { @@ -237,6 +297,12 @@ PageType { clientControlledJunk1 = textField.text } } + + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(controlledJunk1TextField) + } + } } AwgTextField { @@ -255,6 +321,12 @@ PageType { clientControlledJunk2 = textField.text } } + + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(controlledJunk2TextField) + } + } } AwgTextField { @@ -273,6 +345,12 @@ PageType { clientControlledJunk3 = textField.text } } + + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(controlledJunk3TextField) + } + } } AwgTextField { @@ -290,6 +368,12 @@ PageType { clientSpecialHandshakeTimeout = textField.text } } + + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(iTimeTextField) + } + } } Header2TextType { diff --git a/client/ui/qml/Pages2/PageProtocolAwgSettings.qml b/client/ui/qml/Pages2/PageProtocolAwgSettings.qml index 1727b4fe..ff179446 100644 --- a/client/ui/qml/Pages2/PageProtocolAwgSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolAwgSettings.qml @@ -34,6 +34,11 @@ PageType { } } + SmartScroll { + id: smartScroll + listView: listView + } + ListViewType { id: listView @@ -81,6 +86,12 @@ PageType { } } + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(vpnAddressSubnetTextField) + } + } + checkEmptyText: true } @@ -104,6 +115,12 @@ PageType { } } + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(portTextField) + } + } + checkEmptyText: true } @@ -121,6 +138,12 @@ PageType { serverJunkPacketCount = textField.text } } + + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(junkPacketCountTextField) + } + } } AwgTextField { @@ -137,6 +160,12 @@ PageType { serverJunkPacketMinSize = textField.text } } + + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(junkPacketMinSizeTextField) + } + } } AwgTextField { @@ -153,6 +182,12 @@ PageType { serverJunkPacketMaxSize = textField.text } } + + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(junkPacketMaxSizeTextField) + } + } } AwgTextField { @@ -169,6 +204,12 @@ PageType { serverInitPacketJunkSize = textField.text } } + + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(initPacketJunkSizeTextField) + } + } } AwgTextField { @@ -185,6 +226,12 @@ PageType { serverResponsePacketJunkSize = textField.text } } + + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(responsePacketJunkSizeTextField) + } + } } // AwgTextField { @@ -233,6 +280,12 @@ PageType { serverInitPacketMagicHeader = textField.text } } + + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(initPacketMagicHeaderTextField) + } + } } AwgTextField { @@ -249,6 +302,12 @@ PageType { serverResponsePacketMagicHeader = textField.text } } + + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(responsePacketMagicHeaderTextField) + } + } } AwgTextField { @@ -265,6 +324,12 @@ PageType { serverUnderloadPacketMagicHeader = textField.text } } + + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(underloadPacketMagicHeaderTextField) + } + } } AwgTextField { @@ -281,6 +346,12 @@ PageType { serverTransportPacketMagicHeader = textField.text } } + + textField.onActiveFocusChanged: { + if (textField.activeFocus) { + smartScroll.scrollToItem(transportPacketMagicHeaderTextField) + } + } } BasicButtonType { diff --git a/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml b/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml index 34a6e175..4445b08b 100644 --- a/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml +++ b/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml @@ -165,9 +165,11 @@ PageType { ScrollBar.vertical: ScrollBarType { policy: ScrollBar.AlwaysOn } anchors.top: header.bottom - anchors.bottom: addAppButton.top + anchors.bottom: parent.bottom + anchors.bottomMargin: addAppButton.implicitHeight + 48 + SettingsController.safeAreaBottomMargin + (searchField.textField.activeFocus ? 0 : SettingsController.imeHeight) anchors.left: parent.left anchors.right: parent.right + clip: true model: SortFilterProxyModel { id: proxyAppSplitTunnelingModel @@ -215,50 +217,54 @@ PageType { } Rectangle { - anchors.fill: addAppButton - anchors.bottomMargin: -24 - SettingsController.safeAreaBottomMargin - color: AmneziaStyle.color.midnightBlack - opacity: 0.8 - } - - RowLayout { - id: addAppButton - - enabled: root.pageEnabled - - anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: 24 - anchors.rightMargin: 16 - anchors.leftMargin: 16 - anchors.bottomMargin: 24 + SettingsController.safeAreaBottomMargin + anchors.bottom: parent.bottom + + height: addAppButton.implicitHeight + 48 + SettingsController.safeAreaBottomMargin + + color: AmneziaStyle.color.midnightBlack + opacity: 0.8 + + RowLayout { + id: addAppButton - TextFieldWithHeaderType { - id: searchField + enabled: root.pageEnabled - Layout.fillWidth: true + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 24 + anchors.rightMargin: 16 + anchors.leftMargin: 16 + anchors.bottomMargin: 24 + SettingsController.safeAreaBottomMargin - textField.placeholderText: qsTr("application name") - buttonImageSource: "qrc:/images/controls/plus.svg" + TextFieldWithHeaderType { + id: searchField - rightButtonClickedOnEnter: true + Layout.fillWidth: true - clickedFunc: function() { - searchField.focus = false - PageController.showBusyIndicator(true) + textField.placeholderText: qsTr("application name") + buttonImageSource: "qrc:/images/controls/plus.svg" - if (Qt.platform.os === "windows") { - var fileName = SystemController.getFileName(qsTr("Open executable file"), - qsTr("Executable files (*.*)")) - if (fileName !== "") { - AppSplitTunnelingController.addApp(fileName) + rightButtonClickedOnEnter: true + + clickedFunc: function() { + searchField.focus = false + PageController.showBusyIndicator(true) + + if (Qt.platform.os === "windows") { + var fileName = SystemController.getFileName(qsTr("Open executable file"), + qsTr("Executable files (*.*)")) + if (fileName !== "") { + AppSplitTunnelingController.addApp(fileName) + } + } else if (Qt.platform.os === "android"){ + installedAppDrawer.openTriggered() } - } else if (Qt.platform.os === "android"){ - installedAppDrawer.openTriggered() - } - PageController.showBusyIndicator(false) + PageController.showBusyIndicator(false) + } } } } diff --git a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml index 3f6b8a71..27aa5dea 100644 --- a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml +++ b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml @@ -168,11 +168,13 @@ PageType { anchors.top: header.bottom anchors.topMargin: 16 - anchors.bottom: addSiteButton.top + anchors.bottom: parent.bottom + anchors.bottomMargin: addSiteButton.implicitHeight + 48 + (searchField.textField.activeFocus ? 0 : SettingsController.imeHeight) width: parent.width enabled: root.pageEnabled + clip: true model: SortFilterProxyModel { id: proxySitesModel @@ -231,56 +233,60 @@ PageType { } Rectangle { - anchors.fill: addSiteButton - anchors.bottomMargin: -24 - color: AmneziaStyle.color.midnightBlack - opacity: 0.8 - } - - RowLayout { - id: addSiteButton - - enabled: root.pageEnabled - - anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: 24 - anchors.rightMargin: 16 - anchors.leftMargin: 16 - anchors.bottomMargin: 24 + anchors.bottom: parent.bottom + + height: addSiteButton.implicitHeight + 48 + + color: AmneziaStyle.color.midnightBlack + opacity: 0.8 + + RowLayout { + id: addSiteButton - TextFieldWithHeaderType { - id: searchField + enabled: root.pageEnabled - Layout.fillWidth: true - rightButtonClickedOnEnter: true + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 24 + anchors.rightMargin: 16 + anchors.leftMargin: 16 + anchors.bottomMargin: 24 - textField.placeholderText: qsTr("website or IP") - buttonImageSource: "qrc:/images/controls/plus.svg" + TextFieldWithHeaderType { + id: searchField - clickedFunc: function() { - PageController.showBusyIndicator(true) - SitesController.addSite(textField.text) - textField.text = "" - PageController.showBusyIndicator(false) - } - } + Layout.fillWidth: true + rightButtonClickedOnEnter: true - ImageButtonType { - id: addSiteButtonImage - implicitWidth: 56 - implicitHeight: 56 + textField.placeholderText: qsTr("website or IP") + buttonImageSource: "qrc:/images/controls/plus.svg" - image: "qrc:/images/controls/more-vertical.svg" - imageColor: AmneziaStyle.color.paleGray - - onClicked: function () { - moreActionsDrawer.openTriggered() + clickedFunc: function() { + PageController.showBusyIndicator(true) + SitesController.addSite(textField.text) + textField.text = "" + PageController.showBusyIndicator(false) + } } - Keys.onReturnPressed: addSiteButtonImage.clicked() - Keys.onEnterPressed: addSiteButtonImage.clicked() + ImageButtonType { + id: addSiteButtonImage + implicitWidth: 56 + implicitHeight: 56 + + image: "qrc:/images/controls/more-vertical.svg" + imageColor: AmneziaStyle.color.paleGray + + onClicked: function () { + moreActionsDrawer.openTriggered() + } + + Keys.onReturnPressed: addSiteButtonImage.clicked() + Keys.onEnterPressed: addSiteButtonImage.clicked() + } } } diff --git a/client/ui/qml/main2.qml b/client/ui/qml/main2.qml index 020f76c1..de2c24b9 100644 --- a/client/ui/qml/main2.qml +++ b/client/ui/qml/main2.qml @@ -183,7 +183,7 @@ Window { id: privateKeyPassphraseDrawer anchors.fill: parent - expandedHeight: root.height * 0.35 + SettingsController.safeAreaBottomMargin + expandedHeight: root.height * 0.35 + SettingsController.safeAreaBottomMargin + SettingsController.imeHeight expandedStateContent: ColumnLayout { anchors.top: parent.top