Merge branch 'fix/couch-mode-focus' into 'master'

Fix/couch mode focus

See merge request marcoa/a-la-karte!1
This commit is contained in:
Marco Allegretti 2026-01-29 20:06:23 +00:00
commit 51e646ea85
14 changed files with 1306 additions and 123 deletions

View file

@ -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

View file

@ -42,7 +42,7 @@ public:
enum UiMode {
Auto,
Desktop,
Handheld
Couch
};
Q_ENUM(UiMode)

104
src/inputmanager.cpp Normal file
View file

@ -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 <QCoreApplication>
#include <QEvent>
#include <QGuiApplication>
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;
}

54
src/inputmanager.h Normal file
View file

@ -0,0 +1,54 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#pragma once
#include <QObject>
#include <QQmlEngine>
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);
};

View file

@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#include <QByteArray>
#include <QCommandLineParser>
#include <QGuiApplication>
#include <QIcon>
@ -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"));

View file

@ -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

View file

@ -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

View file

@ -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()
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -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()

View file

@ -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) {

View file

@ -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")
}
}

View file

@ -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
}

View file

@ -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()
}
}
}