// // StreamView.m // Moonlight // // Created by Cameron Gutman on 10/19/14. // Copyright (c) 2014 Moonlight Stream. All rights reserved. // #import "StreamView.h" #include #import "DataManager.h" #import "ControllerSupport.h" #import "KeyboardSupport.h" #import "TextFieldKeyboardDelegate.h" static const double X1_MOUSE_SPEED_DIVISOR = 2.5; @implementation StreamView { CGPoint touchLocation, originalLocation; BOOL touchMoved; OnScreenControls* onScreenControls; X1Mouse* x1mouse; BOOL isInputingText; BOOL isDragging; NSTimer* dragTimer; float xDeltaFactor; float yDeltaFactor; float screenFactor; double mouseX; double mouseY; #if TARGET_OS_TV UIGestureRecognizer* remotePressRecognizer; UIGestureRecognizer* remoteLongPressRecognizer; #endif id interactionDelegate; NSTimer* interactionTimer; BOOL hasUserInteracted; TextFieldKeyboardDelegate* textFieldDelegate; } - (void) setMouseDeltaFactors:(float)x y:(float)y { xDeltaFactor = x; yDeltaFactor = y; #if TARGET_OS_TV // The Apple TV uses indirect touch devices, so they should // not be scaled by the screen scaling factor. screenFactor = 1.0f; #else screenFactor = [[UIScreen mainScreen] scale]; #endif } - (void) setupStreamView:(ControllerSupport*)controllerSupport swipeDelegate:(id)swipeDelegate interactionDelegate:(id)interactionDelegate { self->interactionDelegate = interactionDelegate; 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 onScreenControls = [[OnScreenControls alloc] initWithView:self controllerSup:controllerSupport swipeDelegate:swipeDelegate]; OnScreenControlsLevel level = (OnScreenControlsLevel)[settings.onscreenControls integerValue]; if (level == OnScreenControlsLevelAuto) { [controllerSupport initAutoOnScreenControlMode:onScreenControls]; } else { Log(LOG_I, @"Setting manual on-screen controls level: %d", (int)level); [onScreenControls setLevel:level]; } #endif textFieldDelegate = [[TextFieldKeyboardDelegate alloc] initWithTextField:_keyInputField]; x1mouse = [[X1Mouse alloc] init]; x1mouse.delegate = self; if (settings.btMouseSupport) { [x1mouse start]; } } - (void)startInteractionTimer { // Restart user interaction tracking hasUserInteracted = NO; BOOL timerAlreadyRunning = interactionTimer != nil; // Start/restart the timer [interactionTimer invalidate]; interactionTimer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(interactionTimerExpired:) userInfo:nil repeats:NO]; // Notify the delegate if this was a new user interaction if (!timerAlreadyRunning) { [interactionDelegate userInteractionBegan]; } } - (void)interactionTimerExpired:(NSTimer *)timer { if (!hasUserInteracted) { // User has finished touching the screen interactionTimer = nil; [interactionDelegate userInteractionEnded]; } else { // User is still touching the screen. Restart the timer. [self startInteractionTimer]; } } - (void) showOnScreenControls { #if !TARGET_OS_TV [onScreenControls show]; [self becomeFirstResponder]; #endif } - (OnScreenControlsLevel) getCurrentOscState { if (onScreenControls == nil) { return OnScreenControlsLevelOff; } else { return [onScreenControls getLevel]; } } - (Boolean)isConfirmedMove:(CGPoint)currentPoint from:(CGPoint)originalPoint { // Movements of greater than 10 pixels are considered confirmed return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) >= 10; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { Log(LOG_D, @"Touch down"); // Notify of user interaction and start expiration timer [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]; } } } - (void)onDragStart:(NSTimer*)timer { if (!touchMoved && !isDragging){ isDragging = true; LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); } } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { 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; int deltaY = currentLocation.y - touchLocation.y; deltaX *= xDeltaFactor * screenFactor; deltaY *= yDeltaFactor * screenFactor; 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; } } } - (void)pressesBegan:(NSSet *)presses withEvent:(UIPressesEvent *)event { BOOL handled = NO; if (@available(iOS 13.4, tvOS 13.4, *)) { for (UIPress* press in presses) { // For now, we'll treated it as handled if we handle at least one of the // UIPress events inside the set. if (press.key != nil && [KeyboardSupport sendKeyEvent:press.key down:YES]) { // This will prevent the legacy UITextField from receiving the event handled = YES; } } } if (!handled) { [super pressesBegan:presses withEvent:event]; } } - (void)pressesEnded:(NSSet *)presses withEvent:(UIPressesEvent *)event { BOOL handled = NO; if (@available(iOS 13.4, tvOS 13.4, *)) { for (UIPress* press in presses) { // For now, we'll treated it as handled if we handle at least one of the // UIPress events inside the set. if (press.key != nil && [KeyboardSupport sendKeyEvent:press.key down:NO]) { // This will prevent the legacy UITextField from receiving the event handled = YES; } } } if (!handled) { [super pressesEnded:presses withEvent:event]; } } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { Log(LOG_D, @"Touch up"); 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.text = @"0"; [_keyInputField becomeFirstResponder]; // 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; } } } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)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); } #endif - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { // Disable all gesture recognizers to prevent them from eating our touches. // This can happen on iOS 13 where the 3 finger tap gesture is taken over for // displaying custom edit controls. return NO; } - (NSArray *)keyCommands { return [textFieldDelegate keyCommands]; } - (BOOL)canBecomeFirstResponder { return YES; } - (void)connectedStateDidChangeWithIdentifier:(NSUUID * _Nonnull)identifier isConnected:(BOOL)isConnected { NSLog(@"Citrix X1 mouse state change: %@ -> %s", identifier, isConnected ? "connected" : "disconnected"); } - (void)mouseDidMoveWithIdentifier:(NSUUID * _Nonnull)identifier deltaX:(int16_t)deltaX deltaY:(int16_t)deltaY { mouseX += deltaX / X1_MOUSE_SPEED_DIVISOR; mouseY += deltaY / X1_MOUSE_SPEED_DIVISOR; short shortX = (short)mouseX; short shortY = (short)mouseY; if (shortX == 0 && shortY == 0) { return; } LiSendMouseMoveEvent(shortX, shortY); mouseX -= shortX; mouseY -= shortY; } - (int) buttonFromX1ButtonCode:(enum X1MouseButton)button { switch (button) { case X1MouseButtonLeft: return BUTTON_LEFT; case X1MouseButtonRight: return BUTTON_RIGHT; case X1MouseButtonMiddle: return BUTTON_MIDDLE; default: return -1; } } - (void)mouseDownWithIdentifier:(NSUUID * _Nonnull)identifier button:(enum X1MouseButton)button { LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, [self buttonFromX1ButtonCode:button]); } - (void)mouseUpWithIdentifier:(NSUUID * _Nonnull)identifier button:(enum X1MouseButton)button { LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, [self buttonFromX1ButtonCode:button]); } - (void)wheelDidScrollWithIdentifier:(NSUUID * _Nonnull)identifier deltaZ:(int8_t)deltaZ { LiSendScrollEvent(deltaZ); } @end