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

1389 lines
52 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
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 (settingsLayerOpen) return "settings"
if (aboutSheet.opened) return "about"
if (sidebar.modal && sidebar.opened) return "sidebar"
return "library"
}
function closeTopmost() {
if (gameEditDialog.visible) {
gameEditDialog.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.modal && sidebar.opened) {
sidebar.close()
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
&& !(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")
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.Handheld
} 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 bool isMobile: App.config.uiMode === Config.Handheld ? true : deviceIsMobile
readonly property bool isTablet: App.config.uiMode === Config.Handheld ? true : deviceIsTablet
readonly property bool isTouchDevice: App.config.uiMode === Config.Handheld ? true : (App.config.uiMode === Config.Desktop ? false : deviceIsTouchDevice)
readonly property bool isNarrowScreen: width < Kirigami.Units.gridUnit * 30
readonly property int adaptiveCardSize: App.config.gridSize
pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.ToolBar
pageStack.columnView.columnResizeMode: Kirigami.ColumnView.SingleColumn
footer: QQC2.ToolBar {
visible: true
leftPadding: 0
rightPadding: 0
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
contentItem: Item {
implicitHeight: Math.max(centerHints.implicitHeight, rightExtras.implicitHeight)
readonly property bool compactFooter: (centerHints.implicitWidth + rightExtras.implicitWidth + Kirigami.Units.gridUnit * 2) > width
Kirigami.ShadowedRectangle {
id: centerHints
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
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)
implicitWidth: hintRow.implicitWidth + Kirigami.Units.largeSpacing * 2
implicitHeight: hintRow.implicitHeight + Kirigami.Units.smallSpacing * 2
RowLayout {
id: hintRow
anchors.centerIn: parent
spacing: Kirigami.Units.largeSpacing
BottomHintBar {
context: root.hintContext
Layout.alignment: Qt.AlignVCenter
}
}
}
RowLayout {
id: rightExtras
anchors.right: parent.right
anchors.rightMargin: Kirigami.Units.mediumSpacing
anchors.verticalCenter: parent.verticalCenter
spacing: Kirigami.Units.mediumSpacing
visible: !parent.compactFooter
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: {
searchActive = true
Qt.callLater(function() { libraryView.focusSearch() })
}
}
Shortcut {
sequence: "Ctrl+I"
onActivated: importSheet.open()
}
Shortcut {
sequence: "Ctrl+N"
onActivated: {
gameEditDialog.game = null
gameEditDialog.open()
}
}
Shortcut {
sequence: StandardKey.Preferences
onActivated: root.openSettings()
}
Shortcut {
sequence: "Escape"
onActivated: root.closeTopmost()
}
Shortcut {
sequence: "F5"
onActivated: App.importAllGames()
}
Shortcut {
sequence: "Ctrl+H"
onActivated: {
if (root.currentSource === "hidden") {
root.currentSource = "all"
} else {
root.currentSource = "hidden"
}
}
}
Shortcut {
sequence: "Ctrl+D"
onActivated: {
if (root.selectedGame) {
detailsSheet.open()
}
}
}
Shortcut {
sequence: "F9"
onActivated: root.cycleUiMode()
}
Connections {
target: GamepadManager
function onBackPressed() {
root.closeTopmost()
}
function onLeftBumperPressed() {
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 || settingsLayerOpen || aboutSheet.opened) return
if (root.hintContext !== "library" && root.hintContext !== "sidebar") return
sidebarView.cycleSource(1)
}
function onDetailsPressed() {
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
}
return
}
if (!libraryView || !libraryView.visible) return
libraryView.openDetailsForFocusedGame()
}
function onMenuPressed() {
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 (detailsSheet.opened) {
detailsSheet.editRequested()
return
}
if (gameEditDialog.visible || importSheet.opened || settingsLayerOpen || aboutSheet.opened || (sidebar.modal && sidebar.opened)) return
searchActive = true
Qt.callLater(function() { libraryView.focusSearch() })
}
}
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
handleClosedIcon.source: "application-menu"
handleOpenIcon.source: "go-previous"
handleVisible: root.isMobile || root.isNarrowScreen
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: 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) {
sidebar.close()
}
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: {
if (root.selectedGame) {
let gameId = root.selectedGame.id
let gameName = root.selectedGame.name
App.removeGame(root.selectedGame)
detailsSheet.close()
root.selectedGame = null
showPassiveNotification(
i18n("%1 removed", gameName),
"long",
i18n("Undo"),
function() { App.restoreGame(gameId) }
)
}
}
onClosed: {
libraryView.restoreFocus()
}
}
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
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.hasOwnProperty("checked")) {
item.checked = !item.checked
return
}
if (typeof item.clicked === "function") {
item.clicked()
return
}
}
Connections {
target: GamepadManager
function onNavigateUp() {
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() {
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() {
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() {
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() {
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() { if (importScroll.activeFocus) importScroll.scrollBy(-Kirigami.Units.gridUnit * 2) }
function onNavigateDown() { 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")
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
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
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.isDescendant(next, settingsContent)) {
settingsScroll.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 (!settingsSheet.opened || settingsScroll.activeFocus) return
settingsSheet.focusNextInChain(false)
}
function onNavigateDown() {
if (!settingsSheet.opened || settingsScroll.activeFocus) return
settingsSheet.focusNextInChain(true)
}
function onSelectPressed() {
if (!settingsSheet.opened || settingsScroll.activeFocus) 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() { if (settingsScroll.activeFocus) settingsScroll.scrollBy(-Kirigami.Units.gridUnit * 2) }
function onNavigateDown() { if (settingsScroll.activeFocus) settingsScroll.scrollBy(Kirigami.Units.gridUnit * 2) }
}
SettingsPage {
id: settingsContent
width: settingsScroll.availableWidth
}
}
}
Kirigami.OverlaySheet {
id: aboutSheet
title: i18n("About")
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
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.hasOwnProperty("checked")) {
item.checked = !item.checked
return
}
if (typeof item.clicked === "function") {
item.clicked()
return
}
}
Connections {
target: GamepadManager
function onNavigateUp() {
if (!aboutSheet.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!aboutSheet.isDescendant(w.activeFocusItem, aboutSheet)) return
aboutSheet.focusNextInChain(false)
}
function onNavigateDown() {
if (!aboutSheet.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!aboutSheet.isDescendant(w.activeFocusItem, aboutSheet)) return
aboutSheet.focusNextInChain(true)
}
function onNavigateLeft() {
if (!aboutSheet.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!aboutSheet.isDescendant(w.activeFocusItem, aboutSheet)) return
aboutSheet.focusNextInChain(false)
}
function onNavigateRight() {
if (!aboutSheet.opened) return
let w = applicationWindow()
if (!w || !w.activeFocusItem) return
if (!aboutSheet.isDescendant(w.activeFocusItem, aboutSheet)) return
aboutSheet.focusNextInChain(true)
}
function onSelectPressed() {
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()
}
}
}