709 lines
27 KiB
Objective-C
709 lines
27 KiB
Objective-C
//
|
|
// Copyright © 2020 osy. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
#import "UIViewController+Extensions.h"
|
|
#import "VMDisplayMetalViewController.h"
|
|
#import "VMDisplayMetalViewController+Touch.h"
|
|
#import "VMDisplayMetalViewController+Pencil.h"
|
|
#import "VMCursor.h"
|
|
#import "VMScroll.h"
|
|
#import "CSDisplay.h"
|
|
#import "UTMQemuConfiguration.h"
|
|
#import "UTMQemuConfiguration+Miscellaneous.h"
|
|
#import "UTMSpiceIO.h"
|
|
#import "UTMLogging.h"
|
|
#import "UTMQemuVirtualMachine.h"
|
|
#import "UTMQemuVirtualMachine+SPICE.h"
|
|
#import "UTM-Swift.h"
|
|
|
|
const CGFloat kScrollSpeedReduction = 100.0f;
|
|
const CGFloat kCursorResistance = 50.0f;
|
|
const CGFloat kScrollResistance = 10.0f;
|
|
|
|
@implementation VMDisplayMetalViewController (Gestures)
|
|
|
|
- (void)initTouch {
|
|
// mouse cursor
|
|
_cursor = [[VMCursor alloc] initWithVMViewController:self];
|
|
_scroll = [[VMScroll alloc] initWithVMViewController:self];
|
|
|
|
// Set up gesture recognizers because Storyboards is BROKEN and doing it there crashes!
|
|
_swipeUp = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(gestureSwipeUp:)];
|
|
_swipeUp.numberOfTouchesRequired = 3;
|
|
_swipeUp.direction = UISwipeGestureRecognizerDirectionUp;
|
|
_swipeUp.delegate = self;
|
|
_swipeDown = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(gestureSwipeDown:)];
|
|
_swipeDown.numberOfTouchesRequired = 3;
|
|
_swipeDown.direction = UISwipeGestureRecognizerDirectionDown;
|
|
_swipeDown.delegate = self;
|
|
_swipeScrollUp = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(gestureSwipeScroll:)];
|
|
_swipeScrollUp.numberOfTouchesRequired = 2;
|
|
_swipeScrollUp.direction = UISwipeGestureRecognizerDirectionUp;
|
|
_swipeScrollUp.delegate = self;
|
|
_swipeScrollDown = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(gestureSwipeScroll:)];
|
|
_swipeScrollDown.numberOfTouchesRequired = 2;
|
|
_swipeScrollDown.direction = UISwipeGestureRecognizerDirectionDown;
|
|
_swipeScrollDown.delegate = self;
|
|
_pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(gesturePan:)];
|
|
_pan.minimumNumberOfTouches = 1;
|
|
_pan.maximumNumberOfTouches = 1;
|
|
_pan.delegate = self;
|
|
_pan.cancelsTouchesInView = NO;
|
|
_twoPan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(gestureTwoPan:)];
|
|
_twoPan.minimumNumberOfTouches = 2;
|
|
_twoPan.maximumNumberOfTouches = 2;
|
|
_twoPan.delegate = self;
|
|
_threePan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(gestureThreePan:)];
|
|
_threePan.minimumNumberOfTouches = 3;
|
|
_threePan.maximumNumberOfTouches = 3;
|
|
_threePan.delegate = self;
|
|
_tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(gestureTap:)];
|
|
_tap.delegate = self;
|
|
_tap.allowedTouchTypes = @[ @(UITouchTypeDirect) ];
|
|
_tap.cancelsTouchesInView = NO;
|
|
_tapPencil = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(gestureTapPencil:)];
|
|
_tapPencil.delegate = self;
|
|
_tapPencil.allowedTouchTypes = @[ @(UITouchTypePencil) ];
|
|
_tapPencil.cancelsTouchesInView = NO;
|
|
_twoTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(gestureTwoTap:)];
|
|
_twoTap.numberOfTouchesRequired = 2;
|
|
_twoTap.delegate = self;
|
|
_twoTap.allowedTouchTypes = @[ @(UITouchTypeDirect) ];
|
|
_longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(gestureLongPress:)];
|
|
_longPress.delegate = self;
|
|
_longPress.allowedTouchTypes = @[ @(UITouchTypeDirect) ];
|
|
_pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(gesturePinch:)];
|
|
_pinch.delegate = self;
|
|
[self.mtkView addGestureRecognizer:_swipeUp];
|
|
[self.mtkView addGestureRecognizer:_swipeDown];
|
|
[self.mtkView addGestureRecognizer:_swipeScrollUp];
|
|
[self.mtkView addGestureRecognizer:_swipeScrollDown];
|
|
[self.mtkView addGestureRecognizer:_pan];
|
|
[self.mtkView addGestureRecognizer:_twoPan];
|
|
[self.mtkView addGestureRecognizer:_threePan];
|
|
[self.mtkView addGestureRecognizer:_tap];
|
|
[self.mtkView addGestureRecognizer:_tapPencil];
|
|
[self.mtkView addGestureRecognizer:_twoTap];
|
|
[self.mtkView addGestureRecognizer:_longPress];
|
|
[self.mtkView addGestureRecognizer:_pinch];
|
|
|
|
// Feedback generator for clicks
|
|
_clickFeedbackGenerator = [[UISelectionFeedbackGenerator alloc] init];
|
|
_resizeFeedbackGenerator = [[UIImpactFeedbackGenerator alloc] init];
|
|
}
|
|
|
|
#pragma mark - Properties from instance
|
|
|
|
- (CSInputButton)mouseButtonDown {
|
|
CSInputButton button = kCSInputButtonNone;
|
|
if (_mouseLeftDown) {
|
|
button |= kCSInputButtonLeft;
|
|
}
|
|
if (_mouseRightDown) {
|
|
button |= kCSInputButtonRight;
|
|
}
|
|
if (_mouseMiddleDown) {
|
|
button |= kCSInputButtonMiddle;
|
|
}
|
|
return button;
|
|
}
|
|
|
|
#pragma mark - Properties from settings
|
|
|
|
- (VMGestureType)gestureTypeForSetting:(NSString *)key {
|
|
NSInteger integer = [self integerForSetting:key];
|
|
if (integer < VMGestureTypeNone || integer >= VMGestureTypeMax) {
|
|
return VMGestureTypeNone;
|
|
} else {
|
|
return (VMGestureType)integer;
|
|
}
|
|
}
|
|
|
|
- (VMGestureType)longPressType {
|
|
return [self gestureTypeForSetting:@"GestureLongPress"];
|
|
}
|
|
|
|
- (VMGestureType)twoFingerTapType {
|
|
return [self gestureTypeForSetting:@"GestureTwoTap"];
|
|
}
|
|
|
|
- (VMGestureType)twoFingerPanType {
|
|
return [self gestureTypeForSetting:@"GestureTwoPan"];
|
|
}
|
|
|
|
- (VMGestureType)twoFingerScrollType {
|
|
return [self gestureTypeForSetting:@"GestureTwoScroll"];
|
|
}
|
|
|
|
- (VMGestureType)threeFingerPanType {
|
|
return [self gestureTypeForSetting:@"GestureThreePan"];
|
|
}
|
|
|
|
- (VMMouseType)mouseTypeForSetting:(NSString *)key {
|
|
NSInteger integer = [self integerForSetting:key];
|
|
if (integer < VMMouseTypeRelative || integer >= VMMouseTypeMax) {
|
|
return VMMouseTypeRelative;
|
|
} else {
|
|
return (VMMouseType)integer;
|
|
}
|
|
}
|
|
|
|
- (VMMouseType)touchMouseType {
|
|
return [self mouseTypeForSetting:@"MouseTouchType"];
|
|
}
|
|
|
|
- (VMMouseType)pencilMouseType {
|
|
return [self mouseTypeForSetting:@"MousePencilType"];
|
|
}
|
|
|
|
- (VMMouseType)indirectMouseType {
|
|
if (@available(iOS 14.0, *)) {
|
|
return VMMouseTypeRelative;
|
|
} else {
|
|
return VMMouseTypeAbsolute; // legacy iOS 13.4 mouse handling requires absolute
|
|
}
|
|
}
|
|
|
|
#pragma mark - Converting view points to VM display points
|
|
|
|
static CGRect CGRectClipToBounds(CGRect rect1, CGRect rect2) {
|
|
if (rect2.origin.x < rect1.origin.x) {
|
|
rect2.origin.x = rect1.origin.x;
|
|
} else if (rect2.origin.x + rect2.size.width > rect1.origin.x + rect1.size.width) {
|
|
rect2.origin.x = rect1.origin.x + rect1.size.width - rect2.size.width;
|
|
}
|
|
if (rect2.origin.y < rect1.origin.y) {
|
|
rect2.origin.y = rect1.origin.y;
|
|
} else if (rect2.origin.y + rect2.size.height > rect1.origin.y + rect1.size.height) {
|
|
rect2.origin.y = rect1.origin.y + rect1.size.height - rect2.size.height;
|
|
}
|
|
return rect2;
|
|
}
|
|
|
|
static CGFloat CGPointToPixel(CGFloat point) {
|
|
return point * [UIScreen mainScreen].scale; // FIXME: multiple screens?
|
|
}
|
|
|
|
- (CGPoint)clipCursorToDisplay:(CGPoint)pos {
|
|
CGSize screenSize = self.mtkView.drawableSize;
|
|
CGSize scaledSize = {
|
|
self.vmDisplay.displaySize.width * self.vmDisplay.viewportScale,
|
|
self.vmDisplay.displaySize.height * self.vmDisplay.viewportScale
|
|
};
|
|
CGRect drawRect = CGRectMake(
|
|
self.vmDisplay.viewportOrigin.x + screenSize.width/2 - scaledSize.width/2,
|
|
self.vmDisplay.viewportOrigin.y + screenSize.height/2 - scaledSize.height/2,
|
|
scaledSize.width,
|
|
scaledSize.height
|
|
);
|
|
pos.x -= drawRect.origin.x;
|
|
pos.y -= drawRect.origin.y;
|
|
if (pos.x < 0) {
|
|
pos.x = 0;
|
|
} else if (pos.x > scaledSize.width) {
|
|
pos.x = scaledSize.width;
|
|
}
|
|
if (pos.y < 0) {
|
|
pos.y = 0;
|
|
} else if (pos.y > scaledSize.height) {
|
|
pos.y = scaledSize.height;
|
|
}
|
|
pos.x /= self.vmDisplay.viewportScale;
|
|
pos.y /= self.vmDisplay.viewportScale;
|
|
return pos;
|
|
}
|
|
|
|
- (CGPoint)clipDisplayToView:(CGPoint)target {
|
|
CGSize screenSize = self.mtkView.drawableSize;
|
|
CGSize scaledSize = {
|
|
self.vmDisplay.displaySize.width * self.vmDisplay.viewportScale,
|
|
self.vmDisplay.displaySize.height * self.vmDisplay.viewportScale
|
|
};
|
|
CGRect drawRect = CGRectMake(
|
|
target.x + screenSize.width/2 - scaledSize.width/2,
|
|
target.y + screenSize.height/2 - scaledSize.height/2,
|
|
scaledSize.width,
|
|
scaledSize.height
|
|
);
|
|
CGRect boundRect = {
|
|
{
|
|
screenSize.width - MAX(screenSize.width, scaledSize.width),
|
|
screenSize.height - MAX(screenSize.height, scaledSize.height)
|
|
|
|
},
|
|
{
|
|
2*MAX(screenSize.width, scaledSize.width) - screenSize.width,
|
|
2*MAX(screenSize.height, scaledSize.height) - screenSize.height
|
|
}
|
|
};
|
|
CGRect clippedRect = CGRectClipToBounds(boundRect, drawRect);
|
|
clippedRect.origin.x -= (screenSize.width/2 - scaledSize.width/2);
|
|
clippedRect.origin.y -= (screenSize.height/2 - scaledSize.height/2);
|
|
return CGPointMake(clippedRect.origin.x, clippedRect.origin.y);
|
|
}
|
|
|
|
#pragma mark - Gestures
|
|
|
|
- (void)moveMouseWithInertia:(UIPanGestureRecognizer *)sender {
|
|
CGPoint location = [sender locationInView:sender.view];
|
|
CGPoint velocity = [sender velocityInView:sender.view];
|
|
if (sender.state == UIGestureRecognizerStateBegan) {
|
|
[_cursor startMovement:location];
|
|
}
|
|
if (sender.state != UIGestureRecognizerStateCancelled) {
|
|
[_cursor updateMovement:location];
|
|
}
|
|
if (sender.state == UIGestureRecognizerStateEnded) {
|
|
[_cursor endMovementWithVelocity:velocity resistance:kCursorResistance];
|
|
}
|
|
}
|
|
|
|
- (void)scrollWithInertia:(UIPanGestureRecognizer *)sender {
|
|
CGPoint location = [sender locationInView:sender.view];
|
|
CGPoint velocity = [sender velocityInView:sender.view];
|
|
if (sender.state == UIGestureRecognizerStateBegan) {
|
|
[_scroll startMovement:location];
|
|
}
|
|
if (sender.state != UIGestureRecognizerStateCancelled) {
|
|
[_scroll updateMovement:location];
|
|
}
|
|
if (sender.state == UIGestureRecognizerStateEnded) {
|
|
[_scroll endMovementWithVelocity:velocity resistance:kScrollResistance];
|
|
}
|
|
}
|
|
|
|
- (IBAction)gesturePan:(UIPanGestureRecognizer *)sender {
|
|
if (self.serverModeCursor) { // otherwise we handle in touchesMoved
|
|
[self moveMouseWithInertia:sender];
|
|
}
|
|
}
|
|
|
|
- (void)moveScreen:(UIPanGestureRecognizer *)sender {
|
|
if (sender.state == UIGestureRecognizerStateBegan) {
|
|
_lastTwoPanOrigin = self.vmDisplay.viewportOrigin;
|
|
}
|
|
if (sender.state != UIGestureRecognizerStateCancelled) {
|
|
CGPoint translation = [sender translationInView:sender.view];
|
|
CGPoint viewport = self.vmDisplay.viewportOrigin;
|
|
viewport.x = CGPointToPixel(translation.x) + _lastTwoPanOrigin.x;
|
|
viewport.y = CGPointToPixel(translation.y) + _lastTwoPanOrigin.y;
|
|
self.vmDisplay.viewportOrigin = [self clipDisplayToView:viewport];
|
|
// reset the resize toolbar icon
|
|
self.toolbar.isViewportChanged = YES;
|
|
// persist this change in viewState
|
|
self.vm.viewState.displayOriginX = self.vmDisplay.viewportOrigin.x;
|
|
self.vm.viewState.displayOriginY = self.vmDisplay.viewportOrigin.y;
|
|
}
|
|
if (sender.state == UIGestureRecognizerStateEnded) {
|
|
// TODO: decelerate
|
|
}
|
|
}
|
|
|
|
- (IBAction)gestureTwoPan:(UIPanGestureRecognizer *)sender {
|
|
switch (self.twoFingerPanType) {
|
|
case VMGestureTypeMoveScreen:
|
|
[self moveScreen:sender];
|
|
break;
|
|
case VMGestureTypeDragCursor:
|
|
[self dragCursor:sender.state primary:YES secondary:NO middle:NO];
|
|
[self moveMouseWithInertia:sender];
|
|
break;
|
|
case VMGestureTypeMouseWheel:
|
|
[self scrollWithInertia:sender];
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
- (IBAction)gestureThreePan:(UIPanGestureRecognizer *)sender {
|
|
switch (self.threeFingerPanType) {
|
|
case VMGestureTypeMoveScreen:
|
|
[self moveScreen:sender];
|
|
break;
|
|
case VMGestureTypeDragCursor:
|
|
[self dragCursor:sender.state primary:YES secondary:NO middle:NO];
|
|
[self moveMouseWithInertia:sender];
|
|
break;
|
|
case VMGestureTypeMouseWheel:
|
|
[self scrollWithInertia:sender];
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
- (CGPoint)moveMouseAbsolute:(CGPoint)location {
|
|
CGPoint translated = location;
|
|
translated.x = CGPointToPixel(translated.x);
|
|
translated.y = CGPointToPixel(translated.y);
|
|
translated = [self clipCursorToDisplay:translated];
|
|
if (!self.vmInput.serverModeCursor) {
|
|
[self.vmInput sendMousePosition:self.mouseButtonDown absolutePoint:translated];
|
|
[self.vmDisplay.cursor moveTo:translated]; // required to show cursor on screen
|
|
} else {
|
|
UTMLog(@"Warning: ignored mouse set (%f, %f) while mouse is in server mode", translated.x, translated.y);
|
|
}
|
|
return translated;
|
|
}
|
|
|
|
- (CGPoint)moveMouseRelative:(CGPoint)translation {
|
|
translation.x = CGPointToPixel(translation.x) / self.vmDisplay.viewportScale;
|
|
translation.y = CGPointToPixel(translation.y) / self.vmDisplay.viewportScale;
|
|
if (self.vmInput.serverModeCursor) {
|
|
[self.vmInput sendMouseMotion:self.mouseButtonDown relativePoint:translation];
|
|
} else {
|
|
UTMLog(@"Warning: ignored mouse motion (%f, %f) while mouse is in client mode", translation.x, translation.y);
|
|
}
|
|
return translation;
|
|
}
|
|
|
|
- (CGPoint)moveMouseScroll:(CGPoint)translation {
|
|
translation.y = CGPointToPixel(translation.y) / kScrollSpeedReduction;
|
|
if (self.vmQemuConfig.inputScrollInvert) {
|
|
translation.y = -translation.y;
|
|
}
|
|
[self.vmInput sendMouseScroll:kCSInputScrollSmooth button:self.mouseButtonDown dy:translation.y];
|
|
return translation;
|
|
}
|
|
|
|
- (void)mouseClick:(CSInputButton)button location:(CGPoint)location {
|
|
if (!self.serverModeCursor) {
|
|
_cursor.center = location;
|
|
}
|
|
[self.vmInput sendMouseButton:button pressed:YES];
|
|
[self onDelay:0.05f action:^{
|
|
self->_mouseLeftDown = NO;
|
|
self->_mouseRightDown = NO;
|
|
self->_mouseMiddleDown = NO;
|
|
[self.vmInput sendMouseButton:button pressed:NO];
|
|
}];
|
|
[_clickFeedbackGenerator selectionChanged];
|
|
}
|
|
|
|
- (void)dragCursor:(UIGestureRecognizerState)state primary:(BOOL)primary secondary:(BOOL)secondary middle:(BOOL)middle {
|
|
if (state == UIGestureRecognizerStateBegan) {
|
|
[_clickFeedbackGenerator selectionChanged];
|
|
if (primary) {
|
|
_mouseLeftDown = YES;
|
|
}
|
|
if (secondary) {
|
|
_mouseRightDown = YES;
|
|
}
|
|
if (middle) {
|
|
_mouseMiddleDown = YES;
|
|
}
|
|
[self.vmInput sendMouseButton:self.mouseButtonDown pressed:YES];
|
|
} else if (state == UIGestureRecognizerStateEnded) {
|
|
_mouseLeftDown = NO;
|
|
_mouseRightDown = NO;
|
|
_mouseMiddleDown = NO;
|
|
[self.vmInput sendMouseButton:self.mouseButtonDown pressed:NO];
|
|
}
|
|
}
|
|
|
|
- (IBAction)gestureTap:(UITapGestureRecognizer *)sender {
|
|
if (sender.state == UIGestureRecognizerStateEnded &&
|
|
self.serverModeCursor) { // otherwise we handle in touchesBegan
|
|
[self mouseClick:kCSInputButtonLeft location:[sender locationInView:sender.view]];
|
|
}
|
|
}
|
|
|
|
- (IBAction)gestureTapPencil:(UITapGestureRecognizer *)sender {
|
|
if (sender.state == UIGestureRecognizerStateEnded &&
|
|
self.serverModeCursor) { // otherwise we handle in touchesBegan
|
|
|
|
CSInputButton button = kCSInputButtonLeft;
|
|
|
|
if (@available(iOS 12.1, *)) {
|
|
if (_pencilForceRightClickOnce) {
|
|
button = kCSInputButtonRight;
|
|
_pencilForceRightClickOnce = false;
|
|
}
|
|
}
|
|
|
|
[self mouseClick:button location:[sender locationInView:sender.view]];
|
|
}
|
|
}
|
|
|
|
- (IBAction)gestureTwoTap:(UITapGestureRecognizer *)sender {
|
|
if (sender.state == UIGestureRecognizerStateEnded &&
|
|
self.twoFingerTapType == VMGestureTypeRightClick) {
|
|
[self mouseClick:kCSInputButtonRight location:[sender locationInView:sender.view]];
|
|
}
|
|
}
|
|
|
|
- (IBAction)gestureLongPress:(UILongPressGestureRecognizer *)sender {
|
|
if (sender.state == UIGestureRecognizerStateEnded &&
|
|
self.longPressType == VMGestureTypeRightClick) {
|
|
[self mouseClick:kCSInputButtonRight location:[sender locationInView:sender.view]];
|
|
} else if (self.longPressType == VMGestureTypeDragCursor) {
|
|
[self dragCursor:sender.state primary:YES secondary:NO middle:NO];
|
|
}
|
|
}
|
|
|
|
- (IBAction)gesturePinch:(UIPinchGestureRecognizer *)sender {
|
|
// disable pinch if move screen on pan is disabled
|
|
if (!(self.twoFingerPanType == VMGestureTypeMoveScreen || self.threeFingerPanType == VMGestureTypeMoveScreen)) {
|
|
return;
|
|
}
|
|
if (sender.state == UIGestureRecognizerStateBegan ||
|
|
sender.state == UIGestureRecognizerStateChanged ||
|
|
sender.state == UIGestureRecognizerStateEnded) {
|
|
NSAssert(sender.scale > 0, @"sender.scale cannot be 0");
|
|
self.vmDisplay.viewportScale *= sender.scale;
|
|
// reset the resize toolbar icon
|
|
self.toolbar.isViewportChanged = YES;
|
|
// persist this change in viewState
|
|
self.vm.viewState.displayScale = self.vmDisplay.viewportScale;
|
|
sender.scale = 1.0;
|
|
}
|
|
}
|
|
|
|
- (IBAction)gestureSwipeUp:(UISwipeGestureRecognizer *)sender {
|
|
if (sender.state == UIGestureRecognizerStateEnded) {
|
|
if (self.toolbar.hasLegacyToolbar && self.toolbar.isLegacyToolbarVisible) {
|
|
[self.toolbar hideLegacyToolbar];
|
|
} else if (!self.keyboardVisible) {
|
|
self.keyboardVisible = YES;
|
|
}
|
|
}
|
|
}
|
|
|
|
- (IBAction)gestureSwipeDown:(UISwipeGestureRecognizer *)sender {
|
|
if (sender.state == UIGestureRecognizerStateEnded) {
|
|
if (self.keyboardVisible) {
|
|
self.keyboardVisible = NO;
|
|
} else if (self.toolbar.hasLegacyToolbar && !self.toolbar.isLegacyToolbarVisible) {
|
|
[self.toolbar showLegacyToolbar];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (IBAction)gestureSwipeScroll:(UISwipeGestureRecognizer *)sender {
|
|
if (sender.state == UIGestureRecognizerStateEnded &&
|
|
self.twoFingerScrollType == VMGestureTypeMouseWheel) {
|
|
if (sender == _swipeScrollUp) {
|
|
[self.vmInput sendMouseScroll:kCSInputScrollUp button:self.mouseButtonDown dy:0];
|
|
} else if (sender == _swipeScrollDown) {
|
|
[self.vmInput sendMouseScroll:kCSInputScrollDown button:self.mouseButtonDown dy:0];
|
|
} else {
|
|
NSAssert(0, @"Invalid call to gestureSwipeScroll");
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark - UIGestureRecognizerDelegate
|
|
|
|
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
|
|
if (gestureRecognizer == _twoPan && otherGestureRecognizer == _swipeUp) {
|
|
return YES;
|
|
}
|
|
if (gestureRecognizer == _twoPan && otherGestureRecognizer == _swipeDown) {
|
|
return YES;
|
|
}
|
|
if (gestureRecognizer == _twoTap && otherGestureRecognizer == _swipeDown) {
|
|
return YES;
|
|
}
|
|
if (gestureRecognizer == _twoTap && otherGestureRecognizer == _swipeUp) {
|
|
return YES;
|
|
}
|
|
if (gestureRecognizer == _tap && otherGestureRecognizer == _twoTap) {
|
|
return YES;
|
|
}
|
|
if (gestureRecognizer == _tapPencil && otherGestureRecognizer == _twoTap) {
|
|
return YES;
|
|
}
|
|
if (gestureRecognizer == _longPress && otherGestureRecognizer == _tap) {
|
|
return YES;
|
|
}
|
|
if (gestureRecognizer == _longPress && otherGestureRecognizer == _tapPencil) {
|
|
return YES;
|
|
}
|
|
if (gestureRecognizer == _longPress && otherGestureRecognizer == _twoTap) {
|
|
return YES;
|
|
}
|
|
if (gestureRecognizer == _pinch && otherGestureRecognizer == _swipeDown) {
|
|
return YES;
|
|
}
|
|
if (gestureRecognizer == _pinch && otherGestureRecognizer == _swipeUp) {
|
|
return YES;
|
|
}
|
|
if (gestureRecognizer == _pan && otherGestureRecognizer == _swipeUp) {
|
|
return YES;
|
|
}
|
|
if (gestureRecognizer == _pan && otherGestureRecognizer == _swipeDown) {
|
|
return YES;
|
|
}
|
|
if (gestureRecognizer == _threePan && otherGestureRecognizer == _swipeUp) {
|
|
return YES;
|
|
}
|
|
if (gestureRecognizer == _threePan && otherGestureRecognizer == _swipeDown) {
|
|
return YES;
|
|
}
|
|
// only if we do not disable two finger swipe
|
|
if (self.twoFingerScrollType != VMGestureTypeNone) {
|
|
if (gestureRecognizer == _twoPan && otherGestureRecognizer == _swipeScrollUp) {
|
|
return YES;
|
|
}
|
|
if (gestureRecognizer == _twoPan && otherGestureRecognizer == _swipeScrollDown) {
|
|
return YES;
|
|
}
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
|
|
if (gestureRecognizer == _twoPan && otherGestureRecognizer == _pinch) {
|
|
if (self.twoFingerPanType == VMGestureTypeMoveScreen) {
|
|
return YES;
|
|
} else {
|
|
return NO;
|
|
}
|
|
} else if (gestureRecognizer == _pan && otherGestureRecognizer == _longPress) {
|
|
return YES;
|
|
} else if (self.twoFingerScrollType == VMGestureTypeNone && otherGestureRecognizer == _twoPan) {
|
|
// if two finger swipe is disabled, we can also recognize two finger pans
|
|
if (gestureRecognizer == _swipeScrollUp) {
|
|
return YES;
|
|
} else if (gestureRecognizer == _swipeScrollDown) {
|
|
return YES;
|
|
} else {
|
|
return NO;
|
|
}
|
|
} else {
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveEvent:(UIEvent *)event API_AVAILABLE(ios(13.4)) {
|
|
if (event.type == UIEventTypeTransform) {
|
|
UTMLog(@"ignoring UIEventTypeTransform");
|
|
return NO;
|
|
} else {
|
|
return YES;
|
|
}
|
|
}
|
|
|
|
#pragma mark - Touch type
|
|
|
|
- (VMMouseType)touchTypeToMouseType:(UITouchType)type {
|
|
switch (type) {
|
|
case UITouchTypeDirect: {
|
|
return self.touchMouseType;
|
|
}
|
|
case UITouchTypePencil: {
|
|
return self.pencilMouseType;
|
|
}
|
|
case UITouchTypeIndirect: {
|
|
return self.indirectMouseType;
|
|
}
|
|
default: {
|
|
if (@available(iOS 13.4, *)) {
|
|
if (type == UITouchTypeIndirectPointer) {
|
|
return self.indirectMouseType;
|
|
}
|
|
}
|
|
return self.touchMouseType; // compatibility with future values
|
|
}
|
|
}
|
|
}
|
|
|
|
- (BOOL)switchMouseType:(VMMouseType)type {
|
|
BOOL shouldHideCursor = (type == VMMouseTypeAbsoluteHideCursor);
|
|
BOOL shouldUseServerMouse = (type == VMMouseTypeRelative);
|
|
self.vmDisplay.cursor.isInhibited = shouldHideCursor;
|
|
if (shouldUseServerMouse != self.vmInput.serverModeCursor) {
|
|
UTMLog(@"Switching mouse mode to server:%d for type:%ld", shouldUseServerMouse, type);
|
|
[self.vm requestInputTablet:!shouldUseServerMouse];
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
#pragma mark - Touch event handling
|
|
|
|
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
|
if (!self.vmQemuConfig.inputLegacy) {
|
|
for (UITouch *touch in touches) {
|
|
if (@available(iOS 14, *)) {
|
|
if (self.prefersPointerLocked && (touch.type == UITouchTypeIndirect || touch.type == UITouchTypeIndirectPointer)) {
|
|
continue; // skip indirect touches if we are capturing mouse input
|
|
}
|
|
}
|
|
VMMouseType type = [self touchTypeToMouseType:touch.type];
|
|
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
|
|
BOOL primary = YES;
|
|
BOOL secondary = NO;
|
|
BOOL middle = NO;
|
|
CGPoint pos = [touch locationInView:self.mtkView];
|
|
// iOS 13.4+ Pointing device support
|
|
if (@available(iOS 13.4, *)) {
|
|
if (touch.type == UITouchTypeIndirectPointer) {
|
|
primary = (event.buttonMask & UIEventButtonMaskPrimary) != 0;
|
|
secondary = (event.buttonMask & UIEventButtonMaskSecondary) != 0;
|
|
middle = (event.buttonMask & 0x4) != 0; // undocumented mask
|
|
}
|
|
}
|
|
// Apple Pencil 2 right click mode
|
|
if (@available(iOS 12.1, *)) {
|
|
if (touch.type == UITouchTypePencil) {
|
|
primary = !_pencilForceRightClickOnce;
|
|
secondary = _pencilForceRightClickOnce;
|
|
_pencilForceRightClickOnce = false;
|
|
}
|
|
}
|
|
[_cursor startMovement:pos];
|
|
[_cursor updateMovement:pos];
|
|
[self dragCursor:UIGestureRecognizerStateBegan primary:primary secondary:secondary middle:middle];
|
|
}
|
|
break; // handle a single touch only
|
|
}
|
|
}
|
|
[super touchesBegan:touches withEvent:event];
|
|
}
|
|
|
|
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
|
// move cursor in client mode, in server mode we handle in gesturePan
|
|
if (!self.vmQemuConfig.inputLegacy && !self.vmInput.serverModeCursor) {
|
|
for (UITouch *touch in touches) {
|
|
[_cursor updateMovement:[touch locationInView:self.mtkView]];
|
|
break; // handle single touch
|
|
}
|
|
}
|
|
[super touchesMoved:touches withEvent:event];
|
|
}
|
|
|
|
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
|
// release click in client mode, in server mode we handle in gesturePan
|
|
if (!self.vmQemuConfig.inputLegacy && !self.vmInput.serverModeCursor) {
|
|
[self dragCursor:UIGestureRecognizerStateEnded primary:YES secondary:YES middle:YES];
|
|
}
|
|
[super touchesCancelled:touches withEvent:event];
|
|
}
|
|
|
|
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
|
// release click in client mode, in server mode we handle in gesturePan
|
|
if (!self.vmQemuConfig.inputLegacy && !self.vmInput.serverModeCursor) {
|
|
[self dragCursor:UIGestureRecognizerStateEnded primary:YES secondary:YES middle:YES];
|
|
}
|
|
[super touchesEnded:touches withEvent:event];
|
|
}
|
|
|
|
@end
|