From 24b9ee64913a2b8a66b0a5c6b6eec678d0e4d879 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Sat, 24 Jan 2026 13:27:30 +0100 Subject: [PATCH] UI: Add hint bar and UI mode override Add a bottom hint bar with keyboard/gamepad hints and controller-specific icons. Also add a UI mode setting (auto/desktop/handheld) to improve the adaptive layout on different form factors. --- src/CMakeLists.txt | 37 +++ src/config.cpp | 18 ++ src/config.h | 13 + src/gamesortfiltermodel.cpp | 19 +- src/qml/GameCard.qml | 45 ++- src/qml/GameDetailsSheet.qml | 12 +- src/qml/GameEditDialog.qml | 81 +++++ src/qml/LibraryView.qml | 164 +++++++--- src/qml/Main.qml | 300 +++++++++++++++++- src/qml/SettingsPage.qml | 49 ++- src/qml/SidebarView.qml | 38 +++ src/qml/components/BottomHintBar.qml | 280 ++++++++++++++++ src/qml/components/GameGridView.qml | 6 +- src/qml/icons/gamepad/generic/back.svg | 5 + src/qml/icons/gamepad/generic/dpad-down.svg | 4 + src/qml/icons/gamepad/generic/dpad-left.svg | 4 + src/qml/icons/gamepad/generic/dpad-right.svg | 4 + src/qml/icons/gamepad/generic/dpad-up.svg | 4 + src/qml/icons/gamepad/generic/dpad.svg | 3 + src/qml/icons/gamepad/generic/east.svg | 4 + src/qml/icons/gamepad/generic/lb.svg | 4 + src/qml/icons/gamepad/generic/lstick.svg | 5 + src/qml/icons/gamepad/generic/lt.svg | 4 + src/qml/icons/gamepad/generic/menu.svg | 5 + src/qml/icons/gamepad/generic/north.svg | 4 + src/qml/icons/gamepad/generic/rb.svg | 4 + src/qml/icons/gamepad/generic/rstick.svg | 5 + src/qml/icons/gamepad/generic/rt.svg | 4 + src/qml/icons/gamepad/generic/south.svg | 4 + src/qml/icons/gamepad/generic/west.svg | 4 + src/qml/icons/gamepad/nintendo/a.svg | 4 + src/qml/icons/gamepad/nintendo/b.svg | 4 + src/qml/icons/gamepad/nintendo/home.svg | 4 + src/qml/icons/gamepad/nintendo/minus.svg | 4 + src/qml/icons/gamepad/nintendo/plus.svg | 5 + src/qml/icons/gamepad/nintendo/x.svg | 4 + src/qml/icons/gamepad/nintendo/y.svg | 4 + src/qml/icons/gamepad/playstation/circle.svg | 4 + src/qml/icons/gamepad/playstation/cross.svg | 4 + src/qml/icons/gamepad/playstation/options.svg | 5 + src/qml/icons/gamepad/playstation/share.svg | 5 + src/qml/icons/gamepad/playstation/square.svg | 4 + .../icons/gamepad/playstation/triangle.svg | 4 + src/qml/icons/gamepad/xbox/a.svg | 4 + src/qml/icons/gamepad/xbox/b.svg | 4 + src/qml/icons/gamepad/xbox/menu.svg | 5 + src/qml/icons/gamepad/xbox/view.svg | 5 + src/qml/icons/gamepad/xbox/x.svg | 4 + src/qml/icons/gamepad/xbox/y.svg | 4 + 49 files changed, 1140 insertions(+), 74 deletions(-) create mode 100644 src/qml/components/BottomHintBar.qml create mode 100644 src/qml/icons/gamepad/generic/back.svg create mode 100644 src/qml/icons/gamepad/generic/dpad-down.svg create mode 100644 src/qml/icons/gamepad/generic/dpad-left.svg create mode 100644 src/qml/icons/gamepad/generic/dpad-right.svg create mode 100644 src/qml/icons/gamepad/generic/dpad-up.svg create mode 100644 src/qml/icons/gamepad/generic/dpad.svg create mode 100644 src/qml/icons/gamepad/generic/east.svg create mode 100644 src/qml/icons/gamepad/generic/lb.svg create mode 100644 src/qml/icons/gamepad/generic/lstick.svg create mode 100644 src/qml/icons/gamepad/generic/lt.svg create mode 100644 src/qml/icons/gamepad/generic/menu.svg create mode 100644 src/qml/icons/gamepad/generic/north.svg create mode 100644 src/qml/icons/gamepad/generic/rb.svg create mode 100644 src/qml/icons/gamepad/generic/rstick.svg create mode 100644 src/qml/icons/gamepad/generic/rt.svg create mode 100644 src/qml/icons/gamepad/generic/south.svg create mode 100644 src/qml/icons/gamepad/generic/west.svg create mode 100644 src/qml/icons/gamepad/nintendo/a.svg create mode 100644 src/qml/icons/gamepad/nintendo/b.svg create mode 100644 src/qml/icons/gamepad/nintendo/home.svg create mode 100644 src/qml/icons/gamepad/nintendo/minus.svg create mode 100644 src/qml/icons/gamepad/nintendo/plus.svg create mode 100644 src/qml/icons/gamepad/nintendo/x.svg create mode 100644 src/qml/icons/gamepad/nintendo/y.svg create mode 100644 src/qml/icons/gamepad/playstation/circle.svg create mode 100644 src/qml/icons/gamepad/playstation/cross.svg create mode 100644 src/qml/icons/gamepad/playstation/options.svg create mode 100644 src/qml/icons/gamepad/playstation/share.svg create mode 100644 src/qml/icons/gamepad/playstation/square.svg create mode 100644 src/qml/icons/gamepad/playstation/triangle.svg create mode 100644 src/qml/icons/gamepad/xbox/a.svg create mode 100644 src/qml/icons/gamepad/xbox/b.svg create mode 100644 src/qml/icons/gamepad/xbox/menu.svg create mode 100644 src/qml/icons/gamepad/xbox/view.svg create mode 100644 src/qml/icons/gamepad/xbox/x.svg create mode 100644 src/qml/icons/gamepad/xbox/y.svg diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 107a940..2b296fc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -69,11 +69,48 @@ ecm_add_qml_module(alakarte URI org.kde.alakarte qml/components/SearchHeader.qml qml/components/IconWithResourceFallback.qml qml/components/EmptyState.qml + qml/components/BottomHintBar.qml RESOURCES qml/icons/brand/steam-symbolic.svg qml/icons/brand/itchdotio-symbolic.svg qml/icons/brand/retroarch-symbolic.svg qml/icons/app/org.kde.alakarte.svg + qml/icons/gamepad/playstation/cross.svg + qml/icons/gamepad/playstation/circle.svg + qml/icons/gamepad/playstation/square.svg + qml/icons/gamepad/playstation/triangle.svg + qml/icons/gamepad/playstation/options.svg + qml/icons/gamepad/playstation/share.svg + qml/icons/gamepad/xbox/a.svg + qml/icons/gamepad/xbox/b.svg + qml/icons/gamepad/xbox/x.svg + qml/icons/gamepad/xbox/y.svg + qml/icons/gamepad/xbox/menu.svg + qml/icons/gamepad/xbox/view.svg + qml/icons/gamepad/nintendo/a.svg + qml/icons/gamepad/nintendo/b.svg + qml/icons/gamepad/nintendo/x.svg + qml/icons/gamepad/nintendo/y.svg + qml/icons/gamepad/nintendo/plus.svg + qml/icons/gamepad/nintendo/minus.svg + qml/icons/gamepad/nintendo/home.svg + qml/icons/gamepad/generic/south.svg + qml/icons/gamepad/generic/east.svg + qml/icons/gamepad/generic/west.svg + qml/icons/gamepad/generic/north.svg + qml/icons/gamepad/generic/lb.svg + qml/icons/gamepad/generic/rb.svg + qml/icons/gamepad/generic/lt.svg + qml/icons/gamepad/generic/rt.svg + qml/icons/gamepad/generic/dpad.svg + qml/icons/gamepad/generic/dpad-up.svg + qml/icons/gamepad/generic/dpad-down.svg + qml/icons/gamepad/generic/dpad-left.svg + qml/icons/gamepad/generic/dpad-right.svg + qml/icons/gamepad/generic/lstick.svg + qml/icons/gamepad/generic/rstick.svg + qml/icons/gamepad/generic/menu.svg + qml/icons/gamepad/generic/back.svg SOURCES app.h gamemodel.h diff --git a/src/config.cpp b/src/config.cpp index e914254..3007377 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -26,6 +26,20 @@ void Config::setViewMode(ViewMode mode) } } +Config::UiMode Config::uiMode() const +{ + return m_uiMode; +} + +void Config::setUiMode(UiMode mode) +{ + if (m_uiMode != mode) { + m_uiMode = mode; + save(); + Q_EMIT uiModeChanged(); + } +} + int Config::gridSize() const { return m_gridSize; @@ -271,6 +285,7 @@ void Config::save() KConfigGroup generalGroup(config, QStringLiteral("General")); generalGroup.writeEntry(QStringLiteral("viewMode"), static_cast(m_viewMode)); + generalGroup.writeEntry(QStringLiteral("uiMode"), static_cast(m_uiMode)); generalGroup.writeEntry(QStringLiteral("gridSize"), m_gridSize); generalGroup.writeEntry(QStringLiteral("showPlatformBadges"), m_showPlatformBadges); generalGroup.writeEntry(QStringLiteral("theme"), m_theme); @@ -302,6 +317,7 @@ void Config::load() const KConfigGroup generalGroup(config, QStringLiteral("General")); m_viewMode = static_cast(generalGroup.readEntry(QStringLiteral("viewMode"), static_cast(GridView))); + m_uiMode = static_cast(generalGroup.readEntry(QStringLiteral("uiMode"), static_cast(Auto))); m_gridSize = generalGroup.readEntry(QStringLiteral("gridSize"), 180); m_showPlatformBadges = generalGroup.readEntry(QStringLiteral("showPlatformBadges"), true); m_theme = generalGroup.readEntry(QStringLiteral("theme"), QString()); @@ -328,6 +344,7 @@ void Config::load() void Config::resetToDefaults() { m_viewMode = GridView; + m_uiMode = Auto; m_gridSize = 180; m_showPlatformBadges = true; m_autoImportOnStartup = false; @@ -349,6 +366,7 @@ void Config::resetToDefaults() save(); Q_EMIT viewModeChanged(); + Q_EMIT uiModeChanged(); Q_EMIT gridSizeChanged(); Q_EMIT showPlatformBadgesChanged(); Q_EMIT autoImportOnStartupChanged(); diff --git a/src/config.h b/src/config.h index 854445d..5ec87f6 100644 --- a/src/config.h +++ b/src/config.h @@ -12,6 +12,7 @@ class Config : public QObject QML_ELEMENT Q_PROPERTY(ViewMode viewMode READ viewMode WRITE setViewMode NOTIFY viewModeChanged) + Q_PROPERTY(UiMode uiMode READ uiMode WRITE setUiMode NOTIFY uiModeChanged) Q_PROPERTY(int gridSize READ gridSize WRITE setGridSize NOTIFY gridSizeChanged) Q_PROPERTY(bool showPlatformBadges READ showPlatformBadges WRITE setShowPlatformBadges NOTIFY showPlatformBadgesChanged) Q_PROPERTY(bool autoImportOnStartup READ autoImportOnStartup WRITE setAutoImportOnStartup NOTIFY autoImportOnStartupChanged) @@ -38,11 +39,21 @@ public: }; Q_ENUM(ViewMode) + enum UiMode { + Auto, + Desktop, + Handheld + }; + Q_ENUM(UiMode) + explicit Config(QObject *parent = nullptr); ViewMode viewMode() const; void setViewMode(ViewMode mode); + UiMode uiMode() const; + void setUiMode(UiMode mode); + int gridSize() const; void setGridSize(int size); @@ -100,6 +111,7 @@ public: Q_SIGNALS: void viewModeChanged(); + void uiModeChanged(); void gridSizeChanged(); void showPlatformBadgesChanged(); void autoImportOnStartupChanged(); @@ -120,6 +132,7 @@ Q_SIGNALS: private: ViewMode m_viewMode = GridView; + UiMode m_uiMode = Auto; int m_gridSize = 180; bool m_showPlatformBadges = true; bool m_autoImportOnStartup = false; diff --git a/src/gamesortfiltermodel.cpp b/src/gamesortfiltermodel.cpp index a2a6c10..32fe4cf 100644 --- a/src/gamesortfiltermodel.cpp +++ b/src/gamesortfiltermodel.cpp @@ -10,6 +10,22 @@ GameSortFilterModel::GameSortFilterModel(QObject *parent) { setDynamicSortFilter(true); sort(0); + + connect(this, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &, int, int) { + Q_EMIT countChanged(); + }); + connect(this, &QAbstractItemModel::rowsRemoved, this, [this](const QModelIndex &, int, int) { + Q_EMIT countChanged(); + }); + connect(this, &QAbstractItemModel::rowsMoved, this, [this](const QModelIndex &, int, int, const QModelIndex &, int) { + Q_EMIT countChanged(); + }); + connect(this, &QAbstractItemModel::modelReset, this, [this] { + Q_EMIT countChanged(); + }); + connect(this, &QAbstractItemModel::layoutChanged, this, [this](const QList &, QAbstractItemModel::LayoutChangeHint) { + Q_EMIT countChanged(); + }); } QString GameSortFilterModel::filterText() const @@ -169,6 +185,7 @@ bool GameSortFilterModel::lessThan(const QModelIndex &left, const QModelIndex &r void GameSortFilterModel::invalidateAndEmit() { - invalidateFilter(); + beginFilterChange(); + endFilterChange(); Q_EMIT countChanged(); } diff --git a/src/qml/GameCard.qml b/src/qml/GameCard.qml index b15adda..a7280a3 100644 --- a/src/qml/GameCard.qml +++ b/src/qml/GameCard.qml @@ -15,9 +15,14 @@ FocusScope { property bool showPlayButton: true property bool focused: activeFocus - readonly property bool isTouchDevice: Kirigami.Settings.tabletMode || Kirigami.Settings.isMobile + readonly property bool isTouchDevice: { + let w = applicationWindow() + if (w && w.isTouchDevice !== undefined) return w.isTouchDevice + return Kirigami.Settings.tabletMode || Kirigami.Settings.isMobile + } readonly property int adaptiveFocusRingWidth: 1 readonly property real adaptiveHoverScale: 1.015 + readonly property real adaptiveFocusScale: 1.03 readonly property bool useAnimatedCover: App.config.animatedCovers && game @@ -38,24 +43,50 @@ FocusScope { color: Kirigami.Theme.backgroundColor shadow { - size: (hoverHandler.hovered || gameCard.focused) ? Kirigami.Units.mediumSpacing : Kirigami.Units.smallSpacing - color: (hoverHandler.hovered || gameCard.focused) ? Qt.rgba(0, 0, 0, 0.30) : Qt.rgba(0, 0, 0, 0.16) + size: gameCard.focused ? Kirigami.Units.mediumSpacing : (hoverHandler.hovered ? Kirigami.Units.smallSpacing * 1.5 : Kirigami.Units.smallSpacing) + color: gameCard.focused ? Qt.rgba(0, 0, 0, 0.34) : (hoverHandler.hovered ? Qt.rgba(0, 0, 0, 0.24) : Qt.rgba(0, 0, 0, 0.16)) + } + + Behavior on shadow.color { + ColorAnimation { duration: Kirigami.Units.shortDuration } } border.width: gameCard.focused ? gameCard.adaptiveFocusRingWidth : 0 border.color: Kirigami.Theme.highlightColor Behavior on border.width { - NumberAnimation { duration: Kirigami.Units.shortDuration } + NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } Behavior on shadow.size { - NumberAnimation { duration: Kirigami.Units.shortDuration } + NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } - scale: (hoverHandler.hovered || gameCard.focused) ? gameCard.adaptiveHoverScale : 1.0 + y: gameCard.focused ? -Kirigami.Units.smallSpacing : (hoverHandler.hovered ? -Kirigami.Units.smallSpacing * 0.5 : 0) + Behavior on y { + NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } + } + + scale: gameCard.focused ? gameCard.adaptiveFocusScale : (hoverHandler.hovered ? gameCard.adaptiveHoverScale : 1.0) Behavior on scale { - NumberAnimation { duration: Kirigami.Units.shortDuration } + NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } + } + + Rectangle { + anchors.fill: parent + radius: Kirigami.Units.mediumSpacing + color: "transparent" + border.width: 2 + border.color: Kirigami.Theme.highlightColor + opacity: 0.0 + visible: gameCard.focused + + SequentialAnimation on opacity { + running: gameCard.focused + loops: Animation.Infinite + NumberAnimation { from: 0.10; to: 0.28; duration: 900; easing.type: Easing.InOutQuad } + NumberAnimation { from: 0.28; to: 0.12; duration: 900; easing.type: Easing.InOutQuad } + } } // Cover image diff --git a/src/qml/GameDetailsSheet.qml b/src/qml/GameDetailsSheet.qml index b55dbe4..a2b6122 100644 --- a/src/qml/GameDetailsSheet.qml +++ b/src/qml/GameDetailsSheet.qml @@ -13,10 +13,18 @@ Kirigami.OverlaySheet { property var game - readonly property bool isMobile: Kirigami.Settings.isMobile + readonly property bool isMobile: { + let w = applicationWindow() + if (w && w.isMobile !== undefined) return w.isMobile + return Kirigami.Settings.isMobile + } readonly property bool isNarrowScreen: applicationWindow() && applicationWindow().width < Kirigami.Units.gridUnit * 30 readonly property bool useCompactLayout: isMobile || isNarrowScreen - readonly property bool isTouchDevice: Kirigami.Settings.tabletMode || Kirigami.Settings.isMobile + readonly property bool isTouchDevice: { + let w = applicationWindow() + if (w && w.isTouchDevice !== undefined) return w.isTouchDevice + return Kirigami.Settings.tabletMode || Kirigami.Settings.isMobile + } readonly property int actionIconButtonSize: isTouchDevice ? Math.round(Kirigami.Units.gridUnit * 2.5) : Math.round(Kirigami.Units.gridUnit * 2) diff --git a/src/qml/GameEditDialog.qml b/src/qml/GameEditDialog.qml index 20fc14f..19ee729 100644 --- a/src/qml/GameEditDialog.qml +++ b/src/qml/GameEditDialog.qml @@ -67,6 +67,48 @@ Kirigami.Dialog { property string selectedCoverPath: "" + function isDescendant(item, ancestor) { + let p = item + while (p) { + if (p === ancestor || (ancestor.contentItem && p === ancestor.contentItem)) 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 (dialog.isDescendant(next, dialog)) { + 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 + } + } + function loadFields() { selectedCoverPath = "" if (isEditing && game) { @@ -82,6 +124,45 @@ Kirigami.Dialog { } } + Connections { + target: GamepadManager + function onNavigateUp() { + if (!dialog.opened) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!dialog.isDescendant(w.activeFocusItem, dialog)) return + dialog.focusNextInChain(false) + } + function onNavigateDown() { + if (!dialog.opened) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!dialog.isDescendant(w.activeFocusItem, dialog)) return + dialog.focusNextInChain(true) + } + function onNavigateLeft() { + if (!dialog.opened) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!dialog.isDescendant(w.activeFocusItem, dialog)) return + dialog.focusNextInChain(false) + } + function onNavigateRight() { + if (!dialog.opened) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!dialog.isDescendant(w.activeFocusItem, dialog)) return + dialog.focusNextInChain(true) + } + function onSelectPressed() { + if (!dialog.opened) return + let w = applicationWindow() + if (!w || !w.activeFocusItem) return + if (!dialog.isDescendant(w.activeFocusItem, dialog)) return + dialog.activateFocused() + } + } + ColumnLayout { spacing: 0 diff --git a/src/qml/LibraryView.qml b/src/qml/LibraryView.qml index 6d782d1..7009993 100644 --- a/src/qml/LibraryView.qml +++ b/src/qml/LibraryView.qml @@ -4,6 +4,7 @@ import QtQuick import QtQuick.Controls as QQC2 import QtQuick.Layouts +import QtQuick.Effects import org.kde.kirigami as Kirigami import org.kde.alakarte import "components" @@ -21,20 +22,48 @@ FocusScope { signal gameSelected(var game) signal gameLaunched(var game) + readonly property int gameCount: proxyModel.count + property url focusedCoverUrl: "" + function focusSearch() { searchField.forceActiveFocus() } + function clearSearch() { + searchField.text = "" + proxyModel.filterText = "" + } + function restoreFocus() { if (libraryRoot.searchActive) { libraryRoot.focusSearch() } else { + if (libraryRoot.focusedIndex >= 0 && libraryRoot.focusedIndex < proxyModel.count) { + gameGrid.currentIndex = libraryRoot.focusedIndex + } gameGrid.forceActiveFocus() } } + function launchFocusedGame() { + if (!gameGrid || !proxyModel) return + let game = proxyModel.get(gameGrid.currentIndex) + if (game) { + libraryRoot.gameLaunched(game) + } + } + + function openDetailsForFocusedGame() { + if (!gameGrid || !proxyModel) return + let game = proxyModel.get(gameGrid.currentIndex) + if (game) { + libraryRoot.gameSelected(game) + } + } + onSearchActiveChanged: { if (!libraryRoot.searchActive) { + libraryRoot.clearSearch() Qt.callLater(function() { gameGrid.forceActiveFocus() }) @@ -45,6 +74,62 @@ FocusScope { anchors.fill: parent anchors.margins: 0 + Item { + anchors.fill: parent + visible: libraryRoot.gameCount > 0 + + Image { + id: backgroundCoverA + anchors.fill: parent + source: "" + fillMode: Image.PreserveAspectCrop + asynchronous: true + visible: source.toString().length > 0 + smooth: true + mipmap: App.config.highQualityImages + opacity: 0.0 + + Behavior on opacity { + NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic } + } + + layer.enabled: true + layer.effect: MultiEffect { + blurEnabled: true + blur: 0.9 + blurMax: 64 + } + } + + Image { + id: backgroundCoverB + anchors.fill: parent + source: "" + fillMode: Image.PreserveAspectCrop + asynchronous: true + visible: source.toString().length > 0 + smooth: true + mipmap: App.config.highQualityImages + opacity: 0.0 + + Behavior on opacity { + NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic } + } + + layer.enabled: true + layer.effect: MultiEffect { + blurEnabled: true + blur: 0.9 + blurMax: 64 + } + } + + Rectangle { + anchors.fill: parent + color: Qt.rgba(0, 0, 0, 0.55) + } + } + ColumnLayout { anchors.fill: parent spacing: Kirigami.Units.smallSpacing @@ -93,6 +178,27 @@ FocusScope { cardSize: libraryRoot.adaptiveCardSize + onCurrentIndexChanged: { + if (gameGrid.activeFocus) { + libraryRoot.focusedIndex = currentIndex + } + let game = proxyModel.get(currentIndex) + let url = (game && game.coverUrl) ? game.coverUrl : "" + if (url === libraryRoot.focusedCoverUrl) return + if (backgroundCoverA.opacity > 0.1) { + backgroundCoverB.source = url + backgroundCoverB.opacity = 0.0 + backgroundCoverA.opacity = 0.0 + Qt.callLater(function() { backgroundCoverB.opacity = 0.22 }) + } else { + backgroundCoverA.source = url + backgroundCoverA.opacity = 0.0 + backgroundCoverB.opacity = 0.0 + Qt.callLater(function() { backgroundCoverA.opacity = 0.22 }) + } + libraryRoot.focusedCoverUrl = url + } + model: GameSortFilterModel { id: proxyModel sourceModel: App.gameModel @@ -111,9 +217,17 @@ FocusScope { height: gameGrid.cellHeight function clicked() { + gameGrid.currentIndex = index + libraryRoot.focusedIndex = index card.clicked() } + function play() { + gameGrid.currentIndex = index + libraryRoot.focusedIndex = index + card.playClicked() + } + GameCard { id: card width: gameGrid.cardSize @@ -153,14 +267,13 @@ FocusScope { anchors.centerIn: parent visible: proxyModel.count === 0 && !App.importing - icon: proxyModel.filterText.length > 0 ? "edit-find" : "applications-games" + icon: proxyModel.filterText.length > 0 ? "edit-find" : (libraryRoot.filterSource === "favorites" ? "bookmark-new" : (libraryRoot.filterSource === "hidden" ? "view-hidden" : "applications-games")) title: proxyModel.filterText.length > 0 ? - i18n("No games found") : i18n("Your library is empty") + i18n("No games found") : (libraryRoot.filterSource === "favorites" ? i18n("No favorites yet") : (libraryRoot.filterSource === "hidden" ? i18n("No hidden games") : i18n("Your library is empty"))) description: proxyModel.filterText.length > 0 ? - i18n("Try adjusting your search") : - i18n("Import games to get started") + i18n("Try adjusting your search") : (libraryRoot.filterSource === "favorites" ? i18n("Mark games as favorites to see them here") : (libraryRoot.filterSource === "hidden" ? i18n("Hidden games will appear here") : i18n("Import games to get started"))) - actionText: proxyModel.filterText.length > 0 ? "" : i18n("Import Games") + actionText: (proxyModel.filterText.length > 0 || libraryRoot.filterSource === "favorites" || libraryRoot.filterSource === "hidden") ? "" : i18n("Import Games") onActionTriggered: App.importAllGames() } @@ -170,47 +283,6 @@ FocusScope { visible: App.importing } } - - QQC2.ToolBar { - Layout.fillWidth: true - visible: proxyModel.count > 0 - - leftPadding: 0 - rightPadding: 0 - topPadding: Kirigami.Units.smallSpacing - bottomPadding: Kirigami.Units.smallSpacing - - contentItem: RowLayout { - spacing: Kirigami.Units.mediumSpacing - - QQC2.Label { - text: i18np("%1 game", "%1 games", proxyModel.count) - color: Kirigami.Theme.disabledTextColor - font.pointSize: Kirigami.Theme.smallFont.pointSize - Layout.alignment: Qt.AlignVCenter - } - - Item { Layout.fillWidth: true } - - QQC2.Slider { - id: sizeSlider - from: 120 - to: 280 - stepSize: 20 - value: App.config.gridSize - Layout.preferredWidth: Kirigami.Units.gridUnit * 8 - Layout.alignment: Qt.AlignVCenter - - onMoved: App.config.gridSize = value - - QQC2.ToolTip { - parent: sizeSlider.handle - visible: sizeSlider.pressed - text: Math.round(sizeSlider.value) + " px" - } - } - } - } } } } diff --git a/src/qml/Main.qml b/src/qml/Main.qml index 621a53a..b922761 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -22,6 +22,16 @@ Kirigami.ApplicationWindow { property string currentSource: "all" property bool searchActive: false + readonly property string hintContext: { + if (gameEditDialog.visible) return "edit" + if (detailsSheet.opened) return "details" + if (importSheet.opened) return "import" + if (settingsSheet.opened) return "settings" + if (aboutSheet.opened) return "about" + if (sidebar.modal && sidebar.opened) return "sidebar" + return "library" + } + function closeTopmost() { if (gameEditDialog.visible) { gameEditDialog.close() @@ -54,6 +64,8 @@ Kirigami.ApplicationWindow { } if (searchActive) { searchActive = false + libraryView.clearSearch() + Qt.callLater(function() { libraryView.restoreFocus() }) return true } return false @@ -68,9 +80,30 @@ Kirigami.ApplicationWindow { && root.pageStack.layers.depth <= 1 } - readonly property bool isMobile: Kirigami.Settings.isMobile - readonly property bool isTablet: Kirigami.Settings.tabletMode && !Kirigami.Settings.isMobile - readonly property bool isTouchDevice: Kirigami.Settings.tabletMode || Kirigami.Settings.isMobile + 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 @@ -78,6 +111,83 @@ Kirigami.ApplicationWindow { 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: { @@ -134,15 +244,44 @@ Kirigami.ApplicationWindow { } } + Shortcut { + sequence: "F9" + onActivated: root.cycleUiMode() + } + Connections { target: GamepadManager function onBackPressed() { root.closeTopmost() } + function onLeftBumperPressed() { + if (detailsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsSheet.opened || aboutSheet.opened) return + if (root.hintContext !== "library" && root.hintContext !== "sidebar") return + sidebarView.cycleSource(-1) + } + function onRightBumperPressed() { + if (detailsSheet.opened || gameEditDialog.visible || importSheet.opened || settingsSheet.opened || aboutSheet.opened) return + if (root.hintContext !== "library" && root.hintContext !== "sidebar") return + sidebarView.cycleSource(1) + } + function onDetailsPressed() { + if (gameEditDialog.visible || importSheet.opened || settingsSheet.opened || aboutSheet.opened || (sidebar.modal && sidebar.opened)) return + if (detailsSheet.opened) { + if (root.selectedGame) { + root.selectedGame.favorite = !root.selectedGame.favorite + } + return + } + if (!libraryView || !libraryView.visible) return + libraryView.openDetailsForFocusedGame() + } function onMenuPressed() { if (!settingsSheet.opened) { if (root.canOpenSettings()) { settingsSheet.open() + } else if (detailsSheet.opened) { + detailsSheet.close() + Qt.callLater(function() { settingsSheet.open() }) } else { root.closeTopmost() } @@ -151,6 +290,11 @@ Kirigami.ApplicationWindow { } } function onSearchPressed() { + if (detailsSheet.opened) { + detailsSheet.editRequested() + return + } + if (gameEditDialog.visible || importSheet.opened || settingsSheet.opened || aboutSheet.opened || (sidebar.modal && sidebar.opened)) return searchActive = true Qt.callLater(function() { libraryView.focusSearch() }) } @@ -234,8 +378,10 @@ Kirigami.ApplicationWindow { onSourceSelected: function(source) { root.currentSource = source if (sidebar.modal) { - sidebar.close() - Qt.callLater(function() { libraryView.restoreFocus() }) + if (!sidebarView.suppressAutoClose) { + sidebar.close() + Qt.callLater(function() { libraryView.restoreFocus() }) + } } } @@ -278,6 +424,7 @@ Kirigami.ApplicationWindow { if (root.searchActive) { Qt.callLater(function() { libraryView.focusSearch() }) } else { + libraryView.clearSearch() libraryView.restoreFocus() } } @@ -293,11 +440,13 @@ Kirigami.ApplicationWindow { 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: settingsSheet.open() } ] @@ -379,6 +528,8 @@ Kirigami.ApplicationWindow { 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 @@ -421,14 +572,37 @@ Kirigami.ApplicationWindow { 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() } } @@ -497,7 +671,7 @@ Kirigami.ApplicationWindow { secondary: "steam" resourceFallback: Qt.resolvedUrl("icons/brand/steam-symbolic.svg") } - enabled: !App.importing + enabled: !App.importing && App.config.importSteam && !App.gameModel.hasPlatformPrefix("Steam") && App.gameModel.count >= 0 onClicked: App.importFromSteam() } @@ -511,7 +685,7 @@ Kirigami.ApplicationWindow { primary: "lutris" secondary: "applications-games" } - enabled: !App.importing + enabled: !App.importing && App.config.importLutris && !App.gameModel.hasPlatformPrefix("Lutris") && App.gameModel.count >= 0 onClicked: App.importFromLutris() } @@ -525,7 +699,7 @@ Kirigami.ApplicationWindow { primary: "com.heroicgameslauncher.hgl" secondary: "applications-games" } - enabled: !App.importing + enabled: !App.importing && App.config.importHeroic && !App.gameModel.hasPlatformPrefix("Heroic") && App.gameModel.count >= 0 onClicked: App.importFromHeroic() } @@ -539,7 +713,7 @@ Kirigami.ApplicationWindow { primary: "user-desktop" secondary: "computer" } - enabled: !App.importing + enabled: !App.importing && App.config.importDesktop && !App.gameModel.hasPlatformPrefix("Desktop") && App.gameModel.count >= 0 onClicked: App.importFromDesktop() } @@ -553,7 +727,7 @@ Kirigami.ApplicationWindow { primary: "com.usebottles.bottles" secondary: "application-x-executable" } - enabled: !App.importing + enabled: !App.importing && App.config.importBottles && !App.gameModel.hasPlatformPrefix("Bottles") && App.gameModel.count >= 0 onClicked: App.importFromBottles() } @@ -567,7 +741,7 @@ Kirigami.ApplicationWindow { primary: "flatpak-discover" secondary: "applications-games" } - enabled: !App.importing + enabled: !App.importing && App.config.importFlatpak && !App.gameModel.hasPlatformPrefix("Flatpak") && App.gameModel.count >= 0 onClicked: App.importFromFlatpak() } @@ -582,7 +756,7 @@ Kirigami.ApplicationWindow { secondary: "itch" resourceFallback: Qt.resolvedUrl("icons/brand/itchdotio-symbolic.svg") } - enabled: !App.importing + enabled: !App.importing && App.config.importItch && !App.gameModel.hasPlatformPrefix("itch.io") && App.gameModel.count >= 0 onClicked: App.importFromItch() } @@ -596,7 +770,7 @@ Kirigami.ApplicationWindow { primary: "legendary" secondary: "applications-games" } - enabled: !App.importing + enabled: !App.importing && App.config.importLegendary && !App.gameModel.hasPlatformPrefix("Legendary") && App.gameModel.count >= 0 onClicked: App.importFromLegendary() } @@ -611,7 +785,7 @@ Kirigami.ApplicationWindow { secondary: "retroarch" resourceFallback: Qt.resolvedUrl("icons/brand/retroarch-symbolic.svg") } - enabled: !App.importing + enabled: !App.importing && App.config.importRetroArch && !App.gameModel.hasPlatformPrefix("RetroArch") && App.gameModel.count >= 0 onClicked: App.importFromRetroArch() } } @@ -623,7 +797,15 @@ Kirigami.ApplicationWindow { QQC2.Button { text: i18n("Import All") icon.name: "document-import" - enabled: !App.importing + 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() } @@ -649,7 +831,7 @@ Kirigami.ApplicationWindow { } implicitHeight: { if (root.isMobile) return applicationWindow().height - return Math.min(applicationWindow().height - Kirigami.Units.gridUnit * 2, Kirigami.Units.gridUnit * 42) + return Math.min(applicationWindow().height - Kirigami.Units.gridUnit * 2, settingsContent.implicitHeight + Kirigami.Units.gridUnit * 4) } onOpened: settingsContent.focusFirstControl() @@ -697,6 +879,8 @@ Kirigami.ApplicationWindow { 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 @@ -815,8 +999,91 @@ Kirigami.ApplicationWindow { 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 @@ -844,6 +1111,7 @@ Kirigami.ApplicationWindow { } QQC2.ToolButton { + id: aboutCloseButton text: i18n("Close") icon.name: "dialog-close" display: QQC2.AbstractButton.IconOnly diff --git a/src/qml/SettingsPage.qml b/src/qml/SettingsPage.qml index 22b82cb..6eb1b88 100644 --- a/src/qml/SettingsPage.qml +++ b/src/qml/SettingsPage.qml @@ -31,6 +31,45 @@ ColumnLayout { checked: App.config.showPlatformBadges onToggled: App.config.showPlatformBadges = checked } + + FormCard.FormDelegateSeparator {} + + FormCard.FormButtonDelegate { + id: uiModeDelegate + text: i18n("UI mode") + description: { + if (App.config.uiMode === Config.Desktop) return i18n("Desktop") + if (App.config.uiMode === Config.Handheld) return i18n("Handheld") + return i18n("Automatic") + } + icon.name: "view-fullscreen" + onClicked: uiModeMenu.open() + + QQC2.Menu { + id: uiModeMenu + + QQC2.MenuItem { + text: i18n("Automatic") + checkable: true + checked: App.config.uiMode === Config.Auto + onTriggered: App.config.uiMode = Config.Auto + } + + QQC2.MenuItem { + text: i18n("Desktop") + checkable: true + checked: App.config.uiMode === Config.Desktop + onTriggered: App.config.uiMode = Config.Desktop + } + + QQC2.MenuItem { + text: i18n("Handheld") + checkable: true + checked: App.config.uiMode === Config.Handheld + onTriggered: App.config.uiMode = Config.Handheld + } + } + } } FormCard.FormHeader { @@ -293,7 +332,15 @@ ColumnLayout { text: i18n("Import All Games") description: i18n("Scan all enabled sources") icon.name: "document-import" - enabled: !App.importing + 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() } diff --git a/src/qml/SidebarView.qml b/src/qml/SidebarView.qml index 9fc8f98..756d327 100644 --- a/src/qml/SidebarView.qml +++ b/src/qml/SidebarView.qml @@ -14,6 +14,7 @@ ColumnLayout { property string currentSource: "all" property string currentSourceName: i18n("All Games") + property bool suppressAutoClose: false readonly property int adaptiveFocusRingWidth: 1 @@ -33,6 +34,28 @@ ColumnLayout { } } + 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() @@ -93,6 +116,21 @@ ColumnLayout { 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 } } diff --git a/src/qml/components/BottomHintBar.qml b/src/qml/components/BottomHintBar.qml new file mode 100644 index 0000000..ebc77c8 --- /dev/null +++ b/src/qml/components/BottomHintBar.qml @@ -0,0 +1,280 @@ +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kirigami as Kirigami +import org.kde.alakarte + +RowLayout { + id: root + + spacing: Kirigami.Units.largeSpacing + + readonly property bool useGamepadHints: GamepadManager.connected + readonly property int style: GamepadManager.controllerStyle + property string context: "library" + + function iconBasePath() { + if (!useGamepadHints) return "" + switch (style) { + case GamepadManager.PlayStationController: + return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/playstation/" + case GamepadManager.XboxController: + return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/xbox/" + case GamepadManager.NintendoController: + return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/nintendo/" + default: + return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/generic/" + } + } + + function iconForButton(buttonLabel) { + if (!useGamepadHints) return "" + const base = iconBasePath() + if (style === GamepadManager.PlayStationController) { + if (buttonLabel === "Cross") return base + "cross.svg" + if (buttonLabel === "Circle") return base + "circle.svg" + if (buttonLabel === "Square") return base + "square.svg" + if (buttonLabel === "Triangle") return base + "triangle.svg" + } + if (style === GamepadManager.XboxController || style === GamepadManager.NintendoController) { + return base + buttonLabel.toLowerCase() + ".svg" + } + if (buttonLabel === "A") return base + "south.svg" + if (buttonLabel === "B") return base + "east.svg" + if (buttonLabel === "X") return base + "west.svg" + if (buttonLabel === "Y") return base + "north.svg" + return base + "south.svg" + } + + function iconForAux(action) { + if (!useGamepadHints) return "" + + if (action === "lb") { + return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/generic/lb.svg" + } + if (action === "rb") { + return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/generic/rb.svg" + } + if (action === "menu") { + if (style === GamepadManager.PlayStationController) { + return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/playstation/options.svg" + } + if (style === GamepadManager.NintendoController) { + return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/nintendo/plus.svg" + } + if (style === GamepadManager.XboxController) { + return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/xbox/menu.svg" + } + return "qrc:/qt/qml/org/kde/alakarte/qml/icons/gamepad/generic/menu.svg" + } + + return "" + } + + function keyboardLabel(action) { + if (root.context === "library") { + switch (action) { + case "confirm": return "Space" + case "back": return "Esc" + case "details": return "Enter" + case "search": return "Ctrl+F" + case "menu": return "Ctrl+," + default: return "" + } + } + + if (root.context === "edit") { + switch (action) { + case "confirm": return "Enter" + case "back": return "Esc" + default: return "" + } + } + + if (root.context === "details") { + switch (action) { + case "confirm": return "Enter" + case "back": return "Esc" + case "menu": return "Ctrl+," + default: return "" + } + } + + if (root.context === "sidebar") { + switch (action) { + case "confirm": return "Enter" + case "back": return "Esc" + case "menu": return "Ctrl+," + default: return "" + } + } + + if (root.context === "settings" || root.context === "import" || root.context === "sidebar") { + switch (action) { + case "confirm": return "Enter" + case "back": return "Esc" + default: return "" + } + } + + return "" + } + + function actionLabel(action) { + if (root.context === "library") { + switch (action) { + case "confirm": return i18n("Play") + case "back": return i18n("Back") + case "details": return i18n("Details") + case "search": return i18n("Search") + case "lb": return i18n("Prev Source") + case "rb": return i18n("Next Source") + case "menu": return i18n("Settings") + default: return "" + } + } + + if (root.context === "edit") { + switch (action) { + case "confirm": return i18n("Select") + case "back": return i18n("Back") + default: return "" + } + } + + if (root.context === "details") { + switch (action) { + case "confirm": return i18n("Play") + case "back": return i18n("Back") + case "details": return i18n("Favorite") + case "search": return i18n("Edit") + case "menu": return i18n("Settings") + default: return "" + } + } + + if (root.context === "sidebar") { + switch (action) { + case "confirm": return i18n("Select") + case "back": return i18n("Back") + case "lb": return i18n("Prev Source") + case "rb": return i18n("Next Source") + case "menu": return i18n("Settings") + default: return "" + } + } + + if (root.context === "settings" || root.context === "import" || root.context === "sidebar") { + switch (action) { + case "confirm": return i18n("Select") + case "back": return i18n("Back") + default: return "" + } + } + + return "" + } + + component HintItem: RowLayout { + required property string action + property string label: "" + property string iconSource: "" + property string keyLabel: "" + + visible: { + if (root.useGamepadHints) return iconSource != "" && label.length > 0 + return keyLabel.length > 0 && label.length > 0 + } + + spacing: Kirigami.Units.smallSpacing + + Rectangle { + id: buttonFrame + radius: Kirigami.Units.smallSpacing + color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.08) + border.width: 1 + border.color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2) + + Layout.preferredHeight: Kirigami.Units.gridUnit * 1.4 + Layout.preferredWidth: useGamepadHints + ? Layout.preferredHeight + : Math.max(keyText.implicitWidth + Kirigami.Units.mediumSpacing * 2, Layout.preferredHeight) + + Image { + id: buttonIcon + anchors.centerIn: parent + width: parent.height * 0.7 + height: width + source: parent.parent.iconSource + visible: useGamepadHints && source != "" + sourceSize: Qt.size(width * 2, height * 2) + } + + QQC2.Label { + id: keyText + anchors.centerIn: parent + text: parent.parent.keyLabel + font.bold: true + font.pointSize: Kirigami.Theme.smallFont.pointSize + color: Kirigami.Theme.textColor + visible: !useGamepadHints + } + } + + QQC2.Label { + text: parent.label + color: Kirigami.Theme.textColor + font.pointSize: Kirigami.Theme.smallFont.pointSize + Layout.alignment: Qt.AlignVCenter + } + } + + HintItem { + action: "confirm" + label: actionLabel("confirm") + iconSource: iconForButton(GamepadManager.confirmButtonLabel) + keyLabel: keyboardLabel("confirm") + } + + HintItem { + action: "back" + label: actionLabel("back") + iconSource: iconForButton(GamepadManager.backButtonLabel) + keyLabel: keyboardLabel("back") + } + + HintItem { + action: "details" + label: actionLabel("details") + iconSource: iconForButton(GamepadManager.detailsButtonLabel) + keyLabel: keyboardLabel("details") + } + + HintItem { + action: "search" + label: actionLabel("search") + iconSource: iconForButton(GamepadManager.searchButtonLabel) + keyLabel: keyboardLabel("search") + } + + HintItem { + action: "lb" + label: actionLabel("lb") + iconSource: (root.context === "library" || root.context === "sidebar") ? iconForAux("lb") : "" + keyLabel: "" + } + + HintItem { + action: "rb" + label: actionLabel("rb") + iconSource: (root.context === "library" || root.context === "sidebar") ? iconForAux("rb") : "" + keyLabel: "" + } + + HintItem { + action: "menu" + label: actionLabel("menu") + iconSource: (root.context === "library" || root.context === "details" || root.context === "sidebar") ? iconForAux("menu") : "" + keyLabel: keyboardLabel("menu") + } +} diff --git a/src/qml/components/GameGridView.qml b/src/qml/components/GameGridView.qml index 81d9aea..e9fe4d3 100644 --- a/src/qml/components/GameGridView.qml +++ b/src/qml/components/GameGridView.qml @@ -62,7 +62,11 @@ GridView { function onNavigateRight() { if (gridView.activeFocus) gridView.navigateRight() } function onSelectPressed() { if (gridView.activeFocus && gridView.currentItem) { - gridView.currentItem.clicked() + if (gridView.currentItem.play) { + gridView.currentItem.play() + } else { + gridView.currentItem.clicked() + } } } } diff --git a/src/qml/icons/gamepad/generic/back.svg b/src/qml/icons/gamepad/generic/back.svg new file mode 100644 index 0000000..18208c8 --- /dev/null +++ b/src/qml/icons/gamepad/generic/back.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/qml/icons/gamepad/generic/dpad-down.svg b/src/qml/icons/gamepad/generic/dpad-down.svg new file mode 100644 index 0000000..3808900 --- /dev/null +++ b/src/qml/icons/gamepad/generic/dpad-down.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/qml/icons/gamepad/generic/dpad-left.svg b/src/qml/icons/gamepad/generic/dpad-left.svg new file mode 100644 index 0000000..afd17e6 --- /dev/null +++ b/src/qml/icons/gamepad/generic/dpad-left.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/qml/icons/gamepad/generic/dpad-right.svg b/src/qml/icons/gamepad/generic/dpad-right.svg new file mode 100644 index 0000000..3cf1a44 --- /dev/null +++ b/src/qml/icons/gamepad/generic/dpad-right.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/qml/icons/gamepad/generic/dpad-up.svg b/src/qml/icons/gamepad/generic/dpad-up.svg new file mode 100644 index 0000000..bc8173f --- /dev/null +++ b/src/qml/icons/gamepad/generic/dpad-up.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/qml/icons/gamepad/generic/dpad.svg b/src/qml/icons/gamepad/generic/dpad.svg new file mode 100644 index 0000000..e5eb97f --- /dev/null +++ b/src/qml/icons/gamepad/generic/dpad.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/qml/icons/gamepad/generic/east.svg b/src/qml/icons/gamepad/generic/east.svg new file mode 100644 index 0000000..78fb0de --- /dev/null +++ b/src/qml/icons/gamepad/generic/east.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/qml/icons/gamepad/generic/lb.svg b/src/qml/icons/gamepad/generic/lb.svg new file mode 100644 index 0000000..8815470 --- /dev/null +++ b/src/qml/icons/gamepad/generic/lb.svg @@ -0,0 +1,4 @@ + + + LB + diff --git a/src/qml/icons/gamepad/generic/lstick.svg b/src/qml/icons/gamepad/generic/lstick.svg new file mode 100644 index 0000000..c2bcffc --- /dev/null +++ b/src/qml/icons/gamepad/generic/lstick.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/qml/icons/gamepad/generic/lt.svg b/src/qml/icons/gamepad/generic/lt.svg new file mode 100644 index 0000000..95560a3 --- /dev/null +++ b/src/qml/icons/gamepad/generic/lt.svg @@ -0,0 +1,4 @@ + + + LT + diff --git a/src/qml/icons/gamepad/generic/menu.svg b/src/qml/icons/gamepad/generic/menu.svg new file mode 100644 index 0000000..4e6e5d9 --- /dev/null +++ b/src/qml/icons/gamepad/generic/menu.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/qml/icons/gamepad/generic/north.svg b/src/qml/icons/gamepad/generic/north.svg new file mode 100644 index 0000000..68d47b5 --- /dev/null +++ b/src/qml/icons/gamepad/generic/north.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/qml/icons/gamepad/generic/rb.svg b/src/qml/icons/gamepad/generic/rb.svg new file mode 100644 index 0000000..3ac99c3 --- /dev/null +++ b/src/qml/icons/gamepad/generic/rb.svg @@ -0,0 +1,4 @@ + + + RB + diff --git a/src/qml/icons/gamepad/generic/rstick.svg b/src/qml/icons/gamepad/generic/rstick.svg new file mode 100644 index 0000000..c2bcffc --- /dev/null +++ b/src/qml/icons/gamepad/generic/rstick.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/qml/icons/gamepad/generic/rt.svg b/src/qml/icons/gamepad/generic/rt.svg new file mode 100644 index 0000000..3bf67b0 --- /dev/null +++ b/src/qml/icons/gamepad/generic/rt.svg @@ -0,0 +1,4 @@ + + + RT + diff --git a/src/qml/icons/gamepad/generic/south.svg b/src/qml/icons/gamepad/generic/south.svg new file mode 100644 index 0000000..34073f8 --- /dev/null +++ b/src/qml/icons/gamepad/generic/south.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/qml/icons/gamepad/generic/west.svg b/src/qml/icons/gamepad/generic/west.svg new file mode 100644 index 0000000..0126bd7 --- /dev/null +++ b/src/qml/icons/gamepad/generic/west.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/qml/icons/gamepad/nintendo/a.svg b/src/qml/icons/gamepad/nintendo/a.svg new file mode 100644 index 0000000..fda4e4d --- /dev/null +++ b/src/qml/icons/gamepad/nintendo/a.svg @@ -0,0 +1,4 @@ + + + A + diff --git a/src/qml/icons/gamepad/nintendo/b.svg b/src/qml/icons/gamepad/nintendo/b.svg new file mode 100644 index 0000000..aae8d9e --- /dev/null +++ b/src/qml/icons/gamepad/nintendo/b.svg @@ -0,0 +1,4 @@ + + + B + diff --git a/src/qml/icons/gamepad/nintendo/home.svg b/src/qml/icons/gamepad/nintendo/home.svg new file mode 100644 index 0000000..8734ed9 --- /dev/null +++ b/src/qml/icons/gamepad/nintendo/home.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/qml/icons/gamepad/nintendo/minus.svg b/src/qml/icons/gamepad/nintendo/minus.svg new file mode 100644 index 0000000..cac96be --- /dev/null +++ b/src/qml/icons/gamepad/nintendo/minus.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/qml/icons/gamepad/nintendo/plus.svg b/src/qml/icons/gamepad/nintendo/plus.svg new file mode 100644 index 0000000..594cb25 --- /dev/null +++ b/src/qml/icons/gamepad/nintendo/plus.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/qml/icons/gamepad/nintendo/x.svg b/src/qml/icons/gamepad/nintendo/x.svg new file mode 100644 index 0000000..2853fc2 --- /dev/null +++ b/src/qml/icons/gamepad/nintendo/x.svg @@ -0,0 +1,4 @@ + + + X + diff --git a/src/qml/icons/gamepad/nintendo/y.svg b/src/qml/icons/gamepad/nintendo/y.svg new file mode 100644 index 0000000..8bcdfb7 --- /dev/null +++ b/src/qml/icons/gamepad/nintendo/y.svg @@ -0,0 +1,4 @@ + + + Y + diff --git a/src/qml/icons/gamepad/playstation/circle.svg b/src/qml/icons/gamepad/playstation/circle.svg new file mode 100644 index 0000000..8f60a86 --- /dev/null +++ b/src/qml/icons/gamepad/playstation/circle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/qml/icons/gamepad/playstation/cross.svg b/src/qml/icons/gamepad/playstation/cross.svg new file mode 100644 index 0000000..158ee2b --- /dev/null +++ b/src/qml/icons/gamepad/playstation/cross.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/qml/icons/gamepad/playstation/options.svg b/src/qml/icons/gamepad/playstation/options.svg new file mode 100644 index 0000000..4e6e5d9 --- /dev/null +++ b/src/qml/icons/gamepad/playstation/options.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/qml/icons/gamepad/playstation/share.svg b/src/qml/icons/gamepad/playstation/share.svg new file mode 100644 index 0000000..f6fc521 --- /dev/null +++ b/src/qml/icons/gamepad/playstation/share.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/qml/icons/gamepad/playstation/square.svg b/src/qml/icons/gamepad/playstation/square.svg new file mode 100644 index 0000000..6a82ac7 --- /dev/null +++ b/src/qml/icons/gamepad/playstation/square.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/qml/icons/gamepad/playstation/triangle.svg b/src/qml/icons/gamepad/playstation/triangle.svg new file mode 100644 index 0000000..6176daa --- /dev/null +++ b/src/qml/icons/gamepad/playstation/triangle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/qml/icons/gamepad/xbox/a.svg b/src/qml/icons/gamepad/xbox/a.svg new file mode 100644 index 0000000..fda4e4d --- /dev/null +++ b/src/qml/icons/gamepad/xbox/a.svg @@ -0,0 +1,4 @@ + + + A + diff --git a/src/qml/icons/gamepad/xbox/b.svg b/src/qml/icons/gamepad/xbox/b.svg new file mode 100644 index 0000000..aae8d9e --- /dev/null +++ b/src/qml/icons/gamepad/xbox/b.svg @@ -0,0 +1,4 @@ + + + B + diff --git a/src/qml/icons/gamepad/xbox/menu.svg b/src/qml/icons/gamepad/xbox/menu.svg new file mode 100644 index 0000000..4e6e5d9 --- /dev/null +++ b/src/qml/icons/gamepad/xbox/menu.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/qml/icons/gamepad/xbox/view.svg b/src/qml/icons/gamepad/xbox/view.svg new file mode 100644 index 0000000..18208c8 --- /dev/null +++ b/src/qml/icons/gamepad/xbox/view.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/qml/icons/gamepad/xbox/x.svg b/src/qml/icons/gamepad/xbox/x.svg new file mode 100644 index 0000000..2853fc2 --- /dev/null +++ b/src/qml/icons/gamepad/xbox/x.svg @@ -0,0 +1,4 @@ + + + X + diff --git a/src/qml/icons/gamepad/xbox/y.svg b/src/qml/icons/gamepad/xbox/y.svg new file mode 100644 index 0000000..8bcdfb7 --- /dev/null +++ b/src/qml/icons/gamepad/xbox/y.svg @@ -0,0 +1,4 @@ + + + Y +