From 873269e410631940872c487f060126ba84575089 Mon Sep 17 00:00:00 2001 From: True Zhuanjia Date: Sat, 1 Jun 2024 16:37:04 +0800 Subject: [PATCH] Native touch oriented improvements --- .gitignore | 1 + Limelight/Database/DataManager.h | 9 +- Limelight/Database/DataManager.m | 14 + Limelight/Database/TemporarySettings.h | 8 + Limelight/Database/TemporarySettings.m | 7 + Limelight/Input/AbsoluteTouchHandler.m | 158 +-- Limelight/Input/CustomTapGestureRecognizer.h | 22 + Limelight/Input/CustomTapGestureRecognizer.m | 57 + Limelight/Input/NativeTouchHandler.h | 34 + Limelight/Input/NativeTouchHandler.m | 139 +++ Limelight/Input/StreamView.m | 340 ++++-- Limelight/Input/StreamView.m.orig | 1078 +++++++++++++++++ Limelight/Limelight-Info.plist | 4 +- .../Moonlight v1.10.xcdatamodel/contents | 13 +- .../CustomEdgeSwipeGestureRecognizer.h | 20 + .../CustomEdgeSwipeGestureRecognizer.m | 62 + .../ViewControllers/SettingsViewController.h | 13 + .../ViewControllers/SettingsViewController.m | 74 ++ .../StreamFrameViewController.m | 55 +- Moonlight TV/Info.plist | 2 +- Moonlight.xcodeproj/project.pbxproj | 78 +- .../contents.xcworkspacedata | 2 +- iPad.storyboard | 175 ++- iPhone.storyboard | 170 ++- moonlight-common/moonlight-common-c | 2 +- 25 files changed, 2254 insertions(+), 283 deletions(-) create mode 100644 Limelight/Input/CustomTapGestureRecognizer.h create mode 100644 Limelight/Input/CustomTapGestureRecognizer.m create mode 100644 Limelight/Input/NativeTouchHandler.h create mode 100644 Limelight/Input/NativeTouchHandler.m create mode 100644 Limelight/Input/StreamView.m.orig create mode 100644 Limelight/ViewControllers/CustomEdgeSwipeGestureRecognizer.h create mode 100644 Limelight/ViewControllers/CustomEdgeSwipeGestureRecognizer.m diff --git a/.gitignore b/.gitignore index efa4bb33..1b4a98af 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ **/xcschemes/*.xcscheme Build DerivedData +*.xccheckout diff --git a/Limelight/Database/DataManager.h b/Limelight/Database/DataManager.h index 5717a795..eb0d5f5b 100644 --- a/Limelight/Database/DataManager.h +++ b/Limelight/Database/DataManager.h @@ -17,8 +17,15 @@ framerate:(NSInteger)framerate height:(NSInteger)height width:(NSInteger)width - audioConfig:(NSInteger)audioConfig + audioConfig:(NSInteger)audioConfig onscreenControls:(NSInteger)onscreenControls + keyboardToggleFingers:(NSInteger)keyboardToggleFingers + swipeExitScreenEdge:(NSInteger)swipeExitScreenEdge + swipeToExitDistance:(CGFloat)swipeToExitDistance + pointerVelocityModeDivider:(CGFloat)pointerVelocityModeDivider + touchPointerVelocityFactor:(CGFloat)touchPointerVelocityFactor + liftStreamViewForKeyboard:(BOOL)liftStreamViewForKeyboard + showKeyboardToolbar:(BOOL)showKeyboardToolbar optimizeGames:(BOOL)optimizeGames multiController:(BOOL)multiController swapABXYButtons:(BOOL)swapABXYButtons diff --git a/Limelight/Database/DataManager.m b/Limelight/Database/DataManager.m index 0f5edeb1..6c6191c7 100644 --- a/Limelight/Database/DataManager.m +++ b/Limelight/Database/DataManager.m @@ -58,6 +58,13 @@ width:(NSInteger)width audioConfig:(NSInteger)audioConfig onscreenControls:(NSInteger)onscreenControls + keyboardToggleFingers:(NSInteger)keyboardToggleFingers + swipeExitScreenEdge:(NSInteger)swipeExitScreenEdge + swipeToExitDistance:(CGFloat)swipeToExitDistance + pointerVelocityModeDivider:(CGFloat)pointerVelocityModeDivider + touchPointerVelocityFactor:(CGFloat)touchPointerVelocityFactor + liftStreamViewForKeyboard:(BOOL)liftStreamViewForKeyboard + showKeyboardToolbar:(BOOL)showKeyboardToolbar optimizeGames:(BOOL)optimizeGames multiController:(BOOL)multiController swapABXYButtons:(BOOL)swapABXYButtons @@ -77,6 +84,13 @@ settingsToSave.width = [NSNumber numberWithInteger:width]; settingsToSave.audioConfig = [NSNumber numberWithInteger:audioConfig]; settingsToSave.onscreenControls = [NSNumber numberWithInteger:onscreenControls]; + settingsToSave.keyboardToggleFingers = [NSNumber numberWithInteger:(uint16_t)keyboardToggleFingers]; + settingsToSave.swipeExitScreenEdge = [NSNumber numberWithInteger:(uint32_t)swipeExitScreenEdge]; + settingsToSave.swipeToExitDistance = [NSNumber numberWithFloat:swipeToExitDistance]; + settingsToSave.pointerVelocityModeDivider = [NSNumber numberWithFloat:pointerVelocityModeDivider]; + settingsToSave.touchPointerVelocityFactor = [NSNumber numberWithFloat:touchPointerVelocityFactor]; + settingsToSave.liftStreamViewForKeyboard = liftStreamViewForKeyboard; + settingsToSave.showKeyboardToolbar = showKeyboardToolbar; settingsToSave.optimizeGames = optimizeGames; settingsToSave.multiController = multiController; settingsToSave.swapABXYButtons = swapABXYButtons; diff --git a/Limelight/Database/TemporarySettings.h b/Limelight/Database/TemporarySettings.h index da35b30c..d55a00f7 100644 --- a/Limelight/Database/TemporarySettings.h +++ b/Limelight/Database/TemporarySettings.h @@ -18,6 +18,11 @@ @property (nonatomic, retain) NSNumber * width; @property (nonatomic, retain) NSNumber * audioConfig; @property (nonatomic, retain) NSNumber * onscreenControls; +@property (nonatomic, retain) NSNumber * keyboardToggleFingers; +@property (nonatomic, retain) NSNumber * swipeExitScreenEdge; +@property (nonatomic, retain) NSNumber * swipeToExitDistance; +@property (nonatomic, retain) NSNumber * touchPointerVelocityFactor; +@property (nonatomic, retain) NSNumber * pointerVelocityModeDivider; @property (nonatomic, retain) NSString * uniqueId; @property (nonatomic) enum { CODEC_PREF_AUTO, @@ -34,6 +39,9 @@ @property (nonatomic) BOOL btMouseSupport; @property (nonatomic) BOOL absoluteTouchMode; @property (nonatomic) BOOL statsOverlay; +@property (nonatomic) BOOL liftStreamViewForKeyboard; +@property (nonatomic) BOOL showKeyboardToolbar; + - (id) initFromSettings:(Settings*)settings; diff --git a/Limelight/Database/TemporarySettings.m b/Limelight/Database/TemporarySettings.m index e627e0c1..08657aa6 100644 --- a/Limelight/Database/TemporarySettings.m +++ b/Limelight/Database/TemporarySettings.m @@ -85,6 +85,13 @@ self.btMouseSupport = settings.btMouseSupport; self.absoluteTouchMode = settings.absoluteTouchMode; self.statsOverlay = settings.statsOverlay; + self.keyboardToggleFingers = settings.keyboardToggleFingers; + self.swipeExitScreenEdge = settings.swipeExitScreenEdge; + self.swipeToExitDistance = settings.swipeToExitDistance; + self.liftStreamViewForKeyboard = settings.liftStreamViewForKeyboard; + self.showKeyboardToolbar = settings.showKeyboardToolbar; + self.touchPointerVelocityFactor = settings.touchPointerVelocityFactor; + self.pointerVelocityModeDivider = settings.pointerVelocityModeDivider; #endif self.uniqueId = settings.uniqueId; diff --git a/Limelight/Input/AbsoluteTouchHandler.m b/Limelight/Input/AbsoluteTouchHandler.m index b9b8c59c..0066283b 100644 --- a/Limelight/Input/AbsoluteTouchHandler.m +++ b/Limelight/Input/AbsoluteTouchHandler.m @@ -38,83 +38,85 @@ return self; } -- (void)onLongPressStart:(NSTimer*)timer { - // Raise the left click and start a right click - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT); -} - -- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { - // Ignore touch down events with more than one finger - if ([[event allTouches] count] > 1) { - return; - } - - UITouch* touch = [touches anyObject]; - CGPoint touchLocation = [touch locationInView:view]; - - // Don't reposition for finger down events within the deadzone. This makes double-clicking easier. - if (touch.timestamp - lastTouchUp.timestamp > DOUBLE_TAP_DEAD_ZONE_DELAY || - sqrt(pow((touchLocation.x / view.bounds.size.width) - (lastTouchUpLocation.x / view.bounds.size.width), 2) + - pow((touchLocation.y / view.bounds.size.height) - (lastTouchUpLocation.y / view.bounds.size.height), 2)) > DOUBLE_TAP_DEAD_ZONE_DELTA) { - [view updateCursorLocation:touchLocation isMouse:NO]; - } - - // Press the left button down - LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); - - // Start the long press timer - longPressTimer = [NSTimer scheduledTimerWithTimeInterval:LONG_PRESS_ACTIVATION_DELAY - target:self - selector:@selector(onLongPressStart:) - userInfo:nil - repeats:NO]; - - lastTouchDown = touch; - lastTouchDownLocation = touchLocation; -} - -- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { - // Ignore touch move events with more than one finger - if ([[event allTouches] count] > 1) { - return; - } - - UITouch* touch = [touches anyObject]; - CGPoint touchLocation = [touch locationInView:view]; - - if (sqrt(pow((touchLocation.x / view.bounds.size.width) - (lastTouchDownLocation.x / view.bounds.size.width), 2) + - pow((touchLocation.y / view.bounds.size.height) - (lastTouchDownLocation.y / view.bounds.size.height), 2)) > LONG_PRESS_ACTIVATION_DELTA) { - // Moved too far since touch down. Cancel the long press timer. - [longPressTimer invalidate]; - longPressTimer = nil; - } - - [view updateCursorLocation:[[touches anyObject] locationInView:view] isMouse:NO]; -} - -- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { - // Only fire this logic if all touches have ended - if ([[event allTouches] count] == [touches count]) { - // Cancel the long press timer - [longPressTimer invalidate]; - longPressTimer = nil; - - // Left button up on finger up - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); - - // Raise right button too in case we triggered a long press gesture - LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT); - - // Remember this last touch for touch-down deadzoning - lastTouchUp = [touches anyObject]; - lastTouchUpLocation = [lastTouchUp locationInView:view]; - } -} - -- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { - // Treat this as a normal touchesEnded event - [self touchesEnded:touches withEvent:event]; -} +//- (void)onLongPressStart:(NSTimer*)timer { +// // Raise the left click and start a right click +// LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); +// LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT); +//} +// +//- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { +// // Ignore touch down events with more than one finger +// return; +// if ([[event allTouches] count] > 1) { +// return; +// } +// +// UITouch* touch = [touches anyObject]; +// CGPoint touchLocation = [touch locationInView:view]; +// +// // Don't reposition for finger down events within the deadzone. This makes double-clicking easier. +// if (touch.timestamp - lastTouchUp.timestamp > DOUBLE_TAP_DEAD_ZONE_DELAY || +// sqrt(pow((touchLocation.x / view.bounds.size.width) - (lastTouchUpLocation.x / view.bounds.size.width), 2) + +// pow((touchLocation.y / view.bounds.size.height) - (lastTouchUpLocation.y / view.bounds.size.height), 2)) > DOUBLE_TAP_DEAD_ZONE_DELTA) { +// [view updateCursorLocation:touchLocation isMouse:NO]; +// } +// +// // Press the left button down +// LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); +// +// // Start the long press timer +// longPressTimer = [NSTimer scheduledTimerWithTimeInterval:LONG_PRESS_ACTIVATION_DELAY +// target:self +// selector:@selector(onLongPressStart:) +// userInfo:nil +// repeats:NO]; +// +// lastTouchDown = touch; +// lastTouchDownLocation = touchLocation; +//} +// +//- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { +// // Ignore touch move events with more than one finger +// return; +// if ([[event allTouches] count] > 1) { +// return; +// } +// +// UITouch* touch = [touches anyObject]; +// CGPoint touchLocation = [touch locationInView:view]; +// +// if (sqrt(pow((touchLocation.x / view.bounds.size.width) - (lastTouchDownLocation.x / view.bounds.size.width), 2) + +// pow((touchLocation.y / view.bounds.size.height) - (lastTouchDownLocation.y / view.bounds.size.height), 2)) > LONG_PRESS_ACTIVATION_DELTA) { +// // Moved too far since touch down. Cancel the long press timer. +// [longPressTimer invalidate]; +// longPressTimer = nil; +// } +// +// [view updateCursorLocation:[[touches anyObject] locationInView:view] isMouse:NO]; +//} +// +//- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { +// // Only fire this logic if all touches have ended +// if ([[event allTouches] count] == [touches count]) { +// // Cancel the long press timer +// [longPressTimer invalidate]; +// longPressTimer = nil; +// +// // Left button up on finger up +// LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); +// +// // Raise right button too in case we triggered a long press gesture +// LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT); +// +// // Remember this last touch for touch-down deadzoning +// lastTouchUp = [touches anyObject]; +// lastTouchUpLocation = [lastTouchUp locationInView:view]; +// } +//} +// +//- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { +// // Treat this as a normal touchesEnded event +// [self touchesEnded:touches withEvent:event]; +//} @end diff --git a/Limelight/Input/CustomTapGestureRecognizer.h b/Limelight/Input/CustomTapGestureRecognizer.h new file mode 100644 index 00000000..68f74173 --- /dev/null +++ b/Limelight/Input/CustomTapGestureRecognizer.h @@ -0,0 +1,22 @@ +// +// CustomTapGestureRecognizer.h +// Moonlight +// +// Created by ZWM on 2024/5/15. +// Copyright © 2024 Moonlight Game Streaming Project. All rights reserved. +// + +#ifndef CustomTapGestureRecognizer_h +#define CustomTapGestureRecognizer_h + + + +@interface CustomTapGestureRecognizer : UIGestureRecognizer + +@property (nonatomic, assign) uint8_t numberOfTouchesRequired; +@property (nonatomic, assign) double tapDownTimeThreshold; +@property (atomic, readonly) CGFloat lowestTouchPointHeight; + + +@end +#endif /* CustomTapGestureRecognizer_h */ diff --git a/Limelight/Input/CustomTapGestureRecognizer.m b/Limelight/Input/CustomTapGestureRecognizer.m new file mode 100644 index 00000000..9962f890 --- /dev/null +++ b/Limelight/Input/CustomTapGestureRecognizer.m @@ -0,0 +1,57 @@ +// +// CustomTapGestureRecognizer.m +// Moonlight +// +// Created by ZWM on 2024/5/15. +// Copyright © 2024 Moonlight Game Streaming Project. All rights reserved. +// + +#import +#import +#import "CustomTapGestureRecognizer.h" + +@implementation CustomTapGestureRecognizer + +static NSTimeInterval multiFingerDownTime; +static bool multiFingerDown; +static CGFloat screenHeightInPoints; +static CGFloat lowestTouchPointYCoord = 0.0; + +- (instancetype)initWithTarget:(nullable id)target action:(nullable SEL)action { + self = [super initWithTarget:target action:action]; + screenHeightInPoints = CGRectGetHeight([[UIScreen mainScreen] bounds]); + return self; +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + // Check if the number of touches and taps meets the required criteria + if ([[event allTouches] count] == _numberOfTouchesRequired) { + multiFingerDownTime = CACurrentMediaTime(); + multiFingerDown = true; + + for(UITouch *touch in [event allTouches]){ + if(lowestTouchPointYCoord < [touch locationInView:self.view].y) lowestTouchPointYCoord = [touch locationInView:self.view].y; + } + _lowestTouchPointHeight = screenHeightInPoints - lowestTouchPointYCoord; + self.state = UIGestureRecognizerStatePossible; + } +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + // [super touchesEnded:touches withEvent:event]; + if(multiFingerDown && [[event allTouches] count] == [touches count]){ + multiFingerDown = false; + if((CACurrentMediaTime() - multiFingerDownTime) < _tapDownTimeThreshold / 1000){ + lowestTouchPointYCoord = 0.0; //reset for next recognition + self.state = UIGestureRecognizerStateRecognized; + } + } +} + ++ (CGFloat)lowestTouchPointHeight{ + @synchronized (self){ + return self.lowestTouchPointHeight; + } +} + +@end diff --git a/Limelight/Input/NativeTouchHandler.h b/Limelight/Input/NativeTouchHandler.h new file mode 100644 index 00000000..1ebb7b79 --- /dev/null +++ b/Limelight/Input/NativeTouchHandler.h @@ -0,0 +1,34 @@ +// +// NativeTouchHandler.h +// Moonlight +// +// Created by ZWM on 2024/5/14. +// Copyright © 2024 Moonlight Game Streaming Project. All rights reserved. +// + +#import "StreamView.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface TouchPointer : NSObject + ++ (void)initContextWith:(StreamView *)view; ++ (void)populatePointerId:(UITouch*)touch; ++ (void)removePointerId:(UITouch*)touch; ++ (uint32_t) retrievePointerIdFromDict:(UITouch*)touch; ++ (void)setPointerVelocityDivider:(CGFloat)dividerLocation; ++ (void)setPointerVelocityFactor:(CGFloat)velocityFactor; ++ (void)populatePointerObjIntoDict:(UITouch*)touch; ++ (void)removePointerObjFromDict:(UITouch*)touch; ++ (void)updatePointerObjInDict:(UITouch *)touch; ++ (CGPoint)selectCoordsFor:(UITouch *)touch; + + +- (instancetype)initWith:(UITouch *)touch; +@end + + + + +NS_ASSUME_NONNULL_END + diff --git a/Limelight/Input/NativeTouchHandler.m b/Limelight/Input/NativeTouchHandler.m new file mode 100644 index 00000000..ff347d6b --- /dev/null +++ b/Limelight/Input/NativeTouchHandler.m @@ -0,0 +1,139 @@ +// +// NativeTouchHandler.m +// Moonlight +// +// Created by ZWM on 2024/5/14. +// Copyright © 2024 Moonlight Game Streaming Project. All rights reserved. +// + +#import +#import "NativeTouchHandler.h" +#include + + +// Use a Dictionary to store UITouch object's memory address as key, and pointerId as value,字典存放UITouch对象地址和pointerId映射关系 +// pointerId will be generated from electronic noise, by arc4_random, pointerId,由随机噪声生成 +// Use a NSSet store pointerId, for quick repeition inquiry, NSSet存放活跃的pointerId合集,用于快速查找,以防重复. +static NSMutableDictionary *pointerIdDict; //pointerId Dict for native touch. +static NSMutableSet *pointerIdSet; //pointerIdSet for native touch. +static NSMutableDictionary *pointerDict; + +static CGFloat pointerVelocityFactor; +static CGFloat pointerVelocityDivider; +static CGFloat pointerVelocityDividerLocation; + +StreamView *streamView; + +@implementation TouchPointer{ + CGPoint initialPoint; + CGPoint latestPoint; + CGPoint previousPoint; + CGPoint latestRelativePoint; + CGPoint previousRelativePoint; + CGFloat velocityX; + CGFloat velocityY; +} + ++ (void)setPointerVelocityDivider:(CGFloat)dividerLocation{ + pointerVelocityDivider = dividerLocation; +} + ++ (void)setPointerVelocityFactor:(CGFloat)velocityFactor{ + pointerVelocityFactor = velocityFactor; +} + + +- (instancetype)initWith:(UITouch *)touch{ + self = [self init]; + self->initialPoint = [touch locationInView:streamView]; + self->latestPoint = self->initialPoint; + self->latestRelativePoint = self->initialPoint; + return self; + } + +- (void)updatePointerCoords:(UITouch *)touch{ + previousPoint = latestPoint; + latestPoint = [touch locationInView:streamView]; + if (pointerVelocityFactor != 1.0f){ + velocityX = latestPoint.x - previousPoint.x; + velocityY = latestPoint.y - previousPoint.y; + previousRelativePoint = latestRelativePoint; + latestRelativePoint.x = previousRelativePoint.x + pointerVelocityFactor * velocityX; + latestRelativePoint.y = previousRelativePoint.y + pointerVelocityFactor * velocityY; + } +} + ++ (void)initContextWith:(StreamView *)view{ + streamView = view; + pointerIdDict = [NSMutableDictionary dictionary]; + pointerIdSet = [NSMutableSet set]; + pointerDict = [NSMutableDictionary dictionary]; + pointerVelocityDividerLocation = CGRectGetWidth([[UIScreen mainScreen] bounds]) * pointerVelocityDivider; + NSLog(@"pointerVelocityDivider: %.2f", pointerVelocityDivider); + NSLog(@"pointerVelocityDividerLocation: %.2f", pointerVelocityDividerLocation); +} + ++ (void)populatePointerObjIntoDict:(UITouch*)touch{ + [pointerDict setObject:[[TouchPointer alloc] initWith:touch] forKey:@((uint64_t)touch)]; +} + ++ (void)removePointerObjFromDict:(UITouch*)touch{ + uint64_t eventAddrValue = (uint64_t)touch; + TouchPointer* pointer = [pointerDict objectForKey:@(eventAddrValue)]; + if(pointer != nil){ + [pointerDict removeObjectForKey:@(eventAddrValue)]; + } + +} + ++ (void)updatePointerObjInDict:(UITouch *)touch{ + [[pointerDict objectForKey:@((uint64_t)touch)] updatePointerCoords:touch]; +} + + ++ (CGPoint)selectCoordsFor:(UITouch *)touch{ + TouchPointer *pointer = [pointerDict objectForKey:@((uint64_t)touch)]; + if((pointer -> initialPoint).x > pointerVelocityDividerLocation){ //if first contact coords locates on the right side of divider. + return pointer->latestRelativePoint; + } + return [touch locationInView:streamView]; +} + + + +// 随机生成pointerId并填入NSDict和NSSet +// generate & populate pointerId into NSDict & NSSet, called in UITouchPhaseBegan ++ (void)populatePointerId:(UITouch*)touch{ + uint64_t eventAddrValue = (uint64_t)touch; + uint32_t randomPointerId = arc4random_uniform(UINT32_MAX); // generate pointerId from eletronic noise. + for(;;){ + if([pointerIdSet containsObject:@(randomPointerId)]) randomPointerId = arc4random_uniform(UINT32_MAX); // in case of new pointerId collides with existing ones, generate again. + else{ // populate pointerId into NSDict & NSSet. + [pointerIdDict setObject:@(randomPointerId) forKey:@(eventAddrValue)]; + [pointerIdSet addObject:@(randomPointerId)]; + return; + } + } +} + + + +// remove pointerId in UITouchPhaseEnded condition ++ (void)removePointerId:(UITouch*)touch{ + uint64_t eventAddrValue = (uint64_t)touch; + NSNumber* pointerIdObj = [pointerIdDict objectForKey:@(eventAddrValue)]; + if(pointerIdObj != nil){ + [pointerIdSet removeObject:pointerIdObj]; + [pointerIdDict removeObjectForKey:@(eventAddrValue)]; + } +} + +// 从字典中获取UITouch事件对应的pointerId +// call this only when NSDcit & NSSet is up-to-date. ++ (uint32_t) retrievePointerIdFromDict:(UITouch*)touch{ + return [[pointerIdDict objectForKey:@((uint64_t)touch)] unsignedIntValue]; +} + + + +@end diff --git a/Limelight/Input/StreamView.m b/Limelight/Input/StreamView.m index 7dedeb35..9afc56ad 100644 --- a/Limelight/Input/StreamView.m +++ b/Limelight/Input/StreamView.m @@ -11,20 +11,26 @@ #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 { + TemporarySettings* settings; + OnScreenControls* onScreenControls; KeyboardInputField* keyInputField; BOOL isInputingText; NSMutableSet* keysDown; - float streamAspectRatio; + + // iOS 13.4 mouse support NSInteger lastMouseButtonMask; @@ -44,15 +50,18 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; BOOL hasUserInteracted; NSDictionary *dictCodes; + CustomTapGestureRecognizer *keyboardToggleRecognizer; + CGFloat HeightViewLiftedTo; } - (void) setupStreamView:(ControllerSupport*)controllerSupport interactionDelegate:(id)interactionDelegate config:(StreamConfiguration*)streamConfig { + self->interactionDelegate = interactionDelegate; self->streamAspectRatio = (float)streamConfig.width / (float)streamConfig.height; - TemporarySettings* settings = [[[DataManager alloc] init] getSettings]; + settings = [[[DataManager alloc] init] getSettings]; keysDown = [[NSMutableSet alloc] init]; keyInputField = [[KeyboardInputField alloc] initWithFrame:CGRectZero]; @@ -62,13 +71,33 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; [keyInputField setSpellCheckingType:UITextSpellCheckingTypeNo]; [self addSubview:keyInputField]; + isInputingText = false; + 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]; + // self->touchHandler = [[AbsoluteTouchHandler alloc] initWithView:self]; + [TouchPointer setPointerVelocityDivider:settings.pointerVelocityModeDivider.floatValue]; + [TouchPointer setPointerVelocityFactor:settings.touchPointerVelocityFactor.floatValue]; + [TouchPointer initContextWith:self]; } else { self->touchHandler = [[RelativeTouchHandler alloc] initWithView:self]; @@ -87,25 +116,24 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; 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]; +// [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) @@ -129,6 +157,70 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; [self becomeFirstResponder]; } +- (void)keyboardWillShow:(NSNotification *)notification{ + if(settings.liftStreamViewForKeyboard && !isInputingText){ + NSDictionary *userInfo = notification.userInfo; + // Get the keyboard size from the notification + CGRect keyboardFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; + // NSLog(@"keyboard height %f, lowest point height %f", keyboardFrame.size.height, keyboardToggleRecognizer.lowestTouchPointHeight); + if(keyboardFrame.size.height < CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.25) return; // return in case of abnormal keyboard height + HeightViewLiftedTo = keyboardFrame.size.height - keyboardToggleRecognizer.lowestTouchPointHeight + CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.1; // lift the StreamView to the height of lowest touch point of multi-finger tap gesture, while reserving the view of 1/10 screen height for remote typing. + if(HeightViewLiftedTo < 0) HeightViewLiftedTo = 0; // set HeightViewLiftedTo to 0 if it is high enough and not going to be covered by keyboard. + CGRect liftedStreamFrame = self.frame; + liftedStreamFrame.origin.y -= HeightViewLiftedTo; + self.frame = liftedStreamFrame; + isInputingText = true; + } + // NSLog(@"keyboard will show"); +} + +- (void)keyboardWillHide:(NSNotification *)notification{ + + if(isInputingText){ + CGRect liftedStreamFrame = self.frame; + // recover view position in keyboard hiding. + liftedStreamFrame.origin.y += HeightViewLiftedTo; + self.frame = liftedStreamFrame; + isInputingText = false; + } + // NSLog(@"keyboard will hide"); +} + + +- (void)toggleKeyboard{ + if (isInputingText) { + Log(LOG_D, @"Closing the keyboard"); + [keyInputField resignFirstResponder]; + } else { + 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(settings.showKeyboardToolbar){ + 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]; + } +} + - (void)startInteractionTimer { // Restart user interaction tracking hasUserInteracted = NO; @@ -240,6 +332,75 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; return 90 - MIN(90, altitudeDegs); } + +- (void)sendTouchEvent:(UITouch*)event touchType:(uint8_t)touchType{ + CGPoint targetCoords; + if(settings.pointerVelocityModeDivider.floatValue != 1.0 && event.phase == UITouchPhaseMoved) targetCoords = [TouchPointer selectCoordsFor:event]; // coordinates of touch pointer replaced to relative ones here. + else targetCoords = [event locationInView:self]; + CGPoint location = [self adjustCoordinatesForVideoArea:targetCoords]; + CGSize videoSize = [self getVideoAreaSize]; + LiSendTouchEvent(touchType,[TouchPointer 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; + //BOOL pointerVelocityScaleEnabled = (settings.pointerVelocityModeDivider.floatValue != 1.0); // when the divider is 1.0, means 0% of screen shall pass velocity-scaled pointer to sunshine. + // 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; + [TouchPointer populatePointerId:event]; //获取并记录pointerId + if(settings.pointerVelocityModeDivider.floatValue != 1.0) [TouchPointer populatePointerObjIntoDict:event]; + break; + case UITouchPhaseMoved://移动 + case UITouchPhaseStationary: + type = LI_TOUCH_EVENT_MOVE; + if(settings.pointerVelocityModeDivider.floatValue != 1.0) [TouchPointer updatePointerObjInDict:event]; + break; + case UITouchPhaseEnded://触摸结束 + type = LI_TOUCH_EVENT_UP; + [self sendTouchEvent:event touchType:type]; //先发送,再删除 + [TouchPointer removePointerId:event]; //删除pointerId + if(settings.pointerVelocityModeDivider.floatValue != 1.0) [TouchPointer removePointerObjFromDict:event]; + return; + case UITouchPhaseCancelled://触摸取消 + type = LI_TOUCH_EVENT_CANCEL; + [self sendTouchEvent:event touchType:type]; //先发送,再删除 + [TouchPointer removePointerId:event]; //删除pointerId + if(settings.pointerVelocityModeDivider.floatValue != 1.0) [TouchPointer removePointerObjFromDict:event]; + return; + case UITouchPhaseRegionEntered://停留 + case UITouchPhaseRegionMoved://停留 + type = LI_TOUCH_EVENT_HOVER; + break; + default: + return; + } + [self sendTouchEvent:event touchType:type]; +} + + + - (BOOL)sendStylusEvent:(UITouch*)event { uint8_t type; @@ -319,6 +480,40 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; #endif - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + +#if !TARGET_OS_TV + if (@available(iOS 13.4, *)) { + // NSArray *arr1 = [[event allTouches] allObjects]; + //// NSLog(@"touchesBegan - %lu",[[event allTouches] count]); + //// NSLog(@"touchesBegan - %lu",arr1.count); + // + // for(int i=0;i +#import "DataManager.h" +#import "ControllerSupport.h" +#import "KeyboardSupport.h" +#import "RelativeTouchHandler.h" +#import "AbsoluteTouchHandler.h" +#import "KeyboardInputField.h" +#import "zlib.h" //to implement crc32 checksum + +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 interactionDelegate; + NSTimer* interactionTimer; + BOOL hasUserInteracted; + + NSDictionary *dictCodes; +} + +- (void) setupStreamView:(ControllerSupport*)controllerSupport + interactionDelegate:(id)interactionDelegate + config:(StreamConfiguration*)streamConfig { + 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]; + +#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)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); +} + +<<<<<<< HEAD +// convert 64bit pointer to CRC32 checksum +- (uint32_t)crc32OfUint64:(uint64_t)value { + // uint64_t可以被视为字符数组来计算CRC32 + char data[sizeof(value)]; + memcpy(data, &value, sizeof(value)); + uint32_t crc = crc32(0, Z_NULL, 0); + crc = crc32(crc, (const Bytef *)data, sizeof(value)); + return crc; +} + +- (void)trySendTouchEvent:(UITouch*)event index:(int)index{ + uint8_t type; + // NSLog(@"trySendTouchEvent %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; + break; + case UITouchPhaseMoved://移动 + case UITouchPhaseStationary: + type = LI_TOUCH_EVENT_MOVE; + break; + case UITouchPhaseEnded://触摸结束 + type = LI_TOUCH_EVENT_UP; + break; + case UITouchPhaseCancelled://触摸取消 + type = LI_TOUCH_EVENT_CANCEL; + break; + case UITouchPhaseRegionEntered://停留 + case UITouchPhaseRegionMoved://停留 + type = LI_TOUCH_EVENT_HOVER; + break; + default: + return; + } +// 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; +// } + uint32_t pointerId = [self crc32OfUint64:((uint64_t)event)]; // convert pointer event(64bit) to CRC32 checksum as touch pointer ID. + // There's no pointerId like Android in definition of iOS touch event "UITouch", we use pointer to UITouch to generate touch pointer ID. + // Since touch pointer ID is defined to be uint32_t, I implement CRC32 checksum to the 64bit address, which avoids touch pointer ID repetition. + CGPoint location = [self adjustCoordinatesForVideoArea:[event locationInView:self]]; + CGSize videoSize = [self getVideoAreaSize]; + LiSendTouchEvent(type,pointerId,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)sendStylusEvent:(UITouch*)event { +======= +- (BOOL)sendStylusEvent:(UITouch*)event { +>>>>>>> Moonlight-iOS-Official9.0.2 + 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 *arr1 = [[event allTouches] allObjects]; +//// NSLog(@"touchesBegan - %lu",[[event allTouches] count]); +//// NSLog(@"touchesBegan - %lu",arr1.count); +// +// for(int i=0;i *)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 *)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 trySendTouchEvent:touch index:0]; +// return; + } + } + } +#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 trySendTouchEvent:touch index:0]; +// return; + } + } + } +#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 *)keyCommands +{ + NSString *charset = @"qwertyuiopasdfghjklzxcvbnm1234567890\t§[]\\'\"/.,`<>-´ç+`¡'º;ñ= "; + + NSMutableArray * commands = [NSMutableArray 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 diff --git a/Limelight/Limelight-Info.plist b/Limelight/Limelight-Info.plist index 7d95d89b..3b378ef6 100644 --- a/Limelight/Limelight-Info.plist +++ b/Limelight/Limelight-Info.plist @@ -9,7 +9,7 @@ CFBundleDevelopmentRegion en CFBundleDisplayName - ${PRODUCT_NAME} + Moonlight-ZWM CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier @@ -69,7 +69,7 @@ armv7 UIRequiresFullScreen - + UIStatusBarHidden UISupportedInterfaceOrientations diff --git a/Limelight/Limelight.xcdatamodeld/Moonlight v1.10.xcdatamodel/contents b/Limelight/Limelight.xcdatamodeld/Moonlight v1.10.xcdatamodel/contents index a2cc61c3..b6fdc962 100644 --- a/Limelight/Limelight.xcdatamodeld/Moonlight v1.10.xcdatamodel/contents +++ b/Limelight/Limelight.xcdatamodeld/Moonlight v1.10.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -21,7 +21,7 @@ - + @@ -29,15 +29,22 @@ + + + + + + + - + \ No newline at end of file diff --git a/Limelight/ViewControllers/CustomEdgeSwipeGestureRecognizer.h b/Limelight/ViewControllers/CustomEdgeSwipeGestureRecognizer.h new file mode 100644 index 00000000..4e4ef2b0 --- /dev/null +++ b/Limelight/ViewControllers/CustomEdgeSwipeGestureRecognizer.h @@ -0,0 +1,20 @@ +// +// CustomEdgeSwipeGestureRecognizer.h +// Moonlight-ZWM +// +// Created by ZWM on 2024/4/30. +// Copyright © 2024 Moonlight Game Streaming Project. All rights reserved. +// + +#ifndef CustomEdgeSwipeGestureRecognizer_h +#define CustomEdgeSwipeGestureRecognizer_h +// CustomEdgeSwipeGestureRecognizer.h +#import + +@interface CustomEdgeSwipeGestureRecognizer : UIGestureRecognizer + +@property (nonatomic, assign) UIRectEdge edges; // Specify the edge(s) you want to recognize the swipe gesture on +@property (nonatomic, assign) CGFloat normalizedThresholdDistance; // Distance from the edge to start recognizing the gesture + +@end +#endif /* CustomEdgeSwipeGestureRecognizer_h */ diff --git a/Limelight/ViewControllers/CustomEdgeSwipeGestureRecognizer.m b/Limelight/ViewControllers/CustomEdgeSwipeGestureRecognizer.m new file mode 100644 index 00000000..874d2523 --- /dev/null +++ b/Limelight/ViewControllers/CustomEdgeSwipeGestureRecognizer.m @@ -0,0 +1,62 @@ +// +// CustomEdgeSwipeGestureRecognizer.m +// Moonlight-ZWM +// +// Created by ZWM on 2024/4/30. +// Copyright © 2024 Moonlight Game Streaming Project. All rights reserved. +// + +// #import + +#import "CustomEdgeSwipeGestureRecognizer.h" +#import + +@implementation CustomEdgeSwipeGestureRecognizer +UITouch* capturedUITouch; +CGFloat _startPointX; +static CGFloat screenWidthInPoints; +static CGFloat EDGE_TOLERANCE_POINTS = 50.0f; + +- (instancetype)initWithTarget:(nullable id)target action:(nullable SEL)action { + self = [super initWithTarget:target action:action]; + screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]); // Get the screen's bounds (in points) + return self; +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + // [super touchesBegan:touches withEvent:event]; + UITouch *touch = [touches anyObject]; + capturedUITouch = touch; + _startPointX = [capturedUITouch locationInView:self.view].x; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + // [super touchesEnded:touches withEvent:event]; + + if([touches containsObject:capturedUITouch]){ + CGFloat _endPointX = [capturedUITouch locationInView:self.view].x; + CGFloat normalizedGestureDistance = fabs(_endPointX - _startPointX)/screenWidthInPoints; + + if(self.edges & UIRectEdgeLeft){ + if(_startPointX < EDGE_TOLERANCE_POINTS && normalizedGestureDistance > _normalizedThresholdDistance){ + self.state = UIGestureRecognizerStateBegan; + self.state = UIGestureRecognizerStateEnded; + } + // NSLog(@"_startPointX %f , normalizedGestureDeltaX %f", _startPointX, normalizedGestureDistance); + } + if(self.edges & UIRectEdgeRight){ + if((_startPointX > (screenWidthInPoints - EDGE_TOLERANCE_POINTS)) && normalizedGestureDistance > _normalizedThresholdDistance){ + self.state = UIGestureRecognizerStateBegan; + self.state = UIGestureRecognizerStateEnded; + } + // NSLog(@"_startPointX %f , normalizedGestureDeltaX %f", _startPointX, normalizedGestureDistance); + } + } +} + +@end + + + + + diff --git a/Limelight/ViewControllers/SettingsViewController.h b/Limelight/ViewControllers/SettingsViewController.h index 1a79162d..52005628 100644 --- a/Limelight/ViewControllers/SettingsViewController.h +++ b/Limelight/ViewControllers/SettingsViewController.h @@ -27,6 +27,19 @@ @property (strong, nonatomic) IBOutlet UISegmentedControl *btMouseSelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *statsOverlaySelector; @property (strong, nonatomic) IBOutlet UIScrollView *scrollView; +@property (strong, nonatomic) IBOutlet UILabel *keyboardToggleFingerNumLabel; +@property (strong, nonatomic) IBOutlet UISlider *keyboardToggleFingerNumSlider; +@property (strong, nonatomic) IBOutlet UISegmentedControl *liftStreamViewForKeyboardSelector; +@property (strong, nonatomic) IBOutlet UISegmentedControl *showKeyboardToolbarSelector; +@property (strong, nonatomic) IBOutlet UISegmentedControl *swipeExitScreenEdgeSelector; +@property (strong, nonatomic) IBOutlet UILabel *swipeToExitDistanceUILabel; +@property (strong, nonatomic) IBOutlet UISlider *swipeToExitDistanceSlider; +@property (strong, nonatomic) IBOutlet UISlider *pointerVelocityModeDividerSlider; +@property (strong, nonatomic) IBOutlet UILabel *pointerVelocityModeDividerUILabel; +@property (strong, nonatomic) IBOutlet UISlider *touchPointerVelocityFactorSlider; +@property (strong, nonatomic) IBOutlet UILabel *touchPointerVelocityFactorUILabel; + + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability" diff --git a/Limelight/ViewControllers/SettingsViewController.m b/Limelight/ViewControllers/SettingsViewController.m index bbe8eb2d..97587f2e 100644 --- a/Limelight/ViewControllers/SettingsViewController.m +++ b/Limelight/ViewControllers/SettingsViewController.m @@ -10,6 +10,7 @@ #import "TemporarySettings.h" #import "DataManager.h" +#import #import #import @@ -263,11 +264,60 @@ BOOL isCustomResolution(CGSize res) { [self.bitrateSlider addTarget:self action:@selector(bitrateSliderMoved) forControlEvents:UIControlEventValueChanged]; [self updateBitrateText]; [self updateResolutionDisplayViewText]; + + [self.keyboardToggleFingerNumSlider setValue:(CGFloat)currentSettings.keyboardToggleFingers.intValue animated:YES]; // Load old setting. old setting was converted to uint16_t before saving. + if (self.keyboardToggleFingerNumSlider.value > 10.5f) [self.keyboardToggleFingerNumLabel setText:[NSString stringWithFormat:@"Tap-Toggle Local Keyboard Disabled"]]; // Initiate label display. Allow higher required finger number to completely disable keyboard toggle + else [self.keyboardToggleFingerNumLabel setText:[NSString stringWithFormat:@"Fingers to Tap-Toggle Local Keyboard: %d", (uint16_t)self.keyboardToggleFingerNumSlider.value]]; // Initiate label display + [self.keyboardToggleFingerNumSlider addTarget:self action:@selector(keyboardToggleFingerNumSliderMoved) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved. + [self.liftStreamViewForKeyboardSelector setSelectedSegmentIndex:currentSettings.liftStreamViewForKeyboard ? 1 : 0];// Load old setting + [self.showKeyboardToolbarSelector setSelectedSegmentIndex:currentSettings.showKeyboardToolbar ? 1 : 0];// Load old setting + [self.swipeExitScreenEdgeSelector setSelectedSegmentIndex:[self getSelectorIndexFromScreenEdge:(uint32_t)currentSettings.swipeExitScreenEdge.integerValue]]; // Load old setting + [self.swipeToExitDistanceSlider setValue:(CGFloat)currentSettings.swipeToExitDistance.floatValue animated:YES]; // Load old setting. + [self.swipeToExitDistanceUILabel setText:[NSString stringWithFormat:@"Swipe & Exit Distance: %.2f * screen-width", self.swipeToExitDistanceSlider.value]]; // Initiate label display + [self.swipeToExitDistanceSlider addTarget:self action:@selector(swipeToExitDistanceSliderMoved) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved. + + [self.pointerVelocityModeDividerSlider setValue:currentSettings.pointerVelocityModeDivider.floatValue * 100 animated:YES]; // Load old setting. + [self.pointerVelocityModeDividerUILabel setText:[NSString stringWithFormat:@"Touch Pointer Velocity: Scaled on %d%% of Right Screen", 100 - (uint8_t)self.pointerVelocityModeDividerSlider.value]]; // Initiate label display + [self.pointerVelocityModeDividerSlider addTarget:self action:@selector(pointerVelocityModeDividerSliderMoved) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved. + + [self.touchPointerVelocityFactorSlider setValue:currentSettings.touchPointerVelocityFactor.floatValue * 100 animated:YES]; // Load old setting. + [self.touchPointerVelocityFactorUILabel setText:[NSString stringWithFormat:@"Touch Pointer Velocity Factor: %d%%", (uint16_t)self.touchPointerVelocityFactorSlider.value]]; // Initiate label display + [self.touchPointerVelocityFactorSlider addTarget:self action:@selector(touchPointerVelocityFactorSliderMoved) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved. } +- (void) pointerVelocityModeDividerSliderMoved { + [self.pointerVelocityModeDividerUILabel setText:[NSString stringWithFormat:@"Touch Pointer Velocity: Scaled on %d%% of Right Screen", 100 - (uint8_t)self.pointerVelocityModeDividerSlider.value]]; +} + +- (void) touchPointerVelocityFactorSliderMoved { + [self.touchPointerVelocityFactorUILabel setText:[NSString stringWithFormat:@"Touch Pointer Velocity Factor: %d%%", (uint16_t)self.touchPointerVelocityFactorSlider.value]]; // Initiate label display +} + +- (uint32_t) getScreenEdgeFromSelector { + switch (self.swipeExitScreenEdgeSelector.selectedSegmentIndex) { + case 0: return UIRectEdgeLeft; + case 1: return UIRectEdgeRight; + case 2: return UIRectEdgeLeft|UIRectEdgeRight; + default: return UIRectEdgeLeft; + } +} + +- (uint32_t) getSelectorIndexFromScreenEdge: (uint32_t)edge { + switch (edge) { + case UIRectEdgeLeft: return 0; + case UIRectEdgeRight: return 1; + case UIRectEdgeLeft|UIRectEdgeRight: return 2; + default: return 0; + } + return 0; +} + + - (void) touchModeChanged { // Disable on-screen controls in absolute touch mode [self.onscreenControlSelector setEnabled:[self.touchModeSelector selectedSegmentIndex] == 0]; + [self.pointerVelocityModeDividerSlider setEnabled:[self.touchModeSelector selectedSegmentIndex] == 1]; // pointer velocity scaling works only in native touch mode. + [self.touchPointerVelocityFactorSlider setEnabled:[self.touchModeSelector selectedSegmentIndex] == 1]; // pointer velocity scaling works only in native touch mode. } - (void) updateBitrate { @@ -456,6 +506,15 @@ BOOL isCustomResolution(CGSize res) { [self.resolutionDisplayView addSubview:label2]; } +- (void) keyboardToggleFingerNumSliderMoved{ + if (self.keyboardToggleFingerNumSlider.value > 10.5f) [self.keyboardToggleFingerNumLabel setText:[NSString stringWithFormat:@"Tap-Toggle Local Keyboard Disabled"]]; + else [self.keyboardToggleFingerNumLabel setText:[NSString stringWithFormat:@"Fingers to Tap-Toggle Local Keyboard: %d", (uint16_t)self.keyboardToggleFingerNumSlider.value]]; // Initiate label display +} + +- (void) swipeToExitDistanceSliderMoved{ + [self.swipeToExitDistanceUILabel setText:[NSString stringWithFormat:@"Swipe & Exit Distance: %.2f * screen-width", self.swipeToExitDistanceSlider.value]]; +} + - (void) bitrateSliderMoved { assert(self.bitrateSlider.value < (sizeof(bitrateTable) / sizeof(*bitrateTable))); _bitrate = bitrateTable[(int)self.bitrateSlider.value]; @@ -528,6 +587,14 @@ BOOL isCustomResolution(CGSize res) { NSInteger height = [self getChosenStreamHeight]; NSInteger width = [self getChosenStreamWidth]; NSInteger onscreenControls = [self.onscreenControlSelector selectedSegmentIndex]; + NSInteger keyboardToggleFingers = (uint16_t)self.keyboardToggleFingerNumSlider.value; + // NSLog(@"saveSettings keyboardToggleFingers %d", (uint16_t)keyboardToggleFingers); + CGFloat swipeToExitDistance = self.swipeToExitDistanceSlider.value; + uint32_t swipeExitScreenEdge = [self getScreenEdgeFromSelector]; + CGFloat pointerVelocityModeDivider = (CGFloat)(uint8_t)self.pointerVelocityModeDividerSlider.value/100; + CGFloat touchPointerVelocityFactor =(CGFloat)(uint16_t)self.touchPointerVelocityFactorSlider.value/100; + BOOL liftStreamViewForKeyboard = [self.liftStreamViewForKeyboardSelector selectedSegmentIndex] == 1; + BOOL showKeyboardToolbar = [self.showKeyboardToolbarSelector selectedSegmentIndex] == 1; BOOL optimizeGames = [self.optimizeSettingsSelector selectedSegmentIndex] == 1; BOOL multiController = [self.multiControllerSelector selectedSegmentIndex] == 1; BOOL swapABXYButtons = [self.swapABXYButtonsSelector selectedSegmentIndex] == 1; @@ -544,6 +611,13 @@ BOOL isCustomResolution(CGSize res) { width:width audioConfig:2 // Stereo onscreenControls:onscreenControls + keyboardToggleFingers:keyboardToggleFingers + swipeExitScreenEdge:swipeExitScreenEdge + swipeToExitDistance:swipeToExitDistance + pointerVelocityModeDivider:pointerVelocityModeDivider + touchPointerVelocityFactor:touchPointerVelocityFactor + liftStreamViewForKeyboard:liftStreamViewForKeyboard + showKeyboardToolbar:showKeyboardToolbar optimizeGames:optimizeGames multiController:multiController swapABXYButtons:swapABXYButtons diff --git a/Limelight/ViewControllers/StreamFrameViewController.m b/Limelight/ViewControllers/StreamFrameViewController.m index a4c8f344..f558764b 100644 --- a/Limelight/ViewControllers/StreamFrameViewController.m +++ b/Limelight/ViewControllers/StreamFrameViewController.m @@ -17,6 +17,7 @@ #include #include #include +#import "CustomEdgeSwipeGestureRecognizer.h" #if TARGET_OS_TV #import @@ -30,6 +31,8 @@ - (id)initWithRefreshRate:(float)arg1 videoDynamicRange:(int)arg2; @end + + @implementation StreamFrameViewController { ControllerSupport *_controllerSupport; StreamManager *_streamMan; @@ -49,7 +52,7 @@ CGSize _keyboardSize; #if !TARGET_OS_TV - UIScreenEdgePanGestureRecognizer *_exitSwipeRecognizer; + CustomEdgeSwipeGestureRecognizer *_exitSwipeRecognizer; #endif } @@ -83,7 +86,7 @@ [UIApplication sharedApplication].idleTimerDisabled = YES; - _settings = [[[DataManager alloc] init] getSettings]; + _settings = [[[DataManager alloc] init] getSettings]; //StreamFrameViewController retrieve the settings here. _stageLabel = [[UILabel alloc] init]; [_stageLabel setUserInteractionEnabled:NO]; @@ -129,12 +132,13 @@ [self.view addGestureRecognizer:_playPauseTapGestureRecognizer]; #else - _exitSwipeRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(edgeSwiped)]; - _exitSwipeRecognizer.edges = UIRectEdgeLeft; + _exitSwipeRecognizer = [[CustomEdgeSwipeGestureRecognizer alloc] initWithTarget:self action:@selector(edgeSwiped)]; + _exitSwipeRecognizer.edges = _settings.swipeExitScreenEdge.intValue; + _exitSwipeRecognizer.normalizedThresholdDistance = _settings.swipeToExitDistance.floatValue; _exitSwipeRecognizer.delaysTouchesBegan = NO; _exitSwipeRecognizer.delaysTouchesEnded = NO; - [self.view addGestureRecognizer:_exitSwipeRecognizer]; + #endif _tipLabel = [[UILabel alloc] init]; @@ -143,7 +147,7 @@ #if TARGET_OS_TV [_tipLabel setText:@"Tip: Tap the Play/Pause button on the Apple TV Remote to disconnect from your PC"]; #else - [_tipLabel setText:@"Tip: Swipe from the left edge to disconnect from your PC"]; + [_tipLabel setText:@"Tip: Swipe from the left edge to the middle of your screen to disconnect from your PC"]; #endif [_tipLabel sizeToFit]; @@ -175,6 +179,7 @@ #if 0 // FIXME: This doesn't work reliably on iPad for some reason. Showing and hiding the keyboard // several times in a row will not correctly restore the state of the UIScrollView. + // I add this notification to the StreamView, works well on ipad mini6. [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(keyboardWillShow:) name: UIKeyboardWillShowNotification @@ -187,25 +192,25 @@ #endif // Only enable scroll and zoom in absolute touch mode - if (_settings.absoluteTouchMode) { - _scrollView = [[UIScrollView alloc] initWithFrame:self.view.frame]; -#if !TARGET_OS_TV - [_scrollView.panGestureRecognizer setMinimumNumberOfTouches:2]; -#endif - [_scrollView setShowsHorizontalScrollIndicator:NO]; - [_scrollView setShowsVerticalScrollIndicator:NO]; - [_scrollView setDelegate:self]; - [_scrollView setMaximumZoomScale:10.0f]; - - // Add StreamView inside a UIScrollView for absolute mode - [_scrollView addSubview:_streamView]; - [self.view addSubview:_scrollView]; - } - else { - // Add StreamView directly in relative mode - [self.view addSubview:_streamView]; - } - +// if (_settings.absoluteTouchMode) { +// _scrollView = [[UIScrollView alloc] initWithFrame:self.view.frame]; +//#if !TARGET_OS_TV +// [_scrollView.panGestureRecognizer setMinimumNumberOfTouches:2]; +//#endif +// [_scrollView setShowsHorizontalScrollIndicator:NO]; +// [_scrollView setShowsVerticalScrollIndicator:NO]; +// [_scrollView setDelegate:self]; +// [_scrollView setMaximumZoomScale:10.0f]; +// +// // Add StreamView inside a UIScrollView for absolute mode +// [_scrollView addSubview:_streamView]; +// [self.view addSubview:_scrollView]; +// } +// else { +// // Add StreamView directly in relative mode +// [self.view addSubview:_streamView]; +// } + [self.view addSubview:_streamView]; [self.view addSubview:_stageLabel]; [self.view addSubview:_spinner]; [self.view addSubview:_tipLabel]; diff --git a/Moonlight TV/Info.plist b/Moonlight TV/Info.plist index b91ee818..c4d8436e 100644 --- a/Moonlight TV/Info.plist +++ b/Moonlight TV/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Moonlight + Moonlight-ZWM CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/Moonlight.xcodeproj/project.pbxproj b/Moonlight.xcodeproj/project.pbxproj index 517055a1..ca4ebc5b 100644 --- a/Moonlight.xcodeproj/project.pbxproj +++ b/Moonlight.xcodeproj/project.pbxproj @@ -8,6 +8,11 @@ /* Begin PBXBuildFile section */ 693B3A9B218638CD00982F7B /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 693B3A9A218638CD00982F7B /* Settings.bundle */; }; + 840751AF2BF472A30096B657 /* NativeTouchHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 840751AE2BF472A30096B657 /* NativeTouchHandler.m */; }; + 840751B02BF472A30096B657 /* NativeTouchHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 840751AE2BF472A30096B657 /* NativeTouchHandler.m */; }; + 840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */; }; + 840751B42BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */; }; + 84591A512BE1625E006AAB10 /* CustomEdgeSwipeGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 84591A502BE1625E006AAB10 /* CustomEdgeSwipeGestureRecognizer.m */; }; 98181BE92791277400E43572 /* libSDL2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98181BE82791275D00E43572 /* libSDL2.a */; }; 98181BEB2791278F00E43572 /* libSDL2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98181BEA2791278300E43572 /* libSDL2.a */; }; 98181BED2791281100E43572 /* CoreMotion.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98181BEC2791281100E43572 /* CoreMotion.framework */; }; @@ -172,6 +177,12 @@ /* Begin PBXFileReference section */ 566E9D2B2770B23A00EF7BFE /* Moonlight v1.7.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.7.xcdatamodel"; sourceTree = ""; }; 693B3A9A218638CD00982F7B /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; + 840751AD2BF472810096B657 /* NativeTouchHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NativeTouchHandler.h; sourceTree = ""; }; + 840751AE2BF472A30096B657 /* NativeTouchHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NativeTouchHandler.m; sourceTree = ""; }; + 840751B12BF49B5A0096B657 /* CustomTapGestureRecognizer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CustomTapGestureRecognizer.h; sourceTree = ""; }; + 840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomTapGestureRecognizer.m; sourceTree = ""; }; + 84591A4F2BE16240006AAB10 /* CustomEdgeSwipeGestureRecognizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CustomEdgeSwipeGestureRecognizer.h; sourceTree = ""; }; + 84591A502BE1625E006AAB10 /* CustomEdgeSwipeGestureRecognizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CustomEdgeSwipeGestureRecognizer.m; sourceTree = ""; }; 9803CCAB254F9EAF00EE185E /* ConnectionCallbacks.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ConnectionCallbacks.h; sourceTree = ""; }; 98132E8C20BC9A62007A053F /* Moonlight v1.1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.1.xcdatamodel"; sourceTree = ""; }; 98181BE82791275D00E43572 /* libSDL2.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libSDL2.a; path = libs/SDL2/lib/iOS/libSDL2.a; sourceTree = ""; }; @@ -218,7 +229,7 @@ DC1F5A06206436B20037755F /* ConnectionHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConnectionHelper.m; sourceTree = ""; }; FB1A674B2131E65900507771 /* KeyboardSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyboardSupport.h; sourceTree = ""; }; FB1A674C2131E65900507771 /* KeyboardSupport.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KeyboardSupport.m; sourceTree = ""; }; - FB1A67532132419700507771 /* Moonlight TV.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Moonlight TV.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + FB1A67532132419700507771 /* Moonlight-ZWM TV.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Moonlight-ZWM TV.app"; sourceTree = BUILT_PRODUCTS_DIR; }; FB1A675F2132419700507771 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; FB1A67612132419A00507771 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; FB1A67632132419A00507771 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -229,7 +240,7 @@ FB1D59961BBCCB6400F482CA /* ComputerScrollView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ComputerScrollView.m; sourceTree = ""; }; FB1D59981BBCCD7E00F482CA /* AppCollectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppCollectionView.h; sourceTree = ""; }; FB1D59991BBCCD7E00F482CA /* AppCollectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppCollectionView.m; sourceTree = ""; }; - FB290CEE19B2C406004C83CF /* Moonlight.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Moonlight.app; sourceTree = BUILT_PRODUCTS_DIR; }; + FB290CEE19B2C406004C83CF /* Moonlight-ZWM.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Moonlight-ZWM.app"; sourceTree = BUILT_PRODUCTS_DIR; }; FB290CF119B2C406004C83CF /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; FB290CF319B2C406004C83CF /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; FB290CF519B2C406004C83CF /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; @@ -487,8 +498,8 @@ FB290CEF19B2C406004C83CF /* Products */ = { isa = PBXGroup; children = ( - FB290CEE19B2C406004C83CF /* Moonlight.app */, - FB1A67532132419700507771 /* Moonlight TV.app */, + FB290CEE19B2C406004C83CF /* Moonlight-ZWM.app */, + FB1A67532132419700507771 /* Moonlight-ZWM TV.app */, ); name = Products; sourceTree = ""; @@ -580,6 +591,8 @@ FB89460919F646E200339C8A /* Input */ = { isa = PBXGroup; children = ( + 840751AD2BF472810096B657 /* NativeTouchHandler.h */, + 840751AE2BF472A30096B657 /* NativeTouchHandler.m */, FB89460A19F646E200339C8A /* ControllerSupport.h */, FB89460B19F646E200339C8A /* ControllerSupport.m */, FB89460C19F646E200339C8A /* StreamView.h */, @@ -592,6 +605,8 @@ 9897B6A32212610800966419 /* Controller.h */, 9827E7A22514366900F25707 /* HapticContext.m */, 9827E7A7251436EA00F25707 /* HapticContext.h */, + 840751B12BF49B5A0096B657 /* CustomTapGestureRecognizer.h */, + 840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */, 9819CC12254F107A008A7C8E /* RelativeTouchHandler.h */, 9819CC13254F107A008A7C8E /* RelativeTouchHandler.m */, 9819CC20254F180F008A7C8E /* AbsoluteTouchHandler.h */, @@ -648,6 +663,8 @@ FB89462319F646E200339C8A /* ViewControllers */ = { isa = PBXGroup; children = ( + 84591A502BE1625E006AAB10 /* CustomEdgeSwipeGestureRecognizer.m */, + 84591A4F2BE16240006AAB10 /* CustomEdgeSwipeGestureRecognizer.h */, FBD3495119FF36FB002D2A60 /* SWRevealViewController.h */, FBD3495219FF36FB002D2A60 /* SWRevealViewController.m */, FB89462419F646E200339C8A /* MainFrameViewController.h */, @@ -876,9 +893,9 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - FB1A67522132419700507771 /* Moonlight TV */ = { + FB1A67522132419700507771 /* Moonlight-ZWM TV */ = { isa = PBXNativeTarget; - buildConfigurationList = FB1A67682132419A00507771 /* Build configuration list for PBXNativeTarget "Moonlight TV" */; + buildConfigurationList = FB1A67682132419A00507771 /* Build configuration list for PBXNativeTarget "Moonlight-ZWM TV" */; buildPhases = ( FB1A674F2132419700507771 /* Sources */, FB1A67502132419700507771 /* Frameworks */, @@ -889,14 +906,14 @@ dependencies = ( FB1A68172132509800507771 /* PBXTargetDependency */, ); - name = "Moonlight TV"; + name = "Moonlight-ZWM TV"; productName = "Moonlight TV"; - productReference = FB1A67532132419700507771 /* Moonlight TV.app */; + productReference = FB1A67532132419700507771 /* Moonlight-ZWM TV.app */; productType = "com.apple.product-type.application"; }; - FB290CED19B2C406004C83CF /* Moonlight */ = { + FB290CED19B2C406004C83CF /* Moonlight-ZWM */ = { isa = PBXNativeTarget; - buildConfigurationList = FB290D2019B2C406004C83CF /* Build configuration list for PBXNativeTarget "Moonlight" */; + buildConfigurationList = FB290D2019B2C406004C83CF /* Build configuration list for PBXNativeTarget "Moonlight-ZWM" */; buildPhases = ( FB290CEA19B2C406004C83CF /* Sources */, FB290CEB19B2C406004C83CF /* Frameworks */, @@ -907,9 +924,9 @@ dependencies = ( 98AB2E861CAD468B0089BB98 /* PBXTargetDependency */, ); - name = Moonlight; + name = "Moonlight-ZWM"; productName = Limelight; - productReference = FB290CEE19B2C406004C83CF /* Moonlight.app */; + productReference = FB290CEE19B2C406004C83CF /* Moonlight-ZWM.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -956,8 +973,8 @@ ); projectRoot = ""; targets = ( - FB290CED19B2C406004C83CF /* Moonlight */, - FB1A67522132419700507771 /* Moonlight TV */, + FB290CED19B2C406004C83CF /* Moonlight-ZWM */, + FB1A67522132419700507771 /* Moonlight-ZWM TV */, ); }; /* End PBXProject section */ @@ -1040,6 +1057,7 @@ FB1A67CB213245EA00507771 /* ConnectionHelper.m in Sources */, FB1A67BD213245E000507771 /* DiscoveryManager.m in Sources */, FB1A67BF213245E000507771 /* DiscoveryWorker.m in Sources */, + 840751B42BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */, FB1A67C1213245E000507771 /* MDNSManager.m in Sources */, FB1A67B1213245D500507771 /* HttpManager.m in Sources */, FB1A67B3213245D500507771 /* HttpRequest.m in Sources */, @@ -1053,6 +1071,7 @@ 988FCD42293B091B003050E2 /* KeyboardInputField.m in Sources */, FB1A67A3213245BD00507771 /* Connection.m in Sources */, FB1A67A5213245BD00507771 /* StreamConfiguration.m in Sources */, + 840751B02BF472A30096B657 /* NativeTouchHandler.m in Sources */, FB1A67A7213245BD00507771 /* StreamManager.m in Sources */, FB1A67A9213245BD00507771 /* VideoDecoderRenderer.m in Sources */, FB1A67A12132458C00507771 /* main.m in Sources */, @@ -1067,6 +1086,8 @@ 9819CC22254F180F008A7C8E /* AbsoluteTouchHandler.m in Sources */, 9819CC14254F107A008A7C8E /* RelativeTouchHandler.m in Sources */, FB1A674D2131E65900507771 /* KeyboardSupport.m in Sources */, + 84591A512BE1625E006AAB10 /* CustomEdgeSwipeGestureRecognizer.m in Sources */, + 840751AF2BF472A30096B657 /* NativeTouchHandler.m in Sources */, FB89463219F646E200339C8A /* VideoDecoderRenderer.m in Sources */, FB290D0419B2C406004C83CF /* AppDelegate.m in Sources */, FB9AFD401A7E127D00872C98 /* AppListResponse.m in Sources */, @@ -1087,6 +1108,7 @@ FB89462C19F646E200339C8A /* HttpManager.m in Sources */, FB89462D19F646E200339C8A /* MDNSManager.m in Sources */, FB89462B19F646E200339C8A /* StreamView.m in Sources */, + 840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */, FB4678FA1A55FFAD00377732 /* DiscoveryManager.m in Sources */, 98D5856D1C0EA79600F6CC00 /* TemporaryHost.m in Sources */, FB89463519F646E200339C8A /* MainFrameViewController.m in Sources */, @@ -1163,7 +1185,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = DM46QST4M7; + DEVELOPMENT_TEAM = 8S84A8A3QF; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_INCREASE_PRECOMPILED_HEADER_SHARING = NO; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -1191,7 +1213,7 @@ "$(PROJECT_DIR)/libs/SDL2/lib/tvOS-Sim", "$(PROJECT_DIR)/libs/FFmpeg/lib/tvOS-Sim", ); - MARKETING_VERSION = 9.0.2; + MARKETING_VERSION = 9.0.0; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-stream.Moonlight"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1247,7 +1269,7 @@ "$(PROJECT_DIR)/libs/SDL2/lib/tvOS-Sim", "$(PROJECT_DIR)/libs/FFmpeg/lib/tvOS-Sim", ); - MARKETING_VERSION = 9.0.2; + MARKETING_VERSION = 9.0.0; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-stream.Moonlight"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1376,8 +1398,8 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = DM46QST4M7; + CURRENT_PROJECT_VERSION = 2; + DEVELOPMENT_TEAM = 8S84A8A3QF; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Limelight/Limelight-Prefix.pch"; HEADER_SEARCH_PATHS = ( @@ -1388,6 +1410,7 @@ "$(PROJECT_DIR)/libs/**", ); INFOPLIST_FILE = "Limelight/Limelight-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = MoonlightAXI; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1405,8 +1428,8 @@ "$(PROJECT_DIR)/libs/SDL2/lib/iOS-Sim", "$(PROJECT_DIR)/libs/FFmpeg/lib/iOS-Sim", ); - MARKETING_VERSION = 9.0.2; - PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-stream.Moonlight"; + MARKETING_VERSION = 9.0.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-zwm-updated.iPadOS"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; SKIP_INSTALL = NO; @@ -1426,8 +1449,8 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = DM46QST4M7; + CURRENT_PROJECT_VERSION = 2; + DEVELOPMENT_TEAM = 8S84A8A3QF; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Limelight/Limelight-Prefix.pch"; HEADER_SEARCH_PATHS = ( @@ -1438,6 +1461,7 @@ "$(PROJECT_DIR)/libs/**", ); INFOPLIST_FILE = "Limelight/Limelight-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = MoonlightAXI; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1455,8 +1479,8 @@ "$(PROJECT_DIR)/libs/SDL2/lib/iOS-Sim", "$(PROJECT_DIR)/libs/FFmpeg/lib/iOS-Sim", ); - MARKETING_VERSION = 9.0.2; - PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-stream.Moonlight"; + MARKETING_VERSION = 9.0.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-zwm-updated.iPadOS"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; SKIP_INSTALL = NO; @@ -1473,7 +1497,7 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - FB1A67682132419A00507771 /* Build configuration list for PBXNativeTarget "Moonlight TV" */ = { + FB1A67682132419A00507771 /* Build configuration list for PBXNativeTarget "Moonlight-ZWM TV" */ = { isa = XCConfigurationList; buildConfigurations = ( FB1A67662132419A00507771 /* Debug */, @@ -1491,7 +1515,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - FB290D2019B2C406004C83CF /* Build configuration list for PBXNativeTarget "Moonlight" */ = { + FB290D2019B2C406004C83CF /* Build configuration list for PBXNativeTarget "Moonlight-ZWM" */ = { isa = XCConfigurationList; buildConfigurations = ( FB290D2119B2C406004C83CF /* Debug */, diff --git a/Moonlight.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Moonlight.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1aaa8236..cf372ddd 100644 --- a/Moonlight.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/Moonlight.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:/Users/admin/moonlight-ios/Moonlight-ZWM.xcodeproj"> diff --git a/iPad.storyboard b/iPad.storyboard index 9017b15b..0914f23c 100644 --- a/iPad.storyboard +++ b/iPad.storyboard @@ -1,9 +1,9 @@ - - + + - + @@ -59,7 +59,7 @@ - + - + @@ -109,6 +109,25 @@ + + + + + + + + + + + + + @@ -326,15 +432,26 @@ + + + + + + + + + + + diff --git a/iPhone.storyboard b/iPhone.storyboard index 03aa0075..b8b3f7b6 100644 --- a/iPhone.storyboard +++ b/iPhone.storyboard @@ -1,9 +1,9 @@ - + - + @@ -83,7 +83,7 @@ - + - + @@ -150,25 +150,51 @@ - - + + - + + + + + + + + + + + + + - + @@ -180,14 +206,14 @@ - + @@ -196,15 +222,39 @@ + + + + + + + + + + + + - + @@ -214,14 +264,14 @@ - + @@ -231,14 +281,14 @@ - + @@ -248,14 +298,14 @@ - + @@ -267,14 +317,14 @@ - + @@ -284,14 +334,14 @@ - + @@ -301,14 +351,14 @@ - + @@ -318,14 +368,14 @@ - + @@ -334,9 +384,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -346,15 +451,26 @@ + + + + + + + + + + + diff --git a/moonlight-common/moonlight-common-c b/moonlight-common/moonlight-common-c index 48d7f1ac..eb215615 160000 --- a/moonlight-common/moonlight-common-c +++ b/moonlight-common/moonlight-common-c @@ -1 +1 @@ -Subproject commit 48d7f1ace168d07dad339718f1350979545f15d2 +Subproject commit eb21561541874d9e9dbd7da26000c6dd55050748