1
0
Fork 0

Feature: Add gamepad scrolling support (SDL2)

pull/14365/head
Michał Janiszewski 2025-06-14 17:07:54 +02:00
parent a1786fa1f4
commit 55022a0fe2
8 changed files with 279 additions and 0 deletions

View File

@ -75,6 +75,7 @@ void HandleTextInput(std::string_view str, bool marked = false,
std::optional<size_t> insert_location = std::nullopt, std::optional<size_t> replacement_end = std::nullopt);
void HandleCtrlChanged();
void HandleMouseEvents();
void HandleGamepadScrolling(int stick_x, int stick_y, int max_axis_value);
void UpdateWindows();
void ChangeGameSpeed(bool enable_fast_forward);

View File

@ -1689,6 +1689,25 @@ STR_CONFIG_SETTING_SCROLLWHEEL_ZOOM :Zoom map
STR_CONFIG_SETTING_SCROLLWHEEL_SCROLL :Scroll map
STR_CONFIG_SETTING_SCROLLWHEEL_OFF :Off
STR_CONFIG_SETTING_GAMEPAD_STICK_SELECTION :Gamepad stick for scrolling: {STRING2}
STR_CONFIG_SETTING_GAMEPAD_STICK_SELECTION_HELPTEXT :Select which analog stick to use for map scrolling
###length 3
STR_CONFIG_SETTING_GAMEPAD_STICK_DISABLED :Disabled
STR_CONFIG_SETTING_GAMEPAD_STICK_LEFT :Left stick
STR_CONFIG_SETTING_GAMEPAD_STICK_RIGHT :Right stick
STR_CONFIG_SETTING_GAMEPAD_DEADZONE :Gamepad deadzone: {STRING2}%
STR_CONFIG_SETTING_GAMEPAD_DEADZONE_HELPTEXT :Minimum stick movement required before scrolling starts (0-100%)
STR_CONFIG_SETTING_GAMEPAD_SENSITIVITY :Gamepad sensitivity: {STRING2}
STR_CONFIG_SETTING_GAMEPAD_SENSITIVITY_HELPTEXT :Control the sensitivity of gamepad analog stick scrolling
STR_CONFIG_SETTING_GAMEPAD_INVERT_X :Invert gamepad X-axis: {STRING2}
STR_CONFIG_SETTING_GAMEPAD_INVERT_X_HELPTEXT :Invert the horizontal axis movement of the gamepad analog stick
STR_CONFIG_SETTING_GAMEPAD_INVERT_Y :Invert gamepad Y-axis: {STRING2}
STR_CONFIG_SETTING_GAMEPAD_INVERT_Y_HELPTEXT :Invert the vertical axis movement of the gamepad analog stick
STR_CONFIG_SETTING_OSK_ACTIVATION :On screen keyboard: {STRING2}
STR_CONFIG_SETTING_OSK_ACTIVATION_HELPTEXT :Select the method to open the on screen keyboard for entering text into editboxes only using the pointing device. This is meant for small devices without actual keyboard
###length 4

View File

@ -681,6 +681,11 @@ SettingsContainer &GetSettingsTree()
* Since it's also able to completely disable the scrollwheel will we display it on all platforms anyway */
viewports->Add(new SettingEntry("gui.scrollwheel_scrolling"));
viewports->Add(new SettingEntry("gui.scrollwheel_multiplier"));
viewports->Add(new SettingEntry("gui.gamepad_stick_selection"));
viewports->Add(new SettingEntry("gui.gamepad_deadzone"));
viewports->Add(new SettingEntry("gui.gamepad_sensitivity"));
viewports->Add(new SettingEntry("gui.gamepad_invert_x"));
viewports->Add(new SettingEntry("gui.gamepad_invert_y"));
#ifdef __APPLE__
/* We might need to emulate a right mouse button on mac */
viewports->Add(new SettingEntry("gui.right_mouse_btn_emulation"));

View File

@ -140,6 +140,13 @@ enum ScrollWheelScrollingSetting : uint8_t {
SWS_OFF = 2 ///< Scroll wheel has no effect.
};
/** Settings related to gamepad stick selection. */
enum GamepadStickSelection : uint8_t {
GSS_DISABLED = 0, ///< Gamepad scrolling disabled.
GSS_LEFT_STICK = 1, ///< Use left analog stick for scrolling.
GSS_RIGHT_STICK = 2, ///< Use right analog stick for scrolling.
};
/** Settings related to the GUI and other stuff that is not saved in the savegame. */
struct GUISettings {
bool sg_full_load_any; ///< new full load calculation, any cargo must be full read from pre v93 savegames
@ -183,6 +190,11 @@ struct GUISettings {
uint8_t right_mouse_btn_emulation; ///< should we emulate right mouse clicking?
uint8_t scrollwheel_scrolling; ///< scrolling using the scroll wheel?
uint8_t scrollwheel_multiplier; ///< how much 'wheel' per incoming event from the OS?
uint8_t gamepad_deadzone; ///< deadzone for gamepad analog sticks (0-100)
uint8_t gamepad_sensitivity; ///< sensitivity multiplier for gamepad scrolling
bool gamepad_invert_x; ///< invert X axis for gamepad scrolling?
bool gamepad_invert_y; ///< invert Y axis for gamepad scrolling?
uint8_t gamepad_stick_selection; ///< which stick to use for scrolling (left/right/disabled)
bool timetable_arrival_departure; ///< show arrivals and departures in vehicle timetables
RightClickClose right_click_wnd_close; ///< close window with right click
bool pause_on_newgame; ///< whether to start new games paused or not

View File

@ -912,3 +912,62 @@ post_cb = [](auto) { SetupWidgetDimensions(); ReInitAllWindows(true); }
cat = SC_BASIC
startup = true
[SDTC_VAR]
var = gui.gamepad_deadzone
type = SLE_UINT8
flags = SettingFlag::NotInSave, SettingFlag::NoNetworkSync
def = 10
min = 0
max = 100
interval = 5
str = STR_CONFIG_SETTING_GAMEPAD_DEADZONE
strhelp = STR_CONFIG_SETTING_GAMEPAD_DEADZONE_HELPTEXT
strval = STR_JUST_COMMA
cat = SC_BASIC
startup = true
[SDTC_VAR]
var = gui.gamepad_sensitivity
type = SLE_UINT8
flags = SettingFlag::NotInSave, SettingFlag::NoNetworkSync
def = 10
min = 1
max = 100
interval = 5
str = STR_CONFIG_SETTING_GAMEPAD_SENSITIVITY
strhelp = STR_CONFIG_SETTING_GAMEPAD_SENSITIVITY_HELPTEXT
strval = STR_JUST_COMMA
cat = SC_BASIC
startup = true
[SDTC_BOOL]
var = gui.gamepad_invert_x
flags = SettingFlag::NotInSave, SettingFlag::NoNetworkSync
def = false
str = STR_CONFIG_SETTING_GAMEPAD_INVERT_X
strhelp = STR_CONFIG_SETTING_GAMEPAD_INVERT_X_HELPTEXT
cat = SC_BASIC
startup = true
[SDTC_BOOL]
var = gui.gamepad_invert_y
flags = SettingFlag::NotInSave, SettingFlag::NoNetworkSync
def = false
str = STR_CONFIG_SETTING_GAMEPAD_INVERT_Y
strhelp = STR_CONFIG_SETTING_GAMEPAD_INVERT_Y_HELPTEXT
cat = SC_BASIC
startup = true
[SDTC_VAR]
var = gui.gamepad_stick_selection
type = SLE_UINT8
flags = SettingFlag::NotInSave, SettingFlag::NoNetworkSync, SettingFlag::GuiDropdown
def = GSS_LEFT_STICK
min = GSS_DISABLED
max = GSS_RIGHT_STICK
str = STR_CONFIG_SETTING_GAMEPAD_STICK_SELECTION
strhelp = STR_CONFIG_SETTING_GAMEPAD_STICK_SELECTION_HELPTEXT
strval = STR_CONFIG_SETTING_GAMEPAD_STICK_DISABLED
cat = SC_BASIC
startup = true

View File

@ -20,6 +20,7 @@
#include "../fileio_func.h"
#include "../framerate_type.h"
#include "../window_func.h"
#include "../zoom_func.h"
#include "sdl2_v.h"
#include <SDL.h>
#ifdef __EMSCRIPTEN__
@ -524,6 +525,27 @@ bool VideoDriver_SDL_Base::PollEvent()
}
break;
}
case SDL_CONTROLLERDEVICEADDED: {
Debug(driver, 1, "SDL2: Gamepad device added, index: {}", ev.cdevice.which);
/* Try to open the newly connected gamepad */
if (this->gamepad == nullptr) {
this->OpenGamepad();
}
break;
}
case SDL_CONTROLLERDEVICEREMOVED: {
Debug(driver, 1, "SDL2: Gamepad device removed, instance ID: {}", ev.cdevice.which);
/* Close gamepad if it was removed */
if (this->gamepad != nullptr && ev.cdevice.which == SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(this->gamepad))) {
Debug(driver, 1, "SDL2: Current gamepad was removed, closing and reopening");
this->CloseGamepad();
/* Try to open another gamepad if available */
this->OpenGamepad();
}
break;
}
}
return true;
@ -539,6 +561,12 @@ static std::optional<std::string_view> InitializeSDL()
#endif
if (SDL_InitSubSystem(SDL_INIT_VIDEO) < 0) return SDL_GetError();
/* Initialize gamepad subsystem for gamepad scrolling support */
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) < 0) {
Debug(driver, 1, "SDL2: Failed to initialize gamepad subsystem: {}", SDL_GetError());
}
return std::nullopt;
}
@ -590,6 +618,11 @@ std::optional<std::string_view> VideoDriver_SDL_Base::Start(const StringList &pa
SDL_StopTextInput();
this->edit_box_focused = false;
/* Initialize gamepad for scrolling */
Debug(driver, 1, "SDL2: Attempting to initialize gamepad support");
Debug(driver, 1, "SDL2: Gamepad stick selection setting: {}", _settings_client.gui.gamepad_stick_selection);
this->OpenGamepad();
#ifdef __EMSCRIPTEN__
this->is_game_threaded = false;
#else
@ -601,6 +634,8 @@ std::optional<std::string_view> VideoDriver_SDL_Base::Start(const StringList &pa
void VideoDriver_SDL_Base::Stop()
{
this->CloseGamepad();
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
if (SDL_WasInit(SDL_INIT_EVERYTHING) == 0) {
SDL_Quit(); // If there's nothing left, quit SDL
@ -629,6 +664,9 @@ void VideoDriver_SDL_Base::InputLoop()
(keys[SDL_SCANCODE_DOWN] ? 8 : 0);
if (old_ctrl_pressed != _ctrl_pressed) HandleCtrlChanged();
/* Process gamepad input for scrolling */
this->ProcessGamepadInput();
}
void VideoDriver_SDL_Base::LoopOnce()
@ -755,3 +793,84 @@ void VideoDriver_SDL_Base::UnlockVideoBuffer()
this->buffer_locked = false;
}
void VideoDriver_SDL_Base::OpenGamepad()
{
/* Don't open gamepad if already open or if gamepad scrolling is disabled */
if (this->gamepad != nullptr) {
Debug(driver, 1, "SDL2: Gamepad already open, skipping");
return;
}
if (_settings_client.gui.gamepad_stick_selection == GSS_DISABLED) {
Debug(driver, 1, "SDL2: Gamepad scrolling disabled, not opening gamepad");
return;
}
/* Check if any gamepads are available */
int num_gamepads = SDL_NumJoysticks();
Debug(driver, 1, "SDL2: Found {} joystick(s)", num_gamepads);
for (int i = 0; i < num_gamepads; i++) {
if (SDL_IsGameController(i)) {
Debug(driver, 1, "SDL2: Joystick {} is a gamepad, attempting to open", i);
this->gamepad = SDL_GameControllerOpen(i);
if (this->gamepad != nullptr) {
Debug(driver, 1, "SDL2: Opened gamepad: {}", SDL_GameControllerName(this->gamepad));
break;
} else {
Debug(driver, 1, "SDL2: Failed to open gamepad {}: {}", i, SDL_GetError());
}
} else {
Debug(driver, 1, "SDL2: Joystick {} is not a gamepad", i);
}
}
if (this->gamepad == nullptr) {
Debug(driver, 1, "SDL2: No gamepad opened");
}
}
void VideoDriver_SDL_Base::CloseGamepad()
{
if (this->gamepad != nullptr) {
SDL_GameControllerClose(this->gamepad);
this->gamepad = nullptr;
Debug(driver, 1, "SDL2: Closed gamepad");
}
}
void VideoDriver_SDL_Base::ProcessGamepadInput()
{
/* Skip if gamepad is not available */
if (this->gamepad == nullptr) {
static bool logged_no_gamepad = false;
if (!logged_no_gamepad) {
Debug(driver, 2, "SDL2: No gamepad available for input processing");
logged_no_gamepad = true;
}
return;
}
/* Check if gamepad is still connected */
if (!SDL_GameControllerGetAttached(this->gamepad)) {
Debug(driver, 1, "SDL2: Gamepad disconnected, closing and reopening");
this->CloseGamepad();
return;
}
/* Get analog stick values based on stick selection */
Sint16 stick_x = 0, stick_y = 0;
if (_settings_client.gui.gamepad_stick_selection == GSS_LEFT_STICK) {
stick_x = SDL_GameControllerGetAxis(this->gamepad, SDL_CONTROLLER_AXIS_LEFTX);
stick_y = SDL_GameControllerGetAxis(this->gamepad, SDL_CONTROLLER_AXIS_LEFTY);
Debug(driver, 3, "SDL2: Left stick raw values: x={}, y={}", stick_x, stick_y);
} else if (_settings_client.gui.gamepad_stick_selection == GSS_RIGHT_STICK) {
stick_x = SDL_GameControllerGetAxis(this->gamepad, SDL_CONTROLLER_AXIS_RIGHTX);
stick_y = SDL_GameControllerGetAxis(this->gamepad, SDL_CONTROLLER_AXIS_RIGHTY);
Debug(driver, 3, "SDL2: Right stick raw values: x={}, y={}", stick_x, stick_y);
}
/* Use the common gamepad handling function */
HandleGamepadScrolling(stick_x, stick_y, 32767);
}

View File

@ -14,6 +14,10 @@
#include "video_driver.hpp"
/* Forward declaration of SDL_GameController */
struct _SDL_GameController;
typedef struct _SDL_GameController SDL_GameController;
/** The SDL video driver. */
class VideoDriver_SDL_Base : public VideoDriver {
public:
@ -69,6 +73,13 @@ protected:
/** Create the main window. */
virtual bool CreateMainWindow(uint w, uint h, uint flags = 0);
protected:
/** Gamepad support for map scrolling */
SDL_GameController *gamepad = nullptr; ///< Currently opened gamepad.
void OpenGamepad();
void CloseGamepad();
void ProcessGamepadInput();
private:
void LoopOnce();
void MainLoopCleanup();

View File

@ -3572,3 +3572,56 @@ void PickerWindowBase::Close([[maybe_unused]] int data)
ResetObjectToPlace();
this->Window::Close();
}
/**
* Process gamepad analog stick input for viewport scrolling.
* This is a common function that can be used by any video driver that supports gamepads.
* @param stick_x Raw analog stick X value (typically -32768 to 32767 range)
* @param stick_y Raw analog stick Y value (typically -32768 to 32767 range)
* @param max_axis_value Maximum value for the analog stick axes (e.g., 32767 for SDL2)
*/
void HandleGamepadScrolling(int stick_x, int stick_y, int max_axis_value)
{
/* Skip if gamepad stick selection is disabled */
if (_settings_client.gui.gamepad_stick_selection == GSS_DISABLED) {
return;
}
/* Apply deadzone (convert percentage to axis range) */
const int deadzone = (_settings_client.gui.gamepad_deadzone * max_axis_value) / 100;
if (abs(stick_x) < deadzone) stick_x = 0;
if (abs(stick_y) < deadzone) stick_y = 0;
/* Skip if no movement after deadzone */
if (stick_x == 0 && stick_y == 0) {
return;
}
/* Calculate scroll delta with sensitivity */
float sensitivity = _settings_client.gui.gamepad_sensitivity / 10.0f;
int delta_x = (int)(stick_x * sensitivity / (max_axis_value / 16)); // Scale down from axis range
int delta_y = (int)(stick_y * sensitivity / (max_axis_value / 16));
/* Apply axis inversion */
if (_settings_client.gui.gamepad_invert_x) delta_x = -delta_x;
if (_settings_client.gui.gamepad_invert_y) delta_y = -delta_y;
/* Skip if deltas are too small */
if (abs(delta_x) < 1 && abs(delta_y) < 1) {
return;
}
/* Apply scrolling to the main viewport */
if (_game_mode != GM_MENU && _game_mode != GM_BOOTSTRAP) {
Window *main_window = GetMainWindow();
if (main_window != nullptr && main_window->viewport != nullptr) {
/* Cancel vehicle following when gamepad scrolling */
main_window->viewport->CancelFollow(*main_window);
/* Apply the scroll using the same method as keyboard scrolling */
main_window->viewport->dest_scrollpos_x += ScaleByZoom(delta_x, main_window->viewport->zoom);
main_window->viewport->dest_scrollpos_y += ScaleByZoom(delta_y, main_window->viewport->zoom);
}
}
}