further renaming
This commit is contained in:
1 parent
9e41537554
commit
ee03f6b866
353 files changed
+14
-15
No files matched your search
@@ -1,19 +0,0 @@
|
||||
//
|
||||
// AbsoluteTouchHandler.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 11/1/20.
|
||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import "StreamView.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AbsoluteTouchHandler : UIResponder
|
||||
|
||||
-(id)initWithView:(StreamView*)view;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,152 +0,0 @@
|
||||
//
|
||||
// AbsoluteTouchHandler.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 11/1/20.
|
||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved
|
||||
//
|
||||
// Modified by True砖家 since 2024.6.1
|
||||
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved
|
||||
//
|
||||
|
||||
#import "AbsoluteTouchHandler.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
// How long the fingers must be stationary to start a right click
|
||||
#define LONG_PRESS_ACTIVATION_DELAY 0.650f
|
||||
|
||||
// How far the finger can move before it cancels a right click
|
||||
#define LONG_PRESS_ACTIVATION_DELTA 0.01f
|
||||
|
||||
// How long the double tap deadzone stays in effect between touch up and touch down
|
||||
#define DOUBLE_TAP_DEAD_ZONE_DELAY 0.250f
|
||||
|
||||
// How far the finger can move before it can override the double tap deadzone
|
||||
#define DOUBLE_TAP_DEAD_ZONE_DELTA 0.025f
|
||||
|
||||
@implementation AbsoluteTouchHandler {
|
||||
StreamView* streamView;
|
||||
|
||||
NSTimer* longPressTimer;
|
||||
UITouch* lastTouchDown;
|
||||
CGPoint lastTouchDownLocation;
|
||||
UITouch* lastTouchUp;
|
||||
CGPoint lastTouchUpLocation;
|
||||
|
||||
// upper screen edge check
|
||||
bool touchPointSpawnedAtUpperScreenEdge;
|
||||
CGFloat slideGestureVerticalThreshold;
|
||||
CGFloat screenWidthWithThreshold;
|
||||
CGFloat EDGE_TOLERANCE;
|
||||
|
||||
}
|
||||
|
||||
- (id)initWithView:(StreamView*)view {
|
||||
self = [self init];
|
||||
self->streamView = view;
|
||||
|
||||
// upper screen check
|
||||
EDGE_TOLERANCE = 15.0;
|
||||
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
||||
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - EDGE_TOLERANCE;
|
||||
self->touchPointSpawnedAtUpperScreenEdge = false;
|
||||
|
||||
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 {
|
||||
|
||||
CGPoint initialPoint = [[touches anyObject] locationInView:streamView];
|
||||
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < EDGE_TOLERANCE || initialPoint.x > screenWidthWithThreshold)) {
|
||||
self->touchPointSpawnedAtUpperScreenEdge = true;
|
||||
return; // we're done here. this touch event will not be sent to the remote PC.
|
||||
}
|
||||
|
||||
touchPointSpawnedAtUpperScreenEdge = false; // reset this flag immediately if we get a touch event passing the check above, this fixes irresponsive touch after closing the command tool menu.
|
||||
|
||||
// Ignore touch down events with more than one finger
|
||||
if ([[event allTouches] count] > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
UITouch* touch = [touches anyObject];
|
||||
CGPoint touchLocation = [touch locationInView:streamView];
|
||||
|
||||
// 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 / streamView.bounds.size.width) - (lastTouchUpLocation.x / streamView.bounds.size.width), 2) +
|
||||
pow((touchLocation.y / streamView.bounds.size.height) - (lastTouchUpLocation.y / streamView.bounds.size.height), 2)) > DOUBLE_TAP_DEAD_ZONE_DELTA) {
|
||||
[streamView 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 {
|
||||
|
||||
if(touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC.
|
||||
|
||||
// Ignore touch move events with more than one finger
|
||||
if ([[event allTouches] count] > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
UITouch* touch = [touches anyObject];
|
||||
CGPoint touchLocation = [touch locationInView:streamView];
|
||||
|
||||
if (sqrt(pow((touchLocation.x / streamView.bounds.size.width) - (lastTouchDownLocation.x / streamView.bounds.size.width), 2) +
|
||||
pow((touchLocation.y / streamView.bounds.size.height) - (lastTouchDownLocation.y / streamView.bounds.size.height), 2)) > LONG_PRESS_ACTIVATION_DELTA) {
|
||||
// Moved too far since touch down. Cancel the long press timer.
|
||||
[longPressTimer invalidate];
|
||||
longPressTimer = nil;
|
||||
}
|
||||
|
||||
[streamView updateCursorLocation:[[touches anyObject] locationInView:streamView] isMouse:NO];
|
||||
}
|
||||
|
||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
|
||||
if(touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC.
|
||||
|
||||
// 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:streamView];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
// Treat this as a normal touchesEnded event
|
||||
[self touchesEnded:touches withEvent:event];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,601 +0,0 @@
|
||||
//
|
||||
// KeyManager.swift
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2024/7/23.
|
||||
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
// Define the RemoteCommand class
|
||||
@objc public class RemoteCommand: NSObject, NSSecureCoding {
|
||||
// MARK: - NSSecureCoding
|
||||
|
||||
public static var supportsSecureCoding: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
@objc var cmdString: String
|
||||
@objc var alias: String
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(cmdString: String, alias: String) {
|
||||
self.cmdString = cmdString
|
||||
self.alias = alias
|
||||
}
|
||||
|
||||
// MARK: - NSSecureCoding
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
guard let cmdString = coder.decodeObject(of: NSString.self, forKey: "keyboardCmdString") as String?,
|
||||
let alias = coder.decodeObject(of: NSString.self, forKey: "alias") as String? else {
|
||||
return nil
|
||||
}
|
||||
self.cmdString = cmdString
|
||||
self.alias = alias
|
||||
}
|
||||
|
||||
public func encode(with coder: NSCoder) {
|
||||
coder.encode(cmdString, forKey: "keyboardCmdString")
|
||||
coder.encode(alias, forKey: "alias")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Define the CommandManager class
|
||||
@objc public class CommandManager: NSObject {
|
||||
@objc public static let shared = CommandManager()
|
||||
|
||||
@objc public static let mouseButtonMappings: [String: Int32] = [
|
||||
"M_LEFT" : BUTTON_LEFT,
|
||||
"MLEFT" : BUTTON_LEFT,
|
||||
"M_MIDDLE" : BUTTON_MIDDLE,
|
||||
"MMIDDLE" : BUTTON_MIDDLE,
|
||||
"M_RIGHT" : BUTTON_RIGHT,
|
||||
"MRIGHT" : BUTTON_RIGHT,
|
||||
"M_X1" : BUTTON_X1,
|
||||
"MX1" : BUTTON_X1,
|
||||
"M_X2" : BUTTON_X2,
|
||||
"MX2" : BUTTON_X2
|
||||
]
|
||||
|
||||
|
||||
@objc public static let oscButtonMappings: [String: Int32] = [
|
||||
"OSCA" : A_FLAG,
|
||||
"OSCB" : B_FLAG,
|
||||
"OSCX" : X_FLAG,
|
||||
"OSCY" : Y_FLAG,
|
||||
"OSCL1" : LB_FLAG,
|
||||
"L1" : LB_FLAG,
|
||||
"LB" : LB_FLAG,
|
||||
"OSCR1" : RB_FLAG,
|
||||
"R1" : RB_FLAG,
|
||||
"RB" : RB_FLAG,
|
||||
"OSCL3" : LS_CLK_FLAG,
|
||||
"L3" : LS_CLK_FLAG,
|
||||
"LS" : LS_CLK_FLAG,
|
||||
"OSCR3" : RS_CLK_FLAG,
|
||||
"R3" : RS_CLK_FLAG,
|
||||
"RS" : RS_CLK_FLAG,
|
||||
"OSCSTART" : PLAY_FLAG,
|
||||
"OSCPLAY" : PLAY_FLAG,
|
||||
"OSCSELECT" : BACK_FLAG,
|
||||
"OSCBACK" : BACK_FLAG,
|
||||
"OSCUP" : UP_FLAG,
|
||||
"OSCDOWN" : DOWN_FLAG,
|
||||
"OSCLEFT" : LEFT_FLAG,
|
||||
"OSCRIGHT" : RIGHT_FLAG,
|
||||
"OSCL2" : 0,
|
||||
"L2" : 0,
|
||||
"LT" : 0,
|
||||
"OSCR2" : 0,
|
||||
"R2" : 0,
|
||||
"RT" : 0
|
||||
]
|
||||
|
||||
@objc public static let oscRectangleButtonCmds: [String] = [
|
||||
"OSCUP",
|
||||
"OSCDOWN",
|
||||
"OSCLEFT",
|
||||
"OSCRIGHT",
|
||||
"OSCSTART",
|
||||
"OSCPLAY",
|
||||
"OSCSELECT",
|
||||
"OSCBACK"
|
||||
]
|
||||
|
||||
@objc public static let touchPadCmds: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD", "MOUSEPAD", "DPAD", "WASDPAD", "ARROWPAD"]
|
||||
@objc public static let stickTouchPads: [String] = ["LSVPAD", "RSVPAD", "LSPAD", "RSPAD"]
|
||||
@objc public static let nonVectorStickPads: [String] = ["LSPAD", "RSPAD"]
|
||||
@objc public static let specialOverlayButtonCmds: [String] = ["SETTINGS"]
|
||||
|
||||
// @objc public static let specialGameWidgets: [String] = ["YSRSV", "YSLT", "YSRT", "YSRB", "YSB", "YSRT2", "YSRB2", "YSB2", "YSEM", "YSML", "YSMR", "YSWASD"]
|
||||
|
||||
static let keyboardButtonMappings: [String: Int16] = [
|
||||
// Windows Key Codes
|
||||
"NULL": 0xFF,
|
||||
"CTRL": 0x11, // VK_CONTROL
|
||||
"SHIFT": 0x10, // VK_SHIFT
|
||||
"ALT": 0x12, // VK_MENU
|
||||
"F1": 0x70, // VK_F1
|
||||
"F2": 0x71, // VK_F2
|
||||
"F3": 0x72, // VK_F3
|
||||
"F4": 0x73, // VK_F4
|
||||
"F5": 0x74, // VK_F5
|
||||
"F6": 0x75, // VK_F6
|
||||
"F7": 0x76, // VK_F7
|
||||
"F8": 0x77, // VK_F8
|
||||
"F9": 0x78, // VK_F9
|
||||
"F10": 0x79, // VK_F10
|
||||
"F11": 0x7A, // VK_F11
|
||||
"F12": 0x7B, // VK_F12
|
||||
"A": 0x41, // 'A' key
|
||||
"B": 0x42, // 'B' key
|
||||
"C": 0x43, // 'C' key
|
||||
"D": 0x44, // 'D' key
|
||||
"E": 0x45, // 'E' key
|
||||
"F": 0x46, // 'F' key
|
||||
"G": 0x47, // 'G' key
|
||||
"H": 0x48, // 'H' key
|
||||
"I": 0x49, // 'I' key
|
||||
"J": 0x4A, // 'J' key
|
||||
"K": 0x4B, // 'K' key
|
||||
"L": 0x4C, // 'L' key
|
||||
"M": 0x4D, // 'M' key
|
||||
"N": 0x4E, // 'N' key
|
||||
"O": 0x4F, // 'O' key
|
||||
"P": 0x50, // 'P' key
|
||||
"Q": 0x51, // 'Q' key
|
||||
"R": 0x52, // 'R' key
|
||||
"S": 0x53, // 'S' key
|
||||
"T": 0x54, // 'T' key
|
||||
"U": 0x55, // 'U' key
|
||||
"V": 0x56, // 'V' key
|
||||
"W": 0x57, // 'W' key
|
||||
"X": 0x58, // 'X' key
|
||||
"Y": 0x59, // 'Y' key
|
||||
"Z": 0x5A, // 'Z' key
|
||||
"0": 0x30, // '0' key
|
||||
"1": 0x31, // '1' key
|
||||
"2": 0x32, // '2' key
|
||||
"3": 0x33, // '3' key
|
||||
"4": 0x34, // '4' key
|
||||
"5": 0x35, // '5' key
|
||||
"6": 0x36, // '6' key
|
||||
"7": 0x37, // '7' key
|
||||
"8": 0x38, // '8' key
|
||||
"9": 0x39, // '9' key
|
||||
"ESC": 0x1B, // VK_ESCAPE
|
||||
"SPACE": 0x20, // VK_SPACE
|
||||
"ENTER": 0x0D, // VK_RETURN
|
||||
"TAB": 0x09, // VK_TAB
|
||||
"BACKSPACE": 0x08, // VK_BACK
|
||||
"INSERT": 0x2D, // VK_INSERT
|
||||
"DEL": 0x2E, // VK_DELETE
|
||||
"HOME": 0x24, // VK_HOME
|
||||
"END": 0x23, // VK_END
|
||||
"PG_UP": 0x21, // VK_PRIOR
|
||||
"PGUP": 0x21, // VK_PRIOR
|
||||
"PG_DOWN": 0x22, // VK_NEXT
|
||||
"PGDOWN": 0x22, // VK_NEXT
|
||||
"PGDN": 0x22, // VK_NEXT
|
||||
"UP_ARROW": 0x26, // VK_UP
|
||||
"UPARR": 0x26, // VK_UP
|
||||
"DOWN_ARROW": 0x28, // VK_DOWN
|
||||
"DOWNARR": 0x28, // VK_DOWN
|
||||
"LEFT_ARROW": 0x25, // VK_LEFT
|
||||
"LEFTARR": 0x25, // VK_LEFT
|
||||
"RIGHT_ARROW": 0x27, // VK_RIGHT
|
||||
"RIGHTARR": 0x27, // VK_RIGHT
|
||||
"NUM_LCK": 0x90, // VK_NUMLOCK
|
||||
"NUMLCK": 0x90, // VK_NUMLOCK
|
||||
"SCR_LCK": 0x91, // VK_SCROLL
|
||||
"SCRLCK": 0x91, // VK_SCROLL
|
||||
"CAPS_LOCK": 0x14, // VK_CAPITAL
|
||||
"CAPSLOCK": 0x14, // VK_CAPITAL
|
||||
"PAUSE": 0x13, // VK_PAUSE
|
||||
"PR_SCR": 0x2C, // VK_SNAPSHOT
|
||||
"PRSCR": 0x2C, // VK_SNAPSHOT
|
||||
"NUMPAD0": 0x60, // VK_NUMPAD0
|
||||
"NUMPAD1": 0x61, // VK_NUMPAD1
|
||||
"NUMPAD2": 0x62, // VK_NUMPAD2
|
||||
"NUMPAD3": 0x63, // VK_NUMPAD3
|
||||
"NUMPAD4": 0x64, // VK_NUMPAD4
|
||||
"NUMPAD5": 0x65, // VK_NUMPAD5
|
||||
"NUMPAD6": 0x66, // VK_NUMPAD6
|
||||
"NUMPAD7": 0x67, // VK_NUMPAD7
|
||||
"NUMPAD8": 0x68, // VK_NUMPAD8
|
||||
"NUMPAD9": 0x69, // VK_NUMPAD9
|
||||
"MULTIPLY": 0x6A, // VK_MULTIPLY
|
||||
"ADD": 0x6B, // VK_ADD
|
||||
"SUBTRACT": 0x6D, // VK_SUBTRACT
|
||||
"DECIMAL": 0x6E, // VK_DECIMAL
|
||||
"DIVIDE": 0x6F, // VK_DIVIDE
|
||||
"SEMI_COLON": 0xBA, // VK_OEM_1
|
||||
"SEMICOLON": 0xBA, // VK_OEM_1
|
||||
"EQUALS": 0xBB, // VK_OEM_PLUS
|
||||
"COMMA": 0xBC, // VK_OEM_COMMA
|
||||
"MINUS": 0xBD, // VK_OEM_MINUS
|
||||
"PERIOD": 0xBE, // VK_OEM_PERIOD
|
||||
"FORWARD_SLASH": 0xBF, // VK_OEM_2
|
||||
"FORWARDSLASH": 0xBF, // VK_OEM_2
|
||||
"GRAVE_ACCENT": 0xC0, // VK_OEM_3
|
||||
"GRAVEACCENT": 0xC0, // VK_OEM_3
|
||||
"OPEN_BRACKET": 0xDB, // VK_OEM_4
|
||||
"OPENBRACKET": 0xDB, // VK_OEM_4
|
||||
"BACKSLASH": 0xDC, // VK_OEM_5
|
||||
"CLOSE_BRACKET": 0xDD, // VK_OEM_6
|
||||
"CLOSEBRACKET": 0xDD, // VK_OEM_6
|
||||
"SINGLE_QUOTE": 0xDE, // VK_OEM_7
|
||||
"SINGLEQUOTE": 0xDE, // VK_OEM_7
|
||||
"VOLUME_MUTE": 0xAD, // VK_VOLUME_MUTE
|
||||
"VOLMUTE": 0xAD, // VK_VOLUME_MUTE
|
||||
"VOLUME_DOWN": 0xAE, // VK_VOLUME_DOWN
|
||||
"VOLDOWN": 0xAE, // VK_VOLUME_DOWN
|
||||
"VOLUME_UP": 0xAF, // VK_VOLUME_UP
|
||||
"VOLUP": 0xAF, // VK_VOLUME_UP
|
||||
"MEDIA_NEXT": 0xB0, // VK_MEDIA_NEXT_TRACK
|
||||
"MEDIANEXT": 0xB0, // VK_MEDIA_NEXT_TRACK
|
||||
"MEDIA_PREV": 0xB1, // VK_MEDIA_PREV_TRACK
|
||||
"MEDIAPREV": 0xB1, // VK_MEDIA_PREV_TRACK
|
||||
"MEDIA_STOP": 0xB2, // VK_MEDIA_STOP
|
||||
"MEDIASTOP": 0xB2, // VK_MEDIA_STOP
|
||||
"MEDIA_PLAY_PAUSE": 0xB3, // VK_MEDIA_PLAY_PAUSE
|
||||
"PLAYPAUSE": 0xB3, // VK_MEDIA_PLAY_PAUSE
|
||||
"LAUNCH_MAIL": 0xB4, // VK_LAUNCH_MAIL
|
||||
"LAUNCHMAIL": 0xB4, // VK_LAUNCH_MAIL
|
||||
"LAUNCH_MEDIA_SELECT": 0xB5, // VK_LAUNCH_MEDIA_SELECT
|
||||
"MEDIA_SELECT": 0xB5, // VK_LAUNCH_MEDIA_SELECT
|
||||
"LAUNCH_APP1": 0xB6, // VK_LAUNCH_APP1
|
||||
"LAUNCHAPP1": 0xB6, // VK_LAUNCH_APP1
|
||||
"LAUNCH_APP2": 0xB7, // VK_LAUNCH_APP2
|
||||
"LAUNCHAPP2": 0xB7, // VK_LAUNCH_APP2
|
||||
"WIN": 0x5B,
|
||||
"LEFT_WIN": 0x5B, // VK_LWIN
|
||||
"RIGHT_WIN": 0x5C, // VK_RWIN
|
||||
"RIGHTWIN": 0x5C, // VK_RWIN
|
||||
"APPS": 0x5D, // VK_APPS
|
||||
|
||||
// macOS Key Codes
|
||||
"CMD": 0x37, // ⌘ Command
|
||||
"OPT": 0x3A, // ⌥ Option
|
||||
"CONTROL": 0x3B, // ⌃ Control
|
||||
"FUNCTION": 0x3F, // fn
|
||||
"SHIFTMAC": 0x38, // ⇧ Shift
|
||||
"DELETEMAC": 0x75, // Forward Delete
|
||||
"RETURNMAC": 0x24, // Return
|
||||
"ENTERMAC": 0x4C, // Enter
|
||||
"ESCAPEMAC": 0x35, // Escape
|
||||
"TABMAC": 0x30, // Tab
|
||||
"SPACEMAC": 0x31, // Space
|
||||
"UPARRMAC": 0x7E, // Up Arrow
|
||||
"DOWNARRMAC": 0x7D, // Down Arrow
|
||||
"LEFTARRMAC": 0x7B, // Left Arrow
|
||||
"RIGHTARRMAC": 0x7C, // Right Arrow
|
||||
"F1MAC": 0x7A, // F1
|
||||
"F2MAC": 0x78, // F2
|
||||
"F3MAC": 0x63, // F3
|
||||
"F4MAC": 0x76, // F4
|
||||
"F5MAC": 0x60, // F5
|
||||
"F6MAC": 0x61, // F6
|
||||
"F7MAC": 0x62, // F7
|
||||
"F8MAC": 0x64, // F8
|
||||
"F9MAC": 0x65, // F9
|
||||
"F10MAC": 0x6D, // F10
|
||||
"F11MAC": 0x67, // F11
|
||||
"F12MAC": 0x6F, // F12
|
||||
"0MAC": 0x52, // 0
|
||||
"1MAC": 0x53, // 1
|
||||
"2MAC": 0x54, // 2
|
||||
"3MAC": 0x55, // 3
|
||||
"4MAC": 0x56, // 4
|
||||
"5MAC": 0x57, // 5
|
||||
"6MAC": 0x58, // 6
|
||||
"7MAC": 0x59, // 7
|
||||
"8MAC": 0x5A, // 8
|
||||
"9MAC": 0x5B, // 9
|
||||
"NUMPAD0MAC": 0x4F, // Numpad 0
|
||||
"NUMPAD1MAC": 0x50, // Numpad 1
|
||||
"NUMPAD2MAC": 0x51, // Numpad 2
|
||||
"NUMPAD3MAC": 0x52, // Numpad 3
|
||||
"NUMPAD4MAC": 0x53, // Numpad 4
|
||||
"NUMPAD5MAC": 0x54, // Numpad 5
|
||||
"NUMPAD6MAC": 0x55, // Numpad 6
|
||||
"NUMPAD7MAC": 0x56, // Numpad 7
|
||||
"NUMPAD8MAC": 0x57, // Numpad 8
|
||||
"NUMPAD9MAC": 0x58, // Numpad 9
|
||||
"NUMPADADDMAC": 0x45, // Numpad Add
|
||||
"NUMPADSUBTRACTMAC": 0x4A, // Numpad Subtract
|
||||
"NUMPADMULTIPLYMAC": 0x43, // Numpad Multiply
|
||||
"NUMPADDIVIDEMAC": 0x4B, // Numpad Divide
|
||||
"NUMPADDECIMALMAC": 0x41, // Numpad Decimal
|
||||
"SHIFT_MAC": 0x38, // ⇧ Shift
|
||||
"DELETE_MAC": 0x75, // Forward Delete
|
||||
"RETURN_MAC": 0x24, // Return
|
||||
"ENTER_MAC": 0x4C, // Enter
|
||||
"ESCAPE_MAC": 0x35, // Escape
|
||||
"TAB_MAC": 0x30, // Tab
|
||||
"SPACE_MAC": 0x31, // Space
|
||||
"UP_ARROW_MAC": 0x7E, // Up Arrow
|
||||
"DOWN_ARROW_MAC": 0x7D, // Down Arrow
|
||||
"LEFT_ARROW_MAC": 0x7B, // Left Arrow
|
||||
"RIGHT_ARROW_MAC": 0x7C, // Right Arrow
|
||||
"F1_MAC": 0x7A, // F1
|
||||
"F2_MAC": 0x78, // F2
|
||||
"F3_MAC": 0x63, // F3
|
||||
"F4_MAC": 0x76, // F4
|
||||
"F5_MAC": 0x60, // F5
|
||||
"F6_MAC": 0x61, // F6
|
||||
"F7_MAC": 0x62, // F7
|
||||
"F8_MAC": 0x64, // F8
|
||||
"F9_MAC": 0x65, // F9
|
||||
"F10_MAC": 0x6D, // F10
|
||||
"F11_MAC": 0x67, // F11
|
||||
"F12_MAC": 0x6F, // F12
|
||||
"0_MAC": 0x52, // 0
|
||||
"1_MAC": 0x53, // 1
|
||||
"2_MAC": 0x54, // 2
|
||||
"3_MAC": 0x55, // 3
|
||||
"4_MAC": 0x56, // 4
|
||||
"5_MAC": 0x57, // 5
|
||||
"6_MAC": 0x58, // 6
|
||||
"7_MAC": 0x59, // 7
|
||||
"8_MAC": 0x5A, // 8
|
||||
"9_MAC": 0x5B, // 9
|
||||
"NUMPAD0_MAC": 0x4F, // Numpad 0
|
||||
"NUMPAD1_MAC": 0x50, // Numpad 1
|
||||
"NUMPAD2_MAC": 0x51, // Numpad 2
|
||||
"NUMPAD3_MAC": 0x52, // Numpad 3
|
||||
"NUMPAD4_MAC": 0x53, // Numpad 4
|
||||
"NUMPAD5_MAC": 0x54, // Numpad 5
|
||||
"NUMPAD6_MAC": 0x55, // Numpad 6
|
||||
"NUMPAD7_MAC": 0x56, // Numpad 7
|
||||
"NUMPAD8_MAC": 0x57, // Numpad 8
|
||||
"NUMPAD9_MAC": 0x58, // Numpad 9
|
||||
"NUMPAD_ADD_MAC": 0x45, // Numpad Add
|
||||
"NUMPAD_SUBTRACT_MAC": 0x4A, // Numpad Subtract
|
||||
"NUMPAD_MULTIPLY_MAC": 0x43, // Numpad Multiply
|
||||
"NUMPAD_DIVIDE_MAC": 0x4B, // Numpad Divide
|
||||
"NUMPAD_DECIMAL_MAC": 0x41, // Numpad Decimal
|
||||
]
|
||||
|
||||
private var commands: [RemoteCommand] = []
|
||||
|
||||
public weak var viewController: ToolBoxViewController?
|
||||
|
||||
private override init() {
|
||||
super.init()
|
||||
loadCommands()
|
||||
}
|
||||
|
||||
@objc static func presetDefaultCommands() {
|
||||
let defaults = UserDefaults.standard
|
||||
//if true { // save default entries if the data is empty.
|
||||
if defaults.data(forKey: "savedCommands") == nil { // save default entries if the data is empty.
|
||||
var defaultCommands: [RemoteCommand] = [
|
||||
RemoteCommand(cmdString: "WIN", alias: "WIN"),
|
||||
RemoteCommand(cmdString: "F11", alias: "F11"),
|
||||
RemoteCommand(cmdString: "ESC", alias: "ESC"),
|
||||
RemoteCommand(cmdString: "CTRL+SHIFT+ESC", alias: "任务管理器(Task Manager)"),
|
||||
RemoteCommand(cmdString: "ALT+F1", alias: "N卡截图(Nvidia Screenshot)"),
|
||||
RemoteCommand(cmdString: "ALT+F9", alias: "N卡录屏(Nvidia Screen Recording)"),
|
||||
RemoteCommand(cmdString: "ALT+F4", alias: "关闭窗口(ALT+F4)"),
|
||||
RemoteCommand(cmdString: "CTRL+A", alias: "全选(Select All)"),
|
||||
RemoteCommand(cmdString: "CTRL+C", alias: "复制(Copy)"),
|
||||
RemoteCommand(cmdString: "CTRL+V", alias: "粘贴(Paste)"),
|
||||
RemoteCommand(cmdString: "WIN+D", alias: "切换桌面(Switch to Desktop)"),
|
||||
RemoteCommand(cmdString: "WIN+P", alias: "多显模式(Project)"),
|
||||
RemoteCommand(cmdString: "WIN+G", alias: "Xbox Game Bar"),
|
||||
RemoteCommand(cmdString: "SHIFT+TAB", alias: "Steam Overlay"),
|
||||
]
|
||||
|
||||
let data = try? NSKeyedArchiver.archivedData(withRootObject: defaultCommands, requiringSecureCoding: false)
|
||||
defaults.set(data, forKey: "savedCommands")
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func createTestKeyMappings() -> [String: Int16] {
|
||||
return CommandManager.keyboardButtonMappings
|
||||
}
|
||||
|
||||
// extractKeyStrings from keyboardCMDString
|
||||
@objc public func extractKeyStringsFromComboCommand(from input: String) -> [String]? {
|
||||
let keys = CommandManager.keyboardButtonMappings.keys.joined(separator: "|")
|
||||
let pattern = "^(?:(\(keys))(?:\\+(\(keys))*)*)$"
|
||||
|
||||
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
|
||||
print("Failed to create regex")
|
||||
return nil
|
||||
}
|
||||
let range = NSRange(location: 0, length: input.utf16.count)
|
||||
guard let match = regex.firstMatch(in: input, options: [], range: range) else {
|
||||
print("No match found for input: \(input)")
|
||||
return nil
|
||||
}
|
||||
print("Regex matched for input: \(input)")
|
||||
|
||||
let matchedString = (input as NSString).substring(with: match.range(at: 0))
|
||||
let keyStrings = matchedString.split(separator: "+").map { String($0) }
|
||||
|
||||
guard !keyStrings.isEmpty else {
|
||||
print("No key strings found in the matched string")
|
||||
return nil
|
||||
}
|
||||
|
||||
var validKeyStrings: [String] = []
|
||||
|
||||
for key in keyStrings {
|
||||
if CommandManager.keyboardButtonMappings.keys.contains(key) {
|
||||
validKeyStrings.append(key)
|
||||
} else {
|
||||
print(" '\(key)' is not defined in key mappings")
|
||||
return nil //treat any illegal string as a whole
|
||||
}
|
||||
}
|
||||
|
||||
if validKeyStrings.isEmpty {
|
||||
print("No valid key strings found in the matched string")
|
||||
return nil
|
||||
}
|
||||
|
||||
for (index, key) in validKeyStrings.enumerated() {
|
||||
print("Valid Key \(index): \(key)")
|
||||
}
|
||||
|
||||
return validKeyStrings
|
||||
|
||||
}
|
||||
|
||||
//super combo key button strings
|
||||
@objc public func extractSinglCmdStringsFromComboKeys(from input: String) -> [String]? {
|
||||
let combinedStrings = [CommandManager.keyboardButtonMappings.keys.map { $0 as String },
|
||||
CommandManager.oscButtonMappings.keys.map { $0 as String },
|
||||
CommandManager.mouseButtonMappings.keys.map { $0 as String },
|
||||
CommandManager.touchPadCmds.map { $0 as String }
|
||||
]
|
||||
.lazy
|
||||
.flatMap { $0 } // 三维展开
|
||||
.map(String.init(describing:)) // 安全类型转换
|
||||
|
||||
let keys = combinedStrings.joined(separator: "|")
|
||||
let pattern = "^(?:\(keys))(?:-(?:\(keys)))*(?:-\\d+MS)?$"
|
||||
|
||||
|
||||
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
|
||||
print("Failed to create regex")
|
||||
return nil
|
||||
}
|
||||
let range = NSRange(location: 0, length: input.utf16.count)
|
||||
guard let match = regex.firstMatch(in: input, options: [], range: range) else {
|
||||
print("No match found for input: \(input)")
|
||||
return nil
|
||||
}
|
||||
print("Regex matched for input: \(input)")
|
||||
|
||||
let matchedString = (input as NSString).substring(with: match.range(at: 0))
|
||||
let cmdStrings = matchedString.split(separator: "-").map { String($0) }
|
||||
|
||||
guard !cmdStrings.isEmpty else {
|
||||
print("No key strings found in the matched string")
|
||||
return nil
|
||||
}
|
||||
|
||||
var validCmdStrings: [String] = []
|
||||
|
||||
for key in cmdStrings {
|
||||
validCmdStrings.append(key)
|
||||
}
|
||||
|
||||
if validCmdStrings.isEmpty {
|
||||
print("No valid key strings found in the matched string")
|
||||
return nil
|
||||
}
|
||||
|
||||
for (index, key) in validCmdStrings.enumerated() {
|
||||
print("Valid Key \(index): \(key)")
|
||||
}
|
||||
|
||||
return validCmdStrings
|
||||
}
|
||||
|
||||
@objc public func addCommand(_ command: RemoteCommand) -> Bool {
|
||||
command.cmdString = command.cmdString.uppercased() // convert all letters to upper case
|
||||
if(command.alias.trimmingCharacters(in: .whitespacesAndNewlines).count == 0) {command.alias = command.cmdString} // copy cmd string as alias when alias is empty
|
||||
let keyStrings = extractKeyStringsFromComboCommand(from: command.cmdString)
|
||||
if (keyStrings == nil) {return false} // in case of non-keyboard command strings, return false
|
||||
commands.append(command)
|
||||
saveCommands()
|
||||
viewController?.reloadTableView() // don't know why but this reload has to be called from the CommandManager, doesn't work by calling it in the viewcontroller, probably related with the dialog box.
|
||||
return true
|
||||
}
|
||||
|
||||
@objc public func deleteCommand(at index: Int) {
|
||||
guard index >= 0 && index < commands.count else {
|
||||
return
|
||||
}
|
||||
commands.remove(at: index)
|
||||
saveCommands()
|
||||
}
|
||||
|
||||
@objc public func getAllCommands() -> [RemoteCommand] {
|
||||
return commands
|
||||
}
|
||||
|
||||
private func loadCommands() {
|
||||
if let savedCommandsData = UserDefaults.standard.data(forKey: "savedCommands") {
|
||||
do {
|
||||
// Attempt to unarchive the data into an array of RemoteCommand
|
||||
if let savedCommands = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, RemoteCommand.self], from: savedCommandsData) as? [RemoteCommand] {
|
||||
// Assign the unarchived commands to your property
|
||||
print(" Assign the unarchived commands to your property ")
|
||||
commands = savedCommands
|
||||
} else {
|
||||
// Handle the case where the data could not be unarchived into the expected type
|
||||
print("Data could not be unarchived into [RemoteCommand]")
|
||||
}
|
||||
} catch {
|
||||
// Handle any errors that occur during unarchiving
|
||||
print("Failed to unarchive savedCommands with error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func saveCommands() {
|
||||
if let data = try? NSKeyedArchiver.archivedData(withRootObject: commands, requiringSecureCoding: false) {
|
||||
UserDefaults.standard.set(data, forKey: "savedCommands")
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func sendKeyComboCommand(keyboardCmdStrings: [String], delay: TimeInterval = 0.2, index: Int = 0) { // we need a large delay for WAN streaming
|
||||
// 如果已处理完所有按键,则开始释放按键
|
||||
guard index < keyboardCmdStrings.count else {
|
||||
// 释放按键
|
||||
for keyStr in keyboardCmdStrings.reversed() { // 从后往前释放按键
|
||||
if let keyCode = CommandManager.keyboardButtonMappings[keyStr] {
|
||||
LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_UP), 0) // 释放按键
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前按键的映射值
|
||||
if let keyCode = CommandManager.keyboardButtonMappings[keyboardCmdStrings[index]] {
|
||||
// 发送当前按键的按下事件
|
||||
LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_DOWN), 0)
|
||||
|
||||
// 延迟后递归处理下一个按键
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
||||
self.sendKeyComboCommand(keyboardCmdStrings: keyboardCmdStrings, delay: delay, index: index + 1)
|
||||
}
|
||||
} else {
|
||||
print("No mapping found for \(keyboardCmdStrings[index])")
|
||||
// 如果当前按键没有映射,跳过当前按键并继续下一个
|
||||
self.sendKeyComboCommand(keyboardCmdStrings: keyboardCmdStrings, delay: delay, index: index + 1)
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func sendKeyComboDown(keyboardCmdStrings: [String]) { // we need a large delay for WAN streaming
|
||||
for keyStr in keyboardCmdStrings {
|
||||
if let keyCode = CommandManager.keyboardButtonMappings[keyStr] {
|
||||
LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_DOWN), 0) // 释放按键
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func sendKeyComboUp(keyboardCmdStrings: [String]) { // we need a large delay for WAN streaming
|
||||
for keyStr in keyboardCmdStrings {
|
||||
if let keyCode = CommandManager.keyboardButtonMappings[keyStr] {
|
||||
LiSendKeyboardEvent(keyCode, Int8(KEY_ACTION_UP), 0) // 释放按键
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
//
|
||||
// Controller.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 2/11/19.
|
||||
// Copyright © 2019 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HapticContext.h"
|
||||
|
||||
@import GameController;
|
||||
@import CoreHaptics;
|
||||
|
||||
@interface Controller : NSObject
|
||||
|
||||
typedef struct {
|
||||
float lastX;
|
||||
float lastY;
|
||||
} controller_touch_context_t;
|
||||
|
||||
@property (nullable, nonatomic, retain) GCController* gamepad;
|
||||
@property (nonatomic) int playerIndex;
|
||||
@property (nonatomic) int lastButtonFlags;
|
||||
@property (nonatomic) int emulatingButtonFlags;
|
||||
@property (nonatomic) int supportedEmulationFlags;
|
||||
@property (nonatomic) unsigned char lastLeftTrigger;
|
||||
@property (nonatomic) unsigned char lastRightTrigger;
|
||||
@property (nonatomic) short lastLeftStickX;
|
||||
@property (nonatomic) short lastLeftStickY;
|
||||
@property (nonatomic) short lastRightStickX;
|
||||
@property (nonatomic) short lastRightStickY;
|
||||
|
||||
@property (nonatomic) controller_touch_context_t primaryTouch;
|
||||
@property (nonatomic) controller_touch_context_t secondaryTouch;
|
||||
|
||||
@property (nonatomic) HapticContext* _Nullable lowFreqMotor;
|
||||
@property (nonatomic) HapticContext* _Nullable highFreqMotor;
|
||||
@property (nonatomic) HapticContext* _Nullable leftTriggerMotor;
|
||||
@property (nonatomic) HapticContext* _Nullable rightTriggerMotor;
|
||||
|
||||
@property (nonatomic) NSTimer* _Nullable accelTimer;
|
||||
@property (nonatomic) GCAcceleration lastAccelSample;
|
||||
@property (nonatomic) NSTimer* _Nullable gyroTimer;
|
||||
@property (nonatomic) GCRotationRate lastGyroSample;
|
||||
|
||||
@property (nonatomic) NSTimer* _Nullable batteryTimer;
|
||||
@property (nonatomic) GCDeviceBatteryState lastBatteryState;
|
||||
@property (nonatomic) float lastBatteryLevel;
|
||||
|
||||
@property (nonatomic) BOOL reportedArrival;
|
||||
@property (nonatomic) Controller* _Nullable mergedWithController;
|
||||
|
||||
@end
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// Controller.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 2/11/19.
|
||||
// Copyright © 2019 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Controller.h"
|
||||
|
||||
@implementation Controller
|
||||
|
||||
@end
|
||||
@@ -1,55 +0,0 @@
|
||||
//
|
||||
// ControllerSupport.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 10/20/14.
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "StreamConfiguration.h"
|
||||
#import "Controller.h"
|
||||
|
||||
@class OnScreenControls;
|
||||
|
||||
@protocol ControllerSupportDelegate <NSObject>
|
||||
|
||||
- (void) gamepadPresenceChanged;
|
||||
- (void) mousePresenceChanged;
|
||||
- (void) streamExitRequested;
|
||||
|
||||
@end
|
||||
|
||||
@interface ControllerSupport : NSObject
|
||||
|
||||
-(id) initWithConfig:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate;
|
||||
-(void) connectionEstablished;
|
||||
|
||||
-(void) initAutoOnScreenControlMode:(OnScreenControls*)osc;
|
||||
-(void) cleanup;
|
||||
-(Controller*) getOscController;
|
||||
|
||||
-(void) updateLeftStick:(Controller*)controller x:(short)x y:(short)y;
|
||||
-(void) updateRightStick:(Controller*)controller x:(short)x y:(short)y;
|
||||
|
||||
-(void) updateLeftTrigger:(Controller*)controller left:(unsigned char)left;
|
||||
-(void) updateRightTrigger:(Controller*)controller right:(unsigned char)right;
|
||||
-(void) updateTriggers:(Controller*)controller left:(unsigned char)left right:(unsigned char)right;
|
||||
|
||||
-(void) updateButtonFlags:(Controller*)controller flags:(int)flags;
|
||||
-(void) setButtonFlag:(Controller*)controller flags:(int)flags;
|
||||
-(void) clearButtonFlag:(Controller*)controller flags:(int)flags;
|
||||
|
||||
-(void) updateFinished:(Controller*)controller;
|
||||
|
||||
-(void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor;
|
||||
-(void) rumbleTriggers:(uint16_t)controllerNumber leftTrigger:(uint16_t)leftTrigger rightTrigger:(uint16_t)rightTrigger;
|
||||
-(void) setMotionEventState:(uint16_t)controllerNumber motionType:(uint8_t)motionType reportRateHz:(uint16_t)reportRateHz;
|
||||
-(void) setControllerLed:(uint16_t)controllerNumber r:(uint8_t)r g:(uint8_t)g b:(uint8_t)b;
|
||||
|
||||
-(uint16_t) getActiveGamepadMask;
|
||||
|
||||
+(int) getConnectedGamepadMask:(StreamConfiguration*)streamConfig;
|
||||
|
||||
-(NSUInteger) getConnectedGamepadCount;
|
||||
|
||||
@end
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,41 +0,0 @@
|
||||
//
|
||||
// LayoutOnScreenControls.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Long Le on 9/26/22.
|
||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
// Modified by True砖家 since 2024.6.24
|
||||
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "OnScreenControls.h"
|
||||
#import "Moonlight-Swift.h"
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
This object is a subclass of 'OnScreenControls' and adds additional properties and functions which allow the user to drag and drop each of the 19 on screen controller buttons in order to change their positions on screen. The object is used in the 'LayoutOnScreenControlsViewController' thus allowing the user to drag, drop, hide, unhide, on screen controller buttons.
|
||||
Note that this is in contrast to the game stream view which displays an 'OnScreenControls' object on screen that allows the app to register taps on each button as controller input. It does not (and naturally should not) allow the user to move the buttons around the screen.
|
||||
*/
|
||||
@interface LayoutOnScreenControls : OnScreenControls
|
||||
|
||||
@property UIView* _view;
|
||||
@property NSMutableArray *layoutChanges;
|
||||
@property CALayer *layerBeingDragged;
|
||||
@property UIViewController *layoutToolVC;
|
||||
|
||||
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport
|
||||
streamConfig:(StreamConfiguration*)streamConfig oscLevel:(int)oscLevel;
|
||||
|
||||
- (CALayer*) controllerLayerFromName:(NSString*)name;
|
||||
- (BOOL) isLayer:(CALayer*)layer hoveringOverButton:(UIButton*)button;
|
||||
|
||||
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
|
||||
- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
|
||||
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
|
||||
- (void)updateGuidelinesForOnScreenWidget:(OnScreenWidgetView* )widget;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,342 +0,0 @@
|
||||
//
|
||||
// LayoutOnScreenControls.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Long Le on 9/26/22.
|
||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
// Modified by True砖家 since 2024.6.24
|
||||
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import "LayoutOnScreenControls.h"
|
||||
#import "OSCProfilesTableViewController.h"
|
||||
#import "OnScreenButtonState.h"
|
||||
#import "OSCProfilesManager.h"
|
||||
|
||||
@interface LayoutOnScreenControls ()
|
||||
@end
|
||||
|
||||
@implementation LayoutOnScreenControls {
|
||||
|
||||
UIButton *trashCanButton;
|
||||
UIView *horizontalGuideline;
|
||||
UIView *verticalGuideline;
|
||||
}
|
||||
|
||||
@synthesize layerBeingDragged;
|
||||
@synthesize _view;
|
||||
@synthesize layoutChanges;
|
||||
|
||||
|
||||
|
||||
|
||||
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport streamConfig:(StreamConfiguration*)streamConfig oscLevel:(int)oscLevel {
|
||||
|
||||
_view = view;
|
||||
_view.multipleTouchEnabled = false;
|
||||
|
||||
|
||||
self = [super initWithView:view controllerSup:controllerSupport streamConfig:streamConfig];
|
||||
self.isLayingOut = true; // make sure the super class have a flag to tell it's currently laying out the OSCs, intead play mode.
|
||||
self._level = oscLevel;
|
||||
|
||||
layoutChanges = [[NSMutableArray alloc] init]; // will contain OSC button layout changes the user has made for this profile
|
||||
|
||||
[self drawButtons];
|
||||
[self drawGuidelines]; // add the blue guidelines that appear when button is being tapped and dragged
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Drawing
|
||||
|
||||
/**
|
||||
* This method overrides the superclass's drawButtons method. The purpose of this method is to create a dPad parent layer, and add the four dPad buttons to it so that the user can drag the entire dPad around, directional buttons and all, as one unit as is the expected behavior. Note that we do not want the four dPad buttons to be child layers of a CALayer parent layer on the game stream view since the touch logic implemented for the four dPad buttons on the game stream view is written assuming the dPad buttons are not children of another parent CALayer
|
||||
*/
|
||||
- (void) drawButtons {
|
||||
[super setDPadCenter]; // Set custom position for D-Pad here
|
||||
[super setAnalogStickPositions]; // Set custom position for analog sticks here
|
||||
[super drawBumpers];
|
||||
[super drawButtons];
|
||||
|
||||
|
||||
UIImage* downButtonImage = [UIImage imageNamed:@"DownButton"];
|
||||
UIImage* rightButtonImage = [UIImage imageNamed:@"RightButton"];
|
||||
UIImage* upButtonImage = [UIImage imageNamed:@"UpButton"];
|
||||
UIImage* leftButtonImage = [UIImage imageNamed:@"LeftButton"];
|
||||
|
||||
// create dPad background layer
|
||||
self._dPadBackground = [CALayer layer];
|
||||
self._dPadBackground.name = @"dPad";
|
||||
self._dPadBackground.frame = CGRectMake(self.D_PAD_CENTER_X,
|
||||
self.D_PAD_CENTER_Y,
|
||||
self._leftButton.frame.size.width * 2 + BUTTON_DIST,
|
||||
self._leftButton.frame.size.width * 2 + BUTTON_DIST);
|
||||
self._dPadBackground.position = CGPointMake(self.D_PAD_CENTER_X, self.D_PAD_CENTER_Y); // since dPadBackground's dimensions have change after settings its width and height you need to reset its position again here
|
||||
[self.OSCButtonLayers addObject:self._dPadBackground];
|
||||
[_view.layer addSublayer:self._dPadBackground];
|
||||
|
||||
// add dPad buttons to parent layer
|
||||
[self._dPadBackground addSublayer:self._downButton];
|
||||
[self._dPadBackground addSublayer:self._rightButton];
|
||||
[self._dPadBackground addSublayer:self._upButton];
|
||||
[self._dPadBackground addSublayer:self._leftButton];
|
||||
|
||||
[self resizeControllerLayerWith:self._dPadBackground and:_dPadSizeFactor];
|
||||
|
||||
/*
|
||||
// reposition each dPad button within their parent dPadBackground layer
|
||||
self._downButton.frame = CGRectMake(self._dPadBackground.frame.size.width/3, self._dPadBackground.frame.size.height/2 + D_PAD_DIST, downButtonImage.size.width, downButtonImage.size.height);
|
||||
self._rightButton.frame = CGRectMake(self._dPadBackground.frame.size.width/2 + D_PAD_DIST, self._dPadBackground.frame.size.height/3, rightButtonImage.size.width, rightButtonImage.size.height);
|
||||
self._upButton.frame = CGRectMake(self._dPadBackground.frame.size.width/3, 0, upButtonImage.size.width, upButtonImage.size.height);
|
||||
self._leftButton.frame = CGRectMake(0, self._dPadBackground.frame.size.height/3, leftButtonImage.size.width, leftButtonImage.size.height);*/
|
||||
}
|
||||
|
||||
/**
|
||||
* draws a horizontal and vertical line that is made visible and positioned over whichever button the user is dragging around the screen
|
||||
*/
|
||||
- (void) drawGuidelines {
|
||||
horizontalGuideline = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self._view.frame.size.width * 2, 2)];
|
||||
horizontalGuideline.backgroundColor = [UIColor blueColor];
|
||||
horizontalGuideline.hidden = YES;
|
||||
horizontalGuideline.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||
horizontalGuideline.layer.shadowOffset = CGSizeMake(0, 0);
|
||||
horizontalGuideline.layer.shadowOpacity = 0.5;
|
||||
horizontalGuideline.layer.shadowRadius = 2;
|
||||
[self._view addSubview: horizontalGuideline];
|
||||
|
||||
verticalGuideline = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 2, self._view.frame.size.height * 2)];
|
||||
verticalGuideline.backgroundColor = [UIColor blueColor];
|
||||
verticalGuideline.hidden = YES;
|
||||
verticalGuideline.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||
verticalGuideline.layer.shadowOffset = CGSizeMake(0, 0);
|
||||
verticalGuideline.layer.shadowOpacity = 0.5;
|
||||
verticalGuideline.layer.shadowRadius = 2;
|
||||
[self._view addSubview: verticalGuideline];
|
||||
}
|
||||
|
||||
- (void)updateGuidelinesForOnScreenWidget:(OnScreenWidgetView* )widget{
|
||||
/* have guidelines follow wherever the user is touching on the screen */
|
||||
horizontalGuideline.hidden = verticalGuideline.hidden = NO;
|
||||
horizontalGuideline.center = verticalGuideline.center = CGPointMake(CGRectGetMidX(widget.frame), CGRectGetMidY(widget.frame));
|
||||
/*
|
||||
Telegraph to the user whether the horizontal and/or vertical guidelines line up with one or more of the buttons on screen by doing the following:
|
||||
-Change horizontal guideline color to yellow if its y-position is almost equal to that of one of the buttons on screen.
|
||||
-Change vertical guideline color to yellow if its x-position is almost equal to that of one of the buttons on screen.
|
||||
*/
|
||||
|
||||
horizontalGuideline.backgroundColor = [UIColor blueColor]; // change horizontal guideline back to blue if it doesn't line up with one of the on screen buttons
|
||||
for (CALayer *button in self.OSCButtonLayers) { // horizontal guideline position check
|
||||
if ((layerBeingDragged != button) && !button.isHidden) {
|
||||
if ((horizontalGuideline.center.y < button.position.y + 1) &&
|
||||
(horizontalGuideline.center.y > button.position.y - 1)) {
|
||||
horizontalGuideline.backgroundColor = [UIColor yellowColor];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for(UIView* view in self.layoutToolVC.view.subviews){
|
||||
if ([view isKindOfClass:[OnScreenWidgetView class]] && view != widget) {
|
||||
if ((horizontalGuideline.center.y < view.center.y + 1) && (horizontalGuideline.center.y > view.center.y - 1)) {
|
||||
horizontalGuideline.backgroundColor = [UIColor yellowColor];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
verticalGuideline.backgroundColor = [UIColor blueColor]; // change vertical guideline back to blue if it doesn't line up with one of the on screen buttons
|
||||
for (CALayer *button in self.OSCButtonLayers) { // vertical guideline position check
|
||||
if ((layerBeingDragged != button) && !button.isHidden) {
|
||||
if ((verticalGuideline.center.x < button.position.x + 1) &&
|
||||
(verticalGuideline.center.x > button.position.x - 1)) {
|
||||
verticalGuideline.backgroundColor = [UIColor yellowColor];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for(UIView* view in self.layoutToolVC.view.subviews){
|
||||
if ([view isKindOfClass:[OnScreenWidgetView class]] && view != widget) {
|
||||
if ((verticalGuideline.center.x < view.center.x + 1) && (verticalGuideline.center.x > view.center.x - 1)) {
|
||||
verticalGuideline.backgroundColor = [UIColor yellowColor];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
- (void) updateGuidelinesForLegacyOscButton{
|
||||
/* have guidelines follow wherever the user is touching on the screen */
|
||||
horizontalGuideline.center = layerBeingDragged.position;
|
||||
verticalGuideline.center = layerBeingDragged.position;
|
||||
/*
|
||||
Telegraph to the user whether the horizontal and/or vertical guidelines line up with one or more of the buttons on screen by doing the following:
|
||||
-Change horizontal guideline color to yellow if its y-position is almost equal to that of one of the buttons on screen.
|
||||
-Change vertical guideline color to yellow if its x-position is almost equal to that of one of the buttons on screen.
|
||||
*/
|
||||
horizontalGuideline.backgroundColor = [UIColor blueColor]; // change horizontal guideline back to blue if it doesn't line up with one of the on screen buttons
|
||||
for (CALayer *button in self.OSCButtonLayers) { // horizontal guideline position check
|
||||
if ((layerBeingDragged != button) && !button.isHidden) {
|
||||
if ((horizontalGuideline.center.y < button.position.y + 1) &&
|
||||
(horizontalGuideline.center.y > button.position.y - 1)) {
|
||||
horizontalGuideline.backgroundColor = [UIColor yellowColor];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// alignment with onScreenWidgets
|
||||
for(UIView* view in self.layoutToolVC.view.subviews){
|
||||
if ([view isKindOfClass:[OnScreenWidgetView class]]) {
|
||||
if ((horizontalGuideline.center.y < view.center.y + 1) && (horizontalGuideline.center.y > view.center.y - 1)) {
|
||||
horizontalGuideline.backgroundColor = [UIColor yellowColor];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
verticalGuideline.backgroundColor = [UIColor blueColor]; // change vertical guideline back to blue if it doesn't line up with one of the on screen buttons
|
||||
for (CALayer *button in self.OSCButtonLayers) { // vertical guideline position check
|
||||
if ((layerBeingDragged != button) && !button.isHidden) {
|
||||
if ((verticalGuideline.center.x < button.position.x + 1) &&
|
||||
(verticalGuideline.center.x > button.position.x - 1)) {
|
||||
verticalGuideline.backgroundColor = [UIColor yellowColor];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// alignment with onScreenWidgets
|
||||
for(UIView* view in self.layoutToolVC.view.subviews){
|
||||
if ([view isKindOfClass:[OnScreenWidgetView class]]) {
|
||||
if ((verticalGuideline.center.x < view.center.x + 1) && (verticalGuideline.center.x > view.center.x - 1)) {
|
||||
verticalGuideline.backgroundColor = [UIColor yellowColor];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Queries
|
||||
|
||||
/* used to determine whether user is dragging an OSC button (of type CALayer) over the trash can with the intent of hiding that button */
|
||||
- (BOOL) isLayer:(CALayer *)layer hoveringOverButton:(UIButton *)button {
|
||||
CGRect buttonConvertedRect = [self._view convertRect:button.imageView.frame fromView:button.superview];
|
||||
|
||||
if (CGRectIntersectsRect(layer.frame, buttonConvertedRect)) {
|
||||
return YES;
|
||||
}
|
||||
else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
/* returns reference to button layer object given the button's name*/
|
||||
- (CALayer*)controllerLayerFromName: (NSString*)name {
|
||||
for (CALayer *buttonLayer in self.OSCButtonLayers) {
|
||||
|
||||
if ([buttonLayer.name isEqualToString:name]) {
|
||||
return buttonLayer;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
#pragma mark - Touch
|
||||
|
||||
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
/* Reset variables related to on screen controller button drag and drop routine. These variables should be reset in 'touchesCancelled' or 'touchesEnded' but these may not be called in unaccounted-for edge cases such as when the user opens various OS-level control center related views by dragging down from the top of the screen, or dragging up from the bottom of the screen. Since Apple likes to add new control centers and new ways of opening them (i.e. Dynamic Island on iPhone 14 Pro) it's best to reset these variables here when the user is beginning a new on screen controller button drag routine */
|
||||
|
||||
layerBeingDragged = nil;
|
||||
horizontalGuideline.backgroundColor = [UIColor blueColor];
|
||||
verticalGuideline.backgroundColor = [UIColor blueColor];
|
||||
|
||||
for (UITouch* touch in touches) { // Process touch
|
||||
|
||||
CGPoint touchLocation = [touch locationInView:_view];
|
||||
touchLocation = [[touch view] convertPoint:touchLocation toView:nil];
|
||||
CALayer *layer = [_view.layer hitTest:touchLocation];
|
||||
|
||||
/* Don't let user drag and move anything other than on screen controller buttons, which are CALayer types. The reason is that 'LayoutOnScreenControls' should only be responsible for managing and letting users move on screen controller buttons. Since this class's view is currently set to be set equal to the 'LayoutOnScreenControlsViewController' view it belongs to, we need to make sure touches on the VC's objects don't propagate down to 'LayoutOnScreenControls'. Weird stuff can happen to the UI buttons that belong to that VC (trash can button, undo button, save button, etc), such as them being dragged around the screen with the user's touches */
|
||||
for (UIView *subview in self._view.subviews) {
|
||||
|
||||
if (CGRectContainsPoint(subview.frame, touchLocation)) {
|
||||
if (![subview isKindOfClass:[CALayer class]]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (layer == self._upButton ||
|
||||
layer == self._downButton ||
|
||||
layer == self._leftButton ||
|
||||
layer == self._rightButton) { // don't let user drag individual dPad buttons
|
||||
layerBeingDragged = self._dPadBackground;
|
||||
}
|
||||
else if (layer == self._rightStick) { // only let user drag right stick's background, not the inner analog stick itself
|
||||
layerBeingDragged = self._rightStickBackground;
|
||||
}
|
||||
else if (layer == self._leftStick) { // only let user drag left stick's background, not the inner analog stick itself
|
||||
layerBeingDragged = self._leftStickBackground;
|
||||
}
|
||||
else if(layer == self._aButton ||
|
||||
layer == self._bButton ||
|
||||
layer == self._xButton ||
|
||||
layer == self._yButton ||
|
||||
layer == self._l1Button ||
|
||||
layer == self._l2Button ||
|
||||
layer == self._r1Button ||
|
||||
layer == self._r2Button ||
|
||||
layer == self._selectButton ||
|
||||
layer == self._startButton){ // we must iterates all the controller button layers manually here, to prevent other widgets being dragged accidently
|
||||
layerBeingDragged = layer;
|
||||
}
|
||||
|
||||
if(!layerBeingDragged) return;
|
||||
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"LegacyOscCALayerSelectedNotification" object:layerBeingDragged]; // inform that a CALayer of onScreen controller has being selected
|
||||
|
||||
/* save the name, position, and visibility of button being touched in array in case user wants to undo the change later */
|
||||
OnScreenButtonState *onScreenButtonState = [[OnScreenButtonState alloc] initWithButtonName:layerBeingDragged.name buttonType:LegacyOscButton andPosition:layerBeingDragged.position];
|
||||
// add hidden attr here
|
||||
onScreenButtonState.isHidden = layerBeingDragged.isHidden;
|
||||
|
||||
[layoutChanges addObject:onScreenButtonState];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"OSCLayoutChanged" object:self]; // lets the view controller know whether to fade the undo button in or out depending on whether there are any further OSC layout changes the user is allowed to undo
|
||||
|
||||
/* make guide lines visible and position them over the button the user is touching */
|
||||
horizontalGuideline.hidden = NO;
|
||||
verticalGuideline.hidden = NO;
|
||||
[self updateGuidelinesForLegacyOscButton];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
UITouch *touch = [touches anyObject];
|
||||
CGPoint touchLocation = [touch locationInView:_view];
|
||||
[CATransaction begin];
|
||||
[CATransaction setDisableActions:YES];
|
||||
layerBeingDragged.position = touchLocation; // move object to touch location
|
||||
[self updateGuidelinesForLegacyOscButton];
|
||||
[CATransaction commit];
|
||||
}
|
||||
|
||||
- (void) touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
layerBeingDragged = nil;
|
||||
|
||||
horizontalGuideline.hidden = YES;
|
||||
verticalGuideline.hidden = YES;
|
||||
horizontalGuideline.backgroundColor = [UIColor blueColor];
|
||||
verticalGuideline.backgroundColor = [UIColor blueColor];
|
||||
}
|
||||
|
||||
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
layerBeingDragged = nil;
|
||||
|
||||
horizontalGuideline.hidden = YES;
|
||||
verticalGuideline.hidden = YES;
|
||||
horizontalGuideline.backgroundColor = [UIColor blueColor];
|
||||
verticalGuideline.backgroundColor = [UIColor blueColor];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
@@ -1,36 +0,0 @@
|
||||
//
|
||||
// OSCProfile.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Long Le on 12/22/22.
|
||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
// Modified by True砖家 since 2024.6.24
|
||||
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "OnScreenButtonState.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
#define DEFAULT_TEMPLATE_NAME @"高级控件模板(Advanced widget templates)"
|
||||
|
||||
/**
|
||||
This object contains information pertaining to any of the user created, custom on screen controller layout configurations, or 'profiles.' The object contains a 'name' property for easy reference, as well as an 'isSelected' property which is used to determine whether this particular custom OSC layout should show on screen during game stream view. Only one 'OSCProfile' is set to 'isSelected' at any given time. The object also contains an array of 'OnScreenButtonStates' which provides information that allows us to move and hide/unhide each of the 19 on screen buttons. Note that the 'buttonStates' property should contain an NSMutableArray of ENCODED 'OnScreenButtonState' objects. This allows us to save the 'OSCProfile' object to NSUserDefaults.
|
||||
Additionally the 'OSCProfile' object adopts encoding and decoding protocols so that we can encode the object before saving it to NSUserDefaults. By saving this object to NSUserDefaults we allow the user to save and load their custom on screen controller button layouts between app launches
|
||||
*/
|
||||
@interface OSCProfile : NSObject <NSCoding, NSSecureCoding>
|
||||
|
||||
@property NSString *name;
|
||||
@property NSMutableArray <OnScreenButtonState *> *buttonStates;
|
||||
@property BOOL isSelected;
|
||||
|
||||
- (id) initWithName:(NSString*)name buttonStates:(NSMutableArray*)buttonStates isSelected:(BOOL)isSelected;
|
||||
|
||||
+ (BOOL) supportsSecureCoding;
|
||||
- (id) initWithCoder:(NSCoder*)decoder;
|
||||
- (void) encodeWithCoder:(NSCoder*)encoder;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,46 +0,0 @@
|
||||
//
|
||||
// OSCProfile.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Long Le on 12/22/22.
|
||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
// Modified by True砖家 since 2024.6.24
|
||||
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OSCProfile.h"
|
||||
|
||||
@implementation OSCProfile
|
||||
|
||||
- (id) initWithName:(NSString*)name buttonStates:(NSMutableArray*)buttonStates isSelected:(BOOL)isSelected {
|
||||
if ((self = [self init])) {
|
||||
self.name = name;
|
||||
self.buttonStates = buttonStates;
|
||||
self.isSelected = isSelected;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (BOOL) supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void) encodeWithCoder:(NSCoder*)encoder {
|
||||
[encoder encodeObject:self.name forKey:@"name"];
|
||||
[encoder encodeObject:self.buttonStates forKey:@"buttonStates"];
|
||||
[encoder encodeBool:self.isSelected forKey:@"isSelected"];
|
||||
}
|
||||
|
||||
- (id) initWithCoder:(NSCoder*)decoder {
|
||||
if (self = [super init]) {
|
||||
self.name = [decoder decodeObjectForKey:@"name"];
|
||||
self.buttonStates = [decoder decodeObjectForKey:@"buttonStates"];
|
||||
self.isSelected = [decoder decodeBoolForKey:@"isSelected"];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,75 +0,0 @@
|
||||
//
|
||||
// OSCProfilesManager.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Long Le on 1/1/23.
|
||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
// Modified by True砖家 since 2024.6.24
|
||||
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "OSCProfile.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
This singleton object can be accessed from any class and provides methods to get and set on screen controller profile related data.
|
||||
Note that the implementation file contains a number of 'Helper' methods. These helper methods are only used in this class's implementation file and help to reduce re-writing large blocks of code that are called multiple times throughout the file
|
||||
*/
|
||||
@interface OSCProfilesManager : NSObject
|
||||
@property NSMutableArray <OSCProfile *> *currentProfiles;
|
||||
|
||||
|
||||
+ (OSCProfilesManager *) sharedManager:(CGRect)viewBounds;
|
||||
+ (void) setOnScreenWidgetViewsSet:(NSMutableSet* )set;
|
||||
|
||||
#pragma mark - Getters
|
||||
/**
|
||||
* Returns an array of decoded profile objects
|
||||
*/
|
||||
- (NSMutableArray *) getAllProfiles;
|
||||
|
||||
/**
|
||||
* Returns the OSC Profile that is currently selected to be displayed on screen during game streaming
|
||||
*/
|
||||
- (OSCProfile *) getSelectedProfile;
|
||||
|
||||
/**
|
||||
* Returns the index of the 'selected' profile within the array it's in
|
||||
*/
|
||||
- (NSInteger) getIndexOfSelectedProfile;
|
||||
|
||||
- (NSMutableArray *) getEncodedProfiles;
|
||||
- (void) importEncodedProfiles:(NSMutableArray* )profilesEncoded;
|
||||
- (OnScreenButtonState *)unarchiveButtonStateEncoded:(NSData *)data;
|
||||
- (void) importDefaultTemplates;
|
||||
|
||||
#pragma mark - Setters
|
||||
/**
|
||||
* Sets the profile object with the particular 'name' as the selected profile to be displayed on screen during game streaming
|
||||
*/
|
||||
- (void) setProfileToSelected:(NSString *)name;
|
||||
|
||||
/**
|
||||
* Saves a profile object with a particular 'name' and an array of button layers (the CALayer button layers are the objects currently visible on screen) to persistent storage
|
||||
*/
|
||||
- (void) saveProfileWithName:(NSString*)name andButtonLayers:(NSMutableArray *)buttonLayers;
|
||||
|
||||
- (bool) updateSelectedProfile:(NSMutableArray *) oscButtonLayers;
|
||||
/**
|
||||
* Delete current selected profile.
|
||||
*/
|
||||
- (void) deleteCurrentSelectedProfile;
|
||||
|
||||
#pragma mark - Queries
|
||||
/**
|
||||
* Lets the caller of this method know whether a profile with a given name already exists in persistent storage
|
||||
*/
|
||||
- (BOOL) profileNameAlreadyExist:(NSString*)name;
|
||||
- (BOOL) profileName:(NSString*) name alreadyExistIn:(NSMutableArray*)profiles;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,415 +0,0 @@
|
||||
//
|
||||
// OSCProfilesManager.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Long Le on 1/1/23.
|
||||
// Copyright © 2023 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
// Modified by True砖家 since 2024.6.24
|
||||
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OSCProfilesManager.h"
|
||||
#import "OnScreenButtonState.h"
|
||||
#import "Moonlight-Swift.h"
|
||||
#import "LayoutOnScreenControlsViewController.h"
|
||||
#import "OnScreenControls.h"
|
||||
|
||||
@implementation OSCProfilesManager
|
||||
|
||||
static NSMutableSet *OnScreenWidgetViews;
|
||||
static CGRect streamViewBounds;
|
||||
|
||||
#pragma mark - Initializer
|
||||
|
||||
+ (OSCProfilesManager *) sharedManager:(CGRect)viewBounds {
|
||||
static OSCProfilesManager *_sharedManager = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
streamViewBounds = viewBounds;
|
||||
NSLog(@"bounds width: %f, height: %f", streamViewBounds.size.width, streamViewBounds.size.height);
|
||||
dispatch_once(&onceToken, ^{
|
||||
_sharedManager = [[self alloc] init];
|
||||
});
|
||||
// _sharedManager.currentProfiles = [_sharedManager getAllProfiles];
|
||||
return _sharedManager;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Class Helper Methods
|
||||
|
||||
|
||||
+ (void) setOnScreenWidgetViewsSet:(NSMutableSet* )set{
|
||||
OnScreenWidgetViews = set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the profile whose 'name' property has a value that is equal to the 'name' passed into the method
|
||||
*/
|
||||
- (OSCProfile *) OSCProfileWithName:(NSString*)name {
|
||||
NSMutableArray *profiles = [self getAllProfiles];
|
||||
|
||||
for (OSCProfile *profile in profiles) {
|
||||
|
||||
if ([profile.name isEqualToString:name]) {
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of encoded 'OSCProfile' objects from the array of decoded 'OSCProfile' objects passed into this method
|
||||
*/
|
||||
- (NSMutableArray *) encodedProfilesFromArray:(NSMutableArray *)profiles {
|
||||
|
||||
NSMutableArray *profilesEncoded = [[NSMutableArray alloc] init];
|
||||
/* encode each profile and add them back into an array */
|
||||
for (OSCProfile *profile in profiles) { //
|
||||
NSData *profileEncoded = [NSKeyedArchiver archivedDataWithRootObject:profile requiringSecureCoding:YES error:nil];
|
||||
[profilesEncoded addObject:profileEncoded];
|
||||
}
|
||||
|
||||
return profilesEncoded;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete an 'OSCProfile' object of specific index for another in the 'OSCProfile' objects array stored in persistent storage
|
||||
*/
|
||||
-(void)deleteCurrentSelectedProfile{
|
||||
NSMutableArray *profiles = [self getAllProfiles];
|
||||
NSInteger selectedIndex = [self getIndexOfSelectedProfile];
|
||||
|
||||
/* Remove the selected profile */
|
||||
if(selectedIndex != 0) [profiles removeObjectAtIndex:selectedIndex]; //deleting default profile (index 0) is not allowed
|
||||
else {
|
||||
NSLog(@"Default profile!"); // to be updated here.
|
||||
}
|
||||
|
||||
selectedIndex--;
|
||||
if(selectedIndex < 0) selectedIndex = 0;
|
||||
OSCProfile *newSelectedProfile = [profiles objectAtIndex:selectedIndex];
|
||||
newSelectedProfile.isSelected = YES;
|
||||
|
||||
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles]; // encode each 'profile' object in the array and add them to a new array
|
||||
|
||||
/* Encode the array itself, NOT the objects in the array, which are already encoded. Save array to persistent storage */
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
|
||||
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Replaces one 'OSCProfile' object for another in the 'OSCProfile' objects array stored in persistent storage
|
||||
*/
|
||||
- (void) replaceProfile:(OSCProfile*)oldProfile withProfile:(OSCProfile*)newProfile {
|
||||
NSMutableArray *profiles = [self getAllProfiles];
|
||||
|
||||
for (OSCProfile *profile in profiles) { // set all profiles' 'isSelected' property to NO since the new profile we're saving over the old profile with will be the 'selected' profile
|
||||
profile.isSelected = NO;
|
||||
}
|
||||
|
||||
/* Set the new profile as the selected one. The reasoning behind this is that this method is currently being used when the user saves over an existing profile with another profile that has the same name. The expected behavior is that the newly saved profile becomes the selected profile which will show on screen when they launch the game stream view */
|
||||
newProfile.isSelected = YES;
|
||||
|
||||
/* Remove the old profile from the array and insert the new profile into its place */
|
||||
int index = 0;
|
||||
for (int i = 0; i < profiles.count; i++) {
|
||||
|
||||
if ([[profiles[i] name] isEqualToString: oldProfile.name]) {
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
[profiles removeObjectAtIndex:index];
|
||||
[profiles insertObject:newProfile atIndex:index];
|
||||
|
||||
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles]; // encode each 'profile' object in the array and add them to a new array
|
||||
|
||||
/* Encode the array itself, NOT the objects in the array, which are already encoded. Save array to persistent storage */
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
|
||||
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
}
|
||||
|
||||
- (NSMutableArray *) getEncodedProfiles {
|
||||
NSMutableArray *profiles = [self getAllProfiles];
|
||||
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles]; // encode each 'profile' object in the array and add them to a new array
|
||||
return profilesEncoded;
|
||||
}
|
||||
|
||||
- (void) importEncodedProfiles:(NSMutableArray* )profilesEncoded {
|
||||
NSMutableArray* targetProfiles = [_currentProfiles mutableCopy]; //_currentProfiles is availabled as long as getAllProfiles was called before calling this method (_currentProfiles avoids frequent accessing persisted data)
|
||||
if([self profileName:DEFAULT_TEMPLATE_NAME alreadyExistIn:targetProfiles] && targetProfiles.count > 1) [targetProfiles removeObjectAtIndex:1];
|
||||
if(targetProfiles.count > 0) [targetProfiles removeObjectAtIndex:0];
|
||||
NSMutableArray* localEncodedPofiles = [self encodedProfilesFromArray:targetProfiles];
|
||||
[profilesEncoded addObjectsFromArray:localEncodedPofiles];
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
|
||||
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
}
|
||||
|
||||
- (void) importDefaultTemplates{
|
||||
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"widgetTemplates" ofType:@"bin"];
|
||||
if (filePath) {
|
||||
// 2. 读取二进制数据
|
||||
NSError *error;
|
||||
NSData *fileData = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&error];
|
||||
NSMutableArray *profilesEncoded;
|
||||
if (fileData && !error) {
|
||||
// 3. 成功读取
|
||||
NSSet *classes = [NSSet setWithObjects: [NSMutableData class], [NSMutableArray class], nil];
|
||||
profilesEncoded = [NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:fileData error:&error];
|
||||
[self importEncodedProfiles:profilesEncoded];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Globally Accessible Methods
|
||||
|
||||
#pragma mark - Getters
|
||||
|
||||
- (NSMutableArray *) getAllProfiles {
|
||||
|
||||
NSData *profilesArrayEncoded = [[NSUserDefaults standardUserDefaults] objectForKey: @"OSCProfiles"]; // Get the encoded array of encoded OSC profiles from persistent storage
|
||||
NSSet *classes = [NSSet setWithObjects:[NSString class], [NSMutableData class], [NSMutableArray class], [OSCProfile class], [OnScreenButtonState class], nil];
|
||||
|
||||
NSMutableArray *profilesEncoded = [NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:profilesArrayEncoded error:nil]; // Decode the encoded array itself, NOT the objects contained in the array
|
||||
|
||||
/* Decode each of the encoded profiles, place them into an array, then return the array */
|
||||
NSMutableArray *profilesDecoded = [[NSMutableArray alloc] init];
|
||||
OSCProfile *profileDecoded;
|
||||
for (NSData *profileEncoded in profilesEncoded) {
|
||||
|
||||
profileDecoded = [NSKeyedUnarchiver unarchivedObjectOfClasses: classes fromData:profileEncoded error: nil];
|
||||
[profilesDecoded addObject: profileDecoded];
|
||||
}
|
||||
_currentProfiles = profilesDecoded;
|
||||
return profilesDecoded;
|
||||
}
|
||||
|
||||
- (OSCProfile *) getSelectedProfile {
|
||||
NSMutableArray *profiles = [self getAllProfiles];
|
||||
|
||||
for (OSCProfile *profile in profiles) {
|
||||
|
||||
if (profile.isSelected) {
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSInteger) getIndexOfSelectedProfile {
|
||||
NSMutableArray *profiles = [self getAllProfiles];
|
||||
for (OSCProfile *profile in profiles) {
|
||||
|
||||
if (profile.isSelected == YES) {
|
||||
return [profiles indexOfObject:profile];
|
||||
}
|
||||
}
|
||||
return 0; // if none of the profiles in the array have their 'isSelected' property set to YES (which should not be possible) return the 'Default' profile as the 'selected' profile
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Setters
|
||||
|
||||
- (void) setProfileToSelected:(NSString *)name {
|
||||
NSMutableArray *profiles = [self getAllProfiles];
|
||||
|
||||
/* Iterate through each profile. If its name equals the value of the 'name' parameter passed into this method then set the profile's 'isSelected' property to YES, otherwise set the value to NO */
|
||||
for (OSCProfile *profile in profiles) {
|
||||
|
||||
if ([profile.name isEqualToString:name]) {
|
||||
profile.isSelected = YES;
|
||||
}
|
||||
else {
|
||||
profile.isSelected = NO;
|
||||
}
|
||||
}
|
||||
|
||||
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles]; // encode each 'profile' object in the array and add them to a new array
|
||||
|
||||
/* Encode the array itself, NOT the objects inside the array, which have already been encoded by this point */
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
|
||||
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
}
|
||||
|
||||
|
||||
- (bool) updateSelectedProfile:(NSMutableArray *) oscButtonLayers {
|
||||
NSMutableArray* buttonStatesEncoded = [self convertOnScreenControllerAndWidgetsToButtonStates:oscButtonLayers];
|
||||
if([self getIndexOfSelectedProfile]==0) return false;
|
||||
OSCProfile *newProfile = [[OSCProfile alloc] initWithName:[self getSelectedProfile].name
|
||||
buttonStates:buttonStatesEncoded isSelected:YES]; // create a new 'OSCProfile'. Set the array of encoded button states created above to the 'buttonStates' property of the new profile, along with a 'name'. Set 'isSelected' argument to YES which will set this saved profile as the one that will show up in the game stream view
|
||||
|
||||
|
||||
/* set all saved OSCProfiles 'isSelected' property to NO since the new profile you're adding will be set as the selected profile */
|
||||
NSMutableArray *profiles = [self getAllProfiles];
|
||||
for (OSCProfile *profile in profiles) {
|
||||
profile.isSelected = NO;
|
||||
}
|
||||
[self replaceProfile:[self getSelectedProfile] withProfile:newProfile];
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
- (void) saveProfileWithName:(NSString*)name andButtonLayers:(NSMutableArray *)oscButtonLayers {
|
||||
NSMutableArray* buttonStatesEncoded = [self convertOnScreenControllerAndWidgetsToButtonStates:oscButtonLayers];
|
||||
OSCProfile *newProfile = [[OSCProfile alloc] initWithName:name
|
||||
buttonStates:buttonStatesEncoded isSelected:YES]; // create a new 'OSCProfile'. Set the array of encoded button states created above to the 'buttonStates' property of the new profile, along with a 'name'. Set 'isSelected' argument to YES which will set this saved profile as the one that will show up in the game stream view
|
||||
/* set all saved OSCProfiles 'isSelected' property to NO since the new profile you're adding will be set as the selected profile */
|
||||
NSMutableArray *profiles = [self getAllProfiles];
|
||||
for (OSCProfile *profile in profiles) {
|
||||
|
||||
profile.isSelected = NO;
|
||||
}
|
||||
|
||||
if ([self profileNameAlreadyExist:name]) { // if a saved profile with the same 'name' already exists in persistent storage then overwrite it and save the change to persistent storage
|
||||
[self replaceProfile:[self OSCProfileWithName:name] withProfile:newProfile];
|
||||
}
|
||||
else { // otherwise encode then add the new profile to the end of the OSCProfiles array
|
||||
NSData *newProfileEncoded = [NSKeyedArchiver archivedDataWithRootObject:newProfile requiringSecureCoding:YES error:nil];
|
||||
NSMutableArray *profilesEncoded = [self encodedProfilesFromArray:profiles];
|
||||
[profilesEncoded addObject:newProfileEncoded];
|
||||
|
||||
/* Encode the 'profilesEncoded' array itself, NOT the objects in the 'profilesEncoded' array, all of which are already encoded by this point */
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded requiringSecureCoding:YES error:nil];
|
||||
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
|
||||
// saving test:
|
||||
/*
|
||||
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
|
||||
NSString *path = [documentsPath stringByAppendingPathComponent:@"newDefault.bin"];
|
||||
|
||||
[profiles writeToFile:path atomically:YES];
|
||||
NSLog(@"默认 Profile 数据已保存到: %@", path);
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
- (CGPoint)normalizeWidgetPosition:(CGPoint)position {
|
||||
if(position.x > 1.0 && position.y >1.0){
|
||||
position.x = position.x / streamViewBounds.size.width;
|
||||
position.y = position.y / streamViewBounds.size.height;
|
||||
}
|
||||
return position;
|
||||
}
|
||||
|
||||
- (CGPoint)denormalizeWidgetPosition:(CGPoint)position {
|
||||
if(position.x < 1.0 && position.y < 1.0){
|
||||
position.x = position.x * streamViewBounds.size.width;
|
||||
position.y = position.y * streamViewBounds.size.height;
|
||||
}
|
||||
return position;
|
||||
}
|
||||
|
||||
- (NSMutableArray* ) convertOnScreenControllerAndWidgetsToButtonStates:(NSMutableArray *) oscButtonLayers {
|
||||
/* iterate through each OSC button the user sees on screen, create an 'OnScreenButtonState' object from each button, encode each object, and then add each object to an array */
|
||||
/*
|
||||
NSSet *validPositionButtonNames = [NSSet setWithObjects:
|
||||
@"l2Button",
|
||||
@"l1Button",
|
||||
@"dPad",
|
||||
@"selectButton",
|
||||
@"leftStickBackground",
|
||||
@"rightStickBackground",
|
||||
@"r2Button",
|
||||
@"r1Button",
|
||||
@"aButton",
|
||||
@"bButton",
|
||||
@"xButton",
|
||||
@"yButton",
|
||||
@"startButton",
|
||||
nil]; */
|
||||
NSMutableArray *buttonStatesEncoded = [[NSMutableArray alloc] init];
|
||||
|
||||
// save on-screen game controller buttons & sticks as buttonstate:
|
||||
for (CALayer *oscButtonLayer in oscButtonLayers) {
|
||||
oscButtonLayer.position = [self normalizeWidgetPosition:oscButtonLayer.position];
|
||||
OnScreenButtonState *buttonState = [[OnScreenButtonState alloc] initWithButtonName:oscButtonLayer.name buttonType:LegacyOscButton andPosition:oscButtonLayer.position];
|
||||
oscButtonLayer.position = [self denormalizeWidgetPosition:oscButtonLayer.position];
|
||||
// add hidden attr here
|
||||
buttonState.isHidden = oscButtonLayer.isHidden;
|
||||
buttonState.oscLayerSizeFactor = [OnScreenControls getControllerLayerSizeFactor:oscButtonLayer];
|
||||
buttonState.backgroundAlpha = oscButtonLayer.opacity;
|
||||
|
||||
NSNumber *style = [OnScreenControls.layerVibrationStyleDic objectForKey:oscButtonLayer.name];
|
||||
if ([style isKindOfClass:[NSNumber class]]) {
|
||||
buttonState.vibrationStyle = [style unsignedCharValue];
|
||||
// 使用 val
|
||||
}
|
||||
else buttonState.vibrationStyle = UIImpactFeedbackStyleLight;
|
||||
|
||||
// NSLog(@"oscLayerName: %@, opacity: %f, ", oscButtonLayer.name, buttonState.backgroundAlpha);
|
||||
|
||||
|
||||
// buttonState.oscLayerSizeFactor = oscButtonLayer.bounds;
|
||||
|
||||
NSData *buttonStateEncoded = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil];
|
||||
[buttonStatesEncoded addObject: buttonStateEncoded];
|
||||
}
|
||||
|
||||
// save on-screen widget views (keyboard & mouse command) as buttonstate:
|
||||
for(OnScreenWidgetView* widgetView in OnScreenWidgetViews){
|
||||
|
||||
widgetView.center = [self normalizeWidgetPosition:widgetView.center];
|
||||
OnScreenButtonState *buttonState = [[OnScreenButtonState alloc] initWithButtonName:widgetView.buttonLabel buttonType:CustomOnScreenWidget andPosition:widgetView.center];
|
||||
widgetView.center = [self denormalizeWidgetPosition:widgetView.center];
|
||||
buttonState.alias = widgetView.buttonLabel;
|
||||
buttonState.widthFactor = widgetView.widthFactor;
|
||||
buttonState.heightFactor = widgetView.heightFactor;
|
||||
buttonState.backgroundAlpha = widgetView.backgroundAlpha;
|
||||
buttonState.borderWidth = widgetView.borderWidth;
|
||||
buttonState.vibrationStyle = widgetView.vibrationStyle;
|
||||
buttonState.mouseButtonAction = widgetView.mouseButtonAction;
|
||||
buttonState.sensitivityFactor = widgetView.sensitivityFactor;
|
||||
buttonState.stickIndicatorOffset = widgetView.stickIndicatorOffset;
|
||||
buttonState.widgetShape = widgetView.shape;
|
||||
buttonState.minStickOffset = widgetView.minStickOffset;
|
||||
|
||||
NSData *buttonStateEncoded = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil];
|
||||
[buttonStatesEncoded addObject: buttonStateEncoded];
|
||||
|
||||
}
|
||||
return buttonStatesEncoded;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#pragma mark - Queries
|
||||
|
||||
- (BOOL) profileNameAlreadyExist:(NSString*)name {
|
||||
NSMutableArray *profiles = [self getAllProfiles];
|
||||
/* Iterate through the decoded profiles and return 'YES' if one of the profiles' 'name' properties equals the 'name' passed into this method */
|
||||
for (OSCProfile *profile in profiles) {
|
||||
if ([profile.name isEqualToString:name]) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL) profileName:(NSString*) name alreadyExistIn:(NSMutableArray*)profiles {
|
||||
/* Iterate through the decoded profiles and return 'YES' if one of the profiles' 'name' properties equals the 'name' passed into this method */
|
||||
for (OSCProfile *profile in profiles) {
|
||||
if ([profile.name isEqualToString:name]) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (OnScreenButtonState *)unarchiveButtonStateEncoded:(NSData *)data {
|
||||
OnScreenButtonState* buttonState;
|
||||
buttonState = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithObjects:[NSString class], [OnScreenButtonState class], nil]
|
||||
fromData:data
|
||||
error:nil];
|
||||
return buttonState;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,66 +0,0 @@
|
||||
//
|
||||
// OnScreenButtonState.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Long Le on 10/20/22.
|
||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
// Modified by True砖家 since 2024.6.24
|
||||
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
This object is used to save positional and visibility information for any particular on screen virtual controller button.
|
||||
We are able to associate this 'OnScreenButtonState' object and its corresponding CALayer onscreen controller button by setting their 'name' properties equal; in our particular case we give them descriptive names such as 'aButton', 'upButton', 'leftStick', etc. By keeping references to the 19 on screen controller buttons (CALayers) in an array, and creating 19 'OnScreenButtonState' objects with names corresponding to these 19 CALayers and keeping them in an array, we can iterate through both arrays to find OSC buttons (CALayers) with the same name as one of the 'OnScreenButtonStateObjects' and then set the CALayer on screen control button's 'position' and 'hidden' property according to the value of the 'OnScreenButtonState' objects 'position' and 'isHidden' properties.
|
||||
Naturally we would like the user to be able to save their controller layout configurations so that they can load them between app launches, so we adopt encoding/decoding related protocols so we encode these 'OnScreenButtonState' objects and save them to NSUserDefaults
|
||||
*/
|
||||
|
||||
// OS Button state info obj to be modified
|
||||
@interface OnScreenButtonState : NSObject <NSCoding, NSSecureCoding>
|
||||
|
||||
@property NSString *name;
|
||||
@property NSString *alias;
|
||||
@property CGPoint position;
|
||||
@property (nonatomic, assign) BOOL isHidden;
|
||||
@property (nonatomic, assign) uint8_t buttonType;
|
||||
@property (nonatomic, assign) CGFloat widthFactor; // for OnScreenWidgetView
|
||||
@property (nonatomic, assign) CGFloat heightFactor; // for OnScreenWidgetView
|
||||
@property (nonatomic, assign) CGFloat borderWidth; // for OnScreenWidgetView
|
||||
@property (nonatomic, assign) CGFloat sensitivityFactor; // for OnScreenWidgetView
|
||||
@property (nonatomic, assign) CGFloat stickIndicatorOffset; // for OnScreenWidgetView
|
||||
@property (nonatomic, assign) CGFloat minStickOffset; // for OnScreenWidgetView
|
||||
@property NSString* widgetShape; // for OnScreenWidgetView
|
||||
|
||||
@property (nonatomic, assign) CGFloat oscLayerSizeFactor; // for OnScreenController CALayer
|
||||
@property (nonatomic, assign) CGFloat backgroundAlpha; // for OnScreenController CALayer
|
||||
@property (nonatomic, assign) uint8_t vibrationStyle; // for OnScreenController CALayer
|
||||
@property (nonatomic, assign) uint8_t mouseButtonAction; // for OnScreenController CALayer
|
||||
|
||||
// @property (nonatomic, assign) BOOL hasValidPosition;
|
||||
|
||||
typedef NS_ENUM(NSInteger, OnScreenButtonType) {
|
||||
LegacyOscButton,
|
||||
CustomOnScreenWidget
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, MouseButtonAction) {
|
||||
hovering,
|
||||
leftButtonDown,
|
||||
middleButtonDown,
|
||||
rightButtonDown,
|
||||
};
|
||||
|
||||
|
||||
- (id) initWithButtonName:(NSString*)name buttonType:(uint8_t)buttonType andPosition:(CGPoint)position;
|
||||
|
||||
+ (BOOL) supportsSecureCoding;
|
||||
- (void) encodeWithCoder:(NSCoder*)encoder;
|
||||
- (id) initWithCoder:(NSCoder*)decoder;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,73 +0,0 @@
|
||||
//
|
||||
// OnScreenButtonState.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Long Le on 10/20/22.
|
||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
// Modified by True砖家 since 2024.6.24
|
||||
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OnScreenButtonState.h"
|
||||
|
||||
@implementation OnScreenButtonState
|
||||
|
||||
|
||||
- (id) initWithButtonName:(NSString*)name buttonType:(uint8_t)buttonType andPosition:(CGPoint)position {
|
||||
if ((self = [self init])) {
|
||||
self.name = name;
|
||||
//self.isHidden = isHidden;
|
||||
self.position = position;
|
||||
self.buttonType = buttonType;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (BOOL) supportsSecureCoding {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void) encodeWithCoder:(NSCoder*)encoder {
|
||||
[encoder encodeObject:self.name forKey:@"name"];
|
||||
[encoder encodeObject:self.alias forKey:@"alias"];
|
||||
[encoder encodeInt:self.buttonType forKey:@"buttonType"];
|
||||
[encoder encodeInt:self.vibrationStyle forKey:@"vibrationStyle"];
|
||||
[encoder encodeInt:self.mouseButtonAction forKey:@"mouseButtonAction"];
|
||||
[encoder encodeCGPoint:self.position forKey:@"position"];
|
||||
[encoder encodeBool:self.isHidden forKey:@"isHidden"];
|
||||
[encoder encodeFloat:self.widthFactor forKey:@"widthFactor"];
|
||||
[encoder encodeFloat:self.heightFactor forKey:@"heightFactor"];
|
||||
[encoder encodeFloat:self.sensitivityFactor forKey:@"velocityFactor"];
|
||||
[encoder encodeFloat:self.stickIndicatorOffset forKey:@"stickIndicatorOffset"];
|
||||
[encoder encodeFloat:self.oscLayerSizeFactor forKey:@"oscLayerSizeFactor"];
|
||||
[encoder encodeFloat:self.backgroundAlpha forKey:@"backgroundAlpha"];
|
||||
[encoder encodeFloat:self.borderWidth forKey:@"borderWidth"];
|
||||
[encoder encodeObject:self.widgetShape forKey:@"widgetShape"];
|
||||
[encoder encodeFloat:self.minStickOffset forKey:@"minStickOffset"];
|
||||
}
|
||||
|
||||
- (id) initWithCoder:(NSCoder*)decoder {
|
||||
if (self = [super init]) {
|
||||
self.name = [decoder decodeObjectForKey:@"name"];
|
||||
self.alias = [decoder decodeObjectForKey:@"alias"];
|
||||
self.buttonType = [decoder decodeIntForKey:@"buttonType"];
|
||||
self.vibrationStyle = [decoder decodeIntForKey:@"vibrationStyle"];
|
||||
self.mouseButtonAction = [decoder decodeIntForKey:@"mouseButtonAction"];
|
||||
self.position = [decoder decodeCGPointForKey:@"position"];
|
||||
self.isHidden = [decoder decodeBoolForKey:@"isHidden"];
|
||||
self.widthFactor = [decoder decodeFloatForKey:@"widthFactor"];
|
||||
self.heightFactor = [decoder decodeFloatForKey:@"heightFactor"];
|
||||
self.sensitivityFactor = [decoder decodeFloatForKey:@"velocityFactor"];
|
||||
self.stickIndicatorOffset = [decoder decodeFloatForKey:@"stickIndicatorOffset"];
|
||||
self.oscLayerSizeFactor = [decoder decodeFloatForKey:@"oscLayerSizeFactor"];
|
||||
self.backgroundAlpha = [decoder decodeFloatForKey:@"backgroundAlpha"];
|
||||
self.borderWidth = [decoder decodeFloatForKey:@"borderWidth"];
|
||||
self.widgetShape = [decoder decodeObjectForKey:@"widgetShape"];
|
||||
self.minStickOffset = [decoder decodeFloatForKey:@"minStickOffset"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,28 +0,0 @@
|
||||
//
|
||||
// CustomTapGestureRecognizer.h
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2024/5/15.
|
||||
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef CustomTapGestureRecognizer_h
|
||||
#define CustomTapGestureRecognizer_h
|
||||
|
||||
|
||||
@interface CustomTapGestureRecognizer : UIGestureRecognizer{
|
||||
CGFloat lowestTouchPointYCoord;
|
||||
}
|
||||
|
||||
|
||||
@property (nonatomic, assign) uint8_t numberOfTouchesRequired;
|
||||
@property (nonatomic, assign) bool immediateTriggering; // if enabled, trigger the signal on touchesBegan stage.
|
||||
@property (nonatomic, assign) double tapDownTimeThreshold; // tap down threshold in seconds.
|
||||
@property (nonatomic, assign) bool isOnScreenControllerBeingPressed; // will be set by the onscreencontrol class
|
||||
//@property (nonatomic, assign) bool haveOnScreenButtonsOnStreamview;
|
||||
@property (nonatomic, readonly) CGFloat lowestTouchPointHeight;
|
||||
@property (nonatomic, readonly) bool gestureCaptured;
|
||||
@property (nonatomic, readonly) NSTimeInterval gestureCapturedTime;
|
||||
|
||||
@end
|
||||
#endif /* CustomTapGestureRecognizer_h */
|
||||
@@ -1,112 +0,0 @@
|
||||
//
|
||||
// CustomTapGestureRecognizer.m
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2024/5/15.
|
||||
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIGestureRecognizerSubclass.h>
|
||||
#import "CustomTapGestureRecognizer.h"
|
||||
#import "Moonlight-Swift.h"
|
||||
|
||||
// The most accurate & reliable tap gesture recognizer of iOS:
|
||||
// - Almost 100% recoginition rate. UITapGestureRecognizer of Apple API fails frequently, just a piece of crap.
|
||||
// - When immediateTriggering is set to false (for native multi-touch):
|
||||
// Gesture signal will be triggered on touchesEnded stage, multi finger touch operations will not be interrupted by the arising keyboard.
|
||||
// Instances of different [numberOfTouchesRequired] barely compete with each other, for example, the chance of 3-finger gesture get triggered by 4 or 5 finger tap is very small.
|
||||
// - Set property immediateTriggering to true, to ensure the priority of keyboard toggle in non-native touch mode, in compete with 2-finger gestures.
|
||||
// - This recognizer also provides properties like gestureCapturedTime, to be accessed outside the class for useful purpose.
|
||||
|
||||
@implementation CustomTapGestureRecognizer
|
||||
|
||||
static CGFloat screenHeightInPoints;
|
||||
static CGFloat screenWidthInPoints;
|
||||
|
||||
- (instancetype)initWithTarget:(nullable id)target action:(nullable SEL)action {
|
||||
self = [super initWithTarget:target action:action];
|
||||
screenHeightInPoints = CGRectGetHeight([[UIScreen mainScreen] bounds]);
|
||||
screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]);
|
||||
lowestTouchPointYCoord = 0.0;
|
||||
_numberOfTouchesRequired = 3;
|
||||
_immediateTriggering = false;
|
||||
_tapDownTimeThreshold = 0.3;
|
||||
_gestureCaptured = false;
|
||||
_isOnScreenControllerBeingPressed = false;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||||
// Check if the number of touches and taps meets the required criteria
|
||||
if ([[event allTouches] count] == _numberOfTouchesRequired) {
|
||||
_gestureCapturedTime = CACurrentMediaTime();
|
||||
_gestureCaptured = true;
|
||||
for(UITouch *touch in [event allTouches]){
|
||||
if(lowestTouchPointYCoord < [touch locationInView:self.view].y) lowestTouchPointYCoord = [touch locationInView:self.view].y;
|
||||
}
|
||||
|
||||
// this mechanism is deprecated.
|
||||
/* if(_numberOfTouchesRequired == 2){
|
||||
NSArray *twoTouches = [[event allTouches] allObjects];
|
||||
_areVirtualControllerTaps = fabs([twoTouches[1] locationInView:self.view].x - [twoTouches[0] locationInView:self.view].x) > screenWidthInPoints/3;
|
||||
} */
|
||||
|
||||
_lowestTouchPointHeight = screenHeightInPoints - lowestTouchPointYCoord;
|
||||
if(_immediateTriggering){
|
||||
lowestTouchPointYCoord = 0.0; //reset for next recoginition
|
||||
self.state = UIGestureRecognizerStateRecognized;
|
||||
return;
|
||||
}
|
||||
self.state = UIGestureRecognizerStatePossible;
|
||||
}
|
||||
if ([[event allTouches] count] > _numberOfTouchesRequired) {
|
||||
_gestureCaptured = false;
|
||||
self.state = UIGestureRecognizerStateFailed;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||||
// [super touchesEnded:touches withEvent:event];
|
||||
if(_immediateTriggering) return;
|
||||
uint8_t allTouchesCount = [[event allTouches] count];
|
||||
if(allTouchesCount > _numberOfTouchesRequired) {
|
||||
_gestureCaptured = false;
|
||||
self.state = UIGestureRecognizerStateFailed;
|
||||
}
|
||||
else if(_gestureCaptured && allTouchesCount == [touches count] && !_isOnScreenControllerBeingPressed && ![self isOnScreenWidgetViewBeingPressed]){ //must exclude virtual controller & onscreen button taps here to prevent stucked button, _areVirtualControllerTaps flag is set by onscreencontrols, containOnScreenButtonsTaps will be returned by iterating all widget views in streamframeview
|
||||
_gestureCaptured = false; //reset for next recognition
|
||||
if((CACurrentMediaTime() - _gestureCapturedTime) < _tapDownTimeThreshold){
|
||||
lowestTouchPointYCoord = 0.0; //reset for next recognition
|
||||
self.state = UIGestureRecognizerStateRecognized;
|
||||
}
|
||||
}
|
||||
if (allTouchesCount == [touches count]) _isOnScreenControllerBeingPressed = false; // need to reset this flag anyway, when all fingers are lefting
|
||||
}
|
||||
|
||||
|
||||
// it was a not a perfect choice to code OnScreenWidgetView in Swift...
|
||||
// we're unable to import this class to swift codebase by the bridging header,and have to exlucde the onscreen button taps here
|
||||
// by iterating every widget view instances. but it's basically ok since the number of widgetViews are always limited.
|
||||
// and this method will only be called when the recognizer is active & and the taps passes all the checks and is about to ge triggered.
|
||||
- (bool)isOnScreenWidgetViewBeingPressed {
|
||||
NSTimeInterval allFingersTapDownTime = CACurrentMediaTime() - _gestureCapturedTime; //RIGHTCLICK_TAP_DOWN_TIME_THRESHOLD_S has been passed to this recognizer as _tapDownTimeShreshold, we'll decide how to deal with pressed flag of on-screen widget views based on tapDownTime
|
||||
bool gotOneWidgetPressed = false;
|
||||
// the tap gestures are all added to the streamFrameTopLayerView, where all the OnScreenWidgetViews are added. so we can iterate them in this way:
|
||||
for(UIView* view in self.view.subviews){
|
||||
if ([view isKindOfClass:[OnScreenWidgetView class]]) {
|
||||
OnScreenWidgetView* widgetView = (OnScreenWidgetView*) view;
|
||||
if(gotOneWidgetPressed && allFingersTapDownTime <= _tapDownTimeThreshold){ // once we have just 1 button pressed already,& the tapDownTime has not exceeded the threshold, we'll reset pressed flag for the all the widgetViews.
|
||||
widgetView.pressed = false; //reset the flag for widgetView
|
||||
continue;
|
||||
}
|
||||
if(widgetView.pressed){
|
||||
gotOneWidgetPressed = true; //got one button pressed
|
||||
if(allFingersTapDownTime <= _tapDownTimeThreshold) widgetView.pressed = false; // reset the flag for current widgetView if tapDowntime is still within the threshold, if not, leave the flag for mouse scroller gesture in relative touch mode.
|
||||
}
|
||||
}
|
||||
}
|
||||
return gotOneWidgetPressed;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,22 +0,0 @@
|
||||
//
|
||||
// HapticContext.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 9/17/20.
|
||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
@import CoreHaptics;
|
||||
@import GameController;
|
||||
|
||||
@interface HapticContext : NSObject
|
||||
|
||||
-(void)setMotorAmplitude:(unsigned short)amplitude;
|
||||
-(void)cleanup;
|
||||
|
||||
+(HapticContext*) createContextForHighFreqMotor:(GCController*)gamepad;
|
||||
+(HapticContext*) createContextForLowFreqMotor:(GCController*)gamepad;
|
||||
+(HapticContext*) createContextForLeftTrigger:(GCController*)gamepad;
|
||||
+(HapticContext*) createContextForRightTrigger:(GCController*)gamepad;
|
||||
|
||||
@end
|
||||
@@ -1,170 +0,0 @@
|
||||
//
|
||||
// HapticContext.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 9/17/20.
|
||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HapticContext.h"
|
||||
|
||||
@import CoreHaptics;
|
||||
@import GameController;
|
||||
|
||||
@implementation HapticContext {
|
||||
GCControllerPlayerIndex _playerIndex;
|
||||
CHHapticEngine* _hapticEngine API_AVAILABLE(ios(13.0), tvos(14.0));
|
||||
id<CHHapticPatternPlayer> _hapticPlayer API_AVAILABLE(ios(13.0), tvos(14.0));
|
||||
BOOL _playing;
|
||||
}
|
||||
|
||||
-(void)cleanup API_AVAILABLE(ios(14.0), tvos(14.0)) {
|
||||
if (_hapticPlayer != nil) {
|
||||
[_hapticPlayer cancelAndReturnError:nil];
|
||||
_hapticPlayer = nil;
|
||||
}
|
||||
if (_hapticEngine != nil) {
|
||||
[_hapticEngine stopWithCompletionHandler:nil];
|
||||
_hapticEngine = nil;
|
||||
}
|
||||
}
|
||||
|
||||
-(void)setMotorAmplitude:(unsigned short)amplitude API_AVAILABLE(ios(14.0), tvos(14.0)) {
|
||||
NSError* error;
|
||||
|
||||
// Check if the haptic engine died
|
||||
if (_hapticEngine == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop the effect entirely if the amplitude is 0
|
||||
if (amplitude == 0) {
|
||||
if (_playing) {
|
||||
[_hapticPlayer stopAtTime:0 error:&error];
|
||||
_playing = NO;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_hapticPlayer == nil) {
|
||||
// We must initialize the intensity to 1.0f because the dynamic parameters are multiplied by this value before being applied
|
||||
CHHapticEventParameter* intensityParameter = [[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticIntensity value:1.0f];
|
||||
CHHapticEvent* hapticEvent = [[CHHapticEvent alloc] initWithEventType:CHHapticEventTypeHapticContinuous parameters:[NSArray arrayWithObject:intensityParameter] relativeTime:0 duration:GCHapticDurationInfinite];
|
||||
CHHapticPattern* hapticPattern = [[CHHapticPattern alloc] initWithEvents:[NSArray arrayWithObject:hapticEvent] parameters:[[NSArray alloc] init] error:&error];
|
||||
if (error != nil) {
|
||||
Log(LOG_W, @"Controller %d: Haptic pattern creation failed: %@", _playerIndex, error);
|
||||
return;
|
||||
}
|
||||
|
||||
_hapticPlayer = [_hapticEngine createPlayerWithPattern:hapticPattern error:&error];
|
||||
if (error != nil) {
|
||||
Log(LOG_W, @"Controller %d: Haptic player creation failed: %@", _playerIndex, error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CHHapticDynamicParameter* intensityParameter = [[CHHapticDynamicParameter alloc] initWithParameterID:CHHapticDynamicParameterIDHapticIntensityControl value:amplitude / 65535.0f relativeTime:0];
|
||||
[_hapticPlayer sendParameters:[NSArray arrayWithObject:intensityParameter] atTime:CHHapticTimeImmediate error:&error];
|
||||
if (error != nil) {
|
||||
Log(LOG_W, @"Controller %d: Haptic player parameter update failed: %@", _playerIndex, error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_playing) {
|
||||
[_hapticPlayer startAtTime:0 error:&error];
|
||||
if (error != nil) {
|
||||
_hapticPlayer = nil;
|
||||
Log(LOG_W, @"Controller %d: Haptic playback start failed: %@", _playerIndex, error);
|
||||
return;
|
||||
}
|
||||
|
||||
_playing = YES;
|
||||
}
|
||||
}
|
||||
|
||||
-(id) initWithGamepad:(GCController*)gamepad locality:(GCHapticsLocality)locality API_AVAILABLE(ios(14.0), tvos(14.0)) {
|
||||
if (gamepad.haptics == nil) {
|
||||
Log(LOG_W, @"Controller %d does not support haptics", gamepad.playerIndex);
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![[gamepad.haptics supportedLocalities] containsObject:locality]) {
|
||||
Log(LOG_W, @"Controller %d does not support haptic locality: %@", gamepad.playerIndex, locality);
|
||||
return nil;
|
||||
}
|
||||
|
||||
_playerIndex = gamepad.playerIndex;
|
||||
_hapticEngine = [gamepad.haptics createEngineWithLocality:locality];
|
||||
|
||||
NSError* error;
|
||||
[_hapticEngine startAndReturnError:&error];
|
||||
if (error != nil) {
|
||||
Log(LOG_W, @"Controller %d: Haptic engine failed to start: %@", gamepad.playerIndex, error);
|
||||
return nil;
|
||||
}
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
_hapticEngine.stoppedHandler = ^(CHHapticEngineStoppedReason stoppedReason) {
|
||||
HapticContext* me = weakSelf;
|
||||
if (me == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log(LOG_W, @"Controller %d: Haptic engine stopped: %p", me->_playerIndex, stoppedReason);
|
||||
me->_hapticPlayer = nil;
|
||||
me->_hapticEngine = nil;
|
||||
me->_playing = NO;
|
||||
};
|
||||
_hapticEngine.resetHandler = ^{
|
||||
HapticContext* me = weakSelf;
|
||||
if (me == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log(LOG_W, @"Controller %d: Haptic engine reset", me->_playerIndex);
|
||||
me->_hapticPlayer = nil;
|
||||
me->_playing = NO;
|
||||
[me->_hapticEngine startAndReturnError:nil];
|
||||
};
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+(HapticContext*) createContextForHighFreqMotor:(GCController*)gamepad {
|
||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightHandle];
|
||||
}
|
||||
else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
+(HapticContext*) createContextForLowFreqMotor:(GCController*)gamepad {
|
||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityLeftHandle];
|
||||
}
|
||||
else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
+(HapticContext*) createContextForLeftTrigger:(GCController*)gamepad {
|
||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityLeftTrigger];
|
||||
}
|
||||
else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
+(HapticContext*) createContextForRightTrigger:(GCController*)gamepad {
|
||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightTrigger];
|
||||
}
|
||||
else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// KeyboardInputField.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 12/2/22.
|
||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface KeyboardInputField : UITextField
|
||||
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// KeyboardInputField.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 12/2/22.
|
||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import "KeyboardInputField.h"
|
||||
|
||||
@implementation KeyboardInputField
|
||||
|
||||
- (UIEditingInteractionConfiguration) editingInteractionConfiguration {
|
||||
// Suppress the Undo menu that appears with a 3 finger tap
|
||||
return UIEditingInteractionConfigurationNone;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,23 +0,0 @@
|
||||
//
|
||||
// KeyboardSupport.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 8/25/18.
|
||||
// Copyright © 2018 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface KeyboardSupport : NSObject
|
||||
|
||||
struct KeyEvent {
|
||||
u_short keycode;
|
||||
u_short modifierKeycode;
|
||||
u_char modifier;
|
||||
};
|
||||
|
||||
+ (BOOL)sendKeyEventForPress:(UIPress*)press down:(BOOL)down API_AVAILABLE(ios(13.4));
|
||||
+ (BOOL)sendKeyEvent:(UIKey*)key down:(BOOL)down API_AVAILABLE(ios(13.4));
|
||||
+ (struct KeyEvent) translateKeyEvent:(unichar) inputChar withModifierFlags:(UIKeyModifierFlags)modifierFlags;
|
||||
|
||||
@end
|
||||
@@ -1,454 +0,0 @@
|
||||
//
|
||||
// KeyboardSupport.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 8/25/18.
|
||||
// Copyright © 2018 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import "KeyboardSupport.h"
|
||||
#include <Limelight.h>
|
||||
|
||||
@implementation KeyboardSupport
|
||||
|
||||
+ (BOOL)sendKeyEventForPress:(UIPress*)press down:(BOOL)down API_AVAILABLE(ios(13.4)) {
|
||||
if (press.key != nil) {
|
||||
return [KeyboardSupport sendKeyEvent:press.key down:down];
|
||||
}
|
||||
else {
|
||||
short keyCode;
|
||||
|
||||
switch (press.type) {
|
||||
case UIPressTypeUpArrow:
|
||||
keyCode = 0x26;
|
||||
break;
|
||||
case UIPressTypeDownArrow:
|
||||
keyCode = 0x28;
|
||||
break;
|
||||
case UIPressTypeLeftArrow:
|
||||
keyCode = 0x25;
|
||||
break;
|
||||
case UIPressTypeRightArrow:
|
||||
keyCode = 0x27;
|
||||
break;
|
||||
default:
|
||||
// Unhandled press type
|
||||
return NO;
|
||||
}
|
||||
|
||||
LiSendKeyboardEvent(0x8000 | keyCode,
|
||||
down ? KEY_ACTION_DOWN : KEY_ACTION_UP,
|
||||
0);
|
||||
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL)sendKeyEvent:(UIKey*)key down:(BOOL)down API_AVAILABLE(ios(13.4)) {
|
||||
char modifierFlags = 0;
|
||||
short keyCode = 0;
|
||||
|
||||
if (key.modifierFlags & UIKeyModifierShift) {
|
||||
modifierFlags |= MODIFIER_SHIFT;
|
||||
}
|
||||
if (key.modifierFlags & UIKeyModifierAlternate) {
|
||||
modifierFlags |= MODIFIER_ALT;
|
||||
}
|
||||
if (key.modifierFlags & UIKeyModifierControl) {
|
||||
modifierFlags |= MODIFIER_CTRL;
|
||||
}
|
||||
if (key.modifierFlags & UIKeyModifierCommand) {
|
||||
modifierFlags |= MODIFIER_META;
|
||||
}
|
||||
|
||||
// This converts UIKeyboardHIDUsage values to Win32 VK_* values
|
||||
// https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
|
||||
if (key.keyCode >= UIKeyboardHIDUsageKeyboardA &&
|
||||
key.keyCode <= UIKeyboardHIDUsageKeyboardZ) {
|
||||
keyCode = (key.keyCode - UIKeyboardHIDUsageKeyboardA) + 0x41;
|
||||
}
|
||||
else if (key.keyCode == UIKeyboardHIDUsageKeyboard0) {
|
||||
// This key is at the beginning of the VK_ range but the end
|
||||
// of the UIKeyboardHIDUsageKeyboard range.
|
||||
keyCode = 0x30;
|
||||
}
|
||||
else if (key.keyCode >= UIKeyboardHIDUsageKeyboard1 &&
|
||||
key.keyCode <= UIKeyboardHIDUsageKeyboard9) {
|
||||
keyCode = (key.keyCode - UIKeyboardHIDUsageKeyboard1) + 0x31;
|
||||
}
|
||||
else if (key.keyCode == UIKeyboardHIDUsageKeypad0) {
|
||||
// This key is at the beginning of the VK_ range but the end
|
||||
// of the UIKeyboardHIDUsageKeypad range.
|
||||
keyCode = 0x60;
|
||||
}
|
||||
else if (key.keyCode >= UIKeyboardHIDUsageKeypad1 &&
|
||||
key.keyCode <= UIKeyboardHIDUsageKeypad9) {
|
||||
keyCode = (key.keyCode - UIKeyboardHIDUsageKeypad1) + 0x61;
|
||||
}
|
||||
else if (key.keyCode >= UIKeyboardHIDUsageKeyboardF1 &&
|
||||
key.keyCode <= UIKeyboardHIDUsageKeyboardF12) {
|
||||
keyCode = (key.keyCode - UIKeyboardHIDUsageKeyboardF1) + 0x70;
|
||||
}
|
||||
else if (key.keyCode >= UIKeyboardHIDUsageKeyboardF13 &&
|
||||
key.keyCode <= UIKeyboardHIDUsageKeyboardF24) {
|
||||
keyCode = (key.keyCode - UIKeyboardHIDUsageKeyboardF13) + 0x7C;
|
||||
}
|
||||
else {
|
||||
switch (key.keyCode) {
|
||||
case UIKeyboardHIDUsageKeyboardReturnOrEnter:
|
||||
keyCode = 0x0D;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardEscape:
|
||||
keyCode = 0x1B;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardDeleteOrBackspace:
|
||||
keyCode = 0x08;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardTab:
|
||||
keyCode = 0x09;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardSpacebar:
|
||||
keyCode = 0x20;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardHyphen:
|
||||
keyCode = 0xBD;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardEqualSign:
|
||||
keyCode = 0xBB;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardOpenBracket:
|
||||
keyCode = 0xDB;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardCloseBracket:
|
||||
keyCode = 0xDD;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardBackslash:
|
||||
keyCode = 0xDC;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardSemicolon:
|
||||
keyCode = 0xBA;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardQuote:
|
||||
keyCode = 0xDE;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardGraveAccentAndTilde:
|
||||
keyCode = 0xC0;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardComma:
|
||||
keyCode = 0xBC;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardPeriod:
|
||||
keyCode = 0xBE;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardSlash:
|
||||
keyCode = 0xBF;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardCapsLock:
|
||||
keyCode = 0x14;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardPrintScreen:
|
||||
keyCode = 0x2A;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardScrollLock:
|
||||
keyCode = 0x91;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardPause:
|
||||
keyCode = 0x13;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardInsert:
|
||||
keyCode = 0x2D;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardHome:
|
||||
keyCode = 0x24;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardPageUp:
|
||||
keyCode = 0x21;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardDeleteForward:
|
||||
keyCode = 0x2E;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardEnd:
|
||||
keyCode = 0x23;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardPageDown:
|
||||
keyCode = 0x22;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardRightArrow:
|
||||
keyCode = 0x27;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardLeftArrow:
|
||||
keyCode = 0x25;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardDownArrow:
|
||||
keyCode = 0x28;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardUpArrow:
|
||||
keyCode = 0x26;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeypadNumLock:
|
||||
keyCode = 0x90;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeypadSlash:
|
||||
keyCode = 0x6F;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeypadAsterisk:
|
||||
keyCode = 0x6A;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeypadHyphen:
|
||||
keyCode = 0x6D;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeypadPlus:
|
||||
keyCode = 0x6B;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeypadEnter:
|
||||
keyCode = 0x0D;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeypadPeriod:
|
||||
keyCode = 0x6E;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardNonUSBackslash:
|
||||
keyCode = 0xE2;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeypadComma:
|
||||
keyCode = 0x6C;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardCancel:
|
||||
keyCode = 0x03;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardClear:
|
||||
keyCode = 0x0C;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardCrSelOrProps:
|
||||
keyCode = 0xF7;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardExSel:
|
||||
keyCode = 0xF8;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardLeftGUI:
|
||||
keyCode = 0x5B;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardLeftControl:
|
||||
keyCode = 0xA2;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardLeftShift:
|
||||
keyCode = 0xA0;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardLeftAlt:
|
||||
keyCode = 0xA4;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardRightGUI:
|
||||
keyCode = 0x5C;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardRightControl:
|
||||
keyCode = 0xA3;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardRightShift:
|
||||
keyCode = 0xA1;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardRightAlt:
|
||||
keyCode = 0xA5;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardErrorUndefined:
|
||||
NSLog(@"Undefined key pressed");
|
||||
return false;
|
||||
case 669: // This value corresponds to the "Globe" or "Language" key on most Apple branded iPad keyboards.
|
||||
keyCode = 0x1B; // This value corresponds to "Escape", which is missing from most Apple branded iPad keyboards.
|
||||
break;
|
||||
default:
|
||||
NSLog(@"Unhandled HID usage: %lu", (unsigned long)key.keyCode);
|
||||
assert(0);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
LiSendKeyboardEvent(0x8000 | keyCode,
|
||||
down ? KEY_ACTION_DOWN : KEY_ACTION_UP,
|
||||
modifierFlags);
|
||||
return true;
|
||||
}
|
||||
|
||||
+ (struct KeyEvent)translateKeyEvent:(unichar)inputChar withModifierFlags:(UIKeyModifierFlags)modifierFlags {
|
||||
struct KeyEvent event;
|
||||
event.keycode = 0;
|
||||
event.modifier = 0;
|
||||
event.modifierKeycode = 0;
|
||||
|
||||
switch (modifierFlags) {
|
||||
case UIKeyModifierAlphaShift:
|
||||
case UIKeyModifierShift:
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case UIKeyModifierControl:
|
||||
[KeyboardSupport addControlModifier:&event];
|
||||
break;
|
||||
case UIKeyModifierCommand:
|
||||
[KeyboardSupport addMetaModifier:&event];
|
||||
break;
|
||||
case UIKeyModifierAlternate:
|
||||
[KeyboardSupport addAltModifier:&event];
|
||||
break;
|
||||
case UIKeyModifierNumericPad:
|
||||
break;
|
||||
}
|
||||
if (inputChar >= 0x30 && inputChar <= 0x39) {
|
||||
// Numbers 0-9
|
||||
event.keycode = inputChar;
|
||||
} else if (inputChar >= 0x41 && inputChar <= 0x5A) {
|
||||
// Capital letters
|
||||
event.keycode = inputChar;
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
} else if (inputChar >= 0x61 && inputChar <= 0x7A) {
|
||||
// Lower case letters
|
||||
event.keycode = inputChar - (0x61 - 0x41);
|
||||
} switch (inputChar) {
|
||||
case ' ': // Spacebar
|
||||
event.keycode = 0x20;
|
||||
break;
|
||||
case '-': // Hyphen '-'
|
||||
event.keycode = 0xBD;
|
||||
break;
|
||||
case '/': // Forward slash '/'
|
||||
event.keycode = 0xBF;
|
||||
break;
|
||||
case ':': // Colon ':'
|
||||
event.keycode = 0xBA;
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case ';': // Semi-colon ';'
|
||||
event.keycode = 0xBA;
|
||||
break;
|
||||
case '(': // Open parenthesis '('
|
||||
event.keycode = 0x39; // '9'
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case ')': // Close parenthesis ')'
|
||||
event.keycode = 0x30; // '0'
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '$': // Dollar sign '$'
|
||||
event.keycode = 0x34; // '4'
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '&': // Ampresand '&'
|
||||
event.keycode = 0x37; // '7'
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '@': // At-sign '@'
|
||||
event.keycode = 0x32; // '2'
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '"':
|
||||
event.keycode = 0xDE;
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '\'':
|
||||
event.keycode = 0xDE;
|
||||
break;
|
||||
case '!':
|
||||
event.keycode = 0x31; // '1'
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '?':
|
||||
event.keycode = 0xBF; // '/'
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case ',':
|
||||
event.keycode = 0xBC;
|
||||
break;
|
||||
case '<':
|
||||
event.keycode = 0xBC;
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '.':
|
||||
event.keycode = 0xBE;
|
||||
break;
|
||||
case '>':
|
||||
event.keycode = 0xBE;
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '[':
|
||||
event.keycode = 0xDB;
|
||||
break;
|
||||
case ']':
|
||||
event.keycode = 0xDD;
|
||||
break;
|
||||
case '{':
|
||||
event.keycode = 0xDB;
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '}':
|
||||
event.keycode = 0xDD;
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '#':
|
||||
event.keycode = 0x33; // '3'
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '%':
|
||||
event.keycode = 0x35; // '5'
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '^':
|
||||
event.keycode = 0x36; // '6'
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '*':
|
||||
event.keycode = 0x38; // '8'
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '+':
|
||||
event.keycode = 0xBB;
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '=':
|
||||
event.keycode = 0xBB;
|
||||
break;
|
||||
case '_':
|
||||
event.keycode = 0xBD;
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '\\':
|
||||
event.keycode = 0xDC;
|
||||
break;
|
||||
case '|':
|
||||
event.keycode = 0xDC;
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '~':
|
||||
event.keycode = 0xC0;
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '`':
|
||||
event.keycode = 0xC0;
|
||||
break;
|
||||
case '\t':
|
||||
event.keycode = 0x09;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
+ (void) addShiftModifier:(struct KeyEvent*)event {
|
||||
event->modifier = MODIFIER_SHIFT;
|
||||
event->modifierKeycode = 0x10;
|
||||
}
|
||||
|
||||
+ (void) addControlModifier:(struct KeyEvent*)event {
|
||||
event->modifier = MODIFIER_CTRL;
|
||||
event->modifierKeycode = 0x11;
|
||||
}
|
||||
|
||||
+ (void) addMetaModifier:(struct KeyEvent*)event {
|
||||
event->modifier = MODIFIER_META;
|
||||
event->modifierKeycode = 0x5B;
|
||||
}
|
||||
|
||||
+ (void) addAltModifier:(struct KeyEvent*)event {
|
||||
event->modifier = MODIFIER_ALT;
|
||||
event->modifierKeycode = 0x12;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,25 +0,0 @@
|
||||
//
|
||||
// NativeTouchHandler.h
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2024/6/1.
|
||||
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import "StreamView.h"
|
||||
#import "DataManager.h"
|
||||
#include <Limelight.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface NativeTouchHandler : UIResponder
|
||||
@property (nonatomic, assign) NSMutableSet<UITouch *>* touchesCapturedByOnScreenButtons;
|
||||
|
||||
|
||||
- (id)initWithView:(StreamView* )view andSettings:(TemporarySettings*)settings;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,315 +0,0 @@
|
||||
//
|
||||
// NativeTouchHandler.m
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2024/6/1.
|
||||
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "NativeTouchHandler.h"
|
||||
#import "NativeTouchPointer.h"
|
||||
#import "OnScreenControls.h"
|
||||
#import "StreamView.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
|
||||
@implementation NativeTouchHandler {
|
||||
StreamView* streamView;
|
||||
TemporarySettings* currentSettings;
|
||||
bool activateCoordSelector;
|
||||
bool touchPointSpawnedAtUpperScreenEdge;
|
||||
CGFloat pointerVelocityDividerLocationByPoints;
|
||||
uint16_t touchMoveEventIntervalUs;
|
||||
|
||||
bool asyncNativeTouch;
|
||||
unsigned int touchDownQos;
|
||||
unsigned int touchMoveQos;
|
||||
unsigned int touchEndQos;
|
||||
|
||||
// Use a Dictionary to store UITouch object's memory address as key, and pointerId as value,字典存放UITouch对象地址和pointerId映射关系
|
||||
// pointerId will be generated from a pre-defined pool
|
||||
// Use a NSSet store active pointerId,
|
||||
NSMutableDictionary *pointerIdDict; //pointerId Dict for active touches.
|
||||
NSMutableSet<NSNumber *> *activePointerIds; //pointerId Set for active touches.
|
||||
NSMutableSet<NSNumber *> *pointerIdPool; //pre-defined pool of pointerIds.
|
||||
NSMutableSet<NSNumber *> *unassignedPointerIds;
|
||||
|
||||
NSMutableDictionary *pointerObjDict;
|
||||
|
||||
CGFloat slideGestureVerticalThreshold;
|
||||
CGFloat screenWidthWithThreshold;
|
||||
CGFloat EDGE_TOLERANCE;
|
||||
|
||||
CGRect streamViewBounds;
|
||||
}
|
||||
|
||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings{
|
||||
self = [super init];
|
||||
self->streamView = view;
|
||||
self->currentSettings = settings;
|
||||
self->activateCoordSelector = currentSettings.pointerVelocityModeDivider.floatValue != 1.0;
|
||||
self->touchMoveEventIntervalUs = currentSettings.touchMoveEventInterval.intValue;
|
||||
self->streamViewBounds = view.bounds;
|
||||
|
||||
self->pointerIdDict = [NSMutableDictionary dictionary];
|
||||
self->pointerIdPool = [NSMutableSet set];
|
||||
for (uint8_t i = 0; i <= 10; i++) { //ipadOS supports upto 11 finger touches
|
||||
[self->pointerIdPool addObject:@(i)];
|
||||
}
|
||||
self->activePointerIds = [NSMutableSet set];
|
||||
// self->excludedPointerIds = [[NSMutableSet alloc] init];
|
||||
self->touchPointSpawnedAtUpperScreenEdge = false;
|
||||
|
||||
self->asyncNativeTouch = settings.asyncNativeTouchPriority.intValue != AsyncNativeTouchOff;
|
||||
|
||||
switch(settings.asyncNativeTouchPriority.intValue){
|
||||
case TouchDownPriority: // deprecated by GUI
|
||||
touchDownQos = QOS_CLASS_USER_INTERACTIVE;
|
||||
touchMoveQos = QOS_CLASS_USER_INITIATED;
|
||||
touchEndQos = QOS_CLASS_USER_INITIATED;
|
||||
break;
|
||||
case TouchMovePriority: // deprecated by GUI
|
||||
touchDownQos = QOS_CLASS_USER_INITIATED;
|
||||
touchMoveQos = QOS_CLASS_USER_INTERACTIVE;
|
||||
touchEndQos = QOS_CLASS_USER_INITIATED;
|
||||
break;
|
||||
case EqualPriority: // equals to async touch is true now
|
||||
touchDownQos = QOS_CLASS_USER_INTERACTIVE;
|
||||
touchMoveQos = QOS_CLASS_USER_INTERACTIVE;
|
||||
touchEndQos = QOS_CLASS_USER_INITIATED;
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
|
||||
self->pointerObjDict = [NSMutableDictionary dictionary];
|
||||
|
||||
EDGE_TOLERANCE = 15.0;
|
||||
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
||||
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - EDGE_TOLERANCE;
|
||||
|
||||
self->pointerVelocityDividerLocationByPoints = self->streamView.bounds.size.width * settings.pointerVelocityModeDivider.floatValue;
|
||||
|
||||
[NativeTouchPointer initContextWithView:self->streamView andSettings:settings];
|
||||
//_touchesCapturedByOnScreenButtons = [[NSMutableSet alloc] init];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (CGSize) getVideoAreaSize {
|
||||
if (self->streamViewBounds.size.width > self->streamViewBounds.size.height * streamView.streamAspectRatio) {
|
||||
return CGSizeMake(self->streamViewBounds.size.height * streamView.streamAspectRatio, self->streamViewBounds.size.height);
|
||||
} else {
|
||||
return CGSizeMake(self->streamViewBounds.size.width, self->streamViewBounds.size.width / streamView.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->streamViewBounds.origin.x;
|
||||
float y = point.y - self->streamViewBounds.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->streamViewBounds.size.width / 2) {
|
||||
x--;
|
||||
}
|
||||
else {
|
||||
x++;
|
||||
}
|
||||
if (y < self->streamViewBounds.size.height / 2) {
|
||||
y--;
|
||||
}
|
||||
else {
|
||||
y++;
|
||||
}
|
||||
|
||||
// This logic mimics what iOS does with AVLayerVideoGravityResizeAspect
|
||||
CGSize videoSize = [self getVideoAreaSize];
|
||||
CGPoint videoOrigin = CGPointMake(self->streamViewBounds.size.width / 2 - videoSize.width / 2,
|
||||
self->streamViewBounds.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);
|
||||
}
|
||||
|
||||
|
||||
- (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;
|
||||
}
|
||||
|
||||
// generate & populate pointerId into NSDict & NSSet, called in touchesBegan
|
||||
- (void)handleTouchDown:(UITouch*)touch{
|
||||
//populate pointerId
|
||||
uintptr_t memAddrValue = (uintptr_t)touch;
|
||||
unassignedPointerIds = [pointerIdPool mutableCopy]; //reset unassignedPointerIds
|
||||
[unassignedPointerIds minusSet:activePointerIds];
|
||||
uint8_t pointerId = [[unassignedPointerIds anyObject] unsignedIntValue];
|
||||
[pointerIdDict setObject:@(pointerId) forKey:@(memAddrValue)];
|
||||
[activePointerIds addObject:@(pointerId)];
|
||||
|
||||
//check if touch point is spawned on the left or right upper half screen edges, event to remote PC. this is for better handling in-stream slide gesture
|
||||
CGPoint initialPoint = [touch locationInView:self->streamView];
|
||||
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < EDGE_TOLERANCE || initialPoint.x > screenWidthWithThreshold)) {
|
||||
self->touchPointSpawnedAtUpperScreenEdge = true;
|
||||
// [excludedPointerIds addObject:@(pointerId)];
|
||||
}
|
||||
}
|
||||
|
||||
// remove pointerId in touchesEnded or touchesCancelled
|
||||
- (void)removePointerId:(UITouch*)touch{
|
||||
uintptr_t memAddrValue = (uintptr_t)touch;
|
||||
NSNumber* pointerIdObj = [pointerIdDict objectForKey:@(memAddrValue)];
|
||||
if(pointerIdObj != nil){
|
||||
[activePointerIds removeObject:pointerIdObj];
|
||||
[pointerIdDict removeObjectForKey:@(memAddrValue)];
|
||||
// if([excludedPointerIds containsObject:pointerIdObj]) [excludedPointerIds removeObject:pointerIdObj]; // remove pointer id from excludedPointerId NSSet
|
||||
}
|
||||
}
|
||||
|
||||
// 从字典中获取UITouch事件对应的pointerId
|
||||
// called in method of sendTouchEvent
|
||||
- (uint8_t) retrievePointerIdFromDict:(UITouch*)touch{
|
||||
return [[pointerIdDict objectForKey:@((uintptr_t)touch)] unsignedIntValue];
|
||||
}
|
||||
|
||||
|
||||
- (void)sendTouchEvent:(UITouch*)touch withTouchtype:(uint8_t)touchType{
|
||||
if(touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC. and this must be checked after coord selector finishes populating new relative coords, or the app will crash!
|
||||
|
||||
CGPoint targetCoords;
|
||||
//NSLog(@"selecting coords: %d", touch.phase == UITouchPhaseMoved);
|
||||
// NSLog(@"excluded count: %d", (uint32_t)[excludedPointerIds count]);
|
||||
|
||||
targetCoords = activateCoordSelector ? [self selectCoordsFor:touch] : [touch locationInView:streamView];
|
||||
|
||||
CGPoint location = [self adjustCoordinatesForVideoArea:targetCoords];
|
||||
CGSize videoSize = [self getVideoAreaSize];
|
||||
CGFloat normalizedX = location.x / videoSize.width;
|
||||
CGFloat normalizedY = location.y / videoSize.height;
|
||||
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
|
||||
|
||||
if([self getPointerObjFromDict:touch].needResetCoords){ // access whether the current pointer has reached the boundary, and need a coord reset.
|
||||
LiSendTouchEvent(LI_TOUCH_EVENT_UP, pointerId, normalizedX, normalizedY, 0, 0, 0, 0); //event must sent from the lowest level directy by LiSendTouchEvent to simulate continous dragging to another point on screen
|
||||
LiSendTouchEvent(LI_TOUCH_EVENT_DOWN, pointerId, 0.3, 0.4, 0, 0, 0, 0);
|
||||
}else LiSendTouchEvent(touchType, pointerId, normalizedX, normalizedY,(touch.force / touch.maximumPossibleForce) / sin(touch.altitudeAngle),0.0f, 0.0f,[self getRotationFromAzimuthAngle:[touch azimuthAngleInView:streamView]]);
|
||||
}
|
||||
|
||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
if (asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchDownQos, 0), ^{
|
||||
for (UITouch* touch in touches){
|
||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
|
||||
[self handleTouchDown:touch]; //generate & populate pointerId
|
||||
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN];
|
||||
}
|
||||
});
|
||||
else{
|
||||
for (UITouch* touch in touches){
|
||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
|
||||
[self handleTouchDown:touch]; //generate & populate pointerId
|
||||
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
if (asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchMoveQos, 0), ^{
|
||||
for (UITouch* touch in touches){
|
||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
|
||||
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
|
||||
[[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
|
||||
usleep(self->touchMoveEventIntervalUs);
|
||||
}
|
||||
});
|
||||
else {
|
||||
for (UITouch* touch in touches){
|
||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
|
||||
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
|
||||
[[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
|
||||
// usleep(self->touchMoveEventIntervalUs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
if(asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchEndQos, 0), ^{
|
||||
for (UITouch* touch in touches){
|
||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId
|
||||
[self removePointerId:touch]; //then remove pointerId
|
||||
if(self->activateCoordSelector) [self removePointerObjFromDict:touch];
|
||||
}
|
||||
if(self->touchPointSpawnedAtUpperScreenEdge && [[event allTouches] count] == [touches count]) self->touchPointSpawnedAtUpperScreenEdge = false;
|
||||
});
|
||||
else{
|
||||
for (UITouch* touch in touches){
|
||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) continue;
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId
|
||||
[self removePointerId:touch]; //then remove pointerId
|
||||
if(self->activateCoordSelector) [self removePointerObjFromDict:touch];
|
||||
}
|
||||
if(self->touchPointSpawnedAtUpperScreenEdge && [[event allTouches] count] == [touches count]) self->touchPointSpawnedAtUpperScreenEdge = false;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
[self touchesEnded:touches withEvent:event];
|
||||
}
|
||||
|
||||
|
||||
- (void)populatePointerObjIntoDict:(UITouch*)touch{
|
||||
NativeTouchPointer* pointer = [[NativeTouchPointer alloc] initWithTouch:touch];
|
||||
pointer.useRelativeCoords = pointer.initialPoint.x > pointerVelocityDividerLocationByPoints;
|
||||
[pointerObjDict setObject:pointer forKey:@((uintptr_t)touch)];
|
||||
}
|
||||
|
||||
- (NativeTouchPointer* )getPointerObjFromDict:(UITouch*)touch{
|
||||
return [pointerObjDict objectForKey:@((uintptr_t)touch)];
|
||||
}
|
||||
|
||||
- (void)removePointerObjFromDict:(UITouch*)touch{
|
||||
uintptr_t memAddrValue = (uintptr_t)touch;
|
||||
NativeTouchPointer* pointer = [pointerObjDict objectForKey:@(memAddrValue)];
|
||||
if(pointer != nil){
|
||||
[pointerObjDict removeObjectForKey:@(memAddrValue)];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updatePointerObjInDict:(UITouch *)touch{
|
||||
[[pointerObjDict objectForKey:@((uintptr_t)touch)] updatePointerCoords:touch];
|
||||
}
|
||||
|
||||
- (CGPoint)selectCoordsFor:(UITouch *)touch{
|
||||
NativeTouchPointer *pointer = [pointerObjDict objectForKey:@((uintptr_t)touch)];
|
||||
if(pointer == nil) return CGPointMake(0, 0);
|
||||
// NSLog(@"using relative Coords: intialX %f, divider loc %f", pointer.initialPoint.x, pointerVelocityDividerLocationByPoints);
|
||||
return pointer.useRelativeCoords ? pointer.latestRelativePoint : pointer.latestPoint;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,37 +0,0 @@
|
||||
//
|
||||
// NativeTouchPointer.h
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2024/5/14.
|
||||
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import "StreamView.h"
|
||||
#import "NativeTouchHandler.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface NativeTouchPointer : NSObject
|
||||
@property (nonatomic, assign) bool needResetCoords;
|
||||
@property (nonatomic, assign) CGPoint initialPoint;
|
||||
@property (nonatomic, assign) CGPoint latestPoint;
|
||||
@property (nonatomic, assign) CGPoint previousPoint;
|
||||
@property (nonatomic, assign) CGPoint latestRelativePoint;
|
||||
@property (nonatomic, assign) CGPoint previousRelativePoint;
|
||||
@property (nonatomic, assign) bool useRelativeCoords;
|
||||
|
||||
|
||||
+ (void)initContextWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
|
||||
|
||||
- (bool)doesNeedResetCoords;
|
||||
- (void)updatePointerCoords:(UITouch *)touch;
|
||||
|
||||
|
||||
- (instancetype)initWithTouch:(UITouch *)touch;
|
||||
@end
|
||||
|
||||
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
//
|
||||
// NativeTouchPointer.m
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2024/5/14.
|
||||
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "NativeTouchPointer.h"
|
||||
#include <Limelight.h>
|
||||
|
||||
// native touch pointer ojbect that stores & manipulates touch coordinates
|
||||
|
||||
static CGFloat pointerVelocityFactor;
|
||||
static CGFloat streamViewHeight;
|
||||
static CGFloat streamViewWidth;
|
||||
static CGFloat fixedResetCoordX;
|
||||
static CGFloat fixedResetCoordY;
|
||||
static StreamView* streamView;
|
||||
|
||||
@implementation NativeTouchPointer{
|
||||
}
|
||||
|
||||
- (instancetype)initWithTouch:(UITouch *)touch{
|
||||
self = [super init];
|
||||
_initialPoint = [touch locationInView:streamView];
|
||||
_latestPoint = _initialPoint;
|
||||
_latestRelativePoint = _initialPoint;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (bool)doesNeedResetCoords{
|
||||
bool boundaryReached = (_latestRelativePoint.x > streamViewWidth || _latestRelativePoint.x < 0.0f || _latestRelativePoint.y > streamViewHeight || _latestRelativePoint.y < 0.0f);
|
||||
bool withinExcludedArea = (_initialPoint.x > streamViewWidth * (0.75) && _initialPoint.x < streamViewWidth) && (_initialPoint.y > streamViewHeight * (0.8) && _initialPoint.y < streamViewHeight);
|
||||
_needResetCoords = (pointerVelocityFactor > 1.0f) && _useRelativeCoords && boundaryReached && !withinExcludedArea;
|
||||
// boundary detection & coordinates reset to the specific point for HK:StarTrail(needs a very high pointer velocity)
|
||||
// must exclude touch pointer that uses native coords instead of relative ones.
|
||||
// also exclude touch pointer created within the bottom right corner
|
||||
|
||||
return _needResetCoords;
|
||||
}
|
||||
|
||||
- (void)updatePointerCoords:(UITouch *)touch{
|
||||
_previousPoint = _latestPoint;
|
||||
_latestPoint = [touch locationInView:streamView];
|
||||
_previousRelativePoint = _latestRelativePoint;
|
||||
if(_needResetCoords){// boundary detection & coordinates reset to the central screen point for HK:StarTrail(needs a very high pointer velocity); // boundary detection & coordinates reset to specific point for HK:StarTrail(needs a very high pointer velocity)
|
||||
_previousRelativePoint.x = fixedResetCoordX;
|
||||
_previousRelativePoint.y = fixedResetCoordY;
|
||||
}
|
||||
_latestRelativePoint.x = _previousRelativePoint.x + pointerVelocityFactor * (_latestPoint.x - _previousPoint.x);
|
||||
_latestRelativePoint.y = _previousRelativePoint.y + pointerVelocityFactor * (_latestPoint.y - _previousPoint.y);
|
||||
}
|
||||
|
||||
+ (void)initContextWithView:(StreamView*)view andSettings:(TemporarySettings*)settings {
|
||||
streamView = view;
|
||||
streamViewWidth = streamView.frame.size.width;
|
||||
fixedResetCoordX = streamViewWidth * 0.3;
|
||||
streamViewHeight = streamView.frame.size.height;
|
||||
fixedResetCoordY = streamViewHeight * 0.4;
|
||||
pointerVelocityFactor = settings.touchPointerVelocityFactor.floatValue;
|
||||
NSLog(@"stream wdith %f, stream height %f", streamViewWidth, streamViewHeight);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,143 +0,0 @@
|
||||
//
|
||||
// OnScreenControls.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 12/28/14.
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
// Modified by True砖家 since 2024.7.7
|
||||
// Copyright © True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "ControllerSupport.h"
|
||||
#import "CustomTapGestureRecognizer.h"
|
||||
#import "OSCProfile.h"
|
||||
#define MIN_CONTROLLER_LAYER_ALPHA 0.23
|
||||
|
||||
@class ControllerSupport;
|
||||
@class StreamConfiguration;
|
||||
|
||||
// sending self as an instance to OnScreenWidgetView or other related classes
|
||||
@protocol InstanceProviderDelegate <NSObject>
|
||||
- (void)getOnScreenControlsInstance:(id)sender; // Renamed delegate method
|
||||
@end
|
||||
|
||||
|
||||
static const float D_PAD_DIST = 10;
|
||||
static const float BUTTON_DIST = 20;
|
||||
|
||||
@interface OnScreenControls : NSObject{
|
||||
@protected
|
||||
CGFloat _leftStickSizeFactor;
|
||||
CGFloat _leftStickOpacity;
|
||||
CGFloat _rightStickSizeFactor;
|
||||
CGFloat _rightStickOpacity;
|
||||
CGFloat _dPadSizeFactor;
|
||||
}
|
||||
|
||||
+ (NSMutableDictionary *)layerVibrationStyleDic;
|
||||
|
||||
typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
|
||||
OnScreenControlsLevelOff,
|
||||
OnScreenControlsLevelSimple,
|
||||
OnScreenControlsLevelFull,
|
||||
OnScreenControlsLevelCustom,
|
||||
OnScreenControlsLevelAuto, // move it here instead of delete , to ensure integrity of the codes
|
||||
|
||||
// Internal levels selected by ControllerSupport
|
||||
OnScreenControlsLevelAutoGCGamepad,
|
||||
OnScreenControlsLevelAutoGCExtendedGamepad,
|
||||
OnScreenControlsLevelAutoGCExtendedGamepadWithStickButtons
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
@property (nonatomic, weak) id<InstanceProviderDelegate> delegate; // Delegate property
|
||||
- (void)sendInstance;
|
||||
|
||||
// @property (nonatomic, assign) CustomTapGestureRecognizer* mouseRightClickTapRecognizer; // this object will be passed to onscreencontrols class for areVirtualControllerTaps flag setting
|
||||
@property (nonatomic, assign) bool isLayingOut;
|
||||
//@property (nonatomic) NSMutableSet<UITouch* >* touchesCapturedByOnScreenButtons;
|
||||
|
||||
|
||||
@property (nonatomic, assign) CGRect standardRoundButtonBounds;
|
||||
@property (nonatomic, assign) CGRect standardRectangleButtonBounds;
|
||||
@property (nonatomic, assign) CGRect standardStickBounds;
|
||||
@property (nonatomic, assign) CGRect standardStickBackgroundBounds;
|
||||
@property (nonatomic, assign) CGRect standardUpDownButtonBounds;
|
||||
@property (nonatomic, assign) CGRect standardLeftRightButtonBounds;
|
||||
|
||||
@property (nonatomic, assign) CGPoint leftStickCenter;
|
||||
@property (nonatomic, assign) CGPoint rightStickCenter;
|
||||
|
||||
@property CALayer* _aButton;
|
||||
@property (nonatomic, assign) CGFloat aButtonSizeFactor;
|
||||
@property CALayer* _bButton;
|
||||
@property (nonatomic, assign) CGFloat bButtonSizeFactor;
|
||||
@property CALayer* _xButton;
|
||||
@property (nonatomic, assign) CGFloat xButtonSizeFactor;
|
||||
@property CALayer* _yButton;
|
||||
@property (nonatomic, assign) CGFloat yButtonSizeFactor;
|
||||
|
||||
|
||||
@property CALayer* _startButton;
|
||||
@property CALayer* _selectButton;
|
||||
@property CALayer* _r1Button;
|
||||
@property CALayer* _r2Button;
|
||||
@property CALayer* _r3Button;
|
||||
@property CALayer* _l1Button;
|
||||
@property CALayer* _l2Button;
|
||||
@property CALayer* _l3Button;
|
||||
@property CALayer* _upButton;
|
||||
@property CALayer* _downButton;
|
||||
@property CALayer* _leftButton;
|
||||
@property CALayer* _rightButton;
|
||||
@property CALayer* _leftStickBackground;
|
||||
@property CALayer* _leftStick;
|
||||
@property CALayer* _rightStickBackground;
|
||||
@property CALayer* _rightStick;
|
||||
@property CALayer* _dPadBackground; // parent layer that contains each individual dPad button so user can drag them around the screen together
|
||||
|
||||
@property float D_PAD_CENTER_X;
|
||||
@property float D_PAD_CENTER_Y;
|
||||
|
||||
@property OnScreenControlsLevel _level;
|
||||
|
||||
@property NSMutableArray *OSCButtonLayers;
|
||||
@property NSMutableDictionary* layerVibrationStyleDic;
|
||||
|
||||
|
||||
|
||||
+ (NSMutableSet* )touchAddrsCapturedByOnScreenControls;
|
||||
+ (void) testMethod;
|
||||
- (void) sendRightStickTouchPadEvent:(CGFloat) stickX : (CGFloat) stickY;
|
||||
- (void) clearRightStickTouchPadFlag;
|
||||
- (void) sendLeftStickTouchPadEvent:(CGFloat) stickX : (CGFloat) stickY;
|
||||
- (void) clearLeftStickTouchPadFlag;
|
||||
- (void) pressDownControllerButton: (int)flag;
|
||||
- (void) releaseControllerButton: (int)flag;
|
||||
- (void) updateLeftTrigger:(unsigned char)input;
|
||||
- (void) updateRightTrigger:(unsigned char)input;
|
||||
|
||||
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport streamConfig:(StreamConfiguration*)streamConfig;
|
||||
- (BOOL) handleTouchDownEvent:(NSSet*)touches;
|
||||
- (BOOL) handleTouchUpEvent:(NSSet*)touches;
|
||||
- (BOOL) handleTouchMovedEvent:(NSSet*)touches;
|
||||
- (void) setLevel:(OnScreenControlsLevel)level;
|
||||
- (void) show;
|
||||
- (void) setupComplexControls;
|
||||
- (void) drawButtons;
|
||||
- (void) drawBumpers;
|
||||
- (void) updateControls;
|
||||
- (OnScreenControlsLevel) getLevel;
|
||||
- (void) setDPadCenter;
|
||||
- (void) setAnalogStickPositions;
|
||||
- (void) positionAndResizeSingleControllerLayers;
|
||||
- (void) resizeControllerLayerWith:(CALayer*)layer and:(CGFloat)sizeFactor;
|
||||
- (void) adjustControllerLayerOpacityWith:(CALayer*)layer and:(CGFloat)alpha;
|
||||
+ (CGFloat) getControllerLayerSizeFactor:(CALayer*)layer;
|
||||
- (CGFloat) getControllerLayerOpacity:(CALayer*)layer;
|
||||
|
||||
@end
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,22 +0,0 @@
|
||||
//
|
||||
// NativeTouchHandler.h
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2024/6/1.
|
||||
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import "StreamView.h"
|
||||
#import "DataManager.h"
|
||||
#include <Limelight.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface PureNativeTouchHandler : UIResponder
|
||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,302 +0,0 @@
|
||||
//
|
||||
// NativeTouchHandler.m
|
||||
// VoidLink
|
||||
//
|
||||
// Created by True砖家 on 2024/6/1.
|
||||
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "PureNativeTouchHandler.h"
|
||||
#import "NativeTouchPointer.h"
|
||||
#import "OnScreenControls.h"
|
||||
#import "StreamView.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
@implementation PureNativeTouchHandler {
|
||||
StreamView* streamView;
|
||||
TemporarySettings* currentSettings;
|
||||
bool activateCoordSelector;
|
||||
bool touchPointSpawnedAtUpperScreenEdge;
|
||||
CGFloat pointerVelocityDividerLocationByPoints;
|
||||
uint16_t touchMoveEventIntervalUs;
|
||||
|
||||
bool asyncNativeTouch;
|
||||
unsigned int touchDownQos;
|
||||
unsigned int touchMoveQos;
|
||||
unsigned int touchEndQos;
|
||||
|
||||
// Use a Dictionary to store UITouch object's memory address as key, and pointerId as value,字典存放UITouch对象地址和pointerId映射关系
|
||||
// pointerId will be generated from a pre-defined pool
|
||||
// Use a NSSet store active pointerId,
|
||||
NSMutableDictionary *pointerIdDict; //pointerId Dict for active touches.
|
||||
NSMutableSet<NSNumber *> *activePointerIds; //pointerId Set for active touches.
|
||||
NSMutableSet<NSNumber *> *pointerIdPool; //pre-defined pool of pointerIds.
|
||||
NSMutableSet<NSNumber *> *unassignedPointerIds;
|
||||
|
||||
NSMutableDictionary *pointerObjDict;
|
||||
|
||||
CGFloat slideGestureVerticalThreshold;
|
||||
CGFloat screenWidthWithThreshold;
|
||||
CGFloat EDGE_TOLERANCE;
|
||||
|
||||
CGRect streamViewBounds;
|
||||
}
|
||||
|
||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings{
|
||||
self = [super init];
|
||||
self->streamView = view;
|
||||
self->currentSettings = settings;
|
||||
self->activateCoordSelector = currentSettings.pointerVelocityModeDivider.floatValue != 1.0;
|
||||
self->touchMoveEventIntervalUs = currentSettings.touchMoveEventInterval.intValue;
|
||||
self->streamViewBounds = view.bounds;
|
||||
|
||||
self->pointerIdDict = [NSMutableDictionary dictionary];
|
||||
self->pointerIdPool = [NSMutableSet set];
|
||||
for (uint8_t i = 0; i <= 10; i++) { //ipadOS supports upto 11 finger touches
|
||||
[self->pointerIdPool addObject:@(i)];
|
||||
}
|
||||
self->activePointerIds = [NSMutableSet set];
|
||||
// self->excludedPointerIds = [[NSMutableSet alloc] init];
|
||||
self->touchPointSpawnedAtUpperScreenEdge = false;
|
||||
|
||||
self->asyncNativeTouch = settings.asyncNativeTouchPriority.intValue != AsyncNativeTouchOff;
|
||||
|
||||
switch(settings.asyncNativeTouchPriority.intValue){
|
||||
case TouchDownPriority: // deprecated by GUI
|
||||
touchDownQos = QOS_CLASS_USER_INTERACTIVE;
|
||||
touchMoveQos = QOS_CLASS_USER_INITIATED;
|
||||
touchEndQos = QOS_CLASS_USER_INITIATED;
|
||||
break;
|
||||
case TouchMovePriority: // deprecated by GUI
|
||||
touchDownQos = QOS_CLASS_USER_INITIATED;
|
||||
touchMoveQos = QOS_CLASS_USER_INTERACTIVE;
|
||||
touchEndQos = QOS_CLASS_USER_INITIATED;
|
||||
break;
|
||||
case EqualPriority: // equals to async touch is true now
|
||||
touchDownQos = QOS_CLASS_USER_INTERACTIVE;
|
||||
touchMoveQos = QOS_CLASS_USER_INTERACTIVE;
|
||||
touchEndQos = QOS_CLASS_USER_INITIATED;
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
|
||||
self->pointerObjDict = [NSMutableDictionary dictionary];
|
||||
|
||||
EDGE_TOLERANCE = 15.0;
|
||||
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
||||
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - EDGE_TOLERANCE;
|
||||
|
||||
self->pointerVelocityDividerLocationByPoints = self->streamView.bounds.size.width * settings.pointerVelocityModeDivider.floatValue;
|
||||
|
||||
[NativeTouchPointer initContextWithView:self->streamView andSettings:settings];
|
||||
//_touchesCapturedByOnScreenButtons = [[NSMutableSet alloc] init];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (CGSize) getVideoAreaSize {
|
||||
if (self->streamViewBounds.size.width > self->streamViewBounds.size.height * streamView.streamAspectRatio) {
|
||||
return CGSizeMake(self->streamViewBounds.size.height * streamView.streamAspectRatio, self->streamViewBounds.size.height);
|
||||
} else {
|
||||
return CGSizeMake(self->streamViewBounds.size.width, self->streamViewBounds.size.width / streamView.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->streamViewBounds.origin.x;
|
||||
float y = point.y - self->streamViewBounds.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->streamViewBounds.size.width / 2) {
|
||||
x--;
|
||||
}
|
||||
else {
|
||||
x++;
|
||||
}
|
||||
if (y < self->streamViewBounds.size.height / 2) {
|
||||
y--;
|
||||
}
|
||||
else {
|
||||
y++;
|
||||
}
|
||||
|
||||
// This logic mimics what iOS does with AVLayerVideoGravityResizeAspect
|
||||
CGSize videoSize = [self getVideoAreaSize];
|
||||
CGPoint videoOrigin = CGPointMake(self->streamViewBounds.size.width / 2 - videoSize.width / 2,
|
||||
self->streamViewBounds.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);
|
||||
}
|
||||
|
||||
|
||||
- (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;
|
||||
}
|
||||
|
||||
// generate & populate pointerId into NSDict & NSSet, called in touchesBegan
|
||||
- (void)handleTouchDown:(UITouch*)touch{
|
||||
//populate pointerId
|
||||
uintptr_t memAddrValue = (uintptr_t)touch;
|
||||
unassignedPointerIds = [pointerIdPool mutableCopy]; //reset unassignedPointerIds
|
||||
[unassignedPointerIds minusSet:activePointerIds];
|
||||
uint8_t pointerId = [[unassignedPointerIds anyObject] unsignedIntValue];
|
||||
[pointerIdDict setObject:@(pointerId) forKey:@(memAddrValue)];
|
||||
[activePointerIds addObject:@(pointerId)];
|
||||
|
||||
//check if touch point is spawned on the left or right upper half screen edges, event to remote PC. this is for better handling in-stream slide gesture
|
||||
CGPoint initialPoint = [touch locationInView:self->streamView];
|
||||
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < EDGE_TOLERANCE || initialPoint.x > screenWidthWithThreshold)) {
|
||||
self->touchPointSpawnedAtUpperScreenEdge = true;
|
||||
// [excludedPointerIds addObject:@(pointerId)];
|
||||
}
|
||||
}
|
||||
|
||||
// remove pointerId in touchesEnded or touchesCancelled
|
||||
- (void)removePointerId:(UITouch*)touch{
|
||||
uintptr_t memAddrValue = (uintptr_t)touch;
|
||||
NSNumber* pointerIdObj = [pointerIdDict objectForKey:@(memAddrValue)];
|
||||
if(pointerIdObj != nil){
|
||||
[activePointerIds removeObject:pointerIdObj];
|
||||
[pointerIdDict removeObjectForKey:@(memAddrValue)];
|
||||
// if([excludedPointerIds containsObject:pointerIdObj]) [excludedPointerIds removeObject:pointerIdObj]; // remove pointer id from excludedPointerId NSSet
|
||||
}
|
||||
}
|
||||
|
||||
// 从字典中获取UITouch事件对应的pointerId
|
||||
// called in method of sendTouchEvent
|
||||
- (uint8_t) retrievePointerIdFromDict:(UITouch*)touch{
|
||||
return [[pointerIdDict objectForKey:@((uintptr_t)touch)] unsignedIntValue];
|
||||
}
|
||||
|
||||
|
||||
- (void)sendTouchEvent:(UITouch*)touch withTouchtype:(uint8_t)touchType{
|
||||
if(touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC. and this must be checked after coord selector finishes populating new relative coords, or the app will crash!
|
||||
|
||||
CGPoint targetCoords;
|
||||
//NSLog(@"selecting coords: %d", touch.phase == UITouchPhaseMoved);
|
||||
// NSLog(@"excluded count: %d", (uint32_t)[excludedPointerIds count]);
|
||||
|
||||
targetCoords = activateCoordSelector ? [self selectCoordsFor:touch] : [touch locationInView:streamView];
|
||||
|
||||
CGPoint location = [self adjustCoordinatesForVideoArea:targetCoords];
|
||||
CGSize videoSize = [self getVideoAreaSize];
|
||||
CGFloat normalizedX = location.x / videoSize.width;
|
||||
CGFloat normalizedY = location.y / videoSize.height;
|
||||
uint8_t pointerId = [self retrievePointerIdFromDict:touch];
|
||||
|
||||
if([self getPointerObjFromDict:touch].needResetCoords){ // access whether the current pointer has reached the boundary, and need a coord reset.
|
||||
LiSendTouchEvent(LI_TOUCH_EVENT_UP, pointerId, normalizedX, normalizedY, 0, 0, 0, 0); //event must sent from the lowest level directy by LiSendTouchEvent to simulate continous dragging to another point on screen
|
||||
LiSendTouchEvent(LI_TOUCH_EVENT_DOWN, pointerId, 0.3, 0.4, 0, 0, 0, 0);
|
||||
}else LiSendTouchEvent(touchType, pointerId, normalizedX, normalizedY,(touch.force / touch.maximumPossibleForce) / sin(touch.altitudeAngle),0.0f, 0.0f,[self getRotationFromAzimuthAngle:[touch azimuthAngleInView:streamView]]);
|
||||
}
|
||||
|
||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
if (asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchDownQos, 0), ^{
|
||||
for (UITouch* touch in touches){
|
||||
[self handleTouchDown:touch]; //generate & populate pointerId
|
||||
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN];
|
||||
}
|
||||
});
|
||||
else{
|
||||
for (UITouch* touch in touches){
|
||||
[self handleTouchDown:touch]; //generate & populate pointerId
|
||||
if(self->activateCoordSelector) [self populatePointerObjIntoDict:touch];
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_DOWN];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
if (asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchMoveQos, 0), ^{
|
||||
for (UITouch* touch in touches){
|
||||
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
|
||||
[[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
|
||||
usleep(self->touchMoveEventIntervalUs);
|
||||
}
|
||||
});
|
||||
else {
|
||||
for (UITouch* touch in touches){
|
||||
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
|
||||
[[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
|
||||
// usleep(self->touchMoveEventIntervalUs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
if(asyncNativeTouch) dispatch_async(dispatch_get_global_queue(touchEndQos, 0), ^{
|
||||
for (UITouch* touch in touches){
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId
|
||||
[self removePointerId:touch]; //then remove pointerId
|
||||
if(self->activateCoordSelector) [self removePointerObjFromDict:touch];
|
||||
}
|
||||
if(self->touchPointSpawnedAtUpperScreenEdge && [[event allTouches] count] == [touches count]) self->touchPointSpawnedAtUpperScreenEdge = false;
|
||||
});
|
||||
else{
|
||||
for (UITouch* touch in touches){
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_UP]; //send touch event before remove pointerId
|
||||
[self removePointerId:touch]; //then remove pointerId
|
||||
if(self->activateCoordSelector) [self removePointerObjFromDict:touch];
|
||||
}
|
||||
if(self->touchPointSpawnedAtUpperScreenEdge && [[event allTouches] count] == [touches count]) self->touchPointSpawnedAtUpperScreenEdge = false;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
[self touchesEnded:touches withEvent:event];
|
||||
}
|
||||
|
||||
|
||||
- (void)populatePointerObjIntoDict:(UITouch*)touch{
|
||||
NativeTouchPointer* pointer = [[NativeTouchPointer alloc] initWithTouch:touch];
|
||||
pointer.useRelativeCoords = pointer.initialPoint.x > pointerVelocityDividerLocationByPoints;
|
||||
[pointerObjDict setObject:pointer forKey:@((uintptr_t)touch)];
|
||||
}
|
||||
|
||||
- (NativeTouchPointer* )getPointerObjFromDict:(UITouch*)touch{
|
||||
return [pointerObjDict objectForKey:@((uintptr_t)touch)];
|
||||
}
|
||||
|
||||
- (void)removePointerObjFromDict:(UITouch*)touch{
|
||||
uintptr_t memAddrValue = (uintptr_t)touch;
|
||||
NativeTouchPointer* pointer = [pointerObjDict objectForKey:@(memAddrValue)];
|
||||
if(pointer != nil){
|
||||
[pointerObjDict removeObjectForKey:@(memAddrValue)];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updatePointerObjInDict:(UITouch *)touch{
|
||||
[[pointerObjDict objectForKey:@((uintptr_t)touch)] updatePointerCoords:touch];
|
||||
}
|
||||
|
||||
- (CGPoint)selectCoordsFor:(UITouch *)touch{
|
||||
NativeTouchPointer *pointer = [pointerObjDict objectForKey:@((uintptr_t)touch)];
|
||||
if(pointer == nil) return CGPointMake(0, 0);
|
||||
// NSLog(@"using relative Coords: intialX %f, divider loc %f", pointer.initialPoint.x, pointerVelocityDividerLocationByPoints);
|
||||
return pointer.useRelativeCoords ? pointer.latestRelativePoint : pointer.latestPoint;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,30 +0,0 @@
|
||||
//
|
||||
// RelativeTouchHandler.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 11/1/20.
|
||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved
|
||||
//
|
||||
// Modified by True砖家 since 2024.6.23
|
||||
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved
|
||||
//
|
||||
|
||||
#import "StreamView.h"
|
||||
#import "DataManager.h"
|
||||
#import "CustomTapGestureRecognizer.h"
|
||||
|
||||
#define RIGHTCLICK_TAP_DOWN_TIME_THRESHOLD_S 0.15
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface RelativeTouchHandler : UIResponder
|
||||
@property (nonatomic, readonly) CustomTapGestureRecognizer* mouseRightClickTapRecognizer; // this object will be passed to onscreencontrol class for areVirtualControllerTaps flag setting
|
||||
@property (nonatomic, strong) CADisplayLink *displayLink;
|
||||
|
||||
|
||||
+ (void) testMethod;
|
||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,316 +0,0 @@
|
||||
//
|
||||
// RelativeTouchHandler.m
|
||||
// Moonlight
|
||||
//
|
||||
// Completely refactored by True砖家 on 2024.9.13
|
||||
// Copyright © 2024 True砖家 on Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RelativeTouchHandler.h"
|
||||
#import "DataManager.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
|
||||
|
||||
static const int REFERENCE_WIDTH = 1280;
|
||||
static const int REFERENCE_HEIGHT = 720;
|
||||
static const float QUICK_TAP_TIME_INTERVAL = 0.2;
|
||||
|
||||
@implementation RelativeTouchHandler {
|
||||
TemporarySettings* currentSettings;
|
||||
CGPoint latestMousePointerLocation, initialMousePointerLocation;
|
||||
CGPoint twoFingerTouchLocation;
|
||||
NSTimeInterval mousePointerTimestamp;
|
||||
CGRect streamViewBounds;
|
||||
BOOL mousePointerMoved;
|
||||
BOOL quickTapDetected;
|
||||
BOOL isInMouseWheelScrollingMode;
|
||||
|
||||
// upper screen edge check
|
||||
bool touchPointSpawnedAtUpperScreenEdge;
|
||||
CGFloat slideGestureVerticalThreshold;
|
||||
CGFloat screenWidthWithThreshold;
|
||||
CGFloat EDGE_TOLERANCE;
|
||||
|
||||
UITouch* touchLockedForMouseMove;
|
||||
|
||||
#if TARGET_OS_TV
|
||||
UIGestureRecognizer* remotePressRecognizer;
|
||||
UIGestureRecognizer* remoteLongPressRecognizer;
|
||||
#endif
|
||||
|
||||
StreamView* streamView;
|
||||
}
|
||||
|
||||
- (id)initWithView:(StreamView*)view andSettings:(TemporarySettings*)settings {
|
||||
self = [self init];
|
||||
self->streamView = view;
|
||||
self->streamViewBounds = view.bounds;
|
||||
self->currentSettings = settings;
|
||||
// replace righclick recoginizing with my CustomTapGestureRecognizer for better experience, higher recoginizing rate.
|
||||
_mouseRightClickTapRecognizer = [[CustomTapGestureRecognizer alloc] initWithTarget:self action:@selector(mouseRightClick)];
|
||||
_mouseRightClickTapRecognizer.numberOfTouchesRequired = 2;
|
||||
_mouseRightClickTapRecognizer.tapDownTimeThreshold = RIGHTCLICK_TAP_DOWN_TIME_THRESHOLD_S; // tap down time in seconds.
|
||||
_mouseRightClickTapRecognizer.delaysTouchesBegan = NO;
|
||||
_mouseRightClickTapRecognizer.delaysTouchesEnded = NO;
|
||||
[self->streamView.streamFrameTopLayerView addGestureRecognizer:_mouseRightClickTapRecognizer]; // add all additional gestures to the streamFrameTopLayerView instead of the streamview.
|
||||
|
||||
isInMouseWheelScrollingMode = false;
|
||||
mousePointerMoved = false;
|
||||
mousePointerTimestamp = 0;
|
||||
|
||||
// upper screen check
|
||||
EDGE_TOLERANCE = 15.0;
|
||||
slideGestureVerticalThreshold = CGRectGetHeight([[UIScreen mainScreen] bounds]) * 0.4;
|
||||
screenWidthWithThreshold = CGRectGetWidth([[UIScreen mainScreen] bounds]) - EDGE_TOLERANCE;
|
||||
self->touchPointSpawnedAtUpperScreenEdge = false;
|
||||
|
||||
#if TARGET_OS_TV
|
||||
remotePressRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonPressed:)];
|
||||
remotePressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)];
|
||||
|
||||
remoteLongPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonLongPressed:)];
|
||||
remoteLongPressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)];
|
||||
|
||||
[self->view addGestureRecognizer:remotePressRecognizer];
|
||||
[self->view addGestureRecognizer:remoteLongPressRecognizer];
|
||||
#endif
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (bool)isOnScreenControllerBeingPressed:(NSSet* )touches{
|
||||
for(UITouch* touch in touches){
|
||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
- (bool)isOnScreenWidgetViewBeingPressed {
|
||||
bool gotOneButtonPressed = false;
|
||||
for(UIView* view in self->streamView.superview.subviews){ // iterates all on-screen widget views in StreamFrameView
|
||||
if ([view isKindOfClass:[OnScreenWidgetView class]]) {
|
||||
OnScreenWidgetView* widgetView = (OnScreenWidgetView*) view;
|
||||
if(widgetView.pressed){
|
||||
gotOneButtonPressed = true; //got one button pressed
|
||||
}
|
||||
}
|
||||
}
|
||||
return gotOneButtonPressed;
|
||||
}
|
||||
|
||||
- (void)resetAllPressedFlagsForOnScreenWidgetViews {
|
||||
for(UIView* view in self->streamView.superview.subviews){ // iterates all on-screen widget views in StreamFrameView
|
||||
if ([view isKindOfClass:[OnScreenWidgetView class]]) {
|
||||
OnScreenWidgetView* widgetView = (OnScreenWidgetView*) view;
|
||||
widgetView.pressed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)mouseRightClick {
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
Log(LOG_D, @"Sending right mouse button press");
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||
// Wait 100 ms to simulate a real button press
|
||||
usleep(100 * 1000);
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
- (BOOL)isConfirmedMove:(CGPoint)currentPoint from:(CGPoint)originalPoint {
|
||||
// Movements of greater than 1 point are considered confirmed
|
||||
return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) >= 1;
|
||||
}*/
|
||||
|
||||
- (BOOL)isAdjacentTouches:(CGPoint)currentPoint from:(CGPoint)originalPoint {
|
||||
return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) <= 100;
|
||||
}
|
||||
|
||||
|
||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
|
||||
//check if touch point is spawned on the left or right upper half screen edges, this is the highest priority
|
||||
CGPoint initialPoint = [[touches anyObject] locationInView:streamView];
|
||||
if(initialPoint.y < slideGestureVerticalThreshold && (initialPoint.x < EDGE_TOLERANCE || initialPoint.x > screenWidthWithThreshold)) {
|
||||
self->touchPointSpawnedAtUpperScreenEdge = true;
|
||||
return;
|
||||
}
|
||||
|
||||
touchPointSpawnedAtUpperScreenEdge = false; // reset this flag immediately if we get a touch event passing the check above, this fixes irresponsive touch after closing the command tool menu.
|
||||
|
||||
if([[event allTouches] count] == 2 && ![self isOnScreenWidgetViewBeingPressed] && ![self isOnScreenControllerBeingPressed:[event allTouches]]){
|
||||
NSLog(@"get in scrolling mode");
|
||||
isInMouseWheelScrollingMode = true;
|
||||
return; // if we got 2 touches on the blank area, it's gonna be a mouse scroll touch, and must prevent UITtouch object for mouse pointer being captured & locked
|
||||
}
|
||||
|
||||
// NSLog(@"touches count in began stage: %llu", (uint64_t)[touches count]);
|
||||
|
||||
UITouch* candidateTouch = nil;
|
||||
|
||||
// the onscreen controllers are implmented by CALayer, which can not intercept UITouch event, touch will penetrate to the streamView level and captured in the touches callback of this touchHandler class.
|
||||
// the onscreen button are UIViews, they intercept UITouch events, so we don't need to worry about them.
|
||||
for(UITouch* touch in touches){
|
||||
// NSLog(@"candidate touch test: %llu", (uint64_t)touch);
|
||||
if([OnScreenControls.touchAddrsCapturedByOnScreenControls containsObject:@((uintptr_t)touch)]){
|
||||
// NSLog(@"%f controller tap detected", CACurrentMediaTime());
|
||||
continue;
|
||||
}
|
||||
else candidateTouch = touch;
|
||||
}
|
||||
|
||||
// quick double tap detection for dragging. simulates a real notebook computer touchpad
|
||||
CGPoint currentTouchLocation = [candidateTouch locationInView:streamView];
|
||||
NSTimeInterval tapInterval = CACurrentMediaTime() - mousePointerTimestamp;
|
||||
if(tapInterval < QUICK_TAP_TIME_INTERVAL && [self isAdjacentTouches:currentTouchLocation from:initialMousePointerLocation] ) {
|
||||
// NSLog(@"quick click detected");
|
||||
quickTapDetected = true;
|
||||
NSLog(@"quick Tap Detected");
|
||||
// LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT); // do not press down mouse button here, or it wiil easily turn to double click on remote PC
|
||||
}
|
||||
|
||||
// we must use [event allTouches] to check if touchLockedForMouseMove is captured, because the UITouch object could be captured by upper layer of UIView(in cases like tap gestures), not passed to the touches callbacks in this class, but still available in [event allTouches]
|
||||
if(candidateTouch != nil && ![[event allTouches] containsObject:touchLockedForMouseMove]){
|
||||
touchLockedForMouseMove = candidateTouch;
|
||||
NSLog(@"Candidate touch for mouse movement locked");
|
||||
mousePointerTimestamp = CACurrentMediaTime();
|
||||
initialMousePointerLocation = latestMousePointerLocation = [touchLockedForMouseMove locationInView:streamView];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
|
||||
//NSLog(@"%f, touchesMoved callback, is scrolling: %d, touches count: %d", CACurrentMediaTime(), isInMouseWheelScrollingMode, (uint32_t)[touches count]);
|
||||
|
||||
if(isInMouseWheelScrollingMode && [[event allTouches] count] == 2 && ![self isOnScreenWidgetViewBeingPressed] && ![self isOnScreenControllerBeingPressed:touches]){
|
||||
NSSet* twoTouches = [event allTouches];
|
||||
CGPoint firstLocation = [[[twoTouches allObjects] objectAtIndex:0] locationInView:streamView];
|
||||
CGPoint secondLocation = [[[twoTouches allObjects] objectAtIndex:1] locationInView:streamView];
|
||||
|
||||
CGPoint avgLocation = CGPointMake((firstLocation.x + secondLocation.x) / 2, (firstLocation.y + secondLocation.y) / 2);
|
||||
if ((CACurrentMediaTime() - _mouseRightClickTapRecognizer.gestureCapturedTime > RIGHTCLICK_TAP_DOWN_TIME_THRESHOLD_S) && twoFingerTouchLocation.y != avgLocation.y) { //prevent sending scrollevent while right click gesture is being recognized. The time threshold is only 150ms, resulting in a barely noticeable delay before the scroll event is activated.
|
||||
// and we must exclude onscreen button taps & on-screen controller taps
|
||||
LiSendHighResScrollEvent((avgLocation.y - twoFingerTouchLocation.y) * 10);
|
||||
}
|
||||
twoFingerTouchLocation = avgLocation;
|
||||
return;
|
||||
}
|
||||
|
||||
// NSLog(@"%f touchesMoved callback, locked touch: %llu", CACurrentMediaTime(), (uintptr_t)touchLockedForMouseMove);
|
||||
|
||||
if([touches containsObject:touchLockedForMouseMove]){
|
||||
mousePointerMoved = true;
|
||||
[self sendMouseMoveEvent:touchLockedForMouseMove];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
if(isInMouseWheelScrollingMode){
|
||||
isInMouseWheelScrollingMode = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if([touches containsObject:touchLockedForMouseMove]){
|
||||
// dealing with a single first tap, whether the button will be released, is going to be decided in sendLongMouseLeftButtonClickEvent
|
||||
if(!mousePointerMoved && !self->quickTapDetected) [self sendLongMouseLeftButtonClickEvent];
|
||||
|
||||
// dealing with a second quick tap following the first tap:
|
||||
if(self->quickTapDetected){
|
||||
// we're in at least the second tap release of the very short time interval after the first tap.
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT); // must release the button anyway, because the button is likely being held down since the long click turned into a dragging event.
|
||||
if(!mousePointerMoved) [self sendShortMouseLeftButtonClickEvent]; // if it is a quick tap and the pointer was not moved, we must send another click to simulate double click.
|
||||
self->quickTapDetected = false; // reset flag
|
||||
}
|
||||
touchLockedForMouseMove = nil;
|
||||
mousePointerMoved = false;
|
||||
}
|
||||
|
||||
if([[event allTouches] count] == [touches count]){
|
||||
isInMouseWheelScrollingMode = false;
|
||||
touchLockedForMouseMove = nil;
|
||||
mousePointerMoved = false; // need to reset this anyway
|
||||
[self resetAllPressedFlagsForOnScreenWidgetViews]; // reset all pressed flag for on-screen widget views after all fingers lifted from screen.
|
||||
touchPointSpawnedAtUpperScreenEdge = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)sendMouseMoveEvent:(UITouch* )touch{
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
if(self->touchPointSpawnedAtUpperScreenEdge) return; // we're done here. this touch event will not be sent to the remote PC.
|
||||
|
||||
CGPoint currentLocation = [touch locationInView:self->streamView];
|
||||
|
||||
if (self->latestMousePointerLocation.x != currentLocation.x ||
|
||||
self->latestMousePointerLocation.y != currentLocation.y)
|
||||
{
|
||||
int deltaX = (currentLocation.x - self->latestMousePointerLocation.x) * (REFERENCE_WIDTH / self->streamViewBounds.size.width) * self->currentSettings.mousePointerVelocityFactor.floatValue;
|
||||
int deltaY = (currentLocation.y - self->latestMousePointerLocation.y) * (REFERENCE_HEIGHT / self->streamViewBounds.size.height) * self->currentSettings.mousePointerVelocityFactor.floatValue;
|
||||
|
||||
if (deltaX != 0 || deltaY != 0) {
|
||||
LiSendMouseMoveEvent(deltaX, deltaY);
|
||||
self->latestMousePointerLocation = currentLocation;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// this will turn into a dragging anytime...
|
||||
- (void)sendLongMouseLeftButtonClickEvent{
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
// if (!self->isDragging){
|
||||
Log(LOG_D, @"Sending left mouse button press");
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
// Wait 100 ms to simulate a real button press
|
||||
usleep(QUICK_TAP_TIME_INTERVAL * 1000000);
|
||||
if(!self->quickTapDetected){
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
}
|
||||
else NSLog(@"Left mouse button release cancelled, keep pressing down, turning into dragging...");
|
||||
|
||||
// do not release the button if we're still dragging, this will prevent the dragging from being interrupted.
|
||||
});
|
||||
}
|
||||
|
||||
- (void)sendShortMouseLeftButtonClickEvent{
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
usleep(50 * 1000);
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
usleep(50 * 1000);
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
#if TARGET_OS_TV
|
||||
- (void)remoteButtonPressed:(id)sender {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
Log(LOG_D, @"Sending left mouse button press");
|
||||
|
||||
// Mark this as touchMoved to avoid a duplicate press on touch up
|
||||
self->touchMoved = true;
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
|
||||
// Wait 100 ms to simulate a real button press
|
||||
usleep(100 * 1000);
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
});
|
||||
}
|
||||
- (void)remoteButtonLongPressed:(id)sender {
|
||||
Log(LOG_D, @"Holding left mouse button");
|
||||
|
||||
isDragging = true;
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
}
|
||||
#endif
|
||||
|
||||
@end
|
||||
@@ -1,68 +0,0 @@
|
||||
//
|
||||
// StreamView.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 10/19/14.
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
// Modified by True砖家 since 2024.6.16
|
||||
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
|
||||
|
||||
|
||||
//
|
||||
|
||||
#import "ControllerSupport.h"
|
||||
#import "OnScreenControls.h"
|
||||
#import "Moonlight-Swift.h"
|
||||
#import "StreamConfiguration.h"
|
||||
|
||||
@protocol UserInteractionDelegate <NSObject>
|
||||
|
||||
- (void)userInteractionBegan;
|
||||
- (void)userInteractionEnded;
|
||||
- (void)streamExitRequested;
|
||||
- (void)toggleStatsOverlay;
|
||||
- (void)localMousePointerMode;
|
||||
- (void)toggleMouseVisible;
|
||||
|
||||
@end
|
||||
|
||||
#if TARGET_OS_TV
|
||||
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate>
|
||||
#else
|
||||
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate, UIPointerInteractionDelegate>
|
||||
#endif
|
||||
|
||||
@property (assign, nonatomic) UIView* streamFrameTopLayerView;
|
||||
@property (assign, nonatomic) CGFloat streamAspectRatio;
|
||||
|
||||
- (void) setupStreamView:(ControllerSupport*)controllerSupport
|
||||
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
|
||||
config:(StreamConfiguration*)streamConfig
|
||||
streamFrameTopLayerView:(UIView* )topLayerView
|
||||
;
|
||||
- (void) showOnScreenControls;
|
||||
- (void) setOnScreenControls;
|
||||
- (void) disableOnScreenControls;
|
||||
- (void) reloadOnScreenControlsRealtimeWith:(ControllerSupport*)controllerSupport
|
||||
andConfig:(StreamConfiguration*)streamConfig;
|
||||
- (void) reloadOnScreenControlsWith:(ControllerSupport*)controllerSupport
|
||||
andConfig:(StreamConfiguration*)streamConfig;
|
||||
- (void) clearOnScreenKeyboardButtons;
|
||||
- (void) reloadOnScreenWidgetViews;
|
||||
|
||||
- (CGSize) getVideoAreaSize;
|
||||
- (CGPoint) adjustCoordinatesForVideoArea:(CGPoint)point;
|
||||
- (uint16_t)getRotationFromAzimuthAngle:(float)azimuthAngle;
|
||||
|
||||
- (OnScreenControlsLevel) getCurrentOscState;
|
||||
|
||||
- (void)keyboardWillShow:(NSNotification *)notification;
|
||||
- (void)keyboardWillHide;
|
||||
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
- (void) updateCursorLocation:(CGPoint)location isMouse:(BOOL)isMouse;
|
||||
#endif
|
||||
|
||||
@end
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user