From 6b2494ede49b6284df507b918aed7b320c38ae4e Mon Sep 17 00:00:00 2001 From: daboe01 Date: Wed, 2 Jul 2025 19:54:52 +0200 Subject: [PATCH] fixed: arrow navigation --- AppKit/CPEvent.j | 24 ++++++- AppKit/CPTextView/CPTextView.j | 23 ++++--- AppKit/Platform/DOM/CPPlatformWindow+DOM.j | 75 +++++++++++++--------- 3 files changed, 78 insertions(+), 44 deletions(-) diff --git a/AppKit/CPEvent.j b/AppKit/CPEvent.j index 7bc06d501..41d1cefef 100644 --- a/AppKit/CPEvent.j +++ b/AppKit/CPEvent.j @@ -70,6 +70,7 @@ var _CPEventPeriodicEventPeriod = 0, BOOL _isARepeat; unsigned _keyCode; DOMEvent _DOMEvent; + BOOL _isActionKey; int _data1; int _data2; short _subtype; @@ -110,17 +111,18 @@ var _CPEventPeriodicEventPeriod = 0, @param unmodCharacters the string of keys pressed without the presence of any modifiers other than Shift @param repeatKey \c YES if this is caused by the system repeat as opposed to the user pressing the key again @param code a number associated with the keyboard key of this event + @param isAnActionKey a BOOL indicating whether this key is an action key (e.g. a function key) @throws CPInternalInconsistencyException if \c anEventType is not a CPKeyDown, CPKeyUp or CPFlagsChanged @return the keyboard event */ + (CPEvent)keyEventWithType:(CPEventType)anEventType location:(CGPoint)aPoint modifierFlags:(unsigned int)modifierFlags timestamp:(CPTimeInterval)aTimestamp windowNumber:(int)aWindowNumber context:(CPGraphicsContext)aGraphicsContext - characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)repeatKey keyCode:(unsigned short)code + characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)repeatKey keyCode:(unsigned short)code isActionKey:(BOOL)isAnActionKey { return [[self alloc] _initKeyEventWithType:anEventType location:aPoint modifierFlags:modifierFlags timestamp:aTimestamp windowNumber:aWindowNumber context:aGraphicsContext - characters:characters charactersIgnoringModifiers:unmodCharacters isARepeat:repeatKey keyCode:code]; + characters:characters charactersIgnoringModifiers:unmodCharacters isARepeat:repeatKey keyCode:code isActionKey:isAnActionKey]; } /*! @@ -252,7 +254,7 @@ var _CPEventPeriodicEventPeriod = 0, /* @ignore */ - (id)_initKeyEventWithType:(CPEventType)anEventType location:(CGPoint)aPoint modifierFlags:(unsigned int)modifierFlags timestamp:(CPTimeInterval)aTimestamp windowNumber:(int)aWindowNumber context:(CPGraphicsContext)aGraphicsContext - characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)isARepeat keyCode:(unsigned short)code + characters:(CPString)characters charactersIgnoringModifiers:(CPString)unmodCharacters isARepeat:(BOOL)isARepeat keyCode:(unsigned short)code isActionKey:(BOOL)isAnActionKey { if (self = [self _initWithType:anEventType]) { @@ -264,6 +266,7 @@ var _CPEventPeriodicEventPeriod = 0, _charactersIgnoringModifiers = unmodCharacters; _isARepeat = isARepeat; _keyCode = code; + _isActionKey = isAnActionKey; _windowNumber = aWindowNumber; } @@ -571,6 +574,21 @@ var _CPEventPeriodicEventPeriod = 0, return !firstResponderIsText; } +- (BOOL)_isActionOrCommandEvent +{ + // This method is now platform-agnostic. It checks for abstract properties + // of the event, including the _isActionKey flag that was set at creation time. + return ( + // Is it a command shortcut? + (_modifierFlags & (CPCommandKeyMask | CPControlKeyMask | CPAlternateKeyMask)) || + + // Is it a key that doesn't produce a character? + ([_characters length] === 0) || + + // Was it identified as an action key by the platform-specific layer? + _isActionKey + ); +} /*! Return YES if this event is a part of processing a browser controlled cut or paste event where the browser will go ahead and do the work of cutting or pasting within the input diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index c2357c51d..938cc7c32 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -1022,16 +1022,18 @@ Sets the selection to a range of characters in response to user action. - (void)keyDown:(CPEvent)event { + [[_window platformWindow] _propagateCurrentDOMEvent:YES]; - [[_window platformWindow] _propagateCurrentDOMEvent:YES]; // for the _CPNativeInputManager (necessary at least on FF and chrome) - - // Only call interpretKeyEvents for non-printable keys (navigation, commands). - // Printable characters are handled exclusively by _CPNativeInputManager. - - var key = event.key; - - if (key && (key.length > 1 || (event.modifierFlags & (CPCommandKeyMask | CPAlternateKeyMask | CPControlKeyMask)))) + if ([event _isActionOrCommandEvent]) + { + // This is a navigation key, action key, or command shortcut. + // Let the Cappuccino framework's key binding system handle it. [self interpretKeyEvents:[event]]; + } + + // This is a normal printable character ('a', '1', '$', 'é'). + // We do nothing, preventing the double-insertion bug. The _CPNativeInputManager + // will capture it from the hidden input field and insert it correctly. [_caret setPermanentlyVisible:YES]; } @@ -2661,7 +2663,10 @@ var _CPCopyPlaceholder = '-'; var currentFirstResponder = [[CPApp keyWindow] firstResponder]; if (currentFirstResponder && [currentFirstResponder respondsToSelector:@selector(insertText:)]) - [currentFirstResponder insertText:textToInsert]; + // setTimeout to prevent flickering + setTimeout(function(){ + [currentFirstResponder insertText:textToInsert] + }, 20); // CRUCIAL: Clear the field immediately after grabbing its content. _CPNativeInputField.innerHTML = ''; diff --git a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j index a4fc8cae8..ece9c92c3 100644 --- a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j +++ b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j @@ -725,16 +725,13 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio // With a few exceptions, all key events are blocked from propagating to // the browser. The logic here allows browser shortcuts (Cmd/Ctrl keys) // and function keys (F1-F12) to pass through, unless explicitly blacklisted. - StopDOMEventPropagation = YES; + StopDOMEventPropagation = YES; var keyCodeForPropagationCheck = aDOMEvent.keyCode || 0; var charForPropagationCheck = String.fromCharCode(keyCodeForPropagationCheck).toLowerCase(); - // Make sure it is not in the blacklists. if (!(CharacterKeysToPrevent[charForPropagationCheck] || KeyCodesToPrevent[keyCodeForPropagationCheck])) { - // It is not in the blacklist, let it through if the ctrl/cmd key is - // also down or it's in the whitelist. if ((modifierFlags & (CPControlKeyMask | CPCommandKeyMask)) || KeyCodesToAllow[keyCodeForPropagationCheck]) StopDOMEventPropagation = NO; } @@ -742,57 +739,75 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio var characters = @"", charactersIgnoringModifiers = @""; - // Grab and store the keyCode for mapping and compatibility. - // This property is deprecated but necessary for the key maps. var keyCode = aDOMEvent.keyCode; if (keyCode in MozKeyCodeToKeyCodeMap) keyCode = MozKeyCodeToKeyCodeMap[keyCode]; + var isActionKey; + var key = aDOMEvent.key; + + // Modern Browser Path (use event.key) + if (key) { + isActionKey = + key === 'Enter' || + key === 'Backspace' || + key === 'Tab' || + key === 'Escape' || + key === 'Delete' || + key.startsWith('Arrow') || + key === 'Home' || + key === 'End' || + key === 'PageUp' || + key === 'PageDown'; + } + // Legacy Browser Fallback (use event.keyCode) + else + { + isActionKey = + (keyCode === 13) || // Enter + (keyCode === 8) || // Backspace + (keyCode === 9) || // Tab + (keyCode === 27) || // Escape + (keyCode === 46) || // Delete + (keyCode >= 37 && keyCode <= 40); // Arrow keys + } + + switch (aDOMEvent.type) { case "keydown": - // For modifier keys, we create a CPFlagsChanged event and stop processing. + // For modifier keys, create a CPFlagsChanged event and stop processing. + // These are always considered "action keys". if ([ModifierKeyCodes containsObject:keyCode]) { event = [CPEvent keyEventWithType:CPFlagsChanged location:location modifierFlags:modifierFlags timestamp:timestamp windowNumber:windowNumber context:nil - characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:keyCode]; + characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:keyCode isActionKey:YES]; break; } - // Determine if the event is a repeat. Use the modern `event.repeat` property - // with a fallback to our manual state tracking for older browsers. var isARepeat = !!aDOMEvent.repeat || (_charCodes[keyCode] != nil); - _charCodes[keyCode] = YES; // Mark the key as down for fallback repeat detection. + _charCodes[keyCode] = YES; - // Determine the character for the event. - // Priority 1: Use the modern `event.key` property. It's the most reliable. if (aDOMEvent.key) { if (aDOMEvent.key.length === 1) { - // This is a printing character (e.g., "a", "P", "#"). characters = aDOMEvent.key; } else { - // This is a named, non-printing key (e.g., "Enter", "ArrowLeft"). - // Map the standard key name to our framework's internal constant. characters = KeyNameToUnicodeMap[aDOMEvent.key] || aDOMEvent.key; } } - // Priority 2: Fallback for older browsers without `event.key`. else { - // First, check if it's a known non-printing key in our legacy map. characters = KeyCodesToUnicodeMap[keyCode]; - // If not, it's likely a printing character. Use the deprecated fromCharCode. - // This is less reliable for international layouts but is the best fallback. if (!characters) { characters = String.fromCharCode(keyCode); - // Manually handle capitalization for this fallback path. + if (modifierFlags & CPShiftKeyMask || _capsLockActive) characters = characters.toUpperCase(); else @@ -800,24 +815,21 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio } } - // This is a simplification; a fully correct implementation would require extensive mapping. charactersIgnoringModifiers = characters.toLowerCase(); + // Pass the determined `isActionKey` flag. event = [CPEvent keyEventWithType:CPKeyDown location:location modifierFlags:modifierFlags timestamp:timestamp windowNumber:windowNumber context:nil - characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:isARepeat keyCode:keyCode]; + characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:isARepeat keyCode:keyCode isActionKey:isActionKey]; break; case "keyup": - // Clear the key's state for our fallback repeat detection. _charCodes[keyCode] = nil; - // Handle the toggling of Caps Lock state. if (keyCode === CPKeyCodes.CAPS_LOCK) { _capsLockActive = !_capsLockActive; - // Update modifierFlags to reflect the new state for this event. if (_capsLockActive) modifierFlags |= CPAlphaShiftKeyMask; else @@ -825,16 +837,15 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio } // For modifier keys, create a CPFlagsChanged event and stop. + // These are always considered "action keys". if ([ModifierKeyCodes containsObject:keyCode]) { event = [CPEvent keyEventWithType:CPFlagsChanged location:location modifierFlags:modifierFlags timestamp:timestamp windowNumber:windowNumber context:nil - characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:keyCode]; + characters:nil charactersIgnoringModifiers:nil isARepeat:NO keyCode:keyCode isActionKey:YES]; break; } - // Determine the character for the keyup event using the same logic as keydown - // for consistency, as this event no longer has access to `keypress` state. if (aDOMEvent.key) { if (aDOMEvent.key.length === 1) @@ -852,9 +863,10 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio if (!(modifierFlags & CPShiftKeyMask) && (modifierFlags & CPCommandKeyMask) && !_capsLockActive) characters = charactersIgnoringModifiers; + // Pass the determined `isActionKey` flag for keyup as well. event = [CPEvent keyEventWithType:CPKeyUp location:location modifierFlags:modifierFlags timestamp: timestamp windowNumber:windowNumber context:nil - characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:NO keyCode:keyCode]; + characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:NO keyCode:keyCode isActionKey:isActionKey]; break; } @@ -867,12 +879,11 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio if (event && ![_platformPasteboard windowShouldSuppressKeyEvent]) { [CPApp sendEvent:event]; - [_platformPasteboard windowDidSendKeyEvent:event]; } var didStop = NO; - // Platform pasteboard can overrule the decision to stop propagation. + if ([_platformPasteboard windowShouldStopPropagation] || (StopDOMEventPropagation && ![_platformPasteboard windowShouldNotStopPropagation])) { didStop = YES;