2019-11-02 15:17:52 -07:00

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