// 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 QtQuick.Window 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 bool windowGeometryRestored: false Timer { id: windowGeometrySaveTimer interval: 400 repeat: false onTriggered: { if (!root.windowGeometryRestored) return if (root.isMobile) return if (root.visibility === Window.Maximized || root.visibility === Window.FullScreen) return App.config.setWindowSize(root.width, root.height) } } onWidthChanged: { if (!root.windowGeometryRestored) return if (root.isMobile) return windowGeometrySaveTimer.restart() } onHeightChanged: { if (!root.windowGeometryRestored) return if (root.isMobile) return windowGeometrySaveTimer.restart() } onVisibilityChanged: { if (!root.windowGeometryRestored) return if (root.isMobile) return App.config.windowMaximized = root.visibility === Window.Maximized } onClosing: function(close) { if (root.isMobile) return App.config.windowMaximized = root.visibility === Window.Maximized if (root.visibility !== Window.Maximized && root.visibility !== Window.FullScreen) { App.config.setWindowSize(root.width, root.height) } } property var selectedGame: null 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 (diagnosticsSheet.opened) return "diagnostics" 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 if (gameEditDialog.visible && gameEditDialog.anyConfirmOpen) return gameEditDialog.currentConfirmDialog() 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.anyConfirmOpen) { gameEditDialog.closeCurrentConfirmDialog() return true } 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 (diagnosticsSheet.opened) { diagnosticsSheet.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 && !diagnosticsSheet.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 || diagnosticsSheet.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 (diagnosticsSheet.opened) { diagnosticsSheet.close() Qt.callLater(function() { root.openSettings() }) 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 || diagnosticsSheet.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 || diagnosticsSheet.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 || diagnosticsSheet.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 || diagnosticsSheet.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 || diagnosticsSheet.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 || diagnosticsSheet.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 || diagnosticsSheet.opened || 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 (diagnosticsSheet.opened) { root.pendingSidebarOpen = true diagnosticsSheet.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 || diagnosticsSheet.opened || 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() } ] ColumnLayout { anchors.fill: parent spacing: 0 Kirigami.InlineMessage { id: daemonBanner Layout.fillWidth: true Layout.bottomMargin: Kirigami.Units.smallSpacing type: Kirigami.MessageType.Error visible: !App.launcher.daemonAvailable text: i18n("Game Center daemon is not running. Game launching is unavailable.") actions: [ Kirigami.Action { text: i18n("Retry") icon.name: "view-refresh" onTriggered: App.launcher.retryDaemonConnection() } ] } LibraryView { id: libraryView Layout.fillWidth: true Layout.fillHeight: true 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() } onDiagnosticsRequested: { diagnosticsSheet.game = root.selectedGame diagnosticsSheet.open() } onRemoveRequested: { if (!root.selectedGame) return root.pendingRemoveGame = root.selectedGame removeGameConfirmDialog.open() } onClosed: { libraryView.restoreFocus() } } DiagnosticsSheet { id: diagnosticsSheet game: root.selectedGame onClosed: { if (detailsSheet.opened) { detailsSheet.forceActiveFocus() return } libraryView.restoreFocus() } } 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() }) } 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") } } Connections { target: App.runnerManager function onLastErrorChanged() { if (!App.runnerManager) return let msg = String(App.runnerManager.lastError || "") if (msg.trim() === "") return showPassiveNotification(i18n("Runner error: %1", msg), "long") } } Component.onCompleted: { if (!root.isMobile) { const savedW = App.config.windowWidth const savedH = App.config.windowHeight if (savedW > 0) root.width = Math.max(root.minimumWidth, savedW) if (savedH > 0) root.height = Math.max(root.minimumHeight, savedH) root.windowGeometryRestored = true if (App.config.windowMaximized) { root.showMaximized() } } else { root.windowGeometryRestored = true } if (App.gameModel.count === 0) { importSheet.open() } } } }