diff --git a/Limelight/Input/RelativeTouchHandler.h b/Limelight/Input/RelativeTouchHandler.h new file mode 100644 index 0000000..545cb3f --- /dev/null +++ b/Limelight/Input/RelativeTouchHandler.h @@ -0,0 +1,19 @@ +// +// RelativeTouchHandler.h +// Moonlight +// +// Created by Cameron Gutman on 11/1/20. +// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RelativeTouchHandler : UIResponder + +-(id)initWithView:(UIView*)view; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Limelight/Input/RelativeTouchHandler.m b/Limelight/Input/RelativeTouchHandler.m new file mode 100644 index 0000000..2b0576c --- /dev/null +++ b/Limelight/Input/RelativeTouchHandler.m @@ -0,0 +1,196 @@ +// +// RelativeTouchHandler.m +// Moonlight +// +// Created by Cameron Gutman on 11/1/20. +// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved. +// + +#import "RelativeTouchHandler.h" + +#include + +static const int REFERENCE_WIDTH = 1280; +static const int REFERENCE_HEIGHT = 720; + +@implementation RelativeTouchHandler { + CGPoint touchLocation, originalLocation; + BOOL touchMoved; + BOOL isDragging; + NSTimer* dragTimer; + +#if TARGET_OS_TV + UIGestureRecognizer* remotePressRecognizer; + UIGestureRecognizer* remoteLongPressRecognizer; +#endif + + UIView* view; +} + +- (id)initWithView:(UIView*)view { + self = [self init]; + self->view = view; + +#if TARGET_OS_TV + remotePressRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonPressed:)]; + remotePressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)]; + + remoteLongPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonLongPressed:)]; + remoteLongPressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)]; + + [self->view addGestureRecognizer:remotePressRecognizer]; + [self->view addGestureRecognizer:remoteLongPressRecognizer]; +#endif + + return self; +} + +- (BOOL)isConfirmedMove:(CGPoint)currentPoint from:(CGPoint)originalPoint { + // Movements of greater than 5 pixels are considered confirmed + return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) >= 5; +} + +- (void)onDragStart:(NSTimer*)timer { + if (!touchMoved && !isDragging){ + isDragging = true; + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); + } +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + UITouch *touch = [[event allTouches] anyObject]; + originalLocation = touchLocation = [touch locationInView:view]; + touchMoved = false; + if ([[event allTouches] count] == 1 && !isDragging) { + dragTimer = [NSTimer scheduledTimerWithTimeInterval:0.650 + target:self + selector:@selector(onDragStart:) + userInfo:nil + repeats:NO]; + } +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { + if ([[event allTouches] count] == 1) { + UITouch *touch = [[event allTouches] anyObject]; + CGPoint currentLocation = [touch locationInView:view]; + + if (touchLocation.x != currentLocation.x || + touchLocation.y != currentLocation.y) + { + int deltaX = (currentLocation.x - touchLocation.x) * (REFERENCE_WIDTH / view.bounds.size.width); + int deltaY = (currentLocation.y - touchLocation.y) * (REFERENCE_HEIGHT / view.bounds.size.height); + + if (deltaX != 0 || deltaY != 0) { + LiSendMouseMoveEvent(deltaX, deltaY); + touchLocation = currentLocation; + + // If we've moved far enough to confirm this wasn't just human/machine error, + // mark it as such. + if ([self isConfirmedMove:touchLocation from:originalLocation]) { + touchMoved = true; + } + } + } + } else if ([[event allTouches] count] == 2) { + CGPoint firstLocation = [[[[event allTouches] allObjects] objectAtIndex:0] locationInView:view]; + CGPoint secondLocation = [[[[event allTouches] allObjects] objectAtIndex:1] locationInView:view]; + + CGPoint avgLocation = CGPointMake((firstLocation.x + secondLocation.x) / 2, (firstLocation.y + secondLocation.y) / 2); + if (touchLocation.y != avgLocation.y) { + LiSendScrollEvent(avgLocation.y - touchLocation.y); + } + + // If we've moved far enough to confirm this wasn't just human/machine error, + // mark it as such. + if ([self isConfirmedMove:firstLocation from:originalLocation]) { + touchMoved = true; + } + + touchLocation = avgLocation; + } +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + [dragTimer invalidate]; + dragTimer = nil; + if (isDragging) { + isDragging = false; + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); + } else if (!touchMoved) { + if ([[event allTouches] count] == 2) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ + Log(LOG_D, @"Sending right mouse button press"); + + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT); + + // Wait 100 ms to simulate a real button press + usleep(100 * 1000); + + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT); + }); + } else if ([[event allTouches] count] == 1) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ + if (!self->isDragging){ + Log(LOG_D, @"Sending left mouse button press"); + + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); + + // Wait 100 ms to simulate a real button press + usleep(100 * 1000); + } + self->isDragging = false; + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); + }); + } + } + + // We we're moving from 2+ touches to 1. Synchronize the current position + // of the active finger so we don't jump unexpectedly on the next touchesMoved + // callback when finger 1 switches on us. + if ([[event allTouches] count] - [touches count] == 1) { + NSMutableSet *activeSet = [[NSMutableSet alloc] initWithCapacity:[[event allTouches] count]]; + [activeSet unionSet:[event allTouches]]; + [activeSet minusSet:touches]; + touchLocation = [[activeSet anyObject] locationInView:view]; + + // Mark this touch as moved so we don't send a left mouse click if the user + // right clicks without moving their other finger. + touchMoved = true; + } +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { + [dragTimer invalidate]; + dragTimer = nil; + if (isDragging) { + isDragging = false; + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); + } +} + +#if TARGET_OS_TV +- (void)remoteButtonPressed:(id)sender { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ + Log(LOG_D, @"Sending left mouse button press"); + + // Mark this as touchMoved to avoid a duplicate press on touch up + self->touchMoved = true; + + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); + + // Wait 100 ms to simulate a real button press + usleep(100 * 1000); + + LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); + }); +} +- (void)remoteButtonLongPressed:(id)sender { + Log(LOG_D, @"Holding left mouse button"); + + isDragging = true; + LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); +} +#endif + +@end diff --git a/Limelight/Input/StreamView.m b/Limelight/Input/StreamView.m index b6874f3..f989bee 100644 --- a/Limelight/Input/StreamView.m +++ b/Limelight/Input/StreamView.m @@ -11,20 +11,14 @@ #import "DataManager.h" #import "ControllerSupport.h" #import "KeyboardSupport.h" +#import "RelativeTouchHandler.h" static const double X1_MOUSE_SPEED_DIVISOR = 2.5; -static const int REFERENCE_WIDTH = 1280; -static const int REFERENCE_HEIGHT = 720; - @implementation StreamView { - CGPoint touchLocation, originalLocation; - BOOL touchMoved; OnScreenControls* onScreenControls; BOOL isInputingText; - BOOL isDragging; - NSTimer* dragTimer; float streamAspectRatio; @@ -38,10 +32,7 @@ static const int REFERENCE_HEIGHT = 720; double accumulatedMouseDeltaX; double accumulatedMouseDeltaY; -#if TARGET_OS_TV - UIGestureRecognizer* remotePressRecognizer; - UIGestureRecognizer* remoteLongPressRecognizer; -#endif + RelativeTouchHandler* touchHandler; id interactionDelegate; NSTimer* interactionTimer; @@ -57,18 +48,11 @@ static const int REFERENCE_HEIGHT = 720; self->interactionDelegate = interactionDelegate; self->streamAspectRatio = (float)streamConfig.width / (float)streamConfig.height; + self->touchHandler = [[RelativeTouchHandler alloc] initWithView:self]; + TemporarySettings* settings = [[[DataManager alloc] init] getSettings]; -#if TARGET_OS_TV - remotePressRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonPressed:)]; - remotePressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)]; - - remoteLongPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonLongPressed:)]; - remoteLongPressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)]; - - [self addGestureRecognizer:remotePressRecognizer]; - [self addGestureRecognizer:remoteLongPressRecognizer]; -#else +#if !TARGET_OS_TV onScreenControls = [[OnScreenControls alloc] initWithView:self controllerSup:controllerSupport swipeDelegate:swipeDelegate]; OnScreenControlsLevel level = (OnScreenControlsLevel)[settings.onscreenControls integerValue]; if (level == OnScreenControlsLevelAuto) { @@ -149,11 +133,6 @@ static const int REFERENCE_HEIGHT = 720; } } -- (BOOL)isConfirmedMove:(CGPoint)currentPoint from:(CGPoint)originalPoint { - // Movements of greater than 5 pixels are considered confirmed - return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) >= 5; -} - - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { if ([self handleMouseButtonEvent:BUTTON_ACTION_PRESS forTouches:touches @@ -168,23 +147,28 @@ static const int REFERENCE_HEIGHT = 720; [self startInteractionTimer]; if (![onScreenControls handleTouchDownEvent:touches]) { - UITouch *touch = [[event allTouches] anyObject]; - originalLocation = touchLocation = [touch locationInView:self]; - touchMoved = false; - if ([[event allTouches] count] == 1 && !isDragging) { - dragTimer = [NSTimer scheduledTimerWithTimeInterval:0.650 - target:self - selector:@selector(onDragStart:) - userInfo:nil - repeats:NO]; + if ([[event allTouches] count] == 3) { + if (isInputingText) { + Log(LOG_D, @"Closing the keyboard"); + [_keyInputField resignFirstResponder]; + isInputingText = false; + } else { + Log(LOG_D, @"Opening the keyboard"); + // Prepare the textbox used to capture keyboard events. + _keyInputField.delegate = self; + _keyInputField.text = @"0"; + [_keyInputField becomeFirstResponder]; + [_keyInputField addTarget:self action:@selector(onKeyboardPressed:) forControlEvents:UIControlEventEditingChanged]; + + // Undo causes issues for our state management, so turn it off + [_keyInputField.undoManager disableUndoRegistration]; + + isInputingText = true; + } + } + else { + [touchHandler touchesBegan:touches withEvent:event]; } - } -} - -- (void)onDragStart:(NSTimer*)timer { - if (!touchMoved && !isDragging){ - isDragging = true; - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); } } @@ -276,46 +260,8 @@ static const int REFERENCE_HEIGHT = 720; hasUserInteracted = YES; if (![onScreenControls handleTouchMovedEvent:touches]) { - if ([[event allTouches] count] == 1) { - UITouch *touch = [[event allTouches] anyObject]; - CGPoint currentLocation = [touch locationInView:self]; - - if (touchLocation.x != currentLocation.x || - touchLocation.y != currentLocation.y) - { - int deltaX = (currentLocation.x - touchLocation.x) * (REFERENCE_WIDTH / self.bounds.size.width); - int deltaY = (currentLocation.y - touchLocation.y) * (REFERENCE_HEIGHT / self.bounds.size.height); - - if (deltaX != 0 || deltaY != 0) { - LiSendMouseMoveEvent(deltaX, deltaY); - touchLocation = currentLocation; - - // If we've moved far enough to confirm this wasn't just human/machine error, - // mark it as such. - if ([self isConfirmedMove:touchLocation from:originalLocation]) { - touchMoved = true; - } - } - } - } else if ([[event allTouches] count] == 2) { - CGPoint firstLocation = [[[[event allTouches] allObjects] objectAtIndex:0] locationInView:self]; - CGPoint secondLocation = [[[[event allTouches] allObjects] objectAtIndex:1] locationInView:self]; - - CGPoint avgLocation = CGPointMake((firstLocation.x + secondLocation.x) / 2, (firstLocation.y + secondLocation.y) / 2); - if (touchLocation.y != avgLocation.y) { - LiSendScrollEvent(avgLocation.y - touchLocation.y); - } - - // If we've moved far enough to confirm this wasn't just human/machine error, - // mark it as such. - if ([self isConfirmedMove:firstLocation from:originalLocation]) { - touchMoved = true; - } - - touchLocation = avgLocation; - } + [touchHandler touchesMoved:touches withEvent:event]; } - } - (void)pressesBegan:(NSSet *)presses withEvent:(UIPressesEvent *)event { @@ -369,108 +315,18 @@ static const int REFERENCE_HEIGHT = 720; hasUserInteracted = YES; if (![onScreenControls handleTouchUpEvent:touches]) { - [dragTimer invalidate]; - dragTimer = nil; - if (isDragging) { - isDragging = false; - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); - } else if (!touchMoved) { - if ([[event allTouches] count] == 3) { - if (isInputingText) { - Log(LOG_D, @"Closing the keyboard"); - [_keyInputField resignFirstResponder]; - isInputingText = false; - } else { - Log(LOG_D, @"Opening the keyboard"); - // Prepare the textbox used to capture keyboard events. - _keyInputField.delegate = self; - _keyInputField.text = @"0"; - [_keyInputField becomeFirstResponder]; - [_keyInputField addTarget:self action:@selector(onKeyboardPressed:) forControlEvents:UIControlEventEditingChanged]; - - // Undo causes issues for our state management, so turn it off - [_keyInputField.undoManager disableUndoRegistration]; - - isInputingText = true; - } - } else if ([[event allTouches] count] == 2) { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ - Log(LOG_D, @"Sending right mouse button press"); - - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT); - - // Wait 100 ms to simulate a real button press - usleep(100 * 1000); - - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT); - }); - } else if ([[event allTouches] count] == 1) { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ - if (!self->isDragging){ - Log(LOG_D, @"Sending left mouse button press"); - - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); - - // Wait 100 ms to simulate a real button press - usleep(100 * 1000); - } - self->isDragging = false; - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); - }); - } - } - - // We we're moving from 2+ touches to 1. Synchronize the current position - // of the active finger so we don't jump unexpectedly on the next touchesMoved - // callback when finger 1 switches on us. - if ([[event allTouches] count] - [touches count] == 1) { - NSMutableSet *activeSet = [[NSMutableSet alloc] initWithCapacity:[[event allTouches] count]]; - [activeSet unionSet:[event allTouches]]; - [activeSet minusSet:touches]; - touchLocation = [[activeSet anyObject] locationInView:self]; - - // Mark this touch as moved so we don't send a left mouse click if the user - // right clicks without moving their other finger. - touchMoved = true; - } + [touchHandler touchesEnded:touches withEvent:event]; } } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { - [dragTimer invalidate]; - dragTimer = nil; - if (isDragging) { - isDragging = false; - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); - } + [touchHandler touchesCancelled:touches withEvent:event]; [self handleMouseButtonEvent:BUTTON_ACTION_RELEASE forTouches:touches withEvent:event]; } -#if TARGET_OS_TV -- (void)remoteButtonPressed:(id)sender { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ - Log(LOG_D, @"Sending left mouse button press"); - - // Mark this as touchMoved to avoid a duplicate press on touch up - self->touchMoved = true; - - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); - - // Wait 100 ms to simulate a real button press - usleep(100 * 1000); - - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); - }); -} -- (void)remoteButtonLongPressed:(id)sender { - Log(LOG_D, @"Holding left mouse button"); - - isDragging = true; - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); -} -#else +#if !TARGET_OS_TV - (void) updateCursorLocation:(CGPoint)location { // These are now relative to the StreamView, however we need to scale them // further to make them relative to the actual video portion. diff --git a/Moonlight.xcodeproj/project.pbxproj b/Moonlight.xcodeproj/project.pbxproj index 7b9c250..2ea93ce 100644 --- a/Moonlight.xcodeproj/project.pbxproj +++ b/Moonlight.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 693B3A9B218638CD00982F7B /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 693B3A9A218638CD00982F7B /* Settings.bundle */; }; + 9819CC14254F107A008A7C8E /* RelativeTouchHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 9819CC13254F107A008A7C8E /* RelativeTouchHandler.m */; }; + 9819CC1D254F1730008A7C8E /* RelativeTouchHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 9819CC13254F107A008A7C8E /* RelativeTouchHandler.m */; }; 9827E7A32514366900F25707 /* HapticContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 9827E7A22514366900F25707 /* HapticContext.m */; }; 9827E7A42514366900F25707 /* HapticContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 9827E7A22514366900F25707 /* HapticContext.m */; }; 9832D1361BBCD5C50036EF48 /* TemporaryApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 9832D1351BBCD5C50036EF48 /* TemporaryApp.m */; }; @@ -159,6 +161,8 @@ /* Begin PBXFileReference section */ 693B3A9A218638CD00982F7B /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; 98132E8C20BC9A62007A053F /* Moonlight v1.1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.1.xcdatamodel"; sourceTree = ""; }; + 9819CC12254F107A008A7C8E /* RelativeTouchHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RelativeTouchHandler.h; sourceTree = ""; }; + 9819CC13254F107A008A7C8E /* RelativeTouchHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RelativeTouchHandler.m; sourceTree = ""; }; 9827E7A22514366900F25707 /* HapticContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HapticContext.m; sourceTree = ""; }; 9827E7A7251436EA00F25707 /* HapticContext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HapticContext.h; sourceTree = ""; }; 9832D1341BBCD5C50036EF48 /* TemporaryApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TemporaryApp.h; path = Database/TemporaryApp.h; sourceTree = ""; }; @@ -539,6 +543,8 @@ 9897B6A32212610800966419 /* Controller.h */, 9827E7A22514366900F25707 /* HapticContext.m */, 9827E7A7251436EA00F25707 /* HapticContext.h */, + 9819CC12254F107A008A7C8E /* RelativeTouchHandler.h */, + 9819CC13254F107A008A7C8E /* RelativeTouchHandler.m */, ); path = Input; sourceTree = ""; @@ -988,6 +994,7 @@ FB1A67B7213245D500507771 /* ServerInfoResponse.m in Sources */, FB1A67B9213245D500507771 /* AppAssetResponse.m in Sources */, FB1A67BB213245D500507771 /* AppListResponse.m in Sources */, + 9819CC1D254F1730008A7C8E /* RelativeTouchHandler.m in Sources */, FB1A67AB213245C500507771 /* CryptoManager.m in Sources */, FB1A67AF213245C500507771 /* IdManager.m in Sources */, FB1A67A3213245BD00507771 /* Connection.m in Sources */, @@ -1003,6 +1010,7 @@ buildActionMask = 2147483647; files = ( FB290D0719B2C406004C83CF /* Limelight.xcdatamodeld in Sources */, + 9819CC14254F107A008A7C8E /* RelativeTouchHandler.m in Sources */, FB1A674D2131E65900507771 /* KeyboardSupport.m in Sources */, FB89463219F646E200339C8A /* VideoDecoderRenderer.m in Sources */, FB290D0419B2C406004C83CF /* AppDelegate.m in Sources */,