// Copyright 2017 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #if !defined(__has_feature) || !__has_feature(objc_arc) #error "This file requires ARC support." #endif #import "remoting/ios/client_gestures.h" #import "remoting/ios/session/remoting_client.h" #include "remoting/client/gesture_interpreter.h" static remoting::GestureInterpreter::GestureState toGestureState( UIGestureRecognizerState state) { switch (state) { case UIGestureRecognizerStateBegan: return remoting::GestureInterpreter::GESTURE_BEGAN; case UIGestureRecognizerStateChanged: return remoting::GestureInterpreter::GESTURE_CHANGED; default: return remoting::GestureInterpreter::GESTURE_ENDED; } } @interface ClientGestures () { @private UILongPressGestureRecognizer* _longPressRecognizer; UIPanGestureRecognizer* _panRecognizer; UIPanGestureRecognizer* _flingRecognizer; UIPanGestureRecognizer* _scrollRecognizer; // TODO(yuweih): Commented out because this makes two-finger gestures not // quite responsive. Clean these up if it's really unnecessary. // UIPanGestureRecognizer* _threeFingerPanRecognizer; UIPinchGestureRecognizer* _pinchRecognizer; UITapGestureRecognizer* _singleTapRecognizer; UITapGestureRecognizer* _twoFingerTapRecognizer; UITapGestureRecognizer* _threeFingerTapRecognizer; UITapGestureRecognizer* _fourFingerTapRecognizer; __weak UIView* _view; base::WeakPtr _gestureInterpreter; } @end @implementation ClientGestures @synthesize delegate = _delegate; - (instancetype)initWithView:(UIView*)view client:(RemotingClient*)client { _view = view; _gestureInterpreter = client.gestureInterpreter->GetWeakPtr(); _longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressGestureTriggered:)]; _longPressRecognizer.delegate = self; [view addGestureRecognizer:_longPressRecognizer]; _panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureTriggered:)]; _panRecognizer.minimumNumberOfTouches = 1; _panRecognizer.maximumNumberOfTouches = 2; _panRecognizer.delegate = self; [view addGestureRecognizer:_panRecognizer]; _flingRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(flingGestureTriggered:)]; _flingRecognizer.minimumNumberOfTouches = 1; _flingRecognizer.maximumNumberOfTouches = 1; _flingRecognizer.delegate = self; [view addGestureRecognizer:_flingRecognizer]; _scrollRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(scrollGestureTriggered:)]; _scrollRecognizer.minimumNumberOfTouches = 2; _scrollRecognizer.maximumNumberOfTouches = 2; _scrollRecognizer.delegate = self; [view addGestureRecognizer:_scrollRecognizer]; // _threeFingerPanRecognizer = [[UIPanGestureRecognizer alloc] // initWithTarget:self // action:@selector(threeFingerPanGestureTriggered:)]; // _threeFingerPanRecognizer.minimumNumberOfTouches = 3; // _threeFingerPanRecognizer.maximumNumberOfTouches = 3; // _threeFingerPanRecognizer.delegate = self; // [view addGestureRecognizer:_threeFingerPanRecognizer]; _pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchGestureTriggered:)]; _pinchRecognizer.delegate = self; [view addGestureRecognizer:_pinchRecognizer]; _singleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGestureTriggered:)]; _singleTapRecognizer.delegate = self; [view addGestureRecognizer:_singleTapRecognizer]; _twoFingerTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(twoFingerTapGestureTriggered:)]; _twoFingerTapRecognizer.numberOfTouchesRequired = 2; _twoFingerTapRecognizer.delegate = self; [view addGestureRecognizer:_twoFingerTapRecognizer]; _threeFingerTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(threeFingerTapGestureTriggered:)]; _threeFingerTapRecognizer.numberOfTouchesRequired = 3; _threeFingerTapRecognizer.delegate = self; [view addGestureRecognizer:_threeFingerTapRecognizer]; _fourFingerTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(fourFingerTapGestureTriggered:)]; _fourFingerTapRecognizer.numberOfTouchesRequired = 4; _fourFingerTapRecognizer.delegate = self; [view addGestureRecognizer:_fourFingerTapRecognizer]; [_singleTapRecognizer requireGestureRecognizerToFail:_twoFingerTapRecognizer]; [_twoFingerTapRecognizer requireGestureRecognizerToFail:_threeFingerTapRecognizer]; [_pinchRecognizer requireGestureRecognizerToFail:_singleTapRecognizer]; // [_pinchRecognizer // requireGestureRecognizerToFail:_threeFingerPanRecognizer]; [_panRecognizer requireGestureRecognizerToFail:_singleTapRecognizer]; // [_threeFingerPanRecognizer // requireGestureRecognizerToFail:_threeFingerTapRecognizer]; [_panRecognizer requireGestureRecognizerToFail:_scrollRecognizer]; return self; } // Resize the view of the desktop - Zoom in/out. This can occur during a Pan. - (IBAction)pinchGestureTriggered:(UIPinchGestureRecognizer*)sender { if (!_gestureInterpreter) { return; } CGPoint pivot = [sender locationInView:_view]; _gestureInterpreter->Zoom(pivot.x, pivot.y, sender.scale, toGestureState([sender state])); sender.scale = 1.0; // reset scale so next iteration is a relative ratio } - (IBAction)tapGestureTriggered:(UITapGestureRecognizer*)sender { if (!_gestureInterpreter) { return; } CGPoint touchPoint = [sender locationInView:_view]; _gestureInterpreter->Tap(touchPoint.x, touchPoint.y); } // Change position of the viewport. This can occur during a pinch or long press. - (IBAction)panGestureTriggered:(UIPanGestureRecognizer*)sender { if (!_gestureInterpreter) { return; } if ([sender state] == UIGestureRecognizerStateChanged) { CGPoint translation = [sender translationInView:_view]; _gestureInterpreter->Pan(translation.x, translation.y); // Reset translation so next iteration is relative [sender setTranslation:CGPointZero inView:_view]; } } // Do fling on the viewport. This will happen at the end of the one-finger // panning. - (IBAction)flingGestureTriggered:(UIPanGestureRecognizer*)sender { if (!_gestureInterpreter) { return; } if ([sender state] == UIGestureRecognizerStateEnded) { CGPoint velocity = [sender velocityInView:_view]; if (velocity.x != 0 || velocity.y != 0) { _gestureInterpreter->OneFingerFling(velocity.x, velocity.y); } } } // Handles the two finger scrolling gesture. - (IBAction)scrollGestureTriggered:(UIPanGestureRecognizer*)sender { if (!_gestureInterpreter) { return; } if ([sender state] == UIGestureRecognizerStateEnded) { CGPoint velocity = [sender velocityInView:_view]; _gestureInterpreter->ScrollWithVelocity(velocity.x, velocity.y); return; } CGPoint scrollPoint = [sender locationInView:_view]; CGPoint translation = [sender translationInView:_view]; _gestureInterpreter->Scroll(scrollPoint.x, scrollPoint.y, translation.x, translation.y); // Reset translation so next iteration is relative [sender setTranslation:CGPointZero inView:_view]; } // Click-Drag mouse operation. This can occur during a Pan. - (IBAction)longPressGestureTriggered:(UILongPressGestureRecognizer*)sender { if (!_gestureInterpreter) { return; } CGPoint touchPoint = [sender locationInView:_view]; _gestureInterpreter->Drag(touchPoint.x, touchPoint.y, toGestureState([sender state])); } - (IBAction)twoFingerTapGestureTriggered:(UITapGestureRecognizer*)sender { if (!_gestureInterpreter) { return; } CGPoint touchPoint = [sender locationInView:_view]; _gestureInterpreter->TwoFingerTap(touchPoint.x, touchPoint.y); } - (IBAction)threeFingerTapGestureTriggered:(UITapGestureRecognizer*)sender { if (!_gestureInterpreter) { return; } CGPoint touchPoint = [sender locationInView:_view]; _gestureInterpreter->ThreeFingerTap(touchPoint.x, touchPoint.y); } - (IBAction)threeFingerPanGestureTriggered:(UIPanGestureRecognizer*)sender { if (!_gestureInterpreter) { return; } if ([sender state] != UIGestureRecognizerStateEnded) { return; } CGPoint translation = [sender translationInView:_view]; if (translation.y > 0) { // Swiped down - hide keyboard (for now) [_delegate keyboardShouldHide]; } else if (translation.y < 0) { // Swiped up - show keyboard [_delegate keyboardShouldShow]; } } // To trigger the menu. - (IBAction)fourFingerTapGestureTriggered: (UILongPressGestureRecognizer*)sender { if (!_gestureInterpreter) { return; } [_delegate menuShouldShow]; } #pragma mark - UIGestureRecognizerDelegate // Allow panning and zooming to occur simultaneously. // Allow panning and long press to occur simultaneously. // Pinch requires 2 touches, and long press requires a single touch, so they are // mutually exclusive regardless of if panning is the initiating gesture. // Pinch and Scroll are both two-finger gestures. They are mutually exclusive // and whatever comes first should disable the other gesture recognizer. - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer: (UIGestureRecognizer*)otherGestureRecognizer { if (gestureRecognizer == _pinchRecognizer || (gestureRecognizer == _panRecognizer)) { if (otherGestureRecognizer == _pinchRecognizer || otherGestureRecognizer == _panRecognizer) { return YES; } } if (gestureRecognizer == _flingRecognizer || (gestureRecognizer == _panRecognizer)) { if (otherGestureRecognizer == _flingRecognizer || otherGestureRecognizer == _panRecognizer) { return YES; } } if (gestureRecognizer == _longPressRecognizer || gestureRecognizer == _panRecognizer) { if (otherGestureRecognizer == _longPressRecognizer || otherGestureRecognizer == _panRecognizer) { return YES; } } if (gestureRecognizer == _twoFingerTapRecognizer && otherGestureRecognizer == _longPressRecognizer) { return YES; } // TODO(nicholss): If we return NO here, it dismisses the other recognizers. // As we add more types of recognizers, they need to be accounted for in the // above logic. return NO; } @end