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.
This commit is contained in:
Marco Allegretti 2026-01-24 13:27:30 +01:00
parent 24c0137fff
commit 24b9ee6491
49 changed files with 1140 additions and 74 deletions

View file

@ -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

View file

@ -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<int>(m_viewMode));
generalGroup.writeEntry(QStringLiteral("uiMode"), static_cast<int>(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<ViewMode>(generalGroup.readEntry(QStringLiteral("viewMode"), static_cast<int>(GridView)));
m_uiMode = static_cast<UiMode>(generalGroup.readEntry(QStringLiteral("uiMode"), static_cast<int>(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();

View file

@ -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;

View file

@ -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<QPersistentModelIndex> &, 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();
}

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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"
}
}
}
}
}
}
}

View file

@ -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,10 +378,12 @@ Kirigami.ApplicationWindow {
onSourceSelected: function(source) {
root.currentSource = source
if (sidebar.modal) {
if (!sidebarView.suppressAutoClose) {
sidebar.close()
Qt.callLater(function() { libraryView.restoreFocus() })
}
}
}
onSettingsRequested: {
if (sidebar.modal) {
@ -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

View file

@ -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()
}

View file

@ -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 }
}

View file

@ -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")
}
}

View file

@ -62,10 +62,14 @@ GridView {
function onNavigateRight() { if (gridView.activeFocus) gridView.navigateRight() }
function onSelectPressed() {
if (gridView.activeFocus && gridView.currentItem) {
if (gridView.currentItem.play) {
gridView.currentItem.play()
} else {
gridView.currentItem.clicked()
}
}
}
}
readonly property int columns: Math.max(1, Math.floor(width / cellWidth))

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<rect x="6" y="11" width="5" height="4" fill="currentColor" opacity="0.6"/>
<rect x="13" y="11" width="5" height="4" fill="currentColor" opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 350 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M9 2h6v7h7v6h-7v7H9v-7H2V9h7z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
<rect x="9" y="15" width="6" height="7" fill="currentColor" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 287 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M9 2h6v7h7v6h-7v7H9v-7H2V9h7z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
<rect x="2" y="9" width="7" height="6" fill="currentColor" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 286 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M9 2h6v7h7v6h-7v7H9v-7H2V9h7z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
<rect x="15" y="9" width="7" height="6" fill="currentColor" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 287 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M9 2h6v7h7v6h-7v7H9v-7H2V9h7z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
<rect x="9" y="2" width="6" height="7" fill="currentColor" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 286 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M9 2h6v7h7v6h-7v7H9v-7H2V9h7z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 209 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M7 12h7M11 9l3 3-3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 309 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<text x="12" y="16" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="8" fill="currentColor">LB</text>
</svg>

After

Width:  |  Height:  |  Size: 324 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="1.5"/>
<circle cx="12" cy="12" r="5" fill="currentColor" opacity="0.3"/>
<circle cx="12" cy="12" r="3" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 298 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M4 18V8c0-2.2 1.8-4 4-4h8c2.2 0 4 1.8 4 4v10" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<text x="12" y="16" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="8" fill="currentColor">LT</text>
</svg>

After

Width:  |  Height:  |  Size: 354 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<line x1="7" y1="11" x2="17" y2="11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="7" y1="15" x2="17" y2="15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 403 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M12 17v-7M15 13l-3-3-3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 313 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<text x="12" y="16" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="8" fill="currentColor">RB</text>
</svg>

After

Width:  |  Height:  |  Size: 324 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="1.5"/>
<circle cx="12" cy="12" r="5" fill="currentColor" opacity="0.3"/>
<circle cx="12" cy="12" r="3" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 298 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M4 18V8c0-2.2 1.8-4 4-4h8c2.2 0 4 1.8 4 4v10" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<text x="12" y="16" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="8" fill="currentColor">RT</text>
</svg>

After

Width:  |  Height:  |  Size: 354 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M12 7v7M9 11l3 3 3-3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 309 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M17 12h-7M13 15l-3-3 3-3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 313 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">A</text>
</svg>

After

Width:  |  Height:  |  Size: 309 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">B</text>
</svg>

After

Width:  |  Height:  |  Size: 309 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M12 7l5 4v6h-3v-4h-4v4H7v-6z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 296 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="1.5"/>
<line x1="7" y1="12" x2="17" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 279 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="1.5"/>
<line x1="12" y1="7" x2="12" y2="17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="7" y1="12" x2="17" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 382 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">X</text>
</svg>

After

Width:  |  Height:  |  Size: 309 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">Y</text>
</svg>

After

Width:  |  Height:  |  Size: 309 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<circle cx="12" cy="12" r="5" fill="none" stroke="currentColor" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 261 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 8l8 8M16 8l-8 8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 271 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<line x1="7" y1="11" x2="17" y2="11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="7" y1="15" x2="17" y2="15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 403 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 13l4-3 4 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<line x1="12" y1="10" x2="12" y2="16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 427 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<rect x="7" y="7" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" rx="1"/>
</svg>

After

Width:  |  Height:  |  Size: 279 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M12 6l6.5 11h-13z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 283 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">A</text>
</svg>

After

Width:  |  Height:  |  Size: 309 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">B</text>
</svg>

After

Width:  |  Height:  |  Size: 309 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<line x1="7" y1="11" x2="17" y2="11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="7" y1="15" x2="17" y2="15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 403 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<rect x="2" y="8" width="20" height="10" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<rect x="6" y="11" width="5" height="4" fill="currentColor" opacity="0.6"/>
<rect x="13" y="11" width="5" height="4" fill="currentColor" opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 350 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">X</text>
</svg>

After

Width:  |  Height:  |  Size: 309 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
<text x="12" y="16.5" text-anchor="middle" font-family="sans-serif" font-weight="bold" font-size="12" fill="currentColor">Y</text>
</svg>

After

Width:  |  Height:  |  Size: 309 B