From 2df35e9b81ca970e2fc4f79be32cdb9fb1993132 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Mon, 23 Mar 2026 11:42:31 +0100 Subject: [PATCH] fix: restore console UI icon visibility, selection highlight, focus transfer - Force isMask on header Kirigami.Icon contentItems (Search/Import/Settings) so white color is applied regardless of icon theme - Remove MultiEffect shadow from GameCard (was bleeding far outside card bounds) - Add gridActive prop to GameCard so selection highlight depends on actual flickable focus, not wrapper focus - Two-tier selection: full highlight when grid focused, dim border when grid unfocused (card stays visible) - Focus transfer: gamepad Up from top row emits headerFocusRequested; Down from rail/buttons calls libraryView.restoreFocus() - Gate gamepad navigation guards on grid.flickable.activeFocus (Maui.GridBrowser internal GridView) --- src/qml/ConsoleCategoryRail.qml | 112 ++++++++++++++++++++++++++++++-- src/qml/ConsoleGameDetail.qml | 32 +++++++-- src/qml/GameCard.qml | 30 +++------ src/qml/LibraryView.qml | 23 +++++-- src/qml/Main.qml | 35 ++++++++++ 5 files changed, 195 insertions(+), 37 deletions(-) diff --git a/src/qml/ConsoleCategoryRail.qml b/src/qml/ConsoleCategoryRail.qml index cc632c5..5a131d9 100644 --- a/src/qml/ConsoleCategoryRail.qml +++ b/src/qml/ConsoleCategoryRail.qml @@ -16,6 +16,15 @@ Item { signal sourceSelected(string source) signal settingsRequested() signal importRequested() + signal focusDownRequested() + + function focusCurrent() { + if (tabList.currentItem) { + tabList.currentItem.forceActiveFocus() + return + } + tabList.forceActiveFocus() + } function selectNext() { if (sourceModel.count <= 0) return @@ -136,12 +145,25 @@ Item { height: tabList.height - tabList.topMargin - tabList.bottomMargin readonly property bool isActive: model.sourceId === root.currentSource + readonly property bool isFocused: tabDelegate.activeFocus || tabDelegate.hovered - background: Rectangle { + hoverEnabled: true + scale: tabDelegate.isFocused ? 1.02 : (tabDelegate.isActive ? 1.01 : 1.0) + Behavior on scale { + NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } + } + + background: Kirigami.ShadowedRectangle { radius: Kirigami.Units.smallSpacing color: tabDelegate.isActive ? Kirigami.Theme.highlightColor - : (tabDelegate.hovered ? Kirigami.Theme.alternateBackgroundColor : "transparent") - opacity: tabDelegate.isActive ? 0.22 : (tabDelegate.hovered ? 0.12 : 0.0) + : Kirigami.Theme.alternateBackgroundColor + opacity: tabDelegate.isActive ? 0.20 : (tabDelegate.isFocused ? 0.10 : 0.0) + + border.width: tabDelegate.isFocused ? 2 : 0 + border.color: Kirigami.Theme.highlightColor + + shadow.size: tabDelegate.isFocused ? Kirigami.Units.mediumSpacing : 0 + shadow.color: Qt.rgba(0, 0, 0, 0.28) Rectangle { anchors.bottom: parent.bottom @@ -153,7 +175,17 @@ Item { visible: tabDelegate.isActive } - Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } } + Behavior on opacity { + NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } + } + + Behavior on shadow.size { + NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } + } + + Behavior on border.width { + NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } + } } leftPadding: Kirigami.Units.mediumSpacing @@ -184,6 +216,8 @@ Item { root.currentSource = model.sourceId root.sourceSelected(model.sourceId) } + + Keys.onDownPressed: root.focusDownRequested() } } } @@ -191,23 +225,89 @@ Item { Kirigami.Separator { Layout.fillHeight: true } QQC2.ToolButton { + id: importBtn icon.name: "document-import" onClicked: root.importRequested() Layout.fillHeight: true - flat: true + hoverEnabled: true + scale: activeFocus ? 1.02 : (hovered ? 1.01 : 1.0) + Behavior on scale { + NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } + } + + contentItem: Kirigami.Icon { + source: "document-import" + isMask: true + implicitWidth: Kirigami.Units.iconSizes.smallMedium + implicitHeight: Kirigami.Units.iconSizes.smallMedium + width: implicitWidth + height: implicitHeight + anchors.centerIn: parent + color: "white" + } + + background: Kirigami.ShadowedRectangle { + radius: Kirigami.Units.smallSpacing + color: Kirigami.Theme.alternateBackgroundColor + opacity: parent.activeFocus ? 0.18 : (parent.hovered ? 0.10 : 0.0) + border.width: parent.activeFocus ? 2 : 0 + border.color: Kirigami.Theme.highlightColor + shadow.size: parent.activeFocus ? Kirigami.Units.mediumSpacing : 0 + shadow.color: Qt.rgba(0, 0, 0, 0.28) + + Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } + Behavior on shadow.size { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } + Behavior on border.width { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } + } + QQC2.ToolTip.text: i18n("Import Games") QQC2.ToolTip.visible: hovered QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + + Keys.onDownPressed: root.focusDownRequested() } QQC2.ToolButton { + id: settingsBtn icon.name: "configure" onClicked: root.settingsRequested() Layout.fillHeight: true - flat: true + hoverEnabled: true + scale: activeFocus ? 1.02 : (hovered ? 1.01 : 1.0) + Behavior on scale { + NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } + } + + contentItem: Kirigami.Icon { + source: "configure" + isMask: true + implicitWidth: Kirigami.Units.iconSizes.smallMedium + implicitHeight: Kirigami.Units.iconSizes.smallMedium + width: implicitWidth + height: implicitHeight + anchors.centerIn: parent + color: "white" + } + + background: Kirigami.ShadowedRectangle { + radius: Kirigami.Units.smallSpacing + color: Kirigami.Theme.alternateBackgroundColor + opacity: parent.activeFocus ? 0.18 : (parent.hovered ? 0.10 : 0.0) + border.width: parent.activeFocus ? 2 : 0 + border.color: Kirigami.Theme.highlightColor + shadow.size: parent.activeFocus ? Kirigami.Units.mediumSpacing : 0 + shadow.color: Qt.rgba(0, 0, 0, 0.28) + + Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } + Behavior on shadow.size { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } + Behavior on border.width { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } + } + QQC2.ToolTip.text: i18n("Settings") QQC2.ToolTip.visible: hovered QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + + Keys.onDownPressed: root.focusDownRequested() } } } diff --git a/src/qml/ConsoleGameDetail.qml b/src/qml/ConsoleGameDetail.qml index b3ca345..425cbcf 100644 --- a/src/qml/ConsoleGameDetail.qml +++ b/src/qml/ConsoleGameDetail.qml @@ -201,6 +201,12 @@ Item { focus: true KeyNavigation.right: favoriteBtn + hoverEnabled: true + scale: activeFocus ? 1.03 : (hovered ? 1.01 : 1.0) + Behavior on scale { + NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } + } + contentItem: RowLayout { spacing: 8 Kirigami.Icon { @@ -223,7 +229,7 @@ Item { : (playBtn.hovered ? Qt.rgba(1,1,1,0.22) : Qt.rgba(1,1,1,0.14)) border.color: playBtn.activeFocus ? Kirigami.Theme.highlightColor : Qt.rgba(1,1,1,0.28) border.width: playBtn.activeFocus ? 0 : 1 - Behavior on color { ColorAnimation { duration: 100 } } + Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } } implicitWidth: 160 @@ -240,6 +246,12 @@ Item { KeyNavigation.left: playBtn KeyNavigation.right: editBtn + hoverEnabled: true + scale: activeFocus ? 1.03 : (hovered ? 1.01 : 1.0) + Behavior on scale { + NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } + } + contentItem: Kirigami.Icon { source: detailRoot.game && detailRoot.game.favorite ? "starred-symbolic" : "non-starred-symbolic" width: 22; height: 22 @@ -253,7 +265,7 @@ Item { : (favoriteBtn.hovered ? Qt.rgba(1,1,1,0.18) : Qt.rgba(1,1,1,0.10)) border.color: favoriteBtn.activeFocus ? "white" : Qt.rgba(1,1,1,0.25) border.width: 1 - Behavior on color { ColorAnimation { duration: 100 } } + Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } } implicitWidth: 52 @@ -272,6 +284,12 @@ Item { KeyNavigation.left: favoriteBtn KeyNavigation.right: closeBtn + hoverEnabled: true + scale: activeFocus ? 1.03 : (hovered ? 1.01 : 1.0) + Behavior on scale { + NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } + } + contentItem: Kirigami.Icon { source: "document-edit" width: 20; height: 20 @@ -285,7 +303,7 @@ Item { : (editBtn.hovered ? Qt.rgba(1,1,1,0.18) : Qt.rgba(1,1,1,0.10)) border.color: editBtn.activeFocus ? "white" : Qt.rgba(1,1,1,0.25) border.width: 1 - Behavior on color { ColorAnimation { duration: 100 } } + Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } } implicitWidth: 52 @@ -302,6 +320,12 @@ Item { icon.name: "window-close" KeyNavigation.left: editBtn + hoverEnabled: true + scale: activeFocus ? 1.03 : (hovered ? 1.01 : 1.0) + Behavior on scale { + NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } + } + contentItem: Kirigami.Icon { source: "window-close" width: 20; height: 20 @@ -315,7 +339,7 @@ Item { : (closeBtn.hovered ? Qt.rgba(1,1,1,0.18) : Qt.rgba(1,1,1,0.10)) border.color: closeBtn.activeFocus ? "white" : Qt.rgba(1,1,1,0.25) border.width: 1 - Behavior on color { ColorAnimation { duration: 100 } } + Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } } implicitWidth: 52 diff --git a/src/qml/GameCard.qml b/src/qml/GameCard.qml index 38f2d6e..ab4476d 100644 --- a/src/qml/GameCard.qml +++ b/src/qml/GameCard.qml @@ -4,7 +4,6 @@ import QtQuick import QtQuick.Controls as QQC2 import QtQuick.Layouts -import QtQuick.Effects import org.kde.kirigami as Kirigami import org.kde.alakarte @@ -12,10 +11,13 @@ Item { id: gameCard property var game + property bool gridActive: false signal clicked() signal playClicked() readonly property bool isCurrent: GridView.isCurrentItem + readonly property bool isGridFocused: gridActive + readonly property bool isHighlighted: isCurrent && isGridFocused readonly property bool useAnimatedCover: App.config.animatedCovers && game && game.coverUrl @@ -32,21 +34,9 @@ Item { width: parent.width - 16 height: parent.height - 16 - scale: gameCard.isCurrent ? 1.08 : (hoverHandler.hovered ? 1.03 : 1.0) + scale: gameCard.isHighlighted ? 1.08 : ((gameCard.isCurrent || hoverHandler.hovered) ? 1.03 : 1.0) Behavior on scale { - NumberAnimation { duration: 140; easing.type: Easing.OutCubic } - } - - layer.enabled: gameCard.isCurrent - layer.effect: MultiEffect { - shadowEnabled: true - shadowColor: Qt.rgba( - Kirigami.Theme.highlightColor.r, - Kirigami.Theme.highlightColor.g, - Kirigami.Theme.highlightColor.b, 0.55) - shadowBlur: 0.8 - shadowHorizontalOffset: 0 - shadowVerticalOffset: 6 + NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } Rectangle { @@ -73,7 +63,7 @@ Item { source: game ? game.coverUrl : "" fillMode: Image.PreserveAspectCrop asynchronous: true - playing: gameCard.isCurrent + playing: gameCard.isHighlighted visible: gameCard.useAnimatedCover smooth: true } @@ -150,10 +140,10 @@ Item { radius: 8 color: "transparent" border.color: Kirigami.Theme.highlightColor - border.width: gameCard.isCurrent ? 3 : 0 - opacity: gameCard.isCurrent ? 1.0 : 0.0 - Behavior on opacity { NumberAnimation { duration: 120 } } - Behavior on border.width { NumberAnimation { duration: 120 } } + border.width: gameCard.isHighlighted ? 3 : (gameCard.isCurrent ? 2 : 0) + opacity: gameCard.isHighlighted ? 1.0 : (gameCard.isCurrent ? 0.55 : 0.0) + Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } + Behavior on border.width { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } } HoverHandler { id: hoverHandler } diff --git a/src/qml/LibraryView.qml b/src/qml/LibraryView.qml index 4f2df30..52e01d1 100644 --- a/src/qml/LibraryView.qml +++ b/src/qml/LibraryView.qml @@ -17,11 +17,16 @@ FocusScope { signal gameSelected(var game) signal gameFocused(var game) signal gameLaunched(var game) + signal headerFocusRequested() readonly property int gameCount: proxyModel.count function restoreFocus() { - grid.forceActiveFocus() + if (grid.flickable) { + grid.flickable.forceActiveFocus() + } else { + grid.forceActiveFocus() + } } function focusedGame() { @@ -85,6 +90,7 @@ FocusScope { delegate: GameCard { game: model.gameObject + gridActive: grid.flickable ? grid.flickable.activeFocus : grid.activeFocus onClicked: { grid.currentIndex = index @@ -109,26 +115,29 @@ FocusScope { Connections { target: GamepadManager function onSelectPressed() { - if (!grid.activeFocus) return + if (!grid.flickable || !grid.flickable.activeFocus) return libraryRoot.selectFocused() } function onNavigateUp() { - if (!grid.activeFocus) return - if (grid.currentIndex <= 0) return + if (!grid.flickable || !grid.flickable.activeFocus) return let cols = Math.max(1, Math.floor(grid.width / grid.cellWidth)) + if (grid.currentIndex < cols) { + libraryRoot.headerFocusRequested() + return + } grid.currentIndex = Math.max(0, grid.currentIndex - cols) } function onNavigateDown() { - if (!grid.activeFocus) return + if (!grid.flickable || !grid.flickable.activeFocus) return let cols = Math.max(1, Math.floor(grid.width / grid.cellWidth)) grid.currentIndex = Math.min(proxyModel.count - 1, grid.currentIndex + cols) } function onNavigateLeft() { - if (!grid.activeFocus) return + if (!grid.flickable || !grid.flickable.activeFocus) return grid.currentIndex = Math.max(0, grid.currentIndex - 1) } function onNavigateRight() { - if (!grid.activeFocus) return + if (!grid.flickable || !grid.flickable.activeFocus) return grid.currentIndex = Math.min(proxyModel.count - 1, grid.currentIndex + 1) } } diff --git a/src/qml/Main.qml b/src/qml/Main.qml index b631a20..6e30ddb 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -6,6 +6,7 @@ import QtQuick.Controls as QQC2 import QtQuick.Layouts import QtQuick.Effects import org.mauikit.controls as Maui +import org.kde.kirigami as Kirigami import org.kde.alakarte import "components" @@ -120,16 +121,48 @@ Maui.ApplicationWindow { onSourceSelected: function(src) { root.currentCategory = src } onSettingsRequested: root.settingsVisible = true onImportRequested: App.importAllGames() + onFocusDownRequested: libraryView.restoreFocus() } QQC2.ToolButton { + id: searchButton icon.name: "edit-find" flat: true Layout.preferredWidth: 40 Layout.preferredHeight: 40 + hoverEnabled: true + scale: activeFocus ? 1.02 : (hovered ? 1.01 : 1.0) + Behavior on scale { + NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } + } onClicked: root.searchVisible = !root.searchVisible QQC2.ToolTip.text: i18n("Search") QQC2.ToolTip.visible: hovered + + contentItem: Kirigami.Icon { + source: "edit-find" + isMask: true + implicitWidth: Kirigami.Units.iconSizes.smallMedium + implicitHeight: Kirigami.Units.iconSizes.smallMedium + width: implicitWidth + height: implicitHeight + anchors.centerIn: parent + color: "white" + } + + Keys.onDownPressed: libraryView.restoreFocus() + + background: Rectangle { + radius: 8 + color: parent.activeFocus + ? Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, 0.18) + : (parent.hovered ? Qt.rgba(1, 1, 1, 0.10) : Qt.rgba(1, 1, 1, 0.0)) + border.width: parent.activeFocus ? 2 : 0 + border.color: Kirigami.Theme.highlightColor + + Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } + Behavior on border.width { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } } + } } } } @@ -159,6 +192,8 @@ Maui.ApplicationWindow { filterSource: root.currentCategory filterText: root.searchText + onHeaderFocusRequested: categoryRail.focusCurrent() + onGameFocused: function(game) { root.focusedGame = game } onGameSelected: function(game) { root.selectedGame = game