diff --git a/client/ui/controllers/pageController.h b/client/ui/controllers/pageController.h index 0b456a0d..b286b1b1 100644 --- a/client/ui/controllers/pageController.h +++ b/client/ui/controllers/pageController.h @@ -123,6 +123,9 @@ signals: void escapePressed(); void closeTopDrawer(); + void forceTabBarActiveFocus(); + void forceStackActiveFocus(); + private: QSharedPointer m_serversModel; diff --git a/client/ui/qml/Components/ConnectButton.qml b/client/ui/qml/Components/ConnectButton.qml index 14e398a4..58da7ebf 100644 --- a/client/ui/qml/Components/ConnectButton.qml +++ b/client/ui/qml/Components/ConnectButton.qml @@ -49,10 +49,26 @@ Button { verticalOffset: 0 radius: 10 samples: 25 - color: "#FBB26A" + color: root.activeFocus ? "#D7D8DB" : "#FBB26A" source: backgroundCircle } + ShapePath { + fillColor: "transparent" + strokeColor: "#D7D8DB" + strokeWidth: root.activeFocus ? 1 : 0 + capStyle: ShapePath.RoundCap + + PathAngleArc { + centerX: backgroundCircle.width / 2 + centerY: backgroundCircle.height / 2 + radiusX: 94 + radiusY: 94 + startAngle: 0 + sweepAngle: 360 + } + } + ShapePath { fillColor: "transparent" strokeColor: { @@ -64,14 +80,14 @@ Button { return defaultButtonColor } } - strokeWidth: 3 + strokeWidth: root.activeFocus ? 2 : 3 capStyle: ShapePath.RoundCap PathAngleArc { centerX: backgroundCircle.width / 2 centerY: backgroundCircle.height / 2 - radiusX: 93 - radiusY: 93 + radiusX: 93 - (root.activeFocus ? 2 : 0) + radiusY: 93 - (root.activeFocus ? 2 : 0) startAngle: 0 sweepAngle: 360 } @@ -141,4 +157,7 @@ Button { ServersModel.setProcessedServerIndex(ServersModel.defaultIndex) ConnectionController.connectButtonClicked() } + + Keys.onEnterPressed: this.clicked() + Keys.onReturnPressed: this.clicked() } diff --git a/client/ui/qml/Components/ConnectionTypeSelectionDrawer.qml b/client/ui/qml/Components/ConnectionTypeSelectionDrawer.qml index d9dc21f4..23fe0d2a 100644 --- a/client/ui/qml/Components/ConnectionTypeSelectionDrawer.qml +++ b/client/ui/qml/Components/ConnectionTypeSelectionDrawer.qml @@ -26,6 +26,14 @@ DrawerType2 { root.expandedHeight = content.implicitHeight + 32 } + Connections { + target: root + enabled: !GC.isMobile() + function onOpened() { + focusItem.forceActiveFocus() + } + } + Header2Type { Layout.fillWidth: true Layout.topMargin: 24 @@ -36,6 +44,11 @@ DrawerType2 { headerText: qsTr("Add new connection") } + Item { + id: focusItem + KeyNavigation.tab: ip.rightButton + } + LabelWithButtonType { id: ip Layout.fillWidth: true @@ -48,11 +61,14 @@ DrawerType2 { PageController.goToPage(PageEnum.PageSetupWizardCredentials) root.close() } + + KeyNavigation.tab: qrCode.rightButton } DividerType {} LabelWithButtonType { + id: qrCode Layout.fillWidth: true text: qsTr("Open config file, key or QR code") @@ -62,6 +78,8 @@ DrawerType2 { PageController.goToPage(PageEnum.PageSetupWizardConfigSource) root.close() } + + KeyNavigation.tab: focusItem } DividerType {} diff --git a/client/ui/qml/Components/HomeContainersListView.qml b/client/ui/qml/Components/HomeContainersListView.qml index 501dc616..b0e074d0 100644 --- a/client/ui/qml/Components/HomeContainersListView.qml +++ b/client/ui/qml/Components/HomeContainersListView.qml @@ -17,12 +17,56 @@ ListView { property var rootWidth property var selectedText + property bool a: true + width: rootWidth height: menuContent.contentItem.height clip: true interactive: false + property FlickableType parentFlickable + property var lastItemTabClicked + + property int currentFocusIndex: 0 + + activeFocusOnTab: true + onActiveFocusChanged: { + if (activeFocus) { + this.currentFocusIndex = 0 + this.itemAtIndex(currentFocusIndex).forceActiveFocus() + } + } + + Keys.onTabPressed: { + if (currentFocusIndex < this.count - 1) { + currentFocusIndex += 1 + this.itemAtIndex(currentFocusIndex).forceActiveFocus() + } else { + currentFocusIndex = 0 + if (lastItemTabClicked && typeof lastItemTabClicked === "function") { + lastItemTabClicked() + } + } + } + + onVisibleChanged: { + if (visible) { + currentFocusIndex = 0 + focusItem.forceActiveFocus() + } + } + + Item { + id: focusItem + } + + onCurrentFocusIndexChanged: { + if (parentFlickable) { + parentFlickable.ensureVisible(this.itemAtIndex(currentFocusIndex)) + } + } + ButtonGroup { id: containersRadioButtonGroup } @@ -31,6 +75,12 @@ ListView { implicitWidth: rootWidth implicitHeight: content.implicitHeight + onActiveFocusChanged: { + if (activeFocus) { + containerRadioButton.forceActiveFocus() + } + } + ColumnLayout { id: content @@ -76,6 +126,19 @@ ListView { cursorShape: Qt.PointingHandCursor enabled: false } + + Keys.onEnterPressed: { + if (checkable) { + checked = true + } + containerRadioButton.clicked() + } + Keys.onReturnPressed: { + if (checkable) { + checked = true + } + containerRadioButton.clicked() + } } DividerType { diff --git a/client/ui/qml/Components/HomeSplitTunnelingDrawer.qml b/client/ui/qml/Components/HomeSplitTunnelingDrawer.qml index b5049ffb..29a83334 100644 --- a/client/ui/qml/Components/HomeSplitTunnelingDrawer.qml +++ b/client/ui/qml/Components/HomeSplitTunnelingDrawer.qml @@ -24,6 +24,14 @@ DrawerType2 { anchors.right: parent.right spacing: 0 + Connections { + target: root + enabled: !GC.isMobile() + function onOpened() { + focusItem.forceActiveFocus() + } + } + Header2Type { Layout.fillWidth: true Layout.topMargin: 24 @@ -35,7 +43,13 @@ DrawerType2 { descriptionText: qsTr("Allows you to connect to some sites or applications through a VPN connection and bypass others") } + Item { + id: focusItem + KeyNavigation.tab: splitTunnelingSwitch.visible ? splitTunnelingSwitch : siteBasedSplitTunnelingSwitch.rightButton + } + LabelWithButtonType { + id: splitTunnelingSwitch Layout.fillWidth: true Layout.topMargin: 16 @@ -45,6 +59,8 @@ DrawerType2 { descriptionText: qsTr("Enabled \nCan't be disabled for current server") rightImageSource: "qrc:/images/controls/chevron-right.svg" + KeyNavigation.tab: siteBasedSplitTunnelingSwitch.visible ? siteBasedSplitTunnelingSwitch.rightButton : focusItem + clickedFunction: function() { // PageController.goToPage(PageEnum.PageSettingsSplitTunneling) // root.close() @@ -56,6 +72,7 @@ DrawerType2 { } LabelWithButtonType { + id: siteBasedSplitTunnelingSwitch Layout.fillWidth: true Layout.topMargin: 16 @@ -63,6 +80,10 @@ DrawerType2 { descriptionText: enabled && SitesModel.isTunnelingEnabled ? qsTr("Enabled") : qsTr("Disabled") rightImageSource: "qrc:/images/controls/chevron-right.svg" + KeyNavigation.tab: appSplitTunnelingSwitch.visible ? + appSplitTunnelingSwitch.rightButton : + focusItem + clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsSplitTunneling) root.close() @@ -73,6 +94,7 @@ DrawerType2 { } LabelWithButtonType { + id: appSplitTunnelingSwitch visible: isAppSplitTinnelingEnabled Layout.fillWidth: true @@ -81,6 +103,8 @@ DrawerType2 { descriptionText: AppSplitTunnelingModel.isTunnelingEnabled ? qsTr("Enabled") : qsTr("Disabled") rightImageSource: "qrc:/images/controls/chevron-right.svg" + KeyNavigation.tab: focusItem + clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsAppSplitTunneling) root.close() diff --git a/client/ui/qml/Components/QuestionDrawer.qml b/client/ui/qml/Components/QuestionDrawer.qml index c63c07b4..57e6db83 100644 --- a/client/ui/qml/Components/QuestionDrawer.qml +++ b/client/ui/qml/Components/QuestionDrawer.qml @@ -5,6 +5,8 @@ import QtQuick.Layouts import "../Controls2" import "../Controls2/TextTypes" +import "../Config" + DrawerType2 { id: root @@ -29,6 +31,14 @@ DrawerType2 { root.expandedHeight = content.implicitHeight + 32 } + Connections { + target: root + enabled: !GC.isMobile() + function onOpened() { + focusItem.forceActiveFocus() + } + } + Header2TextType { Layout.fillWidth: true Layout.topMargin: 16 @@ -47,7 +57,13 @@ DrawerType2 { text: descriptionText } + Item { + id: focusItem + KeyNavigation.tab: yesButton + } + BasicButtonType { + id: yesButton Layout.fillWidth: true Layout.topMargin: 16 Layout.rightMargin: 16 @@ -60,9 +76,12 @@ DrawerType2 { yesButtonFunction() } } + + KeyNavigation.tab: noButton } BasicButtonType { + id: noButton Layout.fillWidth: true Layout.rightMargin: 16 Layout.leftMargin: 16 @@ -81,6 +100,8 @@ DrawerType2 { noButtonFunction() } } + + KeyNavigation.tab: focusItem } } } diff --git a/client/ui/qml/Components/SelectLanguageDrawer.qml b/client/ui/qml/Components/SelectLanguageDrawer.qml index 260a3a98..dcae22d9 100644 --- a/client/ui/qml/Components/SelectLanguageDrawer.qml +++ b/client/ui/qml/Components/SelectLanguageDrawer.qml @@ -4,6 +4,7 @@ import QtQuick.Layouts import "../Controls2" import "../Controls2/TextTypes" +import "../Config" DrawerType2 { id: root @@ -17,8 +18,21 @@ DrawerType2 { root.expandedHeight = container.implicitHeight } + Connections { + target: root + enabled: !GC.isMobile() + function onOpened() { + focusItem.forceActiveFocus() + } + } + + Item { + id: focusItem + KeyNavigation.tab: backButton + } + ColumnLayout { - id: backButton + id: backButtonLayout anchors.top: parent.top anchors.left: parent.left @@ -26,15 +40,15 @@ DrawerType2 { anchors.topMargin: 16 BackButtonType { + id: backButton backButtonImage: "qrc:/images/controls/arrow-left.svg" - backButtonFunction: function() { - root.close() - } + backButtonFunction: function() { root.close() } + KeyNavigation.tab: listView } } FlickableType { - anchors.top: backButton.bottom + anchors.top: backButtonLayout.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom @@ -71,10 +85,50 @@ DrawerType2 { id: buttonGroup } + property int currentFocusIndex: 0 + + activeFocusOnTab: true + onActiveFocusChanged: { + if (activeFocus) { + this.currentFocusIndex = 0 + this.itemAtIndex(currentFocusIndex).forceActiveFocus() + } + } + + Keys.onTabPressed: { + if (currentFocusIndex < this.count - 1) { + currentFocusIndex += 1 + this.itemAtIndex(currentFocusIndex).forceActiveFocus() + } else { + listViewFocusItem.forceActiveFocus() + focusItem.forceActiveFocus() + } + } + + Item { + id: listViewFocusItem + Keys.onTabPressed: { + root.forceActiveFocus() + } + } + + onVisibleChanged: { + if (visible) { + listViewFocusItem.forceActiveFocus() + focusItem.forceActiveFocus() + } + } + delegate: Item { implicitWidth: root.width implicitHeight: delegateContent.implicitHeight + onActiveFocusChanged: { + if (activeFocus) { + radioButton.forceActiveFocus() + } + } + ColumnLayout { id: delegateContent @@ -89,12 +143,18 @@ DrawerType2 { hoverEnabled: true indicator: Rectangle { - anchors.fill: parent + width: parent.width - 1 + height: parent.height color: radioButton.hovered ? "#2C2D30" : "#1C1D21" + border.color: radioButton.focus ? "#D7D8DB" : "transparent" + border.width: radioButton.focus ? 1 : 0 Behavior on color { PropertyAnimation { duration: 200 } } + Behavior on border.color { + PropertyAnimation { duration: 200 } + } } RowLayout { @@ -137,6 +197,9 @@ DrawerType2 { } } } + + Keys.onEnterPressed: radioButton.clicked() + Keys.onReturnPressed: radioButton.clicked() } } } diff --git a/client/ui/qml/Components/SettingsContainersListView.qml b/client/ui/qml/Components/SettingsContainersListView.qml index 5102f3e6..eda29885 100644 --- a/client/ui/qml/Components/SettingsContainersListView.qml +++ b/client/ui/qml/Components/SettingsContainersListView.qml @@ -22,16 +22,52 @@ ListView { clip: true interactive: false + activeFocusOnTab: true + Keys.onTabPressed: { + if (currentIndex < this.count - 1) { + this.incrementCurrentIndex() + } else { + currentIndex = 0 + lastItemTabClickedSignal() + } + } + + onCurrentIndexChanged: { + if (visible) { + if (fl.contentHeight > fl.height) { + var item = this.currentItem + if (item.y < fl.height) { + fl.contentY = item.y + } else if (item.y + item.height > fl.contentY + fl.height) { + fl.contentY = item.y + item.height - fl.height + } + } + } + } + + onVisibleChanged: { + if (visible) { + this.currentIndex = 0 + } + } + delegate: Item { implicitWidth: root.width implicitHeight: delegateContent.implicitHeight + onActiveFocusChanged: { + if (activeFocus) { + containerRadioButton.rightButton.forceActiveFocus() + } + } + ColumnLayout { id: delegateContent anchors.fill: parent LabelWithButtonType { + id: containerRadioButton implicitWidth: parent.width text: name diff --git a/client/ui/qml/Components/ShareConnectionDrawer.qml b/client/ui/qml/Components/ShareConnectionDrawer.qml index 1f7db930..edd5d42e 100644 --- a/client/ui/qml/Components/ShareConnectionDrawer.qml +++ b/client/ui/qml/Components/ShareConnectionDrawer.qml @@ -121,6 +121,9 @@ DrawerType2 { text: qsTr("Copy") imageSource: "qrc:/images/controls/copy.svg" + Keys.onReturnPressed: { copyConfigTextButton.clicked() } + Keys.onEnterPressed: { copyConfigTextButton.clicked() } + KeyNavigation.tab: copyNativeConfigStringButton.visible ? copyNativeConfigStringButton : showSettingsButton } @@ -174,11 +177,30 @@ DrawerType2 { anchors.fill: parent expandedHeight: parent.height * 0.9 + onClosed: { + if (!GC.isMobile()) { + header.forceActiveFocus() + } + } + expandedContent: Item { id: configContentContainer implicitHeight: configContentDrawer.expandedHeight + Connections { + target: configContentDrawer + enabled: !GC.isMobile() + function onOpened() { + focusItem.forceActiveFocus() + } + } + + Item { + id: focusItem + KeyNavigation.tab: backButton + } + Connections { target: copyNativeConfigStringButton function onClicked() { @@ -196,6 +218,7 @@ DrawerType2 { configText.copy() configText.select(0, 0) PageController.showNotificationMessage(qsTr("Copied")) + header.forceActiveFocus() } } @@ -207,9 +230,9 @@ DrawerType2 { anchors.right: parent.right anchors.topMargin: 16 - backButtonFunction: function() { - configContentDrawer.close() - } + backButtonFunction: function() { configContentDrawer.close() } + + KeyNavigation.tab: focusItem } FlickableType { @@ -256,6 +279,7 @@ DrawerType2 { height: 24 readOnly: true + activeFocusOnTab: false color: "#D7D8DB" selectionColor: "#633303" diff --git a/client/ui/qml/Components/TransportProtoSelector.qml b/client/ui/qml/Components/TransportProtoSelector.qml index bfd82cb1..12e48635 100644 --- a/client/ui/qml/Components/TransportProtoSelector.qml +++ b/client/ui/qml/Components/TransportProtoSelector.qml @@ -17,12 +17,19 @@ Rectangle { color: "#1C1D21" radius: 16 + onFocusChanged: { + if (focus) { + udpButton.forceActiveFocus() + } + } + RowLayout { id: transportProtoButtonGroup spacing: 0 HorizontalRadioButton { + id: udpButton checked: root.currentIndex === 0 hoverEnabled: root.enabled @@ -30,12 +37,15 @@ Rectangle { implicitWidth: (rootWidth - 32) / 2 text: "UDP" + KeyNavigation.tab: tcpButton + onClicked: { root.currentIndex = 0 } } HorizontalRadioButton { + id: tcpButton checked: root.currentIndex === 1 hoverEnabled: root.enabled diff --git a/client/ui/qml/Controls2/BackButtonType.qml b/client/ui/qml/Controls2/BackButtonType.qml index f1044745..42ab8340 100644 --- a/client/ui/qml/Controls2/BackButtonType.qml +++ b/client/ui/qml/Controls2/BackButtonType.qml @@ -13,6 +13,12 @@ Item { visible: backButtonImage !== "" + onActiveFocusChanged: { + if (activeFocus) { + backButton.forceActiveFocus() + } + } + RowLayout { id: content @@ -20,6 +26,7 @@ Item { anchors.leftMargin: 8 ImageButtonType { + id: backButton image: backButtonImage imageColor: "#D7D8DB" @@ -42,4 +49,7 @@ Item { color: "transparent" } } + + Keys.onEnterPressed: backButton.clicked() + Keys.onReturnPressed: backButton.clicked() } diff --git a/client/ui/qml/Controls2/BasicButtonType.qml b/client/ui/qml/Controls2/BasicButtonType.qml index 9a0011e4..646ccd57 100644 --- a/client/ui/qml/Controls2/BasicButtonType.qml +++ b/client/ui/qml/Controls2/BasicButtonType.qml @@ -26,18 +26,29 @@ Button { property bool squareLeftSide: false + property FlickableType parentFlickable + property var clickedFunc implicitHeight: 56 hoverEnabled: true + focusPolicy: Qt.TabFocus + + onFocusChanged: { + if (root.activeFocus) { + if (root.parentFlickable) { + root.parentFlickable.ensureVisible(this) + } + } + } background: Rectangle { id: focusBorder color: "transparent" border.color: root.activeFocus ? root.borderFocusedColor : "transparent" - border.width: root.activeFocus ? root.borderFocusedWidth : "transparent" + border.width: root.activeFocus ? root.borderFocusedWidth : 0 anchors.fill: parent diff --git a/client/ui/qml/Controls2/CheckBoxType.qml b/client/ui/qml/Controls2/CheckBoxType.qml index 962c7fbd..ac77e900 100644 --- a/client/ui/qml/Controls2/CheckBoxType.qml +++ b/client/ui/qml/Controls2/CheckBoxType.qml @@ -23,6 +23,8 @@ CheckBox { property string checkedBorderColor: "#FBB26A" property string checkedBorderDisabledColor: "#402102" + property string borderFocusedColor: "#D7D8DB" + property string checkedImageColor: "#FBB26A" property string pressedImageColor: "#A85809" property string defaultImageColor: "transparent" @@ -30,7 +32,24 @@ CheckBox { property string imageSource: "qrc:/images/controls/check.svg" + property var parentFlickable + onFocusChanged: { + if (root.activeFocus) { + if (root.parentFlickable) { + root.parentFlickable.ensureVisible(root) + } + } + } + hoverEnabled: enabled ? true : false + focusPolicy: Qt.NoFocus + + background: Rectangle { + color: "transparent" + border.color: root.focus ? borderFocusedColor : "transparent" + border.width: 1 + radius: 16 + } indicator: Rectangle { id: background @@ -59,7 +78,11 @@ CheckBox { width: 24 height: 24 color: "transparent" - border.color: root.checked ? (root.enabled ? checkedBorderColor : checkedBorderDisabledColor) : defaultBorderColor + border.color: root.checked ? + (root.enabled ? + checkedBorderColor : + checkedBorderDisabledColor) : + defaultBorderColor border.width: 1 radius: 4 @@ -130,6 +153,16 @@ CheckBox { cursorShape: Qt.PointingHandCursor enabled: false } + + + Keys.onEnterPressed: { + root.checked = !root.checked + } + + Keys.onReturnPressed: { + root.checked = !root.checked + } + } diff --git a/client/ui/qml/Controls2/DropDownType.qml b/client/ui/qml/Controls2/DropDownType.qml index 5b876d79..c1dc1124 100644 --- a/client/ui/qml/Controls2/DropDownType.qml +++ b/client/ui/qml/Controls2/DropDownType.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import "TextTypes" +import "../Config" Item { id: root @@ -27,6 +28,9 @@ Item { property string rootButtonBackgroundHoveredColor: "#1C1D21" property string rootButtonBackgroundPressedColor: "#1C1D21" + property string borderFocusedColor: "#D7D8DB" + property int borderFocusedWidth: 1 + property string rootButtonHoveredBorderColor: "#494B50" property string rootButtonDefaultBorderColor: "#2C2D30" property string rootButtonPressedBorderColor: "#D7D8DB" @@ -42,44 +46,70 @@ Item { signal open signal close + function popupClosedFunc() { + if (!GC.isMobile()) { + this.forceActiveFocus() + } + } + + property var parentFlickable + onFocusChanged: { + if (root.activeFocus) { + if (root.parentFlickable) { + root.parentFlickable.ensureVisible(root) + } + } + } + implicitWidth: rootButtonContent.implicitWidth implicitHeight: rootButtonContent.implicitHeight onOpen: { menu.open() - rootButtonBackground.border.color = rootButtonPressedBorderColor } onClose: { menu.close() - rootButtonBackground.border.color = rootButtonDefaultBorderColor - } - - onEnabledChanged: { - if (enabled) { - rootButtonBackground.color = rootButtonBackgroundColor - rootButtonBackground.border.color = rootButtonDefaultBorderColor - } else { - rootButtonBackground.color = "transparent" - rootButtonBackground.border.color = rootButtonHoveredBorderColor - } } Rectangle { - id: rootButtonBackground + id: focusBorder + + color: "transparent" + border.color: root.activeFocus ? root.borderFocusedColor : "transparent" + border.width: root.activeFocus ? root.borderFocusedWidth : 0 anchors.fill: rootButtonContent - radius: 16 - color: root.enabled ? rootButtonBackgroundColor : "transparent" - border.color: root.enabled ? rootButtonDefaultBorderColor : rootButtonHoveredBorderColor - border.width: 1 - Behavior on border.color { - PropertyAnimation { duration: 200 } - } - Behavior on color { - PropertyAnimation { duration: 200 } + Rectangle { + id: rootButtonBackground + + anchors.fill: focusBorder + anchors.margins: root.activeFocus ? 2 : 0 + radius: root.activeFocus ? 14 : 16 + + color: { + if (root.enabled) { + if (root.pressed) { + return root.rootButtonBackgroundPressedColor + } + return root.hovered ? root.rootButtonBackgroundHoveredColor : root.rootButtonBackgroundColor + } else { + return "transparent" + } + } + + border.color: rootButtonDefaultBorderColor + border.width: 1 + + Behavior on border.color { + PropertyAnimation { duration: 200 } + } + + Behavior on color { + PropertyAnimation { duration: 200 } + } } } @@ -107,6 +137,7 @@ Item { } ButtonTextType { + id: buttonText Layout.fillWidth: true horizontalAlignment: Text.AlignLeft @@ -136,26 +167,6 @@ Item { cursorShape: Qt.PointingHandCursor hoverEnabled: root.enabled ? true : false - onEntered: { - if (menu.isClosed) { - rootButtonBackground.border.color = rootButtonHoveredBorderColor - rootButtonBackground.color = rootButtonBackgroundHoveredColor - } - } - - onExited: { - if (menu.isClosed) { - rootButtonBackground.border.color = rootButtonDefaultBorderColor - rootButtonBackground.color = rootButtonBackgroundColor - } - } - - onPressed: { - if (menu.isClosed) { - rootButtonBackground.color = pressed ? rootButtonBackgroundPressedColor : entered ? rootButtonHoveredBorderColor : rootButtonDefaultBorderColor - } - } - onClicked: { if (rootButtonClickedFunction && typeof rootButtonClickedFunction === "function") { rootButtonClickedFunction() @@ -173,11 +184,27 @@ Item { anchors.fill: parent expandedHeight: drawerParent.height * drawerHeight + onClosed: { + root.popupClosedFunc() + } + expandedContent: Item { id: container - implicitHeight: menu.expandedHeight + Connections { + target: menu + enabled: !GC.isMobile() + function onOpened() { + focusItem.forceActiveFocus() + } + } + + Item { + id: focusItem + KeyNavigation.tab: backButton + } + ColumnLayout { id: header @@ -187,14 +214,15 @@ Item { anchors.topMargin: 16 BackButtonType { + id: backButton backButtonImage: root.headerBackButtonImage - backButtonFunction: function() { - menu.close() - } + backButtonFunction: function() { menu.close() } + KeyNavigation.tab: listViewLoader.item } } FlickableType { + id: flickable anchors.top: header.bottom anchors.topMargin: 16 contentHeight: col.implicitHeight @@ -221,9 +249,28 @@ Item { Loader { id: listViewLoader sourceComponent: root.listView + + onLoaded: { + listViewLoader.item.parentFlickable = flickable + listViewLoader.item.lastItemTabClicked = function() { + focusItem.forceActiveFocus() + } + } } } } } } + + Keys.onEnterPressed: { + if (menu.isClosed) { + menu.open() + } + } + + Keys.onReturnPressed: { + if (menu.isClosed) { + menu.open() + } + } } diff --git a/client/ui/qml/Controls2/FlickableType.qml b/client/ui/qml/Controls2/FlickableType.qml index 073be058..bcd14487 100644 --- a/client/ui/qml/Controls2/FlickableType.qml +++ b/client/ui/qml/Controls2/FlickableType.qml @@ -5,6 +5,14 @@ import "../Config" Flickable { id: fl + function ensureVisible(item) { + if (item.y < fl.contentY) { + fl.contentY = item.y + } else if (item.y + item.height > fl.contentY + fl.height) { + fl.contentY = item.y + item.height - fl.height + 40 // 40 is a bottom margin + } + } + clip: true width: parent.width diff --git a/client/ui/qml/Controls2/Header2Type.qml b/client/ui/qml/Controls2/Header2Type.qml index 4d812f6c..a08e711e 100644 --- a/client/ui/qml/Controls2/Header2Type.qml +++ b/client/ui/qml/Controls2/Header2Type.qml @@ -9,6 +9,8 @@ Item { property string actionButtonImage property var actionButtonFunction + property alias actionButton: headerActionButton + property string headerText property string descriptionText @@ -60,4 +62,16 @@ Item { visible: root.descriptionText !== "" } } + + Keys.onEnterPressed: { + if (actionButtonFunction && typeof actionButtonFunction === "function") { + actionButtonFunction() + } + } + + Keys.onReturnPressed: { + if (actionButtonFunction && typeof actionButtonFunction === "function") { + actionButtonFunction() + } + } } diff --git a/client/ui/qml/Controls2/HeaderType.qml b/client/ui/qml/Controls2/HeaderType.qml index d0ed3418..0bba92e9 100644 --- a/client/ui/qml/Controls2/HeaderType.qml +++ b/client/ui/qml/Controls2/HeaderType.qml @@ -9,12 +9,16 @@ Item { property string actionButtonImage property var actionButtonFunction + property alias actionButton: headerActionButton + property string headerText property int headerTextMaximumLineCount: 2 property int headerTextElide: Qt.ElideRight property string descriptionText + focus: true + implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight @@ -67,4 +71,16 @@ Item { visible: root.descriptionText !== "" } } + + Keys.onEnterPressed: { + if (actionButtonFunction && typeof actionButtonFunction === "function") { + actionButtonFunction() + } + } + + Keys.onReturnPressed: { + if (actionButtonFunction && typeof actionButtonFunction === "function") { + actionButtonFunction() + } + } } diff --git a/client/ui/qml/Controls2/HorizontalRadioButton.qml b/client/ui/qml/Controls2/HorizontalRadioButton.qml index 81cc8ec0..6a0b8125 100644 --- a/client/ui/qml/Controls2/HorizontalRadioButton.qml +++ b/client/ui/qml/Controls2/HorizontalRadioButton.qml @@ -19,6 +19,7 @@ RadioButton { property string checkedBorderColor: "#FBB26A" property string defaultBodredColor: "transparent" property string checkedDisabledBorderColor: "#84603D" + property string borderFocusedColor: "#D7D8DB" property int borderWidth: 0 implicitWidth: content.implicitWidth @@ -47,6 +48,8 @@ RadioButton { return root.pressedBorderColor } else if (root.checked) { return root.checkedBorderColor + } else if (root.activeFocus) { + return root.borderFocusedColor } return root.defaultBodredColor } else { @@ -58,7 +61,7 @@ RadioButton { } border.width: { - if(root.checked) { + if(root.checked || root.activeFocus) { return 1 } return root.pressed ? 1 : 0 @@ -97,4 +100,12 @@ RadioButton { cursorShape: Qt.PointingHandCursor enabled: false } + + Keys.onEnterPressed: { + this.clicked() + } + + Keys.onReturnPressed: { + this.clicked() + } } diff --git a/client/ui/qml/Controls2/ImageButtonType.qml b/client/ui/qml/Controls2/ImageButtonType.qml index 1ab57511..a08b613a 100644 --- a/client/ui/qml/Controls2/ImageButtonType.qml +++ b/client/ui/qml/Controls2/ImageButtonType.qml @@ -18,11 +18,26 @@ Button { property alias backgroundColor: background.color property alias backgroundRadius: background.radius + property string borderFocusedColor: "#D7D8DB" + property int borderFocusedWidth: 1 + hoverEnabled: true + focus: true + focusPolicy: Qt.TabFocus icon.source: image icon.color: root.enabled ? imageColor : disableImageColor + property Flickable parentFlickable + + onFocusChanged: { + if (root.activeFocus) { + if (root.parentFlickable) { + root.parentFlickable.ensureVisible(this) + } + } + } + Behavior on icon.color { PropertyAnimation { duration: 200 } } @@ -31,6 +46,9 @@ Button { id: background anchors.fill: parent + border.color: root.activeFocus ? root.borderFocusedColor : "transparent" + border.width: root.activeFocus ? root.borderFocusedWidth : 0 + color: { if (root.enabled) { if (root.pressed) { @@ -44,6 +62,9 @@ Button { Behavior on color { PropertyAnimation { duration: 200 } } + Behavior on border.color { + PropertyAnimation { duration: 200 } + } } MouseArea { diff --git a/client/ui/qml/Controls2/LabelWithButtonType.qml b/client/ui/qml/Controls2/LabelWithButtonType.qml index 8b85d591..d8860ae6 100644 --- a/client/ui/qml/Controls2/LabelWithButtonType.qml +++ b/client/ui/qml/Controls2/LabelWithButtonType.qml @@ -19,12 +19,18 @@ Item { property string leftImageSource property bool isLeftImageHoverEnabled: true //todo separete this qml file to 3 + property alias rightButton: rightImage + property FlickableType parentFlickable + property string textColor: "#d7d8db" property string textDisabledColor: "#878B91" property string descriptionColor: "#878B91" property string descriptionDisabledColor: "#494B50" property real textOpacity: 1.0 + property string borderFocusedColor: "#D7D8DB" + property int borderFocusedWidth: 1 + property string rightImageColor: "#d7d8db" property bool descriptionOnTop: false @@ -32,6 +38,25 @@ Item { implicitWidth: content.implicitWidth + content.anchors.topMargin + content.anchors.bottomMargin implicitHeight: content.implicitHeight + content.anchors.leftMargin + content.anchors.rightMargin + onFocusChanged: { + if (root.activeFocus) { + if (root.parentFlickable) { + root.parentFlickable.ensureVisible(root) + } + } + } + + Connections { + target: rightImage + function onFocusChanged() { + if (rightImage.activeFocus) { + if (root.parentFlickable) { + root.parentFlickable.ensureVisible(root) + } + } + } + } + RowLayout { id: content anchors.fill: parent @@ -163,6 +188,9 @@ Item { anchors.fill: root color: "transparent" + border.color: root.activeFocus ? root.borderFocusedColor : "transparent" + border.width: root.activeFocus ? root.borderFocusedWidth : 0 + Behavior on color { PropertyAnimation { duration: 200 } @@ -207,4 +235,16 @@ Item { } } } + + Keys.onEnterPressed: { + if (clickedFunction && typeof clickedFunction === "function") { + clickedFunction() + } + } + + Keys.onReturnPressed: { + if (clickedFunction && typeof clickedFunction === "function") { + clickedFunction() + } + } } diff --git a/client/ui/qml/Controls2/ListViewWithRadioButtonType.qml b/client/ui/qml/Controls2/ListViewWithRadioButtonType.qml index 4138c087..8551c7d5 100644 --- a/client/ui/qml/Controls2/ListViewWithRadioButtonType.qml +++ b/client/ui/qml/Controls2/ListViewWithRadioButtonType.qml @@ -26,6 +26,47 @@ ListView { clip: true interactive: false + property FlickableType parentFlickable + property var lastItemTabClicked + + property int currentFocusIndex: 0 + + activeFocusOnTab: true + onActiveFocusChanged: { + if (activeFocus) { + this.currentFocusIndex = 0 + this.itemAtIndex(currentFocusIndex).forceActiveFocus() + } + } + + Keys.onTabPressed: { + if (currentFocusIndex < this.count - 1) { + currentFocusIndex += 1 + } else { + currentFocusIndex = 0 + } + this.itemAtIndex(currentFocusIndex).forceActiveFocus() + } + + Item { + id: focusItem + Keys.onTabPressed: { + root.forceActiveFocus() + } + } + + onVisibleChanged: { + if (visible) { + focusItem.forceActiveFocus() + } + } + + onCurrentFocusIndexChanged: { + if (parentFlickable) { + parentFlickable.ensureVisible(this.itemAtIndex(currentFocusIndex)) + } + } + ButtonGroup { id: buttonGroup } @@ -40,6 +81,12 @@ ListView { implicitWidth: rootWidth implicitHeight: content.implicitHeight + onActiveFocusChanged: { + if (activeFocus) { + radioButton.forceActiveFocus() + } + } + ColumnLayout { id: content @@ -54,12 +101,18 @@ ListView { hoverEnabled: true indicator: Rectangle { - anchors.fill: parent + width: parent.width - 1 + height: parent.height color: radioButton.hovered ? "#2C2D30" : "#1C1D21" + border.color: radioButton.focus ? "#D7D8DB" : "transparent" + border.width: radioButton.focus ? 1 : 0 Behavior on color { PropertyAnimation { duration: 200 } } + Behavior on border.color { + PropertyAnimation { duration: 200 } + } MouseArea { anchors.fill: parent @@ -117,5 +170,13 @@ ListView { root.selectedText = name } } + + Keys.onReturnPressed: { + radioButton.clicked() + } + + Keys.onEnterPressed: { + radioButton.clicked() + } } } diff --git a/client/ui/qml/Controls2/PageType.qml b/client/ui/qml/Controls2/PageType.qml index 2ca75e9d..0a2a2998 100644 --- a/client/ui/qml/Controls2/PageType.qml +++ b/client/ui/qml/Controls2/PageType.qml @@ -11,6 +11,28 @@ Item { property var defaultActiveFocusItem: null + onVisibleChanged: { + if (visible && !GC.isMobile()) { + timer.start() + } + } + + function lastItemTabClicked(focusItem) { + if (GC.isMobile()) { + return + } + + if (focusItem) { + focusItem.forceActiveFocus() + PageController.forceTabBarActiveFocus() + } else { + if (defaultActiveFocusItem) { + defaultActiveFocusItem.forceActiveFocus() + } + PageController.forceTabBarActiveFocus() + } + } + // MouseArea { // id: globalMouseArea // z: 99 diff --git a/client/ui/qml/Controls2/PopupType.qml b/client/ui/qml/Controls2/PopupType.qml index c85997dc..f3771cd4 100644 --- a/client/ui/qml/Controls2/PopupType.qml +++ b/client/ui/qml/Controls2/PopupType.qml @@ -25,6 +25,14 @@ Popup { color: Qt.rgba(14/255, 14/255, 17/255, 0.8) } + onOpened: { + focusItem.forceActiveFocus() + } + + onClosed: { + PageController.forceStackActiveFocus() + } + background: Rectangle { anchors.fill: parent @@ -52,7 +60,13 @@ Popup { text: root.text } + Item { + id: focusItem + KeyNavigation.tab: closeButton + } + BasicButtonType { + id: closeButton visible: closeButtonVisible implicitHeight: 32 @@ -66,6 +80,8 @@ Popup { borderWidth: 0 text: qsTr("Close") + KeyNavigation.tab: focusItem + clickedFunc: function() { root.close() } diff --git a/client/ui/qml/Controls2/SwitcherType.qml b/client/ui/qml/Controls2/SwitcherType.qml index 37024872..9433832f 100644 --- a/client/ui/qml/Controls2/SwitcherType.qml +++ b/client/ui/qml/Controls2/SwitcherType.qml @@ -18,6 +18,9 @@ Switch { property string defaultIndicatorColor: "transparent" property string checkedDisabledIndicatorColor: "#402102" + property string borderFocusedColor: "#D7D8DB" + property int borderFocusedWidth: 1 + property string checkedIndicatorBorderColor: "#633303" property string defaultIndicatorBorderColor: "#494B50" property string checkedDisabledIndicatorBorderColor: "#402102" @@ -31,6 +34,16 @@ Switch { property string defaultIndicatorBackgroundColor: "transparent" hoverEnabled: enabled ? true : false + focusPolicy: Qt.TabFocus + + property FlickableType parentFlickable: null + onFocusChanged: { + if (root.activeFocus) { + if (root.parentFlickable) { + root.parentFlickable.ensureVisible(root) + } + } + } indicator: Rectangle { id: switcher @@ -44,8 +57,9 @@ Switch { radius: 16 color: root.checked ? (root.enabled ? root.checkedIndicatorColor : root.checkedDisabledIndicatorColor) : root.defaultIndicatorColor - border.color: root.checked ? (root.enabled ? root.checkedIndicatorBorderColor : root.checkedDisabledIndicatorBorderColor) - : root.defaultIndicatorBorderColor + + border.color: root.activeFocus ? root.borderFocusedColor : (root.checked ? (root.enabled ? root.checkedIndicatorBorderColor : root.checkedDisabledIndicatorBorderColor) + : root.defaultIndicatorBorderColor) Behavior on color { PropertyAnimation { duration: 200 } @@ -114,4 +128,14 @@ Switch { cursorShape: Qt.PointingHandCursor enabled: false } + + Keys.onEnterPressed: { + root.checked = !root.checked + root.checkedChanged() + } + + Keys.onReturnPressed: { + root.checked = !root.checked + root.checkedChanged() + } } diff --git a/client/ui/qml/Controls2/TabButtonType.qml b/client/ui/qml/Controls2/TabButtonType.qml index f8011f0d..11df83d8 100644 --- a/client/ui/qml/Controls2/TabButtonType.qml +++ b/client/ui/qml/Controls2/TabButtonType.qml @@ -10,11 +10,15 @@ TabButton { property string textColor: "#D7D8DB" + property string borderFocusedColor: "#D7D8DB" + property int borderFocusedWidth: 1 + property bool isSelected: false implicitHeight: 48 hoverEnabled: true + focusPolicy: Qt.TabFocus background: Rectangle { id: background @@ -22,6 +26,9 @@ TabButton { anchors.fill: parent color: "transparent" + border.color: root.activeFocus ? root.borderFocusedColor : "transparent" + border.width: root.activeFocus ? root.borderFocusedWidth : 0 + Rectangle { width: parent.width height: 1 diff --git a/client/ui/qml/Controls2/TabImageButtonType.qml b/client/ui/qml/Controls2/TabImageButtonType.qml index 4d745a0b..f93f4924 100644 --- a/client/ui/qml/Controls2/TabImageButtonType.qml +++ b/client/ui/qml/Controls2/TabImageButtonType.qml @@ -12,7 +12,13 @@ TabButton { property bool isSelected: false + property string borderFocusedColor: "#D7D8DB" + property int borderFocusedWidth: 1 + + property var clickedFunc + hoverEnabled: true + focusPolicy: Qt.TabFocus icon.source: image icon.color: isSelected ? selectedColor : defaultColor @@ -21,6 +27,11 @@ TabButton { id: background anchors.fill: parent color: "transparent" + radius: 10 + + border.color: root.activeFocus ? root.borderFocusedColor : "transparent" + border.width: root.activeFocus ? root.borderFocusedWidth : 0 + } MouseArea { @@ -28,4 +39,22 @@ TabButton { cursorShape: Qt.PointingHandCursor enabled: false } + + Keys.onEnterPressed: { + if (root.clickedFunc && typeof root.clickedFunc === "function") { + root.clickedFunc() + } + } + + Keys.onReturnPressed: { + if (root.clickedFunc && typeof root.clickedFunc === "function") { + root.clickedFunc() + } + } + + onClicked: { + if (root.clickedFunc && typeof root.clickedFunc === "function") { + root.clickedFunc() + } + } } diff --git a/client/ui/qml/Controls2/TextAreaType.qml b/client/ui/qml/Controls2/TextAreaType.qml index f4f75417..653ab477 100644 --- a/client/ui/qml/Controls2/TextAreaType.qml +++ b/client/ui/qml/Controls2/TextAreaType.qml @@ -19,6 +19,15 @@ Rectangle { border.color: getBorderColor(borderNormalColor) radius: 16 + property FlickableType parentFlickable: null + onFocusChanged: { + if (root.activeFocus) { + if (root.parentFlickable) { + root.parentFlickable.ensureVisible(root) + } + } + } + MouseArea { id: parentMouse anchors.fill: parent diff --git a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml index 48f93f13..3a6ac1fa 100644 --- a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml +++ b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml @@ -13,6 +13,7 @@ Item { property alias errorText: errorField.text property bool checkEmptyText: false + property bool rightButtonClickedOnEnter: false property string buttonText property string buttonImageSource @@ -36,6 +37,18 @@ Item { implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight + property FlickableType parentFlickable + Connections { + target: textField + function onFocusChanged() { + if (textField.activeFocus) { + if (root.parentFlickable) { + root.parentFlickable.ensureVisible(root) + } + } + } + } + ColumnLayout { id: content anchors.fill: parent @@ -188,10 +201,22 @@ Item { } Keys.onEnterPressed: { - KeyNavigation.tab.forceActiveFocus(); + if (root.rightButtonClickedOnEnter && root.clickedFunc && typeof root.clickedFunc === "function") { + clickedFunc() + } + + if (KeyNavigation.tab) { + KeyNavigation.tab.forceActiveFocus(); + } } Keys.onReturnPressed: { - KeyNavigation.tab.forceActiveFocus(); + if (root.rightButtonClickedOnEnter &&root.clickedFunc && typeof root.clickedFunc === "function") { + clickedFunc() + } + + if (KeyNavigation.tab) { + KeyNavigation.tab.forceActiveFocus(); + } } } diff --git a/client/ui/qml/Controls2/VerticalRadioButton.qml b/client/ui/qml/Controls2/VerticalRadioButton.qml index ed7ed143..bc696cfa 100644 --- a/client/ui/qml/Controls2/VerticalRadioButton.qml +++ b/client/ui/qml/Controls2/VerticalRadioButton.qml @@ -20,16 +20,23 @@ RadioButton { property string textColor: "#D7D8DB" property string selectedTextColor: "#FBB26A" + property string borderFocusedColor: "#D7D8DB" + property int borderFocusedWidth: 1 + property string imageSource property bool showImage hoverEnabled: true + focusPolicy: Qt.TabFocus indicator: Rectangle { id: background anchors.verticalCenter: parent.verticalCenter + border.color: root.focus ? root.borderFocusedColor : "transparent" + border.width: root.focus ? root.borderFocusedWidth : 0 + implicitWidth: 56 implicitHeight: 56 radius: 16 @@ -51,6 +58,10 @@ RadioButton { PropertyAnimation { duration: 200 } } + Behavior on border.color { + PropertyAnimation { duration: 200 } + } + Image { source: { if (showImage) { diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index e4aebc6e..21098cb2 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -18,6 +18,8 @@ import "../Components" PageType { id: root + defaultActiveFocusItem: focusItem + Connections { target: PageController @@ -38,7 +40,15 @@ PageType { anchors.topMargin: 34 anchors.bottomMargin: 34 + Item { + id: focusItem + KeyNavigation.tab: loggingButton.visible ? + loggingButton : + connectButton + } + BasicButtonType { + id: loggingButton property bool isLoggingEnabled: SettingsController.isLoggingEnabled Layout.alignment: Qt.AlignHCenter @@ -55,6 +65,11 @@ PageType { visible: isLoggingEnabled ? true : false text: qsTr("Logging enabled") + Keys.onEnterPressed: loggingButton.clicked() + Keys.onReturnPressed: loggingButton.clicked() + + KeyNavigation.tab: connectButton + onClicked: { PageController.goToPage(PageEnum.PageSettingsLogging) } @@ -64,9 +79,11 @@ PageType { id: connectButton Layout.fillHeight: true Layout.alignment: Qt.AlignCenter + KeyNavigation.tab: splitTunnelingButton } BasicButtonType { + id: splitTunnelingButton Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom Layout.bottomMargin: 34 leftPadding: 16 @@ -90,6 +107,11 @@ PageType { imageSource: isSplitTunnelingEnabled ? "qrc:/images/controls/split-tunneling.svg" : "" rightImageSource: "qrc:/images/controls/chevron-down.svg" + Keys.onEnterPressed: splitTunnelingButton.clicked() + Keys.onReturnPressed: splitTunnelingButton.clicked() + + KeyNavigation.tab: drawer + onClicked: { homeSplitTunnelingDrawer.open() } @@ -97,6 +119,12 @@ PageType { HomeSplitTunnelingDrawer { id: homeSplitTunnelingDrawer parent: root + + onClosed: { + if (!GC.isMobile()) { + focusItem.forceActiveFocus() + } + } } } } @@ -107,11 +135,26 @@ PageType { id: drawer anchors.fill: parent + onClosed: { + if (!GC.isMobile()) { + focusItem.forceActiveFocus() + } + } + collapsedContent: Item { implicitHeight: Qt.platform.os !== "ios" ? root.height * 0.9 : screen.height * 0.77 Component.onCompleted: { drawer.expandedHeight = implicitHeight } + Connections { + target: drawer + enabled: !GC.isMobile() + function onActiveFocusChanged() { + if (drawer.activeFocus && !drawer.isOpened) { + collapsedButtonChevron.forceActiveFocus() + } + } + } ColumnLayout { id: collapsed @@ -178,6 +221,8 @@ PageType { text: ServersModel.defaultServerName horizontalAlignment: Qt.AlignHCenter + KeyNavigation.tab: tabBar + Behavior on opacity { PropertyAnimation { duration: 200 } } @@ -201,6 +246,11 @@ PageType { topPadding: 4 bottomPadding: 3 + Keys.onEnterPressed: collapsedButtonChevron.clicked() + Keys.onReturnPressed: collapsedButtonChevron.clicked() + Keys.onTabPressed: lastItemTabClicked() + + onClicked: { if (drawer.isCollapsed) { drawer.open() @@ -217,6 +267,16 @@ PageType { } } + Connections { + target: drawer + enabled: !GC.isMobile() + function onIsCollapsedChanged() { + if (!drawer.isCollapsed) { + focusItem1.forceActiveFocus() + } + } + } + ColumnLayout { id: serversMenuHeader @@ -230,6 +290,11 @@ PageType { visible: !ServersModel.isDefaultServerFromApi + Item { + id: focusItem1 + KeyNavigation.tab: containersDropDown + } + DropDownType { id: containersDropDown @@ -252,9 +317,16 @@ PageType { } drawerParent: root + KeyNavigation.tab: serversMenuContent listView: HomeContainersListView { + id: containersListView rootWidth: root.width + onVisibleChanged: { + if (containersDropDown.visible && !GC.isMobile()) { + focusItem1.forceActiveFocus() + } + } Connections { target: ServersModel @@ -317,9 +389,43 @@ PageType { policy: serversMenuContent.height >= serversMenuContent.contentHeight ? ScrollBar.AlwaysOff : ScrollBar.AlwaysOn } + + activeFocusOnTab: true + focus: true + + property int focusItemIndex: 0 + onActiveFocusChanged: { + if (activeFocus) { + serversMenuContent.focusItemIndex = 0 + serversMenuContent.itemAtIndex(focusItemIndex).forceActiveFocus() + } + } + + onFocusItemIndexChanged: { + const focusedElement = serversMenuContent.itemAtIndex(focusItemIndex) + if (focusedElement) { + if (focusedElement.y + focusedElement.height > serversMenuContent.height) { + serversMenuContent.contentY = focusedElement.y + focusedElement.height - serversMenuContent.height + } else { + serversMenuContent.contentY = 0 + } + } + } + Keys.onUpPressed: scrollBar.decrease() Keys.onDownPressed: scrollBar.increase() + Connections { + target: drawer + enabled: !GC.isMobile() + function onIsCollapsedChanged() { + if (drawer.isCollapsed) { + const item = serversMenuContent.itemAtIndex(serversMenuContent.focusItemIndex) + if (item) { item.serverRadioButtonProperty.focus = false } + } + } + } + Connections { target: ServersModel function onDefaultServerIndexChanged(serverIndex) { @@ -333,10 +439,17 @@ PageType { id: menuContentDelegate property variant delegateData: model + property VerticalRadioButton serverRadioButtonProperty: serverRadioButton implicitWidth: serversMenuContent.width implicitHeight: serverRadioButtonContent.implicitHeight + onActiveFocusChanged: { + if (activeFocus) { + serverRadioButton.forceActiveFocus() + } + } + ColumnLayout { id: serverRadioButtonContent @@ -377,9 +490,14 @@ PageType { cursorShape: Qt.PointingHandCursor enabled: false } + + Keys.onTabPressed: serverInfoButton.forceActiveFocus() + Keys.onEnterPressed: serverRadioButton.clicked() + Keys.onReturnPressed: serverRadioButton.clicked() } ImageButtonType { + id: serverInfoButton image: "qrc:/images/controls/settings.svg" imageColor: "#D7D8DB" @@ -388,6 +506,18 @@ PageType { z: 1 + Keys.onTabPressed: { + if (serversMenuContent.focusItemIndex < serversMenuContent.count - 1) { + serversMenuContent.focusItemIndex++ + serversMenuContent.itemAtIndex(serversMenuContent.focusItemIndex).forceActiveFocus() + } else { + focusItem1.forceActiveFocus() + serversMenuContent.contentY = 0 + } + } + Keys.onEnterPressed: serverInfoButton.clicked() + Keys.onReturnPressed: serverInfoButton.clicked() + onClicked: function() { ServersModel.processedIndex = index PageController.goToPage(PageEnum.PageSettingsServerInfo) diff --git a/client/ui/qml/Pages2/PageProtocolAwgSettings.qml b/client/ui/qml/Pages2/PageProtocolAwgSettings.qml index a00378c1..ec4aa010 100644 --- a/client/ui/qml/Pages2/PageProtocolAwgSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolAwgSettings.qml @@ -18,8 +18,18 @@ PageType { defaultActiveFocusItem: listview.currentItem.portTextField.textField + Item { + id: focusItem + onFocusChanged: { + if (activeFocus) { + fl.ensureVisible(focusItem) + } + } + KeyNavigation.tab: backButton + } + ColumnLayout { - id: backButton + id: backButtonLayout anchors.top: parent.top anchors.left: parent.left @@ -28,12 +38,14 @@ PageType { anchors.topMargin: 20 BackButtonType { + id: backButton + KeyNavigation.tab: listview.currentItem.portTextField.textField } } FlickableType { id: fl - anchors.top: backButton.bottom + anchors.top: backButtonLayout.bottom anchors.bottom: parent.bottom contentHeight: content.implicitHeight @@ -47,8 +59,6 @@ PageType { enabled: ServersModel.isProcessedServerHasWriteAccess() ListView { - - id: listview width: parent.width @@ -94,6 +104,7 @@ PageType { textFieldText: port textField.maximumLength: 5 textField.validator: IntValidator { bottom: 1; top: 65535 } + parentFlickable: fl textField.onEditingFinished: { if (textFieldText !== port) { @@ -103,7 +114,7 @@ PageType { checkEmptyText: true - KeyNavigation.tab: junkPacketCountTextField.textField + KeyNavigation.tab: mtuTextField.textField } TextFieldWithHeaderType { @@ -124,6 +135,7 @@ PageType { } } checkEmptyText: true + KeyNavigation.tab: junkPacketCountTextField.textField } TextFieldWithHeaderType { @@ -134,6 +146,7 @@ PageType { headerText: "Jc - Junk packet count" textFieldText: junkPacketCount textField.validator: IntValidator { bottom: 0 } + parentFlickable: fl textField.onEditingFinished: { if (textFieldText === "") { @@ -158,6 +171,7 @@ PageType { headerText: "Jmin - Junk packet minimum size" textFieldText: junkPacketMinSize textField.validator: IntValidator { bottom: 0 } + parentFlickable: fl textField.onEditingFinished: { if (textFieldText !== junkPacketMinSize) { @@ -178,6 +192,7 @@ PageType { headerText: "Jmax - Junk packet maximum size" textFieldText: junkPacketMaxSize textField.validator: IntValidator { bottom: 0 } + parentFlickable: fl textField.onEditingFinished: { if (textFieldText !== junkPacketMaxSize) { @@ -198,6 +213,7 @@ PageType { headerText: "S1 - Init packet junk size" textFieldText: initPacketJunkSize textField.validator: IntValidator { bottom: 0 } + parentFlickable: fl textField.onEditingFinished: { if (textFieldText !== initPacketJunkSize) { @@ -218,6 +234,7 @@ PageType { headerText: "S2 - Response packet junk size" textFieldText: responsePacketJunkSize textField.validator: IntValidator { bottom: 0 } + parentFlickable: fl textField.onEditingFinished: { if (textFieldText !== responsePacketJunkSize) { @@ -238,6 +255,7 @@ PageType { headerText: "H1 - Init packet magic header" textFieldText: initPacketMagicHeader textField.validator: IntValidator { bottom: 0 } + parentFlickable: fl textField.onEditingFinished: { if (textFieldText !== initPacketMagicHeader) { @@ -258,6 +276,7 @@ PageType { headerText: "H2 - Response packet magic header" textFieldText: responsePacketMagicHeader textField.validator: IntValidator { bottom: 0 } + parentFlickable: fl textField.onEditingFinished: { if (textFieldText !== responsePacketMagicHeader) { @@ -278,6 +297,7 @@ PageType { headerText: "H4 - Transport packet magic header" textFieldText: transportPacketMagicHeader textField.validator: IntValidator { bottom: 0 } + parentFlickable: fl textField.onEditingFinished: { if (textFieldText !== transportPacketMagicHeader) { @@ -294,6 +314,7 @@ PageType { id: underloadPacketMagicHeaderTextField Layout.fillWidth: true Layout.topMargin: 16 + parentFlickable: fl headerText: "H3 - Underload packet magic header" textFieldText: underloadPacketMagicHeader @@ -312,6 +333,7 @@ PageType { BasicButtonType { id: saveRestartButton + parentFlickable: fl Layout.fillWidth: true Layout.topMargin: 24 @@ -330,7 +352,9 @@ PageType { text: qsTr("Save") - onClicked: { + Keys.onTabPressed: lastItemTabClicked(focusItem) + + clickedFunc: function() { if (AwgConfigModel.isHeadersEqual(underloadPacketMagicHeaderTextField.textField.text, transportPacketMagicHeaderTextField.textField.text, responsePacketMagicHeaderTextField.textField.text, @@ -362,6 +386,9 @@ PageType { InstallController.updateContainer(AwgConfigModel.getConfig()) } var noButtonFunction = function() { + if (!GC.isMobile()) { + saveRestartButton.forceActiveFocus() + } } showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } diff --git a/client/ui/qml/Pages2/PageProtocolCloakSettings.qml b/client/ui/qml/Pages2/PageProtocolCloakSettings.qml index 9b23454c..5ef5771e 100644 --- a/client/ui/qml/Pages2/PageProtocolCloakSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolCloakSettings.qml @@ -17,8 +17,13 @@ PageType { defaultActiveFocusItem: listview.currentItem.trafficFromField.textField + Item { + id: focusItem + KeyNavigation.tab: backButton + } + ColumnLayout { - id: backButton + id: backButtonLayout anchors.top: parent.top anchors.left: parent.left @@ -27,12 +32,14 @@ PageType { anchors.topMargin: 20 BackButtonType { + id: backButton + KeyNavigation.tab: listview.currentItem.trafficFromField.textField } } FlickableType { id: fl - anchors.top: backButton.bottom + anchors.top: backButtonLayout.bottom anchors.bottom: parent.bottom contentHeight: content.implicitHeight @@ -123,7 +130,7 @@ PageType { } } - KeyNavigation.tab: saveRestartButton + KeyNavigation.tab: cipherDropDown } DropDownType { @@ -135,6 +142,7 @@ PageType { headerText: qsTr("Cipher") drawerParent: root + KeyNavigation.tab: saveRestartButton listView: ListViewWithRadioButtonType { id: cipherListView @@ -175,6 +183,7 @@ PageType { Layout.bottomMargin: 24 text: qsTr("Save") + Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunc: function() { forceActiveFocus() diff --git a/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml b/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml index 95e2581f..4779965f 100644 --- a/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolOpenVpnSettings.qml @@ -18,8 +18,18 @@ PageType { defaultActiveFocusItem: listview.currentItem.vpnAddressSubnetTextField.textField + Item { + id: focusItem + KeyNavigation.tab: backButton + onActiveFocusChanged: { + if (activeFocus) { + fl.ensureVisible(focusItem) + } + } + } + ColumnLayout { - id: backButton + id: backButtonLayout anchors.top: parent.top anchors.left: parent.left @@ -28,12 +38,14 @@ PageType { anchors.topMargin: 20 BackButtonType { + id: backButton + KeyNavigation.tab: listview.currentItem.vpnAddressSubnetTextField.textField } } FlickableType { id: fl - anchors.top: backButton.bottom + anchors.top: backButtonLayout.bottom anchors.bottom: parent.bottom contentHeight: content.implicitHeight @@ -90,13 +102,14 @@ PageType { headerText: qsTr("VPN address subnet") textFieldText: subnetAddress + parentFlickable: fl + KeyNavigation.tab: transportProtoSelector + textField.onEditingFinished: { if (textFieldText !== subnetAddress) { subnetAddress = textFieldText } } - - KeyNavigation.tab: portTextField.enabled ? portTextField.textField : saveRestartButton } ParagraphTextType { @@ -107,6 +120,7 @@ PageType { } TransportProtoSelector { + id: transportProtoSelector Layout.fillWidth: true Layout.topMargin: 16 rootWidth: root.width @@ -117,6 +131,8 @@ PageType { return transportProto === "tcp" ? 1 : 0 } + KeyNavigation.tab: portTextField.enabled ? portTextField.textField : autoNegotiateEncryprionSwitcher + onCurrentIndexChanged: { if (transportProto === "tcp" && currentIndex === 0) { transportProto = "udp" @@ -131,6 +147,7 @@ PageType { Layout.fillWidth: true Layout.topMargin: 40 + parentFlickable: fl enabled: isPortEditable @@ -145,7 +162,7 @@ PageType { } } - KeyNavigation.tab: saveRestartButton + KeyNavigation.tab: autoNegotiateEncryprionSwitcher } SwitcherType { @@ -153,6 +170,7 @@ PageType { Layout.fillWidth: true Layout.topMargin: 24 + parentFlickable: fl text: qsTr("Auto-negotiate encryption") checked: autoNegotiateEncryprion @@ -162,6 +180,10 @@ PageType { autoNegotiateEncryprion = checked } } + + KeyNavigation.tab: hashDropDown.enabled ? + hashDropDown : + tlsAuthCheckBox } DropDownType { @@ -175,6 +197,10 @@ PageType { headerText: qsTr("Hash") drawerParent: root + parentFlickable: fl + KeyNavigation.tab: cipherDropDown.enabled ? + cipherDropDown : + tlsAuthCheckBox listView: ListViewWithRadioButtonType { id: hashListView @@ -223,6 +249,9 @@ PageType { headerText: qsTr("Cipher") drawerParent: root + parentFlickable: fl + + KeyNavigation.tab: tlsAuthCheckBox listView: ListViewWithRadioButtonType { id: cipherListView @@ -261,24 +290,40 @@ PageType { } Rectangle { + id: contentRect Layout.fillWidth: true Layout.topMargin: 32 Layout.preferredHeight: checkboxLayout.implicitHeight color: "#1C1D21" radius: 16 + Connections { + target: tlsAuthCheckBox + enabled: !GC.isMobile() + + function onFocusChanged() { + if (tlsAuthCheckBox.activeFocus) { + fl.ensureVisible(contentRect) + } + } + } + ColumnLayout { id: checkboxLayout anchors.fill: parent CheckBoxType { + id: tlsAuthCheckBox Layout.fillWidth: true text: qsTr("TLS auth") checked: tlsAuth + KeyNavigation.tab: blockDnsCheckBox + onCheckedChanged: { if (checked !== tlsAuth) { + console.log("tlsAuth changed to: " + checked) tlsAuth = checked } } @@ -287,11 +332,14 @@ PageType { DividerType {} CheckBoxType { + id: blockDnsCheckBox Layout.fillWidth: true text: qsTr("Block DNS requests outside of VPN") checked: blockDns + KeyNavigation.tab: additionalClientCommandsSwitcher + onCheckedChanged: { if (checked !== blockDns) { blockDns = checked @@ -305,6 +353,10 @@ PageType { id: additionalClientCommandsSwitcher Layout.fillWidth: true Layout.topMargin: 32 + parentFlickable: fl + KeyNavigation.tab: additionalClientCommandsTextArea.visible ? + additionalClientCommandsTextArea.textArea : + additionalServerCommandsSwitcher checked: additionalClientCommands !== "" @@ -318,10 +370,13 @@ PageType { } TextAreaType { + id: additionalClientCommandsTextArea Layout.fillWidth: true Layout.topMargin: 16 visible: additionalClientCommandsSwitcher.checked + KeyNavigation.tab: additionalServerCommandsSwitcher + parentFlickable: fl textAreaText: additionalClientCommands placeholderText: qsTr("Commands:") @@ -337,6 +392,10 @@ PageType { id: additionalServerCommandsSwitcher Layout.fillWidth: true Layout.topMargin: 16 + parentFlickable: fl + KeyNavigation.tab: additionalServerCommandsTextArea.visible ? + additionalServerCommandsTextArea.textArea : + saveRestartButton checked: additionalServerCommands !== "" @@ -350,6 +409,7 @@ PageType { } TextAreaType { + id: additionalServerCommandsTextArea Layout.fillWidth: true Layout.topMargin: 16 @@ -357,7 +417,8 @@ PageType { textAreaText: additionalServerCommands placeholderText: qsTr("Commands:") - + parentFlickable: fl + KeyNavigation.tab: saveRestartButton textArea.onEditingFinished: { if (additionalServerCommands !== textAreaText) { additionalServerCommands = textAreaText @@ -373,6 +434,8 @@ PageType { Layout.bottomMargin: 24 text: qsTr("Save") + parentFlickable: fl + Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunc: function() { forceActiveFocus() diff --git a/client/ui/qml/Pages2/PageProtocolRaw.qml b/client/ui/qml/Pages2/PageProtocolRaw.qml index 0d8da97d..f51035b1 100644 --- a/client/ui/qml/Pages2/PageProtocolRaw.qml +++ b/client/ui/qml/Pages2/PageProtocolRaw.qml @@ -18,6 +18,13 @@ import "../Components" PageType { id: root + defaultActiveFocusItem: focusItem + + Item { + id: focusItem + KeyNavigation.tab: backButton + } + ColumnLayout { id: header @@ -28,6 +35,8 @@ PageType { anchors.topMargin: 20 BackButtonType { + id: backButton + KeyNavigation.tab: listView } HeaderType { @@ -55,16 +64,29 @@ PageType { anchors.topMargin: 32 ListView { + id: listView width: parent.width height: contentItem.height clip: true interactive: false model: ProtocolsModel + activeFocusOnTab: true + focus: true + + onActiveFocusChanged: { + if (focus) { + listView.currentIndex = 0 + listView.currentItem.focusItem.forceActiveFocus() + } + } + delegate: Item { implicitWidth: parent.width implicitHeight: delegateContent.implicitHeight + property alias focusItem: button + ColumnLayout { id: delegateContent @@ -81,6 +103,8 @@ PageType { configContentDrawer.open() } + KeyNavigation.tab: removeButton + MouseArea { anchors.fill: button cursorShape: Qt.PointingHandCursor @@ -95,14 +119,33 @@ PageType { expandedHeight: root.height * 0.9 + onClosed: { + if (!GC.isMobile()) { + defaultActiveFocusItem.forceActiveFocus() + } + } + parent: root anchors.fill: parent expandedContent: Item { implicitHeight: configContentDrawer.expandedHeight + Connections { + target: configContentDrawer + enabled: !GC.isMobile() + function onOpened() { + focusItem1.forceActiveFocus() + } + } + + Item { + id: focusItem1 + KeyNavigation.tab: backButton1 + } + BackButtonType { - id: backButton + id: backButton1 anchors.top: parent.top anchors.left: parent.left @@ -112,10 +155,12 @@ PageType { backButtonFunction: function() { configContentDrawer.close() } + + KeyNavigation.tab: focusItem1 } FlickableType { - anchors.top: backButton.bottom + anchors.top: backButton1.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom @@ -180,6 +225,7 @@ PageType { text: qsTr("Remove ") + ContainersModel.getProcessedContainerName() textColor: "#EB5757" + Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunction: function() { var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName()) var descriptionText = qsTr("All users with whom you shared a connection with will no longer be able to connect to it.") @@ -191,6 +237,9 @@ PageType { InstallController.removeProcessedContainer() } var noButtonFunction = function() { + if (!GC.isMobile()) { + focusItem.forceActiveFocus() + } } showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) diff --git a/client/ui/qml/Pages2/PageProtocolShadowSocksSettings.qml b/client/ui/qml/Pages2/PageProtocolShadowSocksSettings.qml index b2a1379b..506d2f0e 100644 --- a/client/ui/qml/Pages2/PageProtocolShadowSocksSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolShadowSocksSettings.qml @@ -15,10 +15,17 @@ import "../Components" PageType { id: root - defaultActiveFocusItem: listview.currentItem.portTextField.textField + defaultActiveFocusItem: listview.currentItem.focusItemId.enabled ? + listview.currentItem.focusItemId.textField : + focusItem + + Item { + id: focusItem + KeyNavigation.tab: backButton + } ColumnLayout { - id: backButton + id: backButtonLayout anchors.top: parent.top anchors.left: parent.left @@ -27,12 +34,16 @@ PageType { anchors.topMargin: 20 BackButtonType { + id: backButton + KeyNavigation.tab: listview.currentItem.focusItemId.enabled ? + listview.currentItem.focusItemId.textField : + focusItem } } FlickableType { id: fl - anchors.top: backButton.bottom + anchors.top: backButtonLayout.bottom anchors.bottom: parent.bottom contentHeight: content.implicitHeight @@ -60,7 +71,11 @@ PageType { implicitWidth: listview.width implicitHeight: col.implicitHeight - property alias portTextField: portTextField + property var focusItemId: portTextField.enabled ? + portTextField : + cipherDropDown.enabled ? + cipherDropDown : + saveRestartButton ColumnLayout { id: col @@ -99,7 +114,7 @@ PageType { } } - KeyNavigation.tab: saveRestartButton + KeyNavigation.tab: cipherDropDown } DropDownType { @@ -113,6 +128,7 @@ PageType { headerText: qsTr("Cipher") drawerParent: root + KeyNavigation.tab: saveRestartButton listView: ListViewWithRadioButtonType { id: cipherListView @@ -155,6 +171,7 @@ PageType { enabled: isPortEditable | isCipherEditable text: qsTr("Save") + Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunc: function() { forceActiveFocus() diff --git a/client/ui/qml/Pages2/PageProtocolWireGuardSettings.qml b/client/ui/qml/Pages2/PageProtocolWireGuardSettings.qml index 8c3f1865..4e6a851e 100644 --- a/client/ui/qml/Pages2/PageProtocolWireGuardSettings.qml +++ b/client/ui/qml/Pages2/PageProtocolWireGuardSettings.qml @@ -15,8 +15,15 @@ import "../Components" PageType { id: root + defaultActiveFocusItem: listview + + Item { + id: focusItem + KeyNavigation.tab: backButton + } + ColumnLayout { - id: backButton + id: backButtonLayout anchors.top: parent.top anchors.left: parent.left @@ -25,12 +32,14 @@ PageType { anchors.topMargin: 20 BackButtonType { + id: backButton + KeyNavigation.tab: listview } } FlickableType { id: fl - anchors.top: backButton.bottom + anchors.top: backButtonLayout.bottom anchors.bottom: parent.bottom contentHeight: content.implicitHeight @@ -54,7 +63,16 @@ PageType { model: WireGuardConfigModel + activeFocusOnTab: true + onActiveFocusChanged: { + if (activeFocus) { + listview.itemAtIndex(0)?.focusItemId.forceActiveFocus() + } + } + delegate: Item { + property alias focusItemId: portTextField.textField + implicitWidth: listview.width implicitHeight: col.implicitHeight @@ -85,6 +103,8 @@ PageType { textField.maximumLength: 5 textField.validator: IntValidator { bottom: 1; top: 65535 } + KeyNavigation.tab: mtuTextField.textField + textField.onEditingFinished: { if (textFieldText !== port) { port = textFieldText @@ -103,6 +123,8 @@ PageType { textFieldText: mtu textField.validator: IntValidator { bottom: 576; top: 65535 } + KeyNavigation.tab: saveButton + textField.onEditingFinished: { if (textFieldText === "") { textFieldText = "0" @@ -115,6 +137,7 @@ PageType { } BasicButtonType { + id: saveButton Layout.fillWidth: true Layout.topMargin: 24 Layout.bottomMargin: 24 @@ -124,6 +147,8 @@ PageType { text: qsTr("Save") + Keys.onTabPressed: lastItemTabClicked(focusItem) + onClicked: { forceActiveFocus() @@ -134,7 +159,11 @@ PageType { PageController.goToPage(PageEnum.PageSetupWizardInstalling); InstallController.updateContainer(WireGuardConfigModel.getConfig()) + focusItem.forceActiveFocus() } + + Keys.onEnterPressed: saveButton.clicked() + Keys.onReturnPressed: saveButton.clicked() } } } diff --git a/client/ui/qml/Pages2/PageProtocolXraySettings.qml b/client/ui/qml/Pages2/PageProtocolXraySettings.qml index a2d2d3f4..6a8094d7 100644 --- a/client/ui/qml/Pages2/PageProtocolXraySettings.qml +++ b/client/ui/qml/Pages2/PageProtocolXraySettings.qml @@ -16,8 +16,15 @@ import "../Components" PageType { id: root + defaultActiveFocusItem: listview + + Item { + id: focusItem + KeyNavigation.tab: backButton + } + ColumnLayout { - id: backButton + id: backButtonLayout anchors.top: parent.top anchors.left: parent.left @@ -26,12 +33,14 @@ PageType { anchors.topMargin: 20 BackButtonType { + id: backButton + KeyNavigation.tab: listview } } FlickableType { id: fl - anchors.top: backButton.bottom + anchors.top: backButtonLayout.bottom anchors.bottom: parent.bottom contentHeight: content.implicitHeight @@ -55,7 +64,16 @@ PageType { model: XrayConfigModel + activeFocusOnTab: true + onActiveFocusChanged: { + if (activeFocus) { + listview.itemAtIndex(0)?.focusItemId.forceActiveFocus() + } + } + delegate: Item { + property alias focusItemId: textFieldWithHeaderType.textField + implicitWidth: listview.width implicitHeight: col.implicitHeight @@ -77,12 +95,15 @@ PageType { } TextFieldWithHeaderType { + id: textFieldWithHeaderType Layout.fillWidth: true Layout.topMargin: 32 headerText: qsTr("Disguised as traffic from") textFieldText: site + KeyNavigation.tab: basicButton + textField.onEditingFinished: { if (textFieldText !== site) { var tmpText = textFieldText @@ -99,12 +120,15 @@ PageType { } BasicButtonType { + id: basicButton Layout.fillWidth: true Layout.topMargin: 24 Layout.bottomMargin: 24 text: qsTr("Save") + Keys.onTabPressed: lastItemTabClicked(focusItem) + onClicked: { forceActiveFocus() @@ -115,7 +139,11 @@ PageType { PageController.goToPage(PageEnum.PageSetupWizardInstalling); InstallController.updateContainer(XrayConfigModel.getConfig()) + focusItem.forceActiveFocus() } + + Keys.onEnterPressed: basicButton.clicked() + Keys.onReturnPressed: basicButton.clicked() } } } diff --git a/client/ui/qml/Pages2/PageServiceDnsSettings.qml b/client/ui/qml/Pages2/PageServiceDnsSettings.qml index 457c16b4..34d8b786 100644 --- a/client/ui/qml/Pages2/PageServiceDnsSettings.qml +++ b/client/ui/qml/Pages2/PageServiceDnsSettings.qml @@ -15,8 +15,15 @@ import "../Components" PageType { id: root + defaultActiveFocusItem: focusItem + + Item { + id: focusItem + KeyNavigation.tab: backButton + } + ColumnLayout { - id: backButton + id: backButtonLayout anchors.top: parent.top anchors.left: parent.left @@ -25,12 +32,14 @@ PageType { anchors.topMargin: 20 BackButtonType { + id: backButton + KeyNavigation.tab: removeButton } } FlickableType { id: fl - anchors.top: backButton.bottom + anchors.top: backButtonLayout.bottom anchors.bottom: parent.bottom contentHeight: content.implicitHeight @@ -62,6 +71,8 @@ PageType { text: qsTr("Remove ") + ContainersModel.getProcessedContainerName() textColor: "#EB5757" + Keys.onTabPressed: root.lastItemTabClicked() + clickedFunction: function() { var headerText = qsTr("Remove %1 from server?").arg(ContainersModel.getProcessedContainerName()) var yesButtonText = qsTr("Continue") @@ -78,6 +89,9 @@ PageType { } } var noButtonFunction = function() { + if (!GC.isMobile()) { + removeButton.rightButton.forceActiveFocus() + } } showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) diff --git a/client/ui/qml/Pages2/PageServiceSftpSettings.qml b/client/ui/qml/Pages2/PageServiceSftpSettings.qml index 7b8feb3c..48843e3b 100644 --- a/client/ui/qml/Pages2/PageServiceSftpSettings.qml +++ b/client/ui/qml/Pages2/PageServiceSftpSettings.qml @@ -15,6 +15,8 @@ import "../Components" PageType { id: root + defaultActiveFocusItem: focusItem + Connections { target: InstallController @@ -23,8 +25,13 @@ PageType { } } + Item { + id: focusItem + KeyNavigation.tab: backButton + } + ColumnLayout { - id: backButton + id: backButtonLayout anchors.top: parent.top anchors.left: parent.left @@ -33,12 +40,14 @@ PageType { anchors.topMargin: 20 BackButtonType { + id: backButton + KeyNavigation.tab: listview } } FlickableType { id: fl - anchors.top: backButton.bottom + anchors.top: backButtonLayout.bottom anchors.bottom: parent.bottom contentHeight: content.implicitHeight @@ -62,10 +71,18 @@ PageType { model: SftpConfigModel + onFocusChanged: { + if (focus) { + listview.currentItem.focusItem.forceActiveFocus() + } + } + delegate: Item { implicitWidth: listview.width implicitHeight: col.implicitHeight + property alias focusItem: hostLabel.rightButton + ColumnLayout { id: col @@ -84,9 +101,13 @@ PageType { } LabelWithButtonType { + id: hostLabel Layout.fillWidth: true Layout.topMargin: 32 + parentFlickable: fl + KeyNavigation.tab: portLabel.rightButton + text: qsTr("Host") descriptionText: ServersModel.getProcessedServerData("hostName") @@ -98,10 +119,14 @@ PageType { clickedFunction: function() { GC.copyToClipBoard(descriptionText) PageController.showNotificationMessage(qsTr("Copied")) + if (!GC.isMobile()) { + this.rightButton.forceActiveFocus() + } } } LabelWithButtonType { + id: portLabel Layout.fillWidth: true text: qsTr("Port") @@ -109,16 +134,23 @@ PageType { descriptionOnTop: true + parentFlickable: fl + KeyNavigation.tab: usernameLabel.rightButton + rightImageSource: "qrc:/images/controls/copy.svg" rightImageColor: "#D7D8DB" clickedFunction: function() { GC.copyToClipBoard(descriptionText) PageController.showNotificationMessage(qsTr("Copied")) + if (!GC.isMobile()) { + this.rightButton.forceActiveFocus() + } } } LabelWithButtonType { + id: usernameLabel Layout.fillWidth: true text: qsTr("User name") @@ -126,16 +158,23 @@ PageType { descriptionOnTop: true + parentFlickable: fl + KeyNavigation.tab: passwordLabel.rightButton + rightImageSource: "qrc:/images/controls/copy.svg" rightImageColor: "#D7D8DB" clickedFunction: function() { GC.copyToClipBoard(descriptionText) PageController.showNotificationMessage(qsTr("Copied")) + if (!GC.isMobile()) { + this.rightButton.forceActiveFocus() + } } } LabelWithButtonType { + id: passwordLabel Layout.fillWidth: true text: qsTr("Password") @@ -143,16 +182,29 @@ PageType { descriptionOnTop: true + parentFlickable: fl + Keys.onTabPressed: { + if (mountButton.visible) { + mountButton.forceActiveFocus() + } else { + detailedInstructionsButton.forceActiveFocus() + } + } + rightImageSource: "qrc:/images/controls/copy.svg" rightImageColor: "#D7D8DB" clickedFunction: function() { GC.copyToClipBoard(descriptionText) PageController.showNotificationMessage(qsTr("Copied")) + if (!GC.isMobile()) { + this.rightButton.forceActiveFocus() + } } } BasicButtonType { + id: mountButton visible: !GC.isMobile() Layout.fillWidth: true @@ -168,13 +220,16 @@ PageType { textColor: "#D7D8DB" borderWidth: 1 + parentFlickable: fl + KeyNavigation.tab: detailedInstructionsButton + text: qsTr("Mount folder on device") clickedFunc: function() { PageController.showBusyIndicator(true) InstallController.mountSftpDrive(port, password, username) PageController.showBusyIndicator(false) - } + } } ParagraphTextType { @@ -216,6 +271,7 @@ PageType { } BasicButtonType { + id: detailedInstructionsButton Layout.topMargin: 16 Layout.bottomMargin: 16 Layout.leftMargin: 8 @@ -229,12 +285,16 @@ PageType { text: qsTr("Detailed instructions") + parentFlickable: fl + KeyNavigation.tab: removeButton + clickedFunc: function() { // Qt.openUrlExternally("https://github.com/amnezia-vpn/desktop-client/releases/latest") } } BasicButtonType { + id: removeButton Layout.topMargin: 24 Layout.bottomMargin: 16 Layout.leftMargin: 8 @@ -245,6 +305,9 @@ PageType { pressedColor: Qt.rgba(1, 1, 1, 0.12) textColor: "#EB5757" + parentFlickable: fl + Keys.onTabPressed: lastItemTabClicked() + text: qsTr("Remove SFTP and all data stored there") clickedFunc: function() { @@ -257,6 +320,9 @@ PageType { InstallController.removeProcessedContainer() } var noButtonFunction = function() { + if (!GC.isMobile()) { + removeButton.forceActiveFocus() + } } showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) diff --git a/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml b/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml index 9a4871fd..df8db486 100644 --- a/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml +++ b/client/ui/qml/Pages2/PageServiceTorWebsiteSettings.qml @@ -16,6 +16,8 @@ import "../Components" PageType { id: root + defaultActiveFocusItem: focusItem + Connections { target: InstallController @@ -24,8 +26,13 @@ PageType { } } + Item { + id: focusItem + KeyNavigation.tab: backButton + } + ColumnLayout { - id: backButton + id: backButtonLayout anchors.top: parent.top anchors.left: parent.left @@ -34,12 +41,14 @@ PageType { anchors.topMargin: 20 BackButtonType { + id: backButton + KeyNavigation.tab: websiteName.rightButton } } FlickableType { id: fl - anchors.top: backButton.bottom + anchors.top: backButtonLayout.bottom anchors.bottom: parent.bottom contentHeight: content.implicitHeight @@ -61,6 +70,7 @@ PageType { } LabelWithButtonType { + id: websiteName Layout.fillWidth: true Layout.topMargin: 32 @@ -77,9 +87,14 @@ PageType { rightImageSource: "qrc:/images/controls/copy.svg" rightImageColor: "#D7D8DB" + KeyNavigation.tab: removeButton + clickedFunction: function() { GC.copyToClipBoard(descriptionText) PageController.showNotificationMessage(qsTr("Copied")) + if (!GC.isMobile()) { + this.rightButton.forceActiveFocus() + } } } @@ -113,6 +128,7 @@ PageType { } BasicButtonType { + id: removeButton Layout.topMargin: 24 Layout.bottomMargin: 16 Layout.leftMargin: 8 @@ -125,6 +141,8 @@ PageType { text: qsTr("Remove website") + Keys.onTabPressed: lastItemTabClicked(focusItem) + clickedFunc: function() { var headerText = qsTr("The site with all data will be removed from the tor network.") var yesButtonText = qsTr("Continue") @@ -135,6 +153,9 @@ PageType { InstallController.removeProcessedContainer() } var noButtonFunction = function() { + if (!GC.isMobile()) { + removeButton.forceActiveFocus() + } } showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) diff --git a/client/ui/qml/Pages2/PageSettings.qml b/client/ui/qml/Pages2/PageSettings.qml index 92575dda..f8056c63 100644 --- a/client/ui/qml/Pages2/PageSettings.qml +++ b/client/ui/qml/Pages2/PageSettings.qml @@ -13,6 +13,8 @@ import "../Config" PageType { id: root + defaultActiveFocusItem: header + FlickableType { id: fl anchors.top: parent.top @@ -29,15 +31,19 @@ PageType { spacing: 0 HeaderType { + id: header Layout.fillWidth: true Layout.topMargin: 24 Layout.rightMargin: 16 Layout.leftMargin: 16 headerText: qsTr("Settings") + + KeyNavigation.tab: account.rightButton } LabelWithButtonType { + id: account Layout.fillWidth: true Layout.topMargin: 16 @@ -48,11 +54,14 @@ PageType { clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsServersList) } + + KeyNavigation.tab: connection.rightButton } DividerType {} LabelWithButtonType { + id: connection Layout.fillWidth: true text: qsTr("Connection") @@ -62,11 +71,14 @@ PageType { clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsConnection) } + + KeyNavigation.tab: application.rightButton } DividerType {} LabelWithButtonType { + id: application Layout.fillWidth: true text: qsTr("Application") @@ -76,11 +88,14 @@ PageType { clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsApplication) } + + KeyNavigation.tab: backup.rightButton } DividerType {} LabelWithButtonType { + id: backup Layout.fillWidth: true text: qsTr("Backup") @@ -90,6 +105,8 @@ PageType { clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsBackup) } + + KeyNavigation.tab: about.rightButton } DividerType {} @@ -105,18 +122,23 @@ PageType { clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsAbout) } + KeyNavigation.tab: close + } DividerType {} LabelWithButtonType { + id: close visible: GC.isDesktop() Layout.fillWidth: true Layout.preferredHeight: about.height text: qsTr("Close application") leftImageSource: "qrc:/images/controls/x-circle.svg" - isLeftImageHoverEnabled: false + isLeftImageHoverEnabled: false + + Keys.onTabPressed: lastItemTabClicked(header) clickedFunction: function() { PageController.closeApplication() diff --git a/client/ui/qml/Pages2/PageSettingsAbout.qml b/client/ui/qml/Pages2/PageSettingsAbout.qml index 1f7e9e11..d956c173 100644 --- a/client/ui/qml/Pages2/PageSettingsAbout.qml +++ b/client/ui/qml/Pages2/PageSettingsAbout.qml @@ -13,6 +13,19 @@ import "../Components" PageType { id: root + defaultActiveFocusItem: focusItem + + Item { + id: focusItem + KeyNavigation.tab: backButton + + onFocusChanged: { + if (focusItem.activeFocus) { + fl.contentY = 0 + } + } + } + BackButtonType { id: backButton @@ -20,6 +33,8 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 + + KeyNavigation.tab: telegramButton } FlickableType { @@ -82,6 +97,7 @@ PageType { } LabelWithButtonType { + id: telegramButton Layout.fillWidth: true Layout.topMargin: 16 @@ -89,6 +105,9 @@ PageType { descriptionText: qsTr("To discuss features") leftImageSource: "qrc:/images/controls/telegram.svg" + KeyNavigation.tab: mailButton + parentFlickable: fl + clickedFunction: function() { Qt.openUrlExternally(qsTr("https://t.me/amnezia_vpn_en")) } @@ -97,40 +116,55 @@ PageType { DividerType {} LabelWithButtonType { + id: mailButton Layout.fillWidth: true text: qsTr("Mail") descriptionText: qsTr("For reviews and bug reports") leftImageSource: "qrc:/images/controls/mail.svg" + KeyNavigation.tab: githubButton + parentFlickable: fl + clickedFunction: function() { } + } DividerType {} LabelWithButtonType { + id: githubButton Layout.fillWidth: true text: qsTr("Github") leftImageSource: "qrc:/images/controls/github.svg" + KeyNavigation.tab: websiteButton + parentFlickable: fl + clickedFunction: function() { Qt.openUrlExternally(qsTr("https://github.com/amnezia-vpn/amnezia-client")) } + } DividerType {} LabelWithButtonType { + id: websiteButton Layout.fillWidth: true text: qsTr("Website") leftImageSource: "qrc:/images/controls/amnezia.svg" + KeyNavigation.tab: checkUpdatesButton + parentFlickable: fl + clickedFunction: function() { Qt.openUrlExternally(qsTr("https://amnezia.org")) } + } DividerType {} @@ -146,6 +180,7 @@ PageType { } BasicButtonType { + id: checkUpdatesButton Layout.alignment: Qt.AlignHCenter Layout.topMargin: 8 Layout.bottomMargin: 16 @@ -159,12 +194,16 @@ PageType { text: qsTr("Check for updates") + KeyNavigation.tab: privacyPolicyButton + parentFlickable: fl + clickedFunc: function() { Qt.openUrlExternally("https://github.com/amnezia-vpn/desktop-client/releases/latest") } } BasicButtonType { + id: privacyPolicyButton Layout.alignment: Qt.AlignHCenter Layout.bottomMargin: 16 Layout.topMargin: -15 @@ -178,6 +217,9 @@ PageType { text: qsTr("Privacy Policy") + Keys.onTabPressed: lastItemTabClicked() + parentFlickable: fl + clickedFunc: function() { Qt.openUrlExternally("https://amnezia.org/en/policy") } diff --git a/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml b/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml index b67bc6bb..71f36957 100644 --- a/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml +++ b/client/ui/qml/Pages2/PageSettingsAppSplitTunneling.qml @@ -20,6 +20,8 @@ import "../Components" PageType { id: root + defaultActiveFocusItem: focusItem + property bool pageEnabled Component.onCompleted: { @@ -63,6 +65,11 @@ PageType { } } + Item { + id: focusItem + KeyNavigation.tab: backButton + } + ColumnLayout { id: header @@ -73,6 +80,8 @@ PageType { anchors.topMargin: 20 BackButtonType { + id: backButton + KeyNavigation.tab: switcher } RowLayout { @@ -93,6 +102,10 @@ PageType { enabled: root.pageEnabled + KeyNavigation.tab: selector.enabled ? + selector : + searchField.textField + checked: AppSplitTunnelingModel.isTunnelingEnabled onToggled: { AppSplitTunnelingModel.toggleSplitTunneling(checked) @@ -116,6 +129,8 @@ PageType { enabled: Qt.platform.os === "android" && root.pageEnabled + KeyNavigation.tab: searchField.textField + listView: ListViewWithRadioButtonType { rootWidth: root.width @@ -251,6 +266,9 @@ PageType { textFieldPlaceholderText: qsTr("application name") buttonImageSource: "qrc:/images/controls/plus.svg" + Keys.onTabPressed: lastItemTabClicked(focusItem) + rightButtonClickedOnEnter: true + clickedFunc: function() { searchField.focus = false PageController.showBusyIndicator(true) diff --git a/client/ui/qml/Pages2/PageSettingsApplication.qml b/client/ui/qml/Pages2/PageSettingsApplication.qml index f02224c6..a6472656 100644 --- a/client/ui/qml/Pages2/PageSettingsApplication.qml +++ b/client/ui/qml/Pages2/PageSettingsApplication.qml @@ -13,6 +13,53 @@ import "../Components" PageType { id: root + defaultActiveFocusItem: focusItem + + function getNextComponentInFocusChain(componentId) { + const componentsList = [focusItem, + backButton, + switcher, + switcherAutoStart, + switcherAutoConnect, + switcherStartMinimized, + labelWithButtonLanguage, + labelWithButtonLogging, + labelWithButtonReset, + ] + + const idx = componentsList.indexOf(componentId) + + if (idx === -1) { + return null + } + + let nextIndex = idx + 1 + if (nextIndex >= componentsList.length) { + nextIndex = 0 + } + + if (componentsList[nextIndex].visible) { + if ((nextIndex) >= 6) { + return componentsList[nextIndex].rightButton + } else { + return componentsList[nextIndex] + } + } else { + return getNextComponentInFocusChain(componentsList[nextIndex]) + } + } + + Item { + id: focusItem + KeyNavigation.tab: root.getNextComponentInFocusChain(focusItem) + + onFocusChanged: { + if (focusItem.activeFocus) { + fl.contentY = 0 + } + } + } + BackButtonType { id: backButton @@ -20,6 +67,8 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 + + KeyNavigation.tab: root.getNextComponentInFocusChain(backButton) } FlickableType { @@ -44,6 +93,7 @@ PageType { } SwitcherType { + id: switcher visible: GC.isMobile() Layout.fillWidth: true @@ -57,6 +107,9 @@ PageType { SettingsController.toggleScreenshotsEnabled(checked) } } + + KeyNavigation.tab: root.getNextComponentInFocusChain(switcher) + parentFlickable: fl } DividerType { @@ -64,6 +117,7 @@ PageType { } SwitcherType { + id: switcherAutoStart visible: !GC.isMobile() Layout.fillWidth: true @@ -72,6 +126,9 @@ PageType { text: qsTr("Auto start") descriptionText: qsTr("Launch the application every time the device is starts") + KeyNavigation.tab: root.getNextComponentInFocusChain(switcherAutoStart) + parentFlickable: fl + checked: SettingsController.isAutoStartEnabled() onCheckedChanged: { if (checked !== SettingsController.isAutoStartEnabled()) { @@ -85,6 +142,7 @@ PageType { } SwitcherType { + id: switcherAutoConnect visible: !GC.isMobile() Layout.fillWidth: true @@ -93,6 +151,9 @@ PageType { text: qsTr("Auto connect") descriptionText: qsTr("Connect to VPN on app start") + KeyNavigation.tab: root.getNextComponentInFocusChain(switcherAutoConnect) + parentFlickable: fl + checked: SettingsController.isAutoConnectEnabled() onCheckedChanged: { if (checked !== SettingsController.isAutoConnectEnabled()) { @@ -106,6 +167,7 @@ PageType { } SwitcherType { + id: switcherStartMinimized visible: !GC.isMobile() Layout.fillWidth: true @@ -114,6 +176,9 @@ PageType { text: qsTr("Start minimized") descriptionText: qsTr("Launch application minimized") + KeyNavigation.tab: root.getNextComponentInFocusChain(switcherStartMinimized) + parentFlickable: fl + checked: SettingsController.isStartMinimizedEnabled() onCheckedChanged: { if (checked !== SettingsController.isStartMinimizedEnabled()) { @@ -127,12 +192,16 @@ PageType { } LabelWithButtonType { + id: labelWithButtonLanguage Layout.fillWidth: true text: qsTr("Language") descriptionText: LanguageModel.currentLanguageName rightImageSource: "qrc:/images/controls/chevron-right.svg" + KeyNavigation.tab: root.getNextComponentInFocusChain(labelWithButtonLanguage) + parentFlickable: fl + clickedFunction: function() { selectLanguageDrawer.open() } @@ -142,12 +211,16 @@ PageType { DividerType {} LabelWithButtonType { + id: labelWithButtonLogging Layout.fillWidth: true text: qsTr("Logging") descriptionText: SettingsController.isLoggingEnabled ? qsTr("Enabled") : qsTr("Disabled") rightImageSource: "qrc:/images/controls/chevron-right.svg" + KeyNavigation.tab: root.getNextComponentInFocusChain(labelWithButtonLogging) + parentFlickable: fl + clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsLogging) } @@ -156,12 +229,16 @@ PageType { DividerType {} LabelWithButtonType { + id: labelWithButtonReset Layout.fillWidth: true text: qsTr("Reset settings and remove all data from the application") rightImageSource: "qrc:/images/controls/chevron-right.svg" textColor: "#EB5757" + Keys.onTabPressed: lastItemTabClicked() + parentFlickable: fl + clickedFunction: function() { var headerText = qsTr("Reset settings and remove all data from the application?") var descriptionText = qsTr("All settings will be reset to default. All installed AmneziaVPN services will still remain on the server.") @@ -176,8 +253,15 @@ PageType { SettingsController.clearSettings() PageController.replaceStartPage() } + + if (!GC.isMobile()) { + root.defaultActiveFocusItem.forceActiveFocus() + } } var noButtonFunction = function() { + if (!GC.isMobile()) { + root.defaultActiveFocusItem.forceActiveFocus() + } } showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) @@ -193,5 +277,11 @@ PageType { width: root.width height: root.height + + onClosed: { + if (!GC.isMobile()) { + focusItem.forceActiveFocus() + } + } } } diff --git a/client/ui/qml/Pages2/PageSettingsBackup.qml b/client/ui/qml/Pages2/PageSettingsBackup.qml index 78969564..2a696a77 100644 --- a/client/ui/qml/Pages2/PageSettingsBackup.qml +++ b/client/ui/qml/Pages2/PageSettingsBackup.qml @@ -16,6 +16,8 @@ import "../Controls2/TextTypes" PageType { id: root + defaultActiveFocusItem: focusItem + Connections { target: SettingsController @@ -34,6 +36,11 @@ PageType { } } + Item { + id: focusItem + KeyNavigation.tab: backButton + } + BackButtonType { id: backButton @@ -41,6 +48,8 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 + + KeyNavigation.tab: makeBackupButton } FlickableType { @@ -102,9 +111,12 @@ PageType { PageController.showNotificationMessage(qsTr("Backup file saved")) } } + + KeyNavigation.tab: restoreBackupButton } BasicButtonType { + id: restoreBackupButton Layout.fillWidth: true Layout.topMargin: -8 @@ -124,6 +136,8 @@ PageType { restoreBackup(filePath) } } + + Keys.onTabPressed: lastItemTabClicked() } } } diff --git a/client/ui/qml/Pages2/PageSettingsConnection.qml b/client/ui/qml/Pages2/PageSettingsConnection.qml index 9ec49231..c732e6f2 100644 --- a/client/ui/qml/Pages2/PageSettingsConnection.qml +++ b/client/ui/qml/Pages2/PageSettingsConnection.qml @@ -11,8 +11,15 @@ import "../Config" PageType { id: root + defaultActiveFocusItem: focusItem + property bool isAppSplitTinnelingEnabled: Qt.platform.os === "windows" || Qt.platform.os === "android" + Item { + id: focusItem + KeyNavigation.tab: backButton + } + BackButtonType { id: backButton @@ -20,6 +27,8 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 + + KeyNavigation.tab: amneziaDnsSwitch } FlickableType { @@ -44,6 +53,7 @@ PageType { } SwitcherType { + id: amneziaDnsSwitch Layout.fillWidth: true Layout.margins: 16 @@ -56,11 +66,14 @@ PageType { SettingsController.toggleAmneziaDns(checked) } } + + KeyNavigation.tab: dnsServersButton.rightButton } DividerType {} LabelWithButtonType { + id: dnsServersButton Layout.fillWidth: true text: qsTr("DNS servers") @@ -70,11 +83,14 @@ PageType { clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsDns) } + + KeyNavigation.tab: splitTunnelingButton.rightButton } DividerType {} LabelWithButtonType { + id: splitTunnelingButton Layout.fillWidth: true text: qsTr("Site-based split tunneling") @@ -84,6 +100,10 @@ PageType { clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsSplitTunneling) } + + Keys.onTabPressed: splitTunnelingButton2.visible ? + splitTunnelingButton2.forceActiveFocus() : + lastItemTabClicked() } DividerType { @@ -91,6 +111,7 @@ PageType { } LabelWithButtonType { + id: splitTunnelingButton2 visible: root.isAppSplitTinnelingEnabled Layout.fillWidth: true @@ -102,6 +123,8 @@ PageType { clickedFunction: function() { PageController.goToPage(PageEnum.PageSettingsAppSplitTunneling) } + + Keys.onTabPressed: lastItemTabClicked() } DividerType { diff --git a/client/ui/qml/Pages2/PageSettingsDns.qml b/client/ui/qml/Pages2/PageSettingsDns.qml index be2bd290..967e91bf 100644 --- a/client/ui/qml/Pages2/PageSettingsDns.qml +++ b/client/ui/qml/Pages2/PageSettingsDns.qml @@ -15,6 +15,11 @@ PageType { defaultActiveFocusItem: primaryDns.textField + Item { + id: focusItem + KeyNavigation.tab: backButton + } + BackButtonType { id: backButton @@ -22,6 +27,8 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 + + KeyNavigation.tab: root.defaultActiveFocusItem } FlickableType { @@ -87,10 +94,11 @@ PageType { regularExpression: InstallController.ipAddressRegExp() } - KeyNavigation.tab: saveButton + KeyNavigation.tab: restoreDefaultButton } BasicButtonType { + id: restoreDefaultButton Layout.fillWidth: true defaultColor: "transparent" @@ -113,12 +121,21 @@ PageType { SettingsController.secondaryDns = "1.0.0.1" secondaryDns.textFieldText = SettingsController.secondaryDns PageController.showNotificationMessage(qsTr("Settings have been reset")) + + if (!GC.isMobile()) { + defaultActiveFocusItem.forceActiveFocus() + } } var noButtonFunction = function() { + if (!GC.isMobile()) { + defaultActiveFocusItem.forceActiveFocus() + } } showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) } + + KeyNavigation.tab: saveButton } BasicButtonType { @@ -137,6 +154,8 @@ PageType { } PageController.showNotificationMessage(qsTr("Settings saved")) } + + Keys.onTabPressed: lastItemTabClicked(focusItem) } } } diff --git a/client/ui/qml/Pages2/PageSettingsLogging.qml b/client/ui/qml/Pages2/PageSettingsLogging.qml index cde8c8cb..9cf4edbf 100644 --- a/client/ui/qml/Pages2/PageSettingsLogging.qml +++ b/client/ui/qml/Pages2/PageSettingsLogging.qml @@ -27,6 +27,13 @@ disabled after 14 days, and all log files will be deleted.") } } + defaultActiveFocusItem: focusItem + + Item { + id: focusItem + KeyNavigation.tab: backButton + } + BackButtonType { id: backButton @@ -34,6 +41,8 @@ disabled after 14 days, and all log files will be deleted.") anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 + + KeyNavigation.tab: switcher } FlickableType { @@ -62,12 +71,14 @@ disabled after 14 days, and all log files will be deleted.") } SwitcherType { + id: switcher Layout.fillWidth: true Layout.topMargin: 16 text: qsTr("Save logs") checked: SettingsController.isLoggingEnabled + KeyNavigation.tab: openFolderButton onCheckedChanged: { if (checked !== SettingsController.isLoggingEnabled) { SettingsController.isLoggingEnabled = checked @@ -84,14 +95,18 @@ disabled after 14 days, and all log files will be deleted.") visible: !GC.isMobile() ImageButtonType { + id: openFolderButton Layout.alignment: Qt.AlignHCenter implicitWidth: 56 implicitHeight: 56 image: "qrc:/images/controls/folder-open.svg" + KeyNavigation.tab: saveButton onClicked: SettingsController.openLogsFolder() + Keys.onReturnPressed: openFolderButton.clicked() + Keys.onEnterPressed: openFolderButton.clicked() } CaptionTextType { @@ -108,13 +123,17 @@ disabled after 14 days, and all log files will be deleted.") Layout.preferredWidth: root.width / ( GC.isMobile() ? 2 : 3 ) ImageButtonType { + id: saveButton Layout.alignment: Qt.AlignHCenter implicitWidth: 56 implicitHeight: 56 image: "qrc:/images/controls/save.svg" + KeyNavigation.tab: clearButton + Keys.onReturnPressed: saveButton.clicked() + Keys.onEnterPressed: saveButton.clicked() onClicked: { var fileName = "" if (GC.isMobile()) { @@ -149,13 +168,17 @@ disabled after 14 days, and all log files will be deleted.") Layout.preferredWidth: root.width / ( GC.isMobile() ? 2 : 3 ) ImageButtonType { + id: clearButton Layout.alignment: Qt.AlignHCenter implicitWidth: 56 implicitHeight: 56 image: "qrc:/images/controls/delete.svg" + Keys.onTabPressed: lastItemTabClicked(focusItem) + Keys.onReturnPressed: clearButton.clicked() + Keys.onEnterPressed: clearButton.clicked() onClicked: function() { var headerText = qsTr("Clear logs?") var yesButtonText = qsTr("Continue") @@ -166,8 +189,14 @@ disabled after 14 days, and all log files will be deleted.") SettingsController.clearLogs() PageController.showBusyIndicator(false) PageController.showNotificationMessage(qsTr("Logs have been cleaned up")) + if (!GC.isMobile()) { + focusItem.forceActiveFocus() + } } var noButtonFunction = function() { + if (!GC.isMobile()) { + focusItem.forceActiveFocus() + } } showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) diff --git a/client/ui/qml/Pages2/PageSettingsServerData.qml b/client/ui/qml/Pages2/PageSettingsServerData.qml index 9552d6bb..ec41fb9f 100644 --- a/client/ui/qml/Pages2/PageSettingsServerData.qml +++ b/client/ui/qml/Pages2/PageSettingsServerData.qml @@ -10,10 +10,17 @@ import ProtocolEnum 1.0 import "../Controls2" import "../Controls2/TextTypes" import "../Components" +import "../Config" PageType { id: root + signal lastItemTabClickedSignal() + + onFocusChanged: content.isServerWithWriteAccess ? + labelWithButton.forceActiveFocus() : + labelWithButton3.forceActiveFocus() + Connections { target: InstallController @@ -85,12 +92,15 @@ PageType { property bool isServerWithWriteAccess: ServersModel.isProcessedServerHasWriteAccess() LabelWithButtonType { + id: labelWithButton visible: content.isServerWithWriteAccess Layout.fillWidth: true text: qsTr("Check the server for previously installed Amnezia services") descriptionText: qsTr("Add them to the application if they were not displayed") + KeyNavigation.tab: labelWithButton2 + clickedFunction: function() { PageController.showBusyIndicator(true) InstallController.scanServerForInstalledContainers() @@ -103,12 +113,15 @@ PageType { } LabelWithButtonType { + id: labelWithButton2 visible: content.isServerWithWriteAccess Layout.fillWidth: true text: qsTr("Reboot server") textColor: "#EB5757" + KeyNavigation.tab: labelWithButton3 + clickedFunction: function() { var headerText = qsTr("Do you want to reboot the server?") var descriptionText = qsTr("The reboot process may take approximately 30 seconds. Are you sure you wish to proceed?") @@ -123,8 +136,14 @@ PageType { InstallController.rebootProcessedServer() PageController.showBusyIndicator(false) } + if (!GC.isMobile()) { + labelWithButton5.forceActiveFocus() + } } var noButtonFunction = function() { + if (!GC.isMobile()) { + labelWithButton2.forceActiveFocus() + } } showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) @@ -136,11 +155,22 @@ PageType { } LabelWithButtonType { + id: labelWithButton3 Layout.fillWidth: true text: qsTr("Remove server from application") textColor: "#EB5757" + Keys.onTabPressed: { + if (content.isServerWithWriteAccess) { + labelWithButton4.forceActiveFocus() + } else { + labelWithButton5.visible ? + labelWithButton5.forceActiveFocus() : + lastItemTabClickedSignal() + } + } + clickedFunction: function() { var headerText = qsTr("Do you want to remove the server from application?") var descriptionText = qsTr("All installed AmneziaVPN services will still remain on the server.") @@ -155,8 +185,14 @@ PageType { InstallController.removeProcessedServer() PageController.showBusyIndicator(false) } + if (!GC.isMobile()) { + labelWithButton5.forceActiveFocus() + } } var noButtonFunction = function() { + if (!GC.isMobile()) { + labelWithButton3.forceActiveFocus() + } } showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) @@ -166,12 +202,17 @@ PageType { DividerType {} LabelWithButtonType { + id: labelWithButton4 visible: content.isServerWithWriteAccess Layout.fillWidth: true text: qsTr("Clear server from Amnezia software") textColor: "#EB5757" + Keys.onTabPressed: labelWithButton5.visible ? + labelWithButton5.forceActiveFocus() : + root.lastItemTabClickedSignal() + clickedFunction: function() { var headerText = qsTr("Do you want to clear server from Amnezia software?") var descriptionText = qsTr("All users whom you shared a connection with will no longer be able to connect to it.") @@ -185,8 +226,14 @@ PageType { PageController.goToPage(PageEnum.PageDeinstalling) InstallController.removeAllContainers() } + if (!GC.isMobile()) { + labelWithButton5.forceActiveFocus() + } } var noButtonFunction = function() { + if (!GC.isMobile()) { + labelWithButton4.forceActiveFocus() + } } showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) @@ -198,12 +245,15 @@ PageType { } LabelWithButtonType { + id: labelWithButton5 visible: ServersModel.getProcessedServerData("isServerFromApi") Layout.fillWidth: true text: qsTr("Reset API config") textColor: "#EB5757" + Keys.onTabPressed: root.lastItemTabClickedSignal() + clickedFunction: function() { var headerText = qsTr("Do you want to reset API config?") var descriptionText = "" @@ -218,8 +268,15 @@ PageType { InstallController.removeApiConfig(ServersModel.processedIndex) PageController.showBusyIndicator(false) } + + if (!GC.isMobile()) { + labelWithButton5.forceActiveFocus() + } } var noButtonFunction = function() { + if (!GC.isMobile()) { + labelWithButton5.forceActiveFocus() + } } showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) diff --git a/client/ui/qml/Pages2/PageSettingsServerInfo.qml b/client/ui/qml/Pages2/PageSettingsServerInfo.qml index 1d701825..3b8968e6 100644 --- a/client/ui/qml/Pages2/PageSettingsServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsServerInfo.qml @@ -18,6 +18,8 @@ import "../Components" PageType { id: root + defaultActiveFocusItem: focusItem + Connections { target: PageController @@ -37,6 +39,11 @@ PageType { ] } + Item { + id: focusItem + KeyNavigation.tab: header + } + ColumnLayout { anchors.fill: parent @@ -46,15 +53,26 @@ PageType { id: header model: proxyServersModel + activeFocusOnTab: true + onFocusChanged: { + header.itemAt(0).focusItem.forceActiveFocus() + } + delegate: ColumnLayout { + + property alias focusItem: backButton + id: content Layout.topMargin: 20 BackButtonType { + id: backButton + KeyNavigation.tab: headerContent.actionButton } HeaderType { + id: headerContent Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 @@ -70,6 +88,8 @@ PageType { } } + KeyNavigation.tab: tabBar + actionButtonFunction: function() { serverNameEditDrawer.open() } @@ -83,6 +103,12 @@ PageType { anchors.fill: parent expandedHeight: root.height * 0.35 + onClosed: { + if (!GC.isMobile()) { + headerContent.actionButton.forceActiveFocus() + } + } + expandedContent: ColumnLayout { anchors.top: parent.top anchors.left: parent.left @@ -99,6 +125,11 @@ PageType { } } + Item { + id: focusItem1 + KeyNavigation.tab: serverName.textField + } + TextFieldWithHeaderType { id: serverName @@ -117,6 +148,7 @@ PageType { Layout.fillWidth: true text: qsTr("Save") + KeyNavigation.tab: focusItem1 clickedFunc: function() { if (serverName.textFieldText === "") { @@ -129,12 +161,6 @@ PageType { serverNameEditDrawer.close() } } - - Component.onCompleted: { - if (header.itemAt(0) && !GC.isMobile()) { - defaultActiveFocusItem = serverName.textField - } - } } } } @@ -152,25 +178,52 @@ PageType { color: "transparent" } + activeFocusOnTab: true + onFocusChanged: { + if (activeFocus) { + protocolsTab.forceActiveFocus() + } + } + TabButtonType { + id: protocolsTab visible: protocolsPage.installedProtocolsCount width: protocolsPage.installedProtocolsCount ? undefined : 0 isSelected: tabBar.currentIndex === 0 text: qsTr("Protocols") + + KeyNavigation.tab: servicesTab + Keys.onReturnPressed: tabBar.currentIndex = 0 + Keys.onEnterPressed: tabBar.currentIndex = 0 } TabButtonType { + id: servicesTab visible: servicesPage.installedServicesCount width: servicesPage.installedServicesCount ? undefined : 0 isSelected: tabBar.currentIndex === 1 text: qsTr("Services") + + KeyNavigation.tab: dataTab + Keys.onReturnPressed: tabBar.currentIndex = 1 + Keys.onEnterPressed: tabBar.currentIndex = 1 } TabButtonType { + id: dataTab isSelected: tabBar.currentIndex === 2 text: qsTr("Management") + + Keys.onReturnPressed: tabBar.currentIndex = 2 + Keys.onEnterPressed: tabBar.currentIndex = 2 + KeyNavigation.tab: stackView.currentIndex === 0 ? + protocolsPage : + stackView.currentIndex === 1 ? + servicesPage : + dataPage } } StackLayout { + id: stackView Layout.preferredWidth: root.width Layout.preferredHeight: root.height - tabBar.implicitHeight - header.implicitHeight @@ -179,14 +232,22 @@ PageType { PageSettingsServerProtocols { id: protocolsPage stackView: root.stackView + + onLastItemTabClickedSignal: lastItemTabClicked(focusItem) } PageSettingsServerServices { id: servicesPage stackView: root.stackView + + onLastItemTabClickedSignal: lastItemTabClicked(focusItem) } PageSettingsServerData { + id: dataPage stackView: root.stackView + + onLastItemTabClickedSignal: lastItemTabClicked(focusItem) } } + } } diff --git a/client/ui/qml/Pages2/PageSettingsServerProtocol.qml b/client/ui/qml/Pages2/PageSettingsServerProtocol.qml index 916cbf76..97288733 100644 --- a/client/ui/qml/Pages2/PageSettingsServerProtocol.qml +++ b/client/ui/qml/Pages2/PageSettingsServerProtocol.qml @@ -18,6 +18,13 @@ import "../Components" PageType { id: root + defaultActiveFocusItem: focusItem + + Item { + id: focusItem + KeyNavigation.tab: backButton + } + ColumnLayout { id: header @@ -28,6 +35,8 @@ PageType { anchors.topMargin: 20 BackButtonType { + id: backButton + KeyNavigation.tab: protocols } HeaderType { @@ -47,7 +56,28 @@ PageType { interactive: true model: ProtocolsModel + property int currentFocusIndex: 0 + + activeFocusOnTab: true + onActiveFocusChanged: { + if (activeFocus) { + this.currentFocusIndex = 0 + protocols.itemAtIndex(currentFocusIndex).focusItem.forceActiveFocus() + } + } + + Keys.onTabPressed: { + if (currentFocusIndex < this.count - 1) { + currentFocusIndex += 1 + protocols.itemAtIndex(currentFocusIndex).focusItem.forceActiveFocus() + } else { + clearCacheButton.forceActiveFocus() + } + } + delegate: Item { + property var focusItem: button.rightButton + implicitWidth: protocols.width implicitHeight: delegateContent.implicitHeight @@ -95,6 +125,7 @@ PageType { Layout.fillWidth: true visible: ServersModel.isProcessedServerHasWriteAccess() + KeyNavigation.tab: removeButton text: qsTr("Clear %1 profile").arg(ContainersModel.getProcessedContainerName()) @@ -116,6 +147,9 @@ PageType { PageController.showBusyIndicator(false) } var noButtonFunction = function() { + if (!GC.isMobile()) { + focusItem.forceActiveFocus() + } } showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) @@ -142,6 +176,7 @@ PageType { Layout.fillWidth: true visible: ServersModel.isProcessedServerHasWriteAccess() + Keys.onTabPressed: lastItemTabClicked(focusItem) text: qsTr("Remove ") + ContainersModel.getProcessedContainerName() textColor: "#EB5757" @@ -163,6 +198,9 @@ PageType { } } var noButtonFunction = function() { + if (!GC.isMobile()) { + focusItem.forceActiveFocus() + } } showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) @@ -184,3 +222,4 @@ PageType { } } } + diff --git a/client/ui/qml/Pages2/PageSettingsServerProtocols.qml b/client/ui/qml/Pages2/PageSettingsServerProtocols.qml index 1dfd2886..5ce8fdbb 100644 --- a/client/ui/qml/Pages2/PageSettingsServerProtocols.qml +++ b/client/ui/qml/Pages2/PageSettingsServerProtocols.qml @@ -20,6 +20,9 @@ PageType { property var installedProtocolsCount + onFocusChanged: settingsContainersListView.forceActiveFocus() + signal lastItemTabClickedSignal() + FlickableType { id: fl anchors.top: parent.top @@ -35,6 +38,7 @@ PageType { SettingsContainersListView { id: settingsContainersListView + Connections { target: ServersModel diff --git a/client/ui/qml/Pages2/PageSettingsServerServices.qml b/client/ui/qml/Pages2/PageSettingsServerServices.qml index 8795bd23..72a4d3f7 100644 --- a/client/ui/qml/Pages2/PageSettingsServerServices.qml +++ b/client/ui/qml/Pages2/PageSettingsServerServices.qml @@ -20,6 +20,9 @@ PageType { property var installedServicesCount + onFocusChanged: settingsContainersListView.forceActiveFocus() + signal lastItemTabClickedSignal() + FlickableType { id: fl anchors.top: parent.top @@ -35,6 +38,7 @@ PageType { SettingsContainersListView { id: settingsContainersListView + Connections { target: ServersModel diff --git a/client/ui/qml/Pages2/PageSettingsServersList.qml b/client/ui/qml/Pages2/PageSettingsServersList.qml index eb07eaf4..596505a9 100644 --- a/client/ui/qml/Pages2/PageSettingsServersList.qml +++ b/client/ui/qml/Pages2/PageSettingsServersList.qml @@ -17,6 +17,13 @@ import "../Components" PageType { id: root + defaultActiveFocusItem: focusItem + + Item { + id: focusItem + KeyNavigation.tab: backButton + } + ColumnLayout { id: header @@ -27,6 +34,8 @@ PageType { anchors.topMargin: 20 BackButtonType { + id: backButton + KeyNavigation.tab: servers } HeaderType { @@ -39,6 +48,7 @@ PageType { } FlickableType { + id: fl anchors.top: header.bottom anchors.topMargin: 16 contentHeight: col.implicitHeight @@ -59,10 +69,36 @@ PageType { clip: true interactive: false + activeFocusOnTab: true + focus: true + Keys.onTabPressed: { + if (currentIndex < servers.count - 1) { + servers.incrementCurrentIndex() + } else { + servers.currentIndex = 0 + focusItem.forceActiveFocus() + root.lastItemTabClicked() + } + + fl.ensureVisible(this.currentItem) + } + + onVisibleChanged: { + if (visible) { + currentIndex = 0 + } + } + delegate: Item { implicitWidth: servers.width implicitHeight: delegateContent.implicitHeight + onFocusChanged: { + if (focus) { + server.rightButton.forceActiveFocus() + } + } + ColumnLayout { id: delegateContent @@ -75,6 +111,7 @@ PageType { Layout.fillWidth: true text: name + parentFlickable: fl descriptionText: { var servicesNameString = "" var servicesName = ServersModel.getAllInstalledServicesName(index) diff --git a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml index a8349f60..ce4c391f 100644 --- a/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml +++ b/client/ui/qml/Pages2/PageSettingsSplitTunneling.qml @@ -24,6 +24,11 @@ PageType { defaultActiveFocusItem: searchField.textField + Item { + id: focusItem + KeyNavigation.tab: backButton + } + property bool pageEnabled Component.onCompleted: { @@ -92,6 +97,8 @@ PageType { anchors.topMargin: 20 BackButtonType { + id: backButton + KeyNavigation.tab: switcher } RowLayout { @@ -112,11 +119,17 @@ PageType { Layout.fillWidth: true Layout.rightMargin: 16 - checked: SitesModel.isTunnelingEnabled - onToggled: { - SitesModel.toggleSplitTunneling(checked) + function onToggledFunc() { + SitesModel.toggleSplitTunneling(this.checked) selector.text = root.routeModesModel[getRouteModesModelIndex()].name } + + checked: SitesModel.isTunnelingEnabled + onToggled: { onToggledFunc() } + Keys.onEnterPressed: { onToggledFunc() } + Keys.onReturnPressed: { onToggledFunc() } + + KeyNavigation.tab: selector } } @@ -165,10 +178,17 @@ PageType { } } } + + KeyNavigation.tab: { + return sites.count > 0 ? + sites : + searchField.textField + } } } FlickableType { + id: fl anchors.top: header.bottom anchors.topMargin: 16 contentHeight: col.implicitHeight + addSiteButton.implicitHeight + addSiteButton.anchors.bottomMargin + addSiteButton.anchors.topMargin @@ -208,10 +228,29 @@ PageType { clip: true interactive: false + activeFocusOnTab: true + focus: true + Keys.onTabPressed: { + if (currentIndex < this.count - 1) { + this.incrementCurrentIndex() + } else { + currentIndex = 0 + searchField.textField.forceActiveFocus() + } + + fl.ensureVisible(currentItem) + } + delegate: Item { implicitWidth: sites.width implicitHeight: delegateContent.implicitHeight + onActiveFocusChanged: { + if (activeFocus) { + site.rightButton.forceActiveFocus() + } + } + ColumnLayout { id: delegateContent @@ -220,6 +259,7 @@ PageType { anchors.right: parent.right LabelWithButtonType { + id: site Layout.fillWidth: true text: url @@ -234,8 +274,14 @@ PageType { var yesButtonFunction = function() { SitesController.removeSite(proxySitesModel.mapToSource(index)) + if (!GC.isMobile()) { + site.rightButton.forceActiveFocus() + } } var noButtonFunction = function() { + if (!GC.isMobile()) { + site.rightButton.forceActiveFocus() + } } showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) @@ -246,6 +292,7 @@ PageType { } } } + } } @@ -273,9 +320,11 @@ PageType { id: searchField Layout.fillWidth: true + rightButtonClickedOnEnter: true textFieldPlaceholderText: qsTr("website or IP") buttonImageSource: "qrc:/images/controls/plus.svg" + KeyNavigation.tab: GC.isMobile() ? focusItem : addSiteButtonImage clickedFunc: function() { PageController.showBusyIndicator(true) @@ -286,6 +335,7 @@ PageType { } ImageButtonType { + id: addSiteButtonImage implicitWidth: 56 implicitHeight: 56 @@ -295,6 +345,11 @@ PageType { onClicked: function () { moreActionsDrawer.open() } + + Keys.onReturnPressed: addSiteButtonImage.clicked() + Keys.onEnterPressed: addSiteButtonImage.clicked() + + Keys.onTabPressed: lastItemTabClicked(focusItem) } } @@ -304,6 +359,12 @@ PageType { anchors.fill: parent expandedHeight: parent.height * 0.4375 + onClosed: { + if (root.defaultActiveFocusItem && !GC.isMobile()) { + root.defaultActiveFocusItem.forceActiveFocus() + } + } + expandedContent: ColumnLayout { id: moreActionsDrawerContent @@ -311,6 +372,25 @@ PageType { anchors.left: parent.left anchors.right: parent.right + Connections { + target: moreActionsDrawer + + function onOpened() { + focusItem1.forceActiveFocus() + } + + function onActiveFocusChanged() { + if (!GC.isMobile()) { + focusItem1.forceActiveFocus() + } + } + } + + Item { + id: focusItem1 + KeyNavigation.tab: importSitesButton.rightButton + } + Header2Type { Layout.fillWidth: true Layout.margins: 16 @@ -319,6 +399,7 @@ PageType { } LabelWithButtonType { + id: importSitesButton Layout.fillWidth: true text: qsTr("Import") @@ -327,14 +408,19 @@ PageType { clickedFunction: function() { importSitesDrawer.open() } + + KeyNavigation.tab: exportSitesButton } DividerType {} LabelWithButtonType { + id: exportSitesButton Layout.fillWidth: true text: qsTr("Save site list") + KeyNavigation.tab: focusItem1 + clickedFunction: function() { var fileName = "" if (GC.isMobile()) { @@ -365,9 +451,28 @@ PageType { anchors.fill: parent expandedHeight: parent.height * 0.4375 + onClosed: { + if (!GC.isMobile()) { + moreActionsDrawer.forceActiveFocus() + } + } + expandedContent: Item { implicitHeight: importSitesDrawer.expandedHeight + Connections { + target: importSitesDrawer + enabled: !GC.isMobile() + function onOpened() { + focusItem2.forceActiveFocus() + } + } + + Item { + id: focusItem2 + KeyNavigation.tab: importSitesDrawerBackButton + } + BackButtonType { id: importSitesDrawerBackButton @@ -376,6 +481,8 @@ PageType { anchors.right: parent.right anchors.topMargin: 16 + KeyNavigation.tab: importSitesButton2 + backButtonFunction: function() { importSitesDrawer.close() } @@ -404,9 +511,11 @@ PageType { } LabelWithButtonType { + id: importSitesButton2 Layout.fillWidth: true text: qsTr("Replace site list") + KeyNavigation.tab: importSitesButton3 clickedFunction: function() { var fileName = SystemController.getFileName(qsTr("Open sites file"), @@ -420,8 +529,10 @@ PageType { DividerType {} LabelWithButtonType { + id: importSitesButton3 Layout.fillWidth: true text: qsTr("Add imported sites to existing ones") + KeyNavigation.tab: focusItem2 clickedFunction: function() { var fileName = SystemController.getFileName(qsTr("Open sites file"), diff --git a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml index 1c13eb29..f6855f0c 100644 --- a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml +++ b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml @@ -22,6 +22,8 @@ PageType { } } + defaultActiveFocusItem: focusItem + FlickableType { id: fl anchors.top: parent.top @@ -37,8 +39,15 @@ PageType { spacing: 0 + Item { + id: focusItem + KeyNavigation.tab: backButton + } + BackButtonType { + id: backButton Layout.topMargin: 20 + KeyNavigation.tab: fileButton.rightButton } HeaderType { @@ -61,6 +70,7 @@ PageType { } LabelWithButtonType { + id: fileButton Layout.fillWidth: true Layout.topMargin: 16 @@ -68,6 +78,8 @@ PageType { rightImageSource: "qrc:/images/controls/chevron-right.svg" leftImageSource: "qrc:/images/controls/folder-open.svg" + KeyNavigation.tab: qrButton.visible ? qrButton.rightButton : textButton.rightButton + clickedFunction: function() { var nameFilter = !ServersModel.getServersCount() ? "Config or backup files (*.vpn *.ovpn *.conf *.json *.backup)" : "Config files (*.vpn *.ovpn *.conf *.json)" @@ -83,6 +95,7 @@ PageType { DividerType {} LabelWithButtonType { + id: qrButton Layout.fillWidth: true visible: SettingsController.isCameraPresent() @@ -90,6 +103,8 @@ PageType { rightImageSource: "qrc:/images/controls/chevron-right.svg" leftImageSource: "qrc:/images/controls/qr-code.svg" + KeyNavigation.tab: textButton.rightButton + clickedFunction: function() { ImportController.startDecodingQr() if (Qt.platform.os === "ios") { @@ -103,12 +118,15 @@ PageType { } LabelWithButtonType { + id: textButton Layout.fillWidth: true text: qsTr("Key as text") rightImageSource: "qrc:/images/controls/chevron-right.svg" leftImageSource: "qrc:/images/controls/text-cursor.svg" + Keys.onTabPressed: lastItemTabClicked(focusItem) + clickedFunction: function() { PageController.goToPage(PageEnum.PageSetupWizardTextKey) } diff --git a/client/ui/qml/Pages2/PageSetupWizardCredentials.qml b/client/ui/qml/Pages2/PageSetupWizardCredentials.qml index 6e112c46..ea522363 100644 --- a/client/ui/qml/Pages2/PageSetupWizardCredentials.qml +++ b/client/ui/qml/Pages2/PageSetupWizardCredentials.qml @@ -14,6 +14,11 @@ PageType { defaultActiveFocusItem: hostname.textField + Item { + id: focusItem + KeyNavigation.tab: backButton + } + BackButtonType { id: backButton @@ -21,6 +26,8 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 + + KeyNavigation.tab: hostname.textField } FlickableType { @@ -107,6 +114,8 @@ PageType { text: qsTr("Continue") + Keys.onTabPressed: lastItemTabClicked(focusItem) + clickedFunc: function() { forceActiveFocus() if (!isCredentialsFilled()) { diff --git a/client/ui/qml/Pages2/PageSetupWizardEasy.qml b/client/ui/qml/Pages2/PageSetupWizardEasy.qml index aba71560..acc902f3 100644 --- a/client/ui/qml/Pages2/PageSetupWizardEasy.qml +++ b/client/ui/qml/Pages2/PageSetupWizardEasy.qml @@ -16,6 +16,7 @@ PageType { id: root property bool isEasySetup: true + defaultActiveFocusItem: focusItem SortFilterProxyModel { id: proxyContainersModel @@ -32,6 +33,14 @@ PageType { } } + Item { + id: focusItem + implicitWidth: 1 + implicitHeight: 54 + + KeyNavigation.tab: backButton + } + BackButtonType { id: backButton @@ -39,6 +48,8 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 + + KeyNavigation.tab: continueButton } FlickableType { @@ -145,17 +156,14 @@ PageType { } } - Item { - implicitWidth: 1 - implicitHeight: 54 - } - BasicButtonType { id: continueButton implicitWidth: parent.width text: qsTr("Continue") + KeyNavigation.tab: setupLaterButton + parentFlickable: fl clickedFunc: function() { if (root.isEasySetup) { @@ -184,6 +192,9 @@ PageType { textColor: "#D7D8DB" borderWidth: 1 + Keys.onTabPressed: lastItemTabClicked(focusItem) + parentFlickable: fl + visible: { if (PageController.isTriggeredByConnectButton()) { PageController.setTriggeredByConnectButton(false) diff --git a/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml b/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml index 02e4ee6c..b694dda0 100644 --- a/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml +++ b/client/ui/qml/Pages2/PageSetupWizardProtocolSettings.qml @@ -61,12 +61,19 @@ PageType { anchors.rightMargin: 16 anchors.leftMargin: 16 + Item { + id: focusItem + KeyNavigation.tab: backButton + } + BackButtonType { id: backButton Layout.topMargin: 20 Layout.rightMargin: -16 Layout.leftMargin: -16 + + KeyNavigation.tab: showDetailsButton } HeaderType { @@ -93,6 +100,7 @@ PageType { textColor: "#FBB26A" text: qsTr("More detailed") + KeyNavigation.tab: transportProtoSelector clickedFunc: function() { showDetailsDrawer.open() @@ -102,12 +110,35 @@ PageType { DrawerType2 { id: showDetailsDrawer parent: root + onClosed: { + if (!GC.isMobile()) { + defaultActiveFocusItem.forceActiveFocus() + } + } anchors.fill: parent expandedHeight: parent.height * 0.9 expandedContent: Item { + Connections { + target: showDetailsDrawer + enabled: !GC.isMobile() + function onOpened() { + focusItem2.forceActiveFocus() + } + } + implicitHeight: showDetailsDrawer.expandedHeight + Item { + id: focusItem2 + KeyNavigation.tab: showDetailsBackButton + onFocusChanged: { + if (focusItem2.activeFocus) { + fl.contentY = 0 + } + } + } + BackButtonType { id: showDetailsBackButton @@ -116,12 +147,15 @@ PageType { anchors.right: parent.right anchors.topMargin: 16 + KeyNavigation.tab: showDetailsCloseButton + backButtonFunction: function() { showDetailsDrawer.close() } } FlickableType { + id: fl anchors.top: showDetailsBackButton.bottom anchors.left: parent.left anchors.right: parent.right @@ -158,17 +192,19 @@ PageType { textFormat: Text.MarkdownText } - Rectangle { Layout.fillHeight: true color: "transparent" } BasicButtonType { + id: showDetailsCloseButton Layout.fillWidth: true Layout.bottomMargin: 32 + parentFlickable: fl text: qsTr("Close") + Keys.onTabPressed: lastItemTabClicked(focusItem2) clickedFunc: function() { showDetailsDrawer.close() @@ -192,6 +228,8 @@ PageType { Layout.fillWidth: true rootWidth: root.width + + KeyNavigation.tab: (port.visible && port.enabled) ? port.textField : installButton } TextFieldWithHeaderType { @@ -220,6 +258,8 @@ PageType { text: qsTr("Install") + Keys.onTabPressed: lastItemTabClicked(focusItem) + clickedFunc: function() { PageController.goToPage(PageEnum.PageSetupWizardInstalling); InstallController.install(dockerContainer, port.textFieldText, transportProtoSelector.currentIndex) @@ -241,7 +281,10 @@ PageType { transportProtoSelector.visible = protocolSelectorVisible transportProtoHeader.visible = protocolSelectorVisible - defaultActiveFocusItem = port.textField + if (port.visible && port.enabled) + defaultActiveFocusItem = port.textField + else + defaultActiveFocusItem = focusItem } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardProtocols.qml b/client/ui/qml/Pages2/PageSetupWizardProtocols.qml index c684db21..cb922b4c 100644 --- a/client/ui/qml/Pages2/PageSetupWizardProtocols.qml +++ b/client/ui/qml/Pages2/PageSetupWizardProtocols.qml @@ -14,6 +14,13 @@ import "../Config" PageType { id: root + defaultActiveFocusItem: focusItem + + Item { + id: focusItem + KeyNavigation.tab: backButton + } + SortFilterProxyModel { id: proxyContainersModel sourceModel: ContainersModel @@ -30,7 +37,7 @@ PageType { } ColumnLayout { - id: backButton + id: backButtonLayout anchors.top: parent.top anchors.left: parent.left @@ -39,12 +46,14 @@ PageType { anchors.topMargin: 20 BackButtonType { + id: backButton + KeyNavigation.tab: containers } } FlickableType { id: fl - anchors.top: backButton.bottom + anchors.top: backButtonLayout.bottom anchors.bottom: parent.bottom contentHeight: content.implicitHeight + content.anchors.topMargin + content.anchors.bottomMargin @@ -79,15 +88,49 @@ PageType { id: containers width: parent.width height: containers.contentItem.height - currentIndex: -1 + // currentIndex: -1 clip: true interactive: false model: proxyContainersModel + function ensureCurrentItemVisible() { + if (currentIndex >= 0) { + if (currentItem.y < fl.contentY) { + fl.contentY = currentItem.y + } else if (currentItem.y + currentItem.height + header.height > fl.contentY + fl.height) { + fl.contentY = currentItem.y + currentItem.height + header.height - fl.height + 40 // 40 is a bottom margin + } + } + } + + activeFocusOnTab: true + Keys.onTabPressed: { + if (currentIndex < this.count - 1) { + this.incrementCurrentIndex() + } else { + this.currentIndex = 0 + focusItem.forceActiveFocus() + } + + ensureCurrentItemVisible() + } + + onVisibleChanged: { + if (visible) { + currentIndex = 0 + } + } + delegate: Item { implicitWidth: containers.width implicitHeight: delegateContent.implicitHeight + onActiveFocusChanged: { + if (activeFocus) { + container.rightButton.forceActiveFocus() + } + } + ColumnLayout { id: delegateContent diff --git a/client/ui/qml/Pages2/PageSetupWizardStart.qml b/client/ui/qml/Pages2/PageSetupWizardStart.qml index 0015b295..4be04255 100644 --- a/client/ui/qml/Pages2/PageSetupWizardStart.qml +++ b/client/ui/qml/Pages2/PageSetupWizardStart.qml @@ -15,6 +15,8 @@ PageType { property bool isControlsDisabled: false + defaultActiveFocusItem: focusItem + Connections { target: PageController @@ -136,7 +138,13 @@ PageType { qsTr(" Helps you access blocked content without revealing your privacy, even to VPN providers.") } + Item { + id: focusItem + KeyNavigation.tab: startButton + } + BasicButtonType { + id: startButton Layout.fillWidth: true Layout.topMargin: 32 Layout.leftMargin: 16 @@ -147,9 +155,12 @@ PageType { clickedFunc: function() { connectionTypeSelection.open() } + + KeyNavigation.tab: startButton2 } BasicButtonType { + id: startButton2 Layout.fillWidth: true Layout.topMargin: 8 Layout.leftMargin: 16 @@ -167,11 +178,18 @@ PageType { clickedFunc: function() { Qt.openUrlExternally(qsTr("https://amnezia.org/instructions/0_starter-guide")) } + + Keys.onTabPressed: lastItemTabClicked(focusItem) } } } ConnectionTypeSelectionDrawer { id: connectionTypeSelection + + onClosed: { + PageController.forceTabBarActiveFocus() + root.defaultActiveFocusItem.forceActiveFocus() + } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardTextKey.qml b/client/ui/qml/Pages2/PageSetupWizardTextKey.qml index 064d30ed..5c9da47a 100644 --- a/client/ui/qml/Pages2/PageSetupWizardTextKey.qml +++ b/client/ui/qml/Pages2/PageSetupWizardTextKey.qml @@ -14,6 +14,12 @@ PageType { defaultActiveFocusItem: textKey.textField + + Item { + id: focusItem + KeyNavigation.tab: backButton + } + FlickableType { id: fl anchors.top: parent.top @@ -30,7 +36,9 @@ PageType { spacing: 16 BackButtonType { + id: backButton Layout.topMargin: 20 + KeyNavigation.tab: textKey.textField } HeaderType { @@ -75,6 +83,7 @@ PageType { anchors.bottomMargin: 32 text: qsTr("Continue") + Keys.onTabPressed: lastItemTabClicked(focusItem) clickedFunc: function() { if (ImportController.extractConfigFromData(textKey.textFieldText)) { diff --git a/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml b/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml index 8093805b..658d6604 100644 --- a/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml +++ b/client/ui/qml/Pages2/PageSetupWizardViewConfig.qml @@ -15,6 +15,24 @@ PageType { property bool showContent: false + defaultActiveFocusItem: focusItem + + Item { + id: focusItem + KeyNavigation.tab: backButton + } + + BackButtonType { + id: backButton + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + + KeyNavigation.tab: showContentButton + } + Connections { target: ImportController @@ -39,15 +57,6 @@ PageType { } } - BackButtonType { - id: backButton - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: 20 - } - FlickableType { id: fl anchors.top: backButton.bottom @@ -88,6 +97,7 @@ PageType { } BasicButtonType { + id: showContentButton Layout.topMargin: 16 Layout.leftMargin: -8 implicitHeight: 32 @@ -99,6 +109,7 @@ PageType { textColor: "#FBB26A" text: showContent ? qsTr("Collapse content") : qsTr("Show content") + KeyNavigation.tab: connectButton clickedFunc: function() { showContent = !showContent @@ -138,15 +149,16 @@ PageType { } ColumnLayout { - id: connectButton - anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right anchors.rightMargin: 16 anchors.leftMargin: 16 + Keys.onTabPressed: lastItemTabClicked(focusItem) + BasicButtonType { + id: connectButton Layout.fillWidth: true Layout.bottomMargin: 32 diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 9907aa55..3bf1a892 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -152,6 +152,8 @@ PageType { } FlickableType { + id: a + anchors.top: parent.top anchors.bottom: parent.bottom contentHeight: content.height + 10 @@ -168,7 +170,18 @@ PageType { spacing: 0 + Item { + id: focusItem + KeyNavigation.tab: header.actionButton + onFocusChanged: { + if (focusItem.activeFocus) { + a.contentY = 0 + } + } + } + HeaderType { + id: header Layout.fillWidth: true Layout.topMargin: 24 @@ -179,6 +192,8 @@ PageType { shareFullAccessDrawer.open() } + KeyNavigation.tab: connectionRadioButton + DrawerType2 { id: shareFullAccessDrawer @@ -186,6 +201,11 @@ PageType { anchors.fill: parent expandedHeight: root.height * 0.45 + onClosed: { + if (!GC.isMobile()) { + clientNameTextField.textField.forceActiveFocus() + } + } expandedContent: ColumnLayout { anchors.top: parent.top @@ -195,6 +215,14 @@ PageType { spacing: 0 + Connections { + target: shareFullAccessDrawer + enabled: !GC.isMobile() + function onOpened() { + focusItem.forceActiveFocus() + } + } + Header2Type { Layout.fillWidth: true Layout.bottomMargin: 16 @@ -205,17 +233,24 @@ PageType { descriptionText: qsTr("Use for your own devices, or share with those you trust to manage the server.") } + Item { + id: focusItem + KeyNavigation.tab: shareFullAccessButton.rightButton + } LabelWithButtonType { + id: shareFullAccessButton Layout.fillWidth: true text: qsTr("Share") rightImageSource: "qrc:/images/controls/chevron-right.svg" + KeyNavigation.tab: focusItem clickedFunction: function() { PageController.goToPage(PageEnum.PageShareFullAccess) shareFullAccessDrawer.close() } + } } } @@ -240,28 +275,38 @@ PageType { spacing: 0 HorizontalRadioButton { + id: connectionRadioButton checked: accessTypeSelector.currentIndex === 0 implicitWidth: (root.width - 32) / 2 text: qsTr("Connection") + KeyNavigation.tab: usersRadioButton + onClicked: { accessTypeSelector.currentIndex = 0 + if (!GC.isMobile()) { + clientNameTextField.textField.forceActiveFocus() + } } } HorizontalRadioButton { + id: usersRadioButton checked: accessTypeSelector.currentIndex === 1 implicitWidth: (root.width - 32) / 2 text: qsTr("Users") + KeyNavigation.tab: accessTypeSelector.currentIndex === 0 ? clientNameTextField.textField : serverSelector + onClicked: { accessTypeSelector.currentIndex = 1 PageController.showBusyIndicator(true) ExportController.updateClientManagementModel(ContainersModel.getProcessedContainerIndex(), ServersModel.getProcessedServerCredentials()) PageController.showBusyIndicator(false) + focusItem.forceActiveFocus() } } } @@ -291,7 +336,8 @@ PageType { checkEmptyText: true - KeyNavigation.tab: shareButton + KeyNavigation.tab: serverSelector + } DropDownType { @@ -311,7 +357,6 @@ PageType { listView: ListViewWithRadioButtonType { id: serverSelectorListView - rootWidth: root.width imageSource: "qrc:/images/controls/check.svg" @@ -356,6 +401,8 @@ PageType { ServersModel.processedIndex = proxyServersModel.mapToSource(currentIndex) } } + + KeyNavigation.tab: protocolSelector } DropDownType { @@ -454,6 +501,12 @@ PageType { } } } + + KeyNavigation.tab: accessTypeSelector.currentIndex === 0 ? + exportTypeSelector : + isSearchBarVisible ? + searchTextField.textField : + usersHeader.actionButton } DropDownType { @@ -497,6 +550,9 @@ PageType { exportTypeSelector.currentIndex = currentIndex } } + + KeyNavigation.tab: shareButton + } BasicButtonType { @@ -510,16 +566,22 @@ PageType { visible: accessTypeSelector.currentIndex === 0 text: qsTr("Share") - imageSource: "qrc:/images/controls/share-2.svg" + imageSource: "qrc:/images/controls/share-2.svg" + + Keys.onTabPressed: lastItemTabClicked(focusItem) + + parentFlickable: a clickedFunc: function(){ if (clientNameTextField.textFieldText !== "") { ExportController.generateConfig(root.connectionTypesModel[exportTypeSelector.currentIndex].type) } } + } Header2Type { + id: usersHeader Layout.fillWidth: true Layout.topMargin: 24 Layout.bottomMargin: 16 @@ -531,6 +593,11 @@ PageType { actionButtonFunction: function() { root.isSearchBarVisible = true } + + Keys.onTabPressed: clientsListView.model.count > 0 ? + clientsListView.forceActiveFocus() : + lastItemTabClicked(focusItem) + } RowLayout { @@ -543,16 +610,66 @@ PageType { Layout.fillWidth: true textFieldPlaceholderText: qsTr("Search") + + Connections { + target: root + function onIsSearchBarVisibleChanged() { + if (root.isSearchBarVisible) { + searchTextField.textField.forceActiveFocus() + } else { + searchTextField.textFieldText = "" + if (!GC.isMobile()) { + usersHeader.actionButton.forceActiveFocus() + } + } + } + } + + Keys.onEscapePressed: { + root.isSearchBarVisible = false + } + + function navigateTo() { + if (GC.isMobile()) { + focusItem.forceActiveFocus() + return; + } + + if (searchTextField.textFieldText === "") { + root.isSearchBarVisible = false + usersHeader.actionButton.forceActiveFocus() + } else { + closeSearchButton.forceActiveFocus() + } + } + + Keys.onTabPressed: { navigateTo() } + Keys.onEnterPressed: { navigateTo() } + Keys.onReturnPressed: { navigateTo() } } ImageButtonType { + id: closeSearchButton image: "qrc:/images/controls/close.svg" imageColor: "#D7D8DB" - onClicked: function() { - root.isSearchBarVisible = false - searchTextField.textFieldText = "" + Keys.onTabPressed: { + if (!GC.isMobile()) { + if (clientsListView.model.count > 0) { + clientsListView.forceActiveFocus() + } else { + lastItemTabClicked(focusItem) + } + } } + + function clickedFunc() { + root.isSearchBarVisible = false + } + + onClicked: clickedFunc() + Keys.onEnterPressed: clickedFunc() + Keys.onReturnPressed: clickedFunc() } } @@ -576,10 +693,43 @@ PageType { clip: true interactive: false + activeFocusOnTab: true + focus: true + Keys.onTabPressed: { + if (!GC.isMobile()) { + if (currentIndex < this.count - 1) { + this.incrementCurrentIndex() + currentItem.focusItem.forceActiveFocus() + } else { + this.currentIndex = 0 + lastItemTabClicked(focusItem) + } + } + } + + onActiveFocusChanged: { + if (focus && !GC.isMobile()) { + currentIndex = 0 + currentItem.focusItem.forceActiveFocus() + } + } + + onCurrentIndexChanged: { + if (currentItem) { + if (currentItem.y < a.contentY) { + a.contentY = currentItem.y + } else if (currentItem.y + currentItem.height + clientsListView.y > a.contentY + a.height) { + a.contentY = currentItem.y + clientsListView.y + currentItem.height - a.height + } + } + } + delegate: Item { implicitWidth: clientsListView.width implicitHeight: delegateContent.implicitHeight + property alias focusItem: clientFocusItem.rightButton + ColumnLayout { id: delegateContent @@ -591,6 +741,7 @@ PageType { anchors.leftMargin: -16 LabelWithButtonType { + id: clientFocusItem Layout.fillWidth: true text: clientName @@ -608,6 +759,12 @@ PageType { parent: root + onClosed: { + if (!GC.isMobile()) { + focusItem.forceActiveFocus() + } + } + anchors.fill: parent expandedHeight: root.height * 0.5 @@ -621,6 +778,14 @@ PageType { spacing: 8 + Connections { + target: clientInfoDrawer + enabled: !GC.isMobile() + function onOpened() { + focusItem1.forceActiveFocus() + } + } + Header2Type { Layout.fillWidth: true Layout.bottomMargin: 24 @@ -629,7 +794,13 @@ PageType { descriptionText: qsTr("Creation date: ") + creationDate } + Item { + id: focusItem1 + KeyNavigation.tab: renameButton + } + BasicButtonType { + id: renameButton Layout.fillWidth: true Layout.topMargin: 24 @@ -642,6 +813,8 @@ PageType { text: qsTr("Rename") + KeyNavigation.tab: revokeButton + clickedFunc: function() { clientNameEditDrawer.open() } @@ -654,6 +827,12 @@ PageType { anchors.fill: parent expandedHeight: root.height * 0.35 + onClosed: { + if (!GC.isMobile()) { + focusItem1.forceActiveFocus() + } + } + expandedContent: ColumnLayout { anchors.top: parent.top anchors.left: parent.left @@ -670,6 +849,11 @@ PageType { } } + Item { + id: focusItem2 + KeyNavigation.tab: clientNameEditor.textField + } + TextFieldWithHeaderType { id: clientNameEditor Layout.fillWidth: true @@ -687,6 +871,7 @@ PageType { Layout.fillWidth: true text: qsTr("Save") + KeyNavigation.tab: focusItem2 clickedFunc: function() { if (clientNameEditor.textFieldText === "") { @@ -709,6 +894,7 @@ PageType { } BasicButtonType { + id: revokeButton Layout.fillWidth: true defaultColor: "transparent" @@ -719,6 +905,7 @@ PageType { borderWidth: 1 text: qsTr("Revoke") + KeyNavigation.tab: focusItem1 clickedFunc: function() { var headerText = qsTr("Revoke the config for a user - %1?").arg(clientName) @@ -731,6 +918,9 @@ PageType { root.revokeConfig(index) } var noButtonFunction = function() { + if (!GC.isMobile()) { + focusItem1.forceActiveFocus() + } } showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) @@ -748,6 +938,11 @@ PageType { id: shareConnectionDrawer anchors.fill: parent + onClosed: { + if (!GC.isMobile()) { + clientNameTextField.textField.forceActiveFocus() + } + } } MouseArea { diff --git a/client/ui/qml/Pages2/PageShareFullAccess.qml b/client/ui/qml/Pages2/PageShareFullAccess.qml index 8acb42c2..da182394 100644 --- a/client/ui/qml/Pages2/PageShareFullAccess.qml +++ b/client/ui/qml/Pages2/PageShareFullAccess.qml @@ -12,10 +12,18 @@ import "./" import "../Controls2" import "../Controls2/TextTypes" import "../Components" +import "../Config" PageType { id: root + defaultActiveFocusItem: focusItem + + Item { + id: focusItem + KeyNavigation.tab: backButton + } + BackButtonType { id: backButton @@ -23,6 +31,8 @@ PageType { anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 20 + + KeyNavigation.tab: serverSelector } FlickableType { @@ -74,6 +84,8 @@ PageType { descriptionText: qsTr("Server") headerText: qsTr("Server") + KeyNavigation.tab: shareButton + listView: ListViewWithRadioButtonType { id: serverSelectorListView @@ -100,7 +112,7 @@ PageType { shareConnectionDrawer.headerText = qsTr("Accessing ") + serverSelector.text shareConnectionDrawer.configContentHeaderText = qsTr("File with accessing settings to ") + serverSelector.text - serverSelector.close() + // serverSelector.close() } Component.onCompleted: { @@ -117,12 +129,15 @@ PageType { } BasicButtonType { + id: shareButton Layout.fillWidth: true Layout.topMargin: 40 text: qsTr("Share") imageSource: "qrc:/images/controls/share-2.svg" + Keys.onTabPressed: lastItemTabClicked(focusItem) + clickedFunc: function() { shareConnectionDrawer.headerText = qsTr("Connection to ") + serverSelector.text shareConnectionDrawer.configContentHeaderText = qsTr("File with connection settings to ") + serverSelector.text @@ -149,5 +164,10 @@ PageType { id: shareConnectionDrawer anchors.fill: parent + onClosed: { + if (!GC.isMobile()) { + focusItem.forceActiveFocus() + } + } } } diff --git a/client/ui/qml/Pages2/PageStart.qml b/client/ui/qml/Pages2/PageStart.qml index 0ad7b112..aca26566 100644 --- a/client/ui/qml/Pages2/PageStart.qml +++ b/client/ui/qml/Pages2/PageStart.qml @@ -14,6 +14,8 @@ import "../Components" PageType { id: root + defaultActiveFocusItem: homeTabButton + property bool isControlsDisabled: false property bool isTabBarDisabled: false @@ -82,6 +84,16 @@ PageType { PageController.closePage() } } + + function onForceTabBarActiveFocus() { + homeTabButton.focus = true + tabBar.forceActiveFocus() + } + + function onForceStackActiveFocus() { + homeTabButton.focus = true + tabBarStackView.forceActiveFocus() + } } Connections { @@ -211,13 +223,19 @@ PageType { } TabImageButtonType { + id: homeTabButton isSelected: tabBar.currentIndex === 0 image: "qrc:/images/controls/home.svg" - onClicked: { + clickedFunc: function () { tabBarStackView.goToTabBarPage(PageEnum.PageHome) ServersModel.processedIndex = ServersModel.defaultIndex + tabBar.currentIndex = 0 tabBar.previousIndex = 0 } + + KeyNavigation.tab: shareTabButton + Keys.onEnterPressed: this.clicked() + Keys.onReturnPressed: this.clicked() } TabImageButtonType { @@ -238,27 +256,37 @@ PageType { isSelected: tabBar.currentIndex === 1 image: "qrc:/images/controls/share-2.svg" - onClicked: { + clickedFunc: function () { tabBarStackView.goToTabBarPage(PageEnum.PageShare) + tabBar.currentIndex = 1 tabBar.previousIndex = 1 } + + KeyNavigation.tab: settingsTabButton } TabImageButtonType { + id: settingsTabButton isSelected: tabBar.currentIndex === 2 image: "qrc:/images/controls/settings-2.svg" - onClicked: { + clickedFunc: function () { tabBarStackView.goToTabBarPage(PageEnum.PageSettings) + tabBar.currentIndex = 2 tabBar.previousIndex = 2 } + + KeyNavigation.tab: plusTabButton } TabImageButtonType { + id: plusTabButton isSelected: tabBar.currentIndex === 3 image: "qrc:/images/controls/plus.svg" - onClicked: { + clickedFunc: function () { connectionTypeSelection.open() } + + Keys.onTabPressed: PageController.forceStackActiveFocus() } } @@ -266,6 +294,7 @@ PageType { id: connectionTypeSelection onAboutToHide: { + PageController.forceTabBarActiveFocus() tabBar.setCurrentIndex(tabBar.previousIndex) } }