mirror of
https://github.com/moonlight-stream/moonlight-ios.git
synced 2025-07-01 15:26:11 +00:00
427 lines
18 KiB
Objective-C
427 lines
18 KiB
Objective-C
//
|
|
// StreamView.m
|
|
// Moonlight
|
|
//
|
|
// Created by Cameron Gutman on 10/19/14.
|
|
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
|
//
|
|
|
|
#import "StreamView.h"
|
|
#include <Limelight.h>
|
|
#import "DataManager.h"
|
|
#import "ControllerSupport.h"
|
|
#import "KeyboardSupport.h"
|
|
|
|
@implementation StreamView {
|
|
CGPoint touchLocation, originalLocation;
|
|
BOOL touchMoved;
|
|
OnScreenControls* onScreenControls;
|
|
|
|
BOOL isInputingText;
|
|
BOOL isDragging;
|
|
NSTimer* dragTimer;
|
|
|
|
float xDeltaFactor;
|
|
float yDeltaFactor;
|
|
float screenFactor;
|
|
|
|
#if TARGET_OS_TV
|
|
UIGestureRecognizer* remotePressRecognizer;
|
|
UIGestureRecognizer* remoteLongPressRecognizer;
|
|
#endif
|
|
|
|
id<UserInteractionDelegate> interactionDelegate;
|
|
NSTimer* interactionTimer;
|
|
BOOL hasUserInteracted;
|
|
|
|
NSDictionary<NSString *, NSNumber *> *dictCodes;
|
|
}
|
|
|
|
- (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<EdgeDetectionDelegate>)swipeDelegate interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate {
|
|
self->interactionDelegate = interactionDelegate;
|
|
#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];
|
|
DataManager* dataMan = [[DataManager alloc] init];
|
|
OnScreenControlsLevel level = (OnScreenControlsLevel)[[dataMan getSettings].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
|
|
}
|
|
|
|
- (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)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.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;
|
|
}
|
|
}
|
|
}
|
|
|
|
- (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;
|
|
}
|
|
|
|
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
|
|
// This method is called when the "Return" key is pressed.
|
|
LiSendKeyboardEvent(0x0d, KEY_ACTION_DOWN, 0);
|
|
usleep(50 * 1000);
|
|
LiSendKeyboardEvent(0x0d, KEY_ACTION_UP, 0);
|
|
return NO;
|
|
}
|
|
|
|
- (void)onKeyboardPressed:(UITextField *)textField {
|
|
NSString* inputText = textField.text;
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
|
// If the text became empty, we know the user pressed the backspace key.
|
|
if ([inputText isEqual:@""]) {
|
|
LiSendKeyboardEvent(0x08, KEY_ACTION_DOWN, 0);
|
|
usleep(50 * 1000);
|
|
LiSendKeyboardEvent(0x08, KEY_ACTION_UP, 0);
|
|
} else {
|
|
// Character 0 will be our known sentinel value
|
|
for (int i = 1; i < [inputText length]; i++) {
|
|
struct KeyEvent event = [KeyboardSupport translateKeyEvent:[inputText characterAtIndex:i] withModifierFlags:0];
|
|
if (event.keycode == 0) {
|
|
// If we don't know the code, don't send anything.
|
|
Log(LOG_W, @"Unknown key code: [%c]", [inputText characterAtIndex:i]);
|
|
continue;
|
|
}
|
|
[self sendLowLevelEvent:event];
|
|
}
|
|
}
|
|
});
|
|
|
|
// Reset text field back to known state
|
|
textField.text = @"0";
|
|
|
|
// Move the insertion point back to the end of the text box
|
|
UITextRange *textRange = [textField textRangeFromPosition:textField.endOfDocument toPosition:textField.endOfDocument];
|
|
[textField setSelectedTextRange:textRange];
|
|
}
|
|
|
|
- (void)specialCharPressed:(UIKeyCommand *)cmd {
|
|
struct KeyEvent event = [KeyboardSupport translateKeyEvent:0x20 withModifierFlags:[cmd modifierFlags]];
|
|
event.keycode = [[dictCodes valueForKey:[cmd input]] intValue];
|
|
[self sendLowLevelEvent:event];
|
|
}
|
|
|
|
- (void)keyPressed:(UIKeyCommand *)cmd {
|
|
struct KeyEvent event = [KeyboardSupport translateKeyEvent:[[cmd input] characterAtIndex:0] withModifierFlags:[cmd modifierFlags]];
|
|
[self sendLowLevelEvent:event];
|
|
}
|
|
|
|
- (void)sendLowLevelEvent:(struct KeyEvent)event {
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
|
// When we want to send a modified key (like uppercase letters) we need to send the
|
|
// modifier ("shift") seperately from the key itself.
|
|
if (event.modifier != 0) {
|
|
LiSendKeyboardEvent(event.modifierKeycode, KEY_ACTION_DOWN, event.modifier);
|
|
}
|
|
LiSendKeyboardEvent(event.keycode, KEY_ACTION_DOWN, event.modifier);
|
|
usleep(50 * 1000);
|
|
LiSendKeyboardEvent(event.keycode, KEY_ACTION_UP, event.modifier);
|
|
if (event.modifier != 0) {
|
|
LiSendKeyboardEvent(event.modifierKeycode, KEY_ACTION_UP, event.modifier);
|
|
}
|
|
});
|
|
}
|
|
|
|
- (BOOL)canBecomeFirstResponder {
|
|
return YES;
|
|
}
|
|
|
|
- (NSArray<UIKeyCommand *> *)keyCommands
|
|
{
|
|
NSString *charset = @"qwertyuiopasdfghjklzxcvbnm1234567890\t§[]\\'\"/.,`<>-´ç+`¡'º;ñ= ";
|
|
|
|
NSMutableArray<UIKeyCommand *> * commands = [NSMutableArray<UIKeyCommand *> array];
|
|
dictCodes = [[NSDictionary alloc] initWithObjectsAndKeys: [NSNumber numberWithInt: 0x0d], @"\r", [NSNumber numberWithInt: 0x08], @"\b", [NSNumber numberWithInt: 0x1b], UIKeyInputEscape, [NSNumber numberWithInt: 0x28], UIKeyInputDownArrow, [NSNumber numberWithInt: 0x26], UIKeyInputUpArrow, [NSNumber numberWithInt: 0x25], UIKeyInputLeftArrow, [NSNumber numberWithInt: 0x27], UIKeyInputRightArrow, nil];
|
|
|
|
[charset enumerateSubstringsInRange:NSMakeRange(0, charset.length)
|
|
options:NSStringEnumerationByComposedCharacterSequences
|
|
usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
|
|
[commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:0 action:@selector(keyPressed:)]];
|
|
[commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:UIKeyModifierShift action:@selector(keyPressed:)]];
|
|
[commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:UIKeyModifierControl action:@selector(keyPressed:)]];
|
|
[commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:UIKeyModifierAlternate action:@selector(keyPressed:)]];
|
|
}];
|
|
|
|
for (NSString *c in [dictCodes keyEnumerator]) {
|
|
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
|
modifierFlags:0
|
|
action:@selector(specialCharPressed:)]];
|
|
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
|
modifierFlags:UIKeyModifierShift
|
|
action:@selector(specialCharPressed:)]];
|
|
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
|
modifierFlags:UIKeyModifierShift | UIKeyModifierAlternate
|
|
action:@selector(specialCharPressed:)]];
|
|
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
|
modifierFlags:UIKeyModifierShift | UIKeyModifierControl
|
|
action:@selector(specialCharPressed:)]];
|
|
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
|
modifierFlags:UIKeyModifierControl
|
|
action:@selector(specialCharPressed:)]];
|
|
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
|
modifierFlags:UIKeyModifierControl | UIKeyModifierAlternate
|
|
action:@selector(specialCharPressed:)]];
|
|
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
|
modifierFlags:UIKeyModifierAlternate
|
|
action:@selector(specialCharPressed:)]];
|
|
}
|
|
|
|
return commands;
|
|
}
|
|
|
|
@end
|