diff --git a/src/qml/LibraryView.qml b/src/qml/LibraryView.qml index 7009993..5e510ee 100644 --- a/src/qml/LibraryView.qml +++ b/src/qml/LibraryView.qml @@ -14,7 +14,7 @@ FocusScope { property string filterSource: "all" property bool searchActive: false - property int focusedIndex: 0 + property int focusedIndex: -1 property int adaptiveCardSize: App.config.gridSize property bool isTouchDevice: false @@ -47,6 +47,10 @@ FocusScope { function launchFocusedGame() { if (!gameGrid || !proxyModel) return + if (gameGrid.currentIndex < 0 && proxyModel.count > 0) { + gameGrid.currentIndex = 0 + libraryRoot.focusedIndex = 0 + } let game = proxyModel.get(gameGrid.currentIndex) if (game) { libraryRoot.gameLaunched(game) @@ -55,6 +59,10 @@ FocusScope { function openDetailsForFocusedGame() { if (!gameGrid || !proxyModel) return + if (gameGrid.currentIndex < 0 && proxyModel.count > 0) { + gameGrid.currentIndex = 0 + libraryRoot.focusedIndex = 0 + } let game = proxyModel.get(gameGrid.currentIndex) if (game) { libraryRoot.gameSelected(game) @@ -249,12 +257,20 @@ FocusScope { Keys.onPressed: function(event) { if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + if (currentIndex < 0 && proxyModel.count > 0) { + currentIndex = 0 + libraryRoot.focusedIndex = 0 + } let game = proxyModel.get(currentIndex) if (game) { libraryRoot.gameSelected(game) } event.accepted = true } else if (event.key === Qt.Key_Space) { + if (currentIndex < 0 && proxyModel.count > 0) { + currentIndex = 0 + libraryRoot.focusedIndex = 0 + } let game = proxyModel.get(currentIndex) if (game) { libraryRoot.gameLaunched(game) diff --git a/src/qml/Main.qml b/src/qml/Main.qml index b922761..86744d6 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -21,12 +21,13 @@ Kirigami.ApplicationWindow { property var selectedGame: null property string currentSource: "all" property bool searchActive: false + property bool settingsLayerOpen: false readonly property string hintContext: { if (gameEditDialog.visible) return "edit" 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" return "library" @@ -45,10 +46,6 @@ Kirigami.ApplicationWindow { importSheet.close() return true } - if (settingsSheet.opened) { - settingsSheet.close() - return true - } if (aboutSheet.opened) { aboutSheet.close() return true @@ -76,10 +73,23 @@ Kirigami.ApplicationWindow { && !detailsSheet.opened && !importSheet.opened && !aboutSheet.opened - && !sidebar.opened + && !(sidebar.modal && sidebar.opened) && root.pageStack.layers.depth <= 1 } + function openSettings() { + if (settingsLayerOpen) return + if (!root.canOpenSettings()) return + settingsLayerOpen = true + root.pageStack.layers.push(settingsLayerComponent) + } + + function closeSettings() { + if (!settingsLayerOpen) return + root.pageStack.layers.pop() + settingsLayerOpen = false + } + function uiModeLabel(mode) { if (mode === Config.Desktop) return i18n("Desktop") if (mode === Config.Handheld) return i18n("Handheld") @@ -211,7 +221,7 @@ Kirigami.ApplicationWindow { Shortcut { sequence: StandardKey.Preferences - onActivated: settingsSheet.open() + onActivated: root.openSettings() } Shortcut { @@ -255,17 +265,17 @@ Kirigami.ApplicationWindow { root.closeTopmost() } function onLeftBumperPressed() { - if (detailsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsSheet.opened || aboutSheet.opened) return + if (detailsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsLayerOpen || aboutSheet.opened) return if (root.hintContext !== "library" && root.hintContext !== "sidebar") return sidebarView.cycleSource(-1) } function onRightBumperPressed() { - if (detailsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsSheet.opened || aboutSheet.opened) return + if (detailsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsLayerOpen || aboutSheet.opened) return if (root.hintContext !== "library" && root.hintContext !== "sidebar") return sidebarView.cycleSource(1) } function onDetailsPressed() { - if (gameEditDialog.visible || importSheet.opened || settingsSheet.opened || aboutSheet.opened || (sidebar.modal && sidebar.opened)) return + if (gameEditDialog.visible || importSheet.opened || settingsLayerOpen || aboutSheet.opened || (sidebar.modal && sidebar.opened)) return if (detailsSheet.opened) { if (root.selectedGame) { root.selectedGame.favorite = !root.selectedGame.favorite @@ -276,17 +286,17 @@ Kirigami.ApplicationWindow { libraryView.openDetailsForFocusedGame() } function onMenuPressed() { - if (!settingsSheet.opened) { + if (!settingsLayerOpen) { if (root.canOpenSettings()) { - settingsSheet.open() + root.openSettings() } else if (detailsSheet.opened) { detailsSheet.close() - Qt.callLater(function() { settingsSheet.open() }) + Qt.callLater(function() { root.openSettings() }) } else { root.closeTopmost() } } else { - settingsSheet.close() + root.closeSettings() } } function onSearchPressed() { @@ -294,7 +304,7 @@ Kirigami.ApplicationWindow { detailsSheet.editRequested() return } - if (gameEditDialog.visible || importSheet.opened || settingsSheet.opened || aboutSheet.opened || (sidebar.modal && sidebar.opened)) return + if (gameEditDialog.visible || importSheet.opened || settingsLayerOpen || aboutSheet.opened || (sidebar.modal && sidebar.opened)) return searchActive = true Qt.callLater(function() { libraryView.focusSearch() }) } @@ -304,6 +314,7 @@ Kirigami.ApplicationWindow { target: root.pageStack.layers function onDepthChanged() { if (root.pageStack.layers.depth <= 1) { + root.settingsLayerOpen = false Qt.callLater(function() { libraryView.restoreFocus() }) } } @@ -389,7 +400,7 @@ Kirigami.ApplicationWindow { if (sidebar.modal) { sidebar.close() } - settingsSheet.open() + root.openSettings() } onImportRequested: { if (sidebar.modal) { @@ -447,7 +458,7 @@ Kirigami.ApplicationWindow { icon.name: "configure" text: i18n("Settings") visible: sidebar.modal - onTriggered: settingsSheet.open() + onTriggered: root.openSettings() } ] @@ -818,20 +829,190 @@ Kirigami.ApplicationWindow { } } + Component { + id: settingsLayerComponent + + Kirigami.Page { + id: settingsLayerPage + title: i18n("Settings") + padding: 0 + + actions: [ + Kirigami.Action { + icon.name: "go-previous" + text: i18n("Close") + onTriggered: root.closeSettings() + } + ] + + Component.onCompleted: { + Qt.callLater(function() { + settingsLayerContent.focusFirstControl() + }) + } + + function isDescendant(item, ancestor) { + let p = item + while (p) { + if (p === ancestor) return true + p = p.parent + } + return false + } + + function focusNextInChain(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 (settingsLayerPage.isDescendant(next, settingsLayerPage)) { + next.forceActiveFocus() + if (settingsLayerPage.isDescendant(next, settingsLayerContent)) { + settingsLayerScroll.ensureItemVisible(next) + } + return + } + } + } + + function activateFocused() { + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + let item = w.activeFocusItem + if (typeof item.toggle === "function") { + item.toggle() + return + } + if (item.hasOwnProperty("checked")) { + item.checked = !item.checked + return + } + if (typeof item.clicked === "function") { + item.clicked() + return + } + } + + Connections { + target: GamepadManager + function onNavigateUp() { + 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 onNavigateDown() { + 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 onSelectPressed() { + if (!root.settingsLayerOpen || settingsLayerScroll.activeFocus) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!settingsLayerPage.isDescendant(w.activeFocusItem, settingsLayerPage)) return + settingsLayerPage.activateFocused() + } + } + + QQC2.ScrollView { + id: settingsLayerScroll + anchors.fill: parent + clip: true + + readonly property int horizontalContentPadding: root.isMobile ? Kirigami.Units.largeSpacing : Kirigami.Units.gridUnit + readonly property int verticalContentPadding: root.isMobile ? Kirigami.Units.largeSpacing : Kirigami.Units.gridUnit + + leftPadding: horizontalContentPadding + rightPadding: horizontalContentPadding + topPadding: verticalContentPadding + bottomPadding: verticalContentPadding + + QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff + + function ensureItemVisible(item) { + if (!item || !settingsLayerScroll.contentItem || !settingsLayerScroll.contentItem.contentItem) return + let flick = settingsLayerScroll.contentItem + let content = flick.contentItem + let p = item.mapToItem(content, 0, 0) + let itemTop = p.y + let itemBottom = p.y + item.height + let top = flick.contentY + let bottom = flick.contentY + flick.height + if (itemTop < top) { + flick.contentY = Math.max(0, itemTop) + } else if (itemBottom > bottom) { + flick.contentY = Math.max(0, itemBottom - flick.height) + } + } + + function scrollBy(delta) { + if (!settingsLayerScroll.contentItem) return + let maxY = Math.max(0, settingsLayerScroll.contentItem.contentHeight - settingsLayerScroll.contentItem.height) + settingsLayerScroll.contentItem.contentY = Math.max(0, Math.min(maxY, settingsLayerScroll.contentItem.contentY + delta)) + } + + 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) } + } + + SettingsPage { + id: settingsLayerContent + width: settingsLayerScroll.availableWidth + } + } + } + } + Kirigami.OverlaySheet { id: settingsSheet title: i18n("Settings") + parent: applicationWindow().overlay + + readonly property Item contentArea: applicationWindow().pageStack + readonly property int sideMargin: root.isMobile ? 0 : Kirigami.Units.gridUnit * 2 + readonly property int verticalMargin: root.isMobile ? 0 : Kirigami.Units.gridUnit + + readonly property point contentAreaPos: { + if (!settingsSheet.contentArea) return Qt.point(0, 0) + return settingsSheet.contentArea.mapToItem(applicationWindow().overlay, 0, 0) + } + + readonly property int usableLeft: settingsSheet.contentAreaPos.x + readonly property int usableTop: settingsSheet.contentAreaPos.y + readonly property int usableWidth: settingsSheet.contentArea ? settingsSheet.contentArea.width : applicationWindow().width + readonly property int usableHeight: settingsSheet.contentArea ? settingsSheet.contentArea.height : applicationWindow().height + + readonly property int sheetAvailableWidth: Math.max(0, settingsSheet.usableWidth - settingsSheet.sideMargin * 2) + readonly property int sheetAvailableHeight: Math.max(0, settingsSheet.usableHeight - settingsSheet.verticalMargin * 2) + closePolicy: QQC2.Popup.CloseOnEscape | QQC2.Popup.CloseOnPressOutside - implicitWidth: { - if (root.isMobile) return applicationWindow().width - if (root.isNarrowScreen) return applicationWindow().width - Kirigami.Units.largeSpacing * 2 - return Math.min(applicationWindow().width - Kirigami.Units.gridUnit * 2, Kirigami.Units.gridUnit * 30) + width: { + if (root.isMobile) return settingsSheet.sheetAvailableWidth + if (root.isNarrowScreen) return settingsSheet.sheetAvailableWidth + let desired = Math.round(settingsSheet.sheetAvailableWidth * 0.82) + return Math.min(settingsSheet.sheetAvailableWidth, Math.max(Kirigami.Units.gridUnit * 44, Math.min(desired, Kirigami.Units.gridUnit * 96))) } - implicitHeight: { - if (root.isMobile) return applicationWindow().height - return Math.min(applicationWindow().height - Kirigami.Units.gridUnit * 2, settingsContent.implicitHeight + Kirigami.Units.gridUnit * 4) + height: { + if (root.isMobile) return settingsSheet.sheetAvailableHeight + let contentH = settingsContent.implicitHeight + settingsScroll.topPadding + settingsScroll.bottomPadding + Kirigami.Units.gridUnit * 4 + return Math.max(Math.round(settingsSheet.sheetAvailableHeight * 0.85), Math.min(contentH, settingsSheet.sheetAvailableHeight)) + } + + x: { + return settingsSheet.usableLeft + Math.round((settingsSheet.usableWidth - width) / 2) + } + y: { + return settingsSheet.usableTop + settingsSheet.verticalMargin } onOpened: settingsContent.focusFirstControl() @@ -839,7 +1020,7 @@ Kirigami.ApplicationWindow { header: Kirigami.ShadowedRectangle { id: settingsHeader - implicitWidth: settingsSheet.implicitWidth + implicitWidth: settingsSheet.width implicitHeight: settingsHeaderRow.implicitHeight + Kirigami.Units.largeSpacing * 2 radius: Kirigami.Units.mediumSpacing @@ -941,10 +1122,13 @@ Kirigami.ApplicationWindow { id: settingsScroll clip: true - leftPadding: Kirigami.Units.largeSpacing - rightPadding: Kirigami.Units.largeSpacing - topPadding: Kirigami.Units.largeSpacing - bottomPadding: Kirigami.Units.largeSpacing + readonly property int horizontalContentPadding: root.isMobile ? Kirigami.Units.largeSpacing : Kirigami.Units.gridUnit + readonly property int verticalContentPadding: root.isMobile ? Kirigami.Units.largeSpacing : Kirigami.Units.gridUnit + + leftPadding: horizontalContentPadding + rightPadding: horizontalContentPadding + topPadding: verticalContentPadding + bottomPadding: verticalContentPadding QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff diff --git a/src/qml/SettingsPage.qml b/src/qml/SettingsPage.qml index 6eb1b88..992fb43 100644 --- a/src/qml/SettingsPage.qml +++ b/src/qml/SettingsPage.qml @@ -72,10 +72,11 @@ ColumnLayout { } } - FormCard.FormHeader { - Layout.fillWidth: true - title: i18n("Import Sources") - } +FormCard.FormHeader { + Layout.topMargin: Kirigami.Units.mediumSpacing + Layout.fillWidth: true + title: i18n("Import Sources") +} FormCard.FormCard { Layout.fillWidth: true @@ -215,6 +216,7 @@ ColumnLayout { } FormCard.FormHeader { + Layout.topMargin: Kirigami.Units.mediumSpacing Layout.fillWidth: true title: i18n("Behavior") } @@ -239,6 +241,7 @@ ColumnLayout { } FormCard.FormHeader { + Layout.topMargin: Kirigami.Units.mediumSpacing Layout.fillWidth: true title: i18n("Cover Art") } @@ -263,6 +266,7 @@ ColumnLayout { } FormCard.FormHeader { + Layout.topMargin: Kirigami.Units.mediumSpacing Layout.fillWidth: true title: i18n("SteamGridDB") } @@ -322,6 +326,7 @@ ColumnLayout { } FormCard.FormHeader { + Layout.topMargin: Kirigami.Units.mediumSpacing Layout.fillWidth: true title: i18n("Library") } diff --git a/src/qml/components/GameGridView.qml b/src/qml/components/GameGridView.qml index e9fe4d3..b69d8b0 100644 --- a/src/qml/components/GameGridView.qml +++ b/src/qml/components/GameGridView.qml @@ -22,6 +22,8 @@ GridView { keyNavigationWraps: false activeFocusOnTab: true + currentIndex: -1 + highlightMoveDuration: Kirigami.Units.shortDuration highlightFollowsCurrentItem: true @@ -31,24 +33,40 @@ GridView { Keys.onRightPressed: navigateRight() function navigateUp() { + if (currentIndex < 0 && count > 0) { + currentIndex = 0 + return + } if (currentIndex >= columns) { currentIndex -= columns } } function navigateDown() { + if (currentIndex < 0 && count > 0) { + currentIndex = 0 + return + } if (currentIndex + columns < count) { currentIndex += columns } } function navigateLeft() { + if (currentIndex < 0 && count > 0) { + currentIndex = 0 + return + } if (currentIndex > 0) { currentIndex-- } } function navigateRight() { + if (currentIndex < 0 && count > 0) { + currentIndex = 0 + return + } if (currentIndex < count - 1) { currentIndex++ } @@ -61,7 +79,13 @@ GridView { function onNavigateLeft() { if (gridView.activeFocus) gridView.navigateLeft() } function onNavigateRight() { if (gridView.activeFocus) gridView.navigateRight() } function onSelectPressed() { - if (gridView.activeFocus && gridView.currentItem) { + if (!gridView.activeFocus) { + return + } + if (gridView.currentIndex < 0 && gridView.count > 0) { + gridView.currentIndex = 0 + } + if (gridView.currentItem) { if (gridView.currentItem.play) { gridView.currentItem.play() } else {