mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-29 07:03:08 +00:00
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.
This commit is contained in:
parent
55f778ebe8
commit
d913c2ab0a
6 changed files with 204 additions and 3 deletions
|
|
@ -6,9 +6,34 @@
|
|||
#include <SDL3/SDL_gamepad.h>
|
||||
#include <SDL3/SDL_power.h>
|
||||
#include <SDL3/SDL_properties.h>
|
||||
#include <SDL3/SDL_sensor.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
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<uint16_t>(std::clamp(leftIntensity, 0, 65535));
|
||||
auto right = static_cast<uint16_t>(std::clamp(rightIntensity, 0, 65535));
|
||||
auto dur = static_cast<uint32_t>(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<SDL_GamepadButton>(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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
#include <SDL3/SDL_hints.h>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QQmlEngine>
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue