From 8f21be87fc1fc1987913a40b6cb4ec739797b17f Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Sun, 16 Jul 2023 12:14:52 -0700 Subject: [PATCH] Allow using A and B to navigate the controller binding flow --- test/gamepadutils.c | 66 +++++++++++++- test/gamepadutils.h | 10 ++- test/testcontroller.c | 197 ++++++++++++++++++++++++++++++------------ 3 files changed, 212 insertions(+), 61 deletions(-) diff --git a/test/gamepadutils.c b/test/gamepadutils.c index 0bb7b22b88..c2239d640c 100644 --- a/test/gamepadutils.c +++ b/test/gamepadutils.c @@ -2248,6 +2248,22 @@ static char *RecreateMapping(MappingParts *parts, char *mapping) return mapping; } +static SDL_bool MappingHasKey(const char *mapping, const char *key) +{ + int i; + MappingParts parts; + SDL_bool result = SDL_FALSE; + + SplitMapping(mapping, &parts); + i = FindMappingKey(&parts, key); + if (i >= 0) { + result = SDL_TRUE; + } + FreeMappingParts(&parts); + + return result; +} + static char *GetMappingValue(const char *mapping, const char *key) { int i; @@ -2484,6 +2500,17 @@ static const char *GetElementKey(int element) } } +SDL_bool MappingHasElement(const char *mapping, int element) +{ + const char *key; + + key = GetElementKey(element); + if (!key) { + return SDL_FALSE; + } + return MappingHasKey(mapping, key); +} + char *GetElementBinding(const char *mapping, int element) { const char *key; @@ -2504,6 +2531,34 @@ char *SetElementBinding(char *mapping, int element, const char *binding) } } +int GetElementForBinding(char *mapping, const char *binding) +{ + MappingParts parts; + int i, element; + int result = SDL_GAMEPAD_ELEMENT_INVALID; + + if (!binding) { + return SDL_GAMEPAD_ELEMENT_INVALID; + } + + SplitMapping(mapping, &parts); + for (i = 0; i < parts.num_elements; ++i) { + if (SDL_strcmp(binding, parts.values[i]) == 0) { + for (element = 0; element < SDL_GAMEPAD_ELEMENT_MAX; ++element) { + const char *key = GetElementKey(element); + if (key && SDL_strcmp(key, parts.keys[i]) == 0) { + result = element; + break; + } + } + break; + } + } + FreeMappingParts(&parts); + + return result; +} + SDL_bool MappingHasBinding(const char *mapping, const char *binding) { MappingParts parts; @@ -2515,7 +2570,7 @@ SDL_bool MappingHasBinding(const char *mapping, const char *binding) } SplitMapping(mapping, &parts); - for (i = parts.num_elements - 1; i >= 0; --i) { + for (i = 0; i < parts.num_elements; ++i) { if (SDL_strcmp(binding, parts.values[i]) == 0) { result = SDL_TRUE; break; @@ -2530,6 +2585,7 @@ char *ClearMappingBinding(char *mapping, const char *binding) { MappingParts parts; int i; + SDL_bool modified = SDL_FALSE; if (!binding) { return mapping; @@ -2539,7 +2595,13 @@ char *ClearMappingBinding(char *mapping, const char *binding) for (i = parts.num_elements - 1; i >= 0; --i) { if (SDL_strcmp(binding, parts.values[i]) == 0) { RemoveMappingValueAt(&parts, i); + modified = SDL_TRUE; } } - return RecreateMapping(&parts, mapping); + if (modified) { + return RecreateMapping(&parts, mapping); + } else { + FreeMappingParts(&parts); + return mapping; + } } diff --git a/test/gamepadutils.h b/test/gamepadutils.h index 7dbe3f0d79..738b1a820a 100644 --- a/test/gamepadutils.h +++ b/test/gamepadutils.h @@ -134,15 +134,21 @@ extern char *SetMappingName(char *mapping, const char *name); /* Return the type from a mapping, which should be freed using SDL_free(), or NULL if there is no type specified */ extern char *GetMappingType(const char *mapping); -/* Set the name in a mapping, freeing the mapping passed in and returning a new mapping */ +/* Set the type in a mapping, freeing the mapping passed in and returning a new mapping */ extern char *SetMappingType(char *mapping, const char *type); +/* Return true if a mapping has this element bound */ +extern SDL_bool MappingHasElement(const char *mapping, int element); + /* Get the binding for an element, which should be freed using SDL_free(), or NULL if the element isn't bound */ extern char *GetElementBinding(const char *mapping, int element); -/* Set the binding for an element, or NULL to clear it */ +/* Set the binding for an element, or NULL to clear it, freeing the mapping passed in and returning a new mapping */ extern char *SetElementBinding(char *mapping, int element, const char *binding); +/* Get the element for a binding, or SDL_GAMEPAD_ELEMENT_INVALID if that binding isn't used */ +extern int GetElementForBinding(char *mapping, const char *binding); + /* Return true if a mapping contains this binding */ extern SDL_bool MappingHasBinding(const char *mapping, const char *binding); diff --git a/test/testcontroller.c b/test/testcontroller.c index 1c1a519823..1b32166f6d 100644 --- a/test/testcontroller.c +++ b/test/testcontroller.c @@ -83,6 +83,8 @@ static Controller *controllers; static Controller *controller; static SDL_JoystickID mapping_controller = 0; static int binding_element = SDL_GAMEPAD_ELEMENT_INVALID; +static int last_binding_element = SDL_GAMEPAD_ELEMENT_INVALID; +static SDL_bool binding_flow = SDL_FALSE; static Uint64 binding_advance_time = 0; static SDL_Joystick *virtual_joystick = NULL; static SDL_GamepadAxis virtual_axis_active = SDL_GAMEPAD_AXIS_INVALID; @@ -90,6 +92,44 @@ static float virtual_axis_start_x; static float virtual_axis_start_y; static SDL_GamepadButton virtual_button_active = SDL_GAMEPAD_BUTTON_INVALID; +static int s_arrBindingOrder[] = { + /* Standard sequence */ + SDL_GAMEPAD_BUTTON_A, + SDL_GAMEPAD_BUTTON_B, + SDL_GAMEPAD_BUTTON_Y, + SDL_GAMEPAD_BUTTON_X, + SDL_GAMEPAD_ELEMENT_AXIS_LEFTX_NEGATIVE, + SDL_GAMEPAD_ELEMENT_AXIS_LEFTX_POSITIVE, + SDL_GAMEPAD_ELEMENT_AXIS_LEFTY_NEGATIVE, + SDL_GAMEPAD_ELEMENT_AXIS_LEFTY_POSITIVE, + SDL_GAMEPAD_BUTTON_LEFT_STICK, + SDL_GAMEPAD_ELEMENT_AXIS_RIGHTX_NEGATIVE, + SDL_GAMEPAD_ELEMENT_AXIS_RIGHTX_POSITIVE, + SDL_GAMEPAD_ELEMENT_AXIS_RIGHTY_NEGATIVE, + SDL_GAMEPAD_ELEMENT_AXIS_RIGHTY_POSITIVE, + SDL_GAMEPAD_BUTTON_RIGHT_STICK, + SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, + SDL_GAMEPAD_ELEMENT_AXIS_LEFT_TRIGGER, + SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, + SDL_GAMEPAD_ELEMENT_AXIS_RIGHT_TRIGGER, + SDL_GAMEPAD_BUTTON_DPAD_UP, + SDL_GAMEPAD_BUTTON_DPAD_RIGHT, + SDL_GAMEPAD_BUTTON_DPAD_DOWN, + SDL_GAMEPAD_BUTTON_DPAD_LEFT, + SDL_GAMEPAD_BUTTON_BACK, + SDL_GAMEPAD_BUTTON_START, + SDL_GAMEPAD_BUTTON_GUIDE, + SDL_GAMEPAD_BUTTON_MISC1, + SDL_GAMEPAD_ELEMENT_INVALID, + + /* Paddle sequence */ + SDL_GAMEPAD_BUTTON_PADDLE1, + SDL_GAMEPAD_BUTTON_PADDLE2, + SDL_GAMEPAD_BUTTON_PADDLE3, + SDL_GAMEPAD_BUTTON_PADDLE4, + SDL_GAMEPAD_ELEMENT_INVALID, +}; + static const char *GetSensorName(SDL_SensorType sensor) { @@ -161,7 +201,7 @@ static void CyclePS5TriggerEffect(Controller *device) SDL_SendGamepadEffect(device->gamepad, &state, sizeof(state)); } -static void ClearButtonHighlights() +static void ClearButtonHighlights(void) { ClearGamepadImage(image); SetGamepadDisplayHighlight(gamepad_elements, SDL_GAMEPAD_ELEMENT_INVALID, SDL_FALSE); @@ -215,11 +255,17 @@ static void SetAndFreeGamepadMapping(char *mapping) SDL_free(mapping); } -static void SetCurrentBindingElement(int element) +static void SetCurrentBindingElement(int element, SDL_bool flow) { int i; + if (element == SDL_GAMEPAD_ELEMENT_INVALID) { + last_binding_element = SDL_GAMEPAD_ELEMENT_INVALID; + } else { + last_binding_element = binding_element; + } binding_element = element; + binding_flow = flow || (element == SDL_GAMEPAD_BUTTON_A); binding_advance_time = 0; for (i = 0; i < controller->num_axes; ++i) { @@ -229,45 +275,8 @@ static void SetCurrentBindingElement(int element) SetGamepadDisplaySelected(gamepad_elements, element); } -static void SetNextBindingElement() +static void SetNextBindingElement(void) { - static int s_arrBindingOrder[] = { - /* Standard sequence */ - SDL_GAMEPAD_BUTTON_A, - SDL_GAMEPAD_BUTTON_B, - SDL_GAMEPAD_BUTTON_Y, - SDL_GAMEPAD_BUTTON_X, - SDL_GAMEPAD_ELEMENT_AXIS_LEFTX_NEGATIVE, - SDL_GAMEPAD_ELEMENT_AXIS_LEFTX_POSITIVE, - SDL_GAMEPAD_ELEMENT_AXIS_LEFTY_NEGATIVE, - SDL_GAMEPAD_ELEMENT_AXIS_LEFTY_POSITIVE, - SDL_GAMEPAD_BUTTON_LEFT_STICK, - SDL_GAMEPAD_ELEMENT_AXIS_RIGHTX_NEGATIVE, - SDL_GAMEPAD_ELEMENT_AXIS_RIGHTX_POSITIVE, - SDL_GAMEPAD_ELEMENT_AXIS_RIGHTY_NEGATIVE, - SDL_GAMEPAD_ELEMENT_AXIS_RIGHTY_POSITIVE, - SDL_GAMEPAD_BUTTON_RIGHT_STICK, - SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, - SDL_GAMEPAD_ELEMENT_AXIS_LEFT_TRIGGER, - SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, - SDL_GAMEPAD_ELEMENT_AXIS_RIGHT_TRIGGER, - SDL_GAMEPAD_BUTTON_DPAD_UP, - SDL_GAMEPAD_BUTTON_DPAD_RIGHT, - SDL_GAMEPAD_BUTTON_DPAD_DOWN, - SDL_GAMEPAD_BUTTON_DPAD_LEFT, - SDL_GAMEPAD_BUTTON_BACK, - SDL_GAMEPAD_BUTTON_START, - SDL_GAMEPAD_BUTTON_GUIDE, - SDL_GAMEPAD_BUTTON_MISC1, - SDL_GAMEPAD_ELEMENT_INVALID, - - /* Paddle sequence */ - SDL_GAMEPAD_BUTTON_PADDLE1, - SDL_GAMEPAD_BUTTON_PADDLE2, - SDL_GAMEPAD_BUTTON_PADDLE3, - SDL_GAMEPAD_BUTTON_PADDLE4, - SDL_GAMEPAD_ELEMENT_INVALID, - }; int i; if (binding_element == SDL_GAMEPAD_ELEMENT_INVALID) { @@ -276,21 +285,40 @@ static void SetNextBindingElement() for (i = 0; i < SDL_arraysize(s_arrBindingOrder); ++i) { if (binding_element == s_arrBindingOrder[i]) { - SetCurrentBindingElement(s_arrBindingOrder[i + 1]); + SetCurrentBindingElement(s_arrBindingOrder[i + 1], SDL_TRUE); return; } } - SetCurrentBindingElement(SDL_GAMEPAD_ELEMENT_INVALID); + SetCurrentBindingElement(SDL_GAMEPAD_ELEMENT_INVALID, SDL_FALSE); } -static void CancelBinding(void) +static void SetPrevBindingElement(void) { - SetCurrentBindingElement(SDL_GAMEPAD_ELEMENT_INVALID); + int i; + + if (binding_element == SDL_GAMEPAD_ELEMENT_INVALID) { + return; + } + + for (i = 1; i < SDL_arraysize(s_arrBindingOrder); ++i) { + if (binding_element == s_arrBindingOrder[i]) { + SetCurrentBindingElement(s_arrBindingOrder[i - 1], SDL_TRUE); + return; + } + } + SetCurrentBindingElement(SDL_GAMEPAD_ELEMENT_INVALID, SDL_FALSE); +} + +static void StopBinding(void) +{ + SetCurrentBindingElement(SDL_GAMEPAD_ELEMENT_INVALID, SDL_FALSE); } static void CommitBindingElement(const char *binding, SDL_bool force) { char *mapping; + int direction = 1; + SDL_bool ignore_binding = SDL_FALSE; if (binding_element == SDL_GAMEPAD_ELEMENT_INVALID) { return; @@ -304,7 +332,6 @@ static void CommitBindingElement(const char *binding, SDL_bool force) /* If the controller generates multiple events for a single element, pick the best one */ if (!force && binding_advance_time) { - SDL_bool ignore_binding = SDL_FALSE; char *current = GetElementBinding(mapping, binding_element); SDL_bool native_button = (binding_element < SDL_GAMEPAD_BUTTON_MAX); SDL_bool native_axis = (binding_element >= SDL_GAMEPAD_BUTTON_MAX && @@ -348,18 +375,60 @@ static void CommitBindingElement(const char *binding, SDL_bool force) } } SDL_free(current); + } - if (ignore_binding) { - return; + if (!ignore_binding && binding_flow && !force) { + int existing = GetElementForBinding(mapping, binding); + if (existing != SDL_GAMEPAD_ELEMENT_INVALID) { + if (existing == SDL_GAMEPAD_BUTTON_A) { + if (binding_element == SDL_GAMEPAD_BUTTON_A) { + /* Just move on to the next one */ + ignore_binding = SDL_TRUE; + SetNextBindingElement(); + } else { + /* Clear the current binding and move to the next one */ + binding = NULL; + direction = 1; + force = SDL_TRUE; + } + } else if (existing == SDL_GAMEPAD_BUTTON_B) { + if (binding_element != SDL_GAMEPAD_BUTTON_A && + last_binding_element != SDL_GAMEPAD_BUTTON_A) { + /* Clear the current binding and move to the previous one */ + binding = NULL; + direction = -1; + force = SDL_TRUE; + } + } else if (existing == binding_element) { + /* We're rebinding the same thing, just move to the next one */ + ignore_binding = SDL_TRUE; + SetNextBindingElement(); + } else if (binding_element != SDL_GAMEPAD_BUTTON_A && + binding_element != SDL_GAMEPAD_BUTTON_B) { + ignore_binding = SDL_TRUE; + } } } + if (ignore_binding) { + SDL_free(mapping); + return; + } + mapping = ClearMappingBinding(mapping, binding); mapping = SetElementBinding(mapping, binding_element, binding); SetAndFreeGamepadMapping(mapping); if (force) { - SetNextBindingElement(); + if (binding_flow) { + if (direction > 0) { + SetNextBindingElement(); + } else if (direction < 0) { + SetPrevBindingElement(); + } + } else { + StopBinding(); + } } else { /* Wait to see if any more bindings come in */ binding_advance_time = SDL_GetTicks() + 30; @@ -383,9 +452,9 @@ static void SetDisplayMode(ControllerDisplayMode mode) } mapping_controller = controller->id; if (MappingHasBindings(backup_mapping)) { - SetCurrentBindingElement(SDL_GAMEPAD_ELEMENT_INVALID); + SetCurrentBindingElement(SDL_GAMEPAD_ELEMENT_INVALID, SDL_FALSE); } else { - SetCurrentBindingElement(SDL_GAMEPAD_BUTTON_A); + SetCurrentBindingElement(SDL_GAMEPAD_BUTTON_A, SDL_TRUE); } } else { if (backup_mapping) { @@ -393,7 +462,7 @@ static void SetDisplayMode(ControllerDisplayMode mode) backup_mapping = NULL; } mapping_controller = 0; - CancelBinding(); + StopBinding(); } display_mode = mode; @@ -416,7 +485,7 @@ static void CancelMapping(void) static void ClearMapping(void) { SetAndFreeGamepadMapping(NULL); - SetCurrentBindingElement(SDL_GAMEPAD_ELEMENT_INVALID); + SetCurrentBindingElement(SDL_GAMEPAD_ELEMENT_INVALID, SDL_FALSE); } static void CopyMapping(void) @@ -431,7 +500,7 @@ static void PasteMapping(void) if (controller) { char *mapping = SDL_GetClipboardText(); if (MappingHasBindings(mapping)) { - CancelBinding(); + StopBinding(); SetAndFreeGamepadMapping(mapping); } else { /* Not a valid mapping, ignore it */ @@ -988,6 +1057,7 @@ static void DrawBindingTips(SDL_Renderer *renderer) } else { Uint8 r, g, b, a; SDL_FRect rect; + SDL_bool bound_A, bound_B; y -= (FONT_CHARACTER_SIZE + BUTTON_MARGIN) / 2; @@ -1003,7 +1073,14 @@ static void DrawBindingTips(SDL_Renderer *renderer) SDLTest_DrawString(renderer, (float)x - (FONT_CHARACTER_SIZE * SDL_strlen(text)) / 2, (float)y, text); y += (FONT_CHARACTER_SIZE + BUTTON_MARGIN); - text = "(press SPACE to clear binding and ESC to cancel)"; + + bound_A = MappingHasElement(controller->mapping, SDL_GAMEPAD_BUTTON_A); + bound_B = MappingHasElement(controller->mapping, SDL_GAMEPAD_BUTTON_B); + if (binding_flow && bound_A && bound_B) { + text = "(press A to skip, B to go back, and ESC to cancel)"; + } else { + text = "(press SPACE to clear binding and ESC to cancel)"; + } SDLTest_DrawString(renderer, (float)x - (FONT_CHARACTER_SIZE * SDL_strlen(text)) / 2, (float)y, text); } } @@ -1281,7 +1358,9 @@ static void loop(void *arg) gamepad_element = GetGamepadDisplayElementAt(gamepad_elements, controller->gamepad, event.button.x, event.button.y); if (gamepad_element != SDL_GAMEPAD_ELEMENT_INVALID) { - SetCurrentBindingElement(gamepad_element); + /* Set this to SDL_FALSE if you don't want to start the binding flow at this point */ + const SDL_bool should_start_flow = SDL_TRUE; + SetCurrentBindingElement(gamepad_element, should_start_flow); } joystick_element = GetJoystickDisplayElementAt(joystick_elements, controller->joystick, event.button.x, event.button.y); @@ -1329,7 +1408,7 @@ static void loop(void *arg) ClearBinding(); } else if (event.key.keysym.sym == SDLK_ESCAPE) { if (binding_element != SDL_GAMEPAD_ELEMENT_INVALID) { - CancelBinding(); + StopBinding(); } else { CancelMapping(); } @@ -1348,7 +1427,11 @@ static void loop(void *arg) in case a gamepad sends multiple events for a single control (e.g. axis and button for trigger) */ if (binding_advance_time && SDL_GetTicks() > (binding_advance_time + 30)) { - SetNextBindingElement(); + if (binding_flow) { + SetNextBindingElement(); + } else { + StopBinding(); + } } /* blank screen, set up for drawing this frame. */