From 7133969e3a5ce2a4457eb3243420bdccacac49d1 Mon Sep 17 00:00:00 2001
From: William Hou <[EMAIL REDACTED]>
Date: Sun, 19 Jan 2025 23:34:04 -0500
Subject: [PATCH] Feature add hint to remap option as alt key (#12021)
---
include/SDL3/SDL_hints.h | 25 ++++++++++
src/video/cocoa/SDL_cocoakeyboard.m | 71 +++++++++++++++++++++++++++++
src/video/cocoa/SDL_cocoavideo.h | 9 ++++
3 files changed, 105 insertions(+)
diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h
index 994143cc1c496..e5af037469c7f 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 f73572da59c4a..550f533f18d4a 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 75c1ec79f0490..353fb43509d2c 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