From d913c2ab0ab24f902b1e7cf9e35181d88c3a84be Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Tue, 21 Apr 2026 09:57:51 +0200 Subject: [PATCH] Improve SDL gamepad prompts and feedback Use SDL button labels so shell prompts match the connected controller instead of always using Xbox-style wording. Expose trigger rumble, touchpad count, gyro, and accelerometer support through the gamepad wrapper and surface the relevant capability labels in the overlay. Enable background gamepad events and add short rumble feedback for major shell actions such as opening quick settings, opening the exit prompt, and launching a game. --- .../gamingshellplugin/gamepaddevice.cpp | 96 +++++++++++++++++++ components/gamingshellplugin/gamepaddevice.h | 10 ++ .../gamingshellplugin/gamepadmanager.cpp | 41 ++++++++ components/gamingshellplugin/gamepadmanager.h | 1 + .../folio/qml/gaming/GameCenterOverlay.qml | 42 +++++++- .../folio/qml/gaming/GamingQuickSettings.qml | 17 +++- 6 files changed, 204 insertions(+), 3 deletions(-) diff --git a/components/gamingshellplugin/gamepaddevice.cpp b/components/gamingshellplugin/gamepaddevice.cpp index d558f29e..4403554a 100644 --- a/components/gamingshellplugin/gamepaddevice.cpp +++ b/components/gamingshellplugin/gamepaddevice.cpp @@ -6,9 +6,34 @@ #include #include #include +#include #include +static QString gamepadButtonLabelToString(SDL_GamepadButtonLabel label) +{ + switch (label) { + case SDL_GAMEPAD_BUTTON_LABEL_A: + return QStringLiteral("A"); + case SDL_GAMEPAD_BUTTON_LABEL_B: + return QStringLiteral("B"); + case SDL_GAMEPAD_BUTTON_LABEL_X: + return QStringLiteral("X"); + case SDL_GAMEPAD_BUTTON_LABEL_Y: + return QStringLiteral("Y"); + case SDL_GAMEPAD_BUTTON_LABEL_CROSS: + return QStringLiteral("Cross"); + case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE: + return QStringLiteral("Circle"); + case SDL_GAMEPAD_BUTTON_LABEL_SQUARE: + return QStringLiteral("Square"); + case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE: + return QStringLiteral("Triangle"); + default: + return QStringLiteral("?"); + } +} + GamepadDevice::GamepadDevice(SDL_Gamepad *pad, int id, QObject *parent) : QObject(parent) , m_pad(pad) @@ -76,6 +101,15 @@ bool GamepadDevice::hasRumble() const return SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN, false); } +bool GamepadDevice::hasTriggerRumble() const +{ + if (!m_pad) { + return false; + } + SDL_PropertiesID props = SDL_GetGamepadProperties(m_pad); + return SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_TRIGGER_RUMBLE_BOOLEAN, false); +} + bool GamepadDevice::hasLED() const { if (!m_pad) { @@ -85,6 +119,24 @@ bool GamepadDevice::hasLED() const return SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false); } +int GamepadDevice::touchpadCount() const +{ + if (!m_pad) { + return 0; + } + return SDL_GetNumGamepadTouchpads(m_pad); +} + +bool GamepadDevice::hasGyro() const +{ + return m_pad && SDL_GamepadHasSensor(m_pad, SDL_SENSOR_GYRO); +} + +bool GamepadDevice::hasAccelerometer() const +{ + return m_pad && SDL_GamepadHasSensor(m_pad, SDL_SENSOR_ACCEL); +} + int GamepadDevice::playerIndex() const { if (!m_pad) { @@ -114,6 +166,17 @@ bool GamepadDevice::rumble(int lowIntensity, int highIntensity, int durationMs) return SDL_RumbleGamepad(m_pad, lo, hi, dur); } +bool GamepadDevice::rumbleTriggers(int leftIntensity, int rightIntensity, int durationMs) +{ + if (!m_pad) { + return false; + } + auto left = static_cast(std::clamp(leftIntensity, 0, 65535)); + auto right = static_cast(std::clamp(rightIntensity, 0, 65535)); + auto dur = static_cast(std::clamp(durationMs, 0, durationMs)); + return SDL_RumbleGamepadTriggers(m_pad, left, right, dur); +} + bool GamepadDevice::setLED(int r, int g, int b) { if (!m_pad) { @@ -125,6 +188,39 @@ bool GamepadDevice::setLED(int r, int g, int b) return SDL_SetGamepadLED(m_pad, cr, cg, cb); } +QString GamepadDevice::buttonLabel(int button) const +{ + if (!m_pad) { + return QStringLiteral("?"); + } + + switch (button) { + case SDL_GAMEPAD_BUTTON_SOUTH: + case SDL_GAMEPAD_BUTTON_EAST: + case SDL_GAMEPAD_BUTTON_WEST: + case SDL_GAMEPAD_BUTTON_NORTH: + return gamepadButtonLabelToString(SDL_GetGamepadButtonLabel(m_pad, static_cast(button))); + case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER: + return type() == QLatin1String("playstation") ? QStringLiteral("L1") : type() == QLatin1String("nintendo") ? QStringLiteral("L") : QStringLiteral("LB"); + case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: + return type() == QLatin1String("playstation") ? QStringLiteral("R1") : type() == QLatin1String("nintendo") ? QStringLiteral("R") : QStringLiteral("RB"); + case SDL_GAMEPAD_BUTTON_BACK: + return type() == QLatin1String("playstation") ? QStringLiteral("Create") + : type() == QLatin1String("nintendo") ? QStringLiteral("-") + : QStringLiteral("View"); + case SDL_GAMEPAD_BUTTON_START: + return type() == QLatin1String("playstation") ? QStringLiteral("Options") + : type() == QLatin1String("nintendo") ? QStringLiteral("+") + : QStringLiteral("Menu"); + case SDL_GAMEPAD_BUTTON_GUIDE: + return type() == QLatin1String("playstation") ? QStringLiteral("PS") + : type() == QLatin1String("nintendo") ? QStringLiteral("Home") + : QStringLiteral("Guide"); + default: + return QStringLiteral("?"); + } +} + SDL_Gamepad *GamepadDevice::sdlGamepad() const { return m_pad; diff --git a/components/gamingshellplugin/gamepaddevice.h b/components/gamingshellplugin/gamepaddevice.h index c4534f2c..b4e9acfc 100644 --- a/components/gamingshellplugin/gamepaddevice.h +++ b/components/gamingshellplugin/gamepaddevice.h @@ -20,7 +20,11 @@ class GamepadDevice : public QObject Q_PROPERTY(QString type READ type CONSTANT) Q_PROPERTY(int batteryPercent READ batteryPercent NOTIFY batteryPercentChanged) Q_PROPERTY(bool hasRumble READ hasRumble CONSTANT) + Q_PROPERTY(bool hasTriggerRumble READ hasTriggerRumble CONSTANT) Q_PROPERTY(bool hasLED READ hasLED CONSTANT) + Q_PROPERTY(int touchpadCount READ touchpadCount CONSTANT) + Q_PROPERTY(bool hasGyro READ hasGyro CONSTANT) + Q_PROPERTY(bool hasAccelerometer READ hasAccelerometer CONSTANT) Q_PROPERTY(int playerIndex READ playerIndex WRITE setPlayerIndex NOTIFY playerIndexChanged) public: @@ -32,12 +36,18 @@ public: QString type() const; int batteryPercent() const; bool hasRumble() const; + bool hasTriggerRumble() const; bool hasLED() const; + int touchpadCount() const; + bool hasGyro() const; + bool hasAccelerometer() const; int playerIndex() const; void setPlayerIndex(int index); Q_INVOKABLE bool rumble(int lowIntensity, int highIntensity, int durationMs); + Q_INVOKABLE bool rumbleTriggers(int leftIntensity, int rightIntensity, int durationMs); Q_INVOKABLE bool setLED(int r, int g, int b); + Q_INVOKABLE QString buttonLabel(int button) const; SDL_Gamepad *sdlGamepad() const; void refreshBattery(); diff --git a/components/gamingshellplugin/gamepadmanager.cpp b/components/gamingshellplugin/gamepadmanager.cpp index aefe0d31..023d8e49 100644 --- a/components/gamingshellplugin/gamepadmanager.cpp +++ b/components/gamingshellplugin/gamepadmanager.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -114,6 +115,43 @@ GamepadDevice *GamepadManager::gamepadAt(int index) const return m_gamepads.at(index); } +QString GamepadManager::buttonLabel(int button, int gamepadIndex) const +{ + GamepadDevice *device = nullptr; + if (gamepadIndex >= 0 && gamepadIndex < m_gamepads.size()) { + device = m_gamepads.at(gamepadIndex); + } else { + device = primaryGamepad(); + } + + if (!device) { + switch (button) { + case SDL_GAMEPAD_BUTTON_SOUTH: + return QStringLiteral("A"); + case SDL_GAMEPAD_BUTTON_EAST: + return QStringLiteral("B"); + case SDL_GAMEPAD_BUTTON_WEST: + return QStringLiteral("X"); + case SDL_GAMEPAD_BUTTON_NORTH: + return QStringLiteral("Y"); + case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER: + return QStringLiteral("LB"); + case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: + return QStringLiteral("RB"); + case SDL_GAMEPAD_BUTTON_BACK: + return QStringLiteral("Back"); + case SDL_GAMEPAD_BUTTON_START: + return QStringLiteral("Start"); + case SDL_GAMEPAD_BUTTON_GUIDE: + return QStringLiteral("Guide"); + default: + return QStringLiteral("?"); + } + } + + return device->buttonLabel(button); +} + // --- Lifecycle --- void GamepadManager::start() @@ -121,11 +159,14 @@ void GamepadManager::start() if (m_sdlInitialized) { return; } + + SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); if (!SDL_Init(SDL_INIT_GAMEPAD)) { qWarning() << "GamepadManager: SDL_Init failed:" << SDL_GetError(); return; } m_sdlInitialized = true; + SDL_SetGamepadEventsEnabled(true); // Enumerate already-connected gamepads int count = 0; diff --git a/components/gamingshellplugin/gamepadmanager.h b/components/gamingshellplugin/gamepadmanager.h index 76b2d7ba..620fc64a 100644 --- a/components/gamingshellplugin/gamepadmanager.h +++ b/components/gamingshellplugin/gamepadmanager.h @@ -79,6 +79,7 @@ public: GamepadDevice *primaryGamepad() const; Q_INVOKABLE GamepadDevice *gamepadAt(int index) const; + Q_INVOKABLE QString buttonLabel(int button, int gamepadIndex = -1) const; Q_SIGNALS: void activeChanged(); diff --git a/containments/homescreens/folio/qml/gaming/GameCenterOverlay.qml b/containments/homescreens/folio/qml/gaming/GameCenterOverlay.qml index 25e52c27..2aa43185 100644 --- a/containments/homescreens/folio/qml/gaming/GameCenterOverlay.qml +++ b/containments/homescreens/folio/qml/gaming/GameCenterOverlay.qml @@ -26,17 +26,37 @@ Window { signal gameStarted() signal dismissRequested() + readonly property string actionButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonA) + readonly property string backButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonB) + readonly property string closeButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonX) + readonly property string exitButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonY) + readonly property string leftShoulderLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonLeftShoulder) + readonly property string rightShoulderLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonRightShoulder) + readonly property string quickSettingsButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonBack) + readonly property string searchButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonStart) + + function pulsePrimaryGamepad(lowIntensity, highIntensity, durationMs) { + var pad = GamingShell.GamepadManager.primaryGamepad + if (!pad || !pad.hasRumble) { + return + } + pad.rumble(lowIntensity, highIntensity, durationMs) + } + function requestExitGamingMode() { + pulsePrimaryGamepad(9000, 15000, 60) exitGamingDialog.active = true exitGamingDialog.item.open() } function launchGame(index) { + pulsePrimaryGamepad(14000, 22000, 80) GamingShell.GameLauncherProvider.launch(index) launchFade.restart() } function launchGameByStorageId(storageId) { + pulsePrimaryGamepad(14000, 22000, 80) GamingShell.GameLauncherProvider.launchByStorageId(storageId) launchFade.restart() } @@ -160,6 +180,7 @@ Window { break case GamingShell.GamepadManager.ButtonBack: quickSettings.toggle() + pulsePrimaryGamepad(7000, 11000, 40) break } } @@ -677,6 +698,7 @@ Window { required property string name required property int battery required property string type + required property var device PC3.Label { text: name @@ -688,6 +710,18 @@ Window { font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85 opacity: 0.7 } + PC3.Label { + text: device.touchpadCount > 0 ? i18n("Touchpad") : "" + visible: device.touchpadCount > 0 + font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.8 + opacity: 0.6 + } + PC3.Label { + text: device.hasGyro ? i18n("Gyro") : "" + visible: device.hasGyro + font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.8 + opacity: 0.6 + } } } @@ -696,8 +730,12 @@ Window { // Gamepad legend PC3.Label { text: runningGames.hasTasks - ? i18n("A: Select X: Close B: Back Y: Exit ⊞: Settings ☰: Search") - : i18n("A: Select B: Back Y: Exit LB/RB: Filter ⊞: Settings ☰: Search") + ? i18n("%1: Select %2: Close %3: Back %4: Exit %5: Settings %6: Search", + actionButtonLabel, closeButtonLabel, backButtonLabel, exitButtonLabel, + quickSettingsButtonLabel, searchButtonLabel) + : i18n("%1: Select %2: Back %3: Exit %4/%5: Filter %6: Settings %7: Search", + actionButtonLabel, backButtonLabel, exitButtonLabel, leftShoulderLabel, + rightShoulderLabel, quickSettingsButtonLabel, searchButtonLabel) font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75 opacity: 0.5 } diff --git a/containments/homescreens/folio/qml/gaming/GamingQuickSettings.qml b/containments/homescreens/folio/qml/gaming/GamingQuickSettings.qml index 5c7cebae..8bac0afd 100644 --- a/containments/homescreens/folio/qml/gaming/GamingQuickSettings.qml +++ b/containments/homescreens/folio/qml/gaming/GamingQuickSettings.qml @@ -22,6 +22,16 @@ Item { anchors.fill: parent property bool opened: false + readonly property string acceptButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonA) + readonly property string closeButtonLabel: GamingShell.GamepadManager.buttonLabel(GamingShell.GamepadManager.ButtonB) + + function pulsePrimaryGamepad(lowIntensity, highIntensity, durationMs) { + var pad = GamingShell.GamepadManager.primaryGamepad + if (!pad || !pad.hasRumble) { + return + } + pad.rumble(lowIntensity, highIntensity, durationMs) + } // Focusable controls for gamepad navigation property var _controls: [] @@ -47,9 +57,11 @@ Item { _buildControlsList() _focusIndex = Math.max(0, Math.min(_focusIndex, _controls.length - 1)) _highlightCurrent() + pulsePrimaryGamepad(7000, 11000, 40) } function close() { opened = false + pulsePrimaryGamepad(5000, 8000, 30) } function toggle() { if (opened) close(); else open() @@ -92,11 +104,13 @@ Item { var ctrl = _controls[_focusIndex] if (ctrl === profileRow) { ctrl.increase() + pulsePrimaryGamepad(6000, 9000, 35) return } if (ctrl instanceof QQC2.Switch) { ctrl.toggle() ctrl.toggled() + pulsePrimaryGamepad(6000, 9000, 35) } } @@ -626,7 +640,8 @@ Item { // ---- Gamepad legend ---- PC3.Label { Layout.fillWidth: true - text: i18n("↕: Navigate ↔: Adjust A: Toggle B: Close") + text: i18n("↕: Navigate ↔: Adjust %1: Toggle %2: Close", + acceptButtonLabel, closeButtonLabel) font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.8 opacity: 0.5 horizontalAlignment: Text.AlignHCenter