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