qml: move Settings to pageStack layer

Replace the settings OverlaySheet with a pageStack layer page to avoid header/sidebar overlap. Keep keyboard/gamepad focus navigation and scrolling. Tweak SettingsPage spacing and make grid selection start unselected for better navigation.
This commit is contained in:
Marco Allegretti 2026-01-24 20:50:20 +01:00
parent 76f2678bf6
commit b961a8cc8f
4 changed files with 264 additions and 35 deletions

View file

@ -14,7 +14,7 @@ FocusScope {
property string filterSource: "all" property string filterSource: "all"
property bool searchActive: false property bool searchActive: false
property int focusedIndex: 0 property int focusedIndex: -1
property int adaptiveCardSize: App.config.gridSize property int adaptiveCardSize: App.config.gridSize
property bool isTouchDevice: false property bool isTouchDevice: false
@ -47,6 +47,10 @@ FocusScope {
function launchFocusedGame() { function launchFocusedGame() {
if (!gameGrid || !proxyModel) return if (!gameGrid || !proxyModel) return
if (gameGrid.currentIndex < 0 && proxyModel.count > 0) {
gameGrid.currentIndex = 0
libraryRoot.focusedIndex = 0
}
let game = proxyModel.get(gameGrid.currentIndex) let game = proxyModel.get(gameGrid.currentIndex)
if (game) { if (game) {
libraryRoot.gameLaunched(game) libraryRoot.gameLaunched(game)
@ -55,6 +59,10 @@ FocusScope {
function openDetailsForFocusedGame() { function openDetailsForFocusedGame() {
if (!gameGrid || !proxyModel) return if (!gameGrid || !proxyModel) return
if (gameGrid.currentIndex < 0 && proxyModel.count > 0) {
gameGrid.currentIndex = 0
libraryRoot.focusedIndex = 0
}
let game = proxyModel.get(gameGrid.currentIndex) let game = proxyModel.get(gameGrid.currentIndex)
if (game) { if (game) {
libraryRoot.gameSelected(game) libraryRoot.gameSelected(game)
@ -249,12 +257,20 @@ FocusScope {
Keys.onPressed: function(event) { Keys.onPressed: function(event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { 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) let game = proxyModel.get(currentIndex)
if (game) { if (game) {
libraryRoot.gameSelected(game) libraryRoot.gameSelected(game)
} }
event.accepted = true event.accepted = true
} else if (event.key === Qt.Key_Space) { } else if (event.key === Qt.Key_Space) {
if (currentIndex < 0 && proxyModel.count > 0) {
currentIndex = 0
libraryRoot.focusedIndex = 0
}
let game = proxyModel.get(currentIndex) let game = proxyModel.get(currentIndex)
if (game) { if (game) {
libraryRoot.gameLaunched(game) libraryRoot.gameLaunched(game)

View file

@ -21,12 +21,13 @@ Kirigami.ApplicationWindow {
property var selectedGame: null property var selectedGame: null
property string currentSource: "all" property string currentSource: "all"
property bool searchActive: false property bool searchActive: false
property bool settingsLayerOpen: false
readonly property string hintContext: { readonly property string hintContext: {
if (gameEditDialog.visible) return "edit" if (gameEditDialog.visible) return "edit"
if (detailsSheet.opened) return "details" if (detailsSheet.opened) return "details"
if (importSheet.opened) return "import" if (importSheet.opened) return "import"
if (settingsSheet.opened) return "settings" if (settingsLayerOpen) return "settings"
if (aboutSheet.opened) return "about" if (aboutSheet.opened) return "about"
if (sidebar.modal && sidebar.opened) return "sidebar" if (sidebar.modal && sidebar.opened) return "sidebar"
return "library" return "library"
@ -45,10 +46,6 @@ Kirigami.ApplicationWindow {
importSheet.close() importSheet.close()
return true return true
} }
if (settingsSheet.opened) {
settingsSheet.close()
return true
}
if (aboutSheet.opened) { if (aboutSheet.opened) {
aboutSheet.close() aboutSheet.close()
return true return true
@ -76,10 +73,23 @@ Kirigami.ApplicationWindow {
&& !detailsSheet.opened && !detailsSheet.opened
&& !importSheet.opened && !importSheet.opened
&& !aboutSheet.opened && !aboutSheet.opened
&& !sidebar.opened && !(sidebar.modal && sidebar.opened)
&& root.pageStack.layers.depth <= 1 && 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) { function uiModeLabel(mode) {
if (mode === Config.Desktop) return i18n("Desktop") if (mode === Config.Desktop) return i18n("Desktop")
if (mode === Config.Handheld) return i18n("Handheld") if (mode === Config.Handheld) return i18n("Handheld")
@ -211,7 +221,7 @@ Kirigami.ApplicationWindow {
Shortcut { Shortcut {
sequence: StandardKey.Preferences sequence: StandardKey.Preferences
onActivated: settingsSheet.open() onActivated: root.openSettings()
} }
Shortcut { Shortcut {
@ -255,17 +265,17 @@ Kirigami.ApplicationWindow {
root.closeTopmost() root.closeTopmost()
} }
function onLeftBumperPressed() { 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 if (root.hintContext !== "library" && root.hintContext !== "sidebar") return
sidebarView.cycleSource(-1) sidebarView.cycleSource(-1)
} }
function onRightBumperPressed() { 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 if (root.hintContext !== "library" && root.hintContext !== "sidebar") return
sidebarView.cycleSource(1) sidebarView.cycleSource(1)
} }
function onDetailsPressed() { 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 (detailsSheet.opened) {
if (root.selectedGame) { if (root.selectedGame) {
root.selectedGame.favorite = !root.selectedGame.favorite root.selectedGame.favorite = !root.selectedGame.favorite
@ -276,17 +286,17 @@ Kirigami.ApplicationWindow {
libraryView.openDetailsForFocusedGame() libraryView.openDetailsForFocusedGame()
} }
function onMenuPressed() { function onMenuPressed() {
if (!settingsSheet.opened) { if (!settingsLayerOpen) {
if (root.canOpenSettings()) { if (root.canOpenSettings()) {
settingsSheet.open() root.openSettings()
} else if (detailsSheet.opened) { } else if (detailsSheet.opened) {
detailsSheet.close() detailsSheet.close()
Qt.callLater(function() { settingsSheet.open() }) Qt.callLater(function() { root.openSettings() })
} else { } else {
root.closeTopmost() root.closeTopmost()
} }
} else { } else {
settingsSheet.close() root.closeSettings()
} }
} }
function onSearchPressed() { function onSearchPressed() {
@ -294,7 +304,7 @@ Kirigami.ApplicationWindow {
detailsSheet.editRequested() detailsSheet.editRequested()
return 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 searchActive = true
Qt.callLater(function() { libraryView.focusSearch() }) Qt.callLater(function() { libraryView.focusSearch() })
} }
@ -304,6 +314,7 @@ Kirigami.ApplicationWindow {
target: root.pageStack.layers target: root.pageStack.layers
function onDepthChanged() { function onDepthChanged() {
if (root.pageStack.layers.depth <= 1) { if (root.pageStack.layers.depth <= 1) {
root.settingsLayerOpen = false
Qt.callLater(function() { libraryView.restoreFocus() }) Qt.callLater(function() { libraryView.restoreFocus() })
} }
} }
@ -389,7 +400,7 @@ Kirigami.ApplicationWindow {
if (sidebar.modal) { if (sidebar.modal) {
sidebar.close() sidebar.close()
} }
settingsSheet.open() root.openSettings()
} }
onImportRequested: { onImportRequested: {
if (sidebar.modal) { if (sidebar.modal) {
@ -447,7 +458,7 @@ Kirigami.ApplicationWindow {
icon.name: "configure" icon.name: "configure"
text: i18n("Settings") text: i18n("Settings")
visible: sidebar.modal 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 { Kirigami.OverlaySheet {
id: settingsSheet id: settingsSheet
title: i18n("Settings") 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 closePolicy: QQC2.Popup.CloseOnEscape | QQC2.Popup.CloseOnPressOutside
implicitWidth: { width: {
if (root.isMobile) return applicationWindow().width if (root.isMobile) return settingsSheet.sheetAvailableWidth
if (root.isNarrowScreen) return applicationWindow().width - Kirigami.Units.largeSpacing * 2 if (root.isNarrowScreen) return settingsSheet.sheetAvailableWidth
return Math.min(applicationWindow().width - Kirigami.Units.gridUnit * 2, Kirigami.Units.gridUnit * 30) 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: { height: {
if (root.isMobile) return applicationWindow().height if (root.isMobile) return settingsSheet.sheetAvailableHeight
return Math.min(applicationWindow().height - Kirigami.Units.gridUnit * 2, settingsContent.implicitHeight + Kirigami.Units.gridUnit * 4) 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() onOpened: settingsContent.focusFirstControl()
@ -839,7 +1020,7 @@ Kirigami.ApplicationWindow {
header: Kirigami.ShadowedRectangle { header: Kirigami.ShadowedRectangle {
id: settingsHeader id: settingsHeader
implicitWidth: settingsSheet.implicitWidth implicitWidth: settingsSheet.width
implicitHeight: settingsHeaderRow.implicitHeight + Kirigami.Units.largeSpacing * 2 implicitHeight: settingsHeaderRow.implicitHeight + Kirigami.Units.largeSpacing * 2
radius: Kirigami.Units.mediumSpacing radius: Kirigami.Units.mediumSpacing
@ -941,10 +1122,13 @@ Kirigami.ApplicationWindow {
id: settingsScroll id: settingsScroll
clip: true clip: true
leftPadding: Kirigami.Units.largeSpacing readonly property int horizontalContentPadding: root.isMobile ? Kirigami.Units.largeSpacing : Kirigami.Units.gridUnit
rightPadding: Kirigami.Units.largeSpacing readonly property int verticalContentPadding: root.isMobile ? Kirigami.Units.largeSpacing : Kirigami.Units.gridUnit
topPadding: Kirigami.Units.largeSpacing
bottomPadding: Kirigami.Units.largeSpacing leftPadding: horizontalContentPadding
rightPadding: horizontalContentPadding
topPadding: verticalContentPadding
bottomPadding: verticalContentPadding
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff

View file

@ -72,10 +72,11 @@ ColumnLayout {
} }
} }
FormCard.FormHeader { FormCard.FormHeader {
Layout.topMargin: Kirigami.Units.mediumSpacing
Layout.fillWidth: true Layout.fillWidth: true
title: i18n("Import Sources") title: i18n("Import Sources")
} }
FormCard.FormCard { FormCard.FormCard {
Layout.fillWidth: true Layout.fillWidth: true
@ -215,6 +216,7 @@ ColumnLayout {
} }
FormCard.FormHeader { FormCard.FormHeader {
Layout.topMargin: Kirigami.Units.mediumSpacing
Layout.fillWidth: true Layout.fillWidth: true
title: i18n("Behavior") title: i18n("Behavior")
} }
@ -239,6 +241,7 @@ ColumnLayout {
} }
FormCard.FormHeader { FormCard.FormHeader {
Layout.topMargin: Kirigami.Units.mediumSpacing
Layout.fillWidth: true Layout.fillWidth: true
title: i18n("Cover Art") title: i18n("Cover Art")
} }
@ -263,6 +266,7 @@ ColumnLayout {
} }
FormCard.FormHeader { FormCard.FormHeader {
Layout.topMargin: Kirigami.Units.mediumSpacing
Layout.fillWidth: true Layout.fillWidth: true
title: i18n("SteamGridDB") title: i18n("SteamGridDB")
} }
@ -322,6 +326,7 @@ ColumnLayout {
} }
FormCard.FormHeader { FormCard.FormHeader {
Layout.topMargin: Kirigami.Units.mediumSpacing
Layout.fillWidth: true Layout.fillWidth: true
title: i18n("Library") title: i18n("Library")
} }

View file

@ -22,6 +22,8 @@ GridView {
keyNavigationWraps: false keyNavigationWraps: false
activeFocusOnTab: true activeFocusOnTab: true
currentIndex: -1
highlightMoveDuration: Kirigami.Units.shortDuration highlightMoveDuration: Kirigami.Units.shortDuration
highlightFollowsCurrentItem: true highlightFollowsCurrentItem: true
@ -31,24 +33,40 @@ GridView {
Keys.onRightPressed: navigateRight() Keys.onRightPressed: navigateRight()
function navigateUp() { function navigateUp() {
if (currentIndex < 0 && count > 0) {
currentIndex = 0
return
}
if (currentIndex >= columns) { if (currentIndex >= columns) {
currentIndex -= columns currentIndex -= columns
} }
} }
function navigateDown() { function navigateDown() {
if (currentIndex < 0 && count > 0) {
currentIndex = 0
return
}
if (currentIndex + columns < count) { if (currentIndex + columns < count) {
currentIndex += columns currentIndex += columns
} }
} }
function navigateLeft() { function navigateLeft() {
if (currentIndex < 0 && count > 0) {
currentIndex = 0
return
}
if (currentIndex > 0) { if (currentIndex > 0) {
currentIndex-- currentIndex--
} }
} }
function navigateRight() { function navigateRight() {
if (currentIndex < 0 && count > 0) {
currentIndex = 0
return
}
if (currentIndex < count - 1) { if (currentIndex < count - 1) {
currentIndex++ currentIndex++
} }
@ -61,7 +79,13 @@ GridView {
function onNavigateLeft() { if (gridView.activeFocus) gridView.navigateLeft() } function onNavigateLeft() { if (gridView.activeFocus) gridView.navigateLeft() }
function onNavigateRight() { if (gridView.activeFocus) gridView.navigateRight() } function onNavigateRight() { if (gridView.activeFocus) gridView.navigateRight() }
function onSelectPressed() { 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) { if (gridView.currentItem.play) {
gridView.currentItem.play() gridView.currentItem.play()
} else { } else {