diff --git a/Platform/iOS/Display/VMDisplayMetalViewController+Private.h b/Platform/iOS/Display/VMDisplayMetalViewController+Private.h index 51e58e7d1b..2ed6df65ba 100644 --- a/Platform/iOS/Display/VMDisplayMetalViewController+Private.h +++ b/Platform/iOS/Display/VMDisplayMetalViewController+Private.h @@ -44,18 +44,45 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, nullable) VMScroll *scroll; // Gestures -@property (nonatomic, nullable) UISwipeGestureRecognizer *swipeUp; -@property (nonatomic, nullable) UISwipeGestureRecognizer *swipeDown; @property (nonatomic, nullable) UISwipeGestureRecognizer *swipeScrollUp; @property (nonatomic, nullable) UISwipeGestureRecognizer *swipeScrollDown; @property (nonatomic, nullable) UIPanGestureRecognizer *pan; @property (nonatomic, nullable) UIPanGestureRecognizer *twoPan; @property (nonatomic, nullable) UIPanGestureRecognizer *threePan; +@property (nonatomic, nullable) UIPinchGestureRecognizer *pinch; @property (nonatomic, nullable) UITapGestureRecognizer *tap; @property (nonatomic, nullable) UITapGestureRecognizer *tapPencil; @property (nonatomic, nullable) UITapGestureRecognizer *twoTap; +@property (nonatomic, nullable) UITapGestureRecognizer *threeTap; @property (nonatomic, nullable) UILongPressGestureRecognizer *longPress; -@property (nonatomic, nullable) UIPinchGestureRecognizer *pinch; +@property (nonatomic, nullable) UITouch *multitouchPrimaryTouch; +@property (nonatomic) BOOL multitouchTwoPanConsumed; +@property (nonatomic) BOOL multitouchThreePanConsumed; +@property (nonatomic) BOOL multitouchTwoPanActionStarted; +@property (nonatomic) BOOL multitouchThreePanActionStarted; +@property (nonatomic) BOOL multitouchTwoSwipeDecided; +@property (nonatomic) BOOL multitouchThreeSwipeDecided; +@property (nonatomic) BOOL multitouchTwoSwipeCandidate; +@property (nonatomic) BOOL multitouchThreeSwipeCandidate; +@property (nonatomic) CGPoint multitouchTwoPanLastVelocity; +@property (nonatomic) CGPoint multitouchThreePanLastVelocity; +@property (nonatomic) NSTimeInterval multitouchTwoPanLastTime; +@property (nonatomic) NSTimeInterval multitouchThreePanLastTime; +@property (nonatomic) NSTimeInterval multitouchTwoPanBeginTime; +@property (nonatomic) NSTimeInterval multitouchThreePanBeginTime; +@property (nonatomic) BOOL multitouchPinchActive; +@property (nonatomic) CGFloat multitouchPinchInitialDistance; +@property (nonatomic) NSUInteger multitouchActiveDirectTouchCount; +@property (nonatomic) BOOL multitouchLongPressRecognized; +@property (nonatomic) BOOL multitouchLongPressPending; +@property (nonatomic) BOOL multitouchLongPressDragging; +@property (nonatomic) BOOL multitouchLongPressTouchActive; +@property (nonatomic) BOOL multitouchLongPressCancelledByMovement; +@property (nonatomic) CGPoint multitouchLongPressOrigin; +@property (nonatomic) CGPoint multitouchPrimaryTouchLocation; +@property (nonatomic) CGPoint multitouchScrollLastLocation; +@property (nonatomic) CGPoint multitouchScrollVelocity; +@property (nonatomic) NSTimeInterval multitouchScrollLastTime; //Gamepad @property (nonatomic, nullable) GCController *controller; diff --git a/Platform/iOS/Display/VMDisplayMetalViewController+Touch.h b/Platform/iOS/Display/VMDisplayMetalViewController+Touch.h index 2fdd92d664..8831d547d2 100644 --- a/Platform/iOS/Display/VMDisplayMetalViewController+Touch.h +++ b/Platform/iOS/Display/VMDisplayMetalViewController+Touch.h @@ -23,6 +23,10 @@ typedef NS_ENUM(NSInteger, VMGestureType) { VMGestureTypeRightClick, VMGestureTypeMoveScreen, VMGestureTypeMouseWheel, + VMGestureTypeMiddleClick, + VMGestureTypeRightDrag, + VMGestureTypeMiddleDrag, + VMGestureTypeScaleDisplay, VMGestureTypeMax }; @@ -30,6 +34,7 @@ typedef NS_ENUM(NSInteger, VMMouseType) { VMMouseTypeRelative, VMMouseTypeAbsolute, VMMouseTypeAbsoluteHideCursor, + VMMouseTypeMultitouch, VMMouseTypeMax }; diff --git a/Platform/iOS/Display/VMDisplayMetalViewController+Touch.m b/Platform/iOS/Display/VMDisplayMetalViewController+Touch.m index 8d970b305a..877b230bfd 100644 --- a/Platform/iOS/Display/VMDisplayMetalViewController+Touch.m +++ b/Platform/iOS/Display/VMDisplayMetalViewController+Touch.m @@ -26,10 +26,18 @@ #import "UTMSpiceIO.h" #import "UTMLogging.h" #import "UTM-Swift.h" +#import const CGFloat kScrollSpeedReduction = 100.0f; const CGFloat kCursorResistance = 50.0f; const CGFloat kScrollResistance = 10.0f; +const CGFloat kMultitouchDragThreshold = 8.0f; +const CGFloat kMultitouchPanStartDistance = 12.0f; +const CGFloat kMultitouchSwipeDistance = 60.0f; +const CGFloat kMultitouchSwipeVelocity = 300.0f; +const CGFloat kMultitouchSwipeAcceleration = 3000.0f; +const NSTimeInterval kMultitouchSwipeCandidateWindow = 0.05; +const CGFloat kMultitouchPinchStartDistance = 16.0f; @implementation VMDisplayMetalViewController (Gestures) @@ -53,14 +61,6 @@ - (void)initTouch { [self.mtkView addGestureRecognizer:self.tap]; #else // Set up gesture recognizers because Storyboards is BROKEN and doing it there crashes! - self.swipeUp = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(gestureSwipeUp:)]; - self.swipeUp.numberOfTouchesRequired = 3; - self.swipeUp.direction = UISwipeGestureRecognizerDirectionUp; - self.swipeUp.delegate = self; - self.swipeDown = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(gestureSwipeDown:)]; - self.swipeDown.numberOfTouchesRequired = 3; - self.swipeDown.direction = UISwipeGestureRecognizerDirectionDown; - self.swipeDown.delegate = self; self.swipeScrollUp = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(gestureSwipeScroll:)]; self.swipeScrollUp.numberOfTouchesRequired = 2; self.swipeScrollUp.direction = UISwipeGestureRecognizerDirectionUp; @@ -78,10 +78,12 @@ - (void)initTouch { self.twoPan.minimumNumberOfTouches = 2; self.twoPan.maximumNumberOfTouches = 2; self.twoPan.delegate = self; + self.twoPan.cancelsTouchesInView = NO; self.threePan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(gestureThreePan:)]; self.threePan.minimumNumberOfTouches = 3; self.threePan.maximumNumberOfTouches = 3; self.threePan.delegate = self; + self.threePan.cancelsTouchesInView = NO; self.tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(gestureTap:)]; self.tap.delegate = self; self.tap.allowedTouchTypes = @[ @(UITouchTypeDirect) ]; @@ -90,13 +92,22 @@ - (void)initTouch { self.twoTap.numberOfTouchesRequired = 2; self.twoTap.delegate = self; self.twoTap.allowedTouchTypes = @[ @(UITouchTypeDirect) ]; + self.twoTap.cancelsTouchesInView = NO; + self.threeTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(gestureThreeTap:)]; + self.threeTap.numberOfTouchesRequired = 3; + self.threeTap.delegate = self; + self.threeTap.allowedTouchTypes = @[ @(UITouchTypeDirect) ]; + self.threeTap.cancelsTouchesInView = NO; self.longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(gestureLongPress:)]; self.longPress.delegate = self; self.longPress.allowedTouchTypes = @[ @(UITouchTypeDirect) ]; + self.longPress.cancelsTouchesInView = NO; self.pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(gesturePinch:)]; self.pinch.delegate = self; - [self.mtkView addGestureRecognizer:self.swipeUp]; - [self.mtkView addGestureRecognizer:self.swipeDown]; + self.pinch.cancelsTouchesInView = NO; + [self.tap requireGestureRecognizerToFail:self.longPress]; + [self.twoTap requireGestureRecognizerToFail:self.twoPan]; + [self.threeTap requireGestureRecognizerToFail:self.threePan]; [self.mtkView addGestureRecognizer:self.swipeScrollUp]; [self.mtkView addGestureRecognizer:self.swipeScrollDown]; [self.mtkView addGestureRecognizer:self.pan]; @@ -104,6 +115,7 @@ - (void)initTouch { [self.mtkView addGestureRecognizer:self.threePan]; [self.mtkView addGestureRecognizer:self.tap]; [self.mtkView addGestureRecognizer:self.twoTap]; + [self.mtkView addGestureRecognizer:self.threeTap]; [self.mtkView addGestureRecognizer:self.longPress]; [self.mtkView addGestureRecognizer:self.pinch]; @@ -153,6 +165,10 @@ - (VMGestureType)longPressType { return [self gestureTypeForSetting:@"GestureLongPress"]; } +- (VMGestureType)longPressDragType { + return [self gestureTypeForSetting:@"GestureLongPressDrag"]; +} + - (VMGestureType)twoFingerTapType { return [self gestureTypeForSetting:@"GestureTwoTap"]; } @@ -165,10 +181,105 @@ - (VMGestureType)twoFingerScrollType { return [self gestureTypeForSetting:@"GestureTwoScroll"]; } +- (VMGestureType)twoFingerPinchType { + return [self gestureTypeForSetting:@"GestureTwoPinch"]; +} + +- (VMGestureType)threeFingerTapType { + return [self gestureTypeForSetting:@"GestureThreeTap"]; +} + - (VMGestureType)threeFingerPanType { return [self gestureTypeForSetting:@"GestureThreePan"]; } +- (BOOL)isThreeFingerSwipeEnabled { + return [self integerForSetting:@"GestureThreeSwipe"] != 0; +} + +- (BOOL)isVerticalSwipeForPan:(UIPanGestureRecognizer *)sender accelerationY:(CGFloat)accelerationY { + CGPoint translation = [sender translationInView:sender.view]; + CGPoint velocity = [sender velocityInView:sender.view]; + BOOL velocityWithGesture = translation.y * velocity.y > 0; + BOOL acceleratingWithGesture = translation.y * accelerationY > 0; + return [self isVerticalSwipeDistanceForPan:sender] && + ((velocityWithGesture && fabs(velocity.y) >= kMultitouchSwipeVelocity) || + (acceleratingWithGesture && fabs(accelerationY) >= kMultitouchSwipeAcceleration)) && + fabs(translation.y) > fabs(translation.x) * 1.5f; +} + +- (BOOL)isVerticalSwipeDistanceForPan:(UIPanGestureRecognizer *)sender { + CGPoint translation = [sender translationInView:sender.view]; + return fabs(translation.y) >= kMultitouchSwipeDistance && + fabs(translation.y) > fabs(translation.x); +} + +- (CGFloat)verticalAccelerationForPan:(UIPanGestureRecognizer *)sender { + CGPoint velocity = [sender velocityInView:sender.view]; + NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate]; + CGPoint lastVelocity = CGPointZero; + NSTimeInterval lastTime = 0; + if (sender == self.twoPan) { + lastVelocity = self.multitouchTwoPanLastVelocity; + lastTime = self.multitouchTwoPanLastTime; + if (sender.state == UIGestureRecognizerStateBegan) { + self.multitouchTwoPanBeginTime = now; + } + self.multitouchTwoPanLastVelocity = velocity; + self.multitouchTwoPanLastTime = now; + } else if (sender == self.threePan) { + lastVelocity = self.multitouchThreePanLastVelocity; + lastTime = self.multitouchThreePanLastTime; + if (sender.state == UIGestureRecognizerStateBegan) { + self.multitouchThreePanBeginTime = now; + } + self.multitouchThreePanLastVelocity = velocity; + self.multitouchThreePanLastTime = now; + } + if (sender.state == UIGestureRecognizerStateBegan || lastTime <= 0 || now <= lastTime) { + return 0.0f; + } + return (velocity.y - lastVelocity.y) / (CGFloat)(now - lastTime); +} + +- (BOOL)shouldDeferPan:(UIPanGestureRecognizer *)sender forSwipeEnabled:(BOOL)swipeEnabled decided:(BOOL *)decided candidate:(BOOL *)candidate accelerationY:(CGFloat)accelerationY { + if ([self isTerminalGestureState:sender.state]) { + return NO; + } + CGPoint translation = [sender translationInView:sender.view]; + if (!swipeEnabled) { + return hypot(translation.x, translation.y) < kMultitouchPanStartDistance; + } + NSTimeInterval beginTime = 0; + if (sender == self.twoPan) { + beginTime = self.multitouchTwoPanBeginTime; + } else if (sender == self.threePan) { + beginTime = self.multitouchThreePanBeginTime; + } + if (!*decided) { + NSTimeInterval elapsed = beginTime > 0 ? [NSDate timeIntervalSinceReferenceDate] - beginTime : 0; + if (elapsed < kMultitouchSwipeCandidateWindow) { + return YES; + } + *decided = YES; + *candidate = [self isVerticalSwipeForPan:sender accelerationY:accelerationY]; + return *candidate; + } + if (*candidate) { + return YES; + } + return hypot(translation.x, translation.y) < kMultitouchPanStartDistance; +} + +- (CGFloat)pinchTouchDistance:(UIPinchGestureRecognizer *)sender { + if (sender.numberOfTouches < 2) { + return 0.0f; + } + CGPoint first = [sender locationOfTouch:0 inView:sender.view]; + CGPoint second = [sender locationOfTouch:1 inView:sender.view]; + return hypot(first.x - second.x, first.y - second.y); +} + - (VMMouseType)mouseTypeForSetting:(NSString *)key { NSInteger integer = [self integerForSetting:key]; if (integer < VMMouseTypeRelative || integer >= VMMouseTypeMax) { @@ -302,9 +413,190 @@ - (void)scrollWithInertia:(UIPanGestureRecognizer *)sender { } } +- (BOOL)dragButtonsForGestureType:(VMGestureType)type primary:(BOOL *)primary secondary:(BOOL *)secondary middle:(BOOL *)middle { + *primary = NO; + *secondary = NO; + *middle = NO; + switch (type) { + case VMGestureTypeDragCursor: + *primary = YES; + return YES; + case VMGestureTypeRightDrag: + *secondary = YES; + return YES; + case VMGestureTypeMiddleDrag: + *middle = YES; + return YES; + default: + return NO; + } +} + +- (void)dragFromPrimaryTouchForPan:(UIPanGestureRecognizer *)sender gestureType:(VMGestureType)type state:(UIGestureRecognizerState)state { + BOOL primary = NO; + BOOL secondary = NO; + BOOL middle = NO; + if ([self dragButtonsForGestureType:type primary:&primary secondary:&secondary middle:&middle]) { + [self dragCursor:state primary:primary secondary:secondary middle:middle]; + CGPoint translation = [sender translationInView:sender.view]; + CGPoint location = CGPointMake(self.multitouchPrimaryTouchLocation.x + translation.x, + self.multitouchPrimaryTouchLocation.y + translation.y); + if (state == UIGestureRecognizerStateBegan) { + [self.cursor startMovement:self.multitouchPrimaryTouchLocation]; + } + if (state != UIGestureRecognizerStateCancelled && + state != UIGestureRecognizerStateFailed) { + [self.cursor updateMovement:location]; + } + if (state == UIGestureRecognizerStateEnded) { + CGPoint velocity = [sender velocityInView:sender.view]; + [self.cursor endMovementWithVelocity:velocity resistance:kCursorResistance]; + } + } else if ([self isTerminalGestureState:state]) { + [self dragCursor:state primary:YES secondary:YES middle:YES]; + } +} + +- (void)performMultitouchPan:(UIPanGestureRecognizer *)sender gestureType:(VMGestureType)type actionStarted:(BOOL *)actionStarted { + if ([self isTerminalGestureState:sender.state]) { + return; + } + if (!*actionStarted) { + [self dragFromPrimaryTouchForPan:sender gestureType:type state:UIGestureRecognizerStateBegan]; + *actionStarted = YES; + } + if (sender.state != UIGestureRecognizerStateBegan) { + [self dragFromPrimaryTouchForPan:sender gestureType:type state:sender.state]; + } +} + +- (void)dragCursor:(UIGestureRecognizerState)state gestureType:(VMGestureType)type { + BOOL primary = NO; + BOOL secondary = NO; + BOOL middle = NO; + if ([self dragButtonsForGestureType:type primary:&primary secondary:&secondary middle:&middle]) { + [self dragCursor:state primary:primary secondary:secondary middle:middle]; + } +} + +- (void)showLongPressIndicatorAtLocation:(CGPoint)location { +#if !defined(TARGET_OS_VISION) || !TARGET_OS_VISION + CGFloat diameter = 88.0f; + UIView *indicator = [[UIView alloc] initWithFrame:CGRectMake(0, 0, diameter, diameter)]; + indicator.center = location; + indicator.userInteractionEnabled = NO; + indicator.backgroundColor = UIColor.clearColor; + + CGRect ringRect = CGRectInset(indicator.bounds, 4.0f, 4.0f); + UIBezierPath *ringPath = [UIBezierPath bezierPathWithOvalInRect:ringRect]; + CAShapeLayer *outlineLayer = [CAShapeLayer layer]; + outlineLayer.path = ringPath.CGPath; + outlineLayer.fillColor = UIColor.clearColor.CGColor; + outlineLayer.strokeColor = UIColor.whiteColor.CGColor; + outlineLayer.lineWidth = 6.0f; + [indicator.layer addSublayer:outlineLayer]; + CAShapeLayer *strokeLayer = [CAShapeLayer layer]; + strokeLayer.path = ringPath.CGPath; + strokeLayer.fillColor = UIColor.clearColor.CGColor; + strokeLayer.strokeColor = UIColor.blackColor.CGColor; + strokeLayer.lineWidth = 2.0f; + [indicator.layer addSublayer:strokeLayer]; + + indicator.layer.shadowColor = UIColor.blackColor.CGColor; + indicator.layer.shadowOpacity = 0.35f; + indicator.layer.shadowRadius = 4.0f; + indicator.layer.shadowOffset = CGSizeZero; + indicator.alpha = 1.0f; + [self.mtkView addSubview:indicator]; + [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ + indicator.alpha = 0.0f; + indicator.transform = CGAffineTransformMakeScale(1.35f, 1.35f); + } completion:^(BOOL finished) { + (void)finished; + [indicator removeFromSuperview]; + }]; +#else + (void)location; +#endif +} + +- (void)resetMultitouchSequence { + self.multitouchPrimaryTouch = nil; + self.multitouchTwoPanConsumed = NO; + self.multitouchThreePanConsumed = NO; + self.multitouchTwoPanActionStarted = NO; + self.multitouchThreePanActionStarted = NO; + self.multitouchTwoSwipeDecided = NO; + self.multitouchThreeSwipeDecided = NO; + self.multitouchTwoSwipeCandidate = NO; + self.multitouchThreeSwipeCandidate = NO; + self.multitouchTwoPanLastVelocity = CGPointZero; + self.multitouchThreePanLastVelocity = CGPointZero; + self.multitouchTwoPanLastTime = 0; + self.multitouchThreePanLastTime = 0; + self.multitouchTwoPanBeginTime = 0; + self.multitouchThreePanBeginTime = 0; + self.multitouchPinchActive = NO; + self.multitouchPinchInitialDistance = 0.0f; + self.multitouchActiveDirectTouchCount = 0; + self.multitouchLongPressRecognized = NO; + self.multitouchLongPressPending = NO; + self.multitouchLongPressDragging = NO; + self.multitouchLongPressTouchActive = NO; + self.multitouchLongPressCancelledByMovement = NO; + self.multitouchScrollVelocity = CGPointZero; +} + +- (void)cancelMultitouchLongPress { + BOOL shouldResetRecognizer = !self.multitouchLongPressCancelledByMovement || + self.multitouchLongPressRecognized || + self.multitouchLongPressPending || + self.multitouchLongPressDragging; + if (self.multitouchLongPressDragging) { + [self dragCursor:UIGestureRecognizerStateCancelled gestureType:self.longPressDragType]; + } + self.multitouchLongPressRecognized = NO; + self.multitouchLongPressPending = NO; + self.multitouchLongPressDragging = NO; + self.multitouchLongPressTouchActive = NO; + self.multitouchLongPressCancelledByMovement = YES; + + // Reset only the long-press recognizer so a second finger cannot later + // complete a single-finger right-click while a 2F/3F pan is in progress. + if (shouldResetRecognizer && self.longPress.enabled) { + self.longPress.enabled = NO; + self.longPress.enabled = YES; + } +} + +- (BOOL)isTerminalGestureState:(UIGestureRecognizerState)state { + return state == UIGestureRecognizerStateEnded || + state == UIGestureRecognizerStateCancelled || + state == UIGestureRecognizerStateFailed; +} + +- (NSUInteger)activeDirectTouchCountForEvent:(UIEvent *)event currentTouches:(NSSet *)touches { + NSSet *eventTouches = event.allTouches ?: touches; + NSUInteger count = 0; + for (UITouch *touch in eventTouches) { + if (touch.type != UITouchTypeDirect) { + continue; + } + if (touch.phase == UITouchPhaseEnded || touch.phase == UITouchPhaseCancelled) { + continue; + } + count++; + } + return count; +} + - (IBAction)gesturePan:(UIPanGestureRecognizer *)sender { if (self.serverModeCursor) { // otherwise we handle in touchesMoved [self moveMouseWithInertia:sender]; + } else if (self.touchMouseType == VMMouseTypeMultitouch) { + // In multitouch mode we process single-finger drag directly in touchesMoved + // to avoid UIPan recognition delay and improve gesture responsiveness. + (void)sender; } } @@ -327,6 +619,75 @@ - (void)moveScreen:(UIPanGestureRecognizer *)sender { } - (IBAction)gestureTwoPan:(UIPanGestureRecognizer *)sender { + if (self.touchMouseType == VMMouseTypeMultitouch) { + CGFloat accelerationY = [self verticalAccelerationForPan:sender]; + BOOL swipeEnabled = self.twoFingerScrollType == VMGestureTypeMouseWheel; + if (sender.state == UIGestureRecognizerStateBegan || + sender.state == UIGestureRecognizerStateChanged || + sender.state == UIGestureRecognizerStateEnded) { + self.multitouchTwoPanConsumed = YES; + } + if (self.multitouchPinchActive) { + if ([self isTerminalGestureState:sender.state]) { + [self dragCursor:sender.state primary:YES secondary:YES middle:YES]; + } + return; + } + if (self.multitouchTwoPanActionStarted) { + if ([self isTerminalGestureState:sender.state]) { + [self dragCursor:sender.state gestureType:self.twoFingerPanType]; + self.multitouchTwoPanActionStarted = NO; + return; + } + BOOL actionStarted = YES; + [self performMultitouchPan:sender + gestureType:self.twoFingerPanType + actionStarted:&actionStarted]; + self.multitouchTwoPanActionStarted = actionStarted; + return; + } + if (sender.state == UIGestureRecognizerStateEnded && + swipeEnabled && + !self.multitouchTwoSwipeDecided) { + self.multitouchTwoSwipeDecided = YES; + self.multitouchTwoSwipeCandidate = [self isVerticalSwipeForPan:sender accelerationY:accelerationY]; + } + if (sender.state == UIGestureRecognizerStateEnded && + swipeEnabled && + self.multitouchTwoSwipeCandidate) { + [self dragCursor:UIGestureRecognizerStateEnded primary:YES secondary:YES middle:YES]; + CGPoint translation = [sender translationInView:sender.view]; + if (translation.y < 0) { + [self.vmInput sendMouseScroll:kCSInputScrollUp buttonMask:self.mouseButtonDown dy:0]; + } else { + [self.vmInput sendMouseScroll:kCSInputScrollDown buttonMask:self.mouseButtonDown dy:0]; + } + return; + } + BOOL swipeCandidate = self.multitouchTwoSwipeCandidate; + BOOL swipeDecided = self.multitouchTwoSwipeDecided; + if ([self shouldDeferPan:sender + forSwipeEnabled:swipeEnabled + decided:&swipeDecided + candidate:&swipeCandidate + accelerationY:accelerationY]) { + self.multitouchTwoSwipeDecided = swipeDecided; + self.multitouchTwoSwipeCandidate = swipeCandidate; + return; + } + self.multitouchTwoSwipeDecided = swipeDecided; + self.multitouchTwoSwipeCandidate = swipeCandidate; + BOOL actionStarted = self.multitouchTwoPanActionStarted; + [self performMultitouchPan:sender + gestureType:self.twoFingerPanType + actionStarted:&actionStarted]; + self.multitouchTwoPanActionStarted = actionStarted; + if (self.multitouchTwoPanActionStarted) { + self.multitouchTwoSwipeDecided = YES; + self.multitouchTwoSwipeCandidate = NO; + } + return; + } switch (self.twoFingerPanType) { case VMGestureTypeMoveScreen: [self moveScreen:sender]; @@ -344,6 +705,69 @@ - (IBAction)gestureTwoPan:(UIPanGestureRecognizer *)sender { } - (IBAction)gestureThreePan:(UIPanGestureRecognizer *)sender { + CGFloat accelerationY = [self verticalAccelerationForPan:sender]; + BOOL swipeEnabled = [self isThreeFingerSwipeEnabled]; + if (self.touchMouseType == VMMouseTypeMultitouch && + (sender.state == UIGestureRecognizerStateBegan || + sender.state == UIGestureRecognizerStateChanged || + sender.state == UIGestureRecognizerStateEnded)) { + self.multitouchThreePanConsumed = YES; + } + if (self.touchMouseType == VMMouseTypeMultitouch && + self.multitouchThreePanActionStarted) { + if ([self isTerminalGestureState:sender.state]) { + [self dragCursor:sender.state gestureType:self.threeFingerPanType]; + self.multitouchThreePanActionStarted = NO; + return; + } + BOOL actionStarted = YES; + [self performMultitouchPan:sender + gestureType:self.threeFingerPanType + actionStarted:&actionStarted]; + self.multitouchThreePanActionStarted = actionStarted; + return; + } + if (sender.state == UIGestureRecognizerStateEnded && swipeEnabled) { + if (!self.multitouchThreeSwipeDecided) { + self.multitouchThreeSwipeDecided = YES; + self.multitouchThreeSwipeCandidate = [self isVerticalSwipeForPan:sender accelerationY:accelerationY]; + } + if (self.multitouchThreeSwipeCandidate) { + CGPoint translation = [sender translationInView:sender.view]; + [self dragCursor:UIGestureRecognizerStateEnded primary:YES secondary:YES middle:YES]; + if (translation.y < 0) { + [self showKeyboard]; + } else { + [self hideKeyboard]; + } + return; + } + } + if (self.touchMouseType == VMMouseTypeMultitouch) { + BOOL swipeCandidate = self.multitouchThreeSwipeCandidate; + BOOL swipeDecided = self.multitouchThreeSwipeDecided; + if ([self shouldDeferPan:sender + forSwipeEnabled:swipeEnabled + decided:&swipeDecided + candidate:&swipeCandidate + accelerationY:accelerationY]) { + self.multitouchThreeSwipeDecided = swipeDecided; + self.multitouchThreeSwipeCandidate = swipeCandidate; + return; + } + self.multitouchThreeSwipeDecided = swipeDecided; + self.multitouchThreeSwipeCandidate = swipeCandidate; + BOOL actionStarted = self.multitouchThreePanActionStarted; + [self performMultitouchPan:sender + gestureType:self.threeFingerPanType + actionStarted:&actionStarted]; + self.multitouchThreePanActionStarted = actionStarted; + if (self.multitouchThreePanActionStarted) { + self.multitouchThreeSwipeDecided = YES; + self.multitouchThreeSwipeCandidate = NO; + } + return; + } switch (self.threeFingerPanType) { case VMGestureTypeMoveScreen: [self moveScreen:sender]; @@ -395,14 +819,25 @@ - (CGPoint)moveMouseScroll:(CGPoint)translation { } - (void)mouseClick:(CSInputButton)button location:(CGPoint)location { + if ((button == kCSInputButtonLeft && self.mouseLeftDown) || + (button == kCSInputButtonRight && self.mouseRightDown) || + (button == kCSInputButtonMiddle && self.mouseMiddleDown)) { + return; + } if (!self.serverModeCursor) { self.cursor.center = location; } [self.vmInput sendMouseButton:button mask:kCSInputButtonNone pressed:YES]; [self onDelay:0.05f action:^{ - self.mouseLeftDown = NO; - self.mouseRightDown = NO; - self.mouseMiddleDown = NO; + if (button == kCSInputButtonLeft && self.mouseLeftDown) { + return; + } + if (button == kCSInputButtonRight && self.mouseRightDown) { + return; + } + if (button == kCSInputButtonMiddle && self.mouseMiddleDown) { + return; + } [self.vmInput sendMouseButton:button mask:kCSInputButtonNone pressed:NO]; }]; #if !defined(TARGET_OS_VISION) || !TARGET_OS_VISION @@ -411,17 +846,17 @@ - (void)mouseClick:(CSInputButton)button location:(CGPoint)location { } - (void)dragCursor:(UIGestureRecognizerState)state primary:(BOOL)primary secondary:(BOOL)secondary middle:(BOOL)middle { - CSInputButton button = kCSInputButtonNone; - if (middle) { - button = kCSInputButtonMiddle; - } - if (secondary) { - button = kCSInputButtonRight; - } - if (primary) { - button = kCSInputButtonLeft; - } if (state == UIGestureRecognizerStateBegan) { + CSInputButton button = kCSInputButtonNone; + if (middle) { + button = kCSInputButtonMiddle; + } + if (secondary) { + button = kCSInputButtonRight; + } + if (primary) { + button = kCSInputButtonLeft; + } #if !defined(TARGET_OS_VISION) || !TARGET_OS_VISION [self.clickFeedbackGenerator selectionChanged]; #endif @@ -435,29 +870,147 @@ - (void)dragCursor:(UIGestureRecognizerState)state primary:(BOOL)primary seconda self.mouseMiddleDown = YES; } [self.vmInput sendMouseButton:button mask:self.mouseButtonDown pressed:YES]; - } else if (state == UIGestureRecognizerStateEnded) { - self.mouseLeftDown = NO; - self.mouseRightDown = NO; - self.mouseMiddleDown = NO; - [self.vmInput sendMouseButton:button mask:self.mouseButtonDown pressed:NO]; + } else if (state == UIGestureRecognizerStateEnded || + state == UIGestureRecognizerStateCancelled || + state == UIGestureRecognizerStateFailed) { + if (primary && self.mouseLeftDown) { + self.mouseLeftDown = NO; + [self.vmInput sendMouseButton:kCSInputButtonLeft mask:self.mouseButtonDown pressed:NO]; + } + if (secondary && self.mouseRightDown) { + self.mouseRightDown = NO; + [self.vmInput sendMouseButton:kCSInputButtonRight mask:self.mouseButtonDown pressed:NO]; + } + if (middle && self.mouseMiddleDown) { + self.mouseMiddleDown = NO; + [self.vmInput sendMouseButton:kCSInputButtonMiddle mask:self.mouseButtonDown pressed:NO]; + } } } - (IBAction)gestureTap:(UITapGestureRecognizer *)sender { - if (sender.state == UIGestureRecognizerStateEnded && - self.serverModeCursor) { // otherwise we handle in touchesBegan + if (sender.state != UIGestureRecognizerStateEnded) { + return; + } + if (self.serverModeCursor || self.touchMouseType == VMMouseTypeMultitouch) { [self mouseClick:kCSInputButtonLeft location:[sender locationInView:sender.view]]; } } - (IBAction)gestureTwoTap:(UITapGestureRecognizer *)sender { - if (sender.state == UIGestureRecognizerStateEnded && - self.twoFingerTapType == VMGestureTypeRightClick) { - [self mouseClick:kCSInputButtonRight location:[sender locationInView:sender.view]]; + if (sender.state != UIGestureRecognizerStateEnded) { + return; + } + if (self.touchMouseType == VMMouseTypeMultitouch && self.multitouchTwoPanConsumed) { + return; + } + CGPoint panTranslation = [self.twoPan translationInView:self.twoPan.view]; + if (self.touchMouseType == VMMouseTypeMultitouch && + hypot(panTranslation.x, panTranslation.y) >= kMultitouchDragThreshold) { + return; + } + CGPoint clickLocation = [sender locationInView:sender.view]; + if (self.touchMouseType == VMMouseTypeMultitouch) { + clickLocation = self.multitouchPrimaryTouchLocation; + } + switch (self.twoFingerTapType) { + case VMGestureTypeRightClick: + [self mouseClick:kCSInputButtonRight location:clickLocation]; + break; + case VMGestureTypeMiddleClick: + [self mouseClick:kCSInputButtonMiddle location:clickLocation]; + break; + default: + break; + } +} + +- (IBAction)gestureThreeTap:(UITapGestureRecognizer *)sender { + if (sender.state != UIGestureRecognizerStateEnded) { + return; + } + if (self.touchMouseType == VMMouseTypeMultitouch && self.multitouchThreePanConsumed) { + return; + } + CGPoint panTranslation = [self.threePan translationInView:self.threePan.view]; + if (self.touchMouseType == VMMouseTypeMultitouch && + hypot(panTranslation.x, panTranslation.y) >= kMultitouchDragThreshold) { + return; + } + CGPoint clickLocation = [sender locationInView:sender.view]; + if (self.touchMouseType == VMMouseTypeMultitouch) { + clickLocation = self.multitouchPrimaryTouchLocation; + } + switch (self.threeFingerTapType) { + case VMGestureTypeRightClick: + [self mouseClick:kCSInputButtonRight location:clickLocation]; + break; + case VMGestureTypeMiddleClick: + [self mouseClick:kCSInputButtonMiddle location:clickLocation]; + break; + default: + break; } } - (IBAction)gestureLongPress:(UILongPressGestureRecognizer *)sender { + if (self.touchMouseType == VMMouseTypeMultitouch) { + if (sender.numberOfTouches != 1 && ![self isTerminalGestureState:sender.state]) { + return; + } + CGPoint location = [sender locationInView:sender.view]; + if (sender.state == UIGestureRecognizerStateBegan) { + self.multitouchLongPressRecognized = YES; + self.multitouchLongPressPending = YES; + self.multitouchLongPressDragging = NO; + self.multitouchLongPressOrigin = location; + self.multitouchPrimaryTouchLocation = location; + [self.cursor startMovement:location]; + [self.cursor updateMovement:location]; + [self showLongPressIndicatorAtLocation:location]; +#if !defined(TARGET_OS_VISION) || !TARGET_OS_VISION + [self.clickFeedbackGenerator selectionChanged]; +#endif + } else if (sender.state == UIGestureRecognizerStateChanged) { + if (self.multitouchLongPressPending && !self.multitouchLongPressDragging) { + CGPoint delta = CGPointMake(location.x - self.multitouchLongPressOrigin.x, + location.y - self.multitouchLongPressOrigin.y); + if (hypot(delta.x, delta.y) >= kMultitouchDragThreshold) { + self.multitouchLongPressPending = NO; + self.multitouchLongPressDragging = YES; + [self dragCursor:UIGestureRecognizerStateBegan gestureType:self.longPressDragType]; + } + } + if (self.multitouchLongPressDragging) { + [self.cursor updateMovement:location]; + } + } else if (sender.state == UIGestureRecognizerStateEnded) { + if (self.multitouchLongPressDragging) { + [self dragCursor:UIGestureRecognizerStateEnded gestureType:self.longPressDragType]; + } else if (self.multitouchLongPressPending) { + switch (self.longPressType) { + case VMGestureTypeRightClick: + [self mouseClick:kCSInputButtonRight location:self.multitouchLongPressOrigin]; + break; + case VMGestureTypeMiddleClick: + [self mouseClick:kCSInputButtonMiddle location:self.multitouchLongPressOrigin]; + break; + default: + break; + } + } + self.multitouchLongPressRecognized = NO; + self.multitouchLongPressPending = NO; + self.multitouchLongPressDragging = NO; + } else if (sender.state == UIGestureRecognizerStateCancelled || + sender.state == UIGestureRecognizerStateFailed) { + [self dragCursor:sender.state gestureType:self.longPressDragType]; + self.multitouchLongPressRecognized = NO; + self.multitouchLongPressPending = NO; + self.multitouchLongPressDragging = NO; + } + return; + } if (sender.state == UIGestureRecognizerStateEnded && self.longPressType == VMGestureTypeRightClick) { [self mouseClick:kCSInputButtonRight location:[sender locationInView:sender.view]]; @@ -467,17 +1020,47 @@ - (IBAction)gestureLongPress:(UILongPressGestureRecognizer *)sender { } - (IBAction)gesturePinch:(UIPinchGestureRecognizer *)sender { - // disable pinch if move screen on pan is disabled - if (!(self.twoFingerPanType == VMGestureTypeMoveScreen || self.threeFingerPanType == VMGestureTypeMoveScreen)) { + if (self.twoFingerPinchType != VMGestureTypeScaleDisplay) { return; } + if (self.touchMouseType == VMMouseTypeMultitouch) { + if (sender.numberOfTouches != 2 && ![self isTerminalGestureState:sender.state]) { + return; + } + if (sender.state == UIGestureRecognizerStateBegan) { + self.multitouchPinchInitialDistance = [self pinchTouchDistance:sender]; + sender.scale = 1.0; + return; + } else if ([self isTerminalGestureState:sender.state]) { + self.multitouchPinchActive = NO; + self.multitouchPinchInitialDistance = 0.0f; + return; + } + if (!self.multitouchPinchActive) { + if (self.multitouchTwoPanActionStarted || self.multitouchThreePanConsumed) { + return; + } + CGFloat distance = [self pinchTouchDistance:sender]; + if (self.multitouchPinchInitialDistance <= 0.0f) { + self.multitouchPinchInitialDistance = distance; + sender.scale = 1.0; + return; + } + if (fabs(distance - self.multitouchPinchInitialDistance) < kMultitouchPinchStartDistance) { + sender.scale = 1.0; + return; + } + self.multitouchPinchActive = YES; + self.multitouchTwoPanConsumed = YES; + [self cancelMultitouchLongPress]; + [self dragCursor:UIGestureRecognizerStateEnded primary:YES secondary:YES middle:YES]; + } + } if (sender.state == UIGestureRecognizerStateBegan || - sender.state == UIGestureRecognizerStateChanged || - sender.state == UIGestureRecognizerStateEnded) { + sender.state == UIGestureRecognizerStateChanged) { NSAssert(sender.scale > 0, @"sender.scale cannot be 0"); CGFloat scaling; if (!self.delegate.qemuDisplayIsNativeResolution) { - // will be undo in `-setDisplayScaling:origin:` scaling = CGPixelToPoint(self.view, CGPointToPixel(self.view, self.delegate.displayScale) * sender.scale); } else { scaling = self.delegate.displayScale * sender.scale; @@ -488,19 +1071,10 @@ - (IBAction)gesturePinch:(UIPinchGestureRecognizer *)sender { } } -- (IBAction)gestureSwipeUp:(UISwipeGestureRecognizer *)sender { - if (sender.state == UIGestureRecognizerStateEnded) { - [self showKeyboard]; - } -} - -- (IBAction)gestureSwipeDown:(UISwipeGestureRecognizer *)sender { - if (sender.state == UIGestureRecognizerStateEnded) { - [self hideKeyboard]; - } -} - - (IBAction)gestureSwipeScroll:(UISwipeGestureRecognizer *)sender { + if (self.touchMouseType == VMMouseTypeMultitouch) { + return; + } if (sender.state == UIGestureRecognizerStateEnded && self.twoFingerScrollType == VMGestureTypeMouseWheel) { if (sender == self.swipeScrollUp) { @@ -516,54 +1090,24 @@ - (IBAction)gestureSwipeScroll:(UISwipeGestureRecognizer *)sender { #pragma mark - UIGestureRecognizerDelegate - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { - if (gestureRecognizer == self.twoPan && otherGestureRecognizer == self.swipeUp) { - return YES; - } - if (gestureRecognizer == self.twoPan && otherGestureRecognizer == self.swipeDown) { - return YES; - } - if (gestureRecognizer == self.twoTap && otherGestureRecognizer == self.swipeDown) { - return YES; - } - if (gestureRecognizer == self.twoTap && otherGestureRecognizer == self.swipeUp) { - return YES; - } if (gestureRecognizer == self.tap && otherGestureRecognizer == self.twoTap) { return YES; } - if (gestureRecognizer == self.longPress && otherGestureRecognizer == self.tap) { - return YES; - } - if (gestureRecognizer == self.longPress && otherGestureRecognizer == self.twoTap) { - return YES; - } - if (gestureRecognizer == self.pinch && otherGestureRecognizer == self.swipeDown) { - return YES; - } - if (gestureRecognizer == self.pinch && otherGestureRecognizer == self.swipeUp) { + if (gestureRecognizer == self.tap && otherGestureRecognizer == self.threeTap) { return YES; } - if (gestureRecognizer == self.pan && otherGestureRecognizer == self.swipeUp) { + if (gestureRecognizer == self.twoTap && otherGestureRecognizer == self.twoPan) { return YES; } - if (gestureRecognizer == self.pan && otherGestureRecognizer == self.swipeDown) { + if (gestureRecognizer == self.threeTap && otherGestureRecognizer == self.threePan) { return YES; } - if (gestureRecognizer == self.threePan && otherGestureRecognizer == self.swipeUp) { + if (gestureRecognizer == self.tap && otherGestureRecognizer == self.longPress) { return YES; } - if (gestureRecognizer == self.threePan && otherGestureRecognizer == self.swipeDown) { + if (gestureRecognizer == self.pinch && otherGestureRecognizer == self.threePan) { return YES; } - // only if we do not disable two finger swipe - if (self.twoFingerScrollType != VMGestureTypeNone) { - if (gestureRecognizer == self.twoPan && otherGestureRecognizer == self.swipeScrollUp) { - return YES; - } - if (gestureRecognizer == self.twoPan && otherGestureRecognizer == self.swipeScrollDown) { - return YES; - } - } #if !defined(TARGET_OS_VISION) || !TARGET_OS_VISION return [self pencilGestureRecognizer:gestureRecognizer shouldRequireFailureOfGestureRecognizer:otherGestureRecognizer]; #else @@ -571,14 +1115,56 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequire #endif } +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { + if (gestureRecognizer == self.pinch) { + if (self.touchMouseType == VMMouseTypeMultitouch && self.pinch.numberOfTouches != 2) { + return NO; + } + if (self.touchMouseType == VMMouseTypeMultitouch && + (self.multitouchTwoPanActionStarted || + self.threePan.state == UIGestureRecognizerStateBegan || + self.threePan.state == UIGestureRecognizerStateChanged)) { + return NO; + } + return self.twoFingerPinchType == VMGestureTypeScaleDisplay; + } + if (self.touchMouseType == VMMouseTypeMultitouch) { + if (gestureRecognizer == self.swipeScrollUp || + gestureRecognizer == self.swipeScrollDown) { + return NO; + } + if (gestureRecognizer == self.longPress) { + return self.longPress.numberOfTouches == 1 && + self.multitouchLongPressTouchActive && + (self.longPressType != VMGestureTypeNone || + self.longPressDragType != VMGestureTypeNone); + } + if (gestureRecognizer == self.twoTap) { + CGPoint panTranslation = [self.twoPan translationInView:self.twoPan.view]; + return !self.multitouchTwoPanConsumed && + hypot(panTranslation.x, panTranslation.y) < kMultitouchDragThreshold; + } + if (gestureRecognizer == self.threeTap) { + CGPoint panTranslation = [self.threePan translationInView:self.threePan.view]; + return !self.multitouchThreePanConsumed && + hypot(panTranslation.x, panTranslation.y) < kMultitouchDragThreshold; + } + } + return YES; +} + - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { - if (gestureRecognizer == self.twoPan && otherGestureRecognizer == self.pinch) { - if (self.twoFingerPanType == VMGestureTypeMoveScreen) { - return YES; - } else { + if (gestureRecognizer == self.pan && otherGestureRecognizer == self.longPress) { + return YES; + } else if (gestureRecognizer == self.longPress && otherGestureRecognizer == self.pan) { + return YES; + } else if (self.touchMouseType == VMMouseTypeMultitouch && + self.twoFingerPinchType == VMGestureTypeScaleDisplay && + ((gestureRecognizer == self.twoPan && otherGestureRecognizer == self.pinch) || + (gestureRecognizer == self.pinch && otherGestureRecognizer == self.twoPan))) { + if (self.multitouchTwoPanActionStarted) { return NO; } - } else if (gestureRecognizer == self.pan && otherGestureRecognizer == self.longPress) { return YES; } else if (self.twoFingerScrollType == VMGestureTypeNone && otherGestureRecognizer == self.twoPan) { // if two finger swipe is disabled, we can also recognize two finger pans @@ -657,6 +1243,21 @@ - (BOOL)isTouchGazeGesture:(UITouch *)touch { - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { if (!self.delegate.qemuInputLegacy) { + NSUInteger activeDirectTouches = [self activeDirectTouchCountForEvent:event currentTouches:touches]; + if (self.touchMouseType == VMMouseTypeMultitouch && + activeDirectTouches == touches.count && + !self.vmInput.serverModeCursor) { + [self resetMultitouchSequence]; + } + if (self.touchMouseType == VMMouseTypeMultitouch && + !self.vmInput.serverModeCursor) { + self.multitouchActiveDirectTouchCount = activeDirectTouches; + } + if (self.touchMouseType == VMMouseTypeMultitouch && + activeDirectTouches > 1 && + !self.vmInput.serverModeCursor) { + [self cancelMultitouchLongPress]; + } for (UITouch *touch in touches) { VMMouseType type = [self touchTypeToMouseType:touch.type]; #if TARGET_OS_VISION @@ -667,6 +1268,25 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { if ([self switchMouseType:type]) { [self dragCursor:UIGestureRecognizerStateEnded primary:YES secondary:YES middle:YES]; // reset drag } else if (!self.vmInput.serverModeCursor) { // start click for client mode + if (type == VMMouseTypeMultitouch && touch.type == UITouchTypeDirect) { + CGPoint pos = [touch locationInView:self.mtkView]; + if (!self.multitouchPrimaryTouch) { + self.multitouchPrimaryTouch = touch; + self.multitouchPrimaryTouchLocation = pos; + self.multitouchLongPressOrigin = pos; + self.multitouchLongPressTouchActive = activeDirectTouches == 1; + self.multitouchLongPressPending = NO; + self.multitouchLongPressDragging = NO; + self.multitouchLongPressCancelledByMovement = activeDirectTouches > 1; + [self.cursor startMovement:pos]; + [self.cursor updateMovement:pos]; + [self.scroll startMovement:pos]; + self.multitouchScrollLastLocation = pos; + self.multitouchScrollVelocity = CGPointZero; + self.multitouchScrollLastTime = touch.timestamp; + } + break; + } BOOL primary = YES; BOOL secondary = NO; BOOL middle = NO; @@ -703,6 +1323,46 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { // move cursor in client mode, in server mode we handle in gesturePan if (!self.delegate.qemuInputLegacy && !self.vmInput.serverModeCursor) { + if (self.touchMouseType == VMMouseTypeMultitouch) { + NSUInteger activeDirectTouches = [self activeDirectTouchCountForEvent:event currentTouches:touches]; + if (self.multitouchTwoPanConsumed || + self.multitouchThreePanConsumed || + self.multitouchPinchActive) { + [super touchesMoved:touches withEvent:event]; + return; + } + if (activeDirectTouches != 1) { + if (activeDirectTouches > 1) { + [self cancelMultitouchLongPress]; + } + [super touchesMoved:touches withEvent:event]; + return; + } + for (UITouch *touch in touches) { + if (touch.type != UITouchTypeDirect) { + continue; + } + if (self.multitouchPrimaryTouch && touch != self.multitouchPrimaryTouch) { + continue; + } + CGPoint pos = [touch locationInView:self.mtkView]; + if (!self.multitouchLongPressRecognized && + !self.multitouchLongPressPending && + !self.multitouchLongPressDragging) { + NSTimeInterval elapsed = touch.timestamp - self.multitouchScrollLastTime; + if (elapsed > 0) { + self.multitouchScrollVelocity = CGPointMake((pos.x - self.multitouchScrollLastLocation.x) / elapsed, + (pos.y - self.multitouchScrollLastLocation.y) / elapsed); + } + self.multitouchScrollLastLocation = pos; + self.multitouchScrollLastTime = touch.timestamp; + [self.scroll updateMovement:pos]; + } + break; + } + [super touchesMoved:touches withEvent:event]; + return; + } for (UITouch *touch in touches) { [self.cursor updateMovement:[touch locationInView:self.mtkView]]; break; // handle single touch @@ -714,7 +1374,10 @@ - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { // release click in client mode, in server mode we handle in gesturePan if (!self.delegate.qemuInputLegacy && !self.vmInput.serverModeCursor) { + [super touchesCancelled:touches withEvent:event]; [self dragCursor:UIGestureRecognizerStateEnded primary:YES secondary:YES middle:YES]; + [self resetMultitouchSequence]; + return; } [super touchesCancelled:touches withEvent:event]; } @@ -722,6 +1385,53 @@ - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { // release click in client mode, in server mode we handle in gesturePan if (!self.delegate.qemuInputLegacy && !self.vmInput.serverModeCursor) { + if (self.touchMouseType == VMMouseTypeMultitouch) { + NSUInteger remainingDirectTouches = [self activeDirectTouchCountForEvent:event currentTouches:touches]; + NSUInteger endedDirectTouches = 0; + for (UITouch *touch in touches) { + if (touch.type == UITouchTypeDirect) { + endedDirectTouches++; + } + } + if (endedDirectTouches >= self.multitouchActiveDirectTouchCount) { + self.multitouchActiveDirectTouchCount = 0; + } else { + self.multitouchActiveDirectTouchCount -= endedDirectTouches; + } + if (event.allTouches) { + remainingDirectTouches = MIN(remainingDirectTouches, self.multitouchActiveDirectTouchCount); + } else { + remainingDirectTouches = self.multitouchActiveDirectTouchCount; + } + [super touchesEnded:touches withEvent:event]; + if (self.multitouchTwoPanActionStarted && + (remainingDirectTouches == 0 || [self isTerminalGestureState:self.twoPan.state])) { + [self dragCursor:UIGestureRecognizerStateEnded gestureType:self.twoFingerPanType]; + self.multitouchTwoPanActionStarted = NO; + } + if (self.multitouchThreePanActionStarted && + (remainingDirectTouches == 0 || [self isTerminalGestureState:self.threePan.state])) { + [self dragCursor:UIGestureRecognizerStateEnded gestureType:self.threeFingerPanType]; + self.multitouchThreePanActionStarted = NO; + } + if (self.multitouchLongPressDragging && remainingDirectTouches == 0) { + [self dragCursor:UIGestureRecognizerStateEnded gestureType:self.longPressDragType]; + } + if (!self.multitouchLongPressRecognized && + !self.multitouchLongPressPending && + !self.multitouchLongPressDragging && + !self.multitouchTwoPanConsumed && + !self.multitouchThreePanConsumed) { + [self.scroll endMovementWithVelocity:self.multitouchScrollVelocity resistance:kScrollResistance]; + } + if (remainingDirectTouches == 0) { + [self dragCursor:UIGestureRecognizerStateEnded primary:YES secondary:YES middle:YES]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self resetMultitouchSequence]; + }); + } + return; + } [self dragCursor:UIGestureRecognizerStateEnded primary:YES secondary:YES middle:YES]; } [super touchesEnded:touches withEvent:event]; diff --git a/Platform/iOS/Display/VMDisplayMetalViewController.m b/Platform/iOS/Display/VMDisplayMetalViewController.m index b7bc6db91f..1f1c22850b 100644 --- a/Platform/iOS/Display/VMDisplayMetalViewController.m +++ b/Platform/iOS/Display/VMDisplayMetalViewController.m @@ -119,11 +119,18 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.prefersHomeIndicatorAutoHidden = YES; #if !TARGET_OS_VISION + [self setNeedsUpdateOfScreenEdgesDeferringSystemGestures]; [self startGCMouse]; #endif [self.vmDisplay addRenderer:self.renderer]; } +#if !TARGET_OS_VISION +- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures { + return UIRectEdgeAll; +} +#endif + - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; #if !TARGET_OS_VISION diff --git a/Platform/iOS/Display/VMDisplayViewController.swift b/Platform/iOS/Display/VMDisplayViewController.swift index 502df05245..46e940f6ab 100644 --- a/Platform/iOS/Display/VMDisplayViewController.swift +++ b/Platform/iOS/Display/VMDisplayViewController.swift @@ -43,6 +43,9 @@ public extension VMDisplayViewController { super.viewWillDisappear(animated) if let parent = parent { parent.setChildForHomeIndicatorAutoHidden(nil) + #if !os(visionOS) + parent.setChildForScreenEdgesDeferringSystemGestures(nil) + #endif parent.setChildViewControllerForPointerLock(nil) UIPress.pressResponderOverride = nil } @@ -52,6 +55,9 @@ public extension VMDisplayViewController { super.viewDidAppear(animated) if let parent = parent { parent.setChildForHomeIndicatorAutoHidden(self) + #if !os(visionOS) + parent.setChildForScreenEdgesDeferringSystemGestures(self) + #endif parent.setChildViewControllerForPointerLock(self) UIPress.pressResponderOverride = self } diff --git a/Platform/iOS/Settings.bundle/Root.plist b/Platform/iOS/Settings.bundle/Root.plist index 4dd7e42818..0f55ee043c 100644 --- a/Platform/iOS/Settings.bundle/Root.plist +++ b/Platform/iOS/Settings.bundle/Root.plist @@ -227,7 +227,7 @@ Platform iOS DefaultValue - 1 + 2 Titles Disabled @@ -241,6 +241,32 @@ 2 + + Type + PSMultiValueSpecifier + Title + Long Press + Drag + Key + GestureLongPressDrag + Platform + iOS + DefaultValue + 1 + Titles + + Disabled + Left Mouse Drag + Right Mouse Drag + Middle Mouse Drag + + Values + + 0 + 1 + 6 + 7 + + Type PSMultiValueSpecifier @@ -256,11 +282,13 @@ Disabled Right Click + Middle Click Values 0 2 + 5 @@ -273,13 +301,15 @@ Platform iOS DefaultValue - 3 + 6 Titles Disabled Move Screen - Click & Hold + Left Mouse Drag Mouse Wheel + Right Mouse Drag + Middle Mouse Drag Values @@ -287,6 +317,8 @@ 3 1 4 + 6 + 7 @@ -311,6 +343,52 @@ 4 + + Type + PSMultiValueSpecifier + Title + Two Finger Pinch + Key + GestureTwoPinch + Platform + iOS + DefaultValue + 0 + Titles + + Disabled + Scale Display + + Values + + 0 + 8 + + + + Type + PSMultiValueSpecifier + Title + Three Finger Tap + Key + GestureThreeTap + Platform + iOS + DefaultValue + 5 + Titles + + Disabled + Right Click + Middle Click + + Values + + 0 + 2 + 5 + + Type PSMultiValueSpecifier @@ -321,13 +399,15 @@ Platform iOS DefaultValue - 0 + 7 Titles Disabled Move Screen - Click & Hold + Left Mouse Drag Mouse Wheel + Right Mouse Drag + Middle Mouse Drag Values @@ -335,6 +415,30 @@ 3 1 4 + 6 + 7 + + + + Type + PSMultiValueSpecifier + Title + Three Finger Swipe + Key + GestureThreeSwipe + Platform + iOS + DefaultValue + 0 + Titles + + Disabled + Keyboard Reveal + + Values + + 0 + 1 @@ -359,12 +463,14 @@ Drag cursor Touch mode (always show cursor) Touch mode (try hiding cursor) + Multitouch mode Values 0 1 2 + 3 diff --git a/Platform/iOS/UTMPatches.swift b/Platform/iOS/UTMPatches.swift index fe6f8eb297..fc3bb65892 100644 --- a/Platform/iOS/UTMPatches.swift +++ b/Platform/iOS/UTMPatches.swift @@ -68,6 +68,23 @@ extension UIViewController { } setNeedsUpdateOfPrefersPointerLocked() } + + #if !os(visionOS) + private static var _utm__childForScreenEdgesDeferringSystemGesturesStorage: [UIViewController: UIViewController] = [:] + + @objc private dynamic var _utm__childForScreenEdgesDeferringSystemGestures: UIViewController? { + Self._utm__childForScreenEdgesDeferringSystemGesturesStorage[self] + } + + @objc dynamic func setChildForScreenEdgesDeferringSystemGestures(_ value: UIViewController?) { + if let value = value { + Self._utm__childForScreenEdgesDeferringSystemGesturesStorage[self] = value + } else { + Self._utm__childForScreenEdgesDeferringSystemGesturesStorage.removeValue(forKey: self) + } + setNeedsUpdateOfScreenEdgesDeferringSystemGestures() + } + #endif /// SwiftUI currently does not provide a way to set the View Conrtoller's home indicator or pointer lock fileprivate static func patchViewController() { @@ -77,6 +94,11 @@ extension UIViewController { patch(#selector(getter: Self.childViewControllerForPointerLock), with: #selector(getter: Self._utm__childViewControllerForPointerLock), class: Self.self) + #if !os(visionOS) + patch(#selector(getter: Self.childForScreenEdgesDeferringSystemGestures), + with: #selector(getter: Self._utm__childForScreenEdgesDeferringSystemGestures), + class: Self.self) + #endif } }