Files
VoidLink/Limelight/Input/StreamView.m
T

1099 lines
46 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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"
#import "NativeTouchHandler.h"
#import "RelativeTouchHandler.h"
#import "AbsoluteTouchHandler.h"
#import "KeyboardInputField.h"
#import "CustomTapGestureRecognizer.h"
static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
@implementation StreamView {
OnScreenControls* onScreenControls;
KeyboardInputField* keyInputField;
BOOL isInputingText;
NSMutableSet* keysDown;
float streamAspectRatio;
// iOS 13.4 mouse support
NSInteger lastMouseButtonMask;
float lastMouseX;
float lastMouseY;
CGPoint lastScrollTranslation;
// Citrix X1 mouse support
X1Mouse* x1mouse;
double accumulatedMouseDeltaX;
double accumulatedMouseDeltaY;
UIResponder* touchHandler;
id<UserInteractionDelegate> interactionDelegate;
NSTimer* interactionTimer;
BOOL hasUserInteracted;
NSDictionary<NSString *, NSNumber *> *dictCodes;
CustomTapGestureRecognizer *keyboardToggleRecognizer;
}
- (void) setupStreamView:(ControllerSupport*)controllerSupport
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
config:(StreamConfiguration*)streamConfig {
[NativeTouchHandler initializePointerIdDict];
[NativeTouchHandler initializePointerIdSet];
self->interactionDelegate = interactionDelegate;
self->streamAspectRatio = (float)streamConfig.width / (float)streamConfig.height;
TemporarySettings* settings = [[[DataManager alloc] init] getSettings];
keysDown = [[NSMutableSet alloc] init];
keyInputField = [[KeyboardInputField alloc] initWithFrame:CGRectZero];
[keyInputField setKeyboardType:UIKeyboardTypeDefault];
[keyInputField setAutocorrectionType:UITextAutocorrectionTypeNo];
[keyInputField setAutocapitalizationType:UITextAutocapitalizationTypeNone];
[keyInputField setSpellCheckingType:UITextSpellCheckingTypeNo];
[self addSubview:keyInputField];
keyboardToggleRecognizer = [[CustomTapGestureRecognizer alloc] initWithTarget:self action:@selector(toggleKeyboard)];
keyboardToggleRecognizer.numberOfTouchesRequired = settings.keyboardToggleFingers.intValue;
keyboardToggleRecognizer.tapDownTimeThreshold = 300.0; // tap down time threshold in milli seconds.
keyboardToggleRecognizer.delaysTouchesBegan = NO;
keyboardToggleRecognizer.delaysTouchesEnded = NO;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillShow)
name:UIKeyboardWillShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillHide)
name:UIKeyboardWillHideNotification
object:nil];
[self addGestureRecognizer:keyboardToggleRecognizer];
#if TARGET_OS_TV
// tvOS requires RelativeTouchHandler to manage Apple Remote input
self->touchHandler = [[RelativeTouchHandler alloc] initWithView:self];
#else
// iOS uses RelativeTouchHandler or AbsoluteTouchHandler depending on user preference
if (settings.absoluteTouchMode) {
self->touchHandler = [[AbsoluteTouchHandler alloc] initWithView:self];
}
else {
self->touchHandler = [[RelativeTouchHandler alloc] initWithView:self];
}
onScreenControls = [[OnScreenControls alloc] initWithView:self controllerSup:controllerSupport streamConfig:streamConfig];
OnScreenControlsLevel level = (OnScreenControlsLevel)[settings.onscreenControls integerValue];
if (settings.absoluteTouchMode) {
Log(LOG_I, @"On-screen controls disabled in absolute touch mode");
[onScreenControls setLevel:OnScreenControlsLevelOff];
}
else if (level == OnScreenControlsLevelAuto) {
[controllerSupport initAutoOnScreenControlMode:onScreenControls];
}
else {
Log(LOG_I, @"Setting manual on-screen controls level: %d", (int)level);
[onScreenControls setLevel:level];
}
// It would be nice to just use GCMouse on iOS 14+ and the older API on iOS 13
// but unfortunately that isn't possible today. GCMouse doesn't recognize many
// mice correctly, but UIKit does. We will register for both and ignore UIKit
// events if a GCMouse is connected.
if (@available(iOS 13.4, *)) {
// [self addInteraction:[[UIPointerInteraction alloc] initWithDelegate:self]];
//
// UIPanGestureRecognizer *discreteMouseWheelRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(mouseWheelMovedDiscrete:)];
// discreteMouseWheelRecognizer.maximumNumberOfTouches = 0;
// discreteMouseWheelRecognizer.allowedScrollTypesMask = UIScrollTypeMaskDiscrete;
// discreteMouseWheelRecognizer.allowedTouchTypes = @[@(UITouchTypeIndirectPointer)];
// [self addGestureRecognizer:discreteMouseWheelRecognizer];
//
// UIPanGestureRecognizer *continuousMouseWheelRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(mouseWheelMovedContinuous:)];
// continuousMouseWheelRecognizer.maximumNumberOfTouches = 0;
// continuousMouseWheelRecognizer.allowedScrollTypesMask = UIScrollTypeMaskContinuous;
// continuousMouseWheelRecognizer.allowedTouchTypes = @[@(UITouchTypeIndirectPointer)];
// [self addGestureRecognizer:continuousMouseWheelRecognizer];
}
#if defined(__IPHONE_16_1) || defined(__TVOS_16_1)
if (@available(iOS 16.1, *)) {
UIHoverGestureRecognizer *stylusHoverRecognizer = [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(sendStylusHoverEvent:)];
stylusHoverRecognizer.allowedTouchTypes = @[@(UITouchTypePencil)];
[self addGestureRecognizer:stylusHoverRecognizer];
}
#endif
#endif
x1mouse = [[X1Mouse alloc] init];
x1mouse.delegate = self;
if (settings.btMouseSupport) {
[x1mouse start];
}
// This is critical to ensure keyboard events are delivered to this
// StreamView and not our parent UIView, especially on tvOS.
[self becomeFirstResponder];
}
- (void)keyboardWillShow{
isInputingText = true;
// NSLog(@"keyboard will show");
}
- (void)keyboardWillHide{
isInputingText = false;
// NSLog(@"keyboard will hide");
}
- (void)toggleKeyboard{
if (isInputingText) {
Log(LOG_D, @"Closing the keyboard");
// NSLog(@"Closing the keyboard");
[keyInputField resignFirstResponder];
isInputingText = false;
} else {
// NSLog(@"Opening the keyboard");
Log(LOG_D, @"Opening the keyboard");
// Prepare the textbox used to capture keyboard events.
keyInputField.delegate = self;
keyInputField.text = @"0";
#if !TARGET_OS_TV
// Prepare the toolbar above the keyboard for more options
if(false){
UIToolbar *customToolbarView = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, 44)];
UIBarButtonItem *doneBarButton = [self createButtonWithImageNamed:@"DoneIcon.png" backgroundColor:[UIColor clearColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x00 isToggleable:NO];
UIBarButtonItem *windowsBarButton = [self createButtonWithImageNamed:@"WindowsIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x5B isToggleable:YES];
UIBarButtonItem *tabBarButton = [self createButtonWithImageNamed:@"TabIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x09 isToggleable:NO];
UIBarButtonItem *shiftBarButton = [self createButtonWithImageNamed:@"ShiftIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0xA0 isToggleable:YES];
UIBarButtonItem *escapeBarButton = [self createButtonWithImageNamed:@"EscapeIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x1B isToggleable:NO];
UIBarButtonItem *controlBarButton = [self createButtonWithImageNamed:@"ControlIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0xA2 isToggleable:YES];
UIBarButtonItem *altBarButton = [self createButtonWithImageNamed:@"AltIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0xA4 isToggleable:YES];
UIBarButtonItem *deleteBarButton = [self createButtonWithImageNamed:@"DeleteIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x2E isToggleable:NO];
UIBarButtonItem *flexibleSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
[customToolbarView setItems:[NSArray arrayWithObjects:doneBarButton, windowsBarButton, escapeBarButton, tabBarButton, shiftBarButton, controlBarButton, altBarButton, deleteBarButton, flexibleSpace, nil]];
keyInputField.inputAccessoryView = customToolbarView;
}
#endif
[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;
}
}
- (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];
#endif
}
- (OnScreenControlsLevel) getCurrentOscState {
if (onScreenControls == nil) {
return OnScreenControlsLevelOff;
}
else {
return [onScreenControls getLevel];
}
}
- (CGSize) getVideoAreaSize {
if (self.bounds.size.width > self.bounds.size.height * streamAspectRatio) {
return CGSizeMake(self.bounds.size.height * streamAspectRatio, self.bounds.size.height);
} else {
return CGSizeMake(self.bounds.size.width, self.bounds.size.width / streamAspectRatio);
}
}
- (CGPoint) adjustCoordinatesForVideoArea:(CGPoint)point {
// These are now relative to the StreamView, however we need to scale them
// further to make them relative to the actual video portion.
float x = point.x - self.bounds.origin.x;
float y = point.y - self.bounds.origin.y;
// For some reason, we don't seem to always get to the bounds of the window
// so we'll subtract 1 pixel if we're to the left/below of the origin and
// and add 1 pixel if we're to the right/above. It should be imperceptible
// to the user but it will allow activation of gestures that require contact
// with the edge of the screen (like Aero Snap).
if (x < self.bounds.size.width / 2) {
x--;
}
else {
x++;
}
if (y < self.bounds.size.height / 2) {
y--;
}
else {
y++;
}
// This logic mimics what iOS does with AVLayerVideoGravityResizeAspect
CGSize videoSize = [self getVideoAreaSize];
CGPoint videoOrigin = CGPointMake(self.bounds.size.width / 2 - videoSize.width / 2,
self.bounds.size.height / 2 - videoSize.height / 2);
// Confine the cursor to the video region. We don't just discard events outside
// the region because we won't always get one exactly when the mouse leaves the region.
return CGPointMake(MIN(MAX(x, videoOrigin.x), videoOrigin.x + videoSize.width) - videoOrigin.x,
MIN(MAX(y, videoOrigin.y), videoOrigin.y + videoSize.height) - videoOrigin.y);
}
#if !TARGET_OS_TV
- (uint16_t)getRotationFromAzimuthAngle:(float)azimuthAngle {
// iOS reports azimuth of 0 when the stylus is pointing west, but Moonlight expects
// rotation of 0 to mean the stylus is pointing north. Rotate the azimuth angle
// clockwise by 90 degrees to convert from iOS to Moonlight rotation conventions.
int32_t rotationAngle = (azimuthAngle - M_PI_2) * (180.f / M_PI);
if (rotationAngle < 0) {
rotationAngle += 360;
}
return (uint16_t)rotationAngle;
}
- (uint8_t)getTiltFromAltitudeAngle:(float)altitudeAngle {
// iOS reports an altitude of 0 when the stylus is parallel to the touch surface,
// while Moonlight expects a tilt of 0 when the stylus is perpendicular to the surface.
// Subtract the tilt angle from 90 to convert from iOS to Moonlight tilt conventions.
uint8_t altitudeDegs = abs((int16_t)(altitudeAngle * (180.f / M_PI)));
return 90 - MIN(90, altitudeDegs);
}
- (void)sendTouchEvent:(UITouch*)event touchType:(uint8_t)touchType{
CGPoint location = [self adjustCoordinatesForVideoArea:[event locationInView:self]];
CGSize videoSize = [self getVideoAreaSize];
LiSendTouchEvent(touchType,[NativeTouchHandler retrievePointerIdFromDict:event],location.x / videoSize.width, location.y / videoSize.height,(event.force / event.maximumPossibleForce) / sin(event.altitudeAngle),0.0f, 0.0f,[self getRotationFromAzimuthAngle:[event azimuthAngleInView:self]]);
}
- (void)handleUITouch:(UITouch*)event index:(int)index{
uint8_t type;
// NSLog(@"handleUITouch %ld,%d",(long)event.phase,(uint32_t)event);
//#define LI_TOUCH_EVENT_HOVER 0x00
//#define LI_TOUCH_EVENT_DOWN 0x01
//#define LI_TOUCH_EVENT_UP 0x02
//#define LI_TOUCH_EVENT_MOVE 0x03
//#define LI_TOUCH_EVENT_CANCEL 0x04
//#define LI_TOUCH_EVENT_BUTTON_ONLY 0x05
//#define LI_TOUCH_EVENT_HOVER_LEAVE 0x06
//#define LI_TOUCH_EVENT_CANCEL_ALL 0x07
//#define LI_ROT_UNKNOWN 0xFFFF
// UITouchPhaseBegan, // whenever a finger touches the surface.
// UITouchPhaseMoved, // whenever a finger moves on the surface.
// UITouchPhaseStationary, // whenever a finger is touching the surface but hasn't moved since the previous event.
// UITouchPhaseEnded, // whenever a finger leaves the surface.
// UITouchPhaseCancelled, // whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face)
// UITouchPhaseRegionEntered API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos), // whenever a touch is entering the region of a user interface
// UITouchPhaseRegionMoved API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos), // when a touch is inside the region of a user interface, but hasn’t yet made contact or left the region
// UITouchPhaseRegionExited API_AVAILABLE(ios(13.4), tvos(13.4))
switch (event.phase) {
case UITouchPhaseBegan://开始触摸
type = LI_TOUCH_EVENT_DOWN;
[NativeTouchHandler populatePointerId:event]; //获取并记录pointerId
break;
case UITouchPhaseMoved://移动
case UITouchPhaseStationary:
type = LI_TOUCH_EVENT_MOVE;
break;
case UITouchPhaseRegionEntered://停留
case UITouchPhaseRegionMoved://停留
type = LI_TOUCH_EVENT_HOVER;
break;
case UITouchPhaseCancelled://触摸取消
type = LI_TOUCH_EVENT_CANCEL;
[self sendTouchEvent:event touchType:type]; //先发送,再删除
[NativeTouchHandler removePointerId:event]; //删除pointerId
return;
case UITouchPhaseEnded://触摸结束
type = LI_TOUCH_EVENT_UP;
[self sendTouchEvent:event touchType:type]; //先发送,再删除
[NativeTouchHandler removePointerId:event]; //删除pointerId
return;
default:
return;
}
[self sendTouchEvent:event touchType:type];
}
- (BOOL)sendStylusEvent:(UITouch*)event {
uint8_t type;
// Don't touch stylus events if the host doesn't support them. We want to pass
// them as normal touches for legacy hosts that don't understand pen events.
if (!(LiGetHostFeatureFlags() & LI_FF_PEN_TOUCH_EVENTS)) {
return NO;
}
switch (event.phase) {
case UITouchPhaseBegan:
type = LI_TOUCH_EVENT_DOWN;
break;
case UITouchPhaseMoved:
type = LI_TOUCH_EVENT_MOVE;
break;
case UITouchPhaseEnded:
type = LI_TOUCH_EVENT_UP;
break;
case UITouchPhaseCancelled:
type = LI_TOUCH_EVENT_CANCEL;
break;
default:
return YES;
}
CGPoint location = [self adjustCoordinatesForVideoArea:[event locationInView:self]];
CGSize videoSize = [self getVideoAreaSize];
return LiSendPenEvent(type, LI_TOOL_TYPE_PEN, 0, location.x / videoSize.width, location.y / videoSize.height,
(event.force / event.maximumPossibleForce) / sin(event.altitudeAngle),
0.0f, 0.0f,
[self getRotationFromAzimuthAngle:[event azimuthAngleInView:self]],
[self getTiltFromAltitudeAngle:event.altitudeAngle]) != LI_ERR_UNSUPPORTED;
}
- (void)sendStylusHoverEvent:(UIHoverGestureRecognizer*)gesture API_AVAILABLE(ios(13.0)) {
uint8_t type;
switch (gesture.state) {
case UIGestureRecognizerStateBegan:
case UIGestureRecognizerStateChanged:
type = LI_TOUCH_EVENT_HOVER;
break;
case UIGestureRecognizerStateEnded:
type = LI_TOUCH_EVENT_HOVER_LEAVE;
break;
default:
return;
}
CGPoint location = [self adjustCoordinatesForVideoArea:[gesture locationInView:self]];
CGSize videoSize = [self getVideoAreaSize];
float distance = 0.0f;
#if defined(__IPHONE_16_1) || defined(__TVOS_16_1)
if (@available(iOS 16.1, *)) {
distance = gesture.zOffset;
}
#endif
uint16_t rotationAngle = LI_ROT_UNKNOWN;
uint8_t tiltAngle = LI_TILT_UNKNOWN;
#if defined(__IPHONE_16_4) || defined(__TVOS_16_4)
if (@available(iOS 16.4, *)) {
rotationAngle = [self getRotationFromAzimuthAngle:[gesture azimuthAngleInView:self]];
tiltAngle = [self getTiltFromAltitudeAngle:gesture.altitudeAngle];
}
#endif
LiSendPenEvent(type, LI_TOOL_TYPE_PEN, 0, location.x / videoSize.width, location.y / videoSize.height,
distance, 0.0f, 0.0f, rotationAngle, tiltAngle);
}
#endif
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if ([self handleMouseButtonEvent:BUTTON_ACTION_PRESS
forTouches:touches
withEvent:event]) {
// If it's a mouse event, we're done
return;
}
Log(LOG_D, @"Touch down");
// Notify of user interaction and start expiration timer
[self startInteractionTimer];
#if !TARGET_OS_TV
if (@available(iOS 13.4, *)) {
// NSArray<UITouch*> *arr1 = [[event allTouches] allObjects];
//// NSLog(@"touchesBegan - %lu",[[event allTouches] count]);
//// NSLog(@"touchesBegan - %lu",arr1.count);
//
// for(int i=0;i<arr1.count;i++){
// if (arr1[i].type == UITouchTypePencil) {
// [self sendStylusEvent:arr1[i]];
// return;
//
// }
// TemporarySettings* settings = [[[DataManager alloc] init] getSettings];
// if (settings.absoluteTouchMode) {
// [self handleUITouch:arr1[i] index:i];
// return;
// }
// }
for (UITouch* touch in touches) {
if (touch.type == UITouchTypePencil) {
if ([self sendStylusEvent:touch]) {
return;
}
}
TemporarySettings* settings = [[[DataManager alloc] init] getSettings];
if (settings.absoluteTouchMode) {
[self handleUITouch:touch index:0];
// return;
}
}
// NSLog(@"touchesBegan - allTouches %lu, pointerSet count %lu",[[event allTouches] count], [pointerIdSet count]);
}
#endif
if (![onScreenControls handleTouchDownEvent:touches]) {
// We still inform the touch handler even if we're going trigger the
// keyboard activation gesture. This is important to ensure the touch
// handler has a consistent view of touch events to correctly suppress
// activation of one or two finger gestures when a three finger gesture
// is triggered.
[touchHandler touchesBegan:touches withEvent:event];
// I refactored keyboard toggle by the CustomTapGestureRecognizer
// if ([[event allTouches] count] == keyboardToggleFingers) [self toggleKeyboard];
}
}
- (UIBarButtonItem *)createButtonWithImageNamed:(NSString *)imageName backgroundColor:(UIColor *)backgroundColor target:(id)target action:(SEL)action keyCode:(NSInteger)keyCode isToggleable:(BOOL)isToggleable {
UIImage *image = [UIImage imageNamed:imageName];
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setImage:image forState:UIControlStateNormal];
button.frame = CGRectMake(0, 0, 30, 30);
button.imageView.contentMode = UIViewContentModeScaleAspectFit;
button.imageView.backgroundColor = backgroundColor;
button.imageView.layer.cornerRadius = 10.0;
button.imageEdgeInsets = UIEdgeInsetsMake(6, 6, 6, 6);
[button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
objc_setAssociatedObject(button, "keyCode", @(keyCode), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(button, "isToggleable", @(isToggleable), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(button, "isOn", @(NO), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
UIBarButtonItem *barButton = [[UIBarButtonItem alloc] initWithCustomView:button];
return barButton;
}
- (void)toolbarButtonClicked:(UIButton *)sender {
BOOL isToggleable = [objc_getAssociatedObject(sender, "isToggleable") boolValue];
BOOL isOn = [objc_getAssociatedObject(sender, "isOn") boolValue];
if (isToggleable){
isOn = !isOn;
// Update the button's appearance based on its new state
if (isOn) {
sender.imageView.backgroundColor = [UIColor lightGrayColor];
} else {
sender.imageView.backgroundColor = [UIColor blackColor];
}
}
// Update the new on/off state of the button
objc_setAssociatedObject(sender, "isOn", @(isOn), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// Get the keyCode parameter and convert to short for key press event
short keyCode = [objc_getAssociatedObject(sender, "keyCode") shortValue];
// Close keyboard if done button clicked
if (!keyCode) {
[keyInputField resignFirstResponder];
isInputingText = false;
}
else {
// Send key press event using keyCode parameter, toggle if necessary
if (isToggleable){
if (isOn){
LiSendKeyboardEvent(keyCode, KEY_ACTION_DOWN, 0);
[keysDown addObject:@(keyCode)];
} else {
LiSendKeyboardEvent(keyCode, KEY_ACTION_UP, 0);
[keysDown removeObject:@(keyCode)];
}
}
else {
LiSendKeyboardEvent(keyCode, KEY_ACTION_DOWN, 0);
usleep(50 * 1000);
LiSendKeyboardEvent(keyCode, KEY_ACTION_UP, 0);
}
}
}
- (BOOL)handleMouseButtonEvent:(int)buttonAction forTouches:(NSSet *)touches withEvent:(UIEvent *)event {
#if !TARGET_OS_TV
if (@available(iOS 13.4, *)) {
UITouch* touch = [touches anyObject];
if (touch.type == UITouchTypeIndirectPointer) {
if (@available(iOS 14.0, *)) {
if ([GCMouse current] != nil) {
// We'll handle this with GCMouse. Do nothing here.
return YES;
}
}
UIEventButtonMask normalizedButtonMask;
// iOS 14 includes the released button in the buttonMask for the release
// event, while iOS 13 does not. Normalize that behavior here.
if (@available(iOS 14.0, *)) {
if (buttonAction == BUTTON_ACTION_RELEASE) {
normalizedButtonMask = lastMouseButtonMask & ~event.buttonMask;
}
else {
normalizedButtonMask = event.buttonMask;
}
}
else {
normalizedButtonMask = event.buttonMask;
}
UIEventButtonMask changedButtons = lastMouseButtonMask ^ normalizedButtonMask;
for (int i = BUTTON_LEFT; i <= BUTTON_X2; i++) {
UIEventButtonMask buttonFlag;
switch (i) {
// Right and Middle are reversed from what iOS uses
case BUTTON_RIGHT:
buttonFlag = UIEventButtonMaskForButtonNumber(2);
break;
case BUTTON_MIDDLE:
buttonFlag = UIEventButtonMaskForButtonNumber(3);
break;
default:
buttonFlag = UIEventButtonMaskForButtonNumber(i);
break;
}
if (changedButtons & buttonFlag) {
LiSendMouseButtonEvent(buttonAction, i);
}
}
lastMouseButtonMask = normalizedButtonMask;
return YES;
}
}
#endif
return NO;
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
#if !TARGET_OS_TV
if (@available(iOS 13.4, *)) {
for (UITouch* touch in touches) {
if (touch.type == UITouchTypePencil) {
if ([self sendStylusEvent:touch]) {
return;
}
}
TemporarySettings* settings = [[[DataManager alloc] init] getSettings];
if (settings.absoluteTouchMode) {
[self handleUITouch:touch index:0];
// return;
}
}
// NSLog(@"touchesMoved - allTouches %lu, pointerSet count %lu",[[event allTouches] count], [pointerIdSet count]);
UITouch *touch = [touches anyObject];
if (touch.type == UITouchTypeIndirectPointer) {
if (@available(iOS 14.0, *)) {
if ([GCMouse current] != nil) {
// We'll handle this with GCMouse. Do nothing here.
return;
}
}
// We must handle this event to properly support
// drags while the middle, X1, or X2 mouse buttons are
// held down. For some reason, left and right buttons
// don't require this, but we do it anyway for them too.
// Cursor movement without a button held down is handled
// in pointerInteraction:regionForRequest:defaultRegion.
[self updateCursorLocation:[touch locationInView:self] isMouse:YES];
return;
}
}
#endif
hasUserInteracted = YES;
if (![onScreenControls handleTouchMovedEvent:touches]) {
[touchHandler touchesMoved:touches withEvent:event];
}
}
- (void)pressesBegan:(NSSet<UIPress *> *)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 ([KeyboardSupport sendKeyEventForPress:press down:YES]) {
// This will prevent the legacy UITextField from receiving the event
handled = YES;
}
}
}
if (!handled) {
[super pressesBegan:presses withEvent:event];
}
}
- (void)pressesEnded:(NSSet<UIPress *> *)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 ([KeyboardSupport sendKeyEventForPress:press 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 {
if ([self handleMouseButtonEvent:BUTTON_ACTION_RELEASE
forTouches:touches
withEvent:event]) {
// If it's a mouse event, we're done
return;
}
Log(LOG_D, @"Touch up");
hasUserInteracted = YES;
#if !TARGET_OS_TV
if (@available(iOS 13.4, *)) {
for (UITouch* touch in touches) {
if (touch.type == UITouchTypePencil) {
if ([self sendStylusEvent:touch]) {
return;
}
}
TemporarySettings* settings = [[[DataManager alloc] init] getSettings];
if (settings.absoluteTouchMode) {
[self handleUITouch:touch index:0];
// return;
}
}
// NSLog(@"touchesEnded - allTouches %lu, pointerSet count %lu",[[event allTouches] count], [pointerIdSet count]);
}
#endif
if (![onScreenControls handleTouchUpEvent:touches]) {
[touchHandler touchesEnded:touches withEvent:event];
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
[touchHandler touchesCancelled:touches withEvent:event];
[self handleMouseButtonEvent:BUTTON_ACTION_RELEASE
forTouches:touches
withEvent:event];
#if !TARGET_OS_TV
if (@available(iOS 13.4, *)) {
for (UITouch* touch in touches) {
if (touch.type == UITouchTypePencil) {
[self sendStylusEvent:touch];
}
TemporarySettings* settings = [[[DataManager alloc] init] getSettings];
if (settings.absoluteTouchMode) {
[self handleUITouch:touch index:0];
// return;
}
}
// NSLog(@"touchesCancelled - allTouches %lu, pointerSet count %lu",[[event allTouches] count], [pointerIdSet count]);
}
#endif
}
#if !TARGET_OS_TV
- (void) updateCursorLocation:(CGPoint)location isMouse:(BOOL)isMouse {
CGPoint normalizedLocation = [self adjustCoordinatesForVideoArea:location];
CGSize videoSize = [self getVideoAreaSize];
// Send the mouse position relative to the video region if it has changed
// if we're receiving coordinates from a real mouse.
//
// NB: It is important for functionality (not just optimization) to only
// send it if the value has changed. We will receive one of these events
// any time the user presses a modifier key, which can result in errant
// mouse motion when using a Citrix X1 mouse.
if (normalizedLocation.x != lastMouseX || normalizedLocation.y != lastMouseY || !isMouse) {
if (lastMouseX != 0 || lastMouseY != 0 || !isMouse) {
LiSendMousePositionEvent(normalizedLocation.x, normalizedLocation.y, videoSize.width, videoSize.height);
}
if (isMouse) {
lastMouseX = normalizedLocation.x;
lastMouseY = normalizedLocation.y;
}
}
}
- (UIPointerRegion *)pointerInteraction:(UIPointerInteraction *)interaction
regionForRequest:(UIPointerRegionRequest *)request
defaultRegion:(UIPointerRegion *)defaultRegion API_AVAILABLE(ios(13.4)) {
if (@available(iOS 14.0, *)) {
if ([GCMouse current] != nil) {
// We'll handle this with GCMouse. Do nothing here.
return nil;
}
}
// This logic mimics what iOS does with AVLayerVideoGravityResizeAspect
CGSize videoSize;
CGPoint videoOrigin;
if (self.bounds.size.width > self.bounds.size.height * streamAspectRatio) {
videoSize = CGSizeMake(self.bounds.size.height * streamAspectRatio, self.bounds.size.height);
} else {
videoSize = CGSizeMake(self.bounds.size.width, self.bounds.size.width / streamAspectRatio);
}
videoOrigin = CGPointMake(self.bounds.size.width / 2 - videoSize.width / 2,
self.bounds.size.height / 2 - videoSize.height / 2);
// Move the cursor on the host if no buttons are pressed.
// Motion with buttons pressed in handled in touchesMoved:
if (lastMouseButtonMask == 0) {
[self updateCursorLocation:request.location isMouse:YES];
}
// The pointer interaction should cover the video region only
return [UIPointerRegion regionWithRect:CGRectMake(videoOrigin.x, videoOrigin.y, videoSize.width, videoSize.height) identifier:nil];
}
- (UIPointerStyle *)pointerInteraction:(UIPointerInteraction *)interaction styleForRegion:(UIPointerRegion *)region API_AVAILABLE(ios(13.4)) {
// Always hide the mouse cursor over our stream view
return [UIPointerStyle hiddenPointerStyle];
}
- (void)mouseWheelMovedContinuous:(UIPanGestureRecognizer *)gesture {
switch (gesture.state) {
case UIGestureRecognizerStateBegan:
case UIGestureRecognizerStateChanged:
break;
case UIGestureRecognizerStateEnded:
default:
// Ignore recognition failure and other states
lastScrollTranslation = CGPointMake(0, 0);
return;
}
CGPoint currentScrollTranslation = [gesture translationInView:self];
const short translationMultiplier = 120 * 20; // WHEEL_DELTA * 20
{
short translationDeltaY = ((currentScrollTranslation.y - lastScrollTranslation.y) / self.bounds.size.height) * translationMultiplier;
if (translationDeltaY != 0) {
LiSendHighResScrollEvent(translationDeltaY);
lastScrollTranslation = currentScrollTranslation;
}
}
{
short translationDeltaX = ((currentScrollTranslation.x - lastScrollTranslation.x) / self.bounds.size.width) * translationMultiplier;
if (translationDeltaX != 0) {
// Direction is reversed from vertical scrolling
LiSendHighResHScrollEvent(-translationDeltaX);
lastScrollTranslation = currentScrollTranslation;
}
}
}
- (void)mouseWheelMovedDiscrete:(UIPanGestureRecognizer *)gesture {
switch (gesture.state) {
case UIGestureRecognizerStateBegan:
case UIGestureRecognizerStateChanged:
break;
case UIGestureRecognizerStateEnded:
default:
// Ignore recognition failure and other states
lastScrollTranslation = CGPointMake(0, 0);
return;
}
// Using velocityInView is 0 for discrete scroll events
// when scrolling very slowly, but translationInView does work.
CGPoint currentScrollTranslation = [gesture translationInView:self];
{
short translationDeltaY = currentScrollTranslation.y - lastScrollTranslation.y;
if (translationDeltaY != 0) {
LiSendScrollEvent(translationDeltaY > 0 ? 1 : -1);
}
}
{
short translationDeltaX = currentScrollTranslation.x - lastScrollTranslation.x;
if (translationDeltaX != 0) {
// Direction is reversed from vertical scrolling
LiSendHScrollEvent(translationDeltaX < 0 ? 1 : -1);
}
}
lastScrollTranslation = currentScrollTranslation;
}
#endif
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
if (@available(iOS 13.0, *)) {
// Disable the 3 finger tap gestures that trigger the copy/paste/undo toolbar on iOS 13+
return gestureRecognizer.name == nil || ![gestureRecognizer.name hasPrefix:@"kbProductivity."];
}
else {
return YES;
}
}
- (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)textFieldDidEndEditing:(UITextField *)textField {
for (NSNumber* keyCode in keysDown) {
LiSendKeyboardEvent([keyCode shortValue], KEY_ACTION_UP, 0);
}
[keysDown removeAllObjects];
}
- (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
// Check if any characters exist which can't be represented in a basic key event
for (int i = 1; i < [inputText length]; i++) {
struct KeyEvent event = [KeyboardSupport translateKeyEvent:[inputText characterAtIndex:i] withModifierFlags:0];
if (event.keycode == 0) {
// We found an unknown key, so send the entire string as UTF-8
const char* utf8String = [inputText UTF8String];
// Skip the first character which is our sentinel
LiSendUtf8TextEvent(utf8String + 1, (int)strlen(utf8String) - 1);
return;
}
}
// We didn't find any unknown characters, so send them all as basic key events
for (int i = 1; i < [inputText length]; i++) {
struct KeyEvent event = [KeyboardSupport translateKeyEvent:[inputText characterAtIndex:i] withModifierFlags:0];
assert(event.keycode != 0);
[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);
}
// Let the host know these are not (necessarily) normalized to US English scancodes
LiSendKeyboardEvent2(event.keycode, KEY_ACTION_DOWN, event.modifier, SS_KBE_FLAG_NON_NORMALIZED);
usleep(50 * 1000);
LiSendKeyboardEvent2(event.keycode, KEY_ACTION_UP, event.modifier, SS_KBE_FLAG_NON_NORMALIZED);
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;
}
- (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 {
accumulatedMouseDeltaX += deltaX / X1_MOUSE_SPEED_DIVISOR;
accumulatedMouseDeltaY += deltaY / X1_MOUSE_SPEED_DIVISOR;
short shortX = (short)accumulatedMouseDeltaX;
short shortY = (short)accumulatedMouseDeltaY;
if (shortX == 0 && shortY == 0) {
return;
}
LiSendMouseMoveEvent(shortX, shortY);
accumulatedMouseDeltaX -= shortX;
accumulatedMouseDeltaY -= 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);
}
#if !TARGET_OS_TV
- (BOOL)isMultipleTouchEnabled {
return YES;
}
#endif
@end