[iOS] Hardware Keyboard: All combinations of arrow keys and space key do not scroll...
authorenrica@apple.com <enrica@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 29 Jun 2015 17:43:46 +0000 (17:43 +0000)
committerenrica@apple.com <enrica@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 29 Jun 2015 17:43:46 +0000 (17:43 +0000)
https://bugs.webkit.org/show_bug.cgi?id=146290
rdar://problem/18466015

We don't normally get called by the keyboard to handle the event if we are not
interacting with editable content. In order to receive all the hardware keyboard events
we need to implement _handleKeyUIEvent which is called for every key event
when the view is first responder. This gives us the opportunity to send each keystroke
to the WebProcess to let any Javascript handler intercept it and then perform the default
action for the key combination.
Unfortunately this mechanism does not provide key repeat, which is implemented in the
keyboard layer. In order to have it at least for the arrow keys, we use the keyCommands
mechanism only for those.

Reviewed by Darin Adler.

* Platform/spi/ios/UIKitSPI.h:
* UIProcess/API/Cocoa/WKWebView.mm:
(-[WKWebView _scrollByOffset:]):
* UIProcess/API/Cocoa/WKWebViewInternal.h:
* UIProcess/ios/WKContentViewInteraction.mm:
(-[WKContentView canPerformAction:withSender:]):
(-[WKContentView keyCommands]):
(-[WKContentView _arrowKey:]):
(-[WKContentView _handleKeyUIEvent:]):
(-[WKContentView handleKeyEvent:]):
(-[WKContentView handleKeyWebEvent:]):
(-[WKContentView _interpretKeyEvent:isCharEvent:]):

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@186073 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Source/WebKit2/ChangeLog
Source/WebKit2/Platform/spi/ios/UIKitSPI.h
Source/WebKit2/UIProcess/API/Cocoa/WKWebView.mm
Source/WebKit2/UIProcess/API/Cocoa/WKWebViewInternal.h
Source/WebKit2/UIProcess/ios/WKContentViewInteraction.mm

index c0a0845..827a26c 100644 (file)
@@ -1,3 +1,34 @@
+2015-06-29  Enrica Casucci  <enrica@apple.com>
+
+        [iOS] Hardware Keyboard: All combinations of arrow keys and space key do not scroll the view.
+        https://bugs.webkit.org/show_bug.cgi?id=146290
+        rdar://problem/18466015
+
+        We don't normally get called by the keyboard to handle the event if we are not
+        interacting with editable content. In order to receive all the hardware keyboard events
+        we need to implement _handleKeyUIEvent which is called for every key event
+        when the view is first responder. This gives us the opportunity to send each keystroke
+        to the WebProcess to let any Javascript handler intercept it and then perform the default
+        action for the key combination.
+        Unfortunately this mechanism does not provide key repeat, which is implemented in the
+        keyboard layer. In order to have it at least for the arrow keys, we use the keyCommands
+        mechanism only for those.
+
+        Reviewed by Darin Adler.
+
+        * Platform/spi/ios/UIKitSPI.h:
+        * UIProcess/API/Cocoa/WKWebView.mm:
+        (-[WKWebView _scrollByOffset:]):
+        * UIProcess/API/Cocoa/WKWebViewInternal.h:
+        * UIProcess/ios/WKContentViewInteraction.mm:
+        (-[WKContentView canPerformAction:withSender:]):
+        (-[WKContentView keyCommands]):
+        (-[WKContentView _arrowKey:]):
+        (-[WKContentView _handleKeyUIEvent:]):
+        (-[WKContentView handleKeyEvent:]):
+        (-[WKContentView handleKeyWebEvent:]):
+        (-[WKContentView _interpretKeyEvent:isCharEvent:]):
+
 2015-06-28  Dan Bernstein  <mitz@apple.com>
 
         Fixed the iOS 8.x build after r186066.
index d70d329..b364f5d 100644 (file)
@@ -48,6 +48,7 @@
 #import <UIKit/UIPickerContentView_Private.h>
 #import <UIKit/UIPickerView_Private.h>
 #import <UIKit/UIPresentationController_Private.h>
+#import <UIKit/UIResponder_Private.h>
 #import <UIKit/UIScrollView_Private.h>
 #import <UIKit/UIStringDrawing_Private.h>
 #import <UIKit/UITableViewCell_Private.h>
@@ -110,6 +111,26 @@ typedef NS_ENUM(NSInteger, UIDatePickerPrivateMode)  {
 @end
 
 typedef enum {
+    kUIKeyboardInputRepeat                 = 1 << 0,
+    kUIKeyboardInputPopupVariant           = 1 << 1,
+    kUIKeyboardInputMultitap               = 1 << 2,
+    kUIKeyboardInputSkipCandidateSelection = 1 << 3,
+    kUIKeyboardInputDeadKey                = 1 << 4,
+    kUIKeyboardInputModifierFlagsChanged   = 1 << 5,
+    kUIKeyboardInputFlick                  = 1 << 6,
+    kUIKeyboardInputPreProcessed           = 1 << 7,
+} UIKeyboardInputFlags;
+
+@interface UIEvent (Details)
+@property (nonatomic, readonly) UIKeyboardInputFlags _inputFlags;
+- (void *)_hidEvent;
+- (NSString *)_unmodifiedInput;
+- (NSString *)_modifiedInput;
+- (NSInteger)_modifierFlags;
+- (BOOL)_isKeyDown;
+@end
+
+typedef enum {
     UIFontTraitPlain = 0x00000000,
 } UIFontTrait;
 
@@ -129,6 +150,10 @@ typedef enum {
 - (id)initWithCGImage:(CGImageRef)CGImage imageOrientation:(UIImageOrientation)imageOrientation;
 @end
 
+@interface UIKeyCommand (Details)
+@property (nonatomic, readonly) UIEvent *_triggeringEvent;
+@end
+
 @protocol UIKeyboardImplGeometryDelegate
 @property (nonatomic, readwrite, getter=isMinimized) BOOL minimized;
 - (void)prepareForImplBoundsHeightChange:(CGFloat)endDelta suppressNotification:(BOOL)suppressNotification;
@@ -216,12 +241,17 @@ typedef enum {
 @property (nonatomic, setter=_setMagnifierEnabled:) BOOL _magnifierEnabled;
 @end
 
+@interface UIResponder (Details)
+- (void)_handleKeyUIEvent:(UIEvent *)event;
+@end
+
 @interface UIScrollView (Details)
 - (void)_stopScrollingAndZoomingAnimations;
 - (void)_zoomToCenter:(CGPoint)center scale:(CGFloat)scale duration:(CFTimeInterval)duration force:(BOOL)force;
 - (void)_zoomToCenter:(CGPoint)center scale:(CGFloat)scale duration:(CFTimeInterval)duration;
 @property (nonatomic, getter=isZoomEnabled) BOOL zoomEnabled;
 @property (nonatomic, readonly, getter=_isAnimatingZoom) BOOL isAnimatingZoom;
+@property (nonatomic, readonly, getter=_isAnimatingScroll) BOOL isAnimatingScroll;
 @property (nonatomic) CGFloat horizontalScrollDecelerationFactor;
 @property (nonatomic) CGFloat verticalScrollDecelerationFactor;
 @end
@@ -743,4 +773,7 @@ extern const float UIWebViewGrowsAndShrinksToFitHeight;
 extern const float UIWebViewScalesToFitScale;
 extern const float UIWebViewStandardViewportWidth;
 
+extern NSString *const UIKeyInputPageUp;
+extern NSString *const UIKeyInputPageDown;
+
 WTF_EXTERN_C_END
index ece894d..804b2d9 100644 (file)
 - (void)_adjustForAutomaticKeyboardInfo:(NSDictionary*)info animated:(BOOL)animated lastAdjustment:(CGFloat*)lastAdjustment;
 - (BOOL)_isScrollingToTop;
 - (BOOL)_isInterruptingDeceleration;
+- (CGPoint)_animatedTargetOffset;
 @end
 
 @interface UIPeripheralHost(UIKitInternal)
@@ -1197,6 +1198,18 @@ static WebCore::FloatPoint constrainContentOffset(WebCore::FloatPoint contentOff
     return true;
 }
 
+- (void)_scrollByOffset:(WebCore::FloatPoint)offset
+{
+    CGPoint currentOffset = ([_scrollView _isAnimatingScroll]) ? [_scrollView _animatedTargetOffset] : [_scrollView contentOffset];
+
+    CGPoint boundedOffset = contentOffsetBoundedInValidRange(_scrollView.get(), currentOffset + offset);
+    
+    if (CGPointEqualToPoint(boundedOffset, currentOffset))
+        return;
+    [_contentView willStartZoomOrScroll];
+    [_scrollView setContentOffset:boundedOffset animated:YES];
+}
+
 - (void)_zoomOutWithOrigin:(WebCore::FloatPoint)origin animated:(BOOL)animated
 {
     [self _zoomToPoint:origin atScale:[_scrollView minimumZoomScale] animated:animated];
index a3849ff..fc2f5f3 100644 (file)
@@ -82,6 +82,7 @@ struct PrintInfo;
 
 - (void)_scrollToContentOffset:(WebCore::FloatPoint)contentOffset;
 - (BOOL)_scrollToRect:(WebCore::FloatRect)targetRect origin:(WebCore::FloatPoint)origin minimumScrollDistance:(float)minimumScrollDistance;
+- (void)_scrollByOffset:(WebCore::FloatPoint)offset;
 - (void)_zoomToFocusRect:(WebCore::FloatRect)focusedElementRect selectionRect:(WebCore::FloatRect)selectionRectInDocumentCoordinates fontSize:(float)fontSize minimumScale:(double)minimumScale maximumScale:(double)maximumScale allowScaling:(BOOL)allowScaling forceScroll:(BOOL)forceScroll;
 - (BOOL)_zoomToRect:(WebCore::FloatRect)targetRect withOrigin:(WebCore::FloatPoint)origin fitEntireRect:(BOOL)fitEntireRect minimumScale:(double)minimumScale maximumScale:(double)maximumScale minimumScrollDistance:(float)minimumScrollDistance;
 - (void)_zoomOutWithOrigin:(WebCore::FloatPoint)origin animated:(BOOL)animated;
index 719dcf6..708ea8e 100644 (file)
 #import <WebCore/CoreGraphicsSPI.h>
 #import <WebCore/FloatQuad.h>
 #import <WebCore/Pasteboard.h>
+#import <WebCore/Scrollbar.h>
 #import <WebCore/SoftLinking.h>
 #import <WebCore/WebEvent.h>
 #import <WebKit/WebSelectionRect.h> // FIXME: WK2 should not include WebKit headers!
 #import <WebKitSystemInterfaceIOS.h>
 #import <wtf/RetainPtr.h>
 
+@interface UIEvent(UIEventInternal)
+@property (nonatomic, assign) UIKeyboardInputFlags _inputFlags;
+@end
+
 using namespace WebCore;
 using namespace WebKit;
 
@@ -1423,6 +1428,9 @@ static void cancelPotentialTapIfNecessary(WKContentView* contentView)
 {
     BOOL hasWebSelection = _webSelectionAssistant && !CGRectIsEmpty(_webSelectionAssistant.get().selectionFrame);
 
+    if (action == @selector(_arrowKey:))
+        return [self isFirstResponder];
+        
     if (action == @selector(_showTextStyleOptions:))
         return _page->editorState().isContentRichlyEditable && _page->editorState().selectionIsRange && !_showingTextStyleOptions;
     if (_showingTextStyleOptions)
@@ -2144,8 +2152,44 @@ static void selectionChangedWithTouch(WKContentView *view, const WebCore::IntPoi
 
 - (NSArray *)keyCommands
 {
-    return @[[UIKeyCommand keyCommandWithInput:@"\t" modifierFlags:0 action:@selector(_nextAccessoryTab:)],
-             [UIKeyCommand keyCommandWithInput:@"\t" modifierFlags:UIKeyModifierShift action:@selector(_prevAccessoryTab:)]];
+    static NSArray* nonEditableKeyCommands = [@[
+       [UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow modifierFlags:0 action:@selector(_arrowKey:)],
+       [UIKeyCommand keyCommandWithInput:UIKeyInputDownArrow modifierFlags:0 action:@selector(_arrowKey:)],
+       [UIKeyCommand keyCommandWithInput:UIKeyInputLeftArrow modifierFlags:0 action:@selector(_arrowKey:)],
+       [UIKeyCommand keyCommandWithInput:UIKeyInputRightArrow modifierFlags:0 action:@selector(_arrowKey:)],
+       
+       [UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow modifierFlags:UIKeyModifierCommand action:@selector(_arrowKey:)],
+       [UIKeyCommand keyCommandWithInput:UIKeyInputDownArrow modifierFlags:UIKeyModifierCommand action:@selector(_arrowKey:)],
+       
+       [UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow modifierFlags:UIKeyModifierShift action:@selector(_arrowKey:)],
+       [UIKeyCommand keyCommandWithInput:UIKeyInputDownArrow modifierFlags:UIKeyModifierShift action:@selector(_arrowKey:)],
+       [UIKeyCommand keyCommandWithInput:UIKeyInputLeftArrow modifierFlags:UIKeyModifierShift action:@selector(_arrowKey:)],
+       [UIKeyCommand keyCommandWithInput:UIKeyInputRightArrow modifierFlags:UIKeyModifierShift action:@selector(_arrowKey:)],
+       
+       [UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow modifierFlags:UIKeyModifierAlternate action:@selector(_arrowKey:)],
+       [UIKeyCommand keyCommandWithInput:UIKeyInputDownArrow modifierFlags:UIKeyModifierAlternate action:@selector(_arrowKey:)],
+       [UIKeyCommand keyCommandWithInput:UIKeyInputLeftArrow modifierFlags:UIKeyModifierAlternate action:@selector(_arrowKey:)],
+       [UIKeyCommand keyCommandWithInput:UIKeyInputRightArrow modifierFlags:UIKeyModifierAlternate action:@selector(_arrowKey:)],
+       
+       [UIKeyCommand keyCommandWithInput:@" " modifierFlags:0 action:@selector(_arrowKey:)],
+       [UIKeyCommand keyCommandWithInput:@" " modifierFlags:UIKeyModifierShift action:@selector(_arrowKey:)],
+       
+       [UIKeyCommand keyCommandWithInput:UIKeyInputPageDown modifierFlags:0 action:@selector(_arrowKey:)],
+       [UIKeyCommand keyCommandWithInput:UIKeyInputPageDown modifierFlags:0 action:@selector(_arrowKey:)],
+    ] retain];
+
+    static NSArray* editableKeyCommands = [@[
+       [UIKeyCommand keyCommandWithInput:@"\t" modifierFlags:0 action:@selector(_nextAccessoryTab:)],
+       [UIKeyCommand keyCommandWithInput:@"\t" modifierFlags:UIKeyModifierShift action:@selector(_prevAccessoryTab:)]
+    ] retain];
+    
+    return (_page->editorState().isContentEditable) ? editableKeyCommands : nonEditableKeyCommands;
+}
+
+- (void)_arrowKey:(id)sender
+{
+    UIKeyCommand* command = sender;
+    [self handleKeyEvent:command._triggeringEvent];
 }
 
 - (void)_nextAccessoryTab:(id)sender
@@ -2543,6 +2587,32 @@ static UITextAutocapitalizationType toUITextAutocapitalize(WebAutocapitalizeType
     return YES;
 }
 
+- (void)_handleKeyUIEvent:(::UIEvent *)event
+{
+    // We only want to handle key event from the hardware keyboard when we are
+    // first responder and we are not interacting with editable content.
+    if ([self isFirstResponder] && event._hidEvent && !_page->editorState().isContentEditable)
+        [self handleKeyEvent:event];
+
+    [super _handleKeyUIEvent:event];
+}
+
+- (void)handleKeyEvent:(::UIEvent *)event
+{
+    ::WebEvent *webEvent = [[[::WebEvent alloc] initWithKeyEventType:(event._isKeyDown) ? WebEventKeyDown : WebEventKeyUp
+                                                           timeStamp:event.timestamp
+                                                          characters:event._modifiedInput
+                                         charactersIgnoringModifiers:event._unmodifiedInput
+                                                           modifiers:event._modifierFlags
+                                                         isRepeating:(event._inputFlags & kUIKeyboardInputRepeat)
+                                                           withFlags:event._inputFlags
+                                                             keyCode:0
+                                                            isTabKey:[event._modifiedInput isEqualToString:@"\t"]
+                                                        characterSet:WebEventCharacterSetUnicode] autorelease];
+    
+    [self handleKeyWebEvent:webEvent];    
+}
+
 - (void)handleKeyWebEvent:(WebIOSEvent *)theEvent
 {
     _page->handleKeyboardEvent(NativeWebKeyboardEvent(theEvent));
@@ -2565,88 +2635,96 @@ static UITextAutocapitalizationType toUITextAutocapitalize(WebAutocapitalizeType
     static const unsigned kWebBackspaceKey = 0x0008;
     static const unsigned kWebReturnKey = 0x000D;
     static const unsigned kWebDeleteKey = 0x007F;
-    static const unsigned kWebLeftArrowKey = 0x00AC;
-    static const unsigned kWebUpArrowKey = 0x00AD;
-    static const unsigned kWebRightArrowKey = 0x00AE;
-    static const unsigned kWebDownArrowKey = 0x00AF;
     static const unsigned kWebDeleteForwardKey = 0xF728;
+    static const unsigned kWebSpaceKey = 0x20;
 
     if (!_page->editorState().isContentEditable && event.isTabKey)
         return NO;
 
     BOOL shift = event.modifierFlags & WebEventFlagMaskShift;
+    BOOL command = event.modifierFlags & WebEventFlagMaskCommand;
+    BOOL option = event.modifierFlags & WebEventFlagMaskAlternate;
+    NSString *charactersIgnoringModifiers = [event charactersIgnoringModifiers];
+    BOOL shouldScroll = YES;
+    FloatPoint scrollOffset;
+
+    if ([charactersIgnoringModifiers isEqualToString:UIKeyInputLeftArrow])
+        scrollOffset.setX(-Scrollbar::pixelsPerLineStep());
+    else if ([charactersIgnoringModifiers isEqualToString:UIKeyInputUpArrow]) {
+        if (option)
+            scrollOffset.setY(-_page->unobscuredContentRect().height());
+        else if (command)
+            scrollOffset.setY(-[self bounds].size.height);
+        else
+            scrollOffset.setY(-Scrollbar::pixelsPerLineStep());
+    } else if ([charactersIgnoringModifiers isEqualToString:UIKeyInputRightArrow])
+            scrollOffset.setX(Scrollbar::pixelsPerLineStep());
+    else if ([charactersIgnoringModifiers isEqualToString:UIKeyInputDownArrow]) {
+        if (option)
+            scrollOffset.setY(_page->unobscuredContentRect().height());
+        else if (command)
+            scrollOffset.setY([self bounds].size.height);
+        else
+            scrollOffset.setY(Scrollbar::pixelsPerLineStep());
+    } else if ([charactersIgnoringModifiers isEqualToString:UIKeyInputPageDown])
+        scrollOffset.setY(_page->unobscuredContentRect().height());
+    else if ([charactersIgnoringModifiers isEqualToString:UIKeyInputPageUp])
+        scrollOffset.setY(-_page->unobscuredContentRect().height());
+    else
+        shouldScroll = NO;
 
-    switch (event.characterSet) {
-    case WebEventCharacterSetSymbol: {
-        String command;
-        NSString *characters = [event charactersIgnoringModifiers];
-        if ([characters length] == 0)
-            break;
-        switch ([characters characterAtIndex:0]) {
-        case kWebLeftArrowKey:
-            command = shift ? ASCIILiteral("moveLeftAndModifySelection") :  ASCIILiteral("moveLeft");
-            break;
+    if (shouldScroll) {
+        [_webView _scrollByOffset:scrollOffset];
+        return YES;
+    }
 
-        case kWebUpArrowKey:
-            command = shift ? ASCIILiteral("moveUpAndModifySelection") :  ASCIILiteral("moveUp");
-            break;
+    UIKeyboardImpl *keyboard = [UIKeyboardImpl sharedInstance];
+    NSString *characters = [event characters];
+    
+    if (![characters length])
+        return NO;
 
-        case kWebRightArrowKey:
-            command = shift ? ASCIILiteral("moveRightAndModifySelection") :  ASCIILiteral("moveRight");
-            break;
+    switch ([characters characterAtIndex:0]) {
+    case kWebBackspaceKey:
+    case kWebDeleteKey:
+        // FIXME: remove deleteFromInput once UIKit adopts deleteFromInputWithFlags
+        if ([keyboard respondsToSelector:@selector(deleteFromInputWithFlags:)])
+            [keyboard deleteFromInputWithFlags:event.keyboardFlags];
+        else
+            [keyboard deleteFromInput];
+        return YES;
 
-        case kWebDownArrowKey:
-            command = shift ? ASCIILiteral("moveDownAndModifySelection") :  ASCIILiteral("moveDown");
-            break;
+    case kWebSpaceKey:
+        if (!_page->editorState().isContentEditable) {
+            [_webView _scrollByOffset:FloatPoint(0, shift ? -_page->unobscuredContentRect().height() : _page->unobscuredContentRect().height())];
+            return YES;
         }
-        if (!command.isEmpty()) {
-            _page->executeEditCommand(command);
+        if (isCharEvent) {
+            [keyboard addInputString:event.characters withFlags:event.keyboardFlags];
             return YES;
         }
         break;
-    }
-    case WebEventCharacterSetASCII:
-    case WebEventCharacterSetUnicode: {
-        NSString *characters = [event characters];
-        if ([characters length] == 0)
-            break;
-        UIKeyboardImpl *keyboard = [UIKeyboardImpl sharedInstance];
-        switch ([characters characterAtIndex:0]) {
-        case kWebBackspaceKey:
-        case kWebDeleteKey:
-            // FIXME: remove deleteFromInput once UIKit adopts deleteFromInputWithFlags
-            if ([keyboard respondsToSelector:@selector(deleteFromInputWithFlags:)])
-                [keyboard deleteFromInputWithFlags:event.keyboardFlags];
-            else
-                [keyboard deleteFromInput];
+
+    case kWebEnterKey:
+    case kWebReturnKey:
+        if (isCharEvent) {
+            // Map \r from HW keyboard to \n to match the behavior of the soft keyboard.
+            [keyboard addInputString:@"\n" withFlags:0];
             return YES;
+        }
+        break;
 
-        case kWebEnterKey:
-        case kWebReturnKey:
-            if (isCharEvent) {
-                // Map \r from HW keyboard to \n to match the behavior of the soft keyboard.
-                [keyboard addInputString:@"\n" withFlags:0];
-                return YES;
-            }
-            return NO;
+    case kWebDeleteForwardKey:
+        _page->executeEditCommand(ASCIILiteral("deleteForward"));
+        return YES;
 
-        case kWebDeleteForwardKey:
-            _page->executeEditCommand(ASCIILiteral("deleteForward"));
+    default:
+        if (isCharEvent) {
+            [keyboard addInputString:event.characters withFlags:event.keyboardFlags];
             return YES;
-
-        default: {
-            if (isCharEvent) {
-                [keyboard addInputString:event.characters withFlags:event.keyboardFlags];
-                return YES;
-            }
-            return NO;
         }
-    }
         break;
     }
-    default:
-        return NO;
-    }
 
     return NO;
 }