diff --git a/src/gfx_func.h b/src/gfx_func.h index ed9d2c2cec..1de95284b7 100644 --- a/src/gfx_func.h +++ b/src/gfx_func.h @@ -75,6 +75,7 @@ void HandleTextInput(std::string_view str, bool marked = false, std::optional insert_location = std::nullopt, std::optional 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); diff --git a/src/lang/english.txt b/src/lang/english.txt index 21d0e08940..d2f07f47b0 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -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 diff --git a/src/settingentry_gui.cpp b/src/settingentry_gui.cpp index fcc408adb3..28ddf09d8e 100644 --- a/src/settingentry_gui.cpp +++ b/src/settingentry_gui.cpp @@ -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")); diff --git a/src/settings_type.h b/src/settings_type.h index 255038e537..ca94092b55 100644 --- a/src/settings_type.h +++ b/src/settings_type.h @@ -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 diff --git a/src/table/settings/gui_settings.ini b/src/table/settings/gui_settings.ini index 3d9ee1b75b..0f3079b080 100644 --- a/src/table/settings/gui_settings.ini +++ b/src/table/settings/gui_settings.ini @@ -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 + diff --git a/src/video/sdl2_v.cpp b/src/video/sdl2_v.cpp index 7f4312a289..504288ce72 100644 --- a/src/video/sdl2_v.cpp +++ b/src/video/sdl2_v.cpp @@ -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 #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 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 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 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); +} diff --git a/src/video/sdl2_v.h b/src/video/sdl2_v.h index 360c783612..6098270aeb 100644 --- a/src/video/sdl2_v.h +++ b/src/video/sdl2_v.h @@ -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(); diff --git a/src/window.cpp b/src/window.cpp index 6ae18eda28..65e9dbb2bd 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -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); + } + } +}