diff --git a/AppKit/CPEvent.j b/AppKit/CPEvent.j index 7bc06d501..1d486521a 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,28 @@ 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]; +} + +// for backwards compatibility only ++ (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 +{ + return [[self alloc] _initKeyEventWithType:anEventType location:aPoint modifierFlags:modifierFlags + timestamp:aTimestamp windowNumber:aWindowNumber context:aGraphicsContext + characters:characters charactersIgnoringModifiers:unmodCharacters isARepeat:repeatKey keyCode:code isActionKey:NO]; } /*! @@ -252,7 +264,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 +276,7 @@ var _CPEventPeriodicEventPeriod = 0, _charactersIgnoringModifiers = unmodCharacters; _isARepeat = isARepeat; _keyCode = code; + _isActionKey = isAnActionKey; _windowNumber = aWindowNumber; } @@ -571,6 +584,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/CPTextField.j b/AppKit/CPTextField.j index bdf926b09..03416af31 100644 --- a/AppKit/CPTextField.j +++ b/AppKit/CPTextField.j @@ -332,7 +332,8 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder"); characters:nil charactersIgnoringModifiers:nil isARepeat:NO - keyCode:nil]; + keyCode:nil + isActionKey:NO]; [CPTextFieldInputOwner keyUp:cappEvent]; diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 9327db2a4..7e862f1ae 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -449,19 +449,25 @@ var kDelegateRespondsTo_textShouldBeginEditing { [super copy:sender]; - if (![self isRichText]) - return; - var selectedRange = [self selectedRange], pasteboard = [CPPasteboard generalPasteboard], stringForPasting = [[self textStorage] attributedSubstringFromRange:CPMakeRangeCopy(selectedRange)], richData = [_CPRTFProducer produceRTF:stringForPasting documentAttributes:@{}]; + if ([self isRichText]) + { [pasteboard declareTypes:[CPStringPboardType, CPRTFPboardType, _CPSmartPboardType, _CPASPboardType] owner:nil]; [pasteboard setString:[stringForPasting._string stringByReplacingOccurrencesOfString:_CPAttachmentCharacterAsString withString:''] forType:CPStringPboardType]; [pasteboard setString:richData forType:CPRTFPboardType]; [pasteboard setString:_previousSelectionGranularity + '' forType:_CPSmartPboardType]; [pasteboard setString:[[CPKeyedArchiver archivedDataWithRootObject:stringForPasting] rawString] forType:_CPASPboardType]; + } + else + { + [pasteboard declareTypes:[CPStringPboardType, _CPSmartPboardType] owner:nil]; + [pasteboard setString:stringForPasting._string forType:CPStringPboardType]; + [pasteboard setString:_previousSelectionGranularity + '' forType:_CPSmartPboardType]; + } } - (void)_pasteString:(id)stringForPasting @@ -510,8 +516,8 @@ var kDelegateRespondsTo_textShouldBeginEditing - (void)paste:(id)sender { - if (![sender isKindOfClass:_CPNativeInputManager] && [[CPApp currentEvent] type] != CPAppKitDefined) - return + if ([[CPApp currentEvent] type] != CPAppKitDefined) + return; [self _pasteString:[self _stringForPasting]]; } @@ -1017,35 +1023,6 @@ Sets the selection to a range of characters in response to user action. } #endif - -// interface to the _CPNativeInputManager -- (void)_activateNativeInputElement:(DOMElement)aNativeField -{ - var attributes = [[self typingAttributes] copy]; - - // make it invisible - [attributes setObject:[CPColor colorWithRed:1 green:1 blue:1 alpha:0] forKey:CPForegroundColorAttributeName]; - - // FIXME: this hack to provide the visual space for the inputmanager should at least bypass the undomanager - var placeholderString = [[CPAttributedString alloc] initWithString:aNativeField.innerHTML attributes:attributes]; - [self insertText:placeholderString]; - - var caretOrigin = [_layoutManager boundingRectForGlyphRange:CPMakeRange(MAX(0, _selectionRange.location - 1), 1) inTextContainer:_textContainer].origin; - caretOrigin.y += [_layoutManager _characterOffsetAtLocation:MAX(0, _selectionRange.location - 1)]; - caretOrigin.x += 2; // two pixel offset to the LHS character - var cumulativeOffset = [self _cumulativeOffset]; - - -#if PLATFORM(DOM) - aNativeField.style.left = (caretOrigin.x + cumulativeOffset.x) + "px"; - aNativeField.style.top = (caretOrigin.y + cumulativeOffset.y) + "px"; - aNativeField.style.font = [[_typingAttributes objectForKey:CPFontAttributeName] cssString]; - aNativeField.style.color = [[_typingAttributes objectForKey:CPForegroundColorAttributeName] cssString]; -#endif - - [_caret setVisibility:NO]; // hide our caret because now the system caret takes over -} - - (CPArray)selectedRanges { return [_selectionRange]; @@ -1056,11 +1033,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) - - if (![_CPNativeInputManager isNativeInputFieldActive] && ![_CPNativeInputManager isDeadKey:event]) + 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]; } @@ -1121,6 +1105,9 @@ Sets the selection to a range of characters in response to user action. if (![self isSelectable]) return; + // this is for the ipad-keyboard + [_CPNativeInputManager focusForClipboardOfTextView:self]; + [_CPNativeInputManager cancelCurrentInputSessionIfNeeded]; [_caret setVisibility:NO]; @@ -1728,6 +1715,9 @@ Sets the selection to a range of characters in response to user action. - (void)cut:(id)sender { + if ([[CPApp currentEvent] type] != CPAppKitDefined) + return; + var selectedRange = [self selectedRange]; if (selectedRange.length < 1) @@ -2631,59 +2621,28 @@ var CPTextViewAllowsUndoKey = @"CPTextViewAllowsUndoKey", var _CPNativeInputField, - _CPNativeInputFieldKeyDownCalled, - _CPNativeInputFieldKeyUpCalled, - _CPNativeInputFieldKeyPressedCalled, - _CPNativeInputFieldActive; + _isComposing = NO; // Flag to track if an IME/dead key session is active. var _CPCopyPlaceholder = '-'; @implementation _CPNativeInputManager : CPObject -+ (BOOL)isNativeInputFieldActive -{ - return _CPNativeInputFieldActive; -} + (void)isDeadKey:(CPEvent)event { #if PLATFORM(DOM) - return event._DOMEvent && (event._DOMEvent.key == 'Dead' || event._DOMEvent.key == 'Process'); + return event._DOMEvent && (event._DOMEvent.key === 'Dead' || event._DOMEvent.key === 'Process'); #endif - return NO; } -+ (void)cancelCurrentNativeInputSession -{ - -#if PLATFORM(DOM) - _CPNativeInputField.innerHTML = ''; -#endif - - [self _endInputSessionWithString:_CPNativeInputField.innerHTML]; -} + (void)cancelCurrentInputSessionIfNeeded { - if (!_CPNativeInputFieldActive) - return; - - [self cancelCurrentNativeInputSession]; -} - -+ (void)_endInputSessionWithString:(CPString)aStr -{ - _CPNativeInputFieldActive = NO; - - var currentFirstResponder = [[CPApp keyWindow] firstResponder], - placeholderRange = CPMakeRange([currentFirstResponder selectedRange].location - 1, 1); - - [currentFirstResponder setSelectedRange:placeholderRange]; - [currentFirstResponder insertText:aStr]; - _CPNativeInputField.innerHTML = ''; - - - [self hideInputElement]; - [currentFirstResponder updateInsertionPointStateAndRestartTimer:YES]; +#if PLATFORM(DOM) + if (_CPNativeInputField) { + _CPNativeInputField.innerHTML = ''; + } + _isComposing = NO; +#endif } + (void)initialize @@ -2691,236 +2650,197 @@ var _CPCopyPlaceholder = '-'; #if PLATFORM(DOM) _CPNativeInputField = document.createElement("div"); _CPNativeInputField.contentEditable = YES; - _CPNativeInputField.style.width = "64px"; - _CPNativeInputField.style.zIndex = 10000; + + // Style the input field to be invisible but focusable _CPNativeInputField.style.position = "absolute"; - _CPNativeInputField.style.visibility = "visible"; - _CPNativeInputField.style.padding = "0px"; - _CPNativeInputField.style.margin = "0px"; + _CPNativeInputField.style.top = "-1000px"; + _CPNativeInputField.style.left = "-1000px"; + _CPNativeInputField.style.width = "1px"; + _CPNativeInputField.style.height = "1px"; + _CPNativeInputField.style.opacity = "0"; + _CPNativeInputField.style.overflow = "hidden"; _CPNativeInputField.style.whiteSpace = "pre"; - _CPNativeInputField.style.outline = "0px solid transparent"; + _CPNativeInputField.style.zIndex = -1; // Put it behind everything document.body.appendChild(_CPNativeInputField); - _CPNativeInputField.addEventListener("keyup", function(e) + // Central function to handle inserting text into the CPTextView + var handleInput = function(textToInsert) { - _CPNativeInputFieldKeyUpCalled = YES; - - // filter out the shift-up, cursor keys and friends used to access the deadkeys - // fixme: e.which is depreciated(?) -> find a better way to identify the modifier-keyups - if (e.which < 27 || e.which == 91 || e.which == 93) // include apple command keys - { - if (e.which == 13) - _CPNativeInputField.innerHTML = ''; - - if (_CPNativeInputField.innerHTML.length == 0 || _CPNativeInputField.innerHTML.length > 2) // backspace - [self cancelCurrentInputSessionIfNeeded]; - - return false; // prevent the default behaviour - } - - var currentFirstResponder = [[CPApp keyWindow] firstResponder]; - - if (![currentFirstResponder respondsToSelector:@selector(_activateNativeInputElement:)]) - return false; // prevent the default behaviour - - // chrome-trigger: keypressed is omitted for deadkeys - if (!_CPNativeInputFieldActive && _CPNativeInputFieldKeyPressedCalled == NO && _CPNativeInputField.innerHTML.length && _CPNativeInputField.innerHTML != _CPCopyPlaceholder && _CPNativeInputField.innerHTML.length < 3) - { - _CPNativeInputFieldActive = YES; - [currentFirstResponder _activateNativeInputElement:_CPNativeInputField]; - } - else - { - if (_CPNativeInputFieldActive) - [self _endInputSessionWithString:_CPNativeInputField.innerHTML]; - - // prevent the copy placeholder beeing removed by cursor keys - if (_CPNativeInputFieldKeyPressedCalled) - _CPNativeInputField.innerHTML = ''; - } - - _CPNativeInputFieldKeyDownCalled = NO; - - return false; // prevent the default behaviour - }, true); - - _CPNativeInputField.addEventListener("keydown", function(e) - { - // this protects from heavy typing and the shift key - if (_CPNativeInputFieldKeyDownCalled) - return true; - - _CPNativeInputFieldKeyDownCalled = YES; - _CPNativeInputFieldKeyUpCalled = NO; - _CPNativeInputFieldKeyPressedCalled = NO; - var currentFirstResponder = [[CPApp keyWindow] firstResponder]; - - // webkit-browsers: cursor keys do not emit keypressed and would otherwise activate deadkey mode - if (!CPBrowserIsEngine(CPGeckoBrowserEngine) && e.which >= 37 && e.which <= 40) - _CPNativeInputFieldKeyPressedCalled = YES; - - if (![currentFirstResponder respondsToSelector:@selector(_activateNativeInputElement:)]) + if (!textToInsert) return; - // FF-trigger: here the best way to detect a dead key is the missing keyup event - if (CPBrowserIsEngine(CPGeckoBrowserEngine)) + var currentFirstResponder = [[CPApp keyWindow] firstResponder]; + + if (currentFirstResponder && [currentFirstResponder respondsToSelector:@selector(insertText:)]) + // setTimeout to prevent flickering setTimeout(function(){ - _CPNativeInputFieldKeyDownCalled = NO; - - if (!_CPNativeInputFieldActive && _CPNativeInputFieldKeyUpCalled == NO && _CPNativeInputField.innerHTML.length && _CPNativeInputField.innerHTML != _CPCopyPlaceholder && _CPNativeInputField.innerHTML.length < 3 && !e.repeat) - { - _CPNativeInputFieldActive = YES; - [currentFirstResponder _activateNativeInputElement:_CPNativeInputField]; - } - else if (!_CPNativeInputFieldActive) - [self hideInputElement]; - }, 200); - - return false; - }, true); // capture mode - - _CPNativeInputField.addEventListener("keypress", function(e) - { - _CPNativeInputFieldKeyUpCalled = YES; - _CPNativeInputFieldKeyPressedCalled = YES; - return false; - - }, true); // capture mode - - _CPNativeInputField.onpaste = function(e) - { - var nativeClipboard = (e.originalEvent || e).clipboardData, - richtext, - pasteboard = [CPPasteboard generalPasteboard], - currentFirstResponder = [[CPApp keyWindow] firstResponder], - isPlain = NO; - - if ([currentFirstResponder respondsToSelector:@selector(isRichText)] && ![currentFirstResponder isRichText]) - isPlain = YES; - - // this is the rich chrome / FF codepath (where we can use RTF directly) - if ((richtext = nativeClipboard.getData('text/rtf')) && !(!!(e.originalEvent || e).shiftKey) && !isPlain) - { - e.preventDefault(); - - // setTimeout to prevent flickering in FF - setTimeout(function(){ - [currentFirstResponder insertText:[[_CPRTFParser new] parseRTF:richtext]] + [currentFirstResponder insertText:textToInsert] }, 20); - return false; - } - - // plain is the same in all browsers... - - var data = e.clipboardData.getData('text/plain'), - cappString = [pasteboard stringForType:CPStringPboardType]; - - if (cappString != data) - { - [pasteboard declareTypes:[CPStringPboardType] owner:nil]; - [pasteboard setString:data forType:CPStringPboardType]; - } - - setTimeout(function(){ // prevent dom-flickering (only needed for FF) - [currentFirstResponder paste:self]; - }, 20); - - return false; + // CRUCIAL: Clear the field immediately after grabbing its content. + _CPNativeInputField.innerHTML = ''; }; - if (CPBrowserIsEngine(CPGeckoBrowserEngine)) - { - _CPNativeInputField.oncopy = function(e) - { - var pasteboard = [CPPasteboard generalPasteboard], - string, - currentFirstResponder = [[CPApp keyWindow] firstResponder]; + // Intercept problematic keys before the browser acts. + _CPNativeInputField.addEventListener('keydown', function(e) { - [currentFirstResponder copy:self]; - - var stringForPasting = [pasteboard stringForType:CPStringPboardType]; - e.clipboardData.setData('text/plain', stringForPasting); - - return false; - }; - - _CPNativeInputField.oncut = function(e) - { - var pasteboard = [CPPasteboard generalPasteboard], - string, - currentFirstResponder = [[CPApp keyWindow] firstResponder]; - - // prevent dom-flickering - setTimeout(function(){ - [currentFirstResponder cut:self]; - }, 20); - - // this is necessary because cut will only execute in the future - [currentFirstResponder copy:self]; - - var stringForPasting = [pasteboard stringForType:CPStringPboardType]; - - e.clipboardData.setData('text/plain', stringForPasting); - - return false; + if (e.key === 'Enter' || (e.key === 'Backspace' && _CPNativeInputField.innerHTML === '')) { + // Prevent browser default action: + // - 'Enter': Prevents inserting

. + // - 'Backspace' on empty: Prevents inserting junk characters on iPadOS. + e.preventDefault(); } - } + }); + + // This listener handles all other character input. + _CPNativeInputField.addEventListener('input', function(e) + { + // If we are in a composition (e.g., IME), do nothing yet. + if (_isComposing) + return; + + // Safety net: ignore deletion events, as they are handled by keydown. + if (e.inputType && e.inputType.startsWith('delete')) + { + _CPNativeInputField.innerHTML = ''; + return; + } + + // Robustness: Use 'textContent' instead of 'innerHTML' to strip any + // unexpected HTML tags the browser might have inserted. + var textToInsert = e.target.textContent; + handleInput(textToInsert); + }); + + // Fires when a composition session starts (e.g., user presses a dead key or starts an IME). + _CPNativeInputField.addEventListener('compositionstart', function(e) { + _isComposing = YES; + }); + + // Fires when the composition is finished. + _CPNativeInputField.addEventListener('compositionend', function(e) { + // The composition is over. `e.data` has the final string (e.g., "é"). + handleInput(e.data); + _isComposing = NO; + }); + + // PASTE handler + _CPNativeInputField.onpaste = function(e) + { + e.preventDefault(); + var nativeClipboard = (e.originalEvent || e).clipboardData; + var currentFirstResponder = [[CPApp keyWindow] firstResponder]; + + // Can we accept richtext? Then this is our preference (fixme: shift key to force plain text paste) + if ([currentFirstResponder isRichText]) + { + var richtext = nativeClipboard.getData('text/rtf'); + + // prefer RTF form the outside of cappuccino + if (richtext) + richtext = [[_CPRTFParser new] parseRTF:richtext]; + else + { + var pasteboard = [CPPasteboard generalPasteboard]; + // If no RTF is available, try to get the internal represatation of richtext from the pasteboard + var richData = [pasteboard stringForType:_CPASPboardType]; + + if (richData) + richtext = [CPKeyedUnarchiver unarchiveObjectWithData:[CPData dataWithRawString:richData]]; + } + + if (richtext) + { + [currentFirstResponder _pasteString:richtext]; + + return; + } + // If no richtext is available, fall back to plain text + } + + var nativeString = nativeClipboard.getData('text/plain'); + [currentFirstResponder _pasteString:nativeString || [pasteboard stringForType:CPStringPboardType] || '']; + }; + + // COPY handler + _CPNativeInputField.oncopy = function(e) + { + e.preventDefault(); + var pasteboard = [CPPasteboard generalPasteboard]; + var nativeClipboard = (e.originalEvent || e).clipboardData; + + // First, copy the data to populate the CP clipboard + [[[CPApp keyWindow] firstResponder] copy:self]; + + // Now, copy the data over to the native clipboard + var stringForPasting = [pasteboard stringForType:CPStringPboardType] || ''; + nativeClipboard.setData('text/plain', stringForPasting); + + var rtfForPasting = [pasteboard stringForType:CPRTFPboardType]; + + if (rtfForPasting) + nativeClipboard.setData('text/rtf', rtfForPasting); + }; + + // CUT handler + _CPNativeInputField.oncut = function(e) + { + e.preventDefault(); + var pasteboard = [CPPasteboard generalPasteboard]; + var nativeClipboard = (e.originalEvent || e).clipboardData; + var currentFirstResponder = [[CPApp keyWindow] firstResponder]; + + // First, copy the data to populate the CP clipboard + [currentFirstResponder copy:self]; + + // Now, copy the data to the native clipboard + var stringForPasting = [pasteboard stringForType:CPStringPboardType] || ''; + nativeClipboard.setData('text/plain', stringForPasting); + var rtfForPasting = [pasteboard stringForType:CPRTFPboardType]; + + if (rtfForPasting) + nativeClipboard.setData('text/rtf', rtfForPasting); + + // Then, perform the delete part of the cut operation in the text view + [currentFirstResponder deleteBackward:self]; + }; #endif } + (void)focusForTextView:(CPTextView)currentFirstResponder { - if (![currentFirstResponder respondsToSelector:@selector(_activateNativeInputElement:)]) - return; - - [self hideInputElement]; - #if PLATFORM(DOM) - _CPNativeInputField.focus(); -#endif + if (_CPNativeInputField && document.activeElement !== _CPNativeInputField) + _CPNativeInputField.focus(); +#endif } + (void)focusForClipboardOfTextView:(CPTextView)textview { - #if PLATFORM(DOM) - if (!_CPNativeInputFieldActive && _CPNativeInputField.innerHTML.length == 0) - _CPNativeInputField.innerHTML = _CPCopyPlaceholder; // make sure we have a selection to allow the native pasteboard work in safari + var selectedRange = [textview selectedRange]; + if (selectedRange.length > 0) { + // Put the selected text into the hidden div so the browser can natively copy it. + var textToCopy = [[[textview textStorage] string] substringWithRange:selectedRange]; + _CPNativeInputField.innerHTML = textToCopy; + } else { + // For paste, we just need the field to be focusable. + _CPNativeInputField.innerHTML = _CPCopyPlaceholder; + } [self focusForTextView:textview]; - // select all in the contenteditable div (http://stackoverflow.com/questions/12243898/how-to-select-all-text-in-contenteditable-div) - if (document.body.createTextRange) - { - var range = document.body.createTextRange(); - - range.moveToElementText(_CPNativeInputField); - range.select(); - } - else if (window.getSelection) - { - var selection = window.getSelection(), - range = document.createRange(); - + // Select the content of the hidden div so copy/cut works. + if (window.getSelection && document.createRange) { + var selection = window.getSelection(); + var range = document.createRange(); range.selectNodeContents(_CPNativeInputField); selection.removeAllRanges(); selection.addRange(range); } #endif - -} - -+ (void)hideInputElement -{ - -#if PLATFORM(DOM) - _CPNativeInputField.style.top = "-10000px"; - _CPNativeInputField.style.left = "-10000px"; -#endif - } @end diff --git a/AppKit/Platform/DOM/CPPlatformPasteboard.j b/AppKit/Platform/DOM/CPPlatformPasteboard.j index 50310503f..102feb670 100644 --- a/AppKit/Platform/DOM/CPPlatformPasteboard.j +++ b/AppKit/Platform/DOM/CPPlatformPasteboard.j @@ -305,7 +305,7 @@ var hasEditableTarget = function(aDOMEvent) location = [[CPApp currentEvent] locationInWindow], anEvent = [CPEvent keyEventWithType:CPKeyDown location:location modifierFlags:modifierFlags timestamp:timestamp windowNumber:windowNumber context:nil - characters:characters charactersIgnoringModifiers:characters isARepeat:NO keyCode:keyCode]; + characters:characters charactersIgnoringModifiers:characters isARepeat:NO keyCode:keyCode isActionKey:YES]; anEvent._data1 = @{ "simulated": YES }; anEvent._DOMEvent = aDOMEvent; diff --git a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j index 49de64861..7c714c263 100644 --- a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j +++ b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j @@ -192,6 +192,33 @@ KeyCodesToUnicodeMap[CPKeyCodes.OPEN_SQUARE_BRACKET] = "["; KeyCodesToUnicodeMap[CPKeyCodes.BACKSLASH] = "\\"; KeyCodesToUnicodeMap[CPKeyCodes.CLOSE_SQUARE_BRACKET] = "]"; +var KeyNameToUnicodeMap = {}; +KeyNameToUnicodeMap["Backspace"] = CPDeleteCharacter; +KeyNameToUnicodeMap["Delete"] = CPDeleteFunctionKey; +KeyNameToUnicodeMap["Tab"] = CPTabCharacter; +KeyNameToUnicodeMap["Enter"] = CPCarriageReturnCharacter; +KeyNameToUnicodeMap["Escape"] = CPEscapeFunctionKey; +KeyNameToUnicodeMap["PageUp"] = CPPageUpFunctionKey; +KeyNameToUnicodeMap["PageDown"] = CPPageDownFunctionKey; +KeyNameToUnicodeMap["ArrowLeft"] = CPLeftArrowFunctionKey; +KeyNameToUnicodeMap["ArrowUp"] = CPUpArrowFunctionKey; +KeyNameToUnicodeMap["ArrowRight"] = CPRightArrowFunctionKey; +KeyNameToUnicodeMap["ArrowDown"] = CPDownArrowFunctionKey; +KeyNameToUnicodeMap["Home"] = CPHomeFunctionKey; +KeyNameToUnicodeMap["End"] = CPEndFunctionKey; +// Add safeguards for punctuation +KeyNameToUnicodeMap[";"] = ";"; +KeyNameToUnicodeMap["-"] = "-"; +KeyNameToUnicodeMap["="] = "="; +KeyNameToUnicodeMap[","] = ","; +KeyNameToUnicodeMap["."] = "."; +KeyNameToUnicodeMap["/"] = "/"; +KeyNameToUnicodeMap["`"] = "`"; +KeyNameToUnicodeMap["'"] = "'"; +KeyNameToUnicodeMap["["] = "["; +KeyNameToUnicodeMap["\\"] = "\\"; +KeyNameToUnicodeMap["]"] = "]"; + var ModifierKeyCodes = [ CPKeyCodes.META, CPKeyCodes.WEBKIT_RIGHT_META, @@ -438,7 +465,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio theDocument.addEventListener("keyup", keyEventCallback, NO); theDocument.addEventListener("keydown", keyEventCallback, NO); - theDocument.addEventListener("keypress", keyEventCallback, NO); + // "keypress" listener removed as it's deprecated and no longer used in the new logic. theDocument.addEventListener("touchstart", touchEventCallback, {passive: false}); theDocument.addEventListener("touchend", touchEventCallback, {passive: false}); @@ -470,7 +497,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio theDocument.removeEventListener("keyup", keyEventCallback, NO); theDocument.removeEventListener("keydown", keyEventCallback, NO); - theDocument.removeEventListener("keypress", keyEventCallback, NO); theDocument.removeEventListener("touchstart", touchEventCallback, NO); theDocument.removeEventListener("touchend", touchEventCallback, NO); @@ -503,7 +529,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio theDocument.attachEvent("onkeyup", keyEventCallback); theDocument.attachEvent("onkeydown", keyEventCallback); - theDocument.attachEvent("onkeypress", keyEventCallback); + // "onkeypress" listener removed. _DOMWindow.attachEvent("onresize", resizeEventCallback); @@ -533,7 +559,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio theDocument.detachEvent("onkeyup", keyEventCallback); theDocument.detachEvent("onkeydown", keyEventCallback); - theDocument.detachEvent("onkeypress", keyEventCallback); _DOMWindow.detachEvent("onresize", resizeEventCallback); @@ -709,157 +734,138 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio (_capsLockActive ? CPAlphaShiftKeyMask : 0); // With a few exceptions, all key events are blocked from propagating to - // the browser. Here the following exceptions are being allowed: - // - // - All keys pressed along with a ctrl or cmd key _unless_ they are in - // one of the two blacklists. - // - Any key listed in the whitelist. - // - // The ctrl/cmd keys are used for browser hotkeys as are the keys listed in - // the whitelist (F1-F12 at the time of writing). - // - // If a key is listed in both the blacklist and whitelist, the blacklist is - // checked first. The key will be blocked from propagating in that case. + // the browser. The logic here allows browser shortcuts (Cmd/Ctrl keys) + // and function keys (F1-F12) to pass through, unless explicitly blacklisted. StopDOMEventPropagation = YES; + var keyCodeForPropagationCheck = aDOMEvent.keyCode || 0; + var charForPropagationCheck = String.fromCharCode(keyCodeForPropagationCheck).toLowerCase(); - // Make sure it is not in the blacklists. - if (!(CharacterKeysToPrevent[String.fromCharCode(aDOMEvent.keyCode || aDOMEvent.charCode).toLowerCase()] || KeyCodesToPrevent[aDOMEvent.keyCode])) + 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[aDOMEvent.keyCode]) + if ((modifierFlags & (CPControlKeyMask | CPCommandKeyMask)) || KeyCodesToAllow[keyCodeForPropagationCheck]) StopDOMEventPropagation = NO; } - var overrideCharacters = nil, + var characters = @"", charactersIgnoringModifiers = @""; + var keyCode = aDOMEvent.keyCode; + if (keyCode in MozKeyCodeToKeyCodeMap) + keyCode = MozKeyCodeToKeyCodeMap[keyCode]; + + var isActionKey; + var key = aDOMEvent.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'; + } + else + { + isActionKey = + (keyCode === 13) || (keyCode === 8) || (keyCode === 9) || + (keyCode === 27) || (keyCode === 46) || (keyCode >= 37 && keyCode <= 40); + } + switch (aDOMEvent.type) { case "keydown": - // Grab and store the keycode now since it is correct and consistent at this point. - if (aDOMEvent.keyCode in MozKeyCodeToKeyCodeMap) - _keyCode = MozKeyCodeToKeyCodeMap[aDOMEvent.keyCode]; - else - _keyCode = aDOMEvent.keyCode; - - var characters; - - // Handle key codes for which String.fromCharCode won't work. - // Refs #1036: In Internet Explorer, both 'which' and 'charCode' are undefined for special keys. - if (aDOMEvent.which === 0 || aDOMEvent.charCode === 0 || (aDOMEvent.which === undefined && aDOMEvent.charCode === undefined)) - characters = KeyCodesToUnicodeMap[_keyCode]; - - // The problem with keyCode is that this property refers to keys on the keyboard and not to characters - // This is why String.fromCharCode does not always work in more recent versions of Firefox - // E.g. pressing a '#' on a German keyboard gives you a charCode of 163, which refers to '£' and not '#' - // The property key works fine, though. From there we can get the actual character more robustly. - // Therefore we prefer key over keyCode whenever possible - - if (!characters) - characters = (aDOMEvent.key && aDOMEvent.key.length == 1) ? aDOMEvent.key.toLowerCase() : String.fromCharCode(_keyCode).toLowerCase(); - - overrideCharacters = (modifierFlags & CPShiftKeyMask || _capsLockActive) ? characters.toUpperCase() : characters; - - // check for caps lock state - if (_keyCode === CPKeyCodes.CAPS_LOCK) + if ([ModifierKeyCodes containsObject:keyCode]) { - _capsLockActive = YES; - - // Make sure the caps lock flag is set in modifierFlags - modifierFlags |= CPAlphaShiftKeyMask; - } - - if ([ModifierKeyCodes containsObject:_keyCode]) - { - // A modifier key will never fire keypress. We don't need to do any other processing so we just fire it here and break. 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; + } - break; - } - else if (modifierFlags & (CPControlKeyMask | CPCommandKeyMask)) + var isARepeat = !!aDOMEvent.repeat || (_charCodes[keyCode] != nil); + _charCodes[keyCode] = YES; + + if (aDOMEvent.key) { - //we are simply going to skip all keypress events that use cmd/ctrl key - //this lets us be consistent in all browsers and send on the keydown - //which means we can cancel the event early enough, but only if sendEvent needs to - } - else if (CPKeyCodes.firesKeyPressEvent(_keyCode, aDOMEvent.key, _lastKey, aDOMEvent.shiftKey, aDOMEvent.ctrlKey, aDOMEvent.altKey)) - { - // this branch is taken by events which fire keydown, keypress, and keyup. - // this is the only time we'll ALLOW character keys to propagate (needed for text fields) - StopDOMEventPropagation = NO; - break; + if (aDOMEvent.key.length === 1) + { + characters = aDOMEvent.key; + } + // Correctly handle dead keys to prevent inserting "Dead" + else if (aDOMEvent.key === "Dead" || aDOMEvent.key === "Process") { + characters = @""; + } + // For other named keys, map them or fall back to an empty string. + else + { + characters = KeyNameToUnicodeMap[aDOMEvent.key] || @""; + } } else { - //this branch is taken by "remedial" key events - // In this state we continue to keypress and send the CPEvent + characters = KeyCodesToUnicodeMap[keyCode]; + + if (!characters) + { + characters = String.fromCharCode(keyCode); + if (modifierFlags & CPShiftKeyMask || _capsLockActive) + characters = characters.toUpperCase(); + else + characters = characters.toLowerCase(); + } } - case "keypress": - // we unconditionally break on keypress events with modifiers, - // because we forced the event to be sent on the keydown - if (aDOMEvent.type === "keypress" && (modifierFlags & (CPControlKeyMask | CPCommandKeyMask))) - break; - - var keyCode = _keyCode, - charCode = aDOMEvent.keyCode || aDOMEvent.charCode, - isARepeat = (_charCodes[keyCode] != nil); - - _lastKey = keyCode; - _charCodes[keyCode] = charCode; - - var characters = overrideCharacters; - // Is this a special key? - if (!characters && (aDOMEvent.which === 0 || aDOMEvent.charCode === 0)) - characters = KeyCodesToUnicodeMap[charCode]; - - if (!characters) - characters = String.fromCharCode(charCode); - - charactersIgnoringModifiers = characters.toLowerCase(); // FIXME: This isn't correct. It SHOULD include Shift. - - // Safari won't send proper capitalization during cmd-key events - if (!overrideCharacters && (modifierFlags & CPCommandKeyMask) && ((modifierFlags & CPShiftKeyMask) || _capsLockActive)) - characters = characters.toUpperCase(); + charactersIgnoringModifiers = characters.toLowerCase(); event = [CPEvent keyEventWithType:CPKeyDown location:location modifierFlags:modifierFlags timestamp:timestamp windowNumber:windowNumber context:nil - characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:isARepeat keyCode:charCode]; + characters:characters charactersIgnoringModifiers:charactersIgnoringModifiers isARepeat:isARepeat keyCode:keyCode isActionKey:isActionKey]; break; case "keyup": - var keyCode = aDOMEvent.keyCode, - charCode = _charCodes[keyCode]; - - _keyCode = -1; - _lastKey = -1; _charCodes[keyCode] = nil; - // check for caps lock state if (keyCode === CPKeyCodes.CAPS_LOCK) { - _capsLockActive = NO; - - // Make sure the caps lock flag is cleared in modifierFlags - modifierFlags &= ~CPAlphaShiftKeyMask; + _capsLockActive = !_capsLockActive; + if (_capsLockActive) + modifierFlags |= CPAlphaShiftKeyMask; + else + modifierFlags &= ~CPAlphaShiftKeyMask; } if ([ModifierKeyCodes containsObject:keyCode]) { - // A modifier key will never fire keypress. We don't need to do any other processing so we just fire it here and break. 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; } - var characters = KeyCodesToUnicodeMap[charCode] || String.fromCharCode(charCode); + if (aDOMEvent.key) + { + if (aDOMEvent.key.length === 1) { + characters = aDOMEvent.key; + } + // Ensure keyup events also don't produce "Dead" + else if (aDOMEvent.key === "Dead" || aDOMEvent.key === "Process") { + characters = @""; + } + else + { + characters = KeyNameToUnicodeMap[aDOMEvent.key] || @""; + } + } + else + characters = KeyCodesToUnicodeMap[keyCode] || String.fromCharCode(keyCode); + charactersIgnoringModifiers = characters.toLowerCase(); if (!(modifierFlags & CPShiftKeyMask) && (modifierFlags & CPCommandKeyMask) && !_capsLockActive) @@ -867,7 +873,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio 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; } @@ -880,12 +886,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 either way, or it might have no opinion. + if ([_platformPasteboard windowShouldStopPropagation] || (StopDOMEventPropagation && ![_platformPasteboard windowShouldNotStopPropagation])) { didStop = YES; diff --git a/AppKit/Platform/DOM/CPPlatformWindow+DOMKeys.j b/AppKit/Platform/DOM/CPPlatformWindow+DOMKeys.j index a24d241f3..a76bed207 100644 --- a/AppKit/Platform/DOM/CPPlatformWindow+DOMKeys.j +++ b/AppKit/Platform/DOM/CPPlatformWindow+DOMKeys.j @@ -132,38 +132,36 @@ CPKeyCodes = { /*! * Returns true if the key fires a keypress event in the current browser. + * The keypress event is deprecated, but this function helps manage legacy + * event handling by predicting its behavior. * - * Accoridng to MSDN [1] IE only fires keypress events for the following keys: - * - Letters: A - Z (uppercase and lowercase) - * - Numerals: 0 - 9 - * - Symbols: ! @ # $ % ^ & * ( ) _ - + = < [ ] { } , . / ? \ | ' ` " ~ - * - System: ESC, SPACEBAR, ENTER - * - * That's not entirely correct though, for instance there's no distinction - * between upper and lower case letters. - * - * [1] http://msdn2.microsoft.com/en-us/library/ms536939(VS.85).aspx) - * - * Safari is similar to IE, but does not fire keypress for ESC. - * - * Additionally, IE6 does not fire keydown or keypress events for letters when - * the control or alt keys are held down and the shift key is not. IE7 does - * fire keydown in these cases, though, but not keypress. - * - * @param keyCode A key code. - * @param opt_heldKeyCode Key code of a currently-held key. - * @param opt_shiftKey Whether the shift key is held down. - * @param opt_ctrlKey Whether the control key is held down. - * @param opt_altKey Whether the alt key is held down. - * @return Returns YES if it's a key that fires a keypress event. + * @param {number} keyCode A key code. + * @param {string} key The `key` property from the keyboard event. + * @param {number} opt_heldKeyCode Key code of a currently-held key. + * @param {boolean} opt_shiftKey Whether the shift key is held down. + * @param {boolean} opt_ctrlKey Whether the control key is held down. + * @param {boolean} opt_altKey Whether the alt key is held down. + * @return {boolean} Returns YES if it's a key that fires a keypress event. */ CPKeyCodes.firesKeyPressEvent = function(keyCode, key, opt_heldKeyCode, opt_shiftKey, opt_ctrlKey, opt_altKey) { - // The property key from event is one character wide in case of 'regular' keys (as opposed e.g. to arrow keys) - // Regular keys all fire the keypress event + // Modern approach: Use event.key if available, as it is the most reliable standard. + if (key) + { + // Any key that produces a single, printable character fires a keypress event. + if (key.length === 1) + return true; - if (key && key.length == 1) - return true; + // "Enter" is a special non-printable key that historically fires keypress for compatibility. + if (key === "Enter") + return true; + + // For all other non-printable keys (e.g., "ArrowLeft", "Escape", "F1"), + // modern browsers do not fire a keypress event. + return false; + } + + // --- Legacy Fallback Logic (for browsers that don't support event.key) --- if (!CPFeatureIsCompatible(CPJavaScriptRemedialKeySupport)) return true; @@ -196,9 +194,11 @@ CPKeyCodes.firesKeyPressEvent = function(keyCode, key, opt_heldKeyCode, opt_shif /*! * Test for whether or not a given keyCode represents a character key. + * NOTE: This is a legacy function for browsers that don't support `event.key`. + * It is unreliable because `keyCode` represents a physical key, not the character produced. * - * @param keyCode A key code. - * @return Returns YES if the keyCode is a character key. + * @param {number} keyCode A key code. + * @return {boolean} Returns YES if the keyCode is a character key. */ CPKeyCodes.isCharacterKey = function(keyCode) { diff --git a/Tests/AppKit/CPButtonTest.j b/Tests/AppKit/CPButtonTest.j index 19c0172fb..9abf42e27 100644 --- a/Tests/AppKit/CPButtonTest.j +++ b/Tests/AppKit/CPButtonTest.j @@ -44,11 +44,11 @@ [button setKeyEquivalent:"a"]; [button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil - characters:"b" charactersIgnoringModifiers:"b" isARepeat:NO keyCode:0]]; + characters:"b" charactersIgnoringModifiers:"b" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:wasClicked]; [button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil - characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]]; + characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertTrue:wasClicked]; } @@ -60,11 +60,11 @@ [button setKeyEquivalentModifierMask:CPAlternateKeyMask]; [button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil - characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]]; + characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:wasClicked]; [button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:CPAlternateKeyMask timestamp:0 windowNumber:0 context:nil - characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]]; + characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertTrue:wasClicked]; } @@ -76,12 +76,12 @@ [button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil - characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]]; + characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:wasClicked]; [button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:CPShiftKeyMask timestamp:0 windowNumber:0 context:nil - characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]]; + characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertTrue:wasClicked]; } @@ -92,15 +92,15 @@ [button setKeyEquivalent:CPEscapeFunctionKey]; [button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil - characters:CPDeleteCharacter charactersIgnoringModifiers:CPDeleteCharacter isARepeat:NO keyCode:0]]; + characters:CPDeleteCharacter charactersIgnoringModifiers:CPDeleteCharacter isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:wasClicked]; [button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil - characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]]; + characters:"a" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:wasClicked]; [button performKeyEquivalent:[CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil - characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0]]; + characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertTrue:wasClicked]; } diff --git a/Tests/AppKit/CPEventTest.j b/Tests/AppKit/CPEventTest.j index 7570583dc..feef12694 100644 --- a/Tests/AppKit/CPEventTest.j +++ b/Tests/AppKit/CPEventTest.j @@ -31,7 +31,7 @@ timestamp:400.5 windowNumber:300 context:nil eventNumber:0 clickCount:2 pressure:0.5]; [self assert:@"CPEvent: type=2 loc={50, 50} time=400.5 flags=0x20000 win=undefined winNum=0 ctxt=null evNum=0 click=2 buttonNumber=0 pressure=0.5" equals:[anEvent description]]; - anEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPShiftKeyMask | CPCommandKeyMask timestamp:12345.6 windowNumber:10 context:nil characters:"X" charactersIgnoringModifiers:"x" isARepeat:NO keyCode:10]; + anEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPShiftKeyMask | CPCommandKeyMask timestamp:12345.6 windowNumber:10 context:nil characters:"X" charactersIgnoringModifiers:"x" isARepeat:NO keyCode:10 isActionKey:NO]; [self assert:@"CPEvent: type=10 loc={0, 0} time=12345.6 flags=0x120000 win=null winNum=10 ctxt=null chars=\"X\" unmodchars=\"x\" repeat=0 keyCode=10" equals:[anEvent description]]; @@ -44,13 +44,13 @@ { [self assert:0 equals:[CPEvent modifierFlags] message:@"no modifier flags active in a newly started app"]; - var anEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPShiftKeyMask timestamp:0 windowNumber:0 context:nil characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]; + var anEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPShiftKeyMask timestamp:0 windowNumber:0 context:nil characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]; [CPApp sendEvent:anEvent]; [self assert:CPShiftKeyMask equals:[CPEvent modifierFlags] message:@"shift key pressed"]; // When the key up event is sent the modifier flags are cleared. - anEvent = [CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0]; + anEvent = [CPEvent keyEventWithType:CPKeyUp location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil characters:"A" charactersIgnoringModifiers:"a" isARepeat:NO keyCode:0 isActionKey:NO]; [CPApp sendEvent:anEvent]; [self assert:0 equals:[CPEvent modifierFlags] message:@"shift key released"]; diff --git a/Tests/AppKit/CPMenuTest.j b/Tests/AppKit/CPMenuTest.j index 6449b65aa..b50afdea3 100644 --- a/Tests/AppKit/CPMenuTest.j +++ b/Tests/AppKit/CPMenuTest.j @@ -149,24 +149,24 @@ // Don't match anything. [menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask timestamp:0 windowNumber:0 context:nil - characters:"b" charactersIgnoringModifiers:"b" isARepeat:NO keyCode:0]]; + characters:"b" charactersIgnoringModifiers:"b" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled || undoWasCalled]; [menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil - characters:"o" charactersIgnoringModifiers:"o" isARepeat:NO keyCode:0]]; + characters:"o" charactersIgnoringModifiers:"o" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled || undoWasCalled]; [menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask timestamp:0 windowNumber:0 context:nil - characters:"o" charactersIgnoringModifiers:"o" isARepeat:NO keyCode:0]]; + characters:"o" charactersIgnoringModifiers:"o" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || undoWasCalled]; [self assertTrue:openDocumentWasCalled message:"expect openDocumentWasCalled"]; openDocumentWasCalled = NO; [menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask timestamp:0 windowNumber:0 context:nil - characters:CPUndoKeyEquivalent charactersIgnoringModifiers:CPUndoKeyEquivalent isARepeat:NO keyCode:0]]; + characters:CPUndoKeyEquivalent charactersIgnoringModifiers:CPUndoKeyEquivalent isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled]; [self assertTrue:undoWasCalled]; } @@ -177,7 +177,7 @@ [menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil - characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0]]; + characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:escapeWasCalled || openDocumentWasCalled || undoWasCalled]; [self assertTrue:escapeNoModifierWasCalled]; @@ -185,7 +185,7 @@ [menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask timestamp:0 windowNumber:0 context:nil - characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0]]; + characters:CPEscapeFunctionKey charactersIgnoringModifiers:CPEscapeFunctionKey isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:escapeNoModifierWasCalled || openDocumentWasCalled || undoWasCalled]; [self assertTrue:escapeWasCalled]; } @@ -196,7 +196,7 @@ [menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask timestamp:0 windowNumber:0 context:nil - characters:@"s" charactersIgnoringModifiers:@"s" isARepeat:NO keyCode:0]]; + characters:@"s" charactersIgnoringModifiers:@"s" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled || saveDocumentAsWasCalled || undoWasCalled]; [self assertTrue:saveDocumentWasCalled message:"saveDocumentWasCalled"]; @@ -204,7 +204,7 @@ [menu performKeyEquivalent:[CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPPlatformActionKeyMask | CPShiftKeyMask timestamp:0 windowNumber:0 context:nil - characters:@"s" charactersIgnoringModifiers:@"s" isARepeat:NO keyCode:0]]; + characters:@"s" charactersIgnoringModifiers:@"s" isARepeat:NO keyCode:0 isActionKey:NO]]; [self assertFalse:escapeWasCalled || escapeNoModifierWasCalled || openDocumentWasCalled || saveDocumentWasCalled || undoWasCalled]; [self assertTrue:saveDocumentAsWasCalled message:"saveDocumentAsWasCalled"]; } diff --git a/Tests/AppKit/CPResponderTest.j b/Tests/AppKit/CPResponderTest.j index 4c3159ab3..2aba5adc3 100644 --- a/Tests/AppKit/CPResponderTest.j +++ b/Tests/AppKit/CPResponderTest.j @@ -45,7 +45,7 @@ var keyEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:0 timestamp:0 windowNumber:0 context:nil - characters:character charactersIgnoringModifiers:character isARepeat:NO keyCode:keyCode]; + characters:character charactersIgnoringModifiers:character isARepeat:NO keyCode:keyCode isActionKey:YES]; [responder interpretKeyEvents:[keyEvent]]; [self assert:[selector] equals:responder.doCommandCalls]; } @@ -57,7 +57,7 @@ var keyEvent = [CPEvent keyEventWithType:CPKeyDown location:CGPointMakeZero() modifierFlags:CPShiftKeyMask timestamp:0 windowNumber:0 context:nil - characters:CPLeftArrowFunctionKey charactersIgnoringModifiers:CPLeftArrowFunctionKey isARepeat:NO keyCode:CPKeyCodes.LEFT]; + characters:CPLeftArrowFunctionKey charactersIgnoringModifiers:CPLeftArrowFunctionKey isARepeat:NO keyCode:CPKeyCodes.LEFT isActionKey:YES]; [responder interpretKeyEvents:[keyEvent]]; [self assert:[@selector(moveLeftAndModifySelection:)] equals:responder.doCommandCalls]; }