a-la-karte/src/qml/Main.qml

1956 lines
78 KiB
QML
Raw Normal View History

// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.formcard as FormCard
import org.kde.alakarte
import "components"
Kirigami.ApplicationWindow {
id: root
title: i18n("A-La-Karte")
minimumWidth: Kirigami.Units.gridUnit * 25
minimumHeight: Kirigami.Units.gridUnit * 20
width: Kirigami.Units.gridUnit * 55
height: Kirigami.Units.gridUnit * 40
property var selectedGame: null
2026-01-25 09:03:46 +00:00
property var pendingRemoveGame: null
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.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
}
if (importSheet.opened) {
importSheet.close()
return true
}
if (aboutSheet.opened) {
aboutSheet.close()
return true
}
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) {
root.pageStack.layers.pop()
Qt.callLater(function() { libraryView.restoreFocus() })
return true
}
if (searchActive) {
searchActive = false
libraryView.clearSearch()
Qt.callLater(function() { libraryView.restoreFocus() })
return true
}
return false
}
function canOpenSettings() {
return !gameEditDialog.visible
&& !detailsSheet.opened
&& !importSheet.opened
&& !aboutSheet.opened
&& !settingsSheet.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.Couch) return i18n("Couch")
return i18n("Automatic")
}
function cycleUiMode() {
if (App.config.uiMode === Config.Auto) {
App.config.uiMode = Config.Desktop
} else if (App.config.uiMode === Config.Desktop) {
App.config.uiMode = Config.Couch
} else {
App.config.uiMode = Config.Auto
}
root.showPassiveNotification(i18n("UI mode: %1", uiModeLabel(App.config.uiMode)))
}
readonly property bool deviceIsMobile: Kirigami.Settings.isMobile
readonly property bool deviceIsTablet: Kirigami.Settings.tabletMode && !Kirigami.Settings.isMobile
readonly property bool deviceIsTouchDevice: Kirigami.Settings.tabletMode || Kirigami.Settings.isMobile
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: Math.round(App.config.gridSize * couchScale)
pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.ToolBar
pageStack.columnView.columnResizeMode: Kirigami.ColumnView.SingleColumn
footer: Item {
implicitWidth: root.width
anchors.left: parent ? parent.left : undefined
anchors.right: parent ? parent.right : undefined
implicitHeight: footerBar.implicitHeight
height: implicitHeight
QQC2.ToolBar {
id: footerBar
anchors.fill: parent
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: 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)
readonly property bool compactFooter: (hintBar.implicitWidth + rightExtras.implicitWidth + Kirigami.Units.gridUnit * 2) > width
RowLayout {
id: footerLayout
anchors.fill: parent
anchors.margins: footerRoot.footerMargins
spacing: root.isCouchMode ? Kirigami.Units.largeSpacing : Kirigami.Units.mediumSpacing
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"
}
}
}
}
}
}
}
}
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() })
}
}
Shortcut {
sequence: "Ctrl+I"
onActivated: {
if (root.currentConfirmDialog()) return
importSheet.open()
}
}
Shortcut {
sequence: "Ctrl+N"
onActivated: {
if (root.currentConfirmDialog()) return
gameEditDialog.game = null
gameEditDialog.open()
}
}
Shortcut {
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 {
sequence: "Escape"
onActivated: root.closeTopmost()
}
Shortcut {
sequence: "F5"
onActivated: {
if (root.currentConfirmDialog()) return
App.importAllGames()
}
}
Shortcut {
sequence: "Ctrl+H"
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
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()
}
}
}
Shortcut {
sequence: "F9"
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 (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 (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 (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
}
return
}
if (!libraryView || !libraryView.visible) return
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()
} else if (detailsSheet.opened) {
detailsSheet.close()
Qt.callLater(function() { root.openSettings() })
} else {
root.closeTopmost()
}
} else {
root.closeSettings()
}
}
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 || 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 {
target: root.pageStack.layers
function onDepthChanged() {
if (root.pageStack.layers.depth <= 1) {
root.settingsLayerOpen = false
Qt.callLater(function() { libraryView.restoreFocus() })
}
}
}
globalDrawer: Kirigami.OverlayDrawer {
id: sidebar
edge: Qt.LeftEdge
modal: root.isMobile || root.isNarrowScreen || root.isCouchMode
handleClosedIcon.source: "application-menu"
handleOpenIcon.source: "go-previous"
handleVisible: root.isMobile || root.isNarrowScreen || root.isCouchMode
width: {
if (root.isMobile) return Math.min(root.width * 0.85, Kirigami.Units.gridUnit * 20)
if (root.isTablet) return Kirigami.Units.gridUnit * 16
return Kirigami.Units.gridUnit * 14
}
Binding {
target: sidebar
property: "drawerOpen"
value: true
when: !sidebar.modal
}
onOpened: sidebarView.focusList()
onClosed: {
if (root.pendingAboutOpen) {
root.pendingAboutOpen = false
Qt.callLater(function() { aboutSheet.open() })
return
}
Qt.callLater(function() { libraryView.restoreFocus() })
}
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
contentItem: ColumnLayout {
spacing: 0
Kirigami.AbstractApplicationHeader {
Layout.fillWidth: true
contentItem: Item {
anchors.fill: parent
anchors.margins: Kirigami.Units.largeSpacing
RowLayout {
anchors.fill: parent
spacing: Kirigami.Units.mediumSpacing
IconWithResourceFallback {
primary: "org.kde.alakarte"
secondary: ""
resourceFallback: Qt.resolvedUrl("icons/app/org.kde.alakarte.svg")
mask: false
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
}
Kirigami.Heading {
text: i18n("A-La-Karte")
level: 2
Layout.fillWidth: true
}
}
}
}
SidebarView {
id: sidebarView
Layout.fillWidth: true
Layout.fillHeight: true
onSourceSelected: function(source) {
root.currentSource = source
if (sidebar.modal) {
if (!sidebarView.suppressAutoClose) {
sidebar.close()
Qt.callLater(function() { libraryView.restoreFocus() })
}
}
}
onSettingsRequested: {
if (sidebar.modal) {
sidebar.close()
}
root.openSettings()
}
onImportRequested: {
if (sidebar.modal) {
sidebar.close()
}
importSheet.open()
}
onAboutRequested: {
if (sidebar.modal) {
root.pendingAboutOpen = true
sidebar.close()
return
}
aboutSheet.open()
}
}
}
}
pageStack.initialPage: Kirigami.Page {
id: mainPage
title: sidebarView.currentSourceName
padding: Kirigami.Units.largeSpacing
actions: [
Kirigami.Action {
icon.name: "search"
text: i18n("Search")
checkable: true
checked: root.searchActive
onTriggered: {
root.searchActive = !root.searchActive
if (root.searchActive) {
Qt.callLater(function() { libraryView.focusSearch() })
} else {
libraryView.clearSearch()
libraryView.restoreFocus()
}
}
},
Kirigami.Action {
icon.name: "list-add"
text: i18n("Add Game")
onTriggered: {
gameEditDialog.game = null
gameEditDialog.open()
}
},
Kirigami.Action {
icon.name: "document-import"
text: i18n("Import Games")
visible: sidebar.modal
onTriggered: importSheet.open()
},
Kirigami.Action {
icon.name: "configure"
text: i18n("Settings")
visible: sidebar.modal
onTriggered: root.openSettings()
}
]
LibraryView {
id: libraryView
anchors.fill: parent
filterSource: root.currentSource
searchActive: root.searchActive
adaptiveCardSize: root.adaptiveCardSize
isTouchDevice: root.isTouchDevice
onGameSelected: function(game) {
root.selectedGame = game
detailsSheet.open()
}
onGameLaunched: function(game) {
App.launcher.launchGame(game)
}
}
}
GameDetailsSheet {
id: detailsSheet
game: root.selectedGame
onLaunchRequested: {
App.launcher.launchGame(root.selectedGame)
}
onEditRequested: {
detailsSheet.close()
gameEditDialog.game = root.selectedGame
gameEditDialog.open()
}
onRemoveRequested: {
2026-01-25 09:03:46 +00:00
if (!root.selectedGame) return
root.pendingRemoveGame = root.selectedGame
removeGameConfirmDialog.open()
}
onClosed: {
libraryView.restoreFocus()
}
}
2026-01-25 09:03:46 +00:00
Kirigami.PromptDialog {
id: removeGameConfirmDialog
title: i18n("Remove Game")
subtitle: root.pendingRemoveGame
? 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()
})
}
2026-01-25 09:03:46 +00:00
onAccepted: {
if (!root.pendingRemoveGame) return
let gameId = root.pendingRemoveGame.id
let gameName = root.pendingRemoveGame.name
App.removeGame(root.pendingRemoveGame)
root.pendingRemoveGame = null
detailsSheet.close()
root.selectedGame = null
showPassiveNotification(
i18n("%1 removed", gameName),
"long",
i18n("Undo"),
function() { App.restoreGame(gameId) }
)
}
onRejected: {
root.pendingRemoveGame = null
}
}
Kirigami.OverlaySheet {
id: importSheet
title: i18n("Import Games")
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 * 32)
}
implicitHeight: {
if (root.isMobile) return applicationWindow().height
return Math.min(applicationWindow().height - Kirigami.Units.gridUnit * 2, importContent.implicitHeight + Kirigami.Units.gridUnit * 2)
}
onOpened: steamImportButton.forceActiveFocus()
onClosed: libraryView.restoreFocus()
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 (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
}
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 (importSheet.isDescendant(next, importContent)) {
next.forceActiveFocus()
importScroll.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.checkable !== undefined && item.checkable && item.checked !== undefined) {
item.checked = !item.checked
return
}
if (typeof item.clicked === "function") {
item.clicked()
return
}
}
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
if (!importSheet.isDescendant(w.activeFocusItem, importSheet)) return
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
if (!importSheet.isDescendant(w.activeFocusItem, importSheet)) return
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
if (!importSheet.isDescendant(w.activeFocusItem, importSheet)) return
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
if (!importSheet.isDescendant(w.activeFocusItem, importSheet)) return
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
if (!importSheet.isDescendant(w.activeFocusItem, importSheet)) return
importSheet.activateFocused()
}
}
contentItem: QQC2.ScrollView {
id: importScroll
clip: true
leftPadding: Kirigami.Units.largeSpacing
rightPadding: Kirigami.Units.largeSpacing
topPadding: Kirigami.Units.largeSpacing
bottomPadding: Kirigami.Units.largeSpacing
QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff
function ensureItemVisible(item) {
if (!item || !importScroll.contentItem || !importScroll.contentItem.contentItem) return
let flick = importScroll.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 (!importScroll.contentItem) return
let maxY = Math.max(0, importScroll.contentItem.contentHeight - importScroll.contentItem.height)
importScroll.contentItem.contentY = Math.max(0, Math.min(maxY, importScroll.contentItem.contentY + delta))
}
Connections {
target: GamepadManager
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 {
id: importContent
width: importScroll.availableWidth
spacing: Kirigami.Units.mediumSpacing
Kirigami.InlineMessage {
Layout.fillWidth: true
type: Kirigami.MessageType.Information
text: App.importStatus
visible: App.importing
}
FormCard.FormCard {
Layout.fillWidth: true
FormCard.FormButtonDelegate {
id: steamImportButton
text: i18n("Steam")
description: i18n("Import from Steam library")
icon.name: ""
leading: IconWithResourceFallback {
primary: "com.valvesoftware.Steam"
secondary: "steam"
resourceFallback: Qt.resolvedUrl("icons/brand/steam-symbolic.svg")
}
enabled: !App.importing && App.config.importSteam && !App.gameModel.hasPlatformPrefix("Steam") && App.gameModel.count >= 0
onClicked: App.importFromSteam()
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Lutris")
description: i18n("Import from Lutris")
icon.name: ""
leading: IconWithResourceFallback {
primary: "lutris"
secondary: "applications-games"
}
enabled: !App.importing && App.config.importLutris && !App.gameModel.hasPlatformPrefix("Lutris") && App.gameModel.count >= 0
onClicked: App.importFromLutris()
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Heroic")
description: i18n("Epic, GOG, Amazon games")
icon.name: ""
leading: IconWithResourceFallback {
primary: "com.heroicgameslauncher.hgl"
secondary: "applications-games"
}
enabled: !App.importing && App.config.importHeroic && !App.gameModel.hasPlatformPrefix("Heroic") && App.gameModel.count >= 0
onClicked: App.importFromHeroic()
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Desktop Entries")
description: i18n("Games from system .desktop files")
icon.name: ""
leading: IconWithResourceFallback {
primary: "user-desktop"
secondary: "computer"
}
enabled: !App.importing && App.config.importDesktop && !App.gameModel.hasPlatformPrefix("Desktop") && App.gameModel.count >= 0
onClicked: App.importFromDesktop()
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Bottles")
description: i18n("Wine applications from Bottles")
icon.name: ""
leading: IconWithResourceFallback {
primary: "com.usebottles.bottles"
secondary: "application-x-executable"
}
enabled: !App.importing && App.config.importBottles && !App.gameModel.hasPlatformPrefix("Bottles") && App.gameModel.count >= 0
onClicked: App.importFromBottles()
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Flatpak Games")
description: i18n("Games installed via Flatpak")
icon.name: ""
leading: IconWithResourceFallback {
primary: "flatpak-discover"
secondary: "applications-games"
}
enabled: !App.importing && App.config.importFlatpak && !App.gameModel.hasPlatformPrefix("Flatpak") && App.gameModel.count >= 0
onClicked: App.importFromFlatpak()
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("itch.io")
description: i18n("Games from itch.io app")
icon.name: ""
leading: IconWithResourceFallback {
primary: "io.itch.itch"
secondary: "itch"
resourceFallback: Qt.resolvedUrl("icons/brand/itchdotio-symbolic.svg")
}
enabled: !App.importing && App.config.importItch && !App.gameModel.hasPlatformPrefix("itch.io") && App.gameModel.count >= 0
onClicked: App.importFromItch()
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("Legendary")
description: i18n("Epic Games via Legendary CLI")
icon.name: ""
leading: IconWithResourceFallback {
primary: "legendary"
secondary: "applications-games"
}
enabled: !App.importing && App.config.importLegendary && !App.gameModel.hasPlatformPrefix("Legendary") && App.gameModel.count >= 0
onClicked: App.importFromLegendary()
}
FormCard.FormDelegateSeparator {}
FormCard.FormButtonDelegate {
text: i18n("RetroArch")
description: i18n("Games from RetroArch playlists")
icon.name: ""
leading: IconWithResourceFallback {
primary: "org.libretro.RetroArch"
secondary: "retroarch"
resourceFallback: Qt.resolvedUrl("icons/brand/retroarch-symbolic.svg")
}
enabled: !App.importing && App.config.importRetroArch && !App.gameModel.hasPlatformPrefix("RetroArch") && App.gameModel.count >= 0
onClicked: App.importFromRetroArch()
}
}
RowLayout {
Layout.alignment: Qt.AlignRight
spacing: Kirigami.Units.mediumSpacing
QQC2.Button {
text: i18n("Import All")
icon.name: "document-import"
enabled: !App.importing && App.gameModel.count >= 0 && ((App.config.importSteam && !App.gameModel.hasPlatformPrefix("Steam"))
|| (App.config.importLutris && !App.gameModel.hasPlatformPrefix("Lutris"))
|| (App.config.importHeroic && !App.gameModel.hasPlatformPrefix("Heroic"))
|| (App.config.importDesktop && !App.gameModel.hasPlatformPrefix("Desktop"))
|| (App.config.importBottles && !App.gameModel.hasPlatformPrefix("Bottles"))
|| (App.config.importFlatpak && !App.gameModel.hasPlatformPrefix("Flatpak"))
|| (App.config.importItch && !App.gameModel.hasPlatformPrefix("itch.io"))
|| (App.config.importLegendary && !App.gameModel.hasPlatformPrefix("Legendary"))
|| (App.config.importRetroArch && !App.gameModel.hasPlatformPrefix("RetroArch")))
onClicked: App.importAllGames()
}
QQC2.Button {
text: i18n("Close")
onClicked: importSheet.close()
}
}
}
}
Component {
id: settingsLayerComponent
Kirigami.Page {
id: settingsLayerPage
title: i18n("Settings")
property Item content: settingsLayerContent
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
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
}
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.isActualDescendant(next, settingsLayerContent)) {
settingsLayerScroll.ensureItemVisible(next)
}
return
}
}
}
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
let item = w.activeFocusItem
if (typeof item.toggle === "function") {
item.toggle()
return
}
if (item.checkable !== undefined && item.checkable && item.checked !== undefined) {
item.checked = !item.checked
return
}
if (typeof item.clicked === "function") {
item.clicked()
return
}
}
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
if (!settingsLayerPage.isDescendant(w.activeFocusItem, settingsLayerPage)) return
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
if (!settingsLayerPage.isDescendant(w.activeFocusItem, settingsLayerPage)) return
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
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() {
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 {
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
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)))
}
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()
onClosed: libraryView.restoreFocus()
header: Kirigami.ShadowedRectangle {
id: settingsHeader
implicitWidth: settingsSheet.width
implicitHeight: settingsHeaderRow.implicitHeight + Kirigami.Units.largeSpacing * 2
radius: Kirigami.Units.mediumSpacing
color: Kirigami.Theme.backgroundColor
shadow {
size: Kirigami.Units.smallSpacing
color: Qt.rgba(0, 0, 0, 0.20)
}
RowLayout {
id: settingsHeaderRow
anchors.fill: parent
anchors.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.mediumSpacing
Kirigami.Heading {
text: settingsSheet.title
level: 2
Layout.fillWidth: true
elide: Text.ElideRight
}
QQC2.ToolButton {
text: i18n("Close")
icon.name: "dialog-close"
display: QQC2.AbstractButton.IconOnly
onClicked: settingsSheet.close()
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: text
}
}
}
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 (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
}
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 (settingsSheet.isDescendant(next, settingsSheet)) {
next.forceActiveFocus()
if (settingsSheet.isActualDescendant(next, settingsContent)) {
settingsScroll.ensureItemVisible(next)
}
return
}
}
}
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
let item = w.activeFocusItem
if (typeof item.toggle === "function") {
item.toggle()
return
}
if (item.checkable !== undefined && item.checkable && item.checked !== undefined) {
item.checked = !item.checked
return
}
if (typeof item.clicked === "function") {
item.clicked()
return
}
}
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()
}
}
contentItem: QQC2.ScrollView {
id: settingsScroll
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 || !settingsScroll.contentItem || !settingsScroll.contentItem.contentItem) return
let flick = settingsScroll.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 (!settingsScroll.contentItem) return
let maxY = Math.max(0, settingsScroll.contentItem.contentHeight - settingsScroll.contentItem.height)
settingsScroll.contentItem.contentY = Math.max(0, Math.min(maxY, settingsScroll.contentItem.contentY + delta))
}
Connections {
target: GamepadManager
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 {
id: settingsContent
width: settingsScroll.availableWidth
}
}
}
Kirigami.OverlaySheet {
id: aboutSheet
title: i18n("About")
parent: applicationWindow().overlay
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)
}
implicitHeight: {
if (root.isMobile) return applicationWindow().height
return Math.min(applicationWindow().height - Kirigami.Units.gridUnit * 2, Kirigami.Units.gridUnit * 42)
}
onOpened: aboutCloseButton.forceActiveFocus()
onClosed: libraryView.restoreFocus()
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 (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
}
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 (aboutSheet.isDescendant(next, aboutSheet)) {
next.forceActiveFocus()
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.checkable !== undefined && item.checkable && item.checked !== undefined) {
item.checked = !item.checked
return
}
if (typeof item.clicked === "function") {
item.clicked()
return
}
}
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
if (!aboutSheet.isDescendant(w.activeFocusItem, aboutSheet)) return
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
if (!aboutSheet.isDescendant(w.activeFocusItem, aboutSheet)) return
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
if (!aboutSheet.isDescendant(w.activeFocusItem, aboutSheet)) return
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
if (!aboutSheet.isDescendant(w.activeFocusItem, aboutSheet)) return
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
if (!aboutSheet.isDescendant(w.activeFocusItem, aboutSheet)) return
aboutSheet.activateFocused()
}
}
header: Kirigami.ShadowedRectangle {
id: aboutHeader
implicitWidth: aboutSheet.implicitWidth
implicitHeight: aboutHeaderRow.implicitHeight + Kirigami.Units.largeSpacing * 2
radius: Kirigami.Units.mediumSpacing
color: Kirigami.Theme.backgroundColor
shadow {
size: Kirigami.Units.smallSpacing
color: Qt.rgba(0, 0, 0, 0.20)
}
RowLayout {
id: aboutHeaderRow
anchors.fill: parent
anchors.margins: Kirigami.Units.largeSpacing
spacing: Kirigami.Units.mediumSpacing
Kirigami.Heading {
text: aboutSheet.title
level: 2
Layout.fillWidth: true
elide: Text.ElideRight
}
QQC2.ToolButton {
id: aboutCloseButton
text: i18n("Close")
icon.name: "dialog-close"
display: QQC2.AbstractButton.IconOnly
onClicked: aboutSheet.close()
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.text: text
}
}
}
contentItem: Item {
Loader {
id: aboutPageLoader
anchors.fill: parent
active: aboutSheet.opened
sourceComponent: FormCard.AboutPage {
title: i18n("About")
aboutData: ({
"displayName": i18n("A-La-Karte"),
"componentName": "alakarte",
"shortDescription": i18n("A unified game launcher for KDE Plasma"),
"homepage": "",
"bugAddress": "",
"version": Qt.application.version,
"otherText": "",
"authors": [],
"credits": [],
"translators": [],
"licenses": [
{
"name": i18n("GNU General Public License v3.0 or later"),
"text": "This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\n\nThe full license text is available at:\nhttps://www.gnu.org/licenses/gpl-3.0.txt",
"spdx": "GPL-3.0-or-later"
}
],
"copyrightStatement": "",
"desktopFileName": "org.kde.alakarte"
})
}
}
}
}
GameEditDialog {
id: gameEditDialog
parent: root.overlay
onClosed: libraryView.restoreFocus()
onGameCreated: function(game) {
showPassiveNotification(i18n("Added %1", game.name))
}
onGameUpdated: function(game) {
showPassiveNotification(i18n("Updated %1", game.name))
}
}
Connections {
target: App
function onImportCompleted(count) {
showPassiveNotification(i18np("Imported %1 game", "Imported %1 games", count))
}
function onImportError(error) {
showPassiveNotification(i18n("Import error: %1", error), "long")
}
}
Connections {
target: App.launcher
function onGameStarted(game) {
showPassiveNotification(i18n("Launching %1...", game.name))
}
function onGameError(game, error) {
showPassiveNotification(i18n("Error launching %1: %2", game.name, error), "long")
}
}
Component.onCompleted: {
if (App.gameModel.count === 0) {
importSheet.open()
}
}
}
}