Refactor NativeTouchHandler, restore absolute touch, Bug fixes

This commit is contained in:
True Zhuanjia committed 2024-06-16 15:48:55 +08:00
1 parent d673c4900c
commit e7fd911fb8
17 files changed
+463 -282

No files matched your search

+2 -1
View File
@@ -34,7 +34,8 @@
useFramePacing:(BOOL)useFramePacing
enableHdr:(BOOL)enableHdr
btMouseSupport:(BOOL)btMouseSupport
absoluteTouchMode:(BOOL)absoluteTouchMode
// absoluteTouchMode:(BOOL)absoluteTouchMode
touchMode:(NSInteger)touchMode
statsOverlay:(BOOL)statsOverlay;
- (NSArray*) getHosts;
+4 -2
View File
@@ -73,7 +73,8 @@
useFramePacing:(BOOL)useFramePacing
enableHdr:(BOOL)enableHdr
btMouseSupport:(BOOL)btMouseSupport
absoluteTouchMode:(BOOL)absoluteTouchMode
// absoluteTouchMode:(BOOL)absoluteTouchMode
touchMode:(NSInteger)touchMode
statsOverlay:(BOOL)statsOverlay {
[_managedObjectContext performBlockAndWait:^{
@@ -99,7 +100,8 @@
settingsToSave.useFramePacing = useFramePacing;
settingsToSave.enableHdr = enableHdr;
settingsToSave.btMouseSupport = btMouseSupport;
settingsToSave.absoluteTouchMode = absoluteTouchMode;
// settingsToSave.absoluteTouchMode = absoluteTouchMode;
settingsToSave.touchMode = [NSNumber numberWithInteger:(uint16_t)touchMode];
settingsToSave.statsOverlay = statsOverlay;
[self saveData];
+6 -1
View File
@@ -7,6 +7,10 @@
//
#import "Settings+CoreDataClass.h"
#define RELATIVE_TOUCH_MODE 0
#define NATIVE_TOUCH_MODE 1
#define ABSOLUTE_TOUCH_MODE 2
@interface TemporarySettings : NSObject
@@ -37,7 +41,8 @@
@property (nonatomic) BOOL optimizeGames;
@property (nonatomic) BOOL enableHdr;
@property (nonatomic) BOOL btMouseSupport;
@property (nonatomic) BOOL absoluteTouchMode;
// @property (nonatomic) BOOL absoluteTouchMode;
@property (nonatomic, retain) NSNumber * touchMode;
@property (nonatomic) BOOL statsOverlay;
@property (nonatomic) BOOL liftStreamViewForKeyboard;
@property (nonatomic) BOOL showKeyboardToolbar;
+2 -1
View File
@@ -83,7 +83,8 @@
self.swapABXYButtons = settings.swapABXYButtons;
self.onscreenControls = settings.onscreenControls;
self.btMouseSupport = settings.btMouseSupport;
self.absoluteTouchMode = settings.absoluteTouchMode;
// self.absoluteTouchMode = settings.absoluteTouchMode;
self.touchMode = settings.touchMode;
self.statsOverlay = settings.statsOverlay;
self.keyboardToggleFingers = settings.keyboardToggleFingers;
self.swipeExitScreenEdge = settings.swipeExitScreenEdge;
+78 -80
View File
@@ -38,85 +38,83 @@
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
// 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];
//}
- (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];
}
@end
+1 -1
View File
@@ -1061,7 +1061,7 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
// Even if no gamepads are present, we will always count one if OSC is enabled,
// or it's set to auto and no keyboard or mouse is present. Absolute touch mode
// disables the OSC.
if (level != OnScreenControlsLevelOff && (![ControllerSupport hasKeyboardOrMouse] || level != OnScreenControlsLevelAuto) && !settings.absoluteTouchMode) {
if (level != OnScreenControlsLevelOff && (![ControllerSupport hasKeyboardOrMouse] || level != OnScreenControlsLevelAuto) && (settings.touchMode.intValue == RELATIVE_TOUCH_MODE)) {
mask |= 0x1;
}
+6 -17
View File
@@ -2,33 +2,22 @@
// NativeTouchHandler.h
// Moonlight
//
// Created by ZWM on 2024/5/14.
// Created by ZWM on 2024/6/16.
// Copyright © 2024 Moonlight Game Streaming Project. All rights reserved.
//
#import "StreamView.h"
#import "DataManager.h"
#include <Limelight.h>
NS_ASSUME_NONNULL_BEGIN
@interface TouchPointer : NSObject
@interface NativeTouchHandler : UIResponder
+ (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;
- (id)initWith:(StreamView*)view and:(TemporarySettings*)settings;
- (instancetype)initWith:(UITouch *)touch;
@end
NS_ASSUME_NONNULL_END
+116 -104
View File
@@ -2,134 +2,146 @@
// NativeTouchHandler.m
// Moonlight
//
// Created by ZWM on 2024/5/14.
// Created by Admin on 2024/6/16.
// Copyright © 2024 Moonlight Game Streaming Project. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "NativeTouchHandler.h"
#import "NativeTouchPointer.h"
#import "StreamView.h"
#import "DataManager.h"
#include <Limelight.h>
// 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<NSNumber *> *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;
@implementation NativeTouchHandler {
StreamView* streamView;
bool activateCoordSelector;
}
- (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];
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];
- (id)initWith:(StreamView*)view and:(TemporarySettings*)settings{
self = [self init];
self->streamView = view;
activateCoordSelector = (settings.pointerVelocityModeDivider.floatValue != 1.0);
[NativeTouchPointer setPointerVelocityDivider:settings.pointerVelocityModeDivider.floatValue];
[NativeTouchPointer setPointerVelocityFactor:settings.touchPointerVelocityFactor.floatValue];
[NativeTouchPointer initContextWith:streamView];
return self;
}
+ (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)];
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
#if !TARGET_OS_TV
if (@available(iOS 13.4, *)) {
for (UITouch* touch in touches) [self handleUITouch:touch index:0];// Native touch (absoluteTouch) first!
return;
}
#endif
}
- (void)sendTouchEvent:(UITouch*)event touchType:(uint8_t)touchType{
CGPoint targetCoords;
if(activateCoordSelector && event.phase == UITouchPhaseMoved) targetCoords = [NativeTouchPointer selectCoordsFor:event]; // coordinates of touch pointer replaced to relative ones here.
else targetCoords = [event locationInView:streamView];
CGPoint location = [streamView adjustCoordinatesForVideoArea:targetCoords];
CGSize videoSize = [streamView getVideoAreaSize];
LiSendTouchEvent(touchType,[NativeTouchPointer retrievePointerIdFromDict:event],location.x / videoSize.width, location.y / videoSize.height,(event.force / event.maximumPossibleForce) / sin(event.altitudeAngle),0.0f, 0.0f,[streamView getRotationFromAzimuthAngle:[event azimuthAngleInView:streamView]]);
}
// 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)];
- (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;
[NativeTouchPointer populatePointerId:event]; //获取并记录pointerId
if(activateCoordSelector) [NativeTouchPointer populatePointerObjIntoDict:event];
break;
case UITouchPhaseMoved://移动
case UITouchPhaseStationary:
type = LI_TOUCH_EVENT_MOVE;
if(activateCoordSelector) [NativeTouchPointer updatePointerObjInDict:event];
break;
case UITouchPhaseEnded://触摸结束
type = LI_TOUCH_EVENT_UP;
[self sendTouchEvent:event touchType:type]; //先发送,再删除
[NativeTouchPointer removePointerId:event]; //删除pointerId
if(activateCoordSelector) [NativeTouchPointer removePointerObjFromDict:event];
return;
case UITouchPhaseCancelled://触摸取消
type = LI_TOUCH_EVENT_CANCEL;
[self sendTouchEvent:event touchType:type]; //先发送,再删除
[NativeTouchPointer removePointerId:event]; //删除pointerId
if(activateCoordSelector) [NativeTouchPointer removePointerObjFromDict:event];
return;
case UITouchPhaseRegionEntered://停留
case UITouchPhaseRegionMoved://停留
type = LI_TOUCH_EVENT_HOVER;
break;
default:
return;
}
[self sendTouchEvent:event touchType:type];
}
// 从字典中获取UITouch事件对应的pointerId
// call this only when NSDcit & NSSet is up-to-date.
+ (uint32_t) retrievePointerIdFromDict:(UITouch*)touch{
return [[pointerIdDict objectForKey:@((uint64_t)touch)] unsignedIntValue];
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
#if !TARGET_OS_TV
if (@available(iOS 13.4, *)) {
for (UITouch* touch in touches) [self handleUITouch:touch index:0];// Native touch (absoluteTouch) first!
return;
// NSLog(@"touchesMoved - allTouches %lu, pointerSet count %lu",[[event allTouches] count], [pointerIdSet count]);
}
#endif
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
#if !TARGET_OS_TV
if (@available(iOS 13.4, *)) {
for (UITouch* touch in touches) [self handleUITouch:touch index:0];// Native touch (absoluteTouch) first!
return;
// NSLog(@"touchesEnded - allTouches %lu, pointerSet count %lu",[[event allTouches] count], [pointerIdSet count]);
}
#endif
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
#if !TARGET_OS_TV
if (@available(iOS 13.4, *)) {
for (UITouch* touch in touches) [self handleUITouch:touch index:0];// Native touch (absoluteTouch) first!
return;
}
// NSLog(@"touchesCancelled - allTouches %lu, pointerSet count %lu",[[event allTouches] count], [pointerIdSet count]);
#endif
}
+34
View File
@@ -0,0 +1,34 @@
//
// NativeTouchPointer.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 NativeTouchPointer : 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
+141
View File
@@ -0,0 +1,141 @@
//
// NativeTouchPointer.m
// Moonlight
//
// Created by ZWM on 2024/5/14.
// Copyright © 2024 Moonlight Game Streaming Project. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "NativeTouchPointer.h"
#include <Limelight.h>
// native touch pointer class deals with:
// generating pointerId randomly, pointerId Dic & set management
// pointer instance with coordinate manipulation.
// 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<NSNumber *> *pointerIdSet; //pointerIdSet for native touch.
static NSMutableDictionary *pointerDict;
static CGFloat pointerVelocityFactor;
static CGFloat pointerVelocityDivider;
static CGFloat pointerVelocityDividerLocation;
StreamView *streamView;
@implementation NativeTouchPointer{
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];
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:[[NativeTouchPointer alloc] initWith:touch] forKey:@((uint64_t)touch)];
}
+ (void)removePointerObjFromDict:(UITouch*)touch{
uint64_t eventAddrValue = (uint64_t)touch;
NativeTouchPointer* 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{
NativeTouchPointer *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
+4
View File
@@ -28,6 +28,10 @@
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
config:(StreamConfiguration*)streamConfig;
- (void) showOnScreenControls;
- (CGSize) getVideoAreaSize;
- (CGPoint) adjustCoordinatesForVideoArea:(CGPoint)point;
- (uint16_t)getRotationFromAzimuthAngle:(float)azimuthAngle;
- (OnScreenControlsLevel) getCurrentOscState;
#if !TARGET_OS_TV
+34 -56
View File
@@ -11,6 +11,7 @@
#import "DataManager.h"
#import "ControllerSupport.h"
#import "KeyboardSupport.h"
#import "NativeTouchPointer.h"
#import "NativeTouchHandler.h"
#import "RelativeTouchHandler.h"
#import "AbsoluteTouchHandler.h"
@@ -92,21 +93,24 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
// 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];
[TouchPointer setPointerVelocityDivider:settings.pointerVelocityModeDivider.floatValue];
[TouchPointer setPointerVelocityFactor:settings.touchPointerVelocityFactor.floatValue];
[TouchPointer initContextWith:self];
}
else {
self->touchHandler = [[RelativeTouchHandler alloc] initWithView:self];
// iOS uses touch Mode depending on user preference
switch (settings.touchMode.intValue) {
case NATIVE_TOUCH_MODE:
self->touchHandler = [[NativeTouchHandler alloc] initWith:self and:settings]; break;
case RELATIVE_TOUCH_MODE:
self->touchHandler = [[RelativeTouchHandler alloc] initWithView:self]; break;
case ABSOLUTE_TOUCH_MODE:
self->touchHandler = [[AbsoluteTouchHandler alloc] initWithView:self]; break;
default:
break;
}
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");
if (settings.touchMode.intValue != RELATIVE_TOUCH_MODE) {
Log(LOG_I, @"On-screen controls disabled in non-relative touch mode");
[onScreenControls setLevel:OnScreenControlsLevelOff];
}
else if (level == OnScreenControlsLevelAuto) {
@@ -335,11 +339,11 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
- (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.
if(settings.pointerVelocityModeDivider.floatValue != 1.0 && event.phase == UITouchPhaseMoved) targetCoords = [NativeTouchPointer 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]]);
LiSendTouchEvent(touchType,[NativeTouchPointer 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]]);
}
@@ -369,25 +373,25 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
switch (event.phase) {
case UITouchPhaseBegan://开始触摸
type = LI_TOUCH_EVENT_DOWN;
[TouchPointer populatePointerId:event]; //获取并记录pointerId
if(settings.pointerVelocityModeDivider.floatValue != 1.0) [TouchPointer populatePointerObjIntoDict:event];
[NativeTouchPointer populatePointerId:event]; //获取并记录pointerId
if(settings.pointerVelocityModeDivider.floatValue != 1.0) [NativeTouchPointer populatePointerObjIntoDict:event];
break;
case UITouchPhaseMoved://移动
case UITouchPhaseStationary:
type = LI_TOUCH_EVENT_MOVE;
if(settings.pointerVelocityModeDivider.floatValue != 1.0) [TouchPointer updatePointerObjInDict:event];
if(settings.pointerVelocityModeDivider.floatValue != 1.0) [NativeTouchPointer 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];
[NativeTouchPointer removePointerId:event]; //删除pointerId
if(settings.pointerVelocityModeDivider.floatValue != 1.0) [NativeTouchPointer 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];
[NativeTouchPointer removePointerId:event]; //删除pointerId
if(settings.pointerVelocityModeDivider.floatValue != 1.0) [NativeTouchPointer removePointerObjFromDict:event];
return;
case UITouchPhaseRegionEntered://停留
case UITouchPhaseRegionMoved://停留
@@ -480,29 +484,11 @@ 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<UITouch*> *arr1 = [[event allTouches] allObjects];
//// NSLog(@"touchesBegan - %lu",[[event allTouches] count]);
//// NSLog(@"touchesBegan - %lu",arr1.count);
//
// for(int i=0;i<arr1.count;i++){
// if (arr1[i].type == UITouchTypePencil) {
// [self sendStylusEvent:arr1[i]];
// return;
//
// }
// // TemporarySettings* settings = [[[DataManager alloc] init] getSettings];
// if (settings.absoluteTouchMode) {
// [self handleUITouch:arr1[i] index:i];
// return;
// }
// }
if (settings.absoluteTouchMode) {
for (UITouch* touch in touches) [self handleUITouch:touch index:0];// Native touch (absoluteTouch) first!
return;
if (settings.touchMode.intValue == NATIVE_TOUCH_MODE) {
[touchHandler touchesBegan:touches withEvent:event];
return; //This is a native touch oriented fork, in native touch mode, this call back method deals with native touch only.
}
else{
for (UITouch* touch in touches) {
@@ -511,7 +497,6 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
}
}
}
// NSLog(@"touchesBegan - allTouches %lu, pointerSet count %lu",[[event allTouches] count], [pointerIdSet count]);
}
#endif
if ([self handleMouseButtonEvent:BUTTON_ACTION_PRESS
@@ -659,9 +644,9 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
#if !TARGET_OS_TV
if (@available(iOS 13.4, *)) {
if (settings.absoluteTouchMode) {
for (UITouch* touch in touches) [self handleUITouch:touch index:0];// Native touch (absoluteTouch) first!
return;
if (settings.touchMode.intValue == NATIVE_TOUCH_MODE) {
[touchHandler touchesMoved:touches withEvent:event];
return; //This is a native touch oriented fork, in native touch mode, this call back method deals with native touch only.
}
else{
for (UITouch* touch in touches) {
@@ -670,8 +655,6 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
}
}
}
// NSLog(@"touchesMoved - allTouches %lu, pointerSet count %lu",[[event allTouches] count], [pointerIdSet count]);
UITouch *touch = [touches anyObject];
if (touch.type == UITouchTypeIndirectPointer) {
if (@available(iOS 14.0, *)) {
@@ -741,9 +724,9 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
#if !TARGET_OS_TV
if (@available(iOS 13.4, *)) {
if (settings.absoluteTouchMode) {
for (UITouch* touch in touches) [self handleUITouch:touch index:0];// Native touch (absoluteTouch) first!
return;
if (settings.touchMode.intValue == NATIVE_TOUCH_MODE) {
[touchHandler touchesEnded:touches withEvent:event];
return; //This is a native touch oriented fork, in native touch mode, this call back method deals with native touch only.
}
else{
for (UITouch* touch in touches) {
@@ -752,7 +735,6 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
}
}
}
// NSLog(@"touchesEnded - allTouches %lu, pointerSet count %lu",[[event allTouches] count], [pointerIdSet count]);
}
#endif
if ([self handleMouseButtonEvent:BUTTON_ACTION_RELEASE
@@ -775,10 +757,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
[touchHandler touchesCancelled:touches withEvent:event];
#if !TARGET_OS_TV
if (@available(iOS 13.4, *)) {
if (settings.absoluteTouchMode) {
for (UITouch* touch in touches) [self handleUITouch:touch index:0];// Native touch (absoluteTouch) first!
return;
}
if (settings.touchMode.intValue == NATIVE_TOUCH_MODE) return; //This is a native touch oriented fork, in native touch mode, this call back method deals with native touch only.
else{
for (UITouch* touch in touches) {
if (touch.type == UITouchTypePencil) {
@@ -787,7 +766,6 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
}
}
}
// NSLog(@"touchesCancelled - allTouches %lu, pointerSet count %lu",[[event allTouches] count], [pointerIdSet count]);
#endif
[self handleMouseButtonEvent:BUTTON_ACTION_RELEASE
forTouches:touches
@@ -21,7 +21,6 @@
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
</entity>
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="audioConfig" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
@@ -42,6 +41,7 @@
<attribute name="swapABXYButtons" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="swipeExitScreenEdge" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="swipeToExitDistance" optional="YES" attributeType="Float" defaultValueString="0.5" usesScalarValueType="NO" syncable="YES"/>
<attribute name="touchMode" optional="YES" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="touchPointerVelocityFactor" optional="YES" attributeType="Float" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
<attribute name="useFramePacing" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
@@ -264,7 +264,8 @@ BOOL isCustomResolution(CGSize res) {
[self.hdrSelector setSelectedSegmentIndex:currentSettings.enableHdr ? 1 : 0];
}
[self.touchModeSelector setSelectedSegmentIndex:currentSettings.absoluteTouchMode ? 1 : 0];
// [self.touchModeSelector setSelectedSegmentIndex:currentSettings.absoluteTouchMode ? 1 : 0];
[self.touchModeSelector setSelectedSegmentIndex:currentSettings.touchMode.intValue]; //Load old touchMode setting
[self.touchModeSelector addTarget:self action:@selector(touchModeChanged) forControlEvents:UIControlEventValueChanged];
[self.statsOverlaySelector setSelectedSegmentIndex:currentSettings.statsOverlay ? 1 : 0];
[self.btMouseSelector setSelectedSegmentIndex:currentSettings.btMouseSupport ? 1 : 0];
@@ -280,7 +281,7 @@ BOOL isCustomResolution(CGSize res) {
[self.framerateSelector setSelectedSegmentIndex:framerate];
[self.framerateSelector addTarget:self action:@selector(updateBitrate) forControlEvents:UIControlEventValueChanged];
[self.onscreenControlSelector setSelectedSegmentIndex:onscreenControls];
[self.onscreenControlSelector setEnabled:!currentSettings.absoluteTouchMode];
[self.onscreenControlSelector setEnabled:[self.touchModeSelector selectedSegmentIndex] == RELATIVE_TOUCH_MODE];//initialize controller UI widget
[self.bitrateSlider setMinimumValue:0];
[self.bitrateSlider setMaximumValue:(sizeof(bitrateTable) / sizeof(*bitrateTable)) - 1];
[self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES];
@@ -302,10 +303,13 @@ BOOL isCustomResolution(CGSize res) {
[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.pointerVelocityModeDividerSlider setEnabled:[self.touchModeSelector selectedSegmentIndex] == NATIVE_TOUCH_MODE]; // pointer velocity scaling works only in native touch mode.
[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.
[self.touchPointerVelocityFactorSlider setEnabled:[self.touchModeSelector selectedSegmentIndex] == NATIVE_TOUCH_MODE]; // pointer velocity scaling works only in native touch mode.
}
- (void) pointerVelocityModeDividerSliderMoved {
@@ -338,9 +342,9 @@ BOOL isCustomResolution(CGSize res) {
- (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.
[self.onscreenControlSelector setEnabled:[self.touchModeSelector selectedSegmentIndex] == RELATIVE_TOUCH_MODE];
[self.pointerVelocityModeDividerSlider setEnabled:[self.touchModeSelector selectedSegmentIndex] == NATIVE_TOUCH_MODE]; // pointer velocity scaling works only in native touch mode.
[self.touchPointerVelocityFactorSlider setEnabled:[self.touchModeSelector selectedSegmentIndex] == NATIVE_TOUCH_MODE]; // pointer velocity scaling works only in native touch mode.
}
- (void) updateBitrate {
@@ -625,7 +629,8 @@ BOOL isCustomResolution(CGSize res) {
uint32_t preferredCodec = [self getChosenCodecPreference];
BOOL btMouseSupport = [self.btMouseSelector selectedSegmentIndex] == 1;
BOOL useFramePacing = [self.framePacingSelector selectedSegmentIndex] == 1;
BOOL absoluteTouchMode = [self.touchModeSelector selectedSegmentIndex] == 1;
// BOOL absoluteTouchMode = [self.touchModeSelector selectedSegmentIndex] == 1;
NSInteger touchMode = [self.touchModeSelector selectedSegmentIndex];
BOOL statsOverlay = [self.statsOverlaySelector selectedSegmentIndex] == 1;
BOOL enableHdr = [self.hdrSelector selectedSegmentIndex] == 1;
[dataMan saveSettingsWithBitrate:_bitrate
@@ -649,7 +654,8 @@ BOOL isCustomResolution(CGSize res) {
useFramePacing:useFramePacing
enableHdr:enableHdr
btMouseSupport:btMouseSupport
absoluteTouchMode:absoluteTouchMode
// absoluteTouchMode:absoluteTouchMode
touchMode:touchMode
statsOverlay:statsOverlay];
}