diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h index 994143cc1c..e5af037469 100644 --- a/include/SDL3/SDL_hints.h +++ b/include/SDL3/SDL_hints.h @@ -2348,6 +2348,31 @@ extern "C" { */ #define SDL_HINT_MAC_OPENGL_ASYNC_DISPATCH "SDL_MAC_OPENGL_ASYNC_DISPATCH" +/** + * A variable controlling whether the Option (⌥) key on macOS should be remapped + * to act as the Alt key. + * + * The variable can be set to the following values: + * + * - "none": The Option key is not remapped to Alt. (default) + * - "only_left": Only the left Option key is remapped to Alt. + * - "only_right": Only the right Option key is remapped to Alt. + * - "both": Both Option keys are remapped to Alt. + * + * This will prevent the triggering of key compositions that rely on the Option + * key, but will still send the Alt modifier for keyboard events. In the case + * that both Alt and Option are pressed, the Option key will be ignored. This is + * particularly useful for applications like terminal emulators and graphical + * user interfaces (GUIs) that rely on Alt key functionality for shortcuts or + * navigation. This does not apply to SDL_GetKeyFromScancode and only has an + * effect if IME is enabled. + * + * This hint can be set anytime. + * + * \since This hint is available since 3.2.0 + */ +#define SDL_HINT_MAC_OPTION_AS_ALT "SDL_MAC_OPTION_AS_ALT" + /** * A variable controlling whether SDL_EVENT_MOUSE_WHEEL event values will have * momentum on macOS. diff --git a/src/video/cocoa/SDL_cocoakeyboard.m b/src/video/cocoa/SDL_cocoakeyboard.m index f73572da59..550f533f18 100644 --- a/src/video/cocoa/SDL_cocoakeyboard.m +++ b/src/video/cocoa/SDL_cocoakeyboard.m @@ -369,6 +369,26 @@ static void UpdateKeymap(SDL_CocoaVideoData *data, bool send_event) SDL_SetKeymap(keymap, send_event); } +static void SDLCALL SDL_MacOptionAsAltChanged(void *userdata, const char *name, const char *oldValue, const char *hint) +{ + SDL_VideoDevice *_this = (SDL_VideoDevice *)userdata; + SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal; + + if (hint && *hint) { + if (SDL_strcmp(hint, "none") == 0) { + data.option_as_alt = OptionAsAltNone; + } else if (SDL_strcmp(hint, "only_left") == 0) { + data.option_as_alt = OptionAsAltOnlyLeft; + } else if (SDL_strcmp(hint, "only_right") == 0) { + data.option_as_alt = OptionAsAltOnlyRight; + } else if (SDL_strcmp(hint, "both") == 0) { + data.option_as_alt = OptionAsAltBoth; + } + } else { + data.option_as_alt = OptionAsAltNone; + } +} + void Cocoa_InitKeyboard(SDL_VideoDevice *_this) { SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal; @@ -385,6 +405,8 @@ void Cocoa_InitKeyboard(SDL_VideoDevice *_this) data.modifierFlags = (unsigned int)[NSEvent modifierFlags]; SDL_ToggleModState(SDL_KMOD_CAPS, (data.modifierFlags & NSEventModifierFlagCapsLock) ? true : false); + + SDL_AddHintCallback(SDL_HINT_MAC_OPTION_AS_ALT, SDL_MacOptionAsAltChanged, _this); } bool Cocoa_StartTextInput(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props) @@ -437,6 +459,51 @@ bool Cocoa_UpdateTextInputArea(SDL_VideoDevice *_this, SDL_Window *window) return true; } +static NSEvent *ReplaceEvent(NSEvent *event, OptionAsAlt option_as_alt) +{ + if (option_as_alt == OptionAsAltNone) { + return event; + } + + const unsigned int modflags = (unsigned int)[event modifierFlags]; + + bool ignore_alt_characters = false; + + bool lalt_pressed = IsModifierKeyPressed(modflags, NX_DEVICELALTKEYMASK, + NX_DEVICERALTKEYMASK, NX_ALTERNATEMASK); + bool ralt_pressed = IsModifierKeyPressed(modflags, NX_DEVICERALTKEYMASK, + NX_DEVICELALTKEYMASK, NX_ALTERNATEMASK); + + if (option_as_alt == OptionAsAltOnlyLeft && lalt_pressed) { + ignore_alt_characters = true; + } else if (option_as_alt == OptionAsAltOnlyRight && ralt_pressed) { + ignore_alt_characters = true; + } else if (option_as_alt == OptionAsAltBoth && (lalt_pressed || ralt_pressed)) { + ignore_alt_characters = true; + } + + bool cmd_pressed = modflags & NX_COMMANDMASK; + bool ctrl_pressed = modflags & NX_CONTROLMASK; + + ignore_alt_characters = ignore_alt_characters && !cmd_pressed && !ctrl_pressed; + + if (ignore_alt_characters) { + NSString *charactersIgnoringModifiers = [event charactersIgnoringModifiers]; + return [NSEvent keyEventWithType:[event type] + location:[event locationInWindow] + modifierFlags:modflags + timestamp:[event timestamp] + windowNumber:[event windowNumber] + context:nil + characters:charactersIgnoringModifiers + charactersIgnoringModifiers:charactersIgnoringModifiers + isARepeat:[event isARepeat] + keyCode:[event keyCode]]; + } + + return event; +} + void Cocoa_HandleKeyEvent(SDL_VideoDevice *_this, NSEvent *event) { unsigned short scancode; @@ -446,6 +513,10 @@ void Cocoa_HandleKeyEvent(SDL_VideoDevice *_this, NSEvent *event) return; // can happen when returning from fullscreen Space on shutdown } + if ([event type] == NSEventTypeKeyDown || [event type] == NSEventTypeKeyUp) { + event = ReplaceEvent(event, data.option_as_alt); + } + scancode = [event keyCode]; if ((scancode == 10 || scancode == 50) && KBGetLayoutType(LMGetKbdType()) == kKeyboardISO) { diff --git a/src/video/cocoa/SDL_cocoavideo.h b/src/video/cocoa/SDL_cocoavideo.h index 75c1ec79f0..353fb43509 100644 --- a/src/video/cocoa/SDL_cocoavideo.h +++ b/src/video/cocoa/SDL_cocoavideo.h @@ -44,6 +44,14 @@ @class SDL3TranslatorResponder; +typedef enum +{ + OptionAsAltNone, + OptionAsAltOnlyLeft, + OptionAsAltOnlyRight, + OptionAsAltBoth, +} OptionAsAlt; + @interface SDL_CocoaVideoData : NSObject @property(nonatomic) int allow_spaces; @property(nonatomic) int trackpad_is_touch_only; @@ -53,6 +61,7 @@ @property(nonatomic) NSInteger clipboard_count; @property(nonatomic) IOPMAssertionID screensaver_assertion; @property(nonatomic) SDL_Mutex *swaplock; +@property(nonatomic) OptionAsAlt option_as_alt; @end // Utility functions