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

View file

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

View file

@ -72,10 +72,11 @@ ColumnLayout {
}
}
FormCard.FormHeader {
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")
}

View file

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