// // 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 "VMDisplayMetalViewController.h" #import "VMDisplayMetalViewController+Private.h" #import "VMDisplayMetalViewController+Touch.h" #import "VMDisplayMetalViewController+Pointer.h" #import "VMCursor.h" #import "CSDisplay.h" #import "VMScroll.h" #import "UTMLogging.h" #import "UTM-Swift.h" @interface VMDisplayMetalViewController () - (BOOL)switchMouseType:(VMMouseType)type; // defined in VMDisplayMetalViewController+Touch.m @end NS_AVAILABLE_IOS(13.4) @implementation VMDisplayMetalViewController (Pointer) #pragma mark - GCMouse - (void)startGCMouse { if (@available(iOS 14.0, *)) { //if ios 14.0 above, use CGMouse instead [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(mouseDidBecomeCurrent:) name:GCMouseDidBecomeCurrentNotification object:nil]; [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(mouseDidStopBeingCurrent:) name:GCMouseDidStopBeingCurrentNotification object:nil]; GCMouse *current = GCMouse.current; if (current) { // send the current mouse if already connected [NSNotificationCenter.defaultCenter postNotificationName:GCMouseDidBecomeCurrentNotification object:current]; } } } - (void)stopGCMouse { GCMouse *current = GCMouse.current; [NSNotificationCenter.defaultCenter removeObserver:self name:GCMouseDidBecomeCurrentNotification object:nil]; if (current) { // send the current mouse if already connected [NSNotificationCenter.defaultCenter postNotificationName:GCMouseDidStopBeingCurrentNotification object:current]; } [NSNotificationCenter.defaultCenter removeObserver:self name:GCMouseDidStopBeingCurrentNotification object:nil]; } - (void)mouseDidBecomeCurrent:(NSNotification *)notification API_AVAILABLE(ios(14)) { GCMouse *mouse = notification.object; UTMLog(@"mouseDidBecomeCurrent: %p", mouse); if (!mouse) { UTMLog(@"invalid mouse object!"); return; } mouse.mouseInput.mouseMovedHandler = ^(GCMouseInput * _Nonnull mouse, float deltaX, float deltaY) { [self switchMouseType:VMMouseTypeRelative]; [self.vmInput sendMouseMotion:self.mouseButtonDown relativePoint:CGPointMake(deltaX, -deltaY)]; }; mouse.mouseInput.leftButton.pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) { self.mouseLeftDown = pressed; [self.vmInput sendMouseButton:kCSInputButtonLeft mask:self.mouseButtonDown pressed:pressed]; }; mouse.mouseInput.rightButton.pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) { self.mouseRightDown = pressed; [self.vmInput sendMouseButton:kCSInputButtonRight mask:self.mouseButtonDown pressed:pressed]; }; mouse.mouseInput.middleButton.pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) { self.mouseMiddleDown = pressed; [self.vmInput sendMouseButton:kCSInputButtonMiddle mask:self.mouseButtonDown pressed:pressed]; }; for (int i = 0; i < MIN(2, mouse.mouseInput.auxiliaryButtons.count); i++) { mouse.mouseInput.auxiliaryButtons[i].pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) { switch (i) { case 0: self.mouseSideDown = pressed; [self.vmInput sendMouseButton:kCSInputButtonSide mask:self.mouseButtonDown pressed:pressed]; break; case 1: self.mouseExtraDown = pressed; [self.vmInput sendMouseButton:kCSInputButtonExtra mask:self.mouseButtonDown pressed:pressed]; break; default: break; } }; } // no handler to the gcmouse scroll event, gestureScroll works fine. } - (void)mouseDidStopBeingCurrent:(NSNotification *)notification API_AVAILABLE(ios(14)) { GCMouse *mouse = notification.object; UTMLog(@"mouseDidStopBeingCurrent: %p", mouse); mouse.mouseInput.mouseMovedHandler = nil; mouse.mouseInput.leftButton.pressedChangedHandler = nil; mouse.mouseInput.rightButton.pressedChangedHandler = nil; mouse.mouseInput.middleButton.pressedChangedHandler = nil; for (int i = 0; i < MIN(4, mouse.mouseInput.auxiliaryButtons.count); i++) { mouse.mouseInput.auxiliaryButtons[i].pressedChangedHandler = nil; } } #pragma mark - UIPointerInteractionDelegate // Add pointer interaction to VM view -(void)initPointerInteraction { [self.mtkView addInteraction:[[UIPointerInteraction alloc] initWithDelegate:self]]; if (@available(iOS 13.4, *)) { UIPanGestureRecognizer *scroll = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(gestureScroll:)]; scroll.allowedScrollTypesMask = UIScrollTypeMaskAll; scroll.minimumNumberOfTouches = 0; scroll.maximumNumberOfTouches = 0; [self.mtkView addGestureRecognizer:scroll]; } } - (BOOL)hasTouchpadPointer { return !self.delegate.qemuInputLegacy && !self.vmInput.serverModeCursor && self.indirectMouseType != VMMouseTypeRelative; } - (UIPointerStyle *)pointerInteraction:(UIPointerInteraction *)interaction styleForRegion:(UIPointerRegion *)region { // Hide cursor while hovering in VM view if (interaction.view == self.mtkView && self.hasTouchpadPointer) { #if TARGET_OS_VISION return nil; // FIXME: hidden pointer seems to jump around due to following gaze #else return [UIPointerStyle hiddenPointerStyle]; #endif } return nil; } - (bool)isPointOnVMDisplay:(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; return 0 <= pos.x && pos.x <= scaledSize.width && 0 <= pos.y && pos.y <= scaledSize.height; } - (UIPointerRegion *)pointerInteraction:(UIPointerInteraction *)interaction regionForRequest:(UIPointerRegionRequest *)request defaultRegion:(UIPointerRegion *)defaultRegion { #if !TARGET_OS_VISION if (@available(iOS 14.0, *)) { if (self.prefersPointerLocked) { return nil; } } #endif // Requesting region for the VM display? if (interaction.view == self.mtkView && self.hasTouchpadPointer) { // Then we need to find out if the pointer is in the actual display area or outside CGPoint location = [self.mtkView convertPoint:[request location] fromView:nil]; CGPoint translated = location; translated.x = CGPointToPixel(translated.x); translated.y = CGPointToPixel(translated.y); if ([self isPointOnVMDisplay:translated]) { // move vm cursor, hide iOS cursor [self.cursor updateMovement:location]; return [UIPointerRegion regionWithRect:[self.mtkView bounds] identifier:@"vm view"]; } else { // don't move vm cursor, show iOS cursor return nil; } } else { return nil; } } #pragma mark - Scroll Gesture - (IBAction)gestureScroll:(UIPanGestureRecognizer *)sender API_AVAILABLE(ios(13.4)) { [self scrollWithInertia:sender]; } @end