// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2024 A-La-Karte Contributors import QtQuick import QtQuick.Controls as QQC2 import QtQuick.Layouts import org.kde.kirigami as Kirigami import org.kde.alakarte import "components" ColumnLayout { id: sidebarRoot spacing: 0 property string currentSource: "all" property string currentSourceName: i18n("All Games") property bool suppressAutoClose: false readonly property int adaptiveFocusRingWidth: 1 signal sourceSelected(string source) signal hiddenGamesRequested() signal settingsRequested() signal importRequested() signal aboutRequested() function focusList() { sourceList.forceActiveFocus() } function activateCurrentItem() { if (sourceList.currentItem && sourceList.currentItem.clicked) { sourceList.currentItem.clicked() } } function applySourceAtIndex(i) { if (i < 0 || i >= sourceModel.count) return let item = sourceModel.get(i) if (!item) return sourceList.currentIndex = i sidebarRoot.currentSource = item.sourceId sidebarRoot.currentSourceName = item.name sidebarRoot.sourceSelected(item.sourceId) sourceList.positionViewAtIndex(i, ListView.Contain) } function cycleSource(delta) { if (sourceModel.count <= 0) return let i = sourceList.currentIndex if (i < 0) i = 0 i = (i + delta) % sourceModel.count if (i < 0) i += sourceModel.count sidebarRoot.suppressAutoClose = true applySourceAtIndex(i) Qt.callLater(function() { sidebarRoot.suppressAutoClose = false }) } function iconInfoForPlatform(platformName) { let p = (platformName || "").toLowerCase() // Prefer icon-theme names to stay consistent with the user's Plasma theme. if (p.includes("steam")) return { source: "com.valvesoftware.Steam", fallback: "steam", resourceFallback: Qt.resolvedUrl("icons/brand/steam-symbolic.svg") } if (p.includes("itch")) return { source: "io.itch.itch", fallback: "itch", resourceFallback: Qt.resolvedUrl("icons/brand/itchdotio-symbolic.svg") } if (p.includes("retroarch")) return { source: "org.libretro.RetroArch", fallback: "retroarch", resourceFallback: Qt.resolvedUrl("icons/brand/retroarch-symbolic.svg") } if (p.includes("lutris")) return { source: "lutris", fallback: "applications-games" } if (p.includes("heroic")) return { source: "com.heroicgameslauncher.hgl", fallback: "heroic" } if (p.includes("bottles")) return { source: "com.usebottles.bottles", fallback: "application-x-executable" } if (p.includes("flatpak")) return { source: "flatpak-discover", fallback: "applications-games" } if (p.includes("desktop")) return { source: "user-desktop", fallback: "computer" } if (p.includes("legendary")) return { source: "legendary", fallback: "applications-games" } if (p.includes("manual")) return { source: "applications-other", fallback: "applications-games" } if (p.includes("other")) return { source: "applications-other", fallback: "applications-games" } return { source: "applications-games", fallback: "applications-games" } } QQC2.ScrollView { Layout.fillWidth: true Layout.fillHeight: true QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff ListView { id: sourceList model: sourceModel currentIndex: 0 leftMargin: Kirigami.Units.largeSpacing rightMargin: Kirigami.Units.largeSpacing topMargin: Kirigami.Units.smallSpacing bottomMargin: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing keyNavigationEnabled: true activeFocusOnTab: true Connections { target: GamepadManager function onNavigateUp() { if (sourceList.activeFocus) sourceList.decrementCurrentIndex() } function onNavigateDown() { if (sourceList.activeFocus) sourceList.incrementCurrentIndex() } function onSelectPressed() { if (sourceList.activeFocus) sidebarRoot.activateCurrentItem() } } delegate: QQC2.ItemDelegate { id: sourceDelegate width: ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin highlighted: ListView.isCurrentItem hoverEnabled: true background: Rectangle { radius: Kirigami.Units.smallSpacing color: sourceDelegate.highlighted ? Kirigami.Theme.highlightColor : Kirigami.Theme.alternateBackgroundColor opacity: sourceDelegate.highlighted ? 0.18 : (sourceDelegate.hovered ? 0.08 : 0.0) border.width: (sourceList.activeFocus && ListView.isCurrentItem) ? sidebarRoot.adaptiveFocusRingWidth : 0 border.color: Kirigami.Theme.highlightColor Rectangle { anchors.left: parent.left anchors.top: parent.top anchors.bottom: parent.bottom anchors.margins: Kirigami.Units.smallSpacing width: Kirigami.Units.smallSpacing radius: width / 2 color: Kirigami.Theme.highlightColor opacity: sourceDelegate.highlighted ? 0.9 : (sourceDelegate.hovered ? 0.45 : 0.0) Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } } } Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } } Behavior on border.width { NumberAnimation { duration: Kirigami.Units.shortDuration } } } leftPadding: Kirigami.Units.mediumSpacing rightPadding: Kirigami.Units.mediumSpacing topPadding: Kirigami.Units.smallSpacing bottomPadding: Kirigami.Units.smallSpacing contentItem: RowLayout { spacing: Kirigami.Units.mediumSpacing IconWithResourceFallback { primary: model.icon secondary: model.fallbackIcon ? model.fallbackIcon : "applications-games" resourceFallback: model.resourceFallback ? model.resourceFallback : "" Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium } QQC2.Label { text: model.name Layout.fillWidth: true elide: Text.ElideRight } QQC2.Label { text: model.count visible: model.count > 0 color: Kirigami.Theme.disabledTextColor font.pointSize: Kirigami.Theme.smallFont.pointSize } } onClicked: { sourceList.currentIndex = index sidebarRoot.currentSource = model.sourceId sidebarRoot.currentSourceName = model.name sidebarRoot.sourceSelected(model.sourceId) } Keys.onReturnPressed: clicked() Keys.onEnterPressed: clicked() } section.property: "section" section.delegate: Kirigami.ListSectionHeader { width: ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin text: section leftPadding: Kirigami.Units.mediumSpacing rightPadding: Kirigami.Units.mediumSpacing topPadding: Kirigami.Units.smallSpacing } } } Kirigami.Separator { Layout.fillWidth: true Layout.leftMargin: Kirigami.Units.largeSpacing Layout.rightMargin: Kirigami.Units.largeSpacing } QQC2.ItemDelegate { id: importAction Layout.fillWidth: true leftPadding: Kirigami.Units.largeSpacing rightPadding: Kirigami.Units.largeSpacing hoverEnabled: true activeFocusOnTab: true background: Rectangle { radius: Kirigami.Units.smallSpacing color: parent.activeFocus ? Kirigami.Theme.highlightColor : Kirigami.Theme.alternateBackgroundColor opacity: parent.activeFocus ? 0.18 : (parent.hovered ? 0.08 : 0.0) border.width: parent.activeFocus ? sidebarRoot.adaptiveFocusRingWidth : 0 border.color: Kirigami.Theme.highlightColor Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } } Behavior on border.width { NumberAnimation { duration: Kirigami.Units.shortDuration } } } icon.name: "document-import" icon.width: Kirigami.Units.iconSizes.smallMedium icon.height: Kirigami.Units.iconSizes.smallMedium text: i18n("Import Games") onClicked: sidebarRoot.importRequested() } QQC2.ItemDelegate { id: settingsAction Layout.fillWidth: true leftPadding: Kirigami.Units.largeSpacing rightPadding: Kirigami.Units.largeSpacing hoverEnabled: true activeFocusOnTab: true background: Rectangle { radius: Kirigami.Units.smallSpacing color: parent.activeFocus ? Kirigami.Theme.highlightColor : Kirigami.Theme.alternateBackgroundColor opacity: parent.activeFocus ? 0.18 : (parent.hovered ? 0.08 : 0.0) border.width: parent.activeFocus ? sidebarRoot.adaptiveFocusRingWidth : 0 border.color: Kirigami.Theme.highlightColor Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } } Behavior on border.width { NumberAnimation { duration: Kirigami.Units.shortDuration } } } icon.name: "configure" icon.width: Kirigami.Units.iconSizes.smallMedium icon.height: Kirigami.Units.iconSizes.smallMedium text: i18n("Settings") onClicked: sidebarRoot.settingsRequested() } QQC2.ItemDelegate { id: aboutAction Layout.fillWidth: true leftPadding: Kirigami.Units.largeSpacing rightPadding: Kirigami.Units.largeSpacing hoverEnabled: true activeFocusOnTab: true background: Rectangle { radius: Kirigami.Units.smallSpacing color: parent.activeFocus ? Kirigami.Theme.highlightColor : Kirigami.Theme.alternateBackgroundColor opacity: parent.activeFocus ? 0.18 : (parent.hovered ? 0.08 : 0.0) border.width: parent.activeFocus ? sidebarRoot.adaptiveFocusRingWidth : 0 border.color: Kirigami.Theme.highlightColor Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } } Behavior on border.width { NumberAnimation { duration: Kirigami.Units.shortDuration } } } icon.name: "help-about" icon.width: Kirigami.Units.iconSizes.smallMedium icon.height: Kirigami.Units.iconSizes.smallMedium text: i18n("About") onClicked: sidebarRoot.aboutRequested() } Connections { target: GamepadManager function onNavigateUp() { if (aboutAction.activeFocus) { settingsAction.forceActiveFocus() } else if (settingsAction.activeFocus) { importAction.forceActiveFocus() } else if (importAction.activeFocus) { sourceList.forceActiveFocus() sourceList.currentIndex = Math.max(0, sourceList.count - 1) } } function onNavigateDown() { if (importAction.activeFocus) { settingsAction.forceActiveFocus() } else if (settingsAction.activeFocus) { aboutAction.forceActiveFocus() } } function onSelectPressed() { if (importAction.activeFocus) { importAction.clicked() } else if (settingsAction.activeFocus) { settingsAction.clicked() } else if (aboutAction.activeFocus) { aboutAction.clicked() } } } ListModel { id: sourceModel Component.onCompleted: refresh() function refresh() { clear() let allCount = 0 let hiddenCount = 0 let favoritesCount = 0 let sources = {} for (let i = 0; i < App.gameModel.rowCount(); i++) { let game = App.gameModel.gameAt(i) if (game) { if (game.hidden) { hiddenCount++ } else { allCount++ if (game.favorite) { favoritesCount++ } let platform = game.platform if (!sources[platform]) { sources[platform] = 0 } sources[platform]++ } } } append({ name: i18n("All Games"), sourceId: "all", icon: "view-list-icons", count: allCount, section: i18n("Library") }) append({ name: i18n("Favorites"), sourceId: "favorites", icon: "bookmark-new", count: favoritesCount, section: i18n("Library") }) append({ name: i18n("Hidden"), sourceId: "hidden", icon: "view-hidden", count: hiddenCount, section: i18n("Library") }) for (let platform in sources) { let iconInfo = sidebarRoot.iconInfoForPlatform(platform) append({ name: platform, sourceId: platform, icon: iconInfo.source, fallbackIcon: iconInfo.fallback, resourceFallback: iconInfo.resourceFallback ? iconInfo.resourceFallback : "", count: sources[platform], section: i18n("Sources") }) } } } Connections { target: App.gameModel function onCountChanged() { sourceModel.refresh() } } Connections { target: App function onImportCompleted() { sourceModel.refresh() } } }