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)
This commit is contained in:
Marco Allegretti 2026-03-23 11:42:31 +01:00
parent b5e03fe856
commit 2df35e9b81
5 changed files with 195 additions and 37 deletions

View file

@ -16,6 +16,15 @@ Item {
signal sourceSelected(string source) signal sourceSelected(string source)
signal settingsRequested() signal settingsRequested()
signal importRequested() signal importRequested()
signal focusDownRequested()
function focusCurrent() {
if (tabList.currentItem) {
tabList.currentItem.forceActiveFocus()
return
}
tabList.forceActiveFocus()
}
function selectNext() { function selectNext() {
if (sourceModel.count <= 0) return if (sourceModel.count <= 0) return
@ -136,12 +145,25 @@ Item {
height: tabList.height - tabList.topMargin - tabList.bottomMargin height: tabList.height - tabList.topMargin - tabList.bottomMargin
readonly property bool isActive: model.sourceId === root.currentSource 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 radius: Kirigami.Units.smallSpacing
color: tabDelegate.isActive ? Kirigami.Theme.highlightColor color: tabDelegate.isActive ? Kirigami.Theme.highlightColor
: (tabDelegate.hovered ? Kirigami.Theme.alternateBackgroundColor : "transparent") : Kirigami.Theme.alternateBackgroundColor
opacity: tabDelegate.isActive ? 0.22 : (tabDelegate.hovered ? 0.12 : 0.0) 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 { Rectangle {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
@ -153,7 +175,17 @@ Item {
visible: tabDelegate.isActive 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 leftPadding: Kirigami.Units.mediumSpacing
@ -184,6 +216,8 @@ Item {
root.currentSource = model.sourceId root.currentSource = model.sourceId
root.sourceSelected(model.sourceId) root.sourceSelected(model.sourceId)
} }
Keys.onDownPressed: root.focusDownRequested()
} }
} }
} }
@ -191,23 +225,89 @@ Item {
Kirigami.Separator { Layout.fillHeight: true } Kirigami.Separator { Layout.fillHeight: true }
QQC2.ToolButton { QQC2.ToolButton {
id: importBtn
icon.name: "document-import" icon.name: "document-import"
onClicked: root.importRequested() onClicked: root.importRequested()
Layout.fillHeight: true 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.text: i18n("Import Games")
QQC2.ToolTip.visible: hovered QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
Keys.onDownPressed: root.focusDownRequested()
} }
QQC2.ToolButton { QQC2.ToolButton {
id: settingsBtn
icon.name: "configure" icon.name: "configure"
onClicked: root.settingsRequested() onClicked: root.settingsRequested()
Layout.fillHeight: true 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.text: i18n("Settings")
QQC2.ToolTip.visible: hovered QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
Keys.onDownPressed: root.focusDownRequested()
} }
} }
} }

View file

@ -201,6 +201,12 @@ Item {
focus: true focus: true
KeyNavigation.right: favoriteBtn 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 { contentItem: RowLayout {
spacing: 8 spacing: 8
Kirigami.Icon { Kirigami.Icon {
@ -223,7 +229,7 @@ Item {
: (playBtn.hovered ? Qt.rgba(1,1,1,0.22) : Qt.rgba(1,1,1,0.14)) : (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.color: playBtn.activeFocus ? Kirigami.Theme.highlightColor : Qt.rgba(1,1,1,0.28)
border.width: playBtn.activeFocus ? 0 : 1 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 implicitWidth: 160
@ -240,6 +246,12 @@ Item {
KeyNavigation.left: playBtn KeyNavigation.left: playBtn
KeyNavigation.right: editBtn 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 { contentItem: Kirigami.Icon {
source: detailRoot.game && detailRoot.game.favorite ? "starred-symbolic" : "non-starred-symbolic" source: detailRoot.game && detailRoot.game.favorite ? "starred-symbolic" : "non-starred-symbolic"
width: 22; height: 22 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)) : (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.color: favoriteBtn.activeFocus ? "white" : Qt.rgba(1,1,1,0.25)
border.width: 1 border.width: 1
Behavior on color { ColorAnimation { duration: 100 } } Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } }
} }
implicitWidth: 52 implicitWidth: 52
@ -272,6 +284,12 @@ Item {
KeyNavigation.left: favoriteBtn KeyNavigation.left: favoriteBtn
KeyNavigation.right: closeBtn 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 { contentItem: Kirigami.Icon {
source: "document-edit" source: "document-edit"
width: 20; height: 20 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)) : (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.color: editBtn.activeFocus ? "white" : Qt.rgba(1,1,1,0.25)
border.width: 1 border.width: 1
Behavior on color { ColorAnimation { duration: 100 } } Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } }
} }
implicitWidth: 52 implicitWidth: 52
@ -302,6 +320,12 @@ Item {
icon.name: "window-close" icon.name: "window-close"
KeyNavigation.left: editBtn 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 { contentItem: Kirigami.Icon {
source: "window-close" source: "window-close"
width: 20; height: 20 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)) : (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.color: closeBtn.activeFocus ? "white" : Qt.rgba(1,1,1,0.25)
border.width: 1 border.width: 1
Behavior on color { ColorAnimation { duration: 100 } } Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } }
} }
implicitWidth: 52 implicitWidth: 52

View file

@ -4,7 +4,6 @@
import QtQuick import QtQuick
import QtQuick.Controls as QQC2 import QtQuick.Controls as QQC2
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Effects
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.alakarte import org.kde.alakarte
@ -12,10 +11,13 @@ Item {
id: gameCard id: gameCard
property var game property var game
property bool gridActive: false
signal clicked() signal clicked()
signal playClicked() signal playClicked()
readonly property bool isCurrent: GridView.isCurrentItem readonly property bool isCurrent: GridView.isCurrentItem
readonly property bool isGridFocused: gridActive
readonly property bool isHighlighted: isCurrent && isGridFocused
readonly property bool useAnimatedCover: App.config.animatedCovers readonly property bool useAnimatedCover: App.config.animatedCovers
&& game && game.coverUrl && game && game.coverUrl
@ -32,21 +34,9 @@ Item {
width: parent.width - 16 width: parent.width - 16
height: parent.height - 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 { Behavior on scale {
NumberAnimation { duration: 140; easing.type: Easing.OutCubic } NumberAnimation { duration: Kirigami.Units.shortDuration; 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
} }
Rectangle { Rectangle {
@ -73,7 +63,7 @@ Item {
source: game ? game.coverUrl : "" source: game ? game.coverUrl : ""
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
asynchronous: true asynchronous: true
playing: gameCard.isCurrent playing: gameCard.isHighlighted
visible: gameCard.useAnimatedCover visible: gameCard.useAnimatedCover
smooth: true smooth: true
} }
@ -150,10 +140,10 @@ Item {
radius: 8 radius: 8
color: "transparent" color: "transparent"
border.color: Kirigami.Theme.highlightColor border.color: Kirigami.Theme.highlightColor
border.width: gameCard.isCurrent ? 3 : 0 border.width: gameCard.isHighlighted ? 3 : (gameCard.isCurrent ? 2 : 0)
opacity: gameCard.isCurrent ? 1.0 : 0.0 opacity: gameCard.isHighlighted ? 1.0 : (gameCard.isCurrent ? 0.55 : 0.0)
Behavior on opacity { NumberAnimation { duration: 120 } } Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } }
Behavior on border.width { NumberAnimation { duration: 120 } } Behavior on border.width { NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } }
} }
HoverHandler { id: hoverHandler } HoverHandler { id: hoverHandler }

View file

@ -17,12 +17,17 @@ FocusScope {
signal gameSelected(var game) signal gameSelected(var game)
signal gameFocused(var game) signal gameFocused(var game)
signal gameLaunched(var game) signal gameLaunched(var game)
signal headerFocusRequested()
readonly property int gameCount: proxyModel.count readonly property int gameCount: proxyModel.count
function restoreFocus() { function restoreFocus() {
if (grid.flickable) {
grid.flickable.forceActiveFocus()
} else {
grid.forceActiveFocus() grid.forceActiveFocus()
} }
}
function focusedGame() { function focusedGame() {
if (grid.currentIndex < 0) return null if (grid.currentIndex < 0) return null
@ -85,6 +90,7 @@ FocusScope {
delegate: GameCard { delegate: GameCard {
game: model.gameObject game: model.gameObject
gridActive: grid.flickable ? grid.flickable.activeFocus : grid.activeFocus
onClicked: { onClicked: {
grid.currentIndex = index grid.currentIndex = index
@ -109,26 +115,29 @@ FocusScope {
Connections { Connections {
target: GamepadManager target: GamepadManager
function onSelectPressed() { function onSelectPressed() {
if (!grid.activeFocus) return if (!grid.flickable || !grid.flickable.activeFocus) return
libraryRoot.selectFocused() libraryRoot.selectFocused()
} }
function onNavigateUp() { function onNavigateUp() {
if (!grid.activeFocus) return if (!grid.flickable || !grid.flickable.activeFocus) return
if (grid.currentIndex <= 0) return
let cols = Math.max(1, Math.floor(grid.width / grid.cellWidth)) 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) grid.currentIndex = Math.max(0, grid.currentIndex - cols)
} }
function onNavigateDown() { function onNavigateDown() {
if (!grid.activeFocus) return if (!grid.flickable || !grid.flickable.activeFocus) return
let cols = Math.max(1, Math.floor(grid.width / grid.cellWidth)) let cols = Math.max(1, Math.floor(grid.width / grid.cellWidth))
grid.currentIndex = Math.min(proxyModel.count - 1, grid.currentIndex + cols) grid.currentIndex = Math.min(proxyModel.count - 1, grid.currentIndex + cols)
} }
function onNavigateLeft() { function onNavigateLeft() {
if (!grid.activeFocus) return if (!grid.flickable || !grid.flickable.activeFocus) return
grid.currentIndex = Math.max(0, grid.currentIndex - 1) grid.currentIndex = Math.max(0, grid.currentIndex - 1)
} }
function onNavigateRight() { function onNavigateRight() {
if (!grid.activeFocus) return if (!grid.flickable || !grid.flickable.activeFocus) return
grid.currentIndex = Math.min(proxyModel.count - 1, grid.currentIndex + 1) grid.currentIndex = Math.min(proxyModel.count - 1, grid.currentIndex + 1)
} }
} }

View file

@ -6,6 +6,7 @@ import QtQuick.Controls as QQC2
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Effects import QtQuick.Effects
import org.mauikit.controls as Maui import org.mauikit.controls as Maui
import org.kde.kirigami as Kirigami
import org.kde.alakarte import org.kde.alakarte
import "components" import "components"
@ -120,16 +121,48 @@ Maui.ApplicationWindow {
onSourceSelected: function(src) { root.currentCategory = src } onSourceSelected: function(src) { root.currentCategory = src }
onSettingsRequested: root.settingsVisible = true onSettingsRequested: root.settingsVisible = true
onImportRequested: App.importAllGames() onImportRequested: App.importAllGames()
onFocusDownRequested: libraryView.restoreFocus()
} }
QQC2.ToolButton { QQC2.ToolButton {
id: searchButton
icon.name: "edit-find" icon.name: "edit-find"
flat: true flat: true
Layout.preferredWidth: 40 Layout.preferredWidth: 40
Layout.preferredHeight: 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 onClicked: root.searchVisible = !root.searchVisible
QQC2.ToolTip.text: i18n("Search") QQC2.ToolTip.text: i18n("Search")
QQC2.ToolTip.visible: hovered 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 filterSource: root.currentCategory
filterText: root.searchText filterText: root.searchText
onHeaderFocusRequested: categoryRail.focusCurrent()
onGameFocused: function(game) { root.focusedGame = game } onGameFocused: function(game) { root.focusedGame = game }
onGameSelected: function(game) { onGameSelected: function(game) {
root.selectedGame = game root.selectedGame = game