chromium/remoting/ios/client_gestures.mm

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#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 ()<UIGestureRecognizerDelegate> {
 @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<remoting::GestureInterpreter> _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