From 444ef65a783dfe00ae6e60bacd5218ed65061181 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Thu, 29 Jan 2026 19:49:27 +0100 Subject: [PATCH 1/3] Core: add InputManager and couch UI mode --- src/CMakeLists.txt | 2 + src/config.h | 2 +- src/inputmanager.cpp | 104 +++++++++++++++++++++++++++++++++++++++++++ src/inputmanager.h | 54 ++++++++++++++++++++++ 4 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 src/inputmanager.cpp create mode 100644 src/inputmanager.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c3e902d..7493b46 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,6 +8,7 @@ add_executable(alakarte gamesortfiltermodel.cpp game.cpp gamepadmanager.cpp + inputmanager.cpp mediamanager.cpp screenshotmodel.cpp platformimporter.cpp @@ -118,6 +119,7 @@ ecm_add_qml_module(alakarte URI org.kde.alakarte gamesortfiltermodel.h game.h gamepadmanager.h + inputmanager.h platformimporter.h steamimporter.h lutrisimporter.h diff --git a/src/config.h b/src/config.h index 5ec87f6..0ab65c0 100644 --- a/src/config.h +++ b/src/config.h @@ -42,7 +42,7 @@ public: enum UiMode { Auto, Desktop, - Handheld + Couch }; Q_ENUM(UiMode) diff --git a/src/inputmanager.cpp b/src/inputmanager.cpp new file mode 100644 index 0000000..d792559 --- /dev/null +++ b/src/inputmanager.cpp @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors + +#include "inputmanager.h" + +#include "gamepadmanager.h" + +#include +#include +#include + +InputManager *InputManager::s_instance = nullptr; + +InputManager::InputManager(QObject *parent) + : QObject(parent) +{ + if (QCoreApplication::instance()) { + QCoreApplication::instance()->installEventFilter(this); + } + + auto *pad = GamepadManager::instance(); + connect(pad, &GamepadManager::activeChanged, this, [this, pad]() { + if (pad->active()) { + if (!m_hasSeenGamepad) { + m_hasSeenGamepad = true; + Q_EMIT inputHistoryChanged(); + } + setActiveInput(Gamepad); + } + }); + + connect(pad, &GamepadManager::connectedChanged, this, [this, pad]() { + if (!pad->connected() && m_activeInput == Gamepad) { + setActiveInput(KeyboardMouse); + } + }); +} + +InputManager *InputManager::instance() +{ + if (!s_instance) { + s_instance = new InputManager(); + } + return s_instance; +} + +InputManager *InputManager::create(QQmlEngine *engine, QJSEngine *scriptEngine) +{ + Q_UNUSED(engine) + Q_UNUSED(scriptEngine) + return instance(); +} + +InputManager::ActiveInput InputManager::activeInput() const +{ + return m_activeInput; +} + +bool InputManager::hasSeenKeyboardMouse() const +{ + return m_hasSeenKeyboardMouse; +} + +bool InputManager::hasSeenGamepad() const +{ + return m_hasSeenGamepad; +} + +void InputManager::setActiveInput(ActiveInput input) +{ + if (m_activeInput == input) { + return; + } + + m_activeInput = input; + Q_EMIT activeInputChanged(); +} + +bool InputManager::eventFilter(QObject *watched, QEvent *event) +{ + Q_UNUSED(watched) + + if (!event) { + return false; + } + + switch (event->type()) { + case QEvent::KeyPress: + case QEvent::KeyRelease: + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: + case QEvent::Wheel: + if (!m_hasSeenKeyboardMouse) { + m_hasSeenKeyboardMouse = true; + Q_EMIT inputHistoryChanged(); + } + setActiveInput(KeyboardMouse); + break; + default: + break; + } + + return false; +} diff --git a/src/inputmanager.h b/src/inputmanager.h new file mode 100644 index 0000000..3170b85 --- /dev/null +++ b/src/inputmanager.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors + +#pragma once + +#include +#include + +class QEvent; +class QJSEngine; + +class InputManager : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(ActiveInput activeInput READ activeInput NOTIFY activeInputChanged) + Q_PROPERTY(bool hasSeenKeyboardMouse READ hasSeenKeyboardMouse NOTIFY inputHistoryChanged) + Q_PROPERTY(bool hasSeenGamepad READ hasSeenGamepad NOTIFY inputHistoryChanged) + +public: + enum ActiveInput { + KeyboardMouse = 0, + Gamepad = 1, + }; + Q_ENUM(ActiveInput) + + static InputManager *instance(); + static InputManager *create(QQmlEngine *engine, QJSEngine *scriptEngine); + + ActiveInput activeInput() const; + + bool hasSeenKeyboardMouse() const; + bool hasSeenGamepad() const; + +Q_SIGNALS: + void activeInputChanged(); + void inputHistoryChanged(); + +protected: + bool eventFilter(QObject *watched, QEvent *event) override; + +private: + explicit InputManager(QObject *parent = nullptr); + + static InputManager *s_instance; + + ActiveInput m_activeInput = KeyboardMouse; + bool m_hasSeenKeyboardMouse = false; + bool m_hasSeenGamepad = false; + + void setActiveInput(ActiveInput input); +}; From 5b993cff6b0283530f3c1c63393077374b73b6e9 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Thu, 29 Jan 2026 19:49:45 +0100 Subject: [PATCH 2/3] QML: fix couch focus/menu scoping and overlays --- src/qml/GameDetailsSheet.qml | 37 ++ src/qml/GameEditDialog.qml | 25 +- src/qml/LibraryView.qml | 24 + src/qml/Main.qml | 739 +++++++++++++++++++++++---- src/qml/SettingsPage.qml | 148 +++++- src/qml/SidebarView.qml | 50 +- src/qml/components/BottomHintBar.qml | 120 ++++- src/qml/components/GameGridView.qml | 26 +- src/qml/components/SearchHeader.qml | 96 ++++ 9 files changed, 1143 insertions(+), 122 deletions(-) diff --git a/src/qml/GameDetailsSheet.qml b/src/qml/GameDetailsSheet.qml index be737ab..b68e734 100644 --- a/src/qml/GameDetailsSheet.qml +++ b/src/qml/GameDetailsSheet.qml @@ -54,10 +54,37 @@ Kirigami.OverlaySheet { onOpened: playButton.forceActiveFocus() + Shortcut { + enabled: detailsSheet.opened + sequence: "E" + onActivated: { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return + detailsSheet.editRequested() + } + } + + Shortcut { + enabled: detailsSheet.opened + sequence: "F" + onActivated: { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return + if (game) game.favorite = !game.favorite + } + } + function isDescendant(item, ancestor) { let p = item while (p) { if (p === ancestor) return true + if (ancestor.contentItem && p === ancestor.contentItem) return true + if (ancestor.header && p === ancestor.header) return true + if (p.visualParent !== undefined && p.visualParent !== null) { + if (detailsSheet.isDescendant(p.visualParent, ancestor)) return true + } else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) { + if (detailsSheet.isDescendant(p.popup.visualParent, ancestor)) return true + } p = p.parent } return false @@ -81,6 +108,8 @@ Kirigami.OverlaySheet { Connections { target: GamepadManager function onNavigateLeft() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return if (!detailsSheet.opened) return let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -88,6 +117,8 @@ Kirigami.OverlaySheet { detailsSheet.focusNextInChain(false) } function onNavigateRight() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return if (!detailsSheet.opened) return let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -95,6 +126,8 @@ Kirigami.OverlaySheet { detailsSheet.focusNextInChain(true) } function onNavigateUp() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return if (!detailsSheet.opened) return let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -102,6 +135,8 @@ Kirigami.OverlaySheet { detailsSheet.focusNextInChain(false) } function onNavigateDown() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return if (!detailsSheet.opened) return let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -109,6 +144,8 @@ Kirigami.OverlaySheet { detailsSheet.focusNextInChain(true) } function onSelectPressed() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return if (!detailsSheet.opened) return let w = applicationWindow() if (!w || !w.activeFocusItem) return diff --git a/src/qml/GameEditDialog.qml b/src/qml/GameEditDialog.qml index ebf6090..4729aa4 100644 --- a/src/qml/GameEditDialog.qml +++ b/src/qml/GameEditDialog.qml @@ -86,10 +86,23 @@ Kirigami.Dialog { property string selectedCoverPath: "" + readonly property bool anyMenuOpen: runnerCombo && runnerCombo.popup && runnerCombo.popup.visible + + function closeCurrentMenu() { + if (runnerCombo && runnerCombo.popup && runnerCombo.popup.visible) { + runnerCombo.popup.close() + } + } + function isDescendant(item, ancestor) { let p = item while (p) { if (p === ancestor || (ancestor.contentItem && p === ancestor.contentItem)) return true + if (p.visualParent !== undefined && p.visualParent !== null) { + if (dialog.isDescendant(p.visualParent, ancestor)) return true + } else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) { + if (dialog.isDescendant(p.popup.visualParent, ancestor)) return true + } p = p.parent } return false @@ -118,7 +131,7 @@ Kirigami.Dialog { item.toggle() return } - if (item.hasOwnProperty("checked")) { + if (item.checkable !== undefined && item.checkable && item.checked !== undefined) { item.checked = !item.checked return } @@ -203,6 +216,8 @@ Kirigami.Dialog { Connections { target: GamepadManager function onNavigateUp() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return if (!dialog.opened) return let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -210,6 +225,8 @@ Kirigami.Dialog { dialog.focusNextInChain(false) } function onNavigateDown() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return if (!dialog.opened) return let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -217,6 +234,8 @@ Kirigami.Dialog { dialog.focusNextInChain(true) } function onNavigateLeft() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return if (!dialog.opened) return let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -224,6 +243,8 @@ Kirigami.Dialog { dialog.focusNextInChain(false) } function onNavigateRight() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return if (!dialog.opened) return let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -231,6 +252,8 @@ Kirigami.Dialog { dialog.focusNextInChain(true) } function onSelectPressed() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return if (!dialog.opened) return let w = applicationWindow() if (!w || !w.activeFocusItem) return diff --git a/src/qml/LibraryView.qml b/src/qml/LibraryView.qml index 5e510ee..78cdff8 100644 --- a/src/qml/LibraryView.qml +++ b/src/qml/LibraryView.qml @@ -25,6 +25,12 @@ FocusScope { readonly property int gameCount: proxyModel.count property url focusedCoverUrl: "" + readonly property bool anyMenuOpen: searchHeader.anyMenuOpen + + function closeCurrentMenu() { + searchHeader.closeCurrentMenu() + } + function focusSearch() { searchField.forceActiveFocus() } @@ -35,6 +41,14 @@ FocusScope { } function restoreFocus() { + let w = applicationWindow() + if (w && w.hasOwnProperty("pendingSidebarOpen") && w.pendingSidebarOpen) { + w.pendingSidebarOpen = false + if (w.globalDrawer && typeof w.globalDrawer.open === "function") { + w.globalDrawer.open() + return + } + } if (libraryRoot.searchActive) { libraryRoot.focusSearch() } else { @@ -176,6 +190,16 @@ FocusScope { } Keys.onDownPressed: gameGrid.forceActiveFocus() + + Connections { + target: GamepadManager + function onNavigateDown() { + let w = applicationWindow() + if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return + if (!searchField.activeFocus) return + gameGrid.forceActiveFocus() + } + } } } diff --git a/src/qml/Main.qml b/src/qml/Main.qml index ea86ac4..dc4bc6a 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -23,22 +23,152 @@ Kirigami.ApplicationWindow { property string currentSource: "all" property bool searchActive: false property bool settingsLayerOpen: false + property bool pendingSidebarOpen: false + property bool pendingAboutOpen: false readonly property string hintContext: { if (gameEditDialog.visible) return "edit" + if (removeGameConfirmDialog.opened) return "confirm_remove" + { + let layerContent = root.settingsLayerContentItem() + if (settingsLayerOpen && layerContent && layerContent.anyConfirmOpen) return "confirm" + } + if (settingsSheet.opened && settingsContent.anyConfirmOpen) return "confirm" if (detailsSheet.opened) return "details" if (importSheet.opened) return "import" + if (settingsSheet.opened) return "settings" if (settingsLayerOpen) return "settings" if (aboutSheet.opened) return "about" - if (sidebar.modal && sidebar.opened) return "sidebar" + if (sidebar.opened && (sidebar.modal || sidebarView.hasSidebarFocus)) return "sidebar" return "library" } + function isDescendant(item, ancestor) { + let p = item + while (p) { + if (p === ancestor) return true + if (ancestor.contentItem && p === ancestor.contentItem) return true + if (ancestor.header && p === ancestor.header) return true + if (p.visualParent !== undefined && p.visualParent !== null) { + if (root.isDescendant(p.visualParent, ancestor)) return true + } else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) { + if (root.isDescendant(p.popup.visualParent, ancestor)) return true + } + p = p.parent + } + return false + } + + function currentConfirmDialog() { + if (removeGameConfirmDialog.opened) return removeGameConfirmDialog + let layerContent = root.settingsLayerContentItem() + if (settingsLayerOpen && layerContent && layerContent.anyConfirmOpen) return layerContent.currentConfirmDialog() + if (settingsSheet.opened && settingsContent.anyConfirmOpen) return settingsContent.currentConfirmDialog() + return null + } + + function settingsLayerContentItem() { + if (!settingsLayerOpen) return null + if (!root.pageStack || !root.pageStack.layers) return null + let item = root.pageStack.layers.currentItem + if (!item) return null + if (item.content !== undefined && item.content !== null) return item.content + return null + } + + function focusNextInDialog(dialog, forward) { + let w = applicationWindow() + if (!w || !w.activeFocusItem || !dialog) return + + if (typeof dialog.standardButton === "function") { + let yesButton = dialog.standardButton(Kirigami.Dialog.Yes) + let noButton = dialog.standardButton(Kirigami.Dialog.No) + if (yesButton && noButton) { + if (w.activeFocusItem === yesButton) { + noButton.forceActiveFocus() + return + } + if (w.activeFocusItem === noButton) { + yesButton.forceActiveFocus() + return + } + (forward ? yesButton : noButton).forceActiveFocus() + return + } + } + + let next = w.activeFocusItem + for (let i = 0; i < 50; i++) { + next = next.nextItemInFocusChain(forward) + if (!next) return + if (root.isDescendant(next, dialog)) { + next.forceActiveFocus() + return + } + } + } + + function activateFocusedInDialog(dialog) { + let w = applicationWindow() + if (!w || !w.activeFocusItem || !dialog) return + + if (!root.isDescendant(w.activeFocusItem, dialog)) { + if (typeof dialog.standardButton === "function") { + let noButton = dialog.standardButton(Kirigami.Dialog.No) + if (noButton) { + noButton.forceActiveFocus() + return + } + } + dialog.forceActiveFocus() + return + } + + let item = w.activeFocusItem + if (typeof item.clicked === "function") { + item.clicked() + } + } + function closeTopmost() { + if (libraryView && libraryView.anyMenuOpen) { + libraryView.closeCurrentMenu() + return true + } if (gameEditDialog.visible) { + if (gameEditDialog.anyMenuOpen) { + gameEditDialog.closeCurrentMenu() + return true + } gameEditDialog.close() return true } + if (removeGameConfirmDialog.opened) { + removeGameConfirmDialog.close() + root.pendingRemoveGame = null + return true + } + let layerContent = root.settingsLayerContentItem() + if (settingsLayerOpen && layerContent && layerContent.anyConfirmOpen) { + layerContent.closeCurrentConfirmDialog() + return true + } + if (settingsLayerOpen && layerContent && layerContent.anyMenuOpen) { + layerContent.closeCurrentMenu() + return true + } + if (settingsSheet.opened && settingsContent.anyConfirmOpen) { + settingsContent.closeCurrentConfirmDialog() + return true + } + if (settingsSheet.opened && settingsContent.anyMenuOpen) { + settingsContent.closeCurrentMenu() + return true + } + if (settingsSheet.opened) { + settingsSheet.close() + return true + } if (detailsSheet.opened) { detailsSheet.close() return true @@ -51,8 +181,12 @@ Kirigami.ApplicationWindow { aboutSheet.close() return true } - if (sidebar.modal && sidebar.opened) { - sidebar.close() + if (sidebar.opened && (sidebar.modal || sidebarView.hasSidebarFocus)) { + if (sidebar.modal) { + sidebar.close() + } else { + Qt.callLater(function() { libraryView.restoreFocus() }) + } return true } if (root.pageStack.layers.depth > 1) { @@ -74,6 +208,7 @@ Kirigami.ApplicationWindow { && !detailsSheet.opened && !importSheet.opened && !aboutSheet.opened + && !settingsSheet.opened && !(sidebar.modal && sidebar.opened) && root.pageStack.layers.depth <= 1 } @@ -93,7 +228,7 @@ Kirigami.ApplicationWindow { function uiModeLabel(mode) { if (mode === Config.Desktop) return i18n("Desktop") - if (mode === Config.Handheld) return i18n("Handheld") + if (mode === Config.Couch) return i18n("Couch") return i18n("Automatic") } @@ -101,7 +236,7 @@ Kirigami.ApplicationWindow { if (App.config.uiMode === Config.Auto) { App.config.uiMode = Config.Desktop } else if (App.config.uiMode === Config.Desktop) { - App.config.uiMode = Config.Handheld + App.config.uiMode = Config.Couch } else { App.config.uiMode = Config.Auto } @@ -112,89 +247,148 @@ Kirigami.ApplicationWindow { readonly property bool deviceIsTablet: Kirigami.Settings.tabletMode && !Kirigami.Settings.isMobile readonly property bool deviceIsTouchDevice: Kirigami.Settings.tabletMode || Kirigami.Settings.isMobile - readonly property bool isMobile: App.config.uiMode === Config.Handheld ? true : deviceIsMobile - readonly property bool isTablet: App.config.uiMode === Config.Handheld ? true : deviceIsTablet - readonly property bool isTouchDevice: App.config.uiMode === Config.Handheld ? true : (App.config.uiMode === Config.Desktop ? false : deviceIsTouchDevice) + readonly property int effectiveUiMode: { + if (App.config.uiMode === Config.Auto) { + if (GamepadManager.connected) { + if (InputManager.activeInput === InputManager.Gamepad) { + return Config.Couch + } + if (!InputManager.hasSeenKeyboardMouse) { + return Config.Couch + } + } + return Config.Desktop + } + return App.config.uiMode + } + + readonly property bool isCouchMode: effectiveUiMode === Config.Couch + readonly property real couchScale: isCouchMode ? 1.20 : 1.0 + + readonly property bool isMobile: deviceIsMobile + readonly property bool isTablet: deviceIsTablet + readonly property bool isTouchDevice: deviceIsTouchDevice readonly property bool isNarrowScreen: width < Kirigami.Units.gridUnit * 30 - readonly property int adaptiveCardSize: App.config.gridSize + readonly property int adaptiveCardSize: Math.round(App.config.gridSize * couchScale) pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.ToolBar pageStack.columnView.columnResizeMode: Kirigami.ColumnView.SingleColumn - footer: QQC2.ToolBar { - visible: true + footer: Item { + implicitWidth: root.width + anchors.left: parent ? parent.left : undefined + anchors.right: parent ? parent.right : undefined - leftPadding: 0 - rightPadding: 0 - topPadding: Kirigami.Units.smallSpacing - bottomPadding: Kirigami.Units.smallSpacing + implicitHeight: footerBar.implicitHeight + height: implicitHeight - contentItem: Item { - implicitHeight: Math.max(centerHints.implicitHeight, rightExtras.implicitHeight) + QQC2.ToolBar { + id: footerBar + anchors.fill: parent - readonly property bool compactFooter: (centerHints.implicitWidth + rightExtras.implicitWidth + Kirigami.Units.gridUnit * 2) > width + visible: true + + leftPadding: 0 + rightPadding: 0 + topPadding: Kirigami.Units.smallSpacing + bottomPadding: Kirigami.Units.smallSpacing + + contentItem: Item { + id: footerRoot + + readonly property int footerMargins: root.isCouchMode ? Kirigami.Units.mediumSpacing : Kirigami.Units.smallSpacing + readonly property int footerMinHeight: Math.round(Kirigami.Units.gridUnit * (root.isCouchMode ? 2.2 : 1.6)) + + implicitHeight: Math.max(footerLayout.implicitHeight + footerMargins * 2, footerMinHeight) Kirigami.ShadowedRectangle { - id: centerHints - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter + id: footerBg + anchors.fill: parent radius: Kirigami.Units.largeSpacing color: Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.85) shadow.size: Kirigami.Units.smallSpacing shadow.color: Qt.rgba(0, 0, 0, 0.25) - implicitWidth: hintRow.implicitWidth + Kirigami.Units.largeSpacing * 2 - implicitHeight: hintRow.implicitHeight + Kirigami.Units.smallSpacing * 2 + readonly property bool compactFooter: (hintBar.implicitWidth + rightExtras.implicitWidth + Kirigami.Units.gridUnit * 2) > width RowLayout { - id: hintRow - anchors.centerIn: parent - spacing: Kirigami.Units.largeSpacing + id: footerLayout + anchors.fill: parent + anchors.margins: footerRoot.footerMargins + spacing: root.isCouchMode ? Kirigami.Units.largeSpacing : Kirigami.Units.mediumSpacing - BottomHintBar { - context: root.hintContext + Flickable { + id: hintFlick + clip: true + Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter + Layout.minimumHeight: Math.max(hintBar.implicitHeight, footerRoot.footerMinHeight - footerRoot.footerMargins * 2) + Layout.preferredHeight: Layout.minimumHeight + + contentWidth: hintContent.width + contentHeight: hintContent.height + boundsBehavior: Flickable.StopAtBounds + flickableDirection: Flickable.HorizontalFlick + interactive: hintBar.implicitWidth > width + + QQC2.ScrollBar.horizontal: QQC2.ScrollBar { policy: QQC2.ScrollBar.AlwaysOff } + + Item { + id: hintContent + width: Math.max(hintBar.implicitWidth, hintFlick.width) + height: Math.max(hintBar.implicitHeight, hintFlick.height) + + BottomHintBar { + id: hintBar + context: root.hintContext + uiMode: root.effectiveUiMode + activeInput: InputManager.activeInput + + width: implicitWidth + height: implicitHeight + anchors.centerIn: parent + } + } + } + + RowLayout { + id: rightExtras + spacing: Kirigami.Units.mediumSpacing + + visible: !footerBg.compactFooter + && !root.isCouchMode + + QQC2.Label { + text: i18np("%1 game", "%1 games", libraryView.gameCount) + color: Kirigami.Theme.disabledTextColor + font.pointSize: Kirigami.Theme.smallFont.pointSize + Layout.alignment: Qt.AlignVCenter + visible: root.hintContext === "library" && libraryView.gameCount > 0 + } + + QQC2.Slider { + id: sizeSlider + from: 120 + to: 280 + stepSize: 20 + value: App.config.gridSize + Layout.preferredWidth: Kirigami.Units.gridUnit * 8 + Layout.alignment: Qt.AlignVCenter + visible: root.hintContext === "library" && libraryView.gameCount > 0 + + onMoved: App.config.gridSize = value + + QQC2.ToolTip { + parent: sizeSlider.handle + visible: sizeSlider.pressed + text: Math.round(sizeSlider.value) + " px" + } + } } } } - - RowLayout { - id: rightExtras - anchors.right: parent.right - anchors.rightMargin: Kirigami.Units.mediumSpacing - anchors.verticalCenter: parent.verticalCenter - spacing: Kirigami.Units.mediumSpacing - - visible: !parent.compactFooter - - QQC2.Label { - text: i18np("%1 game", "%1 games", libraryView.gameCount) - color: Kirigami.Theme.disabledTextColor - font.pointSize: Kirigami.Theme.smallFont.pointSize - Layout.alignment: Qt.AlignVCenter - visible: root.hintContext === "library" && libraryView.gameCount > 0 - } - - QQC2.Slider { - id: sizeSlider - from: 120 - to: 280 - stepSize: 20 - value: App.config.gridSize - Layout.preferredWidth: Kirigami.Units.gridUnit * 8 - Layout.alignment: Qt.AlignVCenter - visible: root.hintContext === "library" && libraryView.gameCount > 0 - - onMoved: App.config.gridSize = value - - QQC2.ToolTip { - parent: sizeSlider.handle - visible: sizeSlider.pressed - text: Math.round(sizeSlider.value) + " px" - } - } } } } @@ -202,6 +396,9 @@ Kirigami.ApplicationWindow { Shortcut { sequence: "Ctrl+F" onActivated: { + if (root.currentConfirmDialog()) return + if (detailsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsLayerOpen || settingsSheet.opened || aboutSheet.opened) return + if (sidebar.modal && sidebar.opened) return searchActive = true Qt.callLater(function() { libraryView.focusSearch() }) } @@ -209,20 +406,48 @@ Kirigami.ApplicationWindow { Shortcut { sequence: "Ctrl+I" - onActivated: importSheet.open() + onActivated: { + if (root.currentConfirmDialog()) return + importSheet.open() + } } Shortcut { sequence: "Ctrl+N" onActivated: { + if (root.currentConfirmDialog()) return gameEditDialog.game = null gameEditDialog.open() } } Shortcut { - sequence: StandardKey.Preferences - onActivated: root.openSettings() + sequence: "Ctrl+," + onActivated: { + if (root.currentConfirmDialog()) return + if (settingsSheet.opened) { + settingsSheet.close() + return + } + if (settingsLayerOpen) { + root.closeSettings() + return + } + if (root.canOpenSettings()) { + root.openSettings() + return + } + if (detailsSheet.opened) { + detailsSheet.close() + Qt.callLater(function() { root.openSettings() }) + return + } + if (sidebar.modal && sidebar.opened) { + sidebar.close() + Qt.callLater(function() { root.openSettings() }) + return + } + } } Shortcut { @@ -232,23 +457,51 @@ Kirigami.ApplicationWindow { Shortcut { sequence: "F5" - onActivated: App.importAllGames() + onActivated: { + if (root.currentConfirmDialog()) return + App.importAllGames() + } } Shortcut { sequence: "Ctrl+H" onActivated: { - if (root.currentSource === "hidden") { - root.currentSource = "all" - } else { - root.currentSource = "hidden" - } + if (root.currentConfirmDialog()) return + if (detailsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsLayerOpen || settingsSheet.opened || aboutSheet.opened) return + if (root.hintContext !== "library" && root.hintContext !== "sidebar") return + let target = (root.currentSource === "hidden") ? "all" : "hidden" + sidebarView.suppressAutoClose = true + sidebarView.applySourceById(target) + Qt.callLater(function() { sidebarView.suppressAutoClose = false }) + } + } + + Shortcut { + sequence: "Ctrl+PgUp" + onActivated: { + if (root.currentConfirmDialog()) return + if (detailsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsLayerOpen || settingsSheet.opened || aboutSheet.opened) return + if (root.hintContext !== "library" && root.hintContext !== "sidebar") return + sidebarView.cycleSource(-1) + } + } + + Shortcut { + sequence: "Ctrl+PgDown" + onActivated: { + if (root.currentConfirmDialog()) return + if (detailsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsLayerOpen || settingsSheet.opened || aboutSheet.opened) return + if (root.hintContext !== "library" && root.hintContext !== "sidebar") return + sidebarView.cycleSource(1) } } Shortcut { sequence: "Ctrl+D" onActivated: { + if (root.currentConfirmDialog()) return + if (detailsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsLayerOpen || settingsSheet.opened || aboutSheet.opened) return + if (sidebar.modal && sidebar.opened) return if (root.selectedGame) { detailsSheet.open() } @@ -257,26 +510,38 @@ Kirigami.ApplicationWindow { Shortcut { sequence: "F9" - onActivated: root.cycleUiMode() + onActivated: { + if (root.currentConfirmDialog()) return + root.cycleUiMode() + } } Connections { target: GamepadManager + function onSelectPressed() { + let d = root.currentConfirmDialog() + if (!d) return + root.activateFocusedInDialog(d) + } function onBackPressed() { root.closeTopmost() } function onLeftBumperPressed() { - if (detailsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsLayerOpen || aboutSheet.opened) return + if (root.currentConfirmDialog()) return + if (detailsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsLayerOpen || settingsSheet.opened || aboutSheet.opened) return if (root.hintContext !== "library" && root.hintContext !== "sidebar") return sidebarView.cycleSource(-1) } function onRightBumperPressed() { - if (detailsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsLayerOpen || aboutSheet.opened) return + if (root.currentConfirmDialog()) return + if (detailsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsLayerOpen || settingsSheet.opened || aboutSheet.opened) return if (root.hintContext !== "library" && root.hintContext !== "sidebar") return sidebarView.cycleSource(1) } function onDetailsPressed() { - if (gameEditDialog.visible || importSheet.opened || settingsLayerOpen || aboutSheet.opened || (sidebar.modal && sidebar.opened)) return + if (root.currentConfirmDialog()) return + if (gameEditDialog.visible || importSheet.opened || settingsLayerOpen || settingsSheet.opened || aboutSheet.opened || (sidebar.modal && sidebar.opened)) return + if (root.hintContext !== "library" && root.hintContext !== "details") return if (detailsSheet.opened) { if (root.selectedGame) { root.selectedGame.favorite = !root.selectedGame.favorite @@ -287,6 +552,84 @@ Kirigami.ApplicationWindow { libraryView.openDetailsForFocusedGame() } function onMenuPressed() { + if (root.isCouchMode) { + if (libraryView && libraryView.anyMenuOpen) { + libraryView.closeCurrentMenu() + return + } + if (removeGameConfirmDialog.opened) { + root.pendingSidebarOpen = true + removeGameConfirmDialog.close() + root.pendingRemoveGame = null + Qt.callLater(function() { libraryView.restoreFocus() }) + return + } + { + let layerContent = root.settingsLayerContentItem() + if (settingsLayerOpen && layerContent && layerContent.anyConfirmOpen) { + root.pendingSidebarOpen = true + root.closeSettings() + return + } + if (settingsLayerOpen && layerContent && layerContent.anyMenuOpen) { + layerContent.closeCurrentMenu() + return + } + } + if (settingsSheet.opened && settingsContent.anyConfirmOpen) { + root.pendingSidebarOpen = true + settingsSheet.close() + return + } + if (settingsSheet.opened && settingsContent.anyMenuOpen) { + settingsContent.closeCurrentMenu() + return + } + if (settingsSheet.opened) { + root.pendingSidebarOpen = true + settingsSheet.close() + return + } + if (settingsLayerOpen) { + root.pendingSidebarOpen = true + root.closeSettings() + return + } + if (gameEditDialog.visible) { + root.pendingSidebarOpen = true + gameEditDialog.close() + return + } + if (importSheet.opened) { + root.pendingSidebarOpen = true + importSheet.close() + return + } + if (aboutSheet.opened) { + root.pendingSidebarOpen = true + aboutSheet.close() + return + } + if (detailsSheet.opened) { + root.pendingSidebarOpen = true + detailsSheet.close() + return + } + if (sidebar.modal) { + if (sidebar.opened) { + sidebar.close() + } else { + sidebar.open() + } + return + } + } + + if (settingsSheet.opened) { + settingsSheet.close() + return + } + if (!settingsLayerOpen) { if (root.canOpenSettings()) { root.openSettings() @@ -301,14 +644,36 @@ Kirigami.ApplicationWindow { } } function onSearchPressed() { + if (root.currentConfirmDialog()) return + if (root.hintContext !== "library" && root.hintContext !== "details") return if (detailsSheet.opened) { detailsSheet.editRequested() return } - if (gameEditDialog.visible || importSheet.opened || settingsLayerOpen || aboutSheet.opened || (sidebar.modal && sidebar.opened)) return + if (gameEditDialog.visible || importSheet.opened || settingsLayerOpen || settingsSheet.opened || aboutSheet.opened || (sidebar.modal && sidebar.opened)) return searchActive = true Qt.callLater(function() { libraryView.focusSearch() }) } + function onNavigateUp() { + let d = root.currentConfirmDialog() + if (!d) return + root.focusNextInDialog(d, false) + } + function onNavigateDown() { + let d = root.currentConfirmDialog() + if (!d) return + root.focusNextInDialog(d, true) + } + function onNavigateLeft() { + let d = root.currentConfirmDialog() + if (!d) return + root.focusNextInDialog(d, false) + } + function onNavigateRight() { + let d = root.currentConfirmDialog() + if (!d) return + root.focusNextInDialog(d, true) + } } Connections { @@ -324,10 +689,10 @@ Kirigami.ApplicationWindow { globalDrawer: Kirigami.OverlayDrawer { id: sidebar edge: Qt.LeftEdge - modal: root.isMobile || root.isNarrowScreen + modal: root.isMobile || root.isNarrowScreen || root.isCouchMode handleClosedIcon.source: "application-menu" handleOpenIcon.source: "go-previous" - handleVisible: root.isMobile || root.isNarrowScreen + handleVisible: root.isMobile || root.isNarrowScreen || root.isCouchMode width: { if (root.isMobile) return Math.min(root.width * 0.85, Kirigami.Units.gridUnit * 20) @@ -343,7 +708,14 @@ Kirigami.ApplicationWindow { } onOpened: sidebarView.focusList() - onClosed: Qt.callLater(function() { libraryView.restoreFocus() }) + onClosed: { + if (root.pendingAboutOpen) { + root.pendingAboutOpen = false + Qt.callLater(function() { aboutSheet.open() }) + return + } + Qt.callLater(function() { libraryView.restoreFocus() }) + } leftPadding: 0 rightPadding: 0 @@ -411,7 +783,9 @@ Kirigami.ApplicationWindow { } onAboutRequested: { if (sidebar.modal) { + root.pendingAboutOpen = true sidebar.close() + return } aboutSheet.open() } @@ -516,6 +890,18 @@ Kirigami.ApplicationWindow { ? i18n("Are you sure you want to remove '%1'?", root.pendingRemoveGame.name) : i18n("Are you sure you want to remove this game?") standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No + onOpened: { + Qt.callLater(function() { + if (typeof removeGameConfirmDialog.standardButton === "function") { + let noButton = removeGameConfirmDialog.standardButton(Kirigami.Dialog.No) + if (noButton) { + noButton.forceActiveFocus() + return + } + } + removeGameConfirmDialog.forceActiveFocus() + }) + } onAccepted: { if (!root.pendingRemoveGame) return let gameId = root.pendingRemoveGame.id @@ -559,6 +945,11 @@ Kirigami.ApplicationWindow { if (p === ancestor) return true if (ancestor.contentItem && p === ancestor.contentItem) return true if (ancestor.header && p === ancestor.header) return true + if (p.visualParent !== undefined && p.visualParent !== null) { + if (importSheet.isDescendant(p.visualParent, ancestor)) return true + } else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) { + if (importSheet.isDescendant(p.popup.visualParent, ancestor)) return true + } p = p.parent } return false @@ -587,7 +978,7 @@ Kirigami.ApplicationWindow { item.toggle() return } - if (item.hasOwnProperty("checked")) { + if (item.checkable !== undefined && item.checkable && item.checked !== undefined) { item.checked = !item.checked return } @@ -600,6 +991,8 @@ Kirigami.ApplicationWindow { Connections { target: GamepadManager function onNavigateUp() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return if (!importSheet.opened || importScroll.activeFocus) return let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -607,6 +1000,8 @@ Kirigami.ApplicationWindow { importSheet.focusNextInChain(false) } function onNavigateDown() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return if (!importSheet.opened || importScroll.activeFocus) return let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -614,6 +1009,8 @@ Kirigami.ApplicationWindow { importSheet.focusNextInChain(true) } function onNavigateLeft() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return if (!importSheet.opened || importScroll.activeFocus) return let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -621,6 +1018,8 @@ Kirigami.ApplicationWindow { importSheet.focusNextInChain(false) } function onNavigateRight() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return if (!importSheet.opened || importScroll.activeFocus) return let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -628,6 +1027,8 @@ Kirigami.ApplicationWindow { importSheet.focusNextInChain(true) } function onSelectPressed() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return if (!importSheet.opened || importScroll.activeFocus) return let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -671,8 +1072,16 @@ Kirigami.ApplicationWindow { Connections { target: GamepadManager - function onNavigateUp() { if (importScroll.activeFocus) importScroll.scrollBy(-Kirigami.Units.gridUnit * 2) } - function onNavigateDown() { if (importScroll.activeFocus) importScroll.scrollBy(Kirigami.Units.gridUnit * 2) } + function onNavigateUp() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return + if (importScroll.activeFocus) importScroll.scrollBy(-Kirigami.Units.gridUnit * 2) + } + function onNavigateDown() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return + if (importScroll.activeFocus) importScroll.scrollBy(Kirigami.Units.gridUnit * 2) + } } ColumnLayout { @@ -843,7 +1252,6 @@ Kirigami.ApplicationWindow { onClicked: importSheet.close() } } - } } } @@ -853,7 +1261,8 @@ Kirigami.ApplicationWindow { Kirigami.Page { id: settingsLayerPage title: i18n("Settings") - padding: 0 + + property Item content: settingsLayerContent actions: [ Kirigami.Action { @@ -873,6 +1282,11 @@ Kirigami.ApplicationWindow { let p = item while (p) { if (p === ancestor) return true + if (p.visualParent !== undefined && p.visualParent !== null) { + if (settingsLayerPage.isDescendant(p.visualParent, ancestor)) return true + } else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) { + if (settingsLayerPage.isDescendant(p.popup.visualParent, ancestor)) return true + } p = p.parent } return false @@ -887,7 +1301,7 @@ Kirigami.ApplicationWindow { if (!next) return if (settingsLayerPage.isDescendant(next, settingsLayerPage)) { next.forceActiveFocus() - if (settingsLayerPage.isDescendant(next, settingsLayerContent)) { + if (settingsLayerPage.isActualDescendant(next, settingsLayerContent)) { settingsLayerScroll.ensureItemVisible(next) } return @@ -895,6 +1309,15 @@ Kirigami.ApplicationWindow { } } + function isActualDescendant(item, ancestor) { + let p = item + while (p) { + if (p === ancestor) return true + p = p.parent + } + return false + } + function activateFocused() { let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -903,7 +1326,7 @@ Kirigami.ApplicationWindow { item.toggle() return } - if (item.hasOwnProperty("checked")) { + if (item.checkable !== undefined && item.checkable && item.checked !== undefined) { item.checked = !item.checked return } @@ -916,6 +1339,10 @@ Kirigami.ApplicationWindow { Connections { target: GamepadManager function onNavigateUp() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return + if (settingsLayerContent.anyConfirmOpen) return + if (settingsLayerContent.anyMenuOpen) return if (!root.settingsLayerOpen || settingsLayerScroll.activeFocus) return let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -923,6 +1350,32 @@ Kirigami.ApplicationWindow { settingsLayerPage.focusNextInChain(false) } function onNavigateDown() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return + if (settingsLayerContent.anyConfirmOpen) return + if (settingsLayerContent.anyMenuOpen) return + if (!root.settingsLayerOpen || settingsLayerScroll.activeFocus) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!settingsLayerPage.isDescendant(w.activeFocusItem, settingsLayerPage)) return + settingsLayerPage.focusNextInChain(true) + } + function onNavigateLeft() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return + if (settingsLayerContent.anyConfirmOpen) return + if (settingsLayerContent.anyMenuOpen) return + if (!root.settingsLayerOpen || settingsLayerScroll.activeFocus) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!settingsLayerPage.isDescendant(w.activeFocusItem, settingsLayerPage)) return + settingsLayerPage.focusNextInChain(false) + } + function onNavigateRight() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return + if (settingsLayerContent.anyConfirmOpen) return + if (settingsLayerContent.anyMenuOpen) return if (!root.settingsLayerOpen || settingsLayerScroll.activeFocus) return let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -930,6 +1383,10 @@ Kirigami.ApplicationWindow { settingsLayerPage.focusNextInChain(true) } function onSelectPressed() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return + if (settingsLayerContent.anyConfirmOpen) return + if (settingsLayerContent.anyMenuOpen) return if (!root.settingsLayerOpen || settingsLayerScroll.activeFocus) return let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -977,8 +1434,16 @@ Kirigami.ApplicationWindow { Connections { target: GamepadManager - function onNavigateUp() { if (root.settingsLayerOpen && settingsLayerScroll.activeFocus) settingsLayerScroll.scrollBy(-Kirigami.Units.gridUnit * 2) } - function onNavigateDown() { if (root.settingsLayerOpen && settingsLayerScroll.activeFocus) settingsLayerScroll.scrollBy(Kirigami.Units.gridUnit * 2) } + function onNavigateUp() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return + if (root.settingsLayerOpen && settingsLayerScroll.activeFocus) settingsLayerScroll.scrollBy(-Kirigami.Units.gridUnit * 2) + } + function onNavigateDown() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return + if (root.settingsLayerOpen && settingsLayerScroll.activeFocus) settingsLayerScroll.scrollBy(Kirigami.Units.gridUnit * 2) + } } SettingsPage { @@ -1080,6 +1545,11 @@ Kirigami.ApplicationWindow { if (p === ancestor) return true if (ancestor.contentItem && p === ancestor.contentItem) return true if (ancestor.header && p === ancestor.header) return true + if (p.visualParent !== undefined && p.visualParent !== null) { + if (settingsSheet.isDescendant(p.visualParent, ancestor)) return true + } else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) { + if (settingsSheet.isDescendant(p.popup.visualParent, ancestor)) return true + } p = p.parent } return false @@ -1094,7 +1564,7 @@ Kirigami.ApplicationWindow { if (!next) return if (settingsSheet.isDescendant(next, settingsSheet)) { next.forceActiveFocus() - if (settingsSheet.isDescendant(next, settingsContent)) { + if (settingsSheet.isActualDescendant(next, settingsContent)) { settingsScroll.ensureItemVisible(next) } return @@ -1102,6 +1572,15 @@ Kirigami.ApplicationWindow { } } + function isActualDescendant(item, ancestor) { + let p = item + while (p) { + if (p === ancestor) return true + p = p.parent + } + return false + } + function activateFocused() { let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -1110,7 +1589,7 @@ Kirigami.ApplicationWindow { item.toggle() return } - if (item.hasOwnProperty("checked")) { + if (item.checkable !== undefined && item.checkable && item.checked !== undefined) { item.checked = !item.checked return } @@ -1123,15 +1602,58 @@ Kirigami.ApplicationWindow { Connections { target: GamepadManager function onNavigateUp() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return + if (settingsContent.anyConfirmOpen) return + if (settingsContent.anyMenuOpen) return if (!settingsSheet.opened || settingsScroll.activeFocus) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!settingsSheet.isDescendant(w.activeFocusItem, settingsSheet)) return settingsSheet.focusNextInChain(false) } function onNavigateDown() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return + if (settingsContent.anyConfirmOpen) return + if (settingsContent.anyMenuOpen) return if (!settingsSheet.opened || settingsScroll.activeFocus) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!settingsSheet.isDescendant(w.activeFocusItem, settingsSheet)) return + settingsSheet.focusNextInChain(true) + } + function onNavigateLeft() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return + if (settingsContent.anyConfirmOpen) return + if (settingsContent.anyMenuOpen) return + if (!settingsSheet.opened || settingsScroll.activeFocus) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!settingsSheet.isDescendant(w.activeFocusItem, settingsSheet)) return + settingsSheet.focusNextInChain(false) + } + function onNavigateRight() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return + if (settingsContent.anyConfirmOpen) return + if (settingsContent.anyMenuOpen) return + if (!settingsSheet.opened || settingsScroll.activeFocus) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!settingsSheet.isDescendant(w.activeFocusItem, settingsSheet)) return settingsSheet.focusNextInChain(true) } function onSelectPressed() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return + if (settingsContent.anyConfirmOpen) return + if (settingsContent.anyMenuOpen) return if (!settingsSheet.opened || settingsScroll.activeFocus) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!settingsSheet.isDescendant(w.activeFocusItem, settingsSheet)) return settingsSheet.activateFocused() } } @@ -1174,8 +1696,16 @@ Kirigami.ApplicationWindow { Connections { target: GamepadManager - function onNavigateUp() { if (settingsScroll.activeFocus) settingsScroll.scrollBy(-Kirigami.Units.gridUnit * 2) } - function onNavigateDown() { if (settingsScroll.activeFocus) settingsScroll.scrollBy(Kirigami.Units.gridUnit * 2) } + function onNavigateUp() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return + if (settingsScroll.activeFocus) settingsScroll.scrollBy(-Kirigami.Units.gridUnit * 2) + } + function onNavigateDown() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return + if (settingsScroll.activeFocus) settingsScroll.scrollBy(Kirigami.Units.gridUnit * 2) + } } SettingsPage { @@ -1189,6 +1719,8 @@ Kirigami.ApplicationWindow { id: aboutSheet title: i18n("About") + parent: applicationWindow().overlay + closePolicy: QQC2.Popup.CloseOnEscape | QQC2.Popup.CloseOnPressOutside implicitWidth: { @@ -1210,6 +1742,11 @@ Kirigami.ApplicationWindow { if (p === ancestor) return true if (ancestor.contentItem && p === ancestor.contentItem) return true if (ancestor.header && p === ancestor.header) return true + if (p.visualParent !== undefined && p.visualParent !== null) { + if (aboutSheet.isDescendant(p.visualParent, ancestor)) return true + } else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) { + if (aboutSheet.isDescendant(p.popup.visualParent, ancestor)) return true + } p = p.parent } return false @@ -1237,7 +1774,7 @@ Kirigami.ApplicationWindow { item.toggle() return } - if (item.hasOwnProperty("checked")) { + if (item.checkable !== undefined && item.checkable && item.checked !== undefined) { item.checked = !item.checked return } @@ -1250,6 +1787,8 @@ Kirigami.ApplicationWindow { Connections { target: GamepadManager function onNavigateUp() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return if (!aboutSheet.opened) return let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -1257,6 +1796,8 @@ Kirigami.ApplicationWindow { aboutSheet.focusNextInChain(false) } function onNavigateDown() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return if (!aboutSheet.opened) return let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -1264,6 +1805,8 @@ Kirigami.ApplicationWindow { aboutSheet.focusNextInChain(true) } function onNavigateLeft() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return if (!aboutSheet.opened) return let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -1271,6 +1814,8 @@ Kirigami.ApplicationWindow { aboutSheet.focusNextInChain(false) } function onNavigateRight() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return if (!aboutSheet.opened) return let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -1278,6 +1823,8 @@ Kirigami.ApplicationWindow { aboutSheet.focusNextInChain(true) } function onSelectPressed() { + let app = applicationWindow() + if (app && app.currentConfirmDialog && app.currentConfirmDialog()) return if (!aboutSheet.opened) return let w = applicationWindow() if (!w || !w.activeFocusItem) return @@ -1404,3 +1951,5 @@ Kirigami.ApplicationWindow { } } } + +} diff --git a/src/qml/SettingsPage.qml b/src/qml/SettingsPage.qml index cba46d2..419747b 100644 --- a/src/qml/SettingsPage.qml +++ b/src/qml/SettingsPage.qml @@ -20,6 +20,96 @@ ColumnLayout { property var pendingDisableImportApply: null property string pendingDisableImportName: "" + readonly property bool anyConfirmOpen: disableImportConfirmDialog.opened + || fetchAllCoversConfirmDialog.opened + || removeMissingConfirmDialog.opened + || clearConfirmDialog.opened + || resetConfirmDialog.opened + + readonly property bool anyMenuOpen: uiModeMenu.visible + + function currentConfirmDialog() { + if (disableImportConfirmDialog.opened) return disableImportConfirmDialog + if (fetchAllCoversConfirmDialog.opened) return fetchAllCoversConfirmDialog + if (removeMissingConfirmDialog.opened) return removeMissingConfirmDialog + if (clearConfirmDialog.opened) return clearConfirmDialog + if (resetConfirmDialog.opened) return resetConfirmDialog + return null + } + + function closeCurrentConfirmDialog() { + let d = currentConfirmDialog() + if (!d) return + if (typeof d.reject === "function") { + d.reject() + } else { + d.close() + } + } + + function closeCurrentMenu() { + if (uiModeMenu.visible) { + uiModeMenu.close() + } + } + + function isDescendant(item, ancestor) { + let p = item + while (p) { + if (p === ancestor) return true + if (ancestor.contentItem && p === ancestor.contentItem) return true + if (p.visualParent !== undefined && p.visualParent !== null) { + if (settingsPage.isDescendant(p.visualParent, ancestor)) return true + } else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) { + if (settingsPage.isDescendant(p.popup.visualParent, ancestor)) return true + } + p = p.parent + } + return false + } + + function focusNextInMenu(forward) { + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + let next = w.activeFocusItem + for (let i = 0; i < 50; i++) { + next = next.nextItemInFocusChain(forward) + if (!next) return + if (settingsPage.isDescendant(next, uiModeMenu)) { + next.forceActiveFocus() + return + } + } + } + + function activateFocusedInMenu() { + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + let item = w.activeFocusItem + if (typeof item.triggered === "function") { + item.triggered() + return + } + if (typeof item.clicked === "function") { + item.clicked() + return + } + } + + function focusNoButton(dialog) { + Qt.callLater(function() { + if (!dialog) return + if (typeof dialog.standardButton === "function") { + let noButton = dialog.standardButton(Kirigami.Dialog.No) + if (noButton) { + noButton.forceActiveFocus() + return + } + } + dialog.forceActiveFocus() + }) + } + function requestDisableImport(sourceName, applyFn) { pendingDisableImportName = sourceName pendingDisableImportApply = applyFn @@ -48,7 +138,7 @@ ColumnLayout { text: i18n("UI mode") description: { if (App.config.uiMode === Config.Desktop) return i18n("Desktop") - if (App.config.uiMode === Config.Handheld) return i18n("Handheld") + if (App.config.uiMode === Config.Couch) return i18n("Couch") return i18n("Automatic") } icon.name: "view-fullscreen" @@ -56,8 +146,12 @@ ColumnLayout { QQC2.Menu { id: uiModeMenu + focus: true + + onOpened: Qt.callLater(function() { uiModeAuto.forceActiveFocus() }) QQC2.MenuItem { + id: uiModeAuto text: i18n("Automatic") checkable: true checked: App.config.uiMode === Config.Auto @@ -65,6 +159,7 @@ ColumnLayout { } QQC2.MenuItem { + id: uiModeDesktop text: i18n("Desktop") checkable: true checked: App.config.uiMode === Config.Desktop @@ -72,15 +167,55 @@ ColumnLayout { } QQC2.MenuItem { - text: i18n("Handheld") + id: uiModeCouch + text: i18n("Couch") checkable: true - checked: App.config.uiMode === Config.Handheld - onTriggered: App.config.uiMode = Config.Handheld + checked: App.config.uiMode === Config.Couch + onTriggered: App.config.uiMode = Config.Couch } } } } + Connections { + target: GamepadManager + function onNavigateUp() { + if (!uiModeMenu.visible) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!settingsPage.isDescendant(w.activeFocusItem, uiModeMenu)) return + settingsPage.focusNextInMenu(false) + } + function onNavigateDown() { + if (!uiModeMenu.visible) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!settingsPage.isDescendant(w.activeFocusItem, uiModeMenu)) return + settingsPage.focusNextInMenu(true) + } + function onNavigateLeft() { + if (!uiModeMenu.visible) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!settingsPage.isDescendant(w.activeFocusItem, uiModeMenu)) return + settingsPage.focusNextInMenu(false) + } + function onNavigateRight() { + if (!uiModeMenu.visible) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!settingsPage.isDescendant(w.activeFocusItem, uiModeMenu)) return + settingsPage.focusNextInMenu(true) + } + function onSelectPressed() { + if (!uiModeMenu.visible) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!settingsPage.isDescendant(w.activeFocusItem, uiModeMenu)) return + settingsPage.activateFocusedInMenu() + } + } + FormCard.FormHeader { Layout.topMargin: Kirigami.Units.mediumSpacing Layout.fillWidth: true @@ -553,6 +688,7 @@ FormCard.FormHeader { title: i18n("Disable Import Source") subtitle: i18n("Disabling %1 will remove all games imported from that source. Are you sure?", settingsPage.pendingDisableImportName) standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No + onOpened: settingsPage.focusNoButton(disableImportConfirmDialog) onAccepted: { if (settingsPage.pendingDisableImportApply) { settingsPage.pendingDisableImportApply() @@ -573,6 +709,7 @@ FormCard.FormHeader { ? i18n("This will download cover art for all games and may replace existing covers. Continue?") : i18n("This will download cover art for games that are missing covers. Continue?") standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No + onOpened: settingsPage.focusNoButton(fetchAllCoversConfirmDialog) onAccepted: App.steamGridDB.fetchAllCovers() } @@ -581,6 +718,7 @@ FormCard.FormHeader { title: i18n("Remove Missing Games") subtitle: i18n("This will remove games whose executables cannot be found. This cannot be undone. Continue?") standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No + onOpened: settingsPage.focusNoButton(removeMissingConfirmDialog) onAccepted: App.removeMissingGames() } @@ -589,6 +727,7 @@ FormCard.FormHeader { title: i18n("Clear Library") subtitle: i18n("Are you sure you want to remove all games?") standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No + onOpened: settingsPage.focusNoButton(clearConfirmDialog) onAccepted: App.clearLibrary() } @@ -597,6 +736,7 @@ FormCard.FormHeader { title: i18n("Reset Application") subtitle: i18n("This will remove all games and reset all settings to defaults. Are you sure?") standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.No + onOpened: settingsPage.focusNoButton(resetConfirmDialog) onAccepted: { App.clearLibrary() App.config.resetToDefaults() diff --git a/src/qml/SidebarView.qml b/src/qml/SidebarView.qml index 756d327..bf40100 100644 --- a/src/qml/SidebarView.qml +++ b/src/qml/SidebarView.qml @@ -16,6 +16,8 @@ ColumnLayout { property string currentSourceName: i18n("All Games") property bool suppressAutoClose: false + readonly property bool hasSidebarFocus: sourceList.activeFocus || importAction.activeFocus || settingsAction.activeFocus || aboutAction.activeFocus + readonly property int adaptiveFocusRingWidth: 1 signal sourceSelected(string source) @@ -45,6 +47,17 @@ ColumnLayout { sourceList.positionViewAtIndex(i, ListView.Contain) } + function applySourceById(sourceId) { + for (let i = 0; i < sourceModel.count; i++) { + let item = sourceModel.get(i) + if (item && item.sourceId === sourceId) { + applySourceAtIndex(i) + return + } + } + applySourceAtIndex(0) + } + function cycleSource(delta) { if (sourceModel.count <= 0) return let i = sourceList.currentIndex @@ -97,9 +110,31 @@ ColumnLayout { Connections { target: GamepadManager - function onNavigateUp() { if (sourceList.activeFocus) sourceList.decrementCurrentIndex() } - function onNavigateDown() { if (sourceList.activeFocus) sourceList.incrementCurrentIndex() } - function onSelectPressed() { if (sourceList.activeFocus) sidebarRoot.activateCurrentItem() } + function onNavigateUp() { + let w = applicationWindow() + if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return + if (!sourceList.activeFocus) return + if (sourceList.count > 0 && sourceList.currentIndex <= 0) { + aboutAction.forceActiveFocus() + } else { + sourceList.decrementCurrentIndex() + } + } + function onNavigateDown() { + let w = applicationWindow() + if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return + if (!sourceList.activeFocus) return + if (sourceList.count > 0 && sourceList.currentIndex >= sourceList.count - 1) { + importAction.forceActiveFocus() + } else { + sourceList.incrementCurrentIndex() + } + } + function onSelectPressed() { + let w = applicationWindow() + if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return + if (sourceList.activeFocus) sidebarRoot.activateCurrentItem() + } } delegate: QQC2.ItemDelegate { @@ -294,6 +329,8 @@ ColumnLayout { Connections { target: GamepadManager function onNavigateUp() { + let w = applicationWindow() + if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return if (aboutAction.activeFocus) { settingsAction.forceActiveFocus() } else if (settingsAction.activeFocus) { @@ -304,13 +341,20 @@ ColumnLayout { } } function onNavigateDown() { + let w = applicationWindow() + if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return if (importAction.activeFocus) { settingsAction.forceActiveFocus() } else if (settingsAction.activeFocus) { aboutAction.forceActiveFocus() + } else if (aboutAction.activeFocus) { + sourceList.forceActiveFocus() + sourceList.currentIndex = 0 } } function onSelectPressed() { + let w = applicationWindow() + if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return if (importAction.activeFocus) { importAction.clicked() } else if (settingsAction.activeFocus) { diff --git a/src/qml/components/BottomHintBar.qml b/src/qml/components/BottomHintBar.qml index ebc77c8..9bfc68e 100644 --- a/src/qml/components/BottomHintBar.qml +++ b/src/qml/components/BottomHintBar.qml @@ -7,9 +7,19 @@ import org.kde.alakarte RowLayout { id: root - spacing: Kirigami.Units.largeSpacing + spacing: uiMode === Config.Couch ? Kirigami.Units.largeSpacing * 1.25 : Kirigami.Units.largeSpacing - readonly property bool useGamepadHints: GamepadManager.connected + property int uiMode: Config.Auto + property int activeInput: InputManager.KeyboardMouse + + readonly property bool useGamepadHints: { + if (!GamepadManager.connected) return false + if (uiMode === Config.Couch) { + if (activeInput === InputManager.KeyboardMouse && InputManager.hasSeenKeyboardMouse) return false + return true + } + return activeInput === InputManager.Gamepad + } readonly property int style: GamepadManager.controllerStyle property string context: "library" @@ -49,6 +59,9 @@ RowLayout { function iconForAux(action) { if (!useGamepadHints) return "" + if (action === "dpad") { + return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/generic/dpad.svg" + } if (action === "lb") { return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/generic/lb.svg" } @@ -72,12 +85,25 @@ RowLayout { } function keyboardLabel(action) { + if (root.context === "confirm" || root.context === "confirm_remove") { + switch (action) { + case "navigate": return "Tab" + case "confirm": return "Enter" + case "back": return "Esc" + case "menu": return "" + default: return "" + } + } + if (root.context === "library") { switch (action) { + case "navigate": return i18n("Arrows") case "confirm": return "Space" - case "back": return "Esc" + case "back": return "" case "details": return "Enter" case "search": return "Ctrl+F" + case "lb": return "Ctrl+PgUp" + case "rb": return "Ctrl+PgDown" case "menu": return "Ctrl+," default: return "" } @@ -85,6 +111,7 @@ RowLayout { if (root.context === "edit") { switch (action) { + case "navigate": return "Tab" case "confirm": return "Enter" case "back": return "Esc" default: return "" @@ -93,8 +120,11 @@ RowLayout { if (root.context === "details") { switch (action) { + case "navigate": return "Tab" case "confirm": return "Enter" case "back": return "Esc" + case "details": return "F" + case "search": return "E" case "menu": return "Ctrl+," default: return "" } @@ -102,6 +132,19 @@ RowLayout { if (root.context === "sidebar") { switch (action) { + case "navigate": return i18n("Arrows/Tab") + case "confirm": return "Enter" + case "back": return "Esc" + case "lb": return "Ctrl+PgUp" + case "rb": return "Ctrl+PgDown" + case "menu": return "Ctrl+," + default: return "" + } + } + + if (root.context === "settings") { + switch (action) { + case "navigate": return "Tab" case "confirm": return "Enter" case "back": return "Esc" case "menu": return "Ctrl+," @@ -109,8 +152,9 @@ RowLayout { } } - if (root.context === "settings" || root.context === "import" || root.context === "sidebar") { + if (root.context === "settings" || root.context === "import" || root.context === "sidebar" || root.context === "about") { switch (action) { + case "navigate": return "Tab" case "confirm": return "Enter" case "back": return "Esc" default: return "" @@ -121,53 +165,90 @@ RowLayout { } function actionLabel(action) { + if (root.context === "confirm" || root.context === "confirm_remove") { + switch (action) { + case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : "" + case "confirm": return i18n("Select") + case "back": return i18n("Cancel") + case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : "" + default: return "" + } + } + if (root.context === "library") { switch (action) { + case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : "" case "confirm": return i18n("Play") - case "back": return i18n("Back") + case "back": return "" case "details": return i18n("Details") case "search": return i18n("Search") case "lb": return i18n("Prev Source") case "rb": return i18n("Next Source") - case "menu": return i18n("Settings") + case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : i18n("Settings") + default: return "" + } + } + + if (root.context === "settings") { + switch (action) { + case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : "" + case "confirm": return i18n("Select") + case "back": return i18n("Back") + case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : i18n("Close") default: return "" } } if (root.context === "edit") { switch (action) { + case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : "" case "confirm": return i18n("Select") case "back": return i18n("Back") + case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : "" default: return "" } } if (root.context === "details") { switch (action) { - case "confirm": return i18n("Play") + case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : "" + case "confirm": return i18n("Select") case "back": return i18n("Back") case "details": return i18n("Favorite") case "search": return i18n("Edit") - case "menu": return i18n("Settings") + case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : i18n("Settings") default: return "" } } if (root.context === "sidebar") { switch (action) { + case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : "" case "confirm": return i18n("Select") case "back": return i18n("Back") case "lb": return i18n("Prev Source") case "rb": return i18n("Next Source") - case "menu": return i18n("Settings") + case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Close") : i18n("Settings") default: return "" } } if (root.context === "settings" || root.context === "import" || root.context === "sidebar") { switch (action) { + case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : "" case "confirm": return i18n("Select") case "back": return i18n("Back") + case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : "" + default: return "" + } + } + + if (root.context === "about") { + switch (action) { + case "navigate": return (useGamepadHints || keyboardLabel("navigate") !== "") ? i18n("Navigate") : "" + case "confirm": return i18n("Select") + case "back": return i18n("Back") + case "menu": return (useGamepadHints && uiMode === Config.Couch) ? i18n("Menu") : "" default: return "" } } @@ -195,7 +276,7 @@ RowLayout { border.width: 1 border.color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2) - Layout.preferredHeight: Kirigami.Units.gridUnit * 1.4 + Layout.preferredHeight: uiMode === Config.Couch ? Kirigami.Units.gridUnit * 1.8 : Kirigami.Units.gridUnit * 1.4 Layout.preferredWidth: useGamepadHints ? Layout.preferredHeight : Math.max(keyText.implicitWidth + Kirigami.Units.mediumSpacing * 2, Layout.preferredHeight) @@ -215,7 +296,7 @@ RowLayout { anchors.centerIn: parent text: parent.parent.keyLabel font.bold: true - font.pointSize: Kirigami.Theme.smallFont.pointSize + font.pointSize: uiMode === Config.Couch ? Kirigami.Theme.defaultFont.pointSize : Kirigami.Theme.smallFont.pointSize color: Kirigami.Theme.textColor visible: !useGamepadHints } @@ -224,11 +305,18 @@ RowLayout { QQC2.Label { text: parent.label color: Kirigami.Theme.textColor - font.pointSize: Kirigami.Theme.smallFont.pointSize + font.pointSize: uiMode === Config.Couch ? Kirigami.Theme.defaultFont.pointSize : Kirigami.Theme.smallFont.pointSize Layout.alignment: Qt.AlignVCenter } } + HintItem { + action: "navigate" + label: actionLabel("navigate") + iconSource: iconForAux("dpad") + keyLabel: keyboardLabel("navigate") + } + HintItem { action: "confirm" label: actionLabel("confirm") @@ -261,20 +349,22 @@ RowLayout { action: "lb" label: actionLabel("lb") iconSource: (root.context === "library" || root.context === "sidebar") ? iconForAux("lb") : "" - keyLabel: "" + keyLabel: keyboardLabel("lb") } HintItem { action: "rb" label: actionLabel("rb") iconSource: (root.context === "library" || root.context === "sidebar") ? iconForAux("rb") : "" - keyLabel: "" + keyLabel: keyboardLabel("rb") } HintItem { action: "menu" label: actionLabel("menu") - iconSource: (root.context === "library" || root.context === "details" || root.context === "sidebar") ? iconForAux("menu") : "" + iconSource: (useGamepadHints && uiMode === Config.Couch) + ? iconForAux("menu") + : ((root.context === "library" || root.context === "details" || root.context === "sidebar" || root.context === "settings") ? iconForAux("menu") : "") keyLabel: keyboardLabel("menu") } } diff --git a/src/qml/components/GameGridView.qml b/src/qml/components/GameGridView.qml index b69d8b0..611e491 100644 --- a/src/qml/components/GameGridView.qml +++ b/src/qml/components/GameGridView.qml @@ -74,11 +74,29 @@ GridView { Connections { target: GamepadManager - function onNavigateUp() { if (gridView.activeFocus) gridView.navigateUp() } - function onNavigateDown() { if (gridView.activeFocus) gridView.navigateDown() } - function onNavigateLeft() { if (gridView.activeFocus) gridView.navigateLeft() } - function onNavigateRight() { if (gridView.activeFocus) gridView.navigateRight() } + function onNavigateUp() { + let w = applicationWindow() + if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return + if (gridView.activeFocus) gridView.navigateUp() + } + function onNavigateDown() { + let w = applicationWindow() + if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return + if (gridView.activeFocus) gridView.navigateDown() + } + function onNavigateLeft() { + let w = applicationWindow() + if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return + if (gridView.activeFocus) gridView.navigateLeft() + } + function onNavigateRight() { + let w = applicationWindow() + if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return + if (gridView.activeFocus) gridView.navigateRight() + } function onSelectPressed() { + let w = applicationWindow() + if (w && w.currentConfirmDialog && w.currentConfirmDialog()) return if (!gridView.activeFocus) { return } diff --git a/src/qml/components/SearchHeader.qml b/src/qml/components/SearchHeader.qml index 503d863..818dfe7 100644 --- a/src/qml/components/SearchHeader.qml +++ b/src/qml/components/SearchHeader.qml @@ -13,6 +13,57 @@ QQC2.ToolBar { property alias searchField: searchFieldContainer.data property int currentSortMode: 0 + readonly property bool anyMenuOpen: sortMenu.visible + + function closeCurrentMenu() { + if (sortMenu.visible) { + sortMenu.close() + } + } + + function isDescendant(item, ancestor) { + let p = item + while (p) { + if (p === ancestor) return true + if (ancestor.contentItem && p === ancestor.contentItem) return true + if (p.visualParent !== undefined && p.visualParent !== null) { + if (searchHeader.isDescendant(p.visualParent, ancestor)) return true + } else if (p.popup !== undefined && p.popup !== null && p.popup.visualParent !== undefined && p.popup.visualParent !== null) { + if (searchHeader.isDescendant(p.popup.visualParent, ancestor)) return true + } + p = p.parent + } + return false + } + + function focusNextInMenu(forward) { + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + let next = w.activeFocusItem + for (let i = 0; i < 50; i++) { + next = next.nextItemInFocusChain(forward) + if (!next) return + if (searchHeader.isDescendant(next, sortMenu)) { + next.forceActiveFocus() + return + } + } + } + + function activateFocusedInMenu() { + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + let item = w.activeFocusItem + if (typeof item.triggered === "function") { + item.triggered() + return + } + if (typeof item.clicked === "function") { + item.clicked() + return + } + } + signal searchChanged(string text) signal sortChanged(int mode) @@ -47,8 +98,12 @@ QQC2.ToolBar { QQC2.Menu { id: sortMenu + focus: true + + onOpened: Qt.callLater(function() { sortModeLastPlayed.forceActiveFocus() }) QQC2.MenuItem { + id: sortModeLastPlayed text: i18n("Last Played") checkable: true checked: searchHeader.currentSortMode === 0 @@ -59,6 +114,7 @@ QQC2.ToolBar { } QQC2.MenuItem { + id: sortModeName text: i18n("Name") checkable: true checked: searchHeader.currentSortMode === 1 @@ -69,6 +125,7 @@ QQC2.ToolBar { } QQC2.MenuItem { + id: sortModePlayTime text: i18n("Play Time") checkable: true checked: searchHeader.currentSortMode === 2 @@ -80,4 +137,43 @@ QQC2.ToolBar { } } } + + Connections { + target: GamepadManager + function onNavigateUp() { + if (!sortMenu.visible) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!searchHeader.isDescendant(w.activeFocusItem, sortMenu)) return + searchHeader.focusNextInMenu(false) + } + function onNavigateDown() { + if (!sortMenu.visible) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!searchHeader.isDescendant(w.activeFocusItem, sortMenu)) return + searchHeader.focusNextInMenu(true) + } + function onNavigateLeft() { + if (!sortMenu.visible) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!searchHeader.isDescendant(w.activeFocusItem, sortMenu)) return + searchHeader.focusNextInMenu(false) + } + function onNavigateRight() { + if (!sortMenu.visible) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!searchHeader.isDescendant(w.activeFocusItem, sortMenu)) return + searchHeader.focusNextInMenu(true) + } + function onSelectPressed() { + if (!sortMenu.visible) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!searchHeader.isDescendant(w.activeFocusItem, sortMenu)) return + searchHeader.activateFocusedInMenu() + } + } } From f02b26f6813c8120f15d79ad7102a1611c32f008 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Thu, 29 Jan 2026 19:57:07 +0100 Subject: [PATCH 3/3] Core: set QT_DESKTOP_FILE_NAME early for portals --- src/main.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index 82a72c3..53e7e25 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2026 A-La-Karte Contributors +#include #include #include #include @@ -18,6 +19,7 @@ int main(int argc, char *argv[]) { + qputenv("QT_DESKTOP_FILE_NAME", QByteArray("org.kde.alakarte")); QGuiApplication app(argc, argv); QGuiApplication::setDesktopFileName(QStringLiteral("org.kde.alakarte"));