From 5a079a2d114f96d4847d1ee305d5b7c16eeec50e Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sat, 27 Dec 2025 12:03:39 -0800 Subject: Initial commit --- .../SDL-3.2.8/src/video/cocoa/SDL_cocoakeyboard.m | 604 +++++++++++++++++++++ 1 file changed, 604 insertions(+) create mode 100644 contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoakeyboard.m (limited to 'contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoakeyboard.m') diff --git a/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoakeyboard.m b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoakeyboard.m new file mode 100644 index 0000000..e458be9 --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/cocoa/SDL_cocoakeyboard.m @@ -0,0 +1,604 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_VIDEO_DRIVER_COCOA + +#include "SDL_cocoavideo.h" + +#include "../../events/SDL_events_c.h" +#include "../../events/SDL_keyboard_c.h" +#include "../../events/scancodes_darwin.h" + +#include + +#if 0 +#define DEBUG_IME NSLog +#else +#define DEBUG_IME(...) +#endif + +@interface SDL3TranslatorResponder : NSView +{ + NSString *_markedText; + NSRange _markedRange; + NSRange _selectedRange; + SDL_Rect _inputRect; + int _pendingRawCode; + SDL_Scancode _pendingScancode; + Uint64 _pendingTimestamp; +} +- (void)doCommandBySelector:(SEL)myselector; +- (void)setInputRect:(const SDL_Rect *)rect; +- (void)setPendingKey:(int)rawcode scancode:(SDL_Scancode)scancode timestamp:(Uint64)timestamp; +- (void)sendPendingKey; +- (void)clearPendingKey; +@end + +@implementation SDL3TranslatorResponder + +- (void)setInputRect:(const SDL_Rect *)rect +{ + SDL_copyp(&_inputRect, rect); +} + +- (void)insertText:(id)aString replacementRange:(NSRange)replacementRange +{ + const char *str; + + DEBUG_IME(@"insertText: %@ replacementRange: (%d, %d)", aString, + (int)replacementRange.location, (int)replacementRange.length); + + /* Could be NSString or NSAttributedString, so we have + * to test and convert it before return as SDL event */ + if ([aString isKindOfClass:[NSAttributedString class]]) { + str = [[aString string] UTF8String]; + } else { + str = [aString UTF8String]; + } + + // We're likely sending the composed text, so we reset the IME status. + if ([self hasMarkedText]) { + [self unmarkText]; + } + + // Deliver the raw key event that generated this text + [self sendPendingKey]; + + if ((int)replacementRange.location != -1) { + // We're replacing the last character + SDL_SendKeyboardKey(0, SDL_GLOBAL_KEYBOARD_ID, 0, SDL_SCANCODE_BACKSPACE, true); + SDL_SendKeyboardKey(0, SDL_GLOBAL_KEYBOARD_ID, 0, SDL_SCANCODE_BACKSPACE, false); + } + + SDL_SendKeyboardText(str); +} + +- (void)doCommandBySelector:(SEL)myselector +{ + /* No need to do anything since we are not using Cocoa + selectors to handle special keys, instead we use SDL + key events to do the same job. + */ +} + +- (BOOL)hasMarkedText +{ + return _markedText != nil; +} + +- (NSRange)markedRange +{ + return _markedRange; +} + +- (NSRange)selectedRange +{ + return _selectedRange; +} + +- (void)setMarkedText:(id)aString selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange +{ + if ([aString isKindOfClass:[NSAttributedString class]]) { + aString = [aString string]; + } + + if ([aString length] == 0) { + [self unmarkText]; + return; + } + + if (_markedText != aString) { + _markedText = aString; + } + + _selectedRange = selectedRange; + _markedRange = NSMakeRange(0, [aString length]); + + // This key event was consumed by the IME + [self clearPendingKey]; + + NSUInteger utf32SelectedRangeLocation = [[aString substringToIndex:selectedRange.location] lengthOfBytesUsingEncoding:NSUTF32StringEncoding] / 4; + NSUInteger utf32SelectionRangeEnd = [[aString substringToIndex:(selectedRange.location + selectedRange.length)] lengthOfBytesUsingEncoding:NSUTF32StringEncoding] / 4; + NSUInteger utf32SelectionRangeLength = utf32SelectionRangeEnd - utf32SelectedRangeLocation; + + SDL_SendEditingText([aString UTF8String], + (int)utf32SelectedRangeLocation, (int)utf32SelectionRangeLength); + + DEBUG_IME(@"setMarkedText: %@, (%d, %d) replacement range (%d, %d)", _markedText, + (int)selectedRange.location, (int)selectedRange.length, + (int)replacementRange.location, (int)replacementRange.length); +} + +- (void)unmarkText +{ + _markedText = nil; + + // This key event was consumed by the IME + [self clearPendingKey]; + + SDL_SendEditingText("", 0, 0); +} + +- (NSRect)firstRectForCharacterRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange +{ + NSWindow *window = [self window]; + NSRect contentRect = [window contentRectForFrameRect:[window frame]]; + float windowHeight = contentRect.size.height; + NSRect rect = NSMakeRect(_inputRect.x, windowHeight - _inputRect.y - _inputRect.h, + _inputRect.w, _inputRect.h); + + if (actualRange) { + *actualRange = aRange; + } + + DEBUG_IME(@"firstRectForCharacterRange: (%d, %d): windowHeight = %g, rect = %@", + (int)aRange.location, (int)aRange.length, windowHeight, + NSStringFromRect(rect)); + + rect = [window convertRectToScreen:rect]; + + return rect; +} + +- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange +{ + DEBUG_IME(@"attributedSubstringFromRange: (%d, %d)", (int)aRange.location, (int)aRange.length); + return nil; +} + +- (NSInteger)conversationIdentifier +{ + return (NSInteger)self; +} + +/* This method returns the index for character that is + * nearest to thePoint. thPoint is in screen coordinate system. + */ +- (NSUInteger)characterIndexForPoint:(NSPoint)thePoint +{ + DEBUG_IME(@"characterIndexForPoint: (%g, %g)", thePoint.x, thePoint.y); + return 0; +} + +/* This method is the key to attribute extension. + * We could add new attributes through this method. + * NSInputServer examines the return value of this + * method & constructs appropriate attributed string. + */ +- (NSArray *)validAttributesForMarkedText +{ + return [NSArray array]; +} + +- (void)setPendingKey:(int)rawcode scancode:(SDL_Scancode)scancode timestamp:(Uint64)timestamp +{ + _pendingRawCode = rawcode; + _pendingScancode = scancode; + _pendingTimestamp = timestamp; +} + +- (void)sendPendingKey +{ + if (_pendingRawCode < 0) { + return; + } + + SDL_SendKeyboardKey(_pendingTimestamp, SDL_DEFAULT_KEYBOARD_ID, _pendingRawCode, _pendingScancode, true); + [self clearPendingKey]; +} + +- (void)clearPendingKey +{ + _pendingRawCode = -1; +} + +@end + +static bool IsModifierKeyPressed(unsigned int flags, + unsigned int target_mask, + unsigned int other_mask, + unsigned int either_mask) +{ + bool target_pressed = (flags & target_mask) != 0; + bool other_pressed = (flags & other_mask) != 0; + bool either_pressed = (flags & either_mask) != 0; + + if (either_pressed != (target_pressed || other_pressed)) + return either_pressed; + + return target_pressed; +} + +static void HandleModifiers(SDL_VideoDevice *_this, SDL_Scancode code, unsigned int modifierFlags) +{ + bool pressed = false; + + if (code == SDL_SCANCODE_LSHIFT) { + pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICELSHIFTKEYMASK, + NX_DEVICERSHIFTKEYMASK, NX_SHIFTMASK); + } else if (code == SDL_SCANCODE_LCTRL) { + pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICELCTLKEYMASK, + NX_DEVICERCTLKEYMASK, NX_CONTROLMASK); + } else if (code == SDL_SCANCODE_LALT) { + pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICELALTKEYMASK, + NX_DEVICERALTKEYMASK, NX_ALTERNATEMASK); + } else if (code == SDL_SCANCODE_LGUI) { + pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICELCMDKEYMASK, + NX_DEVICERCMDKEYMASK, NX_COMMANDMASK); + } else if (code == SDL_SCANCODE_RSHIFT) { + pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICERSHIFTKEYMASK, + NX_DEVICELSHIFTKEYMASK, NX_SHIFTMASK); + } else if (code == SDL_SCANCODE_RCTRL) { + pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICERCTLKEYMASK, + NX_DEVICELCTLKEYMASK, NX_CONTROLMASK); + } else if (code == SDL_SCANCODE_RALT) { + pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICERALTKEYMASK, + NX_DEVICELALTKEYMASK, NX_ALTERNATEMASK); + } else if (code == SDL_SCANCODE_RGUI) { + pressed = IsModifierKeyPressed(modifierFlags, NX_DEVICERCMDKEYMASK, + NX_DEVICELCMDKEYMASK, NX_COMMANDMASK); + } else { + return; + } + + if (pressed) { + SDL_SendKeyboardKey(0, SDL_DEFAULT_KEYBOARD_ID, 0, code, true); + } else { + SDL_SendKeyboardKey(0, SDL_DEFAULT_KEYBOARD_ID, 0, code, false); + } +} + +static void UpdateKeymap(SDL_CocoaVideoData *data, bool send_event) +{ + TISInputSourceRef key_layout; + UCKeyboardLayout *keyLayoutPtr = NULL; + CFDataRef uchrDataRef; + + // See if the keymap needs to be updated + key_layout = TISCopyCurrentKeyboardLayoutInputSource(); + if (key_layout == data.key_layout) { + return; + } + data.key_layout = key_layout; + + // Try Unicode data first + uchrDataRef = TISGetInputSourceProperty(key_layout, kTISPropertyUnicodeKeyLayoutData); + if (uchrDataRef) { + keyLayoutPtr = (UCKeyboardLayout *)CFDataGetBytePtr(uchrDataRef); + } + + if (!keyLayoutPtr) { + CFRelease(key_layout); + return; + } + + static struct { + int flags; + SDL_Keymod modstate; + } mods[] = { + { 0, SDL_KMOD_NONE }, + { shiftKey, SDL_KMOD_SHIFT }, + { alphaLock, SDL_KMOD_CAPS }, + { (shiftKey | alphaLock), (SDL_KMOD_SHIFT | SDL_KMOD_CAPS) }, + { optionKey, SDL_KMOD_ALT }, + { (optionKey | shiftKey), (SDL_KMOD_ALT | SDL_KMOD_SHIFT) }, + { (optionKey | alphaLock), (SDL_KMOD_ALT | SDL_KMOD_CAPS) }, + { (optionKey | shiftKey | alphaLock), (SDL_KMOD_ALT | SDL_KMOD_SHIFT | SDL_KMOD_CAPS) } + }; + + UInt32 keyboard_type = LMGetKbdType(); + + SDL_Keymap *keymap = SDL_CreateKeymap(); + for (int m = 0; m < SDL_arraysize(mods); ++m) { + for (int i = 0; i < SDL_arraysize(darwin_scancode_table); i++) { + OSStatus err; + UniChar s[8]; + UniCharCount len; + UInt32 dead_key_state; + + // Make sure this scancode is a valid character scancode + SDL_Scancode scancode = darwin_scancode_table[i]; + if (scancode == SDL_SCANCODE_UNKNOWN || + scancode == SDL_SCANCODE_DELETE || + (SDL_GetKeymapKeycode(NULL, scancode, SDL_KMOD_NONE) & SDLK_SCANCODE_MASK)) { + continue; + } + + /* + * Swap the scancode for these two wrongly translated keys + * UCKeyTranslate() function does not do its job properly for ISO layout keyboards, where the key '@', + * which is located in the top left corner of the keyboard right under the Escape key, and the additional + * key '<', which is on the right of the Shift key, are inverted + */ + if ((scancode == SDL_SCANCODE_NONUSBACKSLASH || scancode == SDL_SCANCODE_GRAVE) && KBGetLayoutType(LMGetKbdType()) == kKeyboardISO) { + // see comments in scancodes_darwin.h + scancode = (SDL_Scancode)((SDL_SCANCODE_NONUSBACKSLASH + SDL_SCANCODE_GRAVE) - scancode); + } + + dead_key_state = 0; + err = UCKeyTranslate(keyLayoutPtr, i, kUCKeyActionDown, + ((mods[m].flags >> 8) & 0xFF), keyboard_type, + kUCKeyTranslateNoDeadKeysMask, + &dead_key_state, 8, &len, s); + if (err != noErr) { + continue; + } + + if (len > 0 && s[0] != 0x10) { + SDL_SetKeymapEntry(keymap, scancode, mods[m].modstate, s[0]); + } else { + // The default keymap doesn't have any SDL_KMOD_ALT entries, so we don't need to override them + if (!(mods[m].modstate & SDL_KMOD_ALT)) { + SDL_SetKeymapEntry(keymap, scancode, mods[m].modstate, SDLK_UNKNOWN); + } + } + } + } + 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; + + UpdateKeymap(data, false); + + // Set our own names for the platform-dependent but layout-independent keys + // This key is NumLock on the MacBook keyboard. :) + // SDL_SetScancodeName(SDL_SCANCODE_NUMLOCKCLEAR, "Clear"); + SDL_SetScancodeName(SDL_SCANCODE_LALT, "Left Option"); + SDL_SetScancodeName(SDL_SCANCODE_LGUI, "Left Command"); + SDL_SetScancodeName(SDL_SCANCODE_RALT, "Right Option"); + SDL_SetScancodeName(SDL_SCANCODE_RGUI, "Right Command"); + + 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) +{ + @autoreleasepool { + NSView *parentView; + SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal; + NSWindow *nswindow = ((__bridge SDL_CocoaWindowData *)window->internal).nswindow; + + parentView = [nswindow contentView]; + + /* We only keep one field editor per process, since only the front most + * window can receive text input events, so it make no sense to keep more + * than one copy. When we switched to another window and requesting for + * text input, simply remove the field editor from its superview then add + * it to the front most window's content view */ + if (!data.fieldEdit) { + data.fieldEdit = [[SDL3TranslatorResponder alloc] initWithFrame:NSMakeRect(0.0, 0.0, 0.0, 0.0)]; + } + + if (![[data.fieldEdit superview] isEqual:parentView]) { + // DEBUG_IME(@"add fieldEdit to window contentView"); + [data.fieldEdit removeFromSuperview]; + [parentView addSubview:data.fieldEdit]; + [nswindow makeFirstResponder:data.fieldEdit]; + } + } + return Cocoa_UpdateTextInputArea(_this, window); +} + +bool Cocoa_StopTextInput(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal; + + if (data && data.fieldEdit) { + [data.fieldEdit removeFromSuperview]; + data.fieldEdit = nil; + } + } + return true; +} + +bool Cocoa_UpdateTextInputArea(SDL_VideoDevice *_this, SDL_Window *window) +{ + SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal; + if (data.fieldEdit) { + [data.fieldEdit setInputRect:&window->text_input_rect]; + } + 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; + SDL_Scancode code; + SDL_CocoaVideoData *data = _this ? ((__bridge SDL_CocoaVideoData *)_this->internal) : nil; + if (!data) { + 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) { + // see comments in scancodes_darwin.h + scancode = 60 - scancode; + } + + if (scancode < SDL_arraysize(darwin_scancode_table)) { + code = darwin_scancode_table[scancode]; + } else { + // Hmm, does this ever happen? If so, need to extend the keymap... + code = SDL_SCANCODE_UNKNOWN; + } + + switch ([event type]) { + case NSEventTypeKeyDown: + if (![event isARepeat]) { + // See if we need to rebuild the keyboard layout + UpdateKeymap(data, true); + } + +#ifdef DEBUG_SCANCODES + if (code == SDL_SCANCODE_UNKNOWN) { + SDL_Log("The key you just pressed is not recognized by SDL. To help get this fixed, report this to the SDL forums/mailing list or to Christian Walther . Mac virtual key code is %d.", scancode); + } +#endif + if (SDL_TextInputActive(SDL_GetKeyboardFocus())) { + [data.fieldEdit setPendingKey:scancode scancode:code timestamp:Cocoa_GetEventTimestamp([event timestamp])]; + [data.fieldEdit interpretKeyEvents:[NSArray arrayWithObject:event]]; + [data.fieldEdit sendPendingKey]; + } else if (SDL_GetKeyboardFocus()) { + SDL_SendKeyboardKey(Cocoa_GetEventTimestamp([event timestamp]), SDL_DEFAULT_KEYBOARD_ID, scancode, code, true); + } + break; + case NSEventTypeKeyUp: + SDL_SendKeyboardKey(Cocoa_GetEventTimestamp([event timestamp]), SDL_DEFAULT_KEYBOARD_ID, scancode, code, false); + break; + case NSEventTypeFlagsChanged: { + // see if the new modifierFlags mean any existing keys should be pressed/released... + const unsigned int modflags = (unsigned int)[event modifierFlags]; + HandleModifiers(_this, SDL_SCANCODE_LSHIFT, modflags); + HandleModifiers(_this, SDL_SCANCODE_LCTRL, modflags); + HandleModifiers(_this, SDL_SCANCODE_LALT, modflags); + HandleModifiers(_this, SDL_SCANCODE_LGUI, modflags); + HandleModifiers(_this, SDL_SCANCODE_RSHIFT, modflags); + HandleModifiers(_this, SDL_SCANCODE_RCTRL, modflags); + HandleModifiers(_this, SDL_SCANCODE_RALT, modflags); + HandleModifiers(_this, SDL_SCANCODE_RGUI, modflags); + break; + } + default: // just to avoid compiler warnings + break; + } +} + +void Cocoa_QuitKeyboard(SDL_VideoDevice *_this) +{ +} + +typedef int CGSConnection; +typedef enum +{ + CGSGlobalHotKeyEnable = 0, + CGSGlobalHotKeyDisable = 1, +} CGSGlobalHotKeyOperatingMode; + +extern CGSConnection _CGSDefaultConnection(void); +extern CGError CGSSetGlobalHotKeyOperatingMode(CGSConnection connection, CGSGlobalHotKeyOperatingMode mode); + +bool Cocoa_SetWindowKeyboardGrab(SDL_VideoDevice *_this, SDL_Window *window, bool grabbed) +{ +#ifdef SDL_MAC_NO_SANDBOX + CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), grabbed ? CGSGlobalHotKeyDisable : CGSGlobalHotKeyEnable); +#endif + return true; +} + +#endif // SDL_VIDEO_DRIVER_COCOA -- cgit v1.2.3